diff --git a/docs/superpowers/specs/2026-08-07-navigation-components-design.md b/docs/superpowers/specs/2026-08-07-navigation-components-design.md new file mode 100644 index 00000000..7549219f --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-navigation-components-design.md @@ -0,0 +1,184 @@ +# Navigation component group — design + +Date: 2026-08-07 +Status: approved, not yet implemented + +## Problem + +The navigation group has nine components. Five of them — Nav, MegaMenu, +Scrollspy, Pagination, Stepper — are byte-identical scaffold stubs that differ +only in one CSS class name. Each renders `{#each items}{label}{/each}` +and nothing else. MegaMenu has no panel, Scrollspy never observes scroll, +Stepper has no steps or progress, Pagination has no pages. + +Of the remaining four, Breadcrumb is genuinely built (323 lines, own style +block). Navbar and Sidebar render but have no keyboard handling and keep their +styles in `ui.css`. Tabs works but is off-pattern in three ways: it is the only +component in the library using Tailwind utility classes rather than `wire-*` + +a local style block, it has no `outputs` block and fires raw `CustomEvent`s via +`dispatchEvent`, and it sets a roving `tabindex` with no `@keydown` handler at +all — which is worse than having no keyboard support, because the roving +tabindex makes every inactive tab unreachable by Tab while arrows do nothing. + +## Scope + +All nine components, delivered in three phases. + +## Decisions + +### Styling + +Every navigation component gets a local `style {}` block using `wire-*` +classes. This matches DataTable, Toaster, Modal, Drawer, ContextMenu and +Breadcrumb — the 22 of 108 components that carry local styles are exactly the +recently built ones. Navbar and Sidebar styles move out of `ui.css` as part of +this work. + +### Dropdowns: Nav vs MegaMenu + +Both get dropdowns, of deliberately different kinds. + +- **Nav** gets multi-level cascading submenus. It is the link bar, so nested + submenus are its job. +- **MegaMenu** gets a single-level rich panel: columns of grouped links with + headings, descriptions and icons. This is deliberate, not a shortcut. A mega + menu exists to show breadth flat so everything is one click away; nesting + inside the panel buries content behind hover-within-hover and is close to + unusable by keyboard and touch. + +Neither reuses the existing Dropdown component: Dropdown is click-triggered and +has no nesting support. + +**Depth limit.** There is no recursive-component precedent in this library, so +multi-level means fixed depth via nested `{#each}` loops — the pattern Navbar +(4 loops) and Sidebar (2 loops) already use. Depth is **3 levels**. Unlimited +nesting would require proving out component self-reference, which is out of +scope here. + +### Roving focus lives in the runtime + +Arrow-key roving focus is identical logic for Tabs, Nav, MegaMenu, Sidebar and +Stepper. It goes in the reactive runtime as a declarative attribute rather than +five near-identical client functions: + +- `data-wrn-roving="horizontal|vertical|both"` on the container +- `[data-wrn-roving-item]` on each focusable child + +The runtime owns arrow keys, Home/End, wrap-around, skip-disabled, and +maintenance of the roving `tabindex`. Components declare intent only. + +This follows the modal-dialog focus work already in the runtime, and for the +same reason: a client function cannot hold focus state across callbacks, +because state written after the function returns is dropped. + +### Scrollspy needs runtime support too + +`data-wrn-scrollspy` backed by `IntersectionObserver`. It cannot be a client +function — the observer callback fires long after the function returns, and +that state write would be lost. + +### Runtime budget + +Roving focus is roughly 3–4k, scrollspy roughly 1.5k. The runtime is at 167k +against the 175k ceiling raised on 2026-08-07. This fits, but leaves little +room. The group after this one forces the decision about splitting the runtime +into loadable chunks so pages pay only for behaviour they use. + +### Tabs URL mode + +`mode="client" | "url"`. URL mode uses a query parameter (`?tab=value`) driven +by `history.pushState`, so content swaps with no page load and the back button +works. + +Query parameter rather than hash: it is shareable, survives reload, and does +not collide with in-page anchors or with Scrollspy, which wants the hash. + +Tabs also gets a panel transition on change. + +### Sidebar composes Drawer + +Sidebar does not reimplement off-canvas behaviour. Desktop renders a static +rail; mobile renders inside Drawer. This inherits Drawer's focus trap and +scroll lock rather than duplicating them. + +### Cross-cutting requirements + +- Icons on every menu item, tab, and step +- Dropdown arrows that animate on open +- Responsive behaviour per component: Nav collapses to a toggle, MegaMenu + stacks its panel, Tabs becomes a scrollable strip, Stepper goes vertical, + Pagination goes compact +- Full ARIA Authoring Practices patterns for each role, including correct + `aria-current` / `aria-selected` / `aria-expanded` + +## Components + +| Component | Work | +| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Nav | Build. Flat or multi-level (3 deep), horizontal/vertical, `items[{label,href,value,icon,badge,disabled,items}]`, `aria-current="page"`, roving focus, animated arrows, collapses on mobile. `outputs: select` | +| Tabs | Rewrite off Tailwind onto `wire-*` + local styles. Real `outputs {change, select}` replacing raw `dispatchEvent`. Roving + Home/End. `mode="client"\|"url"`. Panel transition. Fix the `` that renders regardless of active tab | +| Pagination | Build standalone. `page`/`pageSize`/`total`, compact + numbered styles, windowed page numbers. `outputs: change, previous, next` | +| Stepper | Build. `
    `/`
  1. `, status derived from `active` index, horizontal/vertical, optionally clickable, `aria-current="step"`. Indexed named slots (`data-slot="step-0"`, `step-1`, …) for custom per-step content, falling back to built-in rendering | +| MegaMenu | Build. Trigger plus a single-level panel of link columns. Panel marked `data-wrn-anchored` so the existing clamp handles viewport containment. Hover and focus open, Escape closes, click-outside closes | +| Scrollspy | Build. Observes section ids, moves `aria-current` to the matching link | +| Navbar | Audit. Styles move to a local block, roving focus on the link group, keyboard for the mobile toggle | +| Sidebar | Audit and extend. Local styles, composes Drawer for mobile, single items / labelled groups / multi-level (3 deep), vertical roving | +| Breadcrumb | Audit only — already the strongest of the nine | + +Component count stays at 108; all nine files already exist. + +## Data flow + +Props in, outputs out, no global state. Every component takes its data as props +and reports interaction through declared `outputs`. + +## Error handling + +`items` arrives as an HTML attribute and is frequently a JSON string, so: + +- A non-array `items` renders empty rather than throwing +- Out-of-range `active` / `page` clamps to bounds + +## Testing + +- Per-component entries in `packages/ui/test/ui.test.ts` covering names, + declared outputs and props +- Showcase profiles for each so every component gets live demos +- Regenerate component reference, catalog, showcase, and the UI visual contract +- Each phase ends with `bun run check:production` green + +## Phases + +Each phase ends green, committed and pushed. + +1. **Runtime and primitives** — roving-focus runtime, Nav, Pagination, Stepper +2. **Composed** — Tabs (rewrite, URL sync, animation), Sidebar (on Drawer), + MegaMenu +3. **Audit and polish** — Navbar, Breadcrumb, Scrollspy, responsive pass, + showcase generation + +## Risks + +- **Scrollspy and MegaMenu** are the only two needing new runtime behaviour. + MegaMenu leans on the anchored clamp, which could not be verified + interactively on 2026-08-07 because the browser pane was degraded + (screenshots timing out, `scrollIntoView` inert). Verify the pane works + before starting phase 2. +- **Tabs is a breaking change.** Replacing raw `CustomEvent`s with declared + outputs changes its public contract; anything listening for the old events + breaks. 0.8.5 shipped on 2026-08-07, so this needs a migration entry in + `packages/cli/src/update.ts`. + +## Codebase constraints to respect + +Hazards this codebase has already hit: + +- No apostrophes in `.wrn` comments — the brace scanner breaks on them +- `/* */` only inside style blocks; `//` is not a CSS comment and silently eats + the following rule +- No deferred state writes in client functions; state written after the + function returns is dropped +- Never call a peer function after an application callback — the wrapper + flushes the entry-time snapshot +- No boolean attributes bound to loop variables +- Package components need explicit imports