@titan-design/react-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/bodymap.js +232 -185
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +232 -185
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +164 -8
  6. package/dist/index.d.ts +164 -8
  7. package/dist/index.js +835 -342
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +816 -343
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/pages.js +570 -522
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +570 -522
  14. package/dist/pages.mjs.map +1 -1
  15. package/dist/theme/index.d.mts +461 -199
  16. package/dist/theme/index.d.ts +461 -199
  17. package/dist/theme/index.js +311 -97
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +303 -98
  20. package/dist/theme/index.mjs.map +1 -1
  21. package/dist/theme/tokens-css.js +148 -139
  22. package/dist/theme/tokens-css.js.map +1 -1
  23. package/dist/theme/tokens-css.mjs +148 -139
  24. package/dist/theme/tokens-css.mjs.map +1 -1
  25. package/package.json +3 -1
  26. package/src/components/custom/DateTime/DateTime.stories.tsx +82 -6
  27. package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
  28. package/src/components/custom/DateTime/DateTime.tsx +55 -15
  29. package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
  30. package/src/components/custom/Gauge/Gauge.tsx +6 -3
  31. package/src/components/custom/Scatter/Scatter.tsx +3 -5
  32. package/src/components/custom/Treemap/Treemap.tsx +3 -5
  33. package/src/components/custom/Typography/Typography.stories.tsx +45 -27
  34. package/src/components/custom/Typography/Typography.test.tsx +13 -3
  35. package/src/components/custom/Typography/Typography.tsx +5 -0
  36. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +2 -1
  37. package/src/components/custom/Workout/BodyMap.tsx +6 -3
  38. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
  39. package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
  40. package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
  41. package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
  42. package/src/components/custom/Workout/DeviationBar.tsx +7 -4
  43. package/src/components/custom/Workout/ExerciseDetailPage.tsx +2 -1
  44. package/src/components/custom/Workout/InputBar.tsx +2 -1
  45. package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
  46. package/src/components/custom/Workout/IntensityBar.tsx +10 -6
  47. package/src/components/custom/Workout/MesoCard.tsx +8 -3
  48. package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
  49. package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
  50. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
  51. package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
  52. package/src/components/custom/Workout/PrBadge.tsx +5 -2
  53. package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
  54. package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
  55. package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
  56. package/src/components/custom/Workout/RestTimer.tsx +2 -1
  57. package/src/components/custom/Workout/SetRow.tsx +2 -1
  58. package/src/components/custom/Workout/Sparkline.tsx +2 -1
  59. package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
  60. package/src/components/custom/Workout/StatusDot.tsx +12 -9
  61. package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
  62. package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
  63. package/src/components/custom/Workout/TempoDisplay.tsx +6 -3
  64. package/src/components/custom/Workout/VelocityStrip.test.tsx +3 -3
  65. package/src/components/custom/Workout/WeekRow.tsx +3 -2
  66. package/src/components/custom/Workout/WeightBadge.tsx +6 -2
  67. package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
  68. package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
  69. package/src/components/custom/Workout/icons.tsx +6 -67
  70. package/src/components/icons/SvgIcon.tsx +50 -0
  71. package/src/components/icons/icons.stories.tsx +57 -0
  72. package/src/components/icons/icons.test.tsx +43 -0
  73. package/src/components/icons/icons.tsx +45 -0
  74. package/src/components/icons/index.ts +4 -0
  75. package/src/components/index.ts +6 -0
  76. package/src/components/shell/BrandLockup.stories.tsx +27 -0
  77. package/src/components/shell/BrandLockup.test.tsx +23 -0
  78. package/src/components/shell/BrandLockup.tsx +47 -0
  79. package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
  80. package/src/components/shell/DeviceIndicator.test.tsx +26 -0
  81. package/src/components/shell/DeviceIndicator.tsx +52 -0
  82. package/src/components/shell/DeviceMenu.stories.tsx +47 -0
  83. package/src/components/shell/DeviceMenu.test.tsx +40 -0
  84. package/src/components/shell/DeviceMenu.tsx +65 -0
  85. package/src/components/shell/DeviceRow.stories.tsx +38 -0
  86. package/src/components/shell/DeviceRow.test.tsx +37 -0
  87. package/src/components/shell/DeviceRow.tsx +58 -0
  88. package/src/components/shell/README.md +86 -0
  89. package/src/components/shell/SessionStatePill.stories.tsx +27 -0
  90. package/src/components/shell/SessionStatePill.test.tsx +25 -0
  91. package/src/components/shell/SessionStatePill.tsx +46 -0
  92. package/src/components/shell/TopBar.stories.tsx +49 -0
  93. package/src/components/shell/TopBar.test.tsx +36 -0
  94. package/src/components/shell/TopBar.tsx +89 -0
  95. package/src/components/shell/index.ts +8 -0
  96. package/src/components/ui/indicator/Indicator.stories.tsx +37 -1
  97. package/src/components/ui/indicator/Indicator.test.tsx +19 -1
  98. package/src/components/ui/indicator/Indicator.tsx +45 -6
  99. package/src/components/ui/indicator/index.ts +1 -1
  100. package/src/components/ui/spinner/Spinner.tsx +3 -2
  101. package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
  102. package/src/theme/ColorSystem.stories.tsx +323 -0
  103. package/src/theme/Colors.stories.tsx +69 -1
  104. package/src/theme/barrel.ts +1 -0
  105. package/src/theme/config.ts +16 -3
  106. package/src/theme/extracted-colors-dataviz.ts +21 -0
  107. package/src/theme/extracted-colors-ui.ts +20 -0
  108. package/src/theme/global.css +64 -52
  109. package/src/theme/gradients.test.ts +25 -0
  110. package/src/theme/gradients.ts +36 -0
  111. package/src/theme/tokens/primitives.test.ts +232 -0
  112. package/src/theme/tokens/primitives.ts +247 -0
  113. package/src/theme/tokens/semantic.ts +93 -81
  114. package/src/theme/workout-tokens.ts +22 -21
  115. package/src/utils/avatar-color.ts +3 -3
  116. package/tailwind.config.js +10 -3
