@r2digisolutions/components 0.2.0 → 0.3.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.
Files changed (63) hide show
  1. package/dist/components/atoms/Playhead/Playhead.svelte +44 -5
  2. package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
  3. package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
  4. package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
  5. package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
  6. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
  7. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
  8. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
  9. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
  10. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
  11. package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
  12. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
  13. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
  14. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
  15. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
  16. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
  17. package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
  18. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
  19. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
  20. package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
  21. package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
  22. package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
  23. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
  24. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
  25. package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
  26. package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
  27. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
  28. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
  29. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
  30. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
  31. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
  32. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
  33. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
  34. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
  35. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
  36. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
  37. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
  38. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
  39. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
  40. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
  41. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
  42. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
  43. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
  44. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
  45. package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
  46. package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
  47. package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
  48. package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
  49. package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
  50. package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
  51. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
  52. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
  53. package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
  54. package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
  55. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
  56. package/dist/index.d.ts +11 -3
  57. package/dist/index.js +6 -2
  58. package/dist/utils/mediaTime.d.ts +14 -4
  59. package/dist/utils/mediaTime.js +27 -8
  60. package/dist/utils/mediaTracks.d.ts +154 -0
  61. package/dist/utils/mediaTracks.js +697 -1
  62. package/dist/utils/videoSequence.js +35 -4
  63. package/package.json +1 -1
@@ -6,54 +6,107 @@
6
6
  import Lock from '@lucide/svelte/icons/lock';
7
7
  import LockOpen from '@lucide/svelte/icons/lock-open';
8
8
  import Headphones from '@lucide/svelte/icons/headphones';
9
+ import GripVertical from '@lucide/svelte/icons/grip-vertical';
10
+ import Trash2 from '@lucide/svelte/icons/trash-2';
9
11
 
10
12
  interface VideoTrackHeaderProps {
11
13
  track: MediaTrack;
12
14
  class?: string;
15
+ draggable?: boolean;
16
+ canRemove?: boolean;
17
+ dragging?: boolean;
13
18
  onchange?: (track: MediaTrack) => void;
19
+ onremove?: (trackId: string) => void;
20
+ /** Pointer-based reorder (preferred over HTML5 drag) */
21
+ onreorderstart?: (trackId: string, e: PointerEvent) => void;
14
22
  }
15
23
 
16
- let { track, class: className = '', onchange }: VideoTrackHeaderProps = $props();
24
+ let {
25
+ track,
26
+ class: className = '',
27
+ draggable = false,
28
+ canRemove = false,
29
+ dragging = false,
30
+ onchange,
31
+ onremove,
32
+ onreorderstart
33
+ }: VideoTrackHeaderProps = $props();
17
34
 
18
35
  function patch(partial: Partial<MediaTrack>) {
19
36
  onchange?.({ ...track, ...partial });
20
37
  }
21
38
  </script>
22
39
 
