@ponchia/ui 0.6.12 → 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 (84) hide show
  1. package/CHANGELOG.md +210 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +14 -6
  4. package/behaviors/dialog.d.ts.map +1 -1
  5. package/behaviors/dialog.js +14 -0
  6. package/behaviors/modal.d.ts +4 -0
  7. package/behaviors/modal.d.ts.map +1 -1
  8. package/behaviors/modal.js +4 -0
  9. package/behaviors/splitter.d.ts +2 -0
  10. package/behaviors/splitter.d.ts.map +1 -1
  11. package/behaviors/splitter.js +15 -1
  12. package/behaviors/theme.d.ts +3 -2
  13. package/behaviors/theme.d.ts.map +1 -1
  14. package/behaviors/theme.js +10 -6
  15. package/bin/bronto-ui-check.mjs +286 -0
  16. package/classes/classes.json +86 -7
  17. package/classes/index.d.ts +53 -1
  18. package/classes/index.js +60 -0
  19. package/classes/vscode.css-custom-data.json +24 -0
  20. package/css/app.css +14 -3
  21. package/css/disclosure.css +15 -5
  22. package/css/dots.css +43 -18
  23. package/css/feedback.css +46 -8
  24. package/css/forms.css +3 -3
  25. package/css/navigation.css +1 -1
  26. package/css/overlay.css +19 -5
  27. package/css/primitives.css +101 -6
  28. package/css/report.css +0 -40
  29. package/css/site.css +19 -3
  30. package/css/skins.css +97 -3
  31. package/css/state.css +161 -0
  32. package/css/table.css +1 -1
  33. package/css/tokens.css +6 -0
  34. package/css/workbench.css +151 -0
  35. package/dist/bronto.css +1 -1
  36. package/dist/css/app.css +1 -1
  37. package/dist/css/disclosure.css +1 -1
  38. package/dist/css/dots.css +1 -1
  39. package/dist/css/feedback.css +1 -1
  40. package/dist/css/forms.css +1 -1
  41. package/dist/css/navigation.css +1 -1
  42. package/dist/css/overlay.css +1 -1
  43. package/dist/css/primitives.css +1 -1
  44. package/dist/css/report-kit.css +1 -1
  45. package/dist/css/report.css +1 -1
  46. package/dist/css/site.css +1 -1
  47. package/dist/css/skins.css +1 -1
  48. package/dist/css/state.css +1 -1
  49. package/dist/css/table.css +1 -1
  50. package/dist/css/tokens.css +1 -1
  51. package/dist/css/workbench.css +1 -1
  52. package/docs/adr/0001-color-system.md +32 -3
  53. package/docs/adr/0004-prune-unused-adapters.md +34 -0
  54. package/docs/architecture.md +14 -10
  55. package/docs/command.md +18 -4
  56. package/docs/contrast.md +102 -18
  57. package/docs/migrations/0.6-to-0.7.md +85 -0
  58. package/docs/package-contract.md +10 -5
  59. package/docs/reference.md +52 -1
  60. package/docs/reporting.md +8 -8
  61. package/docs/stability.md +39 -9
  62. package/docs/state.md +51 -1
  63. package/docs/theming.md +67 -5
  64. package/docs/usage.md +119 -19
  65. package/docs/workbench.md +99 -6
  66. package/llms.txt +7 -3
  67. package/package.json +13 -3
  68. package/qwik/index.d.ts.map +1 -1
  69. package/qwik/index.js +4 -0
  70. package/react/index.d.ts.map +1 -1
  71. package/react/index.js +4 -0
  72. package/solid/index.d.ts.map +1 -1
  73. package/solid/index.js +4 -0
  74. package/svelte/index.d.ts.map +1 -1
  75. package/svelte/index.js +4 -0
  76. package/tokens/figma.variables.json +84 -0
  77. package/tokens/index.d.ts +2 -2
  78. package/tokens/index.js +23 -0
  79. package/tokens/index.json +12 -0
  80. package/tokens/resolved.json +6 -0
  81. package/tokens/skins.js +117 -7
  82. package/tokens/tokens.dtcg.json +2514 -399
  83. package/vue/index.d.ts.map +1 -1
  84. package/vue/index.js +4 -0
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);
@@ -44,11 +44,11 @@
44
44
  border-color var(--duration-fast) var(--ease-standard);
