@hyperframes/studio 0.8.104 → 0.8.105

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 (169) hide show
  1. package/dist/assets/{hyperframes-player-DAoaajuM.js → hyperframes-player-tnfCiVdR.js} +1 -1
  2. package/dist/assets/{index-BDK4rTMb.js → index-Ca6LZlVb.js} +1 -1
  3. package/dist/assets/index-Cre9X4RS.css +1 -0
  4. package/dist/assets/{index-De2aO4XM.js → index-DEDhPmPD.js} +205 -205
  5. package/dist/assets/{index-WV9LroAD.js → index-zpe332hb.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-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
  9. package/dist/chunk-NCUBAVGK.js.map +1 -0
  10. package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
  11. package/dist/index.css +6 -1
  12. package/dist/index.css.map +1 -1
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +614 -638
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles/tailwind-preset.js.map +1 -1
  18. package/dist/styles.css +1 -1
  19. package/dist/ui.js +2 -2
  20. package/package.json +8 -8
  21. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  22. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  23. package/src/captions/components/CaptionTimeline.tsx +3 -3
  24. package/src/components/AskAgentModal.tsx +1 -1
  25. package/src/components/LintModal.tsx +35 -35
  26. package/src/components/StudioErrorBoundary.tsx +1 -1
  27. package/src/components/StudioHeader.dom.test.tsx +4 -4
  28. package/src/components/StudioHeader.tsx +2 -1
  29. package/src/components/StudioToast.tsx +7 -16
  30. package/src/components/TimelineToolPicker.tsx +1 -1
  31. package/src/components/TimelineToolbar.tsx +11 -11
  32. package/src/components/dock/Dock.tsx +1 -1
  33. package/src/components/dock/dock.css +9 -1
  34. package/src/components/editor/AnimationCard.tsx +5 -5
  35. package/src/components/editor/AnimationCardParts.tsx +1 -1
  36. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  37. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  38. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  39. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  40. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  41. package/src/components/editor/EaseCurveSection.tsx +14 -19
  42. package/src/components/editor/EaseModeControls.tsx +2 -2
  43. package/src/components/editor/EaseParamFields.tsx +8 -8
  44. package/src/components/editor/FileTree.tsx +1 -1
  45. package/src/components/editor/FileTreeIcons.tsx +47 -26
  46. package/src/components/editor/FileTreeNodes.tsx +11 -8
  47. package/src/components/editor/GestureRecordControl.tsx +3 -3
  48. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  49. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  50. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  51. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  52. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  53. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  54. package/src/components/editor/LayersPanel.tsx +3 -3
  55. package/src/components/editor/PromotableControl.tsx +2 -2
  56. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  57. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  58. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  59. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  60. package/src/components/editor/SnapToolbar.tsx +5 -5
  61. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  62. package/src/components/editor/Transform3DCube.tsx +1 -1
  63. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  64. package/src/components/editor/easeCurveSvg.tsx +1 -1
  65. package/src/components/editor/manualEditsDom.ts +3 -3
  66. package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
  67. package/src/components/editor/manualEditsDomPatches.ts +0 -30
  68. package/src/components/editor/manualEditsSnapshot.ts +1 -0
  69. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  70. package/src/components/editor/plainTranslate.ts +34 -3
  71. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  72. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  73. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  74. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  75. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  76. package/src/components/editor/propertyPanelFill.tsx +6 -6
  77. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  78. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  79. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  80. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  81. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  82. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  83. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  84. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  85. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  86. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  87. package/src/components/editor/propertyPanelFont.tsx +2 -2
  88. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  89. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  90. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  91. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  92. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  93. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  94. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  95. package/src/components/editor/propertyPanelSections.tsx +2 -2
  96. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  97. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  98. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  99. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  100. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  101. package/src/components/nle/NLEPreview.test.ts +8 -4
  102. package/src/components/nle/PreviewOverlays.tsx +1 -1
  103. package/src/components/nle/PreviewPane.tsx +3 -3
  104. package/src/components/nle/PreviewPoster.tsx +29 -1
  105. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  106. package/src/components/panels/SlideshowPanel.tsx +4 -4
  107. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  108. package/src/components/panels/VariablesBindElement.tsx +1 -1
  109. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  110. package/src/components/panels/VariablesPanel.tsx +4 -6
  111. package/src/components/panels/VariablesRowAction.tsx +1 -1
  112. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  113. package/src/components/renders/RenderQueue.tsx +2 -2
  114. package/src/components/renders/RenderQueueItem.tsx +3 -3
  115. package/src/components/sidebar/AssetCard.tsx +3 -3
  116. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  117. package/src/components/sidebar/AssetsTab.tsx +6 -6
  118. package/src/components/sidebar/AudioRow.tsx +5 -3
  119. package/src/components/sidebar/BlocksTab.tsx +6 -6
  120. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  121. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  122. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  123. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  124. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  125. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  126. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  127. package/src/components/timelineToolbarStyles.ts +3 -3
  128. package/src/components/ui/Button.tsx +4 -4
  129. package/src/components/ui/HyperframesLogo.tsx +19 -18
  130. package/src/components/ui/Input.tsx +1 -1
  131. package/src/components/ui/Menu.tsx +4 -2
  132. package/src/hooks/elementOffsetStager.ts +8 -7
  133. package/src/hooks/useClipboard.source.test.ts +42 -4
  134. package/src/hooks/useClipboard.ts +13 -3
  135. package/src/hooks/useDomEditCommits.ts +0 -3
  136. package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
  137. package/src/hooks/useDomGeometryCommit.ts +0 -1
  138. package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
  139. package/src/hooks/useDomGeometryCommits.ts +4 -49
  140. package/src/player/components/ClipContextMenu.tsx +1 -1
  141. package/src/player/components/CompositionThumbnail.tsx +23 -11
  142. package/src/player/components/EditModal.tsx +6 -6
  143. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  144. package/src/player/components/PlayerControls.tsx +3 -3
  145. package/src/player/components/ShortcutsPanel.tsx +8 -20
  146. package/src/player/components/TimelineCanvas.tsx +1 -1
  147. package/src/player/components/TimelineClip.tsx +2 -2
  148. package/src/player/components/TimelineEmptyState.tsx +2 -2
  149. package/src/player/components/TimelineLanes.tsx +0 -1
  150. package/src/player/components/TrackClipCount.tsx +1 -1
  151. package/src/player/components/VolumeControl.tsx +1 -1
  152. package/src/player/components/timelineClipChildren.tsx +1 -1
  153. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  154. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  155. package/src/player/lib/thumbnailScheduler.ts +4 -5
  156. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  157. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  158. package/src/styles/hex-baseline.json +7 -19
  159. package/src/styles/studio.css +0 -9
  160. package/src/styles/tailwind-preset.shared.js +2 -1
  161. package/src/styles/theme.css +204 -147
  162. package/src/styles/theme.test.ts +13 -9
  163. package/src/utils/blockCategories.ts +41 -9
  164. package/src/utils/clipboardPayload.ts +2 -0
  165. package/dist/assets/index-CQN3M56N.css +0 -1
  166. package/dist/chunk-62DDWOSH.js.map +0 -1
  167. package/dist/chunk-6GAO75IU.js.map +0 -1
  168. package/src/hooks/gsapTargetCache.ts +0 -62
  169. /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
@@ -4,7 +4,7 @@ import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
4
4
 
5
5
  const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
6
6
  const ICON_COLOR_BY_KIND = {
7
- text: "text-panel-accent",
7
+ text: "text-accent-ink",
8
8
  media: "text-panel-media",
9
9
  other: "text-panel-container",
10
10
  } as const;
@@ -92,7 +92,7 @@ export function PropertyPanelFlatHeader({
92
92
  track("button", "Copy element info");
93
93
  onCopy();
94
94
  }}
95
- className={copied ? "text-panel-accent" : undefined}
95
+ className={copied ? "text-accent-ink" : undefined}
96
96
  >
97
97
  <ClipboardList size={13} />
98
98
  </button>
@@ -73,7 +73,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
73
73
  type="button"
74
74
  className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
75
75
  motionPathArmed
76
- ? "bg-studio-accent/20 text-studio-accent"
76
+ ? "bg-studio-accent/20 text-accent-ink"
77
77
  : "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
78
78
  }`}
79
79
  onClick={() => setMotionPathArmed(!motionPathArmed)}
@@ -99,7 +99,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
99
99
  type="button"
100
100
  className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
101
101
  visible
102
- ? "bg-studio-accent/20 text-studio-accent"
102
+ ? "bg-studio-accent/20 text-accent-ink"
103
103
  : "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
104
104
  }`}
