@ponchia/ui 0.4.1 → 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 (153) hide show
  1. package/CHANGELOG.md +552 -8
  2. package/MIGRATIONS.json +106 -0
  3. package/README.md +34 -8
  4. package/annotations/index.d.ts +402 -0
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +792 -0
  7. package/behaviors/carousel.js +198 -0
  8. package/behaviors/combobox.js +226 -0
  9. package/behaviors/command.js +190 -0
  10. package/behaviors/connectors.js +95 -0
  11. package/behaviors/crosshair.js +57 -0
  12. package/behaviors/dialog.js +74 -0
  13. package/behaviors/disclosure.js +26 -0
  14. package/behaviors/dismissible.js +25 -0
  15. package/behaviors/forms.js +186 -0
  16. package/behaviors/glyph.js +108 -0
  17. package/behaviors/index.d.ts +79 -0
  18. package/behaviors/index.js +18 -1409
  19. package/behaviors/internal.js +97 -0
  20. package/behaviors/legend.js +67 -0
  21. package/behaviors/menu.js +47 -0
  22. package/behaviors/popover.js +179 -0
  23. package/behaviors/spotlight.js +52 -0
  24. package/behaviors/table.js +136 -0
  25. package/behaviors/tabs.js +103 -0
  26. package/behaviors/theme.js +84 -0
  27. package/behaviors/toast.js +164 -0
  28. package/classes/classes.json +1857 -0
  29. package/classes/index.d.ts +306 -13
  30. package/classes/index.js +339 -12
  31. package/classes/vscode.css-custom-data.json +12 -0
  32. package/connectors/index.d.ts +191 -0
  33. package/connectors/index.d.ts.map +1 -0
  34. package/connectors/index.js +275 -0
  35. package/css/analytical.css +21 -0
  36. package/css/annotations.css +292 -0
  37. package/css/app.css +43 -13
  38. package/css/base.css +15 -10
  39. package/css/command.css +97 -0
  40. package/css/connectors.css +110 -0
  41. package/css/content.css +7 -1
  42. package/css/crosshair.css +100 -0
  43. package/css/dataviz.css +5 -1
  44. package/css/disclosure.css +38 -6
  45. package/css/dots.css +57 -0
  46. package/css/feedback.css +111 -2
  47. package/css/fonts.css +11 -7
  48. package/css/forms.css +42 -1
  49. package/css/generated.css +117 -0
  50. package/css/legend.css +272 -0
  51. package/css/marks.css +174 -0
  52. package/css/motion.css +24 -44
  53. package/css/navigation.css +7 -0
  54. package/css/overlay.css +31 -1
  55. package/css/primitives.css +109 -5
  56. package/css/report.css +39 -81
  57. package/css/selection.css +46 -0
  58. package/css/site.css +16 -2
  59. package/css/sources.css +221 -0
  60. package/css/spotlight.css +104 -0
  61. package/css/state.css +121 -0
  62. package/css/tokens.css +60 -37
  63. package/css/workbench.css +83 -0
  64. package/dist/bronto.css +1 -1
  65. package/dist/css/analytical.css +1 -0
  66. package/dist/css/annotations.css +1 -0
  67. package/dist/css/app.css +1 -1
  68. package/dist/css/base.css +1 -1
  69. package/dist/css/command.css +1 -0
  70. package/dist/css/connectors.css +1 -0
  71. package/dist/css/content.css +1 -1
  72. package/dist/css/crosshair.css +1 -0
  73. package/dist/css/disclosure.css +1 -1
  74. package/dist/css/dots.css +1 -1
  75. package/dist/css/feedback.css +1 -1
  76. package/dist/css/fonts.css +1 -1
  77. package/dist/css/forms.css +1 -1
  78. package/dist/css/generated.css +1 -0
  79. package/dist/css/legend.css +1 -0
  80. package/dist/css/marks.css +1 -0
  81. package/dist/css/motion.css +1 -1
  82. package/dist/css/navigation.css +1 -1
  83. package/dist/css/overlay.css +1 -1
  84. package/dist/css/primitives.css +1 -1
  85. package/dist/css/report.css +1 -1
  86. package/dist/css/selection.css +1 -0
  87. package/dist/css/site.css +1 -1
  88. package/dist/css/sources.css +1 -0
  89. package/dist/css/spotlight.css +1 -0
  90. package/dist/css/state.css +1 -0
  91. package/dist/css/tokens.css +1 -1
  92. package/dist/css/workbench.css +1 -0
  93. package/docs/adr/0003-theme-model.md +7 -4
  94. package/docs/annotations.md +425 -0
  95. package/docs/architecture.md +246 -0
  96. package/docs/command.md +95 -0
  97. package/docs/connectors.md +91 -0
  98. package/docs/contrast.md +116 -92
  99. package/docs/crosshair.md +63 -0
  100. package/docs/d2.md +195 -0
  101. package/docs/generated.md +91 -0
  102. package/docs/legends.md +184 -0
  103. package/docs/marks.md +93 -0
  104. package/docs/mermaid.md +152 -0
  105. package/docs/reference.md +385 -23
  106. package/docs/reporting.md +436 -63
  107. package/docs/selection.md +40 -0
  108. package/docs/sources.md +137 -0
  109. package/docs/spotlight.md +78 -0
  110. package/docs/stability.md +24 -2
  111. package/docs/state.md +85 -0
  112. package/docs/usage.md +123 -4
  113. package/docs/vega.md +225 -0
  114. package/docs/workbench.md +78 -0
  115. package/fonts/doto-400.woff2 +0 -0
  116. package/fonts/doto-500.woff2 +0 -0
  117. package/fonts/doto-600.woff2 +0 -0
  118. package/fonts/doto-700.woff2 +0 -0
  119. package/fonts/doto-800.woff2 +0 -0
  120. package/fonts/doto-900.woff2 +0 -0
  121. package/glyphs/glyphs.js +6 -4
  122. package/llms.txt +362 -14
  123. package/package.json +115 -12
  124. package/qwik/index.d.ts +42 -54
  125. package/qwik/index.d.ts.map +1 -0
  126. package/qwik/index.js +75 -3
  127. package/react/index.d.ts +39 -56
  128. package/react/index.d.ts.map +1 -0
  129. package/react/index.js +67 -3
  130. package/solid/index.d.ts +64 -56
  131. package/solid/index.d.ts.map +1 -0
  132. package/solid/index.js +70 -3
  133. package/tokens/d2.d.ts +38 -0
  134. package/tokens/d2.js +71 -0
  135. package/tokens/d2.json +43 -0
  136. package/tokens/index.d.ts +5 -5
  137. package/tokens/index.js +23 -5
  138. package/tokens/index.json +9 -0
  139. package/tokens/mermaid.d.ts +23 -0
  140. package/tokens/mermaid.js +181 -0
  141. package/tokens/mermaid.json +163 -0
  142. package/tokens/resolved.json +45 -1
  143. package/tokens/skins.js +3 -2
  144. package/tokens/tokens.dtcg.json +26 -0
  145. package/tokens/vega.d.ts +34 -0
  146. package/tokens/vega.js +155 -0
  147. package/tokens/vega.json +179 -0
  148. package/fonts/doto-400.ttf +0 -0
  149. package/fonts/doto-500.ttf +0 -0
  150. package/fonts/doto-600.ttf +0 -0
  151. package/fonts/doto-700.ttf +0 -0
  152. package/fonts/doto-800.ttf +0 -0
  153. package/fonts/doto-900.ttf +0 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * @ponchia/ui/connectors — dependency-free SVG geometry for connecting two
