Prototyping

How to build a single-file HTML prototype on this system, mark what is not real, ask for a decision, and hand it to an engineer.

Prototypes are single HTML files in the prototypes repo; a push deploys them to prototypes.wootdev.com and a PR gets pr-N.prototypes.wootdev.com. They are how a product manager tests an idea in an afternoon and how an engineer later knows what to build. A prototype on this system looks like the product because it uses the same tokens, not because somebody matched the colors by eye.

The public-site rule#

prototypes.wootdev.com is public. Everything in a prototype is invented:

Violet means "not proposed UI"#

Anything violet on a prototype is a note to the reader, never part of the design (DS-023). base.css ships the convention: .ptag for an inline tag, .standin for a placeholder block, and the --proto, --proto-wash, --proto-ink, --proto-line tokens for anything custom.

<h3>All Periods <span class="ptag">v2: drag handle moves here</span></h3>
<div class="standin">Real chart renders here; see ai_insights_scatterplot.png</div>

Use it for: open questions, "this is where the real thing goes", version notes, anything a reviewer must not mistake for a proposal. Do not use violet in the design itself; #6c4fc7 is not a Saga color, which is the point.

Two ways to get the system into a single file (DS-035). Link when the page lives on the prototypes site; inline when it must stand alone.

Linked (default for prototypes repo files):

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://style.wootdev.com/css/tokens.css">
<link rel="stylesheet" href="https://style.wootdev.com/css/base.css">
<link rel="stylesheet" href="https://style.wootdev.com/css/components.css">

The page tracks the system: when a token changes, every linked prototype changes with it, which is what you want for something that is only ever viewed live.

Inlined (a Claude artifact, an attachment, a file opened from disk, a page that must look the same in a year; starter/saga-starter-offline.html already is this): paste the contents of tokens.css, base.css and the parts of components.css you use into a <style> block, in that order, with the first line of the block saying which commit they came from:

<style>
/* saga product-style tokens.css + base.css @ <sha>, https://style.wootdev.com */
:root { … }
</style>

Either way, the prototype's own CSS comes after, references tokens only, and never redefines a --saga-* or --color-* value. A prototype that needs a color the system lacks defines a --proto-* or a page-local --x-* variable and says so in a .ptag.

The starter template#

starter/saga-starter.html is the file to copy (generated by npm run build from starter/saga-starter.template.html; it ships with the site at /starter/saga-starter.html). Its shape, top to bottom:

  1. A header comment: the build it came from, the public-site and violet rules, and a pointer to the offline copy.
  2. <head>: viewport meta, the Google Fonts preconnects and Montserrat link, the iconify-icon web component, then three <link> lines pulling tokens.css, base.css and components.css from style.wootdev.com, and an empty <style id="prototype-only"> for the prototype's own rules. The offline copy, starter/saga-starter-offline.html, is the same page with the three files inlined verbatim in #saga-tokens, #saga-base and #saga-components blocks, for a file that must open with no network.
  3. The shell, exactly the markup of the shell chapter: .saga-shell with a .saga-nav (logo, "Current Program" .saga-select trigger, "Configure program" link, four nav items with Sessions active, district footer) and a .saga-shell__main holding a .saga-topbar (h1 title, a .ptag in the actions slot) and a .saga-page.
  4. One .saga-card in the page with a "Start here" heading and three buttons (primary, secondary, quiet) to delete.
  5. No JavaScript.

It is a page, not a kit: open it, delete the card, build in the space that remains. Anything the system lacks goes in #prototype-only, tokens only, with a .ptag beside the thing it styles so the gap stays visible.

Decision boards#

A decision board is a prototype whose job is to get an answer. Nathan's convention (design_system/*.html in the prototypes repo): a sticky control panel on the left changes the proposal live, the staged scenes on the right show it in context, and a "Copy preferences" button serialises the current choices to text the reviewer pastes onto the GitHub issue.

