@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
@@ -22,7 +22,6 @@ const mocks = vi.hoisted(() => ({
22
22
 
23
23
  vi.mock("./gsapResizeIntercept", () => ({ tryGsapResizeIntercept: mocks.resize }));
24
24
  vi.mock("./gsapRuntimeBridge", () => ({
25
- POSITION_CHANNELS: ["x", "y"],
26
25
  tryGsapDragIntercept: mocks.drag,
27
26
  tryGsapRotationIntercept: vi.fn(),
28
27
  }));
@@ -47,6 +46,11 @@ afterEach(() => {
47
46
  vi.clearAllMocks();
48
47
  });
49
48
 
49
+ /** An element GSAP already renders the transform of, so moves take the GSAP route. */
50
+ function gsapPositioned(tag: string): HTMLElement {
51
+ return Object.assign(document.createElement(tag), { _gsap: { renderTransform: () => {} } });
52
+ }
53
+
50
54
  function mountResizeHandler(
51
55
  animations: GsapAnimation[],
52
56
  targetAnimations: GsapAnimation[] = animations,
@@ -150,6 +154,28 @@ function mountGroupHandler({
150
154
  };
151
155
  }
152
156
 
157
+ describe("useGsapAwareEditing moves of an element GSAP does not position", () => {
158
+ it("saves the move on the element itself, with no GSAP write and no animation read", async () => {
159
+ const save = vi.fn().mockResolvedValue(undefined);
160
+ const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
161
+ const gsapCommitMutation = vi.fn();
162
+ const makeFetchFallback = vi.fn();
163
+ const { pathOffsetCommit, root } = mountGroupHandler({
164
+ gsapCommitMutation,
165
+ makeFetchFallback,
166
+ stageElementPositionOffset,
167
+ });
168
+ const box = { element: document.createElement("div"), id: "box", selector: "#box" };
169
+ await act(() => pathOffsetCommit(box as unknown as DomEditSelection, { x: 130.25, y: 90 }));
170
+ expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 });
171
+ expect(save).toHaveBeenCalledTimes(1);
172
+ expect(gsapCommitMutation).not.toHaveBeenCalled();
173
+ expect(makeFetchFallback).not.toHaveBeenCalled();
174
+ expect(mocks.drag).not.toHaveBeenCalled();
175
+ act(() => root.unmount());
176
+ });
177
+ });
178
+
153
179
  describe("useGsapAwareEditing shared-tween moves", () => {
154
180
  it("saves a single drag through the element's own offset", async () => {
155
181
  mocks.drag.mockResolvedValue({ status: "element-offset" });
@@ -161,7 +187,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
161
187
  makeFetchFallback: () => vi.fn().mockResolvedValue([]),
162
188
  stageElementPositionOffset,
163
189
  });
164
- const word = { element: document.createElement("span"), hfId: "w0", selector: ".w" };
190
+ const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
165
191
  await act(() => pathOffsetCommit(word as unknown as DomEditSelection, { x: 40, y: 20 }));
166
192
  expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 });
167
193
  expect(save).toHaveBeenCalledTimes(1);
@@ -169,7 +195,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
169
195
  act(() => root.unmount());
170
196
  });
171
197
 
