Scheduling

Timer

A countdown set in minutes with Start, Pause and Reset, a ring that empties, presets, and a silent alert at zero.

#schedule#time#clock#progress#interactive

Demo

Preview
00:02

Not ended yet.

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

Example

The code of the preview above.

demos/timer.qwe
component TimerDemoLive() {    state ended = 0    state log = []    <div.demo-stack>        <Timer id="timer-demo-short" seconds={2} label="Tea" presets={[]} done="Tea is ready"               on_end={() => { ended += 1 }} on_start={() => { log = log | push("start") }} on_pause={() => { log = log | push("pause") }}/>        <p.demo-note>{ended == 0 ? "Not ended yet." : `on_end called ${ended} ${ended == 1 ? "time" : "times"}.`} {join(log, " · ")}</p>    </div>}view TimerDemo() =    <div.demo-row style="align-items: flex-start">        <div style="flex: 1 1 260px"><TimerDemoLive/></div>        <div style="flex: 1 1 300px" id="timer-demo-focus"><Timer id="timer-demo-pomodoro" minutes={25} label="Focus"/></div>    </div>

Source

ui/timer.qwe
// Timer: a countdown you set in minutes, with Start, Pause and Reset, the time left// shown large inside a ring that empties, and, when it reaches zero, a message that// screen readers are told and the ring flashes (without a sound). Quick presets set it// in one press.////   <Timer minutes={25} label="Focus"/>//   <Timer minutes={5} presets={[1, 5, 10]} on_end={() => { done = true }}/>//// minutes: what it is set to first (5). seconds: instead, a number of seconds (for short// timers). presets: minutes to offer as buttons ([1, 5, 10, 25]; [] for none). adjust: false// hides the minutes field and the presets. label: what it times, shown above and what// screen readers call it ("Timer"). done: what it says at zero ("Time's up").// on_end(): inside one of your components, called once when it reaches zero.// on_start(), on_pause(): called when it starts and pauses.import ui/theme.qwe use ui_clslet timer_css = css`.ui-timer { display: grid; gap: 14px; 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-timer-label { color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-timer-dial { position: relative; display: grid; place-items: center; width: 168px; height: 168px; }.ui-timer-dial svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }.ui-timer-dial circle { fill: none; stroke-width: 7; }.ui-timer-track { stroke: var(--ui-subtle); }.ui-timer-fill { stroke: var(--ui-accent); stroke-linecap: round; transition: stroke-dashoffset .25s linear; }.ui-timer-time { font: 600 36px/1 var(--ui-mono); font-variant-numeric: tabular-nums; }.ui-timer-done .ui-timer-fill { stroke: var(--ui-danger); }.ui-timer-done .ui-timer-dial { animation: ui-timer-flash 1s ease-in-out 3; }@keyframes ui-timer-flash { 50% { opacity: .35; } }.ui-timer-alert { margin: 0; padding: 6px 12px; border-radius: 999px; background: var(--ui-danger-soft); color: var(--ui-danger); font-weight: 600; }.ui-timer-alert:empty { display: none; }.ui-timer-buttons { display: flex; gap: 10px; }.ui-timer-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;}.ui-timer-btn:hover { background: var(--ui-subtle); }.ui-timer-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-timer-btn:disabled { opacity: .5; cursor: not-allowed; }.ui-timer-go { border-color: transparent; background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-timer-go:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-timer-set { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; }.ui-timer-set label { display: inline-flex; align-items: center; gap: 6px; color: var(--ui-muted); font-size: 13px; }.ui-timer-set input {  width: 64px; height: 30px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-fg); font: 14px/1 var(--ui-font); text-align: right;}.ui-timer-set input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-timer-preset { height: 30px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: 999px; background: transparent; color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-timer-preset:hover { border-color: var(--ui-accent); }.ui-timer-preset[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-timer-preset:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-timer-preset:disabled, .ui-timer-set input:disabled { opacity: .5; cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-timer-fill { transition: none; } .ui-timer-done .ui-timer-dial { animation: none; } }`// Milliseconds left as "MM:SS" (rounded up, so it shows 00:01 until the very end), or "H:MM:SS".fn timer_format(ms: num) -> str {    let two = (n: int) => n < 10 ? `0${n}` : str(n)    let s = ceil(ms / 1000)    let h = floor(s / 3600)    `${h > 0 ? `${h}:` : ""}${two(floor(s / 60) % 60)}:${two(s % 60)}`}component Timer(minutes: num = 5, seconds: int? = null, presets: list = [1, 5, 10, 25], adjust: bool = true, label: str = "Timer",                done: str = "Time's up", id: str = "ui-timer", on_end = null, on_start = null, on_pause = null, class: str? = null) {    style global timer_css    state length = (seconds ?? round(minutes * 60)) * 1000    state left = (seconds ?? round(minutes * 60)) * 1000    state ends = 0    state running = false    state over = false    if running {        every(200, () => {            left = ends - now() > 0 ? ends - now() : 0            if left == 0 {                running = false                over = true                on_end && on_end()            }        })    }    let start = () => {        if left == 0 { left = length }        ends = now() + left        over = false        running = true        on_start && on_start()    }    let pause = () => {        left = ends - now() > 0 ? ends - now() : 0        running = false        on_pause && on_pause()    }    let choose = (ms: num) => {        length = ms        left = ms        over = false    }    let share = length > 0 ? left / length : 0    <div class={ui_cls(["ui-timer", over && "ui-timer-done", class])} role="group" aria-label={label}>        <span.ui-timer-label aria-hidden="true">{label}</span>        <div.ui-timer-dial>            <svg viewBox="0 0 100 100" aria-hidden="true">                <circle.ui-timer-track cx="50" cy="50" r="45"/>                <circle.ui-timer-fill cx="50" cy="50" r="45" stroke-dasharray="282.74" stroke-dashoffset={round(282.74 * (1 - share), 2)}/>            </svg>            <span.ui-timer-time role="timer" aria-label={`${timer_format(left)} left`}>{timer_format(left)}</span>        </div>        <p.ui-timer-alert role="alert">{over ? done : ""}</p>        <div.ui-timer-buttons>            {running                ? <button.ui-timer-btn type="button" @click={pause}>Pause</button>                : <button.ui-timer-btn.ui-timer-go type="button" disabled={length == 0} @click={start}>{left > 0 && left < length ? "Resume" : "Start"}</button>}            <button.ui-timer-btn type="button" disabled={!running && left == length && !over} @click={() => { running = false; choose(length) }}>Reset</button>        </div>        {adjust && <div.ui-timer-set>            <label for={`${id}-minutes`}>Minutes                <input type="number" id={`${id}-minutes`} min="0" max="600" step="1" disabled={running}                       value={str(round(length / 60000, 2))}                       @change={(e) => { choose(round((float(e.value) ?? 0) * 60000)) }}/>            </label>            {for p in presets => <button.ui-timer-preset key={p} type="button" disabled={running}                    aria-pressed={length == p * 60000 ? "true" : "false"} @click={() => { choose(p * 60000) }}>{p} min</button>}        </div>}    </div>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.