@hyperframes/studio 0.6.96 → 0.6.98

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.
Files changed (120) hide show
  1. package/dist/assets/hyperframes-player-DgsMQSvV.js +418 -0
  2. package/dist/assets/index-B62bDCQv.css +1 -0
  3. package/dist/assets/index-Ce3pBm_I.js +252 -0
  4. package/dist/assets/{index-BWFaypdT.js → index-D-ET9M0b.js} +1 -1
  5. package/dist/assets/index-D-bS9Dxx.js +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +7 -5
  8. package/src/App.tsx +182 -177
  9. package/src/captions/store.ts +11 -11
  10. package/src/components/StudioHeader.tsx +4 -4
  11. package/src/components/StudioLeftSidebar.tsx +2 -2
  12. package/src/components/StudioPreviewArea.tsx +225 -183
  13. package/src/components/StudioRightPanel.tsx +3 -3
  14. package/src/components/TimelineToolbar.tsx +25 -0
  15. package/src/components/editor/DomEditOverlay.tsx +2 -5
  16. package/src/components/editor/EaseCurveSection.tsx +2 -3
  17. package/src/components/editor/GestureTrailOverlay.tsx +4 -3
  18. package/src/components/editor/LayersPanel.tsx +3 -9
  19. package/src/components/editor/PropertyPanel.tsx +20 -61
  20. package/src/components/editor/colorValue.ts +3 -1
  21. package/src/components/editor/domEditOverlayGestures.ts +54 -1
  22. package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
  23. package/src/components/editor/gradientValue.ts +3 -3
  24. package/src/components/editor/keyframeMove.test.ts +101 -0
  25. package/src/components/editor/keyframeMove.ts +151 -0
  26. package/src/components/editor/manualEditsDom.ts +0 -12
  27. package/src/components/editor/propertyPanelHelpers.ts +10 -38
  28. package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
  29. package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
  30. package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
  31. package/src/components/editor/studioMotionOps.test.ts +1 -1
  32. package/src/components/editor/studioMotionOps.ts +2 -1
  33. package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
  34. package/src/components/nle/NLELayout.tsx +1 -24
  35. package/src/components/sidebar/BlocksTab.tsx +2 -2
  36. package/src/contexts/DomEditContext.tsx +134 -31
  37. package/src/contexts/StudioContext.tsx +90 -40
  38. package/src/contexts/TimelineEditContext.tsx +47 -0
  39. package/src/hooks/domEditCommitTypes.ts +14 -0
  40. package/src/hooks/gsapDragCommit.ts +9 -24
  41. package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
  42. package/src/hooks/gsapKeyframeCommit.ts +5 -15
  43. package/src/hooks/gsapRuntimeBridge.ts +18 -52
  44. package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
  45. package/src/hooks/gsapRuntimeReaders.ts +19 -26
  46. package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
  47. package/src/hooks/gsapScriptCommitTypes.ts +58 -0
  48. package/src/hooks/gsapShared.ts +157 -0
  49. package/src/hooks/timelineEditingHelpers.ts +63 -2
  50. package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
  51. package/src/hooks/useAppHotkeys.ts +299 -377
  52. package/src/hooks/useConsoleErrorCapture.ts +33 -5
  53. package/src/hooks/useDomEditCommits.ts +35 -293
  54. package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
  55. package/src/hooks/useDomEditSession.ts +78 -249
  56. package/src/hooks/useDomEditTextCommits.ts +1 -1
  57. package/src/hooks/useDomEditWiring.ts +255 -0
  58. package/src/hooks/useDomGeometryCommits.ts +181 -0
  59. package/src/hooks/useDomSelection.ts +10 -27
  60. package/src/hooks/useEditorSave.ts +82 -0
  61. package/src/hooks/useElementLifecycleOps.ts +177 -0
  62. package/src/hooks/useEnableKeyframes.ts +10 -15
  63. package/src/hooks/useFileManager.ts +32 -114
  64. package/src/hooks/useFileTree.ts +80 -0
  65. package/src/hooks/useGestureCommit.ts +7 -5
  66. package/src/hooks/useGestureRecording.ts +1 -1
  67. package/src/hooks/useGsapAnimationOps.ts +122 -0
  68. package/src/hooks/useGsapArcPathOps.ts +61 -0
  69. package/src/hooks/useGsapAwareEditing.ts +242 -0
  70. package/src/hooks/useGsapKeyframeOps.ts +167 -0
  71. package/src/hooks/useGsapPropertyDebounce.ts +135 -0
  72. package/src/hooks/useGsapScriptCommits.ts +58 -570
  73. package/src/hooks/useGsapSelectionHandlers.ts +22 -9
  74. package/src/hooks/useGsapTweenCache.ts +35 -29
  75. package/src/hooks/useLintModal.ts +7 -0
  76. package/src/hooks/useMusicBeatAnalysis.ts +152 -0
  77. package/src/hooks/useRazorSplit.ts +1 -1
  78. package/src/hooks/useRenderClipContent.ts +46 -21
  79. package/src/hooks/useTimelineEditing.ts +48 -4
  80. package/src/player/components/AudioWaveform.tsx +29 -4
  81. package/src/player/components/BeatStrip.tsx +166 -0
  82. package/src/player/components/Timeline.tsx +39 -18
  83. package/src/player/components/TimelineCanvas.tsx +52 -12
  84. package/src/player/components/TimelineClipDiamonds.tsx +130 -20
  85. package/src/player/components/TimelinePropertyRows.tsx +8 -2
  86. package/src/player/components/TimelineRuler.tsx +36 -2
  87. package/src/player/components/timelineEditing.ts +30 -5
  88. package/src/player/components/useTimelineClipDrag.ts +155 -4
  89. package/src/player/components/useTimelinePlayhead.ts +30 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +47 -45
  91. package/src/player/lib/mediaProbe.ts +46 -3
  92. package/src/player/lib/playbackScrub.ts +16 -0
  93. package/src/player/lib/timelineDOM.ts +10 -2
  94. package/src/player/lib/timelineIframeHelpers.ts +89 -0
  95. package/src/player/store/playerStore.ts +92 -33
  96. package/src/utils/beatEditActions.ts +109 -0
  97. package/src/utils/beatEditing.ts +136 -0
  98. package/src/utils/clipboardPayload.ts +3 -2
  99. package/src/utils/compositionPatterns.ts +2 -0
  100. package/src/utils/keyframeSelection.test.ts +45 -0
  101. package/src/utils/keyframeSelection.ts +29 -0
  102. package/src/utils/rounding.ts +9 -0
  103. package/src/utils/studioHelpers.ts +5 -2
  104. package/src/utils/studioUrlState.ts +2 -1
  105. package/src/utils/timelineAssetDrop.ts +6 -5
  106. package/src/utils/timelineInspector.ts +15 -100
  107. package/dist/assets/hyperframes-player-0esDKGRk.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-BA979yF1.js +0 -251
  110. package/src/components/editor/DopesheetStrip.tsx +0 -141
  111. package/src/components/editor/StaggerControls.tsx +0 -61
  112. package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
  113. package/src/components/editor/TimelineLayerPanel.tsx +0 -15
  114. package/src/components/nle/TimelineEditorNotice.tsx +0 -133
  115. package/src/hooks/gsapRuntimePreview.ts +0 -19
  116. package/src/player/components/timelineUtils.ts +0 -211
  117. package/src/utils/audioBeatDetection.ts +0 -58
  118. package/src/utils/keyframeSnapping.test.ts +0 -74
  119. package/src/utils/keyframeSnapping.ts +0 -63
  120. package/src/utils/timelineInspector.test.ts +0 -79
