@ponchia/ui 0.6.5 → 0.6.7

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 (108) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +43 -23
  3. package/behaviors/carousel.d.ts.map +1 -1
  4. package/behaviors/carousel.js +3 -0
  5. package/behaviors/dialog.d.ts.map +1 -1
  6. package/behaviors/dialog.js +14 -8
  7. package/behaviors/forms.d.ts.map +1 -1
  8. package/behaviors/forms.js +11 -5
  9. package/behaviors/index.d.ts +2 -0
  10. package/behaviors/index.d.ts.map +1 -1
  11. package/behaviors/index.js +2 -0
  12. package/behaviors/internal.d.ts +2 -1
  13. package/behaviors/internal.d.ts.map +1 -1
  14. package/behaviors/internal.js +23 -3
  15. package/behaviors/legend.d.ts.map +1 -1
  16. package/behaviors/legend.js +41 -9
  17. package/behaviors/splitter.d.ts +26 -0
  18. package/behaviors/splitter.d.ts.map +1 -0
  19. package/behaviors/splitter.js +200 -0
  20. package/behaviors/table.js +3 -3
  21. package/behaviors/theme.js +2 -2
  22. package/classes/classes.json +230 -4
  23. package/classes/index.d.ts +49 -1
  24. package/classes/index.js +56 -1
  25. package/classes/vscode.css-custom-data.json +1 -1
  26. package/css/analytical.css +3 -1
  27. package/css/app.css +4 -4
  28. package/css/clamp.css +92 -0
  29. package/css/figure.css +102 -0
  30. package/css/highlights.css +50 -0
  31. package/css/interval.css +90 -0
  32. package/css/primitives.css +2 -3
  33. package/css/report-kit.css +38 -0
  34. package/css/report.css +51 -4
  35. package/css/sidenote.css +12 -2
  36. package/css/site.css +2 -1
  37. package/css/sources.css +5 -0
  38. package/css/state.css +120 -1
  39. package/css/table.css +4 -0
  40. package/css/tokens.css +9 -9
  41. package/css/workbench.css +101 -8
  42. package/dist/bronto.css +1 -1
  43. package/dist/css/analytical.css +1 -1
  44. package/dist/css/app.css +1 -1
  45. package/dist/css/clamp.css +1 -0
  46. package/dist/css/figure.css +1 -0
  47. package/dist/css/highlights.css +1 -0
  48. package/dist/css/interval.css +1 -0
  49. package/dist/css/primitives.css +1 -1
  50. package/dist/css/report-kit.css +1 -0
  51. package/dist/css/report.css +1 -1
  52. package/dist/css/sidenote.css +1 -1
  53. package/dist/css/site.css +1 -1
  54. package/dist/css/sources.css +1 -1
  55. package/dist/css/state.css +1 -1
  56. package/dist/css/table.css +1 -1
  57. package/dist/css/tokens.css +1 -1
  58. package/dist/css/workbench.css +1 -1
  59. package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
  60. package/docs/architecture.md +67 -43
  61. package/docs/clamp.md +49 -0
  62. package/docs/contrast.md +34 -24
  63. package/docs/d2.md +37 -0
  64. package/docs/figure.md +71 -0
  65. package/docs/frontier-primitives.md +48 -23
  66. package/docs/highlights.md +52 -0
  67. package/docs/interop/tailwind.md +148 -0
  68. package/docs/interval.md +55 -0
  69. package/docs/legends.md +3 -2
  70. package/docs/mermaid.md +6 -0
  71. package/docs/migrations/0.2-to-0.3.md +80 -0
  72. package/docs/migrations/0.3-to-0.4.md +48 -0
  73. package/docs/migrations/0.4-to-0.5.md +96 -0
  74. package/docs/migrations/0.5-to-0.6.md +82 -0
  75. package/docs/package-contract.md +40 -2
  76. package/docs/reference.md +79 -6
  77. package/docs/reporting.md +132 -56
  78. package/docs/sidenote.md +7 -1
  79. package/docs/sources.md +1 -1
  80. package/docs/stability.md +5 -3
  81. package/docs/state.md +67 -10
  82. package/docs/theming.md +10 -2
  83. package/docs/usage.md +31 -11
  84. package/docs/workbench.md +59 -18
  85. package/llms.txt +82 -14
  86. package/package.json +68 -6
  87. package/qwik/index.d.ts +1 -0
  88. package/qwik/index.d.ts.map +1 -1
  89. package/qwik/index.js +26 -21
  90. package/react/index.d.ts +1 -0
  91. package/react/index.d.ts.map +1 -1
  92. package/react/index.js +4 -1
  93. package/schemas/report-claims.v1.schema.json +137 -0
  94. package/solid/index.d.ts +2 -0
  95. package/solid/index.d.ts.map +1 -1
  96. package/solid/index.js +3 -0
  97. package/svelte/index.d.ts +88 -0
  98. package/svelte/index.d.ts.map +1 -0
  99. package/svelte/index.js +166 -0
  100. package/tailwind.css +87 -0
  101. package/tokens/figma.variables.json +2241 -0
  102. package/tokens/index.js +1 -1
  103. package/tokens/index.json +2 -2
  104. package/tokens/resolved.json +3 -3
  105. package/tokens/tokens.dtcg.json +1 -1
  106. package/vue/index.d.ts +79 -0
  107. package/vue/index.d.ts.map +1 -0
  108. package/vue/index.js +197 -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
