Search: one floating window — quick palette that expands into facets, previews and a photo grid #57

Open
opened 2026-09-24 15:52:57 +00:00 by kayg · 4 comments
Owner

Owner decisions S2, S7 (DESIGN §32): everything appears in a floating window over a dimmed and blurred background; search must be ultra fast.

  • ⌘K opens the centered floating palette (calternal.js UX: animated dim + blur in/out, Esc or click outside dismisses, focus trap and restore, > command mode, ⌘⇧K opens in command mode, recents when empty).
  • "See all results" (⌘↩) expands the same window in place into the full view: facets (type, date range, tag, folder, person), a justified photo grid for photo hits, a list with highlighted snippets for everything else, infinite scroll.
  • Previews (S7): on desktop, arrowing through results shows a right-hand preview (note snippet with the hit highlighted, large photo thumbnail, file Quick Look via the shared viewer #35, event/log-entry popover card #43). On mobile, results are cards; long-press peeks the preview.
  • Grouped sections with counts; keyboard-first; stable ordering while typing (no result jumping); streaming results as providers answer (fan-out with deadline, already in calternal-plugin).
  • Performance: debounce ~60–120 ms, cancel in flight, render virtualized lists; measure and report p50/p95 keystroke-to-render.

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js docs/search.md (read-only at /home/kayg/Developer/calternal.js) — its palette UX, > command mode, ranking weights, a11y combobox pattern and recents carry over.
  • Existing code: crates/calternal-search (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web.
  • Owner rules: search must be ultra fast (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work.
  • Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Owner decisions S2, S7 (DESIGN §32): **everything appears in a floating window over a dimmed and blurred background**; search must be ultra fast. - ⌘K opens the centered floating palette (calternal.js UX: animated dim + blur in/out, Esc or click outside dismisses, focus trap and restore, `>` command mode, ⌘⇧K opens in command mode, recents when empty). - "See all results" (⌘↩) **expands the same window in place** into the full view: facets (type, date range, tag, folder, person), a justified photo grid for photo hits, a list with highlighted snippets for everything else, infinite scroll. - Previews (S7): on desktop, arrowing through results shows a right-hand preview (note snippet with the hit highlighted, large photo thumbnail, file Quick Look via the shared viewer #35, event/log-entry popover card #43). On mobile, results are cards; long-press peeks the preview. - Grouped sections with counts; keyboard-first; stable ordering while typing (no result jumping); streaming results as providers answer (fan-out with deadline, already in calternal-plugin). - Performance: debounce ~60–120 ms, cancel in flight, render virtualized lists; measure and report p50/p95 keystroke-to-render. ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js `docs/search.md` (read-only at /home/kayg/Developer/calternal.js) — its palette UX, `>` command mode, ranking weights, a11y combobox pattern and recents carry over. - Existing code: `crates/calternal-search` (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web. - Owner rules: search must be **ultra fast** (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work. - Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Author
Owner

Starting the search window UI job for #57, #61, #63 and #64 (one job, one worktree). Branch: job/search-ui; base SHA: 8c216759f01d704e69ecf37ef8293f422abf5552.

Plan: extend the existing ⌘K dialog and registry in apps/web (no second palette) into one floating search window with pills, previews and an in-place expanded view; add Files ⌘F and Calendar contextual search; add a saved-search CRUD API (JSON files through calternal-fs) and smart folders. I will post findings and a finish comment here.

Starting the search window UI job for #57, #61, #63 and #64 (one job, one worktree). Branch: `job/search-ui`; base SHA: `8c216759f01d704e69ecf37ef8293f422abf5552`. Plan: extend the existing ⌘K dialog and registry in apps/web (no second palette) into one floating search window with pills, previews and an in-place expanded view; add Files ⌘F and Calendar contextual search; add a saved-search CRUD API (JSON files through `calternal-fs`) and smart folders. I will post findings and a finish comment here.
Author
Owner

Finished on branch job/search-ui (not pushed, not merged). Head: a8d2f644b41c0ebdd2eeff2bf4cfca6c6a3cfa5c.

What is done (#57, one floating window):

  • The existing ⌘K dialog and provider registry are extended; there is no second palette. The window uses the shared OverlaySurface: animated dim and blur scrim, Esc and click outside close it, focus trap and focus restore, reduced motion.
  • Compact palette: calternal.js input row and SearchResultRow, grouped sections with counts (five rows each), recents when the query is empty, > command mode and ⇧⌘K. A wide screen shows a resizable right-hand preview that follows the arrow keys: note snippet with the hit highlighted, a large photo thumbnail, file metadata with the shared Quick Look viewer on ⌘Y, and the calendar ItemPreview card for log entries and events. Phones get a sheet with result cards; a long press peeks the preview.
  • ⌘↩ or the expand button widens the same surface into the full view: facets (type, date, tag, folder, person) that toggle pills, a justified photo grid, a virtualized list (more than 60 rows) and infinite scroll (the new limit parameter grows 50 → 200).
  • Stable ordering while typing: answers are kept per provider until that provider answers again, sections have a fixed order, and the active row is kept by id. Client providers answer in the keystroke's task; the server provider debounces 80 ms and is aborted by the next keystroke.
  • Deep link: /search?q=…&view=all&scope=calendar restores the query, its pills and the view. The copy-link button in the field row copies it.
  • Screen readers: combobox with aria-activedescendant, polite result counts, pill add and remove announcements.

Backend changes needed for the window (search crate, API, server):

  • SearchHit has optional kind, path and modified. GET /api/v1/search has limit (1 to 200). The fan-out no longer cuts the joined list at 20 (the Files name match could hide every index hit) and merges duplicate routes.
  • Log entries link to /d/<date>#^<block>, events to /e/<id> (new web route).
  • Semantic-only hits used raw index paths as links; fixed. A query with operators is keyword-only (the semantic index ignored in: and type:). Keyword hits rank before semantic-only hits. When keywords matched, semantic neighbours below cosine 0.25 are dropped.

Measured (debug server, Playwright headless Chromium on a shared machine; the load average was 12–20 on 8 cores during most runs). Final run, then the range over all runs:

  • Server round trip /api/v1/search, 60 queries at e2e scale: p50 7.4 ms, p95 14.8 ms (range p50 7–40 ms, p95 15–95 ms).
  • Keystroke to DOM with client results: p50 0.6 ms, p95 5.1 ms (range p95 5–25 ms).
  • Keystroke to the next animation frame, client results: p50 27.6 ms, p95 73.9 ms. Headless Chromium composites in software under the full-screen scrim blur; this number is the test browser's paint cost, not script time.
  • Keystroke to server results in the DOM (includes the 80 ms debounce): p50 110 ms, p95 184 ms.
  • The 100k-item budget was not measured in this job.

Tests: apps/web/e2e/search.mjs (all steps pass), unit tests for the grammar, snippets, justified rows, dedupe, targets and the shortcut matcher, Rust tests for the fan-out, the fusion and the saved-search routes. The adversarial probe has a new saved-search and limit section.

Gates (this worktree): cargo fmt --check passed; cargo clippy --workspace --all-targets -- -D warnings passed; cargo test --workspace passed except calternal-collab shared_notes (a WebSocket test that failed once and passed on rerun; it does not touch search); bun run check 0 errors, 0 warnings; bun run test 147 passed; bun run build passed; check-generated.sh passed; apps/web/e2e/search.mjs passed. Adversarial probe: round 2 has 0 findings; round 1 has 6 latency findings only, "Task storm 18–23 SLOW 5.8–8.1 s status 201" (24 concurrent task creates on a shared machine with load average 12–20 on 8 cores; every request succeeded). The new saved-search and limit section has 0 findings, and the semantic recall check passes.

Finished on branch `job/search-ui` (not pushed, not merged). Head: `a8d2f644b41c0ebdd2eeff2bf4cfca6c6a3cfa5c`. What is done (#57, one floating window): - The existing ⌘K dialog and provider registry are extended; there is no second palette. The window uses the shared OverlaySurface: animated dim and blur scrim, Esc and click outside close it, focus trap and focus restore, reduced motion. - Compact palette: calternal.js input row and SearchResultRow, grouped sections with counts (five rows each), recents when the query is empty, `>` command mode and ⇧⌘K. A wide screen shows a resizable right-hand preview that follows the arrow keys: note snippet with the hit highlighted, a large photo thumbnail, file metadata with the shared Quick Look viewer on ⌘Y, and the calendar ItemPreview card for log entries and events. Phones get a sheet with result cards; a long press peeks the preview. - ⌘↩ or the expand button widens the same surface into the full view: facets (type, date, tag, folder, person) that toggle pills, a justified photo grid, a virtualized list (more than 60 rows) and infinite scroll (the new `limit` parameter grows 50 → 200). - Stable ordering while typing: answers are kept per provider until that provider answers again, sections have a fixed order, and the active row is kept by id. Client providers answer in the keystroke's task; the server provider debounces 80 ms and is aborted by the next keystroke. - Deep link: `/search?q=…&view=all&scope=calendar` restores the query, its pills and the view. The copy-link button in the field row copies it. - Screen readers: combobox with aria-activedescendant, polite result counts, pill add and remove announcements. Backend changes needed for the window (search crate, API, server): - `SearchHit` has optional `kind`, `path` and `modified`. `GET /api/v1/search` has `limit` (1 to 200). The fan-out no longer cuts the joined list at 20 (the Files name match could hide every index hit) and merges duplicate routes. - Log entries link to `/d/<date>#^<block>`, events to `/e/<id>` (new web route). - Semantic-only hits used raw index paths as links; fixed. A query with operators is keyword-only (the semantic index ignored `in:` and `type:`). Keyword hits rank before semantic-only hits. When keywords matched, semantic neighbours below cosine 0.25 are dropped. Measured (debug server, Playwright headless Chromium on a shared machine; the load average was 12–20 on 8 cores during most runs). Final run, then the range over all runs: - Server round trip `/api/v1/search`, 60 queries at e2e scale: p50 7.4 ms, p95 14.8 ms (range p50 7–40 ms, p95 15–95 ms). - Keystroke to DOM with client results: p50 0.6 ms, p95 5.1 ms (range p95 5–25 ms). - Keystroke to the next animation frame, client results: p50 27.6 ms, p95 73.9 ms. Headless Chromium composites in software under the full-screen scrim blur; this number is the test browser's paint cost, not script time. - Keystroke to server results in the DOM (includes the 80 ms debounce): p50 110 ms, p95 184 ms. - The 100k-item budget was not measured in this job. Tests: `apps/web/e2e/search.mjs` (all steps pass), unit tests for the grammar, snippets, justified rows, dedupe, targets and the shortcut matcher, Rust tests for the fan-out, the fusion and the saved-search routes. The adversarial probe has a new saved-search and limit section. Gates (this worktree): `cargo fmt --check` passed; `cargo clippy --workspace --all-targets -- -D warnings` passed; `cargo test --workspace` passed except `calternal-collab` `shared_notes` (a WebSocket test that failed once and passed on rerun; it does not touch search); `bun run check` 0 errors, 0 warnings; `bun run test` 147 passed; `bun run build` passed; `check-generated.sh` passed; `apps/web/e2e/search.mjs` passed. Adversarial probe: round 2 has 0 findings; round 1 has 6 latency findings only, "Task storm 18–23 SLOW 5.8–8.1 s status 201" (24 concurrent task creates on a shared machine with load average 12–20 on 8 cores; every request succeeded). The new saved-search and limit section has 0 findings, and the semantic recall check passes.
Author
Owner

Hygiene review: the latest report did not measure the 100k-item budget required by this issue. Keeping #57 open until that hot path is measured.

Hygiene review: the latest report did not measure the 100k-item budget required by this issue. Keeping #57 open until that hot path is measured.
Author
Owner

Copy audit finding for the Search window

apps/web/src/lib/components/search-dialog.svelte:108-109 says Still indexing: {progress}%. Some results may be missing. / Still indexing. Some results may be missing. The word indexing is technical. Use Search is still updating: {progress}%. Some results may be missing. / Search is still updating. Some results may be missing.

At :898, Remove the Calendar scope to search everything. uses a technical word. Use Remove the Calendar filter to search everything.

These strings appear in a tooltip and a screen-reader announcement, and in the no-results hint. Keep the progress value and meaning.

Test idea: check the tooltip and screen-reader announcement while Search updates. Check the no-results message with and without the Calendar filter.

Copy audit finding for the Search window `apps/web/src/lib/components/search-dialog.svelte:108-109` says `Still indexing: {progress}%. Some results may be missing.` / `Still indexing. Some results may be missing.` The word `indexing` is technical. Use `Search is still updating: {progress}%. Some results may be missing.` / `Search is still updating. Some results may be missing.` At `:898`, `Remove the Calendar scope to search everything.` uses a technical word. Use `Remove the Calendar filter to search everything.` These strings appear in a tooltip and a screen-reader announcement, and in the no-results hint. Keep the progress value and meaning. Test idea: check the tooltip and screen-reader announcement while Search updates. Check the no-results message with and without the Calendar filter.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#57
No description provided.