@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
package/src/App.tsx
CHANGED
|
@@ -4,14 +4,15 @@ 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";
|
|
10
11
|
import { useMountEffect } from "./hooks/useMountEffect";
|
|
11
12
|
import { NLELayout } from "./components/nle/NLELayout";
|
|
12
13
|
import { SourceEditor } from "./components/editor/SourceEditor";
|
|
13
|
-
import { LeftSidebar } from "./components/sidebar/LeftSidebar";
|
|
14
|
-
import { RenderQueue } from "./components/renders/RenderQueue";
|
|
14
|
+
import { LeftSidebar, type LeftSidebarHandle } from "./components/sidebar/LeftSidebar";
|
|
15
|
+
import { RenderQueue, type CompositionDimensions } from "./components/renders/RenderQueue";
|
|
15
16
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
16
17
|
import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player";
|
|
17
18
|
import { AudioWaveform } from "./player/components/AudioWaveform";
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
} from "./player/components/timelineZoom";
|
|
56
57
|
import {
|
|
57
58
|
getTimelineToggleTitle,
|
|
59
|
+
isEditableTarget,
|
|
58
60
|
shouldHandleTimelineToggleHotkey,
|
|
59
61
|
} from "./utils/timelineDiscovery";
|
|
60
62
|
import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "./utils/frameCapture";
|
|
@@ -78,10 +80,10 @@ import {
|
|
|
78
80
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
79
81
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
80
82
|
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
83
|
+
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
81
84
|
STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED,
|
|
82
85
|
} from "./components/editor/manualEditingAvailability";
|
|
83
86
|
import {
|
|
84
|
-
buildDefaultDomEditTextField,
|
|
85
87
|
buildDomEditStylePatchOperation,
|
|
86
88
|
buildDomEditTextPatchOperation,
|
|
87
89
|
buildElementAgentPrompt,
|
|
@@ -91,12 +93,16 @@ import {
|
|
|
91
93
|
findElementForTimelineElement,
|
|
92
94
|
getDomEditLayerKey,
|
|
93
95
|
getDomEditTargetKey,
|
|
96
|
+
isLargeRasterDomEditSelection,
|
|
94
97
|
isTextEditableSelection,
|
|
98
|
+
resolveVisualDomEditSelectionTarget,
|
|
95
99
|
serializeDomEditTextFields,
|
|
96
100
|
resolveDomEditSelection,
|
|
101
|
+
type DomEditViewport,
|
|
97
102
|
type DomEditLayerItem,
|
|
98
103
|
type DomEditTextField,
|
|
99
104
|
type DomEditSelection,
|
|
105
|
+
buildDefaultDomEditTextField,
|
|
100
106
|
} from "./components/editor/domEditing";
|
|
101
107
|
import {
|
|
102
108
|
STUDIO_MANUAL_EDITS_PATH,
|
|
@@ -358,10 +364,64 @@ function isManualGeometryStyleProperty(property: string): boolean {
|
|
|
358
364
|
return property === "left" || property === "top" || property === "width" || property === "height";
|
|
359
365
|
}
|
|
360
366
|
|
|
367
|
+
interface PreviewLocalPointer {
|
|
368
|
+
x: number;
|
|
369
|
+
y: number;
|
|
370
|
+
viewport: DomEditViewport;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface AgentModalAnchorPoint {
|
|
374
|
+
x: number;
|
|
375
|
+
y: number;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function resolvePreviewLocalPointer(
|
|
379
|
+
iframe: HTMLIFrameElement,
|
|
380
|
+
doc: Document,
|
|
381
|
+
win: Window,
|
|
382
|
+
clientX: number,
|
|
383
|
+
clientY: number,
|
|
384
|
+
): PreviewLocalPointer | null {
|
|
385
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
386
|
+
const root =
|
|
387
|
+
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
388
|
+
const rootRect = root?.getBoundingClientRect();
|
|
389
|
+
const rootWidth = rootRect?.width || win.innerWidth;
|
|
390
|
+
const rootHeight = rootRect?.height || win.innerHeight;
|
|
391
|
+
if (!rootWidth || !rootHeight) return null;
|
|
392
|
+
|
|
393
|
+
const scaleX = iframeRect.width / rootWidth;
|
|
394
|
+
const scaleY = iframeRect.height / rootHeight;
|
|
395
|
+
return {
|
|
396
|
+
x: (clientX - iframeRect.left) / scaleX,
|
|
397
|
+
y: (clientY - iframeRect.top) / scaleY,
|
|
398
|
+
viewport: { width: rootWidth, height: rootHeight },
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getPreviewLocalPointer(
|
|
403
|
+
iframe: HTMLIFrameElement,
|
|
404
|
+
clientX: number,
|
|
405
|
+
clientY: number,
|
|
406
|
+
): PreviewLocalPointer | null {
|
|
407
|
+
let doc: Document | null = null;
|
|
408
|
+
let win: Window | null = null;
|
|
409
|
+
try {
|
|
410
|
+
doc = iframe.contentDocument;
|
|
411
|
+
win = iframe.contentWindow;
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (!doc || !win) return null;
|
|
416
|
+
|
|
417
|
+
return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
418
|
+
}
|
|
419
|
+
|
|
361
420
|
function getPreviewTargetFromPointer(
|
|
362
421
|
iframe: HTMLIFrameElement,
|
|
363
422
|
clientX: number,
|
|
364
423
|
clientY: number,
|
|
424
|
+
activeCompositionPath: string | null,
|
|
365
425
|
): HTMLElement | null {
|
|
366
426
|
let doc: Document | null = null;
|
|
367
427
|
let win: Window | null = null;
|
|
@@ -373,20 +433,35 @@ function getPreviewTargetFromPointer(
|
|
|
373
433
|
}
|
|
374
434
|
if (!doc || !win) return null;
|
|
375
435
|
|
|
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;
|
|
436
|
+
const localPointer = resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
437
|
+
if (!localPointer) return null;
|
|
383
438
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
439
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
440
|
+
const visualTarget = resolveVisualDomEditSelectionTarget(
|
|
441
|
+
doc.elementsFromPoint(localPointer.x, localPointer.y),
|
|
442
|
+
{
|
|
443
|
+
activeCompositionPath,
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
if (visualTarget) return visualTarget;
|
|
447
|
+
}
|
|
388
448
|
|
|
389
|
-
return getEventTargetElement(doc.elementFromPoint(
|
|
449
|
+
return getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildRasterClickSelectionContext(
|
|
453
|
+
selection: DomEditSelection,
|
|
454
|
+
localPointer: PreviewLocalPointer,
|
|
455
|
+
): string {
|
|
456
|
+
return [
|
|
457
|
+
"The user clicked a large raster/background element in the Studio preview.",
|
|
458
|
+
`Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round(
|
|
459
|
+
localPointer.viewport.width,
|
|
460
|
+
)}x${Math.round(localPointer.viewport.height)} composition.`,
|
|
461
|
+
`Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`,
|
|
462
|
+
"Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.",
|
|
463
|
+
"If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.",
|
|
464
|
+
].join("\n");
|
|
390
465
|
}
|
|
391
466
|
|
|
392
467
|
function domEditSelectionsTargetSame(
|
|
@@ -592,17 +667,47 @@ function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number |
|
|
|
592
667
|
|
|
593
668
|
// ── Ask Agent Modal ──
|
|
594
669
|
|
|
670
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
671
|
+
if (max < min) return min;
|
|
672
|
+
return Math.min(Math.max(value, min), max);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function getAgentModalPositionStyle(
|
|
676
|
+
anchorPoint: AgentModalAnchorPoint | null,
|
|
677
|
+
): CSSProperties | undefined {
|
|
678
|
+
if (!anchorPoint || typeof window === "undefined") return undefined;
|
|
679
|
+
|
|
680
|
+
const modalWidth = 480;
|
|
681
|
+
const estimatedModalHeight = 270;
|
|
682
|
+
const margin = 16;
|
|
683
|
+
const left = clampNumber(
|
|
684
|
+
anchorPoint.x,
|
|
685
|
+
margin + modalWidth / 2,
|
|
686
|
+
window.innerWidth - margin - modalWidth / 2,
|
|
687
|
+
);
|
|
688
|
+
const top = clampNumber(
|
|
689
|
+
anchorPoint.y + 12,
|
|
690
|
+
margin,
|
|
691
|
+
window.innerHeight - margin - estimatedModalHeight,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
return { left, top, transform: "translateX(-50%)" };
|
|
695
|
+
}
|
|
696
|
+
|
|
595
697
|
function AskAgentModal({
|
|
596
698
|
selectionLabel,
|
|
699
|
+
anchorPoint = null,
|
|
597
700
|
onSubmit,
|
|
598
701
|
onClose,
|
|
599
702
|
}: {
|
|
600
703
|
selectionLabel: string;
|
|
704
|
+
anchorPoint?: AgentModalAnchorPoint | null;
|
|
601
705
|
onSubmit: (instruction: string) => void;
|
|
602
706
|
onClose: () => void;
|
|
603
707
|
}) {
|
|
604
708
|
const [value, setValue] = useState("");
|
|
605
709
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
710
|
+
const modalPositionStyle = getAgentModalPositionStyle(anchorPoint);
|
|
606
711
|
|
|
607
712
|
useMountEffect(() => {
|
|
608
713
|
requestAnimationFrame(() => inputRef.current?.focus());
|
|
@@ -615,11 +720,18 @@ function AskAgentModal({
|
|
|
615
720
|
|
|
616
721
|
return (
|
|
617
722
|
<div
|
|
618
|
-
className=
|
|
723
|
+
className={
|
|
724
|
+
anchorPoint
|
|
725
|
+
? "fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
726
|
+
: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
727
|
+
}
|
|
619
728
|
onClick={onClose}
|
|
620
729
|
>
|
|
621
730
|
<div
|
|
622
|
-
className=
|
|
731
|
+
className={`w-[480px] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl ${
|
|
732
|
+
anchorPoint ? "fixed" : ""
|
|
733
|
+
}`}
|
|
734
|
+
style={modalPositionStyle}
|
|
623
735
|
onClick={(e) => e.stopPropagation()}
|
|
624
736
|
>
|
|
625
737
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
@@ -774,13 +886,17 @@ export function StudioApp() {
|
|
|
774
886
|
const [domEditGroupSelections, setDomEditGroupSelections] = useState<DomEditSelection[]>([]);
|
|
775
887
|
const [domEditHoverSelection, setDomEditHoverSelection] = useState<DomEditSelection | null>(null);
|
|
776
888
|
const [agentPromptTagSnippet, setAgentPromptTagSnippet] = useState<string | undefined>();
|
|
889
|
+
const [agentPromptSelectionContext, setAgentPromptSelectionContext] = useState<
|
|
890
|
+
string | undefined
|
|
891
|
+
>();
|
|
892
|
+
const [agentModalAnchorPoint, setAgentModalAnchorPoint] = useState<AgentModalAnchorPoint | null>(
|
|
893
|
+
null,
|
|
894
|
+
);
|
|
777
895
|
const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
|
|
778
896
|
const [agentModalOpen, setAgentModalOpen] = useState(false);
|
|
779
897
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
780
898
|
const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
|
|
781
|
-
const [
|
|
782
|
-
ReadonlySet<string>
|
|
783
|
-
>(() => new Set());
|
|
899
|
+
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
784
900
|
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
|
785
901
|
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
786
902
|
setPreviewDocumentVersion((version) => version + 1);
|
|
@@ -906,6 +1022,28 @@ export function StudioApp() {
|
|
|
906
1022
|
setRightCollapsed(!captionHasSelection);
|
|
907
1023
|
}
|
|
908
1024
|
}, [captionHasSelection, captionEditMode]);
|
|
1025
|
+
|
|
1026
|
+
// Track the active composition's authored dimensions so the render
|
|
1027
|
+
// dropdown can derive landscape vs portrait. The runtime emits
|
|
1028
|
+
// `stage-size` after `applyCompositionSizing` resolves the authoritative
|
|
1029
|
+
// dims, so we use that instead of re-parsing the iframe DOM.
|
|
1030
|
+
const [compositionDimensions, setCompositionDimensions] = useState<CompositionDimensions | null>(
|
|
1031
|
+
null,
|
|
1032
|
+
);
|
|
1033
|
+
useMountEffect(() => {
|
|
1034
|
+
const handleMessage = (e: MessageEvent) => {
|
|
1035
|
+
const data = e.data;
|
|
1036
|
+
if (data?.source !== "hf-preview" || data?.type !== "stage-size") return;
|
|
1037
|
+
const { width, height } = data as { width: number; height: number };
|
|
1038
|
+
if (!(width > 0) || !(height > 0)) return;
|
|
1039
|
+
setCompositionDimensions((prev) =>
|
|
1040
|
+
prev && prev.width === width && prev.height === height ? prev : { width, height },
|
|
1041
|
+
);
|
|
1042
|
+
};
|
|
1043
|
+
window.addEventListener("message", handleMessage);
|
|
1044
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
909
1047
|
const [globalDragOver, setGlobalDragOver] = useState(false);
|
|
910
1048
|
const [appToast, setAppToast] = useState<AppToast | null>(null);
|
|
911
1049
|
const [timelineVisible, setTimelineVisible] = useState(true);
|
|
@@ -916,6 +1054,8 @@ export function StudioApp() {
|
|
|
916
1054
|
const lastBlockedDomMoveToastAtRef = useRef(0);
|
|
917
1055
|
const importedFontAssetsRef = useRef<ImportedFontAsset[]>([]);
|
|
918
1056
|
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
|
1057
|
+
const handleAppKeyDownRef = useRef<((event: KeyboardEvent) => void) | undefined>(undefined);
|
|
1058
|
+
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
919
1059
|
const previewHistoryHotkeyCleanupRef = useRef<(() => void) | null>(null);
|
|
920
1060
|
const panelDragRef = useRef<{
|
|
921
1061
|
side: "left" | "right";
|
|
@@ -983,34 +1123,31 @@ export function StudioApp() {
|
|
|
983
1123
|
[toggleTimelineVisibility],
|
|
984
1124
|
);
|
|
985
1125
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
window.removeEventListener("keydown", handleTimelineToggleHotkey);
|
|
990
|
-
};
|
|
991
|
-
});
|
|
1126
|
+
const previewAppKeyDownHandler = useCallback((event: KeyboardEvent) => {
|
|
1127
|
+
handleAppKeyDownRef.current?.(event);
|
|
1128
|
+
}, []);
|
|
992
1129
|
|
|
993
1130
|
const syncPreviewTimelineHotkey = useCallback(
|
|
994
1131
|
(iframe: HTMLIFrameElement | null) => {
|
|
995
1132
|
const nextWindow = iframe?.contentWindow ?? null;
|
|
996
1133
|
if (previewHotkeyWindowRef.current === nextWindow) return;
|
|
997
1134
|
if (previewHotkeyWindowRef.current) {
|
|
998
|
-
previewHotkeyWindowRef.current.removeEventListener("keydown",
|
|
1135
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
|
|
999
1136
|
}
|
|
1000
1137
|
previewHotkeyWindowRef.current = nextWindow;
|
|
1001
|
-
nextWindow?.addEventListener("keydown",
|
|
1138
|
+
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
|
|
1002
1139
|
},
|
|
1003
|
-
[
|
|
1140
|
+
[previewAppKeyDownHandler],
|
|
1004
1141
|
);
|
|
1005
1142
|
|
|
1006
1143
|
useEffect(
|
|
1007
1144
|
() => () => {
|
|
1008
1145
|
if (previewHotkeyWindowRef.current) {
|
|
1009
|
-
previewHotkeyWindowRef.current.removeEventListener("keydown",
|
|
1146
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
|
|
1010
1147
|
previewHotkeyWindowRef.current = null;
|
|
1011
1148
|
}
|
|
1012
1149
|
},
|
|
1013
|
-
[
|
|
1150
|
+
[previewAppKeyDownHandler],
|
|
1014
1151
|
);
|
|
1015
1152
|
|
|
1016
1153
|
const renderClipContent = useCallback(
|
|
@@ -1393,6 +1530,10 @@ export function StudioApp() {
|
|
|
1393
1530
|
// Debounce the server write (600ms)
|
|
1394
1531
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
1395
1532
|
saveTimerRef.current = setTimeout(() => {
|
|
1533
|
+
// Suppress the file-change watcher echo — the save callback triggers
|
|
1534
|
+
// its own refresh, so a second one from the watcher causes a double-reload
|
|
1535
|
+
// race that can leave the player in a non-playable state.
|
|
1536
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1396
1537
|
saveProjectFilesWithHistory({
|
|
1397
1538
|
projectId: pid,
|
|
1398
1539
|
label: "Edit source",
|
|
@@ -1491,6 +1632,7 @@ export function StudioApp() {
|
|
|
1491
1632
|
throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
|
|
1492
1633
|
}
|
|
1493
1634
|
|
|
1635
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1494
1636
|
await saveProjectFilesWithHistory({
|
|
1495
1637
|
projectId: pid,
|
|
1496
1638
|
label: "Move timeline clip",
|
|
@@ -1575,6 +1717,7 @@ export function StudioApp() {
|
|
|
1575
1717
|
throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
|
|
1576
1718
|
}
|
|
1577
1719
|
|
|
1720
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1578
1721
|
await saveProjectFilesWithHistory({
|
|
1579
1722
|
projectId: pid,
|
|
1580
1723
|
label: "Resize timeline clip",
|
|
@@ -1713,6 +1856,7 @@ export function StudioApp() {
|
|
|
1713
1856
|
});
|
|
1714
1857
|
}
|
|
1715
1858
|
|
|
1859
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1716
1860
|
await saveProjectFilesWithHistory({
|
|
1717
1861
|
projectId: pid,
|
|
1718
1862
|
label: "Delete timeline clip",
|
|
@@ -1741,6 +1885,155 @@ export function StudioApp() {
|
|
|
1741
1885
|
[activeCompPath, editHistory.recordEdit, showToast, timelineElements, writeProjectFile],
|
|
1742
1886
|
);
|
|
1743
1887
|
|
|
1888
|
+
const handleDomEditElementDelete = useCallback(
|
|
1889
|
+
async (selection: DomEditSelection) => {
|
|
1890
|
+
const pid = projectIdRef.current;
|
|
1891
|
+
if (!pid) return;
|
|
1892
|
+
|
|
1893
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
1894
|
+
try {
|
|
1895
|
+
const response = await fetch(
|
|
1896
|
+
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
1897
|
+
);
|
|
1898
|
+
if (!response.ok) throw new Error(`Failed to read ${targetPath}`);
|
|
1899
|
+
|
|
1900
|
+
const data = (await response.json()) as { content?: string };
|
|
1901
|
+
const originalContent = data.content;
|
|
1902
|
+
if (typeof originalContent !== "string")
|
|
1903
|
+
throw new Error(`Missing file contents for ${targetPath}`);
|
|
1904
|
+
|
|
1905
|
+
const patchTarget: { id?: string; selector?: string; selectorIndex?: number } = selection.id
|
|
1906
|
+
? {
|
|
1907
|
+
id: selection.id,
|
|
1908
|
+
selector: selection.selector,
|
|
1909
|
+
selectorIndex: selection.selectorIndex,
|
|
1910
|
+
}
|
|
1911
|
+
: selection.selector
|
|
1912
|
+
? { selector: selection.selector, selectorIndex: selection.selectorIndex }
|
|
1913
|
+
: ({} as never);
|
|
1914
|
+
if (!patchTarget.id && !patchTarget.selector) {
|
|
1915
|
+
throw new Error("Selected element has no patchable target");
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const removeResponse = await fetch(
|
|
1919
|
+
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
|
|
1920
|
+
{
|
|
1921
|
+
method: "POST",
|
|
1922
|
+
headers: { "Content-Type": "application/json" },
|
|
1923
|
+
body: JSON.stringify({ target: patchTarget }),
|
|
1924
|
+
},
|
|
1925
|
+
);
|
|
1926
|
+
if (!removeResponse.ok) throw new Error(`Failed to delete element from ${targetPath}`);
|
|
1927
|
+
|
|
1928
|
+
const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
|
|
1929
|
+
const patchedContent =
|
|
1930
|
+
typeof removeData.content === "string" ? removeData.content : originalContent;
|
|
1931
|
+
|
|
1932
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
1933
|
+
await saveProjectFilesWithHistory({
|
|
1934
|
+
projectId: pid,
|
|
1935
|
+
label: "Delete element",
|
|
1936
|
+
kind: "timeline",
|
|
1937
|
+
files: { [targetPath]: patchedContent },
|
|
1938
|
+
readFile: async () => originalContent,
|
|
1939
|
+
writeFile: writeProjectFile,
|
|
1940
|
+
recordEdit: editHistory.recordEdit,
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
domEditSelectionRef.current = null;
|
|
1944
|
+
domEditGroupSelectionsRef.current = [];
|
|
1945
|
+
setDomEditSelection(null);
|
|
1946
|
+
setDomEditGroupSelections([]);
|
|
1947
|
+
usePlayerStore.getState().setSelectedElementId(null);
|
|
1948
|
+
setRefreshKey((k) => k + 1);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
const message = error instanceof Error ? error.message : "Failed to delete element";
|
|
1951
|
+
showToast(message);
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
[activeCompPath, editHistory.recordEdit, showToast, writeProjectFile],
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
// ── Consolidated keyboard shortcuts ────────────────────────────────
|
|
1958
|
+
// All app-level window keydown handlers live here.
|
|
1959
|
+
// Component-scoped shortcuts (playback J/K/L/Space, caption nudge)
|
|
1960
|
+
// stay in their respective hooks.
|
|
1961
|
+
const handleToggleRef = useRef(handleTimelineToggleHotkey);
|
|
1962
|
+
handleToggleRef.current = handleTimelineToggleHotkey;
|
|
1963
|
+
const handleDeleteRef = useRef(handleTimelineElementDelete);
|
|
1964
|
+
handleDeleteRef.current = handleTimelineElementDelete;
|
|
1965
|
+
const handleDomEditDeleteRef = useRef(handleDomEditElementDelete);
|
|
1966
|
+
handleDomEditDeleteRef.current = handleDomEditElementDelete;
|
|
1967
|
+
|
|
1968
|
+
handleAppKeyDownRef.current = (event: KeyboardEvent) => {
|
|
1969
|
+
// Shift+T — toggle timeline
|
|
1970
|
+
handleToggleRef.current(event);
|
|
1971
|
+
|
|
1972
|
+
// Cmd/Ctrl+Z — undo, Cmd/Ctrl+Shift+Z or Ctrl+Y — redo
|
|
1973
|
+
if (event.metaKey || event.ctrlKey) {
|
|
1974
|
+
if (!shouldIgnoreHistoryShortcut(event.target)) {
|
|
1975
|
+
const key = event.key.toLowerCase();
|
|
1976
|
+
if (key === "z" && !event.shiftKey) {
|
|
1977
|
+
event.preventDefault();
|
|
1978
|
+
void handleUndoRef.current();
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
|
|
1982
|
+
event.preventDefault();
|
|
1983
|
+
void handleRedoRef.current();
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Cmd/Ctrl+1 — sidebar: Compositions tab
|
|
1989
|
+
if (event.key === "1") {
|
|
1990
|
+
event.preventDefault();
|
|
1991
|
+
leftSidebarRef.current?.selectTab("compositions");
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Cmd/Ctrl+2 — sidebar: Assets tab
|
|
1996
|
+
if (event.key === "2") {
|
|
1997
|
+
event.preventDefault();
|
|
1998
|
+
leftSidebarRef.current?.selectTab("assets");
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Delete / Backspace — remove selected element (timeline clip or preview selection)
|
|
2004
|
+
if (
|
|
2005
|
+
(event.key === "Delete" || event.key === "Backspace") &&
|
|
2006
|
+
!event.metaKey &&
|
|
2007
|
+
!event.ctrlKey &&
|
|
2008
|
+
!event.altKey &&
|
|
2009
|
+
!isEditableTarget(event.target)
|
|
2010
|
+
) {
|
|
2011
|
+
const { selectedElementId, elements } = usePlayerStore.getState();
|
|
2012
|
+
if (selectedElementId) {
|
|
2013
|
+
const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
|
|
2014
|
+
if (element) {
|
|
2015
|
+
event.preventDefault();
|
|
2016
|
+
void handleDeleteRef.current(element);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const domSelection = domEditSelectionRef.current;
|
|
2021
|
+
if (domSelection) {
|
|
2022
|
+
event.preventDefault();
|
|
2023
|
+
void handleDomEditDeleteRef.current(domSelection);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
2029
|
+
useEffect(() => {
|
|
2030
|
+
function handleAppKeyDown(event: KeyboardEvent) {
|
|
2031
|
+
handleAppKeyDownRef.current?.(event);
|
|
2032
|
+
}
|
|
2033
|
+
window.addEventListener("keydown", handleAppKeyDown, true);
|
|
2034
|
+
return () => window.removeEventListener("keydown", handleAppKeyDown, true);
|
|
2035
|
+
}, []);
|
|
2036
|
+
|
|
1744
2037
|
const handleBlockedTimelineEdit = useCallback(
|
|
1745
2038
|
(_element: TimelineElement) => {
|
|
1746
2039
|
const now = Date.now();
|
|
@@ -1771,6 +2064,8 @@ export function StudioApp() {
|
|
|
1771
2064
|
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
1772
2065
|
) => {
|
|
1773
2066
|
setAgentPromptTagSnippet(undefined);
|
|
2067
|
+
setAgentPromptSelectionContext(undefined);
|
|
2068
|
+
setAgentModalAnchorPoint(null);
|
|
1774
2069
|
setCopiedAgentPrompt(false);
|
|
1775
2070
|
if (!selection) {
|
|
1776
2071
|
domEditSelectionRef.current = null;
|
|
@@ -2228,6 +2523,8 @@ export function StudioApp() {
|
|
|
2228
2523
|
handleUndoRef.current = handleUndo;
|
|
2229
2524
|
handleRedoRef.current = handleRedo;
|
|
2230
2525
|
|
|
2526
|
+
// History hotkey — no longer has its own window listener (consolidated
|
|
2527
|
+
// handler covers it), but kept as a named callback for iframe forwarding.
|
|
2231
2528
|
const handleHistoryHotkey = useCallback((event: KeyboardEvent) => {
|
|
2232
2529
|
if (!(event.metaKey || event.ctrlKey)) return;
|
|
2233
2530
|
if (shouldIgnoreHistoryShortcut(event.target)) return;
|
|
@@ -2243,12 +2540,6 @@ export function StudioApp() {
|
|
|
2243
2540
|
}
|
|
2244
2541
|
}, []);
|
|
2245
2542
|
|
|
2246
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
2247
|
-
useEffect(() => {
|
|
2248
|
-
window.addEventListener("keydown", handleHistoryHotkey, true);
|
|
2249
|
-
return () => window.removeEventListener("keydown", handleHistoryHotkey, true);
|
|
2250
|
-
}, [handleHistoryHotkey]);
|
|
2251
|
-
|
|
2252
2543
|
const syncPreviewHistoryHotkey = useCallback(
|
|
2253
2544
|
(iframe: HTMLIFrameElement | null) => {
|
|
2254
2545
|
previewHistoryHotkeyCleanupRef.current?.();
|
|
@@ -2296,13 +2587,13 @@ export function StudioApp() {
|
|
|
2296
2587
|
(clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
|
|
2297
2588
|
const iframe = previewIframeRef.current;
|
|
2298
2589
|
if (!iframe || captionEditMode) return null;
|
|
2299
|
-
const target = getPreviewTargetFromPointer(iframe, clientX, clientY);
|
|
2590
|
+
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
2300
2591
|
if (!target) return null;
|
|
2301
2592
|
return buildDomSelectionFromTarget(target, {
|
|
2302
2593
|
preferClipAncestor: options?.preferClipAncestor,
|
|
2303
2594
|
});
|
|
2304
2595
|
},
|
|
2305
|
-
[buildDomSelectionFromTarget, captionEditMode],
|
|
2596
|
+
[activeCompPath, buildDomSelectionFromTarget, captionEditMode],
|
|
2306
2597
|
);
|
|
2307
2598
|
|
|
2308
2599
|
const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
|
|
@@ -2398,8 +2689,21 @@ export function StudioApp() {
|
|
|
2398
2689
|
|
|
2399
2690
|
const selection = buildDomSelectionForTimelineElement(element);
|
|
2400
2691
|
if (selection) applyDomSelection(selection);
|
|
2692
|
+
|
|
2693
|
+
const key = getTimelineElementKey(element);
|
|
2694
|
+
if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
|
|
2695
|
+
setInspectedTimelineElementId(key);
|
|
2696
|
+
setLeftCollapsed(false);
|
|
2697
|
+
|
|
2698
|
+
const iframe = previewIframeRef.current;
|
|
2699
|
+
if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
|
|
2700
|
+
seekStudioPreview(iframe, element.start);
|
|
2701
|
+
}
|
|
2702
|
+
} else {
|
|
2703
|
+
setInspectedTimelineElementId(null);
|
|
2704
|
+
}
|
|
2401
2705
|
},
|
|
2402
|
-
[applyDomSelection, buildDomSelectionForTimelineElement],
|
|
2706
|
+
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
|
|
2403
2707
|
);
|
|
2404
2708
|
|
|
2405
2709
|
const handleTimelineElementInspect = useCallback(
|
|
@@ -2426,17 +2730,6 @@ export function StudioApp() {
|
|
|
2426
2730
|
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
|
|
2427
2731
|
);
|
|
2428
2732
|
|
|
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
2733
|
const handleTimelineLayerSelect = useCallback(
|
|
2441
2734
|
(layer: DomEditLayerItem) => {
|
|
2442
2735
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
@@ -2816,15 +3109,26 @@ export function StudioApp() {
|
|
|
2816
3109
|
buildDomEditStylePatchOperation("background-size", "contain"),
|
|
2817
3110
|
);
|
|
2818
3111
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3112
|
+
try {
|
|
3113
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
3114
|
+
label: "Edit layer style",
|
|
3115
|
+
skipRefresh: true,
|
|
3116
|
+
prepareContent: importedFont
|
|
3117
|
+
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
3118
|
+
: undefined,
|
|
3119
|
+
});
|
|
3120
|
+
} catch (err) {
|
|
3121
|
+
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
|
3122
|
+
}
|
|
3123
|
+
refreshDomEditSelectionFromPreview(domEditSelection);
|
|
2826
3124
|
},
|
|
2827
|
-
[
|
|
3125
|
+
[
|
|
3126
|
+
activeCompPath,
|
|
3127
|
+
domEditSelection,
|
|
3128
|
+
persistDomEditOperations,
|
|
3129
|
+
refreshDomEditSelectionFromPreview,
|
|
3130
|
+
resolveImportedFontAsset,
|
|
3131
|
+
],
|
|
2828
3132
|
);
|
|
2829
3133
|
|
|
2830
3134
|
const handleDomTextCommit = useCallback(
|
|
@@ -3023,6 +3327,8 @@ export function StudioApp() {
|
|
|
3023
3327
|
const handleAskAgent = useCallback(() => {
|
|
3024
3328
|
if (!domEditSelection) return;
|
|
3025
3329
|
setAgentPromptTagSnippet(undefined);
|
|
3330
|
+
setAgentPromptSelectionContext(undefined);
|
|
3331
|
+
setAgentModalAnchorPoint(null);
|
|
3026
3332
|
void preloadAgentPromptSnippet(domEditSelection);
|
|
3027
3333
|
setAgentModalOpen(true);
|
|
3028
3334
|
}, [domEditSelection, preloadAgentPromptSnippet]);
|
|
@@ -3037,6 +3343,7 @@ export function StudioApp() {
|
|
|
3037
3343
|
selection: domEditSelection,
|
|
3038
3344
|
currentTime,
|
|
3039
3345
|
tagSnippet,
|
|
3346
|
+
selectionContext: agentPromptSelectionContext,
|
|
3040
3347
|
userInstruction,
|
|
3041
3348
|
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
|
3042
3349
|
});
|
|
@@ -3048,11 +3355,21 @@ export function StudioApp() {
|
|
|
3048
3355
|
}
|
|
3049
3356
|
|
|
3050
3357
|
setAgentModalOpen(false);
|
|
3358
|
+
setAgentPromptSelectionContext(undefined);
|
|
3359
|
+
setAgentModalAnchorPoint(null);
|
|
3051
3360
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
3052
3361
|
setCopiedAgentPrompt(true);
|
|
3053
3362
|
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
|
3054
3363
|
},
|
|
3055
|
-
[
|
|
3364
|
+
[
|
|
3365
|
+
activeCompPath,
|
|
3366
|
+
agentPromptSelectionContext,
|
|
3367
|
+
agentPromptTagSnippet,
|
|
3368
|
+
currentTime,
|
|
3369
|
+
domEditSelection,
|
|
3370
|
+
projectDir,
|
|
3371
|
+
showToast,
|
|
3372
|
+
],
|
|
3056
3373
|
);
|
|
3057
3374
|
|
|
3058
3375
|
const handlePreviewIframeRef = useCallback(
|
|
@@ -3070,9 +3387,9 @@ export function StudioApp() {
|
|
|
3070
3387
|
|
|
3071
3388
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
3072
3389
|
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3073
|
-
if (!
|
|
3390
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
|
3074
3391
|
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
3075
|
-
preferClipAncestor: options?.preferClipAncestor ??
|
|
3392
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
3076
3393
|
});
|
|
3077
3394
|
if (!nextSelection) {
|
|
3078
3395
|
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
|
@@ -3080,14 +3397,35 @@ export function StudioApp() {
|
|
|
3080
3397
|
}
|
|
3081
3398
|
e.preventDefault();
|
|
3082
3399
|
e.stopPropagation();
|
|
3400
|
+
const localPointer = previewIframeRef.current
|
|
3401
|
+
? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
|
|
3402
|
+
: null;
|
|
3083
3403
|
applyDomSelection(nextSelection, { additive: e.shiftKey });
|
|
3404
|
+
if (
|
|
3405
|
+
!e.shiftKey &&
|
|
3406
|
+
localPointer &&
|
|
3407
|
+
isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
|
|
3408
|
+
) {
|
|
3409
|
+
setAgentPromptSelectionContext(
|
|
3410
|
+
buildRasterClickSelectionContext(nextSelection, localPointer),
|
|
3411
|
+
);
|
|
3412
|
+
setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
3413
|
+
void preloadAgentPromptSnippet(nextSelection);
|
|
3414
|
+
setAgentModalOpen(true);
|
|
3415
|
+
}
|
|
3084
3416
|
},
|
|
3085
|
-
[
|
|
3417
|
+
[
|
|
3418
|
+
applyDomSelection,
|
|
3419
|
+
captionEditMode,
|
|
3420
|
+
compositionLoading,
|
|
3421
|
+
preloadAgentPromptSnippet,
|
|
3422
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3423
|
+
],
|
|
3086
3424
|
);
|
|
3087
3425
|
|
|
3088
3426
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
3089
3427
|
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
3090
|
-
if (!
|
|
3428
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
|
|
3091
3429
|
updateDomEditHoverSelection(null);
|
|
3092
3430
|
return null;
|
|
3093
3431
|
}
|
|
@@ -3098,7 +3436,12 @@ export function StudioApp() {
|
|
|
3098
3436
|
updateDomEditHoverSelection(nextSelection);
|
|
3099
3437
|
return nextSelection;
|
|
3100
3438
|
},
|
|
3101
|
-
[
|
|
3439
|
+
[
|
|
3440
|
+
captionEditMode,
|
|
3441
|
+
compositionLoading,
|
|
3442
|
+
resolveDomSelectionFromPreviewPoint,
|
|
3443
|
+
updateDomEditHoverSelection,
|
|
3444
|
+
],
|
|
3102
3445
|
);
|
|
3103
3446
|
|
|
3104
3447
|
const handlePreviewCanvasPointerLeave = useCallback(() => {
|
|
@@ -3397,6 +3740,7 @@ export function StudioApp() {
|
|
|
3397
3740
|
}),
|
|
3398
3741
|
);
|
|
3399
3742
|
|
|
3743
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
3400
3744
|
await saveProjectFilesWithHistory({
|
|
3401
3745
|
projectId: pid,
|
|
3402
3746
|
label: "Add timeline asset",
|
|
@@ -3884,6 +4228,7 @@ export function StudioApp() {
|
|
|
3884
4228
|
</div>
|
|
3885
4229
|
) : (
|
|
3886
4230
|
<LeftSidebar
|
|
4231
|
+
ref={leftSidebarRef}
|
|
3887
4232
|
width={leftWidth}
|
|
3888
4233
|
projectId={projectId}
|
|
3889
4234
|
compositions={compositions}
|
|
@@ -3965,9 +4310,8 @@ export function StudioApp() {
|
|
|
3965
4310
|
onInspectTimelineElement={handleTimelineElementInspect}
|
|
3966
4311
|
inspectedTimelineElementId={inspectedTimelineElementId}
|
|
3967
4312
|
timelineLayerChildCounts={timelineLayerChildCounts}
|
|
3968
|
-
thumbnailedTimelineElementIds={thumbnailedTimelineElementIds}
|
|
3969
|
-
onToggleTimelineElementThumbnail={handleToggleTimelineElementThumbnail}
|
|
3970
4313
|
onCompIdToSrcChange={setCompIdToSrc}
|
|
4314
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
3971
4315
|
onCompositionChange={(compPath) => {
|
|
3972
4316
|
// Sync activeCompPath when user drills down via timeline double-click
|
|
3973
4317
|
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
|
@@ -3984,7 +4328,7 @@ export function StudioApp() {
|
|
|
3984
4328
|
iframeRef={previewIframeRef}
|
|
3985
4329
|
activeCompositionPath={activeCompPath}
|
|
3986
4330
|
hoverSelection={
|
|
3987
|
-
|
|
4331
|
+
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
|
|
3988
4332
|
? domEditHoverSelection
|
|
3989
4333
|
: null
|
|
3990
4334
|
}
|
|
@@ -4093,6 +4437,7 @@ export function StudioApp() {
|
|
|
4093
4437
|
projectId={projectId}
|
|
4094
4438
|
assets={assets}
|
|
4095
4439
|
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
4440
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
4096
4441
|
copiedAgentPrompt={copiedAgentPrompt}
|
|
4097
4442
|
onClearSelection={clearDomSelection}
|
|
4098
4443
|
onSetStyle={handleDomStyleCommit}
|
|
@@ -4122,10 +4467,11 @@ export function StudioApp() {
|
|
|
4122
4467
|
projectId={projectId}
|
|
4123
4468
|
onDelete={renderQueue.deleteRender}
|
|
4124
4469
|
onClearCompleted={renderQueue.clearCompleted}
|
|
4125
|
-
onStartRender={async (format, quality) => {
|
|
4470
|
+
onStartRender={async (format, quality, resolution, fps) => {
|
|
4126
4471
|
await waitForPendingDomEditSaves();
|
|
4127
|
-
await renderQueue.startRender(
|
|
4472
|
+
await renderQueue.startRender({ fps, quality, format, resolution });
|
|
4128
4473
|
}}
|
|
4474
|
+
compositionDimensions={compositionDimensions}
|
|
4129
4475
|
isRendering={renderQueue.isRendering}
|
|
4130
4476
|
/>
|
|
4131
4477
|
)}
|
|
@@ -4155,8 +4501,13 @@ export function StudioApp() {
|
|
|
4155
4501
|
{agentModalOpen && domEditSelection && (
|
|
4156
4502
|
<AskAgentModal
|
|
4157
4503
|
selectionLabel={domEditSelection.label}
|
|
4504
|
+
anchorPoint={agentModalAnchorPoint}
|
|
4158
4505
|
onSubmit={handleAgentModalSubmit}
|
|
4159
|
-
onClose={() =>
|
|
4506
|
+
onClose={() => {
|
|
4507
|
+
setAgentModalOpen(false);
|
|
4508
|
+
setAgentPromptSelectionContext(undefined);
|
|
4509
|
+
setAgentModalAnchorPoint(null);
|
|
4510
|
+
}}
|
|
4160
4511
|
/>
|
|
4161
4512
|
)}
|
|
4162
4513
|
|