@hyperframes/studio 0.8.50 → 0.8.52

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 (280) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
  2. package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
  3. package/dist/assets/index-CxAu_Okg.css +1 -0
  4. package/dist/assets/index-DocJlW2v.js +376 -0
  5. package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
  6. package/dist/index.d.ts +419 -20
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7496 -8288
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +13 -12
  12. package/src/App.tsx +131 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  15. package/src/captions/components/CaptionOverlay.tsx +8 -4
  16. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  17. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  18. package/src/captions/components/CaptionTimeline.tsx +1 -1
  19. package/src/captions/components/shared.tsx +2 -2
  20. package/src/components/AskAgentModal.tsx +5 -5
  21. package/src/components/EditorShell.tsx +8 -7
  22. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  23. package/src/components/LintModal.tsx +6 -16
  24. package/src/components/MediaPreview.tsx +2 -2
  25. package/src/components/RightPanelTabs.test.tsx +151 -0
  26. package/src/components/RightPanelTabs.tsx +59 -0
  27. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  28. package/src/components/StudioHeader.dom.test.tsx +181 -0
  29. package/src/components/StudioHeader.tsx +110 -210
  30. package/src/components/StudioLeftSidebar.tsx +7 -4
  31. package/src/components/StudioOverlays.tsx +1 -1
  32. package/src/components/StudioRightPanel.tsx +51 -39
  33. package/src/components/StudioToast.tsx +2 -2
  34. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  35. package/src/components/TimelineHistoryButtons.tsx +54 -0
  36. package/src/components/TimelineToolbar.test.tsx +8 -0
  37. package/src/components/TimelineToolbar.tsx +8 -12
  38. package/src/components/dock/Dock.test.tsx +260 -0
  39. package/src/components/dock/Dock.tsx +281 -0
  40. package/src/components/dock/dock.css +28 -0
  41. package/src/components/dock/dockAccessibility.test.ts +166 -0
  42. package/src/components/dock/dockAccessibility.ts +98 -0
  43. package/src/components/dock/dockLayout.test.ts +134 -0
  44. package/src/components/dock/dockLayout.ts +105 -0
  45. package/src/components/dock/dockLayoutSchema.ts +66 -0
  46. package/src/components/dock/dockLayoutStore.ts +91 -0
  47. package/src/components/dock/dockTestHarness.ts +11 -0
  48. package/src/components/dock/panelRegistry.ts +67 -0
  49. package/src/components/editor/AnimationCard.tsx +4 -4
  50. package/src/components/editor/AnimationCardParts.tsx +6 -6
  51. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  52. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  53. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  54. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  55. package/src/components/editor/DomEditOverlay.tsx +1 -1
  56. package/src/components/editor/EaseCurveSection.tsx +1 -1
  57. package/src/components/editor/EaseParamFields.tsx +5 -5
  58. package/src/components/editor/FileTree.tsx +4 -4
  59. package/src/components/editor/FileTreeIcons.tsx +1 -1
  60. package/src/components/editor/FileTreeNodes.tsx +13 -13
  61. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  62. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  63. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  64. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  65. package/src/components/editor/LayersPanel.test.ts +97 -1
  66. package/src/components/editor/LayersPanel.tsx +7 -5
  67. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  68. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  69. package/src/components/editor/PreviewGuides.tsx +1 -1
  70. package/src/components/editor/PromotableControl.tsx +2 -2
  71. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  72. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  73. package/src/components/editor/SnapToolbar.tsx +2 -2
  74. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  75. package/src/components/editor/TopologyLens.test.tsx +28 -0
  76. package/src/components/editor/TopologyLens.tsx +7 -5
  77. package/src/components/editor/Transform3DCube.tsx +2 -2
  78. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  79. package/src/components/editor/domEditing.test.ts +1 -1
  80. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  81. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  82. package/src/components/editor/propertyPanelColor.tsx +11 -11
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  87. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  88. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  89. package/src/components/editor/propertyPanelFill.tsx +4 -4
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  94. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  95. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  96. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  97. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  98. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  99. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.tsx +5 -5
  101. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  102. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  106. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  107. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  108. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  109. package/src/components/editor/propertyPanelSections.tsx +8 -8
  110. package/src/components/editor/useColorGradingController.test.ts +38 -0
  111. package/src/components/editor/useColorGradingController.ts +9 -1
  112. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  113. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  114. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  115. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  116. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  117. package/src/components/feedback/feedbackTrigger.ts +0 -1
  118. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  119. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  120. package/src/components/nle/NLEPreview.tsx +3 -3
  121. package/src/components/nle/PreviewOverlays.tsx +6 -6
  122. package/src/components/nle/PreviewPane.tsx +3 -3
  123. package/src/components/nle/TimelinePane.tsx +3 -3
  124. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  125. package/src/components/panels/SlideshowPanel.tsx +2 -2
  126. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  127. package/src/components/panels/VariablesBindElement.tsx +2 -2
  128. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  129. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  130. package/src/components/panels/VariablesPanel.tsx +3 -3
  131. package/src/components/panels/VariablesValueControls.tsx +2 -2
  132. package/src/components/pointerTargetSize.test.tsx +8 -0
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  134. package/src/components/renders/RenderQueue.test.tsx +7 -0
  135. package/src/components/renders/RenderQueue.tsx +7 -7
  136. package/src/components/renders/RenderQueueItem.tsx +6 -6
  137. package/src/components/sidebar/AssetCard.tsx +8 -8
  138. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  139. package/src/components/sidebar/AssetsTab.tsx +2 -2
  140. package/src/components/sidebar/AudioRow.tsx +6 -6
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  143. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  144. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  145. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/timelineToolbarStyles.ts +5 -0
  148. package/src/components/ui/Button.test.tsx +252 -0
  149. package/src/components/ui/Button.tsx +89 -126
  150. package/src/components/ui/IconButton.tsx +38 -0
  151. package/src/components/ui/Input.tsx +115 -0
  152. package/src/components/ui/Menu.test.tsx +374 -0
  153. package/src/components/ui/Menu.tsx +213 -0
  154. package/src/components/ui/NumberField.tsx +122 -0
  155. package/src/components/ui/Popover.tsx +74 -0
  156. package/src/components/ui/SearchInput.tsx +2 -2
  157. package/src/components/ui/Select.tsx +100 -0
  158. package/src/components/ui/Slider.tsx +153 -0
  159. package/src/components/ui/Tabs.test.tsx +98 -0
  160. package/src/components/ui/Tabs.tsx +71 -0
  161. package/src/components/ui/Toggle.tsx +63 -0
  162. package/src/components/ui/Tooltip.test.tsx +110 -0
  163. package/src/components/ui/Tooltip.tsx +38 -112
  164. package/src/components/ui/cn.test.ts +30 -0
  165. package/src/components/ui/cn.ts +25 -0
  166. package/src/components/ui/index.ts +29 -2
  167. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  168. package/src/components/ui/valueControls.test.tsx +524 -0
  169. package/src/hooks/useAppHotkeys.ts +21 -80
  170. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  171. package/src/hooks/useCaptionDetection.ts +3 -1
  172. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  173. package/src/hooks/useEditHistoryActions.ts +106 -0
  174. package/src/hooks/useLintModal.test.tsx +65 -0
  175. package/src/hooks/useLintModal.ts +3 -0
  176. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  177. package/src/hooks/useSdkSession.ts +43 -10
  178. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  179. package/src/hooks/useServerConnection.ts +43 -10
  180. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  181. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  182. package/src/index.ts +51 -0
  183. package/src/main.tsx +9 -3
  184. package/src/player/components/AudioWaveform.tsx +1 -1
  185. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  186. package/src/player/components/ClipContextMenu.tsx +3 -3
  187. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  188. package/src/player/components/CompositionThumbnail.tsx +3 -4
  189. package/src/player/components/EditModal.tsx +1 -1
  190. package/src/player/components/ImageThumbnail.tsx +1 -1
  191. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  192. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  193. package/src/player/components/PlayerControls.tsx +1 -1
  194. package/src/player/components/ShortcutsPanel.tsx +9 -9
  195. package/src/player/components/SpeedMenu.tsx +3 -5
  196. package/src/player/components/Timeline.tsx +2 -1
  197. package/src/player/components/TimelineCanvas.tsx +21 -14
  198. package/src/player/components/TimelineClip.tsx +0 -1
  199. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  200. package/src/player/components/TimelineEmptyState.tsx +5 -5
  201. package/src/player/components/TimelineFxButton.tsx +6 -6
  202. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  203. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  204. package/src/player/components/TimelineGroupRow.tsx +1 -1
  205. package/src/player/components/TimelineLanes.test.tsx +32 -0
  206. package/src/player/components/TimelineLanes.tsx +2 -5
  207. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  208. package/src/player/components/TimelineRuler.tsx +1 -1
  209. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  210. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  211. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  212. package/src/player/components/TimelineTypes.ts +2 -1
  213. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  214. package/src/player/components/VideoThumbnail.tsx +4 -2
  215. package/src/player/components/VolumeControl.tsx +2 -2
  216. package/src/player/components/timelineCallbacks.ts +8 -8
  217. package/src/player/components/timelineClipDragPreview.ts +51 -27
  218. package/src/player/components/timelineClipDragTypes.ts +3 -0
  219. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  220. package/src/player/components/timelineDragDrop.ts +57 -15
  221. package/src/player/components/timelineLaneProps.ts +3 -0
  222. package/src/player/components/timelineSnapping.test.ts +27 -0
  223. package/src/player/components/timelineSnapping.ts +14 -1
  224. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  225. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  226. package/src/player/components/useTimelineClipDrag.ts +30 -3
  227. package/src/player/lib/describeClips.ts +3 -54
  228. package/src/player/lib/playbackShortcuts.ts +2 -0
  229. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  230. package/src/player/store/previewIframeStore.test.tsx +29 -0
  231. package/src/player/store/previewIframeStore.ts +25 -0
  232. package/src/renderQueueExports.test.tsx +58 -0
  233. package/src/styles/classCandidates.test.ts +105 -0
  234. package/src/styles/classCandidates.ts +281 -0
  235. package/src/styles/hex-baseline.json +84 -0
  236. package/src/styles/hexRatchet.test.ts +201 -0
  237. package/src/styles/iconTokens.ts +20 -0
  238. package/src/styles/palette-reset.css +6 -0
  239. package/src/styles/studio.css +24 -21
  240. package/src/styles/styleSources.test.ts +20 -0
  241. package/src/styles/styleSources.ts +47 -0
  242. package/src/styles/tailwind-preset.shared.js +3 -0
  243. package/src/styles/theme.css +243 -0
  244. package/src/styles/theme.test.ts +183 -0
  245. package/src/styles/tokenGate.test.ts +109 -0
  246. package/src/utils/sourcePatcher.ts +1 -1
  247. package/src/utils/studioHelpers.test.ts +18 -0
  248. package/src/utils/studioHelpers.ts +11 -1
  249. package/src/utils/studioUiPreferences.ts +7 -0
  250. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  251. package/src/utils/timelineDropTrackInsert.ts +83 -0
  252. package/src/utils/typingTarget.ts +4 -0
  253. package/dist/assets/index-n2ml4tUn.js +0 -442
  254. package/dist/assets/index-pO6p0H27.css +0 -1
  255. package/src/components/PanelTabButton.tsx +0 -31
  256. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  257. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  258. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  259. package/src/components/storyboard/FramePoster.tsx +0 -86
  260. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  261. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  262. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  263. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  264. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  265. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  266. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  267. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  268. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  269. package/src/components/storyboard/StoryboardView.tsx +0 -169
  270. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  271. package/src/components/storyboard/frameComments.test.ts +0 -95
  272. package/src/components/storyboard/frameComments.ts +0 -115
  273. package/src/components/storyboard/frameStatus.ts +0 -36
  274. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  275. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  276. package/src/components/storyboard/useFrameComments.ts +0 -98
  277. package/src/contexts/ViewModeContext.tsx +0 -142
  278. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  279. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  280. package/src/hooks/useStoryboard.ts +0 -99
