@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
@@ -1,275 +1,70 @@
1
- import { useState, useCallback, useRef, useEffect } from "react";
2
- import type {
3
- RightInspectorPane,
4
- RightInspectorPanes,
5
- RightPanelTab,
6
- } from "../utils/studioHelpers";
7
- import { readStudioUiPreferences, writeStudioUiPreferences } from "../utils/studioUiPreferences";
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import type { RightPanelTab } from "../utils/studioHelpers";
8
3
  import { trackStudioEvent } from "../utils/studioTelemetry";
9
- import { STUDIO_FLAT_INSPECTOR_ENABLED } from "../components/editor/manualEditingAvailability";
10
- import {
11
- defaultPanelWidths,
12
- fitPanelWidths,
13
- railsEngaged,
14
- type PanelWidths,
15
- } from "../utils/fitPanels";
16
-
17
- const NO_OVERRIDE = { left: false, right: false } as const;
4
+ import { useDockLayoutStore, visiblePanelInZone } from "../components/dock/dockLayoutStore";
5
+ import { PANEL_DEFINITIONS, type PanelId } from "../components/dock/panelRegistry";
18
6
 
19
7
  export interface InitialPanelLayoutState {
20
8
  rightCollapsed?: boolean | null;
21
9
  rightPanelTab?: RightPanelTab | null;
22
10
  }
23
11
 
