@ponchia/ui 0.7.0 → 0.8.1

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 (54) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +3 -3
  3. package/classes/classes.json +79 -7
  4. package/classes/index.d.ts +53 -1
  5. package/classes/index.js +60 -0
  6. package/classes/vscode.css-custom-data.json +24 -0
  7. package/css/app.css +14 -3
  8. package/css/disclosure.css +7 -7
  9. package/css/feedback.css +11 -8
  10. package/css/forms.css +3 -3
  11. package/css/navigation.css +1 -1
  12. package/css/overlay.css +19 -5
  13. package/css/primitives.css +101 -6
  14. package/css/site.css +11 -5
  15. package/css/skins.css +97 -3
  16. package/css/state.css +161 -0
  17. package/css/table.css +1 -1
  18. package/css/tokens.css +6 -0
  19. package/css/workbench.css +151 -0
  20. package/dist/bronto.css +1 -1
  21. package/dist/css/app.css +1 -1
  22. package/dist/css/disclosure.css +1 -1
  23. package/dist/css/feedback.css +1 -1
  24. package/dist/css/forms.css +1 -1
  25. package/dist/css/navigation.css +1 -1
  26. package/dist/css/overlay.css +1 -1
  27. package/dist/css/primitives.css +1 -1
  28. package/dist/css/report-kit.css +1 -1
  29. package/dist/css/site.css +1 -1
  30. package/dist/css/skins.css +1 -1
  31. package/dist/css/state.css +1 -1
  32. package/dist/css/table.css +1 -1
  33. package/dist/css/tokens.css +1 -1
  34. package/dist/css/workbench.css +1 -1
  35. package/docs/adr/0001-color-system.md +32 -3
  36. package/docs/contrast.md +102 -18
  37. package/docs/migrations/0.7-to-0.8.md +216 -0
  38. package/docs/package-contract.md +1 -0
  39. package/docs/reference.md +52 -1
  40. package/docs/reporting.md +8 -8
  41. package/docs/stability.md +31 -2
  42. package/docs/state.md +51 -1
  43. package/docs/theming.md +18 -0
  44. package/docs/usage.md +62 -2
  45. package/docs/workbench.md +83 -4
  46. package/llms.txt +1 -1
  47. package/package.json +13 -12
  48. package/tokens/figma.variables.json +84 -0
  49. package/tokens/index.d.ts +2 -2
  50. package/tokens/index.js +23 -0
  51. package/tokens/index.json +12 -0
  52. package/tokens/resolved.json +6 -0
  53. package/tokens/skins.js +117 -7
  54. package/tokens/tokens.dtcg.json +7 -1
package/docs/stability.md CHANGED
@@ -1,7 +1,7 @@
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. In practical terms: **PATCH releases (`0.7.x`) are
4
+ patches are non-breaking. In practical terms: **PATCH releases (`0.8.x`) are
5
5
  non-breaking bug-fixes and additive changes — safe to upgrade without review;
6
6
  MINOR releases (`0.x.0`) may include breaking changes and consumers should
7
7
  review the CHANGELOG before upgrading.** Pin `~0.x` (tilde) to accept only
@@ -68,13 +68,41 @@ Package-proven is necessary but does not establish demand. During the catalog
68
68
  freeze, do not expand package-only surfaces. Recheck this table against real
69
69
  consumer upgrades before the 1.0 release candidate.
70
70
 
71
+ > **The 0.8.0 consumer audit — read this before trusting the table below.** A
72
+ > full-surface audit of the largest downstream consumer (a Yjs-collaborative
73
+ > spatial canvas workspace: React, Vite, ~10k lines of its own CSS) found it
74
+ > using **26 of the 646 published classes**, and hand-rebuilding much of the
75
+ > rest — 10 bespoke empty states, 15 error surfaces, 17 control bars, three
76
+ > parallel severity vocabularies, its own Markdown prose layer, its own diff
77
+ > rows, its own meters, and two copies of visually-hidden text. Thirteen
78
+ > primitives that map almost exactly onto what it built return **zero** uses:
79
+ > `ui-state`, `ui-prose`, `ui-diff`, `ui-code`, `ui-meter`, `ui-progress`,
80
+ > `ui-toolstrip`, `ui-dot`, `ui-chip`, `ui-tag`, `ui-timeline`, `ui-steps`,
81
+ > `ui-job`.
82
+ >
83
+ > The lesson for this document is not about that consumer. It is that **"no
84
+ > inspected consumer imports the surface" has been measuring discoverability,
85
+ > not demand.** Those surfaces were not rejected; they were never found. The
86
+ > opt-in leaf model is right, but nothing tells a consumer which leaf it is
87
+ > about to reimplement — and `bronto-ui-check`, which catches part of it, was
88
+ > installed in that consumer and had never been run. Before any 1.0 decision
89
+ > retires a package-proven surface for lack of adoption, confirm the surface was
90
+ > *reachable*: named in the consumer's imports, or at least in a leaf it
91
+ > imports. Non-adoption of an unimported leaf is not evidence.
92
+ >
93
+ > The same audit produced this release's four consumer-driven changes (the tap
94
+ > target floor, the safe-area tokens, `ui-button__label`, and the dense tier)
95
+ > and the ADR-0001 canvas amendment. That is the intended loop: the consumer's
96
+ > local classes are the backlog, resolving either to a deletion there or an
97
+ > addition here.
98
+
71
99
  | Surface family | Current evidence | 1.0 disposition |
72
100
  | --- | --- | --- |
73
101
  | Core CSS, class recipes, vanilla behaviors, tokens, and Tailwind bridge | Downstream-proven across five inspected non-example app, site, and service consumers. | Stabilize names and behavior contracts. Use consumer upgrades as the release-candidate proof. |
