@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.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 +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- 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 +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- 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 +158 -126
- 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 +43 -1
- 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 +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- 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/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/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/nle/AssetPreviewOverlay.tsx +147 -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 +240 -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.tsx +326 -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 +39 -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 +12 -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 +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -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/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -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 +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- 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 +303 -0
- 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/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- 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/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- 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/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 +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -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 +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- 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 +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- 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/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- 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 +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- 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/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- 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
|
@@ -10,8 +10,10 @@ import { smoothGestureKeyframes } from "../utils/gestureSmoother";
|
|
|
10
10
|
import { usePlayerStore } from "../player";
|
|
11
11
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
12
12
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
13
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
13
14
|
import { roundTo3 } from "../utils/rounding";
|
|
14
15
|
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
16
|
+
import { isInstantHold } from "./gsapShared";
|
|
15
17
|
|
|
16
18
|
type RecordedKeyframe = {
|
|
17
19
|
percentage: number;
|
|
@@ -68,10 +70,18 @@ interface GestureSessionRef {
|
|
|
68
70
|
selectedGsapAnimations?: GsapAnimation[];
|
|
69
71
|
commitMutation?: (
|
|
70
72
|
mutation: Record<string, unknown>,
|
|
71
|
-
options:
|
|
73
|
+
options: CommitMutationOptions,
|
|
72
74
|
) => Promise<void>;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** Only the LAST group in a per-group commit loop reloads the preview; the
|
|
78
|
+
* earlier ones skip it, so a multi-group gesture recording is one reload. */
|
|
79
|
+
function reloadOnlyLast(index: number, count: number): Partial<CommitMutationOptions> {
|
|
80
|
+
return index === count - 1 ? { softReload: true } : { skipReload: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let gestureRecordingCommitCounter = 0;
|
|
84
|
+
|
|
75
85
|
interface UseGestureCommitParams {
|
|
76
86
|
domEditSessionRef: React.MutableRefObject<GestureSessionRef>;
|
|
77
87
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -112,6 +122,10 @@ export function useGestureCommit({
|
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
124
|
commitInFlightRef.current = true;
|
|
125
|
+
const coalesceOptions = {
|
|
126
|
+
coalesceKey: `gesture-recording:${++gestureRecordingCommitCounter}`,
|
|
127
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
128
|
+
};
|
|
115
129
|
gestureStateRef.current = "idle";
|
|
116
130
|
isGestureRecordingRef.current = false;
|
|
117
131
|
const frozenSamples = gestureRecording.stopRecording();
|
|
@@ -175,9 +189,9 @@ export function useGestureCommit({
|
|
|
175
189
|
? allAnims.find((a) => a.propertyGroup === "position" && a.targetSelector === selector)
|
|
176
190
|
: undefined;
|
|
177
191
|
if (existingPositionTween) {
|
|
178
|
-
if (existingPositionTween
|
|
179
|
-
//
|
|
180
|
-
//
|
|
192
|
+
if (isInstantHold(existingPositionTween)) {
|
|
193
|
+
// An instant hold is not a tween to merge into — replace it with the
|
|
194
|
+
// recorded motion (which already starts from the held position).
|
|
181
195
|
await liveSession.commitMutation(
|
|
182
196
|
{
|
|
183
197
|
type: "replace-with-keyframes",
|
|
@@ -242,7 +256,8 @@ export function useGestureCommit({
|
|
|
242
256
|
// Emit one tween per property group so a mixed-prop gesture (e.g.
|
|
243
257
|
// x/y + opacity) doesn't collapse into an untagged legacy mixed
|
|
244
258
|
// tween that the position-only drag intercept can't edit.
|
|
245
|
-
|
|
259
|
+
const keyframeGroups = partitionKeyframesByGroup(keyframes);
|
|
260
|
+
for (const [index, groupKfs] of keyframeGroups.entries()) {
|
|
246
261
|
await liveSession.commitMutation(
|
|
247
262
|
{
|
|
248
263
|
type: "add-with-keyframes",
|
|
@@ -256,14 +271,19 @@ export function useGestureCommit({
|
|
|
256
271
|
// not inherit a sigmoid.
|
|
257
272
|
easeEach: "none",
|
|
258
273
|
},
|
|
259
|
-
{
|
|
274
|
+
{
|
|
275
|
+
label: "Gesture recording (new range)",
|
|
276
|
+
...coalesceOptions,
|
|
277
|
+
...reloadOnlyLast(index, keyframeGroups.length),
|
|
278
|
+
},
|
|
260
279
|
);
|
|
261
280
|
}
|
|
262
281
|
}
|
|
263
282
|
}
|
|
264
283
|
} else {
|
|
265
284
|
// No existing tween — same per-group split as the new-range branch above.
|
|
266
|
-
|
|
285
|
+
const keyframeGroups = partitionKeyframesByGroup(keyframes);
|
|
286
|
+
for (const [index, groupKfs] of keyframeGroups.entries()) {
|
|
267
287
|
await liveSession.commitMutation(
|
|
268
288
|
{
|
|
269
289
|
type: "add-with-keyframes",
|
|
@@ -274,7 +294,11 @@ export function useGestureCommit({
|
|
|
274
294
|
// Linear fallback (see above) — constant-speed segments stay linear.
|
|
275
295
|
easeEach: "none",
|
|
276
296
|
},
|
|
277
|
-
{
|
|
297
|
+
{
|
|
298
|
+
label: "Gesture recording",
|
|
299
|
+
...coalesceOptions,
|
|
300
|
+
...reloadOnlyLast(index, keyframeGroups.length),
|
|
301
|
+
},
|
|
278
302
|
);
|
|
279
303
|
}
|
|
280
304
|
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
readProjectFileContent,
|
|
4
|
+
saveProjectFilesWithHistory,
|
|
5
|
+
type DomEditCommitBaseParams,
|
|
6
|
+
} from "../utils/studioFileHistory";
|
|
4
7
|
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
6
8
|
|
|
7
|
-
interface
|
|
8
|
-
label: string;
|
|
9
|
-
kind: EditHistoryKind;
|
|
10
|
-
coalesceKey?: string;
|
|
11
|
-
files: Record<string, { before: string; after: string }>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface UseGroupCommitsParams {
|
|
15
|
-
activeCompPath: string | null;
|
|
16
|
-
showToast: (message: string, tone?: "error" | "info") => void;
|
|
17
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
18
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
19
|
-
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
20
|
-
projectIdRef: React.MutableRefObject<string | null>;
|
|
21
|
-
reloadPreview: () => void;
|
|
22
|
-
clearDomSelection: () => void;
|
|
9
|
+
interface UseGroupCommitsParams extends DomEditCommitBaseParams {
|
|
23
10
|
/** Resync the SDK session after a server-side write (the wrapper/unwrap changes
|
|
24
11
|
* structure the in-memory doc doesn't know about). */
|
|
25
12
|
forceReloadSdkSession?: () => void;
|
|
@@ -81,15 +68,7 @@ async function commitStructuralMutation(
|
|
|
81
68
|
| "reloadPreview"
|
|
82
69
|
>,
|
|
83
70
|
): Promise<{ content?: string; groupId?: string }> {
|
|
84
|
-
const
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
|
87
|
-
}
|
|
88
|
-
const data = (await response.json()) as { content?: string };
|
|
89
|
-
const originalContent = data.content;
|
|
90
|
-
if (typeof originalContent !== "string") {
|
|
91
|
-
throw new Error(`Missing file contents for ${targetPath}`);
|
|
92
|
-
}
|
|
71
|
+
const originalContent = await readProjectFileContent(pid, targetPath);
|
|
93
72
|
|
|
94
73
|
deps.domEditSaveTimestampRef.current = Date.now();
|
|
95
74
|
const mutateResponse = await fetch(
|
|
@@ -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
|
+
});
|