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/*.yamlinto the mockup's:rootCSS 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.shonly 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 & widgets — v2</h1>
· <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):
- Hides "via ✦ agent" bylines and file chips.
- Dims badges/counters to ~50% opacity, hover-to-reveal.
- 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.
Related rules¶
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-engskill — Emil Kowalski's design-engineering rules; base for motion.