@ponchia/ui 0.5.0 → 0.6.3

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 (196) hide show
  1. package/CHANGELOG.md +386 -4
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +29 -6
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +350 -77
  7. package/behaviors/carousel.d.ts +28 -0
  8. package/behaviors/carousel.d.ts.map +1 -0
  9. package/behaviors/carousel.js +20 -16
  10. package/behaviors/combobox.d.ts +40 -0
  11. package/behaviors/combobox.d.ts.map +1 -0
  12. package/behaviors/combobox.js +111 -29
  13. package/behaviors/command.d.ts +41 -0
  14. package/behaviors/command.d.ts.map +1 -0
  15. package/behaviors/command.js +27 -15
  16. package/behaviors/connectors.d.ts +17 -0
  17. package/behaviors/connectors.d.ts.map +1 -0
  18. package/behaviors/connectors.js +7 -5
  19. package/behaviors/crosshair.d.ts +42 -0
  20. package/behaviors/crosshair.d.ts.map +1 -0
  21. package/behaviors/crosshair.js +23 -6
  22. package/behaviors/dialog.d.ts +20 -0
  23. package/behaviors/dialog.d.ts.map +1 -0
  24. package/behaviors/dialog.js +6 -2
  25. package/behaviors/disclosure.d.ts +10 -0
  26. package/behaviors/disclosure.d.ts.map +1 -0
  27. package/behaviors/disclosure.js +6 -2
  28. package/behaviors/dismissible.d.ts +10 -0
  29. package/behaviors/dismissible.d.ts.map +1 -0
  30. package/behaviors/dismissible.js +6 -2
  31. package/behaviors/forms.d.ts +27 -0
  32. package/behaviors/forms.d.ts.map +1 -0
  33. package/behaviors/forms.js +54 -13
  34. package/behaviors/glyph.d.ts +14 -0
  35. package/behaviors/glyph.d.ts.map +1 -0
  36. package/behaviors/glyph.js +28 -5
  37. package/behaviors/index.d.ts +31 -237
  38. package/behaviors/index.d.ts.map +1 -0
  39. package/behaviors/index.js +17 -0
  40. package/behaviors/inert.d.ts +20 -0
  41. package/behaviors/inert.d.ts.map +1 -0
  42. package/behaviors/inert.js +46 -0
  43. package/behaviors/internal.d.ts +25 -0
  44. package/behaviors/internal.d.ts.map +1 -0
  45. package/behaviors/internal.js +77 -1
  46. package/behaviors/legend.d.ts +35 -0
  47. package/behaviors/legend.d.ts.map +1 -0
  48. package/behaviors/legend.js +32 -2
  49. package/behaviors/menu.d.ts +16 -0
  50. package/behaviors/menu.d.ts.map +1 -0
  51. package/behaviors/menu.js +6 -2
  52. package/behaviors/modal.d.ts +41 -0
  53. package/behaviors/modal.d.ts.map +1 -0
  54. package/behaviors/modal.js +124 -0
  55. package/behaviors/popover.d.ts +28 -0
  56. package/behaviors/popover.d.ts.map +1 -0
  57. package/behaviors/popover.js +78 -7
  58. package/behaviors/spotlight.d.ts +17 -0
  59. package/behaviors/spotlight.d.ts.map +1 -0
  60. package/behaviors/spotlight.js +7 -5
  61. package/behaviors/table.d.ts +36 -0
  62. package/behaviors/table.d.ts.map +1 -0
  63. package/behaviors/table.js +84 -17
  64. package/behaviors/tabs.d.ts +20 -0
  65. package/behaviors/tabs.d.ts.map +1 -0
  66. package/behaviors/tabs.js +17 -14
  67. package/behaviors/theme.d.ts +54 -0
  68. package/behaviors/theme.d.ts.map +1 -0
  69. package/behaviors/theme.js +22 -3
  70. package/behaviors/toast.d.ts +49 -0
  71. package/behaviors/toast.d.ts.map +1 -0
  72. package/behaviors/toast.js +47 -3
  73. package/classes/classes.json +2527 -0
  74. package/classes/index.d.ts +134 -15
  75. package/classes/index.js +280 -80
  76. package/classes/vscode.css-custom-data.json +12 -0
  77. package/connectors/index.d.ts +201 -69
  78. package/connectors/index.d.ts.map +1 -0
  79. package/connectors/index.js +142 -25
  80. package/css/app.css +69 -13
  81. package/css/base.css +15 -10
  82. package/css/bullet.css +108 -0
  83. package/css/code.css +98 -0
  84. package/css/connectors.css +17 -0
  85. package/css/content.css +22 -3
  86. package/css/crosshair.css +7 -7
  87. package/css/dataviz.css +5 -1
  88. package/css/diff.css +153 -0
  89. package/css/disclosure.css +53 -7
  90. package/css/dots.css +94 -7
  91. package/css/feedback.css +97 -7
  92. package/css/forms.css +113 -4
  93. package/css/legend.css +16 -9
  94. package/css/marks.css +38 -8
  95. package/css/motion.css +98 -53
  96. package/css/navigation.css +7 -0
  97. package/css/overlay.css +90 -3
  98. package/css/primitives.css +158 -13
  99. package/css/report.css +73 -56
  100. package/css/sidenote.css +67 -0
  101. package/css/site.css +16 -2
  102. package/css/sources.css +43 -1
  103. package/css/spark.css +62 -0
  104. package/css/spotlight.css +1 -1
  105. package/css/table.css +9 -2
  106. package/css/term.css +110 -0
  107. package/css/textref.css +63 -0
  108. package/css/toc.css +91 -0
  109. package/css/tokens.css +49 -1
  110. package/css/tree.css +134 -0
  111. package/css/workbench.css +1 -1
  112. package/dist/bronto.css +1 -1
  113. package/dist/css/analytical.css +1 -1
  114. package/dist/css/app.css +1 -1
  115. package/dist/css/base.css +1 -1
  116. package/dist/css/bullet.css +1 -0
  117. package/dist/css/code.css +1 -0
  118. package/dist/css/connectors.css +1 -1
  119. package/dist/css/content.css +1 -1
  120. package/dist/css/crosshair.css +1 -1
  121. package/dist/css/diff.css +1 -0
  122. package/dist/css/disclosure.css +1 -1
  123. package/dist/css/dots.css +1 -1
  124. package/dist/css/feedback.css +1 -1
  125. package/dist/css/forms.css +1 -1
  126. package/dist/css/legend.css +1 -1
  127. package/dist/css/marks.css +1 -1
  128. package/dist/css/motion.css +1 -1
  129. package/dist/css/navigation.css +1 -1
  130. package/dist/css/overlay.css +1 -1
  131. package/dist/css/primitives.css +1 -1
  132. package/dist/css/report.css +1 -1
  133. package/dist/css/sidenote.css +1 -0
  134. package/dist/css/site.css +1 -1
  135. package/dist/css/sources.css +1 -1
  136. package/dist/css/spark.css +1 -0
  137. package/dist/css/spotlight.css +1 -1
  138. package/dist/css/table.css +1 -1
  139. package/dist/css/term.css +1 -0
  140. package/dist/css/textref.css +1 -0
  141. package/dist/css/toc.css +1 -0
  142. package/dist/css/tokens.css +1 -1
  143. package/dist/css/tree.css +1 -0
  144. package/dist/css/workbench.css +1 -1
  145. package/docs/adr/0003-theme-model.md +1 -1
  146. package/docs/annotations.md +133 -14
  147. package/docs/architecture.md +49 -6
  148. package/docs/bullet.md +78 -0
  149. package/docs/code.md +76 -0
  150. package/docs/contrast.md +116 -92
  151. package/docs/d2.md +196 -0
  152. package/docs/diff.md +146 -0
  153. package/docs/legends.md +23 -3
  154. package/docs/marks.md +9 -2
  155. package/docs/mermaid.md +169 -0
  156. package/docs/reference.md +201 -26
  157. package/docs/reporting.md +416 -57
  158. package/docs/sidenote.md +64 -0
  159. package/docs/sources.md +27 -0
  160. package/docs/spark.md +78 -0
  161. package/docs/stability.md +10 -2
  162. package/docs/term.md +81 -0
  163. package/docs/textref.md +78 -0
  164. package/docs/theming.md +44 -5
  165. package/docs/toc.md +83 -0
  166. package/docs/tree.md +74 -0
  167. package/docs/usage.md +354 -16
  168. package/docs/vega.md +244 -0
  169. package/docs/workbench.md +7 -1
  170. package/glyphs/glyphs.js +13 -5
  171. package/llms.txt +285 -14
  172. package/package.json +95 -17
  173. package/qwik/index.d.ts +44 -59
  174. package/qwik/index.d.ts.map +1 -0
  175. package/qwik/index.js +65 -3
  176. package/react/index.d.ts +41 -61
  177. package/react/index.d.ts.map +1 -0
  178. package/react/index.js +63 -3
  179. package/solid/index.d.ts +68 -61
  180. package/solid/index.d.ts.map +1 -0
  181. package/solid/index.js +66 -3
  182. package/tokens/d2.d.ts +38 -0
  183. package/tokens/d2.js +71 -0
  184. package/tokens/d2.json +43 -0
  185. package/tokens/index.d.ts +5 -5
  186. package/tokens/index.js +15 -1
  187. package/tokens/index.json +9 -0
  188. package/tokens/mermaid.d.ts +23 -0
  189. package/tokens/mermaid.js +181 -0
  190. package/tokens/mermaid.json +163 -0
  191. package/tokens/resolved.json +45 -1
  192. package/tokens/skins.js +3 -2
  193. package/tokens/tokens.dtcg.json +26 -0
  194. package/tokens/vega.d.ts +34 -0
  195. package/tokens/vega.js +155 -0
  196. package/tokens/vega.json +179 -0
