@hyperframes/studio 0.8.104 → 0.8.106

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 (225) hide show
  1. package/dist/assets/{hyperframes-player-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
  2. package/dist/assets/index-CMiYVGv5.js +399 -0
  3. package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
  4. package/dist/assets/index-lrT-gfHa.css +1 -0
  5. package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
  6. package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
  7. package/dist/chunk-EEXSYY5F.js.map +1 -0
  8. package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
  9. package/dist/chunk-ICTZJPQQ.js.map +1 -0
  10. package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
  11. package/dist/chunk-NCUBAVGK.js.map +1 -0
  12. package/dist/index.css +6 -1
  13. package/dist/index.css.map +1 -1
  14. package/dist/index.d.ts +15 -4
  15. package/dist/index.html +11 -2
  16. package/dist/index.js +10391 -9836
  17. package/dist/index.js.map +1 -1
  18. package/dist/player.js +2 -2
  19. package/dist/styles/tailwind-preset.js.map +1 -1
  20. package/dist/styles.css +1 -1
  21. package/dist/ui.js +2 -2
  22. package/package.json +9 -9
  23. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  24. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  25. package/src/captions/components/CaptionTimeline.tsx +3 -3
  26. package/src/captions/keyboard.test.ts +6 -0
  27. package/src/captions/keyboard.ts +2 -1
  28. package/src/components/AskAgentModal.tsx +1 -1
  29. package/src/components/LintModal.tsx +35 -35
  30. package/src/components/StudioErrorBoundary.tsx +1 -1
  31. package/src/components/StudioHeader.dom.test.tsx +35 -13
  32. package/src/components/StudioHeader.tsx +6 -2
  33. package/src/components/StudioToast.tsx +7 -16
  34. package/src/components/ThemeToggle.tsx +99 -0
  35. package/src/components/TimelineToolPicker.tsx +1 -1
  36. package/src/components/TimelineToolbar.tsx +11 -11
  37. package/src/components/dock/Dock.tsx +1 -1
  38. package/src/components/dock/dock.css +9 -1
  39. package/src/components/editor/AnimationCard.tsx +5 -5
  40. package/src/components/editor/AnimationCardParts.tsx +1 -1
  41. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  42. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  43. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  44. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  45. package/src/components/editor/DomEditOverlay.tsx +37 -5
  46. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  47. package/src/components/editor/EaseCurveSection.tsx +14 -19
  48. package/src/components/editor/EaseModeControls.tsx +2 -2
  49. package/src/components/editor/EaseParamFields.tsx +8 -8
  50. package/src/components/editor/FileTree.tsx +1 -1
  51. package/src/components/editor/FileTreeIcons.tsx +47 -26
  52. package/src/components/editor/FileTreeNodes.tsx +11 -8
  53. package/src/components/editor/GestureRecordControl.tsx +3 -3
  54. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  55. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  56. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  57. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  58. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  59. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  60. package/src/components/editor/LayersPanel.tsx +3 -3
  61. package/src/components/editor/PromotableControl.tsx +2 -2
  62. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  63. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  64. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  65. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  66. package/src/components/editor/SnapToolbar.tsx +5 -5
  67. package/src/components/editor/SourceEditor.tsx +2 -6
  68. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  69. package/src/components/editor/Transform3DCube.tsx +1 -1
  70. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  71. package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
  72. package/src/components/editor/easeCurveSvg.tsx +1 -1
  73. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
  74. package/src/components/editor/manualEditsDom.ts +36 -3
  75. package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
  76. package/src/components/editor/manualEditsDomPatches.ts +0 -30
  77. package/src/components/editor/manualEditsSnapshot.ts +1 -0
  78. package/src/components/editor/marqueeCommit.test.tsx +69 -1
  79. package/src/components/editor/marqueeCommit.ts +6 -1
  80. package/src/components/editor/overlayFrameLoop.test.ts +27 -0
  81. package/src/components/editor/overlayFrameLoop.ts +21 -0
  82. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  83. package/src/components/editor/plainTranslate.ts +34 -3
  84. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  85. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  86. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  87. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  88. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  89. package/src/components/editor/propertyPanelFill.tsx +6 -6
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  92. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  93. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  94. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  95. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  96. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  97. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  98. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  99. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.test.tsx +80 -3
  101. package/src/components/editor/propertyPanelFont.tsx +71 -48
  102. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  106. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  107. package/src/components/editor/propertyPanelHelpers.ts +1 -2
  108. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  109. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  110. package/src/components/editor/propertyPanelSections.tsx +2 -2
  111. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  112. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  113. package/src/components/editor/studioCodeTheme.test.ts +20 -0
  114. package/src/components/editor/studioCodeTheme.ts +99 -0
  115. package/src/components/editor/useDomEditNudge.test.tsx +31 -0
  116. package/src/components/editor/useDomEditNudge.ts +2 -2
  117. package/src/components/editor/useInlineTextEditing.tsx +1 -1
  118. package/src/components/editor/useMotionPathData.test.tsx +64 -0
  119. package/src/components/editor/useMotionPathData.ts +5 -1
  120. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  121. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  122. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  123. package/src/components/nle/NLEPreview.test.ts +8 -4
  124. package/src/components/nle/PreviewOverlays.tsx +1 -1
  125. package/src/components/nle/PreviewPane.tsx +3 -3
  126. package/src/components/nle/PreviewPoster.tsx +29 -1
  127. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  128. package/src/components/panels/SlideshowPanel.tsx +4 -4
  129. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  130. package/src/components/panels/VariablesBindElement.tsx +1 -1
  131. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  132. package/src/components/panels/VariablesPanel.tsx +4 -6
  133. package/src/components/panels/VariablesRowAction.tsx +1 -1
  134. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  135. package/src/components/renders/RenderQueue.tsx +2 -2
  136. package/src/components/renders/RenderQueueItem.tsx +3 -3
  137. package/src/components/sidebar/AssetCard.tsx +3 -3
  138. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  139. package/src/components/sidebar/AssetsTab.tsx +6 -6
  140. package/src/components/sidebar/AudioRow.tsx +5 -3
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  143. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  144. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  145. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  148. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  149. package/src/components/timelineToolbarStyles.ts +3 -3
  150. package/src/components/ui/Button.tsx +4 -4
  151. package/src/components/ui/HyperframesLogo.tsx +19 -18
  152. package/src/components/ui/Input.tsx +1 -1
  153. package/src/components/ui/Menu.tsx +4 -2
  154. package/src/hooks/appHotkeysDispatch.ts +2 -1
  155. package/src/hooks/elementOffsetStager.ts +8 -7
  156. package/src/hooks/previewPromotionSelection.test.tsx +180 -0
  157. package/src/hooks/useAppHotkeys.test.ts +32 -0
  158. package/src/hooks/useAppHotkeys.ts +1 -2
  159. package/src/hooks/useClipboard.source.test.ts +141 -23
  160. package/src/hooks/useClipboard.ts +154 -123
  161. package/src/hooks/useDomEditCommits.ts +0 -3
  162. package/src/hooks/useDomEditSession.test.tsx +76 -72
  163. package/src/hooks/useDomEditSession.ts +7 -43
  164. package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
  165. package/src/hooks/useDomGeometryCommit.ts +0 -1
  166. package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
  167. package/src/hooks/useDomGeometryCommits.ts +4 -49
  168. package/src/hooks/useGsapTweenCache.ts +1 -2
  169. package/src/hooks/useInlineTextEdit.ts +16 -2
  170. package/src/hooks/useLivePreviewIframe.ts +18 -0
  171. package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
  172. package/src/hooks/usePreviewPersistence.ts +2 -5
  173. package/src/hooks/useRenderClipContent.ts +3 -3
  174. package/src/index.ts +2 -0
  175. package/src/livePreviewIframeExport.test.tsx +33 -0
  176. package/src/main.tsx +4 -1
  177. package/src/player/components/AudioWaveform.test.tsx +36 -15
  178. package/src/player/components/AudioWaveform.tsx +24 -7
  179. package/src/player/components/ClipContextMenu.tsx +1 -1
  180. package/src/player/components/CompositionThumbnail.tsx +23 -11
  181. package/src/player/components/EditModal.tsx +6 -6
  182. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  183. package/src/player/components/PlayerControls.tsx +3 -3
  184. package/src/player/components/ShortcutsPanel.tsx +8 -20
  185. package/src/player/components/TimelineCanvas.tsx +1 -1
  186. package/src/player/components/TimelineClip.test.tsx +38 -0
  187. package/src/player/components/TimelineClip.tsx +16 -8
  188. package/src/player/components/TimelineClipFades.test.tsx +376 -74
  189. package/src/player/components/TimelineClipFades.tsx +368 -137
  190. package/src/player/components/TimelineEmptyState.tsx +2 -2
  191. package/src/player/components/TimelineLanes.tsx +0 -1
  192. package/src/player/components/TrackClipCount.tsx +1 -1
  193. package/src/player/components/VolumeControl.tsx +1 -1
  194. package/src/player/components/timelineClipChildren.tsx +1 -1
  195. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  196. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  197. package/src/player/components/useTimelineKeyboardActor.ts +1 -1
  198. package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
  199. package/src/player/hooks/useShadowPreviewReload.ts +72 -5
  200. package/src/player/lib/automationStoreSync.test.ts +18 -1
  201. package/src/player/lib/automationStoreSync.ts +29 -17
  202. package/src/player/lib/thumbnailScheduler.ts +4 -5
  203. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  204. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  205. package/src/player/sceneSwap.ts +18 -0
  206. package/src/styles/components.css +26 -0
  207. package/src/styles/hex-baseline.json +7 -19
  208. package/src/styles/studio.css +4 -7
  209. package/src/styles/tailwind-preset.shared.js +2 -1
  210. package/src/styles/theme.css +214 -147
  211. package/src/styles/theme.test.ts +13 -9
  212. package/src/utils/blockCategories.ts +41 -9
  213. package/src/utils/clipboardPayload.ts +12 -1
  214. package/src/utils/studioPendingEdits.ts +21 -0
  215. package/src/utils/studioTheme.test.tsx +141 -0
  216. package/src/utils/studioTheme.ts +34 -0
  217. package/src/utils/studioUiPreferences.ts +3 -0
  218. package/src/utils/typingTarget.test.ts +8 -1
  219. package/src/utils/typingTarget.ts +5 -0
  220. package/dist/assets/index-CQN3M56N.css +0 -1
  221. package/dist/assets/index-De2aO4XM.js +0 -399
  222. package/dist/chunk-62DDWOSH.js.map +0 -1
  223. package/dist/chunk-6GAO75IU.js.map +0 -1
  224. package/dist/chunk-HIGHVWYD.js.map +0 -1
  225. package/src/hooks/gsapTargetCache.ts +0 -62
@@ -42,7 +42,6 @@ import {
42
42
  STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
43
43
  } from "./studioMotionTypes";
44
44
  import {
45
- buildPathOffsetPatches,
46
45
  buildClearPathOffsetPatches,
47
46
  buildBoxSizePatches,
48
47
  buildClearBoxSizePatches,
@@ -50,7 +49,7 @@ import {
50
49
  buildMotionPatches,
51
50
  buildClearMotionPatches,
52
51
  } from "./manualEditsDomPatches";
53
- import { applyStudioBoxSize, applyStudioPathOffset } from "./manualEditsDom";
52
+ import { applyStudioBoxSize } from "./manualEditsDom";
54
53
 
55
54
  /* ── helpers ── */
56
55
 
@@ -71,39 +70,7 @@ function assertClearCoversKeys(buildOps: PatchOperation[], clearOps: PatchOperat
71
70
 
72
71
  /* ── Path offset ─────────────────────────────────────────────────────────── */
73
72
 
74
- describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
75
- function populatedPathEl(): HTMLElement {
76
- const e = div();
77
- e.style.setProperty(STUDIO_OFFSET_X_PROP, "10px");
78
- e.style.setProperty(STUDIO_OFFSET_Y_PROP, "20px");
79
- e.style.setProperty("translate", "10px 20px");
80
- e.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, "5px 10px");
81
- e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "3px");
82
- e.style.setProperty("display", "flex");
83
- e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "block");
84
- return e;
85
- }
86
-
87
- it("populated: captures offset styles, attrs, display, and transform-display marker in declaration order", () => {
88
- const ops = buildPathOffsetPatches(populatedPathEl());
89
- expect(ops).toEqual([
90
- { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: "10px" },
91
- { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: "20px" },
92
- { type: "inline-style", property: "translate", value: "10px 20px" },
93
- { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
94
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: "5px 10px" },
95
- { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: "3px" },
96
- { type: "inline-style", property: "display", value: "flex" },
97
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "block" },
98
- ]);
99
- });
100
-
101
- it("empty: bare element yields only the path-offset marker", () => {
102
- expect(buildPathOffsetPatches(div())).toEqual([
103
- { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
104
- ]);
105
- });
106
-
73
+ describe("buildClearPathOffsetPatches", () => {
107
74
  it("clear: restores translate from STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR and display from STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR", () => {
108
75
  const e = div();
109
76
  e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "5px");
@@ -127,11 +94,6 @@ describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
127
94
  const ops = buildClearPathOffsetPatches(e);
128
95
  expect(ops.find((o) => o.property === "translate")?.value).toBeNull();
129
96
  });
130
-
131
- it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
132
- const e = populatedPathEl();
133
- assertClearCoversKeys(buildPathOffsetPatches(e), buildClearPathOffsetPatches(e));
134
- });
135
97
  });
