@ponchia/ui 0.6.7 → 0.6.9

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 (111) hide show
  1. package/CHANGELOG.md +129 -4
  2. package/README.md +4 -4
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +26 -9
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +145 -49
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +220 -92
  9. package/behaviors/command.d.ts.map +1 -1
  10. package/behaviors/command.js +74 -14
  11. package/behaviors/connectors.d.ts.map +1 -1
  12. package/behaviors/connectors.js +131 -32
  13. package/behaviors/crosshair.d.ts.map +1 -1
  14. package/behaviors/crosshair.js +47 -1
  15. package/behaviors/dialog.d.ts.map +1 -1
  16. package/behaviors/dialog.js +24 -9
  17. package/behaviors/disclosure.d.ts.map +1 -1
  18. package/behaviors/disclosure.js +33 -3
  19. package/behaviors/dismissible.d.ts.map +1 -1
  20. package/behaviors/dismissible.js +3 -2
  21. package/behaviors/forms.d.ts.map +1 -1
  22. package/behaviors/forms.js +211 -140
  23. package/behaviors/glyph.d.ts.map +1 -1
  24. package/behaviors/glyph.js +172 -132
  25. package/behaviors/inert.d.ts +1 -1
  26. package/behaviors/inert.d.ts.map +1 -1
  27. package/behaviors/inert.js +4 -3
  28. package/behaviors/internal.d.ts.map +1 -1
  29. package/behaviors/internal.js +4 -3
  30. package/behaviors/legend.d.ts +0 -5
  31. package/behaviors/legend.d.ts.map +1 -1
  32. package/behaviors/legend.js +45 -13
  33. package/behaviors/menu.d.ts.map +1 -1
  34. package/behaviors/menu.js +13 -8
  35. package/behaviors/modal.d.ts.map +1 -1
  36. package/behaviors/modal.js +77 -19
  37. package/behaviors/popover.d.ts +4 -3
  38. package/behaviors/popover.d.ts.map +1 -1
  39. package/behaviors/popover.js +94 -14
  40. package/behaviors/sources.d.ts.map +1 -1
  41. package/behaviors/sources.js +14 -2
  42. package/behaviors/splitter.d.ts.map +1 -1
  43. package/behaviors/splitter.js +149 -110
  44. package/behaviors/spotlight.d.ts.map +1 -1
  45. package/behaviors/spotlight.js +28 -2
  46. package/behaviors/table.d.ts +1 -1
  47. package/behaviors/table.d.ts.map +1 -1
  48. package/behaviors/table.js +108 -17
  49. package/behaviors/tabs.d.ts.map +1 -1
  50. package/behaviors/tabs.js +84 -20
  51. package/behaviors/theme.d.ts.map +1 -1
  52. package/behaviors/theme.js +25 -5
  53. package/behaviors/toast.js +5 -5
  54. package/classes/index.d.ts +15 -2
  55. package/classes/index.js +48 -35
  56. package/connectors/index.d.ts +41 -8
  57. package/connectors/index.d.ts.map +1 -1
  58. package/connectors/index.js +74 -19
  59. package/css/annotations.css +12 -0
  60. package/css/app.css +3 -4
  61. package/css/base.css +1 -1
  62. package/css/content.css +3 -3
  63. package/css/crosshair.css +27 -2
  64. package/css/disclosure.css +3 -3
  65. package/css/dots.css +4 -4
  66. package/css/feedback.css +8 -37
  67. package/css/forms.css +9 -12
  68. package/css/legend.css +1 -1
  69. package/css/marks.css +1 -1
  70. package/css/motion.css +6 -6
  71. package/css/navigation.css +12 -0
  72. package/css/overlay.css +5 -7
  73. package/css/primitives.css +14 -16
  74. package/css/sidenote.css +2 -2
  75. package/css/table.css +2 -2
  76. package/css/tokens.css +16 -0
  77. package/dist/bronto.css +1 -1
  78. package/dist/css/analytical.css +1 -1
  79. package/dist/css/annotations.css +1 -1
  80. package/dist/css/crosshair.css +1 -1
  81. package/dist/css/feedback.css +1 -1
  82. package/dist/css/navigation.css +1 -1
  83. package/dist/css/report-kit.css +1 -1
  84. package/dist/css/tokens.css +1 -1
  85. package/docs/adr/0001-color-system.md +3 -2
  86. package/docs/annotations.md +21 -1
  87. package/docs/architecture.md +74 -13
  88. package/docs/command.md +4 -1
  89. package/docs/connectors.md +16 -0
  90. package/docs/crosshair.md +1 -1
  91. package/docs/dots.md +4 -1
  92. package/docs/glyphs.md +11 -0
  93. package/docs/interop/react-flow.md +89 -0
  94. package/docs/migrations/0.2-to-0.3.md +1 -1
  95. package/docs/package-contract.md +7 -5
  96. package/docs/reporting.md +23 -12
  97. package/docs/stability.md +85 -9
  98. package/docs/theming.md +2 -2
  99. package/docs/usage.md +16 -2
  100. package/docs/vega.md +4 -4
  101. package/glyphs/glyphs.js +43 -33
  102. package/llms.txt +19 -8
  103. package/package.json +23 -4
  104. package/schemas/report-claims.v1.schema.json +1 -1
  105. package/svelte/index.d.ts +71 -45
  106. package/svelte/index.d.ts.map +1 -1
  107. package/svelte/index.js +29 -2
  108. package/tokens/index.js +2 -2
  109. package/vue/index.d.ts +42 -5
  110. package/vue/index.d.ts.map +1 -1
  111. package/vue/index.js +32 -1
