@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
@@ -6,6 +6,14 @@ import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { CompositionsTab } from "./sidebar/CompositionsTab";
7
7
  import { TimelineToolbar } from "./TimelineToolbar";
8
8
 
9
+ vi.mock("../contexts/StudioContext", () => ({
10
+ useStudioShellContext: () => ({
11
+ editHistory: { canUndo: false, canRedo: false },
12
+ handleUndo: vi.fn(),
13
+ handleRedo: vi.fn(),
14
+ }),
15
+ }));
16
+
9
17
  // WCAG 2.2 (2.5.8) requires a 24x24 CSS pixel pointer target. happy-dom has no
10
18
  // CSS engine, so getBoundingClientRect() is 0x0 for everything here and the size
11
19
  // itself cannot be measured — these assert the utility classes that PRODUCE the
@@ -8,7 +8,7 @@ const CUE_MS = 1600;
8
8
  // Matches the focus treatment every other button in this panel uses. A control
9
9
  // the keyboard can reach but not see focus on is unusable without a mouse.
10
10
  const FOCUS_RING =
11
- "outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent";
11
+ "outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent";
12
12
 
13
13
  /**
14
14
  * Shown above Export when the dev server reports no usable FFmpeg.
@@ -81,7 +81,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
81
81
 
82
82
  {status.command ? (
83
83
  <div className="flex items-center gap-1.5">
84
- <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-sm bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
84
+ <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
85
85
  {status.command}
86
86
  </code>
87
87
  {/* Fixed width so swapping the label to "Copied" cannot shift the
@@ -89,7 +89,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
89
89
  <button
90
90
  type="button"
91
91
  onClick={() => void copy(status.command ?? "")}
92
- className={`h-6 w-14 flex-shrink-0 rounded-sm border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
92
+ className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
93
93
  >
94
94
  {copied ? "Copied" : "Copy"}
95
95
  </button>
@@ -107,7 +107,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
107
107
  type="button"
108
108
  onClick={onRecheck}
109
109
  disabled={checking}
110
- className={`rounded-sm py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
110
+ className={`rounded-xs py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
111
111
  >
112
112
  {checking ? "Checking…" : "Recheck"}
113
113
  </button>
@@ -115,7 +115,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
115
115
  href={DOWNLOAD_URL}
116
116
  target="_blank"
117
117
  rel="noreferrer"
118
- className={`rounded-sm py-0.5 text-[10px] text-panel-text-2 underline-offset-2 transition-colors hover:text-panel-text-0 hover:underline ${FOCUS_RING}`}
118
+ className={`rounded-xs py-0.5 text-[10px] text-panel-text-2 underline-offset-2 transition-colors hover:text-panel-text-0 hover:underline ${FOCUS_RING}`}
119
119
  >
120
120
  Other install options
121
121
  </a>
@@ -81,6 +81,13 @@ function exportButtonIn(host: HTMLElement): HTMLButtonElement {
81
81
  return button;
82
82
  }
83
83
 
84
+ describe("RenderQueue Export button", () => {
85
+ it("leaves the font size to the shared Button", () => {
86
+ const host = mountRenderQueue(vi.fn());
87
+ expect(exportButtonIn(host).className).not.toMatch(/text-\[/);
88
+ });
89
+ });
90
+
84
91
  describe("RenderQueue FFmpeg gate", () => {
85
92
  it("refuses Export and shows the install command when the server reports no FFmpeg", () => {
86
93
  ffmpegStatus = {
@@ -22,7 +22,7 @@ type StartRenderHandler = (
22
22
  fps: 24 | 30 | 60,
23
23
  ) => void | Promise<void>;
24
24
 
25
- interface RenderQueueProps {
25
+ export interface RenderQueueProps {
26
26
  jobs: RenderJob[];
27
27
  projectId: string;
28
28
  onDelete: (jobId: string) => void;
@@ -207,7 +207,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
207
207
  onFocus={show}
208
208
  onBlur={hide}
209
209
  onClick={() => setOpen((prev) => !prev)}
210
- className="flex items-center justify-center p-0.5 -m-0.5 rounded text-panel-text-5 hover:text-panel-text-3 transition-colors cursor-help outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent"
210
+ className="flex items-center justify-center p-0.5 -m-0.5 rounded-sm text-panel-text-5 hover:text-panel-text-3 transition-colors cursor-help outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent"
211
211
  >
212
212
  <svg
213
213
  width="12"
@@ -232,7 +232,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
232
232
  role="tooltip"
233
233
  onPointerEnter={show}
234
234
  onPointerLeave={hide}
235
- className="fixed w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-[200]"
235
+ className="fixed w-52 p-2 rounded-sm bg-panel-input border border-neutral-700 shadow-lg z-200"
236
236
  style={{ left: position?.left ?? -9999, top: position?.top ?? -9999 }}
237
237
  >
238
238
  <p className="text-[10px] font-semibold text-panel-text-1 mb-0.5">{info.label}</p>
@@ -303,7 +303,7 @@ function FormatExportButton({
303
303
  const showQuality = format !== "mov";
304
304
 
305
305
  const selectCls =
306
- "h-7 w-full px-2 text-[11px] bg-panel-input rounded-md text-panel-text-1 outline-none cursor-pointer disabled:opacity-50 hover:bg-panel-hover transition-colors";
306
+ "h-7 w-full px-2 text-[11px] bg-panel-input rounded-md text-panel-text-1 outline-hidden cursor-pointer disabled:opacity-50 hover:bg-panel-hover transition-colors";
307
307
 
308
308
  return (
309
309
  <div className="flex flex-col gap-3">
@@ -407,7 +407,7 @@ function FormatExportButton({
407
407
  trackStudioEvent("render_start", { format, quality, resolution: outputResolution, fps });
408
408
  void onStartRender(format, quality, outputResolution, fps);
409
409
  }}
410
- className="w-full text-[11px] font-semibold"
410
+ className="w-full font-semibold"
411
411
  >
412
412
  {isRendering ? "Rendering…" : "Export"}
413
413
  </Button>
@@ -454,7 +454,7 @@ export const RenderQueue = memo(function RenderQueue({
454
454
 
455
455
  return (
456
456
  <div className="flex flex-col h-full">
457
- <div className="px-3 py-3 border-b border-panel-border flex-shrink-0">
457
+ <div className="px-3 py-3 border-b border-panel-border shrink-0">
458
458
  <FormatExportButton
459
459
  onStartRender={onStartRender}
460
460
  isRendering={isRendering}
@@ -476,7 +476,7 @@ export const RenderQueue = memo(function RenderQueue({
476
476
  <button
477
477
  onClick={onDismissActionError}
478
478
  aria-label="Dismiss error"
479
- className="text-[10px] text-panel-text-4 hover:text-panel-text-2 flex-shrink-0"
479
+ className="text-[10px] text-panel-text-4 hover:text-panel-text-2 shrink-0"
480
480
  >
481
481
  ✕
482
482
  </button>
@@ -77,8 +77,8 @@ export const RenderQueueItem = memo(function RenderQueueItem({
77
77
  disabled={!isComplete}
78
78
  aria-label={isComplete ? `Open ${job.filename} in a new tab` : undefined}
79
79
  className={[
80
- "w-20 h-[45px] rounded-md overflow-hidden bg-panel-input flex-shrink-0 relative",
81
- "outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent",
80
+ "w-20 h-[45px] rounded-md overflow-hidden bg-panel-input shrink-0 relative",
81
+ "outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent",
82
82
  isComplete ? "cursor-pointer" : "cursor-default",
83
83
  ].join(" ")}
84
84
  >
@@ -130,7 +130,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
130
130
  {job.filename}
131
131
  </span>
132
132
  {job.durationMs && (
133
- <span className="text-[9px] text-panel-text-5 flex-shrink-0">
133
+ <span className="text-[9px] text-panel-text-5 shrink-0">
134
134
  {formatDuration(job.durationMs)}
135
135
  </span>
136
136
  )}
@@ -171,7 +171,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
171
171
  </div>
172
172
 
173
173
  {/* Actions — always visible to prevent layout shifts */}
