@ultimat3/ui 23.0.0 → 25.0.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 (100) hide show
  1. package/CATALOG.md +119 -15
  2. package/CLAUDE.md +17 -8
  3. package/README.md +119 -33
  4. package/package.json +6 -6
  5. package/src/a11y.ts +27 -13
  6. package/src/catalog/build-catalog.ts +2 -2
  7. package/src/components/Accordion.tsx +8 -2
  8. package/src/components/AppShell.module.scss +105 -4
  9. package/src/components/AppShell.tsx +48 -10
  10. package/src/components/BarChart.module.scss +47 -9
  11. package/src/components/BarChart.tsx +152 -72
  12. package/src/components/Card.tsx +52 -16
  13. package/src/components/ChartFrame.module.scss +188 -0
  14. package/src/components/ChartFrame.tsx +155 -0
  15. package/src/components/CommandPalette.module.scss +8 -5
  16. package/src/components/Container.tsx +46 -9
  17. package/src/components/DataTable.module.scss +190 -0
  18. package/src/components/DataTable.tsx +161 -51
  19. package/src/components/Divider.tsx +0 -2
  20. package/src/components/DonutChart.module.scss +73 -0
  21. package/src/components/DonutChart.tsx +154 -0
  22. package/src/components/Dropzone.module.scss +3 -1
  23. package/src/components/Dropzone.tsx +17 -8
  24. package/src/components/ErrorState.tsx +39 -12
  25. package/src/components/FileInput.module.scss +3 -1
  26. package/src/components/FileInput.tsx +6 -2
  27. package/src/components/Form.tsx +17 -4
  28. package/src/components/Gauge.module.scss +48 -0
  29. package/src/components/Gauge.tsx +67 -0
  30. package/src/components/Grid.tsx +34 -13
  31. package/src/components/Image.tsx +2 -2
  32. package/src/components/InlineBar.module.scss +23 -0
  33. package/src/components/InlineBar.tsx +31 -0
  34. package/src/components/LineChart.module.scss +147 -0
  35. package/src/components/LineChart.tsx +169 -0
  36. package/src/components/Menu.tsx +9 -5
  37. package/src/components/Meter.tsx +3 -3
  38. package/src/components/PageHeader.tsx +5 -2
  39. package/src/components/Popover.module.scss +17 -20
  40. package/src/components/Popover.tsx +14 -7
  41. package/src/components/Section.tsx +45 -26
  42. package/src/components/Stack.tsx +59 -18
  43. package/src/components/Table.module.scss +30 -0
  44. package/src/components/Table.tsx +14 -3
  45. package/src/components/Tabs.module.scss +6 -0
  46. package/src/components/Tabs.tsx +2 -2
  47. package/src/components/Text.tsx +41 -15
  48. package/src/components/Textarea.tsx +7 -4
  49. package/src/components/Toaster.tsx +1 -1
  50. package/src/components/Toolbar.tsx +7 -6
  51. package/src/components/Tooltip.module.scss +8 -3
  52. package/src/components/app-shell-view.ts +6 -4
  53. package/src/components/bar-chart-view.ts +11 -6
  54. package/src/components/chart-frame-view.ts +219 -0
  55. package/src/components/chart-ticks-view.ts +173 -0
  56. package/src/components/data-table-view.ts +68 -0
  57. package/src/components/donut-chart-view.ts +120 -0
  58. package/src/components/file-input-view.ts +32 -13
  59. package/src/components/gauge-view.ts +43 -0
  60. package/src/components/heading-node.tsx +62 -0
  61. package/src/components/image-source.ts +3 -3
  62. package/src/components/line-chart-view.ts +153 -0
  63. package/src/components/link-target.ts +21 -1
  64. package/src/components/meter-view.ts +13 -0
  65. package/src/components/money-view.ts +2 -7
  66. package/src/components/qr-matrix.ts +16 -23
  67. package/src/components/relative-time-view.ts +11 -4
  68. package/src/form/form-binding.ts +32 -14
  69. package/src/form/use-form.ts +3 -3
  70. package/src/global.scss +9 -0
  71. package/src/icons/build-icons.ts +109 -24
  72. package/src/index.ts +47 -15
  73. package/src/jsx-probe.ts +5 -4
  74. package/src/theme/brand-declarations.ts +113 -0
  75. package/src/theme/brand.ts +104 -96
  76. package/src/theme/context.ts +1 -1
  77. package/src/theme/preset-scifi.ts +99 -0
  78. package/src/theme/preset-shape.ts +18 -0
  79. package/src/theme/presets.ts +15 -0
  80. package/src/theme/shadow-value.ts +39 -0
  81. package/src/theme/theme.ts +36 -11
  82. package/src/toast/toast-state.ts +0 -0
  83. package/src/toast/toast-store.ts +1 -1
  84. package/src/toast/use-toasts.ts +1 -1
  85. package/src/tokens/_colors.scss +20 -0
  86. package/src/tokens/_effects.scss +59 -0
  87. package/src/tokens/_index.scss +2 -0
  88. package/src/tokens/_mixins.scss +6 -4
  89. package/src/tokens/_shadow.scss +16 -0
  90. package/src/tokens/_touch.scss +39 -0
  91. package/src/tokens/_typography.scss +14 -0
  92. package/src/tokens/color-tokens.ts +133 -0
  93. package/src/tokens/colour-vision.ts +106 -0
  94. package/src/tokens/contrast-pairs.ts +6 -1
  95. package/src/tokens/contrast.ts +1 -1
  96. package/src/tokens/theme.scss +2 -0
  97. package/src/tokens/tokens.ts +41 -87
  98. package/src/fake-dom.ts +0 -206
  99. package/src/sass-probe.ts +0 -55
  100. package/src/theme/inline-script.ts +0 -49
