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>
355 lines
9.7 KiB
Plaintext
355 lines
9.7 KiB
Plaintext
// Nav -- a navigation link list, flat or with submenus.
|
|
//
|
|
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
|
|
//
|
|
// An item carrying its own items array becomes a submenu. Depth is capped at
|
|
// three levels: this template language has no component recursion, so each
|
|
// level is written out, and three covers any realistic navigation.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
component Nav {
|
|
outputs {
|
|
select(payload: { item: object; value: string })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
items: unknown[] = []
|
|
active: string = ""
|
|
orientation: string = "horizontal"
|
|
label: string = "Main"
|
|
collapsible: boolean = true
|
|
toggleLabel: string = "Menu"
|
|
class: string = ""
|
|
}
|
|
|
|
state expanded = false
|
|
|
|
functions {
|
|
shared function itemList() {
|
|
return Array.isArray(items) ? items : []
|
|
}
|
|
|
|
shared function childrenOf(item) {
|
|
return item && Array.isArray(item.items) ? item.items : []
|
|
}
|
|
|
|
shared function isActive(item) {
|
|
return Boolean(item.value) && item.value === active
|
|
}
|
|
|
|
shared function rovingAxis() {
|
|
return orientation === "vertical" ? "vertical" : "horizontal"
|
|
}
|
|
|
|
client function choose(item) {
|
|
if (item.disabled) {
|
|
return
|
|
}
|
|
output.select({ item: item, value: item.value || "" })
|
|
}
|
|
|
|
client function toggleMenu() {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
|
|
view {
|
|
<nav
|
|
{...attrs}
|
|
data-ui-component="Nav"
|
|
class='wire-nav {class}'
|
|
data-orientation='{orientation}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-expanded='{expanded}'
|
|
role="navigation"
|
|
aria-label='{label}'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wire-nav__toggle"
|
|
data-show="collapsible"
|
|
aria-expanded='{expanded}'
|
|
aria-label='{toggleLabel}'
|
|
@click='toggleMenu()'
|
|
>
|
|
<span class="wire-nav__toggle-bar" aria-hidden="true"></span>
|
|
<span class="wire-nav__toggle-text">{toggleLabel}</span>
|
|
</button>
|
|
|
|
<ul class="wire-nav__list" data-wrn-roving='{rovingAxis()}'>
|
|
{#each itemList() as item}
|
|
<li class="wire-nav__item" data-has-children='{childrenOf(item).length > 0}'>
|
|
<a
|
|
class="wire-nav__link"
|
|
href='{item.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
data-active='{isActive(item)}'
|
|
aria-current='{isActive(item) ? "page" : "false"}'
|
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
|
@click='choose(item)'
|
|
>
|
|
<span
|
|
class='wire-nav__icon {item.icon}'
|
|
data-show="item.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-nav__label">{item.label}</span>
|
|
<span class="wire-nav__badge" data-show="item.badge">{item.badge}</span>
|
|
<span
|
|
class="wire-nav__arrow"
|
|
data-show="childrenOf(item).length > 0"
|
|
aria-hidden="true"
|
|
>›</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wire-nav__submenu"
|
|
data-wrn-anchored="true"
|
|
data-show="childrenOf(item).length > 0"
|
|
>
|
|
{#each childrenOf(item) as child}
|
|
<li class="wire-nav__item" data-has-children='{childrenOf(child).length > 0}'>
|
|
<a
|
|
class="wire-nav__link"
|
|
href='{child.href || "#"}'
|
|
data-active='{isActive(child)}'
|
|
aria-current='{isActive(child) ? "page" : "false"}'
|
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
|
@click='choose(child)'
|
|
>
|
|
<span
|
|
class='wire-nav__icon {child.icon}'
|
|
data-show="child.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-nav__label">{child.label}</span>
|
|
<span class="wire-nav__badge" data-show="child.badge">{child.badge}</span>
|
|
<span
|
|
class="wire-nav__arrow"
|
|
data-show="childrenOf(child).length > 0"
|
|
aria-hidden="true"
|
|
>›</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wire-nav__submenu wire-nav__submenu--level3"
|
|
data-wrn-anchored="true"
|
|
data-show="childrenOf(child).length > 0"
|
|
>
|
|
{#each childrenOf(child) as leaf}
|
|
<li class="wire-nav__item">
|
|
<a
|
|
class="wire-nav__link"
|
|
href='{leaf.href || "#"}'
|
|
data-active='{isActive(leaf)}'
|
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
|
@click='choose(leaf)'
|
|
>
|
|
<span
|
|
class='wire-nav__icon {leaf.icon}'
|
|
data-show="leaf.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-nav__label">{leaf.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<slot />
|
|
</nav>
|
|
}
|
|
|
|
style {
|
|
.wire-nav {
|
|
--nav-accent: var(--wire-color-primary);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-nav[data-color="secondary"] {
|
|
--nav-accent: var(--wire-color-secondary);
|
|
}
|
|
|
|
.wire-nav[data-color="success"] {
|
|
--nav-accent: var(--wire-color-success);
|
|
}
|
|
|
|
.wire-nav[data-color="danger"] {
|
|
--nav-accent: var(--wire-color-danger);
|
|
}
|
|
|
|
.wire-nav[data-color="info"] {
|
|
--nav-accent: var(--wire-color-info);
|
|
}
|
|
|
|
.wire-nav[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wire-nav[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wire-nav__list {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wire-nav[data-orientation="vertical"] .wire-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.wire-nav__item {
|
|
position: relative;
|
|
}
|
|
|
|
.wire-nav__link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.45rem 0.7rem;
|
|
border-radius: var(--wire-radius-sm);
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wire-nav__link:hover {
|
|
background: var(--wire-color-surface-soft);
|
|
color: var(--wire-color-text);
|
|
}
|
|
|
|
.wire-nav__link:focus-visible {
|
|
outline: 2px solid var(--nav-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-nav__link[data-active="true"] {
|
|
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
|
color: var(--nav-accent);
|
|
}
|
|
|
|
.wire-nav__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wire-nav__badge {
|
|
padding: 0.05rem 0.4rem;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
|
color: var(--nav-accent);
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.wire-nav__arrow {
|
|
display: inline-block;
|
|
transition: transform 160ms ease;
|
|
}
|
|
|
|
.wire-nav__item:hover > .wire-nav__link > .wire-nav__arrow,
|
|
.wire-nav__item:focus-within > .wire-nav__link > .wire-nav__arrow {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.wire-nav__submenu {
|
|
position: absolute;
|
|
z-index: 30;
|
|
top: 100%;
|
|
left: 0;
|
|
min-width: 12rem;
|
|
margin: 0;
|
|
padding: 0.35rem;
|
|
border: 1px solid var(--wire-color-border);
|
|
border-radius: var(--wire-radius-md);
|
|
background: var(--wire-color-surface);
|
|
box-shadow: var(--wire-shadow-1);
|
|
list-style: none;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 160ms ease;
|
|
}
|
|
|
|
.wire-nav__item:hover > .wire-nav__submenu,
|
|
.wire-nav__item:focus-within > .wire-nav__submenu {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.wire-nav__submenu--level3 {
|
|
top: 0;
|
|
left: 100%;
|
|
}
|
|
|
|
.wire-nav__toggle {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.7rem;
|
|
border: 1px solid var(--wire-color-border);
|
|
border-radius: var(--wire-radius-sm);
|
|
background: var(--wire-color-surface);
|
|
color: var(--wire-color-text);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wire-nav__toggle-bar {
|
|
width: 1rem;
|
|
height: 2px;
|
|
background: currentColor;
|
|
box-shadow:
|
|
0 -5px 0 currentColor,
|
|
0 5px 0 currentColor;
|
|
}
|
|
|
|
/*
|
|
* On a phone the bar becomes a disclosure: submenus stop being floating
|
|
* overlays and stack inline, because a hover-opened overlay is
|
|
* unreachable on touch.
|
|
*/
|
|
@media (max-width: 767px) {
|
|
.wire-nav__toggle {
|
|
display: inline-flex;
|
|
}
|
|
|
|
.wire-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.wire-nav[data-expanded="false"] .wire-nav__list {
|
|
display: none;
|
|
}
|
|
|
|
.wire-nav__submenu,
|
|
.wire-nav__submenu--level3 {
|
|
position: static;
|
|
opacity: 1;
|
|
visibility: visible;
|
|
border: 0;
|
|
box-shadow: none;
|
|
padding-left: 1rem;
|
|
}
|
|
}
|
|
}
|
|
}
|