@brandon_m_behring/book-scaffold-astro 4.29.0 → 4.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -1
- package/bin/book-scaffold.mjs +1 -1
- package/components/Figure.astro +7 -6
- package/package.json +5 -4
- package/recipes/03-asset-pipelines.md +12 -4
- package/recipes/09-validation.md +21 -2
- package/recipes/16-tikz-figures.md +13 -2
- package/recipes/24-figure-authoring-standard.md +241 -0
- package/recipes/README.md +1 -0
- package/scripts/authored-links.mjs +328 -0
- package/scripts/build-figures.mjs +13 -10
- package/scripts/render-notebooks.mjs +2 -1
- package/scripts/resolve-book-config.mjs +16 -2
- package/scripts/sync-figure-tokens.mjs +44 -0
- package/scripts/validate.mjs +89 -15
- package/src/lib/figure-palette.mjs +162 -0
- package/src/lib/figure.mjs +113 -37
- package/styles/tokens.css +73 -9
package/CLAUDE.md
CHANGED
|
@@ -201,6 +201,7 @@ the full precedence and customization contract.
|
|
|
201
201
|
- Literal `<Theorem n>` values that disagree with `labels.json` (dynamic expressions and `label=` overrides are skipped)
|
|
202
202
|
- Missing `<Figure src>` files under `public/`
|
|
203
203
|
- Internal markdown links that don't resolve
|
|
204
|
+
- Authored root-absolute Markdown/HTML/JSX `href` or `src` targets that escape a configured non-root Astro `base` (#190). Structurally parsed and decoded literal targets fail with file/line context; root-base books, external/protocol-relative URLs, fragments, dynamic JSX expressions, and already base-prefixed targets are unaffected. `rel="external"` does not change URL resolution or opt out. Validation never rewrites prose.
|
|
204
205
|
- Study-guide questions (v4.17.0, #112) — a question whose frontmatter `domain` isn't in `examDomains`, and duplicate question `id`s
|
|
205
206
|
|
|
206
207
|
Missing `src/data/labels.json` or `references.json` self-heals before checks by running the corresponding package build script; an existing artifact is never rewritten implicitly.
|
|
@@ -228,9 +229,11 @@ See `recipes/09-validation.md` to extend.
|
|
|
228
229
|
2. `npm run build:figures` produces `public/figures/<topic>/<name>.svg`.
|
|
229
230
|
3. Reference: `<Figure src="/figures/<topic>/<name>.svg" caption="..." alt="..." id="..." />`.
|
|
230
231
|
|
|
231
|
-
**Accessibility + dark mode (v4.11.0, #84).** `build:figures` rewrites every generated SVG so one file serves both themes: it adds `role="img"
|
|
232
|
+
**Accessibility + dark mode (v4.11.0, #84; #161/#164).** `build:figures` rewrites every generated SVG so one file serves both themes: it adds `role="img"`, maps exact Warm–Tol authoring colors to semantic `--fig-*`, maps the seven chromatic Okabe–Ito colors to stable `--series-1..7`, and remaps neutral fills/strokes to the backward-compatible `--diagram-ink|paper|grid` aliases. Canonical series-8 black is indistinguishable from structural ink after PDF export; both resolve through `--fig-ink` in either theme. Unknown saturated colors stay authored. `<Figure>` **inlines** a local `.svg` (vs `<img>`), so the page's tokens cascade in and the figure tracks the in-page dark-mode toggle; `alt` (falling back to `caption`) becomes the SVG's accessible `<title>`, and `desc` becomes its optional longer `<desc>`. Notes:
|
|
232
233
|
|
|
233
234
|
- `alt` is the short accessible name (defaults to `caption`); `desc` is an optional longer description.
|
|
235
|
+
- For pale fills, export the canonical base color with a separate opacity (`fill opacity` / `alpha`), not a pre-blended tint such as `warmblue!13`; see `recipes/24-figure-authoring-standard.md`.
|
|
236
|
+
- Use `--fig-*` for meaning and `--series-*` only for categorical ordinals. Always add a non-color cue (label, marker, dash, shape, or texture).
|
|
234
237
|
- Non-SVG (`.png` fallback), remote, or unreadable `src` keep the `<img>` render.
|
|
235
238
|
- Opt a figure out of theming with a `%! no-theme` line in its source `.tex`.
|
|
236
239
|
- After upgrading, re-run `npm run build:figures` to theme pre-existing figures (the rewrite is idempotent).
|
package/bin/book-scaffold.mjs
CHANGED
|
@@ -29,7 +29,7 @@ Sub-commands:
|
|
|
29
29
|
build-labels Emit src/data/labels.json for cross-references (Phase C).
|
|
30
30
|
build-bib BibTeX -> references.json (+ sources/manifest.yaml -> sources.json).
|
|
31
31
|
build-figures PDF -> SVG via pdftocairo / pdftoppm fallback (+ TikZ in v4.2.0).
|
|
32
|
-
|
|
32
|
+
Adds a11y + theme-aware semantic/categorical palette mappings.
|
|
33
33
|
build-tips Scan chapters for <Tip> instances; emit src/data/tips.json (v4.3.0).
|
|
34
34
|
build-exercises Scan chapters for <Exercise> instances; emit src/data/exercises.json (v4.4.0).
|
|
35
35
|
render-notebooks ipynb -> HTML via Jupyter nbconvert.
|
package/components/Figure.astro
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* v4.11.0 (#84): a local pipeline `.svg` is **inlined** (read from public/ +
|
|
10
10
|
* `set:html`) rather than referenced via `<img>`. Inlining puts the SVG in the
|
|
11
|
-
* host DOM, so
|
|
12
|
-
* tracks the in-page dark-mode toggle (an `<img>`-loaded SVG
|
|
13
|
-
* and could only follow the OS preference). Inlining also lets a11y
|
|
11
|
+
* host DOM, so tokens.css `--fig-*`, `--series-*`, and `--diagram-*` cascade
|
|
12
|
+
* in — the figure tracks the in-page dark-mode toggle (an `<img>`-loaded SVG
|
|
13
|
+
* is CSS-isolated and could only follow the OS preference). Inlining also lets a11y
|
|
14
14
|
* `<title>`/`<desc>` come from this component's props (assembleSvg). Non-SVG
|
|
15
15
|
* (the pdftoppm `.png` fallback), remote, or unreadable `src` gracefully keep
|
|
16
16
|
* the `<img>` render — a figure must never crash a build.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { readFileSync } from 'node:fs';
|
|
31
31
|
import { join } from 'node:path';
|
|
32
|
-
import {
|
|
32
|
+
import { publicSvgPath, assembleSvg } from '../src/lib/figure.mjs';
|
|
33
33
|
|
|
34
34
|
interface Props {
|
|
35
35
|
src: string;
|
|
@@ -47,9 +47,10 @@ const altText = alt ?? caption ?? '';
|
|
|
47
47
|
// Inline a local pipeline SVG so host CSS variables theme it + a11y nodes come
|
|
48
48
|
// from props. Any read failure falls back to <img> below (never throws).
|
|
49
49
|
let inlineSvg: string | null = null;
|
|
50
|
-
|
|
50
|
+
const localSvgPath = publicSvgPath(src, import.meta.env.BASE_URL);
|
|
51
|
+
if (localSvgPath) {
|
|
51
52
|
try {
|
|
52
|
-
const raw = readFileSync(join(process.cwd(), 'public',
|
|
53
|
+
const raw = readFileSync(join(process.cwd(), 'public', localSvgPath), 'utf8');
|
|
53
54
|
const idBase = id ?? `fig-${src.replace(/^\/+/, '').replace(/\.[^.]+$/, '')}`;
|
|
54
55
|
inlineSvg = assembleSvg(raw, { caption, alt, desc, width, idBase });
|
|
55
56
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books with five typed presets, Tufte typography, citations, search, PDF, and Cloudflare deployment.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.31.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -174,6 +174,8 @@
|
|
|
174
174
|
],
|
|
175
175
|
"scripts": {
|
|
176
176
|
"build": "tsup",
|
|
177
|
+
"sync:figure-tokens": "node scripts/sync-figure-tokens.mjs --write",
|
|
178
|
+
"check:figure-tokens": "node scripts/sync-figure-tokens.mjs --check",
|
|
177
179
|
"check:types": "tsc -p tests/types/tsconfig.json",
|
|
178
180
|
"prepublishOnly": "npm run build && npm run check:types",
|
|
179
181
|
"test": "node --test tests/*.test.mjs"
|
|
@@ -190,9 +192,6 @@
|
|
|
190
192
|
},
|
|
191
193
|
"rehype-katex": {
|
|
192
194
|
"optional": true
|
|
193
|
-
},
|
|
194
|
-
"remark-math": {
|
|
195
|
-
"optional": true
|
|
196
195
|
}
|
|
197
196
|
},
|
|
198
197
|
"dependencies": {
|
|
@@ -203,6 +202,8 @@
|
|
|
203
202
|
"@fontsource-variable/roboto": "^5.2.10",
|
|
204
203
|
"@fontsource-variable/source-code-pro": "^5.2.7",
|
|
205
204
|
"pagefind": "^1.5.2",
|
|
205
|
+
"parse5": "^7.3.0",
|
|
206
|
+
"remark-math": "^6.0.0",
|
|
206
207
|
"remark-mdx": "^3.1.1",
|
|
207
208
|
"remark-parse": "^11.0.0",
|
|
208
209
|
"unified": "^11.0.5",
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
**Profile**: any (build:figures and build:notebooks both graceful-skip when source dirs / tools are absent).
|
|
4
4
|
|
|
5
|
-
**TL;DR**: Put PDFs in `figures/`, Jupyter notebooks in `notebooks
|
|
5
|
+
**TL;DR**: Put PDFs in `figures/`, Jupyter notebooks in `notebooks/`, then run
|
|
6
|
+
`npm run build:figures` and `npm run build:notebooks` explicitly. Output:
|
|
7
|
+
`public/figures/*.svg` (PDF→SVG via `pdftocairo`) and
|
|
8
|
+
`public/notebooks/*.html` (ipynb→HTML via `uv run jupyter nbconvert`). These
|
|
9
|
+
optional system-tool pipelines are not part of `prebuild`; generated books run
|
|
10
|
+
validation there instead.
|
|
6
11
|
|
|
7
12
|
## How each pipeline works
|
|
8
13
|
|
|
@@ -11,6 +16,7 @@
|
|
|
11
16
|
- **Source**: `figures/` at scaffold root (override via `BOOK_FIGURES_PATH` env var; e.g. `BOOK_FIGURES_PATH=../shared/figures` to share with a LaTeX sibling)
|
|
12
17
|
- **Output**: `public/figures/<same-subdir-structure>/<stem>.svg`
|
|
13
18
|
- **Tool**: `pdftocairo` (poppler-utils) with `pdftoppm` fallback for malformed SVG
|
|
19
|
+
- **Theming**: canonical Warm–Tol / Okabe–Ito colors and neutral paints are mapped to theme-aware CSS variables; see [Recipe 24](24-figure-authoring-standard.md)
|
|
14
20
|
- **Idempotency**: skips when SVG mtime >= PDF mtime
|
|
15
21
|
- **Graceful skip**: if `pdftocairo` or `pdftoppm` not on PATH (Cloudflare build container), warns and exits 0 — committed SVGs under `public/figures/` are served as-is
|
|
16
22
|
|
|
@@ -58,8 +64,9 @@ Cloudflare Workers build containers don't have `pdftocairo` or `uv` installed. T
|
|
|
58
64
|
|
|
59
65
|
1. **Commit derived artifacts** (recommended for low-friction deploy):
|
|
60
66
|
- Edit `.gitignore`: remove the `public/figures/` and `public/notebooks/` lines.
|
|
61
|
-
- Run `npm run build:
|
|
62
|
-
|
|
67
|
+
- Run `npm run build:figures` and `npm run build:notebooks` locally; commit
|
|
68
|
+
the generated outputs.
|
|
69
|
+
- CI serves the committed artifacts without needing either optional system tool.
|
|
63
70
|
- Trade-off: ~3 MB of binary artifacts in git history.
|
|
64
71
|
|
|
65
72
|
2. **Install poppler + uv in CI**: prepend `apt-get install -y poppler-utils && curl -LsSf https://astral.sh/uv/install.sh | sh && ...` to the build command. More setup; cleaner repo.
|
|
@@ -71,12 +78,13 @@ post_transformers chose option 1 (see commit `f7fa75d`).
|
|
|
71
78
|
- **Notebooks should be output-free** for a clean rendered HTML — clear outputs before committing, or use `nbstripout` as a pre-commit hook. Cells with embedded outputs render those outputs in the HTML; this may or may not be what you want.
|
|
72
79
|
- **Stub-size threshold** at 1500 bytes is empirical from post_transformers — adjust via `NOTEBOOK_STUB_BYTES` if your placeholder notebooks are larger.
|
|
73
80
|
- **`pdftocairo` produces a tiny SVG** for some PDF inputs (vector layers ungrouped, etc.). The script auto-falls back to `pdftoppm -r 200 -png` at 200 DPI when SVG output is < 200 bytes.
|
|
81
|
+
- **Do not pre-blend semantic fills** (`warmblue!13`, for example). Export the canonical base color with a separate opacity so the SVG rewrite can preserve its role in dark mode. Recipe 24 has TikZ and matplotlib examples.
|
|
74
82
|
|
|
75
83
|
## Canonical files
|
|
76
84
|
|
|
77
85
|
- `scripts/build-figures.mjs:23-32` — path resolution + env overrides
|
|
78
86
|
- `scripts/render-notebooks.mjs:30-46` — same
|
|
79
|
-
- `package.json` `
|
|
87
|
+
- `package.json` `build:figures` / `build:notebooks` — explicit authoring commands
|
|
80
88
|
- `.gitignore` — toggles whether artifacts are committed
|
|
81
89
|
|
|
82
90
|
## Reference implementation
|
package/recipes/09-validation.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Profile**: any (Cite checks skip under non-academic).
|
|
4
4
|
|
|
5
|
-
**TL;DR**: `npm run validate` runs `scripts/validate.mjs` against
|
|
5
|
+
**TL;DR**: `npm run validate` runs `scripts/validate.mjs` against chapter MD/MDX files (plus its question-collection checks). It first regenerates missing `labels.json` and `references.json`, then catches typo'd bibkeys / XRef ids / Figure paths / internal links that `astro build` would either miss or surface with poor context. Auto-runs as `prebuild`; recommend wiring into pre-commit too.
|
|
6
6
|
|
|
7
7
|
## What gets checked
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| `<BookLink book= to=>` both present; `book=` registered; literal fragment target resolves in a declared sibling labels index (#96, #147) | all | Pre-flights the component's build-time throw and dead cross-book anchors across all files at once. |
|
|
18
18
|
| Questions collection: unique `id`s + `domain` in `examDomains` (#112); `<Rationale appendix>` carries `for=` (#114, v4.21.0) | all, when `src/content/questions/` exists | Duplicate ids break the appendix/flashcards cross-ref key; an unregistered domain throws one-at-a-time at build; an appendix rationale without its anchor target throws at build. |
|
|
19
19
|
| `los[].anchor` ↔ `{/* anchor: <slug> */}` prose markers agree both ways (#130, v4.20.0) | all, when frontmatter has `los:` | A declared objective whose prose marker is missing/misspelled (or an orphan marker) builds green otherwise — frontmatter↔prose drift only a hand audit would catch. |
|
|
20
|
+
| Literal authored Markdown/HTML/JSX `href`/`src` stays inside a non-root Astro `base` (#190) | all | A browser resolves `/chapters/...` from the host root, bypassing Astro's deployment base and producing a convincing 404. This is a build-blocking error, not a rewrite. |
|
|
20
21
|
|
|
21
22
|
Validate also emits two **non-blocking shadow-route warnings** (exit code unaffected): a consumer-owned `src/pages/chapters/[...slug].astro` without `routes: { chapters: false }` (v4.6.0, #76), and a consumer-owned `src/pages/index.astro` without `routes: { landing: false }` (v4.20.0, #129 — Astro has announced this collision becomes a hard error). See [recipe 18](./18-chapter-route-ownership.md).
|
|
22
23
|
|
|
@@ -48,6 +49,24 @@ Heading-entry keys are intentionally opaque and path-qualified: `#summary` can l
|
|
|
48
49
|
|
|
49
50
|
A declared `labels` file that is missing, unreadable, malformed, or not a JSON object is an error. An unknown book, unknown fragment, wrong path, or malformed label entry is also an error with the source file and target. Dynamic `book=`/`to=` expressions and prop spreads cannot be evaluated safely, so they emit explicit skip warnings. URL strings and `{ url }` descriptors likewise warn for literal fragment links because no index was declared. Non-fragment route links warn and skip: labels indexes describe anchors, not every sibling route.
|
|
50
51
|
|
|
52
|
+
## Authored links under a non-root base (#190)
|
|
53
|
+
|
|
54
|
+
When evaluated `astro.config.*` sets a non-root base such as `base: '/library/books/'`, a root-absolute authored URL escapes that deployment mount:
|
|
55
|
+
|
|
56
|
+
```mdx
|
|
57
|
+
<!-- Errors: both resolve from the host root, outside /library/books/. -->
|
|
58
|
+
[Chapter one](/chapters/one/)
|
|
59
|
+
<img src="/figures/overview.svg" alt="Overview" />
|
|
60
|
+
|
|
61
|
+
<!-- Valid: explicitly remains inside the configured base. -->
|
|
62
|
+
[Chapter one](/library/books/chapters/one/)
|
|
63
|
+
<a href={`${import.meta.env.BASE_URL}references/`}>References</a>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The validator structurally parses inline Markdown links and images, Markdown reference definitions, quoted or unquoted HTML `href`/`src`, and quoted or statically braced JSX string attributes. HTML entities and JavaScript string escapes are decoded before containment is checked. It reports the authored file and line, the evaluated target, the evaluated base, and a browser-normalized base-prefixed replacement. It also scans question MD/MDX files already visited by validation.
|
|
67
|
+
|
|
68
|
+
The check is deliberately validation-only: it does not mutate or rewrite content, and there is no opt-out. Under the root base (`/`) it is inert. Protocol URLs (`https:`, `mailto:`, `data:`, and others), protocol-relative URLs (`//cdn…`), fragments, relative targets, targets already inside the base, and dynamic JSX expressions are excluded. `rel="external"` describes a relationship but does not change where a browser navigates, so it does not exempt a host-root URL. Frontmatter, comments, fenced/inline/indented code examples, and `<pre>`/`<code>` examples are excluded by their parsed Markdown/MDX structure so documentation snippets do not become false diagnostics.
|
|
69
|
+
|
|
51
70
|
## Missing generated artifacts
|
|
52
71
|
|
|
53
72
|
`src/data/labels.json` and `src/data/references.json` are derived and normally gitignored. v4.27+ `validate` self-heals either missing file unconditionally by invoking the package's own `build-labels` or `build-bib` command before loading data. Existing files are untouched. A failed child command stops validation with the child's original diagnostic and exit status.
|
|
@@ -96,7 +115,7 @@ For pre-commit: add to `.pre-commit-config.yaml`:
|
|
|
96
115
|
|
|
97
116
|
## Preset / chaptersBase resolution (v4.7.0+, #75)
|
|
98
117
|
|
|
99
|
-
The validator evaluates `astro.config.*` through Vite first. A resolved scaffold integration supplies the composed preset and `numberStyle`, so CLI tooling sees the same Style chain as the Astro build. Without such an integration, the legacy preset chain remains: `--preset` → process `BOOK_PRESET`/`BOOK_PROFILE` → root `.env` → the literal value in `defineBookSchemas` → a warned `minimal` v4 compatibility fallback. Every selected value is checked against the five-preset enum; config-evaluation failures stop validation instead of silently using defaults.
|
|
118
|
+
The validator evaluates `astro.config.*` through Vite first. A resolved scaffold integration supplies the composed preset and `numberStyle`, so CLI tooling sees the same Style chain as the Astro build. The evaluated Astro `base` is read from that same config regardless of whether the scaffold integration is present; omission defaults to `/`. Without such an integration, the legacy preset chain remains: `--preset` → process `BOOK_PRESET`/`BOOK_PROFILE` → root `.env` → the literal value in `defineBookSchemas` → a warned `minimal` v4 compatibility fallback. Every selected value is checked against the five-preset enum; config-evaluation failures stop validation instead of silently using defaults.
|
|
100
119
|
|
|
101
120
|
`chaptersBase` resolution still consults `BOOK_CHAPTERS_DIR`, content configuration, then `src/content/chapters`. The v4.5+ canonical form is:
|
|
102
121
|
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## TL;DR
|
|
6
6
|
|
|
7
|
-
Drop `figures/<topic>/diagram.tex` (standalone TikZ source). Run
|
|
7
|
+
Drop `figures/<topic>/diagram.tex` (standalone TikZ source). Run
|
|
8
|
+
`npm run build:figures` explicitly; figure conversion is not wired into
|
|
9
|
+
`prebuild` because TeX and Poppler are optional system tools. The command emits
|
|
10
|
+
`public/figures/<topic>/diagram.svg`, ready to reference in MDX as
|
|
11
|
+
`<Figure src="/figures/<topic>/diagram.svg" />`.
|
|
8
12
|
|
|
9
13
|
## The TikZ source
|
|
10
14
|
|
|
@@ -25,6 +29,13 @@ Use the `standalone` document class; configure for SVG via `tikz` option. Recomm
|
|
|
25
29
|
|
|
26
30
|
The `border=2mm` adds a small margin around the figure so it doesn't crop right at the edge.
|
|
27
31
|
|
|
32
|
+
For theme-aware color, define the canonical Warm–Tol or Okabe–Ito hex and use
|
|
33
|
+
`fill opacity` rather than an xcolor pre-blend such as `warmblue!13`. The base
|
|
34
|
+
color survives PDF export and `build-figures` can map it to `--fig-*` or
|
|
35
|
+
`--series-*`; the pre-blended RGB cannot preserve its meaning. See
|
|
36
|
+
[Recipe 24 — Figure authoring standard](24-figure-authoring-standard.md) for
|
|
37
|
+
the palette, accessible description pattern, and dual-theme release gate.
|
|
38
|
+
|
|
28
39
|
## Discovery rule
|
|
29
40
|
|
|
30
41
|
`build-figures` walks `figures/` (or `BOOK_FIGURES_PATH`) for both `.pdf` and `.tex` files. For each `.tex` source:
|
|
@@ -136,6 +147,6 @@ layout: wide # widens --measure-main to 80ch; omit (or 'default') for the s
|
|
|
136
147
|
|
|
137
148
|
## See also
|
|
138
149
|
|
|
139
|
-
- `recipes/
|
|
150
|
+
- `recipes/24-figure-authoring-standard.md` — palette, matplotlib/TikZ authoring, dark mode, and accessibility
|
|
140
151
|
- `PACKAGE_DESIGN.md §7` — peer dependencies (lists `pdflatex` as optional system dep)
|
|
141
152
|
- `PACKAGE_DESIGN.md §8` — `book-scaffold` CLI reference (build-figures subcommand)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Recipe 24 — Figure authoring standard
|
|
2
|
+
|
|
3
|
+
**Profile**: any profile that publishes diagrams, plots, or TikZ figures.
|
|
4
|
+
|
|
5
|
+
**TL;DR**: Use `--fig-*` when a color means something and `--series-1` through
|
|
6
|
+
`--series-8` when colors only distinguish data series. Author exported figures
|
|
7
|
+
with the canonical hex plus a separate opacity; `build:figures` maps those
|
|
8
|
+
colors to theme-aware CSS variables. Every figure needs a useful caption and
|
|
9
|
+
accessible description, and color must never be the only way to read it.
|
|
10
|
+
|
|
11
|
+
## 1. Choose the right color contract
|
|
12
|
+
|
|
13
|
+
Semantic colors keep the same meaning across a book:
|
|
14
|
+
|
|
15
|
+
| Token | Meaning | Light | Dark | Export hex |
|
|
16
|
+
|---|---|---|---|---|
|
|
17
|
+
| `--fig-blue` | default, lightweight, informational | `#3B6FA0` | `#7297BB` | `#3B6FA0` |
|
|
18
|
+
| `--fig-green` | positive or successful outcome | `#4A7E3F` | `#7DA275` | `#4A7E3F` |
|
|
19
|
+
| `--fig-rose` | caution or problem | `#C06858` | `#D29287` | `#C06858` |
|
|
20
|
+
| `--fig-plum` | authority, control, or heaviest weight | `#8A4E82` | `#AB80A5` | `#8A4E82` |
|
|
21
|
+
| `--fig-gold` | packaging, coordination, or convergence | `#9D7D34` | `#D2B575` | `#C09840` |
|
|
22
|
+
| `--fig-crimson` | failure or severe problem | `#A03838` | `#BB7070` | `#A03838` |
|
|
23
|
+
| `--fig-ink` | labels, essential outlines, axes | `#1A1A19` | `#E8E5DD` | `#1A1A19` or black |
|
|
24
|
+
| `--fig-paper` | figure background | `#FDFCF9` | `#1A1816` | `#FDFCF9` or white |
|
|
25
|
+
| `--fig-grid` | essential gridlines and secondary structure | `#8C8981` | `#746E67` | `#B5B3AA` or a mid-neutral |
|
|
26
|
+
|
|
27
|
+
`--fig-gold` resolves to a slightly darker gold in the light theme so an
|
|
28
|
+
essential stroke clears 3:1 against `--fig-paper`. The export rewrite still
|
|
29
|
+
recognizes the canonical `#C09840` authoring value.
|
|
30
|
+
|
|
31
|
+
Categorical plots use the stable Okabe–Ito order:
|
|
32
|
+
|
|
33
|
+
| Ordinal | Token | Canonical color |
|
|
34
|
+
|---:|---|---|
|
|
35
|
+
| 1 | `--series-1` | orange `#E69F00` |
|
|
36
|
+
| 2 | `--series-2` | sky blue `#56B4E9` |
|
|
37
|
+
| 3 | `--series-3` | bluish green `#009E73` |
|
|
38
|
+
| 4 | `--series-4` | yellow `#F0E442` |
|
|
39
|
+
| 5 | `--series-5` | blue `#0072B2` |
|
|
40
|
+
| 6 | `--series-6` | vermillion `#D55E00` |
|
|
41
|
+
| 7 | `--series-7` | reddish purple `#CC79A7` |
|
|
42
|
+
| 8 | `--series-8` | theme ink (canonical authoring color: black) |
|
|
43
|
+
|
|
44
|
+
Series 1–7 use the same canonical value in light and dark; each already clears
|
|
45
|
+
3:1 against the dark figure paper. Series 8 changes with `--fig-ink`.
|
|
46
|
+
|
|
47
|
+
Assign series in that order and keep the ordinal attached to the same series
|
|
48
|
+
when a chart or theme changes. Never use `--series-*` to mean good, bad,
|
|
49
|
+
warning, or success; those meanings belong to `--fig-*`.
|
|
50
|
+
|
|
51
|
+
After PDF export, canonical series-8 black is indistinguishable from labels and
|
|
52
|
+
axes. The rewrite therefore maps it to structural ink; `--series-8` aliases the
|
|
53
|
+
same `--fig-ink` value, so it still renders correctly in both themes. Use
|
|
54
|
+
`var(--series-8)` directly in a CSS-native SVG if you need to override that
|
|
55
|
+
ordinal independently.
|
|
56
|
+
|
|
57
|
+
## 2. CSS-native SVG: the preferred hand-authored form
|
|
58
|
+
|
|
59
|
+
Use variables directly. Pale fills should be computed from the current figure
|
|
60
|
+
paper, while their solid borders carry the category:
|
|
61
|
+
|
|
62
|
+
```svg
|
|
63
|
+
<svg role="img" aria-labelledby="pipeline-title pipeline-desc"
|
|
64
|
+
viewBox="0 0 420 140" xmlns="http://www.w3.org/2000/svg">
|
|
65
|
+
<title id="pipeline-title">A three-stage publishing pipeline</title>
|
|
66
|
+
<desc id="pipeline-desc">
|
|
67
|
+
Draft flows to review and then publication. Review is the only caution stage.
|
|
68
|
+
</desc>
|
|
69
|
+
<style>
|
|
70
|
+
.label { fill: var(--fig-ink); font: 16px sans-serif; }
|
|
71
|
+
.edge { fill: none; stroke: var(--fig-ink); stroke-width: 2; }
|
|
72
|
+
.draft {
|
|
73
|
+
fill: color-mix(in srgb, var(--fig-blue) 14%, var(--fig-paper));
|
|
74
|
+
stroke: var(--fig-blue);
|
|
75
|
+
}
|
|
76
|
+
.review {
|
|
77
|
+
fill: color-mix(in srgb, var(--fig-rose) 14%, var(--fig-paper));
|
|
78
|
+
stroke: var(--fig-rose);
|
|
79
|
+
}
|
|
80
|
+
.published {
|
|
81
|
+
fill: color-mix(in srgb, var(--fig-green) 14%, var(--fig-paper));
|
|
82
|
+
stroke: var(--fig-green);
|
|
83
|
+
}
|
|
84
|
+
.node { stroke-width: 3; }
|
|
85
|
+
</style>
|
|
86
|
+
<!-- geometry omitted -->
|
|
87
|
+
</svg>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
When this is a local SVG rendered by `<Figure>`, the host page supplies the
|
|
91
|
+
variables and its light/dark toggle changes the figure immediately. An SVG
|
|
92
|
+
loaded from another origin cannot inherit host variables.
|
|
93
|
+
|
|
94
|
+
For a categorical chart, use `stroke: var(--series-1)`, then `--series-2`, and
|
|
95
|
+
so on. Give every line a second channel too: different dashes, markers, or a
|
|
96
|
+
direct text label.
|
|
97
|
+
|
|
98
|
+
## 3. TikZ and matplotlib exports
|
|
99
|
+
|
|
100
|
+
PDF cannot carry CSS variables. `build:figures` therefore recognizes the exact
|
|
101
|
+
Warm–Tol and Okabe–Ito authoring colors in the generated SVG and maps them back
|
|
102
|
+
to variables. Unknown saturated colors stay exactly as authored.
|
|
103
|
+
|
|
104
|
+
The important rule is **base color plus separate opacity**. Do not use a
|
|
105
|
+
pre-blended low-chroma color such as `figblue!13`: after PDF export it is only
|
|
106
|
+
an anonymous near-white RGB and can be mistaken for figure paper.
|
|
107
|
+
|
|
108
|
+
TikZ:
|
|
109
|
+
|
|
110
|
+
```latex
|
|
111
|
+
\definecolor{figblue}{HTML}{3B6FA0}
|
|
112
|
+
\definecolor{figrose}{HTML}{C06858}
|
|
113
|
+
\definecolor{seriesone}{HTML}{E69F00}
|
|
114
|
+
|
|
115
|
+
\node[
|
|
116
|
+
draw=figblue,
|
|
117
|
+
fill=figblue,
|
|
118
|
+
fill opacity=.14,
|
|
119
|
+
text opacity=1,
|
|
120
|
+
line width=1.2pt,
|
|
121
|
+
] (draft) {Draft};
|
|
122
|
+
\node[draw=figrose, fill=figrose, fill opacity=.14, text opacity=1] (review) {Review};
|
|
123
|
+
\draw[seriesone, very thick, dashed] plot coordinates {(0,0) (1,2) (2,3)};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Matplotlib:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import matplotlib.pyplot as plt
|
|
130
|
+
|
|
131
|
+
FIG = {
|
|
132
|
+
"blue": "#3B6FA0",
|
|
133
|
+
"rose": "#C06858",
|
|
134
|
+
"green": "#4A7E3F",
|
|
135
|
+
}
|
|
136
|
+
SERIES = [
|
|
137
|
+
"#E69F00", "#56B4E9", "#009E73", "#F0E442",
|
|
138
|
+
"#0072B2", "#D55E00", "#CC79A7", "#000000",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
fig, ax = plt.subplots()
|
|
142
|
+
# Keep the canvas transparent: the host's --fig-paper must show through after
|
|
143
|
+
# PDF → SVG conversion instead of baking a light-only rectangle into the plot.
|
|
144
|
+
fig.patch.set_alpha(0)
|
|
145
|
+
ax.set_facecolor("none")
|
|
146
|
+
ax.fill_between(x, low, high, color=FIG["blue"], alpha=.14)
|
|
147
|
+
ax.plot(x, mean, color=FIG["blue"], linewidth=2)
|
|
148
|
+
ax.plot(x, baseline, color=SERIES[0], linestyle="--", marker="o", label="Baseline")
|
|
149
|
+
fig.savefig(
|
|
150
|
+
"figures/results/summary.pdf",
|
|
151
|
+
format="pdf",
|
|
152
|
+
transparent=True,
|
|
153
|
+
bbox_inches="tight",
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Then run `npm run build:figures` to preserve the vector PDF→SVG path. Export a
|
|
158
|
+
raster (`.png`) only when the source is true pixel data, such as a photograph
|
|
159
|
+
or instrument raster. Lines, text, markers, and diagram geometry should remain
|
|
160
|
+
vector PDF/SVG. Re-run the command after upgrading the scaffold so
|
|
161
|
+
already-generated SVGs receive new mappings; the rewrite is idempotent.
|
|
162
|
+
|
|
163
|
+
## 4. Accessible name and description
|
|
164
|
+
|
|
165
|
+
Use `<Figure>` as the accessibility boundary:
|
|
166
|
+
|
|
167
|
+
```mdx
|
|
168
|
+
<Figure
|
|
169
|
+
src="/figures/results/summary.svg"
|
|
170
|
+
id="fig-results-summary"
|
|
171
|
+
caption="Review catches most defects before publication."
|
|
172
|
+
alt="Three-stage publishing pipeline"
|
|
173
|
+
desc="Draft flows right to review, then publication. The review node is marked as a caution stage, and the publication node is marked as a successful outcome."
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `caption` states the point the reader should take from the figure.
|
|
178
|
+
- `alt` is a short accessible name, not a duplicate of every visible label.
|
|
179
|
+
- `desc` gives reading order, relationships, trend, and any conclusion carried
|
|
180
|
+
by position, shape, line style, or color.
|
|
181
|
+
- For a local pipeline SVG, `<Figure>` replaces stale `<title>`/`<desc>` nodes
|
|
182
|
+
with these props and wires `aria-labelledby` automatically.
|
|
183
|
+
- Raster and remote images remain `<img>` elements, so give them complete alt
|
|
184
|
+
text and put any longer explanation in adjacent prose.
|
|
185
|
+
|
|
186
|
+
Do not write “blue means success” as the only description. Name the category
|
|
187
|
+
and the redundant cue: “the successful outcome is the green node with a check
|
|
188
|
+
mark.”
|
|
189
|
+
|
|
190
|
+
## 5. Contrast and color-vision rules
|
|
191
|
+
|
|
192
|
+
Okabe–Ito is color-vision-deficiency-friendly; that does not make every hue a
|
|
193
|
+
high-contrast thin line on light paper. In particular, orange, sky blue,
|
|
194
|
+
yellow, and reddish purple need redundant encoding.
|
|
195
|
+
|
|
196
|
+
- Never encode a distinction by color alone. Add marker shape, dash pattern,
|
|
197
|
+
direct labels, texture, or position.
|
|
198
|
+
- Use `--fig-ink` for axes, text, arrowheads, and essential outlines.
|
|
199
|
+
- Give pale semantic areas a solid `--fig-*` border. The semantic strokes and
|
|
200
|
+
`--fig-grid` clear WCAG's 3:1 non-text contrast threshold in both themes.
|
|
201
|
+
- Outline small categorical marks with `--fig-ink`; use thicker/dashed lines
|
|
202
|
+
and markers for categorical series that do not clear 3:1 by color alone.
|
|
203
|
+
- Do not reorder `--series-*` in dark mode. `--series-8` follows theme ink so
|
|
204
|
+
the neutral series remains visible rather than staying literal black.
|
|
205
|
+
|
|
206
|
+
## 6. Dual-theme release gate
|
|
207
|
+
|
|
208
|
+
Before publishing a new figure:
|
|
209
|
+
|
|
210
|
+
1. Run `npm run build:figures` and confirm the SVG contains one
|
|
211
|
+
`data-diagram-map` style block.
|
|
212
|
+
2. Render it through `<Figure>` with explicit light and dark page themes.
|
|
213
|
+
3. Open the standalone SVG with both OS color schemes; its embedded defaults
|
|
214
|
+
should also switch.
|
|
215
|
+
4. Check labels, axes, arrowheads, boundaries, and all series at the smallest
|
|
216
|
+
published size.
|
|
217
|
+
5. Inspect a grayscale or common CVD simulation and verify that markers,
|
|
218
|
+
dashes, labels, or shapes preserve every distinction.
|
|
219
|
+
6. Confirm the caption and screen-reader description communicate the takeaway
|
|
220
|
+
without relying on color names.
|
|
221
|
+
|
|
222
|
+
If automatic recoloring is genuinely wrong for a special TikZ figure, add a
|
|
223
|
+
`%! no-theme` line to its `.tex` source and supply a fully authored light/dark
|
|
224
|
+
solution yourself. That escape hatch opts out of both palette and neutral
|
|
225
|
+
mapping.
|
|
226
|
+
|
|
227
|
+
## Canonical files
|
|
228
|
+
|
|
229
|
+
- `src/lib/figure-palette.mjs` — sole palette manifest + generated CSS/theme renderers
|
|
230
|
+
- `styles/tokens.css` — public `--fig-*`, `--series-*`, and compatibility tokens
|
|
231
|
+
- `src/lib/figure.mjs` — exact palette recognition and SVG theme injection
|
|
232
|
+
- `scripts/sync-figure-tokens.mjs` — checks or refreshes the generated token block
|
|
233
|
+
- `scripts/build-figures.mjs` — PDF/TikZ conversion pipeline
|
|
234
|
+
- `components/Figure.astro` — inlining plus accessible `<title>`/`<desc>`
|
|
235
|
+
- `tests/figure-palette.test.mjs` — token, mapping, theme, and contrast contract
|
|
236
|
+
|
|
237
|
+
## See also
|
|
238
|
+
|
|
239
|
+
- [Recipe 03 — Asset pipelines](03-asset-pipelines.md)
|
|
240
|
+
- [Recipe 04 — Component library](04-component-library.md)
|
|
241
|
+
- [Recipe 16 — TikZ figures](16-tikz-figures.md)
|
package/recipes/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Terse pointers into canonical code for the most common book-authoring workflows.
|
|
|
29
29
|
| 21 | [Multiple guides in one app](21-multi-guide-single-app.md) | any | `generateId` namespacing over one collection; flat-slug footgun; interim until multibook (#80) |
|
|
30
30
|
| 22 | [Responsive navigation and multi-book routing](22-responsive-nav-and-multibook-routing.md) | any | Mobile/desktop nav, base-aware route ownership, and the interim multi-book routing contract |
|
|
31
31
|
| 23 | [Interactive demo substrate](23-interactive-demo-substrate.md) | any | Opt-in Preact shell, slider, stat cards, theme colors, a11y, and reduced motion |
|
|
32
|
+
| 24 | [Figure authoring standard](24-figure-authoring-standard.md) | any | Warm–Tol semantics, Okabe–Ito series, dual-theme exports, contrast, and accessible descriptions |
|
|
32
33
|
|
|
33
34
|
## How to read recipes
|
|
34
35
|
|