@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
@@ -2,21 +2,20 @@
2
2
  * Gesture-begin functions: startGroupDrag and startGesture.
3
3
  * These are pure "start a new gesture" operations — no draft rect updates.
4
4
  */
5
- import { readElementGsapNumber } from "../../utils/elementGsap";
6
5
  import { type DomEditSelection } from "./domEditing";
7
6
  import {
7
+ applyManualOffsetDragDraft,
8
8
  createManualOffsetDragMember,
9
- readGsapRotation,
10
9
  restoreManualOffsetDragMembers,
11
10
  type ManualOffsetDragMember,
12
11
  } from "./manualOffsetDrag";
12
+ import { readCssRotationTarget, readRotationBase } from "./rotationDraft";
13
13
  import {
14
14
  beginStudioManualEditGesture,
15
15
  captureStudioBoxSize,
16
16
  captureStudioPathOffset,
17
17
  captureStudioRotation,
18
18
  readStudioBoxSize,
19
- readStudioRotation,
20
19
  } from "./manualEdits";
21
20
  import {
22
21
  type OverlayRect,
@@ -32,6 +31,7 @@ import {
32
31
  type UseDomEditOverlayGesturesOptions,
33
32
  } from "./domEditOverlayGestures";
34
33
  import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
34
+ import { gsapWritesRotation } from "../../hooks/gsapRuntimeKeyframes";
35
35
  import { logResize, resetResizeMoveLog } from "../../utils/resizeDebug";
36
36
  import { logDrag, readDragPositions, resetDragMoveLog } from "../../utils/dragDebug";
37
37
 
@@ -171,18 +171,6 @@ export function startGesture(
171
171
  Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
172
172
  const contentScaleY =
173
173
  Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
174
- let resizeAnchor: GestureState["resizeAnchor"];
175
- if (kind === "resize") {
176
- const startBcr = sel.element.getBoundingClientRect();
177
- resizeAnchor = {
178
- anchorX: startBcr.x,
179
- anchorY: startBcr.y,
180
- baseGsapX: readElementGsapNumber(sel.element, "x") ?? 0,
181
- baseGsapY: readElementGsapNumber(sel.element, "y") ?? 0,
182
- pinX: 0,
183
- pinY: 0,
184
- };
185
- }
186
174
  let initialPathOffset = captureStudioPathOffset(sel.element);
187
175
  let manualEditDragToken: string | undefined;
188
176
  let pathOffsetMember: ManualOffsetDragMember | undefined;
@@ -205,11 +193,9 @@ export function startGesture(
205
193
  initialPathOffset = result.member.initialPathOffset;
206
194
  manualEditDragToken = result.member.gestureToken;
207
195
  } else {
208
- // Center-anchored corner resize (CapCut model): the element scales about its
209
- // CENTER, which stays planted. All four corners behave identically, so EVERY
210
- // corner needs the manual-offset member that translates the element to re-pin
211
- // its center per frame (the memberless else-branch is only a defensive fallback
212
- // if member creation fails, e.g. the element can't take a manual offset).
196
+ // Center-anchored corner resize (CapCut model): the element scales about its planted CENTER,
197
+ // so every corner needs the member that re-pins the center per frame (the memberless
198
+ // branch is only a fallback for an element that can't take a manual offset).
213
199
  const needsAnchorOffset = kind === "resize" && sel.capabilities.canApplyManualOffset;
214
200
  if (needsAnchorOffset) {
215
201
  const result = createManualOffsetDragMember({
@@ -217,11 +203,14 @@ export function startGesture(
217
203
  selection: sel,
218
204
  element: sel.element,
219
205
  rect,
206
+ gesture: "resize",
220
207
  });
221
208
  if (result.ok) {
222
209
  pathOffsetMember = result.member;
223
210
  initialPathOffset = result.member.initialPathOffset;
224
211
  manualEditDragToken = result.member.gestureToken;
212
+ // Hold a % translate as the same px now, so a growing box can't drag it along mid-frame.
213
+ if (result.member.plainTranslate) applyManualOffsetDragDraft(result.member, 0, 0);
225
214
  } else {
226
215
  manualEditDragToken = beginStudioManualEditGesture(sel.element);
227
216
  }
@@ -230,10 +219,11 @@ export function startGesture(
230
219
  }
231
220
  }
232
221
 
233
- // Rotation base = GSAP's rotation plus the legacy `--hf-studio-rotation` var. A plain-translate
234
- // move never asks GSAP: reading a property makes it bake the CSS translate into its transform.
235
- const gsapRotation = pathOffsetMember?.plainTranslate ? 0 : readGsapRotation(sel.element);
236
- const rotation = { angle: gsapRotation + readStudioRotation(sel.element).angle };
222
+ // Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
223
+ // move, never asks GSAP: reading a property makes it bake the CSS into its transform.
224
+ const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
225
+ const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
226
+ const rotation = { angle: readRotationBase(sel.element, plain) };
237
227
  const overlayBounds = overlayEl?.getBoundingClientRect();
238
228
  const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
239
229
  const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
@@ -281,11 +271,11 @@ export function startGesture(
281
271
  actualWidth,
282
272
  actualHeight,
283
273
  actualRotation: rotation.angle,
274
+ plainRotation,
284
275
  editScaleX: rect.editScaleX,
285
276
  editScaleY: rect.editScaleY,
286
277
  contentScaleX,
287
278
  contentScaleY,
288
- resizeAnchor,
289
279
  manualEditDragToken,
290
280
  snapContext,
291
281
  resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
@@ -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
  />
@@ -10,7 +10,6 @@ import {
10
10
  applyStudioPathOffset,
11
11
  applyStudioPathOffsetDraft,
12
12
  applyStudioRotation,
13
- applyStudioRotationDraft,
14
13
  beginStudioManualEditGesture,
15
14
  captureStudioBoxSize,
16
15
  captureStudioRotation,
@@ -210,16 +209,11 @@ describe("studio manual edits", () => {
210
209
  expect(card.style.getPropertyValue("rotate")).toContain(STUDIO_ROTATION_PROP);
211
210
  expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
212
211
 
213
- applyStudioRotationDraft(card, { angle: -12.2604 });
214
- expect(readStudioRotation(card)).toEqual({ angle: -12.26 });
215
- expect(card.style.getPropertyValue("rotate")).toBe("calc(8deg + -12.26deg)");
216
- expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
217
-
218
212
  const snapshot = captureStudioRotation(card);
219
- applyStudioRotationDraft(card, { angle: 45 });
213
+ applyStudioRotation(card, { angle: 45 });
220
214
  restoreStudioRotation(card, snapshot);
221
- expect(readStudioRotation(card)).toEqual({ angle: -12.26 });
222
- expect(card.style.getPropertyValue("rotate")).toBe("calc(8deg + -12.26deg)");
215
+ expect(readStudioRotation(card)).toEqual({ angle: 24.247 });
216
+ expect(card.style.getPropertyValue(STUDIO_ROTATION_PROP)).toBe("24.247deg");
223
217
  expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
224
218
  });
225
219
 
@@ -29,7 +29,6 @@ export {
29
29
  applyStudioBoxSize,
30
30
  applyStudioBoxSizeDraft,
31
31
  applyStudioRotation,
32
- applyStudioRotationDraft,
33
32
  } from "./manualEditsDom";
34
33
 
35
34
  export { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply";
@@ -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,
@@ -538,13 +538,3 @@ export function applyStudioRotation(element: HTMLElement, rotation: { angle: num
538
538
  composeStudioRotationValue(element, `var(${STUDIO_ROTATION_PROP}, 0deg)`),
539
539
  );
540
540
  }
541
-
542
- export function applyStudioRotationDraft(element: HTMLElement, rotation: { angle: number }): void {
543
- promoteInlineForTransform(element);
544
- writeStudioRotationVars(element, rotation, { updateBase: false });
545
- element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
546
- element.style.setProperty(
547
- "rotate",
548
- composeStudioRotationValue(element, `${roundTo3(rotation.angle)}deg`),
549
- );
550
- }
@@ -42,16 +42,14 @@ 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,
49
- buildRotationPatches,
50
48
  buildClearRotationPatches,
51
49
  buildMotionPatches,
52
50
  buildClearMotionPatches,
53
51
  } from "./manualEditsDomPatches";
54
- import { applyStudioBoxSize, applyStudioPathOffset } from "./manualEditsDom";
52
+ import { applyStudioBoxSize } from "./manualEditsDom";
55
53
 
56
54
  /* ── helpers ── */
57
55
 
@@ -72,39 +70,7 @@ function assertClearCoversKeys(buildOps: PatchOperation[], clearOps: PatchOperat
72
70
 
73
71
  /* ── Path offset ─────────────────────────────────────────────────────────── */
74
72
 
75
- describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
76
- function populatedPathEl(): HTMLElement {
77
- const e = div();
78
- e.style.setProperty(STUDIO_OFFSET_X_PROP, "10px");
79
- e.style.setProperty(STUDIO_OFFSET_Y_PROP, "20px");
80
- e.style.setProperty("translate", "10px 20px");
81
- e.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, "5px 10px");
82
- e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "3px");
83
- e.style.setProperty("display", "flex");
84
- e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "block");
85
- return e;
86
- }
87
-
88
- it("populated: captures offset styles, attrs, display, and transform-display marker in declaration order", () => {
89
- const ops = buildPathOffsetPatches(populatedPathEl());
90
- expect(ops).toEqual([
91
- { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: "10px" },
92
- { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: "20px" },
93
- { type: "inline-style", property: "translate", value: "10px 20px" },
94
- { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
95
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: "5px 10px" },
96
- { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: "3px" },
97
- { type: "inline-style", property: "display", value: "flex" },
98
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "block" },
99
- ]);
100
- });
101
-
102
- it("empty: bare element yields only the path-offset marker", () => {
103
- expect(buildPathOffsetPatches(div())).toEqual([
104
- { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
105
- ]);
106
- });
107
-
73
+ describe("buildClearPathOffsetPatches", () => {
108
74
  it("clear: restores translate from STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR and display from STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR", () => {
109
75
  const e = div();
110
76
  e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "5px");
@@ -128,11 +94,6 @@ describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
128
94
  const ops = buildClearPathOffsetPatches(e);
129
95
  expect(ops.find((o) => o.property === "translate")?.value).toBeNull();
130
96
  });
131
-
132
- it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
133
- const e = populatedPathEl();
134
- assertClearCoversKeys(buildPathOffsetPatches(e), buildClearPathOffsetPatches(e));
135
- });
136
97
  });
137
98
 
138
99
  /* ── Box size ────────────────────────────────────────────────────────────── */
@@ -287,93 +248,9 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
287
248
  });