3
+ * elements (or two points) with a leader line.
4
+ *
5
+ * Pure functions only: they take points/rects and return SVG path strings (or
6
+ * resolved coordinates). They own no DOM, no scales, and no live tracking —
7
+ * that optional glue lives in `@ponchia/ui/behaviors` (`initConnectors`). This
8
+ * is the page-coordinate, element-to-element cousin of the figure-coordinate
9
+ * `@ponchia/ui/annotations` helpers.
10
+ *
11
+ * import { connectRects } from '@ponchia/ui/connectors';
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`.
43
+ */
44
+
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;
50
+
51
+ export function finite(name, value, fallback) {
52
+ const v = value ?? fallback;
53
+ if (!Number.isFinite(v)) throw new TypeError(`${name} must be a finite number`);
54
+ return v;
55
+ }
56
+
57
+ export function dimension(name, value, fallback) {
58
+ const v = finite(name, value, fallback);
59
+ if (v < 0) throw new RangeError(`${name} must be greater than or equal to 0`);
60
+ return v;
61
+ }
62
+
63
+ export function fmt(value) {
64
+ const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
65
+ return String(Object.is(rounded, -0) ? 0 : rounded);
66
+ }
67
+
68
+ export function point(x, y) {
69
+ return `${fmt(x)},${fmt(y)}`;
70
+ }
71
+
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;
76
+ return Math.min(max, Math.max(min, value));
77
+ }
78
+
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
+ */
85
+ export function anchorPoint(rect, side = 'center') {
86
+ const x = finite('rect.x', rect?.x, 0);
87
+ const y = finite('rect.y', rect?.y, 0);
88
+ const w = dimension('rect.width', rect?.width, 0);
89
+ const h = dimension('rect.height', rect?.height, 0);
90
+ switch (side) {
91
+ case 'top':
92
+ return { x: x + w / 2, y };
93
+ case 'bottom':
94
+ return { x: x + w / 2, y: y + h };
95
+ case 'left':
96
+ return { x, y: y + h / 2 };
97
+ case 'right':
98
+ return { x: x + w, y: y + h / 2 };
99
+ case 'center':
100
+ default:
101
+ return { x: x + w / 2, y: y + h / 2 };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Angle (radians) from `from` to `to`.
107
+ * @param {Point} from
108
+ * @param {Point} to
109
+ * @returns {number}
110
+ */
111
+ export function angleBetween(from, to) {
112
+ return Math.atan2(
113
+ finite('to.y', to?.y) - finite('from.y', from?.y),
114
+ finite('to.x', to?.x) - finite('from.x', from?.x),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Straight line from `from` to `to`.
120
+ * @param {Point} from
121
+ * @param {Point} to
122
+ * @returns {string}
123
+ */
124
+ export function straightPath(from, to) {
125
+ return `M${point(finite('from.x', from?.x), finite('from.y', from?.y))}L${point(
126
+ finite('to.x', to?.x),
127
+ finite('to.y', to?.y),
128
+ )}`;
129
+ }
130
+
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
+ */
138
+ export function elbowPath(from, to, opts = {}) {
139
+ const fx = finite('from.x', from?.x);
140
+ const fy = finite('from.y', from?.y);
141
+ const tx = finite('to.x', to?.x);
142
+ const ty = finite('to.y', to?.y);
143
+ const mid = clamp(finite('mid', opts.mid, 0.5), 0, 1);
144
+ const dx = tx - fx;
145
+ const dy = ty - fy;
146
+ if (Math.abs(dx) >= Math.abs(dy)) {
147
+ const mx = fx + dx * mid;
148
+ return `M${point(fx, fy)}H${fmt(mx)}V${fmt(ty)}H${fmt(tx)}`;
149
+ }
150
+ const my = fy + dy * mid;
151
+ return `M${point(fx, fy)}V${fmt(my)}H${fmt(tx)}V${fmt(ty)}`;
152
+ }
153
+
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
+ */
161
+ export function curvePath(from, to, opts = {}) {
162
+ const fx = finite('from.x', from?.x);
163
+ const fy = finite('from.y', from?.y);
164
+ const tx = finite('to.x', to?.x);
165
+ const ty = finite('to.y', to?.y);
166
+ const k = finite('curvature', opts.curvature, 0.5);
167
+ const dx = tx - fx;
168
+ const dy = ty - fy;
169
+ const horizontal = Math.abs(dx) >= Math.abs(dy);
170
+ const c1 = horizontal ? { x: fx + dx * k, y: fy } : { x: fx, y: fy + dy * k };
171
+ const c2 = horizontal ? { x: tx - dx * k, y: ty } : { x: tx, y: ty - dy * k };
172
+ return `M${point(fx, fy)}C${point(c1.x, c1.y)} ${point(c2.x, c2.y)} ${point(tx, ty)}`;
173
+ }
174
+
175
+ /**
176
+ * Build a path between two points by `shape` (`straight` | `elbow` | `curve`).
177
+ * @param {ConnectorPathOptions} [opts]
178
+ * @returns {string}
179
+ */
180
+ export function connectorPath(opts = {}) {
181
+ const { from, to, shape = 'straight' } = opts;
182
+ if (shape === 'elbow') return elbowPath(from, to, opts);
183
+ if (shape === 'curve') return curvePath(from, to, opts);
184
+ return straightPath(from, to);
185
+ }
186
+
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) {
197
+ const px = finite('p.x', p?.x);
198
+ const py = finite('p.y', p?.y);
199
+ const a = finite('angle', angle, 0);
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)');
203
+ const back = a + Math.PI;
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 };
206
+ return `M${point(px, py)}L${point(p1.x, p1.y)}L${point(p2.x, p2.y)}Z`;
207
+ }
208
+
209
+ /**
210
+ * A filled dot at `p`.
211
+ * @param {Point} p
212
+ * @param {number} [radius]
213
+ * @returns {string}
214
+ */
215
+ export function dotMark(p, radius = 3) {
216
+ const px = finite('p.x', p?.x);
217
+ const py = finite('p.y', p?.y);
218
+ const r = dimension('radius', radius, 3);
219
+ if (r === 0) return '';
220
+ return `M${point(px, py - r)}A${fmt(r)},${fmt(r)} 0 1 1 ${point(px, py + r)}A${fmt(r)},${fmt(
221
+ r,
222
+ )} 0 1 1 ${point(px, py - r)}Z`;
223
+ }
224
+
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
+ */
231
+ export function autoSides(fromRect, toRect) {
232
+ const fc = anchorPoint(fromRect, 'center');
233
+ const tc = anchorPoint(toRect, 'center');
234
+ const dx = tc.x - fc.x;
235
+ const dy = tc.y - fc.y;
236
+ if (Math.abs(dx) >= Math.abs(dy)) {
237
+ return dx >= 0 ? { from: 'right', to: 'left' } : { from: 'left', to: 'right' };
238
+ }
239
+ return dy >= 0 ? { from: 'bottom', to: 'top' } : { from: 'top', to: 'bottom' };
240
+ }
241
+
242
+ /**
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}
250
+ */
251
+ export function endTangentAngle(from, to, shape = 'straight') {
252
+ if (shape === 'straight') return angleBetween(from, to);
253
+ const dx = finite('to.x', to?.x) - finite('from.x', from?.x);
254
+ const dy = finite('to.y', to?.y) - finite('from.y', from?.y);
255
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? 0 : Math.PI;
256
+ return dy >= 0 ? Math.PI / 2 : -Math.PI / 2;
257
+ }
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
+ */
266
+ export function connectRects(opts = {}) {
267
+ const { fromRect, toRect, shape = 'straight', curvature, mid } = opts;
268
+ // Honor each side override independently; auto-pick whichever is unset.
269
+ const auto = autoSides(fromRect, toRect);
270
+ const sides = { from: opts.fromSide || auto.from, to: opts.toSide || auto.to };
271
+ const from = anchorPoint(fromRect, sides.from);
272
+ const to = anchorPoint(toRect, sides.to);
273
+ const d = connectorPath({ from, to, shape, curvature, mid });
274
+ return { d, from, to, angle: endTangentAngle(from, to, shape) };
275
+ }
@@ -0,0 +1,21 @@
1
+ /* ==========================================================================
2
+ analytical — convenience roll-up of the opt-in analytical-primitive layers.
3
+
4
+ Import this one file instead of the seven leaves when you're building
5
+ analytical / generated-report UI:
6
+
7
+ @import '@ponchia/ui';
8
+ @import '@ponchia/ui/css/analytical.css';
9
+
10
+ Add @ponchia/ui/css/dataviz.css for the chart colour palette (legend swatches
11
+ fall back to the accent without it) and @ponchia/ui/css/report.css for the
12
+ document grammar as needed — those are separate concerns, kept opt-in.
13
+ ========================================================================== */
14
+
15
+ @import url('./annotations.css') layer(bronto);
16
+ @import url('./legend.css') layer(bronto);
17
+ @import url('./marks.css') layer(bronto);
18
+ @import url('./connectors.css') layer(bronto);
19
+ @import url('./spotlight.css') layer(bronto);
20
+ @import url('./crosshair.css') layer(bronto);
21
+ @import url('./selection.css') layer(bronto);
@@ -0,0 +1,292 @@
1
+ /* ==========================================================================
2
+ annotations — opt-in SVG callouts for analytical figures.
3
+
4
+ Bronto-native take on the subject / connector / note grammar: SVG-first,
5
+ no runtime dependency, no authoring handles. Import only beside charts,
6
+ reports, or generated figures that need explicit callouts.
7
+ ========================================================================== */
8
+
9
+ .ui-annotation {
10
+ --annotation-color: var(--accent);
11
+ --annotation-line: var(--line-strong);
12
+ --annotation-note-bg: var(--panel);
13
+ --annotation-subject-fill: color-mix(in srgb, var(--annotation-color) 8%, transparent);
14
+ --annotation-stroke-width: 1.5;
15
+
16
+ color: var(--annotation-color);
17
+ font-family: var(--mono);
18
+ font-size: var(--text-xs);
19
+ overflow: visible;
20
+ }
21
+
22
+ .ui-annotation--muted {
23
+ --annotation-color: var(--text-dim);
24
+ --annotation-line: var(--line);
25
+ }
26
+
27
+ .ui-annotation--accent {
28
+ --annotation-color: var(--accent);
29
+ --annotation-line: var(--line-strong);
30
+ }
31
+
32
+ .ui-annotation--success {
33
+ --annotation-color: var(--success);
34
+ }
35
+
36
+ .ui-annotation--warning {
37
+ --annotation-color: var(--warning);
38
+ }
39
+
40
+ .ui-annotation--danger {
41
+ --annotation-color: var(--danger);
42
+ }
43
+
44
+ .ui-annotation--info {
45
+ --annotation-color: var(--info);
46
+ }
47
+
48
+ .ui-annotation__subject,
49
+ .ui-annotation__connector,
50
+ .ui-annotation__connector-end,
51
+ .ui-annotation__note-line,
52
+ .ui-annotation__badge {
53
+ vector-effect: non-scaling-stroke;
54
+ }
55
+
56
+ .ui-annotation__subject {
57
+ fill: var(--annotation-subject-fill);
58
+ stroke: var(--annotation-color);
59
+ stroke-linejoin: miter;
60
+ stroke-width: var(--annotation-stroke-width);
61
+ }
62
+
63
+ .ui-annotation__connector,
64
+ .ui-annotation__note-line {
65
+ fill: none;
66
+ stroke: var(--annotation-line);
67
+ stroke-linecap: square;
68
+ stroke-linejoin: miter;
69
+ stroke-opacity: 0.86;
70
+ stroke-width: var(--annotation-stroke-width);
71
+ }
72
+
73
+ .ui-annotation__connector-end {
74
+ fill: var(--annotation-line);
75
+ stroke: none;
76
+ }
77
+
78
+ .ui-annotation__note {
79
+ color: var(--text-soft);
80
+ fill: currentColor;
81
+ }
82
+
83
+ .ui-annotation__note > rect {
84
+ fill: var(--annotation-note-bg);
85
+ stroke: var(--line);
86
+ stroke-width: 1;
87
+ vector-effect: non-scaling-stroke;
88
+ }
89
+
90
+ .ui-annotation__title {
91
+ fill: var(--text);
92
+ font-size: var(--text-xs);
93
+ font-weight: 700;
94
+ letter-spacing: 0;
95
+ paint-order: stroke fill;
96
+ stroke: var(--annotation-note-bg);
97
+ stroke-linejoin: round;
98
+ stroke-width: 3;
99
+ text-transform: uppercase;
100
+ }
101
+
102
+ .ui-annotation__label {
103
+ fill: var(--text-soft);
104
+ font-size: var(--text-xs);
105
+ letter-spacing: 0;
106
+ paint-order: stroke fill;
107
+ stroke: var(--annotation-note-bg);
108
+ stroke-linejoin: round;
109
+ stroke-width: 3;
110
+ }
111
+
112
+ .ui-annotation__badge {
113
+ fill: var(--annotation-note-bg);
114
+ stroke: var(--annotation-color);
115
+ stroke-width: var(--annotation-stroke-width);
116
+ }
117
+
118
+ .ui-annotation--label .ui-annotation__subject,
119
+ .ui-annotation--badge .ui-annotation__connector,
120
+ .ui-annotation--badge .ui-annotation__note-line {
121
+ display: none;
122
+ }
123
+
124
+ .ui-annotation--callout .ui-annotation__note-line,
125
+ .ui-annotation--elbow .ui-annotation__note-line,
126
+ .ui-annotation--curve .ui-annotation__note-line {
127
+ stroke: var(--annotation-color);
128
+ }
129
+
130
+ .ui-annotation--elbow .ui-annotation__connector {
131
+ stroke-linejoin: bevel;
132
+ }
133
+
134
+ .ui-annotation--curve .ui-annotation__connector {
135
+ stroke-linecap: round;
136
+ }
137
+
138
+ .ui-annotation--circle .ui-annotation__subject {
139
+ stroke-dasharray: 0.01 4;
140
+ stroke-linecap: round;
141
+ }
142
+
143
+ .ui-annotation--rect .ui-annotation__subject {
144
+ stroke-dasharray: 5 3;
145
+ }
146
+
147
+ .ui-annotation--threshold .ui-annotation__subject {
148
+ fill: none;
149
+ stroke: var(--annotation-color);
150
+ stroke-dasharray: 6 4;
151
+ }
152
+
153
+ .ui-annotation--badge .ui-annotation__subject {
154
+ fill: var(--annotation-color);
155
+ }
156
+
157
+ .ui-annotation--badge .ui-annotation__badge {
158
+ fill: var(--annotation-color);
159
+ }
160
+
161
+ .ui-annotation--badge .ui-annotation__title,
162
+ .ui-annotation--badge .ui-annotation__label {
163
+ fill: var(--button-text);
164
+ stroke: none;
165
+ }
166
+
167
+ .ui-annotation--bracket .ui-annotation__subject,
168
+ .ui-annotation--compare .ui-annotation__subject,
169
+ .ui-annotation--axis .ui-annotation__subject,
170
+ .ui-annotation--timeline .ui-annotation__subject {
171
+ fill: none;
172
+ stroke: var(--annotation-color);
173
+ stroke-linecap: square;
174
+ }
175
+
176
+ .ui-annotation--band .ui-annotation__subject {
177
+ fill: var(--annotation-subject-fill);
178
+ stroke: var(--annotation-color);
179
+ stroke-dasharray: 5 3;
180
+ }
181
+
182
+ .ui-annotation--slope .ui-annotation__subject {
183
+ fill: none;
184
+ stroke: var(--annotation-color);
185
+ stroke-linecap: round;
186
+ }
187
+
188
+ .ui-annotation--cluster .ui-annotation__subject {
189
+ fill: var(--annotation-subject-fill);
190
+ stroke: var(--annotation-color);
191
+ stroke-dasharray: 0.01 5;
192
+ stroke-linecap: round;
193
+ }
194
+
195
+ .ui-annotation--evidence .ui-annotation__badge {
196
+ fill: var(--annotation-note-bg);
197
+ stroke: var(--annotation-color);
198
+ }
199
+
200
+ .ui-annotation--evidence .ui-annotation__title {
201
+ fill: var(--annotation-color);
202
+ stroke: none;
203
+ }
204
+
205
+ .ui-annotation--focus {
206
+ --annotation-stroke-width: 2;
207
+ --annotation-subject-fill: color-mix(in srgb, var(--annotation-color) 14%, transparent);
208
+ }
209
+
210
+ @media (prefers-reduced-motion: no-preference) {
211
+ .ui-annotation--draw .ui-annotation__connector,
212
+ .ui-annotation--draw .ui-annotation__note-line {
213
+ animation: uiAnnotationDraw var(--duration-slow, 600ms) var(--ease-out, ease-out) both;
214
+ animation-delay: var(--annotation-delay, 0ms);
215
+ stroke-dasharray: var(--annotation-dash, 360);
216
+ stroke-dashoffset: var(--annotation-dash, 360);
217
+ }
218
+
219
+ .ui-annotation--draw .ui-annotation__subject,
220
+ .ui-annotation--draw .ui-annotation__badge {
221
+ animation: uiAnnotationSubjectReveal var(--duration-slow, 600ms) var(--ease-out, ease-out) both;
222
+ animation-delay: var(--annotation-delay, 0ms);
223
+ }
224
+
225
+ .ui-annotation--reveal .ui-annotation__note {
226
+ animation: uiAnnotationReveal var(--duration-slow, 600ms) var(--ease-out, ease-out) both;
227
+ animation-delay: var(--annotation-delay, 0ms);
228
+ }
229
+
230
+ .ui-annotation--pulse .ui-annotation__subject,
231
+ .ui-annotation--pulse .ui-annotation__badge {
232
+ animation: uiAnnotationPulse 1.6s var(--ease-out, ease-out) infinite;
233
+ animation-delay: var(--annotation-delay, 0ms);
234
+ transform-box: fill-box;
235
+ transform-origin: center;
236
+ }
237
+ }
238
+
239
+ @keyframes uiAnnotationDraw {
240
+ to {
241
+ stroke-dashoffset: 0;
242
+ }
243
+ }
244
+
245
+ @keyframes uiAnnotationSubjectReveal {
246
+ from {
247
+ opacity: 0;
248
+ }
249
+
250
+ to {
251
+ opacity: 1;
252
+ }
253
+ }
254
+
255
+ @keyframes uiAnnotationReveal {
256
+ from {
257
+ opacity: 0;
258
+ }
259
+
260
+ to {
261
+ opacity: 1;
262
+ }
263
+ }
264
+
265
+ @keyframes uiAnnotationPulse {
266
+ 50% {
267
+ opacity: 0.62;
268
+ transform: scale(1.06);
269
+ }
270
+ }
271
+
272
+ @media print {
273
+ .ui-annotation__subject,
274
+ .ui-annotation__connector,
275
+ .ui-annotation__note,
276
+ .ui-annotation__note-line,
277
+ .ui-annotation__badge {
278
+ animation: none !important;
279
+ opacity: 1;
280
+ stroke-dashoffset: 0;
281
+ transform: none;
282
+ }
283
+ }
284
+
285
+ @media (forced-colors: active) {
286
+ .ui-annotation {
287
+ --annotation-color: CanvasText;
288
+ --annotation-line: CanvasText;
289
+ --annotation-note-bg: Canvas;
290
+ --annotation-subject-fill: Canvas;
291
+ }
292
+ }
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
  }