@oxyhq/bloom 0.7.6 → 0.8.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 (111) hide show
  1. package/lib/commonjs/normalize-colors.d.js +2 -0
  2. package/lib/commonjs/normalize-colors.d.js.map +1 -0
  3. package/lib/commonjs/theme/apply-dark-class.js +15 -13
  4. package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
  5. package/lib/commonjs/theme/build-theme.js +39 -61
  6. package/lib/commonjs/theme/build-theme.js.map +1 -1
  7. package/lib/commonjs/theme/color-presets.js +11 -5
  8. package/lib/commonjs/theme/color-presets.js.map +1 -1
  9. package/lib/commonjs/theme/color-scope/index.web.js +13 -3
  10. package/lib/commonjs/theme/color-scope/index.web.js.map +1 -1
  11. package/lib/commonjs/theme/color-scope/style-builder.js +7 -6
  12. package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
  13. package/lib/commonjs/theme/color-space.js +76 -0
  14. package/lib/commonjs/theme/color-space.js.map +1 -0
  15. package/lib/commonjs/theme/index.js.map +1 -1
  16. package/lib/commonjs/theme/native-root-vars.native.js +28 -18
  17. package/lib/commonjs/theme/native-root-vars.native.js.map +1 -1
  18. package/lib/commonjs/theme/preset-vars.js +20 -99
  19. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  20. package/lib/commonjs/theme/token-registry.js +123 -0
  21. package/lib/commonjs/theme/token-registry.js.map +1 -0
  22. package/lib/module/normalize-colors.d.js +2 -0
  23. package/lib/module/normalize-colors.d.js.map +1 -0
  24. package/lib/module/theme/apply-dark-class.js +15 -13
  25. package/lib/module/theme/apply-dark-class.js.map +1 -1
  26. package/lib/module/theme/build-theme.js +39 -61
  27. package/lib/module/theme/build-theme.js.map +1 -1
  28. package/lib/module/theme/color-presets.js +11 -5
  29. package/lib/module/theme/color-presets.js.map +1 -1
  30. package/lib/module/theme/color-scope/index.web.js +16 -4
  31. package/lib/module/theme/color-scope/index.web.js.map +1 -1
  32. package/lib/module/theme/color-scope/style-builder.js +7 -6
  33. package/lib/module/theme/color-scope/style-builder.js.map +1 -1
  34. package/lib/module/theme/color-space.js +69 -0
  35. package/lib/module/theme/color-space.js.map +1 -0
  36. package/lib/module/theme/index.js.map +1 -1
  37. package/lib/module/theme/native-root-vars.native.js +29 -18
  38. package/lib/module/theme/native-root-vars.native.js.map +1 -1
  39. package/lib/module/theme/preset-vars.js +19 -98
  40. package/lib/module/theme/preset-vars.js.map +1 -1
  41. package/lib/module/theme/token-registry.js +113 -0
  42. package/lib/module/theme/token-registry.js.map +1 -0
  43. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +13 -9
  44. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/theme/build-theme.d.ts +1 -1
  46. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/theme/color-presets.d.ts +11 -5
  48. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/theme/color-scope/index.web.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +5 -2
  51. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/theme/color-space.d.ts +28 -0
  53. package/lib/typescript/commonjs/theme/color-space.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/theme/index.d.ts +1 -2
  55. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts +25 -15
  57. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/theme/preset-vars.d.ts +12 -45
  59. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/theme/token-registry.d.ts +53 -0
  61. package/lib/typescript/commonjs/theme/token-registry.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/theme/types.d.ts +3 -0
  63. package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
  64. package/lib/typescript/module/theme/apply-dark-class.d.ts +13 -9
  65. package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
  66. package/lib/typescript/module/theme/build-theme.d.ts +1 -1
  67. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  68. package/lib/typescript/module/theme/color-presets.d.ts +11 -5
  69. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  70. package/lib/typescript/module/theme/color-scope/index.web.d.ts.map +1 -1
  71. package/lib/typescript/module/theme/color-scope/style-builder.d.ts +5 -2
  72. package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
  73. package/lib/typescript/module/theme/color-space.d.ts +28 -0
  74. package/lib/typescript/module/theme/color-space.d.ts.map +1 -0
  75. package/lib/typescript/module/theme/index.d.ts +1 -2
  76. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  77. package/lib/typescript/module/theme/native-root-vars.native.d.ts +25 -15
  78. package/lib/typescript/module/theme/native-root-vars.native.d.ts.map +1 -1
  79. package/lib/typescript/module/theme/preset-vars.d.ts +12 -45
  80. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  81. package/lib/typescript/module/theme/token-registry.d.ts +53 -0
  82. package/lib/typescript/module/theme/token-registry.d.ts.map +1 -0
  83. package/lib/typescript/module/theme/types.d.ts +3 -0
  84. package/lib/typescript/module/theme/types.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/__tests__/BloomColorScope.test.tsx +14 -0
  87. package/src/__tests__/BloomThemeProvider.test.tsx +4 -3
  88. package/src/__tests__/apply-dark-class.test.ts +70 -0
  89. package/src/__tests__/native-root-vars.test.ts +8 -7
  90. package/src/__tests__/preset-vars.test.ts +42 -42
  91. package/src/__tests__/theme.test.ts +6 -6
  92. package/src/normalize-colors.d.ts +22 -0
  93. package/src/react-native-css.d.ts +3 -3
  94. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +574 -0
  95. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +838 -0
  96. package/src/theme/__tests__/color-space.test.ts +28 -0
  97. package/src/theme/__tests__/golden-tokens.test.ts +33 -0
  98. package/src/theme/__tests__/runtime-contract.test.ts +49 -0
  99. package/src/theme/__tests__/token-registry.test.ts +30 -0
  100. package/src/theme/__tests__/visual-gallery.test.tsx +10 -0
  101. package/src/theme/apply-dark-class.ts +15 -11
  102. package/src/theme/build-theme.ts +39 -67
  103. package/src/theme/color-presets.ts +11 -5
  104. package/src/theme/color-scope/index.web.tsx +17 -3
  105. package/src/theme/color-scope/style-builder.ts +7 -4
  106. package/src/theme/color-space.ts +56 -0
  107. package/src/theme/index.ts +1 -5
  108. package/src/theme/native-root-vars.native.ts +28 -18
  109. package/src/theme/preset-vars.ts +17 -126
  110. package/src/theme/token-registry.ts +159 -0
  111. package/src/theme/types.ts +3 -0
