@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
@@ -0,0 +1,222 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ // Proves the package's public exports, not internal hook paths, are enough
4
+ // for a host to mount hand editing outside EditorShell: an edit writes
5
+ // through the history path, Undo restores it, and a 409 shows the banner.
6
+ import { act, createElement, useRef } from "react";
7
+ import { createRoot, type Root } from "react-dom/client";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ // Backend/storage mocking only, standing in for a host's own server and
10
+ // persistence — not part of the host's integration code under test.
11
+ import { studioFileContentVersion } from "./utils/studioFileVersion";
12
+ import { createMemoryEditHistoryStorage } from "./utils/editHistoryStorage";
13
+ import {
14
+ flushStudioPendingEdits,
15
+ useTimelineEditing,
16
+ useProjectFileWriter,
17
+ usePersistentEditHistory,
18
+ useEditHistoryActions,
19
+ ExternalFileConflictBanner,
20
+ StudioFileConflictError,
21
+ type ExternalFileChangeCoordinatorHandle,
22
+ type TimelineElement,
23
+ } from "./index";
24
+
25
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
26
+
27
+ const ORIGINAL_HTML = '<div id="clip" data-start="0" data-track-index="0"></div>';
28
+
29
+ let root: Root | null = null;
30
+ afterEach(() => {
31
+ act(() => root?.unmount());
32
+ document.body.innerHTML = "";
33
+ vi.unstubAllGlobals();
34
+ });
35
+
36
+ /** A minimal disk with etag-guarded PUT, faithful enough to exercise a real
37
+ * conflict: a stale If-Match gets a 409 with the current version + content. */
38
+ function stubHostProjectDisk(
39
+ initial: Record<string, string>,
40
+ options?: { editElsewhereAfterFirstRead?: boolean },
41
+ ) {
42
+ const content = { ...initial };
43
+ const readsSeen = new Set<string>();
44
+ // The real backend's etag is a content hash (useProjectFileWriter derives
45
+ // If-Match the same way) — an arbitrary counter here would make every
46
+ // write from a host that hasn't just read the file look stale.
47
+ const versionOf = (path: string) => studioFileContentVersion(content[path]);
48
+
49
+ async function handlePut(path: string, init: RequestInit | undefined) {
50
+ const ifMatch = new Headers(init?.headers).get("If-Match");
51
+ const currentVersion = await versionOf(path);
52
+ if (ifMatch && ifMatch !== currentVersion) {
53
+ return new Response(JSON.stringify({ currentVersion, currentContent: content[path] }), {
54
+ status: 409,
55
+ });
56
+ }
57
+ content[path] = String(init?.body ?? "");
58
+ return new Response(JSON.stringify({ version: await versionOf(path) }), { status: 200 });
59
+ }
60
+
61
+ async function handleGet(path: string) {
62
+ const served = content[path];
63
+ // Simulates another writer landing between this read and the caller's
64
+ // own write: content moves on right after being served once, so the
65
+ // caller's hash of what it just read no longer matches disk.
66
+ if (options?.editElsewhereAfterFirstRead && !readsSeen.has(path)) {
67
+ readsSeen.add(path);
68
+ content[path] = `${served} <!--edited elsewhere-->`;
69
+ }
70
+ return new Response(
71
+ JSON.stringify({ content: served, version: await studioFileContentVersion(served) }),
72
+ { status: 200 },
73
+ );
74
+ }
75
+
76
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
77
+ const match = String(input).match(/\/files\/([^?]+)/);
78
+ const path = match ? decodeURIComponent(match[1]) : "";
79
+ return (init?.method ?? "GET") === "PUT" ? handlePut(path, init) : handleGet(path);
80
+ });
81
+ vi.stubGlobal("fetch", fetchMock);
82
+ return { content, fetchMock };
83
+ }
84
+
85
+ type TimelineEditingHandle = {
86
+ handleTimelineElementMove: (
87
+ element: TimelineElement,
88
+ updates: { start: number; track: number },
89
+ ) => Promise<void>;
90
+ };
91
+
92
+ function mountHost(
93
+ clip: TimelineElement,
94
+ iframe: HTMLIFrameElement,
95
+ ): {
96
+ timelineEditing: TimelineEditingHandle;
97
+ undo: () => Promise<void>;
98
+ bannerHost: HTMLDivElement;
99
+ } {
100
+ let timelineEditing: TimelineEditingHandle | null = null;
101
+ let undo: (() => Promise<void>) | null = null;
102
+ let conflict: ExternalFileChangeCoordinatorHandle = {
103
+ blocked: null,
104
+ retry: async () => {},
105
+ useExternalFile: async () => {},
106
+ keepStudioFile: async () => {},
107
+ };
108
+ const bannerHost = document.createElement("div");
109
+ document.body.append(bannerHost);
110
+ const bannerRoot = createRoot(bannerHost);
111
+ // Created once: a fresh storage object on every render would change
112
+ // usePersistentEditHistory's effect deps each time and loop forever.
113
+ const storage = createMemoryEditHistoryStorage();
114
+
115
+ function Harness() {
116
+ const writer = useProjectFileWriter({ projectId: "p1" });
117
+ const editHistory = usePersistentEditHistory({ projectId: "p1", storage });
118
+ const historyActions = useEditHistoryActions({
119
+ editHistory,
120
+ readOptionalProjectFile: writer.readOptionalProjectFile,
121
+ readProjectFile: writer.readProjectFile,
122
+ writeProjectFile: writer.writeProjectFile,
123
+ showToast: () => {},
124
+ syncHistoryPreviewAfterApply: async () => {},
125
+ // The pattern a host's own dom-edit queue would also drain through —
126
+ // this is the one gate that closes the undo/delete race (#4194).
127
+ waitForPendingDomEditSaves: async () => {
128
+ const result = await flushStudioPendingEdits();
129
+ if (result.status !== "clean") throw result.error;
130
+ },
131
+ });
132
+ undo = historyActions.undo;
133
+ const commitRef = useRef(null);
134
+ timelineEditing = useTimelineEditing({
135
+ projectId: "p1",
136
+ activeCompPath: "index.html",
137
+ timelineElements: [clip],
138
+ showToast: () => {},
139
+ writeProjectFile: async (path, content, expectedContent) => {
140
+ try {
141
+ await writer.writeProjectFile(path, content, expectedContent);
142
+ } catch (error) {
143
+ if (error instanceof StudioFileConflictError) {
144
+ conflict = {
145
+ ...conflict,
146
+ blocked: { status: "conflict", generation: 0, error, payload: null },
147
+ };
148
+ bannerRoot.render(createElement(ExternalFileConflictBanner, { coordinator: conflict }));
149
+ }
150
+ throw error;
151
+ }
152
+ },
153
+ observeProjectFileVersion: writer.observeProjectFileVersion,
154
+ recordEdit: editHistory.recordEdit,
155
+ reloadPreview: () => {},
156
+ previewIframeRef: { current: iframe },
157
+ pendingTimelineEditPathRef: { current: new Set<string>() },
158
+ uploadProjectFiles: async () => [],
159
+ handleDomZIndexReorderCommitRef: commitRef,
160
+ });
161
+ return null;
162
+ }
163
+
164
+ root = createRoot(document.createElement("div"));
165
+ act(() => root!.render(createElement(Harness)));
166
+ if (!timelineEditing) throw new Error("Expected hook to mount");
167
+ if (!undo) throw new Error("Expected undo to be exposed");
168
+ return { timelineEditing, undo, bannerHost };
169
+ }
170
+
171
+ describe("public export surface: a host mounting hand editing outside EditorShell", () => {
172
+ it("writes a move through the history path and undo restores it", async () => {
173
+ const disk = stubHostProjectDisk({ "index.html": ORIGINAL_HTML });
174
+ const iframe = document.createElement("iframe");
175
+ document.body.append(iframe);
176
+ iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
177
+ const clip: TimelineElement = {
178
+ id: "clip",
179
+ domId: "clip",
180
+ tag: "div",
181
+ start: 0,
182
+ duration: 2,
183
+ track: 0,
184
+ };
185
+
186
+ const { timelineEditing, undo } = mountHost(clip, iframe);
187
+ await act(async () => {
188
+ await timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 });
189
+ });
190
+ expect(disk.content["index.html"]).toContain('data-start="5"');
191
+
192
+ await act(async () => {
193
+ await undo();
194
+ });
195
+ expect(disk.content["index.html"]).toContain('data-start="0"');
196
+ });
197
+
198
+ it("surfaces the conflict banner on a 409", async () => {
199
+ stubHostProjectDisk({ "index.html": ORIGINAL_HTML }, { editElsewhereAfterFirstRead: true });
200
+ const iframe = document.createElement("iframe");
201
+ document.body.append(iframe);
202
+ iframe.contentDocument!.body.innerHTML = ORIGINAL_HTML;
203
+ const clip: TimelineElement = {
204
+ id: "clip",
205
+ domId: "clip",
206
+ tag: "div",
207
+ start: 0,
208
+ duration: 2,
209
+ track: 0,
210
+ };
211
+
212
+ const { timelineEditing, bannerHost } = mountHost(clip, iframe);
213
+
214
+ await act(async () => {
215
+ await expect(
216
+ timelineEditing.handleTimelineElementMove(clip, { start: 5, track: 0 }),
217
+ ).rejects.toBeInstanceOf(StudioFileConflictError);
218
+ });
219
+
220
+ expect(bannerHost.textContent).toContain("changed outside Studio");
221
+ });
222
+ });
package/src/index.ts CHANGED
@@ -1,5 +1,36 @@
1
+ // UI primitives
2
+ export { Button, buttonBase, buttonVariants } from "./components/ui/Button";
3
+ export type { ButtonSize, ButtonVariant, PreviewState } from "./components/ui/Button";
4
+ export { IconButton } from "./components/ui/IconButton";
5
+ export { Tab, TabPanel, Tabs, TabsList } from "./components/ui/Tabs";
6
+ export { Tooltip } from "./components/ui/Tooltip";
7
+ export { cn } from "./components/ui/cn";
8
+ export {
9
+ ContextMenu,
10
+ Menu,
11
+ MenuItem,
12
+ MenuRadioGroup,
13
+ MenuRadioItem,
14
+ MenuSeparator,
15
+ MenuShortcut,
16
+ popupSurface,
17
+ } from "./components/ui/Menu";
18
+ export type { MenuItemTone, PopupPreviewState } from "./components/ui/Menu";
19
+ export { Popover } from "./components/ui/Popover";
20
+ export { Input, fieldBase, fieldText } from "./components/ui/Input";
21
+ export type { InputProps } from "./components/ui/Input";
22
+ export { NumberField } from "./components/ui/NumberField";
23
+ export type { NumberFieldProps } from "./components/ui/NumberField";
24
+ export { Select } from "./components/ui/Select";
25
+ export type { SelectOption, SelectProps } from "./components/ui/Select";
26
+ export { Slider } from "./components/ui/Slider";
27
+ export type { SliderProps } from "./components/ui/Slider";
28
+ export { Toggle } from "./components/ui/Toggle";
29
+ export type { ToggleProps } from "./components/ui/Toggle";
30
+
1
31
  // NLE Layout