@@ -43,46 +43,95 @@
43
43
  */
44
44
 
45
45
  // Shared scalar/geometry primitives. Exported so the annotations layer composes
46
- // on the SAME kernel instead of copy-pasting it (the copies had silently
47
- // diverged — see `clamp`). Low-level helpers; the documented API is the path
48
- // builders below.
46
+ // on the same kernel as the connector path builders. Low-level helpers; the
47
+ // documented API is the path builders below.
49
48
  export const PRECISION = 1000;
50
49
 
50
+ /**
51
+ * Resolve a numeric option with an optional fallback.
52
+ * @param {string} name
53
+ * @param {number | null | undefined} value
54
+ * @param {number | null | undefined} [fallback]
55
+ * @returns {number}
56
+ */
51
57
  export function finite(name, value, fallback) {
52
58
  const v = value ?? fallback;
53
59
  if (!Number.isFinite(v)) throw new TypeError(`${name} must be a finite number`);
54
60
  return v;
55
61
  }
56
62
 
63
+ /**
64
+ * Resolve a non-negative numeric option with an optional fallback.
65
+ * @param {string} name
66
+ * @param {number | null | undefined} value
67
+ * @param {number | null | undefined} [fallback]
68
+ * @returns {number}
69
+ */
57
70
  export function dimension(name, value, fallback) {
58
71
  const v = finite(name, value, fallback);
59
72
  if (v < 0) throw new RangeError(`${name} must be greater than or equal to 0`);
60
73
  return v;
61
74
  }
62
75
 
63
- // Round to PRECISION, normalising -0 → 0, and return the NUMBER (the numeric
64
- // core `fmt` stringifies). Shared with the annotations layer for the rounded
65
- // coordinates it echoes back to the host. (code-quality audit Q5.)
76
+ // Round to PRECISION, normalising -0 → 0, and return the NUMBER. `fmt`
77
+ // stringifies the numeric core; annotations use the same rounded coordinates.
78
+ /**
79
+ * @param {number} value
80
+ * @returns {number}
81
+ */
66
82
  export function roundNumber(value) {
67
83
  const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
68
84
  return Object.is(rounded, -0) ? 0 : rounded;
69
85
  }
70
86
 
87
+ /**
88
+ * @param {number} value
89
+ * @returns {string}
90
+ */
71
91
  export function fmt(value) {
72
92
  return String(roundNumber(value));
73
93
  }
74
94
 
95
+ /**
96
+ * @param {number} x
97
+ * @param {number} y
98
+ * @returns {string}
99
+ */
75
100
  export function point(x, y) {
76
101
  return `${fmt(x)},${fmt(y)}`;
77
102
  }
78
103
 
79
- // Guarded form (returns min when the range is inverted) — the reconciled body;
80
- // connectors only ever calls clamp(v, 0, 1) so this is output-identical here.
104
+ // Guarded form: an inverted range resolves to `min`.
105
+ /**
106
+ * @param {number} value
107
+ * @param {number} min
108
+ * @param {number} max
109
+ * @returns {number}
110
+ */
81
111
  export function clamp(value, min, max) {
82
112
  if (max < min) return min;
83
113
  return Math.min(max, Math.max(min, value));
84
114
  }
85
115
 
116
+ function connectorShape(value) {
117
+ const shape = value ?? 'straight';
118
+ if (shape === 'straight' || shape === 'elbow' || shape === 'curve') return shape;
119
+ throw new TypeError('shape must be "straight", "elbow" or "curve"');
120
+ }
121
+
122
+ function sideValue(value) {
123
+ const side = value ?? 'center';
124
+ if (
125
+ side === 'top' ||
126
+ side === 'right' ||
127
+ side === 'bottom' ||
128
+ side === 'left' ||
129
+ side === 'center'
130
+ )
131
+ return side;
132
+ throw new TypeError('side must be "top", "right", "bottom", "left" or "center"');
133
+ }
134
+
86
135
  /**
87
136
  * A point on a rect's edge (or centre). `rect` is `{ x, y, width, height }`.
88
137
  * @param {Rect} rect
@@ -90,11 +139,11 @@ export function clamp(value, min, max) {
90
139
  * @returns {Point}
91
140
  */
