@hyperframes/studio 0.8.50 → 0.8.53-desktop.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 (330) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-PTEnAHhg.js} +3 -3
  2. package/dist/assets/{index-DUAIWxkI.js → index--myXp8ui.js} +1 -1
  3. package/dist/assets/{index-Dt3qBI-9.js → index-BjslOS9g.js} +1 -1
  4. package/dist/assets/index-BnXZuIJD.css +1 -0
  5. package/dist/assets/index-CNyviK_l.js +390 -0
  6. package/dist/index.css +29 -0
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.ts +825 -24
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +8911 -9853
  11. package/dist/index.js.map +1 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +13 -12
  14. package/src/App.tsx +145 -163
  15. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  16. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  17. package/src/captions/components/CaptionOverlay.tsx +8 -4
  18. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  19. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  20. package/src/captions/components/CaptionTimeline.tsx +1 -1
  21. package/src/captions/components/shared.tsx +2 -2
  22. package/src/components/AskAgentModal.tsx +5 -5
  23. package/src/components/EditorShell.selectionSync.test.tsx +9 -4
  24. package/src/components/EditorShell.tsx +59 -62
  25. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  26. package/src/components/LintModal.tsx +6 -16
  27. package/src/components/MediaPreview.tsx +2 -2
  28. package/src/components/RightPanelTabs.test.tsx +151 -0
  29. package/src/components/RightPanelTabs.tsx +59 -0
  30. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  31. package/src/components/StudioHeader.dom.test.tsx +181 -0
  32. package/src/components/StudioHeader.test.ts +0 -12
  33. package/src/components/StudioHeader.tsx +118 -221
  34. package/src/components/StudioLeftPanels.tsx +202 -0
  35. package/src/components/StudioOverlays.tsx +1 -1
  36. package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
  37. package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
  38. package/src/components/StudioToast.tsx +2 -2
  39. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  40. package/src/components/TimelineHistoryButtons.tsx +54 -0
  41. package/src/components/TimelineToolbar.test.tsx +8 -0
  42. package/src/components/TimelineToolbar.tsx +8 -12
  43. package/src/components/dock/Dock.test.tsx +274 -0
  44. package/src/components/dock/Dock.tsx +291 -0
  45. package/src/components/dock/dock.css +28 -0
  46. package/src/components/dock/dockAccessibility.test.ts +166 -0
  47. package/src/components/dock/dockAccessibility.ts +98 -0
  48. package/src/components/dock/dockLayout.test.ts +134 -0
  49. package/src/components/dock/dockLayout.ts +105 -0
  50. package/src/components/dock/dockLayoutSchema.ts +66 -0
  51. package/src/components/dock/dockLayoutStore.test.ts +28 -0
  52. package/src/components/dock/dockLayoutStore.ts +108 -0
  53. package/src/components/dock/dockTestHarness.ts +11 -0
  54. package/src/components/dock/panelRegistry.ts +67 -0
  55. package/src/components/editor/AnimationCard.tsx +4 -4
  56. package/src/components/editor/AnimationCardParts.tsx +6 -6
  57. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  58. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  59. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  60. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  61. package/src/components/editor/DomEditOverlay.tsx +1 -1
  62. package/src/components/editor/EaseCurveSection.tsx +1 -1
  63. package/src/components/editor/EaseParamFields.tsx +5 -5
  64. package/src/components/editor/FileTree.tsx +4 -4
  65. package/src/components/editor/FileTreeIcons.tsx +1 -1
  66. package/src/components/editor/FileTreeNodes.tsx +13 -13
  67. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  68. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  69. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  70. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  71. package/src/components/editor/LayersPanel.test.ts +97 -1
  72. package/src/components/editor/LayersPanel.tsx +7 -5
  73. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  74. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  75. package/src/components/editor/PreviewGuides.tsx +1 -1
  76. package/src/components/editor/PromotableControl.tsx +2 -2
  77. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  78. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  79. package/src/components/editor/SnapToolbar.test.tsx +0 -3
  80. package/src/components/editor/SnapToolbar.tsx +2 -2
  81. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  82. package/src/components/editor/TopologyLens.test.tsx +28 -0
  83. package/src/components/editor/TopologyLens.tsx +7 -5
  84. package/src/components/editor/Transform3DCube.tsx +2 -2
  85. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  86. package/src/components/editor/domEditing.test.ts +1 -1
  87. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  88. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  89. package/src/components/editor/propertyPanelColor.tsx +11 -11
  90. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  91. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  92. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  93. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  94. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  95. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  96. package/src/components/editor/propertyPanelFill.tsx +4 -4
  97. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  98. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  99. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  100. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  101. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  102. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  103. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  104. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  105. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  106. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  107. package/src/components/editor/propertyPanelFont.tsx +5 -5
  108. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  109. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  110. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  111. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  112. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  113. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  114. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  115. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  116. package/src/components/editor/propertyPanelSections.tsx +8 -8
  117. package/src/components/editor/useColorGradingController.test.ts +38 -0
  118. package/src/components/editor/useColorGradingController.ts +9 -1
  119. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  120. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  121. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  122. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  123. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  124. package/src/components/feedback/feedbackTrigger.ts +0 -1
  125. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  126. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  127. package/src/components/nle/NLEContext.tsx +0 -43
  128. package/src/components/nle/NLEPreview.tsx +3 -3
  129. package/src/components/nle/PreviewOverlays.tsx +6 -6
  130. package/src/components/nle/PreviewPane.tsx +3 -3
  131. package/src/components/nle/TimelinePane.tsx +53 -67
  132. package/src/components/panels/SlideshowPanel.tsx +3 -3
  133. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  134. package/src/components/panels/VariablesBindElement.tsx +2 -2
  135. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  136. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  137. package/src/components/panels/VariablesPanel.tsx +4 -4
  138. package/src/components/panels/VariablesValueControls.tsx +2 -2
  139. package/src/components/pointerTargetSize.test.tsx +8 -0
  140. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  141. package/src/components/renders/RenderQueue.test.tsx +7 -0
  142. package/src/components/renders/RenderQueue.tsx +7 -7
  143. package/src/components/renders/RenderQueueItem.tsx +6 -6
  144. package/src/components/renders/RenderQueuePanel.tsx +1 -1
  145. package/src/components/sidebar/AssetCard.tsx +8 -8
  146. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  147. package/src/components/sidebar/AssetsTab.tsx +2 -2
  148. package/src/components/sidebar/AudioRow.tsx +6 -6
  149. package/src/components/sidebar/BlocksTab.tsx +6 -6
  150. package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
  151. package/src/components/sidebar/CompositionsPanel.tsx +15 -0
  152. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  153. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  154. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  155. package/src/components/sidebar/SidebarLintButton.test.tsx +53 -0
  156. package/src/components/sidebar/SidebarLintButton.tsx +50 -0
  157. package/src/components/timelineToolbarStyles.ts +5 -0
  158. package/src/components/ui/Button.test.tsx +252 -0
  159. package/src/components/ui/Button.tsx +89 -126
  160. package/src/components/ui/IconButton.tsx +38 -0
  161. package/src/components/ui/Input.tsx +115 -0
  162. package/src/components/ui/Menu.test.tsx +374 -0
  163. package/src/components/ui/Menu.tsx +213 -0
  164. package/src/components/ui/NumberField.tsx +122 -0
  165. package/src/components/ui/Popover.tsx +74 -0
  166. package/src/components/ui/SearchInput.tsx +2 -2
  167. package/src/components/ui/Select.tsx +100 -0
  168. package/src/components/ui/Slider.tsx +153 -0
  169. package/src/components/ui/Tabs.test.tsx +98 -0
  170. package/src/components/ui/Tabs.tsx +71 -0
  171. package/src/components/ui/Toggle.tsx +63 -0
  172. package/src/components/ui/Tooltip.test.tsx +110 -0
  173. package/src/components/ui/Tooltip.tsx +38 -112
  174. package/src/components/ui/cn.test.ts +30 -0
  175. package/src/components/ui/cn.ts +25 -0
  176. package/src/components/ui/index.ts +29 -2
  177. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  178. package/src/components/ui/valueControls.test.tsx +524 -0
  179. package/src/contexts/PanelLayoutContext.tsx +3 -57
  180. package/src/hooks/appHotkeysDispatch.ts +3 -4
  181. package/src/hooks/timelineEditPermission.ts +31 -0
  182. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
  183. package/src/hooks/useAppHotkeys.test.ts +0 -1
  184. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
  185. package/src/hooks/useAppHotkeys.ts +21 -84
  186. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  187. package/src/hooks/useCaptionDetection.ts +3 -1
  188. package/src/hooks/useDomEditPreviewSync.ts +4 -6
  189. package/src/hooks/useDomEditSession.ts +0 -7
  190. package/src/hooks/useDomEditWiring.ts +4 -9
  191. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  192. package/src/hooks/useEditHistoryActions.ts +106 -0
  193. package/src/hooks/useFileManager.ts +21 -123
  194. package/src/hooks/useLintModal.test.tsx +65 -0
  195. package/src/hooks/useLintModal.ts +3 -0
  196. package/src/hooks/usePanelLayout.test.ts +108 -296
  197. package/src/hooks/usePanelLayout.ts +50 -255
  198. package/src/hooks/usePersistentEditHistory.ts +1 -1
  199. package/src/hooks/useProjectFileWriter.ts +150 -0
  200. package/src/hooks/useRightPanelIntents.test.ts +181 -0
  201. package/src/hooks/useRightPanelIntents.ts +63 -0
  202. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  203. package/src/hooks/useSdkSession.ts +43 -10
  204. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  205. package/src/hooks/useServerConnection.ts +43 -10
  206. package/src/hooks/useSlideshowTabState.test.ts +9 -31
  207. package/src/hooks/useSlideshowTabState.ts +6 -19
  208. package/src/hooks/useStudioContextValue.test.ts +6 -18
  209. package/src/hooks/useStudioContextValue.ts +5 -26
  210. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  211. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  212. package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
  213. package/src/hooks/useTimelineEditing.test.tsx +322 -3
  214. package/src/hooks/useTimelineEditing.ts +103 -16
  215. package/src/hooks/useTimelineEditingTypes.ts +3 -0
  216. package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
  217. package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
  218. package/src/hostMount.test.tsx +222 -0
  219. package/src/index.ts +87 -0
  220. package/src/main.tsx +9 -3
  221. package/src/player/components/AudioWaveform.tsx +1 -1
  222. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  223. package/src/player/components/ClipContextMenu.tsx +3 -3
  224. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  225. package/src/player/components/CompositionThumbnail.tsx +3 -4
  226. package/src/player/components/EditModal.tsx +1 -1
  227. package/src/player/components/ImageThumbnail.tsx +1 -1
  228. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  229. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  230. package/src/player/components/PlayerControls.tsx +1 -1
  231. package/src/player/components/ShortcutsPanel.tsx +9 -9
  232. package/src/player/components/SpeedMenu.tsx +3 -5
  233. package/src/player/components/Timeline.tsx +3 -2
  234. package/src/player/components/TimelineCanvas.tsx +21 -14
  235. package/src/player/components/TimelineClip.tsx +0 -1
  236. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  237. package/src/player/components/TimelineEmptyState.tsx +5 -5
  238. package/src/player/components/TimelineFxButton.tsx +6 -6
  239. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  240. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  241. package/src/player/components/TimelineGroupRow.tsx +1 -1
  242. package/src/player/components/TimelineLanes.test.tsx +32 -0
  243. package/src/player/components/TimelineLanes.tsx +2 -5
  244. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  245. package/src/player/components/TimelineRuler.tsx +1 -1
  246. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  247. package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
  248. package/src/player/components/TimelineTrackHeader.tsx +2 -2
  249. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  250. package/src/player/components/TimelineTypes.ts +2 -1
  251. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  252. package/src/player/components/VideoThumbnail.tsx +4 -2
  253. package/src/player/components/VolumeControl.tsx +2 -2
  254. package/src/player/components/timelineCallbacks.ts +8 -8
  255. package/src/player/components/timelineClipDragPreview.ts +51 -27
  256. package/src/player/components/timelineClipDragTypes.ts +3 -0
  257. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  258. package/src/player/components/timelineDragDrop.ts +57 -15
  259. package/src/player/components/timelineLaneProps.ts +3 -0
  260. package/src/player/components/timelineSnapping.test.ts +27 -0
  261. package/src/player/components/timelineSnapping.ts +14 -1
  262. package/src/player/components/timelineTheme.test.ts +4 -4
  263. package/src/player/components/timelineTheme.ts +30 -30
  264. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  265. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  266. package/src/player/components/useTimelineClipDrag.ts +30 -3
  267. package/src/player/lib/describeClips.ts +3 -54
  268. package/src/player/lib/playbackShortcuts.ts +2 -0
  269. package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
  270. package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
  271. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  272. package/src/player/store/previewIframeStore.test.tsx +29 -0
  273. package/src/player/store/previewIframeStore.ts +25 -0
  274. package/src/renderQueueExports.test.tsx +58 -0
  275. package/src/styles/classCandidates.test.ts +105 -0
  276. package/src/styles/classCandidates.ts +281 -0
  277. package/src/styles/hex-baseline.json +84 -0
  278. package/src/styles/hexRatchet.test.ts +201 -0
  279. package/src/styles/iconTokens.ts +20 -0
  280. package/src/styles/palette-reset.css +6 -0
  281. package/src/styles/studio.css +24 -21
  282. package/src/styles/styleSources.test.ts +20 -0
  283. package/src/styles/styleSources.ts +47 -0
  284. package/src/styles/tailwind-preset.shared.js +3 -0
  285. package/src/styles/theme.css +270 -0
  286. package/src/styles/theme.test.ts +183 -0
  287. package/src/styles/tokenGate.test.ts +109 -0
  288. package/src/utils/sourcePatcher.ts +1 -1
  289. package/src/utils/studioHelpers.test.ts +18 -0
  290. package/src/utils/studioHelpers.ts +11 -8
  291. package/src/utils/studioUiPreferences.test.ts +10 -16
  292. package/src/utils/studioUiPreferences.ts +7 -16
  293. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  294. package/src/utils/timelineDropTrackInsert.ts +83 -0
  295. package/src/utils/typingTarget.ts +4 -0
  296. package/dist/assets/index-n2ml4tUn.js +0 -442
  297. package/dist/assets/index-pO6p0H27.css +0 -1
  298. package/src/components/PanelTabButton.tsx +0 -31
  299. package/src/components/StudioLeftSidebar.tsx +0 -204
  300. package/src/components/nle/TimelineResizeDivider.tsx +0 -101
  301. package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
  302. package/src/components/sidebar/LeftSidebar.tsx +0 -334
  303. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  304. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  305. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  306. package/src/components/storyboard/FramePoster.tsx +0 -86
  307. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  308. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  309. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  310. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  311. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  312. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  313. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  314. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  315. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  316. package/src/components/storyboard/StoryboardView.tsx +0 -169
  317. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  318. package/src/components/storyboard/frameComments.test.ts +0 -95
  319. package/src/components/storyboard/frameComments.ts +0 -115
  320. package/src/components/storyboard/frameStatus.ts +0 -36
  321. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  322. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  323. package/src/components/storyboard/useFrameComments.ts +0 -98
  324. package/src/contexts/ViewModeContext.tsx +0 -142
  325. package/src/hooks/useInspectorSplitResize.ts +0 -51
  326. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  327. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  328. package/src/hooks/useStoryboard.ts +0 -99
  329. package/src/utils/fitPanels.test.ts +0 -164
  330. package/src/utils/fitPanels.ts +0 -151
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { useTimelineAssetDropOps } from "./useTimelineAssetDropOps";
6
6
  import { mountReactHarness } from "./domSelectionTestHarness";
