mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
* feat(studio): add clipboard payload types and ID deduplication * feat(studio): add Ctrl+C/V/X copy/paste for timeline clips and DOM elements * fix(studio): use duck-typing for cross-frame element access in clipboard Elements from the preview iframe are from a different window context, so `el instanceof HTMLElement` always returns false. Use `"outerHTML" in el` instead to correctly detect elements across frame boundaries. * fix(studio): preserve playhead position after paste reloadPreview() used location.reload() which bypassed the NLELayout saveSeekPosition effect, causing the playhead to reset to 0:00 after paste. Switch to setRefreshKey which triggers the effect and restores the seek position after the iframe reloads. * fix(studio): paste DOM elements as siblings, not at composition root DOM element paste was inserting at the composition root, losing the parent context that provides CSS styles and positioning. Now stores the origin selector on copy and inserts the paste as a sibling immediately after the original element, preserving style inheritance. Falls back to root insertion if the selector can't be matched. * fix(studio): address review — deduplicateIds, native copy, altKey guard - deduplicateIds regex used \b which matched data-composition-id, data-clip-id, etc. Switch to lookbehind (?<=\s) so only standalone id="..." attributes are rewritten. Add test pinning this. - Ctrl+C no longer calls preventDefault() before confirming there's a selected element. Native browser copy (text selections outside inputs) is preserved when nothing is selected in the Studio. - Add !event.altKey guard on C/V/X to avoid intercepting Cmd+Alt+V (paste-as-plain-text) and similar OS gestures. - Remove no-op .replace(/"/g, '"') flagged by CodeQL. * fix(studio): address review round 2 — Cmd+X guard, data-start scope, revert drive-by - Cmd+X now pre-checks selection state before preventDefault, mirroring the Cmd+C fix. Native cut preserved when nothing is selected. - handleCut returns Promise<boolean> so the caller can gate on it. - data-start rewrite scoped to the outermost opening tag only, so nested clip timing is preserved on paste. - Removed system clipboard write (cross-tab paste unsupported, in-memory ref is the only read path). - Reverted the reloadPreview drive-by (setRefreshKey→location.reload); the perf branch (#895) handles this properly via refreshPlayer().
501 lines
20 KiB
TypeScript
501 lines
20 KiB
TypeScript
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
|
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
import { usePlayerStore } from "./player";
|
|
import { LintModal } from "./components/LintModal";
|
|
import { useCaptionStore } from "./captions/store";
|
|
import { useCaptionSync } from "./captions/hooks/useCaptionSync";
|
|
import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
|
|
import { usePanelLayout } from "./hooks/usePanelLayout";
|
|
import { useFileManager } from "./hooks/useFileManager";
|
|
import { useManifestPersistence } from "./hooks/useManifestPersistence";
|
|
import { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
import { useDomEditSession } from "./hooks/useDomEditSession";
|
|
import { useAppHotkeys } from "./hooks/useAppHotkeys";
|
|
import { useClipboard } from "./hooks/useClipboard";
|
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "./utils/studioUiPreferences";
|
|
import { useCaptionDetection } from "./hooks/useCaptionDetection";
|
|
import { useRenderClipContent } from "./hooks/useRenderClipContent";
|
|
import { useConsoleErrorCapture } from "./hooks/useConsoleErrorCapture";
|
|
import { useFrameCapture } from "./hooks/useFrameCapture";
|
|
import { useLintModal } from "./hooks/useLintModal";
|
|
import { useCompositionDimensions } from "./hooks/useCompositionDimensions";
|
|
import { useToast } from "./hooks/useToast";
|
|
import { useStudioUrlState } from "./hooks/useStudioUrlState";
|
|
import {
|
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
} from "./components/editor/manualEditingAvailability";
|
|
import { readStudioMotionFromElement } from "./components/editor/studioMotion";
|
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
import { AskAgentModal } from "./components/AskAgentModal";
|
|
import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
|
|
import { StudioHeader } from "./components/StudioHeader";
|
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
import { StudioPreviewArea } from "./components/StudioPreviewArea";
|
|
import { StudioRightPanel } from "./components/StudioRightPanel";
|
|
import { TimelineToolbar } from "./components/TimelineToolbar";
|
|
import { StudioProvider, type StudioContextValue } from "./contexts/StudioContext";
|
|
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
|
|
import { FileManagerProvider } from "./contexts/FileManagerContext";
|
|
import { DomEditProvider } from "./contexts/DomEditContext";
|
|
import { StudioSplash } from "./components/StudioSplash";
|
|
import { useServerConnection } from "./hooks/useServerConnection";
|
|
import {
|
|
normalizeStudioCompositionPath,
|
|
readStudioUrlStateFromWindow,
|
|
} from "./utils/studioUrlState";
|
|
|
|
export function StudioApp() {
|
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
|
|
const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
|
|
const [activeCompPathHydrated, setActiveCompPathHydrated] = useState(
|
|
() => initialUrlStateRef.current.activeCompPath == null,
|
|
);
|
|
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
|
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const [, setPreviewDocumentVersion] = useState(0);
|
|
|
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
const activeCompPathRef = useRef(activeCompPath);
|
|
activeCompPathRef.current = activeCompPath;
|
|
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
const renderQueue = useRenderQueue(projectId);
|
|
const captionEditMode = useCaptionStore((s) => s.isEditMode);
|
|
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
|
|
const captionSync = useCaptionSync(projectId);
|
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
const timelineElements = usePlayerStore((s) => s.elements);
|
|
const setSelectedTimelineElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
const timelineDuration = usePlayerStore((s) => s.duration);
|
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
const activePreviewUrl = activeCompPath
|
|
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
|
|
: null;
|
|
const effectiveTimelineDuration = useMemo(() => {
|
|
const maxEnd =
|
|
timelineElements.length > 0
|
|
? Math.max(...timelineElements.map((el) => el.start + el.duration))
|
|
: 0;
|
|
return Math.max(timelineDuration, maxEnd);
|
|
}, [timelineDuration, timelineElements]);
|
|
const refreshPreviewDocumentVersion = useCallback(() => {
|
|
setPreviewDocumentVersion((v) => v + 1);
|
|
window.setTimeout(() => setPreviewDocumentVersion((v) => v + 1), 80);
|
|
window.setTimeout(() => setPreviewDocumentVersion((v) => v + 1), 300);
|
|
}, []);
|
|
|
|
const [timelineVisible, setTimelineVisible] = useState(
|
|
() =>
|
|
initialUrlStateRef.current.timelineVisible ??
|
|
readStudioUiPreferences().timelineVisible ??
|
|
true,
|
|
);
|
|
const toggleTimelineVisibility = useCallback(() => {
|
|
setTimelineVisible((v) => {
|
|
writeStudioUiPreferences({ timelineVisible: !v });
|
|
return !v;
|
|
});
|
|
}, []);
|
|
const { appToast, showToast } = useToast();
|
|
const panelLayout = usePanelLayout({
|
|
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
|
|
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
|
|
});
|
|
const editHistory = usePersistentEditHistory({ projectId });
|
|
const domEditSaveTimestampRef = useRef(0);
|
|
const reloadPreview = useCallback(() => {
|
|
try {
|
|
previewIframeRef.current?.contentWindow?.location.reload();
|
|
} catch {
|
|
setRefreshKey((k) => k + 1);
|
|
}
|
|
}, []);
|
|
|
|
const fileManager = useFileManager({
|
|
projectId,
|
|
showToast,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
setRefreshKey,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (activeCompPathHydrated) return;
|
|
if (!fileManager.fileTreeLoaded) return;
|
|
|
|
const nextCompPath = normalizeStudioCompositionPath(
|
|
initialUrlStateRef.current.activeCompPath,
|
|
fileManager.fileTree,
|
|
);
|
|
setActiveCompPath((current) => (current === nextCompPath ? current : nextCompPath));
|
|
setActiveCompPathHydrated(true);
|
|
}, [activeCompPathHydrated, fileManager.fileTree, fileManager.fileTreeLoaded]);
|
|
|
|
const manifestPersistence = useManifestPersistence({
|
|
projectId,
|
|
showToast,
|
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
previewIframeRef,
|
|
activeCompPathRef,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview: () => setRefreshKey((k) => k + 1),
|
|
});
|
|
|
|
const timelineEditing = useTimelineEditing({
|
|
projectId,
|
|
activeCompPath,
|
|
timelineElements,
|
|
showToast,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
});
|
|
|
|
const clearDomSelectionRef = useRef<() => void>(() => {});
|
|
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
|
|
const handleDomEditElementDeleteRef = useRef<(s: DomEditSelection) => Promise<void>>(
|
|
async () => {},
|
|
);
|
|
const domEditDeleteBridge = async (s: DomEditSelection) =>
|
|
handleDomEditElementDeleteRef.current(s);
|
|
const { handleCopy, handlePaste, handleCut } = useClipboard({
|
|
projectId,
|
|
activeCompPath,
|
|
domEditSelectionRef: domEditSelectionBridgeRef,
|
|
showToast,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
|
|
handleDomEditElementDelete: domEditDeleteBridge,
|
|
previewIframeRef,
|
|
});
|
|
const appHotkeys = useAppHotkeys({
|
|
toggleTimelineVisibility,
|
|
handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
|
|
handleDomEditElementDelete: domEditDeleteBridge,
|
|
domEditSelectionRef: domEditSelectionBridgeRef,
|
|
clearDomSelectionRef,
|
|
editHistory,
|
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
readProjectFile: fileManager.readProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
domEditSaveTimestampRef,
|
|
showToast,
|
|
syncHistoryPreviewAfterApply: manifestPersistence.syncHistoryPreviewAfterApply,
|
|
waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
|
|
leftSidebarRef,
|
|
handleCopy,
|
|
handlePaste,
|
|
handleCut,
|
|
});
|
|
|
|
const domEditSession = useDomEditSession({
|
|
projectId,
|
|
activeCompPath,
|
|
isMasterView,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
previewIframeRef,
|
|
timelineElements,
|
|
currentTime,
|
|
setSelectedTimelineElementId,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
showToast,
|
|
refreshPreviewDocumentVersion,
|
|
queueDomEditSave: manifestPersistence.queueDomEditSave,
|
|
readProjectFile: fileManager.readProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
domEditSaveTimestampRef,
|
|
editHistory: { recordEdit: editHistory.recordEdit },
|
|
fileTree: fileManager.fileTree,
|
|
importedFontAssetsRef: fileManager.importedFontAssetsRef,
|
|
projectDir: fileManager.projectDir,
|
|
projectIdRef: fileManager.projectIdRef,
|
|
previewIframe,
|
|
refreshKey,
|
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
applyStudioManualEditsToPreviewRef: manifestPersistence.applyStudioManualEditsToPreviewRef,
|
|
syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
|
|
reloadPreview,
|
|
setRefreshKey,
|
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
selectSidebarTab: (tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
|
|
});
|
|
|
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
|
handleDomEditElementDeleteRef.current = domEditSession.handleDomEditElementDelete;
|
|
|
|
useCaptionDetection({
|
|
projectId,
|
|
activeCompPath,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
captionHasSelection,
|
|
previewIframeRef,
|
|
captionSync,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
});
|
|
|
|
const renderClipContent = useRenderClipContent({
|
|
projectIdRef: fileManager.projectIdRef,
|
|
compIdToSrc,
|
|
activePreviewUrl,
|
|
effectiveTimelineDuration,
|
|
});
|
|
|
|
const compositionDimensions = useCompositionDimensions();
|
|
const { lintModal, linting, handleLint, closeLintModal } = useLintModal(projectId);
|
|
const frameCapture = useFrameCapture({
|
|
projectId,
|
|
activeCompPath,
|
|
showToast,
|
|
waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
|
|
});
|
|
const {
|
|
consoleErrors,
|
|
setConsoleErrors,
|
|
resetErrors: resetConsoleErrors,
|
|
} = useConsoleErrorCapture(previewIframe);
|
|
|
|
const [globalDragOver, setGlobalDragOver] = useState(false);
|
|
const dragCounterRef = useRef(0);
|
|
|
|
const { syncPreviewTimelineHotkey, syncPreviewHistoryHotkey } = appHotkeys;
|
|
const handlePreviewIframeRef = useCallback(
|
|
(iframe: HTMLIFrameElement | null) => {
|
|
previewIframeRef.current = iframe;
|
|
setPreviewIframe(iframe);
|
|
syncPreviewTimelineHotkey(iframe);
|
|
syncPreviewHistoryHotkey(iframe);
|
|
resetConsoleErrors();
|
|
refreshPreviewDocumentVersion();
|
|
},
|
|
[
|
|
refreshPreviewDocumentVersion,
|
|
resetConsoleErrors,
|
|
syncPreviewHistoryHotkey,
|
|
syncPreviewTimelineHotkey,
|
|
],
|
|
);
|
|
|
|
const handleSelectComposition = useCallback(
|
|
(comp: string) => {
|
|
setActiveCompPath(comp === "index.html" || comp.startsWith("compositions/") ? comp : null);
|
|
fileManager.setEditingFile({ path: comp, content: null });
|
|
fetch(`/api/projects/${projectId}/files/${comp}`)
|
|
.then((r) => r.json())
|
|
.then((data) => fileManager.setEditingFile({ path: comp, content: data.content }))
|
|
.catch(() => {});
|
|
},
|
|
[projectId, fileManager],
|
|
);
|
|
|
|
const selectedStudioMotion =
|
|
STUDIO_INSPECTOR_PANELS_ENABLED && domEditSession.domEditSelection
|
|
? readStudioMotionFromElement(domEditSession.domEditSelection.element)
|
|
: null;
|
|
const layersPanelActive =
|
|
STUDIO_INSPECTOR_PANELS_ENABLED && panelLayout.rightPanelTab === "layers";
|
|
const designPanelActive =
|
|
STUDIO_INSPECTOR_PANELS_ENABLED && panelLayout.rightPanelTab === "design";
|
|
const motionPanelActive =
|
|
STUDIO_INSPECTOR_PANELS_ENABLED &&
|
|
STUDIO_MOTION_PANEL_ENABLED &&
|
|
panelLayout.rightPanelTab === "motion";
|
|
const inspectorPanelActive = layersPanelActive || designPanelActive || motionPanelActive;
|
|
const shouldShowSelectedDomBounds =
|
|
inspectorPanelActive && !panelLayout.rightCollapsed && !isPlaying;
|
|
const inspectorButtonActive =
|
|
STUDIO_INSPECTOR_PANELS_ENABLED && !panelLayout.rightCollapsed && inspectorPanelActive;
|
|
|
|
useStudioUrlState({
|
|
projectId,
|
|
activeCompPath,
|
|
currentTime,
|
|
duration: effectiveTimelineDuration,
|
|
isPlaying,
|
|
compositionLoading,
|
|
refreshKey,
|
|
previewIframeRef,
|
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
rightCollapsed: panelLayout.rightCollapsed,
|
|
timelineVisible,
|
|
activeCompPathHydrated,
|
|
domEditSelection: domEditSession.domEditSelection,
|
|
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
|
|
applyDomSelection: domEditSession.applyDomSelection,
|
|
initialState: initialUrlStateRef.current,
|
|
});
|
|
|
|
// StudioProvider performs its own useMemo — no need for a second memo here.
|
|
const studioCtxValue: StudioContextValue = {
|
|
projectId: projectId!,
|
|
activeCompPath,
|
|
setActiveCompPath,
|
|
showToast,
|
|
previewIframeRef,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
refreshKey,
|
|
setRefreshKey,
|
|
currentTime,
|
|
timelineElements,
|
|
isPlaying,
|
|
editHistory: {
|
|
canUndo: editHistory.canUndo,
|
|
canRedo: editHistory.canRedo,
|
|
undoLabel: editHistory.undoLabel,
|
|
redoLabel: editHistory.redoLabel,
|
|
},
|
|
handleUndo: appHotkeys.handleUndo,
|
|
handleRedo: appHotkeys.handleRedo,
|
|
renderQueue: {
|
|
jobs: renderQueue.jobs,
|
|
isRendering: renderQueue.isRendering,
|
|
deleteRender: renderQueue.deleteRender,
|
|
clearCompleted: renderQueue.clearCompleted,
|
|
startRender: renderQueue.startRender as (options: unknown) => Promise<void>,
|
|
},
|
|
compositionDimensions,
|
|
waitForPendingDomEditSaves: manifestPersistence.waitForPendingDomEditSaves,
|
|
handlePreviewIframeRef,
|
|
refreshPreviewDocumentVersion,
|
|
timelineVisible,
|
|
toggleTimelineVisibility,
|
|
};
|
|
|
|
if (resolving || waitingForServer || !projectId) {
|
|
return <StudioSplash waiting={waitingForServer} />;
|
|
}
|
|
|
|
const timelineToolbar = <TimelineToolbar toggleTimelineVisibility={toggleTimelineVisibility} />;
|
|
return (
|
|
<StudioProvider value={studioCtxValue}>
|
|
<PanelLayoutProvider value={panelLayout}>
|
|
<FileManagerProvider value={fileManager}>
|
|
<DomEditProvider value={domEditSession}>
|
|
<div
|
|
className="flex flex-col h-full w-full bg-neutral-950 relative"
|
|
onDragOver={(e) => {
|
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
e.preventDefault();
|
|
}}
|
|
onDragEnter={(e) => {
|
|
if (!e.dataTransfer.types.includes("Files")) return;
|
|
e.preventDefault();
|
|
dragCounterRef.current++;
|
|
setGlobalDragOver(true);
|
|
}}
|
|
onDragLeave={() => {
|
|
dragCounterRef.current--;
|
|
if (dragCounterRef.current === 0) setGlobalDragOver(false);
|
|
}}
|
|
onDrop={(e) => {
|
|
dragCounterRef.current = 0;
|
|
setGlobalDragOver(false);
|
|
if (e.defaultPrevented) return;
|
|
e.preventDefault();
|
|
if (e.dataTransfer.files.length)
|
|
fileManager.handleImportFiles(e.dataTransfer.files);
|
|
}}
|
|
>
|
|
<StudioHeader
|
|
captureFrameHref={frameCapture.captureFrameHref}
|
|
captureFrameFilename={frameCapture.captureFrameFilename}
|
|
handleCaptureFrameClick={frameCapture.handleCaptureFrameClick}
|
|
refreshCaptureFrameTime={frameCapture.refreshCaptureFrameTime}
|
|
inspectorButtonActive={inspectorButtonActive}
|
|
inspectorPanelActive={inspectorPanelActive}
|
|
/>
|
|
|
|
<div className="flex flex-1 min-h-0">
|
|
<StudioLeftSidebar
|
|
leftSidebarRef={leftSidebarRef}
|
|
onSelectComposition={handleSelectComposition}
|
|
onLint={handleLint}
|
|
linting={linting}
|
|
/>
|
|
<StudioPreviewArea
|
|
timelineToolbar={timelineToolbar}
|
|
renderClipContent={renderClipContent}
|
|
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
|
|
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
|
|
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
|
|
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
|
|
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
|
|
handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
|
|
setCompIdToSrc={setCompIdToSrc}
|
|
setCompositionLoading={setCompositionLoading}
|
|
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
/>
|
|
|
|
{!panelLayout.rightCollapsed && (
|
|
<StudioRightPanel
|
|
selectedStudioMotion={selectedStudioMotion}
|
|
designPanelActive={designPanelActive}
|
|
motionPanelActive={motionPanelActive}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{lintModal !== null && (
|
|
<LintModal findings={lintModal} projectId={projectId} onClose={closeLintModal} />
|
|
)}
|
|
|
|
{consoleErrors !== null && consoleErrors.length > 0 && (
|
|
<LintModal
|
|
findings={consoleErrors}
|
|
projectId={projectId}
|
|
onClose={() => setConsoleErrors(null)}
|
|
/>
|
|
)}
|
|
|
|
{domEditSession.agentModalOpen && domEditSession.domEditSelection && (
|
|
<AskAgentModal
|
|
selectionLabel={domEditSession.domEditSelection.label}
|
|
anchorPoint={domEditSession.agentModalAnchorPoint}
|
|
onSubmit={domEditSession.handleAgentModalSubmit}
|
|
onClose={() => {
|
|
domEditSession.setAgentModalOpen(false);
|
|
domEditSession.setAgentPromptSelectionContext(undefined);
|
|
domEditSession.setAgentModalAnchorPoint(null);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{globalDragOver && <StudioGlobalDragOverlay />}
|
|
|
|
{appToast && (
|
|
<div
|
|
className={`absolute bottom-6 left-1/2 -translate-x-1/2 z-[91] px-4 py-2 rounded-lg border text-sm shadow-lg animate-in fade-in slide-in-from-bottom-2 ${
|
|
appToast.tone === "error"
|
|
? "bg-red-900/90 border-red-700/50 text-red-200"
|
|
: "bg-neutral-900/95 border-neutral-700/60 text-neutral-100"
|
|
}`}
|
|
>
|
|
{appToast.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DomEditProvider>
|
|
</FileManagerProvider>
|
|
</PanelLayoutProvider>
|
|
</StudioProvider>
|
|
);
|
|
}
|