172
- it("saves a group member that shares a tween through its own offset, under the group key", async () => {
198
+ it("saves shared-tween and GSAP-free group members on themselves, under the group key", async () => {
173
199
  mocks.drag.mockImplementation(async (selection, _next, _a, _i, commit, _f, options) => {
174
200
  if (selection.hfId === "w0") return { status: "element-offset" };
175
201
  if (!options?.preflightOnly) await commit(selection, { type: "move" }, { label: "Move" });
@@ -182,19 +208,20 @@ describe("useGsapAwareEditing shared-tween moves", () => {
182
208
  makeFetchFallback: () => vi.fn().mockResolvedValue([]),
183
209
  stageElementPositionOffset,
184
210
  });
185
- const word = { element: document.createElement("span"), hfId: "w0", selector: ".w" };
186
- const box = { element: document.createElement("div"), id: "box", selector: "#box" };
211
+ const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
212
+ const box = { element: gsapPositioned("div"), id: "box", selector: "#box" };
213
+ const plain = { element: document.createElement("div"), id: "plain", selector: "#plain" };
187
214
  await act(() =>
188
215
  groupCommit([
189
216
  { selection: word, next: { x: 40, y: 20 } },
190
217
  { selection: box, next: { x: 40, y: 20 } },
218
+ { selection: plain, next: { x: 40, y: 20 } },
191
219
  ] as unknown as DomEditGroupPathOffsetCommit[]),
192
220
  );
193
- expect(stageElementPositionOffset).toHaveBeenCalledWith(
194
- word,
195
- { x: 40, y: 20 },
196
- expect.stringMatching(/^group-drag:\d+$/),
197
- );
221
+ const groupKey = expect.stringMatching(/^group-drag:\d+$/);
222
+ expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 }, groupKey);
223
+ expect(stageElementPositionOffset).toHaveBeenCalledWith(plain, { x: 40, y: 20 }, groupKey);
224
+ expect(mocks.drag.mock.calls.some((call) => call[0] === plain)).toBe(false);
198
225
  act(() => root.unmount());
199
226
  });
200
227
  });
