Skip to content

Authoring a mockup

How to build a new mockup HTML so it lands inside the SageOx visual register and works with the publish pipeline.

What a mockup is. A mockup is a stress test for the design system, not a pixel spec for a screen. It asks: can our tokens, components, and rules produce this? Where they can't, the gap is the lesson — codify it into DESIGN.md, the tokens, a new component, a new lint rule. Once codified, agents start producing real product surfaces against it. Mockups stay as archaeology so future readers see where a rule came from. (Philosophy: DESIGN.md §0; practice: HARNESS.md Loop A.)

This is mockup chrome, not product code. The toggles below (Mode, Calm, Implementor notes) live in the mockup-only header so reviewers can audit both modes during a design review — the real app exposes none of them.

File layout

A mockup is a self-contained directory under proposals/YYYY-MM-DD-<slug>/:

proposals/2026-05-15-kb-pages-mockup/
  v1/index.html      ← optional earlier revisions kept for history
  v2/index.html      ← what gets published

Single-file proposals (one HTML, no versions) skip the v1//v2/ split:

proposals/2026-04-25-cast-v1/
  index.html
  mockups/<variant>/cast-calm.html …
  images/<variant>/*.png           ← rendered thumbnails (checked in)
  images/_assets/*.jpg             ← source assets

Rules:

  • Self-contained. No build step. Google Fonts via CDN (<link href="https://fonts.googleapis.com/css2?…">). All CSS inline in a <style> block.
  • Tokens are mirrored, not imported. Copy the relevant subset of tokens/*.yaml into the mockup's :root CSS block. A mockup that breaks when the canonical tokens change has done its job.
  • No JavaScript dependencies. A tiny inline <script> for the chrome toggles is fine; nothing more.
  • Rendered thumbnails check in for galleries with a comparison grid (see Cast). The publish flow does not re-render; rendering is a manual step via scripts/render-cast-mockups.sh only when the source HTML changes.

The standard mockup chrome

Every multi-page or multi-section mockup ships with the same top chrome and left rail. Reviewers learn the controls once.

Top header

<div class="top-header">
  <div class="title">
    <h1>KB pages &amp; widgets — v2</h1>
    &nbsp;·&nbsp; <a href="../v1/index.html">← V1</a>
  </div>
  <div class="chrome-actions">
    <span style="font-size:11px;">Mode</span>
    <div class="mode-toggle" role="group" aria-label="Color mode (mockup only)">
      <button id="mode-dark"  aria-pressed="true"  onclick="setMode('dark')">Dark</button>
      <button id="mode-light" aria-pressed="false" onclick="setMode('light')">Light</button>
    </div>
    <button class="calm-toggle"  id="calm-toggle"  aria-pressed="false"
            title="Suppress looping animations + reduce density"
            onclick="toggleCalm()">○ Calm mode</button>
    <button class="notes-toggle" id="notes-toggle" aria-pressed="false"
            title="Expand every Implementor notes block on the page"
            onclick="toggleNotes()">⊕ Implementor notes</button>
  </div>
</div>

The three controls — Mode, Calm, Implementor notes — are required on any multi-section mockup. They surface what the real app expresses via system preferences (prefers-reduced-motion, OS dark mode) plus the SageOx-specific Calm mode setting.

Mode toggle (light/dark)

The mockup defaults to <html data-mode="dark">. Token CSS uses html[data-mode="dark"] and html[data-mode="light"] selectors to swap surface, text, and border values. Reviewers click the toggle to audit both modes side-by-side.

function setMode(mode) {
  document.documentElement.setAttribute('data-mode', mode);
  document.getElementById('mode-light').setAttribute('aria-pressed', mode === 'light');
  document.getElementById('mode-dark' ).setAttribute('aria-pressed', mode === 'dark' );
}

Both modes must clear WCAG AA contrast (4.5:1). If a mockup looks great in dark and ugly in light, it isn't done — re-check --text-muted and --text-subtle against the surface tier the text sits on. See §11 Rule 5 of DESIGN.md (contrast lock).

Calm mode toggle

Calm is a SageOx-specific quiet mode. It does exactly three things (per §11 Rule 2 of DESIGN.md):

  1. Hides "via ✦ agent" bylines and file chips.
  2. Dims badges/counters to ~50% opacity, hover-to-reveal.
  3. Suppresses looping animations (pulse, shimmer).

Hover/focus transitions stay at 120–150ms — calm is quiet, not silent.

function toggleCalm() {
  const html = document.documentElement;
  const on = html.getAttribute('data-calm') === 'true';
  html.setAttribute('data-calm', !on);
  const btn = document.getElementById('calm-toggle');
  btn.setAttribute('aria-pressed', !on);
  btn.textContent = on ? '○ Calm mode' : '● Calm mode';
}

CSS hooks read html[data-calm="true"] .some-class to apply the dimming / hiding / pulse-suppression. Use animation-play-state: paused and opacity: 0 (not display: none) so the structural layout stays identical between modes — only the noise drops.

Implementor notes toggle

Wrap "why this value" callouts in <details class="impl-notes">:

<details class="impl-notes">
  <summary>Implementor notes</summary>
  <ul>
    <li>Title demoted 24px → 20px because …</li>
  </ul>
</details>

The chrome toggle expands or collapses every .impl-notes block on the page at once — reviewers can read the rationale without opening N disclosures. Keep notes terse; long-form rationale belongs in DESIGN.md §13 once codified.

Left rail (TOC)

Multi-section mockups put a sticky left rail listing every section with anchor links:

<aside class="toc">
  <h2>Pages</h2>
  <a href="#kb-list">1. KB list — /kb</a>
  <a href="#kb-new">2. KB create — /kb/new</a>
  <h2>Widgets</h2>
  <a href="#w-header">12a. KB header + tabs</a>
</aside>

Conventions:

  • Numbered sections. Pages count from 1; widgets are letter-suffixed (12a, 12b, …) so a new widget can land without renumbering pages.
  • Route shown inline for page sections — /kb, /kb/new, etc. The route is the user-facing identity of the page.
  • Star (★) marks notable sections. Cross-cutting rules, accessibility passes, or anything that codifies a new pattern.
  • Color-flag items the reviewer should pay extra attention to: style="color:var(--sage-400);" on the <a>.

Single-section mockups (a one-screen exploration like a phone surface) skip the TOC.

Section structure

Every section follows the same shape:

<section class="section" id="kb-list">
  <div class="section-head">
    <h2>1. KB list</h2>
    <span class="route">/kb</span>
    <span class="who">apps/web/app/(protected)/kb/page.tsx</span>
  </div>

  <!-- frame + browser chrome wraps the actual mockup -->
  <div class="browser-frame">
    <div class="frame-chrome"></div>
    <div class="frame-body">
      <!-- the mockup content -->
    </div>
  </div>

  <details class="impl-notes">
    <summary>Implementor notes</summary>
    <ul><li></li></ul>
  </details>
</section>

Reviewers can land on proposals/.../v2/index.html#kb-list and see the page, its route, its on-disk owner file, and the why.

Token mirror

A mockup that "looks SageOx" needs the canonical palette in the page's :root. Copy these from tokens/colors.yaml and tokens/typography.yaml:

:root {
  /* Sage / Copper / Charcoal scales — copy the stops you use, not all 10 */
  --sage-100: #e5ede3;
  --sage-500: #7a8f78;
  --sage-700: #546a54;
  --copper-500: #c47a4a;
  --char-700: #2c3236;
  --char-900: #111518;

  /* Light surfaces */
  --bg: #fafbfa;
  --surface-1: #ffffff;
  --surface-2: #e5ede3;
  --border: #c4d1c0;
  --text: #111518;
  --text-muted: #4c5358;
  --text-subtle: #6f767c;

  /* Type */
  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display: "Space Grotesk", var(--font-sans);
  --font-mono: "JetBrains Mono", "SF Mono", Monaco, monospace;
}