74
102
  | Report, provenance, analytical CSS, annotations, glyphs, skins, workbench CSS, chart data, and Vega theme | Downstream-proven across four inspected report, site, dashboard, and tool consumers. | Keep opt-in. Stabilize the consumed paths; do not broaden the catalog during the freeze. |
75
103
  | Controlled non-`<dialog>` modal | Package-proven by stack, portal, late-node, focus, and cleanup regressions. None of ten inspected non-example consumers initializes `initModal`. | Deprecated in 0.7; remove no earlier than 0.8 unless a real consumer adopts it. Native `<dialog>` + `initDialog` is the stable path. |
76
104
  | React, Solid, Qwik, Svelte, and Vue lifecycle adapters | Package-proven by packed examples, types, and lifecycle tests. None of ten inspected non-example consumers imports an adapter entrypoint. | Deprecated in 0.7; remove no earlier than 0.8 unless a real consumer adopts one. Vanilla behaviors remain stable. |
77
- | Mermaid, D2, Shiki, Figma Variables, and the report-claims schema | Package-proven by generated-data, render, schema, and drift checks. No inspected non-example consumer currently supplies downstream proof for every path. | Keep compatible through 0.7.x, but decide each 1.0 contract from adoption evidence rather than generator coverage alone. |
105
+ | Mermaid, D2, Shiki, Figma Variables, and the report-claims schema | Package-proven by generated-data, render, schema, and drift checks. No inspected non-example consumer currently supplies downstream proof for every path. | Keep compatible through 0.8.x, but decide each 1.0 contract from adoption evidence rather than generator coverage alone. |
78
106
 
79
107
  After 1.0, breaking changes move to majors. Until then, the table below is the
80
108
  current public-surface matrix and the release policy above still applies.
@@ -85,6 +113,7 @@ current public-surface matrix and the release policy above still applies.
85
113
  | JS module format | Stable | JS subpaths are ESM-only. CommonJS consumers use dynamic `import()`. |
86
114
  | CSS class names (`.ui-*`) | Stable | Names and documented modifier semantics are public. Internal selector structure and leaf-file boundaries may change. |
87
115
  | Class recipes (`@ponchia/ui/classes`) | Stable | Exported `cls`, `ui`, `cx`, `attrs`, recipe names, ARIA attribute helper names, and option unions are public. |
116
+ | Severity ladder (`SEVERITY_LEVELS`, `severity()`, `.ui-severity*`) | Stable additive | The tier names and **their order** are public: `critical` › `error` › `warning` › `notice` › `ok`, with `unknown` deliberately outside the ordering as the fallback for "not measured". `severity()` returns `{ class, 'data-level' }`, and `data-level` is the public attribute contract — a host may carry it on its own element and read `var(--severity-tone)`. New tiers would be a breaking reorder, not an addition, so the ladder is closed; new *parts* (`chip`/`dot`/`row`) are additive. Ships in the opt-in `css/state.css`, not the default bundle. |
88
117
  | Class vocabulary as data (`@ponchia/ui/classes.json`, `@ponchia/ui/vscode.css-custom-data.json`) | Stable additive | The JSON shape (`groups`/`classes`/`states`/`customProperties`) and class/custom-property entries are public — for validating markup from a non-JS/non-TS host or editor integration. Generated from `cls` and CSS selectors; `classes.json`, `.d.ts`, reference docs, and VS Code custom data are drift-checked together. New classes/hooks are additive. |
89
118
  | 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. |
90
119
  | `--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. |
package/docs/state.md CHANGED
@@ -124,14 +124,64 @@ For indeterminate jobs, omit the progress block or omit `aria-valuenow`, and
124
124
  make the written state clear ("Running", "Waiting for worker", "Retrying").
125
125
  Use `ui-job--compact` for dense queues.
126
126
 
127
+ ## Severity — `.ui-severity`
128
+
129
+ `.ui-state` answers *what is this thing doing*. Severity answers *how bad is
130
+ it*. Bronto already shipped the tones as per-component modifiers, but never the
131
+ **scale** — the tier names, their order, and the attribute carrying them. So
132
+ every consumer invents the ladder, and inside one app it drifts: one surface
133
+ saying `critical|error|warning|note`, the next `bad|warn`, a third
134
+ `critical|warning|info|ok`, under two different attribute names. Findings then
135
+ do not sort against alerts, and a filter written for one list misses the other.
136
+
137
+ The ladder, worst to best:
138
+
139
+ | Level | Means |
140
+ | --- | --- |
141
+ | `critical` | Broken now, and still losing something. |
142
+ | `error` | Something failed; it is not currently getting worse. |
143
+ | `warning` | A threshold was crossed; nothing has failed yet. |
144
+ | `notice` | Worth reading, no action implied. |
145
+ | `ok` | Checked and healthy — an **assertion**, not the absence of news. |
146
+ | `unknown` | Not measured, stale, or the check itself failed. |
147
+
148
+ `unknown` sits **outside** the ordering on purpose. It is not "slightly worse
149
+ than ok", it is "we do not know" — and collapsing it into `ok` is how a dead
150
+ collector reads as a healthy system. `SEVERITY_LEVELS` therefore excludes it.
151
+
152
+ The level travels on `data-level`, one attribute name, so the same selector
153
+ works on a chip, a row, a dot, or your own element via `var(--severity-tone)`:
154
+
155
+ ```html
156
+ <span class="ui-severity" data-level="critical">Critical</span>
157
+
158
+ <li class="ui-severity-row" data-level="warning">
159
+ <span class="ui-severity-row__title">Disk 84% on kpi-1</span>
160
+ <span class="ui-severity-row__meta">12m</span>
161
+ </li>
162
+ ```
163
+
164
+ Colour is never the only channel (WCAG 1.4.1): `.ui-severity` carries an
165
+ author-written label, and `.ui-severity-dot` is only for rows that **also** name
166
+ their level in text.
167
+
127
168
  ## Recipe
128
169
 
129
170
  ```js
