@brandon_m_behring/book-scaffold-astro 4.30.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 +3 -1
- package/bin/book-scaffold.mjs +1 -1
- package/components/Figure.astro +7 -6
- package/package.json +3 -1
- package/recipes/03-asset-pipelines.md +12 -4
- 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/build-figures.mjs +13 -10
- package/scripts/render-notebooks.mjs +2 -1
- package/scripts/sync-figure-tokens.mjs +44 -0
- 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
|
@@ -229,9 +229,11 @@ See `recipes/09-validation.md` to extend.
|
|
|
229
229
|
2. `npm run build:figures` produces `public/figures/<topic>/<name>.svg`.
|
|
230
230
|
3. Reference: `<Figure src="/figures/<topic>/<name>.svg" caption="..." alt="..." id="..." />`.
|
|
231
231
|
|
|
232
|
-
**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:
|
|
233
233
|
|
|
234
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).
|
|
235
237
|
- Non-SVG (`.png` fallback), remote, or unreadable `src` keep the `<img>` render.
|
|
236
238
|
- Opt a figure out of theming with a `%! no-theme` line in its source `.tex`.
|
|
237
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"
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -25,21 +25,22 @@
|
|
|
25
25
|
*
|
|
26
26
|
* v4.11.0 (closes #84): each generated SVG gets a post-export rewrite
|
|
27
27
|
* (recolorSvg) that injects role="img" + a CSS-variable theming layer so one
|
|
28
|
-
* SVG serves light + dark.
|
|
29
|
-
* var(--
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* SVG serves light + dark. Known Warm–Tol semantic and Okabe–Ito categorical
|
|
29
|
+
* colors are mapped to var(--fig-*|--series-*, <original>); neutral paints map
|
|
30
|
+
* to var(--diagram-ink|paper|grid, <original>). Unknown saturated colors stay
|
|
31
|
+
* authored. A `%! no-theme` line in the source .tex opts a figure out.
|
|
32
32
|
* <Figure> inlines local SVGs so they track the in-page [data-theme] toggle.
|
|
33
33
|
*
|
|
34
34
|
* Idempotent: skips when the target SVG is newer than the source PDF (and the
|
|
35
35
|
* recolor itself is a no-op on an already-themed SVG, so re-runs after an
|
|
36
36
|
* upgrade safely theme pre-existing figures).
|
|
37
|
-
* Run
|
|
37
|
+
* Run explicitly with `npm run build:figures`; optional system tools keep this
|
|
38
|
+
* authoring pipeline out of the generated project's `prebuild` hook.
|
|
38
39
|
*
|
|
39
40
|
* Graceful skip: when pdftocairo / pdftoppm aren't on PATH (e.g. Cloudflare
|
|
40
41
|
* build container), the script warns and exits 0. Committed SVGs/PNGs under
|
|
41
|
-
* public/figures/ are served as-is. Local
|
|
42
|
-
* from PDFs
|
|
42
|
+
* public/figures/ are served as-is. Local authors with poppler-utils regenerate
|
|
43
|
+
* from PDFs when source figures change.
|
|
43
44
|
*/
|
|
44
45
|
import { readdir, stat, mkdir } from 'node:fs/promises';
|
|
45
46
|
import { existsSync, statSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -56,8 +57,10 @@ Figure pipeline. PDF -> SVG via pdftocairo (PNG fallback via pdftoppm at
|
|
|
56
57
|
Graceful-skip if pdftocairo / pdftoppm not on PATH.
|
|
57
58
|
|
|
58
59
|
Each SVG is rewritten to be accessible + dark-mode-aware: role="img" plus
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
exact Warm–Tol/Okabe–Ito palette mappings and neutral
|
|
61
|
+
var(--diagram-ink|paper|grid, orig) mappings. Use base color + separate opacity,
|
|
62
|
+
not a pre-blended tint. A "%! no-theme" line in the source .tex opts out.
|
|
63
|
+
<Figure> inlines local SVGs so they track the theme.
|
|
61
64
|
|
|
62
65
|
Env:
|
|
63
66
|
BOOK_FIGURES_PATH Override figures source (default: figures/).
|
|
@@ -202,7 +205,7 @@ const NO_THEME_RE = /^\s*%!\s*no-theme\b/m;
|
|
|
202
205
|
/**
|
|
203
206
|
* v4.11.0 (#84): apply the theming rewrite to a generated SVG in place.
|
|
204
207
|
* No-op (returns false) if the file is absent or recolorSvg leaves it unchanged
|
|
205
|
-
* (already themed / nothing
|
|
208
|
+
* (already themed / nothing themeable to remap). Idempotent.
|
|
206
209
|
*/
|
|
207
210
|
function themeIfSvg(svgPath, optOut) {
|
|
208
211
|
if (!existsSync(svgPath)) return false;
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* canonical; the notebook is a code companion.
|
|
17
17
|
*
|
|
18
18
|
* Idempotent: skips when the target HTML is newer than the source .ipynb.
|
|
19
|
-
* Run
|
|
19
|
+
* Run explicitly with `npm run build:notebooks`; optional system tools keep
|
|
20
|
+
* this authoring pipeline out of the generated project's `prebuild` hook.
|
|
20
21
|
*
|
|
21
22
|
* Style scoping: nbconvert's `basic` template emits HTML without nbviewer
|
|
22
23
|
* chrome, but with embedded <style> tags. To prevent CSS bleed, each
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Regenerate or verify the manifest-owned block in styles/tokens.css. */
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
FIGURE_TOKEN_BLOCK_END,
|
|
7
|
+
FIGURE_TOKEN_BLOCK_START,
|
|
8
|
+
renderFigureTokenCssBlock,
|
|
9
|
+
} from '../src/lib/figure-palette.mjs';
|
|
10
|
+
|
|
11
|
+
const TOKENS_PATH = fileURLToPath(new URL('../styles/tokens.css', import.meta.url));
|
|
12
|
+
const args = new Set(process.argv.slice(2));
|
|
13
|
+
const write = args.has('--write');
|
|
14
|
+
if (args.size > 1 || (args.size === 1 && !write && !args.has('--check'))) {
|
|
15
|
+
console.error('Usage: node scripts/sync-figure-tokens.mjs [--check|--write]');
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const css = readFileSync(TOKENS_PATH, 'utf8');
|
|
20
|
+
const start = css.indexOf(FIGURE_TOKEN_BLOCK_START);
|
|
21
|
+
const endStart = css.indexOf(FIGURE_TOKEN_BLOCK_END, start);
|
|
22
|
+
if (start < 0 || endStart < 0) {
|
|
23
|
+
throw new Error('tokens.css is missing the generated figure-token block markers.');
|
|
24
|
+
}
|
|
25
|
+
const end = endStart + FIGURE_TOKEN_BLOCK_END.length;
|
|
26
|
+
const expected = renderFigureTokenCssBlock();
|
|
27
|
+
const actual = css.slice(start, end);
|
|
28
|
+
|
|
29
|
+
if (write) {
|
|
30
|
+
if (actual === expected) {
|
|
31
|
+
console.log('sync-figure-tokens: tokens.css already current');
|
|
32
|
+
} else {
|
|
33
|
+
writeFileSync(TOKENS_PATH, css.slice(0, start) + expected + css.slice(end), 'utf8');
|
|
34
|
+
console.log('sync-figure-tokens: updated styles/tokens.css');
|
|
35
|
+
}
|
|
36
|
+
} else if (actual !== expected) {
|
|
37
|
+
console.error(
|
|
38
|
+
'sync-figure-tokens: styles/tokens.css drifted from src/lib/figure-palette.mjs; ' +
|
|
39
|
+
'run npm run sync:figure-tokens --workspace package',
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
} else {
|
|
43
|
+
console.log('sync-figure-tokens: tokens.css matches the palette manifest');
|
|
44
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical figure palette contract (#161, #164).
|
|
3
|
+
*
|
|
4
|
+
* This pure manifest owns every authored RGB, host token value, and standalone
|
|
5
|
+
* SVG value. `figure.mjs` derives its exact SVG mappings and embedded theme CSS
|
|
6
|
+
* from it; `tokens.css` carries a checked generated block rendered below.
|
|
7
|
+
* Keep semantic Warm–Tol roles separate from ordinal Okabe–Ito series.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function frozenEntries(entries) {
|
|
11
|
+
return Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SEMANTIC_FIGURE_TOKENS = frozenEntries([
|
|
15
|
+
{
|
|
16
|
+
family: 'semantic', name: 'blue', token: '--fig-blue', label: 'default / lightweight',
|
|
17
|
+
authoring: '#3B6FA0', hostLight: '#3B6FA0', standaloneLight: '#3B6FA0',
|
|
18
|
+
hostDark: '#7297BB', standaloneDark: '#7297BB',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
family: 'semantic', name: 'rose', token: '--fig-rose', label: 'caution / problem',
|
|
22
|
+
authoring: '#C06858', hostLight: '#C06858', standaloneLight: '#C06858',
|
|
23
|
+
hostDark: '#D29287', standaloneDark: '#D29287',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
family: 'semantic', name: 'green', token: '--fig-green', label: 'positive outcome',
|
|
27
|
+
authoring: '#4A7E3F', hostLight: '#4A7E3F', standaloneLight: '#4A7E3F',
|
|
28
|
+
hostDark: '#7DA275', standaloneDark: '#7DA275',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
family: 'semantic', name: 'plum', token: '--fig-plum', label: 'authority / heaviest',
|
|
32
|
+
authoring: '#8A4E82', hostLight: '#8A4E82', standaloneLight: '#8A4E82',
|
|
33
|
+
hostDark: '#AB80A5', standaloneDark: '#AB80A5',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
family: 'semantic', name: 'gold', token: '--fig-gold', label: 'packaging / coordination',
|
|
37
|
+
// Export the canonical Warm–Tol gold; the light host value is deliberately
|
|
38
|
+
// darker so an essential stroke clears 3:1 against figure paper.
|
|
39
|
+
authoring: '#C09840', hostLight: '#9D7D34', standaloneLight: '#9D7D34',
|
|
40
|
+
hostDark: '#D2B575', standaloneDark: '#D2B575',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
family: 'semantic', name: 'crimson', token: '--fig-crimson', label: 'failure / severe problem',
|
|
44
|
+
authoring: '#A03838', hostLight: '#A03838', standaloneLight: '#A03838',
|
|
45
|
+
hostDark: '#BB7070', standaloneDark: '#BB7070',
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export const STRUCTURAL_FIGURE_TOKENS = frozenEntries([
|
|
50
|
+
{
|
|
51
|
+
family: 'structural', name: 'ink', token: '--fig-ink', label: 'labels / axes / outlines',
|
|
52
|
+
hostLight: 'var(--color-text)', standaloneLight: '#1A1A19',
|
|
53
|
+
hostDark: 'var(--color-text)', standaloneDark: '#E8E5DD',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
family: 'structural', name: 'paper', token: '--fig-paper', label: 'figure background',
|
|
57
|
+
hostLight: 'var(--color-bg)', standaloneLight: '#FDFCF9',
|
|
58
|
+
hostDark: 'var(--color-bg)', standaloneDark: '#1A1816',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
family: 'structural', name: 'grid', token: '--fig-grid', label: 'essential secondary structure',
|
|
62
|
+
hostLight: '#8C8981', standaloneLight: '#8C8981',
|
|
63
|
+
hostDark: '#746E67', standaloneDark: '#746E67',
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const SERIES_FIGURE_TOKENS = frozenEntries([
|
|
68
|
+
{ family: 'series', ordinal: 1, name: 'orange', token: '--series-1', authoring: '#E69F00', hostLight: '#E69F00', standaloneLight: '#E69F00', hostDark: '#E69F00', standaloneDark: '#E69F00' },
|
|
69
|
+
{ family: 'series', ordinal: 2, name: 'sky blue', token: '--series-2', authoring: '#56B4E9', hostLight: '#56B4E9', standaloneLight: '#56B4E9', hostDark: '#56B4E9', standaloneDark: '#56B4E9' },
|
|
70
|
+
{ family: 'series', ordinal: 3, name: 'bluish green', token: '--series-3', authoring: '#009E73', hostLight: '#009E73', standaloneLight: '#009E73', hostDark: '#009E73', standaloneDark: '#009E73' },
|
|
71
|
+
{ family: 'series', ordinal: 4, name: 'yellow', token: '--series-4', authoring: '#F0E442', hostLight: '#F0E442', standaloneLight: '#F0E442', hostDark: '#F0E442', standaloneDark: '#F0E442' },
|
|
72
|
+
{ family: 'series', ordinal: 5, name: 'blue', token: '--series-5', authoring: '#0072B2', hostLight: '#0072B2', standaloneLight: '#0072B2', hostDark: '#0072B2', standaloneDark: '#0072B2' },
|
|
73
|
+
{ family: 'series', ordinal: 6, name: 'vermillion', token: '--series-6', authoring: '#D55E00', hostLight: '#D55E00', standaloneLight: '#D55E00', hostDark: '#D55E00', standaloneDark: '#D55E00' },
|
|
74
|
+
{ family: 'series', ordinal: 7, name: 'reddish purple', token: '--series-7', authoring: '#CC79A7', hostLight: '#CC79A7', standaloneLight: '#CC79A7', hostDark: '#CC79A7', standaloneDark: '#CC79A7' },
|
|
75
|
+
{
|
|
76
|
+
family: 'series', ordinal: 8, name: 'neutral ink', token: '--series-8',
|
|
77
|
+
authoring: '#000000', hostLight: 'var(--fig-ink)', standaloneLight: 'var(--fig-ink)',
|
|
78
|
+
hostDark: 'var(--fig-ink)', standaloneDark: 'var(--fig-ink)',
|
|
79
|
+
// PDF export loses the distinction between categorical black and labels.
|
|
80
|
+
// Both aliases resolve through --fig-ink, so the rendered value agrees.
|
|
81
|
+
svgVariable: '--diagram-ink',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
export const FIGURE_TOKEN_DEFINITIONS = Object.freeze([
|
|
86
|
+
...SEMANTIC_FIGURE_TOKENS,
|
|
87
|
+
...STRUCTURAL_FIGURE_TOKENS,
|
|
88
|
+
...SERIES_FIGURE_TOKENS,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
export const FIGURE_COMPATIBILITY_ALIASES = frozenEntries([
|
|
92
|
+
{ token: '--diagram-ink', value: 'var(--fig-ink)' },
|
|
93
|
+
{ token: '--diagram-paper', value: 'var(--fig-paper)' },
|
|
94
|
+
{ token: '--diagram-grid', value: 'var(--fig-grid)' },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export const FIGURE_AUTHORING_COLOR_MAP = Object.freeze([
|
|
98
|
+
...SEMANTIC_FIGURE_TOKENS,
|
|
99
|
+
...SERIES_FIGURE_TOKENS,
|
|
100
|
+
].map((entry) => Object.freeze({
|
|
101
|
+
family: entry.family,
|
|
102
|
+
authoring: entry.authoring,
|
|
103
|
+
token: entry.token,
|
|
104
|
+
variable: entry.svgVariable ?? entry.token,
|
|
105
|
+
})));
|
|
106
|
+
|
|
107
|
+
export const FIGURE_TOKEN_BLOCK_START = '/* BEGIN GENERATED FIGURE TOKENS — source: src/lib/figure-palette.mjs */';
|
|
108
|
+
export const FIGURE_TOKEN_BLOCK_END = '/* END GENERATED FIGURE TOKENS */';
|
|
109
|
+
|
|
110
|
+
function declarations(entries, key, { comments = false } = {}) {
|
|
111
|
+
return entries.map((entry) => {
|
|
112
|
+
const comment = comments ? ` /* ${entry.label ?? entry.name} */` : '';
|
|
113
|
+
return ` ${entry.token}: ${entry[key]};${comment}`;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Render the checked block committed in styles/tokens.css. */
|
|
118
|
+
export function renderFigureTokenCssBlock() {
|
|
119
|
+
const light = [
|
|
120
|
+
...declarations(SEMANTIC_FIGURE_TOKENS, 'hostLight', { comments: true }),
|
|
121
|
+
...declarations(STRUCTURAL_FIGURE_TOKENS, 'hostLight', { comments: true }),
|
|
122
|
+
'',
|
|
123
|
+
' /* Canonical Okabe–Ito order. Never reorder ordinals per chart or theme. */',
|
|
124
|
+
...declarations(SERIES_FIGURE_TOKENS, 'hostLight', { comments: true }),
|
|
125
|
+
'',
|
|
126
|
+
' /* Backward-compatible aliases used by build-figures since v4.11.0. */',
|
|
127
|
+
...FIGURE_COMPATIBILITY_ALIASES.map((entry) => ` ${entry.token}: ${entry.value};`),
|
|
128
|
+
];
|
|
129
|
+
const dark = declarations(FIGURE_TOKEN_DEFINITIONS, 'hostDark');
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
FIGURE_TOKEN_BLOCK_START,
|
|
133
|
+
'/* Do not edit this block by hand. Semantic Warm–Tol roles and ordinal',
|
|
134
|
+
' * Okabe–Ito series are intentionally separate public contracts. */',
|
|
135
|
+
':root {',
|
|
136
|
+
...light,
|
|
137
|
+
'}',
|
|
138
|
+
'',
|
|
139
|
+
'@media (prefers-color-scheme: dark) {',
|
|
140
|
+
' :root:not([data-theme="light"]) {',
|
|
141
|
+
...dark.map((line) => ` ${line}`),
|
|
142
|
+
' }',
|
|
143
|
+
'}',
|
|
144
|
+
'',
|
|
145
|
+
':root[data-theme="dark"] {',
|
|
146
|
+
...dark,
|
|
147
|
+
'}',
|
|
148
|
+
FIGURE_TOKEN_BLOCK_END,
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Render minified defaults embedded into standalone generated SVGs. */
|
|
153
|
+
export function renderStandaloneFigureThemeCss() {
|
|
154
|
+
const themed = (key) => FIGURE_TOKEN_DEFINITIONS
|
|
155
|
+
.map((entry) => `${entry.token}:${entry[key]}`)
|
|
156
|
+
.join(';');
|
|
157
|
+
const aliases = FIGURE_COMPATIBILITY_ALIASES
|
|
158
|
+
.map((entry) => `${entry.token}:${entry.value}`)
|
|
159
|
+
.join(';');
|
|
160
|
+
return `:root{${themed('standaloneLight')};${aliases}}` +
|
|
161
|
+
`@media (prefers-color-scheme:dark){:root{${themed('standaloneDark')}}}`;
|
|
162
|
+
}
|
package/src/lib/figure.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Why a stylesheet, not presentation attributes: `var()` inside a presentation
|
|
12
12
|
* attribute (`fill="var(--x, …)"`) has unreliable browser support, whereas
|
|
13
13
|
* `var()` in a real <style> rule is universal. So `recolorSvg` injects an
|
|
14
|
-
* attribute-selector rule per distinct
|
|
14
|
+
* attribute-selector rule per distinct themeable color and never mutates a
|
|
15
15
|
* drawing element — the untouched `fill=""`/`stroke=""` attribute is the
|
|
16
16
|
* automatic fallback where `var()` is unsupported.
|
|
17
17
|
*
|
|
@@ -20,19 +20,22 @@
|
|
|
20
20
|
* prefers-color-scheme via the embedded <style data-diagram-theme>. Inlining
|
|
21
21
|
* puts the SVG in the host DOM, so the host's tokens.css (which tracks the
|
|
22
22
|
* in-page [data-theme] toggle) themes it. `assembleSvg` therefore STRIPS the
|
|
23
|
-
* embedded theme block so the host is the sole source of --
|
|
23
|
+
* embedded theme block so the host is the sole source of --fig-*, --series-*,
|
|
24
|
+
* and the backward-compatible --diagram-* aliases.
|
|
24
25
|
*
|
|
25
26
|
* Pure string ops only — no `node:` imports — so the module bundles for any
|
|
26
27
|
* consumer (Figure.astro imports it; fs lives in the .astro/.mjs callers).
|
|
27
28
|
*/
|
|
28
29
|
|
|
30
|
+
import {
|
|
31
|
+
FIGURE_AUTHORING_COLOR_MAP,
|
|
32
|
+
renderStandaloneFigureThemeCss,
|
|
33
|
+
} from './figure-palette.mjs';
|
|
34
|
+
|
|
29
35
|
// Standalone self-theming defaults: used only when the SVG is NOT inlined
|
|
30
36
|
// (direct file open / <img>). Hex literals because host CSS vars don't exist
|
|
31
|
-
// there.
|
|
32
|
-
export const DIAGRAM_THEME_CSS =
|
|
33
|
-
':root{--diagram-ink:#1A1A19;--diagram-paper:#FDFCF9;--diagram-grid:#B5B3AA}' +
|
|
34
|
-
'@media (prefers-color-scheme:dark){:root{' +
|
|
35
|
-
'--diagram-ink:#E8E5DD;--diagram-paper:#1A1816;--diagram-grid:#3A3632}}';
|
|
37
|
+
// there. Generated from the same pure manifest as the host tokens.css block.
|
|
38
|
+
export const DIAGRAM_THEME_CSS = renderStandaloneFigureThemeCss();
|
|
36
39
|
|
|
37
40
|
const DIAGRAM_VAR = {
|
|
38
41
|
ink: '--diagram-ink',
|
|
@@ -40,6 +43,24 @@ const DIAGRAM_VAR = {
|
|
|
40
43
|
grid: '--diagram-grid',
|
|
41
44
|
};
|
|
42
45
|
|
|
46
|
+
// Exact canonical authoring colors. Match by parsed RGB rather than source
|
|
47
|
+
// spelling so #3B6FA0, rgb(59,111,160), and Poppler's rgb(23.137%,…) all
|
|
48
|
+
// resolve to one semantic variable. Palette recognition MUST run before the
|
|
49
|
+
// neutral heuristic: base hue + fill-opacity remains themeable, whereas a
|
|
50
|
+
// pre-blended pale tint can otherwise look neutral and collapse to paper.
|
|
51
|
+
function rgbKeyFromHex(hex) {
|
|
52
|
+
return [1, 3, 5]
|
|
53
|
+
.map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16))
|
|
54
|
+
.join(',');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PALETTE_VAR_BY_RGB = new Map(
|
|
58
|
+
FIGURE_AUTHORING_COLOR_MAP.map(({ authoring, variable }) => [
|
|
59
|
+
rgbKeyFromHex(authoring),
|
|
60
|
+
variable,
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
|
|
43
64
|
/**
|
|
44
65
|
* Parse a solid SVG paint into {r,g,b} in [0,1], or null when it is not a
|
|
45
66
|
* concrete color we should remap (none / url(...) / currentColor / inherit).
|
|
@@ -86,11 +107,32 @@ export function classifyColor(value) {
|
|
|
86
107
|
return 'grid';
|
|
87
108
|
}
|
|
88
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Return the CSS custom property for a known Warm–Tol / Okabe–Ito color, or
|
|
112
|
+
* for a neutral paint classified as ink/paper/grid. Unknown saturated colors
|
|
113
|
+
* return null and stay exactly as authored.
|
|
114
|
+
*/
|
|
115
|
+
export function figureColorVariable(value) {
|
|
116
|
+
const c = parseColor(value);
|
|
117
|
+
if (c && [c.r, c.g, c.b].every((channel) => channel >= 0 && channel <= 1)) {
|
|
118
|
+
const key = [c.r, c.g, c.b].map((channel) => Math.round(channel * 255)).join(',');
|
|
119
|
+
const paletteVar = PALETTE_VAR_BY_RGB.get(key);
|
|
120
|
+
if (paletteVar) return paletteVar;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cls = classifyColor(value);
|
|
124
|
+
return cls ? DIAGRAM_VAR[cls] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
// Escape a value for use inside a CSS [attr="…"] selector string.
|
|
90
128
|
function cssAttrEscape(s) {
|
|
91
129
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
92
130
|
}
|
|
93
131
|
|
|
132
|
+
const THEME_BLOCK_RE = /<style\b[^>]*\bdata-diagram-theme\b[^>]*>[\s\S]*?<\/style>/gi;
|
|
133
|
+
const MAP_BLOCK_RE = /<style\b[^>]*\bdata-diagram-map\b[^>]*>[\s\S]*?<\/style>/gi;
|
|
134
|
+
const CURRENT_MAP_RE = /\bdata-diagram-map=(?:"2"|'2')/i;
|
|
135
|
+
|
|
94
136
|
/**
|
|
95
137
|
* Rewrite a pdftocairo SVG to be theme-aware + carry role="img". Pure and
|
|
96
138
|
* idempotent (a second pass is a no-op). `opts.optOut` short-circuits, returning
|
|
@@ -98,55 +140,63 @@ function cssAttrEscape(s) {
|
|
|
98
140
|
*
|
|
99
141
|
* Injects, right after the opening <svg> tag:
|
|
100
142
|
* <style data-diagram-theme> — standalone var defaults + @media(dark).
|
|
101
|
-
* <style data-diagram-map>
|
|
102
|
-
* distinct neutral color C.
|
|
103
|
-
* modified; the attribute
|
|
143
|
+
* <style data-diagram-map="2"> — `[fill="C"]{fill:var(--fig-X, C)}` … per
|
|
144
|
+
* distinct known-palette or neutral color C.
|
|
145
|
+
* Elements are NOT modified; the attribute
|
|
146
|
+
* stays as fallback.
|
|
104
147
|
*/
|
|
105
148
|
export function recolorSvg(svg, { optOut = false } = {}) {
|
|
106
149
|
if (typeof svg !== 'string') return svg;
|
|
107
|
-
if (optOut ||
|
|
150
|
+
if (optOut || CURRENT_MAP_RE.test(svg)) return svg;
|
|
151
|
+
if (!/<svg\b[^>]*>/i.test(svg)) return svg;
|
|
108
152
|
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
// A pre-v2 map contains only neutral roles. Remove its generated blocks and
|
|
154
|
+
// rebuild from the untouched presentation attributes so cached SVGs gain
|
|
155
|
+
// palette mappings after an upgrade without needing a source-file touch.
|
|
156
|
+
const cleanSvg = svg.includes('data-diagram-map')
|
|
157
|
+
? svg.replace(THEME_BLOCK_RE, '').replace(MAP_BLOCK_RE, '')
|
|
158
|
+
: svg;
|
|
159
|
+
|
|
160
|
+
const openMatch = cleanSvg.match(/<svg\b[^>]*>/i);
|
|
161
|
+
if (!openMatch) return svg; // defensive: generated-block cleanup never removes <svg>
|
|
111
162
|
|
|
112
163
|
// Distinct concrete colors used as fill="" / stroke="" attributes.
|
|
113
|
-
const found = new Map(); // original string →
|
|
164
|
+
const found = new Map(); // original string → CSS custom property
|
|
114
165
|
const attrRe = /\b(?:fill|stroke)="([^"]+)"/g;
|
|
115
166
|
let am;
|
|
116
|
-
while ((am = attrRe.exec(
|
|
167
|
+
while ((am = attrRe.exec(cleanSvg)) !== null) {
|
|
117
168
|
if (found.has(am[1])) continue;
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
169
|
+
const variable = figureColorVariable(am[1]);
|
|
170
|
+
if (variable) found.set(am[1], variable);
|
|
120
171
|
}
|
|
121
172
|
|
|
122
173
|
let openTag = openMatch[0];
|
|
123
174
|
if (!/\srole=/i.test(openTag)) openTag = openTag.replace(/<svg\b/i, '<svg role="img"');
|
|
124
175
|
|
|
125
|
-
// Nothing
|
|
176
|
+
// Nothing themeable to remap — still surface role="img" for a11y, no <style>.
|
|
126
177
|
if (found.size === 0) {
|
|
127
178
|
return openTag === openMatch[0]
|
|
128
|
-
?
|
|
129
|
-
:
|
|
179
|
+
? cleanSvg
|
|
180
|
+
: cleanSvg.slice(0, openMatch.index) + openTag + cleanSvg.slice(openMatch.index + openMatch[0].length);
|
|
130
181
|
}
|
|
131
182
|
|
|
132
183
|
let mapCss = '';
|
|
133
|
-
for (const [orig,
|
|
134
|
-
const v = DIAGRAM_VAR[cls];
|
|
184
|
+
for (const [orig, variable] of found) {
|
|
135
185
|
const sel = cssAttrEscape(orig);
|
|
136
186
|
mapCss +=
|
|
137
|
-
`[fill="${sel}"]{fill:var(${
|
|
138
|
-
`[stroke="${sel}"]{stroke:var(${
|
|
187
|
+
`[fill="${sel}"]{fill:var(${variable}, ${orig})}` +
|
|
188
|
+
`[stroke="${sel}"]{stroke:var(${variable}, ${orig})}`;
|
|
139
189
|
}
|
|
140
190
|
|
|
141
191
|
const styleBlocks =
|
|
142
192
|
`<style data-diagram-theme>${DIAGRAM_THEME_CSS}</style>` +
|
|
143
|
-
`<style data-diagram-map>${mapCss}</style>`;
|
|
193
|
+
`<style data-diagram-map="2">${mapCss}</style>`;
|
|
144
194
|
|
|
145
195
|
return (
|
|
146
|
-
|
|
196
|
+
cleanSvg.slice(0, openMatch.index) +
|
|
147
197
|
openTag +
|
|
148
198
|
styleBlocks +
|
|
149
|
-
|
|
199
|
+
cleanSvg.slice(openMatch.index + openMatch[0].length)
|
|
150
200
|
);
|
|
151
201
|
}
|
|
152
202
|
|
|
@@ -162,10 +212,36 @@ export function shouldInline(src) {
|
|
|
162
212
|
);
|
|
163
213
|
}
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Convert a local SVG URL into a path relative to the consumer's `public/`
|
|
217
|
+
* directory. Astro prefixes dynamic asset URLs with BASE_URL on non-root
|
|
218
|
+
* deployments, but that prefix is a URL concern and is not present on disk.
|
|
219
|
+
* Literal root URLs remain supported so this helper is backward-compatible.
|
|
220
|
+
*
|
|
221
|
+
* Reject dot segments and backslashes before the filesystem boundary. Figure
|
|
222
|
+
* sources are author-controlled, but an inline helper should never make an
|
|
223
|
+
* out-of-tree file available to `set:html` accidentally.
|
|
224
|
+
*/
|
|
225
|
+
export function publicSvgPath(src, base = '/') {
|
|
226
|
+
if (!shouldInline(src) || src.includes('\\')) return null;
|
|
227
|
+
|
|
228
|
+
const baseSegments = typeof base === 'string'
|
|
229
|
+
? base.split('/').filter(Boolean)
|
|
230
|
+
: [];
|
|
231
|
+
const normalizedBase = baseSegments.length ? `/${baseSegments.join('/')}/` : '/';
|
|
232
|
+
const withoutBase = normalizedBase !== '/' && src.startsWith(normalizedBase)
|
|
233
|
+
? src.slice(normalizedBase.length)
|
|
234
|
+
: src.replace(/^\/+/, '');
|
|
235
|
+
const segments = withoutBase.split('/');
|
|
236
|
+
|
|
237
|
+
if (!segments.length || segments.some((segment) => !segment || segment === '.' || segment === '..')) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return segments.join('/');
|
|
241
|
+
}
|
|
166
242
|
|
|
167
|
-
/** Remove the standalone self-theming block so
|
|
168
|
-
* source of
|
|
243
|
+
/** Remove the standalone self-theming block so host tokens.css is the sole
|
|
244
|
+
* source of figure variables once the SVG is inlined into the page. */
|
|
169
245
|
export function stripThemeBlock(svg) {
|
|
170
246
|
return typeof svg === 'string' ? svg.replace(THEME_BLOCK_RE, '') : svg;
|
|
171
247
|
}
|
|
@@ -204,7 +280,7 @@ function mergeSvgStyle(openTag, css) {
|
|
|
204
280
|
* Prepare a raw pipeline SVG for inline embedding in <Figure>:
|
|
205
281
|
* - strip the standalone <style data-diagram-theme> (host tokens.css themes it);
|
|
206
282
|
* - replace any pre-existing <title>/<desc> with ones from the call-site props
|
|
207
|
-
* (caption → <title>, desc
|
|
283
|
+
* (alt ?? caption → <title>, desc → <desc>) and wire aria-labelledby;
|
|
208
284
|
* - ensure role="img" and a responsive width on the root <svg>.
|
|
209
285
|
* Pure string transform; output is a trusted local build artifact (set:html).
|
|
210
286
|
*/
|
|
@@ -222,23 +298,23 @@ export function assembleSvg(raw, opts = {}) {
|
|
|
222
298
|
.replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '')
|
|
223
299
|
.replace(/<desc\b[^>]*>[\s\S]*?<\/desc>/gi, '');
|
|
224
300
|
|
|
225
|
-
|
|
226
|
-
|
|
301
|
+
// Match the raster fallback: `alt` is the short accessible name. A caption
|
|
302
|
+
// is only its fallback, while `desc` remains the optional long description.
|
|
303
|
+
const titleText = alt ?? caption ?? '';
|
|
304
|
+
const descText = desc ?? '';
|
|
227
305
|
const id = String(idBase).replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
228
306
|
|
|
229
307
|
const a11y = [];
|
|
230
|
-
const labelledby = [];
|
|
231
308
|
if (titleText) {
|
|
232
309
|
a11y.push(`<title id="${id}-title">${escapeXml(titleText)}</title>`);
|
|
233
|
-
labelledby.push(`${id}-title`);
|
|
234
310
|
}
|
|
235
311
|
if (descText) {
|
|
236
312
|
a11y.push(`<desc id="${id}-desc">${escapeXml(descText)}</desc>`);
|
|
237
|
-
labelledby.push(`${id}-desc`);
|
|
238
313
|
}
|
|
239
314
|
|
|
240
315
|
openTag = ensureSvgAttr(openTag, 'role', 'img');
|
|
241
|
-
if (
|
|
316
|
+
if (titleText) openTag = setSvgAttr(openTag, 'aria-labelledby', `${id}-title`);
|
|
317
|
+
if (descText) openTag = setSvgAttr(openTag, 'aria-describedby', `${id}-desc`);
|
|
242
318
|
openTag = mergeSvgStyle(openTag, `width:${width};max-width:100%;height:auto`);
|
|
243
319
|
|
|
244
320
|
return `${svg.slice(0, openMatch.index)}${openTag}${a11y.join('')}${body}`;
|
package/styles/tokens.css
CHANGED
|
@@ -54,15 +54,6 @@
|
|
|
54
54
|
--callout-learn: var(--warm-gold);
|
|
55
55
|
--callout-diagnostic: color-mix(in srgb, var(--warm-blue) 55%, var(--warm-green)); /* #110 DIKTA self-check — teal blend, distinct from plum Practice */
|
|
56
56
|
|
|
57
|
-
/* Diagram semantic roles (v4.11.0, #84): theme-aware TikZ→SVG figures.
|
|
58
|
-
* build-figures remaps an SVG's neutral fills/strokes to these via
|
|
59
|
-
* var(--diagram-*, <original>); <Figure> inlines the SVG so this cascade
|
|
60
|
-
* reaches it. They point at existing roles, so they auto-flip in dark mode
|
|
61
|
-
* (no dark-block edits) — ink↔text, paper↔page bg, grid↔border. */
|
|
62
|
-
--diagram-ink: var(--color-text);
|
|
63
|
-
--diagram-paper: var(--color-bg);
|
|
64
|
-
--diagram-grid: var(--color-border);
|
|
65
|
-
|
|
66
57
|
/* ===== Typography scale ===== */
|
|
67
58
|
--font-body: 'Roboto Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
68
59
|
--font-code: 'Source Code Pro Variable', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
@@ -212,3 +203,76 @@
|
|
|
212
203
|
--astro-code-token-parameter: color-mix(in srgb, var(--warm-blue) 55%, white);
|
|
213
204
|
--astro-code-token-function: color-mix(in srgb, var(--warm-gold) 65%, white);
|
|
214
205
|
}
|
|
206
|
+
|
|
207
|
+
/* BEGIN GENERATED FIGURE TOKENS — source: src/lib/figure-palette.mjs */
|
|
208
|
+
/* Do not edit this block by hand. Semantic Warm–Tol roles and ordinal
|
|
209
|
+
* Okabe–Ito series are intentionally separate public contracts. */
|
|
210
|
+
:root {
|
|
211
|
+
--fig-blue: #3B6FA0; /* default / lightweight */
|
|
212
|
+
--fig-rose: #C06858; /* caution / problem */
|
|
213
|
+
--fig-green: #4A7E3F; /* positive outcome */
|
|
214
|
+
--fig-plum: #8A4E82; /* authority / heaviest */
|
|
215
|
+
--fig-gold: #9D7D34; /* packaging / coordination */
|
|
216
|
+
--fig-crimson: #A03838; /* failure / severe problem */
|
|
217
|
+
--fig-ink: var(--color-text); /* labels / axes / outlines */
|
|
218
|
+
--fig-paper: var(--color-bg); /* figure background */
|
|
219
|
+
--fig-grid: #8C8981; /* essential secondary structure */
|
|
220
|
+
|
|
221
|
+
/* Canonical Okabe–Ito order. Never reorder ordinals per chart or theme. */
|
|
222
|
+
--series-1: #E69F00; /* orange */
|
|
223
|
+
--series-2: #56B4E9; /* sky blue */
|
|
224
|
+
--series-3: #009E73; /* bluish green */
|
|
225
|
+
--series-4: #F0E442; /* yellow */
|
|
226
|
+
--series-5: #0072B2; /* blue */
|
|
227
|
+
--series-6: #D55E00; /* vermillion */
|
|
228
|
+
--series-7: #CC79A7; /* reddish purple */
|
|
229
|
+
--series-8: var(--fig-ink); /* neutral ink */
|
|
230
|
+
|
|
231
|
+
/* Backward-compatible aliases used by build-figures since v4.11.0. */
|
|
232
|
+
--diagram-ink: var(--fig-ink);
|
|
233
|
+
--diagram-paper: var(--fig-paper);
|
|
234
|
+
--diagram-grid: var(--fig-grid);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@media (prefers-color-scheme: dark) {
|
|
238
|
+
:root:not([data-theme="light"]) {
|
|
239
|
+
--fig-blue: #7297BB;
|
|
240
|
+
--fig-rose: #D29287;
|
|
241
|
+
--fig-green: #7DA275;
|
|
242
|
+
--fig-plum: #AB80A5;
|
|
243
|
+
--fig-gold: #D2B575;
|
|
244
|
+
--fig-crimson: #BB7070;
|
|
245
|
+
--fig-ink: var(--color-text);
|
|
246
|
+
--fig-paper: var(--color-bg);
|
|
247
|
+
--fig-grid: #746E67;
|
|
248
|
+
--series-1: #E69F00;
|
|
249
|
+
--series-2: #56B4E9;
|
|
250
|
+
--series-3: #009E73;
|
|
251
|
+
--series-4: #F0E442;
|
|
252
|
+
--series-5: #0072B2;
|
|
253
|
+
--series-6: #D55E00;
|
|
254
|
+
--series-7: #CC79A7;
|
|
255
|
+
--series-8: var(--fig-ink);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
:root[data-theme="dark"] {
|
|
260
|
+
--fig-blue: #7297BB;
|
|
261
|
+
--fig-rose: #D29287;
|
|
262
|
+
--fig-green: #7DA275;
|
|
263
|
+
--fig-plum: #AB80A5;
|
|
264
|
+
--fig-gold: #D2B575;
|
|
265
|
+
--fig-crimson: #BB7070;
|
|
266
|
+
--fig-ink: var(--color-text);
|
|
267
|
+
--fig-paper: var(--color-bg);
|
|
268
|
+
--fig-grid: #746E67;
|
|
269
|
+
--series-1: #E69F00;
|
|
270
|
+
--series-2: #56B4E9;
|
|
271
|
+
--series-3: #009E73;
|
|
272
|
+
--series-4: #F0E442;
|
|
273
|
+
--series-5: #0072B2;
|
|
274
|
+
--series-6: #D55E00;
|
|
275
|
+
--series-7: #CC79A7;
|
|
276
|
+
--series-8: var(--fig-ink);
|
|
277
|
+
}
|
|
278
|
+
/* END GENERATED FIGURE TOKENS */
|