@hyperframes/studio 0.7.78 → 0.7.80

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 (103) hide show
  1. package/dist/assets/{hyperframes-player-uiAAPKvw.js → hyperframes-player-Csai0_vh.js} +1 -1
  2. package/dist/assets/{index-CDSTMtUs.js → index-B5aYTg-4.js} +1 -1
  3. package/dist/assets/{index-BSZrK0bx.js → index-BhdJN49y.js} +202 -202
  4. package/dist/assets/{index-r1tKKlU7.js → index-By-YiRxq.js} +1 -1
  5. package/dist/assets/index-D379YOKT.css +1 -0
  6. package/dist/index.d.ts +45 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +1891 -1522
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/components/StudioLeftSidebar.tsx +10 -5
  12. package/src/components/StudioRightPanel.tsx +7 -5
  13. package/src/components/TimelineToolbar.tsx +3 -1
  14. package/src/components/editor/AnimationCard.test.tsx +225 -18
  15. package/src/components/editor/AnimationCard.tsx +24 -3
  16. package/src/components/editor/EaseCurveSection.test.tsx +147 -2
  17. package/src/components/editor/EaseCurveSection.tsx +77 -15
  18. package/src/components/editor/GsapAnimationSection.test.tsx +102 -0
  19. package/src/components/editor/GsapAnimationSection.tsx +6 -1
  20. package/src/components/editor/KeyframeEaseList.tsx +4 -0
  21. package/src/components/editor/KeyframeNavigation.tsx +5 -0
  22. package/src/components/editor/MotionPathOverlay.tsx +58 -20
  23. package/src/components/editor/PropertyPanel.tsx +7 -7
  24. package/src/components/editor/PropertyPanelFlat.tsx +6 -91
  25. package/src/components/editor/gsapAnimationCallbacks.test.ts +8 -1
  26. package/src/components/editor/gsapAnimationCallbacks.ts +8 -0
  27. package/src/components/editor/holdEaseSeek.test.ts +37 -0
  28. package/src/components/editor/keyframeRetime.test.ts +42 -0
  29. package/src/components/editor/keyframeRetime.ts +4 -1
  30. package/src/components/editor/motionPathSelection.test.ts +65 -0
  31. package/src/components/editor/motionPathSelection.ts +13 -3
  32. package/src/components/editor/propertyPanelColor.test.tsx +181 -12
  33. package/src/components/editor/propertyPanelColor.tsx +32 -26
  34. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +69 -0
  35. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -1
  36. package/src/components/editor/propertyPanelFlatProps.ts +91 -0
  37. package/src/components/editor/propertyPanelPrimitives.tsx +3 -1
  38. package/src/components/editor/propertyPanelTypes.ts +2 -0
  39. package/src/components/nle/TimelineResizeDivider.tsx +9 -4
  40. package/src/components/pointerTargetSize.test.tsx +65 -0
  41. package/src/components/sidebar/CompositionsTab.tsx +4 -1
  42. package/src/contexts/DomEditContext.tsx +4 -0
  43. package/src/hooks/gsapDragCommit.ts +43 -10
  44. package/src/hooks/gsapDragStaticSetHelpers.ts +16 -6
  45. package/src/hooks/gsapKeyframeCacheHelpers.test.ts +195 -43
  46. package/src/hooks/gsapKeyframeCacheHelpers.ts +190 -83
  47. package/src/hooks/gsapKeyframeCommit.ts +11 -2
  48. package/src/hooks/gsapResizeIntercept.ts +2 -2
  49. package/src/hooks/gsapRuntimeBridge.ts +3 -2
  50. package/src/hooks/gsapScriptCommitHelpers.ts +17 -2
  51. package/src/hooks/gsapShared.test.ts +144 -0
  52. package/src/hooks/gsapShared.ts +264 -0
  53. package/src/hooks/gsapShared.writeTarget.test.ts +289 -0
  54. package/src/hooks/gsapTweenSynth.test.ts +70 -15
  55. package/src/hooks/gsapTweenSynth.ts +50 -23
  56. package/src/hooks/keyframeCacheAstLoad.ts +9 -83
  57. package/src/hooks/newTweenTarget.test.ts +364 -0
  58. package/src/hooks/newTweenTargetHooks.test.tsx +255 -0
  59. package/src/hooks/timelineTrackVisibility.test.ts +61 -1
  60. package/src/hooks/timelineTrackVisibility.ts +11 -1
  61. package/src/hooks/useAnimatedPropertyCommit.ts +24 -8
  62. package/src/hooks/useDomEditSession.test.tsx +152 -3
  63. package/src/hooks/useDomEditSession.ts +4 -53
  64. package/src/hooks/useEnableKeyframes.test.ts +44 -0
  65. package/src/hooks/useEnableKeyframes.ts +18 -5
  66. package/src/hooks/useGestureCommit.ts +21 -4
  67. package/src/hooks/useGsapScriptCommits.ts +3 -0
  68. package/src/hooks/useGsapTweenCache.test.ts +11 -3
  69. package/src/hooks/useGsapTweenCache.ts +44 -45
  70. package/src/hooks/useKeyframeEaseCommits.ts +78 -0
  71. package/src/player/components/KeyframeDiamondContextMenu.test.tsx +21 -0
  72. package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -12
  73. package/src/player/components/LayerDisclosureRow.tsx +4 -1
  74. package/src/player/components/ShortcutsPanel.test.tsx +158 -0
  75. package/src/player/components/ShortcutsPanel.tsx +11 -15
  76. package/src/player/components/Timeline.test.ts +3 -1
  77. package/src/player/components/TimelineCanvas.tsx +2 -1
  78. package/src/player/components/TimelineClipDiamonds.test.tsx +35 -8
  79. package/src/player/components/TimelineClipDiamonds.tsx +11 -1
  80. package/src/player/components/TimelineDiamondConnectors.tsx +118 -72
  81. package/src/player/components/TimelineLanes.test.tsx +281 -0
  82. package/src/player/components/TimelineLanes.tsx +43 -97
  83. package/src/player/components/TimelinePropertyLanes.test.tsx +217 -0
  84. package/src/player/components/TimelinePropertyLanes.tsx +80 -11
  85. package/src/player/components/TimelineTrackHeader.test.tsx +75 -5
  86. package/src/player/components/TimelineTrackHeader.tsx +52 -22
  87. package/src/player/components/timelineDiamondTypes.ts +4 -3
  88. package/src/player/components/timelineKeyframeIdentity.ts +3 -0
  89. package/src/player/components/timelineLaneProps.ts +86 -0
  90. package/src/player/components/timelineTrackDisplay.test.ts +35 -0
  91. package/src/player/components/timelineTrackDisplay.ts +38 -0
  92. package/src/player/components/useTimelineKeyframeHandlers.test.tsx +52 -32
  93. package/src/player/components/useTimelineKeyframeHandlers.ts +1 -0
  94. package/src/player/components/useTimelineRangeSelection.ts +5 -0
  95. package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +127 -0
  96. package/src/player/components/useTimelineTrackLayout.test.ts +35 -0
  97. package/src/player/components/useTimelineTrackLayout.ts +4 -4
  98. package/src/player/hooks/useExpandedTimelineElements.test.ts +180 -2
  99. package/src/player/hooks/useExpandedTimelineElements.ts +70 -7
  100. package/src/player/store/keyframeSlice.ts +15 -5
  101. package/src/telemetry/client.test.ts +12 -3
  102. package/src/telemetry/client.ts +3 -2
  103. package/dist/assets/index-DpkO2Hvw.css +0 -1