130
- import { ui } from '@ponchia/ui/classes';
171
+ import { ui, severity, SEVERITY_LEVELS } from '@ponchia/ui/classes';
131
172
 
132
173
  ui.state({ state: 'saving', busy: true }); // "ui-state ui-state--saving ui-state--busy"
133
174
  ui.state({ state: 'conflict' }); // "ui-state ui-state--conflict"
134
175
  ui.job({ state: 'running' }); // "ui-job ui-job--running"
176
+
177
+ // Bundles the class WITH data-level, so the attribute that carries the meaning
178
+ // cannot be forgotten — the class alone paints the neutral tone and silently
179
+ // loses the level.
180
+ severity('critical'); // { class: 'ui-severity', 'data-level': 'critical' }
181
+ severity('warning', { part: 'row' }); // { class: 'ui-severity-row', … }
182
+ severity('nope'); // { class: 'ui-severity', 'data-level': 'unknown' }
183
+
184
+ SEVERITY_LEVELS; // ['critical','error','warning','notice','ok'] — sort/filter from this
135
185
  ```
136
186
 
137
187
  ## Scope
package/docs/theming.md CHANGED
@@ -140,6 +140,24 @@ you change CSS `--accent` later.
140
140
  [ADR-0003](adr/0003-theme-model.md) for the theme-model rationale.
141
141
  - **Radius** — `--radius-sm … --radius-xl`, `--radius-pill`. The Nothing
142
142
  default is near-sharp; raise these for a softer brand.
143
+ - **Tap targets** — `--tap-target` (44px, WCAG 2.5.5 / iOS HIG / Material) is
144
+ the floor every control floats to inside `@media (pointer: coarse)`;
145
+ `--tap-target-min` (24px) is the WCAG 2.5.8 AA minimum used by controls that
146
+ only have to clear the smaller bar. Both are authored as `max(px, rem)` on
147
+ purpose: they scale up with a larger root font but **cannot shrink below the
148
+ standard** if you re-point `html { font-size }`. If you override them, keep
149
+ the clamp — a bare rem is how a 44px floor quietly becomes 43.5px. Raise them
150
+ for a glove-friendly or kiosk build; do not lower them.
151
+ - **Safe areas** — `--safe-area-top / -right / -bottom / -left` default to
152
+ `env(safe-area-inset-*, 0px)`, so they are 0 everywhere except a device with
153
+ a display cutout or a gesture bar. Every viewport-anchored surface Bronto
154
+ ships reads them: the app rail and topbar, a sticky site header, the skip
155
+ link, both toast stacks, the drawer modal and the lightbox. Override them
156
+ when your host supplies its own insets — an embedded webview, a kiosk frame,
157
+ or a test runner that cannot emulate `env()`, which is the reason the values
158
+ are indirected through custom properties rather than called at the point of
159
+ use. Consumers positioning their own floating chrome should follow the same
160
+ convention: `inset-block-end: max(<your offset>, var(--safe-area-bottom))`.
143
161
  - **Type** — `--display` (dot-matrix face), `--mono`, `--sans`. Override
144
162
  to drop Doto or swap the body face; the token layer keeps working even
145
163
  if you self-host fonts (see the `fonts.css` note in the README).
package/docs/usage.md CHANGED
@@ -167,15 +167,50 @@ a `.ui-legend` key, and fallback data. Full LLM/static report cookbook:
167
167
 
168
168
  ## Buttons: variant and size
169
169
 
170
- - **primary** the single most important action in a view. Aim for one.
170
+ - **primary is the bare `ui-button`.** There is no `--primary` and no
171
+ `--accent`: the unmodified class already paints the accent fill, and the
172
+ variants below all step *down* from it. Aim for one per view. Consumers reach
173
+ for a `--primary` or `--accent` modifier often enough that it is worth
174
+ stating plainly: neither exists. Writing one is harmless *and invisible* —
175
+ the unknown class does nothing and the button still looks right, which is why
176
+ the mistake survives review. `bronto-ui-check` is what catches it.
171
177
  - **ghost** — secondary actions; the default for "another button here".
172
178
  - **subtle** — tertiary / low-stakes (toolbar, inline).
179
+ - **danger** — destructive confirmation.
173
180
  - Size: default everywhere; `--sm` for dense tooling (toolbars,
174
- pagination, table row actions), `--lg` for a hero CTA only.
181
+ pagination, table row actions), `--lg` for a hero CTA only, `--dense` when a
182
+ bar's *height* is the constraint (a pane title bar). `--dense` lowers only the
183
+ visual floor — coarse pointers still get the full `--tap-target`, so a control
184
+ you shrink for a mouse is never shrunk for a finger.
175
185
  - Loading is **not** a class: set `aria-busy="true"` (+ `disabled`); the
176
186
  spinner is CSS. This is the ARIA-driven contract — see reference.md
177
187
  → "Composition & state".
178
188
 
189
+ ## Empty state vs invite
190
+
191
+ Both use `ui-empty-state`, and the slots are the same three parts — a quiet
192
+ `__glyph`, one sentence of full ink in `__lead`, a quieter `__hint`. What
193
+ differs is the job:
194
+
195
+ - **Plain `ui-empty-state`** *reports absence*: a dashed card saying this region
196
+ has no rows today. Use it for a list, a table, a results pane.
197
+ - **`--invite`** *offers the next action*: no dashed box (there is nothing to
198
+ outline — the surface itself is what you are being invited into) and it
199
+ centres in whatever block space it is given. Use it for a surface the user is
200
+ meant to fill.
201
+
202
+ ```html
203
+ <div class="ui-empty-state ui-empty-state--invite">
204
+ <span class="ui-empty-state__glyph" aria-hidden="true">+</span>
205
+ <p class="ui-empty-state__lead">Nothing pinned yet</p>
206
+ <p class="ui-empty-state__hint">Drop a file here, or press ⌘K</p>
207
+ </div>
208
+ ```
209
+
210
+ Without the slots, every empty surface in an app re-invents these three parts
211
+ under a different name and they drift — one has a glyph, the next has two
212
+ sentences at the same weight, a third is a bare `<p>`.
213
+
179
214
  ## Link vs link--cta
180
215
 
181
216
  Plain `ui-link` for in-flow links. `ui-link--cta` is the eyebrow-faced
@@ -278,6 +313,31 @@ without it these widgets are unlabelled or unannounced:
278
313
  - **Icon-only buttons** (`ui-button--icon` and any glyph-only control) carry no
279
314
  text node, so they're nameless to AT — give them an `aria-label`
280
315
  (`<button class="ui-button ui-button--icon" aria-label="Delete">`).
316
+ Prefer `ui-button__label` when the same control also appears with its word
317
+ visible, or when a test reads the button by its text. Wrap the text in the
318
+ slot and let `--icon` decide whether it is painted — the markup does not
319
+ change, the accessible name survives, and no `aria-label` can drift out of
320
+ sync with the visible wording:
321
+
322
+ ```js
323
+ // The mask comes from renderGlyph(..., { render: 'mask' }) — there is no
324
+ // --glyph-* token.
325
+ const mask = renderGlyph('trash', { render: 'mask' });
326
+ el.innerHTML =
327
+ `<button class="ui-button ui-button--icon">` +
328
+ `<span class="ui-icon" style="--icon-mask: ${mask}"></span>` +
329
+ `<span class="ui-button__label">Delete</span>` +
330
+ `</button>`;
331
+ ```
332
+
333
+ Drop `--icon` and the same markup renders glyph + word. The slot also
334
+ ellipsises rather than wrapping, so a labelled button in a width-constrained
335
+ bar shrinks instead of pushing its neighbours out.
336
+ - **`ui-button--dense`** is for bars whose height is the constraint — a pane
337
+ title bar, a packed toolbar, a table row's actions. It lowers only the
338
+ *visual* floor, to the WCAG 2.5.8 24px minimum. The coarse-pointer block
339
+ still floats it to the full `--tap-target`, so a control you shrink for a
340
+ mouse is never shrunk for a finger.
281
341
  - **`ui-sitenav` / `ui-app-nav`** — signal the current link with
282
342
  `aria-current="page"` (both honour it; `ui-app-nav` also accepts the
283
343
  visual-only `.is-active`, but prefer `aria-current`).
package/docs/workbench.md CHANGED
@@ -1,4 +1,4 @@
1
- # Workbench — split panes, toolstrips, inspector, properties, selection bar
1
+ # Workbench — panes, split panes, toolstrips, inspector, properties, selection bar
2
2
 
3
3
  `@ponchia/ui/css/workbench.css` is an opt-in set of primitives for **real
4
4
  tools**: resizable split panes, compact toolstrips, button-mode segmented
@@ -52,6 +52,29 @@ hiding, and placement.
52
52
  </header>
53
53
  ```
