@r2digisolutions/components 0.2.0 → 0.3.1

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.
Files changed (65) hide show
  1. package/README.md +10 -0
  2. package/dist/components/atoms/Playhead/Playhead.svelte +44 -5
  3. package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
  4. package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
  5. package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
  6. package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
  7. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
  8. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
  9. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
  10. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
  11. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
  12. package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
  13. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
  14. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
  15. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
  16. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
  17. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
  18. package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
  19. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
  20. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
  21. package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
  22. package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
  23. package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
  24. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
  25. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
  26. package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
  27. package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
  28. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
  29. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
  30. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
  31. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
  32. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
  33. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
  34. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
  35. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
  36. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
  37. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
  38. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
  39. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
  40. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
  41. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
  42. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
  43. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
  44. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
  45. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
  46. package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
  47. package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
  48. package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
  49. package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
  50. package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
  51. package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
  52. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
  53. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
  54. package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
  55. package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
  56. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
  57. package/dist/index.d.ts +11 -3
  58. package/dist/index.js +6 -2
  59. package/dist/styles.css +237 -0
  60. package/dist/utils/mediaTime.d.ts +14 -4
  61. package/dist/utils/mediaTime.js +27 -8
  62. package/dist/utils/mediaTracks.d.ts +154 -0
  63. package/dist/utils/mediaTracks.js +697 -1
  64. package/dist/utils/videoSequence.js +35 -4
  65. package/package.json +4 -2
@@ -1,29 +1,67 @@
1
1
  <script lang="ts">
2
2
  import Playhead from '../../atoms/Playhead/Playhead.svelte';
3
3
  import TimeRuler from '../../atoms/TimeRuler/TimeRuler.svelte';
4
+ import TimelineGrid from '../../atoms/TimelineGrid/TimelineGrid.svelte';
5
+ import MarqueeRect from '../../atoms/MarqueeRect/MarqueeRect.svelte';
4
6
  import TimelineTrack from '../TimelineTrack/TimelineTrack.svelte';
5
7
  import VideoTrackHeader from '../VideoTrackHeader/VideoTrackHeader.svelte';
6
8
  import TrackHeader from '../TrackHeader/TrackHeader.svelte';
7
9
  import IconButton from '../../atoms/IconButton/IconButton.svelte';
8
- import type { MediaClip, MediaTrack } from '../../../utils/mediaTracks.js';
9
- import { clampMs, msToPx, pxPerMsFromZoom, pxToMs } from '../../../utils/mediaTime.js';
10
+ import Button from '../../atoms/Button/Button.svelte';
11
+ import type { MediaClip, MediaTrack, MediaTrackKind } from '../../../utils/mediaTracks.js';
12
+ import {
13
+ clampMs,
14
+ msToPx,
15
+ pxPerMsFromZoom,
16
+ pxToMs,
17
+ snapStepForZoom,
18
+ ticksForDuration
19
+ } from '../../../utils/mediaTime.js';
20
+ import {
21
+ attachMarqueeSelect,
22
+ collectMarqueeItems,
23
+ defaultMarqueeIgnore,
24
+ resolveMarqueeSelection,
25
+ type MarqueeRect as MarqueeBox
26
+ } from '../../../utils/marqueeSelect.js';
10
27
  import ZoomIn from '@lucide/svelte/icons/zoom-in';
11
28
  import ZoomOut from '@lucide/svelte/icons/zoom-out';
29
+ import { onDestroy } from 'svelte';
12
30
 
