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>
336 lines
9.5 KiB
Plaintext
336 lines
9.5 KiB
Plaintext
// Sidebar -- a vertical navigation rail that becomes a drawer on small screens.
|
|
//
|
|
// <Sidebar items='[{"label":"Dashboard","href":"/","value":"dash"}]' active="dash" />
|
|
//
|
|
// An entry is one of three shapes:
|
|
// { label, href, value } a single link
|
|
// { heading, items: [...] } a labelled group
|
|
// { label, items: [...] } a collapsible branch, nested to 3 levels
|
|
//
|
|
// The off-canvas presentation is Drawer rather than a second implementation,
|
|
// so the focus trap and the body scroll lock come from one place.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
import Drawer from "./Drawer.wrn"
|
|
|
|
component Sidebar {
|
|
outputs {
|
|
toggle(payload: { open: boolean })
|
|
open(payload: { sourceEvent: Event })
|
|
close(payload: { source: string })
|
|
select(payload: { item: object; value: string; level: number })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
label: string = "Sidebar"
|
|
items: unknown[] = []
|
|
active: string = ""
|
|
mobileLabel: string = "Open navigation"
|
|
drawerTitle: string = "Navigation"
|
|
class: string = ""
|
|
}
|
|
|
|
state drawerOpen = false
|
|
|
|
functions {
|
|
shared function entryList() {
|
|
return Array.isArray(items) ? items : []
|
|
}
|
|
|
|
// Accepts children as well as items: children is what shipped in 0.8.5
|
|
// and existing callers should keep working.
|
|
shared function childrenOf(entry) {
|
|
if (!entry) {
|
|
return []
|
|
}
|
|
if (Array.isArray(entry.items)) {
|
|
return entry.items
|
|
}
|
|
return Array.isArray(entry.children) ? entry.children : []
|
|
}
|
|
|
|
shared function isGroup(entry) {
|
|
return Boolean(entry.heading)
|
|
}
|
|
|
|
shared function isActive(entry) {
|
|
return Boolean(entry.value) && entry.value === active
|
|
}
|
|
|
|
client function choose(entry, level) {
|
|
if (entry.disabled) {
|
|
return
|
|
}
|
|
output.select({ item: entry, value: entry.value || "", level: level })
|
|
if (drawerOpen) {
|
|
drawerOpen = false
|
|
output.close({ source: "select" })
|
|
}
|
|
}
|
|
|
|
client function openDrawer(sourceEvent) {
|
|
drawerOpen = true
|
|
output.open({ sourceEvent: sourceEvent })
|
|
output.toggle({ open: true })
|
|
}
|
|
|
|
client function closeDrawer() {
|
|
drawerOpen = false
|
|
output.close({ source: "drawer" })
|
|
output.toggle({ open: false })
|
|
}
|
|
}
|
|
|
|
view {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="Sidebar"
|
|
class='wire-sidebar {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wire-sidebar__launcher"
|
|
aria-label='{mobileLabel}'
|
|
aria-expanded='{drawerOpen}'
|
|
@click='openDrawer(event)'
|
|
>
|
|
<span class="wire-sidebar__launcher-bar" aria-hidden="true"></span>
|
|
<span class="wire-sidebar__launcher-text">{mobileLabel}</span>
|
|
</button>
|
|
|
|
<nav class="wire-sidebar__rail" aria-label='{label}'>
|
|
<ul class="wire-sidebar__list" data-wrn-roving="vertical">
|
|
{#each entryList() as entry}
|
|
<li class="wire-sidebar__entry" data-group='{isGroup(entry)}'>
|
|
<p class="wire-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
|
|
|
<a
|
|
class="wire-sidebar__link"
|
|
data-show="!entry.heading"
|
|
href='{entry.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
data-active='{isActive(entry)}'
|
|
aria-current='{isActive(entry) ? "page" : "false"}'
|
|
aria-disabled='{entry.disabled ? "true" : "false"}'
|
|
@click='choose(entry, 1)'
|
|
>
|
|
<span
|
|
class='wire-sidebar__icon {entry.icon}'
|
|
data-show="entry.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-sidebar__label">{entry.label}</span>
|
|
<span class="wire-sidebar__badge" data-show="entry.badge">{entry.badge}</span>
|
|
</a>
|
|
|
|
<ul class="wire-sidebar__sublist" data-show="childrenOf(entry).length > 0">
|
|
{#each childrenOf(entry) as child}
|
|
<li class="wire-sidebar__entry">
|
|
<a
|
|
class="wire-sidebar__link"
|
|
href='{child.href || "#"}'
|
|
data-active='{isActive(child)}'
|
|
aria-current='{isActive(child) ? "page" : "false"}'
|
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
|
@click='choose(child, 2)'
|
|
>
|
|
<span
|
|
class='wire-sidebar__icon {child.icon}'
|
|
data-show="child.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-sidebar__label">{child.label}</span>
|
|
<span class="wire-sidebar__badge" data-show="child.badge">{child.badge}</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wire-sidebar__sublist wire-sidebar__sublist--level3"
|
|
data-show="childrenOf(child).length > 0"
|
|
>
|
|
{#each childrenOf(child) as leaf}
|
|
<li class="wire-sidebar__entry">
|
|
<a
|
|
class="wire-sidebar__link"
|
|
href='{leaf.href || "#"}'
|
|
data-active='{isActive(leaf)}'
|
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
|
@click='choose(leaf, 3)'
|
|
>
|
|
<span class="wire-sidebar__label">{leaf.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<slot />
|
|
</nav>
|
|
|
|
<Drawer
|
|
open={drawerOpen}
|
|
placement="left"
|
|
title={drawerTitle}
|
|
label={label}
|
|
@close="closeDrawer()"
|
|
>
|
|
<slot name="drawer"></slot>
|
|
</Drawer>
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wire-sidebar {
|
|
--sidebar-accent: var(--wire-color-primary);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-sidebar[data-color="secondary"] {
|
|
--sidebar-accent: var(--wire-color-secondary);
|
|
}
|
|
|
|
.wire-sidebar[data-color="success"] {
|
|
--sidebar-accent: var(--wire-color-success);
|
|
}
|
|
|
|
.wire-sidebar[data-color="danger"] {
|
|
--sidebar-accent: var(--wire-color-danger);
|
|
}
|
|
|
|
.wire-sidebar[data-color="info"] {
|
|
--sidebar-accent: var(--wire-color-info);
|
|
}
|
|
|
|
.wire-sidebar[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wire-sidebar[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wire-sidebar__rail {
|
|
width: 100%;
|
|
}
|
|
|
|
.wire-sidebar__list,
|
|
.wire-sidebar__sublist {
|
|
display: grid;
|
|
gap: 0.1rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wire-sidebar__sublist {
|
|
margin-left: 0.85rem;
|
|
padding-left: 0.5rem;
|
|
border-left: 1px solid var(--wire-color-border);
|
|
}
|
|
|
|
.wire-sidebar__entry[data-group="true"] + .wire-sidebar__entry {
|
|
margin-top: 0.35rem;
|
|
}
|
|
|
|
.wire-sidebar__heading {
|
|
margin: 0.9rem 0 0.35rem;
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.wire-sidebar__link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.6rem;
|
|
border-radius: var(--wire-radius-sm);
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wire-sidebar__link:hover {
|
|
background: var(--wire-color-surface-soft);
|
|
color: var(--wire-color-text);
|
|
}
|
|
|
|
.wire-sidebar__link:focus-visible {
|
|
outline: 2px solid var(--sidebar-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-sidebar__link[data-active="true"] {
|
|
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
|
|
color: var(--sidebar-accent);
|
|
}
|
|
|
|
.wire-sidebar__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wire-sidebar__label {
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-sidebar__badge {
|
|
margin-left: auto;
|
|
padding: 0.05rem 0.4rem;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
|
|
color: var(--sidebar-accent);
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.wire-sidebar__launcher {
|
|
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-sidebar__launcher-bar {
|
|
width: 1rem;
|
|
height: 2px;
|
|
background: currentColor;
|
|
box-shadow:
|
|
0 -5px 0 currentColor,
|
|
0 5px 0 currentColor;
|
|
}
|
|
|
|
/*
|
|
* Below the tablet breakpoint the rail is replaced by the drawer launcher.
|
|
* A permanent rail eats most of a phone screen, and Drawer already handles
|
|
* the focus trap and the scroll lock.
|
|
*/
|
|
@media (max-width: 767px) {
|
|
.wire-sidebar__launcher {
|
|
display: inline-flex;
|
|
}
|
|
|
|
.wire-sidebar__rail {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
}
|