@ponchia/ui 0.5.0 → 0.6.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +28 -5
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +315 -45
  7. package/behaviors/carousel.js +17 -16
  8. package/behaviors/combobox.js +47 -16
  9. package/behaviors/command.js +18 -15
  10. package/behaviors/connectors.js +4 -5
  11. package/behaviors/crosshair.js +4 -5
  12. package/behaviors/dialog.js +3 -2
  13. package/behaviors/disclosure.js +3 -2
  14. package/behaviors/dismissible.js +3 -2
  15. package/behaviors/forms.js +41 -13
  16. package/behaviors/glyph.js +4 -5
  17. package/behaviors/internal.js +47 -0
  18. package/behaviors/legend.js +23 -2
  19. package/behaviors/menu.js +3 -2
  20. package/behaviors/popover.js +78 -7
  21. package/behaviors/spotlight.js +4 -5
  22. package/behaviors/table.js +39 -12
  23. package/behaviors/tabs.js +14 -14
  24. package/behaviors/theme.js +5 -3
  25. package/behaviors/toast.js +13 -1
  26. package/classes/classes.json +1857 -0
  27. package/classes/index.d.ts +28 -13
  28. package/classes/index.js +34 -18
  29. package/classes/vscode.css-custom-data.json +12 -0
  30. package/connectors/index.d.ts +189 -69
  31. package/connectors/index.d.ts.map +1 -0
  32. package/connectors/index.js +120 -24
  33. package/css/app.css +43 -13
  34. package/css/base.css +15 -10
  35. package/css/connectors.css +17 -0
  36. package/css/content.css +7 -1
  37. package/css/dataviz.css +5 -1
  38. package/css/disclosure.css +38 -6
  39. package/css/dots.css +57 -0
  40. package/css/feedback.css +60 -2
  41. package/css/forms.css +42 -1
  42. package/css/legend.css +11 -7
  43. package/css/marks.css +38 -8
  44. package/css/motion.css +24 -44
  45. package/css/navigation.css +7 -0
  46. package/css/overlay.css +31 -1
  47. package/css/primitives.css +91 -5
  48. package/css/report.css +40 -63
  49. package/css/site.css +16 -2
  50. package/css/sources.css +43 -1
  51. package/css/spotlight.css +1 -1
  52. package/css/tokens.css +36 -1
  53. package/css/workbench.css +1 -1
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -1
  56. package/dist/css/app.css +1 -1
  57. package/dist/css/base.css +1 -1
  58. package/dist/css/connectors.css +1 -1
  59. package/dist/css/content.css +1 -1
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/forms.css +1 -1
  64. package/dist/css/legend.css +1 -1
  65. package/dist/css/marks.css +1 -1
  66. package/dist/css/motion.css +1 -1
  67. package/dist/css/navigation.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -1
  71. package/dist/css/site.css +1 -1
  72. package/dist/css/sources.css +1 -1
  73. package/dist/css/spotlight.css +1 -1
  74. package/dist/css/tokens.css +1 -1
  75. package/dist/css/workbench.css +1 -1
  76. package/docs/adr/0003-theme-model.md +1 -1
  77. package/docs/annotations.md +94 -14
  78. package/docs/architecture.md +50 -6
  79. package/docs/contrast.md +116 -92
  80. package/docs/d2.md +195 -0
  81. package/docs/legends.md +18 -2
  82. package/docs/marks.md +9 -2
  83. package/docs/mermaid.md +152 -0
  84. package/docs/reference.md +78 -22
  85. package/docs/reporting.md +395 -57
  86. package/docs/sources.md +27 -0
  87. package/docs/stability.md +9 -2
  88. package/docs/usage.md +101 -4
  89. package/docs/vega.md +225 -0
  90. package/docs/workbench.md +7 -1
  91. package/glyphs/glyphs.js +6 -4
  92. package/llms.txt +139 -14
  93. package/package.json +50 -12
  94. package/qwik/index.d.ts +42 -59
  95. package/qwik/index.d.ts.map +1 -0
  96. package/qwik/index.js +55 -3
  97. package/react/index.d.ts +39 -61
  98. package/react/index.d.ts.map +1 -0
  99. package/react/index.js +57 -3
  100. package/solid/index.d.ts +64 -61
  101. package/solid/index.d.ts.map +1 -0
  102. package/solid/index.js +60 -3
  103. package/tokens/d2.d.ts +38 -0
  104. package/tokens/d2.js +71 -0
  105. package/tokens/d2.json +43 -0
  106. package/tokens/index.d.ts +5 -5
  107. package/tokens/index.js +15 -1
  108. package/tokens/index.json +9 -0
  109. package/tokens/mermaid.d.ts +23 -0
  110. package/tokens/mermaid.js +181 -0
  111. package/tokens/mermaid.json +163 -0
  112. package/tokens/resolved.json +45 -1
  113. package/tokens/skins.js +3 -2
  114. package/tokens/tokens.dtcg.json +26 -0
  115. package/tokens/vega.d.ts +34 -0
  116. package/tokens/vega.js +155 -0
  117. package/tokens/vega.json +179 -0
@@ -61,7 +61,7 @@ on top of the CSS, none of which require a framework commitment**:
61
61
  not in three places (the two CSS dark blocks are now identical by
62
62
  construction), resolving the duplication ADR-0003 flagged. The CSS-only
63
63
  presets (density / contrast / OLED) stay hand-authored below a marker and are
