@hyperframes/studio 0.6.0-alpha.8 → 0.6.0

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-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-DUqUmaoH.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +428 -4299
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.test.ts +0 -49
  16. package/src/components/editor/PropertyPanel.tsx +132 -2763
  17. package/src/components/editor/domEditing.ts +38 -5
  18. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  19. package/src/components/editor/manualEditingAvailability.ts +1 -1
  20. package/src/components/editor/manualEdits.ts +32 -0
  21. package/src/components/editor/propertyPanelColor.tsx +371 -0
  22. package/src/components/editor/propertyPanelFill.tsx +421 -0
  23. package/src/components/editor/propertyPanelFont.tsx +455 -0
  24. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  25. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  26. package/src/components/editor/propertyPanelSections.tsx +453 -0
  27. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  28. package/src/components/nle/NLELayout.tsx +8 -11
  29. package/src/components/nle/NLEPreview.tsx +3 -0
  30. package/src/components/renders/RenderQueue.tsx +102 -31
  31. package/src/components/renders/useRenderQueue.ts +8 -2
  32. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  33. package/src/contexts/DomEditContext.tsx +137 -0
  34. package/src/contexts/FileManagerContext.tsx +110 -0
  35. package/src/contexts/PanelLayoutContext.tsx +68 -0
  36. package/src/contexts/StudioContext.tsx +135 -0
  37. package/src/hooks/useAppHotkeys.ts +326 -0
  38. package/src/hooks/useAskAgentModal.ts +162 -0
  39. package/src/hooks/useCaptionDetection.ts +132 -0
  40. package/src/hooks/useCompositionDimensions.ts +25 -0
  41. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  42. package/src/hooks/useDomEditCommits.ts +437 -0
  43. package/src/hooks/useDomEditSession.ts +342 -0
  44. package/src/hooks/useDomEditTextCommits.ts +330 -0
  45. package/src/hooks/useDomSelection.ts +398 -0
  46. package/src/hooks/useFileManager.ts +431 -0
  47. package/src/hooks/useFrameCapture.ts +77 -0
  48. package/src/hooks/useLintModal.ts +35 -0
  49. package/src/hooks/useManifestPersistence.ts +492 -0
  50. package/src/hooks/usePanelLayout.ts +68 -0
  51. package/src/hooks/usePreviewInteraction.ts +153 -0
  52. package/src/hooks/useRenderClipContent.ts +124 -0
  53. package/src/hooks/useTimelineEditing.ts +472 -0
  54. package/src/player/components/Player.tsx +35 -4
  55. package/src/player/components/Timeline.test.ts +0 -8
  56. package/src/player/components/Timeline.tsx +10 -103
  57. package/src/player/components/TimelineClip.tsx +9 -244
  58. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  59. package/src/utils/domEditHelpers.ts +50 -0
  60. package/src/utils/studioFontHelpers.ts +83 -0
  61. package/src/utils/studioHelpers.ts +214 -0
  62. package/src/utils/studioPreviewHelpers.ts +185 -0
  63. package/src/utils/timelineDiscovery.ts +1 -1
  64. package/dist/assets/index-14zH9lqh.css +0 -1
  65. package/dist/assets/index-ClYcrksa.js +0 -108
  66. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,342 @@
