import { useCallback } from "react"; import type { StudioRightPanelProps } from "./StudioRightPanel.types"; export type { StudioRightPanelProps }; import { PropertyPanel } from "./editor/PropertyPanel"; import { LayersPanel } from "./editor/LayersPanel"; import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel"; import { BlockParamsPanel } from "./editor/BlockParamsPanel"; import { RenderQueuePanel } from "./renders/RenderQueuePanel"; import { SlideshowPanel } from "./panels/SlideshowPanel"; import { VariablesPanel } from "./panels/VariablesPanel"; import { PanelTabButton } from "./PanelTabButton"; import type { RenderJob } from "./renders/useRenderQueue"; import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability"; import { useSlideshowPersist } from "../hooks/useSlideshowPersist"; import { useSlideshowTabState } from "../hooks/useSlideshowTabState"; import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider"; import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext"; import { usePanelLayoutContext } from "../contexts/PanelLayoutContext"; import { useFileManagerContext } from "../contexts/FileManagerContext"; import { useDomEditContext } from "../contexts/DomEditContext"; import { usePlayerStore } from "../player"; import { applyColorGradingScopeUpdate, EMPTY_COLOR_GRADING_SCOPE_RESULT, type ColorGradingScope, } from "./studioColorGradingScope"; import { timelineKeysForSelections } from "../utils/studioHelpers"; import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize"; import { useRemoveBackground } from "../hooks/useRemoveBackground"; // fallow-ignore-next-line complexity export function StudioRightPanel({ designPanelActive, activeBlockParams, onCloseBlockParams, recordingState, recordingDuration, onToggleRecording, sdkSession, publishSdkSession, forceReloadSdkSession, reloadPreview, domEditSaveTimestampRef, recordEdit, onToggleElementHidden, onAutoGroupCarveSources, onAddMediaOverlay, }: StudioRightPanelProps) { const { rightWidth, adjustPanelWidth, rightPanelTab, setRightPanelTab, rightInspectorPanes, toggleRightInspectorPane, setExclusiveRightInspectorPane, handlePanelResizeStart, handlePanelResizeMove, handlePanelResizeEnd, } = usePanelLayoutContext(); const { previewIframeRef, projectId, activeCompPath, showToast, waitForPendingDomEditSaves, renderQueue, } = useStudioShellContext(); const { captionEditMode, refreshKey } = useStudioPlaybackContext(); const { domEditSelection, domEditGroupSelections, copiedAgentPrompt, clearDomSelection, handleUngroupSelection, handleGroupSelection, handleDomStyleCommit, handleDomAttributeCommit, handleDomAttributeLiveCommit, handleDomAttributeQuietCommit, handleDomHtmlAttributeCommit, handleDomAttributesCommit, handleDomPathOffsetCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, handleDomRemoveTextField, handleAskAgent, selectedGsapAnimations, gsapMultipleTimelines, gsapUnsupportedTimelinePattern, handleGsapUpdateProperty, handleGsapUpdateMeta, handleGsapDeleteAnimation, handleGsapAddAnimation, handleGsapAddProperty, handleGsapRemoveProperty, handleGsapUpdateFromProperty, handleGsapAddFromProperty, handleGsapRemoveFromProperty, commitAnimatedProperty, commitAnimatedProperties, handleSetArcPath, handleUpdateArcSegment, handleUnroll, handleUpdateKeyframeEase, handleUpdateSegmentEase, handleSetAllKeyframeEases, handleGsapAddKeyframe, handleGsapRemoveKeyframe, handleGsapConvertToKeyframes, } = useDomEditContext(); const { assets, fontAssets, projectDir, handleImportFiles, handleImportFonts, refreshFileTree, readProjectFile, writeProjectFile, fileTree, editingFile, } = useFileManagerContext(); // Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately, // no coalescing — each is a distinct user action that deserves its own undo entry. const onPersistSlideshow = useSlideshowPersist({ sdkSession, activeCompPath, readProjectFile, writeProjectFile, recordEdit, reloadPreview, domEditSaveTimestampRef, publishSdkSession, }); // Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures // rapid writes collapse into a single undo entry via the save-queue infra. const onPersistSlideshowNotes = useSlideshowPersist({ sdkSession, activeCompPath, readProjectFile, writeProjectFile, recordEdit, reloadPreview, domEditSaveTimestampRef, publishSdkSession, coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes", }); const { layersPanePercent, splitContainerRef, handleInspectorSplitResizeStart, handleInspectorSplitResizeMove, handleInspectorSplitResizeEnd, } = useInspectorSplitResize(); const renderJobs = renderQueue.jobs as RenderJob[]; const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers"; const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({ editingFileContent: editingFile?.content, previewIframeRef, refreshKey, rightPanelTab, setRightPanelTab, }); const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive; const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers; const handleInspectorPaneButtonClick = (pane: "design" | "layers") => { if (!inspectorTabActive) { setRightPanelTab(pane); return; } // Flat inspector: Layers always renders full-height by itself (see the // render branch below), so the two panes are mutually exclusive here — // otherwise both tabs could show "active" while only one actually shows. if (STUDIO_FLAT_INSPECTOR_ENABLED) { setExclusiveRightInspectorPane(pane); return; } toggleRightInspectorPane(pane); }; const handleApplyColorGradingScope = useCallback( async (scope: ColorGradingScope, value: string | null) => applyColorGradingScopeUpdate({ scope, value, selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html", fileTree, projectId, domEditSaveTimestampRef, waitForPendingDomEditSaves, readProjectFile, writeProjectFile, recordEdit, reloadPreview, showToast, }).catch((error) => { showToast( `Couldn't apply color grading: ${error instanceof Error ? error.message : String(error)}`, "error", ); return EMPTY_COLOR_GRADING_SCOPE_RESULT; }), [ activeCompPath, domEditSaveTimestampRef, domEditSelection?.sourceFile, fileTree, projectId, readProjectFile, recordEdit, reloadPreview, showToast, waitForPendingDomEditSaves, writeProjectFile, ], ); const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast); /** * A dial being dragged writes to the preview and stops there. * * Every one of these panels previews on each pointermove and commits on * release. Persisting the moves too put a fragment of the drag in the undo * stack — and since those writes race, history could not coalesce them * reliably, so undo took back a sliver of the gesture rather than the gesture. * The release's own commit is what reaches the file and the undo stack. */ const setAttributeWhileDragging = useCallback( (attr: string, value: string | null) => handleDomAttributeLiveCommit(attr, value, undefined, { previewOnly: true }), [handleDomAttributeLiveCommit], ); const handleHideAllSelected = () => { const { elements } = usePlayerStore.getState(); const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath); if (keys.length > 0) void onToggleElementHidden?.(keys, true); }; const propertyPanel = ( 1 ? null : domEditSelection} projectId={projectId} activeCompPath={activeCompPath} showToast={showToast} readProjectFile={readProjectFile} writeProjectFile={writeProjectFile} recordEdit={recordEdit} reloadPreview={reloadPreview} domEditSaveTimestampRef={domEditSaveTimestampRef} forceReloadSharedSdkSession={forceReloadSdkSession} > 1 ? null : domEditSelection} multiSelectCount={domEditGroupSelections.length} multiSelectedElements={domEditGroupSelections} onGroupSelection={handleGroupSelection} onHideAllSelected={handleHideAllSelected} copiedAgentPrompt={copiedAgentPrompt} onClearSelection={clearDomSelection} onToggleElementHidden={onToggleElementHidden} onAutoGroupCarveSources={onAutoGroupCarveSources} onUngroup={handleUngroupSelection} onSetStyle={handleDomStyleCommit} onSetAttribute={handleDomAttributeCommit} onSetAttributes={handleDomAttributesCommit} onSetAttributeLive={setAttributeWhileDragging} onSetAttributeQuiet={handleDomAttributeQuietCommit} onApplyColorGradingScope={handleApplyColorGradingScope} onSetHtmlAttribute={handleDomHtmlAttributeCommit} onRemoveBackground={handleRemoveBackground} onSetManualOffset={handleDomPathOffsetCommit} onSetManualSize={handleDomBoxSizeCommit} onSetManualRotation={handleDomRotationCommit} onSetText={handleDomTextCommit} onSetTextFieldStyle={handleDomTextFieldStyleCommit} onAddTextField={handleDomAddTextField} onRemoveTextField={handleDomRemoveTextField} onAskAgent={handleAskAgent} onImportAssets={handleImportFiles} onAddMediaOverlay={onAddMediaOverlay} fontAssets={fontAssets} onImportFonts={handleImportFonts} previewIframeRef={previewIframeRef} gsapAnimations={selectedGsapAnimations} gsapMultipleTimelines={gsapMultipleTimelines} gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern} onUpdateGsapProperty={handleGsapUpdateProperty} onUpdateGsapMeta={handleGsapUpdateMeta} onDeleteGsapAnimation={handleGsapDeleteAnimation} onAddGsapProperty={handleGsapAddProperty} onRemoveGsapProperty={handleGsapRemoveProperty} onUpdateGsapFromProperty={handleGsapUpdateFromProperty} onAddGsapFromProperty={handleGsapAddFromProperty} onRemoveGsapFromProperty={handleGsapRemoveFromProperty} onAddGsapAnimation={handleGsapAddAnimation} onCommitAnimatedProperty={commitAnimatedProperty} onCommitAnimatedProperties={commitAnimatedProperties} onAddKeyframe={handleGsapAddKeyframe} onRemoveKeyframe={handleGsapRemoveKeyframe} onConvertToKeyframes={(animId, duration) => handleGsapConvertToKeyframes(animId, undefined, duration) } onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)} onSetArcPath={handleSetArcPath} onUpdateArcSegment={handleUpdateArcSegment} onUnroll={handleUnroll} onUpdateKeyframeEase={handleUpdateKeyframeEase} onUpdateSegmentEase={handleUpdateSegmentEase} onSetAllKeyframeEases={handleSetAllKeyframeEases} recordingState={recordingState} recordingDuration={recordingDuration} onToggleRecording={onToggleRecording} /> ); const renderQueuePanel = ; return ( <> {/* Vertical resize divider: 3px visible seam, 13px hit zone via the inner div. */}
handlePanelResizeStart("right", e)} onPointerMove={handlePanelResizeMove} onPointerUp={handlePanelResizeEnd} onPointerCancel={handlePanelResizeEnd} onKeyDown={(e) => { if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return; e.preventDefault(); // Panel is right-anchored: ArrowLeft grows it, ArrowRight shrinks it. const delta = e.key === "ArrowLeft" ? 16 : -16; adjustPanelWidth("right", delta); }} > {/* Asymmetric hit zone: 8px into the preview's p-2 gutter (the only dead space), the 3px seam, 2px into the card. Stops short of the 24px WCAG 2.5.8 target because the next pixel each way is live. */}
{/* Visible hairline */}
{captionEditMode ? ( ) : ( <>
handleInspectorPaneButtonClick("design")} /> handleInspectorPaneButtonClick("layers")} /> 0 ? `Renders (${renderJobs.length})` : "Renders"} tooltip="Render queue and exports" active={rightPanelTab === "renders"} onClick={() => setRightPanelTab("renders")} /> {isSlideshowComposition && ( setRightPanelTab("slideshow")} /> )} setRightPanelTab("variables")} />
{rightPanelTab === "block-params" && activeBlockParams ? ( {})} /> ) : rightPanelTab === "slideshow" && isSlideshowComposition ? ( ) : rightPanelTab === "variables" ? ( ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
{propertyPanel}
) : layersPaneOpen ? ( ) : designPaneOpen ? ( propertyPanel ) : inspectorTabActive ? ( // Inspector tab selected but no pane can render (panes toggled // off, or inspector inactive during playback/recording): show an // explanation instead of silently rendering the render queue // under a highlighted inspector tab.

Inspector is unavailable right now — select the Design or Layers pane above, or pause playback/recording to inspect elements.

) : ( renderQueuePanel )}
)}
); }