@zumer/orbit-kit 0.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -316
- package/package.json +20 -8
- package/src/base.js +606 -41
- package/src/color.js +139 -0
- package/src/o-clock.js +106 -129
- package/src/o-color-wheel.js +322 -0
- package/src/o-compass.js +45 -49
- package/src/o-donut.js +67 -79
- package/src/o-gauge.js +51 -117
- package/src/o-instrument.js +116 -105
- package/src/o-knob.js +137 -129
- package/src/o-menu.js +271 -183
- package/src/o-meter.js +35 -89
- package/src/o-radar-chart.js +391 -0
- package/src/o-radar.js +80 -123
- package/src/o-range.js +509 -0
- package/src/o-rings.js +66 -77
- package/src/o-spinner.js +214 -0
- package/src/o-status.js +209 -0
- package/src/o-sunburst.js +569 -0
- package/src/o-time-picker.js +447 -0
- package/src/o-timer.js +240 -0
- package/src/o-wheel.js +436 -0
- package/src/orbit-kit.js +23 -5
package/src/o-timer.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <o-timer>: a countdown ring with start, pause and reset.
|
|
3
|
+
*
|
|
4
|
+
* .orbit-12 > o-arc.inner-orbit track
|
|
5
|
+
* .orbit-12 > o-progress.inner-orbit time left (shrinks back toward 12 o'clock)
|
|
6
|
+
* .orbit-11 > .vector × ticks optional dial marks
|
|
7
|
+
* .orbit-0 > .satellite.at-center time, caption and a start/pause button
|
|
8
|
+
*
|
|
9
|
+
* Methods: start(), pause(), toggle(), reset(). Properties: remaining,
|
|
10
|
+
* running, duration. Events (bubbling): start, pause, reset, tick, done.
|
|
11
|
+
*/
|
|
12
|
+
import { OrbitKitElement, define, injectStyleOnce, universe, hub, fixed, escapeHTML as e } from './base.js';
|
|
13
|
+
|
|
14
|
+
const TIMER_CSS = `
|
|
15
|
+
o-timer .ok-track { --o-fill: var(--ok-track, rgba(127,127,127,.18)); --o-stroke: var(--o-fill); }
|
|
16
|
+
o-timer .ok-value { --o-fill: var(--ok-fill, #3da9fc); --o-stroke: var(--o-fill); transition: fill .3s; }
|
|
17
|
+
o-timer.is-warning .ok-value { --o-fill: var(--ok-timer-warn, #f59e0b); }
|
|
18
|
+
o-timer.is-done .ok-value { --o-fill: var(--ok-timer-done, #ef4565); }
|
|
19
|
+
o-timer .ok-timer-tick { background: var(--ok-tick, currentColor); opacity: .35; height: max(1px, .5cqmin); }
|
|
20
|
+
o-timer .ok-timer-tick.is-major { opacity: .7; height: max(1px, .8cqmin); }
|
|
21
|
+
o-timer .ok-timer-hub { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .15em; }
|
|
22
|
+
o-timer .ok-timer-time { font-size: 2em; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; letter-spacing: -.02em; }
|
|
23
|
+
o-timer .ok-timer-label { font-size: .75em; opacity: .7; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
|
+
o-timer .ok-timer-controls { display: flex; gap: .4em; margin-top: .35em; }
|
|
25
|
+
o-timer .ok-timer-btn {
|
|
26
|
+
font: inherit; font-size: .8em; line-height: 1; display: grid; place-items: center;
|
|
27
|
+
width: 2.4em; aspect-ratio: 1; padding: 0; border-radius: 50%; cursor: pointer;
|
|
28
|
+
color: var(--ok-timer-button-text, #0c0f13); background: var(--ok-timer-button, var(--ok-fill, #3da9fc));
|
|
29
|
+
border: none; transition: filter .15s, scale .15s;
|
|
30
|
+
}
|
|
31
|
+
o-timer .ok-timer-btn.is-secondary { color: inherit; background: var(--ok-timer-button-alt, rgba(127,127,127,.22)); }
|
|
32
|
+
o-timer .ok-timer-btn:hover { filter: brightness(1.12); }
|
|
33
|
+
o-timer .ok-timer-btn:active { scale: .94; }
|
|
34
|
+
o-timer .ok-timer-btn svg { width: 1em; height: 1em; fill: currentColor; }
|
|
35
|
+
o-timer .ok-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
36
|
+
@media (prefers-reduced-motion: reduce) { o-timer .ok-value, o-timer .ok-timer-btn { transition: none; } }
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const ICONS = {
|
|
40
|
+
play: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 2.5v11l9-5.5z"/></svg>',
|
|
41
|
+
pause: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 2.5h3v11h-3zM9.5 2.5h3v11h-3z"/></svg>',
|
|
42
|
+
reset: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2.5a5.5 5.5 0 1 1-5.2 3.7l1.4.5A4 4 0 1 0 8 4v2L4.5 3.25 8 .5z"/></svg>'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** "90", "1:30", "1:02:03" or "90s" → seconds. */
|
|
46
|
+
export function parseDuration(value, fallback = 60) {
|
|
47
|
+
const text = String(value ?? '').trim();
|
|
48
|
+
if (/^\d+(?:\.\d+)?s?$/.test(text)) return Number.parseFloat(text);
|
|
49
|
+
if (/^\d+(?::\d{1,2}){1,2}$/.test(text)) return text.split(':').map(Number).reduce((total, part) => total * 60 + part, 0);
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Seconds → "m:ss" or "h:mm:ss", rounded up so 0:00 means done. */
|
|
54
|
+
export function formatTime(seconds) {
|
|
55
|
+
const whole = Math.max(0, Math.ceil(seconds - 1e-6));
|
|
56
|
+
const h = Math.floor(whole / 3600), m = Math.floor(whole / 60) % 60, s = whole % 60;
|
|
57
|
+
return h ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class OrbitTimer extends OrbitKitElement {
|
|
61
|
+
static get observedAttributes() {
|
|
62
|
+
return ['duration', 'remaining', 'running', 'label', 'size', 'thickness', 'fill', 'track', 'shape', 'ticks', 'warn', 'controls', 'glow'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
this._frame = this._frame.bind(this);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
readConfig() {
|
|
71
|
+
const duration = Math.max(.001, parseDuration(this.getAttribute('duration'), 60));
|
|
72
|
+
return {
|
|
73
|
+
duration, size: this._size('size', '200px'), thickness: this._thickness(1.4, 6),
|
|
74
|
+
fill: this._color('fill', null), track: this._color('track', null), shape: this._str('shape', 'rounded'),
|
|
75
|
+
ticks: Math.max(0, Math.min(120, Math.round(this._num('ticks', 0)))),
|
|
76
|
+
warn: Math.max(0, parseDuration(this.getAttribute('warn'), 0)),
|
|
77
|
+
controls: this._str('controls', 'true') !== 'false',
|
|
78
|
+
label: this.getAttribute('label'), glow: this.hasAttribute('glow') ? this._glowPx() : null
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
structuralSignature({ duration, label, fill, track, warn, ...rest }) { return JSON.stringify(rest); }
|
|
83
|
+
|
|
84
|
+
build(cfg) {
|
|
85
|
+
injectStyleOnce('orbit-kit-timer', TIMER_CSS);
|
|
86
|
+
const ring = `--o-size-ratio:${fixed(cfg.thickness)};--o-gap:0`;
|
|
87
|
+
const glow = cfg.glow == null ? '' : ` style="filter:drop-shadow(0 0 ${cfg.glow}px var(--ok-fill, #3da9fc))"`;
|
|
88
|
+
const ticks = cfg.ticks
|
|
89
|
+
? `<div class="orbit-${Math.max(1, Math.floor(12 - cfg.thickness - .3))}" style="--o-size-ratio:.6">${Array.from({ length: cfg.ticks }, (_, i) =>
|
|
90
|
+
`<div class="vector inner-orbit ok-timer-tick${cfg.ticks % 4 === 0 && i % (cfg.ticks / 4) === 0 ? ' is-major' : ''}"></div>`).join('')}</div>` : '';
|
|
91
|
+
const controls = cfg.controls ? `<span class="ok-timer-controls">
|
|
92
|
+
<button type="button" class="ok-timer-btn ok-timer-toggle"></button>
|
|
93
|
+
<button type="button" class="ok-timer-btn is-secondary ok-timer-reset" aria-label="Reset">${ICONS.reset}</button>
|
|
94
|
+
</span>` : '';
|
|
95
|
+
const inner = Math.floor(12 - cfg.thickness - (cfg.ticks ? 1.2 : .4));
|
|
96
|
+
this.innerHTML = universe(cfg.size, `
|
|
97
|
+
<div class="orbit-12" style="${ring}"><o-arc class="inner-orbit ok-track" value="100" shape="${e(cfg.shape)}"></o-arc></div>
|
|
98
|
+
<div class="orbit-12" style="${ring}"><o-progress class="inner-orbit ok-value" value="100" shape="${e(cfg.shape)}"${glow}></o-progress></div>
|
|
99
|
+
${ticks}
|
|
100
|
+
${hub(2 * inner, `<span class="ok-timer-time" aria-hidden="true"></span><span class="ok-timer-label" hidden></span>${controls}<span class="ok-sr" aria-live="polite"></span>`, 'ok-timer-hub')}`);
|
|
101
|
+
this._ring = this.querySelector('o-progress');
|
|
102
|
+
this._time = this.querySelector('.ok-timer-time');
|
|
103
|
+
this._label = this.querySelector('.ok-timer-label');
|
|
104
|
+
this._toggle = this.querySelector('.ok-timer-toggle');
|
|
105
|
+
this._reset = this.querySelector('.ok-timer-reset');
|
|
106
|
+
this._live = this.querySelector('.ok-sr');
|
|
107
|
+
this._toggle?.addEventListener('click', () => this.toggle());
|
|
108
|
+
this._reset?.addEventListener('click', () => this.reset());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
sync(cfg) {
|
|
112
|
+
this._cfg = cfg;
|
|
113
|
+
this._applyTheme({ '--ok-fill': cfg.fill, '--ok-track': cfg.track });
|
|
114
|
+
// A new duration (or a remaining attribute) sets the clock while stopped.
|
|
115
|
+
const remainingAttr = this.getAttribute('remaining');
|
|
116
|
+
if (this._remaining == null || this._duration !== cfg.duration || remainingAttr !== this._remainingAttr) {
|
|
117
|
+
this._duration = cfg.duration;
|
|
118
|
+
this._remainingAttr = remainingAttr;
|
|
119
|
+
const start = remainingAttr == null ? cfg.duration : Math.min(cfg.duration, Math.max(0, parseDuration(remainingAttr, cfg.duration)));
|
|
120
|
+
if (this._ticking) this.remaining = start;
|
|
121
|
+
else this._remaining = start;
|
|
122
|
+
}
|
|
123
|
+
if (this._label) { this._label.textContent = cfg.label ?? ''; this._label.hidden = !cfg.label; }
|
|
124
|
+
const wantRunning = this.hasAttribute('running') && this.getAttribute('running') !== 'false';
|
|
125
|
+
if (wantRunning && !this._ticking && this.isConnected) this.start();
|
|
126
|
+
else if (!wantRunning && this._ticking) this.pause();
|
|
127
|
+
this._paint(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get duration() { return this._duration ?? this.readConfig().duration; }
|
|
131
|
+
set duration(value) { this.setAttribute('duration', String(value)); }
|
|
132
|
+
get remaining() { return Math.max(0, this._remaining ?? this.duration); }
|
|
133
|
+
set remaining(value) {
|
|
134
|
+
this._remaining = Math.min(this.duration, Math.max(0, Number(value) || 0));
|
|
135
|
+
if (this._ticking) this._endAt = performance.now() + this._remaining * 1000;
|
|
136
|
+
this._paint(true);
|
|
137
|
+
}
|
|
138
|
+
get running() { return !!this._ticking; }
|
|
139
|
+
set running(value) { value ? this.start() : this.pause(); }
|
|
140
|
+
|
|
141
|
+
start() {
|
|
142
|
+
if (this._ticking) return;
|
|
143
|
+
if (this.remaining <= 0) this._remaining = this.duration;
|
|
144
|
+
this._ticking = true;
|
|
145
|
+
this._doneSent = false;
|
|
146
|
+
this._endAt = performance.now() + this.remaining * 1000;
|
|
147
|
+
if (this.getAttribute('running') == null || this.getAttribute('running') === 'false') this.setAttribute('running', '');
|
|
148
|
+
this._emit('start');
|
|
149
|
+
this._raf = requestAnimationFrame(this._frame);
|
|
150
|
+
this._paint(true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pause() {
|
|
154
|
+
if (!this._ticking) return;
|
|
155
|
+
this._update();
|
|
156
|
+
this._ticking = false;
|
|
157
|
+
cancelAnimationFrame(this._raf);
|
|
158
|
+
if (this.hasAttribute('running')) this.removeAttribute('running');
|
|
159
|
+
this._emit('pause');
|
|
160
|
+
this._paint(true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
toggle() { this._ticking ? this.pause() : this.start(); }
|
|
164
|
+
|
|
165
|
+
reset() {
|
|
166
|
+
const wasRunning = this._ticking;
|
|
167
|
+
if (wasRunning) { this._ticking = false; cancelAnimationFrame(this._raf); if (this.hasAttribute('running')) this.removeAttribute('running'); }
|
|
168
|
+
this._remaining = this.duration;
|
|
169
|
+
this._doneSent = false;
|
|
170
|
+
if (this._live) this._live.textContent = '';
|
|
171
|
+
this._emit('reset');
|
|
172
|
+
this._paint(true);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onDisconnected() {
|
|
176
|
+
if (this._ticking) { this._update(); this._ticking = false; cancelAnimationFrame(this._raf); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onConnected() {
|
|
180
|
+
if (this.hasAttribute('running') && this.getAttribute('running') !== 'false' && !this._ticking) this.start();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_update() {
|
|
184
|
+
if (this._ticking) this._remaining = Math.max(0, (this._endAt - performance.now()) / 1000);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_frame() {
|
|
188
|
+
if (!this._ticking) return;
|
|
189
|
+
this._update();
|
|
190
|
+
this._paint();
|
|
191
|
+
if (this._remaining <= 0) {
|
|
192
|
+
this._ticking = false;
|
|
193
|
+
if (this.hasAttribute('running')) this.removeAttribute('running');
|
|
194
|
+
this._paint(true);
|
|
195
|
+
if (!this._doneSent) {
|
|
196
|
+
this._doneSent = true;
|
|
197
|
+
if (this._live) this._live.textContent = `${this._cfg?.label ? `${this._cfg.label}: ` : ''}time is up`;
|
|
198
|
+
this._emit('done');
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this._raf = requestAnimationFrame(this._frame);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_emit(type) {
|
|
206
|
+
this.dispatchEvent(new CustomEvent(type, { detail: { remaining: this.remaining, duration: this.duration }, bubbles: true, composed: true }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_paint(force = false) {
|
|
210
|
+
const remaining = this.remaining, duration = this.duration, cfg = this._cfg;
|
|
211
|
+
const pct = remaining / duration * 100;
|
|
212
|
+
// Redraw the ring in steps of a thousandth: smooth, without a new path every frame.
|
|
213
|
+
if (this._ring && (force || Math.abs(pct - (this._drawn ?? -1)) >= .1)) {
|
|
214
|
+
this._drawn = pct;
|
|
215
|
+
this._ring.setAttribute('value', pct.toFixed(2));
|
|
216
|
+
}
|
|
217
|
+
const text = formatTime(remaining);
|
|
218
|
+
if (force || text !== this._shown) {
|
|
219
|
+
const changed = this._shown !== undefined && text !== this._shown;
|
|
220
|
+
this._shown = text;
|
|
221
|
+
if (this._time) this._time.textContent = text;
|
|
222
|
+
this._describe(`${cfg?.label ? `${cfg.label}: ` : ''}${text} left of ${formatTime(duration)}`, 'timer');
|
|
223
|
+
if (changed && this._ticking) this._emit('tick');
|
|
224
|
+
}
|
|
225
|
+
const done = remaining <= 0;
|
|
226
|
+
this.classList.toggle('is-running', !!this._ticking);
|
|
227
|
+
this.classList.toggle('is-done', done);
|
|
228
|
+
this.classList.toggle('is-warning', !done && !!cfg?.warn && remaining <= cfg.warn);
|
|
229
|
+
if (this._toggle) {
|
|
230
|
+
const state = this._ticking ? 'pause' : 'play';
|
|
231
|
+
if (this._toggle.dataset.state !== state) {
|
|
232
|
+
this._toggle.dataset.state = state;
|
|
233
|
+
this._toggle.innerHTML = ICONS[state];
|
|
234
|
+
this._toggle.setAttribute('aria-label', this._ticking ? 'Pause' : (remaining < duration && !done ? 'Resume' : 'Start'));
|
|
235
|
+
} else if (!this._ticking) this._toggle.setAttribute('aria-label', remaining < duration && !done ? 'Resume' : 'Start');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
define('o-timer', OrbitTimer);
|