@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.
- package/dist/assets/hyperframes-player-BRjQ5a5_.js +418 -0
- package/dist/assets/index-B7S86vK1.js +370 -0
- package/dist/assets/index-DP8pPIk2.css +1 -0
- package/dist/assets/{index-SlMJloLR.js → index-_IV-vm9l.js} +1 -1
- package/dist/assets/{index-CFCe0Xvn.js → index-x0c2-zQN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/App.tsx +133 -141
- package/src/components/StudioHeader.tsx +44 -0
- package/src/components/StudioOverlays.tsx +70 -0
- package/src/components/editor/SourceEditor.tsx +19 -16
- package/src/components/editor/gsapAnimationConstants.ts +24 -0
- package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
- package/src/components/editor/manualEditingAvailability.ts +25 -7
- package/src/components/storyboard/FramePoster.tsx +56 -0
- package/src/components/storyboard/StoryboardDirection.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
- package/src/components/storyboard/StoryboardGrid.tsx +33 -0
- package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
- package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
- package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
- package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
- package/src/components/storyboard/StoryboardView.tsx +78 -0
- package/src/components/storyboard/frameStatus.ts +36 -0
- package/src/contexts/ViewModeContext.tsx +98 -0
- package/src/hooks/gsapScriptCommitTypes.ts +4 -7
- package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
- package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -0
- package/src/hooks/useAppHotkeys.ts +20 -0
- package/src/hooks/useDomEditCommits.ts +43 -14
- package/src/hooks/useDomEditSession.test.ts +41 -0
- package/src/hooks/useDomEditSession.ts +38 -6
- package/src/hooks/useDomGeometryCommits.ts +5 -9
- package/src/hooks/useElementLifecycleOps.ts +30 -0
- package/src/hooks/useGsapAnimationOps.ts +78 -51
- package/src/hooks/useGsapKeyframeOps.ts +127 -43
- package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
- package/src/hooks/useGsapPropertyDebounce.ts +201 -23
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
- package/src/hooks/useGsapScriptCommits.ts +95 -40
- package/src/hooks/useGsapTweenCache.ts +0 -27
- package/src/hooks/useRazorSplit.ts +4 -10
- package/src/hooks/useSdkSession.test.ts +12 -0
- package/src/hooks/useSdkSession.ts +91 -25
- package/src/hooks/useStoryboard.ts +80 -0
- package/src/hooks/useTimelineEditing.ts +163 -68
- package/src/utils/gsapSoftReload.ts +14 -0
- package/src/utils/sdkCutover.gate.test.ts +61 -0
- package/src/utils/sdkCutover.test.ts +839 -0
- package/src/utils/sdkCutover.ts +465 -0
- package/src/utils/sdkOpMapping.ts +43 -0
- package/src/utils/sdkResolverShadow.test.ts +366 -0
- package/src/utils/sdkResolverShadow.ts +313 -0
- package/src/utils/timelineElementSplit.test.ts +61 -1
- package/src/utils/timelineElementSplit.ts +18 -0
- package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
- package/dist/assets/index-BVqybwMG.css +0 -1
- package/dist/assets/index-ho_f4axK.js +0 -296
- package/src/utils/sdkShadow.test.ts +0 -606
- package/src/utils/sdkShadow.ts +0 -517
- package/src/utils/sdkShadowGsapFidelity.ts +0 -296
- package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
- package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
- 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
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
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
|
-
|
|
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"
|
|
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
|
-
//
|
|
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
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
:
|
|
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
|
|
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 {
|