@zumer/orbit-kit 0.1.0 → 2.0.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.
@@ -0,0 +1,447 @@
1
+ /**
2
+ * <o-time-picker>: pick a time by turning a clock, hours first, then minutes.
3
+ *
4
+ * .ok-tp
5
+ * ├── .ok-tp-head hour and minute fields (role="spinbutton"), AM / PM
6
+ * └── .bigbang (the dial)
7
+ * └── .gravity-spot
8
+ * ├── .orbit-12 face
9
+ * ├── .orbit-10 / .orbit-7 selector: a hand (.vector) and a knob (.satellite),
10
+ * │ turned by one registered angle so they sweep
11
+ * ├── .orbit-10 > .satellite × 12 hours (12h, or 1–12 on a 24h dial)
12
+ * ├── .orbit-7 > .satellite × 12 00 and 13–23, on a 24h dial
13
+ * ├── .orbit-10 > .satellite × 12 minutes
14
+ * └── .orbit-0 pivot
15
+ *
16
+ * Tap or drag the dial; letting go of an hour moves on to the minutes. The
17
+ * fields take arrows, digits, Left / Right, and A / P for the half of the day.
18
+ * The value is "HH:MM" (24-hour), or "" when nothing is picked. It is a form
19
+ * control. Events: `input` on every change, `change` when a gesture or key ends.
20
+ */
21
+ import { OrbitKitElement, FormControl, define, injectStyleOnce, universe, escapeHTML as e, pointerAngle, withShared, FORM_ATTRS } from './base.js';
22
+ import { parseClock, clockValue, clockText, twelveHour } from './o-range.js';
23
+
24
+ const DAY = 1440;
25
+ const pad = (n) => String(n).padStart(2, '0');
26
+ const mod = (v, n) => ((v % n) + n) % n;
27
+
28
+ const TP_CSS = `
29
+ @property --ok-tp-angle { syntax: '<angle>'; inherits: true; initial-value: 0deg; }
30
+ o-time-picker { display: inline-block; vertical-align: middle; -webkit-tap-highlight-color: transparent; }
31
+ o-time-picker .ok-tp { display: flex; flex-direction: column; align-items: center; gap: 5cqw; container-type: inline-size; max-width: 100%; }
32
+ o-time-picker .ok-tp-head { display: flex; align-items: center; justify-content: center; gap: 3cqw; width: 100%; color: var(--ok-text, #e8edf2); }
33
+ o-time-picker .ok-tp-field { display: flex; align-items: center; gap: .5cqw; font-size: 15cqw; line-height: 1; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
34
+ o-time-picker .ok-tp-seg {
35
+ padding: .08em .16em; border-radius: .16em; cursor: pointer; user-select: none; outline: none;
36
+ background: var(--ok-tp-seg, color-mix(in srgb, currentColor 7%, transparent));
37
+ transition: background-color .15s, color .15s;
38
+ }
39
+ o-time-picker .ok-tp-seg.is-active { color: var(--ok-fill, #4fd1c5); background: color-mix(in srgb, var(--ok-fill, #4fd1c5) 16%, transparent); }
40
+ o-time-picker .ok-tp-seg:focus-visible { box-shadow: 0 0 0 2px var(--ok-focus, var(--ok-fill, #4fd1c5)); }
41
+ o-time-picker .ok-tp-colon { opacity: .55; padding-bottom: .08em; }
42
+ o-time-picker .ok-tp-period { display: flex; flex-direction: column; gap: 1.2cqw; }
43
+ o-time-picker .ok-tp-period button {
44
+ font: inherit; font-size: 4.6cqw; font-weight: 600; line-height: 1; padding: .45em .7em; margin: 0; min-width: 0;
45
+ border-radius: .5em; border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
46
+ background: none; color: inherit; opacity: .6; cursor: pointer; transition: background-color .15s, color .15s, opacity .15s;
47
+ }
48
+ o-time-picker .ok-tp-period button[aria-pressed="true"] { opacity: 1; color: var(--ok-fill, #4fd1c5); border-color: color-mix(in srgb, var(--ok-fill, #4fd1c5) 45%, transparent); background: color-mix(in srgb, var(--ok-fill, #4fd1c5) 14%, transparent); }
49
+ o-time-picker .ok-tp-period button:disabled { opacity: .25; cursor: default; }
50
+ o-time-picker .ok-tp-period button:focus-visible { outline: 2px solid var(--ok-focus, var(--ok-fill, #4fd1c5)); outline-offset: 2px; border-radius: .5em; }
51
+ o-time-picker .bigbang { touch-action: none; cursor: pointer; }
52
+ o-time-picker .ok-tp-face {
53
+ background: var(--ok-time-face, var(--ok-face, #161b22));
54
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok-text, #e8edf2) 9%, transparent), 0 10px 28px -12px rgba(0,0,0,.45);
55
+ }
56
+ o-time-picker .ok-tp-sel { transition: --ok-tp-angle .22s cubic-bezier(.2,.8,.2,1), opacity .15s; }
57
+ o-time-picker.is-dragging .ok-tp-sel { transition-duration: .06s; }
58
+ o-time-picker .ok-tp-sel > .satellite, o-time-picker .ok-tp-sel > .vector { --o-angle-composite: calc(var(--ok-tp-angle) - 90deg); }
59
+ o-time-picker .ok-tp-hand { background: var(--ok-fill, #4fd1c5); height: max(2px, .6cqmin); }
60
+ o-time-picker .ok-tp-knob { background: var(--ok-fill, #4fd1c5); }
61
+ o-time-picker .ok-tp-knob.is-between::after { content: ""; width: 18%; aspect-ratio: 1; border-radius: 50%; background: var(--ok-on-fill, var(--ok-timer-button-text, #0c0f13)); }
62
+ o-time-picker .ok-tp-sel:not(.is-on) { opacity: 0; }
63
+ o-time-picker .ok-tp-ring { transition: opacity .2s, scale .25s cubic-bezier(.2,.8,.2,1), visibility .2s; }
64
+ o-time-picker .ok-tp-ring .satellite { pointer-events: none; }
65
+ o-time-picker .ok-tp-ring:not(.is-on) { opacity: 0; scale: 1.08; visibility: hidden; }
66
+ o-time-picker .ok-tp-ring.is-inner:not(.is-on) { scale: .9; }
67
+ o-time-picker .ok-tp-num { font-size: .78em; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; color: var(--ok-text, #e8edf2); transition: color .15s; }
68
+ o-time-picker .is-inner .ok-tp-num { font-size: .62em; opacity: .7; }
69
+ o-time-picker .ok-tp-num.is-selected { color: var(--ok-on-fill, var(--ok-timer-button-text, #0c0f13)); opacity: 1; font-weight: 650; }
70
+ o-time-picker .ok-tp-num.is-off { opacity: .22; }
71
+ o-time-picker .ok-tp-pivot { background: var(--ok-fill, #4fd1c5); }
72
+ o-time-picker[data-disabled] .ok-tp { opacity: .5; }
73
+ o-time-picker[data-disabled] .bigbang, o-time-picker[data-disabled] .ok-tp-seg { cursor: default; }
74
+ @media (prefers-reduced-motion: reduce) { o-time-picker .ok-tp-sel, o-time-picker .ok-tp-ring, o-time-picker .ok-tp-seg { transition: none; } }
75
+ `;
76
+
77
+ export class OrbitTimePicker extends FormControl(OrbitKitElement) {
78
+ static get observedAttributes() {
79
+ return withShared(['value', 'hours', 'minute-step', 'min', 'max', 'view', 'size', 'fill', 'label', 'locale'], FORM_ATTRS);
80
+ }
81
+
82
+ constructor() {
83
+ super();
84
+ for (const name of ['_onDown', '_onMove', '_onUp', '_onKey', '_onFocus', '_onClick']) this[name] = this[name].bind(this);
85
+ this._angles = {};
86
+ }
87
+
88
+ readConfig() {
89
+ const step = Math.round(this._num('minute-step', 1));
90
+ const minuteStep = step >= 1 && step <= 30 ? step : 1;
91
+ const twelve = this.hasAttribute('hours') ? this.getAttribute('hours') === '12' : twelveHour(this._locale());
92
+ const cfg = {
93
+ twelve, minuteStep,
94
+ min: parseClock(this.getAttribute('min')), max: parseClock(this.getAttribute('max')),
95
+ view: this.getAttribute('view') === 'minutes' ? 'minutes' : 'hours',
96
+ size: this._size('size', '280px'), fill: this._color('fill', null),
97
+ label: this.getAttribute('label'), disabled: this._isDisabled()
98
+ };
99
+ const raw = parseClock(this.getAttribute('value'));
100
+ cfg.value = raw == null ? null : this._fit(raw, cfg);
101
+ return cfg;
102
+ }
103
+
104
+ /** Inside min / max. A min after max is a window across midnight (22:00 to 06:00). */
105
+ _allowed(t, { min, max }) {
106
+ if (min == null && max == null) return true;
107
+ const lo = min ?? 0, hi = max ?? DAY - 1;
108
+ return lo <= hi ? t >= lo && t <= hi : t >= lo || t <= hi;
109
+ }
110
+
111
+ /** Snap the minutes to minute-step, then move into min / max (the closer end). */
112
+ _fit(t, cfg) {
113
+ const h = Math.floor(t / 60), last = Math.floor(59 / cfg.minuteStep) * cfg.minuteStep;
114
+ t = h * 60 + Math.min(last, Math.round((t % 60) / cfg.minuteStep) * cfg.minuteStep);
115
+ if (this._allowed(t, cfg)) return t;
116
+ const lo = cfg.min ?? 0, hi = cfg.max ?? DAY - 1;
117
+ const dist = (a) => Math.min(mod(a - t, DAY), mod(t - a, DAY));
118
+ return dist(lo) <= dist(hi) ? lo : hi;
119
+ }
120
+
121
+ /** The allowed minute of hour h closest to minute m, or null when the whole hour is out. */
122
+ _inHour(h, m, cfg) {
123
+ let best = null;
124
+ for (let k = 0; k < 60; k += cfg.minuteStep) {
125
+ if (this._allowed(h * 60 + k, cfg) && (best == null || Math.abs(k - m) < Math.abs(best - m))) best = k;
126
+ }
127
+ return best == null ? null : h * 60 + best;
128
+ }
129
+
130
+ structuralSignature({ twelve, size, minuteStep }) { return JSON.stringify({ twelve, size, minuteStep }); }
131
+
132
+ build(cfg) {
133
+ injectStyleOnce('orbit-kit-time-picker', TP_CSS);
134
+ const ring = (cls, orbit, ratio, labels) => `<div class="orbit-${orbit} ok-tp-ring ${cls}" style="--o-size-ratio:${ratio}">${labels.map((text, i) =>
135
+ `<div class="satellite" data-i="${i}"><div class="capsule ok-tp-num">${e(text)}</div></div>`).join('')}</div>`;
136
+ const twelveLabels = Array.from({ length: 12 }, (_, i) => String(i || 12));
137
+ const sel = (cls, orbit, knob) => `<div class="orbit-${orbit} ok-tp-sel ${cls}"><div class="vector inner-orbit ok-tp-hand" style="--o-size-ratio:${orbit}"></div><div class="satellite ok-tp-knob" style="--o-size-ratio:${knob}"></div></div>`;
138
+ const dial = universe('100%', `
139
+ <div class="orbit-12 ok-tp-face ok-surface"></div>
140
+ ${sel('is-outer', 10, 3)}
141
+ ${twelve(cfg) ? '' : sel('is-inner', 7, 2.5)}
142
+ ${ring('ok-tp-hours', 10, 3, twelveLabels)}
143
+ ${twelve(cfg) ? '' : ring('ok-tp-hours-inner is-inner', 7, 2.5, Array.from({ length: 12 }, (_, i) => (i ? String(i + 12) : '00')))}
144
+ ${ring('ok-tp-minutes', 10, 3, Array.from({ length: 12 }, (_, i) => pad(i * 5)))}
145
+ <div class="orbit-0"><div class="satellite at-center ok-tp-pivot" style="--o-size-ratio:.7"></div></div>`);
146
+ const period = cfg.twelve ? `<span class="ok-tp-period" role="group" aria-label="AM or PM">
147
+ <button type="button" data-period="am" aria-pressed="false">AM</button><button type="button" data-period="pm" aria-pressed="false">PM</button></span>` : '';
148
+ this.innerHTML = `<div class="ok-tp" style="width:${e(cfg.size)}">
149
+ <div class="ok-tp-head">
150
+ <span class="ok-tp-field">
151
+ <span class="ok-tp-seg" data-seg="hours" role="spinbutton" tabindex="0" aria-label="Hours"></span><span class="ok-tp-colon" aria-hidden="true">:</span><span class="ok-tp-seg" data-seg="minutes" role="spinbutton" tabindex="0" aria-label="Minutes"></span>
152
+ </span>${period}
153
+ </div>
154
+ ${dial.replace('<div class="bigbang"', '<div class="bigbang" aria-hidden="true"')}
155
+ </div>`;
156
+ this._box = this.querySelector('.bigbang');
157
+ this._segs = { hours: this.querySelector('[data-seg="hours"]'), minutes: this.querySelector('[data-seg="minutes"]') };
158
+ this._periods = [...this.querySelectorAll('[data-period]')];
159
+ this._sels = { outer: this.querySelector('.ok-tp-sel.is-outer'), inner: this.querySelector('.ok-tp-sel.is-inner') };
160
+ this._rings = { hours: this.querySelector('.ok-tp-hours'), inner: this.querySelector('.ok-tp-hours-inner'), minutes: this.querySelector('.ok-tp-minutes') };
161
+ this._angles = {};
162
+ for (const seg of Object.values(this._segs)) seg.addEventListener('focus', this._onFocus);
163
+ this._box.addEventListener('pointerdown', this._onDown);
164
+ for (const button of this._periods) button.addEventListener('click', this._onClick);
165
+ for (const seg of Object.values(this._segs)) seg.addEventListener('click', this._onFocus);
166
+ }
167
+
168
+ sync(cfg) {
169
+ this._cfg = cfg;
170
+ this._applyTheme({ '--ok-fill': cfg.fill });
171
+ const t = cfg.value, has = t != null;
172
+ const h = has ? Math.floor(t / 60) : null, m = has ? t % 60 : null;
173
+ const pm = has ? h >= 12 : this._pm === true;
174
+ const view = cfg.view;
175
+
176
+ // Fields.
177
+ const shownHour = has ? (cfg.twelve ? h % 12 || 12 : h) : null;
178
+ const hs = this._segs.hours, ms = this._segs.minutes;
179
+ hs.textContent = has ? pad(shownHour) : '--';
180
+ ms.textContent = has ? pad(m) : '--';
181
+ hs.setAttribute('aria-valuemin', cfg.twelve ? '1' : '0');
182
+ hs.setAttribute('aria-valuemax', cfg.twelve ? '12' : '23');
183
+ ms.setAttribute('aria-valuemin', '0');
184
+ ms.setAttribute('aria-valuemax', '59');
185
+ for (const [seg, now, text] of [[hs, shownHour, has ? (cfg.twelve ? `${shownHour} ${pm ? 'PM' : 'AM'}` : String(h)) : 'No value'], [ms, m, has ? pad(m) : 'No value']]) {
186
+ if (now == null) seg.removeAttribute('aria-valuenow'); else seg.setAttribute('aria-valuenow', String(now));
187
+ seg.setAttribute('aria-valuetext', text);
188
+ seg.tabIndex = cfg.disabled ? -1 : 0;
189
+ if (cfg.disabled) seg.setAttribute('aria-disabled', 'true'); else seg.removeAttribute('aria-disabled');
190
+ }
191
+ hs.classList.toggle('is-active', view === 'hours');
192
+ ms.classList.toggle('is-active', view === 'minutes');
193
+ for (const button of this._periods) {
194
+ const isPm = button.dataset.period === 'pm';
195
+ button.setAttribute('aria-pressed', String(isPm === pm));
196
+ const any = Array.from({ length: 12 }, (_, k) => this._inHour(k + (isPm ? 12 : 0), 0, cfg)).some((v) => v != null);
197
+ button.disabled = cfg.disabled || !any;
198
+ }
199
+ this._describe(cfg.label ? `${cfg.label}: ${has ? clockText(t, cfg.twelve) : 'no time'}` : (has ? `Time, ${clockText(t, cfg.twelve)}` : 'Time'), 'group');
200
+
201
+ // Dial: which ring shows, which numbers are out of reach.
202
+ const inner = view === 'hours' && !cfg.twelve && has && (h === 0 || h > 12);
203
+ this._rings.hours.classList.toggle('is-on', view === 'hours');
204
+ this._rings.inner?.classList.toggle('is-on', view === 'hours');
205
+ this._rings.minutes.classList.toggle('is-on', view === 'minutes');
206
+ const half = pm ? 12 : 0;
207
+ const outerHour = (i) => (cfg.twelve ? (i % 12) + half : i || 12);
208
+ this._rings.hours.querySelectorAll('.ok-tp-num').forEach((num, i) => {
209
+ num.classList.toggle('is-off', this._inHour(outerHour(i), 0, cfg) == null);
210
+ num.classList.toggle('is-selected', has && view === 'hours' && !inner && outerHour(i) === h);
211
+ });
212
+ this._rings.inner?.querySelectorAll('.ok-tp-num').forEach((num, i) => {
213
+ const hour = i ? i + 12 : 0;
214
+ num.classList.toggle('is-off', this._inHour(hour, 0, cfg) == null);
215
+ num.classList.toggle('is-selected', has && inner && hour === h);
216
+ });
217
+ this._rings.minutes.querySelectorAll('.ok-tp-num').forEach((num, i) => {
218
+ const k = i * 5;
219
+ num.classList.toggle('is-off', k % cfg.minuteStep !== 0 || (has && !this._allowed(h * 60 + k, cfg)));
220
+ num.classList.toggle('is-selected', has && view === 'minutes' && m === k);
221
+ });
222
+
223
+ // The selector sweeps to its angle by the short way round.
224
+ const deg = view === 'hours' ? (has ? (h % 12) * 30 : 0) : (has ? m * 6 : 0);
225
+ for (const [key, el] of Object.entries(this._sels)) {
226
+ if (!el) continue;
227
+ const on = has && (key === 'inner' ? inner : !inner);
228
+ el.classList.toggle('is-on', on);
229
+ const prev = this._angles[key] ?? deg;
230
+ const next = deg + 360 * Math.round((prev - deg) / 360);
231
+ this._angles[key] = next;
232
+ el.style.setProperty('--ok-tp-angle', `${next}deg`);
233
+ el.querySelector('.ok-tp-knob').classList.toggle('is-between', view === 'minutes' && has && m % 5 !== 0);
234
+ }
235
+ if (cfg.disabled) this._stopDrag();
236
+ }
237
+
238
+ /* ---- value ----------------------------------------------------------- */
239
+
240
+ /** "HH:MM" (24-hour), or "" when no time is picked. Setting it fires no event. */
241
+ get value() { const v = this.readConfig().value; return v == null ? '' : clockValue(v); }
242
+ set value(value) {
243
+ const t = parseClock(value);
244
+ if (t == null && value !== '' && value != null) return;
245
+ this._reflectValue(t == null ? null : clockValue(this._fit(t, this.readConfig())));
246
+ if (this.isConnected) this._render();
247
+ }
248
+ /** Minutes after midnight, or NaN. */
249
+ get valueAsNumber() { const v = this.readConfig().value; return v == null ? NaN : v; }
250
+ get view() { return this.readConfig().view; }
251
+ set view(value) { this._setView(value === 'minutes' ? 'minutes' : 'hours'); }
252
+ get min() { return this.getAttribute('min') ?? ''; }
253
+ set min(value) { this.setAttribute('min', String(value)); }
254
+ get max() { return this.getAttribute('max') ?? ''; }
255
+ set max(value) { this.setAttribute('max', String(value)); }
256
+
257
+ _formValidity() {
258
+ return this.hasAttribute('required') && this.readConfig().value == null
259
+ ? { flags: { valueMissing: true }, message: 'Please pick a time.' } : {};
260
+ }
261
+ _formAnchor() { return this._segs?.hours; }
262
+ _onFormReset() { this._pm = undefined; }
263
+
264
+ _setView(view) {
265
+ if (this.getAttribute('view') !== view) this.setAttribute('view', view);
266
+ }
267
+
268
+ /** Apply a time the user picked; `input`, and `change` when asked. */
269
+ _commit(t, { change = false } = {}) {
270
+ const cfg = this.readConfig();
271
+ const before = cfg.value;
272
+ const next = t == null ? null : this._fit(mod(t, DAY), cfg);
273
+ if (next === before) return false;
274
+ this._reflectValue(next == null ? null : clockValue(next));
275
+ this._render();
276
+ this._emitValue('input', this.value);
277
+ if (change) this._emitValue('change', this.value);
278
+ return true;
279
+ }
280
+
281
+ /* ---- pointer ----------------------------------------------------------- */
282
+
283
+ /** The time under the pointer in the current view, or null for a number out of reach. */
284
+ _timeAt(event, cfg) {
285
+ const angle = pointerAngle(event, this._box);
286
+ const t = cfg.value, has = t != null;
287
+ if (cfg.view === 'minutes') {
288
+ const h = has ? Math.floor(t / 60) : 0;
289
+ let m = Math.round(angle / 6) % 60;
290
+ m = Math.round(m / cfg.minuteStep) * cfg.minuteStep;
291
+ if (m >= 60) m = 0;
292
+ const next = h * 60 + m;
293
+ return this._allowed(next, cfg) ? next : null;
294
+ }
295
+ const i = Math.round(angle / 30) % 12;
296
+ let h;
297
+ if (cfg.twelve) h = i + ((has ? t >= 720 : this._pm) ? 12 : 0);
298
+ else {
299
+ const r = this._box.getBoundingClientRect();
300
+ const lanes = Math.hypot(event.clientX - (r.left + r.width / 2), event.clientY - (r.top + r.height / 2)) / (r.width / 24);
301
+ h = lanes < 8.5 ? (i ? i + 12 : 0) : i || 12;
302
+ }
303
+ return this._inHour(h, has ? t % 60 : 0, cfg);
304
+ }
305
+
306
+ _onDown(event) {
307
+ if (this._isDisabled() || event.button !== 0 || this._drag) return;
308
+ event.preventDefault();
309
+ const cfg = this.readConfig();
310
+ this._segs[cfg.view]?.focus({ preventScroll: true });
311
+ this._drag = { id: event.pointerId, before: cfg.value, view: cfg.view };
312
+ this.classList.add('is-dragging');
313
+ this._box.setPointerCapture?.(event.pointerId);
314
+ const t = this._timeAt(event, cfg);
315
+ if (t != null) this._commit(t);
316
+ window.addEventListener('pointermove', this._onMove);
317
+ window.addEventListener('pointerup', this._onUp);
318
+ window.addEventListener('pointercancel', this._onUp);
319
+ }
320
+
321
+ _onMove(event) {
322
+ if (!this._drag || event.pointerId !== this._drag.id || this._isDisabled()) return;
323
+ const t = this._timeAt(event, this.readConfig());
324
+ if (t != null) this._commit(t);
325
+ }
326
+
327
+ _stopDrag() {
328
+ if (typeof window === 'undefined') return;
329
+ if (this._drag && this._box?.hasPointerCapture?.(this._drag.id)) this._box.releasePointerCapture(this._drag.id);
330
+ this._drag = null;
331
+ this.classList?.remove('is-dragging');
332
+ window.removeEventListener('pointermove', this._onMove);
333
+ window.removeEventListener('pointerup', this._onUp);
334
+ window.removeEventListener('pointercancel', this._onUp);
335
+ }
336
+
337
+ _onUp(event) {
338
+ const drag = this._drag;
339
+ if (!drag || event.pointerId !== drag.id) return;
340
+ this._stopDrag();
341
+ const cfg = this.readConfig();
342
+ if (cfg.value !== drag.before && !this._isDisabled()) this._emitValue('change', this.value);
343
+ // Letting go of an hour moves on to the minutes, like a clock picker.
344
+ if (drag.view === 'hours' && event.type === 'pointerup' && cfg.value != null) {
345
+ this._setView('minutes');
346
+ this._segs.minutes?.focus({ preventScroll: true });
347
+ }
348
+ }
349
+
350
+ /* ---- fields and keys ----------------------------------------------------- */
351
+
352
+ onConnected() { this.addEventListener('keydown', this._onKey); }
353
+ onDisconnected() { this.removeEventListener('keydown', this._onKey); this._stopDrag(); }
354
+
355
+ _onFocus(event) {
356
+ const seg = event.currentTarget?.dataset?.seg;
357
+ if (seg && !this._isDisabled()) this._setView(seg);
358
+ }
359
+
360
+ _onClick(event) {
361
+ const pm = event.currentTarget.dataset.period === 'pm';
362
+ this._setPeriod(pm);
363
+ }
364
+
365
+ _setPeriod(pm) {
366
+ const cfg = this.readConfig();
367
+ this._pm = pm;
368
+ if (cfg.value == null) { this._render(); return; }
369
+ const t = (cfg.value % 720) + (pm ? 720 : 0);
370
+ const h = Math.floor(t / 60);
371
+ const next = this._inHour(h, t % 60, cfg) ?? this._fit(t, cfg);
372
+ if (!this._commit(next, { change: true })) this._render();
373
+ }
374
+
375
+ _onKey(event) {
376
+ if (this._isDisabled()) return;
377
+ const seg = event.target?.closest?.('[data-seg]')?.dataset.seg;
378
+ const cfg = this.readConfig();
379
+ const key = event.key;
380
+ if (cfg.twelve && (key === 'a' || key === 'A' || key === 'p' || key === 'P') && (seg || event.target?.closest?.('[data-period]'))) {
381
+ event.preventDefault();
382
+ this._setPeriod(key.toLowerCase() === 'p');
383
+ return;
384
+ }
385
+ if (!seg) return;
386
+ if (key === 'ArrowLeft' || key === 'ArrowRight') {
387
+ const order = [this._segs.hours, this._segs.minutes, ...this._periods.filter((b) => !b.disabled)];
388
+ const at = order.indexOf(event.target) + (key === 'ArrowRight' ? 1 : -1);
389
+ if (order[at]) { event.preventDefault(); order[at].focus(); }
390
+ return;
391
+ }
392
+ if (/^\d$/.test(key)) { event.preventDefault(); this._type(seg, Number(key), cfg); return; }
393
+ if (key === 'Backspace' || key === 'Delete') {
394
+ if (!this.hasAttribute('required')) { event.preventDefault(); this._commit(null, { change: true }); }
395
+ return;
396
+ }
397
+ const dir = { ArrowUp: 1, ArrowDown: -1, PageUp: 1, PageDown: -1 }[key];
398
+ if (!dir && key !== 'Home' && key !== 'End') return;
399
+ event.preventDefault();
400
+ if (cfg.value == null) {
401
+ // The first press picks a time, like a native time field.
402
+ const first = this._pm ? 720 : 0;
403
+ this._commit(this._allowed(first, cfg) ? first : this._fit(first, cfg), { change: true });
404
+ return;
405
+ }
406
+ const t = cfg.value, h = Math.floor(t / 60), m = t % 60;
407
+ if (key === 'Home' || key === 'End') {
408
+ const to = key === 'Home' ? (cfg.min ?? 0) : (cfg.max ?? DAY - cfg.minuteStep);
409
+ this._commit(to, { change: true });
410
+ return;
411
+ }
412
+ const big = key.startsWith('Page');
413
+ // Hours wrap round the day, minutes round the hour; out-of-range values are skipped.
414
+ for (let n = 1; n <= (seg === 'hours' ? 24 : 60); n++) {
415
+ const next = seg === 'hours'
416
+ ? this._inHour(mod(h + dir * n * (big ? 3 : 1), 24), m, cfg)
417
+ : h * 60 + mod(m + dir * n * cfg.minuteStep * (big ? Math.max(1, Math.round(10 / cfg.minuteStep)) : 1), 60);
418
+ if (next != null && this._allowed(next, cfg)) { this._commit(next, { change: true }); return; }
419
+ }
420
+ }
421
+
422
+ /** Digits typed into a field: one, or two within a second. */
423
+ _type(seg, d, cfg) {
424
+ const now = Date.now();
425
+ const buf = this._typed && this._typed.seg === seg && now - this._typed.at < 1000 ? this._typed.text + d : String(d);
426
+ const n = Number(buf);
427
+ const t = cfg.value ?? (this._pm ? 720 : 0);
428
+ let h = Math.floor(t / 60), m = t % 60, done = buf.length === 2;
429
+ if (seg === 'hours') {
430
+ const pm = h >= 12;
431
+ if (cfg.twelve) {
432
+ if (n < 1 && buf.length === 1) { this._typed = { seg, text: buf, at: now }; return; }
433
+ h = (Math.min(12, n || 12) % 12) + (pm ? 12 : 0);
434
+ done ||= n > 1;
435
+ } else { h = Math.min(23, n); done ||= n > 2; }
436
+ } else { m = Math.min(59, n); done ||= n > 5; }
437
+ this._typed = done ? null : { seg, text: buf, at: now };
438
+ const next = h * 60 + m;
439
+ this._commit(this._allowed(next, cfg) ? next : this._fit(next, cfg), { change: true });
440
+ if (done && seg === 'hours') this._segs.minutes?.focus();
441
+ }
442
+ }
443
+
444
+ /** The 12-hour dial has no inner ring. */
445
+ function twelve(cfg) { return cfg.twelve; }
446
+
447
+ define('o-time-picker', OrbitTimePicker);