54
54
 
55
+ ### Pane-scale — `.ui-toolstrip--pane`
56
+
57
+ The app has one toolstrip; a workbench full of panes has one *per pane*, and
58
+ those are a different thing. The difference is framing, not density: a pane bar
59
+ is a row inside a surface that already has a border, so it drops its own frame
60
+ and corners and rules off from the content below.
61
+
62
+ It also refuses to wrap. A pane bar usually sits directly above content that may
63
+ be a live terminal, an editor, or a video — and a second row would resize that
64
+ content every time a control appears. The row scrolls instead, and
65
+ `.ui-toolstrip__fill` marks the one element that absorbs slack and gives it back
66
+ first (a title, a path, a filter input), so nothing else is pushed out of reach.
67
+
68
+ ```html
69
+ <div class="ui-toolstrip ui-toolstrip--pane">
70
+ <button class="ui-button ui-button--subtle ui-button--dense" type="button">Run</button>
71
+ <span class="ui-toolstrip__fill ui-mono">src/server/routes/reports.ts</span>
72
+ <button class="ui-button ui-button--ghost ui-button--icon ui-button--dense" type="button">
73
+ <span class="ui-button__label">Close</span>
74
+ </button>
75
+ </div>
76
+ ```
77
+
55
78
  ## Button segmented control — `.ui-segmented-buttons`
56
79
 
57
80
  Use `.ui-segmented-buttons` when each option is a real command button and the
@@ -170,11 +193,67 @@ A raised bar of actions on the current selection: a `__count` on one side,
170
193
  </div>