24
- type PanelSide = "left" | "right";
25
-
26
- function getInitialRightInspectorPanes(tab?: RightPanelTab | null): RightInspectorPanes {
27
- if (tab === "layers") return { layers: true, design: false };
28
- return { layers: false, design: true };
29
- }
30
-
31
- function readViewportWidth(): number {
32
- return typeof window === "undefined" ? 1496 : window.innerWidth;
12
+ /** The dock panel each legacy right-panel tab name opens; block params render inside Design. */
13
+ function panelForTab(tab: RightPanelTab): PanelId {
14
+ return tab === "block-params" ? "design" : tab;
33
15
  }
34
16
 
35
- /**
36
- * What the user WANTS each panel to be, before the window gets a say. Stored
37
- * preferences win over the width-derived defaults; neither is clamped here —
38
- * `fitPanelWidths` owns every clamp so there is one place that decides.
39
- */
40
- function getPreferredPanelWidths(): PanelWidths {
41
- const preferences = readStudioUiPreferences();
42
- const defaults = defaultPanelWidths(readViewportWidth());
43
- return {
44
- left: preferences.leftWidth ?? defaults.left,
45
- right: preferences.rightWidth ?? defaults.right,
46
- };
17
+ function tabForPanel(id: PanelId | null): RightPanelTab {
18
+ return id !== null && PANEL_DEFINITIONS[id].zone === "right" ? (id as RightPanelTab) : "design";
47
19
  }
48
20
 
21
+ /** The right column as the rest of Studio reads it, backed by the dock's panel state. */
49
22
  export function usePanelLayout(initialState?: InitialPanelLayoutState) {
50
- const [preferredWidths, setPreferredWidths] = useState(getPreferredPanelWidths);
51
- const [viewportWidth, setViewportWidth] = useState(readViewportWidth);
52
- const [leftCollapsed, setLeftCollapsed] = useState(
53
- () => readStudioUiPreferences().leftCollapsed ?? false,
54
- );
55
- const [rightCollapsed, setRightCollapsed] = useState(initialState?.rightCollapsed ?? false);
56
- const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
57
- initialState?.rightPanelTab ?? "design",
58
- );
59
- const rightPanelTabRef = useRef(rightPanelTab);
60
- rightPanelTabRef.current = rightPanelTab;
61
- const [rightInspectorPanes, setRightInspectorPanes] = useState<RightInspectorPanes>(() =>
62
- getInitialRightInspectorPanes(initialState?.rightPanelTab),
63
- );
64
- const rightInspectorPanesRef = useRef(rightInspectorPanes);
65
- rightInspectorPanesRef.current = rightInspectorPanes;
66
- // Set when the user explicitly reopens a panel the window had auto-collapsed,
67
- // so the rail cannot immediately swallow it again. Cleared once the window is
68
- // wide enough that auto-collapse is no longer in play.
69
- const [autoCollapseOverride, setAutoCollapseOverride] = useState<{
70
- left: boolean;
71
- right: boolean;
72
- }>(NO_OVERRIDE);
23
+ const controller = useDockLayoutStore((state) => state.controller);
24
+ const lastActive = useDockLayoutStore((state) => state.lastActive);
25
+ const visiblePanels = useDockLayoutStore((state) => state.visiblePanels);
73
26
 
74
- // Reconciliation is a live window resize away, not a mount-time snapshot: a
75
- // Studio loaded at 1440 and dragged to a half-screen used to keep its pixel
76
- // widths and squeeze the preview to nothing.
27
+ const initialRef = useRef(initialState);
77
28
  useEffect(() => {
78
- if (typeof window === "undefined") return;
79
- const handleResize = () => {
80
- const width = window.innerWidth;
81
- setViewportWidth(width);
82
- // Cleared here rather than in an effect watching derived state: the rail
83
- // flags depend only on width, and width only changes in this handler.
84
- if (!railsEngaged(width)) {
85
- setAutoCollapseOverride((prev) => (prev.left || prev.right ? NO_OVERRIDE : prev));
86
- }
87
- };
88
- window.addEventListener("resize", handleResize);
89
- return () => window.removeEventListener("resize", handleResize);
90
- }, []);
91
-
92
- const fitted = fitPanelWidths(viewportWidth, preferredWidths, autoCollapseOverride);
93
- const leftCollapsedByWidth = fitted.autoCollapseLeft;
94
- const rightCollapsedByWidth = fitted.autoCollapseRight;
95
-
96
- // Rendered widths, which the drag handles measure from so the seam does not
97
- // jump when a panel is currently narrower than its stored preference.
98
- const fittedRef = useRef(fitted);
99
- fittedRef.current = fitted;
100
-
101
- const panelDragRef = useRef<{
102
- side: PanelSide;
103
- startX: number;
104
- startW: number;
105
- } | null>(null);
106
-
107
- // Preferred widths are also held in a ref so a burst of pointer moves or
108
- // keyboard nudges inside one React batch accumulates, instead of every call
109
- // in the batch reading the same pre-render value.
110
- const preferredRef = useRef(preferredWidths);
111
-
112
- const setPreferred = useCallback((side: PanelSide, width: number) => {
113
- const next = Math.max(0, Math.round(width));
114
- preferredRef.current = { ...preferredRef.current, [side]: next };
115
- setPreferredWidths(preferredRef.current);
116
- return next;
117
- }, []);
118
-
119
- /** Transient: moves the panel without touching the stored preference. */
120
- const updatePanelWidth = useCallback(
121
- (side: PanelSide, width: number) => {
122
- setPreferred(side, width);
123
- },
124
- [setPreferred],
125
- );
126
-
127
- /**
128
- * Durable: only an explicit drag or keyboard nudge writes a preference. A
129
- * width the window forced on us is never persisted, so the user's real
130
- * preference survives a temporary squeeze and returns when the window grows.
131
- */
132
- const commitPanelWidth = useCallback(
133
- (side: PanelSide, width: number) => {
134
- // Persist what the window will actually allow, not a raw pointer delta.
135
- const candidate = { ...preferredRef.current, [side]: Math.max(0, Math.round(width)) };
136
- const settled = fitPanelWidths(readViewportWidth(), candidate)[side];
137
- setPreferred(side, settled);
138
- writeStudioUiPreferences(side === "left" ? { leftWidth: settled } : { rightWidth: settled });
139
- },
140
- [setPreferred],
141
- );
142
-
143
- const adjustPanelWidth = useCallback(
144
- (side: PanelSide, delta: number) => {
145
- commitPanelWidth(side, preferredRef.current[side] + delta);
146
- },
147
- [commitPanelWidth],
148
- );
149
-
150
- // The toggle acts on what the user can SEE, not on stored intent. Toggling
151
- // stored intent instead made the rail's "Show sidebar" button dead in the
152
- // auto-collapsed state: intent was already false, so the click flipped it to
153
- // true (persisting a collapse the user never asked for) while the rail stayed
154
- // railed and nothing visibly happened.
155
- const effectiveLeftCollapsedRef = useRef(false);
156
- effectiveLeftCollapsedRef.current = leftCollapsed || leftCollapsedByWidth;
157
-
158
- const toggleLeftSidebar = useCallback(() => {
159
- const next = !effectiveLeftCollapsedRef.current;
160
- setLeftCollapsed(next);
161
- writeStudioUiPreferences({ leftCollapsed: next });
162
- trackStudioEvent("panel_toggle", { panel: "left_sidebar", collapsed: next });
163
- if (!next) setAutoCollapseOverride((prev) => ({ ...prev, left: true }));
164
- }, []);
165
-
166
- const setRightCollapsedWithOverride = useCallback((collapsed: boolean) => {
167
- setRightCollapsed(collapsed);
168
- if (!collapsed) {
169
- setAutoCollapseOverride((prev) => (prev.right ? prev : { ...prev, right: true }));
29
+ if (!controller) return;
30
+ const { rightCollapsed, rightPanelTab } = initialRef.current ?? {};
31
+ initialRef.current = undefined;
32
+ const store = useDockLayoutStore.getState();
33
+ const wanted = rightPanelTab ? panelForTab(rightPanelTab) : null;
34
+ let unsubscribe = () => {};
35
+ if (wanted && store.openPanels.has(wanted)) {
36
+ store.activatePanel(wanted);
37
+ } else if (wanted === "slideshow") {
38
+ // The slideshow panel only opens once the composition loads and turns out to be one.
39
+ unsubscribe = useDockLayoutStore.subscribe((state) => {
40
+ if (!state.openPanels.has("slideshow")) return;
41
+ unsubscribe();
42
+ state.activatePanel("slideshow");
43
+ });
170
44
  }
45
+ if (rightCollapsed != null) store.setZoneVisible("right", !rightCollapsed);
46
+ return () => unsubscribe();
47
+ }, [controller]);
48
+
49
+ const visibleRight = visiblePanelInZone("right", lastActive, visiblePanels);
50
+ const rightPanelTab = tabForPanel(visibleRight);
51
+ const rightCollapsed = visibleRight === null;
52
+
53
+ const setRightPanelTab = useCallback((tab: RightPanelTab) => {
54
+ const store = useDockLayoutStore.getState();
55
+ const shown = visiblePanelInZone("right", store.lastActive, store.visiblePanels);
56
+ store.activatePanel(panelForTab(tab));
57
+ if (tabForPanel(shown) !== tab) trackStudioEvent("tab_switch", { panel: "right_panel", tab });
171
58
  }, []);
172
59
 
173
- const handlePanelResizeStart = useCallback((side: PanelSide, e: React.PointerEvent) => {
174
- e.preventDefault();
175
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
176
- panelDragRef.current = {
177
- side,
178
- startX: e.clientX,
179
- startW: fittedRef.current[side],
180
- };
181
- }, []);
182
-
183
- const handlePanelResizeMove = useCallback(
184
- (e: React.PointerEvent) => {
185
- const drag = panelDragRef.current;
186
- if (!drag) return;
187
- const delta = e.clientX - drag.startX;
188
- updatePanelWidth(drag.side, drag.startW + (drag.side === "left" ? delta : -delta));
189
- },
190
- [updatePanelWidth],
191
- );
192
-
193
- const handlePanelResizeEnd = useCallback(() => {
194
- const side = panelDragRef.current?.side;
195
- if (side) commitPanelWidth(side, preferredRef.current[side]);
196
- panelDragRef.current = null;
197
- }, [commitPanelWidth]);
198
-
199
- const trackedSetRightPanelTab = useCallback(
200
- (tab: RightPanelTab) => {
201
- const paneAlreadySelected =
202
- tab !== "design" && tab !== "layers"
203
- ? true
204
- : STUDIO_FLAT_INSPECTOR_ENABLED
205
- ? rightInspectorPanesRef.current[tab] &&
206
- !rightInspectorPanesRef.current[tab === "design" ? "layers" : "design"]
207
- : rightInspectorPanesRef.current[tab];
208
- if (rightPanelTabRef.current === tab && paneAlreadySelected) return;
209
- rightPanelTabRef.current = tab;
210
- if (tab === "design" || tab === "layers") {
211
- // Flat inspector: Layers always renders full-height by itself (see
212
- // StudioRightPanel's render gate), so this MUST land on the same
213
- // radio-style exclusivity setExclusiveRightInspectorPane enforces for
214
- // the direct in-panel tab click — every OTHER path that reaches here
215
- // (element select, closing block-params, the header Inspector
216
- // button, and this function's own callers outside an active
217
- // inspector tab) would otherwise additively leave both panes `true`
218
- // and reproduce the "both tabs highlight, only one renders" bug this
219
- // still-additive branch used to cause under the flat flag.
220
- const nextPanes = STUDIO_FLAT_INSPECTOR_ENABLED
221
- ? { design: tab === "design", layers: tab === "layers" }
222
- : { ...rightInspectorPanesRef.current, [tab]: true };
223
- rightInspectorPanesRef.current = nextPanes;
224
- setRightInspectorPanes(nextPanes);
225
- }
226
- setRightPanelTab(tab);
227
- trackStudioEvent("tab_switch", { panel: "right_panel", tab });
228
- },
229
- [setRightPanelTab],
230
- );
231
-
232
- const toggleRightInspectorPane = useCallback((pane: RightInspectorPane) => {
233
- setRightInspectorPanes((panes) => {
234
- const next = { ...panes, [pane]: !panes[pane] };
235
- if (!next.design && !next.layers) return panes;
236
- return next;
237
- });
238
- }, []);
239
-
240
- // Radio-style variant for the flat inspector: Layers always renders full-
241
- // height by itself there (never split-shared with Design), so leaving both
242
- // panes independently toggleable would highlight both tabs as "active"
243
- // while only one actually shows. Selecting one turns the other off.
244
- const setExclusiveRightInspectorPane = useCallback((pane: RightInspectorPane) => {
245
- setRightInspectorPanes({ design: pane === "design", layers: pane === "layers" });
60
+ const setRightCollapsed = useCallback((collapsed: boolean) => {
61
+ const store = useDockLayoutStore.getState();
62
+ if (!collapsed && visiblePanelInZone("right", store.lastActive, store.visiblePanels) === null) {
63
+ store.activatePanel("design");
64
+ return;
65
+ }
66
+ store.setZoneVisible("right", !collapsed);
246
67
  }, []);
247
68
 
248
- return {
249
- leftWidth: fitted.left,
250
- rightWidth: fitted.right,
251
- adjustPanelWidth,
252
- /**
253
- * User intent. Persisted to localStorage; never written by auto-collapse.
254
- * Deliberately read-only outside this hook: `toggleLeftSidebar` is the only
255
- * writer, so it cannot be flipped without also clearing the rail override
256
- * (which would open the sidebar and then immediately rail it again).
257
- */
258
- leftCollapsed,
259
- /** User intent. Synced into the shareable URL; never written by auto-collapse. */
260
- rightCollapsed,
261
- setRightCollapsed: setRightCollapsedWithOverride,
262
- /** What the shell actually renders: intent OR the window forcing a rail. */
263
- effectiveLeftCollapsed: leftCollapsed || leftCollapsedByWidth,
264
- effectiveRightCollapsed: rightCollapsed || rightCollapsedByWidth,
265
- rightPanelTab,
266
- setRightPanelTab: trackedSetRightPanelTab,
267
- rightInspectorPanes,
268
- toggleRightInspectorPane,
269
- setExclusiveRightInspectorPane,
270
- toggleLeftSidebar,
271
- handlePanelResizeStart,
272
- handlePanelResizeMove,
273
- handlePanelResizeEnd,
274
- };
69
+ return { rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab };
275
70
  }
@@ -33,7 +33,7 @@ interface ApplyCallbacks {
33
33
  serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
34
34
  }
35
35
 
36
- interface UsePersistentEditHistoryOptions {
36
+ export interface UsePersistentEditHistoryOptions {
37
37
  projectId: string | null;
38
38
  storage?: EditHistoryStorageAdapter;
39
39
  now?: () => number;
@@ -0,0 +1,150 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import {
3
+ createStudioSaveHttpError,
4
+ retryStudioSave,
5
+ StudioFileConflictError,
6
+ StudioSaveNetworkError,
7
+ } from "../utils/studioSaveDiagnostics";
8
+ import { studioExpectedFileVersion, studioWriteHeaders } from "../utils/studioFileVersion";
9
+
10
+ export interface UseProjectFileWriterOptions {
11
+ projectId: string | null;
12
+ }
13
+
14
+ // The etag-guarded project-file read/write pair every Studio save path
15
+ // shares. A host mounting hand editing outside Studio needs only this, not
16
+ // the full file manager (file tree, editor tabs, uploads, font assets).
17
+ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions) {
18
+ const fileVersionScope = useMemo(
19
+ () => ({ projectId, versions: new Map<string, string | null>() }),
20
+ [projectId],
21
+ );
22
+ const fileVersions = fileVersionScope.versions;
23
+
24
+ const observeProjectFileVersion = useCallback(
25
+ (path: string, version: string | null) => {
26
+ fileVersions.set(path, version);
27
+ },
28
+ [fileVersions],
29
+ );
30
+
31
+ const readProjectFile = useCallback(
32
+ async (path: string): Promise<string> => {
33
+ if (!projectId) throw new Error("No active project");
34
+ const response = await fetch(
35
+ `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`,
36
+ );
37
+ if (!response.ok) throw new Error(`Failed to read ${path}`);
38
+ const data = (await response.json()) as { content?: string; version?: string };
39
+ if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
40
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
41
+ return data.content;
42
+ },
43
+ [fileVersions, projectId],
44
+ );
45
+
46
+ const readOptionalProjectFile = useCallback(
47
+ async (path: string): Promise<string> => {
48
+ if (!projectId) throw new Error("No active project");
49
+ const response = await fetch(
50
+ `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
51
+ );
52
+ if (!response.ok) throw new Error(`Failed to read ${path}`);
53
+ const data = (await response.json()) as { content?: string; version?: string };
54
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
55
+ return typeof data.content === "string" ? data.content : "";
56
+ },
57
+ [fileVersions, projectId],
58
+ );
59
+
60
+ const writeProjectFile = useCallback(
61
+ async (path: string, content: string, expectedContent?: string): Promise<void> => {
62
+ if (!projectId) throw new Error("No active project");
63
+ const writeProjectId = projectId;
64
+ let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
65
+ if (expectedVersion === undefined) {
66
+ const preflight = await fetch(
67
+ `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
68
+ );
69
+ if (preflight.ok) {
70
+ const data = (await preflight.json()) as { content?: string; version?: string };
71
+ throw new StudioFileConflictError({
72
+ filePath: path,
73
+ currentVersion: data.version ?? preflight.headers.get("etag"),
74
+ currentContent: data.content ?? null,
75
+ attemptedContent: content,
76
+ });
77
+ } else if (preflight.status === 404) {
78
+ expectedVersion = null;
79
+ } else {
80
+ throw await createStudioSaveHttpError(preflight, `Failed to read ${path} before save`);
81
+ }
82
+ }
83
+ // fallow-ignore-next-line complexity
84
+ await retryStudioSave(async () => {
85
+ // Each request gets its own receipt identity. If a committed request loses its response,
86
+ // the retry can produce a second filesystem receipt that must be suppressed independently.
87
+ let response: Response;
88
+ try {
89
+ response = await fetch(
90
+ `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
91
+ {
92
+ method: "PUT",
93
+ headers: {
94
+ "Content-Type": "text/plain",
95
+ ...studioWriteHeaders(),
96
+ ...(expectedVersion ? { "If-Match": expectedVersion } : { "If-None-Match": "*" }),
97
+ },
98
+ body: content,
99
+ },
100
+ );
101
+ } catch (error) {
102
+ throw new StudioSaveNetworkError(`Failed to save ${path}: network error`, {
103
+ cause: error,
104
+ });
105
+ }
106
+ if (response.status === 409) {
107
+ const conflict = (await response.json().catch(() => null)) as {
108
+ currentVersion?: string | null;
109
+ currentContent?: string | null;
110
+ } | null;
111
+ const currentVersion = conflict?.currentVersion ?? null;
112
+ if (currentVersion && conflict?.currentContent === content) {
113
+ fileVersions.set(path, currentVersion);
114
+ return;
115
+ }
116
+ throw new StudioFileConflictError({
117
+ filePath: path,
118
+ currentVersion,
119
+ currentContent: conflict?.currentContent ?? null,
120
+ attemptedContent: content,
121
+ });
122
+ }
123
+ if (!response.ok) throw await createStudioSaveHttpError(response, `Failed to save ${path}`);
124
+ const result = (await response.json()) as { version?: string };
125
+ const version = result.version ?? response.headers.get("etag");
126
+ if (!version)
127
+ throw new Error(`Save response for ${path} did not include a content version`);
128
+ fileVersions.set(path, version);
129
+ });
130
+ },
131
+ [fileVersions, projectId],
132
+ );
133
+
134
+ return useMemo(
135
+ () => ({
136
+ fileVersions,
137
+ readProjectFile,
138
+ writeProjectFile,
139
+ readOptionalProjectFile,
140
+ observeProjectFileVersion,
141
+ }),
142
+ [
143
+ fileVersions,
144
+ readProjectFile,
145
+ writeProjectFile,
146
+ readOptionalProjectFile,
147
+ observeProjectFileVersion,
148
+ ],
149
+ );
150
+ }
@@ -0,0 +1,181 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { useDockLayoutStore, type DockController } from "../components/dock/dockLayoutStore";
7
+ import { PANEL_IDS, type PanelId } from "../components/dock/panelRegistry";
8
+ import {
9
+ useBlockParamsDismissal,
10
+ useCaptionDesignFocus,
11
+ useDismissingTabSetter,
12
+ useRightPanelIntent,
13
+ useSlideshowDockPanel,
14
+ } from "./useRightPanelIntents";
15
+
16
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
17
+
18
+ function controllerMock(): DockController {
19
+ return {
20
+ open: vi.fn(),
21
+ activate: vi.fn(),
22
+ close: vi.fn(),
23
+ setTitle: vi.fn(),
24
+ setGroupVisible: vi.fn(),
25
+ reset: vi.fn(),
26
+ };
27
+ }
28
+
29
+ function seed(state: {
30
+ open?: PanelId[];
31
+ visible?: PanelId[];
32
+ lastActive?: Partial<Record<"left" | "center" | "right", PanelId>>;
33
+ }) {
34
+ const controller = controllerMock();
35
+ useDockLayoutStore.setState({
36
+ controller,
37
+ openPanels: new Set(state.open ?? PANEL_IDS.filter((id) => id !== "slideshow")),
38
+ visiblePanels: new Set(state.visible ?? []),
39
+ lastActive: state.lastActive ?? {},
40
+ activePanel: null,
41
+ });
42
+ return controller;
43
+ }
44
+
45
+ const mountedRoots: Array<() => void> = [];
46
+
47
+ function mountHook<P>(hook: (props: P) => unknown, initial: P) {
48
+ const host = document.createElement("div");
49
+ document.body.append(host);
50
+ const root = createRoot(host);
51
+ mountedRoots.push(() => act(() => root.unmount()));
52
+ let latest: unknown;
53
+ function Harness({ props }: { props: P }) {
54
+ latest = hook(props);
55
+ return null;
56
+ }
57
+ const render = (props: P) => act(() => root.render(React.createElement(Harness, { props })));
58
+ render(initial);
59
+ return { render, value: () => latest, unmount: () => act(() => root.unmount()) };
60
+ }
61
+
62
+ afterEach(() => {
63
+ for (const unmount of mountedRoots.splice(0)) unmount();
64
+ vi.restoreAllMocks();
65
+ document.body.innerHTML = "";
66
+ useDockLayoutStore.setState({ controller: null });
67
+ });
68
+
69
+ describe("useSlideshowDockPanel", () => {
70
+ it("opens the panel when the file becomes a slideshow and closes it when it stops", () => {
71
+ const controller = seed({});
72
+ const view = mountHook(useSlideshowDockPanel, false);
73
+ expect(controller.open).not.toHaveBeenCalled();
74
+ view.render(true);
75
+ expect(controller.open).toHaveBeenCalledWith("slideshow");
76
+ act(() =>
77
+ useDockLayoutStore.setState({ openPanels: new Set<PanelId>(["design", "slideshow"]) }),
78
+ );
79
+ view.render(false);
80
+ expect(controller.close).toHaveBeenCalledWith("slideshow");
81
+ });
82
+
83
+ it("keeps a restored slideshow placement while the file is still loading", () => {
84
+ const controller = seed({ open: ["design", "slideshow"] });
85
+ const view = mountHook(useSlideshowDockPanel, false);
86
+ expect(controller.close).not.toHaveBeenCalled();
87
+ view.render(true);
88
+ expect(controller.open).not.toHaveBeenCalled();
89
+ view.render(false);
90
+ expect(controller.close).toHaveBeenCalledWith("slideshow");
91
+ });
92
+
93
+ it("leaves a slideshow panel that a restored layout already holds alone", () => {
94
+ const controller = seed({ open: ["design", "slideshow"] });
95
+ mountHook(useSlideshowDockPanel, true);
96
+ expect(controller.open).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it("waits for the dock to mount, then opens the panel once", () => {
100
+ const controller = seed({});
101
+ useDockLayoutStore.setState({ controller: null });
102
+ mountHook(useSlideshowDockPanel, true);
103
+ expect(controller.open).not.toHaveBeenCalled();
104
+ act(() => useDockLayoutStore.setState({ controller }));
105
+ expect(controller.open).toHaveBeenCalledWith("slideshow");
106
+ });
107
+
108
+ it("does not reopen a panel the user closed while the file is still a slideshow", () => {
109
+ const controller = seed({});
110
+ const view = mountHook(useSlideshowDockPanel, true);
111
+ expect(controller.open).toHaveBeenCalledTimes(1);
112
+ act(() => useDockLayoutStore.setState({ openPanels: new Set<PanelId>(["design"]) }));
113
+ view.render(true);
114
+ expect(controller.open).toHaveBeenCalledTimes(1);
115
+ });
116
+ });
117
+
118
+ describe("useBlockParamsDismissal", () => {
119
+ const props = { hasBlockParams: true, onDismiss: vi.fn() };
120
+
121
+ it("keeps block params while the Design panel is showing", () => {
122
+ seed({ visible: ["design"] });
123
+ const onDismiss = vi.fn();
124
+ mountHook(useBlockParamsDismissal, { ...props, onDismiss });
125
+ expect(onDismiss).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it("dismisses block params when the Design panel is no longer showing", () => {
129
+ seed({ visible: ["design"] });
130
+ const onDismiss = vi.fn();
131
+ mountHook(useBlockParamsDismissal, { ...props, onDismiss });
132
+ act(() => useDockLayoutStore.setState({ visiblePanels: new Set<PanelId>(["layers"]) }));
133
+ expect(onDismiss).toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ describe("useDismissingTabSetter", () => {
138
+ it("dismisses block params for any tab but block-params, then switches", () => {
139
+ const setTab = vi.fn();
140
+ const dismiss = vi.fn();
141
+ const view = mountHook(
142
+ ({ tab }: { tab: string }) => {
143
+ const set = useDismissingTabSetter(setTab, dismiss);
144
+ return () => set(tab);
145
+ },
146
+ { tab: "design" },
147
+ );
148
+ (view.value() as () => void)();
149
+ expect(dismiss).toHaveBeenCalledTimes(1);
150
+ expect(setTab).toHaveBeenCalledWith("design");
151
+ view.render({ tab: "block-params" });
152
+ (view.value() as () => void)();
153
+ expect(dismiss).toHaveBeenCalledTimes(1);
154
+ expect(setTab).toHaveBeenLastCalledWith("block-params");
155
+ });
156
+ });
157
+
158
+ describe("useCaptionDesignFocus", () => {
159
+ it("brings Design forward when caption editing starts over another right panel", () => {
160
+ seed({ visible: ["layers"], lastActive: { right: "layers" } });
161
+ const view = mountHook(useCaptionDesignFocus, false);
162
+ const activate = vi.spyOn(useDockLayoutStore.getState(), "activatePanel");
163
+ view.render(true);
164
+ expect(activate).toHaveBeenCalledWith("design");
165
+ });
166
+
167
+ it("leaves a collapsed right column alone", () => {
168
+ seed({ visible: [], lastActive: { right: "layers" } });
169
+ const activate = vi.spyOn(useDockLayoutStore.getState(), "activatePanel");
170
+ mountHook(useCaptionDesignFocus, true);
171
+ expect(activate).not.toHaveBeenCalled();
172
+ });
173
+ });
174
+
175
+ describe("useRightPanelIntent", () => {
176
+ it("still names the inspector panel while the right column is collapsed", () => {
177
+ seed({ visible: [], lastActive: { right: "design" } });
178
+ const view = mountHook(useRightPanelIntent, undefined);
179
+ expect(view.value()).toBe("design");
180
+ });
181
+ });