288
249
  });
289
250
 
290
- /* ── Combined box-size + path-offset (anchored-corner resize) ──────────────── */
291
-
292
- describe("anchored-corner combined patch: [...buildBoxSizePatches, ...buildPathOffsetPatches]", () => {
293
- // NW/NE/SW resize commits size AND anchor offset in ONE persist. The two
294
- // builders read the same already-mutated element and are concatenated; this
295
- // is only safe if their {type,property} keys are disjoint (no builder
296
- // overwrites the other's op when the source patcher applies them in order).
297
- it("concatenation of both builders emits disjoint {type,property} keys (no collision)", () => {
298
- const e = div();
299
- applyStudioBoxSize(e, { width: 300, height: 200 });
300
- applyStudioPathOffset(e, { x: 10, y: 20 });
301
-
302
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
303
- const keys = combined.map(opKey);
304
- expect(new Set(keys).size, `duplicate {type,property} key in combined patch: ${keys}`).toBe(
305
- keys.length,
306
- );
307
- });
308
-
309
- it("combined patch carries BOTH markers so a soft-reload re-hydrates size and offset together", () => {
310
- const e = div();
311
- applyStudioBoxSize(e, { width: 300, height: 200 });
312
- applyStudioPathOffset(e, { x: 10, y: 20 });
313
-
314
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
315
- const has = (property: string) =>
316
- combined.some((op) => op.type === "attribute" && op.property === property);
317
- expect(has(STUDIO_BOX_SIZE_ATTR)).toBe(true);
318
- expect(has(STUDIO_PATH_OFFSET_ATTR)).toBe(true);
319
- });
320
-
321
- it("order is size-first: every box-size op precedes every path-offset op", () => {
322
- const e = div();
323
- applyStudioBoxSize(e, { width: 300, height: 200 });
324
- applyStudioPathOffset(e, { x: 10, y: 20 });
325
-
326
- const boxKeys = new Set(buildBoxSizePatches(e).map(opKey));
327
- const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
328
- const lastBoxIdx = combined.reduce((acc, op, i) => (boxKeys.has(opKey(op)) ? i : acc), -1);
329
- const firstOffsetIdx = combined.findIndex(
330
- (op) => op.type === "attribute" && op.property === STUDIO_PATH_OFFSET_ATTR,
331
- );
332
- expect(firstOffsetIdx).toBeGreaterThan(lastBoxIdx);
333
- });
334
- });
335
-
336
251
  /* ── Rotation ────────────────────────────────────────────────────────────── */