171
194
  ```
172
195
 
196
+ ### Anchoring a floating bar
197
+
198
+ Both `--floating` bars are raised but position-less, so every consumer
199
+ re-derives the same thing — including the `max(offset, inset)` shape, which is
200
+ the part people get wrong by writing a bare offset that a phone then swallows
201
+ under the home indicator. `--anchored` centres the bar against its positioned
202
+ ancestor and clears the safe area:
203
+
204
+ ```html
205
+ <div class="ui-selectionbar ui-selectionbar--anchored">…</div>
206
+
207
+ <!-- The bar that must NOT sit under the thumb: recovery, destructive actions. -->
208
+ <div class="ui-selectionbar ui-selectionbar--anchor-block-start">…</div>
209
+ ```
210
+
211
+ `--anchored` alone means bottom. Override the gap with `--anchor-offset`. The
212
+ host still owns `z-index`, because only it knows what else is on the canvas.
213
+
214
+ ## Pane — `.ui-pane`
215
+
216
+ `.ui-panel` is a padded card and `.ui-inspector` is head-plus-body; neither is a
217
+ *window*. A pane is what a canvas node, a floating tool window, or a dockable
218
+ panel needs: a header you can drag, a title that renames in place, an actions
219
+ slot that survives a narrow pane, and a body that owns the rest.
220
+
221
+ ```html
222
+ <article class="ui-pane">
223
+ <header class="ui-pane__head">
224
+ <strong class="ui-pane__title">deploy.log</strong>
225
+ <span class="ui-pane__actions">
226
+ <button class="ui-button ui-button--subtle ui-button--icon ui-button--dense" type="button">
227
+ <span class="ui-button__label">Focus</span>
228
+ </button>
229
+ </span>
230
+ </header>
231
+ <div class="ui-pane__body">…</div>
232
+ </article>
233
+ ```
234
+
235
+ Two behaviours are worth knowing, because both come from a real failure:
236
+
237
+ - **The title gives up space first.** It is the only thing in the header that
238
+ can be truncated without losing a function.
239
+ - **`__actions` scrolls rather than pushing.** Sized `flex: 0 1 auto` with
240
+ `overflow-x: auto`, so a pane narrow enough to run out of room scrolls its
241
+ controls instead of pushing the last one past the clipped edge — which is how
242
+ an app ends up with a Focus or Disconnect button that exists, is in the a11y
243
+ tree, and cannot be reached.
244
+
245
+ Swap the title for `.ui-pane__title-input` to rename in place. It inherits the
246
+ type it replaces, so the swap moves no layout; only the accent border says you
247
+ are typing a name now. The host owns dragging, z-order, focus policy and
248
+ persistence — this leaf has no behavior.
249
+
173
250
  ## Scope
174
251
 
175
- No recipes these are structural containers and rows; apply the classes
176
- directly (or read them from `cls.toolstrip`, `cls.splitter`, `cls.inspector`,
177
- `cls.property`, …). Pair the selection bar with the cross-cutting
252
+ No recipes for the structural containers and rows; apply the classes directly
253
+ (or read them from `cls.toolstrip`, `cls.pane`, `cls.splitter`, `cls.inspector`,
254
+ `cls.property`, …). `ui.toolstrip()` and `ui.selectionbar()` exist only to
255
+ compose the variant/anchor modifiers. Pair the selection bar with the
256
+ cross-cutting
178
257
  [`ui-sel`](./selection.md) states on the selected items themselves. Bronto styles
179
258
  the chrome and wires the splitter affordance; the host owns hit-testing,
180
259
  persistence, pane contents, viewport semantics, and commands.
package/llms.txt CHANGED
@@ -45,7 +45,7 @@ the path changes from source `css/` to built `dist/css/`:
45
45
  <!-- installed locally -->
46
46
  <link rel="stylesheet" href="./node_modules/@ponchia/ui/dist/css/<leaf>.css" />
47
47
  <!-- or from a CDN; pin the version (pre-1.0, breaking changes ship in the minor) -->
48
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.7.0/dist/css/<leaf>.css" />
48
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.8.1/dist/css/<leaf>.css" />
49
49
  ```
50
50
 
51
51
  The flattened default bundle is `dist/bronto.css` (bundler shorthand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ponchia/ui",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "CSS-first identity and UI layer for services, tools, sites, and reports — works in HTML, every framework, and PDF, no component runtime. Shared app shell, forms, tables, workflow chrome, plus opt-in analytical/report primitives. Monochrome with one rationed accent. Zero runtime dependencies.",
6
6
  "keywords": [
@@ -113,6 +113,7 @@
113
113
  "docs/migrations/0.4-to-0.5.md",
114
114
  "docs/migrations/0.5-to-0.6.md",
115
115
  "docs/migrations/0.6-to-0.7.md",
116
+ "docs/migrations/0.7-to-0.8.md",
116
117
  "docs/adr/0001-color-system.md",
117
118
  "docs/adr/0002-scope-and-2026-baseline.md",
118
119
  "docs/adr/0003-theme-model.md",
@@ -213,24 +214,24 @@
213
214
  "prepublishOnly": "npm run build:artifacts && npm run check"
214
215
  },
215
216
  "devDependencies": {
216
- "@arethetypeswrong/cli": "^0.18.4",
217
+ "@arethetypeswrong/cli": "^0.18.5",
217
218
  "@axe-core/playwright": "^4.11.3",
218
219
  "@builder.io/qwik": "^1.20.0",
219
220
  "@playwright/test": "1.60.0",
220
221
  "github-actionlint": "^1.7.12",
221
- "jsdom": "^29.1.1",
222
- "knip": "^6.23.0",
223
- "pdfjs-dist": "^6.1.200",
224
- "prettier": "^3.9.1",
225
- "publint": "^0.3.21",
226
- "react": "^19.2.7",
227
- "react-dom": "^19.2.7",
228
- "solid-js": "^1.9.13",
229
- "stylelint": "^17.14.0",
222
+ "jsdom": "^30.0.1",
223
+ "knip": "^6.32.1",
224
+ "pdfjs-dist": "^6.2.108",
225
+ "prettier": "^3.9.6",
226
+ "publint": "^0.3.23",
227
+ "react": "^19.2.8",
228
+ "react-dom": "^19.2.8",
229
+ "solid-js": "^1.9.14",
230
+ "stylelint": "^17.14.1",
230
231
  "stylelint-config-standard": "^40.0.0",
231
232
  "stylelint-use-logical": "^2.1.3",
232
233
  "typescript": "^6.0.3",
233
- "vega": "^6.2.0",
234
+ "vega": "^6.3.1",
234
235
  "vega-lite": "^6.4.3"
235
236
  },
236
237
  "peerDependencies": {
@@ -1906,6 +1906,62 @@
1906
1906
  },
1907
1907
  "unit": "px"
1908
1908
  },
