@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,109 @@
1
+ // The token gate (R11, AE1). Tailwind silently drops a class it cannot compile, so `rounded-button`
2
+ // renders as no radius and nothing goes red. This compiles Studio's real entry stylesheet with every
3
+ // class the source claims and reports any candidate producing no selector as `file: class`.
4
+ // Tailwind is the only judge, so there is no allowlist; a failing class is fixed, never excused.
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { compile } from "tailwindcss";
9
+ import { describe, expect, it } from "vitest";
10
+ import { extractClassCandidates } from "./classCandidates";
11
+ import { listSourceFiles, loadStylesheet, STYLES_DIR } from "./styleSources";
12
+
13
+ // A class selector in the emitted CSS, escapes removed: `.hover\:bg-surface\/50` reads back as its candidate.
14
+ const CLASS_SELECTOR = /\.((?:\\.|[^\s.,{}()>+~:[\]#'"*/\\])+)/g;
15
+
16
+ // Tailwind's `group`/`peer` variant markers (`group/card`) produce no rule of their own, so no
17
+ // compiled selector can vouch for them. Nothing else is exempt.
18
+ const MARKERS = new Set(["group", "peer"]);
19
+
20
+ // `hf-` is HyperFrames' reserved prefix for semantic hooks, not utilities, so Tailwind is the
21
+ // wrong judge of them.
22
+ const HOOK_PREFIX = /^hf-/;
23
+
24
+ /** Compile Studio's stylesheet and return every class it can produce. */
25
+ async function resolvable(candidates: string[]): Promise<Set<string>> {
26
+ const compiled = await compile(readFileSync(path.join(STYLES_DIR, "studio.css"), "utf8"), {
27
+ base: STYLES_DIR,
28
+ loadStylesheet,
29
+ });
30
+ const css = compiled.build(candidates);
31
+ const selectors = new Set<string>();
32
+ for (const [, selector] of css.matchAll(CLASS_SELECTOR)) {
33
+ selectors.add(selector.replace(/\\(.)/g, "$1"));
34
+ }
35
+ return selectors;
36
+ }
37
+
38
+ // `file: class` for every class the sources claim that Tailwind cannot make. Sources are passed
39
+ // in so the fixtures exercise the same code path as the tree.
40
+ async function unresolved(sources: ReadonlyMap<string, string>): Promise<string[]> {
41
+ const claims = new Map<string, string[]>();
42
+ for (const [file, source] of sources) {
43
+ for (const candidate of extractClassCandidates(source)) {
44
+ const seen = claims.get(candidate.base);
45
+ if (seen) seen.push(file);
46
+ else claims.set(candidate.base, [file]);
47
+ }
48
+ }
49
+ const produced = await resolvable([...claims.keys()]);
50
+ const failures: string[] = [];
51
+ for (const [candidate, files] of claims) {
52
+ if (produced.has(candidate) || MARKERS.has(candidate.split("/")[0])) continue;
53
+ if (HOOK_PREFIX.test(candidate)) continue;
54
+ for (const file of files) failures.push(`${file}: ${candidate}`);
55
+ }
56
+ return failures.sort();
57
+ }
58
+
59
+ // Every file the gate reads, mirroring the `@source` globs in `studio.css`. Tests are excluded:
60
+ // a fixture in a test is not markup.
61
+ function studioSources(): Map<string, string> {
62
+ return listSourceFiles((file) => /\.tsx?$/.test(file) && !/\.test\.tsx?$/.test(file));
63
+ }
64
+
65
+ describe("token gate", () => {
66
+ it("names the file and the class when a class resolves to nothing", async () => {
67
+ // AE1: `rounded-hologram` looks like a token name but is defined nowhere. The map is named
68
+ // `buttonSizes`, not `sizeStyles`: the `cn()` that consumes it puts it in front of the gate.
69
+ const failures = await unresolved(
70
+ new Map([
71
+ [
72
+ "ui/Button.tsx",
73
+ `const buttonSizes = { md: "h-8 rounded-hologram text-mega" };
74
+ const Button = ({ size }) => <button className={cn(buttonSizes[size])} />;`,
75
+ ],
76
+ ]),
77
+ );
78
+
79
+ expect(failures).toEqual(["ui/Button.tsx: rounded-hologram", "ui/Button.tsx: text-mega"]);
80
+ });
81
+
82
+ it("accepts an arbitrary value and reports it as one", async () => {
83
+ const source = `<div className="text-[11px]" />`;
84
+
85
+ expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
86
+ expect(extractClassCandidates(source).filter((c) => c.arbitrary)).toHaveLength(1);
87
+ });
88
+
89
+ it("accepts variants, static utilities and Studio's own CSS classes", async () => {
90
+ // `timeline-clip` is a plain rule in `studio.css`, not a utility.
91
+ const source = `<div className="hover:bg-surface/50 text-center border-dashed timeline-clip" />`;
92
+
93
+ expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
94
+ });
95
+
96
+ it("leaves Studio's own hook prefix and Tailwind's variant markers alone", async () => {
97
+ const source = `<div className="group group/card peer hf-fx-node" />`;
98
+
99
+ expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
100
+ });
101
+
102
+ it("claims nothing for a template chunk that touches an interpolation", async () => {
103
+ expect(await unresolved(new Map([["a.tsx", "<div className={`w-${i}`} />"]]))).toEqual([]);
104
+ });
105
+
106
+ it("resolves every class Studio's own source claims", async () => {
107
+ expect(await unresolved(studioSources())).toEqual([]);
108
+ });
109
+ });
@@ -3,7 +3,7 @@
3
3
  * Handles inline style updates, attribute changes, and text content.
4
4
  */
5
5
 
6
- function escapeRegex(s: string): string {
6
+ export function escapeRegex(s: string): string {
7
7
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
8
  }
9
9
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
4
  import {
5
+ historyTooltipLabel,
5
6
  findMatchingTimelineElementId,
6
7
  findTimelineIdByAncestor,
7
8
  resolveDroppedAssetDimensions,
@@ -217,3 +218,20 @@ describe("resolveElementTrack", () => {
217
218
  expect(resolveElementTrack({ authoredTrack: undefined, track: 3.6 })).toBe(4);
218
219
  });
219
220
  });
221
+
222
+ describe("historyTooltipLabel", () => {
223
+ // A disabled control gets no pointer events, so its tooltip cannot be opened
224
+ // to read. The wording is checked here instead of through the DOM.
225
+ it("names the action that would be undone", () => {
226
+ expect(historyTooltipLabel("undo", "Move layer")).toMatch(/^Undo Move layer \(.+\)$/);
227
+ });
228
+
229
+ it("names the action that would be redone", () => {
230
+ expect(historyTooltipLabel("redo", "Move layer")).toMatch(/^Redo Move layer \(.+\)$/);
231
+ });
232
+
233
+ it("keeps the shortcut when the history is empty", () => {
234
+ expect(historyTooltipLabel("undo", undefined)).toMatch(/^Undo \(.+\)$/);
235
+ expect(historyTooltipLabel("redo", null)).toMatch(/^Redo \(.+\)$/);
236
+ });
237
+ });
@@ -119,13 +119,23 @@ export function shouldIgnoreHistoryShortcut(target: EventTarget | null): boolean
119
119
  return isTypingTarget(target);
120
120
  }
121
121
 
122
- export function getHistoryShortcutLabel(action: "undo" | "redo"): string {
122
+ function getHistoryShortcutLabel(action: "undo" | "redo"): string {
123
123
  const isMac =
124
124
  typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
125
125
  const modifier = isMac ? "Cmd" : "Ctrl";
126
126
  return action === "undo" ? `${modifier}+Z` : `${modifier}+Shift+Z`;
127
127
  }
128
128
 
129
+ /** The Undo / Redo tooltip: the shortcut always, the last action's name when there is one. */
130
+ export function historyTooltipLabel(
131
+ action: "undo" | "redo",
132
+ lastAction: string | null | undefined,
133
+ ): string {
134
+ const shortcut = getHistoryShortcutLabel(action);
135
+ const verb = action === "undo" ? "Undo" : "Redo";
136
+ return lastAction ? `${verb} ${lastAction} (${shortcut})` : `${verb} (${shortcut})`;
137
+ }
138
+
129
139
  export type ElementMatchSelection = Pick<
130
140
  DomEditSelection,
131
141
  | "id"
@@ -1,3 +1,6 @@
1
+ import type { SerializedDockview } from "dockview-react";
2
+ import { parseDockLayout } from "../components/dock/dockLayoutSchema";
3
+
1
4
  export interface StoredPreviewZoomState {
2
5
  zoomPercent: number;
3
6
  panX: number;
@@ -47,6 +50,8 @@ export interface StudioUiPreferences {
47
50
  * intentionally scoped to one mount.
48
51
  */
49
52
  agentToolsEnabled?: boolean;
53
+ /** The dock's serialized panel tree; parsed by `parseDockLayout` on read. */
54
+ dockLayout?: SerializedDockview;
50
55
  }
51
56
 
52
57
  const STUDIO_UI_PREFERENCES_KEY = "hf-studio-ui-preferences";
@@ -169,6 +174,8 @@ function readStorage(storage: Storage | null, key: string): StudioUiPreferences
169
174
  if (typeof parsed.agentToolsEnabled === "boolean") {
170
175
  preferences.agentToolsEnabled = parsed.agentToolsEnabled;
171
176
  }
177
+ const dockLayout = parseDockLayout(parsed.dockLayout);
178
+ if (dockLayout) preferences.dockLayout = dockLayout;
172
179
  return preferences;
173
180
  } catch {
174
181
  return {};
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { TimelineElement } from "../player";
3
+ import {
4
+ applyTrackRenumbers,
5
+ planDropTrackInsert,
6
+ resolveDropTrack,
7
+ } from "./timelineDropTrackInsert";
8
+
9
+ function clip(id: string, track: number, start: number): TimelineElement {
10
+ return {
11
+ id,
12
+ key: id,
13
+ tag: "div",
14
+ start,
15
+ duration: 2,
16
+ track,
17
+ authoredTrack: track,
18
+ hfId: `hf-${id}`,
19
+ domId: id,
20
+ // Main-document elements carry no sourceFile, as in production.
21
+ };
22
+ }
23
+
24
+ const trackOrder = [0, 1, 2];
25
+ const dropped = { id: "new", tag: "img", start: 5, duration: 3 };
26
+ const elements = [clip("a", 0, 0), clip("b", 1, 0), clip("c", 2, 0)];
27
+ const source = elements
28
+ .map(
29
+ (e) =>
30
+ `<div data-hf-id="${e.hfId}" id="${e.id}" data-start="0" data-track-index="${e.track}"></div>`,
31
+ )
32
+ .join("\n");
33
+
34
+ describe("planDropTrackInsert", () => {
35
+ it("opens a lane between two rows and pushes the lanes below down", () => {
36
+ const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
37
+ expect(plan?.track).toBe(1);
38
+ expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
39
+ ["b", 2],
40
+ ["c", 3],
41
+ ]);
42
+ });
43
+
44
+ it("opens a lane above the first row", () => {
45
+ const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 0, dropped });
46
+ expect(plan?.track).toBe(0);
47
+ expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
48
+ ["a", 1],
49
+ ["b", 2],
50
+ ["c", 3],
51
+ ]);
52
+ });
53
+
54
+ it("still renumbers when the peers' own sourceFile is a real path, not undefined", () => {
55
+ const subComp = elements.map((e) => ({ ...e, sourceFile: "sub.html" }));
56
+ const plan = planDropTrackInsert({ elements: subComp, trackOrder, insertRow: 1, dropped });
57
+ expect(plan?.track).toBe(1);
58
+ expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
59
+ ["b", 2],
60
+ ["c", 3],
61
+ ]);
62
+ });
63
+ });
64
+
65
+ describe("planDropTrackInsert against the display row order", () => {
66
+ it("counts an audio-group anchor row the way a clip drag does", () => {
67
+ // Rows shown: group anchor (-0.5), track 0, track 1. Row boundary 2 is between 0 and 1.
68
+ const grouped = elements.slice(0, 2).map((e) => (e.id === "a" ? { ...e, audioGroup: "g" } : e));
69
+ const plan = planDropTrackInsert({
70
+ elements: grouped,
71
+ trackOrder: [-0.5, 0, 1],
72
+ insertRow: 2,
73
+ dropped,
74
+ });
75
+ expect(plan?.track).toBe(1);
76
+ expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([["b", 2]]);
77
+ });
78
+ });
79
+
80
+ describe("applyTrackRenumbers", () => {
81
+ it("rewrites only the renumbered clips' data-track-index", () => {
82
+ const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
83
+ const out = applyTrackRenumbers(source, plan!);
84
+ const tracks = [...out.matchAll(/id="(\w)"[^>]*data-track-index="(\d+)"/g)].map((m) => [
85
+ m[1],
86
+ +m[2],
87
+ ]);
88
+ expect(tracks).toEqual([
89
+ ["a", 0],
90
+ ["b", 2],
91
+ ["c", 3],
92
+ ]);
93
+ });
94
+
95
+ it("throws when a clip's opening tag is missing", () => {
96
+ const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
97
+ expect(() => applyTrackRenumbers("<div></div>", plan!)).toThrow(/Cannot renumber/);
98
+ });
99
+ });
100
+
101
+ describe("resolveDropTrack", () => {
102
+ it("keeps the aimed lane and the source when no insert is asked for", () => {
103
+ const out = resolveDropTrack({
104
+ source,
105
+ elements,
106
+ placement: { track: 2 },
107
+ dropped,
108
+ });
109
+ expect(out).toEqual({ source, track: 2 });
110
+ });
111
+
112
+ it("returns the new lane and the source with the lanes below pushed down", () => {
113
+ const out = resolveDropTrack({
114
+ source,
115
+ elements,
116
+ placement: { track: 1, insertRow: 1, trackOrder },
117
+ dropped,
118
+ });
119
+ expect(out.track).toBe(1);
120
+ expect(out.source).toContain('id="b" data-start="0" data-track-index="2"');
121
+ });
122
+
123
+ it("refuses to insert when a locked clip would have to move", () => {
124
+ const locked = elements.map((e) => (e.id === "b" ? { ...e, timelineLocked: true } : e));
125
+ expect(() =>
126
+ resolveDropTrack({
127
+ source,
128
+ elements: locked,
129
+ placement: { track: 1, insertRow: 1, trackOrder },
130
+ dropped,
131
+ }),
132
+ ).toThrow(/locked/);
133
+ });
134
+ });
@@ -0,0 +1,83 @@
1
+ import type { TimelineElement } from "../player";
2
+ import { layoutAfterTrackInsert } from "../player/components/timelineDragLanding";
3
+ import { canMoveTimelineElement } from "../player/components/timelineAuthoredMoveTarget";
4
+ import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
5
+ import { applyPatchByTarget, readAttributeByTarget } from "./sourcePatcher";
6
+ import { buildPatchTarget } from "../hooks/timelineEditingHelpers";
7
+ import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
8
+
9
+ export interface DropTrackInsertPlan {
10
+ /** Lane the dropped clip is written on. */
11
+ track: number;
12
+ /** Existing clips whose lane changes to make room. */
13
+ renumbers: Array<{ element: TimelineElement; track: number }>;
14
+ }
15
+
16
+ /** Plan a new lane at boundary `insertRow` with the renumber a clip drag into a gutter uses.
17
+ * Null when a locked clip would have to move. */
18
+ export function planDropTrackInsert(input: {
19
+ elements: TimelineElement[];
20
+ /** The row order the timeline shows (anchor rows included), which `insertRow` indexes. */
21
+ trackOrder: readonly number[];
22
+ insertRow: number;
23
+ dropped: Pick<TimelineElement, "id" | "tag" | "start" | "duration">;
24
+ }): DropTrackInsertPlan | null {
25
+ const { elements, trackOrder, insertRow, dropped } = input;
26
+ const newElement: TimelineElement = {
27
+ ...dropped,
28
+ key: dropped.id,
29
+ // Parked on an existing lane so it adds no lane of its own to the topology.
30
+ track: elements[0]?.track ?? 0,
31
+ // sameSourceFile compares this raw field: borrow the peers' value, not the resolved path.
32
+ sourceFile: elements[0]?.sourceFile,
33
+ };
34
+ const layout = layoutAfterTrackInsert(newElement, dropped.start, insertRow, null, {
35
+ elements: [...elements, newElement],
36
+ trackOrder: [...trackOrder],
37
+ });
38
+ if (!layout) return null;
39
+ const byKey = new Map(elements.map((e) => [e.key ?? e.id, e]));
40
+ const renumbers: DropTrackInsertPlan["renumbers"] = [];
41
+ for (const norm of layout.normalized) {
42
+ const key = norm.key ?? norm.id;
43
+ if (key === dropped.id) continue;
44
+ const src = byKey.get(key);
45
+ if (!src || norm.track === (src.authoredTrack ?? src.track)) continue;
46
+ if (!canMoveTimelineElement(src)) return null;
47
+ renumbers.push({ element: src, track: norm.track });
48
+ }
49
+ const track = layout.normalized.find((n) => (n.key ?? n.id) === dropped.id)?.track;
50
+ return track == null ? null : { track, renumbers };
51
+ }
52
+
53
+ /** Rewrite `data-track-index` on each renumbered clip's opening tag, via the shared source patcher. */
54
+ export function applyTrackRenumbers(source: string, plan: DropTrackInsertPlan): string {
55
+ let out = source;
56
+ for (const { element, track } of plan.renumbers) {
57
+ const target = buildPatchTarget(element);
58
+ if (!target || readAttributeByTarget(out, target, "track-index") === undefined) {
59
+ throw new Error(`Cannot renumber the track of "${element.id}" in the source`);
60
+ }
61
+ out = applyPatchByTarget(out, target, {
62
+ type: "attribute",
63
+ property: "track-index",
64
+ value: formatTimelineAttributeNumber(track),
65
+ });
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** The lane a dropped clip is written on and the source with any lanes pushed down to make room. */
71
+ export function resolveDropTrack(input: {
72
+ source: string;
73
+ elements: TimelineElement[];
74
+ placement: TimelineDropPlacement;
75
+ dropped: Pick<TimelineElement, "id" | "tag" | "start" | "duration">;
76
+ }): { source: string; track: number } {
77
+ const { source, elements, placement, dropped } = input;
78
+ if (placement.insertRow == null) return { source, track: placement.track };
79
+ const { insertRow, trackOrder } = placement;
80
+ const plan = planDropTrackInsert({ elements, trackOrder, insertRow, dropped });
81
+ if (!plan) throw new Error("Cannot open a new track here: a locked clip would have to move.");
82
+ return { source: applyTrackRenumbers(source, plan), track: plan.track };
83
+ }
@@ -33,6 +33,10 @@ const TYPING_SELECTOR = [
33
33
  "[role='textbox']",
34
34
  "[role='searchbox']",
35
35
  "[role='combobox']",
36
+ // A switch is a button in the DOM, but the native checkbox it replaces was an
37
+ // `input` and matched above. Without this row the shared Toggle would let a
38
+ // global shortcut claim Space instead of flipping the control (KTD13).
39
+ "[role='switch']",
36
40
  ".cm-editor",
37
41
  ].join(",");
38
42