@@ -4,11 +4,13 @@
4
4
 
5
5
  @forward 'breakpoints';
6
6
  @forward 'colors' hide emit, channels;
7
+ @forward 'effects';
7
8
  @forward 'motion' hide emit;
8
9
  @forward 'radius' hide emit;
9
10
  @forward 'shadow' hide emit, levels;
10
11
  @forward 'space' hide emit;
11
12
  @forward 'stroke' hide emit;
13
+ @forward 'touch' hide emit;
12
14
  @forward 'typography' hide emit;
13
15
  @forward 'units';
14
16
  @forward 'z' hide emit;
@@ -241,11 +241,13 @@ $-under: 0.02px;
241
241
  }
242
242
  }
243
243
 
244
- /// The mono family with tabular figures, for slugs, counts, timestamps, ids and URLs — data
245
- /// whose columns must not wobble as the value changes. Lifted from an app that had restated
246
- /// these two lines in six stylesheets before the catalog carried them.
244
+ /// The `data` family with tabular figures, for slugs, counts, timestamps, ids, URLs and every
245
+ /// figure in a chart axis or a table column — data whose columns must not wobble as the value
246
+ /// changes. Lifted from an app that had restated these two lines in six stylesheets before the
247
+ /// catalog carried them; reads `--font-data` since 2026-10, a slot `defineTheme()` can restyle
248
+ /// without touching `mono` (code).
247
249
  @mixin data-text {
248
- font-family: var(--font-mono);
250
+ font-family: var(--font-data);
249
251
  font-variant-numeric: tabular-nums;
250
252
  }
251
253
 
@@ -1,6 +1,10 @@
1
1
  // Elevation. Shadows are themed: a light-theme shadow (dark ink, low alpha)
2
2
  // is invisible on a dark surface, so dark gets its own deeper, higher-alpha set.
3
3
  // Single-layer values on purpose — one string per rung keeps the TS mirror exact.
4
+ //
5
+ // `glow-*` is a halo (no offset) and `tinted-*` a lift whose shadow carries the hue — both drawn
6
+ // from the ACCENT role, so a `defineTheme()` accent recolours them with no second override. Dark
7
+ // gets the higher alpha: a glow is the one shadow a near-black page can actually show.
4
8
 
