MilestoneList
Milestones down a line with dates, done, current, coming or late, a progress bar on those under way, and the whole project's progress.
Demo
Launch
2 of 5 doneDesign signed off (done)
Private alpha (done)
Beta (current)
Due3 of 5 features in
Security review (late)
Was dueWaiting on the auditors
Public launch (to come)
Due
Hire two engineers (to come)
DueOpen the Porto office (current)
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/milestone_list.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/milestone_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/milestone_list.qwe
view MilestoneListDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; align-items: start"> <MilestoneList id="milestone-list-demo-launch" title="Launch" items={[ { title: "Design signed off", date: "2026-09-01", status: "done" }, { title: "Private alpha", date: "2026-09-20", status: "done", href: "#milestone_list" }, { title: "Beta", date: "2026-10-15", status: "current", progress: 60, text: "3 of 5 features in" }, { title: "Security review", date: "2026-10-20", status: "late", progress: 25, text: "Waiting on the auditors" }, { title: "Public launch", date: "2026-11-30" }, ]}/> <MilestoneList id="milestone-list-demo-plain" summary={false} label="Q4" items={[ { title: "Hire two engineers", date: "December" }, { title: "Open the Porto office", status: "current" }, ]}/> </div>Source
ui/milestone_list.qwe
// MilestoneList: the milestones of a project down a line: each with its date,// whether it is done, current, coming or late, and how far along the current ones// are; with the whole project's progress at the top.//// <MilestoneList title="Launch" items={[// { title: "Design signed off", date: "2026-09-01", status: "done" },// { title: "Beta", date: "2026-10-15", status: "current", progress: 60, text: "3 of 5 features in" },// { title: "Public launch", date: "2026-11-30" },// ]}/>//// items: maps of { title, date?, status?, progress?, text?, href? }: status is// "done", "current", "coming" (the default) or "late"; date an ISO date, shown as// "Oct 15, 2026" (other text as it is); progress 0 to 100, a bar under a current// or late one; text a line under the title; href makes the title a link.// title: a heading over the list, with "2 of 5 done" and a bar of the whole.// summary: false leaves that out. label: what screen readers call the list// (the title, else "Milestones"). id: its id. It is an <ol>.import ui/theme.qwe use ui_clslet milestone_list_css = css`.ui-milestone-list { display: grid; gap: 14px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-milestone-list-head { display: grid; gap: 6px; }.ui-milestone-list-top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }.ui-milestone-list-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-milestone-list-sum { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-milestone-list-bar { height: 6px; border-radius: 999px; background: var(--ui-subtle); overflow: hidden; }.ui-milestone-list-fill { display: block; height: 100%; border-radius: inherit; background: var(--ui-success); }.ui-milestone-list-items { margin: 0; padding: 0; list-style: none; }.ui-milestone-list-item { --t: var(--ui-strong); position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 12px; padding-bottom: 18px; }.ui-milestone-list-item:last-child { padding-bottom: 0; }.ui-milestone-list-item::before { content: ""; position: absolute; left: 11px; top: 24px; bottom: 0; width: 2px; background: var(--ui-border); }.ui-milestone-list-item:last-child::before { display: none; }.ui-milestone-list-done::before { background: var(--ui-success); }.ui-milestone-list-done { --t: var(--ui-success); }.ui-milestone-list-current { --t: var(--ui-accent); }.ui-milestone-list-late { --t: var(--ui-danger); }.ui-milestone-list-mark { display: grid; place-items: center; width: 24px; height: 24px; border: 2px solid var(--t); border-radius: 50%; background: var(--ui-surface); color: #fff; }.ui-milestone-list-mark svg { width: 12px; height: 12px; }.ui-milestone-list-done .ui-milestone-list-mark { background: var(--ui-success); }.ui-milestone-list-current .ui-milestone-list-mark { box-shadow: var(--ui-focus); }.ui-milestone-list-current .ui-milestone-list-mark::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-accent); }.ui-milestone-list-late .ui-milestone-list-mark { color: var(--ui-danger); font-size: 13px; font-weight: 800; line-height: 1; }.ui-milestone-list-body { display: grid; gap: 3px; min-width: 0; padding-top: 1px; }.ui-milestone-list-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 12px; }.ui-milestone-list-name { margin: 0; font-size: 14px; font-weight: 600; }.ui-milestone-list-name a { color: inherit; text-decoration: none; }.ui-milestone-list-name a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-milestone-list-name a:focus-visible { outline: none; border-radius: 4px; box-shadow: var(--ui-focus); }.ui-milestone-list-coming .ui-milestone-list-name { color: var(--ui-muted); font-weight: 500; }.ui-milestone-list-when { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-milestone-list-late .ui-milestone-list-when { color: var(--ui-danger); font-weight: 600; }.ui-milestone-list-text { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-milestone-list-progress { display: flex; align-items: center; gap: 8px; margin-top: 4px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-milestone-list-progress .ui-milestone-list-bar { flex: 1; max-width: 240px; }.ui-milestone-list-progress .ui-milestone-list-fill { background: var(--t); }`let milestone_list_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date as "Oct 15, 2026"; other text as it is.fn milestone_list_date(d) -> str { let p = split(slice(str(d), 0, 10), "-") len(p) == 3 && int(p[1]) != null && int(p[2]) != null ? `${milestone_list_months[int(p[1]) - 1]} ${int(p[2])}, ${p[0]}` : str(d)}view MilestoneList(items: list = [], title: str? = null, summary: bool = true, label: str? = null, id: str? = null, class: str? = null) { style global milestone_list_css let done = count(items, (m) => m.status == "done") let pct = len(items) == 0 ? 0 : round(done * 100 / len(items)) <div class={ui_cls(["ui-milestone-list", class])} id={id}> {(title != null || summary) && len(items) > 0 && <div.ui-milestone-list-head> <div.ui-milestone-list-top> {title != null && <h3.ui-milestone-list-title>{title}</h3>} {summary && <span.ui-milestone-list-sum>{done} of {len(items)} done</span>} </div> {summary && <div.ui-milestone-list-bar role="progressbar" aria-label={`${title ?? "Milestones"}: done`} aria-valuemin="0" aria-valuemax="100" aria-valuenow={pct}><span.ui-milestone-list-fill style={`width: ${pct}%`}></span></div>} </div>} <ol.ui-milestone-list-items aria-label={label ?? title ?? "Milestones"}> {for [i, m] in enumerate(items) => { let st = m.status ?? "coming" <li key={i} class={`ui-milestone-list-item ui-milestone-list-${st}`} aria-current={st == "current" ? "step" : null}> <span.ui-milestone-list-mark aria-hidden="true"> {st == "done" && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg>} {st == "late" && "!"} </span> <div.ui-milestone-list-body> <div.ui-milestone-list-row> <h4.ui-milestone-list-name>{m.href != null ? <a href={m.href}>{m.title}</a> : m.title}<span.ui-sr> ({st == "coming" ? "to come" : st})</span></h4> {m.date != null && <span.ui-milestone-list-when>{st == "late" ? "Was due " : (st == "done" ? "" : "Due ")} <time datetime={str(m.date)}>{milestone_list_date(m.date)}</time></span>} </div> {m.text != null && <p.ui-milestone-list-text>{m.text}</p>} {m.progress != null && st != "done" && <div.ui-milestone-list-progress> <div.ui-milestone-list-bar role="progressbar" aria-label={`${m.title}: progress`} aria-valuemin="0" aria-valuemax="100" aria-valuenow={m.progress}><span.ui-milestone-list-fill style={`width: ${m.progress}%`}></span></div> <span aria-hidden="true">{m.progress}%</span> </div>} </div> </li> }} </ol> </div>}Related
FlowStepsA process as boxes joined by arrows, with words on the arrows, tones and done, current or coming steps; horizontal, or vertical where narrow.6 views · 8 downloadsProgressListItems each with a label, a bar and its value or percentage, marked in red when one goes over its limit.6 views · 5 downloadsRoadmapColumns (Now, Next, Later) of items with a status badge, tags, a target date and votes; side by side, stacked where narrow.8 views · 7 downloadsChecklistInputCheckboxes under a progress line that says how many are done and fills as they are ticked; each posts with a form.5 views · 7 downloadsActivityFeedA stream of what people did, with their pictures, an icon for each kind of action, when, quotes and day headings.5 views · 7 downloadsAvatarStackOverlapping avatars with names in tooltips, links to each person, and a +N that names the rest.5 views · 10 downloads
Thread
Thread 0 posts
On MilestoneList: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.