2
32
  export { EditorShell } from "./components/EditorShell";
33
+ export type { EditorShellProps } from "./components/EditorShell";
3
34
  export { NLEPreview } from "./components/nle/NLEPreview";
4
35
  export { CompositionBreadcrumb } from "./components/nle/CompositionBreadcrumb";
5
36
  export type { CompositionLevel } from "./components/nle/CompositionBreadcrumb";
@@ -19,6 +50,10 @@ export {
19
50
  } from "./player";
20
51
  export type { TimelineElement } from "./player";
21
52
 
53
+ // Host overlays: draw over the preview in composition coordinates (see EditorShellProps.gestureOverlay)
54
+ export { usePreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
55
+ export type { PreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
56
+
22
57
  // Editor
23
58
  export { SourceEditor } from "./components/editor/SourceEditor";
24
59
  export { PropertyPanel } from "./components/editor/PropertyPanel";
@@ -37,6 +72,22 @@ export {
37
72
  } from "./components/editor/domEditingAgentPrompt";
38
73
  export type { AgentPromptElementInfo } from "./components/editor/domEditingAgentPrompt";
39
74
 
75
+ // Render queue
76
+ export { RenderQueue } from "./components/renders/RenderQueue";
77
+ export type { RenderQueueProps, CompositionDimensions } from "./components/renders/RenderQueue";
78
+ export { useRenderQueue } from "./components/renders/useRenderQueue";
79
+ export type { FfmpegStatus } from "./components/renders/useFfmpegStatus";
80
+ export type {
81
+ RenderJob,
82
+ ResolutionPreset,
83
+ StartRenderOptions,
84
+ } from "./components/renders/useRenderQueue";
85
+ export {
86
+ getPersistedRenderSettings,
87
+ persistRenderSettings,
88
+ } from "./components/renders/renderSettings";
89
+ export type { PersistedRenderSettings } from "./components/renders/renderSettings";
90
+
40
91
  // Hooks
41
92
  export { useElementPicker } from "./hooks/useElementPicker";
42
93
  export type { PickedElement } from "./hooks/useElementPicker";
@@ -45,3 +96,39 @@ export type { PickedElement } from "./hooks/useElementPicker";
45
96
  export { resolveSourceFile, applyPatch } from "./utils/sourcePatcher";
46
97
  export type { PatchOperation } from "./utils/sourcePatcher";
47
98
  export { parseStyleString, mergeStyleIntoTag, findElementBlock } from "./utils/htmlEditor";
99
+
100
+ // Timeline editing: Studio's own hand-edit path, undo/redo, the
101
+ // etag-guarded writer and the conflict banner, for a host mounting the
102
+ // timeline outside EditorShell.
103
+ export { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
104
+ export type { UsePersistentEditHistoryOptions } from "./hooks/usePersistentEditHistory";
105
+ export { useTimelineEditing } from "./hooks/useTimelineEditing";
106
+ export type { UseTimelineEditingOptions } from "./hooks/useTimelineEditingTypes";
107
+ // A host's own waitForPendingDomEditSaves must also call this, or undo/redo
108
+ // can race a write still in flight (see useTrackPendingTimelineEdit.ts).
109
+ export { flushStudioPendingEdits } from "./utils/studioPendingEdits";
110
+ export type { StudioPendingEditsDrainResult } from "./utils/studioPendingEdits";
111
+ export type {
112
+ CanEditTimelineElement,
113
+ TimelineEditPermission,
114
+ } from "./hooks/timelineEditPermission";
115
+ export { useEditHistoryActions } from "./hooks/useEditHistoryActions";
116
+ export type {
117
+ EditHistoryHandle,
118
+ UseEditHistoryActionsOptions,
119
+ } from "./hooks/useEditHistoryActions";
120
+ export { useProjectFileWriter } from "./hooks/useProjectFileWriter";
121
+ export type { UseProjectFileWriterOptions } from "./hooks/useProjectFileWriter";
122
+ // A host's writeProjectFile throws this on a 409; catch it to know when
123
+ // to show ExternalFileConflictBanner.
124
+ export { StudioFileConflictError } from "./utils/studioSaveDiagnostics";
125
+ export { ExternalFileConflictBanner } from "./components/ExternalFileConflictBanner";
126
+ export type {
127
+ ExternalFileChangeCoordinatorHandle,
128
+ ExternalFileChangeBlockedState,
129
+ } from "./hooks/useExternalFileChangeCoordinator";
130
+ export { TimelinePane } from "./components/nle/TimelinePane";
131
+ export type { TimelinePaneProps } from "./components/nle/TimelinePane";
132
+ export { TimelineEditProvider } from "./contexts/TimelineEditContext";
133
+ export type { TimelineEditCallbacks } from "./player/components/timelineCallbacks";
134
+ export type { BlockedTimelineEditIntent } from "./player/components/timelineEditing";
package/src/main.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
+ import { IconContext } from "@phosphor-icons/react";
3
4
  import { StudioApp } from "./App";
4
5
  import { StudioErrorBoundary } from "./components/StudioErrorBoundary";
6
+ import { readIconTokens } from "./styles/iconTokens";
5
7
  import { trackStudioEvent } from "./utils/studioTelemetry";
6
8
  import "./styles/studio.css";
7
9
 
@@ -93,8 +95,12 @@ window.addEventListener("unhandledrejection", (event) => {
93
95
 
94
96
  createRoot(document.getElementById("root")!).render(
95
97
  <StrictMode>
96
- <StudioErrorBoundary>
97
- <StudioApp />
98
- </StudioErrorBoundary>
98
+ {/* One icon size and weight for the whole app, taken from the theme file.
99
+ Icons that pass their own size or weight still win. */}
100
+ <IconContext.Provider value={readIconTokens()}>
101
+ <StudioErrorBoundary>
102
+ <StudioApp />
103
+ </StudioErrorBoundary>
104
+ </IconContext.Provider>
99
105
  </StrictMode>,
100
106
  );
@@ -191,7 +191,7 @@ export const AudioWaveform = memo(function AudioWaveform({
191
191
  "repeating-linear-gradient(90deg, rgba(75,163,210,0.35) 0 2px, transparent 2px 5px)",
192
192
  }}
193
193
  />
194
- <span className="relative rounded bg-black/50 px-1 text-[8px] text-neutral-500">
194
+ <span className="relative rounded-sm bg-black/50 px-1 text-[8px] text-neutral-500">
195
195
  waveform unavailable
196
196
  </span>
197
197
  </div>
@@ -29,7 +29,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
29
29
  }: AutomationSelectionMenuProps) {
30
30
  const menuRef = useContextMenuDismiss(onClose);
31
31
  const row =
32
- "block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-bg-3 disabled:opacity-40";
32
+ "block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-hover disabled:opacity-40";
33
33
  // Same edge-clamping precedent as TrackGapContextMenu: without it a
34
34
  // right-click near the bottom/right of the timeline renders this menu
35
35
  // partially off-screen.
@@ -41,11 +41,11 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
41
41
  return createPortal(
42
42
  <div
43
43
  ref={menuRef}
44
- // z-[200] for the same reason the timeline's FX popover uses it: this is
44
+ // z-200 for the same reason the timeline's FX popover uses it: this is
45
45
  // portaled to `document.body`, but the ruler's sticky header sits at z-70
46
46
  // in the SAME root stacking context, so a z-50 menu opened near the top of
47
47
  // the timeline is painted through by the ruler and the playhead.
48
- className="hf-automation-menu fixed z-[200] min-w-[140px] rounded border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
48
+ className="hf-automation-menu fixed z-200 min-w-[140px] rounded-sm border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
49
49
  style={{ left: adjustedX, top: adjustedY }}
50
50
  >
51
51
  {AUTOMATION_SHAPES.map((shape) => (
@@ -78,7 +78,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
78
78
  ref={menuRef}
79
79
  role="menu"
80
80
  aria-label="Clip actions"
81
- className="fixed z-[200] bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
81
+ className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
82
82
  style={{ left: adjustedX, top: adjustedY }}
83
83
  >
84
84
  {splitLabel && (
@@ -86,7 +86,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
86
86
  <button
87
87
  type="button"
88
88
  role="menuitem"
89
- className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-none focus-visible:bg-neutral-800 ${
89
+ className={`w-full flex items-center justify-between px-3 py-1.5 text-xs text-left outline-hidden focus-visible:bg-neutral-800 ${
90
90
  canSplit
91
91
  ? "text-neutral-300 hover:bg-neutral-800 cursor-pointer"
92
92
  : "text-neutral-600 cursor-not-allowed"
@@ -159,7 +159,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
159
159
  <button
160
160
  type="button"
161
161
  role="menuitem"
162
- className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-none cursor-pointer text-left"
162
+ className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
163
163
  onClick={() => {
164
164
  onDelete(element);
165
165
  onClose();
@@ -87,9 +87,6 @@ describe("buildCompositionThumbnailUrl", () => {
87
87
 
88
88
  expect(buildCompositionThumbnailUrl(base)).not.toContain("output=");
89
89
  expect(buildCompositionThumbnailUrl({ ...base, output: "source" })).toContain("output=source");
90
- expect(buildCompositionThumbnailUrl({ ...base, output: "storyboard" })).toContain(
91
- "output=storyboard",
92
- );
93
90
  });
94
91
 
95
92
  it("includes the persisted content revision in the cache identity", () => {
@@ -44,10 +44,9 @@ export function buildCompositionThumbnailUrl({
44
44
  /**
45
45
  * Capture density. Omitted, the route bounds the image to its preview cap —
46
46
  * right for the timeline, where thumbnails are small and numerous and their
47
- * decoded bytes are budgeted. `"storyboard"` caps the longest side at a
48
- * high-density review size; `"source"` uses the composition's own dimensions.
47
+ * decoded bytes are budgeted. `"source"` uses the composition's own dimensions.
49
48
  */
50
- output?: "source" | "storyboard";
49
+ output?: "source";
51
50
  contentRevision?: number;
52
51
  }): string {
53
52
  const thumbnailBase = previewUrl
@@ -159,7 +158,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
159
158
  {Array.from({ length: frameCount }, (_, index) => (
160
159
  <div
161
160
  key={index}
162
- className="relative h-full flex-shrink-0 overflow-hidden"
161
+ className="relative h-full shrink-0 overflow-hidden"
163
162
  style={{ width: frameW }}
164
163
  >
165
164
  <img
@@ -168,7 +168,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
168
168
  }}
169
169
  placeholder="What should change?"
170
170
  rows={2}
171
- className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-none focus:border-studio-accent/40 transition-colors"
171
+ className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-hidden focus:border-studio-accent/40 transition-colors"
172
172
  />
173
173
  </div>
174
174
 
@@ -73,7 +73,7 @@ export const ImageThumbnail = memo(function ImageThumbnail({
73
73
  {Array.from({ length: frameCount }, (_, index) => (
74
74
  <div
75
75
  key={index}
76
- className="relative h-full flex-shrink-0 overflow-hidden bg-neutral-900"
76
+ className="relative h-full shrink-0 overflow-hidden bg-neutral-900"
77
77
  style={{ width: frameW }}
78
78
  >
79
79
  <img
@@ -40,9 +40,9 @@ interface KeyframeDiamondContextMenuProps {
40
40
  }
41
41
 
42
42
  const ITEM_CLS =
43
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-none cursor-pointer text-left";
43
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
44
44
  const DESTRUCTIVE_ITEM_CLS =
45
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-none cursor-pointer text-left";
45
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
46
46
 
47
47
  export function KeyframeDiamondContextMenu({
48
48
  state,
@@ -96,7 +96,7 @@ export function KeyframeDiamondContextMenu({
96
96
  ref={menuRef}
97
97
  role="menu"
98
98
  aria-label="Keyframe actions"
99
- className="fixed z-[200] bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px] overflow-y-auto"
99
+ className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px] overflow-y-auto"
100
100
  style={{ left: adjustedX, top: adjustedY, maxHeight: `calc(100vh - ${adjustedY + 8}px)` }}
101
101
  >
102
102
  {onMoveToPlayhead && (
@@ -172,7 +172,7 @@ export function KeyframeDiamondContextMenu({
172
172
  <button
173
173
  type="button"
174
174
  role="menuitem"
175
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-none cursor-pointer text-left"
175
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-950/40 focus-visible:bg-red-950/40 outline-hidden cursor-pointer text-left"
176
176
  onClick={() => {
177
177
  onDeleteAll(state.element, state.animationId);
178
178
  onClose();
@@ -37,7 +37,7 @@ export function LaneToggleButton({
37
37
  title={`${isExpanded ? "Hide" : "Show"} lanes`}
38
38
  // h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
39
39
  // only the hit box grows.
40
- className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
40
+ className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
41
41
  isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
42
42
  }`}
43
43
  onPointerDown={(event) => event.stopPropagation()}
@@ -97,7 +97,7 @@ export function LayerDisclosureRow({
97
97
  </span>
98
98
  {/* Wraps rather than truncating: a truncated name needs a hover to be
99
99
  read, which a scanned column cannot rely on. */}
100
- <span className="min-w-0 flex-1 break-words font-medium leading-tight">{name}</span>
100
+ <span className="min-w-0 flex-1 wrap-break-word font-medium leading-tight">{name}</span>
101
101
  <TrackClipCount clipCount={clipCount} />
102
102
  {children}
103
103
  {/* Anchored right, on every header that has one: the lane toggle is the
@@ -116,7 +116,7 @@ const FullscreenButton = memo(function FullscreenButton({
116
116
  trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
117
117
  onToggleFullscreen();
118
118
  }}
119
- className={`flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md transition-colors ${
119
+ className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
120
120
  isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
121
121
  }`}
122
122
  aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
@@ -153,7 +153,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
153
153
  );
154
154
 
155
155
  return (
156
- <div ref={shortcutsPanelRef} className="relative flex-shrink-0">
156
+ <div ref={shortcutsPanelRef} className="relative shrink-0">
157
157
  <Tooltip label="Shortcuts and tools">
158
158
  <button
159
159
  ref={triggerRef}
@@ -192,7 +192,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
192
192
  // Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
193
193
  // not trapped and the rest of the editor stays operable, so claiming
194
194
  // modality would make assistive tech treat the whole app as inert.
195
- className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-none"
195
+ className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden"
196
196
  style={{
197
197
  background: "#161618",
198
198
  border: "1px solid rgba(255,255,255,0.08)",
@@ -212,7 +212,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
212
212
  pattern="[0-9]*"
213
213
  aria-label="Jump to frame"
214
214
  placeholder="frame number"
215
- className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
215
+ className="h-6 flex-1 rounded-sm border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-hidden transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
216
216
  onKeyDown={handleJumpKeyDown}
217
217
  onBlur={commitJumpFrame}
218
218
  />
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
220
220
  <button
221
221
  type="submit"
222
222
  disabled={disabled}
223
- className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
223
+ className="h-6 px-2 rounded-sm border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
224
224
  >
225
225
  Go
226
226
  </button>
@@ -236,7 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
236
236
  <div className="flex items-center justify-between gap-2">
237
237
  <div className="flex items-center gap-2">
238
238
  <span
239
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
239
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
240
240
  style={{ background: "rgba(255,255,255,0.05)" }}
241
241
  >
242
242
  I
@@ -253,7 +253,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
253
253
  <button
254
254
  type="button"
255
255
  onClick={() => setInPoint(null)}
256
- className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
256
+ className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
257
257
  aria-label="Clear in-point"
258
258
  >
259
259
  <svg
@@ -277,7 +277,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
277
277
  <div className="flex items-center justify-between gap-2">
278
278
  <div className="flex items-center gap-2">
279
279
  <span
280
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
280
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
281
281
  style={{ background: "rgba(255,255,255,0.05)" }}
282
282
  >
283
283
  O
@@ -294,7 +294,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
294
294
  <button
295
295
  type="button"
296
296
  onClick={() => setOutPoint(null)}
297
- className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
297
+ className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
298
298
  aria-label="Clear out-point"
299
299
  >
300
300
  <svg
@@ -328,7 +328,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
328
328
  {section.hints.map((hint) => (
329
329
  <div key={hint.key} className="flex items-center gap-3">
330
330
  <span
331
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
331
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
332
332
  style={{ background: "rgba(255,255,255,0.05)" }}
333
333
  >
334
334
  {hint.key}
@@ -23,7 +23,7 @@ export const SpeedMenu = memo(function SpeedMenu({
23
23
  const speedMenuContainerRef = useContextMenuDismiss(closeMenu);
24
24
 
25
25
  return (
26
- <div ref={speedMenuContainerRef} className="relative flex-shrink-0">
26
+ <div ref={speedMenuContainerRef} className="relative shrink-0">
27
27
  <Tooltip label="Playback speed">
28
28
  <button
29
29
  type="button"
@@ -57,10 +57,8 @@ export const SpeedMenu = memo(function SpeedMenu({
57
57
  setPlaybackRate(rate);
58
58
  setShowSpeedMenu(false);
59
59
  }}
60
- className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-none focus-visible:bg-white/[0.04] ${
61
- isCurrent
62
- ? "text-neutral-50 bg-white/[0.06]"
63
- : "text-neutral-500 hover:bg-white/[0.04]"
60
+ className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-hidden focus-visible:bg-white/4 ${
61
+ isCurrent ? "text-neutral-50 bg-white/6" : "text-neutral-500 hover:bg-white/4"
64
62
  }`}
65
63
  >
66
64
  {rate}x