1909
+ {
1910
+ "name": "safe/area/bottom",
1911
+ "type": "STRING",
1912
+ "sourceCssVariable": "--safe-area-bottom",
1913
+ "valuesByMode": {
1914
+ "global": "env(safe-area-inset-bottom, 0px)"
1915
+ },
1916
+ "$extensions": {
1917
+ "com.ponchia.css": {
1918
+ "variable": "--safe-area-bottom",
1919
+ "value": "env(safe-area-inset-bottom, 0px)"
1920
+ }
1921
+ }
1922
+ },
1923
+ {
1924
+ "name": "safe/area/left",
1925
+ "type": "STRING",
1926
+ "sourceCssVariable": "--safe-area-left",
1927
+ "valuesByMode": {
1928
+ "global": "env(safe-area-inset-left, 0px)"
1929
+ },
1930
+ "$extensions": {
1931
+ "com.ponchia.css": {
1932
+ "variable": "--safe-area-left",
1933
+ "value": "env(safe-area-inset-left, 0px)"
1934
+ }
1935
+ }
1936
+ },
1937
+ {
1938
+ "name": "safe/area/right",
1939
+ "type": "STRING",
1940
+ "sourceCssVariable": "--safe-area-right",
1941
+ "valuesByMode": {
1942
+ "global": "env(safe-area-inset-right, 0px)"
1943
+ },
1944
+ "$extensions": {
1945
+ "com.ponchia.css": {
1946
+ "variable": "--safe-area-right",
1947
+ "value": "env(safe-area-inset-right, 0px)"
1948
+ }
1949
+ }
1950
+ },
1951
+ {
1952
+ "name": "safe/area/top",
1953
+ "type": "STRING",
1954
+ "sourceCssVariable": "--safe-area-top",
1955
+ "valuesByMode": {
1956
+ "global": "env(safe-area-inset-top, 0px)"
1957
+ },
1958
+ "$extensions": {
1959
+ "com.ponchia.css": {
1960
+ "variable": "--safe-area-top",
1961
+ "value": "env(safe-area-inset-top, 0px)"
1962
+ }
1963
+ }
1964
+ },
1909
1965
  {
1910
1966
  "name": "sans",
1911
1967
  "type": "STRING",
@@ -2025,6 +2081,34 @@
2025
2081
  },
2026
2082
  "unit": "rem"
2027
2083
  },
