Skip to content

Commit dbfaf4f

Browse files
jrusso1020claude
andcommitted
feat(studio): storyboard markdown source editor (raw + live preview)
Fourth PR in the Studio storyboarding stack. Adds an in-context way to view and edit the storyboard's canonical files. - Board | Source sub-toggle inside the storyboard view (StoryboardLoaded). - StoryboardSourceEditor: raw CodeMirror markdown editor + live rendered preview (marked), with a file switcher for STORYBOARD.md and SCRIPT.md. - Loads raw file text and saves via the existing files API (GET/PUT /projects/:id/files/*); on save the Board re-parses (reload), so markdown stays the single source of truth. Cmd/Ctrl+S to save. - Deliberately raw, not WYSIWYG, so the structured frame fields can't be mangled. - SourceEditor gains markdown language support (@codemirror/lang-markdown); adds the marked dependency for preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f35dc0c commit dbfaf4f

7 files changed

Lines changed: 315 additions & 91 deletions

File tree

bun.lock

Lines changed: 26 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/studio/fixtures/storyboard-sample/index.html

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=1920, height=1080" />
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=1920, height=1080">
66
<title>Storyboard Demo</title>
77
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
88
<style>
@@ -27,50 +27,11 @@
2727
</style>
2828
</head>
2929
<body>
30-
<div
31-
id="root"
32-
data-composition-id="main"
33-
data-start="0"
34-
data-duration="16"
35-
data-width="1920"
36-
data-height="1080"
37-
>
38-
<div
39-
id="frame-1"
40-
class="scene"
41-
data-composition-id="frame-01-hook"
42-
data-composition-src="compositions/frames/01-hook.html"
43-
data-start="0"
44-
data-duration="3"
45-
data-track-index="1"
46-
></div>
47-
<div
48-
id="frame-2"
49-
class="scene"
50-
data-composition-id="frame-02-problem"
51-
data-composition-src="compositions/frames/02-problem.html"
52-
data-start="3"
53-
data-duration="4"
54-
data-track-index="1"
55-
></div>
56-
<div
57-
id="frame-3"
58-
class="scene"
59-
data-composition-id="frame-03-feature"
60-
data-composition-src="compositions/frames/03-feature.html"
61-
data-start="7"
62-
data-duration="5"
63-
data-track-index="1"
64-
></div>
65-
<div
66-
id="frame-4"
67-
class="scene"
68-
data-composition-id="frame-04-proof"
69-
data-composition-src="compositions/frames/04-proof.html"
70-
data-start="12"
71-
data-duration="4"
72-
data-track-index="1"
73-
></div>
30+
<div data-hf-id="hf-dr1f" id="root" data-composition-id="main" data-start="0" data-duration="16" data-width="1920" data-height="1080">
31+
<div data-hf-id="hf-k4h4" id="frame-1" class="scene" data-composition-id="frame-01-hook" data-composition-src="compositions/frames/01-hook.html" data-start="0" data-duration="3" data-track-index="1"></div>
32+
<div data-hf-id="hf-zp8j" id="frame-2" class="scene" data-composition-id="frame-02-problem" data-composition-src="compositions/frames/02-problem.html" data-start="3" data-duration="4" data-track-index="1"></div>
33+
<div data-hf-id="hf-cp6l" id="frame-3" class="scene" data-composition-id="frame-03-feature" data-composition-src="compositions/frames/03-feature.html" data-start="7" data-duration="5" data-track-index="1"></div>
34+
<div data-hf-id="hf-tj71" id="frame-4" class="scene" data-composition-id="frame-04-proof" data-composition-src="compositions/frames/04-proof.html" data-start="12" data-duration="4" data-track-index="1"></div>
7435
</div>
7536

7637
<script>

