refactor: migrate legacy wire namespace to wrn
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-12 18:51:15 +05:30
parent ec23dd4c93
commit 2c960fc1dc
488 changed files with 27306 additions and 19063 deletions
@@ -4,7 +4,7 @@
**Goal:** Ship runtime-owned roving arrow-key focus, then build Nav, Pagination and Stepper as real components on that foundation.
**Architecture:** One declarative attribute pair (`data-wrn-roving` on a container, `data-wrn-roving-item` on children) implements ARIA roving focus once in the reactive runtime, the same way modal dialog focus already lives there. The three components then declare intent with a single attribute instead of carrying five near-identical `@keydown` handlers. Each component uses a local `style {}` block with `wire-*` classes, declares its `outputs`, and takes props in / reports outputs out with no global state.
**Architecture:** One declarative attribute pair (`data-wrn-roving` on a container, `data-wrn-roving-item` on children) implements ARIA roving focus once in the reactive runtime, the same way modal dialog focus already lives there. The three components then declare intent with a single attribute instead of carrying five near-identical `@keydown` handlers. Each component uses a local `style {}` block with `wrn-*` classes, declares its `outputs`, and takes props in / reports outputs out with no global state.
**Tech Stack:** Bun, TypeScript, `.wrn` components, happy-dom for runtime tests, `@wrnexus/test` (`renderComponent`, `mountHtml`) for component tests.
@@ -456,10 +456,10 @@ test("pagination renders windowed page numbers and emits change", async () => {
siblingCount: 1,
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-pagination") as HTMLElement;
const root = dom.querySelector(".wrn-pagination") as HTMLElement;
expect(root.getAttribute("role")).toBe("navigation");
const current = dom.querySelector('.wire-pagination__page[aria-current="page"]');
const current = dom.querySelector('.wrn-pagination__page[aria-current="page"]');
expect(current!.textContent!.trim()).toBe("5");
expect(root.textContent).toContain("41");
expect(root.textContent).toContain("50");
@@ -478,7 +478,7 @@ test("pagination survives a non-array or 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();
expect(dom.querySelector(".wrn-pagination")).not.toBeNull();
});
```
@@ -488,7 +488,7 @@ test("pagination survives a non-array or empty dataset", async () => {
bun test packages/ui -t "pagination"
```
Expected: FAIL — the scaffold has no `.wire-pagination` class and no page numbers.
Expected: FAIL — the scaffold has no `.wrn-pagination` class and no page numbers.
- [ ] **Step 3: Replace `packages/ui/components/Pagination.wrn`**
@@ -594,33 +594,33 @@ component Pagination {
<nav
{...attrs}
data-ui-component="Pagination"
class='wire-pagination {class}'
class='wrn-pagination {class}'
data-variant='{variant}'
role="navigation"
aria-label='{label}'
>
<p class="wire-pagination__summary" data-show="showSummary">
<p class="wrn-pagination__summary" data-show="showSummary">
{firstShown()} to {lastShown()} of {total}
</p>
<div class="wire-pagination__controls">
<div class="wrn-pagination__controls">
<button
type="button"
class="wire-pagination__step"
class="wrn-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>
<span class="wrn-pagination__step-icon" aria-hidden="true">&#8249;</span>
<span class="wrn-pagination__step-label">{previousLabel}</span>
</button>
<ol class="wire-pagination__pages" data-show="variant === 'numbered'">
<ol class="wrn-pagination__pages" data-show="variant === 'numbered'">
{#each pageNumbers() as entry}
<li>
<span class="wire-pagination__gap" data-show="entry.gap">{entry.label}</span>
<span class="wrn-pagination__gap" data-show="entry.gap">{entry.label}</span>
<button
type="button"
class="wire-pagination__page"
class="wrn-pagination__page"
data-show="!entry.gap"
data-active='{entry.value === currentPage()}'
aria-current='{entry.value === currentPage() ? "page" : "false"}'
@@ -632,18 +632,18 @@ component Pagination {
{/each}
</ol>
<p class="wire-pagination__compact" data-show="variant !== 'numbered'">
<p class="wrn-pagination__compact" data-show="variant !== 'numbered'">
{currentPage()} / {lastPage()}
</p>
<button
type="button"
class="wire-pagination__step"
class="wrn-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>
<span class="wrn-pagination__step-label">{nextLabel}</span>
<span class="wrn-pagination__step-icon" aria-hidden="true">&#8250;</span>
</button>
</div>
@@ -652,7 +652,7 @@ component Pagination {
}
style {
.wire-pagination {
.wrn-pagination {
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -661,20 +661,20 @@ component Pagination {
max-width: 100%;
}
.wire-pagination__summary {
.wrn-pagination__summary {
margin: 0;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.82rem;
}
.wire-pagination__controls {
.wrn-pagination__controls {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.wire-pagination__pages {
.wrn-pagination__pages {
display: flex;
align-items: center;
gap: 0.25rem;
@@ -683,8 +683,8 @@ component Pagination {
list-style: none;
}
.wire-pagination__page,
.wire-pagination__step {
.wrn-pagination__page,
.wrn-pagination__step {
appearance: none;
display: inline-flex;
align-items: center;
@@ -692,50 +692,50 @@ component Pagination {
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);
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.wire-pagination__page:hover,
.wire-pagination__step:hover {
background: var(--wire-color-surface-soft);
.wrn-pagination__page:hover,
.wrn-pagination__step:hover {
background: var(--wrn-color-surface-soft);
}
.wire-pagination__page[data-active="true"] {
border-color: var(--wire-color-primary);
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
.wrn-pagination__page[data-active="true"] {
border-color: var(--wrn-color-primary);
background: var(--wrn-color-primary);
color: var(--wrn-color-primary-contrast);
font-weight: 700;
}
.wire-pagination__gap {
.wrn-pagination__gap {
padding: 0 0.35rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
}
.wire-pagination__compact {
.wrn-pagination__compact {
margin: 0;
padding: 0 0.5rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.85rem;
}
/* Below the small breakpoint the word labels crowd the arrows out. */
@media (max-width: 639px) {
.wire-pagination {
.wrn-pagination {
justify-content: center;
}
.wire-pagination__step-label {
.wrn-pagination__step-label {
display: none;
}
.wire-pagination__summary {
.wrn-pagination__summary {
width: 100%;
text-align: center;
}
@@ -816,14 +816,14 @@ test("stepper marks complete, current and upcoming steps", async () => {
active: 1,
});
const dom = mountHtml(html);
const items = [...dom.querySelectorAll(".wire-stepper__step")];
const items = [...dom.querySelectorAll(".wrn-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");
expect(dom.querySelector(".wrn-stepper")!.tagName.toLowerCase()).toBe("ol");
});
test("stepper renders vertically and clamps an out-of-range active index", async () => {
@@ -834,9 +834,9 @@ test("stepper renders vertically and clamps an out-of-range active index", async
orientation: "vertical",
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-stepper") as HTMLElement;
const root = dom.querySelector(".wrn-stepper") as HTMLElement;
expect(root.getAttribute("data-orientation")).toBe("vertical");
const items = [...dom.querySelectorAll(".wire-stepper__step")];
const items = [...dom.querySelectorAll(".wrn-stepper__step")];
expect(items[1].getAttribute("data-status")).toBe("current");
});
@@ -859,7 +859,7 @@ test("clickable stepper opts into roving focus", async () => {
bun test packages/ui -t "stepper"
```
Expected: FAIL — the scaffold renders a `<nav>` of anchors with no `.wire-stepper__step`.
Expected: FAIL — the scaffold renders a `<nav>` of anchors with no `.wrn-stepper__step`.
- [ ] **Step 3: Replace `packages/ui/components/Stepper.wrn`**
@@ -927,7 +927,7 @@ component Stepper {
<ol
{...attrs}
data-ui-component="Stepper"
class='wire-stepper {class}'
class='wrn-stepper {class}'
data-orientation='{orientation}'
data-clickable='{clickable}'
data-wrn-roving='{clickable ? (orientation === "vertical" ? "vertical" : "horizontal") : ""}'
@@ -935,28 +935,28 @@ component Stepper {
>
{#each stepList() as step, index}
<li
class="wire-stepper__step"
class="wrn-stepper__step"
data-status='{statusFor(index)}'
aria-current='{statusFor(index) === "current" ? "step" : "false"}'
>
<button
type="button"
class="wire-stepper__button"
class="wrn-stepper__button"
data-wrn-roving-item='{clickable}'
@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 class="wrn-stepper__marker" aria-hidden="true">
<span class='wrn-stepper__icon {step.icon}' data-show="step.icon"></span>
<span class="wrn-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">
<span class="wrn-stepper__body">
<span class="wrn-stepper__label">{step.label}</span>
<span class="wrn-stepper__description" data-show="step.description">
{step.description}
</span>
</span>
</button>
<span class="wire-stepper__custom">
<span class="wrn-stepper__custom">
<slot name="step-{index}"></slot>
</span>
</li>
@@ -966,7 +966,7 @@ component Stepper {
}
style {
.wire-stepper {
.wrn-stepper {
display: flex;
gap: 0.5rem;
margin: 0;
@@ -975,11 +975,11 @@ component Stepper {
max-width: 100%;
}
.wire-stepper[data-orientation="vertical"] {
.wrn-stepper[data-orientation="vertical"] {
flex-direction: column;
}
.wire-stepper__step {
.wrn-stepper__step {
display: flex;
flex-direction: column;
flex: 1 1 0;
@@ -987,7 +987,7 @@ component Stepper {
gap: 0.35rem;
}
.wire-stepper__button {
.wrn-stepper__button {
appearance: none;
display: flex;
align-items: center;
@@ -995,7 +995,7 @@ component Stepper {
width: 100%;
padding: 0.5rem;
border: 0;
border-radius: var(--wire-radius-sm);
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
font: inherit;
@@ -1003,67 +1003,67 @@ component Stepper {
cursor: default;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button {
.wrn-stepper[data-clickable="true"] .wrn-stepper__button {
cursor: pointer;
}
.wire-stepper[data-clickable="true"] .wire-stepper__button:hover {
background: var(--wire-color-surface-soft);
.wrn-stepper[data-clickable="true"] .wrn-stepper__button:hover {
background: var(--wrn-color-surface-soft);
}
.wire-stepper__marker {
.wrn-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: 1px solid var(--wrn-color-border);
border-radius: 999px;
background: var(--wire-color-surface);
background: var(--wrn-color-surface);
font-size: 0.85rem;
font-weight: 700;
}
.wire-stepper__step[data-status="complete"] .wire-stepper__marker {
border-color: var(--wire-color-primary);
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
.wrn-stepper__step[data-status="complete"] .wrn-stepper__marker {
border-color: var(--wrn-color-primary);
background: var(--wrn-color-primary);
color: var(--wrn-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);
.wrn-stepper__step[data-status="current"] .wrn-stepper__marker {
border-color: var(--wrn-color-primary);
color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 22%, transparent);
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__marker {
color: var(--wire-color-text-muted);
.wrn-stepper__step[data-status="upcoming"] .wrn-stepper__marker {
color: var(--wrn-color-text-muted);
}
.wire-stepper__body {
.wrn-stepper__body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wire-stepper__label {
.wrn-stepper__label {
font-size: 0.9rem;
font-weight: 600;
}
.wire-stepper__description {
color: var(--wire-color-text-muted);
.wrn-stepper__description {
color: var(--wrn-color-text-muted);
font-size: 0.78rem;
}
.wire-stepper__step[data-status="upcoming"] .wire-stepper__label {
color: var(--wire-color-text-muted);
.wrn-stepper__step[data-status="upcoming"] .wrn-stepper__label {
color: var(--wrn-color-text-muted);
}
/* A horizontal stepper cannot stay side by side on a phone. */
@media (max-width: 639px) {
.wire-stepper {
.wrn-stepper {
flex-direction: column;
}
}
@@ -1163,11 +1163,11 @@ test("nav renders links, marks the active one, and shows icons and badges", asyn
});
const dom = mountHtml(html);
expect(dom.querySelector(".wire-nav")!.getAttribute("role")).toBe("navigation");
expect(dom.querySelector(".wrn-nav")!.getAttribute("role")).toBe("navigation");
const current = dom.querySelector('[aria-current="page"]');
expect(current!.textContent).toContain("Inbox");
expect(dom.querySelector(".wire-nav__badge")!.textContent!.trim()).toBe("9");
expect(dom.querySelector(".wire-nav__icon")).not.toBeNull();
expect(dom.querySelector(".wrn-nav__badge")!.textContent!.trim()).toBe("9");
expect(dom.querySelector(".wrn-nav__icon")).not.toBeNull();
expect(dom.querySelector('[aria-disabled="true"]')).not.toBeNull();
});
@@ -1194,10 +1194,10 @@ test("nav renders a submenu with a disclosure arrow and opts into roving focus",
expect(dom.querySelector("[data-wrn-roving]")).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(".wrn-nav__arrow")).not.toBeNull();
expect(dom.querySelector(".wrn-nav__submenu")).not.toBeNull();
// Third level is rendered, and nothing deeper is attempted.
expect(dom.querySelector(".wire-nav__submenu--level3")).not.toBeNull();
expect(dom.querySelector(".wrn-nav__submenu--level3")).not.toBeNull();
expect(dom.body.innerHTML).toContain("Deep");
});
@@ -1205,8 +1205,8 @@ test("nav renders empty rather than throwing when items is not an array", async
const source = readFileSync(uiComponentPath("Nav"), "utf8");
const html = await renderComponent(source, { items: "not-an-array", active: "" });
const dom = mountHtml(html);
expect(dom.querySelector(".wire-nav")).not.toBeNull();
expect(dom.querySelectorAll(".wire-nav__link").length).toBe(0);
expect(dom.querySelector(".wrn-nav")).not.toBeNull();
expect(dom.querySelectorAll(".wrn-nav__link").length).toBe(0);
});
```
@@ -1216,7 +1216,7 @@ test("nav renders empty rather than throwing when items is not an array", async
bun test packages/ui -t "nav "
```
Expected: FAIL — the scaffold renders bare anchors with no `.wire-nav__link`, no submenu and no roving attributes.
Expected: FAIL — the scaffold renders bare anchors with no `.wrn-nav__link`, no submenu and no roving attributes.
- [ ] **Step 3: Replace `packages/ui/components/Nav.wrn`**
@@ -1274,7 +1274,7 @@ component Nav {
<nav
{...attrs}
data-ui-component="Nav"
class='wire-nav {class}'
class='wrn-nav {class}'
data-orientation='{orientation}'
data-expanded='{expanded}'
role="navigation"
@@ -1282,24 +1282,24 @@ component Nav {
>
<button
type="button"
class="wire-nav__toggle"
class="wrn-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>
<span class="wrn-nav__toggle-bar" aria-hidden="true"></span>
<span class="wrn-nav__toggle-text">{toggleLabel}</span>
</button>
<ul
class="wire-nav__list"
class="wrn-nav__list"
data-wrn-roving='{orientation === "vertical" ? "vertical" : "horizontal"}'
>
{#each itemList() as item}
<li class="wire-nav__item" data-has-children='{childrenOf(item).length > 0}'>
<li class="wrn-nav__item" data-has-children='{childrenOf(item).length > 0}'>
<a
class="wire-nav__link"
class="wrn-nav__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(item)}'
@@ -1307,53 +1307,53 @@ component Nav {
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='wrn-nav__icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
<span class="wrn-nav__label">{item.label}</span>
<span class="wrn-nav__badge" data-show="item.badge">{item.badge}</span>
<span
class="wire-nav__arrow"
class="wrn-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">
<ul class="wrn-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}'>
<li class="wrn-nav__item" data-has-children='{childrenOf(child).length > 0}'>
<a
class="wire-nav__link"
class="wrn-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='wrn-nav__icon {child.icon}' data-show="child.icon" aria-hidden="true"></span>
<span class="wrn-nav__label">{child.label}</span>
<span class="wrn-nav__badge" data-show="child.badge">{child.badge}</span>
<span
class="wire-nav__arrow"
class="wrn-nav__arrow"
data-show="childrenOf(child).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wire-nav__submenu wire-nav__submenu--level3"
class="wrn-nav__submenu wrn-nav__submenu--level3"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wire-nav__item">
<li class="wrn-nav__item">
<a
class="wire-nav__link"
class="wrn-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>
<span class='wrn-nav__icon {leaf.icon}' data-show="leaf.icon" aria-hidden="true"></span>
<span class="wrn-nav__label">{leaf.label}</span>
</a>
</li>
{/each}
@@ -1370,11 +1370,11 @@ component Nav {
}
style {
.wire-nav {
.wrn-nav {
max-width: 100%;
}
.wire-nav__list {
.wrn-nav__list {
display: flex;
align-items: center;
gap: 0.25rem;
@@ -1383,61 +1383,61 @@ component Nav {
list-style: none;
}
.wire-nav[data-orientation="vertical"] .wire-nav__list {
.wrn-nav[data-orientation="vertical"] .wrn-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav__item {
.wrn-nav__item {
position: relative;
}
.wire-nav__link {
.wrn-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);
border-radius: var(--wrn-radius-sm);
color: var(--wrn-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);
.wrn-nav__link:hover {
background: var(--wrn-color-surface-soft);
color: var(--wrn-color-text);
}
.wire-nav__link[data-active="true"] {
background: var(--wire-color-primary-soft);
color: var(--wire-color-primary);
.wrn-nav__link[data-active="true"] {
background: var(--wrn-color-primary-soft);
color: var(--wrn-color-primary);
}
.wire-nav__link[aria-disabled="true"] {
.wrn-nav__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wire-nav__badge {
.wrn-nav__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: var(--wire-color-primary-soft);
color: var(--wire-color-primary);
background: var(--wrn-color-primary-soft);
color: var(--wrn-color-primary);
font-size: 0.72rem;
}
.wire-nav__arrow {
.wrn-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 {
.wrn-nav__item:hover > .wrn-nav__link > .wrn-nav__arrow,
.wrn-nav__item:focus-within > .wrn-nav__link > .wrn-nav__arrow {
transform: rotate(90deg);
}
.wire-nav__submenu {
.wrn-nav__submenu {
position: absolute;
z-index: 30;
top: 100%;
@@ -1445,42 +1445,42 @@ component Nav {
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);
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-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 {
.wrn-nav__item:hover > .wrn-nav__submenu,
.wrn-nav__item:focus-within > .wrn-nav__submenu {
opacity: 1;
visibility: visible;
}
.wire-nav__submenu--level3 {
.wrn-nav__submenu--level3 {
top: 0;
left: 100%;
}
.wire-nav__toggle {
.wrn-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);
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.wire-nav__toggle-bar {
.wrn-nav__toggle-bar {
width: 1rem;
height: 2px;
background: currentColor;
@@ -1493,21 +1493,21 @@ component Nav {
* unreachable on touch.
*/
@media (max-width: 767px) {
.wire-nav__toggle {
.wrn-nav__toggle {
display: inline-flex;
}
.wire-nav__list {
.wrn-nav__list {
flex-direction: column;
align-items: stretch;
}
.wire-nav[data-expanded="false"] .wire-nav__list {
.wrn-nav[data-expanded="false"] .wrn-nav__list {
display: none;
}
.wire-nav__submenu,
.wire-nav__submenu--level3 {
.wrn-nav__submenu,
.wrn-nav__submenu--level3 {
position: static;
opacity: 1;
visibility: visible;
@@ -1656,7 +1656,7 @@ git push
## Self-Review
**Spec coverage for phase 1.** Roving focus in the runtime — Task 1. Nav multi-level to depth 3, icons, badges, animated arrows, responsive collapse, `aria-current` — Task 5. Pagination standalone with compact and numbered variants and windowed numbers — Task 3. Stepper horizontal/vertical with indexed named slots and `aria-current="step"` — Task 4. Local `style {}` blocks with `wire-*` classes — Tasks 3, 4, 5. Error handling for non-array `items` and out-of-range `active`/`page` — covered by a test in each of Tasks 3, 4, 5. Showcase profiles and regeneration — Tasks 36. Runtime budget check — Tasks 1 and 6.
**Spec coverage for phase 1.** Roving focus in the runtime — Task 1. Nav multi-level to depth 3, icons, badges, animated arrows, responsive collapse, `aria-current` — Task 5. Pagination standalone with compact and numbered variants and windowed numbers — Task 3. Stepper horizontal/vertical with indexed named slots and `aria-current="step"` — Task 4. Local `style {}` blocks with `wrn-*` classes — Tasks 3, 4, 5. Error handling for non-array `items` and out-of-range `active`/`page` — covered by a test in each of Tasks 3, 4, 5. Showcase profiles and regeneration — Tasks 36. Runtime budget check — Tasks 1 and 6.
Out of phase 1 by design, carried to later plans: Tabs, Sidebar, MegaMenu (phase 2); Navbar, Breadcrumb, Scrollspy, responsive sweep (phase 3).
@@ -14,7 +14,7 @@ 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-*` +
component in the library using Tailwind utility classes rather than `wrn-*` +
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
@@ -28,7 +28,7 @@ All nine components, delivered in three phases.
### Styling
Every navigation component gets a local `style {}` block using `wire-*`
Every navigation component gets a local `style {}` block using `wrn-*`
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
@@ -116,7 +116,7 @@ scroll lock rather than duplicating them.
| 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 |
| Tabs | Rewrite off Tailwind onto `wrn-*` + 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 |
@@ -10,7 +10,7 @@ Two problems, in different proportions to the navigation group.
**Three components advertise behaviour they do not have.**
`LayoutSplitter` is the worst component in the library. It declares
`resizeStart`, `resize` and `resizeEnd`, so a caller reasonably wires up
`resizeStart`, `resize` and `resizeEnd`, so a caller reasonably connects up
`@resize` and receives nothing, forever, with no error. It has no pointer
handling at all, and its props are `columns`, `gap` and `maxWidth`, copied
wholesale from a grid scaffold. `CustomScrollbar` is the same shape: a
@@ -22,9 +22,9 @@ genuinely does not need more. It is thin, not dishonest.
**Most of the group is built on Tailwind utilities.**
Seven of the ten use utility classes and `class:` conditionals rather than
`wire-*` classes and a local style block. They are theme-aware, because the
`wrn-*` classes and a local style block. They are theme-aware, because the
utilities carry arbitrary values bound to tokens
(`bg-[var(--wire-color-surface-soft)]`), but they depend on Tailwind being
(`bg-[var(--wrn-color-surface-soft)]`), but they depend on Tailwind being
present and produce a class explosion. `SectionHeader` has 38 such lines.
A correction that shaped this survey: when Tabs was rewritten it was described
@@ -50,7 +50,7 @@ audit rather than a rewrite.
### Styling
Every component in scope moves to `wire-*` classes with a local `style {}`
Every component in scope moves to `wrn-*` classes with a local `style {}`
block and `data-*` attributes for variants, matching PageHeader, DataTable,
Toaster and the navigation group.
@@ -95,7 +95,7 @@ something: `axis`, `thickness`, `thumb`, `track`, `maxHeight`.
### Kbd
Left as it is, beyond the `wire-*` migration. It is thin because a `<kbd>`
Left as it is, beyond the `wrn-*` migration. It is thin because a `<kbd>`
is thin.
### Runtime budget