@@ -1,38 +1,26 @@
1
- import { useCallback, useEffect, useRef } from "react";
2
- import type { GsapAnimation, ParsedGsap } from "@hyperframes/core/gsap-parser";
1
+ import { useCallback } from "react";
3
2
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
4
3
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
5
- import type { EditHistoryKind } from "../utils/editHistory";
6
4
  import { applySoftReload } from "../utils/gsapSoftReload";
7
- import { executeOptimistic } from "../utils/optimisticUpdate";
8
- import type { KeyframeCacheEntry } from "../player/store/playerStore";
9
- import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
10
- import {
11
- updateKeyframeCacheFromParsed,
12
- readKeyframeSnapshot,
13
- writeKeyframeCache,
14
- } from "./gsapKeyframeCacheHelpers";
15
- import {
16
- useGsapSaveFailureTelemetry,
17
- useSafeGsapCommitMutation,
18
- } from "./useSafeGsapCommitMutation";
5
+ import { updateKeyframeCacheFromParsed } from "./gsapKeyframeCacheHelpers";
19
6
  import {
20
7
  GsapMutationHttpError,
21
- assignGsapTargetAutoIdIfNeeded,
22
- ensureElementAddressable,
23
8
  formatGsapMutationRejectionToast,
24
- PROPERTY_DEFAULTS,
25
9
  readJsonResponseBody,
26
10
  } from "./gsapScriptCommitHelpers";