packages/studio/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@codemirror/lang-css": "^6.3.1",
3333
"@codemirror/lang-html": "^6.4.9",
3434
"@codemirror/lang-javascript": "^6.2.2",
35+
"@codemirror/lang-markdown": "^6.3.4",
3536
"@codemirror/language": "^6.12.2",
3637
"@codemirror/search": "^6.6.0",
3738
"@codemirror/state": "^6.6.0",
@@ -42,6 +43,8 @@
4243
"@hyperframes/sdk": "workspace:*",
4344
"@phosphor-icons/react": "^2.1.10",
4445
"bpm-detective": "^2.0.5",
46+
"dompurify": "^3.2.4",
47+
"marked": "^14.1.4",
4548
"mediabunny": "^1.45.3"
4649
},
4750
"devDependencies": {

packages/studio/src/components/editor/SourceEditor.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,26 @@ import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language
1212
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
1313
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
1414
import { oneDark } from "@codemirror/theme-one-dark";
15+
import type { Extension } from "@codemirror/state";
1516
import { html } from "@codemirror/lang-html";
1617
import { css } from "@codemirror/lang-css";
1718
import { javascript } from "@codemirror/lang-javascript";
19+
import { markdown } from "@codemirror/lang-markdown";
1820

19-
function getLanguageExtension(language: string) {
20-
switch (language) {
21-
case "html":
22-
return html();
23-
case "css":
24-
return css();
25-
case "javascript":
26-
case "js":
27-
case "typescript":
28-
case "ts":
29-
return javascript({
30-
typescript: language === "typescript" || language === "ts",
31-
});
32-
default:
33-
return html();
34-
}
21+
const LANGUAGE_EXTENSIONS: Record<string, () => Extension> = {
22+
html: () => html(),
23+
css: () => css(),
24+
markdown: () => markdown(),
25+
md: () => markdown(),
26+
javascript: () => javascript(),
27+
js: () => javascript(),
28+
typescript: () => javascript({ typescript: true }),
29+
ts: () => javascript({ typescript: true }),
30+
};
31+
32+
function getLanguageExtension(language: string): Extension {
33+
const factory = LANGUAGE_EXTENSIONS[language] ?? html;
34+
return factory();
3535
}
3636

3737
function detectLanguage(filePath: string): string {
@@ -45,6 +45,8 @@ function detectLanguage(filePath: string): string {
4545
jsx: "javascript",
4646
tsx: "typescript",
4747
json: "javascript",
48+
md: "markdown",
49+
markdown: "markdown",
4850
};
4951
return map[ext] ?? "html";
5052
}
@@ -74,6 +76,7 @@ export const SourceEditor = memo(function SourceEditor({
7476
const contentRef = useRef(content);
7577
contentRef.current = content;
7678

79+
// fallow-ignore-next-line complexity
7780
const mountEditor = useCallback(
7881
(node: HTMLDivElement | null) => {
7982
if (editorRef.current) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useMemo, useState } from "react";
2+
import type { StoryboardResponse } from "../../hooks/useStoryboard";
3+
import { StoryboardDirection } from "./StoryboardDirection";
4+
import { StoryboardGrid } from "./StoryboardGrid";
5+
import { StoryboardStatusLegend } from "./StoryboardStatusLegend";
6+
import { StoryboardScriptPanel } from "./StoryboardScriptPanel";
7+
import { StoryboardSourceEditor, type SourceFile } from "./StoryboardSourceEditor";
8+
9+
type SubView = "board" | "source";
10+
11+
export interface StoryboardLoadedProps {
12+
projectId: string;
13+
data: StoryboardResponse;
14+
/** Re-fetch the manifest after a source edit is saved. */
15+
reload: () => void;
16+
}
17+
18+
/** A storyboard that exists on disk: Board (contact sheet) ↔ Source (markdown editor). */
19+
export function StoryboardLoaded({ projectId, data, reload }: StoryboardLoadedProps) {
20+
const [subView, setSubView] = useState<SubView>("board");
21+
const sourceFiles = useMemo<SourceFile[]>(() => {
22+
const files: SourceFile[] = [{ path: data.path, label: data.path }];
23+
if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
24+
return files;
25+
}, [data.path, data.script]);
26+
27+
return (
28+
<div className="flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200">
29+
<div className="flex items-center border-b border-neutral-800 px-4 py-2">
30+
<SubViewToggle value={subView} onChange={setSubView} />
31+
</div>
32+
{subView === "board" ? (
33+
<div className="flex-1 min-h-0 overflow-auto">
34+
<div className="mx-auto max-w-[1400px] px-8 py-8">
35+
<StoryboardDirection globals={data.globals} frameCount={data.frames.length} />
36+
<div className="mt-5">
37+
<StoryboardStatusLegend />
38+
</div>
39+
<StoryboardGrid projectId={projectId} frames={data.frames} />
40+
{data.script && <StoryboardScriptPanel script={data.script} />}
41+
</div>
42+
</div>
43+
) : (
44+
<StoryboardSourceEditor files={sourceFiles} onSaved={reload} />
45+
)}
46+
</div>
47+
);
48+
}
49+
50+
const SUB_VIEWS: Array<{ value: SubView; label: string }> = [
51+
{ value: "board", label: "Board" },
52+
{ value: "source", label: "Source" },
53+
];
54+
55+
function SubViewToggle({ value, onChange }: { value: SubView; onChange: (next: SubView) => void }) {
56+
return (
57+
<div className="flex items-center gap-0.5 rounded-md bg-neutral-900 p-0.5" role="tablist">
58+
{SUB_VIEWS.map((option) => (
59+
<button
60+
key={option.value}
61+
type="button"
62+
role="tab"
63+
aria-selected={value === option.value}
64+
onClick={() => onChange(option.value)}
65+
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
66+
value === option.value
67+
? "bg-neutral-700 text-neutral-100"
68+
: "text-neutral-400 hover:text-neutral-200"
69+
}`}
70+
>
71+
{option.label}
72+
</button>
73+
))}
74+
</div>
75+
);
76+
}

0 commit comments

Comments
 (0)