@ponchia/ui 0.6.0 → 0.6.4

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 (162) hide show
  1. package/CHANGELOG.md +82 -4
  2. package/README.md +1 -1
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +36 -33
  5. package/behaviors/carousel.d.ts +28 -0
  6. package/behaviors/carousel.d.ts.map +1 -0
  7. package/behaviors/carousel.js +3 -0
  8. package/behaviors/combobox.d.ts +40 -0
  9. package/behaviors/combobox.d.ts.map +1 -0
  10. package/behaviors/combobox.js +71 -20
  11. package/behaviors/command.d.ts +41 -0
  12. package/behaviors/command.d.ts.map +1 -0
  13. package/behaviors/command.js +9 -0
  14. package/behaviors/connectors.d.ts +17 -0
  15. package/behaviors/connectors.d.ts.map +1 -0
  16. package/behaviors/connectors.js +3 -0
  17. package/behaviors/crosshair.d.ts +42 -0
  18. package/behaviors/crosshair.d.ts.map +1 -0
  19. package/behaviors/crosshair.js +19 -1
  20. package/behaviors/dialog.d.ts +20 -0
  21. package/behaviors/dialog.d.ts.map +1 -0
  22. package/behaviors/dialog.js +3 -0
  23. package/behaviors/disclosure.d.ts +10 -0
  24. package/behaviors/disclosure.d.ts.map +1 -0
  25. package/behaviors/disclosure.js +3 -0
  26. package/behaviors/dismissible.d.ts +10 -0
  27. package/behaviors/dismissible.d.ts.map +1 -0
  28. package/behaviors/dismissible.js +3 -0
  29. package/behaviors/forms.d.ts +27 -0
  30. package/behaviors/forms.d.ts.map +1 -0
  31. package/behaviors/forms.js +18 -5
  32. package/behaviors/glyph.d.ts +21 -0
  33. package/behaviors/glyph.d.ts.map +1 -0
  34. package/behaviors/glyph.js +82 -4
  35. package/behaviors/index.d.ts +31 -237
  36. package/behaviors/index.d.ts.map +1 -0
  37. package/behaviors/index.js +17 -0
  38. package/behaviors/inert.d.ts +20 -0
  39. package/behaviors/inert.d.ts.map +1 -0
  40. package/behaviors/inert.js +46 -0
  41. package/behaviors/internal.d.ts +25 -0
  42. package/behaviors/internal.d.ts.map +1 -0
  43. package/behaviors/internal.js +30 -1
  44. package/behaviors/legend.d.ts +35 -0
  45. package/behaviors/legend.d.ts.map +1 -0
  46. package/behaviors/legend.js +9 -0
  47. package/behaviors/menu.d.ts +16 -0
  48. package/behaviors/menu.d.ts.map +1 -0
  49. package/behaviors/menu.js +3 -0
  50. package/behaviors/modal.d.ts +41 -0
  51. package/behaviors/modal.d.ts.map +1 -0
  52. package/behaviors/modal.js +124 -0
  53. package/behaviors/popover.d.ts +28 -0
  54. package/behaviors/popover.d.ts.map +1 -0
  55. package/behaviors/popover.js +17 -17
  56. package/behaviors/spotlight.d.ts +17 -0
  57. package/behaviors/spotlight.d.ts.map +1 -0
  58. package/behaviors/spotlight.js +3 -0
  59. package/behaviors/table.d.ts +36 -0
  60. package/behaviors/table.d.ts.map +1 -0
  61. package/behaviors/table.js +48 -8
  62. package/behaviors/tabs.d.ts +20 -0
  63. package/behaviors/tabs.d.ts.map +1 -0
  64. package/behaviors/tabs.js +3 -0
  65. package/behaviors/theme.d.ts +54 -0
  66. package/behaviors/theme.d.ts.map +1 -0
  67. package/behaviors/theme.js +17 -0
  68. package/behaviors/toast.d.ts +49 -0
  69. package/behaviors/toast.d.ts.map +1 -0
  70. package/behaviors/toast.js +34 -2
  71. package/classes/classes.json +747 -15
  72. package/classes/index.d.ts +118 -3
  73. package/classes/index.js +264 -66
  74. package/connectors/index.d.ts +12 -0
  75. package/connectors/index.d.ts.map +1 -1
  76. package/connectors/index.js +23 -2
  77. package/css/app.css +26 -0
  78. package/css/bullet.css +108 -0
  79. package/css/code.css +98 -0
  80. package/css/content.css +15 -2
  81. package/css/crosshair.css +7 -7
  82. package/css/diff.css +153 -0
  83. package/css/disclosure.css +18 -4
  84. package/css/dots.css +246 -9
  85. package/css/feedback.css +39 -7
  86. package/css/forms.css +71 -3
  87. package/css/legend.css +5 -2
  88. package/css/motion.css +79 -14
  89. package/css/overlay.css +59 -2
  90. package/css/primitives.css +67 -8
  91. package/css/report.css +43 -4
  92. package/css/sidenote.css +67 -0
  93. package/css/skins.css +9 -0
  94. package/css/spark.css +76 -0
  95. package/css/table.css +16 -3
  96. package/css/term.css +110 -0
  97. package/css/textref.css +63 -0
  98. package/css/toc.css +91 -0
  99. package/css/tokens.css +14 -1
  100. package/css/tree.css +134 -0
  101. package/dist/bronto.css +1 -1
  102. package/dist/css/analytical.css +1 -1
  103. package/dist/css/app.css +1 -1
  104. package/dist/css/bullet.css +1 -0
  105. package/dist/css/code.css +1 -0
  106. package/dist/css/content.css +1 -1
  107. package/dist/css/crosshair.css +1 -1
  108. package/dist/css/diff.css +1 -0
  109. package/dist/css/disclosure.css +1 -1
  110. package/dist/css/dots.css +1 -1
  111. package/dist/css/feedback.css +1 -1
  112. package/dist/css/forms.css +1 -1
  113. package/dist/css/legend.css +1 -1
  114. package/dist/css/motion.css +1 -1
  115. package/dist/css/overlay.css +1 -1
  116. package/dist/css/primitives.css +1 -1
  117. package/dist/css/report.css +1 -1
  118. package/dist/css/sidenote.css +1 -0
  119. package/dist/css/skins.css +1 -1
  120. package/dist/css/spark.css +1 -0
  121. package/dist/css/table.css +1 -1
  122. package/dist/css/term.css +1 -0
  123. package/dist/css/textref.css +1 -0
  124. package/dist/css/toc.css +1 -0
  125. package/dist/css/tokens.css +1 -1
  126. package/dist/css/tree.css +1 -0
  127. package/docs/annotations.md +39 -0
  128. package/docs/architecture.md +2 -3
  129. package/docs/bullet.md +78 -0
  130. package/docs/code.md +76 -0
  131. package/docs/d2.md +4 -3
  132. package/docs/diff.md +146 -0
  133. package/docs/dots.md +146 -0
  134. package/docs/glyphs.md +114 -0
  135. package/docs/legends.md +8 -4
  136. package/docs/mermaid.md +21 -4
  137. package/docs/reference.md +168 -8
  138. package/docs/reporting.md +49 -17
  139. package/docs/sidenote.md +64 -0
  140. package/docs/spark.md +78 -0
  141. package/docs/stability.md +1 -0
  142. package/docs/term.md +81 -0
  143. package/docs/textref.md +78 -0
  144. package/docs/theming.md +44 -5
  145. package/docs/toc.md +83 -0
  146. package/docs/tree.md +74 -0
  147. package/docs/usage.md +264 -23
  148. package/docs/vega.md +22 -3
  149. package/glyphs/glyphs.d.ts +61 -0
  150. package/glyphs/glyphs.js +600 -31
  151. package/llms.txt +169 -15
  152. package/package.json +51 -7
  153. package/qwik/index.d.ts +4 -2
  154. package/qwik/index.d.ts.map +1 -1
  155. package/qwik/index.js +10 -0
  156. package/react/index.d.ts +4 -2
  157. package/react/index.d.ts.map +1 -1
  158. package/react/index.js +6 -0
  159. package/solid/index.d.ts +6 -2
  160. package/solid/index.d.ts.map +1 -1
  161. package/solid/index.js +6 -0
  162. package/tokens/skins.js +22 -9
