@hyperframes/studio 0.6.0-alpha.1 → 0.6.0-alpha.11
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-DjsVzYFP.js +418 -0
- package/dist/assets/index-FWg79aJz.css +1 -0
- package/dist/assets/index-xyVaWqe2.js +108 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +422 -71
- package/src/components/editor/PropertyPanel.test.ts +49 -0
- package/src/components/editor/PropertyPanel.tsx +277 -337
- package/src/components/editor/domEditing.test.ts +248 -0
- package/src/components/editor/domEditing.ts +126 -2
- package/src/components/editor/manualEditingAvailability.test.ts +15 -4
- package/src/components/editor/manualEditingAvailability.ts +4 -2
- package/src/components/editor/manualEdits.ts +15 -3
- package/src/components/nle/NLELayout.test.ts +12 -0
- package/src/components/nle/NLELayout.tsx +63 -24
- package/src/components/nle/NLEPreview.tsx +6 -0
- package/src/components/renders/RenderQueue.tsx +56 -4
- package/src/components/renders/useRenderQueue.ts +30 -6
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/player/components/Player.test.ts +58 -0
- package/src/player/components/Player.tsx +71 -4
- package/src/player/components/PlayerControls.tsx +20 -7
- package/src/player/components/Timeline.tsx +45 -20
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-Cd8vYWxP.js +0 -198
- package/dist/assets/index-D04_ZoMm.js +0 -107
- package/dist/assets/index-UWFaHilT.css +0 -1
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
findElementForTimelineElement,
|
|
10
10
|
getDomEditNonEditableReason,
|
|
11
11
|
getDomEditTargetKey,
|
|
12
|
+
isLargeRasterDomEditSelection,
|
|
12
13
|
isTextEditableSelection,
|
|
14
|
+
resolveVisualDomEditSelectionTarget,
|
|
13
15
|
serializeDomEditTextFields,
|
|
14
16
|
type DomEditSelection,
|
|
15
17
|
resolveDomEditCapabilities,
|
|
@@ -23,6 +25,30 @@ function createDocument(markup: string): Document {
|
|
|
23
25
|
return window.document;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function setElementRect(
|
|
29
|
+
element: HTMLElement,
|
|
30
|
+
rect: Partial<Pick<DOMRect, "left" | "top" | "width" | "height">>,
|
|
31
|
+
) {
|
|
32
|
+
const left = rect.left ?? 0;
|
|
33
|
+
const top = rect.top ?? 0;
|
|
34
|
+
const width = rect.width ?? 100;
|
|
35
|
+
const height = rect.height ?? 40;
|
|
36
|
+
Object.defineProperty(element, "getBoundingClientRect", {
|
|
37
|
+
configurable: true,
|
|
38
|
+
value: () => ({
|
|
39
|
+
x: left,
|
|
40
|
+
y: top,
|
|
41
|
+
left,
|
|
42
|
+
top,
|
|
43
|
+
width,
|
|
44
|
+
height,
|
|
45
|
+
right: left + width,
|
|
46
|
+
bottom: top + height,
|
|
47
|
+
toJSON: () => null,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
describe("resolveDomEditCapabilities", () => {
|
|
27
53
|
it("marks absolute px-positioned layers as movable and resizable", () => {
|
|
28
54
|
expect(
|
|
@@ -199,6 +225,154 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
199
225
|
});
|
|
200
226
|
});
|
|
201
227
|
|
|
228
|
+
describe("resolveVisualDomEditSelectionTarget", () => {
|
|
229
|
+
it("prefers the visible leaf under the pointer over an oversized container", () => {
|
|
230
|
+
const document = createDocument(`
|
|
231
|
+
<section id="container" class="hero-shell">
|
|
232
|
+
<span id="headline" class="headline">Launch faster</span>
|
|
233
|
+
</section>
|
|
234
|
+
`);
|
|
235
|
+
const container = document.getElementById("container") as HTMLElement;
|
|
236
|
+
const headline = document.getElementById("headline") as HTMLElement;
|
|
237
|
+
setElementRect(container, { width: 900, height: 520 });
|
|
238
|
+
setElementRect(headline, { left: 240, top: 160, width: 180, height: 36 });
|
|
239
|
+
|
|
240
|
+
expect(
|
|
241
|
+
resolveVisualDomEditSelectionTarget([container, headline], {
|
|
242
|
+
activeCompositionPath: "index.html",
|
|
243
|
+
}),
|
|
244
|
+
).toBe(headline);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("skips hidden and zero-size elements before picking a rendered candidate", () => {
|
|
248
|
+
const document = createDocument(`
|
|
249
|
+
<div id="hidden" style="display: none">Hidden</div>
|
|
250
|
+
<div id="empty">Empty</div>
|
|
251
|
+
<div id="visible">Visible</div>
|
|
252
|
+
`);
|
|
253
|
+
const hidden = document.getElementById("hidden") as HTMLElement;
|
|
254
|
+
const empty = document.getElementById("empty") as HTMLElement;
|
|
255
|
+
const visible = document.getElementById("visible") as HTMLElement;
|
|
256
|
+
setElementRect(hidden, { width: 120, height: 32 });
|
|
257
|
+
setElementRect(empty, { width: 0, height: 0 });
|
|
258
|
+
setElementRect(visible, { width: 120, height: 32 });
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
resolveVisualDomEditSelectionTarget([hidden, empty, visible], {
|
|
262
|
+
activeCompositionPath: "index.html",
|
|
263
|
+
}),
|
|
264
|
+
).toBe(visible);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("skips transparent elements that still report a box", () => {
|
|
268
|
+
const document = createDocument(`
|
|
269
|
+
<button id="transparent" style="opacity: 0">Transparent</button>
|
|
270
|
+
<button id="visible">Visible</button>
|
|
271
|
+
`);
|
|
272
|
+
const transparent = document.getElementById("transparent") as HTMLElement;
|
|
273
|
+
const visible = document.getElementById("visible") as HTMLElement;
|
|
274
|
+
setElementRect(transparent, { width: 120, height: 32 });
|
|
275
|
+
setElementRect(visible, { width: 120, height: 32 });
|
|
276
|
+
|
|
277
|
+
expect(
|
|
278
|
+
resolveVisualDomEditSelectionTarget([transparent, visible], {
|
|
279
|
+
activeCompositionPath: "index.html",
|
|
280
|
+
}),
|
|
281
|
+
).toBe(visible);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("falls back to the nearest stable editable ancestor when a visual child has no target", () => {
|
|
285
|
+
const document = createDocument(`
|
|
286
|
+
<section id="card">
|
|
287
|
+
<span>Unlabeled copy</span>
|
|
288
|
+
</section>
|
|
289
|
+
`);
|
|
290
|
+
const card = document.getElementById("card") as HTMLElement;
|
|
291
|
+
const span = card.querySelector("span") as HTMLElement;
|
|
292
|
+
setElementRect(card, { width: 400, height: 200 });
|
|
293
|
+
setElementRect(span, { left: 40, top: 40, width: 140, height: 28 });
|
|
294
|
+
|
|
295
|
+
expect(
|
|
296
|
+
resolveVisualDomEditSelectionTarget([span, card], {
|
|
297
|
+
activeCompositionPath: "index.html",
|
|
298
|
+
}),
|
|
299
|
+
).toBe(card);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("keeps explicit layer selection able to target containers", () => {
|
|
303
|
+
const document = createDocument(`
|
|
304
|
+
<section id="container" class="hero-shell">
|
|
305
|
+
<span id="headline" class="headline">Launch faster</span>
|
|
306
|
+
</section>
|
|
307
|
+
`);
|
|
308
|
+
const container = document.getElementById("container") as HTMLElement;
|
|
309
|
+
const headline = document.getElementById("headline") as HTMLElement;
|
|
310
|
+
setElementRect(container, { width: 900, height: 520 });
|
|
311
|
+
setElementRect(headline, { left: 240, top: 160, width: 180, height: 36 });
|
|
312
|
+
|
|
313
|
+
const visualTarget = resolveVisualDomEditSelectionTarget([container, headline], {
|
|
314
|
+
activeCompositionPath: "index.html",
|
|
315
|
+
});
|
|
316
|
+
const explicitSelection = resolveDomEditSelection(container, {
|
|
317
|
+
activeCompositionPath: "index.html",
|
|
318
|
+
isMasterView: false,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(visualTarget).toBe(headline);
|
|
322
|
+
expect(explicitSelection?.id).toBe("container");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("isLargeRasterDomEditSelection", () => {
|
|
327
|
+
it("flags large image and background targets for raster click fallback", () => {
|
|
328
|
+
expect(
|
|
329
|
+
isLargeRasterDomEditSelection(
|
|
330
|
+
{
|
|
331
|
+
tagName: "img",
|
|
332
|
+
boundingBox: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
333
|
+
computedStyles: {},
|
|
334
|
+
},
|
|
335
|
+
{ width: 1920, height: 1080 },
|
|
336
|
+
),
|
|
337
|
+
).toBe(true);
|
|
338
|
+
|
|
339
|
+
expect(
|
|
340
|
+
isLargeRasterDomEditSelection(
|
|
341
|
+
{
|
|
342
|
+
tagName: "div",
|
|
343
|
+
boundingBox: { x: 0, y: 0, width: 1280, height: 720 },
|
|
344
|
+
computedStyles: { "background-image": 'url("hero.png")' },
|
|
345
|
+
},
|
|
346
|
+
{ width: 1920, height: 1080 },
|
|
347
|
+
),
|
|
348
|
+
).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("does not flag small media or text selections", () => {
|
|
352
|
+
expect(
|
|
353
|
+
isLargeRasterDomEditSelection(
|
|
354
|
+
{
|
|
355
|
+
tagName: "img",
|
|
356
|
+
boundingBox: { x: 80, y: 80, width: 96, height: 96 },
|
|
357
|
+
computedStyles: {},
|
|
358
|
+
},
|
|
359
|
+
{ width: 1920, height: 1080 },
|
|
360
|
+
),
|
|
361
|
+
).toBe(false);
|
|
362
|
+
|
|
363
|
+
expect(
|
|
364
|
+
isLargeRasterDomEditSelection(
|
|
365
|
+
{
|
|
366
|
+
tagName: "h1",
|
|
367
|
+
boundingBox: { x: 0, y: 0, width: 1600, height: 300 },
|
|
368
|
+
computedStyles: {},
|
|
369
|
+
},
|
|
370
|
+
{ width: 1920, height: 1080 },
|
|
371
|
+
),
|
|
372
|
+
).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
202
376
|
describe("resolveDomEditSelection", () => {
|
|
203
377
|
it("keeps composition host transforms disabled in master view", () => {
|
|
204
378
|
expect(
|
|
@@ -643,6 +817,36 @@ describe("resolveDomEditSelection", () => {
|
|
|
643
817
|
).toBe(root);
|
|
644
818
|
});
|
|
645
819
|
|
|
820
|
+
it("normalizes preview URLs when resolving master timeline composition clips", () => {
|
|
821
|
+
const document = createDocument(`
|
|
822
|
+
<div data-composition-id="main">
|
|
823
|
+
<div
|
|
824
|
+
id="slide-1"
|
|
825
|
+
data-composition-id="slide-core-conviction"
|
|
826
|
+
data-composition-src="compositions/slide-01-core-conviction.html"
|
|
827
|
+
>
|
|
828
|
+
<h1>Core Conviction</h1>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
`);
|
|
832
|
+
const slide = document.getElementById("slide-1") as HTMLElement;
|
|
833
|
+
|
|
834
|
+
expect(
|
|
835
|
+
findElementForTimelineElement(
|
|
836
|
+
document,
|
|
837
|
+
{
|
|
838
|
+
id: "slide-1",
|
|
839
|
+
compositionSrc:
|
|
840
|
+
"http://127.0.0.1:5176/api/projects/apple-presentation-download/preview/compositions/slide-01-core-conviction.html",
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
activeCompositionPath: null,
|
|
844
|
+
isMasterView: true,
|
|
845
|
+
},
|
|
846
|
+
),
|
|
847
|
+
).toBe(slide);
|
|
848
|
+
});
|
|
849
|
+
|
|
646
850
|
it("does not fall back to the root composition when an explicit timeline selector misses", () => {
|
|
647
851
|
const document = createDocument(`
|
|
648
852
|
<div data-composition-id="hook">
|
|
@@ -783,6 +987,50 @@ describe("patch builders and prompt builder", () => {
|
|
|
783
987
|
expect(prompt).not.toContain("Source file: index.html");
|
|
784
988
|
});
|
|
785
989
|
|
|
990
|
+
it("includes raster click context in copied agent prompts", () => {
|
|
991
|
+
const selection = {
|
|
992
|
+
element: {} as HTMLElement,
|
|
993
|
+
id: undefined,
|
|
994
|
+
selector: ".hero-bg",
|
|
995
|
+
selectorIndex: undefined,
|
|
996
|
+
sourceFile: "index.html",
|
|
997
|
+
compositionPath: "index.html",
|
|
998
|
+
compositionSrc: undefined,
|
|
999
|
+
isCompositionHost: false,
|
|
1000
|
+
label: "Hero Bg",
|
|
1001
|
+
tagName: "img",
|
|
1002
|
+
boundingBox: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
1003
|
+
textContent: null,
|
|
1004
|
+
dataAttributes: {},
|
|
1005
|
+
inlineStyles: {},
|
|
1006
|
+
computedStyles: {},
|
|
1007
|
+
textFields: [],
|
|
1008
|
+
capabilities: {
|
|
1009
|
+
canSelect: true,
|
|
1010
|
+
canEditStyles: true,
|
|
1011
|
+
canMove: true,
|
|
1012
|
+
canResize: true,
|
|
1013
|
+
canApplyManualOffset: true,
|
|
1014
|
+
canApplyManualSize: true,
|
|
1015
|
+
canApplyManualRotation: true,
|
|
1016
|
+
},
|
|
1017
|
+
} satisfies DomEditSelection;
|
|
1018
|
+
|
|
1019
|
+
const prompt = buildElementAgentPrompt({
|
|
1020
|
+
selection,
|
|
1021
|
+
currentTime: 3,
|
|
1022
|
+
selectionContext:
|
|
1023
|
+
"The user clicked visible text that is baked into the selected image/background.",
|
|
1024
|
+
userInstruction: "Change the title copy.",
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
expect(prompt).toContain("Selection context:");
|
|
1028
|
+
expect(prompt).toContain(
|
|
1029
|
+
"The user clicked visible text that is baked into the selected image/background.",
|
|
1030
|
+
);
|
|
1031
|
+
expect(prompt).toContain("Change the title copy.");
|
|
1032
|
+
});
|
|
1033
|
+
|
|
786
1034
|
it("serializes child text fields back into HTML", () => {
|
|
787
1035
|
expect(
|
|
788
1036
|
serializeDomEditTextFields([
|
|
@@ -105,6 +105,11 @@ export interface DomEditContextOptions {
|
|
|
105
105
|
preferClipAncestor?: boolean;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export interface DomEditViewport {
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
export interface TimelineElementDomTarget {
|
|
109
114
|
id?: string;
|
|
110
115
|
domId?: string;
|
|
@@ -291,6 +296,27 @@ function escapeCssString(value: string): string {
|
|
|
291
296
|
.replace(/\f/g, "\\c ");
|
|
292
297
|
}
|
|
293
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
|
+
|
|
294
320
|
function querySelectorAllSafely(doc: Document, selector: string): Element[] {
|
|
295
321
|
try {
|
|
296
322
|
return Array.from(doc.querySelectorAll(selector));
|
|
@@ -372,6 +398,7 @@ function buildElementLabel(el: HTMLElement): string {
|
|
|
372
398
|
const DOM_LAYER_IGNORED_TAGS = new Set([
|
|
373
399
|
"base",
|
|
374
400
|
"br",
|
|
401
|
+
"canvas",
|
|
375
402
|
"link",
|
|
376
403
|
"meta",
|
|
377
404
|
"script",
|
|
@@ -416,6 +443,91 @@ function getDomLayerPatchTarget(
|
|
|
416
443
|
};
|
|
417
444
|
}
|
|
418
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
|
+
|
|
419
531
|
function getDirectLayerChildren(el: HTMLElement, options: DomEditContextOptions): HTMLElement[] {
|
|
420
532
|
return Array.from(el.children).filter(
|
|
421
533
|
(child): child is HTMLElement =>
|
|
@@ -848,9 +960,14 @@ export function findElementForTimelineElement(
|
|
|
848
960
|
options: TimelineElementDomTargetOptions,
|
|
849
961
|
): HTMLElement | null {
|
|
850
962
|
const elementId = typeof element.id === "string" ? element.id : "";
|
|
851
|
-
const compositionSource =
|
|
963
|
+
const compositionSource =
|
|
964
|
+
normalizeTimelineCompositionSource(element.compositionSrc) ??
|
|
965
|
+
options.compIdToSrc?.get(elementId);
|
|
852
966
|
const sourceFile =
|
|
853
|
-
compositionSource ??
|
|
967
|
+
compositionSource ??
|
|
968
|
+
normalizeTimelineCompositionSource(element.sourceFile) ??
|
|
969
|
+
options.activeCompositionPath ??
|
|
970
|
+
"index.html";
|
|
854
971
|
const escapedElementId = escapeCssString(elementId);
|
|
855
972
|
const escapedCompositionSource = compositionSource ? escapeCssString(compositionSource) : null;
|
|
856
973
|
const selector =
|
|
@@ -927,12 +1044,14 @@ export function buildElementAgentPrompt({
|
|
|
927
1044
|
selection,
|
|
928
1045
|
currentTime,
|
|
929
1046
|
tagSnippet,
|
|
1047
|
+
selectionContext,
|
|
930
1048
|
userInstruction,
|
|
931
1049
|
sourceFilePath,
|
|
932
1050
|
}: {
|
|
933
1051
|
selection: DomEditSelection;
|
|
934
1052
|
currentTime: number;
|
|
935
1053
|
tagSnippet?: string;
|
|
1054
|
+
selectionContext?: string;
|
|
936
1055
|
userInstruction?: string;
|
|
937
1056
|
sourceFilePath?: string;
|
|
938
1057
|
}): string {
|
|
@@ -957,6 +1076,11 @@ export function buildElementAgentPrompt({
|
|
|
957
1076
|
lines.push(`Text: ${selection.textContent}`);
|
|
958
1077
|
}
|
|
959
1078
|
|
|
1079
|
+
const trimmedSelectionContext = selectionContext?.trim();
|
|
1080
|
+
if (trimmedSelectionContext) {
|
|
1081
|
+
lines.push("", "Selection context:", trimmedSelectionContext);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
960
1084
|
const textFieldsBlock = formatTextFields(selection.textFields);
|
|
961
1085
|
if (textFieldsBlock) {
|
|
962
1086
|
lines.push("", "Text fields:", textFieldsBlock);
|
|
@@ -16,16 +16,17 @@ describe("manual editing availability", () => {
|
|
|
16
16
|
vi.resetModules();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it("
|
|
19
|
+
it("enables inspector selection by default while motion and manual dragging stay opt-in", async () => {
|
|
20
20
|
const availability = await loadAvailabilityWithEnv({});
|
|
21
21
|
|
|
22
22
|
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(false);
|
|
23
|
-
expect(availability.
|
|
23
|
+
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
|
|
24
|
+
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
24
25
|
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
|
25
|
-
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(
|
|
26
|
+
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true);
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
it("
|
|
29
|
+
it("keeps explicit truthy inspector env flags enabled", async () => {
|
|
29
30
|
const availability = await loadAvailabilityWithEnv({
|
|
30
31
|
VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "1",
|
|
31
32
|
VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR: "true",
|
|
@@ -35,6 +36,15 @@ describe("manual editing availability", () => {
|
|
|
35
36
|
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true);
|
|
36
37
|
});
|
|
37
38
|
|
|
39
|
+
it("allows explicit env flags to disable default-on inspector layers", async () => {
|
|
40
|
+
const availability = await loadAvailabilityWithEnv({
|
|
41
|
+
VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR: "off",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
45
|
+
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
38
48
|
it("keeps timeline layer inspection off when the parent inspector flag is off", async () => {
|
|
39
49
|
const availability = await loadAvailabilityWithEnv({
|
|
40
50
|
VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "0",
|
|
@@ -42,6 +52,7 @@ describe("manual editing availability", () => {
|
|
|
42
52
|
});
|
|
43
53
|
|
|
44
54
|
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(false);
|
|
55
|
+
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(false);
|
|
45
56
|
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(false);
|
|
46
57
|
});
|
|
47
58
|
|
|
@@ -38,7 +38,7 @@ export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag
|
|
|
38
38
|
export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|
|
39
39
|
env,
|
|
40
40
|
[STUDIO_INSPECTOR_PANELS_ENV, "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"],
|
|
41
|
-
|
|
41
|
+
true,
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
@@ -52,9 +52,11 @@ export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED =
|
|
|
52
52
|
resolveStudioBooleanEnvFlag(
|
|
53
53
|
env,
|
|
54
54
|
[STUDIO_TIMELINE_LAYER_INSPECTOR_ENV, "VITE_STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED"],
|
|
55
|
-
|
|
55
|
+
true,
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
59
|
+
|
|
58
60
|
export const STUDIO_MANUAL_EDITING_ENABLED = STUDIO_PREVIEW_MANUAL_EDITING_ENABLED;
|
|
59
61
|
|
|
60
62
|
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|
|
@@ -1053,7 +1053,11 @@ function wrapSeekReapplyFunction(
|
|
|
1053
1053
|
return result;
|
|
1054
1054
|
};
|
|
1055
1055
|
markWrapped(wrappedSeek);
|
|
1056
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
owner[key] = wrappedSeek;
|
|
1058
|
+
} catch {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1057
1061
|
return true;
|
|
1058
1062
|
}
|
|
1059
1063
|
|
|
@@ -1161,7 +1165,11 @@ function wrapPlayReapplyFunction(
|
|
|
1161
1165
|
return result;
|
|
1162
1166
|
};
|
|
1163
1167
|
markWrapped(wrappedPlay);
|
|
1164
|
-
|
|
1168
|
+
try {
|
|
1169
|
+
owner[key] = wrappedPlay;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1165
1173
|
return true;
|
|
1166
1174
|
}
|
|
1167
1175
|
|
|
@@ -1181,7 +1189,11 @@ function wrapApplyAfterFunction(
|
|
|
1181
1189
|
return result;
|
|
1182
1190
|
};
|
|
1183
1191
|
markWrapped(wrappedApplyAfter);
|
|
1184
|
-
|
|
1192
|
+
try {
|
|
1193
|
+
owner[key] = wrappedApplyAfter;
|
|
1194
|
+
} catch {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1185
1197
|
return true;
|
|
1186
1198
|
}
|
|
1187
1199
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldDisableTimelineWhileCompositionLoading } from "./NLELayout";
|
|
3
|
+
|
|
4
|
+
describe("timeline loading disable state", () => {
|
|
5
|
+
it("disables the timeline while the composition loading overlay is visible", () => {
|
|
6
|
+
expect(shouldDisableTimelineWhileCompositionLoading(true)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("reenables the timeline after composition loading finishes", () => {
|
|
10
|
+
expect(shouldDisableTimelineWhileCompositionLoading(false)).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
});
|