@ponchia/ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +28 -5
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +315 -45
  7. package/behaviors/carousel.js +17 -16
  8. package/behaviors/combobox.js +47 -16
  9. package/behaviors/command.js +18 -15
  10. package/behaviors/connectors.js +4 -5
  11. package/behaviors/crosshair.js +4 -5
  12. package/behaviors/dialog.js +3 -2
  13. package/behaviors/disclosure.js +3 -2
  14. package/behaviors/dismissible.js +3 -2
  15. package/behaviors/forms.js +41 -13
  16. package/behaviors/glyph.js +4 -5
  17. package/behaviors/internal.js +47 -0
  18. package/behaviors/legend.js +23 -2
  19. package/behaviors/menu.js +3 -2
  20. package/behaviors/popover.js +78 -7
  21. package/behaviors/spotlight.js +4 -5
  22. package/behaviors/table.js +39 -12
  23. package/behaviors/tabs.js +14 -14
  24. package/behaviors/theme.js +5 -3
  25. package/behaviors/toast.js +13 -1
  26. package/classes/classes.json +1857 -0
  27. package/classes/index.d.ts +28 -13
  28. package/classes/index.js +34 -18
  29. package/classes/vscode.css-custom-data.json +12 -0
  30. package/connectors/index.d.ts +189 -69
  31. package/connectors/index.d.ts.map +1 -0
  32. package/connectors/index.js +120 -24
  33. package/css/app.css +43 -13
  34. package/css/base.css +15 -10
  35. package/css/connectors.css +17 -0
  36. package/css/content.css +7 -1
  37. package/css/dataviz.css +5 -1
  38. package/css/disclosure.css +38 -6
  39. package/css/dots.css +57 -0
  40. package/css/feedback.css +60 -2
  41. package/css/forms.css +42 -1
  42. package/css/legend.css +11 -7
  43. package/css/marks.css +38 -8
  44. package/css/motion.css +24 -44
  45. package/css/navigation.css +7 -0
  46. package/css/overlay.css +31 -1
  47. package/css/primitives.css +91 -5
  48. package/css/report.css +40 -63
  49. package/css/site.css +16 -2
  50. package/css/sources.css +43 -1
  51. package/css/spotlight.css +1 -1
  52. package/css/tokens.css +36 -1
  53. package/css/workbench.css +1 -1
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -1
  56. package/dist/css/app.css +1 -1
  57. package/dist/css/base.css +1 -1
  58. package/dist/css/connectors.css +1 -1
  59. package/dist/css/content.css +1 -1
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/forms.css +1 -1
  64. package/dist/css/legend.css +1 -1
  65. package/dist/css/marks.css +1 -1
  66. package/dist/css/motion.css +1 -1
  67. package/dist/css/navigation.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -1
  71. package/dist/css/site.css +1 -1
  72. package/dist/css/sources.css +1 -1
  73. package/dist/css/spotlight.css +1 -1
  74. package/dist/css/tokens.css +1 -1
  75. package/dist/css/workbench.css +1 -1
  76. package/docs/adr/0003-theme-model.md +1 -1
  77. package/docs/annotations.md +94 -14
  78. package/docs/architecture.md +50 -6
  79. package/docs/contrast.md +116 -92
  80. package/docs/d2.md +195 -0
  81. package/docs/legends.md +18 -2
  82. package/docs/marks.md +9 -2
  83. package/docs/mermaid.md +152 -0
  84. package/docs/reference.md +78 -22
  85. package/docs/reporting.md +395 -57
  86. package/docs/sources.md +27 -0
  87. package/docs/stability.md +9 -2
  88. package/docs/usage.md +101 -4
  89. package/docs/vega.md +225 -0
  90. package/docs/workbench.md +7 -1
  91. package/glyphs/glyphs.js +6 -4
  92. package/llms.txt +139 -14
  93. package/package.json +50 -12
  94. package/qwik/index.d.ts +42 -59
  95. package/qwik/index.d.ts.map +1 -0
  96. package/qwik/index.js +55 -3
  97. package/react/index.d.ts +39 -61
  98. package/react/index.d.ts.map +1 -0
  99. package/react/index.js +57 -3
  100. package/solid/index.d.ts +64 -61
  101. package/solid/index.d.ts.map +1 -0
  102. package/solid/index.js +60 -3
  103. package/tokens/d2.d.ts +38 -0
  104. package/tokens/d2.js +71 -0
  105. package/tokens/d2.json +43 -0
  106. package/tokens/index.d.ts +5 -5
  107. package/tokens/index.js +15 -1
  108. package/tokens/index.json +9 -0
  109. package/tokens/mermaid.d.ts +23 -0
  110. package/tokens/mermaid.js +181 -0
  111. package/tokens/mermaid.json +163 -0
  112. package/tokens/resolved.json +45 -1
  113. package/tokens/skins.js +3 -2
  114. package/tokens/tokens.dtcg.json +26 -0
  115. package/tokens/vega.d.ts +34 -0
  116. package/tokens/vega.js +155 -0
  117. package/tokens/vega.json +179 -0
