# 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 `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 `<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 `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