92
141
  export function anchorPoint(rect, side = 'center') {
93
- const x = finite('rect.x', rect?.x, 0);
94
- const y = finite('rect.y', rect?.y, 0);
95
- const w = dimension('rect.width', rect?.width, 0);
96
- const h = dimension('rect.height', rect?.height, 0);
97
- switch (side) {
142
+ const x = finite('rect.x', rect?.x);
143
+ const y = finite('rect.y', rect?.y);
144
+ const w = dimension('rect.width', rect?.width);
145
+ const h = dimension('rect.height', rect?.height);
146
+ switch (sideValue(side)) {
98
147
  case 'top':
99
148
  return { x: x + w / 2, y };
100
149
  case 'bottom':
@@ -185,7 +234,8 @@ export function curvePath(from, to, opts = {}) {
185
234
  * @returns {string}
186
235
  */
187
236
  export function connectorPath(opts = {}) {
188
- const { from, to, shape = 'straight' } = opts;
237
+ const { from, to } = opts;
238
+ const shape = connectorShape(opts.shape);
189
239
  if (shape === 'elbow') return elbowPath(from, to, opts);
190
240
  if (shape === 'curve') return curvePath(from, to, opts);
191
241
  return straightPath(from, to);
@@ -231,8 +281,8 @@ export function dotMark(p, radius = 3) {
231
281
 
232
282
  /**
233
283
  * An axis-aligned rectangle path from its corners (callers derive the corners
234
- * from a centre or a top-left as they need). Shared by the annotation
235
- * rect/band and evidence-marker subjects. (code-quality audit Q5.)
284
+ * from a centre or a top-left as they need). Shared by annotation rect/band
285
+ * and evidence-marker subjects.
236
286
  * @param {number} left
237
287
  * @param {number} top
238
288
  * @param {number} right
@@ -270,7 +320,8 @@ export function autoSides(fromRect, toRect) {
270
320
  * @returns {number}
271
321
  */
272
322
  export function endTangentAngle(from, to, shape = 'straight') {
273
- if (shape === 'straight') return angleBetween(from, to);
323
+ const resolved = connectorShape(shape);
324
+ if (resolved === 'straight') return angleBetween(from, to);
274
325
  const dx = finite('to.x', to?.x) - finite('from.x', from?.x);
275
326
  const dy = finite('to.y', to?.y) - finite('from.y', from?.y);
276
327
  if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? 0 : Math.PI;
@@ -285,10 +336,14 @@ export function endTangentAngle(from, to, shape = 'straight') {
285
336
  * @returns {ConnectRectsResult}
286
337
  */
287
338
  export function connectRects(opts = {}) {
288
- const { fromRect, toRect, shape = 'straight', curvature, mid } = opts;
339
+ const { fromRect, toRect, curvature, mid } = opts;
340
+ const shape = connectorShape(opts.shape);
289
341
  // Honor each side override independently; auto-pick whichever is unset.
290
342
  const auto = autoSides(fromRect, toRect);
291
- const sides = { from: opts.fromSide || auto.from, to: opts.toSide || auto.to };
343
+ const sides = {
344
+ from: opts.fromSide == null ? auto.from : sideValue(opts.fromSide),
345
+ to: opts.toSide == null ? auto.to : sideValue(opts.toSide),
346
+ };
292
347
  const from = anchorPoint(fromRect, sides.from);
293
348
  const to = anchorPoint(toRect, sides.to);
294
349
  const d = connectorPath({ from, to, shape, curvature, mid });
@@ -278,8 +278,20 @@
278
278
  animation: none !important;
279
279
  opacity: 1;
280
280
  stroke-dashoffset: 0;
281
+ }
282
+
283
+ .ui-annotation__subject,
284
+ .ui-annotation__connector,
285
+ .ui-annotation__note-line,
286
+ .ui-annotation__badge {
281
287
  transform: none;
282
288
  }
289
+
290
+ .ui-annotation--draw .ui-annotation__connector,
291
+ .ui-annotation--draw .ui-annotation__note-line {
292
+ stroke-dasharray: none;
293
+ stroke-dashoffset: 0;
294
+ }
283
295
  }
284
296
 
285
297
  @media (forced-colors: active) {
package/css/app.css CHANGED
@@ -123,7 +123,7 @@
123
123
  near-imperceptible tint/dot opacity delta survives, and current-page becomes
124
124
  invisible to HCM users. Re-assert "current" on channels HCM preserves: a
125
125
  Highlight start-border and marker dot, plus a NON-colour bold weight so the
126
- cue does not rely on colour alone (WCAG 1.4.1). (component audit C8.) */
126
+ cue does not rely on colour alone (WCAG 1.4.1). */
127
127
  @media (forced-colors: active) {
128
128
  .ui-app-nav a.is-active,
129
129
  .ui-app-nav a[aria-current]:not([aria-current='false']) {
@@ -276,7 +276,7 @@
276
276
  /* The shell keeps `min-block-size: 100dvh`, so with two auto rows the default
277
277
  `align-content: stretch` distributes the leftover viewport across BOTH
278
278
  tracks — ballooning the horizontal rail to ~half the screen. Pin tracks to
279
- their content and let the content row absorb the slack. (component audit C7.) */
279
+ their content and let the content row absorb the slack. */
280
280
  align-content: start;
281
281
  grid-template-rows: auto 1fr;
282
282
  }
@@ -296,8 +296,7 @@
296
296
 
297
297
  /* The rail is flex-row here, so the base `margin-block-start: auto` (which
298
298
  pushed account to the bottom in the column layout) is inert and account can
299
- scroll off. Push it to the inline end instead so sign-out stays reachable.
300
- (layout review C21.) */
299
+ scroll off. Push it to the inline end instead so sign-out stays reachable. */
301
300
  .ui-app-rail__account {
302
301
  margin-block-start: 0;
303
302
  margin-inline-start: auto;
package/css/base.css CHANGED
@@ -230,7 +230,7 @@ textarea:focus-visible,
230
230
  /* NB: the active-tab forced-colors re-assert lives in disclosure.css, right
231
231
  after the `.ui-tab.is-active` default — placing it here (an earlier bundle
232
232
  leaf) let the later default win even in forced-colors mode, since @media
233
- adds no specificity. (a11y review C10.) */
233
+ adds no specificity. */
234
234
 
235
235
  /* Keyboard focus must never depend on a colour that gets overridden. */
236
236
  a:focus-visible,
package/css/content.css CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  /* Machine-generated Markdown carries long unbreakable tokens (URLs, hashes,
17
17
  code) that otherwise force horizontal page scroll on the prose surface
18
- itself. Break them. (css-robustness review C9; the table cell already does.) */
18
+ itself. Break them; table cells already do this locally. */
19
19
  overflow-wrap: break-word;
20
20
 
21
21
  /* Readable measure; the container can still be wider for tables/media. */
@@ -108,7 +108,7 @@
108
108
  is the opt-in marks leaf (marks.css) and must win: this rule is higher-specificity
109
109
  (0,1,1) than `.ui-mark` (0,1,0) and sits in the same `bronto` layer, so without the
110
110
  `:not(.ui-mark)` it silently overrides every `.ui-mark` tone/draw modifier in prose —
111
- the most-documented mark usage. (component audit C1.) */
111
+ the most-documented mark usage. */
112
112
  .ui-prose mark:not(.ui-mark) {
113
113
  background: var(--accent-soft);
114
114
  color: var(--text);
@@ -255,7 +255,7 @@
255
255
  because `.ui-prose` is generated prose, not a data grid — but for a table whose
256
256
  semantics MUST survive on WebKit, author the `.ui-table` component inside a
257
257
  `.ui-table-wrap` instead (the wrap scrolls, the <table> keeps `display: table`),
258
- or add `role="table"` to the markdown output. (component audit C29.) */
258
+ or add `role="table"` to the markdown output. */
259
259
 
260
260
  .ui-prose table {
261
261
  border: 1px solid var(--line);
package/css/crosshair.css CHANGED
@@ -13,6 +13,9 @@
13
13
  --crosshair-x: 0;
14
14
  --crosshair-y: 0;
15
15
  --crosshair-color: var(--accent);
16
+ --crosshair-readout-gap: 0.35rem;
17
+ --crosshair-readout-x: var(--crosshair-readout-gap);
18
+ --crosshair-readout-y: var(--crosshair-readout-gap);
16
19
 
17
20
  inset: 0;
18
21
  opacity: 0;
@@ -25,6 +28,22 @@
25
28
  opacity: 1;
26
29
  }
27
30
 
31
+ .ui-crosshair[data-readout-inline='before'] {
32
+ --crosshair-readout-x: calc(-100% - var(--crosshair-readout-gap));
33
+ }
34
+
35
+ .ui-crosshair[data-readout-block='above'] {
36
+ --crosshair-readout-y: calc(-100% - var(--crosshair-readout-gap));
37
+ }
38
+
39
+ .ui-crosshair:dir(rtl) {
40
+ --crosshair-readout-x: calc(100% + var(--crosshair-readout-gap));
41
+ }
42
+
43
+ .ui-crosshair:dir(rtl)[data-readout-inline='before'] {
44
+ --crosshair-readout-x: calc(-1 * var(--crosshair-readout-gap));
45
+ }
46
+
28
47
  @media (prefers-reduced-motion: reduce) {
29
48
  .ui-crosshair {
30
49
  transition: none;
@@ -76,8 +95,9 @@
76
95
  color: var(--text);
77
96
  }
78
97
 
79
- /* A pinned readout chip — host fills the content; it follows the crosshair. */
80
- .ui-readout {
98
+ /* A pinned readout chip — host fills the content; it follows the crosshair.
99
+ Scoped so report-kit/crosshair imports do not restyle standalone dot readouts. */
100
+ .ui-crosshair .ui-readout {
81
101
  background: var(--panel);
82
102
  border: 1px solid var(--line);
83
103
  border-radius: var(--radius-sm);
@@ -87,10 +107,15 @@
87
107
  font-size: var(--text-xs);
88
108
  inset-block-start: var(--crosshair-y);
89
109
  inset-inline-start: var(--crosshair-x);
110
+ max-inline-size: calc(100% - var(--crosshair-readout-gap) * 2);
111
+ overflow: hidden;
90
112
  padding-block: 0.2rem;
91
113
  padding-inline: 0.4rem;
92
114
  pointer-events: none;
93
115
  position: absolute;
116
+ text-overflow: ellipsis;
117
+ transform: translate(var(--crosshair-readout-x), var(--crosshair-readout-y));
118
+ white-space: nowrap;
94
119
  }
95
120
 
96
121
  @media (forced-colors: active) {
@@ -59,7 +59,7 @@
59
59
 
60
60
  /* Forced-colors re-assert MUST sit after the default above (same specificity,
61
61
  @media adds none) or the accent default wins and the selected tab loses its
62
- only cue. Moved here from base.css, an earlier bundle leaf. (a11y review C10.) */
62
+ only cue. Moved here from base.css, an earlier bundle leaf. */
63
63
  @media (forced-colors: active) {
64
64
  .ui-tab.is-active {
65
65
  border-block-end-color: Highlight;
@@ -306,7 +306,7 @@
306
306
  /* Active page keys on BOTH `.is-active` and `[aria-current]` — usage.md calls
307
307
  aria-current "the framework rule", and every nav sibling (breadcrumb, sitenav,
308
308
  app-nav) highlights on it, so a pagination author who sets only aria-current
309
- must still get the highlight. (component audit C17.) */
309
+ must still get the highlight. */
310
310
  .ui-pagination__item.is-active,
311
311
  .ui-pagination__item[aria-current]:not([aria-current='false']) {
312
312
  border-color: var(--accent);
@@ -319,7 +319,7 @@
319
319
  (CSS can't intercept keys). For a fully-inert control prefer native
320
320
  `<button disabled>`, run `initDisabledGuard()` (intercepts Enter/Space), or
321
321
  pair aria-disabled with `tabindex="-1"`. See docs/usage.md "Disabled vs
322
- aria-disabled". (audit C4.) */
322
+ aria-disabled". */
323
323
  .ui-pagination__item[aria-disabled='true'],
324
324
  .ui-pagination__item:disabled {
325
325
  cursor: not-allowed;
package/css/dots.css CHANGED
@@ -283,7 +283,7 @@
283
283
 
284
284
  /* The comet expects exactly 8 `<i>`; a 9th+ child has no rotation rule and
285
285
  would pile up dead-centre. Hide the overflow so extra children fail safe
286
- rather than rendering a stray static dot (C26). */
286
+ rather than rendering a stray static dot. */
287
287
  .ui-dotspinner i:nth-child(n + 9) {
288
288
  display: none;
289
289
  }
@@ -523,7 +523,7 @@
523
523
  clipped from-state on `scripting: enabled`: with JS off, `.is-in` is never
524
524
  toggled, so without the gate the content stays permanently clipped away and
525
525
  invisible to every no-JS/static/print reader. Same graceful default as
526
- `.ui-reveal`. (component audit C12.) */
526
+ `.ui-reveal`. */
527
527
  @media (scripting: enabled) {
528
528
  .ui-matrix {
529
529
  clip-path: inset(0 100% 0 0);
@@ -583,7 +583,7 @@
583
583
 
584
584
  /* Brand/live dots aren't status tones, but they still encode meaning via
585
585
  background-color alone, which HCM flattens. Keep them on a distinct,
586
- opted-out system colour for completeness. (audit C31.) */
586
+ opted-out system colour for completeness. */
587
587
  .ui-dot--accent,
588
588
  .ui-dot--live {
589
589
  forced-color-adjust: none;
@@ -599,7 +599,7 @@
599
599
  glyph vanishes (white-on-white) — yet .ui-icon is the recommended
600
600
  icon-at-scale path AND backs .ui-legend__symbol, and the print block
601
601
  already special-cases it. Opt out and pin the fill to the system text
602
- colour so the glyph stays visible. (audit C1.) */
602
+ colour so the glyph stays visible. */
603
603
  .ui-icon {
604
604
  forced-color-adjust: none;
605
605
  background: CanvasText;
package/css/feedback.css CHANGED
@@ -9,8 +9,7 @@
9
9
  a stray `--value: 50%` is invalid against the typed syntax and falls back to
10
10
  the initial `0` (empty bar) instead of poisoning the `clamp()` and painting a
11
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.) */
12
+ `.ui-meter` / `.ui-progress` cascades to the inner `__fill`/`__bar`. */
14
13
  @property --value {
15
14
  syntax: '<number>';
16
15
  inherits: true;
@@ -335,9 +334,7 @@
335
334
  position: absolute;
336
335
  text-transform: uppercase;
337
336
  transform: translate(-50%, 4px);
338
- transition:
339
- opacity var(--duration-fast) var(--ease-standard),
340
- transform var(--duration-fast) var(--ease-standard);
337
+ transition: opacity var(--duration-fast) var(--ease-standard);
341
338
  white-space: nowrap;
342
339
  z-index: var(--z-popover);
343
340
  }
@@ -348,33 +345,6 @@
348
345
  transform: translate(-50%, 0);
349
346
  }
350
347
 
351
- /* Progressive enhancement: where CSS anchor positioning exists, lift
352
- the bubble out of the normal flow so it can't be clipped by an
353
- ancestor's overflow/scroll and auto-flips at the viewport edge.
354
- Unsupported browsers keep the absolutely-positioned fallback above
355
- (fine for short labels; use .ui-popover + initPopover for rich or
356
- edge-critical content). */
357
- @supports (anchor-name: --x) {
358
- .ui-tooltip {
359
- anchor-name: --ui-tooltip;
360
- }
361
-
362
- .ui-tooltip__bubble {
363
- inset: auto;
364
- margin-block-end: 0.5rem;
365
- position: fixed;
366
- position-anchor: --ui-tooltip;
367
- position-area: block-start center;
368
- position-try-fallbacks: flip-block;
369
- transform: translateY(4px);
370
- }
371
-
372
- .ui-tooltip:hover .ui-tooltip__bubble,
373
- .ui-tooltip:focus-within .ui-tooltip__bubble {
374
- transform: translateY(0);
375
- }
376
- }
377
-
378
348
  /* Popover surface — a top-layer panel positioned by initPopover (JS
379
349
  collision-aware, dependency-free). Uses the native [popover] top
380
350
  layer when available so it never clips; the class styles it either
@@ -389,6 +359,7 @@
389
359
  inline-size: max-content;
390
360
  margin: 0;
391
361
  max-inline-size: min(22rem, calc(100vw - 2rem));
362
+ overflow: auto;
392
363
  padding: var(--space-sm) var(--space-md);
393
364
  position: fixed;
394
365
  z-index: var(--z-popover);
@@ -470,7 +441,7 @@
470
441
  /* A still, solid, full-width bar reads as "100% complete" — the opposite of
471
442
  indeterminate. Fall back to a static diagonal hatch that fills the track
472
443
  (so it's clearly active) but doesn't read as done. AT is covered via
473
- aria-busy. (audit C26.) */
444
+ aria-busy. */
474
445
  background: repeating-linear-gradient(
475
446
  -45deg,
476
447
  var(--accent) 0,
@@ -494,7 +465,7 @@
494
465
  Drive the fill with the same --value knob as progress; tone the fill by
495
466
  threshold. Author role="meter" + aria-valuenow/min/max for AT — but role=meter
496
467
  has uneven AT support, so keep the visible .ui-meter__label/__value (they are
497
- the real channel, not just decoration). (component audit C25.) --- */
468
+ the real channel, not just decoration). --- */
498
469
 
499
470
  .ui-meter {
500
471
  background: var(--panel-soft);
@@ -565,7 +536,7 @@
565
536
  /* Prefer the natural one-line width, but never wider than the container: a
566
537
  long step label at `max-content` couldn't shrink and overflowed the page on
567
538
  narrow viewports (tabs scroll; steps didn't). Capping at 100% lets an
568
- over-long label wrap instead of overflowing. (component audit C18.) */
539
+ over-long label wrap instead of overflowing. */
569
540
  min-inline-size: min(100%, max-content);
570
541
  text-transform: uppercase;
571
542
  }
@@ -625,10 +596,10 @@
625
596
  /* Forced-colors flattens the fill's tone to the system palette and can erase it
626
597
  against the track, dropping the only visual cue of the measured proportion.
627
598
  Re-assert a system colour so the bar stays visible; the tone's *semantic* is
628
- carried by the author-written label beside it, not the colour. (a11y C10.)
599
+ carried by the author-written label beside it, not the colour.
629
600
  The toned `.ui-meter--TONE .ui-meter__fill` rules are (0,2,0); the bare
630
601
  `.ui-meter__fill` reset is only (0,1,0), so it lost — a toned fill stayed
631
- `var(--TONE)` and was forced to black-on-black (component-audit C3). Match the
602
+ `var(--TONE)` and was forced to black-on-black. Match the
632
603
  tone specificity here (and set `forced-color-adjust: none`) so every meter,
633
604
  toned or not, paints `Highlight`, mirroring the `.ui-dot` precedent. */
634
605
  @media (forced-colors: active) {
package/css/forms.css CHANGED
@@ -79,8 +79,7 @@
79
79
 
80
80
  /* Read-only is editable-looking but not editable; give it a distinct, quieter
81
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.) */
82
+ disabled — value still submits and the field stays focusable/selectable. */
84
83
  .ui-input:read-only:not(:disabled),
85
84
  .ui-textarea:read-only:not(:disabled) {
86
85
  background: var(--panel-soft);
@@ -91,7 +90,7 @@
91
90
  the controls that WRAP a native input (switch/check/segmented) showed no
92
91
  disabled cue and their label kept cursor:pointer — a lie. Mirror the cue via
93
92
  :has(input:disabled); the native-element controls (range/file) take :disabled
94
- directly. (a11y/forms review C4.) */
93
+ directly. */
95
94
  .ui-range:disabled,
96
95
  .ui-file:disabled,
97
96
  .ui-switch:has(input:disabled),
@@ -102,7 +101,7 @@
102
101
  }
103
102
 
104
103
  /* 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.) */
104
+ the monochrome surface and breaks the contrast story. */
106
105
  .ui-input:autofill,
107
106
  .ui-select:autofill,
108
107
  .ui-textarea:autofill,
@@ -121,8 +120,7 @@
121
120
  /* Wrapper controls (switch / check / segmented) hide their native <input>, so
122
121
  the `[aria-invalid]` the validator sets on it paints nothing — a sighted,
123
122
  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.) */
123
+ the visible surface via :has(), the same way the disabled cue is mirrored. */
126
124
  .ui-check:has(input[aria-invalid='true']) input {
127
125
  outline: 2px solid var(--danger);
128
126
  outline-offset: 1px;
@@ -141,8 +139,7 @@
141
139
  one and sighted HCM users lose the only error cue (WCAG 1.4.1). The switch got
142
140
  a forced-colors block; the error family did not. Re-assert the state on a
143
141
  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.) */
142
+ the error hint with a glyph so the message itself carries the error. */
146
143
  @media (forced-colors: active) {
147
144
  .ui-input[aria-invalid='true'],
148
145
  .ui-select[aria-invalid='true'],
@@ -151,7 +148,7 @@
151
148
  border-width: 3px;
152
149
  }
153
150
 
154
- /* Same NON-colour re-assertion for the wrapper controls (C7): a thicker,
151
+ /* Same NON-colour re-assertion for the wrapper controls: a thicker,
155
152
  doubled outline/border survives the HCM colour flattening. */
156
153
  .ui-check:has(input[aria-invalid='true']) input {
157
154
  outline-width: 3px;
@@ -205,7 +202,7 @@
205
202
 
206
203
  .ui-input-group > .ui-input:focus-visible,
207
204
  .ui-input-group > .ui-select:focus-visible {
208
- z-index: 1; /* keep the focus ring above the adjacent addon border (select too — audit C29) */
205
+ z-index: 1; /* keep the focus ring above the adjacent addon border */
209
206
  }
210
207
 
211
208
  .ui-input-group__addon {
@@ -213,7 +210,7 @@
213
210
  background: var(--panel-soft);
214
211
 
215
212
  /* 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.) */
213
+ isn't a fainter cap than the field it abuts. */
217
214
  border: 1px solid var(--line-strong);
218
215
  color: var(--text-dim);
219
216
  display: flex;
@@ -358,7 +355,7 @@
358
355
 
359
356
  /* Keyboard focus ring to match every sibling input (which use a 2px ring); the
360
357
  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.) */
358
+ :focus-visible on the inner input so it stays keyboard-only. */
362
359
  .ui-search:has(input:focus-visible) {
363
360
  outline: 2px solid var(--focus-ring);
364
361
  outline-offset: 1px;
package/css/legend.css CHANGED
@@ -93,7 +93,7 @@
93
93
  /* Glyph/symbol swatch — fill an `.ui-icon` mask with the series colour. Match the
94
94
  __swatch fallback chain exactly (--chart-color → --chart-1 → --accent): without
95
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.) */
96
+ colours whenever a theme overrides --chart-1. */
97
97
  .ui-legend__symbol {
98
98
  block-size: 0.95rem;
99
99
  color: var(--chart-color, var(--chart-1, var(--accent)));
package/css/marks.css CHANGED
@@ -91,7 +91,7 @@
91
91
  @media (prefers-reduced-motion: no-preference) {
92
92
  /* The sweep animates `background-size`, i.e. the highlight FILL — so it is
93
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.) */
94
+ than let `--draw` look applied but do nothing. */
95
95
  .ui-mark--draw:not(.ui-mark--underline, .ui-mark--box, .ui-mark--strike) {
96
96
  animation: ui-mark-draw 0.6s var(--ease, ease) both;
97
97
  }
package/css/motion.css CHANGED
@@ -57,7 +57,7 @@
57
57
  scroll far enough to finish its range, so a fade-to-opacity-1 would strand the
58
58
  content permanently transparent. Reach full opacity early (35%) and hold it,
59
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.) */
60
+ the rise are left unfinished. */
61
61
  @keyframes uiScrollReveal {
62
62
  0% {
63
63
  opacity: 0;
@@ -183,7 +183,7 @@
183
183
  /* Stagger children: set --i on each child (or use nth-child cap). Cap the manual
184
184
  --i path at index 6 (360ms) to match the .ui-stagger--auto ceiling — a long
185
185
  list with --i:30 would otherwise hold the last child at opacity:0 for 1.8s
186
- before popping in. (audit C32.) */
186
+ before popping in. */
187
187
  .ui-stagger > * {
188
188
  animation: uiRise var(--duration-slow) var(--ease-spring) both;
189
189
  animation-delay: calc(min(var(--i, 0), 6) * 60ms);
@@ -307,7 +307,7 @@
307
307
  rest to one system colour, so the ring looks uniform with no visible sweep,
308
308
  and the scroll-progress bar can vanish into the canvas. Re-assert distinct
309
309
  system colours so each keeps a visible channel; AT is already covered via
310
- aria-busy / role="status" / role="progressbar". (component audit C35.) */
310
+ aria-busy / role="status" / role="progressbar". */
311
311
  @media (forced-colors: active) {
312
312
  .ui-spinner {
313
313
  border-color: CanvasText;
@@ -321,7 +321,7 @@
321
321
  /* HCM strips the shimmer gradient, leaving an invisible box where a loading
322
322
  placeholder should be. Give it a system-colour border so the skeleton
323
323
  still reads as present. Same decorative-loading family as the spinner +
324
- scroll-progress above. (audit C15.) */
324
+ scroll-progress above. */
325
325
  .ui-skeleton {
326
326
  border: 1px solid CanvasText;
327
327
  }
@@ -346,7 +346,7 @@
346
346
  The range is `entry 0% entry 100%`, NOT the old `cover 40%`: `cover` only
347
347
  completes once the element has scrolled most of the way THROUGH the viewport,
348
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`
349
+ leaving a conclusion section permanently semi-transparent. `entry`
350
350
  completes the moment the element is fully in view, which any scroll-to-bottom
351
351
  reaches. Paired with the early-opacity uiScrollReveal keyframe, an element
352
352
  taller than the viewport is legible even if its range never fully completes. */
@@ -405,7 +405,7 @@
405
405
  /* Zero the delay too: `.ui-stagger` children animate `uiRise` with
406
406
  `fill-mode: both`, so a non-zero `animation-delay` holds them at the
407
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). */
408
+ exact late flash a reduced-motion user asked to avoid. */
409
409
  animation-delay: 0s !important;
410
410
  transition-duration: 0.01ms !important;
411
411
  }
@@ -70,6 +70,18 @@
70
70
  transform: translateX(0.6rem);
71
71
  }
72
72
 
73
+ @media (prefers-color-scheme: dark) {
74
+ :root:not([data-theme='light']) .ui-themetoggle__thumb {
75
+ background: var(--accent);
76
+ transform: translateX(0.6rem);
77
+ }
78
+
79
+ :root[dir='rtl']:not([data-theme='light']) .ui-themetoggle__thumb,
80
+ :root:not([data-theme='light']) [dir='rtl'] .ui-themetoggle__thumb {
81
+ transform: translateX(-0.6rem);
82
+ }
83
+ }
84
+
73
85
  [dir='rtl'][data-theme='dark'] .ui-themetoggle__thumb,
74
86
  [dir='rtl'] [data-theme='dark'] .ui-themetoggle__thumb {
75
87
  transform: translateX(-0.6rem);