7
7
  import { usePlayerStore } from "../player/store/playerStore";
8
+ import type { TimelineElement } from "../player";
8
9
 
9
10
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
11
 
@@ -19,6 +20,7 @@ type DropFn = ReturnType<typeof useTimelineAssetDropOps>["handleTimelineAssetDro
19
20
  function renderDropHook(
20
21
  sourceContent: string,
21
22
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>,
23
+ timelineElements: TimelineElement[] = [],
22
24
  ) {
23
25
  vi.stubGlobal(
24
26
  "fetch",
@@ -29,7 +31,7 @@ function renderDropHook(
29
31
  const { handleTimelineAssetDrop } = useTimelineAssetDropOps({
30
32
  projectIdRef: { current: "project" },
31
33
  activeCompPath: "index.html",
32
- timelineElements: [],
34
+ timelineElements,
33
35
  showToast: vi.fn(),
34
36
  writeProjectFile,
35
37
  recordEdit: vi.fn().mockResolvedValue(undefined),
@@ -84,4 +86,42 @@ describe("useTimelineAssetDropOps handleTimelineAssetDrop", () => {
84
86
 
85
87
  expect(usePlayerStore.getState().selectedElementId).toBe("index.html#clip");
86
88
  });
89
+
90
+ it("measures the insert row against the visible rows, not clips the timeline hides", async () => {
91
+ const clip = (id: string, track: number): TimelineElement => ({
92
+ id,
93
+ key: id,
94
+ tag: "div",
95
+ start: 0,
96
+ duration: 2,
97
+ track,
98
+ authoredTrack: track,
99
+ hfId: `hf-${id}`,
100
+ domId: id,
101
+ });
102
+ const source = [
103
+ '<main data-composition-id="scene" data-duration="10" data-width="1920" data-height="1080">',
104
+ ...["a:0", "hidden:1", "b:2"].map((t) => {
105
+ const [id, track] = t.split(":");
106
+ return `<div data-hf-id="hf-${id}" id="${id}" data-start="0" data-track-index="${track}"></div>`;
107
+ }),
108
+ "</main>",
109
+ ].join("\n");
110
+ usePlayerStore.getState().setTopLevelIds(new Set(["a", "b"]));
111
+ const writeProjectFile = vi.fn().mockResolvedValue(undefined);
112
+ const getDrop = renderDropHook(source, writeProjectFile, [
113
+ clip("a", 0),
114
+ clip("hidden", 1),
115
+ clip("b", 2),
116
+ ]);
117
+
118
+ await act(async () => {
119
+ await getDrop()("clip.mp4", { start: 1, track: 1, insertRow: 1, trackOrder: [0, 2] }, 2);
120
+ });
121
+
122
+ const [, written] = writeProjectFile.mock.calls[0] as [string, string];
123
+ expect(written).toContain('id="b" data-start="0" data-track-index="2"');
124
+ expect(written).toContain('id="hidden" data-start="0" data-track-index="1"');
125
+ expect(written).toMatch(/<video id="clip"[^>]*data-track-index="1"/);
126
+ });
87
127
  });
@@ -4,6 +4,8 @@
4
4
  // 600-line cap.
5
5
  import { useCallback, type MutableRefObject, type RefObject } from "react";
6
6
  import type { TimelineElement } from "../player";
7
+ import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
8
+ import { resolveDropTrack } from "../utils/timelineDropTrackInsert";
7
9
  import {
8
10
  buildTimelineAssetId,
9
11
  buildTimelineAssetInsertHtml,
@@ -23,6 +25,18 @@ import { commitTimelineCompositionInsertion } from "../utils/timelineComposition
23
25
  import { extendRootDurationInSource } from "../utils/rootDuration";
24
26
  import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
25
27
  import { selectAndRevealTimelineElement } from "../player/components/timelineDropReveal";
28
+ import { selectTimelineRowElements } from "../player/hooks/useTimelineRowElements";
29
+ import { usePlayerStore } from "../player/store/playerStore";
30
+
31
+ /** The first uploaded file opens the new track (if asked); the rest land on the lane it landed on. */
32
+ function fileDropPlacement(
33
+ index: number,
34
+ next: { start: number; track: number },
35
+ dropped: TimelineDropPlacement | undefined,
36
+ landedTrack: number | undefined,
37
+ ): TimelineDropPlacement {
38
+ return index === 0 ? { ...dropped, ...next } : { ...next, track: landedTrack ?? next.track };
39
+ }
26
40
 
27
41
  interface UseTimelineAssetDropOpsOptions {
28
42
  projectIdRef: MutableRefObject<string | null>;
@@ -52,16 +66,16 @@ export function useTimelineAssetDropOps({
52
66
  observeProjectFileVersion,
53
67
  }: UseTimelineAssetDropOpsOptions) {
54
68
  // fallow-ignore-next-line complexity
55
- const handleTimelineAssetDrop = useCallback(
69
+ const dropAssetAt = useCallback(
56
70
  // fallow-ignore-next-line complexity
57
71
  async (
58
72
  assetPath: string,
59
- placement: Pick<TimelineElement, "start" | "track">,
73
+ placement: TimelineDropPlacement,
60
74
  durationOverride?: number,
61
- ) => {
75
+ ): Promise<number | undefined> => {
62
76
  if (isRecordingRef?.current) {
63
77
  showToast("Cannot edit timeline while recording", "error");
64
- return;
78
+ return undefined;
65
79
  }
66
80
  const pid = projectIdRef.current;
67
81
  if (!pid) throw new Error("No active project");
@@ -69,7 +83,7 @@ export function useTimelineAssetDropOps({
69
83
  const kind = getTimelineAssetKind(assetPath);
70
84
  if (!kind) {
71
85
  showToast("Only image, video, and audio assets can be dropped onto the timeline.");
72
- return;
86
+ return undefined;
73
87
  }
74
88
 
75
89
  const targetPath = activeCompPath || "index.html";
@@ -91,9 +105,24 @@ export function useTimelineAssetDropOps({
91
105
  );
92
106
  const newElementZIndex = Math.max(1, relevantElements.length + 1);
93
107
 
108
+ const { source: sourceWithRoom, track } = resolveDropTrack({
109
+ source: originalContent,
110
+ // insertRow counts the rows the timeline shows, so plan against those.
111
+ elements: selectTimelineRowElements(
112
+ relevantElements,
113
+ usePlayerStore.getState().topLevelIds,
114
+ ),
115
+ placement,
116
+ dropped: {
117
+ id: newId,
118
+ tag: kind === "image" ? "img" : kind,
119
+ start: normalizedStart,
120
+ duration: normalizedDuration,
121
+ },
122
+ });
94
123
  const patchedContent = extendRootDurationInSource(
95
124
  insertTimelineAssetIntoSource(
96
- originalContent,
125
+ sourceWithRoom,
97
126
  buildTimelineAssetInsertHtml({
98
127
  id: newId,
99
128
  hfId: `hf-${generateId()}`,
@@ -101,7 +130,7 @@ export function useTimelineAssetDropOps({
101
130
  kind,
102
131
  start: normalizedStart,
103
132
  duration: normalizedDuration,
104
- track: placement.track,
133
+ track,
105
134
  zIndex: newElementZIndex,
106
135
  geometry: fitTimelineAssetGeometry(
107
136
  null,
@@ -125,10 +154,12 @@ export function useTimelineAssetDropOps({
125
154
  selectAndRevealTimelineElement(deriveTimelineStoreKeyForDomId(newId, targetPath));
126
155
  forceReloadSdkSession?.();
127
156
  reloadPreview();
157
+ return track;
128
158
  } catch (error) {
129
159
  const message =
130
160
  error instanceof Error ? error.message : "Failed to drop asset onto timeline";
131
161
  showToast(message);
162
+ return undefined;
132
163
  }
133
164
  },
134
165
  [
@@ -144,10 +175,17 @@ export function useTimelineAssetDropOps({
144
175
  ],
145
176
  );
146
177
 
178
+ const handleTimelineAssetDrop = useCallback(
179
+ async (assetPath: string, placement: TimelineDropPlacement, durationOverride?: number) => {
180
+ await dropAssetAt(assetPath, placement, durationOverride);
181
+ },
182
+ [dropAssetAt],
183
+ );
184
+
147
185
  // fallow-ignore-next-line complexity
148
186
  const handleTimelineFileDrop = useCallback(
149
187
  // fallow-ignore-next-line complexity
150
- async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
188
+ async (files: File[], placement?: TimelineDropPlacement) => {
151
189
  if (isRecordingRef?.current) {
152
190
  showToast("Cannot edit timeline while recording", "error");
153
191
  return;
@@ -166,15 +204,18 @@ export function useTimelineAssetDropOps({
166
204
  placement ?? { start: 0, track: 0 },
167
205
  durations,
168
206
  );
207
+ let landedTrack: number | undefined;
169
208
  for (const [index, assetPath] of uploaded.entries()) {
170
- await handleTimelineAssetDrop(
209
+ const next = placements[index] ?? placements[0];
210
+ const track = await dropAssetAt(
171
211
  assetPath,
172
- placements[index] ?? placements[0],
212
+ fileDropPlacement(index, next, placement, landedTrack),
173
213
  durations[index],
174
214
  );
215
+ if (index === 0) landedTrack = track;
175
216
  }
176
217
  },
177
- [handleTimelineAssetDrop, projectIdRef, uploadProjectFiles, isRecordingRef, showToast],
218
+ [dropAssetAt, projectIdRef, uploadProjectFiles, isRecordingRef, showToast],
178
219
  );
179
220
 
180
221
  const handleTimelineCompositionDrop = useCallback(
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import type { TimelineElement } from "../player";
6
6
  import { applyRippleShifts, useTimelineDeleteOps } from "./useTimelineDeleteOps";
7
7
  import { installReactActEnvironment, mountReactHarness } from "./domSelectionTestHarness";
8
+ import { useTrackPendingTimelineEdit } from "./useTrackPendingTimelineEdit";
9
+ import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
8
10
 
9
11
  installReactActEnvironment();
10
12
 
@@ -117,3 +119,112 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
117
119
  );
118
120
  });
119
121
  });
122
+
123
+ // Regression: a delete's write can reach disk before its history entry is
124
+ // recorded, so an Undo fired in that window found nothing to restore.
125
+ // Fails without useTrackPendingTimelineEdit's wrap (see that file).
126
+ describe("useTimelineDeleteOps: undo race", () => {
127
+ // data-composition-id (readRootCompositionDuration's key) plus a
128
+ // remove-element mock that actually drops hf-c: needed for a real diff.
129
+ const html = `<!DOCTYPE html><html data-composition-variables='[]'><body>
130
+ <div data-hf-id="hf-stage" data-composition-id="main" data-duration="6">
131
+ <div data-hf-id="hf-a" data-start="0" data-duration="2"></div>
132
+ <div data-hf-id="hf-b" data-start="2" data-duration="2"></div>
133
+ <div data-hf-id="hf-c" data-start="4" data-duration="2"></div>
134
+ </div>
135
+ </body></html>`;
136
+ const htmlAfterDeletingC = `<!DOCTYPE html><html data-composition-variables='[]'><body>
137
+ <div data-hf-id="hf-stage" data-composition-id="main" data-duration="6">
138
+ <div data-hf-id="hf-a" data-start="0" data-duration="2"></div>
139
+ <div data-hf-id="hf-b" data-start="2" data-duration="2"></div>
140
+ </div>
141
+ </body></html>`;
142
+
143
+ beforeEach(() => {
144
+ vi.stubGlobal(
145
+ "fetch",
146
+ vi.fn(async (input: RequestInfo | URL) => {
147
+ const url = String(input);
148
+ if (url.includes("/file-mutations/remove-element/")) {
149
+ return new Response(JSON.stringify({ changed: true, content: htmlAfterDeletingC }), {
150
+ status: 200,
151
+ });
152
+ }
153
+ return new Response(JSON.stringify({ content: html }), { status: 200 });
154
+ }),
155
+ );
156
+ });
157
+
158
+ afterEach(() => {
159
+ document.body.innerHTML = "";
160
+ vi.unstubAllGlobals();
161
+ });
162
+
163
+ // A manually-resolved gate stands in for the delete's write landing — a
164
+ // real timer raced the fix's own microtasks unpredictably (verified: it
165
+ // kept passing even with the fix's tracking call deleted).
166
+ function deferred<T>() {
167
+ let resolve!: (value: T) => void;
168
+ const promise = new Promise<T>((r) => (resolve = r));
169
+ return { promise, resolve };
170
+ }
171
+
172
+ function mountRaceHarness() {
173
+ const elements = [el("hf-a", 0, 2), el("hf-b", 2, 2), el("hf-c", 4, 2)];
174
+ const gate = deferred<void>();
175
+ const recordEdit = vi.fn(() => gate.promise);
176
+ let tracked: ((elements: TimelineElement[]) => Promise<void>) | null = null;
177
+ function Harness() {
178
+ const track = useTrackPendingTimelineEdit();
179
+ const hook = useTimelineDeleteOps({
180
+ projectIdRef: { current: "test-project" },
181
+ activeCompPath: "index.html",
182
+ timelineElements: elements,
183
+ showToast: vi.fn(),
184
+ writeProjectFile: vi.fn().mockResolvedValue(undefined),
185
+ recordEdit,
186
+ reloadPreview: vi.fn(),
187
+ previewIframeRef: { current: null },
188
+ handleTimelineGroupMove: vi.fn().mockResolvedValue(undefined),
189
+ });
190
+ tracked = track(hook.handleTimelineElementsDelete);
191
+ return null;
192
+ }
193
+ mountReactHarness(<Harness />);
194
+ // Deletes the LAST clip (not the middle one): only this shrinks the
195
+ // composition's furthest-clip-end duration, so the write actually
196
+ // differs from the original content and recordEdit is reached — the
197
+ // middle clip's delete leaves duration unchanged and is a no-op write.
198
+ return { gate, recordEdit, deleteSelection: () => tracked!([elements[2]]) };
199
+ }
200
+
201
+ it("the pending-edit flush waits while the delete's history write is still landing", async () => {
202
+ const { gate, recordEdit, deleteSelection } = mountRaceHarness();
203
+ const order: string[] = [];
204
+ const deletePromise = deleteSelection().then(() => order.push("deleted"));
205
+ // Let the delete run up to (and call) recordEdit, which is now gated —
206
+ // this is the exact window a fast Undo used to race into.
207
+ await vi.waitFor(() => expect(recordEdit).toHaveBeenCalled());
208
+ // No React state changes happen inside the flush itself here (no
209
+ // pending-edit-flush listeners are registered in this harness), so it
210
+ // does not need act()'s wrapping — only awaiting the delete's own
211
+ // resolution below does.
212
+ const flushPromise = flushStudioPendingEdits().then(() => order.push("flushed"));
213
+ await Promise.resolve();
214
+ await Promise.resolve();
215
+ expect(order).toEqual([]);
216
+ gate.resolve();
217
+ await deletePromise;
218
+ await flushPromise;
219
+ expect(order).toEqual(["deleted", "flushed"]);
220
+ });
221
+
222
+ it("the pending-edit flush resolves immediately once the delete has already finished", async () => {
223
+ const { gate, deleteSelection } = mountRaceHarness();
224
+ gate.resolve();
225
+ await deleteSelection();
226
+ await act(async () => {
227
+ await flushStudioPendingEdits();
228
+ });
229
+ });
230
+ });