@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/usage.md CHANGED
@@ -39,6 +39,14 @@ All three are small. They are **not** interchangeable:
39
39
  Rule of thumb: state → dot, classification → badge, user-controlled value
40
40
  → chip.
41
41
 
42
+ **Tone vocabulary varies by family — by design.** Colour is rationed, so not
43
+ every component carries every tone: `--info`/`--muted` exist on some families
44
+ (badge, dot, state) and not others (alert/toast/meter lead with
45
+ `--success`/`--warning`/`--danger`). The authoritative per-component tone list is
46
+ each base's `modifiers` array in
47
+ [`@ponchia/ui/classes.json`](../classes/classes.json) — read it rather than
48
+ extrapolating a tone you saw on one component onto another.
49
+
42
50
  ## Numbers: `ui-num` vs the table state classes
43
51
 
44
52
  - Inside `.ui-table`, a numeric cell is `.is-num` (+ `.is-pos` /
@@ -70,10 +78,11 @@ for narrative body content you do not fully control.
70
78
 
71
79
  Do not turn every report block into a card. Use `ui-report__summary`,
72
80
  `ui-report__finding`, and `ui-report__evidence` for document structure; use
73
- `ui-card` only when the block is genuinely a repeated card item. Charts use
74
- the report `ui-chart*` wrappers plus the opt-in data-viz tokens; simple static
75
- bar charts can use `ui-chart__plot`, `ui-chart__bar`, and `ui-chart__fill`.
76
- Always include a caption and fallback data. Full LLM/static report cookbook:
81
+ `ui-card` only when the block is genuinely a repeated card item. bronto ships
82
+ **no chart component**: for a chart, theme Vega-Lite (`@ponchia/ui/vega`, see
83
+ [vega.md](vega.md)) or hand-author a token-themed inline SVG painted from the
84
+ data-viz palette tokens. Always wrap it in a `ui-report__figure` with a caption,
85
+ a `.ui-legend` key, and fallback data. Full LLM/static report cookbook:
77
86
  [reporting.md](reporting.md).
78
87
 
79
88
  ## Buttons: variant and size
@@ -102,6 +111,13 @@ destructive action).
102
111
  | toast | transient, out-of-flow, system-initiated. Danger toasts route to an assertive live region; everything else polite. |
103
112
  | tooltip | supplemental, hover/focus, never essential info (it's not announced reliably; don't hide required content in it). |
104
113
 
114
+ The CSS `ui-tooltip` is hover/focus-only and CSS can't wire it to assistive
115
+ tech for you — associate the bubble with its trigger yourself, or it conveys
116
+ nothing to a screen reader: give `.ui-tooltip__bubble` an `id`, point the
117
+ trigger's `aria-describedby` at it, and keep the bubble `role="tooltip"`. For a
118
+ tooltip that must stay visible near a viewport edge or inside a scroll
119
+ container, use `initPopover` (a real focus-managed panel) instead.
120
+
105
121
  ## Meter vs progress
106
122
 
107
123
  Both are a thin horizontal bar; they mean different things.
@@ -135,6 +151,37 @@ this much* → meter.
135
151
  (`aria-hidden`) and the input keeps its full width. Don't hand-roll an
136
152
  absolute overlay.
137
153
 
154
+ ## Navigation: the landmarks and names the classes don't carry
155
+
156
+ The navigation classes are styling only — the ARIA scaffolding is yours, and
157
+ without it these widgets are unlabelled or unannounced:
158
+
159
+ - **`ui-breadcrumb`** — wrap it in `<nav aria-label="Breadcrumb">` and mark the
160
+ last (current) crumb with `aria-current="page"`.
161
+ - **`ui-pagination`** — wrap it in `<nav aria-label="Pagination">`; give the
162
+ current page `aria-current="page"`; label icon-only prev/next controls
163
+ (`aria-label="Previous page"`). Disable a control with native `disabled`
164
+ (a `<button>`) **or** `aria-disabled="true"` — both now render disabled and
165
+ are non-interactive; don't ship an `aria-disabled` control that still acts.
166
+ - **`ui-tabs`** — `initTabs` adds the full APG wiring (roles, roving tabindex,
167
+ `aria-selected`, panel `hidden`, focusable panel). If you wire tabs yourself,
168
+ name the `ui-tabs__list` (`role="tablist"` + an `aria-label`) and pair each
169
+ tab with its panel via `aria-controls`/`aria-labelledby`.
170
+ - **`ui-sitenav` / `ui-app-nav`** — signal the current link with
171
+ `aria-current="page"` (both honour it; `ui-app-nav` also accepts the
172
+ visual-only `.is-active`, but prefer `aria-current`).
173
+ - **`ui-skiplink`** — keep it the first focusable element and point its `href`
174
+ at the `id` of your main landmark.
175
+
176
+ ## Avatar: it's an unlabelled blob until you name it
177
+
178
+ `ui-avatar` is a presentation box. Give it an accessible name yourself: an
179
+ image avatar needs real `alt` text (`alt=""` only if it's purely decorative
180
+ beside a visible name); an initials avatar needs an accessible name on the
181
+ element (e.g. `aria-label="Ada Lovelace"`) because the initials alone don't
182
+ convey identity to AT. Keep initials to ~2 characters — the box is
183
+ `overflow: hidden` and silently clips a third.
184
+
138
185
  ## Modal: native `<dialog>` vs `is-open`