13
31
  interface MediaTimelineProps {
14
32
  tracks?: MediaTrack[];
15
33
  durationMs?: number;
16
34
  currentTimeMs?: number;
17
35
  zoom?: number;
36
+ selectedClipIds?: string[];
37
+ /** @deprecated use selectedClipIds */
18
38
  selectedClipId?: string | null;
19
39
  showHeaders?: boolean;
20
40
  headerVariant?: 'video' | 'audio' | 'plain';
41
+ snap?: boolean;
42
+ showTrackActions?: boolean;
43
+ /** Allow drag-reorder of tracks via header grip */
44
+ reorderTracks?: boolean;
45
+ /** Show delete control on video track headers */
46
+ allowRemoveTracks?: boolean;
21
47
  class?: string;
22
48
  onseek?: (ms: number) => void;
23
- onselectclip?: (clipId: string) => void;
49
+ onselectclip?: (clipId: string, e: MouseEvent) => void;
50
+ oncontextmenuclip?: (clipId: string, e: MouseEvent) => void;
51
+ oncontextmenutrack?: (trackId: string, e: MouseEvent) => void;
24
52
  onchangeclip?: (clip: MediaClip) => void;
53
+ onmoveclips?: (clipIds: string[], deltaMs: number) => void;
54
+ ondropclip?: (clipId: string, e: DragEvent) => void;
55
+ ondroptrack?: (trackId: string, e: DragEvent, timeMs: number) => void;
25
56
  onchangetrack?: (track: MediaTrack) => void;
26
57
  onzoomchange?: (zoom: number) => void;
58
+ onaddtrack?: (kind: MediaTrackKind) => void;
59
+ onremovetrack?: (trackId: string) => void;
60
+ /** Fired with new track id order after drag reorder */
61
+ onreordertracks?: (orderedIds: string[]) => void;
62
+ onemptyclick?: (e: MouseEvent) => void;
63
+ /** Replace / add / toggle selection from marquee */
64
+ onselectionchange?: (clipIds: string[]) => void;
27
65
  }
28
66
 
29
67
  let {
@@ -31,101 +69,373 @@
31
69
  durationMs = 30_000,
32
70
  currentTimeMs = $bindable(0),
33
71
  zoom = $bindable(1),
72
+ selectedClipIds,
34
73
  selectedClipId = null,
35
74
  showHeaders = true,
36
75
  headerVariant = 'plain',
76
+ snap = true,
77
+ showTrackActions = false,
78
+ reorderTracks = false,
79
+ allowRemoveTracks = false,
37
80
  class: className = '',
38
81
  onseek,
39
82
  onselectclip,
83
+ oncontextmenuclip,
84
+ oncontextmenutrack,
40
85
  onchangeclip,
86
+ onmoveclips,
87
+ ondropclip,
88
+ ondroptrack,
41
89
  onchangetrack,
42
- onzoomchange
90
+ onzoomchange,
91
+ onaddtrack,
92
+ onremovetrack,
93
+ onreordertracks,
94
+ onemptyclick,
95
+ onselectionchange
43
96
  }: MediaTimelineProps = $props();
44
97
 
98
+ const resolvedSelected = $derived(
99
+ selectedClipIds ?? (selectedClipId ? [selectedClipId] : [])
100
+ );
45
101
  const pxPerMs = $derived(pxPerMsFromZoom(zoom));
46
102
  const contentWidth = $derived(Math.max(1, durationMs * pxPerMs));
47
103
  const playheadX = $derived(msToPx(currentTimeMs, pxPerMs));
104
+ const ticks = $derived(ticksForDuration(durationMs, pxPerMs));
105
+ const snapToMs = $derived(snap ? snapStepForZoom(pxPerMs) : 0);
106
+ const headerW = $derived(reorderTracks || allowRemoveTracks ? 200 : 176);
48
107
 
49
108
  let scroller = $state<HTMLDivElement | null>(null);
