mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
docs(.claude/skills/svg-docs-diagrams): skill for authoring SVG docs diagrams
Captures the conventions learned while iterating on the Claude Code self-hosted runners diagrams: * viewBox sizing for the ~657 CSS-pixel docs prose column, with a table mapping viewBox font sizes to on-page font sizes. * The 'paint-order: stroke' white halo pattern for arrow labels so they stay readable over zone-tinted backgrounds. * Card title wrapping rules so titles don't overflow a 144 px card. * Locked-to-one-user badge and per-instance suffix conventions for surfacing the things readers most need to see. * A reproducible test loop: render the docs page (not the raw SVG) with a copy-pastable headless Chrome script, then verify the click-through full-size view with a computer_use subagent. * Pitfalls: subagents reusing stale screenshots, Next.js asset caching, and headless Chrome rendering SVGs at sub-natural width when the file is opened directly. Ships with two reference files: 'render-diagram.sh' (a cache-busting docs-page screenshot script) and 'template.svg' (a starter SVG with all conventions baked in).
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
---
|
||||
name: svg-docs-diagrams
|
||||
description: "Author and iterate on SVG architecture diagrams embedded in Coder docs pages. Covers viewBox sizing for the docs column, readable arrow labels, the docs-page test loop with headless Chrome, and the recurring pitfalls."
|
||||
---
|
||||
|
||||
# SVG Docs Diagrams
|
||||
|
||||
Use this skill when you are writing or editing an SVG diagram that ships in
|
||||
`docs/images/**` and renders inside a docs page.
|
||||
|
||||
Coder docs render at a fixed prose column width (`max-w-[65ch]`, roughly
|
||||
650 to 680 CSS pixels regardless of viewport). Every diagram is scaled to
|
||||
that column. A diagram that looks great at native 1920px will be
|
||||
unreadable on the page. This skill captures the conventions that produce
|
||||
diagrams that read at column width and still look correct when a reader
|
||||
clicks the image to view it full-size.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
- Adding a new SVG to `docs/images/**` for an existing or new doc page.
|
||||
- Iterating on an existing diagram (font sizes, layout, arrow routing).
|
||||
- Debugging an overlap, overflow, or unreadable label complaint.
|
||||
- Removing or splitting a diagram that is too dense for the column width.
|
||||
|
||||
Do not use this skill for PNG screenshots, icon SVGs, or third-party
|
||||
brand SVGs.
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
docs/images/guides/<guide-name>/
|
||||
<diagram>.svg
|
||||
<flow>.svg
|
||||
```
|
||||
|
||||
Reference each SVG from Markdown as a plain `<img>` tag so you control
|
||||
the alt text and the path. The frontend renders SVGs at the column
|
||||
width and lets the reader click for a full-size view, so the SVG must
|
||||
look correct at both sizes.
|
||||
|
||||
```markdown
|
||||
<img
|
||||
src="../../images/guides/<guide-name>/<diagram>.svg"
|
||||
alt="One sentence that fully describes the diagram for screen readers."
|
||||
/>
|
||||
```
|
||||
|
||||
## Authoring rules
|
||||
|
||||
### viewBox sizing
|
||||
|
||||
The docs column is ~657 CSS pixels wide. The SVG is scaled to fit. To
|
||||
keep text readable on the page, target a viewBox aspect ratio where the
|
||||
horizontal scale factor is no smaller than 0.5. That means a viewBox
|
||||
width of about 1000 to 1180 works well. Wider than 1280 and side-card
|
||||
text becomes unreadable.
|
||||
|
||||
For a square-ish concept diagram: `viewBox="0 0 1000 460"` is a good
|
||||
default.
|
||||
|
||||
For a flow diagram: `viewBox="0 0 960 360"` keeps things proportionate
|
||||
and matches the existing flow diagrams in
|
||||
`docs/images/guides/claude-code-self-hosted-runners/`.
|
||||
|
||||
Always set `preserveAspectRatio="xMidYMid meet"` and a `width` attribute
|
||||
matching the viewBox width.
|
||||
|
||||
### Text size minima
|
||||
|
||||
Compute the on-page font size as
|
||||
`viewBoxFontSize × (657 / viewBoxWidth)`. Aim for these on-page sizes:
|
||||
|
||||
| Role | On-page target | Example viewBox size at width 1000 |
|
||||
|---------------------|----------------|------------------------------------|
|
||||
| Layer / zone title | 16 px or more | 26 to 30 px |
|
||||
| Card title | 9 to 10 px | 14 to 16 px |
|
||||
| Body / mono text | 7 to 8 px | 11 to 14 px |
|
||||
| Arrow label | 7 to 8 px | 11 to 12 px |
|
||||
| Footnote | 8 to 9 px | 13 to 14 px |
|
||||
|
||||
Body text rendering below 7 px on the page is unreadable. Either shrink
|
||||
the viewBox, shorten the copy, or drop the text from the diagram and
|
||||
put it in the surrounding Markdown instead.
|
||||
|
||||
### Arrow labels
|
||||
|
||||
Floating gray text over a tinted zone background is hard to read,
|
||||
especially at the docs column width. Always give arrow labels a white
|
||||
halo so they stay legible regardless of background color:
|
||||
|
||||
```css
|
||||
.arrow-label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
fill: #111827;
|
||||
paint-order: stroke;
|
||||
stroke: #ffffff;
|
||||
stroke-width: 3px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
```
|
||||
|
||||
`paint-order: stroke` paints the stroke before the fill, producing a
|
||||
clean white outline that does not eat the glyphs. Bump
|
||||
`stroke-width` until labels are clearly readable on the page; 3px in
|
||||
the viewBox is usually enough.
|
||||
|
||||
### Card title wrapping
|
||||
|
||||
SVG `<text>` does not wrap. A two-word card title like
|
||||
"Developer surfaces" or "Package registries" overflows a 144 px wide
|
||||
card at 15 px bold. When a title is longer than about 10 to 12
|
||||
characters, split it into two `<text>` elements stacked vertically:
|
||||
|
||||
```xml
|
||||
<text class="side-title" x="44" y="100">Developer</text>
|
||||
<text class="side-title" x="44" y="120">surfaces</text>
|
||||
```
|
||||
|
||||
Account for the extra height in the card rect.
|
||||
|
||||
### Visible identity and lock semantics
|
||||
|
||||
When a runner, workspace, or process is locked to a single user, show
|
||||
that on the diagram with a pill badge. Do not bury it in the
|
||||
description. Example:
|
||||
|
||||
```xml
|
||||
<rect class="badge-locked"
|
||||
x="358" y="160" width="148" height="24" rx="6"/>
|
||||
<text class="badge-label badge-locked-l" x="372" y="176">
|
||||
Locked to 1 user
|
||||
</text>
|
||||
```
|
||||
|
||||
The reader should see the lock at a glance, not have to parse a
|
||||
paragraph.
|
||||
|
||||
### Differentiating repeated items
|
||||
|
||||
If the diagram shows N instances of the same concept (sessions, pool
|
||||
slots, replicas) and they differ in some way, surface that difference
|
||||
in the labels. Three identical-looking session cards with identical
|
||||
`cwd: /workspace` lines hide the per-session subtree. Use a unique
|
||||
suffix per instance:
|
||||
|
||||
```xml
|
||||
<text class="session-mono" x="264" y="324">_sessions/cse_01ST</text>
|
||||
<text class="session-mono" x="438" y="324">_sessions/cse_a4Bf</text>
|
||||
<text class="session-mono" x="612" y="324">_sessions/cse_9pXk</text>
|
||||
```
|
||||
|
||||
### Always include accessible metadata
|
||||
|
||||
Every SVG must have `<title>` and `<desc>` elements referenced by
|
||||
`aria-labelledby` on the root `<svg>`. The `<desc>` should be one or
|
||||
two sentences that fully describe what the diagram shows; this is what
|
||||
screen readers announce and what search indexes pick up.
|
||||
|
||||
```xml
|
||||
<svg ... role="img" aria-labelledby="diag-title diag-desc">
|
||||
<title id="diag-title">Short title</title>
|
||||
<desc id="diag-desc">Longer prose description.</desc>
|
||||
...
|
||||
</svg>
|
||||
```
|
||||
|
||||
## Test loop
|
||||
|
||||
Treat diagrams like code: change, render, eyeball, repeat. The docs
|
||||
dev server is the source of truth, not native SVG rendering.
|
||||
|
||||
### Render the diagram inside the actual docs page
|
||||
|
||||
Start the docs dev server (see the project's existing dev instructions;
|
||||
in this repo it's coder.com running on port 4001 with `DOCS_ROOT`
|
||||
pointed at the coder/coder checkout).
|
||||
|
||||
Then render the docs page with headless Chrome and screenshot it. See
|
||||
`references/render-diagram.sh` in this skill directory for a
|
||||
copy-pastable script.
|
||||
|
||||
```bash
|
||||
bash .claude/skills/svg-docs-diagrams/references/render-diagram.sh \
|
||||
http://localhost:4001/docs/ai-coder/claude-code-self-hosted-runners \
|
||||
/tmp/svg-preview/page.png
|
||||
```
|
||||
|
||||
### Click into the full-size view
|
||||
|
||||
The docs frontend lets the reader click the inline image to view it at
|
||||
its natural size. Verify both:
|
||||
|
||||
- The inline rendering at column width: text is readable, no overlap.
|
||||
- The clicked-open view: text is sharp, no overflow past card borders.
|
||||
|
||||
If only the inline version has problems, the viewBox is too wide. If
|
||||
only the full-size has problems, individual elements are mispositioned.
|
||||
|
||||
### Use a `computer_use` subagent for the click-through
|
||||
|
||||
Headless Chrome can capture the inline rendering, but verifying the
|
||||
clicked-open full-size view and the overall feel is best done with a
|
||||
`computer_use` subagent. Spawn one with a clear list of checks:
|
||||
|
||||
```text
|
||||
For the diagram on <url>:
|
||||
1. Click through to the full-size view.
|
||||
2. For each of these labels, report OK or OVERFLOWS / OVERLAPS:
|
||||
- "<label 1>"
|
||||
- "<label 2>"
|
||||
...
|
||||
3. Are arrow labels readable against the zone backgrounds?
|
||||
4. Save the full-size screenshot to /tmp/svg-preview/<name>.png.
|
||||
Read-only. Do not modify any files.
|
||||
```
|
||||
|
||||
Do not trust the subagent's "looks good" if it reuses an old
|
||||
screenshot path. Verify the screenshot's `md5sum` changes when you
|
||||
edit the SVG.
|
||||
|
||||
### Cache busting
|
||||
|
||||
Next.js dev servers cache static assets. If a fresh screenshot shows
|
||||
your old SVG:
|
||||
|
||||
- Confirm the file on disk has the new content (`md5sum` it).
|
||||
- Append a cache-buster to the URL: `<svg-url>?t=$(date +%s%N)`.
|
||||
- Or restart the dev server.
|
||||
|
||||
If `curl <svg-url> | md5sum` matches the on-disk file, the dev server
|
||||
is serving the new SVG. Any stale-looking render is on the client
|
||||
side; force-reload or use incognito.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
### "Looks good" from a subagent without proof
|
||||
|
||||
Computer-use subagents sometimes claim they re-screenshotted when they
|
||||
actually reused a stale file. Always verify with:
|
||||
|
||||
```bash
|
||||
md5sum /tmp/svg-preview/<name>.png
|
||||
```
|
||||
|
||||
If the hash matches a previous version after you edited the SVG,
|
||||
re-run the screenshot yourself.
|
||||
|
||||
### Pixel-counting in headless Chrome is misleading
|
||||
|
||||
Headless Chrome can render an SVG with a sub-natural width when the
|
||||
window is wider than the SVG width. Trust the docs page render, not
|
||||
raw `chrome --headless ... file://.../foo.svg` output. Always test
|
||||
inside the docs page at viewport 1280.
|
||||
|
||||
### Estimating text width
|
||||
|
||||
There is no SVG equivalent of `text-overflow: ellipsis`. A 15 px bold
|
||||
title can fit roughly:
|
||||
|
||||
- 10 to 11 characters in a 144 px wide card.
|
||||
- 14 to 15 characters in a 200 px wide card.
|
||||
- 18 to 20 characters in a 260 px wide card.
|
||||
|
||||
If a title is longer than that, wrap to two lines or shrink the font.
|
||||
|
||||
### Stroked text via `paint-order` breaks if you forget `stroke-linejoin`
|
||||
|
||||
Without `stroke-linejoin: round`, the white halo around arrow labels
|
||||
has sharp corners that look like glitches at small sizes. Always
|
||||
include it.
|
||||
|
||||
### Don't `emdash` or `endash` in `<desc>` or `<text>`
|
||||
|
||||
`make lint/emdash` runs across the entire repo, SVGs included. Use a
|
||||
period, comma, or semicolon. Use plain ASCII hyphens.
|
||||
|
||||
### Don't commit preview PNGs
|
||||
|
||||
Put preview artifacts under `/tmp/svg-preview/`. Never commit them.
|
||||
Add to `.gitignore` if you accidentally `git add -A` them in.
|
||||
|
||||
## Reference checklist
|
||||
|
||||
Before you commit an SVG change:
|
||||
|
||||
- [ ] `viewBox` is no wider than 1280 (target 960 to 1180).
|
||||
- [ ] On-page font sizes hit the targets above.
|
||||
- [ ] Arrow labels use the white halo via `paint-order: stroke`.
|
||||
- [ ] No card title overflows its container at the docs column width.
|
||||
- [ ] Click-through to full-size still looks correct.
|
||||
- [ ] `<title>` and `<desc>` exist and describe the diagram.
|
||||
- [ ] `aria-labelledby` references both IDs.
|
||||
- [ ] No emdash, endash, or `--` punctuation anywhere.
|
||||
- [ ] Preview PNGs are not staged.
|
||||
- [ ] Tested with the docs dev server, not just raw chrome on the file.
|
||||
- [ ] `make lint/emdash` and `pnpm run lint-docs` pass.
|
||||
|
||||
## Related files
|
||||
|
||||
- `references/render-diagram.sh` — copy-pastable headless Chrome
|
||||
rendering script.
|
||||
- `references/template.svg` — minimal SVG template with the
|
||||
conventions baked in (viewBox, arrow-label halo, title and desc,
|
||||
badge classes).
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-diagram.sh — capture a docs page rendering for SVG diagram review.
|
||||
#
|
||||
# Usage:
|
||||
# render-diagram.sh <docs-url> <output-png> [viewport-width] [page-height]
|
||||
#
|
||||
# Defaults: viewport-width=1280, page-height=2400.
|
||||
#
|
||||
# Requires google-chrome (or chromium) in PATH and a docs dev server
|
||||
# running at the URL.
|
||||
#
|
||||
# Why this exists: rendering the SVG directly with `chrome --headless
|
||||
# file://.../foo.svg` does NOT match what the docs page produces. The
|
||||
# docs frontend scales SVGs to a fixed prose-column width, and that is
|
||||
# what readers see. Always test inside the docs page.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
URL="${1:-}"
|
||||
OUT="${2:-}"
|
||||
WIDTH="${3:-1280}"
|
||||
HEIGHT="${4:-2400}"
|
||||
|
||||
if [[ -z "$URL" || -z "$OUT" ]]; then
|
||||
echo "usage: $0 <docs-url> <output-png> [viewport-width] [page-height]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BROWSER=""
|
||||
for c in google-chrome chromium chromium-browser; do
|
||||
if command -v "$c" >/dev/null 2>&1; then
|
||||
BROWSER="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "$BROWSER" ]]; then
|
||||
echo "no chrome or chromium found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use a fresh user-data-dir per invocation to avoid clashing with any
|
||||
# existing Chrome instance and to bypass disk cache.
|
||||
TMP_PROFILE="$(mktemp -d -t chrome-render.XXXXXX)"
|
||||
trap 'rm -rf "$TMP_PROFILE"' EXIT
|
||||
|
||||
# Cache-bust the URL so the dev server returns the latest asset.
|
||||
SEP="?"
|
||||
[[ "$URL" == *\?* ]] && SEP="&"
|
||||
URL_CB="${URL}${SEP}t=$(date +%s%N)"
|
||||
|
||||
"$BROWSER" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--hide-scrollbars \
|
||||
--user-data-dir="$TMP_PROFILE" \
|
||||
--window-size="${WIDTH},${HEIGHT}" \
|
||||
--screenshot="$OUT" \
|
||||
"$URL_CB" 2>&1 |
|
||||
grep -vE 'dbus|GPU process|registration_request|TensorFlow|XNNPACK' || true
|
||||
|
||||
if [[ ! -s "$OUT" ]]; then
|
||||
echo "screenshot failed: $OUT is empty or missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "saved $OUT"
|
||||
md5sum "$OUT" 2>/dev/null || true
|
||||
@@ -0,0 +1,100 @@
|
||||
<!--
|
||||
SVG diagram template for Coder docs pages.
|
||||
|
||||
Conventions baked in:
|
||||
- viewBox sized for the ~657 CSS-pixel docs column.
|
||||
- preserveAspectRatio="xMidYMid meet" and width attribute set.
|
||||
- <title> and <desc> wired up via aria-labelledby.
|
||||
- Arrow labels use paint-order:stroke + white halo for readability.
|
||||
- Locked badge class shown.
|
||||
|
||||
See SKILL.md for the full ruleset and test loop.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 460"
|
||||
width="1000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img"
|
||||
aria-labelledby="diag-title diag-desc">
|
||||
<title id="diag-title">Short, factual diagram title</title>
|
||||
<desc id="diag-desc">One or two sentences fully describing the diagram for screen readers and search indexes.</desc>
|
||||
|
||||
<defs>
|
||||
<style>
|
||||
svg { font-family: -apple-system, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif; }
|
||||
.bg { fill: #ffffff; }
|
||||
|
||||
/* Zones (outer containers, one per managing party). */
|
||||
.zone-coder { fill: #f5f3ff; stroke: #7c3aed; stroke-width: 2; }
|
||||
.zone-anthropic { fill: #fff7ed; stroke: #f59e0b; stroke-width: 1.75; }
|
||||
.zone-network { fill: #ecfeff; stroke: #0e7490; stroke-width: 1.5; }
|
||||
|
||||
/* Cards inside zones. */
|
||||
.card { fill: #ffffff; stroke-width: 1.25; }
|
||||
.card-coder { stroke: #a78bfa; }
|
||||
.card-anthropic { stroke: #fbbf24; }
|
||||
.card-network { stroke: #67e8f9; }
|
||||
|
||||
/* Badges. */
|
||||
.badge-coder { fill: #ede9fe; stroke: #7c3aed; stroke-width: 1; }
|
||||
.badge-anthropic { fill: #fef3c7; stroke: #b45309; stroke-width: 1; }
|
||||
.badge-locked { fill: #fee2e2; stroke: #b91c1c; stroke-width: 1; }
|
||||
|
||||
.badge-coder-l { fill: #6d28d9; }
|
||||
.badge-anthropic-l{ fill: #b45309; }
|
||||
.badge-locked-l { fill: #b91c1c; }
|
||||
|
||||
/* Type. Sizes target the docs column width; see SKILL.md. */
|
||||
.zone-label { font-size: 16px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.layer-title { font-size: 26px; font-weight: 700; fill: #111827; }
|
||||
.layer-def { font-size: 14px; fill: #374151; }
|
||||
.card-title { font-size: 15px; font-weight: 700; fill: #111827; }
|
||||
.card-line { font-size: 13px; fill: #374151; }
|
||||
.mono { font-family: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace; font-size: 11px; fill: #1f2937; }
|
||||
.badge-label { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
||||
.footnote { font-size: 13px; fill: #4b5563; font-style: italic; }
|
||||
|
||||
/* Arrows. The white halo on labels keeps them readable. */
|
||||
.arrow { fill: none; stroke: #374151; stroke-width: 1.4; }
|
||||
.arrow-label {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
fill: #111827;
|
||||
paint-order: stroke;
|
||||
stroke: #ffffff;
|
||||
stroke-width: 3px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
</style>
|
||||
|
||||
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="6" markerHeight="6"
|
||||
orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#374151"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" width="1000" height="460"/>
|
||||
|
||||
<!-- Example zone -->
|
||||
<rect class="zone-coder" x="200" y="40" width="600" height="380" rx="16"/>
|
||||
<text class="zone-label" x="224" y="68" fill="#6d28d9">Coder</text>
|
||||
|
||||
<!-- Example card with locked badge -->
|
||||
<rect class="card card-coder" x="220" y="96" width="560" height="140" rx="10"/>
|
||||
<text class="card-title" x="240" y="124">Card title</text>
|
||||
<text class="card-line" x="240" y="146">Body line one.</text>
|
||||
<text class="card-line" x="240" y="164">Body line two.</text>
|
||||
|
||||
<rect class="badge-locked" x="620" y="106" width="148" height="22" rx="6"/>
|
||||
<text class="badge-label badge-locked-l" x="634" y="121">Locked to 1 user</text>
|
||||
|
||||
<!-- Example arrow with halo label -->
|
||||
<path class="arrow"
|
||||
d="M 220 300 C 360 240, 540 240, 780 300"
|
||||
marker-end="url(#ah)"/>
|
||||
<text class="arrow-label" x="420" y="244">arrow label stays readable</text>
|
||||
|
||||
<!-- Footnote -->
|
||||
<text class="footnote" x="20" y="446">Brief footnote summarizing what the diagram is and is not.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
Reference in New Issue
Block a user