@dorsk/tsumikit 0.71.0 → 0.73.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 +54 -5
- package/dist/autoresize.js +13 -0
- package/dist/clipboard.js +12 -0
- package/dist/components/atoms/Artwork.svelte +4 -2
- package/dist/components/atoms/Avatar.svelte +4 -3
- package/dist/components/atoms/Badge.svelte +13 -0
- package/dist/components/atoms/Button.svelte +25 -9
- package/dist/components/atoms/Card.svelte +2 -0
- package/dist/components/atoms/Dot.svelte +1 -1
- package/dist/components/atoms/Icon.svelte +7 -0
- package/dist/components/atoms/Icon.svelte.d.ts +7 -0
- package/dist/components/atoms/Kbd.svelte +28 -16
- package/dist/components/atoms/Link.svelte +6 -4
- package/dist/components/atoms/Scrim.svelte +18 -1
- package/dist/components/atoms/Slider.svelte +11 -1
- package/dist/components/atoms/Swatch.svelte +12 -0
- package/dist/components/atoms/TimeDisplay.svelte +98 -0
- package/dist/components/atoms/TimeDisplay.svelte.d.ts +24 -0
- package/dist/components/layouts/AppShell.svelte +72 -40
- package/dist/components/layouts/Container.svelte +25 -3
- package/dist/components/layouts/MasterDetail.svelte +59 -33
- package/dist/components/layouts/MasterDetail.svelte.d.ts +1 -1
- package/dist/components/layouts/NavItem.svelte +8 -4
- package/dist/components/layouts/NavSection.svelte +8 -2
- package/dist/components/layouts/ResizablePanel.svelte +19 -4
- package/dist/components/layouts/TabBar.svelte +217 -0
- package/dist/components/layouts/TabBar.svelte.d.ts +27 -0
- package/dist/components/layouts/Toolbar.svelte +5 -0
- package/dist/components/layouts/toolbar-roving.d.ts +16 -0
- package/dist/components/layouts/toolbar-roving.js +47 -1
- package/dist/components/molecules/Accordion.svelte +2 -1
- package/dist/components/molecules/AttachmentList.svelte +1 -0
- package/dist/components/molecules/Breadcrumb.svelte +7 -6
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -1
- package/dist/components/molecules/CapBar.svelte +3 -1
- package/dist/components/molecules/Composer.svelte +27 -5
- package/dist/components/molecules/Drawer.svelte +10 -1
- package/dist/components/molecules/Dropzone.svelte +2 -1
- package/dist/components/molecules/FilterInput.svelte +17 -1
- package/dist/components/molecules/FilterInput.svelte.d.ts +2 -0
- package/dist/components/molecules/Metric.svelte +28 -3
- package/dist/components/molecules/Modal.svelte +20 -4
- package/dist/components/molecules/Popover.svelte +7 -3
- package/dist/components/molecules/PullToRefresh.svelte +205 -0
- package/dist/components/molecules/PullToRefresh.svelte.d.ts +18 -0
- package/dist/components/molecules/SectionHeader.svelte +29 -14
- package/dist/components/molecules/SeekSlider.svelte +370 -0
- package/dist/components/molecules/SeekSlider.svelte.d.ts +37 -0
- package/dist/components/molecules/SegmentedControl.svelte +10 -6
- package/dist/components/molecules/SelectButton.svelte +33 -22
- package/dist/components/molecules/SwipeRow.svelte +349 -0
- package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
- package/dist/components/molecules/Tabs.svelte +11 -5
- package/dist/components/molecules/Timestamp.svelte +1 -1
- package/dist/components/molecules/Toaster.svelte +70 -13
- package/dist/components/molecules/Tooltip.svelte +19 -10
- package/dist/components/molecules/VolumeSlider.svelte +367 -0
- package/dist/components/molecules/VolumeSlider.svelte.d.ts +30 -0
- package/dist/components/molecules/combobox-keyboard.js +2 -1
- package/dist/components/organisms/ConversationFeed.svelte +22 -8
- package/dist/components/organisms/ConversationFeed.svelte.d.ts +2 -0
- package/dist/components/organisms/DataTable.svelte +22 -5
- package/dist/components/organisms/DataTable.svelte.d.ts +4 -2
- package/dist/components/organisms/FilterSearchBar.svelte +4 -0
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/markdown.js +15 -7
- package/dist/media-time.d.ts +6 -0
- package/dist/media-time.js +25 -0
- package/dist/query/edit.js +1 -16
- package/dist/query/parser.js +68 -28
- package/dist/query/query.js +9 -5
- package/dist/query/quotes.d.ts +24 -0
- package/dist/query/quotes.js +78 -0
- package/dist/query/single.js +1 -3
- package/dist/query/suggest.js +25 -32
- package/dist/stores/fontscale.svelte.js +3 -2
- package/dist/stores/storage.d.ts +2 -0
- package/dist/stores/storage.js +16 -0
- package/dist/stores/theme.svelte.js +4 -7
- package/dist/styles/reset.css +30 -0
- package/dist/styles/syntax.css +0 -29
- package/package.json +1 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Media scrubber. Dragging previews (`oninput`) without seeking; the player
|
|
4
|
+
* seeks only on `onchange`, fired once per release or keyboard step. The
|
|
5
|
+
* hit slab is tall enough for touch while the visible track stays thin.
|
|
6
|
+
*/
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
9
|
+
import { describeMediaTime, formatMediaTime, isLiveDuration } from '../../media-time';
|
|
10
|
+
|
|
11
|
+
type Own = {
|
|
12
|
+
/** Position in seconds. Updated only on commit, never while dragging. */
|
|
13
|
+
value?: number;
|
|
14
|
+
/** Duration in seconds. `null`/`Infinity`/`0` = live or unknown: the track is disabled. */
|
|
15
|
+
max?: number | null;
|
|
16
|
+
/** Buffered `[start, end]` ranges in seconds, drawn as a muted fill. */
|
|
17
|
+
buffered?: [number, number][];
|
|
18
|
+
/** Seconds per arrow key. */
|
|
19
|
+
step?: number;
|
|
20
|
+
/** Fraction of the duration per PageUp/PageDown. */
|
|
21
|
+
bigStep?: number;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
size?: 'sm' | 'md';
|
|
24
|
+
label?: string;
|
|
25
|
+
formatTime?: (seconds: number, ref?: number) => string;
|
|
26
|
+
/** Fired on every pointer move while scrubbing with the previewed time. */
|
|
27
|
+
oninput?: (seconds: number) => void;
|
|
28
|
+
/** Fired once on release or per keyboard step with the committed time. */
|
|
29
|
+
onchange?: (seconds: number) => void;
|
|
30
|
+
onscrubstart?: () => void;
|
|
31
|
+
onscrubend?: () => void;
|
|
32
|
+
/** Rendered above the time in the hover/drag tooltip (thumbnail). */
|
|
33
|
+
previewSnippet?: Snippet<[number]>;
|
|
34
|
+
class?: string;
|
|
35
|
+
style?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
value = $bindable(0),
|
|
40
|
+
max = null,
|
|
41
|
+
buffered = [],
|
|
42
|
+
step = 5,
|
|
43
|
+
bigStep = 0.1,
|
|
44
|
+
disabled = false,
|
|
45
|
+
size = 'md',
|
|
46
|
+
label = 'Seek',
|
|
47
|
+
formatTime = formatMediaTime,
|
|
48
|
+
oninput,
|
|
49
|
+
onchange,
|
|
50
|
+
onscrubstart,
|
|
51
|
+
onscrubend,
|
|
52
|
+
previewSnippet,
|
|
53
|
+
class: klass = '',
|
|
54
|
+
style: styleProp,
|
|
55
|
+
...rest
|
|
56
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
57
|
+
|
|
58
|
+
let dragging = $state(false);
|
|
59
|
+
let preview = $state(0);
|
|
60
|
+
let hoverFrac = $state<number | null>(null);
|
|
61
|
+
let pointerId: number | null = null;
|
|
62
|
+
|
|
63
|
+
const live = $derived(isLiveDuration(max));
|
|
64
|
+
const duration = $derived(live ? 0 : (max as number));
|
|
65
|
+
const inert = $derived(disabled || live);
|
|
66
|
+
const clamp = (v: number) => (live ? Math.max(0, v) : Math.min(duration, Math.max(0, v)));
|
|
67
|
+
const shown = $derived(clamp(dragging ? preview : value));
|
|
68
|
+
const frac = (v: number) => (duration > 0 ? clamp(v) / duration : 0);
|
|
69
|
+
const pct = $derived(frac(shown) * 100);
|
|
70
|
+
const tipValue = $derived(dragging ? shown : hoverFrac === null ? null : hoverFrac * duration);
|
|
71
|
+
const tipPct = $derived(tipValue === null ? 0 : frac(tipValue) * 100);
|
|
72
|
+
const ranges = $derived(
|
|
73
|
+
live
|
|
74
|
+
? []
|
|
75
|
+
: buffered
|
|
76
|
+
.map(([a, b]) => [frac(a), frac(b)] as const)
|
|
77
|
+
.filter(([a, b]) => b > a)
|
|
78
|
+
);
|
|
79
|
+
const valueText = $derived(describeMediaTime(shown, live ? null : duration, formatTime));
|
|
80
|
+
|
|
81
|
+
function fracAt(el: HTMLElement, clientX: number) {
|
|
82
|
+
const r = el.getBoundingClientRect();
|
|
83
|
+
if (!r.width) return 0;
|
|
84
|
+
let f = (clientX - r.left) / r.width;
|
|
85
|
+
if (getComputedStyle(el).direction === 'rtl') f = 1 - f;
|
|
86
|
+
return Math.min(1, Math.max(0, f));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function commit(v: number) {
|
|
90
|
+
const next = clamp(v);
|
|
91
|
+
value = next;
|
|
92
|
+
onchange?.(next);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function endDrag(el: HTMLElement, fire: boolean) {
|
|
96
|
+
if (pointerId !== null && typeof el.releasePointerCapture === 'function') {
|
|
97
|
+
try {
|
|
98
|
+
el.releasePointerCapture(pointerId);
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
pointerId = null;
|
|
102
|
+
dragging = false;
|
|
103
|
+
onscrubend?.();
|
|
104
|
+
if (fire) commit(preview);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onpointerdown(e: PointerEvent) {
|
|
108
|
+
if (inert || e.button !== 0 || pointerId !== null) return;
|
|
109
|
+
const el = e.currentTarget as HTMLElement;
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
pointerId = e.pointerId;
|
|
112
|
+
dragging = true;
|
|
113
|
+
preview = fracAt(el, e.clientX) * duration;
|
|
114
|
+
if (typeof el.setPointerCapture === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
el.setPointerCapture(e.pointerId);
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
el.focus?.({ preventScroll: true });
|
|
120
|
+
onscrubstart?.();
|
|
121
|
+
oninput?.(preview);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onpointermove(e: PointerEvent) {
|
|
125
|
+
const el = e.currentTarget as HTMLElement;
|
|
126
|
+
if (pointerId === e.pointerId) {
|
|
127
|
+
preview = fracAt(el, e.clientX) * duration;
|
|
128
|
+
oninput?.(preview);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (inert || pointerId !== null) return;
|
|
132
|
+
hoverFrac = fracAt(el, e.clientX);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onpointerup(e: PointerEvent) {
|
|
136
|
+
if (pointerId !== e.pointerId) return;
|
|
137
|
+
endDrag(e.currentTarget as HTMLElement, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onpointercancel(e: PointerEvent) {
|
|
141
|
+
if (pointerId !== e.pointerId) return;
|
|
142
|
+
endDrag(e.currentTarget as HTMLElement, false);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onlostpointercapture(e: PointerEvent) {
|
|
146
|
+
if (pointerId === null || pointerId !== e.pointerId) return;
|
|
147
|
+
pointerId = null;
|
|
148
|
+
dragging = false;
|
|
149
|
+
onscrubend?.();
|
|
150
|
+
commit(preview);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onpointerleave() {
|
|
154
|
+
hoverFrac = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function onkeydown(e: KeyboardEvent) {
|
|
158
|
+
if (inert) return;
|
|
159
|
+
const base = dragging ? preview : value;
|
|
160
|
+
let next: number | null = null;
|
|
161
|
+
switch (e.key) {
|
|
162
|
+
case 'ArrowRight':
|
|
163
|
+
case 'ArrowUp':
|
|
164
|
+
next = base + step;
|
|
165
|
+
break;
|
|
166
|
+
case 'ArrowLeft':
|
|
167
|
+
case 'ArrowDown':
|
|
168
|
+
next = base - step;
|
|
169
|
+
break;
|
|
170
|
+
case 'PageUp':
|
|
171
|
+
next = base + bigStep * duration;
|
|
172
|
+
break;
|
|
173
|
+
case 'PageDown':
|
|
174
|
+
next = base - bigStep * duration;
|
|
175
|
+
break;
|
|
176
|
+
case 'Home':
|
|
177
|
+
next = 0;
|
|
178
|
+
break;
|
|
179
|
+
case 'End':
|
|
180
|
+
next = duration;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (next === null) return;
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
commit(next);
|
|
186
|
+
}
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<div
|
|
190
|
+
data-tsu="SeekSlider"
|
|
191
|
+
class="seek seek--{size} {klass}"
|
|
192
|
+
class:seek--dragging={dragging}
|
|
193
|
+
class:seek--inert={inert}
|
|
194
|
+
class:seek--tip={tipValue !== null}
|
|
195
|
+
style="--pct: {pct}; --tip: {tipPct};{styleProp ?? ''}"
|
|
196
|
+
role="slider"
|
|
197
|
+
tabindex={inert ? -1 : 0}
|
|
198
|
+
aria-label={label}
|
|
199
|
+
aria-orientation="horizontal"
|
|
200
|
+
aria-valuemin={0}
|
|
201
|
+
aria-valuemax={duration}
|
|
202
|
+
aria-valuenow={shown}
|
|
203
|
+
aria-valuetext={valueText}
|
|
204
|
+
aria-disabled={inert ? 'true' : undefined}
|
|
205
|
+
{onpointerdown}
|
|
206
|
+
{onpointermove}
|
|
207
|
+
{onpointerup}
|
|
208
|
+
{onpointercancel}
|
|
209
|
+
{onlostpointercapture}
|
|
210
|
+
{onpointerleave}
|
|
211
|
+
{onkeydown}
|
|
212
|
+
{...rest}
|
|
213
|
+
>
|
|
214
|
+
<span class="seek-track" data-part="track">
|
|
215
|
+
{#each ranges as [a, b], i (i)}
|
|
216
|
+
<i class="seek-buffered" data-part="buffered" style="--a: {a * 100}; --b: {b * 100}"></i>
|
|
217
|
+
{/each}
|
|
218
|
+
<i class="seek-played" data-part="played"></i>
|
|
219
|
+
</span>
|
|
220
|
+
<span class="seek-anchor seek-anchor--thumb" data-part="thumb-anchor">
|
|
221
|
+
<i class="seek-thumb" data-part="thumb"></i>
|
|
222
|
+
</span>
|
|
223
|
+
{#if tipValue !== null}
|
|
224
|
+
<span class="seek-anchor seek-anchor--tip" aria-hidden="true">
|
|
225
|
+
<span class="seek-tip" data-part="tip">
|
|
226
|
+
{#if previewSnippet}{@render previewSnippet(tipValue)}{/if}
|
|
227
|
+
<span class="seek-tip-time">{formatTime(tipValue, duration)}</span>
|
|
228
|
+
</span>
|
|
229
|
+
</span>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<style>
|
|
234
|
+
.seek {
|
|
235
|
+
--seek-track: 3px;
|
|
236
|
+
--seek-track-active: 5px;
|
|
237
|
+
--seek-thumb: 12px;
|
|
238
|
+
--seek-slab: 1.25rem;
|
|
239
|
+
--seek-accent: var(--slider-accent, var(--accent));
|
|
240
|
+
position: relative;
|
|
241
|
+
display: block;
|
|
242
|
+
width: 100%;
|
|
243
|
+
min-height: var(--seek-slab);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
touch-action: none;
|
|
246
|
+
user-select: none;
|
|
247
|
+
-webkit-user-select: none;
|
|
248
|
+
outline: none;
|
|
249
|
+
}
|
|
250
|
+
.seek--md {
|
|
251
|
+
--seek-track: 4px;
|
|
252
|
+
--seek-track-active: 6px;
|
|
253
|
+
--seek-thumb: 14px;
|
|
254
|
+
--seek-slab: 1.5rem;
|
|
255
|
+
}
|
|
256
|
+
@media (pointer: coarse) {
|
|
257
|
+
.seek {
|
|
258
|
+
min-height: var(--touch-target-compact);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
.seek--inert {
|
|
262
|
+
cursor: default;
|
|
263
|
+
opacity: 0.45;
|
|
264
|
+
}
|
|
265
|
+
.seek-track {
|
|
266
|
+
position: absolute;
|
|
267
|
+
inset-inline: 0;
|
|
268
|
+
top: 50%;
|
|
269
|
+
height: var(--seek-track);
|
|
270
|
+
margin-top: calc(var(--seek-track) / -2);
|
|
271
|
+
border-radius: var(--r-pill);
|
|
272
|
+
background: var(--bg-elevated-2);
|
|
273
|
+
overflow: hidden;
|
|
274
|
+
transition:
|
|
275
|
+
height 120ms var(--ease),
|
|
276
|
+
margin-top 120ms var(--ease);
|
|
277
|
+
}
|
|
278
|
+
.seek:hover .seek-track,
|
|
279
|
+
.seek:focus-visible .seek-track,
|
|
280
|
+
.seek--dragging .seek-track {
|
|
281
|
+
height: var(--seek-track-active);
|
|
282
|
+
margin-top: calc(var(--seek-track-active) / -2);
|
|
283
|
+
}
|
|
284
|
+
.seek--inert:hover .seek-track {
|
|
285
|
+
height: var(--seek-track);
|
|
286
|
+
margin-top: calc(var(--seek-track) / -2);
|
|
287
|
+
}
|
|
288
|
+
.seek-buffered,
|
|
289
|
+
.seek-played {
|
|
290
|
+
position: absolute;
|
|
291
|
+
inset-block: 0;
|
|
292
|
+
inset-inline-start: 0;
|
|
293
|
+
}
|
|
294
|
+
.seek-buffered {
|
|
295
|
+
inset-inline-start: calc(var(--a) * 1%);
|
|
296
|
+
width: calc((var(--b) - var(--a)) * 1%);
|
|
297
|
+
background: var(--border-strong);
|
|
298
|
+
}
|
|
299
|
+
.seek-played {
|
|
300
|
+
width: calc(var(--pct) * 1%);
|
|
301
|
+
background: var(--seek-accent);
|
|
302
|
+
}
|
|
303
|
+
.seek-anchor {
|
|
304
|
+
position: absolute;
|
|
305
|
+
top: 50%;
|
|
306
|
+
width: 0;
|
|
307
|
+
height: 0;
|
|
308
|
+
pointer-events: none;
|
|
309
|
+
}
|
|
310
|
+
.seek-anchor--thumb {
|
|
311
|
+
inset-inline-start: calc(var(--pct) * 1%);
|
|
312
|
+
}
|
|
313
|
+
.seek-anchor--tip {
|
|
314
|
+
inset-inline-start: calc(var(--tip) * 1%);
|
|
315
|
+
}
|
|
316
|
+
.seek-thumb {
|
|
317
|
+
position: absolute;
|
|
318
|
+
left: 0;
|
|
319
|
+
top: 0;
|
|
320
|
+
width: var(--seek-thumb);
|
|
321
|
+
height: var(--seek-thumb);
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
background: var(--seek-accent);
|
|
324
|
+
border: 2px solid var(--bg);
|
|
325
|
+
box-shadow: var(--shadow-sm);
|
|
326
|
+
transform: translate(-50%, -50%) scale(0.6);
|
|
327
|
+
opacity: 0;
|
|
328
|
+
transition:
|
|
329
|
+
opacity 120ms var(--ease),
|
|
330
|
+
transform 120ms var(--ease);
|
|
331
|
+
}
|
|
332
|
+
.seek:hover .seek-thumb,
|
|
333
|
+
.seek:focus-visible .seek-thumb,
|
|
334
|
+
.seek--dragging .seek-thumb {
|
|
335
|
+
opacity: 1;
|
|
336
|
+
transform: translate(-50%, -50%) scale(1);
|
|
337
|
+
}
|
|
338
|
+
.seek--inert .seek-thumb {
|
|
339
|
+
opacity: 0;
|
|
340
|
+
}
|
|
341
|
+
.seek:focus-visible .seek-thumb {
|
|
342
|
+
outline: var(--focus-ring);
|
|
343
|
+
outline-offset: 2px;
|
|
344
|
+
}
|
|
345
|
+
.seek-tip {
|
|
346
|
+
position: absolute;
|
|
347
|
+
left: 0;
|
|
348
|
+
bottom: calc(var(--seek-thumb) / 2 + var(--sp-2));
|
|
349
|
+
transform: translateX(-50%);
|
|
350
|
+
display: flex;
|
|
351
|
+
flex-direction: column;
|
|
352
|
+
align-items: center;
|
|
353
|
+
gap: var(--sp-1);
|
|
354
|
+
padding: var(--sp-1) var(--sp-2);
|
|
355
|
+
border-radius: var(--r-sm);
|
|
356
|
+
background: var(--bg-elevated);
|
|
357
|
+
border: 1px solid var(--border);
|
|
358
|
+
box-shadow: var(--shadow-md);
|
|
359
|
+
color: var(--text);
|
|
360
|
+
font-size: var(--fs-xs);
|
|
361
|
+
font-variant-numeric: tabular-nums;
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
}
|
|
364
|
+
@media (prefers-reduced-motion: reduce) {
|
|
365
|
+
.seek-track,
|
|
366
|
+
.seek-thumb {
|
|
367
|
+
transition: none;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media scrubber. Dragging previews (`oninput`) without seeking; the player
|
|
3
|
+
* seeks only on `onchange`, fired once per release or keyboard step. The
|
|
4
|
+
* hit slab is tall enough for touch while the visible track stays thin.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
8
|
+
type Own = {
|
|
9
|
+
/** Position in seconds. Updated only on commit, never while dragging. */
|
|
10
|
+
value?: number;
|
|
11
|
+
/** Duration in seconds. `null`/`Infinity`/`0` = live or unknown: the track is disabled. */
|
|
12
|
+
max?: number | null;
|
|
13
|
+
/** Buffered `[start, end]` ranges in seconds, drawn as a muted fill. */
|
|
14
|
+
buffered?: [number, number][];
|
|
15
|
+
/** Seconds per arrow key. */
|
|
16
|
+
step?: number;
|
|
17
|
+
/** Fraction of the duration per PageUp/PageDown. */
|
|
18
|
+
bigStep?: number;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
size?: 'sm' | 'md';
|
|
21
|
+
label?: string;
|
|
22
|
+
formatTime?: (seconds: number, ref?: number) => string;
|
|
23
|
+
/** Fired on every pointer move while scrubbing with the previewed time. */
|
|
24
|
+
oninput?: (seconds: number) => void;
|
|
25
|
+
/** Fired once on release or per keyboard step with the committed time. */
|
|
26
|
+
onchange?: (seconds: number) => void;
|
|
27
|
+
onscrubstart?: () => void;
|
|
28
|
+
onscrubend?: () => void;
|
|
29
|
+
/** Rendered above the time in the hover/drag tooltip (thumbnail). */
|
|
30
|
+
previewSnippet?: Snippet<[number]>;
|
|
31
|
+
class?: string;
|
|
32
|
+
style?: string;
|
|
33
|
+
};
|
|
34
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
35
|
+
declare const SeekSlider: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
36
|
+
type SeekSlider = ReturnType<typeof SeekSlider>;
|
|
37
|
+
export default SeekSlider;
|
|
@@ -88,19 +88,23 @@
|
|
|
88
88
|
|
|
89
89
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
90
90
|
const baseId = `seg-${Math.random().toString(36).slice(2, 8)}`;
|
|
91
|
+
// The roving tab stop: the selected option, or the first enabled one when the
|
|
92
|
+
// selection is unknown or disabled, so the group never drops out of the Tab order.
|
|
93
|
+
const stop = $derived(
|
|
94
|
+
options.find((o) => o.value === value && !o.disabled) ?? options.find((o) => !o.disabled)
|
|
95
|
+
);
|
|
96
|
+
const segId = (val: string) => `${baseId}-${val}`;
|
|
91
97
|
|
|
92
98
|
function select(val: string, focus = false) {
|
|
93
99
|
if (options.find((o) => o.value === val)?.disabled) return;
|
|
94
100
|
value = val;
|
|
95
101
|
onchange?.(val);
|
|
96
102
|
if (focus) {
|
|
97
|
-
queueMicrotask(() =>
|
|
98
|
-
listEl?.querySelector<HTMLButtonElement>(`#${baseId}-${val}`)?.focus()
|
|
99
|
-
);
|
|
103
|
+
queueMicrotask(() => listEl?.ownerDocument.getElementById(segId(val))?.focus());
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
function onkeydown(e: KeyboardEvent) {
|
|
103
|
-
const i = options.
|
|
107
|
+
const i = stop ? options.indexOf(stop) : -1;
|
|
104
108
|
const next = nextEnabledSegment(options, i, e.key);
|
|
105
109
|
if (next === undefined) return;
|
|
106
110
|
e.preventDefault();
|
|
@@ -130,11 +134,11 @@
|
|
|
130
134
|
{@const selected = value === o.value}
|
|
131
135
|
<button
|
|
132
136
|
type="button"
|
|
133
|
-
id=
|
|
137
|
+
id={segId(o.value)}
|
|
134
138
|
role="radio"
|
|
135
139
|
aria-checked={selected}
|
|
136
140
|
aria-label={variant === 'icon' && !o.label ? o.value : o.label}
|
|
137
|
-
tabindex={
|
|
141
|
+
tabindex={stop?.value === o.value ? 0 : -1}
|
|
138
142
|
class="seg-item"
|
|
139
143
|
class:has-label={!!o.label}
|
|
140
144
|
class:selected
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
</script>
|
|
67
67
|
|
|
68
68
|
<!-- The wrapper (owned here, so styled scoped) is the positioning context that
|
|
69
|
-
clips the transparent overlaid <select>; the icon Button
|
|
69
|
+
clips the transparent overlaid <select>; the inert icon Button is the face
|
|
70
|
+
behind it, so the select is the only focusable control. -->
|
|
70
71
|
<span
|
|
71
72
|
class="select-button {klass}"
|
|
72
73
|
class:row={variant === 'row'}
|
|
@@ -77,32 +78,33 @@
|
|
|
77
78
|
class:no-shrink={!shrink}
|
|
78
79
|
class:block={block || variant === 'row'}
|
|
79
80
|
>
|
|
80
|
-
<Button variant="ghost" icon={variant === 'square'} block={variant === 'row'} {box} {hitArea}
|
|
81
|
+
<Button variant="ghost" icon={variant === 'square'} block={variant === 'row'} {box} {hitArea} inert tabindex={-1}>
|
|
81
82
|
<span aria-hidden="true" class="sb-glyph">
|
|
82
83
|
{#if iconChildren}<Icon>{@render iconChildren()}</Icon>{:else if icon}<Icon name={icon} />{:else}{glyph}{/if}
|
|
83
84
|
</span>
|
|
84
85
|
{#if variant === 'row'}<span aria-hidden="true" class="sb-label">{label}</span>{/if}
|
|
85
|
-
<Select
|
|
86
|
-
variant="ghost"
|
|
87
|
-
aria-label={label}
|
|
88
|
-
{value}
|
|
89
|
-
onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
|
|
90
|
-
>
|
|
91
|
-
{#if groups}
|
|
92
|
-
{#each groups as g (g.label)}
|
|
93
|
-
<optgroup label={g.label}>
|
|
94
|
-
{#each g.options as o (o.value)}
|
|
95
|
-
<option value={o.value}>{o.label}</option>
|
|
96
|
-
{/each}
|
|
97
|
-
</optgroup>
|
|
98
|
-
{/each}
|
|
99
|
-
{:else}
|
|
100
|
-
{#each options ?? [] as o (o.value)}
|
|
101
|
-
<option value={o.value}>{o.label}</option>
|
|
102
|
-
{/each}
|
|
103
|
-
{/if}
|
|
104
|
-
</Select>
|
|
105
86
|
</Button>
|
|
87
|
+
<Select
|
|
88
|
+
variant="ghost"
|
|
89
|
+
aria-label={label}
|
|
90
|
+
{title}
|
|
91
|
+
{value}
|
|
92
|
+
onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
|
|
93
|
+
>
|
|
94
|
+
{#if groups}
|
|
95
|
+
{#each groups as g (g.label)}
|
|
96
|
+
<optgroup label={g.label}>
|
|
97
|
+
{#each g.options as o (o.value)}
|
|
98
|
+
<option value={o.value}>{o.label}</option>
|
|
99
|
+
{/each}
|
|
100
|
+
</optgroup>
|
|
101
|
+
{/each}
|
|
102
|
+
{:else}
|
|
103
|
+
{#each options ?? [] as o (o.value)}
|
|
104
|
+
<option value={o.value}>{o.label}</option>
|
|
105
|
+
{/each}
|
|
106
|
+
{/if}
|
|
107
|
+
</Select>
|
|
106
108
|
</span>
|
|
107
109
|
|
|
108
110
|
<style>
|
|
@@ -127,6 +129,15 @@
|
|
|
127
129
|
.select-button.active {
|
|
128
130
|
color: var(--accent);
|
|
129
131
|
}
|
|
132
|
+
/* The inert face takes no pointer events: mirror ghost hover and the
|
|
133
|
+
focus ring from the select overlaying it. */
|
|
134
|
+
.select-button:hover :global(.btn-ghost) {
|
|
135
|
+
background: var(--bg-elevated-2);
|
|
136
|
+
}
|
|
137
|
+
.select-button:has(:focus-visible) {
|
|
138
|
+
outline: 2px solid var(--accent);
|
|
139
|
+
outline-offset: 2px;
|
|
140
|
+
}
|
|
130
141
|
.select-button.row :global(.btn) {
|
|
131
142
|
justify-content: flex-start;
|
|
132
143
|
gap: var(--sp-2);
|