fix(ui): refine responsive catalog navigation
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s

This commit is contained in:
2026-08-25 13:46:19 +05:30
parent 9f6552ea62
commit 64db7da08d
6 changed files with 104 additions and 29 deletions
+1 -1
View File
@@ -688,7 +688,7 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
"version": "0.8.42",
"version": "0.8.43",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
+20 -6
View File
@@ -193,11 +193,12 @@ component MegaMenu {
class="wrn-mega__trigger"
aria-haspopup="true"
aria-expanded='{visible}'
aria-label='{label}'
@click='activatePanel(event)'
@mouseenter='hoverPanel(event)'
>
<span class='wrn-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
<span class="wrn-mega__trigger-label">{label}</span>
<span class="wrn-mega__trigger-label" data-label='{label}' aria-hidden="true"></span>
<span class="wrn-mega__arrow" aria-hidden="true">&#8250;</span>
</button>
@@ -242,7 +243,13 @@ component MegaMenu {
<li>
<a class="wrn-mega__link" href='{item.href || "#"}' data-wrn-roving-item="true" aria-disabled='{item.disabled ? "true" : "false"}' @click='choose(item)'>
{#if item.image}<img class="wrn-mega__link-image" src='{item.image}' alt="" loading="lazy" />{/if}
{#if item.icon}<span class='wrn-mega__link-icon {item.icon}' aria-hidden="true"></span>{/if}
{#if item.icon}
<span class='wrn-mega__link-icon {item.icon}' aria-hidden="true"></span>
{:else if variant === "catalog"}
<span class="wrn-mega__link-icon wrn-mega__link-icon--fallback" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M3 9h18"></path><path d="M9 21V9"></path></svg>
</span>
{/if}
<span class="wrn-mega__link-body">
{#if item.eyebrow}<span class="wrn-mega__link-eyebrow">{item.eyebrow}</span>{/if}
<span class="wrn-mega__link-label">{item.label}{#if item.badge}<span class="wrn-mega__badge">{item.badge}</span>{/if}</span>
@@ -328,6 +335,10 @@ component MegaMenu {
color: var(--wrn-color-text);
}
.wrn-mega__trigger-label::before {
content: attr(data-label);
}
.wrn-mega__trigger:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
@@ -489,9 +500,9 @@ component MegaMenu {
.wrn-mega[data-variant="catalog"] .wrn-mega__column-header { width: 100%; min-width: 0; margin: 0 0 0.85rem; padding: 0 0 0.65rem; border: 0; border-bottom: 1px solid var(--wrn-color-border); border-radius: 0; background: transparent; }
.wrn-mega[data-variant="catalog"] .wrn-mega__heading { margin: 0; color: var(--wrn-color-text-muted); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
.wrn-mega[data-variant="catalog"] .wrn-mega__column-description { margin: 0.3rem 0 0; }
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.35rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link { min-height: 3.25rem; padding: 0.7rem 0.8rem; align-items: center; border: 1px solid var(--wrn-color-border); background: color-mix(in srgb, var(--wrn-color-surface-soft) 55%, transparent); }
.wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: color-mix(in srgb, var(--mega-accent) 45%, var(--wrn-color-border)); background: color-mix(in srgb, var(--mega-accent) 7%, var(--wrn-color-surface)); transform: translateY(-1px); }
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.3rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link { min-height: 2.7rem; padding: 0.45rem 0.55rem; align-items: center; gap: 0.55rem; border: 1px solid transparent; background: transparent; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: color-mix(in srgb, var(--mega-accent) 28%, var(--wrn-color-border)); background: color-mix(in srgb, var(--mega-accent) 7%, var(--wrn-color-surface)); transform: translateY(-1px); }
.wrn-mega[data-variant="catalog"] .wrn-mega__column-action { margin-top: auto; padding-top: 1rem; }
.wrn-mega[data-variant="dark"] .wrn-mega__panel { color: #f8fafc; border-color: #2b2b2b; background: #151515; }
.wrn-mega[data-variant="dark"] :is(.wrn-mega__heading, .wrn-mega__link, .wrn-mega__featured strong) { color: #f8fafc; }
@@ -518,7 +529,10 @@ component MegaMenu {
min-width: 0;
}
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon { width: 1.75rem; height: 1.75rem; flex: 0 0 1.75rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon { width: 1.65rem; height: 1.65rem; flex: 0 0 1.65rem; border: 1px solid color-mix(in srgb, var(--mega-accent) 22%, var(--wrn-color-border)); border-radius: 0.45rem; font-size: 0.9rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon--fallback svg { width: 0.9rem; height: 0.9rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-label { font-size: 0.78rem; line-height: 1.25; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-description { font-size: 0.7rem; line-height: 1.35; }
.wrn-mega__link-label {
font-size: 0.88rem;
+42 -20
View File
@@ -100,19 +100,29 @@ size: string = "default"
}
return [{ heading: item.description || "", items: legacy }]
}
client function toggleNavigation() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
client function setMobileNavigation(open, trigger) {
const root = trigger && trigger.closest ? trigger.closest("[data-wrn-navbar]") : null
if (!root) return
const collapse = root.querySelector(".wrn-navbar__collapse")
const toggle = root.querySelector(".wrn-navbar__toggle")
if (collapse) collapse.classList.toggle("is-open", open)
if (toggle) toggle.setAttribute("aria-expanded", open ? "true" : "false")
}
client function selectItem(item, level) {
mobileOpen = false
client function toggleNavigation(event) {
const open = event.currentTarget.getAttribute("aria-expanded") !== "true"
setMobileNavigation(open, event.currentTarget)
output.toggle({ open: open })
output[open ? "open" : "close"]({ source: "mobile" })
}
client function selectItem(item, level, event) {
setMobileNavigation(false, event.currentTarget)
output.select({ item: item, value: item.value || "", level: level })
}
client function selectAction(item) {
mobileOpen = false
client function selectAction(item, event) {
setMobileNavigation(false, event.currentTarget)
output.action({ item: item, value: item.value || "" })
}
@@ -161,6 +171,7 @@ size: string = "default"
</div>
<div class="wrn-navbar__main">
<slot name="brand">
{#if brand.logo || brand.icon || brand.label || brand.description}
<a class="wrn-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
{#if brand.logo}
@@ -177,6 +188,7 @@ size: string = "default"
{/if}
</a>
{/if}
</slot>
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
@@ -187,22 +199,26 @@ size: string = "default"
data-active="{isItemActive(item) ? 'true' : 'false'}"
label="{item.label}"
icon="{item.icon || ''}"
columns="{megaColumns(item)}"
rail="{megaData(item).rail || []}"
featured="{megaData(item).featured || {}}"
columns={megaColumns(item)}
rail={megaData(item).rail || []}
featured={megaData(item).featured || {}}
variant="{megaData(item).variant || 'default'}"
density="{megaData(item).density || 'default'}"
panelWidth="{megaData(item).panelWidth || 'xl'}"
panelAlign="{megaData(item).panelAlign || 'start'}"
columnCount="{megaData(item).columnCount || 0}"
fullWidth="{megaData(item).fullWidth || false}"
openOnHover="{openOnHover}"
columnCount={megaData(item).columnCount || 0}
fullWidth={megaData(item).fullWidth || false}
openOnHover={openOnHover}
activeCategory="{megaData(item).activeCategory || ''}"
actionLabel="{megaData(item).actionLabel || ''}"
actionHref="{megaData(item).actionHref || ''}"
actionIcon="{megaData(item).actionIcon || 'icon-[lucide--arrow-right]'}"
footer="{megaData(item).footer || ''}"
/>
<a class="wrn-navbar__menu-link wrn-navbar__mobile-mega-link" data-wrn-roving-item="true" href="{item.href || megaData(item).actionHref || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{isItemActive(item) ? 'page' : 'false'}" @click="selectItem(item, 1, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
{:else if itemType(item) === "dropdown"}
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
@@ -218,13 +234,13 @@ size: string = "default"
{#if child.label}<strong class="wrn-navbar__group-title" data-t="{translationKey(child, 'label')}">{child.label}</strong>{/if}
{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}
{#each dropdownGroupItems(child) as nested}
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3, event)">
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
<span><strong data-t="{translationKey(nested, 'label')}">{nested.label}</strong>{#if nested.description}<small data-t="{translationKey(nested, 'description')}">{nested.description}</small>{/if}</span>
</a>
{/each}
{:else}
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2, event)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span><strong data-t="{translationKey(child, 'label')}">{child.label}</strong>{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}</span>
</a>
@@ -234,7 +250,7 @@ size: string = "default"
</div>
</details>
{:else}
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
@@ -243,19 +259,19 @@ size: string = "default"
</nav>
<div class="wrn-navbar__actions">
<slot name="actions" />
{#each actions as item}
<a class="wrn-navbar__action wrn-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
<a class="wrn-navbar__action wrn-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
{/each}
<slot name="actions" />
</div>
</div>
<div class="wrn-navbar__utilities"><slot name="utilities" /></div>
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation(event)">
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button>
</div>
@@ -672,6 +688,8 @@ size: string = "default"
background: currentColor;
}
.wrn-navbar__mobile-mega-link { display: none; }
@media (max-width: 900px) {
.wrn-navbar__main {
min-height: 3.5rem;
@@ -724,6 +742,8 @@ size: string = "default"
.wrn-navbar__mega .wrn-mega__trigger {
width: 100%;
}
.wrn-navbar__mega { display: none; }
.wrn-navbar__mobile-mega-link { display: flex; }
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
@@ -785,6 +805,8 @@ size: string = "default"
}
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__menu-link, .wrn-navbar__dropdown > summary, .wrn-navbar__action) { width: 100%; }
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__mega, .wrn-navbar__mega .wrn-mega__trigger) { width: 100%; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega { display: none; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mobile-mega-link { display: flex; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__panel { position: static; width: 100%; max-width: none; transform: none; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega[data-full-width="true"] .wrn-mega__panel { position: static; width: 100%; max-width: 100%; margin-top: 0.25rem; }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.42",
"version": "0.8.43",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -126,6 +126,42 @@ describe("@wrnexus/ui redesign component pack", () => {
expect(source).not.toContain(".wrn-navbar__topbar:not(:empty)");
});
test("Navbar exposes a brand slot while retaining the brand prop fallback", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain('<slot name="brand">');
expect(source).toContain("brand.logo || brand.icon || brand.label || brand.description");
});
test("Navbar renders slotted utilities before its built-in actions", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
const actionsStart = source.indexOf('<div class="wrn-navbar__actions">');
const slotIndex = source.indexOf('<slot name="actions" />', actionsStart);
const actionsLoop = source.indexOf("{#each actions as item}", actionsStart);
expect(slotIndex).toBeGreaterThan(actionsStart);
expect(slotIndex).toBeLessThan(actionsLoop);
});
test("Navbar passes structured MegaMenu props as expressions", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain("columns={megaColumns(item)}");
expect(source).toContain("rail={megaData(item).rail || []}");
expect(source).toContain("featured={megaData(item).featured || {}}");
expect(source).toContain("fullWidth={megaData(item).fullWidth || false}");
expect(source).not.toContain('columns="{megaColumns(item)}"');
});
test("Navbar replaces mega panels with direct links in collapsed navigation", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain("wrn-navbar__mobile-mega-link");
expect(source).toContain("item.href || megaData(item).actionHref");
expect(source).toContain('.wrn-navbar__mega { display: none; }');
expect(source).toContain('.wrn-navbar__mobile-mega-link { display: flex; }');
});
test("Navbar supports in-flow floating and elevated menu panels", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
const ast = parse(source);
+4 -1
View File
@@ -2928,6 +2928,8 @@ test("mega menu renders column groups in an anchored panel", async () => {
const trigger = dom.querySelector(".wrn-mega__trigger") as HTMLElement;
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(trigger.getAttribute("aria-haspopup")).toBe("true");
expect(trigger.getAttribute("aria-label")).toBe("Products");
expect(dom.querySelector(".wrn-mega__trigger-label")?.getAttribute("data-label")).toBe("Products");
const panel = dom.querySelector(".wrn-mega__panel") as HTMLElement;
expect(panel.getAttribute("data-wrn-anchored")).toBe("true");
@@ -2980,7 +2982,8 @@ test("catalog mega menu renders only the active rail category", async () => {
expect(source).toContain("overflow-y: auto");
expect(source).not.toContain("@mouseenter='chooseCategory(item, index)'");
expect(dom.querySelector(".wrn-mega__column-header")?.textContent).toContain("Sales");
expect(dom.querySelectorAll(".wrn-mega__link-icon")).toHaveLength(0);
expect(dom.querySelectorAll(".wrn-mega__link-icon")).toHaveLength(1);
expect(dom.querySelector(".wrn-mega__link-icon--fallback svg")).not.toBeNull();
expect(dom.querySelectorAll(".wrn-mega__link-description")).toHaveLength(0);
expect(source).toContain("border-bottom: 1px solid var(--wrn-color-border)");
expect(source).toContain('.wrn-mega__rail-icon > span');