@@ -1,5 +1,10 @@
1
1
  import { Platform } from 'react-native';
2
2
  import { APP_COLOR_PRESETS, type AppColorName, type PresetTokens } from './color-presets';
3
+ // Circular at the module graph level (`token-registry` imports `getPresetVars`
4
+ // from here), but safe: `getResolvedTokens` is only referenced inside the
5
+ // `applyPresetVarsToDocument` function body — i.e. at call time, after both
6
+ // modules have finished evaluating — never during module initialization.
7
+ import { getResolvedTokens } from './token-registry';
3
8
 
4
9
  function extractHue(hslVar: string): number {
5
10
  return parseInt(hslVar.split(' ')[0] ?? '0', 10);
@@ -9,121 +14,6 @@ function extractSat(hslVar: string): number {
9
14
  return parseInt(hslVar.split(' ')[1] ?? '0', 10);
10
15
  }
11
16
 
12
- /**
13
- * Convert a shadcn-style HSL triple (`'H S% L%'`, optionally with an alpha tail
14
- * `'H S% L% / A'`) into an sRGB `rgb(...)` string.
15
- *
16
- * Why rgb and not hsl (the native alpha-utility bug this fixes)
17
- * -------------------------------------------------------------
18
- * Tailwind v4 compiles an alpha-on-theme-color utility (`bg-primary/10`) into a
19
- * `color-mix(... var(--color-primary) 10%, transparent)` declaration. On native,
20
- * react-native-css resolves that `color-mix` at runtime via `colorjs.io/fn`, but
21
- * its `color-mix` implementation registers ONLY the `sRGB`, `P3`, and `OKLab`
22
- * color spaces — never `HSL`. So if `--color-primary` resolves to an `hsl(...)`
23
- * string, `parse('hsl(...)')` throws, the mix is swallowed, and the utility
24
- * silently produces no color. Emitting the resolved var as `rgb(...)` (a space
25
- * colorjs.io always has registered) makes the alpha utilities resolve on native,
26
- * matching web. Full (non-alpha) utilities already worked because React Native's
27
- * native color parser handles `hsl()` directly; only `color-mix` was affected.
28
- *
29
- * Output is space-separated channels (`rgb(31 153 239)`), the modern CSS syntax
30
- * parsed by both `colorjs.io/fn` (with only sRGB/P3/OKLab registered) and
31
- * react-native-web / browsers. An alpha tail emits `rgb(r g b / a)`, likewise
32
- * parsed by both.
33
- *
34
- * Pure function — standard HSL→sRGB conversion, channels rounded to integers.
35
- * Tolerates a `deg` suffix on the hue and `%` suffixes on saturation/lightness.
36
- */
37
- export function hslTripletToRgb(triplet: string): string {
38
- const [colorPart, alphaPart] = triplet.split('/').map((part) => part.trim());
39
- const channels = (colorPart ?? '').split(/\s+/).filter(Boolean);
40
-
41
- const hue = parseFloat((channels[0] ?? '0').replace(/deg$/i, ''));
42
- const sat = parseFloat((channels[1] ?? '0').replace('%', '')) / 100;
43
- const light = parseFloat((channels[2] ?? '0').replace('%', '')) / 100;
44
-
45
- const h = Number.isFinite(hue) ? hue : 0;
46
- const s = Number.isFinite(sat) ? sat : 0;
47
- const l = Number.isFinite(light) ? light : 0;
48
-
49
- const chroma = (1 - Math.abs(2 * l - 1)) * s;
50
- const huePrime = ((((h % 360) + 360) % 360) / 60);
51
- const second = chroma * (1 - Math.abs((huePrime % 2) - 1));
52
-
53
- let r = 0;
54
- let g = 0;
55
- let b = 0;
56
- if (huePrime < 1) {
57
- [r, g, b] = [chroma, second, 0];
58
- } else if (huePrime < 2) {
59
- [r, g, b] = [second, chroma, 0];
60
- } else if (huePrime < 3) {
61
- [r, g, b] = [0, chroma, second];
62
- } else if (huePrime < 4) {
63
- [r, g, b] = [0, second, chroma];
64
- } else if (huePrime < 5) {
65
- [r, g, b] = [second, 0, chroma];
66
- } else {
67
- [r, g, b] = [chroma, 0, second];
68
- }
69
-
70
- const match = l - chroma / 2;
71
- const red = Math.round((r + match) * 255);
72
- const green = Math.round((g + match) * 255);
73
- const blue = Math.round((b + match) * 255);
74
-
75
- if (alphaPart !== undefined && alphaPart !== '') {
76
- const alpha = alphaPart.endsWith('%')
77
- ? parseFloat(alphaPart) / 100
78
- : parseFloat(alphaPart);
79
- const safeAlpha = Number.isFinite(alpha) ? alpha : 1;
80
- return `rgb(${red} ${green} ${blue} / ${safeAlpha})`;
81
- }
82
-
83
- return `rgb(${red} ${green} ${blue})`;
84
- }
85
-
86
- const RESOLVED_COLOR_MAP: Record<string, string> = {
87
- '--background': '--color-background',
88
- '--foreground': '--color-foreground',
89
- '--primary': '--color-primary',
90
- '--primary-foreground': '--color-primary-foreground',
91
- '--secondary': '--color-secondary',
92
- '--secondary-foreground': '--color-secondary-foreground',
93
- '--muted': '--color-muted',
94
- '--muted-foreground': '--color-muted-foreground',
95
- '--accent': '--color-accent',
96
- '--accent-foreground': '--color-accent-foreground',
97
- '--destructive': '--color-destructive',
98
- '--border': '--color-border',
99
- '--input': '--color-input',
100
- '--ring': '--color-ring',
101
- '--popover': '--color-popover',
102
- '--popover-foreground': '--color-popover-foreground',
103
- '--surface': '--color-surface',
104
- '--surface-foreground': '--color-surface-foreground',
105
- '--card': '--color-card',
106
- '--card-foreground': '--color-card-foreground',
107
- };
108
-
109
- export interface PresetVarsOptions {
110
- /**
111
- * Also emit Tailwind v4 resolved `--color-*` vars (as sRGB `rgb(...)` strings)
112
- * alongside the raw HSL triples. Needed when scoping a subtree where
113
- * Tailwind's `@theme` block has already precomputed `--color-*` at `:root`,
114
- * so overriding `--background` alone wouldn't cascade to `bg-background`.
115
- *
116
- * The resolved vars are emitted as `rgb(...)` rather than `hsl(...)` so the
117
- * alpha-on-theme-color utilities (`bg-primary/10`, `text-foreground/50`)
118
- * resolve on native: Tailwind compiles those to a runtime `color-mix(...)` that
119
- * react-native-css evaluates via `colorjs.io/fn`, which only registers the
120
- * sRGB/P3/OKLab spaces (not HSL) — so an `hsl(...)` var would throw and the
121
- * utility would silently render no color. See `hslTripletToRgb`. Default
122
- * `false`.
123
- */
124
- includeResolvedColorVars?: boolean;
125
- }
126
-
127
17
  /**
128
18
  * Bloom's base preset tokens extended with the surface/card/chart/sidebar
129
19
  * tokens that apps layer on top of the core shadcn palette. Synthesized from
@@ -131,11 +21,15 @@ export interface PresetVarsOptions {
131
21
  *
132
22
  * This is the single source of truth for extended theming vars — consumer apps
133
23
  * must not redefine these per-app.
24
+ *
25
+ * Values are platform-agnostic **raw HSL triples** (`'185 100% 20%'`, optionally
26
+ * with a `/ A` alpha tail). The single canonical resolver `getResolvedTokens`
27
+ * (in `token-registry.ts`) converts these to sRGB `rgb(...)` for BOTH web and
28
+ * native runtime writes — this function only produces the raw triple map.
134
29
  */
135
30
  export function getPresetVars(
136
31
  colorName: AppColorName,
137
32
  mode: 'light' | 'dark',
138
- options: PresetVarsOptions = {},
139
33
  ): PresetTokens {
140
34
  const preset = APP_COLOR_PRESETS[colorName];
141
35
  const base = mode === 'light' ? preset.light : preset.dark;
@@ -168,14 +62,7 @@ export function getPresetVars(
168
62
  '--sidebar-ring': get('--ring'),
169
63
  };
170
64
 
171
- if (!options.includeResolvedColorVars) return extended;
172
-
173
- const resolved: PresetTokens = { ...extended };
174
- for (const [rawKey, colorKey] of Object.entries(RESOLVED_COLOR_MAP)) {
175
- const value = extended[rawKey];
176
- if (value) resolved[colorKey] = hslTripletToRgb(value);
177
- }
178
- return resolved;
65
+ return extended;
179
66
  }
180
67
 
181
68
  /**
@@ -183,15 +70,19 @@ export function getPresetVars(
183
70
  * on native. `BloomThemeProvider` already writes the base preset vars on web;
184
71
  * call this only when an app needs the extended (card/chart/sidebar) tokens on
185
72
  * the document root.
73
+ *
74
+ * Tokens are resolved to sRGB `rgb(...)` via the single canonical
75
+ * `getResolvedTokens` pipeline, so `var(--x)` — the form Tailwind v4
76
+ * `@theme inline` compiles its color utilities to — resolves directly to a
77
+ * valid color on web.
186
78
  */
187
79
  export function applyPresetVarsToDocument(
188
80
  colorName: AppColorName,
189
81
  mode: 'light' | 'dark',
190
- options?: PresetVarsOptions,
191
82
  ): void {
192
83
  if (Platform.OS !== 'web' || typeof document === 'undefined') return;
193
84
 
194
- const vars = getPresetVars(colorName, mode, options);
85
+ const vars = getResolvedTokens(colorName, mode);
195
86
  const root = document.documentElement.style;
196
87
  for (const [key, value] of Object.entries(vars)) {
197
88
  root.setProperty(key, value);
@@ -0,0 +1,159 @@
1
+ import { APP_COLOR_NAMES, type AppColorName } from './color-presets';
2
+ import { getPresetVars } from './preset-vars';
3
+
4
+ /**
5
+ * The canonical set of Bloom theme token names — WITHOUT the leading `--`.
6
+ *
7
+ * This is the single source of truth for which tokens a preset produces. It
8
+ * covers exactly the keys returned by `getPresetVars(name, mode)` (the base
9
+ * shadcn palette plus the synthesized extended tokens: card/chart/content-area/
10
+ * sidebar-*). The list is intentionally hand-maintained and verified by
11
+ * `token-registry.test.ts` against the live `getPresetVars('oxy', 'light')`
12
+ * output, so any drift in the preset shape fails CI.
13
+ *
14
+ * Order mirrors `getPresetVars`'s insertion order: base tokens first (in the
15
+ * order they appear in the preset data), then the synthesized extended tokens.
16
+ */
17
+ export const CANONICAL_TOKENS = [
18
+ 'background',
19
+ 'foreground',
20
+ 'surface',
21
+ 'surface-foreground',
22
+ 'popover',
23
+ 'popover-foreground',
24
+ 'primary',
25
+ 'primary-foreground',
26
+ 'secondary',
27
+ 'secondary-foreground',
28
+ 'muted',
29
+ 'muted-foreground',
30
+ 'accent',
31
+ 'accent-foreground',
32
+ 'destructive',
33
+ 'border',
34
+ 'input',
35
+ 'ring',
36
+ 'sidebar',
37
+ 'card',
38
+ 'card-foreground',
39
+ 'chart-1',
40
+ 'chart-2',
41
+ 'chart-3',
42
+ 'chart-4',
43
+ 'chart-5',
44
+ 'content-area',
45
+ 'sidebar-foreground',
46
+ 'sidebar-primary',
47
+ 'sidebar-primary-foreground',
48
+ 'sidebar-accent',
49
+ 'sidebar-accent-foreground',
50
+ 'sidebar-border',
51
+ 'sidebar-ring',
52
+ ] as const;
53
+
54
+ /** A canonical token name (no leading `--`). */
55
+ export type CanonicalToken = (typeof CANONICAL_TOKENS)[number];
56
+
57
+ /**
58
+ * Matches a shadcn-style raw HSL triple — `H S% L%`, optionally with an alpha
59
+ * tail `H S% L% / A`. The value starts with a digit (or a leading `-`) and
60
+ * carries at least one `%` (the saturation/lightness units), which distinguishes
61
+ * a bare triple from an already-resolved color (`rgb(...)`, `#fff`) or a
62
+ * unitless/length token (`--radius: 0.5rem`).
63
+ */
64
+ const RAW_HSL_TRIPLE = /^-?\d[\d.]*\s+[\d.]+%/;
65
+
66
+ /**
67
+ * Convert a shadcn-style HSL triple (`'H S% L%'`, optionally with an alpha tail
68
+ * `'H S% L% / A'`) into an sRGB `rgb(...)` string.
69
+ *
70
+ * Standard HSL→sRGB math with integer-rounded channels and modern
71
+ * space-separated output (`rgb(r g b)` / `rgb(r g b / a)`). This is the SINGLE
72
+ * conversion the whole library uses — web document writes, web scope vars, and
73
+ * native `rootVariables` all flow through `getResolvedTokens`, which calls this.
74
+ * The byte-for-byte output is frozen by the golden parity fixture
75
+ * (`__fixtures__/golden-resolved-tokens.json`), so the visible palette cannot
76
+ * drift.
77
+ *
78
+ * Task 2's `oklchToSrgb` (in `color-space.ts`) stays available for a FUTURE
79
+ * OKLCH re-authoring of the seed palette; it is intentionally NOT used here —
80
+ * routing through OKLCH would shift the rounded channels off the frozen palette.
81
+ *
82
+ * Pure function — no side effects.
83
+ */
84
+ export function hslToSrgb(triple: string): string {
85
+ const [colorPart, alphaPart] = triple.split('/').map((part) => part.trim());
86
+ const channels = (colorPart ?? '').split(/\s+/).filter(Boolean);
87
+
88
+ const hue = parseFloat((channels[0] ?? '0').replace(/deg$/i, ''));
89
+ const sat = parseFloat((channels[1] ?? '0').replace('%', '')) / 100;
90
+ const light = parseFloat((channels[2] ?? '0').replace('%', '')) / 100;
91
+
92
+ const h = Number.isFinite(hue) ? hue : 0;
93
+ const s = Number.isFinite(sat) ? sat : 0;
94
+ const l = Number.isFinite(light) ? light : 0;
95
+
96
+ const chroma = (1 - Math.abs(2 * l - 1)) * s;
97
+ const huePrime = ((((h % 360) + 360) % 360) / 60);
98
+ const second = chroma * (1 - Math.abs((huePrime % 2) - 1));
99
+
100
+ let r = 0;
101
+ let g = 0;
102
+ let b = 0;
103
+ if (huePrime < 1) {
104
+ [r, g, b] = [chroma, second, 0];
105
+ } else if (huePrime < 2) {
106
+ [r, g, b] = [second, chroma, 0];
107
+ } else if (huePrime < 3) {
108
+ [r, g, b] = [0, chroma, second];
109
+ } else if (huePrime < 4) {
110
+ [r, g, b] = [0, second, chroma];
111
+ } else if (huePrime < 5) {
112
+ [r, g, b] = [second, 0, chroma];
113
+ } else {
114
+ [r, g, b] = [chroma, 0, second];
115
+ }
116
+
117
+ const match = l - chroma / 2;
118
+ const red = Math.round((r + match) * 255);
119
+ const green = Math.round((g + match) * 255);
120
+ const blue = Math.round((b + match) * 255);
121
+
122
+ if (alphaPart !== undefined && alphaPart !== '') {
123
+ const alpha = alphaPart.endsWith('%')
124
+ ? parseFloat(alphaPart) / 100
125
+ : parseFloat(alphaPart);
126
+ const safeAlpha = Number.isFinite(alpha) ? alpha : 1;
127
+ return `rgb(${red} ${green} ${blue} / ${safeAlpha})`;
128
+ }
129
+
130
+ return `rgb(${red} ${green} ${blue})`;
131
+ }
132
+
133
+ /**
134
+ * Resolve a preset's canonical runtime color tokens to sRGB `rgb(...)` strings.
135
+ *
136
+ * THE single source of runtime color tokens for the whole library, on BOTH web
137
+ * and native. Takes the raw triple map from `getPresetVars(preset, mode)` (base
138
+ * + extended) and converts every value that looks like an HSL triple via
139
+ * `hslToSrgb`. Non-triple values (already-resolved colors, length/radius tokens)
140
+ * pass through unchanged. Keys retain the `--` prefix, so each `--x` token IS a
141
+ * full CSS color — no resolved-color companion universe, no per-platform wrapping.
142
+ *
143
+ * The result is frozen by the golden parity fixture
144
+ * (`__fixtures__/golden-resolved-tokens.json`), guaranteeing the visible palette
145
+ * matches the previous resolved-color pipeline byte-for-byte.
146
+ */
147
+ export function getResolvedTokens(
148
+ preset: AppColorName,
149
+ mode: 'light' | 'dark',
150
+ ): Record<string, string> {
151
+ const raw = getPresetVars(preset, mode);
152
+ const resolved: Record<string, string> = {};
153
+ for (const [key, value] of Object.entries(raw)) {
154
+ resolved[key] = RAW_HSL_TRIPLE.test(value) ? hslToSrgb(value) : value;
155
+ }
156
+ return resolved;
157
+ }
158
+
159
+ export { APP_COLOR_NAMES };
@@ -14,9 +14,12 @@ export interface ThemeColors {
14
14
 
15
15
  primary: string;
16
16
  primaryForeground: string;
17
+ /** Preset accent tint (`--accent`) — a soft brand-tinted surface, NOT the page surface. */
17
18
  primaryLight: string;
19
+ /** Focus-ring shade (`--ring`) — the preset's emphasized ring/border color, NOT the page background. */
18
20
  primaryDark: string;
19
21
 
22
+ /** Preset secondary surface (`--secondary`) — a muted companion surface, NOT a mirror of `primary`. */
20
23
  secondary: string;
21
24
 
22
25
  tint: string;