@hyperframes/studio 0.8.114 → 0.8.116
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-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -127,7 +127,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
127
127
|
y: 20,
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
expect(outcome).toEqual({ ok: true });
|
|
130
|
+
expect(outcome).toEqual({ ok: true, changed: true });
|
|
131
131
|
expect(mutations).toEqual([
|
|
132
132
|
expect.objectContaining({ type: "add", targetSelector: "#card", method: "set" }),
|
|
133
133
|
]);
|
|
@@ -146,7 +146,14 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
146
146
|
const url = String(input instanceof Request ? input.url : input);
|
|
147
147
|
calls.urls.push(url);
|
|
148
148
|
if (url.includes("/patch-element/")) calls.patches.push(JSON.parse(String(init?.body)));
|
|
149
|
-
const saved = {
|
|
149
|
+
const saved = {
|
|
150
|
+
ok: true,
|
|
151
|
+
changed: true,
|
|
152
|
+
matched: true,
|
|
153
|
+
path: "index.html",
|
|
154
|
+
content: "AFTER",
|
|
155
|
+
version: "v2",
|
|
156
|
+
};
|
|
150
157
|
const body = url.includes("/files/") ? { content: SOURCE } : saved;
|
|
151
158
|
const status = url.includes("/patch-element/") ? patchStatus : 200;
|
|
152
159
|
return new Response(JSON.stringify(body), {
|
|
@@ -163,7 +170,10 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
163
170
|
const { element, recordEdit, hook, unmount } = renderHost();
|
|
164
171
|
|
|
165
172
|
const card = makeSelection("card", element);
|
|
166
|
-
await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({
|
|
173
|
+
await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({
|
|
174
|
+
ok: true,
|
|
175
|
+
changed: true,
|
|
176
|
+
});
|
|
167
177
|
|
|
168
178
|
expect(element.style.getPropertyValue("translate")).toBe("130.5px 90px");
|
|
169
179
|
expect(calls.patches).toEqual([
|
|
@@ -185,7 +195,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
185
195
|
|
|
186
196
|
await expect(
|
|
187
197
|
hook().commitBoxSize(makeSelection("card", element), { width: 300, height: 90 }),
|
|
188
|
-
).resolves.toEqual({ ok: true });
|
|
198
|
+
).resolves.toEqual({ ok: true, changed: true });
|
|
189
199
|
|
|
190
200
|
expect(element.style.getPropertyValue("width")).toBe("300px");
|
|
191
201
|
expect(calls.patches).toEqual([
|
|
@@ -247,7 +257,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
247
257
|
{ x: 1, y: 2 },
|
|
248
258
|
{ plainTranslate: true },
|
|
249
259
|
),
|
|
250
|
-
).resolves.toEqual({ ok: true });
|
|
260
|
+
).resolves.toEqual({ ok: true, changed: true });
|
|
251
261
|
expect(calls.patches).toHaveLength(1);
|
|
252
262
|
unmount();
|
|
253
263
|
});
|
|
@@ -396,7 +406,14 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
396
406
|
headers: { "content-type": "application/json" },
|
|
397
407
|
});
|
|
398
408
|
}
|
|
399
|
-
return json({
|
|
409
|
+
return json({
|
|
410
|
+
ok: true,
|
|
411
|
+
changed: true,
|
|
412
|
+
matched: true,
|
|
413
|
+
path: "index.html",
|
|
414
|
+
content: "AFTER",
|
|
415
|
+
version: "v2",
|
|
416
|
+
});
|
|
400
417
|
}
|
|
401
418
|
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
402
419
|
calls.gsapMutations.push(JSON.parse(String(init?.body)));
|
|
@@ -426,6 +443,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
426
443
|
|
|
427
444
|
await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
|
|
428
445
|
ok: true,
|
|
446
|
+
changed: true,
|
|
429
447
|
});
|
|
430
448
|
|
|
431
449
|
expect(calls.gsapMutations).toHaveLength(0);
|
|
@@ -455,6 +473,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
|
|
|
455
473
|
]);
|
|
456
474
|
await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
|
|
457
475
|
ok: true,
|
|
476
|
+
changed: true,
|
|
458
477
|
});
|
|
459
478
|
|
|
460
479
|
expect(calls.patches).toHaveLength(2);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeometryCommitResult } from "../utils/previewFeatureUsage";
|
|
1
2
|
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
2
3
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
@@ -67,7 +68,7 @@ export function useDomGeometryCommit({
|
|
|
67
68
|
}: UseDomGeometryCommitOptions): DomGeometryCommits {
|
|
68
69
|
const projectIdRef = useRef(projectId);
|
|
69
70
|
projectIdRef.current = projectId;
|
|
70
|
-
const pending = useRef(new Set<Promise<
|
|
71
|
+
const pending = useRef(new Set<Promise<GeometryCommitResult | undefined>>()).current;
|
|
71
72
|
const editHistory = useMemo(() => ({ recordEdit }), [recordEdit]);
|
|
72
73
|
const reload = useCallback(
|
|
73
74
|
() => (reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload()),
|
|
@@ -127,6 +128,7 @@ export function useDomGeometryCommit({
|
|
|
127
128
|
domEditSelection: null,
|
|
128
129
|
selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
|
|
129
130
|
gsapCommitMutation: gsap.commitMutation,
|
|
131
|
+
activeCompPath,
|
|
130
132
|
previewIframeRef: iframeRef,
|
|
131
133
|
showToast,
|
|
132
134
|
bumpGsapCache,
|
|
@@ -142,7 +144,10 @@ export function useDomGeometryCommit({
|
|
|
142
144
|
updateArcSegment: gsap.updateArcSegment,
|
|
143
145
|
});
|
|
144
146
|
const saved = useCallback(
|
|
145
|
-
async (
|
|
147
|
+
async (
|
|
148
|
+
commit: () => Promise<GeometryCommitResult | undefined>,
|
|
149
|
+
restore = noop,
|
|
150
|
+
): Promise<DomEditCommitOutcome> => {
|
|
146
151
|
const refusal = !projectIdRef.current
|
|
147
152
|
? "No project is open"
|
|
148
153
|
: !isPreviewBooted(projectIdRef.current)
|
|
@@ -156,11 +161,10 @@ export function useDomGeometryCommit({
|
|
|
156
161
|
const run = commit();
|
|
157
162
|
pending.add(run);
|
|
158
163
|
try {
|
|
159
|
-
await run;
|
|
164
|
+
return { ok: true, changed: (await run)?.changed === true };
|
|
160
165
|
} finally {
|
|
161
166
|
pending.delete(run);
|
|
162
167
|
}
|
|
163
|
-
return { ok: true };
|
|
164
168
|
},
|
|
165
169
|
[pending, showToast],
|
|
166
170
|
);
|
|
@@ -305,6 +305,7 @@ describe("Reset after a plain move puts the author's translate back", () => {
|
|
|
305
305
|
async (_selection, patches) => {
|
|
306
306
|
project.history.push(project.file);
|
|
307
307
|
project.file = patches.reduce((html, op) => applyPatch(html, "layer", op), project.file);
|
|
308
|
+
return undefined;
|
|
308
309
|
},
|
|
309
310
|
);
|
|
310
311
|
const selection = { id: "layer", selector: "#layer", element } as unknown as DomEditSelection;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
2
|
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
2
3
|
import { useCallback } from "react";
|
|
3
4
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
@@ -39,7 +40,7 @@ export interface UseDomGeometryCommitsParams {
|
|
|
39
40
|
skipRefresh?: boolean;
|
|
40
41
|
deferRender?: boolean;
|
|
41
42
|
},
|
|
42
|
-
) => Promise<
|
|
43
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
43
44
|
readOnlyPreview: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -73,7 +74,7 @@ export function useDomGeometryCommits({
|
|
|
73
74
|
restore?: () => void,
|
|
74
75
|
undoKey?: string,
|
|
75
76
|
) => {
|
|
76
|
-
if (readOnlyPreview) return Promise.resolve();
|
|
77
|
+
if (readOnlyPreview) return Promise.resolve(undefined);
|
|
77
78
|
const element = selection.element;
|
|
78
79
|
const beforeSize = captureStudioBoxSize(element);
|
|
79
80
|
const beforeOffset = captureStudioPathOffset(element);
|
|
@@ -334,3 +334,29 @@ describe("useDomSelection", () => {
|
|
|
334
334
|
harness.cleanup();
|
|
335
335
|
});
|
|
336
336
|
});
|
|
337
|
+
|
|
338
|
+
it("reports unchanged marquee membership and one new additive member", () => {
|
|
339
|
+
const { harness, card, chip } = renderCardAndChip();
|
|
340
|
+
try {
|
|
341
|
+
act(() => harness.current().applyDomSelection(card));
|
|
342
|
+
let receipt!: ReturnType<ReturnType<typeof useDomSelection>["applyMarqueeSelection"]>;
|
|
343
|
+
act(() => {
|
|
344
|
+
receipt = harness.current().applyMarqueeSelection([chip], true);
|
|
345
|
+
});
|
|
346
|
+
expect(receipt).toEqual({ changed: true, count: 2 });
|
|
347
|
+
act(() => {
|
|
348
|
+
receipt = harness.current().applyMarqueeSelection([card, chip], true);
|
|
349
|
+
});
|
|
350
|
+
expect(receipt).toEqual({ changed: false, count: 2 });
|
|
351
|
+
act(() => {
|
|
352
|
+
receipt = harness.current().applyMarqueeSelection([], true);
|
|
353
|
+
});
|
|
354
|
+
expect(receipt).toEqual({ changed: false, count: 2 });
|
|
355
|
+
act(() => {
|
|
356
|
+
receipt = harness.current().applyMarqueeSelection([], false);
|
|
357
|
+
});
|
|
358
|
+
expect(receipt).toEqual({ changed: true, count: 0 });
|
|
359
|
+
} finally {
|
|
360
|
+
harness.cleanup();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
@@ -487,12 +487,13 @@ export function useDomSelection({
|
|
|
487
487
|
// fallow-ignore-next-line complexity
|
|
488
488
|
(selections: DomEditSelection[], additive: boolean) => {
|
|
489
489
|
logSelect("marquee", { hits: selections.length, additive });
|
|
490
|
+
const current = domEditSelectionRef.current;
|
|
491
|
+
const currentGroup = domEditGroupSelectionsRef.current;
|
|
490
492
|
if (selections.length === 0) {
|
|
493
|
+
const count = seedDomEditGroupWithSelection(currentGroup, current).length;
|
|
491
494
|
if (!additive) applyDomSelection(null, { revealPanel: false });
|
|
492
|
-
return;
|
|
495
|
+
return { changed: !additive && count > 0, count: additive ? count : 0 };
|
|
493
496
|
}
|
|
494
|
-
const current = domEditSelectionRef.current;
|
|
495
|
-
const currentGroup = domEditGroupSelectionsRef.current;
|
|
496
497
|
let nextGroup: DomEditSelection[];
|
|
497
498
|
if (additive) {
|
|
498
499
|
nextGroup = seedDomEditGroupWithSelection(currentGroup, current);
|
|
@@ -507,11 +508,17 @@ export function useDomSelection({
|
|
|
507
508
|
}
|
|
508
509
|
}
|
|
509
510
|
const nextSelection = additive && current ? current : selections[0];
|
|
511
|
+
const previous = seedDomEditGroupWithSelection(currentGroup, current);
|
|
512
|
+
const changed =
|
|
513
|
+
previous.length !== nextGroup.length ||
|
|
514
|
+
nextGroup.some((selection) => !domEditSelectionInGroup(previous, selection)) ||
|
|
515
|
+
!domEditSelectionsTargetSame(current, nextSelection);
|
|
510
516
|
domEditSelectionRef.current = nextSelection;
|
|
511
517
|
domEditGroupSelectionsRef.current = nextGroup;
|
|
512
518
|
setDomEditSelection(nextSelection);
|
|
513
519
|
setDomEditGroupSelections(nextGroup);
|
|
514
520
|
announceTimelineSelection(nextGroup, nextSelection, true);
|
|
521
|
+
return { changed, count: nextGroup.length };
|
|
515
522
|
},
|
|
516
523
|
[applyDomSelection, announceTimelineSelection],
|
|
517
524
|
);
|
|
@@ -3,6 +3,11 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|
|
3
3
|
import type { SelectElementOptions, TimelineElement } from "../player";
|
|
4
4
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
5
5
|
|
|
6
|
+
export interface DomSelectionResult {
|
|
7
|
+
changed: boolean;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface ApplyDomSelectionOptions {
|
|
7
12
|
revealPanel?: boolean;
|
|
8
13
|
additive?: boolean;
|
|
@@ -85,5 +90,5 @@ export interface UseDomSelectionReturn {
|
|
|
85
90
|
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
86
91
|
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
87
92
|
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
88
|
-
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) =>
|
|
93
|
+
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => DomSelectionResult;
|
|
89
94
|
}
|
|
@@ -4,7 +4,7 @@ import { createRoot } from "react-dom/client";
|
|
|
4
4
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
7
|
+
import { afterEach, beforeEach, expect, it, vi, type Mock } from "vitest";
|
|
8
8
|
import {
|
|
9
9
|
createStudioApi,
|
|
10
10
|
openProjectHistory,
|
|
@@ -168,6 +168,20 @@ it("an undo pressed while a save is still running waits for the server instead o
|
|
|
168
168
|
expect(s.box()).toBe("10px");
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it("an undo waits for a save queued before its key, not one queued after", async () => {
|
|
172
|
+
const s = await studio();
|
|
173
|
+
await s.edit();
|
|
174
|
+
let land!: () => void;
|
|
175
|
+
void s.persistence().queueDomEditSave(() => new Promise<void>((resolve) => (land = resolve)));
|
|
176
|
+
await vi.waitFor(() => expect(land).toBeTypeOf("function"));
|
|
177
|
+
|
|
178
|
+
const undone = s.actions().undo();
|
|
179
|
+
void s.persistence().queueDomEditSave(() => new Promise<void>(() => {}));
|
|
180
|
+
land();
|
|
181
|
+
await act(() => undone);
|
|
182
|
+
expect(s.file()).toBe(BEFORE);
|
|
183
|
+
});
|
|
184
|
+
|
|
171
185
|
it("an undo after an outside write ends with the preview showing the file the server restored", async () => {
|
|
172
186
|
const s = await studio();
|
|
173
187
|
await s.edit();
|
|
@@ -202,10 +216,8 @@ function Nudge({ target, save }: { target: HTMLElement; save: () => Promise<void
|
|
|
202
216
|
return null;
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await s.edit();
|
|
208
|
-
resetNudgeKeys();
|
|
219
|
+
/** A nudge save that waits for `finish()`, then writes and records the nudge as the real save does. */
|
|
220
|
+
function queuedNudgeSave(s: Awaited<ReturnType<typeof studio>>) {
|
|
209
221
|
let finish!: () => void;
|
|
210
222
|
const save = vi.fn(async () => {
|
|
211
223
|
await new Promise<void>((resolve) => (finish = resolve));
|
|
@@ -215,6 +227,24 @@ it("an undo pressed while a nudge waits for more keys never shows the move befor
|
|
|
215
227
|
files: { "index.html": { before: AFTER, after: NUDGED } },
|
|
216
228
|
});
|
|
217
229
|
});
|
|
230
|
+
return { save, finish: () => finish() };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Mounts the nudge, presses ArrowRight once and waits for its save; returns the nudge's translate reader. */
|
|
234
|
+
async function pressNudge(s: Awaited<ReturnType<typeof studio>>, save: Mock<() => Promise<void>>) {
|
|
235
|
+
s.mount(createElement(Nudge, { target: s.element("box"), save }));
|
|
236
|
+
act(() => {
|
|
237
|
+
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
|
|
238
|
+
});
|
|
239
|
+
await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1));
|
|
240
|
+
return () => s.element("box").style.getPropertyValue("translate");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it("an undo pressed while a nudge waits for more keys never shows the move before it undone", async () => {
|
|
244
|
+
const s = await studio();
|
|
245
|
+
await s.edit();
|
|
246
|
+
resetNudgeKeys();
|
|
247
|
+
const { save, finish } = queuedNudgeSave(s);
|
|
218
248
|
s.mount(createElement(Nudge, { target: s.element("box"), save }));
|
|
219
249
|
act(() => {
|
|
220
250
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
|
|
@@ -240,21 +270,8 @@ it("an undo pressed while a nudge's save is queued shows the nudge undone in the
|
|
|
240
270
|
const s = await studio();
|
|
241
271
|
await s.edit();
|
|
242
272
|
resetNudgeKeys();
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
await new Promise<void>((resolve) => (finish = resolve));
|
|
246
|
-
writeFileSync(s.path, NUDGED);
|
|
247
|
-
await s.history().recordEdit({
|
|
248
|
-
label: "Move layer",
|
|
249
|
-
files: { "index.html": { before: AFTER, after: NUDGED } },
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
s.mount(createElement(Nudge, { target: s.element("box"), save }));
|
|
253
|
-
act(() => {
|
|
254
|
-
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
|
|
255
|
-
});
|
|
256
|
-
await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1));
|
|
257
|
-
const nudge = () => s.element("box").style.getPropertyValue("translate");
|
|
273
|
+
const { save, finish } = queuedNudgeSave(s);
|
|
274
|
+
const nudge = await pressNudge(s, save);
|
|
258
275
|
expect(nudge()).not.toBe("");
|
|
259
276
|
|
|
260
277
|
const undone = s.actions().undo();
|
|
@@ -267,6 +284,51 @@ it("an undo pressed while a nudge's save is queued shows the nudge undone in the
|
|
|
267
284
|
expect(s.box()).toBe("50px");
|
|
268
285
|
});
|
|
269
286
|
|
|
287
|
+
/** A nudge whose save is queued and then rejects, and the history requests the undo sends. */
|
|
288
|
+
async function failingNudge(s: Awaited<ReturnType<typeof studio>>) {
|
|
289
|
+
resetNudgeKeys();
|
|
290
|
+
let fail!: () => void;
|
|
291
|
+
const save = vi.fn(
|
|
292
|
+
() => new Promise<void>((_, reject) => (fail = () => reject(new Error("The save failed.")))),
|
|
293
|
+
);
|
|
294
|
+
const nudge = await pressNudge(s, save);
|
|
295
|
+
const steps: string[] = [];
|
|
296
|
+
const real = globalThis.fetch;
|
|
297
|
+
vi.stubGlobal("fetch", (url: string, init?: RequestInit) => {
|
|
298
|
+
if (/\/history\/(step|undo)$/.test(url)) steps.push(url);
|
|
299
|
+
return real(url, init);
|
|
300
|
+
});
|
|
301
|
+
return { fail: () => fail(), steps, nudge };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
it("an undo pressed while a nudge's save fails keeps the nudge undone and steps no older edit", async () => {
|
|
305
|
+
const s = await studio();
|
|
306
|
+
await s.edit();
|
|
307
|
+
const n = await failingNudge(s);
|
|
308
|
+
expect(n.nudge()).not.toBe("");
|
|
309
|
+
|
|
310
|
+
const undone = s.actions().undo();
|
|
311
|
+
expect(n.nudge()).toBe("");
|
|
312
|
+
n.fail();
|
|
313
|
+
await act(() => undone);
|
|
314
|
+
expect(n.steps).toEqual([]);
|
|
315
|
+
expect(s.file()).toBe(AFTER);
|
|
316
|
+
expect(s.box()).toBe("50px");
|
|
317
|
+
expect(n.nudge()).toBe("");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("an undo pressed while the only edit's save fails leaves the screen as the file, nothing put back", async () => {
|
|
321
|
+
const s = await studio();
|
|
322
|
+
s.show(BEFORE);
|
|
323
|
+
const n = await failingNudge(s);
|
|
324
|
+
|
|
325
|
+
const undone = s.actions().undo();
|
|
326
|
+
n.fail();
|
|
327
|
+
await act(() => undone);
|
|
328
|
+
expect(s.file()).toBe(BEFORE);
|
|
329
|
+
expect(n.nudge()).toBe("");
|
|
330
|
+
});
|
|
331
|
+
|
|
270
332
|
it("an undo pressed while a queued save fails undoes the edit before it, file and box alike", async () => {
|
|
271
333
|
const s = await studio();
|
|
272
334
|
await s.edit();
|
|
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
7
7
|
import { useEditHistoryActions, type EditHistoryHandle } from "./useEditHistoryActions";
|
|
8
|
+
import { trackStudioPendingEdit } from "../utils/studioPendingEdits";
|
|
8
9
|
|
|
9
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
11
|
|
|
@@ -30,6 +31,7 @@ function mount(
|
|
|
30
31
|
undo: vi.fn<EditHistoryHandle["undo"]>(async () => result),
|
|
31
32
|
redo: vi.fn<EditHistoryHandle["redo"]>(async () => result),
|
|
32
33
|
predict: () => predicted ?? null,
|
|
34
|
+
claims: () => 7,
|
|
33
35
|
};
|
|
34
36
|
const putBack = vi.fn();
|
|
35
37
|
const deps = {
|
|
@@ -60,6 +62,19 @@ const PREDICTED = { id: "e1", files: { "index.html": { previous: "B", restored:
|
|
|
60
62
|
const SERVER_FILES = { "index.html": { previous: "B", restored: "A2" } };
|
|
61
63
|
|
|
62
64
|
describe("useEditHistoryActions", () => {
|
|
65
|
+
it("asks for the edit claimed after the key's claim count only when an edit was saving at the key", async () => {
|
|
66
|
+
const { deps, actions } = mount({ ok: false, reason: "empty" });
|
|
67
|
+
await act(() => actions.undo());
|
|
68
|
+
expect(deps.editHistory.undo.mock.calls[0]![0].claimedAfter).toBeUndefined();
|
|
69
|
+
|
|
70
|
+
let land!: () => void;
|
|
71
|
+
trackStudioPendingEdit(new Promise<void>((resolve) => (land = resolve)));
|
|
72
|
+
const undone = actions.undo();
|
|
73
|
+
land();
|
|
74
|
+
await act(() => undone);
|
|
75
|
+
expect(deps.editHistory.undo.mock.calls[1]![0].claimedAfter).toBe(7);
|
|
76
|
+
});
|
|
77
|
+
|
|
63
78
|
it("corrects a shown step from the server's restore, diffed from what the preview shows", async () => {
|
|
64
79
|
const { deps, actions } = mount(
|
|
65
80
|
{ ok: true, label: "Undid: Move", paths: ["index.html"], undoes: "e1", files: SERVER_FILES },
|
|
@@ -3,7 +3,10 @@ import { useCallback, useMemo } from "react";
|
|
|
3
3
|
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
4
4
|
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
5
5
|
import type { RestoreFiles } from "../utils/gsapUndoRestore";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
hasStudioPendingEdits,
|
|
8
|
+
paintBackNewestStudioPendingEdit,
|
|
9
|
+
} from "../utils/studioPendingEdits";
|
|
7
10
|
|
|
8
11
|
interface HistoryResult {
|
|
9
12
|
ok: boolean;
|
|
@@ -18,11 +21,13 @@ interface HistoryResult {
|
|
|
18
21
|
interface HistoryFileCallbacks {
|
|
19
22
|
readFile: (path: string) => Promise<string>;
|
|
20
23
|
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
24
|
+
claimedAfter?: number;
|
|
21
25
|
}
|
|
22
26
|
export interface EditHistoryHandle {
|
|
23
27
|
undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
24
28
|
redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
25
29
|
predict?: (direction: "undo" | "redo") => { id: string; files: RestoreFiles } | null;
|
|
30
|
+
claims?: () => number;
|
|
26
31
|
state: {
|
|
27
32
|
undo: ReadonlyArray<{ createdAt: number }>;
|
|
28
33
|
redo: ReadonlyArray<{ createdAt: number }>;
|
|
@@ -30,7 +35,7 @@ export interface EditHistoryHandle {
|
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface UseEditHistoryActionsOptions {
|
|
33
|
-
editHistory: Pick<EditHistoryHandle, "undo" | "redo" | "predict">;
|
|
38
|
+
editHistory: Pick<EditHistoryHandle, "undo" | "redo" | "predict" | "claims">;
|
|
34
39
|
readOptionalProjectFile: (path: string) => Promise<string>;
|
|
35
40
|
readProjectFile: (path: string) => Promise<string>;
|
|
36
41
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
@@ -77,22 +82,28 @@ export function useEditHistoryActions({
|
|
|
77
82
|
const predicted = editHistory.predict?.(direction) ?? null;
|
|
78
83
|
const predictedShown = predicted ? (showHistoryRestoreNow?.(predicted.files) ?? null) : null;
|
|
79
84
|
const pendingEditShown =
|
|
80
|
-
!predictedShown && direction === "undo" ?
|
|
81
|
-
const putBack = predictedShown ?? pendingEditShown;
|
|
85
|
+
!predictedShown && direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
|
|
86
|
+
const putBack = predictedShown ?? pendingEditShown?.showAgain;
|
|
87
|
+
const claimedAfter =
|
|
88
|
+
direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : undefined;
|
|
82
89
|
let result: HistoryResult = { ok: false, reason: "failed" };
|
|
83
90
|
let serverSteppedShown = false;
|
|
91
|
+
let revertIsTheUndo = false;
|
|
84
92
|
try {
|
|
85
93
|
await waitForPendingDomEditSaves();
|
|
94
|
+
revertIsTheUndo = Boolean(pendingEditShown && !(await pendingEditShown.landed()));
|
|
95
|
+
if (revertIsTheUndo) return;
|
|
86
96
|
result = await editHistory[direction]({
|
|
87
97
|
readFile: readHistoryFile,
|
|
88
98
|
serialize: serializeHistoryFiles,
|
|
99
|
+
claimedAfter,
|
|
89
100
|
});
|
|
90
101
|
const stepped = Boolean(result.ok && result.label);
|
|
91
102
|
serverSteppedShown = predictedShown
|
|
92
103
|
? stepped && result.undoes === predicted?.id
|
|
93
104
|
: stepped && Boolean(pendingEditShown);
|
|
94
105
|
} finally {
|
|
95
|
-
if (putBack && !serverSteppedShown) putBack();
|
|
106
|
+
if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
|
|
96
107
|
}
|
|
97
108
|
if (!result.ok && result.reason === "content-mismatch") {
|
|
98
109
|
showToast(
|
|
@@ -24,6 +24,7 @@ import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditComm
|
|
|
24
24
|
import { domEditCommitDeclined, type DomEditCommitOutcome } from "./domEditCommitRunner";
|
|
25
25
|
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
|
|
26
26
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
27
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
27
28
|
|
|
28
29
|
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
29
30
|
/** Route delete through SDK when session resolves the hf-id. */
|
|
@@ -156,7 +157,7 @@ export function useElementLifecycleOps({
|
|
|
156
157
|
writeFile: writeProjectFile,
|
|
157
158
|
recordEdit: editHistory.recordEdit,
|
|
158
159
|
rewrite: async (originalContent) => {
|
|
159
|
-
const removeResponse = await
|
|
160
|
+
const removeResponse = await studioApiFetch(
|
|
160
161
|
buildProjectApiPath(
|
|
161
162
|
pid,
|
|
162
163
|
`/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
|
|
@@ -314,7 +314,7 @@ describe("applyArcKeyframeAtPlayhead", () => {
|
|
|
314
314
|
],
|
|
315
315
|
ease: "none",
|
|
316
316
|
},
|
|
317
|
-
{ label: "Add keyframe", softReload: true },
|
|
317
|
+
{ label: "Add keyframe", softReload: true, keyframeAction: "add" },
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
320
|
|
|
@@ -336,7 +336,7 @@ describe("applyArcKeyframeAtPlayhead", () => {
|
|
|
336
336
|
duration: 10,
|
|
337
337
|
keyframes: expect.arrayContaining([{ percentage: 25, properties: { x: 25, y: 25 } }]),
|
|
338
338
|
}),
|
|
339
|
-
{ label: "Add keyframe", softReload: true },
|
|
339
|
+
{ label: "Add keyframe", softReload: true, keyframeAction: "add" },
|
|
340
340
|
);
|
|
341
341
|
});
|
|
342
342
|
});
|
|
@@ -378,7 +378,10 @@ function makeElementSelection(): DomEditSelection {
|
|
|
378
378
|
} as DomEditSelection;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
function stubFlatTweenConversion(
|
|
381
|
+
function stubFlatTweenConversion(
|
|
382
|
+
id: string,
|
|
383
|
+
easeEach?: string,
|
|
384
|
+
): {
|
|
382
385
|
flat: GsapAnimation;
|
|
383
386
|
converted: GsapAnimation;
|
|
384
387
|
} {
|
|
@@ -393,6 +396,7 @@ function stubFlatTweenConversion(id: string): {
|
|
|
393
396
|
{ percentage: 0, properties: { x: 0 } },
|
|
394
397
|
{ percentage: 100, properties: { x: 10 } },
|
|
395
398
|
],
|
|
399
|
+
...(easeEach && { easeEach }),
|
|
396
400
|
},
|
|
397
401
|
});
|
|
398
402
|
flatTweenResponses(flat, converted);
|
|
@@ -437,6 +441,33 @@ describe("useEnableKeyframes — flat tween transaction", () => {
|
|
|
437
441
|
expect(convertOptions?.coalesceKey).toEqual(expect.any(String));
|
|
438
442
|
});
|
|
439
443
|
|
|
444
|
+
it("keeps the converted tween's easeEach when it extends it to the playhead", async () => {
|
|
445
|
+
window.location.hash = "#/project/test-project";
|
|
446
|
+
usePlayerStore.setState({ currentTime: 3 });
|
|
447
|
+
const { flat } = stubFlatTweenConversion("flat-1", "none");
|
|
448
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
449
|
+
const enable = renderEnableKeyframes({
|
|
450
|
+
domEditSelection: makeElementSelection(),
|
|
451
|
+
selectedGsapAnimations: [flat],
|
|
452
|
+
previewIframeRef: {
|
|
453
|
+
current: {
|
|
454
|
+
contentWindow: { gsap: { getProperty: () => 10 } },
|
|
455
|
+
} as unknown as HTMLIFrameElement,
|
|
456
|
+
},
|
|
457
|
+
handleGsapAddAnimation: vi.fn(),
|
|
458
|
+
handleGsapConvertToKeyframes: vi.fn(async () => undefined),
|
|
459
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
460
|
+
commitMutation,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await act(async () => enable());
|
|
464
|
+
|
|
465
|
+
expect(commitMutation.mock.calls[0]?.[0]).toMatchObject({
|
|
466
|
+
type: "replace-with-keyframes",
|
|
467
|
+
easeEach: "none",
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
440
471
|
it("passes the convert coalesce key to an inside-range batch edit", async () => {
|
|
441
472
|
window.location.hash = "#/project/test-project";
|
|
442
473
|
usePlayerStore.setState({ currentTime: 1.5 });
|
|
@@ -15,6 +15,7 @@ import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCa
|
|
|
15
15
|
import {
|
|
16
16
|
existingTweenTargetSelector,
|
|
17
17
|
computeElementPercentage,
|
|
18
|
+
keyframeEases,
|
|
18
19
|
KEYFRAME_PCT_MATCH,
|
|
19
20
|
isInstantHold,
|
|
20
21
|
resolveEditableTweenDuration,
|
|
@@ -159,7 +160,7 @@ async function replaceSetWithSingleKeyframe(
|
|
|
159
160
|
keyframes: [{ percentage: 0, properties: position }],
|
|
160
161
|
ease: setAnim.ease,
|
|
161
162
|
},
|
|
162
|
-
{ label: "Enable keyframes", softReload: true },
|
|
163
|
+
{ label: "Enable keyframes", keyframeAction: "add", softReload: true },
|
|
163
164
|
);
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -266,10 +267,11 @@ async function extendKeyframedTweenToPlayhead(
|
|
|
266
267
|
position: extended.position,
|
|
267
268
|
duration: extended.duration,
|
|
268
269
|
keyframes: extended.keyframes,
|
|
269
|
-
|
|
270
|
+
...keyframeEases(anim),
|
|
270
271
|
},
|
|
271
272
|
{
|
|
272
273
|
label: "Add keyframe",
|
|
274
|
+
keyframeAction: "add",
|
|
273
275
|
softReload: true,
|
|
274
276
|
...commitOverrides,
|
|
275
277
|
},
|
|
@@ -377,7 +379,7 @@ export async function promoteSetToKeyframes(
|
|
|
377
379
|
],
|
|
378
380
|
ease: setAnim.ease,
|
|
379
381
|
},
|
|
380
|
-
{ label: "Add keyframe", softReload: true },
|
|
382
|
+
{ label: "Add keyframe", keyframeAction: "add", softReload: true },
|
|
381
383
|
);
|
|
382
384
|
}
|
|
383
385
|
|
|
@@ -454,7 +456,7 @@ export async function applyArcKeyframeAtPlayhead(
|
|
|
454
456
|
}),
|
|
455
457
|
ease: "none",
|
|
456
458
|
},
|
|
457
|
-
{ label: "Add keyframe", softReload: true },
|
|
459
|
+
{ label: "Add keyframe", keyframeAction: "add", softReload: true },
|
|
458
460
|
);
|
|
459
461
|
}
|
|
460
462
|
|
|
@@ -503,6 +505,7 @@ export function useEnableKeyframes(
|
|
|
503
505
|
enableKeyframesTransactionCounter += 1;
|
|
504
506
|
const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
|
|
505
507
|
const convertCommitOverrides: Partial<CommitMutationOptions> = {
|
|
508
|
+
keyframeTelemetry: false,
|
|
506
509
|
skipReload: true,
|
|
507
510
|
coalesceKey,
|
|
508
511
|
coalesceMs: Number.POSITIVE_INFINITY,
|