337
252
 
338
- describe("buildRotationPatches / buildClearRotationPatches", () => {
339
- function populatedRotEl(): HTMLElement {
340
- const e = div();
341
- e.style.setProperty(STUDIO_ROTATION_PROP, "45");
342
- e.style.setProperty("rotate", "45deg");
343
- e.style.setProperty("transform-origin", "left center");
344
- e.style.setProperty("display", "block");
345
- e.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, "0deg");
346
- e.setAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, "0deg");
347
- e.setAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, "center center");
348
- e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "flex");
349
- return e;
350
- }
351
-
352
- it("populated: captures rotation styles, attrs, and transform-display marker in declaration order", () => {
353
- const ops = buildRotationPatches(populatedRotEl());
354
- expect(ops).toEqual([
355
- { type: "inline-style", property: STUDIO_ROTATION_PROP, value: "45" },
356
- { type: "inline-style", property: "rotate", value: "45deg" },
357
- { type: "inline-style", property: "transform-origin", value: "left center" },
358
- { type: "inline-style", property: "display", value: "block" },
359
- { type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" },
360
- { type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: "0deg" },
361
- { type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: "0deg" },
362
- {
363
- type: "attribute",
364
- property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
365
- value: "center center",
366
- },
367
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "flex" },
368
- ]);
369
- });
370
-
371
- it("empty: bare element yields only the rotation marker", () => {
372
- expect(buildRotationPatches(div())).toEqual([
373
- { type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" },
374
- ]);
375
- });
376
-
253
+ describe("buildClearRotationPatches", () => {
377
254
  it("clear: restores rotate and transform-origin from orig attrs, nulls draft attr", () => {
378
255
  const e = div();
379
256
  e.setAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, "30deg");
@@ -405,11 +282,6 @@ describe("buildRotationPatches / buildClearRotationPatches", () => {
405
282
  const ops = buildClearRotationPatches(e);
406
283
  expect(ops.find((o) => o.property === "rotate")?.value).toBeNull();
407
284
  });
408
-
409
- it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
410
- const e = populatedRotEl();
411
- assertClearCoversKeys(buildRotationPatches(e), buildClearRotationPatches(e));
412
- });
413
285
  });
