@titan-design/react-ui 0.10.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 (190) 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 +416 -18
  6. package/dist/index.d.ts +416 -18
  7. package/dist/index.js +2512 -1307
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2480 -1299
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1644 -1575
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1645 -1576
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +409 -415
  20. package/dist/theme/index.d.ts +409 -415
  21. package/dist/theme/index.js +234 -408
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +227 -399
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +141 -140
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +141 -140
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/arch/Architecture.stories.tsx +9 -9
  31. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  32. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  33. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  34. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  35. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  36. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  37. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  38. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  39. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  40. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  41. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  42. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  43. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  45. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  46. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  47. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  48. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  49. package/src/components/custom/Fatigue/README.md +93 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  51. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  52. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  53. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  54. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  55. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  56. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  57. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  58. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  59. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  60. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  61. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  62. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  63. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  64. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  65. package/src/components/custom/Fatigue/index.ts +41 -0
  66. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  67. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  68. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  69. package/src/components/custom/Table/Table.tsx +2 -2
  70. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  71. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  72. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  73. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  74. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  75. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  76. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  77. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  78. package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
  79. package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
  80. package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
  81. package/src/components/custom/Workout/InputBar.tsx +2 -2
  82. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  83. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  84. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  85. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  86. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  87. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  88. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  89. package/src/components/custom/Workout/README.md +62 -16
  90. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  91. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  92. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  93. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  94. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  95. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  96. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  97. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  98. package/src/components/custom/Workout/SessionRail.tsx +63 -18
  99. package/src/components/custom/Workout/SetBar.tsx +3 -3
  100. package/src/components/custom/Workout/SetRow.test.tsx +12 -9
  101. package/src/components/custom/Workout/SetRow.tsx +9 -2
  102. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  103. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  104. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  105. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  106. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  107. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  108. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  109. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  110. package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
  111. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  112. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  113. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  114. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  115. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  116. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  117. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  118. package/src/components/custom/Workout/index.ts +3 -0
  119. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  120. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  121. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  122. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  123. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  124. package/src/components/custom/index.ts +1 -0
  125. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  126. package/src/components/custom/stepper/Stepper.tsx +3 -3
  127. package/src/components/shell/DeviceMenu.tsx +2 -2
  128. package/src/components/shell/SideNav.tsx +1 -1
  129. package/src/components/shell/TopBar.tsx +1 -1
  130. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  131. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  132. package/src/components/ui/avatar/Avatar.tsx +2 -2
  133. package/src/components/ui/badge/Badge.tsx +2 -2
  134. package/src/components/ui/card/Card.tsx +3 -3
  135. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  136. package/src/components/ui/collapse/Collapse.tsx +1 -1
  137. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  138. package/src/components/ui/drawer/Drawer.tsx +3 -3
  139. package/src/components/ui/form-field/FormField.tsx +1 -1
  140. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  141. package/src/components/ui/menu/Menu.tsx +2 -2
  142. package/src/components/ui/pill/Pill.tsx +2 -2
  143. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  144. package/src/components/ui/popover/Popover.tsx +1 -1
  145. package/src/components/ui/progress/Progress.tsx +3 -3
  146. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  147. package/src/components/ui/radio/Radio.tsx +4 -4
  148. package/src/components/ui/select/Select.tsx +3 -3
  149. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  150. package/src/components/ui/surface/Surface.test.tsx +32 -19
  151. package/src/components/ui/surface/Surface.tsx +3 -3
  152. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  153. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  154. package/src/components/ui/switch/Switch.tsx +1 -1
  155. package/src/components/ui/tabs/Tabs.tsx +2 -2
  156. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  157. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  158. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  159. package/src/stories/PresetShowcase.stories.tsx +21 -21
  160. package/src/test/raw-color-patterns.test.ts +71 -0
  161. package/src/theme/ColorPalettes.stories.tsx +874 -0
  162. package/src/theme/ColorPrimitives.stories.tsx +348 -0
  163. package/src/theme/Depth.stories.tsx +312 -0
  164. package/src/theme/DepthCalibration.stories.tsx +655 -0
  165. package/src/theme/ThemeProvider.tsx +1 -1
  166. package/src/theme/barrel.ts +7 -14
  167. package/src/theme/color-stories.coverage.test.ts +125 -0
  168. package/src/theme/color-story-kit.tsx +170 -0
  169. package/src/theme/color-utils.ts +197 -0
  170. package/src/theme/config.ts +0 -8
  171. package/src/theme/depth-calibration.test.tsx +97 -0
  172. package/src/theme/elevation.test.ts +62 -1
  173. package/src/theme/elevation.ts +34 -8
  174. package/src/theme/global.css +47 -53
  175. package/src/theme/grey-ramp.test.ts +166 -0
  176. package/src/theme/materials.test.ts +106 -0
  177. package/src/theme/materials.ts +211 -0
  178. package/src/theme/presets/audiobook.ts +4 -4
  179. package/src/theme/presets/types.ts +3 -3
  180. package/src/theme/semantic-contrast.test.ts +83 -0
  181. package/src/theme/tokens/primitives.ts +62 -115
  182. package/src/theme/tokens/semantic.ts +130 -82
  183. package/src/utils/colors.ts +1 -1
  184. package/tailwind.config.js +2 -5
  185. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  186. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  187. package/src/theme/ColorSystem.stories.tsx +0 -508
  188. package/src/theme/Colors.stories.tsx +0 -323
  189. package/src/theme/shadows.stories.tsx +0 -523
  190. package/src/theme/shadows.ts +0 -439
