// Sidebar -- a vertical navigation rail that becomes a drawer on small screens. // // // // 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" mobile: boolean = true drawerPlacement: string = "left" 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, sourceEvent) { if (entry.disabled) { sourceEvent.preventDefault() return } output.select({ item: entry, value: entry.value || "", level: level }) if (drawerOpen) { drawerOpen = false output.close({ source: "select" }) output.toggle({ open: false }) } } 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 }) } client function revealActiveItem() { document.querySelector(".wrn-sidebar__rail .wrn-sidebar__link--active") && document.querySelector(".wrn-sidebar__rail .wrn-sidebar__link--active").scrollIntoView({ block: "nearest", inline: "nearest", behavior: "auto" }) } } lifecycle { mount { revealActiveItem() } } view {
{#if mobile} {/if}
} style { .wrn-sidebar { --sidebar-accent: var(--wrn-color-primary); max-width: 100%; } .wrn-sidebar[data-color="secondary"] { --sidebar-accent: var(--wrn-color-secondary); } .wrn-sidebar[data-color="success"] { --sidebar-accent: var(--wrn-color-success); } .wrn-sidebar[data-color="danger"] { --sidebar-accent: var(--wrn-color-danger); } .wrn-sidebar[data-color="info"] { --sidebar-accent: var(--wrn-color-info); } .wrn-sidebar[data-size="sm"] { font-size: 0.82rem; } .wrn-sidebar[data-size="lg"] { font-size: 1rem; } .wrn-sidebar__rail { width: 100%; padding: 0.5rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface-soft, var(--wrn-color-surface)); } .wrn-sidebar__list, .wrn-sidebar__sublist { display: grid; gap: 0.125rem; margin: 0; padding: 0; list-style: none; } .wrn-sidebar__sublist { margin-inline-start: 1rem; margin-block: 0.125rem; padding-inline-start: 0.5rem; border-inline-start: 1px solid var(--wrn-color-border); } .wrn-sidebar__entry[data-group="true"] > .wrn-sidebar__sublist { margin-inline-start: 0; padding-inline-start: 0; border-inline-start: 0; } .wrn-sidebar__icon { width: 1.05rem; height: 1.05rem; flex-shrink: 0; color: var(--wrn-color-text-subtle, var(--wrn-color-text-muted)); } .wrn-sidebar__entry[data-group="true"] + .wrn-sidebar__entry { margin-top: 0.35rem; } .wrn-sidebar__heading { margin: 0.875rem 0.625rem 0.375rem; color: var(--wrn-color-text-subtle, var(--wrn-color-text-muted)); font-family: var(--wrn-font-mono, monospace); font-size: 0.65rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; } .wrn-sidebar__link { position: relative; display: flex; align-items: center; gap: 0.5rem; min-height: 2rem; padding: 0.375rem 0.625rem; border-radius: calc(var(--wrn-radius-sm) - 1px); color: var(--wrn-color-text-muted); font-size: 0.8125rem; font-weight: 550; text-decoration: none; transition: background-color 120ms ease, color 120ms ease; } .wrn-sidebar__link:hover { background: var(--wrn-color-surface-muted, var(--wrn-color-surface-soft)); color: var(--wrn-color-text); } .wrn-sidebar__link:focus-visible { outline: 2px solid var(--sidebar-accent); outline-offset: 2px; } .wrn-sidebar__link[data-active="true"] { background: var(--wrn-color-surface-raised, var(--wrn-color-surface)); color: var(--wrn-color-text); font-weight: 600; box-shadow: 0 0 0 1px var(--wrn-color-border), var(--wrn-shadow-1); } .wrn-sidebar__link[aria-disabled="true"] { opacity: 0.5; pointer-events: none; } .wrn-sidebar__label { min-width: 0; } .wrn-sidebar__badge { margin-inline-start: auto; min-width: 1.25rem; padding: 0.0625rem 0.4rem; border-radius: 999px; background: var(--wrn-color-surface-muted, var(--wrn-color-surface-2)); color: var(--wrn-color-text-muted); font-size: 0.68rem; font-weight: 650; font-variant-numeric: tabular-nums; text-align: center; } .wrn-sidebar__launcher { display: none; align-items: center; gap: 0.5rem; min-height: 2.25rem; padding: 0 0.75rem; border: 1px solid var(--wrn-color-border); border-bottom-color: var(--wrn-color-border-strong, var(--wrn-color-border)); border-radius: var(--wrn-radius-sm); background: linear-gradient(180deg, var(--wrn-color-surface), var(--wrn-color-surface-soft, var(--wrn-color-surface))); box-shadow: var(--wrn-shadow-1); color: var(--wrn-color-text); font: inherit; font-size: 0.8125rem; font-weight: 600; cursor: pointer; } .wrn-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) { .wrn-sidebar[data-mobile="true"] > .wrn-sidebar__launcher { display: inline-flex; } .wrn-sidebar[data-mobile="true"] > .wrn-sidebar__rail { display: none; } } .wrn-sidebar__link[data-active="true"]::before { position: absolute; inset-block: 0.4rem; inset-inline-start: -0.5rem; width: 3px; border-radius: 0 3px 3px 0; background: var(--sidebar-accent); content: ""; } .wrn-sidebar__link[data-active="true"] .wrn-sidebar__icon { color: var(--sidebar-accent); } .wrn-sidebar__link[data-active="true"] .wrn-sidebar__badge { background: color-mix(in srgb, var(--sidebar-accent) 14%, var(--wrn-color-surface)); color: var(--wrn-color-primary-text, var(--sidebar-accent)); } } }