Adaptive themed diagrams

HCI concept pages and the Next.js component architecture page share one reproducible diagram pipeline. You edit a Mermaid (.mmd) source once. site-nextjs/scripts/build-diagrams.mjs renders it and writes SVG files that follow light and dark themes. You commit those generated files next to the source.

Why this exists

Hand-drawn SVGs drift from the product palette, stay light when the reader switches to dark mode, and are hard to review in Git. The pipeline instead:

  • keeps one canonical source per diagram under docs/modules/<module>/images/diagrams/<name>/

  • writes a themed-svg manifest (<name>.theme.json) with light and dark token presets in the ftn-diagram namespace

  • writes a standalone SVG (<name>.svg) whose colors are CSS custom properties with built-in light values and a prefers-color-scheme: dark block, so it adapts without JavaScript

  • writes a host SVG (<name>.host.svg), a sanitized variant that a page script can inline so the diagram follows the page’s own theme toggle

  • copies the standalone and host SVGs of published diagrams into site-nextjs/public/images/ for the Next.js demo pages

There is one SVG pair per diagram, not separate light and dark files: the same file carries both palettes.

Related HCI essays: Context Rails and Role lens UI.

Files per diagram

docs/modules/<module>/images/diagrams/<name>/
├── <name>.mmd              # canonical Mermaid source — edit this
├── <name>.theme.json       # generated manifest (do not hand-edit)
├── <name>.svg              # generated standalone adaptive SVG
└── <name>.host.svg         # generated host SVG for in-page upgrade

When a registry entry has publish: true, the generator also writes:

site-nextjs/public/images/<name>.svg
site-nextjs/public/images/<name>.host.svg

The four HCI diagrams that were migrated from hand-drawn SVGs also keep the original as <name>.legacy.fixed.svg in the same folder. The generator does not read or write that file.

Antora pages reference the generated standalone SVG with the themed-svg role:

image::diagrams/context-rails-overview/context-rails-overview.svg[alt="…",role=themed-svg,width=720]

Registered diagrams

The registry is the diagrams array in site-nextjs/scripts/build-diagrams.mjs.

Diagram Antora module Copied to site-nextjs/public/images/

context-rails-overview, context-rails-sieves

ROOT

yes (publish: true)

hci-role-lens-routing, hci-role-lens-before-after

ROOT

yes (publish: true)

react-component-architecture

nextjs

no (docs only)

Diagrams outside the pipeline

  • hci-role-lens-mockup is a UI mockup, not a Mermaid diagram. Its folder has no .mmd and it is not in the registry. Its .svg, .host.svg and .theme.json were derived once from hci-role-lens-mockup.legacy.fixed.svg and are maintained by hand, together with the copies in site-nextjs/public/images/. pnpm diagrams:check does not cover it.

  • The Git/Vercel topology drawing is an Excalidraw source with fixed exports in docs/modules/nextjs/images/: Git Structure - Vercel Free.excalidraw, Git Structure - Vercel Free.svg and Git Structure - Vercel Free - dark.svg. Move it to Mermaid if you want it themed.

Commands

Install once from the repository root, then run the diagram scripts from site-nextjs/:

pnpm install              # repository root
cd site-nextjs
pnpm diagrams:generate    # render every registered diagram and write all outputs
pnpm diagrams:check       # render in memory; exit 1 and print "stale: <path>" for any output that differs

diagrams:generate runs node ./scripts/build-diagrams.mjs, and diagrams:check runs the same script with --check. The script also accepts --migrate, a one-time helper that moved the original HCI sources and hand-drawn SVGs into the per-diagram folders. You should not need it again.

Regenerate whenever you change a .mmd file or the token, binding or registry tables in build-diagrams.mjs. Then commit the source and every generated file.

Where the check runs

  • pnpm build in site-nextjs/ runs pnpm brand:check && pnpm diagrams:check && next build. Use it locally before you push diagram changes.

  • Vercel builds with pnpm build:vercel (site-nextjs/vercel.json), which runs site-nextjs/vercel-build.mjs. That calls pnpm build:app (next build, behind convex deploy in production) and does not run diagrams:check. Stale diagrams do not fail a deploy, so run the check yourself.

  • The GitHub workflow .github/workflows/deploy-vercel.yml does not run it either.

How rendering works

For each registry entry the script:

  1. reads docs/modules/<module>/images/diagrams/<name>/<name>.mmd and stops with Missing canonical source if it is absent

  2. renders it with @mermaid-js/mermaid-cli (site-nextjs/node_modules/@mermaid-js/mermaid-cli/src/cli.js) to a raw SVG on a transparent background

  3. passes the raw SVG and the manifest to prepareThemedMermaidSvgDualOutput from @dev-centr/mermaid-svg-css-vars, which returns the standalone and host SVGs

  4. writes the manifest, standalone and host files, plus the public/images copies when publish is true

