@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,408 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { describe, it, expect } from 'vitest'
5
+ import { alpha } from './colors'
6
+ import { getSemanticColors } from '../theme/tokens/semantic'
7
+ import { primitiveColors } from '../theme/tokens/primitives'
8
+
9
+ /**
10
+ * VW-78: every `rgba(...)` literal swapped for `alpha(<resolved token>, opacity)`
11
+ * across the functional-colour baseline files must resolve to the exact same
12
+ * colour it replaced, AND the raw literal must actually be gone from the
13
+ * touched source file — two separate checks, because they catch different
14
+ * regressions:
15
+ *
16
+ * - "resolves to the same colour" (below) re-derives the swap independently
17
+ * from the token and `alpha()`, so it catches a bad token value or a bug in
18
+ * `alpha()` itself.
19
+ * - "no longer contains the swapped literal" reads the ACTUAL component file
20
+ * and catches someone reverting a call site back to a raw literal, or
21
+ * copy-pasting the wrong token into it — neither of which the first check
22
+ * can see, since it never looks at the file the swap lives in.
23
+ */
24
+
25
+ function parseRgba(value: string): [number, number, number, number] {
26
+ const m = value.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s]+([\d.]+))?\s*\)/)
27
+ if (!m) throw new Error(`Not an rgb()/rgba() value: ${value}`)
28
+ return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] === undefined ? 1 : Number(m[4])]
29
+ }
30
+
31
+ /**
32
+ * `alphaTolerance` accommodates `.toFixed(3)`-rounded originals (MesoCard's
33
+ * dynamic heatmap opacity): the rounding itself can differ from the exact
34
+ * value by up to 0.0005, which is not a colour difference.
35
+ */
36
+ function expectSameColor(actual: string, original: string, alphaTolerance = 1e-6) {
37
+ const [ar, ag, ab, aa] = parseRgba(actual)
38
+ const [or_, og, ob, oa] = parseRgba(original)
39
+ expect([ar, ag, ab]).toEqual([or_, og, ob])
40
+ expect(Math.abs(aa - oa)).toBeLessThanOrEqual(alphaTolerance)
41
+ }
42
+
43
+ const dark = getSemanticColors('dark')
44
+ const BRAND_PRIMARY = dark['brand-primary']
45
+
46
+ describe('alpha() adoption (VW-78) — swapped literal parity', () => {
47
+ const staticCases: Array<{ label: string; original: string; swapped: string }> = [
48
+ // Gauge.tsx
49
+ {
50
+ label: 'Gauge TRACK',
51
+ original: 'rgba(255,255,255,0.08)',
52
+ swapped: alpha(primitiveColors.white, 0.08),
53
+ },
54
+ // Scatter.tsx
55
+ {
56
+ label: 'Scatter GRID_LINE',
57
+ original: 'rgba(255,255,255,0.07)',
58
+ swapped: alpha(primitiveColors.white, 0.07),
59
+ },
60
+ {
61
+ label: 'Scatter AXIS_LINE',
62
+ original: 'rgba(255,255,255,0.18)',
63
+ swapped: alpha(primitiveColors.white, 0.18),
64
+ },
65
+ {
66
+ label: 'Scatter DIAGONAL_LINE',
67
+ original: 'rgba(255,255,255,0.28)',
68
+ swapped: alpha(primitiveColors.white, 0.28),
69
+ },
70
+ // BaseBadge.tsx
71
+ {
72
+ label: 'BaseBadge pr.bg',
73
+ original: 'rgba(255, 121, 0, 0.12)',
74
+ swapped: alpha(dark['brand-primary'], 0.12),
75
+ },
76
+ {
77
+ label: 'BaseBadge pr.border',
78
+ original: 'rgba(255, 121, 0, 0.3)',
79
+ swapped: alpha(dark['brand-primary'], 0.3),
80
+ },
81
+ // BodyMap.tsx
82
+ {
83
+ label: 'BodyMap OUTLINE_FILL',
84
+ original: 'rgba(255,255,255,0.08)',
85
+ swapped: alpha(primitiveColors.white, 0.08),
86
+ },
87
+ {
88
+ label: 'BodyMap OUTLINE_BORDER',
89
+ original: 'rgba(255,255,255,0.12)',
90
+ swapped: alpha(primitiveColors.white, 0.12),
91
+ },
92
+ {
93
+ label: 'BodyMap toggle active bg',
94
+ original: 'rgba(255,121,0,0.16)',
95
+ swapped: alpha(BRAND_PRIMARY, 0.16),
96
+ },
97
+ // BodyMapDetailPanel.tsx
98
+ {
99
+ label: 'BodyMapDetailPanel bg',
100
+ original: 'rgba(255,121,0,0.12)',
101
+ swapped: alpha(BRAND_PRIMARY, 0.12),
102
+ },
103
+ {
104
+ label: 'BodyMapDetailPanel border',
105
+ original: 'rgba(255,121,0,0.3)',
106
+ swapped: alpha(BRAND_PRIMARY, 0.3),
107
+ },
108
+ // CapacityBandChart.tsx
109
+ {
110
+ label: 'CapacityBandChart BAND_FILL',
111
+ original: 'rgba(46,213,115,0.1)',
112
+ swapped: alpha(dark['status-success'], 0.1),
113
+ },
114
+ {
115
+ label: 'CapacityBandChart BAND_EDGE',
116
+ original: 'rgba(46,213,115,0.45)',
117
+ swapped: alpha(dark['status-success'], 0.45),
118
+ },
119
+ {
120
+ label: 'CapacityBandChart PROJECTION_FILL',
121
+ original: 'rgba(46,213,115,0.05)',
122
+ swapped: alpha(dark['status-success'], 0.05),
123
+ },
124
+ // DeviationBar.tsx (grey stop at 50% has no token match and stays raw — not listed here)
125
+ {
126
+ label: 'DeviationBar gradient success stop',
127
+ original: 'rgba(46,213,115,0.25)',
128
+ swapped: alpha(dark['status-success'], 0.25),
129
+ },
130
+ {
131
+ label: 'DeviationBar gradient warning stop',
132
+ original: 'rgba(249,180,21,0.25)',
133
+ swapped: alpha(dark['status-warning'], 0.25),
134
+ },
135
+ // ExerciseDetailPage.tsx
136
+ {
137
+ label: 'ExerciseDetailPage e1rm bg',
138
+ original: 'rgba(255,121,0,0.10)',
139
+ swapped: alpha(BRAND_PRIMARY, 0.1),
140
+ },
141
+ {
142
+ label: 'ExerciseDetailPage e1rm border',
143
+ original: 'rgba(255,121,0,0.25)',
144
+ swapped: alpha(BRAND_PRIMARY, 0.25),
145
+ },
146
+ // IntensityBar.tsx
147
+ {
148
+ label: 'IntensityBar TARGET_LINE_COLOR',
149
+ original: 'rgba(33, 150, 243, 0.5)',
150
+ swapped: alpha(dark['status-info'], 0.5),
151
+ },
152
+ // MesoStatusCard.tsx
153
+ {
154
+ label: 'MesoStatusCard gradient success',
155
+ original: 'rgba(46,213,115,0.25)',
156
+ swapped: alpha(dark['status-success'], 0.25),
157
+ },
158
+ {
159
+ label: 'MesoStatusCard gradient warning',
160
+ original: 'rgba(249,180,21,0.25)',
161
+ swapped: alpha(dark['status-warning'], 0.25),
162
+ },
163
+ {
164
+ label: 'MesoStatusCard gradient error',
165
+ original: 'rgba(209,67,67,0.25)',
166
+ swapped: alpha(dark['status-error'], 0.25),
167
+ },
168
+ {
169
+ label: 'MesoStatusCard success.bg',
170
+ original: 'rgba(46,213,115,0.15)',
171
+ swapped: alpha(dark['status-success'], 0.15),
172
+ },
173
+ {
174
+ label: 'MesoStatusCard success.border',
175
+ original: 'rgba(46,213,115,0.3)',
176
+ swapped: alpha(dark['status-success'], 0.3),
177
+ },
178
+ {
179
+ label: 'MesoStatusCard warning.bg',
180
+ original: 'rgba(249,180,21,0.15)',
181
+ swapped: alpha(dark['status-warning'], 0.15),
182
+ },
183
+ {
184
+ label: 'MesoStatusCard warning.border',
185
+ original: 'rgba(249,180,21,0.3)',
186
+ swapped: alpha(dark['status-warning'], 0.3),
187
+ },
188
+ {
189
+ label: 'MesoStatusCard error.bg',
190
+ original: 'rgba(209,67,67,0.15)',
191
+ swapped: alpha(dark['status-error'], 0.15),
192
+ },
193
+ {
194
+ label: 'MesoStatusCard error.border',
195
+ original: 'rgba(209,67,67,0.25)',
196
+ swapped: alpha(dark['status-error'], 0.25),
197
+ },
198
+ {
199
+ label: 'MesoStatusCard gauge center line',
200
+ original: 'rgba(255,255,255,0.3)',
201
+ swapped: alpha(primitiveColors.white, 0.3),
202
+ },
203
+ {
204
+ label: 'MesoStatusCard coaching bg',
205
+ original: 'rgba(249,180,21,0.06)',
206
+ swapped: alpha(dark['status-warning'], 0.06),
207
+ },
208
+ {
209
+ label: 'MesoStatusCard coaching border',
210
+ original: 'rgba(249,180,21,0.15)',
211
+ swapped: alpha(dark['status-warning'], 0.15),
212
+ },
213
+ {
214
+ label: 'MesoStatusCard next-target bg',
215
+ original: 'rgba(46,213,115,0.06)',
216
+ swapped: alpha(dark['status-success'], 0.06),
217
+ },
218
+ {
219
+ label: 'MesoStatusCard next-target border',
220
+ original: 'rgba(46,213,115,0.2)',
221
+ swapped: alpha(dark['status-success'], 0.2),
222
+ },
223
+ // ReadinessCheck.tsx
224
+ {
225
+ label: 'ReadinessCheck BRAND_PRIMARY_SUBTLE',
226
+ original: 'rgba(255,121,0,0.12)',
227
+ swapped: alpha(BRAND_PRIMARY, 0.12),
228
+ },
229
+ // RestTimer.tsx
230
+ {
231
+ label: 'RestTimer add-time bg',
232
+ original: 'rgba(255,255,255,0.06)',
233
+ swapped: alpha(primitiveColors.white, 0.06),
234
+ },
235
+ {
236
+ label: 'RestTimer skip bg',
237
+ original: 'rgba(255,121,0,0.12)',
238
+ swapped: alpha(BRAND_PRIMARY, 0.12),
239
+ },
240
+ // StrengthTrendChart.tsx
241
+ {
242
+ label: 'StrengthTrendChart GRID_LINE',
243
+ original: 'rgba(255,255,255,0.06)',
244
+ swapped: alpha(primitiveColors.white, 0.06),
245
+ },
246
+ {
247
+ label: 'StrengthTrendChart SUCCESS_PILL_BG',
248
+ original: 'rgba(46,213,115,0.10)',
249
+ swapped: alpha(dark['status-success'], 0.1),
250
+ },
251
+ {
252
+ label: 'StrengthTrendChart SUCCESS_PILL_BORDER',
253
+ original: 'rgba(46,213,115,0.20)',
254
+ swapped: alpha(dark['status-success'], 0.2),
255
+ },
256
+ {
257
+ label: 'StrengthTrendChart ERROR_PILL_BG',
258
+ original: 'rgba(209,67,67,0.10)',
259
+ swapped: alpha(dark['status-error'], 0.1),
260
+ },
261
+ {
262
+ label: 'StrengthTrendChart ERROR_PILL_BORDER',
263
+ original: 'rgba(209,67,67,0.20)',
264
+ swapped: alpha(dark['status-error'], 0.2),
265
+ },
266
+ {
267
+ label: 'StrengthTrendChart tooltip borderLeftColor',
268
+ original: 'rgba(255,255,255,0.10)',
269
+ swapped: alpha(primitiveColors.white, 0.1),
270
+ },
271
+ // ToolbarButton.tsx
272
+ {
273
+ label: 'ToolbarButton disabledBg',
274
+ original: 'rgba(255, 255, 255, 0.12)',
275
+ swapped: alpha(primitiveColors.white, 0.12),
276
+ },
277
+ ]
278
+
279
+ it.each(staticCases)(
280
+ '$label resolves to the same colour it replaced',
281
+ ({ original, swapped }) => {
282
+ expectSameColor(swapped, original)
283
+ }
284
+ )
285
+
286
+ it('covers all 43 static swaps (the 44th, MesoCard, is dynamic and tested below)', () => {
287
+ expect(staticCases).toHaveLength(43)
288
+ })
289
+
290
+ describe('MesoCard heatmapColor (dynamic opacity)', () => {
291
+ // Mirrors the original `rgba(255,121,0,${opacity.toFixed(3)})` formula this
292
+ // replaced, at representative percentages.
293
+ function originalHeatmapColor(percentage: number): string {
294
+ const clamped = Math.max(0, Math.min(100, percentage))
295
+ const opacity = 0.12 + (clamped / 100) * 0.78
296
+ return `rgba(255,121,0,${opacity.toFixed(3)})`
297
+ }
298
+ function swappedHeatmapColor(percentage: number): string {
299
+ const clamped = Math.max(0, Math.min(100, percentage))
300
+ const opacity = 0.12 + (clamped / 100) * 0.78
301
+ return alpha(BRAND_PRIMARY, opacity)
302
+ }
303
+
304
+ it.each([0, 1, 25, 50, 63, 80, 99, 100])('matches at %i%%', (pct) => {
305
+ expectSameColor(swappedHeatmapColor(pct), originalHeatmapColor(pct), 0.0006)
306
+ })
307
+ })
308
+ })
309
+
310
+ const SRC_ROOT = path.dirname(fileURLToPath(import.meta.url))
311
+
312
+ function readSource(relativeToSrc: string): string {
313
+ return readFileSync(path.join(SRC_ROOT, '..', relativeToSrc), 'utf8')
314
+ }
315
+
316
+ describe('touched source files no longer contain the swapped raw literals', () => {
317
+ const fileCases: Array<{ file: string; removedLiterals: string[] }> = [
318
+ { file: 'components/custom/Gauge/Gauge.tsx', removedLiterals: ['rgba(255,255,255,0.08)'] },
319
+ {
320
+ file: 'components/custom/Scatter/Scatter.tsx',
321
+ removedLiterals: [
322
+ 'rgba(255,255,255,0.07)',
323
+ 'rgba(255,255,255,0.18)',
324
+ 'rgba(255,255,255,0.28)',
325
+ ],
326
+ },
327
+ {
328
+ file: 'components/custom/Workout/BaseBadge.tsx',
329
+ removedLiterals: ['rgba(255, 121, 0, 0.12)', 'rgba(255, 121, 0, 0.3)'],
330
+ },
331
+ {
332
+ file: 'components/custom/Workout/BodyMap.tsx',
333
+ removedLiterals: ['rgba(255,255,255,0.08)', 'rgba(255,255,255,0.12)', 'rgba(255,121,0,0.16)'],
334
+ },
335
+ {
336
+ file: 'components/custom/Workout/BodyMapDetailPanel.tsx',
337
+ removedLiterals: ['rgba(255,121,0,0.12)', 'rgba(255,121,0,0.3)'],
338
+ },
339
+ {
340
+ file: 'components/custom/Workout/CapacityBandChart.tsx',
341
+ removedLiterals: ['rgba(46,213,115,0.1)', 'rgba(46,213,115,0.45)', 'rgba(46,213,115,0.05)'],
342
+ },
343
+ {
344
+ file: 'components/custom/Workout/DeviationBar.tsx',
345
+ removedLiterals: ['rgba(46,213,115,0.25)', 'rgba(249,180,21,0.25)'],
346
+ },
347
+ {
348
+ file: 'components/custom/Workout/ExerciseDetailPage.tsx',
349
+ removedLiterals: ['rgba(255,121,0,0.10)', 'rgba(255,121,0,0.25)'],
350
+ },
351
+ {
352
+ file: 'components/custom/Workout/IntensityBar.tsx',
353
+ removedLiterals: ['rgba(33, 150, 243, 0.5)'],
354
+ },
355
+ {
356
+ file: 'components/custom/Workout/MesoCard.tsx',
357
+ removedLiterals: ['rgba(255,121,0,${opacity.toFixed(3)})'],
358
+ },
359
+ {
360
+ file: 'components/custom/Workout/MesoStatusCard.tsx',
361
+ removedLiterals: [
362
+ 'rgba(46,213,115,0.25)',
363
+ 'rgba(249,180,21,0.25)',
364
+ 'rgba(209,67,67,0.25)',
365
+ 'rgba(46,213,115,0.15)',
366
+ 'rgba(46,213,115,0.3)',
367
+ 'rgba(249,180,21,0.15)',
368
+ 'rgba(249,180,21,0.3)',
369
+ 'rgba(209,67,67,0.15)',
370
+ 'rgba(255,255,255,0.3)',
371
+ 'rgba(249,180,21,0.06)',
372
+ 'rgba(46,213,115,0.06)',
373
+ 'rgba(46,213,115,0.2)',
374
+ ],
375
+ },
376
+ {
377
+ file: 'components/custom/Workout/ReadinessCheck.tsx',
378
+ removedLiterals: ['rgba(255,121,0,0.12)'],
379
+ },
380
+ {
381
+ file: 'components/custom/Workout/RestTimer.tsx',
382
+ removedLiterals: ['rgba(255,255,255,0.06)', 'rgba(255,121,0,0.12)'],
383
+ },
384
+ {
385
+ file: 'components/custom/Workout/StrengthTrendChart.tsx',
386
+ removedLiterals: [
387
+ 'rgba(255,255,255,0.06)',
388
+ 'rgba(46,213,115,0.10)',
389
+ 'rgba(46,213,115,0.20)',
390
+ 'rgba(209,67,67,0.10)',
391
+ 'rgba(209,67,67,0.20)',
392
+ 'rgba(255,255,255,0.10)',
393
+ ],
394
+ },
395
+ {
396
+ file: 'components/ui/toolbar-button/ToolbarButton.tsx',
397
+ removedLiterals: ['rgba(255, 255, 255, 0.12)'],
398
+ },
399
+ ]
400
+
401
+ it.each(fileCases)('$file no longer has its swapped literal(s)', ({ file, removedLiterals }) => {
402
+ const text = readSource(file)
403
+ expect(text).toContain('alpha(')
404
+ for (const literal of removedLiterals) {
405
+ expect(text).not.toContain(literal)
406
+ }
407
+ })
408
+ })
@@ -6,13 +6,23 @@ module.exports = {
6
6
  theme: {
7
7
  extend: {
8
8
  colors: {
9
- // Brand colors
9
+ // Every leaf here is a `var(--color-*)` reference, which is what makes
10
+ // light/dark switching work — and also why an opacity modifier
11
+ // (`bg-brand-primary/10`) emits NO rule at all under Tailwind v3 and
12
+ // renders as nothing. Reach for a wash rung instead; see VW-308 and
13
+ // `titan/no-var-color-opacity`.
14
+ //
15
+ // The wash ladder (`-subtle`/`-muted`/`-strong`) is the translucency
16
+ // vocabulary. All three rungs are published as classes so a component
17
+ // never has to hand-mix alpha.
10
18
  brand: {
11
19
  primary: {
12
20
  DEFAULT: 'var(--color-brand-primary)',
13
21
  light: 'var(--color-brand-primary-light)',
14
22
  dark: 'var(--color-brand-primary-dark)',
15
23
  subtle: 'var(--color-brand-primary-subtle)',
24
+ muted: 'var(--color-brand-primary-muted)',
25
+ strong: 'var(--color-brand-primary-strong)',
16
26
  hover: 'var(--color-brand-primary-hover)',
17
27
  active: 'var(--color-brand-primary-active)',
18
28
  },
@@ -21,14 +31,23 @@ module.exports = {
21
31
  light: 'var(--color-brand-secondary-light)',
22
32
  dark: 'var(--color-brand-secondary-dark)',
23
33
  subtle: 'var(--color-brand-secondary-subtle)',
34
+ muted: 'var(--color-brand-secondary-muted)',
35
+ strong: 'var(--color-brand-secondary-strong)',
24
36
  hover: 'var(--color-brand-secondary-hover)',
25
37
  active: 'var(--color-brand-secondary-active)',
26
38
  },
27
39
  },
28
- // Text on brand backgrounds
40
+ // Text on brand backgrounds. `-subtle` is text ON the `-subtle` fill, a
41
+ // separate role from the white label a solid fill carries.
29
42
  'on-brand': {
30
- primary: 'var(--color-on-brand-primary)',
31
- secondary: 'var(--color-on-brand-secondary)',
43
+ primary: {
44
+ DEFAULT: 'var(--color-on-brand-primary)',
45
+ subtle: 'var(--color-on-brand-primary-subtle)',
46
+ },
47
+ secondary: {
48
+ DEFAULT: 'var(--color-on-brand-secondary)',
49
+ subtle: 'var(--color-on-brand-secondary-subtle)',
50
+ },
32
51
  },
33
52
  // Status colors
34
53
  status: {
@@ -37,6 +56,8 @@ module.exports = {
37
56
  light: 'var(--color-status-success-light)',
38
57
  dark: 'var(--color-status-success-dark)',
39
58
  subtle: 'var(--color-status-success-subtle)',
59
+ muted: 'var(--color-status-success-muted)',
60
+ strong: 'var(--color-status-success-strong)',
40
61
  },
41
62
  // Live-session accent — own role, decoupled from success
42
63
  live: {
@@ -48,32 +69,52 @@ module.exports = {
48
69
  light: 'var(--color-status-error-light)',
49
70
  dark: 'var(--color-status-error-dark)',
50
71
  subtle: 'var(--color-status-error-subtle)',
72
+ muted: 'var(--color-status-error-muted)',
73
+ strong: 'var(--color-status-error-strong)',
51
74
  },
52
75
  'error-vivid': {
53
76
  DEFAULT: 'var(--color-status-error-vivid)',
54
77
  light: 'var(--color-status-error-vivid-light)',
55
78
  dark: 'var(--color-status-error-vivid-dark)',
56
79
  subtle: 'var(--color-status-error-vivid-subtle)',
80
+ muted: 'var(--color-status-error-vivid-muted)',
81
+ strong: 'var(--color-status-error-vivid-strong)',
57
82
  },
58
83
  warning: {
59
84
  DEFAULT: 'var(--color-status-warning)',
60
85
  light: 'var(--color-status-warning-light)',
61
86
  dark: 'var(--color-status-warning-dark)',
62
87
  subtle: 'var(--color-status-warning-subtle)',
88
+ muted: 'var(--color-status-warning-muted)',
89
+ strong: 'var(--color-status-warning-strong)',
63
90
  },
64
91
  info: {
65
92
  DEFAULT: 'var(--color-status-info)',
66
93
  light: 'var(--color-status-info-light)',
67
94
  dark: 'var(--color-status-info-dark)',
68
95
  subtle: 'var(--color-status-info-subtle)',
96
+ muted: 'var(--color-status-info-muted)',
97
+ strong: 'var(--color-status-info-strong)',
69
98
  },
70
99
  },
71
- // Text on status backgrounds
100
+ // Text on status backgrounds. `-subtle` is text ON the `-subtle` fill.
72
101
  'on-status': {
73
- success: 'var(--color-on-status-success)',
74
- error: 'var(--color-on-status-error)',
75
- warning: 'var(--color-on-status-warning)',
76
- info: 'var(--color-on-status-info)',
102
+ success: {
103
+ DEFAULT: 'var(--color-on-status-success)',
104
+ subtle: 'var(--color-on-status-success-subtle)',
105
+ },
106
+ error: {
107
+ DEFAULT: 'var(--color-on-status-error)',
108
+ subtle: 'var(--color-on-status-error-subtle)',
109
+ },
110
+ warning: {
111
+ DEFAULT: 'var(--color-on-status-warning)',
112
+ subtle: 'var(--color-on-status-warning-subtle)',
113
+ },
114
+ info: {
115
+ DEFAULT: 'var(--color-on-status-info)',
116
+ subtle: 'var(--color-on-status-info-subtle)',
117
+ },
77
118
  },
78
119
  // Result/outcome colors
79
120
  result: {