From f0b454ed57f2b7e0b18656a570e8ced5dd8dda2d Mon Sep 17 00:00:00 2001 From: JP Hastings-Spital Date: Tue, 25 Aug 2026 22:05:33 +0100 Subject: [PATCH] feat: docs search trigger + Spotlight-style overlay, wire Pagefind indexing (ATFS-dfm3) Replaces the inline PagefindUI mount in the docs sidebar (which never actually indexed anything) with a minimal search-docs trigger beneath the platform toggle, and a centered modal overlay that lazy-loads Pagefind's widget on first open. cmd/ctrl+K opens the overlay from anywhere on a docs page and focuses its input; Escape and a backdrop click close it. Closes the bean's last open item: pagefind (pinned devDependency) now actually indexes the built site, in both hack/deploy-site.sh and the Makefile's web target, after the existing build+bake steps. --- ...s-split-into-a-multi-page-diataxis-site.md | 30 ++- .changeset/docs-search-overlay.md | 5 + Makefile | 21 +- hack/deploy-site.sh | 10 + web/package.json | 1 + web/pnpm-lock.yaml | 73 ++++++ web/src/lib/docs/Search.svelte | 229 ++++++++++++++++++ web/src/lib/docs/searchShortcut.test.ts | 27 +++ web/src/lib/docs/searchShortcut.ts | 11 + web/src/routes/docs/+layout.svelte | 36 +-- 10 files changed, 388 insertions(+), 55 deletions(-) create mode 100644 .changeset/docs-search-overlay.md create mode 100644 web/src/lib/docs/Search.svelte create mode 100644 web/src/lib/docs/searchShortcut.test.ts create mode 100644 web/src/lib/docs/searchShortcut.ts diff --git a/.beans/ATFS-dfm3--atfsdev-docs-split-into-a-multi-page-diataxis-site.md b/.beans/ATFS-dfm3--atfsdev-docs-split-into-a-multi-page-diataxis-site.md index ba042d8..5a8b771 100644 --- a/.beans/ATFS-dfm3--atfsdev-docs-split-into-a-multi-page-diataxis-site.md +++ b/.beans/ATFS-dfm3--atfsdev-docs-split-into-a-multi-page-diataxis-site.md @@ -77,8 +77,16 @@ afterthought. bullets added - [x] GoSD/SD-card terminology pass (capitalize when naming the tech, prefer "SD card" when we don't need to) -- [ ] Pagefind wiring into `hack/deploy-site.sh` / `make web` — designed, - not yet implemented (see Known gaps) +- [x] Pagefind wiring into `hack/deploy-site.sh` / `make web`, plus the + sidebar UI: a minimal trigger (magnifying glass + "Search docs", + `web/src/lib/docs/Search.svelte`) beneath the platform toggle opens a + centered, Spotlight/Raycast-style overlay hosting Pagefind's own + `PagefindUI` widget, restyled via its CSS custom properties. cmd/ctrl+K + opens it from anywhere on a docs page and focuses the input + (`searchShortcut.ts`, unit-tested). Verified live in a real browser + (chrome-devtools MCP): open/close via trigger, shortcut, Escape and + backdrop click; a real query rendering highlighted sub-results; light + and dark themes. See Known gaps below for what this replaced. - [x] Live visual check in a real browser, for the multi-line-code-block area specifically — chrome-devtools' own profile stayed locked all session (never resolved; would've risked another concurrent @@ -336,17 +344,15 @@ afterthought. ## Known gaps -**Pagefind is wired into the docs layout's client-side script -(`docs/+layout.svelte` loads `/pagefind/pagefind-ui.js`), but the actual -`npx pagefind --site site` indexing step was never added to -`hack/deploy-site.sh` or the Makefile's `web` target.** The search box -will render and silently do nothing until that step exists — not a bug in -what's built, just an unfinished piece of the plan. Needs: a -`pagefind`-indexing line after `pnpm run build` (and after -`hack/bake-site-downloads.sh`, so baked content is searchable too) in both -places. +~~Pagefind is wired into the docs layout's client-side script, but the +actual indexing step was never added to the build~~ — closed: `pagefind` +(pinned devDependency) now runs after the build and bake steps in both +`hack/deploy-site.sh` and the Makefile's `web` target, and the sidebar's +own UI moved from an inline `PagefindUI` mount to a trigger + centered +overlay (see the checklist above). -No live visual review has happened — every check so far is build-level +No live visual review has happened for the platform toggle or the callout +colors — every check so far is build-level (`pnpm run build`, `svelte-check`, `go test ./...`, `vale lint`). Worth an actual `pnpm run preview` pass before this ships, especially for the platform-toggle's no-flash behavior and the five callout color variants in diff --git a/.changeset/docs-search-overlay.md b/.changeset/docs-search-overlay.md new file mode 100644 index 0000000..37cf38c --- /dev/null +++ b/.changeset/docs-search-overlay.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Wired Pagefind indexing into the site build/deploy pipeline, closing out the docs search box that previously rendered but did nothing. diff --git a/Makefile b/Makefile index b24a4a0..a0f2d77 100644 --- a/Makefile +++ b/Makefile @@ -44,15 +44,17 @@ test-web: # Builds the whole site (web/) in one Vite pass — home page, docs page and # the setup flow, plus web/static/ copied in verbatim — into site/, then # bakes the current release's raw downloads in on top -# (site/downloads/images.json, site/downloads/raw/): both gitignored, -# nothing built lives in the repo, and hack/deploy-site.sh runs the same -# two steps, in the same order, to assemble the tree it uploads. The bake -# step must come second: Vite's build empties site/ and copies -# web/static/ into it verbatim, which would otherwise stomp a -# freshly-baked images.json. This target is for working on the site -# locally — serve site/ afterwards and the whole thing is there: /, -# /docs/, /downloads/ (setup flow and raw downloads), /login/login.js and -# /setup/ (the old URL's redirect) included. +# (site/downloads/images.json, site/downloads/raw/), then indexes the built +# docs pages for Pagefind (site/pagefind/): all three gitignored, nothing +# built lives in the repo, and hack/deploy-site.sh runs the same three +# steps, in the same order, to assemble the tree it uploads. The bake step +# must come second and the Pagefind index last: Vite's build empties site/ +# and copies web/static/ into it verbatim (which would otherwise stomp a +# freshly-baked images.json), and Pagefind has to index whatever HTML is +# already on disk, baked downloads page included. This target is for +# working on the site locally — serve site/ afterwards and the whole thing +# is there: /, /docs/ (search included), /downloads/ (setup flow and raw +# downloads), /login/login.js and /setup/ (the old URL's redirect) included. # # The bake step queries a real atfs instance for the current release # (hack/bake-site-downloads.sh — ATFS-ouy7), defaulting to the project's own @@ -67,6 +69,7 @@ ATFS_DOMAIN ?= storage.atfs.dev web: cd web && vp install --frozen-lockfile && vp run build ATFS_DOMAIN=$(ATFS_DOMAIN) ./hack/bake-site-downloads.sh --allow-unpublished + cd web && vp exec pagefind --site ../site vet: go vet ./... diff --git a/hack/deploy-site.sh b/hack/deploy-site.sh index 8437d68..f8e39ee 100755 --- a/hack/deploy-site.sh +++ b/hack/deploy-site.sh @@ -70,6 +70,16 @@ echo "=== baking the release index into the site ===" # so failing here leaves the previous site serving. ./hack/bake-site-downloads.sh "$SITE_PATH" +# Indexes the docs pages just built (data-pagefind-body in DocLayout.svelte, +# TypeIndex.svelte, etc.) for the sidebar's search overlay +# (web/src/lib/docs/Search.svelte) — after the bake step, so anything it +# writes is covered too. Resolved to an absolute path first since the +# pagefind CLI below runs from web/, where a relative $SITE_PATH would +# resolve against the wrong directory. +echo "=== indexing search (pagefind) ===" +site_abs="$(cd "$SITE_PATH" && pwd)" +(cd web && corepack pnpm exec pagefind --site "$site_abs") + # Cheap insurance against uploading a tree that is missing something # load-bearing: .well-known must be at the top level or did:web:atfs.dev # stops resolving, and an empty site/downloads or site/downloads/raw would diff --git a/web/package.json b/web/package.json index 92cebc0..2200e68 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "@tsconfig/svelte": "5.0.8", "@types/node": "26.2.0", "mdsvex": "0.12.8", + "pagefind": "1.5.2", "rehype-slug": "^6.0.0", "shiki": "^4.4.3", "svelte": "5.56.8", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 7fc7059..a0ee24f 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: mdsvex: specifier: 0.12.8 version: 0.12.8(svelte@5.56.8) + pagefind: + specifier: 1.5.2 + version: 1.5.2 rehype-slug: specifier: ^6.0.0 version: 6.0.0 @@ -160,6 +163,41 @@ packages: '@oxc-project/types@0.146.0': resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -665,6 +703,10 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1082,6 +1124,27 @@ snapshots: '@oxc-project/types@0.146.0': {} + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + '@polka/url@1.0.0-next.29': {} '@rolldown/binding-android-arm-eabi@1.2.5': @@ -1503,6 +1566,16 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + pathe@2.0.3: {} picocolors@1.1.1: {} diff --git a/web/src/lib/docs/Search.svelte b/web/src/lib/docs/Search.svelte new file mode 100644 index 0000000..9b4569e --- /dev/null +++ b/web/src/lib/docs/Search.svelte @@ -0,0 +1,229 @@ + + + + +
+ + +
+ + diff --git a/web/src/lib/docs/searchShortcut.test.ts b/web/src/lib/docs/searchShortcut.test.ts new file mode 100644 index 0000000..fe9cb72 --- /dev/null +++ b/web/src/lib/docs/searchShortcut.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "vitest"; + +import { isSearchShortcut } from "./searchShortcut.js"; + +function keydown(init: Partial<{ metaKey: boolean; ctrlKey: boolean; key: string }>) { + return { key: "k", metaKey: false, ctrlKey: false, ...init }; +} + +test("cmd+k (macOS) is a search shortcut", () => { + expect(isSearchShortcut(keydown({ metaKey: true }))).toBe(true); +}); + +test("ctrl+k (Windows/Linux) is a search shortcut", () => { + expect(isSearchShortcut(keydown({ ctrlKey: true }))).toBe(true); +}); + +test("capslock producing an uppercase K still matches", () => { + expect(isSearchShortcut(keydown({ metaKey: true, key: "K" }))).toBe(true); +}); + +test("k alone, with no modifier, is not a shortcut", () => { + expect(isSearchShortcut(keydown({}))).toBe(false); +}); + +test("cmd/ctrl held with a different key is not a shortcut", () => { + expect(isSearchShortcut(keydown({ metaKey: true, key: "j" }))).toBe(false); +}); diff --git a/web/src/lib/docs/searchShortcut.ts b/web/src/lib/docs/searchShortcut.ts new file mode 100644 index 0000000..cc359c6 --- /dev/null +++ b/web/src/lib/docs/searchShortcut.ts @@ -0,0 +1,11 @@ +// A structural subset of KeyboardEvent, not the DOM type itself — keeps this +// pure and testable without a DOM environment in vitest (config runs plain +// "node", like every other $lib test here). +type ShortcutKeyEvent = { metaKey: boolean; ctrlKey: boolean; key: string }; + +// cmd+K on macOS, ctrl+K everywhere else — checking both modifiers rather +// than the platform lets this work correctly for e.g. a Windows keyboard +// remoted into a Mac, with no user-agent sniffing needed. +export function isSearchShortcut(event: ShortcutKeyEvent): boolean { + return (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k"; +} diff --git a/web/src/routes/docs/+layout.svelte b/web/src/routes/docs/+layout.svelte index 4bfcc8f..a0c3cab 100644 --- a/web/src/routes/docs/+layout.svelte +++ b/web/src/routes/docs/+layout.svelte @@ -3,36 +3,16 @@ // automatically inside the root +layout.svelte's topbar. Provides the // sidebar/search/toggle chrome shared by every docs page; knows nothing // about frontmatter — that's DocLayout.svelte's job (see $lib/docs). - import { onMount } from "svelte"; import { onNavigate, afterNavigate } from "$app/navigation"; import DocToc from "$lib/docs/DocToc.svelte"; import PlatformToggle from "$lib/docs/PlatformToggle.svelte"; + import Search from "$lib/docs/Search.svelte"; import { nav } from "$lib/docs/nav"; import { page } from "$app/state"; import { wireCodeCopyButtons } from "$lib/docs/codeCopy"; let { children } = $props(); - // Pagefind indexes the *built* site (hack/deploy-site.sh runs it after - // `pnpm run build`), so /pagefind/pagefind-ui.js only exists post-build — - // never during `pnpm run dev`. Loading it as a plain script (not an ES - // import) matches how Pagefind ships it; failing silently pre-build is - // the correct behavior, not a bug to chase. - onMount(() => { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = "/pagefind/pagefind-ui.css"; - document.head.appendChild(link); - - const script = document.createElement("script"); - script.src = "/pagefind/pagefind-ui.js"; - script.onload = () => { - // @ts-expect-error - global registered by the script above - new window.PagefindUI({ element: "#docs-search" }); - }; - document.body.appendChild(script); - }); - // Gives the sidebar's current-page arrow (view-transition-name below) // something to animate between: without wrapping the navigation in // startViewTransition, the browser just swaps the old DOM for the new one @@ -61,8 +41,8 @@