From 9ea66c03a3f26d207a0d643f51b3a86c88f1ee8e Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Sat, 23 May 2026 16:48:56 -0600 Subject: [PATCH 1/2] chore(excalidraw-skill): encode text-overlap + GitHub-SVG lessons; add rebuild script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recent dev-loop diagram exposed two failure modes the skill didn't guide against. Both got fixed twice (PR #20) instead of being prevented up front. This commit encodes the lessons so future diagrams ship right the first time. SKILL.md additions: Text Width Validation (Prevent Overlap) — new subsection in Layout Principles. Quantifies the issue: SVG doesn't wrap; 150-char line at 14px = ~1260px wide and bleeds into adjacent columns if column width is ~840px. Includes a per-character-width table and the four ways out (shorten, multi-element wrap, single-column restructure, foreignObject). Cites the caught-in-the-wild example from PR #20's Gaps section. SVG Output for GitHub Embedding — new top-level section. Three rules for hand-written SVG to render correctly on GitHub: 1. explicit width + height (without it, GitHub downscales to ~50% and text becomes illegible) 2. white background as first child (without it, transparent background = invisible text on GitHub dark theme) 3. single-column for any text >80 chars (SVG has no auto-wrap) Plus guidance on when the .excalidraw and .svg drift (document it inline). Quality Checklist — bumped from 27 to 31 items, adding four explicit checks for the GitHub-SVG scenario. render_excalidraw.py changes: Path A (Playwright render) now writes a sibling .svg alongside the .png. New `_make_github_friendly()` helper post-processes Excalidraw's exportToSvg output to add the width/height attributes and white background rect — so Playwright-rendered SVGs land with the same GitHub-friendly shape that hand-written ones now require. Docstring + main() print line updated. .claude/scripts/rebuild-diagrams.sh (new): Walks docs/*.excalidraw, regenerates .png + .svg outputs for any source newer than its outputs. Skips up-to-date files unless --force. Single-file mode for targeted rebuilds. CI-friendly exit code on failure. Co-Authored-By: Claude Opus 4.7 --- .claude/scripts/rebuild-diagrams.sh | 91 +++++++++++++++++ .claude/skills/excalidraw-diagram/SKILL.md | 98 ++++++++++++++++++- .../references/render_excalidraw.py | 62 +++++++++++- 3 files changed, 245 insertions(+), 6 deletions(-) create mode 100755 .claude/scripts/rebuild-diagrams.sh diff --git a/.claude/scripts/rebuild-diagrams.sh b/.claude/scripts/rebuild-diagrams.sh new file mode 100755 index 00000000..49997f50 --- /dev/null +++ b/.claude/scripts/rebuild-diagrams.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Rebuild diagram .png + .svg outputs from .excalidraw sources. +# +# Walks docs/*.excalidraw, regenerates the sibling .png + .svg for each file +# whose source is newer than its outputs (or whose outputs don't exist). +# Skips files that are already up-to-date. +# +# Usage: +# .claude/scripts/rebuild-diagrams.sh # incremental (only stale) +# .claude/scripts/rebuild-diagrams.sh --force # force-rebuild everything +# .claude/scripts/rebuild-diagrams.sh path.excalidraw # one file +# +# Requires the excalidraw-diagram skill's Playwright setup. If you've never +# rendered before: +# cd .claude/skills/excalidraw-diagram/references +# uv sync +# uv run playwright install chromium + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +RENDER_SCRIPT="$REPO_ROOT/.claude/skills/excalidraw-diagram/references/render_excalidraw.py" +DOCS_DIR="$REPO_ROOT/docs" + +if [ ! -f "$RENDER_SCRIPT" ]; then + echo "ERROR: render script not found at $RENDER_SCRIPT" >&2 + exit 1 +fi + +FORCE=0 +TARGETS=() +for arg in "$@"; do + case "$arg" in + --force|-f) FORCE=1 ;; + *.excalidraw) TARGETS+=("$arg") ;; + *) echo "ignoring unknown arg: $arg" >&2 ;; + esac +done + +# If no explicit targets, walk every .excalidraw under docs/. +if [ ${#TARGETS[@]} -eq 0 ]; then + while IFS= read -r -d '' f; do + TARGETS+=("$f") + done < <(find "$DOCS_DIR" -maxdepth 2 -name "*.excalidraw" -type f -print0 2>/dev/null) +fi + +if [ ${#TARGETS[@]} -eq 0 ]; then + echo "No .excalidraw files found under $DOCS_DIR/" + exit 0 +fi + +# A file is stale if either output (.png or .svg) is older than the source, or +# either output doesn't exist. +is_stale() { + local src="$1" + local base="${src%.excalidraw}" + local png="${base}.png" + local svg="${base}.svg" + [ ! -f "$png" ] && return 0 + [ ! -f "$svg" ] && return 0 + [ "$src" -nt "$png" ] && return 0 + [ "$src" -nt "$svg" ] && return 0 + return 1 +} + +rebuilt=0 +skipped=0 +failed=0 +cd "$REPO_ROOT/.claude/skills/excalidraw-diagram/references" +for src in "${TARGETS[@]}"; do + rel="${src#$REPO_ROOT/}" + if [ "$FORCE" -eq 0 ] && ! is_stale "$src"; then + echo " [skip] $rel (up-to-date)" + skipped=$((skipped + 1)) + continue + fi + echo " [build] $rel" + if uv run python "$RENDER_SCRIPT" "$src" >/dev/null 2>&1; then + rebuilt=$((rebuilt + 1)) + else + echo " FAILED — try running the command directly to see the error:" + echo " uv run python $RENDER_SCRIPT $src" + failed=$((failed + 1)) + fi +done + +echo +echo "Rebuilt: $rebuilt Skipped: $skipped Failed: $failed" + +# Exit non-zero if anything failed so this script is CI-friendly. +[ "$failed" -eq 0 ] diff --git a/.claude/skills/excalidraw-diagram/SKILL.md b/.claude/skills/excalidraw-diagram/SKILL.md index 93a9bce5..a1dbda55 100644 --- a/.claude/skills/excalidraw-diagram/SKILL.md +++ b/.claude/skills/excalidraw-diagram/SKILL.md @@ -404,9 +404,36 @@ Guide the eye: typically left→right or top→bottom for sequences, radial for ### Connections Required Position alone doesn't show relationships. If A relates to B, there must be an arrow. ---- +### Text Width Validation (Prevent Overlap) + +**The #1 cause of unreadable diagrams is text rendered wider than its container.** SVG `` does NOT wrap automatically — long strings spill into adjacent elements and produce overlapping garbage. This is especially common in multi-column layouts where each cell's available width is narrower than authors anticipate. + +**Compute width before placing.** For monospace fonts at typical sizes: + +| Font size | Approx. px per character | +|-----------|--------------------------| +| 12px | ~7.2 | +| 14px | ~8.4 | +| 16px | ~9.6 | +| 18px | ~10.8 | +| 22px | ~13.2 | + +So a 150-character explanatory line at 14px is ≈ **1260 px wide**. If your two-column layout gives each column only 840 px, that line overflows by ~420 px directly into the adjacent column's title. -## Text Rules +**For every long text element, before committing it to JSON or SVG:** + +1. Count the string length (characters). +2. Multiply by the per-character width for the chosen font size. +3. Compare against the available container width. +4. If it overflows, pick one: + - **Shorten the string** until it fits with ~10% margin. + - **Break the text into multiple `` elements** at explicit line breaks (each element = one wrapped line at a stepped `y`). + - **Restructure to a wider container** — collapse a 2-column layout into 1 column if all the long content sits in one section. + - **Use `` with HTML `
`** so the browser handles wrapping. Adds complexity, but works. + +**Multi-column layout rule of thumb:** only use multi-column when *every* text element in a column is ≤ ~80 characters at the chosen font. Tool names, short labels, and short notes are fine. Paragraph-length explanations belong in a single column. + +**Caught-in-the-wild example.** A "Gaps" section put 10 items in two 840-px-wide columns. Each gap had a one-line "Fix" description averaging 140 characters at 14px (≈ 1175 px wide). Result: left-column fix lines streamed into right-column titles, producing visually garbled text on GitHub. Fix was restructuring to a single full-width column — everything fit on one line each with 1690 px of room. **The cost of computing widths up front is one minute; the cost of fixing it after the fact is reflow + re-render + screenshot review.** **CRITICAL**: The JSON `text` property contains ONLY readable words. @@ -444,6 +471,63 @@ See `references/element-templates.md` for copy-paste JSON templates for each ele --- +## SVG Output for GitHub Embedding + +When a diagram is destined for GitHub markdown (README, `docs/*.md`), the `.excalidraw` source alone isn't enough — you need a sibling `.svg` because GitHub renders SVG inline in markdown views. PNG is fine for screenshots, but SVG is what scales/zooms in the browser without quality loss. Two paths to producing the SVG: + +### Path A — Render via Playwright (preferred) + +The `references/render_excalidraw.py` script writes both `.png` and `.svg` output side-by-side: + +```bash +cd .claude/skills/excalidraw-diagram/references +uv run python render_excalidraw.py docs/diagram.excalidraw +# produces docs/diagram.png AND docs/diagram.svg +``` + +Path A's SVG output goes through Excalidraw's own `exportToSvg()`, so layout/fonts/colors are guaranteed to match the source. Use this whenever it works. + +### Path B — Hand-written SVG (fallback when Playwright fails) + +When the render script can't run (`esm.sh` module-load timeout on slow networks, no Playwright dependency available, etc.), hand-write the SVG. **GitHub renders SVG with three failure modes that you must counter:** + +**1. Explicit `width` and `height` attributes are mandatory.** Without them, GitHub downscales the SVG to fit the markdown column width (~900 px on desktop). An 1800-px-wide design becomes 50% scale, and 14 px text becomes 7 px on screen — unreadable. Fix: +```xml + +``` +Trade-off: the SVG renders at natural size with a horizontal scroll bar instead of fitting the column. Worth it — text readability beats single-screen viewing. + +**2. Explicit white background rect is mandatory.** Without it, the SVG renders with a transparent background. On GitHub's dark theme, text colored for white background (typical `#374151`, `#64748b`) becomes nearly invisible. Fix — put this as the first child of ``: +```xml + +``` + +**3. Single-column for any text >80 chars.** SVG `` doesn't wrap. See the **Text Width Validation** section earlier — multi-column layouts only work for short labels. If your content has paragraph-length lines, stack vertically. + +**4. Font sizes in hand-written SVG should be 16-22 px range** (title 22-36 px). Below 14 px tends to be illegible at GitHub's render size even with explicit `width`/`height`. + +### When the `.excalidraw` and `.svg` drift + +If the `.excalidraw` source is one design and the `.svg` is a different, hand-written design (because the render kept failing while you needed to ship), **document the drift inline in the doc that references them**, e.g.: + +> Source: `dev-loop.excalidraw` is the highway skeleton; `dev-loop.svg` is the full hand-written diagram (Playwright was unavailable when this was authored). + +Don't pretend they match when they don't — that becomes a future debugging tax. + +### Rebuilding diagrams in bulk + +To keep all sibling `.png`/`.svg` outputs in sync with their `.excalidraw` sources, use the rebuild script: + +```bash +.claude/scripts/rebuild-diagrams.sh # incremental (only regenerates stale outputs) +.claude/scripts/rebuild-diagrams.sh --force # rebuild every diagram regardless of mtime +.claude/scripts/rebuild-diagrams.sh docs/specific-diagram.excalidraw # single file +``` + +The script walks `docs/*.excalidraw`, checks each source's mtime against its `.png` and `.svg` outputs, and only re-renders the stale ones. Useful after editing `.excalidraw` files in the Excalidraw VS Code extension (which updates the source but not the sibling outputs). + +--- + ## Render & Validate (MANDATORY) You cannot judge a diagram from JSON alone. After generating or editing the Excalidraw JSON, you MUST render it to PNG, view the image, and fix what you see — in a loop until it's right. This is a core part of the workflow, not a final check. @@ -544,9 +628,15 @@ uv run playwright install chromium ### Visual Validation (Render Required) 21. **Rendered to PNG**: Diagram has been rendered and visually inspected -22. **No text overflow**: All text fits within its container +22. **No text overflow**: All text fits within its container — width computed up front per "Text Width Validation" 23. **No overlapping elements**: Shapes and text don't overlap unintentionally 24. **Even spacing**: Similar elements have consistent spacing 25. **Arrows land correctly**: Arrows connect to intended elements without crossing others 26. **Readable at export size**: Text is legible in the rendered PNG -27. **Balanced composition**: No large empty voids or overcrowded regions \ No newline at end of file +27. **Balanced composition**: No large empty voids or overcrowded regions + +### GitHub SVG (if the diagram is referenced from markdown) +28. **Sibling `.svg` exists**: Markdown that references the `.excalidraw` also has a `.svg` (either Playwright-rendered or hand-written) — GitHub renders SVG inline in markdown views +29. **Explicit `width` and `height` attributes**: SVG has `width="X" height="Y"` matching the viewBox so GitHub doesn't downscale text below readable size +30. **Explicit white background rect**: First child of `` is `` so text is readable on GitHub's dark theme +31. **Fonts ≥ 14 px in body text, ≥ 16 px preferred**: smaller becomes illegible at GitHub's render \ No newline at end of file diff --git a/.claude/skills/excalidraw-diagram/references/render_excalidraw.py b/.claude/skills/excalidraw-diagram/references/render_excalidraw.py index e7f36fd7..f6f561f0 100644 --- a/.claude/skills/excalidraw-diagram/references/render_excalidraw.py +++ b/.claude/skills/excalidraw-diagram/references/render_excalidraw.py @@ -1,4 +1,10 @@ -"""Render Excalidraw JSON to PNG using Playwright + headless Chromium. +"""Render Excalidraw JSON to PNG (and a GitHub-ready SVG) using Playwright + headless Chromium. + +For each input .excalidraw file, writes: + - .png — the screenshot, for embedding via Markdown or direct viewing + - .svg — the same diagram as SVG markup, post-processed for GitHub rendering + (explicit width/height attrs + white background rect — see SKILL.md + "SVG Output for GitHub Embedding") Usage: cd .claude/skills/excalidraw-diagram/references @@ -156,7 +162,7 @@ def render( # Wait for render completion signal page.wait_for_function("window.__renderComplete === true", timeout=15000) - # Screenshot the SVG element + # Screenshot the SVG element to PNG svg_el = page.query_selector("#root svg") if svg_el is None: print("ERROR: No SVG element found after render.", file=sys.stderr) @@ -164,11 +170,60 @@ def render( sys.exit(1) svg_el.screenshot(path=str(output_path)) + + # Also save the SVG markup as a sibling file. GitHub renders SVG inline in + # markdown views (PNG must be embedded with ; SVG can also be linked). + # Post-process to add the GitHub-friendly attributes documented in + # SKILL.md "SVG Output for GitHub Embedding": + # 1. explicit width + height (so GitHub doesn't downscale text below readable) + # 2. white background rect (so text is readable on dark theme) + svg_markup = page.evaluate("document.querySelector('#root svg').outerHTML") + svg_output_path = output_path.with_suffix(".svg") + svg_markup = _make_github_friendly(svg_markup) + svg_output_path.write_text(svg_markup, encoding="utf-8") + browser.close() return output_path +def _make_github_friendly(svg_markup: str) -> str: + """Post-process Excalidraw's exportToSvg output so it renders well on GitHub. + + Two adjustments (see SKILL.md "SVG Output for GitHub Embedding"): + 1. Ensure the root has explicit width/height attributes matching the + viewBox — without them, GitHub downscales to fit the markdown column and + text becomes illegible. + 2. Insert a white as the first child of so text colored for a + light background stays readable on GitHub's dark theme. + """ + import re + + # Parse the root tag attributes. + match = re.match(r"]*)>", svg_markup) + if not match: + return svg_markup # malformed; leave alone + attrs = match.group(1) + + # Extract viewBox dimensions. Excalidraw always emits a viewBox. + vb_match = re.search(r'viewBox="\s*([\d.\-]+)\s+([\d.\-]+)\s+([\d.\-]+)\s+([\d.\-]+)"', attrs) + if vb_match: + _, _, vb_w, vb_h = vb_match.groups() + # Add width/height if missing. + if 'width=' not in attrs: + attrs += f' width="{vb_w}"' + if 'height=' not in attrs: + attrs += f' height="{vb_h}"' + # Build the white bg rect using viewBox dimensions. + bg_rect = f'' + else: + bg_rect = '' + + # Reconstruct: open tag with merged attrs, then inject bg rect as first child. + new_open = f"{bg_rect}" + return svg_markup.replace(match.group(0), new_open, 1) + + def main() -> None: parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG") parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file") @@ -182,7 +237,10 @@ def main() -> None: sys.exit(1) png_path = render(args.input, args.output, args.scale, args.width) + svg_path = png_path.with_suffix(".svg") print(str(png_path)) + if svg_path.exists(): + print(str(svg_path)) if __name__ == "__main__": From b91560d0bf90af1cb377ed5a84b6e86aa68a58f3 Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Sat, 23 May 2026 17:20:34 -0600 Subject: [PATCH 2/2] chore(excalidraw-skill): address CodeRabbit findings on PR #21 (8/8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All eight CodeRabbit findings on the initial commit were Quick-Win / Nit / Minor severity — addressing each: rebuild-diagrams.sh (4): 1. Add `uv` PATH check up front (was discovered at first render attempt, hiding the real error in a redirect) 2. Drop `-maxdepth 2` from the find — discovers diagrams at any depth under docs/ now (e.g., docs/a/b/c/diagram.excalidraw) 3. Quote $REPO_ROOT in parameter expansion: `${src#"$REPO_ROOT"/}` — shellcheck SC2295, prevents accidental glob interpretation 4. Capture render output to a temp file instead of `>/dev/null 2>&1` — on failure, print the last 20 lines of the actual error so the user doesn't need to re-run manually to see what went wrong render_excalidraw.py (1): 5. Guard `svg_markup` non-empty before writing the .svg sibling. Earlier query_selector already proved the element exists, so this is defensive — warn-and-continue on the unexpected empty-outerHTML case rather than silently writing a broken SVG. SKILL.md (3): 6. Add (PR #20) citation to the "Caught-in-the-wild example" so the reference is concrete and grep-able 7. MD031 blank-lines-around-fences — added blank lines before/after the three ```xml fences in "SVG Output for GitHub Embedding" 8. Restart ordered-list numbering at 1 in every Quality Checklist subsection (was continuous 1-31 across all subsections — confused some markdown parsers). Plus MD047 trailing newline on the file. Not addressed: IDE's separate SonarLint warning about render() function having cognitive complexity 19/15 — pre-existing on the function, not flagged by CodeRabbit, and refactoring is scope creep. Co-Authored-By: Claude Opus 4.7 --- .claude/scripts/rebuild-diagrams.sh | 30 +++++++-- .claude/skills/excalidraw-diagram/SKILL.md | 63 +++++++++++-------- .../references/render_excalidraw.py | 12 +++- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/.claude/scripts/rebuild-diagrams.sh b/.claude/scripts/rebuild-diagrams.sh index 49997f50..05d46050 100755 --- a/.claude/scripts/rebuild-diagrams.sh +++ b/.claude/scripts/rebuild-diagrams.sh @@ -27,6 +27,15 @@ if [ ! -f "$RENDER_SCRIPT" ]; then exit 1 fi +# Fail fast if `uv` isn't on PATH. The render script invokes `uv run python ...` +# at line ~78; without `uv` installed, the first render attempt fails with a +# cryptic "command not found" inside a `>` redirect, hiding the real cause. +if ! command -v uv >/dev/null 2>&1; then + echo "ERROR: 'uv' command not found in PATH" >&2 + echo "Install: https://github.com/astral-sh/uv" >&2 + exit 1 +fi + FORCE=0 TARGETS=() for arg in "$@"; do @@ -37,11 +46,12 @@ for arg in "$@"; do esac done -# If no explicit targets, walk every .excalidraw under docs/. +# If no explicit targets, walk every .excalidraw under docs/ (recursively — no +# maxdepth, so diagrams nested in docs/subdir/sub/... are also discovered). if [ ${#TARGETS[@]} -eq 0 ]; then while IFS= read -r -d '' f; do TARGETS+=("$f") - done < <(find "$DOCS_DIR" -maxdepth 2 -name "*.excalidraw" -type f -print0 2>/dev/null) + done < <(find "$DOCS_DIR" -name "*.excalidraw" -type f -print0 2>/dev/null) fi if [ ${#TARGETS[@]} -eq 0 ]; then @@ -68,18 +78,26 @@ skipped=0 failed=0 cd "$REPO_ROOT/.claude/skills/excalidraw-diagram/references" for src in "${TARGETS[@]}"; do - rel="${src#$REPO_ROOT/}" + # Quote $REPO_ROOT in the parameter expansion so shellcheck SC2295 is happy + # and the substring strip doesn't accidentally glob-interpret the prefix. + rel="${src#"$REPO_ROOT"/}" if [ "$FORCE" -eq 0 ] && ! is_stale "$src"; then echo " [skip] $rel (up-to-date)" skipped=$((skipped + 1)) continue fi echo " [build] $rel" - if uv run python "$RENDER_SCRIPT" "$src" >/dev/null 2>&1; then + # Capture render output to a temp file so we can show it on failure. + # On success the file is discarded; on failure we print it so the user + # sees the actual error instead of having to re-run the command manually. + err_file=$(mktemp) + if uv run python "$RENDER_SCRIPT" "$src" >"$err_file" 2>&1; then rebuilt=$((rebuilt + 1)) + rm -f "$err_file" else - echo " FAILED — try running the command directly to see the error:" - echo " uv run python $RENDER_SCRIPT $src" + echo " FAILED — render output:" + sed 's/^/ /' "$err_file" | tail -20 + rm -f "$err_file" failed=$((failed + 1)) fi done diff --git a/.claude/skills/excalidraw-diagram/SKILL.md b/.claude/skills/excalidraw-diagram/SKILL.md index a1dbda55..70f4d269 100644 --- a/.claude/skills/excalidraw-diagram/SKILL.md +++ b/.claude/skills/excalidraw-diagram/SKILL.md @@ -433,7 +433,7 @@ So a 150-character explanatory line at 14px is ≈ **1260 px wide**. If your two **Multi-column layout rule of thumb:** only use multi-column when *every* text element in a column is ≤ ~80 characters at the chosen font. Tool names, short labels, and short notes are fine. Paragraph-length explanations belong in a single column. -**Caught-in-the-wild example.** A "Gaps" section put 10 items in two 840-px-wide columns. Each gap had a one-line "Fix" description averaging 140 characters at 14px (≈ 1175 px wide). Result: left-column fix lines streamed into right-column titles, producing visually garbled text on GitHub. Fix was restructuring to a single full-width column — everything fit on one line each with 1690 px of room. **The cost of computing widths up front is one minute; the cost of fixing it after the fact is reflow + re-render + screenshot review.** +**Caught-in-the-wild example (PR #20).** A "Gaps" section put 10 items in two 840-px-wide columns. Each gap had a one-line "Fix" description averaging 140 characters at 14px (≈ 1175 px wide). Result: left-column fix lines streamed into right-column titles, producing visually garbled text on GitHub. Fix was restructuring to a single full-width column — everything fit on one line each with 1690 px of room. **The cost of computing widths up front is one minute; the cost of fixing it after the fact is reflow + re-render + screenshot review.** **CRITICAL**: The JSON `text` property contains ONLY readable words. @@ -492,12 +492,15 @@ Path A's SVG output goes through Excalidraw's own `exportToSvg()`, so layout/fon When the render script can't run (`esm.sh` module-load timeout on slow networks, no Playwright dependency available, etc.), hand-write the SVG. **GitHub renders SVG with three failure modes that you must counter:** **1. Explicit `width` and `height` attributes are mandatory.** Without them, GitHub downscales the SVG to fit the markdown column width (~900 px on desktop). An 1800-px-wide design becomes 50% scale, and 14 px text becomes 7 px on screen — unreadable. Fix: + ```xml ``` + Trade-off: the SVG renders at natural size with a horizontal scroll bar instead of fitting the column. Worth it — text readability beats single-screen viewing. **2. Explicit white background rect is mandatory.** Without it, the SVG renders with a transparent background. On GitHub's dark theme, text colored for white background (typical `#374151`, `#64748b`) becomes nearly invisible. Fix — put this as the first child of ``: + ```xml ``` @@ -604,39 +607,45 @@ uv run playwright install chromium 5. **Educational value**: Could someone learn something concrete from this? ### Conceptual -6. **Isomorphism**: Does each visual structure mirror its concept's behavior? -7. **Argument**: Does the diagram SHOW something text alone couldn't? -8. **Variety**: Does each major concept use a different visual pattern? -9. **No uniform containers**: Avoided card grids and equal boxes? + +1. **Isomorphism**: Does each visual structure mirror its concept's behavior? +2. **Argument**: Does the diagram SHOW something text alone couldn't? +3. **Variety**: Does each major concept use a different visual pattern? +4. **No uniform containers**: Avoided card grids and equal boxes? ### Container Discipline -10. **Minimal containers**: Could any boxed element work as free-floating text instead? -11. **Lines as structure**: Are tree/timeline patterns using lines + text rather than boxes? -12. **Typography hierarchy**: Are font size and color creating visual hierarchy (reducing need for boxes)? + +1. **Minimal containers**: Could any boxed element work as free-floating text instead? +2. **Lines as structure**: Are tree/timeline patterns using lines + text rather than boxes? +3. **Typography hierarchy**: Are font size and color creating visual hierarchy (reducing need for boxes)? ### Structural -13. **Connections**: Every relationship has an arrow or line -14. **Flow**: Clear visual path for the eye to follow -15. **Hierarchy**: Important elements are larger/more isolated + +1. **Connections**: Every relationship has an arrow or line +2. **Flow**: Clear visual path for the eye to follow +3. **Hierarchy**: Important elements are larger/more isolated ### Technical -16. **Text clean**: `text` contains only readable words -17. **Font**: `fontFamily: 3` -18. **Roughness**: `roughness: 0` for clean/modern (unless hand-drawn style requested) -19. **Opacity**: `opacity: 100` for all elements (no transparency) -20. **Container ratio**: <30% of text elements should be inside containers + +1. **Text clean**: `text` contains only readable words +2. **Font**: `fontFamily: 3` +3. **Roughness**: `roughness: 0` for clean/modern (unless hand-drawn style requested) +4. **Opacity**: `opacity: 100` for all elements (no transparency) +5. **Container ratio**: <30% of text elements should be inside containers ### Visual Validation (Render Required) -21. **Rendered to PNG**: Diagram has been rendered and visually inspected -22. **No text overflow**: All text fits within its container — width computed up front per "Text Width Validation" -23. **No overlapping elements**: Shapes and text don't overlap unintentionally -24. **Even spacing**: Similar elements have consistent spacing -25. **Arrows land correctly**: Arrows connect to intended elements without crossing others -26. **Readable at export size**: Text is legible in the rendered PNG -27. **Balanced composition**: No large empty voids or overcrowded regions + +1. **Rendered to PNG**: Diagram has been rendered and visually inspected +2. **No text overflow**: All text fits within its container — width computed up front per "Text Width Validation" +3. **No overlapping elements**: Shapes and text don't overlap unintentionally +4. **Even spacing**: Similar elements have consistent spacing +5. **Arrows land correctly**: Arrows connect to intended elements without crossing others +6. **Readable at export size**: Text is legible in the rendered PNG +7. **Balanced composition**: No large empty voids or overcrowded regions ### GitHub SVG (if the diagram is referenced from markdown) -28. **Sibling `.svg` exists**: Markdown that references the `.excalidraw` also has a `.svg` (either Playwright-rendered or hand-written) — GitHub renders SVG inline in markdown views -29. **Explicit `width` and `height` attributes**: SVG has `width="X" height="Y"` matching the viewBox so GitHub doesn't downscale text below readable size -30. **Explicit white background rect**: First child of `` is `` so text is readable on GitHub's dark theme -31. **Fonts ≥ 14 px in body text, ≥ 16 px preferred**: smaller becomes illegible at GitHub's render \ No newline at end of file + +1. **Sibling `.svg` exists**: Markdown that references the `.excalidraw` also has a `.svg` (either Playwright-rendered or hand-written) — GitHub renders SVG inline in markdown views +2. **Explicit `width` and `height` attributes**: SVG has `width="X" height="Y"` matching the viewBox so GitHub doesn't downscale text below readable size +3. **Explicit white background rect**: First child of `` is `` so text is readable on GitHub's dark theme +4. **Fonts ≥ 14 px in body text, ≥ 16 px preferred**: smaller becomes illegible at GitHub's render diff --git a/.claude/skills/excalidraw-diagram/references/render_excalidraw.py b/.claude/skills/excalidraw-diagram/references/render_excalidraw.py index f6f561f0..9c0a4443 100644 --- a/.claude/skills/excalidraw-diagram/references/render_excalidraw.py +++ b/.claude/skills/excalidraw-diagram/references/render_excalidraw.py @@ -178,9 +178,15 @@ def render( # 1. explicit width + height (so GitHub doesn't downscale text below readable) # 2. white background rect (so text is readable on dark theme) svg_markup = page.evaluate("document.querySelector('#root svg').outerHTML") - svg_output_path = output_path.with_suffix(".svg") - svg_markup = _make_github_friendly(svg_markup) - svg_output_path.write_text(svg_markup, encoding="utf-8") + if not svg_markup or not svg_markup.strip(): + # Earlier `query_selector` already proved the SVG element exists, so + # an empty outerHTML here would be unexpected — warn loudly but don't + # fail the run (PNG was still written above). + print("WARNING: outerHTML was empty; .svg sibling not written", file=sys.stderr) + else: + svg_output_path = output_path.with_suffix(".svg") + svg_markup = _make_github_friendly(svg_markup) + svg_output_path.write_text(svg_markup, encoding="utf-8") browser.close()