// 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" 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 {
} 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%; } .wrn-sidebar__list, .wrn-sidebar__sublist { display: grid; gap: 0.1rem; margin: 0; padding: 0; list-style: none; } .wrn-sidebar__sublist { margin-left: 0.85rem; padding-left: 0.5rem; border-left: 1px solid var(--wrn-color-border); } .wrn-sidebar__entry[data-group="true"] + .wrn-sidebar__entry { margin-top: 0.35rem; } .wrn-sidebar__heading { margin: 0.9rem 0 0.35rem; color: var(--wrn-color-text-muted); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } .wrn-sidebar__link { display: flex; align-items: center; gap: 0.5rem; padding: 0.45rem 0.6rem; border-radius: var(--wrn-radius-sm); color: var(--wrn-color-text-muted); font-size: 0.88rem; font-weight: 600; text-decoration: none; } .wrn-sidebar__link:hover { background: 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: color-mix(in srgb, var(--sidebar-accent) 16%, transparent); color: var(--sidebar-accent); } .wrn-sidebar__link[aria-disabled="true"] { opacity: 0.5; pointer-events: none; } .wrn-sidebar__label { min-width: 0; } .wrn-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; } .wrn-sidebar__launcher { display: none; align-items: center; gap: 0.5rem; padding: 0.45rem 0.7rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface); color: var(--wrn-color-text); font: inherit; font-size: 0.9rem; 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__launcher { display: inline-flex; } .wrn-sidebar__rail { display: none; } } } }