diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 09e02743b4..fda9515e47 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
jobs:
- build_and_deploy:
+ build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -44,13 +44,27 @@ jobs:
- name: "Sync Donor Info"
run: cd content/donate && ./sync_donors.sh
- - name: "Build and deploy website"
- if: github.repository_owner == 'bevyengine'
+ - name: "Build Website"
uses: shalzz/zola-deploy-action@v0.19.2
env:
- PAGES_BRANCH: gh-pages
+ BUILD_ONLY: true
BUILD_DIR: .
- TOKEN: ${{ secrets.CART_PAT }}
+
+ - name: "Search (1/3): Setup Node.js"
+ uses: actions/setup-node@v4
+
+ # See: https://github.com/CloudCannon/pagefind/issues/675#issuecomment-2267210534
+ - name: "Search (2/3): Change `public` Permissions"
+ run: echo "$(whoami):$(id -gn)" | xargs -I {} sudo chown -R {} public
+
+ - name: "Search (3/3): Build Index"
+ run: npx --yes pagefind --output-path public/pagefind
+
+ - name: Upload GitHub Pages Artifact
+ id: deployment
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: public/
# Caches output of generate-assets for use in ci.yml
- name: Update generate-assets cache
@@ -58,3 +72,19 @@ jobs:
with:
path: content/assets
key: assets-${{ steps.date.outputs.date }}-${{ hashFiles('generate-assets/**/*.rs', 'generate-assets/Cargo.toml', 'generate-assets/generate_assets.sh') }}
+
+ deploy:
+ if: github.repository_owner == 'bevyengine'
+ runs-on: ubuntu-latest
+ needs: build
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+ with:
+ token: ${{ secrets.CART_PAT }}
diff --git a/.gitignore b/.gitignore
index b428d603e0..f8d13c1f6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ content/community/people/
content/community/donate/
static/assets/examples/
static/processed_images/
+static/pagefind/
# Tools
target/
diff --git a/README.md b/README.md
index 1bcbd06620..472e7ced78 100644
--- a/README.md
+++ b/README.md
@@ -21,3 +21,29 @@ A local server should start and you should be able to access a local version of
### Assets, Errors, and Examples pages
These pages need to be generated in a separate step by running the shell scripts in the `generate-assets`, `generate-errors`, and `generate-wasm-examples` directories. On Windows, you can use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) or [git bash](https://gitforwindows.org/).
+
+## Search Index
+
+We use [Pagefind](https://pagefind.app) for the search functionality.
+
+### Configuration
+
+Which pages to index, category assignment and the weight is configured in `/templates/macros/pagefind.html`.
+Note that the category names must be reflected when instancing `SearchCategories` in `/static/search.mjs`.
+
+Pagefind can be tweaked (ignore content, change weights…) at page and element level by using `data-pagefind-*` HTML attributes.
+
+The default configuration for the Pagefind CLI is defined in `/pagefind.yml`.
+
+### Local Development
+
+To generate the index for local development, download the [Pagefind binary](https://github.com/CloudCannon/pagefind/releases) in the project root and the run:
+
+```sh
+rm -rf public
+zola build
+./pagefind --write-playground
+```
+
+This will create the `/static/pagefind` folder which contains the index and JS library.
+You can access `http://127.0.0.1:1111/pagefind/playground/` to debug search issues.
diff --git a/pagefind.yml b/pagefind.yml
new file mode 100644
index 0000000000..fb309bc7cb
--- /dev/null
+++ b/pagefind.yml
@@ -0,0 +1,6 @@
+site: public
+output_path: static/pagefind
+exclude_selectors:
+ - ".tree-menu"
+ - ".example__back"
+ - ".example__github"
diff --git a/sass/_mixins.scss b/sass/_mixins.scss
index 484bab4396..368fa4f16e 100644
--- a/sass/_mixins.scss
+++ b/sass/_mixins.scss
@@ -1,3 +1,19 @@
+@mixin backdrop($z-index) {
+ visibility: hidden;
+ position: fixed;
+ inset: 0px;
+ z-index: $z-index;
+ background-color: var(--backdrop-color);
+ cursor: pointer;
+ opacity: 0.0;
+ transition: opacity $duration * 2;
+}
+
+@mixin backdrop-visible() {
+ visibility: visible;
+ opacity: 1.0;
+}
+
@mixin card() {
display: block;
overflow: hidden;
diff --git a/sass/_utils.scss b/sass/_utils.scss
index bc423240f4..4876fedf4e 100644
--- a/sass/_utils.scss
+++ b/sass/_utils.scss
@@ -1,5 +1,5 @@
.hidden {
- display: none;
+ display: none !important;
}
// Visual outline on focused elements, for accessibility
diff --git a/sass/_vars.scss b/sass/_vars.scss
index 90cebe881a..5f5007ed01 100644
--- a/sass/_vars.scss
+++ b/sass/_vars.scss
@@ -1,3 +1,5 @@
+@use "sass:color";
+
// Typography
$size-body: 16px; // Usual browser default size
$size-body-mobile: 14px; // Size for mobile
@@ -25,6 +27,8 @@ $duration: 250ms;
$z-layout-header: 800;
$z-main-menu-backdrop: 900;
$z-main-menu: 1000;
+$z-search-backdrop: 1100;
+$z-search: 1200;
// General
$max-width: 1200px;
@@ -42,13 +46,45 @@ $border-radius: 10px;
// the `sass:color` package to derive new colors. If a color is not going to be
// used to derive a new color, then CSS vars should be used instead.
//
-// See PR: https://github.com/bevyengine/bevy-website/pull/1953
// See Relative Colors support: https://caniuse.com/css-relative-colors
+//
+// To revert:
+// - Original PR: https://github.com/bevyengine/bevy-website/pull/1953
+// - `.search-category`: https://github.com/bevyengine/bevy-website/pull/1935/commits/30d94f6e9316b03d6741fbdfb4ea493f06485482
+// - `.search-result`: https://github.com/bevyengine/bevy-website/pull/1935/commits/0e0069784f93ab27273384ced13833657c73459e
$color-neutral-17: #2c2c2d;
+$color-neutral-40: #666;
$color-neutral-93: #ececec;
$color-black: #000;
$color-white: #fff;
+$color-dark-search-category-default-bg: $color-neutral-40;
+$color-dark-search-category-book-bg: #537134;
+$color-dark-search-category-examples-bg: #885d26;
+$color-dark-search-category-migrations-bg: #2b5e77;
+$color-dark-search-category-errors-bg: #953562;
+$color-dark-search-category-news-bg: #2c5580;
+$color-dark-search-category-contribute-bg: #78527e;
+
+$color-light-search-category-book-bg: #5b8b2c;
+$color-light-search-category-examples-bg: #b96f0e;
+$color-light-search-category-migrations-bg: #455f6b;
+$color-light-search-category-news-bg: #2c65a2;
+
+// TODO: Remove when CSS Relative Colors are available
+@mixin hack-search-category-computed-colors($category, $color) {
+ --search-category-#{$category}-bg-hover-color: #{color.adjust($color, $lightness: 4%)};
+ --search-category-#{$category}-border-color: #{color.adjust($color, $lightness: 10%)};
+ --search-category-#{$category}-text-color: #{color.change($color, $lightness: 90%)};
+}
+
+// TODO: Remove when CSS Relative Colors are available, check: `_search-category.scss`
+@mixin hack-search-category-modifier-vars($category) {
+ --bg-hover-color: var(--search-category-#{$category}-bg-hover-color);
+ --border-color: var(--search-category-#{$category}-border-color);
+ --text-color: var(--search-category-#{$category}-text-color);
+}
+
// CSS Vars
:root {
// Typography
@@ -67,7 +103,7 @@ $color-white: #fff;
--color-neutral-33: #555;
--color-neutral-33b: #535353;
--color-neutral-36: #59595e;
- --color-neutral-40: #666;
+ --color-neutral-40: #{$color-neutral-40};
--color-neutral-42: #6b6b6b;
--color-neutral-45: #737373;
--color-neutral-50: #808080;
@@ -141,6 +177,8 @@ $color-white: #fff;
--asset-version-select-border-color: var(--color-neutral-22);
--asset-version-select-border-hover-color: var(--color-neutral-42);
+ --backdrop-color: #{rgba($color-black, 0.2)};
+
--bevy-instance-canvas-color: var(--color-neutral-18);
--bevy-instance-text-shadow-color: var(--color-black);
--bevy-instance-progress-track-color: var(--color-neutral-33);
@@ -165,6 +203,8 @@ $color-white: #fff;
--button-pink-bg-hover-color: #954c72;
--button-pink-border-color: #ba789b;
--button-pink-text-color: var(--color-neutral-93);
+
+ --button-square-color: var(--color-neutral-93);
--button-square-bg-color: #{rgba($color-neutral-93, 0.05)};
--callout-caution-accent-color: #e82f5a;
@@ -329,6 +369,44 @@ $color-white: #fff;
--people-role-maintainer-color: rgb(242, 103, 255);
--people-role-sme-color: rgb(80, 200, 50);
+ --search-bg-color: var(--color-neutral-13);
+ --search-clear-icon-color: var(--color-neutral-93);
+ --search-border-color: var(--color-neutral-17);
+ --search-input-bg-color: var(--color-neutral-18);
+ --search-input-border-color: var(--color-neutral-22);
+ --search-input-color: var(--color-neutral-93);
+ --search-no-results-color: var(--color-neutral-75);
+ --search-tip-color: var(--color-neutral-67);
+
+ --search-category-default-bg-color: #{$color-dark-search-category-default-bg};
+ --search-category-default-bg-hover-color: #{rgba($color-white, 0.05)};
+ --search-category-border-color: var(--color-neutral-27);
+ --search-category-text-color: var(--color-neutral-67);
+ --search-category-text-hover-color: var(--color-white);
+ --search-category-book-bg-color: #{$color-dark-search-category-book-bg};
+ --search-category-examples-bg-color: #{$color-dark-search-category-examples-bg};
+ --search-category-migrations-bg-color: #{$color-dark-search-category-migrations-bg};
+ --search-category-errors-bg-color: #{$color-dark-search-category-errors-bg};
+ --search-category-news-bg-color: #{$color-dark-search-category-news-bg};
+ --search-category-contribute-bg-color: #{$color-dark-search-category-contribute-bg};
+
+ --search-category-default-border-color: #{color.adjust($color-dark-search-category-default-bg, $lightness: 10%)};
+ --search-category-default-text-color: #{color.change($color-dark-search-category-default-bg, $lightness: 90%)};
+ @include hack-search-category-computed-colors(book, $color-dark-search-category-book-bg);
+ @include hack-search-category-computed-colors(examples, $color-dark-search-category-examples-bg);
+ @include hack-search-category-computed-colors(migrations, $color-dark-search-category-migrations-bg);
+ @include hack-search-category-computed-colors(errors, $color-dark-search-category-errors-bg);
+ @include hack-search-category-computed-colors(news, $color-dark-search-category-news-bg);
+ @include hack-search-category-computed-colors(contribute, $color-dark-search-category-contribute-bg);
+
+ --search-result-bg-color: #{rgba($color-black, 0.15)};
+ --search-result-bg-hover-color: #{rgba($color-white, 0.04)};
+ --search-result-title-color: var(--color-white);
+ --search-result-hash-sign-color: var(--color-neutral-75);
+ --search-result-excerpt-color: var(--color-neutral-53);
+ --search-result-excerpt-term-bg-color: var(--color-white);
+ --search-result-excerpt-term-color: var(--color-neutral-82);
+
--scrollbar-thumb-color: #{rgba($color-white, 0.2)};
--sponsors-name-color: var(--color-neutral-59);
@@ -470,6 +548,36 @@ $color-white: #fff;
--scrollbar-thumb-color: #{rgba($color-black, 0.2)};
+ --search-bg-color: var(--color-neutral-93);
+ --search-clear-icon-color: var(--color-neutral-13);
+ --search-border-color: var(--color-neutral-69);
+ --search-input-bg-color: var(--color-neutral-97);
+ --search-input-border-color: var(--color-neutral-82);
+ --search-input-color: var(--color-neutral-13);
+ --search-no-results-color: var(--color-neutral-40);
+ --search-tip-color: var(--color-neutral-40);
+
+ --search-category-default-bg-hover-color: #{rgba($color-black, 0.05)};
+ --search-category-border-color: var(--color-neutral-67);
+ --search-category-text-color: var(--color-neutral-33);
+ --search-category-text-hover-color: var(--color-black);
+ --search-category-book-bg-color: #{$color-light-search-category-book-bg};
+ --search-category-examples-bg-color: #{$color-light-search-category-examples-bg};
+ --search-category-migrations-bg-color: #{$color-light-search-category-migrations-bg};
+ --search-category-news-bg-color: #{$color-light-search-category-news-bg};
+
+ @include hack-search-category-computed-colors(book, $color-light-search-category-book-bg);
+ @include hack-search-category-computed-colors(examples, $color-light-search-category-examples-bg);
+ @include hack-search-category-computed-colors(migrations, $color-light-search-category-migrations-bg);
+ @include hack-search-category-computed-colors(news, $color-light-search-category-news-bg);
+
+ --search-result-bg-color: #{rgba($color-white, 0.15)};
+ --search-result-bg-hover-color: #{rgba($color-black, 0.04)};
+ --search-result-title-color: var(--color-black);
+ --search-result-hash-sign-color: var(--color-neutral-50);
+ --search-result-excerpt-term-bg-color: var(--color-black);
+ --search-result-excerpt-term-color: var(--color-neutral-33);
+
--table-border-color: var(--color-neutral-75);
--table-header-bg-color: var(--color-neutral-82);
--table-header-color: var(--color-neutral-22);
diff --git a/sass/components/_button-square.scss b/sass/components/_button-square.scss
index 1120662fd7..042bb52537 100644
--- a/sass/components/_button-square.scss
+++ b/sass/components/_button-square.scss
@@ -3,6 +3,12 @@
position: relative;
cursor: pointer;
+ color: var(--button-square-color);
+
+ * {
+ // Don't capture events on sub-elements
+ pointer-events: none;
+ }
&:hover {
&:before {
diff --git a/sass/components/_header.scss b/sass/components/_header.scss
index 041293e572..23b5eb6760 100644
--- a/sass/components/_header.scss
+++ b/sass/components/_header.scss
@@ -64,13 +64,17 @@
&__cta {
flex-shrink: 0;
- &:not(:last-child) {
- margin-right: 8px;
+ &--search {
+ @media #{$bp-phone-landscape-down} {
+ display: none;
+ }
}
&--github {
@include flex-center;
+ margin-left: 12px;
+
img {
height: 30px;
width: auto;
diff --git a/sass/components/_icon.scss b/sass/components/_icon.scss
index a1be364d98..296c34eb0f 100644
--- a/sass/components/_icon.scss
+++ b/sass/components/_icon.scss
@@ -20,6 +20,7 @@
("chevron-right", 9),
("github", 24),
("pencil", 19),
+ ("search", 24),
("times", 16),
);
diff --git a/sass/components/_main-menu-backdrop.scss b/sass/components/_main-menu-backdrop.scss
index 7bb7f89f2b..b0797b13ea 100644
--- a/sass/components/_main-menu-backdrop.scss
+++ b/sass/components/_main-menu-backdrop.scss
@@ -1,20 +1,9 @@
.main-menu-backdrop {
- visibility: hidden;
- position: fixed;
- top: 0px;
- left: 0px;
- width: 100%;
- height: 100%;
- z-index: $z-main-menu-backdrop;
- background-color: var(--main-menu-backdrop-color);
- cursor: pointer;
- opacity: 0;
- transition: opacity $duration * 2;
+ @include backdrop($z-main-menu-backdrop);
}
@include state-checked("mobile-menu") {
.main-menu-backdrop {
- visibility: visible;
- opacity: 1;
+ @include backdrop-visible();
}
}
diff --git a/sass/components/_main-menu.scss b/sass/components/_main-menu.scss
index e425775fbb..615ab9aece 100644
--- a/sass/components/_main-menu.scss
+++ b/sass/components/_main-menu.scss
@@ -82,7 +82,6 @@
&__header {
display: flex;
align-items: center;
- justify-content: space-between;
padding-left: $padding;
height: var(--layout-header-height);
background-color: var(--layout-header-bg-color);
@@ -94,6 +93,10 @@
}
}
+ &__logo {
+ margin-right: auto;
+ }
+
&__page-menu-switch {
display: none;
padding: $padding $padding 0 $padding;
diff --git a/sass/components/_search-category.scss b/sass/components/_search-category.scss
new file mode 100644
index 0000000000..8b220a1bea
--- /dev/null
+++ b/sass/components/_search-category.scss
@@ -0,0 +1,80 @@
+.search-category {
+ --search-category-bg-color: var(--search-category-default-bg-color);
+ @include hack-search-category-modifier-vars(default);
+
+ background-color: transparent;
+ border: 1px solid var(--search-category-border-color);
+ font-size: calc-rem(18px);
+ padding: 4px 8px;
+ border-radius: 8px;
+ cursor: pointer;
+ user-select: none;
+ color: var(--search-category-text-color);
+
+ &:hover {
+ color: var(--search-category-text-hover-color);
+ background-color: var(--search-category-default-bg-hover-color);
+ // background-color: #{unquote('rgb(from var(--search-category-default-bg-hover-color) r g b / 0.05)')};
+ }
+
+ &--quickstart,
+ &--book {
+ --search-category-bg-color: var(--search-category-book-bg-color);
+ @include hack-search-category-modifier-vars(book);
+ }
+
+ &--examples {
+ --search-category-bg-color: var(--search-category-examples-bg-color);
+ @include hack-search-category-modifier-vars(examples);
+ }
+
+ &--migrations {
+ --search-category-bg-color: var(--search-category-migrations-bg-color);
+ @include hack-search-category-modifier-vars(migrations);
+ }
+
+ &--errors {
+ --search-category-bg-color: var(--search-category-errors-bg-color);
+ @include hack-search-category-modifier-vars(errors);
+ }
+
+ &--news {
+ --search-category-bg-color: var(--search-category-news-bg-color);
+ @include hack-search-category-modifier-vars(news);
+ }
+
+ &--contribute {
+ --search-category-bg-color: var(--search-category-contribute-bg-color);
+ @include hack-search-category-modifier-vars(contribute);
+ }
+
+ &--active {
+ // --text-color: #{unquote("hsl(from var(--search-category-bg-color) h s 90)")};
+
+ background-color: var(--search-category-bg-color);
+ color: var(--text-color);
+ border-color: var(--border-color);
+ // border-color: unquote(
+ // "hsl(from var(--search-category-bg-color) h s calc(l + 10))"
+ // );
+
+ &:hover {
+ color: var(--text-color);
+ background-color: var(--bg-hover-color);
+ // background-color: unquote(
+ // "hsl(from var(--search-category-bg-color) h s calc(l + 4))"
+ // );
+ }
+ }
+
+ &--compact {
+ padding: 2px 4px;
+ font-size: calc-rem(14px);
+ cursor: default;
+ border-radius: 4px;
+
+ &:hover {
+ background-color: var(--search-category-bg-color);
+ }
+ }
+}
diff --git a/sass/components/_search-result.scss b/sass/components/_search-result.scss
new file mode 100644
index 0000000000..f87ca87829
--- /dev/null
+++ b/sass/components/_search-result.scss
@@ -0,0 +1,108 @@
+.search-result {
+ display: block;
+ background-color: var(--search-result-bg-color);
+ border-radius: 8px;
+ padding: 8px;
+
+ &--compact {
+ text-decoration: none;
+
+ &:focus {
+ outline: var(--focus-outline);
+ }
+
+ &:hover {
+ background-color: var(--search-result-bg-hover-color);
+ }
+ }
+
+ &__header {
+ display: flex;
+ align-items: start;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 4px;
+ }
+
+ &__title,
+ &__sub-title {
+ @include override-anchor {
+ color: var(--search-result-title-color);
+ }
+
+ display: block;
+ margin: 0;
+ font-weight: 500;
+ word-break: break-word;
+ }
+
+ &__title {
+ font-size: calc-rem(24px);
+ }
+
+ &__sub-item {
+ border-radius: 8px;
+ text-decoration: none;
+ padding: 4px 6px;
+ position: relative;
+
+ &:focus {
+ outline: var(--focus-outline);
+ }
+
+ &:before {
+ content: "#";
+ position: absolute;
+ top: 4px;
+ left: -16px;
+ font-size: calc-rem(20px);
+ font-weight: 400;
+ color: var(--search-result-hash-sign-color);
+ }
+
+ &:hover {
+ background-color: var(--search-result-bg-hover-color);
+ }
+ }
+
+ &__sub-title {
+ font-size: calc-rem(20px);
+ font-weight: 400;
+ }
+
+ &__category {
+ flex-shrink: 0;
+ }
+
+ &__sub-results {
+ display: flex;
+ flex-direction: column;
+ margin: 8px 0 0;
+ padding-left: 24px;
+
+ li {
+ list-style: disclosure-closed;
+ }
+ }
+
+ &__excerpt {
+ @include text-ellipsis(2);
+ margin: 4px 0 0;
+ color: var(--search-result-excerpt-color);
+ font-size: calc-rem(16px);
+ line-height: 1.4;
+
+ mark {
+ padding: 2px 4px;
+ background-color: #{unquote('rgb(from var(--search-result-excerpt-term-bg-color) r g b / 0.1)')};
+ color: var(--search-result-excerpt-term-color);
+ border-radius: 4px;
+ font-size: 0.95em;
+ }
+ }
+
+ &__more {
+ font-size: calc-rem(18px);
+ padding: 16px 28px 8px;
+ }
+}
diff --git a/sass/components/_search.scss b/sass/components/_search.scss
new file mode 100644
index 0000000000..4795aae562
--- /dev/null
+++ b/sass/components/_search.scss
@@ -0,0 +1,136 @@
+.search {
+ &--searching {
+ .search__header,
+ .search__content {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+
+ &--visible {
+ .search__dialog {
+ display: flex;
+ }
+
+ .search__backdrop {
+ @include backdrop-visible();
+ }
+ }
+
+ &__backdrop {
+ @include backdrop($z-search-backdrop);
+ backdrop-filter: blur(8px);
+ }
+
+ &__dialog {
+ --search-dialog-v-padding: 80px;
+
+ display: none;
+ flex-direction: column;
+ width: calc(100% - 24px);
+ max-width: 800px;
+ max-height: calc(100% - var(--search-dialog-v-padding) * 2);
+ overflow: hidden;
+ border-radius: 8px;
+ border: 2px solid var(--search-border-color);
+ position: fixed;
+ top: var(--search-dialog-v-padding);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: $z-search;
+ background-color: var(--search-bg-color);
+ box-shadow: 0px 16px 40px rgba(#000, 0.35);
+
+ @media #{$bp-tablet-landscape-up} {
+ --search-dialog-v-padding: 160px;
+ }
+ }
+
+ &__header {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px;
+ border-bottom: 1px solid var(--search-border-color);
+ }
+
+ &__header-top {
+ display: flex;
+ gap: 8px;
+ }
+
+ &__categories {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ &__input-wrapper {
+ position: relative;
+ display: flex;
+ width: 100%;
+ }
+
+ &__input {
+ width: 100%;
+ display: block;
+ border-radius: 8px;
+ border: 2px solid var(--search-input-border-color);
+ background-color: var(--search-input-bg-color);
+ text-decoration: none;
+ font-size: 1.2rem;
+ padding: 0.4rem;
+ color: var(--search-input-color);
+ }
+
+ &__clear-filter {
+ @include flex-center;
+
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 40px;
+ cursor: pointer;
+ font-size: 16px;
+ color: var(--search-clear-icon-color);
+ background-color: transparent;
+ border-radius: 8px;
+ border: 0;
+ }
+
+ &__close {
+ cursor: pointer;
+ }
+
+ &__content {
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 12px;
+ }
+
+ &__results {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__no-results {
+ @include flex-center;
+
+ min-height: 100px;
+ text-align: center;
+ word-break: break-word;
+ font-size: calc-rem(20px);
+ color: var(--search-no-results-color);
+ }
+
+ &__tip {
+ color: var(--search-tip-color);
+ border-top: 1px solid var(--search-border-color);
+ padding: 6px 12px;
+ font-size: calc-rem(14px);
+ font-weight: 300;
+ }
+}
diff --git a/sass/elements/_kbd.scss b/sass/elements/_kbd.scss
index 8e93b2658f..5150db6d30 100644
--- a/sass/elements/_kbd.scss
+++ b/sass/elements/_kbd.scss
@@ -1,9 +1,12 @@
kbd {
- font-size: 0.8rem;
- padding: 0.1rem;
+ font-family: var(--font-family-mono);
+ font-size: 0.9em;
+ padding: 0.2em;
+ margin-inline: 0.1em;
line-height: 1;
- border-color: var(--kbd-border-color);
- border-radius: 0.3rem;
- border-style: solid;
- border-width: 1px;
+ border-radius: 0.3em;
+ border: 1px solid var(--kbd-border-color);
+ min-width: 2.5ch;
+ display: inline-block;
+ text-align: center;
}
diff --git a/sass/site.scss b/sass/site.scss
index e4cd932ae8..b919da7cc0 100644
--- a/sass/site.scss
+++ b/sass/site.scss
@@ -48,6 +48,9 @@
@import "components/on-this-page";
@import "components/page-with-menu";
@import "components/pr-list";
+@import "components/search";
+@import "components/search-category";
+@import "components/search-result";
@import "components/sponsors";
@import "components/syntax-theme";
@import "components/themed-picture";
diff --git a/static/assets/icon-search.svg b/static/assets/icon-search.svg
new file mode 100644
index 0000000000..2db09706b3
--- /dev/null
+++ b/static/assets/icon-search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/optional-helpers.js b/static/optional-helpers.js
index b39b0a2a25..df88512ec2 100644
--- a/static/optional-helpers.js
+++ b/static/optional-helpers.js
@@ -1,16 +1,23 @@
// these helpers should be totally optional. if someone disables this javascript in their browser,
// core functionality should not be affected.
+import { isSearchOpen } from "./search.mjs";
+
+function navigateDocsNext(/** @type {KeyboardEvent} */ e) {
+ if (isSearchOpen()) {
+ return;
+ }
-function navigateDocsNext(e) {
if (e.key == 'ArrowLeft') {
- var previous = document.querySelector("[data-docs-nav-previous]");
- if (previous) {
+ const previous = document.querySelector("[data-docs-nav-previous]");
+
+ if (previous instanceof HTMLAnchorElement) {
previous.click();
}
} else if (e.key == 'ArrowRight') {
- var next = document.querySelector("[data-docs-nav-next]");
- if (next) {
+ const next = document.querySelector("[data-docs-nav-next]");
+
+ if (next instanceof HTMLAnchorElement) {
next.click();
}
}
diff --git a/static/search-categories.mjs b/static/search-categories.mjs
new file mode 100644
index 0000000000..9724ab6b46
--- /dev/null
+++ b/static/search-categories.mjs
@@ -0,0 +1,93 @@
+// @ts-check
+/** @import {Category} from "./search.mjs" */
+
+/**
+ * @returns string
+ */
+function getCategoryId(/** @type {string} */ category) {
+ return category.toLowerCase().replace(/\s+/g, "");
+}
+
+class SearchCategories {
+ /** @private @readonly */
+ STORAGE_KEY = "bevy-search-categories";
+
+ constructor(
+ /** @type {string[]} */ pagefindCategories,
+ /** @type {string[]} */ categoriesOrder,
+ /** @type {string[]} */ defaultCheckedCategories
+ ) {
+ // Log warnings if order/checked categories don't match with the Pagefind categories
+ [...categoriesOrder, ...defaultCheckedCategories].forEach((category) => {
+ if (!pagefindCategories.includes(category)) {
+ console.warn(
+ `Category "${category}" not found in Pagefind search index.`
+ );
+ }
+ });
+
+ /** @private @readonly @property {string[]}*/
+ this.order = categoriesOrder;
+
+ const checkedCategories = this.getInitialCheckedCategories(
+ defaultCheckedCategories
+ );
+
+ /** @private @readonly @property {Record}*/
+ this.categories = Object.fromEntries(
+ pagefindCategories.map((name) => {
+ const id = getCategoryId(name);
+ return [id, { id, name, checked: checkedCategories.includes(name) }];
+ })
+ );
+ }
+
+ /**
+ * @returns {Category[]}
+ */
+ getSorted() {
+ // Sort by `order` or by `category` name ASC if not found
+ return Object.values(this.categories).sort((a, b) => {
+ const aIndex = this.order.indexOf(a.name);
+ const bIndex = this.order.indexOf(b.name);
+
+ return (
+ (aIndex === -1 ? Infinity : aIndex) -
+ (bIndex === -1 ? Infinity : bIndex) || a.name.localeCompare(b.name)
+ );
+ });
+ }
+
+ /**
+ * @private
+ * @returns {string[]}
+ */
+ getInitialCheckedCategories(/** @type {string[]} */ fallback) {
+ try {
+ // Load categories status from localStorage
+ const savedCategoriesRaw = localStorage.getItem(this.STORAGE_KEY);
+
+ if (savedCategoriesRaw) {
+ const savedCategories = JSON.parse(savedCategoriesRaw);
+ return Object.values(savedCategories)
+ .filter(({ checked }) => checked)
+ .map(({ name }) => name);
+ }
+ } catch {}
+
+ return fallback;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ toggle(/** @type {string} */ id) {
+ const category = this.categories[id];
+ category.checked = !category.checked;
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.categories));
+
+ return category.checked;
+ }
+}
+
+export { getCategoryId, SearchCategories };
diff --git a/static/search-dialog.mjs b/static/search-dialog.mjs
new file mode 100644
index 0000000000..4892fe5606
--- /dev/null
+++ b/static/search-dialog.mjs
@@ -0,0 +1,334 @@
+// @ts-check
+/** @import {Pagefind} from "./search.mjs" */
+/** @import {SearchCategories} from "./search-categories.mjs" */
+/** @import {SearchTpl} from "./search-tpl.mjs" */
+import { debounce } from "/tools.js";
+
+class SearchDialog {
+ /** @private @readonly */
+ CLASS_SEARCHING = "search--searching";
+ /** @private @readonly */
+ CLASS_VISIBLE = "search--visible";
+
+ // Up/down arrows navigation
+ /** @private @type {HTMLElement[] | undefined} */
+ results = undefined;
+ /** @private @type {HTMLElement | undefined} */
+ activeResult = undefined;
+
+ constructor(
+ /** @type {Pagefind} */ pagefind,
+ /** @type {SearchCategories} */ categories,
+ /** @type {HTMLElement} */ searchEl,
+ /** @type {HTMLElement} */ backdropEl,
+ /** @type {HTMLElement} */ dialogEl,
+ /** @type {HTMLElement} */ categoriesEl,
+ /** @type {HTMLElement} */ contentEl,
+ /** @type {HTMLElement} */ resultsEl,
+ /** @type {HTMLElement} */ noResultsEl,
+ /** @type {HTMLElement} */ closeEl,
+ /** @type {HTMLInputElement} */ inputEl,
+ /** @type {HTMLElement} */ clearFilterEl,
+ /** @type {SearchTpl} */ searchTpl
+ ) {
+ /** @private @readonly @property {Pagefind} */
+ this.pagefind = pagefind;
+ /** @private @readonly @property {SearchCategories} */
+ this.categories = categories;
+ /** @private @readonly @property {HTMLElement} */
+ this.searchEl = searchEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.backdropEl = backdropEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.dialogEl = dialogEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.categoriesEl = categoriesEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.contentEl = contentEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.resultsEl = resultsEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.noResultsEl = noResultsEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.closeEl = closeEl;
+ /** @private @readonly @property {HTMLInputElement} */
+ this.inputEl = inputEl;
+ /** @private @readonly @property {HTMLElement} */
+ this.clearFilterEl = clearFilterEl;
+ /** @private @readonly @property {SearchTpl} */
+ this.searchTpl = searchTpl;
+
+ // Init Categories
+ this.categories.getSorted().forEach((category) => {
+ this.categoriesEl.appendChild(this.searchTpl.createCategoryEl(category));
+ });
+
+ this.categoriesEl.addEventListener("click", (event) => {
+ if (event.target instanceof HTMLElement) {
+ const categoryId = event.target.dataset.category;
+
+ if (categoryId) {
+ const checked = this.categories.toggle(categoryId);
+ event.target.classList.toggle("search-category--active", checked);
+ this.search();
+ }
+ }
+ });
+
+ // Setup global event listeners
+ window.addEventListener("keydown", (event) => {
+ // Close with `Escape`
+ if (event.code === "Escape" && this.isOpen()) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.hide();
+ }
+
+ // Results keyboard navigations
+ if (event.code === "ArrowDown" && this.results) {
+ this.focusNext();
+ event.preventDefault();
+ }
+
+ if (event.code === "ArrowUp" && this.results) {
+ this.focusPrevious();
+ event.preventDefault();
+ }
+
+ // Open with `S`
+ const isSomeTextInputFocused =
+ document.activeElement instanceof HTMLInputElement ||
+ document.activeElement instanceof HTMLTextAreaElement;
+
+ if (event.code === "KeyS" && !isSomeTextInputFocused) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ if (!this.isOpen()) {
+ this.show();
+ } else {
+ this.inputEl.focus();
+ this.inputEl.select();
+ }
+ }
+ });
+
+ // Same page navigation should close the dialog
+ this.dialogEl.addEventListener("click", (event) => {
+ if (
+ event.target instanceof HTMLAnchorElement &&
+ event.target.hasAttribute("data-search-result")
+ ) {
+ const maybeUrl = event.target.getAttribute("href");
+
+ if (maybeUrl) {
+ const newPath = maybeUrl.split("#")[0];
+
+ if (window.location.pathname === newPath) {
+ this.hide();
+ }
+ }
+ }
+ });
+
+ this.inputEl.addEventListener(
+ "input",
+ debounce(() => this.search(), 500)
+ );
+
+ this.clearFilterEl.addEventListener("click", () => this.clearInput());
+ this.closeEl.addEventListener("click", () => this.hide());
+ this.backdropEl.addEventListener("click", () => this.hide());
+
+ // Open the dialog when any "trigger" element is clicked
+ window.addEventListener("click", (event) => {
+ if (
+ event.target instanceof HTMLElement &&
+ event.target.hasAttribute("data-search-open")
+ ) {
+ this.show();
+ }
+ });
+ }
+
+ /**
+ * @private
+ * @returns {void}
+ */
+ clearInput() {
+ this.inputEl.value = "";
+ this.inputEl.focus();
+ this.clearFilterEl.classList.add("hidden");
+ this.resetContentState(true);
+ }
+
+ /**
+ * @private
+ * @returns {void}
+ */
+ focusNext() {
+ if (!this.results) {
+ return;
+ }
+
+ if (!this.activeResult) {
+ this.activeResult = this.results[0];
+ } else {
+ const idx = this.results.indexOf(this.activeResult);
+ const next = this.results[idx + 1];
+
+ if (next) {
+ this.activeResult = next;
+ } else {
+ this.activeResult = this.results[0];
+ }
+ }
+
+ this.activeResult.focus();
+ }
+
+ /**
+ * @private
+ * @returns {void}
+ */
+ focusPrevious() {
+ if (!this.results) {
+ return;
+ }
+
+ if (!this.activeResult) {
+ this.activeResult = this.results[this.results.length - 1];
+ } else {
+ const idx = this.results.indexOf(this.activeResult);
+ const prev = this.results[idx - 1];
+
+ if (prev) {
+ this.activeResult = prev;
+ } else {
+ this.activeResult = this.results[this.results.length - 1];
+ }
+ }
+
+ this.activeResult.focus();
+ }
+
+ /**
+ * @returns {void}
+ */
+ hide() {
+ this.searchEl.classList.remove(this.CLASS_VISIBLE);
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isOpen() {
+ return this.searchEl.classList.contains(this.CLASS_VISIBLE);
+ }
+
+ /**
+ * @private
+ * @returns {Promise}
+ */
+ async search() {
+ const term = this.inputEl.value.trim();
+
+ this.clearFilterEl.classList.toggle("hidden", term === "");
+
+ if (term === "") {
+ this.resetContentState(true);
+ return;
+ }
+
+ let caretPosition = this.inputEl.selectionStart;
+
+ try {
+ this.searchEl.classList.add(this.CLASS_SEARCHING);
+ this.inputEl.disabled = true;
+
+ const { results: allResults } = await this.pagefind.search(term, {
+ filters: {
+ any: this.categories
+ .getSorted()
+ .filter((category) => category.checked)
+ .map(({ name }) => ({ category: name })),
+ },
+ });
+
+ // Reduce results set
+ const maxResults = 20;
+ const results = allResults.slice(0, maxResults);
+
+ if (results.length > 0) {
+ await this.showResults(results);
+ } else {
+ this.showNoResults(term);
+ }
+ } catch (err) {
+ console.error(`Failed to search for term: "${term}"`, err);
+ } finally {
+ this.searchEl.classList.remove(this.CLASS_SEARCHING);
+ this.inputEl.disabled = false;
+ this.inputEl.focus();
+ this.inputEl.setSelectionRange(caretPosition, caretPosition);
+ }
+ }
+
+ /**
+ * @private
+ * @returns {void}
+ */
+ resetContentState(/** @type {boolean} */ hideContentWrapper = false) {
+ this.contentEl.classList.toggle("hidden", hideContentWrapper);
+ this.contentEl.scrollTop = 0;
+ this.resultsEl.innerHTML = "";
+ this.resultsEl.classList.add("hidden");
+ this.noResultsEl.classList.add("hidden");
+ }
+
+ /**
+ * @returns {void}
+ */
+ show() {
+ this.searchEl.classList.add(this.CLASS_VISIBLE);
+ this.inputEl.focus();
+ this.inputEl.value = "";
+ this.clearFilterEl.classList.add("hidden");
+ this.resetContentState(true);
+ }
+
+ /**
+ * @private
+ * @returns {Promise}
+ */
+ async showResults(/** @type any[] */ results) {
+ const data = await Promise.all(results.map((result) => result.data()));
+
+ this.resetContentState();
+ this.resultsEl.classList.remove("hidden");
+
+ data.forEach((item) => {
+ this.resultsEl.appendChild(this.searchTpl.createResultEl(item));
+ });
+
+ this.results = Array.from(
+ this.resultsEl.querySelectorAll("[data-search-result]")
+ );
+ }
+
+ /**
+ * @private
+ * @returns {void}
+ */
+ showNoResults(/** @type {string} */ term) {
+ this.resetContentState();
+ const termEl = this.noResultsEl.querySelector("[data-search-term]");
+ this.noResultsEl.classList.remove("hidden");
+
+ if (termEl) {
+ termEl.innerHTML = term;
+ }
+ }
+}
+
+export { SearchDialog };
diff --git a/static/search-tpl.mjs b/static/search-tpl.mjs
new file mode 100644
index 0000000000..a969e2d849
--- /dev/null
+++ b/static/search-tpl.mjs
@@ -0,0 +1,139 @@
+// @ts-check
+/** @import {Category} from "./search.mjs" */
+import { getCategoryId } from "./search-categories.mjs";
+
+class SearchTpl {
+ constructor(
+ /** @type {HTMLTemplateElement} */ categoryTplEl,
+ /** @type {HTMLTemplateElement} */ resultCompactTplEl,
+ /** @type {HTMLTemplateElement} */ resultTplEl,
+ /** @type {HTMLTemplateElement} */ subResultTplEl
+ ) {
+ /** @private @readonly @property {HTMLTemplateElement} */
+ this.categoryTplEl = categoryTplEl;
+ /** @private @readonly @property {HTMLTemplateElement} */
+ this.resultCompactTplEl = resultCompactTplEl;
+ /** @private @readonly @property {HTMLTemplateElement} */
+ this.resultTplEl = resultTplEl;
+ /** @private @readonly @property {HTMLTemplateElement} */
+ this.subResultTplEl = subResultTplEl;
+ }
+
+ /**
+ * @returns {DocumentFragment}
+ */
+ createResultEl(/** @type {any} */ item) {
+ const isCompact =
+ item.sub_results.length === 1 && item.sub_results[0].url === item.url;
+
+ const resultEl = /** @type {DocumentFragment} */ (
+ isCompact
+ ? this.resultCompactTplEl.content.cloneNode(true)
+ : this.resultTplEl.content.cloneNode(true)
+ );
+
+ const titleEl = resultEl.querySelector("[data-title]");
+ const categoryEl = resultEl.querySelector("[data-category]");
+
+ if (titleEl) {
+ titleEl.innerHTML = item.meta.title;
+ }
+
+ if (categoryEl) {
+ if (item.filters.category?.length > 0) {
+ const category = item.filters.category[0];
+ categoryEl.innerHTML = category;
+ categoryEl.classList.add(`search-category--${getCategoryId(category)}`);
+ } else {
+ categoryEl.classList.add("hidden");
+ }
+ }
+
+ if (isCompact) {
+ const wrapperEl = resultEl.querySelector("[data-wrapper]");
+ const excerptEl = resultEl.querySelector("[data-excerpt]");
+
+ wrapperEl?.setAttribute("href", item.url);
+
+ if (excerptEl) {
+ excerptEl.innerHTML = item.sub_results[0].excerpt;
+ }
+ } else {
+ const subResultsEl = resultEl.querySelector("[data-sub-results]");
+ const moreEl = resultEl.querySelector("[data-more]");
+ const maxSubResults = 5;
+
+ titleEl?.setAttribute("href", item.url);
+
+ if (subResultsEl) {
+ item.sub_results.slice(0, maxSubResults).forEach((sub) => {
+ subResultsEl.appendChild(
+ this.createSubResultEl(sub.title, sub.url, sub.excerpt)
+ );
+ });
+ }
+
+ if (moreEl && item.sub_results.length > maxSubResults) {
+ moreEl.innerHTML = `+${item.sub_results.length - maxSubResults} more`;
+ moreEl.classList.remove("hidden");
+ }
+ }
+
+ return resultEl;
+ }
+
+ /**
+ * @private
+ * @returns {DocumentFragment}
+ */
+ createSubResultEl(
+ /** @type {string} */ title,
+ /** @type {string} */ url,
+ /** @type {string} */ excerpt
+ ) {
+ const subResultEl = /** @type {DocumentFragment} */ (
+ this.subResultTplEl.content.cloneNode(true)
+ );
+ const linkEl = subResultEl.querySelector("[data-link]");
+ const titleEl = subResultEl.querySelector("[data-title]");
+ const subResultsEl = subResultEl.querySelector("[data-excerpt]");
+
+ if (linkEl) {
+ linkEl.setAttribute("href", url);
+ }
+
+ if (titleEl) {
+ titleEl.innerHTML = title.replace(/[#\s]+$/, "");
+ }
+
+ if (subResultsEl) {
+ subResultsEl.innerHTML = excerpt;
+ }
+
+ return subResultEl;
+ }
+
+ /**
+ * @returns {DocumentFragment}
+ */
+ createCategoryEl(/** @type {Category} */ category) {
+ const el = /** @type {DocumentFragment} */ (
+ this.categoryTplEl.content.cloneNode(true)
+ );
+ const categoryEl = el.querySelector("[data-category]");
+
+ if (categoryEl instanceof HTMLElement) {
+ categoryEl.innerHTML = category.name;
+ categoryEl.dataset.category = category.id;
+ categoryEl.classList.add(`search-category--${category.id}`);
+
+ if (category.checked) {
+ categoryEl.classList.add("search-category--active");
+ }
+ }
+
+ return el;
+ }
+}
+
+export { SearchTpl };
diff --git a/static/search.mjs b/static/search.mjs
new file mode 100644
index 0000000000..15475fd303
--- /dev/null
+++ b/static/search.mjs
@@ -0,0 +1,106 @@
+// @ts-check
+import { SearchCategories } from "./search-categories.mjs";
+import { SearchDialog } from "./search-dialog.mjs";
+import { SearchTpl } from "./search-tpl.mjs";
+
+/**
+ * @typedef Pagefind
+ * @prop {(term: string, options: any) => Promise} search
+ */
+/**
+ * @typedef Category
+ * @prop {string} id
+ * @prop {string} name
+ * @prop {boolean} checked
+ */
+
+/** @type {SearchDialog | undefined} */
+let searchDialog;
+
+/** @returns boolean */
+function isSearchOpen() {
+ return searchDialog?.isOpen() ?? false;
+}
+
+window.addEventListener("load", async () => {
+ const getEl = (/** @type {string} */ id) =>
+ document.querySelector(`[data-search-${id}]`);
+ const searchEl = document.querySelector(`[data-search]`);
+ const backdropEl = getEl("backdrop");
+ const dialogEl = getEl("dialog");
+ const categoriesEl = getEl("categories");
+ const contentEl = getEl("content");
+ const resultsEl = getEl("results");
+ const noResultsEl = getEl("no-results");
+ const closeEl = getEl("close");
+ const inputEl = getEl("input");
+ const clearFilterEl = getEl("clear-filter");
+ const categoryTplEl = getEl("category-tpl");
+ const resultCompactTplEl = getEl("result-compact-tpl");
+ const resultTplEl = getEl("result-tpl");
+ const subResultTplEl = getEl("sub-result-tpl");
+
+ if (
+ searchEl instanceof HTMLElement &&
+ backdropEl instanceof HTMLElement &&
+ dialogEl instanceof HTMLElement &&
+ categoriesEl instanceof HTMLElement &&
+ contentEl instanceof HTMLElement &&
+ resultsEl instanceof HTMLElement &&
+ noResultsEl instanceof HTMLElement &&
+ closeEl instanceof HTMLElement &&
+ inputEl instanceof HTMLInputElement &&
+ clearFilterEl instanceof HTMLElement &&
+ categoryTplEl instanceof HTMLTemplateElement &&
+ resultCompactTplEl instanceof HTMLTemplateElement &&
+ resultTplEl instanceof HTMLTemplateElement &&
+ subResultTplEl instanceof HTMLTemplateElement
+ ) {
+ try {
+ // @ts-ignore
+ const pagefind = await import("/pagefind/pagefind.js");
+ const filters = await pagefind.filters();
+ const categories = new SearchCategories(
+ Object.keys(filters.category),
+ [
+ "Quick Start",
+ "Examples",
+ "Migrations",
+ "News",
+ "Contribute",
+ "Errors",
+ ],
+ ["Quick Start", "Examples"]
+ );
+
+ searchDialog = new SearchDialog(
+ pagefind,
+ categories,
+ searchEl,
+ backdropEl,
+ dialogEl,
+ categoriesEl,
+ contentEl,
+ resultsEl,
+ noResultsEl,
+ closeEl,
+ inputEl,
+ clearFilterEl,
+ new SearchTpl(
+ categoryTplEl,
+ resultCompactTplEl,
+ resultTplEl,
+ subResultTplEl
+ )
+ );
+ } catch (err) {
+ console.error("Failed to initialize Pagefind.", err);
+ }
+ } else {
+ console.error(
+ "Not all the elements needed to build the Search dialog were found."
+ );
+ }
+});
+
+export { isSearchOpen };
diff --git a/static/tools.js b/static/tools.js
index ed806d905f..d5681364f6 100644
--- a/static/tools.js
+++ b/static/tools.js
@@ -1,3 +1,12 @@
+function debounce(callback, wait) {
+ let timeoutId;
+
+ return (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => callback(...args), wait);
+ };
+}
+
function getFilename(resource) {
const pathname = (typeof resource === 'string')
? resource
@@ -56,4 +65,4 @@ async function progressiveFetch(resource, callbacks={}) {
return new Response(response.body.pipeThrough(transform), response);
}
-export { progressiveFetch };
+export { debounce, progressiveFetch };
diff --git a/templates/docs.html b/templates/docs.html
index 361dbcef60..ab4299c8b9 100644
--- a/templates/docs.html
+++ b/templates/docs.html
@@ -14,7 +14,7 @@
{% endif %}
{% endfor %}
-
+
{% endblock head_extensions %}
diff --git a/templates/layouts/base.html b/templates/layouts/base.html
index 093491b90f..055cada9b3 100644
--- a/templates/layouts/base.html
+++ b/templates/layouts/base.html
@@ -1,6 +1,7 @@
{% import "macros/header.html" as header_macros %}
{% import "macros/public_draft.html" as public_draft %}
{% import "macros/base.html" as base_macros %}
+{% import "macros/pagefind.html" as pagefind_macros %}
{# Shortcut to get the section/page. This variable will have a value except in some special pages like `404.html`. #}
{% set_global section_or_page = false %}
@@ -88,6 +89,7 @@
+
{% if ancestor_is_public_draft or section_or_page and section_or_page.extra and section_or_page.extra.public_draft %}
@@ -95,6 +97,8 @@
{% endif %}
{% if page.extra.public_draft or section.extra.public_draft %}[DRAFT] {% endif -%}{{ page_title }}
+
+
{% block head_extensions %}{% endblock head_extensions %}
@@ -130,7 +134,7 @@
role="navigation"
data-page-menu-switch-state-container>
-
+
+
+ {% include "search.html" %}