Scheduling

Stopwatch

A stopwatch to the hundredth: Start, Lap, Stop and Reset, with the laps listed and the fastest and slowest marked.

#schedule#time#clock#keyboard#interactive

Demo

Preview
00:00.00

Press Start, or focus it and press Space; L takes a lap.

00:00.00

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

Example

The code of the preview above.

demos/stopwatch.qwe
component StopwatchDemoLive() {    state log = []    <div.demo-stack.demo-narrow>        <Stopwatch label="Run" on_start={() => { log = log | push("start") }} on_stop={(ms) => { log = log | push(`stop ${ms >= 0 ? "ok" : "?"}`) }}                   on_lap={(n, ms) => { log = log | push(`lap ${n}`) }} on_reset={() => { log = log | push("reset") }}/>        <p.demo-note>{len(log) == 0 ? "Press Start, or focus it and press Space; L takes a lap." : join(log, " · ")}</p>    </div>}view StopwatchDemo() =    <div.demo-row style="align-items: flex-start">        <div style="flex: 1 1 300px"><StopwatchDemoLive/></div>        <div id="stopwatch-demo-sm" style="flex: 0 1 220px"><Stopwatch label="Plank" size="sm" laps={false}/></div>    </div>

Source

ui/stopwatch.qwe
// Stopwatch: Start, Lap, Stop and Reset, the time running in minutes, seconds and// hundredths (hours once there is one), and the laps listed under it with each lap's// time and the total, the fastest and slowest marked. Space starts and stops it when// it has the focus, L takes a lap.////   <Stopwatch/>//   <Stopwatch label="Plank" on_stop={(ms) => { best = ms }} on_lap={(lap, ms) => { laps = laps | push(ms) }}/>//// label: what it times, above it and what screen readers call it ("Stopwatch").// laps: false drops the Lap button and the list. size: "sm", "md" (default) or "lg".// on_start(): inside one of your components, called when it starts.// on_stop(ms): called with the total, in milliseconds, when it is stopped.// on_lap(n, ms): called with the lap's number and its time, in milliseconds.// on_reset(): called when it is reset.import ui/theme.qwe use ui_clslet stopwatch_css = css`.ui-stopwatch { display: grid; gap: 12px; justify-items: center; min-width: 0; padding: 18px; 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); }.ui-stopwatch:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stopwatch-label { color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-stopwatch-time { font: 600 44px/1 var(--ui-mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }.ui-stopwatch-time small { font-size: .55em; color: var(--ui-muted); }.ui-stopwatch-sm .ui-stopwatch-time { font-size: 30px; }.ui-stopwatch-lg .ui-stopwatch-time { font-size: 60px; }.ui-stopwatch-buttons { display: flex; gap: 10px; }.ui-stopwatch-btn {  display: inline-flex; align-items: center; justify-content: center; min-width: 84px; height: 38px; padding: 0 16px; border: 1px solid var(--ui-border);  border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: 600 14px/1 var(--ui-font); cursor: pointer; transition: background .15s;}.ui-stopwatch-btn:hover { background: var(--ui-subtle); }.ui-stopwatch-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stopwatch-btn:disabled { opacity: .5; cursor: not-allowed; }.ui-stopwatch-go { border-color: transparent; background: var(--ui-success); color: #fff; }.ui-stopwatch-go:hover { background: color-mix(in srgb, var(--ui-success) 88%, var(--ui-fg)); }.ui-stopwatch-stop { border-color: transparent; background: var(--ui-danger); color: #fff; }.ui-stopwatch-stop:hover { background: color-mix(in srgb, var(--ui-danger) 88%, var(--ui-fg)); }.ui-stopwatch-laps { width: 100%; max-height: 220px; margin: 0; padding: 0; overflow-y: auto; list-style: none; border-top: 1px solid var(--ui-border); }.ui-stopwatch-lap { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; padding: 6px 4px; border-bottom: 1px solid var(--ui-border); font-variant-numeric: tabular-nums; }.ui-stopwatch-lap span:first-child { color: var(--ui-muted); }.ui-stopwatch-lap span:nth-child(2) { font-family: var(--ui-mono); }.ui-stopwatch-lap span:last-child { color: var(--ui-muted); font-family: var(--ui-mono); }.ui-stopwatch-fast span:nth-child(2) { color: var(--ui-success); }.ui-stopwatch-slow span:nth-child(2) { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-stopwatch-btn { transition: none; } }`// Milliseconds as "MM:SS.hh", or "H:MM:SS.hh" from an hour.fn stopwatch_format(ms: num) -> str {    let two = (n: int) => n < 10 ? `0${n}` : str(n)    let cs = floor(ms / 10)    let h = floor(cs / 360000)    let m = floor(cs / 6000) % 60    let s = floor(cs / 100) % 60    `${h > 0 ? `${h}:` : ""}${two(m)}:${two(s)}.${two(cs % 100)}`}// Milliseconds as screen readers hear them: "1 minute, 5.32 seconds".fn stopwatch_said(ms: num) -> str {    let m = floor(ms / 60000)    let s = round((ms - m * 60000) / 1000, 2)    `${m > 0 ? `${m} ${m == 1 ? "minute" : "minutes"}, ` : ""}${s} seconds`}component Stopwatch(label: str = "Stopwatch", laps: bool = true, size: str = "md", on_start = null, on_stop = null,                    on_lap = null, on_reset = null, class: str? = null) {    style global stopwatch_css    state running = false    state since = 0    state banked = 0    state tick = 0    state taken = []    state said = ""    let total = running ? banked + (tick - since) : banked    if running {        every(37, () => { tick = now() })    }    let start = () => {        since = now()        tick = since        running = true        said = "Started"        on_start && on_start()    }    let stop = () => {        let t = now()        banked = banked + (t - since)        tick = t        running = false        said = `Stopped at ${stopwatch_said(banked)}`        on_stop && on_stop(banked)    }    let lap = () => {        let t = now()        let at = banked + (t - since)        let before = len(taken) > 0 ? last(taken).total : 0        let n = len(taken) + 1        taken = taken | push({ n, split: at - before, total: at })        said = `Lap ${n}: ${stopwatch_said(at - before)}`        on_lap && on_lap(n, at - before)    }    let reset = () => {        banked = 0        taken = []        said = "Reset"        on_reset && on_reset()    }    let splits = taken | map((l) => l.split)    let fast = len(taken) > 1 ? min(splits) : null    let slow = len(taken) > 1 ? max(splits) : null    let shown = stopwatch_format(total)    <div class={ui_cls(["ui-stopwatch", size != "md" && `ui-stopwatch-${size}`, class])} role="group" aria-label={label} tabindex="0"         @keydown={(e) => {             if e.target == e.currentTarget && e.key == " " { e.preventDefault(); if running { stop() } else { start() } }             if e.target == e.currentTarget && (e.key == "l" || e.key == "L") && running && laps { e.preventDefault(); lap() }         }}>        <span.ui-stopwatch-label aria-hidden="true">{label}</span>        <span.ui-stopwatch-time role="timer" aria-label={stopwatch_said(total)}>{slice(shown, 0, len(shown) - 3)}<small>{slice(shown, len(shown) - 3, len(shown))}</small></span>        <div.ui-stopwatch-buttons>            {running                ? <button.ui-stopwatch-btn.ui-stopwatch-stop type="button" @click={stop}>Stop</button>                : <button.ui-stopwatch-btn.ui-stopwatch-go type="button" @click={start}>{banked > 0 ? "Resume" : "Start"}</button>}            {running                ? (laps && <button.ui-stopwatch-btn type="button" @click={lap}>Lap</button>)                : <button.ui-stopwatch-btn type="button" disabled={banked == 0} @click={reset}>Reset</button>}        </div>        {laps && len(taken) > 0 && <ol.ui-stopwatch-laps aria-label="Laps">            {for l in reverse(taken) => <li key={l.n} class={ui_cls(["ui-stopwatch-lap", l.split == fast && "ui-stopwatch-fast", l.split == slow && "ui-stopwatch-slow"])}>                <span>Lap {l.n}</span>                <span>{stopwatch_format(l.split)}{l.split == fast ? <span.ui-sr> (fastest)</span> : null}{l.split == slow ? <span.ui-sr> (slowest)</span> : null}</span>                <span>{stopwatch_format(l.total)}</span>            </li>}        </ol>}        <span.ui-sr aria-live="polite">{said}</span>    </div>}
Thread
75Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.