feat(ui): expand mega menu layout system
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m2s

This commit is contained in:
2026-08-24 21:15:50 +05:30
parent 270040fb5f
commit a1a2947910
7 changed files with 242 additions and 36 deletions
+1 -1
View File
@@ -688,7 +688,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.27", "version": "0.8.28",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
+1 -1
View File
@@ -55,7 +55,7 @@
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890", "packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad", "packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b", "packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
"packages/ui/components/MegaMenu.wrn": "6a6db166259f8f2bd7ac8fbb21dba6d915c4b936d87b19805caa78189f54730a", "packages/ui/components/MegaMenu.wrn": "ab065c8e837da5cb69f70f095a2dbed7fc0170f7ae10e758299464b1bf3b6350",
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5", "packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5", "packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2", "packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
+1 -1
View File
@@ -808,7 +808,7 @@ Show responsive hierarchical navigation with home support, separators, current-p
Theme-aware, responsive mega menu component. Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"` - Mount: `data-component="MegaMenu"`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""` - Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Slots: `default` - Slots: `default`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })` - Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
+77
View File
@@ -8345,6 +8345,83 @@
"default": "[]", "default": "[]",
"options": [] "options": []
}, },
{
"name": "rail",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "featured",
"type": "Record<string, unknown>",
"required": false,
"default": "{}",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "density",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "panelWidth",
"type": "string",
"required": false,
"default": "\"xl\"",
"options": []
},
{
"name": "panelAlign",
"type": "string",
"required": false,
"default": "\"start\"",
"options": []
},
{
"name": "columnCount",
"type": "number",
"required": false,
"default": "0",
"options": []
},
{
"name": "fullWidth",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "actionLabel",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionHref",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--arrow-right]\"",
"options": []
},
{ {
"name": "footer", "name": "footer",
"type": "string", "type": "string",
+109 -18
View File
@@ -26,6 +26,17 @@ component MegaMenu {
label: string = "Menu" label: string = "Menu"
icon: string = "" icon: string = ""
columns: unknown[] = [] columns: unknown[] = []
rail: unknown[] = []
featured: Record<string, unknown> = {}
variant: string = "default"
density: string = "default"
panelWidth: string = "xl"
panelAlign: string = "start"
columnCount: number = 0
fullWidth: boolean = false
actionLabel: string = ""
actionHref: string = ""
actionIcon: string = "icon-[lucide--arrow-right]"
footer: string = "" footer: string = ""
defaultOpen: boolean = false defaultOpen: boolean = false
class: string = "" class: string = ""
@@ -42,6 +53,10 @@ component MegaMenu {
return column && Array.isArray(column.items) ? column.items : [] return column && Array.isArray(column.items) ? column.items : []
} }
shared function railList() {
return Array.isArray(rail) ? rail : []
}
client function showPanel(sourceEvent) { client function showPanel(sourceEvent) {
visible = true visible = true
output.open({ sourceEvent: sourceEvent }) output.open({ sourceEvent: sourceEvent })
@@ -84,6 +99,12 @@ component MegaMenu {
data-color='{color}' data-color='{color}'
data-size='{size}' data-size='{size}'
data-open='{visible}' data-open='{visible}'
data-variant='{variant}'
data-density='{density}'
data-panel-width='{panelWidth}'
data-panel-align='{panelAlign}'
data-full-width='{fullWidth ? "true" : "false"}'
data-column-count='{columnCount}'
@keydown='handleKeydown(event)' @keydown='handleKeydown(event)'
@mouseleave='hidePanel("pointer-leave")' @mouseleave='hidePanel("pointer-leave")'
> >
@@ -107,40 +128,57 @@ component MegaMenu {
data-show="visible" data-show="visible"
data-wrn-roving="both" data-wrn-roving="both"
> >
<div class="wrn-mega__layout">
{#if railList().length > 0}
<aside class="wrn-mega__rail">
{#each railList() as item}
<a class="wrn-mega__rail-link" href='{item.href || "#"}' @click='choose(item)'>
{#if item.icon}<span class='wrn-mega__rail-icon {item.icon}' aria-hidden="true"></span>{/if}
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
</a>
{/each}
</aside>
{/if}
<div class="wrn-mega__columns"> <div class="wrn-mega__columns">
{#each columnList() as column} {#each columnList() as column}
<section class="wrn-mega__column"> <section class="wrn-mega__column">
{#if column.eyebrow}<p class="wrn-mega__eyebrow">{column.eyebrow}</p>{/if}
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3> <h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
{#if column.description}<p class="wrn-mega__column-description">{column.description}</p>{/if}
{#if column.image}<img class="wrn-mega__column-image" src='{column.image}' alt='{column.imageAlt || ""}' loading="lazy" />{/if}
<ul class="wrn-mega__list"> <ul class="wrn-mega__list">
{#each itemsOf(column) as item} {#each itemsOf(column) as item}
<li> <li>
<a <a class="wrn-mega__link" href='{item.href || "#"}' data-wrn-roving-item="true" aria-disabled='{item.disabled ? "true" : "false"}' @click='choose(item)'>
class="wrn-mega__link" {#if item.image}<img class="wrn-mega__link-image" src='{item.image}' alt="" loading="lazy" />{/if}
href='{item.href || "#"}' <span class='wrn-mega__link-icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
data-wrn-roving-item="true"
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wrn-mega__link-icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wrn-mega__link-body"> <span class="wrn-mega__link-body">
<span class="wrn-mega__link-label">{item.label}</span> {#if item.eyebrow}<span class="wrn-mega__link-eyebrow">{item.eyebrow}</span>{/if}
<span class="wrn-mega__link-description" data-show="item.description"> <span class="wrn-mega__link-label">{item.label}{#if item.badge}<span class="wrn-mega__badge">{item.badge}</span>{/if}</span>
{item.description} <span class="wrn-mega__link-description" data-show="item.description">{item.description}</span>
</span>
</span> </span>
</a> </a>
</li> </li>
{/each} {/each}
</ul> </ul>
{#if column.actionLabel}<a class="wrn-mega__column-action" href='{column.actionHref || "#"}'>{column.actionLabel}<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span></a>{/if}
</section> </section>
{/each} {/each}
</div> </div>
<p class="wrn-mega__footer" data-show="footer">{footer}</p> {#if featured.title || featured.image}
<aside class="wrn-mega__featured">
{#if featured.eyebrow}<p class="wrn-mega__eyebrow">{featured.eyebrow}</p>{/if}
{#if featured.image}<img src='{featured.image}' alt='{featured.imageAlt || ""}' loading="lazy" />{/if}
{#if featured.title}<strong>{featured.title}</strong>{/if}
{#if featured.description}<p>{featured.description}</p>{/if}
{#if featured.actionLabel}<a href='{featured.actionHref || "#"}'>{featured.actionLabel}<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span></a>{/if}
</aside>
{/if}
</div>
{#if footer || actionLabel}<div class="wrn-mega__footer"><span>{footer}</span>{#if actionLabel}<a href='{actionHref || "#"}'>{actionLabel}<span class='{actionIcon}' aria-hidden="true"></span></a>{/if}</div>{/if}
<slot /> <slot />
</div> </div>
</div> </div>
@@ -231,7 +269,7 @@ component MegaMenu {
.wrn-mega__panel { .wrn-mega__panel {
position: absolute; position: absolute;
z-index: 40; z-index: 1200;
top: calc(100% + 0.4rem); top: calc(100% + 0.4rem);
left: 0; left: 0;
width: max-content; width: max-content;
@@ -243,12 +281,34 @@ component MegaMenu {
box-shadow: var(--wrn-shadow-1); box-shadow: var(--wrn-shadow-1);
} }
.wrn-mega[data-panel-width="lg"] .wrn-mega__panel { width: min(48rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="xl"] .wrn-mega__panel { width: min(72rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="2xl"] .wrn-mega__panel { width: min(88rem, calc(100vw - 2rem)); }
.wrn-mega[data-full-width="true"] .wrn-mega__panel { position: fixed; top: auto; right: 1rem; left: 1rem; width: auto; max-width: none; }
.wrn-mega[data-panel-align="center"] .wrn-mega__panel { left: 50%; transform: translateX(-50%); }
.wrn-mega[data-panel-align="end"] .wrn-mega__panel { right: 0; left: auto; }
.wrn-mega__layout { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1.25rem; }
.wrn-mega__columns { .wrn-mega__columns {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1.25rem; gap: 1.25rem;
} }
.wrn-mega[data-column-count="2"] .wrn-mega__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.wrn-mega[data-column-count="3"] .wrn-mega__columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.wrn-mega[data-column-count="4"] .wrn-mega__columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.wrn-mega[data-column-count="5"] .wrn-mega__columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.wrn-mega__rail { display: grid; min-width: 13rem; padding: 0.75rem; align-content: start; gap: 0.25rem; border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface-soft); }
.wrn-mega__rail-link { display: flex; padding: 0.65rem; align-items: flex-start; gap: 0.65rem; color: var(--wrn-color-text); border-radius: var(--wrn-radius-sm); text-decoration: none; }
.wrn-mega__rail-link:hover { background: var(--wrn-color-surface); }
.wrn-mega__rail-link > span:last-child { display: grid; gap: 0.2rem; }
.wrn-mega__rail-link strong { font-size: 0.82rem; }
.wrn-mega__rail-link small { max-width: 18ch; color: var(--wrn-color-text-muted); font-size: 0.72rem; line-height: 1.45; }
.wrn-mega__rail-icon { display: grid; width: 2.4rem; height: 2.4rem; flex: none; place-items: center; color: var(--mega-accent); border-radius: 0.7rem; background: color-mix(in srgb, var(--mega-accent) 12%, transparent); font-size: 1.15rem; }
.wrn-mega__column { .wrn-mega__column {
min-width: 0; min-width: 0;
} }
@@ -262,6 +322,10 @@ component MegaMenu {
text-transform: uppercase; text-transform: uppercase;
} }
.wrn-mega__eyebrow, .wrn-mega__link-eyebrow { margin: 0 0 0.35rem; color: var(--wrn-color-text-muted); font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.wrn-mega__column-description { margin: -0.25rem 0 0.75rem; color: var(--wrn-color-text-muted); font-size: 0.75rem; line-height: 1.5; }
.wrn-mega__column-image { width: 100%; aspect-ratio: 16 / 7; margin-bottom: 0.65rem; object-fit: cover; border-radius: var(--wrn-radius-sm); }
.wrn-mega__list { .wrn-mega__list {
display: grid; display: grid;
gap: 0.15rem; gap: 0.15rem;
@@ -280,6 +344,26 @@ component MegaMenu {
text-decoration: none; text-decoration: none;
} }
.wrn-mega__link-icon { display: grid; width: 2rem; height: 2rem; flex: none; place-items: center; color: var(--mega-accent); border-radius: 999px; background: color-mix(in srgb, var(--mega-accent) 10%, var(--wrn-color-surface-soft)); }
.wrn-mega__link-image { width: 2.4rem; height: 2.4rem; flex: none; object-fit: cover; border-radius: 0.65rem; }
.wrn-mega__badge { display: inline-flex; margin-left: 0.4rem; padding: 0.12rem 0.35rem; align-items: center; color: var(--mega-accent); border: 1px solid color-mix(in srgb, var(--mega-accent) 35%, transparent); border-radius: 999px; font-size: 0.6rem; font-weight: 700; }
.wrn-mega__column-action, .wrn-mega__featured a, .wrn-mega__footer a { display: inline-flex; margin-top: 0.7rem; align-items: center; gap: 0.35rem; color: var(--mega-accent); font-size: 0.78rem; font-weight: 700; text-decoration: none; }
.wrn-mega__featured { display: grid; width: 14rem; padding-left: 1.25rem; align-content: start; gap: 0.55rem; border-left: 1px solid var(--wrn-color-border); }
.wrn-mega__featured img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--wrn-radius-lg); }
.wrn-mega__featured strong { font-size: 0.85rem; line-height: 1.35; }
.wrn-mega__featured p { margin: 0; color: var(--wrn-color-text-muted); font-size: 0.75rem; line-height: 1.5; }
.wrn-mega[data-density="compact"] :is(.wrn-mega__panel, .wrn-mega__rail) { padding: 0.7rem; }
.wrn-mega[data-density="compact"] .wrn-mega__columns { gap: 0.65rem; }
.wrn-mega[data-variant="icon-grid"] .wrn-mega__list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.35rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link { padding-block: 0.28rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__heading { color: var(--wrn-color-text); font-size: 0.78rem; letter-spacing: 0; text-transform: none; }
.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; }
.wrn-mega[data-variant="dark"] :is(.wrn-mega__link-description, .wrn-mega__column-description, .wrn-mega__eyebrow, .wrn-mega__featured p) { color: #a3a3a3; }
.wrn-mega[data-variant="dark"] .wrn-mega__link:hover { background: #242424; }
.wrn-mega__link:hover { .wrn-mega__link:hover {
background: var(--wrn-color-surface-soft); background: var(--wrn-color-surface-soft);
} }
@@ -312,8 +396,12 @@ component MegaMenu {
} }
.wrn-mega__footer { .wrn-mega__footer {
display: flex;
margin: 1rem 0 0; margin: 1rem 0 0;
padding-top: 0.75rem; padding-top: 0.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
border-top: 1px solid var(--wrn-color-border); border-top: 1px solid var(--wrn-color-border);
color: var(--wrn-color-text-muted); color: var(--wrn-color-text-muted);
font-size: 0.8rem; font-size: 0.8rem;
@@ -339,6 +427,9 @@ component MegaMenu {
.wrn-mega__columns { .wrn-mega__columns {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.wrn-mega__layout { grid-template-columns: 1fr; }
.wrn-mega__featured { width: auto; padding: 1rem 0 0; border-top: 1px solid var(--wrn-color-border); border-left: 0; }
.wrn-mega[data-variant="icon-grid"] .wrn-mega__list { grid-template-columns: 1fr; }
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.27", "version": "0.8.28",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+38
View File
@@ -2938,6 +2938,44 @@ test("mega menu columns take roving focus and survive an empty column list", asy
expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull(); expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull();
}); });
test("mega menu supports rails, icons, badges, media, featured content and footer actions", async () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
const html = await renderComponent(source, {
label: "Explore",
variant: "icon-grid",
panelWidth: "2xl",
columnCount: 3,
rail: [{ label: "Enterprise", description: "For larger teams", icon: "icon-enterprise" }],
columns: [
{
heading: "Products",
description: "Choose a workspace",
image: "/products.png",
items: [
{ label: "CRM", description: "Customer records", icon: "icon-crm", badge: "New" },
],
actionLabel: "All products",
},
],
featured: {
eyebrow: "What's new",
title: "Release notes",
image: "/release.png",
actionLabel: "See more",
},
footer: "One connected platform",
actionLabel: "View everything",
});
const dom = mountHtml(html);
expect(dom.querySelector(".wrn-mega")?.getAttribute("data-variant")).toBe("icon-grid");
expect(dom.querySelector(".wrn-mega__rail-link")?.textContent).toContain("Enterprise");
expect(dom.querySelector(".wrn-mega__badge")?.textContent).toContain("New");
expect(dom.querySelector(".wrn-mega__column-image")).not.toBeNull();
expect(dom.querySelector(".wrn-mega__featured")?.textContent).toContain("Release notes");
expect(dom.querySelector(".wrn-mega__footer")?.textContent).toContain("View everything");
});
test("sidebar renders single items, labelled groups and nested levels", async () => { test("sidebar renders single items, labelled groups and nested levels", async () => {
const source = readFileSync(uiComponentPath("Sidebar"), "utf8"); const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
const html = await renderComponent(source, { const html = await renderComponent(source, {