wisp-diagram: Produce Wisp-Style Diagrams
Goal: Produce stylistically consistent, themeable, portable diagrams. Source files (.excalidraw / .drawio) + generated *.wisp.svg: the same SVG follows the site theme when inlined in a Wisp site, and follows system light/dark when used as an <img> in GitHub README / other blogs.
This skill directory is self-contained and can be copied as a whole to any project (~/.claude/skills/ or <project>/.claude/skills/). The scripts are Python, run with uv: the entry point scripts/wisp_diagram.py declares dependencies (playwright, fonttools) using PEP 723, and uv run will automatically create an isolated environment — no npm / pip install needed.
SKILL.md
palette.json Palette (the only source of colors)
reference/ style-guide.md excalidraw-skeleton.md drawio-cheatsheet.md ml-patterns.md
assets/ wisp.excalidrawlib wisp-drawio-library.xml drawio-config.json drawio-colors.txt
scripts/ wisp_diagram.py (unified entry point) wisp_diagram_lib/ (implementation) tests/ (pytest)
One-Time Setup
The commands below are run from the project root, and $W represents the entry script of the skill (adjust the path according to the actual location of the skill):
W="uv run skills/wisp-diagram/scripts/wisp_diagram.py" # e.g. .claude/skills/wisp-diagram/... or ~/.claude/skills/wisp-diagram/...
uv run --with playwright==1.64.1 playwright install chromium # Headless browser for export (skip if already installed)
$W probe # Check if Chromium, Excalidraw, draw.io export methods (cli / docker / browser) are available
First run downloads and caches (works offline afterward):
| Content | Source | Size |
|---|---|---|
| Python dependencies: playwright 1.64.1, fonttools | PyPI (managed by uv, stored in uv's own cache) | ≈ 55 MB |
| Chromium (only if not installed) | playwright install chromium, stored in ~/.cache/ms-playwright/ | hundreds of MB |
| @excalidraw/excalidraw 0.18.1 and its dependencies (≈110 ES modules) | jsDelivr, pinned versions | ≈ 1.6 MB |
| svgo 4.1.0 browser build (SVG optimization, runs in Chromium) | jsDelivr | 0.8 MB |
| Fonts: Excalifont, Inter 5.3.0, Noto Sans SC 5.3.1 (sharded by unicode-range, downloads only needed shards) | jsDelivr | All ≈ 4.5 MB |
| draw.io viewer (only for browser export method) | viewer.diagrams.net | ≈ 4.7 MB |
Except for Python dependencies and Chromium, the cache is stored in ~/.cache/wisp-diagram/ (can be changed with WISP_DIAGRAM_CACHE). Use $W cache status to view, and $W cache clear to clear.
Offline usage: Run $W cache warm once on a connected network (downloads all frontend resources listed above, ≈ 11 MB), then set WISP_DIAGRAM_OFFLINE=1 to work completely offline; if the cache is missing files, it will report an error and suggest warming up, rather than silently going online. You can also use WISP_EXCALIDRAW_BUNDLE=<local JS> to specify your own Excalidraw build (must define window.__wisp as an IIFE, or export functions like exportToSvg as an ES module), and WISP_DRAWIO_VIEWER=<local file> to specify a local draw.io viewer.
draw.io export falls back automatically in the order: cli → docker → browser. new, build, and export all require Chromium (Excalidraw export and svgo optimization run in headless browser); lint, normalize, check do not.
1. Choose a Tool First
| Scenario | Choice | Source File |
|---|---|---|
| Flowcharts, sequence diagrams, state diagrams, class diagrams — structured diagrams, primarily node relationships | Mermaid (simplest, write directly into ```mermaid code blocks; Wisp site renders during build) | No separate file |
| Concept diagrams, intuitive illustrations, hand-drawn style, ML operator data flow | Excalidraw | name.excalidraw (JSON) |
| Precisely aligned model structure diagrams, layered architecture diagrams, need right-angle bend lines and multiple pages | draw.io | name.drawio (uncompressed XML) |
| Step-by-step demos, interactive | Not part of this skill (React component) | — |
Rule of thumb: if Mermaid can explain it clearly, don't draw a diagram; hand-drawn style should not exceed about 12 nodes; keep style consistent within the same article.
2. Workflow (5 Steps)
The following uses diagrams/attention as an example; adjust paths according to actual location.
① Determine Type (see table above). After selecting, read the corresponding reference: Excalidraw → reference/excalidraw-skeleton.md; draw.io → reference/drawio-cheatsheet.md; common model diagram patterns can be copied and modified from reference/ml-patterns.md. Layout principles are in reference/style-guide.md.
② Generate Source File
- Excalidraw: Write a skeleton JSON (position, size, label, semantic color names, arrow start/end IDs), then convert. Do NOT hand-write complete Excalidraw elements (fields like seed, boundElements, version are very error-prone).
The skeleton file can be deleted after conversion or kept as a generation script; afterward, the$W new diagrams/attention.skeleton.json -o diagrams/attention.excalidraw.excalidrawfile is the authoritative source. - draw.io: Write uncompressed mxGraph XML directly; use only the preset style strings from
reference/drawio-cheatsheet.md. If you receive a compressed.drawiofile, first decompress it with$W normalize diagrams/file.drawio. - Draw manually in an editor: Import
assets/wisp.excalidrawlib(Excalidraw: right-side Library → Open) orassets/wisp-drawio-library.xml(draw.io: File → Open Library from → Device), and configure the palette according toassets/drawio-colors.txt.
③ Lint (fix first) then Build
$W lint diagrams/attention.excalidraw --fix # Snap non-palette colors to nearest token; list ones that cannot be auto-fixed
$W lint diagrams/attention.excalidraw --strict # Pass only if no errors or warnings
$W build diagrams/attention.excalidraw # Generate diagrams/attention.wisp.svg
Common build arguments: --embed-fonts (embed font subsets, use when publishing to external platforms), --fix, --title T --desc D (override accessibility text, single file only), --drawio cli|docker|browser, --out-dir DIR, --strict. For multi-page draw.io files, pages from page 2 onward output as name.page-2.wisp.svg.
④ Self-Check: Export light and dark previews, view with a Reader tool
$W export diagrams/attention.wisp.svg --theme light -o /tmp/attention.light.png
$W export diagrams/attention.wisp.svg --theme dark -o /tmp/attention.dark.png
Check for: text overlap, arrows passing through nodes, labels covering arrowheads, unreadable text in dark mode, Chinese characters being clipped. If issues are found, fix the source file and return to step ③ to rebuild. Keep preview PNGs in a temporary directory; do not commit them.
⑤ Reference and Write alt Text: See "Using Output" below. Alt text must be meaningful (describe the conclusion the diagram is meant to convey, not "Figure 1").
3. Palette
Colors can only come from palette.json. When drawing, use only the light values (left column in the table below); the build step maps them to var(--wd-*) (dark values are switched automatically by the SVG). Do not manually pick dark values. Do not use any color outside the palette (lint will error; --fix snaps to the nearest token).
| Hue | Stroke (lines, text) light / dark | Fill (background) light / dark | Semantic Name |
|---|---|---|---|
blue | #1d4ed8 / #60a5fa | #dbeafe / #1b2740 | q, active |
amber | #b45309 / #fbbf24 | #fef3c7 / #38290c | k, highlight |
green | #15803d / #4ade80 | #dcfce7 / #122d1c | v, done |
rose | #be123c / #fb7185 | #ffe4e6 / #3a1620 | grad, error |
violet | #7c3aed / #a78bfa | #ede9fe / #272040 | weight |
teal | #0f766e / #2dd4bf | #ccfbf1 / #0f2c29 | activation |
orange | #c2410c / #fb923c | #ffedd5 / #3a2010 | bias |
slate | #475569 / #94a3b8 | #e2e8f0 / #222831 | mask, visited |
Base colors: body text fg #1d1d1f (Excalidraw default black #000000/#1e1e1e is also treated as fg), secondary muted #5c6067, surface surface #f4f4f2, border border #e2e2dd, canvas white #ffffff (build removes the background).
Semantic Colors (consistent across all articles and diagrams)
| Semantic Name | Meaning | Hue |
|---|---|---|
q | Query | blue |
k | Key | amber |
v | Value | green |
weight | Weight, parameter | violet |
activation | Activation, softmax | teal |
grad | Gradient | rose |
bias | Bias | orange |
mask | Mask, input | slate |
highlight | Highlight | amber |
visited | Visited | slate |
active | Current/in-progress | blue |
done | Completed | green |
error | Error, loss | rose |
In the skeleton, the color field can be written directly as a semantic name ("color": "q") or a hue name; in draw.io, look up the corresponding fill/stroke from the table by hue.
4. Hard Style Rules
| Item | Rule |
|---|---|
| Grid | All element x/y/width/height must be multiples of 8px |
| Font Size | Use only 12 (annotation) / 14 (label) / 18 (title); lint tolerance ±1 |
| Line Width | 1.5 (normal), 2.5 (emphasis, one or two places per diagram); thin line 1 only for dividers |
| Corner Radius | Rectangle/container corners: Excalidraw round, draw.io rounded=1;arcSize=8 |
| Fill | Solid fill, use the fill from the hue; no gradients |
| Shadow | None |
| Font | Excalidraw: Excalifont (Chinese fallback: Noto Sans SC, do not use 小赖体); draw.io: Inter |
| Hand-drawn | roughness: 1; draw.io: do not use sketch=1 |
| Background | Do not draw a background rectangle; keep it transparent |
| Source File | .excalidraw is JSON; .drawio is uncompressed XML; embedded images bloat the file (warning if >2MB) |
5. Self-Check Checklist
- lint
--strictpasses: no non-palette colors, no shadows/gradients, font sizes compliant, elements on 8px grid - Semantic colors are correct: Q blue, K amber, V green, weight violet, activation teal, gradient/loss rose, bias orange, mask/input slate
- One diagram only covers one topic; nodes ≤ 12 (hand-drawn)
- Lines do not pass through nodes or intersect; short lines (spacing < 96px) have no labels
- Chinese labels have ≤ 8–10 characters per line and are not clipped
- Has a title (text with
size: "title"/ draw.io page name) and one annotation - Light and dark preview PNGs have been viewed with a Reader tool
-
.wisp.svgis generated, and the source file was rebuilt after modification - Alt text is meaningful
6. Using Output
In a Wisp Site (optional)
The site's pnpm diagram build|check|lint [--fix], pnpm diagram new <skeleton.json> --build, pnpm diagram export <file> --png --theme dark are thin wrappers around this skill (they delegate to uv run skills/wisp-diagram/scripts/wisp_diagram.py with --root <repo> --manifest scripts/diagram/manifest.json for incremental builds).
Place source files alongside articles (in the diagrams/ directory); .wisp.svg files must also be committed. In MDX/Markdown, reference the source file using standard image syntax (the site will inline the corresponding .wisp.svg and provide a download link for the source file):


The alt text is written into the SVG's <title>, and the text in quotes becomes the figure caption; #page=<page name> selects a draw.io page. pnpm diagram check only checks content hashes and does not require a browser in CI (but does need uv).
Other projects can also use the same "project mode": $W build --root . --manifest diagrams.manifest.json only rebuilds source files that changed (scans <root>/src/content/**/diagrams/), and $W check --root . --manifest diagrams.manifest.json validates hashes in CI.
Elsewhere (GitHub README, other blogs, IDE preview)
Reference the generated *.wisp.svg directly: it includes its own <style>; when used as an <img>, it follows system light/dark via prefers-color-scheme. Renderers without CSS variable support will fall back to the light values in the attributes.

- Before publishing to external platforms, use
$W build <file> --embed-fontsto embed font subsets (usually a few tens of KB per diagram) to avoid font fallback. - Platforms like WeChat Official Accounts or Zhihu that do not support SVG:
$W export <file> --theme light --scale 2 -o out.png(use--transparentfor a transparent background; fonts are embedded by default). - Keep source files in the repository for later editing;
.excalidrawcan be opened at excalidraw.com,.drawioat app.diagrams.net.
7. Common Mistakes
| Mistake | How to Avoid |
|---|---|
| Hand-writing complete Excalidraw elements | Only write a skeleton; use $W new to convert |
| Used a color outside the palette in the editor (eyedropper, default color) | Use the asset library; run lint --fix to snap; if errors persist, edit the source file |
| Manually picked dark values while drawing | Only use light values; dark values are switched automatically by the SVG |
| Skeleton arrow passes through a third node | Arrows are straight lines on the center connection; adjust the layout, or switch to draw.io bend lines |
| Short line with a label, label covers arrowhead | Only add labels when node spacing ≥ 96px; otherwise, remove the label |
| Coordinates not on the 8px grid | Use only multiples of 8 for all numerical values |
.drawio committed in compressed format | Use $W normalize to decompress; uncheck "Compressed" under File → Properties |
& not escaped in draw.io label | When html=1, write &amp;, and write newlines as <br> |
| Chinese label too long, gets clipped | ≤ 8–10 characters per line, use \n / <br> to break lines, widen the node |
| Changed source file but forgot to rebuild | After modifying, rerun $W build (in the site, pnpm diagram check will catch hash mismatches) |
| Delivered without checking dark preview | Step ④ dark preview must be checked |
| Empty alt text ("Figure 1", "diagram") | Alt text should describe the conclusion the diagram illustrates |
| Used diagram tool to draw data charts | Line/bar/etc. data charts are not part of this skill |
8. Troubleshooting
uv: command not found: Install uv (curl -LsSf https://astral.sh/uv/install.sh | sh). Needs Python ≥ 3.11; uv will automatically download it if needed.Cannot start Chromium: Runuv run --with playwright==1.64.1 playwright install chromium(version must match the pinned playwright version in the script).Download failed/Offline mode: N files missing from cache: Run$W cache warmonce on a connected network; check cache location with$W cache status.- draw.io export fails: Run
$W probeto see which methods are available; force a method with--drawio browser|cli|docker. - To see errors in the browser: Set
WISP_DIAGRAM_DEBUG=1. - lint reports
unrecognised colour: The color is not a hex value (e.g., special values likergb()); manually change it to a palette light value.