@ponchia/ui 0.6.6 → 0.6.8

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 (160) hide show
  1. package/CHANGELOG.md +175 -6
  2. package/README.md +38 -25
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +21 -3
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +91 -32
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +117 -43
  9. package/behaviors/command.d.ts.map +1 -1
  10. package/behaviors/command.js +74 -14
  11. package/behaviors/connectors.d.ts.map +1 -1
  12. package/behaviors/connectors.js +92 -9
  13. package/behaviors/crosshair.d.ts.map +1 -1
  14. package/behaviors/crosshair.js +47 -1
  15. package/behaviors/dialog.d.ts.map +1 -1
  16. package/behaviors/dialog.js +37 -16
  17. package/behaviors/disclosure.d.ts.map +1 -1
  18. package/behaviors/disclosure.js +33 -3
  19. package/behaviors/dismissible.d.ts.map +1 -1
  20. package/behaviors/dismissible.js +3 -2
  21. package/behaviors/forms.d.ts.map +1 -1
  22. package/behaviors/forms.js +78 -5
  23. package/behaviors/glyph.d.ts.map +1 -1
  24. package/behaviors/glyph.js +17 -2
  25. package/behaviors/index.d.ts +2 -0
  26. package/behaviors/index.d.ts.map +1 -1
  27. package/behaviors/index.js +2 -0
  28. package/behaviors/inert.js +3 -2
  29. package/behaviors/internal.d.ts +2 -1
  30. package/behaviors/internal.d.ts.map +1 -1
  31. package/behaviors/internal.js +25 -4
  32. package/behaviors/legend.d.ts +0 -5
  33. package/behaviors/legend.d.ts.map +1 -1
  34. package/behaviors/legend.js +78 -14
  35. package/behaviors/menu.d.ts.map +1 -1
  36. package/behaviors/menu.js +13 -8
  37. package/behaviors/modal.d.ts.map +1 -1
  38. package/behaviors/modal.js +77 -19
  39. package/behaviors/popover.d.ts +4 -3
  40. package/behaviors/popover.d.ts.map +1 -1
  41. package/behaviors/popover.js +89 -9
  42. package/behaviors/sources.d.ts.map +1 -1
  43. package/behaviors/sources.js +14 -2
  44. package/behaviors/splitter.d.ts +26 -0
  45. package/behaviors/splitter.d.ts.map +1 -0
  46. package/behaviors/splitter.js +239 -0
  47. package/behaviors/spotlight.d.ts.map +1 -1
  48. package/behaviors/spotlight.js +28 -2
  49. package/behaviors/table.d.ts.map +1 -1
  50. package/behaviors/table.js +105 -13
  51. package/behaviors/tabs.d.ts.map +1 -1
  52. package/behaviors/tabs.js +82 -18
  53. package/behaviors/theme.d.ts.map +1 -1
  54. package/behaviors/theme.js +26 -6
  55. package/classes/classes.json +230 -4
  56. package/classes/index.d.ts +64 -3
  57. package/classes/index.js +56 -2
  58. package/classes/vscode.css-custom-data.json +1 -1
  59. package/connectors/index.d.ts +39 -6
  60. package/connectors/index.d.ts.map +1 -1
  61. package/connectors/index.js +67 -9
  62. package/css/analytical.css +3 -1
  63. package/css/annotations.css +12 -0
  64. package/css/app.css +4 -4
  65. package/css/clamp.css +92 -0
  66. package/css/crosshair.css +27 -2
  67. package/css/feedback.css +2 -30
  68. package/css/figure.css +102 -0
  69. package/css/highlights.css +50 -0
  70. package/css/interval.css +90 -0
  71. package/css/navigation.css +12 -0
  72. package/css/primitives.css +2 -3
  73. package/css/report-kit.css +38 -0
  74. package/css/report.css +23 -4
  75. package/css/sidenote.css +12 -2
  76. package/css/site.css +2 -1
  77. package/css/sources.css +5 -0
  78. package/css/state.css +120 -1
  79. package/css/table.css +4 -0
  80. package/css/tokens.css +25 -9
  81. package/css/workbench.css +101 -8
  82. package/dist/bronto.css +1 -1
  83. package/dist/css/analytical.css +1 -1
  84. package/dist/css/annotations.css +1 -1
  85. package/dist/css/app.css +1 -1
  86. package/dist/css/clamp.css +1 -0
  87. package/dist/css/crosshair.css +1 -1
  88. package/dist/css/feedback.css +1 -1
  89. package/dist/css/figure.css +1 -0
  90. package/dist/css/highlights.css +1 -0
  91. package/dist/css/interval.css +1 -0
  92. package/dist/css/navigation.css +1 -1
  93. package/dist/css/primitives.css +1 -1
  94. package/dist/css/report-kit.css +1 -0
  95. package/dist/css/report.css +1 -1
  96. package/dist/css/sidenote.css +1 -1
  97. package/dist/css/site.css +1 -1
  98. package/dist/css/sources.css +1 -1
  99. package/dist/css/state.css +1 -1
  100. package/dist/css/table.css +1 -1
  101. package/dist/css/tokens.css +1 -1
  102. package/dist/css/workbench.css +1 -1
  103. package/docs/adr/0001-color-system.md +3 -2
  104. package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
  105. package/docs/annotations.md +12 -1
  106. package/docs/architecture.md +105 -48
  107. package/docs/clamp.md +49 -0
  108. package/docs/command.md +4 -1
  109. package/docs/connectors.md +16 -0
  110. package/docs/contrast.md +34 -24
  111. package/docs/crosshair.md +1 -1
  112. package/docs/d2.md +37 -0
  113. package/docs/dots.md +4 -1
  114. package/docs/figure.md +71 -0
  115. package/docs/frontier-primitives.md +25 -24
  116. package/docs/glyphs.md +11 -0
  117. package/docs/highlights.md +52 -0
  118. package/docs/interop/tailwind.md +148 -0
  119. package/docs/interval.md +55 -0
  120. package/docs/legends.md +3 -2
  121. package/docs/mermaid.md +6 -0
  122. package/docs/migrations/0.2-to-0.3.md +80 -0
  123. package/docs/migrations/0.3-to-0.4.md +48 -0
  124. package/docs/migrations/0.4-to-0.5.md +96 -0
  125. package/docs/migrations/0.5-to-0.6.md +82 -0
  126. package/docs/package-contract.md +44 -6
  127. package/docs/reference.md +78 -5
  128. package/docs/reporting.md +126 -60
  129. package/docs/sidenote.md +7 -1
  130. package/docs/sources.md +1 -1
  131. package/docs/stability.md +23 -5
  132. package/docs/state.md +67 -10
  133. package/docs/theming.md +12 -4
  134. package/docs/usage.md +47 -13
  135. package/docs/vega.md +4 -4
  136. package/docs/workbench.md +59 -18
  137. package/llms.txt +89 -16
  138. package/package.json +82 -6
  139. package/qwik/index.d.ts +1 -0
  140. package/qwik/index.d.ts.map +1 -1
  141. package/qwik/index.js +26 -21
  142. package/react/index.d.ts +1 -0
  143. package/react/index.d.ts.map +1 -1
  144. package/react/index.js +4 -1
  145. package/schemas/report-claims.v1.schema.json +137 -0
  146. package/solid/index.d.ts +2 -0
  147. package/solid/index.d.ts.map +1 -1
  148. package/solid/index.js +3 -0
  149. package/svelte/index.d.ts +114 -0
  150. package/svelte/index.d.ts.map +1 -0
  151. package/svelte/index.js +193 -0
  152. package/tailwind.css +87 -0
  153. package/tokens/figma.variables.json +2241 -0
  154. package/tokens/index.js +1 -1
  155. package/tokens/index.json +2 -2
  156. package/tokens/resolved.json +3 -3
  157. package/tokens/tokens.dtcg.json +1 -1
  158. package/vue/index.d.ts +116 -0
  159. package/vue/index.d.ts.map +1 -0
  160. package/vue/index.js +228 -0
