@hyperframes/studio 0.6.0-alpha.2 → 0.6.0-alpha.4
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-BfnyZllX.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 +217 -98
- 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 +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 +49 -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
|
+
}
|
|
388
446
|
|
|
389
|
-
return getEventTargetElement(doc.elementFromPoint(
|
|
447
|
+
return getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
448
|
+
}
|
|
449
|
+
|
|
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,16 @@ 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 [thumbnailedTimelineElementIds, setThumbnailedTimelineElementIds] = useState<
|
|
782
|
-
ReadonlySet<string>
|
|
783
|
-
>(() => new Set());
|
|
784
897
|
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
|
785
898
|
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
786
899
|
setPreviewDocumentVersion((version) => version + 1);
|
|
@@ -1771,6 +1884,8 @@ export function StudioApp() {
|
|
|
1771
1884
|
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
1772
1885
|
) => {
|
|
1773
1886
|
setAgentPromptTagSnippet(undefined);
|
|
1887
|
+
setAgentPromptSelectionContext(undefined);
|
|
1888
|
+
setAgentModalAnchorPoint(null);
|
|
1774
1889
|
setCopiedAgentPrompt(false);
|
|
1775
1890
|
if (!selection) {
|
|
1776
1891
|
domEditSelectionRef.current = null;
|
|
@@ -2296,13 +2411,13 @@ export function StudioApp() {
|
|
|
2296
2411
|
(clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
|
|
2297
2412
|
const iframe = previewIframeRef.current;
|
|
2298
2413
|
if (!iframe || captionEditMode) return null;
|
|
2299
|
-
const target = getPreviewTargetFromPointer(iframe, clientX, clientY);
|
|
2414
|
+
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
2300
2415
|
if (!target) return null;
|
|
2301
2416
|
return buildDomSelectionFromTarget(target, {
|
|
2302
2417
|
preferClipAncestor: options?.preferClipAncestor,
|
|
2303
2418
|
});
|
|
2304
2419
|
},
|
|
2305
|
-
[buildDomSelectionFromTarget, captionEditMode],
|
|
2420
|
+
[activeCompPath, buildDomSelectionFromTarget, captionEditMode],
|
|
2306
2421
|
);
|
|
2307
2422
|
|
|
2308
2423
|
const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
|
|
@@ -2398,8 +2513,21 @@ export function StudioApp() {
|
|
|
2398
2513
|
|
|
2399
2514
|
const selection = buildDomSelectionForTimelineElement(element);
|
|
2400
2515
|
if (selection) applyDomSelection(selection);
|
|
2516
|
+
|
|
2517
|
+
const key = getTimelineElementKey(element);
|
|
2518
|
+
if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
|
|
2519
|
+
setInspectedTimelineElementId(key);
|
|
2520
|
+
setLeftCollapsed(false);
|
|
2521
|
+
|
|
2522
|
+
const iframe = previewIframeRef.current;
|
|
2523
|
+
if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
|
|
2524
|
+
seekStudioPreview(iframe, element.start);
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
setInspectedTimelineElementId(null);
|
|
2528
|
+
}
|
|
2401
2529
|
},
|
|
2402
|
-
[applyDomSelection, buildDomSelectionForTimelineElement],
|
|
2530
|
+
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
|
|
2403
2531
|
);
|
|
2404
2532
|
|
|
2405
2533
|
const handleTimelineElementInspect = useCallback(
|
|
@@ -2426,17 +2554,6 @@ export function StudioApp() {
|
|
|
2426
2554
|
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
|
|
2427
2555
|
);
|
|
2428
2556
|
|
|
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
2557
|
const handleTimelineLayerSelect = useCallback(
|
|
2441
2558
|
(layer: DomEditLayerItem) => {
|
|
2442
2559
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
@@ -2816,15 +2933,26 @@ export function StudioApp() {
|
|
|
2816
2933
|
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
2817
2934
|
);
|
|
2818
2935
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2936
|
+
try {
|
|
2937
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
2938
|
+
label: "Edit layer style",
|
|
2939
|
+
skipRefresh: true,
|
|
2940
|
+
prepareContent: importedFont
|
|
2941
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
2942
|
+
: undefined,
|
|
2943
|
+
});
|
|
2944
|
+
} catch (err) {
|
|
2945
|
+
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
|
2946
|
+
}
|
|
2947
|
+
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
2826
2948
|
},
|
|
2827
|
-
[
|
|
2949
|
+
[
|
|
2950
|
+
activeCompPath,
|
|
2951
|
+
domEditSelection,
|
|
2952
|
+
persistDomEditOperations,
|
|
2953
|
+
refreshDomEditSelectionFromPreview,
|
|
2954
|
+
resolveImportedFontAsset,
|
|
2955
|
+
],
|
|
2828
2956
|
);
|
|
2829
2957
|
|
|
2830
2958
|
const handleDomTextCommit = useCallback(
|
|
@@ -2978,51 +3106,11 @@ export function StudioApp() {
|
|
|
2978
3106
|
[commitDomTextFields, domEditSelection, handleDomStyleCommit, resolveImportedFontAsset],
|
|
2979
3107
|
);
|
|
2980
3108
|
|
|
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
3109
|
const handleAskAgent = useCallback(() => {
|
|
3024
3110
|
if (!domEditSelection) return;
|
|
3025
3111
|
setAgentPromptTagSnippet(undefined);
|
|
3112
|
+
setAgentPromptSelectionContext(undefined);
|
|
3113
|
+
setAgentModalAnchorPoint(null);
|
|
3026
3114
|
void preloadAgentPromptSnippet(domEditSelection);
|
|
3027
3115
|
setAgentModalOpen(true);
|
|
3028
3116
|
}, [domEditSelection, preloadAgentPromptSnippet]);
|
|
@@ -3037,6 +3125,7 @@ export function StudioApp() {
|
|
|
3037
3125
|
selection: domEditSelection,
|
|
3038
3126
|
currentTime,
|
|
3039
3127
|
tagSnippet,
|
|
3128
|
+
selectionContext: agentPromptSelectionContext,
|
|
3040
3129
|
userInstruction,
|
|
3041
3130
|
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
|
3042
3131
|
});
|
|
@@ -3048,11 +3137,21 @@ export function StudioApp() {
|
|
|
3048
3137
|
}
|
|
3049
3138
|
|
|
3050
3139
|
setAgentModalOpen(false);
|
|
3140
|
+
setAgentPromptSelectionContext(undefined);
|
|
3141
|
+
setAgentModalAnchorPoint(null);
|
|
3051
3142
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
3052
3143
|
setCopiedAgentPrompt(true);
|
|
3053
3144
|
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
|
3054
3145
|
},
|
|
3055
|
-
[
|
|
3146
|
+
[
|
|
3147
|
+
activeCompPath,
|
|
3148
|
+
agentPromptSelectionContext,
|
|
3149
|
+
agentPromptTagSnippet,
|
|
3150
|
+
currentTime,
|
|
3151
|
+
domEditSelection,
|
|
3152
|
+
projectDir,
|
|
3153
|
+
showToast,
|
|
3154
|
+
],
|
|
3056
3155
|
);
|
|
3057
3156
|
|
|
3058
3157
|
const handlePreviewIframeRef = useCallback(
|
|
@@ -3070,9 +3169,9 @@ export function StudioApp() {
|
|
|
3070
3169
|
|
|
3071
3170
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
3072
3171
|
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3073
|
-
if (!
|
|
3172
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode) return;
|
|
3074
3173
|
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
3075
|
-
preferClipAncestor: options?.preferClipAncestor ??
|
|
3174
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
3076
3175
|
});
|
|
3077
3176
|
if (!nextSelection) {
|
|
3078
3177
|
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
|
@@ -3080,14 +3179,34 @@ export function StudioApp() {
|
|
|
3080
3179
|
}
|
|
3081
3180
|
e.preventDefault();
|
|
3082
3181
|
e.stopPropagation();
|
|
3182
|
+
const localPointer = previewIframeRef.current
|
|
3183
|
+
? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
|
|
3184
|
+
: null;
|
|
3083
3185
|
applyDomSelection(nextSelection, { additive: e.shiftKey });
|
|
3186
|
+
if (
|
|
3187
|
+
!e.shiftKey &&
|
|
3188
|
+
localPointer &&
|
|
3189
|
+
isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
|
|
3190
|
+
) {
|
|
3191
|
+
setAgentPromptSelectionContext(
|
|
3192
|
+
buildRasterClickSelectionContext(nextSelection, localPointer),
|
|
3193
|
+
);
|
|
3194
|
+
setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
3195
|
+
void preloadAgentPromptSnippet(nextSelection);
|
|
3196
|
+
setAgentModalOpen(true);
|
|
3197
|
+
}
|
|
3084
3198
|
},
|
|
3085
|
-
[
|
|
3199
|
+
[
|
|
3200
|
+
applyDomSelection,
|
|
3201
|
+
captionEditMode,
|
|
3202
|
+
preloadAgentPromptSnippet,
|
|
3203
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3204
|
+
],
|
|
3086
3205
|
);
|
|
3087
3206
|
|
|
3088
3207
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
3089
3208
|
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3090
|
-
if (!
|
|
3209
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode) {
|
|
3091
3210
|
updateDomEditHoverSelection(null);
|
|
3092
3211
|
return null;
|
|
3093
3212
|
}
|
|
@@ -3965,8 +4084,6 @@ export function StudioApp() {
|
|
|
3965
4084
|
onInspectTimelineElement={handleTimelineElementInspect}
|
|
3966
4085
|
inspectedTimelineElementId={inspectedTimelineElementId}
|
|
3967
4086
|
timelineLayerChildCounts={timelineLayerChildCounts}
|
|
3968
|
-
thumbnailedTimelineElementIds={thumbnailedTimelineElementIds}
|
|
3969
|
-
onToggleTimelineElementThumbnail={handleToggleTimelineElementThumbnail}
|
|
3970
4087
|
onCompIdToSrcChange={setCompIdToSrc}
|
|
3971
4088
|
onCompositionChange={(compPath) => {
|
|
3972
4089
|
// Sync activeCompPath when user drills down via timeline double-click
|
|
@@ -3984,7 +4101,7 @@ export function StudioApp() {
|
|
|
3984
4101
|
iframeRef={previewIframeRef}
|
|
3985
4102
|
activeCompositionPath={activeCompPath}
|
|
3986
4103
|
hoverSelection={
|
|
3987
|
-
|
|
4104
|
+
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
|
|
3988
4105
|
? domEditHoverSelection
|
|
3989
4106
|
: null
|
|
3990
4107
|
}
|
|
@@ -4090,21 +4207,18 @@ export function StudioApp() {
|
|
|
4090
4207
|
<div className="min-h-0 flex-1">
|
|
4091
4208
|
{designPanelActive ? (
|
|
4092
4209
|
<PropertyPanel
|
|
4093
|
-
projectId={projectId}
|
|
4094
|
-
assets={assets}
|
|
4095
4210
|
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
4211
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
4096
4212
|
copiedAgentPrompt={copiedAgentPrompt}
|
|
4097
4213
|
onClearSelection={clearDomSelection}
|
|
4098
4214
|
onSetStyle={handleDomStyleCommit}
|
|
4099
4215
|
onSetManualOffset={handleDomPathOffsetCommit}
|
|
4100
4216
|
onSetManualSize={handleDomBoxSizeCommit}
|
|
4217
|
+
onSetRotation={handleDomRotationCommit}
|
|
4101
4218
|
onSetText={handleDomTextCommit}
|
|
4102
4219
|
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
|
4103
|
-
onAddTextField={handleDomAddTextField}
|
|
4104
|
-
onRemoveTextField={handleDomRemoveTextField}
|
|
4105
4220
|
onResetManualEdits={handleDomManualEditsReset}
|
|
4106
4221
|
onAskAgent={handleAskAgent}
|
|
4107
|
-
onImportAssets={handleImportFiles}
|
|
4108
4222
|
fontAssets={fontAssets}
|
|
4109
4223
|
onImportFonts={handleImportFonts}
|
|
4110
4224
|
/>
|
|
@@ -4122,9 +4236,9 @@ export function StudioApp() {
|
|
|
4122
4236
|
projectId={projectId}
|
|
4123
4237
|
onDelete={renderQueue.deleteRender}
|
|
4124
4238
|
onClearCompleted={renderQueue.clearCompleted}
|
|
4125
|
-
onStartRender={async (format, quality) => {
|
|
4239
|
+
onStartRender={async (format, quality, resolution, fps) => {
|
|
4126
4240
|
await waitForPendingDomEditSaves();
|
|
4127
|
-
await renderQueue.startRender(
|
|
4241
|
+
await renderQueue.startRender({ fps, quality, format, resolution });
|
|
4128
4242
|
}}
|
|
4129
4243
|
isRendering={renderQueue.isRendering}
|
|
4130
4244
|
/>
|
|
@@ -4155,8 +4269,13 @@ export function StudioApp() {
|
|
|
4155
4269
|
{agentModalOpen && domEditSelection && (
|
|
4156
4270
|
<AskAgentModal
|
|
4157
4271
|
selectionLabel={domEditSelection.label}
|
|
4272
|
+
anchorPoint={agentModalAnchorPoint}
|
|
4158
4273
|
onSubmit={handleAgentModalSubmit}
|
|
4159
|
-
onClose={() =>
|
|
4274
|
+
onClose={() => {
|
|
4275
|
+
setAgentModalOpen(false);
|
|
4276
|
+
setAgentPromptSelectionContext(undefined);
|
|
4277
|
+
setAgentModalAnchorPoint(null);
|
|
4278
|
+
}}
|
|
4160
4279
|
/>
|
|
4161
4280
|
)}
|
|
4162
4281
|
|
|
@@ -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
|
});
|