1.5.2 AppLayout + Sidebar primitives (responsive, keyboard-navigable, persistence-aware)
Estimate: 26m · Depends on: 1.0.5.2, 1.5.1
Ship the canonical AppLayout + Sidebar primitives in components/ui/. Data-agnostic — accept children + slot props, no knowledge of workspaces / projects / routes. The wiring to the (authed) layout lands in 1.5.3.
API:
<AppLayout topNav={...} sidebar={...}>{children}</AppLayout>— CSS-grid two-column layout (sidebar · main) at ≥md, single-column with the sidebar off-canvas at <md. Persists the collapsed state via the standard localStorage key (motir.shell.sidebar.collapsed— mirrors the 1.0.5.2 theme key naming).<Sidebar header={...} sections={[...]} footer={...} />— each section is{ id, label, items: [{ icon, label, href, kbd?, active? }] }. Renders nav semantics (<nav aria-label="Primary">), uses Radix'sCollapsiblefor any section the consumer markscollapsible. Active item getsaria-current="page".<SidebarToggle />— the collapse / expand button that reads + writes the same localStorage key. Lives in the sidebar footer for desktop, in the top-nav as a hamburger for mobile (the propvariant: 'footer' | 'hamburger'picks the affordance).<SidebarDrawer />— the mobile off-canvas variant, a RadixDialog.Rootwith a custom scrim + slide-in animation. Closes on route change (consumeusePathname).
Tokens: introduce 4 new --el-* tokens in app/globals.css per the 1.0.5.2 growth principle (minimal tokens, add as needed): --el-sidebar-bg, --el-sidebar-border, --el-sidebar-item-bg-hover, --el-sidebar-item-bg-active. Reference them in the new primitive's classes; document each in /docs/design-system.md.
/tokens specimen: add an "App shell" section to app/tokens/page.tsx rendering the full variant matrix — sidebar expanded, sidebar collapsed (via a local useState control on the page), sample sections with active and hover states. Per Story 1.0.5's convention every new primitive lands in /tokens with its variants.
Keyboard: ⌘\ (Mac) / Ctrl+\ (Win/Linux) toggles the sidebar's collapsed state. Use a single shared keyboard-shortcut hook (lib/hooks/useShortcut.ts) so 1.5.4 can register additional shortcuts consistently.
No data wiring in this Subtask — pure presentational primitives. The (authed) layout migration is 1.5.3.
Acceptance criteria
components/ui/AppLayout.tsx+components/ui/Sidebar.tsx+components/ui/SidebarDrawer.tsxship with the API above; all props strongly typed;forwardRefwhere the primitive wraps a focusable element.- Sidebar persists its collapsed state via
localStorageundermotir.shell.sidebar.collapsed; reads the saved value viauseSyncExternalStore+ lazyuseStateinitializer (React 19 set-state-in-effect rule, established in 1.0.5.2). - Below the
mdbreakpoint the sidebar mounts as aSidebarDrawer(off-canvas Dialog); abovemdit mounts as a persistent column. - Keyboard:
⌘\/Ctrl+\toggles collapse;esccloses the mobile drawer; focus is trapped inside the drawer when open (Radix Dialog handles this); skip-link to#mainrendered at the top ofAppLayout. - 4 new
--el-*tokens added toapp/globals.cssunder the existing tier-4 block; each documented in/docs/design-system.md. app/tokens/page.tsxgrows an "App shell" section showing the primitive's variant matrix (expanded / collapsed / drawer).- Vitest tests under
tests/components/app-layout.test.tsxcover: collapse toggle persistence, drawer open/close on route change, keyboard shortcut firing, skip-link target. - All quality gates green; existing tests + the existing
/tokensroute screenshot regression (if any) still pass.
Context refs
motir-core/CLAUDE.md— 4-layer rule (auto-loaded)/design/shell/*.pen+ PNG exports from 1.5.1 — the visual contractcomponents/ui/Dialog.tsx+Tooltip.tsx+Popover.tsx— primitives to compose fromlib/theme/— the existinguseSyncExternalStore+ localStorage pattern (mirror it)app/tokens/page.tsx— the living-spec route to extend/docs/design-system.md— token + primitive documentation