@hyperframes/studio 0.6.94 → 0.6.96
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-B0twsRu0.css +1 -0
- package/dist/assets/index-BA979yF1.js +251 -0
- package/dist/assets/{index-qQxjvtjI.js → index-BWFaypdT.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +10 -5
- package/src/components/SaveQueuePausedBanner.tsx +23 -0
- package/src/components/StudioPreviewArea.tsx +7 -0
- package/src/components/StudioRightPanel.tsx +1 -38
- package/src/components/editor/DomEditOverlay.test.ts +169 -29
- package/src/components/editor/DomEditOverlay.tsx +13 -23
- package/src/components/editor/GestureRecordControl.tsx +98 -0
- package/src/components/editor/PropertyPanel.tsx +22 -38
- package/src/components/editor/domEditing.test.ts +84 -0
- package/src/components/editor/domEditingLayers.ts +19 -0
- package/src/components/editor/domEditingRootLayer.ts +64 -0
- package/src/components/editor/manualEditingAvailability.test.ts +1 -2
- package/src/components/editor/manualEditingAvailability.ts +0 -7
- package/src/contexts/DomEditContext.tsx +1 -6
- package/src/hooks/gsapScriptCommitHelpers.ts +128 -0
- package/src/hooks/useDomEditCommits.ts +97 -123
- package/src/hooks/useDomEditPositionPatchCommit.ts +53 -0
- package/src/hooks/useDomEditSession.ts +59 -65
- package/src/hooks/useFileManager.ts +19 -5
- package/src/hooks/useGsapAnimationFetchFallback.ts +19 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +25 -0
- package/src/hooks/useGsapScriptCommits.ts +152 -140
- package/src/hooks/useGsapSelectionHandlers.ts +38 -8
- package/src/hooks/usePreviewPersistence.ts +90 -51
- package/src/hooks/useSafeGsapCommitMutation.ts +66 -0
- package/src/hooks/useStudioContextValue.ts +3 -19
- package/src/player/hooks/useTimelinePlayer.ts +25 -28
- package/src/player/lib/playbackAdapter.test.ts +86 -1
- package/src/player/lib/playbackAdapter.ts +62 -0
- package/src/utils/domEditSaveQueue.test.ts +117 -0
- package/src/utils/domEditSaveQueue.ts +87 -0
- package/src/utils/studioHelpers.ts +1 -1
- package/src/utils/studioSaveDiagnostics.test.ts +127 -0
- package/src/utils/studioSaveDiagnostics.ts +200 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/studioUrlState.ts +2 -8
- package/dist/assets/index-DvlSlmGV.js +0 -251
- package/dist/assets/index-rm9tn9nH.css +0 -1
- package/src/components/editor/EaseCurveEditor.tsx +0 -221
- package/src/components/editor/MotionPanel.tsx +0 -277
- package/src/components/editor/MotionPanelFields.tsx +0 -185
- package/src/components/editor/MotionPathOverlay.tsx +0 -146
- package/src/components/editor/SpringEaseEditor.tsx +0 -256
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
3
|
+
import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCache";
|
|
4
|
+
|
|
5
|
+
export function useGsapAnimationFetchFallback(projectId: string | null, gsapSourceFile: string) {
|
|
6
|
+
return useCallback(
|
|
7
|
+
(selection: DomEditSelection) => async () => {
|
|
8
|
+
const pid = projectId;
|
|
9
|
+
if (!pid) return [];
|
|
10
|
+
const parsed = await fetchParsedAnimations(pid, gsapSourceFile);
|
|
11
|
+
if (!parsed) return [];
|
|
12
|
+
return getAnimationsForElement(parsed.animations, {
|
|
13
|
+
id: selection.id ?? null,
|
|
14
|
+
selector: selection.selector ?? null,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
[projectId, gsapSourceFile],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
3
|
+
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
4
|
+
|
|
5
|
+
export function useGsapInteractionFailureTelemetry(
|
|
6
|
+
activeCompPath: string | null,
|
|
7
|
+
showToast: (message: string, tone?: "error" | "info") => void,
|
|
8
|
+
) {
|
|
9
|
+
return useCallback(
|
|
10
|
+
(error: unknown, selection: DomEditSelection, mutationType: string, label: string) => {
|
|
11
|
+
trackStudioSaveFailure({
|
|
12
|
+
source: "gsap_commit",
|
|
13
|
+
error,
|
|
14
|
+
filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
|
|
15
|
+
mutationType,
|
|
16
|
+
label,
|
|
17
|
+
targetId: selection.id,
|
|
18
|
+
targetSelector: selection.selector,
|
|
19
|
+
targetSourceFile: selection.sourceFile,
|
|
20
|
+
});
|
|
21
|
+
showToast("Failed to save animated edit.", "error");
|
|
22
|
+
},
|
|
23
|
+
[activeCompPath, showToast],
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { GsapAnimation, ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
3
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
5
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
5
6
|
import { applySoftReload } from "../utils/gsapSoftReload";
|
|
@@ -11,43 +12,18 @@ import {
|
|
|
11
12
|
readKeyframeSnapshot,
|
|
12
13
|
writeKeyframeCache,
|
|
13
14
|
} from "./gsapKeyframeCacheHelpers";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Ensures the element has an id so it can be targeted by a GSAP selector.
|
|
29
|
-
* If the element already has an id or a CSS selector, returns those.
|
|
30
|
-
* Otherwise mints a unique id and sets it on the live element.
|
|
31
|
-
*/
|
|
32
|
-
function ensureElementAddressable(selection: DomEditSelection): {
|
|
33
|
-
selector: string;
|
|
34
|
-
autoId?: string;
|
|
35
|
-
} {
|
|
36
|
-
if (selection.id) return { selector: `#${selection.id}` };
|
|
37
|
-
if (selection.selector) return { selector: selection.selector };
|
|
38
|
-
|
|
39
|
-
const el = selection.element;
|
|
40
|
-
const doc = el.ownerDocument;
|
|
41
|
-
const tag = el.tagName.toLowerCase();
|
|
42
|
-
let id = tag;
|
|
43
|
-
let n = 1;
|
|
44
|
-
while (doc.getElementById(id)) {
|
|
45
|
-
n += 1;
|
|
46
|
-
id = `${tag}-${n}`;
|
|
47
|
-
}
|
|
48
|
-
el.setAttribute("id", id);
|
|
49
|
-
return { selector: `#${id}`, autoId: id };
|
|
50
|
-
}
|
|
15
|
+
import {
|
|
16
|
+
useGsapSaveFailureTelemetry,
|
|
17
|
+
useSafeGsapCommitMutation,
|
|
18
|
+
} from "./useSafeGsapCommitMutation";
|
|
19
|
+
import {
|
|
20
|
+
GsapMutationHttpError,
|
|
21
|
+
assignGsapTargetAutoIdIfNeeded,
|
|
22
|
+
ensureElementAddressable,
|
|
23
|
+
formatGsapMutationRejectionToast,
|
|
24
|
+
PROPERTY_DEFAULTS,
|
|
25
|
+
readJsonResponseBody,
|
|
26
|
+
} from "./gsapScriptCommitHelpers";
|
|
51
27
|
|
|
52
28
|
interface MutationResult {
|
|
53
29
|
ok: boolean;
|
|
@@ -62,22 +38,44 @@ async function mutateGsapScript(
|
|
|
62
38
|
projectId: string,
|
|
63
39
|
sourceFile: string,
|
|
64
40
|
mutation: Record<string, unknown>,
|
|
65
|
-
): Promise<MutationResult
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return (await res.json()) as MutationResult;
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
41
|
+
): Promise<MutationResult> {
|
|
42
|
+
const res = await fetch(
|
|
43
|
+
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
|
|
44
|
+
{
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify(mutation),
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
|
|
79
52
|
}
|
|
53
|
+
const result = (await res.json()) as MutationResult;
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
throw new Error(`Failed to update GSAP in ${sourceFile}`);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
80
58
|
}
|
|
59
|
+
|
|
60
|
+
function executeOptimisticKeyframeCacheUpdate(options: {
|
|
61
|
+
sourceFile: string;
|
|
62
|
+
elementId: string | null | undefined;
|
|
63
|
+
apply: (entry: KeyframeCacheEntry) => KeyframeCacheEntry;
|
|
64
|
+
persist: () => Promise<void>;
|
|
65
|
+
}): Promise<void> {
|
|
66
|
+
return executeOptimistic<KeyframeCacheEntry | undefined>({
|
|
67
|
+
apply: () => {
|
|
68
|
+
const prev = readKeyframeSnapshot(options.sourceFile, options.elementId);
|
|
69
|
+
if (prev) writeKeyframeCache(options.sourceFile, options.elementId, options.apply(prev));
|
|
70
|
+
return prev;
|
|
71
|
+
},
|
|
72
|
+
persist: options.persist,
|
|
73
|
+
rollback: (prev) => {
|
|
74
|
+
writeKeyframeCache(options.sourceFile, options.elementId, prev);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
81
79
|
interface GsapScriptCommitsParams {
|
|
82
80
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
83
81
|
activeCompPath: string | null;
|
|
@@ -94,10 +92,11 @@ interface GsapScriptCommitsParams {
|
|
|
94
92
|
reloadPreview: () => void;
|
|
95
93
|
onCacheInvalidate: () => void;
|
|
96
94
|
onFileContentChanged?: (path: string, content: string) => void;
|
|
95
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
97
96
|
}
|
|
98
97
|
const DEBOUNCE_MS = 150;
|
|
99
98
|
|
|
100
|
-
// fallow-ignore-next-line complexity
|
|
99
|
+
// fallow-ignore-next-line complexity
|
|
101
100
|
export function useGsapScriptCommits({
|
|
102
101
|
projectIdRef,
|
|
103
102
|
activeCompPath,
|
|
@@ -107,6 +106,7 @@ export function useGsapScriptCommits({
|
|
|
107
106
|
reloadPreview,
|
|
108
107
|
onCacheInvalidate,
|
|
109
108
|
onFileContentChanged,
|
|
109
|
+
showToast,
|
|
110
110
|
}: GsapScriptCommitsParams) {
|
|
111
111
|
const pendingPropertyEditRef = useRef<{
|
|
112
112
|
selection: DomEditSelection;
|
|
@@ -131,11 +131,27 @@ export function useGsapScriptCommits({
|
|
|
131
131
|
) => {
|
|
132
132
|
const pid = projectIdRef.current;
|
|
133
133
|
if (!pid) return;
|
|
134
|
+
const unsafeFields = findUnsafeMutationValues(mutation);
|
|
135
|
+
if (unsafeFields.length > 0) {
|
|
136
|
+
showToast?.(
|
|
137
|
+
"Couldn't read element layout — try again at a different playhead time",
|
|
138
|
+
"error",
|
|
139
|
+
);
|
|
140
|
+
if (options.skipReload) return;
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
134
145
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
let result: MutationResult;
|
|
147
|
+
try {
|
|
148
|
+
result = await mutateGsapScript(pid, targetPath, mutation);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof GsapMutationHttpError) {
|
|
151
|
+
showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
152
|
+
}
|
|
137
153
|
if (options.skipReload) return;
|
|
138
|
-
throw
|
|
154
|
+
throw error;
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
if (result.changed === false) {
|
|
@@ -195,14 +211,23 @@ export function useGsapScriptCommits({
|
|
|
195
211
|
reloadPreview,
|
|
196
212
|
onCacheInvalidate,
|
|
197
213
|
onFileContentChanged,
|
|
214
|
+
showToast,
|
|
198
215
|
],
|
|
199
216
|
);
|
|
217
|
+
|
|
218
|
+
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
|
|
219
|
+
const commitMutationSafely = useSafeGsapCommitMutation(
|
|
220
|
+
commitMutation,
|
|
221
|
+
trackGsapSaveFailure,
|
|
222
|
+
showToast,
|
|
223
|
+
);
|
|
224
|
+
|
|
200
225
|
const flushPendingPropertyEdit = useCallback(() => {
|
|
201
226
|
const pending = pendingPropertyEditRef.current;
|
|
202
227
|
if (!pending) return;
|
|
203
228
|
pendingPropertyEditRef.current = null;
|
|
204
229
|
const { selection, animationId, property, value } = pending;
|
|
205
|
-
|
|
230
|
+
commitMutationSafely(
|
|
206
231
|
selection,
|
|
207
232
|
{ type: "update-property", animationId, property, value },
|
|
208
233
|
{
|
|
@@ -211,7 +236,7 @@ export function useGsapScriptCommits({
|
|
|
211
236
|
softReload: true,
|
|
212
237
|
},
|
|
213
238
|
);
|
|
214
|
-
}, [
|
|
239
|
+
}, [commitMutationSafely]);
|
|
215
240
|
|
|
216
241
|
const updateGsapProperty = useCallback(
|
|
217
242
|
(
|
|
@@ -239,7 +264,7 @@ export function useGsapScriptCommits({
|
|
|
239
264
|
animationId: string,
|
|
240
265
|
updates: { duration?: number; ease?: string; position?: number },
|
|
241
266
|
) => {
|
|
242
|
-
|
|
267
|
+
commitMutationSafely(
|
|
243
268
|
selection,
|
|
244
269
|
{ type: "update-meta", animationId, updates },
|
|
245
270
|
{
|
|
@@ -248,17 +273,17 @@ export function useGsapScriptCommits({
|
|
|
248
273
|
},
|
|
249
274
|
);
|
|
250
275
|
},
|
|
251
|
-
[
|
|
276
|
+
[commitMutationSafely],
|
|
252
277
|
);
|
|
253
278
|
const deleteGsapAnimation = useCallback(
|
|
254
279
|
(selection: DomEditSelection, animationId: string) => {
|
|
255
|
-
|
|
280
|
+
commitMutationSafely(
|
|
256
281
|
selection,
|
|
257
282
|
{ type: "delete", animationId, stripStudioEdits: true },
|
|
258
283
|
{ label: "Delete GSAP animation" },
|
|
259
284
|
);
|
|
260
285
|
},
|
|
261
|
-
[
|
|
286
|
+
[commitMutationSafely],
|
|
262
287
|
);
|
|
263
288
|
const deleteAllForSelector = useCallback(
|
|
264
289
|
(selection: DomEditSelection, targetSelector: string) => {
|
|
@@ -283,25 +308,14 @@ export function useGsapScriptCommits({
|
|
|
283
308
|
const pid = projectIdRef.current;
|
|
284
309
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
285
310
|
if (!pid) return;
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
hfId: selection.hfId,
|
|
295
|
-
selector: selection.selector,
|
|
296
|
-
selectorIndex: selection.selectorIndex,
|
|
297
|
-
},
|
|
298
|
-
operations: [{ type: "html-attribute", property: "id", value: autoId }],
|
|
299
|
-
}),
|
|
300
|
-
},
|
|
301
|
-
);
|
|
302
|
-
if (!res.ok) return;
|
|
303
|
-
const data = (await res.json()) as { changed?: boolean };
|
|
304
|
-
if (!data.changed) return;
|
|
311
|
+
const assigned = await assignGsapTargetAutoIdIfNeeded({
|
|
312
|
+
projectId: pid,
|
|
313
|
+
targetPath,
|
|
314
|
+
selection,
|
|
315
|
+
autoId,
|
|
316
|
+
showToast,
|
|
317
|
+
});
|
|
318
|
+
if (!assigned) return;
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
@@ -330,7 +344,7 @@ export function useGsapScriptCommits({
|
|
|
330
344
|
{ label: `Add GSAP ${method} animation` },
|
|
331
345
|
);
|
|
332
346
|
},
|
|
333
|
-
[commitMutation, projectIdRef, activeCompPath],
|
|
347
|
+
[commitMutation, projectIdRef, activeCompPath, showToast],
|
|
334
348
|
);
|
|
335
349
|
const addGsapProperty = useCallback(
|
|
336
350
|
// fallow-ignore-next-line complexity
|
|
@@ -344,23 +358,23 @@ export function useGsapScriptCommits({
|
|
|
344
358
|
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
345
359
|
defaultValue = cs ? Number.parseFloat(cs.opacity) || 1 : 1;
|
|
346
360
|
}
|
|
347
|
-
|
|
361
|
+
commitMutationSafely(
|
|
348
362
|
selection,
|
|
349
363
|
{ type: "add-property", animationId, property, defaultValue },
|
|
350
364
|
{ label: `Add GSAP ${property}` },
|
|
351
365
|
);
|
|
352
366
|
},
|
|
353
|
-
[
|
|
367
|
+
[commitMutationSafely],
|
|
354
368
|
);
|
|
355
369
|
const removeGsapProperty = useCallback(
|
|
356
370
|
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
357
|
-
|
|
371
|
+
commitMutationSafely(
|
|
358
372
|
selection,
|
|
359
373
|
{ type: "remove-property", animationId, property },
|
|
360
374
|
{ label: `Remove GSAP ${property}` },
|
|
361
375
|
);
|
|
362
376
|
},
|
|
363
|
-
[
|
|
377
|
+
[commitMutationSafely],
|
|
364
378
|
);
|
|
365
379
|
const updateGsapFromProperty = useCallback(
|
|
366
380
|
(
|
|
@@ -369,7 +383,7 @@ export function useGsapScriptCommits({
|
|
|
369
383
|
property: string,
|
|
370
384
|
value: number | string,
|
|
371
385
|
) => {
|
|
372
|
-
|
|
386
|
+
commitMutationSafely(
|
|
373
387
|
selection,
|
|
374
388
|
{ type: "update-from-property", animationId, property, value },
|
|
375
389
|
{
|
|
@@ -378,28 +392,28 @@ export function useGsapScriptCommits({
|
|
|
378
392
|
},
|
|
379
393
|
);
|
|
380
394
|
},
|
|
381
|
-
[
|
|
395
|
+
[commitMutationSafely],
|
|
382
396
|
);
|
|
383
397
|
const addGsapFromProperty = useCallback(
|
|
384
398
|
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
385
399
|
const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
|
|
386
|
-
|
|
400
|
+
commitMutationSafely(
|
|
387
401
|
selection,
|
|
388
402
|
{ type: "add-from-property", animationId, property, defaultValue },
|
|
389
403
|
{ label: `Add GSAP from-${property}` },
|
|
390
404
|
);
|
|
391
405
|
},
|
|
392
|
-
[
|
|
406
|
+
[commitMutationSafely],
|
|
393
407
|
);
|
|
394
408
|
const removeGsapFromProperty = useCallback(
|
|
395
409
|
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
396
|
-
|
|
410
|
+
commitMutationSafely(
|
|
397
411
|
selection,
|
|
398
412
|
{ type: "remove-from-property", animationId, property },
|
|
399
413
|
{ label: `Remove GSAP from-${property}` },
|
|
400
414
|
);
|
|
401
415
|
},
|
|
402
|
-
[
|
|
416
|
+
[commitMutationSafely],
|
|
403
417
|
);
|
|
404
418
|
const addKeyframe = useCallback(
|
|
405
419
|
(
|
|
@@ -411,30 +425,31 @@ export function useGsapScriptCommits({
|
|
|
411
425
|
) => {
|
|
412
426
|
const sf = selection.sourceFile || activeCompPath || "index.html";
|
|
413
427
|
const elementId = selection.id;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
commitMutation(
|
|
428
|
-
selection,
|
|
429
|
-
{ type: "add-keyframe", animationId, percentage, properties: { [property]: value } },
|
|
430
|
-
{ label: `Add keyframe at ${percentage}%`, softReload: true },
|
|
428
|
+
const mutation = {
|
|
429
|
+
type: "add-keyframe",
|
|
430
|
+
animationId,
|
|
431
|
+
percentage,
|
|
432
|
+
properties: { [property]: value },
|
|
433
|
+
};
|
|
434
|
+
void executeOptimisticKeyframeCacheUpdate({
|
|
435
|
+
sourceFile: sf,
|
|
436
|
+
elementId,
|
|
437
|
+
apply: (prev) => ({
|
|
438
|
+
...prev,
|
|
439
|
+
keyframes: [...prev.keyframes, { percentage, properties: { [property]: value } }].sort(
|
|
440
|
+
(a, b) => a.percentage - b.percentage,
|
|
431
441
|
),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
442
|
+
}),
|
|
443
|
+
persist: () =>
|
|
444
|
+
commitMutation(selection, mutation, {
|
|
445
|
+
label: `Add keyframe at ${percentage}%`,
|
|
446
|
+
softReload: true,
|
|
447
|
+
}),
|
|
448
|
+
}).catch((error) => {
|
|
449
|
+
trackGsapSaveFailure(error, selection, mutation, `Add keyframe at ${percentage}%`);
|
|
435
450
|
});
|
|
436
451
|
},
|
|
437
|
-
[commitMutation, activeCompPath],
|
|
452
|
+
[commitMutation, activeCompPath, trackGsapSaveFailure],
|
|
438
453
|
);
|
|
439
454
|
const addKeyframeBatch = useCallback(
|
|
440
455
|
(
|
|
@@ -455,29 +470,26 @@ export function useGsapScriptCommits({
|
|
|
455
470
|
(selection: DomEditSelection, animationId: string, percentage: number) => {
|
|
456
471
|
const sf = selection.sourceFile || activeCompPath || "index.html";
|
|
457
472
|
const elementId = selection.id;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
return prev;
|
|
468
|
-
},
|
|
469
|
-
persist: () =>
|
|
470
|
-
commitMutation(
|
|
471
|
-
selection,
|
|
472
|
-
{ type: "remove-keyframe", animationId, percentage },
|
|
473
|
-
{ label: `Remove keyframe at ${percentage}%`, softReload: true },
|
|
473
|
+
const mutation = { type: "remove-keyframe", animationId, percentage };
|
|
474
|
+
void executeOptimisticKeyframeCacheUpdate({
|
|
475
|
+
sourceFile: sf,
|
|
476
|
+
elementId,
|
|
477
|
+
apply: (prev) => ({
|
|
478
|
+
...prev,
|
|
479
|
+
keyframes: prev.keyframes.filter(
|
|
480
|
+
(kf) => Math.abs((kf.tweenPercentage ?? kf.percentage) - percentage) > 0.2,
|
|
474
481
|
),
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
482
|
+
}),
|
|
483
|
+
persist: () =>
|
|
484
|
+
commitMutation(selection, mutation, {
|
|
485
|
+
label: `Remove keyframe at ${percentage}%`,
|
|
486
|
+
softReload: true,
|
|
487
|
+
}),
|
|
488
|
+
}).catch((error) => {
|
|
489
|
+
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
|
|
478
490
|
});
|
|
479
491
|
},
|
|
480
|
-
[commitMutation, activeCompPath],
|
|
492
|
+
[commitMutation, activeCompPath, trackGsapSaveFailure],
|
|
481
493
|
);
|
|
482
494
|
const convertToKeyframes = useCallback(
|
|
483
495
|
(
|
|
@@ -495,13 +507,13 @@ export function useGsapScriptCommits({
|
|
|
495
507
|
);
|
|
496
508
|
const removeAllKeyframes = useCallback(
|
|
497
509
|
(selection: DomEditSelection, animationId: string) => {
|
|
498
|
-
|
|
510
|
+
commitMutationSafely(
|
|
499
511
|
selection,
|
|
500
512
|
{ type: "remove-all-keyframes", animationId },
|
|
501
513
|
{ label: "Remove all keyframes", softReload: true },
|
|
502
514
|
);
|
|
503
515
|
},
|
|
504
|
-
[
|
|
516
|
+
[commitMutationSafely],
|
|
505
517
|
);
|
|
506
518
|
const setArcPath = useCallback(
|
|
507
519
|
(
|
|
@@ -517,13 +529,13 @@ export function useGsapScriptCommits({
|
|
|
517
529
|
}>;
|
|
518
530
|
},
|
|
519
531
|
) => {
|
|
520
|
-
|
|
532
|
+
commitMutationSafely(
|
|
521
533
|
selection,
|
|
522
534
|
{ type: "set-arc-path" as const, animationId, ...config },
|
|
523
535
|
{ label: config.enabled ? "Enable arc path" : "Disable arc path", softReload: true },
|
|
524
536
|
);
|
|
525
537
|
},
|
|
526
|
-
[
|
|
538
|
+
[commitMutationSafely],
|
|
527
539
|
);
|
|
528
540
|
const updateArcSegment = useCallback(
|
|
529
541
|
(
|
|
@@ -536,23 +548,23 @@ export function useGsapScriptCommits({
|
|
|
536
548
|
cp2?: { x: number; y: number };
|
|
537
549
|
},
|
|
538
550
|
) => {
|
|
539
|
-
|
|
551
|
+
commitMutationSafely(
|
|
540
552
|
selection,
|
|
541
553
|
{ type: "update-arc-segment" as const, animationId, segmentIndex, ...update },
|
|
542
554
|
{ label: "Update arc segment", softReload: true },
|
|
543
555
|
);
|
|
544
556
|
},
|
|
545
|
-
[
|
|
557
|
+
[commitMutationSafely],
|
|
546
558
|
);
|
|
547
559
|
const removeArcPath = useCallback(
|
|
548
560
|
(selection: DomEditSelection, animationId: string) => {
|
|
549
|
-
|
|
561
|
+
commitMutationSafely(
|
|
550
562
|
selection,
|
|
551
563
|
{ type: "remove-arc-path" as const, animationId },
|
|
552
564
|
{ label: "Remove arc path", softReload: true },
|
|
553
565
|
);
|
|
554
566
|
},
|
|
555
|
-
[
|
|
567
|
+
[commitMutationSafely],
|
|
556
568
|
);
|
|
557
569
|
const commitKeyframeAtTime = useCallback(
|
|
558
570
|
(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
3
3
|
import { usePlayerStore } from "../player";
|
|
4
|
+
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Thin useCallback wrappers that guard on `domEditSelection` before
|
|
@@ -46,7 +47,7 @@ export function useGsapSelectionHandlers({
|
|
|
46
47
|
sel: DomEditSelection,
|
|
47
48
|
method: "to" | "from" | "set" | "fromTo",
|
|
48
49
|
time: number,
|
|
49
|
-
) => void
|
|
50
|
+
) => Promise<void>;
|
|
50
51
|
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
|
|
51
52
|
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
|
|
52
53
|
updateGsapFromProperty: (
|
|
@@ -75,7 +76,7 @@ export function useGsapSelectionHandlers({
|
|
|
75
76
|
sel: DomEditSelection,
|
|
76
77
|
animId: string,
|
|
77
78
|
resolvedFromValues?: Record<string, number | string>,
|
|
78
|
-
) => void
|
|
79
|
+
) => Promise<void>;
|
|
79
80
|
removeAllKeyframes: (sel: DomEditSelection, animId: string) => void;
|
|
80
81
|
|
|
81
82
|
handleDomManualEditsReset: (sel: DomEditSelection) => void;
|
|
@@ -84,6 +85,22 @@ export function useGsapSelectionHandlers({
|
|
|
84
85
|
const lastSelectionRef = useRef<DomEditSelection | null>(null);
|
|
85
86
|
if (domEditSelection) lastSelectionRef.current = domEditSelection;
|
|
86
87
|
|
|
88
|
+
const trackGsapHandlerFailure = useCallback(
|
|
89
|
+
(error: unknown, selection: DomEditSelection, mutationType: string, label: string) => {
|
|
90
|
+
trackStudioSaveFailure({
|
|
91
|
+
source: "gsap_commit",
|
|
92
|
+
error,
|
|
93
|
+
filePath: selection.sourceFile ?? undefined,
|
|
94
|
+
mutationType,
|
|
95
|
+
label,
|
|
96
|
+
targetId: selection.id,
|
|
97
|
+
targetSelector: selection.selector,
|
|
98
|
+
targetSourceFile: selection.sourceFile,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
[],
|
|
102
|
+
);
|
|
103
|
+
|
|
87
104
|
const handleGsapUpdateProperty = useCallback(
|
|
88
105
|
(animId: string, prop: string, value: number | string) => {
|
|
89
106
|
if (!domEditSelection) return;
|
|
@@ -121,12 +138,16 @@ export function useGsapSelectionHandlers({
|
|
|
121
138
|
const handleGsapAddAnimation = useCallback(
|
|
122
139
|
(method: "to" | "from" | "set" | "fromTo") => {
|
|
123
140
|
if (!domEditSelection) return;
|
|
124
|
-
addGsapAnimation(domEditSelection, method, usePlayerStore.getState().currentTime)
|
|
141
|
+
void addGsapAnimation(domEditSelection, method, usePlayerStore.getState().currentTime).catch(
|
|
142
|
+
(error) => {
|
|
143
|
+
trackGsapHandlerFailure(error, domEditSelection, "add", `Add GSAP ${method} animation`);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
125
146
|
if (domEditSelection.element.hasAttribute("data-hf-studio-path-offset")) {
|
|
126
147
|
handleDomManualEditsReset(domEditSelection);
|
|
127
148
|
}
|
|
128
149
|
},
|
|
129
|
-
[domEditSelection, addGsapAnimation, handleDomManualEditsReset],
|
|
150
|
+
[domEditSelection, addGsapAnimation, handleDomManualEditsReset, trackGsapHandlerFailure],
|
|
130
151
|
);
|
|
131
152
|
|
|
132
153
|
const handleGsapAddProperty = useCallback(
|
|
@@ -180,9 +201,11 @@ export function useGsapSelectionHandlers({
|
|
|
180
201
|
const handleGsapAddKeyframeBatch = useCallback(
|
|
181
202
|
(animId: string, percentage: number, properties: Record<string, number | string>) => {
|
|
182
203
|
if (!domEditSelection) return Promise.resolve();
|
|
183
|
-
return addKeyframeBatch(domEditSelection, animId, percentage, properties)
|
|
204
|
+
return addKeyframeBatch(domEditSelection, animId, percentage, properties).catch((error) => {
|
|
205
|
+
trackGsapHandlerFailure(error, domEditSelection, "add-keyframe", "Add keyframe");
|
|
206
|
+
});
|
|
184
207
|
},
|
|
185
|
-
[domEditSelection, addKeyframeBatch],
|
|
208
|
+
[domEditSelection, addKeyframeBatch, trackGsapHandlerFailure],
|
|
186
209
|
);
|
|
187
210
|
const handleGsapRemoveKeyframe = useCallback(
|
|
188
211
|
(animId: string, percentage: number) => {
|
|
@@ -195,9 +218,16 @@ export function useGsapSelectionHandlers({
|
|
|
195
218
|
const handleGsapConvertToKeyframes = useCallback(
|
|
196
219
|
(animId: string, resolvedFromValues?: Record<string, number | string>) => {
|
|
197
220
|
if (!domEditSelection) return Promise.resolve();
|
|
198
|
-
return convertToKeyframes(domEditSelection, animId, resolvedFromValues)
|
|
221
|
+
return convertToKeyframes(domEditSelection, animId, resolvedFromValues).catch((error) => {
|
|
222
|
+
trackGsapHandlerFailure(
|
|
223
|
+
error,
|
|
224
|
+
domEditSelection,
|
|
225
|
+
"convert-to-keyframes",
|
|
226
|
+
"Convert to keyframes",
|
|
227
|
+
);
|
|
228
|
+
});
|
|
199
229
|
},
|
|
200
|
-
[domEditSelection, convertToKeyframes],
|
|
230
|
+
[domEditSelection, convertToKeyframes, trackGsapHandlerFailure],
|
|
201
231
|
);
|
|
202
232
|
|
|
203
233
|
const handleGsapRemoveAllKeyframes = useCallback(
|