package/css/forms.css CHANGED
@@ -77,12 +77,99 @@
77
77
  opacity: 0.5;
78
78
  }
79
79
 
80
+ /* Read-only is editable-looking but not editable; give it a distinct, quieter
81
+ cue (muted fill + default cursor) so it doesn't read as a live field. Not
82
+ disabled — value still submits and the field stays focusable/selectable.
83
+ (component audit C28.) */
84
+ .ui-input:read-only:not(:disabled),
85
+ .ui-textarea:read-only:not(:disabled) {
86
+ background: var(--panel-soft);
87
+ cursor: default;
88
+ }
89
+
90
+ /* Disabled affordance parity. The text inputs above style :disabled directly;
91
+ the controls that WRAP a native input (switch/check/segmented) showed no
92
+ disabled cue and their label kept cursor:pointer — a lie. Mirror the cue via
93
+ :has(input:disabled); the native-element controls (range/file) take :disabled
94
+ directly. (a11y/forms review C4.) */
95
+ .ui-range:disabled,
96
+ .ui-file:disabled,
97
+ .ui-switch:has(input:disabled),
98
+ .ui-check:has(input:disabled),
99
+ .ui-segmented__option:has(input:disabled) {
100
+ cursor: not-allowed;
101
+ opacity: 0.5;
102
+ }
103
+
104
+ /* Keep autofilled fields on-theme — the UA's yellow fill otherwise paints over
105
+ the monochrome surface and breaks the contrast story. (forms review C24.) */
106
+ .ui-input:autofill,
107
+ .ui-select:autofill,
108
+ .ui-textarea:autofill,
109
+ .ui-search input:autofill {
110
+ -webkit-text-fill-color: var(--text);
111
+ box-shadow: inset 0 0 0 100rem var(--bg-elevated);
112
+ caret-color: var(--text);
113
+ }
114
+
80
115
  .ui-input[aria-invalid='true'],
