Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
345 lines
8.9 KiB
Plaintext
345 lines
8.9 KiB
Plaintext
// MegaMenu -- a trigger and a wide panel of grouped links.
|
|
//
|
|
// <MegaMenu label="Products" columns='[{"heading":"Platform",
|
|
// "items":[{"label":"Runtime","href":"/runtime"}]}]' />
|
|
//
|
|
// 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 {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="MegaMenu"
|
|
class='wire-mega {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-open='{visible}'
|
|
@keydown='handleKeydown(event)'
|
|
@mouseleave='hidePanel("pointer-leave")'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wire-mega__trigger"
|
|
aria-haspopup="true"
|
|
aria-expanded='{visible}'
|
|
@click='togglePanel(event)'
|
|
@mouseenter='showPanel(event)'
|
|
@focus='showPanel(event)'
|
|
>
|
|
<span class='wire-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
|
|
<span class="wire-mega__trigger-label">{label}</span>
|
|
<span class="wire-mega__arrow" aria-hidden="true">›</span>
|
|
</button>
|
|
|
|
<div
|
|
class="wire-mega__panel"
|
|
data-wrn-anchored="true"
|
|
data-show="visible"
|
|
data-wrn-roving="both"
|
|
>
|
|
<div class="wire-mega__columns">
|
|
{#each columnList() as column}
|
|
<section class="wire-mega__column">
|
|
<h3 class="wire-mega__heading" data-show="column.heading">{column.heading}</h3>
|
|
<ul class="wire-mega__list">
|
|
{#each itemsOf(column) as item}
|
|
<li>
|
|
<a
|
|
class="wire-mega__link"
|
|
href='{item.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
|
@click='choose(item)'
|
|
>
|
|
<span
|
|
class='wire-mega__link-icon {item.icon}'
|
|
data-show="item.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-mega__link-body">
|
|
<span class="wire-mega__link-label">{item.label}</span>
|
|
<span class="wire-mega__link-description" data-show="item.description">
|
|
{item.description}
|
|
</span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
{/each}
|
|
</div>
|
|
|
|
<p class="wire-mega__footer" data-show="footer">{footer}</p>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wire-mega {
|
|
--mega-accent: var(--wire-color-primary);
|
|
position: relative;
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-mega[data-color="secondary"] {
|
|
--mega-accent: var(--wire-color-secondary);
|
|
}
|
|
|
|
.wire-mega[data-color="success"] {
|
|
--mega-accent: var(--wire-color-success);
|
|
}
|
|
|
|
.wire-mega[data-color="danger"] {
|
|
--mega-accent: var(--wire-color-danger);
|
|
}
|
|
|
|
.wire-mega[data-color="info"] {
|
|
--mega-accent: var(--wire-color-info);
|
|
}
|
|
|
|
.wire-mega[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wire-mega[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wire-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(--wire-radius-sm);
|
|
background: transparent;
|
|
color: var(--wire-color-text-muted);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wire-mega__trigger:hover,
|
|
.wire-mega[data-open="true"] .wire-mega__trigger {
|
|
background: var(--wire-color-surface-soft);
|
|
color: var(--wire-color-text);
|
|
}
|
|
|
|
.wire-mega__trigger:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-mega__arrow {
|
|
display: inline-block;
|
|
transition: transform 160ms ease;
|
|
}
|
|
|
|
.wire-mega[data-open="true"] .wire-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.
|
|
*/
|
|
.wire-mega__panel::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 100%;
|
|
height: 0.6rem;
|
|
}
|
|
|
|
.wire-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(--wire-color-border);
|
|
border-radius: var(--wire-radius-lg);
|
|
background: var(--wire-color-surface);
|
|
box-shadow: var(--wire-shadow-1);
|
|
}
|
|
|
|
.wire-mega__columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.wire-mega__column {
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-mega__heading {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.wire-mega__list {
|
|
display: grid;
|
|
gap: 0.15rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wire-mega__link {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.5rem;
|
|
border-radius: var(--wire-radius-sm);
|
|
color: var(--wire-color-text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wire-mega__link:hover {
|
|
background: var(--wire-color-surface-soft);
|
|
}
|
|
|
|
.wire-mega__link:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-mega__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wire-mega__link-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-mega__link-label {
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wire-mega__link-description {
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.78rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.wire-mega__footer {
|
|
margin: 1rem 0 0;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--wire-color-border);
|
|
color: var(--wire-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) {
|
|
.wire-mega {
|
|
display: block;
|
|
}
|
|
|
|
.wire-mega__panel {
|
|
position: static;
|
|
width: auto;
|
|
max-width: 100%;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.wire-mega__columns {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
}
|
|
}
|