From a1a2947910d7fc29ab7dcec35d053fb5c8107b2b Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 24 Aug 2026 21:15:50 +0530 Subject: [PATCH] feat(ui): expand mega menu layout system --- bun.lock | 2 +- docs/ui-visual-contract-0.8.json | 2 +- packages/ui/COMPONENTS.md | 2 +- packages/ui/component-reference.json | 77 +++++++++++++ packages/ui/components/MegaMenu.wrn | 155 +++++++++++++++++++++------ packages/ui/package.json | 2 +- packages/ui/test/ui.test.ts | 38 +++++++ 7 files changed, 242 insertions(+), 36 deletions(-) diff --git a/bun.lock b/bun.lock index 8a04dd29..637326bb 100644 --- a/bun.lock +++ b/bun.lock @@ -688,7 +688,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.27", + "version": "0.8.28", "dependencies": { "@wrnexus/core": "workspace:*", }, diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index e872b4d0..f4b56f55 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -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", diff --git a/packages/ui/COMPONENTS.md b/packages/ui/COMPONENTS.md index 02f51fcb..4f9d3b98 100644 --- a/packages/ui/COMPONENTS.md +++ b/packages/ui/COMPONENTS.md @@ -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 = {}`, `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 })` diff --git a/packages/ui/component-reference.json b/packages/ui/component-reference.json index c28029f9..f887e27c 100644 --- a/packages/ui/component-reference.json +++ b/packages/ui/component-reference.json @@ -8345,6 +8345,83 @@ "default": "[]", "options": [] }, + { + "name": "rail", + "type": "unknown[]", + "required": false, + "default": "[]", + "options": [] + }, + { + "name": "featured", + "type": "Record", + "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", diff --git a/packages/ui/components/MegaMenu.wrn b/packages/ui/components/MegaMenu.wrn index 49c904f2..cad3a993 100644 --- a/packages/ui/components/MegaMenu.wrn +++ b/packages/ui/components/MegaMenu.wrn @@ -26,6 +26,17 @@ component MegaMenu { label: string = "Menu" icon: string = "" columns: unknown[] = [] + rail: unknown[] = [] + featured: Record = {} + 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" > -
- {#each columnList() as column} -
-

{column.heading}

- + {#if column.actionLabel}{column.actionLabel}{/if} +
+ {/each} +
+ + {#if featured.title || featured.image} + + {/if} - + {#if footer || actionLabel}{/if} @@ -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; } } } } diff --git a/packages/ui/package.json b/packages/ui/package.json index b887985e..c1114308 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.27", + "version": "0.8.28", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/test/ui.test.ts b/packages/ui/test/ui.test.ts index 00780b3e..fabb4d50 100644 --- a/packages/ui/test/ui.test.ts +++ b/packages/ui/test/ui.test.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, {