html[data-mode="dark"] {
  --bg: #0a0d0f;
  --surface-1: #111518;
  --surface-2: #1a1f22;
  --border: #242a2e;
  --text: #e7e9ea;
  --text-muted: #9aa3ab;
  --text-subtle: #7d858c;
}

Always use --text-muted / --text-subtle on surface-1. On surface-2 or surface-3, switch to the next tier up (--text for muted, --text-muted for subtle) or verify with contrast-check.html. See §11 Rule 5.

What every mockup checklist before publish

  • Both light and dark modes render without contrast failures.
  • Calm mode suppresses looping animations and dims optional metadata; structural layout doesn't shift.
  • Top header has Mode / Calm / Implementor notes chrome.
  • Left rail TOC present (skip only for single-section mockups).
  • Section IDs match the TOC anchors.
  • One Copper CTA per visible viewport. If the page has many sections, count per-section, not cumulatively.
  • Implementor notes wrap any non-obvious value choice or "was → now" delta.
  • No external JS beyond the chrome toggle scripts.
  • Google Fonts loaded via CDN; no local font files.
  • <title> set and meaningful — it shows up in the browser tab and on the published gallery.

Publishing

# preview deploy first (ephemeral URL, doesn't touch prod)
bash scripts/publish-mockups.sh --draft

# once you're happy
bash scripts/publish-mockups.sh

Or run the /publish-design-mockups Claude skill.

  • DESIGN.md — canonical system. §11 cross-cutting rules, §12 mockup explorations, §13 v2 evolution archaeology.
  • .claude/rules/motion-sageox.md (sageox-mono) — motion craft overlay.
  • emil-design-eng skill — Emil Kowalski's design-engineering rules; base for motion.