@@ -207,7 +234,7 @@ describe("useGsapAwareEditing anchored resize", () => {
207
234
  const h = mountResizeHandler([humanAnimation], [targetAnimation]);
208
235
  const target = {
209
236
  ...h.selection,
210
- element: document.createElement("div"),
237
+ element: gsapPositioned("div"),
211
238
  id: "agent-target",
212
239
  selector: "#agent-target",
213
240
  } as DomEditSelection;
@@ -447,11 +474,11 @@ describe("useGsapAwareEditing anchored resize", () => {
447
474
  });
448
475
  const updates = [
449
476
  {
450
- selection: { element: document.createElement("div"), id: "a", selector: "#a" },
477
+ selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
451
478
  next: { x: 10, y: 10 },
452
479
  },
453
480
  {
454
- selection: { element: document.createElement("div"), id: "b", selector: "#b" },
481
+ selection: { element: gsapPositioned("div"), id: "b", selector: "#b" },
455
482
  next: { x: 10, y: 10 },
456
483
  },
457
484
  ] as unknown as DomEditGroupPathOffsetCommit[];
@@ -485,12 +512,12 @@ describe("useGsapAwareEditing anchored resize", () => {
485
512
  });
486
513
  const updates = [
487
514
  {
488
- selection: { element: document.createElement("div"), id: "ok", selector: "#ok" },
515
+ selection: { element: gsapPositioned("div"), id: "ok", selector: "#ok" },
489
516
  next: { x: 10, y: 10 },
490
517
  },
491
518
  {
492
519
  selection: {
493
- element: document.createElement("div"),
520
+ element: gsapPositioned("div"),
494
521
  id: "blocked",
495
522
  selector: "#blocked",
496
523
  },
@@ -526,7 +553,7 @@ describe("useGsapAwareEditing anchored resize", () => {
526
553
  });
527
554
  const updates = [
528
555
  {
529
- selection: { element: document.createElement("div"), id: "a", selector: "#a" },
556
+ selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
530
557
  next: { x: 10, y: 10 },
531
558
  },
532
559
  ] as unknown as DomEditGroupPathOffsetCommit[];
@@ -551,11 +578,11 @@ describe("useGsapAwareEditing anchored resize", () => {
551
578
  });
552
579
  const updates = [
553
580
  {
554
- selection: { element: document.createElement("div"), id: "a", selector: "#a" },
581
+ selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
555
582
  next: { x: 10, y: 10 },
556
583
  },
557
584
  {
558
- selection: { element: document.createElement("div"), id: "b", selector: "#b" },
585
+ selection: { element: gsapPositioned("div"), id: "b", selector: "#b" },
559
586
  next: { x: 20, y: 20 },
560
587
  },
561
588
  ] as unknown as DomEditGroupPathOffsetCommit[];
@@ -605,6 +632,7 @@ describe("useGsapAwareEditing anchored resize", () => {
605
632
  */
606
633
  it("applies the anchor when the resize leaves the drop point to the caller", async () => {
607
634
  mocks.resize.mockResolvedValue({ status: "persisted" });
635
+ mocks.drag.mockResolvedValue({ status: "persisted" });
608
636
  const scale = { propertyGroup: "scale" } as GsapAnimation;
609
637
  const h = mountResizeHandler([scale]);
610
638
  await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
@@ -10,11 +10,7 @@
10
10
  import { useCallback } from "react";
11
11
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
12
12
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
13
- import {
14
- POSITION_CHANNELS,
15
- tryGsapDragIntercept,
16
- tryGsapRotationIntercept,
17
- } from "./gsapRuntimeBridge";
13
+ import { tryGsapDragIntercept, tryGsapRotationIntercept } from "./gsapRuntimeBridge";
18
14
  import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
19
15
  import { computeDraggedGsapPosition } from "./draggedGsapPosition";
20
16
  import { readGsapPositionFromIframe } from "./gsapPositionDetection";
@@ -33,7 +29,11 @@ import { setElementGsapPosition } from "../utils/elementGsap";
33
29
  import { logResize, logResizeSettle } from "../utils/resizeDebug";
34
30
  import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
35
31
  import { runGestureTransaction } from "./gestureTransaction";
36
- import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes";
32
+ import {
33
+ gsapWritesPosition,
34
+ hasNonHoldTweenForElement,
35
+ POSITION_CHANNELS,
36
+ } from "./gsapRuntimeKeyframes";
37
37
  import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
38
38
  import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
39
39
  import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
@@ -153,6 +153,8 @@ export function useGsapAwareEditing({
153
153
  next: { x: number; y: number },
154
154
  modifiers?: { altKey?: boolean },
155
155
  ) => {
156
+ if (!gsapWritesPosition(selection.element))
157
+ return stageElementPositionOffset(selection, next).save();
156
158
  if (gsapCommitMutation) {
157
159
  try {
158
160
  const ownedAnimations = getGsapAnimationsForSelection(selection);
@@ -185,11 +187,8 @@ export function useGsapAwareEditing({
185
187
  ],
186
188
  );
187
189
 
188
- // Multi-select (group) drag: route EACH element through the SAME GSAP intercept as
189
- // a single drag, so every position is written as GSAP code (tl.set / keyframes /
190
- // gsap.set) — NEVER the deprecated `--hf-studio-offset` CSS var, and GSAP-animated
191
- // elements are no longer blocked in a group. No CSS fallback: with no GSAP
192
- // composition there's nothing to write (a no-op, exactly like the single-drag path).
190
+ // Multi-select (group) drag: each member takes the single drag's writer, so a member GSAP
191
+ // does not position is saved on itself and the rest go through the GSAP intercept.
193
192
  const handleGsapAwareGroupPathOffsetCommit = useCallback(
194
193
  async (updates: DomEditGroupPathOffsetCommit[]) => {
195
194
  if (!gsapCommitMutation || updates.length === 0) return;
@@ -247,6 +246,7 @@ export function useGsapAwareEditing({
247
246
  // turns N sequential round trips into one.
248
247
  const preflightResults = await Promise.allSettled(
249
248
  updates.map(async ({ selection }) => {
249
+ if (!gsapWritesPosition(selection.element)) return void offsetMembers.add(selection);
250
250
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
251
251
  preflightAnimations.set(selection, animations);
252
252
  const outcome = await tryGsapDragIntercept(