136
98
 
137
99
  /* ── Box size ────────────────────────────────────────────────────────────── */
@@ -286,52 +248,6 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
286
248
  });
287
249
  });
288
250
 
289
- /* ── Combined box-size + path-offset (anchored-corner resize) ──────────────── */
290
-
291
- describe("anchored-corner combined patch: [...buildBoxSizePatches, ...buildPathOffsetPatches]", () => {
292
- // NW/NE/SW resize commits size AND anchor offset in ONE persist. The two
293
- // builders read the same already-mutated element and are concatenated; this
294
- // is only safe if their {type,property} keys are disjoint (no builder
295
- // overwrites the other's op when the source patcher applies them in order).
296
- it("concatenation of both builders emits disjoint {type,property} keys (no collision)", () => {
297
- const e = div();
298
- applyStudioBoxSize(e, { width: 300, height: 200 });
299
- applyStudioPathOffset(e, { x: 10, y: 20 });
300
-
301
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
302
- const keys = combined.map(opKey);
303
- expect(new Set(keys).size, `duplicate {type,property} key in combined patch: ${keys}`).toBe(
304
- keys.length,
305
- );
306
- });
307
-
308
- it("combined patch carries BOTH markers so a soft-reload re-hydrates size and offset together", () => {
309
- const e = div();
310
- applyStudioBoxSize(e, { width: 300, height: 200 });
311
- applyStudioPathOffset(e, { x: 10, y: 20 });
312
-
313
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
314
- const has = (property: string) =>
315
- combined.some((op) => op.type === "attribute" && op.property === property);
316
- expect(has(STUDIO_BOX_SIZE_ATTR)).toBe(true);
317
- expect(has(STUDIO_PATH_OFFSET_ATTR)).toBe(true);
318
- });
319
-
320
- it("order is size-first: every box-size op precedes every path-offset op", () => {
321
- const e = div();
322
- applyStudioBoxSize(e, { width: 300, height: 200 });
323
- applyStudioPathOffset(e, { x: 10, y: 20 });
324
-
325
- const boxKeys = new Set(buildBoxSizePatches(e).map(opKey));
326
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
327
- const lastBoxIdx = combined.reduce((acc, op, i) => (boxKeys.has(opKey(op)) ? i : acc), -1);
328
- const firstOffsetIdx = combined.findIndex(
329
- (op) => op.type === "attribute" && op.property === STUDIO_PATH_OFFSET_ATTR,
330
- );
331
- expect(firstOffsetIdx).toBeGreaterThan(lastBoxIdx);
332
- });
333
- });
334
-
335
251
  /* ── Rotation ────────────────────────────────────────────────────────────── */
