@titan-design/react-ui 0.13.0 → 0.14.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 (146) hide show
  1. package/README.md +14 -0
  2. package/dist/bodymap.js +129 -76
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +129 -76
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +117 -26
  7. package/dist/index.d.ts +117 -26
  8. package/dist/index.js +677 -450
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +673 -451
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-_3Ajpiqu.d.ts → pages-CsjsUn0G.d.ts} +30 -13
  13. package/dist/{pages-CpW3OB3g.d.mts → pages-ZbT_pD_U.d.mts} +30 -13
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +676 -701
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +676 -701
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +31 -7
  21. package/dist/theme/index.d.ts +31 -7
  22. package/dist/theme/index.js +84 -20
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +83 -21
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +84 -20
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +84 -20
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +56 -7
  31. package/dist/theme/tokens.d.ts +56 -7
  32. package/dist/theme/tokens.js +72 -20
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +71 -21
  35. package/dist/theme/tokens.mjs.map +1 -1
  36. package/docs/render-testing-pattern.md +130 -0
  37. package/package.json +1 -1
  38. package/src/arch/Architecture.stories.tsx +3 -1
  39. package/src/arch/arch-graph.json +74 -72
  40. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +2 -1
  41. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +8 -3
  42. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  43. package/src/components/custom/Gauge/Gauge.stories.tsx +4 -1
  44. package/src/components/custom/Gauge/Gauge.tsx +3 -1
  45. package/src/components/custom/Scatter/Scatter.stories.tsx +10 -7
  46. package/src/components/custom/Scatter/Scatter.tsx +6 -4
  47. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  48. package/src/components/custom/Table/Table.stories.tsx +1 -1
  49. package/src/components/custom/Table/Table.tsx +2 -2
  50. package/src/components/custom/Treemap/Treemap.stories.tsx +13 -10
  51. package/src/components/custom/Treemap/Treemap.tsx +4 -1
  52. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +2 -1
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +11 -7
  54. package/src/components/custom/Workout/BaseBadge.tsx +3 -2
  55. package/src/components/custom/Workout/BodyMap.tsx +5 -3
  56. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +13 -3
  57. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +5 -3
  58. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +2 -1
  59. package/src/components/custom/Workout/CapacityBandChart.tsx +5 -3
  60. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  61. package/src/components/custom/Workout/DeviationBar.tsx +5 -2
  62. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +7 -6
  63. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +29 -16
  64. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -8
  65. package/src/components/custom/Workout/ExerciseCard.test.tsx +39 -19
  66. package/src/components/custom/Workout/ExerciseCard.tsx +92 -167
  67. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +89 -6
  68. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
  69. package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
  71. package/src/components/custom/Workout/ExerciseHeading.tsx +192 -47
  72. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -5
  73. package/src/components/custom/Workout/InputBar.tsx +4 -2
  74. package/src/components/custom/Workout/IntensityBar.tsx +4 -1
  75. package/src/components/custom/Workout/MesoCard.tsx +2 -1
  76. package/src/components/custom/Workout/MesoStatusCard.tsx +11 -10
  77. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +8 -3
  78. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +12 -2
  79. package/src/components/custom/Workout/README.md +58 -12
  80. package/src/components/custom/Workout/ReadinessCheck.tsx +4 -2
  81. package/src/components/custom/Workout/RestTimer.stories.tsx +13 -2
  82. package/src/components/custom/Workout/RestTimer.tsx +4 -2
  83. package/src/components/custom/Workout/SegmentedBar.stories.tsx +16 -12
  84. package/src/components/custom/Workout/SegmentedBar.tsx +4 -3
  85. package/src/components/custom/Workout/SetStrip.stories.tsx +3 -2
  86. package/src/components/custom/Workout/SetsRepsLoad.tsx +23 -9
  87. package/src/components/custom/Workout/Sparkline.stories.tsx +13 -10
  88. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +2 -1
  89. package/src/components/custom/Workout/StrengthTrendChart.tsx +8 -6
  90. package/src/components/custom/Workout/TempoDisplay.stories.tsx +4 -3
  91. package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
  92. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +3 -2
  93. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +1 -1
  94. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +40 -0
  95. package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
  96. package/src/components/custom/Workout/VelocityStrip.tsx +23 -14
  97. package/src/components/custom/Workout/ZoneTrack.stories.tsx +3 -1
  98. package/src/components/custom/Workout/exerciseRowState.ts +29 -0
  99. package/src/components/custom/Workout/index.ts +12 -2
  100. package/src/components/custom/Workout/metricText.stories.tsx +9 -8
  101. package/src/components/custom/Workout/metricText.tsx +4 -2
  102. package/src/components/custom/Workout/setHeadingKit.tsx +12 -10
  103. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  104. package/src/components/custom/charts/README.md +5 -0
  105. package/src/components/custom/charts/SetBarChart.test.tsx +16 -0
  106. package/src/components/custom/charts/SetBarChart.tsx +14 -5
  107. package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
  108. package/src/components/custom/charts/flatBarGeometry.ts +32 -0
  109. package/src/components/custom/stepper/Stepper.tsx +8 -6
  110. package/src/components/ui/alert/Alert.stories.tsx +14 -4
  111. package/src/components/ui/alert/Alert.tsx +24 -3
  112. package/src/components/ui/button/Button.tsx +12 -12
  113. package/src/components/ui/checkbox/Checkbox.tsx +2 -2
  114. package/src/components/ui/data-row/DataRow.stories.tsx +6 -2
  115. package/src/components/ui/drawer/Drawer.tsx +4 -1
  116. package/src/components/ui/icon-box/IconBox.tsx +8 -6
  117. package/src/components/ui/indicator/Indicator.stories.tsx +4 -3
  118. package/src/components/ui/list-item/ListItem.stories.tsx +7 -4
  119. package/src/components/ui/modal/Modal.tsx +2 -0
  120. package/src/components/ui/pill/Pill.stories.tsx +53 -0
  121. package/src/components/ui/pill/Pill.tsx +10 -6
  122. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  123. package/src/components/ui/progress/Progress.tsx +9 -6
  124. package/src/components/ui/radio/Radio.tsx +1 -1
  125. package/src/components/ui/section/Section.stories.tsx +10 -10
  126. package/src/components/ui/select/Select.tsx +2 -1
  127. package/src/components/ui/spinner/Spinner.tsx +3 -1
  128. package/src/components/ui/stack/Stack.stories.tsx +7 -7
  129. package/src/components/ui/surface/Surface.stories.tsx +75 -28
  130. package/src/components/ui/switch/Switch.tsx +1 -1
  131. package/src/components/ui/tabs/Tabs.tsx +4 -2
  132. package/src/components/ui/toast/Toast.tsx +6 -4
  133. package/src/components/ui/toolbar-button/ToolbarButton.tsx +13 -6
  134. package/src/stories/PresetShowcase.stories.tsx +1 -1
  135. package/src/test/no-raw-color.test.ts +39 -0
  136. package/src/theme/Alpha.stories.tsx +103 -0
  137. package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
  138. package/src/theme/config.ts +12 -0
  139. package/src/theme/global.css +15 -3
  140. package/src/theme/subtle-tone-weight.test.ts +163 -0
  141. package/src/theme/tailwind-var-opacity.test.ts +163 -0
  142. package/src/theme/tokens/primitives.ts +33 -0
  143. package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
  144. package/src/theme/tokens/semantic.ts +56 -20
  145. package/src/utils/colors.alpha-adoption.test.ts +408 -0
  146. package/tailwind.config.js +50 -9
