@r2digisolutions/components 0.1.7 → 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 (350) hide show
  1. package/dist/components/atoms/IconButton/IconButton.svelte +1 -1
  2. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.stories.js +8 -0
  3. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.svelte +122 -0
  4. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.svelte.d.ts +11 -0
  5. package/dist/components/atoms/MediaKindIcon/MediaKindIconStory.svelte +12 -0
  6. package/dist/components/atoms/MediaKindIcon/MediaKindIconStory.svelte.d.ts +18 -0
  7. package/dist/components/atoms/PanKnob/PanKnob.stories.d.ts +9 -0
  8. package/dist/components/atoms/PanKnob/PanKnob.stories.js +8 -0
  9. package/dist/components/atoms/PanKnob/PanKnob.svelte +218 -0
  10. package/dist/components/atoms/PanKnob/PanKnob.svelte.d.ts +19 -0
  11. package/dist/components/atoms/PanKnob/PanKnobStory.svelte +15 -0
  12. package/dist/components/atoms/PanKnob/PanKnobStory.svelte.d.ts +3 -0
  13. package/dist/components/atoms/Playhead/Playhead.stories.js +8 -0
  14. package/dist/components/atoms/Playhead/Playhead.svelte +98 -0
  15. package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +21 -0
  16. package/dist/components/atoms/Playhead/PlayheadStory.svelte +9 -0
  17. package/dist/components/atoms/Playhead/PlayheadStory.svelte.d.ts +18 -0
  18. package/dist/components/atoms/TimeRuler/TimeRuler.stories.js +8 -0
  19. package/dist/components/atoms/TimeRuler/TimeRuler.svelte +117 -0
  20. package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +17 -0
  21. package/dist/components/atoms/TimeRuler/TimeRulerStory.svelte +7 -0
  22. package/dist/components/atoms/TimeRuler/TimeRulerStory.svelte.d.ts +18 -0
  23. package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
  24. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
  25. package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
  26. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
  27. package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
  28. package/dist/components/atoms/Tooltip/Tooltip.svelte +1 -1
  29. package/dist/components/atoms/TrimHandle/TrimHandle.stories.js +8 -0
  30. package/dist/components/atoms/TrimHandle/TrimHandle.svelte +63 -0
  31. package/dist/components/atoms/TrimHandle/TrimHandle.svelte.d.ts +12 -0
  32. package/dist/components/atoms/TrimHandle/TrimHandleStory.svelte +11 -0
  33. package/dist/components/atoms/TrimHandle/TrimHandleStory.svelte.d.ts +18 -0
  34. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.stories.d.ts +9 -0
  35. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.stories.js +8 -0
  36. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.svelte +102 -0
  37. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.svelte.d.ts +20 -0
  38. package/dist/components/molecules/AiAssistPanel/AiAssistPanelStory.svelte +34 -0
  39. package/dist/components/molecules/AiAssistPanel/AiAssistPanelStory.svelte.d.ts +3 -0
  40. package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte +97 -61
  41. package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte.d.ts +2 -0
  42. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.stories.js +8 -0
  43. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.svelte +117 -0
  44. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.svelte.d.ts +23 -0
  45. package/dist/components/molecules/BulkEditPanel/BulkEditPanelStory.svelte +18 -0
  46. package/dist/components/molecules/BulkEditPanel/BulkEditPanelStory.svelte.d.ts +18 -0
  47. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.stories.js +8 -0
  48. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.svelte +858 -0
  49. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.svelte.d.ts +43 -0
  50. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanelStory.svelte +7 -0
  51. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanelStory.svelte.d.ts +18 -0
  52. package/dist/components/molecules/CanvasInspector/CanvasInspector.stories.d.ts +9 -0
  53. package/dist/components/molecules/CanvasInspector/CanvasInspector.stories.js +8 -0
  54. package/dist/components/molecules/CanvasInspector/CanvasInspector.svelte +1140 -0
  55. package/dist/components/molecules/CanvasInspector/CanvasInspector.svelte.d.ts +28 -0
  56. package/dist/components/molecules/CanvasInspector/CanvasInspectorStory.svelte +20 -0
  57. package/dist/components/molecules/CanvasInspector/CanvasInspectorStory.svelte.d.ts +3 -0
  58. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +46 -5
  59. package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
  60. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.stories.d.ts +9 -0
  61. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.stories.js +8 -0
  62. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.svelte +108 -0
  63. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.svelte.d.ts +18 -0
  64. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenuStory.svelte +15 -0
  65. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenuStory.svelte.d.ts +3 -0
  66. package/dist/components/molecules/ConflictResolver/ConflictResolver.stories.js +8 -0
  67. package/dist/components/molecules/ConflictResolver/ConflictResolver.svelte +73 -0
  68. package/dist/components/molecules/ConflictResolver/ConflictResolver.svelte.d.ts +16 -0
  69. package/dist/components/molecules/ConflictResolver/ConflictResolverStory.svelte +11 -0
  70. package/dist/components/molecules/ConflictResolver/ConflictResolverStory.svelte.d.ts +18 -0
  71. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +237 -66
  72. package/dist/components/molecules/ContextMenu/ContextMenu.svelte.d.ts +9 -0
  73. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.stories.d.ts +9 -0
  74. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.stories.js +8 -0
  75. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.svelte +56 -0
  76. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.svelte.d.ts +12 -0
  77. package/dist/components/molecules/ExpressionEditor/ExpressionEditorStory.svelte +12 -0
  78. package/dist/components/molecules/ExpressionEditor/ExpressionEditorStory.svelte.d.ts +3 -0
  79. package/dist/components/molecules/FilterList/FilterList.stories.d.ts +9 -0
  80. package/dist/components/molecules/FilterList/FilterList.stories.js +8 -0
  81. package/dist/components/molecules/FilterList/FilterList.svelte +101 -0
  82. package/dist/components/molecules/FilterList/FilterList.svelte.d.ts +18 -0
  83. package/dist/components/molecules/FilterList/FilterListStory.svelte +31 -0
  84. package/dist/components/molecules/FilterList/FilterListStory.svelte.d.ts +3 -0
  85. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.stories.d.ts +9 -0
  86. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.stories.js +8 -0
  87. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +109 -0
  88. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +20 -0
  89. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowserStory.svelte +18 -0
  90. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowserStory.svelte.d.ts +3 -0
  91. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.stories.js +8 -0
  92. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.svelte +50 -0
  93. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.svelte.d.ts +11 -0
  94. package/dist/components/molecules/MediaAssetCard/MediaAssetCardStory.svelte +13 -0
  95. package/dist/components/molecules/MediaAssetCard/MediaAssetCardStory.svelte.d.ts +18 -0
  96. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.stories.js +8 -0
  97. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.svelte +380 -0
  98. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.svelte.d.ts +33 -0
  99. package/dist/components/molecules/MediaLayerItem/MediaLayerItemStory.svelte +14 -0
  100. package/dist/components/molecules/MediaLayerItem/MediaLayerItemStory.svelte.d.ts +18 -0
  101. package/dist/components/molecules/MediaLayerList/MediaLayerList.stories.d.ts +9 -0
  102. package/dist/components/molecules/MediaLayerList/MediaLayerList.stories.js +8 -0
  103. package/dist/components/molecules/MediaLayerList/MediaLayerList.svelte +399 -0
  104. package/dist/components/molecules/MediaLayerList/MediaLayerList.svelte.d.ts +37 -0
  105. package/dist/components/molecules/MediaLayerList/MediaLayerListStory.svelte +24 -0
  106. package/dist/components/molecules/MediaLayerList/MediaLayerListStory.svelte.d.ts +3 -0
  107. package/dist/components/molecules/MediaStage/MediaStage.stories.d.ts +9 -0
  108. package/dist/components/molecules/MediaStage/MediaStage.stories.js +8 -0
  109. package/dist/components/molecules/MediaStage/MediaStage.svelte +1498 -0
  110. package/dist/components/molecules/MediaStage/MediaStage.svelte.d.ts +45 -0
  111. package/dist/components/molecules/MediaStage/MediaStageStory.svelte +35 -0
  112. package/dist/components/molecules/MediaStage/MediaStageStory.svelte.d.ts +3 -0
  113. package/dist/components/molecules/MediaTimeline/MediaTimeline.stories.d.ts +9 -0
  114. package/dist/components/molecules/MediaTimeline/MediaTimeline.stories.js +8 -0
  115. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +441 -0
  116. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +39 -0
  117. package/dist/components/molecules/MediaTimeline/MediaTimelineStory.svelte +20 -0
  118. package/dist/components/molecules/MediaTimeline/MediaTimelineStory.svelte.d.ts +3 -0
  119. package/dist/components/molecules/MixerChannel/MixerChannel.stories.d.ts +9 -0
  120. package/dist/components/molecules/MixerChannel/MixerChannel.stories.js +8 -0
  121. package/dist/components/molecules/MixerChannel/MixerChannel.svelte +92 -0
  122. package/dist/components/molecules/MixerChannel/MixerChannel.svelte.d.ts +11 -0
  123. package/dist/components/molecules/MixerChannel/MixerChannelStory.svelte +13 -0
  124. package/dist/components/molecules/MixerChannel/MixerChannelStory.svelte.d.ts +3 -0
  125. package/dist/components/molecules/NodeGraph/GraphBlueprintNode.svelte +454 -0
  126. package/dist/components/molecules/NodeGraph/GraphBlueprintNode.svelte.d.ts +8 -0
  127. package/dist/components/molecules/NodeGraph/GraphFlowBridge.svelte +26 -0
  128. package/dist/components/molecules/NodeGraph/GraphFlowBridge.svelte.d.ts +22 -0
  129. package/dist/components/molecules/NodeGraph/NodeGraph.stories.d.ts +12 -0
  130. package/dist/components/molecules/NodeGraph/NodeGraph.stories.js +9 -0
  131. package/dist/components/molecules/NodeGraph/NodeGraph.svelte +1409 -0
  132. package/dist/components/molecules/NodeGraph/NodeGraph.svelte.d.ts +33 -0
  133. package/dist/components/molecules/NodeGraph/NodeGraphStory.svelte +90 -0
  134. package/dist/components/molecules/NodeGraph/NodeGraphStory.svelte.d.ts +3 -0
  135. package/dist/components/molecules/NodePalette/NodePalette.stories.js +8 -0
  136. package/dist/components/molecules/NodePalette/NodePalette.svelte +86 -0
  137. package/dist/components/molecules/NodePalette/NodePalette.svelte.d.ts +11 -0
  138. package/dist/components/molecules/NodePalette/NodePaletteStory.svelte +11 -0
  139. package/dist/components/molecules/NodePalette/NodePaletteStory.svelte.d.ts +18 -0
  140. package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
  141. package/dist/components/molecules/Poll/Poll.stories.d.ts +1 -1
  142. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.stories.js +8 -0
  143. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +794 -0
  144. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +25 -0
  145. package/dist/components/molecules/ProgramMonitor/ProgramMonitorStory.svelte +16 -0
  146. package/dist/components/molecules/ProgramMonitor/ProgramMonitorStory.svelte.d.ts +18 -0
  147. package/dist/components/molecules/PropertyField/PropertyField.svelte +57 -3
  148. package/dist/components/molecules/PropertyField/PropertyField.svelte.d.ts +10 -0
  149. package/dist/components/molecules/PropertyField/PropertyFieldStory.svelte +21 -0
  150. package/dist/components/molecules/PropertyGroup/PropertyGroup.svelte +1 -1
  151. package/dist/components/molecules/QueryBuilder/QueryBuilder.stories.d.ts +9 -0
  152. package/dist/components/molecules/QueryBuilder/QueryBuilder.stories.js +8 -0
  153. package/dist/components/molecules/QueryBuilder/QueryBuilder.svelte +52 -0
  154. package/dist/components/molecules/QueryBuilder/QueryBuilder.svelte.d.ts +18 -0
  155. package/dist/components/molecules/QueryBuilder/QueryBuilderStory.svelte +62 -0
  156. package/dist/components/molecules/QueryBuilder/QueryBuilderStory.svelte.d.ts +3 -0
  157. package/dist/components/molecules/RelationPicker/RelationPicker.stories.d.ts +9 -0
  158. package/dist/components/molecules/RelationPicker/RelationPicker.stories.js +8 -0
  159. package/dist/components/molecules/RelationPicker/RelationPicker.svelte +180 -0
  160. package/dist/components/molecules/RelationPicker/RelationPicker.svelte.d.ts +24 -0
  161. package/dist/components/molecules/RelationPicker/RelationPickerStory.svelte +16 -0
  162. package/dist/components/molecules/RelationPicker/RelationPickerStory.svelte.d.ts +3 -0
  163. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.stories.d.ts +9 -0
  164. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.stories.js +8 -0
  165. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.svelte +154 -0
  166. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.svelte.d.ts +12 -0
  167. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilderStory.svelte +16 -0
  168. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilderStory.svelte.d.ts +3 -0
  169. package/dist/components/molecules/SecretField/SecretField.stories.d.ts +9 -0
  170. package/dist/components/molecules/SecretField/SecretField.stories.js +8 -0
  171. package/dist/components/molecules/SecretField/SecretField.svelte +58 -0
  172. package/dist/components/molecules/SecretField/SecretField.svelte.d.ts +15 -0
  173. package/dist/components/molecules/SecretField/SecretFieldStory.svelte +8 -0
  174. package/dist/components/molecules/SecretField/SecretFieldStory.svelte.d.ts +3 -0
  175. package/dist/components/molecules/SplitPane/SplitPane.svelte +99 -40
  176. package/dist/components/molecules/SplitPane/SplitPane.svelte.d.ts +10 -2
  177. package/dist/components/molecules/TableToolbar/TableToolbar.svelte.d.ts +1 -1
  178. package/dist/components/molecules/Terminal/Terminal.stories.js +20 -0
  179. package/dist/components/molecules/Terminal/Terminal.svelte +238 -0
  180. package/dist/components/molecules/Terminal/Terminal.svelte.d.ts +19 -0
  181. package/dist/components/molecules/Terminal/TerminalStory.svelte +47 -0
  182. package/dist/components/molecules/Terminal/TerminalStory.svelte.d.ts +6 -0
  183. package/dist/components/molecules/TimelineClip/TimelineClip.stories.js +8 -0
  184. package/dist/components/molecules/TimelineClip/TimelineClip.svelte +193 -0
  185. package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +18 -0
  186. package/dist/components/molecules/TimelineClip/TimelineClipStory.svelte +18 -0
  187. package/dist/components/molecules/TimelineClip/TimelineClipStory.svelte.d.ts +18 -0
  188. package/dist/components/molecules/TimelineTrack/TimelineTrack.stories.js +8 -0
  189. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +126 -0
  190. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +27 -0
  191. package/dist/components/molecules/TimelineTrack/TimelineTrackStory.svelte +35 -0
  192. package/dist/components/molecules/TimelineTrack/TimelineTrackStory.svelte.d.ts +18 -0
  193. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.stories.js +8 -0
  194. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.svelte +158 -0
  195. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.svelte.d.ts +18 -0
  196. package/dist/components/molecules/TraceWaterfall/TraceWaterfallStory.svelte +55 -0
  197. package/dist/components/molecules/TraceWaterfall/TraceWaterfallStory.svelte.d.ts +18 -0
  198. package/dist/components/molecules/TrackHeader/TrackHeader.stories.d.ts +9 -0
  199. package/dist/components/molecules/TrackHeader/TrackHeader.stories.js +8 -0
  200. package/dist/components/molecules/TrackHeader/TrackHeader.svelte +54 -0
  201. package/dist/components/molecules/TrackHeader/TrackHeader.svelte.d.ts +9 -0
  202. package/dist/components/molecules/TrackHeader/TrackHeaderStory.svelte +10 -0
  203. package/dist/components/molecules/TrackHeader/TrackHeaderStory.svelte.d.ts +3 -0
  204. package/dist/components/molecules/TransportControls/TransportControls.stories.d.ts +9 -0
  205. package/dist/components/molecules/TransportControls/TransportControls.stories.js +8 -0
  206. package/dist/components/molecules/TransportControls/TransportControls.svelte +135 -0
  207. package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +24 -0
  208. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +40 -0
  209. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte.d.ts +3 -0
  210. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.stories.js +8 -0
  211. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.svelte +117 -0
  212. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.svelte.d.ts +20 -0
  213. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanelStory.svelte +36 -0
  214. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanelStory.svelte.d.ts +18 -0
  215. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
  216. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
  217. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
  218. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
  219. package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
  220. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
  221. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
  222. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
  223. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
  224. package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
  225. package/dist/components/molecules/VideoPlayer/VideoPlayer.stories.d.ts +1 -1
  226. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.stories.d.ts +9 -0
  227. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.stories.js +8 -0
  228. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +112 -0
  229. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +15 -0
  230. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeaderStory.svelte +10 -0
  231. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeaderStory.svelte.d.ts +3 -0
  232. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
  233. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
  234. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
  235. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
  236. package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
  237. package/dist/components/molecules/WaveformRegion/WaveformRegion.stories.js +8 -0
  238. package/dist/components/molecules/WaveformRegion/WaveformRegion.svelte +82 -0
  239. package/dist/components/molecules/WaveformRegion/WaveformRegion.svelte.d.ts +13 -0
  240. package/dist/components/molecules/WaveformRegion/WaveformRegionStory.svelte +17 -0
  241. package/dist/components/molecules/WaveformRegion/WaveformRegionStory.svelte.d.ts +18 -0
  242. package/dist/components/molecules/WidgetCanvas/WidgetCanvas.svelte +13 -4
  243. package/dist/components/molecules/WidgetCanvas/WidgetCanvas.svelte.d.ts +3 -1
  244. package/dist/components/molecules/WidgetCanvas/widgetCanvasContext.d.ts +2 -0
  245. package/dist/components/molecules/WidgetFrame/WidgetFrame.svelte +206 -37
  246. package/dist/components/molecules/WidgetFrame/WidgetFrame.svelte.d.ts +31 -0
  247. package/dist/components/organisms/AudioEditor/AudioEditor.stories.d.ts +12 -0
  248. package/dist/components/organisms/AudioEditor/AudioEditor.stories.js +9 -0
  249. package/dist/components/organisms/AudioEditor/AudioEditor.svelte +324 -0
  250. package/dist/components/organisms/AudioEditor/AudioEditor.svelte.d.ts +13 -0
  251. package/dist/components/organisms/AudioEditor/AudioEditorStory.svelte +10 -0
  252. package/dist/components/organisms/AudioEditor/AudioEditorStory.svelte.d.ts +3 -0
  253. package/dist/components/organisms/BlogEditor/BlogEditor.stories.d.ts +12 -0
  254. package/dist/components/organisms/BlogEditor/BlogEditor.stories.js +9 -0
  255. package/dist/components/organisms/BlogEditor/BlogEditor.svelte +1208 -0
  256. package/dist/components/organisms/BlogEditor/BlogEditor.svelte.d.ts +16 -0
  257. package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte +144 -0
  258. package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte.d.ts +3 -0
  259. package/dist/components/organisms/BlogEditor/DemoBlogCard.svelte +28 -0
  260. package/dist/components/organisms/BlogEditor/DemoBlogCard.svelte.d.ts +10 -0
  261. package/dist/components/organisms/BlogEditor/DemoBlogStats.svelte +34 -0
  262. package/dist/components/organisms/BlogEditor/DemoBlogStats.svelte.d.ts +11 -0
  263. package/dist/components/organisms/BlogEditor/DemoBlogWidget.svelte +13 -0
  264. package/dist/components/organisms/BlogEditor/DemoBlogWidget.svelte.d.ts +8 -0
  265. package/dist/components/organisms/BlogRenderer/BlogRenderer.stories.js +8 -0
  266. package/dist/components/organisms/BlogRenderer/BlogRenderer.svelte +469 -0
  267. package/dist/components/organisms/BlogRenderer/BlogRenderer.svelte.d.ts +14 -0
  268. package/dist/components/organisms/BlogRenderer/BlogRendererStory.svelte +20 -0
  269. package/dist/components/organisms/BlogRenderer/BlogRendererStory.svelte.d.ts +18 -0
  270. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.stories.d.ts +12 -0
  271. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.stories.js +9 -0
  272. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.svelte +1012 -0
  273. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.svelte.d.ts +16 -0
  274. package/dist/components/organisms/BlueprintEditor/BlueprintEditorStory.svelte +76 -0
  275. package/dist/components/organisms/BlueprintEditor/BlueprintEditorStory.svelte.d.ts +3 -0
  276. package/dist/components/organisms/CanvasEditor/CanvasEditor.stories.d.ts +12 -0
  277. package/dist/components/organisms/CanvasEditor/CanvasEditor.stories.js +9 -0
  278. package/dist/components/organisms/CanvasEditor/CanvasEditor.svelte +1680 -0
  279. package/dist/components/organisms/CanvasEditor/CanvasEditor.svelte.d.ts +18 -0
  280. package/dist/components/organisms/CanvasEditor/CanvasEditorStory.svelte +329 -0
  281. package/dist/components/organisms/CanvasEditor/CanvasEditorStory.svelte.d.ts +3 -0
  282. package/dist/components/organisms/DashboardBuilderPage/DashboardBuilderPage.svelte.d.ts +1 -1
  283. package/dist/components/organisms/DesktopShell/DesktopShell.svelte.d.ts +1 -1
  284. package/dist/components/organisms/EditorShell/EditorShell.svelte +162 -57
  285. package/dist/components/organisms/EditorShell/EditorShell.svelte.d.ts +21 -1
  286. package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
  287. package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
  288. package/dist/components/organisms/FileUploader/FileUploader.svelte +127 -25
  289. package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +20 -0
  290. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
  291. package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
  292. package/dist/components/organisms/ImportWizard/ImportWizard.stories.js +8 -0
  293. package/dist/components/organisms/ImportWizard/ImportWizard.svelte +209 -0
  294. package/dist/components/organisms/ImportWizard/ImportWizard.svelte.d.ts +22 -0
  295. package/dist/components/organisms/ImportWizard/ImportWizardStory.svelte +16 -0
  296. package/dist/components/organisms/ImportWizard/ImportWizardStory.svelte.d.ts +18 -0
  297. package/dist/components/organisms/VideoEditor/VideoEditor.stories.d.ts +12 -0
  298. package/dist/components/organisms/VideoEditor/VideoEditor.stories.js +9 -0
  299. package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1687 -0
  300. package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +14 -0
  301. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +11 -0
  302. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte.d.ts +3 -0
  303. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.stories.d.ts +12 -0
  304. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.stories.js +9 -0
  305. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte +22 -0
  306. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte.d.ts +13 -0
  307. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplateStory.svelte +8 -0
  308. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplateStory.svelte.d.ts +3 -0
  309. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.stories.d.ts +12 -0
  310. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.stories.js +9 -0
  311. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.svelte +32 -0
  312. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.svelte.d.ts +17 -0
  313. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplateStory.svelte +21 -0
  314. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplateStory.svelte.d.ts +3 -0
  315. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.stories.d.ts +12 -0
  316. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.stories.js +9 -0
  317. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.svelte +22 -0
  318. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.svelte.d.ts +13 -0
  319. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplateStory.svelte +8 -0
  320. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplateStory.svelte.d.ts +3 -0
  321. package/dist/index.d.ts +91 -1
  322. package/dist/index.js +64 -0
  323. package/dist/types/xyflow-svelte.d.ts +62 -0
  324. package/dist/utils/audioProject.d.ts +19 -0
  325. package/dist/utils/audioProject.js +70 -0
  326. package/dist/utils/blogDocument.d.ts +175 -0
  327. package/dist/utils/blogDocument.js +220 -0
  328. package/dist/utils/blueprintRuntime.d.ts +34 -0
  329. package/dist/utils/blueprintRuntime.js +289 -0
  330. package/dist/utils/canvasDocument.d.ts +239 -0
  331. package/dist/utils/canvasDocument.js +443 -0
  332. package/dist/utils/canvasExport.d.ts +10 -0
  333. package/dist/utils/canvasExport.js +469 -0
  334. package/dist/utils/canvasHierarchy.d.ts +189 -0
  335. package/dist/utils/canvasHierarchy.js +674 -0
  336. package/dist/utils/canvasHistory.d.ts +19 -0
  337. package/dist/utils/canvasHistory.js +48 -0
  338. package/dist/utils/canvasWidget.d.ts +81 -0
  339. package/dist/utils/canvasWidget.js +597 -0
  340. package/dist/utils/mediaTime.d.ts +26 -0
  341. package/dist/utils/mediaTime.js +76 -0
  342. package/dist/utils/mediaTracks.d.ts +215 -0
  343. package/dist/utils/mediaTracks.js +798 -0
  344. package/dist/utils/nodeGraph.d.ts +354 -0
  345. package/dist/utils/nodeGraph.js +1453 -0
  346. package/dist/utils/queryBridge.d.ts +18 -0
  347. package/dist/utils/queryBridge.js +118 -0
  348. package/dist/utils/videoSequence.d.ts +13 -0
  349. package/dist/utils/videoSequence.js +94 -0
  350. package/package.json +5 -2
