@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,151 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * The strip's keyboard behaviour, which tab reads as selected, and how the hotkey
5
+ * filters classify the elements (KTD13). The arrow-key tests are new behaviour, not a port.
6
+ */
7
+ import React, { act } from "react";
8
+ import { createRoot, type Root } from "react-dom/client";
9
+ import { afterEach, expect, it, vi } from "vitest";
10
+ import { RightPanelTabs, type RightPanelTabDescriptor } from "./RightPanelTabs";
11
+ import { isTypingTarget } from "../utils/typingTarget";
12
+ import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
13
+
14
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ const IDS = ["design", "layers", "renders", "variables"];
17
+
18
+ let mounted: { root: Root; host: HTMLElement } | null = null;
19
+
20
+ afterEach(() => {
21
+ if (!mounted) return;
22
+ const { root, host } = mounted;
23
+ mounted = null;
24
+ act(() => root.unmount());
25
+ host.remove();
26
+ });
27
+
28
+ /** The harness owns the active state too, so a selection moves as it does in the real panel. */
29
+ function mount(
30
+ initialActive: string[],
31
+ activateOnFocus = true,
32
+ ): {
33
+ host: HTMLElement;
34
+ selections: ReturnType<typeof vi.fn>;
35
+ } {
36
+ const selections = vi.fn();
37
+
38
+ function Harness() {
39
+ const [active, setActive] = React.useState(initialActive);
40
+ const tabs: RightPanelTabDescriptor[] = IDS.map((id) => ({
41
+ id,
42
+ label: id,
43
+ tooltip: `${id} tooltip`,
44
+ active: active.includes(id),
45
+ onSelect: () => {
46
+ selections(id);
47
+ setActive([id]);
48
+ },
49
+ }));
50
+ return <RightPanelTabs tabs={tabs} activateOnFocus={activateOnFocus} />;
51
+ }
52
+
53
+ const host = document.createElement("div");
54
+ document.body.append(host);
55
+ const root = createRoot(host);
56
+ mounted = { root, host };
57
+ act(() => root.render(<Harness />));
58
+ return { host, selections };
59
+ }
60
+
61
+ function tab(host: HTMLElement, id: string): HTMLElement {
62
+ const el = host.querySelector<HTMLElement>(`[data-tab-id="${id}"]`);
63
+ if (!el) throw new Error(`no tab for ${id}`);
64
+ return el;
65
+ }
66
+
67
+ function selected(host: HTMLElement): string | null {
68
+ return (
69
+ host.querySelector('[role="tab"][aria-selected="true"]')?.getAttribute("data-tab-id") ?? null
70
+ );
71
+ }
72
+
73
+ /** Base UI moves the roving tabindex synchronously and the focus one task later. */
74
+ async function arrow(key: string): Promise<void> {
75
+ const target = document.activeElement ?? document.body;
76
+ act(() => {
77
+ target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, composed: true }));
78
+ });
79
+ await act(async () => {
80
+ await new Promise((resolve) => setTimeout(resolve, 0));
81
+ });
82
+ }
83
+
84
+ it("moves from Design to Layers with ArrowRight and asks the panel to switch", async () => {
85
+ const { host, selections } = mount(["design"]);
86
+ act(() => tab(host, "design").focus());
87
+
88
+ await arrow("ArrowRight");
89
+
90
+ expect(document.activeElement).toBe(tab(host, "layers"));
91
+ expect(selections).toHaveBeenCalledWith("layers");
92
+ });
93
+
94
+ it("only moves focus on an arrow key when selection would toggle a pane", async () => {
95
+ const { host, selections } = mount(["design"], false);
96
+ act(() => tab(host, "design").focus());
97
+
98
+ await arrow("ArrowRight");
99
+
100
+ expect(document.activeElement).toBe(tab(host, "layers"));
101
+ expect(selections).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it("jumps to the first and last tab with Home and End", async () => {
105
+ const { host, selections } = mount(["design"]);
106
+ act(() => tab(host, "design").focus());
107
+
108
+ await arrow("End");
109
+ expect(document.activeElement).toBe(tab(host, "variables"));
110
+ expect(selections).toHaveBeenLastCalledWith("variables");
111
+
112
+ await arrow("Home");
113
+ expect(document.activeElement).toBe(tab(host, "design"));
114
+ expect(selections).toHaveBeenLastCalledWith("design");
115
+ });
116
+
117
+ it("selects the tab whose content is on screen", () => {
118
+ const { host } = mount(["renders"]);
119
+
120
+ expect(selected(host)).toBe("renders");
121
+ });
122
+
123
+ it("leaves every tab unselected when the panel shows something the strip has no tab for", () => {
124
+ // Block params take over the panel body without a tab of their own. The old
125
+ // buttons all read unpressed in that state; nothing should read selected now.
126
+ const { host } = mount([]);
127
+
128
+ expect(selected(host)).toBe(null);
129
+ });
130
+
131
+ it("keeps a second open pane looking open in the legacy split inspector", () => {
132
+ // Design and Layers render together there. Only one tab can carry
133
+ // aria-selected, so the other has to keep the selected look or the strip
134
+ // would claim a pane is closed while it is on screen.
135
+ const { host } = mount(["design", "layers"]);
136
+
137
+ expect(selected(host)).toBe("design");
138
+ expect(tab(host, "layers").className).toContain("bg-hover");
139
+ });
140
+
141
+ it("classifies its tabs for the hotkey filters exactly as the old buttons did (KTD13)", () => {
142
+ // The old strip rendered plain <button>s: not a typing target, and claimed by
143
+ // the playback filter through its `button` selector. Base UI's tab is a
144
+ // <button> too, so both verdicts have to be unchanged.
145
+ const { host } = mount(["design"]);
146
+ const el = tab(host, "design");
147
+
148
+ expect(el.tagName).toBe("BUTTON");
149
+ expect(isTypingTarget(el)).toBe(false);
150
+ expect(shouldIgnorePlaybackShortcutTarget(el)).toBe(true);
151
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The inspector tab strip on the shared Tabs primitive. The selected tab is derived from the
3
+ * panel's state; in the legacy split inspector two can be open, only one holds `aria-selected`.
4
+ */
5
+
6
+ import { Tab, Tabs, TabsList, Tooltip, cn } from "./ui";
7
+
8
+ export interface RightPanelTabDescriptor {
9
+ /** Stable id, also the `data-tab-id` attribute the strip is queried by. */
10
+ id: string;
11
+ label: string;
12
+ tooltip: string;
13
+ /** Whether this tab's content is on screen right now. */
14
+ active: boolean;
15
+ /** Run when the strip moves to this tab, by click or by arrow key. */
16
+ onSelect: () => void;
17
+ }
18
+
19
+ /** The look `aria-selected` gives the selected tab, for a second open pane. */
20
+ const SELECTED_LOOK = "bg-hover text-text-0";
21
+
22
+ export function RightPanelTabs({
23
+ tabs,
24
+ activateOnFocus = true,
25
+ }: {
26
+ tabs: readonly RightPanelTabDescriptor[];
27
+ /** False when `onSelect` toggles a pane, so an arrow key only moves focus. */
28
+ activateOnFocus?: boolean;
29
+ }) {
30
+ // `null` when the layout holds a tab this strip does not show (block params),
31
+ // which leaves every tab unselected, exactly as the old buttons did.
32
+ const value = tabs.find((tab) => tab.active)?.id ?? null;
33
+
34
+ return (
35
+ <Tabs
36
+ value={value}
37
+ onValueChange={(next) => {
38
+ tabs.find((tab) => tab.id === next)?.onSelect();
39
+ }}
40
+ >
41
+ <TabsList
42
+ aria-label="Inspector panels"
43
+ activateOnFocus={activateOnFocus}
44
+ className="flex min-w-0 items-center gap-1 overflow-hidden rounded-none border-b border-border-strong bg-transparent px-3 py-2"
45
+ >
46
+ {tabs.map((tab) => (
47
+ <Tooltip key={tab.id} label={tab.tooltip} side="bottom">
48
+ <Tab
49
+ value={tab.id}
50
+ className={cn("h-ctl-lg rounded-lg px-3 font-medium", tab.active && SELECTED_LOOK)}
51
+ >
52
+ {tab.label}
53
+ </Tab>
54
+ </Tooltip>
55
+ ))}
56
+ </TabsList>
57
+ </Tabs>
58
+ );
59
+ }
@@ -8,14 +8,14 @@ interface SaveQueuePausedBannerProps {
8
8
  export function SaveQueuePausedBanner({ message, onRetry }: SaveQueuePausedBannerProps) {
9
9
  return (
10
10
  <div
11
- className="hf-backdrop-in absolute left-1/2 top-14 z-[92] flex max-w-[calc(100vw-32px)] -translate-x-1/2 items-center gap-3 rounded-md border border-red-500/30 bg-red-950/85 px-4 py-2 text-[12px] font-medium text-red-100 shadow-lg shadow-black/30"
11
+ className="hf-backdrop-in absolute left-1/2 top-14 z-92 flex max-w-[calc(100vw-32px)] -translate-x-1/2 items-center gap-3 rounded-md border border-red-500/30 bg-red-950/85 px-4 py-2 text-[12px] font-medium text-red-100 shadow-lg shadow-black/30"
12
12
  role="alert"
13
13
  >
14
14
  <span>{message}</span>
15
15
  <button
16
16
  type="button"
17
17
  onClick={onRetry}
18
- className="rounded border border-red-300/20 px-2 py-1 text-[11px] text-red-100 transition-colors hover:bg-red-400/10 active:scale-[0.98]"
18
+ className="rounded-sm border border-red-300/20 px-2 py-1 text-[11px] text-red-100 transition-colors hover:bg-red-400/10 active:scale-[0.98]"
19
19
  >
20
20
  Retry saving
21
21
  </button>
@@ -0,0 +1,181 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * The header on the shared primitives: Capture and Inspector grouped, Export separate,
5
+ * hotkey filters unchanged (KTD13). Contexts are mocked, not provided.
6
+ */
7
+ import React, { act } from "react";
8
+ import { createRoot, type Root } from "react-dom/client";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import { buttonSizes, buttonVariants } from "./ui";
11
+ import { isTypingTarget } from "../utils/typingTarget";
12
+ import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
13
+
14
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ const editHistory = {
17
+ canUndo: false,
18
+ canRedo: false,
19
+ undoLabel: undefined as string | undefined,
20
+ redoLabel: undefined as string | undefined,
21
+ };
22
+ const renderQueue = { isRendering: false, ffmpegMissing: false };
23
+
24
+ vi.mock("../contexts/StudioContext", () => ({
25
+ useStudioShellContext: () => ({
26
+ projectId: "demo",
27
+ editHistory,
28
+ handleUndo: vi.fn(),
29
+ handleRedo: vi.fn(),
30
+ renderQueue,
31
+ }),
32
+ }));
33
+
34
+ vi.mock("../contexts/PanelLayoutContext", () => ({
35
+ usePanelLayoutContext: () => ({
36
+ rightCollapsed: false,
37
+ setRightCollapsed: vi.fn(),
38
+ setRightPanelTab: vi.fn(),
39
+ }),
40
+ }));
41
+
42
+ vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
43
+
44
+ const { StudioHeader } = await import("./StudioHeader");
45
+
46
+ let mounted: { root: Root; host: HTMLElement } | null = null;
47
+
48
+ beforeEach(() => {
49
+ editHistory.canUndo = false;
50
+ editHistory.canRedo = false;
51
+ editHistory.undoLabel = undefined;
52
+ editHistory.redoLabel = undefined;
53
+ renderQueue.isRendering = false;
54
+ });
55
+
56
+ afterEach(() => {
57
+ if (!mounted) return;
58
+ const { root, host } = mounted;
59
+ mounted = null;
60
+ act(() => root.unmount());
61
+ host.remove();
62
+ });
63
+
64
+ function mount(props: { inspectorButtonActive?: boolean } = {}): HTMLElement {
65
+ const host = document.createElement("div");
66
+ document.body.append(host);
67
+ const root = createRoot(host);
68
+ mounted = { root, host };
69
+ act(() =>
70
+ root.render(
71
+ <StudioHeader
72
+ captureFrameHref="blob:frame"
73
+ captureFrameFilename="frame.png"
74
+ handleCaptureFrameClick={vi.fn()}
75
+ refreshCaptureFrameTime={vi.fn()}
76
+ inspectorButtonActive={props.inspectorButtonActive ?? false}
77
+ inspectorPanelActive={false}
78
+ />,
79
+ ),
80
+ );
81
+ return host;
82
+ }
83
+
84
+ function query(host: HTMLElement, selector: string): HTMLElement {
85
+ const el = host.querySelector<HTMLElement>(selector);
86
+ if (!el) throw new Error(`not rendered: ${selector}`);
87
+ return el;
88
+ }
89
+
90
+ /** Every class the recipe asks for, on the element the header rendered. */
91
+ function expectRecipe(el: HTMLElement, ...recipes: string[]): void {
92
+ const applied = new Set(el.className.split(/\s+/));
93
+ for (const recipe of recipes) {
94
+ for (const token of recipe.split(/\s+/)) {
95
+ expect(applied, `${token} missing from: ${el.className}`).toContain(token);
96
+ }
97
+ }
98
+ }
99
+
100
+ it("renders Export as the shared primary Button at the medium size", () => {
101
+ const host = mount();
102
+
103
+ expectRecipe(
104
+ query(host, '[data-testid="header-export"]'),
105
+ buttonVariants.primary,
106
+ buttonSizes.md,
107
+ );
108
+ });
109
+
110
+ it("no longer renders Undo or Redo in the header", () => {
111
+ const host = mount();
112
+
113
+ expect(host.querySelector('[aria-label="Undo"]')).toBeNull();
114
+ expect(host.querySelector('[aria-label="Redo"]')).toBeNull();
115
+ });
116
+
117
+ it("groups Capture and Inspector in one bordered segment, Export outside it", () => {
118
+ const host = mount();
119
+ const group = query(host, '[aria-label="Capture current frame"]').closest(".divide-x");
120
+
121
+ expect(group?.contains(query(host, '[aria-label="Inspector"]'))).toBe(true);
122
+ expect(group?.contains(query(host, '[data-testid="header-export"]'))).toBe(false);
123
+ expect(group).not.toBeNull();
124
+ });
125
+
126
+ it("drops the Capture label below 1000px but keeps its accessible name", () => {
127
+ const host = mount();
128
+ const capture = query(host, '[aria-label="Capture current frame"]');
129
+
130
+ expect(query(capture, "span").className).toContain("max-[1000px]:hidden");
131
+ });
132
+
133
+ /** The bare token, not a `hover:`/`data-[…]:`-prefixed variant of it. */
134
+ function hasToken(className: string, token: string): boolean {
135
+ return className.split(/\s+/).includes(token);
136
+ }
137
+
138
+ it("shows Inspector pressed and filled only when on", () => {
139
+ const host = mount({ inspectorButtonActive: true });
140
+ const on = query(host, '[aria-label="Inspector"]');
141
+ expect(on.getAttribute("aria-pressed")).toBe("true");
142
+ expect(hasToken(on.className, "text-accent")).toBe(true);
143
+ expect(hasToken(on.className, "bg-hover")).toBe(true);
144
+ act(() => mounted?.root.unmount());
145
+ mounted?.host.remove();
146
+ mounted = null;
147
+
148
+ const off = query(mount(), '[aria-label="Inspector"]');
149
+ expect(off.getAttribute("aria-pressed")).toBe("false");
150
+ expect(hasToken(off.className, "text-accent")).toBe(false);
151
+ expect(hasToken(off.className, "bg-hover")).toBe(false);
152
+ });
153
+
154
+ it("keeps Capture a real download link rather than a button", () => {
155
+ // `download` is what saves the frame. A Button here would render a <button>
156
+ // and the control would quietly stop downloading anything.
157
+ const host = mount();
158
+ const capture = query(host, '[aria-label="Capture current frame"]');
159
+
160
+ expect(capture.tagName).toBe("A");
161
+ expect(capture.getAttribute("download")).toBe("frame.png");
162
+ // h-full replaces the md height: the group's own h-ctl sets the shared control height.
163
+ expectRecipe(capture, "px-3", "text-step-12");
164
+ });
165
+
166
+ it("classifies the new header controls for the hotkey filters as the old ones were (KTD13)", () => {
167
+ // Every one of these was a <button> or an <a href> before the sweep: never a
168
+ // typing target, always claimed by the playback filter. A primitive that
169
+ // rendered a different element would leak or swallow hotkeys in silence.
170
+ const host = mount();
171
+ const controls = [
172
+ query(host, '[data-testid="header-export"]'),
173
+ query(host, '[aria-label="Inspector"]'),
174
+ query(host, '[aria-label="Capture current frame"]'),
175
+ ];
176
+
177
+ for (const el of controls) {
178
+ expect(isTypingTarget(el), el.getAttribute("aria-label") ?? el.tagName).toBe(false);
179
+ expect(shouldIgnorePlaybackShortcutTarget(el), el.tagName).toBe(true);
180
+ }
181
+ });
@@ -13,16 +13,4 @@ describe("shouldOpenInspector", () => {
13
13
  it("closes when the inspector is genuinely on screen", () => {
14
14
  expect(shouldOpenInspector(false, true)).toBe(false);
15
15
  });
16
-
17
- it("opens when the window railed the panel away", () => {
18
- // The regression this guards: the button used to branch on the raw
19
- // rightCollapsed intent, which is still `false` while the window has the
20
- // panel railed. That took the close branch, wrote rightCollapsed=true, and
21
- // since that value is synced into the shareable Studio URL, a click that
22
- // did nothing visible rewrote the link.
23
- const userIntentIsOpen = false;
24
- const windowRailedItAway = true;
25
- expect(shouldOpenInspector(windowRailedItAway, true)).toBe(true);
26
- expect(shouldOpenInspector(userIntentIsOpen, true)).toBe(false);
27
- });
28
16
  });