references/design-system.md
# Design System Reference
Plan documents use Plannotator's semantic theme tokens. This makes them theme-aware: standalone files render with bundled defaults; embedded in the Plannotator UI, they inherit whatever theme is active (30+ themes, light and dark variants).
## Standalone defaults
Include this `:root` block so the plan works when opened directly in a browser. These are the Plannotator light theme values — they get overridden when embedded.
```css
:root {
--background: oklch(0.97 0.005 260);
--foreground: oklch(0.18 0.02 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.18 0.02 260);
--primary: oklch(0.50 0.25 280);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.50 0.18 180);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.92 0.01 260);
--muted-foreground: oklch(0.40 0.02 260);
--accent: oklch(0.60 0.22 50);
--accent-foreground: oklch(0.18 0.02 260);
--destructive: oklch(0.50 0.25 25);
--destructive-foreground: oklch(1 0 0);
--success: oklch(0.45 0.20 150);
--success-foreground: oklch(1 0 0);
--warning: oklch(0.55 0.18 85);
--warning-foreground: oklch(0.18 0.02 260);
--border: oklch(0.88 0.01 260);
--input: oklch(0.92 0.01 260);
--ring: oklch(0.50 0.25 280);
--code-bg: oklch(0.92 0.01 260);
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--font-display: ui-serif, Georgia, 'Times New Roman', serif;
--radius: 0.625rem;
}
```
`--font-display` is plan-specific — used for headings and titles to create visual contrast with the body. It's not part of the core Plannotator theme, so it won't be overridden when embedded (which is the desired behavior).
## Token usage map
| Role | Token | SVG equivalent |
|------|-------|----------------|
| Page background | `--background` | — |
| Primary text | `--foreground` | `fill` on text |
| Card / panel background | `--card` | `fill` on rects |
| Subdued text, labels, captions | `--muted-foreground` | `fill` on labels |
| Soft backgrounds, secondary fills | `--muted` | `fill` on secondary rects |
| Primary accent (CTA, attention, keywords) | `--primary` | `stroke` / `fill` on highlighted elements |
| Warm accent | `--accent` | — |
| Positive / success | `--success` | `stroke` / `fill` on success paths |
| Caution | `--warning` | `fill` on warning badges |
| Error / destructive | `--destructive` | `stroke` / `fill` on error paths |
| Borders, dividers | `--border` | `stroke` on box outlines |
| Arrow strokes, diagram lines | `--muted-foreground` | `stroke` on connectors |
| Code block background | `--code-bg` | — |
| Body font | `--font-sans` | `font-family` on SVG body text |
| Code / labels font | `--font-mono` | `font-family` on SVG labels |
| Display / heading font | `--font-display` | `font-family` on SVG titles |
## Base styles
```css
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-sans);
background: var(--background);
color: var(--foreground);
line-height: 1.65;
font-size: 15px;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1080px;
margin: 0 auto;
padding: 64px 24px;
}
```
## Typography
**Headings** — `var(--font-display)`, weight 500, `var(--foreground)`.
- H1: `2rem` page title
- H2: `1.4rem` section headers
- H3: `1.15rem` subsection headers
**Body** — `var(--font-sans)`, weight 400, `0.95rem`, line-height `1.65`. Max paragraph width `65ch`.
**Labels & metadata** — `var(--font-mono)`, `0.7–0.8rem`, weight 500, uppercase, letter-spacing `0.06em`, `var(--muted-foreground)`.
**Code** — `var(--font-mono)`, `0.85rem`, line-height `1.55`.
## Component patterns
### Page header
```html
<header>
<span class="eyebrow">Implementation plan · Project name</span>
<h1>Plan Title Goes Here</h1>
<div class="prompt-box">
<span class="prompt-label">Brief</span>
<p>The original task or problem statement that motivated this plan.</p>
</div>
</header>
```
```css
.eyebrow {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted-foreground);
}
header h1 {
font-family: var(--font-display);
font-size: 2rem;
font-weight: 500;
margin: 8px 0 24px;
line-height: 1.2;
}
.prompt-box {
background: var(--muted);
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 16px 24px;
}
.prompt-label {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted-foreground);
display: block;
margin-bottom: 4px;
}
.prompt-box p {
font-size: 0.92rem;
color: var(--muted-foreground);
line-height: 1.55;
}
```
### Summary strip (stat cards)
```html
<div class="summary-strip">
<div class="stat-card">
<span class="stat-value">4</span>
<span class="stat-label">Components</span>
</div>
<!-- more cards -->
</div>
```
```css
.summary-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 16px;
margin: 32px 0;
}
.stat-card {
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 16px 24px;
text-align: center;
background: var(--card);
}
.stat-value {
font-family: var(--font-display);
font-size: 1.8rem;
font-weight: 500;
display: block;
color: var(--foreground);
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted-foreground);
margin-top: 4px;
display: block;
}
```
### Section header
```html
<section>
<div class="section-header">
<span class="section-number">01</span>
<h2>Solution Overview</h2>
</div>
<!-- content -->
</section>
```
```css
section { margin-top: 64px; }
.section-header {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 8px;
border-bottom: 1.5px solid var(--border);
}
.section-number {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 600;
color: var(--primary);
}
.section-header h2 {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 500;
}
```
### Code block (dark theme)
```html
<div class="code-panel">
<span class="code-label">src/api/handler.ts</span>
<pre><code><span class="kw">interface</span> <span class="fn">PlanRequest</span> {
<span class="fn">title</span>: <span class="kw">string</span>;
<span class="fn">sections</span>: <span class="fn">Section</span>[];
}</code></pre>
</div>
```
```css
.code-panel {
background: var(--code-bg);
border-radius: var(--radius);
padding: 24px;
overflow-x: auto;
margin: 16px 0;
border: 1.5px solid var(--border);
}
.code-label {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted-foreground);
display: block;
margin-bottom: 8px;
}
.code-panel pre {
margin: 0;
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.55;
color: var(--foreground);
}
/* Syntax tokens — these use semantic roles, not fixed colors */
.code-panel .kw { color: var(--primary); } /* keywords */
.code-panel .fn { color: var(--accent); } /* identifiers, types */
.code-panel .str { color: var(--success); } /* strings */
.code-panel .cm { color: var(--muted-foreground); font-style: italic; } /* comments */
.code-panel .num { color: var(--warning); } /* numbers */
```
### Risk table
```html
<div class="risk-grid">
<div class="risk-row">
<div class="risk-name">Database migration on large table</div>
<div><span class="badge high">HIGH</span></div>
<div class="risk-mitigation">Run during off-peak with online DDL</div>
</div>
<!-- more rows -->
</div>
```
```css
.risk-grid {
border: 1.5px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.risk-row {
display: grid;
grid-template-columns: 1fr auto 1.5fr;
gap: 24px;
padding: 16px 24px;
align-items: center;
border-bottom: 1px solid var(--border);
}
.risk-row:last-child { border-bottom: none; }
.risk-name { font-weight: 500; }
.risk-mitigation { font-size: 0.9rem; color: var(--muted-foreground); }
.badge {
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 600;
padding: 2px 8px;
border-radius: calc(var(--radius) - 4px);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge.high {
background: color-mix(in oklab, var(--destructive) 15%, transparent);
color: var(--destructive);
}
.badge.med {
background: color-mix(in oklab, var(--warning) 15%, transparent);
color: var(--warning);
}
.badge.low {
background: color-mix(in oklab, var(--success) 15%, transparent);
color: var(--success);
}
```
### Callout / open question
```html
<div class="callout">
<h3>Should we use WebSockets or SSE?</h3>
<p>SSE is simpler but unidirectional. WebSockets add infrastructure complexity.</p>
<span class="decide-with">Decide with: infrastructure team</span>
</div>
```
```css
.callout {
border-left: 3px solid var(--primary);
padding: 16px 24px;
margin: 16px 0;
background: var(--card);
border-radius: 0 var(--radius) var(--radius) 0;
}
.callout h3 {
font-family: var(--font-display);
font-size: 1.05rem;
font-weight: 500;
margin-bottom: 4px;
}
.callout p {
font-size: 0.9rem;
color: var(--muted-foreground);
line-height: 1.55;
}
.decide-with {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--primary);
font-weight: 500;
display: block;
margin-top: 8px;
}
```
### Tag chips
```html
<div class="tags">
<span class="tag">packages/server</span>
<span class="tag highlight">new endpoint</span>
</div>
```
```css
.tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.tag {
font-family: var(--font-mono);
font-size: 0.68rem;
padding: 2px 8px;
border-radius: calc(var(--radius) - 4px);
background: var(--muted);
color: var(--muted-foreground);
}
.tag.highlight {
background: color-mix(in oklab, var(--primary) 12%, transparent);
color: var(--primary);
}
```
### Diagram panel
Wraps SVG diagrams in a bordered container:
```html
<div class="diagram-panel">
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" style="width:100%">
<!-- diagram content -->
</svg>
<span class="diagram-caption">Request flow through the API gateway</span>
</div>
```
```css
.diagram-panel {
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin: 24px 0;
background: var(--card);
}
.diagram-caption {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--muted-foreground);
display: block;
margin-top: 8px;
text-align: center;
}
```
### Two-column grid
```css
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 720px) {
.two-col { grid-template-columns: 1fr; }
}
```
### Collapsible details
```html
<details>
<summary>Implementation details</summary>
<div class="details-body"><!-- content --></div>
</details>
```
```css
details {
border: 1.5px solid var(--border);
border-radius: var(--radius);
margin: 16px 0;
}
summary {
font-family: var(--font-sans);
font-weight: 500;
padding: 16px 24px;
cursor: pointer;
list-style: none;
}
summary::before {
content: '▸';
display: inline-block;
margin-right: 8px;
transition: transform 0.2s;
}
details[open] summary::before { transform: rotate(90deg); }
.details-body { padding: 0 24px 24px; }
```
### Milestone timeline
Vertical timeline showing phases without time estimates.
```html
<div class="milestones">
<div class="milestone">
<div class="when">Phase 1</div>
<div class="dot-col"><span class="dot done"></span><span class="line"></span></div>
<div class="body">
<h3>Foundation</h3>
<p>Set up core infrastructure and initial integrations.</p>
<div class="tags"><span class="tag">packages/server</span></div>
</div>
</div>
</div>
```
```css
.milestones { display: flex; flex-direction: column; gap: 0; }
.milestone {
display: grid;
grid-template-columns: 120px 28px 1fr;
gap: 0 18px;
}
.milestone .when {
text-align: right;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted-foreground);
padding-top: 4px;
}
.milestone .dot-col { display: flex; flex-direction: column; align-items: center; }
.milestone .dot {
width: 14px; height: 14px; border-radius: 50%;
background: var(--card);
border: 3px solid var(--primary);
flex-shrink: 0;
}
.milestone .dot.done { background: var(--success); border-color: var(--success); }
.milestone .line { width: 2px; flex: 1; background: var(--border); margin: 4px 0; }
.milestone:last-child .line { display: none; }
.milestone .body { padding-bottom: 36px; }
.milestone .body h3 {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 500;
margin-bottom: 4px;
}
.milestone .body p {
font-size: 0.88rem;
color: var(--muted-foreground);
max-width: 620px;
margin-bottom: 10px;
}
```
references/pr-components.md
# PR Component Patterns
Component patterns specific to PR explainer documents. For the base design system (colors, typography, layout), see `../../plannotator-visual-plan/references/design-system.md`.
## Table of Contents
1. [PR Header](#pr-header)
2. [TL;DR Box](#tldr-box)
3. [Diff Rendering](#diff-rendering)
4. [Review Comments](#review-comments)
5. [Risk Map](#risk-map)
6. [File Cards](#file-cards)
7. [Before / After](#before--after)
8. [Where to Focus](#where-to-focus)
9. [Test Plan](#test-plan)
10. [File Badges](#file-badges)
## PR header
```html
<header>
<span class="eyebrow">Pull request · repo-name</span>
<h1>Add real-time notification system</h1>
<div class="pr-meta">
<span>6 files</span>
<span class="additions">+142</span>
<span class="deletions">-38</span>
<span>feature/notifications → main</span>
</div>
</header>
```
```css
.pr-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--muted-foreground);
margin-top: 8px;
}
.pr-meta .additions { color: var(--success); }
.pr-meta .deletions { color: var(--destructive); }
```
## TL;DR box
```html
<div class="tldr">
<h3>TL;DR</h3>
<p>Adds WebSocket-based notifications with per-user channels. Messages fan out
from a new NotificationService through Redis pub/sub. Existing REST endpoints
are unchanged.</p>
</div>
```
```css
.tldr {
background: var(--card);
border: 1.5px solid var(--border);
border-left: 4px solid var(--primary);
border-radius: var(--radius);
padding: 20px 24px;
max-width: 760px;
margin: 24px 0;
}
.tldr h3 {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--primary);
margin-bottom: 8px;
}
.tldr p {
font-size: 0.95rem;
line-height: 1.6;
color: var(--muted-foreground);
}
```
## Diff rendering
Use Pierre diffs via CDN for syntax-highlighted, theme-aware diff rendering. Renders into shadow DOM (no style conflicts) with Shiki syntax highlighting.
```html
<script type="module">
import { getSingularPatch, registerDiffsComponent } from 'https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.21/+esm';
registerDiffsComponent();
const patch = `--- a/src/handler.ts
+++ b/src/handler.ts
@@ -1,4 +1,6 @@
import { Router } from 'express';
+import { NotificationService } from './notifications';
-import { legacyPoll } from './polling';`;
const container = document.querySelector('diffs-container');
container.fileDiff = getSingularPatch(patch);
container.options = {
themeType: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
diffStyle: 'unified',
diffIndicators: 'bars',
lineDiffType: 'word-alt',
unsafeCSS: `
:host {
--diffs-bg: var(--background);
--diffs-fg: var(--foreground);
border-radius: var(--radius);
border: 1.5px solid var(--border);
overflow: hidden;
}
`,
};
</script>
<diffs-container></diffs-container>
```
For multiple diffs, create one `<diffs-container>` per file. Pierre handles syntax highlighting, line numbers, add/del coloring, and word-level diffs automatically.
## Review comments
Speech bubbles with severity-coded left borders, attached below a diff block.
```html
<div class="comments">
<div class="bubble blocking">
<span class="anchor">line 11</span>
<span class="severity">BLOCKING</span>
<p>This mutation isn't wrapped in a transaction. If the second write
fails, the first persists — leaving the user in a broken state.</p>
</div>
<div class="bubble nit">
<span class="anchor">line 24</span>
<span class="severity">NIT</span>
<p>Prefer <code>const</code> here since <code>config</code> is never reassigned.</p>
</div>
</div>
```
```css
.comments {
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 14px;
background: var(--muted);
border-top: 1px solid var(--border);
}
.bubble {
position: relative;
background: var(--card);
border: 1.5px solid var(--border);
border-left-width: 4px;
border-radius: 8px;
padding: 12px 14px 12px 16px;
max-width: 680px;
}
.bubble.blocking { border-left-color: var(--primary); }
.bubble.nit { border-left-color: var(--border); }
.bubble.suggestion { border-left-color: var(--success); }
.bubble::before {
content: "";
position: absolute;
left: -9px;
top: 16px;
width: 12px;
height: 12px;
background: var(--card);
border-left: 1.5px solid var(--border);
border-bottom: 1.5px solid var(--border);
transform: rotate(45deg);
}
.bubble.blocking::before {
border-left-color: var(--primary);
border-bottom-color: var(--primary);
}
.anchor {
font-family: var(--font-mono);
font-size: 0.68rem;
color: var(--muted-foreground);
}
.severity {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-left: 8px;
}
.bubble.blocking .severity { color: var(--primary); }
.bubble.nit .severity { color: var(--muted-foreground); }
.bubble.suggestion .severity { color: var(--success); }
.bubble p {
margin-top: 6px;
font-size: 0.88rem;
line-height: 1.55;
color: var(--foreground);
}
.bubble code {
font-family: var(--font-mono);
font-size: 0.82rem;
background: var(--muted);
padding: 1px 5px;
border-radius: 3px;
}
```
## Risk map
Chips that give a quick overview of file risk levels.
```html
<div class="risk-map">
<a href="#file-auth" class="chip attention">
<span class="dot"></span>
src/auth/middleware.ts
</a>
<a href="#file-db" class="chip medium">
<span class="dot"></span>
src/db/migrations/004.sql
</a>
<a href="#file-types" class="chip safe">
<span class="dot"></span>
src/types/index.ts
</a>
</div>
```
```css
.risk-map {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 24px 0;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 6px 12px;
border: 1.5px solid var(--border);
border-radius: 20px;
text-decoration: none;
color: var(--foreground);
transition: box-shadow 0.15s;
}
.chip:hover {
box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary) 25%, transparent);
}
.chip .dot {
width: 9px;
height: 9px;
border-radius: 50%;
}
.chip.attention {
background: color-mix(in oklab, var(--destructive) 8%, transparent);
border-color: color-mix(in oklab, var(--destructive) 40%, transparent);
}
.chip.attention .dot { background: var(--destructive); }
.chip.medium {
background: color-mix(in oklab, var(--warning) 10%, transparent);
border-color: color-mix(in oklab, var(--warning) 30%, transparent);
}
.chip.medium .dot { background: var(--warning); }
.chip.safe {
background: color-mix(in oklab, var(--success) 8%, transparent);
border-color: color-mix(in oklab, var(--success) 35%, transparent);
}
.chip.safe .dot { background: var(--success); }
```
## File cards
Expandable cards grouping a file's diff and review commentary.
```html
<div class="file-card" id="file-auth">
<div class="file-head">
<div class="file-info">
<span class="file-path">src/auth/middleware.ts</span>
<span class="file-badge mod">MOD</span>
<span class="file-stats"><span class="additions">+28</span> <span class="deletions">-12</span></span>
</div>
<span class="risk-tag attention">ATTENTION</span>
</div>
<div class="file-why">
<p>Replaced session-cookie auth with JWT verification. The trust boundary
moves from the session store to the token signature check.</p>
</div>
<div class="diff"><!-- diff rows --></div>
<div class="comments"><!-- review bubbles --></div>
</div>
<!-- Safe files: collapsed -->
<details class="file-collapsed">
<summary>
<span class="file-path">src/types/index.ts</span>
<span class="file-badge mod">MOD</span>
<span class="file-stats"><span class="additions">+4</span> <span class="deletions">-0</span></span>
<span class="risk-tag safe">SAFE</span>
</summary>
<div class="file-why">
<p>Added NotificationPayload type export.</p>
</div>
</details>
```
```css
.file-card {
border: 1.5px solid var(--border);
border-radius: var(--radius);
background: var(--card);
overflow: hidden;
scroll-margin-top: 20px;
margin: 16px 0;
}
.file-head {
padding: 16px 20px;
border-bottom: 1.5px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.file-info {
display: flex;
align-items: center;
gap: 12px;
}
.file-path {
font-family: var(--font-mono);
font-size: 0.82rem;
font-weight: 600;
}
.file-stats {
font-family: var(--font-mono);
font-size: 0.72rem;
}
.file-stats .additions { color: var(--success); }
.file-stats .deletions { color: var(--destructive); }
.file-why {
padding: 12px 20px;
border-bottom: 1px solid var(--border);
}
.file-why p {
font-size: 0.9rem;
color: var(--muted-foreground);
line-height: 1.55;
}
/* Collapsed (safe) files */
.file-collapsed {
border: 1.5px solid var(--border);
border-radius: var(--radius);
background: var(--card);
margin: 8px 0;
}
.file-collapsed summary {
list-style: none;
cursor: pointer;
padding: 14px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.file-collapsed summary::after {
content: '+';
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--muted-foreground);
margin-left: auto;
}
.file-collapsed[open] summary::after {
content: '\2212';
}
```
## Before / after
Two-column comparison grid.
```html
<div class="before-after">
<div class="ba-panel before">
<h4>Before</h4>
<p>Auth checked via session cookie on every request.
Session store hit adds ~15ms latency.</p>
</div>
<div class="ba-panel after">
<h4>After</h4>
<p>JWT signature verified in-process. No external store hit.
Latency drops to ~1ms per request.</p>
</div>
</div>
```
```css
.before-after {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 16px 0;
}
@media (max-width: 640px) {
.before-after { grid-template-columns: 1fr; }
}
.ba-panel {
background: var(--card);
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 18px 20px;
}
.ba-panel.after {
border-color: var(--success);
}
.ba-panel h4 {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
.ba-panel.before h4 { color: var(--muted-foreground); }
.ba-panel.after h4 { color: var(--success); }
.ba-panel p {
font-size: 0.9rem;
line-height: 1.55;
color: var(--foreground);
}
```
## Where to focus
Numbered callout cards directing reviewers.
```html
<div class="focus-list">
<div class="focus-item">
<span class="focus-number">1</span>
<div>
<strong>src/auth/middleware.ts:verifyToken()</strong>
<p>New trust boundary. Verify the JWT validation covers all edge cases:
expired tokens, malformed signatures, missing claims.</p>
</div>
</div>
</div>
```
```css
.focus-list {
display: flex;
flex-direction: column;
gap: 12px;
margin: 16px 0;
}
.focus-item {
background: var(--card);
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
display: flex;
gap: 16px;
align-items: flex-start;
}
.focus-number {
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 600;
color: var(--primary-foreground);
background: var(--primary);
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.focus-item strong {
font-family: var(--font-mono);
font-size: 0.82rem;
display: block;
margin-bottom: 4px;
}
.focus-item p {
font-size: 0.88rem;
color: var(--muted-foreground);
line-height: 1.5;
}
```
## Test plan
Checkbox-style verification checklist.
```html
<div class="test-list">
<div class="test-item done">
<span class="check"></span>
<span>Expired JWT returns 401 with proper error body</span>
</div>
<div class="test-item">
<span class="check"></span>
<span>Concurrent WebSocket connections scale to 1000 without memory leak</span>
</div>
</div>
```
```css
.test-list {
display: flex;
flex-direction: column;
gap: 8px;
margin: 16px 0;
}
.test-item {
display: flex;
align-items: flex-start;
gap: 12px;
background: var(--card);
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 13px 18px;
font-size: 0.88rem;
}
.check {
width: 18px;
height: 18px;
border-radius: 5px;
border: 1.5px solid var(--border);
flex-shrink: 0;
position: relative;
margin-top: 2px;
}
.test-item.done .check {
background: var(--success);
border-color: var(--success);
}
.test-item.done .check::after {
content: '';
position: absolute;
left: 5px;
top: 2px;
width: 5px;
height: 9px;
border-right: 2px solid var(--card);
border-bottom: 2px solid var(--card);
transform: rotate(40deg);
}
```
## File badges
```css
.file-badge {
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 6px;
border-radius: calc(var(--radius) - 4px);
}
.file-badge.new {
background: color-mix(in oklab, var(--success) 15%, transparent);
color: var(--success);
}
.file-badge.mod {
background: color-mix(in oklab, var(--warning) 15%, transparent);
color: var(--warning);
}
.file-badge.del {
background: color-mix(in oklab, var(--destructive) 15%, transparent);
color: var(--destructive);
}
.risk-tag {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: calc(var(--radius) - 4px);
}
.risk-tag.attention {
background: color-mix(in oklab, var(--destructive) 12%, transparent);
color: var(--destructive);
}
.risk-tag.medium {
background: color-mix(in oklab, var(--warning) 12%, transparent);
color: var(--warning);
}
.risk-tag.safe {
background: color-mix(in oklab, var(--success) 12%, transparent);
color: var(--success);
}
```
## Rollout plan
Phased deployment strip showing ramp percentages.
```html
<div class="rollout">
<div class="rollout-step">
<div class="rollout-when">Day 0</div>
<div class="rollout-pct">internal</div>
<div class="rollout-desc">Team only. Watch error rates.</div>
</div>
<div class="rollout-step">
<div class="rollout-when">Day 2</div>
<div class="rollout-pct">10%</div>
<div class="rollout-desc">Random sample. Alert on anomalies.</div>
</div>
<div class="rollout-step">
<div class="rollout-when">Day 4</div>
<div class="rollout-pct">100%</div>
<div class="rollout-desc">Full ramp.</div>
</div>
</div>
```
```css
.rollout { display: flex; gap: 0; }
.rollout-step {
flex: 1;
background: var(--card);
border: 1.5px solid var(--border);
padding: 16px 18px;
}
.rollout-step:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.rollout-step:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
.rollout-step + .rollout-step { border-left: none; }
.rollout-when {
font-family: var(--font-mono);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-foreground);
margin-bottom: 8px;
}
.rollout-pct {
font-family: var(--font-mono);
font-size: 1.35rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 6px;
}
.rollout-desc {
font-size: 0.82rem;
color: var(--muted-foreground);
}
@media (max-width: 720px) {
.rollout { flex-direction: column; }
.rollout-step { border-radius: var(--radius); }
.rollout-step + .rollout-step { border-left: 1.5px solid var(--border); margin-top: 10px; }
}
```
references/svg-patterns.md
# SVG Diagram Patterns
Building blocks for creating diagrams in implementation plans. All SVGs are inline — no external dependencies. Compose these patterns to build architecture diagrams, data flow visualizations, flowcharts, and charts.
All colors reference Plannotator theme tokens. In SVG, use the CSS custom property values directly via `style` attributes or the corresponding CSS classes.
## Table of Contents
1. [Arrow Markers](#arrow-markers)
2. [Architecture Diagrams](#architecture-diagrams)
3. [Flowcharts](#flowcharts)
4. [Data Flow](#data-flow)
5. [Bar Charts](#bar-charts)
6. [Positioning & Layout](#positioning--layout)
## Arrow markers
Define reusable markers in `<defs>`. Reference them via `marker-end="url(#arrow)"`.
```svg
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--muted-foreground)"/>
</marker>
<marker id="arrow-primary" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--primary)"/>
</marker>
<marker id="arrow-success" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--success)"/>
</marker>
<marker id="arrow-destructive" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--destructive)"/>
</marker>
</defs>
```
**Note on CSS vars in SVG:** `fill="var(--primary)"` works in inline SVG within an HTML document. For broader compatibility, you can also style via CSS classes instead of inline attributes.
## Architecture diagrams
Box-and-arrow diagrams showing how components connect.
### Box node
```svg
<g transform="translate(100, 80)">
<rect width="140" height="56" rx="10" fill="var(--card)"
stroke="var(--border)" stroke-width="1.5"/>
<text x="70" y="24" text-anchor="middle"
font-family="var(--font-sans)" font-size="13" font-weight="600"
fill="var(--foreground)">API Server</text>
<text x="70" y="40" text-anchor="middle"
font-family="var(--font-mono)" font-size="10.5"
fill="var(--muted-foreground)">Express + middleware</text>
</g>
```
### Highlighted box (new or hot-path component)
```svg
<g transform="translate(100, 80)">
<rect width="140" height="56" rx="10"
fill="color-mix(in oklab, var(--primary) 8%, transparent)"
stroke="var(--primary)" stroke-width="1.5"/>
<text x="70" y="24" text-anchor="middle"
font-family="var(--font-sans)" font-size="13" font-weight="600"
fill="var(--foreground)">New Service</text>
<text x="70" y="40" text-anchor="middle"
font-family="var(--font-mono)" font-size="10.5"
fill="var(--primary)">to be created</text>
</g>
```
### Connecting arrows
```svg
<!-- Horizontal -->
<line x1="240" y1="108" x2="320" y2="108"
stroke="var(--muted-foreground)" stroke-width="1.5"
marker-end="url(#arrow)"/>
<!-- Vertical -->
<line x1="170" y1="136" x2="170" y2="200"
stroke="var(--muted-foreground)" stroke-width="1.5"
marker-end="url(#arrow)"/>
<!-- Dashed (async, optional, or secondary) -->
<line x1="240" y1="108" x2="320" y2="108"
stroke="var(--primary)" stroke-width="1.5"
stroke-dasharray="5 4"
marker-end="url(#arrow-primary)"/>
```
### Edge labels
Place at the midpoint of an arrow, offset above:
```svg
<text x="280" y="100" text-anchor="middle"
font-family="var(--font-mono)" font-size="9.5"
fill="var(--muted-foreground)">REST</text>
```
### Full architecture example
```svg
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg"
style="width:100%;max-width:720px">
<style>
.box { fill: var(--card); stroke: var(--border); stroke-width: 1.5; }
.box-new { fill: color-mix(in oklab, var(--primary) 8%, transparent);
stroke: var(--primary); stroke-width: 1.5; }
.label { font-family: var(--font-sans); font-size: 13px;
font-weight: 600; fill: var(--foreground); }
.sublabel { font-family: var(--font-mono); font-size: 10.5px;
fill: var(--muted-foreground); }
.edge { stroke: var(--muted-foreground); stroke-width: 1.5; }
.edge-label { font-family: var(--font-mono); font-size: 9.5px;
fill: var(--muted-foreground); }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--muted-foreground)"/>
</marker>
</defs>
<!-- Browser -->
<rect x="20" y="100" width="120" height="56" rx="10" class="box"/>
<text x="80" y="124" text-anchor="middle" class="label">Browser</text>
<text x="80" y="140" text-anchor="middle" class="sublabel">React SPA</text>
<!-- Arrow: Browser → API -->
<line x1="140" y1="128" x2="220" y2="128" class="edge" marker-end="url(#a)"/>
<text x="180" y="120" text-anchor="middle" class="edge-label">HTTPS</text>
<!-- API Gateway (new) -->
<rect x="220" y="100" width="140" height="56" rx="10" class="box-new"/>
<text x="290" y="124" text-anchor="middle" class="label">API Gateway</text>
<text x="290" y="140" text-anchor="middle" class="sublabel"
fill="var(--primary)">new</text>
<!-- Arrow: API → DB -->
<line x1="360" y1="128" x2="440" y2="128" class="edge" marker-end="url(#a)"/>
<!-- Postgres -->
<rect x="440" y="100" width="120" height="56" rx="10" class="box"/>
<text x="500" y="124" text-anchor="middle" class="label">Postgres</text>
<text x="500" y="140" text-anchor="middle" class="sublabel">existing</text>
<!-- Arrow: API → Cache (vertical) -->
<line x1="290" y1="156" x2="290" y2="210" class="edge" marker-end="url(#a)"/>
<!-- Redis -->
<rect x="220" y="210" width="140" height="48" rx="10" class="box"/>
<text x="290" y="240" text-anchor="middle" class="label">Redis Cache</text>
</svg>
```
## Flowcharts
### Process box
```svg
<rect x="250" y="80" width="120" height="40" rx="8"
fill="var(--card)" stroke="var(--border)" stroke-width="1.5"/>
<text x="310" y="105" text-anchor="middle"
font-family="var(--font-sans)" font-size="12" font-weight="500"
fill="var(--foreground)">Parse input</text>
```
### Decision diamond
```svg
<path d="M310,262 L352,294 L310,326 L268,294 Z"
fill="var(--card)" stroke="var(--border)" stroke-width="1.5"/>
<text x="310" y="298" text-anchor="middle"
font-family="var(--font-sans)" font-size="11" font-weight="500"
fill="var(--foreground)">Valid?</text>
```
### Terminal / pill node
```svg
<rect x="260" y="20" width="100" height="36" rx="18"
fill="var(--card)" stroke="var(--border)" stroke-width="1.5"/>
<text x="310" y="43" text-anchor="middle"
font-family="var(--font-sans)" font-size="12" font-weight="500"
fill="var(--foreground)">Start</text>
```
### Success / failure endpoints
```svg
<!-- Success -->
<rect x="260" y="400" width="100" height="36" rx="18"
fill="color-mix(in oklab, var(--success) 12%, transparent)"
stroke="var(--success)" stroke-width="1.5"/>
<text x="310" y="423" text-anchor="middle"
font-family="var(--font-sans)" font-size="12" font-weight="600"
fill="var(--success)">Done</text>
<!-- Failure -->
<rect x="100" y="400" width="100" height="36" rx="18"
fill="color-mix(in oklab, var(--destructive) 12%, transparent)"
stroke="var(--destructive)" stroke-width="1.5"/>
<text x="150" y="423" text-anchor="middle"
font-family="var(--font-sans)" font-size="12" font-weight="600"
fill="var(--destructive)">Error</text>
```
### Curved branch path
For routing flow from a decision to a side branch:
```svg
<path d="M268,294 C200,294 160,294 160,240"
fill="none" stroke="var(--destructive)" stroke-width="1.5"
marker-end="url(#arrow-destructive)"/>
```
## Data flow
### Request / response pair
```svg
<!-- Request (solid) -->
<line x1="140" y1="100" x2="280" y2="100"
stroke="var(--muted-foreground)" stroke-width="1.5"
marker-end="url(#arrow)"/>
<text x="210" y="92" text-anchor="middle"
font-family="var(--font-mono)" font-size="9.5"
fill="var(--muted-foreground)">POST /api/plan</text>
<!-- Response (dashed) -->
<line x1="280" y1="116" x2="140" y2="116"
stroke="var(--primary)" stroke-width="1.5" stroke-dasharray="5 4"
marker-end="url(#arrow-primary)"/>
<text x="210" y="132" text-anchor="middle"
font-family="var(--font-mono)" font-size="9.5"
fill="var(--primary)">{ plan, status }</text>
```
### Fan-out pattern
```svg
<!-- Source box radiating to multiple targets -->
<line x1="200" y1="100" x2="340" y2="60"
stroke="var(--muted-foreground)" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="200" y1="100" x2="340" y2="100"
stroke="var(--muted-foreground)" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="200" y1="100" x2="340" y2="140"
stroke="var(--muted-foreground)" stroke-width="1.5" marker-end="url(#arrow)"/>
```
## Bar charts
```svg
<svg viewBox="0 0 400 180" xmlns="http://www.w3.org/2000/svg"
style="width:100%;max-width:400px">
<!-- Gridlines -->
<line x1="40" y1="20" x2="380" y2="20"
stroke="var(--border)" stroke-width="1" opacity="0.5"/>
<line x1="40" y1="60" x2="380" y2="60"
stroke="var(--border)" stroke-width="1" opacity="0.5"/>
<line x1="40" y1="100" x2="380" y2="100"
stroke="var(--border)" stroke-width="1" opacity="0.5"/>
<line x1="40" y1="140" x2="380" y2="140"
stroke="var(--border)" stroke-width="1"/>
<!-- Y-axis labels -->
<text x="35" y="24" text-anchor="end"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">30</text>
<text x="35" y="64" text-anchor="end"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">20</text>
<text x="35" y="104" text-anchor="end"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">10</text>
<!-- Bars (muted default, primary for peak) -->
<rect x="60" y="60" width="40" height="80" rx="4"
fill="var(--muted)"/>
<rect x="120" y="40" width="40" height="100" rx="4"
fill="var(--primary)"/>
<rect x="180" y="80" width="40" height="60" rx="4"
fill="var(--muted)"/>
<rect x="240" y="100" width="40" height="40" rx="4"
fill="var(--muted)"/>
<!-- Value labels -->
<text x="80" y="55" text-anchor="middle"
font-family="var(--font-mono)" font-size="10" font-weight="600"
fill="var(--foreground)">20</text>
<text x="140" y="35" text-anchor="middle"
font-family="var(--font-mono)" font-size="10" font-weight="600"
fill="var(--primary)">25</text>
<!-- X-axis labels -->
<text x="80" y="158" text-anchor="middle"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">Q1</text>
<text x="140" y="158" text-anchor="middle"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">Q2</text>
<text x="200" y="158" text-anchor="middle"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">Q3</text>
<text x="260" y="158" text-anchor="middle"
font-family="var(--font-mono)" font-size="9"
fill="var(--muted-foreground)">Q4</text>
</svg>
```
## Positioning & layout
### SVG container sizing
- Use `viewBox` with fixed coordinates; set `style="width:100%;max-width:NNNpx"` for responsive scaling
- Standard widths: `720px` full-width, `480px` half-width, `360px` sidebar
- Standard heights: `180–320px` for most diagrams
### Box sizing
- Standard node: `120–160px` wide, `48–56px` tall
- Minimum gap between nodes: `60px` horizontal, `40px` vertical
- Arrow label offset: `8–12px` above the line
- Diagram padding: `20px` inside the viewBox edges
### Color roles in diagrams
| Element | Fill / stroke | Token |
|---------|---------------|-------|
| Box background | fill | `var(--card)` |
| Box stroke | stroke | `var(--border)` |
| Highlighted box bg | fill | `color-mix(in oklab, var(--primary) 8%, transparent)` |
| Highlighted box stroke | stroke | `var(--primary)` |
| Arrow / connector | stroke | `var(--muted-foreground)` |
| Title text | fill | `var(--foreground)` |
| Subtitle / label text | fill | `var(--muted-foreground)` |
| Success path | stroke | `var(--success)` |
| Error path | stroke | `var(--destructive)` |
| Warning | fill | `var(--warning)` |
### Using CSS classes in SVG
For cleaner markup, define reusable classes in a `<style>` block inside the SVG:
```svg
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg">
<style>
.box { fill: var(--card); stroke: var(--border); stroke-width: 1.5; }
.new { fill: color-mix(in oklab, var(--primary) 8%, transparent);
stroke: var(--primary); stroke-width: 1.5; }
.title { font-family: var(--font-sans); font-size: 13px;
font-weight: 600; fill: var(--foreground); }
.sub { font-family: var(--font-mono); font-size: 10.5px;
fill: var(--muted-foreground); }
.conn { stroke: var(--muted-foreground); stroke-width: 1.5; }
</style>
<!-- nodes and connectors use class="box", class="title", etc. -->
</svg>
```
references/theme-override.md
# Plannotator Theme Override
When visual-explainer's workflow says to pick a palette and font pairing, use these Plannotator tokens instead. Everything else — layout, structure, components, anti-slop rules — stays as visual-explainer prescribes.
## Host theme opt-in (required)
Plannotator's HTML viewer renders arbitrary documents untouched — it never injects bare theme tokens into a document unless the document asks for them. For the generated file to follow the active Plannotator theme when embedded in raw HTML annotation mode, it MUST declare the opt-in in its `<head>`:
```html
<meta name="plannotator-theme" content="host">
```
With this tag present, the viewer overrides the document's bare tokens (`--background`, `--muted`, …) with the host theme's values and mirrors the host's light/dark mode. Without it, the `:root` defaults below are all the document ever sees.
## CSS Custom Properties
Replace visual-explainer's `--bg`, `--surface`, `--border`, `--text`, `--accent` variables with Plannotator's semantic tokens. Include these as `:root` defaults so the file works standalone. When embedded in Plannotator's raw HTML annotation mode (with the meta opt-in above), these get overridden by the active theme.
```css
:root {
/* Surfaces */
--background: oklch(0.97 0.005 260);
--foreground: oklch(0.18 0.02 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.18 0.02 260);
--muted: oklch(0.92 0.01 260);
--muted-foreground: oklch(0.40 0.02 260);
/* Accents */
--primary: oklch(0.50 0.25 280);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.50 0.18 180);
--accent: oklch(0.60 0.22 50);
--accent-foreground: oklch(0.18 0.02 260);
/* Semantic */
--destructive: oklch(0.50 0.25 25);
--success: oklch(0.45 0.20 150);
--warning: oklch(0.55 0.18 85);
/* Structure */
--border: oklch(0.88 0.01 260);
--code-bg: oklch(0.92 0.01 260);
--ring: oklch(0.50 0.25 280);
--radius: 0.625rem;
/* Typography */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--font-display: ui-serif, Georgia, 'Times New Roman', serif;
}
```
## Mapping visual-explainer variables to Plannotator tokens
When visual-explainer references or templates use these variables, substitute:
| visual-explainer | Plannotator | Notes |
|-----------------|-------------|-------|
| `--bg` | `var(--background)` | Page background |
| `--surface` | `var(--card)` | Card/panel surfaces |
| `--border` | `var(--border)` | Borders and dividers |
| `--text` | `var(--foreground)` | Primary text |
| `--text-dim` | `var(--muted-foreground)` | Secondary/subdued text |
| `--accent` (primary) | `var(--primary)` | Primary accent |
| `--accent-dim` | `color-mix(in oklab, var(--primary) 15%, transparent)` | Accent backgrounds |
| `--accent-2` | `var(--accent)` | Secondary accent (warm) |
| `--accent-3` | `var(--secondary)` | Tertiary accent |
| `--success` | `var(--success)` | Positive indicators |
| `--warning` | `var(--warning)` | Caution indicators |
| `--error` / `--danger` | `var(--destructive)` | Error/destructive indicators |
| `--font-body` | `var(--font-sans)` | Body text font |
| `--font-mono` | `var(--font-mono)` | Code and labels |
| `--font-heading` | `var(--font-display)` | Headings (serif) |
## Typography exception
Visual-explainer forbids Inter as `--font-body`. Plannotator uses Inter as its default sans-serif. This is intentional — Plannotator's identity is defined by its theme tokens, not font novelty. When using this skill, Inter is permitted as the body font because the output is meant to look like part of Plannotator, not like an independent design piece.
The `--font-display` (serif) is still used for headings to create visual contrast, matching the visual-explainer's emphasis on distinctive typography.
## Mermaid theming
Mermaid processes `themeVariables` itself and derives additional colors from them. That color-processing boundary does not accept every color syntax that browsers accept in CSS. Use Mermaid-compatible literal hex colors here instead of copying the semantic CSS token declarations above.
Do not pass OKLCH color functions, CSS custom-property references such as `var()`, or `color-mix()` values directly into `themeVariables`. This restriction applies only to Mermaid's color-processing boundary. Continue using Plannotator's OKLCH custom properties and other modern color functions for ordinary page CSS.
Use the same light/dark state as the page, but keep both Mermaid palettes literal. With the host-theme opt-in, Plannotator synchronizes `color-scheme`; standalone documents fall back to the operating-system preference:
```javascript
const colorScheme = getComputedStyle(document.documentElement).colorScheme;
const isDark = colorScheme === 'dark'
|| (colorScheme === 'normal'
&& window.matchMedia('(prefers-color-scheme: dark)').matches);
const mermaidThemeVariables = isDark
? {
darkMode: true,
primaryColor: '#9a9dff',
primaryTextColor: '#070b14',
primaryBorderColor: '#343b45',
lineColor: '#9da5b2',
secondaryColor: '#1e242e',
secondaryTextColor: '#dadee5',
tertiaryColor: '#1e242e',
tertiaryTextColor: '#dadee5',
background: '#070b14',
}
: {
primaryColor: '#5537eb',
primaryTextColor: '#ffffff',
primaryBorderColor: '#d4d8de',
lineColor: '#414853',
secondaryColor: '#e1e5eb',
secondaryTextColor: '#414853',
tertiaryColor: '#e1e5eb',
tertiaryTextColor: '#414853',
background: '#f3f5f9',
};
mermaid.initialize({
theme: 'base',
themeVariables: {
...mermaidThemeVariables,
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: '14px',
}
});
```
## Dark mode
Plannotator handles dark/light via theme classes, not `prefers-color-scheme`. The standalone defaults above are the light theme. When embedded in raw HTML annotation mode (with the `plannotator-theme` meta opt-in), the active theme's tokens override automatically — no media query needed in the generated HTML.
For standalone viewing, you may optionally add a `prefers-color-scheme: dark` block with the Plannotator dark theme values:
```css
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.15 0.02 260);
--foreground: oklch(0.90 0.01 260);
--card: oklch(0.22 0.02 260);
--card-foreground: oklch(0.90 0.01 260);
--muted: oklch(0.26 0.02 260);
--muted-foreground: oklch(0.72 0.02 260);
--primary: oklch(0.75 0.18 280);
--primary-foreground: oklch(0.15 0.02 260);
--accent: oklch(0.70 0.20 60);
--border: oklch(0.35 0.02 260);
--code-bg: oklch(0.26 0.02 260);
--destructive: oklch(0.65 0.20 25);
--success: oklch(0.72 0.17 150);
--warning: oklch(0.75 0.15 85);
}
}
```
## Depth tiers
Visual-explainer defines depth tiers (hero, elevated, default, recessed). Map them using Plannotator tokens:
```css
/* Hero — elevated, accent-tinted */
.ve-card--hero {
background: color-mix(in oklab, var(--primary) 5%, var(--card));
border-color: var(--primary);
box-shadow: 0 4px 24px color-mix(in oklab, var(--primary) 10%, transparent);
}
/* Default — standard card */
.ve-card {
background: var(--card);
border: 1.5px solid var(--border);
border-radius: var(--radius);
}
/* Recessed — subdued */
.ve-card--recessed {
background: var(--muted);
border-color: transparent;
}
```
## Code blocks
```css
.code-block {
background: var(--code-bg);
border: 1.5px solid var(--border);
border-radius: var(--radius);
font-family: var(--font-mono);
color: var(--foreground);
}
/* Syntax tokens */
.code-block .kw { color: var(--primary); }
.code-block .fn { color: var(--accent); }
.code-block .str { color: var(--success); }
.code-block .cm { color: var(--muted-foreground); font-style: italic; }
.code-block .num { color: var(--warning); }
```
SKILL.md
---
name: plannotator-visual-explainer
disable-model-invocation: true
description: >
Generate self-contained HTML visualizations with Plannotator theming. Use for implementation
plans, PR explainers, architecture diagrams, data tables, slide decks, and any visual
explanation of technical concepts. Plans and PR explainers follow Plannotator's prescriptive
approach; all other visual content delegates to nicobailon/visual-explainer.
---
# Plannotator Visual Explainer
Three paths depending on content type. Each has its own references and structure.
## Route by content type
**Implementation plan, design doc, or proposal** → Follow the [Plan path](#plan-path). Read `references/design-system.md` and `references/svg-patterns.md`. Prescriptive structure.
**PR explainer, diff review, or code change walkthrough** → Follow the [PR path](#pr-path). Read `references/design-system.md` and `references/pr-components.md`. Prescriptive structure.
**Everything else** (architecture diagrams, data tables, slide decks, project recaps, general visual explanations) → Follow the [Visual explainer path](#visual-explainer-path). Delegates to nicobailon/visual-explainer with Plannotator theme tokens.
## Delivery
Always deliver via Plannotator's annotation UI. Do NOT use `open` or `xdg-open`.
For any deliverable that uses Mermaid, render every diagram with Mermaid 11 in both the light
and dark palettes before opening the annotation UI. Rendering is a hard gate: an exception,
empty SVG, or error output such as `aria-roledescription="error"` or `Syntax error in text`
means the explainer is not deliverable. Fix the diagram or theme configuration and rerun both
palettes until every SVG passes.
**Plans/proposals** (user should approve/deny):
```bash
plannotator annotate <file> --gate
```
**Everything else** (informational):
```bash
plannotator annotate <file>
```
---
## Plan path
For implementation plans, design docs, feature specs, migration guides, and proposals.
**Before generating, read:**
1. `references/design-system.md` — Plannotator theme tokens, typography, component patterns
2. `references/svg-patterns.md` — inline SVG building blocks for architecture diagrams, flowcharts, data flow
**Document structure (in order, pick what fits):**
1. **Header** — eyebrow label (mono, uppercase), title (serif, large), prompt box (the original brief)
2. **Summary strip** — 3-5 stat cards showing key numbers at a glance (components, endpoints, tables, etc.)
3. **Milestones / timeline** — vertical timeline showing phases without time estimates. Phases show sequence and dependencies, not duration.
4. **Architecture / data flow** — inline SVG diagram. Use for 3+ interacting components. Highlighted boxes for new components, dashed arrows for async paths.
5. **Mockups** — build UI mockups in HTML/CSS directly, not as descriptions
6. **Key code** — dark-theme code blocks with syntax highlighting. Only architecturally significant interfaces/schemas — not every function.
7. **Risks & mitigations** — table with severity badges (HIGH/MED/LOW)
8. **Open questions** — callout cards with decision owner ("Decide with: backend team")
Not every plan needs every section. Skip what doesn't serve the content. Never include time estimates, boilerplate sections, or exhaustive file lists.
**Adapt to the task:** Backend → lead with data flow. Frontend → lead with mockups. Refactoring → lead with before/after diagrams. Infrastructure → lead with architecture.
**Quality bar:** The plan answers "what, why, and how" within 30 seconds of reading. Whitespace is a feature — one idea per viewport.
---
## PR path
For PR walkthroughs, diff reviews, code change explainers, and reviewer guides.
**Before generating, read:**
1. `references/design-system.md` — Plannotator theme tokens, typography, component patterns
2. `references/pr-components.md` — diff rendering, review comment bubbles, risk chips, file cards, before/after panels
**Document structure (in order, pick what fits):**
1. **Header** — PR title, meta strip (file count, +/- lines, branch, author)
2. **TL;DR** — bordered card with primary accent left border. 2-3 sentences. Readers who see nothing else should get the gist.
3. **Why** — motivation and before/after comparison (two-column grid)
4. **File tour** — collapsible cards per file. Each has: file path + badge (NEW/MOD/DEL) + line stats, a "why" paragraph, and important diff hunks. High-risk files expanded, safe files collapsed.
5. **Risk map** — visual chips showing which files need careful review vs. which are mechanical. Three tiers: attention (destructive), medium (warning), safe (success).
6. **Where to focus** — numbered callout cards. Each names a file/function and describes the concern.
7. **Test plan** — checkbox-style verification checklist
8. **Rollout** (if applicable) — phased deployment with feature flags
Use Pierre diffs via CDN for syntax-highlighted inline diffs — see `references/pr-components.md` for the pattern.
---
## Visual explainer path
For architecture diagrams, data tables, slide decks, project recaps, comparisons, and any other visual explanation.
**Before generating:**
1. Ensure `visual-explainer` is installed:
- Check: `~/.claude/skills/visual-explainer/SKILL.md` or `~/.agents/skills/visual-explainer/SKILL.md`
- If not found: `npx skills add nicobailon/visual-explainer -g --yes`
2. Read visual-explainer's `SKILL.md` (workflow, diagram types, anti-slop rules)
3. Read the relevant visual-explainer references and templates for your content type
4. Read `references/theme-override.md` — Plannotator tokens replacing Nico's palettes
Follow visual-explainer's structure, component classes (`.ve-card`, `.kpi-card`, `.pipeline`), and anti-slop rules. The only override is the color/typography layer — Plannotator tokens instead of Nico's custom palettes.
---
## Design philosophy (all paths)
- **Whitespace is a feature.** Generous padding, large section gaps. If cramped, add space — don't shrink text.
- **One idea per viewport.** Hero section, then diagram, then detail grid — not all crammed together.
- **Show, don't describe.** A timeline shows sequencing. A diagram shows relationships. A code block shows the interface.
- **No time estimates.** Timelines show phases and dependencies. Never attach hour/day estimates.
SKILL.test.ts
/// <reference types="bun-types" />
/// <reference types="node" />
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const themeOverride = readFileSync(
join(import.meta.dir, "references/theme-override.md"),
"utf-8",
);
const mermaidStart = themeOverride.indexOf("## Mermaid theming");
const mermaidEnd = themeOverride.indexOf("\n## ", mermaidStart + 1);
const mermaidSection = themeOverride.slice(mermaidStart, mermaidEnd);
const exampleStart = mermaidSection.indexOf("```javascript");
const exampleEnd = mermaidSection.indexOf("```", exampleStart + 3);
const mermaidExample = mermaidSection.slice(exampleStart, exampleEnd);
const mermaidExampleCode = mermaidExample.slice(
mermaidExample.indexOf("\n") + 1,
);
const skill = readFileSync(join(import.meta.dir, "SKILL.md"), "utf-8");
interface CapturedMermaidConfig {
theme?: string;
themeVariables?: Record<string, unknown>;
}
function captureMermaidConfig(colorScheme: "light" | "dark") {
let captured: CapturedMermaidConfig | undefined;
const runExample = new Function(
"mermaid",
"getComputedStyle",
"window",
"document",
mermaidExampleCode,
);
runExample(
{
initialize: (config: CapturedMermaidConfig) => {
captured = config;
},
},
() => ({ colorScheme }),
{ matchMedia: () => ({ matches: colorScheme === "dark" }) },
{ documentElement: {} },
);
if (!captured?.themeVariables) {
throw new Error(`Mermaid example did not initialize the ${colorScheme} palette`);
}
return { name: colorScheme, config: captured };
}
describe("plannotator-visual-explainer Mermaid theming", () => {
test("keeps the Mermaid theming section", () => {
expect(mermaidStart).toBeGreaterThan(-1);
expect(mermaidEnd).toBeGreaterThan(mermaidStart);
expect(exampleStart).toBeGreaterThan(-1);
expect(exampleEnd).toBeGreaterThan(exampleStart);
});
test("uses Mermaid-compatible literal colors", () => {
const literalColors = mermaidExample.match(/#[0-9a-f]{6}\b/gi) ?? [];
expect(mermaidExample).toContain("themeVariables");
expect(literalColors.length).toBeGreaterThanOrEqual(10);
});
test("keeps CSS color processing outside Mermaid themeVariables", () => {
expect(mermaidSection).not.toMatch(/\boklch\(\s*[^)]/i);
expect(mermaidSection).not.toMatch(/\bvar\(\s*[^)]/i);
expect(mermaidSection).not.toMatch(/\bcolor-mix\(\s*[^)]/i);
});
test("preserves OKLCH for ordinary page CSS", () => {
expect(themeOverride).toMatch(/--background:\s+oklch\(/);
expect(themeOverride).toMatch(
/@media \(prefers-color-scheme: dark\)[\s\S]*--background:\s+oklch\(/,
);
});
test("renders representative Mermaid 11 diagrams in both palettes", async () => {
const palettes = [captureMermaidConfig("light"), captureMermaidConfig("dark")];
const uiPackageDir = join(import.meta.dir, "../../../../packages/ui");
const renderProbe = String.raw`
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register();
const [{ default: mermaid }, { default: mermaidPackage }] = await Promise.all([
import("mermaid"),
import("mermaid/package.json", { with: { type: "json" } }),
]);
if (!String(mermaidPackage.version).startsWith("11.")) {
throw new Error("Expected Mermaid 11, received " + mermaidPackage.version);
}
const palettes = JSON.parse(process.env.PLANNOTATOR_MERMAID_PALETTES ?? "[]");
const diagrams = [
{
name: "architecture",
source: "flowchart TD\n A[Provider] --> B[Database]",
labels: ["Provider", "Database"],
},
{
name: "review-flow",
source: "flowchart LR\n U([Reviewer]) --> D{Approve?}\n D -->|Yes| M[(Merge)]\n D -->|No| R[Revise]",
labels: ["Reviewer", "Approve?", "Merge", "Revise"],
},
];
const errorSignature = /aria-roledescription\s*=\s*["']error["']|Syntax error in text/i;
const knownErrorOutputs = [
'<svg aria-roledescription="error"></svg>',
'<svg><text>Syntax error in text</text></svg>',
];
if (knownErrorOutputs.some((output) => !errorSignature.test(output))) {
throw new Error("Mermaid error-output guard does not recognize its required signatures");
}
for (const palette of palettes) {
for (const diagram of diagrams) {
mermaid.initialize({
...palette.config,
startOnLoad: false,
// Happy DOM cannot run DOMPurify's strict-mode serialization, but Mermaid's
// parser, theme derivation, layout, and SVG renderer all execute in loose mode.
securityLevel: "loose",
});
const { diagramType, svg } = await mermaid.render(
"probe-" + palette.name + "-" + diagram.name,
diagram.source,
);
if (!svg || !/<svg\b/i.test(svg)) {
throw new Error(palette.name + "/" + diagram.name + " produced an empty SVG");
}
if (diagramType === "error" || errorSignature.test(svg)) {
throw new Error(palette.name + "/" + diagram.name + " produced Mermaid error output");
}
if (!/aria-roledescription=["']flowchart-v2["']/.test(svg)) {
throw new Error(palette.name + "/" + diagram.name + " was not rendered as a flowchart");
}
for (const colorKey of ["primaryColor", "primaryTextColor"]) {
const color = String(palette.config.themeVariables?.[colorKey] ?? "").toLowerCase();
if (!color || !svg.toLowerCase().includes(color)) {
throw new Error(
palette.name + "/" + diagram.name + " did not apply " + colorKey,
);
}
}
for (const label of diagram.labels) {
if (!svg.includes(label)) {
throw new Error(palette.name + "/" + diagram.name + " omitted label " + label);
}
}
}
}
console.log(
"Rendered " + (palettes.length * diagrams.length) +
" Mermaid " + mermaidPackage.version + " SVGs without error signatures",
);
`;
const child = Bun.spawn(
[process.execPath, "--cwd", uiPackageDir, "-e", renderProbe],
{
env: {
...process.env,
PLANNOTATOR_MERMAID_PALETTES: JSON.stringify(palettes),
},
stdout: "pipe",
stderr: "pipe",
},
);
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
if (exitCode !== 0) {
throw new Error(`Mermaid render probe failed:\n${stderr || stdout}`);
}
expect(stdout).toMatch(
/Rendered 4 Mermaid 11\.[0-9.]+\.[0-9]+ SVGs without error signatures/,
);
}, 20_000);
test("keeps Mermaid rendering as a pre-delivery gate", () => {
expect(skill).toContain("render every diagram with Mermaid 11");
expect(skill).toContain('aria-roledescription="error"');
expect(skill).toContain("Syntax error in text");
expect(skill).toContain("the explainer is not deliverable");
});
});