@hyperframes/studio 0.8.99 → 0.8.100

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 (28) hide show
  1. package/dist/assets/{hyperframes-player-Rml34FRW.js → hyperframes-player-fWt87gyh.js} +1 -1
  2. package/dist/assets/{index-C_fi-jTe.js → index-D34Oy5DN.js} +1 -1
  3. package/dist/assets/{index-CVDArg3m.js → index-D6TjRAvz.js} +176 -176
  4. package/dist/assets/{index-D9YzDuBx.js → index-DofdzdxJ.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +423 -312
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -8
  9. package/src/components/editor/PropertyPanel.tsx +3 -3
  10. package/src/components/editor/domEditOverlayStartGesture.test.ts +83 -0
  11. package/src/components/editor/domEditOverlayStartGesture.ts +6 -4
  12. package/src/components/editor/manualOffsetDrag.test.ts +70 -0
  13. package/src/components/editor/manualOffsetDrag.ts +50 -40
  14. package/src/components/editor/plainTranslate.test.ts +62 -0
  15. package/src/components/editor/plainTranslate.ts +85 -0
  16. package/src/components/editor/propertyPanelTransformCommit.ts +3 -2
  17. package/src/components/editor/useDomEditNudge.ts +1 -1
  18. package/src/hooks/elementOffsetStager.ts +33 -2
  19. package/src/hooks/gsapRuntimeBridge.ts +1 -17
  20. package/src/hooks/gsapRuntimeKeyframes.ts +35 -0
  21. package/src/hooks/useDomEditCommits.test.tsx +12 -3
  22. package/src/hooks/useDomEditCommits.ts +2 -1
  23. package/src/hooks/useDomEditTextCommits.test.tsx +62 -5
  24. package/src/hooks/useDomEditTextCommits.ts +35 -35
  25. package/src/hooks/useDomGeometryCommit.test.tsx +62 -4
  26. package/src/hooks/useDomGeometryCommits.test.tsx +4 -2
  27. package/src/hooks/useGsapAwareEditing.test.tsx +46 -18
  28. package/src/hooks/useGsapAwareEditing.ts +11 -11
package/dist/index.js CHANGED
@@ -32383,6 +32383,334 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
32383
32383
  };
32384
32384
  }
32385
32385
 