@@ -0,0 +1,134 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import type { DockviewApi } from "dockview-react";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import {
6
+ applySideMinimums,
7
+ buildEditLayout,
8
+ defaultSideWidths,
9
+ sideMinimumWidth,
10
+ } from "./dockLayout";
11
+ import { mountBareDockview } from "./dockTestHarness";
12
+
13
+ class ResizeObserverStub {
14
+ observe() {}
15
+ unobserve() {}
16
+ disconnect() {}
17
+ }
18
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
19
+
20
+ // Expected values are hand-derived from the old fitPanels rule: the preview keeps 360 first,
21
+ // then each side column takes half of what remains, never above 200 or below 120.
22
+ describe("sideMinimumWidth", () => {
23
+ it.each([
24
+ [1680, 200],
25
+ [1200, 200],
26
+ [860, 200],
27
+ [760, 200],
28
+ [700, 170],
29
+ [600, 120],
30
+ [560, 120],
31
+ [400, 120],
32
+ ])("is %ipx wide -> sides shrink to %ipx at most", (viewport, expected) => {
33
+ expect(sideMinimumWidth(viewport)).toBe(expected);
34
+ });
35
+ });
36
+
37
+ describe("defaultSideWidths", () => {
38
+ it.each([
39
+ [1680, 384, 424],
40
+ [1280, 329, 364],
41
+ [860, 221, 244],
42
+ [560, 120, 120],
43
+ ])("at %ipx gives %i / %i", (viewport, left, right) => {
44
+ expect(defaultSideWidths(viewport)).toEqual({ left, right });
45
+ });
46
+
47
+ it.each([1680, 1280, 1000, 860, 760, 700, 600, 560, 480])(
48
+ "leaves the preview its 360px floor at %ipx whenever the sides can shrink enough",
49
+ (viewport) => {
50
+ const { left, right } = defaultSideWidths(viewport);
51
+ const floor = sideMinimumWidth(viewport);
52
+ const overflow = left + right + 360 - viewport;
53
+ expect(overflow <= 0 || (left === floor && right === floor)).toBe(true);
54
+ },
55
+ );
56
+ });
57
+
58
+ describe("applySideMinimums", () => {
59
+ let api: DockviewApi;
60
+ beforeEach(() => {
61
+ ({ api } = mountBareDockview());
62
+ });
63
+ afterEach(() => {
64
+ api.dispose();
65
+ document.body.innerHTML = "";
66
+ });
67
+
68
+ it("lowers side groups' minimum on a narrow window and shrinks them, never touching the preview", () => {
69
+ api.layout(1200, 700);
70
+ buildEditLayout(api, 1200);
71
+ applySideMinimums(api, 560);
72
+ expect(api.getPanel("compositions")?.group.minimumWidth).toBe(120);
73
+ expect(api.getPanel("design")?.group.minimumWidth).toBe(120);
74
+ expect(api.getPanel("compositions")?.group.width).toBe(120);
75
+ expect(api.getPanel("design")?.group.width).toBe(120);
76
+ expect(api.getPanel("preview")?.group.minimumWidth).toBe(360);
77
+ });
78
+
79
+ it("leaves a wide window's user-chosen widths alone", () => {
80
+ api.layout(1200, 700);
81
+ buildEditLayout(api, 1200);
82
+ api.getPanel("compositions")?.group.api.setSize({ width: 300 });
83
+ applySideMinimums(api, 1200);
84
+ expect(api.getPanel("compositions")?.group.width).toBe(300);
85
+ });
86
+
87
+ it("keeps the preview's floor when a side panel is tabbed into the preview group", () => {
88
+ api.layout(1200, 700);
89
+ buildEditLayout(api, 1200);
90
+ const preview = api.getPanel("preview");
91
+ const design = api.getPanel("design");
92
+ if (!preview || !design) throw new Error("default layout is missing panels");
93
+ design.api.moveTo({ group: preview.group, position: "center", index: 0 });
94
+ preview.api.setActive();
95
+ applySideMinimums(api, 560);
96
+ expect(preview.group.minimumWidth).toBe(360);
97
+ });
98
+
99
+ it("pins the preview floor when a side panel that was showing gets the preview dragged in", () => {
100
+ api.layout(1200, 700);
101
+ buildEditLayout(api, 1200);
102
+ applySideMinimums(api, 560);
103
+ const preview = api.getPanel("preview");
104
+ const design = api.getPanel("design");
105
+ if (!preview || !design) throw new Error("default layout is missing panels");
106
+ preview.api.moveTo({ group: design.group });
107
+ applySideMinimums(api, 560);
108
+ expect(preview.group.minimumWidth).toBe(360);
109
+ });
110
+
111
+ it("rewrites the minimum of a group that is neither the preview nor side-only", () => {
112
+ api.layout(1200, 700);
113
+ buildEditLayout(api, 1200);
114
+ const timeline = api.getPanel("timeline");
115
+ const compositions = api.getPanel("compositions");
116
+ if (!timeline || !compositions) throw new Error("default layout is missing panels");
117
+ timeline.api.moveTo({ group: compositions.group });
118
+ applySideMinimums(api, 560);
119
+ expect(compositions.group.minimumWidth).toBe(120);
120
+ });
121
+
122
+ it("drops the preview floor from a group the preview has left", () => {
123
+ api.layout(1200, 700);
124
+ buildEditLayout(api, 1200);
125
+ const preview = api.getPanel("preview");
126
+ const timeline = api.getPanel("timeline");
127
+ if (!preview || !timeline) throw new Error("default layout is missing panels");
128
+ preview.api.moveTo({ group: timeline.group });
129
+ applySideMinimums(api, 560);
130
+ preview.api.moveTo({ position: "right", group: api.getPanel("design")?.group });
131
+ applySideMinimums(api, 560);
132
+ expect(timeline.group.minimumWidth).toBe(120);
133
+ });
134
+ });
@@ -0,0 +1,105 @@
1
+ import type { Direction, DockviewApi } from "dockview-react";
2
+ import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
3
+ import { PANEL_DEFINITIONS, isPanelId, type PanelId } from "./panelRegistry";
4
+
5
+ const MIN_PREVIEW_W = 360;
6
+ const MIN_PREVIEW_H = 200;
7
+ const MIN_TIMELINE_H = 100;
8
+ const MIN_SIDE_W = 200;
9
+ const MIN_SIDE_W_FLOOR = 120;
10
+ const DEFAULT_TIMELINE_H = 360;
11
+
12
+ /** Preferred side widths; when they overflow the preview's floor the right yields first, then the left. */
13
+ export function defaultSideWidths(viewportWidth: number) {
14
+ const floor = sideMinimumWidth(viewportWidth);
15
+ const left = Math.max(floor, Math.min(384, Math.round(viewportWidth * 0.257)));
16
+ const right = Math.max(Math.min(280, floor), Math.min(424, Math.round(viewportWidth * 0.284)));
17
+ const excess = left + right - Math.max(0, viewportWidth - MIN_PREVIEW_W);
18
+ if (excess <= 0) return { left, right };
19
+ const shrunkRight = Math.max(floor, right - excess);
20
+ return { left: Math.max(floor, left - (excess - (right - shrunkRight))), right: shrunkRight };
21
+ }
22
+
23
+ /**
24
+ * Side groups keep MIN_SIDE_W while both fit beside the preview's floor, then shrink down to
25
+ * MIN_SIDE_W_FLOOR; below that the dock overflows and clips rather than squeezing the preview.
26
+ */
27
+ export function sideMinimumWidth(dockWidth: number): number {
28
+ const fair = Math.floor((dockWidth - MIN_PREVIEW_W) / 2);
29
+ return Math.min(MIN_SIDE_W, Math.max(MIN_SIDE_W_FLOOR, fair));
30
+ }
31
+
32
+ function holdsPreview(group: DockviewApi["groups"][number]) {
33
+ return group.panels.some((panel) => panel.id === "preview");
34
+ }
35
+
36
+ /** The zone of a group whose every tab is a side panel, else null. */
37
+ function sideZone(group: DockviewApi["groups"][number]): "left" | "right" | null {
38
+ const zones = group.panels.map((panel) =>
39
+ isPanelId(panel.id) ? PANEL_DEFINITIONS[panel.id].zone : "center",
40
+ );
41
+ const first = zones[0];
42
+ const allSide = zones.every((zone) => zone !== "center");
43
+ return allSide && first && first !== "center" ? first : null;
44
+ }
45
+
46
+ /** Idempotent; rewrites every group's minimum, since dockview keeps a constraint once set. */
47
+ export function applySideMinimums(api: DockviewApi, dockWidth = window.innerWidth) {
48
+ const minimumWidth = sideMinimumWidth(dockWidth);
49
+ const cap = defaultSideWidths(dockWidth);
50
+ for (const group of api.groups) {
51
+ if (holdsPreview(group)) {
52
+ group.api.setConstraints({ minimumWidth: MIN_PREVIEW_W });
53
+ continue;
54
+ }
55
+ group.api.setConstraints({ minimumWidth });
56
+ const zone = sideZone(group);
57
+ const limit = zone === "right" ? cap.right : cap.left;
58
+ if (zone && minimumWidth < MIN_SIDE_W && group.width > limit) {
59
+ group.api.setSize({ width: limit });
60
+ }
61
+ }
62
+ }
63
+
64
+ function minimumSize(id: PanelId) {
65
+ if (id === "preview") return { minimumWidth: MIN_PREVIEW_W, minimumHeight: MIN_PREVIEW_H };
66
+ if (id === "timeline") return { minimumHeight: MIN_TIMELINE_H };
67
+ return { minimumWidth: MIN_SIDE_W };
68
+ }
69
+
70
+ export function addRegisteredPanel(
71
+ api: DockviewApi,
72
+ id: PanelId,
73
+ position?: { referencePanel: PanelId; direction: Direction },
74
+ ) {
75
+ return api.addPanel({
76
+ id,
77
+ component: DOCK_PANEL_COMPONENT,
78
+ title: PANEL_DEFINITIONS[id].title,
79
+ renderer: "always",
80
+ ...minimumSize(id),
81
+ ...(position ? { position } : {}),
82
+ });
83
+ }
84
+
85
+ /** The default Edit layout: [library | preview | inspector] over a full-width timeline. */
86
+ export function buildEditLayout(api: DockviewApi, viewportWidth: number) {
87
+ api.clear();
88
+ const widths = defaultSideWidths(viewportWidth);
89
+ addRegisteredPanel(api, "preview");
90
+ addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" });
91
+ addRegisteredPanel(api, "compositions", { referencePanel: "preview", direction: "left" });
92
+ for (const id of ["assets", "code", "catalog"] as const) {
93
+ addRegisteredPanel(api, id, { referencePanel: "compositions", direction: "within" });
94
+ }
95
+ addRegisteredPanel(api, "design", { referencePanel: "preview", direction: "right" });
96
+ for (const id of ["layers", "renders", "variables"] as const) {
97
+ addRegisteredPanel(api, id, { referencePanel: "design", direction: "within" });
98
+ }
99
+ api.getPanel("compositions")?.api.setActive();
100
+ api.getPanel("design")?.api.setActive();
101
+ applySideMinimums(api, viewportWidth);
102
+ api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
103
+ api.getPanel("design")?.group.api.setSize({ width: widths.right });
104
+ api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
105
+ }
@@ -0,0 +1,66 @@
1
+ import type { SerializedDockview } from "dockview-react";
2
+ import { isPanelId } from "./panelRegistry";
3
+
4
+ export const DOCK_PANEL_COMPONENT = "panel";
5
+
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ function collectLeafViews(node: unknown, into: unknown[]): boolean {
11
+ if (!isRecord(node)) return false;
12
+ if (node.type === "leaf") {
13
+ if (!isRecord(node.data) || !Array.isArray(node.data.views)) return false;
14
+ into.push(...node.data.views);
15
+ return true;
16
+ }
17
+ if (node.type === "branch") {
18
+ return Array.isArray(node.data) && node.data.every((child) => collectLeafViews(child, into));
19
+ }
20
+ return false;
21
+ }
22
+
23
+ function hasGridShape(grid: Record<string, unknown>): boolean {
24
+ const oriented = grid.orientation === "HORIZONTAL" || grid.orientation === "VERTICAL";
25
+ return oriented && typeof grid.width === "number" && typeof grid.height === "number";
26
+ }
27
+
28
+ function panelsAreRegistered(panels: Record<string, unknown>): boolean {
29
+ const ids = Object.keys(panels);
30
+ return (
31
+ ids.length > 0 &&
32
+ ids.every((id) => {
33
+ const state = panels[id];
34
+ return (
35
+ isPanelId(id) &&
36
+ isRecord(state) &&
37
+ state.id === id &&
38
+ state.contentComponent === DOCK_PANEL_COMPONENT
39
+ );
40
+ })
41
+ );
42
+ }
43
+
44
+ function collectFloatingViews(floating: unknown, into: unknown[]): boolean {
45
+ if (floating === undefined) return true;
46
+ if (!Array.isArray(floating)) return false;
47
+ return floating.every((group) => {
48
+ if (!isRecord(group) || !isRecord(group.data) || !Array.isArray(group.data.views)) return false;
49
+ into.push(...group.data.views);
50
+ return true;
51
+ });
52
+ }
53
+
54
+ /** Parses a stored layout at the trust boundary; null (stale or foreign shape) means use the default preset. */
55
+ export function parseDockLayout(value: unknown): SerializedDockview | null {
56
+ if (!isRecord(value) || !isRecord(value.grid) || !isRecord(value.panels)) return null;
57
+ const { grid, panels } = value;
58
+ if (!hasGridShape(grid) || !panelsAreRegistered(panels)) return null;
59
+
60
+ const views: unknown[] = [];
61
+ if (!collectLeafViews(grid.root, views)) return null;
62
+ if (!collectFloatingViews(value.floatingGroups, views)) return null;
63
+ const everyViewIsAPanel = views.every((id) => typeof id === "string" && id in panels);
64
+ const everyPanelIsPlaced = Object.keys(panels).every((id) => views.includes(id));
65
+ return everyViewIsAPanel && everyPanelIsPlaced ? (value as unknown as SerializedDockview) : null;
66
+ }
@@ -0,0 +1,91 @@
1
+ import { create } from "zustand";
2
+ import {
3
+ PANEL_DEFINITIONS,
4
+ PANEL_IDS,
5
+ panelsInZone,
6
+ type PanelId,
7
+ type PanelZone,
8
+ } from "./panelRegistry";
9
+
10
+ /** The narrow surface Dock.Root exposes; dockview types never leave the dock folder. */
11
+ export interface DockController {
12
+ /** Adds the panel at its default place if it is closed; never moves focus. */
13
+ open: (id: PanelId) => void;
14
+ /** Focuses the panel, reopening it at its default place if it was closed. */
15
+ activate: (id: PanelId) => void;
16
+ setTitle: (id: PanelId, title: string) => void;
17
+ close: (id: PanelId) => void;
18
+ /** Hides or shows the panel's whole tab group without closing it. */
19
+ setGroupVisible: (id: PanelId, visible: boolean) => void;
20
+ reset: () => void;
21
+ }
22
+
23
+ export interface DockSnapshot {
24
+ openPanels: ReadonlySet<PanelId>;
25
+ /** Open panels whose tab is showing and whose group is not hidden. */
26
+ visiblePanels: ReadonlySet<PanelId>;
27
+ activePanel: PanelId | null;
28
+ }
29
+
30
+ type LastActive = Partial<Record<PanelZone, PanelId>>;
31
+
32
+ interface DockLayoutState extends DockSnapshot {
33
+ controller: DockController | null;
34
+ lastActive: LastActive;
35
+ /** An activation requested before the dock mounted; Dock.Root applies it on ready. */
36
+ pendingActivation: PanelId | null;
37
+ attach: (controller: DockController) => void;
38
+ detach: () => void;
39
+ sync: (snapshot: DockSnapshot) => void;
40
+ takePendingActivation: () => PanelId | null;
41
+ activatePanel: (id: PanelId) => void;
42
+ closePanel: (id: PanelId) => void;
43
+ togglePanel: (id: PanelId) => void;
44
+ setZoneVisible: (zone: PanelZone, visible: boolean) => void;
45
+ resetLayout: () => void;
46
+ }
47
+
48
+ export const useDockLayoutStore = create<DockLayoutState>((set, get) => ({
49
+ controller: null,
50
+ openPanels: new Set(PANEL_IDS),
51
+ visiblePanels: new Set(PANEL_IDS),
52
+ activePanel: null,
53
+ lastActive: {},
54
+ pendingActivation: null,
55
+ attach: (controller) => set({ controller }),
56
+ detach: () => set({ controller: null }),
57
+ sync: (snapshot) =>
58
+ set((state) => {
59
+ const { activePanel } = snapshot;
60
+ if (!activePanel) return snapshot;
61
+ const zone = PANEL_DEFINITIONS[activePanel].zone;
62
+ return { ...snapshot, lastActive: { ...state.lastActive, [zone]: activePanel } };
63
+ }),
64
+ takePendingActivation: () => {
65
+ const { pendingActivation } = get();
66
+ set({ pendingActivation: null });
67
+ return pendingActivation;
68
+ },
69
+ activatePanel: (id) => {
70
+ const { controller } = get();
71
+ if (!controller) {
72
+ set({ pendingActivation: id });
73
+ return;
74
+ }
75
+ controller.setGroupVisible(id, true);
76
+ controller.activate(id);
77
+ },
78
+ closePanel: (id) => get().controller?.close(id),
79
+ togglePanel: (id) => {
80
+ const { openPanels, activatePanel, closePanel } = get();
81
+ if (openPanels.has(id)) closePanel(id);
82
+ else activatePanel(id);
83
+ },
84
+ setZoneVisible: (zone, visible) => {
85
+ const { controller, openPanels } = get();
86
+ for (const id of panelsInZone(zone)) {
87
+ if (openPanels.has(id)) controller?.setGroupVisible(id, visible);
88
+ }
89
+ },
90
+ resetLayout: () => get().controller?.reset(),
91
+ }));
@@ -0,0 +1,11 @@
1
+ import { createDockview, type DockviewApi } from "dockview-react";
2
+
3
+ /** A real dockview mounted on a fresh host element, with empty panel bodies. */
4
+ export function mountBareDockview(): { host: HTMLElement; api: DockviewApi } {
5
+ const host = document.createElement("div");
6
+ document.body.append(host);
7
+ const api = createDockview(host, {
8
+ createComponent: () => ({ element: document.createElement("div"), init() {}, dispose() {} }),
9
+ });
10
+ return { host, api };
11
+ }
@@ -0,0 +1,67 @@
1
+ import type { Direction } from "dockview-react";
2
+
3
+ export const PANEL_IDS = [
4
+ "preview",
5
+ "timeline",
6
+ "compositions",
7
+ "assets",
8
+ "code",
9
+ "catalog",
10
+ "design",
11
+ "layers",
12
+ "renders",
13
+ "variables",
14
+ "slideshow",
15
+ ] as const;
16
+
17
+ export type PanelId = (typeof PANEL_IDS)[number];
18
+ export type PanelZone = "left" | "center" | "right";
19
+
20
+ export interface PanelDefinition {
21
+ title: string;
22
+ zone: PanelZone;
23
+ /** Where Window > <panel> puts it when it has no saved place: next to `near`. */
24
+ reopen: { near: PanelId; direction: Direction };
25
+ /** Content stays mounted while its tab is hidden (the preview iframe must not reload). */
26
+ keepMounted?: true;
27
+ }
28
+
29
+ export const PANEL_DEFINITIONS = {
30
+ preview: {
31
+ title: "Preview",
32
+ zone: "center",
33
+ reopen: { near: "timeline", direction: "above" },
34
+ keepMounted: true,
35
+ },
36
+ timeline: {
37
+ title: "Timeline",
38
+ zone: "center",
39
+ reopen: { near: "preview", direction: "below" },
40
+ keepMounted: true,
41
+ },
42
+ compositions: {
43
+ title: "Compositions",
44
+ zone: "left",
45
+ reopen: { near: "preview", direction: "left" },
46
+ },
47
+ assets: { title: "Assets", zone: "left", reopen: { near: "compositions", direction: "within" } },
48
+ code: { title: "Code", zone: "left", reopen: { near: "compositions", direction: "within" } },
49
+ catalog: {
50
+ title: "Catalog",
51
+ zone: "left",
52
+ reopen: { near: "compositions", direction: "within" },
53
+ },
54
+ design: { title: "Design", zone: "right", reopen: { near: "preview", direction: "right" } },
55
+ layers: { title: "Layers", zone: "right", reopen: { near: "design", direction: "within" } },
56
+ renders: { title: "Renders", zone: "right", reopen: { near: "design", direction: "within" } },
57
+ variables: { title: "Variables", zone: "right", reopen: { near: "design", direction: "within" } },
58
+ slideshow: { title: "Slideshow", zone: "right", reopen: { near: "design", direction: "within" } },
59
+ } as const satisfies Record<PanelId, PanelDefinition>;
60
+
61
+ export function isPanelId(value: unknown): value is PanelId {
62
+ return typeof value === "string" && (PANEL_IDS as readonly string[]).includes(value);
63
+ }
64
+
65
+ export function panelsInZone(zone: PanelZone): PanelId[] {
66
+ return PANEL_IDS.filter((id) => PANEL_DEFINITIONS[id].zone === zone);
67
+ }
@@ -172,7 +172,7 @@ export const AnimationCard = memo(function AnimationCard({
172
172
  return (
173
173
  <div className="border-b border-neutral-800 pb-2">
174
174
  <div className="flex items-center gap-2 py-1.5">
175
- <span className="rounded bg-neutral-800 px-1.5 py-0.5 text-[10px] font-medium text-neutral-400">
175
+ <span className="rounded-sm bg-neutral-800 px-1.5 py-0.5 text-[10px] font-medium text-neutral-400">
176
176
  Position
177
177
  </span>
178
178
  <span className="text-[11px] text-neutral-500">
@@ -201,7 +201,7 @@ export const AnimationCard = memo(function AnimationCard({
201
201
  className="flex w-full items-center gap-2 py-1.5 active:scale-[0.99]"
202
202
  >
203
203
  <span
204
- className="rounded bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
204
+ className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
205
205
  title={METHOD_TOOLTIPS[animation.method]}
206
206
  >
207
207
  {methodLabel}
@@ -226,7 +226,7 @@ export const AnimationCard = memo(function AnimationCard({
226
226
  height="10"
227
227
  viewBox="0 0 10 10"
228
228
  fill="currentColor"
229
- className={`flex-shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
229
+ className={`shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
230
230
  >
231
231
  <path d="M2 3l3 4 3-4z" />
232
232
  </svg>
@@ -262,7 +262,7 @@ export const AnimationCard = memo(function AnimationCard({
262
262
  setCopied(true);
263
263
  setTimeout(() => setCopied(false), 1500);
264
264
  }}
265
- className="flex-shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
265
+ className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
266
266
  title="Copy description to clipboard — paste into agent prompts"
267
267
  >
268
268
  {copied ? "Copied" : "Copy"}
@@ -46,7 +46,7 @@ function RemoveButton({ onClick, title }: { onClick: () => void; title: string }
46
46
  <button
47
47
  type="button"
48
48
  onClick={onClick}
49
- className="relative flex-shrink-0 rounded p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400 active:scale-[0.95]"
49
+ className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400 active:scale-[0.95]"
50
50
  title={title}
51
51
  aria-label={title}
52
52
  >
@@ -91,7 +91,7 @@ export function PropertyRow({
91
91
  role="switch"
92
92
  aria-checked={isVisible}
93
93
  onClick={() => onCommit(isVisible ? "hidden" : "visible")}
94
- className="flex-shrink-0 rounded-full transition-colors duration-200 relative"
94
+ className="shrink-0 rounded-full transition-colors duration-200 relative"
95
95
  style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
96
96
  title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
97
97
  >
@@ -117,13 +117,13 @@ export function PropertyRow({
117
117
  <div className="flex flex-col gap-1">
118
118
  <div className="flex items-center gap-1">
119
119
  <div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
120
- <span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">
120
+ <span className="shrink-0 text-[11px] font-medium text-neutral-500">
121
121
  {PROP_LABELS[prop] ?? prop}
122
122
  </span>
123
123
  <input
124
124
  type="text"
125
125
  defaultValue={String(val)}
126
- className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-none"
126
+ className="flex-1 bg-transparent text-[11px] text-neutral-200 outline-hidden"
127
127
  onBlur={(e) => onCommit(e.currentTarget.value)}
128
128
  onKeyDown={(e) => {
129
129
  if (e.key === "Enter") e.currentTarget.blur();
@@ -139,7 +139,7 @@ export function PropertyRow({
139
139
  key={p.value}
140
140
  type="button"
141
141
  onClick={() => onCommit(p.value)}
142
- className="px-1.5 py-0.5 rounded text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
142
+ className="px-1.5 py-0.5 rounded-sm text-[9px] font-medium text-neutral-500 bg-neutral-800/50 hover:bg-neutral-800 hover:text-neutral-300 transition-colors"
143
143
  >
144
144
  {p.label}
145
145
  </button>
@@ -190,7 +190,7 @@ export function AddPropertyTrigger({
190
190
  return (
191
191
  <select
192
192
  autoFocus
193
- className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-none"
193
+ className="min-w-0 rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-[11px] text-neutral-100 outline-hidden"
194
194
  defaultValue=""
195
195
  onChange={(e) => {
196
196
  if (e.target.value) onAdd(e.target.value);
@@ -222,7 +222,7 @@ function ParamControl({
222
222
  disabled={disabled}
223
223
  aria-label={`${param.label} color`}
224
224
  onChange={(e) => onChange(e.target.value)}
225
- className="w-7 h-7 rounded border border-neutral-700 bg-transparent cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
225
+ className="w-7 h-7 rounded-sm border border-neutral-700 bg-transparent cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
226
226
  />
227
227
  <input
228
228
  type="text"
@@ -230,7 +230,7 @@ function ParamControl({
230
230
  disabled={disabled}
231
231
  aria-label={`${param.label} value`}
232
232
  onChange={(e) => onChange(e.target.value)}
233
- className="flex-1 bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 font-mono focus:outline-none focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
233
+ className="flex-1 bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 font-mono focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
234
234
  />
235
235
  </div>
236
236
  )}
@@ -259,7 +259,7 @@ function ParamControl({
259
259
  disabled={disabled}
260
260
  aria-label={param.label}
261
261
  onChange={(e) => onChange(e.target.value)}
262
- className="w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-none focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
262
+ className="w-full bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
263
263
  />
264
264
  )}
265
265
 
@@ -269,7 +269,7 @@ function ParamControl({
269
269
  disabled={disabled}
270
270
  aria-label={param.label}
271
271
  onChange={(e) => onChange(e.target.value)}
272
- className="w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-none focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
272
+ className="w-full bg-neutral-900 border border-neutral-800 rounded-sm px-2 py-1 text-[10px] text-neutral-200 focus:outline-hidden focus:border-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
273
273
  >
274
274
  {param.options.map((opt) => (
275
275
  <option key={opt.value} value={opt.value}>
@@ -71,7 +71,7 @@ export function BorderRadiusEditor({
71
71
  width={PREVIEW_W}
72
72
  height={PREVIEW_H}
73
73
  viewBox={`0 0 ${PREVIEW_W} ${PREVIEW_H}`}
74
- className="flex-shrink-0"
74
+ className="shrink-0"
75
75
  >
76
76
  <path
77
77
  d={path}
@@ -212,7 +212,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
212
212
  return createPortal(
213
213
  <div
214
214
  ref={menuRef}
215
- className="fixed z-[200] bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
215
+ className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
216
216
  style={{ left: adjustedX, top: adjustedY }}
217
217
  onPointerDown={stopBubble}
218
218
  onMouseDown={stopBubble}
@@ -29,7 +29,7 @@ export function ComputedTweenNotice({
29
29
  <button
30
30
  type="button"
31
31
  onClick={onUnroll}
32
- className="flex-shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium text-panel-accent hover:bg-neutral-800"
32
+ className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-panel-accent hover:bg-neutral-800"
33
33
  title="Rewrite the helper/loop into explicit tweens so this keyframe edits directly"
34
34
  >
35
35
  Unroll to edit
@@ -465,7 +465,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
465
465
  ref={overlayRef}
466
466
  // Standing aside is the only way the caret below can be reached, and is
467
467
  // what keeps selection, drag and marquee from firing mid-edit.
468
- className={`absolute inset-0 z-10 outline-none ${
468
+ className={`absolute inset-0 z-10 outline-hidden ${
469
469
  inlineText.editing ? "pointer-events-none" : "pointer-events-auto"
470
470
  }`}
471
471
  data-editing-text={inlineText.editing ? "true" : undefined}
@@ -518,7 +518,7 @@ export function EaseCurveSection({
518
518
  aria-valuemax={1}
519
519
  aria-valuenow={key === "p1" ? x1 : x2}
520
520
  aria-valuetext={`x ${key === "p1" ? x1 : x2}, y ${key === "p1" ? y1 : y2}`}
521
- className="cursor-grab stroke-transparent outline-none active:cursor-grabbing focus-visible:stroke-white focus-visible:stroke-[2px]"
521
+ className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-white focus-visible:stroke-[2px]"
522
522
  onPointerDown={(e) => handlePointerDown(key, e)}
523
523
  onKeyDown={(event) => handleKeyDown(key, event)}
524
524
  onPointerEnter={() => setHover(key)}