@@ -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',
@@ -49,7 +50,7 @@ describe('ExerciseCard', () => {
49
50
  expect(screen.getByTestId('pr-badge-star')).toBeInTheDocument()
50
51
  })
51
52
 
52
- it('renders mini velocity strips for logged sets', () => {
53
+ it('renders compact velocity strips for logged sets', () => {
53
54
  render(
54
55
  <ExerciseCard
55
56
  {...baseCollapsedProps}
@@ -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} />)
@@ -158,10 +158,10 @@ describe('ExerciseCard', () => {
158
158
  expect(within(rows[2]).getByText('3')).toHaveStyle({ color: T_MUTED })
159
159
  })
160
160
 
161
- it('uses the compact velocity-height spotlight for the live set and mini elsewhere', () => {
161
+ it('uses the velocity-height spotlight for the live set and the flat compact strip elsewhere', () => {
162
162
  render(<ExerciseCard {...expandedProps} />)
163
- expect(screen.getAllByTestId('velocity-strip-compact')).toHaveLength(1)
164
- expect(screen.getAllByTestId('velocity-strip-mini')).toHaveLength(2)
163
+ expect(screen.getAllByTestId('velocity-strip-spotlight')).toHaveLength(1)
164
+ expect(screen.getAllByTestId('velocity-strip-compact')).toHaveLength(2)
165
165
  })
166
166
 
167
167
  it('renders the TempoDisplay in the header when tempo provided', () => {
@@ -194,7 +194,9 @@ function CollapsedCard({
194
194
  key={i}
195
195
  velocities={velocities}
196
196
  zones={velocityZones}
197
- variant="mini"
197
+ variant="compact"
198
+ height={8}
199
+ hideBaseline
198
200
  testID={`exercise-card-velocity-strip-${i}`}
199
201
  />
200
202
  ))}
@@ -218,7 +220,7 @@ function CollapsedCard({
218
220
 
219
221
  const DARK = getSemanticColors('dark')
220
222
  /** The header↔body seam. */
221
- const BODY_DIVIDER = DARK['border-subtle']
223
+ const BODY_DIVIDER = DARK['hairline-subtle']
222
224
 
223
225
  /** Project the set rows onto the heading strip's per-set state (done / active / todo). */
224
226
  function deriveHeaderSetStates(sets: SetRowProps[]): SetStripSet[] {
@@ -260,7 +262,7 @@ function ExpandedCard({
260
262
 
261
263
  return (
262
264
  <View
263
- className="bg-surface-elevated border-border"
265
+ className="bg-surface-elevated border-hairline"
264
266
  style={{
265
267
  borderWidth: 1,
266
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,
@@ -386,7 +386,13 @@ function VbtBreakdown({
386
386
  {set.label}
387
387
  </Text>
388
388
  <View className="flex-1" style={{ height: 8, justifyContent: 'center' }}>
389
- <VelocityStrip velocities={set.velocities} zones={zones} variant="mini" />
389
+ <VelocityStrip
390
+ velocities={set.velocities}
391
+ zones={zones}
392
+ variant="compact"
393
+ height={8}
394
+ hideBaseline
395
+ />
390
396
  </View>
391
397
  <Text
392
398
  className="text-text-tertiary"
@@ -415,7 +421,7 @@ function VbtSummaryRow({ summary }: { summary: VbtSummary }) {
415
421
  {cells.map((cell) => (
416
422
  <View
417
423
  key={cell.label}
418
- className="bg-surface-elevated border-border"
424
+ className="bg-surface-elevated border-hairline"
419
425
  style={{
420
426
  flex: 1,
421
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,
@@ -13,8 +13,8 @@ DeviationBar · IntensityBar · WorkoutPill · MuscleGroupChip · Sparkline ·
13
13
  SupersetWrapper · InputBar · MetricCell · SetsRepsLoad · ExerciseIndicator · SetBar
14
14
 
15
15
  **Molecules** — compose atoms, own a little local state:
16
- VelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar · WeekRow ·
17
- WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
16
+ VelocityStrip · DualVelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar ·
17
+ WeekRow · WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
18
18
 
19
19
  **Organisms** — full features, often with their own data contract:
20
20
  ExerciseCard · SessionRail · MesoCard · MesoStatusCard · PrHistoryModal ·
@@ -66,11 +66,12 @@ type props without pulling the dependency.
66
66
  needs the same yellow/green/red "time-to-target" tone (a coach card, a rep-tempo
67
67
  summary). It is pure logic, trivially portable.
68
68
  3. **`LiveTempoRow` → a `TempoLiveRow` molecule** — only if a consumer wants the running
69
- row *without* the chip chrome (label/background/padding). Until then the chrome and the
69
+ row _without_ the chip chrome (label/background/padding). Until then the chrome and the
70
70
  row belong together as one component with a `live` prop, not two.
71
71
 
72
72
  Not worth splitting today (speculative extraction the workflow warns against); this note
73
73
  is the trigger list for when it stops being speculative.
74
+
74
75
  - **S3 session-rail family** — `SessionRail` (organism) composes the standalone
75
76
  `ExerciseCardHeading` molecule per exercise, which composes an `ExerciseHeading`
76
77
  info block + a `SetStrip`:
@@ -96,7 +97,7 @@ type props without pulling the dependency.
96
97
  vs. variable-todo cyan), `drop` (one set, sub-loads split by 2px notches), `myo`
97
98
  (done rest-pause — activation + clusters split by 3px cluster gaps), and
98
99
  `myo-upcoming` (planned myo, length unknown — grey activation + a fading, right-open
99
- cyan "clusters-to-failure" trail). The chunk-size *pattern* carries set-type identity:
100
+ cyan "clusters-to-failure" trail). The chunk-size _pattern_ carries set-type identity:
100
101
  **butted reps (0) < notch (2px) < cluster gap (3px) < set gap (5px)**. `cyan-900`
101
102
  (`SET_STRIP_VARIABLE_COLOR`) is the shared "variable / unknown / opportunity" pin.
102
103
  `SegmentedBar` carries these via additive `leadingGap` (per-segment left margin) and
@@ -113,6 +114,14 @@ type props without pulling the dependency.
113
114
  **Composes** link down the tree — matching the S1/S2 shell families. (The component
114
115
  files stay flat on disk in `custom/Workout/`; only the story `title`s build the tree.)
115
116
 
117
+ - **Dual at the RAIL level — rejected, removed** — `DualSessionRail` (two
118
+ `SessionRail` columns side by side) was explored and rejected: bilateral
119
+ asymmetry is a property of how a SET was performed, not of the session's
120
+ structure, so the rail stays single and consistent and the dual distinction is
121
+ surfaced per-set/per-rep instead (`DualVelocityStrip`). Deleted 2026-07-26 —
122
+ see `packages/ui/REJECTED.md` for the full reasoning. Do not reintroduce a
123
+ rail-level split without reading it first.
124
+
116
125
  - **VelocityStrip set-type modes (`set` prop)** — beyond the flat `velocities`
117
126
  array (unchanged, still the source of truth for `SetRow` / `ExerciseCard`), the
118
127
  strip accepts an optional structured `VelocitySet` descriptor and renders the
@@ -133,7 +142,7 @@ type props without pulling the dependency.
133
142
  active-set spotlight (velocity-height done reps + short grey stubs) and renders
134
143
  the advanced types as a short mini-style encoding. Every modality is documented
135
144
  with a copy-paste `set` config in **`Workout/DataViz/VelocityStrip/Set
136
- Modalities`** (each card carries a `Collapse` accordion; promoted from the
145
+ Modalities`** (each card carries a `Collapse` accordion; promoted from the
137
146
  now-deleted `S3SetModalities` Lab specimen).
138
147
  - **VelocityStrip `hero` variant** — the across-the-room, single-set **wall**
139
148
  treatment (the north-star live page's velocity hero). Tall bars (default 220px
@@ -147,12 +156,49 @@ type props without pulling the dependency.
147
156
  left-packed) with a caller-set fixed height; the eyebrow/section title is
148
157
  organism chrome, not part of the primitive. **Live-update model:** the plan's
149
158
  slots are pre-allocated (`max(done, target)` columns), so a landing rep converts
150
- placeholder→bar in the *same* slot with a pop — no reflow within the plan; pair
159
+ placeholder→bar in the _same_ slot with a pop — no reflow within the plan; pair
151
160
  with `scale="fixed"` so heights never rescale either. The one reflow case is
152
161
  set-expansion **beyond** `targetReps` (AMRAP overflow adds a column and flex-
153
162
  narrows the rest — currently snaps; smooth overflow reflow is a deferred
154
163
  follow-up). Documented by the `HeroPlayground` / `Hero*` stories on the wall
155
164
  background.
165
+ - **DualVelocityStrip** (molecule) — the two-device (LEFT + RIGHT voltra) **diverging**
166
+ wall/rail chart. `composes ↓` the `VelocityStrip` machinery in the same file
167
+ (`buildSlots`/`VelocitySlot` slot model, the `makeBarColorFor` zone scale, the hero
168
+ geometry constants, the shared `useLiveRepPop` entrance, and the extracted
169
+ `DashedReferenceLine`) rather than restating it. `used-by ↑` the north-star dual-voltra
170
+ live page (organism chrome). ONE diverging chart shares a horizontal centre axis: LEFT
171
+ reps grow **up**, RIGHT reps grow **down**, one mirrored pair per rep index, so the L/R
172
+ asymmetry reads pre-attentively as the silhouette. **Side is POSITION only, never hue** —
173
+ both wings colour reps by velocity zone through the same resolver as the single strip.
174
+ Each side takes a `DualVelocityStream` (`velocities` OR a structured `set` — the same
175
+ shapes `VelocityStrip` accepts — plus an optional `label`). The **vertical edge label is
176
+ data**: each side renders its `DualVelocityStream.label` (a slot name, e.g. "Left Arm"),
177
+ NOT a hardcoded LEFT/RIGHT — a side with no `label` (or an empty one) renders no tag, and
178
+ when neither side carries one the gutter is omitted entirely. The label keeps the prior
179
+ vertical (rotated) orientation so it never overlaps the bars. Two scales: `hero` (tall
180
+ wings, per-rep m/s velocity labels, a dashed running-best reference line per side) and
181
+ `rail` (compact — no velocity labels / reference lines, slot names in a narrow gutter).
182
+ **Rep-index alignment is the invariant:** column *i* is rep *i* on
183
+ both sides, so the set-type slot *kinds* carry through (rep / todo / variable / continue,
184
+ coloured as in the single strip) but the wide-notch chunk **gaps** (drop / myo / cluster
185
+ boundaries) are intentionally NOT rendered — per-side horizontal gaps would break the
186
+ mirrored L↔R column alignment. Single-voltra sets keep using `VelocityStrip`
187
+ `variant="hero"`. Documented by the `Playground` / `Hero*` / `Rail` stories on the wall
188
+ background (`Workout/DataViz/DualVelocityStrip`).
189
+
190
+ **Reuse audit — `DashedReferenceLine` (in-file today, top-level follow-up).** The dashed
191
+ running-best line was hand-rolled three times inside `VelocityStrip.tsx` (the single
192
+ `hero`, plus the dual's L and R wings); it is now one in-file `DashedReferenceLine`
193
+ (`anchor: 'top' | 'bottom'`, pixel `offset`, `testID`) those three sites share. A FOURTH
194
+ consumer exists across files — `Sparkline`'s `referenceLines` prop renders the same dashed
195
+ overlay (with an added opacity, an optional label, and a data-domain→Y projection), and two
196
+ Lab specimens duplicate it again. Promoting `DashedReferenceLine` to a **top-level
197
+ `ReferenceLine` overlay primitive** and migrating `Sparkline` (keeping its label/opacity
198
+ options as props) is the ≥2-consumer extraction the DoD favours — deliberately deferred to
199
+ a follow-up so the hero PR stays focused (the cross-file migration touches `Sparkline`'s
200
+ test surface and the Lab specimens, which are out of this branch's scope). Proposed API:
201
+ `<ReferenceLine anchor offset color dashed opacity? label? testID />`.
156
202
  - **RestTimer `ring` variant** — the across-the-room **wall** rest treatment (the
157
203
  north-star rest page). Built as a **three-tier decomposition** (not a one-off):
158
204
  `CircularProgress` (atom, gained a **`children`** center slot + a **`fill`**
@@ -174,7 +220,7 @@ type props without pulling the dependency.
174
220
  dashboard scale, added as the idiomatic titan `size` union (a JS number-map per
175
221
  component; **`default` values are byte-identical** to before, so existing consumers are
176
222
  untouched). **ZoneTrack** (the shared gauge primitive) gained `size` that scales track,
177
- needle, tick lines and tick labels together; its *other* consumers (TrainingLoadGauge,
223
+ needle, tick lines and tick labels together; its _other_ consumers (TrainingLoadGauge,
178
224
  RpeCalibration) default to `default` and are unaffected. **FatigueMeter** passes `size`
179
225
  through and lets `trackHeight` flow from it. `Wall*` / `WallDensity` stories on the wall
180
226
  background. (**TempoBar** was retired here — the standalone active-tempo bar is superseded
@@ -187,17 +233,17 @@ type props without pulling the dependency.
187
233
  per exercise, six distinct kinds over four tier colors (bound to titan status
188
234
  tokens, read as literal hex via `getSemanticColors('dark')`, not `resolveColor`,
189
235
  so tests can assert them). Glyph = the specific signal; color = the severity tier. Chips are
190
- outlined (border + glyph, no fill) so they never collide with the *filled*
236
+ outlined (border + glyph, no fill) so they never collide with the _filled_
191
237
  velocity strip, and static (no pulse — they are chrome). Precedence, high → low:
192
238
 
193
- | # | kind | tier | glyph (mirrors lucide) |
194
- |---|------|------|------------------------|
195
- | 1 | `imbalance` | danger (`status-error`) | `Scale` |
196
- | 2 | `overshoot` | danger (`status-error`) | `AlertTriangle` |
197
- | 3 | `velocity-loss` | warning (`status-warning`) | `TrendingDown` |
198
- | 4 | `missed-reps` | warning (`status-warning`) | `CircleSlash` |
199
- | 5 | `pr` | success (`status-success`) | `Award` |
200
- | 6 | `info` | info (`status-info`) | `Info` |
239
+ | # | kind | tier | glyph (mirrors lucide) |
240
+ | --- | --------------- | -------------------------- | ---------------------- |
241
+ | 1 | `imbalance` | danger (`status-error`) | `Scale` |
242
+ | 2 | `overshoot` | danger (`status-error`) | `AlertTriangle` |
243
+ | 3 | `velocity-loss` | warning (`status-warning`) | `TrendingDown` |
244
+ | 4 | `missed-reps` | warning (`status-warning`) | `CircleSlash` |
245
+ | 5 | `pr` | success (`status-success`) | `Award` |
246
+ | 6 | `info` | info (`status-info`) | `Info` |
201
247
 
202
248
  `resolveIndicator(candidates)` collapses the active signals to the single
203
249
  highest-precedence kind to show (`undefined` when none). Alerts outrank `pr`. The
@@ -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],
@@ -56,7 +56,7 @@ export const Weighted: Story = {
56
56
  args: {
57
57
  height: 12,
58
58
  gap: 4,
59
- radius: 6,
59
+ radius: 2,
60
60
  segments: [
61
61
  { color: '#2ED573', weight: 3 },
62
62
  { color: '#F9B415', weight: 2 },
@@ -72,7 +72,7 @@ export const PartialFills: Story = {
72
72
  args: {
73
73
  height: 12,
74
74
  gap: 4,
75
- radius: 6,
75
+ radius: 2,
76
76
  segments: [
77
77
  { color: '#2ED573', fill: 1 },
78
78
  { color: '#F9B415', fill: 0.6 },
@@ -89,7 +89,7 @@ export const PartialFills: Story = {
89
89
  export const WithMarker: Story = {
90
90
  args: {
91
91
  height: 10,
92
- radius: 5,
92
+ radius: 2,
93
93
  segments: [{ color: '#01B5D1', fill: 0.7 }],
94
94
  marker: { position: 0.5, color: '#FFFFFF' },
95
95
  },
@@ -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. ' +
@@ -76,6 +76,49 @@ describe('SessionRail', () => {
76
76
  expect(onExercisePress).toHaveBeenCalledWith(exercises[1], 1)
77
77
  })
78
78
 
79
+ describe('expandedIndex', () => {
80
+ const withSets: SessionRailExercise[] = exercises.map((ex, i) =>
81
+ i === 1
82
+ ? {
83
+ ...ex,
84
+ sets: [
85
+ {
86
+ state: 'done' as const,
87
+ setNumber: 1,
88
+ unit: 'lbs' as const,
89
+ reps: 10,
90
+ weight: 90,
91
+ velocities: [0.72, 0.66],
92
+ },
93
+ ],
94
+ }
95
+ : ex
96
+ )
97
+
98
+ it('renders the named exercise expanded in place, leaving the others collapsed', () => {
99
+ render(<SessionRail {...baseProps} exercises={withSets} expandedIndex={1} />)
100
+ // The expanded row reveals the set table; the collapsed ones do not.
101
+ expect(screen.getByText('SET')).toBeInTheDocument()
102
+ expect(screen.getByText('RPE')).toBeInTheDocument()
103
+ // Every exercise still has a row.
104
+ expect(screen.getByText('Seated Cable Row')).toBeInTheDocument()
105
+ expect(screen.getByText('Cable Chest Press')).toBeInTheDocument()
106
+ expect(screen.getByText('Face Pull')).toBeInTheDocument()
107
+ })
108
+
109
+ it('stays collapsed when the named exercise has no sets', () => {
110
+ // Guards the real case: an exercise whose sets have not loaded yet must not
111
+ // expand into an empty table.
112
+ render(<SessionRail {...baseProps} expandedIndex={1} />)
113
+ expect(screen.queryByText('SET')).not.toBeInTheDocument()
114
+ })
115
+
116
+ it('leaves every row collapsed when expandedIndex is omitted', () => {
117
+ render(<SessionRail {...baseProps} exercises={withSets} />)
118
+ expect(screen.queryByText('SET')).not.toBeInTheDocument()
119
+ })
120
+ })
121
+
79
122
  describe('accessibility', () => {
80
123
  it('has no accessibility violations', async () => {
81
124
  const { container } = render(<SessionRail {...baseProps} />)