32386
+ // src/hooks/gsapRuntimeKeyframes.ts
32387
+ import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
32388
+ var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
32389
+ "ease",
32390
+ "duration",
32391
+ "delay",
32392
+ "stagger",
32393
+ "motionPath",
32394
+ "overwrite",
32395
+ "immediateRender",
32396
+ "onComplete",
32397
+ "onUpdate",
32398
+ "onStart",
32399
+ "keyframes"
32400
+ ]);
32401
+ function timelinesOf(iframe) {
32402
+ if (!iframe?.contentWindow) return null;
32403
+ try {
32404
+ return iframe.contentWindow.__timelines ?? null;
32405
+ } catch {
32406
+ return null;
32407
+ }
32408
+ }
32409
+ function isXY(p) {
32410
+ return !!p && typeof p.x === "number" && typeof p.y === "number";
32411
+ }
32412
+ function isZeroDurationSet(duration) {
32413
+ return !(duration > 0);
32414
+ }
32415
+ function coordsFromMotionPath(mp) {
32416
+ if (!mp || typeof mp !== "object") return null;
32417
+ const obj = mp;
32418
+ const pathVal = Array.isArray(mp) ? mp : obj.path;
32419
+ if (!Array.isArray(pathVal)) return null;
32420
+ const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
32421
+ if (coords.length < 2) return null;
32422
+ const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
32423
+ const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
32424
+ return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
32425
+ }
32426
+ function flatTweenKeyframes(vars) {
32427
+ const properties = {};
32428
+ for (const [k, v] of Object.entries(vars)) {
32429
+ if (FLAT_SKIP_KEYS.has(k)) continue;
32430
+ if (typeof v === "number") properties[k] = roundTo3(v);
32431
+ else if (typeof v === "string") properties[k] = v;
32432
+ }
32433
+ if (Object.keys(properties).length === 0) return null;
32434
+ return [
32435
+ { percentage: 0, properties },
32436
+ { percentage: 100, properties }
32437
+ ];
32438
+ }
32439
+ function readTween(vars) {
32440
+ if (vars.keyframes && typeof vars.keyframes === "object") {
32441
+ const parsed = parsePercentageKeyframes(vars.keyframes);
32442
+ if (parsed) return parsed;
32443
+ }
32444
+ const mp = coordsFromMotionPath(vars.motionPath);
32445
+ if (mp) {
32446
+ const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
32447
+ if (shape) {
32448
+ const n = shape.waypoints.length;
32449
+ const keyframes = shape.waypoints.map((wp, i) => ({
32450
+ percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
32451
+ properties: { x: wp.x, y: wp.y }
32452
+ }));
32453
+ return { keyframes, arcPath: shape.arcPath };
32454
+ }
32455
+ }
32456
+ const flat = flatTweenKeyframes(vars);
32457
+ return flat ? { keyframes: flat } : null;
32458
+ }
32459
+ function matchesElement(tween, el) {
32460
+ if (!tween.targets) return false;
32461
+ for (const t of tween.targets()) {
32462
+ if (t === el || el.id && t.id === el.id) return true;
32463
+ }
32464
+ return false;
32465
+ }
32466
+ function tweenTiming(tween) {
32467
+ const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
32468
+ const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
32469
+ return {
32470
+ start: Number.isFinite(rawStart) ? rawStart : 0,
32471
+ duration: Number.isFinite(rawDur) ? rawDur : 0
32472
+ };
32473
+ }
32474
+ function varsCarryChannel(vars, channels) {
32475
+ if (!vars) return false;
32476
+ for (const ch of channels) {
32477
+ if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
32478
+ }
32479
+ return false;
32480
+ }
32481
+ function keyframeVarsCarryChannel(vars, channels) {
32482
+ const kf = vars?.keyframes;
32483
+ if (!kf || typeof kf !== "object") return false;
32484
+ if (!Array.isArray(kf) && channels.some((ch) => Object.prototype.hasOwnProperty.call(kf, ch)))
32485
+ return true;
32486
+ const steps = Array.isArray(kf) ? kf : Object.values(kf);
32487
+ return steps.some(
32488
+ (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
32489
+ );
32490
+ }
32491
+ function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
32492
+ const timelines = timelinesOf(iframe);
32493
+ if (!timelines) return null;
32494
+ let targetEl = null;
32495
+ try {
32496
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32497
+ } catch {
32498
+ return null;
32499
+ }
32500
+ if (!targetEl) return null;
32501
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
32502
+ const wantChannels = channels && channels.length > 0 ? channels : null;
32503
+ let first = null;
32504
+ let channelMatch = null;
32505
+ for (const tlId of tlIds) {
32506
+ const timeline = timelines[tlId];
32507
+ if (!timeline?.getChildren) continue;
32508
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
32509
+ for (const tween of timeline.getChildren(true)) {
32510
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32511
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32512
+ const isSet = !(dur > 0);
32513
+ if (kind === "set" ? !isSet : isSet) continue;
32514
+ if (wantChannels) {
32515
+ const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
32516
+ if (carries) {
32517
+ if (channelMatch === null) channelMatch = { tween, timeline };
32518
+ } else if (first === null) {
32519
+ first = { tween, timeline };
32520
+ }
32521
+ continue;
32522
+ }
32523
+ if (first === null) first = { tween, timeline };
32524
+ if (kind === "keyframe" && now2 != null) {
32525
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
32526
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
32527
+ }
32528
+ }
32529
+ }
32530
+ return channelMatch ?? first;
32531
+ }
32532
+ function readCarriesChannel(read, channels) {
32533
+ return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
32534
+ }
32535
+ function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
32536
+ const timelines = timelinesOf(iframe);
32537
+ if (!timelines) return null;
32538
+ let targetEl = null;
32539
+ try {
32540
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32541
+ } catch {
32542
+ return null;
32543
+ }
32544
+ if (!targetEl) return null;
32545
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
32546
+ if (tlIds.length === 0) return null;
32547
+ let firstRead = null;
32548
+ for (const tlId of tlIds) {
32549
+ const timeline = timelines[tlId];
32550
+ if (!timeline?.getChildren) continue;
32551
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
32552
+ for (const tween of timeline.getChildren(true)) {
32553
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32554
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32555
+ if (isZeroDurationSet(dur)) continue;
32556
+ const read = readTween(tween.vars);
32557
+ if (!read) continue;
32558
+ if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
32559
+ if (firstRead === null) firstRead = read;
32560
+ if (now2 != null) {
32561
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
32562
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
32563
+ }
32564
+ }
32565
+ }
32566
+ return firstRead;
32567
+ }
32568
+ function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
32569
+ let targetEl = null;
32570
+ try {
32571
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32572
+ } catch {
32573
+ return false;
32574
+ }
32575
+ return !!targetEl && hasNonHoldTween(timelinesOf(iframe), targetEl, channels, compositionId);
32576
+ }
32577
+ var POSITION_CHANNELS = [
32578
+ "x",
32579
+ "y",
32580
+ "xPercent",
32581
+ "yPercent",
32582
+ "left",
32583
+ "top",
32584
+ // readTween reads the authored translateX/Y; GSAP normalizes them to x/y only at play time.
32585
+ "translateX",
32586
+ "translateY"
32587
+ ];
32588
+ var MOVE_CHANNELS = [...POSITION_CHANNELS, "motionPath"];
32589
+ function gsapWritesChannels(el, channels) {
32590
+ const win = el.ownerDocument.defaultView;
32591
+ return Object.values(win?.__timelines ?? {}).some(
32592
+ (tl) => (tl?.getChildren?.(true) ?? []).some(
32593
+ (tween) => !!tween.vars && matchesElement(tween, el) && (channels.some((ch) => ch in tween.vars) || keyframeVarsCarryChannel(tween.vars, channels))
32594
+ )
32595
+ );
32596
+ }
32597
+ function gsapWritesPosition(el) {
32598
+ const cache2 = el._gsap;
32599
+ return !!cache2?.renderTransform || gsapWritesChannels(el, MOVE_CHANNELS);
32600
+ }
32601
+ function elementHasNonHoldTween(el, channels) {
32602
+ const win = el.ownerDocument.defaultView;
32603
+ return hasNonHoldTween(win?.__timelines ?? null, el, channels);
32604
+ }
32605
+ function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
32606
+ if (!timelines) return false;
32607
+ for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
32608
+ for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
32609
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32610
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32611
+ if (isZeroDurationSet(dur)) continue;
32612
+ if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
32613
+ const read = readTween(tween.vars);
32614
+ if (read && (!channels || readCarriesChannel(read, channels))) return true;
32615
+ }
32616
+ }
32617
+ return false;
32618
+ }
32619
+ function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
32620
+ if (!clip || clip.duration <= 0) return keyframes;
32621
+ return keyframes.map((kf) => {
32622
+ const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
32623
+ return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
32624
+ });
32625
+ }
32626
+ function buildEntry(read, start, duration, clip) {
32627
+ return {
32628
+ keyframes: toClipRelative(read.keyframes, start, duration, clip),
32629
+ tweenStart: start,
32630
+ tweenDuration: duration,
32631
+ ...read.easeEach ? { easeEach: read.easeEach } : {},
32632
+ ...read.arcPath ? { arcPath: read.arcPath } : {}
32633
+ };
32634
+ }
32635
+ function addScanEntry(result, tween, clipById) {
32636
+ if (!tween.targets || !tween.vars) return;
32637
+ const { start, duration } = tweenTiming(tween);
32638
+ if (isZeroDurationSet(duration)) return;
32639
+ const read = readTween(tween.vars);
32640
+ if (!read) return;
32641
+ for (const target of tween.targets()) {
32642
+ const id = target.id;
32643
+ if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
32644
+ }
32645
+ }
32646
+ function scanAllRuntimeKeyframes(iframe, clipById) {
32647
+ const result = /* @__PURE__ */ new Map();
32648
+ const timelines = timelinesOf(iframe);
32649
+ if (!timelines) return result;
32650
+ for (const timeline of Object.values(timelines)) {
32651
+ if (!timeline?.getChildren) continue;
32652
+ for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
32653
+ }
32654
+ return result;
32655
+ }
32656
+
32657
+ // src/components/editor/plainTranslate.ts
32658
+ var TERM = /^(-?\d*\.?\d+(?:e[-+]?\d+)?)(px|%)$/i;
32659
+ var SIGN = { "+": 1, "-": -1 };
32660
+ function termPx(term, side) {
32661
+ const match = TERM.exec(term ?? "");
32662
+ return match ? Number(match[1]) * (match[2] === "%" ? side / 100 : 1) : null;
32663
+ }
32664
+ function resolveLength(value, side) {
32665
+ const parts = (/^calc\((.*)\)$/.exec(value)?.[1] ?? value).split(" ");
32666
+ if (parts.length % 2 === 0) return null;
32667
+ let total = termPx(parts[0], side);
32668
+ for (let i = 1; i < parts.length && total !== null; i += 2) {
32669
+ const term = termPx(parts[i + 1], side);
32670
+ const sign = SIGN[parts[i] ?? ""];
32671
+ total = term === null || !sign ? null : total + sign * term;
32672
+ }
32673
+ return total;
32674
+ }
32675
+ function borderBox(cs, size, a, b) {
32676
+ const n = (prop) => Number.parseFloat(cs.getPropertyValue(prop)) || 0;
32677
+ const pad = n(`padding-${a}`) + n(`padding-${b}`) + n(`border-${a}-width`) + n(`border-${b}-width`);
32678
+ return n(size) + (cs.boxSizing === "border-box" ? 0 : pad);
32679
+ }
32680
+ function resolveInTransform(el, x, y) {
32681
+ const style = el.style;
32682
+ const saved2 = [style.getPropertyValue("transform"), style.getPropertyPriority("transform")];
32683
+ style.setProperty("transform", `translate(${x}, ${y})`, "important");
32684
+ try {
32685
+ const resolved2 = el.ownerDocument.defaultView?.getComputedStyle(el).transform ?? "";
32686
+ const matrix = new DOMMatrixReadOnly(resolved2.startsWith("matrix") ? resolved2 : void 0);
32687
+ return { x: matrix.m41, y: matrix.m42 };
32688
+ } finally {
32689
+ style.setProperty("transform", saved2[0] ?? "", saved2[1] ?? "");
32690
+ }
32691
+ }
32692
+ function readTranslatePx(el) {
32693
+ const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
32694
+ const value = el.style.getPropertyValue("translate") || cs?.translate || "none";
32695
+ if (value === "none") return { x: 0, y: 0 };
32696
+ const [x = "0px", y = "0px"] = splitTopLevelWhitespace(value);
32697
+ const px = cs && resolveLength(x, borderBox(cs, "width", "left", "right"));
32698
+ const py = cs && resolveLength(y, borderBox(cs, "height", "top", "bottom"));
32699
+ return px != null && py != null ? { x: px, y: py } : resolveInTransform(el, x, y);
32700
+ }
32701
+ function formatTranslatePx(p) {
32702
+ return `${roundTo3(p.x)}px ${roundTo3(p.y)}px`;
32703
+ }
32704
+ function translatePatch(p) {
32705
+ return { type: "inline-style", property: "translate", value: formatTranslatePx(p) };
32706
+ }
32707
+ function writeTranslatePx(el, p) {
32708
+ el.style.setProperty("translate", formatTranslatePx(p));
32709
+ }
32710
+ function readMoveOffset(el) {
32711
+ return gsapWritesPosition(el) ? readStudioPathOffset(el) : readTranslatePx(el);
32712
+ }
32713
+
32386
32714
  // src/components/editor/manualOffsetDrag.ts
