# Spec: Context-scoped search with filter chips and configurable group order > ⚠️ **Superseded in part by > [scoped-search-boundary.md](scoped-search-boundary.md).** The "frontend only, > no Rust changes" decision below (§Background 2, §Design "Scope model" and > "Threading scope through the store") left Jellyfin's item-type taxonomy in the > presentation layer, which violates the backend/frontend boundary. The taxonomy > is being moved into Rust. The **user-facing behaviour and UX in this spec are > unchanged**; only where the scope→item-type mapping and result bucketing live > changes. Read the boundary spec before touching search code. > > **Progress:** the scope→item-type mapping now lives in Rust > (`SearchScope::item_types()`); the frontend sends an opaque scope. Result-side > bucketing (`GROUP_ITEM_TYPES`) is still frontend-side — see > [scoped-search-boundary-implementation.md](scoped-search-boundary-implementation.md) > §Stage 2. **Status:** Implemented (boundary revision: query side done, result side pending) **Scope:** Frontend only. No Rust changes required. *(Revised — see banner.)* **Requirements:** UR-049 → DR-063, DR-064, DR-065; UR-050 → DR-066, DR-067 (see [requirements.md](../requirements.md)). **UX spec:** [ux-flows.md §6](../ux-flows.md) — §6.1 scope, §6.2 layout, §6.3 group order, §6.4 current deviations. ## Summary Two related changes to search: 1. **Scope** — a search started inside a library searches *that* library. Started from Home, `/library`, or the search tab, it searches everything. The active scope shows as a chip row under the search bar, preselected from context and freely changeable without retyping. 2. **Group order** — the order result groups appear in (Songs, Albums, Artists, Movies, TV Shows) becomes a drag-and-drop setting instead of being hardcoded. ## Motivation Searching "office" while browsing TV currently returns music albums, because both search entry points call the same unscoped query. The user has already told us what they're looking at; ignoring that makes search feel indiscriminate and pushes the relevant result below unrelated media. ## Background: what already exists Verified in code — **most of the plumbing is already there.** This is substantially a wiring task, not new infrastructure. 1. **`SearchOptions` already carries the filter.** [bindings.ts](../../src/lib/api/bindings.ts) — `SearchOptions = { limit?, includeItemTypes?, searchTerm? }`. 2. **Rust already honours `include_item_types` on both paths** — online ([online.rs](../../src-tauri/src/repository/online.rs), in the `get_items` options mapping) and offline ([offline.rs](../../src-tauri/src/repository/offline.rs), which builds a SQL type filter from it). **Do not add Rust code for filtering.** 3. **Per-page list search already does this correctly.** [GenericMediaListPage.svelte](../../src/lib/components/library/GenericMediaListPage.svelte) passes `includeItemTypes: [config.itemType]` to `repo.search(...)`. Use it as the reference for the call shape, including the `requestId` handling. 4. **The gap is exactly one function.** [library.ts](../../src/lib/stores/library.ts) — `search(query)` takes only a query and calls `repo.search(query, { limit: 10000 }, requestId)`, dropping any scope. Both callers ([search/+page.svelte](../../src/routes/search/+page.svelte) and [library/+layout.svelte](../../src/routes/library/+layout.svelte)) go through it. 5. **Group order is hardcoded in markup.** [SearchResults.svelte](../../src/lib/components/search/SearchResults.svelte) categorizes into `music{tracks,albums,artists} / movies / tvShows` and renders three fixed sections in source order. 6. **Frontend preferences persist via `localStorage`**, per the existing `viewMode` precedent in [library.ts](../../src/lib/stores/library.ts) (`jellytau-view-mode`). Follow that pattern — **do not** add a Rust settings command for this. ## Design ### Scope model One `SearchScope` type, defined once and shared: | Scope | `includeItemTypes` | Chip label | |-------|--------------------|------------| | `all` | *unset* | All | | `music` | `MusicAlbum`, `MusicArtist`, `Audio`, `Playlist` | Music | | `movies` | `Movie` | Movies | | `tv` | `Series`, `Episode` | TV | `all` must send **no** `includeItemTypes` key rather than a list of every type — the two are not equivalent for item types not enumerated here (Person, folders). ### Route → scope resolution (DR-063) A pure function, unit-testable without a DOM: ```ts resolveSearchScope(pathname: string): SearchScope ``` - `/library/music*` → `music` - `/library/movies*` → `movies` - `/library/tv*` → `tv` - `/`, `/library`, `/search`, anything else → `all` Note `/library/shows/genres` exists as a route; treat `shows` as `tv`. Check the current route list before finalising — do not assume this table is exhaustive. ### Scope is a starting point, not a lock (DR-064) The resolved scope sets the **initial** chip only. Once the user taps a chip, their choice governs until they leave the search surface. Concretely: derive the initial value from the route, hold it in component state, and do not re-derive it on every navigation — otherwise a user who widens to All snaps back to TV. Changing a chip re-runs the current query at the new scope. Changing the query keeps the current scope. ### Threading scope through the store (DR-065) Extend the store's search signature to accept an optional scope and pass `includeItemTypes` down to `repo.search`. Preserve the existing behaviour exactly: the `requestId` bump, the stale-response guard, the `search-event` listener merge, the 10s timeout, and the empty-query clear path. This is an additive parameter — no caller should break. ### Group order (DR-066, DR-067) Persist an ordered array of group ids: ``` ["songs", "albums", "artists", "movies", "tvShows"] // shipped default ``` Rendering composes scope and order as **two independent axes**, in this order: 1. drop groups outside the active scope, 2. sort the remainder by the user's saved order, 3. omit groups that came back empty. Scope never rewrites the saved order — narrowing to Music and back to All must restore the user's full arrangement. See [ux-flows.md §6.3](../ux-flows.md) for the worked example. Settings gets a reorderable list. **Dragging alone is not sufficient**: provide keyboard-operable move up/down controls with proper labels, or the setting is unusable with a screen reader and on any pointerless input. Unknown or missing ids in the stored array must not crash rendering — treat the stored order as a hint, append any group it doesn't mention, and ignore ids that no longer exist. A user upgrading from a build with fewer groups must not lose the new ones. ## Out of scope - Ranking *within* a group. Order is presentation-only. - Server-side search ranking or the Jellyfin query itself. - Scope chips on the per-page list search in `GenericMediaListPage` — that page is already implicitly scoped by its own `itemType`. - Any Rust change. ## Acceptance criteria - [ ] Searching from inside Music returns no movies or TV; from inside TV, no music. - [ ] Searching from Home, `/library`, or the search tab returns all types. - [ ] The chip row renders under the search bar on both the search page and the in-library header search, with the context-derived chip preselected. - [ ] Tapping a chip re-runs the search with the query preserved; editing the query preserves the selected chip. - [ ] Tapping "All" from a context-scoped search widens results without retyping. - [ ] Result groups render in the user's configured order, with out-of-scope and empty groups omitted and relative order preserved. - [ ] Group order is reorderable by drag **and** by keyboard, persists across restarts, and ships with the documented default. - [ ] Offline search respects scope (the offline path already filters — verify, don't reimplement). - [ ] `bun run check` and `bun run test` pass. ## Testing Follow the existing frontend test conventions (vitest, `src/lib/**/*.test.ts`). - `resolveSearchScope` — pure unit tests over the route table, including the `/library/shows/genres` case and unknown routes falling back to `all`. - Scope → `includeItemTypes` mapping, asserting `all` omits the key entirely. - The compose step: scope filter + user order + empty-group omission, including the "narrow then widen restores order" case and a stored order containing an unknown id. - Store-level: scoped search forwards `includeItemTypes` to the repository, and the existing stale-`requestId` guard still discards superseded responses. New requirement-implementing code needs `TRACES:` comments — see [CLAUDE.md](../../CLAUDE.md). Suggested tags: the scope resolver and chip row `UR-049 | DR-063, DR-064`, the store change `UR-049 | DR-065`, the settings list and ordered rendering `UR-050 | DR-066, DR-067`. ## Notes for the implementer - Read [ux-flows.md §6](../ux-flows.md) first — it is the behavioural spec; this document is the implementation plan. - The IPC camelCase rule applies to anything new that crosses the boundary ([CLAUDE.md](../../CLAUDE.md)) — though this change should not add commands. - Another session may be active in this repo. Check `git diff` before "repairing" unexpected changes.