@titan-design/react-ui 0.11.0 → 0.12.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 (142) hide show
  1. package/dist/bodymap.js +173 -343
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +173 -343
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +24 -21
  6. package/dist/index.d.ts +24 -21
  7. package/dist/index.js +366 -561
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +360 -553
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/pages.js +242 -407
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +242 -407
  14. package/dist/pages.mjs.map +1 -1
  15. package/dist/theme/index.d.mts +409 -415
  16. package/dist/theme/index.d.ts +409 -415
  17. package/dist/theme/index.js +234 -408
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +227 -399
  20. package/dist/theme/index.mjs.map +1 -1
  21. package/dist/theme/tokens-css.js +141 -140
  22. package/dist/theme/tokens-css.js.map +1 -1
  23. package/dist/theme/tokens-css.mjs +141 -140
  24. package/dist/theme/tokens-css.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/src/arch/Architecture.stories.tsx +9 -9
  27. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  28. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +3 -3
  29. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  30. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +3 -3
  31. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +1 -1
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +2 -2
  33. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  34. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +4 -4
  35. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +3 -3
  36. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  37. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -12
  38. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  39. package/src/components/custom/Table/Table.tsx +2 -2
  40. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  41. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  42. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  43. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  44. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  45. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  46. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  47. package/src/components/custom/Workout/ExerciseCard.test.tsx +3 -3
  48. package/src/components/custom/Workout/ExerciseCard.tsx +2 -2
  49. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -4
  50. package/src/components/custom/Workout/InputBar.tsx +2 -2
  51. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  52. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  53. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  54. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  55. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  56. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  57. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  58. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  59. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  60. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  61. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  62. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  63. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  64. package/src/components/custom/Workout/SessionRail.tsx +13 -5
  65. package/src/components/custom/Workout/SetBar.tsx +3 -3
  66. package/src/components/custom/Workout/SetRow.test.tsx +6 -3
  67. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  68. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  69. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  70. package/src/components/custom/Workout/VelocityStrip.test.tsx +5 -2
  71. package/src/components/custom/Workout/VelocityStrip.tsx +4 -4
  72. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  73. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  74. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  75. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  76. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  77. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  78. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  79. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  80. package/src/components/custom/charts/SetBarChart.tsx +3 -3
  81. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  82. package/src/components/custom/stepper/Stepper.tsx +3 -3
  83. package/src/components/shell/DeviceMenu.tsx +2 -2
  84. package/src/components/shell/SideNav.tsx +1 -1
  85. package/src/components/shell/TopBar.tsx +1 -1
  86. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  87. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  88. package/src/components/ui/avatar/Avatar.tsx +2 -2
  89. package/src/components/ui/badge/Badge.tsx +2 -2
  90. package/src/components/ui/card/Card.tsx +3 -3
  91. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  92. package/src/components/ui/collapse/Collapse.tsx +1 -1
  93. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  94. package/src/components/ui/drawer/Drawer.tsx +3 -3
  95. package/src/components/ui/form-field/FormField.tsx +1 -1
  96. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  97. package/src/components/ui/menu/Menu.tsx +2 -2
  98. package/src/components/ui/pill/Pill.tsx +2 -2
  99. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  100. package/src/components/ui/popover/Popover.tsx +1 -1
  101. package/src/components/ui/progress/Progress.tsx +3 -3
  102. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  103. package/src/components/ui/radio/Radio.tsx +4 -4
  104. package/src/components/ui/select/Select.tsx +3 -3
  105. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  106. package/src/components/ui/surface/Surface.test.tsx +32 -19
  107. package/src/components/ui/surface/Surface.tsx +3 -3
  108. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  109. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  110. package/src/components/ui/switch/Switch.tsx +1 -1
  111. package/src/components/ui/tabs/Tabs.tsx +2 -2
  112. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  113. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  114. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  115. package/src/stories/PresetShowcase.stories.tsx +21 -21
  116. package/src/theme/ColorPalettes.stories.tsx +41 -38
  117. package/src/theme/ColorPrimitives.stories.tsx +89 -41
  118. package/src/theme/Depth.stories.tsx +312 -0
  119. package/src/theme/DepthCalibration.stories.tsx +655 -0
  120. package/src/theme/ThemeProvider.tsx +1 -1
  121. package/src/theme/barrel.ts +7 -14
  122. package/src/theme/color-stories.coverage.test.ts +15 -20
  123. package/src/theme/color-story-kit.tsx +1 -1
  124. package/src/theme/color-utils.ts +197 -0
  125. package/src/theme/config.ts +0 -8
  126. package/src/theme/depth-calibration.test.tsx +97 -0
  127. package/src/theme/elevation.test.ts +62 -1
  128. package/src/theme/elevation.ts +34 -8
  129. package/src/theme/global.css +47 -53
  130. package/src/theme/grey-ramp.test.ts +166 -0
  131. package/src/theme/materials.test.ts +106 -0
  132. package/src/theme/materials.ts +211 -0
  133. package/src/theme/presets/audiobook.ts +4 -4
  134. package/src/theme/presets/types.ts +3 -3
  135. package/src/theme/semantic-contrast.test.ts +83 -0
  136. package/src/theme/tokens/primitives.ts +62 -115
  137. package/src/theme/tokens/semantic.ts +130 -82
  138. package/src/utils/colors.ts +1 -1
  139. package/tailwind.config.js +2 -5
  140. package/src/theme/bar-paper.ts +0 -59
  141. package/src/theme/shadows.stories.tsx +0 -523
  142. package/src/theme/shadows.ts +0 -439
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { Meta, StoryObj } from '@storybook/react-vite'
10
10
  import { View, Text } from 'react-native'
