CollapsibleNav
Side navigation in groups that fold under their titles, the browser remembering which are open.
Demo
Fold a group: the browser remembers it (reload the page to see).
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/collapsible_nav.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/collapsible_nav.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/collapsible_nav.qwe
let collapsible_nav_demo_groups = [ { label: "Overview", href: "#overview" }, { title: "Getting started", items: [ { label: "Install", href: "#install" }, { label: "Your first program", href: "#collapsible_nav" }, { label: "Editors", href: "#editors" } ] }, { title: "Language", items: [ { label: "Values", href: "#values" }, { label: "Functions", href: "#functions" }, { label: "Views", href: "#views" }, { label: "Components", href: "#components", badge: 4 } ] }, { title: "Deploying", open: false, items: [ { label: "One server", href: "#server" }, { label: "Docker", href: "#docker" } ] }]component CollapsibleNavDemoLive() { state last = null <div.demo-row style="align-items: flex-start; gap: 24px"> <div style="width: 240px"> <CollapsibleNav id="collapsible-nav-demo" groups={collapsible_nav_demo_groups} current="#collapsible_nav" on_toggle={(t, o) => { last = `${t} ${o ? "opened" : "closed"}` }}/> </div> <p.demo-note style="flex: 1 1 200px">{last ?? "Fold a group: the browser remembers it (reload the page to see)."}</p> </div>}view CollapsibleNavDemo() = <div.demo-stack> <CollapsibleNavDemoLive/> </div>Source
ui/collapsible_nav.qwe
// CollapsibleNav: navigation down the side of docs or an app, its links in groups// that fold and unfold under their titles (the browser's own <details>), and which// groups are open is remembered in the browser (localStorage), so a page later shows// them as they were left. The group holding the current page is always open, and// the page is marked.//// <CollapsibleNav id="docs" current={request.path} groups={[// { title: "Getting started", items: [{ label: "Install", href: "/docs/install" }, { label: "Hello", href: "/docs/hello" }] },// { title: "Guides", open: false, items: [{ label: "Forms", href: "/docs/forms" }] }// ]}/>//// groups: { title, items: [{ label, href, badge? }], open? } (open: whether it starts// open the first time, true by default); and { label, href } alone for a link outside// any group. current: the href of the page showing (aria-current="page").// id (required): names the navigation, and what the browser remembers it under.// remember: keep the open groups in localStorage (true). label: what screen readers// call it ("Navigation"). on_toggle(title, open): inside one of your components,// called when a group folds or unfolds.import ui/theme.qwe use ui_clslet collapsible_nav_css = css`.ui-colnav { display: grid; gap: 4px; min-width: 0; padding: 4px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-colnav ul { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }.ui-colnav-group { margin: 0; }.ui-colnav-group + .ui-colnav-group { margin-top: 4px; }.ui-colnav-title { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: var(--ui-radius-sm); color: var(--ui-fg); font-weight: 600; cursor: pointer; list-style: none; user-select: none;}.ui-colnav-title::-webkit-details-marker { display: none; }.ui-colnav-title:hover { background: var(--ui-subtle); }.ui-colnav-title:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-colnav-chev { flex: none; width: 14px; height: 14px; color: var(--ui-muted); transition: transform .15s; }.ui-colnav-group[open] > .ui-colnav-title .ui-colnav-chev { transform: rotate(90deg); }.ui-colnav-title span { flex: 1; }.ui-colnav-group > ul { margin: 2px 0 6px 16px; padding-left: 8px; border-left: 1px solid var(--ui-border); }.ui-colnav-link { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-decoration: none; transition: color .12s, background .12s;}.ui-colnav-link:hover { color: var(--ui-fg); background: var(--ui-subtle); }.ui-colnav-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-colnav-link[aria-current="page"] { color: var(--ui-accent); background: var(--ui-accent-soft); font-weight: 500; }.ui-colnav-link span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-colnav-badge { min-width: 20px; padding: 1px 6px; border-radius: 999px; background: var(--ui-subtle); border: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px; font-weight: 600; text-align: center;}.ui-colnav-top > .ui-colnav-link { font-weight: 600; color: var(--ui-fg); }@media (prefers-reduced-motion: reduce) { .ui-colnav-chev, .ui-colnav-link { transition: none; } }`fn collapsible_nav_holds(g, current: str?) -> bool = current != null && any(get(g, "items") ?? [], (it) => it.href == current)component CollapsibleNav(id: str, groups: list = [], current: str? = null, remember: bool = true, label: str = "Navigation", on_toggle = null, class: str? = null) { style global collapsible_nav_css // The titles of the groups the browser remembers as open or closed: { title: bool }. state saved = {} let is_open = (g) => collapsible_nav_holds(g, current) || (has(saved, g.title) ? saved[g.title] : get(g, "open") != false) <nav class={ui_cls(["ui-colnav", class])} id={id} aria-label={label} @mount={(el) => { if remember { let text = js("localStorage.getItem", `ui-colnav-${id}`) if text != null { saved = json.parse(text) ?? {} } } }}> {for g in groups => get(g, "items") == null ? <li.ui-colnav-top key={g.label} style="list-style: none"><a.ui-colnav-link href={g.href} aria-current={current != null && g.href == current ? "page" : null}><span>{g.label}</span></a></li> : <details.ui-colnav-group key={g.title} open={is_open(g)} @toggle={(e) => { let now = e.target.open if has(saved, g.title) ? saved[g.title] != now : now != is_open(g) { saved = set(saved, g.title, now) if remember { js("localStorage.setItem", `ui-colnav-${id}`, json.stringify(saved)) } on_toggle && on_toggle(g.title, now) } }}> <summary.ui-colnav-title> <svg.ui-colnav-chev viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg> <span>{g.title}</span> </summary> <ul> {for it in g.items => <li><a.ui-colnav-link href={it.href} aria-current={current != null && it.href == current ? "page" : null}> <span>{it.label}</span> {get(it, "badge") != null && <span.ui-colnav-badge>{it.badge}</span>} </a></li>} </ul> </details>} </nav>}Related
MegaMenuA top bar whose items open wide panels of grouped links under the whole bar, on hover and from the keyboard, with no script.5 views · 8 downloadsMenuA vertical list of links and actions in titled sections, with icons, shortcuts, counts, disabled items and the active one marked.6 views · 7 downloadsPagerPrevious and next links with their titles, for the pages of an article, a book or docs.6 views · 6 downloadsAlphabetIndexA to Z links into a list sorted by name; letters with nothing under them stay in place, disabled.4 views · 6 downloadsBreadcrumbsWhere this page sits: a trail of links up to the home page, the last one marked as this page.5 views · 6 downloadsDirectoryNavA long list of names grouped under sticky letter headers, with a row of letters at the top that jumps to each group.5 views · 5 downloads
Thread
Thread 0 posts
On CollapsibleNav: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.