139
186
 
140
187
  Prefer the **native `<dialog>`** path — you get top-layer, backdrop and
@@ -181,6 +228,13 @@ that fuses the cells into a square, gapless pixel glyph that stays crisp and
181
228
  legible down to **~16px** — so the same set doubles as real inline UI icons,
182
229
  not just decoration. (Below the dot fragments into dot-soup; solid does not.)
183
230
 
231
+ One caveat on ink: `solid` cells inherit the dot palette (`--field-dot-hot`,
232
+ ~40% alpha), so at small sizes a solid glyph reads as a soft grey, not full
233
+ ink. When you want a crisp, full-strength small icon (toolbar, button affordance),
234
+ use the one-node mask renderer instead — `renderGlyph(name, { render: 'mask' })`
235
+ paints the glyph in `currentColor` on a `.ui-icon`, so it tracks text colour at
236
+ any size.
237
+
184
238
  `renderGlyph(name, { label })` returns an SSR-safe string: decorative
185
239
  (`aria-hidden`) by default, or `role="img"` + `aria-label` when you pass a
186
240
  `label` — which is how it conveys meaning to assistive tech. Prefer the
@@ -285,6 +339,49 @@ authoring engine.
285
339
  - Annotation text must be visible or represented in the figure caption, SVG
286
340
  `<desc>`, or fallback table. Full detail in [annotations.md](annotations.md).
287
341
 
342
+ ## Forms: the contracts the markup alone won't tell you
343
+
344
+ - **Disabled — pick one mechanism.** Use the native `disabled` attribute for a
345
+ genuinely inert control (`ui-input`, `ui-select`, `ui-textarea`, `ui-switch`
346
+ /`ui-check`/`ui-segmented` wrapping a native input, `ui-range`, `ui-file`,
347
+ `ui-button`): the browser greys it, blocks activation, and skips it in tab
348
+ order, and bronto styles the disabled cue. Use `aria-disabled="true"` **only**
349
+ when the control must stay focusable/announced — bronto then adds
350
+ `pointer-events: none` to `ui-button`/`ui-link` so it can't be activated, but
351
+ you still own removing it from the submit logic.
352
+ - **Combobox** (`data-bronto-combobox`) reads its options from the DOM at
353
+ `initCombobox()` time — re-run it after you replace the option list. The
354
+ `<input>` owns the value; the listbox is a view.
355
+ - **Validation** is opt-in via `data-bronto-validate` on the form plus
356
+ `initFormValidation()`; it surfaces messages into a `ui-error-summary` you
357
+ provide. The summary's title is the legible sans, not the display face — it's
358
+ meant to be read.
359
+
360
+ ## Reveal: `ui-reveal` needs JS, `ui-scroll-reveal` doesn't
361
+
362
+ `ui-scroll-reveal` is scroll-driven and **zero-JS** — reach for it in a static
363
+ or LLM-authored report. `ui-reveal` is the JS variant: it starts hidden and you
364
+ toggle `is-visible` (e.g. from an `IntersectionObserver` you own) to play it in.
365
+ With scripting disabled it degrades to fully visible, but if scripting is *on*
366
+ and nothing toggles `is-visible`, the content stays hidden — so only use
367
+ `ui-reveal` when you are wiring that toggle.
368
+
369
+ ## Loading affordances need a role you supply
370
+
371
+ `ui-spinner`, `ui-dotspinner`, `ui-skeleton`, and an indeterminate `ui-progress`
372
+ are decorative animations — bronto can't know their semantics. Give the busy
373
+ region `aria-busy="true"` (or `role="status"` with an `aria-live` text label like
374
+ "Loading…"), and mark a purely decorative spinner `aria-hidden="true"`. Without
375
+ one of these a screen reader announces nothing while the user waits.
376
+
377
+ ## Popover: prefer the native top layer
378
+
379
+ `initPopover()` shows a `.ui-popover` in the browser **top layer** when the panel
380
+ carries the native `popover` attribute (never clipped by `overflow`/stacking);
381
+ without it, it falls back to an `is-open` class that a clipping ancestor can cut
382
+ off. Add `popover` to the panel for the robust path — the `is-open` form is a
383
+ fallback, not the default to copy.
384
+
288
385
  ## When to add a behavior
289
386
 
290
387
  The CSS is the framework; `@ponchia/ui/behaviors` is the *sanctioned*
