Files
WRNexusJS/docs/superpowers/specs/2026-08-07-navigation-components-design.md
T
2026-08-07 16:28:30 +05:30

9.6 KiB
Raw Blame History

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}<a href>{label}</a>{/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 CustomEvents 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 34k, 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 <slot> that renders regardless of active tab
Pagination Build standalone. page/pageSize/total, compact + numbered styles, windowed page numbers. outputs: change, previous, next
Stepper Build. <ol>/<li>, 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 CustomEvents 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