81
116
  .ui-select[aria-invalid='true'],
82
117
  .ui-textarea[aria-invalid='true'] {
83
118
  border-color: var(--danger);
84
119
  }
85
120
 
121
+ /* Wrapper controls (switch / check / segmented) hide their native <input>, so
122
+ the `[aria-invalid]` the validator sets on it paints nothing — a sighted,
123
+ non-AT user couldn't see the error (WCAG 1.4.1). Mirror the invalid cue onto
124
+ the visible surface via :has(), the same way the disabled cue is mirrored.
125
+ (component audit C7.) */
126
+ .ui-check:has(input[aria-invalid='true']) input {
127
+ outline: 2px solid var(--danger);
128
+ outline-offset: 1px;
129
+ }
130
+
131
+ .ui-switch:has(input[aria-invalid='true']) .ui-switch__track {
132
+ border-color: var(--danger);
133
+ }
134
+
135
+ .ui-segmented:has(input[aria-invalid='true']) {
136
+ border-color: var(--danger);
137
+ }
138
+
139
+ /* Forced colours flatten `var(--danger)` to a system colour identical to the
140
+ resting border, so the invalid border becomes indistinguishable from a valid
141
+ one and sighted HCM users lose the only error cue (WCAG 1.4.1). The switch got
142
+ a forced-colors block; the error family did not. Re-assert the state on a
143
+ NON-colour channel — a thicker, doubled border — that survives HCM, and prefix
144
+ the error hint with a glyph so the message itself carries the error.
145
+ (component audit C5.) */
146
+ @media (forced-colors: active) {
147
+ .ui-input[aria-invalid='true'],
148
+ .ui-select[aria-invalid='true'],
149
+ .ui-textarea[aria-invalid='true'] {
150
+ border-style: double;
151
+ border-width: 3px;
152
+ }
153
+
154
+ /* Same NON-colour re-assertion for the wrapper controls (C7): a thicker,
155
+ doubled outline/border survives the HCM colour flattening. */
156
+ .ui-check:has(input[aria-invalid='true']) input {
157
+ outline-width: 3px;
158
+ outline-style: double;
159
+ }
160
+
161
+ .ui-switch:has(input[aria-invalid='true']) .ui-switch__track,
162
+ .ui-segmented:has(input[aria-invalid='true']) {
163
+ border-style: double;
164
+ border-width: 3px;
165
+ }
166
+
167
+ .ui-hint--error::before,
168
+ .ui-error-summary__title::before {
169
+ content: '⚠ ';
170
+ }
171
+ }
172
+
86
173
  .ui-hint {
87
174
  color: var(--text-dim);
88
175
  font-size: var(--text-2xs);
@@ -116,14 +203,18 @@
116
203
  border-end-end-radius: var(--radius-md);
117
204
  }
118
205
 