45
45
  }
46
46
 
47
- /* Coarse-pointer tap-target floor (~2.9rem ≈ 44px), matching inputs/sitenav —
47
+ /* Coarse-pointer tap-target floor (`--tap-target`), matching inputs/sitenav —
48
48
  tabs are easy to mis-tap on touch (WCAG 2.5.8 — C24). */
49
49
  @media (pointer: coarse) {
50
50
  .ui-tab {
51
- min-block-size: 2.9rem;
51
+ min-block-size: var(--tap-target);
52
52
  }
53
53
  }
54
54
 
@@ -272,6 +272,16 @@
272
272
  }
273
273
  }
274
274
 
275
+ @media (pointer: coarse) {
276
+ .ui-breadcrumb__item a {
277
+ align-items: center;
278
+ display: inline-flex;
279
+ justify-content: center;
280
+ min-block-size: var(--tap-target-min);
281
+ min-inline-size: var(--tap-target-min);
282
+ }
283
+ }
284
+
275
285
  /* --- Pagination --- */
276
286
 
277
287
  .ui-pagination {
@@ -339,12 +349,12 @@
339
349
  }
340
350
  }
341
351
 
342
- /* Coarse-pointer tap-target floor, matching inputs/sitenav (~2.9rem ≈ 44px)
352
+ /* Coarse-pointer tap-target floor (`--tap-target`), matching inputs/sitenav —
343
353
  pagination controls are easy to mis-tap on touch (WCAG 2.5.8 — C24). */
344
354
  @media (pointer: coarse) {
345
355
  .ui-pagination__item {
346
- min-block-size: 2.9rem;
347
- min-inline-size: 2.9rem;
356
+ min-block-size: var(--tap-target);
357
+ min-inline-size: var(--tap-target);
348
358
  }
349
359
  }
350
360
 
package/css/dots.css CHANGED
@@ -136,7 +136,9 @@
136
136
 
137
137
  /* Status dot — the glyph-style state indicator. */
138
138
  .ui-dot {
139
- background: var(--text-dim);
139
+ --ui-dot-color: var(--text-dim);
140
+
141
+ background: var(--ui-dot-color);
140
142
  border-radius: 50%;
141
143
  display: inline-block;
142
144
  flex: 0 0 auto;
@@ -145,34 +147,45 @@
145
147
  }
146
148
 
147
149
  .ui-dot--accent {
148
- background: var(--accent);
150
+ --ui-dot-color: var(--accent);
149
151
  }
150
152
 
151
153
  .ui-dot--success {
152
- background: var(--success);
154
+ --ui-dot-color: var(--success);
153
155
  }
154
156
 
155
157
  .ui-dot--warning {
156
- background: var(--warning);
158
+ --ui-dot-color: var(--warning);
157
159
  }
158
160
 
159
161
  .ui-dot--danger {
160
- background: var(--danger);
162
+ --ui-dot-color: var(--danger);
161
163
  }
162
164
 
163
165
  .ui-dot--info {
164
- background: var(--info);
166
+ --ui-dot-color: var(--info);
165
167
  }
166
168
 
167
169
  .ui-dot--live {
168
- background: var(--success);
169
- box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 70%, transparent);
170
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--ui-dot-color) 70%, transparent);
170
171
  position: relative;
171
172
  }
172
173
 
