@hyperframes/studio 0.6.97 → 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-HveJ0MuV.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-Daj5djxa.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-Cfye9xzo.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
@@ -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
- const origError = win.console.error.bind(win.console);
48
+ patchedWin = win;
49
+ origConsoleError = win.console.error.bind(win.console);
27
50
  win.console.error = function (...args: unknown[]) {
28
- origError(...args);
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
- win.addEventListener("error", (e: ErrorEvent) => {
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 () => previewIframe.removeEventListener("load", handleLoad);
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
- import type { PatchOperation } from "../utils/sourcePatcher";
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
- // ── Position commits ──
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
- // fallow-ignore-next-line complexity
431
- const handleDomEditElementDelete = useCallback(
432
- // fallow-ignore-next-line complexity
433
- async (selection: DomEditSelection) => {
434
- const pid = projectIdRef.current;
435
- if (!pid) return;
436
- const label = selection.label || selection.id || selection.selector || selection.tagName;
437
-
438
- const targetPath = selection.sourceFile || activeCompPath || "index.html";
439
- try {
440
- const response = await fetch(
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
- domEditSaveTimestampRef.current = Date.now();
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
- const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
474
- const patchedContent =
475
- typeof removeData.content === "string" ? removeData.content : originalContent;
476
- await saveProjectFilesWithHistory({
477
- projectId: pid,
478
- label: "Delete element",
479
- kind: "timeline",
480
- files: { [targetPath]: patchedContent },
481
- readFile: async () => originalContent,
482
- writeFile: writeProjectFile,
483
- recordEdit: editHistory.recordEdit,
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 "./useDomEditCommits";
6
+ import type { PersistDomEditOperations } from "./domEditCommitTypes";
7
7
 
8
8
  interface UseDomEditPositionPatchCommitParams {
9
9
  activeCompPath: string | null;