docs: navigation component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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}<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
|
||||||
Reference in New Issue
Block a user