From 64db7da08d722024d848ce6106df8b2561f3ae4b Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 25 Aug 2026 13:46:19 +0530 Subject: [PATCH] fix(ui): refine responsive catalog navigation --- bun.lock | 2 +- packages/ui/components/MegaMenu.wrn | 26 ++++++-- packages/ui/components/Navbar.wrn | 62 +++++++++++++------ packages/ui/package.json | 2 +- packages/ui/test/ui-redesign-contract.test.ts | 36 +++++++++++ packages/ui/test/ui.test.ts | 5 +- 6 files changed, 104 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index 9f48d326..837f2967 100644 --- a/bun.lock +++ b/bun.lock @@ -688,7 +688,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.42", + "version": "0.8.43", "dependencies": { "@wrnexus/core": "workspace:*", }, diff --git a/packages/ui/components/MegaMenu.wrn b/packages/ui/components/MegaMenu.wrn index e011868e..57196c63 100644 --- a/packages/ui/components/MegaMenu.wrn +++ b/packages/ui/components/MegaMenu.wrn @@ -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)' > - {label} + @@ -242,7 +243,13 @@ component MegaMenu {
  • {#if item.image}{/if} - {#if item.icon}{/if} + {#if item.icon} + + {:else if variant === "catalog"} + + {/if} {#if item.eyebrow}{item.eyebrow}{/if} {item.label}{#if item.badge}{item.badge}{/if} @@ -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; diff --git a/packages/ui/components/Navbar.wrn b/packages/ui/components/Navbar.wrn index 1402b4f2..4d3f4ba0 100644 --- a/packages/ui/components/Navbar.wrn +++ b/packages/ui/components/Navbar.wrn @@ -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"
    + {#if brand.logo || brand.icon || brand.label || brand.description} {#if brand.logo} @@ -177,6 +188,7 @@ size: string = "default" {/if} {/if} +
    {:else} - + {#if item.icon}{/if} {item.label} @@ -243,19 +259,19 @@ size: string = "default"
    + {#each actions as item} - + {#if item.icon}{/if} {item.label} {/each} -
    - @@ -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; } } diff --git a/packages/ui/package.json b/packages/ui/package.json index a8dfe039..7f2d7b49 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.42", + "version": "0.8.43", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/test/ui-redesign-contract.test.ts b/packages/ui/test/ui-redesign-contract.test.ts index 859f3075..203d9341 100644 --- a/packages/ui/test/ui-redesign-contract.test.ts +++ b/packages/ui/test/ui-redesign-contract.test.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(''); + 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('
    '); + const slotIndex = source.indexOf('', 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); diff --git a/packages/ui/test/ui.test.ts b/packages/ui/test/ui.test.ts index 4b478ae0..df715e5b 100644 --- a/packages/ui/test/ui.test.ts +++ b/packages/ui/test/ui.test.ts @@ -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');