@hyperframes/studio 0.6.47 → 0.6.49
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-BP6jGdt0.js +418 -0
- package/dist/assets/index-B4Cr7MVx.js +138 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/editor/DomEditOverlay.test.ts +3 -2
- package/src/components/editor/DomEditOverlay.tsx +4 -7
- package/src/components/editor/LayersPanel.tsx +8 -7
- package/src/components/editor/domEditing.test.ts +58 -43
- package/src/components/editor/domEditingLayers.ts +56 -5
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -1
- package/src/components/nle/NLEPreview.test.ts +17 -1
- package/src/components/nle/NLEPreview.tsx +58 -8
- package/src/hooks/useDomEditCommits.ts +10 -1
- package/src/hooks/useDomEditSession.ts +4 -4
- package/src/hooks/useDomEditTextCommits.ts +3 -3
- package/src/hooks/useDomSelection.ts +28 -16
- package/src/hooks/usePreviewInteraction.ts +7 -6
- package/src/hooks/useStudioUrlState.ts +4 -3
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +95 -143
- package/src/player/hooks/useTimelinePlayer.ts +26 -27
- package/src/player/lib/playbackAdapter.test.ts +165 -2
- package/src/player/lib/playbackAdapter.ts +12 -4
- package/src/player/lib/playbackSeek.ts +21 -0
- package/src/utils/studioUrlState.test.ts +6 -4
- package/dist/assets/hyperframes-player-CWb0VPYD.js +0 -418
- package/dist/assets/index-DpbZouXZ.js +0 -138
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-B4Cr7MVx.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-SKRp8mGz.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.49",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"mediabunny": "^1.45.3",
|
|
34
|
-
"@hyperframes/core": "0.6.
|
|
35
|
-
"@hyperframes/player": "0.6.
|
|
34
|
+
"@hyperframes/core": "0.6.49",
|
|
35
|
+
"@hyperframes/player": "0.6.49"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "19",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vite": "^6.4.2",
|
|
47
47
|
"vitest": "^3.2.4",
|
|
48
48
|
"zustand": "^5.0.0",
|
|
49
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.49"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
|
@@ -143,10 +143,11 @@ describe("DomEditOverlay", () => {
|
|
|
143
143
|
iframeRef,
|
|
144
144
|
activeCompositionPath: null,
|
|
145
145
|
selection: selected,
|
|
146
|
-
|
|
146
|
+
// Simulate the element being hovered before pointer-down (real users always hover first)
|
|
147
|
+
hoverSelection: selection,
|
|
147
148
|
groupSelections: [],
|
|
148
149
|
onCanvasMouseDown: () => {},
|
|
149
|
-
onCanvasPointerMove: () => selection,
|
|
150
|
+
onCanvasPointerMove: () => Promise.resolve(selection),
|
|
150
151
|
onCanvasPointerLeave: () => {},
|
|
151
152
|
onSelectionChange: (next: DomEditSelection) => setSelected(next),
|
|
152
153
|
onBlockedMove: () => {},
|
|
@@ -43,7 +43,7 @@ interface DomEditOverlayProps {
|
|
|
43
43
|
onCanvasPointerMove: (
|
|
44
44
|
event: React.PointerEvent<HTMLDivElement>,
|
|
45
45
|
options?: { preferClipAncestor?: boolean },
|
|
46
|
-
) => DomEditSelection | null
|
|
46
|
+
) => Promise<DomEditSelection | null>;
|
|
47
47
|
onCanvasPointerLeave: () => void;
|
|
48
48
|
onSelectionChange: (
|
|
49
49
|
selection: DomEditSelection,
|
|
@@ -195,9 +195,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
195
195
|
const handleOverlayPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
196
196
|
if (!allowCanvasMovement || event.button !== 0) return;
|
|
197
197
|
if (event.shiftKey) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
hoverSelectionRef.current;
|
|
198
|
+
// Use the already-updated hover selection rather than re-resolving async
|
|
199
|
+
const candidate = hoverSelectionRef.current;
|
|
201
200
|
if (!candidate) return;
|
|
202
201
|
event.preventDefault();
|
|
203
202
|
event.stopPropagation();
|
|
@@ -211,9 +210,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
211
210
|
const target = event.target as HTMLElement | null;
|
|
212
211
|
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
|
|
213
212
|
|
|
214
|
-
const candidate =
|
|
215
|
-
onCanvasPointerMoveRef.current(event, { preferClipAncestor: false }) ??
|
|
216
|
-
hoverSelectionRef.current;
|
|
213
|
+
const candidate = hoverSelectionRef.current;
|
|
217
214
|
if (!candidate?.capabilities.canApplyManualOffset) return;
|
|
218
215
|
|
|
219
216
|
const overlayEl = overlayRef.current;
|
|
@@ -119,12 +119,13 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
119
119
|
isMasterView,
|
|
120
120
|
preferClipAncestor: false,
|
|
121
121
|
}),
|
|
122
|
+
// LayersPanel has no projectId; probe is skipped when projectId is absent
|
|
122
123
|
[activeCompPath, isMasterView],
|
|
123
124
|
);
|
|
124
125
|
|
|
125
126
|
const seekToLayer = useCallback(
|
|
126
|
-
(layer: DomEditLayerItem) => {
|
|
127
|
-
const selection = resolveSelection(layer);
|
|
127
|
+
async (layer: DomEditLayerItem) => {
|
|
128
|
+
const selection = await resolveSelection(layer);
|
|
128
129
|
if (!selection) return;
|
|
129
130
|
|
|
130
131
|
let matchedId = findMatchingTimelineElementId(selection, timelineElements);
|
|
@@ -158,22 +159,22 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
158
159
|
);
|
|
159
160
|
|
|
160
161
|
const handleSelectLayer = useCallback(
|
|
161
|
-
(layer: DomEditLayerItem) => {
|
|
162
|
-
const selection = resolveSelection(layer);
|
|
162
|
+
async (layer: DomEditLayerItem) => {
|
|
163
|
+
const selection = await resolveSelection(layer);
|
|
163
164
|
if (!selection) return;
|
|
164
165
|
applyDomSelection(selection);
|
|
165
|
-
seekToLayer(layer);
|
|
166
|
+
await seekToLayer(layer);
|
|
166
167
|
},
|
|
167
168
|
[resolveSelection, applyDomSelection, seekToLayer],
|
|
168
169
|
);
|
|
169
170
|
|
|
170
171
|
const handleLayerHover = useCallback(
|
|
171
|
-
(layer: DomEditLayerItem | null) => {
|
|
172
|
+
async (layer: DomEditLayerItem | null) => {
|
|
172
173
|
if (!layer) {
|
|
173
174
|
updateDomEditHoverSelection(null);
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
|
-
const selection = resolveSelection(layer);
|
|
177
|
+
const selection = await resolveSelection(layer);
|
|
177
178
|
updateDomEditHoverSelection(selection);
|
|
178
179
|
},
|
|
179
180
|
[resolveSelection, updateDomEditHoverSelection],
|
|
@@ -226,6 +226,7 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
describe("resolveVisualDomEditSelectionTarget", () => {
|
|
229
|
+
// fallow-ignore-next-line code-duplication
|
|
229
230
|
it("prefers the visible leaf under the pointer over an oversized container", () => {
|
|
230
231
|
const document = createDocument(`
|
|
231
232
|
<section id="container" class="hero-shell">
|
|
@@ -299,7 +300,7 @@ describe("resolveVisualDomEditSelectionTarget", () => {
|
|
|
299
300
|
).toBe(card);
|
|
300
301
|
});
|
|
301
302
|
|
|
302
|
-
it("keeps explicit layer selection able to target containers", () => {
|
|
303
|
+
it("keeps explicit layer selection able to target containers", async () => {
|
|
303
304
|
const document = createDocument(`
|
|
304
305
|
<section id="container" class="hero-shell">
|
|
305
306
|
<span id="headline" class="headline">Launch faster</span>
|
|
@@ -313,7 +314,7 @@ describe("resolveVisualDomEditSelectionTarget", () => {
|
|
|
313
314
|
const visualTarget = resolveVisualDomEditSelectionTarget([container, headline], {
|
|
314
315
|
activeCompositionPath: "index.html",
|
|
315
316
|
});
|
|
316
|
-
const explicitSelection = resolveDomEditSelection(container, {
|
|
317
|
+
const explicitSelection = await resolveDomEditSelection(container, {
|
|
317
318
|
activeCompositionPath: "index.html",
|
|
318
319
|
isMasterView: false,
|
|
319
320
|
});
|
|
@@ -430,7 +431,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
430
431
|
});
|
|
431
432
|
});
|
|
432
433
|
|
|
433
|
-
it("resolves child clicks inside a composition host to the child in master view", () => {
|
|
434
|
+
it("resolves child clicks inside a composition host to the child in master view", async () => {
|
|
434
435
|
const document = createDocument(`
|
|
435
436
|
<div data-composition-id="main">
|
|
436
437
|
<div
|
|
@@ -445,7 +446,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
445
446
|
`);
|
|
446
447
|
|
|
447
448
|
const child = document.getElementById("inner-copy") as HTMLElement;
|
|
448
|
-
const selection = resolveDomEditSelection(child, {
|
|
449
|
+
const selection = await resolveDomEditSelection(child, {
|
|
449
450
|
activeCompositionPath: null,
|
|
450
451
|
isMasterView: true,
|
|
451
452
|
});
|
|
@@ -457,7 +458,8 @@ describe("resolveDomEditSelection", () => {
|
|
|
457
458
|
expect(selection?.capabilities.canEditStyles).toBe(true);
|
|
458
459
|
});
|
|
459
460
|
|
|
460
|
-
|
|
461
|
+
// fallow-ignore-next-line code-duplication
|
|
462
|
+
it("does not prefer a scene host clip ancestor when selecting inside it", async () => {
|
|
461
463
|
const document = createDocument(`
|
|
462
464
|
<div data-composition-id="main">
|
|
463
465
|
<div
|
|
@@ -472,7 +474,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
472
474
|
`);
|
|
473
475
|
|
|
474
476
|
const child = document.getElementById("inner-copy") as HTMLElement;
|
|
475
|
-
const selection = resolveDomEditSelection(child, {
|
|
477
|
+
const selection = await resolveDomEditSelection(child, {
|
|
476
478
|
activeCompositionPath: null,
|
|
477
479
|
isMasterView: true,
|
|
478
480
|
preferClipAncestor: true,
|
|
@@ -483,7 +485,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
483
485
|
expect(selection?.isCompositionHost).toBe(false);
|
|
484
486
|
});
|
|
485
487
|
|
|
486
|
-
it("still prefers an internal clip ancestor inside a scene", () => {
|
|
488
|
+
it("still prefers an internal clip ancestor inside a scene", async () => {
|
|
487
489
|
const document = createDocument(`
|
|
488
490
|
<div data-composition-id="main">
|
|
489
491
|
<div
|
|
@@ -500,7 +502,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
500
502
|
`);
|
|
501
503
|
|
|
502
504
|
const child = document.getElementById("inner-copy") as HTMLElement;
|
|
503
|
-
const selection = resolveDomEditSelection(child, {
|
|
505
|
+
const selection = await resolveDomEditSelection(child, {
|
|
504
506
|
activeCompositionPath: null,
|
|
505
507
|
isMasterView: true,
|
|
506
508
|
preferClipAncestor: true,
|
|
@@ -511,7 +513,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
511
513
|
expect(selection?.isCompositionHost).toBe(false);
|
|
512
514
|
});
|
|
513
515
|
|
|
514
|
-
it("scopes class selector indexing to the same source file", () => {
|
|
516
|
+
it("scopes class selector indexing to the same source file", async () => {
|
|
515
517
|
const document = createDocument(`
|
|
516
518
|
<div data-composition-id="main">
|
|
517
519
|
<div class="chip">Root chip</div>
|
|
@@ -522,7 +524,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
522
524
|
`);
|
|
523
525
|
|
|
524
526
|
const rootChip = document.getElementsByClassName("chip")[0] as HTMLElement;
|
|
525
|
-
const selection = resolveDomEditSelection(rootChip, {
|
|
527
|
+
const selection = await resolveDomEditSelection(rootChip, {
|
|
526
528
|
activeCompositionPath: null,
|
|
527
529
|
isMasterView: true,
|
|
528
530
|
});
|
|
@@ -533,7 +535,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
533
535
|
expect(findElementForSelection(document, selection!, null)).toBe(rootChip);
|
|
534
536
|
});
|
|
535
537
|
|
|
536
|
-
it("resolves nested duplicate ids from master view without treating root as the nested source", () => {
|
|
538
|
+
it("resolves nested duplicate ids from master view without treating root as the nested source", async () => {
|
|
537
539
|
const document = createDocument(`
|
|
538
540
|
<div data-composition-id="main">
|
|
539
541
|
<div id="card">Root card</div>
|
|
@@ -546,7 +548,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
546
548
|
const nestedCard = document.querySelector(
|
|
547
549
|
'[data-composition-file="scenes/nested.html"] #card',
|
|
548
550
|
) as HTMLElement;
|
|
549
|
-
const selection = resolveDomEditSelection(nestedCard, {
|
|
551
|
+
const selection = await resolveDomEditSelection(nestedCard, {
|
|
550
552
|
activeCompositionPath: null,
|
|
551
553
|
isMasterView: true,
|
|
552
554
|
});
|
|
@@ -588,7 +590,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
588
590
|
).toBeNull();
|
|
589
591
|
});
|
|
590
592
|
|
|
591
|
-
it("escapes ids and composition ids when creating stable selectors", () => {
|
|
593
|
+
it("escapes ids and composition ids when creating stable selectors", async () => {
|
|
592
594
|
const document = createDocument(`
|
|
593
595
|
<div data-composition-id="main">
|
|
594
596
|
<div id="logo:light">Logo</div>
|
|
@@ -600,11 +602,11 @@ describe("resolveDomEditSelection", () => {
|
|
|
600
602
|
(element) => element.getAttribute("data-composition-id") === "scene:one",
|
|
601
603
|
) as HTMLElement;
|
|
602
604
|
|
|
603
|
-
const logoSelection = resolveDomEditSelection(logo, {
|
|
605
|
+
const logoSelection = await resolveDomEditSelection(logo, {
|
|
604
606
|
activeCompositionPath: null,
|
|
605
607
|
isMasterView: true,
|
|
606
608
|
});
|
|
607
|
-
const sceneSelection = resolveDomEditSelection(scene, {
|
|
609
|
+
const sceneSelection = await resolveDomEditSelection(scene, {
|
|
608
610
|
activeCompositionPath: null,
|
|
609
611
|
isMasterView: true,
|
|
610
612
|
});
|
|
@@ -615,7 +617,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
615
617
|
expect(findElementForSelection(document, sceneSelection!, null)).toBe(scene);
|
|
616
618
|
});
|
|
617
619
|
|
|
618
|
-
it("prefers the nearest clip ancestor on single-click style selection", () => {
|
|
620
|
+
it("prefers the nearest clip ancestor on single-click style selection", async () => {
|
|
619
621
|
const document = createDocument(`
|
|
620
622
|
<section id="card" class="clip" style="left: 10px; top: 20px; width: 200px; height: 100px; position: absolute;">
|
|
621
623
|
<p id="copy">Hello</p>
|
|
@@ -623,7 +625,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
623
625
|
`);
|
|
624
626
|
|
|
625
627
|
const child = document.getElementById("copy") as HTMLElement;
|
|
626
|
-
const selection = resolveDomEditSelection(child, {
|
|
628
|
+
const selection = await resolveDomEditSelection(child, {
|
|
627
629
|
activeCompositionPath: null,
|
|
628
630
|
isMasterView: false,
|
|
629
631
|
preferClipAncestor: true,
|
|
@@ -633,7 +635,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
633
635
|
expect(selection?.selector).toBe("#card");
|
|
634
636
|
});
|
|
635
637
|
|
|
636
|
-
it("can resolve the exact child when clip-ancestor preference is disabled", () => {
|
|
638
|
+
it("can resolve the exact child when clip-ancestor preference is disabled", async () => {
|
|
637
639
|
const document = createDocument(`
|
|
638
640
|
<section id="card" class="clip" style="left: 10px; top: 20px; width: 200px; height: 100px; position: absolute;">
|
|
639
641
|
<p id="copy">Hello</p>
|
|
@@ -641,7 +643,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
641
643
|
`);
|
|
642
644
|
|
|
643
645
|
const child = document.getElementById("copy") as HTMLElement;
|
|
644
|
-
const selection = resolveDomEditSelection(child, {
|
|
646
|
+
const selection = await resolveDomEditSelection(child, {
|
|
645
647
|
activeCompositionPath: null,
|
|
646
648
|
isMasterView: false,
|
|
647
649
|
preferClipAncestor: false,
|
|
@@ -651,7 +653,8 @@ describe("resolveDomEditSelection", () => {
|
|
|
651
653
|
expect(selection?.selector).toBe("#copy");
|
|
652
654
|
});
|
|
653
655
|
|
|
654
|
-
|
|
656
|
+
// fallow-ignore-next-line code-duplication
|
|
657
|
+
it("collects simple child text blocks as separate editable fields", async () => {
|
|
655
658
|
const document = createDocument(`
|
|
656
659
|
<section id="card" class="clip" style="left: 10px; top: 20px; width: 200px; height: 100px; position: absolute;">
|
|
657
660
|
<strong>Headline</strong>
|
|
@@ -659,10 +662,13 @@ describe("resolveDomEditSelection", () => {
|
|
|
659
662
|
</section>
|
|
660
663
|
`);
|
|
661
664
|
|
|
662
|
-
const selection = resolveDomEditSelection(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
665
|
+
const selection = await resolveDomEditSelection(
|
|
666
|
+
document.getElementById("card") as HTMLElement,
|
|
667
|
+
{
|
|
668
|
+
activeCompositionPath: null,
|
|
669
|
+
isMasterView: false,
|
|
670
|
+
},
|
|
671
|
+
);
|
|
666
672
|
|
|
667
673
|
expect(selection?.textFields.map((field) => field.label)).toEqual(["Text 1", "Text 2"]);
|
|
668
674
|
expect(selection?.textFields.map((field) => field.value)).toEqual([
|
|
@@ -671,30 +677,36 @@ describe("resolveDomEditSelection", () => {
|
|
|
671
677
|
]);
|
|
672
678
|
});
|
|
673
679
|
|
|
674
|
-
it("preserves user-entered text spacing in editable text fields", () => {
|
|
680
|
+
it("preserves user-entered text spacing in editable text fields", async () => {
|
|
675
681
|
const document = createDocument(`
|
|
676
682
|
<section id="card" class="clip" style="position: absolute;">
|
|
677
683
|
<strong>Headline with trailing space </strong>
|
|
678
684
|
</section>
|
|
679
685
|
`);
|
|
680
686
|
|
|
681
|
-
const selection = resolveDomEditSelection(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
687
|
+
const selection = await resolveDomEditSelection(
|
|
688
|
+
document.getElementById("card") as HTMLElement,
|
|
689
|
+
{
|
|
690
|
+
activeCompositionPath: null,
|
|
691
|
+
isMasterView: false,
|
|
692
|
+
},
|
|
693
|
+
);
|
|
685
694
|
|
|
686
695
|
expect(selection?.textFields[0]?.value).toBe("Headline with trailing space ");
|
|
687
696
|
});
|
|
688
697
|
|
|
689
|
-
it("keeps an emptied text layer editable so users can type into it again", () => {
|
|
698
|
+
it("keeps an emptied text layer editable so users can type into it again", async () => {
|
|
690
699
|
const document = createDocument(`
|
|
691
700
|
<div id="card" class="clip" style="position: absolute;"></div>
|
|
692
701
|
`);
|
|
693
702
|
|
|
694
|
-
const selection = resolveDomEditSelection(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
703
|
+
const selection = await resolveDomEditSelection(
|
|
704
|
+
document.getElementById("card") as HTMLElement,
|
|
705
|
+
{
|
|
706
|
+
activeCompositionPath: null,
|
|
707
|
+
isMasterView: false,
|
|
708
|
+
},
|
|
709
|
+
);
|
|
698
710
|
|
|
699
711
|
expect(selection?.textFields).toMatchObject([
|
|
700
712
|
{
|
|
@@ -707,7 +719,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
707
719
|
expect(selection ? isTextEditableSelection(selection) : false).toBe(true);
|
|
708
720
|
});
|
|
709
721
|
|
|
710
|
-
it("keeps emptied child text layers editable after their content is cleared", () => {
|
|
722
|
+
it("keeps emptied child text layers editable after their content is cleared", async () => {
|
|
711
723
|
const document = createDocument(`
|
|
712
724
|
<div id="card" class="clip" style="position: absolute;">
|
|
713
725
|
<strong></strong>
|
|
@@ -715,16 +727,19 @@ describe("resolveDomEditSelection", () => {
|
|
|
715
727
|
</div>
|
|
716
728
|
`);
|
|
717
729
|
|
|
718
|
-
const selection = resolveDomEditSelection(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
730
|
+
const selection = await resolveDomEditSelection(
|
|
731
|
+
document.getElementById("card") as HTMLElement,
|
|
732
|
+
{
|
|
733
|
+
activeCompositionPath: null,
|
|
734
|
+
isMasterView: false,
|
|
735
|
+
},
|
|
736
|
+
);
|
|
722
737
|
|
|
723
738
|
expect(selection?.textFields.map((field) => field.tagName)).toEqual(["strong", "span"]);
|
|
724
739
|
expect(selection?.textFields.map((field) => field.value)).toEqual(["", ""]);
|
|
725
740
|
});
|
|
726
741
|
|
|
727
|
-
it("explains anonymous child elements that resolve to an editable parent", () => {
|
|
742
|
+
it("explains anonymous child elements that resolve to an editable parent", async () => {
|
|
728
743
|
const document = createDocument(`
|
|
729
744
|
<div data-composition-id="main">
|
|
730
745
|
<div id="card">
|
|
@@ -734,7 +749,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
734
749
|
`);
|
|
735
750
|
|
|
736
751
|
const child = document.querySelector("strong") as HTMLElement;
|
|
737
|
-
const selection = resolveDomEditSelection(child, {
|
|
752
|
+
const selection = await resolveDomEditSelection(child, {
|
|
738
753
|
activeCompositionPath: null,
|
|
739
754
|
isMasterView: false,
|
|
740
755
|
preferClipAncestor: false,
|
|
@@ -744,7 +759,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
744
759
|
expect(getDomEditNonEditableReason(child, selection)).toBe("Selection resolves to Card");
|
|
745
760
|
});
|
|
746
761
|
|
|
747
|
-
it("does not mark an element as non-editable when Studio can edit it directly", () => {
|
|
762
|
+
it("does not mark an element as non-editable when Studio can edit it directly", async () => {
|
|
748
763
|
const document = createDocument(`
|
|
749
764
|
<div data-composition-id="main">
|
|
750
765
|
<div id="card">Editable</div>
|
|
@@ -752,7 +767,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
752
767
|
`);
|
|
753
768
|
|
|
754
769
|
const element = document.getElementById("card") as HTMLElement;
|
|
755
|
-
const selection = resolveDomEditSelection(element, {
|
|
770
|
+
const selection = await resolveDomEditSelection(element, {
|
|
756
771
|
activeCompositionPath: null,
|
|
757
772
|
isMasterView: false,
|
|
758
773
|
});
|
|
@@ -73,6 +73,7 @@ function buildTextField(
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// fallow-ignore-next-line complexity
|
|
76
77
|
export function collectDomEditTextFields(el: HTMLElement): DomEditTextField[] {
|
|
77
78
|
const childElements = Array.from(el.children).filter(isHtmlElement).filter(isEditableTextLeaf);
|
|
78
79
|
|
|
@@ -169,6 +170,7 @@ export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>):
|
|
|
169
170
|
|
|
170
171
|
// ─── Capabilities ────────────────────────────────────────────────────────────
|
|
171
172
|
|
|
173
|
+
// fallow-ignore-next-line complexity
|
|
172
174
|
export function resolveDomEditCapabilities(args: {
|
|
173
175
|
selector?: string;
|
|
174
176
|
tagName?: string;
|
|
@@ -178,6 +180,7 @@ export function resolveDomEditCapabilities(args: {
|
|
|
178
180
|
isCompositionHost: boolean;
|
|
179
181
|
isInsideLockedComposition: boolean;
|
|
180
182
|
isMasterView: boolean;
|
|
183
|
+
existsInSource?: boolean;
|
|
181
184
|
}): DomEditCapabilities {
|
|
182
185
|
if (!args.selector || args.isInsideLockedComposition) {
|
|
183
186
|
return {
|
|
@@ -194,6 +197,19 @@ export function resolveDomEditCapabilities(args: {
|
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
if (args.existsInSource === false) {
|
|
201
|
+
return {
|
|
202
|
+
canSelect: true,
|
|
203
|
+
canEditStyles: false,
|
|
204
|
+
canMove: false,
|
|
205
|
+
canResize: false,
|
|
206
|
+
canApplyManualOffset: false,
|
|
207
|
+
canApplyManualSize: false,
|
|
208
|
+
canApplyManualRotation: false,
|
|
209
|
+
reasonIfDisabled: "This element is generated by a script and cannot be edited visually.",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
197
213
|
const position = args.computedStyles.position;
|
|
198
214
|
const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
|
|
199
215
|
const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
|
|
@@ -243,6 +259,7 @@ export function resolveDomEditCapabilities(args: {
|
|
|
243
259
|
|
|
244
260
|
// ─── Element label ────────────────────────────────────────────────────────────
|
|
245
261
|
|
|
262
|
+
// fallow-ignore-next-line complexity
|
|
246
263
|
export function buildElementLabel(el: HTMLElement): string {
|
|
247
264
|
const compositionId = el.getAttribute("data-composition-id");
|
|
248
265
|
if (compositionId && compositionId !== "main") {
|
|
@@ -267,12 +284,37 @@ export function buildElementLabel(el: HTMLElement): string {
|
|
|
267
284
|
return el.tagName.toLowerCase();
|
|
268
285
|
}
|
|
269
286
|
|
|
287
|
+
// ─── Source probe ────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async function probeSourceElement(
|
|
290
|
+
projectId: string,
|
|
291
|
+
sourceFile: string,
|
|
292
|
+
target: { id?: string; selector?: string; selectorIndex?: number },
|
|
293
|
+
): Promise<boolean> {
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(
|
|
296
|
+
`/api/projects/${projectId}/file-mutations/probe-element/${encodeURIComponent(sourceFile)}`,
|
|
297
|
+
{
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: { "Content-Type": "application/json" },
|
|
300
|
+
body: JSON.stringify({ target }),
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
if (!response.ok) return true;
|
|
304
|
+
const data = (await response.json()) as { exists?: boolean };
|
|
305
|
+
return data.exists !== false;
|
|
306
|
+
} catch {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
270
311
|
// ─── Selection resolution ────────────────────────────────────────────────────
|
|
271
312
|
|
|
272
|
-
|
|
313
|
+
// fallow-ignore-next-line complexity
|
|
314
|
+
export async function resolveDomEditSelection(
|
|
273
315
|
startEl: HTMLElement | null,
|
|
274
|
-
options: DomEditContextOptions,
|
|
275
|
-
): DomEditSelection | null {
|
|
316
|
+
options: DomEditContextOptions & { projectId?: string | null; skipSourceProbe?: boolean },
|
|
317
|
+
): Promise<DomEditSelection | null> {
|
|
276
318
|
if (!startEl) return null;
|
|
277
319
|
const doc = startEl.ownerDocument;
|
|
278
320
|
|
|
@@ -303,6 +345,14 @@ export function resolveDomEditSelection(
|
|
|
303
345
|
const computedStyles = getCuratedComputedStyles(current);
|
|
304
346
|
const textFields = collectDomEditTextFields(current);
|
|
305
347
|
const isInsideLocked = Boolean(findClosestByAttribute(current, ["data-timeline-locked"]));
|
|
348
|
+
let existsInSource: boolean | undefined;
|
|
349
|
+
if (!options.skipSourceProbe && options.projectId && (current.id || selector)) {
|
|
350
|
+
const probeTarget: { id?: string; selector?: string; selectorIndex?: number } = {};
|
|
351
|
+
if (current.id) probeTarget.id = current.id;
|
|
352
|
+
if (selector) probeTarget.selector = selector;
|
|
353
|
+
if (selectorIndex != null) probeTarget.selectorIndex = selectorIndex;
|
|
354
|
+
existsInSource = await probeSourceElement(options.projectId, sourceFile, probeTarget);
|
|
355
|
+
}
|
|
306
356
|
const capabilities = resolveDomEditCapabilities({
|
|
307
357
|
selector,
|
|
308
358
|
tagName: current.tagName.toLowerCase(),
|
|
@@ -312,6 +362,7 @@ export function resolveDomEditSelection(
|
|
|
312
362
|
isCompositionHost: Boolean(compositionSrc),
|
|
313
363
|
isInsideLockedComposition: isInsideLocked,
|
|
314
364
|
isMasterView: options.isMasterView,
|
|
365
|
+
existsInSource,
|
|
315
366
|
});
|
|
316
367
|
const rect = current.getBoundingClientRect();
|
|
317
368
|
|
|
@@ -345,10 +396,10 @@ export function resolveDomEditSelection(
|
|
|
345
396
|
return null;
|
|
346
397
|
}
|
|
347
398
|
|
|
348
|
-
export function refreshDomEditSelection(
|
|
399
|
+
export async function refreshDomEditSelection(
|
|
349
400
|
selection: DomEditSelection,
|
|
350
401
|
activeCompositionPath: string | null,
|
|
351
|
-
): DomEditSelection | null {
|
|
402
|
+
): Promise<DomEditSelection | null> {
|
|
352
403
|
const doc = selection.element.ownerDocument;
|
|
353
404
|
const nextElement = findElementForSelection(doc, selection, activeCompositionPath);
|
|
354
405
|
return nextElement
|
|
@@ -73,7 +73,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
73
73
|
(
|
|
74
74
|
e: React.PointerEvent<HTMLDivElement>,
|
|
75
75
|
o?: { preferClipAncestor?: boolean },
|
|
76
|
-
) => DomEditSelection | null
|
|
76
|
+
) => Promise<DomEditSelection | null>
|
|
77
77
|
>;
|
|
78
78
|
onCanvasMouseDown: (
|
|
79
79
|
e: React.MouseEvent<HTMLDivElement>,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { act, createRef } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { NLEPreview, getPreviewPlayerKey } from "./NLEPreview";
|
|
6
|
+
import { NLEPreview, getPreviewPlayerKey, resolvePreviewStageSize } from "./NLEPreview";
|
|
7
7
|
|
|
8
8
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
9
|
|
|
@@ -133,6 +133,22 @@ describe("getPreviewPlayerKey", () => {
|
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
describe("resolvePreviewStageSize", () => {
|
|
137
|
+
it("fits portrait composition dimensions by height in a narrow viewport", () => {
|
|
138
|
+
expect(resolvePreviewStageSize(512, 402, { width: 1080, height: 1920 }, undefined)).toEqual({
|
|
139
|
+
width: 217.125,
|
|
140
|
+
height: 386,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("uses composition dimensions ahead of the legacy portrait fallback", () => {
|
|
145
|
+
expect(resolvePreviewStageSize(512, 402, { width: 1920, height: 1080 }, true)).toEqual({
|
|
146
|
+
width: 496,
|
|
147
|
+
height: 279,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
136
152
|
describe("NLEPreview", () => {
|
|
137
153
|
beforeEach(() => {
|
|
138
154
|
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver;
|