The board is a page around the shell, so it does not use the starter's viewport-height shell directly: each scene is a frame with a .saga-shell--framed inside it (the shell fills the frame instead of the window; .saga-scrim--local and .saga-toast-stack--local keep overlays inside the frame). The layout needs three rules, which go in #prototype-only:

/* Decision board: page wrap, two columns, sticky panel. */
.board { max-width: 90rem; margin: 0 auto; padding: var(--space-xl) var(--space-lg); display: grid; grid-template-columns: 18rem minmax(0, 1fr); gap: var(--space-lg); align-items: start; }
.board__panel { position: sticky; top: var(--space-lg); display: grid; gap: var(--space-md); }
.board__scene { height: 32rem; border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; }
<div class="board">
  <aside class="board__panel saga-card">
    <h4>Proposal</h4>
    <div class="saga-field">
      <label class="saga-label" for="radius">Button radius</label>
      <input class="saga-input" id="radius" type="range" min="4" max="8" step="2" data-pref="radius" value="6">
    </div>
    <label class="saga-check"><input type="checkbox" class="saga-checkbox" data-pref="flatModalButtons" checked> Modal buttons are flat</label>
    <button class="saga-btn saga-btn--primary" id="copy">Copy preferences</button>
    <p class="saga-help" id="copied" hidden>Copied. Paste it on the issue.</p>
  </aside>
  <main>
    <div class="board__scene"><div class="saga-shell saga-shell--framed">…</div></div>
  </main>
</div>
<script>
  const prefs = () => Object.fromEntries([...document.querySelectorAll('[data-pref]')]
    .map(el => [el.dataset.pref, el.type === 'checkbox' ? el.checked : el.value]));
  document.querySelectorAll('[data-pref]').forEach(el => el.addEventListener('input', () => {
    document.documentElement.style.setProperty('--btn-radius', prefs().radius + 'px');
  }));
  document.getElementById('copy').onclick = async () => {
    await navigator.clipboard.writeText('```json\n' + JSON.stringify(prefs(), null, 2) + '\n```');
    document.getElementById('copied').hidden = false;
  };
</script>

Rules for a board:

Handing it to an engineer#

The tokens map 1:1. --color-primary in the prototype is --color-primary in dash's tokens.css, Connect's --cv3-* aliases and coach's --color-* resolve to the same hex, so an engineer picks up the file and reads the design off the CSS, not off a screenshot.

Prototype Svelte (dash core-ui) Vanilla (Connect)
.saga-btn StandardButton .saga-btn from components.css
.saga-card, .saga-grid, .saga-list SagaCard, page layout the classes
.saga-input, .saga-select, .saga-datepicker SagaInput, SelectMenu, DatePicker (dash's CI bans a native <select>) the classes; a native <select> in .saga-select for a short plain list, the painted trigger and menu when it needs search, groups or multi-select
.saga-tabs Tabs .saga-tabs
.saga-modal + .saga-scrim Modal, ConfirmDialog util/modal.ts (showModal)
.saga-toast showToast from @saga-ed/dash-core-ui/toast util/toast.ts
.saga-skeleton Skeleton, SkeletonList .saga-skeleton
.pop-in, .overlay-in, .list-in popIn, overlayPanel, listItem the CSS classes
.ptag, .standin delete delete

What the engineer does:

  1. Reads the header comment for the questions the prototype left open, and the .ptags for the answers.
  2. Replaces each .saga-* block with the matching component; the props are the modifiers (--primary becomes variant="primary").
  3. Moves any page-local --x-* variable into the app's Tier 3 alias layer, or files a ledger entry if it is really a missing token.
  4. Deletes every violet thing.
  5. Links the prototype URL from the PR so reviewers can compare.

What the engineer does not do: copy the prototype's CSS. If the prototype needed a rule the system lacks, that is a gap in the system, not a thing to paste.

Checklist before opening the PR#