--- key: spa.sidebar-collapsible-accordions title: '2026-07-18 — Collapsible sidebar + nav-group accordions: two `ctv-sidebar-*` localStorage keys, labeled groups default-collapsed (#396)' status: active since: '2026-07-18' supersedes: none superseded-by: none rule: The shell sidebar's collapse + nav-group-accordion state persists under two hyphenated `ctv-sidebar-*` localStorage keys (matching the repo's `ctv-` convention, not the prototype's dotted names); labeled groups default-collapsed. signals: 'sidebar, collapsible nav groups, localStorage keys · paths: `web/src/app/AppShell.tsx`, `web/src/app/sidebarState.ts` · issues: #396' mechanics: spa-conventions.md §5d; `ctv-sidebar-collapsed` / `ctv-sidebar-groups` keys --- The shell sidebar (`web/src/app/AppShell.tsx`) gained (a) a header toggle that collapses it to a 60px icon rail and (b) collapsible accordions per **labeled** nav group (Media, System); the unlabeled **Primary** group is always open. Mirrors the Claude Design prototype's updated `Sidebar`. - **State lives in a small hook, not App.** `web/src/app/sidebarState.ts` `useSidebarState()` owns both pieces of state + persistence; `AppShell` consumes it (nothing else needs it) and stamps `ctv-app-shell-collapsed` on the shell root so the collapse is CSS-driven from one class. - **Persistence keys use the established `ctv-` hyphen convention, NOT the prototype's dotted names.** The issue quoted `ctv.sidebar.collapsed` / `ctv.sidebar.groups`, but every existing client-local pref is hyphenated (`ctv-theme`, `ctv-logs-page-size` — spa-conventions §5d), so we use **`ctv-sidebar-collapsed`** (`"1"`/`"0"`) and **`ctv-sidebar-groups`** (JSON `{groupKey: boolean}`, boolean = *collapsed*). Deliberate deviation from the issue's literal key text in favour of the repo convention the issue itself points to; helpers validate/parse defensively (bad JSON / non-boolean values → default). - **Labeled groups default to COLLAPSED** (absent `ctv-sidebar-groups` entry ⇒ collapsed), so a fresh load shows only Primary — matching the prototype ("default-collapsed, leaving only Primary visible"). A behavior change for existing users; `App.test.tsx`'s shell/nav suite seeds the two groups open because it clicks Media/System nav links directly (the accordion behavior is covered in its own describe). - **Accordions apply only in the expanded sidebar.** In the rail, group-collapse is ignored — every item renders as an icon (label kept in the a11y tree via an sr-only span so the accessible name/tests survive; surfaced as a native `title` tooltip), groups separated by a hairline divider, numeric badges shown as a corner dot. The active-route indicator (left rail bar + active background) works in both states. - **Group keys are explicit + stable** (`SidebarNavGroupDefinition.key`: `'media'`, `'system'`) rather than derived from the label, so renaming a label doesn't silently orphan persisted state. - No route/screen was added or redirected (shell-chrome only), so no `blazor-route-parity.md` change.