@hyperframes/studio 0.8.50 → 0.8.51

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 (215) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-Cn_PpHqP.js} +3 -3
  2. package/dist/assets/index-C05SNrvY.js +360 -0
  3. package/dist/assets/{index-DUAIWxkI.js → index-CPS3Cvj8.js} +1 -1
  4. package/dist/assets/{index-Dt3qBI-9.js → index-Clrb11lj.js} +1 -1
  5. package/dist/assets/index-DUR9qzQM.css +1 -0
  6. package/dist/index.d.ts +18 -13
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +1867 -3335
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +11 -12
  12. package/src/App.tsx +128 -141
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  15. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  16. package/src/captions/components/CaptionTimeline.tsx +1 -1
  17. package/src/captions/components/shared.tsx +2 -2
  18. package/src/components/AskAgentModal.tsx +5 -5
  19. package/src/components/EditorShell.tsx +4 -7
  20. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  21. package/src/components/LintModal.tsx +6 -16
  22. package/src/components/MediaPreview.tsx +2 -2
  23. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  24. package/src/components/StudioHeader.tsx +2 -62
  25. package/src/components/StudioLeftSidebar.tsx +7 -4
  26. package/src/components/StudioOverlays.tsx +1 -1
  27. package/src/components/StudioRightPanel.tsx +4 -4
  28. package/src/components/StudioToast.tsx +2 -2
  29. package/src/components/TimelineToolbar.tsx +7 -7
  30. package/src/components/dock/Dock.test.tsx +192 -0
  31. package/src/components/dock/Dock.tsx +264 -0
  32. package/src/components/dock/dock.css +23 -0
  33. package/src/components/dock/dockLayout.ts +58 -0
  34. package/src/components/dock/dockLayoutSchema.ts +66 -0
  35. package/src/components/dock/dockLayoutStore.ts +91 -0
  36. package/src/components/dock/panelRegistry.ts +67 -0
  37. package/src/components/editor/AnimationCard.tsx +4 -4
  38. package/src/components/editor/AnimationCardParts.tsx +6 -6
  39. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  40. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  41. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  42. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  43. package/src/components/editor/DomEditOverlay.tsx +1 -1
  44. package/src/components/editor/EaseCurveSection.tsx +1 -1
  45. package/src/components/editor/EaseParamFields.tsx +5 -5
  46. package/src/components/editor/FileTree.tsx +4 -4
  47. package/src/components/editor/FileTreeIcons.tsx +1 -1
  48. package/src/components/editor/FileTreeNodes.tsx +13 -13
  49. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  50. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  51. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  52. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  53. package/src/components/editor/LayersPanel.tsx +3 -3
  54. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  55. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  56. package/src/components/editor/PromotableControl.tsx +2 -2
  57. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  58. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  59. package/src/components/editor/SnapToolbar.tsx +2 -2
  60. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  61. package/src/components/editor/TopologyLens.tsx +4 -4
  62. package/src/components/editor/Transform3DCube.tsx +2 -2
  63. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  64. package/src/components/editor/domEditing.test.ts +1 -1
  65. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  66. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  67. package/src/components/editor/propertyPanelColor.tsx +11 -11
  68. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  69. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  70. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  71. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  72. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  73. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  74. package/src/components/editor/propertyPanelFill.tsx +4 -4
  75. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  76. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  77. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  78. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  79. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  80. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  81. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  82. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  83. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  84. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  85. package/src/components/editor/propertyPanelFont.tsx +5 -5
  86. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  87. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  88. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  89. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  90. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  91. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  92. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  93. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  94. package/src/components/editor/propertyPanelSections.tsx +8 -8
  95. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  96. package/src/components/feedback/feedbackTrigger.ts +0 -1
  97. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  98. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  99. package/src/components/nle/NLEPreview.tsx +3 -3
  100. package/src/components/nle/PreviewOverlays.tsx +6 -6
  101. package/src/components/nle/PreviewPane.tsx +2 -2
  102. package/src/components/nle/TimelinePane.tsx +3 -3
  103. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  104. package/src/components/panels/SlideshowPanel.tsx +2 -2
  105. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  106. package/src/components/panels/VariablesBindElement.tsx +2 -2
  107. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  108. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  109. package/src/components/panels/VariablesPanel.tsx +3 -3
  110. package/src/components/panels/VariablesValueControls.tsx +2 -2
  111. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  112. package/src/components/renders/RenderQueue.test.tsx +7 -0
  113. package/src/components/renders/RenderQueue.tsx +6 -6
  114. package/src/components/renders/RenderQueueItem.tsx +6 -6
  115. package/src/components/sidebar/AssetCard.tsx +8 -8
  116. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  117. package/src/components/sidebar/AssetsTab.tsx +2 -2
  118. package/src/components/sidebar/AudioRow.tsx +6 -6
  119. package/src/components/sidebar/BlocksTab.tsx +6 -6
  120. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  121. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  122. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  123. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  124. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  125. package/src/components/ui/Button.test.tsx +60 -0
  126. package/src/components/ui/Button.tsx +4 -4
  127. package/src/components/ui/SearchInput.tsx +2 -2
  128. package/src/components/ui/Tooltip.tsx +1 -1
  129. package/src/hooks/useAppHotkeys.ts +1 -1
  130. package/src/hooks/useLintModal.test.tsx +65 -0
  131. package/src/hooks/useLintModal.ts +3 -0
  132. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  133. package/src/hooks/useSdkSession.ts +43 -10
  134. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  135. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  136. package/src/main.tsx +9 -3
  137. package/src/player/components/AudioWaveform.tsx +1 -1
  138. package/src/player/components/AutomationSelectionMenu.tsx +2 -2
  139. package/src/player/components/ClipContextMenu.tsx +3 -3
  140. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  141. package/src/player/components/CompositionThumbnail.tsx +3 -4
  142. package/src/player/components/EditModal.tsx +1 -1
  143. package/src/player/components/ImageThumbnail.tsx +1 -1
  144. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  145. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  146. package/src/player/components/PlayerControls.tsx +1 -1
  147. package/src/player/components/ShortcutsPanel.tsx +9 -9
  148. package/src/player/components/SpeedMenu.tsx +3 -5
  149. package/src/player/components/Timeline.tsx +2 -1
  150. package/src/player/components/TimelineCanvas.tsx +17 -10
  151. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  152. package/src/player/components/TimelineEmptyState.tsx +5 -5
  153. package/src/player/components/TimelineFxButton.tsx +6 -6
  154. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  155. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  156. package/src/player/components/TimelineGroupRow.tsx +1 -1
  157. package/src/player/components/TimelineLanes.test.tsx +32 -0
  158. package/src/player/components/TimelineLanes.tsx +2 -5
  159. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  160. package/src/player/components/TimelineRuler.tsx +1 -1
  161. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  162. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  163. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  164. package/src/player/components/TimelineTypes.ts +2 -1
  165. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  166. package/src/player/components/VideoThumbnail.tsx +4 -2
  167. package/src/player/components/VolumeControl.tsx +2 -2
  168. package/src/player/components/timelineCallbacks.ts +8 -8
  169. package/src/player/components/timelineClipDragPreview.ts +51 -27
  170. package/src/player/components/timelineClipDragTypes.ts +3 -0
  171. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  172. package/src/player/components/timelineDragDrop.ts +57 -15
  173. package/src/player/components/timelineLaneProps.ts +3 -0
  174. package/src/player/components/timelineSnapping.test.ts +27 -0
  175. package/src/player/components/timelineSnapping.ts +14 -1
  176. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  177. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  178. package/src/player/components/useTimelineClipDrag.ts +30 -3
  179. package/src/player/lib/describeClips.ts +3 -54
  180. package/src/styles/iconTokens.ts +20 -0
  181. package/src/styles/palette-reset.css +6 -0
  182. package/src/styles/studio.css +24 -21
  183. package/src/styles/tailwind-preset.shared.js +3 -0
  184. package/src/styles/theme.css +213 -0
  185. package/src/styles/theme.test.ts +196 -0
  186. package/src/utils/studioUiPreferences.ts +7 -0
  187. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  188. package/src/utils/timelineDropTrackInsert.ts +83 -0
  189. package/dist/assets/index-n2ml4tUn.js +0 -442
  190. package/dist/assets/index-pO6p0H27.css +0 -1
  191. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  192. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  193. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  194. package/src/components/storyboard/FramePoster.tsx +0 -86
  195. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  196. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  197. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  198. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  199. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  200. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  201. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  202. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  203. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  204. package/src/components/storyboard/StoryboardView.tsx +0 -169
  205. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  206. package/src/components/storyboard/frameComments.test.ts +0 -95
  207. package/src/components/storyboard/frameComments.ts +0 -115
  208. package/src/components/storyboard/frameStatus.ts +0 -36
  209. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  210. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  211. package/src/components/storyboard/useFrameComments.ts +0 -98
  212. package/src/contexts/ViewModeContext.tsx +0 -142
  213. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  214. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  215. package/src/hooks/useStoryboard.ts +0 -99
