Navigation

DirectoryNav

A long list of names grouped under sticky letter headers, with a row of letters at the top that jumps to each group.

#navigation#list#link#user#no-js

Demo

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.qwe
main.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>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

On DirectoryNav: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.