@titan-design/react-ui 0.6.0 → 0.7.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 (248) hide show
  1. package/dist/bodymap.js +12 -7
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +12 -7
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +855 -124
  6. package/dist/index.d.ts +855 -124
  7. package/dist/index.js +2276 -801
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2230 -802
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1255 -798
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1255 -798
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +16 -82
  20. package/dist/theme/index.d.ts +16 -82
  21. package/dist/theme/index.js +13 -115
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +13 -115
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +14 -6
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +14 -6
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/arch/Architecture.stories.tsx +870 -0
  31. package/src/arch/annotations.json +48 -0
  32. package/src/arch/arch-graph.json +4301 -0
  33. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  34. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  36. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  37. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  38. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  39. package/src/components/custom/Table/Table.stories.tsx +1 -1
  40. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  41. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  42. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  43. package/src/components/custom/TimerReadout/index.ts +1 -0
  44. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  45. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  46. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  47. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  48. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  49. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  50. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  51. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  52. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  53. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  54. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  55. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  56. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  57. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  58. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  59. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  60. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  61. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  62. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  63. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  64. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  65. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  66. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  67. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  68. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  69. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  70. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  71. package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
  72. package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
  73. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  74. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  75. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  76. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  77. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  78. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  79. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  80. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  81. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  82. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  83. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  84. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  85. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  86. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  87. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  88. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  89. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  90. package/src/components/custom/Workout/README.md +146 -4
  91. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  92. package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
  93. package/src/components/custom/Workout/RestTimer.tsx +15 -10
  94. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  95. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  96. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  97. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  98. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  99. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  100. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  101. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  102. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  103. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  104. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  105. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  106. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  107. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  108. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  109. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  110. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  111. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  112. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  113. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  114. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  115. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  116. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  117. package/src/components/custom/Workout/SetBar.tsx +171 -0
  118. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  119. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  120. package/src/components/custom/Workout/SetRow.tsx +169 -232
  121. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  122. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  123. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  124. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  125. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  126. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  127. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  128. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  129. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  130. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  131. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  132. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  133. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  134. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  135. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  136. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  137. package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
  138. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  139. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  140. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  141. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  142. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  143. package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
  144. package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
  145. package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
  146. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  147. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  148. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  149. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  150. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  151. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  152. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  153. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  154. package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
  155. package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
  156. package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
  157. package/src/components/custom/Workout/index.ts +55 -2
  158. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  159. package/src/components/custom/Workout/metricText.tsx +34 -0
  160. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  161. package/src/components/custom/Workout/paceTone.ts +27 -0
  162. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  163. package/src/components/custom/index.ts +1 -0
  164. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  165. package/src/components/icons/icons.stories.tsx +67 -2
  166. package/src/components/icons/icons.test.tsx +32 -2
  167. package/src/components/icons/icons.tsx +108 -0
  168. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  169. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  170. package/src/components/shell/DashboardShell.test.tsx +42 -0
  171. package/src/components/shell/DashboardShell.tsx +74 -0
  172. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  173. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  174. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  175. package/src/components/shell/NavItem.stories.tsx +52 -0
  176. package/src/components/shell/NavItem.test.tsx +35 -0
  177. package/src/components/shell/NavItem.tsx +75 -0
  178. package/src/components/shell/README.md +30 -0
  179. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  180. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  181. package/src/components/shell/SessionStatePill.tsx +1 -1
  182. package/src/components/shell/SideNav.stories.tsx +81 -0
  183. package/src/components/shell/SideNav.test.tsx +51 -0
  184. package/src/components/shell/SideNav.tsx +74 -0
  185. package/src/components/shell/TopBar.stories.tsx +5 -5
  186. package/src/components/shell/index.ts +5 -0
  187. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  188. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  189. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  190. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  191. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  192. package/src/components/ui/button/Button.stories.tsx +1 -1
  193. package/src/components/ui/card/Card.stories.tsx +1 -1
  194. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  195. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  196. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  197. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  198. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  199. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  200. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  201. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  202. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  203. package/src/components/ui/index.ts +1 -0
  204. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  205. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  206. package/src/components/ui/indicator/Indicator.tsx +6 -1
  207. package/src/components/ui/input/Input.stories.tsx +1 -1
  208. package/src/components/ui/link/Link.stories.tsx +1 -1
  209. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  210. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  211. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  212. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  213. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  214. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  215. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  216. package/src/components/ui/section/Section.stories.tsx +1 -1
  217. package/src/components/ui/select/Select.stories.tsx +1 -1
  218. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  219. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  220. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  221. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  222. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  223. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  224. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  225. package/src/components/ui/tile/Tile.test.tsx +48 -0
  226. package/src/components/ui/tile/Tile.tsx +64 -0
  227. package/src/components/ui/tile/index.ts +2 -0
  228. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  229. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  230. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  231. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  232. package/src/hooks/index.ts +2 -0
  233. package/src/hooks/useTimer.test.ts +150 -0
  234. package/src/hooks/useTimer.ts +77 -0
  235. package/src/index.ts +3 -0
  236. package/src/pages.ts +1 -1
  237. package/src/stories/PresetShowcase.stories.tsx +1 -1
  238. package/src/stories/ThemePresets.stories.tsx +1 -1
  239. package/src/theme/ColorSystem.stories.tsx +1 -1
  240. package/src/theme/Colors.stories.tsx +5 -8
  241. package/src/theme/config.ts +4 -0
  242. package/src/theme/elevation.stories.tsx +1 -1
  243. package/src/theme/global.css +5 -1
  244. package/src/theme/shadows.stories.tsx +1 -1
  245. package/src/theme/tokens/primitives.ts +11 -107
  246. package/src/theme/tokens/semantic.ts +8 -0
  247. package/src/theme/workout-tokens.ts +11 -7
  248. package/tailwind.config.js +5 -0