11
- import { primitiveColors } from '../../../theme/tokens/primitives'
11
+ import { primitiveColors, greyRamp } from '../../../theme/tokens/primitives'
12
12
  import { alpha } from '../../../utils/colors'
13
13
  import { getSemanticColors } from '../../../theme/tokens/semantic'
14
14
  import {
@@ -23,8 +23,8 @@ import {
23
23
  FONT_MONO,
24
24
  } from './fatigue-tokens'
25
25
 
26
- const PAGE_BG = primitiveColors.charcoal[900]
27
- const PANEL_BG = primitiveColors.charcoal[800]
26
+ const PAGE_BG = greyRamp[975]
27
+ const PANEL_BG = greyRamp[950]
28
28
  const t = getSemanticColors('dark')
29
29
 
30
30
  const meta: Meta = {
@@ -161,7 +161,7 @@ const PHASE_AXIS: SwatchDef[] = [
161
161
  },
162
162
  {
163
163
  name: 'Idle',
164
- token: 'PHASE_AXIS_COLOR.idle · charcoal[300]',
164
+ token: 'PHASE_AXIS_COLOR.idle · grey[300]',
165
165
  value: PHASE_AXIS_COLOR.idle,
166
166
  note: 'Pauses / holds on the phase band.',
167
167
  },
@@ -1,12 +1,12 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
3
  import { VelocityHero } from './VelocityHero'
4
- import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
5
5
  import { getSemanticColors } from '../../../theme/tokens/semantic'
6
6
  import { MOCK_MEAN_VELOCITIES } from './fatigue-mock'
7
7
 
8
- const PAGE_BG = primitiveColors.charcoal[900]
9
- const PANEL_BG = primitiveColors.charcoal[800]
8
+ const PAGE_BG = greyRamp[975]
9
+ const PANEL_BG = greyRamp[950]
10
10
  const t = getSemanticColors('dark')
11
11
 
12
12
  const meta: Meta<typeof VelocityHero> = {
@@ -1,11 +1,11 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
3
  import { VerdictHero } from './VerdictHero'
4
- import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
5
5
  import { getSemanticColors } from '../../../theme/tokens/semantic'
6
6
  import { FATIGUE_STATES, WARMING_UP_MODEL } from './fatigue-mock'
7
7
 
8
- const PAGE_BG = primitiveColors.charcoal[900]
8
+ const PAGE_BG = greyRamp[975]
9
9
  const t = getSemanticColors('dark')
10
10
 
11
11
  const meta: Meta<typeof VerdictHero> = {
@@ -5,7 +5,7 @@
5
5
  * the family inherits the surface-ramp refresh when it lands.
6
6
  */
7
7
  import { getSemanticColors } from '../../../theme/tokens/semantic'
8
- import { primitiveColors, primitiveRamps } from '../../../theme/tokens/primitives'
8
+ import { primitiveRamps, greyRamp } from '../../../theme/tokens/primitives'
9
9
  import type { DimensionTone, FatigueVerdictState, SamplePhase } from './fatigue-model'
10
10
 
11
11
  const t = getSemanticColors('dark')
@@ -34,9 +34,9 @@ export const STATE_LABEL: Record<FatigueVerdictState, string> = {
34
34
  * language: eccentric magenta, concentric cyan, hold and idle in the grey family.
35
35
  *
36
36
  * The idle grey has to read as BAND MATERIAL (a pause the lifter held), not as a
37
- * hole in the strip: at charcoal[300] it sat inside the surface-ramp's own value
37
+ * hole in the strip: at grey[300] it sat inside the surface-ramp's own value
38
38
  * range (`surface-raised` #31302F) so a short pause between ECC and CON read as a
39
- * gap. charcoal[200] clears every content plane while staying quieter than the two
39
+ * gap. grey[200] clears every content plane while staying quieter than the two
40
40
  * saturated phase tones, so the band reads contiguous and the phases still lead.
41
41
  *
42
42
  * `hold` stays GREY rather than taking a hue of its own. It was briefly amber, which
@@ -48,17 +48,17 @@ export const STATE_LABEL: Record<FatigueVerdictState, string> = {
48
48
  *
49
49
  * Hold and idle are separated by VALUE rather than hue, plus the `HOLD` label and the
50
50
  * fact that a hold PACES (it has a prescribed duration) while idle never fills:
51
- * unfilled hold charcoal[300] — darker than idle
52
- * idle charcoal[200]
53
- * filled hold charcoal[100] — brighter than idle
51
+ * unfilled hold grey[300] — darker than idle
52
+ * idle grey[200]
53
+ * filled hold grey[100] — brighter than idle
54
54
  * so a hold reads apart from dead time at any fill level, including the narrow runs
55
55
  * where the label drops out.
56
56
  */
57
57
  export const PHASE_AXIS_COLOR: Record<SamplePhase, string> = {
58
58
  eccentric: primitiveRamps.magenta[800],
59
59
  concentric: primitiveRamps.cyan[800],
60
- hold: primitiveColors.charcoal[100],
61
- idle: primitiveColors.charcoal[200],
60
+ hold: greyRamp[700],
61
+ idle: greyRamp[800],
62
62
  }
63
63
 
64
64
  /**
@@ -72,8 +72,8 @@ export const PHASE_AXIS_COLOR: Record<SamplePhase, string> = {
72
72
  export const PHASE_AXIS_BASE_COLOR: Record<SamplePhase, string> = {
73
73
  eccentric: primitiveRamps.magenta[950],
74
74
  concentric: primitiveRamps.cyan[950],
75
- hold: primitiveColors.charcoal[300],
76
- idle: primitiveColors.charcoal[200],
75
+ hold: greyRamp[900],
76
+ idle: greyRamp[800],
77
77
  }
78
78
 
79
79
  /**
@@ -114,7 +114,7 @@ export const PACING_TONE_MIN_CONTRAST = 4.5
114
114
  // SHADES OF RED when there's an issue. No greens, no ambers — those languages belong
115
115
  // to the verdict tones / velocity-loss bands, not here.
116
116
  /** On-track / at-or-above-working. */
117
- export const SILVER = primitiveColors.neutral[300]
117
+ export const SILVER = greyRamp[200]
118
118
  export const RED_LIGHT = primitiveRamps.red[400]
119
119
  export const RED_MID = primitiveRamps.red[600]
120
120
  export const RED_DEEP = primitiveRamps.red[800]
@@ -124,7 +124,7 @@ export const GRIND_THRESHOLD = 0.35
124
124
  /** A quiet grey the controlled line dims toward as it drifts (never a colour): a dimmed
125
125
  * cool grey in the SAME neutral family as SILVER, so a fully-drifted line reads dim-silver
126
126
  * rather than sinking toward black. Shared with the ROM chart. */
127
- export const DRIFT_GREY = primitiveColors.neutral[600]
127
+ export const DRIFT_GREY = greyRamp[700]
128
128
 
129
129
  export function clamp01(v: number): number {
130
130
  return Math.min(1, Math.max(0, v))
@@ -64,7 +64,7 @@ export function Sidebar({
64
64
  <View
65
65
  style={{ width: currentWidth }}
66
66
  className={cn(
67
- 'h-full flex-col bg-surface-base border-r border-border transition-all duration-200',
67
+ 'h-full flex-col bg-surface-base border-r border-hairline transition-all duration-200',
68
68
  className
69
69
  )}
70
70
  {...props}
@@ -85,7 +85,7 @@ export interface SidebarHeaderProps {
85
85
  */
86
86
  export function SidebarHeader({ children, className }: SidebarHeaderProps) {
87
87
  return (
88
- <View className={cn('px-4 py-4 border-b border-border-subtle', className)}>
88
+ <View className={cn('px-4 py-4 border-b border-hairline-subtle', className)}>
89
89
  {children}
90
90
  </View>
91
91
  )
@@ -120,7 +120,7 @@ export interface SidebarFooterProps {
120
120
  */
121
121
  export function SidebarFooter({ children, className }: SidebarFooterProps) {
122
122
  return (
123
- <View className={cn('px-2 py-4 border-t border-border-subtle', className)}>
123
+ <View className={cn('px-2 py-4 border-t border-hairline-subtle', className)}>
124
124
  {children}
125
125
  </View>
126
126
  )
@@ -252,6 +252,6 @@ export interface SidebarDividerProps {
252
252
  */
253
253
  export function SidebarDivider({ className }: SidebarDividerProps) {
254
254
  return (
255
- <View className={cn('h-px mx-4 my-2 bg-border-subtle', className)} />
255
+ <View className={cn('h-px mx-4 my-2 bg-hairline-subtle', className)} />
256
256
  )
257
257
  }
@@ -590,7 +590,7 @@ export function TableSelectAllCell({ className }: TableSelectAllCellProps) {
590
590
  ? 'bg-brand-primary border-brand-primary'
591
591
  : someSelected
592
592
  ? 'bg-brand-primary-subtle border-brand-primary'
593
- : 'border-border-strong bg-transparent'
593
+ : 'border-hairline-strong bg-transparent'
594
594
  )}
595
595
  >
596
596
  {(allSelected || someSelected) && (
@@ -629,7 +629,7 @@ export function TableSelectCell({ rowId, className }: TableSelectCellProps) {
629
629
  'w-5 h-5 rounded border-2 items-center justify-center',
630
630
  isSelected
631
631
  ? 'bg-brand-primary border-brand-primary'
632
- : 'border-border-strong bg-transparent web:hover:border-brand-primary'
632
+ : 'border-hairline-strong bg-transparent web:hover:border-brand-primary'
633
633
  )}
634
634
  >
635
635
  {isSelected && (
@@ -273,7 +273,7 @@ function WorkoutHeader({
273
273
  </View>
274
274
  </View>
275
275
  <View
276
- className="bg-border"
276
+ className="bg-hairline"
277
277
  style={{ height: 4, borderRadius: 2 }}
278
278
  testID="active-workout-page-progress-track"
279
279
  >
@@ -305,7 +305,7 @@ function ExerciseList({ groups, focusedId, onToggle }: ExerciseListProps) {
305
305
  return (
306
306
  <View
307
307
  key={group.exercise.id}
308
- className="bg-surface-elevated border-border"
308
+ className="bg-surface-elevated border-hairline"
309
309
  style={{
310
310
  borderWidth: 1,
311
311
  borderRadius: 12,
@@ -399,7 +399,7 @@ export function ActiveWorkoutPage({
399
399
 
400
400
  {(showRest || showInput) && (
401
401
  <View
402
- style={{ borderTopWidth: 1, borderTopColor: resolveColor('border-default') }}
402
+ style={{ borderTopWidth: 1, borderTopColor: resolveColor('hairline-default') }}
403
403
  testID="active-workout-page-bottom-bar"
404
404
  >
405
405
  {showRest && rest != null ? (
@@ -3,6 +3,11 @@ import { render, screen, fireEvent } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { Text, View } from 'react-native'
5
5
  import { BaseBadge } from './BaseBadge'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
+
8
+ // Read from the token, not pinned: these used to be hand-copied hexes and
9
+ // silently desynced when borders became alpha hairlines (TD-07.16).
10
+ const T = getSemanticColors('dark')
6
11
 
7
12
  describe('BaseBadge', () => {
8
13
  it('renders children', () => {
@@ -12,7 +17,7 @@ describe('BaseBadge', () => {
12
17
 
13
18
  it('defaults to the plain variant border color', () => {
14
19
  render(<BaseBadge testID="bb"><Text>x</Text></BaseBadge>)
15
- expect(screen.getByTestId('bb')).toHaveStyle({ borderTopColor: '#1F1F1F' })
20
+ expect(screen.getByTestId('bb')).toHaveStyle({ borderTopColor: T['hairline-default'] })
16
21
  })
17
22
 
18
23
  it('applies the pr variant border color', () => {
@@ -30,7 +30,7 @@ export const baseBadgeSizeConfig: Record<
30
30
  }
31
31
 
32
32
  const variantColors: Record<BaseBadgeVariant, { backgroundColor: string; borderColor: string }> = {
33
- plain: { backgroundColor: t['surface-raised'], borderColor: '#1F1F1F' },
33
+ plain: { backgroundColor: t['surface-raised'], borderColor: t['hairline-default'] },
34
34
  pr: {
35
35
  backgroundColor: 'rgba(255, 121, 0, 0.12)',
36
36
  borderColor: 'rgba(255, 121, 0, 0.3)',
@@ -278,7 +278,7 @@ function ViewToggle({ view, onViewChange }: ViewToggleProps) {
278
278
  borderRadius: 9999,
279
279
  backgroundColor: active ? 'rgba(255,121,0,0.16)' : 'transparent',
280
280
  borderWidth: 1,
281
- borderColor: active ? BRAND_PRIMARY : resolveColor('border-strong'),
281
+ borderColor: active ? BRAND_PRIMARY : resolveColor('hairline-strong'),
282
282
  }}
283
283
  testID={`body-map-toggle-${side}`}
284
284
  >
@@ -324,7 +324,7 @@ function MuscleButton({ entry, highlighted, onMusclePress }: MuscleButtonProps)
324
324
  paddingVertical: 3,
325
325
  borderRadius: 9999,
326
326
  borderWidth: 1,
327
- borderColor: highlighted ? BRAND_PRIMARY : resolveColor('border-default'),
327
+ borderColor: highlighted ? BRAND_PRIMARY : resolveColor('hairline-default'),
328
328
  }}
329
329
  testID={`body-map-muscle-${entry.key}`}
330
330
  >
@@ -220,7 +220,7 @@ export function BodyMapDetailPanel({
220
220
  testID="body-map-detail-panel-handle"
221
221
  >
222
222
  <View
223
- className="bg-border-strong"
223
+ className="bg-hairline-strong"
224
224
  style={{ width: 40, height: 4, borderRadius: 2 }}
225
225
  accessibilityElementsHidden
226
226
  />
@@ -380,7 +380,7 @@ export function BodyMapDetailPanel({
380
380
  {contributingExercises!.map((exercise, index) => (
381
381
  <View
382
382
  key={`${exercise.name}-${index}`}
383
- className="flex-row items-center justify-between bg-surface-raised border-border"
383
+ className="flex-row items-center justify-between bg-surface-raised border-hairline"
384
384
  style={{
385
385
  gap: 10,
386
386
  paddingVertical: 8,
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { DeviationBar } from './DeviationBar'
5
+ import { greyRamp } from '../../../theme/tokens/primitives'
5
6
 
6
7
  describe('DeviationBar', () => {
7
8
  it('renders the bar and dot', () => {
@@ -62,7 +63,7 @@ describe('DeviationBar', () => {
62
63
  const { getByTestId } = render(<DeviationBar deviation={0} />)
63
64
  const dot = getByTestId('deviation-dot')
64
65
  expect(dot).toHaveStyle({ width: '8px', height: '8px' })
65
- expect(dot).toHaveStyle({ borderTopWidth: '1.5px', borderTopColor: '#F3F4F6' })
66
+ expect(dot).toHaveStyle({ borderTopWidth: '1.5px', borderTopColor: greyRamp[50] })
66
67
  expect(dot).toHaveStyle({ boxShadow: '0 0 4px rgba(0,0,0,0.5)' })
67
68
  })
68
69
 
@@ -1,6 +1,7 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, type ViewProps, type ViewStyle } from 'react-native'
3
3
  import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
4
5
 
5
6
  const t = getSemanticColors('dark')
6
7
 
@@ -13,7 +14,7 @@ export interface DeviationBarProps extends ViewProps {
13
14
  function getDotColor(deviation: number): string {
14
15
  const abs = Math.abs(deviation)
15
16
  if (deviation < -0.3) return t['status-success']
16
- if (abs <= 0.3) return '#6B7280'
17
+ if (abs <= 0.3) return greyRamp[500]
17
18
  if (abs <= 0.7) return t['status-warning']
18
19
  return t['status-error']
19
20
  }
@@ -68,7 +69,7 @@ export function DeviationBar({
68
69
  height: dotSize,
69
70
  borderRadius: 9999,
70
71
  borderWidth: 1.5,
71
- borderColor: '#F3F4F6',
72
+ borderColor: greyRamp[50],
72
73
  boxShadow: '0 0 4px rgba(0,0,0,0.5)',
73
74
  backgroundColor: getDotColor(clamped),
74
75
  left: Math.max(0, Math.min(dotPosition - dotSize / 2, resolvedWidth - dotSize)),
@@ -3,6 +3,7 @@ import { render, screen, fireEvent, within } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { ExerciseCard } from './ExerciseCard'
5
5
  import type { SetRowProps } from './SetRow'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
6
7
 
7
8
  const baseCollapsedProps = {
8
9
  name: 'Bench Press',
@@ -86,9 +87,8 @@ describe('ExerciseCard', () => {
86
87
  sets: unifiedSets,
87
88
  }
88
89
 
89
- // Literal-hex text treatments (dark theme neutral 100 / 400).
90
- const T_ACTIVE = '#F3F4F6'
91
- const T_MUTED = '#9CA3AF'
90
+ // Read from the tokens, not pinned (see SetRow.test.tsx).
91
+ const { 'text-primary': T_ACTIVE, 'text-secondary': T_MUTED } = getSemanticColors('dark')
92
92
 
93
93
  it('renders exercise name via the real ExerciseCardHeading header', () => {
94
94
  render(<ExerciseCard {...expandedProps} />)
@@ -220,7 +220,7 @@ function CollapsedCard({
220
220
 
221
221
  const DARK = getSemanticColors('dark')
222
222
  /** The header↔body seam. */
223
- const BODY_DIVIDER = DARK['border-subtle']
223
+ const BODY_DIVIDER = DARK['hairline-subtle']
224
224
 
225
225
  /** Project the set rows onto the heading strip's per-set state (done / active / todo). */
226
226
  function deriveHeaderSetStates(sets: SetRowProps[]): SetStripSet[] {
@@ -262,7 +262,7 @@ function ExpandedCard({
262
262
 
263
263
  return (
264
264
  <View
265
- className="bg-surface-elevated border-border"
265
+ className="bg-surface-elevated border-hairline"
266
266
  style={{
267
267
  borderWidth: 1,
268
268
  ...borderRadius,
@@ -193,7 +193,7 @@ function TabBar({ active, onSelect }: TabBarProps) {
193
193
  className="flex-row"
194
194
  style={{
195
195
  borderBottomWidth: 1,
196
- borderBottomColor: resolveColor('border-default'),
196
+ borderBottomColor: resolveColor('hairline-default'),
197
197
  }}
198
198
  accessibilityRole={'tablist' as ViewProps['accessibilityRole']}
199
199
  testID="exercise-detail-page-tabs"
@@ -247,7 +247,7 @@ function StatStrip({ stats, unit }: { stats: ExerciseDetailStats; unit: 'lbs' |
247
247
  return (
248
248
  <View
249
249
  key={key}
250
- className="bg-surface-elevated border-border"
250
+ className="bg-surface-elevated border-hairline"
251
251
  style={{
252
252
  flex: 1,
253
253
  padding: 12,
@@ -297,7 +297,7 @@ function SectionCard({
297
297
  }) {
298
298
  return (
299
299
  <View
300
- className="bg-surface-elevated border-border"
300
+ className="bg-surface-elevated border-hairline"
301
301
  style={{
302
302
  borderWidth: 1,
303
303
  borderRadius: 12,
@@ -421,7 +421,7 @@ function VbtSummaryRow({ summary }: { summary: VbtSummary }) {
421
421
  {cells.map((cell) => (
422
422
  <View
423
423
  key={cell.label}
424
- className="bg-surface-elevated border-border"
424
+ className="bg-surface-elevated border-hairline"
425
425
  style={{
426
426
  flex: 1,
427
427
  padding: 12,
@@ -19,7 +19,7 @@ export interface InputBarProps {
19
19
  }
20
20
 
21
21
  const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
22
- const INPUT_CLASSNAME = 'bg-surface-raised border-border-strong text-text-primary'
22
+ const INPUT_CLASSNAME = 'bg-surface-raised border-hairline-strong text-text-primary'
23
23
 
24
24
  const inputStyle = {
25
25
  fontSize: 14,
@@ -55,7 +55,7 @@ export function InputBar({
55
55
  style={{
56
56
  width: '100%',
57
57
  borderTopWidth: 1,
58
- borderTopColor: resolveColor('border-default'),
58
+ borderTopColor: resolveColor('hairline-default'),
59
59
  paddingTop: 10,
60
60
  paddingHorizontal: 16,
61
61
  paddingBottom: 12,
@@ -82,7 +82,7 @@ const meta: Meta<typeof LiveAuraFrame> = {
82
82
  docs: {
83
83
  description: {
84
84
  component:
85
- 'Full-surface color-flood frame tied to a coaching category. Wraps content on a charcoal ' +
85
+ 'Full-surface color-flood frame tied to a coaching category. Wraps content on a grey ' +
86
86
  'base and layers a radial wash — amber at threshold (VL20), red at stop (VL28+, pulsing) — ' +
87
87
  'derived from the semantic warning / error tokens via `alpha()`. Children pass through. ' +
88
88
  'Rendered here at live-screen size so the flood reads (it is meant to tint a whole panel, ' +
@@ -19,7 +19,7 @@ describe('LiveAuraFrame', () => {
19
19
  expect(screen.getByText('Cable Row')).toBeInTheDocument()
20
20
  })
21
21
 
22
- it('sits on the charcoal base surface', () => {
22
+ it('sits on the grey base surface', () => {
23
23
  render(<LiveAuraFrame category="productive" />)
24
24
  expect(screen.getByTestId('live-aura-frame')).toHaveStyle({
25
25
  backgroundColor: t['background-base'],
@@ -45,7 +45,7 @@ export function liveAuraColor(category: LiveAuraCategory): string | null {
45
45
 
46
46
  /**
47
47
  * Full-surface color-flood frame tied to a coaching category. Wraps `children`
48
- * on a charcoal base and layers a radial wash (amber at threshold, red at stop)
48
+ * on a grey base and layers a radial wash (amber at threshold, red at stop)
49
49
  * as a decorative, pointer-transparent overlay. The wash uses a solid
50
50
  * `backgroundColor` fallback (native) plus a web-only `backgroundImage` radial
51
51
  * gradient; both derive from the same semantic token via `alpha()`.
@@ -62,7 +62,7 @@ export function LiveAuraFrame({
62
62
  const tint = color ? alpha(color, floodOpacity[category]) : 'transparent'
63
63
 
64
64
  // Match R2's canonical aura: a top-down radial wash that FADES TO TRANSPARENT
65
- // by ~62%, so the charcoal base shows through the lower panel — NOT a flat
65
+ // by ~62%, so the grey base shows through the lower panel — NOT a flat
66
66
  // full-surface tint. On web (the target: MCP dashboard + Storybook) render the
67
67
  // gradient only; native has no gradient support, so fall back to a solid tint.
68
68
  const floodStyle = {
@@ -84,7 +84,7 @@ export function LiveAuraFrame({
84
84
  borderRadius: 10,
85
85
  backgroundColor: t['background-base'],
86
86
  borderWidth: 1,
87
- borderColor: t['border-default'],
87
+ borderColor: t['hairline-default'],
88
88
  },
89
89
  style,
90
90
  ]}
@@ -10,10 +10,12 @@ import {
10
10
  MESO_ACCENT_GRADIENT_LIGHT,
11
11
  } from '../../../theme/extracted-colors-dataviz'
12
12
 
13
+ const HAIRLINE_DEFAULT = getSemanticColors('dark')['hairline-default']
14
+
13
15
  const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
14
16
  const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
15
17
  const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
16
- const BORDER_DEFAULT = '#1F1F1F'
18
+ const BORDER_DEFAULT = HAIRLINE_DEFAULT
17
19
 
18
20
  /** Gradient stops for the 3px top accent: dark -> primary -> light. */
19
21
  const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
@@ -1,6 +1,7 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, Text, Pressable, type ViewProps } from 'react-native'
3
3
  import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
4
5
 
5
6
  const t = getSemanticColors('dark')
6
7
 
@@ -15,7 +16,7 @@ export interface MuscleGroupChipProps extends ViewProps {
15
16
  }
16
17
 
17
18
  const dotColorMap: Record<VolumeStatus, string> = {
18
- untrained: '#2C2C2C',
19
+ untrained: greyRamp[900],
19
20
  behind: t['brand-secondary'],
20
21
  ontrack: t['status-success'],
21
22
  target: t['brand-primary'],
@@ -31,13 +32,13 @@ export function MuscleGroupChip({
31
32
  }: MuscleGroupChipProps) {
32
33
  const dotColor = volumeStatus
33
34
  ? dotColorMap[volumeStatus]
34
- : '#2C2C2C'
35
+ : greyRamp[900]
35
36
 
36
37
  const statusLabel = volumeStatus ?? 'no status'
37
38
 
38
39
  const content = (
39
40
  <View
40
- className={['bg-surface-raised border-border', className].filter(Boolean).join(' ')}
41
+ className={['bg-surface-raised border-hairline', className].filter(Boolean).join(' ')}
41
42
  style={{
42
43
  flexDirection: 'row',
43
44
  alignItems: 'center',
@@ -10,7 +10,7 @@ const RECENT_BORDER = 'rgba(249,180,21,0.3)'
10
10
  const RECENT_GLOW = '0 0 12px rgba(249,180,21,0.06)'
11
11
  // Native-safe fallback for the conditional row border (recent uses a computed
12
12
  // rgba, so className can't express both branches).
13
- const BORDER_DEFAULT = resolveColor('border-default')
13
+ const BORDER_DEFAULT = resolveColor('hairline-default')
14
14
 
15
15
  export type PrRecordType = 'e1rm' | 'weight' | 'reps' | 'volume' | 'velocity'
16
16
 
@@ -172,7 +172,7 @@ export function PrHistoryModal({
172
172
  >
173
173
  <View
174
174
  accessibilityElementsHidden
175
- className="bg-border"
175
+ className="bg-hairline"
176
176
  style={{
177
177
  alignSelf: 'center',
178
178
  width: 40,
@@ -388,7 +388,7 @@ export function ProgramPlanningPage({
388
388
  )}
389
389
 
390
390
  <View
391
- className="bg-surface-elevated border-border"
391
+ className="bg-surface-elevated border-hairline"
392
392
  style={{
393
393
  borderWidth: 1,
394
394
  borderRadius: 12,
@@ -116,7 +116,7 @@ function EmojiSlider({ factor }: EmojiSliderProps) {
116
116
  accessibilityLabel={`${factor.label} level ${level} of 5`}
117
117
  aria-checked={selected}
118
118
  testID="readiness-check-emoji"
119
- className={selected ? undefined : 'border-border bg-surface-raised'}
119
+ className={selected ? undefined : 'border-hairline bg-surface-raised'}
120
120
  style={({ pressed }) => ({
121
121
  flex: 1,
122
122
  alignItems: 'center',
@@ -172,7 +172,7 @@ function WarmUpCard({ validation }: { validation: WarmUpValidation }) {
172
172
  const badge = WARMUP_BADGE[validation.status]
173
173
  return (
174
174
  <View
175
- className="border-border bg-surface-raised"
175
+ className="border-hairline bg-surface-raised"
176
176
  style={{
177
177
  marginTop: 16,
178
178
  padding: 12,
@@ -188,7 +188,7 @@ export function RestTimer({
188
188
  style={{
189
189
  width: '100%',
190
190
  borderTopWidth: 1,
191
- borderTopColor: resolveColor('border-default'),
191
+ borderTopColor: resolveColor('hairline-default'),
192
192
  paddingVertical: 12,
193
193
  paddingHorizontal: 16,
194
194
  }}
@@ -253,7 +253,7 @@ export function RestTimer({
253
253
 
254
254
  {/* Progress bar */}
255
255
  <View
256
- className="bg-border"
256
+ className="bg-hairline"
257
257
  style={{
258
258
  height: 3,
259
259
  borderRadius: 2,
@@ -9,11 +9,11 @@
9
9
  */
10
10
  import type { Meta, StoryObj } from '@storybook/react-vite'
11
11
  import { Fragment } from 'react'
12
- import { primitiveColors, primitiveRamps as ramp } from '../../../theme/tokens/primitives'
12
+ import { primitiveRamps as ramp, greyRamp } from '../../../theme/tokens/primitives'
13
13
  import { GREY, RAIL_W, SET_GAP, T_PRIMARY, T_SECONDARY, T_TERTIARY, INSET, velColor, reps, Page, sectionTitle } from './setHeadingKit'
14
14
 
15
15
  // ---- variable-zone (floor→max) upcoming color — real ramp candidates (no invented hex)
16
- const VAR_GREY = primitiveColors.charcoal[200] // #3C3C3C — lighter charcoal (the paler-grey reference)
16
+ const VAR_GREY = greyRamp[800] // #3C3C3C — lighter grey (the paler-grey reference)
17
17
  const VAR_BLUE_800 = ramp.blue[800] // #14407E — clearly blue, a touch brighter
18
18
  const VAR_BLUE_900 = ramp.blue[900] // #112C5A — dark muted navy, recessive
19
19
  const VAR_CYAN_900 = ramp.cyan[900] // #0B3149 — teal alt, sidesteps info-blue adjacency
@@ -115,7 +115,7 @@ function Sheet() {
115
115
  {/* 1. variable rep-range — real ramp candidates for the variable zone */}
116
116
  <Col label="1 · variable rep-range — LOCKED: cyan 900">
117
117
  {([
118
- ['grey · charcoal 200', VAR_GREY],
118
+ ['grey · grey 200', VAR_GREY],
119
119
  ['blue 800 · #14407E', VAR_BLUE_800],
120
120
  ['blue 900 · #112C5A', VAR_BLUE_900],
121
121
  ['cyan 900 · #0B3149', VAR_CYAN_900],
@@ -9,7 +9,7 @@ import { SessionHeader, type SessionHeaderPlanEntry } from './SessionHeader'
9
9
  * bar (fill ∝ sets done, coloured against the elapsed-vs-budget pace with a live marker),
10
10
  * a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming, the tiles become
11
11
  * Date/Time/Until, the bar is the empty plan shape, and the readout shows the planned
12
- * duration. Rendered on the raised charcoal plane.
12
+ * duration. Rendered on the raised grey plane.
13
13
  */
14
14
  const meta: Meta<typeof SessionHeader> = {
15
15
  title: 'Shell/SessionRail/SessionHeader',
@@ -20,7 +20,7 @@ const meta: Meta<typeof SessionHeader> = {
20
20
  description: {
21
21
  component:
22
22
  '**Molecule** (shell S3). The session-rail heading — title, stat tiles, chunked ' +
23
- 'pace bar, sets label + ⏱ readout, on the raised charcoal plane. Composes ' +
23
+ 'pace bar, sets label + ⏱ readout, on the raised grey plane. Composes ' +
24
24
  '[MetricTiles](?path=/docs/workout-metrictiles--docs) / ' +
25
25
  '[ScheduleTiles](?path=/docs/workout-scheduletiles--docs) + ' +
26
26
  '[SegmentedProgressBar](?path=/docs/workout-segmentedprogressbar--docs) + ' +
@@ -51,7 +51,7 @@ export interface SessionHeaderProps extends ViewProps {
51
51
  * done, coloured green/amber against the elapsed-vs-budget pace, with a live marker),
52
52
  * a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming (`next` set), the tiles
53
53
  * become Date/Time/Until, the bar is the empty plan shape, and the readout shows the
54
- * planned duration. Rendered on the raised charcoal plane; presentational. Composes
54
+ * planned duration. Rendered on the raised grey plane; presentational. Composes
55
55
  * {@link MetricTiles} / {@link ScheduleTiles} + {@link SegmentedProgressBar} +
56
56
  * {@link TimerReadout}.
57
57
  */