@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,7 +5,6 @@ import { createRoot, type Root } from "react-dom/client";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { TextAreaField } from "../components/editor/propertyPanelSections";
7
7
  import type { DomEditSelection } from "../components/editor/domEditing";
8
- import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
9
8
  import { usePlayerStore } from "../player/store/playerStore";
10
9
  import { useAppHotkeys } from "./useAppHotkeys";
11
10
 
@@ -70,7 +69,6 @@ function Harness() {
70
69
  const [selectionRefreshed, setSelectionRefreshed] = useState(false);
71
70
  const selectionRef = useRef<DomEditSelection | null>(parentWithTextChild());
72
71
  const clearSelectionRef = useRef<() => void>(() => undefined);
73
- const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
74
72
 
75
73
  useAppHotkeys({
76
74
  handleTimelineElementsDelete: timelineDeleteMany,
@@ -89,7 +87,6 @@ function Harness() {
89
87
  showToast: vi.fn(),
90
88
  syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
91
89
  waitForPendingDomEditSaves: vi.fn(async () => undefined),
92
- leftSidebarRef,
93
90
  handleCopy: vi.fn(() => false),
94
91
  handlePaste: vi.fn(async () => undefined),
95
92
  handleCut: vi.fn(async () => false),
@@ -2,8 +2,6 @@ import { useCallback, useEffect, useRef } from "react";
2
2
  import { usePlayerStore } from "../player";
3
3
  import type { TimelineElement } from "../player";
4
4
  import type { DomEditSelection } from "../components/editor/domEditing";
5
- import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
6
- import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
5
  import { isTypingTarget } from "../utils/typingTarget";
8
6
  import { useCaptionStore } from "../captions/store";
9
7
  import {
@@ -11,13 +9,17 @@ import {
11
9
  isCaptionPreviewVisible,
12
10
  } from "../captions/components/CaptionOverlayUtils";
13
11
  import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
14
- import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
15
12
  import {
16
13
  type HotkeyCallbacks,
17
14
  dispatchModifierKey,
18
15
  dispatchPlainKey,
19
16
  handleUndoRedoKey,
20
17
  } from "./appHotkeysDispatch";
18
+ import {
19
+ useEditHistoryActions,
20
+ type EditHistoryHandle,
21
+ type UseEditHistoryActionsOptions,
22
+ } from "./useEditHistoryActions";
21
23
 
22
24
  function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
23
25
  try {
@@ -69,28 +71,6 @@ function tryApplyBeatHistory(
69
71
 
70
72
  // ── Types ──
71
73
 
72
- interface HistoryResult {
73
- ok: boolean;
74
- reason?: string;
75
- label?: string;
76
- paths?: string[];
77
- /** Per-file restored/previous content, used to soft-apply the preview. */
78
- files?: Record<string, { previous: string; restored: string }>;
79
- }
80
- interface HistoryFileCallbacks {
81
- readFile: (path: string) => Promise<string>;
82
- writeFile: (path: string, content: string) => Promise<void>;
83
- serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
84
- }
85
- interface EditHistoryHandle {
86
- undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
87
- redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
88
- state: {
89
- undo: ReadonlyArray<{ createdAt: number }>;
90
- redo: ReadonlyArray<{ createdAt: number }>;
91
- };
92
- }
93
-
94
74
  interface UseAppHotkeysParams {
95
75
  handleTimelineElementsDelete: (elements: TimelineElement[]) => Promise<void>;
96
76
  handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void>;
@@ -105,12 +85,8 @@ interface UseAppHotkeysParams {
105
85
  readProjectFile: (path: string) => Promise<string>;
106
86
  writeProjectFile: (path: string, content: string) => Promise<void>;
107
87
  showToast: (message: string, tone?: "error" | "info") => void;
108
- syncHistoryPreviewAfterApply: (restore: {
109
- paths?: string[];
110
- files?: Record<string, { previous: string; restored: string }>;
111
- }) => Promise<void>;
88
+ syncHistoryPreviewAfterApply: UseEditHistoryActionsOptions["syncHistoryPreviewAfterApply"];
112
89
  waitForPendingDomEditSaves: () => Promise<void>;
113
- leftSidebarRef: React.RefObject<LeftSidebarHandle | null>;
114
90
  handleCopy: () => boolean;
115
91
  handlePaste: () => Promise<void>;
116
92
  handleCut: () => Promise<boolean>;
@@ -147,7 +123,6 @@ export function useAppHotkeys({
147
123
  showToast,
148
124
  syncHistoryPreviewAfterApply,
149
125
  waitForPendingDomEditSaves,
150
- leftSidebarRef,
151
126
  handleCopy,
152
127
  handlePaste,
153
128
  handleCut,
@@ -165,22 +140,18 @@ export function useAppHotkeys({
165
140
 
166
141
  // ── Undo / Redo ──
167
142
 
168
- const readHistoryFile = useCallback(
169
- (path: string): Promise<string> =>
170
- path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
171
- [readOptionalProjectFile, readProjectFile],
172
- );
173
- const writeHistoryFile = useCallback(
174
- async (path: string, content: string): Promise<void> => {
175
- await writeProjectFile(path, content);
176
- },
177
- [writeProjectFile],
178
- );
179
- const serializeHistoryFiles = useCallback(
180
- <T>(paths: readonly string[], task: () => Promise<T>) =>
181
- serializeStudioFileMutations(writeProjectFile, paths, task),
182
- [writeProjectFile],
183
- );
143
+ const fileHistory = useEditHistoryActions({
144
+ editHistory,
145
+ readOptionalProjectFile,
146
+ readProjectFile,
147
+ writeProjectFile,
148
+ showToast,
149
+ syncHistoryPreviewAfterApply,
150
+ waitForPendingDomEditSaves,
151
+ onAfterUndoRedo,
152
+ activeCompPath,
153
+ forceReloadSdkSession,
154
+ });
184
155
 
185
156
  const applyHistory = useCallback(
186
157
  async (direction: "undo" | "redo") => {
@@ -189,7 +160,7 @@ export function useAppHotkeys({
189
160
  // earlier file edit (which would ALSO leave the caption change intact).
190
161
  const captionState = useCaptionStore.getState();
191
162
  // Only when the caption preview is actually visible: isEditMode stays
192
- // true while the preview is hidden (storyboard view), and eating ⌘Z
163
+ // true while the preview is hidden, and eating ⌘Z
193
164
  // there would pop invisible caption edits instead of file history.
194
165
  if (captionState.isEditMode && isCaptionPreviewVisible()) {
195
166
  const restored = direction === "undo" ? captionState.undo() : captionState.redo();
@@ -204,42 +175,9 @@ export function useAppHotkeys({
204
175
  // Beat edits interleave with file history by timestamp; handle them first.
205
176
  if (tryApplyBeatHistory(direction, editHistory.state, showToast)) return;
206
177
 
207
- await waitForPendingDomEditSaves();
208
- const result = await editHistory[direction]({
209
- readFile: readHistoryFile,
210
- writeFile: writeHistoryFile,
211
- serialize: serializeHistoryFiles,
212
- });
213
- if (!result.ok && result.reason === "content-mismatch") {
214
- showToast(
215
- `File changed outside Studio. ${direction === "undo" ? "Undo" : "Redo"} history was not applied.`,
216
- "info",
217
- );
218
- return;
219
- }
220
- if (result.ok && result.label) {
221
- onAfterUndoRedo?.();
222
- // If the active composition was among the written files, force-reload
223
- // the SDK session so its in-memory doc matches the reverted content.
224
- if (activeCompPath && result.paths?.includes(activeCompPath)) {
225
- forceReloadSdkSession?.();
226
- }
227
- await syncHistoryPreviewAfterApply({ paths: result.paths, files: result.files });
228
- showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
229
- }
178
+ await fileHistory[direction]();
230
179
  },
231
- [
232
- editHistory,
233
- readHistoryFile,
234
- showToast,
235
- syncHistoryPreviewAfterApply,
236
- waitForPendingDomEditSaves,
237
- writeHistoryFile,
238
- serializeHistoryFiles,
239
- onAfterUndoRedo,
240
- activeCompPath,
241
- forceReloadSdkSession,
242
- ],
180
+ [editHistory.state, fileHistory, showToast],
243
181
  );
244
182
 
245
183
  const handleUndo = useCallback(() => applyHistory("undo"), [applyHistory]);
@@ -263,7 +201,6 @@ export function useAppHotkeys({
263
201
  onToggleRecording,
264
202
  onGroupSelection,
265
203
  onUngroupSelection,
266
- leftSidebarRef,
267
204
  domEditSelectionRef,
268
205
  showToast,
269
206
  };
@@ -3,6 +3,7 @@ import { act } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
  import { expect, it, vi } from "vitest";
5
5
  import { useCaptionDetection } from "./useCaptionDetection";
6
+ import { usePreviewIframeStore } from "../player/store/previewIframeStore";
6
7
 
7
8
  Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
8
9
 
@@ -52,3 +53,48 @@ it("does not let a foreign window trigger caption activation", () => {
52
53
  vi.unstubAllGlobals();
53
54
  }
54
55
  });
56
+
57
+ it("re-runs caption activation on the promoted iframe's document", () => {
58
+ const host = document.createElement("div");
59
+ const a = document.createElement("iframe");
60
+ const b = document.createElement("iframe");
61
+ document.body.append(host, a, b);
62
+ const ref = { current: a };
63
+ const root = createRoot(host);
64
+ const fetchMock = vi.fn(() => new Promise<Response>(() => {}));
65
+ vi.stubGlobal("fetch", fetchMock);
66
+ const params = {
67
+ projectId: "demo",
68
+ activeCompPath: "captions.html",
69
+ compIdToSrc: new Map<string, string>(),
70
+ captionEditMode: false,
71
+ captionHasSelection: false,
72
+ previewIframeRef: ref,
73
+ captionSync: { save: vi.fn(), loadOverrides: vi.fn(async () => {}) },
74
+ setRightCollapsed: vi.fn(),
75
+ };
76
+ function Harness() {
77
+ useCaptionDetection(params);
78
+ return null;
79
+ }
80
+ try {
81
+ act(() => usePreviewIframeStore.getState().setIframe(a));
82
+ act(() => root.render(<Harness />));
83
+ const group = b.contentDocument!.createElement("div");
84
+ group.className = "caption-group";
85
+ b.contentDocument!.body.append(group);
86
+ expect(fetchMock).not.toHaveBeenCalled();
87
+ act(() => {
88
+ ref.current = b;
89
+ usePreviewIframeStore.getState().setIframe(b);
90
+ });
91
+ expect(fetchMock).toHaveBeenCalledOnce();
92
+ } finally {
93
+ act(() => root.unmount());
94
+ usePreviewIframeStore.setState({ iframe: null });
95
+ host.remove();
96
+ a.remove();
97
+ b.remove();
98
+ vi.unstubAllGlobals();
99
+ }
100
+ });
@@ -1,3 +1,4 @@
1
+ import { useLivePreviewIframe } from "../player/store/previewIframeStore";
1
2
  import { buildProjectApiPath } from "../utils/projectRouting";
2
3
  import { useEffect, useRef } from "react";
3
4
  import { useCaptionStore } from "../captions/store";
@@ -26,6 +27,7 @@ export function useCaptionDetection({
26
27
  captionSync,
27
28
  setRightCollapsed,
28
29
  }: UseCaptionDetectionParams) {
30
+ const livePreviewIframe = useLivePreviewIframe();
29
31
  // Switching compositions must drop the previous comp's caption state — a
30
32
  // stale model + full-canvas overlay otherwise blocks normal element editing
31
33
  // on the new composition (and edit mode could never be exited).
@@ -145,7 +147,7 @@ export function useCaptionDetection({
145
147
  return () => {
146
148
  window.removeEventListener("message", handleMessage);
147
149
  };
148
- }, [activeCompPath, projectId, compIdToSrc, captionSync, previewIframeRef]);
150
+ }, [activeCompPath, projectId, compIdToSrc, captionSync, previewIframeRef, livePreviewIframe]);
149
151
 
150
152
  // eslint-disable-next-line no-restricted-syntax
151
153
  useEffect(() => {
@@ -6,7 +6,7 @@
6
6
  import { useEffect, useRef } from "react";
7
7
  import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
8
8
  import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
9
- import type { SidebarTab } from "../components/sidebar/LeftSidebar";
9
+ import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
10
10
  import type { PatchTarget } from "../utils/sourcePatcher";
11
11
  import { logSelect } from "../utils/selectDebug";
12
12
 
@@ -30,7 +30,6 @@ interface UseDomEditPreviewSyncParams {
30
30
  (iframe: HTMLIFrameElement) => Promise<void>
31
31
  >;
32
32
  openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
33
- getSidebarTab?: () => SidebarTab;
34
33
  gsapCacheVersion?: number;
35
34
  }
36
35
 
@@ -48,7 +47,6 @@ export function useDomEditPreviewSync({
48
47
  syncPreviewHotkeys,
49
48
  applyStudioManualEditsToPreviewRef,
50
49
  openSourceForSelection,
51
- getSidebarTab,
52
50
  gsapCacheVersion,
53
51
  }: UseDomEditPreviewSyncParams): void {
54
52
  // Sync selection from preview document on load / refresh
@@ -142,15 +140,15 @@ export function useDomEditPreviewSync({
142
140
  useEffect(
143
141
  // fallow-ignore-next-line complexity
144
142
  () => {
145
- if (!domEditSelection || !openSourceRef.current || !getSidebarTab) return;
143
+ if (!domEditSelection || !openSourceRef.current) return;
146
144
  if (!domEditSelection.sourceFile) return;
147
- if (getSidebarTab() !== "code") return;
145
+ if (!useDockLayoutStore.getState().visiblePanels.has("code")) return;
148
146
  openSourceRef.current(domEditSelection.sourceFile, {
149
147
  id: domEditSelection.id,
150
148
  selector: domEditSelection.selector,
151
149
  selectorIndex: domEditSelection.selectorIndex,
152
150
  });
153
151
  },
154
- [domEditSelection, getSidebarTab],
152
+ [domEditSelection],
155
153
  );
156
154
  }
@@ -5,7 +5,6 @@ import type { TimelineElement } from "../player";
5
5
  import type { ImportedFontAsset } from "../components/editor/fontAssets";
6
6
  import type { RightPanelTab } from "../utils/studioHelpers";
7
7
  import type { PatchTarget } from "../utils/sourcePatcher";
8
- import type { SidebarTab } from "../components/sidebar/LeftSidebar";
9
8
  import type { Composition } from "@hyperframes/sdk";
10
9
  import { sdkCutoverPersist, sdkDeletePersist, type PublishSdkSession } from "../utils/sdkCutover";
11
10
  import { runResolverShadow, recordResolverParity } from "../utils/sdkResolverShadow";
@@ -53,8 +52,6 @@ export interface UseDomEditSessionParams extends DomEditTimelineParams {
53
52
  reloadPreview: () => void;
54
53
  setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
55
54
  openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
56
- selectSidebarTab?: (tab: SidebarTab) => void;
57
- getSidebarTab?: () => SidebarTab;
58
55
  sdkSession?: Composition | null;
59
56
  publishSdkSession?: PublishSdkSession;
60
57
  forceReloadSdkSession?: () => void;
@@ -97,8 +94,6 @@ export function useDomEditSession({
97
94
  reloadPreview,
98
95
  setRefreshKey: _setRefreshKey,
99
96
  openSourceForSelection,
100
- selectSidebarTab,
101
- getSidebarTab,
102
97
  sdkSession,
103
98
  publishSdkSession,
104
99
  forceReloadSdkSession,
@@ -430,8 +425,6 @@ export function useDomEditSession({
430
425
  applyDomSelection,
431
426
  buildDomSelectionFromTarget,
432
427
  openSourceForSelection,
433
- selectSidebarTab,
434
- getSidebarTab,
435
428
  updateGsapProperty,
436
429
  updateGsapMeta,
437
430
  deleteGsapAnimation,
@@ -16,7 +16,7 @@ import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
16
16
  import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
17
17
  import { useGsapSelectionHandlers } from "./useGsapSelectionHandlers";
18
18
  import type { PatchTarget } from "../utils/sourcePatcher";
19
- import type { SidebarTab } from "../components/sidebar/LeftSidebar";
19
+ import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
20
20
 
21
21
  export interface UseDomEditWiringParams {
22
22
  projectId: string | null;
@@ -43,8 +43,6 @@ export interface UseDomEditWiringParams {
43
43
  ) => void;
44
44
  buildDomSelectionFromTarget: (element: HTMLElement) => Promise<DomEditSelection | null>;
45
45
  openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
46
- selectSidebarTab?: (tab: SidebarTab) => void;
47
- getSidebarTab?: () => SidebarTab;
48
46
  // GSAP script commit ops (from useGsapScriptCommits)
49
47
  updateGsapProperty: (
50
48
  sel: DomEditSelection,
@@ -132,8 +130,6 @@ export function useDomEditWiring({
132
130
  applyDomSelection,
133
131
  buildDomSelectionFromTarget,
134
132
  openSourceForSelection,
135
- selectSidebarTab,
136
- getSidebarTab,
137
133
  updateGsapProperty,
138
134
  updateGsapMeta,
139
135
  deleteGsapAnimation,
@@ -157,16 +153,16 @@ export function useDomEditWiring({
157
153
 
158
154
  const onClickToSource = useCallback(
159
155
  (selection: DomEditSelection) => {
160
- if (!openSourceForSelection || !selectSidebarTab) return;
156
+ if (!openSourceForSelection) return;
161
157
  if (!selection.sourceFile) return;
162
- selectSidebarTab("code");
158
+ useDockLayoutStore.getState().activatePanel("code");
163
159
  openSourceForSelection(selection.sourceFile, {
164
160
  id: selection.id,
165
161
  selector: selection.selector,
166
162
  selectorIndex: selection.selectorIndex,
167
163
  });
168
164
  },
169
- [openSourceForSelection, selectSidebarTab],
165
+ [openSourceForSelection],
170
166
  );
171
167
 
172
168
  // ── DOM selection -> timeline element sync ──
@@ -267,7 +263,6 @@ export function useDomEditWiring({
267
263
  syncPreviewHotkeys,
268
264
  applyStudioManualEditsToPreviewRef,
269
265
  openSourceForSelection,
270
- getSidebarTab,
271
266
  gsapCacheVersion,
272
267
  });
273
268
 
@@ -0,0 +1,90 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act, createElement } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
+ import { useEditHistoryActions, type EditHistoryHandle } from "./useEditHistoryActions";
8
+
9
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ let root: Root | null = null;
12
+ afterEach(() => act(() => root?.unmount()));
13
+
14
+ function mount(result: { ok: boolean; reason?: string; label?: string; paths?: string[] }) {
15
+ const editHistory = {
16
+ undo: vi.fn<EditHistoryHandle["undo"]>(async (cb) => {
17
+ if (result.ok) await cb.writeFile("index.html", "before");
18
+ return result;
19
+ }),
20
+ redo: vi.fn<EditHistoryHandle["redo"]>(async () => result),
21
+ };
22
+ const deps = {
23
+ editHistory,
24
+ readOptionalProjectFile: vi.fn(async () => ""),
25
+ readProjectFile: vi.fn(async () => ""),
26
+ writeProjectFile: vi.fn(async () => undefined),
27
+ showToast: vi.fn(),
28
+ syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
29
+ waitForPendingDomEditSaves: vi.fn(async () => undefined),
30
+ onAfterUndoRedo: vi.fn(),
31
+ activeCompPath: "index.html",
32
+ forceReloadSdkSession: vi.fn(),
33
+ };
34
+ let actions!: ReturnType<typeof useEditHistoryActions>;
35
+ function Probe() {
36
+ actions = useEditHistoryActions(deps);
37
+ return null;
38
+ }
39
+ root = createRoot(document.createElement("div"));
40
+ act(() => root!.render(createElement(Probe)));
41
+ return { deps, actions };
42
+ }
43
+
44
+ describe("useEditHistoryActions", () => {
45
+ it("undo writes through the host writer, resyncs the preview and toasts the label", async () => {
46
+ const { deps, actions } = mount({ ok: true, label: "Move clip", paths: ["index.html"] });
47
+ await act(() => actions.undo());
48
+ expect(deps.waitForPendingDomEditSaves).toHaveBeenCalled();
49
+ expect(deps.writeProjectFile).toHaveBeenCalledWith("index.html", "before");
50
+ expect(deps.onAfterUndoRedo).toHaveBeenCalled();
51
+ expect(deps.forceReloadSdkSession).toHaveBeenCalled();
52
+ expect(deps.syncHistoryPreviewAfterApply).toHaveBeenCalled();
53
+ expect(deps.showToast).toHaveBeenCalledWith("Undid Move clip", "info");
54
+ });
55
+
56
+ it("redo reports the redone label and skips the SDK reload for other files", async () => {
57
+ const { deps, actions } = mount({ ok: true, label: "Split clip", paths: ["other.html"] });
58
+ await act(() => actions.redo());
59
+ expect(deps.forceReloadSdkSession).not.toHaveBeenCalled();
60
+ expect(deps.showToast).toHaveBeenCalledWith("Redid Split clip", "info");
61
+ });
62
+
63
+ it("explains a refused undo when the file changed on disk", async () => {
64
+ const { deps, actions } = mount({ ok: false, reason: "content-mismatch" });
65
+ await act(() => actions.undo());
66
+ expect(deps.showToast).toHaveBeenCalledWith(
67
+ "File changed outside Studio. Undo history was not applied.",
68
+ "info",
69
+ );
70
+ expect(deps.syncHistoryPreviewAfterApply).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it("waits for pending saves first and reads the motion file through the optional reader", async () => {
74
+ const { deps, actions } = mount({ ok: true, label: "Move clip", paths: ["index.html"] });
75
+ const order: string[] = [];
76
+ deps.waitForPendingDomEditSaves.mockImplementation(async () => void order.push("wait"));
77
+ deps.editHistory.undo.mockImplementation(async (cb) => {
78
+ order.push("undo");
79
+ await cb.readFile(STUDIO_MOTION_PATH);
80
+ await cb.readFile("index.html");
81
+ await cb.serialize?.(["index.html"], async () => order.push("serialized"));
82
+ return { ok: true };
83
+ });
84
+ await act(() => actions.undo());
85
+ expect(order).toEqual(["wait", "undo", "serialized"]);
86
+ expect(deps.readOptionalProjectFile).toHaveBeenCalledWith(STUDIO_MOTION_PATH);
87
+ expect(deps.readProjectFile).toHaveBeenCalledWith("index.html");
88
+ expect(deps.readProjectFile).not.toHaveBeenCalledWith(STUDIO_MOTION_PATH);
89
+ });
90
+ });
@@ -0,0 +1,106 @@
1
+ // fallow-ignore-file complexity
2
+ import { useCallback, useMemo } from "react";
3
+ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
4
+ import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
5
+
6
+ interface HistoryResult {
7
+ ok: boolean;
8
+ reason?: string;
9
+ label?: string;
10
+ paths?: string[];
11
+ /** Per-file restored/previous content, used to soft-apply the preview. */
12
+ files?: Record<string, { previous: string; restored: string }>;
13
+ }
14
+ interface HistoryFileCallbacks {
15
+ readFile: (path: string) => Promise<string>;
16
+ writeFile: (path: string, content: string) => Promise<void>;
17
+ serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
18
+ }
19
+ export interface EditHistoryHandle {
20
+ undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
21
+ redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
22
+ state: {
23
+ undo: ReadonlyArray<{ createdAt: number }>;
24
+ redo: ReadonlyArray<{ createdAt: number }>;
25
+ };
26
+ }
27
+
28
+ export interface UseEditHistoryActionsOptions {
29
+ editHistory: Pick<EditHistoryHandle, "undo" | "redo">;
30
+ readOptionalProjectFile: (path: string) => Promise<string>;
31
+ readProjectFile: (path: string) => Promise<string>;
32
+ writeProjectFile: (path: string, content: string) => Promise<void>;
33
+ showToast: (message: string, tone?: "error" | "info") => void;
34
+ syncHistoryPreviewAfterApply: (restore: Pick<HistoryResult, "paths" | "files">) => Promise<void>;
35
+ waitForPendingDomEditSaves: () => Promise<void>;
36
+ onAfterUndoRedo?: () => void;
37
+ /** Active composition path — decides whether undo/redo must resync the SDK session. */
38
+ activeCompPath?: string | null;
39
+ /** Reloads the SDK session after a revert of the active comp, past the self-write suppress window. */
40
+ forceReloadSdkSession?: () => void;
41
+ }
42
+
43
+ /** Applies one persisted file-history step: the single owner of undo/redo over project files. */
44
+ export function useEditHistoryActions({
45
+ editHistory,
46
+ readOptionalProjectFile,
47
+ readProjectFile,
48
+ writeProjectFile,
49
+ showToast,
50
+ syncHistoryPreviewAfterApply,
51
+ waitForPendingDomEditSaves,
52
+ onAfterUndoRedo,
53
+ activeCompPath,
54
+ forceReloadSdkSession,
55
+ }: UseEditHistoryActionsOptions) {
56
+ const readHistoryFile = useCallback(
57
+ (path: string): Promise<string> =>
58
+ path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
59
+ [readOptionalProjectFile, readProjectFile],
60
+ );
61
+ const serializeHistoryFiles = useCallback(
62
+ <T>(paths: readonly string[], task: () => Promise<T>) =>
63
+ serializeStudioFileMutations(writeProjectFile, paths, task),
64
+ [writeProjectFile],
65
+ );
66
+
67
+ const apply = useCallback(
68
+ async (direction: "undo" | "redo") => {
69
+ const [noun, verb] = direction === "undo" ? ["Undo", "Undid"] : ["Redo", "Redid"];
70
+ await waitForPendingDomEditSaves();
71
+ const result = await editHistory[direction]({
72
+ readFile: readHistoryFile,
73
+ writeFile: writeProjectFile,
74
+ serialize: serializeHistoryFiles,
75
+ });
76
+ if (!result.ok && result.reason === "content-mismatch") {
77
+ showToast(`File changed outside Studio. ${noun} history was not applied.`, "info");
78
+ return;
79
+ }
80
+ if (result.ok && result.label) {
81
+ onAfterUndoRedo?.();
82
+ if (activeCompPath && result.paths?.includes(activeCompPath)) {
83
+ forceReloadSdkSession?.();
84
+ }
85
+ await syncHistoryPreviewAfterApply({ paths: result.paths, files: result.files });
86
+ showToast(`${verb} ${result.label}`, "info");
87
+ }
88
+ },
89
+ [
90
+ editHistory,
91
+ readHistoryFile,
92
+ showToast,
93
+ syncHistoryPreviewAfterApply,
94
+ waitForPendingDomEditSaves,
95
+ writeProjectFile,
96
+ serializeHistoryFiles,
97
+ onAfterUndoRedo,
98
+ activeCompPath,
99
+ forceReloadSdkSession,
100
+ ],
101
+ );
102
+
103
+ const undo = useCallback(() => apply("undo"), [apply]);
104
+ const redo = useCallback(() => apply("redo"), [apply]);
105
+ return useMemo(() => ({ undo, redo }), [undo, redo]);
106
+ }