@hyperframes/studio 0.6.0-alpha.2 → 0.6.0-alpha.5
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-CEnWY28J.js +417 -0
- package/dist/assets/index-BTa7zV4Z.js +106 -0
- package/dist/assets/index-pZvEUcY0.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +226 -99
- package/src/components/editor/PropertyPanel.test.ts +49 -0
- package/src/components/editor/PropertyPanel.tsx +258 -1276
- package/src/components/editor/domEditing.test.ts +248 -0
- package/src/components/editor/domEditing.ts +126 -2
- package/src/components/editor/manualEditingAvailability.test.ts +3 -1
- package/src/components/editor/manualEditingAvailability.ts +2 -0
- package/src/components/editor/manualEdits.ts +15 -3
- package/src/components/nle/NLELayout.test.ts +12 -0
- package/src/components/nle/NLELayout.tsx +56 -24
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/renders/RenderQueue.tsx +66 -4
- package/src/components/renders/useRenderQueue.ts +30 -6
- package/src/player/components/Player.test.ts +58 -0
- package/src/player/components/Player.tsx +58 -4
- package/src/player/components/PlayerControls.tsx +20 -7
- package/src/player/components/Timeline.tsx +38 -1
- package/dist/assets/hyperframes-player-Cd8vYWxP.js +0 -198
- package/dist/assets/index-UWFaHilT.css +0 -1
- package/dist/assets/index-cPJbxeAk.js +0 -107
package/src/App.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useEffect,
|
|
6
6
|
useMemo,
|
|
7
|
+
type CSSProperties,
|
|
7
8
|
type MouseEvent,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
} from "react";
|
|
@@ -78,10 +79,10 @@ import {
|
|
|
78
79
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
79
80
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
80
81
|
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
82
|
+
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
81
83
|
STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED,
|
|
82
84
|
} from "./components/editor/manualEditingAvailability";
|
|
83
85
|
import {
|
|
84
|
-
buildDefaultDomEditTextField,
|
|
85
86
|
buildDomEditStylePatchOperation,
|
|
86
87
|
buildDomEditTextPatchOperation,
|
|
87
88
|
buildElementAgentPrompt,
|
|
@@ -91,9 +92,12 @@ import {
|
|
|
91
92
|
findElementForTimelineElement,
|
|
92
93
|
getDomEditLayerKey,
|
|
93
94
|
getDomEditTargetKey,
|
|
95
|
+
isLargeRasterDomEditSelection,
|
|
94
96
|
isTextEditableSelection,
|
|
97
|
+
resolveVisualDomEditSelectionTarget,
|
|
95
98
|
serializeDomEditTextFields,
|
|
96
99
|
resolveDomEditSelection,
|
|
100
|
+
type DomEditViewport,
|
|
97
101
|
type DomEditLayerItem,
|
|
98
102
|
type DomEditTextField,
|
|
99
103
|
type DomEditSelection,
|
|
@@ -358,10 +362,64 @@ function isManualGeometryStyleProperty(property: string): boolean {
|
|
|
358
362
|
return property === "left" || property === "top" || property === "width" || property === "height";
|
|
359
363
|
}
|
|
360
364
|
|
|
365
|
+
interface PreviewLocalPointer {
|
|
366
|
+
x: number;
|
|
367
|
+
y: number;
|
|
368
|
+
viewport: DomEditViewport;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
interface AgentModalAnchorPoint {
|
|
372
|
+
x: number;
|
|
373
|
+
y: number;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolvePreviewLocalPointer(
|
|
377
|
+
iframe: HTMLIFrameElement,
|
|
378
|
+
doc: Document,
|
|
379
|
+
win: Window,
|
|
380
|
+
clientX: number,
|
|
381
|
+
clientY: number,
|
|
382
|
+
): PreviewLocalPointer | null {
|
|
383
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
384
|
+
const root =
|
|
385
|
+
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
386
|
+
const rootRect = root?.getBoundingClientRect();
|
|
387
|
+
const rootWidth = rootRect?.width || win.innerWidth;
|
|
388
|
+
const rootHeight = rootRect?.height || win.innerHeight;
|
|
389
|
+
if (!rootWidth || !rootHeight) return null;
|
|
390
|
+
|
|
391
|
+
const scaleX = iframeRect.width / rootWidth;
|
|
392
|
+
const scaleY = iframeRect.height / rootHeight;
|
|
393
|
+
return {
|
|
394
|
+
x: (clientX - iframeRect.left) / scaleX,
|
|
395
|
+
y: (clientY - iframeRect.top) / scaleY,
|
|
396
|
+
viewport: { width: rootWidth, height: rootHeight },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getPreviewLocalPointer(
|
|
401
|
+
iframe: HTMLIFrameElement,
|
|
402
|
+
clientX: number,
|
|
403
|
+
clientY: number,
|
|
404
|
+
): PreviewLocalPointer | null {
|
|
405
|
+
let doc: Document | null = null;
|
|
406
|
+
let win: Window | null = null;
|
|
407
|
+
try {
|
|
408
|
+
doc = iframe.contentDocument;
|
|
409
|
+
win = iframe.contentWindow;
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
if (!doc || !win) return null;
|
|
414
|
+
|
|
415
|
+
return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
416
|
+
}
|
|
417
|
+
|
|
361
418
|
function getPreviewTargetFromPointer(
|
|
362
419
|
iframe: HTMLIFrameElement,
|
|
363
420
|
clientX: number,
|
|
364
421
|
clientY: number,
|
|
422
|
+
activeCompositionPath: string | null,
|
|
365
423
|
): HTMLElement | null {
|
|
366
424
|
let doc: Document | null = null;
|
|
367
425
|
let win: Window | null = null;
|
|
@@ -373,20 +431,35 @@ function getPreviewTargetFromPointer(
|
|
|
373
431
|
}
|
|
374
432
|
if (!doc || !win) return null;
|
|
375
433
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
379
|
-
const rootRect = root?.getBoundingClientRect();
|
|
380
|
-
const rootWidth = rootRect?.width || win.innerWidth;
|
|
381
|
-
const rootHeight = rootRect?.height || win.innerHeight;
|
|
382
|
-
if (!rootWidth || !rootHeight) return null;
|
|
434
|
+
const localPointer = resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
435
|
+
if (!localPointer) return null;
|
|
383
436
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
437
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
438
|
+
const visualTarget = resolveVisualDomEditSelectionTarget(
|
|
439
|
+
doc.elementsFromPoint(localPointer.x, localPointer.y),
|
|
440
|
+
{
|
|
441
|
+
activeCompositionPath,
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
if (visualTarget) return visualTarget;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
448
|
+
}
|
|
388
449
|
|
|
389
|
-
|
|
450
|
+
function buildRasterClickSelectionContext(
|
|
451
|
+
selection: DomEditSelection,
|
|
452
|
+
localPointer: PreviewLocalPointer,
|
|
453
|
+
): string {
|
|
454
|
+
return [
|
|
455
|
+
"The user clicked a large raster/background element in the Studio preview.",
|
|
456
|
+
`Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round(
|
|
457
|
+
localPointer.viewport.width,
|
|
458
|
+
)}x${Math.round(localPointer.viewport.height)} composition.`,
|
|
459
|
+
`Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`,
|
|
460
|
+
"Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.",
|
|
461
|
+
"If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.",
|
|
462
|
+
].join("\n");
|
|
390
463
|
}
|
|
391
464
|
|
|
392
465
|
function domEditSelectionsTargetSame(
|
|
@@ -592,17 +665,47 @@ function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number |
|
|
|
592
665
|
|
|
593
666
|
// ── Ask Agent Modal ──
|
|
594
667
|
|
|
668
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
669
|
+
if (max < min) return min;
|
|
670
|
+
return Math.min(Math.max(value, min), max);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getAgentModalPositionStyle(
|
|
674
|
+
anchorPoint: AgentModalAnchorPoint | null,
|
|
675
|
+
): CSSProperties | undefined {
|
|
676
|
+
if (!anchorPoint || typeof window === "undefined") return undefined;
|
|
677
|
+
|
|
678
|
+
const modalWidth = 480;
|
|
679
|
+
const estimatedModalHeight = 270;
|
|
680
|
+
const margin = 16;
|
|
681
|
+
const left = clampNumber(
|
|
682
|
+
anchorPoint.x,
|
|
683
|
+
margin + modalWidth / 2,
|
|
684
|
+
window.innerWidth - margin - modalWidth / 2,
|
|
685
|
+
);
|
|
686
|
+
const top = clampNumber(
|
|
687
|
+
anchorPoint.y + 12,
|
|
688
|
+
margin,
|
|
689
|
+
window.innerHeight - margin - estimatedModalHeight,
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
return { left, top, transform: "translateX(-50%)" };
|
|
693
|
+
}
|
|
694
|
+
|
|
595
695
|
function AskAgentModal({
|
|
596
696
|
selectionLabel,
|
|
697
|
+
anchorPoint = null,
|
|
597
698
|
onSubmit,
|
|
598
699
|
onClose,
|
|
599
700
|
}: {
|
|
600
701
|
selectionLabel: string;
|
|
702
|
+
anchorPoint?: AgentModalAnchorPoint | null;
|
|
601
703
|
onSubmit: (instruction: string) => void;
|
|
602
704
|
onClose: () => void;
|
|
603
705
|
}) {
|
|
604
706
|
const [value, setValue] = useState("");
|
|
605
707
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
708
|
+
const modalPositionStyle = getAgentModalPositionStyle(anchorPoint);
|
|
606
709
|
|
|
607
710
|
useMountEffect(() => {
|
|
608
711
|
requestAnimationFrame(() => inputRef.current?.focus());
|
|
@@ -615,11 +718,18 @@ function AskAgentModal({
|
|
|
615
718
|
|
|
616
719
|
return (
|
|
617
720
|
<div
|
|
618
|
-
className=
|
|
721
|
+
className={
|
|
722
|
+
anchorPoint
|
|
723
|
+
? "fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
724
|
+
: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
725
|
+
}
|
|
619
726
|
onClick={onClose}
|
|
620
727
|
>
|
|
621
728
|
<div
|
|
622
|
-
className=
|
|
729
|
+
className={`w-[480px] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl ${
|
|
730
|
+
anchorPoint ? "fixed" : ""
|
|
731
|
+
}`}
|
|
732
|
+
style={modalPositionStyle}
|
|
623
733
|
onClick={(e) => e.stopPropagation()}
|
|
624
734
|
>
|
|
625
735
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
@@ -774,13 +884,17 @@ export function StudioApp() {
|
|
|
774
884
|
const [domEditGroupSelections, setDomEditGroupSelections] = useState<DomEditSelection[]>([]);
|
|
775
885
|
const [domEditHoverSelection, setDomEditHoverSelection] = useState<DomEditSelection | null>(null);
|
|
776
886
|
const [agentPromptTagSnippet, setAgentPromptTagSnippet] = useState<string | undefined>();
|
|
887
|
+
const [agentPromptSelectionContext, setAgentPromptSelectionContext] = useState<
|
|
888
|
+
string | undefined
|
|
889
|
+
>();
|
|
890
|
+
const [agentModalAnchorPoint, setAgentModalAnchorPoint] = useState<AgentModalAnchorPoint | null>(
|
|
891
|
+
null,
|
|
892
|
+
);
|
|
777
893
|
const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
|
|
778
894
|
const [agentModalOpen, setAgentModalOpen] = useState(false);
|
|
779
895
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
780
896
|
const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
|
|
781
|
-
const [
|
|
782
|
-
ReadonlySet<string>
|
|
783
|
-
>(() => new Set());
|
|
897
|
+
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
784
898
|
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
|
785
899
|
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
786
900
|
setPreviewDocumentVersion((version) => version + 1);
|
|
@@ -1771,6 +1885,8 @@ export function StudioApp() {
|
|
|
1771
1885
|
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
1772
1886
|
) => {
|
|
1773
1887
|
setAgentPromptTagSnippet(undefined);
|
|
1888
|
+
setAgentPromptSelectionContext(undefined);
|
|
1889
|
+
setAgentModalAnchorPoint(null);
|
|
1774
1890
|
setCopiedAgentPrompt(false);
|
|
1775
1891
|
if (!selection) {
|
|
1776
1892
|
domEditSelectionRef.current = null;
|
|
@@ -2296,13 +2412,13 @@ export function StudioApp() {
|
|
|
2296
2412
|
(clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
|
|
2297
2413
|
const iframe = previewIframeRef.current;
|
|
2298
2414
|
if (!iframe || captionEditMode) return null;
|
|
2299
|
-
const target = getPreviewTargetFromPointer(iframe, clientX, clientY);
|
|
2415
|
+
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
2300
2416
|
if (!target) return null;
|
|
2301
2417
|
return buildDomSelectionFromTarget(target, {
|
|
2302
2418
|
preferClipAncestor: options?.preferClipAncestor,
|
|
2303
2419
|
});
|
|
2304
2420
|
},
|
|
2305
|
-
[buildDomSelectionFromTarget, captionEditMode],
|
|
2421
|
+
[activeCompPath, buildDomSelectionFromTarget, captionEditMode],
|
|
2306
2422
|
);
|
|
2307
2423
|
|
|
2308
2424
|
const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
|
|
@@ -2398,8 +2514,21 @@ export function StudioApp() {
|
|
|
2398
2514
|
|
|
2399
2515
|
const selection = buildDomSelectionForTimelineElement(element);
|
|
2400
2516
|
if (selection) applyDomSelection(selection);
|
|
2517
|
+
|
|
2518
|
+
const key = getTimelineElementKey(element);
|
|
2519
|
+
if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
|
|
2520
|
+
setInspectedTimelineElementId(key);
|
|
2521
|
+
setLeftCollapsed(false);
|
|
2522
|
+
|
|
2523
|
+
const iframe = previewIframeRef.current;
|
|
2524
|
+
if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
|
|
2525
|
+
seekStudioPreview(iframe, element.start);
|
|
2526
|
+
}
|
|
2527
|
+
} else {
|
|
2528
|
+
setInspectedTimelineElementId(null);
|
|
2529
|
+
}
|
|
2401
2530
|
},
|
|
2402
|
-
[applyDomSelection, buildDomSelectionForTimelineElement],
|
|
2531
|
+
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
|
|
2403
2532
|
);
|
|
2404
2533
|
|
|
2405
2534
|
const handleTimelineElementInspect = useCallback(
|
|
@@ -2426,17 +2555,6 @@ export function StudioApp() {
|
|
|
2426
2555
|
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
|
|
2427
2556
|
);
|
|
2428
2557
|
|
|
2429
|
-
const handleToggleTimelineElementThumbnail = useCallback((element: TimelineElement) => {
|
|
2430
|
-
const key = getTimelineElementKey(element);
|
|
2431
|
-
if (!key) return;
|
|
2432
|
-
setThumbnailedTimelineElementIds((current) => {
|
|
2433
|
-
const next = new Set(current);
|
|
2434
|
-
if (next.has(key)) next.delete(key);
|
|
2435
|
-
else next.add(key);
|
|
2436
|
-
return next;
|
|
2437
|
-
});
|
|
2438
|
-
}, []);
|
|
2439
|
-
|
|
2440
2558
|
const handleTimelineLayerSelect = useCallback(
|
|
2441
2559
|
(layer: DomEditLayerItem) => {
|
|
2442
2560
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
@@ -2816,15 +2934,26 @@ export function StudioApp() {
|
|
|
2816
2934
|
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
2817
2935
|
);
|
|
2818
2936
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2937
|
+
try {
|
|
2938
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
2939
|
+
label: "Edit layer style",
|
|
2940
|
+
skipRefresh: true,
|
|
2941
|
+
prepareContent: importedFont
|
|
2942
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
2943
|
+
: undefined,
|
|
2944
|
+
});
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
|
2947
|
+
}
|
|
2948
|
+
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
2826
2949
|
},
|
|
2827
|
-
[
|
|
2950
|
+
[
|
|
2951
|
+
activeCompPath,
|
|
2952
|
+
domEditSelection,
|
|
2953
|
+
persistDomEditOperations,
|
|
2954
|
+
refreshDomEditSelectionFromPreview,
|
|
2955
|
+
resolveImportedFontAsset,
|
|
2956
|
+
],
|
|
2828
2957
|
);
|
|
2829
2958
|
|
|
2830
2959
|
const handleDomTextCommit = useCallback(
|
|
@@ -2978,51 +3107,11 @@ export function StudioApp() {
|
|
|
2978
3107
|
[commitDomTextFields, domEditSelection, handleDomStyleCommit, resolveImportedFontAsset],
|
|
2979
3108
|
);
|
|
2980
3109
|
|
|
2981
|
-
const handleDomAddTextField = useCallback(
|
|
2982
|
-
async (afterFieldKey?: string) => {
|
|
2983
|
-
if (!domEditSelection) return null;
|
|
2984
|
-
if (!domEditSelection.textFields.some((field) => field.source === "child")) return null;
|
|
2985
|
-
|
|
2986
|
-
const insertionIndex = domEditSelection.textFields.findIndex(
|
|
2987
|
-
(field) => field.key === afterFieldKey,
|
|
2988
|
-
);
|
|
2989
|
-
const baseField =
|
|
2990
|
-
domEditSelection.textFields[insertionIndex >= 0 ? insertionIndex : 0] ??
|
|
2991
|
-
domEditSelection.textFields[0];
|
|
2992
|
-
const nextField = buildDefaultDomEditTextField(baseField);
|
|
2993
|
-
const nextTextFields = [...domEditSelection.textFields];
|
|
2994
|
-
nextTextFields.splice(
|
|
2995
|
-
insertionIndex >= 0 ? insertionIndex + 1 : nextTextFields.length,
|
|
2996
|
-
0,
|
|
2997
|
-
nextField,
|
|
2998
|
-
);
|
|
2999
|
-
|
|
3000
|
-
await commitDomTextFields(domEditSelection, nextTextFields);
|
|
3001
|
-
return nextField.key;
|
|
3002
|
-
},
|
|
3003
|
-
[commitDomTextFields, domEditSelection],
|
|
3004
|
-
);
|
|
3005
|
-
|
|
3006
|
-
const handleDomRemoveTextField = useCallback(
|
|
3007
|
-
async (fieldKey: string) => {
|
|
3008
|
-
if (!domEditSelection) return;
|
|
3009
|
-
const field = domEditSelection.textFields.find((entry) => entry.key === fieldKey);
|
|
3010
|
-
if (!field) return;
|
|
3011
|
-
|
|
3012
|
-
if (field.source === "self") {
|
|
3013
|
-
await handleDomTextCommit("", fieldKey);
|
|
3014
|
-
return;
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
const nextTextFields = domEditSelection.textFields.filter((entry) => entry.key !== fieldKey);
|
|
3018
|
-
await commitDomTextFields(domEditSelection, nextTextFields);
|
|
3019
|
-
},
|
|
3020
|
-
[commitDomTextFields, domEditSelection, handleDomTextCommit],
|
|
3021
|
-
);
|
|
3022
|
-
|
|
3023
3110
|
const handleAskAgent = useCallback(() => {
|
|
3024
3111
|
if (!domEditSelection) return;
|
|
3025
3112
|
setAgentPromptTagSnippet(undefined);
|
|
3113
|
+
setAgentPromptSelectionContext(undefined);
|
|
3114
|
+
setAgentModalAnchorPoint(null);
|
|
3026
3115
|
void preloadAgentPromptSnippet(domEditSelection);
|
|
3027
3116
|
setAgentModalOpen(true);
|
|
3028
3117
|
}, [domEditSelection, preloadAgentPromptSnippet]);
|
|
@@ -3037,6 +3126,7 @@ export function StudioApp() {
|
|
|
3037
3126
|
selection: domEditSelection,
|
|
3038
3127
|
currentTime,
|
|
3039
3128
|
tagSnippet,
|
|
3129
|
+
selectionContext: agentPromptSelectionContext,
|
|
3040
3130
|
userInstruction,
|
|
3041
3131
|
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
|
3042
3132
|
});
|
|
@@ -3048,11 +3138,21 @@ export function StudioApp() {
|
|
|
3048
3138
|
}
|
|
3049
3139
|
|
|
3050
3140
|
setAgentModalOpen(false);
|
|
3141
|
+
setAgentPromptSelectionContext(undefined);
|
|
3142
|
+
setAgentModalAnchorPoint(null);
|
|
3051
3143
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
3052
3144
|
setCopiedAgentPrompt(true);
|
|
3053
3145
|
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
|
3054
3146
|
},
|
|
3055
|
-
[
|
|
3147
|
+
[
|
|
3148
|
+
activeCompPath,
|
|
3149
|
+
agentPromptSelectionContext,
|
|
3150
|
+
agentPromptTagSnippet,
|
|
3151
|
+
currentTime,
|
|
3152
|
+
domEditSelection,
|
|
3153
|
+
projectDir,
|
|
3154
|
+
showToast,
|
|
3155
|
+
],
|
|
3056
3156
|
);
|
|
3057
3157
|
|
|
3058
3158
|
const handlePreviewIframeRef = useCallback(
|
|
@@ -3070,9 +3170,9 @@ export function StudioApp() {
|
|
|
3070
3170
|
|
|
3071
3171
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
3072
3172
|
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3073
|
-
if (!
|
|
3173
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
|
3074
3174
|
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
3075
|
-
preferClipAncestor: options?.preferClipAncestor ??
|
|
3175
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
3076
3176
|
});
|
|
3077
3177
|
if (!nextSelection) {
|
|
3078
3178
|
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
|
@@ -3080,14 +3180,35 @@ export function StudioApp() {
|
|
|
3080
3180
|
}
|
|
3081
3181
|
e.preventDefault();
|
|
3082
3182
|
e.stopPropagation();
|
|
3183
|
+
const localPointer = previewIframeRef.current
|
|
3184
|
+
? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
|
|
3185
|
+
: null;
|
|
3083
3186
|
applyDomSelection(nextSelection, { additive: e.shiftKey });
|
|
3187
|
+
if (
|
|
3188
|
+
!e.shiftKey &&
|
|
3189
|
+
localPointer &&
|
|
3190
|
+
isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
|
|
3191
|
+
) {
|
|
3192
|
+
setAgentPromptSelectionContext(
|
|
3193
|
+
buildRasterClickSelectionContext(nextSelection, localPointer),
|
|
3194
|
+
);
|
|
3195
|
+
setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
3196
|
+
void preloadAgentPromptSnippet(nextSelection);
|
|
3197
|
+
setAgentModalOpen(true);
|
|
3198
|
+
}
|
|
3084
3199
|
},
|
|
3085
|
-
[
|
|
3200
|
+
[
|
|
3201
|
+
applyDomSelection,
|
|
3202
|
+
captionEditMode,
|
|
3203
|
+
compositionLoading,
|
|
3204
|
+
preloadAgentPromptSnippet,
|
|
3205
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3206
|
+
],
|
|
3086
3207
|
);
|
|
3087
3208
|
|
|
3088
3209
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
3089
3210
|
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3090
|
-
if (!
|
|
3211
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
|
|
3091
3212
|
updateDomEditHoverSelection(null);
|
|
3092
3213
|
return null;
|
|
3093
3214
|
}
|
|
@@ -3098,7 +3219,12 @@ export function StudioApp() {
|
|
|
3098
3219
|
updateDomEditHoverSelection(nextSelection);
|
|
3099
3220
|
return nextSelection;
|
|
3100
3221
|
},
|
|
3101
|
-
[
|
|
3222
|
+
[
|
|
3223
|
+
captionEditMode,
|
|
3224
|
+
compositionLoading,
|
|
3225
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3226
|
+
updateDomEditHoverSelection,
|
|
3227
|
+
],
|
|
3102
3228
|
);
|
|
3103
3229
|
|
|
3104
3230
|
const handlePreviewCanvasPointerLeave = useCallback(() => {
|
|
@@ -3965,9 +4091,8 @@ export function StudioApp() {
|
|
|
3965
4091
|
onInspectTimelineElement={handleTimelineElementInspect}
|
|
3966
4092
|
inspectedTimelineElementId={inspectedTimelineElementId}
|
|
3967
4093
|
timelineLayerChildCounts={timelineLayerChildCounts}
|
|
3968
|
-
thumbnailedTimelineElementIds={thumbnailedTimelineElementIds}
|
|
3969
|
-
onToggleTimelineElementThumbnail={handleToggleTimelineElementThumbnail}
|
|
3970
4094
|
onCompIdToSrcChange={setCompIdToSrc}
|
|
4095
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
3971
4096
|
onCompositionChange={(compPath) => {
|
|
3972
4097
|
// Sync activeCompPath when user drills down via timeline double-click
|
|
3973
4098
|
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
|
@@ -3984,7 +4109,7 @@ export function StudioApp() {
|
|
|
3984
4109
|
iframeRef={previewIframeRef}
|
|
3985
4110
|
activeCompositionPath={activeCompPath}
|
|
3986
4111
|
hoverSelection={
|
|
3987
|
-
|
|
4112
|
+
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
|
|
3988
4113
|
? domEditHoverSelection
|
|
3989
4114
|
: null
|
|
3990
4115
|
}
|
|
@@ -4090,21 +4215,18 @@ export function StudioApp() {
|
|
|
4090
4215
|
<div className="min-h-0 flex-1">
|
|
4091
4216
|
{designPanelActive ? (
|
|
4092
4217
|
<PropertyPanel
|
|
4093
|
-
projectId={projectId}
|
|
4094
|
-
assets={assets}
|
|
4095
4218
|
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
4219
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
4096
4220
|
copiedAgentPrompt={copiedAgentPrompt}
|
|
4097
4221
|
onClearSelection={clearDomSelection}
|
|
4098
4222
|
onSetStyle={handleDomStyleCommit}
|
|
4099
4223
|
onSetManualOffset={handleDomPathOffsetCommit}
|
|
4100
4224
|
onSetManualSize={handleDomBoxSizeCommit}
|
|
4225
|
+
onSetRotation={handleDomRotationCommit}
|
|
4101
4226
|
onSetText={handleDomTextCommit}
|
|
4102
4227
|
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
|
4103
|
-
onAddTextField={handleDomAddTextField}
|
|
4104
|
-
onRemoveTextField={handleDomRemoveTextField}
|
|
4105
4228
|
onResetManualEdits={handleDomManualEditsReset}
|
|
4106
4229
|
onAskAgent={handleAskAgent}
|
|
4107
|
-
onImportAssets={handleImportFiles}
|
|
4108
4230
|
fontAssets={fontAssets}
|
|
4109
4231
|
onImportFonts={handleImportFonts}
|
|
4110
4232
|
/>
|
|
@@ -4122,9 +4244,9 @@ export function StudioApp() {
|
|
|
4122
4244
|
projectId={projectId}
|
|
4123
4245
|
onDelete={renderQueue.deleteRender}
|
|
4124
4246
|
onClearCompleted={renderQueue.clearCompleted}
|
|
4125
|
-
onStartRender={async (format, quality) => {
|
|
4247
|
+
onStartRender={async (format, quality, resolution, fps) => {
|
|
4126
4248
|
await waitForPendingDomEditSaves();
|
|
4127
|
-
await renderQueue.startRender(
|
|
4249
|
+
await renderQueue.startRender({ fps, quality, format, resolution });
|
|
4128
4250
|
}}
|
|
4129
4251
|
isRendering={renderQueue.isRendering}
|
|
4130
4252
|
/>
|
|
@@ -4155,8 +4277,13 @@ export function StudioApp() {
|
|
|
4155
4277
|
{agentModalOpen && domEditSelection && (
|
|
4156
4278
|
<AskAgentModal
|
|
4157
4279
|
selectionLabel={domEditSelection.label}
|
|
4280
|
+
anchorPoint={agentModalAnchorPoint}
|
|
4158
4281
|
onSubmit={handleAgentModalSubmit}
|
|
4159
|
-
onClose={() =>
|
|
4282
|
+
onClose={() => {
|
|
4283
|
+
setAgentModalOpen(false);
|
|
4284
|
+
setAgentPromptSelectionContext(undefined);
|
|
4285
|
+
setAgentModalAnchorPoint(null);
|
|
4286
|
+
}}
|
|
4160
4287
|
/>
|
|
4161
4288
|
)}
|
|
4162
4289
|
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
buildStrokeWidthStyleUpdates,
|
|
5
5
|
getClipPathInsetPx,
|
|
6
6
|
getCssFilterFunctionPx,
|
|
7
|
+
getPropertyPanelVisibleSections,
|
|
7
8
|
inferBoxShadowPreset,
|
|
8
9
|
inferClipPathPreset,
|
|
10
|
+
isPropertyPanelMediaLikeSelection,
|
|
9
11
|
normalizePanelPxValue,
|
|
10
12
|
setCssFilterFunctionPx,
|
|
11
13
|
} from "./PropertyPanel";
|
|
@@ -64,4 +66,51 @@ describe("PropertyPanel style helpers", () => {
|
|
|
64
66
|
expect(buildStrokeStyleUpdates("none", "4px")).toEqual([["border-style", "none"]]);
|
|
65
67
|
expect(buildStrokeStyleUpdates("solid", "4px")).toEqual([["border-style", "solid"]]);
|
|
66
68
|
});
|
|
69
|
+
|
|
70
|
+
it("orders the simplified default inspector sections around high-confidence edits", () => {
|
|
71
|
+
expect(
|
|
72
|
+
getPropertyPanelVisibleSections({
|
|
73
|
+
hasSelection: true,
|
|
74
|
+
canEditStyles: true,
|
|
75
|
+
hasTextControls: true,
|
|
76
|
+
hasColorControls: true,
|
|
77
|
+
}),
|
|
78
|
+
).toEqual(["Text", "Layout", "Colors", "Radius", "Shadow"]);
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
getPropertyPanelVisibleSections({
|
|
82
|
+
hasSelection: true,
|
|
83
|
+
canEditStyles: true,
|
|
84
|
+
hasTextControls: false,
|
|
85
|
+
hasColorControls: false,
|
|
86
|
+
}),
|
|
87
|
+
).toEqual(["Layout", "Radius", "Shadow"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("treats media tags and background-image layers as image-like controls", () => {
|
|
91
|
+
expect(
|
|
92
|
+
isPropertyPanelMediaLikeSelection({
|
|
93
|
+
tagName: "img",
|
|
94
|
+
styles: {},
|
|
95
|
+
}),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
|
|
98
|
+
expect(
|
|
99
|
+
isPropertyPanelMediaLikeSelection({
|
|
100
|
+
tagName: "div",
|
|
101
|
+
styles: {
|
|
102
|
+
"background-image": "url(/assets/studio.png)",
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
isPropertyPanelMediaLikeSelection({
|
|
109
|
+
tagName: "div",
|
|
110
|
+
styles: {
|
|
111
|
+
"background-image": "none",
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
).toBe(false);
|
|
115
|
+
});
|
|
67
116
|
});
|