32387
32715
  function getOffsetDragGsap(element) {
32388
32716
  const win = element.ownerDocument.defaultView;
@@ -32494,14 +32822,15 @@ function measureManualOffsetDragScreenToOffsetMatrix(element, initialOffset, opt
32494
32822
  if (!Number.isFinite(probeSize) || probeSize <= 0) {
32495
32823
  return { ok: false, reason: "Invalid movement probe size." };
32496
32824
  }
32825
+ const draw = options.plainTranslate ? writeTranslatePx : applyStudioPathOffsetDraft;
32497
32826
  const snapshot3 = captureStudioPathOffset(element);
32498
32827
  try {
32499
- applyStudioPathOffsetDraft(element, initialOffset);
32828
+ draw(element, initialOffset);
32500
32829
  const origin = getRectCenter(element);
32501
32830
  if (!origin) {
32502
32831
  return { ok: false, reason: "Element has no measurable box." };
32503
32832
  }
32504
- applyStudioPathOffsetDraft(element, {
32833
+ draw(element, {
32505
32834
  x: initialOffset.x + probeSize,
32506
32835
  y: initialOffset.y
32507
32836
  });
@@ -32509,7 +32838,7 @@ function measureManualOffsetDragScreenToOffsetMatrix(element, initialOffset, opt
32509
32838
  if (!probeX) {
32510
32839
  return { ok: false, reason: "Element X movement could not be measured." };
32511
32840
  }
32512
- applyStudioPathOffsetDraft(element, {
32841
+ draw(element, {
32513
32842
  x: initialOffset.x,
32514
32843
  y: initialOffset.y + probeSize
32515
32844
  });
@@ -32554,14 +32883,13 @@ function resolveManualOffsetForPointerDelta(input) {
32554
32883
  };
32555
32884
  }
32556
32885
  function createManualOffsetDragMember(input) {
32557
- const initialOffset = readAppliedStudioPathOffset(input.element);
32558
- input.element.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
32559
- input.element.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
32886
+ const plainTranslate = !!input.gesture && !gsapWritesPosition(input.element);
32887
+ const initialOffset = plainTranslate ? readTranslatePx(input.element) : readAppliedStudioPathOffset(input.element);
32560
32888
  const win = input.element.ownerDocument.defaultView;
32561
- const gsapX = win?.gsap?.getProperty?.(input.element, "x") || 0;
32562
- const gsapY = win?.gsap?.getProperty?.(input.element, "y") || 0;
32563
- input.element.setAttribute("data-hf-drag-gsap-base-x", String(gsapX));
32564
- input.element.setAttribute("data-hf-drag-gsap-base-y", String(gsapY));
32889
+ const gsapX = !plainTranslate && win?.gsap?.getProperty?.(input.element, "x") || 0;
32890
+ const gsapY = !plainTranslate && win?.gsap?.getProperty?.(input.element, "y") || 0;
32891
+ const baseGsap = { x: gsapX, y: gsapY };
32892
+ if (!plainTranslate) stampGestureBase(input.element, initialOffset, baseGsap);
32565
32893
  if (win?.__timelines) {
32566
32894
  const paused = [];
32567
32895
  for (const [id, tl] of Object.entries(win.__timelines)) {
@@ -32581,9 +32909,9 @@ function createManualOffsetDragMember(input) {
32581
32909
  const gestureToken = beginStudioManualEditGesture(input.element);
32582
32910
  const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
32583
32911
  scaleX: input.rect.editScaleX,
32584
- scaleY: input.rect.editScaleY
32912
+ scaleY: input.rect.editScaleY,
32913
+ plainTranslate
32585
32914
  });
32586
- const baseGsap = { x: gsapX, y: gsapY };
32587
32915
  if (!measured.ok) {
32588
32916
  const scaleX = input.rect.editScaleX || 1;
32589
32917
  const scaleY = input.rect.editScaleY || 1;
@@ -32595,6 +32923,7 @@ function createManualOffsetDragMember(input) {
32595
32923
  selection: input.selection,
32596
32924
  element: input.element,
32597
32925
  initialOffset,
32926
+ plainTranslate,
32598
32927
  baseGsap,
32599
32928
  initialPathOffset,
32600
32929
  gestureToken,
@@ -32610,6 +32939,7 @@ function createManualOffsetDragMember(input) {
32610
32939
  selection: input.selection,
32611
32940
  element: input.element,
32612
32941
  initialOffset,
32942
+ plainTranslate,
32613
32943
  baseGsap,
32614
32944
  initialPathOffset,
32615
32945
  gestureToken,
@@ -32626,24 +32956,27 @@ function resolveManualOffsetDragMemberOffset(member, dx, dy) {
32626
32956
  dy
32627
32957
  });
32628
32958
  }
32959
+ function drawMemberOffset(member, offset, commit) {
32960
+ if (member.plainTranslate) writeTranslatePx(member.element, offset);
32961
+ else if (applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) return;
32962
+ else if (commit) applyStudioPathOffset(member.element, offset);
32963
+ else applyStudioPathOffsetDraft(member.element, offset);
32964
+ }
32629
32965
  function applyManualOffsetDragDraft(member, dx, dy) {
32630
32966
  const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
32631
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32632
- applyStudioPathOffsetDraft(member.element, offset);
32633
- }
32967
+ drawMemberOffset(member, offset, false);
32634
32968
  return offset;
32635
32969
  }
32636
- function restampManualOffsetDragGestureBase(member) {
32637
- member.element.setAttribute("data-hf-drag-gsap-base-x", String(member.baseGsap.x));
32638
- member.element.setAttribute("data-hf-drag-gsap-base-y", String(member.baseGsap.y));
32639
- member.element.setAttribute("data-hf-drag-initial-offset-x", String(member.initialOffset.x));
32640
- member.element.setAttribute("data-hf-drag-initial-offset-y", String(member.initialOffset.y));
32970
+ function stampGestureBase(el, initialOffset, baseGsap) {
32971
+ el.setAttribute("data-hf-drag-gsap-base-x", String(baseGsap.x));
32972
+ el.setAttribute("data-hf-drag-gsap-base-y", String(baseGsap.y));
32973
+ el.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
32974
+ el.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
32641
32975
  }
32642
32976
  function applyManualOffsetCommitValue(member, offset) {
32643
- restampManualOffsetDragGestureBase(member);
32644
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32645
- applyStudioPathOffset(member.element, offset);
32646
- }
32977
+ if (!member.plainTranslate)
32978
+ stampGestureBase(member.element, member.initialOffset, member.baseGsap);
32979
+ drawMemberOffset(member, offset, true);
32647
32980
  return offset;
32648
32981
  }
32649
32982
  function applyManualOffsetDragCommit(member, dx, dy) {
@@ -32654,9 +32987,7 @@ function applyManualOffsetNudgeDraft(member, delta) {
32654
32987
  x: member.initialOffset.x + delta.x,
32655
32988
  y: member.initialOffset.y + delta.y
32656
32989
  };
32657
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32658
- applyStudioPathOffsetDraft(member.element, offset);
32659
- }
32990
+ drawMemberOffset(member, offset, false);
32660
32991
  return offset;
32661
32992
  }
32662
32993
  function applyManualOffsetNudgeCommit(member, delta) {
@@ -32667,7 +32998,9 @@ function applyManualOffsetNudgeCommit(member, delta) {
32667
32998
  }
32668
32999
  function restoreManualOffsetDragMember(member) {
32669
33000
  restoreStudioPathOffset(member.element, member.initialPathOffset);
32670
- getOffsetDragGsap(member.element)?.set(member.element, { ...member.baseGsap });
33001
+ if (!member.plainTranslate) {
33002
+ getOffsetDragGsap(member.element)?.set(member.element, { ...member.baseGsap });
33003
+ }
32671
33004
  endStudioManualEditGesture(member.element, member.gestureToken);
32672
33005
  }
32673
33006
  function restoreManualOffsetDragMembers(members) {
@@ -32692,7 +33025,7 @@ function endManualOffsetDragMembers(members) {
32692
33025
  member.element.removeAttribute("data-hf-drag-initial-offset-y");
32693
33026
  member.element.removeAttribute("data-hf-drag-gsap-base-x");
32694
33027
  member.element.removeAttribute("data-hf-drag-gsap-base-y");
32695
- const gsapOwnsPosition = Boolean(getOffsetDragGsap(member.element));
33028
+ const gsapOwnsPosition = !member.plainTranslate && Boolean(getOffsetDragGsap(member.element));
32696
33029
  if (gsapOwnsPosition) clearStudioPathOffset(member.element);
32697
33030
  settleDraftTranslate(member.element, gsapOwnsPosition);
32698
33031
  resumeGsapTimelines(member.element);
@@ -33345,7 +33678,8 @@ function startGroupDrag(e, opts) {
33345
33678
  key: item.key,
33346
33679
  selection: item.selection,
33347
33680
  element: item.element,
33348
- rect: item.rect
33681
+ rect: item.rect,
33682
+ gesture: "drag"
33349
33683
  });
33350
33684
  if (!result.ok) {
33351
33685
  restoreManualOffsetDragMembers(members);
@@ -33409,7 +33743,6 @@ function startGesture(kind, e, opts, options) {
33409
33743
  if (kind === "resize" && (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)))
33410
33744
  return false;
33411
33745
  const size = readStudioBoxSize(sel.element);
33412
- const rotation = { angle: readGsapRotation(sel.element) + readStudioRotation(sel.element).angle };
33413
33746
  const layoutWidth = sel.element.offsetWidth;
33414
33747
  const layoutHeight = sel.element.offsetHeight;
33415
33748
  const actualWidth = size.width > 0 ? size.width : layoutWidth > 0 ? layoutWidth : rect.width / rect.editScaleX;
@@ -33440,7 +33773,8 @@ function startGesture(kind, e, opts, options) {
33440
33773
  key: selectionCacheKey(sel),
33441
33774
  selection: sel,
33442
33775
  element: sel.element,
33443
- rect
33776
+ rect,
33777
+ gesture: "drag"
33444
33778
  });
33445
33779
  if (!result.ok) {
33446
33780
  opts.onBlockedMoveRef.current(result.selection);
@@ -33469,6 +33803,8 @@ function startGesture(kind, e, opts, options) {
33469
33803
  manualEditDragToken = beginStudioManualEditGesture(sel.element);
33470
33804
  }
33471
33805
  }
33806
+ const gsapRotation = pathOffsetMember?.plainTranslate ? 0 : readGsapRotation(sel.element);
33807
+ const rotation = { angle: gsapRotation + readStudioRotation(sel.element).angle };
33472
33808
  const overlayBounds = overlayEl?.getBoundingClientRect();
33473
33809
  const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
33474
33810
  const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
@@ -34185,7 +34521,7 @@ function useDomEditNudge(params) {
34185
34521
  if (!targets) return null;
34186
34522
  const members = [];
34187
34523
  for (const target of targets) {
34188
- const result = createManualOffsetDragMember(target);
34524
+ const result = createManualOffsetDragMember({ ...target, gesture: "nudge" });
34189
34525
  if (!result.ok) {
34190
34526
  restoreManualOffsetDragMembers(members);
34191
34527
  return null;
@@ -34375,253 +34711,6 @@ var SnapGuideOverlay = memo23(function SnapGuideOverlay2({
34375
34711
  // src/components/editor/DomEditCropHandles.tsx
34376
34712
  import { useEffect as useEffect44, useRef as useRef65, useState as useState40 } from "react";
34377
34713
 
34378
- // src/hooks/gsapRuntimeKeyframes.ts
34379
- import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
34380
- var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
34381
- "ease",
34382
- "duration",
34383
- "delay",
34384
- "stagger",
34385
- "motionPath",
34386
- "overwrite",
34387
- "immediateRender",
34388
- "onComplete",
34389
- "onUpdate",
34390
- "onStart",
34391
- "keyframes"
34392
- ]);
34393
- function timelinesOf(iframe) {
34394
- if (!iframe?.contentWindow) return null;
34395
- try {
34396
- return iframe.contentWindow.__timelines ?? null;
34397
- } catch {
34398
- return null;
34399
- }
34400
- }
34401
- function isXY(p) {
34402
- return !!p && typeof p.x === "number" && typeof p.y === "number";
34403
- }
34404
- function isZeroDurationSet(duration) {
34405
- return !(duration > 0);
34406
- }
34407
- function coordsFromMotionPath(mp) {
34408
- if (!mp || typeof mp !== "object") return null;
34409
- const obj = mp;
34410
- const pathVal = Array.isArray(mp) ? mp : obj.path;
34411
- if (!Array.isArray(pathVal)) return null;
34412
- const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
34413
- if (coords.length < 2) return null;
34414
- const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
34415
- const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
34416
- return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
34417
- }
34418
- function flatTweenKeyframes(vars) {
34419
- const properties = {};
34420
- for (const [k, v] of Object.entries(vars)) {
34421
- if (FLAT_SKIP_KEYS.has(k)) continue;
34422
- if (typeof v === "number") properties[k] = roundTo3(v);
34423
- else if (typeof v === "string") properties[k] = v;
34424
- }
34425
- if (Object.keys(properties).length === 0) return null;
34426
- return [
34427
- { percentage: 0, properties },
34428
- { percentage: 100, properties }
34429
- ];
34430
- }
34431
- function readTween(vars) {
34432
- if (vars.keyframes && typeof vars.keyframes === "object") {
34433
- const parsed = parsePercentageKeyframes(vars.keyframes);
34434
- if (parsed) return parsed;
34435
- }
34436
- const mp = coordsFromMotionPath(vars.motionPath);
34437
- if (mp) {
34438
- const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
34439
- if (shape) {
34440
- const n = shape.waypoints.length;
34441
- const keyframes = shape.waypoints.map((wp, i) => ({
34442
- percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
34443
- properties: { x: wp.x, y: wp.y }
34444
- }));
34445
- return { keyframes, arcPath: shape.arcPath };
34446
- }
34447
- }
34448
- const flat = flatTweenKeyframes(vars);
34449
- return flat ? { keyframes: flat } : null;
34450
- }
34451
- function matchesElement(tween, el) {
34452
- if (!tween.targets) return false;
34453
- for (const t of tween.targets()) {
34454
- if (t === el || el.id && t.id === el.id) return true;
34455
- }
34456
- return false;
34457
- }
34458
- function tweenTiming(tween) {
34459
- const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
34460
- const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
34461
- return {
34462
- start: Number.isFinite(rawStart) ? rawStart : 0,
34463
- duration: Number.isFinite(rawDur) ? rawDur : 0
34464
- };
34465
- }
34466
- function varsCarryChannel(vars, channels) {
34467
- if (!vars) return false;
34468
- for (const ch of channels) {
34469
- if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
34470
- }
34471
- return false;
34472
- }
34473
- function keyframeVarsCarryChannel(vars, channels) {
34474
- const kf = vars?.keyframes;
34475
- if (!kf || typeof kf !== "object") return false;
34476
- if (!Array.isArray(kf) && channels.some((ch) => Object.prototype.hasOwnProperty.call(kf, ch)))
34477
- return true;
34478
- const steps = Array.isArray(kf) ? kf : Object.values(kf);
34479
- return steps.some(
34480
- (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
34481
- );
34482
- }
34483
- function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
34484
- const timelines = timelinesOf(iframe);
34485
- if (!timelines) return null;
34486
- let targetEl = null;
34487
- try {
34488
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34489
- } catch {
34490
- return null;
34491
- }
34492
- if (!targetEl) return null;
34493
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34494
- const wantChannels = channels && channels.length > 0 ? channels : null;
34495
- let first = null;
34496
- let channelMatch = null;
34497
- for (const tlId of tlIds) {
34498
- const timeline = timelines[tlId];
34499
- if (!timeline?.getChildren) continue;
34500
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34501
- for (const tween of timeline.getChildren(true)) {
34502
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34503
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34504
- const isSet = !(dur > 0);
34505
- if (kind === "set" ? !isSet : isSet) continue;
34506
- if (wantChannels) {
34507
- const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
34508
- if (carries) {
34509
- if (channelMatch === null) channelMatch = { tween, timeline };
34510
- } else if (first === null) {
34511
- first = { tween, timeline };
34512
- }
34513
- continue;
34514
- }
34515
- if (first === null) first = { tween, timeline };
34516
- if (kind === "keyframe" && now2 != null) {
34517
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34518
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
34519
- }
34520
- }
34521
- }
34522
- return channelMatch ?? first;
34523
- }
34524
- function readCarriesChannel(read, channels) {
34525
- return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
34526
- }
34527
- function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
34528
- const timelines = timelinesOf(iframe);
34529
- if (!timelines) return null;
34530
- let targetEl = null;
34531
- try {
34532
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34533
- } catch {
34534
- return null;
34535
- }
34536
- if (!targetEl) return null;
34537
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34538
- if (tlIds.length === 0) return null;
34539
- let firstRead = null;
34540
- for (const tlId of tlIds) {
34541
- const timeline = timelines[tlId];
34542
- if (!timeline?.getChildren) continue;
34543
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34544
- for (const tween of timeline.getChildren(true)) {
34545
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34546
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34547
- if (isZeroDurationSet(dur)) continue;
34548
- const read = readTween(tween.vars);
34549
- if (!read) continue;
34550
- if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
34551
- if (firstRead === null) firstRead = read;
34552
- if (now2 != null) {
34553
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34554
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
34555
- }
34556
- }
34557
- }
34558
- return firstRead;
34559
- }
34560
- function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
34561
- let targetEl = null;
34562
- try {
34563
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34564
- } catch {
34565
- return false;
34566
- }
34567
- return !!targetEl && hasNonHoldTween(timelinesOf(iframe), targetEl, channels, compositionId);
34568
- }
34569
- function elementHasNonHoldTween(el, channels) {
34570
- const win = el.ownerDocument.defaultView;
34571
- return hasNonHoldTween(win?.__timelines ?? null, el, channels);
34572
- }
34573
- function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
34574
- if (!timelines) return false;
34575
- for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
34576
- for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
34577
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34578
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34579
- if (isZeroDurationSet(dur)) continue;
34580
- if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
34581
- const read = readTween(tween.vars);
34582
- if (read && (!channels || readCarriesChannel(read, channels))) return true;
34583
- }
34584
- }
34585
- return false;
34586
- }
34587
- function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
34588
- if (!clip || clip.duration <= 0) return keyframes;
34589
- return keyframes.map((kf) => {
34590
- const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
34591
- return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
34592
- });
34593
- }
34594
- function buildEntry(read, start, duration, clip) {
34595
- return {
34596
- keyframes: toClipRelative(read.keyframes, start, duration, clip),
34597
- tweenStart: start,
34598
- tweenDuration: duration,
34599
- ...read.easeEach ? { easeEach: read.easeEach } : {},
34600
- ...read.arcPath ? { arcPath: read.arcPath } : {}
34601
- };
34602
- }
34603
- function addScanEntry(result, tween, clipById) {
34604
- if (!tween.targets || !tween.vars) return;
34605
- const { start, duration } = tweenTiming(tween);
34606
- if (isZeroDurationSet(duration)) return;
34607
- const read = readTween(tween.vars);
34608
- if (!read) return;
34609
- for (const target of tween.targets()) {
34610
- const id = target.id;
34611
- if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
34612
- }
34613
- }
34614
- function scanAllRuntimeKeyframes(iframe, clipById) {
34615
- const result = /* @__PURE__ */ new Map();
34616
- const timelines = timelinesOf(iframe);
34617
- if (!timelines) return result;
34618
- for (const timeline of Object.values(timelines)) {
34619
- if (!timeline?.getChildren) continue;
34620
- for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
34621
- }
34622
- return result;
34623
- }
34624
-
34625
34714
  // src/components/editor/cropResize.ts
34626
34715
  var boxOf = (element) => ({
34627
34716
  width: element.offsetWidth,
@@ -42384,19 +42473,6 @@ function getAnimationsForElement(animations, target, element) {
42384
42473
  }
42385
42474
 
42386
42475
  // src/hooks/gsapRuntimeBridge.ts
42387
- var POSITION_CHANNELS = [
42388
- "x",
42389
- "y",
42390
- "xPercent",
42391
- "yPercent",
42392
- "left",
42393
- "top",
42394
- // GSAP normalizes translateX/Y to x/y at play time, but readTween reads the
42395
- // AUTHORED shape — include them so a hand-authored translateX/Y position tween
42396
- // still counts as a live position tween.
42397
- "translateX",
42398
- "translateY"
42399
- ];
42400
42476
  var POSITION_CHANNEL_SET = new Set(POSITION_CHANNELS);
42401
42477
  var ROTATION_CHANNELS = ["rotation", "rotationX", "rotationY", "rotationZ"];
42402
42478
  var ROTATION_CHANNEL_SET = new Set(ROTATION_CHANNELS);
@@ -47838,7 +47914,7 @@ function createTransformCommitHandlers({
47838
47914
  "This element's position can't be edited here yet \u2014 it is driven by its animation"
47839
47915
  ))
47840
47916
  return;
47841
- const current = readStudioPathOffset(element.element);
47917
+ const current = readMoveOffset(element.element);
47842
47918
  await Promise.resolve(
47843
47919
  onSetManualOffset(element, {
47844
47920
  x: axis === "x" ? parsed : current.x,
@@ -64921,7 +64997,7 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
64921
64997
  const sourceLabel = element.id ? `#${element.id}` : element.selector ?? "";
64922
64998
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
64923
64999
  const showEditableSections = element.capabilities.canEditStyles && sections.style;
64924
- const manualOffset = readStudioPathOffset(element.element);
65000
+ const manualOffset = readMoveOffset(element.element);
64925
65001
  const manualSize = readStudioBoxSize(element.element);
64926
65002
  const resolvedWidth = manualSize.width > 0 ? manualSize.width : parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width;
64927
65003
  const resolvedHeight = manualSize.height > 0 ? manualSize.height : parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height;
@@ -73716,10 +73792,6 @@ function canCommitInlineTextSelection(selection, element) {
73716
73792
  if (selection.isCompositionHost || selection.isInsideLockedComposition) return false;
73717
73793
  return canEditElementTextInline(element);
73718
73794
  }
73719
- function ownsCurrentPreviewElement(selection, element, document2) {
73720
- if (!document2 || !element.isConnected) return false;
73721
- return element === selection.element && element.ownerDocument === document2;
73722
- }
73723
73795
  async function resyncDomTextSelectionFromPreview(doc, selection, activeCompPath, buildDomSelectionFromTarget, applyDomSelection) {
73724
73796
  if (!doc) return;
73725
73797
  const refreshed = findElementForSelection(doc, selection, activeCompPath);
@@ -73742,6 +73814,8 @@ function useDomEditTextCommits({
73742
73814
  }) {
73743
73815
  const latestReadOnlyPreviewRef = useRef142(readOnlyPreview);
73744
73816
  latestReadOnlyPreviewRef.current = readOnlyPreview;
73817
+ const latestSelectionRef = useRef142(domEditSelection);
73818
+ latestSelectionRef.current = domEditSelection;
73745
73819
  const domTextCommitVersionRef = useRef142(/* @__PURE__ */ new Map());
73746
73820
  const domStyleCommitVersionRef = useRef142(/* @__PURE__ */ new Map());
73747
73821
  const {
@@ -73853,18 +73927,32 @@ function useDomEditTextCommits({
73853
73927
  );
73854
73928
  const handleDomRichTextCommit = useCallback123(
73855
73929
  async ({ element, html: html2, previousHtml }) => {
73856
- if (!domEditSelection) return;
73857
- if (latestReadOnlyPreviewRef.current) {
73930
+ const putBack = () => {
73858
73931
  if (element.isConnected && element.innerHTML === html2) element.innerHTML = previousHtml;
73859
- return;
73932
+ };
73933
+ if (latestReadOnlyPreviewRef.current) return putBack();
73934
+ const refuse = (reason) => {
73935
+ console.error("[Studio] text edit not saved:", reason, element);
73936
+ showToast(`Couldn't save the text edit: ${reason}`, "error");
73937
+ putBack();
73938
+ };
73939
+ const doc = previewIframeRef.current?.contentDocument;
73940
+ if (!doc || !element.isConnected || element.ownerDocument !== doc) {
73941
+ return refuse("the text's element is gone from the preview");
73942
+ }
73943
+ const selection = await buildDomSelectionFromTarget(element, {
73944
+ exactTarget: true,
73945
+ skipSourceProbe: true
73946
+ });
73947
+ if (selection?.element !== element) {
73948
+ return refuse("this text was not found in the composition's source");
73949
+ }
73950
+ if (!canCommitInlineTextSelection(selection, element)) {
73951
+ return refuse("this text can't be edited in place");
73860
73952
  }
73861
- if (!canCommitInlineTextSelection(domEditSelection, element)) return;
73862
- const iframe = previewIframeRef.current;
73863
- const doc = iframe?.contentDocument;
73864
- if (!ownsCurrentPreviewElement(domEditSelection, element, doc)) return;
73865
73953
  const isLatestTextCommit = bumpDomEditCommitMapVersion(
73866
73954
  domTextCommitVersionRef.current,
73867
- getDomEditTargetKey(domEditSelection)
73955
+ getDomEditTargetKey(selection)
73868
73956
  );
73869
73957
  const operations = [buildDomEditRichTextPatchOperation(html2)];
73870
73958
  let appliedHtml = "";
@@ -73876,7 +73964,7 @@ function useDomEditTextCommits({
73876
73964
  appliedHtml = element.innerHTML;
73877
73965
  },
73878
73966
  persist: async () => {
73879
- await persistDomEditOperations(domEditSelection, operations, {
73967
+ await persistDomEditOperations(selection, operations, {
73880
73968
  label: "Edit text",
73881
73969
  skipRefresh: true,
73882
73970
  shouldSave: isLatestTextCommit
@@ -73888,11 +73976,12 @@ function useDomEditTextCommits({
73888
73976
  element.innerHTML = previousHtml;
73889
73977
  }
73890
73978
  },
73891
- onError: (error) => reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
73892
- shouldResync: isLatestTextCommit,
73979
+ onError: (error) => reportDomEditPersistFailure(selection, operations, error, showToast),
73980
+ // Re-select only what is still selected: the selection may have moved on, or a host cleared it.
73981
+ shouldResync: () => isLatestTextCommit() && latestSelectionRef.current?.element === element,
73893
73982
  resync: () => resyncDomTextSelectionFromPreview(
73894
73983
  doc,
73895
- domEditSelection,
73984
+ selection,
73896
73985
  activeCompPath,
73897
73986
  buildDomSelectionFromTarget,
73898
73987
  applyDomSelection
@@ -73904,7 +73993,6 @@ function useDomEditTextCommits({
73904
73993
  activeCompPath,
73905
73994
  applyDomSelection,
73906
73995
  buildDomSelectionFromTarget,
73907
- domEditSelection,
73908
73996
  persistDomEditOperations,
73909
73997
  previewIframeRef,
73910
73998
  showToast
@@ -74122,9 +74210,29 @@ function settleGsapDraftAtGestureStart(el) {
74122
74210
  const y = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
74123
74211
  if (gsap3 && Number.isFinite(x) && Number.isFinite(y)) gsap3.set(el, { x, y });
74124
74212
  }
74213
+ var plainMoveCounter = 0;
74214
+ function stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey) {
74215
+ const style = selection.element.style;
74216
+ const before = style.getPropertyValue("translate");
74217
+ const patch = translatePatch(next);
74218
+ style.setProperty("translate", patch.value);
74219
+ const rollback = () => {
74220
+ if (style.getPropertyValue("translate") === patch.value) style.setProperty("translate", before);
74221
+ };
74222
+ const key2 = coalesceKey ?? `move:${++plainMoveCounter}`;
74223
+ const options = { label: "Move layer", coalesceKey: key2, coalesceMs: Number.POSITIVE_INFINITY };
74224
+ const save = () => commitPositionPatchToHtml(selection, [patch], options).catch((error) => {
74225
+ rollback();
74226
+ throw error;
74227
+ });
74228
+ return { save, rollback };
74229
+ }
74125
74230
  function stageElementOffset({ commitPositionPatchToHtml, showToast, readOnlyPreview }, selection, next, coalesceKey) {
74126
74231
  const el = selection.element;
74127
74232
  if (readOnlyPreview) return { save: () => Promise.resolve(), rollback: () => void 0 };
74233
+ if (!gsapWritesPosition(el)) {
74234
+ return stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey);
74235
+ }
74128
74236
  const previous = { position: el.style.position, left: el.style.left, top: el.style.top };
74129
74237
  const liftMarker = el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
74130
74238
  const result = applyElementPositionOffset(el, next);
@@ -77602,6 +77710,8 @@ function useGsapAwareEditing({
77602
77710
  );
77603
77711
  const handleGsapAwarePathOffsetCommit = useCallback138(
77604
77712
  async (selection, next, modifiers) => {
77713
+ if (!gsapWritesPosition(selection.element))
77714
+ return stageElementPositionOffset(selection, next).save();
77605
77715
  if (gsapCommitMutation) {
77606
77716
  try {
77607
77717
  const ownedAnimations = getGsapAnimationsForSelection(selection);
@@ -77667,6 +77777,7 @@ function useGsapAwareEditing({
77667
77777
  const offsetMembers = /* @__PURE__ */ new Set();
77668
77778
  const preflightResults = await Promise.allSettled(
77669
77779
  updates.map(async ({ selection }) => {
77780
+ if (!gsapWritesPosition(selection.element)) return void offsetMembers.add(selection);
77670
77781
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
77671
77782
  preflightAnimations.set(selection, animations);
77672
77783
  const outcome = await tryGsapDragIntercept(