64
- preserved across regeneration. `scripts/check-tokens.mjs` fails CI if
64
+ preserved across regeneration. `scripts/check-fresh.mjs` fails CI if
65
65
  `css/tokens.css` drifts from the model.
66
66
  - **classes/** — `cls` is the flat registry; recipes only emit from it;
67
67
  `scripts/check-classes.mjs` enforces a bidirectional match with the
@@ -79,6 +79,51 @@ on top of the CSS, none of which require a framework commitment**:
79
79
  - **react/** / **solid/** / **qwik/** — optional lifecycle adapters over `behaviors/`.
80
80
  They do not define markup, own state, or fork behavior logic; they only run
81
81
  the vanilla initializers on mount and cleanup on unmount/dispose.
82
+ - **`css/analytical.css` — the analytical roll-up.** This convenience file
83
+ `@import`s exactly **seven** analytical-figure leaves: `annotations`,
84
+ `legend`, `marks`, `connectors`, `spotlight`, `crosshair`, and `selection`.
85
+ The adjacent opt-in leaves — `sources`, `state`, `generated`, `workbench`,
86
+ and `command` — are report/tooling/trust surfaces that are intentionally
87
+ **not** part of the analytical roll-up and must be imported individually.
88
+ Importing `analytical.css` does not pull in any of those five.
89
+ - **Root export (`.`) is CSS-only.** `exports["."]` resolves to the CSS
90
+ bundle (`dist/bronto.css`). It is a CSS side-effect import for CSS-aware
91
+ bundlers (`@import '@ponchia/ui'` in CSS, or a side-effect
92
+ `import '@ponchia/ui'` in Vite/Astro/SvelteKit). There is no runtime JS at
93
+ the package root — Node/runtime JS imports of `.` are not supported. All JS
94
+ entrypoints are explicit subpaths (`/behaviors`, `/classes`, `/tokens`,
95
+ `/glyphs`, `/react`, `/solid`, `/qwik`, `/skins`, `/charts`). This is a
96
+ permanent, intentional contract.
97
+
98
+ ## Repository layout
99
+
100
+ The repo root mixes five kinds of directory that look alike but follow very
101
+ different rules. Two distinctions matter most: several are **path-frozen
102
+ published subpaths** — the directory name _is_ the public import specifier
103
+ (`@ponchia/ui/react` resolves to `./react/`), so they cannot be moved or
104
+ renamed — and several are **generated** and must never be hand-edited (a
105
+ generator overwrites them and a drift gate fails CI).
106
+
107
+ | Path | Kind | Edit here? | Notes |
108
+ | --- | --- | --- | --- |
109
+ | `css/` | source | yes | The framework. Hand-authored `@layer bronto` CSS. (`css/tokens.css` palette blocks and `css/generated.css` are generated — see below.) |
110
+ | `tokens/index.js` | source | yes | The single source of truth for token **values** (`cssVars`). |
111
+ | `classes/index.js`, `behaviors/`, `annotations/`, `connectors/`, `react/`, `solid/`, `qwik/`, `glyphs/`, `shiki/` | source · published-subpath (path-frozen) | yes — but **do not move** | Authored ESM shipped as-is; the dir name is the public import path. The `.d.ts` beside them are generated/drift-checked: `connectors`/`annotations`/`react`/`solid`/`qwik` are emitted from JSDoc by `tsc` (`npm run dts:emit`), `classes`/`tokens`/`glyphs` from the runtime; only `behaviors/index.d.ts` is still hand-maintained (its barrel + destructured-param shape emit poorly), guarded by `check-behaviors`. |
112
+ | `dist/` | generated | no | Build of `css/` (`npm run dist:build`); byte-checked by `check:dist`. |
113
+ | `tokens/index.json`, `tokens/resolved.json`, `tokens/tokens.dtcg.json`, `tokens/charts.json`, `classes/index.d.ts`, `tokens/index.d.ts`, `tokens/{skins,charts}.d.ts`, `glyphs/glyphs.d.ts`, `classes/vscode.css-custom-data.json`, `docs/reference.md` | generated | no | Committed build artifacts; regenerate with `npm run prepack`, never hand-edit. Drift-checked in `npm run check`. |
114
+ | `fonts/` | vendored | — | The Doto webfont (woff2) + its OFL license. |
115
+ | `scripts/` | tooling | yes | `gen-*` regenerate artifacts, `check-*` are the drift/contract gates wired into `npm run check`, plus `build-dist`, `serve`, `size-report`. |
116
+ | `docs/` | source (mostly) | yes | Hand-authored docs + ADRs; the curated subset in `package.json` `files` ships in the tarball. `docs/reference.md` is generated. |
117
+ | `demo/`, `test/`, `examples/` | fixtures | yes | The self-driving demo/showcase, the unit + Playwright e2e suite, and consumer example apps built against the packed tarball. |
118
+ | `.github/`, `*.config.mjs`, `.prettierrc`, `.stylelintrc.json`, `tsconfig.json`, `.editorconfig` | config | yes | CI workflows and tool config. |
119
+ | `package.json`, `llms.txt`, `CHANGELOG.md`, `MIGRATIONS.json`, `README.md`, `CONTRIBUTING.md`, `ROADMAP.md`, `LICENSE` | meta | yes | Manifest, the agent entrypoint, the curated changelog, the rename map, and project docs. |
120
+
121
+ The **path-frozen** dirs are the cost of zero-build, path-stable publishing:
122
+ `files` map 1:1 to published paths and the consumer's own bundler tree-shakes
123
+ the ESM, so there is no `src/` indirection (and no JS bundler — see the
124
+ distribution decision below). **Generated** files are regenerated from their
125
+ source and policed by a drift gate — edit the source, run the generator, commit
126
+ the result.
82
127
 
83
128
  ## Drift control
84
129
 
@@ -89,12 +134,11 @@ gating" below), so a version that fails any invariant never reaches npm.
89
134
  | Invariant | Enforced by |
90
135
  | ----------------------------------------------- | ------------------- |
91
136
  | exports / import graph / `files` consistent | `check-exports.mjs` |
92
- | `tokens.css` `tokens/index.js` `.json` | `check-tokens.mjs` |
137
+ | pure generated mirrors fresh — `tokens.css`/`index.json`, `dtcg.json`, `resolved.json`, `classes`/`tokens` `.d.ts`, `reference.md`, vscode data — each byte-equal to its generator (registry: `scripts/lib/artifacts.mjs`) | `check-fresh.mjs` |
93
138
  | `classes` `cls` ⇄ `.ui-*` selectors | `check-classes.mjs` |
94
- | `classes`/`tokens` `.d.ts` JS runtime (exact) | `check-dts.mjs` |
95
- | `annotations`/`connectors` hand-written `.d.ts` ⇄ exports | `check-helpers-dts.mjs` |
139
+ | `connectors`/`annotations`/`react`/`solid`/`qwik` `.d.ts` (+ maps) == fresh `tsc` emit of their JSDoc | `check-dts-emit.mjs` |
140
+ | `behaviors/index.d.ts` ⇄ `behaviors/*` exports (the one hand-maintained leaf `.d.ts`) | `check-behaviors.mjs` |
96
141
  | legend swatch colours ⊆ `charts.js` · opt-in | `check-legend.mjs` |
97
- | `tokens.dtcg.json` ⇄ token model | `check-dtcg.mjs` |
98
142
  | color tokens tiered · no raw chromatic color in components | `check-color-policy.mjs` |
99
143
  | `css/skins.css` ⇄ `tokens/skins.js` · colorways opt-in | `check-skins.mjs` |
100
144
  | every shipped colorway accent meets its WCAG floor | `check-contrast.mjs` |
@@ -116,7 +160,7 @@ payload contract, raised only intentionally with a CHANGELOG note.
116
160
  `test/types.test-d.ts`, whose `@ts-expect-error`s would fail to compile
117
161
  if the generated literal `cls`/token types stopped rejecting typos —
118
162
  so the *value* of the generated `.d.ts` is itself gated, not just their
119
- freshness (`check-dts`).
163
+ freshness (`check-fresh`).
120
164
 
121
165
  ## Release gating
122
166
 
package/docs/contrast.md CHANGED
@@ -40,51 +40,57 @@ Overall: **all contractual pairings meet their floor ✅**.
40
40
 
41
41
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
42
42
  | --- | --- | --- | --- | --- | --- | --- |
43
- | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 17.98:1 | Lc 99 | ✅ pass |
44
- | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 19.80:1 | Lc 106 | ✅ pass |
45
- | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 16.74:1 | Lc 94 | ✅ pass |
46
- | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.16:1 | Lc 91 | ✅ pass |
47
- | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 12.29:1 | Lc 98 | ✅ pass |
48
- | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 10.39:1 | Lc 87 | ✅ pass |
49
- | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.09:1 | Lc 71 | ✅ pass |
50
- | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.60:1 | Lc 78 | ✅ pass |
51
- | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.74:1 | Lc 67 | ✅ pass |
52
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.32:1 | Lc 75 | ✅ pass |
53
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.96:1 | Lc 82 | ✅ pass |
54
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.18:1 | Lc 79 | pass |
55
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 67 | pass |
56
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.18:1 | Lc 73 | ✅ pass |
57
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 67 | ✅ pass |
58
- | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 5.04:1 | Lc 75 | ✅ pass |
59
- | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 5.60:1 | Lc 78 | ✅ pass |
60
- | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.21:1 | Lc 79 | ✅ pass |
61
- | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 5.77:1 | Lc 78 | ✅ pass |
62
- | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.39:1 | Lc 47 | ℹ️ not gated |
43
+ | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 17.98:1 | Lc 99.2 | ✅ pass |
44
+ | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 19.80:1 | Lc 105.8 | ✅ pass |
45
+ | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 16.74:1 | Lc 94.5 | ✅ pass |
46
+ | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.16:1 | Lc 91.4 | ✅ pass |
47
+ | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 12.29:1 | Lc 98.1 | ✅ pass |
48
+ | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 10.39:1 | Lc 86.7 | ✅ pass |
49
+ | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.09:1 | Lc 71.4 | ✅ pass |
50
+ | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.60:1 | Lc 78.0 | ✅ pass |
51
+ | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.74:1 | Lc 66.7 | ✅ pass |
52
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.32:1 | Lc 75.4 | ✅ pass |
53
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.96:1 | Lc 82.1 | ✅ pass |
54
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 5.91:1 | Lc 71.0 | ℹ️ not gated |
55
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 6.31:1 | Lc 75.3 | ℹ️ not gated |
56
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.18:1 | Lc 78.9 | ✅ pass |
57
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.18:1 | Lc 78.9 | ✅ pass |
58
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 66.8 | ✅ pass |
59
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.18:1 | Lc 73.5 | ✅ pass |
60
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 66.8 | ✅ pass |
61
+ | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 5.04:1 | Lc 74.6 | ✅ pass |
62
+ | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 5.60:1 | Lc 77.8 | pass |
63
+ | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.21:1 | Lc 78.9 | ✅ pass |
64
+ | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 5.77:1 | Lc 78.3 | ✅ pass |
65
+ | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.39:1 | Lc 47.1 | ℹ️ not gated |
63
66
 
64
67
  ## Dark theme
65
68
 
66
69
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
67
70
  | --- | --- | --- | --- | --- | --- | --- |
68
- | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 15.01:1 | Lc 91 | ✅ pass |
69
- | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 13.65:1 | Lc 90 | ✅ pass |
70
- | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 12.44:1 | Lc 89 | ✅ pass |
71
- | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.20:1 | Lc 73 | ✅ pass |
72
- | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 10.19:1 | Lc 72 | ✅ pass |
73
- | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 9.28:1 | Lc 71 | ✅ pass |
74
- | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 7.16:1 | Lc 50 | ✅ pass |
75
- | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 6.52:1 | Lc 49 | ✅ pass |
76
- | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 5.94:1 | Lc 48 | ✅ pass |
77
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.14:1 | Lc 45 | ✅ pass |
78
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 5.58:1 | Lc 44 | ✅ pass |
79
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.95:1 | Lc 43 | pass |
80
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.31:1 | Lc 40 | pass |
81
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 4.83:1 | Lc 39 | ✅ pass |
82
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.31:1 | Lc 40 | ✅ pass |
83
- | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 7.58:1 | Lc 57 | ✅ pass |
84
- | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 9.29:1 | Lc 67 | ✅ pass |
85
- | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 5.23:1 | Lc 42 | ✅ pass |
86
- | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 7.33:1 | Lc 55 | ✅ pass |
87
- | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.29:1 | Lc 15 | ℹ️ not gated |
71
+ | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 15.01:1 | Lc 91.1 | ✅ pass |
72
+ | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 13.65:1 | Lc 90.1 | ✅ pass |
73
+ | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 12.44:1 | Lc 88.9 | ✅ pass |
74
+ | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.20:1 | Lc 72.7 | ✅ pass |
75
+ | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 10.19:1 | Lc 71.7 | ✅ pass |
76
+ | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 9.28:1 | Lc 70.6 | ✅ pass |
77
+ | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 7.16:1 | Lc 50.3 | ✅ pass |
78
+ | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 6.52:1 | Lc 49.3 | ✅ pass |
79
+ | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 5.94:1 | Lc 48.1 | ✅ pass |
80
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.14:1 | Lc 44.9 | ✅ pass |
81
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 5.58:1 | Lc 43.9 | ✅ pass |
82
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 2.53:1 | Lc 43.8 | ℹ️ not gated |
83
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 2.75:1 | Lc 49.1 | ℹ️ not gated |
84
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.95:1 | Lc 42.9 | ✅ pass |
85
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.95:1 | Lc 42.9 | ✅ pass |
86
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.31:1 | Lc 40.0 | ✅ pass |
87
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 4.83:1 | Lc 39.0 | ✅ pass |
88
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.31:1 | Lc 40.0 | ✅ pass |
89
+ | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 7.58:1 | Lc 56.6 | ✅ pass |
90
+ | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 9.29:1 | Lc 66.6 | pass |
91
+ | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 5.23:1 | Lc 41.5 | ✅ pass |
92
+ | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 7.33:1 | Lc 54.8 | ✅ pass |
93
+ | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.29:1 | Lc 14.5 | ℹ️ not gated |
88
94
 
89
95
  ## Display colorways (skins)
90
96
 
@@ -101,67 +107,85 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
101
107
 
102
108
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
103
109
  | --- | --- | --- | --- | --- | --- | --- |
104
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.75:1 | Lc 79 | ✅ pass |
105
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 7.44:1 | Lc 86 | ✅ pass |
106
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.66:1 | Lc 83 | pass |
107
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.14:1 | Lc 71 | pass |
108
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.66:1 | Lc 78 | ✅ pass |
109
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.14:1 | Lc 71 | ✅ pass |
110
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.75:1 | Lc 78.9 | ✅ pass |
111
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 7.44:1 | Lc 85.6 | ✅ pass |
112
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 6.31:1 | Lc 74.5 | ℹ️ not gated |
113
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 6.74:1 | Lc 78.8 | ℹ️ not gated |
114
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.66:1 | Lc 83.3 | ✅ pass |
115
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.66:1 | Lc 83.3 | ✅ pass |
116
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.14:1 | Lc 71.4 | ✅ pass |
117
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.66:1 | Lc 78.0 | ✅ pass |
118
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.14:1 | Lc 71.4 | ✅ pass |
110
119
 
111
120
  ### Amber CRT — dark
112
121
 
113
122
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
114
123
  | --- | --- | --- | --- | --- | --- | --- |
115
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 11.57:1 | Lc 75 | ✅ pass |
116
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 10.52:1 | Lc 74 | ✅ pass |
117
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.88:1 | Lc 72 | pass |
118
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.60:1 | Lc 70 | pass |
119
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 9.64:1 | Lc 69 | ✅ pass |
120
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 10.60:1 | Lc 70 | ✅ pass |
124
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 11.57:1 | Lc 74.8 | ✅ pass |
125
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 10.52:1 | Lc 73.8 | ✅ pass |
126
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.34:1 | Lc 15.1 | ℹ️ not gated |
127
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.46:1 | Lc 20.3 | ℹ️ not gated |
128
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.88:1 | Lc 71.6 | ✅ pass |
129
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 11.88:1 | Lc 71.6 | ✅ pass |
130
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.60:1 | Lc 69.9 | ✅ pass |
131
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 9.64:1 | Lc 68.9 | ✅ pass |
132
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 10.60:1 | Lc 69.9 | ✅ pass |
121
133
 
122
134
  ### E-ink — light
123
135
 
124
136
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
125
137
  | --- | --- | --- | --- | --- | --- | --- |
126
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.23:1 | Lc 93 | ✅ pass |
127
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 13.47:1 | Lc 100 | ✅ pass |
128
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.74:1 | Lc 101 | pass |
129
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.66:1 | Lc 90 | pass |
130
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.74:1 | Lc 97 | ✅ pass |
131
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 10.66:1 | Lc 90 | ✅ pass |
138
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.23:1 | Lc 93.3 | ✅ pass |
139
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 13.47:1 | Lc 99.9 | ✅ pass |
140
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 11.43:1 | Lc 88.8 | ℹ️ not gated |
141
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 12.22:1 | Lc 93.2 | ℹ️ not gated |
142
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.74:1 | Lc 100.6 | ✅ pass |
143
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 11.74:1 | Lc 100.6 | ✅ pass |
144
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.66:1 | Lc 90.4 | ✅ pass |
145
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.74:1 | Lc 97.0 | ✅ pass |
146
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 10.66:1 | Lc 90.4 | ✅ pass |
132
147
 
133
148
  ### E-ink — dark
134
149
 
135
150
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
136
151
  | --- | --- | --- | --- | --- | --- | --- |
137
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.47:1 | Lc 79 | ✅ pass |
138
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.35:1 | Lc 78 | ✅ pass |
139
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 12.86:1 | Lc 76 | pass |
140
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 11.48:1 | Lc 74 | pass |
141
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 10.44:1 | Lc 73 | ✅ pass |
142
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 11.48:1 | Lc 74 | ✅ pass |
152
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.47:1 | Lc 79.1 | ✅ pass |
153
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.35:1 | Lc 78.1 | ✅ pass |
154
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.25:1 | Lc 11.0 | ℹ️ not gated |
155
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.35:1 | Lc 16.3 | ℹ️ not gated |
156
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 12.86:1 | Lc 75.6 | ✅ pass |
157
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 12.86:1 | Lc 75.6 | ✅ pass |
158
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 11.48:1 | Lc 74.1 | ✅ pass |
159
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 10.44:1 | Lc 73.1 | ✅ pass |
160
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 11.48:1 | Lc 74.1 | ✅ pass |
143
161
 
144
162
  ### Phosphor Green — light
145
163
 
146
164
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
147
165
  | --- | --- | --- | --- | --- | --- | --- |
148
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.22:1 | Lc 77 | ✅ pass |
149
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.85:1 | Lc 83 | ✅ pass |
150
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.19:1 | Lc 81 | pass |
151
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 69 | pass |
152
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.19:1 | Lc 75 | ✅ pass |
153
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 69 | ✅ pass |
166
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.22:1 | Lc 76.6 | ✅ pass |
167
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.85:1 | Lc 83.3 | ✅ pass |
168
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 5.81:1 | Lc 72.2 | ℹ️ not gated |
169
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 6.21:1 | Lc 76.5 | ℹ️ not gated |
170
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.19:1 | Lc 80.7 | ✅ pass |
171
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.19:1 | Lc 80.7 | ✅ pass |
172
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 68.6 | ✅ pass |
173
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.19:1 | Lc 75.3 | ✅ pass |
174
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 68.6 | ✅ pass |
154
175
 
155
176
  ### Phosphor Green — dark
156
177
 
157
178
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
158
179
  | --- | --- | --- | --- | --- | --- | --- |
159
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.97:1 | Lc 82 | ✅ pass |
160
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.80:1 | Lc 81 | ✅ pass |
161
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 13.75:1 | Lc 80 | pass |
162
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 12.27:1 | Lc 79 | pass |
163
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.16:1 | Lc 78 | ✅ pass |
164
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 12.27:1 | Lc 79 | ✅ pass |
180
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.97:1 | Lc 81.8 | ✅ pass |
181
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.80:1 | Lc 80.8 | ✅ pass |
182
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.20:1 | Lc 8.4 | ℹ️ not gated |
183
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.30:1 | Lc 13.7 | ℹ️ not gated |
184
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 13.75:1 | Lc 79.7 | ✅ pass |
185
+ | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 13.75:1 | Lc 79.7 | ✅ pass |
186
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 12.27:1 | Lc 78.5 | ✅ pass |
187
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.16:1 | Lc 77.5 | ✅ pass |
188
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 12.27:1 | Lc 78.5 | ✅ pass |
165
189
 
166
190
  ## Data-viz palette (advisory)
167
191
 
@@ -178,27 +202,27 @@ the brand accent.
178
202
 
179
203
  | Series | Colour | Ratio _(advisory)_ | APCA _(advisory)_ |
180
204
  | --- | --- | --- | --- |
181
- | 1 _(accent)_ | `#d71921` | 4.71:1 | Lc 67 |
182
- | 2 | `#e69f00` | 2.05:1 | Lc 37 |
183
- | 3 | `#56b4e9` | 2.10:1 | Lc 39 |
184
- | 4 | `#009e73` | 3.11:1 | Lc 54 |
185
- | 5 | `#f0e442` | 1.20:1 | Lc 9 |
186
- | 6 | `#0072b2` | 4.71:1 | Lc 68 |
187
- | 7 | `#cc79a7` | 2.78:1 | Lc 51 |
188
- | 8 | `#4d5358` | 7.08:1 | Lc 80 |
205
+ | 1 _(accent)_ | `#d71921` | 4.71:1 | Lc 66.8 |
206
+ | 2 | `#e69f00` | 2.05:1 | Lc 37.4 |
207
+ | 3 | `#56b4e9` | 2.10:1 | Lc 38.6 |
208
+ | 4 | `#009e73` | 3.11:1 | Lc 54.4 |
209
+ | 5 | `#f0e442` | 1.20:1 | Lc 9.1 |
210
+ | 6 | `#0072b2` | 4.71:1 | Lc 68.4 |
211
+ | 7 | `#cc79a7` | 2.78:1 | Lc 50.6 |
212
+ | 8 | `#4d5358` | 7.08:1 | Lc 80.4 |
189
213
 
190
214
  ### Dark theme — categorical vs `--bg`
191
215
 
192
216
  | Series | Colour | Ratio _(advisory)_ | APCA _(advisory)_ |
193
217
  | --- | --- | --- | --- |
194
- | 1 _(accent)_ | `#ff3b41` | 5.31:1 | Lc 40 |
195
- | 2 | `#e69f00` | 8.32:1 | Lc 58 |
196
- | 3 | `#56b4e9` | 8.12:1 | Lc 56 |
197
- | 4 | `#009e73` | 5.48:1 | Lc 40 |
198
- | 5 | `#f0e442` | 14.17:1 | Lc 87 |
199
- | 6 | `#0072b2` | 3.61:1 | Lc 26 |
200
- | 7 | `#cc79a7` | 6.12:1 | Lc 44 |
201
- | 8 | `#4d5358` | 2.40:1 | Lc 14 |
218
+ | 1 _(accent)_ | `#ff3b41` | 5.31:1 | Lc 40.0 |
219
+ | 2 | `#e69f00` | 8.32:1 | Lc 57.5 |
220
+ | 3 | `#56b4e9` | 8.12:1 | Lc 56.3 |
221
+ | 4 | `#009e73` | 5.48:1 | Lc 40.0 |
222
+ | 5 | `#f0e442` | 14.17:1 | Lc 87.4 |
223
+ | 6 | `#0072b2` | 3.61:1 | Lc 26.1 |
224
+ | 7 | `#cc79a7` | 6.12:1 | Lc 43.9 |
225
+ | 8 | `#4d5358` | 2.40:1 | Lc 14.4 |
202
226
 
203
227
  ## Scope & caveats
204
228
 
package/docs/d2.md ADDED
@@ -0,0 +1,195 @@
1
+ # D2
2
+
3
+ [D2](https://d2lang.com) compiles a diagram script to **SVG** (Go CLI or the
4
+ `@terrastruct/d2` WASM build). Like the [Mermaid integration](./mermaid.md),
5
+ `@ponchia/ui` doesn't render diagrams — it **themes** them from your tokens and
6
+ lets you **annotate** the result. Two things ship:
7
+
8
+ - `@ponchia/ui/d2` — helpers that produce on-brand D2 theme overrides.
9
+ - `@ponchia/ui/d2.json` — the resolved slot → hex maps, for any consumer.
10
+
11
+ D2 stays the consumer's renderer; this is config only, and D2 is **not** a
12
+ dependency.
13
+
14
+ ## Theme a diagram
15
+
16
+ D2's theme is a compact set of named colour slots. Override them with the bronto
17
+ palette by prepending a `vars` block to your diagram source — `brontoD2Vars()`
18
+ returns exactly that block (both light and dark):
19
+
20
+ ```js
21
+ import { brontoD2Vars } from '@ponchia/ui/d2';
22
+
23
+ const source = brontoD2Vars() + `
24
+ api -> db: query
25
+ db: Store { shape: cylinder }
26
+ `;
27
+ // → render `source` with the D2 CLI or @terrastruct/d2
28
+ ```
29
+
30
+ If you render through D2's Go or WASM API instead of source, pass the slot map to
31
+ its `themeOverrides` / `darkThemeOverrides`:
32
+
33
+ ```js
34
+ import { brontoD2Overrides } from '@ponchia/ui/d2';
35
+ brontoD2Overrides('dark'); // { N1: '#e6e6e6', B1: '#555555', … }
36
+ ```
37
+
38
+ For a build step or non-JS host, read `@ponchia/ui/d2.json` directly.
39
+
40
+ > **file:// portability.** A report opened from disk (`file://`) cannot
41
+ > `import` the `@ponchia/ui/d2` module **nor** `fetch('…/d2.json')` — the browser
42
+ > blocks both across the `null`/file origin (CORS), exactly as with
43
+ > [Vega](./vega.md#from-a-cdn-no-bundler) and [Mermaid](./mermaid.md#theme-a-diagram).
44
+ > This is rarely an issue for D2, whose native path is **build-time
45
+ > pre-rendering** to a frozen SVG (no client runtime — see below), but if you do
46
+ > theme D2 in the browser over `file://`, **inline** the slot map (paste the
47
+ > object from `d2.json`) rather than importing it. Over `http(s)` the
48
+ > `import`/`fetch` forms both work.
49
+
50
+ ### Why resolved colours, not `var(--x)`
51
+
52
+ D2 compiles to a **frozen SVG** in Go/WASM — there is no client CSS cascade, so a
53
+ `var(--accent)` could never resolve. The maps therefore ship **resolved hex per
54
+ theme**, projected from the same token source as
55
+ [`tokens/resolved.json`](./architecture.md) / `charts.json`. Each slot is set
56
+ explicitly (D2 does not derive a ramp from one seed). Because the output is a
57
+ static SVG, **build-time pre-rendering is D2's native path** — ideal for the
58
+ [report layer](./reporting.md).
59
+
60
+ ### What the slots paint
61
+
62
+ The mapping keeps a diagram **monochrome by default** — the rationed accent is
63
+ not spent on borders, edges, or shapes the author never marked:
64
+
65
+ | Slots | Paint | bronto mapping |
66
+ | --- | --- | --- |
67
+ | `N1`–`N3` | Text · muted · subtle | `--text` · `--text-soft` · `--text-dim` |
68
+ | `N4`–`N5` | Strong / regular lines | `--line-strong` · `--line` |
69
+ | `N6`–`N7` | Subtle background · canvas | `--panel-soft` · `--bg` |
70
+ | `B1` | Shape borders **and** connections (edges) | `--line-strong` |
71
+ | `B2`–`B3` | Muted borders | `--line` |
72
+ | `B4`–`B6` | Container fills (outer → leaf) | `--panel-soft` · `--bg-elevated` · `--panel` |
73
+ | `AA*` / `AB*` | Alternative-accent ramps (special-shape fills, class/sql headers) | kept **neutral** |
74
+
75
+ The alt-accent ramps are deliberately neutral: D2 spends them on special shapes
76
+ (cylinders, class/sql-table headers) by default, so mapping them to the accent
77
+ would colour shapes you never marked. Keeping them neutral is what holds the
78
+ monochrome default.
79
+
80
+ ### Spending the accent
81
+
82
+ To emphasise a node, opt it into a class that sets the accent explicitly — the
83
+ accent appears only where you ask for it:
84
+
85
+ ```d2
86
+ classes: {
87
+ accent: {
88
+ style: { fill: "#d71921"; font-color: "#ffffff"; stroke: "#b2151b" }
89
+ }
90
+ }
91
+
92
+ alert: Alert { class: accent }
93
+ ```
94
+
95
+ Use the resolved `--accent` / `--accent-strong` hex (from
96
+ [`tokens/resolved.json`](./architecture.md), per theme) for the `fill` / `stroke`
97
+ so the emphasis matches the rest of the surface. Reserve it for the one thing a
98
+ reader must not miss.
99
+
100
+ > **The label on an accent fill uses `--on-accent`, not `--accent-text`.** A
101
+ > filled-accent node needs **on-accent ink** for its `font-color` — the resolved
102
+ > `--on-accent` token (white on the light accent, black on the dark accent;
103
+ > ≥ 4.5:1, gated in [contrast.md](./contrast.md)). Do **not** reach for
104
+ > `--accent-text` by name: that is the inverse token — _accent-coloured text for
105
+ > a neutral background_ (it resolves to `--accent-strong`, ~1.3:1 on the accent
106
+ > fill, an unreadable label). The literal `#ffffff` above is `--on-accent` for
107
+ > the light theme; pull the per-theme hex from `tokens/resolved.json`.
108
+
109
+ ### Frozen inline SVG (no D2 runtime)
110
+
111
+ A static, PDF-first report often has no D2 binary in its pipeline. When you only
112
+ need a few boxes and arrows, **hand-author a token-themed inline `<svg>`** instead
113
+ of running D2 — the same frozen-figure route the [report chart
114
+ recipe](./reporting.md#chart-figure-recipe) uses. Paint each element from the
115
+ token that the live theme map would have resolved, so the frozen diagram still
116
+ re-skins (drive fills from `var(--token)` in the inline `style`/attributes, or
117
+ paste the resolved hex from [`tokens/resolved.json`](./architecture.md) for a
118
+ `file://` PDF):
119
+
120
+ | Diagram element | bronto token | (live D2 slot it mirrors) |
121
+ | --- | --- | --- |
122
+ | Node fill | `--panel` | `B4`–`B6` container fills |
123
+ | Node border | `--line-strong` | `B1` |
124
+ | Node label | `--text` | `N1` |
125
+ | Edge / connector + arrowhead | `--line-strong` | `B1` |
126
+ | Edge label | `--text-soft` | `N2` |
127
+ | Cluster / container fill | `--panel-soft` | `B4` |
128
+ | Accent (emphasised) node fill · its label | `--accent` · `--on-accent` | the `accent` class above |
129
+
130
+ ```html
131
+ <svg viewBox="0 0 320 120" role="img" aria-labelledby="flow-title">
132
+ <title id="flow-title">Ingest → Queue → Worker</title>
133
+ <!-- edge -->
134
+ <line x1="92" y1="40" x2="128" y2="40" stroke="var(--line-strong)" marker-end="url(#arrow)" />
135
+ <line x1="220" y1="40" x2="256" y2="40" stroke="var(--line-strong)" marker-end="url(#arrow)" />
136
+ <defs>
137
+ <marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto">
138
+ <path d="M0,0 L8,4 L0,8 z" fill="var(--line-strong)" />
139
+ </marker>
140
+ </defs>
141
+ <!-- neutral node -->
142
+ <rect x="16" y="22" width="76" height="36" rx="6" fill="var(--panel)" stroke="var(--line-strong)" />
143
+ <text x="54" y="44" text-anchor="middle" fill="var(--text)" font-size="12">Ingest</text>
144
+ <!-- accent (emphasised) node: on-accent ink on the accent fill -->
145
+ <rect x="128" y="22" width="92" height="36" rx="6" fill="var(--accent)" stroke="var(--accent-strong)" />
146
+ <text x="174" y="44" text-anchor="middle" fill="var(--on-accent)" font-size="12">Queue</text>
147
+ <rect x="256" y="22" width="64" height="36" rx="6" fill="var(--panel)" stroke="var(--line-strong)" />
148
+ <text x="288" y="44" text-anchor="middle" fill="var(--text)" font-size="12">Worker</text>
149
+ </svg>
150
+ ```
151
+
152
+ For anything larger or graph-laid-out, run D2 with the theme map and freeze its
153
+ output — don't hand-lay a complex graph.
154
+
155
+ ### Fit to small screens
156
+
157
+ D2 emits an SVG with explicit `width`/`height` from its layout, so on a narrow
158
+ screen it overflows unless you make it fluid. Two build-time options:
159
+
160
+ - **Scale to fit** — drop the fixed `width`/`height` attributes (keep the
161
+ `viewBox`) so the SVG shrinks to its container. Best for diagrams that stay
162
+ legible at smaller sizes.
163
+ - **Scroll a wide diagram** — wrap it in `overflow-x: auto` so a large diagram
164
+ scrolls inside its box instead of pushing the page wide, the same pattern the
165
+ report layer uses for wide figures.
166
+
167
+ ```css
168
+ .diagram-scroll {
169
+ overflow-x: auto;
170
+ }
171
+ .diagram-scroll svg {
172
+ max-inline-size: 100%;
173
+ block-size: auto;
174
+ }
175
+ ```
176
+
177
+ Render **non-sketch** at a fixed `scale` for predictable boxes.
178
+
179
+ ## Annotate a diagram
180
+
181
+ D2 output is SVG, so the [annotation layer](./annotations.md) composes onto it
182
+ exactly as in the [Mermaid recipe](./mermaid.md#annotate-a-diagram): render to a
183
+ frozen SVG (the D2 CLI `d2 in.d2 out.svg`, or `@terrastruct/d2` in a build
184
+ script), read the target shape's box, and paste a `<g class="ui-annotation">`
185
+ computed with `@ponchia/ui/annotations`. The same caveats apply — D2's internal
186
+ SVG (element ids, the root transform) is not a public contract, so pin your D2
187
+ version, render **non-sketch**, account for `pad` / `scale`, and avoid
188
+ `animate-interval` (a multi-board animated SVG would not track a single overlay).
189
+
190
+ ## Scope
191
+
192
+ bronto owns the theme map (gated: every slot resolves to a colour, both themes,
193
+ no `var()` leaks) and the annotation geometry. It does not own D2's rendering,
194
+ its internal SVG, or its CLI — those stay D2's, and the overlay is a documented
195
+ composition, not a shipped runtime binding.
package/docs/legends.md CHANGED
@@ -13,8 +13,9 @@ drifts from the series it describes.
13
13
  ```
14
14
 
15
15
  Bronto **paints and positions** the key; it owns no scales, no data→pixel
16
- mapping, and no series state. Pair it with Bronto's own `.ui-chart` bars, an
17
- SVG/canvas figure, or any external chart engine — the host owns the chart.
16
+ mapping, and no series state. Pair it with any chart — a token-themed inline
17
+ SVG, or an external engine like [Vega-Lite](./vega.md) — the host owns the
18
+ chart.
18
19
 
19
20
  ## What it is not
20
21
 
@@ -89,6 +90,21 @@ chart mark uses, or with a `--N` index helper for the categorical palette.
89
90
  `ui-legend__swatch--circle` and `ui-legend__swatch--line` change the chip shape
90
91
  (dot series, line series).
91
92
 
93
+ A `ui-legend__symbol` chip is an `.ui-icon` mask — it needs a `--icon-mask`
94
+ (e.g. `style="--icon-mask: var(--glyph-dot)"`) or it paints a solid square, like
95
+ any [icon](./reference.md). And an **interactive** legend entry must be a real
96
+ `<button>` (as in the example below) — a non-button `ui-legend__item` carrying
97
+ `data-series` is not keyboard-reachable.
98
+
99
+ **Keying the de-emphasised series.** In an accent-rationed chart — one mark
100
+ painted with [`brontoVegaAccent`](./vega.md#spending-the-accent), the rest left
101
+ quiet with `brontoVegaNeutral` — the quiet neutral **is** the last categorical
102
+ series, `--chart-8` (`#4d5358`). So a legend that honestly mirrors that chart
103
+ uses `ui-legend__swatch--1` for the highlighted entry and
104
+ `ui-legend__swatch--8` for the "everything else" entry — both are real palette
105
+ tokens, so the key never drifts from the marks and `check:legend` stays happy.
106
+ Don't hand-roll a grey: `--chart-8` is the neutral by construction.
107
+
92
108
  ## Variants
93
109
 
94
110
  | Modifier | Effect |