@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.
- package/dist/assets/hyperframes-player-DgsMQSvV.js +418 -0
- package/dist/assets/index-B62bDCQv.css +1 -0
- package/dist/assets/index-Ce3pBm_I.js +252 -0
- package/dist/assets/{index-BWFaypdT.js → index-D-ET9M0b.js} +1 -1
- package/dist/assets/index-D-bS9Dxx.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +7 -5
- package/src/App.tsx +182 -177
- package/src/captions/store.ts +11 -11
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -2
- package/src/components/StudioPreviewArea.tsx +225 -183
- package/src/components/StudioRightPanel.tsx +3 -3
- package/src/components/TimelineToolbar.tsx +25 -0
- package/src/components/editor/DomEditOverlay.tsx +2 -5
- package/src/components/editor/EaseCurveSection.tsx +2 -3
- package/src/components/editor/GestureTrailOverlay.tsx +4 -3
- package/src/components/editor/LayersPanel.tsx +3 -9
- package/src/components/editor/PropertyPanel.tsx +20 -61
- package/src/components/editor/colorValue.ts +3 -1
- package/src/components/editor/domEditOverlayGestures.ts +54 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/gradientValue.ts +3 -3
- package/src/components/editor/keyframeMove.test.ts +101 -0
- package/src/components/editor/keyframeMove.ts +151 -0
- package/src/components/editor/manualEditsDom.ts +0 -12
- package/src/components/editor/propertyPanelHelpers.ts +10 -38
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
- package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
- package/src/components/editor/studioMotionOps.test.ts +1 -1
- package/src/components/editor/studioMotionOps.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
- package/src/components/nle/NLELayout.tsx +1 -24
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +134 -31
- package/src/contexts/StudioContext.tsx +90 -40
- package/src/contexts/TimelineEditContext.tsx +47 -0
- package/src/hooks/domEditCommitTypes.ts +14 -0
- package/src/hooks/gsapDragCommit.ts +9 -24
- package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
- package/src/hooks/gsapKeyframeCommit.ts +5 -15
- package/src/hooks/gsapRuntimeBridge.ts +18 -52
- package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
- package/src/hooks/gsapRuntimeReaders.ts +19 -26
- package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
- package/src/hooks/gsapScriptCommitTypes.ts +58 -0
- package/src/hooks/gsapShared.ts +157 -0
- package/src/hooks/timelineEditingHelpers.ts +63 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
- package/src/hooks/useAppHotkeys.ts +299 -377
- package/src/hooks/useConsoleErrorCapture.ts +33 -5
- package/src/hooks/useDomEditCommits.ts +35 -293
- package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
- package/src/hooks/useDomEditSession.ts +78 -249
- package/src/hooks/useDomEditTextCommits.ts +1 -1
- package/src/hooks/useDomEditWiring.ts +255 -0
- package/src/hooks/useDomGeometryCommits.ts +181 -0
- package/src/hooks/useDomSelection.ts +10 -27
- package/src/hooks/useEditorSave.ts +82 -0
- package/src/hooks/useElementLifecycleOps.ts +177 -0
- package/src/hooks/useEnableKeyframes.ts +10 -15
- package/src/hooks/useFileManager.ts +32 -114
- package/src/hooks/useFileTree.ts +80 -0
- package/src/hooks/useGestureCommit.ts +7 -5
- package/src/hooks/useGestureRecording.ts +1 -1
- package/src/hooks/useGsapAnimationOps.ts +122 -0
- package/src/hooks/useGsapArcPathOps.ts +61 -0
- package/src/hooks/useGsapAwareEditing.ts +242 -0
- package/src/hooks/useGsapKeyframeOps.ts +167 -0
- package/src/hooks/useGsapPropertyDebounce.ts +135 -0
- package/src/hooks/useGsapScriptCommits.ts +58 -570
- package/src/hooks/useGsapSelectionHandlers.ts +22 -9
- package/src/hooks/useGsapTweenCache.ts +35 -29
- package/src/hooks/useLintModal.ts +7 -0
- package/src/hooks/useMusicBeatAnalysis.ts +152 -0
- package/src/hooks/useRazorSplit.ts +1 -1
- package/src/hooks/useRenderClipContent.ts +46 -21
- package/src/hooks/useTimelineEditing.ts +48 -4
- package/src/player/components/AudioWaveform.tsx +29 -4
- package/src/player/components/BeatStrip.tsx +166 -0
- package/src/player/components/Timeline.tsx +39 -18
- package/src/player/components/TimelineCanvas.tsx +52 -12
- package/src/player/components/TimelineClipDiamonds.tsx +130 -20
- package/src/player/components/TimelinePropertyRows.tsx +8 -2
- package/src/player/components/TimelineRuler.tsx +36 -2
- package/src/player/components/timelineEditing.ts +30 -5
- package/src/player/components/useTimelineClipDrag.ts +155 -4
- package/src/player/components/useTimelinePlayhead.ts +30 -1
- package/src/player/hooks/useTimelinePlayer.ts +47 -45
- package/src/player/lib/mediaProbe.ts +46 -3
- package/src/player/lib/playbackScrub.ts +16 -0
- package/src/player/lib/timelineDOM.ts +10 -2
- package/src/player/lib/timelineIframeHelpers.ts +89 -0
- package/src/player/store/playerStore.ts +92 -33
- package/src/utils/beatEditActions.ts +109 -0
- package/src/utils/beatEditing.ts +136 -0
- package/src/utils/clipboardPayload.ts +3 -2
- package/src/utils/compositionPatterns.ts +2 -0
- package/src/utils/keyframeSelection.test.ts +45 -0
- package/src/utils/keyframeSelection.ts +29 -0
- package/src/utils/rounding.ts +9 -0
- package/src/utils/studioHelpers.ts +5 -2
- package/src/utils/studioUrlState.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +6 -5
- package/src/utils/timelineInspector.ts +15 -100
- package/dist/assets/hyperframes-player-0esDKGRk.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-BA979yF1.js +0 -251
- package/src/components/editor/DopesheetStrip.tsx +0 -141
- package/src/components/editor/StaggerControls.tsx +0 -61
- package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
- package/src/components/editor/TimelineLayerPanel.tsx +0 -15
- package/src/components/nle/TimelineEditorNotice.tsx +0 -133
- package/src/hooks/gsapRuntimePreview.ts +0 -19
- package/src/player/components/timelineUtils.ts +0 -211
- package/src/utils/audioBeatDetection.ts +0 -58
- package/src/utils/keyframeSnapping.test.ts +0 -74
- package/src/utils/keyframeSnapping.ts +0 -63
- package/src/utils/timelineInspector.test.ts +0 -79
|
@@ -17,15 +17,38 @@ export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null)
|
|
|
17
17
|
// eslint-disable-next-line no-restricted-syntax
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (!previewIframe) return;
|
|
20
|
+
|
|
21
|
+
let patchedWin: (Window & typeof globalThis) | null = null;
|
|
22
|
+
let origConsoleError: ((...args: unknown[]) => void) | null = null;
|
|
23
|
+
let errorHandler: ((e: ErrorEvent) => void) | null = null;
|
|
24
|
+
|
|
25
|
+
const detachErrorCapture = () => {
|
|
26
|
+
const win = patchedWin;
|
|
27
|
+
if (!win) return;
|
|
28
|
+
patchedWin = null;
|
|
29
|
+
try {
|
|
30
|
+
// origConsoleError and errorHandler are always set alongside patchedWin
|
|
31
|
+
win.console.error = origConsoleError!;
|
|
32
|
+
win.removeEventListener("error", errorHandler!);
|
|
33
|
+
delete (win as unknown as Record<string, unknown>).__hfErrorCapture;
|
|
34
|
+
} catch {
|
|
35
|
+
/* cross-origin or destroyed window */
|
|
36
|
+
}
|
|
37
|
+
origConsoleError = null;
|
|
38
|
+
errorHandler = null;
|
|
39
|
+
};
|
|
40
|
+
|
|
20
41
|
const attachErrorCapture = () => {
|
|
42
|
+
detachErrorCapture();
|
|
21
43
|
try {
|
|
22
44
|
const win = previewIframe.contentWindow as (Window & typeof globalThis) | null;
|
|
23
45
|
if (!win) return;
|
|
24
46
|
if ((win as unknown as Record<string, unknown>).__hfErrorCapture) return;
|
|
25
47
|
(win as unknown as Record<string, unknown>).__hfErrorCapture = true;
|
|
26
|
-
|
|
48
|
+
patchedWin = win;
|
|
49
|
+
origConsoleError = win.console.error.bind(win.console);
|
|
27
50
|
win.console.error = function (...args: unknown[]) {
|
|
28
|
-
|
|
51
|
+
origConsoleError!(...args);
|
|
29
52
|
const text = args.map((a) => (a instanceof Error ? a.message : String(a))).join(" ");
|
|
30
53
|
if (text.includes("favicon")) return;
|
|
31
54
|
consoleErrorsRef.current = [
|
|
@@ -34,18 +57,20 @@ export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null)
|
|
|
34
57
|
];
|
|
35
58
|
setConsoleErrors([...consoleErrorsRef.current]);
|
|
36
59
|
};
|
|
37
|
-
|
|
60
|
+
errorHandler = (e: ErrorEvent) => {
|
|
38
61
|
const text = e.message || String(e);
|
|
39
62
|
consoleErrorsRef.current = [
|
|
40
63
|
...consoleErrorsRef.current,
|
|
41
64
|
{ severity: "error", message: text },
|
|
42
65
|
];
|
|
43
66
|
setConsoleErrors([...consoleErrorsRef.current]);
|
|
44
|
-
}
|
|
67
|
+
};
|
|
68
|
+
win.addEventListener("error", errorHandler);
|
|
45
69
|
} catch {
|
|
46
70
|
/* same-origin only */
|
|
47
71
|
}
|
|
48
72
|
};
|
|
73
|
+
|
|
49
74
|
attachErrorCapture();
|
|
50
75
|
const handleLoad = () => {
|
|
51
76
|
consoleErrorsRef.current = [];
|
|
@@ -53,7 +78,10 @@ export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null)
|
|
|
53
78
|
attachErrorCapture();
|
|
54
79
|
};
|
|
55
80
|
previewIframe.addEventListener("load", handleLoad);
|
|
56
|
-
return () =>
|
|
81
|
+
return () => {
|
|
82
|
+
previewIframe.removeEventListener("load", handleLoad);
|
|
83
|
+
detachErrorCapture();
|
|
84
|
+
};
|
|
57
85
|
}, [previewIframe]);
|
|
58
86
|
|
|
59
87
|
return { consoleErrors, setConsoleErrors, resetErrors };
|
|
@@ -1,43 +1,23 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
3
|
-
import { usePlayerStore } from "../player";
|
|
4
|
-
import { STUDIO_GSAP_DRAG_INTERCEPT_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
5
3
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
6
|
-
|
|
4
|
+
|
|
7
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
-
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
9
6
|
import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
|
|
10
7
|
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
|
11
|
-
import {
|
|
12
|
-
buildDomEditPatchTarget,
|
|
13
|
-
getDomEditTargetKey,
|
|
14
|
-
readHfId,
|
|
15
|
-
type DomEditSelection,
|
|
16
|
-
} from "../components/editor/domEditing";
|
|
17
|
-
import {
|
|
18
|
-
applyStudioPathOffset,
|
|
19
|
-
applyStudioBoxSize,
|
|
20
|
-
applyStudioRotation,
|
|
21
|
-
clearStudioPathOffset,
|
|
22
|
-
clearStudioBoxSize,
|
|
23
|
-
clearStudioRotation,
|
|
24
|
-
} from "../components/editor/manualEdits";
|
|
25
|
-
import {
|
|
26
|
-
buildPathOffsetPatches,
|
|
27
|
-
buildBoxSizePatches,
|
|
28
|
-
buildRotationPatches,
|
|
29
|
-
buildClearPathOffsetPatches,
|
|
30
|
-
buildClearBoxSizePatches,
|
|
31
|
-
buildClearRotationPatches,
|
|
32
|
-
} from "../components/editor/manualEditsDom";
|
|
8
|
+
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
|
|
33
9
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
34
|
-
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
35
10
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
11
|
+
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
36
12
|
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
|
37
13
|
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
|
14
|
+
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
15
|
+
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
|
16
|
+
|
|
17
|
+
// Re-export so existing consumers keep their import path
|
|
18
|
+
export { GSAP_CSS_FALLBACK_BLOCKED_MESSAGE } from "./useDomGeometryCommits";
|
|
38
19
|
|
|
39
20
|
// ── Helpers ──
|
|
40
|
-
type TimelineLike = { getChildren?: (nested: boolean) => Array<{ targets?: () => Element[] }> };
|
|
41
21
|
|
|
42
22
|
function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
43
23
|
return fields.map((field) => field.path).join(", ");
|
|
@@ -60,40 +40,6 @@ function formatPatchRejectionMessage(body: { error?: string; fields?: string[] }
|
|
|
60
40
|
return `Couldn't save edit: ${body.error}${suffix}`;
|
|
61
41
|
}
|
|
62
42
|
|
|
63
|
-
export const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
|
64
|
-
"This element is GSAP-animated — dragging via CSS would corrupt keyframes";
|
|
65
|
-
|
|
66
|
-
// fallow-ignore-next-line complexity
|
|
67
|
-
function isElementGsapTargeted(iframe: HTMLIFrameElement | null, element: HTMLElement): boolean {
|
|
68
|
-
// When the GSAP drag intercept is disabled for debugging, treat every
|
|
69
|
-
// element as un-targeted so commits take the plain CSS persist path.
|
|
70
|
-
if (!STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) return false;
|
|
71
|
-
if (!iframe?.contentWindow) return false;
|
|
72
|
-
let timelines: Record<string, TimelineLike> | undefined;
|
|
73
|
-
try {
|
|
74
|
-
timelines = (iframe.contentWindow as Window & { __timelines?: Record<string, TimelineLike> })
|
|
75
|
-
.__timelines;
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
if (!timelines) return false;
|
|
80
|
-
const id = element.id;
|
|
81
|
-
for (const tl of Object.values(timelines)) {
|
|
82
|
-
if (!tl?.getChildren) continue;
|
|
83
|
-
try {
|
|
84
|
-
for (const child of tl.getChildren(true)) {
|
|
85
|
-
if (!child.targets) continue;
|
|
86
|
-
for (const t of child.targets()) {
|
|
87
|
-
if (t === element || (id && t.id === id)) return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
43
|
// ── Types ──
|
|
98
44
|
|
|
99
45
|
interface RecordEditInput {
|
|
@@ -103,17 +49,7 @@ interface RecordEditInput {
|
|
|
103
49
|
files: Record<string, { before: string; after: string }>;
|
|
104
50
|
}
|
|
105
51
|
|
|
106
|
-
export type PersistDomEditOperations
|
|
107
|
-
selection: DomEditSelection,
|
|
108
|
-
operations: PatchOperation[],
|
|
109
|
-
options?: {
|
|
110
|
-
label?: string;
|
|
111
|
-
coalesceKey?: string;
|
|
112
|
-
skipRefresh?: boolean;
|
|
113
|
-
prepareContent?: (html: string, sourceFile: string) => string;
|
|
114
|
-
shouldSave?: () => boolean;
|
|
115
|
-
},
|
|
116
|
-
) => Promise<void>;
|
|
52
|
+
export type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
117
53
|
|
|
118
54
|
export interface UseDomEditCommitsParams {
|
|
119
55
|
activeCompPath: string | null;
|
|
@@ -322,6 +258,8 @@ export function useDomEditCommits({
|
|
|
322
258
|
resolveImportedFontAsset,
|
|
323
259
|
});
|
|
324
260
|
|
|
261
|
+
// ── Position patch helper (shared by geometry + lifecycle hooks) ──
|
|
262
|
+
|
|
325
263
|
const commitPositionPatchToHtml = useDomEditPositionPatchCommit({
|
|
326
264
|
activeCompPath,
|
|
327
265
|
persistDomEditOperations,
|
|
@@ -329,229 +267,33 @@ export function useDomEditCommits({
|
|
|
329
267
|
showToast,
|
|
330
268
|
});
|
|
331
269
|
|
|
332
|
-
// ──
|
|
333
|
-
|
|
334
|
-
const handleDomPathOffsetCommit = useCallback(
|
|
335
|
-
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
336
|
-
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
337
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
338
|
-
showToast(error.message, "error");
|
|
339
|
-
return Promise.reject(error);
|
|
340
|
-
}
|
|
341
|
-
applyStudioPathOffset(selection.element, next);
|
|
342
|
-
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
343
|
-
label: "Move layer",
|
|
344
|
-
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
|
345
|
-
});
|
|
346
|
-
},
|
|
347
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
const handleDomGroupPathOffsetCommit = useCallback(
|
|
351
|
-
(updates: DomEditGroupPathOffsetCommit[]) => {
|
|
352
|
-
if (updates.length === 0) return Promise.resolve();
|
|
353
|
-
const blockedUpdate = updates.find(({ selection }) =>
|
|
354
|
-
isElementGsapTargeted(previewIframeRef.current, selection.element),
|
|
355
|
-
);
|
|
356
|
-
if (blockedUpdate) {
|
|
357
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
358
|
-
showToast(error.message, "error");
|
|
359
|
-
return Promise.reject(error);
|
|
360
|
-
}
|
|
361
|
-
const coalesceKey = updates
|
|
362
|
-
.map((u) => getDomEditTargetKey(u.selection))
|
|
363
|
-
.sort()
|
|
364
|
-
.join(":");
|
|
365
|
-
const saves = updates.map(({ selection, next }) => {
|
|
366
|
-
applyStudioPathOffset(selection.element, next);
|
|
367
|
-
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
368
|
-
label: `Move ${updates.length} layers`,
|
|
369
|
-
coalesceKey: `group-path-offset:${coalesceKey}`,
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
return Promise.all(saves).then(() => undefined);
|
|
373
|
-
},
|
|
374
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const handleDomBoxSizeCommit = useCallback(
|
|
378
|
-
(selection: DomEditSelection, next: { width: number; height: number }) => {
|
|
379
|
-
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
380
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
381
|
-
showToast(error.message, "error");
|
|
382
|
-
return Promise.reject(error);
|
|
383
|
-
}
|
|
384
|
-
applyStudioBoxSize(selection.element, next);
|
|
385
|
-
return commitPositionPatchToHtml(selection, buildBoxSizePatches(selection.element), {
|
|
386
|
-
label: "Resize layer box",
|
|
387
|
-
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
388
|
-
});
|
|
389
|
-
},
|
|
390
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
const handleDomRotationCommit = useCallback(
|
|
394
|
-
(selection: DomEditSelection, next: { angle: number }) => {
|
|
395
|
-
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
396
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
397
|
-
showToast(error.message, "error");
|
|
398
|
-
return Promise.reject(error);
|
|
399
|
-
}
|
|
400
|
-
applyStudioRotation(selection.element, next);
|
|
401
|
-
return commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
|
|
402
|
-
label: "Rotate layer",
|
|
403
|
-
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
|
404
|
-
});
|
|
405
|
-
},
|
|
406
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
const handleDomManualEditsReset = useCallback(
|
|
410
|
-
(selection: DomEditSelection) => {
|
|
411
|
-
const element = selection.element;
|
|
412
|
-
const clearPatches = [
|
|
413
|
-
...buildClearPathOffsetPatches(element),
|
|
414
|
-
...buildClearBoxSizePatches(element),
|
|
415
|
-
...buildClearRotationPatches(element),
|
|
416
|
-
];
|
|
417
|
-
clearStudioPathOffset(element);
|
|
418
|
-
clearStudioBoxSize(element);
|
|
419
|
-
clearStudioRotation(element);
|
|
420
|
-
// skipRefresh:false triggers reloadPreview() which re-syncs selection on load
|
|
421
|
-
void commitPositionPatchToHtml(selection, clearPatches, {
|
|
422
|
-
label: "Reset layer edits",
|
|
423
|
-
coalesceKey: `manual-reset:${getDomEditTargetKey(selection)}`,
|
|
424
|
-
skipRefresh: false,
|
|
425
|
-
}).catch(() => undefined);
|
|
426
|
-
},
|
|
427
|
-
[commitPositionPatchToHtml],
|
|
428
|
-
);
|
|
270
|
+
// ── Geometry commits (path offset, box size, rotation) ──
|
|
429
271
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
442
|
-
);
|
|
443
|
-
if (!response.ok) {
|
|
444
|
-
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const data = (await response.json()) as { content?: string };
|
|
448
|
-
const originalContent = data.content;
|
|
449
|
-
if (typeof originalContent !== "string")
|
|
450
|
-
throw new Error(`Missing file contents for ${targetPath}`);
|
|
451
|
-
|
|
452
|
-
const patchTarget = buildDomEditPatchTarget(selection);
|
|
453
|
-
if (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId) {
|
|
454
|
-
throw new Error("Selected element has no patchable target");
|
|
455
|
-
}
|
|
272
|
+
const {
|
|
273
|
+
handleDomPathOffsetCommit,
|
|
274
|
+
handleDomGroupPathOffsetCommit,
|
|
275
|
+
handleDomBoxSizeCommit,
|
|
276
|
+
handleDomRotationCommit,
|
|
277
|
+
handleDomManualEditsReset,
|
|
278
|
+
} = useDomGeometryCommits({
|
|
279
|
+
previewIframeRef,
|
|
280
|
+
showToast,
|
|
281
|
+
commitPositionPatchToHtml,
|
|
282
|
+
});
|
|
456
283
|
|
|
457
|
-
|
|
458
|
-
const removeResponse = await fetch(
|
|
459
|
-
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
|
|
460
|
-
{
|
|
461
|
-
method: "POST",
|
|
462
|
-
headers: { "Content-Type": "application/json" },
|
|
463
|
-
body: JSON.stringify({ target: patchTarget }),
|
|
464
|
-
},
|
|
465
|
-
);
|
|
466
|
-
if (!removeResponse.ok) {
|
|
467
|
-
throw await createStudioSaveHttpError(
|
|
468
|
-
removeResponse,
|
|
469
|
-
`Failed to delete element from ${targetPath}`,
|
|
470
|
-
);
|
|
471
|
-
}
|
|
284
|
+
// ── Element lifecycle (delete, z-index reorder) ──
|
|
472
285
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
clearDomSelection();
|
|
487
|
-
usePlayerStore.getState().setSelectedElementId(null);
|
|
488
|
-
reloadPreview();
|
|
489
|
-
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
|
490
|
-
} catch (error) {
|
|
491
|
-
const message = error instanceof Error ? error.message : "Failed to delete element";
|
|
492
|
-
showToast(message);
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
[
|
|
496
|
-
activeCompPath,
|
|
497
|
-
clearDomSelection,
|
|
498
|
-
domEditSaveTimestampRef,
|
|
499
|
-
editHistory.recordEdit,
|
|
500
|
-
projectIdRef,
|
|
501
|
-
reloadPreview,
|
|
502
|
-
showToast,
|
|
503
|
-
writeProjectFile,
|
|
504
|
-
],
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
const handleDomZIndexReorderCommit = useCallback(
|
|
508
|
-
(
|
|
509
|
-
entries: Array<{
|
|
510
|
-
element: HTMLElement;
|
|
511
|
-
zIndex: number;
|
|
512
|
-
id?: string;
|
|
513
|
-
selector?: string;
|
|
514
|
-
selectorIndex?: number;
|
|
515
|
-
sourceFile: string;
|
|
516
|
-
}>,
|
|
517
|
-
) => {
|
|
518
|
-
if (entries.length === 0) return;
|
|
519
|
-
const coalesceKey = `z-reorder:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
|
|
520
|
-
for (let i = 0; i < entries.length; i++) {
|
|
521
|
-
const entry = entries[i];
|
|
522
|
-
entry.element.style.zIndex = String(entry.zIndex);
|
|
523
|
-
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
|
|
524
|
-
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
|
|
525
|
-
];
|
|
526
|
-
try {
|
|
527
|
-
const win = entry.element.ownerDocument?.defaultView;
|
|
528
|
-
if (win && win.getComputedStyle(entry.element).position === "static") {
|
|
529
|
-
entry.element.style.position = "relative";
|
|
530
|
-
patches.push({ type: "inline-style", property: "position", value: "relative" });
|
|
531
|
-
}
|
|
532
|
-
} catch {
|
|
533
|
-
/* cross-origin or detached — skip */
|
|
534
|
-
}
|
|
535
|
-
void commitPositionPatchToHtml(
|
|
536
|
-
{
|
|
537
|
-
element: entry.element,
|
|
538
|
-
id: entry.id ?? null,
|
|
539
|
-
hfId: readHfId(entry.element),
|
|
540
|
-
selector: entry.selector,
|
|
541
|
-
selectorIndex: entry.selectorIndex,
|
|
542
|
-
sourceFile: entry.sourceFile,
|
|
543
|
-
} as unknown as DomEditSelection,
|
|
544
|
-
patches,
|
|
545
|
-
{
|
|
546
|
-
label: "Reorder layers",
|
|
547
|
-
coalesceKey,
|
|
548
|
-
skipRefresh: i < entries.length - 1,
|
|
549
|
-
},
|
|
550
|
-
).catch(() => undefined);
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
|
-
[commitPositionPatchToHtml],
|
|
554
|
-
);
|
|
286
|
+
const { handleDomEditElementDelete, handleDomZIndexReorderCommit } = useElementLifecycleOps({
|
|
287
|
+
activeCompPath,
|
|
288
|
+
showToast,
|
|
289
|
+
writeProjectFile,
|
|
290
|
+
domEditSaveTimestampRef,
|
|
291
|
+
editHistory,
|
|
292
|
+
projectIdRef,
|
|
293
|
+
reloadPreview,
|
|
294
|
+
clearDomSelection,
|
|
295
|
+
commitPositionPatchToHtml,
|
|
296
|
+
});
|
|
555
297
|
|
|
556
298
|
return {
|
|
557
299
|
resolveImportedFontAsset,
|
|
@@ -3,7 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing";
|
|
|
3
3
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
4
4
|
import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
5
5
|
import { DomEditSaveQueueOpenError } from "../utils/domEditSaveQueue";
|
|
6
|
-
import type { PersistDomEditOperations } from "./
|
|
6
|
+
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
7
7
|
|
|
8
8
|
interface UseDomEditPositionPatchCommitParams {
|
|
9
9
|
activeCompPath: string | null;
|