Compare commits

...
8 Commits
Author SHA1 Message Date
ClintchizandClaude Opus 5 124da548b8 chore(ui): wire color and size into the new navigation components
Every bundled component must expose color and size; the rewrites dropped
them, which the library-wide invariant test caught. Rather than re-adding
them as dead props, each component now maps color onto an accent variable
that its active, current and focus affordances actually use, and size onto
the root font scale.

Regenerates the component reference, showcase and visual contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:57:10 +05:30
ClintchizandClaude Opus 5 f4960f2fc5 feat(ui): build the Nav component with submenus and roving focus
Replaces a scaffold that rendered bare anchors. Flat or nested to three
levels, icons, badges, disabled items, aria-current on the active link,
arrow-key roving focus, and a disclosure arrow that rotates on open.

Three levels rather than arbitrary depth because this template language has
no component recursion, so each level is written out.

On a phone the bar becomes a toggle and submenus stack inline rather than
floating: a hover-opened overlay cannot be reached on touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:47:20 +05:30
ClintchizandClaude Opus 5 45ef035bae feat(ui): build the Stepper component
Replaces a scaffold that rendered bare anchors with ordered steps: complete,
current and upcoming status derived from the active index, horizontal or
vertical, optional icons, and indexed named slots (step-0, step-1, ...) for
authoring a step body by hand.

Also tightens the roving contract from the previous commit. A template writes
data-wrn-roving="" or data-wrn-roving-item="false" to mean not this time, but
a bare [attr] selector matches either, so a read-only stepper would still have
taken arrow-key focus. The container now requires a named axis, while a bare
item marker still counts as opted in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:44:12 +05:30
ClintchizandClaude Opus 5 b67a5e43eb feat(ui): build the Pagination component
Replaces a scaffold that rendered a bare list of anchors with real page
controls: compact arrows or windowed page numbers, a range summary, and a
change output carrying the requested page. Out-of-range pages clamp rather
than rendering nothing, because page arrives as an HTML attribute and callers
compute it from data that may have shrunk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:41:56 +05:30
ClintchizandClaude Opus 5 7ac6e08544 fix(csr): make dialog visibility testable and cover the focus trap
The focus trap and scroll lock shipped in 0.8.5 gated on
getBoundingClientRect, which the test DOM always reports as zero, so a dialog
never counted as open and none of that behaviour ran under test. focusableWithin
had the same measurement gate and would have found no items even once the
visibility check was fixed.

Both now use the hidden attribute and the data-show marker the components
already emit. Behaviour in a real browser is unchanged; the difference is that
it is now covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:40:30 +05:30
ClintchizandClaude Opus 5 f94004648d feat(csr): runtime-owned roving arrow-key focus
A container marked data-wrn-roving owns its [data-wrn-roving-item]
descendants: one carries tabindex=0 so Tab reaches the group once, and the
arrow keys move within it, with Home/End, wrap-around and skip-disabled.

Written once here rather than five times across Tabs, Nav, MegaMenu, Sidebar
and Stepper, and because focus bookkeeping cannot live in component state --
a client function writing after it returns has that write dropped.