@@ -39,9 +39,12 @@ on top of the CSS, none of which require a framework commitment**:
39
39
  ├── connectors/ pure SVG leader-line geometry kernel (no DOM) [optional]
40
40
  ├── annotations/ pure SVG callout geometry (builds on connectors) [optional]
41
41
  ├── glyphs/ dot-matrix glyph registry/renderers [optional]
42
+ ├── schemas/ declarative JSON contracts for report/tooling data [optional]
42
43
  ├── react/ thin React hooks over behaviors [optional peer]
43
44
  ├── solid/ thin Solid primitives over behaviors [optional peer]
44
- └── qwik/ thin Qwik hooks over behaviors (useVisibleTask$) [optional peer]
45
+ ├── qwik/ thin Qwik hooks over behaviors (useVisibleTask$) [optional peer]
46
+ ├── svelte/ thin Svelte actions over behaviors [optional]
47
+ └── vue/ thin Vue directives over behaviors [optional]
45
48
  ```
46
49
 
47
50
  ### Consequences of each layer
@@ -57,12 +60,12 @@ on top of the CSS, none of which require a framework commitment**:
57
60
  - **tokens/** — `index.js` (`cssVars`) is the single source of truth for token
58
61
  values. The four `:root` palette blocks of `css/tokens.css` are **generated**
59
62
  from it (`scripts/gen-tokens-css.mjs`), as are the JSON artifacts (`index.json`,
60
- `tokens.dtcg.json`, `resolved.json`). So the dark palette is authored once,
61
- not in three places (the two CSS dark blocks are now identical by
62
- construction), resolving the duplication ADR-0003 flagged. The CSS-only
63
- presets (density / contrast / OLED) stay hand-authored below a marker and are
64
- preserved across regeneration. `scripts/check-fresh.mjs` fails CI if
65
- `css/tokens.css` drifts from the model.
63
+ `tokens.dtcg.json`, `resolved.json`, `figma.variables.json`). So the dark
64
+ palette is authored once, not in three places (the two CSS dark blocks are now
65
+ identical by construction), resolving the duplication ADR-0003 flagged. The
66
+ CSS-only presets (density / contrast / OLED) stay hand-authored below a marker
67
+ and are preserved across regeneration. `scripts/check-fresh.mjs` fails CI if a
68
+ generated mirror drifts from the model.
66
69
  - **classes/** — `cls` is the flat registry; recipes only emit from it;
67
70
  `scripts/check-classes.mjs` enforces a bidirectional match with the
68
71
  stylesheet's `.ui-*` selectors. The class contract cannot silently rot.
@@ -76,24 +79,28 @@ on top of the CSS, none of which require a framework commitment**:
76
79
  - **glyphs/** — static bitmap data and SSR-safe render helpers. The
77
80
  256-cell DOM renderers are for display and solid inline icons; the `.ui-icon`
78
81
  mask renderer is for dense icon-at-scale use.
79
- - **react/** / **solid/** / **qwik/** — optional lifecycle adapters over `behaviors/`.
80
- They do not define markup, own state, or fork behavior logic; they only run
81
- the vanilla initializers on mount and cleanup on unmount/dispose.
82
+ - **react/** / **solid/** / **qwik/** / **svelte/** / **vue/** — optional lifecycle
83
+ adapters over `behaviors/`. They do not define markup, own state, or fork
84
+ behavior logic; they only run the vanilla initializers on mount and cleanup
85
+ on unmount/dispose. The Svelte and Vue adapters are plain action/directive
86
+ objects, so they do not add runtime dependencies to the package.
82
87
  - **`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.
