@ponchia/ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +28 -5
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +315 -45
  7. package/behaviors/carousel.js +17 -16
  8. package/behaviors/combobox.js +47 -16
  9. package/behaviors/command.js +18 -15
  10. package/behaviors/connectors.js +4 -5
  11. package/behaviors/crosshair.js +4 -5
  12. package/behaviors/dialog.js +3 -2
  13. package/behaviors/disclosure.js +3 -2
  14. package/behaviors/dismissible.js +3 -2
  15. package/behaviors/forms.js +41 -13
  16. package/behaviors/glyph.js +4 -5
  17. package/behaviors/internal.js +47 -0
  18. package/behaviors/legend.js +23 -2
  19. package/behaviors/menu.js +3 -2
  20. package/behaviors/popover.js +78 -7
  21. package/behaviors/spotlight.js +4 -5
  22. package/behaviors/table.js +39 -12
  23. package/behaviors/tabs.js +14 -14
  24. package/behaviors/theme.js +5 -3
  25. package/behaviors/toast.js +13 -1
  26. package/classes/classes.json +1857 -0
  27. package/classes/index.d.ts +28 -13
  28. package/classes/index.js +34 -18
  29. package/classes/vscode.css-custom-data.json +12 -0
  30. package/connectors/index.d.ts +189 -69
  31. package/connectors/index.d.ts.map +1 -0
  32. package/connectors/index.js +120 -24
  33. package/css/app.css +43 -13
  34. package/css/base.css +15 -10
  35. package/css/connectors.css +17 -0
  36. package/css/content.css +7 -1
  37. package/css/dataviz.css +5 -1
  38. package/css/disclosure.css +38 -6
  39. package/css/dots.css +57 -0
  40. package/css/feedback.css +60 -2
  41. package/css/forms.css +42 -1
  42. package/css/legend.css +11 -7
  43. package/css/marks.css +38 -8
  44. package/css/motion.css +24 -44
  45. package/css/navigation.css +7 -0
  46. package/css/overlay.css +31 -1
  47. package/css/primitives.css +91 -5
  48. package/css/report.css +40 -63
  49. package/css/site.css +16 -2
  50. package/css/sources.css +43 -1
  51. package/css/spotlight.css +1 -1
  52. package/css/tokens.css +36 -1
  53. package/css/workbench.css +1 -1
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -1
  56. package/dist/css/app.css +1 -1
  57. package/dist/css/base.css +1 -1
  58. package/dist/css/connectors.css +1 -1
  59. package/dist/css/content.css +1 -1
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/forms.css +1 -1
  64. package/dist/css/legend.css +1 -1
  65. package/dist/css/marks.css +1 -1
  66. package/dist/css/motion.css +1 -1
  67. package/dist/css/navigation.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -1
  71. package/dist/css/site.css +1 -1
  72. package/dist/css/sources.css +1 -1
  73. package/dist/css/spotlight.css +1 -1
  74. package/dist/css/tokens.css +1 -1
  75. package/dist/css/workbench.css +1 -1
  76. package/docs/adr/0003-theme-model.md +1 -1
  77. package/docs/annotations.md +94 -14
  78. package/docs/architecture.md +50 -6
  79. package/docs/contrast.md +116 -92
  80. package/docs/d2.md +195 -0
  81. package/docs/legends.md +18 -2
  82. package/docs/marks.md +9 -2
  83. package/docs/mermaid.md +152 -0
  84. package/docs/reference.md +78 -22
  85. package/docs/reporting.md +395 -57
  86. package/docs/sources.md +27 -0
  87. package/docs/stability.md +9 -2
  88. package/docs/usage.md +101 -4
  89. package/docs/vega.md +225 -0
  90. package/docs/workbench.md +7 -1
  91. package/glyphs/glyphs.js +6 -4
  92. package/llms.txt +139 -14
  93. package/package.json +50 -12
  94. package/qwik/index.d.ts +42 -59
  95. package/qwik/index.d.ts.map +1 -0
  96. package/qwik/index.js +55 -3
  97. package/react/index.d.ts +39 -61
  98. package/react/index.d.ts.map +1 -0
  99. package/react/index.js +57 -3
  100. package/solid/index.d.ts +64 -61
  101. package/solid/index.d.ts.map +1 -0
  102. package/solid/index.js +60 -3
  103. package/tokens/d2.d.ts +38 -0
  104. package/tokens/d2.js +71 -0
  105. package/tokens/d2.json +43 -0
  106. package/tokens/index.d.ts +5 -5
  107. package/tokens/index.js +15 -1
  108. package/tokens/index.json +9 -0
  109. package/tokens/mermaid.d.ts +23 -0
  110. package/tokens/mermaid.js +181 -0
  111. package/tokens/mermaid.json +163 -0
  112. package/tokens/resolved.json +45 -1
  113. package/tokens/skins.js +3 -2
  114. package/tokens/tokens.dtcg.json +26 -0
  115. package/tokens/vega.d.ts +34 -0
  116. package/tokens/vega.js +155 -0
  117. package/tokens/vega.json +179 -0
package/docs/reporting.md CHANGED
@@ -15,10 +15,17 @@ rewrites them:
15
15
  @import '@ponchia/ui/css/legend.css';