5
9
  $shadow-light: (
6
10
  xs: 0 1px 2px rgb(17 15 13 / 0.06),
@@ -8,6 +12,12 @@ $shadow-light: (
8
12
  md: 0 4px 14px rgb(17 15 13 / 0.1),
9
13
  lg: 0 12px 32px rgb(17 15 13 / 0.14),
10
14
  xl: 0 24px 56px rgb(17 15 13 / 0.18),
15
+ glow-sm: 0 0 6px rgb(var(--color-accent) / 0.28),
16
+ glow-md: 0 0 14px rgb(var(--color-accent) / 0.26),
17
+ glow-lg: 0 0 28px rgb(var(--color-accent) / 0.24),
18
+ tinted-sm: 0 2px 8px rgb(var(--color-accent) / 0.14),
19
+ tinted-md: 0 6px 18px rgb(var(--color-accent) / 0.16),
20
+ tinted-lg: 0 14px 36px rgb(var(--color-accent) / 0.18),
11
21
  );
12
22
 
13
23
  $shadow-dark: (
@@ -16,6 +26,12 @@ $shadow-dark: (
16
26
  md: 0 6px 20px rgb(0 0 0 / 0.56),
17
27
  lg: 0 16px 40px rgb(0 0 0 / 0.64),
18
28
  xl: 0 28px 64px rgb(0 0 0 / 0.72),
29
+ glow-sm: 0 0 6px rgb(var(--color-accent) / 0.45),
30
+ glow-md: 0 0 14px rgb(var(--color-accent) / 0.4),
31
+ glow-lg: 0 0 28px rgb(var(--color-accent) / 0.35),
32
+ tinted-sm: 0 2px 8px rgb(var(--color-accent) / 0.24),
33
+ tinted-md: 0 6px 18px rgb(var(--color-accent) / 0.28),
34
+ tinted-lg: 0 14px 36px rgb(var(--color-accent) / 0.32),
19
35
  );
20
36
 
21
37
  @mixin levels($map) {
@@ -0,0 +1,39 @@
1
+ // The touch target: the smallest box a finger can hit reliably. WCAG 2.5.5's 44 CSS px, as rem
2
+ // so a reader's larger text grows the target with it. One rung, in a map so it emits and reads
3
+ // back like every other scale.
4
+
5
+ $touch: (
6
+ target: 2.75rem,
7
+ );
8
+
9
+ @mixin emit {
10
+ :root {
11
+ @each $name, $value in $touch {
12
+ --touch-#{$name}: #{$value};
13
+ }
14
+ }
15
+ }
16
+
17
+ @function touch($name) {
18
+ @return var(--touch-#{$name});
19
+ }
20
+
21
+ /// Make the element hittable. Default: the box itself is at least the target, both axes. With
22
+ /// `$extend: true` the BOX keeps its size and an invisible `::after` grows the hit area around it,
23
+ /// centred — for a chart point, a dense icon row, a close glyph whose visual size is the design.
24
+ /// The pseudo-element is the element's own, so its clicks are the element's clicks.
25
+ @mixin touch-target($extend: false) {
26
+ @if $extend {
27
+ position: relative;
28
+
29
+ &::after {
30
+ content: '';
31
+ position: absolute;
32
+ inset-block: min(0px, calc((100% - #{touch(target)}) / 2));
33
+ inset-inline: min(0px, calc((100% - #{touch(target)}) / 2));
34
+ }
35
+ } @else {
36
+ min-block-size: touch(target);
37
+ min-inline-size: touch(target);
38
+ }
39
+ }
@@ -18,6 +18,20 @@ $font-family: (
18
18
  Consolas,
19
19
  monospace,
20
20
  ),
21
+ // Numbers in columns: a monospace-friendly stack whose figures align without `tnum` support,
22
+ // read through `@include t.data-text`, which adds `tabular-nums` for every family that has it.
23
+ // A slot of its own so a brand can put a proportional face with tabular figures here and keep
24
+ // `mono` for code.
25
+ data: (
26
+ ui-monospace,
27
+ 'SFMono-Regular',
28
+ 'Cascadia Mono',
29
+ 'JetBrains Mono',
30
+ 'Roboto Mono',
31
+ Menlo,
32
+ Consolas,
33
+ monospace,
34
+ ),
21
35
  );
22
36
 
23
37
  $font-size: (
@@ -0,0 +1,133 @@
1
+ // The colour half of the typed token mirror: every role, the chart series among them, and their
2
+ // channels per theme. `_colors.scss` is CANONICAL; `tokens.test.ts` fails if the two disagree.
3
+ // Split from `tokens.ts` by size alone — `tokens.ts` re-exports all of it, so there is one import.
4
+
5
+ export type Theme = 'light' | 'dark';
6
+
7
+ export const COLOR_ROLES = [
8
+ 'bg',
9
+ 'bg-soft',
10
+ 'surface',
11
+ 'surface-raised',
12
+ 'fg',
13
+ 'fg-strong',
14
+ 'fg-muted',
15
+ 'line',
16
+ 'scrim',
17
+ 'accent',
18
+ 'accent-strong',
19
+ 'accent-fg',
20
+ 'success',
21
+ 'success-soft',
22
+ 'success-fg',
23
+ 'warning',
24
+ 'warning-soft',
25
+ 'warning-fg',
26
+ 'danger',
27
+ 'danger-soft',
28
+ 'danger-fg',
29
+ 'info',
30
+ 'info-soft',
31
+ 'info-fg',
32
+ 'chart-1',
33
+ 'chart-2',
34
+ 'chart-3',
35
+ 'chart-4',
36
+ 'chart-5',
37
+ 'chart-6',
38
+ 'chart-7',
39
+ 'chart-8',
40
+ ] as const;
41
+
42
+ export type ColorRole = (typeof COLOR_ROLES)[number];
43
+
44
+ /**
45
+ * The categorical series, in the order a chart assigns them. Okabe–Ito-derived and measured, not
46
+ * picked: 3:1 against every surface (`CONTRAST_PAIRS`) and apart under protan, deutan and tritan
47
+ * simulation (`colour-vision.test.ts`). A ninth series reuses `chart-1` with a second encoding —
48
+ * a dash, a pattern, a label — never a ninth hue.
49
+ */
50
+ export const CHART_ROLES = [
51
+ 'chart-1',
52
+ 'chart-2',
53
+ 'chart-3',
54
+ 'chart-4',
55
+ 'chart-5',
56
+ 'chart-6',
57
+ 'chart-7',
58
+ 'chart-8',
59
+ ] as const satisfies readonly ColorRole[];
60
+
61
+ export type ChartRole = (typeof CHART_ROLES)[number];
62
+
63
+ /** Space-separated RGB channels, mirroring `_colors.scss`. */
64
+ export const colorTokens: Readonly<Record<Theme, Readonly<Record<ColorRole, string>>>> = {
65
+ light: {
66
+ bg: '253 246 240',
67
+ 'bg-soft': '245 237 230',
68
+ surface: '250 245 241',
69
+ 'surface-raised': '255 255 255',
70
+ fg: '38 34 31',
71
+ 'fg-strong': '17 15 13',
72
+ 'fg-muted': '110 102 94',
73
+ line: '208 198 188',
74
+ scrim: '17 15 13',
75
+ accent: '31 110 178',
76
+ 'accent-strong': '21 92 152',
77
+ 'accent-fg': '255 255 255',
78
+ success: '21 123 80',
79
+ 'success-soft': '222 244 232',
80
+ 'success-fg': '255 255 255',
81
+ warning: '155 93 7',
82
+ 'warning-soft': '253 240 213',
83
+ 'warning-fg': '255 255 255',
84
+ danger: '190 42 42',
85
+ 'danger-soft': '253 227 227',
86
+ 'danger-fg': '255 255 255',
87
+ info: '31 110 178',
88
+ 'info-soft': '224 239 252',
89
+ 'info-fg': '255 255 255',
90
+ 'chart-1': '0 107 170',
91
+ 'chart-2': '189 107 14',
92
+ 'chart-3': '0 125 90',
93
+ 'chart-4': '189 44 16',
94
+ 'chart-5': '170 70 140',
95
+ 'chart-6': '47 135 195',
96
+ 'chart-7': '118 97 18',
97
+ 'chart-8': '60 60 60',
98
+ },
99
+ dark: {
100
+ bg: '18 18 20',
101
+ 'bg-soft': '28 28 32',
102
+ surface: '34 34 39',
103
+ 'surface-raised': '44 44 50',
104
+ fg: '228 226 222',
105
+ 'fg-strong': '248 247 245',
106
+ 'fg-muted': '155 151 145',
107
+ line: '72 72 80',
108
+ scrim: '0 0 0',
109
+ accent: '96 170 240',
110
+ 'accent-strong': '130 190 248',
111
+ 'accent-fg': '16 20 26',
112
+ success: '74 190 130',
113
+ 'success-soft': '22 46 34',
114
+ 'success-fg': '12 26 18',
115
+ warning: '226 170 66',
116
+ 'warning-soft': '52 42 20',
117
+ 'warning-fg': '28 20 6',
118
+ danger: '240 110 110',
119
+ 'danger-soft': '56 26 26',
120
+ 'danger-fg': '30 12 12',
121
+ info: '96 170 240',
122
+ 'info-soft': '22 38 56',
123
+ 'info-fg': '12 20 30',
124
+ 'chart-1': '105 201 255',
125
+ 'chart-2': '230 153 0',
126
+ 'chart-3': '0 167 113',
127
+ 'chart-4': '236 111 60',
128
+ 'chart-5': '203 126 189',
129
+ 'chart-6': '124 153 252',
130
+ 'chart-7': '246 234 72',
131
+ 'chart-8': '181 181 180',
132
+ },
133
+ } as const;
@@ -0,0 +1,106 @@
1
+ // Colour-vision simulation and perceptual distance over channel tokens, so "the chart series are
2
+ // colour-blind safe" is a measured claim (`colour-vision.test.ts`) and an app can measure its own
3
+ // series before it ships them. Machado, Oliveira & Fernandes (2009) at full severity, applied in
4
+ // linear sRGB; distance is CIE76 ΔE in CIELAB (D65).
5
+
6
+ /**
7
+ * A simulated dichromat can land a hair outside sRGB; the display clips it. Its own helper, away
8
+ * from the transfer curve, because the clip is a gamut fact, not part of the curve.
9
+ */
10
+ const inGamut = (linear: number): number => Math.min(1, Math.max(0, linear));
11
+
12
+ import { linearize, parseChannels } from './contrast';
13
+
14
+ /** The three dichromacies. `typical` is the identity, so one loop covers every reader. */
15
+ export const COLOUR_VISIONS = ['protan', 'deutan', 'tritan'] as const;
16
+ export type ColourVision = (typeof COLOUR_VISIONS)[number] | 'typical';
17
+
18
+ /**
19
+ * The floor two series must stay apart by, in ΔE76, under every vision. ~2.3 is a just-noticeable
20
+ * difference for two large flat patches; a chart compares thin lines and small slices with a gap
21
+ * between them, so the floor is set well above it. Measured headroom (`As of 2026-10`): the
22
+ * shipped light palette's closest pair is 10.7, its dark one 20.1.
23
+ */
24
+ export const CHART_DISTINCT_MIN = 10;
25
+
26
+ type Matrix = readonly [
27
+ readonly [number, number, number],
28
+ readonly [number, number, number],
29
+ readonly [number, number, number],
30
+ ];
31
+
32
+ const MACHADO: Readonly<Record<(typeof COLOUR_VISIONS)[number], Matrix>> = {
33
+ protan: [
34
+ [0.152286, 1.052583, -0.204868],
35
+ [0.114503, 0.786281, 0.099216],
36
+ [-0.003882, -0.048116, 1.051998],
37
+ ],
38
+ deutan: [
39
+ [0.367322, 0.860646, -0.227968],
40
+ [0.280085, 0.672501, 0.047413],
41
+ [-0.01182, 0.04294, 0.968881],
42
+ ],
43
+ tritan: [
44
+ [1.255528, -0.076749, -0.178779],
45
+ [-0.078411, 0.930809, 0.147602],
46
+ [0.004733, 0.691367, 0.3039],
47
+ ],
48
+ };
49
+
50
+ /** Linear light back to an sRGB channel — the inverse of contrast.ts's `linearize`. */
51
+ const toChannel = (linear: number): number => {
52
+ const v = inGamut(linear);
53
+ const encoded = v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;
54
+ return Math.round(encoded * 255);
55
+ };
56
+
57
+ /** `"R G B"` as a reader with `vision` sees it, in the same channel spelling. */
58
+ export function simulateColourVision(channels: string, vision: ColourVision): string {
59
+ if (vision === 'typical') return channels;
60
+ const [r, g, b] = parseChannels(channels).map(linearize) as [number, number, number];
61
+ return MACHADO[vision].map((row) => toChannel(row[0] * r + row[1] * g + row[2] * b)).join(' ');
62
+ }
63
+
64
+ const labF = (t: number): number =>
65
+ t > 216 / 24389 ? Math.cbrt(t) : ((24389 / 27) * t) / 116 + 16 / 116;
66
+
67
+ function lab(channels: string): readonly [number, number, number] {
68
+ const [r, g, b] = parseChannels(channels).map(linearize) as [number, number, number];
69
+ const x = labF((0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047);
70
+ const y = labF(0.2126 * r + 0.7152 * g + 0.0722 * b);
71
+ const z = labF((0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883);
72
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
73
+ }
74
+
75
+ /** CIE76 ΔE between two channel strings. 0 is identical; black to white is 100. */
76
+ export function colourDistance(a: string, b: string): number {
77
+ const [l1, a1, b1] = lab(a);
78
+ const [l2, a2, b2] = lab(b);
79
+ return Math.hypot(l1 - l2, a1 - a2, b1 - b2);
80
+ }
81
+
82
+ export interface ClosestPair {
83
+ /** Indexes into the palette, `a < b`. */
84
+ readonly a: number;
85
+ readonly b: number;
86
+ readonly vision: ColourVision;
87
+ readonly distance: number;
88
+ }
89
+
90
+ /**
91
+ * The two series hardest to tell apart, and for whom. One answer rather than a matrix: a palette
92
+ * is as safe as its closest pair, and the pair is what an author has to move.
93
+ */
94
+ export function closestChartPair(palette: readonly string[]): ClosestPair {
95
+ let closest: ClosestPair = { a: 0, b: 0, vision: 'typical', distance: Number.POSITIVE_INFINITY };
96
+ for (const vision of ['typical', ...COLOUR_VISIONS] as const) {
97
+ const seen = palette.map((channels) => simulateColourVision(channels, vision));
98
+ for (let a = 0; a < seen.length; a += 1) {
99
+ for (let b = a + 1; b < seen.length; b += 1) {
100
+ const distance = colourDistance(seen[a] as string, seen[b] as string);
101
+ if (distance < closest.distance) closest = { a, b, vision, distance };
102
+ }
103
+ }
104
+ }
105
+ return closest;
106
+ }
@@ -11,8 +11,8 @@
11
11
  // benchmark (EN 301 549, ADA Title II, the EAA). 4.5:1 for body text (1.4.3), 3:1 for large text
12
12
  // and non-text UI (1.4.11).
13
13
 
14
+ import { CHART_ROLES, type ColorRole } from './color-tokens';
14
15
  import { AA_LARGE, AA_TEXT } from './contrast';
15
- import type { ColorRole } from './tokens';
16
16
 
17
17
  /** What a page, a card and a raised panel are made of. */
18
18
  const SURFACES: readonly ColorRole[] = ['bg', 'bg-soft', 'surface', 'surface-raised'];
@@ -68,4 +68,9 @@ export const CONTRAST_PAIRS: readonly ContrastPair[] = [
68
68
  ...STATUS.flatMap((status) => onSurfaces(status, AA_TEXT, 'status text on a surface')),
69
69
  ...onSurfaces('accent', AA_LARGE, 'the focus ring against a surface'),
70
70
  ...onSurfaces('line', VISIBLE_EDGE, 'a border or divider against a surface'),
71
+ // A series mark is non-text UI (1.4.11), and a dashboard puts the same series on the page, in a
72
+ // card and in a raised popover — so every surface, not the one a chart usually sits on.
73
+ ...CHART_ROLES.flatMap((role) =>
74
+ onSurfaces(role, AA_LARGE, `the ${role} series mark — a bar, line or slice — on a surface`),
75
+ ),
71
76
  ];
@@ -31,7 +31,7 @@ export function parseChannels(value: string): Channels {
31
31
  }
32
32
 
33
33
  // sRGB → linear, then the ITU-R BT.709 luma weights WCAG specifies.
34
- function linearize(channel: number): number {
34
+ export function linearize(channel: number): number {
35
35
  const v = channel / 255;
36
36
  return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
37
37
  }
@@ -8,6 +8,7 @@
8
8
  @use 'shadow';
9
9
  @use 'space';
10
10
  @use 'stroke';
11
+ @use 'touch';
11
12
  @use 'typography';
12
13
  @use 'z';
13
14
 
@@ -16,6 +17,7 @@
16
17
  @include space.emit;
17
18
  @include radius.emit;
18
19
  @include stroke.emit;
20
+ @include touch.emit;
19
21
  @include typography.emit;
20
22
  @include motion.emit;
21
23
  @include z.emit;
@@ -5,93 +5,10 @@
5
5
  // `tokens.test.ts` (run by `x verify`) fails if the two ever disagree.
6
6
 
7
7
  import { unknownTokenError } from '../errors';
8
+ import { COLOR_ROLES, type ColorRole, colorTokens, type Theme } from './color-tokens';
8
9
 
9
- export type Theme = 'light' | 'dark';
10
-
11
- export const COLOR_ROLES = [
12
- 'bg',
13
- 'bg-soft',
14
- 'surface',
15
- 'surface-raised',
16
- 'fg',
17
- 'fg-strong',
18
- 'fg-muted',
19
- 'line',
20
- 'scrim',
21
- 'accent',
22
- 'accent-strong',
23
- 'accent-fg',
24
- 'success',
25
- 'success-soft',
26
- 'success-fg',
27
- 'warning',
28
- 'warning-soft',
29
- 'warning-fg',
30
- 'danger',
31
- 'danger-soft',
32
- 'danger-fg',
33
- 'info',
34
- 'info-soft',
35
- 'info-fg',
36
- ] as const;
37
-
38
- export type ColorRole = (typeof COLOR_ROLES)[number];
39
-
40
- /** Space-separated RGB channels, mirroring `_colors.scss`. */
41
- export const colorTokens: Readonly<Record<Theme, Readonly<Record<ColorRole, string>>>> = {
42
- light: {
43
- bg: '253 246 240',
44
- 'bg-soft': '245 237 230',
45
- surface: '250 245 241',
46
- 'surface-raised': '255 255 255',
47
- fg: '38 34 31',
48
- 'fg-strong': '17 15 13',
49
- 'fg-muted': '110 102 94',
50
- line: '208 198 188',
51
- scrim: '17 15 13',
52
- accent: '31 110 178',
53
- 'accent-strong': '21 92 152',
54
- 'accent-fg': '255 255 255',
55
- success: '21 123 80',
56
- 'success-soft': '222 244 232',
57
- 'success-fg': '255 255 255',
58
- warning: '155 93 7',
59
- 'warning-soft': '253 240 213',
60
- 'warning-fg': '255 255 255',
61
- danger: '190 42 42',
62
- 'danger-soft': '253 227 227',
63
- 'danger-fg': '255 255 255',
64
- info: '31 110 178',
65
- 'info-soft': '224 239 252',
66
- 'info-fg': '255 255 255',
67
- },
68
- dark: {
69
- bg: '18 18 20',
70
- 'bg-soft': '28 28 32',
71
- surface: '34 34 39',
72
- 'surface-raised': '44 44 50',
73
- fg: '228 226 222',
74
- 'fg-strong': '248 247 245',
75
- 'fg-muted': '155 151 145',
76
- line: '72 72 80',
77
- scrim: '0 0 0',
78
- accent: '96 170 240',
79
- 'accent-strong': '130 190 248',
80
- 'accent-fg': '16 20 26',
81
- success: '74 190 130',
82
- 'success-soft': '22 46 34',
83
- 'success-fg': '12 26 18',
84
- warning: '226 170 66',
85
- 'warning-soft': '52 42 20',
86
- 'warning-fg': '28 20 6',
87
- danger: '240 110 110',
88
- 'danger-soft': '56 26 26',
89
- 'danger-fg': '30 12 12',
90
- info: '96 170 240',
91
- 'info-soft': '22 38 56',
92
- 'info-fg': '12 20 30',
93
- },
94
- } as const;
10
+ export type { ChartRole, ColorRole, Theme } from './color-tokens';
11
+ export { CHART_ROLES, COLOR_ROLES, colorTokens } from './color-tokens';
95
12
 
96
13
  export const spaceTokens = {
97
14
  '0': '0',
@@ -153,13 +70,39 @@ export const easingTokens = {
153
70
  spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
154
71
  } as const;
155
72
 
156
- export const shadowTokens: Readonly<Record<Theme, Readonly<Record<string, string>>>> = {
73
+ /**
74
+ * The elevation rungs, then the two accent-drawn families: `glow-*` (a halo, no offset — a focused
75
+ * or live edge) and `tinted-*` (a lift whose shadow carries the accent hue — coloured elevation).
76
+ */
77
+ export const SHADOW_NAMES = [
78
+ 'xs',
79
+ 'sm',
80
+ 'md',
81
+ 'lg',
82
+ 'xl',
83
+ 'glow-sm',
84
+ 'glow-md',
85
+ 'glow-lg',
86
+ 'tinted-sm',
87
+ 'tinted-md',
88
+ 'tinted-lg',
89
+ ] as const;
90
+
91
+ export type ShadowName = (typeof SHADOW_NAMES)[number];
92
+
93
+ export const shadowTokens: Readonly<Record<Theme, Readonly<Record<ShadowName, string>>>> = {
157
94
  light: {
158
95
  xs: '0 1px 2px rgb(17 15 13 / 0.06)',
159
96
  sm: '0 2px 6px rgb(17 15 13 / 0.08)',
160
97
  md: '0 4px 14px rgb(17 15 13 / 0.1)',
161
98
  lg: '0 12px 32px rgb(17 15 13 / 0.14)',
162
99
  xl: '0 24px 56px rgb(17 15 13 / 0.18)',
100
+ 'glow-sm': '0 0 6px rgb(var(--color-accent) / 0.28)',
101
+ 'glow-md': '0 0 14px rgb(var(--color-accent) / 0.26)',
102
+ 'glow-lg': '0 0 28px rgb(var(--color-accent) / 0.24)',
103
+ 'tinted-sm': '0 2px 8px rgb(var(--color-accent) / 0.14)',
104
+ 'tinted-md': '0 6px 18px rgb(var(--color-accent) / 0.16)',
105
+ 'tinted-lg': '0 14px 36px rgb(var(--color-accent) / 0.18)',
163
106
  },
164
107
  dark: {
165
108
  xs: '0 1px 2px rgb(0 0 0 / 0.4)',
@@ -167,9 +110,20 @@ export const shadowTokens: Readonly<Record<Theme, Readonly<Record<string, string
167
110
  md: '0 6px 20px rgb(0 0 0 / 0.56)',
168
111
  lg: '0 16px 40px rgb(0 0 0 / 0.64)',
169
112
  xl: '0 28px 64px rgb(0 0 0 / 0.72)',
113
+ 'glow-sm': '0 0 6px rgb(var(--color-accent) / 0.45)',
114
+ 'glow-md': '0 0 14px rgb(var(--color-accent) / 0.4)',
115
+ 'glow-lg': '0 0 28px rgb(var(--color-accent) / 0.35)',
116
+ 'tinted-sm': '0 2px 8px rgb(var(--color-accent) / 0.24)',
117
+ 'tinted-md': '0 6px 18px rgb(var(--color-accent) / 0.28)',
118
+ 'tinted-lg': '0 14px 36px rgb(var(--color-accent) / 0.32)',
170
119
  },
171
120
  } as const;
172
121
 
122
+ /** WCAG 2.5.5's 44 CSS px, as rem so a reader's larger text grows the target with it. */
123
+ export const touchTokens = {
124
+ target: '2.75rem',
125
+ } as const;
126
+
173
127
  export const breakpointTokens = {
174
128
  sm: '480px',
175
129
  md: '768px',