@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
@@ -1,5 +1,8 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { Treemap, type TreemapDatum } from './Treemap'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+
5
+ const t = getSemanticColors('dark')
3
6
 
4
7
  const meta: Meta<typeof Treemap> = {
5
8
  title: 'Custom/Charts/Treemap',
@@ -16,14 +19,14 @@ export default meta
16
19
  type Story = StoryObj<typeof Treemap>
17
20
 
18
21
  const sample: TreemapDatum[] = [
19
- { id: 'router', value: 5035, color: '#F83030', label: 'router.ts' },
20
- { id: 'engine', value: 4378, color: '#F83030', label: 'engine.ts' },
21
- { id: 'linker', value: 1695, color: '#F4A736', label: 'linker.ts' },
22
- { id: 'validate', value: 1288, color: '#F4A736', label: 'validate.ts' },
23
- { id: 'hook', value: 1050, color: '#F4A736', label: 'hook.ts' },
24
- { id: 'update', value: 890, color: '#5B9BD5', label: 'update.ts' },
25
- { id: 'pool', value: 504, color: '#5B9BD5', label: 'pool.ts' },
26
- { id: 'log', value: 342, color: '#5B9BD5', label: 'log.ts' },
22
+ { id: 'router', value: 5035, color: t['data-4'], label: 'router.ts' },
23
+ { id: 'engine', value: 4378, color: t['data-4'], label: 'engine.ts' },
24
+ { id: 'linker', value: 1695, color: t['data-6'], label: 'linker.ts' },
25
+ { id: 'validate', value: 1288, color: t['data-6'], label: 'validate.ts' },
26
+ { id: 'hook', value: 1050, color: t['data-6'], label: 'hook.ts' },
27
+ { id: 'update', value: 890, color: t['data-7'], label: 'update.ts' },
28
+ { id: 'pool', value: 504, color: t['data-7'], label: 'pool.ts' },
29
+ { id: 'log', value: 342, color: t['data-7'], label: 'log.ts' },
27
30
  ]
28
31
 
29
32
  export const Default: Story = {
@@ -33,7 +36,7 @@ export const Default: Story = {
33
36
  /** A single outlier under a linear scale swallows the canvas... */
34
37
  export const OutlierLinear: Story = {
35
38
  args: {
36
- data: [{ id: 'mega', value: 205422, color: '#F83030', label: 'mega.ts' }, ...sample],
39
+ data: [{ id: 'mega', value: 205422, color: t['data-4'], label: 'mega.ts' }, ...sample],
37
40
  width: 560,
38
41
  height: 300,
39
42
  scale: 'linear',
@@ -43,7 +46,7 @@ export const OutlierLinear: Story = {
43
46
  /** ...the same data with a sqrt scale keeps every tile legible. */
44
47
  export const OutlierSqrt: Story = {
45
48
  args: {
46
- data: [{ id: 'mega', value: 205422, color: '#F83030', label: 'mega.ts' }, ...sample],
49
+ data: [{ id: 'mega', value: 205422, color: t['data-4'], label: 'mega.ts' }, ...sample],
47
50
  width: 560,
48
51
  height: 300,
49
52
  scale: 'sqrt',
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import { View, Text, Pressable, type ViewProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
4
  import { DATAVIZ_CATEGORICAL_PALETTE } from '../../../theme/extracted-colors-dataviz'
5
+ import { primitiveColors } from '../../../theme/tokens/primitives'
5
6
 
6
7
  export interface TreemapDatum {
7
8
  /** Stable identity — returned by onPress and used as the React key. */
@@ -146,6 +147,7 @@ export function Treemap({
146
147
  {
147
148
  id: '__more__',
148
149
  value: restValue,
150
+ // VW-82: no token at this value (nearest surface-overlay #373635); proposed, not swapped.
149
151
  color: '#3a3a3a',
150
152
  label: `+${clean.length - (maxTiles - 1)} more`,
151
153
  },
@@ -177,12 +179,13 @@ export function Treemap({
177
179
  borderRadius: 3,
178
180
  opacity: selected ? 1 : 0.9,
179
181
  borderWidth: selected ? 2 : 0,
180
- borderColor: '#ffffff',
182
+ borderColor: primitiveColors.white,
181
183
  padding: 4,
182
184
  overflow: 'hidden',
183
185
  }}
184
186
  >
185
187
  {labelFits && (
188
+ // VW-82: label on a data-coloured tile; no on-data token exists yet.
186
189
  <Text numberOfLines={2} className="text-[10px] font-semibold text-[#0b0b0b]">
187
190
  {r.datum.label ?? r.datum.id}
188
191
  </Text>
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { ActiveWorkoutPage, type ActiveWorkoutExercise } from './ActiveWorkoutPage'
3
3
  import type { SetRowProps } from './SetRow'
4
+ import { primitiveRamps } from '../../../theme/tokens/primitives'
4
5
 
5
6
  const velocities = (base: number): number[] =>
6
7
  Array.from({ length: 8 }, (_, i) => Number((base - i * 0.03).toFixed(2)))
@@ -109,7 +110,7 @@ const meta: Meta<typeof ActiveWorkoutPage> = {
109
110
  title: 'Push Day A',
110
111
  subtitle: 'Hypertrophy · Week 6 of 8',
111
112
  exercises,
112
- supersets: [{ id: 'ss1', label: 'SS1', color: '#22D3EE' }],
113
+ supersets: [{ id: 'ss1', label: 'SS1', color: primitiveRamps.cyan[300] }],
113
114
  input: { reps: '8', weight: '195' },
114
115
  rest: { totalSeconds: 120, elapsedMs: 42_000, nextSetInfo: 'Set 3 · 8 × 195 lbs' },
115
116
  },
@@ -2,6 +2,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
3
  import { DumbbellIcon } from './icons'
4
4
  import { BaseBadge } from './BaseBadge'
5
+ import { greyRamp } from '../../../theme/tokens/primitives'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
+
8
+ const t = getSemanticColors('dark')
5
9
 
6
10
  const meta: Meta<typeof BaseBadge> = {
7
11
  title: 'Custom/Workout/BaseBadge',
@@ -17,7 +21,7 @@ const meta: Meta<typeof BaseBadge> = {
17
21
  export default meta
18
22
  type Story = StoryObj<typeof BaseBadge>
19
23
 
20
- const Label = ({ children, color = '#9CA3AF' }: { children: string; color?: string }) => (
24
+ const Label = ({ children, color = greyRamp[400] }: { children: string; color?: string }) => (
21
25
  <Text
22
26
  style={{
23
27
  fontFamily: '"Space Grotesk", sans-serif',
@@ -40,14 +44,14 @@ export const Plain: Story = {
40
44
  export const Pr: Story = {
41
45
  args: {
42
46
  variant: 'pr',
43
- children: <Label color="#FF7900">PR</Label>,
47
+ children: <Label color={t['brand-primary']}>PR</Label>,
44
48
  },
45
49
  }
46
50
 
47
51
  export const WithIcon: Story = {
48
52
  args: {
49
53
  variant: 'plain',
50
- icon: <DumbbellIcon size={12} color="#9CA3AF" strokeWidth={2} />,
54
+ icon: <DumbbellIcon size={12} color={greyRamp[400]} strokeWidth={2} />,
51
55
  children: <Label>225 lbs</Label>,
52
56
  },
53
57
  }
@@ -68,17 +72,17 @@ export const AllVariants: Story = {
68
72
  </View>
69
73
  <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
70
74
  <BaseBadge variant="pr" size="sm">
71
- <Label color="#FF7900">PR</Label>
75
+ <Label color={t['brand-primary']}>PR</Label>
72
76
  </BaseBadge>
73
77
  <BaseBadge variant="pr" size="md">
74
- <Label color="#FF7900">PR</Label>
78
+ <Label color={t['brand-primary']}>PR</Label>
75
79
  </BaseBadge>
76
80
  <BaseBadge variant="pr" size="lg">
77
- <Label color="#FF7900">PR</Label>
81
+ <Label color={t['brand-primary']}>PR</Label>
78
82
  </BaseBadge>
79
83
  </View>
80
84
  <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
81
- <BaseBadge icon={<DumbbellIcon size={12} color="#9CA3AF" strokeWidth={2} />}>
85
+ <BaseBadge icon={<DumbbellIcon size={12} color={greyRamp[400]} strokeWidth={2} />}>
82
86
  <Label>225 lbs</Label>
83
87
  </BaseBadge>
84
88
  </View>
@@ -2,6 +2,7 @@
2
2
  import { useState, type ReactNode } from 'react'
3
3
  import { View, Pressable, Animated, type ViewProps } from 'react-native'
4
4
  import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+ import { alpha } from '../../../utils/colors'
5
6
 
6
7
  const t = getSemanticColors('dark')
7
8
 
@@ -32,8 +33,8 @@ export const baseBadgeSizeConfig: Record<
32
33
  const variantColors: Record<BaseBadgeVariant, { backgroundColor: string; borderColor: string }> = {
33
34
  plain: { backgroundColor: t['surface-raised'], borderColor: t['hairline-default'] },
34
35
  pr: {
35
- backgroundColor: 'rgba(255, 121, 0, 0.12)',
36
- borderColor: 'rgba(255, 121, 0, 0.3)',
36
+ backgroundColor: alpha(t['brand-primary'], 0.12),
37
+ borderColor: alpha(t['brand-primary'], 0.3),
37
38
  },
38
39
  }
39
40
 
@@ -5,6 +5,8 @@ import BodyHighlighter, { type ExtendedBodyPart, type Slug } from 'react-native-
5
5
  import { cn } from '../../../utils/cn'
6
6
  import { resolveColor } from '../../../theme/resolve-color'
7
7
  import { getSemanticColors } from '../../../theme/tokens/semantic'
8
+ import { primitiveColors } from '../../../theme/tokens/primitives'
9
+ import { alpha } from '../../../utils/colors'
8
10
  import { getGlowShadow } from '../../../theme/elevation'
9
11
  import {
10
12
  MuscleGroup,
@@ -29,8 +31,8 @@ const Body = ((BodyHighlighter as unknown as { default?: typeof BodyHighlighter
29
31
 
30
32
  const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
31
33
 
32
- const OUTLINE_FILL = 'rgba(255,255,255,0.08)'
33
- const OUTLINE_BORDER = 'rgba(255,255,255,0.12)'
34
+ const OUTLINE_FILL = alpha(primitiveColors.white, 0.08)
35
+ const OUTLINE_BORDER = alpha(primitiveColors.white, 0.12)
34
36
  const BODY_SCALE = 0.8 // 200x400 intrinsic -> ~160x320 px
35
37
 
36
38
  export interface BodyMapData {
@@ -278,7 +280,7 @@ function ViewToggle({ view, onViewChange }: ViewToggleProps) {
278
280
  paddingHorizontal: 12,
279
281
  paddingVertical: 4,
280
282
  borderRadius: 9999,
281
- backgroundColor: active ? 'rgba(255,121,0,0.16)' : 'transparent',
283
+ backgroundColor: active ? alpha(BRAND_PRIMARY, 0.16) : 'transparent',
282
284
  borderWidth: 1,
283
285
  borderColor: active ? BRAND_PRIMARY : resolveColor('hairline-strong'),
284
286
  }}
@@ -8,6 +8,10 @@ import {
8
8
  } from './BodyMapDetailPanel'
9
9
  import { MuscleGroup } from './muscleTaxonomy'
10
10
  import { Surface } from '../../ui/surface'
11
+ import { primitiveColors } from '../../../theme/tokens/primitives'
12
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
13
+
14
+ const t = getSemanticColors('dark')
11
15
 
12
16
  const contributing: ContributingExercise[] = [
13
17
  { name: 'Barbell Bench Press', sets: 4, contributionWeight: 1 },
@@ -141,7 +145,7 @@ function InteractiveBodyMapDetailPanel() {
141
145
  position: 'relative',
142
146
  height: 640,
143
147
  width: 380,
144
- backgroundColor: '#0E0E0E',
148
+ backgroundColor: t['background-frame'],
145
149
  padding: 16,
146
150
  }}
147
151
  >
@@ -150,13 +154,19 @@ function InteractiveBodyMapDetailPanel() {
150
154
  accessibilityRole="button"
151
155
  style={{
152
156
  alignSelf: 'flex-start',
153
- backgroundColor: '#FF7900',
157
+ backgroundColor: t['brand-primary'],
154
158
  paddingVertical: 10,
155
159
  paddingHorizontal: 20,
156
160
  borderRadius: 8,
157
161
  }}
158
162
  >
159
- <Text style={{ color: '#FFFFFF', fontWeight: '700', fontFamily: 'Inter, sans-serif' }}>
163
+ <Text
164
+ style={{
165
+ color: primitiveColors.white,
166
+ fontWeight: '700',
167
+ fontFamily: 'Inter, sans-serif',
168
+ }}
169
+ >
160
170
  Tap Chest
161
171
  </Text>
162
172
  </Pressable>
@@ -19,7 +19,9 @@ import {
19
19
  type VolumeStatus,
20
20
  } from './muscleTaxonomy'
21
21
  import { getSemanticColors } from '../../../theme/tokens/semantic'
22
+ import { primitiveColors } from '../../../theme/tokens/primitives'
22
23
  import { liftStyle } from '../../../theme/lift'
24
+ import { alpha } from '../../../utils/colors'
23
25
 
24
26
  const t = getSemanticColors('dark')
25
27
 
@@ -191,7 +193,7 @@ export function BodyMapDetailPanel({
191
193
  left: 0,
192
194
  right: 0,
193
195
  bottom: 0,
194
- backgroundColor: '#000000',
196
+ backgroundColor: primitiveColors.black,
195
197
  opacity: backdrop,
196
198
  }}
197
199
  testID="body-map-detail-panel-backdrop-anim"
@@ -488,9 +490,9 @@ export function BodyMapDetailPanel({
488
490
  paddingVertical: 10,
489
491
  borderRadius: 8,
490
492
  alignItems: 'center',
491
- backgroundColor: 'rgba(255,121,0,0.12)',
493
+ backgroundColor: alpha(BRAND_PRIMARY, 0.12),
492
494
  borderWidth: 1,
493
- borderColor: 'rgba(255,121,0,0.3)',
495
+ borderColor: alpha(BRAND_PRIMARY, 0.3),
494
496
  }}
495
497
  testID="body-map-detail-panel-view-exercises"
496
498
  >
@@ -3,6 +3,7 @@ import { useState } from 'react'
3
3
  import { View, Text } from 'react-native'
4
4
  import { CapacityBandChart } from './CapacityBandChart'
5
5
  import type { CapacityBandDataPoint, CapacityBandProjection, WorkoutDot } from './CapacityBandChart'
6
+ import { greyRamp } from '../../../theme/tokens/primitives'
6
7
 
7
8
  const band: CapacityBandDataPoint[] = [
8
9
  { date: '2026-06-01', bandLow: 40, bandHigh: 70 },
@@ -113,7 +114,7 @@ function InteractiveCapacityBandChart() {
113
114
  height={200}
114
115
  onWorkoutPress={setSelected}
115
116
  />
116
- <Text style={{ fontSize: 12, fontFamily: 'Inter, sans-serif', color: '#9CA3AF' }}>
117
+ <Text style={{ fontSize: 12, fontFamily: 'Inter, sans-serif', color: greyRamp[400] }}>
117
118
  {selected
118
119
  ? `Selected ${selected.date}: load ${selected.load} (${selected.status})`
119
120
  : 'Tap a workout dot to inspect the session.'}
@@ -2,6 +2,7 @@
2
2
  import { useEffect, useState, useMemo } from 'react'
3
3
  import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-native'
4
4
  import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+ import { alpha } from '../../../utils/colors'
5
6
 
6
7
  const t = getSemanticColors('dark')
7
8
 
@@ -10,10 +11,11 @@ const STATUS_WARNING = t['status-warning']
10
11
  const STATUS_INFO = t['status-info']
11
12
  /** Dot outline: near-white ring so load dots read on the band fill and any
12
13
  * surface (matches the MesoStatusCard gauge-marker convention). */
14
+ // VW-82: no token at this value (nearest text-primary #F9F6F3). Left raw, proposed.
13
15
  const DOT_BORDER = '#F3F4F6'
14
- const BAND_FILL = 'rgba(46,213,115,0.1)'
15
- const BAND_EDGE = 'rgba(46,213,115,0.45)'
16
- const PROJECTION_FILL = 'rgba(46,213,115,0.05)'
16
+ const BAND_FILL = alpha(STATUS_SUCCESS, 0.1)
17
+ const BAND_EDGE = alpha(STATUS_SUCCESS, 0.45)
18
+ const PROJECTION_FILL = alpha(STATUS_SUCCESS, 0.05)
17
19
 
18
20
  const PADDING_LEFT = 28
19
21
  const PADDING_RIGHT = 10
@@ -3,6 +3,10 @@ import { render, screen } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { DeviationBar } from './DeviationBar'
5
5
  import { greyRamp } from '../../../theme/tokens/primitives'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
+ import { alpha } from '../../../utils/colors'
8
+
9
+ const t = getSemanticColors('dark')
6
10
 
7
11
  describe('DeviationBar', () => {
8
12
  it('renders the bar and dot', () => {
@@ -75,8 +79,7 @@ describe('DeviationBar', () => {
75
79
  const bar = getByTestId('deviation-bar')
76
80
  const track = bar.firstElementChild as HTMLElement
77
81
  expect(track).toHaveStyle({
78
- backgroundImage:
79
- 'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(249,180,21,0.25) 100%)',
82
+ backgroundImage: `linear-gradient(90deg, ${alpha(t['status-success'], 0.25)} 0%, rgba(107,114,128,0.15) 50%, ${alpha(t['status-warning'], 0.25)} 100%)`,
80
83
  })
81
84
  })
82
85
 
@@ -3,6 +3,7 @@ import { View, type ViewProps, type ViewStyle } from 'react-native'
3
3
  import { getSemanticColors } from '../../../theme/tokens/semantic'
4
4
  import { greyRamp } from '../../../theme/tokens/primitives'
5
5
  import { liftStyle } from '../../../theme/lift'
6
+ import { alpha } from '../../../utils/colors'
6
7
 
7
8
  const t = getSemanticColors('dark')
8
9
 
@@ -60,8 +61,10 @@ export function DeviationBar({ deviation, width, className, ...props }: Deviatio
60
61
  width: resolvedWidth,
61
62
  borderRadius: 3,
62
63
  // react-native-web renders backgroundImage at runtime; not in RN ViewStyle types
63
- backgroundImage:
64
- 'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(249,180,21,0.25) 100%)',
64
+ // rgba(107,114,128,0.15) at 50% has no exact token match (nearest:
65
+ // semanticPins.textSecondaryLight #65748B, ΔRGB ~12.7) — left as a
66
+ // raw literal (VW-78).
67
+ backgroundImage: `linear-gradient(90deg, ${alpha(t['status-success'], 0.25)} 0%, rgba(107,114,128,0.15) 50%, ${alpha(t['status-warning'], 0.25)} 100%)`,
65
68
  } as ViewStyle
66
69
  }
67
70
  />
@@ -27,13 +27,14 @@ const meta: Meta<typeof DualVelocityStrip> = {
27
27
  'silhouette. Reuses VelocityStrip’s slot vocabulary (rep / todo / variable / ' +
28
28
  'continue), its velocity-zone colors, hero geometry, and the live-rep pop. Color is ' +
29
29
  'ALWAYS the velocity zone; SIDE is encoded by position only, never hue. `variant="hero"` ' +
30
- 'is the wall scale (value labels + per-side running-best reference lines); `variant="rail"` ' +
31
- 'is the compact rail-expanded scale. Single-voltra sets keep using VelocityStrip.',
30
+ 'is the wall scale (value labels + per-side running-best reference lines); ' +
31
+ '`variant="dual-expanded"` is the compact rail-expanded scale (`"rail"` is a deprecated ' +
32
+ 'alias for the same renderer). Single-voltra sets keep using VelocityStrip.',
32
33
  },
33
34
  },
34
35
  },
35
36
  argTypes: {
36
- variant: { control: 'inline-radio', options: ['hero', 'rail'] },
37
+ variant: { control: 'inline-radio', options: ['hero', 'dual-expanded'] },
37
38
  targetReps: { control: 'number' },
38
39
  liveRepIndex: { control: 'number' },
39
40
  height: { control: { type: 'number', min: 48, max: 320, step: 4 } },
@@ -81,9 +82,9 @@ export const Hero: Story = {
81
82
  ),
82
83
  }
83
84
 
84
- /** RAIL — the same diverging language at the compact rail-expanded scale (no labels/refs). */
85
- export const RailExpanded: Story = {
86
- args: { variant: 'rail', targetReps: 8, height: 84 },
85
+ /** DUAL-EXPANDED — the same diverging language at the compact rail-expanded scale (no labels/refs). */
86
+ export const DualExpanded: Story = {
87
+ args: { variant: 'dual-expanded', targetReps: 8, height: 84 },
87
88
  render: (args) => (
88
89
  <Page>
89
90
  <Panel width={320}>
@@ -182,16 +182,16 @@ describe('DualVelocityStrip side labels', () => {
182
182
  expect(screen.queryByTestId('dual-velocity-side-label-L')).not.toBeInTheDocument()
183
183
  })
184
184
 
185
- it('drops the gutter/slot labels AND the centre axis at rail scale (the lean dual-expanded)', () => {
185
+ it('drops the gutter/slot labels AND the centre axis at dual-expanded scale', () => {
186
186
  render(
187
187
  <DualVelocityStrip
188
188
  left={{ velocities: [0.9], label: 'Left Arm' }}
189
189
  right={{ velocities: [0.8], label: 'Right Arm' }}
190
- variant="rail"
190
+ variant="dual-expanded"
191
191
  />
192
192
  )
193
- // Rail = the lean dual-expanded: no gutter/vertical-axis + no slot labels (the two wings read as
194
- // separate rows via the vertical gap), and no centre axis — only the bolder hero keeps those.
193
+ // dual-expanded = the lean renderer: no gutter/vertical-axis + no slot labels (the two wings
194
+ // read as separate rows via the vertical gap), and no centre axis — only the bolder hero keeps those.
195
195
  expect(screen.queryByTestId('dual-velocity-side-label-L')).not.toBeInTheDocument()
196
196
  expect(screen.queryByTestId('dual-velocity-side-label-R')).not.toBeInTheDocument()
197
197
  expect(screen.queryByTestId('dual-velocity-axis')).not.toBeInTheDocument()
@@ -225,12 +225,12 @@ describe('DualVelocityStrip reference lines', () => {
225
225
  expect(wingDown().getByTestId('velocity-hero-reference')).toBeInTheDocument()
226
226
  })
227
227
 
228
- it('omits reference lines at rail scale', () => {
228
+ it('omits reference lines at dual-expanded scale', () => {
229
229
  render(
230
230
  <DualVelocityStrip
231
231
  left={{ velocities: [0.9, 0.85] }}
232
232
  right={{ velocities: [0.82, 0.74] }}
233
- variant="rail"
233
+ variant="dual-expanded"
234
234
  />
235
235
  )
236
236
  expect(screen.queryByTestId('velocity-hero-reference')).not.toBeInTheDocument()
@@ -478,12 +478,12 @@ describe('DualVelocityStrip live mode', () => {
478
478
  ])
479
479
  })
480
480
 
481
- it('arms both wings at rail scale too — its wings are composed strips as well', () => {
481
+ it('arms both wings at dual-expanded scale too — its wings are composed strips as well', () => {
482
482
  render(
483
483
  <DualVelocityStrip
484
484
  left={{ velocities: [0.9, 0.85, 0.8] }}
485
485
  right={{ velocities: [0.82, 0.78, 0.7] }}
486
- variant="rail"
486
+ variant="dual-expanded"
487
487
  liveRepIndex={2}
488
488
  />
489
489
  )
@@ -591,16 +591,16 @@ describe('DualVelocityStrip compact variant (folded)', () => {
591
591
  })
592
592
  })
593
593
 
594
- describe('DualVelocityStrip rail variant', () => {
594
+ describe('DualVelocityStrip dual-expanded variant', () => {
595
595
  it('renders compact bars but no value labels or reference lines', () => {
596
596
  render(
597
597
  <DualVelocityStrip
598
598
  left={{ velocities: [0.9, 0.85] }}
599
599
  right={{ velocities: [0.82, 0.74] }}
600
- variant="rail"
600
+ variant="dual-expanded"
601
601
  />
602
602
  )
603
- // The rail is now COMPOSED from two bare `expanded` strips, so each wing emits the shared
603
+ // dual-expanded is COMPOSED from two bare `expanded` strips, so each wing emits the shared
604
604
  // SetBarChart testIDs rather than the old bespoke `dual-velocity-bar-*` ones. Query per wing.
605
605
  const up = within(screen.getByTestId('dual-velocity-wing-up'))
606
606
  const down = within(screen.getByTestId('dual-velocity-wing-down'))
@@ -616,7 +616,7 @@ describe('DualVelocityStrip rail variant', () => {
616
616
  <DualVelocityStrip
617
617
  left={{ velocities: [0.9] }}
618
618
  right={{ velocities: [0.8] }}
619
- variant="rail"
619
+ variant="dual-expanded"
620
620
  targetReps={2}
621
621
  />
622
622
  )
@@ -633,7 +633,7 @@ describe('DualVelocityStrip rail variant', () => {
633
633
  <DualVelocityStrip
634
634
  left={{ velocities: [0.9, 0.86, 0.82] }}
635
635
  right={{ velocities: [0.85, 0.8] }}
636
- variant="rail"
636
+ variant="dual-expanded"
637
637
  />
638
638
  )
639
639
  const up = within(screen.getByTestId('dual-velocity-wing-up'))
@@ -650,7 +650,7 @@ describe('DualVelocityStrip rail variant', () => {
650
650
  <DualVelocityStrip
651
651
  left={{ set: { type: 'range', velocities: [0.9, 0.86], floor: 3, max: 4 } }}
652
652
  right={{ set: { type: 'range', velocities: [0.85, 0.8], floor: 3, max: 4 } }}
653
- variant="rail"
653
+ variant="dual-expanded"
654
654
  />
655
655
  )
656
656
  const up = within(screen.getByTestId('dual-velocity-wing-up'))
@@ -658,6 +658,19 @@ describe('DualVelocityStrip rail variant', () => {
658
658
  expect(up.queryAllByTestId('velocity-slot-variable').length).toBeGreaterThan(0)
659
659
  expect(down.queryAllByTestId('velocity-slot-variable').length).toBeGreaterThan(0)
660
660
  })
661
+
662
+ it('accepts the deprecated `rail` alias, rendering identically to `dual-expanded`', () => {
663
+ const props = {
664
+ left: { velocities: [0.9, 0.86, 0.82] },
665
+ right: { velocities: [0.85, 0.8] },
666
+ targetReps: 4,
667
+ }
668
+ const viaAlias = render(<DualVelocityStrip {...props} variant="rail" />)
669
+ const aliasHtml = viaAlias.container.innerHTML
670
+ viaAlias.unmount()
671
+ const viaCanonical = render(<DualVelocityStrip {...props} variant="dual-expanded" />)
672
+ expect(viaCanonical.container.innerHTML).toBe(aliasHtml)
673
+ })
661
674
  })
662
675
 
663
676
  describe('DualVelocityStrip accessibility', () => {
@@ -673,12 +686,12 @@ describe('DualVelocityStrip accessibility', () => {
673
686
  expect(await axe(container)).toHaveNoViolations()
674
687
  })
675
688
 
676
- it('has no violations at rail scale', async () => {
689
+ it('has no violations at dual-expanded scale', async () => {
677
690
  const { container } = render(
678
691
  <DualVelocityStrip
679
692
  left={{ velocities: [0.9] }}
680
693
  right={{ velocities: [0.8] }}
681
- variant="rail"
694
+ variant="dual-expanded"
682
695
  />
683
696
  )
684
697
  expect(await axe(container)).toHaveNoViolations()
@@ -11,14 +11,14 @@ const meta: Meta<typeof ExerciseCard> = {
11
11
  docs: {
12
12
  description: {
13
13
  component:
14
- '**Organism** (data-contract card). Three representations: `upcoming` (a dimmed, ' +
15
- 'not-yet-reached row), collapsed (name + summary + per-set velocity strips), and expanded ' +
16
- ' the unified card whose persistent header is the real ' +
17
- '[ExerciseCardHeading](?path=/docs/custom-workout-exercisecardheading--docs) over a SET · REPS · ' +
18
- 'LBS · RPE body of [SetRow](?path=/docs/custom-workout-setrow--docs)s (compact spotlight on the ' +
19
- 'live set). Expand is controlled (`expanded` + `onExpandedChange`) or uncontrolled ' +
20
- '(`defaultExpanded`); `upcoming` overrides expand.' +
21
- ' Composes **VelocityStrip** · **PlaceholderStrip** · **PrBadge** · **SetRow** · **SetTableHeader** · **ExerciseCardHeading**.',
14
+ '**Organism** (data-contract card). Three representations, all ONE ' +
15
+ '[ExerciseCardHeading](?path=/docs/custom-workout-exercisecardheading--docs) selected by its ' +
16
+ '`density`: `upcoming` (a dimmed, not-yet-reached row), collapsed (name + prescription + the ' +
17
+ 'per-set strip) and expanded — the `rail` heading over a SET · REPS · LBS · RPE body of ' +
18
+ '[SetRow](?path=/docs/custom-workout-setrow--docs)s (compact spotlight on the live set). ' +
19
+ 'Expand is controlled (`expanded` + `onExpandedChange`) or uncontrolled (`defaultExpanded`); ' +
20
+ '`upcoming` overrides expand.' +
21
+ ' Composes **ExerciseCardHeading** · **SetRow** · **SetTableHeader**.',
22
22
  },
23
23
  },
24
24
  },