414
286
 
415
287
  /* ── Motion ──────────────────────────────────────────────────────────────── */
@@ -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[] = [
@@ -118,7 +88,7 @@ export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperatio
118
88
 
119
89
  /* ── Box size patches ────────────────────────────────────────────── */
120
90
 
121
- const BOX_SIZE_STYLE_PROPS = [
91
+ export const BOX_SIZE_STYLE_PROPS = [
122
92
  "width",
123
93
  "height",
124
94
  "min-width",
@@ -185,28 +155,6 @@ export function buildClearBoxSizePatches(element: HTMLElement): PatchOperation[]
185
155
 
186
156
  /* ── Rotation patches ────────────────────────────────────────────── */
187
157
 
188
- const ROTATION_STYLE_PROPS = [
189
- STUDIO_ROTATION_PROP,
190
- "rotate",
191
- "transform-origin",
192
- "display",
193
- ] as const;
194
-
195
- const ROTATION_ORIG_ATTRS = [
196
- STUDIO_ORIGINAL_ROTATE_ATTR,
197
- STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
198
- STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
199
- STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
200
- ] as const;
201
-
202
- export function buildRotationPatches(element: HTMLElement): PatchOperation[] {
203
- const ops: PatchOperation[] = [];
204
- collectInlineStyleOps(element, ROTATION_STYLE_PROPS, ops);
205
- ops.push({ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" });
206
- collectAttributeOps(element, ROTATION_ORIG_ATTRS, ops);
207
- return ops;
208
- }
209
-
210
158
  export function buildClearRotationPatches(element: HTMLElement): PatchOperation[] {
211
159
  const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
212
160
  const origRotationTransformOrigin = element.getAttribute(
@@ -82,6 +82,8 @@ export function captureStudioBoxSize(element: HTMLElement): StudioBoxSizeSnapsho
82
82
  export function captureStudioRotation(element: HTMLElement): StudioRotationSnapshot {
83
83
  return {
84
84
  rotate: element.style.getPropertyValue("rotate"),
85
+ transform: element.style.getPropertyValue("transform"),
86
+ display: element.style.getPropertyValue("display"),
85
87
  transformOrigin: element.style.getPropertyValue("transform-origin"),
86
88
  studioRotation: element.style.getPropertyValue(STUDIO_ROTATION_PROP),
87
89
  marker: element.getAttribute(STUDIO_ROTATION_ATTR),
@@ -263,6 +265,7 @@ function restoreOriginalTranslateProperty(element: HTMLElement): void {
263
265
  export function clearStudioPathOffset(element: HTMLElement): void {
264
266
  if (
265
267
  element.hasAttribute(STUDIO_PATH_OFFSET_ATTR) ||
268
+ element.hasAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR) ||
266
269
  styleUsesStudioOffset(element.style.getPropertyValue("translate"))
267
270
  ) {
268
271
  restoreOriginalTranslateProperty(element);
@@ -99,6 +99,8 @@ export interface StudioBoxSizeSnapshot {
99
99
 
100
100
  export interface StudioRotationSnapshot {
101
101
  rotate: string;
102
+ transform: string;
103
+ display: string;
102
104
  transformOrigin: string;
103
105
  studioRotation: string;
104
106
  marker: string | null;
@@ -652,3 +652,44 @@ describe("a move of an element GSAP does not position", () => {
652
652
  expect(member().plainTranslate).toBe(false);
653
653
  });
654
654
  });
655
+
656
+ describe("a resize's anchor member", () => {
657
+ function anchorMember(tweenVars?: Record<string, number>) {
658
+ const window = new Window();
659
+ const element = window.document.createElement("div");
660
+ element.id = "box";
661
+ window.document.body.append(element);
662
+ const tweens = tweenVars ? [{ vars: tweenVars, targets: () => [element] }] : [];
663
+ Object.assign(window, { __timelines: { main: { getChildren: () => tweens } } });
664
+ element.getBoundingClientRect = () => {
665
+ const [x = 0, y = 0] = element.style
666
+ .getPropertyValue("translate")
667
+ .split(" ")
668
+ .map((v) => Number.parseFloat(v));
669
+ return new window.DOMRect(10 + (x || 0), 20 + (y || 0), 100, 50);
670
+ };
671
+ const result = createManualOffsetDragMember({
672
+ key: "box",
673
+ selection: { element } as never,
674
+ element,
675
+ rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
676
+ gesture: "resize",
677
+ });
678
+ if (!result.ok) throw new Error("member");
679
+ return result.member;
680
+ }
681
+
682
+ it("keeps the centre with the element's own plain translate when GSAP does not own the box", () => {
683
+ const member = anchorMember();
684
+ expect(member.plainTranslate).toBe(true);
685
+ applyManualOffsetDragDraft(member, -10.25, 3.5);
686
+ expect(member.element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px");
687
+ });
688
+
689
+ it.each(["width", "maxWidth", "minHeight", "flexBasis", "scale", "scaleX"])(
690
+ "leaves a box whose %s GSAP tweens, which the CSS box writer also sets, to the GSAP writer",
691
+ (channel) => {
692
+ expect(anchorMember({ [channel]: 300 }).plainTranslate).toBe(false);
693
+ },
694
+ );
695
+ });
@@ -11,7 +11,7 @@ import {
11
11
  type StudioPathOffsetSnapshot,
12
12
  } from "./manualEdits";
13
13
  import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
14
- import { gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
14
+ import { gsapWritesBox, gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
15
15
  import { readTranslatePx, UNREADABLE_TRANSLATE, writeTranslatePx } from "./plainTranslate";
16
16
 
17
17
  interface OffsetDragGsap {
@@ -19,7 +19,7 @@ interface OffsetDragGsap {
19
19
  getProperty: (el: Element, prop: string) => number;
20
20
  }
21
21
 
22
- function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
22
+ export function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
23
23
  const win = element.ownerDocument.defaultView as
24
24
  | (Window & { gsap?: Partial<OffsetDragGsap> })
25
25
  | null;
@@ -53,27 +53,6 @@ function applyOffsetDragDraftViaGsap(
53
53
  return true;
54
54
  }
55
55
 
56
- /**
57
- * Live rotation preview through the GSAP channel — the SAME channel the commit
58
- * lands in (a `tl.set`/keyframe rotation), mirroring `applyOffsetDragDraftViaGsap`.
59
- * GSAP owns the transform rotation, so neutralize the CSS `rotate` longhand to keep
60
- * the two channels from composing. `angle` is the absolute target rotation. Returns
61
- * false when gsap is unavailable (caller falls back to the CSS draft).
62
- */
63
- export function applyRotationDraftViaGsap(element: HTMLElement, angle: number): boolean {
64
- const gsap = getOffsetDragGsap(element);
65
- if (!gsap) return false;
66
- element.style.setProperty("rotate", "none");
67
- gsap.set(element, { rotation: angle });
68
- return true;
69
- }
70
-
71
- /** Current GSAP transform rotation — the single-source rotation base. 0 if gsap is unavailable. */
72
- export function readGsapRotation(element: HTMLElement): number {
73
- const gsap = getOffsetDragGsap(element);
74
- return gsap ? Number(gsap.getProperty(element, "rotation")) || 0 : 0;
75
- }
76
-
77
56
  const DEFAULT_OFFSET_PROBE_PX = 100;
78
57
  const MIN_PROBE_VECTOR_LENGTH_PX = 0.01;
79
58
  const MIN_MATRIX_DETERMINANT = 0.000001;
@@ -337,11 +316,11 @@ export function createManualOffsetDragMember(input: {
337
316
  selection: DomEditSelection;
338
317
  element: HTMLElement;
339
318
  rect: ManualOffsetDragRect;
340
- gesture?: "drag" | "nudge"; // a resize's anchor member keeps the legacy offset channel
319
+ gesture?: "drag" | "nudge" | "resize"; // resize: the anchor that keeps its centre planted
341
320
  }): ManualOffsetDragMemberResult {
342
- const plainTranslate = !!input.gesture && !gsapWritesPosition(input.element);
343
- // Base the drag on the offset ACTUALLY applied, never a dormant var (readAppliedStudioPathOffset),
344
- // so a stale offset can't fling the element off-screen.
321
+ const gsapOwns = input.gesture === "resize" ? gsapWritesBox : gsapWritesPosition;
322
+ const plainTranslate = !!input.gesture && !gsapOwns(input.element);
323
+ // The APPLIED offset, never a dormant var, so a stale one can't fling the element off-screen.
345
324
  const initialOffset = plainTranslate
346
325
  ? readTranslatePx(input.element)
347
326
  : readAppliedStudioPathOffset(input.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,