@@ -0,0 +1,322 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { semanticColorsLight, semanticColorsDark, getSemanticColors } from './semantic'
3
+ import {
4
+ primitiveColors,
5
+ primitiveRamps,
6
+ greyRamp,
7
+ discreteRainbow,
8
+ alertRedVivid,
9
+ resultPaletteColors,
10
+ semanticPins,
11
+ } from './primitives'
12
+
13
+ /**
14
+ * Guard (VW-83): every hex-looking semantic token value must be a `primitiveRamps`
15
+ * step or a named primitive from `./primitives` — never a literal typed straight
16
+ * into `semantic.ts`. Scoped to hex values only: the many `rgba(...)` alpha
17
+ * overlays in this file are hand-composited, pre-existing, and out of scope for
18
+ * this ticket (which is about raw HEX occurrences).
19
+ *
20
+ * Deep-equal against the primitives module (not a regex on shape) so a future
21
+ * hex that happens to be well-formed but unbacked by any primitive still fails.
22
+ */
23
+ const HEX = /^#[0-9A-Fa-f]{3,8}$/
24
+
25
+ const allowedHexValues = new Set(
26
+ [
27
+ ...Object.values(primitiveColors),
28
+ ...Object.values(greyRamp),
29
+ ...Object.values(primitiveRamps).flatMap((hue) => Object.values(hue)),
30
+ ...discreteRainbow,
31
+ alertRedVivid,
32
+ ...Object.values(resultPaletteColors),
33
+ ...Object.values(semanticPins),
34
+ ]
35
+ .filter((v): v is string => HEX.test(v))
36
+ .map((v) => v.toLowerCase())
37
+ )
38
+
39
+ function hexEntries(colors: Record<string, string>): [string, string][] {
40
+ return Object.entries(colors).filter(([, value]) => HEX.test(value))
41
+ }
42
+
43
+ describe('semantic token hex guard (VW-83)', () => {
44
+ it('has hex tokens to check in both modes (sanity: the guard is not vacuous)', () => {
45
+ expect(hexEntries(semanticColorsLight).length).toBeGreaterThan(0)
46
+ expect(hexEntries(semanticColorsDark).length).toBeGreaterThan(0)
47
+ })
48
+
49
+ it('resolves every light-mode hex token to a ramp step or a named primitive', () => {
50
+ for (const [token, value] of hexEntries(semanticColorsLight)) {
51
+ expect(allowedHexValues.has(value.toLowerCase()), `light '${token}': ${value}`).toBe(true)
52
+ }
53
+ })
54
+
55
+ it('resolves every dark-mode hex token to a ramp step or a named primitive', () => {
56
+ for (const [token, value] of hexEntries(semanticColorsDark)) {
57
+ expect(allowedHexValues.has(value.toLowerCase()), `dark '${token}': ${value}`).toBe(true)
58
+ }
59
+ })
60
+ })
61
+
62
+ /**
63
+ * Resolved values captured from `main` (pre-VW-83), before any semantic.ts
64
+ * literal was replaced by a ramp step or a named primitive. The migration must
65
+ * not change what any token paints — only where its value comes from.
66
+ *
67
+ * AMENDED by AW-133, which is NOT a refactor and does change what some tokens
68
+ * paint. Splitting text-on-subtle from the base tone token added the six
69
+ * `on-*-subtle` entries, and rebased three dark `-subtle` fills onto the hue
70
+ * their label now uses (`brand-secondary`, `status-error`, `status-info`). Those
71
+ * deltas are deliberate and reviewed; everything else here is still the
72
+ * pre-migration pin. Treat a diff against this fixture as a question to answer,
73
+ * not a number to refresh — regenerating it wholesale defeats the guard.
74
+ */
75
+ const resolvedBeforeFixture = {
76
+ light: {
77
+ 'brand-primary': '#FF7900',
78
+ 'brand-primary-light': '#FFA063',
79
+ 'brand-primary-dark': '#DA5F00',
80
+ 'brand-primary-subtle': 'rgba(255, 121, 0, 0.08)',
81
+ 'brand-primary-muted': 'rgba(255, 121, 0, 0.30)',
82
+ 'brand-primary-strong': 'rgba(255, 121, 0, 0.50)',
83
+ 'brand-primary-hover': '#DA5F00',
84
+ 'brand-primary-active': '#B94A00',
85
+ 'brand-secondary': '#307B9B',
86
+ 'brand-secondary-light': '#2697B7',
87
+ 'brand-secondary-dark': '#2A617F',
88
+ 'brand-secondary-subtle': 'rgba(48, 123, 155, 0.08)',
89
+ 'brand-secondary-muted': 'rgba(48, 123, 155, 0.30)',
90
+ 'brand-secondary-strong': 'rgba(48, 123, 155, 0.50)',
91
+ 'brand-secondary-hover': '#2A617F',
92
+ 'brand-secondary-active': '#22465F',
93
+ 'on-brand-primary': '#FFFFFF',
94
+ 'on-brand-secondary': '#FFFFFF',
95
+ 'on-brand-primary-subtle': '#FF7900',
96
+ 'on-brand-secondary-subtle': '#307B9B',
97
+ 'status-success': '#2ED573',
98
+ 'status-success-light': '#58F69E',
99
+ 'status-success-dark': '#298732',
100
+ 'status-success-subtle': '#E3FFEE',
101
+ 'status-success-muted': 'rgba(46, 213, 115, 0.30)',
102
+ 'status-success-strong': 'rgba(46, 213, 115, 0.50)',
103
+ 'status-live': '#2ED573',
104
+ 'status-live-muted': '#22A444',
105
+ 'status-error': '#D14343',
106
+ 'status-error-light': '#E05254',
107
+ 'status-error-dark': '#A4221C',
108
+ 'status-error-subtle': '#FFF4F4',
109
+ 'status-error-muted': 'rgba(209, 67, 67, 0.30)',
110
+ 'status-error-strong': 'rgba(209, 67, 67, 0.50)',
111
+ 'status-error-vivid': '#FF4757',
112
+ 'status-error-vivid-light': '#E05254',
113
+ 'status-error-vivid-dark': '#A4221C',
114
+ 'status-error-vivid-subtle': 'rgba(255, 71, 87, 0.12)',
115
+ 'status-error-vivid-muted': 'rgba(255, 71, 87, 0.30)',
116
+ 'status-error-vivid-strong': 'rgba(255, 71, 87, 0.50)',
117
+ 'status-warning': '#F9B415',
118
+ 'status-warning-light': '#FFD352',
119
+ 'status-warning-dark': '#C27400',
120
+ 'status-warning-subtle': '#FFF7DD',
121
+ 'status-warning-muted': 'rgba(249, 180, 21, 0.30)',
122
+ 'status-warning-strong': 'rgba(249, 180, 21, 0.50)',
123
+ 'status-info': '#2196F3',
124
+ 'status-info-light': '#78C2FF',
125
+ 'status-info-dark': '#1072CB',
126
+ 'status-info-subtle': '#EFF8FF',
127
+ 'status-info-muted': 'rgba(33, 150, 243, 0.30)',
128
+ 'status-info-strong': 'rgba(33, 150, 243, 0.50)',
129
+ 'on-status-success': '#FFFFFF',
130
+ 'on-status-error': '#FFFFFF',
131
+ 'on-status-warning': '#FFFFFF',
132
+ 'on-status-info': '#FFFFFF',
133
+ 'on-status-success-subtle': '#2ED573',
134
+ 'on-status-error-subtle': '#D14343',
135
+ 'on-status-warning-subtle': '#F9B415',
136
+ 'on-status-info-subtle': '#2196F3',
137
+ 'result-improve': '#4caf50',
138
+ 'result-improve-light': 'rgba(76, 175, 80, 0.12)',
139
+ 'result-improve-dark': '#248a24',
140
+ 'result-degrade': '#ef5350',
141
+ 'result-degrade-light': 'rgba(239, 83, 80, 0.12)',
142
+ 'result-degrade-dark': '#b30000',
143
+ 'result-inconclusive': '#9E9A97',
144
+ 'result-inconclusive-light': 'rgba(158, 154, 151, 0.12)',
145
+ 'result-neutral': '#72716F',
146
+ 'on-result-improve': '#FFFFFF',
147
+ 'on-result-degrade': '#FFFFFF',
148
+ 'on-result-inconclusive': '#FFFFFF',
149
+ 'data-1': '#1965B0',
150
+ 'data-2': '#4EB265',
151
+ 'data-3': '#F7F056',
152
+ 'data-4': '#DC050C',
153
+ 'data-5': '#882E72',
154
+ 'data-6': '#F4A736',
155
+ 'data-7': '#7BAFDE',
156
+ 'data-8': '#90C987',
157
+ 'data-9': '#CAACCB',
158
+ 'data-10': '#EE8026',
159
+ 'text-primary': '#121828',
160
+ 'text-secondary': '#65748B',
161
+ 'text-tertiary': '#A29F9D',
162
+ 'text-disabled': 'rgba(55, 65, 81, 0.48)',
163
+ 'text-inverse': '#FFFFFF',
164
+ 'text-link': '#1072CB',
165
+ 'text-link-hover': '#135AA8',
166
+ 'surface-base': '#FFFFFF',
167
+ 'surface-elevated': '#F9F6F3',
168
+ 'surface-raised': '#EDEAE7',
169
+ 'surface-overlay': '#FFFFFF',
170
+ 'surface-input': '#F9F6F3',
171
+ 'background-base': '#EBEBEB',
172
+ 'background-default': '#FFFFFF',
173
+ 'background-subtle': '#F9F6F3',
174
+ 'background-frame': '#A29F9D',
175
+ 'border-prominent': '#A29F9D',
176
+ 'border-focus': '#1072CB',
177
+ 'border-input': '#D4D1CE',
178
+ 'border-input-hover': '#A29F9D',
179
+ 'border-input-focus': '#1072CB',
180
+ 'border-input-error': '#D14343',
181
+ 'hairline-subtle': 'rgba(0, 0, 0, 0.06)',
182
+ 'hairline-default': 'rgba(0, 0, 0, 0.09)',
183
+ 'hairline-strong': 'rgba(0, 0, 0, 0.14)',
184
+ 'interactive-hover': 'rgba(55, 65, 81, 0.04)',
185
+ 'interactive-focus': 'rgba(55, 65, 81, 0.12)',
186
+ 'interactive-active': 'rgba(55, 65, 81, 0.16)',
187
+ 'interactive-selected': 'rgba(55, 65, 81, 0.08)',
188
+ 'interactive-disabled': 'rgba(55, 65, 81, 0.12)',
189
+ 'interactive-disabled-text': 'rgba(55, 65, 81, 0.26)',
190
+ divider: '#E8E9EB',
191
+ 'avatar-background': '#72716F',
192
+ 'avatar-text': '#FFFFFF',
193
+ },
194
+ dark: {
195
+ 'brand-primary': '#FF7900',
196
+ 'brand-primary-light': '#FFA063',
197
+ 'brand-primary-dark': '#DA5F00',
198
+ 'brand-primary-subtle': 'rgba(255, 121, 0, 0.12)',
199
+ 'brand-primary-muted': 'rgba(255, 121, 0, 0.30)',
200
+ 'brand-primary-strong': 'rgba(255, 121, 0, 0.50)',
201
+ 'brand-primary-hover': '#FFA063',
202
+ 'brand-primary-active': '#FFC7A2',
203
+ 'brand-secondary': '#307B9B',
204
+ 'brand-secondary-light': '#2697B7',
205
+ 'brand-secondary-dark': '#2A617F',
206
+ 'brand-secondary-subtle': 'rgba(34, 211, 238, 0.12)',
207
+ 'brand-secondary-muted': 'rgba(48, 123, 155, 0.30)',
208
+ 'brand-secondary-strong': 'rgba(48, 123, 155, 0.50)',
209
+ 'brand-secondary-hover': '#2697B7',
210
+ 'brand-secondary-active': '#01B5D1',
211
+ 'on-brand-primary': '#FFFFFF',
212
+ 'on-brand-secondary': '#FFFFFF',
213
+ 'on-brand-primary-subtle': '#FF7900',
214
+ 'on-brand-secondary-subtle': '#22D3EE',
215
+ 'status-success': '#2ED573',
216
+ 'status-success-light': '#58F69E',
217
+ 'status-success-dark': '#298732',
218
+ 'status-success-subtle': 'rgba(46, 213, 115, 0.12)',
219
+ 'status-success-muted': 'rgba(46, 213, 115, 0.30)',
220
+ 'status-success-strong': 'rgba(46, 213, 115, 0.50)',
221
+ 'status-live': '#2ED573',
222
+ 'status-live-muted': '#22A444',
223
+ 'status-error': '#D14343',
224
+ 'status-error-light': '#E05254',
225
+ 'status-error-dark': '#A4221C',
226
+ 'status-error-subtle': 'rgba(247, 113, 117, 0.08)',
227
+ 'status-error-muted': 'rgba(209, 67, 67, 0.30)',
228
+ 'status-error-strong': 'rgba(209, 67, 67, 0.50)',
229
+ 'status-error-vivid': '#FF4757',
230
+ 'status-error-vivid-light': '#E05254',
231
+ 'status-error-vivid-dark': '#A4221C',
232
+ 'status-error-vivid-subtle': 'rgba(255, 71, 87, 0.12)',
233
+ 'status-error-vivid-muted': 'rgba(255, 71, 87, 0.30)',
234
+ 'status-error-vivid-strong': 'rgba(255, 71, 87, 0.50)',
235
+ 'status-warning': '#F9B415',
236
+ 'status-warning-light': '#FFD352',
237
+ 'status-warning-dark': '#C27400',
238
+ 'status-warning-subtle': 'rgba(249, 180, 21, 0.12)',
239
+ 'status-warning-muted': 'rgba(249, 180, 21, 0.30)',
240
+ 'status-warning-strong': 'rgba(249, 180, 21, 0.50)',
241
+ 'status-info': '#2196F3',
242
+ 'status-info-light': '#78C2FF',
243
+ 'status-info-dark': '#1072CB',
244
+ 'status-info-subtle': 'rgba(120, 194, 255, 0.12)',
245
+ 'status-info-muted': 'rgba(33, 150, 243, 0.30)',
246
+ 'status-info-strong': 'rgba(33, 150, 243, 0.50)',
247
+ 'on-status-success': '#FFFFFF',
248
+ 'on-status-error': '#FFFFFF',
249
+ 'on-status-warning': '#FFFFFF',
250
+ 'on-status-info': '#FFFFFF',
251
+ 'on-status-success-subtle': '#2ED573',
252
+ 'on-status-error-subtle': '#F77175',
253
+ 'on-status-warning-subtle': '#F9B415',
254
+ 'on-status-info-subtle': '#78C2FF',
255
+ 'result-improve': '#4caf50',
256
+ 'result-improve-light': 'rgba(76, 175, 80, 0.16)',
257
+ 'result-improve-dark': '#248a24',
258
+ 'result-degrade': '#ef5350',
259
+ 'result-degrade-light': 'rgba(239, 83, 80, 0.16)',
260
+ 'result-degrade-dark': '#b30000',
261
+ 'result-inconclusive': '#9E9A97',
262
+ 'result-inconclusive-light': 'rgba(158, 154, 151, 0.16)',
263
+ 'result-neutral': '#A29F9D',
264
+ 'on-result-improve': '#FFFFFF',
265
+ 'on-result-degrade': '#FFFFFF',
266
+ 'on-result-inconclusive': '#FFFFFF',
267
+ 'data-1': '#1965B0',
268
+ 'data-2': '#4EB265',
269
+ 'data-3': '#F7F056',
270
+ 'data-4': '#DC050C',
271
+ 'data-5': '#882E72',
272
+ 'data-6': '#F4A736',
273
+ 'data-7': '#7BAFDE',
274
+ 'data-8': '#90C987',
275
+ 'data-9': '#CAACCB',
276
+ 'data-10': '#EE8026',
277
+ 'text-primary': '#F9F6F3',
278
+ 'text-secondary': '#A29F9D',
279
+ 'text-tertiary': '#888684',
280
+ 'text-disabled': 'rgba(255, 255, 255, 0.38)',
281
+ 'text-inverse': '#1C1916',
282
+ 'text-link': '#828DF8',
283
+ 'text-link-hover': '#3CA8FF',
284
+ 'surface-base': '#252321',
285
+ 'surface-elevated': '#2C2A28',
286
+ 'surface-raised': '#31302F',
287
+ 'surface-overlay': '#373635',
288
+ 'surface-input': '#2C2A28',
289
+ 'background-base': '#1C1916',
290
+ 'background-default': '#252321',
291
+ 'background-subtle': '#2C2A28',
292
+ 'background-frame': '#100D0A',
293
+ 'border-prominent': '#424140',
294
+ 'border-focus': '#828DF8',
295
+ 'border-input': '#5A5958',
296
+ 'border-input-hover': '#72716F',
297
+ 'border-input-focus': '#828DF8',
298
+ 'border-input-error': '#E05254',
299
+ 'hairline-subtle': 'rgba(255, 255, 255, 0.10)',
300
+ 'hairline-default': 'rgba(255, 255, 255, 0.15)',
301
+ 'hairline-strong': 'rgba(255, 255, 255, 0.22)',
302
+ 'interactive-hover': 'rgba(255, 255, 255, 0.04)',
303
+ 'interactive-focus': 'rgba(255, 255, 255, 0.12)',
304
+ 'interactive-active': 'rgba(255, 255, 255, 0.16)',
305
+ 'interactive-selected': 'rgba(255, 255, 255, 0.08)',
306
+ 'interactive-disabled': 'rgba(255, 255, 255, 0.12)',
307
+ 'interactive-disabled-text': 'rgba(255, 255, 255, 0.26)',
308
+ divider: 'rgba(255, 255, 255, 0.09)',
309
+ 'avatar-background': '#5A5958',
310
+ 'avatar-text': '#FFFFFF',
311
+ },
312
+ } as const
313
+
314
+ describe('semantic token resolved values are unchanged by the hex migration (VW-83)', () => {
315
+ it('matches the pre-migration fixture in light mode', () => {
316
+ expect(getSemanticColors('light')).toEqual(resolvedBeforeFixture.light)
317
+ })
318
+
319
+ it('matches the pre-migration fixture in dark mode', () => {
320
+ expect(getSemanticColors('dark')).toEqual(resolvedBeforeFixture.dark)
321
+ })
322
+ })
@@ -23,6 +23,8 @@ import {
23
23
  greyRamp,
24
24
  discreteRainbow,
25
25
  alertRedVivid,
26
+ resultPaletteColors,
27
+ semanticPins,
26
28
  } from './primitives'
