// MegaMenu -- a trigger and a wide panel of grouped links. // // // // Deliberately one level deep. A mega menu exists to show breadth flat, so // everything is one click away; nesting inside the panel buries content behind // hover-within-hover and is close to unusable by keyboard and touch. Use Nav // when you actually want cascading submenus. // // The panel carries data-wrn-anchored so the runtime clamp keeps it inside the // viewport instead of hanging off the edge of a wide layout. // // NOTE: the style block uses /* */ comments only -- // is not a CSS comment // and silently swallows the rule that follows it. component MegaMenu { outputs { open(payload: { sourceEvent: Event }) close(payload: { reason: string }) select(payload: { item: object; value: string }) category(payload: { item: object; value: string; index: number }) } 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 openOnHover: boolean = true activeCategory: string = "" actionLabel: string = "" actionHref: string = "" actionIcon: string = "icon-[lucide--arrow-right]" footer: string = "" defaultOpen: boolean = false class: string = "" } state visible = defaultOpen state closeTimer = null state openTimer = null state selectedCategory = activeCategory functions { shared function columnList() { return Array.isArray(columns) ? columns : [] } shared function itemsOf(column) { return column && Array.isArray(column.items) ? column.items : [] } shared function railList() { return Array.isArray(rail) ? rail : [] } shared function categoryValue(item) { return item ? String(item.value || item.href || item.label || "") : "" } shared function activeCategoryIndex() { const entries = railList() if (!entries.length) { return 0 } if (!selectedCategory) { return 0 } for (let index = 0; index < entries.length; index += 1) { if (categoryValue(entries[index]) === selectedCategory) { return index } } return 0 } shared function visibleColumns() { const entries = columnList() if (variant !== "catalog") { return entries } const selected = entries[activeCategoryIndex()] return selected ? [selected] : [] } client function showPanel(sourceEvent) { cancelScheduledOpen() cancelScheduledHide() visible = true output.open({ sourceEvent: sourceEvent }) } client function hidePanel(reason) { cancelScheduledOpen() cancelScheduledHide() visible = false output.close({ reason: reason }) } client function cancelScheduledHide() { if (closeTimer) { window.clearTimeout(closeTimer) closeTimer = null } } client function cancelScheduledOpen() { if (openTimer) { window.clearTimeout(openTimer) openTimer = null } } client function scheduleOpen(sourceEvent) { cancelScheduledOpen() cancelScheduledHide() openTimer = window.setTimeout(function () { openTimer = null showPanel(sourceEvent) }, 140) } client function scheduleHide() { cancelScheduledHide() closeTimer = window.setTimeout(function () { closeTimer = null hidePanel("pointer-leave") }, 220) } client function togglePanel(sourceEvent) { if (visible) { hidePanel("toggle") } else { showPanel(sourceEvent) } } client function activatePanel(sourceEvent) { if (openOnHover) { showPanel(sourceEvent) } else { togglePanel(sourceEvent) } } client function hoverPanel(sourceEvent) { if (openOnHover) { scheduleOpen(sourceEvent) } } client function chooseCategory(item, index) { selectedCategory = categoryValue(item) output.category({ item: item, value: selectedCategory, index: index }) } client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Escape" && visible) { sourceEvent.preventDefault() hidePanel("escape") } } client function choose(item) { if (item.disabled) { return } output.select({ item: item, value: item.value || item.label || "" }) hidePanel("select") } } view { {label} › {#if railList().length > 0} {/if} {#each visibleColumns() as column} {#if column.eyebrow}{column.eyebrow}{/if} {column.heading} {#if column.description}{column.description}{/if} {#if column.image}{/if} {#each itemsOf(column) as item} {#if item.image}{/if} {#if item.eyebrow}{item.eyebrow}{/if} {item.label}{#if item.badge}{item.badge}{/if} {item.description} {/each} {#if column.actionLabel}{column.actionLabel}{/if} {/each} {#if featured.title || featured.image} {/if} {#if footer || actionLabel}{/if} } style { .wrn-mega { --mega-accent: var(--wrn-color-primary); position: relative; display: inline-block; max-width: 100%; } .wrn-mega[data-color="secondary"] { --mega-accent: var(--wrn-color-secondary); } .wrn-mega[data-color="success"] { --mega-accent: var(--wrn-color-success); } .wrn-mega[data-color="danger"] { --mega-accent: var(--wrn-color-danger); } .wrn-mega[data-color="info"] { --mega-accent: var(--wrn-color-info); } .wrn-mega[data-size="sm"] { font-size: 0.82rem; } .wrn-mega[data-size="lg"] { font-size: 1rem; } .wrn-mega__trigger { appearance: none; display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.45rem 0.7rem; border: 1px solid transparent; border-radius: var(--wrn-radius-sm); background: transparent; color: var(--wrn-color-text-muted); font: inherit; font-size: 0.9rem; font-weight: 600; cursor: pointer; } .wrn-mega__trigger:hover, .wrn-mega[data-open="true"] .wrn-mega__trigger { background: var(--wrn-color-surface-soft); color: var(--wrn-color-text); } .wrn-mega__trigger:focus-visible { outline: 2px solid var(--mega-accent); outline-offset: 2px; } .wrn-mega__arrow { display: inline-block; transition: transform 160ms ease; } .wrn-mega[data-open="true"] .wrn-mega__arrow { transform: rotate(90deg); } /* * The panel is offset below the trigger for looks, which used to close the * menu on the way to it: that offset is dead space belonging to neither * element, so crossing it fired mouseleave on the root. The bridge below * covers the gap with a descendant, so the pointer never actually leaves. */ .wrn-mega__panel::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 0.85rem; } .wrn-mega__panel { position: absolute; z-index: 1200; top: calc(100% + 0.4rem); left: 0; width: max-content; max-width: min(60rem, calc(100vw - 2rem)); max-height: calc(100dvh - 5.5rem); padding: 1rem; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); } /* Keep SSR output closed before the reactive runtime hydrates it. */ .wrn-mega__panel[data-show="false"] { display: none; } .wrn-mega__panel[data-show="true"] { display: flex; } .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 { width: calc(100vw - 2rem); 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; min-height: 0; max-height: calc(100dvh - 8rem); flex: 1; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1.25rem; overflow: hidden; } .wrn-mega__columns { display: grid; min-height: 0; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 1.25rem; overflow-y: auto; overscroll-behavior: contain; } .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; min-height: 0; padding: 0.75rem; align-content: start; gap: 0.25rem; overflow-y: auto; overscroll-behavior: contain; border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface-soft); } .wrn-mega__rail-link { appearance: none; display: flex; width: 100%; padding: 0.65rem; align-items: flex-start; gap: 0.65rem; color: var(--wrn-color-text); border: 0; border-radius: var(--wrn-radius-sm); background: transparent; font: inherit; text-align: left; text-decoration: none; cursor: pointer; } .wrn-mega__rail-link:hover { background: var(--wrn-color-surface); } .wrn-mega__rail-link[data-active="true"] { color: var(--mega-accent); background: var(--wrn-color-surface); box-shadow: inset 3px 0 0 var(--mega-accent); } .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; } .wrn-mega__heading { margin: 0 0 0.5rem; color: var(--wrn-color-text-muted); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; 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; margin: 0; padding: 0; list-style: none; } .wrn-mega__link { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.45rem 0.5rem; border-radius: var(--wrn-radius-sm); color: var(--wrn-color-text); 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__panel { height: min(36rem, calc(100dvh - 6.5rem)); } .wrn-mega[data-variant="catalog"] .wrn-mega__layout { grid-template-columns: minmax(14rem, 17rem) minmax(0, 1fr); gap: 0; } .wrn-mega[data-variant="catalog"] .wrn-mega__rail { border-radius: calc(var(--wrn-radius-lg) - 0.25rem); } .wrn-mega[data-variant="catalog"] .wrn-mega__columns { padding: 0.35rem 0.5rem 0.75rem 1.25rem; grid-template-columns: minmax(0, 1fr); } .wrn-mega[data-variant="catalog"] .wrn-mega__column { display: flex; min-height: 100%; flex-direction: column; } .wrn-mega[data-variant="catalog"] .wrn-mega__heading { margin: 0 0 1rem; color: var(--wrn-color-text); font-size: 1rem; letter-spacing: 0; text-transform: none; } .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; border: 1px solid transparent; } .wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: var(--wrn-color-border); } .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; } .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); } .wrn-mega__link:focus-visible { outline: 2px solid var(--mega-accent); outline-offset: 2px; } .wrn-mega__link[aria-disabled="true"] { opacity: 0.5; pointer-events: none; } .wrn-mega__link-body { display: flex; flex-direction: column; min-width: 0; } .wrn-mega__link-label { font-size: 0.88rem; font-weight: 600; } .wrn-mega__link-description { color: var(--wrn-color-text-muted); font-size: 0.78rem; line-height: 1.5; } .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; } /* * On a phone the panel stops floating and becomes part of the flow. A * pointer-opened overlay pinned to a trigger is unreachable on touch, and * a 60rem grid has nowhere to go on a 360px screen. */ @media (max-width: 767px) { .wrn-mega { display: block; } .wrn-mega__panel { position: static; width: auto; max-width: 100%; max-height: none; overflow: visible; box-shadow: none; } .wrn-mega__panel[data-show="false"] { display: none; } .wrn-mega__columns { grid-template-columns: 1fr; } .wrn-mega__layout { max-height: none; grid-template-columns: 1fr; overflow: visible; } .wrn-mega__rail, .wrn-mega__columns { overflow: visible; } .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; } .wrn-mega[data-variant="catalog"] .wrn-mega__panel { height: auto; } .wrn-mega[data-variant="catalog"] .wrn-mega__layout { grid-template-columns: 1fr; } .wrn-mega[data-variant="catalog"] .wrn-mega__columns { padding: 1rem 0 0; } .wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: 1fr; } } } }
{column.eyebrow}
{column.description}