@titan-design/react-ui 0.11.0 → 0.12.1

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 (152) 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 +27 -22
  6. package/dist/index.d.ts +27 -22
  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 +250 -1157
  16. package/dist/theme/index.d.ts +250 -1157
  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/dist/theme/tokens.d.mts +904 -0
  26. package/dist/theme/tokens.d.ts +904 -0
  27. package/dist/theme/tokens.js +856 -0
  28. package/dist/theme/tokens.js.map +1 -0
  29. package/dist/theme/tokens.mjs +833 -0
  30. package/dist/theme/tokens.mjs.map +1 -0
  31. package/package.json +7 -1
  32. package/src/arch/Architecture.stories.tsx +9 -9
  33. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  34. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +3 -3
  35. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  36. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +3 -3
  37. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +1 -1
  38. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +2 -2
  39. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  40. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +4 -4
  41. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +3 -3
  42. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  43. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -12
  44. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  45. package/src/components/custom/Table/Table.tsx +2 -2
  46. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  47. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  48. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  49. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  50. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  51. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  52. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +3 -3
  54. package/src/components/custom/Workout/ExerciseCard.tsx +2 -2
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -4
  56. package/src/components/custom/Workout/InputBar.tsx +2 -2
  57. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  58. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  59. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  60. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  61. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  62. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  63. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  64. package/src/components/custom/Workout/README.md +1 -1
  65. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  66. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  67. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  68. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  69. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  70. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  71. package/src/components/custom/Workout/SessionRail.tsx +13 -5
  72. package/src/components/custom/Workout/SetBar.tsx +3 -3
  73. package/src/components/custom/Workout/SetRow.test.tsx +6 -3
  74. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  75. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  76. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  77. package/src/components/custom/Workout/VelocityStrip.test.tsx +5 -2
  78. package/src/components/custom/Workout/VelocityStrip.tsx +4 -4
  79. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  80. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  81. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  82. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  83. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  84. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  85. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  86. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  87. package/src/components/custom/charts/SetBarChart.tsx +3 -3
  88. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  89. package/src/components/custom/stepper/Stepper.tsx +3 -3
  90. package/src/components/shell/DeviceMenu.tsx +2 -2
  91. package/src/components/shell/README.md +1 -1
  92. package/src/components/shell/SessionRailSpecimen.stories.tsx +1 -1
  93. package/src/components/shell/SideNav.tsx +1 -1
  94. package/src/components/shell/TopBar.tsx +1 -1
  95. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  96. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  97. package/src/components/ui/avatar/Avatar.tsx +2 -2
  98. package/src/components/ui/badge/Badge.tsx +2 -2
  99. package/src/components/ui/card/Card.tsx +3 -3
  100. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  101. package/src/components/ui/collapse/Collapse.tsx +1 -1
  102. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  103. package/src/components/ui/drawer/Drawer.tsx +3 -3
  104. package/src/components/ui/form-field/FormField.tsx +1 -1
  105. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  106. package/src/components/ui/menu/Menu.tsx +2 -2
  107. package/src/components/ui/pill/Pill.tsx +2 -2
  108. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  109. package/src/components/ui/popover/Popover.tsx +1 -1
  110. package/src/components/ui/progress/Progress.tsx +3 -3
  111. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  112. package/src/components/ui/radio/Radio.tsx +4 -4
  113. package/src/components/ui/select/Select.tsx +3 -3
  114. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  115. package/src/components/ui/surface/Surface.test.tsx +32 -19
  116. package/src/components/ui/surface/Surface.tsx +3 -3
  117. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  118. package/src/components/ui/surface/surface.contract.test.ts +58 -45
  119. package/src/components/ui/switch/Switch.tsx +1 -1
  120. package/src/components/ui/tabs/Tabs.tsx +2 -2
  121. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  122. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  123. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  124. package/src/stories/PresetShowcase.stories.tsx +21 -21
  125. package/src/theme/ColorPalettes.stories.tsx +41 -38
  126. package/src/theme/ColorPrimitives.stories.tsx +89 -41
  127. package/src/theme/Depth.stories.tsx +312 -0
  128. package/src/theme/DepthCalibration.stories.tsx +655 -0
  129. package/src/theme/ThemeProvider.tsx +1 -1
  130. package/src/theme/barrel.ts +7 -14
  131. package/src/theme/color-stories.coverage.test.ts +15 -20
  132. package/src/theme/color-story-kit.tsx +1 -1
  133. package/src/theme/color-utils.ts +197 -0
  134. package/src/theme/config.ts +0 -8
  135. package/src/theme/depth-calibration.test.tsx +97 -0
  136. package/src/theme/elevation.test.ts +62 -1
  137. package/src/theme/elevation.ts +34 -8
  138. package/src/theme/global.css +47 -53
  139. package/src/theme/grey-ramp.test.ts +166 -0
  140. package/src/theme/materials.test.ts +106 -0
  141. package/src/theme/materials.ts +211 -0
  142. package/src/theme/presets/audiobook.ts +4 -4
  143. package/src/theme/presets/types.ts +3 -3
  144. package/src/theme/semantic-contrast.test.ts +83 -0
  145. package/src/theme/token-export-guard.test.ts +104 -0
  146. package/src/theme/tokens/primitives.ts +63 -116
  147. package/src/theme/tokens/semantic.ts +130 -82
  148. package/src/utils/colors.ts +1 -1
  149. package/tailwind.config.js +2 -5
  150. package/src/theme/bar-paper.ts +0 -59
  151. package/src/theme/shadows.stories.tsx +0 -523
  152. package/src/theme/shadows.ts +0 -439
