feat(ui): expand mega menu layout system
This commit is contained in:
@@ -688,7 +688,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.27",
|
||||
"version": "0.8.28",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
||||
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
||||
"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/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
||||
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
||||
|
||||
@@ -808,7 +808,7 @@ Show responsive hierarchical navigation with home support, separators, current-p
|
||||
Theme-aware, responsive mega menu component.
|
||||
|
||||
- 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`
|
||||
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
|
||||
|
||||
|
||||
@@ -8345,6 +8345,83 @@
|
||||
"default": "[]",
|
||||
"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",
|
||||
"type": "string",
|
||||
|
||||
@@ -26,6 +26,17 @@ component MegaMenu {
|
||||
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 = ""
|
||||
@@ -42,6 +53,10 @@ component MegaMenu {
|
||||
return column && Array.isArray(column.items) ? column.items : []
|
||||
}
|
||||
|
||||
shared function railList() {
|
||||
return Array.isArray(rail) ? rail : []
|
||||
}
|
||||
|
||||
client function showPanel(sourceEvent) {
|
||||
visible = true
|
||||
output.open({ sourceEvent: sourceEvent })
|
||||
@@ -84,6 +99,12 @@ component MegaMenu {
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
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)'
|
||||
@mouseleave='hidePanel("pointer-leave")'
|
||||
>
|
||||
@@ -107,40 +128,57 @@ component MegaMenu {
|
||||
data-show="visible"
|
||||
data-wrn-roving="both"
|
||||
>
|
||||
<div class="wrn-mega__columns">
|
||||
{#each columnList() as column}
|
||||
<section class="wrn-mega__column">
|
||||
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
|
||||
<ul class="wrn-mega__list">
|
||||
{#each itemsOf(column) as item}
|
||||
<li>
|
||||
<a
|
||||
class="wrn-mega__link"
|
||||
href='{item.href || "#"}'
|
||||
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-label">{item.label}</span>
|
||||
<span class="wrn-mega__link-description" data-show="item.description">
|
||||
{item.description}
|
||||
<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">
|
||||
{#each columnList() as 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>
|
||||
{#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">
|
||||
{#each itemsOf(column) as item}
|
||||
<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}
|
||||
<span class='wrn-mega__link-icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
|
||||
<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>
|
||||
<span class="wrn-mega__link-description" data-show="item.description">{item.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/each}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</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>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#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>
|
||||
|
||||
<p class="wrn-mega__footer" data-show="footer">{footer}</p>
|
||||
{#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 />
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,7 +269,7 @@ component MegaMenu {
|
||||
|
||||
.wrn-mega__panel {
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
z-index: 1200;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
@@ -243,12 +281,34 @@ component MegaMenu {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
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 {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -262,6 +322,10 @@ component MegaMenu {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
@@ -280,6 +344,26 @@ component MegaMenu {
|
||||
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 {
|
||||
background: var(--wrn-color-surface-soft);
|
||||
}
|
||||
@@ -312,8 +396,12 @@ component MegaMenu {
|
||||
}
|
||||
|
||||
.wrn-mega__footer {
|
||||
display: flex;
|
||||
margin: 1rem 0 0;
|
||||
padding-top: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid var(--wrn-color-border);
|
||||
color: var(--wrn-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
@@ -339,6 +427,9 @@ component MegaMenu {
|
||||
.wrn-mega__columns {
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.27",
|
||||
"version": "0.8.28",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
|
||||
Reference in New Issue
Block a user