
Plannotator Visual Explainer
- 5.4k installs
- 7.5k repo stars
- Updated August 5, 2026
- backnotprop/plannotator
A skill that routes content (plans, PRs, diagrams) through prescriptive paths and generates self-contained HTML visualizations with Plannotator theming, delivered via CLI annotation UI.
About
Plannotator Visual Explainer is a skill that generates self-contained HTML visualizations with consistent theming for technical documentation. It routes content through three paths: Plan path for implementation plans and design docs (prescriptive structure with timelines, architecture diagrams, and risk tables); PR path for code change walkthroughs (diff reviews, file tours, risk maps); and Visual Explainer path delegating to nicobailon/visual-explainer for diagrams, tables, and decks. All outputs render via Plannotator's annotation UI with design-system tokens, SVG patterns, and whitespace-first layout principles. Designed for developers explaining complex technical concepts visually.
- Three content-routed paths: plans, PRs, and general visual explanations
- Prescriptive HTML structure with Plannotator design-system theming
- Inline SVG diagram building blocks for architecture and data flows
- CLI delivery via plannotator annotate with optional approval gates
- Whitespace-first design: one idea per viewport, no time estimates
Plannotator Visual Explainer by the numbers
- 5,411 all-time installs (skills.sh)
- +521 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #72 of 1,879 Documentation skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/backnotprop/plannotator --skill plannotator-visual-explainerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.4k |
|---|---|
| repo stars | ★ 7.5k |
| Security audit | 0 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | backnotprop/plannotator ↗ |
What it does
Generate prescriptive HTML visualizations for implementation plans, PR explainers, and architecture diagrams with Plannotator theming via CLI annotation UI.
Who is it for?
Backend/frontend/infra teams authoring implementation plans, PR reviews, architecture documentation, design specs, and migration guides.
Skip if: Real-time collaborative design; pixel-perfect UI mockups; content without technical substance; teams rejecting CLI workflows.
When should I use this skill?
Writing implementation plans, submitting PRs for review, documenting architecture, creating design specs, explaining complex code changes, or building proposal decks.
What you get
Teams produce polished, on-brand technical documentation (plans, PR explainers, diagrams) in minutes, improving review clarity and decision-making.
- Self-contained HTML visualization
- Themed plan or PR explainer page
Files
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. Read references/design-system.md and references/svg-patterns.md. Prescriptive structure.
PR explainer, diff review, or code change walkthrough → Follow the 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. Delegates to nicobailon/visual-explainer with Plannotator theme tokens.
Delivery
Always deliver via Plannotator's annotation UI. Do NOT use open or xdg-open.
Plans/proposals (user should approve/deny):
plannotator annotate <file> --gateEverything else (informational):
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.mdor~/.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.
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.
: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
*, *::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:
2rempage title - H2:
1.4remsection headers - H3:
1.15remsubsection 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
<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>.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)
<div class="summary-strip">
<div class="stat-card">
<span class="stat-value">4</span>
<span class="stat-label">Components</span>
</div>
<!-- more cards -->
</div>.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
<section>
<div class="section-header">
<span class="section-number">01</span>
<h2>Solution Overview</h2>
</div>
<!-- content -->
</section>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)
<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>.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
<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>.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
<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>.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
<div class="tags">
<span class="tag">packages/server</span>
<span class="tag highlight">new endpoint</span>
</div>.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:
<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>.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
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 720px) {
.two-col { grid-template-columns: 1fr; }
}Collapsible details
<details>
<summary>Implementation details</summary>
<div class="details-body"><!-- content --></div>
</details>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.
<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>.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;
}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 2. TL;DR Box 3. Diff Rendering 4. Review Comments 5. Risk Map 6. File Cards 7. Before / After 8. Where to Focus 9. Test Plan 10. File Badges
PR header
<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>.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
<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>.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.
<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.
<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>.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.
<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>.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.
<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>.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.
<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>.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.
<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>.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.
<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>.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
.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.
<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>.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; }
}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 2. Architecture Diagrams 3. Flowcharts 4. Data Flow 5. Bar Charts 6. Positioning & Layout
Arrow markers
Define reusable markers in <defs>. Reference them via marker-end="url(#arrow)".
<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
<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)
<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
<!-- 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:
<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 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
<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
<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
<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
<!-- 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:
<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
<!-- 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
<!-- 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 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
viewBoxwith fixed coordinates; setstyle="width:100%;max-width:NNNpx"for responsive scaling - Standard widths:
720pxfull-width,480pxhalf-width,360pxsidebar - Standard heights:
180–320pxfor most diagrams
Box sizing
- Standard node:
120–160pxwide,48–56pxtall - Minimum gap between nodes:
60pxhorizontal,40pxvertical - Arrow label offset:
8–12pxabove the line - Diagram padding:
20pxinside 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 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>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.
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, these get overridden by the active theme.
: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
When visual-explainer instructs you to set themeVariables in Mermaid config, use Plannotator tokens:
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'oklch(0.50 0.25 280)', // --primary
primaryTextColor: 'oklch(1 0 0)', // --primary-foreground
primaryBorderColor: 'oklch(0.88 0.01 260)', // --border
lineColor: 'oklch(0.40 0.02 260)', // --muted-foreground
secondaryColor: 'oklch(0.92 0.01 260)', // --muted
tertiaryColor: 'oklch(0.92 0.01 260)', // --muted
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, 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:
@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:
/* 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
.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); }Related skills
How it compares
Choose plannotator-visual-explainer for plan and PR HTML with Plannotator conventions; use pure visual-explainer when theming and plan-structure rules are not required.
FAQ
What content types does plannotator-visual-explainer handle?
plannotator-visual-explainer handles implementation plans, design docs, proposals, PR explainers, architecture diagrams, data tables, and slide decks. Plans and PR explainers use Plannotator's prescriptive path; other visuals delegate to visual-explainer.
What output format does plannotator-visual-explainer produce?
plannotator-visual-explainer produces self-contained HTML files with Plannotator theming. The HTML is designed so agents and reviewers can follow implementation plans and technical explanations without ambiguity.
Is Plannotator Visual Explainer safe to install?
skills.sh reports 0 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.