// scaffold-design.ts — write a minimal, BOOTABLE `.design/` into a folder (Phase 28). // // Used by two native-app flows: "New project" (create a GitHub repo → init a local // project) and the "open a repo with no Maude design system" fallback ("set it up?"). // It deliberately scaffolds only the MINIMUM the dev-server needs to boot (per // context.ts the only hard requirement is a `.design/` dir; config needs just // `name` + `designRoot`). A real design system is still created by /design:setup-ds // (agent-driven) — this just gets the project to open instead of crash-looping. import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { basename, join } from 'node:path'; const CONFIG_SCHEMA = 'https://raw.githubusercontent.com/1aGh/maude/main/apps/studio/config.schema.json'; // A neutral, token-free starter canvas seeded into `ui/` so a freshly-created project // opens to a real artboard instead of an empty studio (the "empty-studio after Start a // new project" onboarding gap). It depends ONLY on @maude/canvas-lib + inline styles — // no design-system tokens — so it renders before /design:setup-ds exists. The button // vocabulary mirrors the shipped GitPanel (Save version · Publish · Get latest). const STARTER_CANVAS_TSX = `/** * @canvas welcome · Your first canvas — seeded when a new Maude project is created. * @platform desktop * @stack React 19 · TSX · Bun.build * * Neutral + token-free so it renders the moment the project is created — before * /design:setup-ds builds a design system. Edit it, replace it, or delete it once you * have your own canvases. */ import { DCArtboard, DCSection, DesignCanvas } from "@maude/canvas-lib"; export default function Welcome() { return (
Your first canvas

You're in. This is a canvas.

Everything you design lives on canvases like this one. Edit it, or start fresh — your work saves on its own, and you can version and share it without ever opening a terminal.

{[ ["Save version", "keep a checkpoint, just for you"], ["Publish", "share it with your team"], ["Get latest", "pull in everyone else's work"], ].map(([title, desc]) => (
{title}
{desc}
))}