package/docs/vega.md ADDED
@@ -0,0 +1,225 @@
1
+ # Vega-Lite
2
+
3
+ [Vega-Lite](https://vega.github.io/vega-lite/) is a **declarative JSON grammar
4
+ of graphics** — you describe a chart as data and it compiles (through
5
+ [Vega](https://vega.github.io/vega/)) to **SVG or canvas**. Like the
6
+ [Mermaid](./mermaid.md) and [D2](./d2.md) integrations, `@ponchia/ui` doesn't
7
+ render charts — it **themes** them from your tokens. Two things ship:
8
+
9
+ - `@ponchia/ui/vega` — `brontoVegaConfig(theme)`, the on-brand Vega-Lite
10
+ [`config`](https://vega.github.io/vega-lite/docs/config.html) object.
11
+ - `@ponchia/ui/vega.json` — the resolved per-theme config, for any consumer.
12
+
13
+ This is the idiomatic Vega theme shape — a `config`, the same kind the
14
+ [`vega-themes`](https://github.com/vega/vega-themes) package ships. Vega stays
15
+ the consumer's renderer; this is config only, and **Vega is not a dependency**
16
+ of bronto (the dev-only render-probe aside).
17
+
18
+ > Why Vega-Lite and not a bronto chart component? A chart needs **scales**
19
+ > (data → pixels) and **data binding** — the two things the analytical layer
20
+ > [refuses to own](./architecture.md). A spec is also something an
21
+ > LLM-from-another-system can emit as data, the same way it emits Mermaid/D2.
22
+ > So bronto themes a real charting grammar instead of shipping a fragile one.
23
+
24
+ ## Theme a chart
25
+
26
+ `brontoVegaConfig(theme)` returns a `config` object. Spread it into a spec, or
27
+ hand it to [vega-embed](https://github.com/vega/vega-embed):
28
+
29
+ ```js
30
+ import vegaEmbed from 'vega-embed';
31
+ import { brontoVegaConfig } from '@ponchia/ui/vega';
32
+
33
+ const theme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
34
+
35
+ vegaEmbed('#chart', {
36
+ data: { values: [
37
+ { quarter: 'Q1', value: 42 },
38
+ { quarter: 'Q2', value: 58 },
39
+ { quarter: 'Q3', value: 50 },
40
+ ] },
41
+ mark: 'bar',
42
+ encoding: {
43
+ x: { field: 'quarter', type: 'nominal' },
44
+ y: { field: 'value', type: 'quantitative' },
45
+ },
46
+ }, { config: brontoVegaConfig(theme), renderer: 'svg', actions: false });
47
+ ```
48
+
49
+ Pass **`renderer: 'svg'`** (not vega-embed's `canvas` default): an SVG chart is
50
+ inspectable, themeable, survives the print/PDF pipeline, and is what the
51
+ [annotation layer](#annotate-a-chart) composes onto — a canvas chart prints as a
52
+ raster and carries no text alternative.
53
+
54
+ ### From a CDN, no bundler
55
+
56
+ Load Vega + Vega-Lite + vega-embed from **pinned `/build/*.min.js` UMD files**,
57
+ then pass the config. Pin exact versions and use the `/build/` path — a bare
58
+ `cdn.jsdelivr.net/npm/vega@6` redirect resolves to a module bundle that does
59
+ **not** register the global `window.vega`, so vega-embed throws and nothing
60
+ renders. Keep the three majors aligned: **Vega-Lite 6 targets Vega 6** (and
61
+ vega-embed 7), so don't mix a Vega-Lite 6 with a Vega 5 runtime:
62
+
63
+ ```html
64
+ <script src="https://cdn.jsdelivr.net/npm/vega@6.2.0/build/vega.min.js"></script>
65
+ <script src="https://cdn.jsdelivr.net/npm/vega-lite@6.4.3/build/vega-lite.min.js"></script>
66
+ <script src="https://cdn.jsdelivr.net/npm/vega-embed@7.1.0/build/vega-embed.min.js"></script>
67
+ <script>
68
+ // INLINE the config (copy the object for your theme from @ponchia/ui/vega.json).
69
+ // This is the only path that also works from a file:// report — see below.
70
+ const brontoLight = {
71
+ /* …paste tokens/vega.json → light here… */
72
+ };
73
+ vegaEmbed('#chart', spec, { config: brontoLight, renderer: 'svg', actions: false });
74
+ </script>
75
+ ```
76
+
77
+ > **file:// portability.** A report opened straight from disk (`file://`) cannot
78
+ > `import` the `@ponchia/ui/vega` module **nor** `fetch('…/vega.json')` — the
79
+ > browser blocks both across the `null`/file origin (CORS). So for a
80
+ > double-clickable or PDF-bound report, **inline the resolved config object**
81
+ > (as above) rather than fetching it. Over an `http(s)` origin (a dev server, a
82
+ > static host, a bundler), the `import { brontoVegaConfig }` form and a
83
+ > `fetch('https://cdn.jsdelivr.net/npm/@ponchia/ui@VERSION/tokens/vega.json')`
84
+ > both work — pin the package version in the URL, since the unversioned latest
85
+ > may predate this target.
86
+
87
+ For a build step or non-JS host, read `@ponchia/ui/vega.json` directly
88
+ (`{ light, dark }`, each a ready Vega-Lite `config`).
89
+
90
+ ### Why resolved colours, not `var(--x)`
91
+
92
+ Vega-Lite compiles a spec to a Vega scene that renders to **SVG or canvas** —
93
+ colours are **baked into the output** and parsed by `d3-color`, which understands
94
+ real hex/rgb but **not** `var()` (nor `oklch()`). So the config ships **resolved
95
+ hex per theme**, projected from the same token source as
96
+ [`tokens/resolved.json`](./architecture.md) / [`charts.json`](./theming.md).
97
+ Re-call `brontoVegaConfig()` when the theme toggles and re-embed.
98
+
99
+ ### What the slots paint
100
+
101
+ The config keeps a chart **monochrome by default** — the rationed accent is the
102
+ one chromatic default (series 1 / the lone mark), never the chrome:
103
+
104
+ | Slot | Paint | bronto token |
105
+ | --- | --- | --- |
106
+ | `background` | Chart canvas | `--bg` |
107
+ | `view.stroke` | Plot frame | `--line` |
108
+ | `mark.color` | Default / single-series mark | `--accent` |
109
+ | `rule.color` | Reference rules, annotations | `--line-strong` |
110
+ | `axis.domainColor` · `tickColor` | Axis line · ticks | `--line-strong` |
111
+ | `axis.gridColor` | Gridlines | `--line` |
112
+ | `axis.labelColor` · `titleColor` | Tick labels · axis title | `--text-soft` · `--text` |
113
+ | `text.color` | Free `text`/`label` marks | `--text` |
114
+ | `legend.*` · `header.*` · `title.*` | Legend, facet headers, title | `--text-soft` / `--text` / `--text-dim` |
115
+ | `*.font` / `*Font` | All text | `--sans` |
116
+ | `range.category` | 8-series categorical palette | `charts.json` categorical (series 1 = accent) |
117
+ | `range.ordinal` · `ramp` · `heatmap` | Single-hue sequential ramp | `charts.json` sequential |
118
+ | `range.diverging` | − … neutral … + ramp | `charts.json` diverging |
119
+
120
+ The palette is the same CVD-safe, pattern-paired set documented in
121
+ [theming](./theming.md#data-viz) — colour is never the sole channel. When a
122
+ series needs the redundant second channel, drive the mark's fill from the
123
+ `--chart-pattern-*` tokens or pair a [legend](./legends.md) swatch.
124
+
125
+ ### Spending the accent
126
+
127
+ Series 1 of `range.category` **is** the live accent, so a single-series chart and
128
+ the first category re-skin for free with `--accent`. To emphasise one mark in a
129
+ multi-series chart, paint just that mark with the accent and leave the rest
130
+ neutral — the same "reserve the accent for the one thing a reader must not miss"
131
+ rule the rest of the system follows. Two small helpers hand you the exact
132
+ per-theme hexes so you never hard-code a palette array index:
133
+
134
+ ```js
135
+ import { brontoVegaAccent, brontoVegaNeutral } from '@ponchia/ui/vega';
136
+
137
+ // e.g. a bar chart where only the 'Alert' category is loud:
138
+ const spec = {
139
+ /* …data… */
140
+ mark: 'bar',
141
+ encoding: {
142
+ x: { field: 'name', type: 'nominal' },
143
+ y: { field: 'value', type: 'quantitative' },
144
+ color: {
145
+ condition: { test: "datum.name === 'Alert'", value: brontoVegaAccent(theme) },
146
+ value: brontoVegaNeutral(theme),
147
+ },
148
+ legend: null,
149
+ },
150
+ };
151
+ ```
152
+
153
+ `brontoVegaAccent(theme)` is `range.category[0]` (the live accent) and
154
+ `brontoVegaNeutral(theme)` is the last category (the quiet neutral); re-read both
155
+ when the theme toggles. Prefer them over digging the hex out of
156
+ `tokens/resolved.json` — they are guaranteed to match the palette the config
157
+ already ships. In token terms the accent is `--chart-1` and the neutral is
158
+ `--chart-8`, so a [legend](./legends.md#swatch-colour) for an accent-rationed
159
+ chart keys those two series with `ui-legend__swatch--1` and
160
+ `ui-legend__swatch--8` — the swatches mirror the marks exactly.
161
+
162
+ ### Selecting the themed ramp in a spec
163
+
164
+ The config registers the ramps as **named ranges**, so a quantitative encoding
165
+ opts in with `scale: { range: 'heatmap' }` (or `'ramp'` / `'diverging'`) — the
166
+ range **name**, not a colour scheme:
167
+
168
+ ```js
169
+ {
170
+ mark: 'rect',
171
+ encoding: {
172
+ x: { field: 'x', type: 'nominal' },
173
+ y: { field: 'y', type: 'nominal' },
174
+ color: { field: 'v', type: 'quantitative', scale: { range: 'heatmap' } },
175
+ },
176
+ }
177
+ ```
178
+
179
+ > Use `scale: { range: 'heatmap' }`, **not** `scale: { scheme: 'heatmap' }`.
180
+ > `scheme:` looks up a registered Vega/d3 scheme by name and **throws** for
181
+ > `'heatmap'` (no such scheme) — the ramp is a custom `range` the bronto config
182
+ > defines, addressed by range name. A `quantitative` colour encoding already
183
+ > defaults to `range.heatmap`; name it explicitly only when a chart has several
184
+ > quantitative scales and you want a specific one (`'diverging'` for a signed
185
+ > domain around a neutral centre).
186
+
187
+ ### Sequential & diverging ramps invert by theme
188
+
189
+ `range.heatmap` / `ramp` / `ordinal` is a single-hue ramp that runs **pale → deep
190
+ as the value rises in light theme, and deep → pale in dark theme** (the bg flips,
191
+ so the ramp flips to stay legible against it). Two consequences:
192
+
193
+ - **Don't hard-code ink on a heatmap cell.** A fixed black (or white) label is
194
+ readable at one end of the ramp and invisible at the other — and the readable
195
+ end swaps between themes. Either omit per-cell labels and rely on the fallback
196
+ `ui-table`, or compute the label colour from the cell's luminance at render
197
+ time. bronto themes the ramp; it can't know your data domain, so it does not
198
+ ship a cell-ink helper.
199
+ - **A CSS gradient key won't pixel-match the Vega ramp.** A native
200
+ [`ui-legend--gradient`](./legends.md) track is interpolated in OKLCH; Vega
201
+ interpolates its `range.*` ramp in d3's RGB space. They share endpoints but
202
+ drift in the mid-tones, so a continuous gradient key placed beside a Vega
203
+ heatmap will not match its mid cells exactly. Use a **stepped** legend (one
204
+ swatch per band, each from the same `charts.json` ramp stop) when the key sits
205
+ next to the chart.
206
+
207
+ ## Annotate a chart
208
+
209
+ Vega renders to SVG, so the [annotation layer](./annotations.md) composes onto it
210
+ exactly as in the [Mermaid recipe](./mermaid.md#annotate-a-diagram): render to a
211
+ frozen SVG (vega-embed's `view.toSVG()`, or the Vega CLI), read the target mark's
212
+ box, and paste a `<g class="ui-annotation">` computed with
213
+ `@ponchia/ui/annotations`. The same caveat applies — Vega's internal SVG (element
214
+ ids, the `role`/`aria` structure, the scene transform) is **not a public
215
+ contract**, so pin your Vega version and key off the data, not generated ids.
216
+
217
+ ## Scope
218
+
219
+ bronto owns the theme config — gated structurally by `check:vega` (every colour
220
+ slot resolves, both themes, no `var()` leaks, every `range.*` ramp populated),
221
+ and separately a dev-only render-probe (`npm test`, via the `vega`/`vega-lite`
222
+ dev deps) asserts the colours actually land on a rendered chart — and the
223
+ annotation geometry. It does not own Vega's grammar, its rendering, or its internal SVG —
224
+ those stay Vega's, and the chart is a documented composition, not a shipped
225
+ runtime binding.
package/docs/workbench.md CHANGED
@@ -22,7 +22,7 @@ a `__body` of property rows.
22
22
 
23
23
  ```html
24
24
  <aside class="ui-inspector">
25
- <div class="ui-inspector__header">
25
+ <div class="ui-inspector__head">
26
26
  <h2 class="ui-eyebrow">Rectangle</h2>
27
27
  <button class="ui-button ui-button--subtle ui-button--sm" type="button">Reset</button>
28
28
  </div>
@@ -50,6 +50,12 @@ A label/value pair, denser than `ui-key-value` and tuned for an inspector. The
50
50
 
51
51
  ## Selection bar — `.ui-selectionbar`
52
52
 
53
+ > **Name note:** `.ui-selectionbar` is the workbench bulk-action bar (this
54
+ > section). It is unrelated to the `.ui-sel--on` / `.ui-sel--off` /
55
+ > `.ui-sel--maybe` selection-emphasis state classes in
56
+ > [`css/selection.css`](./selection.md), which style host-managed selection
57
+ > state on individual items.
58
+
53
59
  A raised bar of actions on the current selection: a `__count` on one side,
54
60
  `__actions` on the other. The host owns what is selected and what the actions do.
55
61
 
package/glyphs/glyphs.js CHANGED
@@ -23,7 +23,7 @@ export const GLYPH_SIZE = 16;
23
23
 
24
24
  // Raw bitmaps. Each is GLYPH_SIZE rows of GLYPH_SIZE chars over [.#*]:
25
25
  // `.` off · `#` hot · `*` accent. Only `spark` uses accent dots — it is the
26
- // canonical two-tone demo; the gate in check-glyphs.mjs enforces the shape.
26
+ // canonical two-tone demo; test/glyphs.test.mjs asserts the spark-only-* rule.
27
27
  const RAW = {
28
28
  circle: [
29
29
  '................',
@@ -941,9 +941,9 @@ function esc(s) {
941
941
  .replace(/"/g, '&quot;');
942
942
  }
943
943
 
944
- // `dot`/`gap` land in an inline-CSS context (`style="--dotmatrix-dot:VALUE"`),
945
- // where HTML-escaping a `"` stops attribute breakout but a `;` would still open
946
- // a second CSS declaration (overlay/clickjacking, selector exfil). So restrict
944
+ // `dot`, `gap`, and `size` land in an inline-CSS context (`style=""`), where
945
+ // HTML-escaping a `"` stops attribute breakout but a `;` would still open a
946
+ // second CSS declaration (overlay/clickjacking, selector exfil). So restrict
947
947
  // them to length/calc syntax — digits, units, %, whitespace and `()+-*/.,` for
948
948
  // calc()/clamp()/var() — and drop anything else rather than emit it.
949
949
  function cssLen(v) {
@@ -1004,6 +1004,8 @@ function maskUrl(rows) {
1004
1004
  * bitmap (one DOM node, not GLYPH_SIZE²) — the icon-at-scale path: it sizes to
1005
1005
  * `size` (or `--icon-size` / `1em`) and inherits `currentColor`. The
1006
1006
  * cell-mode options (grid/solid/anim/dot/gap) don't apply; `label` does.
1007
+ * Mask mode is single-tone: accent `*` cells (used by `spark`) render
1008
+ * identically to hot `#` cells — both become opaque mask regions.
1007
1009
  * Needs `@ponchia/ui/css` (the `.ui-icon` rule).
1008
1010
  */
1009
1011
  export function renderGlyph(name, options = {}) {
package/llms.txt CHANGED
@@ -31,6 +31,23 @@ later layer) always win over framework styles regardless of selector
31
31
  specificity — this is the intended override mechanism; do not fight it
32
32
  with `!important`.
33
33
 
34
+ Loading a stylesheet — read before copying a `<link>` below. The opt-in
35
+ leaves are written as `@ponchia/ui/css/<leaf>.css` for brevity. That
36
+ package-specifier form resolves ONLY inside a CSS-aware bundler
37
+ (Vite/webpack); in a standalone `.html` file it does not resolve. For plain
38
+ HTML (the usual case when an LLM emits a report) use a real URL — and note
39
+ the path changes from source `css/` to built `dist/css/`:
40
+
41
+ ```html
42
+ <!-- installed locally -->
43
+ <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/css/<leaf>.css" />
44
+ <!-- or from a CDN; pin the version (pre-1.0, breaking changes ship in the minor) -->
45
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.0/dist/css/<leaf>.css" />
46
+ ```
47
+
48
+ The flattened default bundle is `dist/bronto.css` (bundler shorthand
49
+ `@ponchia/ui`). A complete, copy-pasteable CDN report is in `docs/reporting.md`.
50
+
34
51
  Typed class vocabulary (returns plain strings — works in any framework):
35
52
 
36
53
  ```js
@@ -69,6 +86,29 @@ import { renderGlyph, GLYPH_NAMES } from '@ponchia/ui/glyphs';
69
86
  el.innerHTML = renderGlyph('check', { label: 'Done' });
70
87
  ```
71
88
 
89
+ Layout primitives (core bundle, no media queries — intrinsic Every-Layout
90
+ composition that responds to content + available space): `ui-stack` (vertical
91
+ rhythm), `ui-cluster` (wrapping inline row), `ui-grid` (auto-fit columns),
92
+ `ui-sidebar` (sidebar + main that stacks when cramped), `ui-switcher` (row →
93
+ column together), `ui-center` (measure-bounded column), `ui-ratio` (aspect-ratio
94
+ box), and the `ui-app-shell`/`ui-app-rail`/`ui-app-nav` admin shell. Each is
95
+ tuned with an inline custom property rather than a modifier — set
96
+ `style="--grid-min: 12rem"`, `--sidebar-width`, `--switcher-min`, `--center-max`
97
+ (the INNER measure; gutters add to the total), `--ratio`, `--stack-gap`,
98
+ `--cluster-gap`, `--app-rail`, etc. The full list with defaults is in
99
+ `classes.json` `customProperties`. App-shell/sitenav current page is
100
+ `aria-current="page"`.
101
+
102
+ Motion utilities (core bundle, all reduced-motion-safe — they collapse to the
103
+ static end state, not just a zeroed duration): `ui-animate-in`,
104
+ `ui-animate-fade`, `ui-animate-dot`, `ui-animate-matrix` (enter animations),
105
+ `ui-stagger` (+ `--i` per child, or `ui-stagger--auto` to
106
+ derive the delay from `:nth-child`), `ui-reveal` (JS/IntersectionObserver — adds
107
+ `is-visible`), `ui-scroll-reveal` and `ui-scroll-progress` (zero-JS,
108
+ scroll-driven; the recommended reveal for static reports), and `ui-vt` (View
109
+ Transitions — needs the required `--ui-vt-name`). Prefer `ui-scroll-reveal` over
110
+ `ui-reveal` when no JS will run. Details: `docs/usage.md`.
111
+
72
112
  Optional display colorways — opt-in, never in the default bundle. Root-level,
73
113
  like `data-theme` (the derived accent family only recomputes at `:root`):
74
114
 
@@ -92,6 +132,54 @@ import charts from '@ponchia/ui/charts.json' with { type: 'json' };
92
132
  // charts.dark.categorical → ['#ff3b41', …] (series 0 = the resolved accent)
93
133
  ```
94
134
 
135
+ On-brand Mermaid diagrams use the same tokens-as-data path — Mermaid is the
136
+ consumer's renderer, bronto just hands it a resolved `base` theme (Mermaid can't
137
+ read `var(--x)`):
138
+
139
+ ```js
140
+ import { brontoMermaidTheme } from '@ponchia/ui/mermaid';
141
+ mermaid.initialize(brontoMermaidTheme('dark')); // { theme: 'base', themeVariables }
142
+ ```
143
+
144
+ Annotate a rendered diagram with the SVG annotation layer as a build-time
145
+ recipe. Details: `docs/mermaid.md`.
146
+
147
+ D2 (d2lang.com) is themed the same way — it compiles to a frozen SVG, so the
148
+ slots take resolved hex; the base ramp is monochrome and the accent is opt-in
149
+ per node:
150
+
151
+ ```js
152
+ import { brontoD2Vars } from '@ponchia/ui/d2';
153
+ const source = brontoD2Vars() + 'a -> b'; // prepend the vars block, then render
154
+ ```
155
+
156
+ Details: `docs/d2.md`.
157
+
158
+ Charting is the same path: bronto does NOT ship a chart component (a chart needs
159
+ scales + data binding, which the analytical layer refuses). For charts, recommend
160
+ Vega-Lite — a declarative JSON spec an external system can emit — themed by the
161
+ on-brand `config` (resolved hex; Vega bakes colours into SVG/canvas, can't read
162
+ `var()`). `range.category` is the `--chart-*` palette, series 1 = accent:
163
+
164
+ ```js
165
+ import { brontoVegaConfig, brontoVegaAccent, brontoVegaNeutral } from '@ponchia/ui/vega';
166
+ vegaEmbed('#chart', spec, { config: brontoVegaConfig('dark'), renderer: 'svg', actions: false });
167
+ ```
168
+
169
+ Pass `renderer: 'svg'` (canvas is the wrong default for print/inspection).
170
+ `brontoVegaAccent(theme)`/`brontoVegaNeutral(theme)` give the exact hexes to spend
171
+ the accent on one mark. From a CDN, pin the `/build/*.min.js` UMD bundles (a bare
172
+ `vega@6` tag has no `window.vega`; keep Vega-Lite 6 / Vega 6 / vega-embed 7
173
+ aligned) and INLINE the config from `vega.json` — a
174
+ `file://` report can't `import`/`fetch` it (CORS). A label on an accent fill in a
175
+ foreign renderer (a themed Vega/D2/Mermaid node or bar) reads the `--on-accent`
176
+ token, never `--accent-text` (that is accent-coloured text for a neutral bg).
177
+ `--on-accent` is a READ-ONLY export for those renderers — no in-DOM `.ui-*` rule
178
+ consumes it, so setting it does not re-ink a component; to colour text on an
179
+ accent-filled DOM control, set `--button-text`. Details: `docs/vega.md`. (Observable
180
+ Plot works too — it inherits the page CSS, so it needs even less theming;
181
+ Vega-Lite is the recommended LLM-emittable path.)
182
+
95
183
  `--chart-1..8` (categorical; series 1 = accent; colourblind-safe, gated under
96
184
  simulated protan/deutan/tritan), `--chart-seq-*` (sequential), `--chart-div-*`
97
185
  (diverging), and `--chart-pattern-1..8` (dot-matrix fills — pair colour N with
@@ -245,12 +333,15 @@ reports/AI output:
245
333
  ```
246
334
 
247
335
  `ui-citation` (inline ref on a real `<a>`/`<button>`; `--chip` = named-source
248
- pill), `ui-source-card` (+`__title`/`__origin`/`__time`/`__excerpt`/`__actions`),
249
- `ui-source-list` (+`__item`), and `ui-provenance` (+`__item`). A cross-cutting
250
- trust state sets the tone: `ui-src--verified`/`--reviewed`/`--generated`/
251
- `--unverified`/`--stale`/`--conflict` always paired with an author-written
252
- label (never colour alone). Bronto styles + states; the host owns fetching,
253
- numbering, and trust. `ui.citation({chip,state})` / `ui.source({state})` /
336
+ pill), `ui-source-card` (+`__title`/`__origin`/`__time`/`__excerpt`/`__actions`;
337
+ the body part is `__excerpt`, not `__detail`), `ui-source-list` (+`__item`),
338
+ `ui-provenance` (+`__item`), and `ui-src` (a standalone trust pill). A
339
+ cross-cutting trust state sets the tone: `ui-src--verified`/`--reviewed`/
340
+ `--generated`/`--unverified`/`--stale`/`--conflict` always paired with an
341
+ author-written label (never colour alone). A tone class **needs a host**:
342
+ `ui-src--verified` on its own renders nothing — put it on `ui-src`,
343
+ `ui-citation`, `ui-source-card`, or `ui-provenance__item`. Bronto styles +
344
+ states; the host owns fetching, numbering, and trust. `ui.citation({chip,state})` / `ui.source({state})` /
254
345
  `ui.provenance({state})`. Details: `docs/sources.md`.
255
346
 
256
347
  Optional lifecycle/system-state vocabulary — opt-in, CSS only, never in the
@@ -351,10 +442,18 @@ Canonical report skeleton:
351
442
  Report rules for agents: static output by default; do not initialize behaviors
352
443
  over untrusted generated content; sanitize arbitrary LLM/CMS/user HTML before
353
444
  rendering; use semantic headings; give tables captions and header cells; give
354
- charts captions, legends/direct labels, and fallback data; use
355
- `ui-chart__plot`/`ui-chart__bar`/`ui-chart__fill` for simple static bar charts;
356
- use `ui-report__section--unnumbered` for appendices in numbered reports; never
357
- use raw chromatic inline colors. Full cookbook: `docs/reporting.md`.
445
+ charts captions, legends/direct labels, and fallback data; for a chart, theme
446
+ Vega-Lite with `@ponchia/ui/vega` (or hand-author a token-themed inline `<svg>`)
447
+ inside a `ui-report__figure` bronto ships no chart component; use
448
+ `ui-delta` (`--up`/`--down`/`--flat`, `--invert` when up is bad) for a
449
+ trend figure and `ui-compare` (`--2up`) for an A/B / before-after section; use
450
+ `ui-report__section--unnumbered` for appendices in numbered reports; never
451
+ use raw chromatic inline colors. Format numbers/dates yourself (the framework
452
+ aligns figures, it does not format them) — see the reporting cookbook. To make
453
+ a PDF, render with `chrome-headless-shell` (or any Chromium) using
454
+ `printBackground: true` so figure fills survive — reports are static/zero-JS, so
455
+ it is just load → print; see "Print and PDF" in `docs/reporting.md`. Full
456
+ cookbook: `docs/reporting.md`.
358
457
 
359
458
  ## Authoritative offline references (shipped in this package)
360
459
 
@@ -363,6 +462,11 @@ Read these from `node_modules/@ponchia/ui/` — no network needed:
363
462
  - `classes/index.d.ts` — literal-typed `cls` map (exact class strings),
364
463
  every `ui.*()` recipe signature, every `*Opts` interface. The complete
365
464
  class surface; an agent should read this to know valid class names.
465
+ - `classes/classes.json` — the same vocabulary as language-neutral data, to
466
+ VALIDATE markup without executing ESM/parsing TS: `groups` (base →
467
+ modifiers/parts, the BEM structure), `classes` (flat list), `states` (the
468
+ `is-*` hooks that live outside `cls` by design, with their scope), and
469
+ `customProperties` (the author-set inline-style knobs like `--chart-color` / `--value`).
366
470
  - `docs/reference.md` — the full class catalog as prose tables (every
367
471
  class grouped by component, with registry key + kind). Generated from
368
472
  the same source as the types and CI-drift-checked.
@@ -378,6 +482,13 @@ Read these from `node_modules/@ponchia/ui/` — no network needed:
378
482
  boundaries.
379
483
  - `docs/annotations.md` — SVG annotation recipes and helper guidance for
380
484
  analytical figures.
485
+ - `docs/mermaid.md` — theme Mermaid diagrams from bronto tokens (resolved
486
+ `base` themeVariables, gated) and annotate the rendered SVG.
487
+ - `docs/d2.md` — theme D2 diagrams from bronto tokens (theme-override slots,
488
+ monochrome base + opt-in accent, gated) and annotate the rendered SVG.
489
+ - `docs/vega.md` — theme Vega-Lite charts from bronto tokens (resolved `config`,
490
+ gated + render-probed) — the recommended path when a report needs a chart,
491
+ since bronto ships no chart component.
381
492
  - `docs/contrast.md` — the published, CI-gated WCAG 2.1 contrast matrix
382
493
  for every contractual token pairing, per theme. Generated from the
383
494
  resolved palette; the build fails below the declared floor.
@@ -385,8 +496,11 @@ Read these from `node_modules/@ponchia/ui/` — no network needed:
385
496
  - `tokens/index.json` — tokens as plain data (global / light / dark).
386
497
  - `tokens/tokens.dtcg.json` — same tokens in W3C DTCG format.
387
498
  - `tokens/resolved.json` — every colour token resolved to a static
388
- `#rrggbb` / `rgba(...)` per theme (var() + color-mix() evaluated).
389
- Use this for non-CSS render targets: MapLibre/canvas/WebGL/SVG.
499
+ `#rrggbb` / `rgba(...)` per theme (var() + color-mix() evaluated) in
500
+ `light`/`dark`, plus a `scale` block of the non-colour scales
501
+ (spacing/radius/type/z/motion, var() chains flattened). The complete token
502
+ contract for non-CSS render targets: MapLibre/canvas/WebGL/SVG, or a
503
+ non-JS host (Python/Go) building a themed report.
390
504
  - `behaviors/index.d.ts` — typed signatures for the optional behaviors.
391
505
  - `glyphs/glyphs.d.ts` — the `GlyphName` literal union plus `renderGlyph` /
392
506
  `glyphCells` signatures for the display glyphs. (The DOM form,
@@ -404,13 +518,24 @@ Read these from `node_modules/@ponchia/ui/` — no network needed:
404
518
 
405
519
  ## Rules an agent should respect
406
520
 
407
- - Never invent `ui-*` class names. Every valid class is a member of
408
- `cls` in `classes/index.d.ts`; if it is not there, it does not exist.
521
+ - Never invent `ui-*` class names. Every valid `ui-*` class is a member of
522
+ `cls` in `classes/index.d.ts` (and `classes/classes.json`); if a `ui-*`
523
+ name is not there, it does not exist. The `is-*` state hooks
524
+ (`is-num`/`is-pos`/`is-neg`/`is-key` on `.ui-table` cells and `.ui-stat`
525
+ deltas, `is-open`/`is-active`/`is-inactive` on stateful components) are the
526
+ one exception — they are real, documented in `docs/reference.md`
527
+ (§"Table-local state classes" / §"Composition & state") and emitted by the
528
+ `ui.*()` recipes, but deliberately NOT in `cls`. Do not strip them.
409
529
  - Prefer the `ui.*()` recipes over hand-concatenating modifier strings.
410
530
  Report/layout classes added in 0.4.1 are the exception: use `cls.report*`,
411
531
  `cls.chart*`, `cls.printOnly`, or literal documented class names because
412
532
  there are no `ui.report()` / `ui.chart()` recipes yet. Annotations do have a
413
533
  `ui.annotation()` recipe for variant/tone selection.
534
+ - For a standalone HTML report, never `<link>` a `@ponchia/ui/css/*.css`
535
+ package specifier (it does not resolve in a browser) and never assume
536
+ `dist/bronto.css` carries the report/chart/legend/annotation layers — it
537
+ does not. Link each opt-in leaf from `dist/css/` via a real URL
538
+ (`./node_modules/…` or a pinned CDN). See the loading note above.
414
539
  - Override framework styles by writing your own rules outside
415
540
  `@layer bronto` — not with higher specificity or `!important`.
416
541
  - Color is tiered (ADR-0001): neutral canvas · one rationed accent · locked