@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.
- package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +421 -4303
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +167 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +88 -993
- package/src/components/editor/EaseCurveEditor.tsx +221 -0
- package/src/components/editor/FileTree.tsx +13 -621
- package/src/components/editor/FileTreeIcons.tsx +128 -0
- package/src/components/editor/FileTreeNodes.tsx +496 -0
- package/src/components/editor/MotionPanel.tsx +16 -390
- package/src/components/editor/MotionPanelFields.tsx +185 -0
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditOverlayGeometry.ts +211 -0
- package/src/components/editor/domEditOverlayGestures.ts +138 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
- package/src/components/editor/domEditing.ts +44 -1117
- package/src/components/editor/domEditingAgentPrompt.ts +97 -0
- package/src/components/editor/domEditingDom.ts +266 -0
- package/src/components/editor/domEditingElement.ts +329 -0
- package/src/components/editor/domEditingLayers.ts +460 -0
- package/src/components/editor/domEditingTypes.ts +125 -0
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +84 -1049
- package/src/components/editor/manualEditsDom.ts +436 -0
- package/src/components/editor/manualEditsParsing.ts +280 -0
- package/src/components/editor/manualEditsSnapshot.ts +333 -0
- package/src/components/editor/manualEditsTypes.ts +141 -0
- package/src/components/editor/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
- package/src/components/editor/studioMotion.ts +47 -434
- package/src/components/editor/studioMotionOps.ts +299 -0
- package/src/components/editor/studioMotionTypes.ts +168 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
- package/src/components/editor/useDomEditOverlayRects.ts +207 -0
- package/src/components/nle/NLELayout.tsx +68 -155
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/nle/useCompositionStack.ts +126 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/hooks/useToast.ts +20 -0
- package/src/player/components/Player.tsx +33 -2
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +196 -1518
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/components/TimelineEmptyState.tsx +102 -0
- package/src/player/components/TimelineRuler.tsx +90 -0
- package/src/player/components/timelineIcons.tsx +49 -0
- package/src/player/components/timelineLayout.ts +215 -0
- package/src/player/components/timelineUtils.ts +211 -0
- package/src/player/components/useTimelineClipDrag.ts +388 -0
- package/src/player/components/useTimelinePlayhead.ts +200 -0
- package/src/player/components/useTimelineRangeSelection.ts +135 -0
- package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
- package/src/player/hooks/useTimelinePlayer.ts +105 -1371
- package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
- package/src/player/lib/playbackAdapter.ts +145 -0
- package/src/player/lib/playbackShortcuts.ts +68 -0
- package/src/player/lib/playbackTypes.ts +60 -0
- package/src/player/lib/timelineDOM.ts +373 -0
- package/src/player/lib/timelineElementHelpers.ts +303 -0
- package/src/player/lib/timelineIframeHelpers.ts +269 -0
- package/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-DYCiFGWQ.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer items, text fields, capabilities, selection resolution, and patch operations
|
|
3
|
+
* for dom editing.
|
|
4
|
+
*/
|
|
5
|
+
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
6
|
+
import type {
|
|
7
|
+
DomEditCapabilities,
|
|
8
|
+
DomEditContextOptions,
|
|
9
|
+
DomEditLayerItem,
|
|
10
|
+
DomEditSelection,
|
|
11
|
+
DomEditTextField,
|
|
12
|
+
} from "./domEditingTypes";
|
|
13
|
+
import {
|
|
14
|
+
buildStableSelector,
|
|
15
|
+
getCuratedComputedStyles,
|
|
16
|
+
getDataAttributes,
|
|
17
|
+
getInlineStyles,
|
|
18
|
+
getPreferredClassSelector,
|
|
19
|
+
getSelectorIndex,
|
|
20
|
+
getSourceFileForElement,
|
|
21
|
+
humanizeIdentifier,
|
|
22
|
+
isHtmlElement,
|
|
23
|
+
isIdentityTransform,
|
|
24
|
+
isTextBearingTag,
|
|
25
|
+
parsePx,
|
|
26
|
+
} from "./domEditingDom";
|
|
27
|
+
import {
|
|
28
|
+
findElementForSelection,
|
|
29
|
+
getDomLayerPatchTarget,
|
|
30
|
+
getDirectLayerChildren,
|
|
31
|
+
getSelectionCandidate,
|
|
32
|
+
} from "./domEditingElement";
|
|
33
|
+
|
|
34
|
+
// ─── Text fields ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export function isEditableTextLeaf(el: HTMLElement): boolean {
|
|
37
|
+
return isTextBearingTag(el.tagName.toLowerCase()) && el.children.length === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getTextFieldLabel(
|
|
41
|
+
_tagName: string,
|
|
42
|
+
index: number,
|
|
43
|
+
total: number,
|
|
44
|
+
source: "self" | "child",
|
|
45
|
+
): string {
|
|
46
|
+
if (source === "self" || total === 1) return "Content";
|
|
47
|
+
return `Text ${index + 1}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildTextField(
|
|
51
|
+
el: HTMLElement,
|
|
52
|
+
index: number,
|
|
53
|
+
total: number,
|
|
54
|
+
source: "self" | "child",
|
|
55
|
+
): DomEditTextField {
|
|
56
|
+
const tagName = el.tagName.toLowerCase();
|
|
57
|
+
const key = el.getAttribute("data-hf-text-key") ?? `${source}:${index}:${tagName}`;
|
|
58
|
+
return {
|
|
59
|
+
key,
|
|
60
|
+
label: getTextFieldLabel(tagName, index, total, source),
|
|
61
|
+
value: el.textContent ?? "",
|
|
62
|
+
tagName,
|
|
63
|
+
attributes: Array.from(el.attributes)
|
|
64
|
+
.filter((attribute) => attribute.name !== "style")
|
|
65
|
+
.map((attribute) => ({
|
|
66
|
+
name: attribute.name,
|
|
67
|
+
value: attribute.value,
|
|
68
|
+
})),
|
|
69
|
+
inlineStyles: getInlineStyles(el),
|
|
70
|
+
computedStyles: getCuratedComputedStyles(el),
|
|
71
|
+
source,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function collectDomEditTextFields(el: HTMLElement): DomEditTextField[] {
|
|
76
|
+
const childFields = Array.from(el.children).filter(isHtmlElement).filter(isEditableTextLeaf);
|
|
77
|
+
if (childFields.length > 0) {
|
|
78
|
+
return childFields.map((child, index) =>
|
|
79
|
+
buildTextField(child, index, childFields.length, "child"),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isEditableTextLeaf(el)) {
|
|
84
|
+
return [buildTextField(el, 0, 1, "self")];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function escapeHtmlText(value: string): string {
|
|
91
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function serializeTextFieldStyle(field: DomEditTextField): string {
|
|
95
|
+
const entries = Object.entries(field.inlineStyles).filter(([, value]) => Boolean(value));
|
|
96
|
+
if (entries.length === 0) return "";
|
|
97
|
+
return entries.map(([key, value]) => `${key}: ${value}`).join("; ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function serializeDomEditTextFields(fields: DomEditTextField[]): string {
|
|
101
|
+
return fields
|
|
102
|
+
.filter((field) => field.source === "child")
|
|
103
|
+
.map((field) => {
|
|
104
|
+
const attrs = [
|
|
105
|
+
...field.attributes.filter((attribute) => attribute.name !== "data-hf-text-key"),
|
|
106
|
+
{ name: "data-hf-text-key", value: field.key },
|
|
107
|
+
]
|
|
108
|
+
.map((attribute) => ` ${attribute.name}="${attribute.value.replace(/"/g, """)}"`)
|
|
109
|
+
.join("");
|
|
110
|
+
const style = serializeTextFieldStyle(field);
|
|
111
|
+
const styleAttr = style ? ` style="${style.replace(/"/g, """)}"` : "";
|
|
112
|
+
return `<${field.tagName}${attrs}${styleAttr}>${escapeHtmlText(field.value)}</${field.tagName}>`;
|
|
113
|
+
})
|
|
114
|
+
.join("");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>): DomEditTextField {
|
|
118
|
+
return {
|
|
119
|
+
key: `child:new:${Date.now()}`,
|
|
120
|
+
label: "Text",
|
|
121
|
+
value: "New text",
|
|
122
|
+
tagName: "span",
|
|
123
|
+
attributes: [],
|
|
124
|
+
inlineStyles: {
|
|
125
|
+
"font-family": base?.computedStyles?.["font-family"] ?? "inherit",
|
|
126
|
+
"font-size": base?.computedStyles?.["font-size"] ?? "16px",
|
|
127
|
+
"font-weight": base?.computedStyles?.["font-weight"] ?? "400",
|
|
128
|
+
color: base?.computedStyles?.color ?? "inherit",
|
|
129
|
+
},
|
|
130
|
+
computedStyles: {},
|
|
131
|
+
source: "child",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Capabilities ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function resolveDomEditCapabilities(args: {
|
|
138
|
+
selector?: string;
|
|
139
|
+
tagName?: string;
|
|
140
|
+
className?: string;
|
|
141
|
+
inlineStyles: Record<string, string>;
|
|
142
|
+
computedStyles: Record<string, string>;
|
|
143
|
+
isCompositionHost: boolean;
|
|
144
|
+
isMasterView: boolean;
|
|
145
|
+
}): DomEditCapabilities {
|
|
146
|
+
if (!args.selector) {
|
|
147
|
+
return {
|
|
148
|
+
canSelect: false,
|
|
149
|
+
canEditStyles: false,
|
|
150
|
+
canMove: false,
|
|
151
|
+
canResize: false,
|
|
152
|
+
canApplyManualOffset: false,
|
|
153
|
+
canApplyManualSize: false,
|
|
154
|
+
canApplyManualRotation: false,
|
|
155
|
+
reasonIfDisabled: "Studio could not resolve a stable patch target for this element.",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const position = args.computedStyles.position;
|
|
160
|
+
const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
|
|
161
|
+
const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
|
|
162
|
+
const width = parsePx(args.inlineStyles.width) ?? parsePx(args.computedStyles.width);
|
|
163
|
+
const height = parsePx(args.inlineStyles.height) ?? parsePx(args.computedStyles.height);
|
|
164
|
+
const hasTransformDrivenGeometry = !isIdentityTransform(args.computedStyles.transform);
|
|
165
|
+
|
|
166
|
+
const canMove =
|
|
167
|
+
(position === "absolute" || position === "fixed") &&
|
|
168
|
+
left != null &&
|
|
169
|
+
top != null &&
|
|
170
|
+
!hasTransformDrivenGeometry;
|
|
171
|
+
|
|
172
|
+
const canResize = canMove && (width != null || height != null);
|
|
173
|
+
const canApplyManualGeometry = !args.isCompositionHost;
|
|
174
|
+
const canApplyManualOffset = canApplyManualGeometry;
|
|
175
|
+
const canApplyManualSize = canApplyManualGeometry;
|
|
176
|
+
const canApplyManualRotation = canApplyManualGeometry;
|
|
177
|
+
const reasonIfDisabled = canApplyManualGeometry
|
|
178
|
+
? undefined
|
|
179
|
+
: "Select an internal layer to transform it.";
|
|
180
|
+
|
|
181
|
+
if (args.isCompositionHost && args.isMasterView) {
|
|
182
|
+
return {
|
|
183
|
+
canSelect: true,
|
|
184
|
+
canEditStyles: false,
|
|
185
|
+
canMove,
|
|
186
|
+
canResize,
|
|
187
|
+
canApplyManualOffset,
|
|
188
|
+
canApplyManualSize,
|
|
189
|
+
canApplyManualRotation,
|
|
190
|
+
reasonIfDisabled,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
canSelect: true,
|
|
196
|
+
canEditStyles: true,
|
|
197
|
+
canMove,
|
|
198
|
+
canResize,
|
|
199
|
+
canApplyManualOffset,
|
|
200
|
+
canApplyManualSize,
|
|
201
|
+
canApplyManualRotation,
|
|
202
|
+
reasonIfDisabled,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Element label ────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
export function buildElementLabel(el: HTMLElement): string {
|
|
209
|
+
const compositionId = el.getAttribute("data-composition-id");
|
|
210
|
+
if (compositionId && compositionId !== "main") {
|
|
211
|
+
return humanizeIdentifier(compositionId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const compositionSrc =
|
|
215
|
+
el.getAttribute("data-composition-src") ?? el.getAttribute("data-composition-file");
|
|
216
|
+
if (compositionSrc) {
|
|
217
|
+
return humanizeIdentifier(compositionSrc);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (el.id) return humanizeIdentifier(el.id);
|
|
221
|
+
|
|
222
|
+
const preferredClass = getPreferredClassSelector(el);
|
|
223
|
+
if (preferredClass) {
|
|
224
|
+
return humanizeIdentifier(preferredClass.replace(/^\./, ""));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
|
|
228
|
+
if (text) return text.length > 40 ? `${text.slice(0, 39)}…` : text;
|
|
229
|
+
return el.tagName.toLowerCase();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Selection resolution ────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
export function resolveDomEditSelection(
|
|
235
|
+
startEl: HTMLElement | null,
|
|
236
|
+
options: DomEditContextOptions,
|
|
237
|
+
): DomEditSelection | null {
|
|
238
|
+
if (!startEl) return null;
|
|
239
|
+
const doc = startEl.ownerDocument;
|
|
240
|
+
|
|
241
|
+
let current: HTMLElement | null = getSelectionCandidate(startEl, options);
|
|
242
|
+
while (current && current !== doc.body && current !== doc.documentElement) {
|
|
243
|
+
const selector = buildStableSelector(current);
|
|
244
|
+
if (!selector) {
|
|
245
|
+
current = current.parentElement;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { sourceFile, compositionPath } = getSourceFileForElement(
|
|
250
|
+
current,
|
|
251
|
+
options.activeCompositionPath,
|
|
252
|
+
);
|
|
253
|
+
const selectorIndex = getSelectorIndex(
|
|
254
|
+
doc,
|
|
255
|
+
current,
|
|
256
|
+
selector,
|
|
257
|
+
sourceFile,
|
|
258
|
+
options.activeCompositionPath,
|
|
259
|
+
);
|
|
260
|
+
const compositionSrc =
|
|
261
|
+
current.getAttribute("data-composition-src") ??
|
|
262
|
+
current.getAttribute("data-composition-file") ??
|
|
263
|
+
undefined;
|
|
264
|
+
const inlineStyles = getInlineStyles(current);
|
|
265
|
+
const computedStyles = getCuratedComputedStyles(current);
|
|
266
|
+
const textFields = collectDomEditTextFields(current);
|
|
267
|
+
const capabilities = resolveDomEditCapabilities({
|
|
268
|
+
selector,
|
|
269
|
+
tagName: current.tagName.toLowerCase(),
|
|
270
|
+
className: current.className,
|
|
271
|
+
inlineStyles,
|
|
272
|
+
computedStyles,
|
|
273
|
+
isCompositionHost: Boolean(compositionSrc),
|
|
274
|
+
isMasterView: options.isMasterView,
|
|
275
|
+
});
|
|
276
|
+
const rect = current.getBoundingClientRect();
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
element: current,
|
|
280
|
+
id: current.id || undefined,
|
|
281
|
+
selector,
|
|
282
|
+
selectorIndex,
|
|
283
|
+
sourceFile,
|
|
284
|
+
compositionPath,
|
|
285
|
+
compositionSrc,
|
|
286
|
+
isCompositionHost: Boolean(compositionSrc),
|
|
287
|
+
label: buildElementLabel(current),
|
|
288
|
+
tagName: current.tagName.toLowerCase(),
|
|
289
|
+
boundingBox: {
|
|
290
|
+
x: rect.left,
|
|
291
|
+
y: rect.top,
|
|
292
|
+
width: rect.width,
|
|
293
|
+
height: rect.height,
|
|
294
|
+
},
|
|
295
|
+
textContent: current.textContent?.trim() || null,
|
|
296
|
+
dataAttributes: getDataAttributes(current),
|
|
297
|
+
inlineStyles,
|
|
298
|
+
computedStyles,
|
|
299
|
+
textFields,
|
|
300
|
+
capabilities,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function refreshDomEditSelection(
|
|
308
|
+
selection: DomEditSelection,
|
|
309
|
+
activeCompositionPath: string | null,
|
|
310
|
+
): DomEditSelection | null {
|
|
311
|
+
const doc = selection.element.ownerDocument;
|
|
312
|
+
const nextElement = findElementForSelection(doc, selection, activeCompositionPath);
|
|
313
|
+
return nextElement
|
|
314
|
+
? resolveDomEditSelection(nextElement, {
|
|
315
|
+
activeCompositionPath,
|
|
316
|
+
isMasterView: !activeCompositionPath || activeCompositionPath === "index.html",
|
|
317
|
+
})
|
|
318
|
+
: null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Layer items ─────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
export function getDomEditLayerKey(
|
|
324
|
+
target: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
|
325
|
+
): string {
|
|
326
|
+
const selectorIndex = target.selectorIndex ?? 0;
|
|
327
|
+
return `${target.sourceFile}:${target.id ?? target.selector ?? "layer"}:${selectorIndex}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function countDomEditChildLayers(
|
|
331
|
+
root: HTMLElement | null | undefined,
|
|
332
|
+
options: DomEditContextOptions,
|
|
333
|
+
maxCount = 99,
|
|
334
|
+
): number {
|
|
335
|
+
if (!root) return 0;
|
|
336
|
+
|
|
337
|
+
let count = 0;
|
|
338
|
+
const visit = (el: HTMLElement) => {
|
|
339
|
+
for (const child of Array.from(el.children)) {
|
|
340
|
+
if (!isHtmlElement(child)) continue;
|
|
341
|
+
if (getDomLayerPatchTarget(child, options.activeCompositionPath)) {
|
|
342
|
+
count += 1;
|
|
343
|
+
if (count >= maxCount) return;
|
|
344
|
+
}
|
|
345
|
+
visit(child);
|
|
346
|
+
if (count >= maxCount) return;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
visit(root);
|
|
351
|
+
return count;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function collectDomEditLayerItems(
|
|
355
|
+
root: HTMLElement | null | undefined,
|
|
356
|
+
options: DomEditContextOptions,
|
|
357
|
+
maxItems = 80,
|
|
358
|
+
): DomEditLayerItem[] {
|
|
359
|
+
if (!root) return [];
|
|
360
|
+
|
|
361
|
+
const items: DomEditLayerItem[] = [];
|
|
362
|
+
const visit = (el: HTMLElement, depth: number) => {
|
|
363
|
+
if (items.length >= maxItems) return;
|
|
364
|
+
|
|
365
|
+
const target = getDomLayerPatchTarget(el, options.activeCompositionPath);
|
|
366
|
+
if (target) {
|
|
367
|
+
items.push({
|
|
368
|
+
key: getDomEditLayerKey(target),
|
|
369
|
+
element: el,
|
|
370
|
+
label: buildElementLabel(el),
|
|
371
|
+
tagName: el.tagName.toLowerCase(),
|
|
372
|
+
depth,
|
|
373
|
+
childCount: getDirectLayerChildren(el, options).length,
|
|
374
|
+
id: target.id ?? undefined,
|
|
375
|
+
selector: target.selector ?? undefined,
|
|
376
|
+
selectorIndex: target.selectorIndex,
|
|
377
|
+
sourceFile: target.sourceFile,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const nextDepth = target ? depth + 1 : depth;
|
|
382
|
+
for (const child of Array.from(el.children)) {
|
|
383
|
+
if (!isHtmlElement(child)) continue;
|
|
384
|
+
visit(child, nextDepth);
|
|
385
|
+
if (items.length >= maxItems) return;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
visit(root, 0);
|
|
390
|
+
return items;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Patch operations ────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
export function buildDomEditStylePatchOperation(property: string, value: string): PatchOperation {
|
|
396
|
+
return {
|
|
397
|
+
type: "inline-style",
|
|
398
|
+
property,
|
|
399
|
+
value,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function buildDomEditTextPatchOperation(value: string): PatchOperation {
|
|
404
|
+
return {
|
|
405
|
+
type: "text-content",
|
|
406
|
+
property: "text",
|
|
407
|
+
value,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── Non-editable reason ─────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
function hasSupportedDirectEdit(capabilities: DomEditCapabilities): boolean {
|
|
414
|
+
return (
|
|
415
|
+
capabilities.canEditStyles ||
|
|
416
|
+
capabilities.canMove ||
|
|
417
|
+
capabilities.canResize ||
|
|
418
|
+
capabilities.canApplyManualOffset ||
|
|
419
|
+
capabilities.canApplyManualSize ||
|
|
420
|
+
capabilities.canApplyManualRotation
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function getDomEditNonEditableReason(
|
|
425
|
+
element: HTMLElement,
|
|
426
|
+
selection: DomEditSelection | null,
|
|
427
|
+
): string | null {
|
|
428
|
+
if (!selection) {
|
|
429
|
+
return "No stable source target";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (selection.element !== element) {
|
|
433
|
+
return selection.isCompositionHost
|
|
434
|
+
? "Nested composition boundary"
|
|
435
|
+
: `Selection resolves to ${selection.label}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!hasSupportedDirectEdit(selection.capabilities)) {
|
|
439
|
+
return selection.capabilities.reasonIfDisabled ?? "No supported direct edits";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function getDomEditTargetKey(
|
|
446
|
+
selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
|
447
|
+
): string {
|
|
448
|
+
return [
|
|
449
|
+
selection.sourceFile || "index.html",
|
|
450
|
+
selection.id ?? "",
|
|
451
|
+
selection.selector ?? "",
|
|
452
|
+
selection.selectorIndex ?? "",
|
|
453
|
+
].join("|");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function isTextEditableSelection(selection: DomEditSelection): boolean {
|
|
457
|
+
return selection.textFields.length > 0 && !selection.isCompositionHost;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// buildElementAgentPrompt is in domEditingAgentPrompt.ts
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { PatchTarget } from "../../utils/sourcePatcher";
|
|
2
|
+
|
|
3
|
+
export const CURATED_STYLE_PROPERTIES = [
|
|
4
|
+
"position",
|
|
5
|
+
"display",
|
|
6
|
+
"top",
|
|
7
|
+
"left",
|
|
8
|
+
"right",
|
|
9
|
+
"bottom",
|
|
10
|
+
"inset",
|
|
11
|
+
"width",
|
|
12
|
+
"height",
|
|
13
|
+
"gap",
|
|
14
|
+
"justify-content",
|
|
15
|
+
"align-items",
|
|
16
|
+
"flex-direction",
|
|
17
|
+
"font-size",
|
|
18
|
+
"font-style",
|
|
19
|
+
"font-weight",
|
|
20
|
+
"font-family",
|
|
21
|
+
"line-height",
|
|
22
|
+
"letter-spacing",
|
|
23
|
+
"text-align",
|
|
24
|
+
"text-transform",
|
|
25
|
+
"color",
|
|
26
|
+
"background-color",
|
|
27
|
+
"background-image",
|
|
28
|
+
"opacity",
|
|
29
|
+
"mix-blend-mode",
|
|
30
|
+
"border-radius",
|
|
31
|
+
"border-width",
|
|
32
|
+
"border-style",
|
|
33
|
+
"border-color",
|
|
34
|
+
"border-top-width",
|
|
35
|
+
"border-top-style",
|
|
36
|
+
"border-top-color",
|
|
37
|
+
"outline-color",
|
|
38
|
+
"overflow",
|
|
39
|
+
"clip-path",
|
|
40
|
+
"box-shadow",
|
|
41
|
+
"filter",
|
|
42
|
+
"backdrop-filter",
|
|
43
|
+
"z-index",
|
|
44
|
+
"transform",
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export interface DomEditCapabilities {
|
|
48
|
+
canSelect: boolean;
|
|
49
|
+
canEditStyles: boolean;
|
|
50
|
+
/** Directly editable authored left/top style fields. Canvas drag uses manual edits instead. */
|
|
51
|
+
canMove: boolean;
|
|
52
|
+
/** Directly editable authored width/height style fields. Canvas resize uses manual edits instead. */
|
|
53
|
+
canResize: boolean;
|
|
54
|
+
canApplyManualOffset: boolean;
|
|
55
|
+
canApplyManualSize: boolean;
|
|
56
|
+
canApplyManualRotation: boolean;
|
|
57
|
+
reasonIfDisabled?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DomEditTextField {
|
|
61
|
+
key: string;
|
|
62
|
+
label: string;
|
|
63
|
+
value: string;
|
|
64
|
+
tagName: string;
|
|
65
|
+
attributes: Array<{ name: string; value: string }>;
|
|
66
|
+
inlineStyles: Record<string, string>;
|
|
67
|
+
computedStyles: Record<string, string>;
|
|
68
|
+
source: "self" | "child";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DomEditSelection extends PatchTarget {
|
|
72
|
+
element: HTMLElement;
|
|
73
|
+
label: string;
|
|
74
|
+
tagName: string;
|
|
75
|
+
sourceFile: string;
|
|
76
|
+
compositionPath: string;
|
|
77
|
+
compositionSrc?: string;
|
|
78
|
+
isCompositionHost: boolean;
|
|
79
|
+
boundingBox: { x: number; y: number; width: number; height: number };
|
|
80
|
+
textContent: string | null;
|
|
81
|
+
dataAttributes: Record<string, string>;
|
|
82
|
+
inlineStyles: Record<string, string>;
|
|
83
|
+
computedStyles: Record<string, string>;
|
|
84
|
+
textFields: DomEditTextField[];
|
|
85
|
+
capabilities: DomEditCapabilities;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DomEditLayerItem {
|
|
89
|
+
key: string;
|
|
90
|
+
element: HTMLElement;
|
|
91
|
+
label: string;
|
|
92
|
+
tagName: string;
|
|
93
|
+
depth: number;
|
|
94
|
+
childCount: number;
|
|
95
|
+
id?: string;
|
|
96
|
+
selector?: string;
|
|
97
|
+
selectorIndex?: number;
|
|
98
|
+
sourceFile: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DomEditContextOptions {
|
|
102
|
+
activeCompositionPath: string | null;
|
|
103
|
+
isMasterView: boolean;
|
|
104
|
+
preferClipAncestor?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DomEditViewport {
|
|
108
|
+
width: number;
|
|
109
|
+
height: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TimelineElementDomTarget {
|
|
113
|
+
id?: string;
|
|
114
|
+
domId?: string;
|
|
115
|
+
selector?: string;
|
|
116
|
+
selectorIndex?: number;
|
|
117
|
+
sourceFile?: string;
|
|
118
|
+
compositionSrc?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TimelineElementDomTargetOptions {
|
|
122
|
+
activeCompositionPath: string | null;
|
|
123
|
+
compIdToSrc?: ReadonlyMap<string, string>;
|
|
124
|
+
isMasterView: boolean;
|
|
125
|
+
}
|
|
@@ -16,10 +16,10 @@ describe("manual editing availability", () => {
|
|
|
16
16
|
vi.resetModules();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it("enables inspector selection by default while motion
|
|
19
|
+
it("enables inspector selection and manual dragging by default while motion stays opt-in", async () => {
|
|
20
20
|
const availability = await loadAvailabilityWithEnv({});
|
|
21
21
|
|
|
22
|
-
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(
|
|
22
|
+
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true);
|
|
23
23
|
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
|
|
24
24
|
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
25
25
|
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
|
@@ -32,7 +32,7 @@ const env = import.meta.env as StudioFeatureFlagEnv;
|
|
|
32
32
|
export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag(
|
|
33
33
|
env,
|
|
34
34
|
[STUDIO_PREVIEW_MANUAL_DRAGGING_ENV, "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED"],
|
|
35
|
-
|
|
35
|
+
true,
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|