Next: build a design system, or just start editing this canvas.

); } `; const STARTER_CANVAS_META = { title: 'Welcome', subtitle: 'Your first canvas — seeded on project creation', brief: 'Neutral, token-free welcome canvas written by scaffoldDesign() so a new project opens to a real artboard instead of an empty studio. Safe to edit or delete.', platform: 'desktop', sections: [ { id: 'welcome', title: 'Welcome to Maude · START HERE', artboards: [{ id: 'welcome', label: 'WELCOME/01', width: 680, height: 440 }], }, ], css_mode: 'inline', layout: { artboards: [{ id: 'welcome', x: 0, y: 0 }] }, }; // ── "How to use Maude" — a 10-artboard reference canvas, seeded alongside // Welcome.tsx (Phase 1, DDR-166's onboarding plan — replaces the earlier // explainer-video plan item). Genuinely self-contained: no `system/maude/` // import (doesn't exist yet on a fresh project), tokens inlined as custom // properties. Source of truth for edits: .design/ui/How to use Maude.tsx in // this repo — re-sync this constant by hand after editing there. const HOW_TO_USE_MAUDE_TSX = `/** * @canvas How to use Maude · seeded into every new project (scaffold-design.ts) · 10 artboards, one per capability * @ds maude * @platform desktop * @opt_out palette * @artboards start-here | design-system | edit-canvas | ai-assistant | point-comment-draw | generate-images | photo-editing | media-and-templates | save-share-collab | draw-as-code * @brief A "how to use Maude" reference canvas seeded alongside Welcome.tsx on every new project. One artboard per capability cluster (start here, design system, editing, AI assistant, annotation-driven iteration, AI image generation, photo editing, media/templates, git+collab, draw-as-code). Real domain nouns, no Lorem, honest mini-mockups instead of live screenshots (the seeded copy has to work with zero server state). * @stack React 19 · TSX · Bun.build · css_mode=inline (matches Welcome.tsx's seeded-canvas precedent — no sibling CSS to keep in sync across the scaffold template) * @history .design/_history/how-to-use-maude/ * * Genuinely self-contained (no DS import): this canvas is SEEDED into a * brand-new project by scaffoldDesign() before any design system exists, so * unlike Studio Docs.tsx it can't rely on \`system/maude/\` being present. The * token VALUES below are copied from system/maude/colors_and_type.css's dark * theme (kept in sync by eye — this is a fixed onboarding surface, not a * living DS specimen) and applied as inline custom properties on \`Board\`, so * every \`var(--token)\` below still resolves without an external stylesheet. */ import { DCArtboard, DCSection, DesignCanvas } from "@maude/canvas-lib"; // Copied from system/maude/colors_and_type.css (dark theme) — see the note above. const TOKENS: React.CSSProperties = { "--bg-0": "oklch(0.165 0.012 255)", "--bg-1": "oklch(0.198 0.012 255)", "--bg-2": "oklch(0.232 0.013 255)", "--bg-3": "oklch(0.270 0.013 252)", "--border-subtle": "oklch(0.290 0.012 255)", "--border-default": "oklch(0.360 0.013 252)", "--fg-0": "oklch(0.955 0.005 250)", "--fg-1": "oklch(0.790 0.008 250)", "--fg-2": "oklch(0.660 0.010 250)", "--fg-3": "oklch(0.500 0.010 250)", "--accent": "oklch(0.680 0.180 268)", "--accent-muted": "oklch(0.460 0.110 268)", "--accent-tint": "color-mix(in oklab, oklch(0.680 0.180 268) 16%, transparent)", "--status-success": "oklch(0.760 0.150 162)", "--presence-agent": "oklch(0.700 0.190 322)", "--radius-sm": "5px", "--radius-md": "7px", "--radius-lg": "10px", "--radius-pill": "999px", "--space-2": "4px", "--space-3": "8px", "--space-4": "12px", "--space-5": "16px", "--space-6": "24px", "--space-7": "32px", "--font-display": '"Inter Tight", "Inter", system-ui, -apple-system, sans-serif', "--font-body": '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif', "--font-mono": '"JetBrains Mono", "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace', "--type-xs": "11px", "--lh-xs": "16px", "--type-sm": "12px", "--lh-sm": "18px", "--type-base": "14px", "--type-md": "16px", "--lh-md": "24px", "--type-2xl": "28px", "--tracking-tight": "-0.014em", "--tracking-wide": "0.04em", } as React.CSSProperties; /* ── Icon set — thin-stroke (1.4) geometric glyphs, no emoji (DS rule) ── */ const ICONS: Record = { cursor: , folder: , palette: (<>), layers: (<>), sliders: (<>), message: , sparkle: , pen: (<>), pin: (<>), highlighter: , image: (<>), wand: (<>), scissors: (<>), drop: , grid: (<>), git: (<>), users: (<>), play: , code: (<>), check: , "arrow-right": (<>), "chevron-right": , terminal: (<>), }; function Icon({ name, size = 16, style }: { name: string; size?: number; style?: React.CSSProperties }) { return ( ); } function Kbd({ children }: { children: React.ReactNode }) { return ( {children} ); } /* ── Shared frame chrome ── */ function Board({ children }: { children: React.ReactNode }) { return (
{children}
); } function ArtHeader({ n, total, eyebrow, title }: { n: number; total: number; eyebrow: string; title: string }) { return (
{eyebrow}

{title}

{String(n).padStart(2, "0")} / {String(total).padStart(2, "0")}
); } function Body({ children }: { children: React.ReactNode }) { return
{children}
; } function Lede({ children }: { children: React.ReactNode }) { return

{children}

; } function FeatureCard({ icon, title, body }: { icon: string; title: string; body: string }) { return (
{title}
{body}
); } function CardRow({ children }: { children: React.ReactNode }) { return
{children}
; } // A real, hands-on instruction — every one of these describes something you // can genuinely do RIGHT NOW on this artboard or in this app (verified // against the actual keyboard-shortcut table in client/app.jsx, not guessed). function TryIt({ children }: { children: React.ReactNode }) { return (
Try it {children}
); } // A big hand-drawn-feel curved arrow + numbered callout, pointing at a real // interactive target elsewhere in the artboard (the same "point at it" // vocabulary the product itself uses for annotations). function PointAt({ x, y, w = 150, h = 70, flip = false, children }: { x: number; y: number; w?: number; h?: number; flip?: boolean; children: React.ReactNode }) { // Starts below the label (clears the text instead of striking through it) // and ends short of the box's bottom edge (clears whatever sits just below). const path = flip ? \`M\${w - 10} 20 C \${w * 0.4} 18, \${w * 0.15} \${h * 0.45}, 6 \${h - 28}\` : \`M6 20 C \${w * 0.6} 18, \${w * 0.85} \${h * 0.45}, \${w - 6} \${h - 28}\`; return (
{children}
); } function StepRow({ n, title, body }: { n: number; title: string; body: string }) { return (
{n}
{title}
{body}
); } function MiniPanelHd({ label, right }: { label: string; right?: React.ReactNode }) { return (
{label} {right}
); } function Panel({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return
{children}
; } /* ─────────────────────────── 01 · Start here ─────────────────────────── */ function ArtStartHere() { return ( Everything you design lives on a canvas. Canvases are grouped in the file tree on the left. A fresh project starts with two groups: Design system (your tokens and components) and UI kit (the screens you build).
} />
Design system
(empty until set up)
UI kit
Welcome.tsx
How to use Maude.tsx
How to make video.tsx
Click "How to make video.tsx" in the file list on the left, right now. That's the real file tree, not a picture of it.
); } /* ────────────────────── 02 · Build a design system ───────────────────── */ function ArtDesignSystem() { const swatches = ["oklch(0.68 0.18 268)", "oklch(0.76 0.15 162)", "oklch(0.80 0.13 78)", "oklch(0.66 0.19 25)"]; return ( Ask the Assistant to run /design:setup-ds and it interviews you: a handful of sharp questions about your product, mood, and brand. Then it generates a real, tokenized system: color, type, space, motion, and components. No blank Figma file, no picking a font at random.
Colors
{swatches.map((c) => )}
Type
Display heading
Body copy at a readable measure.
Open the Assistant (⌘⇧A) and type /design:setup-ds. It starts the real interview.
); } /* ────────────────────────── 03 · Edit a canvas ────────────────────────── */ function ArtEditCanvas() { return ( This isn't a mockup. The button below is a real element on this real artboard. Try the two moves that cover most editing:
Get started
① ⌘-click this button
The Inspector opens (or press ⌘⇧I). Try changing its Fill or Radius, or double-click "Get started" to retype it.
-click the button above right now. Then in the Inspector's CSS tab, change its background. This artboard updates live, the same way any canvas does.
); } /* ───────────────────────── 04 · The AI Assistant ──────────────────────── */ function ArtAiAssistant() { return ( The chat panel runs on your own Claude subscription: never a separate key, never metered billing. It sees whatever's open and selected, so short requests work: "make this bigger," not a paragraph of context.
Ready} />
Make the hero title bigger and tighten the tracking
Bumped it to var(--type-3xl) and tightened tracking. Want it bolder too?
Press ⌘⇧A right now to open the Assistant. It's the sparkle icon, top-right of the window.
); } /* ──────────────────── 05 · Point, comment, draw → it acts ────────────── */ function ArtPointCommentDraw() { return ( You don't have to describe a change in words. The box below is real. Try it:
① ⌘-click this box
② right-click it for the annotation menu, or press ⌘⇧M to open Comments and drop a pin on it.
Right-click the box above right now. The context menu has highlighter, sticky notes, and freehand drawing. Anything you mark stays attached to that exact spot as context for the Assistant. ); } /* ──────────────── 06 · Generate images with your own AI key ───────────── */ function ArtGenerateImages() { return ( Bring your own Google (Nano Banana) key and generate an image right onto the canvas, via ⌘K → "Generate with AI", or just ask in the chat ("generate a hero image of a mountain lake"). The result lands as a normal asset you can place, edit, and re-generate.
} />
Press ⌘K right now and pick "Generate with AI…" from the list.
); } /* ─────────────────────────── 07 · Photo editing ───────────────────────── */ function ArtPhotoEditing() { return ( Select any photo and open the Photo tab in the Inspector: adjustments, duotone, grain, masking, and one-click background removal. Every edit is non-destructive, the original stays untouched.
{["Brightness", "Contrast", "Saturation"].map((k) => (
{k}
))}
Drop any photo from Finder onto this canvas, ⌘-click it, then open the Photo tab (⌘⇧I) to try Remove background. ); } /* ────────────────────── 08 · Drop in media & templates ────────────────── */ function ArtMediaTemplates() { return ( Drag images, video, or audio straight from Finder onto the canvas: one file or a whole batch. Paste a link to unfurl it. Create or delete a canvas from the file tree without ever touching the filesystem yourself. Drag any image file from Finder and drop it right on this artboard, now. It lands as a real asset. ); } /* ────────────────── 09 · Save, share, collaborate ──────────────────────── */ function ArtSaveShareCollab() { return ( Version control lives in plain words: Save version keeps a checkpoint just for you, Publish shares it with your team, Get latest pulls in everyone else's work. No terminal, no git vocabulary required.
Press ⌘⇧G right now to open Changes. It tracks everything since your last save, live.
); } /* ───────────────────────────── 10 · Draw as code ───────────────────────── */ function ArtDrawAsCode() { return ( Ask for a logo, icon, or diagram and it's built by a deterministic geometry engine: real computed vectors, never a guessed path. Animate the result once and ship one file that plays identically on web and native.
Open the Assistant (⌘⇧A) and ask it to draw a simple compass icon. Watch it build from geometry, not guess a path.
); } export default function Canvas() { return ( ); } `; const HOW_TO_USE_MAUDE_META = { title: 'How to use Maude', subtitle: 'Reference canvas seeded into every new project \u2014 10 artboards, one per capability', brief: 'Onboarding reference canvas (replaces the Phase 1 explainer-video plan item). Seeded alongside Welcome.tsx by scaffold-design.ts on every new project. One artboard per capability cluster: start here, design system, editing, AI assistant, point/comment/draw iteration, AI image generation (Nano Banana), photo editing, media & templates, save/share/collaborate, draw-as-code. Honest inline mini-mockups (no live screenshots \u2014 has to work with zero server state on a freshly scaffolded project).', platform: 'desktop', designSystem: 'maude', opt_out_scope: 'none', sections: [ { id: 'how-to-use-maude', title: 'How to use Maude', subtitle: 'REFERENCE \u00b7 10 artboards, one per capability', }, ], artboards: [ { id: 'start-here', label: '01 \u00b7 Start here', }, { id: 'design-system', label: '02 \u00b7 Design system', }, { id: 'edit-canvas', label: '03 \u00b7 Edit a canvas', }, { id: 'ai-assistant', label: '04 \u00b7 AI Assistant', }, { id: 'point-comment-draw', label: '05 \u00b7 Point, comment, draw', }, { id: 'generate-images', label: '06 \u00b7 Generate images', }, { id: 'photo-editing', label: '07 \u00b7 Photo editing', }, { id: 'media-and-templates', label: '08 \u00b7 Media & templates', }, { id: 'save-share-collab', label: '09 \u00b7 Save, share, collaborate', }, { id: 'draw-as-code', label: '10 \u00b7 Draw as code', }, ], layout: { artboards: [ { id: 'start-here', x: 0, y: 0, }, { id: 'design-system', x: 1120, y: 0, }, { id: 'edit-canvas', x: 2240, y: 0, }, { id: 'ai-assistant', x: 3360, y: 0, }, { id: 'point-comment-draw', x: 4480, y: 0, }, { id: 'generate-images', x: 5600, y: 0, }, { id: 'photo-editing', x: 6720, y: 0, }, { id: 'media-and-templates', x: 7840, y: 0, }, { id: 'save-share-collab', x: 8960, y: 0, }, { id: 'draw-as-code', x: 10080, y: 0, }, ], }, css_mode: 'inline', iteration_count: 1, }; // ── "How to make video" — the video-comp companion (7 artboards). Same // self-contained shape. Source of truth: .design/ui/How to make video.tsx. const HOW_TO_MAKE_VIDEO_TSX = `/** * @canvas How to make video · seeded into every new project (scaffold-design.ts) · 7 artboards, one per video capability * @ds maude * @platform desktop * @opt_out palette * @artboards start-video-comp | timeline | ai-assistant-video | footage-to-cut | generate-video-audio | captions | export * @brief A "how to make video" reference canvas seeded alongside How to use Maude.tsx on every new project. One artboard per capability cluster: what a video-comp is, the Timeline, the Assistant for video, turning raw footage into a cut, AI video/audio generation, automatic captions (whisper.cpp / cloud), export. Honest inline mini-mockups, no live screenshots. * @stack React 19 · TSX · Bun.build · css_mode=inline (matches Welcome.tsx's seeded-canvas precedent) * @history .design/_history/how-to-make-video/ * * Genuinely self-contained (no DS import) — same reasoning as How to use * Maude.tsx: this is seeded into a brand-new project before any design * system exists, so it can't rely on \`system/maude/\` being present. Token * VALUES are copied from system/maude/colors_and_type.css's dark theme and * applied as inline custom properties on \`Board\`. */ import { DCArtboard, DCSection, DesignCanvas, VideoComp } from "@maude/canvas-lib"; import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; // Copied from system/maude/colors_and_type.css (dark theme) — kept in sync // by eye with How to use Maude.tsx's identical block (a fixed onboarding // surface, not a living DS specimen). const TOKENS: React.CSSProperties = { "--bg-0": "oklch(0.165 0.012 255)", "--bg-1": "oklch(0.198 0.012 255)", "--bg-2": "oklch(0.232 0.013 255)", "--bg-3": "oklch(0.270 0.013 252)", "--border-subtle": "oklch(0.290 0.012 255)", "--border-default": "oklch(0.360 0.013 252)", "--fg-0": "oklch(0.955 0.005 250)", "--fg-1": "oklch(0.790 0.008 250)", "--fg-2": "oklch(0.660 0.010 250)", "--fg-3": "oklch(0.500 0.010 250)", "--accent": "oklch(0.680 0.180 268)", "--accent-muted": "oklch(0.460 0.110 268)", "--accent-tint": "color-mix(in oklab, oklch(0.680 0.180 268) 16%, transparent)", "--radius-sm": "5px", "--radius-md": "7px", "--radius-lg": "10px", "--space-2": "4px", "--space-3": "8px", "--space-4": "12px", "--space-5": "16px", "--space-6": "24px", "--space-7": "32px", "--font-display": '"Inter Tight", "Inter", system-ui, -apple-system, sans-serif', "--font-body": '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif', "--font-mono": '"JetBrains Mono", "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace', "--type-xs": "11px", "--type-sm": "12px", "--lh-sm": "18px", "--type-base": "14px", "--type-md": "16px", "--lh-md": "24px", "--type-2xl": "28px", "--tracking-tight": "-0.014em", "--tracking-wide": "0.04em", } as React.CSSProperties; /* ── Icon set ── */ const ICONS: Record = { play: , pause: (<>), film: (<>), scissors: (<>), message: , folder: , eye: (<>), sparkle: , volume: (<>), waveform: (<>), captions: (<>), download: (<>), check: , wand: (<>), drag: (<>), }; function Icon({ name, size = 16, style }: { name: string; size?: number; style?: React.CSSProperties }) { return ( ); } function Kbd({ children }: { children: React.ReactNode }) { return ( {children} ); } // Real, hands-on instructions (same shape as How to use Maude.tsx's TryIt — // verified against the actual keyboard-shortcut table in client/app.jsx). function TryIt({ children }: { children: React.ReactNode }) { return (
Try it {children}
); } function PointAt({ x, y, w = 150, h = 70, flip = false, children }: { x: number; y: number; w?: number; h?: number; flip?: boolean; children: React.ReactNode }) { const path = flip ? \`M\${w - 6} 6 C \${w * 0.35} 4, \${w * 0.1} \${h * 0.5}, 6 \${h - 6}\` : \`M6 6 C \${w * 0.65} 4, \${w * 0.9} \${h * 0.5}, \${w - 6} \${h - 6}\`; return (
{children}
); } /* ── Shared chrome (same shapes as How to use Maude.tsx) ── */ function Board({ children }: { children: React.ReactNode }) { return (
{children}
); } function ArtHeader({ n, total, eyebrow, title }: { n: number; total: number; eyebrow: string; title: string }) { return (
{eyebrow}

{title}

{String(n).padStart(2, "0")} / {String(total).padStart(2, "0")}
); } function Body({ children }: { children: React.ReactNode }) { return
{children}
; } function Lede({ children }: { children: React.ReactNode }) { return

{children}

; } function FeatureCard({ icon, title, body }: { icon: string; title: string; body: string }) { return (
{title}
{body}
); } function CardRow({ children }: { children: React.ReactNode }) { return
{children}
; } function MiniPanelHd({ label, right }: { label: string; right?: React.ReactNode }) { return (
{label} {right}
); } function Panel({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return
{children}
; } function Chip({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return {children}; } function PrimaryButton({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return ; } /* ─────────────────────── 01 · Start a video comp ──────────────────────── */ function ArtStartVideoComp() { return ( A video comp is a canvas artboard whose body is a real Remotion composition. You author it in TSX like any other canvas, but every animated value is a function of a frame number, so it plays back, scrubs, and exports frame-perfect.
0:04} />
Ask the Assistant (⌘⇧A) to add a video comp to this canvas. A 2-second title card is a good first try.
); } // A real, tiny frame-driven Remotion comp — not a mockup. 90 frames @ 30fps // (3s): a mark springs in, then keeps rotating for the rest of the loop, so // scrubbing the REAL Timeline against this REAL comp shows real motion at // every point, not a frozen frame (DDR-094 — freeze-frames lie). const TIMELINE_DEMO_FRAMES = 90; const TimelineDemoAnim = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const pop = spring({ frame, fps, config: { damping: 200 } }); const scale = interpolate(pop, [0, 1], [0.6, 1]); const rotate = interpolate(frame, [0, TIMELINE_DEMO_FRAMES], [0, 360]); return (
); }; /* ────────────────────────── 02 · Edit on the Timeline ─────────────────── */ // Accepts the VideoComp as \`children\` (not rendered inline) — canvas-lib.tsx's // \`subtreeHasVideoComp()\` walks the STATIC JSX tree via \`props.children\` to // decide whether to render the real top-right video badge, and it does NOT // reach inside a function component's own return value — only literal JSX // nesting from the DCArtboard call site down. So the VideoComp must arrive // as a literal child at that call site (see Canvas() below), not be // constructed inside this function's own body. Real bug, caught by checking // \`document.querySelector('.dc-artboard-video-badge')\` live and finding // nothing, not by assuming the mockup-style code would just work. function ArtTimeline({ children }: { children: React.ReactNode }) { const clips = [ { w: 90, label: "intro" }, { w: 140, label: "clip-a" }, { w: 110, label: "clip-b" }, { w: 70, label: "outro" }, ]; return ( {/* Points at the REAL video-artboard badge canvas-lib.tsx renders top-right (top:4px, right:6px) the instant any real is in this artboard's subtree — not drawn by this canvas, just aimed at. */}
① that real icon, or ⌘⇧T
The clip on the left is a real, playing Remotion comp, not a screenshot. Click the small video icon in this artboard's own top-right corner (real chrome, not drawn by this canvas), or press ⌘⇧T, to open the real Timeline and scrub it.
{children}
00:00 / 00:12
{clips.map((c, i) => (
{c.label}
))}
(a 4-clip cut, for scale: the real player on the left is the actual demo)
Click the small video-camera icon in this artboard's own top-right corner right now (or press ⌘⇧T). The real Timeline opens, and you can scrub the clip on the left frame by frame.
); } /* ───────────────────── 03 · AI Assistant for video ─────────────────────── */ function ArtAiAssistantVideo() { return ( Ask for beats, transitions, and motion graphics in plain language. The Assistant writes real Remotion, frame-driven and deterministic, never a CSS animation that would tear on export.
Add a kinetic-type title card, then crossfade into the first clip
Added a per-letter spring-in title, 18-frame crossfade into clip-a. Want a chromatic-split flourish on the title too?
Prompt ideas
{["Crossfade these three clips with a music bed", "Add a lower-third caption to every clip", "Cinematic grade, teal/orange, subtle grain", "Ken-Burns push on the hero shot", "Kinetic-type title, per-letter spring-in"].map((p) => (
"{p}"
))}
Open the Assistant (⌘⇧A) and paste one of the prompts on the right, against any video comp you've already added.
); } /* ───────────────────── 04 · Turn footage into a cut ────────────────────── */ function ArtFootageToCut() { return ( Drop a folder of raw footage on the canvas and ask for a reel. An analyst watches every clip first (good moments, subject, motion, quality), then a director assembles an edit decision list before a single frame of comp code is written.
{[["folder", "Raw clips"], ["eye", "Analyst watches"], ["wand", "Director cuts"], ["film", "Comp generated"]].map(([icon, label], i, arr) => (
{label}
{i < arr.length - 1 ? : null}
))}
Drag a folder of real clips from Finder onto this canvas, then ask the Assistant (⌘⇧A) for "a reel from these clips."
); } /* ─────────────────── 05 · Generate video & audio ────────────────────────── */ function ArtGenerateVideoAudio() { return ( Bring your own key and fill a gap without hand-sourcing it. A generated clip drops into your assets like real footage, and Maude checks what you've already made before spending a new generation. Add your key under File → Settings → AI generation, then ask the Assistant for "a 4-second clip of ocean waves at sunset." ); } /* ──────────────────────────── 06 · Captions ──────────────────────────────── */ function ArtCaptions() { return ( Subtitles are free and need no key. whisper.cpp runs locally (one-click model download) and writes word-timed captions on any clip. Prefer a cloud engine? Pick ElevenLabs Scribe or Groq in Settings. Maude never silently switches you to a paid engine.
the canvas fills in live
local · whisper.cpp cloud · Scribe
Select a clip on this canvas, then ask the Assistant (⌘⇧A) to "add captions to this clip."
); } /* ────────────────────────────── 07 · Export ────────────────────────────── */ function ArtExport() { return ( ⌘⇧E opens the export dialog: MP4 (H.264) or palette-quantized GIF, resolved from the comp's own frame rate and duration. Everything renders through Maude's own capture spine: no renderer binaries, no install step for whoever opens the project next.
MP4 GIF
Export ⌘⇧E
Press ⌘⇧E right now. The real export dialog opens for whatever's active.
); } export default function Canvas() { return ( ); } `; const HOW_TO_MAKE_VIDEO_META = { title: 'How to make video', subtitle: 'Reference canvas seeded into every new project \u2014 7 artboards, one per video capability', brief: 'Onboarding reference canvas (companion to How to use Maude.tsx). Seeded by scaffold-design.ts on every new project. One artboard per video-comp capability: starting a comp, the Timeline, the Assistant for video, turning raw footage into a cut, AI video/audio generation, automatic captions, export.', platform: 'desktop', designSystem: 'maude', opt_out_scope: 'none', sections: [ { id: 'how-to-make-video', title: 'How to make video', subtitle: 'REFERENCE \u00b7 7 artboards, one per video capability', }, ], artboards: [ { id: 'start-video-comp', label: '01 \u00b7 Start a video comp', }, { id: 'timeline', label: '02 \u00b7 Edit on the Timeline', }, { id: 'ai-assistant-video', label: '03 \u00b7 AI Assistant for video', }, { id: 'footage-to-cut', label: '04 \u00b7 Footage into a cut', }, { id: 'generate-video-audio', label: '05 \u00b7 Generate video & audio', }, { id: 'captions', label: '06 \u00b7 Automatic captions', }, { id: 'export', label: '07 \u00b7 Export', }, ], layout: { artboards: [ { id: 'start-video-comp', x: 0, y: 0, }, { id: 'timeline', x: 1120, y: 0, }, { id: 'ai-assistant-video', x: 2240, y: 0, }, { id: 'footage-to-cut', x: 3360, y: 0, }, { id: 'generate-video-audio', x: 4480, y: 0, }, { id: 'captions', x: 5600, y: 0, }, { id: 'export', x: 6720, y: 0, }, ], }, css_mode: 'inline', iteration_count: 1, }; /** Whether `dir` is already a Maude project (has `.design/config.json`). */ export function hasDesign(dir: string): boolean { return existsSync(join(dir, '.design', 'config.json')); } export interface ScaffoldResult { ok: boolean; error?: string; } /** Scaffold a minimal bootable `.design/` (no design system yet). Refuses to * clobber an existing project. `name` is the human project label. */ export function scaffoldDesign(dir: string, name?: string): ScaffoldResult { const designDir = join(dir, '.design'); const configPath = join(designDir, 'config.json'); if (existsSync(configPath)) { return { ok: false, error: 'This folder is already a Maude project.' }; } const projectName = (name && name.trim()) || basename(dir) || 'Untitled'; try { mkdirSync(join(designDir, 'ui'), { recursive: true }); mkdirSync(join(designDir, 'system'), { recursive: true }); const config = { $schema: CONFIG_SCHEMA, name: projectName, designRoot: '.design', canvasGroups: [ { label: 'Design system', path: 'system' }, { label: 'UI kit', path: 'ui' }, ], designSystems: [], completenessProfile: 'standard', }; writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); // Seed a starter canvas so the studio opens to a real artboard, not an empty list. writeFileSync(join(designDir, 'ui', 'Welcome.tsx'), STARTER_CANVAS_TSX, 'utf8'); writeFileSync( join(designDir, 'ui', 'Welcome.meta.json'), `${JSON.stringify(STARTER_CANVAS_META, null, 2)}\n`, 'utf8' ); // Seed the two onboarding reference canvases (Phase 1, DDR-166) — every // new project gets a live, in-app "how to" alongside the blank Welcome // canvas, covering the full capability surface with honest mini-mockups // (no live screenshots — has to work with zero server state). writeFileSync(join(designDir, 'ui', 'How to use Maude.tsx'), HOW_TO_USE_MAUDE_TSX, 'utf8'); writeFileSync( join(designDir, 'ui', 'How to use Maude.meta.json'), `${JSON.stringify({ ...HOW_TO_USE_MAUDE_META, created: new Date().toISOString().slice(0, 10) }, null, 2)}\n`, 'utf8' ); writeFileSync(join(designDir, 'ui', 'How to make video.tsx'), HOW_TO_MAKE_VIDEO_TSX, 'utf8'); writeFileSync( join(designDir, 'ui', 'How to make video.meta.json'), `${JSON.stringify({ ...HOW_TO_MAKE_VIDEO_META, created: new Date().toISOString().slice(0, 10) }, null, 2)}\n`, 'utf8' ); // `system/` is genuinely empty until /design:setup-ds — keep it in git. writeFileSync(join(designDir, 'system', '.gitkeep'), '', 'utf8'); return { ok: true }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'Could not set up the project.' }; } }