27
-
28
- interface MutationResult {
29
- ok: boolean;
30
- changed?: boolean;
31
- parsed?: ParsedGsap;
32
- before?: string;
33
- after?: string;
34
- scriptText?: string;
35
- }
11
+ import type {
12
+ CommitMutationOptions,
13
+ GsapScriptCommitsParams,
14
+ MutationResult,
15
+ } from "./gsapScriptCommitTypes";
16
+ import { useGsapAnimationOps } from "./useGsapAnimationOps";
17
+ import { useGsapArcPathOps } from "./useGsapArcPathOps";
18
+ import { useGsapKeyframeOps } from "./useGsapKeyframeOps";
19
+ import { useGsapPropertyDebounce } from "./useGsapPropertyDebounce";
20
+ import {
21
+ useGsapSaveFailureTelemetry,
22
+ useSafeGsapCommitMutation,
23
+ } from "./useSafeGsapCommitMutation";
36
24
 
37
25
  async function mutateGsapScript(
38
26
  projectId: string,
@@ -47,554 +35,54 @@ async function mutateGsapScript(
47
35
  body: JSON.stringify(mutation),
48
36
  },
49
37
  );
50
- if (!res.ok) {
51
- throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
52
- }
38
+ if (!res.ok) throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
53
39
  const result = (await res.json()) as MutationResult;
54
- if (!result.ok) {
55
- throw new Error(`Failed to update GSAP in ${sourceFile}`);
56
- }
40
+ if (!result.ok) throw new Error(`Failed to update GSAP in ${sourceFile}`);
57
41
  return result;
58
42
  }
59
43
 
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
-
79
- interface GsapScriptCommitsParams {
80
- projectIdRef: React.MutableRefObject<string | null>;
81
- activeCompPath: string | null;
82
- previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
83
- editHistory: {
84
- recordEdit: (entry: {
85
- label: string;
86
- kind: EditHistoryKind;
87
- coalesceKey?: string;
88
- files: Record<string, { before: string; after: string }>;
89
- }) => Promise<void>;
90
- };
91
- domEditSaveTimestampRef: React.MutableRefObject<number>;
92
- reloadPreview: () => void;
93
- onCacheInvalidate: () => void;
94
- onFileContentChanged?: (path: string, content: string) => void;
95
- showToast: (message: string, tone?: "error" | "info") => void;
96
- }
97
- const DEBOUNCE_MS = 150;
98
-
44
+ // oxfmt-ignore
99
45
  // fallow-ignore-next-line complexity