@@ -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,
@@ -105,7 +105,7 @@ type props without pulling the dependency.
105
105
  `SetStrip`/`SetBar` colors are the real titan ramp pins (`primitiveRamps` red-600 /
106
106
  orange-400 / amber-300 / green-300); the rail surfaces bind to the charcoal ramp +
107
107
  a subtle neumorphic inset (`neumorphicShadows.charcoal.pressed.subtle`). The heading
108
- design is locked in `coordination/.../S3-sessionrail/DECISIONS-ExerciseRow.md`; the
108
+ design is locked in `sources/design/shell/S3-sessionrail/DECISIONS-ExerciseRow.md`; the
109
109
  exploration specimens live under `Custom/Workout/Explorations/*` (do not repoint yet).
110
110
  In Storybook the family nests by composition under **`Shell/SessionRail/…`**
111
111
  (organism → `ExerciseCardHeading` → `ExerciseHeading` → its atoms/molecules →
@@ -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
  */
@@ -10,7 +10,7 @@ const t = getSemanticColors('dark')
10
10
  * `SessionRail` (shell organism) — the live-workout exercise list: a flat raised
11
11
  * `SessionHeader` glance (title, stat tiles, chunked pace bar) over a sunk, inset list
12
12
  * of ExerciseCard rail headings. The header plan (chunk widths ∝ sets) is derived from
13
- * `exercises`. Surfaces bind to the charcoal ramp; the list depth is a subtle neumorphic
13
+ * `exercises`. Surfaces bind to the grey ramp; the list depth is a subtle neumorphic
14
14
  * inset. Driven entirely by props.
15
15
  */
16
16
  const meta: Meta<typeof SessionRail> = {
@@ -37,7 +37,7 @@ const meta: Meta<typeof SessionRail> = {
37
37
  description: {
38
38
  component:
39
39
  '**Organism** (shell S3). The live-workout exercise list: raised header glance over ' +
40
- 'a sunk inset list, subtle neumorphic depth (charcoal ramp + `neumorphicShadows`). ' +
40
+ 'a sunk inset list, subtle neumorphic depth (grey ramp + `neumorphicShadows`). ' +
41
41
  'Composes ' +
42
42
  '[SessionHeader](?path=/docs/shell-sessionrail-sessionheader--docs) + ' +
43
43
  '[ExerciseCardHeading](?path=/docs/workout-exercisecardheading--docs) × N. ' +
@@ -1,7 +1,8 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, type ViewProps } from 'react-native'
3
- import { primitiveColors } from '../../../theme/tokens/primitives'
4
- import { neumorphicShadows } from '../../../theme/shadows'
3
+ import { greyRamp } from '../../../theme/tokens/primitives'
4
+ import { insetWell } from '../../../theme/materials'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
6
  import { Surface } from '../../ui/surface'
6
7
  import { ExerciseCardHeading } from './ExerciseCardHeading'
7
8
  import { ExerciseCard } from './ExerciseCard'
@@ -17,7 +18,9 @@ import type { ExerciseIndicatorKind } from './ExerciseIndicator'
17
18
  // recessed via its inner shadow yet paler than the header, so the transparent
18
19
  // exercise headings on it never blend into the header plane. Surface owns both backgrounds,
19
20
  // so the rail no longer hand-sets them.
20
- const DIVIDER = primitiveColors.charcoal[300] // #2C2C2C — row divider (raised to read on surface-elevated #2A2827)
21
+ const SEMANTIC = getSemanticColors('dark')
22
+ /** Row divider — one step up from the well it sits in so the line reads. */
23
+ const DIVIDER = greyRamp[875]
21
24
 
22
25
  const DEFAULT_WIDTH = 246
23
26
 
@@ -124,9 +127,14 @@ export function SessionRail({
124
127
  next={next}
125
128
  />
126
129
 
130
+ {/* The list is a WELL cut into the rail: one plane down from the rail's
131
+ `elevated`, recessed by the shared inset material rather than by a
132
+ neumorphic pressed shadow (which had no room to read at this
133
+ lightness). `level` follows the well's tone so on-surface text
134
+ resolves against what is actually painted. */}
127
135
  <Surface
128
- level="elevated"
129
- style={[{ flex: 1 }, neumorphicShadows.charcoal.pressed.subtle]}
136
+ level="base"
137
+ style={[{ flex: 1 }, insetWell(SEMANTIC['surface-base'])]}
130
138
  testID="session-rail-list"
131
139
  >
132
140
  {exercises.map((ex, i) => (
@@ -1,5 +1,5 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
- import { primitiveColors, primitiveRamps } from '../../../theme/tokens/primitives'
2
+ import { primitiveRamps, greyRamp } from '../../../theme/tokens/primitives'
3
3
  import { SegmentedBar, type SegmentedBarSegment } from './SegmentedBar'
4
4
 
5
5
  /**
@@ -13,8 +13,8 @@ export const SET_STRIP_ZONES = {
13
13
  fastest: primitiveRamps.green[300],
14
14
  } as const
15
15
 
16
- /** Grey fill for planned-but-unperformed reps / upcoming sets (charcoal placeholder). */
17
- const TODO_COLOR = primitiveColors.charcoal[300]
16
+ /** Grey fill for planned-but-unperformed reps / upcoming sets (grey placeholder). */
17
+ const TODO_COLOR = greyRamp[900]
18
18
 
19
19
  /**
20
20
  * The learned "variable / unknown / opportunity" semantic (cyan-900, a real ramp
@@ -3,10 +3,13 @@ import { render, screen } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { View } from 'react-native'
5
5
  import { SetRow, type SetRowProps } from './SetRow'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
6
7
 
7
- // Literal-hex text treatments (dark theme neutral 100 / 400).
8
- const T_ACTIVE = '#F3F4F6'
9
- const T_MUTED = '#9CA3AF'
8
+ // Text treatments, read from the tokens rather than pinned as hexes. These
9
+ // used to be hand-copied literals and silently desynced when the greys moved
10
+ // to the warm ramp (TD-07.14). The assertion still bites — it checks the
11
+ // component reaches for the right ROLE — it just no longer restates the value.
12
+ const { 'text-primary': T_ACTIVE, 'text-secondary': T_MUTED } = getSemanticColors('dark')
10
13
 
11
14
  const doneRow: SetRowProps = {
12
15
  state: 'done',
@@ -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, 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
 
@@ -28,7 +29,7 @@ const solidVariantColors: Record<string, string> = {
28
29
  success: t['status-success'],
29
30
  warning: t['status-warning'],
30
31
  error: t['status-error'],
31
- neutral: '#6B7280',
32
+ neutral: greyRamp[500],
32
33
  }
33
34
 
34
35
  const ringVariantStyles: Record<string, Record<string, unknown>> = {
@@ -71,10 +72,10 @@ const iconColors: Record<StatusDotVariant, string> = {
71
72
  success: '#0A5C52',
72
73
  warning: '#6B4000',
73
74
  error: '#5C1A1A',
74
- neutral: '#D1D5DB',
75
+ neutral: greyRamp[200],
75
76
  'on-track': t['status-success'],
76
77
  deviation: t['status-warning'],
77
- future: '#6B7280',
78
+ future: greyRamp[500],
78
79
  }
79
80
 
80
81
  const sizeDimensions = {
@@ -146,7 +147,7 @@ export function StatusDot({
146
147
  >
147
148
  <View accessibilityElementsHidden>{dot}</View>
148
149
  <Text
149
- style={{ fontSize: 12, lineHeight: 16, color: '#9CA3AF' }}
150
+ style={{ fontSize: 12, lineHeight: 16, color: greyRamp[400] }}
150
151
  accessibilityElementsHidden
151
152
  >
152
153
  {label}
@@ -477,7 +477,7 @@ export function StrengthTrendChart({
477
477
  <View
478
478
  testID="strength-trend-chart-tooltip"
479
479
  accessibilityElementsHidden
480
- className="bg-surface-elevated border-border-strong"
480
+ className="bg-surface-elevated border-hairline-strong"
481
481
  style={{
482
482
  position: 'absolute',
483
483
  left: Math.max(
@@ -114,7 +114,7 @@ function SummaryCards({ summary }: { summary: TrainingStatusSummary }) {
114
114
  return (
115
115
  <View
116
116
  key={key}
117
- className="bg-surface-elevated border-border"
117
+ className="bg-surface-elevated border-hairline"
118
118
  style={{
119
119
  flexGrow: 1,
120
120
  flexBasis: '46%',
@@ -265,7 +265,7 @@ export function TrainingStatusPage({
265
265
  <SummaryCards summary={summary} />
266
266
 
267
267
  <View
268
- className="bg-surface-elevated border-border"
268
+ className="bg-surface-elevated border-hairline"
269
269
  style={{
270
270
  borderWidth: 1,
271
271
  borderRadius: 12,
@@ -414,8 +414,11 @@ describe('VelocityStrip all-zero velocities (NaN guard)', () => {
414
414
 
415
415
  // Literal-hex slot colors (must match the component's tokens exactly).
416
416
  // TODO_SOLID = the LANDED surface-relative solid to-do tone (base/dark plane blended toward the
417
- // on-surface neutral) — replaced the old fixed charcoal #2C2C2C. Solid fill, not a dashed outline.
418
- const TODO_SOLID = '#4C4E55'
417
+ // on-surface neutral) — replaced the old fixed grey #2C2C2C. Solid fill, not a dashed outline.
418
+ // Pinned rather than recomputed on purpose: deriving it here would just mirror
419
+ // SetBarChart's mixHex and stop catching a change to the blend itself.
420
+ // = mixHex(grey-925, text-tertiary, 0.55); moved with the warm ramp (TD-07.14).
421
+ const TODO_SOLID = '#5B5957'
419
422
  const VARIABLE_CYAN = '#0B3149'
420
423
  const CONTINUE_OUTLINE = '#22465F'
421
424
  describe('VelocityStrip set-type modes (compact)', () => {
@@ -2,7 +2,7 @@
2
2
  import { useEffect, useState } from 'react'
3
3
  import { View, Text, Pressable, Animated, type ViewProps, type ViewStyle } from 'react-native'
4
4
  import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
5
- import { primitiveColors, sequentialEffort } from '../../../theme/tokens/primitives'
5
+ import { sequentialEffort, greyRamp } from '../../../theme/tokens/primitives'
6
6
  import { getSemanticColors } from '../../../theme/tokens/semantic'
7
7
  import { alpha } from '../../../utils/colors'
8
8
  import { formatVelocity } from '../../../utils/workout-format'
@@ -456,14 +456,14 @@ const EXPANDED_HEIGHT = 60
456
456
  /** Default `compact` (flat resting strip) height (px) — a THIN radius-2 pill row, the resting glance. */
457
457
  const COMPACT_HEIGHT = 8
458
458
 
459
- /** The dashed running-best reference line color (the lightest charcoal step). */
460
- const HERO_REFERENCE_COLOR = primitiveColors.charcoal[0]
459
+ /** The dashed running-best reference line color (the lightest grey step). */
460
+ const HERO_REFERENCE_COLOR = greyRamp[600]
461
461
 
462
462
  /**
463
463
  * A dashed running-best reference line spanning the plot width, absolutely positioned a
464
464
  * pixel `offset` from the given `anchor` edge. Shared by the single `hero` chart (anchored
465
465
  * to the baseline, `bottom`) and BOTH wings of the diverging dual chart (anchored to the
466
- * centre axis, `top`) so the three sites render one line treatment (charcoal-0, 1px dashed).
466
+ * centre axis, `top`) so the three sites render one line treatment (grey-0, 1px dashed).
467
467
  * Presentation-only + non-interactive — the numeric best lives in each chart's container
468
468
  * accessibility label, so the line itself is hidden from the a11y tree.
469
469
  *
@@ -1,7 +1,7 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, Text, type ViewProps } from 'react-native'
3
3
  import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
4
- import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
5
5
  import { ZoneTrack } from './ZoneTrack'
6
6
  import { DataRow } from '../../ui/data-row/DataRow'
7
7
  import type { VolumeLandmarks } from './muscleTaxonomy'
@@ -11,9 +11,9 @@ import type { VolumeLandmarks } from './muscleTaxonomy'
11
11
  // BodyMap / MesoProgressBar so a muscle reads identically across the app.
12
12
  const HEAT = WORKOUT_TOKENS.heatmap
13
13
 
14
- // The muted, un-reached track colour — the same charcoal step ZoneTrack defaults
14
+ // The muted, un-reached track colour — the same grey step ZoneTrack defaults
15
15
  // to, so the bar sits on the shared gauge-track surface.
16
- const NEUTRAL_TRACK = primitiveColors.charcoal[200]
16
+ const NEUTRAL_TRACK = greyRamp[800]
17
17
  const MONO = 'monospace'
18
18
 
19
19
  export type VolumeZone = 'under' | 'maintenance' | 'productive' | 'approaching' | 'over'
@@ -2,6 +2,11 @@ import { describe, it, expect, vi } from 'vitest'
2
2
  import { render, screen, fireEvent } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { WeightBadge } from './WeightBadge'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
6
+
7
+ // Read from the token, not pinned: these used to be hand-copied hexes and
8
+ // silently desynced when borders became alpha hairlines (TD-07.16).
9
+ const T = getSemanticColors('dark')
5
10
 
6
11
  describe('WeightBadge', () => {
7
12
  it('renders value and default unit', () => {
@@ -71,7 +76,7 @@ describe('WeightBadge', () => {
71
76
  it('uses default border color when not a PR', () => {
72
77
  render(<WeightBadge value={225} />)
73
78
  expect(screen.getByTestId('weight-badge')).toHaveStyle({
74
- borderTopColor: '#1F1F1F',
79
+ borderTopColor: T['hairline-default'],
75
80
  })
76
81
  })
77
82