@@ -0,0 +1,1687 @@
1
+ <script lang="ts">
2
+ import EditorShell from '../EditorShell/EditorShell.svelte';
3
+ import MediaAssetBrowser from '../../molecules/MediaAssetBrowser/MediaAssetBrowser.svelte';
4
+ import VideoComponentsPanel, {
5
+ VIDEO_COMPONENT_MIME,
6
+ type VideoComponentDef
7
+ } from '../../molecules/VideoComponentsPanel/VideoComponentsPanel.svelte';
8
+ import VideoAnimationsPanel, {
9
+ VIDEO_ANIMATION_MIME,
10
+ type VideoAnimationDef
11
+ } from '../../molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte';
12
+ import VideoTransitionsPanel, {
13
+ VIDEO_TRANSITION_MIME,
14
+ type VideoTransitionDef
15
+ } from '../../molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte';
16
+ import ProgramMonitor from '../../molecules/ProgramMonitor/ProgramMonitor.svelte';
17
+ import TransportControls from '../../molecules/TransportControls/TransportControls.svelte';
18
+ import MediaTimeline from '../../molecules/MediaTimeline/MediaTimeline.svelte';
19
+ import ContextMenu from '../../molecules/ContextMenu/ContextMenu.svelte';
20
+ import type { ContextMenuItem } from '../../molecules/ContextMenu/ContextMenu.svelte';
21
+ import BulkActionBar from '../../molecules/BulkActionBar/BulkActionBar.svelte';
22
+ import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
23
+ import FileUploader from '../FileUploader/FileUploader.svelte';
24
+ import PropertyGroup from '../../molecules/PropertyGroup/PropertyGroup.svelte';
25
+ import PropertyField from '../../molecules/PropertyField/PropertyField.svelte';
26
+ import Input from '../../atoms/Input/Input.svelte';
27
+ import NumberInput from '../../molecules/NumberInput/NumberInput.svelte';
28
+ import ColorPicker from '../../molecules/ColorPicker/ColorPicker.svelte';
29
+ import Button from '../../atoms/Button/Button.svelte';
30
+ import IconButton from '../../atoms/IconButton/IconButton.svelte';
31
+ import {
32
+ createMediaClip,
33
+ createMediaTrack,
34
+ updateClipInTracks,
35
+ removeClipsFromTracks,
36
+ splitClipsAt,
37
+ mergeClipsInTracks,
38
+ canMergeMediaClips,
39
+ duplicateClipInTracks,
40
+ findClipById,
41
+ nextTrackName,
42
+ reorderTracks,
43
+ removeTrack,
44
+ defaultOverlayRect,
45
+ alignClipRect,
46
+ resolveClipRect,
47
+ moveClipsByDelta,
48
+ isMediaClipFieldModified,
49
+ resetMediaClipField,
50
+ kindFromFile,
51
+ probeMediaSrc,
52
+ createMediaAsset,
53
+ type MediaAsset,
54
+ type MediaAssetKind,
55
+ type MediaClip,
56
+ type MediaClipAlign,
57
+ type MediaClipResettableField,
58
+ type MediaTrack,
59
+ type MediaTrackKind
60
+ } from '../../../utils/mediaTracks.js';
61
+ import { emptyVideoSequence, type VideoSequence } from '../../../utils/videoSequence.js';
62
+ import { clampMs } from '../../../utils/mediaTime.js';
63
+ import Tooltip from '../../atoms/Tooltip/Tooltip.svelte';
64
+ import Slider from '../../atoms/Slider/Slider.svelte';
65
+ import Download from '@lucide/svelte/icons/download';
66
+ import PanelLeft from '@lucide/svelte/icons/panel-left';
67
+ import PanelRight from '@lucide/svelte/icons/panel-right';
68
+ import Scissors from '@lucide/svelte/icons/scissors';
69
+ import Copy from '@lucide/svelte/icons/copy';
70
+ import Combine from '@lucide/svelte/icons/combine';
71
+ import Trash2 from '@lucide/svelte/icons/trash-2';
72
+ import AlignStartVertical from '@lucide/svelte/icons/align-start-vertical';
73
+ import AlignCenterVertical from '@lucide/svelte/icons/align-center-vertical';
74
+ import AlignEndVertical from '@lucide/svelte/icons/align-end-vertical';
75
+ import AlignStartHorizontal from '@lucide/svelte/icons/align-start-horizontal';
76
+ import AlignCenterHorizontal from '@lucide/svelte/icons/align-center-horizontal';
77
+ import AlignEndHorizontal from '@lucide/svelte/icons/align-end-horizontal';
78
+ import AlignCenter from '@lucide/svelte/icons/align-center';
79
+ import AlignLeft from '@lucide/svelte/icons/align-left';
80
+ import AlignRight from '@lucide/svelte/icons/align-right';
81
+ import Maximize2 from '@lucide/svelte/icons/maximize-2';
82
+ import type { Component } from 'svelte';
83
+ import { onDestroy } from 'svelte';
84
+
85
+ type IconComp = Component<{ class?: string }>;
86
+
87
+ interface VideoEditorProps {
88
+ value?: VideoSequence;
89
+ class?: string;
90
+ onchange?: (seq: VideoSequence) => void;
91
+ onexport?: (payload: { format: 'json'; document: VideoSequence }) => void;
92
+ }
93
+
94
+ let {
95
+ value = $bindable(emptyVideoSequence()),
96
+ class: className = '',
97
+ onchange,
98
+ onexport
99
+ }: VideoEditorProps = $props();
100
+
101
+ let selectedClipIds = $state<string[]>([]);
102
+ let currentTimeMs = $state(0);
103
+ let playing = $state(false);
104
+ let playbackRate = $state(1);
105
+ let muted = $state(false);
106
+ let volume = $state(1);
107
+ let zoom = $state(1);
108
+ let showSidebar = $state(true);
109
+ let showInspector = $state(true);
110
+ let sidebarTab = $state<'components' | 'animations' | 'transitions' | 'assets'>('components');
111
+ let ctxOpen = $state(false);
112
+ let ctxAnchor = $state<{ x: number; y: number } | null>(null);
113
+ let ctxTrackId = $state<string | null>(null);
114
+ let raf = 0;
115
+ let lastTs = 0;
116
+ let transitionPreviewTimer = 0;
117
+ const SKIP_MS = 5000;
118
+
119
+ onDestroy(() => {
120
+ window.clearTimeout(transitionPreviewTimer);
121
+ });
122
+
123
+ const primaryClipId = $derived(selectedClipIds[selectedClipIds.length - 1] ?? null);
124
+ const selectedClip = $derived(primaryClipId ? findClipById(value.tracks, primaryClipId) : null);
125
+ const canMerge = $derived(canMergeMediaClips(value.tracks, selectedClipIds));
126
+ const canSplit = $derived(
127
+ !!selectedClip && (selectedClip.kind === 'video' || selectedClip.kind === 'audio')
128
+ );
129
+ const canBulkSplit = $derived(
130
+ selectedClipIds.some((id) => {
131
+ const c = findClipById(value.tracks, id);
132
+ return !!c && (c.kind === 'video' || c.kind === 'audio');
133
+ })
134
+ );
135
+
136
+ const bulkActions = $derived.by((): BulkAction[] => [
137
+ {
138
+ id: 'split',
139
+ label: 'Split at playhead',
140
+ icon: Scissors,
141
+ variant: 'ghost',
142
+ disabled: !canBulkSplit
143
+ },
144
+ { id: 'merge', label: 'Merge', icon: Combine, variant: 'ghost', disabled: !canMerge },
145
+ { id: 'duplicate', label: 'Duplicate', icon: Copy, variant: 'ghost' },
146
+ {
147
+ id: 'delete',
148
+ label: 'Delete',
149
+ icon: Trash2,
150
+ variant: 'destructive',
151
+ confirm: true,
152
+ confirmLabel: `Delete ${selectedClipIds.length}`
153
+ }
154
+ ]);
155
+
156
+ function handleBulk(id: string) {
157
+ if (id === 'split') splitSelected();
158
+ else if (id === 'merge') mergeSelected();
159
+ else if (id === 'duplicate') duplicateSelected();
160
+ else if (id === 'delete') deleteSelected();
161
+ }
162
+
163
+ const ctxItems = $derived.by((): ContextMenuItem[] => {
164
+ const hasSel = selectedClipIds.length > 0;
165
+ const canDeleteTrack = !!ctxTrackId && value.tracks.length > 1;
166
+ return [
167
+ {
168
+ id: 'split',
169
+ label: 'Split at playhead',
170
+ shortcut: 'S',
171
+ disabled: !canSplit,
172
+ category: 'Edit'
173
+ },
174
+ {
175
+ id: 'merge',
176
+ label: 'Merge selected',
177
+ disabled: !canMerge,
178
+ category: 'Edit'
179
+ },
180
+ {
181
+ id: 'duplicate',
182
+ label: 'Duplicate',
183
+ shortcut: '⌘D',
184
+ disabled: !hasSel,
185
+ category: 'Edit'
186
+ },
187
+ { id: 'sep-1', label: '', separator: true },
188
+ {
189
+ id: 'add-clip',
190
+ label: 'Add empty clip',
191
+ category: 'Insert',
192
+ disabled: !ctxTrackId && !hasSel
193
+ },
194
+ { id: 'add-video-track', label: 'Add video track', category: 'Tracks' },
195
+ { id: 'add-audio-track', label: 'Add audio track', category: 'Tracks' },
196
+ {
197
+ id: 'delete-track',
198
+ label: 'Delete track',
199
+ destructive: true,
200
+ disabled: !canDeleteTrack,
201
+ category: 'Tracks'
202
+ },
203
+ { id: 'sep-2', label: '', separator: true },
204
+ {
205
+ id: 'delete',
206
+ label: selectedClipIds.length > 1 ? `Delete ${selectedClipIds.length} clips` : 'Delete',
207
+ destructive: true,
208
+ shortcut: '⌫',
209
+ disabled: !hasSel,
210
+ category: 'Edit'
211
+ }
212
+ ];
213
+ });
214
+
215
+ const alignDock: { id: MediaClipAlign; label: string; icon: IconComp }[] = [
216
+ { id: 'left', label: 'Align left', icon: AlignStartVertical },
217
+ { id: 'center-h', label: 'Align center', icon: AlignCenterVertical },
218
+ { id: 'right', label: 'Align right', icon: AlignEndVertical },
219
+ { id: 'top', label: 'Align top', icon: AlignStartHorizontal },
220
+ { id: 'center-v', label: 'Align middle', icon: AlignCenterHorizontal },
221
+ { id: 'bottom', label: 'Align bottom', icon: AlignEndHorizontal },
222
+ { id: 'center', label: 'Center on frame', icon: AlignCenter },
223
+ { id: 'full', label: 'Fill frame', icon: Maximize2 }
224
+ ];
225
+
226
+ const textAlignDock: { id: 'left' | 'center' | 'right'; label: string; icon: IconComp }[] = [
227
+ { id: 'left', label: 'Align left', icon: AlignLeft },
228
+ { id: 'center', label: 'Align center', icon: AlignCenter },
229
+ { id: 'right', label: 'Align right', icon: AlignRight }
230
+ ];
231
+
232
+ const selectedRect = $derived(
233
+ selectedClip
234
+ ? resolveClipRect(selectedClip, { width: value.width, height: value.height })
235
+ : null
236
+ );
237
+ const showTransform = $derived(
238
+ !!selectedClip &&
239
+ selectedClip.kind !== 'audio' &&
240
+ (selectedClip.kind === 'text' ||
241
+ !!selectedClip.rect ||
242
+ (selectedClip.kind === 'image' && !selectedClip.src))
243
+ );
244
+
245
+ function emit(next: VideoSequence) {
246
+ value = next;
247
+ onchange?.(next);
248
+ }
249
+
250
+ function setTracks(tracks: MediaTrack[]) {
251
+ emit({ ...value, tracks });
252
+ }
253
+
254
+ function patchSequence(partial: Partial<VideoSequence>) {
255
+ emit({ ...value, ...partial });
256
+ }
257
+
258
+ function selectClip(id: string, e?: MouseEvent) {
259
+ const multi = e && (e.metaKey || e.ctrlKey || e.shiftKey);
260
+ if (multi) {
261
+ if (selectedClipIds.includes(id)) {
262
+ selectedClipIds = selectedClipIds.filter((x) => x !== id);
263
+ } else {
264
+ selectedClipIds = [...selectedClipIds, id];
265
+ }
266
+ } else if (selectedClipIds.includes(id)) {
267
+ // Keep multi-selection when dragging / clicking an already-selected clip
268
+ return;
269
+ } else {
270
+ selectedClipIds = [id];
271
+ }
272
+ }
273
+
274
+ function setSelection(ids: string[]) {
275
+ selectedClipIds = ids;
276
+ }
277
+
278
+ function openClipMenu(clipId: string, e: MouseEvent) {
279
+ e.preventDefault();
280
+ e.stopPropagation();
281
+ if (!selectedClipIds.includes(clipId)) {
282
+ selectedClipIds = [clipId];
283
+ }
284
+ ctxTrackId = findClipById(value.tracks, clipId)?.trackId ?? null;
285
+ // Update anchor in place so an already-open menu retargets without flicker
286
+ ctxAnchor = { x: e.clientX, y: e.clientY };
287
+ ctxOpen = true;
288
+ }
289
+
290
+ function openTrackMenu(trackId: string, e: MouseEvent) {
291
+ e.preventDefault();
292
+ e.stopPropagation();
293
+ ctxTrackId = trackId;
294
+ ctxAnchor = { x: e.clientX, y: e.clientY };
295
+ ctxOpen = true;
296
+ }
297
+
298
+ function addAssetAsClip(asset: MediaAsset) {
299
+ const trackKind: MediaTrackKind = asset.kind === 'audio' ? 'audio' : 'video';
300
+ const track =
301
+ value.tracks.find((t) => t.kind === trackKind) ??
302
+ value.tracks.find((t) => t.kind === 'video') ??
303
+ value.tracks[0];
304
+ if (!track) return;
305
+ const startMs = currentTimeMs;
306
+ const dur = asset.durationMs ?? (asset.kind === 'audio' ? 8000 : 5000);
307
+ const clip = createMediaClip({
308
+ trackId: track.id,
309
+ assetId: asset.id,
310
+ name: asset.name,
311
+ kind: asset.kind,
312
+ src: asset.src,
313
+ startMs,
314
+ endMs: startMs + dur,
315
+ color: track.color ?? (trackKind === 'audio' ? '#10b981' : '#3b82f6'),
316
+ text: asset.kind === 'text' ? asset.name : undefined
317
+ });
318
+ const assets = value.assets.some((a) => a.id === asset.id)
319
+ ? value.assets
320
+ : [...value.assets, asset];
321
+ emit({
322
+ ...value,
323
+ assets,
324
+ tracks: value.tracks.map((t) =>
325
+ t.id === track.id ? { ...t, clips: [...t.clips, clip] } : t
326
+ )
327
+ });
328
+ selectedClipIds = [clip.id];
329
+ }
330
+
331
+ function addComponent(def: VideoComponentDef, at?: { trackId?: string; startMs?: number }) {
332
+ const kind: MediaAssetKind =
333
+ def.kind === 'solid' ? 'image' : def.kind === 'image' ? 'image' : 'text';
334
+ const startMs = at?.startMs ?? currentTimeMs;
335
+ const frame = { width: value.width, height: value.height };
336
+ const rect = defaultOverlayRect(frame, def.rectPreset ?? (kind === 'text' ? 'center' : 'full'));
337
+
338
+ if (at?.trackId) {
339
+ const track = value.tracks.find((t) => t.id === at.trackId);
340
+ if (!track || track.kind === 'audio') {
341
+ // Fall through to new track
342
+ } else {
343
+ const clip = createMediaClip({
344
+ trackId: track.id,
345
+ name: def.label,
346
+ kind,
347
+ text: def.kind === 'text' ? def.text : undefined,
348
+ src: def.src,
349
+ startMs,
350
+ endMs: startMs + def.durationMs,
351
+ color: def.color ?? '#6366f1',
352
+ textColor: def.textColor,
353
+ fontSize: def.fontSize,
354
+ fontWeight: def.fontWeight,
355
+ fontFamily: def.fontFamily,
356
+ textAlign: def.textAlign,
357
+ opacity: 1,
358
+ previewOpacity: def.previewOpacity ?? def.opacity,
359
+ rect
360
+ });
361
+ setTracks(
362
+ value.tracks.map((t) =>
363
+ t.id === track.id ? { ...t, clips: [...t.clips, clip] } : t
364
+ )
365
+ );
366
+ selectedClipIds = [clip.id];
367
+ return;
368
+ }
369
+ }
370
+
371
+ const trackKind: MediaTrackKind = 'video';
372
+ const track = createMediaTrack({
373
+ name: nextTrackName(value.tracks, trackKind),
374
+ kind: trackKind,
375
+ color: def.color ?? '#6366f1'
376
+ });
377
+ const clip = createMediaClip({
378
+ trackId: track.id,
379
+ name: def.label,
380
+ kind,
381
+ text: def.kind === 'text' ? def.text : undefined,
382
+ src: def.src,
383
+ startMs,
384
+ endMs: startMs + def.durationMs,
385
+ color: def.color ?? '#6366f1',
386
+ textColor: def.textColor,
387
+ fontSize: def.fontSize,
388
+ fontWeight: def.fontWeight,
389
+ fontFamily: def.fontFamily,
390
+ textAlign: def.textAlign,
391
+ opacity: 1,
392
+ previewOpacity: def.previewOpacity ?? def.opacity,
393
+ rect
394
+ });
395
+ const audioIdx = value.tracks.findIndex((t) => t.kind === 'audio');
396
+ const next = [...value.tracks];
397
+ const insertAt = audioIdx >= 0 ? audioIdx : next.length;
398
+ next.splice(insertAt, 0, { ...track, clips: [clip] });
399
+ setTracks(next);
400
+ selectedClipIds = [clip.id];
401
+ }
402
+
403
+ function addEmptyClip(trackId?: string | null) {
404
+ const track =
405
+ (trackId && value.tracks.find((t) => t.id === trackId)) ||
406
+ (selectedClip && value.tracks.find((t) => t.id === selectedClip.trackId)) ||
407
+ value.tracks.find((t) => t.kind === 'video') ||
408
+ value.tracks[0];
409
+ if (!track) return;
410
+ const startMs = currentTimeMs;
411
+ const kind: MediaAssetKind = track.kind === 'audio' ? 'audio' : 'video';
412
+ const clip = createMediaClip({
413
+ trackId: track.id,
414
+ name: 'New clip',
415
+ kind,
416
+ startMs,
417
+ endMs: startMs + 3000,
418
+ color: track.color ?? (kind === 'audio' ? '#10b981' : '#3b82f6')
419
+ });
420
+ setTracks(
421
+ value.tracks.map((t) =>
422
+ t.id === track.id ? { ...t, clips: [...t.clips, clip] } : t
423
+ )
424
+ );
425
+ selectedClipIds = [clip.id];
426
+ }
427
+
428
+ function addTrack(kind: MediaTrackKind) {
429
+ const name = nextTrackName(value.tracks, kind);
430
+ const track = createMediaTrack({
431
+ name,
432
+ kind,
433
+ color: kind === 'audio' ? '#10b981' : '#3b82f6'
434
+ });
435
+ setTracks([...value.tracks, track]);
436
+ }
437
+
438
+ function deleteTrack(trackId: string) {
439
+ if (value.tracks.length <= 1) return;
440
+ const track = value.tracks.find((t) => t.id === trackId);
441
+ if (!track) return;
442
+ const removedClipIds = new Set(track.clips.map((c) => c.id));
443
+ setTracks(removeTrack(value.tracks, trackId));
444
+ selectedClipIds = selectedClipIds.filter((id) => !removedClipIds.has(id));
445
+ if (ctxTrackId === trackId) ctxTrackId = null;
446
+ }
447
+
448
+ function reorderTrackIds(orderedIds: string[]) {
449
+ setTracks(reorderTracks(value.tracks, orderedIds));
450
+ }
451
+
452
+ function applyAlign(align: MediaClipAlign) {
453
+ if (!selectedClipIds.length) return;
454
+ const frame = { width: value.width, height: value.height };
455
+ let tracks = value.tracks;
456
+ for (const id of selectedClipIds) {
457
+ tracks = updateClipInTracks(tracks, id, (c) => {
458
+ if (c.kind === 'audio' || c.kind === 'video') {
459
+ // Allow transform on overlays / text / solids; skip pure A/V full-frame
460
+ if (!c.rect && c.kind === 'video') return c;
461
+ if (!c.rect && c.kind === 'audio') return c;
462
+ }
463
+ return alignClipRect(c, frame, align);
464
+ });
465
+ }
466
+ setTracks(tracks);
467
+ }
468
+
469
+ function patchSelectedRect(partial: Partial<{ x: number; y: number; w: number; h: number }>) {
470
+ if (!selectedClip || !selectedRect) return;
471
+ patchClip({
472
+ ...selectedClip,
473
+ rect: {
474
+ x: partial.x ?? selectedRect.x,
475
+ y: partial.y ?? selectedRect.y,
476
+ w: Math.max(1, partial.w ?? selectedRect.w),
477
+ h: Math.max(1, partial.h ?? selectedRect.h)
478
+ }
479
+ });
480
+ }
481
+
482
+ function clipFieldModified(field: MediaClipResettableField): boolean {
483
+ return (
484
+ !!selectedClip &&
485
+ isMediaClipFieldModified(selectedClip, field, {
486
+ width: value.width,
487
+ height: value.height
488
+ })
489
+ );
490
+ }
491
+
492
+ function resetClipField(field: MediaClipResettableField) {
493
+ if (!selectedClip) return;
494
+ patchClip(
495
+ resetMediaClipField(selectedClip, field, {
496
+ width: value.width,
497
+ height: value.height
498
+ })
499
+ );
500
+ }
501
+
502
+ function acceptForClipKind(kind: MediaAssetKind): string {
503
+ if (kind === 'audio') return 'audio/*';
504
+ if (kind === 'image') return 'image/*';
505
+ if (kind === 'video') return 'video/*';
506
+ return 'image/*,video/*,audio/*';
507
+ }
508
+
509
+ async function replaceSelectedMedia(files: File[]) {
510
+ const file = files[0];
511
+ if (!file || !selectedClip) return;
512
+ const kind = kindFromFile(file);
513
+ if (kind === 'text') return;
514
+ // Keep clip kind aligned with the track role when possible
515
+ const nextKind: MediaAssetKind =
516
+ selectedClip.kind === 'audio'
517
+ ? 'audio'
518
+ : kind === 'audio'
519
+ ? selectedClip.kind
520
+ : kind;
521
+ if (selectedClip.kind === 'audio' && kind !== 'audio') return;
522
+ if (selectedClip.kind !== 'audio' && kind === 'audio') return;
523
+
524
+ const src = URL.createObjectURL(file);
525
+ const meta = await probeMediaSrc(src, nextKind);
526
+ const asset = createMediaAsset({
527
+ kind: nextKind,
528
+ name: file.name,
529
+ src,
530
+ ...meta
531
+ });
532
+ const dur = meta.durationMs;
533
+ const startMs = selectedClip.startMs;
534
+ const endMs =
535
+ dur != null
536
+ ? startMs + dur
537
+ : selectedClip.endMs > startMs
538
+ ? selectedClip.endMs
539
+ : startMs + 5000;
540
+
541
+ emit({
542
+ ...value,
543
+ assets: value.assets.some((a) => a.id === asset.id)
544
+ ? value.assets
545
+ : [...value.assets, asset],
546
+ tracks: updateClipInTracks(value.tracks, selectedClip.id, (c) => ({
547
+ ...c,
548
+ assetId: asset.id,
549
+ name: file.name,
550
+ kind: nextKind,
551
+ src,
552
+ endMs
553
+ }))
554
+ });
555
+ }
556
+
557
+ function clearSelectedMedia() {
558
+ if (!selectedClip) return;
559
+ patchClip({
560
+ ...selectedClip,
561
+ src: undefined,
562
+ assetId: undefined
563
+ });
564
+ }
565
+
566
+ function applyAnimation(def: VideoAnimationDef, clipIds = selectedClipIds) {
567
+ if (!clipIds.length) return;
568
+ let tracks = value.tracks;
569
+ for (const id of clipIds) {
570
+ tracks = updateClipInTracks(tracks, id, (c) => {
571
+ if (def.slot === 'in') {
572
+ return { ...c, animationIn: { type: def.type, durationMs: def.durationMs } };
573
+ }
574
+ if (def.slot === 'out') {
575
+ return { ...c, animationOut: { type: def.type, durationMs: def.durationMs } };
576
+ }
577
+ // combo
578
+ return {
579
+ ...c,
580
+ animationIn: { type: def.type, durationMs: def.durationMs },
581
+ animationOut: {
582
+ type: def.outType ?? def.type,
583
+ durationMs: def.durationMs
584
+ }
585
+ };
586
+ });
587
+ }
588
+ setTracks(tracks);
589
+ }
590
+
591
+ function applyTransition(def: VideoTransitionDef, clipIds = selectedClipIds) {
592
+ if (!clipIds.length) return;
593
+ let tracks = value.tracks;
594
+ for (const id of clipIds) {
595
+ tracks = updateClipInTracks(tracks, id, (c) => ({
596
+ ...c,
597
+ transitionOut: { type: def.type, durationMs: def.durationMs }
598
+ }));
599
+ }
600
+ setTracks(tracks);
601
+ // Preview: play through the transition window so the effect is obvious
602
+ const primary = findClipById(tracks, clipIds[clipIds.length - 1] ?? '');
603
+ if (primary?.transitionOut) {
604
+ const dur = Math.min(
605
+ primary.transitionOut.durationMs,
606
+ Math.max(120, primary.endMs - primary.startMs)
607
+ );
608
+ const start = clampMs(primary.endMs - dur, 0, value.durationMs);
609
+ currentTimeMs = start;
610
+ playing = true;
611
+ window.clearTimeout(transitionPreviewTimer);
612
+ transitionPreviewTimer = window.setTimeout(
613
+ () => {
614
+ playing = false;
615
+ currentTimeMs = clampMs(primary.endMs - 1, 0, value.durationMs);
616
+ },
617
+ Math.round(dur / Math.max(0.25, playbackRate)) + 120
618
+ );
619
+ }
620
+ }
621
+
622
+ function handleClipDrop(clipId: string, e: DragEvent) {
623
+ const animRaw = e.dataTransfer?.getData(VIDEO_ANIMATION_MIME);
624
+ if (animRaw) {
625
+ try {
626
+ applyAnimation(JSON.parse(animRaw) as VideoAnimationDef, [clipId]);
627
+ selectedClipIds = [clipId];
628
+ } catch {
629
+ /* ignore */
630
+ }
631
+ return;
632
+ }
633
+ const transRaw = e.dataTransfer?.getData(VIDEO_TRANSITION_MIME);
634
+ if (transRaw) {
635
+ try {
636
+ applyTransition(JSON.parse(transRaw) as VideoTransitionDef, [clipId]);
637
+ selectedClipIds = [clipId];
638
+ } catch {
639
+ /* ignore */
640
+ }
641
+ return;
642
+ }
643
+ const compRaw = e.dataTransfer?.getData(VIDEO_COMPONENT_MIME);
644
+ if (compRaw) {
645
+ try {
646
+ const def = JSON.parse(compRaw) as VideoComponentDef;
647
+ const clip = findClipById(value.tracks, clipId);
648
+ addComponent(def, { trackId: clip?.trackId, startMs: clip?.endMs ?? currentTimeMs });
649
+ } catch {
650
+ /* ignore */
651
+ }
652
+ }
653
+ }
654
+
655
+ function handleTrackDrop(trackId: string, e: DragEvent, timeMs: number) {
656
+ const animRaw = e.dataTransfer?.getData(VIDEO_ANIMATION_MIME);
657
+ if (animRaw) {
658
+ const track = value.tracks.find((t) => t.id === trackId);
659
+ const hit = track?.clips.find((c) => timeMs >= c.startMs && timeMs < c.endMs);
660
+ if (hit) {
661
+ try {
662
+ applyAnimation(JSON.parse(animRaw) as VideoAnimationDef, [hit.id]);
663
+ selectedClipIds = [hit.id];
664
+ } catch {
665
+ /* ignore */
666
+ }
667
+ }
668
+ return;
669
+ }
670
+ const transRaw = e.dataTransfer?.getData(VIDEO_TRANSITION_MIME);
671
+ if (transRaw) {
672
+ const track = value.tracks.find((t) => t.id === trackId);
673
+ const hit = track?.clips.find((c) => timeMs >= c.startMs && timeMs < c.endMs);
674
+ if (hit) {
675
+ try {
676
+ applyTransition(JSON.parse(transRaw) as VideoTransitionDef, [hit.id]);
677
+ selectedClipIds = [hit.id];
678
+ } catch {
679
+ /* ignore */
680
+ }
681
+ }
682
+ return;
683
+ }
684
+ const compRaw = e.dataTransfer?.getData(VIDEO_COMPONENT_MIME);
685
+ if (!compRaw) return;
686
+ try {
687
+ const def = JSON.parse(compRaw) as VideoComponentDef;
688
+ addComponent(def, { trackId, startMs: timeMs });
689
+ } catch {
690
+ /* ignore */
691
+ }
692
+ }
693
+
694
+ function patchClip(clip: MediaClip) {
695
+ setTracks(updateClipInTracks(value.tracks, clip.id, () => clip));
696
+ }
697
+
698
+ function patchClips(clips: MediaClip[]) {
699
+ if (!clips.length) return;
700
+ let tracks = value.tracks;
701
+ for (const clip of clips) {
702
+ tracks = updateClipInTracks(tracks, clip.id, () => clip);
703
+ }
704
+ setTracks(tracks);
705
+ }
706
+
707
+ function moveSelectedClips(clipIds: string[], deltaMs: number) {
708
+ setTracks(moveClipsByDelta(value.tracks, clipIds, deltaMs));
709
+ }
710
+
711
+ function patchTrack(track: MediaTrack) {
712
+ setTracks(value.tracks.map((t) => (t.id === track.id ? track : t)));
713
+ }
714
+
715
+ function splitSelected() {
716
+ if (!selectedClipIds.length) return;
717
+ setTracks(splitClipsAt(value.tracks, selectedClipIds, currentTimeMs));
718
+ }
719
+
720
+ function mergeSelected() {
721
+ const idsBefore = new Set(value.tracks.flatMap((t) => t.clips.map((c) => c.id)));
722
+ const next = mergeClipsInTracks(value.tracks, selectedClipIds);
723
+ if (!next) return;
724
+ setTracks(next);
725
+ const created = next.flatMap((t) => t.clips).find((c) => !idsBefore.has(c.id));
726
+ selectedClipIds = created ? [created.id] : [];
727
+ }
728
+
729
+ function duplicateSelected() {
730
+ let tracks = value.tracks;
731
+ const created: string[] = [];
732
+ for (const id of selectedClipIds) {
733
+ const before = new Set(tracks.flatMap((t) => t.clips.map((c) => c.id)));
734
+ tracks = duplicateClipInTracks(tracks, id);
735
+ const neu = tracks.flatMap((t) => t.clips).find((c) => !before.has(c.id));
736
+ if (neu) created.push(neu.id);
737
+ }
738
+ setTracks(tracks);
739
+ if (created.length) selectedClipIds = created;
740
+ }
741
+
742
+ function deleteSelected() {
743
+ if (!selectedClipIds.length) return;
744
+ setTracks(removeClipsFromTracks(value.tracks, selectedClipIds));
745
+ selectedClipIds = [];
746
+ }
747
+
748
+ function handleCtx(id: string) {
749
+ if (id === 'split') splitSelected();
750
+ else if (id === 'merge') mergeSelected();
751
+ else if (id === 'duplicate') duplicateSelected();
752
+ else if (id === 'delete') deleteSelected();
753
+ else if (id === 'add-clip') addEmptyClip(ctxTrackId);
754
+ else if (id === 'add-video-track') addTrack('video');
755
+ else if (id === 'add-audio-track') addTrack('audio');
756
+ else if (id === 'delete-track' && ctxTrackId) deleteTrack(ctxTrackId);
757
+ ctxOpen = false;
758
+ }
759
+
760
+ function secValue(ms: number) {
761
+ const s = Math.round(ms / 100) / 10; // 1 decimal second
762
+ return Number.isInteger(s) ? String(s) : s.toFixed(1);
763
+ }
764
+
765
+ function msToSec(ms: number) {
766
+ return Math.round(ms / 100) / 10;
767
+ }
768
+
769
+ function secToMs(s: number) {
770
+ return Math.max(0, s) * 1000;
771
+ }
772
+
773
+ function clearSelection() {
774
+ selectedClipIds = [];
775
+ }
776
+
777
+ function seekBy(deltaMs: number) {
778
+ currentTimeMs = clampMs(currentTimeMs + deltaMs, 0, value.durationMs);
779
+ }
780
+
781
+ function seekTo(ms: number) {
782
+ currentTimeMs = clampMs(ms, 0, value.durationMs);
783
+ }
784
+
785
+ function tick(ts: number) {
786
+ if (!playing) return;
787
+ if (lastTs) {
788
+ const delta = (ts - lastTs) * playbackRate;
789
+ currentTimeMs = clampMs(currentTimeMs + delta, 0, value.durationMs);
790
+ if (currentTimeMs >= value.durationMs) {
791
+ playing = false;
792
+ currentTimeMs = value.durationMs;
793
+ }
794
+ }
795
+ lastTs = ts;
796
+ raf = requestAnimationFrame(tick);
797
+ }
798
+
799
+ $effect(() => {
800
+ if (playing) {
801
+ lastTs = 0;
802
+ raf = requestAnimationFrame(tick);
803
+ return () => cancelAnimationFrame(raf);
804
+ }
805
+ cancelAnimationFrame(raf);
806
+ });
807
+
808
+ function handleKeydown(e: KeyboardEvent) {
809
+ const tag = (e.target as HTMLElement)?.tagName;
810
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
811
+ if (e.code === 'Space') {
812
+ e.preventDefault();
813
+ playing = !playing;
814
+ return;
815
+ }
816
+ if (e.key === 'Escape') {
817
+ e.preventDefault();
818
+ clearSelection();
819
+ return;
820
+ }
821
+ if (e.key === 'ArrowLeft') {
822
+ e.preventDefault();
823
+ seekBy(e.shiftKey ? -SKIP_MS : -(1000 / Math.max(1, value.fps)));
824
+ return;
825
+ }
826
+ if (e.key === 'ArrowRight') {
827
+ e.preventDefault();
828
+ seekBy(e.shiftKey ? SKIP_MS : 1000 / Math.max(1, value.fps));
829
+ return;
830
+ }
831
+ if (e.key === 'Home') {
832
+ e.preventDefault();
833
+ seekTo(0);
834
+ return;
835
+ }
836
+ if (e.key === 'End') {
837
+ e.preventDefault();
838
+ seekTo(value.durationMs);
839
+ return;
840
+ }
841
+ if (e.key === 'm' || e.key === 'M') {
842
+ e.preventDefault();
843
+ muted = !muted;
844
+ return;
845
+ }
846
+ if ((e.key === 's' || e.key === 'S') && canSplit) {
847
+ e.preventDefault();
848
+ splitSelected();
849
+ return;
850
+ }
851
+ if ((e.key === 'd' || e.key === 'D') && (e.metaKey || e.ctrlKey) && selectedClipIds.length) {
852
+ e.preventDefault();
853
+ duplicateSelected();
854
+ return;
855
+ }
856
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selectedClipIds.length) {
857
+ e.preventDefault();
858
+ deleteSelected();
859
+ }
860
+ }
861
+
862
+ function exportJson() {
863
+ onexport?.({ format: 'json', document: value });
864
+ const blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json' });
865
+ const url = URL.createObjectURL(blob);
866
+ const a = document.createElement('a');
867
+ a.href = url;
868
+ a.download = 'video-sequence.json';
869
+ a.click();
870
+ URL.revokeObjectURL(url);
871
+ }
872
+
873
+ const tabBtn = (active: boolean) =>
874
+ [
875
+ 'flex-1 rounded-md px-1.5 py-1.5 text-[10px] font-medium transition-colors',
876
+ active ? 'bg-brand-500 text-white' : 'text-secondary hover:bg-surface-overlay'
877
+ ].join(' ');
878
+ </script>
879
+
880
+ <svelte:window onkeydown={handleKeydown} />
881
+
882
+ <div class={['relative flex h-full min-h-0 flex-col', className]}>
883
+ <div class="flex shrink-0 items-center gap-2 border-b border-border bg-surface-elevated px-3 py-2">
884
+ <span class="text-sm font-semibold text-primary">Video Editor</span>
885
+ <div class="ml-auto flex items-center gap-1">
886
+ <IconButton label="Toggle sidebar" size="sm" onclick={() => (showSidebar = !showSidebar)}>
887
+ <PanelLeft class="h-4 w-4" />
888
+ </IconButton>
889
+ <IconButton
890
+ label="Toggle inspector"
891
+ size="sm"
892
+ onclick={() => (showInspector = !showInspector)}
893
+ >
894
+ <PanelRight class="h-4 w-4" />
895
+ </IconButton>
896
+ <Button size="sm" variant="secondary" onclick={exportJson}>
897
+ <span class="inline-flex items-center gap-1.5">
898
+ <Download class="h-3.5 w-3.5" />
899
+ Export JSON
900
+ </span>
901
+ </Button>
902
+ </div>
903
+ </div>
904
+
905
+ <div class="min-h-0 flex-1 overflow-hidden">
906
+ <EditorShell
907
+ sidebarTitle="Library"
908
+ inspectorTitle={selectedClip
909
+ ? selectedClipIds.length > 1
910
+ ? `${selectedClipIds.length} clips`
911
+ : 'Clip'
912
+ : 'Sequence'}
913
+ workspaceTitle="Program"
914
+ bottomTitle="Timeline"
915
+ showBottom={true}
916
+ {showSidebar}
917
+ {showInspector}
918
+ wrapSidebar={true}
919
+ wrapInspector={true}
920
+ wrapWorkspace={false}
921
+ wrapBottom={false}
922
+ sidebarSize={280}
923
+ inspectorSize={280}
924
+ bottomSize={300}
925
+ sidebarMin={180}
926
+ sidebarMax={420}
927
+ inspectorMin={200}
928
+ inspectorMax={420}
929
+ bottomMin={180}
930
+ bottomMax={560}
931
+ sizeUnit="px"
932
+ fullHeight
933
+ class="h-full"
934
+ >
935
+ {#snippet sidebar()}
936
+ <div class="flex h-full min-h-0 flex-col">
937
+ <div class="flex shrink-0 gap-1 border-b border-border p-2">
938
+ <button
939
+ type="button"
940
+ class={tabBtn(sidebarTab === 'components')}
941
+ onclick={() => (sidebarTab = 'components')}
942
+ >
943
+ Comp
944
+ </button>
945
+ <button
946
+ type="button"
947
+ class={tabBtn(sidebarTab === 'animations')}
948
+ onclick={() => (sidebarTab = 'animations')}
949
+ >
950
+ Anim
951
+ </button>
952
+ <button
953
+ type="button"
954
+ class={tabBtn(sidebarTab === 'transitions')}
955
+ onclick={() => (sidebarTab = 'transitions')}
956
+ >
957
+ Trans
958
+ </button>
959
+ <button
960
+ type="button"
961
+ class={tabBtn(sidebarTab === 'assets')}
962
+ onclick={() => (sidebarTab = 'assets')}
963
+ >
964
+ Assets
965
+ </button>
966
+ </div>
967
+ <div class="min-h-0 flex-1 overflow-auto">
968
+ {#if sidebarTab === 'components'}
969
+ <VideoComponentsPanel onadd={addComponent} />
970
+ {:else if sidebarTab === 'animations'}
971
+ <VideoAnimationsPanel
972
+ disabled={!selectedClip}
973
+ onapply={applyAnimation}
974
+ />
975
+ {:else if sidebarTab === 'transitions'}
976
+ <VideoTransitionsPanel
977
+ disabled={!selectedClip}
978
+ onapply={applyTransition}
979
+ />
980
+ {:else}
981
+ <MediaAssetBrowser
982
+ assets={value.assets}
983
+ accept="image/*,video/*,audio/*"
984
+ helperText="Image, video or audio"
985
+ uploadLayout="vertical"
986
+ onassetschange={(assets) => emit({ ...value, assets })}
987
+ onselect={(id) => {
988
+ const asset = value.assets.find((a) => a.id === id);
989
+ if (asset) addAssetAsClip(asset);
990
+ }}
991
+ onadd={(asset) => {
992
+ emit({ ...value, assets: [...value.assets, asset] });
993
+ }}
994
+ />
995
+ {/if}
996
+ </div>
997
+ </div>
998
+ {/snippet}
999
+
1000
+ {#snippet workspace()}
1001
+ <div class="flex h-full min-h-0 flex-col bg-surface p-3">
1002
+ <div class="min-h-0 flex-1">
1003
+ <ProgramMonitor
1004
+ width={value.width}
1005
+ height={value.height}
1006
+ background={value.background}
1007
+ {currentTimeMs}
1008
+ {playing}
1009
+ {playbackRate}
1010
+ {muted}
1011
+ {volume}
1012
+ tracks={value.tracks}
1013
+ {selectedClipIds}
1014
+ editable={!playing}
1015
+ onselectclip={selectClip}
1016
+ onchangeclip={patchClip}
1017
+ onchangeclips={patchClips}
1018
+ onselectionchange={setSelection}
1019
+ onemptyclick={clearSelection}
1020
+ class="mx-auto max-h-full"
1021
+ />
1022
+ </div>
1023
+ </div>
1024
+ {/snippet}
1025
+
1026
+ {#snippet inspector()}
1027
+ {#if !selectedClip}
1028
+ <PropertyGroup title="Sequence">
1029
+ <PropertyField label="Width">
1030
+ <NumberInput
1031
+ size="sm"
1032
+ min={1}
1033
+ value={value.width}
1034
+ onchange={(v) => patchSequence({ width: v || value.width })}
1035
+ />
1036
+ </PropertyField>
1037
+ <PropertyField label="Height">
1038
+ <NumberInput
1039
+ size="sm"
1040
+ min={1}
1041
+ value={value.height}
1042
+ onchange={(v) => patchSequence({ height: v || value.height })}
1043
+ />
1044
+ </PropertyField>
1045
+ <PropertyField label="FPS">
1046
+ <NumberInput
1047
+ size="sm"
1048
+ min={1}
1049
+ value={value.fps}
1050
+ onchange={(v) => patchSequence({ fps: v || value.fps })}
1051
+ />
1052
+ </PropertyField>
1053
+ <PropertyField label="Duration (s)">
1054
+ <NumberInput
1055
+ size="sm"
1056
+ step={0.1}
1057
+ min={0.1}
1058
+ value={msToSec(value.durationMs)}
1059
+ onchange={(v) =>
1060
+ patchSequence({ durationMs: secToMs(v) || value.durationMs })}
1061
+ />
1062
+ </PropertyField>
1063
+ <PropertyField label="Background">
1064
+ <ColorPicker
1065
+ size="sm"
1066
+ value={value.background}
1067
+ showSwatches={false}
1068
+ onchange={(v) => patchSequence({ background: v })}
1069
+ />
1070
+ </PropertyField>
1071
+ </PropertyGroup>
1072
+ <p class="px-3 py-4 text-center text-xs text-muted">Select a clip to edit</p>
1073
+ {:else}
1074
+ <PropertyGroup title="Clip">
1075
+ <PropertyField label="Name">
1076
+ <Input
1077
+ size="sm"
1078
+ value={selectedClip.name}
1079
+ oninput={(e) =>
1080
+ patchClip({
1081
+ ...selectedClip,
1082
+ name: (e.currentTarget as HTMLInputElement).value
1083
+ })}
1084
+ />
1085
+ </PropertyField>
1086
+ <PropertyField label="Kind">
1087
+ <span class="text-xs capitalize text-secondary">{selectedClip.kind}</span>
1088
+ </PropertyField>
1089
+ <PropertyField label="Start (s)">
1090
+ <NumberInput
1091
+ size="sm"
1092
+ step={0.1}
1093
+ min={0}
1094
+ value={msToSec(selectedClip.startMs)}
1095
+ onchange={(v) => {
1096
+ const startMs = secToMs(v);
1097
+ const dur = selectedClip.endMs - selectedClip.startMs;
1098
+ patchClip({ ...selectedClip, startMs, endMs: startMs + dur });
1099
+ }}
1100
+ />
1101
+ </PropertyField>
1102
+ <PropertyField label="End (s)">
1103
+ <NumberInput
1104
+ size="sm"
1105
+ step={0.1}
1106
+ min={0}
1107
+ value={msToSec(selectedClip.endMs)}
1108
+ onchange={(v) => patchClip({ ...selectedClip, endMs: secToMs(v) })}
1109
+ />
1110
+ </PropertyField>
1111
+ <PropertyField label="Duration (s)">
1112
+ <NumberInput
1113
+ size="sm"
1114
+ step={0.1}
1115
+ min={0.1}
1116
+ value={msToSec(selectedClip.endMs - selectedClip.startMs)}
1117
+ onchange={(v) =>
1118
+ patchClip({
1119
+ ...selectedClip,
1120
+ endMs: selectedClip.startMs + Math.max(50, secToMs(v))
1121
+ })}
1122
+ />
1123
+ </PropertyField>
1124
+ <PropertyField
1125
+ label="Clip opacity"
1126
+ modified={clipFieldModified('opacity')}
1127
+ onreset={() => resetClipField('opacity')}
1128
+ >
1129
+ <Slider
1130
+ size="sm"
1131
+ min={0}
1132
+ max={1}
1133
+ step={0.01}
1134
+ value={selectedClip.opacity ?? 1}
1135
+ showValue
1136
+ valuePosition="header"
1137
+ oninput={(v) => patchClip({ ...selectedClip, opacity: v })}
1138
+ />
1139
+ </PropertyField>
1140
+ {#if selectedClip.kind !== 'audio'}
1141
+ <PropertyField
1142
+ label="Preview opacity"
1143
+ modified={clipFieldModified('previewOpacity')}
1144
+ onreset={() => resetClipField('previewOpacity')}
1145
+ >
1146
+ <Slider
1147
+ size="sm"
1148
+ min={0}
1149
+ max={1}
1150
+ step={0.01}
1151
+ value={selectedClip.previewOpacity ?? 1}
1152
+ showValue
1153
+ valuePosition="header"
1154
+ oninput={(v) => patchClip({ ...selectedClip, previewOpacity: v })}
1155
+ />
1156
+ </PropertyField>
1157
+ {/if}
1158
+ <PropertyField
1159
+ label="Color"
1160
+ modified={clipFieldModified('color')}
1161
+ onreset={() => resetClipField('color')}
1162
+ >
1163
+ <ColorPicker
1164
+ size="sm"
1165
+ value={selectedClip.color ?? '#3b82f6'}
1166
+ showSwatches={false}
1167
+ onchange={(v) => patchClip({ ...selectedClip, color: v })}
1168
+ />
1169
+ </PropertyField>
1170
+ </PropertyGroup>
1171
+
1172
+ {#if showTransform && selectedRect}
1173
+ <PropertyGroup title="Transform">
1174
+ <div class="mx-1 mb-1.5 flex w-full min-w-0 flex-col gap-1.5">
1175
+ <div
1176
+ class="flex w-full flex-nowrap items-center justify-between gap-0 overflow-x-auto rounded-xl border border-border bg-surface-elevated p-0.5 shadow-sm"
1177
+ role="toolbar"
1178
+ aria-label="Position"
1179
+ >
1180
+ {#each alignDock as action (action.id)}
1181
+ {@const Icon = action.icon}
1182
+ <Tooltip content={action.label} side="top" class="shrink-0">
1183
+ <IconButton
1184
+ label={action.label}
1185
+ size="xs"
1186
+ variant="ghost"
1187
+ onclick={() => applyAlign(action.id)}
1188
+ >
1189
+ <Icon class="h-3.5 w-3.5" />
1190
+ </IconButton>
1191
+ </Tooltip>
1192
+ {/each}
1193
+ </div>
1194
+ </div>
1195
+ <PropertyField
1196
+ label="X"
1197
+ modified={clipFieldModified('rect.x')}
1198
+ onreset={() => resetClipField('rect.x')}
1199
+ >
1200
+ <NumberInput
1201
+ size="sm"
1202
+ value={Math.round(selectedRect.x)}
1203
+ onchange={(v) => patchSelectedRect({ x: v })}
1204
+ />
1205
+ </PropertyField>
1206
+ <PropertyField
1207
+ label="Y"
1208
+ modified={clipFieldModified('rect.y')}
1209
+ onreset={() => resetClipField('rect.y')}
1210
+ >
1211
+ <NumberInput
1212
+ size="sm"
1213
+ value={Math.round(selectedRect.y)}
1214
+ onchange={(v) => patchSelectedRect({ y: v })}
1215
+ />
1216
+ </PropertyField>
1217
+ <PropertyField
1218
+ label="W"
1219
+ modified={clipFieldModified('rect.w')}
1220
+ onreset={() => resetClipField('rect.w')}
1221
+ >
1222
+ <NumberInput
1223
+ size="sm"
1224
+ min={1}
1225
+ value={Math.round(selectedRect.w)}
1226
+ onchange={(v) => patchSelectedRect({ w: v })}
1227
+ />
1228
+ </PropertyField>
1229
+ <PropertyField
1230
+ label="H"
1231
+ modified={clipFieldModified('rect.h')}
1232
+ onreset={() => resetClipField('rect.h')}
1233
+ >
1234
+ <NumberInput
1235
+ size="sm"
1236
+ min={1}
1237
+ value={Math.round(selectedRect.h)}
1238
+ onchange={(v) => patchSelectedRect({ h: v })}
1239
+ />
1240
+ </PropertyField>
1241
+ </PropertyGroup>
1242
+ {/if}
1243
+
1244
+ {#if selectedClip.kind === 'text'}
1245
+ <PropertyGroup title="Text">
1246
+ <PropertyField label="Content">
1247
+ <Input
1248
+ size="sm"
1249
+ value={selectedClip.text ?? ''}
1250
+ oninput={(e) =>
1251
+ patchClip({
1252
+ ...selectedClip,
1253
+ text: (e.currentTarget as HTMLInputElement).value
1254
+ })}
1255
+ />
1256
+ </PropertyField>
1257
+ <PropertyField
1258
+ label="Color"
1259
+ modified={clipFieldModified('textColor')}
1260
+ onreset={() => resetClipField('textColor')}
1261
+ >
1262
+ <ColorPicker
1263
+ size="sm"
1264
+ value={selectedClip.textColor ?? '#ffffff'}
1265
+ showSwatches={false}
1266
+ onchange={(v) => patchClip({ ...selectedClip, textColor: v })}
1267
+ />
1268
+ </PropertyField>
1269
+ <PropertyField
1270
+ label="Size"
1271
+ modified={clipFieldModified('fontSize')}
1272
+ onreset={() => resetClipField('fontSize')}
1273
+ >
1274
+ <Slider
1275
+ size="sm"
1276
+ min={8}
1277
+ max={120}
1278
+ step={1}
1279
+ value={selectedClip.fontSize ?? 18}
1280
+ showValue
1281
+ valuePosition="header"
1282
+ oninput={(v) => patchClip({ ...selectedClip, fontSize: v })}
1283
+ />
1284
+ </PropertyField>
1285
+ <PropertyField
1286
+ label="Weight"
1287
+ modified={clipFieldModified('fontWeight')}
1288
+ onreset={() => resetClipField('fontWeight')}
1289
+ >
1290
+ <Slider
1291
+ size="sm"
1292
+ min={100}
1293
+ max={900}
1294
+ step={100}
1295
+ value={Number(selectedClip.fontWeight ?? 600)}
1296
+ showValue
1297
+ valuePosition="header"
1298
+ oninput={(v) => patchClip({ ...selectedClip, fontWeight: v })}
1299
+ />
1300
+ </PropertyField>
1301
+ <PropertyField
1302
+ label="Align"
1303
+ modified={clipFieldModified('textAlign')}
1304
+ onreset={() => resetClipField('textAlign')}
1305
+ >
1306
+ <div
1307
+ class="flex w-full items-center justify-between gap-0 rounded-xl border border-border bg-surface-elevated p-0.5 shadow-sm"
1308
+ role="toolbar"
1309
+ aria-label="Text align"
1310
+ >
1311
+ {#each textAlignDock as action (action.id)}
1312
+ {@const Icon = action.icon}
1313
+ {@const active = (selectedClip.textAlign ?? 'center') === action.id}
1314
+ <Tooltip content={action.label} side="top" class="min-w-0 flex-1">
1315
+ <IconButton
1316
+ label={action.label}
1317
+ size="xs"
1318
+ variant={active ? 'secondary' : 'ghost'}
1319
+ class="w-full"
1320
+ onclick={() =>
1321
+ patchClip({ ...selectedClip, textAlign: action.id })}
1322
+ >
1323
+ <Icon class="h-3.5 w-3.5" />
1324
+ </IconButton>
1325
+ </Tooltip>
1326
+ {/each}
1327
+ </div>
1328
+ </PropertyField>
1329
+ </PropertyGroup>
1330
+ {/if}
1331
+
1332
+ {#if selectedClip.kind === 'video' || selectedClip.kind === 'image'}
1333
+ <PropertyGroup title={selectedClip.kind === 'video' ? 'Video' : 'Image'}>
1334
+ <div class="px-1 pb-1">
1335
+ {#key selectedClip.id}
1336
+ <FileUploader
1337
+ label="Source"
1338
+ variant="single"
1339
+ layout="vertical"
1340
+ accept={acceptForClipKind(selectedClip.kind)}
1341
+ helperText={selectedClip.kind === 'video' ? 'MP4, WebM…' : 'PNG, JPG, WebP…'}
1342
+ src={selectedClip.src ?? ''}
1343
+ srcName={selectedClip.name}
1344
+ maxSizeMb={200}
1345
+ showFileList={false}
1346
+ onchange={(files) => void replaceSelectedMedia(files)}
1347
+ onclear={clearSelectedMedia}
1348
+ />
1349
+ {/key}
1350
+ </div>
1351
+ {#if selectedClip.kind === 'video'}
1352
+ <PropertyField
1353
+ label="Volume"
1354
+ modified={clipFieldModified('volume')}
1355
+ onreset={() => resetClipField('volume')}
1356
+ >
1357
+ <Slider
1358
+ size="sm"
1359
+ min={0}
1360
+ max={1}
1361
+ step={0.01}
1362
+ value={selectedClip.volume ?? 1}
1363
+ showValue
1364
+ valuePosition="header"
1365
+ oninput={(v) => patchClip({ ...selectedClip, volume: v })}
1366
+ />
1367
+ </PropertyField>
1368
+ {/if}
1369
+ <PropertyField
1370
+ label="Trim in (s)"
1371
+ modified={clipFieldModified('trimInMs')}
1372
+ onreset={() => resetClipField('trimInMs')}
1373
+ >
1374
+ <NumberInput
1375
+ size="sm"
1376
+ step={0.1}
1377
+ min={0}
1378
+ value={msToSec(selectedClip.trimInMs ?? 0)}
1379
+ onchange={(v) => patchClip({ ...selectedClip, trimInMs: secToMs(v) })}
1380
+ />
1381
+ </PropertyField>
1382
+ <PropertyField
1383
+ label="Trim out (s)"
1384
+ modified={clipFieldModified('trimOutMs')}
1385
+ onreset={() => resetClipField('trimOutMs')}
1386
+ >
1387
+ <NumberInput
1388
+ size="sm"
1389
+ step={0.1}
1390
+ min={0}
1391
+ value={msToSec(selectedClip.trimOutMs ?? 0)}
1392
+ onchange={(v) =>
1393
+ patchClip({
1394
+ ...selectedClip,
1395
+ trimOutMs: secToMs(v) || undefined
1396
+ })}
1397
+ />
1398
+ </PropertyField>
1399
+ </PropertyGroup>
1400
+ {/if}
1401
+
1402
+ {#if selectedClip.kind === 'audio'}
1403
+ <PropertyGroup title="Audio">
1404
+ <div class="px-1 pb-1">
1405
+ {#key selectedClip.id}
1406
+ <FileUploader
1407
+ label="Source"
1408
+ variant="single"
1409
+ layout="vertical"
1410
+ accept={acceptForClipKind('audio')}
1411
+ helperText="MP3, WAV, OGG…"
1412
+ src={selectedClip.src ?? ''}
1413
+ srcName={selectedClip.name}
1414
+ maxSizeMb={200}
1415
+ showFileList={false}
1416
+ onchange={(files) => void replaceSelectedMedia(files)}
1417
+ onclear={clearSelectedMedia}
1418
+ />
1419
+ {/key}
1420
+ </div>
1421
+ <PropertyField
1422
+ label="Volume"
1423
+ modified={clipFieldModified('volume')}
1424
+ onreset={() => resetClipField('volume')}
1425
+ >
1426
+ <Slider
1427
+ size="sm"
1428
+ min={0}
1429
+ max={1}
1430
+ step={0.01}
1431
+ value={selectedClip.volume ?? 1}
1432
+ showValue
1433
+ valuePosition="header"
1434
+ oninput={(v) => patchClip({ ...selectedClip, volume: v })}
1435
+ />
1436
+ </PropertyField>
1437
+ <PropertyField
1438
+ label="Trim in (s)"
1439
+ modified={clipFieldModified('trimInMs')}
1440
+ onreset={() => resetClipField('trimInMs')}
1441
+ >
1442
+ <NumberInput
1443
+ size="sm"
1444
+ step={0.1}
1445
+ min={0}
1446
+ value={msToSec(selectedClip.trimInMs ?? 0)}
1447
+ onchange={(v) => patchClip({ ...selectedClip, trimInMs: secToMs(v) })}
1448
+ />
1449
+ </PropertyField>
1450
+ <PropertyField
1451
+ label="Trim out (s)"
1452
+ modified={clipFieldModified('trimOutMs')}
1453
+ onreset={() => resetClipField('trimOutMs')}
1454
+ >
1455
+ <NumberInput
1456
+ size="sm"
1457
+ step={0.1}
1458
+ min={0}
1459
+ value={msToSec(selectedClip.trimOutMs ?? 0)}
1460
+ onchange={(v) =>
1461
+ patchClip({
1462
+ ...selectedClip,
1463
+ trimOutMs: secToMs(v) || undefined
1464
+ })}
1465
+ />
1466
+ </PropertyField>
1467
+ </PropertyGroup>
1468
+ {/if}
1469
+
1470
+ {#if selectedClip.animationIn || selectedClip.animationOut || selectedClip.transitionOut}
1471
+ <PropertyGroup title="Effects">
1472
+ {#if selectedClip.animationIn}
1473
+ <PropertyField label="Anim in">
1474
+ <div class="flex min-w-0 items-center gap-1">
1475
+ <span class="min-w-0 flex-1 truncate text-xs capitalize text-secondary">
1476
+ {selectedClip.animationIn.type}
1477
+ </span>
1478
+ <IconButton
1479
+ label="Remove animation in"
1480
+ size="xs"
1481
+ onclick={() => {
1482
+ const { animationIn: _a, ...rest } = selectedClip;
1483
+ patchClip(rest);
1484
+ }}
1485
+ >
1486
+ <Trash2 class="h-3 w-3" />
1487
+ </IconButton>
1488
+ </div>
1489
+ </PropertyField>
1490
+ <PropertyField label="In duration (s)">
1491
+ <NumberInput
1492
+ size="sm"
1493
+ step={0.05}
1494
+ min={0.05}
1495
+ value={msToSec(selectedClip.animationIn.durationMs)}
1496
+ onchange={(v) =>
1497
+ patchClip({
1498
+ ...selectedClip,
1499
+ animationIn: {
1500
+ ...selectedClip.animationIn!,
1501
+ durationMs: Math.max(50, secToMs(v))
1502
+ }
1503
+ })}
1504
+ />
1505
+ </PropertyField>
1506
+ {/if}
1507
+ {#if selectedClip.animationOut}
1508
+ <PropertyField label="Anim out">
1509
+ <div class="flex min-w-0 items-center gap-1">
1510
+ <span class="min-w-0 flex-1 truncate text-xs capitalize text-secondary">
1511
+ {selectedClip.animationOut.type}
1512
+ </span>
1513
+ <IconButton
1514
+ label="Remove animation out"
1515
+ size="xs"
1516
+ onclick={() => {
1517
+ const { animationOut: _a, ...rest } = selectedClip;
1518
+ patchClip(rest);
1519
+ }}
1520
+ >
1521
+ <Trash2 class="h-3 w-3" />
1522
+ </IconButton>
1523
+ </div>
1524
+ </PropertyField>
1525
+ <PropertyField label="Out duration (s)">
1526
+ <NumberInput
1527
+ size="sm"
1528
+ step={0.05}
1529
+ min={0.05}
1530
+ value={msToSec(selectedClip.animationOut.durationMs)}
1531
+ onchange={(v) =>
1532
+ patchClip({
1533
+ ...selectedClip,
1534
+ animationOut: {
1535
+ ...selectedClip.animationOut!,
1536
+ durationMs: Math.max(50, secToMs(v))
1537
+ }
1538
+ })}
1539
+ />
1540
+ </PropertyField>
1541
+ {/if}
1542
+ {#if selectedClip.transitionOut}
1543
+ <PropertyField label="Transition">
1544
+ <div class="flex min-w-0 items-center gap-1">
1545
+ <span class="min-w-0 flex-1 truncate text-xs capitalize text-secondary">
1546
+ {selectedClip.transitionOut.type}
1547
+ </span>
1548
+ <IconButton
1549
+ label="Remove transition"
1550
+ size="xs"
1551
+ onclick={() => {
1552
+ const { transitionOut: _t, ...rest } = selectedClip;
1553
+ patchClip(rest);
1554
+ }}
1555
+ >
1556
+ <Trash2 class="h-3 w-3" />
1557
+ </IconButton>
1558
+ </div>
1559
+ </PropertyField>
1560
+ <PropertyField label="Trans duration (s)">
1561
+ <NumberInput
1562
+ size="sm"
1563
+ step={0.05}
1564
+ min={0.05}
1565
+ value={msToSec(selectedClip.transitionOut.durationMs)}
1566
+ onchange={(v) =>
1567
+ patchClip({
1568
+ ...selectedClip,
1569
+ transitionOut: {
1570
+ ...selectedClip.transitionOut!,
1571
+ durationMs: Math.max(50, secToMs(v))
1572
+ }
1573
+ })}
1574
+ />
1575
+ </PropertyField>
1576
+ {/if}
1577
+ </PropertyGroup>
1578
+ {:else}
1579
+ <PropertyGroup title="Effects">
1580
+ <p class="px-2 py-2 text-[10px] leading-relaxed text-muted">
1581
+ Select Animations / Transitions in the library, then click or drag onto this clip.
1582
+ CapCut-style: one <span class="text-secondary">In</span> + one
1583
+ <span class="text-secondary"> Out</span> + optional transition at the end.
1584
+ </p>
1585
+ </PropertyGroup>
1586
+ {/if}
1587
+
1588
+ {#if canSplit || canMerge}
1589
+ <div class="mt-3 flex flex-col gap-2 px-3 pb-3">
1590
+ {#if canSplit}
1591
+ <Button size="sm" variant="secondary" onclick={splitSelected}>
1592
+ <span class="inline-flex items-center gap-1.5">
1593
+ <Scissors class="h-3.5 w-3.5" />
1594
+ Split at playhead
1595
+ </span>
1596
+ </Button>
1597
+ {/if}
1598
+ {#if canMerge}
1599
+ <Button size="sm" variant="secondary" onclick={mergeSelected}>Merge selected</Button>
1600
+ {/if}
1601
+ </div>
1602
+ {/if}
1603
+ {/if}
1604
+ {/snippet}
1605
+
1606
+ {#snippet bottom()}
1607
+ <div class="flex h-full min-h-0 flex-col gap-1 p-1">
1608
+ <div class="shrink-0 rounded-md border border-border bg-surface-elevated px-2 py-1">
1609
+ <TransportControls
1610
+ {playing}
1611
+ {currentTimeMs}
1612
+ durationMs={value.durationMs}
1613
+ bind:playbackRate
1614
+ bind:muted
1615
+ bind:volume
1616
+ skipMs={SKIP_MS}
1617
+ onplay={() => (playing = true)}
1618
+ onpause={() => (playing = false)}
1619
+ onstop={() => {
1620
+ playing = false;
1621
+ seekTo(0);
1622
+ }}
1623
+ onseekstart={() => seekTo(0)}
1624
+ onseekend={() => seekTo(value.durationMs)}
1625
+ onseekback={() => seekBy(-SKIP_MS)}
1626
+ onseekforward={() => seekBy(SKIP_MS)}
1627
+ />
1628
+ </div>
1629
+ <div class="min-h-0 flex-1">
1630
+ <MediaTimeline
1631
+ tracks={value.tracks}
1632
+ durationMs={value.durationMs}
1633
+ bind:currentTimeMs
1634
+ bind:zoom
1635
+ {selectedClipIds}
1636
+ showTrackActions
1637
+ reorderTracks
1638
+ allowRemoveTracks
1639
+ onselectclip={selectClip}
1640
+ oncontextmenuclip={openClipMenu}
1641
+ oncontextmenutrack={openTrackMenu}
1642
+ onchangeclip={patchClip}
1643
+ onmoveclips={moveSelectedClips}
1644
+ ondropclip={handleClipDrop}
1645
+ ondroptrack={handleTrackDrop}
1646
+ onselectionchange={setSelection}
1647
+ onchangetrack={patchTrack}
1648
+ onaddtrack={addTrack}
1649
+ onremovetrack={deleteTrack}
1650
+ onreordertracks={reorderTrackIds}
1651
+ onemptyclick={() => clearSelection()}
1652
+ headerVariant="video"
1653
+ onseek={(ms) => (currentTimeMs = ms)}
1654
+ class="h-full min-h-0"
1655
+ />
1656
+ </div>
1657
+ </div>
1658
+ {/snippet}
1659
+ </EditorShell>
1660
+ </div>
1661
+
1662
+ {#if selectedClipIds.length > 1}
1663
+ <div class="pointer-events-none absolute inset-x-0 bottom-4 z-40 flex justify-center px-4">
1664
+ <div class="pointer-events-auto">
1665
+ <BulkActionBar
1666
+ count={selectedClipIds.length}
1667
+ itemLabel="clip"
1668
+ itemLabelPlural="clips"
1669
+ placement="dock"
1670
+ compact
1671
+ showSelectAll={false}
1672
+ maxPrimary={4}
1673
+ actions={bulkActions}
1674
+ onaction={handleBulk}
1675
+ onclear={clearSelection}
1676
+ />
1677
+ </div>
1678
+ </div>
1679
+ {/if}
1680
+ </div>
1681
+
1682
+ <ContextMenu
1683
+ items={ctxItems}
1684
+ bind:open={ctxOpen}
1685
+ bind:anchor={ctxAnchor}
1686
+ onselect={handleCtx}
1687
+ />