100
- export function useGsapScriptCommits({
101
- projectIdRef,
102
- activeCompPath,
103
- previewIframeRef,
104
- editHistory,
105
- domEditSaveTimestampRef,
106
- reloadPreview,
107
- onCacheInvalidate,
108
- onFileContentChanged,
109
- showToast,
110
- }: GsapScriptCommitsParams) {
111
- const pendingPropertyEditRef = useRef<{
112
- selection: DomEditSelection;
113
- animationId: string;
114
- property: string;
115
- value: number | string;
116
- } | null>(null);
117
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
118
- /** Send a mutation and record the edit in undo history. */
119
- const commitMutation = useCallback(
120
- // fallow-ignore-next-line complexity
121
- async (
122
- selection: DomEditSelection,
123
- mutation: Record<string, unknown>,
124
- options: {
125
- label: string;
126
- coalesceKey?: string;
127
- softReload?: boolean;
128
- skipReload?: boolean;
129
- beforeReload?: () => void;
130
- },
131
- ) => {
132
- const pid = projectIdRef.current;
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
- }
145
- const targetPath = selection.sourceFile || activeCompPath || "index.html";
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
- }
153
- if (options.skipReload) return;
154
- throw error;
155
- }
156
-
157
- if (result.changed === false) {
158
- if (options.skipReload) return;
159
- return;
160
- }
161
-
162
- domEditSaveTimestampRef.current = Date.now();
163
-
164
- if (result.before != null && result.after != null) {
165
- await editHistory.recordEdit({
166
- label: options.label,
167
- kind: "manual",
168
- coalesceKey: options.coalesceKey,
169
- files: { [targetPath]: { before: result.before, after: result.after } },
170
- });
171
- }
172
-
173
- if (result.after != null) {
174
- onFileContentChanged?.(targetPath, result.after);
175
- }
176
-
46
+ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast }: GsapScriptCommitsParams) {
47
+ const commitMutation = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
48
+ const pid = projectIdRef.current;
49
+ if (!pid) return;
50
+ const unsafeFields = findUnsafeMutationValues(mutation);
51
+ if (unsafeFields.length > 0) {
52
+ showToast?.("Couldn't read element layout — try again at a different playhead time", "error");
177
53
  if (options.skipReload) return;
178
-
179
- // Write the keyframe cache immediately from the parsed response
180
- // (synchronous the timeline diamonds appear on the next render).
181
- if (result.parsed?.animations) {
182
- updateKeyframeCacheFromParsed(
183
- result.parsed.animations,
184
- targetPath,
185
- selection.id ?? undefined,
186
- mutation,
187
- );
188
- }
189
-
190
- options.beforeReload?.();
191
-
192
- if (options.softReload && result.scriptText) {
193
- if (!applySoftReload(previewIframeRef.current, result.scriptText)) {
194
- reloadPreview();
195
- }
196
- } else {
197
- reloadPreview();
198
- }
199
-
200
- // Bump the cache version AFTER reload so the async re-fetch in
201
- // useGsapAnimationsForElement reads the post-reload script, not
202
- // the stale pre-reload version that would overwrite fresh data.
203
- onCacheInvalidate();
204
- },
205
- [
206
- projectIdRef,
207
- activeCompPath,
208
- previewIframeRef,
209
- editHistory,
210
- domEditSaveTimestampRef,
211
- reloadPreview,
212
- onCacheInvalidate,
213
- onFileContentChanged,
214
- showToast,
215
- ],
216
- );
217
-
54
+ throw new Error(`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`);
55
+ }
56
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
57
+ let result: MutationResult;
58
+ try {
59
+ result = await mutateGsapScript(pid, targetPath, mutation);
60
+ } catch (error) {
61
+ if (error instanceof GsapMutationHttpError) showToast?.(formatGsapMutationRejectionToast(error), "error");
62
+ if (options.skipReload) return;
63
+ throw error;
64
+ }
65
+ if (result.changed === false) return;
66
+ domEditSaveTimestampRef.current = Date.now();
67
+ if (result.before != null && result.after != null) {
68
+ await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
69
+ }
70
+ if (result.after != null) onFileContentChanged?.(targetPath, result.after);
71
+ if (options.skipReload) return;
72
+ if (result.parsed?.animations) updateKeyframeCacheFromParsed(result.parsed.animations, targetPath, selection.id ?? undefined, mutation);
73
+ options.beforeReload?.();
74
+ if (options.softReload && result.scriptText) {
75
+ if (!applySoftReload(previewIframeRef.current, result.scriptText)) reloadPreview();
76
+ } else {
77
+ reloadPreview();
78
+ }
79
+ onCacheInvalidate();
80
+ }, [projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast]);
218
81
  const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