88
+ `@import`s exactly **nine** analytical figure/evidence leaves: `figure`,
89
+ `annotations`, `legend`, `marks`, `connectors`, `spotlight`, `crosshair`,
90
+ `selection`, and `highlights`. The adjacent opt-in leaves — `sources`,
91
+ `interval`, `clamp`, `state`, `generated`, `workbench`, and `command` are
92
+ report/tooling/trust surfaces that are intentionally **not** part of the
93
+ analytical roll-up and must be imported individually. Importing
94
+ `analytical.css` does not pull in any of those seven.
89
95
  - **Root export (`.`) is CSS-only.** `exports["."]` resolves to the CSS
90
96
  bundle (`dist/bronto.css`). It is a CSS side-effect import for CSS-aware
91
97
  bundlers (`@import '@ponchia/ui'` in CSS, or a side-effect
92
98
  `import '@ponchia/ui'` in Vite/Astro/SvelteKit). There is no runtime JS at
93
99
  the package root — Node/runtime JS imports of `.` are not supported. All JS
94
100
  entrypoints are explicit subpaths (`/behaviors`, `/classes`, `/tokens`,
95
- `/glyphs`, `/react`, `/solid`, `/qwik`, `/skins`, `/charts`). This is a
96
- permanent, intentional contract.
101
+ `/glyphs`, `/annotations`, `/connectors`, `/react`, `/solid`, `/qwik`,
102
+ `/svelte`, `/vue`, `/skins`, `/charts`, `/mermaid`, `/d2`, `/vega`). This is
103
+ a permanent, intentional contract.
97
104
 
98
105
  ## Repository layout
99
106
 
@@ -108,9 +115,10 @@ generator overwrites them and a drift gate fails CI).
108
115
  | --- | --- | --- | --- |
109
116
  | `css/` | source | yes | The framework. Hand-authored `@layer bronto` CSS. (`css/tokens.css` palette blocks and `css/generated.css` are generated — see below.) |
110
117
  | `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`/`behaviors` are emitted from JSDoc by `tsc` (`npm run dts:emit`), `classes`/`tokens`/`glyphs` from the runtime. No leaf `.d.ts` is hand-maintained. |
118
+ | `classes/index.js`, `behaviors/`, `annotations/`, `connectors/`, `react/`, `solid/`, `qwik/`, `svelte/`, `vue/`, `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`/`svelte`/`vue`/`behaviors` are emitted from JSDoc by `tsc` (`npm run dts:emit`), `classes`/`tokens`/`glyphs` from the runtime. No leaf `.d.ts` is hand-maintained. |
119
+ | `schemas/*.schema.json` | source · published schema files (path-frozen) | yes — but **do not move exported files** | Declarative JSON Schema contracts for sidecars/tooling data. Each exported schema file path is public; the directory itself is not a wildcard import. No validator runtime ships. |
112
120
  | `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`. |
