@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.
- package/README.md +10 -0
- package/dist/components/atoms/Playhead/Playhead.svelte +44 -5
- package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
- package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
- package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
- package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
- package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
- package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
- package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
- package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
- package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
- package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
- package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
- package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -2
- package/dist/styles.css +237 -0
- package/dist/utils/mediaTime.d.ts +14 -4
- package/dist/utils/mediaTime.js +27 -8
- package/dist/utils/mediaTracks.d.ts +154 -0
- package/dist/utils/mediaTracks.js +697 -1
- package/dist/utils/videoSequence.js +35 -4
- 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
|
|
9
|
-
import {
|
|
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 ?
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 ?
|
|
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
|
|
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
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
{#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 ?
|
|
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
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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, {}, "
|
|
18
|
+
declare const PdfViewer: import("svelte").Component<PdfViewerProps, {}, "zoom" | "page">;
|
|
19
19
|
type PdfViewer = ReturnType<typeof PdfViewer>;
|
|
20
20
|
export default PdfViewer;
|