@hyperframes/studio 0.8.114 → 0.8.116

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 (266) hide show
  1. package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
  4. package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
  6. package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
  7. package/dist/chunk-3F47EFCT.js.map +1 -0
  8. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  9. package/dist/chunk-5EKWJZLM.js.map +1 -0
  10. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
  12. package/dist/index.d.ts +146 -40
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +4021 -3591
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +9 -8
  19. package/src/App.tsx +2 -2
  20. package/src/captions/hooks/useCaptionSync.ts +10 -6
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  23. package/src/components/dock/Dock.tsx +5 -7
  24. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  25. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  26. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  27. package/src/components/editor/DomEditOverlay.tsx +5 -1
  28. package/src/components/editor/FileTreeNodes.tsx +13 -10
  29. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  30. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  31. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  32. package/src/components/editor/SnapToolbar.tsx +43 -5
  33. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  34. package/src/components/editor/audioFxTelemetry.ts +2 -3
  35. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  36. package/src/components/editor/cropResize.ts +4 -3
  37. package/src/components/editor/domEditingDom.ts +4 -0
  38. package/src/components/editor/domEditingLayers.ts +1 -5
  39. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  40. package/src/components/editor/domGroupMoveBy.ts +95 -0
  41. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  42. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  43. package/src/components/editor/manualEditingAvailability.ts +8 -0
  44. package/src/components/editor/manualEdits.ts +1 -0
  45. package/src/components/editor/manualEditsParsing.ts +7 -0
  46. package/src/components/editor/manualOffsetDrag.ts +5 -6
  47. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  48. package/src/components/editor/marqueeCommit.ts +24 -8
  49. package/src/components/editor/motionPathCommit.test.ts +29 -1
  50. package/src/components/editor/motionPathCommit.ts +21 -9
  51. package/src/components/editor/motionPathSelection.test.ts +6 -31
  52. package/src/components/editor/probeSourceElement.test.ts +45 -0
  53. package/src/components/editor/probeSourceElement.ts +41 -16
  54. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  55. package/src/components/editor/propertyPanelFont.tsx +3 -2
  56. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  57. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  58. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  59. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  60. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  61. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  62. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  63. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  64. package/src/components/editor/propertyPanelTypes.ts +6 -3
  65. package/src/components/editor/useColorGradingController.ts +2 -1
  66. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  67. package/src/components/editor/useDomEditNudge.ts +2 -0
  68. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  69. package/src/components/editor/useDomEditZOrder.ts +12 -2
  70. package/src/components/editor/useFxCarveNodes.ts +2 -1
  71. package/src/components/editor/useFxLevelling.ts +2 -1
  72. package/src/components/feedback/projectProvenance.ts +2 -1
  73. package/src/components/nle/NLEContext.test.ts +1 -0
  74. package/src/components/nle/NLEContext.tsx +2 -1
  75. package/src/components/nle/NLEPreview.tsx +12 -10
  76. package/src/components/nle/PreviewPoster.tsx +2 -1
  77. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  78. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  79. package/src/components/renders/useFfmpegStatus.ts +2 -1
  80. package/src/components/renders/useRenderQueue.ts +5 -4
  81. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  82. package/src/components/sidebar/AssetsTab.tsx +2 -1
  83. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  84. package/src/components/ui/menuStyle.test.ts +61 -0
  85. package/src/components/ui/menuStyle.ts +10 -0
  86. package/src/contexts/DomEditContext.tsx +5 -1
  87. package/src/contexts/TimelineEditContext.tsx +1 -0
  88. package/src/domEditExports.test.tsx +9 -1
  89. package/src/hooks/domEditCommitRunner.ts +1 -1
  90. package/src/hooks/domSelectionTestHarness.ts +23 -0
  91. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  92. package/src/hooks/domStyleCommit.ts +8 -1
  93. package/src/hooks/draggedGsapPosition.ts +10 -0
  94. package/src/hooks/elementOffsetStager.ts +5 -4
  95. package/src/hooks/gsapDragCommit.test.ts +75 -164
  96. package/src/hooks/gsapDragCommit.ts +4 -125
  97. package/src/hooks/gsapDragPositionCommit.ts +36 -384
  98. package/src/hooks/gsapEditOutcome.ts +22 -2
  99. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  100. package/src/hooks/gsapGestureOutcome.ts +64 -0
  101. package/src/hooks/gsapGroupPreflight.ts +34 -0
  102. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  103. package/src/hooks/gsapMutationClient.ts +4 -3
  104. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  105. package/src/hooks/gsapParsedTween.test.ts +62 -0
  106. package/src/hooks/gsapParsedTween.ts +243 -0
  107. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  108. package/src/hooks/gsapPositionDetection.ts +52 -33
  109. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  110. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  111. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  112. package/src/hooks/gsapResizeIntercept.test.ts +96 -11
  113. package/src/hooks/gsapResizeIntercept.ts +79 -168
  114. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  115. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  116. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  117. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  118. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  119. package/src/hooks/gsapScriptCommitTypes.ts +13 -0
  120. package/src/hooks/gsapShared.ts +7 -0
  121. package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
  122. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  123. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  124. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  125. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  126. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  127. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  128. package/src/hooks/keyframeCacheAstLoad.ts +8 -6
  129. package/src/hooks/newTweenTarget.test.ts +41 -76
  130. package/src/hooks/plainRotation.ts +3 -2
  131. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  132. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  133. package/src/hooks/refreshTimelineRowText.ts +29 -0
  134. package/src/hooks/timelineTimingSync.test.ts +1 -0
  135. package/src/hooks/timelineTimingSync.ts +2 -1
  136. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  137. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  138. package/src/hooks/useAskAgentModal.ts +2 -1
  139. package/src/hooks/useBlockCatalog.ts +2 -1
  140. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  141. package/src/hooks/useCaptionDetection.ts +2 -1
  142. package/src/hooks/useCompositionContentLoader.ts +2 -1
  143. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  144. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  145. package/src/hooks/useDomEditSession.test.tsx +30 -1
  146. package/src/hooks/useDomEditSession.ts +10 -0
  147. package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
  148. package/src/hooks/useDomEditTextCommits.ts +13 -3
  149. package/src/hooks/useDomEditWiring.ts +4 -2
  150. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  151. package/src/hooks/useDomGeometryCommit.ts +8 -4
  152. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  153. package/src/hooks/useDomGeometryCommits.ts +3 -2
  154. package/src/hooks/useDomSelection.test.ts +26 -0
  155. package/src/hooks/useDomSelection.ts +10 -3
  156. package/src/hooks/useDomSelectionTypes.ts +6 -1
  157. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  158. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  159. package/src/hooks/useEditHistoryActions.ts +16 -5
  160. package/src/hooks/useElementLifecycleOps.ts +2 -1
  161. package/src/hooks/useEnableKeyframes.test.ts +34 -3
  162. package/src/hooks/useEnableKeyframes.ts +7 -4
  163. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
  164. package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
  165. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  166. package/src/hooks/useFileManager.ts +14 -10
  167. package/src/hooks/useFileTree.ts +3 -2
  168. package/src/hooks/useFrameCapture.ts +5 -1
  169. package/src/hooks/useFreezeFrame.ts +3 -2
  170. package/src/hooks/useGestureCommit.test.tsx +225 -55
  171. package/src/hooks/useGestureCommit.ts +86 -55
  172. package/src/hooks/useGroupCommits.ts +2 -1
  173. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  174. package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
  175. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  176. package/src/hooks/useGsapAwareEditing.ts +101 -195
  177. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  178. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  179. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  180. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  181. package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
  182. package/src/hooks/useGsapScriptCommits.ts +15 -10
  183. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  184. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  185. package/src/hooks/useGsapTweenCache.ts +18 -12
  186. package/src/hooks/useLintModal.ts +2 -1
  187. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  188. package/src/hooks/usePersistentEditHistory.ts +47 -8
  189. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  190. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  191. package/src/hooks/usePreviewInteraction.ts +2 -0
  192. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  193. package/src/hooks/usePreviewPersistence.ts +13 -11
  194. package/src/hooks/useProjectFileWriter.ts +20 -19
  195. package/src/hooks/useRemoveBackground.ts +2 -1
  196. package/src/hooks/useRenderClipContent.test.ts +18 -0
  197. package/src/hooks/useRenderClipContent.ts +4 -5
  198. package/src/hooks/useSdkSession.ts +2 -1
  199. package/src/hooks/useServerConnection.ts +4 -3
  200. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  201. package/src/hooks/useThumbnailStill.test.ts +100 -0
  202. package/src/hooks/useThumbnailStill.ts +37 -0
  203. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  204. package/src/hooks/useTimelineDeleteOps.ts +22 -27
  205. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  206. package/src/index.ts +4 -0
  207. package/src/player/components/AudioWaveform.tsx +3 -2
  208. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  209. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  210. package/src/player/components/ClipContextMenu.tsx +33 -31
  211. package/src/player/components/CompositionThumbnail.tsx +2 -1
  212. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  213. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  214. package/src/player/components/TextClipContent.test.ts +19 -0
  215. package/src/player/components/TextClipContent.tsx +27 -0
  216. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  217. package/src/player/components/clipMenuNormalize.ts +2 -1
  218. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  219. package/src/player/components/clipMenuToolItems.tsx +13 -9
  220. package/src/player/components/clipPeakMap.ts +2 -1
  221. package/src/player/components/timelineCallbacks.ts +1 -0
  222. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  223. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  224. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  225. package/src/player/components/timelineEditing.ts +3 -1
  226. package/src/player/components/timelineLinkSelection.ts +35 -1
  227. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  228. package/src/player/components/useTimelineClipDrag.ts +6 -7
  229. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  230. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  231. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  232. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  233. package/src/player/lib/thumbnailScheduler.ts +10 -2
  234. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  235. package/src/player/lib/timelineElementHelpers.ts +6 -15
  236. package/src/player/lib/timelineText.ts +40 -0
  237. package/src/player/sceneSwap.ts +2 -1
  238. package/src/player/store/timelineElement.ts +12 -0
  239. package/src/styles/theme.css +3 -0
  240. package/src/telemetry/client.ts +2 -1
  241. package/src/utils/blockInstaller.ts +2 -1
  242. package/src/utils/elementGsap.test.ts +9 -0
  243. package/src/utils/elementGsap.ts +7 -0
  244. package/src/utils/gsapSoftReload.ts +2 -3
  245. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  246. package/src/utils/gsapUndoRestore.ts +3 -6
  247. package/src/utils/keyframeUsage.ts +79 -0
  248. package/src/utils/previewFeatureUsage.test.ts +32 -0
  249. package/src/utils/previewFeatureUsage.ts +46 -0
  250. package/src/utils/previewPrefetch.ts +2 -1
  251. package/src/utils/razorSplitTransaction.ts +12 -8
  252. package/src/utils/softReloadTargets.ts +1 -3
  253. package/src/utils/studioApiFetch.test.ts +75 -0
  254. package/src/utils/studioApiFetch.ts +13 -0
  255. package/src/utils/studioFileHistory.ts +4 -1
  256. package/src/utils/studioHelpers.ts +7 -1
  257. package/src/utils/studioPendingEdits.test.ts +23 -6
  258. package/src/utils/studioPendingEdits.ts +32 -8
  259. package/src/utils/studioTelemetry.test.ts +107 -0
  260. package/src/utils/studioTelemetry.ts +51 -5
  261. package/src/utils/timelineCompositionInsert.ts +3 -2
  262. package/src/webmcp/StudioAgentTools.tsx +4 -3
  263. package/dist/chunk-BIUXP3PG.js.map +0 -1
  264. package/dist/chunk-K47E2ZJA.js.map +0 -1
  265. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  266. /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -6,11 +6,22 @@ import {
6
6
  StudioSaveNetworkError,
7
7
  } from "../utils/studioSaveDiagnostics";
