Timer
A countdown set in minutes with Start, Pause and Reset, a ring that empties, presets, and a silent alert at zero.
Demo
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.qwemain.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>}Related
StopwatchA stopwatch to the hundredth: Start, Lap, Stop and Reset, with the laps listed and the fastest and slowest marked.7 views · 5 downloadsWorldClockThe time now in several cities, ticking, with the day there and how far ahead or behind the first one each is.7 views · 7 downloadsAgendaListEvents grouped by day with the date at the left, each with its time, title and place; today and tomorrow named.6 views · 6 downloadsDayViewOne day as a column of hours with its events drawn at their times, side by side when they overlap, and all-day events above.7 views · 7 downloadsEventCardAn event as a card: a date block, title, time, place, a line about it, an optional picture and badge, and a button.7 views · 6 downloadsOpeningHoursA week's opening hours with today marked and whether it is open now, closing soon, or when it opens next.6 views · 6 downloads
Thread
Thread 0 posts
On Timer: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.