@@ -10,36 +10,78 @@
10
10
  *
11
11
  * import { connectRects } from '@ponchia/ui/connectors';
12
12
  * const { d } = connectRects({ fromRect: a, toRect: b, shape: 'elbow' });
13
+ *
14
+ * The public types below are JSDoc `@typedef`s; the shipped `index.d.ts` is
15
+ * generated from them (and these signatures) by `tsc --emitDeclarationOnly`.
16
+ *
17
+ * @typedef {{ x: number, y: number }} Point
18
+ * @typedef {{ x: number, y: number, width: number, height: number }} Rect
19
+ * @typedef {'top' | 'right' | 'bottom' | 'left' | 'center'} Side
20
+ * @typedef {'straight' | 'elbow' | 'curve'} ConnectorShape
21
+ *
22
+ * @typedef {object} ConnectorPathOptions
23
+ * @property {Point} from
24
+ * @property {Point} to
25
+ * @property {ConnectorShape} [shape]
26
+ * @property {number} [curvature] Curve control-point reach along the dominant axis (curve shape). Default 0.5.
27
+ * @property {number} [mid] Turn position 0..1 along the span (elbow shape). Default 0.5.
28
+ *
29
+ * @typedef {object} ConnectRectsOptions
30
+ * @property {Rect} fromRect
31
+ * @property {Rect} toRect
32
+ * @property {Side} [fromSide] Anchor edges. Omit both to auto-pick facing edges from the rects.
33
+ * @property {Side} [toSide]
34
+ * @property {ConnectorShape} [shape]
35
+ * @property {number} [curvature]
36
+ * @property {number} [mid]
37
+ *
38
+ * @typedef {object} ConnectRectsResult
39
+ * @property {string} d SVG path data.
40
+ * @property {Point} from
41
+ * @property {Point} to
42
+ * @property {number} angle The path's end-tangent at `to` in radians — the direction the path arrives, so rotating an arrowhead at `to` by this points it along the path. Equals the straight `from`→`to` angle for `shape: 'straight'`; axis-aligned for `elbow`/`curve`.
13
43
  */
14
44
 
15
- const PRECISION = 1000;
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.
49
+ export const PRECISION = 1000;
16
50
 
