@hyperframes/studio 0.6.109 → 0.6.111

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 (66) hide show
  1. package/dist/assets/hyperframes-player-BRjQ5a5_.js +418 -0
  2. package/dist/assets/index-B7S86vK1.js +370 -0
  3. package/dist/assets/index-DP8pPIk2.css +1 -0
  4. package/dist/assets/{index-SlMJloLR.js → index-_IV-vm9l.js} +1 -1
  5. package/dist/assets/{index-CFCe0Xvn.js → index-x0c2-zQN.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/package.json +8 -5
  8. package/src/App.tsx +133 -141
  9. package/src/components/StudioHeader.tsx +44 -0
  10. package/src/components/StudioOverlays.tsx +70 -0
  11. package/src/components/editor/SourceEditor.tsx +19 -16
  12. package/src/components/editor/gsapAnimationConstants.ts +24 -0
  13. package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
  14. package/src/components/editor/manualEditingAvailability.ts +25 -7
  15. package/src/components/storyboard/FramePoster.tsx +56 -0
  16. package/src/components/storyboard/StoryboardDirection.tsx +45 -0
  17. package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
  18. package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
  19. package/src/components/storyboard/StoryboardGrid.tsx +33 -0
  20. package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
  21. package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
  22. package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
  23. package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
  24. package/src/components/storyboard/StoryboardView.tsx +78 -0
  25. package/src/components/storyboard/frameStatus.ts +36 -0
  26. package/src/contexts/ViewModeContext.tsx +98 -0
  27. package/src/hooks/gsapScriptCommitTypes.ts +4 -7
  28. package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
  29. package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
  30. package/src/hooks/timelineEditingHelpers.ts +2 -0
  31. package/src/hooks/useAppHotkeys.ts +20 -0
  32. package/src/hooks/useDomEditCommits.ts +43 -14
  33. package/src/hooks/useDomEditSession.test.ts +41 -0
  34. package/src/hooks/useDomEditSession.ts +38 -6
  35. package/src/hooks/useDomGeometryCommits.ts +5 -9
  36. package/src/hooks/useElementLifecycleOps.ts +30 -0
  37. package/src/hooks/useGsapAnimationOps.ts +78 -51
  38. package/src/hooks/useGsapKeyframeOps.ts +127 -43
  39. package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
  40. package/src/hooks/useGsapPropertyDebounce.ts +201 -23
  41. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
  42. package/src/hooks/useGsapScriptCommits.ts +95 -40
  43. package/src/hooks/useGsapTweenCache.ts +0 -27
  44. package/src/hooks/useRazorSplit.ts +4 -10
  45. package/src/hooks/useSdkSession.test.ts +12 -0
  46. package/src/hooks/useSdkSession.ts +91 -25
  47. package/src/hooks/useStoryboard.ts +80 -0
  48. package/src/hooks/useTimelineEditing.ts +163 -68
  49. package/src/utils/gsapSoftReload.ts +14 -0
  50. package/src/utils/sdkCutover.gate.test.ts +61 -0
  51. package/src/utils/sdkCutover.test.ts +839 -0
  52. package/src/utils/sdkCutover.ts +465 -0
  53. package/src/utils/sdkOpMapping.ts +43 -0
  54. package/src/utils/sdkResolverShadow.test.ts +366 -0
  55. package/src/utils/sdkResolverShadow.ts +313 -0
  56. package/src/utils/timelineElementSplit.test.ts +61 -1
  57. package/src/utils/timelineElementSplit.ts +18 -0
  58. package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
  59. package/dist/assets/index-BVqybwMG.css +0 -1
  60. package/dist/assets/index-ho_f4axK.js +0 -296
  61. package/src/utils/sdkShadow.test.ts +0 -606
  62. package/src/utils/sdkShadow.ts +0 -517
  63. package/src/utils/sdkShadowGsapFidelity.ts +0 -296
  64. package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
  65. package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
  66. package/src/utils/sdkShadowNumeric.ts +0 -11
@@ -117,6 +117,14 @@ interface UseAppHotkeysParams {
117
117
  onDeleteSelectedKeyframes: () => void;
118
118
  onAfterUndoRedo?: () => void;
119
119
  onToggleRecording?: () => void;
120
+ /** Active composition path — used to decide whether undo/redo must resync the SDK session. */
121
+ activeCompPath?: string | null;
122
+ /**
123
+ * Force-reload the SDK session after undo/redo reverts the active comp file,
124
+ * bypassing the self-write suppress window. Without this, the suppress window
125
+ * blocks the file-change reload and the SDK session stays on pre-undo content.
126
+ */
127
+ forceReloadSdkSession?: () => void;
120
128
  }
121
129
 
122
130
  // ── Extracted keydown dispatch (pure function, no hooks) ──
@@ -302,6 +310,8 @@ export function useAppHotkeys({
302
310
  onDeleteSelectedKeyframes,
303
311
  onAfterUndoRedo,
304
312
  onToggleRecording,
313
+ activeCompPath,
314
+ forceReloadSdkSession,
305
315
  }: UseAppHotkeysParams) {
306
316
  const previewHotkeyWindowRef = useRef<Window | null>(null);
307
317
  const previewHistoryCleanupRef = useRef<(() => void) | null>(null);
@@ -349,6 +359,14 @@ export function useAppHotkeys({
349
359
  }
350
360
  if (result.ok && result.label) {
351
361
  onAfterUndoRedo?.();
362
+ // If the active composition was among the written files, force-reload
363
+ // the SDK session so its in-memory doc matches the reverted content.
364
+ // writeHistoryFile sets domEditSaveTimestampRef which activates the
365
+ // 2 s suppress window — without this call the file-change event would
366
+ // be swallowed and the SDK session would stay on stale pre-undo content.
367
+ if (activeCompPath && result.paths?.includes(activeCompPath)) {
368
+ forceReloadSdkSession?.();
369
+ }
352
370
  await syncHistoryPreviewAfterApply(result.paths);
353
371
  showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
354
372
  }
@@ -361,6 +379,8 @@ export function useAppHotkeys({
361
379
  waitForPendingDomEditSaves,
362
380
  writeHistoryFile,
363
381
  onAfterUndoRedo,
382
+ activeCompPath,
383
+ forceReloadSdkSession,
364
384
  ],
365
385
  );
366
386
 
@@ -16,9 +16,6 @@ import { useDomGeometryCommits } from "./useDomGeometryCommits";
16
16
  import { useElementLifecycleOps } from "./useElementLifecycleOps";
17
17
  import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
18
18
 
19
- // Re-export so existing consumers keep their import path
20
- export { GSAP_CSS_FALLBACK_BLOCKED_MESSAGE } from "./useDomGeometryCommits";
21
-
22
19
  // ── Helpers ──
23
20
 
24
21
  function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
@@ -45,8 +42,6 @@ interface RecordEditInput {
45
42
  files: Record<string, { before: string; after: string }>;
46
43
  }
47
44
 
48
- export type { PersistDomEditOperations } from "./domEditCommitTypes";
49
-
50
45
  export interface UseDomEditCommitsParams {
51
46
  activeCompPath: string | null;
52
47
  previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
@@ -73,10 +68,20 @@ export interface UseDomEditCommitsParams {
73
68
  target: HTMLElement,
74
69
  options?: { preferClipAncestor?: boolean },
75
70
  ) => Promise<DomEditSelection | null>;
76
- /** Stage 7 Step 3b: called after a successful server-side element patch. */
77
- onDomEditPersisted?: (selection: DomEditSelection, operations: PatchOperation[]) => void;
78
- /** Stage 7 Step 3b: called after a successful server-side element delete. */
79
- onElementDeleted?: (selection: DomEditSelection) => void;
71
+ /** Resync the in-memory SDK session after a SERVER-side write (NOT the SDK
72
+ * path, whose session is already current) so a later SDK edit doesn't
73
+ * serialize the pre-write doc and revert the server's change. */
74
+ forceReloadSdkSession?: () => void;
75
+ /** Stage 7 Step 3c: called before the server-side patch path; returns true if SDK handled it. */
76
+ onTrySdkPersist?: (
77
+ selection: DomEditSelection,
78
+ operations: PatchOperation[],
79
+ originalContent: string,
80
+ targetPath: string,
81
+ options?: { label?: string; coalesceKey?: string; skipRefresh?: boolean },
82
+ ) => Promise<boolean>;
83
+ /** Stage 7 §3.1: called before the server-side delete path; returns true if SDK handled it. */
84
+ onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
80
85
  }
81
86
 
82
87
  export function useDomEditCommits({
@@ -97,8 +102,9 @@ export function useDomEditCommits({
97
102
  clearDomSelection,
98
103
  refreshDomEditSelectionFromPreview,
99
104
  buildDomSelectionFromTarget,
100
- onDomEditPersisted,
101
- onElementDeleted,
105
+ forceReloadSdkSession,
106
+ onTrySdkPersist,
107
+ onTrySdkDelete,
102
108
  }: UseDomEditCommitsParams) {
103
109
  const resolveImportedFontAsset = useCallback(
104
110
  (fontFamilyValue: string): ImportedFontAsset | null => {
@@ -149,6 +155,10 @@ export function useDomEditCommits({
149
155
 
150
156
  if (options?.shouldSave && !options.shouldSave()) return;
151
157
 
158
+ // Validate layout values BEFORE any persist path runs. The SDK cutover
159
+ // path (onTrySdkPersist) returns early on success, so leaving this check
160
+ // after it let invalid numeric values bypass the guard whenever the
161
+ // cutover flag was on.
152
162
  const patchTarget = buildDomEditPatchTarget(selection);
153
163
  const patchBody = { target: patchTarget, operations };
154
164
  const unsafeFields = findUnsafeDomPatchValues(patchBody);
@@ -158,6 +168,23 @@ export function useDomEditCommits({
158
168
  throw new Error(`DOM patch contains unsafe values: ${fields}`);
159
169
  }
160
170
 
171
+ // Skip the SDK path when prepareContent is set (e.g. @font-face injection
172
+ // for a custom font): sdkCutoverPersist serializes only the patched DOM
173
+ // and would drop the injected content. Let the server path run prepareContent.
174
+ if (
175
+ onTrySdkPersist &&
176
+ !options?.prepareContent &&
177
+ (await onTrySdkPersist(selection, operations, originalContent, targetPath, {
178
+ label: options?.label,
179
+ coalesceKey: options?.coalesceKey,
180
+ skipRefresh: options?.skipRefresh,
181
+ }))
182
+ ) {
183
+ // SDK handled it — its in-memory doc is already current, so do NOT
184
+ // forceReload (that would echo-reload the session we just wrote).
185
+ return;
186
+ }
187
+
161
188
  // Mark the save timestamp before the file write so the SSE file-change
162
189
  // handler suppresses the reload even if the event arrives before the
163
190
  // response (the server writes the file and emits SSE during the fetch).
@@ -220,7 +247,7 @@ export function useDomEditCommits({
220
247
  coalesceKey: options?.coalesceKey,
221
248
  files: { [targetPath]: { before: originalContent, after: finalContent } },
222
249
  });
223
- onDomEditPersisted?.(selection, operations);
250
+ forceReloadSdkSession?.();
224
251
 
225
252
  if (!options?.skipRefresh) {
226
253
  reloadPreview();
@@ -234,7 +261,8 @@ export function useDomEditCommits({
234
261
  domEditSaveTimestampRef,
235
262
  reloadPreview,
236
263
  showToast,
237
- onDomEditPersisted,
264
+ forceReloadSdkSession,
265
+ onTrySdkPersist,
238
266
  ],
239
267
  );
240
268
 
@@ -295,8 +323,9 @@ export function useDomEditCommits({
295
323
  projectIdRef,
296
324
  reloadPreview,
297
325
  clearDomSelection,
326
+ onTrySdkDelete,
327
+ forceReloadSdkSession,
298
328
  commitPositionPatchToHtml,
299
- onElementDeleted,
300
329
  });
301
330
 
302
331
  return {
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { shouldUseSdkCutover } from "../utils/sdkCutover";
3
+ import type { PatchOperation } from "../utils/sourcePatcher";
4
+
5
+ const styleOp = (property: string, value: string): PatchOperation => ({
6
+ type: "inline-style",
7
+ property,
8
+ value,
9
+ });
10
+
11
+ const attrOp = (property: string, value: string): PatchOperation => ({
12
+ type: "attribute",
13
+ property,
14
+ value,
15
+ });
16
+
17
+ describe("shouldUseSdkCutover", () => {
18
+ it("returns false when flag is disabled", () => {
19
+ expect(shouldUseSdkCutover(false, true, "hf-abc", [styleOp("color", "red")])).toBe(false);
20
+ });
21
+
22
+ it("returns false when no SDK session", () => {
23
+ expect(shouldUseSdkCutover(true, false, "hf-abc", [styleOp("color", "red")])).toBe(false);
24
+ });
25
+
26
+ it("returns false when selection has no hfId", () => {
27
+ expect(shouldUseSdkCutover(true, true, null, [styleOp("color", "red")])).toBe(false);
28
+ expect(shouldUseSdkCutover(true, true, undefined, [styleOp("color", "red")])).toBe(false);
29
+ });
30
+
31
+ it("returns false when ops array is empty", () => {
32
+ expect(shouldUseSdkCutover(true, true, "hf-abc", [])).toBe(false);
33
+ });
34
+
35
+ it("returns true when all conditions met with supported op types", () => {
36
+ expect(shouldUseSdkCutover(true, true, "hf-abc", [styleOp("color", "red")])).toBe(true);
37
+ expect(
38
+ shouldUseSdkCutover(true, true, "hf-abc", [styleOp("color", "red"), attrOp("data-x", "1")]),
39
+ ).toBe(true);
40
+ });
41
+ });
@@ -1,15 +1,16 @@
1
- import type { Composition } from "@hyperframes/sdk";
2
1
  import type { TimelineElement } from "../player";
3
2
  import type { ImportedFontAsset } from "../components/editor/fontAssets";
4
3
  import type { EditHistoryKind } from "../utils/editHistory";
5
4
  import type { RightPanelTab } from "../utils/studioHelpers";
6
5
  import type { PatchTarget } from "../utils/sourcePatcher";
7
6
  import type { SidebarTab } from "../components/sidebar/LeftSidebar";
7
+ import type { Composition } from "@hyperframes/sdk";
8
+ import { sdkCutoverPersist, sdkDeletePersist } from "../utils/sdkCutover";
9
+ import { runResolverShadow } from "../utils/sdkResolverShadow";
8
10
  import { useAskAgentModal } from "./useAskAgentModal";
9
11
  import { useDomSelection } from "./useDomSelection";
10
12
  import { usePreviewInteraction } from "./usePreviewInteraction";
11
13
  import { useDomEditCommits } from "./useDomEditCommits";
12
- import { runShadowDispatch, runShadowDelete } from "../utils/sdkShadow";
13
14
  import { useGsapScriptCommits } from "./useGsapScriptCommits";
14
15
  import { useGsapCacheVersion } from "./useGsapTweenCache";
15
16
  import { useDomEditWiring } from "./useDomEditWiring";
@@ -60,8 +61,8 @@ export interface UseDomEditSessionParams {
60
61
  openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
61
62
  selectSidebarTab?: (tab: SidebarTab) => void;
62
63
  getSidebarTab?: () => SidebarTab;
63
- /** Stage 7 Step 3b: SDK session for shadow dispatch parity tracking. */
64
64
  sdkSession?: Composition | null;
65
+ forceReloadSdkSession?: () => void;
65
66
  }
66
67
 
67
68
  // ── Hook ──
@@ -101,6 +102,7 @@ export function useDomEditSession({
101
102
  selectSidebarTab,
102
103
  getSidebarTab,
103
104
  sdkSession,
105
+ forceReloadSdkSession,
104
106
  }: UseDomEditSessionParams) {
105
107
  void _setRefreshKey;
106
108
  void _readProjectFile;
@@ -195,6 +197,8 @@ export function useDomEditSession({
195
197
  onFileContentChanged: updateEditingFileContent,
196
198
  showToast,
197
199
  sdkSession,
200
+ writeProjectFile,
201
+ forceReloadSdkSession,
198
202
  });
199
203
 
200
204
  // ── DOM commit handlers ──
@@ -234,10 +238,38 @@ export function useDomEditSession({
234
238
  clearDomSelection,
235
239
  refreshDomEditSelectionFromPreview,
236
240
  buildDomSelectionFromTarget,
237
- onDomEditPersisted: sdkSession
238
- ? (sel, ops) => runShadowDispatch(sdkSession, sel, ops)
241
+ forceReloadSdkSession,
242
+ onTrySdkPersist: sdkSession
243
+ ? (selection, operations, originalContent, targetPath, options) => {
244
+ // Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
245
+ runResolverShadow(sdkSession, selection.hfId, operations);
246
+ return sdkCutoverPersist(
247
+ selection,
248
+ operations,
249
+ originalContent,
250
+ targetPath,
251
+ sdkSession,
252
+ {
253
+ editHistory,
254
+ writeProjectFile,
255
+ reloadPreview,
256
+ domEditSaveTimestampRef,
257
+ compositionPath: activeCompPath,
258
+ },
259
+ options,
260
+ );
261
+ }
262
+ : undefined,
263
+ onTrySdkDelete: sdkSession
264
+ ? (hfId, originalContent, targetPath) =>
265
+ sdkDeletePersist(hfId, originalContent, targetPath, sdkSession, {
266
+ editHistory,
267
+ writeProjectFile,
268
+ reloadPreview,
269
+ domEditSaveTimestampRef,
270
+ compositionPath: activeCompPath,
271
+ })
239
272
  : undefined,
240
- onElementDeleted: sdkSession ? (sel) => runShadowDelete(sdkSession, sel.hfId) : undefined,
241
273
  });
242
274
 
243
275
  // ── Wiring: selection sync, GSAP cache, preview sync, selection handlers ──
@@ -42,15 +42,11 @@ export function useDomGeometryCommits({
42
42
  }: UseDomGeometryCommitsParams) {
43
43
  const handleDomPathOffsetCommit = useCallback(
44
44
  (selection: DomEditSelection, next: { x: number; y: number }) => {
45
- const gsapBlocked = isElementGsapTargeted(previewIframeRef.current, selection.element);
46
- console.log(
47
- "[drag:7] handleDomPathOffsetCommit (CSS path)",
48
- JSON.stringify({
49
- sel: selection.id,
50
- gsapBlocked,
51
- }),
52
- );
53
- if (gsapBlocked) {
45
+ // ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path
46
+ // elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
47
+ // onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
48
+ // Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
49
+ if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
54
50
  const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
55
51
  showToast(error.message, "error");
56
52
  return Promise.reject(error);
@@ -26,6 +26,10 @@ interface UseElementLifecycleOpsParams {
26
26
  projectIdRef: React.MutableRefObject<string | null>;
27
27
  reloadPreview: () => void;
28
28
  clearDomSelection: () => void;
29
+ /** Route delete through SDK when session resolves the hf-id; returns true if handled. */
30
+ onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
31
+ /** Resync the SDK session after a server-fallback delete. */
32
+ forceReloadSdkSession?: () => void;
29
33
  commitPositionPatchToHtml: (
30
34
  selection: DomEditSelection,
31
35
  patches: PatchOperation[],
@@ -44,6 +48,8 @@ export function useElementLifecycleOps({
44
48
  projectIdRef,
45
49
  reloadPreview,
46
50
  clearDomSelection,
51
+ onTrySdkDelete,
52
+ forceReloadSdkSession,
47
53
  commitPositionPatchToHtml,
48
54
  onElementDeleted,
49
55
  }: UseElementLifecycleOpsParams) {
@@ -74,6 +80,16 @@ export function useElementLifecycleOps({
74
80
  throw new Error("Selected element has no patchable target");
75
81
  }
76
82
 
83
+ if (onTrySdkDelete && selection.hfId) {
84
+ const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
85
+ if (handled) {
86
+ clearDomSelection();
87
+ usePlayerStore.getState().setSelectedElementId(null);
88
+ showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
89
+ return;
90
+ }
91
+ }
92
+
77
93
  domEditSaveTimestampRef.current = Date.now();
78
94
  const removeResponse = await fetch(
79
95
  `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
@@ -93,6 +109,12 @@ export function useElementLifecycleOps({
93
109
  const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
94
110
  const patchedContent =
95
111
  typeof removeData.content === "string" ? removeData.content : originalContent;
112
+ // ponytail: the server remove-element route (removeElementFromHtml) strips
113
+ // only the element node — it does NOT cascade-remove GSAP tweens targeting
114
+ // it, unlike the SDK path (removeElement → cascadeRemoveAnimations). This
115
+ // fallback runs only when the element isn't in the SDK doc (e.g. runtime-
116
+ // generated / unaddressable), where targeting tweens are unlikely. Upgrade
117
+ // path: cascade in removeElementFromHtml by selector/hf-id to fully match.
96
118
  await saveProjectFilesWithHistory({
97
119
  projectId: pid,
98
120
  label: "Delete element",
@@ -105,6 +127,9 @@ export function useElementLifecycleOps({
105
127
 
106
128
  clearDomSelection();
107
129
  usePlayerStore.getState().setSelectedElementId(null);
130
+ // Server wrote the file; resync the stale in-memory SDK doc so a later
131
+ // SDK edit doesn't resurrect the deleted element.
132
+ forceReloadSdkSession?.();
108
133
  reloadPreview();
109
134
  onElementDeleted?.(selection);
110
135
  showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
@@ -118,7 +143,9 @@ export function useElementLifecycleOps({
118
143
  clearDomSelection,
119
144
  domEditSaveTimestampRef,
120
145
  editHistory.recordEdit,
146
+ onTrySdkDelete,
121
147
  onElementDeleted,
148
+ forceReloadSdkSession,
122
149
  projectIdRef,
123
150
  reloadPreview,
124
151
  showToast,
@@ -126,6 +153,9 @@ export function useElementLifecycleOps({
126
153
  ],
127
154
  );
128
155
 
156
+ // ponytail: z-index reorder writes inline-style patches via commitPositionPatchToHtml →
157
+ // persistDomEditOperations → onTrySdkPersist, so it is already SDK-cut-over as setStyle.
158
+ // No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
129
159
  const handleDomZIndexReorderCommit = useCallback(
130
160
  (
131
161
  entries: Array<{
@@ -2,21 +2,28 @@ import { useCallback } from "react";
2
2
  import type { Composition } from "@hyperframes/sdk";
3
3
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
4
4
  import { roundTo3 } from "../utils/rounding";
5
- import { runShadowGsapTween, type ShadowGsapOp } from "../utils/sdkShadow";
5
+ import {
6
+ sdkGsapTweenPersist,
7
+ sdkGsapDeleteAllForSelectorPersist,
8
+ type CutoverDeps,
9
+ } from "../utils/sdkCutover";
6
10
  import {
7
11
  assignGsapTargetAutoIdIfNeeded,
8
12
  ensureElementAddressable,
9
13
  } from "./gsapScriptCommitHelpers";
10
14
  import type { CommitMutation, SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
11
15
 
12
- interface GsapAnimationOpsParams {
16
+ interface SdkAnimationDeps {
17
+ sdkSession?: Composition | null;
18
+ sdkDeps?: CutoverDeps | null;
19
+ }
20
+
21
+ interface GsapAnimationOpsParams extends SdkAnimationDeps {
13
22
  projectIdRef: React.MutableRefObject<string | null>;
14
23
  activeCompPath: string | null;
15
24
  commitMutation: CommitMutation;
16
25
  commitMutationSafely: SafeGsapCommitMutation;
17
26
  showToast: (message: string, tone?: "error" | "info") => void;
18
- /** Stage 7 Step 3b: SDK session for shadow GSAP dispatch (server stays authoritative). */
19
- sdkSession?: Composition | null;
20
27
  }
21
28
 
22
29
  export function useGsapAnimationOps({
@@ -26,60 +33,79 @@ export function useGsapAnimationOps({
26
33
  commitMutationSafely,
27
34
  showToast,
28
35
  sdkSession,
36
+ sdkDeps,
29
37
  }: GsapAnimationOpsParams) {
30
38
  const updateGsapMeta = useCallback(
31
- (
39
+ async (
32
40
  selection: DomEditSelection,
33
41
  animationId: string,
34
42
  updates: { duration?: number; ease?: string; position?: number },
35
43
  ) => {
36
- // Shadow op (server animationId shares the SDK id-space): existence via
37
- // runShadowGsapTween (live session) + value fidelity via the chokepoint.
38
- const shadowGsapOp: ShadowGsapOp = {
39
- kind: "set",
40
- animationId,
41
- properties: { duration: updates.duration, ease: updates.ease, position: updates.position },
42
- };
43
- // coalesceKey groups rapid meta edits into one history entry. Request
44
- // serialization is now handled per-file at the commitMutation chokepoint
45
- // (useGsapScriptCommits), so no per-op serializeKey is needed here.
46
- const metaKey = `gsap:${animationId}:meta`;
44
+ if (sdkSession && sdkDeps) {
45
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
46
+ const handled = await sdkGsapTweenPersist(
47
+ targetPath,
48
+ { kind: "set", animationId, properties: updates },
49
+ sdkSession,
50
+ sdkDeps,
51
+ { label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
52
+ );
53
+ if (handled) return;
54
+ }
47
55
  commitMutationSafely(
48
56
  selection,
49
57
  { type: "update-meta", animationId, updates },
50
- { label: "Edit GSAP animation", coalesceKey: metaKey, shadowGsapOp },
58
+ { label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
51
59
  );
52
- if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
53
60
  },
54
- [commitMutationSafely, sdkSession],
61
+ [commitMutationSafely, activeCompPath, sdkSession, sdkDeps],
55
62
  );
56
63
 
57
64
  const deleteGsapAnimation = useCallback(
58
- (selection: DomEditSelection, animationId: string) => {
59
- const shadowGsapOp: ShadowGsapOp = { kind: "remove", animationId };
65
+ async (selection: DomEditSelection, animationId: string) => {
66
+ if (sdkSession && sdkDeps) {
67
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
68
+ const handled = await sdkGsapTweenPersist(
69
+ targetPath,
70
+ { kind: "remove", animationId },
71
+ sdkSession,
72
+ sdkDeps,
73
+ { label: "Delete GSAP animation" },
74
+ );
75
+ if (handled) return;
76
+ }
60
77
  commitMutationSafely(
61
78
  selection,
62
79
  { type: "delete", animationId, stripStudioEdits: true },
63
- { label: "Delete GSAP animation", shadowGsapOp },
80
+ { label: "Delete GSAP animation" },
64
81
  );
65
- if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
66
82
  },
67
- [commitMutationSafely, sdkSession],
83
+ [commitMutationSafely, activeCompPath, sdkSession, sdkDeps],
68
84
  );
69
85
 
70
86
  const deleteAllForSelector = useCallback(
71
- (selection: DomEditSelection, targetSelector: string) => {
87
+ async (selection: DomEditSelection, targetSelector: string) => {
88
+ if (sdkSession && sdkDeps) {
89
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
90
+ const handled = await sdkGsapDeleteAllForSelectorPersist(
91
+ targetPath,
92
+ targetSelector,
93
+ sdkSession,
94
+ sdkDeps,
95
+ { label: "Delete all animations for element" },
96
+ );
97
+ if (handled) return;
98
+ }
72
99
  void commitMutation(
73
100
  selection,
74
101
  { type: "delete-all-for-selector", targetSelector },
75
102
  { label: "Delete all animations for element" },
76
103
  );
77
104
  },
78
- [commitMutation],
105
+ [commitMutation, activeCompPath, sdkSession, sdkDeps],
79
106
  );
80
107
 
81
- // Pre-existing complexity (auto-id assignment + per-method defaults); this PR
82
- // adds only a guarded shadow-op construction at the tail.
108
+ // fallow-ignore-next-line complexity
83
109
  const addGsapAnimation = useCallback(
84
110
  // fallow-ignore-next-line complexity
85
111
  async (
@@ -114,25 +140,28 @@ export function useGsapAnimationOps({
114
140
  fromTo: { x: 0, y: 0, opacity: 1 },
115
141
  };
116
142
 
117
- // Shadow op (server stays authoritative). "set" has no SDK method, so it
118
- // is not shadowed; otherwise: existence via runShadowGsapTween (live) +
119
- // value fidelity via the chokepoint (shadowGsapOp in options).
120
- const shadowGsapOp: ShadowGsapOp | undefined =
121
- selection.hfId && method !== "set"
122
- ? {
123
- kind: "add",
124
- target: selection.hfId,
125
- tween: {
126
- method,
127
- position,
128
- duration,
129
- ease: "power2.out",
130
- ...(method === "fromTo"
131
- ? { fromProperties: { opacity: 0 }, toProperties: toDefaults[method] }
132
- : { properties: toDefaults[method] ?? { opacity: 1 } }),
133
- },
134
- }
135
- : undefined;
143
+ // Skip SDK path when an id was just assigned server-side (autoId): the
144
+ // SDK session hasn't reloaded that write yet, so persisting its
145
+ // serialization would clobber the new id let the server add the tween
146
+ // atomically with the id it wrote.
147
+ if (!autoId && selection.hfId && sdkSession && sdkDeps) {
148
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
149
+ const spec = {
150
+ method,
151
+ position,
152
+ ...(method !== "set" ? { duration, ease: "power2.out" as const } : {}),
153
+ properties: toDefaults[method] ?? { opacity: 1 },
154
+ ...(method === "fromTo" ? { fromProperties: { opacity: 0 } } : {}),
155
+ };
156
+ const handled = await sdkGsapTweenPersist(
157
+ targetPath,
158
+ { kind: "add", target: selection.hfId, spec },
159
+ sdkSession,
160
+ sdkDeps,
161
+ { label: `Add GSAP ${method} animation` },
162
+ );
163
+ if (handled) return;
164
+ }
136
165
 
137
166
  await commitMutation(
138
167
  selection,
@@ -146,12 +175,10 @@ export function useGsapAnimationOps({
146
175
  properties: toDefaults[method] ?? { opacity: 1 },
147
176
  fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
148
177
  },
149
- { label: `Add GSAP ${method} animation`, shadowGsapOp },
178
+ { label: `Add GSAP ${method} animation` },
150
179
  );
151
-
152
- if (sdkSession && shadowGsapOp) runShadowGsapTween(sdkSession, shadowGsapOp);
153
180
  },
154
- [activeCompPath, commitMutation, projectIdRef, showToast, sdkSession],
181
+ [activeCompPath, commitMutation, projectIdRef, showToast, sdkSession, sdkDeps],
155
182
  );
156
183
 
157
184
  return {