2084
+ {
2085
+ "name": "tap/target",
2086
+ "type": "STRING",
2087
+ "sourceCssVariable": "--tap-target",
2088
+ "valuesByMode": {
2089
+ "global": "max(44px, 2.9rem)"
2090
+ },
2091
+ "$extensions": {
2092
+ "com.ponchia.css": {
2093
+ "variable": "--tap-target",
2094
+ "value": "max(44px, 2.9rem)"
2095
+ }
2096
+ }
2097
+ },
2098
+ {
2099
+ "name": "tap/target/min",
2100
+ "type": "STRING",
2101
+ "sourceCssVariable": "--tap-target-min",
2102
+ "valuesByMode": {
2103
+ "global": "max(24px, 1.6rem)"
2104
+ },
2105
+ "$extensions": {
2106
+ "com.ponchia.css": {
2107
+ "variable": "--tap-target-min",
2108
+ "value": "max(24px, 1.6rem)"
2109
+ }
2110
+ }
2111
+ },
2028
2112
  {
2029
2113
  "name": "text/2xs",
2030
2114
  "type": "FLOAT",
package/tokens/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  export type ThemeName = 'light' | 'dark';
5
5
 
6
- export type GlobalTokenName = '--radius-xl' | '--radius-lg' | '--radius-md' | '--radius-sm' | '--radius-pill' | '--space-2xs' | '--space-xs' | '--space-sm' | '--space-md' | '--space-lg' | '--space-xl' | '--space-2xl' | '--mono' | '--sans' | '--dot-font' | '--display' | '--display-weight' | '--display-weight-strong' | '--text-2xs' | '--text-xs' | '--text-sm' | '--text-base' | '--text-lg' | '--text-xl' | '--tracking-wide' | '--tracking-wider' | '--ease-standard' | '--ease-spring' | '--ease-out' | '--duration-fast' | '--duration-base' | '--duration-slow' | '--dot-size' | '--dot-gap' | '--z-base' | '--z-raised' | '--z-sticky' | '--z-overlay' | '--z-popover' | '--z-toast' | '--accent-1' | '--accent-2' | '--accent-3' | '--accent-4' | '--accent-5' | '--accent-6' | '--surface-1' | '--surface-2' | '--surface-3' | '--surface-4' | '--surface-5' | '--surface-6' | '--bronto-color-bg' | '--bronto-color-surface' | '--bronto-color-surface-raised' | '--bronto-color-border' | '--bronto-color-border-strong' | '--bronto-color-text' | '--bronto-color-text-muted' | '--bronto-color-action' | '--bronto-color-on-action' | '--bronto-color-focus' | '--bronto-color-success' | '--bronto-color-warning' | '--bronto-color-danger' | '--bronto-color-info' | '--surface' | '--surface-raised' | '--surface-muted' | '--border' | '--border-strong';
6
+ export type GlobalTokenName = '--radius-xl' | '--radius-lg' | '--radius-md' | '--radius-sm' | '--radius-pill' | '--space-2xs' | '--space-xs' | '--space-sm' | '--space-md' | '--space-lg' | '--space-xl' | '--space-2xl' | '--tap-target' | '--tap-target-min' | '--safe-area-top' | '--safe-area-right' | '--safe-area-bottom' | '--safe-area-left' | '--mono' | '--sans' | '--dot-font' | '--display' | '--display-weight' | '--display-weight-strong' | '--text-2xs' | '--text-xs' | '--text-sm' | '--text-base' | '--text-lg' | '--text-xl' | '--tracking-wide' | '--tracking-wider' | '--ease-standard' | '--ease-spring' | '--ease-out' | '--duration-fast' | '--duration-base' | '--duration-slow' | '--dot-size' | '--dot-gap' | '--z-base' | '--z-raised' | '--z-sticky' | '--z-overlay' | '--z-popover' | '--z-toast' | '--accent-1' | '--accent-2' | '--accent-3' | '--accent-4' | '--accent-5' | '--accent-6' | '--surface-1' | '--surface-2' | '--surface-3' | '--surface-4' | '--surface-5' | '--surface-6' | '--bronto-color-bg' | '--bronto-color-surface' | '--bronto-color-surface-raised' | '--bronto-color-border' | '--bronto-color-border-strong' | '--bronto-color-text' | '--bronto-color-text-muted' | '--bronto-color-action' | '--bronto-color-on-action' | '--bronto-color-focus' | '--bronto-color-success' | '--bronto-color-warning' | '--bronto-color-danger' | '--bronto-color-info' | '--surface' | '--surface-raised' | '--surface-muted' | '--border' | '--border-strong';
7
7
  export type LightTokenName = '--bg' | '--bg-elevated' | '--bg-accent' | '--panel' | '--panel-strong' | '--panel-soft' | '--line' | '--line-strong' | '--text' | '--text-soft' | '--text-dim' | '--accent' | '--accent-ramp-end' | '--accent-strong' | '--accent-text' | '--on-accent' | '--accent-soft' | '--success' | '--success-soft' | '--warning' | '--warning-soft' | '--danger' | '--danger-soft' | '--info' | '--info-soft' | '--code-bg' | '--button-text' | '--field-dot' | '--field-dot-hot' | '--field-dot-accent' | '--focus-ring' | '--shadow' | '--shadow-raised';
8
8
  export type DarkTokenName = '--bg' | '--bg-elevated' | '--bg-accent' | '--panel' | '--panel-strong' | '--panel-soft' | '--line' | '--line-strong' | '--text' | '--text-soft' | '--text-dim' | '--accent' | '--accent-ramp-end' | '--accent-strong' | '--accent-text' | '--on-accent' | '--accent-soft' | '--success' | '--success-soft' | '--warning' | '--warning-soft' | '--danger' | '--danger-soft' | '--info' | '--info-soft' | '--code-bg' | '--button-text' | '--field-dot' | '--field-dot-hot' | '--field-dot-accent' | '--focus-ring' | '--shadow' | '--shadow-raised';
9
9
 
@@ -14,7 +14,7 @@ export declare const cssVars: {
14
14
  dark: Record<DarkTokenName, string>;
15
15
  };
16
16
 
17
- export type ScaleKey = 'radius-xl' | 'radius-lg' | 'radius-md' | 'radius-sm' | 'radius-pill' | 'space-2xs' | 'space-xs' | 'space-sm' | 'space-md' | 'space-lg' | 'space-xl' | 'space-2xl' | 'mono' | 'sans' | 'dot-font' | 'display' | 'display-weight' | 'display-weight-strong' | 'text-2xs' | 'text-xs' | 'text-sm' | 'text-base' | 'text-lg' | 'text-xl' | 'tracking-wide' | 'tracking-wider' | 'ease-standard' | 'ease-spring' | 'ease-out' | 'duration-fast' | 'duration-base' | 'duration-slow' | 'dot-size' | 'dot-gap' | 'z-base' | 'z-raised' | 'z-sticky' | 'z-overlay' | 'z-popover' | 'z-toast' | 'accent-1' | 'accent-2' | 'accent-3' | 'accent-4' | 'accent-5' | 'accent-6' | 'surface-1' | 'surface-2' | 'surface-3' | 'surface-4' | 'surface-5' | 'surface-6' | 'bronto-color-bg' | 'bronto-color-surface' | 'bronto-color-surface-raised' | 'bronto-color-border' | 'bronto-color-border-strong' | 'bronto-color-text' | 'bronto-color-text-muted' | 'bronto-color-action' | 'bronto-color-on-action' | 'bronto-color-focus' | 'bronto-color-success' | 'bronto-color-warning' | 'bronto-color-danger' | 'bronto-color-info' | 'surface' | 'surface-raised' | 'surface-muted' | 'border' | 'border-strong';
17
+ export type ScaleKey = 'radius-xl' | 'radius-lg' | 'radius-md' | 'radius-sm' | 'radius-pill' | 'space-2xs' | 'space-xs' | 'space-sm' | 'space-md' | 'space-lg' | 'space-xl' | 'space-2xl' | 'tap-target' | 'tap-target-min' | 'safe-area-top' | 'safe-area-right' | 'safe-area-bottom' | 'safe-area-left' | 'mono' | 'sans' | 'dot-font' | 'display' | 'display-weight' | 'display-weight-strong' | 'text-2xs' | 'text-xs' | 'text-sm' | 'text-base' | 'text-lg' | 'text-xl' | 'tracking-wide' | 'tracking-wider' | 'ease-standard' | 'ease-spring' | 'ease-out' | 'duration-fast' | 'duration-base' | 'duration-slow' | 'dot-size' | 'dot-gap' | 'z-base' | 'z-raised' | 'z-sticky' | 'z-overlay' | 'z-popover' | 'z-toast' | 'accent-1' | 'accent-2' | 'accent-3' | 'accent-4' | 'accent-5' | 'accent-6' | 'surface-1' | 'surface-2' | 'surface-3' | 'surface-4' | 'surface-5' | 'surface-6' | 'bronto-color-bg' | 'bronto-color-surface' | 'bronto-color-surface-raised' | 'bronto-color-border' | 'bronto-color-border-strong' | 'bronto-color-text' | 'bronto-color-text-muted' | 'bronto-color-action' | 'bronto-color-on-action' | 'bronto-color-focus' | 'bronto-color-success' | 'bronto-color-warning' | 'bronto-color-danger' | 'bronto-color-info' | 'surface' | 'surface-raised' | 'surface-muted' | 'border' | 'border-strong';
18
18
  export type ColorKey = 'bg' | 'bg-elevated' | 'bg-accent' | 'panel' | 'panel-strong' | 'panel-soft' | 'line' | 'line-strong' | 'text' | 'text-soft' | 'text-dim' | 'accent' | 'accent-ramp-end' | 'accent-strong' | 'accent-text' | 'on-accent' | 'accent-soft' | 'success' | 'success-soft' | 'warning' | 'warning-soft' | 'danger' | 'danger-soft' | 'info' | 'info-soft' | 'code-bg' | 'button-text' | 'field-dot' | 'field-dot-hot' | 'field-dot-accent' | 'focus-ring' | 'shadow' | 'shadow-raised';
19
19
 
20
20
  /** Ergonomic view derived from {@link cssVars} (`--` prefix stripped). */
package/tokens/index.js CHANGED
@@ -33,6 +33,29 @@ export const cssVars = {
33
33
  '--space-lg': '1.35rem',
34
34
  '--space-xl': '1.75rem',
35
35
  '--space-2xl': '2.5rem',
36
+ // Tap targets. Both floors are clamped in px against the rem so they cannot
37
+ // shrink below the standard when a host re-points the root font size —
38
+ // Bronto's own base sets `html { font-size: 0.9375rem }`, under which a bare
39
+ // 2.9rem is 43.5px, half a pixel short of the target floor.
40
+ // `--tap-target` is the WCAG 2.5.5 (AAA) / iOS-HIG / Material 44px target
41
+ // every coarse-pointer control floats to. `--tap-target-min` is the WCAG
42
+ // 2.5.8 (AA) 24px minimum, for controls that only have to clear the smaller
43
+ // bar (standalone CTA links, disclosure carets).
44
+ '--tap-target': 'max(44px, 2.9rem)',
45
+ '--tap-target-min': 'max(24px, 1.6rem)',
46
+ // Display cutouts and system gesture areas. Every viewport-anchored surface
47
+ // Bronto ships — the app rail and topbar, a sticky site header, the skip
48
+ // link, the toast stacks, the drawer and the lightbox — reads these instead
49
+ // of calling env() at the point of use. The indirection is the point: env()
50
+ // cannot be emulated by a desktop browser runner, so routing every inset
51
+ // through an overrideable custom property is what makes full-bleed framing
52
+ // testable, and what lets a host running inside its own chrome (an embedded
53
+ // webview, a kiosk frame) declare the real insets. They resolve to 0px off
54
+ // a notched device, so every consuming rule is a no-op on desktop.
55
+ '--safe-area-top': 'env(safe-area-inset-top, 0px)',
56
+ '--safe-area-right': 'env(safe-area-inset-right, 0px)',
57
+ '--safe-area-bottom': 'env(safe-area-inset-bottom, 0px)',
58
+ '--safe-area-left': 'env(safe-area-inset-left, 0px)',
36
59
  '--mono': "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace",
37
60
  '--sans':
38
61
  "'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
package/tokens/index.json CHANGED
@@ -14,6 +14,12 @@
14
14
  "--space-lg": "1.35rem",
15
15
  "--space-xl": "1.75rem",
16
16
  "--space-2xl": "2.5rem",
17
+ "--tap-target": "max(44px, 2.9rem)",
18
+ "--tap-target-min": "max(24px, 1.6rem)",
19
+ "--safe-area-top": "env(safe-area-inset-top, 0px)",
20
+ "--safe-area-right": "env(safe-area-inset-right, 0px)",
21
+ "--safe-area-bottom": "env(safe-area-inset-bottom, 0px)",
22
+ "--safe-area-left": "env(safe-area-inset-left, 0px)",
17
23
  "--mono": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace",
18
24
  "--sans": "'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
19
25
  "--dot-font": "'Doto', var(--mono)",
@@ -159,6 +165,12 @@
159
165
  "space-lg": "1.35rem",
160
166
  "space-xl": "1.75rem",
161
167
  "space-2xl": "2.5rem",
168
+ "tap-target": "max(44px, 2.9rem)",
169
+ "tap-target-min": "max(24px, 1.6rem)",
170
+ "safe-area-top": "env(safe-area-inset-top, 0px)",
171
+ "safe-area-right": "env(safe-area-inset-right, 0px)",
172
+ "safe-area-bottom": "env(safe-area-inset-bottom, 0px)",
173
+ "safe-area-left": "env(safe-area-inset-left, 0px)",
162
174
  "mono": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace",
163
175
  "sans": "'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
164
176
  "dot-font": "'Doto', var(--mono)",
@@ -141,6 +141,12 @@
141
141
  "--space-lg": "1.35rem",
142
142
  "--space-xl": "1.75rem",
143
143
  "--space-2xl": "2.5rem",
144
+ "--tap-target": "max(44px, 2.9rem)",
145
+ "--tap-target-min": "max(24px, 1.6rem)",
146
+ "--safe-area-top": "env(safe-area-inset-top, 0px)",
147
+ "--safe-area-right": "env(safe-area-inset-right, 0px)",
148
+ "--safe-area-bottom": "env(safe-area-inset-bottom, 0px)",
149
+ "--safe-area-left": "env(safe-area-inset-left, 0px)",
144
150
  "--mono": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace",
145
151
  "--sans": "'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
146
152
  "--dot-font": "'Doto', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace",