@hyperframes/studio 0.8.97 → 0.8.99
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.
- package/dist/assets/{hyperframes-player-Biu5_W0l.js → hyperframes-player-Rml34FRW.js} +1 -1
- package/dist/assets/{index-BdV5T-QI.js → index-CVDArg3m.js} +188 -188
- package/dist/assets/{index-B5Z-1RW9.js → index-C_fi-jTe.js} +1 -1
- package/dist/assets/{index-CWuozokW.js → index-D9YzDuBx.js} +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.html +1 -1
- package/dist/index.js +758 -607
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
- package/src/components/EditorShell.tsx +7 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +34 -66
- package/src/components/editor/DomEditCropHandles.tsx +30 -62
- package/src/components/editor/DomEditOverlay.test.ts +2 -2
- package/src/components/editor/DomEditOverlay.tsx +0 -27
- package/src/components/editor/DomEditSelectionChrome.test.tsx +0 -5
- package/src/components/editor/DomEditSelectionChrome.tsx +0 -6
- package/src/components/editor/clickAfterPreventedPress.test.tsx +122 -0
- package/src/components/editor/cropResize.test.ts +129 -0
- package/src/components/editor/cropResize.ts +143 -0
- package/src/components/editor/domEditOverlayCrop.ts +65 -15
- package/src/components/editor/domEditOverlayCropLift.test.ts +44 -0
- package/src/components/editor/domEditOverlayGestures.ts +2 -5
- package/src/components/editor/elementPositionOffset.ts +5 -4
- package/src/components/editor/manualEdits.test.ts +22 -8
- package/src/components/editor/manualEditsDom.ts +27 -11
- package/src/components/editor/manualEditsParsing.ts +0 -4
- package/src/components/editor/manualEditsSnapshot.ts +3 -0
- package/src/components/editor/manualOffsetDrag.test.ts +14 -0
- package/src/components/editor/propertyPanelFont.test.tsx +48 -5
- package/src/components/editor/propertyPanelFont.tsx +2 -1
- package/src/components/editor/shiftDragAxisLock.test.tsx +0 -2
- package/src/components/nle/TimelinePane.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +8 -0
- package/src/hooks/draggedGsapPosition.ts +4 -2
- package/src/hooks/gsapDragCommit.ts +6 -6
- package/src/hooks/gsapEditOutcome.ts +6 -0
- package/src/hooks/gsapResizeIntercept.ts +5 -5
- package/src/hooks/gsapRuntimeBridge.test.ts +11 -0
- package/src/hooks/gsapRuntimeBridge.ts +2 -1
- package/src/hooks/gsapRuntimeKeyframes.test.ts +7 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +34 -21
- package/src/hooks/timelineTimingSync.ts +40 -4
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +40 -1
- package/src/hooks/useAnimatedPropertyCommit.ts +11 -13
- package/src/hooks/useCommitPreflightCapabilities.ts +3 -11
- package/src/hooks/useCropOverlay.ts +8 -2
- package/src/hooks/useDomEditCommits.test.tsx +6 -1
- package/src/hooks/useDomEditCommits.ts +1 -0
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -2
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomGeometryCommit.ts +1 -0
- package/src/hooks/useDomGeometryCommits.test.tsx +112 -0
- package/src/hooks/useDomGeometryCommits.ts +5 -0
- package/src/hooks/useElementLifecycleOps.multiDelete.test.tsx +3 -1
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +44 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +11 -35
- package/src/hooks/useGsapAwareEditing.test.tsx +101 -5
- package/src/hooks/useGsapAwareEditing.ts +37 -23
- package/src/hooks/useGsapTweenCache.element.test.tsx +23 -1
- package/src/hooks/useGsapTweenCache.ts +0 -24
- package/src/hooks/useTimelineEditing.test.tsx +190 -135
- package/src/hooks/useTimelineEditing.ts +13 -13
- package/src/hooks/useTimelineGroupEditing.ts +13 -9
- package/src/index.ts +4 -1
- package/src/player/components/ClipContextMenu.test.tsx +182 -0
- package/src/player/components/ClipContextMenu.tsx +52 -10
- package/src/player/components/TimelineOverlays.test.ts +30 -7
- package/src/player/components/TimelineOverlays.tsx +7 -1
- package/src/player/components/TimelineProvider.tsx +1 -0
- package/src/player/components/TimelineTypes.ts +10 -0
- package/src/player/components/menuKeyboardNav.ts +12 -5
- package/src/player/components/useTimelineProviderState.tsx +2 -0
- package/src/player/components/useTimelineVirtualRows.test.tsx +30 -1
- package/src/utils/rounding.ts +5 -0
- package/src/utils/sdkEditTransaction.ts +10 -3
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
7
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
8
|
-
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
8
|
+
import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
9
|
+
import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
|
|
10
|
+
import { applyStudioBoxSizeDraft } from "../components/editor/manualEdits";
|
|
9
11
|
|
|
10
12
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
13
|
|
|
@@ -56,6 +58,7 @@ function mountResizeHandler(
|
|
|
56
58
|
const anchorRollback = vi.fn();
|
|
57
59
|
const elementOffset = vi.fn(() => ({ save: anchorSave, rollback: anchorRollback }));
|
|
58
60
|
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
61
|
+
const commitPatch = vi.fn().mockResolvedValue(undefined);
|
|
59
62
|
let resize:
|
|
60
63
|
| ((
|
|
61
64
|
selection: DomEditSelection,
|
|
@@ -64,8 +67,9 @@ function mountResizeHandler(
|
|
|
64
67
|
restore?: () => void,
|
|
65
68
|
) => Promise<void>)
|
|
66
69
|
| null = null;
|
|
70
|
+
let property: ReturnType<typeof useGsapAwareEditing>["commitAnimatedProperty"] | null = null;
|
|
67
71
|
function Harness() {
|
|
68
|
-
|
|
72
|
+
const editing = useGsapAwareEditing({
|
|
69
73
|
domEditSelection: selection,
|
|
70
74
|
selectedGsapAnimations: animations,
|
|
71
75
|
gsapCommitMutation: commitMutation,
|
|
@@ -76,11 +80,14 @@ function mountResizeHandler(
|
|
|
76
80
|
trackGsapInteractionFailure: vi.fn(),
|
|
77
81
|
stageElementPositionOffset: elementOffset,
|
|
78
82
|
handleDomBoxSizeCommit: fallback,
|
|
83
|
+
commitPositionPatchToHtml: commitPatch,
|
|
79
84
|
addGsapAnimation: vi.fn(),
|
|
80
85
|
convertToKeyframes: vi.fn(),
|
|
81
86
|
setArcPath: vi.fn(),
|
|
82
87
|
updateArcSegment: vi.fn(),
|
|
83
|
-
})
|
|
88
|
+
});
|
|
89
|
+
resize = editing.handleGsapAwareBoxSizeCommit;
|
|
90
|
+
property = editing.commitAnimatedProperty;
|
|
84
91
|
return null;
|
|
85
92
|
}
|
|
86
93
|
const root = mountReactHarness(<Harness />);
|
|
@@ -91,7 +98,9 @@ function mountResizeHandler(
|
|
|
91
98
|
anchorSave,
|
|
92
99
|
anchorRollback,
|
|
93
100
|
commitMutation,
|
|
101
|
+
commitPatch,
|
|
94
102
|
resize: resize!,
|
|
103
|
+
property: property!,
|
|
95
104
|
root,
|
|
96
105
|
};
|
|
97
106
|
}
|
|
@@ -122,6 +131,7 @@ function mountGroupHandler({
|
|
|
122
131
|
trackGsapInteractionFailure,
|
|
123
132
|
stageElementPositionOffset,
|
|
124
133
|
handleDomBoxSizeCommit: vi.fn(),
|
|
134
|
+
commitPositionPatchToHtml: vi.fn(),
|
|
125
135
|
addGsapAnimation: vi.fn(),
|
|
126
136
|
convertToKeyframes: vi.fn(),
|
|
127
137
|
setArcPath: vi.fn(),
|
|
@@ -257,6 +267,92 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
257
267
|
act(() => h.root.unmount());
|
|
258
268
|
});
|
|
259
269
|
|
|
270
|
+
function mountCropped() {
|
|
271
|
+
const h = mountResizeHandler([]);
|
|
272
|
+
const el = withInlineLayoutBox(h.selection.element);
|
|
273
|
+
el.id = "clip";
|
|
274
|
+
el.style.cssText = "width: 300px; height: 200px; clip-path: inset(0px 60px 0px 0px)";
|
|
275
|
+
document.body.append(el);
|
|
276
|
+
const rect = { left: 0, top: 0, width: 300, height: 200, editScaleX: 1, editScaleY: 1 };
|
|
277
|
+
const crop = mountReactHarness(
|
|
278
|
+
<DomEditCropHandles selection={h.selection} overlayRect={rect} onStyleCommit={vi.fn()} />,
|
|
279
|
+
);
|
|
280
|
+
return { ...h, el, crop };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resizeLandsWhen(where: "intercept" | "dispatch", h: { commitMutation: Mock }) {
|
|
284
|
+
const land = (el: HTMLElement) => {
|
|
285
|
+
el.style.width = "450px";
|
|
286
|
+
el.style.height = "300px";
|
|
287
|
+
};
|
|
288
|
+
mocks.resize.mockImplementation(async (selection, _next, _a, _i, commit) => {
|
|
289
|
+
if (where === "intercept") land(selection.element);
|
|
290
|
+
await commit(selection, { type: "size" }, { label: "Resize", softReload: true });
|
|
291
|
+
return { status: "persisted" };
|
|
292
|
+
});
|
|
293
|
+
if (where === "dispatch") h.commitMutation.mockImplementation(async (s) => land(s.element));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const scaled = "inset(0px 90px 0px 0px)";
|
|
297
|
+
|
|
298
|
+
it("saves the crop scaled with a size that lands only when saved (the W/H fields)", async () => {
|
|
299
|
+
const h = mountCropped();
|
|
300
|
+
resizeLandsWhen("dispatch", h);
|
|
301
|
+
await act(() => h.resize(h.selection, { width: 450, height: 300 }));
|
|
302
|
+
|
|
303
|
+
expect(h.commitPatch).toHaveBeenCalledWith(
|
|
304
|
+
h.selection,
|
|
305
|
+
[{ type: "inline-style", property: "clip-path", value: scaled }],
|
|
306
|
+
expect.objectContaining({ coalesceKey: h.commitMutation.mock.calls[0]![2].coalesceKey }),
|
|
307
|
+
);
|
|
308
|
+
act(() => h.crop.unmount());
|
|
309
|
+
expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
|
|
310
|
+
act(() => h.root.unmount());
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("scales from the box before a draft its caller applied (the agent tool)", async () => {
|
|
314
|
+
const h = mountCropped();
|
|
315
|
+
resizeLandsWhen("intercept", h);
|
|
316
|
+
applyStudioBoxSizeDraft(h.el, { width: 450, height: 300 });
|
|
317
|
+
await act(() => h.resize(h.selection, { width: 450, height: 300 }));
|
|
318
|
+
|
|
319
|
+
expect(h.commitPatch.mock.calls[0]![1]).toEqual([
|
|
320
|
+
{ type: "inline-style", property: "clip-path", value: scaled },
|
|
321
|
+
]);
|
|
322
|
+
act(() => h.crop.unmount());
|
|
323
|
+
act(() => h.root.unmount());
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("a W field on an animated element saves the scaled crop in the same undo step", async () => {
|
|
327
|
+
const h = mountCropped();
|
|
328
|
+
mocks.commitAnimatedProperties.mockImplementation(async (selection, _props, keyed) => {
|
|
329
|
+
await (keyed ?? h.commitMutation)(selection, { type: "set" }, { label: "Edit width" });
|
|
330
|
+
selection.element.style.width = "450px";
|
|
331
|
+
});
|
|
332
|
+
await act(() => h.property(h.selection, "width", 450));
|
|
333
|
+
|
|
334
|
+
const key = h.commitMutation.mock.calls[0]![2].coalesceKey;
|
|
335
|
+
expect(key).toBeTruthy();
|
|
336
|
+
expect(h.commitPatch).toHaveBeenCalledWith(
|
|
337
|
+
h.selection,
|
|
338
|
+
[{ type: "inline-style", property: "clip-path", value: "inset(0px 90px 0px 0px)" }],
|
|
339
|
+
expect.objectContaining({ coalesceKey: key }),
|
|
340
|
+
);
|
|
341
|
+
act(() => h.crop.unmount());
|
|
342
|
+
act(() => h.root.unmount());
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("shows the scaled crop when deselected before the save lands", async () => {
|
|
346
|
+
const h = mountCropped();
|
|
347
|
+
resizeLandsWhen("intercept", h);
|
|
348
|
+
const saved = h.resize(h.selection, { width: 450, height: 300 });
|
|
349
|
+
act(() => h.crop.unmount());
|
|
350
|
+
await act(() => saved);
|
|
351
|
+
|
|
352
|
+
expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
|
|
353
|
+
act(() => h.root.unmount());
|
|
354
|
+
});
|
|
355
|
+
|
|
260
356
|
it("does not keep the anchor move when the size save fails", async () => {
|
|
261
357
|
resizeWritesSize();
|
|
262
358
|
const h = mountResizeHandler([]);
|
|
@@ -288,7 +384,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
288
384
|
commit = h.resize(h.selection, { width: 300, height: 200 }, { x: -50.2, y: -25.6 });
|
|
289
385
|
});
|
|
290
386
|
|
|
291
|
-
expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70,
|
|
387
|
+
expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70.2, 54.6);
|
|
292
388
|
expect(mocks.setPosition.mock.invocationCallOrder[0]).toBeLessThan(
|
|
293
389
|
mocks.resize.mock.invocationCallOrder[0]!,
|
|
294
390
|
);
|
|
@@ -34,14 +34,14 @@ import { logResize, logResizeSettle } from "../utils/resizeDebug";
|
|
|
34
34
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
35
35
|
import { runGestureTransaction } from "./gestureTransaction";
|
|
36
36
|
import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes";
|
|
37
|
-
import { assertGsapEditPersisted,
|
|
37
|
+
import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
|
|
38
38
|
import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
39
|
+
import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
40
|
+
import {
|
|
41
|
+
prepareCropResize,
|
|
42
|
+
saveCropResize,
|
|
43
|
+
writeSizeWithCrop,
|
|
44
|
+
} from "../components/editor/cropResize";
|
|
45
45
|
|
|
46
46
|
// Distinct coalesceKey per group drag so consecutive group drags don't fold
|
|
47
47
|
// into one another's undo entry (module-local counter, not Date.now()).
|
|
@@ -87,6 +87,7 @@ export interface UseGsapAwareEditingParams {
|
|
|
87
87
|
next: { width: number; height: number },
|
|
88
88
|
offset?: { x: number; y: number },
|
|
89
89
|
) => Promise<void>;
|
|
90
|
+
commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
|
|
90
91
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
91
92
|
addGsapAnimation: (
|
|
92
93
|
sel: DomEditSelection,
|
|
@@ -130,6 +131,7 @@ export function useGsapAwareEditing({
|
|
|
130
131
|
trackGsapInteractionFailure,
|
|
131
132
|
stageElementPositionOffset,
|
|
132
133
|
handleDomBoxSizeCommit,
|
|
134
|
+
commitPositionPatchToHtml,
|
|
133
135
|
addGsapAnimation,
|
|
134
136
|
convertToKeyframes,
|
|
135
137
|
setArcPath,
|
|
@@ -353,6 +355,8 @@ export function useGsapAwareEditing({
|
|
|
353
355
|
animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
|
|
354
356
|
});
|
|
355
357
|
let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
|
|
358
|
+
const stageCrop = prepareCropResize(selection.element);
|
|
359
|
+
let cropUndoKey: string | null = null;
|
|
356
360
|
return runGestureTransaction({
|
|
357
361
|
element: selection.element,
|
|
358
362
|
label: "Resize layer",
|
|
@@ -381,6 +385,7 @@ export function useGsapAwareEditing({
|
|
|
381
385
|
makeFetchFallback(selection),
|
|
382
386
|
);
|
|
383
387
|
assertGsapEditPersisted(outcome);
|
|
388
|
+
cropUndoKey = coalesceKey;
|
|
384
389
|
// What the resize actually did, not what its animations suggest
|
|
385
390
|
// it would do. An element whose scale is an instant hold has a
|
|
386
391
|
// scale-group tween and still commits width/height, so guessing
|
|
@@ -424,7 +429,13 @@ export function useGsapAwareEditing({
|
|
|
424
429
|
logResizeSettle(selection.element, "dom-route");
|
|
425
430
|
await handleDomBoxSizeCommit(selection, next, offset);
|
|
426
431
|
},
|
|
427
|
-
afterBufferedCommitsSaved: () =>
|
|
432
|
+
afterBufferedCommitsSaved: async () => {
|
|
433
|
+
await anchorMove?.save();
|
|
434
|
+
// Only now is the size live for every caller, drag or not.
|
|
435
|
+
if (cropUndoKey) {
|
|
436
|
+
await saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
428
439
|
restore: () => {
|
|
429
440
|
anchorMove?.rollback();
|
|
430
441
|
restore();
|
|
@@ -434,6 +445,7 @@ export function useGsapAwareEditing({
|
|
|
434
445
|
},
|
|
435
446
|
[
|
|
436
447
|
handleDomBoxSizeCommit,
|
|
448
|
+
commitPositionPatchToHtml,
|
|
437
449
|
stageElementPositionOffset,
|
|
438
450
|
gsapCommitMutation,
|
|
439
451
|
previewIframeRef,
|
|
@@ -481,10 +493,7 @@ export function useGsapAwareEditing({
|
|
|
481
493
|
|
|
482
494
|
// ── Animated property commit ──
|
|
483
495
|
|
|
484
|
-
const {
|
|
485
|
-
commitAnimatedProperty: commitAnimatedPropertyRaw,
|
|
486
|
-
commitAnimatedProperties: commitAnimatedPropertiesRaw,
|
|
487
|
-
} = useAnimatedPropertyCommit({
|
|
496
|
+
const { commitAnimatedProperties: commitAnimatedPropertiesRaw } = useAnimatedPropertyCommit({
|
|
488
497
|
selectedGsapAnimations,
|
|
489
498
|
gsapCommitMutation,
|
|
490
499
|
addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
|
|
@@ -496,25 +505,30 @@ export function useGsapAwareEditing({
|
|
|
496
505
|
const commitAnimatedProperties = useCallback(
|
|
497
506
|
async (selection: DomEditSelection, properties: Record<string, number | string>) => {
|
|
498
507
|
try {
|
|
499
|
-
await
|
|
508
|
+
await writeSizeWithCrop(
|
|
509
|
+
selection,
|
|
510
|
+
properties,
|
|
511
|
+
gsapCommitMutation,
|
|
512
|
+
commitPositionPatchToHtml,
|
|
513
|
+
(keyed) => commitAnimatedPropertiesRaw(selection, properties, keyed),
|
|
514
|
+
);
|
|
500
515
|
} catch (error) {
|
|
501
516
|
trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
|
|
502
517
|
throw error;
|
|
503
518
|
}
|
|
504
519
|
},
|
|
505
|
-
[
|
|
520
|
+
[
|
|
521
|
+
commitAnimatedPropertiesRaw,
|
|
522
|
+
commitPositionPatchToHtml,
|
|
523
|
+
gsapCommitMutation,
|
|
524
|
+
trackGsapInteractionFailure,
|
|
525
|
+
],
|
|
506
526
|
);
|
|
507
527
|
|
|
508
528
|
const commitAnimatedProperty = useCallback(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
} catch (error) {
|
|
513
|
-
trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
|
|
514
|
-
throw error;
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
[commitAnimatedPropertyRaw, trackGsapInteractionFailure],
|
|
529
|
+
(selection: DomEditSelection, property: string, value: number | string) =>
|
|
530
|
+
commitAnimatedProperties(selection, { [property]: value }),
|
|
531
|
+
[commitAnimatedProperties],
|
|
518
532
|
);
|
|
519
533
|
|
|
520
534
|
// ── Arc path wrappers ──
|
|
@@ -16,9 +16,10 @@ const dotTween = {
|
|
|
16
16
|
} as GsapAnimation;
|
|
17
17
|
vi.mock("./keyframeCacheAstLoad", async (importOriginal) => ({
|
|
18
18
|
...(await importOriginal<typeof import("./keyframeCacheAstLoad")>()),
|
|
19
|
-
fetchParsedAnimations: async () => ({ animations: [dotTween] }),
|
|
19
|
+
fetchParsedAnimations: vi.fn(async () => ({ animations: [dotTween] })),
|
|
20
20
|
}));
|
|
21
21
|
const { useGsapAnimationsForElement } = await import("./useGsapTweenCache");
|
|
22
|
+
const { fetchParsedAnimations } = await import("./keyframeCacheAstLoad");
|
|
22
23
|
|
|
23
24
|
it("attributes a class tween by the selected element, not an earlier same-id copy in a sub-composition", async () => {
|
|
24
25
|
document.body.innerHTML =
|
|
@@ -37,3 +38,24 @@ it("attributes a class tween by the selected element, not an earlier same-id cop
|
|
|
37
38
|
expect(animations.map((animation) => animation.id)).toEqual(["dots"]);
|
|
38
39
|
act(() => root.unmount());
|
|
39
40
|
});
|
|
41
|
+
|
|
42
|
+
it("reads a file with no tweens once: the parse endpoint answers from the file on disk", async () => {
|
|
43
|
+
vi.useFakeTimers();
|
|
44
|
+
vi.mocked(fetchParsedAnimations)
|
|
45
|
+
.mockClear()
|
|
46
|
+
.mockResolvedValue({ animations: [] } as never);
|
|
47
|
+
const selection = { id: "card", sourceFile: "index.html" } as DomEditSelection;
|
|
48
|
+
function Harness() {
|
|
49
|
+
useGsapAnimationsForElement("p", "index.html", selection, 0);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const root = mountReactHarness(<Harness />);
|
|
53
|
+
try {
|
|
54
|
+
await act(async () => {});
|
|
55
|
+
await act(async () => vi.advanceTimersByTime(2000));
|
|
56
|
+
expect(fetchParsedAnimations).toHaveBeenCalledTimes(1);
|
|
57
|
+
} finally {
|
|
58
|
+
act(() => root.unmount());
|
|
59
|
+
vi.useRealTimers();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -41,7 +41,6 @@ export function useGsapAnimationsForElement(
|
|
|
41
41
|
const [multipleTimelines, setMultipleTimelines] = useState(false);
|
|
42
42
|
const [unsupportedTimelinePattern, setUnsupportedTimelinePattern] = useState(false);
|
|
43
43
|
const lastFetchKeyRef = useRef("");
|
|
44
|
-
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
45
44
|
// Re-run the per-element cache populate when sub-comp DOM children appear, so a
|
|
46
45
|
// sub-comp element gets its host-relative keyframe percentages (not elDuration=1).
|
|
47
46
|
const domClipChildrenKey = usePlayerStore((s) =>
|
|
@@ -54,11 +53,6 @@ export function useGsapAnimationsForElement(
|
|
|
54
53
|
if (fetchKey === lastFetchKeyRef.current) return;
|
|
55
54
|
lastFetchKeyRef.current = fetchKey;
|
|
56
55
|
|
|
57
|
-
if (retryTimerRef.current) {
|
|
58
|
-
clearTimeout(retryTimerRef.current);
|
|
59
|
-
retryTimerRef.current = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
56
|
if (!projectId) {
|
|
63
57
|
setAllAnimations([]);
|
|
64
58
|
setMultipleTimelines(false);
|
|
@@ -80,28 +74,10 @@ export function useGsapAnimationsForElement(
|
|
|
80
74
|
setAllAnimations(parsed.animations);
|
|
81
75
|
setMultipleTimelines(parsed.multipleTimelines === true);
|
|
82
76
|
setUnsupportedTimelinePattern(parsed.unsupportedTimelinePattern === true);
|
|
83
|
-
|
|
84
|
-
// Retry once if initial fetch returned 0 animations — handles
|
|
85
|
-
// cold-load race where the sourceFile isn't resolved yet.
|
|
86
|
-
if (parsed.animations.length === 0 && targetKey) {
|
|
87
|
-
retryTimerRef.current = setTimeout(() => {
|
|
88
|
-
if (cancelled) return;
|
|
89
|
-
fetchParsedAnimations(projectId, sourceFile).then((retryParsed) => {
|
|
90
|
-
if (cancelled) return;
|
|
91
|
-
if (retryParsed && retryParsed.animations.length > 0) {
|
|
92
|
-
setAllAnimations(retryParsed.animations);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}, 800);
|
|
96
|
-
}
|
|
97
77
|
});
|
|
98
78
|
|
|
99
79
|
return () => {
|
|
100
80
|
cancelled = true;
|
|
101
|
-
if (retryTimerRef.current) {
|
|
102
|
-
clearTimeout(retryTimerRef.current);
|
|
103
|
-
retryTimerRef.current = null;
|
|
104
|
-
}
|
|
105
81
|
};
|
|
106
82
|
}, [projectId, sourceFile, version, target?.id, target?.selector]);
|
|
107
83
|
|