1
+ import { useEffect } from "react";
2
+ import type { TimelineElement } from "../player";
3
+ import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
4
+ import { findElementForSelection } from "../components/editor/domEditing";
5
+ import type { StudioManualEditManifest } from "../components/editor/manualEdits";
6
+ import type { StudioMotionManifest } from "../components/editor/studioMotion";
7
+ import type { ImportedFontAsset } from "../components/editor/fontAssets";
8
+ import type { EditHistoryKind } from "../utils/editHistory";
9
+ import type { RightPanelTab } from "../utils/studioHelpers";
10
+ import { useAskAgentModal } from "./useAskAgentModal";
11
+ import { useDomSelection } from "./useDomSelection";
12
+ import { usePreviewInteraction } from "./usePreviewInteraction";
13
+ import { useDomEditCommits } from "./useDomEditCommits";
14
+
15
+ // ── Types ──
16
+
17
+ interface RecordEditInput {
18
+ label: string;
19
+ kind: EditHistoryKind;
20
+ coalesceKey?: string;
21
+ files: Record<string, { before: string; after: string }>;
22
+ }
23
+
24
+ export interface UseDomEditSessionParams {
25
+ projectId: string | null;
26
+ activeCompPath: string | null;
27
+ isMasterView: boolean;
28
+ compIdToSrc: Map<string, string>;
29
+ captionEditMode: boolean;
30
+ compositionLoading: boolean;
31
+ previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
32
+ timelineElements: TimelineElement[];
33
+ currentTime: number;
34
+ setSelectedTimelineElementId: (id: string | null) => void;
35
+ setRightCollapsed: (collapsed: boolean) => void;
36
+ setRightPanelTab: (tab: RightPanelTab) => void;
37
+ showToast: (message: string, tone?: "error" | "info") => void;
38
+ refreshPreviewDocumentVersion: () => void;
39
+ commitStudioManualEditManifestOptimistically: (
40
+ updateManifest: (manifest: StudioManualEditManifest) => StudioManualEditManifest,
41
+ options: { label: string; coalesceKey: string },
42
+ ) => void;
43
+ commitStudioMotionManifestOptimistically: (
44
+ updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
45
+ options: { label: string; coalesceKey: string },
46
+ ) => void;
47
+ applyCurrentStudioManualEditsToPreview: (iframe: HTMLIFrameElement | null) => void;
48
+ applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
49
+ readProjectFile: (path: string) => Promise<string>;
50
+ writeProjectFile: (path: string, content: string) => Promise<void>;
51
+ domEditSaveTimestampRef: React.MutableRefObject<number>;
52
+ editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
53
+ fileTree: string[];
54
+ importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
55
+ projectDir: string | null;
56
+ projectIdRef: React.MutableRefObject<string | null>;
57
+ previewIframe: HTMLIFrameElement | null;
58
+ refreshKey: number;
59
+ rightPanelTab: RightPanelTab;
60
+ applyStudioManualEditsToPreviewRef: React.MutableRefObject<
61
+ (iframe: HTMLIFrameElement) => Promise<void>
62
+ >;
63
+ applyStudioMotionToPreviewRef: React.MutableRefObject<
64
+ (iframe: HTMLIFrameElement) => Promise<void>
65
+ >;
66
+ syncPreviewHistoryHotkey: (iframe: HTMLIFrameElement | null) => void;
67
+ reloadPreview: () => void;
68
+ setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
69
+ }
70
+
71
+ // ── Hook ──
72
+
73
+ export function useDomEditSession({
74
+ projectId,
75
+ activeCompPath,
76
+ isMasterView,
77
+ compIdToSrc,
78
+ captionEditMode,
79
+ compositionLoading,
80
+ previewIframeRef,
81
+ timelineElements,
82
+ currentTime,
83
+ setSelectedTimelineElementId,
84
+ setRightCollapsed,
85
+ setRightPanelTab,
86
+ showToast,
87
+ refreshPreviewDocumentVersion,
88
+ commitStudioManualEditManifestOptimistically,
89
+ commitStudioMotionManifestOptimistically,
90
+ applyCurrentStudioManualEditsToPreview,
91
+ applyCurrentStudioMotionToPreview,
92
+ readProjectFile: _readProjectFile,
93
+ writeProjectFile,
94
+ domEditSaveTimestampRef,
95
+ editHistory,
96
+ fileTree,
97
+ importedFontAssetsRef,
98
+ projectDir,
99
+ projectIdRef,
100
+ previewIframe,
101
+ refreshKey,
102
+ rightPanelTab,
103
+ applyStudioManualEditsToPreviewRef,
104
+ applyStudioMotionToPreviewRef,
105
+ syncPreviewHistoryHotkey,
106
+ reloadPreview,
107
+ setRefreshKey: _setRefreshKey,
108
+ }: UseDomEditSessionParams) {
109
+ void _setRefreshKey;
110
+ // ── Selection (delegated to useDomSelection) ──
111
+
112
+ const {
113
+ domEditSelection,
114
+ domEditGroupSelections,
115
+ domEditHoverSelection,
116
+ domEditSelectionRef,
117
+ domEditGroupSelectionsRef,
118
+ applyDomSelection,
119
+ clearDomSelection,
120
+ buildDomSelectionFromTarget,
121
+ resolveDomSelectionFromPreviewPoint,
122
+ updateDomEditHoverSelection,
123
+ buildDomSelectionForTimelineElement,
124
+ handleTimelineElementSelect,
125
+ refreshDomEditSelectionFromPreview,
126
+ refreshDomEditGroupSelectionsFromPreview,
127
+ } = useDomSelection({
128
+ projectId,
129
+ activeCompPath,
130
+ isMasterView,
131
+ compIdToSrc,
132
+ captionEditMode,
133
+ previewIframeRef,
134
+ timelineElements,
135
+ setSelectedTimelineElementId,
136
+ setRightCollapsed,
137
+ setRightPanelTab,
138
+ previewIframe,
139
+ refreshKey,
140
+ rightPanelTab,
141
+ });
142
+
143
+ // ── Agent modal (delegated to useAskAgentModal) ──
144
+
145
+ const {
146
+ agentModalOpen,
147
+ agentModalAnchorPoint,
148
+ copiedAgentPrompt,
149
+ agentPromptSelectionContext,
150
+ setAgentModalOpen,
151
+ setAgentPromptSelectionContext,
152
+ setAgentModalAnchorPoint,
153
+ preloadAgentPromptSnippet,
154
+ handleAskAgent,
155
+ handleAgentModalSubmit,
156
+ } = useAskAgentModal({
157
+ projectId,
158
+ activeCompPath,
159
+ projectDir,
160
+ projectIdRef,
161
+ currentTime,
162
+ showToast,
163
+ domEditSelectionRef,
164
+ domEditSelection,
165
+ });
166
+
167
+ // ── Preview interaction (delegated to usePreviewInteraction) ──
168
+
169
+ const {
170
+ handlePreviewCanvasMouseDown,
171
+ handlePreviewCanvasPointerMove,
172
+ handlePreviewCanvasPointerLeave,
173
+ handleBlockedDomMove,
174
+ handleDomManualDragStart,
175
+ } = usePreviewInteraction({
176
+ captionEditMode,
177
+ compositionLoading,
178
+ previewIframeRef,
179
+ activeCompPath,
180
+ showToast,
181
+ applyDomSelection,
182
+ resolveDomSelectionFromPreviewPoint,
183
+ updateDomEditHoverSelection,
184
+ preloadAgentPromptSnippet,
185
+ setAgentPromptSelectionContext,
186
+ setAgentModalAnchorPoint,
187
+ setAgentModalOpen,
188
+ });
189
+
190
+ // ── Commit handlers (delegated to useDomEditCommits) ──
191
+
192
+ const {
193
+ resolveImportedFontAsset,
194
+ handleDomStyleCommit,
195
+ handleDomTextCommit,
196
+ handleDomTextFieldStyleCommit,
197
+ handleDomAddTextField,
198
+ handleDomRemoveTextField,
199
+ handleDomPathOffsetCommit,
200
+ handleDomGroupPathOffsetCommit,
201
+ handleDomBoxSizeCommit,
202
+ handleDomRotationCommit,
203
+ handleDomManualEditsReset,
204
+ handleDomMotionCommit,
205
+ handleDomMotionClear,
206
+ handleDomEditElementDelete,
207
+ } = useDomEditCommits({
208
+ activeCompPath,
209
+ previewIframeRef,
210
+ showToast,
211
+ commitStudioManualEditManifestOptimistically,
212
+ commitStudioMotionManifestOptimistically,
213
+ applyCurrentStudioManualEditsToPreview,
214
+ applyCurrentStudioMotionToPreview,
215
+ writeProjectFile,
216
+ domEditSaveTimestampRef,
217
+ editHistory,
218
+ fileTree,
219
+ importedFontAssetsRef,
220
+ projectId,
221
+ projectIdRef,
222
+ reloadPreview,
223
+ domEditSelection,
224
+ domEditSelectionRef,
225
+ domEditGroupSelectionsRef,
226
+ applyDomSelection,
227
+ clearDomSelection,
228
+ refreshDomEditSelectionFromPreview,
229
+ refreshDomEditGroupSelectionsFromPreview,
230
+ buildDomSelectionFromTarget,
231
+ });
232
+
233
+ // ── Effects ──
234
+
235
+ // Sync selection from preview document on load / refresh
236
+ // eslint-disable-next-line no-restricted-syntax
237
+ useEffect(() => {
238
+ if (!previewIframe) return;
239
+
240
+ const syncSelectionFromDocument = () => {
241
+ if (!STUDIO_INSPECTOR_PANELS_ENABLED || captionEditMode) return;
242
+ const currentSelection = domEditSelectionRef.current;
243
+ if (!currentSelection) return;
244
+ let doc: Document | null = null;
245
+ try {
246
+ doc = previewIframe.contentDocument;
247
+ } catch {
248
+ return;
249
+ }
250
+ if (!doc) return;
251
+
252
+ const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
253
+ if (!nextElement) {
254
+ applyDomSelection(null, { revealPanel: false });
255
+ return;
256
+ }
257
+
258
+ const nextSelection = buildDomSelectionFromTarget(nextElement);
259
+ if (nextSelection) {
260
+ applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
261
+ }
262
+ };
263
+
264
+ syncPreviewHistoryHotkey(previewIframe);
265
+ void (async () => {
266
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
267
+ await applyStudioMotionToPreviewRef.current(previewIframe);
268
+ })();
269
+ syncSelectionFromDocument();
270
+ refreshPreviewDocumentVersion();
271
+
272
+ const handleLoad = () => {
273
+ syncPreviewHistoryHotkey(previewIframe);
274
+ void (async () => {
275
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
276
+ await applyStudioMotionToPreviewRef.current(previewIframe);
277
+ })();
278
+ syncSelectionFromDocument();
279
+ refreshPreviewDocumentVersion();
280
+ };
281
+
282
+ previewIframe.addEventListener("load", handleLoad);
283
+ return () => {
284
+ previewIframe.removeEventListener("load", handleLoad);
285
+ };
286
+ }, [
287
+ activeCompPath,
288
+ applyDomSelection,
289
+ buildDomSelectionFromTarget,
290
+ captionEditMode,
291
+ domEditSelectionRef,
292
+ previewIframe,
293
+ refreshPreviewDocumentVersion,
294
+ syncPreviewHistoryHotkey,
295
+ applyStudioManualEditsToPreviewRef,
296
+ applyStudioMotionToPreviewRef,
297
+ ]);
298
+
299
+ return {
300
+ // State
301
+ domEditSelection,
302
+ domEditGroupSelections,
303
+ domEditHoverSelection,
304
+ agentModalOpen,
305
+ agentModalAnchorPoint,
306
+ copiedAgentPrompt,
307
+ agentPromptSelectionContext,
308
+
309
+ // Refs
310
+ domEditSelectionRef,
311
+
312
+ // Callbacks
313
+ handleTimelineElementSelect,
314
+ handlePreviewCanvasMouseDown,
315
+ handlePreviewCanvasPointerMove,
316
+ handlePreviewCanvasPointerLeave,
317
+ applyDomSelection,
318
+ clearDomSelection,
319
+ handleDomStyleCommit,
320
+ handleDomPathOffsetCommit,
321
+ handleDomGroupPathOffsetCommit,
322
+ handleDomBoxSizeCommit,
323
+ handleDomRotationCommit,
324
+ handleDomManualEditsReset,
325
+ handleDomMotionCommit,
326
+ handleDomMotionClear,
327
+ handleDomTextCommit,
328
+ handleDomTextFieldStyleCommit,
329
+ handleDomAddTextField,
330
+ handleDomRemoveTextField,
331
+ handleAskAgent,
332
+ handleAgentModalSubmit,
333
+ handleBlockedDomMove,
334
+ handleDomManualDragStart,
335
+ handleDomEditElementDelete,
336
+ buildDomSelectionForTimelineElement,
337
+ resolveImportedFontAsset,
338
+ setAgentModalOpen,
339
+ setAgentPromptSelectionContext,
340
+ setAgentModalAnchorPoint,
341
+ };
342
+ }
@@ -0,0 +1,330 @@
1
+ import { useCallback, useRef } from "react";
2
+ import type { PatchOperation } from "../utils/sourcePatcher";
3
+ import {
4
+ isImageBackgroundValue,
5
+ isManualGeometryStyleProperty,
6
+ normalizeDomEditStyleValue,
7
+ } from "../utils/studioHelpers";
8
+ import {
9
+ injectPreviewGoogleFont,
10
+ injectPreviewImportedFont,
11
+ ensureImportedFontFace,
12
+ } from "../utils/studioFontHelpers";
13
+ import {
14
+ buildDomEditStylePatchOperation,
15
+ buildDomEditTextPatchOperation,
16
+ findElementForSelection,
17
+ isTextEditableSelection,
18
+ serializeDomEditTextFields,
19
+ buildDefaultDomEditTextField,
20
+ type DomEditTextField,
21
+ type DomEditSelection,
22
+ } from "../components/editor/domEditing";
23
+ import type { ImportedFontAsset } from "../components/editor/fontAssets";
24
+ import type { PersistDomEditOperations } from "./useDomEditCommits";
25
+
26
+ // ── Types ──
27
+
28
+ export interface UseDomEditTextCommitsParams {
29
+ activeCompPath: string | null;
30
+ previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
31
+ domEditSelection: DomEditSelection | null;
32
+ applyDomSelection: (
33
+ selection: DomEditSelection | null,
34
+ options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
35
+ ) => void;
36
+ refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => void;
37
+ buildDomSelectionFromTarget: (
38
+ target: HTMLElement,
39
+ options?: { preferClipAncestor?: boolean },
40
+ ) => DomEditSelection | null;
41
+ persistDomEditOperations: PersistDomEditOperations;
42
+ resolveImportedFontAsset: (fontFamilyValue: string) => ImportedFontAsset | null;
43
+ }
44
+
45
+ // ── Hook ──
46
+
47
+ export function useDomEditTextCommits({
48
+ activeCompPath,
49
+ previewIframeRef,
50
+ domEditSelection,
51
+ applyDomSelection,
52
+ refreshDomEditSelectionFromPreview,
53
+ buildDomSelectionFromTarget,
54
+ persistDomEditOperations,
55
+ resolveImportedFontAsset,
56
+ }: UseDomEditTextCommitsParams) {
57
+ const domTextCommitVersionRef = useRef(0);
58
+
59
+ const handleDomStyleCommit = useCallback(
60
+ async (property: string, value: string) => {
61
+ if (!domEditSelection) return;
62
+ if (isManualGeometryStyleProperty(property)) return;
63
+ if (!domEditSelection.capabilities.canEditStyles) return;
64
+ const importedFont = property === "font-family" ? resolveImportedFontAsset(value) : null;
65
+ const iframe = previewIframeRef.current;
66
+ const doc = iframe?.contentDocument;
67
+ if (doc) {
68
+ const el = findElementForSelection(doc, domEditSelection, activeCompPath);
69
+ if (el) {
70
+ el.style.setProperty(property, normalizeDomEditStyleValue(property, value));
71
+ if (property === "font-family") {
72
+ injectPreviewGoogleFont(doc, value);
73
+ if (importedFont) injectPreviewImportedFont(doc, importedFont);
74
+ }
75
+ if (property === "background-image" && isImageBackgroundValue(value)) {
76
+ el.style.setProperty("background-position", "center");
77
+ el.style.setProperty("background-repeat", "no-repeat");
78
+ el.style.setProperty("background-size", "contain");
79
+ }
80
+ }
81
+ }
82
+ const operations: PatchOperation[] = [
83
+ buildDomEditStylePatchOperation(property, normalizeDomEditStyleValue(property, value)),
84
+ ];
85
+ if (property === "background-image" && isImageBackgroundValue(value)) {
86
+ operations.push(
87
+ buildDomEditStylePatchOperation("background-position", "center"),
88
+ buildDomEditStylePatchOperation("background-repeat", "no-repeat"),
89
+ buildDomEditStylePatchOperation("background-size", "contain"),
90
+ );
91
+ }
92
+ const skipRefresh = property !== "z-index";
93
+ try {
94
+ await persistDomEditOperations(domEditSelection, operations, {
95
+ label: "Edit layer style",
96
+ skipRefresh,
97
+ prepareContent: importedFont
98
+ ? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
99
+ : undefined,
100
+ });
101
+ } catch (err) {
102
+ console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
103
+ }
104
+ refreshDomEditSelectionFromPreview(domEditSelection);
105
+ },
106
+ [
107
+ activeCompPath,
108
+ domEditSelection,
109
+ persistDomEditOperations,
110
+ refreshDomEditSelectionFromPreview,
111
+ resolveImportedFontAsset,
112
+ previewIframeRef,
113
+ ],
114
+ );
115
+
116
+ const handleDomTextCommit = useCallback(
117
+ async (value: string, fieldKey?: string) => {
118
+ if (!domEditSelection) return;
119
+ if (!isTextEditableSelection(domEditSelection)) return;
120
+ const commitVersion = domTextCommitVersionRef.current + 1;
121
+ domTextCommitVersionRef.current = commitVersion;
122
+ const nextTextFields =
123
+ domEditSelection.textFields.length > 0
124
+ ? domEditSelection.textFields.map((field) =>
125
+ field.key === fieldKey ? { ...field, value } : field,
126
+ )
127
+ : [];
128
+ const nextContent =
129
+ nextTextFields.length > 1 || nextTextFields.some((field) => field.source === "child")
130
+ ? serializeDomEditTextFields(nextTextFields)
131
+ : value;
132
+ const iframe = previewIframeRef.current;
133
+ const doc = iframe?.contentDocument;
134
+ if (doc) {
135
+ const el = findElementForSelection(doc, domEditSelection, activeCompPath);
136
+ if (el) {
137
+ if (
138
+ nextTextFields.length > 1 ||
139
+ nextTextFields.some((field) => field.source === "child")
140
+ ) {
141
+ el.innerHTML = nextContent;
142
+ } else {
143
+ el.textContent = value;
144
+ }
145
+ }
146
+ }
147
+ await persistDomEditOperations(
148
+ domEditSelection,
149
+ [buildDomEditTextPatchOperation(nextContent)],
150
+ {
151
+ label: "Edit text",
152
+ skipRefresh: true,
153
+ shouldSave: () => domTextCommitVersionRef.current === commitVersion,
154
+ },
155
+ );
156
+ if (domTextCommitVersionRef.current !== commitVersion) return;
157
+
158
+ if (doc) {
159
+ const refreshed = findElementForSelection(doc, domEditSelection, activeCompPath);
160
+ if (refreshed) {
161
+ const nextSelection = buildDomSelectionFromTarget(refreshed);
162
+ if (nextSelection) {
163
+ applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
164
+ }
165
+ }
166
+ }
167
+ },
168
+ [
169
+ activeCompPath,
170
+ applyDomSelection,
171
+ buildDomSelectionFromTarget,
172
+ domEditSelection,
173
+ persistDomEditOperations,
174
+ previewIframeRef,
175
+ ],
176
+ );
177
+
178
+ const commitDomTextFields = useCallback(
179
+ async (
180
+ selection: DomEditSelection,
181
+ nextTextFields: DomEditTextField[],
182
+ options?: { importedFont?: ImportedFontAsset | null },
183
+ ) => {
184
+ const nextContent =
185
+ nextTextFields.length > 1 || nextTextFields.some((field) => field.source === "child")
186
+ ? serializeDomEditTextFields(nextTextFields)
187
+ : (nextTextFields[0]?.value ?? "");
188
+
189
+ const iframe = previewIframeRef.current;
190
+ const doc = iframe?.contentDocument;
191
+ if (doc) {
192
+ const el = findElementForSelection(doc, selection, activeCompPath);
193
+ if (el) {
194
+ if (
195
+ nextTextFields.length > 1 ||
196
+ nextTextFields.some((field) => field.source === "child")
197
+ ) {
198
+ el.innerHTML = nextContent;
199
+ } else {
200
+ el.textContent = nextContent;
201
+ }
202
+ }
203
+ }
204
+
205
+ const importedFont = options?.importedFont ?? null;
206
+ await persistDomEditOperations(selection, [buildDomEditTextPatchOperation(nextContent)], {
207
+ label: "Edit text",
208
+ skipRefresh: true,
209
+ prepareContent: importedFont
210
+ ? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
211
+ : undefined,
212
+ });
213
+
214
+ if (doc) {
215
+ const refreshed = findElementForSelection(doc, selection, activeCompPath);
216
+ if (refreshed) {
217
+ const nextSelection = buildDomSelectionFromTarget(refreshed);
218
+ if (nextSelection) {
219
+ applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
220
+ }
221
+ }
222
+ }
223
+ },
224
+ [
225
+ activeCompPath,
226
+ applyDomSelection,
227
+ buildDomSelectionFromTarget,
228
+ persistDomEditOperations,
229
+ previewIframeRef,
230
+ ],
231
+ );
232
+
233
+ const handleDomTextFieldStyleCommit = useCallback(
234
+ async (fieldKey: string, property: string, value: string) => {
235
+ if (!domEditSelection) return;
236
+ const field = domEditSelection.textFields.find((entry) => entry.key === fieldKey);
237
+ if (!field) return;
238
+
239
+ if (field.source === "self") {
240
+ await handleDomStyleCommit(property, value);
241
+ return;
242
+ }
243
+
244
+ const normalizedValue = normalizeDomEditStyleValue(property, value);
245
+ const importedFont = property === "font-family" ? resolveImportedFontAsset(value) : null;
246
+ if (property === "font-family") {
247
+ const doc = previewIframeRef.current?.contentDocument;
248
+ if (doc) {
249
+ injectPreviewGoogleFont(doc, normalizedValue);
250
+ if (importedFont) injectPreviewImportedFont(doc, importedFont);
251
+ }
252
+ }
253
+ const nextTextFields = domEditSelection.textFields.map((entry) =>
254
+ entry.key === fieldKey
255
+ ? {
256
+ ...entry,
257
+ inlineStyles: {
258
+ ...entry.inlineStyles,
259
+ [property]: normalizedValue,
260
+ },
261
+ computedStyles: {
262
+ ...entry.computedStyles,
263
+ [property]: normalizedValue,
264
+ },
265
+ }
266
+ : entry,
267
+ );
268
+
269
+ await commitDomTextFields(domEditSelection, nextTextFields, { importedFont });
270
+ },
271
+ [
272
+ commitDomTextFields,
273
+ domEditSelection,
274
+ handleDomStyleCommit,
275
+ resolveImportedFontAsset,
276
+ previewIframeRef,
277
+ ],
278
+ );
279
+
280
+ const handleDomAddTextField = useCallback(
281
+ async (afterFieldKey?: string) => {
282
+ if (!domEditSelection) return null;
283
+ if (!domEditSelection.textFields.some((field) => field.source === "child")) return null;
284
+
285
+ const insertionIndex = domEditSelection.textFields.findIndex(
286
+ (field) => field.key === afterFieldKey,
287
+ );
288
+ const baseField =
289
+ domEditSelection.textFields[insertionIndex >= 0 ? insertionIndex : 0] ??
290
+ domEditSelection.textFields[0];
291
+ const nextField = buildDefaultDomEditTextField(baseField);
292
+ const nextTextFields = [...domEditSelection.textFields];
293
+ nextTextFields.splice(
294
+ insertionIndex >= 0 ? insertionIndex + 1 : nextTextFields.length,
295
+ 0,
296
+ nextField,
297
+ );
298
+
299
+ await commitDomTextFields(domEditSelection, nextTextFields);
300
+ return nextField.key;
301
+ },
302
+ [commitDomTextFields, domEditSelection],
303
+ );
304
+
305
+ const handleDomRemoveTextField = useCallback(
306
+ async (fieldKey: string) => {
307
+ if (!domEditSelection) return;
308
+ const field = domEditSelection.textFields.find((entry) => entry.key === fieldKey);
309
+ if (!field) return;
310
+
311
+ if (field.source === "self") {
312
+ await handleDomTextCommit("", fieldKey);
313
+ return;
314
+ }
315
+
316
+ const nextTextFields = domEditSelection.textFields.filter((entry) => entry.key !== fieldKey);
317
+ await commitDomTextFields(domEditSelection, nextTextFields);
318
+ },
319
+ [commitDomTextFields, domEditSelection, handleDomTextCommit],
320
+ );
321
+
322
+ return {
323
+ handleDomStyleCommit,
324
+ handleDomTextCommit,
325
+ commitDomTextFields,
326
+ handleDomTextFieldStyleCommit,
327
+ handleDomAddTextField,
328
+ handleDomRemoveTextField,
329
+ };
330
+ }