487 lines
20 KiB
Plaintext
487 lines
20 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"
|
|
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 {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="Sidebar"
|
|
class='wrn-sidebar {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-mobile='{mobile ? "true" : "false"}'
|
|
data-wrn-owner="Sidebar" data-wrn-part="root" data-wrn-component="Sidebar" data-wrn-axis-color='{color}' data-wrn-axis-size='{size}'>
|
|
<button
|
|
type="button"
|
|
class="wrn-sidebar__launcher"
|
|
aria-label='{mobileLabel}'
|
|
aria-expanded='{drawerOpen}'
|
|
@click='openDrawer(event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="launcher" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span class="wrn-sidebar__launcher-bar" aria-hidden="true" data-wrn-owner="Sidebar" data-wrn-part="launcherBar" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"></span>
|
|
<span class="wrn-sidebar__launcher-text" data-wrn-owner="Sidebar" data-wrn-part="launcherText" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{mobileLabel}</span>
|
|
</button>
|
|
|
|
<nav class="wrn-sidebar__rail" aria-label='{label}' data-wrn-owner="Sidebar" data-wrn-part="rail" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<ul class="wrn-sidebar__list" data-wrn-roving="vertical" data-wrn-owner="Sidebar" data-wrn-part="list" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each entryList() as entry}
|
|
<li class="wrn-sidebar__entry" data-group='{isGroup(entry)}' data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<p class="wrn-sidebar__heading" data-show="entry.heading" data-wrn-owner="Sidebar" data-wrn-part="heading" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.heading}</p>
|
|
|
|
<a
|
|
class='wrn-sidebar__link {isActive(entry) ? "wrn-sidebar__link--active" : ""}'
|
|
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"}'
|
|
tabindex='{entry.disabled ? "-1" : "0"}'
|
|
@click='choose(entry, 1, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span
|
|
class='wrn-sidebar__icon {entry.icon}'
|
|
data-show="entry.icon"
|
|
aria-hidden="true"
|
|
data-wrn-owner="Sidebar" data-wrn-part="icon" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"></span>
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.label}</span>
|
|
<span class="wrn-sidebar__badge" data-show="entry.badge" data-wrn-owner="Sidebar" data-wrn-part="badge" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.badge}</span>
|
|
</a>
|
|
|
|
<ul class="wrn-sidebar__sublist" data-show="childrenOf(entry).length > 0" data-wrn-owner="Sidebar" data-wrn-part="sublist" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each childrenOf(entry) as child}
|
|
<li class="wrn-sidebar__entry" data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<a
|
|
class='wrn-sidebar__link {isActive(child) ? "wrn-sidebar__link--active" : ""}'
|
|
href='{child.href || "#"}'
|
|
data-active='{isActive(child)}'
|
|
aria-current='{isActive(child) ? "page" : "false"}'
|
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
|
data-wrn-roving-item="true"
|
|
tabindex='{child.disabled ? "-1" : "0"}'
|
|
@click='choose(child, 2, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span
|
|
class='wrn-sidebar__icon {child.icon}'
|
|
data-show="child.icon"
|
|
aria-hidden="true"
|
|
data-wrn-owner="Sidebar" data-wrn-part="icon" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"></span>
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{child.label}</span>
|
|
<span class="wrn-sidebar__badge" data-show="child.badge" data-wrn-owner="Sidebar" data-wrn-part="badge" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{child.badge}</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wrn-sidebar__sublist wrn-sidebar__sublist--level3"
|
|
data-show="childrenOf(child).length > 0"
|
|
data-wrn-owner="Sidebar" data-wrn-part="sublist" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each childrenOf(child) as leaf}
|
|
<li class="wrn-sidebar__entry" data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<a
|
|
class='wrn-sidebar__link {isActive(leaf) ? "wrn-sidebar__link--active" : ""}'
|
|
href='{leaf.href || "#"}'
|
|
data-active='{isActive(leaf)}'
|
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
|
data-wrn-roving-item="true"
|
|
tabindex='{leaf.disabled ? "-1" : "0"}'
|
|
@click='choose(leaf, 3, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{leaf.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<slot />
|
|
</nav>
|
|
|
|
{#if mobile}
|
|
<Drawer
|
|
open={drawerOpen}
|
|
placement={drawerPlacement}
|
|
title={drawerTitle}
|
|
label={label}
|
|
@close="closeDrawer()"
|
|
>
|
|
<nav class="wrn-sidebar__drawer-nav" aria-label='{label}' data-wrn-owner="Sidebar" data-wrn-part="drawerNav" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<ul class="wrn-sidebar__list" data-wrn-roving="vertical" data-wrn-owner="Sidebar" data-wrn-part="list" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each entryList() as entry}
|
|
<li class="wrn-sidebar__entry" data-group='{isGroup(entry)}' data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<p class="wrn-sidebar__heading" data-show="entry.heading" data-wrn-owner="Sidebar" data-wrn-part="heading" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.heading}</p>
|
|
|
|
<a
|
|
class='wrn-sidebar__link {isActive(entry) ? "wrn-sidebar__link--active" : ""}'
|
|
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"}'
|
|
tabindex='{entry.disabled ? "-1" : "0"}'
|
|
@click='choose(entry, 1, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span
|
|
class='wrn-sidebar__icon {entry.icon}'
|
|
data-show="entry.icon"
|
|
aria-hidden="true"
|
|
data-wrn-owner="Sidebar" data-wrn-part="icon" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"></span>
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.label}</span>
|
|
<span class="wrn-sidebar__badge" data-show="entry.badge" data-wrn-owner="Sidebar" data-wrn-part="badge" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{entry.badge}</span>
|
|
</a>
|
|
|
|
<ul class="wrn-sidebar__sublist" data-show="childrenOf(entry).length > 0" data-wrn-owner="Sidebar" data-wrn-part="sublist" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each childrenOf(entry) as child}
|
|
<li class="wrn-sidebar__entry" data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<a
|
|
class='wrn-sidebar__link {isActive(child) ? "wrn-sidebar__link--active" : ""}'
|
|
href='{child.href || "#"}'
|
|
data-active='{isActive(child)}'
|
|
aria-current='{isActive(child) ? "page" : "false"}'
|
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
|
data-wrn-roving-item="true"
|
|
tabindex='{child.disabled ? "-1" : "0"}'
|
|
@click='choose(child, 2, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span
|
|
class='wrn-sidebar__icon {child.icon}'
|
|
data-show="child.icon"
|
|
aria-hidden="true"
|
|
data-wrn-owner="Sidebar" data-wrn-part="icon" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}"></span>
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{child.label}</span>
|
|
<span class="wrn-sidebar__badge" data-show="child.badge" data-wrn-owner="Sidebar" data-wrn-part="badge" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{child.badge}</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wrn-sidebar__sublist wrn-sidebar__sublist--level3"
|
|
data-show="childrenOf(child).length > 0"
|
|
data-wrn-owner="Sidebar" data-wrn-part="sublist" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
{#each childrenOf(child) as leaf}
|
|
<li class="wrn-sidebar__entry" data-wrn-owner="Sidebar" data-wrn-part="entry" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<a
|
|
class='wrn-sidebar__link {isActive(leaf) ? "wrn-sidebar__link--active" : ""}'
|
|
href='{leaf.href || "#"}'
|
|
data-active='{isActive(leaf)}'
|
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
|
data-wrn-roving-item="true"
|
|
tabindex='{leaf.disabled ? "-1" : "0"}'
|
|
@click='choose(leaf, 3, event)'
|
|
data-wrn-owner="Sidebar" data-wrn-part="link" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">
|
|
<span class="wrn-sidebar__label" data-wrn-owner="Sidebar" data-wrn-part="label" data-wrn-axis-color="{color}" data-wrn-axis-size="{size}">{leaf.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
</nav>
|
|
<slot name="drawer"></slot>
|
|
</Drawer>
|
|
{/if}
|
|
</div>
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|