219
- const commitMutationSafely = useSafeGsapCommitMutation(
220
- commitMutation,
221
- trackGsapSaveFailure,
222
- showToast,
223
- );
224
-
225
- const flushPendingPropertyEdit = useCallback(() => {
226
- const pending = pendingPropertyEditRef.current;
227
- if (!pending) return;
228
- pendingPropertyEditRef.current = null;
229
- const { selection, animationId, property, value } = pending;
230
- commitMutationSafely(
231
- selection,
232
- { type: "update-property", animationId, property, value },
233
- {
234
- label: `Edit GSAP ${property}`,
235
- coalesceKey: `gsap:${animationId}:${property}`,
236
- softReload: true,
237
- },
238
- );
239
- }, [commitMutationSafely]);
240
-
241
- const updateGsapProperty = useCallback(
242
- (
243
- selection: DomEditSelection,
244
- animationId: string,
245
- property: string,
246
- value: number | string,
247
- ) => {
248
- pendingPropertyEditRef.current = { selection, animationId, property, value };
249
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
250
- debounceTimerRef.current = setTimeout(flushPendingPropertyEdit, DEBOUNCE_MS);
251
- },
252
- [flushPendingPropertyEdit],
253
- );
254
- useEffect(() => {
255
- return () => {
256
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
257
- flushPendingPropertyEdit();
258
- };
259
- }, [flushPendingPropertyEdit]);
260
-
261
- const updateGsapMeta = useCallback(
262
- (
263
- selection: DomEditSelection,
264
- animationId: string,
265
- updates: { duration?: number; ease?: string; position?: number },
266
- ) => {
267
- commitMutationSafely(
268
- selection,
269
- { type: "update-meta", animationId, updates },
270
- {
271
- label: "Edit GSAP animation",
272
- coalesceKey: `gsap:${animationId}:meta`,
273
- },
274
- );
275
- },
276
- [commitMutationSafely],
277
- );
278
- const deleteGsapAnimation = useCallback(
279
- (selection: DomEditSelection, animationId: string) => {
280
- commitMutationSafely(
281
- selection,
282
- { type: "delete", animationId, stripStudioEdits: true },
283
- { label: "Delete GSAP animation" },
284
- );
285
- },
286
- [commitMutationSafely],
287
- );
288
- const deleteAllForSelector = useCallback(
289
- (selection: DomEditSelection, targetSelector: string) => {
290
- void commitMutation(
291
- selection,
292
- { type: "delete-all-for-selector", targetSelector },
293
- { label: "Delete all animations for element" },
294
- );
295
- },
296
- [commitMutation],
297
- );
298
- const addGsapAnimation = useCallback(
299
- // fallow-ignore-next-line complexity
300
- async (
301
- selection: DomEditSelection,
302
- method: "to" | "from" | "set" | "fromTo",
303
- _currentTime?: number,
304
- ) => {
305
- const { selector, autoId } = ensureElementAddressable(selection);
306
-
307
- if (autoId) {
308
- const pid = projectIdRef.current;
309
- const targetPath = selection.sourceFile || activeCompPath || "index.html";
310
- if (!pid) return;
311
- const assigned = await assignGsapTargetAutoIdIfNeeded({
312
- projectId: pid,
313
- targetPath,
314
- selection,
315
- autoId,
316
- showToast,
317
- });
318
- if (!assigned) return;
319
- }
320
-
321
- const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
322
- const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
323
- const position = Math.round(elStart * 1000) / 1000;
324
- const duration = Math.round(elDuration * 1000) / 1000;
325
- const toDefaults: Record<string, Record<string, number>> = {
326
- from: { opacity: 0 },
327
- to: { x: 0, y: 0, opacity: 1 },
328
- set: { opacity: 1 },
329
- fromTo: { x: 0, y: 0, opacity: 1 },
330
- };
331
-
332
- await commitMutation(
333
- selection,
334
- {
335
- type: "add",
336
- targetSelector: selector,
337
- method,
338
- position,
339
- duration: method === "set" ? undefined : duration,
340
- ease: method === "set" ? undefined : "power2.out",
341
- properties: toDefaults[method] ?? { opacity: 1 },
342
- fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
343
- },
344
- { label: `Add GSAP ${method} animation` },
345
- );
346
- },
347
- [commitMutation, projectIdRef, activeCompPath, showToast],
348
- );
349
- const addGsapProperty = useCallback(
350
- // fallow-ignore-next-line complexity
351
- (selection: DomEditSelection, animationId: string, property: string) => {
352
- let defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
353
- const el = selection.element;
354
- if (property === "width" || property === "height") {
355
- const rect = el.getBoundingClientRect();
356
- defaultValue = Math.round(property === "width" ? rect.width : rect.height);
357
- } else if (property === "opacity" || property === "autoAlpha") {
358
- const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
359
- defaultValue = cs ? Number.parseFloat(cs.opacity) || 1 : 1;
360
- }
361
- commitMutationSafely(
362
- selection,
363
- { type: "add-property", animationId, property, defaultValue },
364
- { label: `Add GSAP ${property}` },
365
- );
366
- },
367
- [commitMutationSafely],
368
- );
369
- const removeGsapProperty = useCallback(
370
- (selection: DomEditSelection, animationId: string, property: string) => {
371
- commitMutationSafely(
372
- selection,
373
- { type: "remove-property", animationId, property },
374
- { label: `Remove GSAP ${property}` },
375
- );
376
- },
377
- [commitMutationSafely],
378
- );
379
- const updateGsapFromProperty = useCallback(
380
- (
381
- selection: DomEditSelection,
382
- animationId: string,
383
- property: string,
384
- value: number | string,
385
- ) => {
386
- commitMutationSafely(
387
- selection,
388
- { type: "update-from-property", animationId, property, value },
389
- {
390
- label: `Edit GSAP from-${property}`,
391
- coalesceKey: `gsap:${animationId}:from:${property}`,
392
- },
393
- );
394
- },
395
- [commitMutationSafely],
396
- );
397
- const addGsapFromProperty = useCallback(
398
- (selection: DomEditSelection, animationId: string, property: string) => {
399
- const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
400
- commitMutationSafely(
401
- selection,
402
- { type: "add-from-property", animationId, property, defaultValue },
403
- { label: `Add GSAP from-${property}` },
404
- );
405
- },
406
- [commitMutationSafely],
407
- );
408
- const removeGsapFromProperty = useCallback(
409
- (selection: DomEditSelection, animationId: string, property: string) => {
410
- commitMutationSafely(
411
- selection,
412
- { type: "remove-from-property", animationId, property },
413
- { label: `Remove GSAP from-${property}` },
414
- );
415
- },
416
- [commitMutationSafely],
417
- );
418
- const addKeyframe = useCallback(
419
- (
420
- selection: DomEditSelection,
421
- animationId: string,
422
- percentage: number,
423
- property: string,
424
- value: number | string,
425
- ) => {
426
- const sf = selection.sourceFile || activeCompPath || "index.html";
427
- const elementId = selection.id;
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,
441
- ),
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}%`);
450
- });
451
- },
452
- [commitMutation, activeCompPath, trackGsapSaveFailure],
453
- );
454
- const addKeyframeBatch = useCallback(
455
- (
456
- selection: DomEditSelection,
457
- animationId: string,
458
- percentage: number,
459
- properties: Record<string, number | string>,
460
- ) => {
461
- return commitMutation(
462
- selection,
463
- { type: "add-keyframe", animationId, percentage, properties },
464
- { label: `Add keyframe at ${percentage}%`, softReload: true },
465
- );
466
- },
467
- [commitMutation],
468
- );
469
- const removeKeyframe = useCallback(
470
- (selection: DomEditSelection, animationId: string, percentage: number) => {
471
- const sf = selection.sourceFile || activeCompPath || "index.html";
472
- const elementId = selection.id;
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,
481
- ),
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}%`);
490
- });
491
- },
492
- [commitMutation, activeCompPath, trackGsapSaveFailure],
493
- );
494
- const convertToKeyframes = useCallback(
495
- (
496
- selection: DomEditSelection,
497
- animationId: string,
498
- resolvedFromValues?: Record<string, number | string>,
499
- ) => {
500
- return commitMutation(
501
- selection,
502
- { type: "convert-to-keyframes", animationId, resolvedFromValues },
503
- { label: "Convert to keyframes" },
504
- );
505
- },
506
- [commitMutation],
507
- );
508
- const removeAllKeyframes = useCallback(
509
- (selection: DomEditSelection, animationId: string) => {
510
- commitMutationSafely(
511
- selection,
512
- { type: "remove-all-keyframes", animationId },
513
- { label: "Remove all keyframes", softReload: true },
514
- );
515
- },
516
- [commitMutationSafely],
517
- );
518
- const setArcPath = useCallback(
519
- (
520
- selection: DomEditSelection,
521
- animationId: string,
522
- config: {
523
- enabled: boolean;
524
- autoRotate?: boolean | number;
525
- segments?: Array<{
526
- curviness: number;
527
- cp1?: { x: number; y: number };
528
- cp2?: { x: number; y: number };
529
- }>;
530
- },
531
- ) => {
532
- commitMutationSafely(
533
- selection,
534
- { type: "set-arc-path" as const, animationId, ...config },
535
- { label: config.enabled ? "Enable arc path" : "Disable arc path", softReload: true },
536
- );
537
- },
538
- [commitMutationSafely],
539
- );
540
- const updateArcSegment = useCallback(
541
- (
542
- selection: DomEditSelection,
543
- animationId: string,
544
- segmentIndex: number,
545
- update: {
546
- curviness?: number;
547
- cp1?: { x: number; y: number };
548
- cp2?: { x: number; y: number };
549
- },
550
- ) => {
551
- commitMutationSafely(
552
- selection,
553
- { type: "update-arc-segment" as const, animationId, segmentIndex, ...update },
554
- { label: "Update arc segment", softReload: true },
555
- );
556
- },
557
- [commitMutationSafely],
558
- );
559
- const removeArcPath = useCallback(
560
- (selection: DomEditSelection, animationId: string) => {
561
- commitMutationSafely(
562
- selection,
563
- { type: "remove-arc-path" as const, animationId },
564
- { label: "Remove arc path", softReload: true },
565
- );
566
- },
567
- [commitMutationSafely],
568
- );
569
- const commitKeyframeAtTime = useCallback(
570
- (
571
- selection: DomEditSelection,
572
- absoluteTime: number,
573
- animations: GsapAnimation[],
574
- properties: Record<string, number | string>,
575
- ) => commitKeyframeAtTimeImpl(selection, absoluteTime, animations, properties, commitMutation),
576
- [commitMutation],
577
- );
578
- return {
579
- commitMutation,
580
- updateGsapProperty,
581
- updateGsapMeta,
582
- deleteGsapAnimation,
583
- deleteAllForSelector,
584
- addGsapAnimation,
585
- addGsapProperty,
586
- removeGsapProperty,
587
- updateGsapFromProperty,
588
- addGsapFromProperty,
589
- removeGsapFromProperty,
590
- addKeyframe,
591
- addKeyframeBatch,
592
- removeKeyframe,
593
- convertToKeyframes,
594
- removeAllKeyframes,
595
- setArcPath,
596
- updateArcSegment,
597
- removeArcPath,
598
- commitKeyframeAtTime,
599
- };
82
+ const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToast);
83
+ const propertyOps = useGsapPropertyDebounce(commitMutationSafely);
84
+ const animationOps = useGsapAnimationOps({ projectIdRef, activeCompPath, commitMutation, commitMutationSafely, showToast });
85
+ const keyframeOps = useGsapKeyframeOps({ activeCompPath, commitMutation, commitMutationSafely, trackGsapSaveFailure });
86
+ const arcPathOps = useGsapArcPathOps(commitMutationSafely);
87
+ return { commitMutation, ...propertyOps, ...animationOps, ...keyframeOps, ...arcPathOps };
600
88
  }
