svelte-p5-components 0.4.0 → 0.5.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 +10 -0
- package/dist/ActivityBar.svelte +227 -0
- package/dist/ActivityBar.svelte.d.ts +66 -0
- package/dist/ContextMenu.svelte +439 -0
- package/dist/ContextMenu.svelte.d.ts +80 -0
- package/dist/DraggableWindow.svelte +63 -25
- package/dist/EntityToggleList.svelte +175 -21
- package/dist/EntityToggleList.svelte.d.ts +7 -0
- package/dist/SidePanel.svelte +272 -0
- package/dist/SidePanel.svelte.d.ts +59 -0
- package/dist/TimelineScrubber.svelte +223 -70
- package/dist/TimelineScrubber.svelte.d.ts +32 -8
- package/dist/TimelineScrubber.test.svelte.js +4 -3
- package/dist/TimelineTrack.svelte +483 -150
- package/dist/TimelineTrack.svelte.d.ts +33 -18
- package/dist/TimelineTrack.test.svelte.js +70 -9
- package/dist/createMediaPlayback.svelte.d.ts +60 -0
- package/dist/createMediaPlayback.svelte.js +169 -0
- package/dist/createMediaPlayback.test.d.ts +1 -0
- package/dist/createMediaPlayback.test.js +176 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -2,31 +2,38 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import TimelineTrack, {
|
|
4
4
|
type TimelineSegment,
|
|
5
|
-
type
|
|
5
|
+
type TimelineSelection
|
|
6
6
|
} from './TimelineTrack.svelte';
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// without reinventing controls.
|
|
8
|
+
// Stacked player bar for canvas apps. Lays the timeline out like a
|
|
9
|
+
// video player:
|
|
11
10
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// [=========================track (full width)==========================]
|
|
12
|
+
// [▶ ◀◀ 1×] [00:12 / 03:45]
|
|
14
13
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// Track takes the full width of the component. Controls live on a row
|
|
15
|
+
// below it: play / skip / speed on the left, time display on the right.
|
|
16
|
+
// Clicking the time display toggles between total duration and
|
|
17
|
+
// remaining time (YouTube-style).
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// The track itself is mostly chrome-free at rest — only the playhead
|
|
20
|
+
// and a tinted selection band are visible. Hovering the track reveals
|
|
21
|
+
// grab affordances for the selection handles and grows the playhead
|
|
22
|
+
// knob. This keeps the resting state quiet without hiding the
|
|
23
|
+
// existence of the selection range.
|
|
23
24
|
//
|
|
24
|
-
//
|
|
25
|
+
// Bindable: currentTime, isPlaying, speed, selectionStart, selectionEnd.
|
|
26
|
+
// The consumer owns the animation loop that advances currentTime when
|
|
27
|
+
// isPlaying is true.
|
|
25
28
|
|
|
26
29
|
interface Props {
|
|
30
|
+
/** Total timeline length. */
|
|
27
31
|
duration: number;
|
|
32
|
+
/** Current playhead position. Bindable. */
|
|
28
33
|
currentTime?: number;
|
|
34
|
+
/** Is playback running? Bindable. */
|
|
29
35
|
isPlaying?: boolean;
|
|
36
|
+
/** Current playback speed multiplier. Bindable. */
|
|
30
37
|
speed?: number;
|
|
31
38
|
/** Available speed presets. Default: [1, 2, 4, 8, 16]. */
|
|
32
39
|
speedOptions?: number[];
|
|
@@ -34,17 +41,37 @@
|
|
|
34
41
|
speedLocked?: boolean;
|
|
35
42
|
/** Title/tooltip shown when `speedLocked` is true. */
|
|
36
43
|
speedLockedReason?: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
44
|
+
/** Selection range start. Bindable. */
|
|
45
|
+
selectionStart?: number;
|
|
46
|
+
/** Selection range end. Bindable. */
|
|
47
|
+
selectionEnd?: number;
|
|
48
|
+
/** Hide the selection band + handles. Default: auto (show when both bounds set). */
|
|
49
|
+
showSelection?: boolean;
|
|
50
|
+
/** Include a "skip to selection start" button next to play. */
|
|
51
|
+
showSkipToStart?: boolean;
|
|
52
|
+
/** Highlighted labeled regions. */
|
|
39
53
|
segments?: TimelineSegment[];
|
|
40
|
-
/** Format
|
|
54
|
+
/** Format a time value for display. Default: mm:ss (or h:mm:ss for ≥1h). */
|
|
41
55
|
formatTime?: (seconds: number) => string;
|
|
56
|
+
/** Seek callback (from track click or playhead drag). */
|
|
42
57
|
onSeek?: (time: number) => void;
|
|
58
|
+
/** Fired every frame while a selection handle is dragged. */
|
|
59
|
+
onSelectionChange?: (selection: TimelineSelection) => void;
|
|
60
|
+
/** Fired when a selection drag is released. */
|
|
61
|
+
onSelectionCommit?: (selection: TimelineSelection) => void;
|
|
62
|
+
/** Fired when the user toggles play. */
|
|
43
63
|
onPlayToggle?: (nowPlaying: boolean) => void;
|
|
64
|
+
/** Fired when the user cycles the speed. */
|
|
44
65
|
onSpeedChange?: (speed: number) => void;
|
|
66
|
+
/** Fired when the user clicks a segment. */
|
|
45
67
|
onSegmentClick?: (segment: TimelineSegment) => void;
|
|
46
|
-
/** Snippet rendered above the
|
|
47
|
-
|
|
68
|
+
/** Snippet rendered above the hover x position. */
|
|
69
|
+
hoverPreview?: Snippet<[{ time: number; xPercent: number }]>;
|
|
70
|
+
/**
|
|
71
|
+
* When dragging the selection start handle, also move the playhead to
|
|
72
|
+
* the new start (and fire `onSeek`). Default: false.
|
|
73
|
+
*/
|
|
74
|
+
playheadFollowsSelectionStart?: boolean;
|
|
48
75
|
class?: string;
|
|
49
76
|
}
|
|
50
77
|
|
|
@@ -56,21 +83,36 @@
|
|
|
56
83
|
speedOptions = [1, 2, 4, 8, 16],
|
|
57
84
|
speedLocked = false,
|
|
58
85
|
speedLockedReason,
|
|
59
|
-
|
|
86
|
+
selectionStart = $bindable<number | undefined>(undefined),
|
|
87
|
+
selectionEnd = $bindable<number | undefined>(undefined),
|
|
88
|
+
showSelection,
|
|
89
|
+
showSkipToStart = true,
|
|
60
90
|
segments = [],
|
|
61
91
|
formatTime = defaultFormatTime,
|
|
62
92
|
onSeek,
|
|
93
|
+
onSelectionChange,
|
|
94
|
+
onSelectionCommit,
|
|
63
95
|
onPlayToggle,
|
|
64
96
|
onSpeedChange,
|
|
65
97
|
onSegmentClick,
|
|
66
|
-
|
|
98
|
+
hoverPreview,
|
|
99
|
+
playheadFollowsSelectionStart = false,
|
|
67
100
|
class: className = ''
|
|
68
101
|
}: Props = $props();
|
|
69
102
|
|
|
103
|
+
let timeMode = $state<'total' | 'remaining'>('total');
|
|
104
|
+
|
|
105
|
+
const remaining = $derived(Math.max(0, duration - currentTime));
|
|
106
|
+
|
|
70
107
|
function defaultFormatTime(seconds: number): string {
|
|
71
108
|
if (!isFinite(seconds)) return '00:00';
|
|
72
|
-
const
|
|
73
|
-
const
|
|
109
|
+
const total = Math.max(0, Math.floor(seconds));
|
|
110
|
+
const h = Math.floor(total / 3600);
|
|
111
|
+
const m = Math.floor((total % 3600) / 60);
|
|
112
|
+
const s = total % 60;
|
|
113
|
+
if (h > 0) {
|
|
114
|
+
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
115
|
+
}
|
|
74
116
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
75
117
|
}
|
|
76
118
|
|
|
@@ -87,38 +129,70 @@
|
|
|
87
129
|
speed = next;
|
|
88
130
|
onSpeedChange?.(next);
|
|
89
131
|
}
|
|
132
|
+
|
|
133
|
+
function handleSkipToStart() {
|
|
134
|
+
const target = selectionStart ?? 0;
|
|
135
|
+
currentTime = target;
|
|
136
|
+
onSeek?.(target);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toggleTimeMode() {
|
|
140
|
+
timeMode = timeMode === 'total' ? 'remaining' : 'total';
|
|
141
|
+
}
|
|
90
142
|
</script>
|
|
91
143
|
|
|
92
144
|
<div class="timeline-scrubber {className}">
|
|
93
|
-
<div class="timeline-scrubber__track">
|
|
94
|
-
<span class="timeline-scrubber__time" aria-label="start"
|
|
95
|
-
>{formatTime(viewWindow?.start ?? 0)}</span
|
|
96
|
-
>
|
|
145
|
+
<div class="timeline-scrubber__track-row">
|
|
97
146
|
<TimelineTrack
|
|
98
147
|
{duration}
|
|
99
148
|
bind:currentTime
|
|
100
|
-
bind:
|
|
149
|
+
bind:selectionStart
|
|
150
|
+
bind:selectionEnd
|
|
151
|
+
{showSelection}
|
|
101
152
|
{segments}
|
|
102
153
|
{onSeek}
|
|
154
|
+
{onSelectionChange}
|
|
155
|
+
{onSelectionCommit}
|
|
103
156
|
{onSegmentClick}
|
|
104
|
-
{
|
|
157
|
+
{hoverPreview}
|
|
158
|
+
{playheadFollowsSelectionStart}
|
|
105
159
|
/>
|
|
106
|
-
<span class="timeline-scrubber__time" aria-label="end">
|
|
107
|
-
{formatTime(viewWindow?.end ?? duration)}
|
|
108
|
-
</span>
|
|
109
160
|
</div>
|
|
110
161
|
|
|
111
|
-
<div class="timeline-scrubber__controls">
|
|
162
|
+
<div class="timeline-scrubber__controls-row">
|
|
112
163
|
<button
|
|
113
164
|
type="button"
|
|
114
|
-
class="timeline-scrubber__btn"
|
|
165
|
+
class="timeline-scrubber__btn timeline-scrubber__btn--play"
|
|
115
166
|
aria-pressed={isPlaying}
|
|
116
167
|
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
168
|
+
title={isPlaying ? 'Pause' : 'Play'}
|
|
117
169
|
onclick={togglePlay}
|
|
118
170
|
>
|
|
119
|
-
{isPlaying
|
|
171
|
+
{#if isPlaying}
|
|
172
|
+
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
|
|
173
|
+
<path d="M6 4h4v16H6zM14 4h4v16h-4z" fill="currentColor" />
|
|
174
|
+
</svg>
|
|
175
|
+
{:else}
|
|
176
|
+
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
|
|
177
|
+
<path d="M8 5v14l11-7z" fill="currentColor" />
|
|
178
|
+
</svg>
|
|
179
|
+
{/if}
|
|
120
180
|
</button>
|
|
121
181
|
|
|
182
|
+
{#if showSkipToStart}
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
class="timeline-scrubber__btn"
|
|
186
|
+
aria-label="Skip to start"
|
|
187
|
+
title="Skip to start"
|
|
188
|
+
onclick={handleSkipToStart}
|
|
189
|
+
>
|
|
190
|
+
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
|
191
|
+
<path d="M6 5h2v14H6zM20 5 9 12l11 7z" fill="currentColor" />
|
|
192
|
+
</svg>
|
|
193
|
+
</button>
|
|
194
|
+
{/if}
|
|
195
|
+
|
|
122
196
|
<button
|
|
123
197
|
type="button"
|
|
124
198
|
class="timeline-scrubber__btn timeline-scrubber__btn--speed"
|
|
@@ -130,12 +204,24 @@
|
|
|
130
204
|
disabled={speedLocked}
|
|
131
205
|
onclick={cycleSpeed}
|
|
132
206
|
>
|
|
133
|
-
{speed}
|
|
207
|
+
<span>{speed}×</span>
|
|
134
208
|
</button>
|
|
135
209
|
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
class="timeline-scrubber__time-display"
|
|
213
|
+
aria-label="Toggle time display"
|
|
214
|
+
title={timeMode === 'total' ? 'Click to show remaining time' : 'Click to show total time'}
|
|
215
|
+
onclick={toggleTimeMode}
|
|
216
|
+
>
|
|
217
|
+
<span class="timeline-scrubber__time-curr">{formatTime(currentTime)}</span>
|
|
218
|
+
<span class="timeline-scrubber__time-sep">/</span>
|
|
219
|
+
{#if timeMode === 'remaining'}
|
|
220
|
+
<span class="timeline-scrubber__time-alt">-{formatTime(remaining)}</span>
|
|
221
|
+
{:else}
|
|
222
|
+
<span class="timeline-scrubber__time-total">{formatTime(duration)}</span>
|
|
223
|
+
{/if}
|
|
224
|
+
</button>
|
|
139
225
|
</div>
|
|
140
226
|
</div>
|
|
141
227
|
|
|
@@ -143,52 +229,77 @@
|
|
|
143
229
|
.timeline-scrubber {
|
|
144
230
|
display: flex;
|
|
145
231
|
flex-direction: column;
|
|
232
|
+
width: 100%;
|
|
146
233
|
gap: 4px;
|
|
234
|
+
padding: 6px 10px 8px;
|
|
235
|
+
box-sizing: border-box;
|
|
147
236
|
font:
|
|
148
|
-
|
|
237
|
+
14px/1 system-ui,
|
|
149
238
|
-apple-system,
|
|
150
239
|
Segoe UI,
|
|
151
240
|
sans-serif;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.timeline-scrubber__track {
|
|
155
|
-
display: flex;
|
|
156
|
-
align-items: center;
|
|
157
|
-
gap: 8px;
|
|
158
|
-
}
|
|
159
241
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
242
|
+
--ts-btn-bg: #ffffff;
|
|
243
|
+
--ts-btn-bg-hover: #f9fafb;
|
|
244
|
+
--ts-btn-bg-active: #f3f4f6;
|
|
245
|
+
--ts-btn-border: rgba(0, 0, 0, 0.12);
|
|
246
|
+
--ts-btn-border-hover: rgba(0, 0, 0, 0.22);
|
|
247
|
+
--ts-btn-fg: #1f2937;
|
|
248
|
+
--ts-btn-fg-hover: #111827;
|
|
249
|
+
--ts-time-fg: #1f2937;
|
|
250
|
+
--ts-time-muted-fg: #6b7280;
|
|
163
251
|
}
|
|
164
252
|
|
|
165
|
-
.timeline-
|
|
166
|
-
|
|
167
|
-
font-size: 11px;
|
|
168
|
-
color: #6b7280;
|
|
169
|
-
min-width: 3.5em;
|
|
170
|
-
text-align: center;
|
|
253
|
+
.timeline-scrubber__track-row {
|
|
254
|
+
width: 100%;
|
|
171
255
|
}
|
|
172
256
|
|
|
173
|
-
.timeline-scrubber__controls {
|
|
257
|
+
.timeline-scrubber__controls-row {
|
|
174
258
|
display: flex;
|
|
175
259
|
align-items: center;
|
|
176
|
-
|
|
177
|
-
|
|
260
|
+
justify-content: flex-start;
|
|
261
|
+
width: 100%;
|
|
262
|
+
gap: 4px;
|
|
263
|
+
min-height: 36px;
|
|
264
|
+
flex-wrap: nowrap;
|
|
178
265
|
}
|
|
179
266
|
|
|
180
267
|
.timeline-scrubber__btn {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
268
|
+
display: inline-flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
gap: 4px;
|
|
272
|
+
background: var(--ts-btn-bg);
|
|
273
|
+
border: 1px solid var(--ts-btn-border);
|
|
274
|
+
border-radius: 6px;
|
|
275
|
+
padding: 0 10px;
|
|
276
|
+
min-width: 36px;
|
|
277
|
+
height: 36px;
|
|
278
|
+
color: var(--ts-btn-fg);
|
|
185
279
|
cursor: pointer;
|
|
186
280
|
font: inherit;
|
|
187
281
|
line-height: 1;
|
|
282
|
+
transition:
|
|
283
|
+
background-color 120ms ease,
|
|
284
|
+
border-color 120ms ease,
|
|
285
|
+
color 120ms ease,
|
|
286
|
+
opacity 120ms ease;
|
|
287
|
+
flex: 0 0 auto;
|
|
188
288
|
}
|
|
189
289
|
|
|
190
290
|
.timeline-scrubber__btn:hover:not(:disabled) {
|
|
191
|
-
background:
|
|
291
|
+
background: var(--ts-btn-bg-hover);
|
|
292
|
+
border-color: var(--ts-btn-border-hover);
|
|
293
|
+
color: var(--ts-btn-fg-hover);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.timeline-scrubber__btn:active:not(:disabled) {
|
|
297
|
+
background: var(--ts-btn-bg-active);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.timeline-scrubber__btn:focus-visible {
|
|
301
|
+
outline: 2px solid var(--ts-btn-fg);
|
|
302
|
+
outline-offset: 2px;
|
|
192
303
|
}
|
|
193
304
|
|
|
194
305
|
.timeline-scrubber__btn:disabled {
|
|
@@ -196,19 +307,61 @@
|
|
|
196
307
|
}
|
|
197
308
|
|
|
198
309
|
.timeline-scrubber__btn--speed {
|
|
199
|
-
min-width: 3em;
|
|
200
310
|
font-variant-numeric: tabular-nums;
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
font-size: 13px;
|
|
313
|
+
padding: 0 10px;
|
|
314
|
+
min-width: 42px;
|
|
201
315
|
}
|
|
202
316
|
|
|
203
317
|
.timeline-scrubber__btn--speed.is-locked {
|
|
204
318
|
opacity: 0.5;
|
|
205
319
|
}
|
|
206
320
|
|
|
207
|
-
.timeline-
|
|
321
|
+
.timeline-scrubber__time-display {
|
|
322
|
+
background: transparent;
|
|
323
|
+
border: 1px solid transparent;
|
|
324
|
+
padding: 6px 10px;
|
|
325
|
+
margin: 0;
|
|
326
|
+
border-radius: 6px;
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
font: inherit;
|
|
208
329
|
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
209
|
-
font-size:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
font-variant-numeric: tabular-nums;
|
|
332
|
+
color: var(--ts-time-fg);
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
gap: 6px;
|
|
336
|
+
flex: 0 0 auto;
|
|
337
|
+
transition:
|
|
338
|
+
background-color 120ms ease,
|
|
339
|
+
border-color 120ms ease;
|
|
340
|
+
white-space: nowrap;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.timeline-scrubber__time-display:hover {
|
|
344
|
+
background: var(--ts-btn-bg-hover);
|
|
345
|
+
border-color: var(--ts-btn-border);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.timeline-scrubber__time-display:focus-visible {
|
|
349
|
+
outline: 2px solid var(--ts-btn-fg);
|
|
350
|
+
outline-offset: 2px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.timeline-scrubber__time-curr {
|
|
354
|
+
color: var(--ts-time-fg);
|
|
355
|
+
font-weight: 500;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.timeline-scrubber__time-sep {
|
|
359
|
+
color: var(--ts-time-muted-fg);
|
|
360
|
+
opacity: 0.5;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.timeline-scrubber__time-total,
|
|
364
|
+
.timeline-scrubber__time-alt {
|
|
365
|
+
color: var(--ts-time-muted-fg);
|
|
213
366
|
}
|
|
214
367
|
</style>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import { type TimelineSegment, type
|
|
2
|
+
import { type TimelineSegment, type TimelineSelection } from './TimelineTrack.svelte';
|
|
3
3
|
interface Props {
|
|
4
|
+
/** Total timeline length. */
|
|
4
5
|
duration: number;
|
|
6
|
+
/** Current playhead position. Bindable. */
|
|
5
7
|
currentTime?: number;
|
|
8
|
+
/** Is playback running? Bindable. */
|
|
6
9
|
isPlaying?: boolean;
|
|
10
|
+
/** Current playback speed multiplier. Bindable. */
|
|
7
11
|
speed?: number;
|
|
8
12
|
/** Available speed presets. Default: [1, 2, 4, 8, 16]. */
|
|
9
13
|
speedOptions?: number[];
|
|
@@ -11,22 +15,42 @@ interface Props {
|
|
|
11
15
|
speedLocked?: boolean;
|
|
12
16
|
/** Title/tooltip shown when `speedLocked` is true. */
|
|
13
17
|
speedLockedReason?: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
18
|
+
/** Selection range start. Bindable. */
|
|
19
|
+
selectionStart?: number;
|
|
20
|
+
/** Selection range end. Bindable. */
|
|
21
|
+
selectionEnd?: number;
|
|
22
|
+
/** Hide the selection band + handles. Default: auto (show when both bounds set). */
|
|
23
|
+
showSelection?: boolean;
|
|
24
|
+
/** Include a "skip to selection start" button next to play. */
|
|
25
|
+
showSkipToStart?: boolean;
|
|
26
|
+
/** Highlighted labeled regions. */
|
|
16
27
|
segments?: TimelineSegment[];
|
|
17
|
-
/** Format
|
|
28
|
+
/** Format a time value for display. Default: mm:ss (or h:mm:ss for ≥1h). */
|
|
18
29
|
formatTime?: (seconds: number) => string;
|
|
30
|
+
/** Seek callback (from track click or playhead drag). */
|
|
19
31
|
onSeek?: (time: number) => void;
|
|
32
|
+
/** Fired every frame while a selection handle is dragged. */
|
|
33
|
+
onSelectionChange?: (selection: TimelineSelection) => void;
|
|
34
|
+
/** Fired when a selection drag is released. */
|
|
35
|
+
onSelectionCommit?: (selection: TimelineSelection) => void;
|
|
36
|
+
/** Fired when the user toggles play. */
|
|
20
37
|
onPlayToggle?: (nowPlaying: boolean) => void;
|
|
38
|
+
/** Fired when the user cycles the speed. */
|
|
21
39
|
onSpeedChange?: (speed: number) => void;
|
|
40
|
+
/** Fired when the user clicks a segment. */
|
|
22
41
|
onSegmentClick?: (segment: TimelineSegment) => void;
|
|
23
|
-
/** Snippet rendered above the
|
|
24
|
-
|
|
42
|
+
/** Snippet rendered above the hover x position. */
|
|
43
|
+
hoverPreview?: Snippet<[{
|
|
25
44
|
time: number;
|
|
26
|
-
|
|
45
|
+
xPercent: number;
|
|
27
46
|
}]>;
|
|
47
|
+
/**
|
|
48
|
+
* When dragging the selection start handle, also move the playhead to
|
|
49
|
+
* the new start (and fire `onSeek`). Default: false.
|
|
50
|
+
*/
|
|
51
|
+
playheadFollowsSelectionStart?: boolean;
|
|
28
52
|
class?: string;
|
|
29
53
|
}
|
|
30
|
-
declare const TimelineScrubber: import("svelte").Component<Props, {}, "currentTime" | "
|
|
54
|
+
declare const TimelineScrubber: import("svelte").Component<Props, {}, "currentTime" | "selectionStart" | "selectionEnd" | "isPlaying" | "speed">;
|
|
31
55
|
type TimelineScrubber = ReturnType<typeof TimelineScrubber>;
|
|
32
56
|
export default TimelineScrubber;
|
|
@@ -53,14 +53,15 @@ describe('<TimelineScrubber>', () => {
|
|
|
53
53
|
await fireEvent.click(btn);
|
|
54
54
|
expect(onSpeedChange).not.toHaveBeenCalled();
|
|
55
55
|
});
|
|
56
|
-
it('uses the custom formatTime prop for
|
|
56
|
+
it('uses the custom formatTime prop for the current-time and duration readout', () => {
|
|
57
|
+
// YouTube-style readout shows `currentTime / duration`; there is no
|
|
58
|
+
// separate start-edge label (that was the pre-redesign layout).
|
|
57
59
|
const formatTime = (s) => `[${Math.round(s)}s]`;
|
|
58
60
|
const { container } = render(TimelineScrubber, {
|
|
59
61
|
props: { duration: 120, currentTime: 42, formatTime }
|
|
60
62
|
});
|
|
61
63
|
const text = container.textContent ?? '';
|
|
62
|
-
expect(text).toContain('[0s]');
|
|
63
|
-
expect(text).toContain('[120s]');
|
|
64
64
|
expect(text).toContain('[42s]');
|
|
65
|
+
expect(text).toContain('[120s]');
|
|
65
66
|
});
|
|
66
67
|
});
|