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>
This commit is contained in:
2026-08-07 16:57:10 +05:30
co-authored by Claude Opus 5
parent f4960f2fc5
commit 124da548b8
12 changed files with 457 additions and 1352 deletions
+23 -16
View File
@@ -895,6 +895,17 @@ test("comments are ignored inside interpreted statements", () => {
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">
@@ -911,20 +922,18 @@ test("roving focus moves with arrow keys and wraps", () => {
expect(doc.querySelector("#b")!.getAttribute("tabindex")).toBe("-1");
a.focus();
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
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")!
.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
(doc.querySelector("#b") as unknown as HTMLElement).dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
a.dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("c");
c.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
c.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("a");
});
@@ -940,15 +949,13 @@ test("roving focus honours Home and End and skips disabled items", () => {
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(doc.activeElement!.id).toBe("c");
doc
.querySelector("#c")!
.dispatchEvent(new win.KeyboardEvent("keydown", { key: "Home", bubbles: true }));
(doc.querySelector("#c") as unknown as HTMLElement).dispatchEvent(keydown(win, "Home"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "End", bubbles: true }));
a.dispatchEvent(keydown(win, "End"));
expect(doc.activeElement!.id).toBe("c");
});
@@ -961,7 +968,7 @@ test("horizontal roving ignores vertical arrows so the page still scrolls", () =
);
const a = win.document.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(win.document.activeElement!.id).toBe("a");
});
@@ -990,7 +997,7 @@ test("nested roving groups do not capture the outer group items", () => {
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("b");
});
@@ -1009,7 +1016,7 @@ test("opening a modal dialog traps Tab inside it", () => {
const doc = win.document;
const last = doc.querySelector("#last") as unknown as HTMLElement;
last.focus();
last.dispatchEvent(new win.KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
last.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("first");
});
@@ -1027,7 +1034,7 @@ test("a hidden dialog does not trap Tab", () => {
const doc = win.document;
const outside = doc.querySelector("#outside") as unknown as HTMLElement;
outside.focus();
outside.dispatchEvent(new win.KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
outside.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("outside");
});
@@ -1042,6 +1049,6 @@ test("an empty or false roving attribute opts the group out entirely", () => {
const a = doc.querySelector("#a") as unknown as HTMLElement;
expect(a.getAttribute("tabindex")).toBeNull();
a.focus();
a.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
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"
},
{
+34 -5
View File
@@ -14,6 +14,8 @@ component Nav {
}
props {
color: string = "primary"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
@@ -60,6 +62,8 @@ component Nav {
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}'
@@ -164,9 +168,34 @@ component 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;
@@ -203,13 +232,13 @@ component Nav {
}
.wire-nav__link:focus-visible {
outline: 2px solid var(--wire-color-primary);
outline: 2px solid var(--nav-accent);
outline-offset: 2px;
}
.wire-nav__link[data-active="true"] {
background: var(--wire-color-primary-soft);
color: var(--wire-color-primary);
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
}
.wire-nav__link[aria-disabled="true"] {
@@ -220,8 +249,8 @@ component Nav {
.wire-nav__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: var(--wire-color-primary-soft);
color: var(--wire-color-primary);
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
font-size: 0.72rem;
}
+32 -3
View File
@@ -15,6 +15,8 @@ component Pagination {
}
props {
color: string = "primary"
size: string = "default"
page: number = 1
pageSize: number = 10
total: number = 0
@@ -104,6 +106,8 @@ component Pagination {
data-ui-component="Pagination"
class='wire-pagination {class}'
data-variant='{variant}'
data-color='{color}'
data-size='{size}'
role="navigation"
aria-label='{label}'
>
@@ -161,6 +165,7 @@ component Pagination {
style {
.wire-pagination {
--pagination-accent: var(--wire-color-primary);
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -169,6 +174,30 @@ component Pagination {
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);
@@ -220,13 +249,13 @@ component Pagination {
.wire-pagination__page:focus-visible,
.wire-pagination__step:focus-visible {
outline: 2px solid var(--wire-color-primary);
outline: 2px solid var(--pagination-accent);
outline-offset: 2px;
}
.wire-pagination__page[data-active="true"] {
border-color: var(--wire-color-primary);
background: var(--wire-color-primary);
border-color: var(--pagination-accent);
background: var(--pagination-accent);
color: var(--wire-color-primary-contrast);
font-weight: 700;
}
+35 -6
View File
@@ -17,6 +17,8 @@ component Stepper {
}
props {
color: string = "primary"
size: string = "default"
steps: unknown[] = []
active: number = 0
orientation: string = "horizontal"
@@ -75,6 +77,8 @@ component Stepper {
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}'
@@ -113,6 +117,7 @@ component Stepper {
style {
.wire-stepper {
--stepper-accent: var(--wire-color-primary);
display: flex;
gap: 0.5rem;
margin: 0;
@@ -121,6 +126,30 @@ component Stepper {
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;
}
@@ -158,7 +187,7 @@ component Stepper {
}
.wire-stepper__button:focus-visible {
outline: 2px solid var(--wire-color-primary);
outline: 2px solid var(--stepper-accent);
outline-offset: 2px;
}
@@ -177,15 +206,15 @@ component Stepper {
}
.wire-stepper__step[data-status="complete"] .wire-stepper__marker {
border-color: var(--wire-color-primary);
background: var(--wire-color-primary);
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(--wire-color-primary);
color: var(--wire-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-color-primary) 22%, transparent);
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 {