import { useCallback, useMemo, useRef, useState, type MutableRefObject, type PointerEvent as ReactPointerEvent, } from "react"; import { Tooltip } from "./ui"; import { PropertyPanel } from "./editor/PropertyPanel"; import { LayersPanel } from "./editor/LayersPanel"; import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel"; import { BlockParamsPanel } from "./editor/BlockParamsPanel"; import { RenderQueue } from "./renders/RenderQueue"; import { SlideshowPanel } from "./panels/SlideshowPanel"; import type { SceneInfo } from "./panels/SlideshowPanel"; import type { RenderJob } from "./renders/useRenderQueue"; import type { BlockParam } from "@hyperframes/core/registry"; import type { IframeWindow } from "../player/lib/playbackTypes"; import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability"; import type { Composition } from "@hyperframes/sdk"; import type { EditHistoryKind } from "../utils/editHistory"; import { useSlideshowPersist } from "../hooks/useSlideshowPersist"; 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"; const MIN_INSPECTOR_SPLIT_PERCENT = 20; const MAX_INSPECTOR_SPLIT_PERCENT = 75; export interface StudioRightPanelProps { designPanelActive: boolean; activeBlockParams?: { blockName: string; blockTitle: string; params: BlockParam[]; compositionPath: string; } | null; onCloseBlockParams?: () => void; recordingState?: "idle" | "recording" | "preview"; recordingDuration?: number; onToggleRecording?: () => void; /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */ sdkSession: Composition | null; reloadPreview: () => void; domEditSaveTimestampRef: MutableRefObject; recordEdit: (entry: { label: string; kind: EditHistoryKind; files: Record; }) => Promise; } // fallow-ignore-next-line complexity export function StudioRightPanel({ designPanelActive, activeBlockParams, onCloseBlockParams, recordingState, recordingDuration, onToggleRecording, sdkSession, reloadPreview, domEditSaveTimestampRef, recordEdit, }: StudioRightPanelProps) { const { rightWidth, rightPanelTab, setRightPanelTab, rightInspectorPanes, toggleRightInspectorPane, handlePanelResizeStart, handlePanelResizeMove, handlePanelResizeEnd, } = usePanelLayoutContext(); const { previewIframeRef, projectId, activeCompPath, compositionDimensions, waitForPendingDomEditSaves, renderQueue, } = useStudioShellContext(); const { captionEditMode, refreshKey } = useStudioPlaybackContext(); const { domEditSelection, domEditGroupSelections, copiedAgentPrompt, clearDomSelection, handleDomStyleCommit, handleDomAttributeCommit, handleDomAttributeLiveCommit, handleDomHtmlAttributeCommit, handleDomPathOffsetCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomTextCommit, handleDomTextFieldStyleCommit, handleDomAddTextField, handleDomRemoveTextField, handleAskAgent, selectedGsapAnimations, gsapMultipleTimelines, gsapUnsupportedTimelinePattern, handleGsapUpdateProperty, handleGsapUpdateMeta, handleGsapDeleteAnimation, handleGsapAddAnimation, handleGsapAddProperty, handleGsapRemoveProperty, handleGsapUpdateFromProperty, handleGsapAddFromProperty, handleGsapRemoveFromProperty, commitAnimatedProperty, handleSetArcPath, handleUpdateArcSegment, handleUnroll, handleUpdateKeyframeEase, handleGsapAddKeyframe, handleGsapRemoveKeyframe, handleGsapConvertToKeyframes, } = useDomEditContext(); const { assets, fontAssets, projectDir, handleImportFiles, handleImportFonts, readProjectFile, writeProjectFile, } = 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, }); // 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, coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes", }); const [layersPanePercent, setLayersPanePercent] = useState(40); const splitContainerRef = useRef(null); const splitDragRef = useRef<{ startY: number; startPercent: number; height: number; } | null>(null); const renderJobs = renderQueue.jobs as RenderJob[]; const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers"; // Derive scene list from the live clip manifest in the preview iframe. // fallow-ignore-next-line complexity const slideshowScenes = useMemo(() => { try { const win = previewIframeRef.current?.contentWindow as IframeWindow | null; return (win?.__clipManifest?.scenes ?? []).map((s) => ({ id: s.id, label: s.label, start: s.start, duration: s.duration, })); } catch { return []; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [previewIframeRef, rightPanelTab, refreshKey]); const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive; const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED; const handleInspectorPaneButtonClick = (pane: "design" | "layers") => { if (!inspectorTabActive) { setRightPanelTab(pane); return; } toggleRightInspectorPane(pane); }; const handleInspectorSplitResizeStart = useCallback( (event: ReactPointerEvent) => { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0; splitDragRef.current = { startY: event.clientY, startPercent: layersPanePercent, height, }; }, [layersPanePercent], ); const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent) => { const drag = splitDragRef.current; if (!drag || drag.height <= 0) return; const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100; const next = Math.min( MAX_INSPECTOR_SPLIT_PERCENT, Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent), ); setLayersPanePercent(next); }, []); const handleInspectorSplitResizeEnd = useCallback(() => { splitDragRef.current = null; }, []); const propertyPanel = ( 1 ? null : domEditSelection} multiSelectCount={domEditGroupSelections.length} copiedAgentPrompt={copiedAgentPrompt} onClearSelection={clearDomSelection} onSetStyle={handleDomStyleCommit} onSetAttribute={handleDomAttributeCommit} onSetAttributeLive={handleDomAttributeLiveCommit} onSetHtmlAttribute={handleDomHtmlAttributeCommit} onSetManualOffset={handleDomPathOffsetCommit} onSetManualSize={handleDomBoxSizeCommit} onSetManualRotation={handleDomRotationCommit} onSetText={handleDomTextCommit} onSetTextFieldStyle={handleDomTextFieldStyleCommit} onAddTextField={handleDomAddTextField} onRemoveTextField={handleDomRemoveTextField} onAskAgent={handleAskAgent} onImportAssets={handleImportFiles} 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} onAddKeyframe={handleGsapAddKeyframe} onRemoveKeyframe={handleGsapRemoveKeyframe} onConvertToKeyframes={handleGsapConvertToKeyframes} onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)} onSetArcPath={handleSetArcPath} onUpdateArcSegment={handleUpdateArcSegment} onUnroll={handleUnroll} onUpdateKeyframeEase={handleUpdateKeyframeEase} recordingState={recordingState} recordingDuration={recordingDuration} onToggleRecording={onToggleRecording} /> ); const renderQueuePanel = ( { await waitForPendingDomEditSaves(); const composition = activeCompPath && activeCompPath !== "index.html" ? activeCompPath : undefined; await renderQueue.startRender({ fps, quality, format, resolution, composition, }); }} compositionDimensions={compositionDimensions} isRendering={renderQueue.isRendering} /> ); return ( <>
handlePanelResizeStart("right", e)} onPointerMove={handlePanelResizeMove} onPointerUp={handlePanelResizeEnd} >
{captionEditMode ? ( ) : ( <>
{STUDIO_INSPECTOR_PANELS_ENABLED && ( <> )}
{rightPanelTab === "block-params" && activeBlockParams ? ( {})} /> ) : rightPanelTab === "slideshow" ? ( ) : layersPaneOpen && designPaneOpen ? (
{propertyPanel}
) : layersPaneOpen ? ( ) : designPaneOpen ? ( propertyPanel ) : ( renderQueuePanel )}
)}
); }