From 3588af83e598e5aa5037414c83d1bee967dcdf95 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 25 Apr 2026 14:49:01 -0400 Subject: [PATCH] docs(spec): add web UI search design Specifies header search bar (debounced, URL-synced ?q=), /search page with three sections of top-10 reusing library components, three overflow routes (/search/artists, /search/albums, /search/tracks) backed by createInfiniteQuery. Track click triggers playRadio with seed via a new /api/radio stub that returns the seed alone (M4 fills in real similarity-based selection later, response shape final). Adds +queue button on TrackRow and AlbumCard via new enqueueTrack / enqueueTracks player-store actions. --- .../specs/2026-04-25-web-ui-search-design.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-25-web-ui-search-design.md diff --git a/docs/superpowers/specs/2026-04-25-web-ui-search-design.md b/docs/superpowers/specs/2026-04-25-web-ui-search-design.md new file mode 100644 index 00000000..fc831bdb --- /dev/null +++ b/docs/superpowers/specs/2026-04-25-web-ui-search-design.md @@ -0,0 +1,189 @@ +# Web UI Search — Design Spec + +**Status:** approved 2026-04-25 +**Slice:** M6 sub-plan #5 (after Player). Backend `GET /api/search` already exists; this slice ships the SPA UI plus a small `/api/radio` stub. + +## Goal + +Replace the `/search` placeholder with a working search experience: a persistent header search box, live debounced results across artists/albums/tracks, per-facet overflow pages, click-to-play-as-radio for tracks, and an "add to queue" affordance on tracks and albums. + +## Non-goals + +- Real radio recommendations. The `/api/radio` endpoint ships as a stub returning `{ tracks: [seed] }`. The full M4 implementation (similarity-driven candidate pool + scoring + 80% queue refresh) lands later; only the request/response contract is locked here. +- Search-result pre-fetch / typeahead suggestions / autocomplete. Plain results page only. +- Mobile-specific layout polish. Header bar fits on desktop; mobile responsive pass is its own future slice. +- Keyboard shortcuts (e.g. Cmd-K to focus search). Out of scope. +- Cross-page search history / recent-searches list. + +## Architecture + +A persistent `SearchInput` component lives in `Shell.svelte`'s header. It binds a local `value` state, debounces 250 ms, and uses `goto()` to keep `/search?q=…` synced. First nav from elsewhere is a normal push; subsequent typing while on `/search` uses `replaceState` so back-navigation skips the typing log. Empty input on `/search` clears the param. + +`/search/+page.svelte` reads `?q=` reactively and runs a TanStack Query against `GET /api/search?q=…&limit=10`, which returns three small paged facets. The page renders up to three sections (Artists / Albums / Tracks), each reusing the existing library components. Each section also renders a "See all N →" link to its overflow page when `total > items.length`. Empty facets are not rendered. + +Three overflow pages — `/search/artists/`, `/search/albums/`, `/search/tracks/` — each read `?q=` and run their own `createInfiniteQuery` against the same `/api/search` endpoint with `limit=50`. They render only their own facet, ignoring the other two facets in the response (minor over-fetch, simpler than three new endpoints). "Load more" pulls the next page. + +The Track click handler on the search results page calls a new player-store action `playRadio(seedTrackId)` which fetches `/api/radio?seed_track=…` and pipes the response into `playQueue(tracks, 0)`. On the album-detail page, Track clicks keep their current behavior (queue the album). + +A new `+ queue` button on each track row and album card calls `enqueueTrack` / `enqueueTracks`, which append to the existing `_queue` rune state without disturbing the current playback or `_index`. + +## API contracts + +### `GET /api/search?q=&limit=&offset=` (existing — no change) + +```ts +type SearchResponse = { + artists: Page; + albums: Page; + tracks: Page; +}; +type Page = { items: T[]; total: number; limit: number; offset: number }; +``` + +Server applies the shared `limit/offset` to each facet's `LIMIT/OFFSET` query independently. `q` is required; empty/whitespace is `400`. Default `limit` is bounded server-side; this slice sends `10` from the main `/search` page and `50` from each overflow page. + +### `GET /api/radio?seed_track=` (new — stub) + +```ts +type RadioResponse = { tracks: TrackRef[] }; +``` + +Stub behavior: `200` with `{ tracks: [] }` (one entry, the seed itself). `404 { code: 'not_found' }` if the track id doesn't exist. `400 { code: 'bad_request' }` if `seed_track` is missing or empty. Same `TrackRef` shape used everywhere else; `resolveTrackRefs` builds the response. + +When M4 fills the endpoint, the response shape is final: extra entries appended after the seed, same `TrackRef` shape. + +## Components & files + +### New + +| Path | Responsibility | +|---|---| +| `web/src/lib/components/SearchInput.svelte` | Header search box; debounced URL sync via `goto`; pre-fills from `page.url.searchParams` | +| `web/src/lib/components/SearchSkeleton.svelte` | Three-section placeholder (artist list / album grid / track list shapes) for `/search` loading state | +| `web/src/routes/search/+page.svelte` | Main results page — reads `?q=`, fires search query, renders three sections | +| `web/src/routes/search/artists/+page.svelte` | Artists overflow with `createInfiniteQuery` + Load more | +| `web/src/routes/search/albums/+page.svelte` | Albums overflow with `createInfiniteQuery` + Load more | +| `web/src/routes/search/tracks/+page.svelte` | Tracks overflow with `createInfiniteQuery` + Load more | +| `web/src/lib/components/SearchInput.test.ts` | Debounce / pre-fill / clear behavior | +| `web/src/routes/search/search.test.ts` | Page integration: 3 sections, empty hide, "See all" link, no-results, empty `?q=` prompt | +| `web/src/routes/search/artists/artists.test.ts` | Overflow tests (analogous for `albums.test.ts`, `tracks.test.ts`) | +| `internal/api/radio.go` | Stub handler | +| `internal/api/radio_test.go` | Stub tests (200/404/400) | + +### Modified + +| Path | Change | +|---|---| +| `web/src/lib/components/Shell.svelte` | Insert `` in the header between brand and user-menu | +| `web/src/lib/components/TrackRow.svelte` | Refactor outer element from `