@@ -62,6 +62,34 @@ export const primitiveColors = {
62
62
  900: '#7F1D1D',
63
63
  },
64
64
 
65
+ // Vivid green scale (live/go success)
66
+ greenVivid: {
67
+ 50: '#E9FBF0',
68
+ 100: '#C8F7DA',
69
+ 200: '#9FF0BF',
70
+ 300: '#6BE79D',
71
+ 400: '#47DE84', // Success vivid light
72
+ 500: '#2ED573', // Success vivid main
73
+ 600: '#22B85F',
74
+ 700: '#1A9950', // Success vivid dark
75
+ 800: '#157A40',
76
+ 900: '#105C30',
77
+ },
78
+
79
+ // Vivid red scale (alert)
80
+ redVivid: {
81
+ 50: '#FFECEE',
82
+ 100: '#FFD6DB',
83
+ 200: '#FFB3BC',
84
+ 300: '#FF8593',
85
+ 400: '#FF6070', // Error vivid light
86
+ 500: '#FF4757', // Error vivid main
87
+ 600: '#E63548',
88
+ 700: '#C42539', // Error vivid dark
89
+ 800: '#9E1C2C',
90
+ 900: '#7A1520',
91
+ },
92
+
65
93
  // Amber scale (warning)
66
94
  amber: {
67
95
  50: '#FFFBEB',
@@ -152,6 +180,225 @@ export const primitiveColors = {
152
180
  transparent: 'transparent',
153
181
  } as const
154
182
 
183
+ /**
184
+ * Derived tonal ramps (TD-05.09 color foundations)
185
+ *
186
+ * Seven OKLCH-generated hue ramps, 11 perceptual lightness steps each (50 -> 950).
187
+ * Built with hue-torsion + a chroma arc, anchored through existing brand/semantic
188
+ * hexes. `pin` marks the step each ramp flows through its anchor.
189
+ * This is the single source of truth for chromatic hexes — the categorical palette
190
+ * below references these steps rather than duplicating values.
191
+ * See coordination/design-explorations/foundations.
192
+ */
193
+ export const primitiveRamps = {
194
+ red: {
195
+ 50: '#FFF4F4',
196
+ 100: '#FFE3E5',
197
+ 200: '#FFC2C5',
198
+ 300: '#FF9A9D',
199
+ 400: '#F77175',
200
+ 500: '#E05254',
201
+ 600: '#D14343', // pin
202
+ 700: '#A4221C',
203
+ 800: '#7E1002',
204
+ 900: '#5A0900',
205
+ 950: '#3D0400',
206
+ },
207
+ orange: {
208
+ 50: '#FFF5ED',
209
+ 100: '#FFE6D4',
210
+ 200: '#FFC7A2',
211
+ 300: '#FFA063',
212
+ 400: '#FF7900', // pin
213
+ 500: '#DA5F00',
214
+ 600: '#B94A00',
215
+ 700: '#983804',
216
+ 800: '#6F290F',
217
+ 900: '#4E1C0C',
218
+ 950: '#331107',
219
+ },
220
+ amber: {
221
+ 50: '#FFF7DD',
222
+ 100: '#FFEAA9',
223
+ 200: '#FFD352',
224
+ 300: '#F9B415', // pin
225
+ 400: '#E08C00',
226
+ 500: '#C27400',
227
+ 600: '#A45E00',
228
+ 700: '#814D14',
229
+ 800: '#5B3A1A',
230
+ 900: '#442503',
231
+ 950: '#301500',
232
+ },
233
+ green: {
234
+ 50: '#E3FFEE',
235
+ 100: '#B5FFD2',
236
+ 200: '#58F69E',
237
+ 300: '#2ED573', // pin
238
+ 400: '#21C05D',
239
+ 500: '#22A444',
240
+ 600: '#298732',
241
+ 700: '#2B6B25',
242
+ 800: '#264D1C',
243
+ 900: '#1B3515',
244
+ 950: '#11220D',
245
+ },
246
+ cyan: {
247
+ 50: '#E6FBFF',
248
+ 100: '#C2F6FF',
249
+ 200: '#62EAFF',
250
+ 300: '#22D3EE', // pin
251
+ 400: '#01B5D1',
252
+ 500: '#2697B7',
253
+ 600: '#307B9B',
254
+ 700: '#2A617F',
255
+ 800: '#22465F',
256
+ 900: '#0B3149',
257
+ 950: '#001F36',
258
+ },
259
+ blue: {
260
+ 50: '#EFF8FF',
261
+ 100: '#D9EFFF',
262
+ 200: '#ACDBFF',
263
+ 300: '#78C2FF',
264
+ 400: '#3CA8FF',
265
+ 500: '#2196F3', // pin
266
+ 600: '#1072CB',
267
+ 700: '#135AA8',
268
+ 800: '#14407E',
269
+ 900: '#112C5A',
270
+ 950: '#091B3C',
271
+ },
272
+ magenta: {
273
+ 50: '#FFF3F9',
274
+ 100: '#FFE2F1',
275
+ 200: '#FFBDE2',
276
+ 300: '#FF8FD5',
277
+ 400: '#ED69C3',
278
+ 500: '#D548AF',
279
+ 600: '#BA2996',
280
+ 700: '#9C0D7A', // pin
281
+ 800: '#77005A',
282
+ 900: '#56003F',
283
+ 950: '#3B0029',
284
+ },
285
+ } as const
286
+
287
+ /**
288
+ * Categorical (qualitative) palette — an ordered, CVD-safe series expressed as
289
+ * references into {@link primitiveRamps} (one source of truth; the palette moves
290
+ * with the ramps).
291
+ *
292
+ * Design: a single canonical hue order (blue, magenta, red, orange, green, cyan,
293
+ * amber), with steps chosen vivid-midtone-first and fanned lighter/darker as the
294
+ * series grows to preserve separation. The sequence is nested-stable — a chart
295
+ * with N series uses the first N colors, and the series index is stable as N
296
+ * changes. Colorblind-safe worst-case deuteranopia/protanopia floor 8 holds
297
+ * through {@link CATEGORICAL_CVD_SAFE_MAX} colors; the 7th is an extended color
298
+ * that should be paired with a legend or other secondary encoding.
299
+ *
300
+ * Two variants: `default` (vivid, sits on neutral/light surfaces, legible under
301
+ * black text) and `dark` (deeper shades legible under white text on a fill). A
302
+ * separate "light" variant was dropped — the vivid `default` picks already clear
303
+ * black-text contrast, so it doubles as the light-context palette.
304
+ */
305
+ export const categoricalPalette = {
306
+ default: [
307
+ primitiveRamps.blue[500],
308
+ primitiveRamps.magenta[500],
309
+ primitiveRamps.red[500],
310
+ primitiveRamps.orange[400],
311
+ primitiveRamps.green[300],
312
+ primitiveRamps.cyan[300],
313
+ primitiveRamps.amber[600], // extended (7th) — pair with a legend
314
+ ],
315
+ dark: [
316
+ primitiveRamps.blue[600],
317
+ primitiveRamps.magenta[600],
318
+ primitiveRamps.red[500],
319
+ primitiveRamps.orange[600],
320
+ primitiveRamps.green[800],
321
+ primitiveRamps.cyan[800],
322
+ primitiveRamps.amber[500], // extended (7th) — pair with a legend
323
+ ],
324
+ } as const
325
+
326
+ /** Largest series count that holds the CVD floor without a legend. */
327
+ export const CATEGORICAL_CVD_SAFE_MAX = 6
328
+
329
+ /**
330
+ * Categorical palette variant.
331
+ * - `default` : vivid; neutral/light surfaces, legible under black text
332
+ * - `dark` : deeper shades, legible under white text on a filled swatch
333
+ */
334
+ export type CategoricalVariant = keyof typeof categoricalPalette
335
+
336
+ /**
337
+ * Get a color from the ordered categorical palette by series index.
338
+ * The palette is nested-stable, so the color for a given index does not change
339
+ * with the total series count; `index` simply wraps past the end.
340
+ *
341
+ * @param index - 0-based series index (wraps past the end)
342
+ * @param variant - `default` (black-text / light surfaces) or `dark` (white text)
343
+ * @returns The hex color string
344
+ */
345
+ export function getCategoricalColor(index: number, variant: CategoricalVariant = 'default'): string {
346
+ const palette = categoricalPalette[variant]
347
+ index = Math.max(0, Math.floor(index))
348
+ return palette[index % palette.length]
349
+ }
350
+
351
+ /**
352
+ * Diverging status scale (BodyMap training-status: under → optimal → over) as
353
+ * references into {@link primitiveRamps}. A true diverging shape — `optimal` is
354
+ * the lightest center, the extremes go darker — so the signal reads in lightness
355
+ * (colorblind-robust; worst-case deut/protanopia min ΔE ~10.9) and the direction
356
+ * reads in the blue↔red axis (the CVD-safe hue pair).
357
+ *
358
+ * Tuned so every stop is dark enough for the fill yet light enough that BLACK
359
+ * text clears 4.5:1 on all five — no per-cell white/black flipping. The value
360
+ * valley is symmetric: the two mid-arms (maintenance / approaching) sit at equal
361
+ * lightness (both ~0.53 relative luminance), the ends darker.
362
+ */
363
+ export const divergingScale = [
364
+ primitiveRamps.blue[500], // under
365
+ primitiveRamps.cyan[300], // maintenance
366
+ primitiveRamps.green[200], // optimal (light center)
367
+ primitiveRamps.amber[300], // approaching
368
+ primitiveRamps.red[600], // over
369
+ ] as const
370
+
371
+ /**
372
+ * Sequential "effort" scale (low → high intensity: green → yellow → orange → red)
373
+ * as references into {@link primitiveRamps}. A 6-stop ordinal palette; the
374
+ * velocity/RPE strip sub-samples it. Every adjacent pair clears the CVD floor
375
+ * (min deut/protan ΔE ≥ 4.5), and within that the hue walk is kept smooth —
376
+ * the amber-200 → amber-300 → orange-400 run steps the yellow→orange transition
377
+ * gradually rather than lurching. Order encodes magnitude, so it tolerates the
378
+ * green↔red CVD pair. Stops 0–3 take black text; the dark reds take white.
379
+ */
380
+ export const sequentialEffort = [
381
+ primitiveRamps.green[300],
382
+ primitiveRamps.amber[200],
383
+ primitiveRamps.amber[300],
384
+ primitiveRamps.orange[400],
385
+ primitiveRamps.red[600],
386
+ primitiveRamps.red[700],
387
+ ] as const
388
+
389
+ /**
390
+ * Best-contrast text color (black or white) for a solid fill — for labels sitting
391
+ * on categorical/diverging/effort swatches. Uses the WCAG relative-luminance ratio.
392
+ */
393
+ export function bestTextColor(hex: string): '#000000' | '#FFFFFF' {
394
+ const [r, g, b] = [0, 2, 4].map((i) => {
395
+ const c = parseInt(hex.replace('#', '').slice(i, i + 2), 16) / 255
396
+ return c >= 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92
397
+ })
398
+ const l = 0.2126 * r + 0.7152 * g + 0.0722 * b
399
+ return (l + 0.05) / 0.05 >= 1.05 / (l + 0.05) ? '#000000' : '#FFFFFF'
400
+ }
401
+
155
402
  /**
156
403
  * Discrete rainbow palette for data visualization
157
404
  * Based on Paul Tol's color schemes: https://personal.sron.nl/~pault/#fig:scheme_rainbow_discrete
@@ -17,49 +17,54 @@
17
17
  * - interactive-* : Hover/focus/active/disabled states
18
18
  */
19
19
 
20
- import { primitiveColors as p, discreteRainbow } from './primitives'
20
+ import { primitiveColors as p, primitiveRamps as ramp, discreteRainbow } from './primitives'
21
21
 
22
22
  // Light mode semantic colors (default)
23
23
  export const semanticColorsLight = {
24
24
  // Brand colors (brand-*)
25
- 'brand-primary': p.accent[400], // #FF7900
26
- 'brand-primary-light': p.accent[200], // #FF9630
27
- 'brand-primary-dark': p.accent[700], // #D0620C
25
+ 'brand-primary': ramp.orange[400],
26
+ 'brand-primary-light': ramp.orange[300],
27
+ 'brand-primary-dark': ramp.orange[500],
28
28
  'brand-primary-subtle': 'rgba(255, 121, 0, 0.08)',
29
- 'brand-primary-hover': p.accent[500],
30
- 'brand-primary-active': p.accent[600],
29
+ 'brand-primary-hover': ramp.orange[500],
30
+ 'brand-primary-active': ramp.orange[600],
31
31
 
32
- 'brand-secondary': p.steel[500], // #406D87
33
- 'brand-secondary-light': p.steel[300], // #678498
34
- 'brand-secondary-dark': p.steel[700], // #32556D
35
- 'brand-secondary-subtle': 'rgba(64, 109, 135, 0.08)',
36
- 'brand-secondary-hover': p.steel[600],
37
- 'brand-secondary-active': p.steel[700],
32
+ 'brand-secondary': ramp.cyan[600],
33
+ 'brand-secondary-light': ramp.cyan[500],
34
+ 'brand-secondary-dark': ramp.cyan[700],
35
+ 'brand-secondary-subtle': 'rgba(48, 123, 155, 0.08)',
36
+ 'brand-secondary-hover': ramp.cyan[700],
37
+ 'brand-secondary-active': ramp.cyan[800],
38
38
 
39
39
  // Text on brand backgrounds (on-*)
40
40
  'on-brand-primary': p.white,
41
41
  'on-brand-secondary': p.white,
42
42
 
43
43
  // Status colors (status-*)
44
- 'status-success': p.teal[500], // #14B8A6
45
- 'status-success-light': p.teal[400], // #43C6B7
46
- 'status-success-dark': p.teal[700], // #0E8074
47
- 'status-success-subtle': p.teal[50],
48
-
49
- 'status-error': p.red[600], // #D14343
50
- 'status-error-light': p.red[400], // #DA6868
51
- 'status-error-dark': p.red[700], // #922E2E
52
- 'status-error-subtle': p.red[50],
53
-
54
- 'status-warning': p.amber[500], // #FFB020
55
- 'status-warning-light': p.amber[400], // #FFBF4C
56
- 'status-warning-dark': p.amber[600], // #B27B16
57
- 'status-warning-subtle': p.amber[50],
58
-
59
- 'status-info': p.sky[500], // #2196F3
60
- 'status-info-light': p.sky[400], // #64B6F7
61
- 'status-info-dark': p.sky[700], // #0B79D0
62
- 'status-info-subtle': p.sky[50],
44
+ 'status-success': ramp.green[300],
45
+ 'status-success-light': ramp.green[200],
46
+ 'status-success-dark': ramp.green[600],
47
+ 'status-success-subtle': ramp.green[50],
48
+
49
+ 'status-error': ramp.red[600],
50
+ 'status-error-light': ramp.red[500],
51
+ 'status-error-dark': ramp.red[700],
52
+ 'status-error-subtle': ramp.red[50],
53
+
54
+ 'status-error-vivid': p.redVivid[500], // vivid alert red
55
+ 'status-error-vivid-light': p.redVivid[400],
56
+ 'status-error-vivid-dark': p.redVivid[700],
57
+ 'status-error-vivid-subtle': 'rgba(255, 71, 87, 0.12)',
58
+
59
+ 'status-warning': ramp.amber[300],
60
+ 'status-warning-light': ramp.amber[200],
61
+ 'status-warning-dark': ramp.amber[500],
62
+ 'status-warning-subtle': ramp.amber[50],
63
+
64
+ 'status-info': ramp.blue[500],
65
+ 'status-info-light': ramp.blue[300],
66
+ 'status-info-dark': ramp.blue[600],
67
+ 'status-info-subtle': ramp.blue[50],
63
68
 
64
69
  // Text on status backgrounds (on-status-*)
65
70
  'on-status-success': p.white,
@@ -85,16 +90,16 @@ export const semanticColorsLight = {
85
90
 
86
91
  // Data visualization colors (data-*)
87
92
  // First 10 colors from discrete rainbow optimized for charts
88
- 'data-1': discreteRainbow[9], // #1965B0 - Blue
89
- 'data-2': discreteRainbow[14], // #4EB265 - Green
90
- 'data-3': discreteRainbow[17], // #F7F056 - Yellow
91
- 'data-4': discreteRainbow[25], // #DC050C - Red
92
- 'data-5': discreteRainbow[8], // #882E72 - Purple
93
- 'data-6': discreteRainbow[20], // #F4A736 - Orange
94
- 'data-7': discreteRainbow[13], // #7BAFDE - Light Blue
95
- 'data-8': discreteRainbow[15], // #90C987 - Light Green
96
- 'data-9': discreteRainbow[3], // #CAACCB - Lavender
97
- 'data-10': discreteRainbow[22], // #EE8026 - Dark Orange
93
+ 'data-1': discreteRainbow[9], // Blue
94
+ 'data-2': discreteRainbow[14], // Green
95
+ 'data-3': discreteRainbow[17], // Yellow
96
+ 'data-4': discreteRainbow[25], // Red
97
+ 'data-5': discreteRainbow[8], // Purple
98
+ 'data-6': discreteRainbow[20], // Orange
99
+ 'data-7': discreteRainbow[13], // Light Blue
100
+ 'data-8': discreteRainbow[15], // Light Green
101
+ 'data-9': discreteRainbow[3], // Lavender
102
+ 'data-10': discreteRainbow[22], // Dark Orange
98
103
 
99
104
  // Text colors (text-*)
100
105
  'text-primary': '#121828',
@@ -107,8 +112,8 @@ export const semanticColorsLight = {
107
112
 
108
113
  // Surface colors (surface-*) - for elevated containers
109
114
  'surface-base': p.white,
110
- 'surface-elevated': p.neutral[50], // #FAFAFA - slightly off-white for elevated cards
111
- 'surface-raised': p.neutral[100], // #F3F4F6 - light gray for raised cards
115
+ 'surface-elevated': p.neutral[50], // slightly off-white for elevated cards
116
+ 'surface-raised': p.neutral[100], // light gray for raised cards
112
117
  'surface-overlay': p.white,
113
118
  'surface-input': p.neutral[50], // Input field background (filled variant)
114
119
 
@@ -121,6 +126,7 @@ export const semanticColorsLight = {
121
126
  'border-default': '#E8E9EB',
122
127
  'border-subtle': p.neutral[100],
123
128
  'border-strong': p.neutral[300],
129
+ 'border-prominent': p.neutral[400], // high-visibility divider
124
130
  'border-focus': p.blue[600],
125
131
  'border-input': p.neutral[300], // Input field border
126
132
  'border-input-hover': p.neutral[400], // Input field border on hover
@@ -146,43 +152,48 @@ export const semanticColorsLight = {
146
152
  // Dark mode semantic colors
147
153
  export const semanticColorsDark = {
148
154
  // Brand colors stay the same in dark mode
149
- 'brand-primary': p.accent[400],
150
- 'brand-primary-light': p.accent[200],
151
- 'brand-primary-dark': p.accent[700],
155
+ 'brand-primary': ramp.orange[400],
156
+ 'brand-primary-light': ramp.orange[300],
157
+ 'brand-primary-dark': ramp.orange[500],
152
158
  'brand-primary-subtle': 'rgba(255, 121, 0, 0.12)',
153
- 'brand-primary-hover': p.accent[300],
154
- 'brand-primary-active': p.accent[200],
159
+ 'brand-primary-hover': ramp.orange[300],
160
+ 'brand-primary-active': ramp.orange[200],
155
161
 
156
- 'brand-secondary': p.steel[500],
157
- 'brand-secondary-light': p.steel[300],
158
- 'brand-secondary-dark': p.steel[700],
159
- 'brand-secondary-subtle': 'rgba(64, 109, 135, 0.12)',
160
- 'brand-secondary-hover': p.steel[400],
161
- 'brand-secondary-active': p.steel[300],
162
+ 'brand-secondary': ramp.cyan[600],
163
+ 'brand-secondary-light': ramp.cyan[500],
164
+ 'brand-secondary-dark': ramp.cyan[700],
165
+ 'brand-secondary-subtle': 'rgba(48, 123, 155, 0.12)',
166
+ 'brand-secondary-hover': ramp.cyan[500],
167
+ 'brand-secondary-active': ramp.cyan[400],
162
168
 
163
169
  // Text on brand backgrounds
164
170
  'on-brand-primary': p.white,
165
171
  'on-brand-secondary': p.white,
166
172
 
167
173
  // Status colors
168
- 'status-success': p.teal[500],
169
- 'status-success-light': p.teal[400],
170
- 'status-success-dark': p.teal[700],
171
- 'status-success-subtle': 'rgba(20, 184, 166, 0.12)',
172
-
173
- 'status-error': p.red[600],
174
- 'status-error-light': p.red[400],
175
- 'status-error-dark': p.red[700],
174
+ 'status-success': ramp.green[300],
175
+ 'status-success-light': ramp.green[200],
176
+ 'status-success-dark': ramp.green[600],
177
+ 'status-success-subtle': 'rgba(46, 213, 115, 0.12)',
178
+
179
+ 'status-error': ramp.red[600],
180
+ 'status-error-light': ramp.red[500],
181
+ 'status-error-dark': ramp.red[700],
176
182
  'status-error-subtle': 'rgba(209, 67, 67, 0.12)',
177
183
 
178
- 'status-warning': p.amber[500],
179
- 'status-warning-light': p.amber[400],
180
- 'status-warning-dark': p.amber[600],
181
- 'status-warning-subtle': 'rgba(255, 176, 32, 0.12)',
184
+ 'status-error-vivid': p.redVivid[500],
185
+ 'status-error-vivid-light': p.redVivid[400],
186
+ 'status-error-vivid-dark': p.redVivid[700],
187
+ 'status-error-vivid-subtle': 'rgba(255, 71, 87, 0.12)',
182
188
 
183
- 'status-info': p.sky[500],
184
- 'status-info-light': p.sky[400],
185
- 'status-info-dark': p.sky[700],
189
+ 'status-warning': ramp.amber[300],
190
+ 'status-warning-light': ramp.amber[200],
191
+ 'status-warning-dark': ramp.amber[500],
192
+ 'status-warning-subtle': 'rgba(249, 180, 21, 0.12)',
193
+
194
+ 'status-info': ramp.blue[500],
195
+ 'status-info-light': ramp.blue[300],
196
+ 'status-info-dark': ramp.blue[600],
186
197
  'status-info-subtle': 'rgba(33, 150, 243, 0.12)',
187
198
 
188
199
  // Text on status backgrounds
@@ -229,21 +240,22 @@ export const semanticColorsDark = {
229
240
  'text-link-hover': p.blue[400],
230
241
 
231
242
  // Surface colors - dark backgrounds
232
- 'surface-base': p.charcoal[700], // #161616 - main surface
233
- 'surface-elevated': p.charcoal[600], // #191919 - elevated surface
234
- 'surface-raised': p.charcoal[500], // #1C1C1C - raised surface
235
- 'surface-overlay': p.charcoal[600], // #191919 - overlay surface
236
- 'surface-input': p.charcoal[600], // #191919 - input surface
243
+ 'surface-base': p.charcoal[700], // main surface
244
+ 'surface-elevated': p.charcoal[600], // elevated surface
245
+ 'surface-raised': p.charcoal[500], // raised surface
246
+ 'surface-overlay': p.charcoal[600], // overlay surface
247
+ 'surface-input': p.charcoal[600], // input surface
237
248
 
238
249
  // Background colors
239
- 'background-base': p.charcoal[900], // #101010 - darkest charcoal
240
- 'background-default': p.charcoal[700], // #161616 - main background
241
- 'background-subtle': p.charcoal[500], // #1C1C1C - subtle background
250
+ 'background-base': p.charcoal[900], // darkest charcoal
251
+ 'background-default': p.charcoal[700], // main background
252
+ 'background-subtle': p.charcoal[500], // subtle background
242
253
 
243
254
  // Border colors
244
- 'border-default': p.charcoal[400], // #1F1F1F - default border
245
- 'border-subtle': p.charcoal[500], // #1C1C1C - subtle border
246
- 'border-strong': p.charcoal[300], // #2C2C2C - strong border
255
+ 'border-default': p.charcoal[400], // default border
256
+ 'border-subtle': p.charcoal[500], // subtle border
257
+ 'border-strong': p.charcoal[300], // strong border
258
+ 'border-prominent': p.charcoal[200], // high-visibility divider
247
259
  'border-focus': '#828DF8',
248
260
  'border-input': p.neutral[600],
249
261
  'border-input-hover': p.neutral[500],
@@ -259,7 +271,7 @@ export const semanticColorsDark = {
259
271
  'interactive-disabled-text': 'rgba(255, 255, 255, 0.26)',
260
272
 
261
273
  // Divider
262
- 'divider': p.charcoal[400], // #1F1F1F - divider color
274
+ 'divider': p.charcoal[400], // divider color
263
275
 
264
276
  // Avatar default
265
277
  'avatar-background': p.neutral[600],
@@ -2,31 +2,32 @@
2
2
  * Workout-specific tokens not yet in the main Tailwind config.
3
3
  * Use these inline instead of Tailwind classes.
4
4
  */
5
+ import { divergingScale, sequentialEffort, primitiveRamps as ramp } from './tokens/primitives'
6
+
5
7
  export const WORKOUT_TOKENS = {
6
8
  // Canonical 4-band performance scale — the single source for BOTH the
7
9
  // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
8
10
  // is intentionally inverted between the two consumers: for velocity, green =
9
- // fastest/best; for RPE, green = easiest. Each component maps its own
10
- // thresholds onto these four colors. Kept as vivid data-viz values (distinct
11
- // from the muted status tokens) and theme-independent, so — like the heatmap
12
- // scale below — they are consumed inline as a JS import rather than CSS vars.
13
- // A JS import also resolves on native RN, unlike var(--…) strings.
11
+ // fastest/best; for RPE, green = easiest. This is the [0,1,2,4] subsample of
12
+ // the canonical `sequentialEffort` primitive. The `orange` key is the band
13
+ // label. Consumed inline (RN).
14
14
  scale: {
15
- green: '#2ed573',
16
- yellow: '#ffd43b',
17
- orange: '#ffa502',
18
- red: '#ff4757',
15
+ green: sequentialEffort[0],
16
+ yellow: sequentialEffort[1],
17
+ orange: sequentialEffort[2], // gold band
18
+ red: sequentialEffort[4],
19
19
  },
20
20
 
21
- // BodyMap volume heatmap scale (drive dynamic SVG fills, so inline values).
22
- // Maps weekly-volume status against MEV/MAV/MRV landmarks to a fill color.
21
+ // BodyMap volume heatmap the canonical `divergingScale` (under optimal
22
+ // over): a true diverging shape with a light green center, cool-blue under-
23
+ // trained end and warm-red over-reaching end (colorblind-robust in lightness).
23
24
  heatmap: {
24
- none: '#E0E0E0', // gray — no training data
25
- under: '#4A90D9', // cool blue — below MEV
26
- maintenance: '#F5C842', // warm yellow — MEV to MAV
27
- productive: '#4CAF50', // green — MAV to MRV
28
- approaching: '#FFA502', // orange — near MRV
29
- over: '#FF6B35', // red-orange — over MRV
25
+ none: '#E0E0E0', // no training data
26
+ under: divergingScale[0], // below MEV
27
+ maintenance: divergingScale[1], // MEV to MAV
28
+ productive: divergingScale[2], // optimal center — MAV to MRV
29
+ approaching: divergingScale[3], // near MRV
30
+ over: divergingScale[4], // over MRV
30
31
  },
31
32
 
32
33
  // Badge border-radius (rounded-sm is 4px, we need 2px)
@@ -36,12 +37,12 @@ export const WORKOUT_TOKENS = {
36
37
  // components use the CSS custom properties (var(--color-surface-*/border-*))
37
38
  // directly. Only theme-independent data-viz values remain below.
38
39
 
39
- // Intensity bar specific
40
+ // Intensity bar specific — over-target tiers deepen along the red ramp.
40
41
  intensity: {
41
42
  track: '#333333',
42
- over1: '#D14343',
43
- over2: '#A62626',
44
- over3: '#7A1C1C',
43
+ over1: ramp.red[600], // matches status-error
44
+ over2: ramp.red[700],
45
+ over3: ramp.red[800],
45
46
  targetLine: 'rgba(33, 150, 243, 0.5)',
46
47
  atTargetGlow:
47
48
  '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)',
@@ -1,6 +1,6 @@
1
- const AVATAR_COLORS = [
2
- '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'
3
- ]
1
+ import { AVATAR_CATEGORICAL_COLORS } from '../theme/extracted-colors-ui'
2
+
3
+ const AVATAR_COLORS = AVATAR_CATEGORICAL_COLORS
4
4
 
5
5
  export function avatarColor(name: string): string {
6
6
  let hash = 0
@@ -44,6 +44,12 @@ module.exports = {
44
44
  dark: 'var(--color-status-error-dark)',
45
45
  subtle: 'var(--color-status-error-subtle)',
46
46
  },
47
+ 'error-vivid': {
48
+ DEFAULT: 'var(--color-status-error-vivid)',
49
+ light: 'var(--color-status-error-vivid-light)',
50
+ dark: 'var(--color-status-error-vivid-dark)',
51
+ subtle: 'var(--color-status-error-vivid-subtle)',
52
+ },
47
53
  warning: {
48
54
  DEFAULT: 'var(--color-status-warning)',
49
55
  light: 'var(--color-status-warning-light)',
@@ -132,6 +138,7 @@ module.exports = {
132
138
  DEFAULT: 'var(--color-border-default)',
133
139
  subtle: 'var(--color-border-subtle)',
134
140
  strong: 'var(--color-border-strong)',
141
+ prominent: 'var(--color-border-prominent)',
135
142
  focus: 'var(--color-border-focus)',
136
143
  input: {
137
144
  DEFAULT: 'var(--color-border-input)',
@@ -184,10 +191,10 @@ module.exports = {
184
191
  xl: '0px 20px 25px rgba(100, 116, 139, 0.12)',
185
192
  // Glow shadows (colored radial glow for emphasis)
186
193
  'glow-primary': '0 0 20px 2px rgba(var(--color-brand-primary-rgb, 255, 121, 0), 0.4)',
187
- 'glow-secondary': '0 0 20px 2px rgba(var(--color-brand-secondary-rgb, 64, 109, 135), 0.4)',
188
- 'glow-success': '0 0 20px 2px rgba(var(--color-status-success-rgb, 20, 184, 166), 0.35)',
194
+ 'glow-secondary': '0 0 20px 2px rgba(var(--color-brand-secondary-rgb, 48, 123, 155), 0.4)',
195
+ 'glow-success': '0 0 20px 2px rgba(var(--color-status-success-rgb, 46, 213, 115), 0.35)',
189
196
  'glow-error': '0 0 20px 2px rgba(var(--color-status-error-rgb, 209, 67, 67), 0.4)',
190
- 'glow-warning': '0 0 20px 2px rgba(var(--color-status-warning-rgb, 255, 176, 32), 0.35)',
197
+ 'glow-warning': '0 0 20px 2px rgba(var(--color-status-warning-rgb, 249, 180, 21), 0.35)',
191
198
  'glow-info': '0 0 20px 2px rgba(var(--color-status-info-rgb, 33, 150, 243), 0.35)',
192
199
  'glow-sm': '0 0 12px 0px',
193
200
  'glow-md': '0 0 20px 2px',