refactor: migrate legacy wire namespace to wrn
This commit is contained in:
@@ -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">‹</span>
|
||||
<span class="wire-pagination__step-label">{previousLabel}</span>
|
||||
<span class="wrn-pagination__step-icon" aria-hidden="true">‹</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">›</span>
|
||||
<span class="wrn-pagination__step-label">{nextLabel}</span>
|
||||
<span class="wrn-pagination__step-icon" aria-hidden="true">›</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"
|
||||
>›</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"
|
||||
>›</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 3–6. 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 3–6. 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
|
||||
|
||||
Reference in New Issue
Block a user