docs(gallery): refresh canonical 38-type visual catalog (#119)

* docs(gallery): refresh canonical visual catalog

* docs: document screenshot renderer setup
This commit is contained in:
Cathryn Lavery
2026-08-19 18:47:20 -05:00
committed by GitHub
parent 4691a2f40c
commit e8b7d7ae62
50 changed files with 646 additions and 38 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Render the 38 canonical minimal-light examples and record their digests."""
from __future__ import annotations
import json
from playwright.sync_api import sync_playwright
from screenshot_catalog import (
MANIFEST,
ROOT,
SCREENSHOT_DIR,
canonical_slugs,
png_dimensions,
screenshot_path,
sha256,
source_path,
)
def main() -> int:
slugs = canonical_slugs()
if len(slugs) != 38 or len(slugs) != len(set(slugs)):
raise SystemExit(f"expected 38 unique canonical types; found {len(slugs)}")
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
entries: list[dict[str, object]] = []
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page(
viewport={"width": 1440, "height": 1000},
device_scale_factor=2,
)
for index, slug in enumerate(slugs, 1):
source = source_path(slug)
output = screenshot_path(slug)
if not source.is_file():
raise SystemExit(f"missing canonical example: {source.relative_to(ROOT)}")
page.goto(source.resolve().as_uri(), wait_until="networkidle")
page.evaluate("() => document.fonts.ready")
svg = page.locator("svg").first
if svg.count() != 1:
raise SystemExit(f"no first SVG found in {source.relative_to(ROOT)}")
svg.screenshot(path=str(output), omit_background=True)
width, height = png_dimensions(output)
entries.append(
{
"slug": slug,
"source": source.relative_to(ROOT).as_posix(),
"screenshot": output.relative_to(ROOT).as_posix(),
"source_sha256": sha256(source),
"screenshot_sha256": sha256(output),
"width": width,
"height": height,
}
)
print(f"[{index:02d}/38] {slug}: {width}x{height}")
browser.close()
payload = {
"schema_version": 1,
"renderer": {
"engine": "playwright-chromium",
"scale": 2.0,
"viewport": [1440, 1000],
"capture": "first-svg",
"font_gate": "document.fonts.ready",
},
"entries": entries,
}
MANIFEST.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {MANIFEST.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
"""Shared helpers for the canonical README screenshot catalog."""
from __future__ import annotations
import hashlib
import re
import struct
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SKILL = ROOT / "skills/diagram-design/SKILL.md"
ASSET_DIR = ROOT / "skills/diagram-design/assets"
SCREENSHOT_DIR = ROOT / "docs/screenshots"
MANIFEST = SCREENSHOT_DIR / "manifest.json"
def canonical_slugs() -> list[str]:
"""Return the visual-type order declared by SKILL.md's selection table."""
markdown = SKILL.read_text(encoding="utf-8")
start = markdown.find("### Visual-type guide")
end = markdown.find("Rules of thumb", start)
if start < 0 or end < 0:
raise ValueError("SKILL.md visual-type guide is missing")
return re.findall(
r"^\|[^\n]*\]\(references/type-([a-z0-9-]+)\.md\)\s*\|$",
markdown[start:end],
re.MULTILINE,
)
def source_path(slug: str) -> Path:
return ASSET_DIR / f"example-{slug}.html"
def screenshot_path(slug: str) -> Path:
return SCREENSHOT_DIR / f"{slug}.png"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def png_dimensions(path: Path) -> tuple[int, int]:
with path.open("rb") as handle:
header = handle.read(24)
if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n":
raise ValueError(f"not a PNG: {path}")
return struct.unpack(">II", header[16:24])
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Fail when a canonical example or committed screenshot drifts from its manifest."""
from __future__ import annotations
import json
import sys
from screenshot_catalog import (
MANIFEST,
ROOT,
canonical_slugs,
png_dimensions,
screenshot_path,
sha256,
source_path,
)
def main() -> int:
errors: list[str] = []
slugs = canonical_slugs()
if len(slugs) != 38 or len(slugs) != len(set(slugs)):
errors.append(f"expected 38 unique canonical types; found {len(slugs)}")
if not MANIFEST.is_file():
errors.append("docs/screenshots/manifest.json is missing")
payload: dict[str, object] = {}
else:
try:
payload = json.loads(MANIFEST.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
errors.append(f"cannot read screenshot manifest: {exc}")
payload = {}
if payload.get("schema_version") != 1:
errors.append("screenshot manifest schema_version must be 1")
renderer = payload.get("renderer")
expected_renderer = {
"engine": "playwright-chromium",
"scale": 2.0,
"viewport": [1440, 1000],
"capture": "first-svg",
"font_gate": "document.fonts.ready",
}
if renderer != expected_renderer:
errors.append("screenshot manifest must use the canonical 1440x1000, 2x SVG renderer")
entries = payload.get("entries", [])
if not isinstance(entries, list):
errors.append("screenshot manifest entries must be a list")
entries = []
manifest_slugs = [entry.get("slug") for entry in entries if isinstance(entry, dict)]
if manifest_slugs != slugs:
errors.append("screenshot manifest order does not match SKILL.md's canonical type order")
by_slug = {
entry.get("slug"): entry
for entry in entries
if isinstance(entry, dict) and isinstance(entry.get("slug"), str)
}
for slug in slugs:
source = source_path(slug)
screenshot = screenshot_path(slug)
entry = by_slug.get(slug)
if not source.is_file():
errors.append(f"missing canonical example: {source.relative_to(ROOT)}")
continue
if not screenshot.is_file():
errors.append(f"missing canonical screenshot: {screenshot.relative_to(ROOT)}")
continue
if not entry:
errors.append(f"manifest has no entry for {slug}")
continue
expected_source = source.relative_to(ROOT).as_posix()
expected_screenshot = screenshot.relative_to(ROOT).as_posix()
if entry.get("source") != expected_source:
errors.append(f"{slug}: manifest source must be {expected_source}")
if entry.get("screenshot") != expected_screenshot:
errors.append(f"{slug}: manifest screenshot must be {expected_screenshot}")
if entry.get("source_sha256") != sha256(source):
errors.append(f"{slug}: source changed; rerun scripts/render-canonical-screenshots.py")
if entry.get("screenshot_sha256") != sha256(screenshot):
errors.append(f"{slug}: screenshot changed without a matching manifest refresh")
try:
width, height = png_dimensions(screenshot)
except ValueError as exc:
errors.append(str(exc))
else:
if [entry.get("width"), entry.get("height")] != [width, height]:
errors.append(f"{slug}: manifest dimensions do not match the PNG")
if errors:
print("FAIL screenshot freshness")
for error in errors:
print(f" - {error}")
return 1
print("OK screenshot freshness: 38 canonical sources and PNG digests match")
return 0
if __name__ == "__main__":
sys.exit(main())