@@ -110,9 +110,14 @@ export function useGsapSelectionHandlers({
110
110
  );
111
111
 
112
112
  const handleGsapUpdateMeta = useCallback(
113
- (animId: string, updates: { duration?: number; ease?: string; position?: number }) => {
114
- if (!domEditSelection) return;
115
- updateGsapMeta(domEditSelection, animId, updates);
113
+ (
114
+ animId: string,
115
+ updates: { duration?: number; ease?: string; position?: number },
116
+ selectionOverride?: DomEditSelection | null,
117
+ ) => {
118
+ const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
119
+ if (!sel) return;
120
+ updateGsapMeta(sel, animId, updates);
116
121
  },
117
122
  [domEditSelection, updateGsapMeta],
118
123
  );
@@ -191,9 +196,16 @@ export function useGsapSelectionHandlers({
191
196
  );
192
197
 
193
198
  const handleGsapAddKeyframe = useCallback(
194
- (animId: string, percentage: number, property: string, value: number | string) => {
195
- if (!domEditSelection) return;
196
- addKeyframe(domEditSelection, animId, percentage, property, value);
199
+ (
200
+ animId: string,
201
+ percentage: number,
202
+ property: string,
203
+ value: number | string,
204
+ selectionOverride?: DomEditSelection | null,
205
+ ) => {
206
+ const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
207
+ if (!sel) return;
208
+ addKeyframe(sel, animId, percentage, property, value);
197
209
  },
198
210
  [domEditSelection, addKeyframe],
199
211
  );
@@ -208,9 +220,10 @@ export function useGsapSelectionHandlers({
208
220
  [domEditSelection, addKeyframeBatch, trackGsapHandlerFailure],
209
221
  );
210
222
  const handleGsapRemoveKeyframe = useCallback(
211
- (animId: string, percentage: number) => {
212
- if (!domEditSelection) return;
213
- removeKeyframe(domEditSelection, animId, percentage);
223
+ (animId: string, percentage: number, selectionOverride?: DomEditSelection | null) => {
224
+ const sel = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
225
+ if (!sel) return;
226
+ removeKeyframe(sel, animId, percentage);
214
227
  },
215
228
  [domEditSelection, removeKeyframe],
216
229
  );