@hyperframes/studio 0.8.103 → 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 (200) hide show
  1. package/dist/assets/{hyperframes-player-DG795Hwu.js → hyperframes-player-tnfCiVdR.js} +1 -1
  2. package/dist/assets/{index-BiEtDkae.js → index-Ca6LZlVb.js} +1 -1
  3. package/dist/assets/index-Cre9X4RS.css +1 -0
  4. package/dist/assets/{index-BPJRf2X9.js → index-DEDhPmPD.js} +215 -215
  5. package/dist/assets/{index-DuCZ3pQw.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.d.ts +181 -168
  14. package/dist/index.html +2 -2
  15. package/dist/index.js +1018 -919
  16. package/dist/index.js.map +1 -1
  17. package/dist/player.js +2 -2
  18. package/dist/styles/tailwind-preset.js.map +1 -1
  19. package/dist/styles.css +1 -1
  20. package/dist/ui.js +2 -2
  21. package/package.json +8 -8
  22. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  23. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  24. package/src/captions/components/CaptionTimeline.tsx +3 -3
  25. package/src/components/AskAgentModal.tsx +1 -1
  26. package/src/components/LintModal.tsx +35 -35
  27. package/src/components/StudioErrorBoundary.tsx +1 -1
  28. package/src/components/StudioHeader.dom.test.tsx +4 -4
  29. package/src/components/StudioHeader.tsx +2 -1
  30. package/src/components/StudioToast.tsx +7 -16
  31. package/src/components/TimelineToolPicker.tsx +1 -1
  32. package/src/components/TimelineToolbar.tsx +11 -11
  33. package/src/components/dock/Dock.tsx +1 -1
  34. package/src/components/dock/dock.css +9 -1
  35. package/src/components/editor/AnimationCard.tsx +5 -5
  36. package/src/components/editor/AnimationCardParts.tsx +1 -1
  37. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  38. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  39. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  40. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  41. package/src/components/editor/DomEditOverlay.tsx +5 -6
  42. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  43. package/src/components/editor/EaseCurveSection.tsx +14 -19
  44. package/src/components/editor/EaseModeControls.tsx +2 -2
  45. package/src/components/editor/EaseParamFields.tsx +8 -8
  46. package/src/components/editor/FileTree.tsx +1 -1
  47. package/src/components/editor/FileTreeIcons.tsx +47 -26
  48. package/src/components/editor/FileTreeNodes.tsx +11 -8
  49. package/src/components/editor/GestureRecordControl.tsx +3 -3
  50. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  51. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  52. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  53. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  54. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  55. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  56. package/src/components/editor/LayersPanel.tsx +3 -3
  57. package/src/components/editor/PromotableControl.tsx +2 -2
  58. package/src/components/editor/PropertyPanel.tsx +3 -3
  59. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  60. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  61. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  62. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  63. package/src/components/editor/SnapToolbar.tsx +5 -5
  64. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  65. package/src/components/editor/Transform3DCube.tsx +1 -1
  66. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
  67. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  68. package/src/components/editor/domEditOverlayGestures.ts +13 -19
  69. package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
  70. package/src/components/editor/domEditOverlayStartGesture.ts +15 -25
  71. package/src/components/editor/easeCurveSvg.tsx +1 -1
  72. package/src/components/editor/manualEdits.test.ts +3 -9
  73. package/src/components/editor/manualEdits.ts +0 -1
  74. package/src/components/editor/manualEditsDom.ts +3 -13
  75. package/src/components/editor/manualEditsDomPatches.test.ts +3 -131
  76. package/src/components/editor/manualEditsDomPatches.ts +1 -53
  77. package/src/components/editor/manualEditsSnapshot.ts +3 -0
  78. package/src/components/editor/manualEditsTypes.ts +2 -0
  79. package/src/components/editor/manualOffsetDrag.test.ts +41 -0
  80. package/src/components/editor/manualOffsetDrag.ts +6 -27
  81. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  82. package/src/components/editor/plainTranslate.ts +42 -6
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  87. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  88. package/src/components/editor/propertyPanelFill.tsx +6 -6
  89. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  90. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  94. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  95. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  96. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  97. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  98. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  99. package/src/components/editor/propertyPanelFont.tsx +2 -2
  100. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  101. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  102. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  103. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  104. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  105. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  106. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  107. package/src/components/editor/propertyPanelSections.tsx +2 -2
  108. package/src/components/editor/propertyPanelTransformCommit.test.ts +29 -0
  109. package/src/components/editor/propertyPanelTransformCommit.ts +8 -5
  110. package/src/components/editor/propertyPanelTypes.ts +1 -0
  111. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  112. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  113. package/src/components/editor/rotateInlineSave.test.ts +111 -0
  114. package/src/components/editor/rotationDraft.test.ts +111 -0
  115. package/src/components/editor/rotationDraft.ts +145 -0
  116. package/src/components/editor/useDomEditNudge.test.tsx +33 -0
  117. package/src/components/editor/useDomEditNudge.ts +6 -2
  118. package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
  119. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  120. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  121. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  122. package/src/components/nle/NLEPreview.test.ts +8 -4
  123. package/src/components/nle/PreviewOverlays.tsx +1 -1
  124. package/src/components/nle/PreviewPane.tsx +3 -3
  125. package/src/components/nle/PreviewPoster.tsx +29 -1
  126. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  127. package/src/components/panels/SlideshowPanel.tsx +4 -4
  128. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  129. package/src/components/panels/VariablesBindElement.tsx +1 -1
  130. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  131. package/src/components/panels/VariablesPanel.tsx +4 -6
  132. package/src/components/panels/VariablesRowAction.tsx +1 -1
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  134. package/src/components/renders/RenderQueue.tsx +2 -2
  135. package/src/components/renders/RenderQueueItem.tsx +3 -3
  136. package/src/components/sidebar/AssetCard.tsx +3 -3
  137. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  138. package/src/components/sidebar/AssetsTab.tsx +6 -6
  139. package/src/components/sidebar/AudioRow.tsx +5 -3
  140. package/src/components/sidebar/BlocksTab.tsx +6 -6
  141. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  142. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  143. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  144. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  145. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  146. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  147. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  148. package/src/components/timelineToolbarStyles.ts +3 -3
  149. package/src/components/ui/Button.tsx +4 -4
  150. package/src/components/ui/HyperframesLogo.tsx +19 -18
  151. package/src/components/ui/Input.tsx +1 -1
  152. package/src/components/ui/Menu.tsx +4 -2
  153. package/src/hooks/draggedGsapPosition.ts +28 -0
  154. package/src/hooks/elementOffsetStager.ts +34 -13
  155. package/src/hooks/gsapRuntimeBridge.ts +5 -3
  156. package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
  157. package/src/hooks/plainRotation.test.ts +122 -0
  158. package/src/hooks/plainRotation.ts +42 -0
  159. package/src/hooks/useClipboard.source.test.ts +42 -4
  160. package/src/hooks/useClipboard.ts +13 -3
  161. package/src/hooks/useDomEditCommits.ts +0 -3
  162. package/src/hooks/useDomEditSession.ts +5 -5
  163. package/src/hooks/useDomGeometryCommit.test.tsx +79 -1
  164. package/src/hooks/useDomGeometryCommit.ts +16 -17
  165. package/src/hooks/useDomGeometryCommits.test.tsx +233 -33
  166. package/src/hooks/useDomGeometryCommits.ts +27 -80
  167. package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
  168. package/src/hooks/useGsapAwareEditing.ts +43 -42
  169. package/src/hooks/useInlineTextEdit.test.tsx +33 -3
  170. package/src/hooks/useInlineTextEdit.ts +2 -2
  171. package/src/player/components/ClipContextMenu.tsx +1 -1
  172. package/src/player/components/CompositionThumbnail.tsx +23 -11
  173. package/src/player/components/EditModal.tsx +6 -6
  174. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  175. package/src/player/components/PlayerControls.tsx +3 -3
  176. package/src/player/components/ShortcutsPanel.tsx +8 -20
  177. package/src/player/components/TimelineCanvas.tsx +1 -1
  178. package/src/player/components/TimelineClip.tsx +2 -2
  179. package/src/player/components/TimelineEmptyState.tsx +2 -2
  180. package/src/player/components/TimelineLanes.tsx +0 -1
  181. package/src/player/components/TrackClipCount.tsx +1 -1
  182. package/src/player/components/VolumeControl.tsx +1 -1
  183. package/src/player/components/timelineClipChildren.tsx +1 -1
  184. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  185. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  186. package/src/player/lib/thumbnailScheduler.ts +4 -5
  187. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  188. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  189. package/src/styles/hex-baseline.json +7 -19
  190. package/src/styles/studio.css +0 -9
  191. package/src/styles/tailwind-preset.shared.js +2 -1
  192. package/src/styles/theme.css +204 -147
  193. package/src/styles/theme.test.ts +13 -9
  194. package/src/utils/blockCategories.ts +41 -9
  195. package/src/utils/clipboardPayload.ts +2 -0
  196. package/dist/assets/index-CQN3M56N.css +0 -1
  197. package/dist/chunk-62DDWOSH.js.map +0 -1
  198. package/dist/chunk-6GAO75IU.js.map +0 -1
  199. package/src/hooks/gsapTargetCache.ts +0 -62
  200. /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
@@ -13,6 +13,9 @@ import {
13
13
  readStudioRotation,
14
14
  } from "../components/editor/manualEdits";
15
15
  import { useDomGeometryCommits, type UseDomGeometryCommitsParams } from "./useDomGeometryCommits";
16
+ import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
17
+ import { writeTranslatePx } from "../components/editor/plainTranslate";
18
+ import { applyPatch } from "../utils/sourcePatcher";
16
19
  import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
17
20
  import { withInlineLayoutBox } from "./domSelectionTestHarness";
18
21
 
@@ -25,7 +28,6 @@ function mountCommits(
25
28
  let commits: ReturnType<typeof useDomGeometryCommits> | null = null;
26
29
  function Probe() {
27
30
  commits = useDomGeometryCommits({
28
- previewIframeRef: { current: null },
29
31
  showToast: vi.fn(),
30
32
  commitPositionPatchToHtml,
31
33
  readOnlyPreview,
@@ -61,7 +63,6 @@ describe("useDomGeometryCommits rollback", () => {
61
63
 
62
64
  function Probe() {
63
65
  commits = useDomGeometryCommits({
64
- previewIframeRef: { current: null },
65
66
  showToast: vi.fn(),
66
67
  commitPositionPatchToHtml,
67
68
  readOnlyPreview: false,
@@ -70,9 +71,6 @@ describe("useDomGeometryCommits rollback", () => {
70
71
  }
71
72
 
72
73
  act(() => root.render(<Probe />));
73
- await expect(commits!.handleDomPathOffsetCommit(selection, { x: 50, y: 60 })).rejects.toBe(
74
- failure,
75
- );
76
74
  await expect(
77
75
  commits!.handleDomBoxSizeCommit(selection, { width: 200, height: 160 }, { x: 30, y: 40 }),
78
76
  ).rejects.toBe(failure);
@@ -90,20 +88,6 @@ describe("useDomGeometryCommits read-only preview", () => {
90
88
  const selectionOn = (element: HTMLElement) =>
91
89
  ({ id: element.id, selector: `#${element.id}`, element }) as unknown as DomEditSelection;
92
90
 
93
- it("refuses a manual offset commit: no write, no history entry", async () => {
94
- const element = document.createElement("div");
95
- element.id = "ro-offset";
96
- document.body.append(element);
97
- applyStudioPathOffset(element, { x: 1, y: 2 });
98
- const commitPositionPatchToHtml =
99
- vi.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>();
100
- const { commits, unmount } = mountCommits(commitPositionPatchToHtml, true);
101
- await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 99, y: 99 });
102
- expect(readStudioPathOffset(element)).toEqual({ x: 1, y: 2 });
103
- expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
104
- unmount();
105
- });
106
-
107
91
  it("refuses a manual box-size commit: no write, no history entry", async () => {
108
92
  const element = document.createElement("div");
109
93
  element.id = "ro-size";
@@ -131,19 +115,6 @@ describe("useDomGeometryCommits read-only preview", () => {
131
115
  expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
132
116
  unmount();
133
117
  });
134
-
135
- it("still commits an offset with the flag off", async () => {
136
- const element = document.createElement("div");
137
- element.id = "rw-offset";
138
- document.body.append(element);
139
- const commitPositionPatchToHtml = vi
140
- .fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
141
- .mockResolvedValue(undefined);
142
- const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
143
- await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 5, y: 6 });
144
- expect(commitPositionPatchToHtml).toHaveBeenCalledTimes(1);
145
- unmount();
146
- });
147
118
  });
148
119
 
149
120
  describe("useDomGeometryCommits element position offset", () => {
@@ -162,7 +133,7 @@ describe("useDomGeometryCommits element position offset", () => {
162
133
  .mockResolvedValue(undefined);
163
134
  const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
164
135
 
165
- await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }).save();
136
+ await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }, false).save();
166
137
 
167
138
  const patches = commitPositionPatchToHtml.mock.calls[0]![1];
168
139
  expect(patches).toEqual([
@@ -175,6 +146,235 @@ describe("useDomGeometryCommits element position offset", () => {
175
146
  });
176
147
  });
177
148
 
149
+ describe("useDomGeometryCommits resize anchor", () => {
150
+ it("saves the anchor as the element's own plain px translate, sub-pixel, with the size", async () => {
151
+ const element = document.createElement("div");
152
+ element.id = "anchored";
153
+ element.style.setProperty("translate", "40px 30px");
154
+ document.body.append(element);
155
+ const selection = {
156
+ id: "anchored",
157
+ selector: "#anchored",
158
+ element,
159
+ } as unknown as DomEditSelection;
160
+ const commit = vi
161
+ .fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
162
+ .mockResolvedValue(undefined);
163
+ const { commits, unmount } = mountCommits(commit);
164
+
165
+ await commits().handleDomBoxSizeCommit(
166
+ selection,
167
+ { width: 340, height: 227 },
168
+ { x: -10.25, y: 3.5 },
169
+ );
170
+
171
+ expect(commit).toHaveBeenCalledTimes(1);
172
+ const patches = commit.mock.calls[0]![1];
173
+ expect(patches).toContainEqual({ type: "inline-style", property: "width", value: "340px" });
174
+ expect(patches).toContainEqual({
175
+ type: "inline-style",
176
+ property: "translate",
177
+ value: "-10.25px 3.5px",
178
+ });
179
+ expect(patches.some((p) => p.property.startsWith("--hf-studio-offset"))).toBe(false);
180
+ expect(element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px");
181
+ unmount();
182
+ });
183
+ });
184
+
185
+ describe("useDomGeometryCommits resize rollback", () => {
186
+ it("rolls the whole gesture back once when the resize save fails", async () => {
187
+ const element = document.createElement("div");
188
+ document.body.append(element);
189
+ const selection = { id: "r", selector: "#r", element } as unknown as DomEditSelection;
190
+ const failure = new Error("save failed");
191
+ const commit = vi
192
+ .fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
193
+ .mockRejectedValue(failure);
194
+ const restore = vi.fn();
195
+ const { commits, unmount } = mountCommits(commit);
196
+ await expect(
197
+ commits().handleDomBoxSizeCommit(selection, { width: 50, height: 40 }, undefined, restore),
198
+ ).rejects.toBe(failure);
199
+ expect(restore).toHaveBeenCalledTimes(1);
200
+ unmount();
201
+ });
202
+ });
203
+
204
+ describe("a plain move of an element an older Studio moved by its offset vars", () => {
205
+ function legacyMoved() {
206
+ const element = document.createElement("div");
207
+ element.id = "legacy";
208
+ element.style.setProperty("translate", "10px 5px");
209
+ document.body.append(element);
210
+ applyStudioPathOffset(element, { x: 40, y: 30 });
211
+ const selection = { id: "legacy", selector: "#legacy", element } as unknown as DomEditSelection;
212
+ return { element, selection };
213
+ }
214
+ const legacyEnds = [
215
+ { type: "inline-style", property: "--hf-studio-offset-x", value: null },
216
+ { type: "inline-style", property: "--hf-studio-offset-y", value: null },
217
+ { type: "attribute", property: "data-hf-studio-path-offset", value: null },
218
+ ];
219
+ const keptMarks = [
220
+ "data-hf-studio-original-inline-translate",
221
+ "data-hf-studio-original-translate",
222
+ ];
223
+ const studioMarks = (el: HTMLElement) =>
224
+ el
225
+ .getAttributeNames()
226
+ .filter((name) => name.startsWith("data-hf-studio"))
227
+ .sort();
228
+
229
+ it("saves the move as its translate, ends the old offset, and a seek keeps it", async () => {
230
+ const { element, selection } = legacyMoved();
231
+ const commitPositionPatchToHtml = vi
232
+ .fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
233
+ .mockResolvedValue(undefined);
234
+ const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
235
+
236
+ await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
237
+ reapplyPositionEditsAfterSeek(document);
238
+
239
+ expect(element.style.getPropertyValue("translate")).toBe("140px 30px");
240
+ expect(commitPositionPatchToHtml.mock.calls[0]![1]).toEqual([
241
+ { type: "inline-style", property: "translate", value: "140px 30px" },
242
+ {
243
+ type: "attribute",
244
+ property: "data-hf-studio-original-inline-translate",
245
+ value: "10px 5px",
246
+ },
247
+ ...legacyEnds,
248
+ ]);
249
+ expect(studioMarks(element)).toEqual(keptMarks);
250
+ unmount();
251
+ });
252
+
253
+ it("resizes with the anchor as its translate, ends the old offset, and a seek keeps it", async () => {
254
+ const { element, selection } = legacyMoved();
255
+ const commit = vi
256
+ .fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
257
+ .mockResolvedValue(undefined);
258
+ const { commits, unmount } = mountCommits(commit);
259
+
260
+ await commits().handleDomBoxSizeCommit(selection, { width: 80, height: 60 }, { x: 60, y: 40 });
261
+ reapplyPositionEditsAfterSeek(document);
262
+
263
+ expect(element.style.getPropertyValue("translate")).toBe("60px 40px");
264
+ expect(commit.mock.calls[0]![1]).toEqual(expect.arrayContaining(legacyEnds));
265
+ expect(studioMarks(element)).toEqual(expect.arrayContaining(keptMarks));
266
+ expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(false);
267
+ unmount();
268
+ });
269
+
270
+ it.each(["move", "resize"])(
271
+ "puts the old offset back whole when the %s save fails",
272
+ async (kind) => {
273
+ const { element, selection } = legacyMoved();
274
+ const translate = element.style.getPropertyValue("translate");
275
+ const { commits, unmount } = mountCommits(
276
+ vi.fn().mockRejectedValue(new Error("save failed")),
277
+ );
278
+
279
+ const save =
280
+ kind === "move"
281
+ ? commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save()
282
+ : commits().handleDomBoxSizeCommit(
283
+ selection,
284
+ { width: 80, height: 60 },
285
+ { x: 60, y: 40 },
286
+ );
287
+ await expect(save).rejects.toThrow();
288
+ expect(element.style.getPropertyValue("translate")).toBe(translate);
289
+ expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(true);
290
+ expect(readStudioPathOffset(element)).toEqual({ x: 40, y: 30 });
291
+ unmount();
292
+ },
293
+ );
294
+ });
295
+
296
+ describe("Reset after a plain move puts the author's translate back", () => {
297
+ function authoredLayer(translate: string | null, legacy = false) {
298
+ const element = document.createElement("div");
299
+ element.id = "layer";
300
+ if (translate) element.style.setProperty("translate", translate);
301
+ document.body.append(element);
302
+ if (legacy) applyStudioPathOffset(element, { x: 40, y: 30 });
303
+ const project = { file: element.outerHTML, history: [] as string[] };
304
+ const commit = vi.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>(
305
+ async (_selection, patches) => {
306
+ project.history.push(project.file);
307
+ project.file = patches.reduce((html, op) => applyPatch(html, "layer", op), project.file);
308
+ },
309
+ );
310
+ const selection = { id: "layer", selector: "#layer", element } as unknown as DomEditSelection;
311
+ return { element, selection, project, ...mountCommits(commit) };
312
+ }
313
+ const saved = (html: string) =>
314
+ new DOMParser().parseFromString(html, "text/html").getElementById("layer")!;
315
+ const studioLeftovers = (el: Element) => [
316
+ ...el.getAttributeNames().filter((name) => name.startsWith("data-hf-studio")),
317
+ ...((el as HTMLElement).style.cssText.match(/--hf-studio-[\w-]+/g) ?? []),
318
+ ];
319
+
320
+ it("restores an older Studio's layer to its authored 50% 10px, in the file and live", async () => {
321
+ const { element, selection, project, commits, unmount } = authoredLayer("50% 10px", true);
322
+
323
+ await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
324
+ await commits().handleDomManualEditsReset(selection);
325
+
326
+ expect(saved(project.file).style.getPropertyValue("translate")).toBe("50% 10px");
327
+ expect(element.style.getPropertyValue("translate")).toBe("50% 10px");
328
+ expect(studioLeftovers(saved(project.file))).toEqual([]);
329
+ expect(studioLeftovers(element)).toEqual([]);
330
+ unmount();
331
+ });
332
+
333
+ it("restores the authored 12px 4px after a drag and a second move", async () => {
334
+ const { element, selection, project, commits, unmount } = authoredLayer("12px 4px");
335
+
336
+ writeTranslatePx(element, { x: 90, y: 0 });
337
+ await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
338
+ await commits().stageElementPositionOffset(selection, { x: 200, y: 50 }, true).save();
339
+ await commits().handleDomManualEditsReset(selection);
340
+
341
+ expect(saved(project.file).style.getPropertyValue("translate")).toBe("12px 4px");
342
+ expect(element.style.getPropertyValue("translate")).toBe("12px 4px");
343
+ expect(studioLeftovers(saved(project.file))).toEqual([]);
344
+ unmount();
345
+ });
346
+
347
+ it("removes the translate of a layer that had none, in the file and live", async () => {
348
+ const { element, selection, project, commits, unmount } = authoredLayer(null);
349
+
350
+ await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
351
+ await commits().handleDomManualEditsReset(selection);
352
+
353
+ expect(saved(project.file).style.getPropertyValue("translate")).toBe("");
354
+ expect(element.style.getPropertyValue("translate")).toBe("");
355
+ expect(studioLeftovers(saved(project.file))).toEqual([]);
356
+ unmount();
357
+ });
358
+
359
+ it("undo of the Reset brings the move back, and Reset still restores the author's", async () => {
360
+ const { selection, project, commits, unmount } = authoredLayer("12px 4px");
361
+
362
+ await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
363
+ const moved = project.file;
364
+ await commits().handleDomManualEditsReset(selection);
365
+ project.file = project.history.pop()!;
366
+ expect(project.file).toBe(moved);
367
+
368
+ const reloaded = saved(project.file) as HTMLElement;
369
+ document.body.append(reloaded);
370
+ expect(reloaded.style.getPropertyValue("translate")).toBe("140px 30px");
371
+ await commits().handleDomManualEditsReset({ ...selection, element: reloaded });
372
+ expect(saved(project.file).style.getPropertyValue("translate")).toBe("12px 4px");
373
+ expect(reloaded.style.getPropertyValue("translate")).toBe("12px 4px");
374
+ unmount();
375
+ });
376
+ });
377
+
178
378
  describe("useDomGeometryCommits resize of a cropped element", () => {
179
379
  it("saves the crop scaled per axis with the box in the resize's own commit", async () => {
180
380
  const element = withInlineLayoutBox(document.createElement("div"));
@@ -1,9 +1,8 @@
1
+ import type { RotationCommit } from "../components/editor/rotationDraft";
1
2
  import { useCallback } from "react";
2
3
  import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
3
4
  import {
4
- applyStudioPathOffset,
5
5
  applyStudioBoxSize,
6
- applyStudioRotation,
7
6
  captureStudioPathOffset,
8
7
  captureStudioBoxSize,
9
8
  captureStudioRotation,
@@ -15,36 +14,20 @@ import {
15
14
  clearStudioRotation,
16
15
  } from "../components/editor/manualEdits";
17
16
  import { stageElementOffset } from "./elementOffsetStager";
17
+ import { savePlainRotation } from "./plainRotation";
18
18
  import { prepareCropResize } from "../components/editor/cropResize";
19
+ import { writePlainMove } from "../components/editor/plainTranslate";
19
20
  import {
20
- buildPathOffsetPatches,
21
21
  buildBoxSizePatches,
22
- buildRotationPatches,
23
22
  buildClearPathOffsetPatches,
24
23
  buildClearBoxSizePatches,
25
24
  buildClearRotationPatches,
26
25
  } from "../components/editor/manualEditsDomPatches";
27
26
  import type { PatchOperation } from "../utils/sourcePatcher";
28
- import { isElementGsapTargeted } from "./gsapTargetCache";
29
-
30
- const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
31
- "This element is GSAP-animated — dragging via CSS would corrupt keyframes";
32
-
33
- function rejectGsapCssFallback(
34
- selection: DomEditSelection,
35
- previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>,
36
- showToast: (message: string, tone?: "error" | "info") => void,
37
- ): Promise<never> | null {
38
- if (!isElementGsapTargeted(previewIframeRef.current, selection.element)) return null;
39
- const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
40
- showToast(error.message, "error");
41
- return Promise.reject(error);
42
- }
43
27
 
44
28
  // ── Hook ──
45
29
 
46
30
  export interface UseDomGeometryCommitsParams {
47
- previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
48
31
  showToast: (message: string, tone?: "error" | "info") => void;
49
32
  commitPositionPatchToHtml: (
50
33
  selection: DomEditSelection,
@@ -55,99 +38,64 @@ export interface UseDomGeometryCommitsParams {
55
38
  }
56
39
 
57
40
  export function useDomGeometryCommits({
58
- previewIframeRef,
59
41
  showToast,
60
42
  commitPositionPatchToHtml,
61
43
  readOnlyPreview,
62
44
  }: UseDomGeometryCommitsParams) {
63
45
  const stageElementPositionOffset = useCallback(
64
- (selection: DomEditSelection, next: { x: number; y: number }, coalesceKey?: string) =>
46
+ (
47
+ selection: DomEditSelection,
48
+ next: { x: number; y: number },
49
+ plainTranslate: boolean,
50
+ coalesceKey?: string,
51
+ ) =>
65
52
  stageElementOffset(
66
53
  { commitPositionPatchToHtml, showToast, readOnlyPreview },
67
54
  selection,
68
55
  next,
56
+ plainTranslate,
69
57
  coalesceKey,
70
58
  ),
71
59
  [commitPositionPatchToHtml, readOnlyPreview, showToast],
72
60
  );
73
61
 
74
- const handleDomPathOffsetCommit = useCallback(
75
- (selection: DomEditSelection, next: { x: number; y: number }) => {
76
- if (readOnlyPreview) return Promise.resolve();
77
- // ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path
78
- // elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
79
- // onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
80
- // Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
81
- const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast);
82
- if (gsapFallback) return gsapFallback;
83
- const before = captureStudioPathOffset(selection.element);
84
- applyStudioPathOffset(selection.element, next);
85
- return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
86
- label: "Move layer",
87
- coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
88
- }).catch((error) => {
89
- restoreStudioPathOffset(selection.element, before);
90
- throw error;
91
- });
92
- },
93
- [commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
94
- );
95
-
96
62
  const handleDomBoxSizeCommit = useCallback(
97
63
  (
98
64
  selection: DomEditSelection,
99
65
  next: { width: number; height: number },
100
66
  offset?: { x: number; y: number },
67
+ restore?: () => void,
101
68
  ) => {
102
69
  if (readOnlyPreview) return Promise.resolve();
103
- const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast);
104
- if (gsapFallback) return gsapFallback;
105
- const beforeSize = captureStudioBoxSize(selection.element);
106
- const beforeOffset = offset ? captureStudioPathOffset(selection.element) : null;
107
- const stageCrop = prepareCropResize(selection.element);
108
- applyStudioBoxSize(selection.element, next);
70
+ const element = selection.element;
71
+ const beforeSize = captureStudioBoxSize(element);
72
+ const beforeOffset = captureStudioPathOffset(element);
73
+ const stageCrop = prepareCropResize(element);
74
+ applyStudioBoxSize(element, next);
109
75
  const crop = stageCrop();
110
- // Anchored-corner resize (NW/NE/SW) also moves the element to keep the
111
- // opposite corner fixed. Apply the offset and emit BOTH patch sets in a
112
- // SINGLE commit: one persist = one undo entry, and there is no
113
- // intermediate re-stamp where the new size is in source but the anchor
114
- // offset is not (that frame was the release "jump"). Both builders read
115
- // the already-mutated live element, so concatenation is safe.
116
- const patches = buildBoxSizePatches(selection.element);
76
+ // One commit, one undo entry: the size, the crop that follows it, and the translate
77
+ // (as a move writes it) that keeps the centre planted.
78
+ const patches = buildBoxSizePatches(element);
117
79
  if (crop) patches.push(crop.patch);
118
- if (offset) {
119
- applyStudioPathOffset(selection.element, offset);
120
- patches.push(...buildPathOffsetPatches(selection.element));
121
- }
80
+ if (offset) patches.push(...writePlainMove(element, offset));
122
81
  return commitPositionPatchToHtml(selection, patches, {
123
82
  label: "Resize layer box",
124
83
  coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
125
84
  }).catch((error) => {
126
- restoreStudioBoxSize(selection.element, beforeSize);
127
- if (beforeOffset) restoreStudioPathOffset(selection.element, beforeOffset);
85
+ restoreStudioBoxSize(element, beforeSize);
86
+ if (offset) restoreStudioPathOffset(element, beforeOffset);
128
87
  crop?.revert();
88
+ restore?.();
129
89
  throw error;
130
90
  });
131
91
  },
132
- [commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
92
+ [commitPositionPatchToHtml, readOnlyPreview],
133
93
  );
134
94
 
135
95
  const handleDomRotationCommit = useCallback(
136
- (selection: DomEditSelection, next: { angle: number }) => {
137
- if (readOnlyPreview) return Promise.resolve();
138
- const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast);
139
- if (gsapFallback) return gsapFallback;
140
- const before = captureStudioRotation(selection.element);
141
- applyStudioRotation(selection.element, next);
142
- return commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
143
- label: "Rotate layer",
144
- coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
145
- }).catch((error) => {
146
- restoreStudioRotation(selection.element, before);
147
- throw error;
148
- });
149
- },
150
- [commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
96
+ (selection: DomEditSelection, next: RotationCommit) =>
97
+ savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selection, next),
98
+ [commitPositionPatchToHtml, readOnlyPreview],
151
99
  );
152
100
 
153
101
  const handleDomManualEditsReset = useCallback(
@@ -181,7 +129,6 @@ export function useDomGeometryCommits({
181
129
 
182
130
  return {
183
131
  stageElementPositionOffset,
184
- handleDomPathOffsetCommit,
185
132
  handleDomBoxSizeCommit,
186
133
  handleDomRotationCommit,
187
134
  handleDomManualEditsReset,