17
- function finite(name, value, fallback) {
51
+ export function finite(name, value, fallback) {
18
52
  const v = value ?? fallback;
19
53
  if (!Number.isFinite(v)) throw new TypeError(`${name} must be a finite number`);
20
54
  return v;
21
55
  }
22
56
 
23
- function dimension(name, value, fallback) {
57
+ export function dimension(name, value, fallback) {
24
58
  const v = finite(name, value, fallback);
25
59
  if (v < 0) throw new RangeError(`${name} must be greater than or equal to 0`);
26
60
  return v;
27
61
  }
28
62
 
29
- function fmt(value) {
63
+ export function fmt(value) {
30
64
  const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
31
65
  return String(Object.is(rounded, -0) ? 0 : rounded);
32
66
  }
33
67
 
34
- function point(x, y) {
68
+ export function point(x, y) {
35
69
  return `${fmt(x)},${fmt(y)}`;
36
70
  }
37
71
 
38
- function clamp(value, min, max) {
72
+ // Guarded form (returns min when the range is inverted) — the reconciled body;
73
+ // connectors only ever calls clamp(v, 0, 1) so this is output-identical here.
74
+ export function clamp(value, min, max) {
75
+ if (max < min) return min;
39
76
  return Math.min(max, Math.max(min, value));
40
77
  }
41
78
 
42
- /** A point on a rect's edge (or centre). `rect` is `{ x, y, width, height }`. */
79
+ /**
80
+ * A point on a rect's edge (or centre). `rect` is `{ x, y, width, height }`.
81
+ * @param {Rect} rect
82
+ * @param {Side} [side]
83
+ * @returns {Point}
84
+ */
43
85
  export function anchorPoint(rect, side = 'center') {
44
86
  const x = finite('rect.x', rect?.x, 0);
45
87
  const y = finite('rect.y', rect?.y, 0);
@@ -60,7 +102,12 @@ export function anchorPoint(rect, side = 'center') {
60
102
  }
61
103
  }
62
104
 
63
- /** Angle (radians) from `from` to `to`. */
105
+ /**
106
+ * Angle (radians) from `from` to `to`.
107
+ * @param {Point} from
108
+ * @param {Point} to
109
+ * @returns {number}
110
+ */
64
111
  export function angleBetween(from, to) {
65
112
  return Math.atan2(
66
113
  finite('to.y', to?.y) - finite('from.y', from?.y),
@@ -68,6 +115,12 @@ export function angleBetween(from, to) {
68
115
  );
69
116
  }
70
117
 
118
+ /**
119
+ * Straight line from `from` to `to`.
120
+ * @param {Point} from
121
+ * @param {Point} to
122
+ * @returns {string}
123
+ */
71
124
  export function straightPath(from, to) {
72
125
  return `M${point(finite('from.x', from?.x), finite('from.y', from?.y))}L${point(
73
126
  finite('to.x', to?.x),
@@ -75,7 +128,13 @@ export function straightPath(from, to) {
75
128
  )}`;
76
129
  }
77
130
 
78
- /** Right-angle dogleg. Turns on the dominant axis at `mid` (0..1) of the span. */
131
+ /**
132
+ * Right-angle dogleg. Turns on the dominant axis at `mid` (0..1) of the span.
133
+ * @param {Point} from
134
+ * @param {Point} to
135
+ * @param {{ mid?: number }} [opts]
136
+ * @returns {string}
137
+ */
79
138
  export function elbowPath(from, to, opts = {}) {
80
139
  const fx = finite('from.x', from?.x);
81
140
  const fy = finite('from.y', from?.y);
@@ -92,7 +151,13 @@ export function elbowPath(from, to, opts = {}) {
92
151
  return `M${point(fx, fy)}V${fmt(my)}H${fmt(tx)}V${fmt(ty)}`;
93
152
  }
94
153
 
95
- /** Cubic curve; control points extend along the dominant axis by `curvature`. */
154
+ /**
155
+ * Cubic curve; control points extend along the dominant axis by `curvature`.
156
+ * @param {Point} from
157
+ * @param {Point} to
158
+ * @param {{ curvature?: number }} [opts]
159
+ * @returns {string}
160
+ */
96
161
  export function curvePath(from, to, opts = {}) {
97
162
  const fx = finite('from.x', from?.x);
98
163
  const fy = finite('from.y', from?.y);
@@ -107,7 +172,11 @@ export function curvePath(from, to, opts = {}) {
107
172
  return `M${point(fx, fy)}C${point(c1.x, c1.y)} ${point(c2.x, c2.y)} ${point(tx, ty)}`;
108
173
  }
109
174
 
110
- /** Build a path between two points by `shape` (`straight` | `elbow` | `curve`). */
175
+ /**
176
+ * Build a path between two points by `shape` (`straight` | `elbow` | `curve`).
177
+ * @param {ConnectorPathOptions} [opts]
178
+ * @returns {string}
179
+ */
111
180
  export function connectorPath(opts = {}) {
112
181
  const { from, to, shape = 'straight' } = opts;
113
182
  if (shape === 'elbow') return elbowPath(from, to, opts);
@@ -115,20 +184,34 @@ export function connectorPath(opts = {}) {
115
184
  return straightPath(from, to);
116
185
  }
117
186
 
118
- /** A filled triangle arrowhead at `p`, pointing along `angle` (radians). */
119
- export function arrowHead(p, angle, size = 8) {
187
+ /**
188
+ * A filled triangle arrowhead at `p`, pointing along `angle` (radians).
189
+ * @param {Point} p
190
+ * @param {number} angle
191
+ * @param {number} [size]
192
+ * @param {number} [spread] Half-angle of the head in radians (default 0.45).
193
+ * Smaller is crisper/sharper; must be in (0, π/2).
194
+ * @returns {string}
195
+ */
196
+ export function arrowHead(p, angle, size = 8, spread = 0.45) {
120
197
  const px = finite('p.x', p?.x);
121
198
  const py = finite('p.y', p?.y);
122
199
  const a = finite('angle', angle, 0);
123
200
  const s = dimension('size', size, 8);
201
+ const sp = finite('spread', spread, 0.45);
202
+ if (sp <= 0 || sp >= Math.PI / 2) throw new RangeError('spread must be in (0, π/2)');
124
203
  const back = a + Math.PI;
125
- const spread = 0.45;
126
- const p1 = { x: px + Math.cos(back - spread) * s, y: py + Math.sin(back - spread) * s };
127
- const p2 = { x: px + Math.cos(back + spread) * s, y: py + Math.sin(back + spread) * s };
204
+ const p1 = { x: px + Math.cos(back - sp) * s, y: py + Math.sin(back - sp) * s };
205
+ const p2 = { x: px + Math.cos(back + sp) * s, y: py + Math.sin(back + sp) * s };
128
206
  return `M${point(px, py)}L${point(p1.x, p1.y)}L${point(p2.x, p2.y)}Z`;
129
207
  }
130
208
 
131
- /** A filled dot at `p`. */
209
+ /**
210
+ * A filled dot at `p`.
211
+ * @param {Point} p
212
+ * @param {number} [radius]
213
+ * @returns {string}
214
+ */
132
215
  export function dotMark(p, radius = 3) {
133
216
  const px = finite('p.x', p?.x);
134
217
  const py = finite('p.y', p?.y);
@@ -139,7 +222,12 @@ export function dotMark(p, radius = 3) {
139
222
  )} 0 1 1 ${point(px, py - r)}Z`;
140
223
  }
141
224
 
142
- /** Pick facing edges from the rects' relative centres. */
225
+ /**
226
+ * Pick facing edges from the rects' relative centres.
227
+ * @param {Rect} fromRect
228
+ * @param {Rect} toRect
229
+ * @returns {{ from: Side, to: Side }}
230
+ */
143
231
  export function autoSides(fromRect, toRect) {
144
232
  const fc = anchorPoint(fromRect, 'center');
145
233
  const tc = anchorPoint(toRect, 'center');
@@ -152,13 +240,14 @@ export function autoSides(fromRect, toRect) {
152
240
  }
153
241
 
154
242
  /**
155
- * Connect two rects. Resolves anchor points (explicit `fromSide`/`toSide`, else
156
- * auto), builds the path, and returns `{ d, from, to, angle }` so the caller can
157
- * place an arrowhead/dot at `to` rotated by `angle`.
243
+ * Angle (radians) at which a `shape` path *arrives* at `to` — straight is the
244
+ * chord; elbow/curve arrive axis-aligned along the dominant axis. Rotate an
245
+ * end marker by this so it points along the path, not the chord.
246
+ * @param {Point} from
247
+ * @param {Point} to
248
+ * @param {ConnectorShape} [shape]
249
+ * @returns {number}
158
250
  */
159
- /** Angle (radians) at which a `shape` path *arrives* at `to` — straight is the
160
- * chord; elbow/curve arrive axis-aligned along the dominant axis. Rotate an
161
- * end marker by this so it points along the path, not the chord. */
162
251
  export function endTangentAngle(from, to, shape = 'straight') {
163
252
  if (shape === 'straight') return angleBetween(from, to);
164
253
  const dx = finite('to.x', to?.x) - finite('from.x', from?.x);
@@ -167,6 +256,13 @@ export function endTangentAngle(from, to, shape = 'straight') {
167
256
  return dy >= 0 ? Math.PI / 2 : -Math.PI / 2;
168
257
  }
169
258
 
259
+ /**
260
+ * Connect two rects. Resolves anchor points (explicit `fromSide`/`toSide`, else
261
+ * auto), builds the path, and returns `{ d, from, to, angle }` so the caller can
262
+ * place an arrowhead/dot at `to` rotated by `angle`.
263
+ * @param {ConnectRectsOptions} [opts]
264
+ * @returns {ConnectRectsResult}
265
+ */
170
266
  export function connectRects(opts = {}) {
171
267
  const { fromRect, toRect, shape = 'straight', curvature, mid } = opts;
172
268
  // Honor each side override independently; auto-pick whichever is unset.
package/css/app.css CHANGED
@@ -6,7 +6,7 @@
6
6
  .ui-app-shell {
7
7
  display: grid;
8
8
  grid-template-columns: var(--app-rail, 14rem) minmax(0, 1fr);
9
- min-block-size: 100vh;
9
+ min-block-size: 100dvh;
10
10
  }
11
11
 
12
12
  .ui-app-shell--full {
@@ -24,7 +24,7 @@
24
24
  padding: var(--space-md);
25
25
  position: sticky;
26
26
  inset-block-start: 0;
27
- block-size: 100vh;
27
+ block-size: 100svh;
28
28
  overflow-y: auto;
29
29
  }
30
30
 
@@ -34,6 +34,7 @@
34
34
  display: flex;
35
35
  font-family: var(--display);
36
36
  font-size: 1.05rem;
37
+ font-weight: var(--display-weight);
37
38
  gap: 0.5rem;
38
39
  letter-spacing: var(--tracking-wide);
39
40
  padding: 0.35rem 0.5rem;
@@ -87,18 +88,33 @@
87
88
  inline-size: 0.34rem;
88
89
  }
89
90
 
90
- .ui-app-nav a:hover {
91
- background: var(--bg-accent);
92
- color: var(--text);
91
+ @media (hover: hover) {
92
+ .ui-app-nav a:hover {
93
+ background: var(--bg-accent);
94
+ color: var(--text);
95
+ }
93
96
  }
94
97
 
95
- .ui-app-nav a.is-active {
98
+ /* Current page honours BOTH the `.is-active` class and `aria-current` — the
99
+ sibling `.ui-sitenav` signals current-page with `aria-current="page"`, so the
100
+ app-shell nav now accepts the same programmatic cue (author it for AT, not
101
+ just the visual class — C19). */
102
+ .ui-app-nav a.is-active,
103
+ .ui-app-nav a[aria-current]:not([aria-current='false']) {
96
104
  background: var(--accent-soft);
97
105
  border-inline-start-color: var(--accent);
98
106
  color: var(--accent-text);
99
107
  }
100
108
 
101
- .ui-app-nav a.is-active::before {
109
+ @media (pointer: coarse) {
110
+ .ui-app-nav a {
111
+ min-block-size: 2.9rem;
112
+ padding-inline: 0.9rem;
113
+ }
114
+ }
115
+
116
+ .ui-app-nav a.is-active::before,
117
+ .ui-app-nav a[aria-current]:not([aria-current='false'])::before {
102
118
  opacity: 1;
103
119
  }
104
120
 
@@ -153,6 +169,7 @@
153
169
  color: var(--text);
154
170
  font-family: var(--display);
155
171
  font-size: 1.1rem;
172
+ font-weight: var(--display-weight);
156
173
  letter-spacing: var(--tracking-wide);
157
174
  margin: 0;
158
175
  text-transform: uppercase;
@@ -207,6 +224,7 @@
207
224
  color: var(--text);
208
225
  font-family: var(--display);
209
226
  font-size: 0.95rem;
227
+ font-weight: var(--display-weight);
210
228
  letter-spacing: var(--tracking-wide);
211
229
  margin: 0;
212
230
  text-transform: uppercase;
@@ -230,11 +248,7 @@
230
248
  permanent admin-shell alias (grouped on the canonical rules there —
231
249
  identical output). Nothing app-specific left to define here. */
232
250
 
233
- /* --- Mobile rail collapse --- */
234
-
235
- .ui-app-rail__toggle {
236
- display: none;
237
- }
251
+ /* --- Mobile rail: collapses to a horizontal scrolling strip --- */
238
252
 
239
253
  @media (max-width: 880px) {
240
254
  .ui-app-shell {
@@ -254,6 +268,15 @@
254
268
  display: none;
255
269
  }
256
270
 
271
+ /* The rail is flex-row here, so the base `margin-block-start: auto` (which
272
+ pushed account to the bottom in the column layout) is inert and account can
273
+ scroll off. Push it to the inline end instead so sign-out stays reachable.
274
+ (layout review C21.) */
275
+ .ui-app-rail__account {
276
+ margin-block-start: 0;
277
+ margin-inline-start: auto;
278
+ }
279
+
257
280
  .ui-app-nav {
258
281
  grid-auto-flow: column;
259
282
  gap: 0.15rem;
@@ -269,11 +292,18 @@
269
292
  white-space: nowrap;
270
293
  }
271
294
 
272
- .ui-app-nav a.is-active {
295
+ .ui-app-nav a.is-active,
296
+ .ui-app-nav a[aria-current]:not([aria-current='false']) {
273
297
  border-inline-start: 0;
274
298
  border-block-end-color: var(--accent);
275
299
  }
276
300
 
301
+ /* No --app-rail-height token exists, so drop the redundant second sticky;
302
+ only the horizontal rail stays pinned. */
303
+ .ui-app-topbar {
304
+ position: static;
305
+ }
306
+
277
307
  .ui-app-content {
278
308
  padding: var(--space-md);
279
309
  }
package/css/base.css CHANGED
@@ -12,7 +12,10 @@ html {
12
12
  background: var(--bg);
13
13
  color: var(--text);
14
14
  font-family: var(--sans);
15
- font-size: 15px;
15
+
16
+ /* = 15px at the 16px default, but rem honours a user's browser font-size
17
+ preference (which a px root would ignore). */
18
+ font-size: 0.9375rem;
16
19
  line-height: 1.55;
17
20
  scroll-behavior: smooth;
18
21
  text-rendering: optimizeLegibility;
@@ -24,12 +27,16 @@ body {
24
27
  background: var(--bg);
25
28
  color: var(--text);
26
29
  margin: 0;
27
- min-block-size: 100vh;
30
+ min-block-size: 100dvh;
28
31
  position: relative;
29
32
  }
30
33
 
34
+ :root {
35
+ --scroll-offset: 6rem;
36
+ }
37
+
31
38
  main [id] {
32
- scroll-margin-top: 6rem;
39
+ scroll-margin-block-start: var(--scroll-offset);
33
40
  }
34
41
 
35
42
  ::selection {
@@ -176,6 +183,7 @@ textarea:focus-visible,
176
183
 
177
184
  .ui-display {
178
185
  font-family: var(--display);
186
+ font-weight: var(--display-weight);
179
187
  text-transform: uppercase;
180
188
  }
181
189
 
@@ -219,13 +227,10 @@ textarea:focus-visible,
219
227
  outline-offset: -2px;
220
228
  }
221
229
 
222
- /* The active tab signals selection only via accent colour/border, both
223
- flattened by the forced palettere-assert it with a system colour
224
- so the selected tab stays distinguishable (a11y review L3). */
225
- .ui-tab.is-active {
226
- border-block-end-color: Highlight;
227
- color: Highlight;
228
- }
230
+ /* NB: the active-tab forced-colors re-assert lives in disclosure.css, right
231
+ after the `.ui-tab.is-active` defaultplacing it here (an earlier bundle
232
+ leaf) let the later default win even in forced-colors mode, since @media
233
+ adds no specificity. (a11y review C10.) */
229
234
 
230
235
  /* Keyboard focus must never depend on a colour that gets overridden. */
231
236
  a:focus-visible,
@@ -91,3 +91,20 @@
91
91
  fill: CanvasText;
92
92
  }
93
93
  }
94
+
95
+ /* Print: keep the relationship lines and arrowheads in their meaning-carrying
96
+ colour, and settle any draw-on to a solid line (the draw needs JS to set
97
+ pathLength, which never runs for a static PDF). */
98
+ @media print {
99
+ .ui-connector__path,
100
+ .ui-connector__end {
101
+ -webkit-print-color-adjust: exact;
102
+ print-color-adjust: exact;
103
+ }
104
+
105
+ .ui-connector--draw .ui-connector__path {
106
+ animation: none;
107
+ stroke-dasharray: none;
108
+ stroke-dashoffset: 0;
109
+ }
110
+ }
package/css/content.css CHANGED
@@ -13,6 +13,11 @@
13
13
  font-size: var(--text-base);
14
14
  line-height: 1.7;
15
15
 
16
+ /* Machine-generated Markdown carries long unbreakable tokens (URLs, hashes,
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.) */
19
+ overflow-wrap: break-word;
20
+
16
21
  /* Readable measure; the container can still be wider for tables/media. */
17
22
  --prose-measure: 72ch;
18
23
  }
@@ -75,7 +80,7 @@
75
80
 
76
81
  /* Anchored headings clear a sticky chrome and expose a dot on hover. */
77
82
  .ui-prose :is(h1, h2, h3, h4, h5, h6)[id] {
78
- scroll-margin-block-start: 6rem;
83
+ scroll-margin-block-start: var(--scroll-offset);
79
84
  }
80
85
 
81
86
  /* --- Text-level --- */
@@ -300,6 +305,7 @@
300
305
  display: grid;
301
306
  font-family: var(--display);
302
307
  font-size: var(--text-xl);
308
+ font-weight: var(--display-weight);
303
309
  gap: var(--space-sm);
304
310
  line-height: 1.2;
305
311
  margin-block: var(--space-xl);
package/css/dataviz.css CHANGED
@@ -30,7 +30,11 @@
30
30
  --chart-div-6: oklch(66% 0.13 55deg);
31
31
  --chart-div-7: oklch(56% 0.15 45deg);
32
32
 
33
- /* Dot-matrix pattern fills — pair with the matching colour (WCAG 1.4.1). */
33
+ /* Dot-matrix pattern fills — pair with the matching colour (WCAG 1.4.1).
34
+ Series 1 (the accent) is intentionally `none`: absence-of-pattern IS its
35
+ redundant channel. The colour palette is CVD-safe on its own (gated by
36
+ check:charts), so a colour+pattern chart stays distinguishable; a
37
+ pattern-ONLY chart must still give series 1 a fill or a labelled legend. */
34
38
  --chart-pattern-size: 8px;
35
39
  --chart-pattern-ink: rgb(0 0 0 / 0.34);
36
40
  --chart-pattern-1: none;
@@ -44,11 +44,29 @@
44
44
  border-color var(--duration-fast) var(--ease-standard);
45
45
  }
46
46
 
47
+ /* Coarse-pointer tap-target floor (~2.9rem ≈ 44px), matching inputs/sitenav —
48
+ tabs are easy to mis-tap on touch (WCAG 2.5.8 — C24). */
49
+ @media (pointer: coarse) {
50
+ .ui-tab {
51
+ min-block-size: 2.9rem;
52
+ }
53
+ }
54
+
47
55
  .ui-tab.is-active {
48
56
  border-block-end-color: var(--accent);
49
57
  color: var(--accent-text);
50
58
  }
51
59
 
60
+ /* Forced-colors re-assert MUST sit after the default above (same specificity,
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.) */
63
+ @media (forced-colors: active) {
64
+ .ui-tab.is-active {
65
+ border-block-end-color: Highlight;
66
+ color: Highlight;
67
+ }
68
+ }
69
+
52
70
  .ui-tab:focus-visible {
53
71
  /* An inset box, deliberately distinct from the .is-active bottom-border
54
72
  so focus ≠ selection for low-vision keyboard users: the global
@@ -199,7 +217,7 @@
199
217
 
200
218
  .ui-segmented__option:has(input:focus-visible) {
201
219
  outline: 2px solid var(--focus-ring);
202
- outline-offset: 2px;
220
+ outline-offset: -2px;
203
221
  }
204
222
 
205
223
  @media (hover: hover) {
@@ -240,7 +258,7 @@
240
258
  inline-size: 0.22rem;
241
259
  }
242
260
 
243
- .ui-breadcrumb__item[aria-current] {
261
+ .ui-breadcrumb__item[aria-current]:not([aria-current='false']) {
244
262
  color: var(--text);
245
263
  }
246
264
 
@@ -290,18 +308,32 @@
290
308
  color: var(--accent-text);
291
309
  }
292
310
 
293
- .ui-pagination__item[aria-disabled='true'] {
311
+ /* Disabled covers BOTH the native `<button disabled>` the demo ships and the
312
+ `aria-disabled="true"` ARIA path; `pointer-events: none` makes the
313
+ aria-disabled item genuinely inert (it was clickable before — C5). */
314
+ .ui-pagination__item[aria-disabled='true'],
315
+ .ui-pagination__item:disabled {
294
316
  cursor: not-allowed;
295
317
  opacity: 0.4;
318
+ pointer-events: none;
296
319
  }
297
320
 
298
321
  @media (hover: hover) {
299
- .ui-pagination__item:not(.is-active, [aria-disabled='true']):hover {
322
+ .ui-pagination__item:not(.is-active, [aria-disabled='true'], :disabled):hover {
300
323
  border-color: var(--line-strong);
301
324
  color: var(--text);
302
325
  }
303
326
  }
304
327
 
328
+ /* Coarse-pointer tap-target floor, matching inputs/sitenav (~2.9rem ≈ 44px) —
329
+ pagination controls are easy to mis-tap on touch (WCAG 2.5.8 — C24). */
330
+ @media (pointer: coarse) {
331
+ .ui-pagination__item {
332
+ min-block-size: 2.9rem;
333
+ min-inline-size: 2.9rem;
334
+ }
335
+ }
336
+
305
337
  /* --- Avatar — sharp, monospace initials or image --- */
306
338
 
307
339
  .ui-avatar {
@@ -429,8 +461,8 @@
429
461
  .ui-carousel__prev::before,
430
462
  .ui-carousel__next::before {
431
463
  block-size: 0.5rem;
432
- border-block-start: 1.5px solid currentColor;
433
- border-inline-end: 1.5px solid currentColor;
464
+ border-block-start: 1.5px solid currentcolor;
465
+ border-inline-end: 1.5px solid currentcolor;
434
466
  content: '';
435
467
  inline-size: 0.5rem;
436
468
  }
package/css/dots.css CHANGED
@@ -281,6 +281,13 @@
281
281
  transform: rotate(315deg) translateY(calc(var(--ds-box) / -2 + var(--ds-dot) / 2));
282
282
  }
283
283
 
284
+ /* The comet expects exactly 8 `<i>`; a 9th+ child has no rotation rule and
285
+ would pile up dead-centre. Hide the overflow so extra children fail safe
286
+ rather than rendering a stray static dot (C26). */
287
+ .ui-dotspinner i:nth-child(n + 9) {
288
+ display: none;
289
+ }
290
+
284
291
  .ui-dotspinner--sm {
285
292
  --ds-box: 1.05rem;
286
293
  --ds-dot: 0.18rem;
@@ -370,3 +377,53 @@
370
377
  clip-path: none;
371
378
  }
372
379
  }
380
+
381
+ /* Forced-colors (Windows HCM): semantic status dots and hot/accent matrix
382
+ cells encode meaning purely via background-color, which collapses to one
383
+ system colour under HCM (WCAG 1.4.1). Opt out of forced-color remapping and
384
+ apply four visually-distinct system colours so the signals stay
385
+ differentiable. */
386
+ @media (forced-colors: active) {
387
+ .ui-dot--success {
388
+ forced-color-adjust: none;
389
+ background: LinkText;
390
+ }
391
+
392
+ .ui-dot--warning {
393
+ forced-color-adjust: none;
394
+ background: Mark;
395
+ }
396
+
397
+ .ui-dot--danger {
398
+ forced-color-adjust: none;
399
+ background: Highlight;
400
+ }
401
+
402
+ .ui-dot--info {
403
+ forced-color-adjust: none;
404
+ background: ButtonText;
405
+ }
406
+
407
+ .ui-dotmatrix__cell--hot {
408
+ forced-color-adjust: none;
409
+ background: Highlight;
410
+ }
411
+
412
+ .ui-dotmatrix__cell--accent {
413
+ forced-color-adjust: none;
414
+ background: LinkText;
415
+ }
416
+ }
417
+
418
+ /* 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. */
421
+ @media print {
422
+ .ui-dotmatrix__cell,
423
+ .ui-dotbar i,
424
+ .ui-dot,
425
+ .ui-icon {
426
+ -webkit-print-color-adjust: exact;
427
+ print-color-adjust: exact;
428
+ }
429
+ }