@ponchia/ui 0.7.0 → 0.8.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 (52) hide show
  1. package/CHANGELOG.md +133 -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/reference.md +52 -1
  38. package/docs/reporting.md +8 -8
  39. package/docs/stability.md +31 -2
  40. package/docs/state.md +51 -1
  41. package/docs/theming.md +18 -0
  42. package/docs/usage.md +62 -2
  43. package/docs/workbench.md +83 -4
  44. package/llms.txt +1 -1
  45. package/package.json +1 -1
  46. package/tokens/figma.variables.json +84 -0
  47. package/tokens/index.d.ts +2 -2
  48. package/tokens/index.js +23 -0
  49. package/tokens/index.json +12 -0
  50. package/tokens/resolved.json +6 -0
  51. package/tokens/skins.js +117 -7
  52. package/tokens/tokens.dtcg.json +7 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,139 @@
5
5
  |> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
6
6
  |> the deprecation policy in CONTRIBUTING.md.
7
7
 
8
+ ## 0.8.0 — 2026-08-11
9
+
10
+ A single-consumer release. A full-surface audit of the largest downstream
11
+ consumer — a Yjs-collaborative spatial canvas workspace with roughly ten
12
+ thousand lines of its own CSS — found it using 26 of the 646 published classes
13
+ and hand-rebuilding much of the rest. Everything here comes from what that
14
+ consumer had to write because the framework did not provide it, or got wrong.
15
+
16
+ ### BREAKING
17
+
18
+ - **A colorway now re-points the neutral canvas, so every skinned surface
19
+ changes appearance.** No class, token, attribute, or export was removed or
20
+ renamed, and `bronto-ui-check` will report nothing — the break is *visual*,
21
+ which is why it is called out here rather than left in Changed. Until 0.7,
22
+ `data-bronto-skin` moved `--accent` and nothing else, and ADR-0001 step 4 said
23
+ so explicitly; a consumer could reasonably have relied on the canvas staying
24
+ neutral. From 0.8 the ten `SKIN_CANVAS_TOKENS` (`--bg`, `--bg-elevated`,
25
+ `--panel`, `--panel-strong`, `--panel-soft`, `--line`, `--line-strong`,
26
+ `--text`, `--text-soft`, `--text-dim`) are re-pointed per skin per theme.
27
+ - **If you want the old look**, re-declare those ten tokens after the skin
28
+ import; they are ordinary custom properties on `:root[data-bronto-skin=…]`
29
+ and un-layered app CSS wins.
30
+ - **If you already hand-wrote a canvas** for a skin — the case this change
31
+ exists to serve — delete it and check the result; yours was almost certainly
32
+ not contrast-gated, and this one is.
33
+ - **Contrast is not a regression risk.** Each neutral keeps the core token's
34
+ OKLCH lightness exactly, and `check-contrast` re-measures all 21 gated
35
+ pairings per skin per theme. Status colours are untouched by design.
36
+
37
+ ### Fixed
38
+
39
+ - **The coarse-pointer tap target was 43.5px, not 44px.** The floor was written
40
+ as a bare `2.9rem` at 23 sites across 8 stylesheets, and `css/base.css` sets
41
+ `html { font-size: 0.9375rem }` — so every control the framework floats on
42
+ touch landed half a pixel under WCAG 2.5.5 and both platform HIGs, and drifted
43
+ further under any host that shrank the root. Several comments asserted
44
+ "≈ 44px". Both floors are now published tokens clamped in px
45
+ (`--tap-target: max(44px, 2.9rem)`, `--tap-target-min: max(24px, 1.6rem)`) —
46
+ the same `max()` shape the 24px floor already used correctly in five places.
47
+ The e2e that should have caught this was derived from the token (`2.9 * rem`),
48
+ so it agreed with the bug; it now asserts the external 44px standard, and a
49
+ unit gate walks every `@media (pointer: coarse)` block and fails on any floor
50
+ written as a bare length. That gate immediately found one more:
51
+ `.ui-table__sort`.
52
+
53
+ ### Added
54
+
55
+ - **`.ui-button__label`** — the label slot for icon buttons. Wrap a button's
56
+ text in it and `--icon` decides whether the words are painted; they stay in
57
+ the accessible name and in text-based test selectors either way. One markup
58
+ shape serves both the labelled and icon-only forms, and no `aria-label` can
59
+ drift out of sync with the visible wording. The slot also ellipsises rather
60
+ than wrapping, so a labelled button in a tight bar shrinks instead of pushing
61
+ its neighbours out. `ui.button()` is unchanged; `cls.buttonLabel` is new.
62
+ - **`.ui-button--dense`** — a size tier for bars whose *height* is the
63
+ constraint (a pane title bar, a packed toolbar, a table row's actions). It
64
+ lowers only the visual floor, to `--tap-target-min`; the coarse-pointer block
65
+ still floats it to the full `--tap-target`, so a control shrunk for a mouse is
66
+ never shrunk for a finger. `ui.button({ size: 'dense' })`.
67
+ - **A canonical severity ladder** (`css/state.css`, opt-in). Bronto shipped the
68
+ *tones* long ago but never the *scale* — the tier names, their order, and the
69
+ attribute carrying them — so every consumer invented the ladder and it drifted
70
+ inside a single app: one surface saying `critical|error|warning|note`, the
71
+ next `bad|warn`, a third `critical|warning|info|ok`, under two different
72
+ attribute names, so findings did not sort against alerts. The ladder is
73
+ `critical` › `error` › `warning` › `notice` › `ok`, carried on **one**
74
+ attribute (`data-level`) across `.ui-severity`, `.ui-severity-dot` and
75
+ `.ui-severity-row`, with `SEVERITY_LEVELS` and `severity()` exported so a host
76
+ drives filters and sorts from the same list the CSS paints. `unknown` sits
77
+ deliberately *outside* the ordering: it means "not measured", and collapsing
78
+ it into `ok` is how a dead collector reads as a healthy system.
79
+ - **`.ui-pane`** (`css/workbench.css`, opt-in) — the window that `.ui-panel`
80
+ (a padded card) and `.ui-inspector` (head plus body) are not: a grab header,
81
+ a title that renames in place via `__title-input` without moving layout, and
82
+ an `__actions` slot that **scrolls rather than pushing** its last control past
83
+ the pane's clipped edge — the failure that leaves a Focus or Disconnect button
84
+ present, in the a11y tree, and unreachable.
85
+ - **`.ui-toolstrip--pane`** — the app has one toolstrip; a workbench full of
86
+ panes has one *per pane*, and those need different framing (no frame of their
87
+ own, a rule against the content below) and must not wrap, since a second row
88
+ would resize live content on every state change. `.ui-toolstrip__fill` marks
89
+ the element that absorbs slack and gives it back first.
90
+ - **`--anchored` / `--anchor-block-start` / `--anchor-block-end`** on
91
+ `.ui-selectionbar` and `.ui-toolstrip`. Both `--floating` bars were raised but
92
+ position-less, so every consumer re-derived the placement — including the
93
+ `max(offset, inset)` shape that keeps a bar out from under the home indicator.
94
+ - **Empty-state slots and an invite variant.** `.ui-empty-state` was a dashed
95
+ box that styled a `<p>`, so every empty surface in an app re-invented the same
96
+ three parts under a different name and they drifted. `__glyph` / `__lead` /
97
+ `__hint` name them, and `--invite` is the different job: an empty state
98
+ *reports absence*, an invite *offers the next action*, so it drops the dashed
99
+ frame and centres in the space it is given.
100
+ - **Safe-area tokens** — `--safe-area-top / -right / -bottom / -left`, defaulting
101
+ to `env(safe-area-inset-*, 0px)`. The framework had **no** `env()` awareness
102
+ while shipping eight viewport-anchored surfaces; all eight now read them: the
103
+ app rail and topbar, a sticky site header, the skip link, both toast stacks,
104
+ the drawer modal and the lightbox. Every rule uses
105
+ `max(<authored>, var(--safe-area-*))`, so desktop rendering is unchanged. The
106
+ values are indirected through custom properties rather than called at the
107
+ point of use because `env()` cannot be emulated by a desktop test runner —
108
+ and because a host inside its own chrome (embedded webview, kiosk frame) needs
109
+ to declare the real insets.
110
+
111
+ ### Changed
112
+
113
+ - **ADR-0001 step 4 amended** to permit the canvas re-point above (see
114
+ BREAKING). "Amber CRT" used to leave the surface grey, which is what drove the
115
+ consumer to hand-write an amber canvas in raw hex — dark theme only, outside
116
+ OKLCH, outside the contrast gate, with `e-ink` silently un-tinted. The canvas
117
+ is **derived rather than picked**: every neutral keeps the core token's OKLCH
118
+ *lightness* exactly and moves only hue and a small role-scaled chroma.
119
+ `check-skins` now rejects a partial canvas and a one-theme-only canvas, the
120
+ two shapes the hand-written version had.
121
+ - **`check:recipe-types` no longer mis-attributes options.** It sliced the
122
+ factory body to end-of-file, so the *last* recipe's chunk swallowed everything
123
+ declared after the `ui` object — an unrelated helper exported below it had its
124
+ string branches blamed on whichever recipe happened to be last. It now stops
125
+ at the object's own closing brace.
126
+ - `docs/stability.md` records the audit and the correction it forces on this
127
+ project's adoption model: "no inspected consumer imports the surface" has been
128
+ measuring **discoverability, not demand**. Those thirteen zero-use primitives
129
+ were never rejected; they were never found. Non-adoption of a leaf the
130
+ consumer never imported is not evidence for retiring it at 1.0.
131
+
132
+ ### Notes
133
+
134
+ - Anything that reads `tokens.dtcg.json` should know that six new scale tokens
135
+ are **deliberately absent** from it, listed in the root extension's
136
+ `omittedCssVariables`: the two tap-target floors are `max()` comparisons and
137
+ the four safe-area insets are `env()` reads, neither of which has a conforming
138
+ DTCG shape. `tokens.json` carries the authored CSS. This is the same treatment
139
+ `--shadow` and the em trackings already get.
140
+
8
141
  ## 0.7.0 — 2026-07-20
9
142
 
10
143
  A consumer-first contract-hardening release. Ten real applications and tools,
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@ponchia/ui?logo=npm)](https://www.npmjs.com/package/@ponchia/ui)
4
4
  [![npm provenance](https://img.shields.io/badge/npm-provenance-blue?logo=npm)](https://www.npmjs.com/package/@ponchia/ui#provenance)
5
5
  [![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
6
- [![default CSS](https://img.shields.io/badge/default%20CSS-~90kB%20%2F%20~15kB%20gzip-informational)](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
6
+ [![default CSS](https://img.shields.io/badge/default%20CSS-~92kB%20%2F%20~16kB%20gzip-informational)](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
7
7
  [![CI](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Ponchia/bronto-ui/badge)](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
@@ -81,12 +81,12 @@ Or drop it in with no build step, straight from a CDN (replace the version only
81
81
  when deliberately upgrading across a breaking pre-1.0 minor):
82
82
 
83
83
  ```html
84
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.7.0/dist/bronto.css">
84
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.8.0/dist/bronto.css">
85
85
  ```
86
86
 
87
87
  ## Quick start
88
88
 
89
- **1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~90 kB raw / ~15 kB gzip) — that is `dist/bronto.css`, not the whole package tarball:
89
+ **1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~92 kB raw / ~16 kB gzip) — that is `dist/bronto.css`, not the whole package tarball:
90
90
 
91
91
  ```css
92
92
  @import '@ponchia/ui'; /* via a bundler */
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$comment": "@ponchia/ui class vocabulary as language-neutral data — validate emitted markup without executing the ESM cls map or parsing the .d.ts. Generated from classes/index.js — do not edit by hand; run `npm run classes:json:build`. Drift-checked in CI. `groups[].base` is null for a parts-only namespace (no standalone base class — do NOT emit it). A modifier whose name contains `__` (e.g. `ui-spark__bar--pos`) attaches to THAT part, not the base host. `states` is the author-applied `is-*` hooks (runtime/behavior-managed hooks are excluded); `behaviorAttributes` are the `data-bronto-*` wiring hooks the optional behaviors delegate on; `requiredAria` is the role/aria a generator must emit per component. `states` + `customProperties` are documented in docs/reference.md and ship outside `cls` by design.",
3
3
  "counts": {
4
- "classes": 644,
5
- "groups": 179
4
+ "classes": 669,
5
+ "groups": 183
6
6
  },
7
7
  "groups": {
8
8
  "ui-accordion": {
@@ -242,13 +242,16 @@
242
242
  "base": "ui-button",
243
243
  "modifiers": [
244
244
  "ui-button--danger",
245
+ "ui-button--dense",
245
246
  "ui-button--ghost",
246
247
  "ui-button--icon",
247
248
  "ui-button--lg",
248
249
  "ui-button--sm",
249
250
  "ui-button--subtle"
250
251
  ],
251
- "parts": []
252
+ "parts": [
253
+ "ui-button__label"
254
+ ]
252
255
  },
253
256
  "ui-card": {
254
257
  "base": "ui-card",
@@ -545,8 +548,14 @@
545
548
  },
546
549
  "ui-empty-state": {
547
550
  "base": "ui-empty-state",
548
- "modifiers": [],
549
- "parts": []
551
+ "modifiers": [
552
+ "ui-empty-state--invite"
553
+ ],
554
+ "parts": [
555
+ "ui-empty-state__glyph",
556
+ "ui-empty-state__hint",
557
+ "ui-empty-state__lead"
558
+ ]
550
559
  },
551
560
  "ui-error-summary": {
552
561
  "base": "ui-error-summary",
@@ -910,6 +919,17 @@
910
919
  "ui-pagination__item"
911
920
  ]
912
921
  },
922
+ "ui-pane": {
923
+ "base": "ui-pane",
924
+ "modifiers": [],
925
+ "parts": [
926
+ "ui-pane__actions",
927
+ "ui-pane__body",
928
+ "ui-pane__head",
929
+ "ui-pane__title",
930
+ "ui-pane__title-input"
931
+ ]
932
+ },
913
933
  "ui-panel": {
914
934
  "base": "ui-panel",
915
935
  "modifiers": [],
@@ -1104,12 +1124,34 @@
1104
1124
  },
1105
1125
  "ui-selectionbar": {
1106
1126
  "base": "ui-selectionbar",
1107
- "modifiers": [],
1127
+ "modifiers": [
1128
+ "ui-selectionbar--anchor-block-end",
1129
+ "ui-selectionbar--anchor-block-start",
1130
+ "ui-selectionbar--anchored"
1131
+ ],
1108
1132
  "parts": [
1109
1133
  "ui-selectionbar__actions",
1110
1134
  "ui-selectionbar__count"
1111
1135
  ]
1112
1136
  },
1137
+ "ui-severity": {
1138
+ "base": "ui-severity",
1139
+ "modifiers": [],
1140
+ "parts": []
1141
+ },
1142
+ "ui-severity-dot": {
1143
+ "base": "ui-severity-dot",
1144
+ "modifiers": [],
1145
+ "parts": []
1146
+ },
1147
+ "ui-severity-row": {
1148
+ "base": "ui-severity-row",
1149
+ "modifiers": [],
1150
+ "parts": [
1151
+ "ui-severity-row__meta",
1152
+ "ui-severity-row__title"
1153
+ ]
1154
+ },
1113
1155
  "ui-shortcut": {
1114
1156
  "base": "ui-shortcut",
1115
1157
  "modifiers": [],
@@ -1447,13 +1489,18 @@
1447
1489
  "ui-toolstrip": {
1448
1490
  "base": "ui-toolstrip",
1449
1491
  "modifiers": [
1492
+ "ui-toolstrip--anchor-block-end",
1493
+ "ui-toolstrip--anchor-block-start",
1494
+ "ui-toolstrip--anchored",
1450
1495
  "ui-toolstrip--compact",
1451
- "ui-toolstrip--floating"
1496
+ "ui-toolstrip--floating",
1497
+ "ui-toolstrip--pane"
1452
1498
  ],
1453
1499
  "parts": [
1454
1500
  "ui-toolstrip__actions",
1455
1501
  "ui-toolstrip__brand",
1456
1502
  "ui-toolstrip__context",
1503
+ "ui-toolstrip__fill",
1457
1504
  "ui-toolstrip__group",
1458
1505
  "ui-toolstrip__search"
1459
1506
  ]
@@ -1610,11 +1657,13 @@
1610
1657
  "ui-bullet__target",
1611
1658
  "ui-button",
1612
1659
  "ui-button--danger",
1660
+ "ui-button--dense",
1613
1661
  "ui-button--ghost",
1614
1662
  "ui-button--icon",
1615
1663
  "ui-button--lg",
1616
1664
  "ui-button--sm",
1617
1665
  "ui-button--subtle",
1666
+ "ui-button__label",
1618
1667
  "ui-card",
1619
1668
  "ui-card--accent",
1620
1669
  "ui-card--interactive",
@@ -1748,6 +1797,10 @@
1748
1797
  "ui-dotspinner--lg",
1749
1798
  "ui-dotspinner--sm",
1750
1799
  "ui-empty-state",
1800
+ "ui-empty-state--invite",
1801
+ "ui-empty-state__glyph",
1802
+ "ui-empty-state__hint",
1803
+ "ui-empty-state__lead",
1751
1804
  "ui-error-summary",
1752
1805
  "ui-error-summary__list",
1753
1806
  "ui-error-summary__title",
@@ -1907,6 +1960,12 @@
1907
1960
  "ui-pagehead__title",
1908
1961
  "ui-pagination",
1909
1962
  "ui-pagination__item",
1963
+ "ui-pane",
1964
+ "ui-pane__actions",
1965
+ "ui-pane__body",
1966
+ "ui-pane__head",
1967
+ "ui-pane__title",
1968
+ "ui-pane__title-input",
1910
1969
  "ui-panel",
1911
1970
  "ui-panel__head",
1912
1971
  "ui-popover",
@@ -1994,8 +2053,16 @@
1994
2053
  "ui-sel--on",
1995
2054
  "ui-select",
1996
2055
  "ui-selectionbar",
2056
+ "ui-selectionbar--anchor-block-end",
2057
+ "ui-selectionbar--anchor-block-start",
2058
+ "ui-selectionbar--anchored",
1997
2059
  "ui-selectionbar__actions",
1998
2060
  "ui-selectionbar__count",
2061
+ "ui-severity",
2062
+ "ui-severity-dot",
2063
+ "ui-severity-row",
2064
+ "ui-severity-row__meta",
2065
+ "ui-severity-row__title",
1999
2066
  "ui-shortcut",
2000
2067
  "ui-shortcut__sep",
2001
2068
  "ui-sidebar",
@@ -2124,11 +2191,16 @@
2124
2191
  "ui-tool-call__status",
2125
2192
  "ui-tool-log",
2126
2193
  "ui-toolstrip",
2194
+ "ui-toolstrip--anchor-block-end",
2195
+ "ui-toolstrip--anchor-block-start",
2196
+ "ui-toolstrip--anchored",
2127
2197
  "ui-toolstrip--compact",
2128
2198
  "ui-toolstrip--floating",
2199
+ "ui-toolstrip--pane",
2129
2200
  "ui-toolstrip__actions",
2130
2201
  "ui-toolstrip__brand",
2131
2202
  "ui-toolstrip__context",
2203
+ "ui-toolstrip__fill",
2132
2204
  "ui-toolstrip__group",
2133
2205
  "ui-toolstrip__search",
2134
2206
  "ui-tooltip",
@@ -21,6 +21,8 @@ export declare const cls: {
21
21
  readonly buttonIcon: 'ui-button--icon';
22
22
  readonly buttonSm: 'ui-button--sm';
23
23
  readonly buttonLg: 'ui-button--lg';
24
+ readonly buttonDense: 'ui-button--dense';
25
+ readonly buttonLabel: 'ui-button__label';
24
26
  readonly card: 'ui-card';
25
27
  readonly cardHead: 'ui-card__head';
26
28
  readonly cardAccent: 'ui-card--accent';
@@ -54,6 +56,10 @@ export declare const cls: {
54
56
  readonly linkCta: 'ui-link--cta';
55
57
  readonly keyValue: 'ui-key-value';
56
58
  readonly emptyState: 'ui-empty-state';
59
+ readonly emptyStateInvite: 'ui-empty-state--invite';
60
+ readonly emptyStateGlyph: 'ui-empty-state__glyph';
61
+ readonly emptyStateLead: 'ui-empty-state__lead';
62
+ readonly emptyStateHint: 'ui-empty-state__hint';
57
63
  readonly dot: 'ui-dot';
58
64
  readonly dotAccent: 'ui-dot--accent';
59
65
  readonly dotSuccess: 'ui-dot--success';
@@ -560,6 +566,11 @@ export declare const cls: {
560
566
  readonly jobBlocked: 'ui-job--blocked';
561
567
  readonly jobFailed: 'ui-job--failed';
562
568
  readonly jobComplete: 'ui-job--complete';
569
+ readonly severity: 'ui-severity';
570
+ readonly severityDot: 'ui-severity-dot';
571
+ readonly severityRow: 'ui-severity-row';
572
+ readonly severityRowTitle: 'ui-severity-row__title';
573
+ readonly severityRowMeta: 'ui-severity-row__meta';
563
574
  readonly generated: 'ui-generated';
564
575
  readonly generatedLabel: 'ui-generated__label';
565
576
  readonly originLabel: 'ui-origin-label';
@@ -577,11 +588,22 @@ export declare const cls: {
577
588
  readonly toolstrip: 'ui-toolstrip';
578
589
  readonly toolstripFloating: 'ui-toolstrip--floating';
579
590
  readonly toolstripCompact: 'ui-toolstrip--compact';
591
+ readonly toolstripPane: 'ui-toolstrip--pane';
592
+ readonly toolstripAnchored: 'ui-toolstrip--anchored';
593
+ readonly toolstripAnchorBlockStart: 'ui-toolstrip--anchor-block-start';
594
+ readonly toolstripAnchorBlockEnd: 'ui-toolstrip--anchor-block-end';
580
595
  readonly toolstripBrand: 'ui-toolstrip__brand';
581
596
  readonly toolstripContext: 'ui-toolstrip__context';
582
597
  readonly toolstripGroup: 'ui-toolstrip__group';
583
598
  readonly toolstripActions: 'ui-toolstrip__actions';
599
+ readonly toolstripFill: 'ui-toolstrip__fill';
584
600
  readonly toolstripSearch: 'ui-toolstrip__search';
601
+ readonly pane: 'ui-pane';
602
+ readonly paneHead: 'ui-pane__head';
603
+ readonly paneTitle: 'ui-pane__title';
604
+ readonly paneTitleInput: 'ui-pane__title-input';
605
+ readonly paneActions: 'ui-pane__actions';
606
+ readonly paneBody: 'ui-pane__body';
585
607
  readonly segmentedButtons: 'ui-segmented-buttons';
586
608
  readonly segmentedButtonsButton: 'ui-segmented-buttons__button';
587
609
  readonly property: 'ui-property';
@@ -590,6 +612,9 @@ export declare const cls: {
590
612
  readonly selectionbar: 'ui-selectionbar';
591
613
  readonly selectionbarCount: 'ui-selectionbar__count';
592
614
  readonly selectionbarActions: 'ui-selectionbar__actions';
615
+ readonly selectionbarAnchored: 'ui-selectionbar--anchored';
616
+ readonly selectionbarAnchorBlockStart: 'ui-selectionbar--anchor-block-start';
617
+ readonly selectionbarAnchorBlockEnd: 'ui-selectionbar--anchor-block-end';
593
618
  readonly splitter: 'ui-splitter';
594
619
  readonly splitterVertical: 'ui-splitter--vertical';
595
620
  readonly splitterHorizontal: 'ui-splitter--horizontal';
@@ -666,7 +691,17 @@ export declare function cx(...parts: readonly ClassValue[]): string;
666
691
  export interface ButtonOpts {
667
692
  variant?: 'ghost' | 'subtle' | 'danger';
668
693
  icon?: boolean;
669
- size?: 'sm' | 'lg';
694
+ size?: 'sm' | 'lg' | 'dense';
695
+ }
696
+ export interface EmptyStateOpts {
697
+ invite?: boolean;
698
+ }
699
+ export interface ToolstripOpts {
700
+ variant?: 'floating' | 'compact' | 'pane';
701
+ anchor?: 'block-start' | 'block-end';
702
+ }
703
+ export interface SelectionbarOpts {
704
+ anchor?: 'block-start' | 'block-end';
670
705
  }
671
706
  export interface CardOpts {
672
707
  accent?: boolean;
@@ -952,11 +987,28 @@ export interface Ui {
952
987
  state(opts?: StateOpts): string;
953
988
  job(opts?: JobOpts): string;
954
989
  originLabel(opts?: OriginLabelOpts): string;
990
+ emptyState(opts?: EmptyStateOpts): string;
991
+ toolstrip(opts?: ToolstripOpts): string;
992
+ selectionbar(opts?: SelectionbarOpts): string;
955
993
  }
956
994
 
957
995
  export declare const ui: Ui;
958
996
  export default ui;
959
997
 
998
+ /** The canonical severity ladder, worst to best. `unknown` is deliberately not
999
+ * a member: it means "not measured", not "nearly ok". */
1000
+ export declare const SEVERITY_LEVELS: readonly ['critical', 'error', 'warning', 'notice', 'ok'];
1001
+
1002
+ export type SeverityLevel = (typeof SEVERITY_LEVELS)[number];
1003
+
1004
+ /** Bundles `data-level` with the severity class so the attribute that carries
1005
+ * the meaning cannot be forgotten. An unrecognised level resolves to
1006
+ * `'unknown'`, which paints the neutral tone. */
1007
+ export declare function severity(
1008
+ level: SeverityLevel | (string & {}) | null | undefined,
1009
+ opts?: { part?: 'chip' | 'dot' | 'row' },
1010
+ ): { class: string; 'data-level': string };
1011
+
960
1012
  /** Min/max for the value-bearing fills; defaults to 0–100. */
961
1013
  export interface ValueRangeOpts {
962
1014
  min?: number;
package/classes/index.js CHANGED
@@ -25,6 +25,8 @@ export const cls = Object.freeze({
25
25
  buttonIcon: 'ui-button--icon',
26
26
  buttonSm: 'ui-button--sm',
27
27
  buttonLg: 'ui-button--lg',
28
+ buttonDense: 'ui-button--dense',
29
+ buttonLabel: 'ui-button__label',
28
30
  card: 'ui-card',
29
31
  cardHead: 'ui-card__head',
30
32
  cardAccent: 'ui-card--accent',
@@ -58,6 +60,10 @@ export const cls = Object.freeze({
58
60
  linkCta: 'ui-link--cta',
59
61
  keyValue: 'ui-key-value',
60
62
  emptyState: 'ui-empty-state',
63
+ emptyStateInvite: 'ui-empty-state--invite',
64
+ emptyStateGlyph: 'ui-empty-state__glyph',
65
+ emptyStateLead: 'ui-empty-state__lead',
66
+ emptyStateHint: 'ui-empty-state__hint',
61
67
  // dots
62
68
  dot: 'ui-dot',
63
69
  dotAccent: 'ui-dot--accent',
@@ -597,6 +603,12 @@ export const cls = Object.freeze({
597
603
  jobBlocked: 'ui-job--blocked',
598
604
  jobFailed: 'ui-job--failed',
599
605
  jobComplete: 'ui-job--complete',
606
+ // severity ladder — the second axis of state (css/state.css)
607
+ severity: 'ui-severity',
608
+ severityDot: 'ui-severity-dot',
609
+ severityRow: 'ui-severity-row',
610
+ severityRowTitle: 'ui-severity-row__title',
611
+ severityRowMeta: 'ui-severity-row__meta',
600
612
  // generated content / AI-trust surfaces (css/generated.css)
601
613
  generated: 'ui-generated',
602
614
  generatedLabel: 'ui-generated__label',
@@ -616,11 +628,22 @@ export const cls = Object.freeze({
616
628
  toolstrip: 'ui-toolstrip',
617
629
  toolstripFloating: 'ui-toolstrip--floating',
618
630
  toolstripCompact: 'ui-toolstrip--compact',
631
+ toolstripPane: 'ui-toolstrip--pane',
632
+ toolstripAnchored: 'ui-toolstrip--anchored',
633
+ toolstripAnchorBlockStart: 'ui-toolstrip--anchor-block-start',
634
+ toolstripAnchorBlockEnd: 'ui-toolstrip--anchor-block-end',
619
635
  toolstripBrand: 'ui-toolstrip__brand',
620
636
  toolstripContext: 'ui-toolstrip__context',
621
637
  toolstripGroup: 'ui-toolstrip__group',
622
638
  toolstripActions: 'ui-toolstrip__actions',
639
+ toolstripFill: 'ui-toolstrip__fill',
623
640
  toolstripSearch: 'ui-toolstrip__search',
641
+ pane: 'ui-pane',
642
+ paneHead: 'ui-pane__head',
643
+ paneTitle: 'ui-pane__title',
644
+ paneTitleInput: 'ui-pane__title-input',
645
+ paneActions: 'ui-pane__actions',
646
+ paneBody: 'ui-pane__body',
624
647
  segmentedButtons: 'ui-segmented-buttons',
625
648
  segmentedButtonsButton: 'ui-segmented-buttons__button',
626
649
  property: 'ui-property',
@@ -629,6 +652,9 @@ export const cls = Object.freeze({
629
652
  selectionbar: 'ui-selectionbar',
630
653
  selectionbarCount: 'ui-selectionbar__count',
631
654
  selectionbarActions: 'ui-selectionbar__actions',
655
+ selectionbarAnchored: 'ui-selectionbar--anchored',
656
+ selectionbarAnchorBlockStart: 'ui-selectionbar--anchor-block-start',
657
+ selectionbarAnchorBlockEnd: 'ui-selectionbar--anchor-block-end',
632
658
  splitter: 'ui-splitter',
633
659
  splitterVertical: 'ui-splitter--vertical',
634
660
  splitterHorizontal: 'ui-splitter--horizontal',
@@ -934,6 +960,7 @@ export const ui = {
934
960
  icon && cls.buttonIcon,
935
961
  size === 'sm' && cls.buttonSm,
936
962
  size === 'lg' && cls.buttonLg,
963
+ size === 'dense' && cls.buttonDense,
937
964
  ),
938
965
  card: ({ accent, interactive } = {}) =>
939
966
  j(cls.card, accent && cls.cardAccent, interactive && cls.cardInteractive),
@@ -1092,8 +1119,41 @@ export const ui = {
1092
1119
  state: ({ state, busy } = {}) => j(cls.state, stateTone(state), busy && cls.stateBusy),
1093
1120
  job: ({ state, compact } = {}) => j(cls.job, jobTone(state), compact && cls.jobCompact),
1094
1121
  originLabel: ({ ai } = {}) => j(cls.originLabel, ai && cls.originLabelAi),
1122
+ emptyState: ({ invite } = {}) => j(cls.emptyState, invite && cls.emptyStateInvite),
1123
+ toolstrip: ({ variant, anchor } = {}) =>
1124
+ j(
1125
+ cls.toolstrip,
1126
+ variant === 'floating' && cls.toolstripFloating,
1127
+ variant === 'compact' && cls.toolstripCompact,
1128
+ variant === 'pane' && cls.toolstripPane,
1129
+ anchor && cls.toolstripAnchored,
1130
+ anchor === 'block-start' && cls.toolstripAnchorBlockStart,
1131
+ anchor === 'block-end' && cls.toolstripAnchorBlockEnd,
1132
+ ),
1133
+ selectionbar: ({ anchor } = {}) =>
1134
+ j(
1135
+ cls.selectionbar,
1136
+ anchor && cls.selectionbarAnchored,
1137
+ anchor === 'block-start' && cls.selectionbarAnchorBlockStart,
1138
+ anchor === 'block-end' && cls.selectionbarAnchorBlockEnd,
1139
+ ),
1095
1140
  };
1096
1141
 
1142
+ /** The canonical severity ladder, worst to best. `unknown` is deliberately NOT
1143
+ * in the ordering — it means "not measured", not "nearly ok", and sorting it
1144
+ * next to `ok` is how a dead collector reads as a healthy system. Exported as
1145
+ * data so a host can drive filters and sorts from the same list the CSS
1146
+ * paints, instead of re-typing the tiers. */
1147
+ export const SEVERITY_LEVELS = Object.freeze(['critical', 'error', 'warning', 'notice', 'ok']);
1148
+
1149
+ /** `data-level` + the severity class, bundled so the attribute that carries the
1150
+ * meaning cannot be forgotten — the class alone paints the neutral tone and
1151
+ * silently loses the level. Unknown/absent levels resolve to `unknown`. */
1152
+ export const severity = (level, { part = 'chip' } = {}) => ({
1153
+ class: part === 'dot' ? cls.severityDot : part === 'row' ? cls.severityRow : cls.severity,
1154
+ 'data-level': SEVERITY_LEVELS.includes(level) ? level : 'unknown',
1155
+ });
1156
+
1097
1157
  // Attribute + style bundle for the data-bearing fills (`ui-meter`/`ui-progress`).
1098
1158
  // The class string alone paints a 0-width, unannounced bar: the fill width is a
1099
1159
  // UNITLESS `--value` (0–100) and AT needs role + aria-valuenow/min/max. This sets
@@ -257,6 +257,22 @@
257
257
  "name": "--radius-xl",
258
258
  "description": "Global scale token. Value: `4px`"
259
259
  },
260
+ {
261
+ "name": "--safe-area-bottom",
262
+ "description": "Global scale token. Value: `env(safe-area-inset-bottom, 0px)`"
263
+ },
264
+ {
265
+ "name": "--safe-area-left",
266
+ "description": "Global scale token. Value: `env(safe-area-inset-left, 0px)`"
267
+ },
268
+ {
269
+ "name": "--safe-area-right",
270
+ "description": "Global scale token. Value: `env(safe-area-inset-right, 0px)`"
271
+ },
272
+ {
273
+ "name": "--safe-area-top",
274
+ "description": "Global scale token. Value: `env(safe-area-inset-top, 0px)`"
275
+ },
260
276
  {
261
277
  "name": "--sans",
262
278
  "description": "Global scale token. Value: `'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif`"
@@ -341,6 +357,14 @@
341
357
  "name": "--surface-raised",
342
358
  "description": "Global scale token. Value: `var(--panel-strong)`"
343
359
  },
360
+ {
361
+ "name": "--tap-target",
362
+ "description": "Global scale token. Value: `max(44px, 2.9rem)`"
363
+ },
364
+ {
365
+ "name": "--tap-target-min",
366
+ "description": "Global scale token. Value: `max(24px, 1.6rem)`"
367
+ },
344
368
  {
345
369
  "name": "--text",
346
370
  "description": "Theme token. Light: `#0a0a0a` · Dark: `#e6e6e6`"
package/css/app.css CHANGED
@@ -21,7 +21,15 @@
21
21
  display: flex;
22
22
  flex-direction: column;
23
23
  gap: var(--space-md);
24
- padding: var(--space-md);
24
+
25
+ /* The rail runs the full viewport height, so on a notched device its top and
26
+ bottom sit under the status bar and the home indicator. Grow the padding to
27
+ clear them; `max()` keeps the desktop value exactly as authored, since the
28
+ insets resolve to 0px there. The inline-start inset covers a landscape
29
+ rotation that puts the cutout on the rail's side. */
30
+ padding: max(var(--space-md), var(--safe-area-top)) var(--space-md)
31
+ max(var(--space-md), var(--safe-area-bottom))
32
+ max(var(--space-md), var(--safe-area-left));
25
33
  position: sticky;
26
34
  inset-block-start: 0;
27
35
  block-size: 100svh;
@@ -108,7 +116,7 @@
108
116
 
109
117
  @media (pointer: coarse) {
110
118
  .ui-app-nav a {
111
- min-block-size: 2.9rem;
119
+ min-block-size: var(--tap-target);
112
120
  padding-inline: 0.9rem;
113
121
  }
114
122
  }
@@ -178,7 +186,10 @@
178
186
  display: flex;
179
187
  gap: var(--space-md);
180
188
  justify-content: space-between;
181
- padding: 0.75rem var(--space-lg);
189
+
190
+ /* Sticks to the very top, so the bar's own background must bleed under the
191
+ status bar while its CONTENT clears it — pad, do not offset. */
192
+ padding: max(0.75rem, var(--safe-area-top)) var(--space-lg) 0.75rem;
182
193
  position: sticky;
183
194
  inset-block-start: 0;
184
195
  z-index: var(--z-raised);