mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
feat(studio): storyboard view-mode toggle and shell (#1529)
Second PR in the Studio storyboarding stack. Adds the top-level toggle between the storyboard and the timeline/preview stage, behind the flag. - STUDIO_STORYBOARD_ENABLED flag (VITE_STUDIO_ENABLE_STORYBOARD, default off) now gates the UI. - ViewModeContext: timeline|storyboard state mirrored to the ?view= query param, so it survives reloads and an agent can deep-link ?view=storyboard. - Segmented Storyboard|Preview control in StudioHeader (flag-gated). - StudioApp swaps the whole center stage for a full-width StoryboardView when storyboard mode is active. - useStoryboard hook + StoryboardView shell: global-direction header, loading/error/empty states. The frame contact-sheet grid lands in PR3. - Extract StudioOverlays from App.tsx to stay within the 600-line studio decomposition budget. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a13a07074
commit
195d7aa7bd
@@ -0,0 +1,72 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type {
|
||||
StoryboardFrame,
|
||||
StoryboardGlobals,
|
||||
StoryboardWarning,
|
||||
} from "@hyperframes/core/storyboard";
|
||||
import { buildProjectApiPath } from "../utils/projectRouting";
|
||||
|
||||
/** A frame as returned by the API: parsed frame + disk-resolution info. */
|
||||
export interface StoryboardFrameView extends StoryboardFrame {
|
||||
/** Whether `src` resolves to an existing file inside the project. */
|
||||
srcExists: boolean;
|
||||
}
|
||||
|
||||
/** Shape of `GET /api/projects/:id/storyboard`. */
|
||||
export interface StoryboardResponse {
|
||||
exists: boolean;
|
||||
path: string;
|
||||
globals: StoryboardGlobals;
|
||||
frames: StoryboardFrameView[];
|
||||
warnings: StoryboardWarning[];
|
||||
}
|
||||
|
||||
export interface UseStoryboardResult {
|
||||
data: StoryboardResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the parsed storyboard manifest for a project. Markdown stays canonical on
|
||||
* disk; this fetches the server-derived JSON the storyboard view renders.
|
||||
*/
|
||||
export function useStoryboard(projectId: string | null): UseStoryboardResult {
|
||||
const [data, setData] = useState<StoryboardResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
|
||||
const reload = useCallback(() => setReloadKey((k) => k + 1), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Route through buildProjectApiPath so the (URL-derived) projectId is encoded
|
||||
// into the path rather than interpolated raw (CodeQL js/client-side-request-forgery).
|
||||
fetch(buildProjectApiPath(projectId, "/storyboard"))
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`storyboard request failed: ${res.status}`);
|
||||
return res.json() as Promise<StoryboardResponse>;
|
||||
})
|
||||
.then((json) => {
|
||||
if (!cancelled) setData(json);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : "failed to load storyboard");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId, reloadKey]);
|
||||
|
||||
return { data, loading, error, reload };
|
||||
}
|
||||
Reference in New Issue
Block a user