@hyperframes/studio 0.7.90 → 0.7.93
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-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +26 -29
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -26,18 +26,23 @@ import {
|
|
|
26
26
|
import { commitWholePropertyOffset } from "./gsapWholePropertyOffsetCommit";
|
|
27
27
|
import { resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
28
28
|
import type { GsapDragCommitCallbacks } from "./gsapDragCommit";
|
|
29
|
-
import { isInstantHold, selectorFromSelection } from "./gsapShared";
|
|
29
|
+
import { isInstantHold, selectorFromSelection, writeTargetSelector } from "./gsapShared";
|
|
30
30
|
import {
|
|
31
31
|
findGsapPositionAnimation,
|
|
32
32
|
pickClosestToPlayhead,
|
|
33
33
|
readGsapPositionFromIframe,
|
|
34
34
|
} from "./gsapPositionDetection";
|
|
35
35
|
import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes";
|
|
36
|
+
import {
|
|
37
|
+
animationWritesAnyProperty,
|
|
38
|
+
directEditOutcomeForProperties,
|
|
39
|
+
type GsapEditOutcome,
|
|
40
|
+
} from "./gsapEditOutcome";
|
|
36
41
|
|
|
37
42
|
// Position channels — used to scope the "has a live position tween?" check so a
|
|
38
43
|
// sibling rotation/scale animation never forces a static position hold into the
|
|
39
44
|
// keyframe branch (which corrupts it into a frozen duration-0 keyframed tween).
|
|
40
|
-
export const POSITION_CHANNELS = [
|
|
45
|
+
export const POSITION_CHANNELS: string[] = [
|
|
41
46
|
"x",
|
|
42
47
|
"y",
|
|
43
48
|
"xPercent",
|
|
@@ -50,6 +55,10 @@ export const POSITION_CHANNELS = [
|
|
|
50
55
|
"translateX",
|
|
51
56
|
"translateY",
|
|
52
57
|
];
|
|
58
|
+
const POSITION_CHANNEL_SET = new Set<string>(POSITION_CHANNELS);
|
|
59
|
+
|
|
60
|
+
const ROTATION_CHANNELS: string[] = ["rotation", "rotationX", "rotationY", "rotationZ"];
|
|
61
|
+
const ROTATION_CHANNEL_SET = new Set<string>(ROTATION_CHANNELS);
|
|
53
62
|
|
|
54
63
|
// ── Property-group tween resolution ───────────────────────────────────────
|
|
55
64
|
|
|
@@ -121,11 +130,40 @@ export type { GsapDragCommitCallbacks };
|
|
|
121
130
|
/**
|
|
122
131
|
* Attempt to handle a drag commit via the GSAP script mutation path.
|
|
123
132
|
*
|
|
124
|
-
* Returns
|
|
125
|
-
*
|
|
126
|
-
* exists.
|
|
133
|
+
* Returns an explicit persisted/blocked outcome. Callers must reject blocked
|
|
134
|
+
* outcomes so the gesture layer restores its runtime and overlay drafts.
|
|
127
135
|
*/
|
|
128
136
|
// fallow-ignore-next-line complexity
|
|
137
|
+
async function preflightGsapDragIntercept(
|
|
138
|
+
selection: DomEditSelection,
|
|
139
|
+
animations: GsapAnimation[],
|
|
140
|
+
iframe: HTMLIFrameElement | null,
|
|
141
|
+
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
142
|
+
): Promise<GsapEditOutcome> {
|
|
143
|
+
const selector = selectorFromSelection(selection);
|
|
144
|
+
if (!selector) return { status: "blocked", reason: "no-selector" };
|
|
145
|
+
|
|
146
|
+
const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
|
|
147
|
+
// The fallback API currently represents both a definitive empty parse and an
|
|
148
|
+
// exhausted fetch failure as `[]`. Keep the selected cache in the preflight
|
|
149
|
+
// set as well: ignoring it would let a transient fetch failure bypass helper /
|
|
150
|
+
// runtime-source ownership and reach a destructive split or property write.
|
|
151
|
+
const allKnownAnimations = [...animations, ...fetchedAnimations];
|
|
152
|
+
const editability = directEditOutcomeForProperties(allKnownAnimations, POSITION_CHANNEL_SET);
|
|
153
|
+
if (editability.status === "blocked") return editability;
|
|
154
|
+
const sourceAnimations = fetchedAnimations.length > 0 ? fetchedAnimations : animations;
|
|
155
|
+
const posAnim = findGsapPositionAnimation(sourceAnimations, selector);
|
|
156
|
+
const hasLivePosition = hasNonHoldTweenForElement(iframe, selector, undefined, POSITION_CHANNELS);
|
|
157
|
+
|
|
158
|
+
if (hasLivePosition && !posAnim) {
|
|
159
|
+
return { status: "blocked", reason: "source-uneditable" };
|
|
160
|
+
}
|
|
161
|
+
if (!posAnim && !writeTargetSelector(selection)) {
|
|
162
|
+
return { status: "blocked", reason: "no-selector" };
|
|
163
|
+
}
|
|
164
|
+
return { status: "persisted" };
|
|
165
|
+
}
|
|
166
|
+
|
|
129
167
|
export async function tryGsapDragIntercept(
|
|
130
168
|
selection: DomEditSelection,
|
|
131
169
|
offset: { x: number; y: number },
|
|
@@ -133,12 +171,20 @@ export async function tryGsapDragIntercept(
|
|
|
133
171
|
iframe: HTMLIFrameElement | null,
|
|
134
172
|
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
135
173
|
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
136
|
-
options?: { altKey?: boolean },
|
|
137
|
-
): Promise<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
options?: { altKey?: boolean; preflightOnly?: boolean; preflightPassed?: boolean },
|
|
175
|
+
): Promise<GsapEditOutcome> {
|
|
176
|
+
if (!options?.preflightPassed) {
|
|
177
|
+
const preflight = await preflightGsapDragIntercept(
|
|
178
|
+
selection,
|
|
179
|
+
animations,
|
|
180
|
+
iframe,
|
|
181
|
+
fetchFallbackAnimations,
|
|
182
|
+
);
|
|
183
|
+
if (preflight.status === "blocked" || options?.preflightOnly) return preflight;
|
|
141
184
|
}
|
|
185
|
+
const selector = selectorFromSelection(selection);
|
|
186
|
+
// The preflight above proves this; retain a defensive result for DOM churn.
|
|
187
|
+
if (!selector) return { status: "blocked", reason: "no-selector" };
|
|
142
188
|
|
|
143
189
|
// Self-heal: enforce a single position write BEFORE committing. A corrupted
|
|
144
190
|
// file can carry 2+ conflicting position writes for one selector (e.g. a
|
|
@@ -218,11 +264,11 @@ export async function tryGsapDragIntercept(
|
|
|
218
264
|
commitMutation,
|
|
219
265
|
fetchAnimations: fetchFallbackAnimations,
|
|
220
266
|
});
|
|
221
|
-
return
|
|
267
|
+
return { status: "persisted" };
|
|
222
268
|
}
|
|
223
269
|
|
|
224
270
|
if (!posAnim) {
|
|
225
|
-
return
|
|
271
|
+
return { status: "blocked", reason: "source-uneditable" };
|
|
226
272
|
}
|
|
227
273
|
|
|
228
274
|
// Verify the anim ID is still valid in the current file. The React-state
|
|
@@ -251,7 +297,7 @@ export async function tryGsapDragIntercept(
|
|
|
251
297
|
} else {
|
|
252
298
|
await commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
|
|
253
299
|
}
|
|
254
|
-
return
|
|
300
|
+
return { status: "persisted" };
|
|
255
301
|
}
|
|
256
302
|
|
|
257
303
|
// ── Runtime property readers (re-exported for external callers) ───────────
|
|
@@ -267,28 +313,47 @@ export async function tryGsapRotationIntercept(
|
|
|
267
313
|
iframe: HTMLIFrameElement | null,
|
|
268
314
|
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
269
315
|
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
270
|
-
): Promise<
|
|
271
|
-
const selector = selectorFromSelection(selection);
|
|
272
|
-
if (!selector) return
|
|
316
|
+
): Promise<GsapEditOutcome> {
|
|
317
|
+
const selector = selectorFromSelection(selection) ?? writeTargetSelector(selection);
|
|
318
|
+
if (!selector) return { status: "blocked", reason: "no-selector" };
|
|
319
|
+
|
|
320
|
+
const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
|
|
321
|
+
const workingAnimations = animations.length > 0 ? animations : fetchedAnimations;
|
|
322
|
+
const editability = directEditOutcomeForProperties(
|
|
323
|
+
[...animations, ...fetchedAnimations],
|
|
324
|
+
ROTATION_CHANNEL_SET,
|
|
325
|
+
);
|
|
326
|
+
if (editability.status === "blocked") return editability;
|
|
327
|
+
const postSplitFetch = workingAnimations.some((animation) => !animation.propertyGroup)
|
|
328
|
+
? fetchFallbackAnimations
|
|
329
|
+
: undefined;
|
|
273
330
|
|
|
274
331
|
// Resolve the rotation-group tween, splitting legacy mixed tweens if needed.
|
|
275
332
|
const resolved = await resolveGroupTween(
|
|
276
333
|
"rotation",
|
|
277
|
-
|
|
334
|
+
workingAnimations,
|
|
278
335
|
selection,
|
|
279
336
|
commitMutation,
|
|
280
|
-
|
|
337
|
+
postSplitFetch,
|
|
281
338
|
);
|
|
282
|
-
const resolvedAnimations = resolved?.animations ??
|
|
339
|
+
const resolvedAnimations = resolved?.animations ?? workingAnimations;
|
|
283
340
|
|
|
284
341
|
// Fallback: legacy heuristic for hand-written scripts
|
|
285
|
-
let anim =
|
|
342
|
+
let anim =
|
|
343
|
+
resolved?.anim && animationWritesAnyProperty(resolved.anim, ROTATION_CHANNEL_SET)
|
|
344
|
+
? resolved.anim
|
|
345
|
+
: null;
|
|
286
346
|
if (!anim) {
|
|
287
|
-
anim =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
347
|
+
anim =
|
|
348
|
+
workingAnimations.find((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET)) ?? null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const liveSelector = selectorFromSelection(selection);
|
|
352
|
+
const hasLiveRotationTween = liveSelector
|
|
353
|
+
? hasNonHoldTweenForElement(iframe, liveSelector, undefined, ROTATION_CHANNELS)
|
|
354
|
+
: false;
|
|
355
|
+
if (!anim && hasLiveRotationTween) {
|
|
356
|
+
return { status: "blocked", reason: "source-uneditable" };
|
|
292
357
|
}
|
|
293
358
|
|
|
294
359
|
// `angle` is the ABSOLUTE target rotation resolved by the gesture (gsap base +
|
|
@@ -307,7 +372,7 @@ export async function tryGsapRotationIntercept(
|
|
|
307
372
|
commitMutation,
|
|
308
373
|
fetchAnimations: fetchFallbackAnimations,
|
|
309
374
|
});
|
|
310
|
-
return
|
|
375
|
+
return { status: "persisted" };
|
|
311
376
|
}
|
|
312
377
|
|
|
313
378
|
const pct = computeCurrentPercentage(selection, anim);
|
|
@@ -325,7 +390,7 @@ export async function tryGsapRotationIntercept(
|
|
|
325
390
|
{ commitMutation, fetchAnimations: fetchFallbackAnimations },
|
|
326
391
|
"Rotate animation",
|
|
327
392
|
);
|
|
328
|
-
return
|
|
393
|
+
return { status: "persisted" };
|
|
329
394
|
}
|
|
330
395
|
|
|
331
396
|
// fallow-ignore-next-line code-duplication
|
|
@@ -363,7 +428,7 @@ export async function tryGsapRotationIntercept(
|
|
|
363
428
|
},
|
|
364
429
|
{ label: `Rotate (keyframe ${pct}%)`, softReload: true },
|
|
365
430
|
);
|
|
366
|
-
return
|
|
431
|
+
return { status: "persisted" };
|
|
367
432
|
}
|
|
368
433
|
|
|
369
434
|
export { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyframes";
|
|
@@ -41,19 +41,45 @@ type Commit = (
|
|
|
41
41
|
/** Renders the hook and hands its commit function to the caller via a ref callback. */
|
|
42
42
|
function renderHookWith(
|
|
43
43
|
animations: GsapAnimation[],
|
|
44
|
-
onMutation: (mutation: Record<string, unknown>, label: string) =>
|
|
44
|
+
onMutation: (mutation: Record<string, unknown>, label: string) => unknown | Promise<unknown>,
|
|
45
45
|
onReady: (commit: Commit) => void,
|
|
46
|
+
bumpGsapCache = vi.fn(),
|
|
47
|
+
onBatch?: (
|
|
48
|
+
calls: Array<{ mutation: Record<string, unknown>; options: { label: string } }>,
|
|
49
|
+
label: string,
|
|
50
|
+
) => unknown | Promise<unknown>,
|
|
46
51
|
) {
|
|
47
52
|
function Harness() {
|
|
53
|
+
const gsapCommitMutation = Object.assign(
|
|
54
|
+
async (
|
|
55
|
+
_sel: DomEditSelection,
|
|
56
|
+
mutation: Record<string, unknown>,
|
|
57
|
+
options: { label: string },
|
|
58
|
+
) => {
|
|
59
|
+
await onMutation(mutation, options.label);
|
|
60
|
+
},
|
|
61
|
+
onBatch
|
|
62
|
+
? {
|
|
63
|
+
batch: async (
|
|
64
|
+
calls: Array<{
|
|
65
|
+
selection: DomEditSelection;
|
|
66
|
+
mutation: Record<string, unknown>;
|
|
67
|
+
options: { label: string };
|
|
68
|
+
}>,
|
|
69
|
+
options: { label: string },
|
|
70
|
+
) => {
|
|
71
|
+
await onBatch(calls, options.label);
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {},
|
|
75
|
+
);
|
|
48
76
|
const { commitAnimatedProperties } = useAnimatedPropertyCommit({
|
|
49
77
|
selectedGsapAnimations: animations,
|
|
50
|
-
gsapCommitMutation
|
|
51
|
-
onMutation(mutation, options.label);
|
|
52
|
-
},
|
|
78
|
+
gsapCommitMutation,
|
|
53
79
|
addGsapAnimation: vi.fn(),
|
|
54
80
|
convertToKeyframes: vi.fn(),
|
|
55
81
|
previewIframeRef: { current: null },
|
|
56
|
-
bumpGsapCache
|
|
82
|
+
bumpGsapCache,
|
|
57
83
|
});
|
|
58
84
|
onReady(commitAnimatedProperties);
|
|
59
85
|
return null;
|
|
@@ -61,11 +87,106 @@ function renderHookWith(
|
|
|
61
87
|
return mountReactHarness(<Harness />);
|
|
62
88
|
}
|
|
63
89
|
|
|
90
|
+
describe("useAnimatedPropertyCommit — ownership and rejection propagation", () => {
|
|
91
|
+
it("rejects a helper-authored property before sending a mutation", async () => {
|
|
92
|
+
const helperRotation = {
|
|
93
|
+
id: "#box-to-rotation",
|
|
94
|
+
targetSelector: "#box",
|
|
95
|
+
propertyGroup: "rotation",
|
|
96
|
+
method: "to",
|
|
97
|
+
properties: { rotationX: 10 },
|
|
98
|
+
provenance: { kind: "helper", fn: "spin", callSite: 1 },
|
|
99
|
+
} as unknown as GsapAnimation;
|
|
100
|
+
const mutations: Array<Record<string, unknown>> = [];
|
|
101
|
+
let commit!: Commit;
|
|
102
|
+
const root = renderHookWith(
|
|
103
|
+
[helperRotation],
|
|
104
|
+
(mutation) => mutations.push(mutation),
|
|
105
|
+
(ready) => (commit = ready),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await expect(commit(selection, { rotationX: 12 })).rejects.toMatchObject({
|
|
109
|
+
name: "GsapEditBlockedError",
|
|
110
|
+
reason: "unroll-required",
|
|
111
|
+
});
|
|
112
|
+
expect(mutations).toHaveLength(0);
|
|
113
|
+
act(() => root.unmount());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects runtime-computed property ownership before sending a mutation", async () => {
|
|
117
|
+
const runtimePosition = {
|
|
118
|
+
...keyframedAnim,
|
|
119
|
+
hasUnresolvedKeyframes: true,
|
|
120
|
+
} as GsapAnimation;
|
|
121
|
+
const mutations: Array<Record<string, unknown>> = [];
|
|
122
|
+
let commit!: Commit;
|
|
123
|
+
const root = renderHookWith(
|
|
124
|
+
[runtimePosition],
|
|
125
|
+
(mutation) => mutations.push(mutation),
|
|
126
|
+
(ready) => (commit = ready),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await expect(commit(selection, { x: 50 })).rejects.toMatchObject({
|
|
130
|
+
reason: "source-uneditable",
|
|
131
|
+
});
|
|
132
|
+
expect(mutations).toHaveLength(0);
|
|
133
|
+
act(() => root.unmount());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rejects every property before a mixed-group commit can partially persist", async () => {
|
|
137
|
+
const helperOpacity = {
|
|
138
|
+
id: "#box-to-visual",
|
|
139
|
+
targetSelector: "#box",
|
|
140
|
+
propertyGroup: "visual",
|
|
141
|
+
method: "to",
|
|
142
|
+
properties: { opacity: 0.5 },
|
|
143
|
+
provenance: { kind: "helper", fn: "fade", callSite: 1 },
|
|
144
|
+
} as unknown as GsapAnimation;
|
|
145
|
+
const mutations: Array<Record<string, unknown>> = [];
|
|
146
|
+
let commit!: Commit;
|
|
147
|
+
const root = renderHookWith(
|
|
148
|
+
[helperOpacity],
|
|
149
|
+
(mutation) => mutations.push(mutation),
|
|
150
|
+
(ready) => (commit = ready),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await expect(commit(selection, { x: 50, opacity: 0.8 })).rejects.toMatchObject({
|
|
154
|
+
reason: "unroll-required",
|
|
155
|
+
});
|
|
156
|
+
expect(mutations).toHaveLength(0);
|
|
157
|
+
act(() => root.unmount());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("rethrows a persistence failure to the telemetry wrapper", async () => {
|
|
161
|
+
const failure = new Error("save failed");
|
|
162
|
+
const bumpGsapCache = vi.fn();
|
|
163
|
+
let commit!: Commit;
|
|
164
|
+
const root = renderHookWith(
|
|
165
|
+
[keyframedAnim],
|
|
166
|
+
async () => {
|
|
167
|
+
throw failure;
|
|
168
|
+
},
|
|
169
|
+
(ready) => (commit = ready),
|
|
170
|
+
bumpGsapCache,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await expect(commit(selection, { x: 50 })).rejects.toBe(failure);
|
|
174
|
+
expect(bumpGsapCache).toHaveBeenCalledTimes(1);
|
|
175
|
+
act(() => root.unmount());
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
64
179
|
function renderCommitHook(
|
|
65
180
|
mutations: Array<Record<string, unknown>>,
|
|
66
181
|
onReady: (commit: Commit) => void,
|
|
67
182
|
) {
|
|
68
|
-
return renderHookWith(
|
|
183
|
+
return renderHookWith(
|
|
184
|
+
[keyframedAnim],
|
|
185
|
+
(mutation) => {
|
|
186
|
+
mutations.push(mutation);
|
|
187
|
+
},
|
|
188
|
+
onReady,
|
|
189
|
+
);
|
|
69
190
|
}
|
|
70
191
|
|
|
71
192
|
// Regression (#1808): a "3D transform" / design-panel property edit on an
|
|
@@ -74,35 +195,28 @@ function renderCommitHook(
|
|
|
74
195
|
// off, it must shift the whole tween instead of adding/updating a keyframe
|
|
75
196
|
// at the playhead.
|
|
76
197
|
describe("useAnimatedPropertyCommit — autoKeyframeEnabled toggle (#1808)", () => {
|
|
77
|
-
|
|
78
|
-
usePlayerStore.setState({ autoKeyframeEnabled:
|
|
198
|
+
async function runCommitWithAutoKeyframe(enabled: boolean) {
|
|
199
|
+
usePlayerStore.setState({ autoKeyframeEnabled: enabled, currentTime: 0 });
|
|
79
200
|
const mutations: Array<Record<string, unknown>> = [];
|
|
80
201
|
let commit: Commit | undefined;
|
|
81
202
|
const root = renderCommitHook(mutations, (fn) => (commit = fn));
|
|
203
|
+
await act(async () => commit!(selection, { x: 50 }));
|
|
204
|
+
act(() => root.unmount());
|
|
205
|
+
return mutations;
|
|
206
|
+
}
|
|
82
207
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
|
|
208
|
+
it("shifts the whole tween instead of updating a keyframe when the toggle is off", async () => {
|
|
209
|
+
const mutations = await runCommitWithAutoKeyframe(false);
|
|
87
210
|
expect(mutations).toHaveLength(1);
|
|
88
211
|
expect(mutations[0]!.type).toBe("replace-with-keyframes");
|
|
89
|
-
act(() => root.unmount());
|
|
90
212
|
});
|
|
91
213
|
|
|
92
214
|
it("still updates a keyframe at the playhead when the toggle is on (default)", async () => {
|
|
93
|
-
const mutations
|
|
94
|
-
let commit: Commit | undefined;
|
|
95
|
-
const root = renderCommitHook(mutations, (fn) => (commit = fn));
|
|
96
|
-
|
|
97
|
-
await act(async () => {
|
|
98
|
-
await commit!(selection, { x: 50 });
|
|
99
|
-
});
|
|
100
|
-
|
|
215
|
+
const mutations = await runCommitWithAutoKeyframe(true);
|
|
101
216
|
expect(mutations.some((m) => m.type === "update-keyframe" || m.type === "add-keyframe")).toBe(
|
|
102
217
|
true,
|
|
103
218
|
);
|
|
104
219
|
expect(mutations.some((m) => m.type === "replace-with-keyframes")).toBe(false);
|
|
105
|
-
act(() => root.unmount());
|
|
106
220
|
});
|
|
107
221
|
});
|
|
108
222
|
|
|
@@ -125,7 +239,9 @@ describe("commitStaticSet group routing", () => {
|
|
|
125
239
|
) {
|
|
126
240
|
return renderHookWith(
|
|
127
241
|
[positionSet],
|
|
128
|
-
(mutation, label) =>
|
|
242
|
+
(mutation, label) => {
|
|
243
|
+
committed.push({ mutation, label });
|
|
244
|
+
},
|
|
129
245
|
onReady,
|
|
130
246
|
);
|
|
131
247
|
}
|
|
@@ -173,7 +289,9 @@ describe("commitStaticSet group routing", () => {
|
|
|
173
289
|
let commit!: Commit;
|
|
174
290
|
renderHookWith(
|
|
175
291
|
[positionSet, instantSizeHold],
|
|
176
|
-
(mutation, label) =>
|
|
292
|
+
(mutation, label) => {
|
|
293
|
+
committed.push({ mutation, label });
|
|
294
|
+
},
|
|
177
295
|
(c) => (commit = c),
|
|
178
296
|
);
|
|
179
297
|
|
|
@@ -191,4 +309,56 @@ describe("commitStaticSet group routing", () => {
|
|
|
191
309
|
expect(committed.some(({ mutation }) => mutation.type === "add")).toBe(false);
|
|
192
310
|
expect(committed[0]!.mutation.animationId).not.toBe(positionSet.id);
|
|
193
311
|
});
|
|
312
|
+
|
|
313
|
+
it("persists multiple property groups in one atomic batch", async () => {
|
|
314
|
+
const committed: Array<{ mutation: Record<string, unknown>; label: string }> = [];
|
|
315
|
+
const batches: Array<{
|
|
316
|
+
calls: Array<{ mutation: Record<string, unknown>; options: { label: string } }>;
|
|
317
|
+
label: string;
|
|
318
|
+
}> = [];
|
|
319
|
+
let commit!: Commit;
|
|
320
|
+
const root = renderHookWith(
|
|
321
|
+
[positionSet],
|
|
322
|
+
(mutation, label) => committed.push({ mutation, label }),
|
|
323
|
+
(ready) => (commit = ready),
|
|
324
|
+
vi.fn(),
|
|
325
|
+
(calls, label) => batches.push({ calls, label }),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
await act(async () => {
|
|
329
|
+
await commit(selection, { x: 400, width: 500 });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(committed).toHaveLength(0);
|
|
333
|
+
expect(batches).toHaveLength(1);
|
|
334
|
+
expect(batches[0]!.label).toBe("Set properties");
|
|
335
|
+
expect(batches[0]!.calls.map(({ mutation }) => mutation)).toEqual([
|
|
336
|
+
{
|
|
337
|
+
type: "update-properties",
|
|
338
|
+
animationId: positionSet.id,
|
|
339
|
+
properties: { x: 400 },
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
type: "add",
|
|
343
|
+
targetSelector: "#box",
|
|
344
|
+
method: "set",
|
|
345
|
+
position: 0,
|
|
346
|
+
properties: { width: 500 },
|
|
347
|
+
global: true,
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
act(() => root.unmount());
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("fails before sending anything when an atomic multi-group batch is unavailable", async () => {
|
|
354
|
+
const committed: Array<{ mutation: Record<string, unknown>; label: string }> = [];
|
|
355
|
+
let commit!: Commit;
|
|
356
|
+
const root = renderStaticHook(committed, (ready) => (commit = ready));
|
|
357
|
+
|
|
358
|
+
await expect(commit(selection, { x: 400, width: 500 })).rejects.toThrow(
|
|
359
|
+
"Atomic GSAP property batch is unavailable",
|
|
360
|
+
);
|
|
361
|
+
expect(committed).toHaveLength(0);
|
|
362
|
+
act(() => root.unmount());
|
|
363
|
+
});
|
|
194
364
|
});
|