16
16
  ```
17
17
 
18
+ **`dist/bronto.css` is the standard component set only — it does NOT contain
19
+ the report, chart, annotation, or legend layers.** Those are opt-in leaves
20
+ under `dist/css/`; a report links the default bundle *and* each leaf it uses.
21
+ Forgetting them is the most common way an LLM-emitted report renders unstyled.
22
+
18
23
  For standalone browser HTML, use real stylesheet URLs. Package specifiers like
19
- `@ponchia/ui/css/report.css` do not resolve in a saved `.html` file:
24
+ `@ponchia/ui/css/report.css` do not resolve in a saved `.html` file — and note
25
+ the path is `dist/css/`, the built leaf, not the source `css/`:
20
26
 
21
27
  ```html
28
+ <!-- installed locally -->
22
29
  <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/bronto.css" />
23
30
  <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/css/report.css" />
24
31
  <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/css/dataviz.css" />
@@ -26,16 +33,51 @@ For standalone browser HTML, use real stylesheet URLs. Package specifiers like
26
33
  <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/css/legend.css" />
27
34
  ```
28
35
 
29
- If you copy the built CSS next to the report, keep the same relationship between
30
- `dist/bronto.css`, `dist/css/report.css`, `dist/css/dataviz.css`,
31
- `dist/css/annotations.css`, `dist/css/legend.css`, and `fonts/` so font URLs
32
- continue to resolve.
36
+ No install? Link the same files from a CDN. Pin the version pre-1.0, breaking
37
+ changes ship in the minor (see [stability.md](./stability.md)):
38
+
39
+ ```html
40
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/bronto.css" />
41
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/css/report.css" />
42
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/css/dataviz.css" />
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/css/annotations.css" />
44
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/css/legend.css" />
45
+ ```
46
+
47
+ The CDN serves the package's own `fonts/` next to the CSS, so font URLs resolve
48
+ with no extra setup. If instead you copy the built CSS next to the report, keep
49
+ the same relationship between `dist/bronto.css`, the `dist/css/*` leaves, and
50
+ `fonts/` so the relative font URLs continue to resolve.
33
51
 
34
52
  The report layer is static and PDF-first. It does not initialize behaviors and
35
53
  does not sanitize content. If a report includes arbitrary LLM, CMS, or user HTML,
36
54
  sanitize that content before rendering it and do not initialize
37
55
  `data-bronto-*` behaviors on the generated region.
38
56
 
57
+ ## The analytical toolbox in a report
58
+
59
+ `css/report.css` gives you the document grammar (covers, sections, findings,
60
+ evidence). The _content_ inside those sections is where the rest of the
61
+ analytical layer earns its place. Each one is an opt-in import that stays out of
62
+ the default bundle — add the leaves a given report actually needs, or pull the
63
+ whole set with `@ponchia/ui/css/analytical.css`. Reach for:
64
+
65
+ | Layer | Import | Reach for it when… |
66
+ | --- | --- | --- |
67
+ | **Marks** (`.ui-mark`, `.ui-bracket-note`) | `css/marks.css` | You want to emphasise a phrase _in prose_ — a highlight on the finding, an underline on a risk, or a bracket around an evidence/caveat passage. The inline counterpart to annotations. See [marks.md](./marks.md). |
68
+ | **Sources / provenance** (`.ui-citation`, `.ui-source-card`, `.ui-source-list`, `.ui-provenance`) | `css/sources.css` | The report makes claims a reader will question — "where did this come from?". A CSS-only trust layer whose cross-cutting state modifier (`.ui-src--verified`, plus reviewed / generated / unverified / stale / conflict) sets a rationed tone, always paired with a written label, never colour alone. See [sources.md](./sources.md). |
69
+ | **Annotations** (`.ui-annotation*`) | `css/annotations.css` | A figure needs an explicit callout — a peak, a limit, a watched region — or a small decorative margin mark. SVG only. See [annotations.md](./annotations.md) and the [off-chart + scaling notes](./annotations.md#using-annotations-off-chart) before you size one. |
70
+ | **Legends / data keys** (`.ui-legend*`) | `css/legend.css` | A chart figure needs a colour key. WCAG 1.4.1 by construction. See [legends.md](./legends.md). |
71
+ | **Mermaid theme** (`@ponchia/ui/mermaid`) | _(JS/JSON, no CSS)_ | The report embeds a [Mermaid](https://mermaid.js.org) diagram (flowchart, sequence, pie…) and you want it on-brand instead of generic. A resolved `base` theme projected from the same tokens as `charts.json`; annotate the rendered SVG with the annotation layer. See [mermaid.md](./mermaid.md). |
72
+ | **D2 theme** (`@ponchia/ui/d2`) | _(JS/JSON, no CSS)_ | The report embeds a [D2](https://d2lang.com) diagram and you want it on-brand. Resolved theme-override slots (monochrome base + one rationed accent) projected from the same tokens; annotate the rendered SVG. See [d2.md](./d2.md). |
73
+ | **Generated-content trust** (`.ui-generated`, `.ui-origin-label`, `.ui-reasoning`, `.ui-tool-log`) | `css/generated.css` | The report (or a section of it) is AI/system-authored and should _say so_ — an origin label plus quiet, collapsible reasoning / tool-call logs. Pairs with the sources layer. See [generated.md](./generated.md). |
74
+ | **Lifecycle / system state** (`.ui-state`, `.ui-syncbar`) | `css/state.css` | A status report needs to show the state a thing is in — saving / queued / stale / conflict / reviewed — as a labelled object, not a bare coloured dot. See [state.md](./state.md). |
75
+
76
+ These compose with the report-native primitives already called out in
77
+ [Composition rules](#composition-rules): `ui-statgrid`, `ui-alert`, `ui-table`,
78
+ `ui-timeline`, `ui-meter`, and `ui-num`. None of them require behavior JS, so
79
+ they are all safe in the static, PDF-first report path.
80
+
39
81
  ## Canonical skeleton
40
82
 
41
83
  ```html
@@ -94,7 +136,7 @@ sanitize that content before rendering it and do not initialize
94
136
  below, which only shrinks the cover.
95
137
  - Use `ui-report__cover` for title, subtitle, author/date, and generation
96
138
  metadata. Add `ui-report__cover--compact` for short screen-first reports.
97
- Use `ui-report__header` for a compact in-page header instead of a full cover
139
+ Use `ui-report__head` for a compact in-page header instead of a full cover
98
140
  (same role, no tall hero block). Author `ui-report__meta` as a `<ul>` — the
99
141
  facts it lists are unordered.
100
142
  - Use `ui-report__section` and `ui-report__section-head` for report chapters.
@@ -125,23 +167,193 @@ sanitize that content before rendering it and do not initialize
125
167
  inside `ui-prose`; use `.ui-table` for curated evidence tables. If a
126
168
  `ui-report__evidence` block contains only a `ui-table-wrap`, the report layer
127
169
  removes the inner frame so evidence tables do not look double-boxed.
128
- - Every `<figure>` should include a `figcaption` using
129
- `ui-chart__caption` (chart figures) or `ui-report__caption` (any other
130
- report figure); the two are interchangeable in style.
170
+ - Every `<figure>` should include a `figcaption` using `ui-report__caption`.
131
171
  - Do not use raw color values. Theme with `--accent`; use status tones for
132
172
  status; use chart tokens only in chart figures.
133
173
 
174
+ ## Numbers and dates
175
+
176
+ The framework **aligns** figures; it does not **format** them. `.is-num` (table
177
+ cells) and `ui-num` (standalone) give tabular figures and end-alignment so a
178
+ column lines up — but a raw `1240` or `2026-6-1` still reads as machine output.
179
+ Format the values yourself, before they reach the markup:
180
+
181
+ - **Numbers** — group thousands and fix the precision. In JS,
182
+ `new Intl.NumberFormat('en-US').format(1240)` → `1,240`;
183
+ `{ style: 'currency', currency: 'USD' }` → `$1,240.00`;
184
+ `{ style: 'percent', maximumFractionDigits: 1 }` for rates;
185
+ `{ notation: 'compact' }` → `1.2M` for headline KPIs. Pick one locale and
186
+ precision per report and apply it consistently.
187
+ - **Dates** — render a human label but keep the machine value in a
188
+ `<time datetime="…">` (ISO-8601), as the report skeleton and `ui-timeline`
189
+ already do. `new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })`
190
+ → `Jun 1, 2026`.
191
+ - **Signs and units** — write the sign and unit into the text (`+1`, `−3.2%`,
192
+ `18 h`); never rely on a tone or arrow alone to say "down" (WCAG 1.4.1).
193
+ - Keep a column's precision uniform so the tabular alignment actually reads.
194
+
195
+ A non-JS host formats with its own locale library (Python `babel`, Go
196
+ `golang.org/x/text/message`, etc.) and emits the finished strings — bronto only
197
+ styles them.
198
+
199
+ ## Trend deltas
200
+
201
+ For a standalone change indicator (a KPI's movement, a row's quarter-over-quarter
202
+ shift), use `ui-delta`. A direction modifier sets both the arrow glyph — the
203
+ non-colour channel — and the conventional tone:
204
+
205
+ ```html
206
+ <span class="ui-delta ui-delta--up">+12.4%</span>
207
+ <span class="ui-delta ui-delta--down">−3</span>
208
+ <span class="ui-delta ui-delta--flat">0</span>
209
+ ```
210
+
211
+ Direction tone follows the common case (up = good, green; down = bad, red). When
212
+ **up is the bad direction** — latency, error rate, cost, churn — add
213
+ `ui-delta--invert` to swap only the tone; the arrow still reports real direction:
214
+
215
+ ```html
216
+ <span class="ui-delta ui-delta--up ui-delta--invert">+48 ms p95</span>
217
+ ```
218
+
219
+ The arrow is visual; always include the number and unit in the text.
220
+
221
+ A stat card's own `ui-stat__delta is-pos` / `is-neg` carries **tone only** (good
222
+ vs bad, as colour). That is fine on screen, but tone is a single channel: in a
223
+ greyscale PDF or for a colour-blind reader a positive and a negative delta look
224
+ identical, and the tone is deliberately decoupled from direction (a *dropped*
225
+ latency is `is-pos`). When the change must read without colour — most printed
226
+ reports — prefer `ui-delta` for the card's delta line, because its `--up`/`--down`
227
+ arrow is a real non-colour channel:
228
+
229
+ ```html
230
+ <span class="ui-stat__label">p95 latency</span>
231
+ <span class="ui-stat__value">172 ms</span>
232
+ <span class="ui-delta ui-delta--down ui-delta--invert">−48 ms</span>
233
+ ```
234
+
235
+ Here the arrow points **down** (latency fell) while `--invert` keeps the tone
236
+ green (down is good) — both channels stay honest. Reach for `ui-stat__delta` only
237
+ when a bare tonal accent is enough.
238
+
239
+ ## Comparison layout
240
+
241
+ For an "A vs B" or before/after section, use `ui-compare` — a fluid grid that
242
+ wraps to a single stack on a narrow screen, so two panels never overflow.
243
+ `ui-compare__col` is one side; label it with `ui-compare__head`. Add
244
+ `ui-compare--2up` to pin exactly two equal columns for a hard pairing.
245
+
246
+ ```html
247
+ <div class="ui-compare ui-compare--2up">
248
+ <div class="ui-compare__col">
249
+ <p class="ui-compare__head">Before</p>
250
+ <div class="ui-statgrid">
251
+ <div class="ui-stat">
252
+ <span class="ui-stat__label">p95 latency</span>
253
+ <span class="ui-stat__value">220 ms</span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ <div class="ui-compare__col">
258
+ <p class="ui-compare__head">After</p>
259
+ <div class="ui-statgrid">
260
+ <div class="ui-stat">
261
+ <span class="ui-stat__label">p95 latency</span>
262
+ <span class="ui-stat__value">172 ms</span>
263
+ <span class="ui-stat__delta is-pos">−48 ms</span>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ ```
269
+
134
270
  ## Chart figure recipe
135
271
 
136
- The report layer supplies chart containers and a small static bar pattern, not a
137
- chart engine. The data key is the standalone, portable `.ui-legend`
138
- (`@ponchia/ui/css/legend.css`) see [legends.md](./legends.md). For
139
- CSS/HTML/SVG charts, pair each chart color with a direct label, a pattern, or a
140
- fallback table.
272
+ bronto ships **no chart component** a chart needs scales and data binding, the
273
+ two things the analytical layer [refuses to own](./architecture.md). It supplies
274
+ the figure frame, the data key, and the colour palette; the chart itself comes
275
+ from one of two routes:
276
+
277
+ - **Live, interactive, or many-series** — theme [Vega-Lite](./vega.md):
278
+ `brontoVegaConfig(theme)` (from `@ponchia/ui/vega`) returns an on-brand
279
+ Vega-Lite `config` you spread into a spec and hand to vega-embed. Vega renders
280
+ the SVG/canvas; bronto only paints it. See [vega.md](./vega.md) for the full
281
+ recipe and the resolved `@ponchia/ui/vega.json` for non-JS hosts.
282
+ - **Frozen, print, or zero-JS** — hand-author a token-themed inline `<svg>`,
283
+ painting marks from the `--chart-N` palette so the figure prints exactly and
284
+ carries no runtime.
285
+
286
+ Whichever route, the figure frame is the same: wrap it in `ui-report__figure`,
287
+ caption it with `ui-report__caption`, give it the standalone, portable
288
+ `.ui-legend` data key (`@ponchia/ui/css/legend.css` — see
289
+ [legends.md](./legends.md)), and pair every colour with a direct label, a
290
+ pattern, **and** a fallback `ui-table` so the figure survives mono print and
291
+ colour-vision deficiency.
292
+
293
+ A Vega-Lite figure. The live mount is **`ui-screen-only`** and the fallback
294
+ `ui-table` carries the data into print — a live chart bakes the on-screen theme
295
+ into its SVG/canvas at render time, so printing it would emit a dark-baked chart
296
+ on white paper. Print the table; keep the chart for screen:
141
297
 
142
298
  ```html
143
- <figure class="ui-report__figure ui-chart ui-print-exact" role="group" aria-labelledby="chart-title">
144
- <figcaption id="chart-title" class="ui-chart__caption">
299
+ <figure class="ui-report__figure" role="group" aria-labelledby="chart-title">
300
+ <figcaption id="chart-title" class="ui-report__caption">
301
+ Fig 1 - Weekly focus split
302
+ </figcaption>
303
+ <div id="focus-chart" class="ui-screen-only" style="min-block-size: 240px">
304
+ <noscript>Chart needs JavaScript — the data is in the table below.</noscript>
305
+ </div>
306
+ <div class="ui-table-wrap">
307
+ <table class="ui-table ui-table--dense">
308
+ <caption>Chart source data</caption>
309
+ <thead>
310
+ <tr><th>Series</th><th class="is-num">Hours</th></tr>
311
+ </thead>
312
+ <tbody>
313
+ <tr><td>Research</td><td class="is-num">18</td></tr>
314
+ <tr><td>Delivery</td><td class="is-num">11</td></tr>
315
+ </tbody>
316
+ </table>
317
+ </div>
318
+ </figure>
319
+ <script type="module">
320
+ import vegaEmbed from 'vega-embed';
321
+ import { brontoVegaConfig } from '@ponchia/ui/vega';
322
+ const theme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
323
+ vegaEmbed('#focus-chart', {
324
+ data: { values: [
325
+ { series: 'Research', hours: 18 },
326
+ { series: 'Delivery', hours: 11 },
327
+ ] },
328
+ mark: 'bar',
329
+ encoding: {
330
+ x: { field: 'series', type: 'nominal' },
331
+ y: { field: 'hours', type: 'quantitative' },
332
+ },
333
+ }, { config: brontoVegaConfig(theme), renderer: 'svg', actions: false });
334
+ </script>
335
+ ```
336
+
337
+ Give the mount a `min-block-size` so the figure is not a collapsed blank band
338
+ before the chart paints (or while JS is blocked), and pass **`renderer: 'svg'`**
339
+ (vega-embed defaults to `canvas`, which doesn't theme-inspect and prints as a
340
+ raster).
341
+
342
+ > The bare-specifier `import`s above assume a **build step** (or an import map),
343
+ > and even then `import`/`fetch` of the config only works over an `http(s)`
344
+ > origin. A static report **opened from disk (`file://`) cannot import the module
345
+ > nor fetch `vega.json`** (CORS) — load Vega + Vega-Lite + vega-embed from pinned
346
+ > `/build/*.min.js` CDN tags and **inline the resolved `config` object**, the
347
+ > file://-safe recipe in [vega.md](./vega.md#from-a-cdn-no-bundler). For a report
348
+ > you intend to **print/PDF**, prefer the frozen inline `<svg>` below — it has no
349
+ > runtime, prints exactly, and sidesteps all of this.
350
+
351
+ A frozen, token-themed inline `<svg>` for the same data — no runtime, prints
352
+ exactly, with a `.ui-legend` key and the fallback table:
353
+
354
+ ```html
355
+ <figure class="ui-report__figure ui-print-exact" role="group" aria-labelledby="chart-title">
356
+ <figcaption id="chart-title" class="ui-report__caption">
145
357
  Fig 1 - Weekly focus split
146
358
  </figcaption>
147
359
  <ul class="ui-legend" aria-label="Series">
@@ -162,42 +374,43 @@ fallback table.
162
374
  <span class="ui-legend__label">Delivery</span>
163
375
  </li>
164
376
  </ul>
165
- <div class="ui-chart__plot" aria-hidden="true">
166
- <div
167
- class="ui-chart__bar"
168
- style="--chart-value: 72%; --chart-color: var(--chart-1); --chart-pattern: var(--chart-pattern-1)"
169
- >
170
- <div class="ui-chart__label"><span>Research</span><span>18 h</span></div>
171
- <div class="ui-chart__track"><div class="ui-chart__fill"></div></div>
172
- </div>
173
- <div
174
- class="ui-chart__bar"
175
- style="--chart-value: 44%; --chart-color: var(--chart-2); --chart-pattern: var(--chart-pattern-2)"
176
- >
177
- <div class="ui-chart__label"><span>Delivery</span><span>11 h</span></div>
178
- <div class="ui-chart__track"><div class="ui-chart__fill"></div></div>
179
- </div>
180
- </div>
181
- <div class="ui-chart__fallback">
182
- <div class="ui-table-wrap">
183
- <table class="ui-table ui-table--dense">
184
- <caption>Chart source data</caption>
185
- <thead>
186
- <tr><th>Series</th><th class="is-num">Hours</th></tr>
187
- </thead>
188
- <tbody>
189
- <tr><td>Research</td><td class="is-num">18</td></tr>
190
- <tr><td>Delivery</td><td class="is-num">11</td></tr>
191
- </tbody>
192
- </table>
193
- </div>
377
+ <svg viewBox="0 0 360 160" role="img" aria-labelledby="focus-svg-title">
378
+ <title id="focus-svg-title">Weekly focus split</title>
379
+ <line x1="36" y1="132" x2="324" y2="132" stroke="var(--line)" />
380
+ <rect x="72" y="42" width="96" height="90" fill="var(--chart-1)" />
381
+ <rect x="200" y="77" width="96" height="55" fill="var(--chart-2)" />
382
+ </svg>
383
+ <div class="ui-table-wrap">
384
+ <table class="ui-table ui-table--dense">
385
+ <caption>Chart source data</caption>
386
+ <thead>
387
+ <tr><th>Series</th><th class="is-num">Hours</th></tr>
388
+ </thead>
389
+ <tbody>
390
+ <tr><td>Research</td><td class="is-num">18</td></tr>
391
+ <tr><td>Delivery</td><td class="is-num">11</td></tr>
392
+ </tbody>
393
+ </table>
194
394
  </div>
195
395
  </figure>
196
396
  ```
197
397
 
198
- For canvas or SVG libraries, import resolved series colors from
199
- `@ponchia/ui/charts.json` and keep the same legend/caption/fallback structure in
200
- the surrounding HTML.
398
+ For a frozen figure, drive the SVG fills from the `--chart-N` palette tokens
399
+ directly; for a Vega chart, the same colours arrive through
400
+ `brontoVegaConfig`'s `range.*` ramps, projected from `@ponchia/ui/charts.json`.
401
+
402
+ For a **sequential** figure (a heatmap, a choropleth, a magnitude ramp) fill the
403
+ cells from the single-hue ramp tokens `--chart-seq-1` … `--chart-seq-6`
404
+ (low → high); for a **diverging** figure (−…0…+) use `--chart-div-1` …
405
+ `--chart-div-7` (the middle band is the neutral midpoint). Both ramps live in
406
+ `css/dataviz.css`, and their resolved per-theme hexes are in
407
+ `@ponchia/ui/charts.json` (`sequential` / `diverging`) for a non-JS or `file://`
408
+ host that needs literal values. The same ramps back Vega's
409
+ `range.heatmap`/`ramp`/`diverging`, so a frozen sequential figure and a live Vega
410
+ heatmap read identically — but note the ramp **runs pale→deep in light theme and
411
+ deep→pale in dark** (it flips to stay legible against the background), so don't
412
+ print a colour key that assumes one direction, and don't bake a fixed ink colour
413
+ onto the cells. Pair the figure with a stepped legend and the fallback table.
201
414
 
202
415
  ## Annotation recipe
203
416
 
@@ -246,6 +459,95 @@ style itself.
246
459
  </ol>
247
460
  ```
248
461
 
462
+ ## Meters and quotes
463
+
464
+ Two more report-native primitives. A **meter** shows a measured value as a
465
+ proportion — set the percentage on the `--value` custom property of
466
+ `ui-meter__fill` (a number 0–100, see `classes.json` `customProperties`); a tone
467
+ modifier picks the fill colour, and the readable label is yours to write beside
468
+ it (never rely on the bar alone — WCAG 1.4.1):
469
+
470
+ ```html
471
+ <div class="ui-meter ui-meter--accent">
472
+ <span class="ui-meter__fill" style="--value: 72"></span>
473
+ </div>
474
+ <span class="ui-num">72% of quota</span>
475
+ ```
476
+
477
+ `--value` is a percentage and the fill **clamps at 100**, so an over-target
478
+ reading (e.g. 112 % of plan) shows a full bar — put the true figure in the
479
+ written label beside it (`ui-num`), which is the data of record anyway.
480
+
481
+ A **pull-quote** lifts a source sentence out of the prose — `ui-quote` is the
482
+ block, `ui-quote__cite` attributes it:
483
+
484
+ ```html
485
+ <blockquote class="ui-quote">
486
+ <p>The migration paid for itself within the first billing cycle.</p>
487
+ <cite class="ui-quote__cite">Q2 finance review</cite>
488
+ </blockquote>
489
+ ```
490
+
491
+ ## Theming a live report
492
+
493
+ The report layer is static, but a screen report often offers a light/dark
494
+ **toggle**. The chart palette is the one piece that does not re-skin from CSS
495
+ alone: Vega and Mermaid/D2 bake resolved colours into their SVG at render time
496
+ (they can't read `var()`), so on a theme flip you must **re-render** the foreign
497
+ figures. Set the theme on the root and re-embed:
498
+
499
+ ```html
500
+ <button type="button" class="ui-button ui-button--ghost ui-button--sm" id="theme-toggle">
501
+ Toggle theme
502
+ </button>
503
+ <script type="module">
504
+ import { brontoVegaConfig } from '@ponchia/ui/vega'; // http(s) origin only
505
+ const root = document.documentElement;
506
+ let view; // the previous Vega view, so we can tear it down before re-embedding
507
+ const renderChart = async () => {
508
+ const theme = root.dataset.theme === 'dark' ? 'dark' : 'light';
509
+ const host = document.querySelector('#focus-chart');
510
+ view?.finalize(); // 1. finalize the old view first — frees its listeners/RAF (see below)
511
+ host.replaceChildren(); // 2. clear the host — re-embedding into a non-empty node stacks SVGs
512
+ const res = await vegaEmbed(host, spec, {
513
+ config: brontoVegaConfig(theme),
514
+ renderer: 'svg',
515
+ actions: false,
516
+ });
517
+ view = res.view;
518
+ };
519
+ document.querySelector('#theme-toggle').addEventListener('click', () => {
520
+ root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
521
+ renderChart(); // 2. re-render on every flip — the baked SVG does not follow CSS
522
+ });
523
+ renderChart();
524
+ </script>
525
+ ```
526
+
527
+ Four foot-guns, each verified while dogfooding:
528
+
529
+ - **Finalize the previous view before re-embedding.** `vegaEmbed` resolves to
530
+ `{ view }`; a Vega view registers event listeners and an animation frame loop
531
+ that `replaceChildren()` does **not** unwind. Call `view.finalize()` on the
532
+ prior view before each re-render (as above), or a report that toggles theme
533
+ repeatedly leaks a view per toggle.
534
+ - **Clear the host before re-embedding.** vega-embed appends; embedding twice
535
+ into the same node stacks a second chart under the first. `replaceChildren()`
536
+ (or `host.innerHTML = ''`) first.
537
+ - **Avoid `width: 'container'` if the chart can be re-rendered while hidden.** A
538
+ container-sized Vega chart measures its parent at embed time; if that parent is
539
+ `display: none` (a collapsed section, an inactive tab) it measures `0` and
540
+ renders empty. Give the spec an explicit `width`, or re-embed when the section
541
+ becomes visible.
542
+ - **Mermaid: don't `innerHTML = svg` over the source.** `mermaid.render()`
543
+ returns the SVG string — write it to a *separate* mount, not back into the
544
+ `<pre class="mermaid">` that still holds the diagram source, or a re-theme has
545
+ nothing to re-render from. Keep the source and the rendered output in different
546
+ nodes.
547
+
548
+ For a report you will only ever **print/PDF**, skip all of this: render once in
549
+ the target theme, or use the frozen inline `<svg>` route, which has no runtime.
550
+
249
551
  ## Common templates
250
552
 
251
553
  - Executive brief: compact cover, one summary block, KPI `ui-statgrid`, short
@@ -259,8 +561,9 @@ style itself.
259
561
 
260
562
  ## Print and PDF
261
563
 
262
- The supported export target is modern Chromium print/PDF. Two ways to produce
263
- the file:
564
+ The supported export target is modern Chromium print/PDF. A bronto report is
565
+ static and zero-JS, so producing the PDF is just _load → print_ — you do not
566
+ need a full automatable browser, only a Chromium-class layout+print pass.
264
567
 
265
568
  - **By hand:** open the report in Chrome/Edge → Print (Cmd/Ctrl+P) → "Save as
266
569
  PDF". In **More settings**, enable **Background graphics** (the dialog's
@@ -268,13 +571,45 @@ the file:
268
571
  out), and pick the paper size there. Paper size is a browser print setting,
269
572
  not a token; the layer only themes the page _margin_ via
270
573
  `--report-page-margin`.
271
- - **Headless (agents/CI):** `await page.pdf({ format: 'A4', printBackground: true })`
272
- with Playwright or Puppeteer.
273
-
274
- The report prints ink-on-white regardless of the on-screen theme. Older
574
+ - **Headless, lightweight:** use **`chrome-headless-shell`** the minimal
575
+ headless-Chromium binary built for exactly this, a fraction of a full
576
+ browser's weight. Drive it through Playwright/Puppeteer (or raw CDP) and
577
+ always pass `printBackground: true`, or chart fills and legend swatches drop
578
+ out:
579
+
580
+ ```js
581
+ import { chromium } from 'playwright'; // or puppeteer
582
+ const browser = await chromium.launch({ channel: 'chromium-headless-shell' });
583
+ const page = await browser.newPage();
584
+ await page.goto('file:///abs/path/report.html', { waitUntil: 'networkidle' });
585
+ await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
586
+ await browser.close();
587
+ ```
588
+
589
+ Install the binary with `npx playwright install chromium-headless-shell`
590
+ (Puppeteer ships its own). The repo's `scripts/render-pdf.mjs` is a working
591
+ copy of this (`npm run report:pdf -- report.html`); it is a dev/example
592
+ helper, not part of the published API — bronto does not own rendering.
593
+ - **As a service / from another language:** run Chromium-as-a-service
594
+ (e.g. **Gotenberg**'s `POST /forms/chromium/convert/html`, or a hosted CDP
595
+ endpoint) and POST the HTML + the `dist/css/*` assets. A Python/Go/any host
596
+ then needs no local browser. This is the natural fit for reports generated
597
+ by an LLM or service in another system.
598
+
599
+ The report prints ink-on-white regardless of the on-screen theme. The chart
600
+ fills and swatches carry `print-color-adjust: exact`, but the engine still
601
+ needs background printing enabled (`printBackground: true` headless, or
602
+ "Background graphics" by hand). The bare `chrome --headless --print-to-pdf`
603
+ CLI flag does **not** print backgrounds — use the scripted CDP/`page.pdf()`
604
+ path above for any report with charts.
605
+
606
+ A browserless engine (WeasyPrint, Prince, …) can work for text-and-table
607
+ reports if you feed it the **unlayered** CSS (`@ponchia/ui/css/unlayered/*` —
608
+ no `@layer`) and resolve colours from `tokens/resolved.json`; but `:has()` and
609
+ modern paged-media are not universally supported, so charts and edge cases may
610
+ degrade. For faithful output, stay on a Chromium-class engine. Older
275
611
  HTML-to-PDF engines are not part of the browser floor and may not support
276
- cascade layers, `oklch()`, `color-mix()`, `:has()`, or modern paged-media
277
- behavior.
612
+ cascade layers, `oklch()`, `color-mix()`, `:has()`, or modern paged-media.
278
613
 
279
614
  - Use `ui-print-only` for content that should appear only in print.
280
615
  - Use `ui-screen-only` for navigation or helper content that should not print.
@@ -296,7 +631,10 @@ fake page numbers with inert markup.
296
631
 
297
632
  Before returning a report, an LLM should verify:
298
633
 
299
- - All `ui-*` classes exist in `@ponchia/ui/classes`.
634
+ - All `ui-*` classes exist in `@ponchia/ui/classes` (or `classes/classes.json`).
635
+ The `is-*` state hooks (`is-num`/`is-pos`/`is-neg`/`is-key` in `.ui-table`
636
+ cells and `.ui-stat` deltas, `is-open`/`is-active`) are valid but live
637
+ outside `cls` by design — keep them.
300
638
  - The document has one `h1`, ordered headings, and a single main report region.
301
639
  - Tables have captions and header cells.
302
640
  - Charts have captions, direct labels or legends, and fallback data.
package/docs/sources.md CHANGED
@@ -31,6 +31,29 @@ the only one.
31
31
  | Stale | `ui-src--stale` | warning |
32
32
  | Conflict | `ui-src--conflict` | danger |
33
33
 
34
+ A trust-state class **needs a host element** — on its own a tone class
35
+ (`ui-src--verified` and the rest) only sets a `--src-tone`, it does not draw
36
+ anything. The host is one of the elements below, or the standalone `.ui-src`
37
+ pill.
38
+
39
+ ## Standalone trust pill — `.ui-src`
40
+
41
+ When you just need a bare labelled chip (a row of provenance tags, an inline
42
+ "verified" badge) with no surrounding card or citation, use `.ui-src` as the
43
+ host and add a tone. It draws a small pill with a leading trust dot:
44
+
45
+ ```html
46
+ <span class="ui-src ui-src--verified">Verified</span>
47
+ <span class="ui-src ui-src--generated">AI-generated</span>
48
+ <span class="ui-src ui-src--stale">Stale · 14d</span>
49
+ ```
50
+
51
+ The base `.ui-src` is required — `<span class="ui-src--verified">` alone
52
+ **validates against `classes.json` but renders nothing**, because the tone class
53
+ only carries the colour for a host to consume. The word inside is the channel
54
+ (WCAG 1.4.1); the dot and tint are reinforcement. Reach for `.ui-citation--chip`
55
+ instead when the pill is a real link/button to the source.
56
+
34
57
  ## Inline citation — `.ui-citation`
35
58
 
36
59
  A reference marker on a real `<a>` or `<button>` (the visual index is never the
@@ -64,6 +87,10 @@ A single source preview: title, origin, time, excerpt, actions. The
64
87
  </article>
65
88
  ```
66
89
 
90
+ The parts are `__title`, `__origin`, `__time`, `__excerpt` (the body text — not
91
+ `__detail`/`__body`), and `__actions`. A card nests inside a
92
+ `ui-source-list__item` when it sits in a references section (below).
93
+
67
94
  ## Source list — `.ui-source-list`
68
95
 
69
96
  A references section: a reset list of source cards (or rows).
package/docs/stability.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # Public API Stability
2
2
 
3
3
  `@ponchia/ui` is pre-1.0. Breaking changes ship in the minor (`0.x.0`), and
4
- patches are non-breaking. This matrix defines what counts as public API.
4
+ patches are non-breaking. In practical terms: **PATCH releases (`0.5.x`) are
5
+ non-breaking bug-fixes and additive changes — safe to upgrade without review;
6
+ MINOR releases (`0.x.0`) may include breaking changes and consumers should
7
+ review the CHANGELOG before upgrading.** Pin `~0.x` (tilde) to accept only
8
+ patches, or `^0.x` only if you accept minor-level churn. This policy holds
9
+ until `1.0.0` is tagged. This matrix defines what counts as public API.
5
10
 
6
11
  | Surface | Stability | Contract |
7
12
  | --- | --- | --- |
@@ -9,8 +14,10 @@ patches are non-breaking. This matrix defines what counts as public API.
9
14
  | JS module format | Stable | JS subpaths are ESM-only. CommonJS consumers use dynamic `import()`. |
10
15
  | CSS class names (`.ui-*`) | Stable | Names and documented modifier semantics are public. Internal selector structure and leaf-file boundaries may change. |
11
16
  | Class recipes (`@ponchia/ui/classes`) | Stable | Exported `cls`, `ui`, `cx`, recipe names, and option unions are public. |
17
+ | Class vocabulary as data (`@ponchia/ui/classes.json`) | Stable additive | The JSON shape (`groups`/`classes`/`states`/`customProperties`) and its entries are public — for validating markup from a non-JS/non-TS host. Generated from `cls` (the `classes` list cannot drift from the CSS); `states`/`customProperties` are gated against the stylesheet. New classes/hooks are additive. |
12
18
  | Design tokens | Stable names/roles | Token names and documented roles are public. Exact values and generated colour math outputs may change for visual tuning before 1.0. |
13
19
  | `--accent-1..6` | Stable names/roles | A subtle-to-bold accent ramp derived from `--accent`. Exact resolved values are visual tuning; algorithm changes require release-note visibility and resolver/browser checks. |
20
+ | Tokens as data (`tokens.json`, `tokens.dtcg.json`, `tokens/resolved.json`) | Stable additive | The JSON shapes are public for non-CSS/non-JS consumers. `resolved.json` exposes `light`/`dark` (resolved colours) and `scale` (resolved non-colour scales). Token names/roles are stable; exact resolved values are visual tuning (pin `~0.x`). |
14
21
  | Theme axes | Mixed | `data-theme` (light/dark) is the **contractual** base. `data-surface="oled"`, `data-density`, and `data-contrast` are **convenience presets** — best-effort visual variants, **not** part of the stability contract; their presence and exact values may change for tuning. (A computed-style smoke test guards the OLED `--bg` flip; the others are unverified.) |
15
22
  | Behavior attributes (`data-bronto-*`) | Stable | Attribute names and documented markup relationships are public. Behavior internals are not. |
16
23
  | Behavior functions (`@ponchia/ui/behaviors`) | Stable | Exported function names, option names, custom events, SSR no-op behavior, idempotency, and cleanup-returning contract are public. |
@@ -19,7 +26,7 @@ patches are non-breaking. This matrix defines what counts as public API.
19
26
  | React/Solid/Qwik bindings | Stable thin adapters | Hook/primitive names, optional peer behavior, root ref/signal/resolver support, and cleanup lifecycle are public. They remain wrappers over vanilla behaviors, not component APIs. |
20
27
  | Skins (`@ponchia/ui/skins`, `css/skins.css`) | Stable additive | Existing skin names stay valid. New skins are additive. Skins are root-level choices. |
21
28
  | Charts (`@ponchia/ui/charts`, `charts.json`, `css/dataviz.css`) | Stable additive | Token names, JSON shape, and 8 categorical slots are public. Exact palette values may tune if gates and release notes justify it. |
22
- | Reports (`css/report.css`, `.ui-report*`, `.ui-chart*`, print utilities) | Stable additive | Report class names, BEM part names, chart helper class names, and print utility names are public. Report CSS is opt-in and not imported by the default bundle. The data key now lives in the standalone Legends layer (below), not `css/report.css`. |
29
+ | Reports (`css/report.css`, `.ui-report*`, print utilities) | Stable additive | Report class names, BEM part names, and print utility names are public. Report CSS is opt-in and not imported by the default bundle. The data key now lives in the standalone Legends layer (below), not `css/report.css`; charting is via the Vega theme target (`@ponchia/ui/vega`, see [vega](./vega.md)) or a token-themed inline SVG, not a shipped renderer. |
23
30
  | Annotations (`@ponchia/ui/annotations`, `css/annotations.css`, `.ui-annotation*`) | Stable additive | SVG annotation class names, recipe option names, and helper function names are public. Helper internals and exact path-control heuristics may tune before 1.0. |
24
31
  | Legends (`css/legend.css`, `.ui-legend*`, `@ponchia/ui/behaviors` `initLegend`) | Stable additive | Legend class names, recipe option names, and the `bronto:legend:toggle` event contract (`aria-pressed="true"` ⇒ shown) are public. Opt-in, not in the default bundle; swatch colours are gated to the `--chart-*` palette. |
25
32
  | Marks (`css/marks.css`, `.ui-mark*`, `.ui-bracket-note*`) | Stable additive | Text-mark and bracket-note class names and recipe option names are public. Opt-in, not in the default bundle. Uses semantic tones only. |