23
- <div class={['flex min-w-0 flex-1 items-center gap-1', className]}>
40
+ <div class={['flex min-w-0 flex-1 items-center gap-1', className, dragging && 'pointer-events-none']}>
41
+ {#if draggable}
42
+ <button
43
+ type="button"
44
+ class="flex h-6 w-5 shrink-0 cursor-grab touch-none items-center justify-center rounded text-muted hover:bg-surface-overlay hover:text-secondary active:cursor-grabbing"
45
+ aria-label={`Reorder ${track.name}`}
46
+ title="Drag to reorder"
47
+ data-marquee-ignore
48
+ onpointerdown={(e) => {
49
+ if (e.button !== 0) return;
50
+ e.preventDefault();
51
+ e.stopPropagation();
52
+ onreorderstart?.(track.id, e);
53
+ }}
54
+ >
55
+ <GripVertical class="h-3.5 w-3.5" />
56
+ </button>
57
+ {/if}
24
58
  <span
25
59
  class="h-2 w-2 shrink-0 rounded-full"
26
60
  style:background={track.color ?? (track.kind === 'audio' ? '#10b981' : '#3b82f6')}
27
61
  aria-hidden="true"
28
62
  ></span>
29
- <span class="min-w-0 flex-1 truncate text-xs font-medium text-secondary">{track.name}</span>
30
- <IconButton
31
- label={track.muted ? 'Unmute' : 'Mute'}
32
- size="xs"
33
- onclick={() => patch({ muted: !track.muted })}
34
- >
35
- {#if track.muted}
36
- <VolumeX class="h-3.5 w-3.5" />
37
- {:else}
38
- <Volume2 class="h-3.5 w-3.5" />
39
- {/if}
40
- </IconButton>
41
- <IconButton
42
- label={track.solo ? 'Unsolo' : 'Solo'}
43
- size="xs"
44
- onclick={() => patch({ solo: !track.solo })}
45
- >
46
- <Headphones class={['h-3.5 w-3.5', track.solo && 'text-brand-500']} />
47
- </IconButton>
48
- <IconButton
49
- label={track.locked ? 'Unlock' : 'Lock'}
50
- size="xs"
51
- onclick={() => patch({ locked: !track.locked })}
52
- >
53
- {#if track.locked}
54
- <Lock class="h-3.5 w-3.5" />
55
- {:else}
56
- <LockOpen class="h-3.5 w-3.5" />
57
- {/if}
58
- </IconButton>
63
+ <span class="min-w-0 flex-1 truncate text-xs font-semibold text-primary" title={track.name}>
64
+ {track.name}
65
+ </span>
66
+ {#if !dragging}
67
+ <div class="flex shrink-0 items-center">
68
+ <IconButton
69
+ label={track.muted ? 'Unmute' : 'Mute'}
70
+ size="xs"
71
+ class="!h-5 !w-5"
72
+ onclick={() => patch({ muted: !track.muted })}
73
+ >
74
+ {#if track.muted}
75
+ <VolumeX class="h-3 w-3" />
76
+ {:else}
77
+ <Volume2 class="h-3 w-3" />
78
+ {/if}
79
+ </IconButton>
80
+ <IconButton
81
+ label={track.solo ? 'Unsolo' : 'Solo'}
82
+ size="xs"
83
+ class="!h-5 !w-5"
84
+ onclick={() => patch({ solo: !track.solo })}
85
+ >
86
+ <Headphones class={['h-3 w-3', track.solo && 'text-brand-500']} />
87
+ </IconButton>
88
+ <IconButton
89
+ label={track.locked ? 'Unlock' : 'Lock'}
90
+ size="xs"
91
+ class="!h-5 !w-5"
92
+ onclick={() => patch({ locked: !track.locked })}
93
+ >
94
+ {#if track.locked}
95
+ <Lock class="h-3 w-3" />
96
+ {:else}
97
+ <LockOpen class="h-3 w-3" />
98
+ {/if}
99
+ </IconButton>
100
+ {#if canRemove && onremove}
101
+ <IconButton
102
+ label="Delete track"
103
+ size="xs"
104
+ class="!h-5 !w-5"
105
+ onclick={() => onremove(track.id)}
106
+ >
107
+ <Trash2 class="h-3 w-3" />
108
+ </IconButton>
109
+ {/if}
110
+ </div>
111
+ {/if}
59
112
  </div>
@@ -2,7 +2,13 @@ import type { MediaTrack } from '../../../utils/mediaTracks.js';
2
2
  interface VideoTrackHeaderProps {
3
3
  track: MediaTrack;
4
4
  class?: string;
5
+ draggable?: boolean;
6
+ canRemove?: boolean;
7
+ dragging?: boolean;
5
8
  onchange?: (track: MediaTrack) => void;
9
+ onremove?: (trackId: string) => void;
10
+ /** Pointer-based reorder (preferred over HTML5 drag) */
11
+ onreorderstart?: (trackId: string, e: PointerEvent) => void;
6
12
  }
7
13
  declare const VideoTrackHeader: import("svelte").Component<VideoTrackHeaderProps, {}, "">;
8
14
  type VideoTrackHeader = ReturnType<typeof VideoTrackHeader>;
@@ -0,0 +1,8 @@
1
+ import VideoTransitionsPanelStory from './VideoTransitionsPanelStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/VideoTransitionsPanel',
4
+ component: VideoTransitionsPanelStory,
5
+ tags: ['autodocs']
6
+ };
7
+ export default meta;
8
+ export const Default = {};
@@ -0,0 +1,113 @@
1
+ <script lang="ts" module>
2
+ export interface VideoTransitionDef {
3
+ id: string;
4
+ label: string;
5
+ group: string;
6
+ type: string;
7
+ durationMs: number;
8
+ glyph?: string;
9
+ }
10
+
11
+ export const VIDEO_TRANSITION_MIME = 'application/x-r2-video-transition';
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ interface VideoTransitionsPanelProps {
16
+ class?: string;
17
+ disabled?: boolean;
18
+ hint?: string;
19
+ onapply?: (def: VideoTransitionDef) => void;
20
+ }
21
+
22
+ let {
23
+ class: className = '',
24
+ disabled = false,
25
+ hint = 'Select a clip, or drag onto its end',
26
+ onapply
27
+ }: VideoTransitionsPanelProps = $props();
28
+
29
+ const transitions: VideoTransitionDef[] = [
30
+ /* Dissolve */
31
+ { id: 'crossfade', label: 'Crossfade', group: 'Dissolve', type: 'crossfade', durationMs: 900, glyph: '╳' },
32
+ { id: 'dip-black', label: 'Dip black', group: 'Dissolve', type: 'dip-black', durationMs: 1000, glyph: '■' },
33
+ { id: 'dip-white', label: 'Dip white', group: 'Dissolve', type: 'dip-white', durationMs: 1000, glyph: '□' },
34
+ { id: 'fade-soft', label: 'Soft fade', group: 'Dissolve', type: 'fade-soft', durationMs: 1200, glyph: '≈' },
35
+ { id: 'blur-dissolve', label: 'Blur', group: 'Dissolve', type: 'blur', durationMs: 1000, glyph: '◈' },
36
+ { id: 'flash', label: 'Flash', group: 'Dissolve', type: 'flash', durationMs: 500, glyph: '⚡' },
37
+ /* Wipe */
38
+ { id: 'wipe-left', label: 'Wipe left', group: 'Wipe', type: 'wipe-left', durationMs: 900, glyph: '◀' },
39
+ { id: 'wipe-right', label: 'Wipe right', group: 'Wipe', type: 'wipe-right', durationMs: 900, glyph: '▶' },
40
+ { id: 'wipe-up', label: 'Wipe up', group: 'Wipe', type: 'wipe-up', durationMs: 900, glyph: '▲' },
41
+ { id: 'wipe-down', label: 'Wipe down', group: 'Wipe', type: 'wipe-down', durationMs: 900, glyph: '▼' },
42
+ { id: 'wipe-diagonal', label: 'Diagonal', group: 'Wipe', type: 'wipe-diagonal', durationMs: 900, glyph: '╱' },
43
+ { id: 'iris', label: 'Iris', group: 'Wipe', type: 'iris', durationMs: 900, glyph: '◎' },
44
+ { id: 'barn-doors', label: 'Barn doors', group: 'Wipe', type: 'barn-doors', durationMs: 900, glyph: ' ⟺ ' },
45
+ /* Move */
46
+ { id: 'push-left', label: 'Push left', group: 'Move', type: 'push-left', durationMs: 900, glyph: '⇇' },
47
+ { id: 'push-right', label: 'Push right', group: 'Move', type: 'push-right', durationMs: 900, glyph: '⇉' },
48
+ { id: 'push-up', label: 'Push up', group: 'Move', type: 'push-up', durationMs: 900, glyph: '⇈' },
49
+ { id: 'push-down', label: 'Push down', group: 'Move', type: 'push-down', durationMs: 900, glyph: '⇊' },
50
+ { id: 'slide-left', label: 'Slide left', group: 'Move', type: 'slide-left', durationMs: 900, glyph: '◁' },
51
+ { id: 'slide-right', label: 'Slide right', group: 'Move', type: 'slide-right', durationMs: 900, glyph: '▷' },
52
+ /* Style */
53
+ { id: 'zoom-through', label: 'Zoom in', group: 'Style', type: 'zoom-in', durationMs: 900, glyph: '⊕' },
54
+ { id: 'zoom-out', label: 'Zoom out', group: 'Style', type: 'zoom-out', durationMs: 900, glyph: '⊖' },
55
+ { id: 'spin', label: 'Spin', group: 'Style', type: 'spin', durationMs: 900, glyph: '↻' },
56
+ { id: 'flip-x', label: 'Flip H', group: 'Style', type: 'flip-x', durationMs: 900, glyph: '↔' },
57
+ { id: 'flip-y', label: 'Flip V', group: 'Style', type: 'flip-y', durationMs: 900, glyph: '↕' },
58
+ { id: 'squeeze-h', label: 'Squeeze H', group: 'Style', type: 'squeeze-h', durationMs: 800, glyph: '↔︎' },
59
+ { id: 'squeeze-v', label: 'Squeeze V', group: 'Style', type: 'squeeze-v', durationMs: 800, glyph: '↕︎' }
60
+ ];
61
+
62
+ const groups = $derived(
63
+ [...new Set(transitions.map((t) => t.group))].map((group) => ({
64
+ group,
65
+ items: transitions.filter((t) => t.group === group)
66
+ }))
67
+ );
68
+
69
+ function onDragStart(e: DragEvent, item: VideoTransitionDef) {
70
+ if (disabled) {
71
+ e.preventDefault();
72
+ return;
73
+ }
74
+ e.dataTransfer?.setData(VIDEO_TRANSITION_MIME, JSON.stringify(item));
75
+ e.dataTransfer?.setData('text/plain', item.label);
76
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'copy';
77
+ }
78
+ </script>
79
+
80
+ <div class={['flex flex-col gap-4 p-2', className]}>
81
+ {#if disabled}
82
+ <p class="px-1 py-4 text-center text-xs text-muted">{hint}</p>
83
+ {:else}
84
+ <p class="px-1 text-[10px] leading-relaxed text-muted">
85
+ CapCut-style: transition sits at the <strong class="font-medium text-secondary">end</strong> of
86
+ a clip (into the next). Drag onto a clip or click with one selected.
87
+ </p>
88
+ {/if}
89
+ {#each groups as { group, items } (group)}
90
+ <section class="flex flex-col gap-2">
91
+ <h3 class="px-1 text-[10px] font-semibold uppercase tracking-wide text-muted">{group}</h3>
92
+ <div class="grid grid-cols-3 gap-1.5">
93
+ {#each items as item (item.id)}
94
+ <button
95
+ type="button"
96
+ disabled={disabled}
97
+ draggable={!disabled}
98
+ ondragstart={(e) => onDragStart(e, item)}
99
+ class="flex cursor-grab flex-col items-center gap-1.5 rounded-lg border border-border bg-surface-elevated px-1.5 py-2 text-center transition-colors enabled:hover:border-brand-500/50 enabled:hover:bg-surface-overlay active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-50"
100
+ onclick={() => onapply?.(item)}
101
+ >
102
+ <span
103
+ class="flex h-9 w-full items-center justify-center rounded-md bg-surface text-sm font-semibold text-secondary"
104
+ >
105
+ {item.glyph ?? item.label.slice(0, 2)}
106
+ </span>
107
+ <span class="text-[10px] font-medium text-secondary">{item.label}</span>
108
+ </button>
109
+ {/each}
110
+ </div>
111
+ </section>
112
+ {/each}
113
+ </div>
@@ -0,0 +1,18 @@
1
+ export interface VideoTransitionDef {
2
+ id: string;
3
+ label: string;
4
+ group: string;
5
+ type: string;
6
+ durationMs: number;
7
+ glyph?: string;
8
+ }
9
+ export declare const VIDEO_TRANSITION_MIME = "application/x-r2-video-transition";
10
+ interface VideoTransitionsPanelProps {
11
+ class?: string;
12
+ disabled?: boolean;
13
+ hint?: string;
14
+ onapply?: (def: VideoTransitionDef) => void;
15
+ }
16
+ declare const VideoTransitionsPanel: import("svelte").Component<VideoTransitionsPanelProps, {}, "">;
17
+ type VideoTransitionsPanel = ReturnType<typeof VideoTransitionsPanel>;
18
+ export default VideoTransitionsPanel;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import VideoTransitionsPanel from './VideoTransitionsPanel.svelte';
3
+ </script>
4
+
5
+ <div class="h-96 w-72 rounded-lg border border-border bg-surface">
6
+ <VideoTransitionsPanel onapply={(d) => console.log('trans', d)} />
7
+ </div>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const VideoTransitionsPanelStory: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type VideoTransitionsPanelStory = InstanceType<typeof VideoTransitionsPanelStory>;
18
+ export default VideoTransitionsPanelStory;
@@ -41,6 +41,10 @@
41
41
  let selectedClipId = $state<string | null>(null);
42
42
  let currentTimeMs = $state(0);
43
43
  let playing = $state(false);
44
+ let playbackRate = $state(1);
45
+ let muted = $state(false);
46
+ let volume = $state(1);
47
+ const SKIP_MS = 5000;
44
48
  let zoom = $state(1);
45
49
  let showSidebar = $state(true);
46
50
  let showInspector = $state(true);
@@ -114,10 +118,14 @@
114
118
  });
115
119
  }
116
120
 
121
+ function seekBy(deltaMs: number) {
122
+ currentTimeMs = clampMs(currentTimeMs + deltaMs, 0, value.durationMs);
123
+ }
124
+
117
125
  function tick(ts: number) {
118
126
  if (!playing) return;
119
127
  if (lastTs) {
120
- const delta = ts - lastTs;
128
+ const delta = (ts - lastTs) * playbackRate;
121
129
  currentTimeMs = clampMs(currentTimeMs + delta, 0, value.durationMs);
122
130
  if (currentTimeMs >= value.durationMs) {
123
131
  playing = false;
@@ -145,6 +153,36 @@
145
153
  playing = !playing;
146
154
  return;
147
155
  }
156
+ if (e.key === 'Escape') {
157
+ e.preventDefault();
158
+ selectedClipId = null;
159
+ return;
160
+ }
161
+ if (e.key === 'ArrowLeft') {
162
+ e.preventDefault();
163
+ seekBy(e.shiftKey ? -SKIP_MS : -1000);
164
+ return;
165
+ }
166
+ if (e.key === 'ArrowRight') {
167
+ e.preventDefault();
168
+ seekBy(e.shiftKey ? SKIP_MS : 1000);
169
+ return;
170
+ }
171
+ if (e.key === 'Home') {
172
+ e.preventDefault();
173
+ currentTimeMs = 0;
174
+ return;
175
+ }
176
+ if (e.key === 'End') {
177
+ e.preventDefault();
178
+ currentTimeMs = value.durationMs;
179
+ return;
180
+ }
181
+ if (e.key === 'm' || e.key === 'M') {
182
+ e.preventDefault();
183
+ muted = !muted;
184
+ return;
185
+ }
148
186
  if ((e.key === 'Delete' || e.key === 'Backspace') && selectedClipId) {
149
187
  e.preventDefault();
150
188
  setTracks(removeClipFromTracks(value.tracks, selectedClipId));
@@ -231,12 +269,24 @@
231
269
  {playing}
232
270
  {currentTimeMs}
233
271
  durationMs={value.durationMs}
272
+ bind:playbackRate
273
+ bind:muted
274
+ bind:volume
275
+ skipMs={SKIP_MS}
234
276
  onplay={() => (playing = true)}
235
277
  onpause={() => (playing = false)}
236
278
  onstop={() => {
237
279
  playing = false;
238
280
  currentTimeMs = 0;
239
281
  }}
282
+ onseekstart={() => {
283
+ currentTimeMs = 0;
284
+ }}
285
+ onseekend={() => {
286
+ currentTimeMs = value.durationMs;
287
+ }}
288
+ onseekback={() => seekBy(-SKIP_MS)}
289
+ onseekforward={() => seekBy(SKIP_MS)}
240
290
  />
241
291
  <div class="min-h-0 flex-1">
242
292
  <MediaTimeline
@@ -247,6 +297,7 @@
247
297
  {selectedClipId}
248
298
  headerVariant="audio"
249
299
  onselectclip={(id) => (selectedClipId = id)}
300
+ onemptyclick={() => (selectedClipId = null)}
250
301
  onchangeclip={patchClip}
251
302
  onchangetrack={patchTrack}
252
303
  onseek={(ms) => (currentTimeMs = ms)}
@@ -127,104 +127,114 @@
127
127
  </div>
128
128
 
129
129
  {#snippet mainAndInspector()}
130
- {#if showInspector}
131
- <SplitPane
132
- orientation="horizontal"
133
- bind:size={inspectorSize}
134
- sizePane="end"
135
- unit={sizeUnit}
136
- minSize={inspectorMin}
137
- maxSize={inspectorMax}
138
- endCollapsed={wrapInspector && inspectorCollapsed}
139
- revealOnHover={revealResizeOnHover}
140
- class="h-full"
141
- >
142
- {#snippet start()}
143
- {@render centerColumn()}
144
- {/snippet}
145
- {#snippet end()}
146
- {#if wrapInspector}
147
- <Panel
148
- title={inspectorTitle}
149
- padding="none"
150
- collapsible={collapsiblePanels}
151
- bind:collapsed={inspectorCollapsed}
152
- >
153
- {#if inspector}
130
+ <div class="h-full min-h-0 min-w-0">
131
+ {#if showInspector}
132
+ <SplitPane
133
+ orientation="horizontal"
134
+ bind:size={inspectorSize}
135
+ sizePane="end"
136
+ unit={sizeUnit}
137
+ minSize={inspectorMin}
138
+ maxSize={inspectorMax}
139
+ endCollapsed={wrapInspector && inspectorCollapsed}
140
+ revealOnHover={revealResizeOnHover}
141
+ class="h-full"
142
+ >
143
+ {#snippet start()}
144
+ {@render centerColumn()}
145
+ {/snippet}
146
+ {#snippet end()}
147
+ {#if wrapInspector}
148
+ <Panel
149
+ title={inspectorTitle}
150
+ padding="none"
151
+ collapsible={collapsiblePanels}
152
+ bind:collapsed={inspectorCollapsed}
153
+ >
154
+ {#if inspector}
155
+ {@render inspector()}
156
+ {/if}
157
+ </Panel>
158
+ {:else if inspector}
159
+ <div class="h-full min-h-0 min-w-0">
154
160
  {@render inspector()}
155
- {/if}
156
- </Panel>
157
- {:else if inspector}
158
- {@render inspector()}
159
- {/if}
160
- {/snippet}
161
- </SplitPane>
162
- {:else}
163
- {@render centerColumn()}
164
- {/if}
161
+ </div>
162
+ {/if}
163
+ {/snippet}
164
+ </SplitPane>
165
+ {:else}
166
+ {@render centerColumn()}
167
+ {/if}
168
+ </div>
165
169
  {/snippet}
166
170
 
167
171
  {#snippet centerColumn()}
168
- {#if showBottom}
169
- <SplitPane
170
- orientation="vertical"
171
- bind:size={bottomSize}
172
- sizePane="end"
173
- unit={sizeUnit}
174
- minSize={bottomMin}
175
- maxSize={bottomMax}
176
- startCollapsed={wrapWorkspace && workspaceCollapsed}
177
- endCollapsed={wrapBottom && bottomCollapsed}
178
- revealOnHover={revealResizeOnHover}
179
- class="h-full"
180
- >
181
- {#snippet start()}
182
- {#if wrapWorkspace}
183
- <Panel
184
- title={workspaceTitle}
185
- padding="none"
186
- collapsible={collapsiblePanels}
187
- bind:collapsed={workspaceCollapsed}
188
- >
189
- {#if workspace}
172
+ <div class="h-full min-h-0 min-w-0">
173
+ {#if showBottom}
174
+ <SplitPane
175
+ orientation="vertical"
176
+ bind:size={bottomSize}
177
+ sizePane="end"
178
+ unit={sizeUnit}
179
+ minSize={bottomMin}
180
+ maxSize={bottomMax}
181
+ startCollapsed={wrapWorkspace && workspaceCollapsed}
182
+ endCollapsed={wrapBottom && bottomCollapsed}
183
+ revealOnHover={revealResizeOnHover}
184
+ class="h-full"
185
+ >
186
+ {#snippet start()}
187
+ {#if wrapWorkspace}
188
+ <Panel
189
+ title={workspaceTitle}
190
+ padding="none"
191
+ collapsible={collapsiblePanels}
192
+ bind:collapsed={workspaceCollapsed}
193
+ >
194
+ {#if workspace}
195
+ {@render workspace()}
196
+ {/if}
197
+ </Panel>
198
+ {:else if workspace}
199
+ <div class="h-full min-h-0 min-w-0">
190
200
  {@render workspace()}
191
- {/if}
192
- </Panel>
193
- {:else if workspace}
194
- {@render workspace()}
195
- {/if}
196
- {/snippet}
197
- {#snippet end()}
198
- {#if wrapBottom}
199
- <Panel
200
- title={bottomTitle}
201
- padding="none"
202
- collapsible={collapsiblePanels}
203
- bind:collapsed={bottomCollapsed}
204
- >
205
- {#if bottom}
201
+ </div>
202
+ {/if}
203
+ {/snippet}
204
+ {#snippet end()}
205
+ {#if wrapBottom}
206
+ <Panel
207
+ title={bottomTitle}
208
+ padding="none"
209
+ collapsible={collapsiblePanels}
210
+ bind:collapsed={bottomCollapsed}
211
+ >
212
+ {#if bottom}
213
+ {@render bottom()}
214
+ {/if}
215
+ </Panel>
216
+ {:else if bottom}
217
+ <div class="h-full min-h-0 min-w-0">
206
218
  {@render bottom()}
207
- {/if}
208
- </Panel>
209
- {:else if bottom}
210
- {@render bottom()}
219
+ </div>
220
+ {/if}
221
+ {/snippet}
222
+ </SplitPane>
223
+ {:else if wrapWorkspace}
224
+ <Panel
225
+ title={workspaceTitle}
226
+ padding="none"
227
+ collapsible={collapsiblePanels}
228
+ bind:collapsed={workspaceCollapsed}
229
+ >
230
+ {#if workspace}
231
+ {@render workspace()}
211
232
  {/if}
212
- {/snippet}
213
- </SplitPane>
214
- {:else if wrapWorkspace}
215
- <Panel
216
- title={workspaceTitle}
217
- padding="none"
218
- collapsible={collapsiblePanels}
219
- bind:collapsed={workspaceCollapsed}
220
- >
221
- {#if workspace}
233
+ </Panel>
234
+ {:else if workspace}
235
+ <div class="h-full min-h-0 min-w-0">
222
236
  {@render workspace()}
223
- {/if}
224
- </Panel>
225
- {:else if workspace}
226
- <div class="h-full min-h-0 min-w-0">
227
- {@render workspace()}
228
- </div>
229
- {/if}
237
+ </div>
238
+ {/if}
239
+ </div>
230
240
  {/snippet}
@@ -7,9 +7,12 @@ declare const meta: {
7
7
  accept?: string;
8
8
  variant?: "multiple" | "single" | "avatar";
9
9
  view?: "list" | "grid";
10
+ layout?: "horizontal" | "vertical" | "compact";
10
11
  showViewToggle?: boolean;
11
12
  maxSizeMb?: number;
12
13
  disabled?: boolean;
14
+ src?: string;
15
+ srcName?: string;
13
16
  }, {}, "">;
14
17
  tags: string[];
15
18
  argTypes: {
@@ -30,6 +33,10 @@ declare const meta: {
30
33
  control: "select";
31
34
  options: string[];
32
35
  };
36
+ layout: {
37
+ control: "select";
38
+ options: string[];
39
+ };
33
40
  showViewToggle: {
34
41
  control: "boolean";
35
42
  };
@@ -46,6 +53,7 @@ declare const meta: {
46
53
  accept: string;
47
54
  variant: "multiple";
48
55
  view: "list";
56
+ layout: "horizontal";
49
57
  showViewToggle: true;
50
58
  maxSizeMb: number;
51
59
  disabled: false;
@@ -55,6 +63,8 @@ export default meta;
55
63
  type Story = StoryObj<typeof meta>;
56
64
  export declare const Multiple: Story;
57
65
  export declare const GridView: Story;
66
+ export declare const VerticalSidebar: Story;
67
+ export declare const Compact: Story;
58
68
  export declare const SingleFile: Story;
59
69
  export declare const Avatar: Story;
60
70
  export declare const Disabled: Story;