8
8
  import { studioExpectedFileVersion, studioWriteHeaders } from "../utils/studioFileVersion";
9
+ import { studioApiFetch } from "../utils/studioApiFetch";
9
10
 
10
11
  export interface UseProjectFileWriterOptions {
11
12
  projectId: string | null;
12
13
  }
13
14
 
15
+ async function fetchProjectFile(projectId: string | null, path: string, query: string) {
16
+ if (!projectId) throw new Error("No active project");
17
+ const response = await studioApiFetch(
18
+ `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}${query}`,
19
+ );
20
+ if (!response.ok) throw new Error(`Failed to read ${path}`);
21
+ const data = (await response.json()) as { content?: string; version?: string };
22
+ return { content: data.content, version: data.version ?? response.headers.get("etag") };
23
+ }
24
+
14
25
  // The etag-guarded project-file read/write pair every Studio save path
15
26
  // shares. A host mounting hand editing outside Studio needs only this, not
16
27
  // the full file manager (file tree, editor tabs, uploads, font assets).
@@ -30,29 +41,19 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
30
41
 
31
42
  const readProjectFile = useCallback(
32
43
  async (path: string): Promise<string> => {
33
- if (!projectId) throw new Error("No active project");
34
- const response = await fetch(
35
- `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`,
36
- );
37
- if (!response.ok) throw new Error(`Failed to read ${path}`);
38
- const data = (await response.json()) as { content?: string; version?: string };
39
- if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
40
- fileVersions.set(path, data.version ?? response.headers.get("etag"));
41
- return data.content;
44
+ const { content, version } = await fetchProjectFile(projectId, path, "");
45
+ if (typeof content !== "string") throw new Error(`Missing file contents for ${path}`);
46
+ fileVersions.set(path, version);
47
+ return content;
42
48
  },
43
49
  [fileVersions, projectId],
44
50
  );