The Mermaid and Puppeteer settings are written to a temporary folder on every run. There is no committed config file to edit:

  • Mermaid config: htmlLabels: false (top level and flowchart), flowchart.curve: basis, securityLevel: strict, theme: default.

  • Puppeteer config: {"args": ["--no-sandbox", "--disable-setuid-sandbox"]}.

Browser for Puppeteer

Mermaid CLI drives a headless Chromium through Puppeteer. The script picks the browser in this order:

  1. PUPPETEER_EXECUTABLE_PATH, if set.

  2. On Windows, the first of C:\Program Files\Google\Chrome\Application\chrome.exe and C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe that exists.

  3. Otherwise, the browser that Puppeteer’s postinstall downloaded into its cache (~/.cache/puppeteer).

The download only happens when pnpm lets Puppeteer run its install script. Root pnpm-workspace.yaml allows that with puppeteer: true under allowBuilds.

Colors and class names

Every diagram uses the same manifest, defined in build-diagrams.mjs:

  • tokens: ten color tokens (color.surface.primary … color.surface.danger, color.text.primary, color.edge, color.edge.label, color.border.primary), each with a light and a dark value. They become custom properties such as --themed-svg-ftn-diagram-color-surface-primary.

  • Bindings map Mermaid output to those tokens. Background fills are bound through six class names: primary, secondary, accent, success, warning and danger. Node text, subgraph titles, borders, edges, edge labels and arrowheads are bound for every diagram.

To color a node, assign one of the six class names in the .mmd file (class myNode accent). Subgraphs work the same way once they have an id (subgraph stores [Stores], then class stores primary). Without a class, a subgraph keeps Mermaid’s light default background in dark mode. Keep a matching classDef line for each class so the diagram still looks right in plain Mermaid previews; the pipeline replaces those colors with the theme tokens. A class name outside the six keeps Mermaid’s hard-coded colors and will not change in dark mode.

Per-diagram Mermaid settings can go in a front-matter config: block at the top of the .mmd file. react-component-architecture.mmd uses one to raise flowchart.wrappingWidth so long file paths stay on one line.

Where diagrams are upgraded

The standalone SVG already adapts to the reader’s operating-system color scheme. Pages that have their own theme toggle swap it for the host SVG so the diagram follows the toggle instead:

  • Docs site (docs.foodtrucknerdz.com): the FoodTruckNerdz/docs supplemental UI script site/supplemental-ui/js/site-themed-svg.js marks img.themed-svg and .themed-svg img (what role=themed-svg produces) and loads the matching .host.svg beside each image.

  • Next.js app: site-nextjs/components/theme/themed-svg-boot.tsx (mounted in app/layout.tsx) calls defineThemedSvgElement() and upgradeThemedSvgImages(document) from @dev-centr/themed-svg/runtime on every route change. It upgrades img[data-themed-svg] elements. The demo pages /demo/role-lens and /demo/context-rails-unification use that attribute with the public/images/<name>.svg files.

  • In the app, site-nextjs/app/globals.css sets the --themed-svg-ftn-diagram-* values for the light (:root) and dark (.dark) site themes. Those values can differ from the manifest presets (for example accent fills tuned to the product palette), so an upgraded diagram in the app can look slightly different from the same standalone SVG.

Adding or changing a diagram

  1. Create docs/modules/<module>/images/diagrams/<name>/<name>.mmd.

  2. Add an entry to the diagrams array in site-nextjs/scripts/build-diagrams.mjs with name, module, title and description. Add publish: true only if the Next.js app needs a copy in public/images/. The title and description become the SVG’s accessible title and description.

  3. Color nodes and subgraphs with the six class names described above.

  4. Run pnpm diagrams:generate from site-nextjs/, then pnpm diagrams:check to confirm the outputs are current.

  5. Reference the generated SVG from the Antora page in that module: image::diagrams/<name>/<name>.svg[alt="…",role=themed-svg,width=720].

  6. Commit the .mmd source and all generated files.

Packages

The pinned versions are in site-nextjs/package.json (devDependencies):

  • @mermaid-js/mermaid-cli: headless Mermaid rendering through Puppeteer

  • @dev-centr/mermaid-svg-css-vars: turns Mermaid SVG plus a manifest into the standalone and host outputs

  • @dev-centr/themed-svg: browser runtime that upgrades marked images to host SVGs

Manifest schema: schema/themed-svg-manifest-v1.schema.json in the @dev-centr/themed-svg package (source on GitHub).