@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
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./gsapKeyframeCacheHelpers";
|
|
20
20
|
import type {
|
|
21
21
|
CommitMutation,
|
|
22
|
+
CommitMutationOptions,
|
|
22
23
|
SafeGsapCommitMutation,
|
|
23
24
|
TrackGsapSaveFailure,
|
|
24
25
|
} from "./gsapScriptCommitTypes";
|
|
@@ -54,6 +55,21 @@ interface GsapKeyframeOpsParams extends SdkKeyframeDeps {
|
|
|
54
55
|
trackGsapSaveFailure: TrackGsapSaveFailure;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Translate a gesture's commit overrides into the SDK persist options. The
|
|
60
|
+
* server path's `softReload`/`skipReload` maps to the SDK's `skipRefresh`, and
|
|
61
|
+
* `coalesceKey`/`coalesceMs` must ride along so an SDK-routed edit folds into
|
|
62
|
+
* one undo entry the same way the server path does.
|
|
63
|
+
*/
|
|
64
|
+
function toSdkPersistOptions(label: string, overrides?: Partial<CommitMutationOptions>) {
|
|
65
|
+
return {
|
|
66
|
+
label,
|
|
67
|
+
coalesceKey: overrides?.coalesceKey,
|
|
68
|
+
coalesceMs: overrides?.coalesceMs,
|
|
69
|
+
skipRefresh: overrides?.skipReload,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
export function useGsapKeyframeOps({
|
|
58
74
|
activeCompPath,
|
|
59
75
|
commitMutation,
|
|
@@ -140,6 +156,7 @@ export function useGsapKeyframeOps({
|
|
|
140
156
|
animationId: string,
|
|
141
157
|
percentage: number,
|
|
142
158
|
properties: Record<string, number | string>,
|
|
159
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
143
160
|
) => {
|
|
144
161
|
if (sdkSession && sdkDeps) {
|
|
145
162
|
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -150,21 +167,30 @@ export function useGsapKeyframeOps({
|
|
|
150
167
|
properties,
|
|
151
168
|
sdkSession,
|
|
152
169
|
sdkDeps,
|
|
153
|
-
|
|
170
|
+
toSdkPersistOptions(`Add keyframe at ${percentage}%`, commitOverrides),
|
|
154
171
|
);
|
|
155
172
|
if (handled) return;
|
|
156
173
|
}
|
|
157
174
|
return commitMutation(
|
|
158
175
|
selection,
|
|
159
176
|
{ type: "add-keyframe", animationId, percentage, properties },
|
|
160
|
-
{
|
|
177
|
+
{
|
|
178
|
+
label: `Add keyframe at ${percentage}%`,
|
|
179
|
+
softReload: true,
|
|
180
|
+
...commitOverrides,
|
|
181
|
+
},
|
|
161
182
|
);
|
|
162
183
|
},
|
|
163
184
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
164
185
|
);
|
|
165
186
|
|
|
166
187
|
const removeKeyframe = useCallback(
|
|
167
|
-
(
|
|
188
|
+
(
|
|
189
|
+
selection: DomEditSelection,
|
|
190
|
+
animationId: string,
|
|
191
|
+
percentage: number,
|
|
192
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
193
|
+
) => {
|
|
168
194
|
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
169
195
|
const mutation = { type: "remove-keyframe", animationId, percentage };
|
|
170
196
|
void executeOptimisticKeyframeCacheUpdate({
|
|
@@ -182,6 +208,7 @@ export function useGsapKeyframeOps({
|
|
|
182
208
|
),
|
|
183
209
|
}),
|
|
184
210
|
persist: async () => {
|
|
211
|
+
const label = `Remove keyframe at ${percentage}%`;
|
|
185
212
|
if (sdkSession && sdkDeps) {
|
|
186
213
|
const handled = await sdkGsapRemoveKeyframePersist(
|
|
187
214
|
sourceFile,
|
|
@@ -189,14 +216,14 @@ export function useGsapKeyframeOps({
|
|
|
189
216
|
percentage,
|
|
190
217
|
sdkSession,
|
|
191
218
|
sdkDeps,
|
|
192
|
-
|
|
219
|
+
toSdkPersistOptions(label, commitOverrides),
|
|
193
220
|
);
|
|
194
221
|
if (handled) return;
|
|
195
222
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
softReload: true,
|
|
199
|
-
|
|
223
|
+
const commitOptions = commitOverrides?.skipReload
|
|
224
|
+
? { label, ...commitOverrides }
|
|
225
|
+
: { label, softReload: true, ...commitOverrides };
|
|
226
|
+
await commitMutation(selection, mutation, commitOptions);
|
|
200
227
|
},
|
|
201
228
|
}).catch((error) => {
|
|
202
229
|
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
|
|
@@ -261,6 +288,7 @@ export function useGsapKeyframeOps({
|
|
|
261
288
|
animationId: string,
|
|
262
289
|
resolvedFromValues?: Record<string, number | string>,
|
|
263
290
|
duration?: number,
|
|
291
|
+
commitOverrides: Partial<CommitMutationOptions> = { softReload: true },
|
|
264
292
|
) => {
|
|
265
293
|
if (sdkSession && sdkDeps) {
|
|
266
294
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -270,7 +298,7 @@ export function useGsapKeyframeOps({
|
|
|
270
298
|
resolvedFromValues,
|
|
271
299
|
sdkSession,
|
|
272
300
|
sdkDeps,
|
|
273
|
-
|
|
301
|
+
toSdkPersistOptions("Convert to keyframes", commitOverrides),
|
|
274
302
|
);
|
|
275
303
|
if (handled) return;
|
|
276
304
|
}
|
|
@@ -279,7 +307,7 @@ export function useGsapKeyframeOps({
|
|
|
279
307
|
// `duration` only applies when the target is a static `set` (which has
|
|
280
308
|
// none) — it spans the converted keyframes across the element's clip.
|
|
281
309
|
{ type: "convert-to-keyframes", animationId, resolvedFromValues, duration },
|
|
282
|
-
{ label: "Convert to keyframes" },
|
|
310
|
+
{ label: "Convert to keyframes", ...commitOverrides },
|
|
283
311
|
);
|
|
284
312
|
},
|
|
285
313
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
@@ -217,6 +217,7 @@ let cleanup: (() => void) | null = null;
|
|
|
217
217
|
function renderCommitHook() {
|
|
218
218
|
const reloadPreview = vi.fn();
|
|
219
219
|
const onCacheInvalidate = vi.fn();
|
|
220
|
+
const onFileContentChanged = vi.fn();
|
|
220
221
|
const forceReloadSdkSession = vi.fn();
|
|
221
222
|
const recordEdit = vi.fn(async () => {});
|
|
222
223
|
const showToast = vi.fn();
|
|
@@ -231,7 +232,7 @@ function renderCommitHook() {
|
|
|
231
232
|
domEditSaveTimestampRef: { current: 0 },
|
|
232
233
|
reloadPreview,
|
|
233
234
|
onCacheInvalidate,
|
|
234
|
-
onFileContentChanged
|
|
235
|
+
onFileContentChanged,
|
|
235
236
|
showToast,
|
|
236
237
|
sdkSession: null,
|
|
237
238
|
writeProjectFile: undefined,
|
|
@@ -252,6 +253,7 @@ function renderCommitHook() {
|
|
|
252
253
|
api: hookApi,
|
|
253
254
|
reloadPreview,
|
|
254
255
|
onCacheInvalidate,
|
|
256
|
+
onFileContentChanged,
|
|
255
257
|
forceReloadSdkSession,
|
|
256
258
|
recordEdit,
|
|
257
259
|
showToast,
|
|
@@ -383,4 +385,41 @@ describe("runCommit — instantPatch wiring", () => {
|
|
|
383
385
|
expectSoftReloadedWith(deps.reloadPreview, "AFTER");
|
|
384
386
|
expect(deps.reloadPreview).not.toHaveBeenCalled();
|
|
385
387
|
});
|
|
388
|
+
|
|
389
|
+
it("batch capability posts ordered mutations and finalizes the result once", async () => {
|
|
390
|
+
applySoftReload.mockReturnValue("applied");
|
|
391
|
+
mockFetchResult();
|
|
392
|
+
const deps = renderCommitHook();
|
|
393
|
+
const firstMutation = { type: "add", value: 1 };
|
|
394
|
+
const lastMutation = { type: "delete", value: 2 };
|
|
395
|
+
const batch = deps.api.commitMutation.batch;
|
|
396
|
+
if (!batch) throw new Error("batch capability missing");
|
|
397
|
+
|
|
398
|
+
await act(async () => {
|
|
399
|
+
await batch(
|
|
400
|
+
[
|
|
401
|
+
{ selection, mutation: firstMutation, options: { label: "Resize", skipReload: true } },
|
|
402
|
+
{ selection, mutation: lastMutation, options: { label: "Resize", softReload: true } },
|
|
403
|
+
],
|
|
404
|
+
{ label: "Resize", coalesceKey: "tx:resize:1", coalesceMs: Infinity, softReload: true },
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
410
|
+
"/api/projects/proj-1/gsap-mutations-batch/index.html",
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify({ mutations: [firstMutation, lastMutation] }),
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
expect(deps.recordEdit).toHaveBeenCalledTimes(1);
|
|
417
|
+
expect(deps.recordEdit).toHaveBeenCalledWith(
|
|
418
|
+
expect.objectContaining({ label: "Resize", coalesceKey: "tx:resize:1" }),
|
|
419
|
+
);
|
|
420
|
+
expect(deps.onFileContentChanged).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(deps.forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
422
|
+
expect(applySoftReload).toHaveBeenCalledTimes(1);
|
|
423
|
+
expect(deps.onCacheInvalidate).toHaveBeenCalledTimes(1);
|
|
424
|
+
});
|
|
386
425
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
3
|
+
import { readProjectFileContent as readSharedProjectFileContent } from "../utils/studioFileHistory";
|
|
3
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
5
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
6
|
import { applySoftReload, extractGsapScriptText } from "../utils/gsapSoftReload";
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
readJsonResponseBody,
|
|
16
17
|
} from "./gsapScriptCommitHelpers";
|
|
17
18
|
import type {
|
|
19
|
+
CommitMutation,
|
|
20
|
+
CommitMutationCall,
|
|
18
21
|
CommitMutationOptions,
|
|
19
22
|
GsapScriptCommitsParams,
|
|
20
23
|
MutationResult,
|
|
@@ -47,6 +50,76 @@ async function mutateGsapScript(
|
|
|
47
50
|
return result;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
async function mutateGsapScriptBatch(
|
|
54
|
+
projectId: string,
|
|
55
|
+
sourceFile: string,
|
|
56
|
+
mutations: Record<string, unknown>[],
|
|
57
|
+
): Promise<MutationResult> {
|
|
58
|
+
const res = await fetch(
|
|
59
|
+
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
|
|
60
|
+
{
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ mutations }),
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
if (!res.ok) throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
|
|
67
|
+
const result = (await res.json()) as MutationResult;
|
|
68
|
+
if (!result.ok) throw new Error(`Failed to update GSAP in ${sourceFile}`);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ShowToast = (message: string, tone?: "error" | "info") => void;
|
|
73
|
+
|
|
74
|
+
async function runMutationRequest(
|
|
75
|
+
mutations: Record<string, unknown>[],
|
|
76
|
+
options: CommitMutationOptions,
|
|
77
|
+
showToast: ShowToast | undefined,
|
|
78
|
+
request: () => Promise<MutationResult>,
|
|
79
|
+
): Promise<MutationResult | undefined> {
|
|
80
|
+
const unsafeFields = mutations.flatMap((mutation) => findUnsafeMutationValues(mutation));
|
|
81
|
+
if (unsafeFields.length > 0) {
|
|
82
|
+
showToast?.("Couldn't read element layout — try again at a different playhead time", "error");
|
|
83
|
+
if (options.skipReload) return;
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return await request();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof GsapMutationHttpError)
|
|
92
|
+
showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
93
|
+
if (options.skipReload) return;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function finishUnchangedMutation(
|
|
99
|
+
iframe: HTMLIFrameElement | null,
|
|
100
|
+
result: MutationResult,
|
|
101
|
+
options: CommitMutationOptions,
|
|
102
|
+
reloadPreview: () => void,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (result.changed !== false) return false;
|
|
105
|
+
if (!options.skipReload && options.instantPatch) {
|
|
106
|
+
applyPreviewSync(iframe, result, options, reloadPreview);
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function refreshMutationPreview(
|
|
112
|
+
iframe: HTMLIFrameElement | null,
|
|
113
|
+
result: MutationResult,
|
|
114
|
+
options: CommitMutationOptions,
|
|
115
|
+
reloadPreview: () => void,
|
|
116
|
+
onCacheInvalidate: () => void,
|
|
117
|
+
): void {
|
|
118
|
+
options.beforeReload?.();
|
|
119
|
+
applyPreviewSync(iframe, result, options, reloadPreview);
|
|
120
|
+
onCacheInvalidate();
|
|
121
|
+
}
|
|
122
|
+
|
|
50
123
|
/**
|
|
51
124
|
* Apply a soft reload and enforce the U4 invariant via the richer
|
|
52
125
|
* `SoftReloadResult`, with telemetry on every non-success path so the invariant
|
|
@@ -141,63 +214,78 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
141
214
|
// the same animationId so their POSTs can't interleave. Held in a ref so the
|
|
142
215
|
// chain survives re-renders.
|
|
143
216
|
const serializerRef = useRef(createKeyedSerializer());
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (error instanceof GsapMutationHttpError) showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
160
|
-
if (options.skipReload) return;
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
if (result.changed === false) {
|
|
164
|
-
// The FILE already matched, but a deferred instant patch may still be
|
|
165
|
-
// owed to the RUNTIME: paired commits (x with skipReload, then y carrying
|
|
166
|
-
// the patch for both) rely on the SECOND commit to sync the preview — if
|
|
167
|
-
// that half happens to be a no-op (a purely-horizontal drag or resize
|
|
168
|
-
// compensation), returning here would leave the runtime showing the old
|
|
169
|
-
// value while the file holds the new one. Patching in place is idempotent
|
|
170
|
-
// when the values truly match everywhere.
|
|
171
|
-
if (!options.skipReload && options.instantPatch) {
|
|
172
|
-
applyPreviewSync(previewIframeRef.current, result, options, reloadPreview);
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
217
|
+
const recordMutationEdit = useCallback(async (targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
218
|
+
if (result.before == null || result.after == null) return;
|
|
219
|
+
await editHistory.recordEdit({
|
|
220
|
+
label: options.label,
|
|
221
|
+
kind: "manual",
|
|
222
|
+
coalesceKey: options.coalesceKey,
|
|
223
|
+
coalesceMs: options.coalesceMs,
|
|
224
|
+
files: { [targetPath]: { before: result.before, after: result.after } },
|
|
225
|
+
});
|
|
226
|
+
}, [editHistory]);
|
|
227
|
+
|
|
228
|
+
const finalizeSuccessfulMutation = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, targetPath: string, result: MutationResult, options: CommitMutationOptions) => {
|
|
229
|
+
// A no-op file write may still owe the runtime a deferred instant patch.
|
|
230
|
+
if (finishUnchangedMutation(previewIframeRef.current, result, options, reloadPreview)) return;
|
|
176
231
|
domEditSaveTimestampRef.current = Date.now();
|
|
177
|
-
|
|
178
|
-
await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
|
|
179
|
-
}
|
|
232
|
+
await recordMutationEdit(targetPath, result, options);
|
|
180
233
|
if (result.after != null) onFileContentChanged?.(targetPath, result.after);
|
|
181
234
|
// Server wrote the file; the in-memory SDK doc is now stale. Resync it so a
|
|
182
235
|
// later SDK-routed edit doesn't serialize the pre-write doc and revert this.
|
|
183
236
|
forceReloadSdkSession?.();
|
|
184
237
|
if (options.skipReload) return;
|
|
185
238
|
if (result.parsed?.animations) updateKeyframeCacheFromParsed(result.parsed.animations, targetPath, selection.id ?? undefined, mutation);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
239
|
+
refreshMutationPreview(
|
|
240
|
+
previewIframeRef.current,
|
|
241
|
+
result,
|
|
242
|
+
options,
|
|
243
|
+
reloadPreview,
|
|
244
|
+
onCacheInvalidate,
|
|
245
|
+
);
|
|
246
|
+
}, [previewIframeRef, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, forceReloadSdkSession, recordMutationEdit]);
|
|
247
|
+
|
|
248
|
+
const runCommit = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
|
|
249
|
+
const pid = projectIdRef.current;
|
|
250
|
+
if (!pid) return;
|
|
251
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
252
|
+
const result = await runMutationRequest([mutation], options, showToast, () =>
|
|
253
|
+
mutateGsapScript(pid, targetPath, mutation),
|
|
254
|
+
);
|
|
255
|
+
if (!result) return;
|
|
256
|
+
await finalizeSuccessfulMutation(selection, mutation, targetPath, result, options);
|
|
257
|
+
}, [projectIdRef, activeCompPath, showToast, finalizeSuccessfulMutation]);
|
|
258
|
+
|
|
259
|
+
const runBatchCommit = useCallback(async (calls: CommitMutationCall[], options: CommitMutationOptions) => {
|
|
260
|
+
const pid = projectIdRef.current;
|
|
261
|
+
const first = calls[0];
|
|
262
|
+
const last = calls.at(-1);
|
|
263
|
+
if (!pid || !first || !last) return;
|
|
264
|
+
const targetPath = first.selection.sourceFile || activeCompPath || "index.html";
|
|
265
|
+
const mutations = calls.map(({ mutation }) => mutation);
|
|
266
|
+
const result = await runMutationRequest(mutations, options, showToast, () =>
|
|
267
|
+
mutateGsapScriptBatch(pid, targetPath, mutations),
|
|
268
|
+
);
|
|
269
|
+
if (!result) return;
|
|
270
|
+
await finalizeSuccessfulMutation(last.selection, last.mutation, targetPath, result, options);
|
|
271
|
+
}, [projectIdRef, activeCompPath, showToast, finalizeSuccessfulMutation]);
|
|
272
|
+
|
|
190
273
|
// Every GSAP-script commit is a read-modify-write of one file. Overlapping
|
|
191
274
|
// commits to the SAME file (any op type, any animation) interleave server-side,
|
|
192
275
|
// so serialize per target file by default; an explicit serializeKey overrides.
|
|
193
|
-
const commitMutation =
|
|
194
|
-
(selection
|
|
276
|
+
const commitMutation = useMemo<CommitMutation>(() => {
|
|
277
|
+
const commit: CommitMutation = (selection, mutation, options) => {
|
|
195
278
|
const file = selection.sourceFile || activeCompPath || "index.html";
|
|
196
279
|
const key = options.serializeKey ?? `gsap-file:${file}`;
|
|
197
280
|
return serializerRef.current(key, () => runCommit(selection, mutation, options));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
281
|
+
};
|
|
282
|
+
commit.batch = (calls, options) => {
|
|
283
|
+
const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
|
|
284
|
+
const key = options.serializeKey ?? `gsap-file:${file}`;
|
|
285
|
+
return serializerRef.current(key, () => runBatchCommit(calls, options));
|
|
286
|
+
};
|
|
287
|
+
return commit;
|
|
288
|
+
}, [runCommit, runBatchCommit, activeCompPath]);
|
|
201
289
|
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
|
|
202
290
|
const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToast);
|
|
203
291
|
|
|
@@ -238,14 +326,10 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
|
|
|
238
326
|
// exact prior content as its undo `before` (matching the style/delete paths),
|
|
239
327
|
// instead of a normalized full-DOM re-emit that would reformat the whole file.
|
|
240
328
|
const readProjectFileContent = useCallback(
|
|
241
|
-
|
|
329
|
+
(path: string): Promise<string> => {
|
|
242
330
|
const pid = projectIdRef.current;
|
|
243
331
|
if (!pid) throw new Error("No active project");
|
|
244
|
-
|
|
245
|
-
if (!res.ok) throw new Error(`Failed to read ${path}`);
|
|
246
|
-
const data = (await res.json()) as { content?: string };
|
|
247
|
-
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
248
|
-
return data.content;
|
|
332
|
+
return readSharedProjectFileContent(pid, path);
|
|
249
333
|
},
|
|
250
334
|
[projectIdRef],
|
|
251
335
|
);
|
|
@@ -5,6 +5,7 @@ import { usePlayerStore } from "../player";
|
|
|
5
5
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
6
6
|
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
7
7
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
+
import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Thin useCallback wrappers that guard on `domEditSelection` before
|
|
@@ -75,8 +76,14 @@ export function useGsapSelectionHandlers({
|
|
|
75
76
|
animId: string,
|
|
76
77
|
percentage: number,
|
|
77
78
|
properties: Record<string, number | string>,
|
|
79
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
78
80
|
) => Promise<void>;
|
|
79
|
-
removeKeyframe: (
|
|
81
|
+
removeKeyframe: (
|
|
82
|
+
sel: DomEditSelection,
|
|
83
|
+
animId: string,
|
|
84
|
+
percentage: number,
|
|
85
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
86
|
+
) => void;
|
|
80
87
|
moveKeyframe: (
|
|
81
88
|
sel: DomEditSelection,
|
|
82
89
|
animId: string,
|
|
@@ -95,6 +102,7 @@ export function useGsapSelectionHandlers({
|
|
|
95
102
|
animId: string,
|
|
96
103
|
resolvedFromValues?: Record<string, number | string>,
|
|
97
104
|
duration?: number,
|
|
105
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
98
106
|
) => Promise<void>;
|
|
99
107
|
removeAllKeyframes: (sel: DomEditSelection, animId: string) => void;
|
|
100
108
|
|
|
@@ -232,20 +240,36 @@ export function useGsapSelectionHandlers({
|
|
|
232
240
|
);
|
|
233
241
|
|
|
234
242
|
const handleGsapAddKeyframeBatch = useCallback(
|
|
235
|
-
(
|
|
243
|
+
(
|
|
244
|
+
animId: string,
|
|
245
|
+
percentage: number,
|
|
246
|
+
properties: Record<string, number | string>,
|
|
247
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
248
|
+
) => {
|
|
236
249
|
if (!domEditSelection) return Promise.resolve();
|
|
237
|
-
return addKeyframeBatch(
|
|
250
|
+
return addKeyframeBatch(
|
|
251
|
+
domEditSelection,
|
|
252
|
+
animId,
|
|
253
|
+
percentage,
|
|
254
|
+
properties,
|
|
255
|
+
commitOverrides,
|
|
256
|
+
).catch((error) => {
|
|
238
257
|
trackGsapHandlerFailure(error, domEditSelection, "add-keyframe", "Add keyframe");
|
|
239
258
|
});
|
|
240
259
|
},
|
|
241
260
|
[domEditSelection, addKeyframeBatch, trackGsapHandlerFailure],
|
|
242
261
|
);
|
|
243
262
|
const handleGsapRemoveKeyframe = useCallback(
|
|
244
|
-
(
|
|
263
|
+
(
|
|
264
|
+
animId: string,
|
|
265
|
+
percentage: number,
|
|
266
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
267
|
+
selectionOverride?: DomEditSelection | null,
|
|
268
|
+
) => {
|
|
245
269
|
const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
|
|
246
270
|
if (!sel) return;
|
|
247
271
|
trackStudioEvent("keyframe", { action: "remove" });
|
|
248
|
-
removeKeyframe(sel, animId, percentage);
|
|
272
|
+
removeKeyframe(sel, animId, percentage, commitOverrides);
|
|
249
273
|
},
|
|
250
274
|
[domEditSelection, removeKeyframe],
|
|
251
275
|
);
|
|
@@ -302,18 +326,27 @@ export function useGsapSelectionHandlers({
|
|
|
302
326
|
);
|
|
303
327
|
|
|
304
328
|
const handleGsapConvertToKeyframes = useCallback(
|
|
305
|
-
(
|
|
329
|
+
(
|
|
330
|
+
animId: string,
|
|
331
|
+
resolvedFromValues?: Record<string, number | string>,
|
|
332
|
+
duration?: number,
|
|
333
|
+
commitOverrides?: Partial<CommitMutationOptions>,
|
|
334
|
+
) => {
|
|
306
335
|
if (!domEditSelection) return Promise.resolve();
|
|
307
|
-
return convertToKeyframes(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
336
|
+
return convertToKeyframes(
|
|
337
|
+
domEditSelection,
|
|
338
|
+
animId,
|
|
339
|
+
resolvedFromValues,
|
|
340
|
+
duration,
|
|
341
|
+
commitOverrides,
|
|
342
|
+
).catch((error) => {
|
|
343
|
+
trackGsapHandlerFailure(
|
|
344
|
+
error,
|
|
345
|
+
domEditSelection,
|
|
346
|
+
"convert-to-keyframes",
|
|
347
|
+
"Convert to keyframes",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
317
350
|
},
|
|
318
351
|
[domEditSelection, convertToKeyframes, trackGsapHandlerFailure],
|
|
319
352
|
);
|