DirectoryNav
A long list of names grouped under sticky letter headers, with a row of letters at the top that jumps to each group.
Demo
Scroll the list: each letter's header stays at the top while its names pass; the row of letters jumps.
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/directory_nav.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/directory_nav.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/directory_nav.qwe
let directory_nav_demo_people = [ { label: "Grace Hopper", href: "#grace", meta: "Navy" }, { label: "Ada Lovelace", href: "#directory_nav", meta: "Engineering" }, { label: "Alan Turing", href: "#alan", meta: "Research" }, { label: "Barbara Liskov", href: "#barbara", meta: "Engineering" }, { label: "Dennis Ritchie", href: "#dennis" }, { label: "Donald Knuth", href: "#donald", meta: "Research" }, { label: "Edsger Dijkstra", href: "#edsger" }, { label: "Frances Allen", href: "#frances", meta: "Compilers" }, { label: "Guido van Rossum", href: "#guido" }, { label: "Hedy Lamarr", href: "#hedy", meta: "Radio" }, { label: "John McCarthy", href: "#john" }, { label: "Ken Thompson", href: "#ken" }, { label: "Linus Torvalds", href: "#linus", meta: "Kernel" }, { label: "Margaret Hamilton", href: "#margaret", meta: "Apollo" }, { label: "Niklaus Wirth", href: "#niklaus" }, { label: "Radia Perlman", href: "#radia", meta: "Networks" }, { label: "Tim Berners-Lee", href: "#tim", meta: "Web" }, { label: "Yukihiro Matsumoto", href: "#matz" }, { label: "3Blue1Brown", href: "#3b1b", meta: "Videos" }]view DirectoryNavDemo() = <div.demo-stack> <div style="max-width: 420px"> <DirectoryNav id="directory-nav-demo" items={directory_nav_demo_people} current="#directory_nav" height={360}/> </div> <p.demo-note>Scroll the list: each letter's header stays at the top while its names pass; the row of letters jumps.</p> </div>Source
ui/directory_nav.qwe
// DirectoryNav: a long list of names, grouped under the letters they start with, as// on a phone's contacts: each letter's header stays at the top while its names scroll// past, and a row of letters at the top jumps to each group. Letters with no names// are shown but cannot be followed.//// <DirectoryNav id="people" items={[// { label: "Ada Lovelace", href: "/people/ada", meta: "Engineering" },// { label: "Alan Turing", href: "/people/alan" },// { label: "Grace Hopper", href: "/people/grace", meta: "Navy" }// ]}/>//// items: { label, href, meta? }; meta is a quiet short text at the right (a team, a// place). They are sorted by label (sorted={false} keeps their order) and grouped by// the first letter; names starting with a digit or a symbol go under "#", last.// id (required): the prefix of the letter headers' ids ("people-A"), which the row// at the top links to. height: a height in pixels past which the list scrolls inside// itself, with the letter row and the headers sticking as it does (null: the page// scrolls, and they stick at var(--ui-dirnav-top, 0)). label: what screen readers// call it ("Directory"). current: the href of the item marked aria-current="page".import ui/theme.qwe use ui_clslet directory_nav_css = css`.ui-dirnav { position: relative; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); background: var(--ui-bg); }.ui-dirnav-boxed { overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }.ui-dirnav-bar { position: sticky; top: var(--ui-dirnav-top, 0); z-index: 3; display: flex; flex-wrap: wrap; gap: 1px; margin: 0; padding: 6px 8px; list-style: none; background: var(--ui-bg); border-bottom: 1px solid var(--ui-border);}.ui-dirnav-boxed .ui-dirnav-bar { top: 0; }.ui-dirnav-letter { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; padding: 0 3px; border-radius: var(--ui-radius-sm); color: var(--ui-accent); font-size: 12px; font-weight: 600; text-decoration: none;}a.ui-dirnav-letter:hover { background: var(--ui-accent-soft); }.ui-dirnav-letter:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-dirnav-off { color: var(--ui-strong); }.ui-dirnav-group { margin: 0; }.ui-dirnav-head { position: sticky; top: calc(var(--ui-dirnav-top, 0px) + 37px); z-index: 2; margin: 0; padding: 4px 14px; background: var(--ui-subtle); border-top: 1px solid var(--ui-border); border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px; font-weight: 700; letter-spacing: .06em;}.ui-dirnav-boxed .ui-dirnav-head { top: 37px; }.ui-dirnav ul { margin: 0; padding: 0; list-style: none; }.ui-dirnav-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 14px; color: var(--ui-fg); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 55%, transparent);}.ui-dirnav-item:hover { background: var(--ui-subtle); }.ui-dirnav-item:focus-visible { outline: none; box-shadow: inset var(--ui-focus); }.ui-dirnav-item[aria-current="page"] { color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-dirnav-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-dirnav-meta { flex: none; color: var(--ui-muted); font-size: 13px; }`let directory_nav_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"// The letter a name is filed under: its first letter, in capitals, or "#".fn directory_nav_letter(label: str) -> str { let c = upper(slice(trim(label), 0, 1)) contains(directory_nav_letters, c) && c != "" ? c : "#"}view DirectoryNav(id: str, items: list = [], current: str? = null, sorted: bool = true, height: int? = null, label: str = "Directory", class: str? = null) { style global directory_nav_css let all = sorted ? items | sort_by((it) => lower(it.label)) : items let groups = group_by(all, (it) => directory_nav_letter(it.label)) let letters = (for i in range(0, 26) => slice(directory_nav_letters, i, i + 1)) + ["#"] <nav class={ui_cls(["ui-dirnav", height != null && "ui-dirnav-boxed", class])} id={id} aria-label={label} style={height != null ? `max-height: ${height}px` : null}> <ul.ui-dirnav-bar aria-label="Jump to a letter"> {for l in letters => <li>{has(groups, l) ? <a.ui-dirnav-letter href={`#${id}-${l == "#" ? "other" : l}`} aria-label={l == "#" ? "Other" : null}>{l}</a> : <span.ui-dirnav-letter.ui-dirnav-off aria-disabled="true" aria-label={l == "#" ? "Other, nothing" : `${l}, nothing`}>{l}</span>} </li>} </ul> {for l in letters | filter((l) => has(groups, l)) => <section.ui-dirnav-group aria-labelledby={`${id}-${l == "#" ? "other" : l}`}> <h3.ui-dirnav-head id={`${id}-${l == "#" ? "other" : l}`}>{l}</h3> <ul> {for it in groups[l] => <li> <a.ui-dirnav-item href={it.href} aria-current={current != null && it.href == current ? "page" : null}> <span.ui-dirnav-name>{it.label}</span> {get(it, "meta") != null && <span.ui-dirnav-meta>{it.meta}</span>} </a> </li>} </ul> </section>} </nav>}Related
AlphabetIndexA to Z links into a list sorted by name; letters with nothing under them stay in place, disabled.4 views · 6 downloadsLinkListA plain list of links, each with a line saying where it leads, an icon and a short note at the right.5 views · 6 downloadsMenuA vertical list of links and actions in titled sections, with icons, shortcuts, counts, disabled items and the active one marked.6 views · 6 downloadsSitemapListA site's pages as a nested list of links, as deep as the site goes, flowing into columns on wide screens.4 views · 5 downloadsAccordionSections that open under their titles, on the browser's own <details>: exclusive with a group.5 views · 7 downloadsBreadcrumbsWhere this page sits: a trail of links up to the home page, the last one marked as this page.5 views · 6 downloads
Thread
Thread 0 posts
On DirectoryNav: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.