109
+ let tracksEl = $state<HTMLDivElement | null>(null);
110
+ let dragTrackId = $state<string | null>(null);
111
+ let dragTrackHeight = $state(44);
112
+ let floatPos = $state({ x: 0, y: 0, w: 0 });
113
+ let dragOffset = $state({ x: 0, y: 0 });
114
+ let scrubbing = $state(false);
115
+ let marquee = $state<MarqueeBox | null>(null);
116
+ let disposeMarquee: (() => void) | null = null;
117
+ const rulerH = 28;
118
+
119
+ const draggingTrack = $derived(dragTrackId ? tracks.find((t) => t.id === dragTrackId) : null);
120
+
121
+ $effect(() => {
122
+ disposeMarquee?.();
123
+ disposeMarquee = null;
124
+ const el = tracksEl;
125
+ if (!el || !onselectionchange) return;
126
+ disposeMarquee = attachMarqueeSelect(el, {
127
+ getItems: () => collectMarqueeItems(el),
128
+ onRect: (r) => (marquee = r),
129
+ shouldIgnore: (target) =>
130
+ defaultMarqueeIgnore(target) ||
131
+ (target instanceof Element &&
132
+ Boolean(
133
+ target.closest(
134
+ '[data-marquee-id], [data-marquee-ignore], [aria-label="Playhead"], [role="slider"], .sticky'
135
+ )
136
+ )),
137
+ onSelect: ({ ids, modifier }) => {
138
+ onselectionchange(resolveMarqueeSelection(ids, resolvedSelected, modifier));
139
+ }
140
+ });
141
+ });
142
+
143
+ onDestroy(() => {
144
+ disposeMarquee?.();
145
+ endTrackReorder();
146
+ });
50
147
 
51
148
  function seekFromClientX(clientX: number) {
52
149
  if (!scroller) return;
53
150
  const rect = scroller.getBoundingClientRect();
54
- const x = clientX - rect.left + scroller.scrollLeft - (showHeaders ? 144 : 0);
151
+ const x = clientX - rect.left + scroller.scrollLeft - (showHeaders ? headerW : 0);
55
152
  const ms = clampMs(pxToMs(x, pxPerMs), 0, durationMs);
56
153
  currentTimeMs = ms;
57
154
  onseek?.(ms);
58
155
  }
59
156
 
157
+ function seekToMs(ms: number) {
158
+ currentTimeMs = clampMs(ms, 0, durationMs);
159
+ onseek?.(currentTimeMs);
160
+ }
161
+
60
162
  function setZoom(next: number) {
61
163
  zoom = Math.min(8, Math.max(0.25, next));
62
164
  onzoomchange?.(zoom);
63
165
  }
166
+
167
+ function trackIndexFromPoint(clientY: number): number {
168
+ if (!tracksEl) return -1;
169
+ const rows = [...tracksEl.querySelectorAll<HTMLElement>('[data-track-row]')];
170
+ if (!rows.length) return -1;
171
+ for (let i = 0; i < rows.length; i++) {
172
+ const rect = rows[i].getBoundingClientRect();
173
+ const mid = rect.top + rect.height / 2;
174
+ if (clientY < mid) return i;
175
+ }
176
+ return rows.length - 1;
177
+ }
178
+
179
+ function moveTrackToIndex(sourceId: string, toIndex: number) {
180
+ const ids = tracks.map((t) => t.id);
181
+ const from = ids.indexOf(sourceId);
182
+ if (from < 0 || toIndex < 0 || toIndex >= ids.length || from === toIndex) return;
183
+ const next = [...ids];
184
+ next.splice(from, 1);
185
+ next.splice(toIndex, 0, sourceId);
186
+ onreordertracks?.(next);
187
+ }
188
+
189
+ function onTrackReorderMove(e: PointerEvent) {
190
+ if (!dragTrackId) return;
191
+ floatPos = {
192
+ x: e.clientX - dragOffset.x,
193
+ y: e.clientY - dragOffset.y,
194
+ w: floatPos.w
195
+ };
196
+ const to = trackIndexFromPoint(e.clientY);
197
+ if (to >= 0) moveTrackToIndex(dragTrackId, to);
198
+ }
199
+
200
+ function endTrackReorder() {
201
+ dragTrackId = null;
202
+ window.removeEventListener('pointermove', onTrackReorderMove);
203
+ window.removeEventListener('pointerup', endTrackReorder);
204
+ window.removeEventListener('pointercancel', endTrackReorder);
205
+ }
206
+
207
+ function startTrackReorder(trackId: string, e: PointerEvent) {
208
+ if (!reorderTracks) return;
209
+ const row = (e.currentTarget as HTMLElement).closest('[data-track-row]') as HTMLElement | null;
210
+ if (!row) return;
211
+ const rect = row.getBoundingClientRect();
212
+ const scrollerRect = scroller?.getBoundingClientRect();
213
+ const ghostW = Math.min(
214
+ Math.max(headerW + 180, scroller?.clientWidth ?? 360),
215
+ window.innerWidth - 16
216
+ );
217
+ const ghostX = scrollerRect?.left ?? Math.max(8, rect.left);
218
+ dragTrackHeight = rect.height;
219
+ dragOffset = { x: e.clientX - ghostX, y: e.clientY - rect.top };
220
+ floatPos = { x: ghostX, y: rect.top, w: ghostW };
221
+ dragTrackId = trackId;
222
+ window.addEventListener('pointermove', onTrackReorderMove);
223
+ window.addEventListener('pointerup', endTrackReorder);
224
+ window.addEventListener('pointercancel', endTrackReorder);
225
+ }
64
226
  </script>
