RatingSummary
The average rating out of 5, how many ratings, and a bar per star count, each a filter button or link if you like.
Demo
Showing all reviews.
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/rating_summary.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/rating_summary.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/rating_summary.qwe
component RatingSummaryDemoLive() { state only = null <div.demo-stack> <RatingSummary id="rating-summary-demo-live" counts={{ "5": 61, "4": 22, "3": 9, "2": 3, "1": 5 }} picked={only} on_pick={(s) => { only = s }}/> <p.demo-note>{only == null ? "Showing all reviews." : `Showing ${only}-star reviews.`}</p> </div>}view RatingSummaryDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 28px; align-items: start"> <RatingSummary id="rating-summary-demo-plain" label="Kettle" counts={{ "5": 120, "4": 40, "3": 10, "2": 4, "1": 6 }} hrefs={{ "5": "#rating_summary", "4": "#rating_summary" }}/> <RatingSummaryDemoLive/> <RatingSummary id="rating-summary-demo-compact" compact={true} average={4.8} counts={{ "5": 9, "4": 1 }}/> <RatingSummary id="rating-summary-demo-none" counts={{}}/> </div>Source
ui/rating_summary.qwe
// RatingSummary: how a thing is rated: the average out of 5, how many ratings, and// a bar for each star count, so a glance says whether the fives or the ones win.//// <RatingSummary counts={{ "5": 120, "4": 40, "3": 10, "2": 4, "1": 6 }}/>// <RatingSummary counts={tally} picked={only} on_pick={(s) => { only = s }}/>//// counts: a map of star to how many gave it ({ "5": 120, ..., "1": 6 }; a star left// out counts 0). average: the average to show, worked out from the counts when// left out (one decimal). label: what it is a rating of, for screen readers.// on_pick(stars): inside one of your components, makes each row a button that// is called with its stars (null again for the one already picked); picked: the// stars whose row shows pressed. Without on_pick, hrefs: a map of star to a link// each row goes to. compact: the average and bars side by side in less room.import ui/theme.qwe use ui_clslet rating_summary_css = css`.ui-rating-summary { display: grid; grid-template-columns: auto 1fr; gap: 8px 20px; align-items: center; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-rating-summary-avg { display: grid; justify-items: center; gap: 2px; min-width: 96px; text-align: center; }.ui-rating-summary-num { font-size: 40px; font-weight: 700; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }.ui-rating-summary-stars { display: inline-flex; gap: 1px; color: var(--ui-warning); }.ui-rating-summary-stars svg { width: 16px; height: 16px; }.ui-rating-summary-off { color: var(--ui-border); }.ui-rating-summary-half { clip-path: inset(0 50% 0 0); }.ui-rating-summary-total { color: var(--ui-muted); font-size: 13px; }.ui-rating-summary-rows { display: grid; gap: 5px; margin: 0; padding: 0; list-style: none; }.ui-rating-summary-row { display: grid; grid-template-columns: 4.2em 1fr 3.2em; align-items: center; gap: 10px; width: 100%; padding: 2px 0; border: 0; border-radius: var(--ui-radius-sm); background: none; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; }.ui-rating-summary-row:is(button, a) { cursor: pointer; }.ui-rating-summary-row:is(button, a):hover { background: var(--ui-subtle); }.ui-rating-summary-row:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-rating-summary-row[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-rating-summary-row[aria-pressed="true"] .ui-rating-summary-fill { background: var(--ui-accent); }.ui-rating-summary-name { white-space: nowrap; font-size: 13px; }.ui-rating-summary-bar { height: 8px; border-radius: 999px; background: var(--ui-subtle); overflow: hidden; }.ui-rating-summary-fill { display: block; height: 100%; border-radius: inherit; background: var(--ui-warning); }.ui-rating-summary-n { color: var(--ui-muted); font-size: 13px; text-align: right; font-variant-numeric: tabular-nums; }.ui-rating-summary-compact { gap: 4px 14px; }.ui-rating-summary-compact .ui-rating-summary-num { font-size: 28px; }.ui-rating-summary-compact .ui-rating-summary-avg { min-width: 72px; }.ui-rating-summary-compact .ui-rating-summary-rows { gap: 2px; }@container (max-width: 360px) { .ui-rating-summary { grid-template-columns: 1fr; } }`fn rating_summary_count(counts: map, s: int) -> int = int(get(counts, str(s)) ?? 0) ?? 0view RatingSummary(counts: map = {}, average: num? = null, label: str = "Rating", picked: int? = null, on_pick = null, hrefs: map = {}, compact: bool = false, id: str? = null, class: str? = null) { style global rating_summary_css let total = sum(for s in range(1, 6) => rating_summary_count(counts, s)) let avg = average ?? (total == 0 ? 0 : round(sum(for s in range(1, 6) => s * rating_summary_count(counts, s)) / total, 1)) let top = max(for s in range(1, 6) => rating_summary_count(counts, s)) ?? 0 <div class={ui_cls(["ui-rating-summary", compact && "ui-rating-summary-compact", class])} id={id}> <div.ui-rating-summary-avg> <span.ui-rating-summary-num>{avg}</span> <span.ui-rating-summary-stars role="img" aria-label={`${label}: ${avg} out of 5 stars`}> {for i in range(0, 5) => { let fill = avg >= i + 1 ? "full" : avg >= i + 0.5 ? "half" : "off" <svg viewBox="0 0 24 24" class={fill == "off" ? "ui-rating-summary-off" : null} fill="currentColor" aria-hidden="true"> {fill == "half" && <path.ui-rating-summary-off d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>} <path class={fill == "half" ? "ui-rating-summary-half" : null} d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/> </svg> }} </span> <span.ui-rating-summary-total>{total} {total == 1 ? "rating" : "ratings"}</span> </div> <ul.ui-rating-summary-rows aria-label="Ratings by stars"> {for s in [5, 4, 3, 2, 1] => { let n = rating_summary_count(counts, s) let pct = total == 0 ? 0 : round(n * 100 / total) let body = [ <span.ui-rating-summary-name>{s} {s == 1 ? "star" : "stars"}</span>, <span.ui-rating-summary-bar aria-hidden="true"><span.ui-rating-summary-fill style={`width: ${top == 0 ? 0 : round(n * 100 / top)}%`}></span></span>, <span.ui-rating-summary-n>{pct}%</span>, <span.ui-sr>, {n} {n == 1 ? "rating" : "ratings"}</span>, ] <li key={s}> {on_pick != null ? <button.ui-rating-summary-row type="button" aria-pressed={picked == s ? "true" : "false"} disabled={n == 0} @click={() => { on_pick(picked == s ? null : s) }}>{body}</button> : get(hrefs, str(s)) != null ? <a.ui-rating-summary-row href={get(hrefs, str(s))} aria-current={picked == s ? "true" : null}>{body}</a> : <span.ui-rating-summary-row>{body}</span>} </li> }} </ul> </div>}Related
ActivityFeedA stream of what people did, with their pictures, an icon for each kind of action, when, quotes and day headings.5 views · 7 downloadsBarcodeA Code 128 barcode drawn as SVG from text, switching to the compact digit set for long numbers, with the text under it.4 views · 10 downloadsComparisonTableFeatures down the side and options across, with ticks, crosses and values in the cells, and one option picked out.5 views · 6 downloadsKpiCardA 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 downloadsLeaderboardPeople ranked by score with medals for the top three, shared places for ties, and your own row kept in view.7 views · 7 downloadsPriceBreakdownWhat a price is made of, a line each (a discount in green), and the total.6 views · 4 downloads
Thread
Thread 0 posts
On RatingSummary: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.