174
- <div className="flex items-center gap-1.5 flex-shrink-0">
174
+ <div className="flex items-center gap-1.5 shrink-0">
175
175
  {isRendering ? (
176
176
  <Button
177
177
  size="sm"
@@ -211,7 +211,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
211
211
  <>
212
212
  <button
213
213
  onClick={isComplete ? handleDownload : undefined}
214
- className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent ${
214
+ className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent ${
215
215
  isComplete
216
216
  ? "text-panel-text-5 hover:text-panel-accent"
217
217
  : "text-panel-text-5/30 cursor-default"
@@ -240,7 +240,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
240
240
  e.stopPropagation();
241
241
  setConfirmingDelete(true);
242
242
  }}
243
- className="p-1.5 min-w-6 min-h-6 rounded text-panel-text-5 hover:text-red-400 transition-colors outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent"
243
+ className="p-1.5 min-w-6 min-h-6 rounded-sm text-panel-text-5 hover:text-red-400 transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent"
244
244
  title="Delete render file"
245
245
  aria-label={`Delete ${job.filename}`}
246
246
  >
@@ -7,7 +7,7 @@ import { usePreviewVariablesStore } from "../../hooks/previewVariablesStore";
7
7
  /**
8
8
  * The Renders tab, wired to the shell context.
9
9
  *
10
- * Split out of StudioRightPanel because every field it needs already lives in
10
+ * Split out of StudioRightPanels because every field it needs already lives in
11
11
  * that context, so routing them through the panel only made the panel longer
12
12
  * without giving anything a second reader.
13
13
  */
@@ -30,7 +30,7 @@ function CopyChip({ feedback, asset }: { feedback: CopyFeedback; asset: string }
30
30
  return (
31
31
  <span
32
32
  role="status"
33
- className={`flex-shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
33
+ className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
34
34
  feedback.ok ? "text-panel-accent bg-panel-accent/10" : "text-red-400 bg-red-500/10"
35
35
  }`}
36
36
  >
@@ -223,12 +223,12 @@ export function AssetCard({
223
223
  onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
224
224
  onPointerEnter={() => setHovered(true)}
225
225
  onPointerLeave={() => setHovered(false)}
226
- className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors outline-none focus-visible:bg-neutral-800/60 ${
226
+ className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors outline-hidden focus-visible:bg-neutral-800/60 ${
227
227
  isCopied ? "bg-studio-accent/10" : "hover:bg-neutral-800/40"
228
228
  }`}
229
229
  >
230
230
  {/* Thumbnail */}
231
- <div className="w-full aspect-video rounded overflow-hidden bg-neutral-900 relative">
231
+ <div className="w-full aspect-video rounded-sm overflow-hidden bg-neutral-900 relative">
232
232
  {isImage && !imgError && (
233
233
  <img
234
234
  src={serveUrl}
@@ -261,14 +261,14 @@ export function AssetCard({
261
261
 
262
262
  {/* "Added" badge — top-left */}
263
263
  {used && (
264
- <span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-1">
264
+ <span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded-sm bg-neutral-950/80 text-panel-text-1">
265
265
  Added
266
266
  </span>
267
267
  )}
268
268
 
269
269
  {/* Duration badge — top-right, media only */}
270
270
  {durationLabel && (
271
- <span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-2 tabular-nums">
271
+ <span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded-sm bg-neutral-950/80 text-panel-text-2 tabular-nums">
272
272
  {durationLabel}
273
273
  </span>
274
274
  )}
@@ -348,13 +348,13 @@ export function FontRow({
348
348
  }}
349
349
  onDragStart={(e) => writeAssetDragData(e, asset)}
350
350
  onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
351
- className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors outline-none focus-visible:bg-neutral-800/60 ${
351
+ className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors outline-hidden focus-visible:bg-neutral-800/60 ${
352
352
  isCopied
353
353
  ? "bg-studio-accent/10 border-l-2 border-studio-accent"
354
354
  : "border-l-2 border-transparent hover:bg-neutral-800/50"
355
355
  }`}
356
356
  >
357
- <div className="w-[50px] h-[32px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 flex items-center justify-center">
357
+ <div className="w-[50px] h-[32px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 flex items-center justify-center">
358
358
  <span className="text-[9px] font-medium text-neutral-700">{extension}</span>
359
359
  </div>
360
360
  <div className="min-w-0 flex-1">
@@ -366,7 +366,7 @@ export function FontRow({
366
366
  <div className="flex items-center gap-1.5">
367
367
  <span className="text-[10px] text-neutral-600 truncate">{extension}</span>
368
368
  {used && (
369
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
369
+ <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm">
370
370
  in use
371
371
  </span>
372
372
  )}
@@ -95,11 +95,11 @@ export function ContextMenu({
95
95
  }, [renameDraft, asset, onRename, onClose]);
96
96
 
97
97
  const itemCls =
98
- "w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-none active:bg-neutral-700/70 transition-colors";
98
+ "w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden active:bg-neutral-700/70 transition-colors";
99
99
 
100
100
  return (
101
101
  <div
102
- className="fixed inset-0 z-[200]"
102
+ className="fixed inset-0 z-200"
103
103
  onClick={onClose}
104
104
  onContextMenu={(e) => {
105
105
  e.preventDefault();
@@ -181,19 +181,19 @@ export function ContextMenu({
181
181
  }
182
182
  }}
183
183
  aria-label={`Rename ${filename(asset)}`}
184
- className="w-full bg-neutral-800 border border-neutral-600 rounded px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-none"
184
+ className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
185
185
  />
186
186
  {renameError && <span className="text-[10px] text-red-400">{renameError}</span>}
187
187
  <div className="flex items-center justify-end gap-1">
188
188
  <button
189
189
  onClick={() => setMode("menu")}
190
- className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
190
+ className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 transition-colors"
191
191
  >
192
192
  Cancel
193
193
  </button>
194
194
  <button
195
195
  onClick={commitRename}
196
- className="px-2 py-0.5 text-[10px] rounded bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
196
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
197
197
  >
198
198
  Rename
199
199
  </button>
@@ -217,16 +217,16 @@ function DeleteConfirm({
217
217
  return (
218
218
  <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
219
219
  <span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
220
- <div className="flex items-center gap-1 flex-shrink-0">
220
+ <div className="flex items-center gap-1 shrink-0">
221
221
  <button
222
222
  onClick={onConfirm}
223
- className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
223
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
224
224
  >
225
225
  Delete
226
226
  </button>
227
227
  <button
228
228
  onClick={onCancel}
229
- className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
229
+ className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 transition-colors"
230
230
  >
231
231
  Cancel
232
232
  </button>
@@ -331,7 +331,7 @@ export const AssetsTab = memo(function AssetsTab({
331
331
  : [activeFilter as MediaCategory].filter((c) => categorized[c].length > 0);
332
332
  return (
333
333
  <div
334
- className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/[0.05]" : ""}`}
334
+ className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/5" : ""}`}
335
335
  onDragOver={(e) => {
336
336
  e.preventDefault();
337
337
  setDragOver(true);
@@ -340,7 +340,7 @@ export const AssetsTab = memo(function AssetsTab({
340
340
  onDrop={handleDrop}
341
341
  >
342
342
  {/* Header — matches design panel Section pattern */}
343
- <div className="px-4 pt-2.5 pb-1.5 flex-shrink-0">
343
+ <div className="px-4 pt-2.5 pb-1.5 shrink-0">
344
344
  {/* Scope toggle */}
345
345
  <div className="flex gap-1 mb-2.5 p-0.5 rounded-md bg-panel-input">
346
346
  {(["local", "global"] as const).map((m) => (
@@ -227,9 +227,9 @@ export function AudioRow({
227
227
  e.preventDefault();
228
228
  setContextMenu({ x: e.clientX, y: e.clientY });
229
229
  }}
230
- className={`group w-full text-left px-4 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer outline-none focus-visible:bg-neutral-800/60 ${
230
+ className={`group w-full text-left px-4 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer outline-hidden focus-visible:bg-neutral-800/60 ${
231
231
  playing
232
- ? "bg-panel-accent/[0.06]"
232
+ ? "bg-panel-accent/6"
233
233
  : isCopied
234
234
  ? "bg-panel-accent/10"
235
235
  : "hover:bg-panel-surface-hover"
@@ -238,7 +238,7 @@ export function AudioRow({
238
238
  <button
239
239
  aria-label={playing ? `Pause preview of ${name}` : `Play preview of ${name}`}
240
240
  aria-pressed={playing}
241
- className={`w-7 h-7 rounded-md flex-shrink-0 flex items-center justify-center transition-colors active:scale-[0.95] ${
241
+ className={`w-7 h-7 rounded-md shrink-0 flex items-center justify-center transition-colors active:scale-[0.95] ${
242
242
  playing
243
243
  ? "bg-panel-accent/15 text-panel-accent"
244
244
  : "text-panel-text-5 group-hover:text-panel-text-3"
@@ -267,20 +267,20 @@ export function AudioRow({
267
267
  {name}
268
268
  </span>
269
269
  {!playing && (
270
- <span className="text-[11px] text-panel-text-5 flex-shrink-0">
270
+ <span className="text-[11px] text-panel-text-5 shrink-0">
271
271
  {meta?.duration ? `${meta.duration}s · ` : ""}
272
272
  {subtype}
273
273
  </span>
274
274
  )}
275
275
  {used && (
276
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded flex-shrink-0">
276
+ <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
277
277
  in use
278
278
  </span>
279
279
  )}
280
280
  {(isCopied || copyFailed) && (
281
281
  <span
282
282
  role="status"
283
- className={`flex-shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
283
+ className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
284
284
  copyFailed ? "text-red-400 bg-red-500/10" : "text-panel-accent bg-panel-accent/10"
285
285
  }`}
286
286
  >
@@ -49,7 +49,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
49
49
  return (
50
50
  <div className="flex flex-col flex-1 min-h-0">
51
51
  {/* Search */}
52
- <div className="px-3 pt-2 pb-1 flex-shrink-0">
52
+ <div className="px-3 pt-2 pb-1 shrink-0">
53
53
  <SearchInput
54
54
  value={search}
55
55
  onChange={(e) => setSearch(e.target.value)}
@@ -59,7 +59,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
59
59
  </div>
60
60
 
61
61
  {/* Category pills */}
62
- <div className="px-3 pt-1 pb-2 flex-shrink-0 overflow-x-auto">
62
+ <div className="px-3 pt-1 pb-2 shrink-0 overflow-x-auto">
63
63
  <div className="flex gap-1">
64
64
  <CategoryPill label="All" active={category === null} onClick={() => setCategory(null)} />
65
65
  {BLOCK_CATEGORIES.map((cat) => (
@@ -151,7 +151,7 @@ function CategoryPill({
151
151
  type="button"
152
152
  onClick={onClick}
153
153
  aria-pressed={active}
154
- className={`flex-shrink-0 px-2 py-1 rounded-full text-[10px] font-medium transition-colors active:scale-[0.98] ${
154
+ className={`shrink-0 px-2 py-1 rounded-full text-[10px] font-medium transition-colors active:scale-[0.98] ${
155
155
  active
156
156
  ? colors
157
157
  ? `${colors.bg} ${colors.text}`
@@ -474,12 +474,12 @@ function BlockCard({
474
474
  {/* Badges */}
475
475
  <div className="absolute top-1 right-1 flex items-center gap-0.5 pointer-events-none">
476
476
  {needsWebGL && (
477
- <span className="px-1 py-px rounded text-[7px] font-semibold text-purple-300 bg-purple-900/70">
477
+ <span className="px-1 py-px rounded-sm text-[7px] font-semibold text-purple-300 bg-purple-900/70">
478
478
  WebGL
479
479
  </span>
480
480
  )}
481
481
  {duration != null && (
482
- <span className="px-1 py-px rounded text-[8px] font-medium text-white/80 bg-black/50">
482
+ <span className="px-1 py-px rounded-sm text-[8px] font-medium text-white/80 bg-black/50">
483
483
  {duration}s
484
484
  </span>
485
485
  )}
@@ -492,7 +492,7 @@ function BlockCard({
492
492
  {title}
493
493
  </div>
494
494
  <div className="flex items-center gap-1 mt-0.5">
495
- <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${colors.dot}`} />
495
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${colors.dot}`} />
496
496
  <span className={`text-[8px] ${colors.text}`}>
497
497
  {BLOCK_CATEGORIES.find((c) => c.id === category)?.label}
498
498
  </span>
@@ -4,7 +4,7 @@ import { act } from "react";
4
4
  import { createRoot, type Root } from "react-dom/client";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { usePlayerStore } from "../../player/store/playerStore";
7
- import { LeftSidebar } from "./LeftSidebar";
7
+ import { CompositionsPanel } from "./CompositionsPanel";
8
8
 
9
9
  Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
10
10
  (
@@ -20,23 +20,20 @@ afterEach(() => {
20
20
  usePlayerStore.setState({ thumbnailContentRevision: 0 });
21
21
  });
22
22
 
23
- describe("LeftSidebar root badge source", () => {
23
+ describe("CompositionsPanel root badge source", () => {
24
24
  it("derives the root badge from the filtered compositions list, not the raw file tree", () => {
25
- // `fileTree` includes a non-composition index.html (a vendored preset) that the
26
- // server's filtered `compositions` list excludes — the badge must source from
27
- // `compositions`, not the raw file tree.
25
+ // The project's raw file tree can hold a non-composition index.html (a vendored
26
+ // preset); the badge must source from the server's filtered `compositions` list.
28
27
  const host = document.createElement("div");
29
28
  document.body.append(host);
30
29
  root = createRoot(host);
31
30
  act(() => {
32
31
  root?.render(
33
- <LeftSidebar
32
+ <CompositionsPanel
34
33
  projectId="demo"
35
34
  compositions={["hero.html"]}
36
- assets={[]}
37
- fileTree={["index.html", "hero.html"]}
38
35
  activeComposition={null}
39
- onSelectComposition={vi.fn()}
36
+ onSelect={vi.fn()}
40
37
  />,
41
38
  );
42
39
  });
@@ -0,0 +1,15 @@
1
+ import type { ComponentProps } from "react";
2
+ import { resolveMasterCompositionPath } from "../../utils/studioUrlState";
3
+ import { CompositionsTab } from "./CompositionsTab";
4
+
5
+ type Props = Omit<ComponentProps<typeof CompositionsTab>, "masterCompositionPath">;
6
+
7
+ /** The Compositions dock panel body; the root badge comes from the filtered composition list. */
8
+ export function CompositionsPanel(props: Props) {
9
+ return (
10
+ <CompositionsTab
11
+ {...props}
12
+ masterCompositionPath={resolveMasterCompositionPath(props.compositions)}
13
+ />
14
+ );
15
+ }
@@ -238,13 +238,13 @@ function CompCard({
238
238
  }}
239
239
  onPointerEnter={handleEnter}
240
240
  onPointerLeave={handleLeave}
241
- className={`group/card w-full select-none text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-grab active:cursor-grabbing outline-none focus-visible:bg-neutral-800/60 ${
241
+ className={`group/card w-full select-none text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-grab active:cursor-grabbing outline-hidden focus-visible:bg-neutral-800/60 ${
242
242
  isActive
243
243
  ? "bg-studio-accent/10 border-l-2 border-studio-accent"
244
244
  : "border-l-2 border-transparent hover:bg-neutral-800/50"
245
245
  }`}
246
246
  >
247
- <div className="w-20 h-[45px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 relative">
247
+ <div className="w-20 h-[45px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 relative">
248
248
  {thumbnailFailed ? (
249
249
  <div className="absolute inset-0 flex items-center justify-center px-1 text-center text-[8px] leading-tight text-neutral-600">
250
250
  Preview unavailable
@@ -314,7 +314,7 @@ function CompCard({
314
314
  {lintInfo && lintInfo.count > 0 && (
315
315
  <span
316
316
  aria-label={`${lintInfo.count} lint finding${lintInfo.count === 1 ? "" : "s"}`}
317
- className="flex-shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400"
317
+ className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400"
318
318
  >
319
319
  {lintInfo.count}
320
320
  </span>
@@ -331,7 +331,7 @@ function CompCard({
331
331
  event.stopPropagation();
332
332
  onAddToTimeline();
333
333
  }}
334
- className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-studio-accent group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
334
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-studio-accent group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
335
335
  >
336
336
  <span aria-hidden="true">+</span>
337
337
  </button>
@@ -349,7 +349,7 @@ function CompCard({
349
349
  // h-6 w-6 = the 24x24 WCAG 2.2 (2.5.8) minimum target; the 14px glyph
350
350
  // is unchanged, only the box grows. The sibling "+" button is h-8 w-8,
351
351
  // so the card row already has the room.
352
- className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded transition-colors ${
352
+ className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
353
353
  isRendering
354
354
  ? "text-neutral-600 cursor-not-allowed"
355
355
  : "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
@@ -80,7 +80,7 @@ export function GlobalAssetsView({ searchQuery }: { searchQuery: string }) {
80
80
  className="px-4 py-1.5 flex items-center gap-2.5 border-l-2 border-transparent hover:bg-neutral-800/50"
81
81
  title={`${row.id} · ${row.type}`}
82
82
  >
83
- <span className="text-[9px] font-medium text-neutral-600 uppercase w-10 flex-shrink-0">
83
+ <span className="text-[9px] font-medium text-neutral-600 uppercase w-10 shrink-0">
84
84
  {row.type}
85
85
  </span>
86
86
  <span className="text-xs text-panel-text-1 truncate">{row.label}</span>
@@ -46,7 +46,7 @@ export function PromptPreviewModal({
46
46
 
47
47
  return (
48
48
  <div
49
- className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm transition-opacity duration-150 ease-out ${
49
+ className={`fixed inset-0 z-100 flex items-center justify-center bg-black/60 backdrop-blur-xs transition-opacity duration-150 ease-out ${
50
50
  entered ? "opacity-100" : "opacity-0"
51
51
  }`}
52
52
  onClick={requestClose}
@@ -57,7 +57,7 @@ export function PromptPreviewModal({
57
57
  aria-modal="true"
58
58
  aria-label={`Ask agent — ${title}`}
59
59
  tabIndex={-1}
60
- className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-none"
60
+ className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-hidden"
61
61
  onClick={(e) => e.stopPropagation()}
62
62
  >
63
63
  <div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
@@ -96,7 +96,7 @@ export function PromptPreviewModal({
96
96
  onKeyDown={(e) => {
97
97
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void handleCopy();
98
98
  }}
99
- className="w-full min-h-[240px] text-[11px] text-neutral-200 leading-relaxed font-mono bg-neutral-900/60 rounded-lg p-3 border border-neutral-800 resize-y focus:outline-none focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
99
+ className="w-full min-h-[240px] text-[11px] text-neutral-200 leading-relaxed font-mono bg-neutral-900/60 rounded-lg p-3 border border-neutral-800 resize-y focus:outline-hidden focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
100
100
  />
101
101
  </div>
102
102
  <div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
@@ -0,0 +1,53 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { SidebarLintButton } from "./SidebarLintButton";
7
+
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
9
+ (
10
+ window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
11
+ ).happyDOM.settings.disableIframePageLoading = true;
12
+
13
+ let root: Root | null = null;
14
+
15
+ afterEach(() => {
16
+ if (root) act(() => root?.unmount());
17
+ root = null;
18
+ document.body.innerHTML = "";
19
+ });
20
+
21
+ function renderBadge(lintHasError: boolean): HTMLElement | null {
22
+ const host = document.createElement("div");
23
+ document.body.append(host);
24
+ root = createRoot(host);
25
+ act(() => {
26
+ root?.render(
27
+ <SidebarLintButton
28
+ onLint={vi.fn()}
29
+ linting={false}
30
+ findingCount={2}
31
+ hasError={lintHasError}
32
+ />,
33
+ );
34
+ });
35
+ return host.querySelector<HTMLElement>("[data-lint-badge]");
36
+ }
37
+
38
+ describe("SidebarLintButton badge", () => {
39
+ it("pulses, and stops for reduced motion, while an error finding exists", () => {
40
+ const badge = renderBadge(true);
41
+ expect(badge?.dataset.lintBadge).toBe("error");
42
+ expect(badge?.className).toContain("animate-pulse");
43
+ expect(badge?.className).toContain("motion-reduce:animate-none");
44
+ expect(badge?.textContent).toContain("including errors");
45
+ });
46
+
47
+ it("stays still when the findings are warnings only", () => {
48
+ const badge = renderBadge(false);
49
+ expect(badge?.dataset.lintBadge).toBe("warning");
50
+ expect(badge?.className).not.toContain("animate-pulse");
51
+ expect(badge?.textContent).toContain("warnings only");
52
+ });
53
+ });