@@ -0,0 +1,91 @@
1
+ import type { resolveEditingSections } from "@hyperframes/core/editing";
2
+ import type { DomEditSelection } from "./domEditing";
3
+ import type { PropertyPanelProps } from "./propertyPanelHelpers";
4
+ import type { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
5
+
6
+ export type PropertyPanelFlatProps = Pick<
7
+ PropertyPanelProps,
8
+ | "projectId"
9
+ | "projectDir"
10
+ | "assets"
11
+ | "previewIframeRef"
12
+ | "onClearSelection"
13
+ | "onUngroup"
14
+ | "onSetStyle"
15
+ | "onPreviewStyle"
16
+ | "onSetAttribute"
17
+ | "onSetAttributes"
18
+ | "onSetAttributeLive"
19
+ | "onApplyColorGradingScope"
20
+ | "onSetHtmlAttribute"
21
+ | "onRemoveBackground"
22
+ | "onSetText"
23
+ | "onSetTextFieldStyle"
24
+ | "onPreviewTextFieldStyle"
25
+ | "onAddTextField"
26
+ | "onRemoveTextField"
27
+ | "onAskAgent"
28
+ | "onToggleElementHidden"
29
+ | "onImportAssets"
30
+ | "onAddMediaOverlay"
31
+ | "onImportFonts"
32
+ | "fontAssets"
33
+ | "gsapAnimations"
34
+ | "gsapMultipleTimelines"
35
+ | "gsapUnsupportedTimelinePattern"
36
+ | "onUpdateGsapProperty"
37
+ | "onUpdateGsapMeta"
38
+ | "onDeleteGsapAnimation"
39
+ | "onAddGsapProperty"
40
+ | "onRemoveGsapProperty"
41
+ | "onUpdateGsapFromProperty"
42
+ | "onAddGsapFromProperty"
43
+ | "onRemoveGsapFromProperty"
44
+ | "onAddGsapAnimation"
45
+ | "onSetArcPath"
46
+ | "onUpdateArcSegment"
47
+ | "onUnroll"
48
+ | "onUpdateKeyframeEase"
49
+ | "onSetAllKeyframeEases"
50
+ | "onUpdateSegmentEase"
51
+ | "recordingState"
52
+ | "recordingDuration"
53
+ | "onToggleRecording"
54
+ > &
55
+ Pick<
56
+ Parameters<typeof FlatLayoutSection>[0],
57
+ | "displayX"
58
+ | "displayY"
59
+ | "displayW"
60
+ | "displayH"
61
+ | "displayR"
62
+ | "manualOffsetEditingDisabled"
63
+ | "manualSizeEditingDisabled"
64
+ | "manualRotationEditingDisabled"
65
+ | "commitManualOffset"
66
+ | "commitManualSize"
67
+ | "commitManualRotation"
68
+ | "gsapAnimId"
69
+ | "navKeyframes"
70
+ | "animIdForProp"
71
+ | "gsapRuntimeValues"
72
+ | "elStart"
73
+ | "elDuration"
74
+ | "onCommitAnimatedProperty"
75
+ | "onCommitAnimatedProperties"
76
+ | "onSeekToTime"
77
+ | "onRemoveKeyframe"
78
+ | "onConvertToKeyframes"
79
+ > & {
80
+ element: DomEditSelection;
81
+ styles: Record<string, string>;
82
+ sections: ReturnType<typeof resolveEditingSections>;
83
+ sourceLabel: string;
84
+ gsapBorderRadius: { tl: number; tr: number; br: number; bl: number } | null;
85
+ showEditableSections: boolean;
86
+ selectedElementHidden: boolean;
87
+ selectedElementId: string | null;
88
+ clipboardCopied: boolean;
89
+ onCopyElementInfo: () => void;
90
+ currentTime: number;
91
+ };
@@ -189,7 +189,9 @@ export function SliderControl({
189
189
  onMouseUp={() => commitDraft(draft)}
190
190
  onTouchEnd={() => commitDraft(draft)}
191
191
  onBlur={() => commitDraft(draft)}
192
- className="h-4 min-w-0 w-full cursor-pointer appearance-none bg-transparent disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-panel-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0C0C0E,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
192
+ // h-6 is the 24x24 WCAG 2.2 (2.5.8) target: the visible track stays 2px
193
+ // and the thumb 10px, only the pointer box grows.
194
+ className="h-6 min-w-0 w-full cursor-pointer appearance-none bg-transparent disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-panel-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0C0C0E,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
193
195
  />
194
196
  <div className="min-w-[44px] rounded-md bg-panel-input px-2 py-1.5 text-right text-[11px] font-medium text-panel-text-1 tabular-nums">
195
197
  {formatDisplayValue?.(draft) ?? displayValue}
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
2
2
  import type { ArcPathSegment, GsapAnimation } from "@hyperframes/parsers/gsap-parser";
3
3
  import type { DomEditSelection } from "./domEditing";
4
4
  import type { ImportedFontAsset } from "./fontAssets";
5
+ import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
5
6
 
6
7
  export interface BackgroundRemovalProgress {
7
8
  status: "processing" | "complete" | "failed";
@@ -123,6 +124,7 @@ export interface PropertyPanelProps {
123
124
  onRemoveKeyframe?: (animationId: string, percentage: number) => void;
124
125
  onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
125
126
  onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
127
+ onUpdateSegmentEase?: NonNullable<GsapAnimationEditCallbacks["onUpdateSegmentEase"]>;
126
128
  onConvertToKeyframes?: (animationId: string, duration?: number) => void;
127
129
  onCommitAnimatedProperty?: (
128
130
  selection: DomEditSelection,
@@ -73,9 +73,9 @@ export function TimelineResizeDivider({
73
73
  );
74
74
 
75
75
  return (
76
- // Horizontal resize divider: 3px visible seam (h-[3px]), 8px pointer-capture
76
+ // Horizontal resize divider: 3px visible seam (h-[3px]), 10px pointer-capture
77
77
  // zone via the absolutely-positioned inner hit area so the layout gap stays
78
- // at 3px while draggability is preserved over the full 8px band.
78
+ // at 3px while draggability is preserved over the full band.
79
79
  <div
80
80
  role="separator"
81
81
  aria-orientation="horizontal"
@@ -94,8 +94,13 @@ export function TimelineResizeDivider({
94
94
  onPointerCancel={handlePointerUp}
95
95
  onKeyDown={handleKeyDown}
96
96
  >
97
- {/* Expanded hit zone: 8px tall, centered on the 3px seam */}
98
- <div className="absolute inset-x-0 -top-[2.5px] h-2" />
97
+ {/* Expanded hit zone, deliberately asymmetric: 4px up into the transport
98
+ row's vertical centring slack, the 3px seam, then 3px down into the
99
+ timeline card's border + toolbar padding. It stops at 10px rather than
100
+ the 24px WCAG 2.2 (2.5.8) target because that is all the dead space
101
+ there is: 28px transport buttons sit above and 28px toolbar buttons
102
+ below, and silently stealing their clicks is the worse bug. */}
103
+ <div className="absolute inset-x-0 -top-[4px] h-[10px]" />
99
104
  {/* Visible hairline — invisible at rest, subtle wash on hover/drag/focus */}
100
105
  <div className="h-[3px] w-full bg-transparent transition-colors group-hover:bg-white/12 group-active:bg-white/18 group-focus-visible:bg-studio-accent/60" />
101
106
  </div>
@@ -0,0 +1,65 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { CompositionsTab } from "./sidebar/CompositionsTab";
7
+ import { TimelineToolbar } from "./TimelineToolbar";
8
+
9
+ // WCAG 2.2 (2.5.8) requires a 24x24 CSS pixel pointer target. happy-dom has no
10
+ // CSS engine, so getBoundingClientRect() is 0x0 for everything here and the size
11
+ // itself cannot be measured — these assert the utility classes that PRODUCE the
12
+ // size instead, which is enough to fail if someone drops them. Real geometry
13
+ // still needs a browser check.
14
+
15
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
16
+ (
17
+ window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
18
+ ).happyDOM.settings.disableIframePageLoading = true;
19
+
20
+ let root: Root | null = null;
21
+
22
+ afterEach(() => {
23
+ if (root) act(() => root?.unmount());
24
+ root = null;
25
+ document.body.innerHTML = "";
26
+ });
27
+
28
+ function mount(element: React.ReactNode) {
29
+ const host = document.createElement("div");
30
+ document.body.append(host);
31
+ root = createRoot(host);
32
+ act(() => root?.render(element));
33
+ return host;
34
+ }
35
+
36
+ describe("pointer target sizing classes (proxy for WCAG 2.5.8, not a geometry check)", () => {
37
+ it("gives the timeline zoom slider a 24px box without changing its 2px track or 10px thumb", () => {
38
+ const host = mount(<TimelineToolbar />);
39
+ const slider = host.querySelector<HTMLInputElement>('input[aria-label="Timeline zoom"]');
40
+ if (!slider) throw new Error("zoom slider did not render");
41
+
42
+ expect(slider.className).toContain("h-6");
43
+ expect(slider.className).toContain("[&::-webkit-slider-runnable-track]:h-[2px]");
44
+ expect(slider.className).toContain("[&::-webkit-slider-thumb]:h-[10px]");
45
+ expect(slider.className).toContain("[&::-webkit-slider-thumb]:w-[10px]");
46
+ });
47
+
48
+ it("gives the composition card's render button a 24x24 box around its 14px glyph", () => {
49
+ const host = mount(
50
+ <CompositionsTab
51
+ projectId="demo"
52
+ compositions={["compositions/headline.html"]}
53
+ activeComposition={null}
54
+ onSelect={vi.fn()}
55
+ onRenderComposition={vi.fn()}
56
+ />,
57
+ );
58
+ const button = host.querySelector<HTMLButtonElement>('button[aria-label="Render headline"]');
59
+ if (!button) throw new Error("render button did not render");
60
+
61
+ expect(button.className).toContain("h-6");
62
+ expect(button.className).toContain("w-6");
63
+ expect(button.querySelector("svg")?.getAttribute("width")).toBe("14");
64
+ });
65
+ });
@@ -317,7 +317,10 @@ function CompCard({
317
317
  e.stopPropagation();
318
318
  onRender();
319
319
  }}
320
- className={`flex-shrink-0 p-1 rounded transition-colors ${
320
+ // h-6 w-6 = the 24x24 WCAG 2.2 (2.5.8) minimum target; the 14px glyph
321
+ // is unchanged, only the box grows. The sibling "+" button is h-8 w-8,
322
+ // so the card row already has the room.
323
+ className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded transition-colors ${
321
324
  isRendering
322
325
  ? "text-neutral-600 cursor-not-allowed"
323
326
  : "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
@@ -71,6 +71,7 @@ export interface DomEditActionsValue extends Pick<
71
71
  | "commitMutation"
72
72
  | "applyMarqueeSelection"
73
73
  | "handleUpdateKeyframeEase"
74
+ | "handleUpdateSegmentEase"
74
75
  | "handleSetAllKeyframeEases"
75
76
  > {}
76
77
 
@@ -200,6 +201,7 @@ export function DomEditProvider({
200
201
  applyMarqueeSelection,
201
202
  handleUpdateKeyframeEase,
202
203
  handleSetAllKeyframeEases,
204
+ handleUpdateSegmentEase,
203
205
  },
204
206
  children,
205
207
  }: {
@@ -281,6 +283,7 @@ export function DomEditProvider({
281
283
  commitMutation: stableCommitMutation,
282
284
  applyMarqueeSelection,
283
285
  handleUpdateKeyframeEase,
286
+ handleUpdateSegmentEase,
284
287
  handleSetAllKeyframeEases,
285
288
  }),
286
289
  [
@@ -349,6 +352,7 @@ export function DomEditProvider({
349
352
  stableCommitMutation,
350
353
  applyMarqueeSelection,
351
354
  handleUpdateKeyframeEase,
355
+ handleUpdateSegmentEase,
352
356
  handleSetAllKeyframeEases,
353
357
  ],
354
358
  );
@@ -12,7 +12,7 @@ import { usePlayerStore } from "../player/store/playerStore";
12
12
  import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyframes";
13
13
  import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
14
14
  import { roundTo3 } from "../utils/rounding";
15
- import { computeElementPercentage, idSelector } from "./gsapShared";
15
+ import { computeElementPercentage, idSelector, writeTargetSelector } from "./gsapShared";
16
16
  import { computeDraggedGsapPosition } from "./draggedGsapPosition";
17
17
  import type { RuntimeTweenChange } from "./gsapRuntimePatch";
18
18
  import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
@@ -44,6 +44,23 @@ export interface GsapDragCommitCallbacks {
44
44
  fetchAnimations?: () => Promise<GsapAnimation[]>;
45
45
  }
46
46
 
47
+ /**
48
+ * The target for a tween these helpers are about to CREATE. Callers derive
49
+ * `selector` with `selectorFromSelection`, which hands back a bare class for an
50
+ * id-less element: authoring that widens a one-element drag/resize/rotate into a
51
+ * write over every sibling sharing the class. Retargets of an EXISTING tween
52
+ * must NOT come through here (they keep `anim.targetSelector`, so a tween the
53
+ * author aimed at a group stays aimed at it).
54
+ *
55
+ * Null means no one-element form exists, and every caller drops the commit
56
+ * rather than falling back to `selector` (see writeTargetSelector): the drag
57
+ * reverts on the next reload, which is recoverable, where a `.group` write is
58
+ * not.
59
+ */
60
+ function newTweenTarget(selection: DomEditSelection): string | null {
61
+ return writeTargetSelector(selection);
62
+ }
63
+
47
64
  // Re-export for backward compatibility with existing imports.
48
65
  export function computeCurrentPercentage(
49
66
  selection: DomEditSelection,
@@ -65,17 +82,18 @@ export function parkPlayheadOnKeyframe(anim: GsapAnimation, pct: number): void {
65
82
 
66
83
  async function replaceKeyframedPositionHold(
67
84
  selection: DomEditSelection,
68
- selector: string,
69
85
  existingSet: GsapAnimation,
70
86
  properties: { x: number; y: number },
71
87
  commitMutation: GsapDragCommitCallbacks["commitMutation"],
72
88
  ): Promise<void> {
89
+ const target = newTweenTarget(selection);
90
+ if (!target) return;
73
91
  const persist = async (commit: GsapDragCommitCallbacks["commitMutation"]) => {
74
92
  await commit(
75
93
  selection,
76
94
  {
77
95
  type: "add",
78
- targetSelector: selector,
96
+ targetSelector: target,
79
97
  method: "set",
80
98
  position: 0,
81
99
  properties,
@@ -174,7 +192,6 @@ export async function commitStaticGsapPosition(
174
192
  // least one hold on disk, then delete the corrupt tween in one transaction.
175
193
  await replaceKeyframedPositionHold(
176
194
  selection,
177
- selector,
178
195
  existingSet,
179
196
  { x: newX, y: newY },
180
197
  callbacks.commitMutation,
@@ -199,11 +216,15 @@ export async function commitStaticGsapPosition(
199
216
  }
200
217
  // New static hold → a base `gsap.set` (off-timeline, no 0% keyframe marker), with
201
218
  // an instant patch so the first nudge shows immediately (no soft-reload flash).
219
+ // The patch reuses the WRITTEN target so the runtime moves exactly the element
220
+ // the source write names.
221
+ const target = newTweenTarget(selection);
222
+ if (!target) return;
202
223
  await callbacks.commitMutation(
203
224
  selection,
204
225
  {
205
226
  type: "add",
206
- targetSelector: selector,
227
+ targetSelector: target,
207
228
  method: "set",
208
229
  position: 0,
209
230
  properties: { x: newX, y: newY },
@@ -212,7 +233,10 @@ export async function commitStaticGsapPosition(
212
233
  {
213
234
  label: "Move layer",
214
235
  softReload: true,
215
- instantPatch: { selector, change: { kind: "global-set", props: { x: newX, y: newY } } },
236
+ instantPatch: {
237
+ selector: target,
238
+ change: { kind: "global-set", props: { x: newX, y: newY } },
239
+ },
216
240
  },
217
241
  );
218
242
  }
@@ -252,11 +276,13 @@ export async function commitStaticGsapRotation(
252
276
  return;
253
277
  }
254
278
  // New static hold → off-timeline `gsap.set` (no 0% keyframe marker) + instant patch.
279
+ const target = newTweenTarget(selection);
280
+ if (!target) return;
255
281
  await callbacks.commitMutation(
256
282
  selection,
257
283
  {
258
284
  type: "add",
259
- targetSelector: selector,
285
+ targetSelector: target,
260
286
  method: "set",
261
287
  position: 0,
262
288
  properties: { rotation: newRotation },
@@ -265,7 +291,10 @@ export async function commitStaticGsapRotation(
265
291
  {
266
292
  label: "Rotate layer",
267
293
  softReload: true,
268
- instantPatch: { selector, change: { kind: "global-set", props: { rotation: newRotation } } },
294
+ instantPatch: {
295
+ selector: target,
296
+ change: { kind: "global-set", props: { rotation: newRotation } },
297
+ },
269
298
  },
270
299
  );
271
300
  }
@@ -301,11 +330,13 @@ export async function commitStaticGsapSize(
301
330
  );
302
331
  return;
303
332
  }
333
+ const target = newTweenTarget(selection);
334
+ if (!target) return;
304
335
  await callbacks.commitMutation(
305
336
  selection,
306
337
  {
307
338
  type: "add",
308
- targetSelector: selector,
339
+ targetSelector: target,
309
340
  method: "set",
310
341
  position: 0,
311
342
  properties: { width, height },
@@ -389,11 +420,13 @@ export async function commitKeyframedSizeFromResize(
389
420
  // transport applies both in one ordered batch; a plain commit fallback keeps the
390
421
  // same recoverable ordering. Only the transaction's result triggers the reload.
391
422
  const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
423
+ const target = newTweenTarget(selection);
424
+ if (!target) return false;
392
425
  await callbacks.commitMutation(
393
426
  selection,
394
427
  {
395
428
  type: "add-with-keyframes",
396
- targetSelector: selector,
429
+ targetSelector: target,
397
430
  position: roundTo3(ts),
398
431
  duration: roundTo3(td),
399
432
  keyframes,
@@ -1,6 +1,6 @@
1
1
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
2
2
  import type { RuntimeTweenChange, SetPatchProps } from "./gsapRuntimePatch";
3
- import { isInstantHold } from "./gsapShared";
3
+ import { isInstantHold, tweenTargetsElement } from "./gsapShared";
4
4
 
5
5
  /** The shape of an `update-property` mutation a static-set nudge POSTs. */
6
6
  interface UpdatePropertyMutation {
@@ -34,12 +34,13 @@ export function setPatchFromUpdateProperty(
34
34
  function findPositionSetAnimation(
35
35
  animations: GsapAnimation[],
36
36
  selector: string,
37
+ element?: Element | null,
37
38
  ): GsapAnimation | null {
38
39
  return (
39
40
  animations.find(
40
41
  (a) =>
41
42
  a.method === "set" &&
42
- a.targetSelector === selector &&
43
+ tweenTargetsElement(a.targetSelector, selector, element) &&
43
44
  ("x" in a.properties || "y" in a.properties),
44
45
  ) ?? null
45
46
  );
@@ -61,12 +62,16 @@ function findPositionSetAnimation(
61
62
  export function findExistingPositionWrite(
62
63
  animations: GsapAnimation[],
63
64
  selector: string,
65
+ element?: Element | null,
64
66
  ): GsapAnimation | null {
65
- const set = findPositionSetAnimation(animations, selector);
67
+ const set = findPositionSetAnimation(animations, selector, element);
66
68
  if (set) return set;
67
69
  return (
68
70
  animations.find(
69
- (a) => a.targetSelector === selector && a.propertyGroup === "position" && isInstantHold(a),
71
+ (a) =>
72
+ tweenTargetsElement(a.targetSelector, selector, element) &&
73
+ a.propertyGroup === "position" &&
74
+ isInstantHold(a),
70
75
  ) ?? null
71
76
  );
72
77
  }
@@ -74,10 +79,14 @@ export function findExistingPositionWrite(
74
79
  export function findRotationSetAnimation(
75
80
  animations: GsapAnimation[],
76
81
  selector: string,
82
+ element?: Element | null,
77
83
  ): GsapAnimation | null {
78
84
  return (
79
85
  animations.find(
80
- (a) => isInstantHold(a) && a.targetSelector === selector && "rotation" in a.properties,
86
+ (a) =>
87
+ isInstantHold(a) &&
88
+ tweenTargetsElement(a.targetSelector, selector, element) &&
89
+ "rotation" in a.properties,
81
90
  ) ?? null
82
91
  );
83
92
  }
@@ -85,12 +94,13 @@ export function findRotationSetAnimation(
85
94
  export function findSizeSetAnimation(
86
95
  animations: GsapAnimation[],
87
96
  selector: string,
97
+ element?: Element | null,
88
98
  ): GsapAnimation | null {
89
99
  return (
90
100
  animations.find(
91
101
  (a) =>
92
102
  isInstantHold(a) &&
93
- a.targetSelector === selector &&
103
+ tweenTargetsElement(a.targetSelector, selector, element) &&
94
104
  ("width" in a.properties || "height" in a.properties),
95
105
  ) ?? null
96
106
  );