@@ -1,24 +1,39 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
- import { render, screen, fireEvent } from '@testing-library/react'
2
+ 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
6
 
7
7
  const baseCollapsedProps = {
8
8
  name: 'Bench Press',
9
- state: 'collapsed' as const,
10
- onToggle: vi.fn(),
11
9
  summary: { sets: 3, reps: 6, weight: 175, unit: 'lbs' as const },
12
10
  }
13
11
 
14
- const baseSets: SetRowProps[] = [
15
- { mode: 'completed', setNumber: 1, reps: 8, weight: 135, unit: 'lbs' },
16
- { mode: 'completed', setNumber: 2, reps: 8, weight: 135, unit: 'lbs' },
17
- { mode: 'active', setNumber: 3, reps: null, weight: null, unit: 'lbs', isNextSet: true },
12
+ // Done · LIVE · upcoming — the locked "one object" narrative.
13
+ const unifiedSets: SetRowProps[] = [
14
+ {
15
+ state: 'done',
16
+ setNumber: 1,
17
+ unit: 'lbs',
18
+ reps: 10,
19
+ weight: 90,
20
+ rpe: 7.5,
21
+ velocities: [1.05, 0.9, 0.82],
22
+ },
23
+ {
24
+ state: 'live',
25
+ setNumber: 2,
26
+ unit: 'lbs',
27
+ target: { reps: 10, weight: 90 },
28
+ reps: 2,
29
+ weight: 90,
30
+ velocities: [0.95, 0.9],
31
+ },
32
+ { state: 'todo', setNumber: 3, unit: 'lbs', target: { reps: 10, weight: 90 } },
18
33
  ]
19
34
 
20
35
  describe('ExerciseCard', () => {
21
- describe('collapsed state', () => {
36
+ describe('collapsed', () => {
22
37
  it('renders exercise name', () => {
23
38
  render(<ExerciseCard {...baseCollapsedProps} />)
24
39
  expect(screen.getByTestId('exercise-card-name')).toHaveTextContent('Bench Press')
@@ -26,19 +41,7 @@ describe('ExerciseCard', () => {
26
41
 
27
42
  it('renders summary text', () => {
28
43
  render(<ExerciseCard {...baseCollapsedProps} />)
29
- expect(screen.getByTestId('exercise-card-summary')).toHaveTextContent(
30
- '3\u00D76 @ 175 lbs',
31
- )
32
- })
33
-
34
- it('renders WeightBadge when e1rm provided', () => {
35
- render(
36
- <ExerciseCard
37
- {...baseCollapsedProps}
38
- e1rm={{ value: 225, unit: 'lbs' }}
39
- />,
40
- )
41
- expect(screen.getByTestId('exercise-card-e1rm')).toBeInTheDocument()
44
+ expect(screen.getByTestId('exercise-card-summary')).toHaveTextContent('3×6 @ 175 lbs')
42
45
  })
43
46
 
44
47
  it('renders PrBadge when isPR is true', () => {
@@ -46,13 +49,16 @@ describe('ExerciseCard', () => {
46
49
  expect(screen.getByTestId('pr-badge-star')).toBeInTheDocument()
47
50
  })
48
51
 
49
- it('renders mini velocity strips for completed sets', () => {
52
+ it('renders mini velocity strips for logged sets', () => {
50
53
  render(
51
54
  <ExerciseCard
52
55
  {...baseCollapsedProps}
53
- setVelocities={[[1.1, 0.95], [0.9, 0.8]]}
56
+ setVelocities={[
57
+ [1.1, 0.95],
58
+ [0.9, 0.8],
59
+ ]}
54
60
  totalPlannedSets={4}
55
- />,
61
+ />
56
62
  )
57
63
  expect(screen.getByTestId('exercise-card-velocity-strip-0')).toBeInTheDocument()
58
64
  expect(screen.getByTestId('exercise-card-velocity-strip-1')).toBeInTheDocument()
@@ -60,11 +66,7 @@ describe('ExerciseCard', () => {
60
66
 
61
67
  it('renders placeholder strips for remaining planned sets', () => {
62
68
  render(
63
- <ExerciseCard
64
- {...baseCollapsedProps}
65
- setVelocities={[[1.1, 0.95]]}
66
- totalPlannedSets={3}
67
- />,
69
+ <ExerciseCard {...baseCollapsedProps} setVelocities={[[1.1, 0.95]]} totalPlannedSets={3} />
68
70
  )
69
71
  expect(screen.getByTestId('exercise-card-placeholder-0')).toBeInTheDocument()
70
72
  expect(screen.getByTestId('exercise-card-placeholder-1')).toBeInTheDocument()
@@ -76,36 +78,49 @@ describe('ExerciseCard', () => {
76
78
  })
77
79
  })
78
80
 
79
- describe('expanded state', () => {
81
+ describe('expanded (unified: rail heading header + faded body)', () => {
80
82
  const expandedProps = {
81
83
  name: 'Squat',
82
- state: 'expanded' as const,
83
- onToggle: vi.fn(),
84
+ defaultExpanded: true,
84
85
  summary: { sets: 3, reps: 8, weight: 185, unit: 'lbs' as const },
85
- sets: baseSets,
86
+ sets: unifiedSets,
86
87
  }
87
88
 
88
- it('renders exercise name in header', () => {
89
+ // Literal-hex text treatments (dark theme neutral 100 / 400).
90
+ const T_ACTIVE = '#F3F4F6'
91
+ const T_MUTED = '#9CA3AF'
92
+
93
+ it('renders exercise name via the real ExerciseCardHeading header', () => {
89
94
  render(<ExerciseCard {...expandedProps} />)
90
95
  expect(screen.getByTestId('exercise-card-name')).toHaveTextContent('Squat')
91
96
  })
92
97
 
93
- it('renders column headers', () => {
98
+ it('is a single object: the heading is nested in the card, not a duplicate exercise-card', () => {
99
+ render(<ExerciseCard {...expandedProps} />)
100
+ expect(screen.getByTestId('exercise-card')).toBeInTheDocument()
101
+ expect(screen.getByTestId('exercise-card-heading')).toBeInTheDocument()
102
+ expect(screen.getByTestId('exercise-card-body')).toBeInTheDocument()
103
+ })
104
+
105
+ it('renders the header per-set strip derived from the set rows', () => {
106
+ render(<ExerciseCard {...expandedProps} />)
107
+ expect(screen.getByTestId('exercise-card-strip')).toBeInTheDocument()
108
+ expect(screen.getByTestId('set-strip')).toBeInTheDocument()
109
+ })
110
+
111
+ it('drops the PREV column (showPrevious=false) but keeps SET/REPS/LBS/RPE', () => {
94
112
  render(<ExerciseCard {...expandedProps} />)
95
113
  const headers = screen.getByTestId('exercise-card-column-headers')
96
114
  expect(headers).toHaveTextContent('SET')
97
- expect(headers).toHaveTextContent('PREV')
98
115
  expect(headers).toHaveTextContent('REPS')
99
116
  expect(headers).toHaveTextContent('LBS')
100
117
  expect(headers).toHaveTextContent('RPE')
118
+ expect(headers).not.toHaveTextContent('PREV')
101
119
  })
102
120
 
103
121
  it('shows the KG weight header when the unit is kg', () => {
104
122
  render(
105
- <ExerciseCard
106
- {...expandedProps}
107
- summary={{ sets: 3, reps: 8, weight: 84, unit: 'kg' }}
108
- />,
123
+ <ExerciseCard {...expandedProps} summary={{ sets: 3, reps: 8, weight: 84, unit: 'kg' }} />
109
124
  )
110
125
  const headers = screen.getByTestId('exercise-card-column-headers')
111
126
  expect(headers).toHaveTextContent('KG')
@@ -116,56 +131,61 @@ describe('ExerciseCard', () => {
116
131
  render(
117
132
  <ExerciseCard
118
133
  name="Squat"
119
- state="expanded"
120
- onToggle={vi.fn()}
121
- sets={[{ mode: 'completed', setNumber: 1, reps: 8, weight: 60, unit: 'kg' }]}
122
- />,
134
+ defaultExpanded
135
+ sets={[{ state: 'done', setNumber: 1, unit: 'kg', reps: 8, weight: 60, velocities: [] }]}
136
+ />
123
137
  )
124
138
  expect(screen.getByTestId('exercise-card-column-headers')).toHaveTextContent('KG')
125
139
  })
126
140
 
127
- it('renders SetRow components', () => {
141
+ it('renders one SetRow per set', () => {
128
142
  render(<ExerciseCard {...expandedProps} />)
129
- const setRows = screen.getAllByTestId('set-row')
130
- expect(setRows).toHaveLength(3)
143
+ expect(screen.getAllByTestId('set-row')).toHaveLength(3)
131
144
  })
132
145
 
133
- it('renders TempoDisplay when tempo provided', () => {
134
- render(
135
- <ExerciseCard
136
- {...expandedProps}
137
- tempo={[2, 1, 3, 0]}
138
- />,
139
- )
140
- expect(screen.getByTestId('exercise-card-tempo')).toBeInTheDocument()
141
- expect(screen.getByTestId('tempo-display')).toBeInTheDocument()
146
+ it('shows the TARGET reps for the live set (no "reps-done/target")', () => {
147
+ render(<ExerciseCard {...expandedProps} />)
148
+ const rows = screen.getAllByTestId('set-row')
149
+ expect(rows[1]).toHaveTextContent('10')
150
+ expect(rows[1]).not.toHaveTextContent('2/10')
142
151
  })
143
152
 
144
- it('does not render TempoDisplay when no tempo', () => {
153
+ it('spotlights the active set by brightness and mutes done + upcoming', () => {
145
154
  render(<ExerciseCard {...expandedProps} />)
146
- expect(screen.queryByTestId('exercise-card-tempo')).not.toBeInTheDocument()
155
+ const rows = screen.getAllByTestId('set-row')
156
+ expect(within(rows[0]).getByText('1')).toHaveStyle({ color: T_MUTED })
157
+ expect(within(rows[1]).getByText('2')).toHaveStyle({ color: T_ACTIVE })
158
+ expect(within(rows[2]).getByText('3')).toHaveStyle({ color: T_MUTED })
159
+ })
160
+
161
+ it('uses the compact velocity-height spotlight for the live set and mini elsewhere', () => {
162
+ render(<ExerciseCard {...expandedProps} />)
163
+ expect(screen.getAllByTestId('velocity-strip-compact')).toHaveLength(1)
164
+ expect(screen.getAllByTestId('velocity-strip-mini')).toHaveLength(2)
165
+ })
166
+
167
+ it('renders the TempoDisplay in the header when tempo provided', () => {
168
+ render(<ExerciseCard {...expandedProps} tempo={[2, 1, 3, 0]} />)
169
+ expect(screen.getByTestId('tempo-display')).toBeInTheDocument()
147
170
  })
148
171
 
149
- it('sets aria-expanded on header button', () => {
172
+ it('does not render TempoDisplay when no tempo', () => {
150
173
  render(<ExerciseCard {...expandedProps} />)
151
- const header = screen.getByTestId('exercise-card-header')
152
- expect(header).toHaveAttribute('aria-expanded', 'true')
174
+ expect(screen.queryByTestId('tempo-display')).not.toBeInTheDocument()
153
175
  })
154
176
  })
155
177
 
156
- describe('upcoming state', () => {
178
+ describe('upcoming', () => {
157
179
  const upcomingProps = {
158
180
  name: 'Deadlift',
159
- state: 'upcoming' as const,
160
- onToggle: vi.fn(),
161
- prescription: '3\u00D78-12 @ RPE 8',
162
- previousBest: '185 lbs \u00D7 10',
181
+ upcoming: true,
182
+ prescription: '3×8-12 @ RPE 8',
183
+ previousBest: '185 lbs × 10',
163
184
  }
164
185
 
165
186
  it('renders with reduced opacity', () => {
166
187
  render(<ExerciseCard {...upcomingProps} />)
167
- const card = screen.getByTestId('exercise-card')
168
- expect(card).toHaveStyle({ opacity: 0.6 })
188
+ expect(screen.getByTestId('exercise-card')).toHaveStyle({ opacity: 0.6 })
169
189
  })
170
190
 
171
191
  it('renders exercise name', () => {
@@ -175,63 +195,72 @@ describe('ExerciseCard', () => {
175
195
 
176
196
  it('renders prescription text', () => {
177
197
  render(<ExerciseCard {...upcomingProps} />)
178
- expect(screen.getByTestId('exercise-card-prescription')).toHaveTextContent(
179
- '3\u00D78-12 @ RPE 8',
180
- )
198
+ expect(screen.getByTestId('exercise-card-prescription')).toHaveTextContent('3×8-12 @ RPE 8')
181
199
  })
182
200
 
183
201
  it('renders previous best text', () => {
184
202
  render(<ExerciseCard {...upcomingProps} />)
185
- expect(screen.getByTestId('exercise-card-previous-best')).toHaveTextContent(
186
- '185 lbs \u00D7 10',
187
- )
203
+ expect(screen.getByTestId('exercise-card-previous-best')).toHaveTextContent('185 lbs × 10')
188
204
  })
189
205
 
190
- it('does not render interactive elements', () => {
191
- render(<ExerciseCard {...upcomingProps} />)
192
- expect(screen.queryByTestId('exercise-card-strips')).not.toBeInTheDocument()
206
+ it('ignores expand and never reveals the body', () => {
207
+ render(<ExerciseCard {...upcomingProps} defaultExpanded sets={unifiedSets} />)
208
+ expect(screen.queryByTestId('exercise-card-body')).not.toBeInTheDocument()
193
209
  expect(screen.queryByTestId('exercise-card-column-headers')).not.toBeInTheDocument()
194
210
  })
195
211
  })
196
212
 
197
- describe('onToggle', () => {
198
- it('fires onToggle when collapsed card is pressed', () => {
199
- const onToggle = vi.fn()
213
+ describe('expand control', () => {
214
+ it('uncontrolled: defaultExpanded opens, and pressing the header collapses it', () => {
215
+ render(<ExerciseCard {...baseCollapsedProps} defaultExpanded sets={unifiedSets} />)
216
+ expect(screen.getByTestId('exercise-card-body')).toBeInTheDocument()
217
+ fireEvent.click(screen.getByTestId('exercise-card-header'))
218
+ expect(screen.queryByTestId('exercise-card-body')).not.toBeInTheDocument()
219
+ })
220
+
221
+ it('uncontrolled: collapsed by default, pressing the header expands it', () => {
222
+ render(<ExerciseCard {...baseCollapsedProps} sets={unifiedSets} />)
223
+ expect(screen.queryByTestId('exercise-card-body')).not.toBeInTheDocument()
224
+ fireEvent.click(screen.getByTestId('exercise-card-header'))
225
+ expect(screen.getByTestId('exercise-card-body')).toBeInTheDocument()
226
+ })
227
+
228
+ it('controlled: reports the requested state but does not self-toggle', () => {
229
+ const onExpandedChange = vi.fn()
200
230
  render(
201
231
  <ExerciseCard
202
232
  {...baseCollapsedProps}
203
- onToggle={onToggle}
204
- />,
233
+ expanded={false}
234
+ onExpandedChange={onExpandedChange}
235
+ sets={unifiedSets}
236
+ />
205
237
  )
238
+ expect(screen.queryByTestId('exercise-card-body')).not.toBeInTheDocument()
206
239
  fireEvent.click(screen.getByTestId('exercise-card-header'))
207
- expect(onToggle).toHaveBeenCalledOnce()
240
+ expect(onExpandedChange).toHaveBeenCalledWith(true)
241
+ // Controlled: still collapsed until the parent flips `expanded`.
242
+ expect(screen.queryByTestId('exercise-card-body')).not.toBeInTheDocument()
208
243
  })
209
244
 
210
- it('fires onToggle when expanded header is pressed', () => {
211
- const onToggle = vi.fn()
245
+ it('fires onExpandedChange(false) when an expanded header is pressed', () => {
246
+ const onExpandedChange = vi.fn()
212
247
  render(
213
248
  <ExerciseCard
214
- name="Squat"
215
- state="expanded"
216
- onToggle={onToggle}
217
- sets={baseSets}
218
- />,
249
+ {...baseCollapsedProps}
250
+ defaultExpanded
251
+ onExpandedChange={onExpandedChange}
252
+ sets={unifiedSets}
253
+ />
219
254
  )
220
255
  fireEvent.click(screen.getByTestId('exercise-card-header'))
221
- expect(onToggle).toHaveBeenCalledOnce()
256
+ expect(onExpandedChange).toHaveBeenCalledWith(false)
222
257
  })
223
258
  })
224
259
 
225
260
  describe('superset position border radius', () => {
226
261
  it('applies first position border radius', () => {
227
- render(
228
- <ExerciseCard
229
- {...baseCollapsedProps}
230
- supersetPosition="first"
231
- />,
232
- )
233
- const card = screen.getByTestId('exercise-card')
234
- const container = card.firstChild as HTMLElement
262
+ render(<ExerciseCard {...baseCollapsedProps} supersetPosition="first" />)
263
+ const container = screen.getByTestId('exercise-card').firstChild as HTMLElement
235
264
  expect(container).toHaveStyle({
236
265
  borderTopLeftRadius: '12px',
237
266
  borderTopRightRadius: '12px',
@@ -241,14 +270,8 @@ describe('ExerciseCard', () => {
241
270
  })
242
271
 
243
272
  it('applies last position border radius', () => {
244
- render(
245
- <ExerciseCard
246
- {...baseCollapsedProps}
247
- supersetPosition="last"
248
- />,
249
- )
250
- const card = screen.getByTestId('exercise-card')
251
- const container = card.firstChild as HTMLElement
273
+ render(<ExerciseCard {...baseCollapsedProps} supersetPosition="last" />)
274
+ const container = screen.getByTestId('exercise-card').firstChild as HTMLElement
252
275
  expect(container).toHaveStyle({
253
276
  borderTopLeftRadius: '8px',
254
277
  borderTopRightRadius: '8px',
@@ -258,14 +281,8 @@ describe('ExerciseCard', () => {
258
281
  })
259
282
 
260
283
  it('applies middle position border radius', () => {
261
- render(
262
- <ExerciseCard
263
- {...baseCollapsedProps}
264
- supersetPosition="middle"
265
- />,
266
- )
267
- const card = screen.getByTestId('exercise-card')
268
- const container = card.firstChild as HTMLElement
284
+ render(<ExerciseCard {...baseCollapsedProps} supersetPosition="middle" />)
285
+ const container = screen.getByTestId('exercise-card').firstChild as HTMLElement
269
286
  expect(container).toHaveStyle({
270
287
  borderTopLeftRadius: '8px',
271
288
  borderTopRightRadius: '8px',
@@ -276,65 +293,51 @@ describe('ExerciseCard', () => {
276
293
  })
277
294
 
278
295
  describe('accessibility', () => {
279
- it('has correct accessibility label for collapsed state', () => {
296
+ it('has correct accessibility label for collapsed', () => {
280
297
  render(<ExerciseCard {...baseCollapsedProps} />)
281
- expect(
282
- screen.getByLabelText('Bench Press, 3\u00D76 @ 175 lbs'),
283
- ).toBeInTheDocument()
298
+ expect(screen.getByLabelText('Bench Press, 3×6 @ 175 lbs')).toBeInTheDocument()
284
299
  })
285
300
 
286
- it('has correct accessibility label for upcoming state with prescription', () => {
287
- render(
288
- <ExerciseCard
289
- name="Deadlift"
290
- state="upcoming"
291
- onToggle={vi.fn()}
292
- prescription="3x8 @ RPE 8"
293
- />,
294
- )
295
- expect(
296
- screen.getByLabelText('Deadlift, 3x8 @ RPE 8'),
297
- ).toBeInTheDocument()
301
+ it('has correct accessibility label for upcoming with prescription', () => {
302
+ render(<ExerciseCard name="Deadlift" upcoming prescription="3x8 @ RPE 8" />)
303
+ expect(screen.getByLabelText('Deadlift, 3x8 @ RPE 8')).toBeInTheDocument()
298
304
  })
299
305
 
300
- it('has no accessibility violations in collapsed state', async () => {
306
+ it('has no accessibility violations when collapsed', async () => {
301
307
  const { container } = render(
302
308
  <ExerciseCard
303
309
  {...baseCollapsedProps}
304
- e1rm={{ value: 225, unit: 'lbs' }}
305
310
  isPR
306
311
  setVelocities={[[1.1, 0.95]]}
307
312
  totalPlannedSets={3}
308
- />,
313
+ />
309
314
  )
310
315
  const results = await axe(container)
311
316
  expect(results).toHaveNoViolations()
312
317
  })
313
318
 
314
- it('has no accessibility violations in expanded state', async () => {
319
+ it('has no accessibility violations when expanded', async () => {
315
320
  const { container } = render(
316
321
  <ExerciseCard
317
322
  name="Squat"
318
- state="expanded"
319
- onToggle={vi.fn()}
323
+ defaultExpanded
320
324
  summary={{ sets: 3, reps: 8, weight: 185, unit: 'lbs' }}
321
- sets={baseSets}
325
+ sets={unifiedSets}
322
326
  tempo={[2, 1, 3, 0]}
323
- />,
327
+ />
324
328
  )
325
329
  const results = await axe(container)
326
330
  expect(results).toHaveNoViolations()
327
331
  })
328
332
 
329
- it('has no accessibility violations in upcoming state', async () => {
333
+ it('has no accessibility violations when upcoming', async () => {
330
334
  const { container } = render(
331
335
  <ExerciseCard
332
336
  name="Deadlift"
333
- state="upcoming"
334
- onToggle={vi.fn()}
337
+ upcoming
335
338
  prescription="3x8 @ RPE 8"
336
339
  previousBest="185 lbs x 10"
337
- />,
340
+ />
338
341
  )
339
342
  const results = await axe(container)
340
343
  expect(results).toHaveNoViolations()