27
29
 
28
30
  /**
@@ -77,6 +79,13 @@ export const semanticColorsLight = {
77
79
  'on-brand-primary': p.white,
78
80
  'on-brand-secondary': p.white,
79
81
 
82
+ // Text ON a `-subtle` fill. Light mode keeps today's pairing (the base token) so
83
+ // nothing moves here: light has the same disease mirrored — a light rung on a
84
+ // near-white ramp[50] fill — but correcting it belongs with the light-mode pass
85
+ // in AW-121, not this one.
86
+ 'on-brand-primary-subtle': ramp.orange[400],
87
+ 'on-brand-secondary-subtle': ramp.cyan[600],
88
+
80
89
  // Status colors (status-*)
81
90
  'status-success': ramp.green[300],
82
91
  'status-success-light': ramp.green[200],
@@ -117,6 +126,12 @@ export const semanticColorsLight = {
117
126
  'status-info-muted': 'rgba(33, 150, 243, 0.30)',
118
127
  'status-info-strong': 'rgba(33, 150, 243, 0.50)',
119
128
 
129
+ // Text ON a `-subtle` fill — see the on-brand-*-subtle note above.
130
+ 'on-status-success-subtle': ramp.green[300],
131
+ 'on-status-error-subtle': ramp.red[600],
132
+ 'on-status-warning-subtle': ramp.amber[300],
133
+ 'on-status-info-subtle': ramp.blue[500],
134
+
120
135
  // Text on status backgrounds (on-status-*)
121
136
  'on-status-success': p.white,
122
137
  'on-status-error': p.white,
@@ -124,13 +139,13 @@ export const semanticColorsLight = {
124
139
  'on-status-info': p.white,
125
140
 
126
141
  // Result/outcome indicators (result-*)
127
- 'result-improve': '#4caf50', // Green - positive outcome
142
+ 'result-improve': resultPaletteColors.improve, // Green - positive outcome
128
143
  'result-improve-light': 'rgba(76, 175, 80, 0.12)',
129
- 'result-improve-dark': '#248a24',
130
- 'result-degrade': '#ef5350', // Red - negative outcome
144
+ 'result-improve-dark': resultPaletteColors.improveDark,
145
+ 'result-degrade': resultPaletteColors.degrade, // Red - negative outcome
131
146
  'result-degrade-light': 'rgba(239, 83, 80, 0.12)',
132
- 'result-degrade-dark': '#b30000',
133
- 'result-inconclusive': '#9E9A97', // Gray - no clear result
147
+ 'result-degrade-dark': resultPaletteColors.degradeDark,
148
+ 'result-inconclusive': resultPaletteColors.inconclusive, // Gray - no clear result
134
149
  'result-inconclusive-light': 'rgba(158, 154, 151, 0.12)',
135
150
  'result-neutral': greyRamp[600], // Neutral baseline
136
151
 
@@ -153,8 +168,8 @@ export const semanticColorsLight = {
153
168
  'data-10': discreteRainbow[22], // Dark Orange
154
169
 
155
170
  // Text colors (text-*)
156
- 'text-primary': '#121828',
157
- 'text-secondary': '#65748B',
171
+ 'text-primary': semanticPins.textPrimaryLight,
172
+ 'text-secondary': semanticPins.textSecondaryLight,
158
173
  'text-tertiary': greyRamp[400],
159
174
  'text-disabled': 'rgba(55, 65, 81, 0.48)',
160
175
  'text-inverse': p.white,
@@ -169,7 +184,7 @@ export const semanticColorsLight = {
169
184
  'surface-input': greyRamp[50], // Input field background (filled variant)
170
185
 
171
186
  // Background colors (background-*)
172
- 'background-base': '#EBEBEB',
187
+ 'background-base': semanticPins.backgroundBaseLight,
173
188
  'background-default': p.white,
174
189
  'background-subtle': greyRamp[50],
175
190
  // Frame/bezel chrome — top bar + side nav shell, one step below
@@ -199,7 +214,7 @@ export const semanticColorsLight = {
199
214
  'interactive-disabled-text': 'rgba(55, 65, 81, 0.26)',
200
215
 
201
216
  // Divider
202
- divider: '#E8E9EB',
217
+ divider: semanticPins.dividerLight,
203
218
 
204
219
  // Avatar default
205
220
  'avatar-background': greyRamp[600],
@@ -221,7 +236,7 @@ export const semanticColorsDark = {
221
236
  'brand-secondary': ramp.cyan[600],
222
237
  'brand-secondary-light': ramp.cyan[500],
223
238
  'brand-secondary-dark': ramp.cyan[700],
224
- 'brand-secondary-subtle': 'rgba(48, 123, 155, 0.12)',
239
+ 'brand-secondary-subtle': 'rgba(34, 211, 238, 0.12)',
225
240
  'brand-secondary-muted': 'rgba(48, 123, 155, 0.30)',
226
241
  'brand-secondary-strong': 'rgba(48, 123, 155, 0.50)',
227
242
  'brand-secondary-hover': ramp.cyan[500],
@@ -231,6 +246,15 @@ export const semanticColorsDark = {
231
246
  'on-brand-primary': p.white,
232
247
  'on-brand-secondary': p.white,
233
248
 
249
+ // Text ON a `-subtle` fill. Its own role: `brand-primary` and friends are tuned to
250
+ // carry a white label as a solid fill, which makes the two deepest of them
251
+ // (cyan[600], red[600]) too dark to read as text on a dark plane. Levelling the
252
+ // family at rung 300 puts every tone within OKLCH L 0.769-0.813 instead of
253
+ // 0.550-0.813. Brand is deliberately left at its own orange[400] (operator, AW-133)
254
+ // so the Voltras tone never drifts; it is the one tone under AA on a raised card.
255
+ 'on-brand-primary-subtle': ramp.orange[400],
256
+ 'on-brand-secondary-subtle': ramp.cyan[300],
257
+
234
258
  // Status colors
235
259
  'status-success': ramp.green[300],
236
260
  'status-success-light': ramp.green[200],
@@ -246,7 +270,10 @@ export const semanticColorsDark = {
246
270
  'status-error': ramp.red[600],
247
271
  'status-error-light': ramp.red[500],
248
272
  'status-error-dark': ramp.red[700],
249
- 'status-error-subtle': 'rgba(209, 67, 67, 0.12)',
273
+ // Alpha 0.08, not the family's 0.12: error's label is red[400], a rung darker than
274
+ // its siblings so it reads RED rather than pink (rung 300 has only 0.121 chroma).
275
+ // Thinning the fill buys back the contrast that extra darkness costs. See AW-133.
276
+ 'status-error-subtle': 'rgba(247, 113, 117, 0.08)',
250
277
  'status-error-muted': 'rgba(209, 67, 67, 0.30)',
251
278
  'status-error-strong': 'rgba(209, 67, 67, 0.50)',
252
279
 
@@ -267,7 +294,7 @@ export const semanticColorsDark = {
267
294
  'status-info': ramp.blue[500],
268
295
  'status-info-light': ramp.blue[300],
269
296
  'status-info-dark': ramp.blue[600],
270
- 'status-info-subtle': 'rgba(33, 150, 243, 0.12)',
297
+ 'status-info-subtle': 'rgba(120, 194, 255, 0.12)',
271
298
  'status-info-muted': 'rgba(33, 150, 243, 0.30)',
272
299
  'status-info-strong': 'rgba(33, 150, 243, 0.50)',
273
300
 
@@ -277,14 +304,23 @@ export const semanticColorsDark = {
277
304
  'on-status-warning': p.white,
278
305
  'on-status-info': p.white,
279
306
 
307
+ // Text ON a `-subtle` fill — see the on-brand-*-subtle note above. Error is the
308
+ // second deliberate exception to the rung-300 rule (operator, AW-133): red[300]
309
+ // levelled perfectly but read PINK, because a red that light can only hold 0.121
310
+ // chroma. red[400] carries 0.165 and reads red; its fill is thinned to compensate.
311
+ 'on-status-success-subtle': ramp.green[300],
312
+ 'on-status-error-subtle': ramp.red[400],
313
+ 'on-status-warning-subtle': ramp.amber[300],
314
+ 'on-status-info-subtle': ramp.blue[300],
315
+
280
316
  // Result/outcome indicators (result-*)
281
- 'result-improve': '#4caf50',
317
+ 'result-improve': resultPaletteColors.improve,
282
318
  'result-improve-light': 'rgba(76, 175, 80, 0.16)',
283
- 'result-improve-dark': '#248a24',
284
- 'result-degrade': '#ef5350',
319
+ 'result-improve-dark': resultPaletteColors.improveDark,
320
+ 'result-degrade': resultPaletteColors.degrade,
285
321
  'result-degrade-light': 'rgba(239, 83, 80, 0.16)',
286
- 'result-degrade-dark': '#b30000',
287
- 'result-inconclusive': '#9E9A97',
322
+ 'result-degrade-dark': resultPaletteColors.degradeDark,
323
+ 'result-inconclusive': resultPaletteColors.inconclusive,
288
324
  'result-inconclusive-light': 'rgba(158, 154, 151, 0.16)',
289
325
  'result-neutral': greyRamp[400],
290
326
 
@@ -318,7 +354,7 @@ export const semanticColorsDark = {
318
354
  'text-tertiary': greyRamp[500],
319
355
  'text-disabled': 'rgba(255, 255, 255, 0.38)',
320
356
  'text-inverse': greyRamp[950],
321
- 'text-link': '#828DF8',
357
+ 'text-link': semanticPins.focusIndigoDark,
322
358
  'text-link-hover': ramp.blue[400],
323
359
 
324
360
  // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
@@ -361,10 +397,10 @@ export const semanticColorsDark = {
361
397
  // outright (4 call sites, high-visibility dividers), and grey-800 is not a
362
398
  // plane, so it keeps its job without collision.
363
399
  'border-prominent': greyRamp[800], // high-visibility divider
364
- 'border-focus': '#828DF8',
400
+ 'border-focus': semanticPins.focusIndigoDark,
365
401
  'border-input': greyRamp[700],
366
402
  'border-input-hover': greyRamp[600],
367
- 'border-input-focus': '#828DF8',
403
+ 'border-input-focus': semanticPins.focusIndigoDark,
368
404
  'border-input-error': ramp.red[500],
369
405
 
370
406
  // Alpha hairline separators — the primary separation cue (§4/S-2). Self-