Scorecard
A score out of a maximum as a filled ring, with a letter grade, a verdict and the parts it is made of.
Demo
Performance
AExcellent
Largest paint 1.1 s
SEO
DFair
3 pages lack a description
Quiz
RetryNot yet
Accessibility
CFair
5 issues to fix
- Contrast18 / 25
- Labels and names20 / 25
- Keyboard25 / 25
- Structure8 / 25
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/scorecard.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/scorecard.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/scorecard.qwe
view ScorecardDemo() = <div.demo-stack> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px"> <Scorecard id="scorecard-demo-perf" label="Performance" score={92} hint="Largest paint 1.1 s"/> <Scorecard id="scorecard-demo-seo" label="SEO" score={64} hint="3 pages lack a description"/> <Scorecard id="scorecard-demo-quiz" label="Quiz" score={4} limit={10} grade="Retry" verdict="Not yet" tone="accent"/> </div> <Scorecard id="scorecard-demo-a11y" label="Accessibility" score={71} size="lg" hint="5 issues to fix" items={[ { label: "Contrast", score: 18, limit: 25 }, { label: "Labels and names", score: 20, limit: 25 }, { label: "Keyboard", score: 25, limit: 25 }, { label: "Structure", score: 8, limit: 25 }, ]}/> </div>Source
ui/scorecard.qwe
// Scorecard: a score out of a maximum, as a ring filled that far with the score in// it, a letter grade and a verdict, and optionally the parts it is made of: a// site audit, a credit score, a quiz result, a health check.//// <Scorecard label="Performance" score={92}/>// <Scorecard label="Accessibility" score={71} hint="5 issues to fix" items={[// { label: "Contrast", score: 18, limit: 25 },// { label: "Labels", score: 20, limit: 25 },// ]}/>// <Scorecard label="Quiz" score={7} limit={10} grade="Pass" tone="success"/>//// score: the number scored. limit: the most there is (100). label: what is// scored, above the ring and in what screen readers say. grade: the letter or word// shown (by default from the share of limit: A from 90%, B from 80%, C from 70%,// D from 60%, else F). verdict: a word under it (by default "Excellent", "Good",// "Fair" or "Poor"). tone: the ring's colour, "success", "warning" or "danger"// (by default: from 80% success, from 50% warning, else danger), or "accent".// hint: a line under the verdict. items: parts as { label, score, limit? (the// same limit by default) }, each a thin bar beside the ring. size: "md"// (default, 96px ring) or "lg" (128px). card: false drops the border.// The ring is a role="meter" with its value, so screen readers read the score.import ui/theme.qwe use ui_clslet scorecard_css = css`.ui-scorecard { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 24px; min-width: 0; padding: 18px 20px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg); --t: var(--ui-accent); --s: 96px; }.ui-scorecard-bare { padding: 0; border: 0; background: none; }.ui-scorecard-success { --t: var(--ui-success); }.ui-scorecard-warning { --t: var(--ui-warning); }.ui-scorecard-danger { --t: var(--ui-danger); }.ui-scorecard-lg { --s: 128px; }.ui-scorecard-main { display: flex; align-items: center; gap: 16px; }.ui-scorecard-ring { position: relative; flex: none; display: grid; place-items: center; width: var(--s); height: var(--s); }.ui-scorecard-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }.ui-scorecard-ring circle { fill: none; stroke-width: 9; }.ui-scorecard-track { stroke: var(--ui-subtle); }.ui-scorecard-arc { stroke: var(--t); stroke-linecap: round; transition: stroke-dasharray .4s ease; }.ui-scorecard-num { display: grid; justify-items: center; line-height: 1; }.ui-scorecard-num b { font-size: calc(var(--s) * .3); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }.ui-scorecard-num span { margin-top: 3px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-scorecard-text { display: grid; gap: 3px; }.ui-scorecard-label { margin: 0; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-scorecard-grade { display: flex; align-items: center; gap: 8px; margin: 0; }.ui-scorecard-letter { display: inline-grid; place-items: center; min-width: 32px; height: 32px; padding: 0 8px; border-radius: var(--ui-radius-sm); background: color-mix(in srgb, var(--t) 15%, transparent); color: var(--t); font-size: 18px; font-weight: 700; }.ui-scorecard-verdict { font-size: 16px; font-weight: 600; }.ui-scorecard-hint { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-scorecard-items { flex: 1 1 200px; display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-scorecard-item { display: grid; grid-template-columns: 1fr auto; gap: 3px 10px; font-size: 13px; }.ui-scorecard-item span:last-child { color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-scorecard-bar { grid-column: 1 / -1; height: 6px; overflow: hidden; border-radius: 999px; background: var(--ui-subtle); }.ui-scorecard-bar i { display: block; height: 100%; border-radius: inherit; background: var(--t); }@media (prefers-reduced-motion: reduce) { .ui-scorecard-arc { transition: none; } }`// A number as text, whole ones without ".0".fn scorecard_num(n: num) -> str = n == floor(n) ? str(floor(n)) : str(round(n * 10) / 10)fn scorecard_grade(pct: num) -> str = pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 70 ? "C" : pct >= 60 ? "D" : "F"fn scorecard_verdict(pct: num) -> str = pct >= 90 ? "Excellent" : pct >= 75 ? "Good" : pct >= 50 ? "Fair" : "Poor"view Scorecard(score: num, limit: num = 100, label: str? = null, grade: str? = null, verdict: str? = null, tone: str? = null, hint: str? = null, items: list = [], size: str = "md", card: bool = true, id: str? = null, class: str? = null) { style global scorecard_css let part = limit <= 0 ? 0 : (score < 0 ? 0 : (score > limit ? limit : score)) let pct = limit <= 0 ? 0 : round(part * 1000 / limit) / 10 let colour = tone ?? (pct >= 80 ? "success" : pct >= 50 ? "warning" : "danger") let letter = grade ?? scorecard_grade(pct) let word = verdict ?? scorecard_verdict(pct) // The ring: a circle of radius 42 in a 100 box; its length is 2πr, about 263.9. let dash = round(pct * 263.9 / 100 * 10) / 10 <div class={ui_cls(["ui-scorecard", `ui-scorecard-${colour}`, size == "lg" && "ui-scorecard-lg", !card && "ui-scorecard-bare", class])} id={id}> <div.ui-scorecard-main> <div.ui-scorecard-ring role="meter" aria-valuemin="0" aria-valuemax={scorecard_num(limit)} aria-valuenow={scorecard_num(part)} aria-valuetext={`${scorecard_num(score)} out of ${scorecard_num(limit)}, grade ${letter}`} aria-label={label ?? "Score"}> <svg viewBox="0 0 100 100" aria-hidden="true"> <circle.ui-scorecard-track cx="50" cy="50" r="42"/> <circle.ui-scorecard-arc cx="50" cy="50" r="42" stroke-dasharray={`${dash} 264`}/> </svg> <span.ui-scorecard-num aria-hidden="true"><b>{scorecard_num(score)}</b><span>of {scorecard_num(limit)}</span></span> </div> <div.ui-scorecard-text> {label != null && <p.ui-scorecard-label>{label}</p>} <p.ui-scorecard-grade><span.ui-scorecard-letter>{letter}</span><span.ui-scorecard-verdict>{word}</span></p> {hint != null && <p.ui-scorecard-hint>{hint}</p>} </div> </div> {len(items) > 0 && <ul.ui-scorecard-items aria-label={label != null ? `${label}, by part` : "By part"}> {for it in items => { let top = it.limit ?? limit let w = top <= 0 ? 0 : round(min([max([it.score, 0]), top]) * 1000 / top) / 10 <li.ui-scorecard-item> <span>{it.label}</span> <span>{scorecard_num(it.score)} / {scorecard_num(top)}</span> <span.ui-scorecard-bar aria-hidden="true"><i style={`width: ${w}%`}></i></span> </li> }} </ul>} </div>}Related
KpiCardA number with its label, the change since last period coloured by whether that is good, and a small trend as a line or bars.7 views · 7 downloadsProgressListItems each with a label, a bar and its value or percentage, marked in red when one goes over its limit.6 views · 4 downloadsFlowStepsA 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 · 7 downloadsMetricTrendA value with an up or down arrow and its change, coloured by whether the move is good; it can work the percentage out.7 views · 6 downloadsRatingSummaryThe average rating out of 5, how many ratings, and a bar per star count, each a filter button or link if you like.6 views · 5 downloadsStatA KPI tile: a label, a value, how it changed (coloured by whether that is good), and a tiny trend line.6 views · 5 downloads
Thread
Thread 0 posts
On Scorecard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.