MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-54

1.5.2 AppLayout + Sidebar primitives (responsive, keyboard-navigable, persistence-aware)

Done
Description

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's Collapsible for any section the consumer marks collapsible. Active item gets aria-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 prop variant: 'footer' | 'hamburger' picks the affordance).
  • <SidebarDrawer /> — the mobile off-canvas variant, a Radix Dialog.Root with a custom scrim + slide-in animation. Closes on route change (consume usePathname).

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.tsx ship with the API above; all props strongly typed; forwardRef where the primitive wraps a focusable element.
  • Sidebar persists its collapsed state via localStorage under motir.shell.sidebar.collapsed; reads the saved value via useSyncExternalStore + lazy useState initializer (React 19 set-state-in-effect rule, established in 1.0.5.2).
  • Below the md breakpoint the sidebar mounts as a SidebarDrawer (off-canvas Dialog); above md it mounts as a persistent column.
  • Keyboard: ⌘\ / Ctrl+\ toggles collapse; esc closes the mobile drawer; focus is trapped inside the drawer when open (Radix Dialog handles this); skip-link to #main rendered at the top of AppLayout.
  • 4 new --el-* tokens added to app/globals.css under the existing tier-4 block; each documented in /docs/design-system.md.
  • app/tokens/page.tsx grows an "App shell" section showing the primitive's variant matrix (expanded / collapsed / drawer).
  • Vitest tests under tests/components/app-layout.test.tsx cover: collapse toggle persistence, drawer open/close on route change, keyboard shortcut firing, skip-link target.
  • All quality gates green; existing tests + the existing /tokens route 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 contract
  • components/ui/Dialog.tsx + Tooltip.tsx + Popover.tsx — primitives to compose from
  • lib/theme/ — the existing useSyncExternalStore + localStorage pattern (mirror it)
  • app/tokens/page.tsx — the living-spec route to extend
  • /docs/design-system.md — token + primitive documentation
Status
Done
Type
Sub-task