105
105
  onClick={toggle}
@@ -115,7 +115,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
115
115
  type="button"
116
116
  className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
117
117
  prefs.snapEnabled
118
- ? "bg-studio-accent/20 text-studio-accent"
118
+ ? "bg-studio-accent/20 text-accent-ink"
119
119
  : "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
120
120
  }`}
121
121
  onClick={toggleSnap}
@@ -131,7 +131,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
131
131
  type="button"
132
132
  className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
133
133
  prefs.gridVisible
134
- ? "bg-studio-accent/20 text-studio-accent"
134
+ ? "bg-studio-accent/20 text-accent-ink"
135
135
  : "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
136
136
  }`}
137
137
  onClick={toggleGrid}
@@ -164,7 +164,7 @@ export const SnapToolbar = memo(function SnapToolbar() {
164
164
  {gridPopoverOpen && (
165
165
  <div
166
166
  ref={popoverRef}
167
- className="absolute right-0 top-full mt-1 rounded-lg bg-neutral-800 border border-neutral-700 p-3 shadow-xl min-w-[180px]"
167
+ className="absolute right-0 top-full mt-1 rounded-lg bg-raised border border-border p-3 shadow-xl min-w-[180px]"
168
168
  >
169
169
  <label className="flex items-center justify-between text-xs text-white/80 mb-2">
170
170
  <span>Grid spacing</span>
@@ -139,7 +139,7 @@ export function TimelineFxPopover({
139
139
  ref={rootRef}
140
140
  role="dialog"
141
141
  aria-label="Effects"
142
- className="z-200 flex flex-col overflow-hidden rounded-md border border-white/10 bg-[#1b1b1f] p-2 shadow-xl"
142
+ className="z-200 flex flex-col overflow-hidden rounded-md border border-border bg-raised p-2 shadow-xl"
143
143
  style={clampedStyle(anchorRect)}
144
144
  onKeyDown={onKeyDown}
145
145
  onPointerDown={(event) => event.stopPropagation()}
@@ -165,10 +165,10 @@ export function TimelineFxPopover({
165
165
  onAuditionTracked={(id) => trackPresetAuditioned(id, { trackKind })}
166
166
  />
167
167
  </div>
168
- <div className="mt-2 flex shrink-0 items-center justify-between border-t border-white/10 pt-2 text-[10px] text-white/55">
168
+ <div className="mt-2 flex shrink-0 items-center justify-between border-t border-border pt-2 text-[10px] text-text-muted">
169
169
  <button
170
170
  type="button"
171
- className="hover:text-white"
171
+ className="hover:text-text-0"
172
172
  onClick={() => {
173
173
  onClose();
174
174
  onOpenRack();
@@ -178,7 +178,7 @@ export function TimelineFxPopover({
178
178
  </button>
179
179
  <button
180
180
  type="button"
181
- className="hover:text-white"
181
+ className="hover:text-text-0"
182
182
  onClick={() => {
183
183
  onClose();
184
184
  onOpenRack();
@@ -314,7 +314,7 @@ export function Transform3DCube({
314
314
  aria-label="Keyframe 3D transform"
315
315
  aria-pressed={keyframed}
316
316
  className={`absolute left-1.5 top-1.5 rounded p-0.5 hover:bg-neutral-800 ${
317
- keyframed ? "text-[#5ff0bf]" : "text-neutral-500 hover:text-neutral-200"
317
+ keyframed ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
318
318
  }`}
319
319
  >
320
320
  <svg
@@ -1,12 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
  import { afterEach, describe, expect, it } from "vitest";
3
- import { applyStudioBoxSize, applyStudioPathOffset, readStudioBoxSize } from "./manualEditsDom";
4
- import { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply";
5
- import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches";
6
- import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag";
7
3
  import { computeNextResizeAnchor } from "./domEditResizeLocal";
8
- import type { PatchOperation } from "../../utils/sourcePatcher";
9
- import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
10
4
 
11
5
  /**
12
6
  * Center-anchored corner resize (CapCut model): the element scales about its
@@ -38,40 +32,6 @@ afterEach(() => {
38
32
  document.body.innerHTML = "";
39
33
  });
40
34
 
41
- /** Apply a built PatchOperation[] to a live element, mirroring sourcePatcher's
42
- * inline-style / attribute application — i.e. what the persisted source carries
43
- * when it is re-parsed into the DOM on the next preview load. */
44
- function applyPatchesToElement(el: HTMLElement, ops: PatchOperation[]): void {
45
- for (const op of ops) {
46
- if (op.type === "inline-style") {
47
- if (op.value === null) el.style.removeProperty(op.property);
48
- else el.style.setProperty(op.property, op.value);
49
- } else if (op.type === "attribute") {
50
- if (op.value === null) el.removeAttribute(op.property);
51
- else el.setAttribute(op.property, op.value);
52
- }
53
- }
54
- }
55
-
56
- /** Net translate applied to an element, resolving the studio offset var()
57
- * expression to its px value so we compare the actually-rendered translation. */
58
- function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } {
59
- const raw = el.style.getPropertyValue("translate").trim();
60
- if (!raw || raw === "none") return { x: 0, y: 0 };
61
- const vx = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-x")) || 0;
62
- const vy = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-y")) || 0;
63
- const parts = splitTopLevelWhitespace(raw);
64
- const parseAxis = (part: string, varVal: number): number => {
65
- if (part && part.includes("--hf-studio-offset")) return varVal;
66
- const n = Number.parseFloat(part);
67
- return Number.isFinite(n) ? n : 0;
68
- };
69
- return {
70
- x: parseAxis(parts[0] ?? "", vx),
71
- y: parseAxis(parts[1] ?? "", vy),
72
- };
73
- }
74
-
75
35
  describe("center-anchored corner resize — no shift after release", () => {
76
36
  it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => {
77
37
  // Model the pointermove anchor loop that pins the element's CENTER. The physical
@@ -157,48 +117,4 @@ describe("center-anchored corner resize — no shift after release", () => {
157
117
  }
158
118
  }
159
119
  });
160
-
161
- it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => {
162
- // The committed offset flows through the real apply → persist → reload chain
163
- // unchanged (this hop was proved clean; the shift is upstream in the anchor
164
- // loop above, not in persistence).
165
- const el = document.createElement("div");
166
- el.style.setProperty("width", "200px");
167
- el.style.setProperty("height", "100px");
168
- document.body.appendChild(el);
169
-
170
- const anchorDx = -30;
171
- const anchorDy = -18;
172
- const finalSize = { width: 240, height: 130 };
173
-
174
- applyStudioBoxSize(el, finalSize);
175
- const memberResult = createManualOffsetDragMember({
176
- key: "k",
177
- selection: { element: el } as never,
178
- element: el,
179
- rect: { left: 0, top: 0, width: 240, height: 130, editScaleX: 1, editScaleY: 1 },
180
- });
181
- expect(memberResult.ok).toBe(true);
182
- if (!memberResult.ok) return;
183
-
184
- const finalOffset = applyManualOffsetDragCommit(memberResult.member, anchorDx, anchorDy);
185
-
186
- applyStudioBoxSize(el, finalSize);
187
- const patches = buildBoxSizePatches(el);
188
- applyStudioPathOffset(el, finalOffset);
189
- patches.push(...buildPathOffsetPatches(el));
190
-
191
- expect(resolvedTranslatePx(el)).toEqual({ x: anchorDx, y: anchorDy });
192
-
193
- // Persist → fresh element re-parsed from source → reload re-stamp.
194
- const reloaded = document.createElement("div");
195
- reloaded.style.setProperty("width", "200px");
196
- reloaded.style.setProperty("height", "100px");
197
- document.body.appendChild(reloaded);
198
- applyPatchesToElement(reloaded, patches);
199
- reapplyPositionEditsAfterSeek(reloaded.ownerDocument);
200
-
201
- expect(resolvedTranslatePx(reloaded)).toEqual({ x: anchorDx, y: anchorDy });
202
- expect(readStudioBoxSize(reloaded)).toEqual(finalSize);
203
- });
204
120
  });
@@ -56,7 +56,7 @@ export function MiniCurveSvg({
56
56
  <path
57
57
  d={d}
58
58
  fill="none"
59
- stroke={active ? "#3CE6AC" : "#737373"}
59
+ className={active ? "stroke-accent-ink" : "stroke-text-muted"}
60
60
  strokeWidth="1.5"
61
61
  strokeLinecap="round"
62
62
  />
@@ -253,9 +253,9 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void {
253
253
  // — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the
254
254
  // subsequent strip/reapply math compounds into a runaway matrix that flings the
255
255
  // element off-canvas. So for GSAP-animated elements we keep `translate: none`
256
- // and push the offset straight into GSAP's x/y via gsap.set; the var() offset is
257
- // still persisted (buildPathOffsetPatches), and GSAP re-reads it at init on
258
- // reload. Returns true when handled as GSAP (caller must skip the CSS path).
256
+ // and push the offset straight into GSAP's x/y via gsap.set; the var() offset an
257
+ // older Studio saved stays in the file, and GSAP re-reads it at init on reload.
258
+ // Returns true when handled as GSAP (caller must skip the CSS path).
259
259
  // fallow-ignore-next-line complexity
260
260
  function applyStudioPathOffsetViaGsap(
261
261
  element: HTMLElement,
@@ -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);
@@ -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>