+ `/skins`, `/charts`, `/mermaid`, `/d2`, `/vega`). This is a permanent,
103
+ 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. |
@@ -134,9 +142,9 @@ gating" below), so a version that fails any invariant never reaches npm.
134
142
  | Invariant | Enforced by |
135
143
  | ----------------------------------------------- | ------------------- |
136
144
  | 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` |
145
+ | 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
146
  | `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` |
147
+ | `connectors`/`annotations`/`react`/`solid`/`qwik`/`svelte`/`vue`/`behaviors` `.d.ts` (+ maps) == fresh `tsc` emit of their JSDoc | `check-dts-emit.mjs` |
140
148
  | legend swatch colours ⊆ `charts.js` · opt-in | `check-legend.mjs` |
141
149
  | color tokens tiered · no raw chromatic color in components | `check-color-policy.mjs` |
142
150
  | `css/skins.css` ⇄ `tokens/skins.js` · colorways opt-in | `check-skins.mjs` |
@@ -145,6 +153,10 @@ gating" below), so a version that fails any invariant never reaches npm.
145
153
  | `shiki/nothing.json` valid + on rationed palette | `check-shiki.mjs` |
146
154
  | `dist/*.css` == fresh build of `css/` + budget | `check-dist.mjs` |
147
155
  | published tarball == intended `files` only | `check-pack.mjs` |
156
+ | packed public text contains no private terms, local paths, or secret-looking assignments | `check-public-hygiene.mjs` |
157
+ | CSS custom-property references resolve or carry an explicit fallback/host boundary | `check-variables.mjs` |
158
+ | `MIGRATIONS.json` edges have structured rules and matching docs | `check-migrations.mjs` |
159
+ | example inventory ⇄ CI matrix ⇄ browser-smoke list ⇄ README rows ⇄ preview ports | `check-examples.mjs` |
148
160
  | published `.d.ts` compile + reject typos | `tsc` (`check:types`) |
149
161
  | CSS style/correctness | Stylelint |
150
162
  | non-CSS source style | Prettier (`check:format`) |
@@ -163,29 +175,40 @@ freshness (`check-fresh`).
163
175
 
164
176
  ## Release gating
165
177
 
166
- `release.yml` (on a pushed `v*` tag) is a five-job DAG, serialized by a
178
+ `release.yml` (on a pushed `v*` tag) is a six-job DAG, serialized by a
167
179
  `concurrency: release-publish` group so two tags can't race the dist-tag
168
180
  pointer:
169
181
 
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.
182
+ - `validate` — read-only: verifies the tag commit is reachable from `main`,
183
+ then runs `npm run check`, `npm test`, and the tag↔version match. `check`
184
+ includes `check:release`; for a prerelease tag the base version's CHANGELOG
185
+ section need only exist (`## Unreleased x.y.z` is fine) — only a stable
186
+ release must carry a dated heading.
187
+ - `e2e` — `needs: validate`: Playwright (visual + axe a11y, both themes,
188
+ cross-engine) in the pinned `mcr.microsoft.com/playwright` container. Local
189
+ cross-engine reproduction without screenshot rasterisation is
190
+ `npm run test:e2e:nonpixel`; use `npm run test:e2e` or
191
+ `npm run test:e2e:chromium` only in the pinned container when the pixel
192
+ baseline gate itself is in scope.
176
193
  - `examples` — `needs: validate`: builds the downstream example
177
194
  apps against the **packed tarball**, mirroring CI. Catches a broken
178
195
  published surface (exports map / missing file / unresolved subpath)
179
196
  that `check:pack`'s file-allowlist inspection cannot — so the release
180
197
  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`).
198
+ - `publish-preflight` — `needs: [validate, e2e, examples]`: installs with
199
+ lifecycle scripts disabled, runs `npm pack --dry-run --ignore-scripts`, and
200
+ writes the pack manifest + size report to the job summary for review before
201
+ the protected publish approval.
202
+ - `publish-npm` `needs: publish-preflight`: `npm publish --ignore-scripts`
203
+ with provenance. Runs in the `npm-publish` **Environment**
204
+ (required-reviewer protection), so after the gates and preflight pass the run
205
+ pauses for a manual approval in the Actions UI before anything reaches npm —
206
+ a guard against an accidental tag push publishing. Dist-tag is derived from
207
+ the tag: stable (`v0.4.0`) → `latest`; SemVer prerelease (`v0.4.0-rc.1`, any
208
+ hyphenated identifier) → `next`, so the default `npm i @ponchia/ui` never
209
+ moves onto an unstable build (opt in with `@ponchia/ui@next`). Post-publish
210
+ `npm view` registry observation is best-effort only: a registry read flake must
211
+ not fail the job after the immutable publish already succeeded.
189
212
  - `release-notes` — `needs: publish-npm`: a GitHub Release for visibility
190
213
  (transitively gated on a successful publish, hence on the gates above);
191
214
  prerelease tags are flagged so they aren't surfaced as "Latest". The Release
@@ -194,7 +217,7 @@ pointer:
194
217
  source of truth, surfaced where readers look.
195
218
 
196
219
  Because the documented install path is the npm package, **the npm publish
197
- is a real gate**: if `validate`, `e2e`, *or* `examples` fails,
220
+ is a real gate**: if `validate`, `e2e`, `examples`, or `publish-preflight` fails,
198
221
  `publish-npm` never runs, the version never reaches the registry, and
199
222
  consumers never resolve it.
200
223
  (Corollary: a flaky `e2e` blocks releases — that is deliberate; fix the
@@ -210,9 +233,9 @@ Process still applies: bump `package.json`, land on `main`, go green, tag.
210
233
  ## Decision — distribution: npm public `@ponchia/ui`
211
234
 
212
235
  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
236
+ heterogeneous web frontends (Astro, SvelteKit, React, Solid, Qwik, Vue,
237
+ Tailwind, vanilla), several deploying via third-party CI. The only option where
238
+ onboarding a new frontend is `npm i @ponchia/ui` with zero per-consumer config is **npm
216
239
  public**, and it uniquely also closes the release-gating gap (publish *is*
217
240
  the gate). GitHub Packages was rejected: it requires auth to install even
218
241
  public packages, i.e. an `.npmrc` + token on every frontend and CI runner —
@@ -237,8 +260,9 @@ explained, not surprising.
237
260
  provenance.
238
261
  - Run `npm pack --dry-run --json` locally or from CI logs and confirm the
239
262
  intended file count/payload.
240
- - Build the packed examples matrix (vanilla, Astro, SvelteKit, React, Solid, Qwik)
241
- from the tarball, not a workspace link.
263
+ - Build the packed examples matrix from the tarball, not a workspace link:
264
+ `npm run test:examples` covers vanilla, Astro, SvelteKit, Vue, React, Solid,
265
+ Qwik, Tailwind, and report-static, with browser smokes for runtime examples.
242
266
  - Confirm the GitHub Release body matches the curated changelog section.
243
267
  - If a bad package is published, deprecate that exact version on npm, publish a
244
268
  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/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/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/figure.md ADDED
@@ -0,0 +1,71 @@
1
+ # Figure
2
+
3
+ `@ponchia/ui/css/figure.css` is an opt-in analytical/report figure stage. It
4
+ does not render charts. It gives charts, diagrams, screenshots, and annotated
5
+ SVGs a stable frame: caption, media stage, optional overlay, optional key, and
6
+ fallback data.
7
+
8
+ ```css
9
+ @import '@ponchia/ui';
10
+ @import '@ponchia/ui/css/figure.css';
11
+ @import '@ponchia/ui/css/legend.css';
12
+ ```
13
+
14
+ Use it with `ui-report__figure` when the figure sits in a report. Use it alone
15
+ when the same stage appears in a dashboard, doc page, or generated artifact.
16
+
17
+ ```html
18
+ <figure class="ui-figure ui-report__figure ui-print-exact" role="group" aria-labelledby="fig-title">
19
+ <figcaption id="fig-title" class="ui-figure__caption ui-report__caption">
20
+ Fig 1 - Weekly focus split
21
+ </figcaption>
22
+ <div class="ui-figure__body ui-figure__body--key-right">
23
+ <div class="ui-figure__stage" style="--figure-max-inline: 30rem; --figure-min-block: 12rem">
24
+ <svg class="ui-figure__media" viewBox="0 0 320 120" role="img" aria-labelledby="svg-title svg-desc">
25
+ <title id="svg-title">Weekly focus split</title>
26
+ <desc id="svg-desc">Research is the largest category.</desc>
27
+ <rect x="80" y="24" width="180" height="20" fill="var(--chart-1)" />
28
+ <rect x="80" y="64" width="110" height="20" fill="var(--chart-2)" />
29
+ </svg>
30
+ <svg class="ui-figure__overlay" viewBox="0 0 320 120" aria-hidden="true">
31
+ <path class="ui-annotation__connector" d="M260,34L292,14" />
32
+ </svg>
33
+ </div>
34
+ <div class="ui-figure__key">
35
+ <ul class="ui-legend" aria-label="Series">...</ul>
36
+ </div>
37
+ </div>
38
+ <div class="ui-figure__data ui-table-wrap">
39
+ <table class="ui-table ui-table--dense">...</table>
40
+ </div>
41
+ </figure>
42
+ ```
43
+
44
+ ## Contract
45
+
46
+ | Class | Role |
47
+ | --- | --- |
48
+ | `.ui-figure` | Figure wrapper. |
49
+ | `.ui-figure__caption` | Caption text; can compose with `ui-report__caption`. |
50
+ | `.ui-figure__body` | Stage/key layout wrapper. |
51
+ | `.ui-figure__body--key-right` | Two-column body: visual stage plus right-side key. |
52
+ | `.ui-figure__stage` | Stable, centered media stage; `position: relative` for overlays. |
53
+ | `.ui-figure__media` | Primary SVG, image, canvas, or rendered figure output. |
54
+ | `.ui-figure__overlay` | Absolute, pointer-transparent overlay for annotations or guides. |
55
+ | `.ui-figure__key` | Legend/key slot. |
56
+ | `.ui-figure__data` | Fallback data slot, usually a `ui-table-wrap`. |
57
+
58
+ | Custom property | On | Meaning |
59
+ | --- | --- | --- |
60
+ | `--figure-max-inline` | `.ui-figure__stage` | Maximum stage width, default `42rem`. |
61
+ | `--figure-min-block` | `.ui-figure__stage` | Reserved stage height for late-rendered media. |
62
+ | `--figure-key-width` | `.ui-figure__body--key-right` | Right key column width before mobile collapse. |
63
+
64
+ ## Boundary
65
+
66
+ - Bronto owns layout, responsive collapse, overlay positioning, print spacing,
67
+ and class names.
68
+ - The host owns scales, data binding, SVG/canvas/chart rendering, fallback table
69
+ rows, annotation text, and accessibility labels.
70
+ - A figure should always have a `<figcaption>`. Data-bearing SVGs still need
71
+ `<title>` and `<desc>`.
@@ -25,7 +25,7 @@ The pattern that worked for annotations should stay the rule:
25
25
  - Prefer CSS and markup first. Add JS only when the browser cannot express the
26
26
  behavior without measuring, filtering, keyboard state, or pointer tracking.
27
27
 
28
- This keeps Bronto useful across Astro, SvelteKit, React, Solid, Qwik, plain
28
+ This keeps Bronto useful across Astro, SvelteKit, Vue, React, Solid, Qwik, plain
29
29
  HTML, and generated static reports without becoming a framework component kit.
30
30
 
31
31
  ## Already aligned in 0.5.0
@@ -164,19 +164,21 @@ and footnotes, but not a trust grammar. The shipped surface and its trust-state
164
164
  vocabulary are documented with the component; richer preview popovers remain
165
165
  host-owned.
166
166
 
167
- ### 2. Lifecycle and system-state UI — 🟡 `ui-state` family shipped in 0.5.0
167
+ ### 2. Lifecycle and system-state UI — 🟡 `ui-state` family shipped in 0.5.0, `ui-job` added for 0.6.7
168
168
 
169
169
  Shipped as `@ponchia/ui/css/state.css` (`ui-state` + the canonical state matrix
170
- + `ui-syncbar`), matching the "good first build" below. `ui-job` (background
171
- progress) and `ui-conflict` (resolution affordances) remain deferred until a
172
- consumer needs them; `ui-review-state` is covered by the reviewed/needs-review
170
+ + `ui-syncbar`), matching the "good first build" below. The 0.6.7 local pass
171
+ adds `ui-job`: a persistent background-job row with determinate progress,
172
+ written status, and action slots, while polling/retry/cancel semantics stay in
173
+ the host. `ui-conflict` (resolution affordances) remains deferred until a
174
+ consumer needs it; `ui-review-state` is covered by the reviewed/needs-review
173
175
  state modifiers.
174
176
 
175
177
  Why it matters: serious apps spend a lot of time in states like saving, saved,
176
178
  queued, offline, stale, retrying, conflicted, locked, reviewed, and background
177
179
  job running. These states are usually improvised per product, so even good apps
178
- feel inconsistent. Still deferred: `ui-job` (background progress) and
179
- `ui-conflict` (resolution affordances), each until a consumer needs it.
180
+ feel inconsistent. Still deferred: `ui-conflict` (resolution affordances), until
181
+ a consumer needs it.
180
182
 
181
183
  ### 3. Command-first UI — ✅ shipped in 0.5.0
182
184
 
@@ -193,20 +195,19 @@ design-system contract: shortcuts, actions, groups, disabled reasons, context,
193
195
  and command result feedback. The host still owns the action registry and
194
196
  execution; global Cmd/Ctrl+K stays opt-in by design.
195
197
 
196
- ### 4. Workbench UI — 🟡 inspector / property / selectionbar shipped in 0.5.0
198
+ ### 4. Workbench UI — 🟡 inspector / property / selectionbar shipped in 0.5.0, splitter added in 0.6.7
197
199
 
198
200
  Shipped as `@ponchia/ui/css/workbench.css` (`ui-inspector`, `ui-property`,
199
- `ui-selectionbar`) — the low-risk CSS core below. Resizable split panes
200
- (`ui-splitter`, an ARIA window-splitter behavior) and drag handles remain
201
- deferred until a consumer needs them.
201
+ `ui-selectionbar`, `ui-splitter`) plus `initSplitter` — the low-risk workbench
202
+ core below. Splitters own the focusable ARIA separator, keyboard/pointer resize,
203
+ `--splitter-pos`, and `aria-valuenow`; the host owns pane contents, persistence,
204
+ collapse policy, and saved layout state.
202
205
 
203
206
  Why it matters: real tools need inspectors, object action bars, split panes,
204
207
  resize handles, property rows, dense trees, and selected-object affordances.
205
208
  Generic UI kits tend to stop at cards/tables/forms, leaving every app to build
206
- its own half-consistent workbench. Still open: a `ui-splitter` ARIA
207
- window-splitter behavior (focusable separator, `aria-valuemin/max/now`,
208
- arrow-key resize) and drag/drop affordances — both deferred, and Bronto should
209
- style drag handles, not become a drag-and-drop framework.
209
+ its own half-consistent workbench. Still open: drag/drop affordances. Bronto
210
+ should style drag handles and drop targets, not become a drag-and-drop framework.
210
211
 
211
212
  ### 5. Generated-content and AI trust primitives — 🟡 shipped in 0.5.0
212
213
 
@@ -226,14 +227,38 @@ precision signal the product does not have).
226
227
 
227
228
  The CSS cores of candidates 1–5 shipped in 0.5.0, the 2026 scout batch shipped
228
229
  in PRs #97/#100, and scout batch #2 (textref / bullet / term / toc / tree)
229
- shipped 2026-06-04. The remaining active work, in order:
230
-
231
- 1. `ui-job` / `ui-conflict` lifecycle surfaces (candidate 2).
232
- 2. The `ui-splitter` ARIA window-splitter behavior + drag affordances
233
- (candidate 4) also the gating consumer for `ui-tree`'s deferred roving-focus
234
- tree kernel.
235
-
236
- Deferred items stay gated on a real consumer needing them. This order keeps
230
+ shipped 2026-06-04.
231
+
232
+ **The proven lane is report / provenance / explanation** it is the only lane
233
+ with a real consumer (LLM-authored reports). The command, workbench, and
234
+ durable-state lanes shipped their CSS cores and have had **no non-demo
235
+ consumer since**; their follow-ons are demand-gated, not queued. Active work
236
+ is therefore consolidation of the report lane (hub routing, print/PDF
237
+ fidelity, consumer-contract gates), not new surfaces.
238
+
239
+ ### Report-lane primitives shipped in 0.6.7
240
+
241
+ From the 2026-06-09 local scout. These were kept on merit, then shipped only
242
+ after `docs/reporting.md` carried routing rows so the leaves are discoverable:
243
+
244
+ 1. `ui-interval` — honest low–high uncertainty span + ± chip, inline in
245
+ reports; the error-bar grammar generic kits never ship.
246
+ 2. `ui-clamp` — N-line clamp + fade + native show-more for claim-basis and
247
+ source excerpts (`-webkit-line-clamp` + `mask-image` + `<details>`).
248
+ 3. `ui-highlights` — cited-evidence / search-hit spans painted from host
249
+ Ranges via the CSS Custom Highlight API (progressive enhancement; clean
250
+ no-op below the floor).
251
+ 4. `ui-figure` — stable chart/diagram/screenshot stage with overlay/key/fallback
252
+ slots; it composes with report figures but still refuses chart scales.
253
+
254
+ ### Dormant (build with the first real app consumer, not before)
255
+
256
+ - `ui-conflict` lifecycle surface (candidate 2).
257
+ - Drag/drop workbench affordances (candidate 4) and any gating consumer for
258
+ `ui-tree`'s deferred roving-focus tree kernel.
259
+ - Any command/workbench follow-ons beyond the shipped cores.
260
+
261
+ Dormant items stay gated on a real consumer needing them. This posture keeps
237
262
  Bronto differentiated while staying inside its core philosophy: small,
238
263
  framework-agnostic primitives that make complex interfaces clearer.
239
264