119
- .ui-input-group > .ui-input:focus-visible {
120
- z-index: 1; /* keep the focus ring above the adjacent addon border */
206
+ .ui-input-group > .ui-input:focus-visible,
207
+ .ui-input-group > .ui-select:focus-visible {
208
+ z-index: 1; /* keep the focus ring above the adjacent addon border (select too — audit C29) */
121
209
  }
122
210
 
123
211
  .ui-input-group__addon {
124
212
  align-items: center;
125
213
  background: var(--panel-soft);
126
- border: 1px solid var(--line);
214
+
215
+ /* Match the wrapped control's `--line-strong` border so the prefix/suffix seam
216
+ isn't a fainter cap than the field it abuts. (component audit C33.) */
217
+ border: 1px solid var(--line-strong);
127
218
  color: var(--text-dim);
128
219
  display: flex;
129
220
  font-size: var(--text-sm);
@@ -221,7 +312,10 @@
221
312
 
222
313
  .ui-error-summary__title {
223
314
  color: var(--danger);
224
- font-family: var(--display);
315
+
316
+ /* The must-be-read failure heading uses the legible sans, not the low-legibility
317
+ Doto display face — reserve --display for decorative numerals. (forms C27.) */
318
+ font-family: var(--sans);
225
319
  font-size: var(--text-sm);
226
320
  font-weight: 700;
227
321
  letter-spacing: var(--tracking-wide);
@@ -262,6 +356,14 @@
262
356
  border-color: var(--accent);
263
357
  }
264
358
 
359
+ /* Keyboard focus ring to match every sibling input (which use a 2px ring); the
360
+ border-colour shift alone read markedly fainter on this one control. Keyed to
361
+ :focus-visible on the inner input so it stays keyboard-only. (a11y review C12.) */
362
+ .ui-search:has(input:focus-visible) {
363
+ outline: 2px solid var(--focus-ring);
364
+ outline-offset: 1px;
365
+ }
366
+
265
367
  .ui-search:focus-within::before {
266
368
  background: var(--accent);
267
369
  }
@@ -384,6 +486,13 @@
384
486
  min-block-size: 2.9rem;
385
487
  }
386
488
 
489
+ /* The whole label is the toggle target — float it to the ~44px floor so the
490
+ small track/box isn't a sub-target on touch (WCAG 2.5.8 — C24). */
491
+ .ui-switch,
492
+ .ui-check {
493
+ min-block-size: 2.9rem;
494
+ }
495
+
387
496
  .ui-check input {
388
497
  block-size: 1.15rem;
389
498
  inline-size: 1.15rem;
package/css/legend.css CHANGED
@@ -1,10 +1,10 @@
1
1
  /* ==========================================================================
2
2
  legend — opt-in data keys for charts, reports, and analytical figures.
3
3
 
4
- A standalone, portable successor to the report kit's inline chart legend:
5
- `.ui-legend` reads the Tier-4 `--chart-*` tokens and pairs with any chart
6
- (Bronto's own `.ui-chart` bars, an SVG/canvas figure, or an external chart
7
- engine). Not imported by core.css — import it beside css/dataviz.css.
4
+ A standalone, portable data key: `.ui-legend` reads the Tier-4 `--chart-*`
5
+ tokens and pairs with any chart (a token-themed inline SVG/canvas figure, or
6
+ an external engine like Vega-Lite see docs/vega.md). Not imported by
7
+ core.css — import it beside css/dataviz.css.
8
8
 
9
9
  Bronto paints and positions; it owns no scales, data mapping, or series
10
10
  state. Colour is never the sole channel: a swatch mirrors its chart mark
@@ -90,10 +90,13 @@
90
90
  inline-size: 1.1rem;
91
91
  }
92
92
 
93
- /* Glyph/symbol swatch — fill an `.ui-icon` mask with the series colour. */
93
+ /* Glyph/symbol swatch — fill an `.ui-icon` mask with the series colour. Match the
94
+ __swatch fallback chain exactly (--chart-color → --chart-1 → --accent): without
95
+ the --chart-1 layer, a symbol and a swatch for the SAME series diverge to two
96
+ colours whenever a theme overrides --chart-1. (audit C22.) */
94
97
  .ui-legend__symbol {
95
98
  block-size: 0.95rem;
96
- color: var(--chart-color, var(--accent));
99
+ color: var(--chart-color, var(--chart-1, var(--accent)));
97
100
  flex: 0 0 auto;
98
101
  inline-size: 0.95rem;
99
102
  }
@@ -205,12 +208,16 @@
205
208
 
206
209
  .ui-legend--with-values .ui-legend__value {
207
210
  margin-inline-start: 0;
211
+ text-align: end;
208
212
  }
209
213
 
210
214
  /* Interactive entries are real <button aria-pressed> controls. Bronto styles
211
215
  the control + the inactive state; behaviors/legend.js (optional) flips
212
- aria-pressed and emits an event. The host hides the series and announces it. */
213
- .ui-legend--interactive .ui-legend__item {
216
+ aria-pressed and emits an event. The host hides the series and announces it.
217
+ Scoped to button/role=button so a non-button entry never gets cursor:pointer
218
+ — a pointer-only affordance with no keyboard path is a lie (WCAG 2.1.1 — C11;
219
+ the behavior ignores and warns about non-button entries to match). */
220
+ .ui-legend--interactive .ui-legend__item:is(button, [role='button']) {
214
221
  background: none;
215
222
  border: 0;
216
223
  border-radius: var(--radius-sm);
@@ -222,7 +229,7 @@
222
229
  text-align: start;
223
230
  }
224
231
 
225
- .ui-legend--interactive .ui-legend__item:focus-visible {
232
+ .ui-legend--interactive .ui-legend__item:is(button, [role='button']):focus-visible {
226
233
  outline: 2px solid var(--accent);
227
234
  outline-offset: 2px;
228
235
  }
package/css/marks.css CHANGED
@@ -12,12 +12,20 @@
12
12
  ========================================================================== */
13
13
 
14
14
  .ui-mark {
15
- --mark-color: var(--line-strong);
16
-
15
+ /* No base `--mark-color`: each draw style supplies its own fallback default
16
+ (highlight/box keep the subtle `--line-strong`; the full-opacity
17
+ `--underline`/`--strike` decorations default to the darker `--text-dim` so
18
+ they read on a light surface — C33). A tone modifier SETS `--mark-color`,
19
+ so it still wins everywhere the fallback would otherwise apply. */
20
+
21
+ /* A bare `<mark>` carries the UA `background-color: yellow`; the translucent
22
+ highlight gradient sits on top of it, so without this reset the UA yellow
23
+ bleeds through (an author declaration always beats the UA origin). */
24
+ background-color: transparent;
17
25
  background-image: linear-gradient(
18
26
  90deg,
19
- color-mix(in srgb, var(--mark-color) 30%, transparent),
20
- color-mix(in srgb, var(--mark-color) 30%, transparent)
27
+ color-mix(in srgb, var(--mark-color, var(--line-strong)) 30%, transparent),
28
+ color-mix(in srgb, var(--mark-color, var(--line-strong)) 30%, transparent)
21
29
  );
22
30
  background-repeat: no-repeat;
23
31
  background-size: 100% 100%;
@@ -59,7 +67,7 @@
59
67
  .ui-mark--underline {
60
68
  background: none;
61
69
  padding: 0;
62
- text-decoration-color: var(--mark-color);
70
+ text-decoration-color: var(--mark-color, var(--text-dim));
63
71
  text-decoration-line: underline;
64
72
  text-decoration-thickness: 0.12em;
65
73
  text-underline-offset: 0.18em;
@@ -67,21 +75,24 @@
67
75
 
68
76
  .ui-mark--box {
69
77
  background: none;
70
- border: 1px solid var(--mark-color);
78
+ border: 1px solid var(--mark-color, var(--line-strong));
71
79
  border-radius: var(--radius-sm);
72
80
  }
73
81
 
74
82
  .ui-mark--strike {
75
83
  background: none;
76
84
  padding: 0;
77
- text-decoration-color: var(--mark-color);
85
+ text-decoration-color: var(--mark-color, var(--text-dim));
78
86
  text-decoration-line: line-through;
79
87
  }
80
88
 
81
89
  /* Draw-on highlight sweep. Only the highlight fill animates, and only when
82
90
  motion is welcome — reduced-motion keeps the resting full highlight. */
83
91
  @media (prefers-reduced-motion: no-preference) {
84
- .ui-mark--draw {
92
+ /* The sweep animates `background-size`, i.e. the highlight FILL — so it is
93
+ inert on the no-fill styles (underline/box/strike). Scope it out rather
94
+ than let `--draw` look applied but do nothing. (content review C14.) */
95
+ .ui-mark--draw:not(.ui-mark--underline, .ui-mark--box, .ui-mark--strike) {
85
96
  animation: ui-mark-draw 0.6s var(--ease, ease) both;
86
97
  }
87
98
  }
@@ -120,6 +131,10 @@
120
131
  --mark-color: var(--accent);
121
132
  }
122
133
 
134
+ .ui-bracket-note--success {
135
+ --mark-color: var(--success);
136
+ }
137
+
123
138
  .ui-bracket-note--warning {
124
139
  --mark-color: var(--warning);
125
140
  }
@@ -142,3 +157,18 @@
142
157
  text-decoration-line: underline;
143
158
  }
144
159
  }
160
+
161
+ /* Print: the highlight is a painted background, so force it through the print
162
+ "economy" default that would otherwise drop it. Settle the draw sweep to its
163
+ resting full highlight in case it is mid-animation when the page is printed. */
164
+ @media print {
165
+ .ui-mark {
166
+ -webkit-print-color-adjust: exact;
167
+ background-size: 100% 100%;
168
+ print-color-adjust: exact;
169
+ }
170
+
171
+ .ui-mark--draw {
172
+ animation: none;
173
+ }
174
+ }
package/css/motion.css CHANGED
@@ -1,8 +1,6 @@
1
1
  /* ==========================================================================
2
2
  motion — keyframes + animation utilities
3
3
  Restrained, dot-flavoured. Everything collapses under reduced-motion.
4
- Keyframes from the legacy responsive layer live here so consumers that
5
- import only core.css keep their existing animations.
6
4
  ========================================================================== */
7
5
 
8
6
  @keyframes pulseDot {
@@ -18,45 +16,6 @@
18
16
  }
19
17
  }
20
18
 
21
- @keyframes scan {
22
- 0% {
23
- transform: translateY(-120%);
24
- }
25
-
26
- 100% {
27
- transform: translateY(320%);
28
- }
29
- }
30
-
31
- @keyframes growBar {
32
- to {
33
- transform: scaleX(1);
34
- }
35
- }
36
-
37
- @keyframes drawLine {
38
- from {
39
- opacity: 0;
40
- stroke-dasharray: 0 999;
41
- }
42
-
43
- to {
44
- opacity: 1;
45
- stroke-dasharray: 999 0;
46
- }
47
- }
48
-
49
- @keyframes pulseNode {
50
- 0%,
51
- 100% {
52
- transform: scale(1);
53
- }
54
-
55
- 50% {
56
- transform: scale(1.08);
57
- }
58
- }
59
-
60
19
  @keyframes pulseRing {
61
20
  0% {
62
21
  opacity: 0.7;
@@ -93,6 +52,28 @@
93
52
  }
94
53
  }
95
54
 
55
+ /* Scroll-driven reveal (`.ui-scroll-reveal`). Unlike the time-based uiRise, a
56
+ scroll timeline can FREEZE part-way: an element near the document bottom can't
57
+ scroll far enough to finish its range, so a fade-to-opacity-1 would strand the
58
+ content permanently transparent. Reach full opacity early (35%) and hold it,
59
+ so even a partially-driven reveal is fully legible — only the last few px of
60
+ the rise are left unfinished. (component audit C9.) */
61
+ @keyframes uiScrollReveal {
62
+ 0% {
63
+ opacity: 0;
64
+ transform: translateY(10px);
65
+ }
66
+
67
+ 35% {
68
+ opacity: 1;
69
+ }
70
+
71
+ 100% {
72
+ opacity: 1;
73
+ transform: translateY(0);
74
+ }
75
+ }
76
+
96
77
  @keyframes uiDotIn {
97
78
  0% {
98
79
  opacity: 0;
@@ -199,10 +180,13 @@
199
180
  animation: uiMatrixReveal var(--duration-slow) var(--ease-out) both;
200
181
  }
201
182
 
202
- /* Stagger children: set --i on each child (or use nth-child cap). */
183
+ /* Stagger children: set --i on each child (or use nth-child cap). Cap the manual
184
+ --i path at index 6 (360ms) to match the .ui-stagger--auto ceiling — a long
185
+ list with --i:30 would otherwise hold the last child at opacity:0 for 1.8s
186
+ before popping in. (audit C32.) */
203
187
  .ui-stagger > * {
204
188
  animation: uiRise var(--duration-slow) var(--ease-spring) both;
205
- animation-delay: calc(var(--i, 0) * 60ms);
189
+ animation-delay: calc(min(var(--i, 0), 6) * 60ms);
206
190
  }
207
191
 
208
192
  .ui-stagger--auto > *:nth-child(1) {
@@ -233,9 +217,19 @@
233
217
  animation-delay: 360ms;
234
218
  }
235
219
 
236
- /* Reveal-on-scroll: add .ui-reveal, toggle .is-visible via IntersectionObserver.
237
- Degrades to visible with no JS. */
238
- @media (prefers-reduced-motion: no-preference) {
220
+ /* Reveal-on-scroll: add .ui-reveal, then toggle .is-visible (e.g. from an
221
+ IntersectionObserver you own) to play it in.
222
+
223
+ ⚠ `.ui-reveal` requires you to wire that observer. The hidden initial state is
224
+ gated only on `scripting: enabled` — it can't detect whether an observer is
225
+ actually attached, so merely loading bronto.js (or any script) hides every
226
+ `.ui-reveal` until YOUR code adds `.is-visible`. If you add the class and never
227
+ wire the toggle, the content stays invisible. For an LLM-authored or no-build
228
+ report, DON'T use `.ui-reveal` — use `.ui-scroll-reveal` below (scroll-driven,
229
+ CSS-only, no observer, and it can never strand content invisible). With
230
+ scripting OFF, `.ui-reveal` content is fully visible and never silently hidden
231
+ behind a script that will never run. */
232
+ @media (prefers-reduced-motion: no-preference) and (scripting: enabled) {
239
233
  .ui-reveal {
240
234
  opacity: 0;
241
235
  transform: translateY(14px);
@@ -283,10 +277,12 @@
283
277
 
284
278
  /* --- Scroll-driven (progressive enhancement) — the scroll/view timeline
285
279
  IS the engine, no JS. Everything is gated on `@supports
286
- (animation-timeline: …)` so engines without it (today, Firefox/Safari)
287
- keep the static end state, and on `prefers-reduced-motion: no-preference`
288
- (a scroll timeline ignores animation-duration, so the global reduced-motion
289
- reset below can't neutralise it — it must be gated here). --- */
280
+ (animation-timeline: …)` so an engine without it keeps the static end state
281
+ (as of 2026 Chrome 115+ and Safari 18.4+ drive scroll()/view(); Firefox is
282
+ the remaining holdout, still graceful via the @supports gate), and on
283
+ `prefers-reduced-motion: no-preference` (a scroll timeline ignores
284
+ animation-duration, so the global reduced-motion reset below can't neutralise
285
+ it — it must be gated here). --- */
290
286
 
291
287
  /* Reading-progress bar. Fixed hairline that fills with document scroll;
292
288
  unsupported → a static, empty (scaleX(0)) bar. Pair with role="progressbar"
@@ -307,6 +303,30 @@
307
303
  transform-origin: 100% 50%;
308
304
  }
309
305
 
306
+ /* Forced colours (HCM) flatten the spinner's accent top-border and its --line
307
+ rest to one system colour, so the ring looks uniform with no visible sweep,
308
+ and the scroll-progress bar can vanish into the canvas. Re-assert distinct
309
+ system colours so each keeps a visible channel; AT is already covered via
310
+ aria-busy / role="status" / role="progressbar". (component audit C35.) */
311
+ @media (forced-colors: active) {
312
+ .ui-spinner {
313
+ border-color: CanvasText;
314
+ border-block-start-color: Highlight;
315
+ }
316
+
317
+ .ui-scroll-progress {
318
+ background: Highlight;
319
+ }
320
+
321
+ /* HCM strips the shimmer gradient, leaving an invisible box where a loading
322
+ placeholder should be. Give it a system-colour border so the skeleton
323
+ still reads as present. Same decorative-loading family as the spinner +
324
+ scroll-progress above. (audit C15.) */
325
+ .ui-skeleton {
326
+ border: 1px solid CanvasText;
327
+ }
328
+ }
329
+
310
330
  @supports (animation-timeline: scroll()) {
311
331
  @media (prefers-reduced-motion: no-preference) {
312
332
  .ui-scroll-progress {
@@ -321,13 +341,21 @@
321
341
 
322
342
  /* Reveal-on-scroll with no JS and no IntersectionObserver: the element
323
343
  rises + fades as it scrolls into view. Unsupported → fully visible
324
- (the `uiRise` end state), same graceful default as `.ui-reveal`. */
344
+ (the keyframe end state), same graceful default as `.ui-reveal`.
345
+
346
+ The range is `entry 0% entry 100%`, NOT the old `cover 40%`: `cover` only
347
+ completes once the element has scrolled most of the way THROUGH the viewport,
348
+ which an element near the document bottom can never do — it froze part-way,
349
+ leaving a conclusion section permanently semi-transparent (C9). `entry`
350
+ completes the moment the element is fully in view, which any scroll-to-bottom
351
+ reaches. Paired with the early-opacity uiScrollReveal keyframe, an element
352
+ taller than the viewport is legible even if its range never fully completes. */
325
353
  @supports (animation-timeline: view()) {
326
354
  @media (prefers-reduced-motion: no-preference) {
327
355
  .ui-scroll-reveal {
328
- animation: uiRise linear both;
356
+ animation: uiScrollReveal linear both;
329
357
  animation-timeline: view();
330
- animation-range: entry 0% cover 40%;
358
+ animation-range: entry 0% entry 100%;
331
359
  }
332
360
  }
333
361
  }
@@ -365,14 +393,31 @@
365
393
  scroll-behavior: auto;
366
394
  }
367
395
 
396
+ /* !important is required: inside @layer bronto, layered rules lose to
397
+ unlayered author rules at equal specificity — only a layered !important
398
+ stays authoritative over unlayered declarations (CSS cascade §6.2). */
368
399
  *,
369
400
  *::before,
370
401
  *::after {
371
402
  animation-duration: 0.01ms !important;
372
403
  animation-iteration-count: 1 !important;
404
+
405
+ /* Zero the delay too: `.ui-stagger` children animate `uiRise` with
406
+ `fill-mode: both`, so a non-zero `animation-delay` holds them at the
407
+ `opacity: 0` from-state for the full delay and then pops them in — the
408
+ exact late flash a reduced-motion user asked to avoid (C4). */
409
+ animation-delay: 0s !important;
373
410
  transition-duration: 0.01ms !important;
374
411
  }
375
412
 
413
+ /* Freeze the shimmer gradient mid-sweep would expose a diagonal band.
414
+ Flatten the skeleton to its base panel colour instead. */
415
+ .ui-skeleton {
416
+ animation: none;
417
+ background: var(--panel-soft);
418
+ background-size: auto;
419
+ }
420
+
376
421
  .ui-reveal {
377
422
  opacity: 1 !important;
378
423
  transform: none !important;
@@ -80,3 +80,10 @@
80
80
  transform: translateY(1px);
81
81
  }
82
82
  }
83
+
84
+ @media (pointer: coarse) {
85
+ .ui-themetoggle__button {
86
+ min-block-size: 2.9rem;
87
+ padding-inline: 0.9rem;
88
+ }
89
+ }
package/css/overlay.css CHANGED
@@ -78,14 +78,25 @@ dialog.ui-modal[open]::backdrop {
78
78
 
79
79
  /* Controlled (non-<dialog>) usage. A portal/React modal that can't be a
80
80
  native <dialog> wears the same skin + open layout via `.is-open`.
81
- Backdrop, top-layer stacking and focus-trapping are then the
82
- consumer's responsibility (the native <dialog> path gets them free). */
81
+ Backdrop and top-layer stacking are then the consumer's responsibility
82
+ (the native <dialog> path gets them free); focus-trapping no longer is —
83
+ mark the overlay `data-bronto-modal` and run `initModal` for an
84
+ inert-based trap + focus-return + Escape signal. */
83
85
  .ui-modal.is-open {
84
86
  animation: uiToastIn var(--duration-base) var(--ease-spring) both;
85
87
  display: grid;
86
88
  grid-template-rows: auto 1fr auto;
87
89
  }
88
90
 
91
+ /* Background scroll-lock. A native <dialog> top layer does NOT stop the page
92
+ behind it scrolling, and the no-JS native path can't lock it in script — so
93
+ lock it in CSS, for both the native [open] dialog and the controlled .is-open
94
+ overlay (incl. the drawer modifier, same base). (component audit C13.) */
95
+ html:has(dialog.ui-modal[open]),
96
+ html:has(.ui-modal.is-open) {
97
+ overflow: hidden;
98
+ }
99
+
89
100
  .ui-modal__head {
90
101
  align-items: flex-start;
91
102
  border-block-end: 1px solid var(--line);
@@ -150,6 +161,21 @@ dialog.ui-modal[open]::backdrop {
150
161
  }
151
162
  }
152
163
 
164
+ /* 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
167
+ already clears 24×24, is unchanged; centre the glyph in the enlarged box.
168
+ (audit C3.) */
169
+ @media (pointer: coarse) {
170
+ .ui-modal__close {
171
+ align-items: center;
172
+ display: inline-flex;
173
+ justify-content: center;
174
+ min-block-size: 2.9rem;
175
+ min-inline-size: 2.9rem;
176
+ }
177
+ }
178
+
153
179
  /* --- Lightbox — a full-viewport <dialog> wrapping a .ui-carousel. The
154
180
  native <dialog> brings the top layer, focus-trap and Escape; initDialog
155
181
  adds open (`data-bronto-open`) + focus-return; initCarousel drives the
@@ -249,7 +275,7 @@ dialog.ui-modal[open]::backdrop {
249
275
  }
250
276
 
251
277
  .ui-menu__item::before {
252
- background: currentColor;
278
+ background: currentcolor;
253
279
  border-radius: 50%;
254
280
  content: '';
255
281
  block-size: 0.3rem;
@@ -275,6 +301,34 @@ dialog.ui-modal[open]::backdrop {
275
301
  }
276
302
  }
277
303
 
304
+ .ui-menu__item:focus-visible {
305
+ background: var(--bg-accent);
306
+ color: var(--text);
307
+ }
308
+
309
+ .ui-menu__item:focus-visible::before {
310
+ opacity: 1;
311
+ }
312
+
313
+ /* Menu hover/focus paints var(--bg-accent), which HCM strips, so the hovered row
314
+ is indistinguishable from its siblings (focus keeps the UA ring; hover has no
315
+ such fallback). Re-assert a system Highlight like the combobox/command rows,
316
+ for parity with those sibling surfaces. (component audit C22.) */
317
+ @media (forced-colors: active) {
318
+ .ui-menu__item:hover,
319
+ .ui-menu__item:focus-visible {
320
+ forced-color-adjust: none;
321
+ background: Highlight;
322
+ color: HighlightText;
323
+ }
324
+
325
+ .ui-menu__item:hover::before,
326
+ .ui-menu__item:focus-visible::before {
327
+ background: HighlightText;
328
+ opacity: 1;
329
+ }
330
+ }
331
+
278
332
  /* --- Combobox: an input with a filtered listbox popup (APG pattern,
279
333
  wired by initCombobox). Reuses the menu surface tokens. --- */
280
334
  .ui-combobox {
@@ -328,6 +382,18 @@ dialog.ui-modal[open]::backdrop {
328
382
  color: var(--text);
329
383
  }
330
384
 
385
+ /* The active option is tracked by aria-activedescendant — it is NEVER DOM-focused,
386
+ so it gets no UA focus ring under HCM, and its only cue (--bg-accent) is stripped.
387
+ Re-assert a system Highlight like the command palette does. (audit C2.) */
388
+ @media (forced-colors: active) {
389
+ .ui-combobox__option.is-active,
390
+ .ui-combobox__option[aria-selected='true'] {
391
+ forced-color-adjust: none;
392
+ background: Highlight;
393
+ color: HighlightText;
394
+ }
395
+ }
396
+
331
397
  .ui-combobox__empty {
332
398
  color: var(--text-dim);
333
399
  font-size: var(--text-2xs);
@@ -363,6 +429,14 @@ dialog.ui-modal[open]::backdrop {
363
429
  inset-inline: 0;
364
430
  min-inline-size: 0;
365
431
  }
432
+
433
+ /* Let the combobox shed its 14rem floor and span the viewport so the input
434
+ (and its full-width listbox) can't overflow a narrow screen. */
435
+ .ui-combobox {
436
+ display: block;
437
+ inline-size: 100%;
438
+ min-inline-size: 0;
439
+ }
366
440
  }
367
441
 
368
442
  @media (prefers-reduced-motion: reduce) {
@@ -376,3 +450,16 @@ dialog.ui-modal[open]::backdrop {
376
450
  transition: none;
377
451
  }
378
452
  }
453
+
454
+ /* Forced-colors drops both `backdrop-filter: blur()` and the `color-mix()`
455
+ scrim, so the dialog/lightbox would float over an undimmed page with no
456
+ separation. Re-assert a translucent scrim with `forced-color-adjust: none`
457
+ so the layering reads in High Contrast, matching the meter/dot precedent
458
+ (C28). */
459
+ @media (forced-colors: active) {
460
+ .ui-modal::backdrop,
461
+ .ui-lightbox::backdrop {
462
+ background: color-mix(in srgb, #000 50%, transparent);
463
+ forced-color-adjust: none;
464
+ }
465
+ }