Item visibility is checked via hidden and data-show rather than measured
size: the test DOM reports every element as zero-sized, which is exactly what
left the dialog focus trap uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:38:44 +05:30
ClintchizandClaude Opus 5 e8e1a2623b docs: navigation phase 1 implementation plan
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:35:42 +05:30
ClintchizandClaude Opus 5 ecb93c7116 docs: navigation component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:28:30 +05:30
17 changed files with 3615 additions and 1353 deletions
File diff suppressed because it is too large Load Diff
@@ -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 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 `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
+3 -3
View File
@@ -65,10 +65,10 @@
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
"packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364",
"packages/ui/components/Modal.wrn": "59d4ae9d6dd2700692d9edecfe53ee868e9f864363a4885961d1813cb2bee51f",
"packages/ui/components/Nav.wrn": "78f215c94caf68e0968449a23e23bd3409a689e0c52a6c1770aa8373c767d080",
"packages/ui/components/Nav.wrn": "544483dbe7f2c3107c9c315ac0e7032c0989d8c7ad90e05a27ece12ac0652dfd",
"packages/ui/components/Navbar.wrn": "e68f9d3643e500e43124e9c7a6d4c7f6722657377ea7313f3e3cf5093a81b360",
"packages/ui/components/PageHeader.wrn": "0761235a4924eec09877be40b292d27b70db22d210be7d8e989493165c20df86",
"packages/ui/components/Pagination.wrn": "9169e724f89992dacd10e9492a95438c8016affb39c0fd17a4f6fe26bd21d8b4",
"packages/ui/components/Pagination.wrn": "9ceb74745b159150b015bbbb1974c68e14a7d4b92bd03491cb23b8855aa07983",
"packages/ui/components/PinInput.wrn": "5196f584de8d548a5dfa03688c3c95da3c948cead2662b05e927386ccea74299",
"packages/ui/components/Popover.wrn": "167f6c476cf3114ac5062ecf7739bddd9b5a81b1d209396a3675067dad1577b2",
"packages/ui/components/PortalDashboard.wrn": "037d4300b59c7d60543abc7d4aba5c738efc143e9b61abc48aad0ddfcfe6845b",
@@ -85,7 +85,7 @@
"packages/ui/components/Sidebar.wrn": "05f4bd216ae8bfcee2460fe638e431174d5f3c8e10003f346644e5a2939e7ce5",
"packages/ui/components/SplitHero.wrn": "70e843565ff869bcf413b11b6d9830b2e2bd229863a00904596f71e2dff0e76d",
"packages/ui/components/StatsBar.wrn": "c7d1df3895f25b6d3a2e1012a18b97592c7f33e65418b880d486f20c2d490686",
"packages/ui/components/Stepper.wrn": "4fec0c91a9006319ec27f3198d1176acdfd93abf62d76159aa55c0879bb081d9",
"packages/ui/components/Stepper.wrn": "09f216a44f888421091bd1bce760a076927500a573550d5b2b10c4f27f608e92",
"packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e",
"packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336",
"packages/ui/components/Switch.wrn": "ccb74599fab72b0d68b09a7f1f90b7732cb2f9cbed67a84219e897575ce30c28",
@@ -1,12 +1,14 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page NavDetail {
layout = "showcase"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Nav"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_items = JSON.parse(ctx.url.searchParams.get("pg_items") ?? "[{\"id\":\"nav-0-1\",\"key\":\"nav-0-1\",\"value\":\"primary\",\"label\":\"Primary workflow\",\"title\":\"Primary workflow\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"A configurable sample message.\",\"href\":\"#nav-demo\",\"actionHref\":\"#nav-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":true,\"selected\":true,\"checked\":true,\"disabled\":false,\"outgoing\":false,\"open\":true,\"number\":1,\"count\":16,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 1\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]},{\"id\":\"nav-0-2\",\"key\":\"nav-0-2\",\"value\":\"secondary\",\"label\":\"Additional option\",\"title\":\"Supporting example\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"Another configurable message.\",\"href\":\"#nav-demo\",\"actionHref\":\"#nav-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":false,\"selected\":false,\"checked\":false,\"disabled\":false,\"outgoing\":true,\"open\":true,\"number\":2,\"count\":32,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 2\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]}]")
state playground_active = ctx.url.searchParams.get("pg_active") ?? ""
state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Nav"
state playground_collapsible = (ctx.url.searchParams.get("pg_collapsible") ?? "true") === "true"
state playground_toggleLabel = ctx.url.searchParams.get("pg_toggleLabel") ?? "Nav"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Nav"
@@ -21,16 +23,16 @@ page NavDetail {
<span class="showcase-eyebrow">Navigation component</span>
<h1>Nav</h1>
<p>Theme-aware, responsive nav component.</p>
<div class="detail-badges"><span>7 props</span><span>1 slots</span><span>2 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>9 props</span><span>1 slots</span><span>1 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="Nav" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="select,change">
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="Nav" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="select">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Nav</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Nav size='{playground_size}' color='{playground_color}' label='{playground_label}' items='{playground_items}' active='{playground_active}' orientation='{playground_orientation}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Nav color='{playground_color}' size='{playground_size}' items='{playground_items}' active='{playground_active}' orientation='{playground_orientation}' label='{playground_label}' collapsible='{playground_collapsible}' toggleLabel='{playground_toggleLabel}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Nav
@@ -142,14 +144,16 @@ page NavDetail {
]
<span>&#125;</span>
]'
label="Nav"
toggleLabel="Nav"
/&gt;</code></pre>
</section>
<div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>7 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-nav-size"><span><strong>size</strong><small>string</small></span><select id="playground-nav-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-nav-color"><span><strong>color</strong><small>string</small></span><select id="playground-nav-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-nav-label"><span><strong>label</strong><small>string</small></span><input id="playground-nav-label" name="pg_label" type="text" value="Nav" /></label><label class="playground-field" for="playground-nav-items"><span><strong>items</strong><small>unknown[]</small></span><textarea id="playground-nav-items" name="pg_items" rows="5" spellcheck="false" data-playground-json>[
<header><div><span>Component props</span><strong>9 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-nav-color"><span><strong>color</strong><small>string</small></span><select id="playground-nav-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-nav-size"><span><strong>size</strong><small>string</small></span><select id="playground-nav-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-nav-items"><span><strong>items</strong><small>unknown[]</small></span><textarea id="playground-nav-items" name="pg_items" rows="5" spellcheck="false" data-playground-json>[
{
"id": "nav-0-1",
"key": "nav-0-1",
@@ -256,7 +260,7 @@ page NavDetail {
"Standard"
]
}
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-nav-active"><span><strong>active</strong><small>string</small></span><input id="playground-nav-active" name="pg_active" type="text" value="" /></label><label class="playground-field" for="playground-nav-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-nav-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-nav-class"><span><strong>class</strong><small>string</small></span><input id="playground-nav-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-nav-active"><span><strong>active</strong><small>string</small></span><input id="playground-nav-active" name="pg_active" type="text" value="" /></label><label class="playground-field" for="playground-nav-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-nav-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-nav-label"><span><strong>label</strong><small>string</small></span><input id="playground-nav-label" name="pg_label" type="text" value="Nav" /></label><label class="playground-toggle" for="playground-nav-collapsible"><span><strong>collapsible</strong><small>boolean</small></span><input id="playground-nav-collapsible" name="pg_collapsible" type="checkbox" value="true" checked data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-nav-toggleLabel"><span><strong>toggle Label</strong><small>string</small></span><input id="playground-nav-toggleLabel" name="pg_toggleLabel" type="text" value="Nav" /></label><label class="playground-field" for="playground-nav-class"><span><strong>class</strong><small>string</small></span><input id="playground-nav-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -265,404 +269,163 @@ page NavDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Horizontal links</h2><p>A flat link bar. The active item is marked with aria-current, and the arrow keys move between items.</p></div>
<span class="demo-case-number">01</span>
</header>
<div class="demo-workbench">
<div id="nav-demo-1" class="demo-canvas demo-canvas--1">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Nav size="default" label="Nav" items='[{"id":"nav-0-1","key":"nav-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"nav-0-2","key":"nav-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="demo-render"><Nav size="default" items='[{"label":"Home","href":"/","value":"home","icon":"icon-[lucide--house]"},{"label":"Inbox","href":"/inbox","value":"inbox","badge":"9"},{"label":"Reports","href":"/reports","value":"reports"},{"label":"Archive","href":"/archive","value":"archive","disabled":true}]' active="inbox" label="Nav" collapsible="true" toggleLabel="Nav" class="showcase-instance showcase-instance--1" /></div>
</div>
<section class="demo-code" aria-label="Production default usage">
<section class="demo-code" aria-label="Horizontal links usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Nav
items='[
<span>&#123;</span>
"id": "nav-0-1",
"key": "nav-0-1",
"value": "primary",
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Home",
"href": "/",
"value": "home",
"icon": "icon-[lucide--house]"
<span>&#125;</span>,
<span>&#123;</span>
"id": "nav-0-2",
"key": "nav-0-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Inbox",
"href": "/inbox",
"value": "inbox",
"badge": "9"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Reports",
"href": "/reports",
"value": "reports"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Archive",
"href": "/archive",
"value": "archive",
"disabled": true
<span>&#125;</span>
]'
active="inbox"
label="Nav"
toggleLabel="Nav"
/&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div>
<div><span class="demo-case-eyebrow">Advanced</span><h2>Nested submenus</h2><p>An item carrying its own items array opens a submenu on hover or focus, to a maximum of three levels. On a phone the submenus stack inline instead of floating, because a hover-opened overlay is unreachable on touch.</p></div>
<span class="demo-case-number">02</span>
</header>
<div class="demo-workbench">
<div id="nav-demo-2" class="demo-canvas demo-canvas--2">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Nav size="sm" label="Compact example" items='[{"id":"nav-1-1","key":"nav-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"nav-1-2","key":"nav-1-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A compact configuration designed for dense application layouts.","text":"Another configurable message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":48,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--2" /></div>
<div class="demo-render"><Nav size="sm" items='[{"label":"Home","href":"/","value":"home"},{"label":"Products","value":"products","items":[{"label":"Overview","href":"/p","value":"p-overview"},{"label":"Platform","value":"p-platform","items":[{"label":"Runtime","href":"/p/runtime","value":"runtime"},{"label":"Compiler","href":"/p/compiler","value":"compiler"}]}]}]' active="p-overview" label="Compact example" collapsible="true" toggleLabel="Compact example" class="showcase-instance showcase-instance--2" /></div>
</div>
<section class="demo-code" aria-label="Compact application usage">
<section class="demo-code" aria-label="Nested submenus usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Nav
size="sm"
label="Compact example"
items='[
<span>&#123;</span>
"id": "nav-1-1",
"key": "nav-1-1",
"value": "primary",
"label": "Secondary workflow",
"title": "Secondary workflow",
"description": "A compact configuration designed for dense application layouts.",
"text": "A configurable sample message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Home",
"href": "/",
"value": "home"
<span>&#125;</span>,
<span>&#123;</span>
"id": "nav-1-2",
"key": "nav-1-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A compact configuration designed for dense application layouts.",
"text": "Another configurable message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 48,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 2",
"label": "Products",
"value": "products",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
"label": "Overview",
"href": "/p",
"value": "p-overview"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
"label": "Platform",
"value": "p-platform",
"items": [
<span>&#123;</span>
"label": "Runtime",
"href": "/p/runtime",
"value": "runtime"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Compiler",
"href": "/p/compiler",
"value": "compiler"
<span>&#125;</span>
]
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>
]'
active="p-overview"
label="Compact example"
toggleLabel="Compact example"
/&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Vertical rail</h2><p>Vertical orientation switches the arrow keys to up and down.</p></div>
<span class="demo-case-number">03</span>
</header>
<div class="demo-workbench">
<div id="nav-demo-3" class="demo-canvas demo-canvas--3">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Nav size="lg" label="Advanced example" items='[{"id":"nav-2-1","key":"nav-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]},{"id":"nav-2-2","key":"nav-2-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A richer configuration with more supporting information and actions.","text":"Another configurable message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":false,"selected":false,"checked":false,"disabled":true,"outgoing":true,"open":true,"number":2,"count":64,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]}]' class="showcase-instance showcase-instance--3" /></div>
<div class="demo-render"><Nav size="lg" items='[{"label":"Dashboard","href":"/","value":"dash","icon":"icon-[lucide--gauge]"},{"label":"Team","href":"/team","value":"team","icon":"icon-[lucide--users]"},{"label":"Settings","href":"/settings","value":"settings","icon":"icon-[lucide--settings]"}]' active="team" orientation="vertical" label="Advanced example" collapsible="false" toggleLabel="Advanced example" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Rich configuration usage">
<section class="demo-code" aria-label="Vertical rail usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Nav
size="lg"
label="Advanced example"
items='[
<span>&#123;</span>
"id": "nav-2-1",
"key": "nav-2-1",
"value": "primary",
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
"label": "Dashboard",
"href": "/",
"value": "dash",
"icon": "icon-[lucide--gauge]"
<span>&#125;</span>,
<span>&#123;</span>
"id": "nav-2-2",
"key": "nav-2-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A richer configuration with more supporting information and actions.",
"text": "Another configurable message.",
"href": "#nav-demo",
"actionHref": "#nav-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": false,
"selected": false,
"checked": false,
"disabled": true,
"outgoing": true,
"open": true,
"number": 2,
"count": 64,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
"label": "Team",
"href": "/team",
"value": "team",
"icon": "icon-[lucide--users]"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Settings",
"href": "/settings",
"value": "settings",
"icon": "icon-[lucide--settings]"
<span>&#125;</span>
]'
active="team"
orientation="vertical"
label="Advanced example"
collapsible="false"
toggleLabel="Advanced example"
/&gt;</code></pre>
</section>
</div>
</article></div></section>
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@select</code><span>Fires when an item, option, card, or result is selected.</span></div><div><code>@change</code><span>Fires when the component's committed value or selection changes.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Nav
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@select</code><span>Fires when an item, option, card, or result is selected.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Nav
@select='console.log(payload)'
@change='console.log(payload)'
/&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs"
const component = document.querySelector("[data-ui-component=\"Nav\"], [data-component=\"Nav\"]")
if (component) registerOutputHandler(component, "select", (payload) =&gt; <span>&#123;</span>
console.log("select", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "change", (payload) =&gt; <span>&#123;</span>
console.log("change", payload)
<span>&#125;</span>)</code></pre></section></div></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Nav"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Main"</code></td><td>No</td></tr><tr><td><code>collapsible</code></td><td>boolean</td><td><code>true</code></td><td>No</td></tr><tr><td><code>toggleLabel</code></td><td>string</td><td><code>"Menu"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#nav-demo-1">Production default</a><a href="#nav-demo-2">Compact application</a><a href="#nav-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#nav-demo-1">Horizontal links</a><a href="#nav-demo-2">Nested submenus</a><a href="#nav-demo-3">Vertical rail</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside>
</div>
<nav class="detail-pagination">
@@ -1,12 +1,17 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page PaginationDetail {
layout = "showcase"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_page = Number(ctx.url.searchParams.get("pg_page") ?? "3")
state playground_pageSize = Number(ctx.url.searchParams.get("pg_pageSize") ?? "3")
state playground_total = Number(ctx.url.searchParams.get("pg_total") ?? "3")
state playground_variant = ctx.url.searchParams.get("pg_variant") ?? "compact"
state playground_siblingCount = Number(ctx.url.searchParams.get("pg_siblingCount") ?? "3")
state playground_showSummary = (ctx.url.searchParams.get("pg_showSummary") ?? "true") === "true"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Pagination"
state playground_items = JSON.parse(ctx.url.searchParams.get("pg_items") ?? "[{\"id\":\"pagination-0-1\",\"key\":\"pagination-0-1\",\"value\":\"primary\",\"label\":\"Primary workflow\",\"title\":\"Primary workflow\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"A configurable sample message.\",\"href\":\"#pagination-demo\",\"actionHref\":\"#pagination-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":true,\"selected\":true,\"checked\":true,\"disabled\":false,\"outgoing\":false,\"open\":true,\"number\":1,\"count\":16,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 1\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]},{\"id\":\"pagination-0-2\",\"key\":\"pagination-0-2\",\"value\":\"secondary\",\"label\":\"Additional option\",\"title\":\"Supporting example\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"Another configurable message.\",\"href\":\"#pagination-demo\",\"actionHref\":\"#pagination-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":false,\"selected\":false,\"checked\":false,\"disabled\":false,\"outgoing\":true,\"open\":true,\"number\":2,\"count\":32,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 2\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]}]")
state playground_active = ctx.url.searchParams.get("pg_active") ?? ""
state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_previousLabel = ctx.url.searchParams.get("pg_previousLabel") ?? "Pagination"
state playground_nextLabel = ctx.url.searchParams.get("pg_nextLabel") ?? "Pagination"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Pagination"
@@ -21,7 +26,7 @@ page PaginationDetail {
<span class="showcase-eyebrow">Navigation component</span>
<h1>Pagination</h1>
<p>Theme-aware, responsive pagination component.</p>
<div class="detail-badges"><span>7 props</span><span>1 slots</span><span>3 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>12 props</span><span>1 slots</span><span>3 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
@@ -30,233 +35,24 @@ page PaginationDetail {
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Pagination size='{playground_size}' color='{playground_color}' label='{playground_label}' items='{playground_items}' active='{playground_active}' orientation='{playground_orientation}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Pagination color='{playground_color}' size='{playground_size}' page='{playground_page}' pageSize='{playground_pageSize}' total='{playground_total}' variant='{playground_variant}' siblingCount='{playground_siblingCount}' showSummary='{playground_showSummary}' label='{playground_label}' previousLabel='{playground_previousLabel}' nextLabel='{playground_nextLabel}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Pagination
items='[
<span>&#123;</span>
"id": "pagination-0-1",
"key": "pagination-0-1",
"value": "primary",
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "pagination-0-2",
"key": "pagination-0-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>
]'
page="3"
pageSize="3"
total="3"
siblingCount="3"
previousLabel="Pagination"
nextLabel="Pagination"
/&gt;</code></pre>
</section>
<div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>7 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-pagination-size"><span><strong>size</strong><small>string</small></span><select id="playground-pagination-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-pagination-color"><span><strong>color</strong><small>string</small></span><select id="playground-pagination-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-pagination-label"><span><strong>label</strong><small>string</small></span><input id="playground-pagination-label" name="pg_label" type="text" value="Pagination" /></label><label class="playground-field" for="playground-pagination-items"><span><strong>items</strong><small>unknown[]</small></span><textarea id="playground-pagination-items" name="pg_items" rows="5" spellcheck="false" data-playground-json>[
{
"id": "pagination-0-1",
"key": "pagination-0-1",
"value": "primary",
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
{
"label": "Nested option A",
"value": "nested-a"
},
{
"label": "Nested option B",
"value": "nested-b"
}
],
"links": [
{
"label": "Documentation",
"href": "#documentation"
},
{
"label": "API reference",
"href": "#api-reference"
}
],
"values": [
"Included",
"Standard"
]
},
{
"id": "pagination-0-2",
"key": "pagination-0-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
{
"label": "Nested option A",
"value": "nested-a"
},
{
"label": "Nested option B",
"value": "nested-b"
}
],
"links": [
{
"label": "Documentation",
"href": "#documentation"
},
{
"label": "API reference",
"href": "#api-reference"
}
],
"values": [
"Included",
"Standard"
]
}
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-pagination-active"><span><strong>active</strong><small>string</small></span><input id="playground-pagination-active" name="pg_active" type="text" value="" /></label><label class="playground-field" for="playground-pagination-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-pagination-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-pagination-class"><span><strong>class</strong><small>string</small></span><input id="playground-pagination-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
<header><div><span>Component props</span><strong>12 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-pagination-color"><span><strong>color</strong><small>string</small></span><select id="playground-pagination-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-pagination-size"><span><strong>size</strong><small>string</small></span><select id="playground-pagination-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-pagination-page"><span><strong>page</strong><small>number</small></span><input id="playground-pagination-page" name="pg_page" type="number" value="3" /></label><label class="playground-field" for="playground-pagination-pageSize"><span><strong>page Size</strong><small>number</small></span><input id="playground-pagination-pageSize" name="pg_pageSize" type="number" value="3" /></label><label class="playground-field" for="playground-pagination-total"><span><strong>total</strong><small>number</small></span><input id="playground-pagination-total" name="pg_total" type="number" value="3" /></label><label class="playground-field" for="playground-pagination-variant"><span><strong>variant</strong><small>string</small></span><input id="playground-pagination-variant" name="pg_variant" type="text" value="compact" /></label><label class="playground-field" for="playground-pagination-siblingCount"><span><strong>sibling Count</strong><small>number</small></span><input id="playground-pagination-siblingCount" name="pg_siblingCount" type="number" value="3" /></label><label class="playground-toggle" for="playground-pagination-showSummary"><span><strong>show Summary</strong><small>boolean</small></span><input id="playground-pagination-showSummary" name="pg_showSummary" type="checkbox" value="true" checked data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-pagination-label"><span><strong>label</strong><small>string</small></span><input id="playground-pagination-label" name="pg_label" type="text" value="Pagination" /></label><label class="playground-field" for="playground-pagination-previousLabel"><span><strong>previous Label</strong><small>string</small></span><input id="playground-pagination-previousLabel" name="pg_previousLabel" type="text" value="Pagination" /></label><label class="playground-field" for="playground-pagination-nextLabel"><span><strong>next Label</strong><small>string</small></span><input id="playground-pagination-nextLabel" name="pg_nextLabel" type="text" value="Pagination" /></label><label class="playground-field" for="playground-pagination-class"><span><strong>class</strong><small>string</small></span><input id="playground-pagination-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -265,382 +61,75 @@ page PaginationDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Compact arrows</h2><p>The default: previous and next with a page counter, and a summary of the range in view.</p></div>
<span class="demo-case-number">01</span>
</header>
<div class="demo-workbench">
<div id="pagination-demo-1" class="demo-canvas demo-canvas--1">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Pagination size="default" label="Pagination" items='[{"id":"pagination-0-1","key":"pagination-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"pagination-0-2","key":"pagination-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="demo-render"><Pagination size="default" page="2" pageSize="10" total="137" variant="compact" siblingCount="3" showSummary="true" label="Pagination" previousLabel="Pagination" nextLabel="Pagination" class="showcase-instance showcase-instance--1" /></div>
</div>
<section class="demo-code" aria-label="Production default usage">
<section class="demo-code" aria-label="Compact arrows usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Pagination
items='[
<span>&#123;</span>
"id": "pagination-0-1",
"key": "pagination-0-1",
"value": "primary",
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "pagination-0-2",
"key": "pagination-0-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>
]'
page="2"
total="137"
siblingCount="3"
previousLabel="Pagination"
nextLabel="Pagination"
/&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Numbered pages</h2><p>Page numbers windowed around the current page with ellipsis gaps, so a large set never renders hundreds of buttons.</p></div>
<span class="demo-case-number">02</span>
</header>
<div class="demo-workbench">
<div id="pagination-demo-2" class="demo-canvas demo-canvas--2">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Pagination size="sm" label="Compact example" items='[{"id":"pagination-1-1","key":"pagination-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"pagination-1-2","key":"pagination-1-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A compact configuration designed for dense application layouts.","text":"Another configurable message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":48,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--2" /></div>
<div class="demo-render"><Pagination size="sm" page="5" pageSize="10" total="200" variant="numbered" siblingCount="1" showSummary="true" label="Compact example" previousLabel="Compact example" nextLabel="Compact example" class="showcase-instance showcase-instance--2" /></div>
</div>
<section class="demo-code" aria-label="Compact application usage">
<section class="demo-code" aria-label="Numbered pages usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Pagination
size="sm"
page="5"
total="200"
variant="numbered"
label="Compact example"
items='[
<span>&#123;</span>
"id": "pagination-1-1",
"key": "pagination-1-1",
"value": "primary",
"label": "Secondary workflow",
"title": "Secondary workflow",
"description": "A compact configuration designed for dense application layouts.",
"text": "A configurable sample message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "pagination-1-2",
"key": "pagination-1-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A compact configuration designed for dense application layouts.",
"text": "Another configurable message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 48,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
<span>&#125;</span>
]'
previousLabel="Compact example"
nextLabel="Compact example"
/&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div>
<div><span class="demo-case-eyebrow">Advanced</span><h2>Wider window</h2><p>siblingCount widens how many pages sit either side of the current one.</p></div>
<span class="demo-case-number">03</span>
</header>
<div class="demo-workbench">
<div id="pagination-demo-3" class="demo-canvas demo-canvas--3">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Pagination size="lg" label="Advanced example" items='[{"id":"pagination-2-1","key":"pagination-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]},{"id":"pagination-2-2","key":"pagination-2-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A richer configuration with more supporting information and actions.","text":"Another configurable message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":false,"selected":false,"checked":false,"disabled":true,"outgoing":true,"open":true,"number":2,"count":64,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]}]' class="showcase-instance showcase-instance--3" /></div>
<div class="demo-render"><Pagination size="lg" page="8" pageSize="25" total="900" variant="numbered" siblingCount="2" showSummary="true" label="Advanced example" previousLabel="Advanced example" nextLabel="Advanced example" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Rich configuration usage">
<section class="demo-code" aria-label="Wider window usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Pagination
size="lg"
page="8"
pageSize="25"
total="900"
variant="numbered"
siblingCount="2"
label="Advanced example"
items='[
<span>&#123;</span>
"id": "pagination-2-1",
"key": "pagination-2-1",
"value": "primary",
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span>,
<span>&#123;</span>
"id": "pagination-2-2",
"key": "pagination-2-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A richer configuration with more supporting information and actions.",
"text": "Another configurable message.",
"href": "#pagination-demo",
"actionHref": "#pagination-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": false,
"selected": false,
"checked": false,
"disabled": true,
"outgoing": true,
"open": true,
"number": 2,
"count": 64,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
<span>&#125;</span>
]'
previousLabel="Advanced example"
nextLabel="Advanced example"
/&gt;</code></pre>
</section>
</div>
@@ -664,10 +153,10 @@ if (component) registerOutputHandler(component, "previous", (payload) =&gt; <spa
if (component) registerOutputHandler(component, "next", (payload) =&gt; <span>&#123;</span>
console.log("next", payload)
<span>&#125;</span>)</code></pre></section></div></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Pagination"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>page</code></td><td>number</td><td><code>1</code></td><td>No</td></tr><tr><td><code>pageSize</code></td><td>number</td><td><code>10</code></td><td>No</td></tr><tr><td><code>total</code></td><td>number</td><td><code>0</code></td><td>No</td></tr><tr><td><code>variant</code></td><td>string</td><td><code>"compact"</code></td><td>No</td></tr><tr><td><code>siblingCount</code></td><td>number</td><td><code>1</code></td><td>No</td></tr><tr><td><code>showSummary</code></td><td>boolean</td><td><code>true</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Pagination"</code></td><td>No</td></tr><tr><td><code>previousLabel</code></td><td>string</td><td><code>"Previous"</code></td><td>No</td></tr><tr><td><code>nextLabel</code></td><td>string</td><td><code>"Next"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#pagination-demo-1">Production default</a><a href="#pagination-demo-2">Compact application</a><a href="#pagination-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#pagination-demo-1">Compact arrows</a><a href="#pagination-demo-2">Numbered pages</a><a href="#pagination-demo-3">Wider window</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside>
</div>
<nav class="detail-pagination">
@@ -1,12 +1,13 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page StepperDetail {
layout = "showcase"
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Stepper"
state playground_items = JSON.parse(ctx.url.searchParams.get("pg_items") ?? "[{\"id\":\"stepper-0-1\",\"key\":\"stepper-0-1\",\"value\":\"primary\",\"label\":\"Primary workflow\",\"title\":\"Primary workflow\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"A configurable sample message.\",\"href\":\"#stepper-demo\",\"actionHref\":\"#stepper-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":true,\"selected\":true,\"checked\":true,\"disabled\":false,\"outgoing\":false,\"open\":true,\"number\":1,\"count\":16,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 1\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]},{\"id\":\"stepper-0-2\",\"key\":\"stepper-0-2\",\"value\":\"secondary\",\"label\":\"Additional option\",\"title\":\"Supporting example\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"Another configurable message.\",\"href\":\"#stepper-demo\",\"actionHref\":\"#stepper-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":false,\"selected\":false,\"checked\":false,\"disabled\":false,\"outgoing\":true,\"open\":true,\"number\":2,\"count\":32,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 2\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]}]")
state playground_active = ctx.url.searchParams.get("pg_active") ?? ""
state playground_size = ctx.url.searchParams.get("pg_size") ?? "default"
state playground_steps = JSON.parse(ctx.url.searchParams.get("pg_steps") ?? "[{\"id\":\"stepper-0-1\",\"key\":\"stepper-0-1\",\"value\":\"primary\",\"label\":\"Primary workflow\",\"title\":\"Primary workflow\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"A configurable sample message.\",\"href\":\"#stepper-demo\",\"actionHref\":\"#stepper-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":true,\"selected\":true,\"checked\":true,\"disabled\":false,\"outgoing\":false,\"open\":true,\"number\":1,\"count\":16,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 1\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]},{\"id\":\"stepper-0-2\",\"key\":\"stepper-0-2\",\"value\":\"secondary\",\"label\":\"Additional option\",\"title\":\"Supporting example\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"Another configurable message.\",\"href\":\"#stepper-demo\",\"actionHref\":\"#stepper-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":false,\"selected\":false,\"checked\":false,\"disabled\":false,\"outgoing\":true,\"open\":true,\"number\":2,\"count\":32,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 2\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]}]")
state playground_active = Number(ctx.url.searchParams.get("pg_active") ?? "0")
state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal"
state playground_clickable = (ctx.url.searchParams.get("pg_clickable") ?? "false") === "true"
state playground_label = ctx.url.searchParams.get("pg_label") ?? "Stepper"
state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1"
seo {
title = "Stepper"
@@ -21,20 +22,20 @@ page StepperDetail {
<span class="showcase-eyebrow">Navigation component</span>
<h1>Stepper</h1>
<p>Theme-aware, responsive stepper component.</p>
<div class="detail-badges"><span>7 props</span><span>1 slots</span><span>4 outputs</span><span>Theme ready</span><span>Responsive</span></div>
<div class="detail-badges"><span>8 props</span><span>2 slots</span><span>1 outputs</span><span>Theme ready</span><span>Responsive</span></div>
</div>
<div class="detail-hero-icon"><span class="icon-[lucide--component] size-10" aria-hidden="true"></span></div>
</header>
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="Stepper" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="change,previous,next,complete">
<section id="playground" class="detail-section playground-section" data-playground data-playground-component="Stepper" data-playground-public-tag="true" data-playground-has-slot="true" data-playground-events="change">
<div class="detail-section-heading"><span>Interactive playground</span><h2>Configure Stepper</h2><p>Change any prop and inspect the server-rendered component immediately.</p></div>
<div class="playground-workbench">
<div class="playground-preview">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="playground-preview-source" data-playground-preview><Stepper size='{playground_size}' color='{playground_color}' label='{playground_label}' items='{playground_items}' active='{playground_active}' orientation='{playground_orientation}' class='{playground_class}' /></div>
<div class="playground-preview-source" data-playground-preview><Stepper color='{playground_color}' size='{playground_size}' steps='{playground_steps}' active='{playground_active}' orientation='{playground_orientation}' clickable='{playground_clickable}' label='{playground_label}' class='{playground_class}' /></div>
<section class="playground-code" aria-label="Current component code">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component code</span><button type="button" data-playground-copy><span class="icon-[lucide--copy] size-4" aria-hidden="true"></span>Copy</button></header>
<pre><code data-playground-code>&lt;Stepper
items='[
steps='[
<span>&#123;</span>
"id": "stepper-0-1",
"key": "stepper-0-1",
@@ -142,14 +143,18 @@ page StepperDetail {
]
<span>&#125;</span>
]'
/&gt;</code></pre>
label="Stepper"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
<div class="playground-status" data-playground-status aria-live="polite"></div>
<div class="playground-event-log" data-playground-event-log aria-live="polite"><strong>Event output</strong><span>Interact with the preview to inspect the typed <code>payload</code>.</span></div>
</div>
<form class="playground-controls" data-playground-form>
<header><div><span>Component props</span><strong>7 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-stepper-size"><span><strong>size</strong><small>string</small></span><select id="playground-stepper-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-stepper-color"><span><strong>color</strong><small>string</small></span><select id="playground-stepper-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-stepper-label"><span><strong>label</strong><small>string</small></span><input id="playground-stepper-label" name="pg_label" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-items"><span><strong>items</strong><small>unknown[]</small></span><textarea id="playground-stepper-items" name="pg_items" rows="5" spellcheck="false" data-playground-json>[
<header><div><span>Component props</span><strong>8 controls</strong></div><button type="reset"><span class="icon-[lucide--rotate-ccw] size-4" aria-hidden="true"></span>Reset</button></header>
<div class="playground-fields"><label class="playground-field" for="playground-stepper-color"><span><strong>color</strong><small>string</small></span><select id="playground-stepper-color" name="pg_color"><option value="primary" selected>primary</option><option value="secondary">secondary</option><option value="success">success</option><option value="warning">warning</option><option value="danger">danger</option><option value="info">info</option></select></label><label class="playground-field" for="playground-stepper-size"><span><strong>size</strong><small>string</small></span><select id="playground-stepper-size" name="pg_size"><option value="default" selected>default</option><option value="xs">xs</option><option value="sm">sm</option><option value="md">md</option><option value="lg">lg</option><option value="xl">xl</option><option value="icon">icon</option><option value="icon-xs">icon-xs</option><option value="icon-sm">icon-sm</option><option value="icon-lg">icon-lg</option></select></label><label class="playground-field" for="playground-stepper-steps"><span><strong>steps</strong><small>unknown[]</small></span><textarea id="playground-stepper-steps" name="pg_steps" rows="5" spellcheck="false" data-playground-json>[
{
"id": "stepper-0-1",
"key": "stepper-0-1",
@@ -256,7 +261,7 @@ page StepperDetail {
"Standard"
]
}
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-stepper-active"><span><strong>active</strong><small>string</small></span><input id="playground-stepper-active" name="pg_active" type="text" value="" /></label><label class="playground-field" for="playground-stepper-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-stepper-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-field" for="playground-stepper-class"><span><strong>class</strong><small>string</small></span><input id="playground-stepper-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
]</textarea><em data-playground-error></em></label><label class="playground-field" for="playground-stepper-active"><span><strong>active</strong><small>number</small></span><input id="playground-stepper-active" name="pg_active" type="number" value="0" /></label><label class="playground-field" for="playground-stepper-orientation"><span><strong>orientation</strong><small>string</small></span><select id="playground-stepper-orientation" name="pg_orientation"><option value="horizontal" selected>horizontal</option><option value="vertical">vertical</option></select></label><label class="playground-toggle" for="playground-stepper-clickable"><span><strong>clickable</strong><small>boolean</small></span><input id="playground-stepper-clickable" name="pg_clickable" type="checkbox" value="true" data-playground-boolean /><i aria-hidden="true"></i></label><label class="playground-field" for="playground-stepper-label"><span><strong>label</strong><small>string</small></span><input id="playground-stepper-label" name="pg_label" type="text" value="Stepper" /></label><label class="playground-field" for="playground-stepper-class"><span><strong>class</strong><small>string</small></span><input id="playground-stepper-class" name="pg_class" type="text" value="showcase-instance showcase-instance--1" /></label></div>
</form>
</div>
</section>
@@ -265,414 +270,128 @@ page StepperDetail {
<section class="detail-section"><div class="detail-section-heading"><span>Live examples</span><h2>Designed for real product surfaces</h2><p>Compare configurations and resize the browser to check responsive behavior.</p></div><div class="demo-list">
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Recommended</span><h2>Production default</h2><p>Balanced spacing, hierarchy, and content for the most common product workflow.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Horizontal progress</h2><p>Steps before the active one read as complete, the active one is highlighted, and the rest are muted.</p></div>
<span class="demo-case-number">01</span>
</header>
<div class="demo-workbench">
<div id="stepper-demo-1" class="demo-canvas demo-canvas--1">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="default" label="Stepper" items='[{"id":"stepper-0-1","key":"stepper-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"stepper-0-2","key":"stepper-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="demo-render"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" class="showcase-instance showcase-instance--1" /></div>
</div>
<section class="demo-code" aria-label="Production default usage">
<section class="demo-code" aria-label="Horizontal progress usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Stepper
items='[
steps='[
<span>&#123;</span>
"id": "stepper-0-1",
"key": "stepper-0-1",
"value": "primary",
"label": "Primary workflow",
"title": "Primary workflow",
"description": "A clean default configuration for everyday product interfaces.",
"text": "A configurable sample message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 16,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Account",
"description": "Your details"
<span>&#125;</span>,
<span>&#123;</span>
"id": "stepper-0-2",
"key": "stepper-0-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A clean default configuration for everyday product interfaces.",
"text": "Another configurable message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--sparkles]",
"iconClass": "icon-[lucide--sparkles]",
"variant": "primary",
"status": "Active",
"time": "09:30",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 32,
"percentage": 72,
"color": "#7c3aed",
"target": "_self",
"ariaLabel": "Open primary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Billing",
"description": "Payment method"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Confirm",
"description": "Review and submit"
<span>&#125;</span>
]'
/&gt;</code></pre>
active="1"
label="Stepper"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Dense UI</span><h2>Compact application</h2><p>A tighter variation for dashboards, side panels, tables, and operational interfaces.</p></div>
<div><span class="demo-case-eyebrow">Recommended</span><h2>Vertical with icons</h2><p>Vertical orientation suits a sidebar or a narrow column. Any step may carry an iconify class instead of its number.</p></div>
<span class="demo-case-number">02</span>
</header>
<div class="demo-workbench">
<div id="stepper-demo-2" class="demo-canvas demo-canvas--2">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="sm" label="Compact example" items='[{"id":"stepper-1-1","key":"stepper-1-1","value":"primary","label":"Secondary workflow","title":"Secondary workflow","description":"A compact configuration designed for dense application layouts.","text":"A configurable sample message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":24,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"stepper-1-2","key":"stepper-1-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A compact configuration designed for dense application layouts.","text":"Another configurable message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--zap]","iconClass":"icon-[lucide--zap]","variant":"secondary","status":"Active","time":"10:15","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":48,"percentage":48,"color":"#0284c7","target":"_self","ariaLabel":"Open secondary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--2" /></div>
<div class="demo-render"><Stepper size="sm" steps='[{"label":"Cloned","icon":"icon-[lucide--git-branch]"},{"label":"Built","icon":"icon-[lucide--hammer]"},{"label":"Deployed","icon":"icon-[lucide--rocket]"}]' active="2" orientation="vertical" clickable="false" label="Compact example" class="showcase-instance showcase-instance--2" /></div>
</div>
<section class="demo-code" aria-label="Compact application usage">
<section class="demo-code" aria-label="Vertical with icons usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Stepper
size="sm"
label="Compact example"
items='[
steps='[
<span>&#123;</span>
"id": "stepper-1-1",
"key": "stepper-1-1",
"value": "primary",
"label": "Secondary workflow",
"title": "Secondary workflow",
"description": "A compact configuration designed for dense application layouts.",
"text": "A configurable sample message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 24,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Cloned",
"icon": "icon-[lucide--git-branch]"
<span>&#125;</span>,
<span>&#123;</span>
"id": "stepper-1-2",
"key": "stepper-1-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A compact configuration designed for dense application layouts.",
"text": "Another configurable message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "View details",
"icon": "icon-[lucide--zap]",
"iconClass": "icon-[lucide--zap]",
"variant": "secondary",
"status": "Active",
"time": "10:15",
"current": false,
"selected": false,
"checked": false,
"disabled": false,
"outgoing": true,
"open": true,
"number": 2,
"count": 48,
"percentage": 48,
"color": "#0284c7",
"target": "_self",
"ariaLabel": "Open secondary workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Standard"
]
"label": "Built",
"icon": "icon-[lucide--hammer]"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Deployed",
"icon": "icon-[lucide--rocket]"
<span>&#125;</span>
]'
/&gt;</code></pre>
active="2"
orientation="vertical"
label="Compact example"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
</div>
</article>
<article class="demo-case">
<header class="demo-case-header">
<div><span class="demo-case-eyebrow">Extended</span><h2>Rich configuration</h2><p>A more expressive variation using additional data, stronger emphasis, and optional states.</p></div>
<div><span class="demo-case-eyebrow">Advanced</span><h2>Clickable steps</h2><p>With clickable the steps emit a change output and take arrow-key roving focus. Without it the stepper is read-only and stays out of the tab order.</p></div>
<span class="demo-case-number">03</span>
</header>
<div class="demo-workbench">
<div id="stepper-demo-3" class="demo-canvas demo-canvas--3">
<div class="demo-browser-chrome"><span></span><span></span><span></span><small>Live preview</small></div>
<div class="demo-render"><Stepper size="lg" label="Advanced example" items='[{"id":"stepper-2-1","key":"stepper-2-1","value":"primary","label":"Advanced workflow","title":"Advanced workflow","description":"A richer configuration with more supporting information and actions.","text":"A configurable sample message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":32,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]},{"id":"stepper-2-2","key":"stepper-2-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A richer configuration with more supporting information and actions.","text":"Another configurable message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"Explore workflow","icon":"icon-[lucide--circle-check]","iconClass":"icon-[lucide--circle-check]","variant":"success","status":"Complete","time":"11:45","current":false,"selected":false,"checked":false,"disabled":true,"outgoing":true,"open":true,"number":2,"count":64,"percentage":91,"color":"#059669","target":"_self","ariaLabel":"Open advanced workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Unlimited"]}]' class="showcase-instance showcase-instance--3" /></div>
<div class="demo-render"><Stepper size="lg" steps='[{"label":"One"},{"label":"Two"},{"label":"Three"}]' active="0" clickable="true" label="Advanced example" class="showcase-instance showcase-instance--3" /></div>
</div>
<section class="demo-code" aria-label="Rich configuration usage">
<section class="demo-code" aria-label="Clickable steps usage">
<header><span><span class="icon-[lucide--code-2] size-4" aria-hidden="true"></span>Component usage</span><small>.wrn</small></header>
<pre><code>&lt;Stepper
size="lg"
label="Advanced example"
items='[
steps='[
<span>&#123;</span>
"id": "stepper-2-1",
"key": "stepper-2-1",
"value": "primary",
"label": "Advanced workflow",
"title": "Advanced workflow",
"description": "A richer configuration with more supporting information and actions.",
"text": "A configurable sample message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": true,
"selected": true,
"checked": true,
"disabled": false,
"outgoing": false,
"open": true,
"number": 1,
"count": 32,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 1",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
"label": "One"
<span>&#125;</span>,
<span>&#123;</span>
"id": "stepper-2-2",
"key": "stepper-2-2",
"value": "secondary",
"label": "Additional option",
"title": "Supporting example",
"description": "A richer configuration with more supporting information and actions.",
"text": "Another configurable message.",
"href": "#stepper-demo",
"actionHref": "#stepper-demo",
"actionLabel": "Explore workflow",
"icon": "icon-[lucide--circle-check]",
"iconClass": "icon-[lucide--circle-check]",
"variant": "success",
"status": "Complete",
"time": "11:45",
"current": false,
"selected": false,
"checked": false,
"disabled": true,
"outgoing": true,
"open": true,
"number": 2,
"count": 64,
"percentage": 91,
"color": "#059669",
"target": "_self",
"ariaLabel": "Open advanced workflow 2",
"items": [
<span>&#123;</span>
"label": "Nested option A",
"value": "nested-a"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Nested option B",
"value": "nested-b"
<span>&#125;</span>
],
"links": [
<span>&#123;</span>
"label": "Documentation",
"href": "#documentation"
<span>&#125;</span>,
<span>&#123;</span>
"label": "API reference",
"href": "#api-reference"
<span>&#125;</span>
],
"values": [
"Included",
"Unlimited"
]
"label": "Two"
<span>&#125;</span>,
<span>&#123;</span>
"label": "Three"
<span>&#125;</span>
]'
/&gt;</code></pre>
clickable="true"
label="Advanced example"&gt;
&lt;div data-slot="step-<span>&#123;</span>index<span>&#125;</span>"&gt;
&lt;div class="showcase-slot"&gt;step-<span>&#123;</span>index<span>&#125;</span> slot&lt;/div&gt;
&lt;/div&gt;
&lt;/Stepper&gt;</code></pre>
</section>
</div>
</article></div></section>
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@change</code><span>Fires when the component's committed value or selection changes.</span></div><div><code>@previous</code><span>Fires when the component emits the previous event.</span></div><div><code>@next</code><span>Fires when the component emits the next event.</span></div><div><code>@complete</code><span>Fires when the component emits the complete event.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Stepper
<section id="events" class="detail-section"><div class="detail-section-heading"><span>Component outputs</span><h2>Receive every typed component output</h2><p>Use declarative output handlers in <code>.wrn</code> files or register a direct output handler from JavaScript. The canonical API exposes <code>payload</code> and does not require <code>event.detail</code>.</p></div><div class="detail-events-grid"><div class="detail-events-list"><div><code>@change</code><span>Fires when the component's committed value or selection changes.</span></div></div><div class="detail-event-examples"><section class="demo-code"><header><span><span class="icon-[lucide--braces] size-4" aria-hidden="true"></span>Declarative handlers</span><small>.wrn</small></header><pre><code>&lt;Stepper
@change='console.log(payload)'
@previous='console.log(payload)'
@next='console.log(payload)'
@complete='console.log(payload)'
/&gt;</code></pre></section><section class="demo-code"><header><span><span class="icon-[lucide--radio] size-4" aria-hidden="true"></span>Direct output handlers</span><small>.js</small></header><pre><code>import <span>&#123;</span> registerOutputHandler <span>&#125;</span> from "@wrnexus/csr/outputs"
const component = document.querySelector("[data-ui-component=\"Stepper\"], [data-component=\"Stepper\"]")
if (component) registerOutputHandler(component, "change", (payload) =&gt; <span>&#123;</span>
console.log("change", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "previous", (payload) =&gt; <span>&#123;</span>
console.log("previous", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "next", (payload) =&gt; <span>&#123;</span>
console.log("next", payload)
<span>&#125;</span>)
if (component) registerOutputHandler(component, "complete", (payload) =&gt; <span>&#123;</span>
console.log("complete", payload)
<span>&#125;</span>)</code></pre></section></div></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Stepper"</code></td><td>No</td></tr><tr><td><code>items</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>string</td><td><code>""</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
<section id="api" class="detail-section"><div class="detail-section-heading"><span>Component API</span><h2>Props and configuration</h2><p>All content and behavior shown above is supplied through these props and slots.</p></div><div class="docs-table-wrap"><table class="docs-table"><thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Required</th></tr></thead><tbody><tr><td><code>color</code></td><td>string</td><td><code>"primary"</code></td><td>No</td></tr><tr><td><code>size</code></td><td>string</td><td><code>"default"</code></td><td>No</td></tr><tr><td><code>steps</code></td><td>unknown[]</td><td><code>[]</code></td><td>No</td></tr><tr><td><code>active</code></td><td>number</td><td><code>0</code></td><td>No</td></tr><tr><td><code>orientation</code></td><td>string</td><td><code>"horizontal"</code></td><td>No</td></tr><tr><td><code>clickable</code></td><td>boolean</td><td><code>false</code></td><td>No</td></tr><tr><td><code>label</code></td><td>string</td><td><code>"Progress"</code></td><td>No</td></tr><tr><td><code>class</code></td><td>string</td><td><code>""</code></td><td>No</td></tr></tbody></table></div></section>
</main>
<aside class="detail-aside">
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#stepper-demo-1">Production default</a><a href="#stepper-demo-2">Compact application</a><a href="#stepper-demo-3">Rich configuration</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
<div class="detail-toc"><strong>On this page</strong><a href="#playground">Playground</a><a href="#stepper-demo-1">Horizontal progress</a><a href="#stepper-demo-2">Vertical with icons</a><a href="#stepper-demo-3">Clickable steps</a><a href="#events">Outputs</a><a href="#api">Props API</a></div>
</aside>
</div>
<nav class="detail-pagination">
@@ -46,11 +46,11 @@ page NavigationShowcase {
<article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Nav size="default" label="Nav" items='[{"id":"nav-0-1","key":"nav-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"nav-0-2","key":"nav-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#nav-demo","actionHref":"#nav-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="catalog-card-stage"><Nav size="default" items='[{"label":"Home","href":"/","value":"home","icon":"icon-[lucide--house]"},{"label":"Inbox","href":"/inbox","value":"inbox","badge":"9"},{"label":"Reports","href":"/reports","value":"reports"},{"label":"Archive","href":"/archive","value":"archive","disabled":true}]' active="inbox" label="Nav" collapsible="true" toggleLabel="Nav" class="showcase-instance showcase-instance--1" /></div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Navigation</span><h2>Nav</h2><p>Theme-aware, responsive nav component.</p></div>
<div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/nav">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>9 props · 1 slots</span><a href="/components/nav">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -66,11 +66,11 @@ page NavigationShowcase {
<article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Pagination size="default" label="Pagination" items='[{"id":"pagination-0-1","key":"pagination-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"pagination-0-2","key":"pagination-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#pagination-demo","actionHref":"#pagination-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="catalog-card-stage"><Pagination size="default" page="2" pageSize="10" total="137" variant="compact" siblingCount="3" showSummary="true" label="Pagination" previousLabel="Pagination" nextLabel="Pagination" class="showcase-instance showcase-instance--1" /></div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Navigation</span><h2>Pagination</h2><p>Theme-aware, responsive pagination component.</p></div>
<div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/pagination">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>12 props · 1 slots</span><a href="/components/pagination">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -96,11 +96,11 @@ page NavigationShowcase {
<article class="catalog-card" data-interactive-preview="false">
<div class="catalog-card-preview">
<div class="catalog-card-glow" aria-hidden="true"></div>
<div class="catalog-card-stage"><Stepper size="default" label="Stepper" items='[{"id":"stepper-0-1","key":"stepper-0-1","value":"primary","label":"Primary workflow","title":"Primary workflow","description":"A clean default configuration for everyday product interfaces.","text":"A configurable sample message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":true,"selected":true,"checked":true,"disabled":false,"outgoing":false,"open":true,"number":1,"count":16,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 1","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]},{"id":"stepper-0-2","key":"stepper-0-2","value":"secondary","label":"Additional option","title":"Supporting example","description":"A clean default configuration for everyday product interfaces.","text":"Another configurable message.","href":"#stepper-demo","actionHref":"#stepper-demo","actionLabel":"View details","icon":"icon-[lucide--sparkles]","iconClass":"icon-[lucide--sparkles]","variant":"primary","status":"Active","time":"09:30","current":false,"selected":false,"checked":false,"disabled":false,"outgoing":true,"open":true,"number":2,"count":32,"percentage":72,"color":"#7c3aed","target":"_self","ariaLabel":"Open primary workflow 2","items":[{"label":"Nested option A","value":"nested-a"},{"label":"Nested option B","value":"nested-b"}],"links":[{"label":"Documentation","href":"#documentation"},{"label":"API reference","href":"#api-reference"}],"values":["Included","Standard"]}]' class="showcase-instance showcase-instance--1" /></div>
<div class="catalog-card-stage"><Stepper size="default" steps='[{"label":"Account","description":"Your details"},{"label":"Billing","description":"Payment method"},{"label":"Confirm","description":"Review and submit"}]' active="1" clickable="false" label="Stepper" class="showcase-instance showcase-instance--1" /></div>
</div>
<div class="catalog-card-body">
<div><span class="catalog-card-category">Navigation</span><h2>Stepper</h2><p>Theme-aware, responsive stepper component.</p></div>
<div class="catalog-card-footer"><span>7 props · 1 slots</span><a href="/components/stepper">Explore component <span aria-hidden="true">→</span></a></div>
<div class="catalog-card-footer"><span>8 props · 2 slots</span><a href="/components/stepper">Explore component <span aria-hidden="true">→</span></a></div>
</div>
</article>
<article class="catalog-card" data-interactive-preview="false">
@@ -253,6 +253,120 @@ const DATATABLE_ROWS =
'[{"id": 1, "name": "Northwind", "plan": "Scale", "owner": "A. Okafor", "seats": 6, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 2, "name": "Acme Industrial", "plan": "Team", "owner": "R. Silva", "seats": 13, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 3, "name": "Globex", "plan": "Enterprise", "owner": "M. Chen", "seats": 20, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 4, "name": "Initech", "plan": "Starter", "owner": "J. Dubois", "seats": 27, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 5, "name": "Umbrella", "plan": "Scale", "owner": "P. Novak", "seats": 34, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 6, "name": "Stark Labs", "plan": "Team", "owner": "A. Okafor", "seats": 41, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 7, "name": "Wayne Foods", "plan": "Enterprise", "owner": "R. Silva", "seats": 48, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 8, "name": "Soylent", "plan": "Starter", "owner": "M. Chen", "seats": 55, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 9, "name": "Hooli", "plan": "Scale", "owner": "J. Dubois", "seats": 62, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 10, "name": "Vehement", "plan": "Team", "owner": "P. Novak", "seats": 69, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 11, "name": "Massive Dynamic", "plan": "Enterprise", "owner": "A. Okafor", "seats": 76, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 12, "name": "Cyberdyne", "plan": "Starter", "owner": "R. Silva", "seats": 83, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}, {"id": 13, "name": "Tyrell", "plan": "Scale", "owner": "M. Chen", "seats": 90, "status": "Active", "statusHtml": "<span class=\\"showcase-pill showcase-pill--success\\">Active</span>"}, {"id": 14, "name": "Aperture", "plan": "Team", "owner": "J. Dubois", "seats": 97, "status": "Trial", "statusHtml": "<span class=\\"showcase-pill showcase-pill--info\\">Trial</span>"}, {"id": 15, "name": "Black Mesa", "plan": "Enterprise", "owner": "P. Novak", "seats": 104, "status": "Past due", "statusHtml": "<span class=\\"showcase-pill showcase-pill--danger\\">Past due</span>"}]';
export const componentProfiles = {
Nav: {
demos: [
standard(
"Horizontal links",
"A flat link bar. The active item is marked with aria-current, and the arrow keys move between items.",
{
items: [
{ label: "Home", href: "/", value: "home", icon: "icon-[lucide--house]" },
{ label: "Inbox", href: "/inbox", value: "inbox", badge: "9" },
{ label: "Reports", href: "/reports", value: "reports" },
{ label: "Archive", href: "/archive", value: "archive", disabled: true },
],
active: "inbox",
},
),
advanced(
"Nested submenus",
"An item carrying its own items array opens a submenu on hover or focus, to a maximum of three levels. On a phone the submenus stack inline instead of floating, because a hover-opened overlay is unreachable on touch.",
{
items: [
{ label: "Home", href: "/", value: "home" },
{
label: "Products",
value: "products",
items: [
{ label: "Overview", href: "/p", value: "p-overview" },
{
label: "Platform",
value: "p-platform",
items: [
{ label: "Runtime", href: "/p/runtime", value: "runtime" },
{ label: "Compiler", href: "/p/compiler", value: "compiler" },
],
},
],
},
],
active: "p-overview",
},
),
standard("Vertical rail", "Vertical orientation switches the arrow keys to up and down.", {
items: [
{ label: "Dashboard", href: "/", value: "dash", icon: "icon-[lucide--gauge]" },
{ label: "Team", href: "/team", value: "team", icon: "icon-[lucide--users]" },
{
label: "Settings",
href: "/settings",
value: "settings",
icon: "icon-[lucide--settings]",
},
],
active: "team",
orientation: "vertical",
collapsible: false,
}),
],
},
Stepper: {
demos: [
standard(
"Horizontal progress",
"Steps before the active one read as complete, the active one is highlighted, and the rest are muted.",
{
steps: [
{ label: "Account", description: "Your details" },
{ label: "Billing", description: "Payment method" },
{ label: "Confirm", description: "Review and submit" },
],
active: 1,
},
),
standard(
"Vertical with icons",
"Vertical orientation suits a sidebar or a narrow column. Any step may carry an iconify class instead of its number.",
{
steps: [
{ label: "Cloned", icon: "icon-[lucide--git-branch]" },
{ label: "Built", icon: "icon-[lucide--hammer]" },
{ label: "Deployed", icon: "icon-[lucide--rocket]" },
],
active: 2,
orientation: "vertical",
},
),
advanced(
"Clickable steps",
"With clickable the steps emit a change output and take arrow-key roving focus. Without it the stepper is read-only and stays out of the tab order.",
{
steps: [{ label: "One" }, { label: "Two" }, { label: "Three" }],
active: 0,
clickable: true,
},
),
],
},
Pagination: {
demos: [
standard(
"Compact arrows",
"The default: previous and next with a page counter, and a summary of the range in view.",
{ page: 2, pageSize: 10, total: 137, variant: "compact" },
),
standard(
"Numbered pages",
"Page numbers windowed around the current page with ellipsis gaps, so a large set never renders hundreds of buttons.",
{ page: 5, pageSize: 10, total: 200, variant: "numbered", siblingCount: 1 },
),
advanced(
"Wider window",
"siblingCount widens how many pages sit either side of the current one.",
{ page: 8, pageSize: 25, total: 900, variant: "numbered", siblingCount: 2 },
),
],
},
DataTable: {
demos: [
standard(
@@ -835,10 +835,10 @@
"category": "navigation",
"purpose": "Theme-aware, responsive nav component.",
"demoCount": 3,
"propCount": 7,
"propCount": 9,
"slots": ["default"],
"events": ["select", "change"],
"profiled": false
"events": ["select"],
"profiled": true
},
{
"name": "Navbar",
@@ -871,10 +871,10 @@
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component.",
"demoCount": 3,
"propCount": 7,
"propCount": 12,
"slots": ["default"],
"events": ["change", "previous", "next"],
"profiled": false
"profiled": true
},
{
"name": "PinInput",
@@ -1111,10 +1111,10 @@
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component.",
"demoCount": 3,
"propCount": 7,
"slots": ["default"],
"events": ["change", "previous", "next", "complete"],
"profiled": false
"propCount": 8,
"slots": ["step-{index}", "default"],
"events": ["change"],
"profiled": true
},
{
"name": "StrongPassword",
+157 -5
View File
@@ -2927,11 +2927,18 @@ export const REACTIVE_RUNTIME = String.raw`
var dialogRestoreFocus = null;
var dialogScrollLock = null;
/*
* Open/closed is expressed by the data-show marker these components already
* emit, not by measured size. Measuring looks more thorough but made the
* trap and the scroll lock impossible to cover: the test DOM reports every
* element as zero-sized, so a dialog never counted as open and none of this
* behaviour ran under test.
*/
function isDialogVisible(dialog) {
if (!dialog || !dialog.getBoundingClientRect) return false;
if (!dialog || !dialog.isConnected) return false;
if (dialog.hasAttribute("hidden")) return false;
if (dialog.closest('[data-show="false"]')) return false;
var rect = dialog.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
return true;
}
function focusableWithin(dialog) {
@@ -2939,8 +2946,10 @@ export const REACTIVE_RUNTIME = String.raw`
var candidates = dialog.querySelectorAll(FOCUSABLE_SELECTOR);
for (var index = 0; index < candidates.length; index += 1) {
var candidate = candidates[index];
var rect = candidate.getBoundingClientRect();
if (rect.width > 0 || rect.height > 0) found.push(candidate);
// Same rule as the roving items: markers, not measurement.
if (candidate.hasAttribute("hidden")) continue;
if (candidate.closest('[data-show="false"]')) continue;
found.push(candidate);
}
return found;
}
@@ -3047,6 +3056,148 @@ export const REACTIVE_RUNTIME = String.raw`
syncDialogs();
}
/*
* Roving arrow-key focus, the ARIA pattern shared by tabs, menus, sidebars
* and steppers. A container marked data-wrn-roving owns its
* [data-wrn-roving-item] descendants: exactly one carries tabindex="0" so
* Tab reaches the group once, and the arrow keys move within it.
*
* This lives here rather than in each component because it is the same code
* five times over, and because focus bookkeeping cannot be held in component
* state -- a client function writing after it returns has that write dropped.
*/
var ROVING_SELECTOR = "[data-wrn-roving]";
var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]";
/*
* These attributes are written by templates, so they arrive stringified:
* data-wrn-roving="" or data-wrn-roving-item="false" is how a component
* says "not this time". A bare [attr] selector matches either, so the value
* has to be checked -- otherwise a stepper with clickable=false still takes
* arrow-key focus.
*/
// A bare data-wrn-roving-item means yes; only an explicit "false" opts out.
function rovingItemOff(value) {
return value === null || value === "false";
}
/*
* The container is stricter: it must name an axis. An empty value is what a
* template emits for {cond ? "horizontal" : ""}, so empty means off rather
* than defaulting to horizontal.
*/
function rovingOrientation(container) {
var value = container.getAttribute("data-wrn-roving");
if (value === null || value === "" || value === "false") return "";
return value;
}
function rovingItems(container) {
var found = [];
var candidates = container.querySelectorAll(ROVING_ITEM_SELECTOR);
for (var index = 0; index < candidates.length; index += 1) {
var candidate = candidates[index];
if (rovingItemOff(candidate.getAttribute("data-wrn-roving-item"))) continue;
// A nested group owns its own items; do not steal them.
if (candidate.closest(ROVING_SELECTOR) !== container) continue;
if (candidate.hasAttribute("disabled")) continue;
if (candidate.getAttribute("aria-disabled") === "true") continue;
/*
* Deliberately not a size check. The test DOM reports every element as
* zero-sized, so measuring here would make the whole feature impossible
* to cover -- the same trap the dialog visibility check fell into.
*/
if (candidate.hasAttribute("hidden")) continue;
if (candidate.closest('[data-show="false"]')) continue;
found.push(candidate);
}
return found;
}
function rovingActiveIndex(items) {
for (var index = 0; index < items.length; index += 1) {
var item = items[index];
if (item.getAttribute("aria-selected") === "true") return index;
var current = item.getAttribute("aria-current");
if (current === "page" || current === "step" || current === "true") return index;
}
return 0;
}
function applyRovingTabindex(items, activeIndex) {
for (var index = 0; index < items.length; index += 1) {
items[index].setAttribute("tabindex", index === activeIndex ? "0" : "-1");
}
}
function syncRovingGroup(container) {
if (!rovingOrientation(container)) return;
var items = rovingItems(container);
if (!items.length) return;
applyRovingTabindex(items, rovingActiveIndex(items));
}
function syncRovingGroups() {
var groups = document.querySelectorAll(ROVING_SELECTOR);
for (var index = 0; index < groups.length; index += 1) syncRovingGroup(groups[index]);
}
function handleRovingKeydown(event) {
var target = event.target;
if (!target || !target.closest) return;
var item = target.closest(ROVING_ITEM_SELECTOR);
if (!item) return;
var container = item.closest(ROVING_SELECTOR);
if (!container) return;
var items = rovingItems(container);
var index = items.indexOf(item);
if (index === -1) return;
var orientation = rovingOrientation(container);
if (!orientation) return;
var horizontal = orientation === "horizontal" || orientation === "both";
var vertical = orientation === "vertical" || orientation === "both";
var key = event.key;
var next = -1;
if ((horizontal && key === "ArrowRight") || (vertical && key === "ArrowDown")) {
next = (index + 1) % items.length;
} else if ((horizontal && key === "ArrowLeft") || (vertical && key === "ArrowUp")) {
next = (index - 1 + items.length) % items.length;
} else if (key === "Home") {
next = 0;
} else if (key === "End") {
next = items.length - 1;
} else {
return;
}
event.preventDefault();
applyRovingTabindex(items, next);
if (items[next].focus) items[next].focus();
}
function setupRovingFocus() {
if (window.__wrnexusRovingBound) return;
window.__wrnexusRovingBound = true;
document.addEventListener("keydown", handleRovingKeydown, true);
if (typeof MutationObserver === "function") {
new MutationObserver(function () {
window.setTimeout(syncRovingGroups, 0);
}).observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["aria-selected", "aria-current", "disabled", "aria-disabled", "hidden"],
});
}
syncRovingGroups();
}
function hydrateScopes(root) {
var host = root || document;
@@ -5264,6 +5415,7 @@ export const REACTIVE_RUNTIME = String.raw`
window.__wrnexusHydrateAsyncBoundaries = hydrateAsyncBoundaries;
setupAnchoredOverlays();
setupModalDialogs();
setupRovingFocus();
window.__wrnexusRepositionAnchored = repositionAnchored;
window.__wrnexusHydrateScopes = hydrateScopes;
window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); };
+158
View File
@@ -894,3 +894,161 @@ test("comments are ignored inside interpreted statements", () => {
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("2");
});
/*
* happy-dom builds its own KeyboardEvent, which is structurally distinct from
* the DOM lib Event that dispatchEvent is typed against. Same cast the window
* dispatches above use, kept in one place.
*/
function keydown(win: Window, key: string): Event {
const ctor = (win as unknown as { KeyboardEvent: new (type: string, init: unknown) => unknown })
.KeyboardEvent;
return new ctor("keydown", { key, bubbles: true }) as unknown as Event;
}
test("roving focus moves with arrow keys and wraps", () => {
const win = mount(
`<div data-wrn-roving="horizontal">
<button data-wrn-roving-item id="a">A</button>
<button data-wrn-roving-item id="b">B</button>
<button data-wrn-roving-item id="c">C</button>
</div>`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
const c = doc.querySelector("#c") as unknown as HTMLElement;
expect(a.getAttribute("tabindex")).toBe("0");
expect(doc.querySelector("#b")!.getAttribute("tabindex")).toBe("-1");
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("b");
expect(doc.querySelector("#b")!.getAttribute("tabindex")).toBe("0");
expect(a.getAttribute("tabindex")).toBe("-1");
(doc.querySelector("#b") as unknown as HTMLElement).dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("c");
c.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("a");
});
test("roving focus honours Home and End and skips disabled items", () => {
const win = mount(
`<div data-wrn-roving="vertical">
<button data-wrn-roving-item id="a">A</button>
<button data-wrn-roving-item id="b" disabled>B</button>
<button data-wrn-roving-item id="c">C</button>
</div>`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(doc.activeElement!.id).toBe("c");
(doc.querySelector("#c") as unknown as HTMLElement).dispatchEvent(keydown(win, "Home"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(keydown(win, "End"));
expect(doc.activeElement!.id).toBe("c");
});
test("horizontal roving ignores vertical arrows so the page still scrolls", () => {
const win = mount(
`<div data-wrn-roving="horizontal">
<button data-wrn-roving-item id="a">A</button>
<button data-wrn-roving-item id="b">B</button>
</div>`,
);
const a = win.document.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(win.document.activeElement!.id).toBe("a");
});
test("roving tabindex starts on the selected item, not the first", () => {
const win = mount(
`<div data-wrn-roving="horizontal">
<button data-wrn-roving-item id="a">A</button>
<button data-wrn-roving-item id="b" aria-selected="true">B</button>
</div>`,
);
expect(win.document.querySelector("#b")!.getAttribute("tabindex")).toBe("0");
expect(win.document.querySelector("#a")!.getAttribute("tabindex")).toBe("-1");
});
test("nested roving groups do not capture the outer group items", () => {
const win = mount(
`<div data-wrn-roving="horizontal" id="outer">
<button data-wrn-roving-item id="a">A</button>
<div data-wrn-roving="vertical" id="inner">
<button data-wrn-roving-item id="x">X</button>
<button data-wrn-roving-item id="y">Y</button>
</div>
<button data-wrn-roving-item id="b">B</button>
</div>`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("b");
});
test("opening a modal dialog traps Tab inside it", () => {
const win = mount(
`<div>
<button id="outside">Outside</button>
<div data-show="true">
<section role="dialog" aria-modal="true" tabindex="-1">
<button id="first">First</button>
<button id="last">Last</button>
</section>
</div>
</div>`,
);
const doc = win.document;
const last = doc.querySelector("#last") as unknown as HTMLElement;
last.focus();
last.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("first");
});
test("a hidden dialog does not trap Tab", () => {
const win = mount(
`<div>
<button id="outside">Outside</button>
<div data-show="false">
<section role="dialog" aria-modal="true" tabindex="-1">
<button id="first">First</button>
</section>
</div>
</div>`,
);
const doc = win.document;
const outside = doc.querySelector("#outside") as unknown as HTMLElement;
outside.focus();
outside.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("outside");
});
test("an empty or false roving attribute opts the group out entirely", () => {
const win = mount(
`<div data-wrn-roving="">
<button data-wrn-roving-item="false" id="a">A</button>
<button data-wrn-roving-item="false" id="b">B</button>
</div>`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
expect(a.getAttribute("tabindex")).toBeNull();
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("a");
});
+7 -7
View File
@@ -871,9 +871,9 @@ Theme-aware, responsive mega menu component.
Theme-aware, responsive nav component.
- Mount: `data-component="Nav"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Nav"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `label: string = "Main"`, `collapsible: boolean = true`, `toggleLabel: string = "Menu"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
- Outputs: `select({ item: object; value: string })`
### Navbar
@@ -889,9 +889,9 @@ Theme-aware, responsive navbar component.
Theme-aware, responsive pagination component.
- Mount: `data-component="Pagination"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Pagination"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `page: number = 1`, `pageSize: number = 10`, `total: number = 0`, `variant: string = "compact"`, `siblingCount: number = 1`, `showSummary: boolean = true`, `label: string = "Pagination"`, `previousLabel: string = "Previous"`, `nextLabel: string = "Next"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Outputs: `change({ page: number; pageSize: number })`, `previous({ page: number })`, `next({ page: number })`
### Scrollspy
@@ -916,9 +916,9 @@ Theme-aware, responsive sidebar component.
Theme-aware, responsive stepper component.
- Mount: `data-component="Stepper"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Stepper"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
- Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `class: string = ""`
- Slots: `step-{index}`, `default`
- Outputs: `change({ index: number; step: object })`
### Tabs
+99 -59
View File
@@ -8961,13 +8961,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive nav component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -8976,10 +8969,10 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Nav\"",
"default": "\"default\"",
"options": []
},
{
@@ -9003,6 +8996,27 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Main\"",
"options": []
},
{
"name": "collapsible",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "toggleLabel",
"type": "string",
"required": false,
"default": "\"Menu\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -9015,14 +9029,10 @@
"outputs": [
{
"name": "select",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
},
{
"name": "change",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
"payloadType": "{ item: object; value: string }"
}
],
"events": ["select", "change"],
"events": ["select"],
"source": "components/Nav.wrn"
},
{
@@ -9342,6 +9352,13 @@
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component.",
"props": [
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "size",
"type": "string",
@@ -9350,10 +9367,45 @@
"options": []
},
{
"name": "color",
"name": "page",
"type": "number",
"required": false,
"default": "1",
"options": []
},
{
"name": "pageSize",
"type": "number",
"required": false,
"default": "10",
"options": []
},
{
"name": "total",
"type": "number",
"required": false,
"default": "0",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"primary\"",
"default": "\"compact\"",
"options": []
},
{
"name": "siblingCount",
"type": "number",
"required": false,
"default": "1",
"options": []
},
{
"name": "showSummary",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
@@ -9364,24 +9416,17 @@
"options": []
},
{
"name": "items",
"type": "unknown[]",
"name": "previousLabel",
"type": "string",
"required": false,
"default": "[]",
"default": "\"Previous\"",
"options": []
},
{
"name": "active",
"name": "nextLabel",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "orientation",
"type": "string",
"required": false,
"default": "\"horizontal\"",
"default": "\"Next\"",
"options": []
},
{
@@ -9396,15 +9441,15 @@
"outputs": [
{
"name": "change",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
"payloadType": "{ page: number; pageSize: number }"
},
{
"name": "previous",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ page: number }"
},
{
"name": "next",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ page: number }"
}
],
"events": ["change", "previous", "next"],
@@ -11682,13 +11727,6 @@
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
@@ -11697,14 +11735,14 @@
"options": []
},
{
"name": "label",
"name": "size",
"type": "string",
"required": false,
"default": "\"Stepper\"",
"default": "\"default\"",
"options": []
},
{
"name": "items",
"name": "steps",
"type": "unknown[]",
"required": false,
"default": "[]",
@@ -11712,9 +11750,9 @@
},
{
"name": "active",
"type": "string",
"type": "number",
"required": false,
"default": "\"\"",
"default": "0",
"options": []
},
{
@@ -11724,6 +11762,20 @@
"default": "\"horizontal\"",
"options": []
},
{
"name": "clickable",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "label",
"type": "string",
"required": false,
"default": "\"Progress\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -11732,26 +11784,14 @@
"options": []
}
],
"slots": ["default"],
"slots": ["step-{index}", "default"],
"outputs": [
{
"name": "change",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
},
{
"name": "previous",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "next",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
},
{
"name": "complete",
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
"payloadType": "{ index: number; step: object }"
}
],
"events": ["change", "previous", "next", "complete"],
"events": ["change"],
"source": "components/Stepper.wrn"
},
{
+333 -6
View File
@@ -1,22 +1,349 @@
// Nav -- a navigation link list, flat or with submenus.
//
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
//
// An item carrying its own items array becomes a submenu. Depth is capped at
// three levels: this template language has no component recursion, so each
// level is written out, and three covers any realistic navigation.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Nav {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { item: object; value: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Nav"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
label: string = "Main"
collapsible: boolean = true
toggleLabel: string = "Menu"
class: string = ""
}
state expanded = false
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function childrenOf(item) {
return item && Array.isArray(item.items) ? item.items : []
}
shared function isActive(item) {
return Boolean(item.value) && item.value === active
}
shared function rovingAxis() {
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function choose(item) {
if (item.disabled) {
return
}
output.select({ item: item, value: item.value || "" })
}
client function toggleMenu() {
expanded = !expanded
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--nav wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Nav"
class='wire-nav {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-expanded='{expanded}'
role="navigation"
aria-label='{label}'
>
<button
type="button"
class="wire-nav__toggle"
data-show="collapsible"
aria-expanded='{expanded}'
aria-label='{toggleLabel}'
@click='toggleMenu()'
>
<span class="wire-nav__toggle-bar" aria-hidden="true"></span>
<span class="wire-nav__toggle-text">{toggleLabel}</span>
</button>
<ul class="wire-nav__list" data-wrn-roving='{rovingAxis()}'>
{#each itemList() as item}
<li class="wire-nav__item" data-has-children='{childrenOf(item).length > 0}'>
<a
class="wire-nav__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(item)}'
aria-current='{isActive(item) ? "page" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wire-nav__icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{item.label}</span>
<span class="wire-nav__badge" data-show="item.badge">{item.badge}</span>
<span
class="wire-nav__arrow"
data-show="childrenOf(item).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul class="wire-nav__submenu" data-show="childrenOf(item).length > 0">
{#each childrenOf(item) as child}
<li class="wire-nav__item" data-has-children='{childrenOf(child).length > 0}'>
<a
class="wire-nav__link"
href='{child.href || "#"}'
data-active='{isActive(child)}'
aria-current='{isActive(child) ? "page" : "false"}'
aria-disabled='{child.disabled ? "true" : "false"}'
@click='choose(child)'
>
<span
class='wire-nav__icon {child.icon}'
data-show="child.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{child.label}</span>
<span class="wire-nav__badge" data-show="child.badge">{child.badge}</span>
<span
class="wire-nav__arrow"
data-show="childrenOf(child).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wire-nav__submenu wire-nav__submenu--level3"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wire-nav__item">
<a
class="wire-nav__link"
href='{leaf.href || "#"}'
data-active='{isActive(leaf)}'
aria-current='{isActive(leaf) ? "page" : "false"}'
aria-disabled='{leaf.disabled ? "true" : "false"}'
@click='choose(leaf)'
>
<span
class='wire-nav__icon {leaf.icon}'
data-show="leaf.icon"
aria-hidden="true"
></span>
<span class="wire-nav__label">{leaf.label}</span>
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
<slot />
</nav>
}
style {
.wire-nav {
--nav-accent: var(--wire-color-primary);
max-width: 100%;
}
.wire-nav[data-color="secondary"] {
--nav-accent: var(--wire-color-secondary);
}
.wire-nav[data-color="success"] {
--nav-accent: var(--wire-color-success);
}
.wire-nav[data-color="danger"] {
--nav-accent: var(--wire-color-danger);
}
.wire-nav[data-color="info"] {
--nav-accent: var(--wire-color-info);
}
.wire-nav[data-size="sm"] {
font-size: 0.82rem;
}
.wire-nav[data-size="lg"] {
font-size: 1rem;
}
.wire-nav__list {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-nav[data-orientation="vertical"] .wire-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav__item {
position: relative;
}
.wire-nav__link {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.7rem;
border-radius: var(--wire-radius-sm);
color: var(--wire-color-text-muted);
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
}
.wire-nav__link:hover {
background: var(--wire-color-surface-soft);
color: var(--wire-color-text);
}
.wire-nav__link:focus-visible {
outline: 2px solid var(--nav-accent);
outline-offset: 2px;
}
.wire-nav__link[data-active="true"] {
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
}
.wire-nav__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-nav__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
font-size: 0.72rem;
}
.wire-nav__arrow {
display: inline-block;
transition: transform 160ms ease;
}
.wire-nav__item:hover > .wire-nav__link > .wire-nav__arrow,
.wire-nav__item:focus-within > .wire-nav__link > .wire-nav__arrow {
transform: rotate(90deg);
}
.wire-nav__submenu {
position: absolute;
z-index: 30;
top: 100%;
left: 0;
min-width: 12rem;
margin: 0;
padding: 0.35rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
box-shadow: var(--wire-shadow-1);
list-style: none;
opacity: 0;
visibility: hidden;
transition: opacity 160ms ease;
}
.wire-nav__item:hover > .wire-nav__submenu,
.wire-nav__item:focus-within > .wire-nav__submenu {
opacity: 1;
visibility: visible;
}
.wire-nav__submenu--level3 {
top: 0;
left: 100%;
}
.wire-nav__toggle {
display: none;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.wire-nav__toggle-bar {
width: 1rem;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
/*
* On a phone the bar becomes a disclosure: submenus stop being floating
* overlays and stack inline, because a hover-opened overlay is
* unreachable on touch.
*/
@media (max-width: 767px) {
.wire-nav__toggle {
display: inline-flex;
}
.wire-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav[data-expanded="false"] .wire-nav__list {
display: none;
}
.wire-nav__submenu,
.wire-nav__submenu--level3 {
position: static;
opacity: 1;
visibility: visible;
border: 0;
box-shadow: none;
padding-left: 1rem;
}
}
}
}
+277 -9
View File
@@ -1,23 +1,291 @@
// Pagination -- page controls over a known total.
//
// <Pagination page={2} pageSize={10} total={137} variant="numbered" />
//
// The component owns no data. It reports the requested page through its
// change output and lets the caller fetch or slice.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Pagination {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { page: number; pageSize: number })
previous(payload: { page: number })
next(payload: { page: number })
}
props {
size: string = "default"
color: string = "primary"
size: string = "default"
page: number = 1
pageSize: number = 10
total: number = 0
variant: string = "compact"
siblingCount: number = 1
showSummary: boolean = true
label: string = "Pagination"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
previousLabel: string = "Previous"
nextLabel: string = "Next"
class: string = ""
}
functions {
shared function lastPage() {
var size = Number(pageSize) > 0 ? Number(pageSize) : 10
var count = Number(total) > 0 ? Number(total) : 0
return Math.max(1, Math.ceil(count / size))
}
// Out-of-range values arrive routinely: page travels as an HTML attribute
// and callers compute it from data that may have shrunk underneath them.
shared function currentPage() {
var value = Number(page)
if (!value || value < 1) {
return 1
}
return Math.min(value, lastPage())
}
shared function firstShown() {
if (Number(total) < 1) {
return 0
}
return (currentPage() - 1) * (Number(pageSize) || 10) + 1
}
shared function lastShown() {
return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
}
shared function pageNumbers() {
var last = lastPage()
var current = currentPage()
var siblings = Math.max(0, Number(siblingCount) || 0)
var start = Math.max(1, current - siblings)
var end = Math.min(last, current + siblings)
var pages = []
if (start > 1) {
pages.push({ value: 1, label: "1", gap: false })
if (start > 2) {
pages.push({ value: 0, label: "...", gap: true })
}
}
for (var index = start; index <= end; index += 1) {
pages.push({ value: index, label: String(index), gap: false })
}
if (end < last) {
if (end < last - 1) {
pages.push({ value: 0, label: "...", gap: true })
}
pages.push({ value: last, label: String(last), gap: false })
}
return pages
}
client function goToPage(target) {
var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
output.change({ page: next, pageSize: Number(pageSize) || 10 })
}
client function goPrevious() {
var target = Math.max(1, currentPage() - 1)
output.previous({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
client function goNext() {
var target = Math.min(lastPage(), currentPage() + 1)
output.next({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pagination wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Pagination"
class='wire-pagination {class}'
data-variant='{variant}'
data-color='{color}'
data-size='{size}'
role="navigation"
aria-label='{label}'
>
<p class="wire-pagination__summary" data-show="showSummary">
{firstShown()} to {lastShown()} of {total}
</p>
<div class="wire-pagination__controls">
<button
type="button"
class="wire-pagination__step"
aria-label='{previousLabel}'
@click='goPrevious()'
>
<span class="wire-pagination__step-icon" aria-hidden="true">&#8249;</span>
<span class="wire-pagination__step-label">{previousLabel}</span>
</button>
<ol class="wire-pagination__pages" data-show="variant === 'numbered'">
{#each pageNumbers() as entry}
<li class="wire-pagination__slot">
<span class="wire-pagination__gap" data-show="entry.gap">{entry.label}</span>
<button
type="button"
class="wire-pagination__page"
data-show="!entry.gap"
data-active='{entry.value === currentPage()}'
aria-current='{entry.value === currentPage() ? "page" : "false"}'
@click='goToPage(entry.value)'
>
{entry.label}
</button>
</li>
{/each}
</ol>
<p class="wire-pagination__compact" data-show="variant !== 'numbered'">
{currentPage()} / {lastPage()}
</p>
<button
type="button"
class="wire-pagination__step"
aria-label='{nextLabel}'
@click='goNext()'
>
<span class="wire-pagination__step-label">{nextLabel}</span>
<span class="wire-pagination__step-icon" aria-hidden="true">&#8250;</span>
</button>
</div>
<slot />
</nav>
}
style {
.wire-pagination {
--pagination-accent: var(--wire-color-primary);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
max-width: 100%;
}
.wire-pagination[data-color="secondary"] {
--pagination-accent: var(--wire-color-secondary);
}
.wire-pagination[data-color="success"] {
--pagination-accent: var(--wire-color-success);
}
.wire-pagination[data-color="danger"] {
--pagination-accent: var(--wire-color-danger);
}
.wire-pagination[data-color="info"] {
--pagination-accent: var(--wire-color-info);
}
.wire-pagination[data-size="sm"] {
font-size: 0.82rem;
}
.wire-pagination[data-size="lg"] {
font-size: 1rem;
}
.wire-pagination__summary {
margin: 0;
color: var(--wire-color-text-muted);
font-size: 0.82rem;
}
.wire-pagination__controls {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.wire-pagination__pages {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wire-pagination__slot {
display: inline-flex;
}
.wire-pagination__page,
.wire-pagination__step {
appearance: none;
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 2.25rem;
justify-content: center;
padding: 0.4rem 0.6rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm);
background: var(--wire-color-surface);
color: var(--wire-color-text);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.wire-pagination__page:hover,
.wire-pagination__step:hover {
background: var(--wire-color-surface-soft);
}
.wire-pagination__page:focus-visible,
.wire-pagination__step:focus-visible {
outline: 2px solid var(--pagination-accent);
outline-offset: 2px;
}
.wire-pagination__page[data-active="true"] {
border-color: var(--pagination-accent);
background: var(--pagination-accent);
color: var(--wire-color-primary-contrast);
font-weight: 700;
}
.wire-pagination__gap {
padding: 0 0.35rem;
color: var(--wire-color-text-muted);
}
.wire-pagination__compact {
margin: 0;
padding: 0 0.5rem;
color: var(--wire-color-text-muted);
font-size: 0.85rem;
}
/* Below the small breakpoint the word labels crowd the arrows out. */
@media (max-width: 639px) {
.wire-pagination {
justify-content: center;
}
.wire-pagination__step-label {
display: none;
}
.wire-pagination__summary {
width: 100%;
text-align: center;
}
}
}
}
+238 -11
View File
@@ -1,24 +1,251 @@
// Stepper -- ordered progress through a sequence.
//
// <Stepper steps='[{"label":"Account"},{"label":"Billing"}]' active={1} />
//
// Each step can be authored by hand instead of using the built-in body, by
// passing a slot named for its index:
//
// <Stepper steps={steps} active={1}>
// <div data-slot="step-1">...anything...</div>
// </Stepper>
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Stepper {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { index: number; step: object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Stepper"
items: unknown[] = []
active: string = ""
size: string = "default"
steps: unknown[] = []
active: number = 0
orientation: string = "horizontal"
clickable: boolean = false
label: string = "Progress"
class: string = ""
}
functions {
shared function stepList() {
return Array.isArray(steps) ? steps : []
}
shared function activeIndex() {
var count = stepList().length
if (count < 1) {
return 0
}
var value = Number(active)
if (!value || value < 0) {
return 0
}
return Math.min(value, count - 1)
}
shared function statusFor(index) {
if (index < activeIndex()) {
return "complete"
}
if (index === activeIndex()) {
return "current"
}
return "upcoming"
}
// An empty orientation is how the roving runtime is told to stay out of
// the way, so a read-only stepper never takes arrow-key focus.
shared function rovingAxis() {
if (!clickable) {
return ""
}
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function selectStep(index, step) {
if (!clickable) {
return
}
output.change({ index: index, step: step })
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--stepper wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<ol
{...attrs}
data-ui-component="Stepper"
class='wire-stepper {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-clickable='{clickable}'
data-wrn-roving='{rovingAxis()}'
aria-label='{label}'
>
{#each stepList() as step, index}
<li
class="wire-stepper__step"
data-status='{statusFor(index)}'
aria-current='{statusFor(index) === "current" ? "step" : "false"}'
>
<button
type="button"
class="wire-stepper__button"
data-wrn-roving-item='{clickable ? "true" : "false"}'
@click='selectStep(index, step)'
>
<span class="wire-stepper__marker" aria-hidden="true">
<span class='wire-stepper__icon {step.icon}' data-show="step.icon"></span>
<span class="wire-stepper__number" data-show="!step.icon">{index + 1}</span>
</span>
<span class="wire-stepper__body">
<span class="wire-stepper__label">{step.label}</span>
<span class="wire-stepper__description" data-show="step.description">
{step.description}
</span>
</span>
</button>
<span class="wire-stepper__custom">
<slot name="step-{index}"></slot>
</span>
</li>
{/each}
<slot />
</nav>
</ol>
}
style {
.wire-stepper {
--stepper-accent: var(--wire-color-primary);
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
max-width: 100%;
}
.wire-stepper[data-color="secondary"] {
--stepper-accent: var(--wire-color-secondary);
}
.wire-stepper[data-color="success"] {
--stepper-accent: var(--wire-color-success);
}
.wire-stepper[data-color="danger"] {
--stepper-accent: var(--wire-color-danger);
}
.wire-stepper[data-color="info"] {
--stepper-accent: var(--wire-color-info);
}
.wire-stepper[data-size="sm"] {
font-size: 0.82rem;
}
.wire-stepper[data-size="lg"] {
font-size: 1rem;
}
.wire-stepper[data-orientation="vertical"] {
flex-direction: column;
}
.wire-stepper__step {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
gap: 0.35rem;
}
.wire-stepper__button {
appearance: none;
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.5rem;
border: 0;
border-radius: var(--wire-radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: default;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button {
cursor: pointer;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button:hover {
background: var(--wire-color-surface-soft);
}
.wire-stepper__button:focus-visible {
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
.wire-stepper__marker {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 2rem;
height: 2rem;
border: 1px solid var(--wire-color-border);
border-radius: 999px;
background: var(--wire-color-surface);
font-size: 0.85rem;
font-weight: 700;
}
.wire-stepper__step[data-status="complete"] .wire-stepper__marker {
border-color: var(--stepper-accent);
background: var(--stepper-accent);
color: var(--wire-color-primary-contrast);
}
.wire-stepper__step[data-status="current"] .wire-stepper__marker {
border-color: var(--stepper-accent);
color: var(--stepper-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-accent) 22%, transparent);
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__marker {
color: var(--wire-color-text-muted);
}
.wire-stepper__body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wire-stepper__label {
font-size: 0.9rem;
font-weight: 600;
}
.wire-stepper__description {
color: var(--wire-color-text-muted);
font-size: 0.78rem;
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__label {
color: var(--wire-color-text-muted);
}
/* A horizontal stepper cannot stay side by side on a phone. */
@media (max-width: 639px) {
.wire-stepper {
flex-direction: column;
}
}
}
}
+156
View File
@@ -2458,3 +2458,159 @@ test("toggle password supports checkbox control and synchronized password fields
expect(checkboxPassword.type).toBe("text");
});
test("pagination renders windowed page numbers and emits change", async () => {
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
const html = await renderComponent(source, {
page: 5,
pageSize: 10,
total: 200,
variant: "numbered",
siblingCount: 1,
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-pagination") as HTMLElement;
expect(root.getAttribute("role")).toBe("navigation");
const current = dom.querySelector('.wire-pagination__page[aria-current="page"]');
expect(current!.textContent!.trim()).toBe("5");
expect(root.textContent).toContain("41");
expect(root.textContent).toContain("50");
expect(root.textContent).toContain("200");
});
test("pagination clamps an out-of-range page instead of rendering nothing", async () => {
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
const html = await renderComponent(source, { page: 99, pageSize: 10, total: 30 });
const dom = mountHtml(html);
const current = dom.querySelector(".wire-pagination__compact");
expect(current!.textContent).toContain("3");
});
test("pagination survives an empty dataset", async () => {
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
const html = await renderComponent(source, { page: 1, pageSize: 10, total: 0 });
const dom = mountHtml(html);
expect(dom.querySelector(".wire-pagination")).not.toBeNull();
});
test("stepper marks complete, current and upcoming steps", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [
{ label: "Account", description: "Your details" },
{ label: "Billing", description: "Payment method" },
{ label: "Confirm", description: "Review and submit" },
],
active: 1,
});
const dom = mountHtml(html);
const items = [...dom.querySelectorAll(".wire-stepper__step")];
expect(items).toHaveLength(3);
expect(items[0]!.getAttribute("data-status")).toBe("complete");
expect(items[1]!.getAttribute("data-status")).toBe("current");
expect(items[2]!.getAttribute("data-status")).toBe("upcoming");
expect(items[1]!.getAttribute("aria-current")).toBe("step");
expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("ol");
});
test("stepper renders vertically and clamps an out-of-range active index", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [{ label: "One" }, { label: "Two" }],
active: 99,
orientation: "vertical",
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-stepper") as HTMLElement;
expect(root.getAttribute("data-orientation")).toBe("vertical");
const items = [...dom.querySelectorAll(".wire-stepper__step")];
expect(items[1]!.getAttribute("data-status")).toBe("current");
});
test("clickable stepper opts into roving focus", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [{ label: "One" }, { label: "Two" }],
active: 0,
clickable: true,
});
const dom = mountHtml(html);
expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull();
expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBe(2);
});
test("nav renders links, marks the active one, and shows icons and badges", async () => {
const source = readFileSync(uiComponentPath("Nav"), "utf8");
const html = await renderComponent(source, {
items: [
{ label: "Home", href: "/", value: "home", icon: "icon-[lucide--house]" },
{ label: "Inbox", href: "/inbox", value: "inbox", badge: "9" },
{ label: "Archive", href: "/archive", value: "archive", disabled: true },
],
active: "inbox",
});
const dom = mountHtml(html);
expect(dom.querySelector(".wire-nav")!.getAttribute("role")).toBe("navigation");
const current = dom.querySelector('[aria-current="page"]');
expect(current!.textContent).toContain("Inbox");
// Every item renders a badge span; the ones without a badge are empty and
// carry data-show="false", so match on content rather than first-in-document.
const badges = [...dom.querySelectorAll(".wire-nav__badge")]
.map((node) => node.textContent!.trim())
.filter(Boolean);
expect(badges).toContain("9");
expect(dom.querySelector(".wire-nav__icon")).not.toBeNull();
expect(dom.querySelector('[aria-disabled="true"]')).not.toBeNull();
});
test("nav renders a submenu with a disclosure arrow and opts into roving focus", async () => {
const source = readFileSync(uiComponentPath("Nav"), "utf8");
const html = await renderComponent(source, {
items: [
{
label: "Products",
value: "products",
items: [
{ label: "Overview", href: "/p", value: "p-overview" },
{
label: "More",
value: "p-more",
items: [{ label: "Deep", href: "/d", value: "deep" }],
},
],
},
],
active: "p-overview",
});
const dom = mountHtml(html);
expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull();
expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBeGreaterThan(0);
expect(dom.querySelector(".wire-nav__arrow")).not.toBeNull();
expect(dom.querySelector(".wire-nav__submenu")).not.toBeNull();
expect(dom.querySelector(".wire-nav__submenu--level3")).not.toBeNull();
// Every second-level item renders a third-level list; only the one whose
// item actually has children is populated, so search across them all.
const thirdLevel = [...dom.querySelectorAll(".wire-nav__submenu--level3")]
.map((node) => node.textContent!.trim())
.filter(Boolean);
expect(thirdLevel.join(" ")).toContain("Deep");
});
test("nav renders its shell with no items and rejects a non-array", async () => {
const source = readFileSync(uiComponentPath("Nav"), "utf8");
const html = await renderComponent(source, { items: [], active: "" });
const dom = mountHtml(html);
expect(dom.querySelector(".wire-nav")).not.toBeNull();
expect(dom.querySelectorAll(".wire-nav__link").length).toBe(0);
// A declared unknown[] prop is coerced by the framework, which rejects a
// non-array before the component runs. The itemList() guard is the second
// line of defence for a missing prop, not a way to render junk.
await expect(renderComponent(source, { items: "not-an-array", active: "" })).rejects.toThrow(
"Expected an array prop",
);
});