SeriesNav
Where a post stands in a series: Part 2 of 5, a bar of the parts, the list of them all with this one marked, and the parts before and after.
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/series_nav.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/series_nav.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/series_nav.qwe
let series_nav_demo_parts = [ { title: "Routes and pages", href: "#series_nav-1" }, { title: "A database of posts", href: "#series_nav-2" }, { title: "Comments that resume in the browser", href: "#series_nav-3" }, { title: "Feeds, sitemaps and search", href: "#series_nav-4" }, { title: "Deploying it on a small server", soon: "Nov 12" },]view SeriesNavDemo() = <div.demo-stack> <SeriesNav id="series-nav-demo" title="Building a blog in Quillo" href="#series_nav" current={2} parts={series_nav_demo_parts}/> <SeriesNav id="series-nav-demo-end" title="Building a blog in Quillo" current={4} position="bottom" open={true} parts={series_nav_demo_parts}/> </div>Source
ui/series_nav.qwe
// SeriesNav: where a post stands in a series: "Part 2 of 5" with the series' name, a// bar of the parts with the ones before this marked read, the whole list of parts in a// disclosure (this one marked aria-current="page", those not out yet said so), and links// to the part before and the part after. It is a <nav>; the list opens without JavaScript// (a <details>).//// <SeriesNav title="Building a blog in Quillo" href="/series/blog" current={2} parts={[// { title: "Routes and pages", href: "/blog/routes" },// { title: "A database of posts", href: "/blog/db" },// { title: "Comments", href: "/blog/comments" },// { title: "Feeds and search", soon: "Nov 12" },// ]}/>//// title: the series' name, a link to href when given. parts: in order, maps of { title,// href?, soon? }: a part without href is not out yet, and says soon ("Coming Nov 12") or// "Coming soon". current: the number of this post's part, from 1 (0 when the post is// the series' introduction: no part is marked and Next goes to part 1). open: the list// shown to start with (false). position: "top" (default: compact, for over the post)// or "bottom" (the previous and next parts large, for after it). label: what screen// readers call it ("Series"). id: the <nav>'s id.import ui/theme.qwe use ui_clslet series_nav_css = css`.ui-bseries { display: grid; gap: 12px; margin: 0; padding: 16px 18px; border: 1px solid var(--ui-border); border-left: 4px solid var(--ui-accent); border-radius: var(--ui-radius); background: var(--ui-surface); font: 15px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-bseries-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }.ui-bseries-count { color: var(--ui-accent); font-size: 13px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; }.ui-bseries-name { font-weight: 650; }.ui-bseries-name a { color: inherit; text-decoration: none; border-radius: 3px; }.ui-bseries-name a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-bseries-name a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bseries-bar { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; }.ui-bseries-bar li { flex: 1; height: 5px; border-radius: 3px; background: var(--ui-border); }.ui-bseries-bar .ui-bseries-done { background: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)); }.ui-bseries-bar .ui-bseries-here { background: var(--ui-accent); }.ui-bseries-list summary { display: inline-flex; align-items: center; gap: 6px; padding: 2px 0; color: var(--ui-muted); font-size: 14px; font-weight: 500; cursor: pointer; list-style: none; border-radius: 3px; }.ui-bseries-list summary::-webkit-details-marker { display: none; }.ui-bseries-list summary::before { content: ""; width: 7px; height: 7px; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(-45deg); transition: transform .15s; }.ui-bseries-list[open] summary::before { transform: rotate(45deg); }.ui-bseries-list summary:hover { color: var(--ui-accent); }.ui-bseries-list summary:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bseries-list ol { display: grid; gap: 2px; margin: 8px 0 0; padding: 0; list-style: none; counter-reset: ui-bseries; }.ui-bseries-list li { counter-increment: ui-bseries; display: flex; align-items: baseline; gap: 10px; padding: 5px 8px; border-radius: var(--ui-radius-sm); }.ui-bseries-list li::before { content: counter(ui-bseries); flex: none; display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 650; }.ui-bseries-list li.ui-bseries-done::before { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-bseries-list li.ui-bseries-here { background: var(--ui-accent-soft); }.ui-bseries-list li.ui-bseries-here::before { background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-bseries-list li a { color: var(--ui-fg); text-decoration: none; border-radius: 3px; }.ui-bseries-list li a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-bseries-list li a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bseries-list li [aria-current] { font-weight: 650; }.ui-bseries-soon { color: var(--ui-muted); }.ui-bseries-step.ui-bseries-soon { border-style: dashed; }.ui-bseries-list .ui-bseries-soon small { margin-left: 6px; padding: 1px 7px; border-radius: 999px; background: var(--ui-subtle); font-size: 12px; white-space: nowrap; }.ui-bseries-steps { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }.ui-bseries-step { display: grid; gap: 1px; min-width: 0; padding: 8px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-fg); text-decoration: none; font-size: 14px; }.ui-bseries-step small { color: var(--ui-muted); font-size: 12px; }.ui-bseries-step span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }.ui-bseries-next { grid-column: 2; text-align: end; }a.ui-bseries-step:hover { border-color: var(--ui-accent); }a.ui-bseries-step:hover span { color: var(--ui-accent); }a.ui-bseries-step:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bseries-bottom .ui-bseries-step { padding: 12px 16px; }.ui-bseries-bottom .ui-bseries-step span { white-space: normal; font-size: 16px; }@media (max-width: 480px) { .ui-bseries-steps { grid-template-columns: 1fr; } .ui-bseries-next { grid-column: 1; } }@media (prefers-reduced-motion: reduce) { .ui-bseries-list summary::before { transition: none; } }`view SeriesNav(title: str, parts: list, current: int = 1, href: str? = null, open: bool = false, position: str = "top", label: str = "Series", id: str? = null, class: str? = null) { style global series_nav_css let n = len(parts) let at = current < 0 ? 0 : (current > n ? n : current) let prev = at > 1 ? parts[at - 2] : null let next = at < n ? parts[at] : null let soon = (p) => p.soon != null ? `Coming ${p.soon}` : "Coming soon" <nav id={id} class={ui_cls(["ui-bseries", `ui-bseries-${position}`, class])} aria-label={`${label}: ${title}`}> <div.ui-bseries-head> <span.ui-bseries-count>{at > 0 ? `Part ${at} of ${n}` : `A series in ${n} parts`}</span> <span.ui-bseries-name>{href != null ? <a href={href}>{title}</a> : title}</span> </div> <ol.ui-bseries-bar aria-hidden="true"> {for i in range(n) => <li class={i + 1 < at ? "ui-bseries-done" : (i + 1 == at ? "ui-bseries-here" : null)}></li>} </ol> <details.ui-bseries-list open={open}> <summary>All {n} parts</summary> <ol> {for i in range(n) => { let p = parts[i] let here = i + 1 == at <li class={i + 1 < at ? "ui-bseries-done" : (here ? "ui-bseries-here" : null)}> {p.href == null ? <span.ui-bseries-soon>{p.title}<small>{soon(p)}</small></span> : (here ? <a href={p.href} aria-current="page">{p.title}</a> : <a href={p.href}>{p.title}</a>)} </li> }} </ol> </details> {(prev != null || next != null) && <div.ui-bseries-steps> {prev != null && (prev.href != null ? <a.ui-bseries-step.ui-bseries-prev href={prev.href} rel="prev"><small>← Part {at - 1}</small><span>{prev.title}</span></a> : <div.ui-bseries-step.ui-bseries-prev><small>← Part {at - 1}</small><span>{prev.title}</span></div>)} {next != null && (next.href != null ? <a.ui-bseries-step.ui-bseries-next href={next.href} rel="next"><small>Part {at + 1} →</small><span>{next.title}</span></a> : <div.ui-bseries-step.ui-bseries-next.ui-bseries-soon><small>Part {at + 1}, {next.soon != null ? `coming ${next.soon}` : "coming soon"}</small><span>{next.title}</span></div>)} </div>} </nav>}Related
PostArchiveA blog's posts by year and month, newest first: each year a disclosure with its count, its months and their posts, or only the months as links.10 views · 10 downloadsBlogrollThe blogs a writer recommends, in groups: name, a line about each, its latest post and how long ago, its feed, and the list as OPML.9 views · 9 downloadsRelatedPostsRead next: other posts at the end of one, those sharing the most tags first, with picture, title, excerpt, date and the shared tags marked.8 views · 9 downloadsMasterDetailA list on the left and the item picked in detail on the right; one at a time with a Back button when narrow.7 views · 7 downloadsAccordionSections that open under their titles, on the browser's own <details>: exclusive with a group.5 views · 9 downloadsAlphabetIndexA to Z links into a list sorted by name; letters with nothing under them stay in place, disabled.4 views · 7 downloads
Thread
Thread 0 posts
On SeriesNav: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.