@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,135 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import React, { act } from "react";
4
- import { createRoot, type Root } from "react-dom/client";
5
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
- import { useProjectSignaturePoll } from "./useProjectSignaturePoll";
7
-
8
- Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
9
-
10
- function HookHost({
11
- signature,
12
- onChange,
13
- }: {
14
- signature: string | undefined;
15
- onChange: () => void;
16
- }) {
17
- useProjectSignaturePoll("demo", signature, onChange);
18
- return null;
19
- }
20
-
21
- let root: Root | null = null;
22
- let container: HTMLElement | null = null;
23
-
24
- function mount(signature: string | undefined, onChange: () => void): void {
25
- act(() => {
26
- if (!root) {
27
- container = document.createElement("div");
28
- document.body.appendChild(container);
29
- root = createRoot(container);
30
- }
31
- root.render(<HookHost signature={signature} onChange={onChange} />);
32
- });
33
- }
34
-
35
- function unmount(): void {
36
- if (root) {
37
- act(() => root?.unmount());
38
- root = null;
39
- }
40
- container?.remove();
41
- container = null;
42
- }
43
-
44
- function mockSignatureResponse(signature: string): ReturnType<typeof vi.fn> {
45
- const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ signature }) }));
46
- vi.stubGlobal("fetch", fetchMock);
47
- return fetchMock;
48
- }
49
-
50
- describe("useProjectSignaturePoll", () => {
51
- beforeEach(() => {
52
- vi.useFakeTimers();
53
- });
54
-
55
- afterEach(() => {
56
- unmount();
57
- vi.useRealTimers();
58
- vi.unstubAllGlobals();
59
- vi.restoreAllMocks();
60
- });
61
-
62
- it("fires onChange when the polled signature moves past the loaded one", async () => {
63
- const fetchMock = mockSignatureResponse("sig-b");
64
- const onChange = vi.fn();
65
- mount("sig-a", onChange);
66
-
67
- await vi.advanceTimersByTimeAsync(2000);
68
- expect(fetchMock).toHaveBeenCalledWith("/api/projects/demo/signature");
69
- expect(onChange).toHaveBeenCalledTimes(1);
70
- });
71
-
72
- it("stays quiet while the polled signature matches", async () => {
73
- mockSignatureResponse("sig-a");
74
- const onChange = vi.fn();
75
- mount("sig-a", onChange);
76
-
77
- await vi.advanceTimersByTimeAsync(6000);
78
- expect(onChange).not.toHaveBeenCalled();
79
- });
80
-
81
- it("does not poll before a baseline signature exists", async () => {
82
- const fetchMock = mockSignatureResponse("sig-a");
83
- const onChange = vi.fn();
84
- mount(undefined, onChange);
85
-
86
- await vi.advanceTimersByTimeAsync(6000);
87
- expect(fetchMock).not.toHaveBeenCalled();
88
- expect(onChange).not.toHaveBeenCalled();
89
- });
90
-
91
- it("re-arms against the caller's refreshed signature without re-mounting", async () => {
92
- mockSignatureResponse("sig-b");
93
- const onChange = vi.fn();
94
- mount("sig-a", onChange);
95
-
96
- await vi.advanceTimersByTimeAsync(2000);
97
- expect(onChange).toHaveBeenCalledTimes(1);
98
-
99
- // The caller refetched and now holds the polled signature — no more firing.
100
- mount("sig-b", onChange);
101
- await vi.advanceTimersByTimeAsync(6000);
102
- expect(onChange).toHaveBeenCalledTimes(1);
103
- });
104
-
105
- it("skips ticks while the tab is hidden and re-checks on visibility", async () => {
106
- const fetchMock = mockSignatureResponse("sig-b");
107
- const onChange = vi.fn();
108
- let hidden = true;
109
- vi.spyOn(document, "hidden", "get").mockImplementation(() => hidden);
110
- mount("sig-a", onChange);
111
-
112
- await vi.advanceTimersByTimeAsync(6000);
113
- expect(fetchMock).not.toHaveBeenCalled();
114
-
115
- hidden = false;
116
- await act(async () => {
117
- document.dispatchEvent(new Event("visibilitychange"));
118
- await vi.advanceTimersByTimeAsync(0);
119
- });
120
- expect(onChange).toHaveBeenCalledTimes(1);
121
- });
122
-
123
- it("stops polling after unmount", async () => {
124
- mockSignatureResponse("sig-b");
125
- const onChange = vi.fn();
126
- mount("sig-a", onChange);
127
-
128
- await vi.advanceTimersByTimeAsync(2000);
129
- expect(onChange).toHaveBeenCalledTimes(1);
130
-
131
- unmount();
132
- await vi.advanceTimersByTimeAsync(10_000);
133
- expect(onChange).toHaveBeenCalledTimes(1);
134
- });
135
- });
@@ -1,68 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { buildProjectApiPath } from "../utils/projectRouting";
3
-
4
- const POLL_INTERVAL_MS = 2000;
5
-
6
- /** One poll: the current signature, or null on any failure (skip this tick). */
7
- async function fetchProjectSignature(projectId: string): Promise<string | null> {
8
- try {
9
- const res = await fetch(buildProjectApiPath(projectId, "/signature"));
10
- if (!res.ok) return null;
11
- const body = (await res.json()) as { signature?: string };
12
- return typeof body.signature === "string" ? body.signature : null;
13
- } catch {
14
- return null;
15
- }
16
- }
17
-
18
- /**
19
- * Poll the project's content signature and fire `onChange` when it no longer
20
- * matches `currentSignature` — the storyboard board uses this to refresh itself
21
- * while an agent writes sketch frames to disk.
22
- *
23
- * The comparison baseline is the signature the caller's data was loaded with,
24
- * so a refetch triggered by `onChange` naturally re-arms the poll with the new
25
- * value. Ticks are skipped while the tab is hidden (a visibility flip re-checks
26
- * immediately) and while a previous request is still in flight; request
27
- * failures are ignored — polling degrades to today's manual-reload behavior.
28
- */
29
- export function useProjectSignaturePoll(
30
- projectId: string | null,
31
- currentSignature: string | undefined,
32
- onChange: () => void,
33
- ): void {
34
- const signatureRef = useRef(currentSignature);
35
- const onChangeRef = useRef(onChange);
36
- signatureRef.current = currentSignature;
37
- onChangeRef.current = onChange;
38
-
39
- useEffect(() => {
40
- if (!projectId) return;
41
- let disposed = false;
42
- let inFlight = false;
43
-
44
- const tick = async () => {
45
- if (disposed || inFlight || document.hidden) return;
46
- // No baseline yet (initial storyboard fetch still loading, or an older
47
- // server without the signature field) — nothing to compare against.
48
- if (signatureRef.current === undefined) return;
49
- inFlight = true;
50
- const latest = await fetchProjectSignature(projectId);
51
- inFlight = false;
52
- if (disposed || latest === null) return;
53
- if (latest !== signatureRef.current) onChangeRef.current();
54
- };
55
-
56
- const interval = window.setInterval(() => void tick(), POLL_INTERVAL_MS);
57
- const onVisibilityChange = () => {
58
- if (!document.hidden) void tick();
59
- };
60
- document.addEventListener("visibilitychange", onVisibilityChange);
61
-
62
- return () => {
63
- disposed = true;
64
- window.clearInterval(interval);
65
- document.removeEventListener("visibilitychange", onVisibilityChange);
66
- };
67
- }, [projectId]);
68
- }
@@ -1,99 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import type {
3
- StoryboardFrame,
4
- StoryboardGlobals,
5
- StoryboardWarning,
6
- } from "@hyperframes/core/storyboard";
7
- import { buildProjectApiPath } from "../utils/projectRouting";
8
-
9
- /** A frame as returned by the API: parsed frame + disk-resolution info. */
10
- export interface StoryboardFrameView extends StoryboardFrame {
11
- /** Whether `src` resolves to an existing file inside the project. */
12
- srcExists: boolean;
13
- }
14
-
15
- /** The companion narration script (SCRIPT.md), when present alongside the storyboard. */
16
- export interface StoryboardScript {
17
- exists: boolean;
18
- path: string;
19
- content: string;
20
- }
21
-
22
- /** Shape of `GET /api/projects/:id/storyboard`. */
23
- export interface StoryboardResponse {
24
- exists: boolean;
25
- path: string;
26
- globals: StoryboardGlobals;
27
- frames: StoryboardFrameView[];
28
- warnings: StoryboardWarning[];
29
- script?: StoryboardScript;
30
- /** Project content signature this payload was derived from (poster cache-bust + change poll). */
31
- signature?: string;
32
- }
33
-
34
- export interface UseStoryboardResult {
35
- data: StoryboardResponse | null;
36
- loading: boolean;
37
- error: string | null;
38
- reload: () => void;
39
- }
40
-
41
- /**
42
- * Load the parsed storyboard manifest for a project. Markdown stays canonical on
43
- * disk; this fetches the server-derived JSON the storyboard view renders.
44
- *
45
- * `reload()` refetches in place: the blocking spinner only shows on first load
46
- * and project switches, so background refreshes (file-change polling, post-save
47
- * reloads) swap data without flashing the board.
48
- */
49
- export function useStoryboard(projectId: string | null): UseStoryboardResult {
50
- const [data, setData] = useState<StoryboardResponse | null>(null);
51
- const [loading, setLoading] = useState(true);
52
- const [error, setError] = useState<string | null>(null);
53
- const [reloadKey, setReloadKey] = useState(0);
54
- const hasDataRef = useRef(false);
55
- const lastProjectRef = useRef<string | null>(null);
56
-
57
- const reload = useCallback(() => setReloadKey((k) => k + 1), []);
58
-
59
- useEffect(() => {
60
- if (!projectId) return;
61
- let cancelled = false;
62
- if (lastProjectRef.current !== projectId) {
63
- lastProjectRef.current = projectId;
64
- hasDataRef.current = false;
65
- setData(null);
66
- }
67
- if (!hasDataRef.current) setLoading(true);
68
- setError(null);
69
-
70
- // Route through buildProjectApiPath so the (URL-derived) projectId is encoded
71
- // into the path rather than interpolated raw (CodeQL js/client-side-request-forgery).
72
- fetch(buildProjectApiPath(projectId, "/storyboard"))
73
- .then((res) => {
74
- if (!res.ok) throw new Error(`storyboard request failed: ${res.status}`);
75
- return res.json() as Promise<StoryboardResponse>;
76
- })
77
- .then((json) => {
78
- if (cancelled) return;
79
- hasDataRef.current = true;
80
- setData(json);
81
- })
82
- .catch((err: unknown) => {
83
- if (cancelled) return;
84
- // A failed background refresh keeps the last good board on screen;
85
- // the blocking error state is only for loads with nothing to show.
86
- if (hasDataRef.current) return;
87
- setError(err instanceof Error ? err.message : "failed to load storyboard");
88
- })
89
- .finally(() => {
90
- if (!cancelled) setLoading(false);
91
- });
92
-
93
- return () => {
94
- cancelled = true;
95
- };
96
- }, [projectId, reloadKey]);
97
-
98
- return { data, loading, error, reload };
99
- }
@@ -1,164 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- MIN_PREVIEW_H,
4
- MIN_PREVIEW_W,
5
- MIN_TIMELINE_H,
6
- RAIL_W,
7
- defaultPanelWidths,
8
- fitPanelWidths,
9
- fitTimelineHeight,
10
- } from "./fitPanels";
11
-
12
- function fitAt(viewportWidth: number) {
13
- return fitPanelWidths(viewportWidth, defaultPanelWidths(viewportWidth));
14
- }
15
-
16
- describe("defaultPanelWidths", () => {
17
- // Frozen literals, deliberately NOT compared against the live hook: the hook
18
- // now delegates here, so asserting against it would be circular and would
19
- // silently stop guarding the "wide windows are untouched" promise.
20
- it.each([
21
- [1680, 384, 424],
22
- [1512, 384, 424],
23
- [1280, 329, 364],
24
- ])("keeps %ipx identical to the pre-change defaults", (vw, left, right) => {
25
- expect(defaultPanelWidths(vw)).toEqual({ left, right });
26
- });
27
- });
28
-
29
- describe("fitPanelWidths", () => {
30
- it.each([1680, 1512, 1280])("leaves %ipx untouched by reconciliation", (vw) => {
31
- const preferred = defaultPanelWidths(vw);
32
- const fitted = fitPanelWidths(vw, preferred);
33
- expect(fitted.left).toBe(preferred.left);
34
- expect(fitted.right).toBe(preferred.right);
35
- expect(fitted.autoCollapseLeft).toBe(false);
36
- expect(fitted.autoCollapseRight).toBe(false);
37
- });
38
-
39
- it.each([1680, 1512, 1280, 1100, 960, 860, 760, 640, 560, 480])(
40
- "never starves the preview at %ipx",
41
- (vw) => {
42
- expect(fitAt(vw).preview).toBeGreaterThanOrEqual(MIN_PREVIEW_W);
43
- },
44
- );
45
-
46
- it("matches the projected preview widths from the plan", () => {
47
- const projected: Array<[number, number]> = [
48
- [1680, 866],
49
- [1512, 698],
50
- [1280, 581],
51
- [1100, 499],
52
- [960, 427],
53
- [860, 360],
54
- [760, 432],
55
- [640, 592],
56
- [560, 512],
57
- [480, 432],
58
- ];
59
- for (const [vw, preview] of projected) {
60
- expect({ vw, preview: fitAt(vw).preview }).toEqual({ vw, preview });
61
- }
62
- });
63
-
64
- it("fits everything at its minimum at exactly 860, the derived threshold", () => {
65
- const fitted = fitAt(860);
66
- expect(fitted).toMatchObject({
67
- left: 214,
68
- right: 280,
69
- preview: MIN_PREVIEW_W,
70
- autoCollapseLeft: false,
71
- });
72
- });
73
-
74
- it("rails the sidebar one pixel below the threshold", () => {
75
- expect(fitAt(860).autoCollapseLeft).toBe(false);
76
- expect(fitAt(859).autoCollapseLeft).toBe(true);
77
- expect(fitAt(859).left).toBe(RAIL_W);
78
- });
79
-
80
- it("hides the inspector one pixel below its own threshold", () => {
81
- expect(fitAt(700).autoCollapseRight).toBe(false);
82
- expect(fitAt(700).right).toBeGreaterThan(0);
83
- expect(fitAt(699).autoCollapseRight).toBe(true);
84
- expect(fitAt(699).right).toBe(0);
85
- });
86
-
87
- it("never drives an EXPANDED sidebar below its usable minimum", () => {
88
- // Guards the leftFloor/rightFloor split: the squeeze fallback may only reach
89
- // the rail width when the panel is actually railed.
90
- const fitted = fitPanelWidths(900, { left: 600, right: 600 });
91
- expect(fitted.left).toBeGreaterThanOrEqual(200);
92
- expect(fitted.right).toBeGreaterThanOrEqual(280);
93
- });
94
-
95
- it("caps a single panel at 40% of the window", () => {
96
- expect(fitPanelWidths(1600, { left: 900, right: 280 }).left).toBe(640);
97
- });
98
-
99
- it("lets the preview floor win when the cap alone is not enough", () => {
100
- // 40% of 1000 is 400, but 400 + 280 + 6 leaves the preview at 314.
101
- const fitted = fitPanelWidths(1000, { left: 900, right: 280 });
102
- expect(fitted.left).toBe(354);
103
- expect(fitted.preview).toBe(MIN_PREVIEW_W);
104
- });
105
-
106
- it("survives a viewport narrower than the preview floor alone", () => {
107
- const fitted = fitAt(300);
108
- expect(fitted.preview).toBeGreaterThanOrEqual(0);
109
- expect(Number.isNaN(fitted.preview)).toBe(false);
110
- expect(fitted.left).toBe(RAIL_W);
111
- });
112
-
113
- it("returns preferences untouched before the shell is measured", () => {
114
- const preferred = { left: 320, right: 400 };
115
- expect(fitPanelWidths(0, preferred)).toMatchObject({ ...preferred, preview: 0 });
116
- expect(fitPanelWidths(Number.NaN, preferred)).toMatchObject(preferred);
117
- });
118
-
119
- it("is idempotent, so a resize loop cannot oscillate", () => {
120
- for (const vw of [1512, 1100, 860, 760, 560]) {
121
- const once = fitPanelWidths(vw, defaultPanelWidths(vw));
122
- const twice = fitPanelWidths(vw, once);
123
- expect(twice).toEqual(once);
124
- }
125
- });
126
- });
127
-
128
- describe("fitTimelineHeight", () => {
129
- it("keeps a height that fits", () => {
130
- expect(fitTimelineHeight(717, 429)).toBe(429);
131
- });
132
-
133
- it("reclaims height for the preview when the shell shrinks", () => {
134
- // The measured bug: mounted at 760 tall, dragged to 520. Preview was 47px.
135
- expect(fitTimelineHeight(477, 429)).toBe(477 - MIN_PREVIEW_H);
136
- expect(477 - fitTimelineHeight(477, 429)).toBeGreaterThanOrEqual(MIN_PREVIEW_H);
137
- });
138
-
139
- it("raises a too-small preference to the timeline minimum", () => {
140
- expect(fitTimelineHeight(717, 10)).toBe(MIN_TIMELINE_H);
141
- });
142
-
143
- it("keeps the timeline usable when the shell is shorter than both minimums", () => {
144
- expect(fitTimelineHeight(120, 429)).toBe(MIN_TIMELINE_H);
145
- });
146
-
147
- it("returns the preference before the shell is measured", () => {
148
- expect(fitTimelineHeight(0, 429)).toBe(429);
149
- });
150
-
151
- it("is idempotent", () => {
152
- const once = fitTimelineHeight(477, 429);
153
- expect(fitTimelineHeight(477, once)).toBe(once);
154
- });
155
-
156
- it("would return the preferred height if callers kept one", () => {
157
- // The helper is preference-preserving; the vertical CALLER still stores the
158
- // clamped value, so a shrink-then-grow does not restore the old height the
159
- // way panel widths do. Tracked as a known asymmetry, not fixed here.
160
- const preferred = 429;
161
- expect(fitTimelineHeight(477, preferred)).toBe(277);
162
- expect(fitTimelineHeight(717, preferred)).toBe(429);
163
- });
164
- });
@@ -1,151 +0,0 @@
1
- /**
2
- * Panel reconciliation for the Studio shell.
3
- *
4
- * The shell is `[sidebar | preview | inspector]` over a full-width timeline.
5
- * Only the preview is flexible (`flex-1 min-w-0`), so without an explicit floor
6
- * it absorbs every squeeze: at a 560px window the old 240/320 panel floors alone
7
- * overflowed the viewport and the preview rendered at 2px, on a *fresh* load.
8
- * The same held vertically once the window was resized after mount.
9
- *
10
- * Every caller (mount, resize, drag, keyboard nudge, preference restore) routes
11
- * through {@link fitPanelWidths} / {@link fitTimelineHeight} so exactly one place
12
- * decides who yields. The rule is: the preview gets its floor first, panels
13
- * shrink toward their minimums, then collapse to rails.
14
- */
15
-
16
- /** Smallest preview box worth editing in. Chosen, not derived — see the plan. */
17
- export const MIN_PREVIEW_W = 360;
18
- export const MIN_PREVIEW_H = 200;
19
-
20
- const MIN_LEFT = 200;
21
- const MIN_RIGHT = 280;
22
- /**
23
- * Collapsed sidebar rail, as a layout FOOTPRINT: the `w-10` box (40) plus the
24
- * `mr-0.5` gap (2) it contributes to the flex row. Measured in the browser at a
25
- * 760px window: rail box 40, margin-right 2, preview starts at x=43.
26
- */
27
- export const RAIL_W = 42;
28
- export const MIN_TIMELINE_H = 100;
29
-
30
- /** The two 3px resize seams between the three top-row panels. */
31
- const PANEL_SEAM_W = 6;
32
-
33
- /** No single panel may claim more than this share of the window. */
34
- const PANEL_MAX_RATIO = 0.4;
35
-
36
- /*
37
- * Thresholds below are DERIVED from the minimums above, not chosen:
38
- *
39
- * MIN_LEFT 200 + MIN_RIGHT 280 + MIN_PREVIEW_W 360 + PANEL_SEAM_W 6 = 846
40
- * -> below ~860 the sidebar can no longer fit at its minimum.
41
- * RAIL_W 40 + MIN_RIGHT 280 + MIN_PREVIEW_W 360 + PANEL_SEAM_W 6 = 686
42
- * -> below ~700 the inspector can no longer fit either.
43
- *
44
- * Change a minimum and move the matching threshold with it.
45
- */
46
- const RAIL_LEFT_BELOW = 860;
47
- const RAIL_RIGHT_BELOW = 700;
48
-
49
- /**
50
- * True when the window is narrow enough to force at least one panel to a rail.
51
- * The sidebar threshold is the wider of the two, so it is the whole condition.
52
- */
53
- export function railsEngaged(viewportWidth: number): boolean {
54
- return viewportWidth < RAIL_LEFT_BELOW;
55
- }
56
-
57
- export interface PanelWidths {
58
- left: number;
59
- right: number;
60
- }
61
-
62
- export interface FittedPanelWidths extends PanelWidths {
63
- preview: number;
64
- /**
65
- * Width-driven collapse. Derived per render; NEVER persisted and never written
66
- * back to the user's own collapse preference — `leftCollapsed` lives in
67
- * localStorage and `rightCollapsed` is synced into the shareable Studio URL,
68
- * so a ten-second window drag must not rewrite either.
69
- */
70
- autoCollapseLeft: boolean;
71
- autoCollapseRight: boolean;
72
- }
73
-
74
- function clamp(value: number, min: number, max: number): number {
75
- return Math.max(min, Math.min(max, value));
76
- }
77
-
78
- /**
79
- * The width each panel wants on a given viewport, before reconciliation.
80
- * Upper bounds (384 / 424) are unchanged from the original implementation, which
81
- * is what keeps windows at or above 1280px byte-identical to before.
82
- */
83
- export function defaultPanelWidths(viewportWidth: number): PanelWidths {
84
- return {
85
- left: Math.max(MIN_LEFT, Math.min(384, Math.round(viewportWidth * 0.257))),
86
- right: Math.max(MIN_RIGHT, Math.min(424, Math.round(viewportWidth * 0.284))),
87
- };
88
- }
89
-
90
- /**
91
- * Resolve preferred panel widths against the viewport, reserving the preview.
92
- *
93
- * Order of yielding: window-relative cap, then the inspector shrinks toward its
94
- * minimum, then the sidebar, then rails. A viewport of 0 (element not measured
95
- * yet) returns the preferences untouched so a pre-measurement frame cannot
96
- * clobber a real preference.
97
- */
98
- // fallow-ignore-next-line complexity
99
- export function fitPanelWidths(
100
- viewportWidth: number,
101
- preferred: PanelWidths,
102
- /**
103
- * Sides the user explicitly reopened while the window was narrow. A side
104
- * listed here keeps its real width instead of the rail: without this the
105
- * panel would render expanded at 42px and squash its own content.
106
- */
107
- reopened: { left: boolean; right: boolean } = { left: false, right: false },
108
- ): FittedPanelWidths {
109
- const vw = Number.isFinite(viewportWidth) ? Math.max(0, viewportWidth) : 0;
110
- if (vw <= 0) {
111
- return { ...preferred, preview: 0, autoCollapseLeft: false, autoCollapseRight: false };
112
- }
113
-
114
- const autoCollapseLeft = vw < RAIL_LEFT_BELOW && !reopened.left;
115
- const autoCollapseRight = vw < RAIL_RIGHT_BELOW && !reopened.right;
116
- const cap = Math.floor(vw * PANEL_MAX_RATIO);
117
-
118
- // A railed panel's floor is its rail width; an open panel's floor is its
119
- // minimum usable width. Without this split the squeeze fallback below would
120
- // drive an *expanded* sidebar down to 40px and render its content unusable.
121
- const leftFloor = autoCollapseLeft ? RAIL_W : MIN_LEFT;
122
- const rightFloor = autoCollapseRight ? 0 : MIN_RIGHT;
123
-
124
- let left = autoCollapseLeft ? RAIL_W : clamp(preferred.left, MIN_LEFT, Math.max(MIN_LEFT, cap));
125
- let right = autoCollapseRight ? 0 : clamp(preferred.right, MIN_RIGHT, Math.max(MIN_RIGHT, cap));
126
-
127
- const budget = vw - MIN_PREVIEW_W - PANEL_SEAM_W;
128
- if (left + right > budget) right = Math.max(rightFloor, budget - left);
129
- if (left + right > budget) left = Math.max(leftFloor, budget - right);
130
-
131
- return {
132
- left,
133
- right,
134
- preview: Math.max(0, vw - left - right - PANEL_SEAM_W),
135
- autoCollapseLeft,
136
- autoCollapseRight,
137
- };
138
- }
139
-
140
- /**
141
- * Resolve a preferred timeline height against the shell's measured height.
142
- *
143
- * Subsumes the clamping that used to be inlined in TimelineResizeDivider's
144
- * pointer and keyboard handlers and duplicated (mount-only) in NLEContext.
145
- */
146
- export function fitTimelineHeight(containerHeight: number, preferred: number): number {
147
- const h = Number.isFinite(containerHeight) ? containerHeight : 0;
148
- const wanted = Number.isFinite(preferred) ? preferred : MIN_TIMELINE_H;
149
- if (h <= 0) return Math.max(MIN_TIMELINE_H, wanted);
150
- return clamp(wanted, MIN_TIMELINE_H, Math.max(MIN_TIMELINE_H, h - MIN_PREVIEW_H));
151
- }