@@ -0,0 +1,196 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /** Guards the ways `theme.css` can silently break: a dropped CSS custom
4
+ * property, a utility that stops compiling, the stock Tailwind palette
5
+ * leaking back in, or the JS preset / legacy colours drifting from it. */
6
+
7
+ import { createRequire } from "node:module";
8
+ import { readFileSync } from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import React, { act } from "react";
12
+ import { createRoot } from "react-dom/client";
13
+ import { Gear, IconContext } from "@phosphor-icons/react";
14
+ import { compile } from "tailwindcss";
15
+ import { afterEach, describe, expect, it } from "vitest";
16
+ import studioPreset from "./tailwind-preset.shared.js";
17
+ import { readIconTokens } from "./iconTokens";
18
+
19
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
20
+
21
+ const STYLES_DIR = path.dirname(fileURLToPath(import.meta.url));
22
+ const require = createRequire(import.meta.url);
23
+ const TAILWIND_DIR = path.dirname(require.resolve("tailwindcss/package.json"));
24
+
25
+ function loadStylesheet(id: string, base: string) {
26
+ const file = id === "tailwindcss" ? path.join(TAILWIND_DIR, "index.css") : path.resolve(base, id);
27
+ return { path: file, base: path.dirname(file), content: readFileSync(file, "utf8") };
28
+ }
29
+
30
+ async function buildSource(source: string, candidates: string[]): Promise<string> {
31
+ const compiled = await compile(source, {
32
+ base: STYLES_DIR,
33
+ loadStylesheet,
34
+ });
35
+ return compiled.build(candidates);
36
+ }
37
+
38
+ const build = (entry: string, candidates: string[]) =>
39
+ buildSource(readFileSync(path.join(STYLES_DIR, entry), "utf8"), candidates);
40
+
41
+ function rootVariables(css: string): Map<string, string> {
42
+ const block = css.match(/:root, :host \{([\s\S]*?)\n {2}\}/);
43
+ const vars = new Map<string, string>();
44
+ for (const line of (block?.[1] ?? "").split(";")) {
45
+ const match = line.match(/(--[a-z0-9-]+):([\s\S]+)/i);
46
+ if (match) vars.set(match[1], match[2].trim().replace(/\s+/g, " "));
47
+ }
48
+ return vars;
49
+ }
50
+
51
+ const themeSource = readFileSync(path.join(STYLES_DIR, "theme.css"), "utf8");
52
+
53
+ function declaredValue(name: string): string {
54
+ const match = themeSource.match(new RegExp(`\\n\\s*${name}:\\s*([^;]+);`));
55
+ if (!match) throw new Error(`theme.css does not declare ${name}`);
56
+ return match[1].trim();
57
+ }
58
+
59
+ afterEach(() => {
60
+ document.body.innerHTML = "";
61
+ document.documentElement.removeAttribute("style");
62
+ });
63
+
64
+ describe("studio theme", () => {
65
+ it("emits the semantic palette as custom properties and as utilities", async () => {
66
+ const css = await build("studio.css", ["bg-accent", "text-text-2", "border-border"]);
67
+
68
+ expect(rootVariables(css).get("--color-accent")).toBe("#3ce6ac");
69
+ expect(css).toContain(".bg-accent {");
70
+ expect(css).toContain("background-color: var(--color-accent)");
71
+ });
72
+
73
+ it("resolves an alpha modifier on a plain hex token through color-mix", async () => {
74
+ const css = await build("studio.css", ["bg-accent/30"]);
75
+
76
+ expect(css).toMatch(/\.bg-accent\\\/30 \{[\s\S]*?color-mix\(/);
77
+ });
78
+
79
+ it("drops every Tailwind default color Studio does not use", async () => {
80
+ // `palette-reset.css` clears the stock palette. The entries Studio's
81
+ // markup still references are re-declared as deprecated aliases (U12
82
+ // removes them); anything outside that set must no longer resolve.
83
+ const css = await build("studio.css", ["bg-neutral-750", "text-teal-500", "bg-neutral-800"]);
84
+
85
+ expect(css).not.toContain("bg-neutral-750");
86
+ expect(css).not.toContain("text-teal-500");
87
+ expect(css).toContain(".bg-neutral-800 {");
88
+ });
89
+
90
+ it("leaves a host's stock Tailwind colors alone when only theme.css is imported", async () => {
91
+ const css = await buildSource('@import "tailwindcss";\n@import "./theme.css";', [
92
+ "bg-teal-500",
93
+ "text-accent",
94
+ ]);
95
+
96
+ expect(css).toContain(".bg-teal-500 {");
97
+ expect(css).toContain(".text-accent {");
98
+ });
99
+
100
+ it("keeps selection, playhead and accent as three distinct colors", () => {
101
+ const accent = declaredValue("--color-accent");
102
+ const selection = declaredValue("--color-selection");
103
+ const playhead = declaredValue("--color-playhead");
104
+
105
+ expect(new Set([accent, selection, playhead]).size).toBe(3);
106
+ });
107
+
108
+ it("names every micro type step in use and leaves the Tailwind sizes alone", async () => {
109
+ const steps = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
110
+ const css = await build(
111
+ "studio.css",
112
+ steps.map((step) => `text-step-${step}`).concat(["text-xs", "text-sm", "text-lg"]),
113
+ );
114
+ const vars = rootVariables(css);
115
+
116
+ for (const step of steps) {
117
+ expect(vars.get(`--text-step-${step}`)).toBe(`${step}px`);
118
+ expect(css).toContain(`.text-step-${step} {`);
119
+ }
120
+ expect(vars.get("--text-xs")).toBe("0.75rem");
121
+ expect(vars.get("--text-sm")).toBe("0.875rem");
122
+ expect(vars.get("--text-lg")).toBe("1.125rem");
123
+ });
124
+
125
+ it("compiles a motion-duration utility with a reduced-motion variant", async () => {
126
+ const css = await build("studio.css", ["duration-press", "duration-open"]);
127
+
128
+ expect(css).toMatch(
129
+ /\.duration-press \{[\s\S]*?transition-duration: var\(--duration-press\)[\s\S]*?prefers-reduced-motion: reduce[\s\S]*?transition-duration: 0ms/,
130
+ );
131
+ expect(css).toContain(".duration-open {");
132
+ });
133
+
134
+ it("keeps every legacy Tailwind palette entry at its upstream default value", async () => {
135
+ // These are copies of Tailwind's own values, kept only so existing markup
136
+ // renders unchanged. A Tailwind upgrade that moves one of them would
137
+ // otherwise silently change Studio's colors.
138
+ const upstream = new Map(
139
+ [
140
+ ...readFileSync(path.join(TAILWIND_DIR, "theme.css"), "utf8").matchAll(
141
+ /\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g,
142
+ ),
143
+ ].map(([, name, value]) => [name, value.trim()]),
144
+ );
145
+ const legacy = [...themeSource.matchAll(/\n\s*(--color-[a-z]+-(?:50|\d00|950)):\s*([^;]+);/g)];
146
+
147
+ expect(legacy.length).toBeGreaterThan(0);
148
+ for (const [, name, value] of legacy) {
149
+ expect(upstream.has(name), `${name} is not a Tailwind default`).toBe(true);
150
+ expect(`${name}: ${value.trim()}`).toBe(`${name}: ${upstream.get(name)}`);
151
+ }
152
+ });
153
+
154
+ it("keeps the deprecated JS preset in step with the CSS it shadows", () => {
155
+ const colors = studioPreset.theme.extend.colors;
156
+
157
+ expect(colors.studio.accent.toLowerCase()).toBe(declaredValue("--color-accent"));
158
+ expect(colors.panel.surface.toLowerCase()).toBe(declaredValue("--color-surface"));
159
+ });
160
+
161
+ it("reads the icon defaults off the theme rather than hard-coding them", () => {
162
+ document.documentElement.style.setProperty("--icon-size", "14px");
163
+ document.documentElement.style.setProperty("--icon-weight", "bold");
164
+
165
+ expect(readIconTokens()).toEqual({ size: "14px", weight: "bold" });
166
+ });
167
+
168
+ it("gives a child icon the token size and weight when it sets neither", () => {
169
+ document.documentElement.style.setProperty("--icon-size", "14px");
170
+ document.documentElement.style.setProperty("--icon-weight", "bold");
171
+
172
+ const host = document.createElement("div");
173
+ document.body.append(host);
174
+ const root = createRoot(host);
175
+ act(() => {
176
+ root.render(
177
+ React.createElement(
178
+ IconContext.Provider,
179
+ { value: readIconTokens() },
180
+ React.createElement(Gear, null),
181
+ ),
182
+ );
183
+ });
184
+ const inherited = host.querySelector("svg");
185
+ const inheritedShape = inherited?.innerHTML;
186
+ act(() => {
187
+ root.render(React.createElement(Gear, { size: 14, weight: "bold" }));
188
+ });
189
+ const explicitShape = host.querySelector("svg")?.innerHTML;
190
+ act(() => root.unmount());
191
+
192
+ expect(inherited?.getAttribute("width")).toBe("14px");
193
+ expect(inherited?.getAttribute("height")).toBe("14px");
194
+ expect(inheritedShape).toBe(explicitShape);
195
+ });
196
+ });
@@ -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
+ }