174
+ /* Live describes motion, not status. A standalone live dot defaults to
175
+ success; an explicit tone remains the colour of both the dot and ring. */
176
+ .ui-dot--live:not(
177
+ .ui-dot--accent,
178
+ .ui-dot--success,
179
+ .ui-dot--warning,
180
+ .ui-dot--danger,
181
+ .ui-dot--info
182
+ ) {
183
+ --ui-dot-color: var(--success);
184
+ }
185
+
173
186
  .ui-dot--live::after {
174
187
  animation: pulseRing 1.8s var(--ease-out) infinite;
175
- border: 1px solid var(--success);
188
+ border: 1px solid var(--ui-dot-color);
176
189
  border-radius: 50%;
177
190
  content: '';
178
191
  inset: -3px;
@@ -562,36 +575,48 @@
562
575
  differentiable. */
563
576
  @media (forced-colors: active) {
564
577
  .ui-dot--success {
578
+ --ui-dot-color: LinkText;
579
+
565
580
  forced-color-adjust: none;
566
- background: LinkText;
567
581
  }
568
582
 
569
583
  .ui-dot--warning {
584
+ --ui-dot-color: Mark;
585
+
570
586
  forced-color-adjust: none;
571
- background: Mark;
572
587
  }
573
588
 
574
589
  .ui-dot--danger {
590
+ --ui-dot-color: Highlight;
591
+
575
592
  forced-color-adjust: none;
576
- background: Highlight;
577
593
  }
578
594
 
579
595
  .ui-dot--info {
596
+ --ui-dot-color: ButtonText;
597
+
580
598
  forced-color-adjust: none;
581
- background: ButtonText;
582
599
  }
583
600
 
584
601
  /* Brand/live dots aren't status tones, but they still encode meaning via
585
602
  background-color alone, which HCM flattens. Keep them on a distinct,
586
603
  opted-out system colour for completeness. */
587
- .ui-dot--accent,
588
- .ui-dot--live {
604
+ .ui-dot--accent {
605
+ --ui-dot-color: LinkText;
606
+
589
607
  forced-color-adjust: none;
590
- background: LinkText;
591
608
  }
592
609
 
593
- .ui-dot--live::after {
594
- border-color: LinkText;
610
+ .ui-dot--live:not(
611
+ .ui-dot--accent,
612
+ .ui-dot--success,
613
+ .ui-dot--warning,
614
+ .ui-dot--danger,
615
+ .ui-dot--info
616
+ ) {
617
+ --ui-dot-color: LinkText;
618
+
619
+ forced-color-adjust: none;
595
620
  }
596
621
 
597
622
  /* The masked one-node icon paints `background: currentcolor` through an SVG
package/css/feedback.css CHANGED
@@ -130,9 +130,12 @@
130
130
  display: grid;
131
131
  gap: 0.5rem;
132
132
 
133
- /* Logical so the stack anchors bottom inline-end and mirrors in RTL. */
134
- inset-block: auto 1.25rem;
135
- inset-inline: auto 1.25rem;
133
+ /* Logical so the stack anchors bottom inline-end and mirrors in RTL. The
134
+ bottom offset clears the home-indicator gesture area: a toast landing there
135
+ is both hard to read and hard to dismiss, because the system swallows the
136
+ swipe. `max()` leaves the desktop offset untouched. */
137
+ inset-block: auto max(1.25rem, var(--safe-area-bottom));
138
+ inset-inline: auto max(1.25rem, var(--safe-area-right));
136
139
  justify-items: end;
137
140
  max-inline-size: min(22rem, calc(100vw - 2rem));
138
141
  pointer-events: none;
@@ -144,7 +147,7 @@
144
147
  overlaps the polite bottom stack and reads as a distinct, urgent
145
148
  channel. */
146
149
  .ui-toast-stack--assertive {
147
- inset-block: 1.25rem auto;
150
+ inset-block: max(1.25rem, var(--safe-area-top)) auto;
148
151
  }
149
152
 
150
153
  .ui-toast {
@@ -225,15 +228,15 @@
225
228
  }
226
229
 
227
230
  /* Touch: enlarge the dismiss/close hit areas to the project's coarse-pointer
228
- tap-target floor (2.9rem, as in primitives.css / forms.css) without changing
229
- the desktop glyph size. The box is centred so the glyph stays put. */
231
+ tap-target floor (`--tap-target`, as in primitives.css / forms.css) without
232
+ changing the desktop glyph size. The box is centred so the glyph stays put. */
230
233
  @media (pointer: coarse) {
231
234
  .ui-alert__close,
232
235
  .ui-toast__close {
233
236
  display: inline-grid;
234
237
  place-items: center;
235
- min-block-size: 2.9rem;
236
- min-inline-size: 2.9rem;
238
+ min-block-size: var(--tap-target);
239
+ min-inline-size: var(--tap-target);
237
240
  }
238
241
 
239
242
  .ui-alert__close {
@@ -517,6 +520,41 @@
517
520
  background: var(--info);
518
521
  }
519
522
 
523
+ /* A labelled meter is core feedback grammar, not report-only layout. The
524
+ visible value remains the data of record because role=meter support varies. */
525
+ .ui-meter__row {
526
+ align-items: center;
527
+ display: grid;
528
+ gap: var(--space-2xs) var(--space-md);
529
+ grid-template-columns: minmax(9rem, 14rem) 1fr auto;
530
+ margin-block: var(--space-2xs);
531
+ }
532
+
533
+ .ui-meter__row .ui-meter {
534
+ min-inline-size: 8rem;
535
+ }
536
+
537
+ .ui-meter__label {
538
+ color: var(--text-soft);
539
+ }
540
+
541
+ .ui-meter__value {
542
+ color: var(--text);
543
+ font-family: var(--mono);
544
+ font-variant-numeric: tabular-nums;
545
+ text-align: end;
546
+ }
547
+
548
+ @media (max-width: 32rem) {
549
+ .ui-meter__row {
550
+ grid-template-columns: 1fr;
551
+ }
552
+
553
+ .ui-meter__value {
554
+ text-align: start;
555
+ }
556
+ }
557
+
520
558
  /* --- Steps — progress through a multi-step flow. Use an <ol>; the
521
559
  current step is aria-current="step" (no class), completed steps take
522
560
  --done. Counter-numbered markers, hairline connectors. --- */
package/css/forms.css CHANGED
@@ -476,18 +476,18 @@
476
476
  .ui-input,
477
477
  .ui-select,
478
478
  .ui-textarea {
479
- min-block-size: 2.9rem;
479
+ min-block-size: var(--tap-target);
480
480
  }
481
481
 
482
482
  .ui-search {
483
- min-block-size: 2.9rem;
483
+ min-block-size: var(--tap-target);
484
484
  }
485
485
 
486
486
  /* The whole label is the toggle target — float it to the ~44px floor so the
487
487
  small track/box isn't a sub-target on touch (WCAG 2.5.8 — C24). */
488
488
  .ui-switch,
489
489
  .ui-check {
490
- min-block-size: 2.9rem;
490
+ min-block-size: var(--tap-target);
491
491
  }
492
492
 
493
493
  .ui-check input {
@@ -95,7 +95,7 @@
95
95
 
96
96
  @media (pointer: coarse) {
97
97
  .ui-themetoggle__button {
98
- min-block-size: 2.9rem;
98
+ min-block-size: var(--tap-target);
99
99
  padding-inline: 0.9rem;
100
100
  }
101
101
  }
package/css/overlay.css CHANGED
@@ -149,6 +149,15 @@ html:has(.ui-modal.is-open) {
149
149
  margin-inline-start: auto;
150
150
  max-block-size: 100vh;
151
151
  max-inline-size: min(26rem, 100vw);
152
+
153
+ /* Full-height by design, so the panel's edges are the screen's edges: its
154
+ surface should still reach them, but its content must clear the cutout and
155
+ the home indicator. Add the INSET ONLY — `.ui-modal` is deliberately
156
+ `padding: 0` (its head/body parts own their spacing), so a `max(space, …)`
157
+ floor here would silently pad the drawer on every device, not just a
158
+ notched one. */
159
+ padding-block: var(--safe-area-top) var(--safe-area-bottom);
160
+ padding-inline-end: var(--safe-area-right);
152
161
  border-radius: 0;
153
162
  border-width: 0;
154
163
  border-inline-start-width: 1px;
@@ -162,16 +171,16 @@ html:has(.ui-modal.is-open) {
162
171
  }
163
172
 
164
173
  /* Comfortable hit target on coarse pointers — this bespoke close button measured
165
- ~26px on touch, below the 2.9rem floor the rest of the button family meets
166
- (WCAG 2.5.8). Scoped to coarse so the fine-pointer (mouse) rendering, which
174
+ ~26px on touch, below the `--tap-target` floor the rest of the button family
175
+ meets. Scoped to coarse so the fine-pointer (mouse) rendering, which
167
176
  already clears 24×24, is unchanged; centre the glyph in the enlarged box. */
168
177
  @media (pointer: coarse) {
169
178
  .ui-modal__close {
170
179
  align-items: center;
171
180
  display: inline-flex;
172
181
  justify-content: center;
173
- min-block-size: 2.9rem;
174
- min-inline-size: 2.9rem;
182
+ min-block-size: var(--tap-target);
183
+ min-inline-size: var(--tap-target);
175
184
  }
176
185
  }
177
186
 
@@ -187,7 +196,12 @@ html:has(.ui-modal.is-open) {
187
196
  color: var(--text);
188
197
  max-block-size: 100vh;
189
198
  max-inline-size: 100vw;
190
- padding: 0;
199
+
200
+ /* Deliberately full-bleed — the dimmed ground SHOULD cover the whole screen,
201
+ including the cutout. Only the insets keep the carousel's own controls out
202
+ from under it. */
203
+ padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom)
204
+ var(--safe-area-left);
191
205
  inline-size: 100vw;
192
206
  }
193
207
 
@@ -423,6 +423,53 @@
423
423
  padding: 0.85rem 1.4rem;
424
424
  }
425
425
 
426
+ /* Dense — for bars whose HEIGHT is the constraint: a pane/node title bar, a
427
+ packed toolbar, a table row's actions. It lowers only the *visual* floor, to
428
+ the WCAG 2.5.8 minimum, and deliberately declares no coarse-pointer opinion:
429
+ the `@media (pointer: coarse)` block below matches at equal specificity and
430
+ later in the file, so it still floats a dense control to the full
431
+ `--tap-target`. Shrinking a control for a mouse therefore never shrinks it
432
+ for a finger — which is the reason this is a separate tier and not just a
433
+ smaller `--sm`. */
434
+ .ui-button--dense {
435
+ gap: 0.25rem;
436
+ min-block-size: var(--tap-target-min);
437
+ min-inline-size: var(--tap-target-min);
438
+ padding: 0.2rem 0.35rem;
439
+ }
440
+
441
+ /* Keep the square icon well; padding is safe to raise specificity on because
442
+ it is not the tap floor. */
443
+ .ui-button--dense.ui-button--icon {
444
+ padding: 0;
445
+ }
446
+
447
+ /* The label slot. An icon button still needs its words — for the accessible
448
+ name, and for the test/automation selector that reads the button by its
449
+ text — but not the pixels. Wrapping the text in this slot lets ONE markup
450
+ shape serve both: the button keeps a real text node, and `--icon` is what
451
+ decides whether it is painted. Without it, an icon-only button has to choose
452
+ between an empty accessible name and a title-bar-inflating word.
453
+
454
+ The hiding technique matches `.ui-visually-hidden` (base.css) rather than
455
+ `display: none`, which would take the text out of the accessible name too —
456
+ the exact opposite of the point. */
457
+ .ui-button__label {
458
+ min-inline-size: 0;
459
+ overflow: hidden;
460
+ text-overflow: ellipsis;
461
+ white-space: nowrap;
462
+ }
463
+
464
+ .ui-button--icon .ui-button__label {
465
+ clip-path: inset(50%);
466
+ block-size: 1px;
467
+ overflow: hidden;
468
+ position: absolute;
469
+ white-space: nowrap;
470
+ inline-size: 1px;
471
+ }
472
+
426
473
  .ui-button:disabled,
427
474
  .ui-button[aria-disabled='true'] {
428
475
  cursor: not-allowed;
@@ -539,11 +586,11 @@
539
586
  /* Standalone CTA links are tap targets, not inline prose links: on a coarse
540
587
  pointer float them to the WCAG 2.5.8 AA 24px floor (the 2.5.8 inline-link
541
588
  exception doesn't cover a block-level call-to-action, which is what these
542
- are). Buttons already auto-grow to ~44px on coarse pointers. */
589
+ are). Buttons already float to the full `--tap-target` on coarse pointers. */
543
590
  @media (pointer: coarse) {
544
591
  .ui-link--arrow,
545
592
  .ui-link--cta {
546
- min-block-size: max(24px, 1.6rem);
593
+ min-block-size: var(--tap-target-min);
547
594
  }
548
595
  }
549
596
 
@@ -743,16 +790,23 @@
743
790
  }
744
791
  }
745
792
 
746
- /* --- Touch: comfortable hit targets on coarse pointers --- */
793
+ /* --- Touch: comfortable hit targets on coarse pointers.
794
+
795
+ The floor is `--tap-target`, not a bare rem. Bronto's own base sets
796
+ `html { font-size: 0.9375rem }`, so the 2.9rem this family used to carry
797
+ resolved to 43.5px — half a pixel under the 44px WCAG 2.5.5 / platform-HIG
798
+ target, and under any host that shrinks the root further it drifted further
799
+ still. The token clamps in px against the rem so the standard holds however
800
+ the root is re-pointed. --- */
747
801
 
748
802
  @media (pointer: coarse) {
749
803
  .ui-button {
750
- min-block-size: 2.9rem;
751
- min-inline-size: 2.9rem;
804
+ min-block-size: var(--tap-target);
805
+ min-inline-size: var(--tap-target);
752
806
  }
753
807
 
754
808
  .ui-button--icon {
755
- min-inline-size: 2.9rem;
809
+ min-inline-size: var(--tap-target);
756
810
  }
757
811
  }
758
812
 
@@ -774,6 +828,47 @@
774
828
  margin: 0.3rem 0 0;
775
829
  }
776
830
 
831
+ /* Slots. Without them every empty surface in an app re-invents the same three
832
+ parts under a different name, and they drift: one has a glyph, the next has
833
+ two sentences in the same weight, a third is a bare <p>. The shape is always
834
+ mark → one sentence in full ink → a quieter follow-up. */
835
+ .ui-empty-state__glyph {
836
+ display: block;
837
+ font-size: 1.5rem;
838
+ line-height: 1;
839
+ opacity: 0.5;
840
+ }
841
+
842
+ /* The one sentence that says what is missing. Full ink — it is the message,
843
+ not a caption, and the container's --text-dim would bury it. */
844
+ .ui-empty-state__lead {
845
+ color: var(--text);
846
+ margin: 0;
847
+ }
848
+
849
+ .ui-empty-state__hint {
850
+ font-size: var(--text-2xs);
851
+ margin: 0;
852
+ opacity: 0.8;
853
+ }
854
+
855
+ /* An empty state REPORTS absence; an invite OFFERS the next action. That is a
856
+ different job, so it gets a different frame: no dashed box (there is nothing
857
+ to outline — the surface itself is the thing you are being invited into) and
858
+ it centres in whatever block space it is given rather than sitting as a
859
+ card. Use it for a surface the user is meant to fill, and keep the plain
860
+ `.ui-empty-state` for a region that simply has no rows today. */
861
+ .ui-empty-state--invite {
862
+ align-items: center;
863
+ border: 0;
864
+ display: flex;
865
+ flex-direction: column;
866
+ gap: var(--space-2xs);
867
+ justify-content: center;
868
+ min-block-size: 100%;
869
+ padding: var(--space-sm);
870
+ }
871
+
777
872
  /* --- Keyboard key — inline glyph for shortcut hints. Wrap a <kbd>. --- */
778
873
 
779
874
  .ui-kbd {