import { useMemo, useState } from "react"; import type { StoryboardResponse } from "../../hooks/useStoryboard"; import { StoryboardDirection } from "./StoryboardDirection"; import { StoryboardGrid } from "./StoryboardGrid"; import { StoryboardStatusLegend } from "./StoryboardStatusLegend"; import { StoryboardScriptPanel } from "./StoryboardScriptPanel"; import { StoryboardSourceEditor, type SourceFile } from "./StoryboardSourceEditor"; import { StoryboardFrameFocus } from "./StoryboardFrameFocus"; type SubView = "board" | "source"; export interface StoryboardLoadedProps { projectId: string; data: StoryboardResponse; /** Re-fetch the manifest after a source edit is saved. */ reload: () => void; /** Select a composition in the timeline (used by "Open in Preview"). */ onSelectComposition: (path: string) => void; } function clampIndex(index: number, count: number): number { return Math.max(1, Math.min(count, index)); } /** A storyboard that exists on disk: Board (contact sheet) ↔ Source ↔ frame focus. */ // fallow-ignore-next-line complexity export function StoryboardLoaded({ projectId, data, reload, onSelectComposition, }: StoryboardLoadedProps) { const [subView, setSubView] = useState("board"); const [sourceDirty, setSourceDirty] = useState(false); const [focusedIndex, setFocusedIndex] = useState(null); const sourceFiles = useMemo(() => { const files: SourceFile[] = [{ path: data.path, label: data.path }]; if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path }); return files; // Depend on the stable fields, not the `data.script` object — every reload() // produces a fresh object and would needlessly re-create this array. }, [data.path, data.script?.path, data.script?.exists]); // Leaving the source editor drops its in-memory buffer; confirm when it's dirty. // fallow-ignore-next-line complexity const changeSubView = (next: SubView) => { if (next === subView) return; if ( subView === "source" && sourceDirty && !window.confirm("Discard unsaved markdown changes?") ) { return; } setSubView(next); }; const focusedFrame = focusedIndex != null ? (data.frames.find((f) => f.index === focusedIndex) ?? null) : null; if (focusedFrame) { return ( setFocusedIndex(null)} onNavigate={(delta) => setFocusedIndex(clampIndex(focusedFrame.index + delta, data.frames.length)) } onSaved={reload} onSelectComposition={onSelectComposition} /> ); } return (
{subView === "board" ? (
{data.script && }
) : ( )}
); } const SUB_VIEWS: Array<{ value: SubView; label: string }> = [ { value: "board", label: "Board" }, { value: "source", label: "Source" }, ]; function SubViewToggle({ value, onChange }: { value: SubView; onChange: (next: SubView) => void }) { return (
{SUB_VIEWS.map((option) => ( ))}
); }