Files
hyperframes/packages/studio/src/App.tsx
Miguel Ángel dc244d765c fix(studio): drags after a resize, undo or reload land where they were pressed (#5088)
* test(studio): bench moves pressed right after Add keyframe

* fix(studio): an undo that reloads the preview keeps the selection, so a drag after it moves

* fix(studio): a drag started while a resize saves keeps its animations paused

* fix(parsers): an edit pins no position on a keyframed tween it did not touch

* fix(studio): a move pressed while the preview changes lands on the new preview, at the time pressed

* test(studio): bench waiting presses, a held box resized twice, and reloads at the last playhead

* refactor(studio): tidy the waiting press, bound its wait, and freeze edit stamps after early exits

* test(studio): load the property panel once so its first test stops timing out

* fix(studio): presses queued on a held box keep their release, hover and newest outline

* test(studio): judge bench undo by the history groups Studio claims, not by file writes

* refactor(studio): one waiting-box lookup in the bench and one raf-pause write per release

Behaviour-identical tidy of the waiting-press work: the bench reads the waiting
box through one helper, releaseActiveGesture sets the raf pause once, clearPointerState
writes the false it always computed, and previewGestureStarts calls the opts setters
directly instead of through aliases named like the gesture hook's gated setters.

* fix(studio): a queued press lands on the element it pressed and survives a window switch

* fix(studio): undo cancels a waiting press, a stuck save stops holding presses, scene swaps hold them

* refactor(studio): keep the complexity waiver on clearPointerState itself

* fix(studio): undo of a queued press leaves nothing waiting, and hosts keep their own undo

* fix(studio): a waiting press keeps the box still after a blur, and undo reports what it took back

* style(studio): bring the PR's comments within the comment limits

* fix(studio): bank the four bench cases this fixes and meet the comment, complexity and duplication gates

* test(studio): pin that a waiting press is never a pending edit, and name the new duplication waivers
2026-10-06 20:22:37 +00:00

595 lines
26 KiB
TypeScript

import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
import { buildProjectApiPath } from "./utils/projectRouting";
import { useState, useCallback, useRef, useMemo, type SetStateAction } from "react";
import { useStableHandlers } from "./hooks/useStableHandlers";
import { useHistoryFlags, useToolbarSession } from "./hooks/useShellSlices";
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
import { useRenderQueue } from "./components/renders/useRenderQueue";
import { usePlayerStore } from "./player";
import { StudioOverlays } from "./components/StudioOverlays";
import { SaveQueuePausedBanner } from "./components/SaveQueuePausedBanner";
import { ExternalFileConflictBanner } from "./components/ExternalFileConflictBanner";
import { ProjectUnreachableBanner } from "./components/ProjectUnreachableBanner";
import { CompositionMissingBanner } from "./components/CompositionMissingBanner";
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 { usePreviewPersistence } from "./hooks/usePreviewPersistence";
import { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
import { useTimelineEditing } from "./hooks/useTimelineEditing";
import { useTimelineMoveEditsHandler } from "./hooks/timelineMoveAdapter";
import type { TimelineZIndexReorderCommit } from "./hooks/useTimelineEditingTypes";
import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab";
import { useDomEditSession } from "./hooks/useDomEditSession";
import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
import { useStudioExternalFileChanges } from "./hooks/useStudioExternalFileChanges";
import { useBlockHandlers } from "./hooks/useBlockHandlers";
import { useAppHotkeys } from "./hooks/useAppHotkeys";
import { trackedStudioEdit } from "./utils/studioPendingEdits";
import { useClipboard } from "./hooks/useClipboard";
import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
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 { useEffectiveTimelineDuration } from "./hooks/useEffectiveTimelineDuration";
import {
buildStudioContextValue,
useGlobalFileDrop,
useInspectorState,
} from "./hooks/useStudioContextValue";
import type { DomEditSelection } from "./components/editor/domEditing";
import { StudioHeader } from "./components/StudioHeader";
import { useGestureCommit } from "./hooks/useGestureCommit";
import { useGestureTrailOverlay } from "./hooks/useGestureTrailOverlay";
import { EditorShell } from "./components/EditorShell";
import { useStudioSidePanels } from "./hooks/useStudioSidePanels";
import { TimelineToolbar } from "./components/TimelineToolbar";
import { StudioPlaybackProvider, StudioShellProvider } 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 { useStudioSessionStart } from "./hooks/useStudioSessionStart";
import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
import { readStudioUrlStateFromWindow, resolveMasterCompositionPath } from "./utils/studioUrlState";
import { useActiveComposition } from "./hooks/useActiveComposition";
import { requestPreviewReload } from "./player/previewReloading";
const getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
export interface StudioAppProps {
/** Clicks still select and report; the preview cannot move, edit or delete anything. */
readOnlyPreview?: boolean;
/** Short text shown on disabled hand-edit controls while `readOnlyPreview` is set. */
readOnlyPreviewReason?: string;
}
// fallow-ignore-next-line complexity
export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: StudioAppProps = {}) {
const { projectId, resolving, waitingForServer } = useServerConnection();
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
useStudioSessionStart(projectId, resolving, waitingForServer);
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
const previewIframe = useOwnPreviewIframe();
const [compositionLoading, setCompositionLoading] = useState(true);
const [refreshKey, setRefreshKeyState] = useState(0);
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
const captionEditMode = useCaptionStore((s) => s.isEditMode);
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
const captionSync = useCaptionSync(projectId);
const timelineElements = usePlayerStore((s) => s.elements);
const setSelectedTimelineElementId = usePlayerStore((s) => s.setSelectedElementId);
const setTimelineSelectionSet = usePlayerStore((s) => s.setSelectedElementIds);
const timelineDuration = usePlayerStore((s) => s.duration);
const isPlaying = usePlayerStore((s) => s.isPlaying);
const effectiveTimelineDuration = useEffectiveTimelineDuration(
timelineDuration,
timelineElements,
);
const { toasts, showToast, dismissToast } = useToast();
const panelLayout = usePanelLayout({
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
});
const editHistory = useStableHandlers(usePersistentEditHistory({ projectId }), projectId);
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
const pendingTimelineEditPathRef = useRef(new Set<string>());
const isGestureRecordingRef = useRef(false);
const setRefreshKey = useCallback((next: SetStateAction<number>) => {
requestPreviewReload();
setRefreshKeyState(next);
}, []);
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), [setRefreshKey]);
const fileManagerResult = useFileManager({
projectId,
showToast,
recordEdit: editHistory.recordEdit,
setRefreshKey,
});
const fileManager = useStableHandlers(fileManagerResult, projectId);
const masterCompPath = useMemo(
() => resolveMasterCompositionPath(fileManager.compositions),
[fileManager.compositions],
);
const { activeCompPath, activeCompPathHydrated, setActiveCompPath, handleSelectComposition } =
useActiveComposition({
projectId,
initialUrlStateRef,
fileTree: fileManager.fileTree,
fileTreeLoaded: fileManager.fileTreeLoaded,
masterCompPath,
setEditingFile: fileManager.setEditingFile,
showToast,
});
const { sdkHandle, editFlowSdkSession } = useStudioSdkSessions(
projectId,
activeCompPath,
masterCompPath,
fileManager.fileTree,
fileManager.fileTreeLoaded,
fileManager.refreshFileTree,
);
const activeCompPathRef = useRef(activeCompPath);
activeCompPathRef.current = activeCompPath;
const renderQueue = useStableHandlers(useRenderQueue(projectId, activeCompPathRef), projectId);
const previewPersistenceResult = usePreviewPersistence({
showToast,
readOptionalProjectFile: fileManager.readOptionalProjectFile,
writeProjectFile: fileManager.writeProjectFile,
recordEdit: editHistory.recordEdit,
previewIframeRef,
activeCompPathRef,
reloadPreview,
});
const previewPersistence = useStableHandlers(previewPersistenceResult, projectId);
const externalFileChanges = useStudioExternalFileChanges({
projectId,
activeCompPath,
masterCompPath,
fileManager,
previewPersistence,
pendingTimelineEditPathRef,
reloadPreview,
onOutsideChange: editHistory.noteOutsideChange,
});
const invalidateGsapCacheRef = useRef<() => void>(() => {});
const invalidateGsapCache = useCallback(() => invalidateGsapCacheRef.current(), []);
const timelineEditing = useTimelineEditing({
projectId,
activeCompPath,
timelineElements,
showToast,
writeProjectFile: fileManager.writeProjectFile,
observeProjectFileVersion: fileManager.observeProjectFileVersion,
recordEdit: editHistory.recordEdit,
reloadPreview,
previewIframeRef,
pendingTimelineEditPathRef,
uploadProjectFiles: fileManager.uploadProjectFiles,
isRecordingRef: isGestureRecordingRef,
sdkSession: editFlowSdkSession,
publishSdkSession: sdkHandle.publish,
forceReloadSdkSession: sdkHandle.forceReload,
invalidateGsapCache,
handleDomZIndexReorderCommitRef,
});
const handleTimelineElementsMove = useTimelineMoveEditsHandler(
timelineEditing.handleTimelineGroupMove,
);
const {
addAssetAtPlayhead: handleAddAssetAtPlayhead,
addCompositionAtPlayhead: handleAddCompositionAtPlayhead,
} = useTimelineAddAtPlayhead(
timelineEditing.handleTimelineAssetDrop,
timelineEditing.handleTimelineCompositionDrop,
projectId,
);
const blockHandlersResult = useBlockHandlers({
projectId,
blockCtxDeps: {
activeCompPath,
timelineElements,
readProjectFile: fileManager.readProjectFile,
writeProjectFile: fileManager.writeProjectFile,
recordEdit: editHistory.recordEdit,
refreshFileTree: fileManager.refreshFileTree,
reloadPreview,
showToast,
dismissToast,
},
previewIframeRef,
setRightCollapsed: panelLayout.setRightCollapsed,
setRightPanelTab: panelLayout.setRightPanelTab,
});
const {
activeBlockParams,
setActiveBlockParams,
handleAddBlock,
handleTimelineBlockDrop,
handleAddMediaOverlay,
handlePreviewBlockDrop,
} = useStableHandlers(blockHandlersResult, projectId);
const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
const layout = useMemo(
() => ({ ...panelLayout, setRightPanelTab }),
[panelLayout, setRightPanelTab],
);
const clearDomSelectionRef = useRef<() => void>(() => {});
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
type DomEditDelete = (s: DomEditSelection, o?: { expandGroup?: boolean }) => Promise<void>;
const handleDomEditElementDeleteRef = useRef<DomEditDelete>(async () => {});
const domEditDeleteBridge: DomEditDelete = (s, o) => handleDomEditElementDeleteRef.current(s, o);
const resetKeyframesRef = useRef<() => boolean>(() => false);
const deleteSelectedKeyframesRef = useRef<() => void>(() => {});
const { handleCopy, handlePaste, handleCut, handleDuplicate, canPaste } = useClipboard({
projectId,
activeCompPath,
domEditSelectionRef: domEditSelectionBridgeRef,
showToast,
writeProjectFile: fileManager.writeProjectFile,
recordEdit: editHistory.recordEdit,
reloadPreview,
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
handleDomEditElementDelete: domEditDeleteBridge,
previewIframeRef,
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
});
const appHotkeys = useAppHotkeys({
projectId,
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
handleLinkEdit: timelineEditing.handleLinkEdit,
handleTimelineElementDeleteOnly: timelineEditing.handleTimelineElementDeleteOnly,
handleTimelineElementSplit: timelineEditing.handleTimelineElementSplit,
handleDomEditElementDelete: domEditDeleteBridge,
domEditSelectionRef: domEditSelectionBridgeRef,
clearDomSelectionRef,
editHistory,
readOptionalProjectFile: fileManager.readOptionalProjectFile,
readProjectFile: fileManager.readProjectFile,
writeProjectFile: fileManager.writeProjectFile,
showToast,
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
showHistoryRestoreNow: previewPersistence.showHistoryRestoreNow,
settlePendingEdits: previewPersistence.settlePendingEdits,
handleCopy,
handlePaste,
handleCut,
handleDuplicate,
onResetKeyframes: () => resetKeyframesRef.current(),
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
onAfterUndoRedo: (restore) => {
invalidateGsapCacheRef.current();
timelineEditing.restoreLiveLanes(restore);
},
onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
activeCompPath,
forceReloadSdkSession: sdkHandle.forceReload,
onToggleRecording: () => handleToggleRecordingRef.current("keyboard"),
readOnlyPreview,
});
const domEditSessionResult = useDomEditSession({
projectId,
activeCompPath,
compIdToSrc,
captionEditMode,
compositionLoading,
previewIframeRef,
timelineElements,
getTimelineSelectionSet,
setSelectedTimelineElementId,
setTimelineSelectionSet,
setRightCollapsed: panelLayout.setRightCollapsed,
setRightPanelTab,
showToast,
isRecordingRef: isGestureRecordingRef,
refreshPreviewDocumentVersion,
queueDomEditSave: previewPersistence.queueDomEditSave,
readProjectFile: fileManager.readProjectFile,
writeProjectFile: fileManager.writeProjectFile,
updateEditingFileContent: fileManager.updateEditingFileContent,
editHistory: { recordEdit: editHistory.recordEdit },
fileTree: fileManager.fileTree,
importedFontAssetsRef: fileManager.importedFontAssetsRef,
projectDir: fileManager.projectDir,
projectIdRef: fileManager.projectIdRef,
previewIframe,
refreshKey,
previewDocumentVersion,
rightPanelTab: panelLayout.rightPanelTab,
applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
syncPreviewHotkeys: appHotkeys.syncPreviewHotkeys,
reloadPreview,
setRefreshKey,
openSourceForSelection: fileManager.openSourceForSelection,
sdkSession: editFlowSdkSession,
publishSdkSession: sdkHandle.publish,
forceReloadSdkSession: sdkHandle.forceReload,
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
readOnlyPreview,
});
const domEditSession = useStableHandlers(domEditSessionResult, projectId);
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
const { handleDomZIndexReorderCommit: zCommit, handleDomEditElementDelete: del } = domEditSession;
handleDomZIndexReorderCommitRef.current = trackedStudioEdit(zCommit);
clearDomSelectionRef.current = domEditSession.clearDomSelection;
handleDomEditElementDeleteRef.current = trackedStudioEdit(del);
resetKeyframesRef.current = domEditSession.handleResetSelectedElementKeyframes;
invalidateGsapCacheRef.current = domEditSession.invalidateGsapCache;
deleteSelectedKeyframesRef.current = () => deleteSelectedKeyframes(domEditSession);
useSdkSelectionSync(
editFlowSdkSession,
domEditSession.domEditSelection,
domEditSession.domEditGroupSelections,
);
useCaptionDetection({
projectId,
activeCompPath,
compIdToSrc,
captionEditMode,
captionHasSelection,
previewIframeRef,
captionSync,
setRightCollapsed: panelLayout.setRightCollapsed,
});
const renderClipContent = useRenderClipContent({
projectIdRef: fileManager.projectIdRef,
compIdToSrc,
activePreviewUrl:
activeCompPath && projectId
? buildProjectApiPath(projectId, `/preview/comp/${activeCompPath}`)
: null,
effectiveTimelineDuration,
});
const compositionDimensions = useCompositionDimensions(previewIframeRef);
const lint = useStableHandlers(useLintModal(projectId, refreshKey), projectId);
const frameCaptureResult = useFrameCapture({
projectId,
activeCompPath,
showToast,
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
});
const frameCapture = useStableHandlers(frameCaptureResult, projectId);
const { consoleErrors, setConsoleErrors } = useConsoleErrorCapture(previewIframe);
const fileDrop = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
const handleToggleRecordingRef = useRef<(method?: "button" | "keyboard") => void>(() => {});
const domEditSessionRef = useRef(domEditSession);
domEditSessionRef.current = domEditSession;
const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
projectId,
domEditSessionRef,
previewIframeRef,
showToast,
isGestureRecordingRef,
readOnlyPreview,
});
handleToggleRecordingRef.current = handleToggleRecording;
const gestureOverlay = useGestureTrailOverlay(
{ gestureState, gestureRecording },
previewIframe,
compositionDimensions,
);
const handlePreviewIframeRef = useCallback(
(iframe: HTMLIFrameElement | null) => {
previewIframeRef.current = iframe;
usePreviewIframeStore.getState().setIframe(iframe);
appHotkeys.syncPreviewHotkeys(iframe);
refreshPreviewDocumentVersion();
},
[appHotkeys, refreshPreviewDocumentVersion],
);
const rightPanel = useRightPanelIntent();
const { inspectorPanelActive, shouldShowMotionPath, shouldShowSelectedDomBounds } =
useInspectorState(
rightPanel,
isPlaying,
domEditSession.domEditSelection,
gestureState === "recording",
);
// The dock has no separate "railed by window width" state (it shrinks
// panels, never auto-hides the group), so rightCollapsed is already the
// value that decides whether the panel is actually showing.
const inspectorButtonActive = !panelLayout.rightCollapsed && inspectorPanelActive;
useStudioUrlState({
projectId,
activeCompPath,
duration: effectiveTimelineDuration,
isPlaying,
compositionLoading,
refreshKey,
previewIframeRef,
rightPanelTab: panelLayout.rightPanelTab,
rightCollapsed: panelLayout.rightCollapsed,
activeCompPathHydrated,
domEditSelection: domEditSession.domEditSelection,
domEditGroupSelections: domEditSession.domEditGroupSelections,
applyMarqueeSelection: domEditSession.applyMarqueeSelection,
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
applyDomSelection: domEditSession.applyDomSelection,
setRightPanelTab,
initialState: initialUrlStateRef.current,
});
const historyFlags = useHistoryFlags(editHistory);
const toolbarSession = useToolbarSession(domEditSession);
const studioCtxValue = buildStudioContextValue({
projectId: projectId!,
activeCompPath,
setActiveCompPath,
showToast,
previewIframeRef,
captionEditMode,
compositionLoading,
refreshKey,
setRefreshKey,
timelineElements,
isPlaying,
editHistory: historyFlags,
handleUndo: appHotkeys.handleUndo,
handleRedo: appHotkeys.handleRedo,
renderQueue,
compositionDimensions,
domEditSaveQueuePaused: previewPersistence.domEditSaveQueuePaused,
externalFileConflict: externalFileChanges.blocked !== null,
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
handlePreviewIframeRef,
refreshPreviewDocumentVersion,
});
const timelineToolbar = useMemo(
() => (
<TimelineToolbar
domEditSession={toolbarSession}
onSplitElement={timelineEditing.handleTimelineElementSplit}
/>
),
[toolbarSession, timelineEditing.handleTimelineElementSplit],
);
const handleExport = useCallback(() => {
void previewPersistence
.waitForPendingDomEditSaves()
.then(() => renderQueue.startRender(undefined));
}, [previewPersistence, renderQueue]);
const clearConsoleErrors = useCallback(() => setConsoleErrors(null), [setConsoleErrors]);
const panels = useStudioSidePanels(
{
onSelectComposition: handleSelectComposition,
onAddBlock: handleAddBlock,
onPreviewBlock: setBlockPreview,
onLint: lint.handleLint,
linting: lint.linting,
lintFindingCount: lint.lintFindingCount,
lintFindingsByFile: lint.findingsByFile,
lintHasError: lint.hasLintError,
onAddAssetToTimeline: handleAddAssetAtPlayhead,
onAddCompositionToTimeline: handleAddCompositionAtPlayhead,
},
{
activeBlockParams,
onDismissBlockParams: dismissBlockParams,
onCloseBlockParams: () => {
setActiveBlockParams(null);
panelLayout.setRightPanelTab("design");
},
recordingState: gestureState,
recordingDuration: gestureRecording.recordingDuration,
onToggleRecording: handleToggleRecording,
sdkSession: sdkHandle.session,
publishSdkSession: sdkHandle.publish,
forceReloadSdkSession: sdkHandle.forceReload,
reloadPreview,
recordEdit: editHistory.recordEdit,
onToggleElementHidden: timelineEditing.handleToggleElementHidden,
onAutoGroupCarveSources: timelineEditing.handleAutoGroupCarveSources,
onAddMediaOverlay: handleAddMediaOverlay,
},
projectId,
);
if (resolving || waitingForServer || !projectId)
return <StudioSplash waiting={waitingForServer} />;
return (
<StudioShellProvider value={studioCtxValue}>
<StudioPlaybackProvider value={studioCtxValue}>
<PanelLayoutProvider value={layout}>
<FileManagerProvider value={fileManager}>
<DomEditProvider value={domEditSession}>
<div
className="flex flex-col h-full w-full bg-neutral-950 relative"
onDragOver={fileDrop.onDragOver}
onDrop={fileDrop.onDrop}
>
<StudioHeader
captureFrameHref={frameCapture.captureFrameHref}
captureFrameFilename={frameCapture.captureFrameFilename}
handleCaptureFrameClick={frameCapture.handleCaptureFrameClick}
refreshCaptureFrameTime={frameCapture.refreshCaptureFrameTime}
capturing={frameCapture.capturing}
inspectorButtonActive={inspectorButtonActive}
inspectorPanelActive={inspectorPanelActive}
onExport={handleExport}
/>
{previewPersistence.domEditSaveQueuePaused && !externalFileChanges.blocked && (
<SaveQueuePausedBanner
message={previewPersistence.domEditSaveQueuePaused}
onRetry={previewPersistence.resetDomEditSaveQueueBreaker}
/>
)}
<ExternalFileConflictBanner coordinator={externalFileChanges} />
{sdkHandle.unreachableProject && (
<ProjectUnreachableBanner projectId={sdkHandle.unreachableProject} />
)}
{sdkHandle.compositionMissing && activeCompPath && (
<CompositionMissingBanner path={activeCompPath} />
)}
<EditorShell
readOnlyPreview={readOnlyPreview}
readOnlyPreviewReason={readOnlyPreviewReason}
panels={panels}
timelineToolbar={timelineToolbar}
renderClipContent={renderClipContent}
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
handleTimelineBlockDrop={handleTimelineBlockDrop}
handleTimelineCompositionDrop={timelineEditing.handleTimelineCompositionDrop}
handlePreviewBlockDrop={handlePreviewBlockDrop}
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
handleTimelineElementsMove={handleTimelineElementsMove}
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
setAudioGroupAttribute={timelineEditing.setAudioGroupAttribute}
handleGroupClips={timelineEditing.handleAutoGroupCarveSources}
setElementFxAttribute={timelineEditing.setElementFxAttribute}
handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
handleRazorSplit={timelineEditing.handleRazorSplit}
handleRazorSplitAll={timelineEditing.handleRazorSplitAll}
handleFreezeFrame={timelineEditing.handleFreezeFrame}
handleLinkEdit={timelineEditing.handleLinkEdit}
handleTimelineElementDeleteOnly={timelineEditing.handleTimelineElementDeleteOnly}
onCopyClip={handleCopy}
onPasteClip={handlePaste}
onDuplicateClip={handleDuplicate}
canPasteClip={canPaste}
setCompIdToSrc={setCompIdToSrc}
setCompositionLoading={setCompositionLoading}
shouldShowMotionPath={shouldShowMotionPath}
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
isGestureRecording={gestureState === "recording"}
recordingState={gestureState}
onToggleRecording={handleToggleRecording}
blockPreview={blockPreview}
gestureOverlay={gestureOverlay}
/>
<StudioOverlays
projectId={projectId}
projectDir={fileManager.projectDir}
lintModal={lint.lintModal}
closeLintModal={lint.closeLintModal}
consoleErrors={consoleErrors}
clearConsoleErrors={clearConsoleErrors}
domEditSession={domEditSession}
activeCompPath={activeCompPath}
toasts={toasts}
dismissToast={dismissToast}
/>
</div>
</DomEditProvider>
</FileManagerProvider>
</PanelLayoutProvider>
</StudioPlaybackProvider>
</StudioShellProvider>
);
}