65
227
 
66
- <div class={['flex flex-col overflow-hidden rounded-lg border border-border bg-surface', className]}>
67
- <div class="flex items-center justify-end gap-1 border-b border-border px-2 py-1">
68
- <IconButton label="Zoom out" size="xs" onclick={() => setZoom(zoom / 1.25)}>
69
- <ZoomOut class="h-3.5 w-3.5" />
70
- </IconButton>
71
- <IconButton label="Zoom in" size="xs" onclick={() => setZoom(zoom * 1.25)}>
72
- <ZoomIn class="h-3.5 w-3.5" />
73
- </IconButton>
228
+ <div
229
+ class={[
230
+ 'flex h-full min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-surface',
231
+ className
232
+ ]}
233
+ >
234
+ <div class="flex shrink-0 items-center gap-2 border-b border-border px-2 py-1">
235
+ {#if showTrackActions}
236
+ <Button size="xs" variant="ghost" onclick={() => onaddtrack?.('video')}>+ Video</Button>
237
+ <Button size="xs" variant="ghost" onclick={() => onaddtrack?.('audio')}>+ Audio</Button>
238
+ {/if}
239
+ <div class="ml-auto flex items-center gap-1">
240
+ <IconButton label="Zoom out" size="xs" onclick={() => setZoom(zoom / 1.25)}>
241
+ <ZoomOut class="h-3.5 w-3.5" />
242
+ </IconButton>
243
+ <IconButton label="Zoom in" size="xs" onclick={() => setZoom(zoom * 1.25)}>
244
+ <ZoomIn class="h-3.5 w-3.5" />
245
+ </IconButton>
246
+ </div>
74
247
  </div>
75
248
  <div bind:this={scroller} class="relative min-h-0 flex-1 overflow-auto">
76
- <div class="relative" style:width={`${contentWidth + (showHeaders ? 144 : 0)}px`}>
249
+ <div class="relative" style:width={`${contentWidth + (showHeaders ? headerW : 0)}px`}>
77
250
  <div class="sticky top-0 z-30 flex bg-surface">
78
251
  {#if showHeaders}
79
- <div class="sticky left-0 z-10 w-36 shrink-0 border-r border-border bg-surface-elevated"></div>
252
+ <div
253
+ class="sticky left-0 z-10 shrink-0 border-r border-border bg-surface-elevated"
254
+ style:width={`${headerW}px`}
255
+ style:height={`${rulerH}px`}
256
+ ></div>
80
257
  {/if}
81
- <TimeRuler
82
- {durationMs}
83
- {pxPerMs}
84
- onclick={(ms) => {
85
- currentTimeMs = clampMs(ms, 0, durationMs);
86
- onseek?.(currentTimeMs);
87
- }}
88
- />
258
+ <div class="relative" style:width={`${contentWidth}px`}>
259
+ <TimeRuler
260
+ {durationMs}
261
+ {pxPerMs}
262
+ {ticks}
263
+ height={rulerH}
264
+ onclick={(ms) => seekToMs(ms)}
265
+ onscrub={(ms) => seekToMs(ms)}
266
+ onscrubstart={() => (scrubbing = true)}
267
+ onscrubend={() => (scrubbing = false)}
268
+ />
269
+ <!-- Head sits on top of the time guide -->
270
+ <Playhead
271
+ x={playheadX}
272
+ variant="head"
273
+ headSize={rulerH}
274
+ height={rulerH}
275
+ dragging={scrubbing}
276
+ class="pointer-events-auto"
277
+ ondrag={(e) => seekFromClientX(e.clientX)}
278
+ ondragstart={(e) => {
279
+ scrubbing = true;
280
+ seekFromClientX(e.clientX);
281
+ }}
282
+ ondragend={() => (scrubbing = false)}
283
+ />
284
+ </div>
89
285
  </div>
90
286
 
91
- <div class="relative">
287
+ <div
288
+ bind:this={tracksEl}
289
+ class="relative"
290
+ role="presentation"
291
+ onclick={(e) => {
292
+ if (e.target === e.currentTarget) onemptyclick?.(e);
293
+ }}
294
+ >
295
+ <div
296
+ class="absolute inset-y-0 z-0"
297
+ style:left={`${showHeaders ? headerW : 0}px`}
298
+ style:width={`${contentWidth}px`}
299
+ >
300
+ <TimelineGrid {durationMs} {pxPerMs} {ticks} />
301
+ </div>
302
+
92
303
  {#each tracks as track (track.id)}
93
- <TimelineTrack
94
- {track}
95
- {pxPerMs}
96
- {durationMs}
97
- {selectedClipId}
98
- onselectclip={onselectclip}
99
- onchangeclip={onchangeclip}
304
+ {@const isDragging = dragTrackId === track.id}
305
+ <div
306
+ data-track-row
307
+ data-track-id={track.id}
308
+ class={[
309
+ 'relative',
310
+ isDragging &&
311
+ 'rounded-md border border-dashed border-brand-400 bg-brand-50/50 dark:bg-brand-950/30'
312
+ ]}
313
+ style:height={isDragging ? `${dragTrackHeight}px` : undefined}
100
314
  >
101
- {#snippet header()}
102
- {#if showHeaders}
103
- {#if headerVariant === 'video' && onchangetrack}
104
- <VideoTrackHeader {track} onchange={onchangetrack} />
105
- {:else if headerVariant === 'audio' && onchangetrack}
106
- <TrackHeader {track} onchange={onchangetrack} />
107
- {:else}
108
- <span class="truncate text-xs font-medium text-secondary">{track.name}</span>
109
- {/if}
110
- {/if}
111
- {/snippet}
112
- </TimelineTrack>
315
+ {#if isDragging}
316
+ <div
317
+ class="flex h-full items-center gap-2 px-3"
318
+ style:padding-left={showHeaders ? undefined : undefined}
319
+ >
320
+ <div class="sticky left-0 flex items-center gap-2" style:width={`${headerW}px`}>
321
+ <span class="text-[10px] font-medium text-brand-600 dark:text-brand-400"
322
+ >Drop here</span
323
+ >
324
+ </div>
325
+ </div>
326
+ {:else}
327
+ <TimelineTrack
328
+ {track}
329
+ {pxPerMs}
330
+ {durationMs}
331
+ selectedClipIds={resolvedSelected}
332
+ {snapToMs}
333
+ headerWidth={headerW}
334
+ onselectclip={onselectclip}
335
+ oncontextmenuclip={oncontextmenuclip}
336
+ oncontextmenutrack={oncontextmenutrack}
337
+ onchangeclip={onchangeclip}
338
+ {onmoveclips}
339
+ {ondropclip}
340
+ {ondroptrack}
341
+ {onemptyclick}
342
+ >
343
+ {#snippet header()}
344
+ {#if showHeaders}
345
+ {#if headerVariant === 'video' && onchangetrack}
346
+ <VideoTrackHeader
347
+ {track}
348
+ onchange={onchangetrack}
349
+ draggable={reorderTracks}
350
+ canRemove={allowRemoveTracks && tracks.length > 1}
351
+ onremove={onremovetrack}
352
+ onreorderstart={startTrackReorder}
353
+ />
354
+ {:else if headerVariant === 'audio' && onchangetrack}
355
+ <TrackHeader {track} onchange={onchangetrack} />
356
+ {:else}
357
+ <span class="truncate text-xs font-medium text-secondary">{track.name}</span>
358
+ {/if}
359
+ {/if}
360
+ {/snippet}
361
+ </TimelineTrack>
362
+ {/if}
363
+ </div>
113
364
  {/each}
114
365
 
366
+ <MarqueeRect rect={marquee} />
367
+
368
+ <!-- Needle continues through the tracks -->
115
369
  <div
116
- class="pointer-events-none absolute inset-y-0"
117
- style:left={`${showHeaders ? 144 : 0}px`}
370
+ class="pointer-events-none absolute inset-y-0 z-20"
371
+ style:left={`${showHeaders ? headerW : 0}px`}
118
372
  style:width={`${contentWidth}px`}
119
373
  >
120
- <div class="pointer-events-auto absolute inset-y-0" style:width={`${contentWidth}px`}>
121
- <Playhead
122
- x={playheadX}
123
- ondrag={(e) => seekFromClientX(e.clientX)}
124
- ondragstart={(e) => seekFromClientX(e.clientX)}
125
- />
126
- </div>
374
+ <Playhead
375
+ x={playheadX}
376
+ variant="line"
377
+ class="pointer-events-auto"
378
+ dragging={scrubbing}
379
+ ondrag={(e) => seekFromClientX(e.clientX)}
380
+ ondragstart={(e) => {
381
+ scrubbing = true;
382
+ seekFromClientX(e.clientX);
383
+ }}
384
+ ondragend={() => (scrubbing = false)}
385
+ />
127
386
  </div>
128
387
  </div>
129
388
  </div>
130
389
  </div>
131
390
  </div>
391
+
392
+ {#if draggingTrack && dragTrackId}
393
+ <div
394
+ class="pointer-events-none fixed z-[200] overflow-hidden rounded-lg border border-brand-400 bg-surface-elevated shadow-xl ring-2 ring-brand-500/25"
395
+ style:left={`${floatPos.x}px`}
396
+ style:top={`${floatPos.y}px`}
397
+ style:width={`${floatPos.w}px`}
398
+ style:height={`${dragTrackHeight}px`}
399
+ aria-hidden="true"
400
+ >
401
+ <div class="flex h-full items-center border-b border-border/60 bg-surface-elevated">
402
+ <div
403
+ class="flex h-full shrink-0 items-center gap-1.5 border-r border-border px-1.5"
404
+ style:width={`${headerW}px`}
405
+ >
406
+ <span class="flex h-6 w-5 items-center justify-center text-muted">
407
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
408
+ <circle cx="9" cy="7" r="1.5" />
409
+ <circle cx="15" cy="7" r="1.5" />
410
+ <circle cx="9" cy="12" r="1.5" />
411
+ <circle cx="15" cy="12" r="1.5" />
412
+ <circle cx="9" cy="17" r="1.5" />
413
+ <circle cx="15" cy="17" r="1.5" />
414
+ </svg>
415
+ </span>
416
+ <span
417
+ class="h-2 w-2 shrink-0 rounded-full"
418
+ style:background={draggingTrack.color ??
419
+ (draggingTrack.kind === 'audio' ? '#10b981' : '#3b82f6')}
420
+ ></span>
421
+ <span class="min-w-0 flex-1 truncate text-xs font-semibold text-primary"
422
+ >{draggingTrack.name}</span
423
+ >
424
+ </div>
425
+ <div class="relative min-h-0 min-w-0 flex-1 self-stretch bg-surface/80">
426
+ {#each draggingTrack.clips as clip (clip.id)}
427
+ {@const left = msToPx(clip.startMs, pxPerMs)}
428
+ {@const w = Math.max(8, msToPx(clip.endMs - clip.startMs, pxPerMs))}
429
+ <div
430
+ class="absolute top-1 bottom-1 overflow-hidden rounded-md border border-black/20 text-[10px] text-white"
431
+ style:left={`${left}px`}
432
+ style:width={`${w}px`}
433
+ style:background={clip.color ?? draggingTrack.color ?? '#3b82f6'}
434
+ >
435
+ <span class="block truncate px-1.5 py-0.5 font-medium">{clip.name}</span>
436
+ </div>
437
+ {/each}
438
+ </div>
439
+ </div>
440
+ </div>
441
+ {/if}
@@ -1,18 +1,38 @@
1
- import type { MediaClip, MediaTrack } from '../../../utils/mediaTracks.js';
1
+ import type { MediaClip, MediaTrack, MediaTrackKind } from '../../../utils/mediaTracks.js';
2
2
  interface MediaTimelineProps {
3
3
  tracks?: MediaTrack[];
4
4
  durationMs?: number;
5
5
  currentTimeMs?: number;
6
6
  zoom?: number;
7
+ selectedClipIds?: string[];
8
+ /** @deprecated use selectedClipIds */
7
9
  selectedClipId?: string | null;
8
10
  showHeaders?: boolean;
9
11
  headerVariant?: 'video' | 'audio' | 'plain';
12
+ snap?: boolean;
13
+ showTrackActions?: boolean;
14
+ /** Allow drag-reorder of tracks via header grip */
15
+ reorderTracks?: boolean;
16
+ /** Show delete control on video track headers */
17
+ allowRemoveTracks?: boolean;
10
18
  class?: string;
11
19
  onseek?: (ms: number) => void;
12
- onselectclip?: (clipId: string) => void;
20
+ onselectclip?: (clipId: string, e: MouseEvent) => void;
21
+ oncontextmenuclip?: (clipId: string, e: MouseEvent) => void;
22
+ oncontextmenutrack?: (trackId: string, e: MouseEvent) => void;
13
23
  onchangeclip?: (clip: MediaClip) => void;
24
+ onmoveclips?: (clipIds: string[], deltaMs: number) => void;
25
+ ondropclip?: (clipId: string, e: DragEvent) => void;
26
+ ondroptrack?: (trackId: string, e: DragEvent, timeMs: number) => void;
14
27
  onchangetrack?: (track: MediaTrack) => void;
15
28
  onzoomchange?: (zoom: number) => void;
29
+ onaddtrack?: (kind: MediaTrackKind) => void;
30
+ onremovetrack?: (trackId: string) => void;
31
+ /** Fired with new track id order after drag reorder */
32
+ onreordertracks?: (orderedIds: string[]) => void;
33
+ onemptyclick?: (e: MouseEvent) => void;
34
+ /** Replace / add / toggle selection from marquee */
35
+ onselectionchange?: (clipIds: string[]) => void;
16
36
  }
17
37
  declare const MediaTimeline: import("svelte").Component<MediaTimelineProps, {}, "zoom" | "currentTimeMs">;
18
38
  type MediaTimeline = ReturnType<typeof MediaTimeline>;
@@ -15,6 +15,6 @@ interface PdfViewerProps {
15
15
  /** Custom page body for the active page. Receives page index (1-based). */
16
16
  children?: Snippet<[number]>;
17
17
  }
18
- declare const PdfViewer: import("svelte").Component<PdfViewerProps, {}, "page" | "zoom">;
18
+ declare const PdfViewer: import("svelte").Component<PdfViewerProps, {}, "zoom" | "page">;
19
19
  type PdfViewer = ReturnType<typeof PdfViewer>;
20
20
  export default PdfViewer;