feat(studio): storyboard markdown source editor (raw + live preview) (#1531)

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>
This commit is contained in:
James Russo
2026-06-17 14:59:40 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 13af5540c1
commit 29809069c8
6 changed files with 354 additions and 44 deletions
@@ -12,26 +12,26 @@ import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { oneDark } from "@codemirror/theme-one-dark";
import type { Extension } from "@codemirror/state";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown";
function getLanguageExtension(language: string) {
switch (language) {
case "html":
return html();
case "css":
return css();
case "javascript":
case "js":
case "typescript":
case "ts":
return javascript({
typescript: language === "typescript" || language === "ts",
});
default:
return html();
}
const LANGUAGE_EXTENSIONS: Record<string, () => Extension> = {
html: () => html(),
css: () => css(),
markdown: () => markdown(),
md: () => markdown(),
javascript: () => javascript(),
js: () => javascript(),
typescript: () => javascript({ typescript: true }),
ts: () => javascript({ typescript: true }),
};
function getLanguageExtension(language: string): Extension {
const factory = LANGUAGE_EXTENSIONS[language] ?? html;
return factory();
}
function detectLanguage(filePath: string): string {
@@ -45,6 +45,8 @@ function detectLanguage(filePath: string): string {
jsx: "javascript",
tsx: "typescript",
json: "javascript",
md: "markdown",
markdown: "markdown",
};
return map[ext] ?? "html";
}
@@ -74,6 +76,7 @@ export const SourceEditor = memo(function SourceEditor({
const contentRef = useRef(content);
contentRef.current = content;
// fallow-ignore-next-line complexity
const mountEditor = useCallback(
(node: HTMLDivElement | null) => {
if (editorRef.current) {