45
51
 
46
52
  const readOptionalProjectFile = useCallback(
47
53
  async (path: string): Promise<string> => {
48
- if (!projectId) throw new Error("No active project");
49
- const response = await fetch(
50
- `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
51
- );
52
- if (!response.ok) throw new Error(`Failed to read ${path}`);
53
- const data = (await response.json()) as { content?: string; version?: string };
54
- fileVersions.set(path, data.version ?? response.headers.get("etag"));
55
- return typeof data.content === "string" ? data.content : "";
54
+ const { content, version } = await fetchProjectFile(projectId, path, "?optional=1");
55
+ fileVersions.set(path, version);
56
+ return typeof content === "string" ? content : "";
56
57
  },
57
58
  [fileVersions, projectId],
58
59
  );
@@ -63,7 +64,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
63
64
  const writeProjectId = projectId;
64
65
  let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
65
66
  if (expectedVersion === undefined) {
66
- const preflight = await fetch(
67
+ const preflight = await studioApiFetch(
67
68
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
68
69
  );
69
70
  if (preflight.ok) {
@@ -86,7 +87,7 @@ export function useProjectFileWriter({ projectId }: UseProjectFileWriterOptions)
86
87
  // the retry can produce a second filesystem receipt that must be suppressed independently.
87
88
  let response: Response;
88
89
  try {
89
- response = await fetch(
90
+ response = await studioApiFetch(
90
91
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
91
92
  {
92
93
  method: "PUT",
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
2
  import { waitForMediaJob } from "../components/studioMediaJobs";
3
3
  import type { BackgroundRemovalProgress } from "../components/editor/propertyPanelTypes";
4
+ import { studioApiFetch } from "../utils/studioApiFetch";
4
5
 
5
6
  interface RemoveBackgroundOptions {
6
7
  createBackgroundPlate?: boolean;
@@ -31,7 +32,7 @@ export function useRemoveBackground(
31
32
  return useCallback(
32
33
  // fallow-ignore-next-line complexity
33
34
  async (inputPath: string, options: RemoveBackgroundOptions) => {
34
- const response = await fetch(
35
+ const response = await studioApiFetch(
35
36
  `/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
36
37
  {
37
38
  method: "POST",
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
6
6
  import { CompositionThumbnail, VideoThumbnail } from "../player";
7
7
  import { AudioWaveform } from "../player/components/AudioWaveform";
8
8
  import { AudibleVideoClipContent } from "../player/components/AudibleVideoClipContent";
9
+ import { TextClipContent } from "../player/components/TextClipContent";
9
10
  import { ClipPeakMarks } from "../player/components/ClipPeakMarks";
10
11
 
11
12
  function unwrapPeakMarks(node: ReactNode): ReactNode {
@@ -280,6 +281,23 @@ describe("useRenderClipContent", () => {
280
281
  expect(isValidElement(content) && content.type).toBe(AudibleVideoClipContent);
281
282
  });
282
283
 
284
+ it("gives a layer spanning the whole film its own frame, whatever its id", () => {
285
+ usePlayerStore.setState({ thumbnailMode: "adaptive" });
286
+ for (const id of ["waves", "background-glyphs"]) {
287
+ const content = renderClipContent({ id, tag: "div", start: 0, duration: 12, track: 0 }, null);
288
+ expect(isValidElement(content) && content.type).toBe(CompositionThumbnail);
289
+ }
290
+ });
291
+
292
+ it("draws a text layer's own words instead of capturing them", () => {
293
+ usePlayerStore.setState({ thumbnailMode: "adaptive" });
294
+ const content = renderClipContent(
295
+ { id: "title", tag: "h1", start: 0, duration: 12, track: 0, text: { value: "Ship it" } },
296
+ null,
297
+ );
298
+ expect(isValidElement(content) && content.type).toBe(TextClipContent);
299
+ });
300
+
283
301
  it("passes empty labels to thumbnail content so TimelineClip owns clip names", () => {
284
302
  usePlayerStore.setState({ thumbnailMode: "adaptive" });
285
303
 
@@ -12,6 +12,7 @@ import type { TimelineClipRenderContext } from "../player/components/TimelineTyp
12
12
  import { audioPillFlags } from "../player/components/audioClipLink";
13
13
  import { AudioWaveform, rendersWaveform } from "../player/components/AudioWaveform";
14
14
  import { ImageThumbnail } from "../player/components/ImageThumbnail";
15
+ import { TextClipContent } from "../player/components/TextClipContent";
15
16
  import { AudibleVideoClipContent } from "../player/components/AudibleVideoClipContent";
16
17
  import { ClipPeakMarks } from "../player/components/ClipPeakMarks";
17
18
  import { clipPeaksUrl, clipSourceWindow } from "../player/components/clipPeakMap";
@@ -184,6 +185,8 @@ export function useRenderClipContent({
184
185
  return renderAudioClip(el, pid, sessionEpoch, style.label, context, elements);
185
186
  }
186
187
 
188
+ if (el.text) return createElement(TextClipContent, { text: el.text });
189
+
187
190
  // When drilled into a composition, render all inner elements via
188
191
  // CompositionThumbnail at their start time — most accurate visual.
189
192
  if (activePreviewUrl && el.duration > 0) {
@@ -207,11 +210,7 @@ export function useRenderClipContent({
207
210
  });
208
211
  }
209
212
 
210
- const htmlPreviewEligible =
211
- el.duration > 0 &&
212
- effectiveTimelineDuration > 0 &&
213
- el.duration < effectiveTimelineDuration * 0.92 &&
214
- !/(backdrop|background|overlay|scrim|mask)/i.test(el.id);
213
+ const htmlPreviewEligible = el.duration > 0 && effectiveTimelineDuration > 0;
215
214
 
216
215
  if ((el.tag === "video" || el.tag === "img") && el.src) {
217
216
  const mediaSrc = resolveMediaPreviewUrl(el.src, pid, window.location.origin);
@@ -6,6 +6,7 @@ import { trackStudioEvent } from "../utils/studioTelemetry";
6
6
  import type { PublishSdkSession } from "../utils/sdkCutover";
7
7
  import { addExternalFileReloadListener } from "./externalFileReloadBus";
8
8
  import { whenPreviewBooted } from "../player/store/playerStore";
9
+ import { studioApiFetch } from "../utils/studioApiFetch";
9
10
 
10
11
  /**
11
12
  * Why an optional project-file read produced no usable content. `stage: "read"`
@@ -194,7 +195,7 @@ async function readProjectFileOptional(
194
195
  let res: Response;
195
196
  const fetchStarted = performance.now();
196
197
  try {
197
- res = await fetch(
198
+ res = await studioApiFetch(
198
199
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
199
200
  );
200
201
  } catch {
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
2
2
  import { buildProjectHash, parseProjectIdFromHash } from "../utils/projectRouting";
3
3
  import { useMountEffect } from "./useMountEffect";
4
4
  import { trackStudioEvent } from "../utils/studioTelemetry";
5
+ import { studioApiFetch } from "../utils/studioApiFetch";
5
6
 
6
7
  interface ServerConnectionState {
7
8
  projectId: string | null;
@@ -37,7 +38,7 @@ type ProjectHashVerdict = {
37
38
 
38
39
  async function resolveHashProject(id: string): Promise<ProjectHashVerdict> {
39
40
  try {
40
- const res = await fetch(`/api/projects/${encodeURIComponent(id)}`);
41
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(id)}`);
41
42
  if (res.ok) return { outcome: "ok", status: res.status };
42
43
  return { outcome: res.status === 404 ? "missing" : "unknown", status: res.status };
43
44
  } catch {
@@ -72,7 +73,7 @@ function replaceProjectHash(projectId: string): void {
72
73
  */
73
74
  async function firstProjectId(): Promise<string | null> {
74
75
  try {
75
- const res = await fetch("/api/projects");
76
+ const res = await studioApiFetch("/api/projects");
76
77
  const data = (await res.json()) as { projects?: Array<{ id?: string }> };
77
78
  return data.projects?.[0]?.id ?? null;
78
79
  } catch {
@@ -107,7 +108,7 @@ export function useServerConnection(): ServerConnectionState {
107
108
  }
108
109
 
109
110
  function tryConnect() {
110
- fetch("/api/projects")
111
+ studioApiFetch("/api/projects")
111
112
  .then((r) => r.json())
112
113
  .then(async (data) => {
113
114
  if (cancelled) return;
@@ -3,6 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing";
3
3
  import { usePlayerStore } from "../player";
4
4
  import { buildStudioSelectionSnapshot } from "../utils/studioSelectionSnapshot";
5
5
  import { trackStudioEvent } from "../utils/studioTelemetry";
6
+ import { studioApiFetch } from "../utils/studioApiFetch";
6
7
 
7
8
  interface UseStudioSelectionPublisherParams {
8
9
  projectId: string | null;
@@ -26,7 +27,7 @@ function reportSelectionPublishError(error: unknown): void {
26
27
  }
27
28
 
28
29
  function putSelection(projectId: string, selection: unknown, signal?: AbortSignal): Promise<void> {
29
- return fetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
30
+ return studioApiFetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
30
31
  method: "PUT",
31
32
  headers: { "Content-Type": "application/json" },
32
33
  body: JSON.stringify({ selection }),
@@ -0,0 +1,100 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { thumbnailScheduler } from "../player/lib/thumbnailScheduler";
7
+ // By package name, as a host imports it: the test fails if the export goes.
8
+ import { useThumbnailStill, type ThumbnailStill } from "@hyperframes/studio";
9
+
10
+ Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { configurable: true, value: true });
11
+
12
+ class MockImage {
13
+ onload: (() => void) | null = null;
14
+ onerror: (() => void) | null = null;
15
+ naturalWidth = 320;
16
+ naturalHeight = 180;
17
+ set src(_value: string) {
18
+ queueMicrotask(() => this.onload?.());
19
+ }
20
+ }
21
+
22
+ const URL_ONE = "http://localhost:3000/api/projects/demo/thumbnail/index.html?t=1.00&revision=1";
23
+ const originalImage = globalThis.Image;
24
+ const originalFetch = globalThis.fetch;
25
+ const originalCreate = URL.createObjectURL;
26
+ const originalRevoke = URL.revokeObjectURL;
27
+ let host: HTMLDivElement;
28
+ let root: Root | null = null;
29
+ let shown: ThumbnailStill | null = null;
30
+
31
+ function Still({ url }: { url: string | null }) {
32
+ shown = useThumbnailStill(url, "demo");
33
+ return null;
34
+ }
35
+
36
+ const mount = (url: string | null) =>
37
+ act(async () => {
38
+ root ??= createRoot(host);
39
+ root.render(React.createElement(Still, { url }));
40
+ });
41
+
42
+ beforeEach(() => {
43
+ globalThis.Image = MockImage as unknown as typeof Image;
44
+ globalThis.fetch = vi.fn(async () => new Response(new Blob(["still"]), { status: 200 }));
45
+ URL.createObjectURL = vi.fn(() => "blob:still");
46
+ URL.revokeObjectURL = vi.fn();
47
+ shown = null;
48
+ host = document.createElement("div");
49
+ document.body.append(host);
50
+ });
51
+
52
+ afterEach(() => {
53
+ act(() => root?.unmount());
54
+ root = null;
55
+ thumbnailScheduler.setPreviewReloading(false);
56
+ thumbnailScheduler.invalidateProject("demo");
57
+ globalThis.Image = originalImage;
58
+ globalThis.fetch = originalFetch;
59
+ URL.createObjectURL = originalCreate;
60
+ URL.revokeObjectURL = originalRevoke;
61
+ document.body.replaceChildren();
62
+ });
63
+
64
+ describe("useThumbnailStill", () => {
65
+ it("hands back the still's object URL once the scheduler has loaded it", async () => {
66
+ await mount(URL_ONE);
67
+ expect(globalThis.fetch).toHaveBeenCalledWith(URL_ONE, expect.anything());
68
+ await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
69
+ });
70
+
71
+ it("hands back the same object while nothing changes", async () => {
72
+ await mount(URL_ONE);
73
+ await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
74
+ const first = shown;
75
+ await mount(URL_ONE);
76
+ expect(shown).toBe(first);
77
+ });
78
+
79
+ it("asks for nothing without a url", async () => {
80
+ await mount(null);
81
+ expect(globalThis.fetch).not.toHaveBeenCalled();
82
+ expect(shown).toEqual({ status: "loading" });
83
+ });
84
+
85
+ it("says failed when the still cannot load, so a host can fall back", async () => {
86
+ globalThis.fetch = vi.fn(async () => new Response("", { status: 500 }));
87
+ await mount(URL_ONE);
88
+ await vi.waitFor(() => expect(shown).toEqual({ status: "failed" }));
89
+ });
90
+
91
+ it("holds the fetch while a preview reloads, and runs it once the reload is over", async () => {
92
+ thumbnailScheduler.setPreviewReloading(true);
93
+ await mount(URL_ONE);
94
+ expect(globalThis.fetch).not.toHaveBeenCalled();
95
+ expect(shown).toEqual({ status: "loading" });
96
+ await act(async () => thumbnailScheduler.setPreviewReloading(false));
97
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
98
+ await vi.waitFor(() => expect(shown).toEqual({ status: "ready", url: "blob:still" }));
99
+ });
100
+ });
@@ -0,0 +1,37 @@
1
+ import { useMemo } from "react";
2
+ import { compositionThumbnailRequest } from "../player/components/CompositionThumbnail";
3
+ import type { ThumbnailPriority } from "../player/lib/thumbnailScheduler";
4
+ import { useThumbnailLease } from "./useThumbnailLease";
5
+
6
+ export interface UseThumbnailStillOptions {
7
+ sessionEpoch?: number;
8
+ priority?: ThumbnailPriority;
9
+ }
10
+
11
+ export type ThumbnailStill =
12
+ | { status: "loading" }
13
+ | { status: "ready"; url: string }
14
+ | { status: "failed" };
15
+
16
+ const LOADING: ThumbnailStill = Object.freeze({ status: "loading" });
17
+ const FAILED: ThumbnailStill = Object.freeze({ status: "failed" });
18
+
19
+ /** A host picture's server still through the thumbnail scheduler, so it yields to a preview reload. */
20
+ export function useThumbnailStill(
21
+ url: string | null,
22
+ projectId: string,
23
+ { sessionEpoch = 0, priority = "visible" }: UseThumbnailStillOptions = {},
24
+ ): ThumbnailStill {
25
+ const request = useMemo(
26
+ () => (url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null),
27
+ [priority, projectId, sessionEpoch, url],
28
+ );
29
+ const snapshot = useThumbnailLease(request);
30
+ const ready =
31
+ snapshot.status === "ready" && snapshot.value.kind === "image" ? snapshot.value.url : null;
32
+ return useMemo(
33
+ () =>
34
+ ready ? { status: "ready", url: ready } : snapshot.status === "error" ? FAILED : LOADING,
35
+ [ready, snapshot.status],
36
+ );
37
+ }
@@ -28,6 +28,17 @@ describe("applyRippleShifts", () => {
28
28
  });
29
29
  });
30
30
 
31
+ function stubRemoveElementsFetch(current: string, afterRemoval: string) {
32
+ vi.stubGlobal(
33
+ "fetch",
34
+ vi.fn(async (input: RequestInfo | URL) => {
35
+ const isRemoval = String(input).includes("/file-mutations/remove-elements/");
36
+ const content = isRemoval ? afterRemoval : current;
37
+ return new Response(JSON.stringify({ changed: isRemoval, content }), { status: 200 });
38
+ }),
39
+ );
40
+ }
41
+
31
42
  describe("useTimelineDeleteOps: ripple undo label", () => {
32
43
  const html = `<!DOCTYPE html><html data-composition-variables='[]'><body>
33
44
  <div data-hf-id="hf-stage" data-hf-root data-duration="6">
@@ -38,16 +49,7 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
38
49
  </body></html>`;
39
50
 
40
51
  beforeEach(() => {
41
- vi.stubGlobal(
42
- "fetch",
43
- vi.fn(async (input: RequestInfo | URL) => {
44
- const url = String(input);
45
- if (url.includes("/file-mutations/remove-element/")) {
46
- return new Response(JSON.stringify({ changed: true, content: html }), { status: 200 });
47
- }
48
- return new Response(JSON.stringify({ content: html }), { status: 200 });
49
- }),
50
- );
52
+ stubRemoveElementsFetch(html, html);
51
53
  });
52
54
 
53
55
  afterEach(() => {
@@ -118,6 +120,23 @@ describe("useTimelineDeleteOps: ripple undo label", () => {
118
120
  );
119
121
  });
120
122
 
123
+ it("removes a multi-clip selection with one request, however many clips it holds", async () => {
124
+ const handleTimelineGroupMove = vi.fn().mockResolvedValue(undefined);
125
+ const { elements, getHook } = mountDeleteHarness({ handleTimelineGroupMove });
126
+
127
+ await act(async () => {
128
+ await getHook().handleTimelineElementsDelete(elements);
129
+ });
130
+
131
+ const removals = vi
132
+ .mocked(fetch)
133
+ .mock.calls.filter(([url]) => String(url).includes("/file-mutations/remove-element"));
134
+ expect(removals).toHaveLength(1);
135
+ const [, init] = removals[0] as [unknown, RequestInit];
136
+ const body = JSON.parse(String(init.body));
137
+ expect(body.targets).toHaveLength(elements.length);
138
+ });
139
+
121
140
  it("shows exactly one toast when a ripple fails to persist after a committed delete", async () => {
122
141
  const handleTimelineGroupMove = vi.fn().mockRejectedValue(new Error("persist failed"));
123
142
  const showToast = vi.fn();
@@ -162,18 +181,7 @@ describe("useTimelineDeleteOps: undo race", () => {
162
181
  </body></html>`;
163
182
 
164
183
  beforeEach(() => {
165
- vi.stubGlobal(
166
- "fetch",
167
- vi.fn(async (input: RequestInfo | URL) => {
168
- const url = String(input);
169
- if (url.includes("/file-mutations/remove-element/")) {
170
- return new Response(JSON.stringify({ changed: true, content: htmlAfterDeletingC }), {
171
- status: 200,
172
- });
173
- }
174
- return new Response(JSON.stringify({ content: html }), { status: 200 });
175
- }),
176
- );
184
+ stubRemoveElementsFetch(html, htmlAfterDeletingC);
177
185
  });
178
186
 
179
187
  afterEach(() => {
@@ -21,6 +21,7 @@ import type {
21
21
  TimelineGroupCommitOptions,
22
22
  TimelineGroupMoveChange,
23
23
  } from "./useTimelineGroupEditing";
24
+ import { studioApiFetch } from "../utils/studioApiFetch";
24
25
 
25
26
  /** Apply already-resolved ripple changes to the surviving elements for the
26
27
  * optimistic store update after a delete. Pure — no IO. */
@@ -121,38 +122,32 @@ export function useTimelineDeleteOps({
121
122
  writeFile: writeProjectFile,
122
123
  recordEdit,
123
124
  rewrite: async (originalContent) => {
124
- // Remove every selected element before saving once. The server rewrites
125
- // the file per call, so `removedContent` after the last one holds them
126
- // all — which is what makes this a single history entry, and a single
127
- // undo, rather than one per clip.
128
- let removedContent = originalContent;
129
- for (const target of sameFile) {
125
+ // One request removes every selected element and rewrites the file once, so a
126
+ // large selection is a single round trip and a single history entry.
127
+ const patchTargets = sameFile.map((target) => {
130
128
  const patchTarget = buildPatchTarget(target);
131
129
  if (!patchTarget) {
132
130
  throw new Error(`Timeline element ${target.id} is missing a patchable target`);
133
131
  }
134
-
135
- const removeResponse = await fetch(
136
- buildProjectApiPath(
137
- pid,
138
- `/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
139
- ),
140
- {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
143
- body: JSON.stringify({ target: patchTarget }),
144
- },
145
- );
146
- if (!removeResponse.ok) {
147
- throw new Error(`Failed to delete ${target.id} from ${targetPath}`);
148
- }
149
-
150
- const removeData = (await removeResponse.json()) as {
151
- changed?: boolean;
152
- content?: string;
153
- };
154
- if (typeof removeData.content === "string") removedContent = removeData.content;
132
+ return patchTarget;
133
+ });
134
+ const removeResponse = await studioApiFetch(
135
+ buildProjectApiPath(
136
+ pid,
137
+ `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
138
+ ),
139
+ {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
142
+ body: JSON.stringify({ targets: patchTargets }),
143
+ },
144
+ );
145
+ if (!removeResponse.ok) {
146
+ throw new Error(`Failed to delete ${sameFile.length} clips from ${targetPath}`);
155
147
  }
148
+ const removeData = (await removeResponse.json()) as { content?: string };
149
+ const removedContent =
150
+ typeof removeData.content === "string" ? removeData.content : originalContent;
156
151
  // Shrink to the furthest remaining clip end, read from the post-removal source:
157
152
  // store durations are runtime-truncated.
158
153
  const deleteContentEnd = furthestClipEndFromSource(removedContent);
@@ -1296,7 +1296,7 @@ describe("useTimelineEditing duration rollback on failed persist", () => {
1296
1296
  if (url.includes("/api/projects/p1/files/")) {
1297
1297
  return jsonResponse({ content: DELETE_SOURCE });
1298
1298
  }
1299
- if (url.includes("/api/projects/p1/file-mutations/remove-element/")) {
1299
+ if (url.includes("/api/projects/p1/file-mutations/remove-elements/")) {
1300
1300
  return jsonResponse({ changed: true, content: DELETE_REMOVED_SOURCE });
1301
1301
  }
1302
1302
  throw new Error(`Unexpected fetch: ${url}`);
package/src/index.ts CHANGED
@@ -76,6 +76,8 @@ export type { AudioWaveformProps } from "./player/components/AudioWaveform";
76
76
  export { ImageThumbnail } from "./player/components/ImageThumbnail";
77
77
  export type { ImageThumbnailProps } from "./player/components/ImageThumbnail";
78
78
  export { useRenderClipContent } from "./hooks/useRenderClipContent";
79
+ export { useThumbnailStill } from "./hooks/useThumbnailStill";
80
+ export type { ThumbnailStill, UseThumbnailStillOptions } from "./hooks/useThumbnailStill";
79
81
  export type { UseRenderClipContentOptions } from "./hooks/useRenderClipContent";
80
82
  export type { ThumbnailPriority } from "./player/lib/thumbnailScheduler";
81
83
  export type {
@@ -208,6 +210,8 @@ export type { DomEditZOrder } from "./components/editor/useDomEditZOrder";
208
210
  export type { ZOrderAction } from "./components/editor/canvasContextMenuZOrder";
209
211
  export type { DomEditCapabilities, DomEditSelection } from "./components/editor/domEditingTypes";
210
212
  export { useDomStyleCommit } from "./hooks/useDomStyleCommit";
213
+ export { ColorField } from "./components/editor/propertyPanelColor";
214
+ export { GradientField } from "./components/editor/propertyPanelFill";
211
215
  export type { UseDomStyleCommitOptions } from "./hooks/useDomStyleCommit";
212
216
  export type { DomEditCommitDeclineReason, DomEditCommitOutcome } from "./hooks/domEditCommitRunner";
213
217
  export { resolveDomEditSelection } from "./components/editor/domEditingLayers";
@@ -5,6 +5,7 @@ import { useThumbnailLease } from "../../hooks/useThumbnailLease";
5
5
  import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
6
6
  import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
7
7
  import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
8
+ import { studioApiFetch } from "../../utils/studioApiFetch";
8
9
 
9
10
  export interface AudioWaveformProps {
10
11
  audioUrl: string;
@@ -124,7 +125,7 @@ async function loadWaveform(
124
125
  }
125
126
 
126
127
  async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
127
- const response = await fetch(url, { signal });
128
+ const response = await studioApiFetch(url, { signal });
128
129
  if (!response.ok) throw new Error(`Waveform request failed (${response.status})`);
129
130
  const data: unknown = await response.json();
130
131
  if (
@@ -140,7 +141,7 @@ async function fetchWaveformPeaks(url: string, signal: AbortSignal): Promise<num
140
141
  }
141
142
 
142
143
  async function decodeWaveformPeaks(url: string, signal: AbortSignal): Promise<number[]> {
143
- const response = await fetch(url, { signal });
144
+ const response = await studioApiFetch(url, { signal });
144
145
  if (!response.ok) throw new Error(`Audio request failed (${response.status})`);
145
146
  const buffer = await response.arrayBuffer();
146
147
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
@@ -8,6 +8,7 @@ import { memo } from "react";
8
8
  import { createPortal } from "react-dom";
9
9
  import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
10
10
  import { AUTOMATION_SHAPES, type AutomationShapeId } from "./automationShapes";
11
+ import { menuClasses } from "../../components/ui/menuStyle";
11
12
 
12
13
  interface AutomationSelectionMenuProps {
13
14
  x: number;
@@ -45,7 +46,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
45
46
  // portaled to `document.body`, but the ruler's sticky header sits at z-70
46
47
  // in the SAME root stacking context, so a z-50 menu opened near the top of
47
48
  // the timeline is painted through by the ruler and the playhead.
48
- className="hf-automation-menu fixed z-200 min-w-[140px] rounded-sm border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
49
+ className={`${menuClasses.panel} hf-automation-menu fixed z-200 min-w-[140px]`}
49
50
  style={{ left: adjustedX, top: adjustedY }}
50
51
  >
51
52
  {AUTOMATION_SHAPES.map((shape) => (
@@ -61,7 +62,7 @@ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
61
62
  {shape.label}
62
63
  </button>
63
64
  ))}
64
- <div className="my-1 border-t border-panel-border-input" />
65
+ <div className={menuClasses.divider} />
65
66
  <button
66
67
  type="button"
67
68
  className={row}