{"slug":"storyboard-html-preview","title":"storyboard-html-preview","summary":"Render a structured storyboard (storyboard.md, shots.json, text-overlays.json, brand-lock.snapshot.md) into a single-file HTML preview that is shareable, printable, and offline. Use when the user wants to share a storyboard, export for review, hand off to an editor, or print a ha","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-24T15:43:10.168926Z","repo":{"url":"https://github.com/whystrohm/shotkit","stars":21,"forks":5,"license":"Apache-2.0","updatedAt":"2026-09-24T12:25:06Z"},"bodyHtml":"<hr>\n<h2>name: storyboard-html-preview\ndescription: Render a structured storyboard (storyboard.md, shots.json, text-overlays.json, brand-lock.snapshot.md) into a single-file HTML preview that is shareable, printable, and offline. Use when the user wants to share a storyboard, export for review, hand off to an editor, or print a hard copy. Triggers on \"preview the storyboard\", \"share this\", \"export to HTML\", \"print version\", or after a storyboard-architect run. Produces one self-contained .html file with no build or server.</h2>\n<h1>Storyboard HTML Preview</h1>\n<p>You are turning structured storyboard files into a single shareable HTML document. The output is what an editor, stakeholder, or client opens in a browser without thinking about it.</p>\n<p>The constraint is non-negotiable: <strong>single file, no build step, no server, works offline.</strong> Any time the output requires \"run this build command\" or \"host this somewhere,\" the skill has failed.</p>\n<h2>When to use</h2>\n<p>Trigger when the user:</p>\n<ul>\n<li>Asks to preview, share, or export a storyboard</li>\n<li>Wants a printable version</li>\n<li>Says \"what's the next step\" after a storyboard-architect run</li>\n<li>Hands off <code>storyboard.md</code> + <code>shots.json</code> + asks for a deliverable for review</li>\n</ul>\n<h2>What you produce</h2>\n<p>One file: <code>preview.html</code>. Self-contained. Inline CSS. No JavaScript dependencies (vanilla JS only, embedded). No external font files (uses system stack with brand-font fallbacks). No external images (placeholder slots; if generated images exist, embed as base64 OR reference relative paths).</p>\n<pre><code>output/\n├── run.json                   # input, for the run id and date\n├── storyboard.md              # input\n├── shots.json                 # input\n├── text-overlays.json         # input\n├── brand-lock.snapshot.md     # input\n├── frames/round-N/            # input, if generation has happened\n├── critiques/round-N/         # input, for the verdict badges\n└── preview.html               # ← what this skill produces\n</code></pre>\n<p>If the user has generated frames, the HTML references them via relative paths so the file\nworks when the whole <code>output/</code> folder is shared. Resolve a shot's frame in this order:</p>\n<ol>\n<li>An entry in <code>shot.assets.generated</code> marked <code>accepted: true</code></li>\n<li>The newest entry in <code>shot.assets.generated</code></li>\n<li><code>frames/round-{highest}/{shot_id}.{png,jpg,jpeg,webp}</code></li>\n<li><code>generated/{shot_id}.{ext}</code>, the pre-3.0.0 flat layout</li>\n</ol>\n<p>Data first, convention second. Reading the path convention first meant the page showed\nwhatever file happened to sit there, accepted or rejected, first draft or fifth re-roll.</p>\n<p>If no frames exist yet, the HTML uses styled placeholder cards with the shot spec, still\nuseful for review and handoff.</p>\n<h2>Workflow</h2>\n<h3>Step 1. Read inputs</h3>\n<p>Required:</p>\n<ul>\n<li><code>shots.json</code></li>\n<li><code>text-overlays.json</code></li>\n<li><code>brand-lock.snapshot.md</code></li>\n</ul>\n<p>Optional:</p>\n<ul>\n<li><code>run.json</code> (for the run id and date; without it the page says \"not recorded\")</li>\n<li><code>storyboard.md</code> (for narrative context, surface the brief at the top)</li>\n<li><code>frames/round-N/{shot_id}.{png,jpg,jpeg,webp}</code> (if generation has happened)</li>\n<li><code>critiques/round-N/{shot_id}.critique.json</code> (for verdict badges)</li>\n</ul>\n<p>Validate before rendering, and stop if it fails:</p>\n<pre><code>python tools/validate_shots.py output/\n</code></pre>\n<h3>Step 2. Extract brand parameters</h3>\n<p>From <code>brand-lock.snapshot.md</code>, extract:</p>\n<ul>\n<li>Palette (hex values), used for HTML accent colors</li>\n<li>Display font and body font names, used as font-family values with system fallbacks</li>\n<li>Brand voice / mood, used in subtle copy choices</li>\n</ul>\n<p>The HTML preview should <em>feel</em> like the brand without going overboard. Quiet branding, not loud.</p>\n<h3>Step 3. Generate the HTML</h3>\n<p>Use <code>templates/preview.html.tpl</code> as the structural template. Read it before generating.</p>\n<p>The HTML structure:</p>\n<pre><code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n  &lt;meta&gt;\n  &lt;title&gt;{project title}&lt;/title&gt;\n  &lt;style&gt;\n    /* All CSS inline. ~200 lines. Brand-aware. */\n    /* Print stylesheet included. */\n  &lt;/style&gt;\n&lt;/head&gt;\n&lt;body&gt;\n  &lt;header&gt;\n    &lt;!-- Project title, duration, aspect, generated timestamp --&gt;\n  &lt;/header&gt;\n\n  &lt;section class=\"brief\"&gt;\n    &lt;!-- Brief summary if storyboard.md provides one --&gt;\n  &lt;/section&gt;\n\n  &lt;section class=\"series-lock\"&gt;\n    &lt;!-- Character / environment / lighting / color grade --&gt;\n  &lt;/section&gt;\n\n  &lt;section class=\"shots\"&gt;\n    &lt;!-- One card per shot --&gt;\n    &lt;article class=\"shot\" id=\"shot_01\"&gt;\n      &lt;div class=\"shot-frame\"&gt;\n        &lt;!-- generated image OR styled placeholder --&gt;\n      &lt;/div&gt;\n      &lt;div class=\"shot-meta\"&gt;\n        &lt;!-- timestamp, framing, angle, motion --&gt;\n      &lt;/div&gt;\n      &lt;div class=\"shot-subject\"&gt;\n        &lt;!-- subject description --&gt;\n      &lt;/div&gt;\n      &lt;div class=\"shot-text-overlay\"&gt;\n        &lt;!-- if on_screen_text exists, show overlay content with timing --&gt;\n      &lt;/div&gt;\n      &lt;div class=\"shot-rationale\"&gt;\n        &lt;!-- rationale text --&gt;\n      &lt;/div&gt;\n    &lt;/article&gt;\n    &lt;!-- ... --&gt;\n  &lt;/section&gt;\n\n  &lt;footer&gt;\n    &lt;!-- audit trail: brand-lock snapshot reference, timestamp --&gt;\n  &lt;/footer&gt;\n\n  &lt;script&gt;\n    /* Vanilla JS only. Optional: keyboard nav, jump-to-shot, expand/collapse. */\n  &lt;/script&gt;\n&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<h3>Step 4. Embed frames if available</h3>\n<p>Resolve each shot's frame by the order in \"What you produce\" above, then reference it by a\npath relative to the output root:</p>\n<pre><code>&lt;img src=\"frames/round-2/shot_01.png\" alt=\"shot_01: hook\" loading=\"lazy\" /&gt;\n</code></pre>\n<p>This works when the whole output folder is zipped and shared.</p>\n<p>For hard-copy print (a single file with no folder structure), the skill can offer to inline\nframes as base64. Ask the user which they prefer if frames are present.</p>\n<p>If a shot's <code>assets.generated</code> entry carries a <code>sha256</code> and the file no longer matches it,\nrender the frame but say so on the page. That mismatch means the frame changed after it was\nrecorded, which is exactly the case where a preview quietly showing the new file is worse\nthan one that flags it.</p>\n<p>If no frames exist, render styled placeholder cards showing the framing, subject, and shot\nspec. These are still useful for stakeholder review at the storyboard stage.</p>\n<p><strong>Template flag convention.</strong> When composing the per-shot context for <code>preview.html.tpl</code>, set exactly one of:</p>\n<ul>\n<li><code>has_image: true</code> and <code>image_path: \"frames/round-2/shot_NN.png\"</code>, when a frame exists</li>\n<li><code>has_no_image: true</code>, when none does (renders the placeholder card)</li>\n</ul>\n<p>The template uses two parallel <code>{{#if}}</code> blocks rather than <code>{{#if}}/{{else}}</code> to keep the rendering portable across template engines.</p>\n<p>For text overlays, set <code>has_overlays: true</code> and an <code>overlays</code> array on the shot. Each entry\ncarries <code>id</code>, <code>content</code>, <code>font</code>, <code>weight</code>, <code>color</code>, <code>size</code>, <code>position_class</code>,\n<code>position_label</code>, <code>enter_at</code>, <code>enter_animation</code>, <code>exit_at</code>, <code>exit_animation</code>. The template\niterates that array with <code>{{#each overlays}}</code>.</p>\n<p>It is an array because <code>shots.json</code> lets a shot carry several overlays and\n<code>text-overlays.json</code> always did. A single set of <code>overlay_*</code> fields could hold one, so the\nsecond overlay on a shot rendered nowhere and nothing reported it.</p>\n<p>For verdict badges, set <code>has_verdict</code>, <code>verdict</code>, <code>verdict_round</code>, and <code>verdict_class</code>\n(the lowercased verdict) from the newest critique for that shot under <code>critiques/</code>. Omit\nthem when the shot has no critique.</p>\n<p><strong>Escape everything.</strong> Subjects, rationales, VO lines, and overlay copy are model-generated\nprose that lands in both text and attribute contexts. One angle bracket in a rationale, or\none quote in an overlay font name, breaks the page a client is reading.\n<code>tools/shots-to-html.py</code> escapes every substitution by default and reserves raw output for\nthe inlined CSS alone.</p>\n<h3>Step 5. Render text overlays visually</h3>\n<p>For every shot with an <code>on_screen_text</code> reference, the HTML shows:</p>\n<ul>\n<li>The text content rendered in approximately the brand font (or visible fallback)</li>\n<li>The position indicated visually (lower-third, center, etc.)</li>\n<li>Timing info (enter/exit beats)</li>\n</ul>\n<p>This gives the reviewer a sense of what the final composited frame will look like, even before final compositing happens.</p>\n<h3>Step 6. Print stylesheet</h3>\n<p>Include <code>@media print</code> rules that:</p>\n<ul>\n<li>Hide nav, footer scripts, expand/collapse UI</li>\n<li>Force one shot per page (or two if compact)</li>\n<li>Ensure text overlays render legibly</li>\n<li>Use black-on-white where brand colors won't print well</li>\n</ul>\n<p>The user should be able to hit Cmd-P / Ctrl-P and get a clean PDF.</p>\n<h2>Hard rules</h2>\n<h3>Rule 1. Single file, no exceptions</h3>\n<p>The output is one <code>.html</code> file. If you find yourself wanting a separate stylesheet or JS file, inline it. If you find yourself wanting a build step, you're solving the wrong problem.</p>\n<h3>Rule 2. No external dependencies at runtime</h3>\n<p>No CDN scripts. No Google Fonts. No external CSS frameworks. The file must work with no internet connection.</p>\n<p>The exception: if the user explicitly opts in (e.g. \"make it pretty, I'm online\"), Tailwind via CDN is acceptable. Default is no.</p>\n<h3>Rule 3. Print must work</h3>\n<p>Hit Cmd-P. The result should be a clean PDF. If layout breaks across page boundaries, the print stylesheet is broken.</p>\n<h3>Rule 4. Brand-aware but quiet</h3>\n<p>Use brand colors as accents, not as full backgrounds. The reviewer's job is to read the storyboard, not admire the design. Subtle.</p>\n<h3>Rule 5. Mobile-readable</h3>\n<p>Stakeholders open links on phones. The HTML should be readable on mobile without horizontal scroll. Simple responsive CSS.</p>\n<h2>Templates</h2>\n<ul>\n<li><code>templates/preview.html.tpl</code>, the structural template</li>\n<li><code>templates/styles.css.tpl</code>, the CSS to inline</li>\n<li><code>templates/print.css.tpl</code>, the print rules</li>\n</ul>\n<p>The skill reads all three and assembles them into a single <code>preview.html</code>.</p>\n<h2>Quality bar</h2>\n<p>Before declaring done, verify:</p>\n<ul>\n<li><input disabled=\"disabled\" type=\"checkbox\"> File opens in any browser (Chrome, Safari, Firefox) with no errors</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> No external network requests fire on load</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Print preview produces a clean PDF</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Mobile viewport (375px) renders without horizontal scroll</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Brand colors and fonts come from the brand-lock, not from a fallback</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Every shot from <code>shots.json</code> is present</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Every overlay referenced by a shot is rendered, including second and third overlays</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> <code>brand_lock_ref</code> from <code>shots.json</code> is what the footer links to, not a hardcoded name</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> The run date and the render date are both shown, and labelled differently</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> No <code>{{</code> remains anywhere in the output</li>\n</ul>\n<p>The CLI renderer checks the mechanical half of that list against itself:</p>\n<pre><code>python tools/shots-to-html.py --selftest\n</code></pre>\n<h2>Two timestamps, not one</h2>\n<p>\"Run\" is when the storyboard was produced, read from <code>run.json</code>. \"Rendered\" is when the page\nwas written. They are separate lines in the footer and they must stay separate.</p>\n<p>Collapsing them into a single \"Generated\" date meant re-rendering a preview six months later\nrestamped the run as today, and the footer went on asserting the page was built against a\nbrand-lock on a date that had nothing to do with the frames above it.</p>\n<p>If the brand-lock on disk no longer hashes to what <code>run.json</code> recorded, say so on the page.\nThe reader is looking at frames built against a brand state they can no longer see.</p>\n<h2>Examples</h2>\n<p>Generated <code>preview.html</code> files ship next to the storyboards that produced them:</p>\n<ul>\n<li><code>../storyboard-architect/examples/30s-pain-proof-promise/preview.html</code></li>\n<li><code>../storyboard-architect/examples/60s-founder-explainer/preview.html</code></li>\n<li><code>../storyboard-architect/examples/shotkit-explainer/preview.html</code>, including the\ntwo-overlay shot</li>\n<li><code>../visual-asset-critic/examples/worked-run/preview.html</code>, with frames and verdict badges</li>\n</ul>\n<p>Open them in a browser to calibrate quality. All four are re-rendered in CI with pinned\ntimestamps and the build fails if the output moves, so they are also the regression test for\nthis skill's output.</p>\n","files":[{"path":"SKILL.md","sizeBytes":11610,"isText":true},{"path":"templates/preview.html.tpl","sizeBytes":5419,"isText":false},{"path":"templates/print.css.tpl","sizeBytes":2938,"isText":false},{"path":"templates/styles.css.tpl","sizeBytes":9816,"isText":false}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-24T15:44:13.500205Z","sha256":"67570A3DAF5233E30B4F39984C83E62FC33247D61534A0263184DE500FDC95AE","sizeBytes":10465},"review":null,"source":{"repositoryUrl":"https://github.com/whystrohm/shotkit","path":"skills/storyboard-html-preview","license":"Apache-2.0","commit":"1b56831c035b11f24278365962ad11d2fb2f5de4","subtreeSha":"574E5E1B28D39B42E6DD3038E4F9F85C25E88E73E8EC9787A9AA5B0454A7509E","lastSyncedAt":"2026-09-24T15:43:09.838799Z"},"reviewedAt":"2026-09-24T15:47:18.333576Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/whystrohm/shotkit/tree/main/skills/storyboard-html-preview"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install whystrohm-shotkit@llmmart"},{"target":"git","command":"git clone https://github.com/whystrohm/shotkit.git"}]}