336
252
 
337
253
  describe("buildClearRotationPatches", () => {
@@ -72,36 +72,6 @@ function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]):
72
72
 
73
73
  /* ── Path offset patches ─────────────────────────────────────────── */
74
74
 
75
- export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
76
- const ops: PatchOperation[] = [];
77
- collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP], ops);
78
- // When GSAP owns the element's transform, the live inline translate is kept
79
- // at "none" (the offset lives in GSAP's cache — see applyStudioPathOffset).
80
- // Persist the var() expression in that case, so a reload re-folds the offset.
81
- const inlineTranslate = element.style.getPropertyValue("translate");
82
- const hasOffsetVars =
83
- element.style.getPropertyValue(STUDIO_OFFSET_X_PROP) ||
84
- element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
85
- const translateValue =
86
- inlineTranslate && inlineTranslate !== "none"
87
- ? inlineTranslate
88
- : hasOffsetVars
89
- ? `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`
90
- : null;
91
- if (translateValue) {
92
- ops.push({ type: "inline-style", property: "translate", value: translateValue });
93
- }
94
- ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
95
- collectAttributeOps(
96
- element,
97
- [STUDIO_ORIGINAL_TRANSLATE_ATTR, STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR],
98
- ops,
99
- );
100
- collectInlineStyleOps(element, ["display"], ops);
101
- collectAttributeOps(element, [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR], ops);
102
- return ops;
103
- }
104
-
105
75
  export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] {
106
76
  const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
107
77
  const ops: PatchOperation[] = [
@@ -265,6 +265,7 @@ function restoreOriginalTranslateProperty(element: HTMLElement): void {
265
265
  export function clearStudioPathOffset(element: HTMLElement): void {
266
266
  if (
267
267
  element.hasAttribute(STUDIO_PATH_OFFSET_ATTR) ||
268
+ element.hasAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR) ||
268
269
  styleUsesStudioOffset(element.style.getPropertyValue("translate"))
269
270
  ) {
270
271
  restoreOriginalTranslateProperty(element);
@@ -1,10 +1,20 @@
1
1
  // @vitest-environment happy-dom
2
2
  import { act, useRef } from "react";
3
3
  import type { Root } from "react-dom/client";
4
- import { afterEach, beforeEach, expect, it } from "vitest";
4
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
5
5
  import { installReactActEnvironment, mountReactHarness } from "../../hooks/domSelectionTestHarness";
6
6
  import { useMarqueeGestures } from "./marqueeCommit";
7
7
 
8
+ // Layout stands in for a real preview: every element is visible and sits at the band's start.
9
+ vi.mock("./domEditingElement", async (importOriginal) => ({
10
+ ...(await importOriginal<typeof import("./domEditingElement")>()),
11
+ isElementComputedVisible: () => true,
12
+ }));
13
+ vi.mock("./domEditOverlayGeometry", async (importOriginal) => ({
14
+ ...(await importOriginal<typeof import("./domEditOverlayGeometry")>()),
15
+ toVisibleOverlayRect: () => ({ left: 10, top: 10, width: 40, height: 40 }),
16
+ }));
17
+
8
18
  installReactActEnvironment();
9
19
  HTMLElement.prototype.setPointerCapture ??= () => {};
10
20
  HTMLElement.prototype.releasePointerCapture ??= () => {};
@@ -82,3 +92,61 @@ it("an escape with no band in flight still reaches the host", () => {
82
92
  expect(event.defaultPrevented).toBe(false);
83
93
  expect(hostEscapes).toHaveLength(1);
84
94
  });
95
+
96
+ it("a reload promoted mid-band selects from the preview on screen, not the retired one", async () => {
97
+ const preview = () => {
98
+ const iframe = document.createElement("iframe");
99
+ const doc = document.implementation.createHTMLDocument("preview");
100
+ doc.body.innerHTML = '<div data-composition-id="main"><h1 id="title">Title</h1></div>';
101
+ Object.defineProperty(iframe, "contentDocument", { value: doc });
102
+ return iframe;
103
+ };
104
+ const [retired, live] = [preview(), preview()];
105
+ const picked: HTMLElement[][] = [];
106
+ const iframeRef = { current: retired as HTMLIFrameElement | null };
107
+ function Band() {
108
+ const overlayRef = useRef<HTMLDivElement>(null);
109
+ const marquee = useMarqueeGestures({
110
+ iframeRef,
111
+ overlayRef,
112
+ activeCompositionPathRef: useRef<string | null>("index.html"),
113
+ onMarqueeSelectRef: useRef(() => undefined),
114
+ resolveHits: async (elements: HTMLElement[]) => {
115
+ picked.push(elements);
116
+ return [];
117
+ },
118
+ });
119
+ return (
120
+ <div
121
+ ref={overlayRef}
122
+ data-band-overlay
123
+ onPointerDown={marquee.begin}
124
+ onPointerMove={marquee.onPointerMove}
125
+ onPointerUp={marquee.onPointerUp}
126
+ />
127
+ );
128
+ }
129
+ const band = mountReactHarness(<Band />);
130
+ const fire = (type: string, x: number, y: number) =>
131
+ act(() => {
132
+ document.querySelector("[data-band-overlay]")!.dispatchEvent(
133
+ new PointerEvent(type, {
134
+ bubbles: true,
135
+ button: 0,
136
+ pointerId: 1,
137
+ clientX: x,
138
+ clientY: y,
139
+ }),
140
+ );
141
+ });
142
+ try {
143
+ fire("pointerdown", 0, 0);
144
+ fire("pointermove", 120, 90);
145
+ iframeRef.current = live;
146
+ await act(async () => fire("pointerup", 120, 90));
147
+ expect(picked.at(-1)?.length).toBeGreaterThan(0);
148
+ for (const element of picked.at(-1)!) expect(element.ownerDocument).toBe(live.contentDocument);
149
+ } finally {
150
+ act(() => band.unmount());
151
+ }
152
+ });
@@ -159,7 +159,12 @@ export function useMarqueeGestures<T>(deps: MarqueeGesturesDeps<T>): MarqueeGest
159
159
  const overlay = deps.overlayRef.current;
160
160
  if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
161
161
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
162
- const candidates = candidatesRef.current ?? collectMarqueeCandidates(iframe, overlay, acp);
162
+ const cached = candidatesRef.current;
163
+ const measuredOnScreen = cached?.every(
164
+ (hit) => hit.element.ownerDocument === iframe.contentDocument,
165
+ );
166
+ const candidates =
167
+ cached && measuredOnScreen ? cached : collectMarqueeCandidates(iframe, overlay, acp);
163
168
  const elements = hitsWithin(rect, candidates).map((hit) => hit.element);
164
169
  const resolveHits = deps.resolveHits;
165
170
  const picks = resolveHits
@@ -3,8 +3,10 @@
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import {
5
5
  IDLE_POLL_MS,
6
+ IDLE_SLICE_MS,
6
7
  requestOverlayFrames,
7
8
  resetOverlayFrameLoopForTests,
9
+ runWhenInputIdle,
8
10
  subscribeOverlayFrame,
9
11
  } from "./overlayFrameLoop";
10
12
  import { usePlayerStore } from "../../player/store/playerStore";
@@ -71,6 +73,31 @@ describe("overlay frame loop", () => {
71
73
  expect(runs).toBeGreaterThan(0);
72
74
  });
73
75
 
76
+ it("runs idle work in capped slices, only while no input arrives", () => {
77
+ const budgets: number[] = [];
78
+ requestOverlayFrames();
79
+ runWhenInputIdle((timeLeft) => {
80
+ budgets.push(timeLeft());
81
+ return budgets.length === 3;
82
+ });
83
+ vi.advanceTimersByTime(300);
84
+ expect(budgets).toEqual([]);
85
+ vi.advanceTimersByTime(200);
86
+ expect(budgets).toHaveLength(3);
87
+ expect(Math.max(...budgets)).toBeLessThanOrEqual(IDLE_SLICE_MS);
88
+
89
+ // Input during a slice holds the next one until the loop has been idle again.
90
+ const slices: number[] = [];
91
+ runWhenInputIdle(() => {
92
+ slices.push(performance.now());
93
+ if (slices.length === 1) requestOverlayFrames();
94
+ return slices.length === 2;
95
+ });
96
+ vi.advanceTimersByTime(1000);
97
+ expect(slices).toHaveLength(2);
98
+ expect(slices[1] - slices[0]).toBeGreaterThanOrEqual(400);
99
+ });
100
+
74
101
  it("runs every subscriber on one frame, not one frame each", () => {
75
102
  const calls: string[] = [];
76
103
  subscribeOverlayFrame(() => calls.push("a"));
@@ -143,6 +143,27 @@ function schedule(): void {
143
143
  }, IDLE_POLL_MS);
144
144
  }
145
145
 
146
+ /** Longest idle slice, so input that arrives during one waits at most this long for its frame. */
147
+ export const IDLE_SLICE_MS = 3;
148
+
149
+ /**
150
+ * Runs `step` in idle slices once no input has woken the loop for AWAKE_MS, so it never shares a gesture's
151
+ * frames. `step` works while `timeLeft()` is positive and returns true when it has finished.
152
+ */
153
+ export function runWhenInputIdle(step: (timeLeft: () => number) => boolean): void {
154
+ const wait = awakeUntil - performance.now();
155
+ if (wait > 0) {
156
+ setTimeout(() => runWhenInputIdle(step), wait);
157
+ return;
158
+ }
159
+ const idle = window.requestIdleCallback ?? ((callback: () => void) => setTimeout(callback, 0));
160
+ idle(() => {
161
+ const end = performance.now() + IDLE_SLICE_MS;
162
+ if (performance.now() < awakeUntil || !step(() => end - performance.now()))
163
+ runWhenInputIdle(step);
164
+ });
165
+ }
166
+
146
167
  /** Something moved, or might have. Run frames at full rate for a moment. */
147
168
  export function requestOverlayFrames(): void {
148
169
  awakeUntil = performance.now() + AWAKE_MS;
@@ -14,8 +14,6 @@ import {
14
14
  buildDomEditStylePatchOperation,
15
15
  buildDomEditTextPatchOperation,
16
16
  } from "./domEditingLayers";
17
- import { buildPathOffsetPatches } from "./manualEditsDomPatches";
18
- import { STUDIO_OFFSET_X_PROP, STUDIO_PATH_OFFSET_ATTR } from "./manualEditsTypes";
19
17
  import { makeSelection } from "../../hooks/domSelectionTestHarness";
20
18
  import { buildTextFieldChildOperations } from "../../hooks/domEditTextFieldCommitOps";
21
19
 
@@ -126,22 +124,6 @@ describe("persist seam source mutation", () => {
126
124
  expect(findByHfId(html, "qa-image").getAttribute("style")).toContain("opacity: 0.4");
127
125
  });
128
126
 
129
- it("persists detached jsdom path offset operations", () => {
130
- const element = document.createElement("div");
131
- element.style.setProperty(STUDIO_OFFSET_X_PROP, "24px");
132
-
133
- const html = patchAndExpectChange(
134
- indexHtml,
135
- clientTarget({ id: "qa-shape", hfId: "qa-shape", tagName: "div" }),
136
- buildPathOffsetPatches(element),
137
- );
138
- const shape = findByHfId(html, "qa-shape");
139
-
140
- expect(shape.getAttribute("style")).toContain(`${STUDIO_OFFSET_X_PROP}: 24px`);
141
- expect(shape.getAttribute("style")).toContain("translate: var(--hf-studio-offset-x, 0px)");
142
- expect(shape.getAttribute(STUDIO_PATH_OFFSET_ATTR)).toBe("true");
143
- });
144
-
145
127
  it("persists timeline data-start attribute operation", () => {
146
128
  const html = patchAndExpectChange(
147
129
  indexHtml,
@@ -1,8 +1,14 @@
1
1
  import { roundTo3 } from "../../utils/rounding";
2
2
  import { gsapWritesPosition, gsapWritesRotation } from "../../hooks/gsapRuntimeKeyframes";
3
3
  import { readCssRotation } from "../../hooks/draggedGsapPosition";
4
- import { readStudioPathOffset, readStudioRotation } from "./manualEditsDom";
4
+ import { readStudioPathOffset, readStudioRotation, styleUsesStudioOffset } from "./manualEditsDom";
5
5
  import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
6
+ import {
7
+ STUDIO_OFFSET_X_PROP,
8
+ STUDIO_OFFSET_Y_PROP,
9
+ STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
10
+ STUDIO_PATH_OFFSET_ATTR,
11
+ } from "./manualEditsTypes";
6
12
  import type { PatchOperation } from "../../utils/sourcePatcher";
7
13
 
8
14
  type Point = { x: number; y: number };
@@ -102,11 +108,36 @@ function formatTranslatePx(p: Point): string {
102
108
  return `${roundTo3(p.x)}px ${roundTo3(p.y)}px`;
103
109
  }
104
110
 
105
- export function translatePatch(p: Point): PatchOperation & { value: string } {
106
- return { type: "inline-style", property: "translate", value: formatTranslatePx(p) };
111
+ const LEGACY_OFFSET_VARS = [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP];
112
+
113
+ /** Writes a move as the element's own translate; ends a legacy offset's vars and mark. */
114
+ export function writePlainMove(el: HTMLElement, p: Point): PatchOperation[] {
115
+ writeTranslatePx(el, p);
116
+ const patches: PatchOperation[] = [
117
+ { type: "inline-style", property: "translate", value: formatTranslatePx(p) },
118
+ {
119
+ type: "attribute",
120
+ property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
121
+ value: el.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR),
122
+ },
123
+ ];
124
+ for (const prop of LEGACY_OFFSET_VARS.filter((v) => el.style.getPropertyValue(v))) {
125
+ el.style.removeProperty(prop);
126
+ patches.push({ type: "inline-style", property: prop, value: null });
127
+ }
128
+ if (el.hasAttribute(STUDIO_PATH_OFFSET_ATTR)) {
129
+ el.removeAttribute(STUDIO_PATH_OFFSET_ATTR);
130
+ patches.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: null });
131
+ }
132
+ return patches;
107
133
  }
108
134
 
135
+ /** The first write records the author's own translate ("" for none), which Reset puts back. */
109
136
  export function writeTranslatePx(el: HTMLElement, p: Point): void {
137
+ if (!el.hasAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR)) {
138
+ const own = el.style.getPropertyValue("translate");
139
+ el.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, styleUsesStudioOffset(own) ? "" : own);
140
+ }
110
141
  el.style.setProperty("translate", formatTranslatePx(p));
111
142
  }
112
143
 
@@ -477,7 +477,7 @@ export function CurveGraph({
477
477
  onSettle();
478
478
  }
479
479
  }}
480
- className="mx-auto aspect-square w-full max-w-[200px] touch-none rounded-sm border border-panel-border-input bg-black/20 outline-hidden focus:ring-1 focus:ring-panel-accent"
480
+ className="mx-auto aspect-square w-full max-w-[200px] touch-none rounded-sm scheme-dark border border-panel-border-input bg-bg-0 outline-hidden focus:ring-1 focus:ring-inset focus:ring-panel-accent"
481
481
  >
482
482
  <defs>
483
483
  <linearGradient id={`hf-hue-axis-${tab.key}`}>
@@ -491,7 +491,7 @@ export function CurveGraph({
491
491
  </linearGradient>
492
492
  </defs>
493
493
  {[0.25, 0.5, 0.75].map((ratio) => (
494
- <g key={ratio} stroke="rgba(255,255,255,0.08)" strokeWidth="0.5">
494
+ <g key={ratio} className="stroke-text-0/10" strokeWidth="0.5">
495
495
  <line
496
496
  x1={GRAPH_PADDING}
497
497
  y1={GRAPH_PADDING + ratio * (GRAPH_SIZE - GRAPH_PADDING * 2)}
@@ -431,7 +431,7 @@ export function ColorGradingControls({
431
431
  />
432
432
  </div>
433
433
  {lutImportError && (
434
- <div className="text-[10px] text-red-400" role="alert">
434
+ <div className="text-[10px] text-danger-ink" role="alert">
435
435
  {lutImportError}
436
436
  </div>
437
437
  )}
@@ -14,11 +14,11 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
14
14
  function StatusPill({ status }: { status: RuntimeColorGradingStatus }) {
15
15
  const dotClass =
16
16
  status.state === "active"
17
- ? "bg-emerald-400"
17
+ ? "bg-accent"
18
18
  : status.state === "pending"
19
- ? "bg-amber-300"
19
+ ? "bg-warning-ink"
20
20
  : status.state === "unavailable"
21
- ? "bg-red-400"
21
+ ? "bg-danger-ink"
22
22
  : "bg-panel-text-5";
23
23
  return (
24
24
  <div
@@ -44,18 +44,18 @@ function HdrMediaWarning({ metadata }: { metadata: MediaMetadata | null }) {
44
44
  .join(" · ");
45
45
 
46
46
  return (
47
- <div className="mb-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-4 text-amber-100">
47
+ <div className="mb-3 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-4 text-warning-ink">
48
48
  <div className="mb-1 flex min-w-0 items-center justify-between gap-2">
49
49
  <span className="font-semibold">{metadata.color.label} source</span>
50
- <span className="rounded-sm bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
50
+ <span className="rounded-sm bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-warning-ink">
51
51
  SDR preview
52
52
  </span>
53
53
  </div>
54
- <p className="text-amber-100/80">
54
+ <p className="text-text-2">
55
55
  These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
56
56
  is not true HDR color grading yet.
57
57
  </p>
58
- {details && <p className="mt-1 truncate text-[10px] text-amber-100/55">{details}</p>}
58
+ {details && <p className="mt-1 truncate text-[10px] text-text-muted">{details}</p>}
59
59
  </div>
60
60
  );
61
61
  }
@@ -120,7 +120,7 @@ function HoldBeforeButton({
120
120
  }}
121
121
  className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
122
122
  active
123
- ? "bg-studio-accent text-black"
123
+ ? "bg-studio-accent text-on-accent"
124
124
  : "text-panel-text-4 hover:bg-panel-hover hover:text-panel-text-1"
125
125
  } disabled:cursor-not-allowed disabled:opacity-40`}
126
126
  title="Hold to show original"
@@ -158,7 +158,7 @@ export function ColorGradingSliderControl({
158
158
  settings.onClick();
159
159
  }}
160
160
  className={`relative flex h-5 w-5 shrink-0 items-center justify-center rounded transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40 ${
161
- settings.active ? "text-studio-accent" : "text-panel-text-5"
161
+ settings.active ? "text-accent-ink" : "text-panel-text-5"
162
162
  }`}
163
163
  title={settings.label}
164
164
  >
@@ -204,13 +204,13 @@ export function PropertyPanelColorSecondary({
204
204
  setSampling(false);
205
205
  }
206
206
  }}
207
- className="flex min-h-7 items-center gap-1.5 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:opacity-50"
207
+ className="flex min-h-7 items-center gap-1.5 text-[10px] font-medium text-accent-ink hover:text-accent-ink/80 disabled:opacity-50"
208
208
  >
209
209
  <Eyedropper size={12} />
210
210
  {sampling ? "Capturing frame" : "Sample color from frame"}
211
211
  </button>
212
212
  {captureError && (
213
- <p role="alert" className="text-[9px] leading-4 text-red-300">
213
+ <p role="alert" className="text-[9px] leading-4 text-danger-ink">
214
214
  {captureError}
215
215
  </p>
216
216
  )}
@@ -221,7 +221,7 @@ export function PropertyPanelColorSecondary({
221
221
  type="button"
222
222
  aria-pressed={!showMatte}
223
223
  onClick={() => setShowMatte(false)}
224
- className={!showMatte ? "text-panel-accent" : "text-panel-text-4"}
224
+ className={!showMatte ? "text-accent-ink" : "text-panel-text-4"}
225
225
  >
226
226
  Source
227
227
  </button>
@@ -229,7 +229,7 @@ export function PropertyPanelColorSecondary({
229
229
  type="button"
230
230
  aria-pressed={showMatte}
231
231
  onClick={() => setShowMatte(true)}
232
- className={showMatte ? "text-panel-accent" : "text-panel-text-4"}
232
+ className={showMatte ? "text-accent-ink" : "text-panel-text-4"}
233
233
  >
234
234
  Selection matte
235
235
  </button>
@@ -131,7 +131,7 @@ export function ImageFillField({
131
131
  className={`inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors ${
132
132
  disabled || uploading
133
133
  ? "cursor-not-allowed text-neutral-600"
134
- : "cursor-pointer hover:border-neutral-600 hover:text-white"
134
+ : "cursor-pointer hover:border-neutral-600 hover:text-text-0"
135
135
  }`}
136
136
  >
137
137
  <Plus size={12} className="shrink-0" />
@@ -151,7 +151,7 @@ export function ImageFillField({
151
151
  />
152
152
  </div>
153
153
  {uploadError && (
154
- <div className="text-[10px] text-red-400" role="alert">
154
+ <div className="text-[10px] text-danger-ink" role="alert">
155
155
  {uploadError}
156
156
  </div>
157
157
  )}
@@ -340,7 +340,7 @@ export function GradientField({
340
340
  track("toggle", "Repeat gradient");
341
341
  patch({ repeating: e.target.checked });
342
342
  }}
343
- className="h-4 w-4 rounded-sm border-neutral-700 bg-neutral-950 text-panel-accent focus:ring-panel-accent"
343
+ className="h-4 w-4 rounded-sm border-neutral-700 bg-neutral-950 text-accent-ink focus:ring-panel-accent"
344
344
  />
345
345
  Repeat
346
346
  </label>
@@ -357,7 +357,7 @@ export function GradientField({
357
357
  })),
358
358
  });
359
359
  }}
360
- className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-600"
360
+ className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0 disabled:cursor-not-allowed disabled:text-neutral-600"
361
361
  >
362
362
  <ReverseGradientIcon size={16} />
363
363
  Reverse
@@ -442,7 +442,7 @@ export function GradientField({
442
442
  disabled={disabled || parsed.stops.length >= 6}
443
443
  onClick={() => addStop()}
444
444
  title={parsed.stops.length >= 6 ? "Maximum 6 stops" : "Add a gradient stop"}
445
- className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white active:scale-[0.98] disabled:cursor-not-allowed disabled:text-neutral-600"
445
+ className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-text-0 active:scale-[0.98] disabled:cursor-not-allowed disabled:text-neutral-600"
446
446
  >
447
447
  <Plus size={12} />
448
448
  Add stop
@@ -474,7 +474,7 @@ export function GradientField({
474
474
  type="button"
475
475
  disabled={disabled || parsed.stops.length <= 2}
476
476
  onClick={() => removeStop(index)}
477
- className="mt-[22px] flex h-10 items-center justify-center rounded-lg border border-neutral-700 bg-neutral-950 text-neutral-400 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-700"
477
+ className="mt-[22px] flex h-10 items-center justify-center rounded-lg border border-neutral-700 bg-neutral-950 text-neutral-400 transition-colors hover:border-neutral-600 hover:text-text-0 disabled:cursor-not-allowed disabled:text-text-off"
478
478
  aria-label={`Remove stop ${index + 1}`}
479
479
  >
480
480
  <X size={12} />
@@ -5,9 +5,9 @@ import { Compare, RotateCcw } from "../../icons/SystemIcons";
5
5
  import type { ColorGradingControllerState } from "./useColorGradingController";
6
6
 
7
7
  const STATUS_DOT_CLASS: Record<ColorGradingControllerState["runtimeStatus"]["state"], string> = {
8
- active: "bg-emerald-400",
9
- pending: "bg-amber-300",
10
- unavailable: "bg-red-400",
8
+ active: "bg-accent",
9
+ pending: "bg-warning-ink",
10
+ unavailable: "bg-danger-ink",
11
11
  missing: "bg-panel-text-5",
12
12
  inactive: "bg-panel-text-5",
13
13
  };
@@ -81,7 +81,7 @@ describe("FlatColorGradingAccessory", () => {
81
81
  const dot = host.querySelector('[data-flat-grade-status-dot="true"]');
82
82
  expect(dot).not.toBeNull();
83
83
  expect(dot?.getAttribute("title")).toBe("Shader active");
84
- expect(dot?.className).toContain("bg-emerald-400");
84
+ expect(dot?.className).toContain("bg-accent");
85
85
  act(() => root.unmount());
86
86
  });
87
87