121
+ | `tokens/index.json`, `tokens/resolved.json`, `tokens/tokens.dtcg.json`, `tokens/figma.variables.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
122
  | `fonts/` | vendored | — | The Doto webfont (woff2) + its OFL license. |
115
123
  | `scripts/` | tooling | yes | `gen-*` regenerate artifacts, `check-*` are the drift/contract gates wired into `npm run check`, plus `build-dist`, `serve`, `size-report`. |
116
124
  | `docs/` | source (mostly) | yes | Hand-authored docs + ADRs; the curated subset in `package.json` `files` ships in the tarball. `docs/reference.md` is generated. |
@@ -127,24 +135,47 @@ the result.
127
135
 
128
136
  ## Drift control
129
137
 
130
- Every data mirror is backed by a check wired into `npm run check`, run by CI
131
- on every push/PR and again by `release.yml` before publish (see "Release
132
- gating" below), so a version that fails any invariant never reaches npm.
138
+ Every data mirror and public documentation contract is backed by a check wired
139
+ into `npm run check`, run by CI on every push/PR and again by `release.yml`
140
+ before publish (see "Release gating" below), so a version that fails any
141
+ invariant never reaches npm. Public authoring docs are treated as public surface
142
+ too: `check:doc-links` fails stale local paths/anchors across shipped docs,
143
+ GitHub-only docs, and the docs viewer route list, while shipped-doc links must
144
+ also point at files present in the npm tarball. `check:contract` verifies
145
+ documented named imports, `check:doc-recipes` fails copy-paste CDN recipes that
146
+ silently no-op, and `check:report` validates fenced HTML snippets before they
147
+ are copied into consumer reports.
133
148
 
134
149
  | Invariant | Enforced by |
135
150
  | ----------------------------------------------- | ------------------- |
136
- | exports / import graph / `files` consistent | `check-exports.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` |
151
+ | exports / import graph / source CSS `layer(bronto)` imports / layered-vs-unlayered CSS target map / `files` consistent | `check-exports.mjs` |
152
+ | pure generated mirrors fresh — `tokens.css`/`index.json`, `dtcg.json`, `resolved.json`, `figma.variables.json`, `classes`/`tokens` `.d.ts`, `reference.md`, vscode data — each byte-equal to its generator (registry: `scripts/lib/artifacts.mjs`) | `check-fresh.mjs` |
138
153
  | `classes` `cls` ⇄ `.ui-*` selectors | `check-classes.mjs` |
139
- | `connectors`/`annotations`/`react`/`solid`/`qwik`/`behaviors` `.d.ts` (+ maps) == fresh `tsc` emit of their JSDoc | `check-dts-emit.mjs` |
154
+ | `connectors`/`annotations`/`react`/`solid`/`qwik`/`svelte`/`vue`/`behaviors` `.d.ts` (+ maps) == fresh `tsc` emit of their JSDoc | `check-dts-emit.mjs` |
140
155
  | legend swatch colours ⊆ `charts.js` · opt-in | `check-legend.mjs` |
141
156
  | color tokens tiered · no raw chromatic color in components | `check-color-policy.mjs` |
142
157
  | `css/skins.css` ⇄ `tokens/skins.js` · colorways opt-in | `check-skins.mjs` |
143
158
  | every shipped colorway accent meets its WCAG floor | `check-contrast.mjs` |
144
159
  | `dataviz.css`/`charts.json`/`charts.d.ts` ⇄ `tokens/charts.js` · CVD-distinguishable · opt-in | `check-charts.mjs` |
145
160
  | `shiki/nothing.json` valid + on rationed palette | `check-shiki.mjs` |
146
- | `dist/*.css` == fresh build of `css/` + budget | `check-dist.mjs` |
161
+ | `dist/*.css` == fresh single-`@layer bronto` build of `css/` + budget | `check-dist.mjs` |
147
162
  | published tarball == intended `files` only | `check-pack.mjs` |
163
+ | packed core JS/JSON public subpaths import without optional framework peers, packed JS named exports exactly match source modules, peer-backed adapters import after peers are linked, concrete CSS/doc/font subpaths resolve, and packed behavior initializers/toast no-op in a clean consumer with no DOM globals | `check-consumer-surface.mjs` |
164
+ | packed typed public subpaths compile through package exports in a clean TypeScript consumer | `check-consumer-types.mjs` |
165
+ | GitHub Actions workflow syntax and embedded shell snippets lint | `check:workflows` (`github-actionlint`) |
166
+ | every shipped CSS leaf is classified as foundation or has explicit docs/demo/e2e ownership | `check-component-matrix.mjs` |
167
+ | every public behavior export has explicit docs, unit-test, and browser-test ownership | `check-behavior-matrix.mjs` |
168
+ | every public helper export in `classes`/`annotations`/`connectors`/`glyphs` has explicit docs, unit-test, and type-test ownership | `check-helper-matrix.mjs` |
169
+ | every delegated behavior has React/Solid/Qwik hook, Svelte action, Vue directive, docs, example, unit, and type ownership | `check-binding-matrix.mjs` |
170
+ | `@playwright/test` version ⇄ pinned Playwright container image ⇄ visual workflows/docs/local runner | `check-playwright-container.mjs` |
171
+ | every shipped JSON schema is exported, documented, validates its public cookbook example, and rejects malformed sidecars | `check-schemas.mjs` |
172
+ | packed public text contains no private terms, local paths, or secret-looking assignments | `check-public-hygiene.mjs` |
173
+ | CSS custom-property references resolve or carry an explicit fallback/host boundary | `check-variables.mjs` |
174
+ | `MIGRATIONS.json` edges have structured rules and matching docs | `check-migrations.mjs` |
175
+ | example inventory ⇄ CI matrix ⇄ browser-smoke list ⇄ README rows ⇄ preview ports | `check-examples.mjs` |
176
+ | demo visual snapshot declarations ⇄ committed Chromium baseline inventory | `check-visual-baselines.mjs` |
177
+ | public authoring docs keep valid local paths/anchors; shipped-doc links resolve inside the tarball; docs viewer routes resolve | `check-doc-links.mjs` |
178
+ | report/docs snippets use valid `ui-*` classes and public authoring snippets keep intact local id/ARIA/behavior references | `check-report.mjs` |
148
179
  | published `.d.ts` compile + reject typos | `tsc` (`check:types`) |
149
180
  | CSS style/correctness | Stylelint |
150
181
  | non-CSS source style | Prettier (`check:format`) |
@@ -160,32 +191,50 @@ payload contract, raised only intentionally with a CHANGELOG note.
160
191
  if the generated literal `cls`/token types stopped rejecting typos —
161
192
  so the *value* of the generated `.d.ts` is itself gated, not just their
162
193
  freshness (`check-fresh`).
194
+ `check:consumer-types` then installs the packed tarball in a clean temp
195
+ project and compiles package-subpath imports, so `exports.types` and
196
+ internal declaration references are proven through consumer resolution.
163
197
 
164
198
  ## Release gating
165
199
 
166
- `release.yml` (on a pushed `v*` tag) is a five-job DAG, serialized by a
200
+ `release.yml` (on a pushed `v*` tag) is a six-job DAG, serialized by a
167
201
  `concurrency: release-publish` group so two tags can't race the dist-tag
168
202
  pointer:
169
203
 
170
- - `validate` — read-only: `npm run check` + tag↔version match. `check`
171
- includes `check:release`; for a prerelease tag the base version's
172
- CHANGELOG section need only exist (`## Unreleased x.y.z` is fine) —
173
- only a stable release must carry a dated heading.
174
- - `e2e` Playwright (visual + axe a11y, both themes, cross-engine) in
175
- the pinned `mcr.microsoft.com/playwright` container.
204
+ - `validate` — read-only: verifies the tag commit is reachable from `main`,
205
+ then runs `npm run check` and the tag↔version match. `check`
206
+ includes the node:test unit and contract suite, plus `check:release`;
207
+ for a prerelease tag the base version's CHANGELOG
208
+ section need only exist (`## Unreleased x.y.z` is fine) — only a stable
209
+ release must carry a dated heading.
210
+ - `e2e` — `needs: validate`: Playwright (visual + axe a11y, both themes,
211
+ demo structural integrity, both themes, cross-engine) in the pinned
212
+ `mcr.microsoft.com/playwright` container. Local cross-engine reproduction
213
+ without screenshot rasterisation is
214
+ `npm run test:e2e:nonpixel`; use `npm run test:e2e` or
215
+ `npm run test:e2e:chromium` only in the pinned container when the pixel
216
+ baseline gate itself is in scope. With Docker running,
217
+ `npm run test:e2e:visual:container` is the local shortcut for the same
218
+ Chromium screenshot environment.
176
219
  - `examples` — `needs: validate`: builds the downstream example
177
220
  apps against the **packed tarball**, mirroring CI. Catches a broken
178
221
  published surface (exports map / missing file / unresolved subpath)
179
222
  that `check:pack`'s file-allowlist inspection cannot — so the release
180
223
  path runs the same consumer smoke as merge-to-main.
181
- - `publish-npm` — `needs: [validate, e2e, examples]`: `npm publish` with
182
- provenance. Runs in the `npm-publish` **Environment** (required-reviewer
183
- protection), so after the gates pass the run pauses for a manual approval
184
- in the Actions UI before anything reaches npm — a guard against an
185
- accidental tag push publishing. Dist-tag is derived from the tag: stable
186
- (`v0.4.0`) `latest`; SemVer prerelease (`v0.4.0-rc.1`, any hyphenated
187
- identifier) `next`, so the default `npm i @ponchia/ui` never moves onto
188
- an unstable build (opt in with `@ponchia/ui@next`).
224
+ - `publish-preflight` — `needs: [validate, e2e, examples]`: installs with
225
+ lifecycle scripts disabled, runs `npm pack --dry-run --ignore-scripts`, and
226
+ writes the pack manifest + size report to the job summary for review before
227
+ the protected publish approval.
228
+ - `publish-npm` `needs: publish-preflight`: `npm publish --ignore-scripts`
229
+ with provenance. Runs in the `npm-publish` **Environment**
230
+ (required-reviewer protection), so after the gates and preflight pass the run
231
+ pauses for a manual approval in the Actions UI before anything reaches npm —
232
+ a guard against an accidental tag push publishing. Dist-tag is derived from
233
+ the tag: stable (`v0.4.0`) → `latest`; SemVer prerelease (`v0.4.0-rc.1`, any
234
+ hyphenated identifier) → `next`, so the default `npm i @ponchia/ui` never
235
+ moves onto an unstable build (opt in with `@ponchia/ui@next`). Post-publish
236
+ `npm view` registry observation is best-effort only: a registry read flake must
237
+ not fail the job after the immutable publish already succeeded.
189
238
  - `release-notes` — `needs: publish-npm`: a GitHub Release for visibility
190
239
  (transitively gated on a successful publish, hence on the gates above);
191
240
  prerelease tags are flagged so they aren't surfaced as "Latest". The Release
@@ -194,7 +243,7 @@ pointer:
194
243
  source of truth, surfaced where readers look.
195
244
 
196
245
  Because the documented install path is the npm package, **the npm publish
197
- is a real gate**: if `validate`, `e2e`, *or* `examples` fails,
246
+ is a real gate**: if `validate`, `e2e`, `examples`, or `publish-preflight` fails,
198
247
  `publish-npm` never runs, the version never reaches the registry, and
199
248
  consumers never resolve it.
200
249
  (Corollary: a flaky `e2e` blocks releases — that is deliberate; fix the
@@ -210,9 +259,9 @@ Process still applies: bump `package.json`, land on `main`, go green, tag.
210
259
  ## Decision — distribution: npm public `@ponchia/ui`
211
260
 
212
261
  Decided 2026-05-15. The framework is consumed by a growing set of
213
- heterogeneous web frontends (Astro, SvelteKit, React, Solid, Qwik, vanilla),
214
- several deploying via third-party CI. The only option where onboarding a new
215
- frontend is `npm i @ponchia/ui` with zero per-consumer config is **npm
262
+ heterogeneous web frontends (Astro, SvelteKit, React, Solid, Qwik, Vue,
263
+ Tailwind, vanilla), several deploying via third-party CI. The only option where
264
+ onboarding a new frontend is `npm i @ponchia/ui` with zero per-consumer config is **npm
216
265
  public**, and it uniquely also closes the release-gating gap (publish *is*
217
266
  the gate). GitHub Packages was rejected: it requires auth to install even
218
267
  public packages, i.e. an `.npmrc` + token on every frontend and CI runner —
@@ -237,8 +286,16 @@ explained, not surprising.
237
286
  provenance.
238
287
  - Run `npm pack --dry-run --json` locally or from CI logs and confirm the
239
288
  intended file count/payload.
240
- - Build the packed examples matrix (vanilla, Astro, SvelteKit, React, Solid, Qwik)
241
- from the tarball, not a workspace link.
289
+ - Build the packed examples matrix from the tarball, not a workspace link:
290
+ `npm run test:examples` covers vanilla, Astro, SvelteKit, Vue, React, Solid,
291
+ Qwik, Tailwind, and report-static, with Chromium browser smokes for runtime
292
+ examples. For a deeper consumer pass, `npm run test:examples:cross-browser`
293
+ runs the same packed smokes in Chromium, Firefox, and WebKit; manual CI
294
+ dispatches pass the same cross-browser flag to the reusable examples workflow.
295
+ `npm run test:examples:visual` adds local-safe desktop + mobile
296
+ screenshot/layout health smokes for packed examples; it detects blank,
297
+ under-painted, or horizontally overflowing output but intentionally does not
298
+ author OS-sensitive committed PNG baselines.
242
299
  - Confirm the GitHub Release body matches the curated changelog section.
243
300
  - If a bad package is published, deprecate that exact version on npm, publish a
244
301
  patched version, and link the deprecation note to the changelog/security
package/docs/clamp.md ADDED
@@ -0,0 +1,49 @@
1
+ # Clamp
2
+
3
+ `@ponchia/ui/css/clamp.css` is an opt-in bounded excerpt primitive for source
4
+ excerpts, claim basis, caveats, and evidence text that should scan compactly but
5
+ remain reachable.
6
+
7
+ ```css
8
+ @import '@ponchia/ui';
9
+ @import '@ponchia/ui/css/clamp.css';
10
+ ```
11
+
12
+ ## Bounded excerpt
13
+
14
+ ```html
15
+ <div class="ui-clamp" style="--clamp-lines: 3">
16
+ <input class="ui-clamp__toggle" id="source-excerpt" type="checkbox" />
17
+ <p class="ui-clamp__body">
18
+ The source excerpt remains real text in the DOM. The visible block is
19
+ clamped for scanning, but the full passage is available through the reveal
20
+ control and is expanded for print.
21
+ </p>
22
+ <label class="ui-clamp__control" for="source-excerpt">
23
+ <span class="ui-clamp__more">Show more</span>
24
+ <span class="ui-clamp__less">Show less</span>
25
+ </label>
26
+ </div>
27
+ ```
28
+
29
+ If the host should not offer expansion, omit the toggle and control and keep
30
+ only `ui-clamp` plus `ui-clamp__body`.
31
+
32
+ ## Contract
33
+
34
+ | Class | Role |
35
+ | --- | --- |
36
+ | `.ui-clamp` | Wrapper and `--clamp-lines` host. |
37
+ | `.ui-clamp__body` | The clamped text block. |
38
+ | `.ui-clamp__toggle` | Optional checkbox state for CSS-only reveal. |
39
+ | `.ui-clamp__control` | Optional visible reveal control. |
40
+ | `.ui-clamp__more` / `.ui-clamp__less` | Explicit labels for closed/open state. |
41
+
42
+ | Custom property | On | Meaning |
43
+ | --- | --- | --- |
44
+ | `--clamp-lines` | `.ui-clamp` | Number of lines before the excerpt is clamped. Default `4`. |
45
+
46
+ ## Print
47
+
48
+ Print expands the body and hides the toggle/control. Do not use `ui-clamp` to
49
+ hide information from archived or PDF output.
package/docs/command.md CHANGED
@@ -84,7 +84,10 @@ document.querySelector('[data-bronto-command]').addEventListener('bronto:command
84
84
  );
85
85
  ```
86
86
 
87
- Framework hook: `useCommand()` in `@ponchia/ui/react` · `/solid` · `/qwik`.
87
+ Framework adapters: `useCommand()` in `@ponchia/ui/react`, `/solid`, and
88
+ `/qwik`; the `command` / `useCommand` action in `@ponchia/ui/svelte`; and
89
+ `vCommand` (or the `v-bronto-command` directive from `brontoVue`) in
90
+ `@ponchia/ui/vue`.
88
91
 
89
92
  ## Accessibility
90
93
 
@@ -41,6 +41,17 @@ import { initConnectors } from '@ponchia/ui/behaviors';
41
41
  const stop = initConnectors(); // redraws on resize/scroll; returns a cleanup
42
42
  ```
43
43
 
44
+ For static SVG output, author the same parts directly: `.ui-connector__path`
45
+ holds the line path, and `.ui-connector__end` holds an optional arrowhead/dot
46
+ path from `arrowHead()` or `dotMark()`.
47
+
48
+ ```html
49
+ <svg class="ui-connector ui-connector--accent" aria-hidden="true">
50
+ <path class="ui-connector__path" d="M20,10C48,10 72,50 100,50" />
51
+ <path class="ui-connector__end" d="M100,50L92,46L92,54Z" />
52
+ </svg>
53
+ ```
54
+
44
55
  A connector is decorative — mark it `aria-hidden="true"` and make sure the
45
56
  relationship it depicts is also clear from the content/DOM order.
46
57
 
@@ -81,6 +92,11 @@ const head = arrowHead(to, angle); // place at the endpoint
81
92
  - `endTangentAngle(from, to, shape)` — the angle the path *arrives* at `to`
82
93
  (chord for `straight`, axis-aligned for `elbow`/`curve`); rotate an end marker
83
94
  by this so it points along the path. `connectRects().angle` already uses it.
95
+ - Low-level scalar/SVG kernel helpers are exported for hosts that need the same
96
+ rounding and guard semantics as the path builders: `PRECISION`,
97
+ `roundNumber(value)`, `fmt(value)`, `point(x, y)`, `finite(name, value,
98
+ fallback)`, `dimension(name, value, fallback)`, `clamp(value, min, max)`, and
99
+ `rectPath(left, top, right, bottom)`.
84
100
 
85
101
  ## Coordinate model
86
102
 
package/docs/contrast.md CHANGED
@@ -15,7 +15,8 @@ model (`tokens/resolved.json`) so it cannot drift from the palette, and
15
15
 
16
16
  - **Body / UI text** pairings are guaranteed **WCAG 2.1 AA — 4.5:1**
17
17
  (1.4.3). This covers `--text`, `--text-soft`, `--text-dim`,
18
- `--accent-text`, and the primary-button label.
18
+ `--accent-text`, neutral text on soft accent-tint components, and the
19
+ primary-button label.
19
20
  - **Non-text UI** (focus ring, accent fill, status colour) is guaranteed
20
21
  **3:1** (1.4.11 non-text contrast / the large-text bar). These are
21
22
  deliberately *not* held to 4.5:1 — a focus
@@ -32,7 +33,8 @@ model (`tokens/resolved.json`) so it cannot drift from the palette, and
32
33
  further but is out of scope for this gated baseline.
33
34
 
34
35
  Translucent foregrounds (soft fills) are alpha-flattened over their
35
- background before measuring the ratio the eye actually gets.
36
+ background before measuring. When a translucent background is a component
37
+ tint, the table names the neutral base it is composited over.
36
38
 
37
39
  Overall: **all contractual pairings meet their floor ✅**.
38
40
 
@@ -51,6 +53,7 @@ Overall: **all contractual pairings meet their floor ✅**.
51
53
  | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.74:1 | Lc 66.7 | ✅ pass |
52
54
  | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.32:1 | Lc 75.4 | ✅ pass |
53
55
  | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.96:1 | Lc 82.1 | ✅ pass |
56
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 8.88:1 | Lc 77.0 | ✅ pass |
54
57
  | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 5.91:1 | Lc 71.0 | ℹ️ not gated |
55
58
  | `--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
59
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.18:1 | Lc 78.9 | ✅ pass |
@@ -77,10 +80,11 @@ Overall: **all contractual pairings meet their floor ✅**.
77
80
  | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 7.16:1 | Lc 50.3 | ✅ pass |
78
81
  | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 6.52:1 | Lc 49.3 | ✅ pass |
79
82
  | `--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 |
83
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.42:1 | Lc 46.6 | ✅ pass |
84
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 5.84:1 | Lc 45.6 | ✅ pass |
85
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 8.05:1 | Lc 68.4 | pass |
86
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 2.42:1 | Lc 42.2 | ℹ️ not gated |
87
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 2.63:1 | Lc 47.4 | ℹ️ not gated |
84
88
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.95:1 | Lc 42.9 | ✅ pass |
85
89
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.95:1 | Lc 42.9 | ✅ pass |
86
90
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.31:1 | Lc 40.0 | ✅ pass |
@@ -109,8 +113,9 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
109
113
  | --- | --- | --- | --- | --- | --- | --- |
110
114
  | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.75:1 | Lc 78.9 | ✅ pass |
111
115
  | `--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 |
116
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 9.13:1 | Lc 78.7 | pass |
117
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 6.48:1 | Lc 76.1 | ℹ️ not gated |
118
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 6.85:1 | Lc 79.9 | ℹ️ not gated |
114
119
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.66:1 | Lc 83.3 | ✅ pass |
115
120
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.66:1 | Lc 83.3 | ✅ pass |
116
121
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.14:1 | Lc 71.4 | ✅ pass |
@@ -121,10 +126,11 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
121
126
 
122
127
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
123
128
  | --- | --- | --- | --- | --- | --- | --- |
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 |
129
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 11.84:1 | Lc 76.1 | ✅ pass |
130
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 10.77:1 | Lc 75.1 | ✅ pass |
131
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 6.83:1 | Lc 65.5 | pass |
132
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.46:1 | Lc 20.8 | ℹ️ not gated |
133
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.51:1 | Lc 23.2 | ℹ️ not gated |
128
134
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.88:1 | Lc 71.6 | ✅ pass |
129
135
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 11.88:1 | Lc 71.6 | ✅ pass |
130
136
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.60:1 | Lc 69.9 | ✅ pass |
@@ -137,8 +143,9 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
137
143
  | --- | --- | --- | --- | --- | --- | --- |
138
144
  | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.23:1 | Lc 93.3 | ✅ pass |
139
145
  | `--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 |
146
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 8.78:1 | Lc 76.3 | pass |
147
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 11.31:1 | Lc 88.2 | ℹ️ not gated |
148
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 12.15:1 | Lc 92.8 | ℹ️ not gated |
142
149
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.74:1 | Lc 100.6 | ✅ pass |
143
150
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 11.74:1 | Lc 100.6 | ✅ pass |
144
151
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.66:1 | Lc 90.4 | ✅ pass |
@@ -149,10 +156,11 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
149
156
 
150
157
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
151
158
  | --- | --- | --- | --- | --- | --- | --- |
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 |
159
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.72:1 | Lc 80.3 | ✅ pass |
160
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.57:1 | Lc 79.3 | ✅ pass |
161
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 6.66:1 | Lc 65.0 | pass |
162
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.38:1 | Lc 18.0 | ℹ️ not gated |
163
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.42:1 | Lc 19.9 | ℹ️ not gated |
156
164
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 12.86:1 | Lc 75.6 | ✅ pass |
157
165
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 12.86:1 | Lc 75.6 | ✅ pass |
158
166
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 11.48:1 | Lc 74.1 | ✅ pass |
@@ -165,8 +173,9 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
165
173
  | --- | --- | --- | --- | --- | --- | --- |
166
174
  | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.22:1 | Lc 76.6 | ✅ pass |
167
175
  | `--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 |
176
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 9.16:1 | Lc 78.9 | pass |
177
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 5.98:1 | Lc 74.1 | ℹ️ not gated |
178
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 6.32:1 | Lc 77.7 | ℹ️ not gated |
170
179
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.19:1 | Lc 80.7 | ✅ pass |
171
180
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 5.19:1 | Lc 80.7 | ✅ pass |
172
181
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 68.6 | ✅ pass |
@@ -177,10 +186,11 @@ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
177
186
 
178
187
  | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
179
188
  | --- | --- | --- | --- | --- | --- | --- |
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 |
189
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 13.18:1 | Lc 82.8 | ✅ pass |
190
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 11.99:1 | Lc 81.8 | ✅ pass |
191
+ | `--text-soft` | `--accent-soft` over `--surface-muted` | Neutral tag/badge text on an accent tint | AA text (4.5:1) | 6.66:1 | Lc 65.0 | pass |
192
+ | `--accent-text` | `--accent-soft` | Accent text on an accent tint | Advisory (translucent tint — not gated) | 1.33:1 | Lc 15.3 | ℹ️ not gated |
193
+ | `--accent-text` | `--bg-accent` | Accent text on an accent-tinted surface | Advisory (translucent tint — not gated) | 1.37:1 | Lc 17.3 | ℹ️ not gated |
184
194
  | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 13.75:1 | Lc 79.7 | ✅ pass |
185
195
  | `--on-accent` | `--accent` | Ink on an accent fill | AA text (4.5:1) | 13.75:1 | Lc 79.7 | ✅ pass |
186
196
  | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 12.27:1 | Lc 78.5 | ✅ pass |
package/docs/crosshair.md CHANGED
@@ -48,7 +48,7 @@ document.querySelector('[data-bronto-crosshair]').addEventListener(
48
48
  | `ui-crosshair` | The overlay. Hidden until `.is-active` (set on the first pointer move over the plot). |
49
49
  | `ui-crosshair__line` + `--x` / `--y` | Vertical / horizontal rule, positioned by `--crosshair-x` / `--crosshair-y`. |
50
50
  | `ui-crosshair__badge` | An axis value chip (you set its text + edge). |
51
- | `ui-readout` | A pinned readout chip; follows the crosshair point. |
51
+ | `ui-readout` inside `.ui-crosshair` | A pinned readout chip; follows the crosshair point and flips before/above near plot edges via `data-readout-inline` / `data-readout-block`. Standalone `.ui-readout` remains the dot-matrix numeric row from `dots.css`. |
52
52
 
53
53
  `ui-crosshair--muted` is a subtler neutral crosshair. `ui.crosshair({ muted })`
54
54
  builds the class string. Include only the lines you need (just `--x` for a
package/docs/d2.md CHANGED
@@ -153,6 +153,43 @@ paste the resolved hex from [`tokens/resolved.json`](./architecture.md) for a
153
153
  For anything larger or graph-laid-out, run D2 with the theme map and freeze its
154
154
  output — don't hand-lay a complex graph.
155
155
 
156
+ ### Tokenize D2 output — one inline SVG that re-skins live
157
+
158
+ A frozen D2 SVG carries **resolved hex**, so a dynamic (screen-only) report
159
+ would need a light SVG and a dark SVG and JS/CSS to swap them — and the hidden
160
+ twin is dead weight. Instead, post-process the rendered SVG's colours back into
161
+ tokens, and ONE inline SVG re-skins live when `data-theme` flips (this is the
162
+ inverse of the resolved-hex rule above: it only works for **inline** SVG in a
163
+ themed page, never for `file://`/PDF artifacts or `<img>` embeds):
164
+
165
+ 1. Render **light only** with the theme map (`brontoD2Vars()` prepended).
166
+ 2. D2 emits each colour twice: as inline `fill="#hex"`/`stroke="#hex"` AND as
167
+ class rules in an embedded `<style>` block — **not just `.fill-*` /
168
+ `.stroke-*`: there are also `.color-*` and `.background-color-*` rules**
169
+ (they carry the same hex and trip any raw-colour gate). **The style rules
170
+ win over the inline attributes**, so strip ALL hex-bearing rules from the
171
+ `<style>` first — only then do attribute rewrites take effect.
172
+ 3. Rewrite the inline hex → `var(--token)` using the slot table above
173
+ (`N1`→`--text`, `N4`/`B1`→`--line-strong`, `N6`/`B4`→`--panel-soft`,
174
+ `B6`→`--panel`, accent class fill→`--accent`, its ink→`--on-accent`, …).
175
+ 4. Leave `<mask>` `fill="black"`/`"white"` keywords alone — that is a
176
+ luminance mask, not a colour.
177
+ 5. Make the outer `<svg>` fluid (drop `width`/`height`, keep `viewBox`) and
178
+ inject `<title>` + `<desc>` with `role="img" aria-labelledby` before
179
+ inlining.
180
+
181
+ The result follows the page theme with zero swap machinery, and avoids the
182
+ visual-QA traps of the two-SVG approach (a `display:none` twin is easy to
183
+ flag as a blank figure).
184
+
185
+ > **Avoid `tooltip:` and `|md` markdown shapes in frozen report SVGs.** Both
186
+ > make D2 embed GitHub-Primer styling that survives tokenization: tooltips
187
+ > render Octicon info-icons and markdown text ships Primer CSS, each full of
188
+ > foreign `var(--color-*)` references and extra hex (`#2e3346`-class values
189
+ > outside the theme map). Fold tooltip text into the node label and use plain
190
+ > labels or `shape: text` instead — or strip the tooltip appendix from the
191
+ > SVG before inlining.
192
+
156
193
  ### Fit to small screens
157
194
 
158
195
  D2 emits an SVG with explicit `width`/`height` from its layout, so on a narrow
package/docs/dots.md CHANGED
@@ -62,7 +62,7 @@ container MUST carry a host-written `role="img"` + `aria-label` with the exact
62
62
  value — rounding to whole cells is presentation-only, so keep the figure in the
63
63
  label (WCAG 1.4.1).
64
64
 
65
- ### `.ui-matrix` cell grid
65
+ ### `.ui-dotmatrix` cell grid
66
66
 
67
67
  `.ui-dotmatrix` is the raw data-bound grid (the one the glyphs render on): a grid
68
68
  of `.ui-dotmatrix__cell` (with `--hot` / `--accent` tones) plus the `--reveal` /
@@ -125,6 +125,9 @@ Knobs: `--v`, `--gauge-size`, `--gauge-sweep` (default 270deg), `--gauge-from`,
125
125
  The row wrapper produced by `renderReadout` (see `docs/glyphs.md`) — a Nothing-style
126
126
  hero numeric composed from digit glyphs. Tune character spacing with
127
127
  `--readout-gap`; spaces render as a `.ui-readout__spacer` of width `--readout-space`.
128
+ Large multi-character readouts are intentionally intrinsic; on narrow report or
129
+ demo pages, either tune the glyph dot size or wrap the readout in a local
130
+ keyboard-focusable horizontal scroller so it does not widen the page.
128
131
 
129
132
  ### `.ui-spark--dots`
130
133