// 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 }) } props { color: string = "primary" size: string = "default" label: string = "Menu" icon: string = "" columns: unknown[] = [] footer: string = "" defaultOpen: boolean = false class: string = "" } state visible = defaultOpen functions { shared function columnList() { return Array.isArray(columns) ? columns : [] } shared function itemsOf(column) { return column && Array.isArray(column.items) ? column.items : [] } client function showPanel(sourceEvent) { visible = true output.open({ sourceEvent: sourceEvent }) } client function hidePanel(reason) { visible = false output.close({ reason: reason }) } client function togglePanel(sourceEvent) { if (visible) { hidePanel("toggle") } else { showPanel(sourceEvent) } } 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 {
{#each columnList() as column}

{column.heading}

{/each}
} 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.6rem; } .wrn-mega__panel { position: absolute; z-index: 40; top: calc(100% + 0.4rem); left: 0; width: max-content; max-width: min(60rem, calc(100vw - 2rem)); padding: 1rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); } .wrn-mega__columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 1.25rem; } .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__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: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 { margin: 1rem 0 0; padding-top: 0.75rem; 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%; box-shadow: none; } .wrn-mega__columns { grid-template-columns: 1fr; } } } }