Dashboard

UsageMeter

Used of allowed as a bar with the numbers, amber near the limit, red at it, and plain when there is no limit.

#dashboard#progress#status#data#no-js

Demo

Preview
Files, backups and exports
Resets on the 1stNear the limit
Limit reached
Over the limit

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/usage_meter.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/usage_meter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/usage_meter.qwe
view UsageMeterDemo() =    <div.demo-stack.demo-narrow>        <UsageMeter id="usage-meter-demo" label="Storage" used={38.2} allowed={50} unit=" GB" hint="Files, backups and exports"/>        <UsageMeter label="API calls" used={92000} allowed={100000} warn_at={85} hint="Resets on the 1st"/>        <UsageMeter label="Team members" used={5} allowed={5}/>        <UsageMeter label="Email sends" used={12400} allowed={10000} size="lg"/>        <UsageMeter label="Projects" used={3} allowed={null} size="sm"/>    </div>

Source

ui/usage_meter.qwe
// UsageMeter: how much of an allowance is used, as a bar with the numbers, that// turns amber near the limit and red at it. For several at once, QuotaBar.////   <UsageMeter label="Storage" used={38.2} allowed={50} unit=" GB"/>//   <UsageMeter label="API calls" used={92000} allowed={100000} warn_at={85} hint="Resets on the 1st"/>//   <UsageMeter label="Seats" used={3} allowed={null}/>                 no limit//// label: what is measured. used and allowed: the numbers; unit comes after each// ("%", " GB"; thousands get commas). allowed null means no limit: the numbers// say so and the bar stays empty. warn_at: the percentage from which it is amber// and says "Near the limit" (80); at 100 it is red and says "Limit reached"; over// it, "Over the limit" with the bar full. hint: a line under it. show_pct: the// percentage after the numbers (true). size: "sm", "md" (default) or "lg". It is// a role="meter" with the numbers as its text.import ui/theme.qwe use ui_clslet usage_meter_css = css`.ui-usage { display: grid; gap: 6px; min-width: 0; font: 13px/1.4 var(--ui-font); color: var(--ui-fg); --t: var(--ui-accent); }.ui-usage-warn { --t: var(--ui-warning); }.ui-usage-full, .ui-usage-over { --t: var(--ui-danger); }.ui-usage-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; }.ui-usage-label { font-weight: 500; font-size: 14px; }.ui-usage-nums { color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-usage-nums b { color: var(--ui-fg); font-weight: 600; }.ui-usage-track { position: relative; height: 8px; overflow: hidden; border-radius: 999px; background: var(--ui-subtle);  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-border) 60%, transparent); }.ui-usage-sm .ui-usage-track { height: 4px; }.ui-usage-lg .ui-usage-track { height: 12px; }.ui-usage-fill { height: 100%; border-radius: inherit; background: var(--t); transition: width .3s ease; }.ui-usage-mark { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--ui-bg); }.ui-usage-foot { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; color: var(--ui-muted); }.ui-usage-warning { display: inline-flex; align-items: center; gap: 5px; color: var(--t); font-weight: 600; }.ui-usage-warning svg { width: 14px; height: 14px; }@media (prefers-reduced-motion: reduce) { .ui-usage-fill { transition: none; } }`// A number with thousands separators, to two decimals at most: 92,000 or 38.2.fn usage_meter_num(n: num) -> str {    let r = round(n * 100) / 100    let whole = floor(abs(r))    let frac = r == floor(r) ? "" : slice(str(round((abs(r) - whole) * 100) / 100), 1)    let digits = str(whole)    let groups = []    let rest = digits    while len(rest) > 3 {        groups = [slice(rest, len(rest) - 3, len(rest))] + groups        rest = slice(rest, 0, len(rest) - 3)    }    (r < 0 ? "-" : "") + join([rest] + groups, ",") + frac}view UsageMeter(label: str, used: num, allowed: num? = null, unit: str = "", warn_at: num = 80, hint: str? = null,                show_pct: bool = true, size: str = "md", id: str? = null, class: str? = null) {    style global usage_meter_css    let pct = allowed != null && allowed > 0 ? used * 100 / allowed : 0    let shown = pct < 0 ? 0 : (pct > 100 ? 100 : pct)    let mood = allowed == null ? null : pct > 100 ? "over" : pct >= 100 ? "full" : pct >= warn_at ? "warn" : null    let words = match mood { "over" => "Over the limit", "full" => "Limit reached", "warn" => "Near the limit", _ => null }    let text = allowed == null        ? `${usage_meter_num(used)}${unit} used, no limit`        : `${usage_meter_num(used)}${unit} of ${usage_meter_num(allowed)}${unit}${show_pct ? ` (${round(pct)}%)` : ""}`    <div id={id} class={ui_cls(["ui-usage", mood != null && `ui-usage-${mood}`, size != "md" && `ui-usage-${size}`, class])}>        <div.ui-usage-head aria-hidden="true">            <span.ui-usage-label>{label}</span>            <span.ui-usage-nums>                <b>{usage_meter_num(used)}{unit}</b>                {allowed == null ? " used, no limit" : ` of ${usage_meter_num(allowed)}${unit}`}                {allowed != null && show_pct && ` · ${round(pct)}%`}            </span>        </div>        <div.ui-usage-track role="meter" aria-label={label} aria-valuemin="0" aria-valuemax={allowed != null ? usage_meter_num(allowed) : null}             aria-valuenow={usage_meter_num(used)} aria-valuetext={text}>            <div.ui-usage-fill style={`width: ${round(shown * 10) / 10}%`}></div>            {allowed != null && warn_at > 0 && warn_at < 100 && <span.ui-usage-mark style={`left: ${warn_at}%`}></span>}        </div>        {(hint != null || words != null) && <div.ui-usage-foot>            <span>{hint}</span>            {words != null && <span.ui-usage-warning role="status">                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"                     aria-hidden="true"><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"/></svg>                {words}            </span>}        </div>}    </div>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On UsageMeter: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.