package/css/dots.css CHANGED
@@ -348,14 +348,191 @@
348
348
  animation-delay: 0.63s;
349
349
  }
350
350
 
351
- /* Matrix-reveal wrapper — content wipes in left→right on .is-in. */
352
- .ui-matrix {
353
- clip-path: inset(0 100% 0 0);
354
- transition: clip-path var(--duration-slow) var(--ease-out);
351
+ /* ==========================================================================
352
+ Data-bound dot surfaces — the reporting/dashboard family. Each is a thin,
353
+ token-driven leaf over the same lit/dim/accent dot vocabulary: the HOST
354
+ normalises the data (lights `is-on`, sets `data-level`, or writes `--v`
355
+ 0..1) and the leaf only lays out + tones. None compute a scale, bin, or
356
+ threshold — that stays with the host (the spark/meter boundary).
357
+
358
+ a11y: every one is opaque to assistive tech, so the container MUST carry a
359
+ host-written `role="img"` + `aria-label` with the exact value/meaning
360
+ (rounding to whole cells is presentation-only; keep the figure in the label).
361
+ ========================================================================== */
362
+
363
+ /* Waffle / unit chart — part-to-whole as an N×N field of lit dots ("73 of
364
+ 100"). Host marks the lit cells with `is-on`. */
365
+ .ui-waffle {
366
+ display: grid;
367
+ gap: var(--waffle-gap, 0.18em);
368
+ grid-template-columns: repeat(var(--waffle-cols, 10), 1fr);
369
+ inline-size: var(--waffle-size, 7em);
370
+ }
371
+
372
+ .ui-waffle i {
373
+ aspect-ratio: 1;
374
+ background: var(--field-dot);
375
+ border-radius: var(--dotmatrix-dot-radius, 50%);
376
+ }
377
+
378
+ .ui-waffle i.is-on {
379
+ background: var(--field-dot-accent);
380
+ box-shadow: 0 0 var(--dotmatrix-glow, 0) var(--field-dot-accent);
381
+ }
382
+
383
+ /* Activity / contribution grid — density-over-time. A flat list of day cells
384
+ flows down each weekday column via grid-auto-flow; intensity is a 5-step
385
+ ramp on `data-level="0..4"` (the host bins data → level). */
386
+ .ui-activity {
387
+ display: grid;
388
+ gap: var(--activity-gap, 0.18em);
389
+ grid-auto-columns: var(--activity-cell, 0.82em);
390
+ grid-auto-flow: column;
391
+ grid-template-rows: repeat(var(--activity-rows, 7), 1fr);
392
+ }
393
+
394
+ .ui-activity i {
395
+ aspect-ratio: 1;
396
+ background: var(--field-dot);
397
+ border-radius: var(--dotmatrix-dot-radius, 2px);
398
+ }
399
+
400
+ .ui-activity i[data-level='1'] {
401
+ background: color-mix(in oklab, var(--field-dot-accent) 30%, var(--field-dot));
402
+ }
403
+
404
+ .ui-activity i[data-level='2'] {
405
+ background: color-mix(in oklab, var(--field-dot-accent) 55%, var(--field-dot));
406
+ }
407
+
408
+ .ui-activity i[data-level='3'] {
409
+ background: color-mix(in oklab, var(--field-dot-accent) 78%, var(--field-dot));
410
+ }
411
+
412
+ .ui-activity i[data-level='4'] {
413
+ background: var(--field-dot-accent);
414
+ }
415
+
416
+ /* LED level meter — a column of discrete segments lit to a threshold (signal /
417
+ load / VU). Fills from the bottom; host marks lit segments with `is-on`.
418
+ `--warn`/`--danger` re-point the lit colour for the whole meter when the host
419
+ crosses a threshold (the host owns the threshold, not the leaf). */
420
+ .ui-level {
421
+ block-size: var(--level-height, 4em);
422
+ display: flex;
423
+ flex-direction: column-reverse;
424
+ gap: var(--level-gap, 2px);
425
+ inline-size: var(--level-size, 0.7em);
426
+ }
427
+
428
+ .ui-level i {
429
+ background: var(--field-dot);
430
+ border-radius: var(--radius-sm);
431
+ flex: 1;
432
+ }
433
+
434
+ .ui-level i.is-on {
435
+ background: var(--accent);
436
+ box-shadow: 0 0 var(--dotmatrix-glow, 0) var(--accent);
437
+ }
438
+
439
+ .ui-level--warn i.is-on {
440
+ background: var(--warning);
441
+ }
442
+
443
+ .ui-level--danger i.is-on {
444
+ background: var(--danger);
445
+ }
446
+
447
+ /* Dot gauge — a 0..1 reading (`--v`) as a ring of dots filling along an arc.
448
+ A conic-gradient sweep (accent up to --v, dim beyond) intersected with a
449
+ donut-ring × dot-pattern mask, so the lit arc reads as discrete dots. */
450
+ .ui-dotgauge {
451
+ --_v: var(--v, 0);
452
+ --_sweep: var(--gauge-sweep, 270deg);
453
+
454
+ aspect-ratio: 1;
455
+ background: conic-gradient(
456
+ from var(--gauge-from, 135deg),
457
+ var(--field-dot-accent) calc(var(--_v) * var(--_sweep)),
458
+ var(--field-dot) calc(var(--_v) * var(--_sweep)) var(--_sweep),
459
+ transparent var(--_sweep)
460
+ );
461
+ border-radius: 50%;
462
+ inline-size: var(--gauge-size, 5em);
463
+
464
+ /* ring (donut) ∩ dot lattice → a ring of dots */
465
+ /* stylelint-disable property-no-vendor-prefix -- Safari still needs the prefixed mask props. */
466
+ -webkit-mask:
467
+ radial-gradient(closest-side, transparent 58%, #000 59%),
468
+ radial-gradient(circle, #000 34%, transparent 36%) 0 0 / var(--gauge-dot, 0.5em) var(--gauge-dot, 0.5em);
469
+ -webkit-mask-composite: source-in;
470
+ /* stylelint-enable property-no-vendor-prefix */
471
+ mask:
472
+ radial-gradient(closest-side, transparent 58%, #000 59%),
473
+ radial-gradient(circle, #000 34%, transparent 36%) 0 0 / var(--gauge-dot, 0.5em) var(--gauge-dot, 0.5em);
474
+ mask-composite: intersect;
475
+ }
476
+
477
+ /* Readout — a row of dot-matrix glyphs forming a big numeric (renderReadout).
478
+ The glyphs are decorative; the row carries role=img + the value as its name. */
479
+ .ui-readout {
480
+ align-items: flex-end;
481
+ display: inline-flex;
482
+ gap: var(--readout-gap, 0.12em);
355
483
  }
356
484
 
357
- .ui-matrix.is-in {
358
- clip-path: inset(0 0 0 0);
485
+ .ui-readout__spacer {
486
+ display: inline-block;
487
+ inline-size: var(--readout-space, 0.5em);
488
+ }
489
+
490
+ /* Halftone — render host content (an <img> or a box with its own background)
491
+ through a dot lattice so a thumbnail/cover takes on the dot look. A style
492
+ filter, NOT a data viz: the dots are a fixed lattice, not value-modulated. */
493
+ .ui-halftone {
494
+ /* stylelint-disable-next-line property-no-vendor-prefix -- Safari still needs the prefixed mask property. */
495
+ -webkit-mask: radial-gradient(circle, #000 var(--halftone-dot, 38%), transparent calc(var(--halftone-dot, 38%) + 1%)) 0 0 /
496
+ var(--halftone-gap, 0.5em) var(--halftone-gap, 0.5em);
497
+ mask: radial-gradient(circle, #000 var(--halftone-dot, 38%), transparent calc(var(--halftone-dot, 38%) + 1%)) 0 0 /
498
+ var(--halftone-gap, 0.5em) var(--halftone-gap, 0.5em);
499
+ }
500
+
501
+ /* Density wrapper — make any dot surface inside respond to the CARD it sits in,
502
+ not the viewport, so the same component reads well in a wide hero and a
503
+ narrow tile. Opt-in: wrap the surface and the breakpoint below densifies it. */
504
+ .ui-dotfit {
505
+ container: dotfit / inline-size;
506
+ }
507
+
508
+ @container dotfit (width < 18rem) {
509
+ .ui-dotfit .ui-dotgrid {
510
+ --dot-gap: 8px;
511
+ }
512
+
513
+ .ui-dotfit .ui-activity {
514
+ --activity-cell: 0.6em;
515
+ }
516
+
517
+ .ui-dotfit .ui-waffle {
518
+ --waffle-size: 5em;
519
+ }
520
+ }
521
+
522
+ /* Matrix-reveal wrapper — content wipes in left→right on .is-in. Gate the
523
+ clipped from-state on `scripting: enabled`: with JS off, `.is-in` is never
524
+ toggled, so without the gate the content stays permanently clipped away and
525
+ invisible to every no-JS/static/print reader. Same graceful default as
526
+ `.ui-reveal`. (component audit C12.) */
527
+ @media (scripting: enabled) {
528
+ .ui-matrix {
529
+ clip-path: inset(0 100% 0 0);
530
+ transition: clip-path var(--duration-slow) var(--ease-out);
531
+ }
532
+
533
+ .ui-matrix.is-in {
534
+ clip-path: inset(0 0 0 0);
535
+ }
359
536
  }
360
537
 
361
538
  @media (prefers-reduced-motion: reduce) {
@@ -404,6 +581,30 @@
404
581
  background: ButtonText;
405
582
  }
406
583
 
584
+ /* Brand/live dots aren't status tones, but they still encode meaning via
585
+ background-color alone, which HCM flattens. Keep them on a distinct,
586
+ opted-out system colour for completeness. (audit C31.) */
587
+ .ui-dot--accent,
588
+ .ui-dot--live {
589
+ forced-color-adjust: none;
590
+ background: LinkText;
591
+ }
592
+
593
+ .ui-dot--live::after {
594
+ border-color: LinkText;
595
+ }
596
+
597
+ /* The masked one-node icon paints `background: currentcolor` through an SVG
598
+ mask. Under HCM, forced-color-adjust:auto can drop the mask fill so the
599
+ glyph vanishes (white-on-white) — yet .ui-icon is the recommended
600
+ icon-at-scale path AND backs .ui-legend__symbol, and the print block
601
+ already special-cases it. Opt out and pin the fill to the system text
602
+ colour so the glyph stays visible. (audit C1.) */
603
+ .ui-icon {
604
+ forced-color-adjust: none;
605
+ background: CanvasText;
606
+ }
607
+
407
608
  .ui-dotmatrix__cell--hot {
408
609
  forced-color-adjust: none;
409
610
  background: Highlight;
@@ -413,16 +614,52 @@
413
614
  forced-color-adjust: none;
414
615
  background: LinkText;
415
616
  }
617
+
618
+ /* The data-bound dot surfaces encode their value via background-color, which
619
+ HCM flattens. Opt out and pin the lit state to a system colour so the
620
+ reading survives. The activity ramp's 5 steps can't all stay distinct under
621
+ HCM — collapse to present (CanvasText) vs absent (the unstyled track). */
622
+ .ui-waffle i.is-on,
623
+ .ui-level i.is-on {
624
+ forced-color-adjust: none;
625
+ background: LinkText;
626
+ }
627
+
628
+ .ui-level--warn i.is-on {
629
+ background: Mark;
630
+ }
631
+
632
+ .ui-level--danger i.is-on {
633
+ background: Highlight;
634
+ }
635
+
636
+ .ui-activity i[data-level='1'],
637
+ .ui-activity i[data-level='2'],
638
+ .ui-activity i[data-level='3'],
639
+ .ui-activity i[data-level='4'] {
640
+ forced-color-adjust: none;
641
+ background: CanvasText;
642
+ }
643
+
644
+ /* The gauge is a masked gradient; keep its paint rather than let HCM drop it. */
645
+ .ui-dotgauge {
646
+ forced-color-adjust: none;
647
+ }
416
648
  }
417
649
 
418
650
  /* Print: the dot surfaces carry data (heatmap cells, the segmented meter, the
419
- status dot, the masked glyph), so their painted fills must survive the print
420
- "economy" default that drops backgrounds. */
651
+ status dot, the masked glyph, the waffle/activity/level/gauge readings), so
652
+ their painted fills must survive the print "economy" default that drops
653
+ backgrounds. */
421
654
  @media print {
422
655
  .ui-dotmatrix__cell,
423
656
  .ui-dotbar i,
424
657
  .ui-dot,
425
- .ui-icon {
658
+ .ui-icon,
659
+ .ui-waffle i,
660
+ .ui-activity i,
661
+ .ui-level i,
662
+ .ui-dotgauge {
426
663
  -webkit-print-color-adjust: exact;
427
664
  print-color-adjust: exact;
428
665
  }
package/css/feedback.css CHANGED
@@ -4,6 +4,19 @@
4
4
  never a fill. Nothing-flat, hairline, sharp.
5
5
  ========================================================================== */
6
6
 
7
+ /* `--value` drives the progress/meter fill width: a UNITLESS number 0–100,
8
+ never a percentage. Registering it as `<number>` makes that contract real —
9
+ a stray `--value: 50%` is invalid against the typed syntax and falls back to
10
+ the initial `0` (empty bar) instead of poisoning the `clamp()` and painting a
11
+ FULL bar (the old failure mode). It inherits so the value set on the host
12
+ `.ui-meter` / `.ui-progress` cascades to the inner `__fill`/`__bar`.
13
+ (component audit C8.) */
14
+ @property --value {
15
+ syntax: '<number>';
16
+ inherits: true;
17
+ initial-value: 0;
18
+ }
19
+
7
20
  /* --- Alert / callout — inline, dismissible-compatible --- */
8
21
 
9
22
  .ui-alert {
@@ -50,7 +63,7 @@
50
63
  margin: 0;
51
64
  }
52
65
 
53
- .ui-alert__dismiss {
66
+ .ui-alert__close {
54
67
  background: transparent;
55
68
  border: 0;
56
69
  color: var(--text-dim);
@@ -62,7 +75,7 @@
62
75
  padding: 0.1rem 0.3rem;
63
76
  }
64
77
 
65
- .ui-alert:has(.ui-alert__dismiss) {
78
+ .ui-alert:has(.ui-alert__close) {
66
79
  grid-template-columns: auto 1fr auto;
67
80
  }
68
81
 
@@ -107,7 +120,7 @@
107
120
  }
108
121
 
109
122
  @media (hover: hover) {
110
- .ui-alert__dismiss:hover {
123
+ .ui-alert__close:hover {
111
124
  color: var(--text);
112
125
  }
113
126
  }
@@ -216,7 +229,7 @@
216
229
  tap-target floor (2.9rem, as in primitives.css / forms.css) without changing
217
230
  the desktop glyph size. The box is centred so the glyph stays put. */
218
231
  @media (pointer: coarse) {
219
- .ui-alert__dismiss,
232
+ .ui-alert__close,
220
233
  .ui-toast__close {
221
234
  display: inline-grid;
222
235
  place-items: center;
@@ -224,7 +237,7 @@
224
237
  min-inline-size: 2.9rem;
225
238
  }
226
239
 
227
- .ui-alert__dismiss {
240
+ .ui-alert__close {
228
241
  padding: 0;
229
242
  }
230
243
  }
@@ -453,6 +466,18 @@
453
466
  @media (prefers-reduced-motion: reduce) {
454
467
  .ui-progress--indeterminate .ui-progress__bar {
455
468
  animation-duration: 0.01ms;
469
+
470
+ /* A still, solid, full-width bar reads as "100% complete" — the opposite of
471
+ indeterminate. Fall back to a static diagonal hatch that fills the track
472
+ (so it's clearly active) but doesn't read as done. AT is covered via
473
+ aria-busy. (audit C26.) */
474
+ background: repeating-linear-gradient(
475
+ -45deg,
476
+ var(--accent) 0,
477
+ var(--accent) 0.3rem,
478
+ color-mix(in srgb, var(--accent) 35%, transparent) 0.3rem,
479
+ color-mix(in srgb, var(--accent) 35%, transparent) 0.6rem
480
+ );
456
481
  inset-inline-start: 0;
457
482
  inline-size: 100%;
458
483
  }
@@ -467,7 +492,9 @@
467
492
  (task progress, can be indeterminate), a meter shows a measured static
468
493
  value for data display (coverage, capacity, a KPI against a target).
469
494
  Drive the fill with the same --value knob as progress; tone the fill by
470
- threshold. Author role="meter" + aria-valuenow/min/max for AT. --- */
495
+ threshold. Author role="meter" + aria-valuenow/min/max for AT but role=meter
496
+ has uneven AT support, so keep the visible .ui-meter__label/__value (they are
497
+ the real channel, not just decoration). (component audit C25.) --- */
471
498
 
472
499
  .ui-meter {
473
500
  background: var(--panel-soft);
@@ -534,7 +561,12 @@
534
561
  font-size: var(--text-xs);
535
562
  gap: 0.5rem;
536
563
  letter-spacing: var(--tracking-wide);
537
- min-inline-size: max-content;
564
+
565
+ /* Prefer the natural one-line width, but never wider than the container: a
566
+ long step label at `max-content` couldn't shrink and overflowed the page on
567
+ narrow viewports (tabs scroll; steps didn't). Capping at 100% lets an
568
+ over-long label wrap instead of overflowing. (component audit C18.) */
569
+ min-inline-size: min(100%, max-content);
538
570
  text-transform: uppercase;
539
571
  }
540
572
 
package/css/forms.css CHANGED
@@ -77,6 +77,16 @@
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
+
80
90
  /* Disabled affordance parity. The text inputs above style :disabled directly;
81
91
  the controls that WRAP a native input (switch/check/segmented) showed no
82
92
  disabled cue and their label kept cursor:pointer — a lie. Mirror the cue via
@@ -94,6 +104,8 @@
94
104
  /* Keep autofilled fields on-theme — the UA's yellow fill otherwise paints over
95
105
  the monochrome surface and breaks the contrast story. (forms review C24.) */
96
106
  .ui-input:autofill,
107
+ .ui-select:autofill,
108
+ .ui-textarea:autofill,
97
109
  .ui-search input:autofill {
98
110
  -webkit-text-fill-color: var(--text);
99
111
  box-shadow: inset 0 0 0 100rem var(--bg-elevated);
@@ -106,6 +118,58 @@
106
118
  border-color: var(--danger);
107
119
  }
108
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
+
109
173
  .ui-hint {
110
174
  color: var(--text-dim);
111
175
  font-size: var(--text-2xs);
@@ -139,14 +203,18 @@
139
203
  border-end-end-radius: var(--radius-md);
140
204
  }
141
205
 
142
- .ui-input-group > .ui-input:focus-visible {
143
- 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) */
144
209
  }
145
210
 
146
211
  .ui-input-group__addon {
147
212
  align-items: center;
148
213
  background: var(--panel-soft);
149
- 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);
150
218
  color: var(--text-dim);
151
219
  display: flex;
152
220
  font-size: var(--text-sm);
package/css/legend.css CHANGED
@@ -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
  }
package/css/motion.css CHANGED
@@ -52,6 +52,28 @@
52
52
  }
53
53
  }
54
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
+
55
77
  @keyframes uiDotIn {
56
78
  0% {
57
79
  opacity: 0;
@@ -158,10 +180,13 @@
158
180
  animation: uiMatrixReveal var(--duration-slow) var(--ease-out) both;
159
181
  }
160
182
 
161
- /* 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.) */
162
187
  .ui-stagger > * {
163
188
  animation: uiRise var(--duration-slow) var(--ease-spring) both;
164
- animation-delay: calc(var(--i, 0) * 60ms);
189
+ animation-delay: calc(min(var(--i, 0), 6) * 60ms);
165
190
  }
166
191
 
167
192
  .ui-stagger--auto > *:nth-child(1) {
@@ -193,11 +218,17 @@
193
218
  }
194
219
 
195
220
  /* Reveal-on-scroll: add .ui-reveal, then toggle .is-visible (e.g. from an
196
- IntersectionObserver you own) to play it in. For a ZERO-JS reveal, use
197
- `.ui-scroll-reveal` (scroll-driven, CSS-only) instead — that is the path for
198
- an LLM-authored or no-build report. The hidden initial state below is gated
199
- on `scripting: enabled`, so with scripting OFF the content is fully visible
200
- and never silently hidden behind a script that will never run. */
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. */
201
232
  @media (prefers-reduced-motion: no-preference) and (scripting: enabled) {
202
233
  .ui-reveal {
203
234
  opacity: 0;
@@ -246,10 +277,12 @@
246
277
 
247
278
  /* --- Scroll-driven (progressive enhancement) — the scroll/view timeline
248
279
  IS the engine, no JS. Everything is gated on `@supports
249
- (animation-timeline: …)` so engines without it (today, Firefox/Safari)
250
- keep the static end state, and on `prefers-reduced-motion: no-preference`
251
- (a scroll timeline ignores animation-duration, so the global reduced-motion
252
- 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). --- */
253
286
 
254
287
  /* Reading-progress bar. Fixed hairline that fills with document scroll;
255
288
  unsupported → a static, empty (scaleX(0)) bar. Pair with role="progressbar"
@@ -270,6 +303,30 @@
270
303
  transform-origin: 100% 50%;
271
304
  }
272
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
+
273
330
  @supports (animation-timeline: scroll()) {
274
331
  @media (prefers-reduced-motion: no-preference) {
275
332
  .ui-scroll-progress {
@@ -284,13 +341,21 @@
284
341
 
285
342
  /* Reveal-on-scroll with no JS and no IntersectionObserver: the element
286
343
  rises + fades as it scrolls into view. Unsupported → fully visible
287
- (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. */
288
353
  @supports (animation-timeline: view()) {
289
354
  @media (prefers-reduced-motion: no-preference) {
290
355
  .ui-scroll-reveal {
291
- animation: uiRise linear both;
356
+ animation: uiScrollReveal linear both;
292
357
  animation-timeline: view();
293
- animation-range: entry 0% cover 40%;
358
+ animation-range: entry 0% entry 100%;
294
359
  }
295
360
  }
296
361
  }