Merge pull request #12 from cathrynlavery/add-terminal-primitive

Add terminal primitive: charcoal-black CLI-window skin
This commit is contained in:
Cathryn Lavery
2026-07-14 20:35:42 -07:00
committed by GitHub
7 changed files with 621 additions and 3 deletions
+6 -3
View File
@@ -240,16 +240,18 @@ diagram-design/
│ ├── type-venn.md
│ ├── type-pyramid.md
│ ├── primitive-annotation.md — italic-serif editorial callouts
│ └── primitive-sketchy.md — hand-drawn SVG filter variant
│ ├── primitive-sketchy.md — hand-drawn SVG filter variant
│ └── primitive-terminal.md — charcoal-black CLI-window variant
├── assets/
│ ├── index.html — live gallery, tabbed
│ ├── template*.html — scaffolds for new diagrams
│ ├── example-<type>.html — 3 variants × 27 types
│ ├── example-loop-terminal.html — terminal-variant flagship
│ └── example-quadrant-consultant.html — consultant-special 2×2 scenario matrix
└── docs/screenshots/ — the images in this README
```
This keeps Claude's working context tight (only load what you need) and makes the skill easy to extend — drop a new `type-<name>.md` and wire it into the selection guide. The skill ships with 33 reference files covering every diagram type, primitive, and utility.
This keeps Claude's working context tight (only load what you need) and makes the skill easy to extend — drop a new `type-<name>.md` and wire it into the selection guide. The skill ships with 34 reference files covering every diagram type, primitive, and utility.
### Contributing / skin lint
@@ -258,7 +260,7 @@ The repository-wide check `python3 scripts/lint-skin.py --all --baseline` must s
### What loads when
The top-level `SKILL.md` is always in context. Everything else is pulled in only when relevant — this is what keeps the skill fast even with 33 reference files.
The top-level `SKILL.md` is always in context. Everything else is pulled in only when relevant — this is what keeps the skill fast even with 34 reference files.
| You ask for… | Claude loads |
|---|---|
@@ -267,6 +269,7 @@ The top-level `SKILL.md` is always in context. Everything else is pulled in only
| "Onboard this skill to my site" | `SKILL.md` + `references/onboarding.md` + `references/style-guide.md` |
| "Add an editorial callout to this diagram" | `SKILL.md` + `references/primitive-annotation.md` |
| "Give me a hand-drawn version" | `SKILL.md` + `references/primitive-sketchy.md` |
| "Give me a terminal / CLI-window version" | `SKILL.md` + `references/primitive-terminal.md` |
| Routine diagram-making (any of the 27 diagrams) | Only `SKILL.md` + that one type's reference |
No matter how many types exist, Claude only reads the one you need. Add a new type tomorrow and nothing else changes.
+1
View File
@@ -78,6 +78,7 @@ def allowed_colors():
markdown = STYLE_GUIDE.read_text(encoding="utf-8")
colors = table_hexes(markdown, "### Semantic roles")
colors.update(table_hexes(markdown, "### Series palette"))
colors.update(table_hexes(markdown, "### Terminal skin"))
colors.update({"#fff", "#ffffff"})
rgb_triplets = set()
+3
View File
@@ -193,6 +193,7 @@ Universal building blocks. Type-specialized primitives (lifeline, activation bar
- Editorial callouts → [primitive-annotation.md](references/primitive-annotation.md)
- Hand-drawn variant → [primitive-sketchy.md](references/primitive-sketchy.md)
- Icon set (laptop, server, DB, K8s, Docker, AWS, …) → [primitive-icons.md](references/primitive-icons.md). Browse the gallery at [`assets/icons.html`](../assets/icons.html).
- Terminal / CLI-window variant → [primitive-terminal.md](references/primitive-terminal.md)
### Background
@@ -458,6 +459,8 @@ Every diagram ships in three variants (see `assets/`):
**Sketchy variant** (optional, applied to any of the above) — see [primitive-sketchy.md](references/primitive-sketchy.md). SVG turbulence filter wobbles strokes for a hand-drawn feel. Good for essays, not for technical docs.
**Terminal variant** (optional, replaces any of the above) — see [primitive-terminal.md](references/primitive-terminal.md). `template-terminal.html`, `example-<type>-terminal.html`. Charcoal-black CLI-window chrome, monospace type, one red-orange accent. Good for dev-tool / CLI-product posts and technical social cards; not brand-tokenized, so skip it for onboarded/brand-matched output.
### To create a new diagram
1. Copy the variant closest to what you want (`template.html` for minimal, `template-full.html` for cards).
@@ -0,0 +1,347 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The self-improving loop · Terminal</title>
<link
href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600;700&amp;display=swap"
rel="stylesheet"
/>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--page: #0a0a0a;
--paper: #141414;
--bar: #1b1b1b;
--border: #2b2b2b;
--ink: #f5f5f5;
--muted: #9a9a9a;
--soft: #5c5c5c;
--accent: #ff5a36;
--accent-tint: rgba(255, 90, 54, 0.12);
--mono: "Geist Mono", ui-monospace, monospace;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
background: var(--page);
font-family: var(--mono);
}
.terminal {
width: 100%;
max-width: 1200px;
background: var(--paper);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.titlebar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--bar);
border-bottom: 1px solid var(--border);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--soft);
}
.dot.accent {
background: var(--accent);
}
.titlebar-name {
flex: 1;
text-align: center;
color: var(--muted);
font: 500 0.7rem var(--mono);
letter-spacing: 0.04em;
margin-right: 46px;
}
.frame {
padding: 2.25rem 2.5rem 2.5rem;
}
.prompt {
display: flex;
gap: 0.6em;
margin-bottom: 0.9rem;
color: var(--muted);
font: 500 0.8rem var(--mono);
letter-spacing: 0.01em;
}
.prompt .sign {
color: var(--accent);
}
h1 {
margin-bottom: 1.5rem;
color: var(--ink);
font: 700 clamp(1.5rem, 2.2vw + 0.8rem, 2.05rem)/1.2 var(--mono);
letter-spacing: -0.01em;
}
h1::before {
content: "# ";
color: var(--soft);
}
svg {
display: block;
width: 100%;
min-width: 900px;
}
.ring {
fill: none;
stroke: var(--muted);
stroke-width: 1.2;
}
.spoke {
fill: none;
stroke: var(--soft);
stroke-width: 1;
stroke-dasharray: 5 4;
}
.station {
fill: var(--paper);
stroke: var(--ink);
stroke-width: 1;
}
.station.focal {
fill: var(--accent-tint);
stroke: var(--accent);
stroke-width: 1.4;
}
.hub {
fill: var(--ink);
}
.node-name {
fill: var(--ink);
font: 600 14px var(--mono);
text-anchor: middle;
}
.focal-name {
fill: var(--accent);
}
.sublabel {
fill: var(--muted);
font: 400 9.5px var(--mono);
text-anchor: middle;
}
.hub-name {
fill: var(--paper);
font: 700 18px var(--mono);
text-anchor: middle;
}
.hub-sub {
fill: var(--paper);
opacity: 0.7;
font: 400 9.5px var(--mono);
text-anchor: middle;
}
.arrow-label {
fill: var(--muted);
font: 500 9px var(--mono);
letter-spacing: 0.08em;
text-anchor: middle;
}
</style>
</head>
<body>
<div class="terminal">
<div class="titlebar">
<div class="dot accent"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="titlebar-name">loop.sh — self-improving-loop</div>
</div>
<main class="frame">
<p class="prompt">
<span class="sign">$</span> diagram-design render --type loop
</p>
<h1>The self-improving loop</h1>
<svg
viewBox="0 0 1040 680"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="loop-title loop-desc"
>
<title id="loop-title">The self-improving loop</title>
<desc id="loop-desc">
Six stations flow clockwise from Capture through Learn and back to
Capture. Each station writes shared state into one central memory
hub, with Decide highlighted as the human approval gate.
</desc>
<defs>
<pattern
id="dots"
width="22"
height="22"
patternUnits="userSpaceOnUse"
>
<circle cx="1" cy="1" r="0.9" fill="rgba(255,255,255,0.08)" />
</pattern>
<marker
id="arrow"
markerWidth="8"
markerHeight="6"
refX="7"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="#9a9a9a" />
</marker>
<marker
id="arrow-soft"
markerWidth="8"
markerHeight="6"
refX="7"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="#5c5c5c" />
</marker>
</defs>
<rect width="1040" height="680" fill="#141414" />
<rect width="1040" height="680" fill="url(#dots)" opacity="0.7" />
<!-- Solid clockwise ring: six arcs on the same r=240 station circle. -->
<path
class="ring"
d="M600 113.726 A240 240 0 0 1 704.969 187.073"
marker-end="url(#arrow)"
/>
<path
class="ring"
d="M743.285 252 A240 240 0 0 1 743.722 426.882"
marker-end="url(#arrow)"
/>
<path
class="ring"
d="M705.731 492 A240 240 0 0 1 601.130 565.871"
marker-end="url(#arrow)"
/>
<path
class="ring"
d="M440 566.274 A240 240 0 0 1 335.031 492.927"
marker-end="url(#arrow)"
/>
<path
class="ring"
d="M296.715 428 A240 240 0 0 1 296.278 253.118"
marker-end="url(#arrow)"
/>
<path
class="ring"
d="M334.269 188 A240 240 0 0 1 438.870 114.129"
marker-end="url(#arrow)"
/>
<!-- Dashed write-backs: station inner edge to 6px before the hub stroke. -->
<path class="spoke" d="M520 132 V280" marker-end="url(#arrow-soft)" />
<path
class="spoke"
d="M672 252 L616 284"
marker-end="url(#arrow-soft)"
/>
<path
class="spoke"
d="M672 428 L616 396"
marker-end="url(#arrow-soft)"
/>
<path class="spoke" d="M520 548 V400" marker-end="url(#arrow-soft)" />
<path
class="spoke"
d="M368 428 L424 396"
marker-end="url(#arrow-soft)"
/>
<path
class="spoke"
d="M368 252 L424 284"
marker-end="url(#arrow-soft)"
/>
<!-- Curated spoke labels sit beside the line with an 8px visible gap. -->
<rect x="530" y="199" width="52" height="17" rx="4" fill="#141414" />
<text x="556" y="211.5" class="arrow-label">SIGNALS</text>
<rect x="457" y="463" width="58" height="17" rx="4" fill="#141414" />
<text x="486" y="475.5" class="arrow-label">OUTCOMES</text>
<!-- Stations -->
<rect class="station" x="440" y="68" width="160" height="64" rx="6" />
<text x="520" y="96.5" class="node-name">Capture</text>
<text x="520" y="116.5" class="sublabel">signals in / intake</text>
<rect
class="station"
x="648"
y="188"
width="160"
height="64"
rx="6"
/>
<text x="728" y="216.5" class="node-name">Research</text>
<text x="728" y="236.5" class="sublabel">evidence pulled</text>
<rect
class="station focal"
x="648"
y="428"
width="160"
height="64"
rx="6"
/>
<text x="728" y="456.5" class="node-name focal-name">Decide</text>
<text x="728" y="476.5" class="sublabel">human approves</text>
<rect
class="station"
x="440"
y="548"
width="160"
height="64"
rx="6"
/>
<text x="520" y="576.5" class="node-name">Act</text>
<text x="520" y="596.5" class="sublabel">work ships</text>
<rect
class="station"
x="232"
y="428"
width="160"
height="64"
rx="6"
/>
<text x="312" y="456.5" class="node-name">Measure</text>
<text x="312" y="476.5" class="sublabel">outcomes logged</text>
<rect
class="station"
x="232"
y="188"
width="160"
height="64"
rx="6"
/>
<text x="312" y="216.5" class="node-name">Learn</text>
<text x="312" y="236.5" class="sublabel">playbook updated</text>
<!-- The one shared-state hub. -->
<rect class="hub" x="420" y="288" width="200" height="104" rx="8" />
<text x="520" y="337.5" class="hub-name">Shared memory</text>
<text x="520" y="360.5" class="hub-sub">one record, every loop</text>
</svg>
</main>
</div>
</body>
</html>
@@ -0,0 +1,170 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagram</title>
<link
href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--color-page: #0a0a0a;
--color-paper: #141414;
--color-bar: #1b1b1b;
--color-border: #2b2b2b;
--color-ink: #f5f5f5;
--color-muted: #9a9a9a;
--color-soft: #5c5c5c;
--color-accent: #ff5a36;
--color-accent-tint: rgba(255, 90, 54, 0.12);
--font-mono: "Geist Mono", ui-monospace, monospace;
}
body {
font-family: var(--font-mono);
background: var(--color-page);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.terminal {
width: 100%;
max-width: 1200px;
background: var(--color-paper);
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
}
.titlebar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--color-bar);
border-bottom: 1px solid var(--color-border);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-soft);
}
.dot.accent {
background: var(--color-accent);
} /* exactly one of the three dots gets this */
.titlebar-name {
flex: 1;
text-align: center;
color: var(--color-muted);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.04em;
margin-right: 46px; /* offsets the three dots so the name optically centers */
}
.frame {
padding: 2.25rem 2.5rem 2.5rem;
}
.prompt {
display: flex;
gap: 0.6em;
margin-bottom: 0.9rem;
color: var(--color-muted);
font-size: 0.8rem;
font-weight: 500;
}
.prompt .sign {
color: var(--color-accent);
}
h1 {
font-family: var(--font-mono);
font-size: clamp(1.5rem, 2.2vw + 0.8rem, 2.05rem);
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.2;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
h1::before {
content: "# ";
color: var(--color-soft);
}
svg {
width: 100%;
min-width: 900px;
display: block;
}
</style>
</head>
<body>
<div class="terminal">
<div class="titlebar">
<div class="dot accent"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="titlebar-name">[file.sh] — [diagram-slug]</div>
</div>
<main class="frame">
<p class="prompt">
<span class="sign">$</span> [command that "generated" this]
</p>
<h1>[Diagram title]</h1>
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker
id="arrow"
markerWidth="8"
markerHeight="6"
refX="7"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="#9a9a9a" />
</marker>
<marker
id="arrow-accent"
markerWidth="8"
markerHeight="6"
refX="7"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="#ff5a36" />
</marker>
<!-- OPTIONAL: dot-pattern background. Uncomment and add <rect fill="url(#dots)"/> after the paper rect to enable. -->
<!--
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(255,255,255,0.08)"/>
</pattern>
--></defs>
<rect width="100%" height="100%" fill="#141414" />
<!-- Draw arrows first, then nodes. Replace with your content.
Text sizes run ~1-2px above the default scale: node-name 14px,
sublabel/arrow-label 9-10px, hub label 18px. See primitive-terminal.md. -->
</svg>
</main>
</div>
</body>
</html>
@@ -0,0 +1,76 @@
# Terminal Window (CLI-chrome variant)
Optional full-page skin that wraps any diagram in a fake terminal window — titlebar with three dots, a `$` prompt line, monospace type throughout. Use for dev-tool announcements, CLI-product posts, and technical social cards where a screenshot needs to read as "terminal," not "editorial doc."
This is a **second, fixed skin** — see [style-guide.md § Terminal skin](style-guide.md#terminal-skin-opt-in-alternate) for the token table. It does not inherit from `onboarding.md` brand tokens and isn't part of the light/dark inversion rule; every terminal example uses the same nine tokens regardless of the host site's brand.
## Grammar
```html
<div class="terminal">
<div class="titlebar">
<div class="dot accent"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="titlebar-name">loop.sh — self-improving-loop</div>
</div>
<main class="frame">
<p class="prompt">
<span class="sign">$</span> diagram-design render --type loop
</p>
<h1># The self-improving loop</h1>
<svg>...</svg>
</main>
</div>
```
```css
body {
background: var(--terminal-page);
}
.terminal {
background: var(--terminal-paper);
border: 1px solid var(--terminal-border);
border-radius: 12px;
}
.titlebar {
background: var(--terminal-bar);
border-bottom: 1px solid var(--terminal-border);
}
.dot {
background: var(--terminal-soft);
}
.dot.accent {
background: var(--terminal-accent);
}
```
Inside the SVG, swap the default light/dark tokens 1:1 for their `terminal-*` equivalents: `paper` → `terminal-paper`, `ink` → `terminal-ink`, `muted`/`soft` → `terminal-muted`/`terminal-soft`, `accent`/`accent-tint` → `terminal-accent`/`terminal-accent-tint`. The hub/focal-node pattern (inverted fill for the one highlighted element) still applies.
## Typography
**Everything is monospace** — this is the one variant where that's correct. Drop Instrument Serif and Geist sans entirely; set the page title in mono, bold, prefixed with `# ` (reads as a comment line). The eyebrow becomes a shell prompt: `$ ` in `terminal-accent`, the command in `terminal-muted`.
Run every text role about **1–2px above** the default type scale in `style-guide.md` (e.g. `node-name` 12px → 14px, `sublabel`/`arrow-label` 8–9px → 9–10px, hub label 16px → 18px). Monospace at the default sizes reads small next to the sans/serif mix it's replacing, and these cards are usually viewed at social-feed scale, not full-bleed.
## Titlebar dots
Three 10px circles, macOS-style. The **1-accent rule caps the color use here too**: one dot is `terminal-accent`, the other two are `terminal-soft`. Do not use a red/yellow/green traffic-light triad — that's a second and third hue, which the palette forbids.
## Critical rules
- No pure black (`#000000`) — use `terminal-page` (`#0a0a0a`) / `terminal-paper` (`#141414`). Same rule as the default skin, same reason: true black clips on OLED and in print.
- One accent only. If a diagram needs a second focal element, use `terminal-ink` (white) for emphasis via weight/size, not a second color.
- Background dot-grid pattern (if used) stays `rgba(255,255,255,0.06–0.08)` — barely visible texture, not a visual competitor to the titlebar chrome.
## When to use
- Dev-tool / CLI-product launch posts (npm package, CLI flag, terminal-based workflow).
- Technical social cards where "this is a tool for engineers" is part of the message.
- Screenshots meant to pop in a dark-mode-heavy feed (X, Discord, dev blogs).
## When not to use
- Editorial / long-form posts — pair with the default light or full-editorial variant instead.
- Brand-matched output from `onboarding.md` — terminal is a fixed skin, not brand-tokenized. Don't try to reconcile the two.
- Any diagram where the audience isn't developer-coded to read `$`/`#`/titlebar-dots as chrome rather than content.
@@ -49,6 +49,24 @@ A small set of desaturated, editorial-tone colors for chart types that genuinely
Fills sit at `0.18` opacity light, `0.22` dark; strokes use the full color. **Don't backfill these tokens to non-chart types** — architecture, swimlane, etc. continue to use muted-ink variants. The series palette is opt-in for diagrams where overlapping shapes demand distinguishable color, not a license to add color elsewhere.
### Terminal skin (opt-in alternate)
A self-contained palette for the terminal-window primitive (see [primitive-terminal.md](primitive-terminal.md)) — a CLI-chrome register for dev-tool posts and technical social cards. It does not replace the default skin above and isn't affected by onboarding; it's a second, fixed skin you opt into per-diagram.
| Token | Hex | Purpose |
|---|---|---|
| `terminal-page` | `#0a0a0a` | Page background behind the window |
| `terminal-paper` | `#141414` | Window body, node fill |
| `terminal-bar` | `#1b1b1b` | Titlebar strip |
| `terminal-border` | `#2b2b2b` | Window border, hairlines |
| `terminal-ink` | `#f5f5f5` | Primary text, primary stroke (same white-smoke as default `ink`) |
| `terminal-muted` | `#9a9a9a` | Secondary text, sublabels, ring stroke |
| `terminal-soft` | `#5c5c5c` | Tertiary — inactive dots, spokes |
| `terminal-accent` | `#ff5a36` | The one accent — focal station, prompt sign, active dot |
| `terminal-accent-tint` | `rgba(255,90,54,0.12)` | Fill for accent-bordered boxes |
**1-accent rule still holds.** Everything that isn't `terminal-ink` or `terminal-muted`/`terminal-soft` should be `terminal-accent` — never introduce a second hue.
---
## Typography