@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

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 (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.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 +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -1,1117 +1,44 @@
1
- import { formatTime } from "../../player/lib/time";
2
- import type { PatchOperation, PatchTarget } from "../../utils/sourcePatcher";
3
-
4
- const CURATED_STYLE_PROPERTIES = [
5
- "position",
6
- "display",
7
- "top",
8
- "left",
9
- "right",
10
- "bottom",
11
- "inset",
12
- "width",
13
- "height",
14
- "gap",
15
- "justify-content",
16
- "align-items",
17
- "flex-direction",
18
- "font-size",
19
- "font-style",
20
- "font-weight",
21
- "font-family",
22
- "line-height",
23
- "letter-spacing",
24
- "text-align",
25
- "text-transform",
26
- "color",
27
- "background-color",
28
- "background-image",
29
- "opacity",
30
- "mix-blend-mode",
31
- "border-radius",
32
- "border-width",
33
- "border-style",
34
- "border-color",
35
- "border-top-width",
36
- "border-top-style",
37
- "border-top-color",
38
- "outline-color",
39
- "overflow",
40
- "clip-path",
41
- "box-shadow",
42
- "filter",
43
- "backdrop-filter",
44
- "z-index",
45
- "transform",
46
- ] as const;
47
-
48
- export interface DomEditCapabilities {
49
- canSelect: boolean;
50
- canEditStyles: boolean;
51
- /** Directly editable authored left/top style fields. Canvas drag uses manual edits instead. */
52
- canMove: boolean;
53
- /** Directly editable authored width/height style fields. Canvas resize uses manual edits instead. */
54
- canResize: boolean;
55
- canApplyManualOffset: boolean;
56
- canApplyManualSize: boolean;
57
- canApplyManualRotation: boolean;
58
- reasonIfDisabled?: string;
59
- }
60
-
61
- export interface DomEditTextField {
62
- key: string;
63
- label: string;
64
- value: string;
65
- tagName: string;
66
- attributes: Array<{ name: string; value: string }>;
67
- inlineStyles: Record<string, string>;
68
- computedStyles: Record<string, string>;
69
- source: "self" | "child";
70
- }
71
-
72
- export interface DomEditSelection extends PatchTarget {
73
- element: HTMLElement;
74
- label: string;
75
- tagName: string;
76
- sourceFile: string;
77
- compositionPath: string;
78
- compositionSrc?: string;
79
- isCompositionHost: boolean;
80
- boundingBox: { x: number; y: number; width: number; height: number };
81
- textContent: string | null;
82
- dataAttributes: Record<string, string>;
83
- inlineStyles: Record<string, string>;
84
- computedStyles: Record<string, string>;
85
- textFields: DomEditTextField[];
86
- capabilities: DomEditCapabilities;
87
- }
88
-
89
- export interface DomEditLayerItem {
90
- key: string;
91
- element: HTMLElement;
92
- label: string;
93
- tagName: string;
94
- depth: number;
95
- childCount: number;
96
- id?: string;
97
- selector?: string;
98
- selectorIndex?: number;
99
- sourceFile: string;
100
- }
101
-
102
- export interface DomEditContextOptions {
103
- activeCompositionPath: string | null;
104
- isMasterView: boolean;
105
- preferClipAncestor?: boolean;
106
- }
107
-
108
- export interface DomEditViewport {
109
- width: number;
110
- height: number;
111
- }
112
-
113
- export interface TimelineElementDomTarget {
114
- id?: string;
115
- domId?: string;
116
- selector?: string;
117
- selectorIndex?: number;
118
- sourceFile?: string;
119
- compositionSrc?: string;
120
- }
121
-
122
- export interface TimelineElementDomTargetOptions {
123
- activeCompositionPath: string | null;
124
- compIdToSrc?: ReadonlyMap<string, string>;
125
- isMasterView: boolean;
126
- }
127
-
128
- function isHtmlElement(value: unknown): value is HTMLElement {
129
- return (
130
- typeof value === "object" &&
131
- value !== null &&
132
- "nodeType" in value &&
133
- typeof (value as { nodeType?: unknown }).nodeType === "number" &&
134
- (value as { nodeType: number }).nodeType === 1
135
- );
136
- }
137
-
138
- function parsePx(value: string | undefined): number | null {
139
- if (!value) return null;
140
- const trimmed = value.trim();
141
- if (!trimmed.endsWith("px")) return null;
142
- const parsed = parseFloat(trimmed);
143
- return Number.isFinite(parsed) ? parsed : null;
144
- }
145
-
146
- function isIdentityTransform(value: string | undefined): boolean {
147
- const transform = (value ?? "none").trim();
148
- if (!transform || transform === "none") return true;
149
-
150
- const matrix = transform.match(/^matrix\(([^)]+)\)$/i);
151
- if (matrix) {
152
- const values = matrix[1].split(",").map((part) => Number.parseFloat(part.trim()));
153
- if (values.length !== 6 || values.some((part) => !Number.isFinite(part))) return false;
154
- return (
155
- Math.abs(values[0] - 1) < 0.0001 &&
156
- Math.abs(values[1]) < 0.0001 &&
157
- Math.abs(values[2]) < 0.0001 &&
158
- Math.abs(values[3] - 1) < 0.0001 &&
159
- Math.abs(values[4]) < 0.0001 &&
160
- Math.abs(values[5]) < 0.0001
161
- );
162
- }
163
-
164
- const matrix3d = transform.match(/^matrix3d\(([^)]+)\)$/i);
165
- if (!matrix3d) return false;
166
- const values = matrix3d[1].split(",").map((part) => Number.parseFloat(part.trim()));
167
- if (values.length !== 16 || values.some((part) => !Number.isFinite(part))) return false;
168
- const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
169
- return values.every((part, index) => Math.abs(part - identity[index]) < 0.0001);
170
- }
171
-
172
- function isTextBearingTag(tagName: string): boolean {
173
- return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
174
- }
175
-
176
- function getCuratedComputedStyles(el: HTMLElement): Record<string, string> {
177
- const styles: Record<string, string> = {};
178
- const computed = el.ownerDocument.defaultView?.getComputedStyle(el);
179
- if (!computed) return styles;
180
-
181
- for (const prop of CURATED_STYLE_PROPERTIES) {
182
- const value = computed.getPropertyValue(prop);
183
- if (value) styles[prop] = value;
184
- }
185
-
186
- return styles;
187
- }
188
-
189
- function findClosestByAttribute(el: HTMLElement, attributeNames: string[]): HTMLElement | null {
190
- let current: HTMLElement | null = el;
191
- while (current) {
192
- const candidate = current;
193
- if (attributeNames.some((attribute) => candidate.hasAttribute(attribute))) {
194
- return candidate;
195
- }
196
- current = current.parentElement;
197
- }
198
- return null;
199
- }
200
-
201
- function getSourceFileForElement(
202
- el: HTMLElement,
203
- activeCompositionPath: string | null,
204
- ): { sourceFile: string; compositionPath: string } {
205
- const sourceHost = findClosestByAttribute(el, ["data-composition-file", "data-composition-src"]);
206
- const ownerRoot = findClosestByAttribute(el, ["data-composition-id"]);
207
- const sourceFile =
208
- sourceHost?.getAttribute("data-composition-file") ??
209
- sourceHost?.getAttribute("data-composition-src") ??
210
- ownerRoot?.getAttribute("data-composition-file") ??
211
- ownerRoot?.getAttribute("data-composition-src") ??
212
- activeCompositionPath ??
213
- "index.html";
214
-
215
- return {
216
- sourceFile,
217
- compositionPath: sourceFile,
218
- };
219
- }
220
-
221
- function getPreferredClipAncestor(startEl: HTMLElement): HTMLElement | null {
222
- let current: HTMLElement | null = startEl;
223
- while (current) {
224
- if (current.classList.contains("clip")) {
225
- const isCompositionHost =
226
- current.hasAttribute("data-composition-src") ||
227
- current.hasAttribute("data-composition-file");
228
- if (!isCompositionHost || current === startEl) return current;
229
- }
230
- current = current.parentElement;
231
- }
232
- return null;
233
- }
234
-
235
- function getSelectionCandidate(startEl: HTMLElement, options: DomEditContextOptions): HTMLElement {
236
- if (options.preferClipAncestor) {
237
- const clipAncestor = getPreferredClipAncestor(startEl);
238
- if (clipAncestor) {
239
- return clipAncestor;
240
- }
241
- }
242
-
243
- return startEl;
244
- }
245
-
246
- function getPreferredClassSelector(el: HTMLElement): string | undefined {
247
- const classes = Array.from(el.classList)
248
- .map((value) => value.trim())
249
- .filter(Boolean);
250
- if (classes.length === 0) return undefined;
251
- const preferred =
252
- classes.find((value) => value !== "clip" && !value.startsWith("__hf-")) ?? classes[0];
253
- return preferred ? `.${escapeCssIdentifier(preferred)}` : undefined;
254
- }
255
-
256
- function escapeCssIdentifier(value: string): string {
257
- const css = globalThis.CSS as { escape?: (input: string) => string } | undefined;
258
- if (typeof css?.escape === "function") return css.escape(value);
259
-
260
- if (value === "-") return "\\-";
261
-
262
- let escaped = "";
263
- for (let index = 0; index < value.length; index += 1) {
264
- const char = value[index] ?? "";
265
- const code = char.charCodeAt(0);
266
- if (code === 0) {
267
- escaped += "\uFFFD";
268
- continue;
269
- }
270
-
271
- const isDigit = code >= 48 && code <= 57;
272
- const isUpperAlpha = code >= 65 && code <= 90;
273
- const isLowerAlpha = code >= 97 && code <= 122;
274
- const isControl = (code >= 1 && code <= 31) || code === 127;
275
- const isLeadingDigit = index === 0 && isDigit;
276
- const isSecondDigitAfterDash = index === 1 && value.startsWith("-") && isDigit;
277
- if (isControl || isLeadingDigit || isSecondDigitAfterDash) {
278
- escaped += `\\${code.toString(16)} `;
279
- continue;
280
- }
281
- if (isUpperAlpha || isLowerAlpha || isDigit || char === "-" || char === "_" || code >= 128) {
282
- escaped += char;
283
- continue;
284
- }
285
- escaped += `\\${char}`;
286
- }
287
- return escaped;
288
- }
289
-
290
- function escapeCssString(value: string): string {
291
- return value
292
- .replace(/\\/g, "\\\\")
293
- .replace(/"/g, '\\"')
294
- .replace(/\n/g, "\\a ")
295
- .replace(/\r/g, "\\d ")
296
- .replace(/\f/g, "\\c ");
297
- }
298
-
299
- function normalizeTimelineCompositionSource(value: string | undefined): string | undefined {
300
- const trimmed = value?.trim();
301
- if (!trimmed) return undefined;
302
-
303
- let pathname = trimmed;
304
- try {
305
- pathname = new URL(trimmed, "http://studio.local").pathname;
306
- } catch {
307
- pathname = trimmed;
308
- }
309
-
310
- for (const marker of ["/preview/comp/", "/preview/"]) {
311
- const markerIndex = pathname.indexOf(marker);
312
- if (markerIndex < 0) continue;
313
- const sourcePath = pathname.slice(markerIndex + marker.length).replace(/^\/+/, "");
314
- return sourcePath || trimmed;
315
- }
316
-
317
- return trimmed;
318
- }
319
-
320
- function querySelectorAllSafely(doc: Document, selector: string): Element[] {
321
- try {
322
- return Array.from(doc.querySelectorAll(selector));
323
- } catch {
324
- return [];
325
- }
326
- }
327
-
328
- function humanizeIdentifier(value: string): string {
329
- return (
330
- value
331
- .replace(/\.html$/i, "")
332
- .replace(/^compositions\//i, "")
333
- .split("/")
334
- .at(-1)
335
- ?.replace(/[-_]+/g, " ")
336
- .replace(/\b\w/g, (char) => char.toUpperCase()) ?? value
337
- );
338
- }
339
-
340
- function buildStableSelector(el: HTMLElement): string | undefined {
341
- if (el.id) return `#${escapeCssIdentifier(el.id)}`;
342
-
343
- const compositionId = el.getAttribute("data-composition-id");
344
- if (compositionId) return `[data-composition-id="${escapeCssString(compositionId)}"]`;
345
-
346
- return getPreferredClassSelector(el);
347
- }
348
-
349
- function getSelectorIndex(
350
- doc: Document,
351
- el: HTMLElement,
352
- selector: string | undefined,
353
- sourceFile: string,
354
- activeCompositionPath: string | null,
355
- ): number | undefined {
356
- if (!selector?.startsWith(".")) return undefined;
357
-
358
- const candidates = querySelectorAllSafely(doc, selector).filter(
359
- (candidate): candidate is HTMLElement =>
360
- isHtmlElement(candidate) &&
361
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile === sourceFile,
362
- );
363
- const index = candidates.indexOf(el);
364
- return index >= 0 ? index : undefined;
365
- }
366
-
367
- export function getDomEditLayerKey(
368
- target: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
369
- ): string {
370
- const selectorIndex = target.selectorIndex ?? 0;
371
- return `${target.sourceFile}:${target.id ?? target.selector ?? "layer"}:${selectorIndex}`;
372
- }
373
-
374
- function buildElementLabel(el: HTMLElement): string {
375
- const compositionId = el.getAttribute("data-composition-id");
376
- if (compositionId && compositionId !== "main") {
377
- return humanizeIdentifier(compositionId);
378
- }
379
-
380
- const compositionSrc =
381
- el.getAttribute("data-composition-src") ?? el.getAttribute("data-composition-file");
382
- if (compositionSrc) {
383
- return humanizeIdentifier(compositionSrc);
384
- }
385
-
386
- if (el.id) return humanizeIdentifier(el.id);
387
-
388
- const preferredClass = getPreferredClassSelector(el);
389
- if (preferredClass) {
390
- return humanizeIdentifier(preferredClass.replace(/^\./, ""));
391
- }
392
-
393
- const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
394
- if (text) return text.length > 40 ? `${text.slice(0, 39)}…` : text;
395
- return el.tagName.toLowerCase();
396
- }
397
-
398
- const DOM_LAYER_IGNORED_TAGS = new Set([
399
- "base",
400
- "br",
401
- "canvas",
402
- "link",
403
- "meta",
404
- "script",
405
- "source",
406
- "style",
407
- "template",
408
- "track",
409
- "wbr",
410
- ]);
411
-
412
- function isInspectableLayerElement(el: HTMLElement): boolean {
413
- const tagName = el.tagName.toLowerCase();
414
- if (DOM_LAYER_IGNORED_TAGS.has(tagName)) return false;
415
-
416
- const computed = el.ownerDocument.defaultView?.getComputedStyle(el);
417
- if (computed?.display === "none" || computed?.visibility === "hidden") return false;
418
-
419
- return true;
420
- }
421
-
422
- function getDomLayerPatchTarget(
423
- el: HTMLElement,
424
- activeCompositionPath: string | null,
425
- ): Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile"> | null {
426
- if (!isInspectableLayerElement(el)) return null;
427
-
428
- const selector = buildStableSelector(el);
429
- if (!selector) return null;
430
-
431
- const { sourceFile } = getSourceFileForElement(el, activeCompositionPath);
432
- return {
433
- id: el.id || undefined,
434
- selector,
435
- selectorIndex: getSelectorIndex(
436
- el.ownerDocument,
437
- el,
438
- selector,
439
- sourceFile,
440
- activeCompositionPath,
441
- ),
442
- sourceFile,
443
- };
444
- }
445
-
446
- function getElementDepth(el: HTMLElement): number {
447
- let depth = 0;
448
- let current = el.parentElement;
449
- while (current) {
450
- depth += 1;
451
- current = current.parentElement;
452
- }
453
- return depth;
454
- }
455
-
456
- function hasRenderedBox(el: HTMLElement): boolean {
457
- const rect = el.getBoundingClientRect();
458
- if (rect.width <= 1 || rect.height <= 1) return false;
459
-
460
- const computed = el.ownerDocument.defaultView?.getComputedStyle(el);
461
- if (!computed) return true;
462
- if (computed.display === "none" || computed.visibility === "hidden") return false;
463
-
464
- const opacity = Number.parseFloat(computed.opacity);
465
- if (Number.isFinite(opacity) && opacity <= 0.01) return false;
466
-
467
- return true;
468
- }
469
-
470
- function getVisualElementScore(el: HTMLElement, pointerStackIndex: number): number {
471
- const tagName = el.tagName.toLowerCase();
472
- const rect = el.getBoundingClientRect();
473
- const area = Math.max(1, rect.width * rect.height);
474
- const smallerElementBonus = Math.max(0, 1_000_000 - Math.min(area, 1_000_000)) / 1_000;
475
- const visualLeafBonus =
476
- isEditableTextLeaf(el) || ["img", "video", "canvas", "svg"].includes(tagName) ? 2_000 : 0;
477
-
478
- return getElementDepth(el) * 10_000 + visualLeafBonus + smallerElementBonus - pointerStackIndex;
479
- }
480
-
481
- export function resolveVisualDomEditSelectionTarget(
482
- elementsFromPoint: Iterable<Element | null | undefined>,
483
- options: Pick<DomEditContextOptions, "activeCompositionPath">,
484
- ): HTMLElement | null {
485
- let best: { element: HTMLElement; score: number } | null = null;
486
- let pointerStackIndex = 0;
487
-
488
- for (const entry of elementsFromPoint) {
489
- if (!isHtmlElement(entry)) {
490
- pointerStackIndex += 1;
491
- continue;
492
- }
493
-
494
- if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
495
- const score = getVisualElementScore(entry, pointerStackIndex);
496
- if (!best || score > best.score) {
497
- best = { element: entry, score };
498
- }
499
- }
500
- pointerStackIndex += 1;
501
- }
502
-
503
- return best?.element ?? null;
504
- }
505
-
506
- function hasRasterBackground(selection: Pick<DomEditSelection, "computedStyles">): boolean {
507
- const backgroundImage = selection.computedStyles["background-image"]?.trim();
508
- return Boolean(backgroundImage && backgroundImage !== "none");
509
- }
510
-
511
- export function isLargeRasterDomEditSelection(
512
- selection: Pick<DomEditSelection, "boundingBox" | "computedStyles" | "tagName">,
513
- viewport?: DomEditViewport | null,
514
- ): boolean {
515
- const tagName = selection.tagName.toLowerCase();
516
- const isRasterLike = tagName === "img" || hasRasterBackground(selection);
517
- if (!isRasterLike) return false;
518
-
519
- const { width, height } = selection.boundingBox;
520
- if (width <= 1 || height <= 1) return false;
521
- if (!viewport || viewport.width <= 1 || viewport.height <= 1) {
522
- return width >= 960 && height >= 540;
523
- }
524
-
525
- const areaRatio = (width * height) / (viewport.width * viewport.height);
526
- const widthRatio = width / viewport.width;
527
- const heightRatio = height / viewport.height;
528
- return areaRatio >= 0.4 || (widthRatio >= 0.7 && heightRatio >= 0.5);
529
- }
530
-
531
- function getDirectLayerChildren(el: HTMLElement, options: DomEditContextOptions): HTMLElement[] {
532
- return Array.from(el.children).filter(
533
- (child): child is HTMLElement =>
534
- isHtmlElement(child) && getDomLayerPatchTarget(child, options.activeCompositionPath) !== null,
535
- );
536
- }
537
-
538
- export function countDomEditChildLayers(
539
- root: HTMLElement | null | undefined,
540
- options: DomEditContextOptions,
541
- maxCount = 99,
542
- ): number {
543
- if (!root) return 0;
544
-
545
- let count = 0;
546
- const visit = (el: HTMLElement) => {
547
- for (const child of Array.from(el.children)) {
548
- if (!isHtmlElement(child)) continue;
549
- if (getDomLayerPatchTarget(child, options.activeCompositionPath)) {
550
- count += 1;
551
- if (count >= maxCount) return;
552
- }
553
- visit(child);
554
- if (count >= maxCount) return;
555
- }
556
- };
557
-
558
- visit(root);
559
- return count;
560
- }
561
-
562
- export function collectDomEditLayerItems(
563
- root: HTMLElement | null | undefined,
564
- options: DomEditContextOptions,
565
- maxItems = 80,
566
- ): DomEditLayerItem[] {
567
- if (!root) return [];
568
-
569
- const items: DomEditLayerItem[] = [];
570
- const visit = (el: HTMLElement, depth: number) => {
571
- if (items.length >= maxItems) return;
572
-
573
- const target = getDomLayerPatchTarget(el, options.activeCompositionPath);
574
- if (target) {
575
- items.push({
576
- key: getDomEditLayerKey(target),
577
- element: el,
578
- label: buildElementLabel(el),
579
- tagName: el.tagName.toLowerCase(),
580
- depth,
581
- childCount: getDirectLayerChildren(el, options).length,
582
- id: target.id ?? undefined,
583
- selector: target.selector ?? undefined,
584
- selectorIndex: target.selectorIndex,
585
- sourceFile: target.sourceFile,
586
- });
587
- }
588
-
589
- const nextDepth = target ? depth + 1 : depth;
590
- for (const child of Array.from(el.children)) {
591
- if (!isHtmlElement(child)) continue;
592
- visit(child, nextDepth);
593
- if (items.length >= maxItems) return;
594
- }
595
- };
596
-
597
- visit(root, 0);
598
- return items;
599
- }
600
-
601
- function getDataAttributes(el: HTMLElement): Record<string, string> {
602
- const attrs: Record<string, string> = {};
603
- for (const attr of el.attributes) {
604
- if (attr.name.startsWith("data-")) {
605
- attrs[attr.name.slice(5)] = attr.value;
606
- }
607
- }
608
- return attrs;
609
- }
610
-
611
- function getInlineStyles(el: HTMLElement): Record<string, string> {
612
- const styles: Record<string, string> = {};
613
- for (const property of CURATED_STYLE_PROPERTIES) {
614
- const value = el.style.getPropertyValue(property);
615
- if (value) styles[property] = value;
616
- }
617
- return styles;
618
- }
619
-
620
- function isEditableTextLeaf(el: HTMLElement): boolean {
621
- return isTextBearingTag(el.tagName.toLowerCase()) && el.children.length === 0;
622
- }
623
-
624
- function getTextFieldLabel(
625
- _tagName: string,
626
- index: number,
627
- total: number,
628
- source: "self" | "child",
629
- ): string {
630
- if (source === "self" || total === 1) return "Content";
631
- return `Text ${index + 1}`;
632
- }
633
-
634
- function buildTextField(
635
- el: HTMLElement,
636
- index: number,
637
- total: number,
638
- source: "self" | "child",
639
- ): DomEditTextField {
640
- const tagName = el.tagName.toLowerCase();
641
- const key = el.getAttribute("data-hf-text-key") ?? `${source}:${index}:${tagName}`;
642
- return {
643
- key,
644
- label: getTextFieldLabel(tagName, index, total, source),
645
- value: el.textContent ?? "",
646
- tagName,
647
- attributes: Array.from(el.attributes)
648
- .filter((attribute) => attribute.name !== "style")
649
- .map((attribute) => ({
650
- name: attribute.name,
651
- value: attribute.value,
652
- })),
653
- inlineStyles: getInlineStyles(el),
654
- computedStyles: getCuratedComputedStyles(el),
655
- source,
656
- };
657
- }
658
-
659
- function collectDomEditTextFields(el: HTMLElement): DomEditTextField[] {
660
- const childFields = Array.from(el.children).filter(isHtmlElement).filter(isEditableTextLeaf);
661
- if (childFields.length > 0) {
662
- return childFields.map((child, index) =>
663
- buildTextField(child, index, childFields.length, "child"),
664
- );
665
- }
666
-
667
- if (isEditableTextLeaf(el)) {
668
- return [buildTextField(el, 0, 1, "self")];
669
- }
670
-
671
- return [];
672
- }
673
-
674
- function escapeHtmlText(value: string): string {
675
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
676
- }
677
-
678
- function serializeTextFieldStyle(field: DomEditTextField): string {
679
- const entries = Object.entries(field.inlineStyles).filter(([, value]) => Boolean(value));
680
- if (entries.length === 0) return "";
681
- return entries.map(([key, value]) => `${key}: ${value}`).join("; ");
682
- }
683
-
684
- export function serializeDomEditTextFields(fields: DomEditTextField[]): string {
685
- return fields
686
- .filter((field) => field.source === "child")
687
- .map((field) => {
688
- const attrs = [
689
- ...field.attributes.filter((attribute) => attribute.name !== "data-hf-text-key"),
690
- { name: "data-hf-text-key", value: field.key },
691
- ]
692
- .map((attribute) => ` ${attribute.name}="${attribute.value.replace(/"/g, "&quot;")}"`)
693
- .join("");
694
- const style = serializeTextFieldStyle(field);
695
- const styleAttr = style ? ` style="${style.replace(/"/g, "&quot;")}"` : "";
696
- return `<${field.tagName}${attrs}${styleAttr}>${escapeHtmlText(field.value)}</${field.tagName}>`;
697
- })
698
- .join("");
699
- }
700
-
701
- export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>): DomEditTextField {
702
- return {
703
- key: `child:new:${Date.now()}`,
704
- label: "Text",
705
- value: "New text",
706
- tagName: "span",
707
- attributes: [],
708
- inlineStyles: {
709
- "font-family": base?.computedStyles?.["font-family"] ?? "inherit",
710
- "font-size": base?.computedStyles?.["font-size"] ?? "16px",
711
- "font-weight": base?.computedStyles?.["font-weight"] ?? "400",
712
- color: base?.computedStyles?.color ?? "inherit",
713
- },
714
- computedStyles: {},
715
- source: "child",
716
- };
717
- }
718
-
719
- export function resolveDomEditCapabilities(args: {
720
- selector?: string;
721
- tagName?: string;
722
- className?: string;
723
- inlineStyles: Record<string, string>;
724
- computedStyles: Record<string, string>;
725
- isCompositionHost: boolean;
726
- isMasterView: boolean;
727
- }): DomEditCapabilities {
728
- if (!args.selector) {
729
- return {
730
- canSelect: false,
731
- canEditStyles: false,
732
- canMove: false,
733
- canResize: false,
734
- canApplyManualOffset: false,
735
- canApplyManualSize: false,
736
- canApplyManualRotation: false,
737
- reasonIfDisabled: "Studio could not resolve a stable patch target for this element.",
738
- };
739
- }
740
-
741
- const position = args.computedStyles.position;
742
- const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
743
- const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
744
- const width = parsePx(args.inlineStyles.width) ?? parsePx(args.computedStyles.width);
745
- const height = parsePx(args.inlineStyles.height) ?? parsePx(args.computedStyles.height);
746
- const hasTransformDrivenGeometry = !isIdentityTransform(args.computedStyles.transform);
747
-
748
- const canMove =
749
- (position === "absolute" || position === "fixed") &&
750
- left != null &&
751
- top != null &&
752
- !hasTransformDrivenGeometry;
753
-
754
- const canResize = canMove && (width != null || height != null);
755
- const canApplyManualGeometry = !args.isCompositionHost;
756
- const canApplyManualOffset = canApplyManualGeometry;
757
- const canApplyManualSize = canApplyManualGeometry;
758
- const canApplyManualRotation = canApplyManualGeometry;
759
- const reasonIfDisabled = canApplyManualGeometry
760
- ? undefined
761
- : "Select an internal layer to transform it.";
762
-
763
- if (args.isCompositionHost && args.isMasterView) {
764
- return {
765
- canSelect: true,
766
- canEditStyles: false,
767
- canMove,
768
- canResize,
769
- canApplyManualOffset,
770
- canApplyManualSize,
771
- canApplyManualRotation,
772
- reasonIfDisabled,
773
- };
774
- }
775
-
776
- return {
777
- canSelect: true,
778
- canEditStyles: true,
779
- canMove,
780
- canResize,
781
- canApplyManualOffset,
782
- canApplyManualSize,
783
- canApplyManualRotation,
784
- reasonIfDisabled,
785
- };
786
- }
787
-
788
- export function resolveDomEditSelection(
789
- startEl: HTMLElement | null,
790
- options: DomEditContextOptions,
791
- ): DomEditSelection | null {
792
- if (!startEl) return null;
793
- const doc = startEl.ownerDocument;
794
-
795
- let current: HTMLElement | null = getSelectionCandidate(startEl, options);
796
- while (current && current !== doc.body && current !== doc.documentElement) {
797
- const selector = buildStableSelector(current);
798
- if (!selector) {
799
- current = current.parentElement;
800
- continue;
801
- }
802
-
803
- const { sourceFile, compositionPath } = getSourceFileForElement(
804
- current,
805
- options.activeCompositionPath,
806
- );
807
- const selectorIndex = getSelectorIndex(
808
- doc,
809
- current,
810
- selector,
811
- sourceFile,
812
- options.activeCompositionPath,
813
- );
814
- const compositionSrc =
815
- current.getAttribute("data-composition-src") ??
816
- current.getAttribute("data-composition-file") ??
817
- undefined;
818
- const inlineStyles = getInlineStyles(current);
819
- const computedStyles = getCuratedComputedStyles(current);
820
- const textFields = collectDomEditTextFields(current);
821
- const capabilities = resolveDomEditCapabilities({
822
- selector,
823
- tagName: current.tagName.toLowerCase(),
824
- className: current.className,
825
- inlineStyles,
826
- computedStyles,
827
- isCompositionHost: Boolean(compositionSrc),
828
- isMasterView: options.isMasterView,
829
- });
830
- const rect = current.getBoundingClientRect();
831
-
832
- return {
833
- element: current,
834
- id: current.id || undefined,
835
- selector,
836
- selectorIndex,
837
- sourceFile,
838
- compositionPath,
839
- compositionSrc,
840
- isCompositionHost: Boolean(compositionSrc),
841
- label: buildElementLabel(current),
842
- tagName: current.tagName.toLowerCase(),
843
- boundingBox: {
844
- x: rect.left,
845
- y: rect.top,
846
- width: rect.width,
847
- height: rect.height,
848
- },
849
- textContent: current.textContent?.trim() || null,
850
- dataAttributes: getDataAttributes(current),
851
- inlineStyles,
852
- computedStyles,
853
- textFields,
854
- capabilities,
855
- };
856
- }
857
-
858
- return null;
859
- }
860
-
861
- export function refreshDomEditSelection(
862
- selection: DomEditSelection,
863
- activeCompositionPath: string | null,
864
- ): DomEditSelection | null {
865
- const doc = selection.element.ownerDocument;
866
- const nextElement = findElementForSelection(doc, selection, activeCompositionPath);
867
- return nextElement
868
- ? resolveDomEditSelection(nextElement, {
869
- activeCompositionPath,
870
- isMasterView: !activeCompositionPath || activeCompositionPath === "index.html",
871
- })
872
- : null;
873
- }
874
-
875
- export function getDomEditTargetKey(
876
- selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
877
- ): string {
878
- return [
879
- selection.sourceFile || "index.html",
880
- selection.id ?? "",
881
- selection.selector ?? "",
882
- selection.selectorIndex ?? "",
883
- ].join("|");
884
- }
885
-
886
- function hasSupportedDirectEdit(capabilities: DomEditCapabilities): boolean {
887
- return (
888
- capabilities.canEditStyles ||
889
- capabilities.canMove ||
890
- capabilities.canResize ||
891
- capabilities.canApplyManualOffset ||
892
- capabilities.canApplyManualSize ||
893
- capabilities.canApplyManualRotation
894
- );
895
- }
896
-
897
- export function getDomEditNonEditableReason(
898
- element: HTMLElement,
899
- selection: DomEditSelection | null,
900
- ): string | null {
901
- if (!selection) {
902
- return "No stable source target";
903
- }
904
-
905
- if (selection.element !== element) {
906
- return selection.isCompositionHost
907
- ? "Nested composition boundary"
908
- : `Selection resolves to ${selection.label}`;
909
- }
910
-
911
- if (!hasSupportedDirectEdit(selection.capabilities)) {
912
- return selection.capabilities.reasonIfDisabled ?? "No supported direct edits";
913
- }
914
-
915
- return null;
916
- }
917
-
918
- export function findElementForSelection(
919
- doc: Document,
920
- selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
921
- activeCompositionPath: string | null = null,
922
- ): HTMLElement | null {
923
- if (selection.id) {
924
- const byId = doc.getElementById(selection.id);
925
- if (
926
- isHtmlElement(byId) &&
927
- (!selection.sourceFile ||
928
- getSourceFileForElement(byId, activeCompositionPath).sourceFile === selection.sourceFile)
929
- ) {
930
- return byId;
931
- }
932
- }
933
-
934
- if (!selection.selector) return null;
935
-
936
- if (selection.selector.startsWith(".") && selection.selectorIndex != null) {
937
- const matches = querySelectorAllSafely(doc, selection.selector).filter(
938
- (candidate): candidate is HTMLElement =>
939
- isHtmlElement(candidate) &&
940
- (!selection.sourceFile ||
941
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
942
- selection.sourceFile),
943
- );
944
- return matches[selection.selectorIndex] ?? null;
945
- }
946
-
947
- const matches = querySelectorAllSafely(doc, selection.selector).filter(
948
- (candidate): candidate is HTMLElement =>
949
- isHtmlElement(candidate) &&
950
- (!selection.sourceFile ||
951
- getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
952
- selection.sourceFile),
953
- );
954
- return matches[0] ?? null;
955
- }
956
-
957
- export function findElementForTimelineElement(
958
- doc: Document,
959
- element: TimelineElementDomTarget,
960
- options: TimelineElementDomTargetOptions,
961
- ): HTMLElement | null {
962
- const elementId = typeof element.id === "string" ? element.id : "";
963
- const compositionSource =
964
- normalizeTimelineCompositionSource(element.compositionSrc) ??
965
- options.compIdToSrc?.get(elementId);
966
- const sourceFile =
967
- compositionSource ??
968
- normalizeTimelineCompositionSource(element.sourceFile) ??
969
- options.activeCompositionPath ??
970
- "index.html";
971
- const escapedElementId = escapeCssString(elementId);
972
- const escapedCompositionSource = compositionSource ? escapeCssString(compositionSource) : null;
973
- const selector =
974
- element.selector ??
975
- (compositionSource
976
- ? `[data-composition-src="${escapedCompositionSource}"],[data-composition-file="${escapedCompositionSource}"],[data-composition-id="${escapedElementId}"]`
977
- : escapedElementId
978
- ? `[data-composition-id="${escapedElementId}"]`
979
- : undefined);
980
-
981
- if (selector || element.domId) {
982
- const targetElement = findElementForSelection(
983
- doc,
984
- {
985
- id: element.domId ?? undefined,
986
- selector,
987
- selectorIndex: element.selectorIndex,
988
- sourceFile,
989
- },
990
- options.activeCompositionPath,
991
- );
992
- if (targetElement) return targetElement;
993
- }
994
-
995
- const hasExplicitDomTarget = Boolean(element.domId || element.selector || compositionSource);
996
- if (options.isMasterView || hasExplicitDomTarget || !options.activeCompositionPath) {
997
- return null;
998
- }
999
-
1000
- const root = doc.querySelector("[data-composition-id]");
1001
- if (!isHtmlElement(root)) return null;
1002
- return getSourceFileForElement(root, options.activeCompositionPath).sourceFile === sourceFile
1003
- ? root
1004
- : null;
1005
- }
1006
-
1007
- export function buildDomEditStylePatchOperation(property: string, value: string): PatchOperation {
1008
- return {
1009
- type: "inline-style",
1010
- property,
1011
- value,
1012
- };
1013
- }
1014
-
1015
- export function buildDomEditTextPatchOperation(value: string): PatchOperation {
1016
- return {
1017
- type: "text-content",
1018
- property: "text",
1019
- value,
1020
- };
1021
- }
1022
-
1023
- function formatBoundingBox(bounds: DomEditSelection["boundingBox"]): string {
1024
- return `x=${Math.round(bounds.x)}, y=${Math.round(bounds.y)}, width=${Math.round(bounds.width)}, height=${Math.round(bounds.height)}`;
1025
- }
1026
-
1027
- function formatStyleBlock(styles: Record<string, string>): string {
1028
- return Object.entries(styles)
1029
- .filter(([, value]) => value && value !== "initial")
1030
- .map(([key, value]) => `${key}: ${value}`)
1031
- .join("\n");
1032
- }
1033
-
1034
- function formatTextFields(fields: DomEditTextField[]): string {
1035
- return fields
1036
- .map(
1037
- (field) =>
1038
- `- key=${field.key}; tag=<${field.tagName}>; source=${field.source}; text=${JSON.stringify(field.value)}`,
1039
- )
1040
- .join("\n");
1041
- }
1042
-
1043
- export function buildElementAgentPrompt({
1044
- selection,
1045
- currentTime,
1046
- tagSnippet,
1047
- selectionContext,
1048
- userInstruction,
1049
- sourceFilePath,
1050
- }: {
1051
- selection: DomEditSelection;
1052
- currentTime: number;
1053
- tagSnippet?: string;
1054
- selectionContext?: string;
1055
- userInstruction?: string;
1056
- sourceFilePath?: string;
1057
- }): string {
1058
- const displayedSourceFile = sourceFilePath?.trim() || selection.sourceFile;
1059
- const lines = [
1060
- "## HyperFrames element edit request v1",
1061
- "Schema version: 1",
1062
- "",
1063
- userInstruction?.trim() || "Edit this selected HyperFrames element.",
1064
- "",
1065
- `Composition: ${selection.compositionPath}`,
1066
- `Playback time: ${formatTime(currentTime)}`,
1067
- `Source file: ${displayedSourceFile}`,
1068
- `DOM id: ${selection.id ?? "(none)"}`,
1069
- `Selector: ${selection.selector ?? "(none)"}`,
1070
- `Selector index: ${selection.selectorIndex ?? 0}`,
1071
- `Tag: <${selection.tagName}>`,
1072
- `Bounds: ${formatBoundingBox(selection.boundingBox)}`,
1073
- ];
1074
-
1075
- if (selection.textContent) {
1076
- lines.push(`Text: ${selection.textContent}`);
1077
- }
1078
-
1079
- const trimmedSelectionContext = selectionContext?.trim();
1080
- if (trimmedSelectionContext) {
1081
- lines.push("", "Selection context:", trimmedSelectionContext);
1082
- }
1083
-
1084
- const textFieldsBlock = formatTextFields(selection.textFields);
1085
- if (textFieldsBlock) {
1086
- lines.push("", "Text fields:", textFieldsBlock);
1087
- }
1088
-
1089
- const inlineStyleBlock = formatStyleBlock(selection.inlineStyles);
1090
- if (inlineStyleBlock) {
1091
- lines.push("", "Inline styles:", inlineStyleBlock);
1092
- }
1093
-
1094
- const computedStyleBlock = formatStyleBlock(selection.computedStyles);
1095
- if (computedStyleBlock) {
1096
- lines.push("", "Computed styles (browser-resolved):", computedStyleBlock);
1097
- }
1098
-
1099
- if (tagSnippet) {
1100
- lines.push("", "Target HTML:", tagSnippet);
1101
- }
1102
-
1103
- lines.push(
1104
- "",
1105
- "Guardrails:",
1106
- "- Make a targeted change to this element only.",
1107
- "- Preserve the rest of the composition and its timing.",
1108
- "- Do not modify other elements' data-* attributes or positioning.",
1109
- "- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.",
1110
- );
1111
-
1112
- return lines.join("\n");
1113
- }
1114
-
1115
- export function isTextEditableSelection(selection: DomEditSelection): boolean {
1116
- return selection.textFields.length > 0 && !selection.isCompositionHost;
1117
- }
1
+ /**
2
+ * Public API for dom editing — re-exports from focused sub-modules.
3
+ * Import from this file to avoid breaking existing import paths.
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ DomEditCapabilities,
9
+ DomEditContextOptions,
10
+ DomEditLayerItem,
11
+ DomEditSelection,
12
+ DomEditTextField,
13
+ DomEditViewport,
14
+ TimelineElementDomTarget,
15
+ TimelineElementDomTargetOptions,
16
+ } from "./domEditingTypes";
17
+
18
+ // Element finders, visibility, visual scoring, raster detection
19
+ export {
20
+ findElementForSelection,
21
+ findElementForTimelineElement,
22
+ isLargeRasterDomEditSelection,
23
+ resolveVisualDomEditSelectionTarget,
24
+ } from "./domEditingElement";
25
+
26
+ // Layers, text fields, capabilities, selection, patch ops
27
+ export {
28
+ buildDefaultDomEditTextField,
29
+ buildDomEditStylePatchOperation,
30
+ buildDomEditTextPatchOperation,
31
+ collectDomEditLayerItems,
32
+ countDomEditChildLayers,
33
+ getDomEditLayerKey,
34
+ getDomEditNonEditableReason,
35
+ getDomEditTargetKey,
36
+ isTextEditableSelection,
37
+ refreshDomEditSelection,
38
+ resolveDomEditCapabilities,
39
+ resolveDomEditSelection,
40
+ serializeDomEditTextFields,
41
+ } from "./domEditingLayers";
42
+
43
+ // Agent prompt
44
+ export { buildElementAgentPrompt } from "./domEditingAgentPrompt";