// Nav -- a navigation link list, flat or with submenus.
//
//
//
// 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 {
}
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;
}
}
}
}