PostUpdates
The note that a post changed since it came out: Updated Oct 28, how many changes, and each one newest first, with corrections marked.
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/post_updates.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/post_updates.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/post_updates.qwe
view PostUpdatesDemo() = <div.demo-stack style="max-width: 680px"> <PostUpdates id="post-updates-demo" published="2026-10-14" updates={[ { date: "2026-10-20", kind: "addition", text: "Added a section on [prefetching](#post_updates-prefetch), after questions from readers." }, { date: "2026-10-28T09:30:00Z", kind: "correction", text: "The runtime is **11 KB** gzipped, not 15 KB as first written." }, { date: "2026-10-16", text: "Fixed the example's `every` call, which took its arguments the wrong way round." }, ]}/> <PostUpdates id="post-updates-demo-plain" published="2026-09-02" updates={[{ date: "2026-09-05", text: "Linked the talk this post was based on." }]}/> </div>Source
ui/post_updates.qwe
// PostUpdates: the note at the top (or the end) of a post that says it changed since// it was published: "Updated Oct 28, 2026", how many changes, and, opened, each change// newest first with its date, its kind (an update, a correction, an addition) and what// changed. Corrections are marked so a reader sees one at once. The list opens without// JavaScript (a <details>).//// <PostUpdates published="2026-10-14" updates={[// { date: "2026-10-28", kind: "correction", text: "The bundle is 11 KB gzipped, not 15 KB." },// { date: "2026-10-20", text: "Added a section on [prefetching](#prefetch)." },// ]}/>//// updates: maps of { date, text, kind? }, in any order: date an ISO date or date-time;// text what changed, Markdown inside a line (bold, italic, code, links); kind "update"// (default), "correction" or "addition". published: when the post first came out, said// under the summary. open: the list shown to start with (false; true when there is a// correction and open_corrections is left true). title: the summary's first word// ("Updated"; "Corrected" when the latest change is a correction). locale: how dates are// written ("en-US"). id: the element's id.import ui/theme.qwe use ui_clslet post_updates_css = css`.ui-bupd { margin: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-subtle); font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-bupd-fix { border-color: color-mix(in srgb, var(--ui-danger) 35%, var(--ui-border)); }.ui-bupd summary { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 10px; padding: 10px 14px; cursor: pointer; list-style: none; border-radius: var(--ui-radius); }.ui-bupd summary::-webkit-details-marker { display: none; }.ui-bupd summary:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bupd-icon { flex: none; width: 18px; height: 18px; color: var(--ui-info); }.ui-bupd-fix .ui-bupd-icon { color: var(--ui-danger); }.ui-bupd-head { font-weight: 650; }.ui-bupd-sub { color: var(--ui-muted); }.ui-bupd-more { margin-left: auto; color: var(--ui-accent); font-size: 13px; font-weight: 600; white-space: nowrap; }.ui-bupd-more::after { content: ""; display: inline-block; width: 6px; height: 6px; margin: 0 2px 2px 7px; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(45deg); transition: transform .15s; }.ui-bupd details[open] .ui-bupd-more::after { transform: translateY(3px) rotate(-135deg); }.ui-bupd-shut { display: inline; } .ui-bupd-opened { display: none; }.ui-bupd details[open] .ui-bupd-shut { display: none; } .ui-bupd details[open] .ui-bupd-opened { display: inline; }.ui-bupd-list { display: grid; gap: 0; margin: 0; padding: 0 14px 6px; list-style: none; }.ui-bupd-item { display: grid; grid-template-columns: 7.5em minmax(0, 1fr); gap: 2px 12px; padding: 10px 0; border-top: 1px solid var(--ui-border); }.ui-bupd-item time { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-bupd-kind { display: inline-block; margin-right: 8px; padding: 0 7px; border-radius: 999px; background: var(--ui-info-soft); color: var(--ui-info); font-size: 11px; font-weight: 700; letter-spacing: .04em; line-height: 18px; text-transform: uppercase; vertical-align: 1px; }.ui-bupd-correction .ui-bupd-kind { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-bupd-addition .ui-bupd-kind { background: var(--ui-success-soft); color: var(--ui-success); }.ui-bupd-text a { color: var(--ui-accent); }.ui-bupd-text code { font-family: var(--ui-mono); font-size: .92em; }@media (max-width: 480px) { .ui-bupd-item { grid-template-columns: 1fr; } .ui-bupd-more { margin-left: 0; } }@media (prefers-reduced-motion: reduce) { .ui-bupd-more::after { transition: none; } }`// An ISO date or date-time as the locale writes it ("Oct 28, 2026").fn post_updates_day(d: str, locale: str) -> str = date.valid(d) ? date.format(slice(d, 0, 10), { locale, style: "medium" }) : dview PostUpdates(updates: list, published: str? = null, open: bool = false, open_corrections: bool = true, title: str? = null, locale: str = "en-US", id: str? = null, class: str? = null) { style global post_updates_css let items = updates | sort_by((u) => str(u.date)) | reverse let latest = first(items) let fixes = items | filter((u) => u.kind == "correction") | len let head = title ?? (latest != null && latest.kind == "correction" ? "Corrected" : "Updated") let n = len(items) let kind_name = (k) => match k { "correction" => "Correction", "addition" => "Addition", _ => "Update" } if latest == null { return null } <aside id={id} class={ui_cls(["ui-bupd", fixes > 0 && "ui-bupd-fix", class])} aria-label="Changes to this post"> <details open={open || (open_corrections && fixes > 0)}> <summary> <svg.ui-bupd-icon viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> {fixes > 0 ? <path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/> : [<path d="M21 12a9 9 0 1 1-3-6.7L21 8"/>, <path d="M21 3v5h-5M12 7v5l3 2"/>]} </svg> <span.ui-bupd-head>{head} <time datetime={str(latest.date)}>{post_updates_day(str(latest.date), locale)}</time></span> <span.ui-bupd-sub> {n == 1 ? "1 change" : `${n} changes`}{fixes > 0 && `, ${fixes == 1 ? "1 correction" : `${fixes} corrections`}`} {published != null && [" since it was published on ", <time datetime={published}>{post_updates_day(published, locale)}</time>]} </span> <span.ui-bupd-more aria-hidden="true"><span.ui-bupd-shut>See what changed</span><span.ui-bupd-opened>Hide</span></span> </summary> <ol.ui-bupd-list> {for u in items => <li class={ui_cls(["ui-bupd-item", `ui-bupd-${u.kind ?? "update"}`])}> <time datetime={str(u.date)}>{post_updates_day(str(u.date), locale)}</time> <div><span.ui-bupd-kind>{kind_name(u.kind)}</span><span.ui-bupd-text>{markdown(u.text, { inline: true })}</span></div> </li>} </ol> </details> </aside>}Related
PostHeaderThe top of a post: category, title and standfirst, the authors with their pictures, published and updated dates, reading time, tags and a cover.8 views · 5 downloadsSidenoteNotes in the margin beside the line they belong to, numbered or not; on a narrow page they fold, and the number opens each under its line.8 views · 7 downloadsAuthorBioThe box at the end of a post about its author: picture or initials, name and role, a short bio, links elsewhere, their post count and a follow link.8 views · 9 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 · 8 downloadsClapButtonApplause for a post: each press adds a clap up to a limit per reader, a +3 rises for presses in a row, and a small × takes them back.8 views · 8 downloadsFeedSubscribeFollow this blog: its RSS, Atom or JSON feeds with their addresses to copy, links into feed readers, what a feed is, and an email box.8 views · 7 downloads
Thread
Thread 0 posts
On PostUpdates: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.