@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
8
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
9
|
+
|
|
10
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
const mocks = vi.hoisted(() => ({
|
|
13
|
+
resize: vi.fn(),
|
|
14
|
+
drag: vi.fn(),
|
|
15
|
+
readPosition: vi.fn(),
|
|
16
|
+
setPosition: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("./gsapResizeIntercept", () => ({ tryGsapResizeIntercept: mocks.resize }));
|
|
20
|
+
vi.mock("./gsapRuntimeBridge", () => ({
|
|
21
|
+
POSITION_CHANNELS: ["x", "y"],
|
|
22
|
+
tryGsapDragIntercept: mocks.drag,
|
|
23
|
+
tryGsapRotationIntercept: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("./gsapPositionDetection", () => ({
|
|
26
|
+
readGsapPositionFromIframe: mocks.readPosition,
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../utils/elementGsap", () => ({ setElementGsapPosition: mocks.setPosition }));
|
|
29
|
+
vi.mock("./useAnimatedPropertyCommit", () => ({
|
|
30
|
+
useAnimatedPropertyCommit: () => ({
|
|
31
|
+
commitAnimatedProperty: vi.fn(),
|
|
32
|
+
commitAnimatedProperties: vi.fn(),
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("./useSafeGsapCommitMutation", () => ({
|
|
36
|
+
useGsapSaveFailureTelemetry: () => vi.fn(),
|
|
37
|
+
useSafeGsapCommitMutation: (commit: unknown) => commit,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function mountResizeHandler(animations: GsapAnimation[]) {
|
|
47
|
+
const element = document.createElement("div");
|
|
48
|
+
const selection = { element, id: "clip", selector: "#clip" } as unknown as DomEditSelection;
|
|
49
|
+
const fallback = vi.fn().mockResolvedValue(undefined);
|
|
50
|
+
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
51
|
+
let resize:
|
|
52
|
+
| ((
|
|
53
|
+
selection: DomEditSelection,
|
|
54
|
+
size: { width: number; height: number },
|
|
55
|
+
offset?: { x: number; y: number },
|
|
56
|
+
restore?: () => void,
|
|
57
|
+
) => Promise<void>)
|
|
58
|
+
| null = null;
|
|
59
|
+
function Harness() {
|
|
60
|
+
resize = useGsapAwareEditing({
|
|
61
|
+
domEditSelection: selection,
|
|
62
|
+
selectedGsapAnimations: animations,
|
|
63
|
+
gsapCommitMutation: commitMutation,
|
|
64
|
+
previewIframeRef: { current: null },
|
|
65
|
+
showToast: vi.fn(),
|
|
66
|
+
bumpGsapCache: vi.fn(),
|
|
67
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue(animations),
|
|
68
|
+
trackGsapInteractionFailure: vi.fn(),
|
|
69
|
+
handleDomBoxSizeCommit: fallback,
|
|
70
|
+
addGsapAnimation: vi.fn(),
|
|
71
|
+
convertToKeyframes: vi.fn(),
|
|
72
|
+
setArcPath: vi.fn(),
|
|
73
|
+
updateArcSegment: vi.fn(),
|
|
74
|
+
}).handleGsapAwareBoxSizeCommit;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const root = mountReactHarness(<Harness />);
|
|
78
|
+
return { selection, fallback, commitMutation, resize: resize!, root };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("useGsapAwareEditing anchored resize", () => {
|
|
82
|
+
it("forwards the anchor offset to the DOM fallback when GSAP does not handle resize", async () => {
|
|
83
|
+
mocks.resize.mockResolvedValue(false);
|
|
84
|
+
const h = mountResizeHandler([]);
|
|
85
|
+
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
86
|
+
expect(h.fallback).toHaveBeenCalledWith(
|
|
87
|
+
h.selection,
|
|
88
|
+
{ width: 300, height: 200 },
|
|
89
|
+
{ x: -50, y: -25 },
|
|
90
|
+
);
|
|
91
|
+
act(() => h.root.unmount());
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("persists the anchor exactly once through GSAP position when size route handles resize", async () => {
|
|
95
|
+
mocks.resize.mockResolvedValue(true);
|
|
96
|
+
mocks.drag.mockResolvedValue(true);
|
|
97
|
+
const h = mountResizeHandler([]);
|
|
98
|
+
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
99
|
+
expect(h.fallback).not.toHaveBeenCalled();
|
|
100
|
+
expect(mocks.drag).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(mocks.drag.mock.calls[0]![1]).toEqual({ x: -50, y: -25 });
|
|
102
|
+
act(() => h.root.unmount());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("settles the live GSAP position before resize persistence reaches its first await", async () => {
|
|
106
|
+
let resolveResize!: (handled: boolean) => void;
|
|
107
|
+
const pendingResize = new Promise<boolean>((resolve) => {
|
|
108
|
+
resolveResize = resolve;
|
|
109
|
+
});
|
|
110
|
+
mocks.resize.mockReturnValue(pendingResize);
|
|
111
|
+
mocks.drag.mockResolvedValue(true);
|
|
112
|
+
mocks.readPosition.mockReturnValue({ x: 120.4, y: 80.2 });
|
|
113
|
+
const h = mountResizeHandler([]);
|
|
114
|
+
h.selection.element.setAttribute("data-hf-drag-gsap-base-x", "120.4");
|
|
115
|
+
h.selection.element.setAttribute("data-hf-drag-gsap-base-y", "80.2");
|
|
116
|
+
h.selection.element.setAttribute("data-hf-drag-initial-offset-x", "0");
|
|
117
|
+
h.selection.element.setAttribute("data-hf-drag-initial-offset-y", "0");
|
|
118
|
+
|
|
119
|
+
let commit!: Promise<void>;
|
|
120
|
+
act(() => {
|
|
121
|
+
commit = h.resize(h.selection, { width: 300, height: 200 }, { x: -50.2, y: -25.6 });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70, 55);
|
|
125
|
+
expect(mocks.setPosition.mock.invocationCallOrder[0]).toBeLessThan(
|
|
126
|
+
mocks.resize.mock.invocationCallOrder[0]!,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
resolveResize(true);
|
|
130
|
+
await act(() => commit);
|
|
131
|
+
act(() => h.root.unmount());
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("passes a transaction-scoped commit wrapper into the resize path", async () => {
|
|
135
|
+
mocks.resize.mockImplementation(async (selection, _size, _animations, _iframe, commit) => {
|
|
136
|
+
await commit(selection, { type: "resize" }, { label: "Resize", softReload: true });
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
const h = mountResizeHandler([]);
|
|
140
|
+
|
|
141
|
+
await act(() => h.resize(h.selection, { width: 300, height: 200 }));
|
|
142
|
+
|
|
143
|
+
expect(h.commitMutation).toHaveBeenCalledWith(
|
|
144
|
+
h.selection,
|
|
145
|
+
{ type: "resize" },
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
coalesceKey: expect.stringMatching(/^tx:Resize layer:\d+$/),
|
|
148
|
+
softReload: true,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
act(() => h.root.unmount());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("folds a group drag's member writes into one undo entry via a shared coalesceKey", async () => {
|
|
155
|
+
const capturedKeys: Array<string | undefined> = [];
|
|
156
|
+
mocks.drag.mockImplementation(
|
|
157
|
+
async (
|
|
158
|
+
selection: DomEditSelection,
|
|
159
|
+
_next: unknown,
|
|
160
|
+
_anims: unknown,
|
|
161
|
+
_iframe: unknown,
|
|
162
|
+
commit: (
|
|
163
|
+
s: DomEditSelection,
|
|
164
|
+
m: unknown,
|
|
165
|
+
o: { coalesceKey?: string; label?: string; softReload?: boolean },
|
|
166
|
+
) => Promise<void>,
|
|
167
|
+
) => {
|
|
168
|
+
await commit(selection, { type: "move" }, { label: "Move", softReload: true });
|
|
169
|
+
return true;
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
const commitMutation = vi.fn(
|
|
173
|
+
(_s: DomEditSelection, _m: unknown, o: { coalesceKey?: string }) => {
|
|
174
|
+
capturedKeys.push(o.coalesceKey);
|
|
175
|
+
return Promise.resolve();
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
let groupCommit!: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void>;
|
|
179
|
+
function Harness() {
|
|
180
|
+
groupCommit = useGsapAwareEditing({
|
|
181
|
+
domEditSelection: null,
|
|
182
|
+
selectedGsapAnimations: [],
|
|
183
|
+
gsapCommitMutation: commitMutation,
|
|
184
|
+
previewIframeRef: { current: null },
|
|
185
|
+
showToast: vi.fn(),
|
|
186
|
+
bumpGsapCache: vi.fn(),
|
|
187
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
188
|
+
trackGsapInteractionFailure: vi.fn(),
|
|
189
|
+
handleDomBoxSizeCommit: vi.fn(),
|
|
190
|
+
addGsapAnimation: vi.fn(),
|
|
191
|
+
convertToKeyframes: vi.fn(),
|
|
192
|
+
setArcPath: vi.fn(),
|
|
193
|
+
updateArcSegment: vi.fn(),
|
|
194
|
+
}).handleGsapAwareGroupPathOffsetCommit;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const root = mountReactHarness(<Harness />);
|
|
198
|
+
const updates = [
|
|
199
|
+
{
|
|
200
|
+
selection: { element: document.createElement("div"), id: "a", selector: "#a" },
|
|
201
|
+
next: { x: 10, y: 10 },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
selection: { element: document.createElement("div"), id: "b", selector: "#b" },
|
|
205
|
+
next: { x: 10, y: 10 },
|
|
206
|
+
},
|
|
207
|
+
] as unknown as DomEditGroupPathOffsetCommit[];
|
|
208
|
+
|
|
209
|
+
await act(() => groupCommit(updates));
|
|
210
|
+
|
|
211
|
+
expect(capturedKeys).toHaveLength(2);
|
|
212
|
+
expect(capturedKeys[0]).toMatch(/^group-drag:\d+$/);
|
|
213
|
+
// Both members share ONE coalesceKey → they fold into a single undo entry.
|
|
214
|
+
expect(capturedKeys[0]).toBe(capturedKeys[1]);
|
|
215
|
+
act(() => root.unmount());
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("restores once when resize persistence fails", async () => {
|
|
219
|
+
const error = new Error("resize failed");
|
|
220
|
+
const restore = vi.fn();
|
|
221
|
+
mocks.resize.mockRejectedValue(error);
|
|
222
|
+
const h = mountResizeHandler([]);
|
|
223
|
+
|
|
224
|
+
const commit = h.resize(h.selection, { width: 300, height: 200 }, undefined, restore);
|
|
225
|
+
await expect(commit).rejects.toBe(error);
|
|
226
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
227
|
+
act(() => h.root.unmount());
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not apply the anchor twice when scale route already settles the drop point", async () => {
|
|
231
|
+
mocks.resize.mockResolvedValue(true);
|
|
232
|
+
const scale = { propertyGroup: "scale" } as GsapAnimation;
|
|
233
|
+
const h = mountResizeHandler([scale]);
|
|
234
|
+
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
235
|
+
expect(mocks.drag).not.toHaveBeenCalled();
|
|
236
|
+
expect(h.fallback).not.toHaveBeenCalled();
|
|
237
|
+
act(() => h.root.unmount());
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -10,15 +10,30 @@
|
|
|
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 {
|
|
13
|
+
import {
|
|
14
|
+
POSITION_CHANNELS,
|
|
15
|
+
tryGsapDragIntercept,
|
|
16
|
+
tryGsapRotationIntercept,
|
|
17
|
+
} from "./gsapRuntimeBridge";
|
|
14
18
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
19
|
+
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
20
|
+
import { readGsapPositionFromIframe } from "./gsapPositionDetection";
|
|
21
|
+
import { selectorFromSelection } from "./gsapShared";
|
|
15
22
|
import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
|
|
16
23
|
import {
|
|
17
24
|
useGsapSaveFailureTelemetry,
|
|
18
25
|
useSafeGsapCommitMutation,
|
|
19
26
|
} from "./useSafeGsapCommitMutation";
|
|
20
27
|
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
28
|
+
import { setElementGsapPosition } from "../utils/elementGsap";
|
|
29
|
+
import { logResize, logResizeSettle } from "../utils/resizeDebug";
|
|
21
30
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
31
|
+
import { runGestureTransaction } from "./gestureTransaction";
|
|
32
|
+
import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes";
|
|
33
|
+
|
|
34
|
+
// Distinct coalesceKey per group drag so consecutive group drags don't fold
|
|
35
|
+
// into one another's undo entry (module-local counter, not Date.now()).
|
|
36
|
+
let groupDragCommitCounter = 0;
|
|
22
37
|
|
|
23
38
|
export interface UseGsapAwareEditingParams {
|
|
24
39
|
domEditSelection: DomEditSelection | null;
|
|
@@ -38,6 +53,7 @@ export interface UseGsapAwareEditingParams {
|
|
|
38
53
|
handleDomBoxSizeCommit: (
|
|
39
54
|
selection: DomEditSelection,
|
|
40
55
|
next: { width: number; height: number },
|
|
56
|
+
offset?: { x: number; y: number },
|
|
41
57
|
) => Promise<void>;
|
|
42
58
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
43
59
|
addGsapAnimation: (
|
|
@@ -127,7 +143,18 @@ export function useGsapAwareEditing({
|
|
|
127
143
|
// composition there's nothing to write (a no-op, exactly like the single-drag path).
|
|
128
144
|
const handleGsapAwareGroupPathOffsetCommit = useCallback(
|
|
129
145
|
async (updates: DomEditGroupPathOffsetCommit[]) => {
|
|
130
|
-
if (!gsapCommitMutation) return;
|
|
146
|
+
if (!gsapCommitMutation || updates.length === 0) return;
|
|
147
|
+
// A group drag is ONE user action: fold every member's position write into
|
|
148
|
+
// a single undo entry by forcing a shared coalesceKey (infinite window, so
|
|
149
|
+
// it survives the N sequential server round-trips) onto each commit —
|
|
150
|
+
// otherwise each member records its own entry and it takes N presses to undo.
|
|
151
|
+
const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
|
|
152
|
+
const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) =>
|
|
153
|
+
gsapCommitMutation(selection, mutation, {
|
|
154
|
+
...options,
|
|
155
|
+
coalesceKey,
|
|
156
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
157
|
+
});
|
|
131
158
|
for (const { selection, next } of updates) {
|
|
132
159
|
try {
|
|
133
160
|
await tryGsapDragIntercept(
|
|
@@ -135,7 +162,7 @@ export function useGsapAwareEditing({
|
|
|
135
162
|
next,
|
|
136
163
|
[],
|
|
137
164
|
previewIframeRef.current,
|
|
138
|
-
|
|
165
|
+
coalescedCommit,
|
|
139
166
|
makeFetchFallback(selection),
|
|
140
167
|
);
|
|
141
168
|
} catch (error) {
|
|
@@ -148,24 +175,92 @@ export function useGsapAwareEditing({
|
|
|
148
175
|
);
|
|
149
176
|
|
|
150
177
|
const handleGsapAwareBoxSizeCommit = useCallback(
|
|
151
|
-
async (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
async (
|
|
179
|
+
selection: DomEditSelection,
|
|
180
|
+
next: { width: number; height: number },
|
|
181
|
+
offset?: { x: number; y: number },
|
|
182
|
+
restore: () => void = () => undefined,
|
|
183
|
+
) => {
|
|
184
|
+
const scaleRoute = selectedGsapAnimations.some((anim) => anim.propertyGroup === "scale");
|
|
185
|
+
const selector = selectorFromSelection(selection);
|
|
186
|
+
const hasLivePositionTween = selector
|
|
187
|
+
? hasNonHoldTweenForElement(
|
|
158
188
|
previewIframeRef.current,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
selector,
|
|
190
|
+
undefined,
|
|
191
|
+
POSITION_CHANNELS,
|
|
192
|
+
)
|
|
193
|
+
: false;
|
|
194
|
+
logResize("commit-route", {
|
|
195
|
+
next,
|
|
196
|
+
offset: offset ?? null,
|
|
197
|
+
scaleRoute,
|
|
198
|
+
animCount: selectedGsapAnimations.length,
|
|
199
|
+
animGroups: selectedGsapAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
|
|
200
|
+
});
|
|
201
|
+
return runGestureTransaction({
|
|
202
|
+
element: selection.element,
|
|
203
|
+
label: "Resize layer",
|
|
204
|
+
settle: () => {
|
|
205
|
+
// Scale resize settles its center-scale residual after the scale commit
|
|
206
|
+
// renders. Width/height can settle its anchored position immediately.
|
|
207
|
+
if (!offset || scaleRoute || !selector) return;
|
|
208
|
+
const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
|
|
209
|
+
x: 0,
|
|
210
|
+
y: 0,
|
|
211
|
+
};
|
|
212
|
+
const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
|
|
213
|
+
logResize("sync-settle", { gsapPos, offset, newX, newY });
|
|
214
|
+
setElementGsapPosition(selection.element, newX, newY);
|
|
215
|
+
},
|
|
216
|
+
persist: async (commit) => {
|
|
217
|
+
if (gsapCommitMutation) {
|
|
218
|
+
const commitMutation = commit(gsapCommitMutation);
|
|
219
|
+
try {
|
|
220
|
+
const handled = await tryGsapResizeIntercept(
|
|
221
|
+
selection,
|
|
222
|
+
next,
|
|
223
|
+
selectedGsapAnimations,
|
|
224
|
+
previewIframeRef.current,
|
|
225
|
+
commitMutation,
|
|
226
|
+
makeFetchFallback(selection),
|
|
227
|
+
);
|
|
228
|
+
if (handled) {
|
|
229
|
+
logResize("intercept-handled", {
|
|
230
|
+
scaleRoute,
|
|
231
|
+
willForwardOffset: !!(offset && !scaleRoute),
|
|
232
|
+
});
|
|
233
|
+
// Scale-route resize persists its residual position internally.
|
|
234
|
+
// Width/height persists the already-settled anchor through drag.
|
|
235
|
+
if (offset && !scaleRoute) {
|
|
236
|
+
await tryGsapDragIntercept(
|
|
237
|
+
selection,
|
|
238
|
+
offset,
|
|
239
|
+
selectedGsapAnimations,
|
|
240
|
+
previewIframeRef.current,
|
|
241
|
+
commitMutation,
|
|
242
|
+
makeFetchFallback(selection),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
logResizeSettle(selection.element, scaleRoute ? "gsap-scale" : "gsap-size");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
logResize("dom-route", {
|
|
254
|
+
next,
|
|
255
|
+
offset: offset ?? null,
|
|
256
|
+
hadGsapMutation: !!gsapCommitMutation,
|
|
257
|
+
});
|
|
258
|
+
logResizeSettle(selection.element, "dom-route");
|
|
259
|
+
await handleDomBoxSizeCommit(selection, next, offset);
|
|
260
|
+
},
|
|
261
|
+
restore,
|
|
262
|
+
skipPixelAssert: hasLivePositionTween,
|
|
263
|
+
});
|
|
169
264
|
},
|
|
170
265
|
[
|
|
171
266
|
handleDomBoxSizeCommit,
|
|
@@ -99,3 +99,62 @@ describe("useGsapKeyframeOps — resizeKeyframedTween", () => {
|
|
|
99
99
|
expect(labelArg).toBe("Retime keyframe (resize tween)");
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
describe("useGsapKeyframeOps — keyframe transaction options", () => {
|
|
104
|
+
it("soft-reloads a standalone convert when the SDK path is unavailable", async () => {
|
|
105
|
+
const commitMutation = vi.fn<(...args: unknown[]) => Promise<unknown>>(async () => ({
|
|
106
|
+
ok: true,
|
|
107
|
+
}));
|
|
108
|
+
const api = renderKeyframeOps({ commitMutation, trackGsapSaveFailure: vi.fn() });
|
|
109
|
+
|
|
110
|
+
await act(async () => {
|
|
111
|
+
await api.convertToKeyframes(selection, "box-to-0-opacity");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(commitMutation).toHaveBeenCalledWith(
|
|
115
|
+
selection,
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
type: "convert-to-keyframes",
|
|
118
|
+
animationId: "box-to-0-opacity",
|
|
119
|
+
}),
|
|
120
|
+
{ label: "Convert to keyframes", softReload: true },
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("threads one coalesce key through skipped convert reload and terminal batch edit", async () => {
|
|
125
|
+
const commitMutation = vi.fn<(...args: unknown[]) => Promise<unknown>>(async () => ({
|
|
126
|
+
ok: true,
|
|
127
|
+
}));
|
|
128
|
+
const api = renderKeyframeOps({ commitMutation, trackGsapSaveFailure: vi.fn() });
|
|
129
|
+
const coalesceKey = "enable-keyframes:box-to-0-opacity:1";
|
|
130
|
+
|
|
131
|
+
await act(async () => {
|
|
132
|
+
await api.convertToKeyframes(selection, "box-to-0-opacity", undefined, undefined, {
|
|
133
|
+
skipReload: true,
|
|
134
|
+
coalesceKey,
|
|
135
|
+
coalesceMs: Infinity,
|
|
136
|
+
});
|
|
137
|
+
await api.addKeyframeBatch(
|
|
138
|
+
selection,
|
|
139
|
+
"box-to-0-opacity",
|
|
140
|
+
50,
|
|
141
|
+
{ opacity: 0.5 },
|
|
142
|
+
{
|
|
143
|
+
coalesceKey,
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(commitMutation.mock.calls[0]?.[2]).toEqual({
|
|
149
|
+
label: "Convert to keyframes",
|
|
150
|
+
skipReload: true,
|
|
151
|
+
coalesceKey,
|
|
152
|
+
coalesceMs: Infinity,
|
|
153
|
+
});
|
|
154
|
+
expect(commitMutation.mock.calls[1]?.[2]).toEqual({
|
|
155
|
+
label: "Add keyframe at 50%",
|
|
156
|
+
softReload: true,
|
|
157
|
+
coalesceKey,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./gsapKeyframeCacheHelpers";
|
|
20
20
|
import type {
|
|
21
21
|
CommitMutation,
|
|
22
|
+
CommitMutationOptions,
|
|
22
23
|
SafeGsapCommitMutation,
|
|
23
24
|
TrackGsapSaveFailure,
|
|
24
25
|
} from "./gsapScriptCommitTypes";
|
|
@@ -54,6 +55,21 @@ interface GsapKeyframeOpsParams extends SdkKeyframeDeps {
|
|
|
54
55
|
trackGsapSaveFailure: TrackGsapSaveFailure;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Translate a gesture's commit overrides into the SDK persist options. The
|
|
60
|
+
* server path's `softReload`/`skipReload` maps to the SDK's `skipRefresh`, and
|
|
61
|
+
* `coalesceKey`/`coalesceMs` must ride along so an SDK-routed edit folds into
|
|
62
|
+
* one undo entry the same way the server path does.
|
|
63
|
+
*/
|
|
64
|
+
function toSdkPersistOptions(label: string, overrides?: Partial<CommitMutationOptions>) {
|
|
65
|
+
return {
|
|
66
|
+
label,
|
|
67
|
+
coalesceKey: overrides?.coalesceKey,
|
|
68
|
+
coalesceMs: overrides?.coalesceMs,
|
|
69
|
+
skipRefresh: overrides?.skipReload,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
export function useGsapKeyframeOps({
|
|
58
74
|
activeCompPath,
|
|
59
75
|
commitMutation,
|
|
@@ -140,6 +156,7 @@ export function useGsapKeyframeOps({
|
|
|
140
156
|
animationId: string,
|
|
141
157
|
percentage: number,
|
|
142
158
|
properties: Record<string, number | string>,
|
|
159
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
143
160
|
) => {
|
|
144
161
|
if (sdkSession && sdkDeps) {
|
|
145
162
|
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -150,21 +167,30 @@ export function useGsapKeyframeOps({
|
|
|
150
167
|
properties,
|
|
151
168
|
sdkSession,
|
|
152
169
|
sdkDeps,
|
|
153
|
-
|
|
170
|
+
toSdkPersistOptions(`Add keyframe at ${percentage}%`, commitOverrides),
|
|
154
171
|
);
|
|
155
172
|
if (handled) return;
|
|
156
173
|
}
|
|
157
174
|
return commitMutation(
|
|
158
175
|
selection,
|
|
159
176
|
{ type: "add-keyframe", animationId, percentage, properties },
|
|
160
|
-
{
|
|
177
|
+
{
|
|
178
|
+
label: `Add keyframe at ${percentage}%`,
|
|
179
|
+
softReload: true,
|
|
180
|
+
...commitOverrides,
|
|
181
|
+
},
|
|
161
182
|
);
|
|
162
183
|
},
|
|
163
184
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
164
185
|
);
|
|
165
186
|
|
|
166
187
|
const removeKeyframe = useCallback(
|
|
167
|
-
(
|
|
188
|
+
(
|
|
189
|
+
selection: DomEditSelection,
|
|
190
|
+
animationId: string,
|
|
191
|
+
percentage: number,
|
|
192
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
193
|
+
) => {
|
|
168
194
|
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
169
195
|
const mutation = { type: "remove-keyframe", animationId, percentage };
|
|
170
196
|
void executeOptimisticKeyframeCacheUpdate({
|
|
@@ -182,6 +208,7 @@ export function useGsapKeyframeOps({
|
|
|
182
208
|
),
|
|
183
209
|
}),
|
|
184
210
|
persist: async () => {
|
|
211
|
+
const label = `Remove keyframe at ${percentage}%`;
|
|
185
212
|
if (sdkSession && sdkDeps) {
|
|
186
213
|
const handled = await sdkGsapRemoveKeyframePersist(
|
|
187
214
|
sourceFile,
|
|
@@ -189,14 +216,14 @@ export function useGsapKeyframeOps({
|
|
|
189
216
|
percentage,
|
|
190
217
|
sdkSession,
|
|
191
218
|
sdkDeps,
|
|
192
|
-
|
|
219
|
+
toSdkPersistOptions(label, commitOverrides),
|
|
193
220
|
);
|
|
194
221
|
if (handled) return;
|
|
195
222
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
softReload: true,
|
|
199
|
-
|
|
223
|
+
const commitOptions = commitOverrides?.skipReload
|
|
224
|
+
? { label, ...commitOverrides }
|
|
225
|
+
: { label, softReload: true, ...commitOverrides };
|
|
226
|
+
await commitMutation(selection, mutation, commitOptions);
|
|
200
227
|
},
|
|
201
228
|
}).catch((error) => {
|
|
202
229
|
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
|
|
@@ -261,6 +288,7 @@ export function useGsapKeyframeOps({
|
|
|
261
288
|
animationId: string,
|
|
262
289
|
resolvedFromValues?: Record<string, number | string>,
|
|
263
290
|
duration?: number,
|
|
291
|
+
commitOverrides: Partial<CommitMutationOptions> = { softReload: true },
|
|
264
292
|
) => {
|
|
265
293
|
if (sdkSession && sdkDeps) {
|
|
266
294
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -270,7 +298,7 @@ export function useGsapKeyframeOps({
|
|
|
270
298
|
resolvedFromValues,
|
|
271
299
|
sdkSession,
|
|
272
300
|
sdkDeps,
|
|
273
|
-
|
|
301
|
+
toSdkPersistOptions("Convert to keyframes", commitOverrides),
|
|
274
302
|
);
|
|
275
303
|
if (handled) return;
|
|
276
304
|
}
|
|
@@ -279,7 +307,7 @@ export function useGsapKeyframeOps({
|
|
|
279
307
|
// `duration` only applies when the target is a static `set` (which has
|
|
280
308
|
// none) — it spans the converted keyframes across the element's clip.
|
|
281
309
|
{ type: "convert-to-keyframes", animationId, resolvedFromValues, duration },
|
|
282
|
-
{ label: "Convert to keyframes" },
|
|
310
|
+
{ label: "Convert to keyframes", ...commitOverrides },
|
|
283
311
|
);
|
|
284
312
|
},
|
|
285
313
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
@@ -217,6 +217,7 @@ let cleanup: (() => void) | null = null;
|
|
|
217
217
|
function renderCommitHook() {
|
|
218
218
|
const reloadPreview = vi.fn();
|
|
219
219
|
const onCacheInvalidate = vi.fn();
|
|
220
|
+
const onFileContentChanged = vi.fn();
|
|
220
221
|
const forceReloadSdkSession = vi.fn();
|
|
221
222
|
const recordEdit = vi.fn(async () => {});
|
|
222
223
|
const showToast = vi.fn();
|
|
@@ -231,7 +232,7 @@ function renderCommitHook() {
|
|
|
231
232
|
domEditSaveTimestampRef: { current: 0 },
|
|
232
233
|
reloadPreview,
|
|
233
234
|
onCacheInvalidate,
|
|
234
|
-
onFileContentChanged
|
|
235
|
+
onFileContentChanged,
|
|
235
236
|
showToast,
|
|
236
237
|
sdkSession: null,
|
|
237
238
|
writeProjectFile: undefined,
|
|
@@ -252,6 +253,7 @@ function renderCommitHook() {
|
|
|
252
253
|
api: hookApi,
|
|
253
254
|
reloadPreview,
|
|
254
255
|
onCacheInvalidate,
|
|
256
|
+
onFileContentChanged,
|
|
255
257
|
forceReloadSdkSession,
|
|
256
258
|
recordEdit,
|
|
257
259
|
showToast,
|
|
@@ -383,4 +385,41 @@ describe("runCommit — instantPatch wiring", () => {
|
|
|
383
385
|
expectSoftReloadedWith(deps.reloadPreview, "AFTER");
|
|
384
386
|
expect(deps.reloadPreview).not.toHaveBeenCalled();
|
|
385
387
|
});
|
|
388
|
+
|
|
389
|
+
it("batch capability posts ordered mutations and finalizes the result once", async () => {
|
|
390
|
+
applySoftReload.mockReturnValue("applied");
|
|
391
|
+
mockFetchResult();
|
|
392
|
+
const deps = renderCommitHook();
|
|
393
|
+
const firstMutation = { type: "add", value: 1 };
|
|
394
|
+
const lastMutation = { type: "delete", value: 2 };
|
|
395
|
+
const batch = deps.api.commitMutation.batch;
|
|
396
|
+
if (!batch) throw new Error("batch capability missing");
|
|
397
|
+
|
|
398
|
+
await act(async () => {
|
|
399
|
+
await batch(
|
|
400
|
+
[
|
|
401
|
+
{ selection, mutation: firstMutation, options: { label: "Resize", skipReload: true } },
|
|
402
|
+
{ selection, mutation: lastMutation, options: { label: "Resize", softReload: true } },
|
|
403
|
+
],
|
|
404
|
+
{ label: "Resize", coalesceKey: "tx:resize:1", coalesceMs: Infinity, softReload: true },
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
410
|
+
"/api/projects/proj-1/gsap-mutations-batch/index.html",
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify({ mutations: [firstMutation, lastMutation] }),
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
expect(deps.recordEdit).toHaveBeenCalledTimes(1);
|
|
417
|
+
expect(deps.recordEdit).toHaveBeenCalledWith(
|
|
418
|
+
expect.objectContaining({ label: "Resize", coalesceKey: "tx:resize:1" }),
|
|
419
|
+
);
|
|
420
|
+
expect(deps.onFileContentChanged).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(deps.forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
422
|
+
expect(applySoftReload).toHaveBeenCalledTimes(1);
|
|
423
|
+
expect(deps.onCacheInvalidate).toHaveBeenCalledTimes(1);
|
|
424
|
+
});
|
|
386
425
|
});
|