@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
|
@@ -1,42 +1,26 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { usePlayerStore } from "../player";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
readProjectFileContent,
|
|
5
|
+
saveProjectFilesWithHistory,
|
|
6
|
+
type DomEditCommitBaseParams,
|
|
7
|
+
} from "../utils/studioFileHistory";
|
|
4
8
|
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
|
5
9
|
import {
|
|
6
10
|
buildDomEditPatchTarget,
|
|
7
11
|
readHfId,
|
|
8
12
|
type DomEditSelection,
|
|
9
13
|
} from "../components/editor/domEditing";
|
|
10
|
-
import type {
|
|
11
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
12
|
-
|
|
13
|
-
interface RecordEditInput {
|
|
14
|
-
label: string;
|
|
15
|
-
kind: EditHistoryKind;
|
|
16
|
-
coalesceKey?: string;
|
|
17
|
-
files: Record<string, { before: string; after: string }>;
|
|
18
|
-
}
|
|
14
|
+
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
|
|
19
15
|
|
|
20
|
-
interface UseElementLifecycleOpsParams {
|
|
21
|
-
activeCompPath: string | null;
|
|
22
|
-
showToast: (message: string, tone?: "error" | "info") => void;
|
|
23
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
24
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
25
|
-
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
26
|
-
projectIdRef: React.MutableRefObject<string | null>;
|
|
27
|
-
reloadPreview: () => void;
|
|
28
|
-
clearDomSelection: () => void;
|
|
16
|
+
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
29
17
|
/** Route delete through SDK when session resolves the hf-id; returns true if handled. */
|
|
30
18
|
onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
|
|
31
19
|
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
|
|
32
20
|
onReorderShadow?: (targets: string[]) => void;
|
|
33
21
|
/** Resync the SDK session after a server-fallback delete. */
|
|
34
22
|
forceReloadSdkSession?: () => void;
|
|
35
|
-
|
|
36
|
-
selection: DomEditSelection,
|
|
37
|
-
patches: PatchOperation[],
|
|
38
|
-
options: { label: string; coalesceKey: string; skipRefresh?: boolean },
|
|
39
|
-
) => Promise<void>;
|
|
23
|
+
commitDomEditPatchBatches: CommitDomEditPatchBatches;
|
|
40
24
|
/** Stage 7 Step 3b: called after a successful server-side element delete (shadow). */
|
|
41
25
|
onElementDeleted?: (selection: DomEditSelection) => void;
|
|
42
26
|
}
|
|
@@ -53,7 +37,7 @@ export function useElementLifecycleOps({
|
|
|
53
37
|
onTrySdkDelete,
|
|
54
38
|
onReorderShadow,
|
|
55
39
|
forceReloadSdkSession,
|
|
56
|
-
|
|
40
|
+
commitDomEditPatchBatches,
|
|
57
41
|
onElementDeleted,
|
|
58
42
|
}: UseElementLifecycleOpsParams) {
|
|
59
43
|
// fallow-ignore-next-line complexity
|
|
@@ -66,17 +50,7 @@ export function useElementLifecycleOps({
|
|
|
66
50
|
|
|
67
51
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
68
52
|
try {
|
|
69
|
-
const
|
|
70
|
-
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
71
|
-
);
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const data = (await response.json()) as { content?: string };
|
|
77
|
-
const originalContent = data.content;
|
|
78
|
-
if (typeof originalContent !== "string")
|
|
79
|
-
throw new Error(`Missing file contents for ${targetPath}`);
|
|
53
|
+
const originalContent = await readProjectFileContent(pid, targetPath);
|
|
80
54
|
|
|
81
55
|
const patchTarget = buildDomEditPatchTarget(selection);
|
|
82
56
|
if (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId) {
|
|
@@ -156,8 +130,8 @@ export function useElementLifecycleOps({
|
|
|
156
130
|
],
|
|
157
131
|
);
|
|
158
132
|
|
|
159
|
-
// ponytail: z-index reorder
|
|
160
|
-
//
|
|
133
|
+
// ponytail: z-index reorder folds every element patch for a source file in memory and sends
|
|
134
|
+
// one patch-elements-batch request, so each file is persisted with one atomic disk write.
|
|
161
135
|
// No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
|
|
162
136
|
const handleDomZIndexReorderCommit = useCallback(
|
|
163
137
|
// fallow-ignore-next-line complexity
|
|
@@ -171,6 +145,7 @@ export function useElementLifecycleOps({
|
|
|
171
145
|
sourceFile: string;
|
|
172
146
|
key?: string;
|
|
173
147
|
}>,
|
|
148
|
+
gestureCoalesceKey?: string,
|
|
174
149
|
) => {
|
|
175
150
|
if (entries.length === 0) return Promise.resolve();
|
|
176
151
|
// Resolver shadow (telemetry-only, decoupled from cutover): record whether
|
|
@@ -178,11 +153,12 @@ export function useElementLifecycleOps({
|
|
|
178
153
|
onReorderShadow?.(
|
|
179
154
|
entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
|
|
180
155
|
);
|
|
181
|
-
const coalesceKey =
|
|
182
|
-
|
|
156
|
+
const coalesceKey =
|
|
157
|
+
gestureCoalesceKey ??
|
|
158
|
+
`z-reorder:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
|
|
159
|
+
const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
|
|
183
160
|
const rollbacks: Array<() => void> = [];
|
|
184
|
-
for (
|
|
185
|
-
const entry = entries[i];
|
|
161
|
+
for (const entry of entries) {
|
|
186
162
|
const priorZIndex = entry.element.style.zIndex;
|
|
187
163
|
const priorPosition = entry.element.style.position;
|
|
188
164
|
const priorStoreEntry = entry.key
|
|
@@ -218,39 +194,32 @@ export function useElementLifecycleOps({
|
|
|
218
194
|
});
|
|
219
195
|
}
|
|
220
196
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
label: "Reorder layers",
|
|
234
|
-
coalesceKey,
|
|
235
|
-
skipRefresh: i < entries.length - 1,
|
|
236
|
-
},
|
|
237
|
-
),
|
|
238
|
-
);
|
|
197
|
+
const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
|
|
198
|
+
filePatches.push({
|
|
199
|
+
target: buildDomEditPatchTarget({
|
|
200
|
+
id: entry.id,
|
|
201
|
+
hfId: readHfId(entry.element),
|
|
202
|
+
selector: entry.selector,
|
|
203
|
+
selectorIndex: entry.selectorIndex,
|
|
204
|
+
}),
|
|
205
|
+
operations: patches,
|
|
206
|
+
});
|
|
207
|
+
patchesBySourceFile.set(entry.sourceFile, filePatches);
|
|
239
208
|
}
|
|
240
|
-
|
|
209
|
+
const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
|
|
210
|
+
sourceFile,
|
|
211
|
+
patches,
|
|
212
|
+
}));
|
|
213
|
+
// Resolves once every source-file batch is persisted so a same-file timing write
|
|
241
214
|
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
245
|
-
);
|
|
246
|
-
if (rejected) {
|
|
215
|
+
return commitDomEditPatchBatches(batches, { label: "Reorder layers", coalesceKey }).catch(
|
|
216
|
+
(error) => {
|
|
247
217
|
for (const rollback of rollbacks) rollback();
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
});
|
|
218
|
+
throw error;
|
|
219
|
+
},
|
|
220
|
+
);
|
|
252
221
|
},
|
|
253
|
-
[
|
|
222
|
+
[commitDomEditPatchBatches, onReorderShadow],
|
|
254
223
|
);
|
|
255
224
|
|
|
256
225
|
return {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
7
|
import {
|
|
@@ -7,8 +10,20 @@ import {
|
|
|
7
10
|
isPlayheadWithinTween,
|
|
8
11
|
promoteSetToKeyframes,
|
|
9
12
|
resolveNewTweenRange,
|
|
13
|
+
useEnableKeyframes,
|
|
10
14
|
type EnableKeyframesSession,
|
|
11
15
|
} from "./useEnableKeyframes";
|
|
16
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
17
|
+
|
|
18
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
19
|
+
|
|
20
|
+
let cleanup: (() => void) | null = null;
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup?.();
|
|
23
|
+
cleanup = null;
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
window.location.hash = "";
|
|
26
|
+
});
|
|
12
27
|
|
|
13
28
|
function anim(overrides: Partial<GsapAnimation>): GsapAnimation {
|
|
14
29
|
return {
|
|
@@ -203,3 +218,129 @@ describe("promoteSetToKeyframes — auto endpoint", () => {
|
|
|
203
218
|
expect(kfs[0].percentage).toBe(0);
|
|
204
219
|
});
|
|
205
220
|
});
|
|
221
|
+
|
|
222
|
+
function renderEnableKeyframes(session: EnableKeyframesSession): () => Promise<void> {
|
|
223
|
+
let enable: (() => Promise<void>) | null = null;
|
|
224
|
+
function Probe() {
|
|
225
|
+
const sessionRef = useRef<EnableKeyframesSession | undefined>(session);
|
|
226
|
+
enable = useEnableKeyframes(sessionRef);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const container = document.createElement("div");
|
|
230
|
+
const root = createRoot(container);
|
|
231
|
+
act(() => root.render(createElement(Probe)));
|
|
232
|
+
cleanup = () => act(() => root.unmount());
|
|
233
|
+
if (!enable) throw new Error("hook did not initialize");
|
|
234
|
+
return enable;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function flatTweenResponses(flat: GsapAnimation, converted: GsapAnimation) {
|
|
238
|
+
const responses = [{ animations: [flat] }, { animations: [converted] }];
|
|
239
|
+
vi.stubGlobal(
|
|
240
|
+
"fetch",
|
|
241
|
+
vi.fn(async () => ({
|
|
242
|
+
ok: true,
|
|
243
|
+
json: async () => responses.shift() ?? { animations: [] },
|
|
244
|
+
})),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function makeElementSelection(): DomEditSelection {
|
|
249
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
250
|
+
element.id = "el";
|
|
251
|
+
return {
|
|
252
|
+
id: "el",
|
|
253
|
+
selector: "#el",
|
|
254
|
+
sourceFile: "index.html",
|
|
255
|
+
element,
|
|
256
|
+
} as DomEditSelection;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stubFlatTweenConversion(id: string): {
|
|
260
|
+
flat: GsapAnimation;
|
|
261
|
+
converted: GsapAnimation;
|
|
262
|
+
} {
|
|
263
|
+
const flat = anim({ id, position: 1, duration: 1, properties: { x: 10 } });
|
|
264
|
+
const converted = anim({
|
|
265
|
+
id,
|
|
266
|
+
position: 1,
|
|
267
|
+
duration: 1,
|
|
268
|
+
keyframes: {
|
|
269
|
+
format: "object-array",
|
|
270
|
+
keyframes: [
|
|
271
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
272
|
+
{ percentage: 100, properties: { x: 10 } },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
flatTweenResponses(flat, converted);
|
|
277
|
+
return { flat, converted };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
describe("useEnableKeyframes — flat tween transaction", () => {
|
|
281
|
+
it("skips the convert reload and coalesces an outside-range terminal soft reload", async () => {
|
|
282
|
+
window.location.hash = "#/project/test-project";
|
|
283
|
+
usePlayerStore.setState({ currentTime: 3 });
|
|
284
|
+
const selection = makeElementSelection();
|
|
285
|
+
const { flat } = stubFlatTweenConversion("flat-1");
|
|
286
|
+
const handleConvert = vi.fn(async () => undefined);
|
|
287
|
+
const commitMutation = vi.fn(async () => undefined);
|
|
288
|
+
const enable = renderEnableKeyframes({
|
|
289
|
+
domEditSelection: selection,
|
|
290
|
+
selectedGsapAnimations: [flat],
|
|
291
|
+
previewIframeRef: {
|
|
292
|
+
current: {
|
|
293
|
+
contentWindow: { gsap: { getProperty: () => 10 } },
|
|
294
|
+
} as unknown as HTMLIFrameElement,
|
|
295
|
+
},
|
|
296
|
+
handleGsapAddAnimation: vi.fn(),
|
|
297
|
+
handleGsapConvertToKeyframes: handleConvert,
|
|
298
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
299
|
+
commitMutation,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await act(async () => enable());
|
|
303
|
+
|
|
304
|
+
const convertOptions = handleConvert.mock.calls[0]?.[3];
|
|
305
|
+
const terminalOptions = commitMutation.mock.calls[0]?.[1];
|
|
306
|
+
expect(convertOptions).toMatchObject({ skipReload: true, coalesceMs: Infinity });
|
|
307
|
+
expect(terminalOptions).toMatchObject({
|
|
308
|
+
softReload: true,
|
|
309
|
+
coalesceKey: convertOptions?.coalesceKey,
|
|
310
|
+
});
|
|
311
|
+
expect(convertOptions?.coalesceKey).toEqual(expect.any(String));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("passes the convert coalesce key to an inside-range batch edit", async () => {
|
|
315
|
+
window.location.hash = "#/project/test-project";
|
|
316
|
+
usePlayerStore.setState({ currentTime: 1.5 });
|
|
317
|
+
const selection = makeElementSelection();
|
|
318
|
+
const { flat } = stubFlatTweenConversion("flat-2");
|
|
319
|
+
const handleConvert = vi.fn(async () => undefined);
|
|
320
|
+
const addKeyframeBatch = vi.fn(async () => undefined);
|
|
321
|
+
const enable = renderEnableKeyframes({
|
|
322
|
+
domEditSelection: selection,
|
|
323
|
+
selectedGsapAnimations: [flat],
|
|
324
|
+
previewIframeRef: {
|
|
325
|
+
current: {
|
|
326
|
+
contentWindow: { gsap: { getProperty: () => 5 } },
|
|
327
|
+
} as unknown as HTMLIFrameElement,
|
|
328
|
+
},
|
|
329
|
+
handleGsapAddAnimation: vi.fn(),
|
|
330
|
+
handleGsapConvertToKeyframes: handleConvert,
|
|
331
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
332
|
+
handleGsapAddKeyframeBatch: addKeyframeBatch,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await act(async () => enable());
|
|
336
|
+
|
|
337
|
+
const convertOptions = handleConvert.mock.calls[0]?.[3];
|
|
338
|
+
expect(addKeyframeBatch.mock.calls[0]?.[3]).toEqual({
|
|
339
|
+
softReload: true,
|
|
340
|
+
coalesceKey: convertOptions?.coalesceKey,
|
|
341
|
+
// Must carry the convert phase's infinite window so the inside-range
|
|
342
|
+
// apply folds into one undo entry regardless of round-trip latency.
|
|
343
|
+
coalesceMs: Infinity,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -12,7 +12,7 @@ import type { GsapAnimation, GsapPercentageKeyframe } from "@hyperframes/core/gs
|
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
13
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
14
14
|
import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCache";
|
|
15
|
-
import { selectorFromSelection, computeElementPercentage } from "./gsapShared";
|
|
15
|
+
import { selectorFromSelection, computeElementPercentage, isInstantHold } from "./gsapShared";
|
|
16
16
|
import {
|
|
17
17
|
resolveTweenStart,
|
|
18
18
|
resolveTweenDuration,
|
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
import { POSITION_PROPS } from "./gsapRuntimeReaders";
|
|
22
22
|
import { roundTo3 } from "../utils/rounding";
|
|
23
23
|
import { nearestPointOnPath } from "../components/editor/motionPathGeometry";
|
|
24
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
25
|
+
|
|
26
|
+
let enableKeyframesTransactionCounter = 0;
|
|
24
27
|
|
|
25
28
|
export interface EnableKeyframesSession {
|
|
26
29
|
domEditSelection: DomEditSelection | null;
|
|
@@ -30,16 +33,19 @@ export interface EnableKeyframesSession {
|
|
|
30
33
|
handleGsapConvertToKeyframes: (
|
|
31
34
|
animId: string,
|
|
32
35
|
resolvedFromValues?: Record<string, number | string>,
|
|
36
|
+
duration?: number,
|
|
37
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
33
38
|
) => void | Promise<void>;
|
|
34
39
|
handleGsapRemoveKeyframe: (animId: string, pct: number) => void;
|
|
35
40
|
handleGsapAddKeyframeBatch?: (
|
|
36
41
|
animId: string,
|
|
37
42
|
pct: number,
|
|
38
43
|
properties: Record<string, number | string>,
|
|
44
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
39
45
|
) => Promise<void>;
|
|
40
46
|
commitMutation?: (
|
|
41
47
|
mutation: Record<string, unknown>,
|
|
42
|
-
options:
|
|
48
|
+
options: CommitMutationOptions,
|
|
43
49
|
) => Promise<void>;
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -107,6 +113,36 @@ export function buildExtendedKeyframes(
|
|
|
107
113
|
return { position: roundTo3(newStart), duration: newDuration, keyframes };
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
async function replaceSetWithSingleKeyframe(
|
|
117
|
+
session: EnableKeyframesSession,
|
|
118
|
+
sel: DomEditSelection,
|
|
119
|
+
setAnim: GsapAnimation,
|
|
120
|
+
t: number,
|
|
121
|
+
iframe: HTMLIFrameElement | null,
|
|
122
|
+
selector: string,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const position = readElementPosition(iframe, sel, setAnim);
|
|
125
|
+
if (Object.keys(position).length === 0) {
|
|
126
|
+
for (const [key, held] of Object.entries(setAnim.properties ?? {})) {
|
|
127
|
+
if (typeof held === "number") position[key] = held;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(position).length === 0 || !session.commitMutation) return;
|
|
131
|
+
const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t);
|
|
132
|
+
await session.commitMutation(
|
|
133
|
+
{
|
|
134
|
+
type: "replace-with-keyframes",
|
|
135
|
+
animationId: setAnim.id,
|
|
136
|
+
targetSelector: selector,
|
|
137
|
+
position: roundTo3(range.start),
|
|
138
|
+
duration: roundTo3(range.duration),
|
|
139
|
+
keyframes: [{ percentage: 0, properties: position }],
|
|
140
|
+
ease: setAnim.ease,
|
|
141
|
+
},
|
|
142
|
+
{ label: "Enable keyframes", softReload: true },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
110
146
|
// fallow-ignore-next-line complexity
|
|
111
147
|
function readElementPosition(
|
|
112
148
|
iframe: HTMLIFrameElement | null,
|
|
@@ -199,6 +235,7 @@ async function applyKeyframeAtPlayhead(
|
|
|
199
235
|
kfAnim: GsapAnimation,
|
|
200
236
|
t: number,
|
|
201
237
|
iframe: HTMLIFrameElement | null,
|
|
238
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
202
239
|
): Promise<void> {
|
|
203
240
|
if (!isPlayheadWithinTween(kfAnim, t)) {
|
|
204
241
|
const position = readElementPosition(iframe, sel, kfAnim);
|
|
@@ -215,7 +252,11 @@ async function applyKeyframeAtPlayhead(
|
|
|
215
252
|
keyframes: extended.keyframes,
|
|
216
253
|
ease: kfAnim.ease,
|
|
217
254
|
},
|
|
218
|
-
{
|
|
255
|
+
{
|
|
256
|
+
label: "Add keyframe",
|
|
257
|
+
softReload: true,
|
|
258
|
+
...commitOverrides,
|
|
259
|
+
},
|
|
219
260
|
);
|
|
220
261
|
}
|
|
221
262
|
return;
|
|
@@ -229,7 +270,7 @@ async function applyKeyframeAtPlayhead(
|
|
|
229
270
|
if (session.handleGsapAddKeyframeBatch) {
|
|
230
271
|
const position = readElementPosition(iframe, sel, kfAnim);
|
|
231
272
|
if (Object.keys(position).length > 0) {
|
|
232
|
-
await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position);
|
|
273
|
+
await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position, commitOverrides);
|
|
233
274
|
}
|
|
234
275
|
}
|
|
235
276
|
}
|
|
@@ -259,27 +300,7 @@ export async function promoteSetToKeyframes(
|
|
|
259
300
|
// the set with a single keyframe at the playhead holding its value, matching the
|
|
260
301
|
// no-animation branch: one diamond the user can build motion from.
|
|
261
302
|
if (t <= setStart) {
|
|
262
|
-
|
|
263
|
-
if (Object.keys(position).length === 0) {
|
|
264
|
-
for (const key of Object.keys(setAnim.properties ?? {})) {
|
|
265
|
-
const held = setAnim.properties?.[key];
|
|
266
|
-
if (typeof held === "number") position[key] = held;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (Object.keys(position).length === 0) return;
|
|
270
|
-
const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t);
|
|
271
|
-
await session.commitMutation(
|
|
272
|
-
{
|
|
273
|
-
type: "replace-with-keyframes",
|
|
274
|
-
animationId: setAnim.id,
|
|
275
|
-
targetSelector: selector,
|
|
276
|
-
position: roundTo3(range.start),
|
|
277
|
-
duration: roundTo3(range.duration),
|
|
278
|
-
keyframes: [{ percentage: 0, properties: position }],
|
|
279
|
-
ease: setAnim.ease,
|
|
280
|
-
},
|
|
281
|
-
{ label: "Enable keyframes", softReload: true },
|
|
282
|
-
);
|
|
303
|
+
await replaceSetWithSingleKeyframe(session, sel, setAnim, t, iframe, selector);
|
|
283
304
|
return;
|
|
284
305
|
}
|
|
285
306
|
const endPosition = readElementPosition(iframe, sel, setAnim);
|
|
@@ -395,8 +416,8 @@ export function useEnableKeyframes(
|
|
|
395
416
|
// the curve.
|
|
396
417
|
const arcAnim = anims.find((a) => a.arcPath);
|
|
397
418
|
const kfAnim = anims.find((a) => a.keyframes && !a.arcPath);
|
|
398
|
-
const setAnim = anims.find((a) => a
|
|
399
|
-
const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && a
|
|
419
|
+
const setAnim = anims.find((a) => isInstantHold(a) && !a.keyframes && !a.arcPath);
|
|
420
|
+
const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && !isInstantHold(a));
|
|
400
421
|
|
|
401
422
|
if (arcAnim) {
|
|
402
423
|
await applyArcWaypointAtPlayhead(session, sel, arcAnim, t, iframe);
|
|
@@ -409,9 +430,32 @@ export function useEnableKeyframes(
|
|
|
409
430
|
// resolvedFromValues, so the 0%/100% stops keep the real start→end motion
|
|
410
431
|
// (passing the playhead value would flatten it). Then apply uniformly so an
|
|
411
432
|
// out-of-range playhead extends the range just like a keyframe tween.
|
|
412
|
-
|
|
433
|
+
enableKeyframesTransactionCounter += 1;
|
|
434
|
+
const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
|
|
435
|
+
const convertCommitOverrides: Partial<CommitMutationOptions> = {
|
|
436
|
+
skipReload: true,
|
|
437
|
+
coalesceKey,
|
|
438
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
439
|
+
};
|
|
440
|
+
await session.handleGsapConvertToKeyframes(
|
|
441
|
+
flatAnim.id,
|
|
442
|
+
undefined,
|
|
443
|
+
undefined,
|
|
444
|
+
convertCommitOverrides,
|
|
445
|
+
);
|
|
413
446
|
const converted = (await fetchAnimationsForElement(sel)).find((a) => a.keyframes);
|
|
414
|
-
if (converted)
|
|
447
|
+
if (converted) {
|
|
448
|
+
const applyCommitOverrides: Partial<CommitMutationOptions> = {
|
|
449
|
+
softReload: true,
|
|
450
|
+
coalesceKey,
|
|
451
|
+
// Must match the convert phase's window: editHistory keys coalescing
|
|
452
|
+
// off the incoming entry, so without Infinity here the apply (landing
|
|
453
|
+
// after two POSTs + a fetch) falls back to the 300ms default and the
|
|
454
|
+
// conversion splits into two undo entries under real latency.
|
|
455
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
456
|
+
};
|
|
457
|
+
await applyKeyframeAtPlayhead(session, sel, converted, t, iframe, applyCommitOverrides);
|
|
458
|
+
}
|
|
415
459
|
} else {
|
|
416
460
|
const position = readElementPosition(iframe, sel, null);
|
|
417
461
|
const { start: elStart, duration: elDuration } = resolveNewTweenRange(
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
+
import { usePlayerStore } from "../player";
|
|
6
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
7
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
8
|
+
import { useGestureCommit } from "./useGestureCommit";
|
|
9
|
+
|
|
10
|
+
const gestureRecording = vi.hoisted(() => ({
|
|
11
|
+
startRecording: vi.fn(),
|
|
12
|
+
stopRecording: vi.fn(() => [
|
|
13
|
+
{ time: 0, properties: { x: 0, y: 0, opacity: 1 } },
|
|
14
|
+
{ time: 0.5, properties: { x: 50, y: 25, opacity: 0.5 } },
|
|
15
|
+
{ time: 1, properties: { x: 100, y: 50, opacity: 0 } },
|
|
16
|
+
]),
|
|
17
|
+
clearSamples: vi.fn(),
|
|
18
|
+
cancelRecording: vi.fn(),
|
|
19
|
+
isRecording: false,
|
|
20
|
+
recordingDuration: 0,
|
|
21
|
+
samplesRef: { current: [] },
|
|
22
|
+
trailRef: { current: [] },
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("./useGestureRecording", () => ({
|
|
26
|
+
useGestureRecording: () => gestureRecording,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("../utils/rdpSimplify", () => ({
|
|
30
|
+
simplifyGestureSamples: () =>
|
|
31
|
+
new Map([
|
|
32
|
+
[0, { x: 0, y: 0, opacity: 1 }],
|
|
33
|
+
[50, { x: 50, y: 25, opacity: 0.5 }],
|
|
34
|
+
[100, { x: 100, y: 50, opacity: 0 }],
|
|
35
|
+
]),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../utils/gestureSmoother", () => ({
|
|
39
|
+
smoothGestureKeyframes: (keyframes: unknown) => keyframes,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("../utils/velocityEaseFitter", () => ({
|
|
43
|
+
fitEasesFromVelocity: (keyframes: unknown) => keyframes,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
47
|
+
|
|
48
|
+
let cleanup: (() => void) | null = null;
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
cleanup?.();
|
|
52
|
+
cleanup = null;
|
|
53
|
+
usePlayerStore.getState().reset();
|
|
54
|
+
document.body.replaceChildren();
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function makeSelection(element: HTMLElement): DomEditSelection {
|
|
59
|
+
return {
|
|
60
|
+
id: element.id,
|
|
61
|
+
element,
|
|
62
|
+
label: "Card",
|
|
63
|
+
tagName: "div",
|
|
64
|
+
sourceFile: "index.html",
|
|
65
|
+
compositionPath: "index.html",
|
|
66
|
+
isCompositionHost: false,
|
|
67
|
+
isInsideLockedComposition: false,
|
|
68
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
69
|
+
textContent: null,
|
|
70
|
+
dataAttributes: { start: "0", duration: "2" },
|
|
71
|
+
inlineStyles: {},
|
|
72
|
+
computedStyles: {},
|
|
73
|
+
textFields: [],
|
|
74
|
+
capabilities: {
|
|
75
|
+
canSelect: true,
|
|
76
|
+
canEditStyles: true,
|
|
77
|
+
canCrop: true,
|
|
78
|
+
canMove: true,
|
|
79
|
+
canResize: true,
|
|
80
|
+
canApplyManualOffset: true,
|
|
81
|
+
canApplyManualSize: true,
|
|
82
|
+
canApplyManualRotation: true,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("useGestureCommit", () => {
|
|
88
|
+
it("coalesces property-group commits and reloads only the terminal group", async () => {
|
|
89
|
+
const iframe = document.createElement("iframe");
|
|
90
|
+
document.body.append(iframe);
|
|
91
|
+
const element = document.createElement("div");
|
|
92
|
+
element.id = "card";
|
|
93
|
+
const commitMutation = vi.fn<
|
|
94
|
+
(mutation: Record<string, unknown>, options: CommitMutationOptions) => Promise<void>
|
|
95
|
+
>(async () => {});
|
|
96
|
+
const sessionRef = {
|
|
97
|
+
current: {
|
|
98
|
+
domEditSelection: makeSelection(element),
|
|
99
|
+
selectedGsapAnimations: [],
|
|
100
|
+
commitMutation,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const captured: { hook: ReturnType<typeof useGestureCommit> | null } = { hook: null };
|
|
104
|
+
function Probe() {
|
|
105
|
+
captured.hook = useGestureCommit({
|
|
106
|
+
domEditSessionRef: sessionRef,
|
|
107
|
+
previewIframeRef: { current: iframe },
|
|
108
|
+
showToast: vi.fn(),
|
|
109
|
+
isGestureRecordingRef: { current: false },
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const root = mountReactHarness(<Probe />);
|
|
114
|
+
cleanup = () => act(() => root.unmount());
|
|
115
|
+
if (!captured.hook) throw new Error("hook did not initialize");
|
|
116
|
+
|
|
117
|
+
act(() => captured.hook?.handleToggleRecording());
|
|
118
|
+
act(() => captured.hook?.handleToggleRecording());
|
|
119
|
+
await act(async () => {
|
|
120
|
+
await vi.waitFor(() => expect(commitMutation).toHaveBeenCalledTimes(2));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const options = commitMutation.mock.calls.map((call) => call[1]);
|
|
124
|
+
expect(new Set(options.map((entry) => entry.coalesceKey)).size).toBe(1);
|
|
125
|
+
expect(options[0]).toEqual(expect.objectContaining({ coalesceMs: Infinity, skipReload: true }));
|
|
126
|
+
expect(options[0]).not.toHaveProperty("softReload");
|
|
127
|
+
expect(options[1]).toEqual(expect.objectContaining({ coalesceMs: Infinity, softReload: true }));
|
|
128
|
+
expect(options[1]).not.toHaveProperty("skipReload");
|
|
129
|
+
});
|
|
130
|
+
});
|