@titan-design/react-ui 0.6.0 → 0.8.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 (258) 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 +956 -128
  6. package/dist/index.d.ts +956 -128
  7. package/dist/index.js +2873 -981
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2826 -982
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1659 -799
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1660 -800
  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/CircularTimer/CircularTimer.stories.tsx +90 -0
  34. package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
  35. package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
  36. package/src/components/custom/CircularTimer/index.ts +1 -0
  37. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  38. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  39. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  40. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  41. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  42. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  43. package/src/components/custom/Table/Table.stories.tsx +1 -1
  44. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  45. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  46. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  47. package/src/components/custom/TimerReadout/index.ts +1 -0
  48. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  49. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  54. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  55. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  56. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  57. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  58. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  59. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  60. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  61. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  62. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  63. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  64. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  65. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  66. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  67. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  68. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  69. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  70. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  71. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  72. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  73. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  74. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  75. package/src/components/custom/Workout/FatigueMeter.test.tsx +85 -0
  76. package/src/components/custom/Workout/FatigueMeter.tsx +86 -0
  77. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  78. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  79. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  80. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  81. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  82. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  84. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  85. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  86. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  87. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  88. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  89. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  90. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  91. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  92. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  93. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  94. package/src/components/custom/Workout/README.md +193 -4
  95. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  96. package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
  97. package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
  98. package/src/components/custom/Workout/RestTimer.tsx +155 -59
  99. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  100. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  101. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  102. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  103. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  104. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  105. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  106. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  107. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  108. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  109. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  110. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  111. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  112. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  113. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  114. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  115. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  116. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  117. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  118. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  119. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  120. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  121. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  122. package/src/components/custom/Workout/SetBar.tsx +171 -0
  123. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  124. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  125. package/src/components/custom/Workout/SetRow.tsx +169 -232
  126. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  127. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  128. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  129. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  130. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  131. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  132. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  133. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  134. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  135. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  136. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  137. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  138. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  139. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  140. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  141. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  142. package/src/components/custom/Workout/TempoBar.stories.tsx +70 -1
  143. package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
  144. package/src/components/custom/Workout/TempoBar.tsx +107 -22
  145. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  146. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  147. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  148. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  149. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  150. package/src/components/custom/Workout/VelocityStrip.stories.tsx +262 -56
  151. package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
  152. package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
  153. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  154. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  155. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  156. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  157. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  158. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  159. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  160. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  161. package/src/components/custom/Workout/ZoneTrack.stories.tsx +139 -0
  162. package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
  163. package/src/components/custom/Workout/ZoneTrack.tsx +373 -0
  164. package/src/components/custom/Workout/index.ts +55 -2
  165. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  166. package/src/components/custom/Workout/metricText.tsx +34 -0
  167. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  168. package/src/components/custom/Workout/paceTone.ts +27 -0
  169. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  170. package/src/components/custom/index.ts +2 -0
  171. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  172. package/src/components/icons/icons.stories.tsx +67 -2
  173. package/src/components/icons/icons.test.tsx +32 -2
  174. package/src/components/icons/icons.tsx +108 -0
  175. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  176. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  177. package/src/components/shell/DashboardShell.test.tsx +42 -0
  178. package/src/components/shell/DashboardShell.tsx +74 -0
  179. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  180. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  181. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  182. package/src/components/shell/NavItem.stories.tsx +52 -0
  183. package/src/components/shell/NavItem.test.tsx +35 -0
  184. package/src/components/shell/NavItem.tsx +75 -0
  185. package/src/components/shell/README.md +30 -0
  186. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  187. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  188. package/src/components/shell/SessionStatePill.tsx +1 -1
  189. package/src/components/shell/SideNav.stories.tsx +81 -0
  190. package/src/components/shell/SideNav.test.tsx +51 -0
  191. package/src/components/shell/SideNav.tsx +74 -0
  192. package/src/components/shell/TopBar.stories.tsx +5 -5
  193. package/src/components/shell/index.ts +5 -0
  194. package/src/components/ui/alert/Alert.stories.tsx +86 -3
  195. package/src/components/ui/alert/Alert.test.tsx +52 -0
  196. package/src/components/ui/alert/Alert.tsx +54 -20
  197. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  198. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  199. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  200. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  201. package/src/components/ui/button/Button.stories.tsx +1 -1
  202. package/src/components/ui/card/Card.stories.tsx +1 -1
  203. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  204. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  205. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  206. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  207. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  208. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  209. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  210. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  211. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  212. package/src/components/ui/index.ts +1 -0
  213. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  214. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  215. package/src/components/ui/indicator/Indicator.tsx +6 -1
  216. package/src/components/ui/input/Input.stories.tsx +1 -1
  217. package/src/components/ui/link/Link.stories.tsx +1 -1
  218. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  219. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  220. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  221. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  222. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  223. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  224. package/src/components/ui/progress/Progress.tsx +47 -22
  225. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  226. package/src/components/ui/section/Section.stories.tsx +1 -1
  227. package/src/components/ui/select/Select.stories.tsx +1 -1
  228. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  229. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  230. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  231. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  232. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  233. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  234. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  235. package/src/components/ui/tile/Tile.test.tsx +48 -0
  236. package/src/components/ui/tile/Tile.tsx +64 -0
  237. package/src/components/ui/tile/index.ts +2 -0
  238. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  239. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  240. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  241. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  242. package/src/hooks/index.ts +2 -0
  243. package/src/hooks/useTimer.test.ts +150 -0
  244. package/src/hooks/useTimer.ts +77 -0
  245. package/src/index.ts +3 -0
  246. package/src/pages.ts +1 -1
  247. package/src/stories/PresetShowcase.stories.tsx +1 -1
  248. package/src/stories/ThemePresets.stories.tsx +1 -1
  249. package/src/theme/ColorSystem.stories.tsx +1 -1
  250. package/src/theme/Colors.stories.tsx +5 -8
  251. package/src/theme/config.ts +4 -0
  252. package/src/theme/elevation.stories.tsx +1 -1
  253. package/src/theme/global.css +5 -1
  254. package/src/theme/shadows.stories.tsx +1 -1
  255. package/src/theme/tokens/primitives.ts +11 -107
  256. package/src/theme/tokens/semantic.ts +8 -0
  257. package/src/theme/workout-tokens.ts +11 -7
  258. package/tailwind.config.js +5 -0
@@ -11,170 +11,137 @@ import {
11
11
 
12
12
  const sampleVelocities = [1.1, 0.95, 0.82, 0.68, 0.45]
13
13
 
14
- describe('VelocityStrip', () => {
15
- it('renders correct number of segments', () => {
14
+ describe('VelocityStrip expanded (framed chart)', () => {
15
+ it('renders the open chart with bars, labels and info by default', () => {
16
16
  render(<VelocityStrip velocities={sampleVelocities} />)
17
+ expect(screen.getByTestId('velocity-strip')).toBeInTheDocument()
18
+ expect(screen.getByTestId('velocity-info-row')).toBeInTheDocument()
17
19
  for (let i = 0; i < sampleVelocities.length; i++) {
18
20
  expect(screen.getByTestId(`velocity-bar-${i}`)).toBeInTheDocument()
21
+ expect(screen.getByTestId(`velocity-label-${i}`)).toBeInTheDocument()
19
22
  }
20
23
  })
21
24
 
22
- it('renders with expanded state', () => {
23
- render(<VelocityStrip velocities={sampleVelocities} expanded />)
24
- expect(screen.getByTestId('velocity-strip')).toBeInTheDocument()
25
- expect(screen.getByTestId('velocity-info-row')).toBeInTheDocument()
25
+ it('labels the open chart as tap-to-collapse (open by default)', () => {
26
+ render(<VelocityStrip velocities={sampleVelocities} />)
27
+ expect(
28
+ screen.getByLabelText('Velocity chart for set, 5 reps, tap to collapse')
29
+ ).toBeInTheDocument()
26
30
  })
27
31
 
28
- it('shows velocity labels when expanded', () => {
29
- render(<VelocityStrip velocities={sampleVelocities} expanded />)
30
- for (let i = 0; i < sampleVelocities.length; i++) {
31
- expect(screen.getByTestId(`velocity-label-${i}`)).toBeInTheDocument()
32
- }
32
+ it('renders the 3px collapsed strip when expanded is false', () => {
33
+ render(<VelocityStrip velocities={sampleVelocities} expanded={false} />)
34
+ expect(
35
+ screen.getByLabelText('Velocity chart for set, 5 reps, tap to expand')
36
+ ).toBeInTheDocument()
37
+ expect(screen.queryByTestId('velocity-info-row')).not.toBeInTheDocument()
33
38
  })
34
39
 
35
40
  it('fires onToggle on press', () => {
36
41
  const onToggle = vi.fn()
37
- render(
38
- <VelocityStrip velocities={sampleVelocities} onToggle={onToggle} />,
39
- )
40
- const pressable = screen.getByTestId('velocity-strip-pressable')
41
- fireEvent.click(pressable)
42
+ render(<VelocityStrip velocities={sampleVelocities} onToggle={onToggle} />)
43
+ fireEvent.click(screen.getByTestId('velocity-strip-pressable'))
42
44
  expect(onToggle).toHaveBeenCalledOnce()
43
45
  })
44
46
 
45
- it('fires onRepPress with correct index when bar is tapped', () => {
47
+ it('fires onRepPress with the correct index when a bar is tapped', () => {
46
48
  const onRepPress = vi.fn()
47
- render(
48
- <VelocityStrip
49
- velocities={sampleVelocities}
50
- expanded
51
- onRepPress={onRepPress}
52
- />,
53
- )
54
- const bar = screen.getByTestId('velocity-bar-pressable-2')
55
- fireEvent.click(bar)
49
+ render(<VelocityStrip velocities={sampleVelocities} onRepPress={onRepPress} />)
50
+ fireEvent.click(screen.getByTestId('velocity-bar-pressable-2'))
56
51
  expect(onRepPress).toHaveBeenCalledWith(2, 0.82)
57
52
  })
58
53
 
59
- it('sets accessibility role and label', () => {
60
- render(<VelocityStrip velocities={sampleVelocities} />)
61
- expect(
62
- screen.getByLabelText(
63
- 'Velocity chart for set, 5 reps, tap to expand',
64
- ),
65
- ).toBeInTheDocument()
66
- })
67
-
68
- it('sets expanded accessibility label when expanded', () => {
69
- render(<VelocityStrip velocities={sampleVelocities} expanded />)
70
- expect(
71
- screen.getByLabelText(
72
- 'Velocity chart for set, 5 reps, tap to collapse',
73
- ),
74
- ).toBeInTheDocument()
75
- })
76
-
77
- describe('showInfo prop', () => {
78
- it('shows info row when expanded and showInfo is true (default)', () => {
79
- render(<VelocityStrip velocities={sampleVelocities} expanded />)
54
+ describe('showInfo', () => {
55
+ it('shows the info row by default', () => {
56
+ render(<VelocityStrip velocities={sampleVelocities} />)
80
57
  expect(screen.getByTestId('velocity-info-row')).toBeInTheDocument()
81
58
  })
82
59
 
83
- it('hides info row when expanded and showInfo is false', () => {
84
- render(<VelocityStrip velocities={sampleVelocities} expanded showInfo={false} />)
85
- expect(screen.queryByTestId('velocity-info-row')).not.toBeInTheDocument()
86
- })
87
-
88
- it('does not show info row when collapsed regardless of showInfo', () => {
89
- render(<VelocityStrip velocities={sampleVelocities} showInfo />)
60
+ it('hides the info row when showInfo is false', () => {
61
+ render(<VelocityStrip velocities={sampleVelocities} showInfo={false} />)
90
62
  expect(screen.queryByTestId('velocity-info-row')).not.toBeInTheDocument()
91
63
  })
92
64
  })
93
65
 
94
- describe('mini variant', () => {
95
- it('renders mini strip', () => {
96
- render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
97
- expect(screen.getByTestId('velocity-strip-mini')).toBeInTheDocument()
98
- })
99
-
100
- it('does not respond to press', () => {
101
- const onToggle = vi.fn()
102
- render(
103
- <VelocityStrip
104
- velocities={sampleVelocities}
105
- variant="mini"
106
- onToggle={onToggle}
107
- />,
108
- )
109
- expect(
110
- screen.queryByTestId('velocity-strip-pressable'),
111
- ).not.toBeInTheDocument()
66
+ describe('showNumbers', () => {
67
+ it('shows per-bar labels by default', () => {
68
+ render(<VelocityStrip velocities={sampleVelocities} />)
69
+ expect(screen.getByTestId('velocity-label-0')).toBeInTheDocument()
112
70
  })
113
71
 
114
- it('renders correct number of segments', () => {
115
- render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
116
- for (let i = 0; i < sampleVelocities.length; i++) {
117
- expect(screen.getByTestId(`velocity-bar-${i}`)).toBeInTheDocument()
118
- }
72
+ it('hides per-bar labels when showNumbers is false (but keeps the framed chart via showInfo)', () => {
73
+ render(<VelocityStrip velocities={sampleVelocities} showNumbers={false} />)
74
+ expect(screen.queryByTestId('velocity-label-0')).not.toBeInTheDocument()
75
+ expect(screen.getByTestId('velocity-strip')).toBeInTheDocument()
76
+ expect(screen.getByTestId('velocity-info-row')).toBeInTheDocument()
119
77
  })
120
78
  })
121
79
 
122
- describe('accessibility', () => {
123
- it('has no accessibility violations', async () => {
124
- const { container } = render(
125
- <VelocityStrip velocities={sampleVelocities} onToggle={() => {}} />,
126
- )
127
- const results = await axe(container)
128
- expect(results).toHaveNoViolations()
80
+ describe('scale', () => {
81
+ it('peak (default) scales the tallest rep to full height', () => {
82
+ // maxV=1.0 -> denom 1.15 -> 1.0/1.15 = 87%.
83
+ render(<VelocityStrip velocities={[1.0, 0.5]} showInfo={false} />)
84
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '87%' })
129
85
  })
130
86
 
131
- it('has no accessibility violations when expanded', async () => {
132
- const { container } = render(
133
- <VelocityStrip velocities={sampleVelocities} expanded onToggle={() => {}} />,
134
- )
135
- const results = await axe(container)
136
- expect(results).toHaveNoViolations()
87
+ it('fixed scales against the 1.15 ceiling regardless of the set max', () => {
88
+ // fixed denom 1.15 -> 1.0/1.15 = 87%, 0.5/1.15 = 43%.
89
+ render(<VelocityStrip velocities={[1.0, 0.5]} scale="fixed" showInfo={false} />)
90
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '87%' })
91
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ height: '43%' })
137
92
  })
93
+ })
138
94
 
139
- it('has no accessibility violations when expanded with rep press', async () => {
95
+ describe('accessibility', () => {
96
+ it('has no violations (open, interactive)', async () => {
140
97
  const { container } = render(
141
- <VelocityStrip
142
- velocities={sampleVelocities}
143
- expanded
144
- onRepPress={() => {}}
145
- />,
98
+ <VelocityStrip velocities={sampleVelocities} onToggle={() => {}} />
146
99
  )
147
- const results = await axe(container)
148
- expect(results).toHaveNoViolations()
100
+ expect(await axe(container)).toHaveNoViolations()
149
101
  })
150
102
 
151
- it('has no accessibility violations in mini variant', async () => {
103
+ it('has no violations with rep press', async () => {
152
104
  const { container } = render(
153
- <VelocityStrip velocities={sampleVelocities} variant="mini" />,
105
+ <VelocityStrip velocities={sampleVelocities} onRepPress={() => {}} />
154
106
  )
155
- const results = await axe(container)
156
- expect(results).toHaveNoViolations()
107
+ expect(await axe(container)).toHaveNoViolations()
157
108
  })
158
109
 
159
- it('outer pressable has accessibility label when onToggle provided', () => {
160
- render(<VelocityStrip velocities={sampleVelocities} onToggle={() => {}} />)
110
+ it('rep pressables carry accessibility labels', () => {
111
+ render(<VelocityStrip velocities={sampleVelocities} onRepPress={() => {}} />)
161
112
  expect(
162
- screen.getByLabelText('Velocity chart for set, 5 reps, tap to expand'),
113
+ screen.getByLabelText('Rep 1: 1.10 meters per second, tap for details')
163
114
  ).toBeInTheDocument()
164
115
  })
116
+ })
117
+ })
165
118
 
166
- it('rep pressables have accessibility labels when expanded with onRepPress', () => {
167
- render(
168
- <VelocityStrip
169
- velocities={sampleVelocities}
170
- expanded
171
- onRepPress={() => {}}
172
- />,
173
- )
174
- expect(
175
- screen.getByLabelText('Rep 1: 1.10 meters per second, tap for details'),
176
- ).toBeInTheDocument()
177
- })
119
+ describe('VelocityStrip mini variant', () => {
120
+ it('renders the flat mini strip', () => {
121
+ render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
122
+ expect(screen.getByTestId('velocity-strip-mini')).toBeInTheDocument()
123
+ })
124
+
125
+ it('does not respond to press', () => {
126
+ render(<VelocityStrip velocities={sampleVelocities} variant="mini" onToggle={() => {}} />)
127
+ expect(screen.queryByTestId('velocity-strip-pressable')).not.toBeInTheDocument()
128
+ })
129
+
130
+ it('renders one bar per rep', () => {
131
+ render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
132
+ for (let i = 0; i < sampleVelocities.length; i++) {
133
+ expect(screen.getByTestId(`velocity-bar-${i}`)).toBeInTheDocument()
134
+ }
135
+ })
136
+
137
+ it('has no accessibility violations', async () => {
138
+ const { container } = render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
139
+ expect(await axe(container)).toHaveNoViolations()
140
+ })
141
+
142
+ it('does not apply live marking', () => {
143
+ render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} variant="mini" />)
144
+ expect(screen.queryByLabelText(/latest rep/)).not.toBeInTheDocument()
178
145
  })
179
146
  })
180
147
 
@@ -211,13 +178,10 @@ describe('getVelocityZoneName', () => {
211
178
 
212
179
  describe('calculateVelocityLoss', () => {
213
180
  it('calculates percentage loss from running-best to last rep', () => {
214
- // Best is the first rep here, so best->last matches the legacy result.
215
181
  expect(calculateVelocityLoss([1.0, 0.8])).toBe(20)
216
182
  })
217
183
 
218
184
  it('uses the best rep (not the first) as the reference for non-monotonic sets', () => {
219
- // best = 1.0 (rep 2), last = 0.6 -> (1.0 - 0.6) / 1.0 = 40%.
220
- // Legacy first->last would have been (0.8 - 0.6) / 0.8 = 25%.
221
185
  expect(calculateVelocityLoss([0.8, 1.0, 0.6])).toBe(40)
222
186
  })
223
187
 
@@ -226,7 +190,6 @@ describe('calculateVelocityLoss', () => {
226
190
  })
227
191
 
228
192
  it('clamps to 0 when a later rep exceeds the first rep', () => {
229
- // best = 1.2 (last), so loss is negative pre-clamp -> 0.
230
193
  expect(calculateVelocityLoss([1.0, 0.9, 1.2])).toBe(0)
231
194
  })
232
195
 
@@ -265,14 +228,18 @@ const compoundBands = [
265
228
  describe('VelocityStrip zones prop', () => {
266
229
  it('colors bars from the supplied bands (mini)', () => {
267
230
  render(<VelocityStrip velocities={[1.1, 0.45]} zones={compoundBands} variant="mini" />)
268
- // 1.1 -> speed -> green; 0.45 -> maximalStrength -> red (shared with grinding).
269
231
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
270
232
  expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#D14343' })
271
233
  })
272
234
 
273
- it('labels the summary row with the band containing the mean velocity', () => {
274
- // mean of the sample = 0.80 -> power band.
275
- render(<VelocityStrip velocities={sampleVelocities} zones={compoundBands} expanded />)
235
+ it('gives grinding and maximalStrength DISTINCT reds (5-band, no collapse)', () => {
236
+ render(<VelocityStrip velocities={[0.45, 0.2]} zones={compoundBands} variant="mini" />)
237
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#D14343' })
238
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#A4221C' })
239
+ })
240
+
241
+ it('labels the info row with the band containing the mean velocity', () => {
242
+ render(<VelocityStrip velocities={sampleVelocities} zones={compoundBands} />)
276
243
  expect(screen.getByTestId('velocity-info-row')).toHaveTextContent('Power')
277
244
  })
278
245
 
@@ -291,16 +258,14 @@ describe('VelocityStrip live mode', () => {
291
258
  })
292
259
 
293
260
  it('marks the latest rep bar in its accessibility label', () => {
294
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={3} expanded />)
295
- expect(
296
- screen.getByLabelText(/Rep 4: 0\.68 meters per second, latest rep$/),
297
- ).toBeInTheDocument()
261
+ render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={3} />)
262
+ expect(screen.getByLabelText(/Rep 4: 0\.68 meters per second, latest rep$/)).toBeInTheDocument()
298
263
  })
299
264
 
300
265
  it('flags a new set peak on the latest bar', () => {
301
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} expanded />)
266
+ render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} />)
302
267
  expect(
303
- screen.getByLabelText(/Rep 1: 1\.10 meters per second, latest rep, new set peak$/),
268
+ screen.getByLabelText(/Rep 1: 1\.10 meters per second, latest rep, new set peak$/)
304
269
  ).toBeInTheDocument()
305
270
  })
306
271
 
@@ -310,22 +275,356 @@ describe('VelocityStrip live mode', () => {
310
275
  addEventListener: vi.fn(),
311
276
  removeEventListener: vi.fn(),
312
277
  }) as unknown as typeof window.matchMedia
313
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} expanded />)
278
+ render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} />)
314
279
  expect(screen.getByTestId('velocity-bar-0')).toBeInTheDocument()
315
280
  })
316
-
317
- it('does not apply live marking in the mini variant', () => {
318
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} variant="mini" />)
319
- expect(screen.queryByLabelText(/latest rep/)).not.toBeInTheDocument()
320
- })
321
281
  })
322
282
 
323
283
  describe('VelocityStrip all-zero velocities (NaN guard)', () => {
324
- it('emits a valid 0% bar height (not NaN) when every velocity is zero (expanded)', () => {
325
- render(<VelocityStrip velocities={[0, 0, 0]} expanded />)
326
- // Without the guard, 0 / (0 * 1.15) === NaN and the bar height becomes
327
- // 'NaN%' (dropped by jsdom, leaving no height); the guard flattens to '0%'.
284
+ it('emits a valid 0% bar height (not NaN) when every velocity is zero', () => {
285
+ render(<VelocityStrip velocities={[0, 0, 0]} showInfo={false} />)
328
286
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '0%' })
329
287
  expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ height: '0%' })
330
288
  })
331
289
  })
290
+
291
+ // Literal-hex slot colors (must match the component's tokens exactly).
292
+ const TODO_GREY = '#2C2C2C'
293
+ const VARIABLE_CYAN = '#0B3149'
294
+ const CONTINUE_OUTLINE = '#22465F'
295
+ const CONTAINER_GAP_PX = '2px'
296
+ const REP_SLOT_ML = '0px'
297
+ const WIDE_SLOT_ML = '6px'
298
+
299
+ describe('VelocityStrip set-type modes (mini)', () => {
300
+ it('straight: done reps colored + grey todo remainder to the planned count', () => {
301
+ render(
302
+ <VelocityStrip
303
+ set={{ type: 'straight', velocities: [1.1, 0.9, 0.8], planned: 5 }}
304
+ variant="mini"
305
+ />
306
+ )
307
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(3)
308
+ expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(2)
309
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
310
+ expect(screen.getAllByTestId('velocity-slot-todo')[0]).toHaveStyle({
311
+ backgroundColor: TODO_GREY,
312
+ })
313
+ })
314
+
315
+ it('range: committed grey todo + a cyan variable window floor..max', () => {
316
+ render(
317
+ <VelocityStrip
318
+ set={{ type: 'range', velocities: [1.1, 1.0, 0.9, 0.85, 0.8, 0.7], floor: 8, max: 12 }}
319
+ variant="mini"
320
+ />
321
+ )
322
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(6)
323
+ expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(2)
324
+ expect(screen.getAllByTestId('velocity-slot-variable')).toHaveLength(4)
325
+ expect(screen.getAllByTestId('velocity-slot-variable')[0]).toHaveStyle({
326
+ backgroundColor: VARIABLE_CYAN,
327
+ })
328
+ })
329
+
330
+ it('amrap: done reps + one trailing outlined cyan continue slot', () => {
331
+ render(<VelocityStrip set={{ type: 'amrap', velocities: [1.1, 1.0, 0.9] }} variant="mini" />)
332
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(3)
333
+ const cont = screen.getByTestId('velocity-slot-continue')
334
+ expect(cont).toHaveStyle({ backgroundColor: VARIABLE_CYAN })
335
+ expect(cont).toHaveStyle({ borderTopColor: CONTINUE_OUTLINE })
336
+ })
337
+
338
+ it('drop: sub-loads split by a WIDE gap before each load after the first', () => {
339
+ render(
340
+ <VelocityStrip
341
+ set={{
342
+ type: 'drop',
343
+ subloads: [
344
+ [1.0, 0.9],
345
+ [0.8, 0.7],
346
+ [0.6, 0.5],
347
+ ],
348
+ }}
349
+ variant="mini"
350
+ />
351
+ )
352
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(6)
353
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ marginLeft: '0px' })
354
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ marginLeft: REP_SLOT_ML })
355
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
356
+ expect(screen.getByTestId('velocity-bar-4')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
357
+ })
358
+
359
+ it('myo: activation + clusters split by WIDE gaps, open adds a cyan continue', () => {
360
+ render(
361
+ <VelocityStrip
362
+ set={{
363
+ type: 'myo',
364
+ activation: [1.0, 0.9, 0.8],
365
+ clusters: [
366
+ [0.7, 0.6],
367
+ [0.6, 0.5],
368
+ ],
369
+ open: true,
370
+ }}
371
+ variant="mini"
372
+ />
373
+ )
374
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(7)
375
+ expect(screen.getByTestId('velocity-bar-3')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
376
+ expect(screen.getByTestId('velocity-bar-5')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
377
+ expect(screen.getByTestId('velocity-slot-continue')).toBeInTheDocument()
378
+ })
379
+
380
+ it('cluster: fixed count grouped by WIDE intra-rest gaps, grey planned remainder', () => {
381
+ render(
382
+ <VelocityStrip
383
+ set={{
384
+ type: 'cluster',
385
+ velocities: [1.0, 0.95, 0.9, 0.88, 0.85],
386
+ groupSize: 2,
387
+ planned: 8,
388
+ }}
389
+ variant="mini"
390
+ />
391
+ )
392
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(5)
393
+ expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(3)
394
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
395
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ marginLeft: REP_SLOT_ML })
396
+ })
397
+
398
+ it('back-compat: a velocities-only mini strip is unchanged (rep colors, rep gaps only)', () => {
399
+ render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
400
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(sampleVelocities.length)
401
+ expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
402
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ marginLeft: '0px' })
403
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
404
+ expect(screen.getByTestId('velocity-strip-mini')).toHaveStyle({ gap: CONTAINER_GAP_PX })
405
+ })
406
+
407
+ it('derives the summary from a set descriptor (mean / loss info row)', () => {
408
+ render(<VelocityStrip set={{ type: 'straight', velocities: [1.0, 0.8], planned: 5 }} />)
409
+ expect(screen.getByTestId('velocity-info-row')).toHaveTextContent('Loss: 20%')
410
+ })
411
+
412
+ it('labels the strip per set type', () => {
413
+ render(<VelocityStrip set={{ type: 'amrap', velocities: [1.0, 0.9] }} variant="mini" />)
414
+ expect(
415
+ screen.getByLabelText('Velocity strip, AMRAP set, 2 reps and counting')
416
+ ).toBeInTheDocument()
417
+ })
418
+
419
+ it('renders nothing when neither velocities nor set is provided', () => {
420
+ const { container } = render(<VelocityStrip variant="mini" />)
421
+ expect(container.firstChild).toBeNull()
422
+ })
423
+ })
424
+
425
+ describe('VelocityStrip set-type modes (expanded framed)', () => {
426
+ it('straight: velocity-height done reps + short grey planned stubs', () => {
427
+ render(<VelocityStrip set={{ type: 'straight', velocities: [1.1, 0.8], planned: 4 }} />)
428
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(2)
429
+ expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(2)
430
+ expect(screen.getAllByTestId('velocity-slot-todo')[0]).toHaveStyle({
431
+ backgroundColor: TODO_GREY,
432
+ })
433
+ })
434
+
435
+ it('advanced types render their slot encoding in the framed view', () => {
436
+ render(
437
+ <VelocityStrip
438
+ set={{
439
+ type: 'drop',
440
+ subloads: [
441
+ [1.0, 0.9],
442
+ [0.8, 0.7],
443
+ ],
444
+ }}
445
+ />
446
+ )
447
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(4)
448
+ expect(screen.getByTestId('velocity-bar-2')).toBeInTheDocument()
449
+ })
450
+ })
451
+
452
+ describe('VelocityStrip expanded bare strip (spotlight: numbers + info off)', () => {
453
+ const bareProps = { variant: 'expanded' as const, showNumbers: false, showInfo: false }
454
+
455
+ it('renders the bare strip container (no framed chart, no mini)', () => {
456
+ render(
457
+ <VelocityStrip
458
+ {...bareProps}
459
+ set={{ type: 'straight', velocities: [1.0, 0.8], planned: 2 }}
460
+ />
461
+ )
462
+ expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
463
+ expect(screen.queryByTestId('velocity-strip-mini')).not.toBeInTheDocument()
464
+ expect(screen.queryByTestId('velocity-strip')).not.toBeInTheDocument()
465
+ })
466
+
467
+ it('scales bar height to velocity against the fixed ceiling at the given height', () => {
468
+ render(
469
+ <VelocityStrip
470
+ {...bareProps}
471
+ scale="fixed"
472
+ height={24}
473
+ set={{ type: 'straight', velocities: [1.15, 0.575], planned: 2 }}
474
+ />
475
+ )
476
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '24px' })
477
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ height: '12px' })
478
+ })
479
+
480
+ it('honors height for the scaling', () => {
481
+ render(
482
+ <VelocityStrip
483
+ {...bareProps}
484
+ scale="fixed"
485
+ height={40}
486
+ set={{ type: 'straight', velocities: [1.15], planned: 1 }}
487
+ />
488
+ )
489
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '40px' })
490
+ })
491
+
492
+ it('draws planned-but-undone reps as short grey todo stubs (set-type aware)', () => {
493
+ render(
494
+ <VelocityStrip
495
+ {...bareProps}
496
+ height={24}
497
+ set={{ type: 'straight', velocities: [1.0], planned: 3 }}
498
+ />
499
+ )
500
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(1)
501
+ const stubs = screen.getAllByTestId('velocity-slot-todo')
502
+ expect(stubs).toHaveLength(2)
503
+ expect(stubs[0]).toHaveStyle({ height: '3px', backgroundColor: TODO_GREY })
504
+ })
505
+
506
+ it('colors performed reps from the default effort scale', () => {
507
+ render(
508
+ <VelocityStrip
509
+ {...bareProps}
510
+ set={{ type: 'straight', velocities: [1.1, 0.45], planned: 2 }}
511
+ />
512
+ )
513
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
514
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#D14343' })
515
+ })
516
+
517
+ it('carries the set-type WIDE-gap chunk encoding (drop sub-loads)', () => {
518
+ render(
519
+ <VelocityStrip
520
+ {...bareProps}
521
+ set={{
522
+ type: 'drop',
523
+ subloads: [
524
+ [1.0, 0.9],
525
+ [0.8, 0.7],
526
+ ],
527
+ }}
528
+ />
529
+ )
530
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ marginLeft: REP_SLOT_ML })
531
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
532
+ })
533
+
534
+ it('has no chrome: no per-bar labels, no info row, no pressable', () => {
535
+ render(
536
+ <VelocityStrip
537
+ {...bareProps}
538
+ onToggle={() => {}}
539
+ set={{ type: 'straight', velocities: [1.0, 0.8], planned: 3 }}
540
+ />
541
+ )
542
+ expect(screen.queryByTestId('velocity-label-0')).not.toBeInTheDocument()
543
+ expect(screen.queryByTestId('velocity-info-row')).not.toBeInTheDocument()
544
+ expect(screen.queryByTestId('velocity-strip-pressable')).not.toBeInTheDocument()
545
+ })
546
+
547
+ it('labels the strip per set type for accessibility', () => {
548
+ render(
549
+ <VelocityStrip
550
+ {...bareProps}
551
+ set={{ type: 'straight', velocities: [1.0, 0.8], planned: 3 }}
552
+ />
553
+ )
554
+ expect(screen.getByLabelText('Velocity strip, straight set, 2 reps')).toBeInTheDocument()
555
+ })
556
+
557
+ it('has no accessibility violations', async () => {
558
+ const { container } = render(
559
+ <VelocityStrip
560
+ {...bareProps}
561
+ set={{ type: 'straight', velocities: [1.0, 0.8], planned: 3 }}
562
+ />
563
+ )
564
+ expect(await axe(container)).toHaveNoViolations()
565
+ })
566
+ })
567
+
568
+ describe('VelocityStrip hero variant', () => {
569
+ const heroSet = [0.9, 0.86, 0.82, 0.78]
570
+
571
+ it('renders one bar per performed rep, each with a value label', () => {
572
+ render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={8} />)
573
+ expect(screen.getByTestId('velocity-strip-hero')).toBeInTheDocument()
574
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(heroSet.length)
575
+ expect(screen.getAllByTestId(/^velocity-label-\d+$/)).toHaveLength(heroSet.length)
576
+ expect(screen.getByTestId('velocity-label-0')).toHaveTextContent('0.90')
577
+ })
578
+
579
+ it('draws a dashed placeholder for each rep still to come (target − done)', () => {
580
+ render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={8} />)
581
+ expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(4)
582
+ })
583
+
584
+ it('draws no placeholders when the target is met exactly', () => {
585
+ render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={heroSet.length} />)
586
+ expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
587
+ })
588
+
589
+ it('set expansion: reps beyond target render as bars with no negative placeholders', () => {
590
+ const overflow = [0.9, 0.86, 0.82, 0.78, 0.74, 0.7, 0.66, 0.62, 0.58]
591
+ render(<VelocityStrip velocities={overflow} variant="hero" targetReps={8} />)
592
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(overflow.length)
593
+ expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
594
+ })
595
+
596
+ it('draws no placeholders when targetReps is omitted', () => {
597
+ render(<VelocityStrip velocities={heroSet} variant="hero" />)
598
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(heroSet.length)
599
+ expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
600
+ })
601
+
602
+ it('renders the running-best reference line when there is a positive best', () => {
603
+ render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={8} />)
604
+ expect(screen.getByTestId('velocity-hero-reference')).toBeInTheDocument()
605
+ })
606
+
607
+ it('omits the reference line when every velocity is zero (NaN / no-best guard)', () => {
608
+ render(<VelocityStrip velocities={[0, 0, 0]} variant="hero" targetReps={4} />)
609
+ expect(screen.queryByTestId('velocity-hero-reference')).not.toBeInTheDocument()
610
+ })
611
+
612
+ it('summarizes reps done, target and best in the accessibility label', () => {
613
+ render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={8} />)
614
+ expect(
615
+ screen.getByLabelText('Velocity chart, 4 of 8 reps, best 0.90 meters per second')
616
+ ).toBeInTheDocument()
617
+ })
618
+
619
+ it('renders nothing when neither velocities nor set is provided', () => {
620
+ render(<VelocityStrip variant="hero" targetReps={8} />)
621
+ expect(screen.queryByTestId('velocity-strip-hero')).not.toBeInTheDocument()
622
+ })
623
+
624
+ it('has no accessibility violations', async () => {
625
+ const { container } = render(
626
+ <VelocityStrip velocities={heroSet} variant="hero" targetReps={8} liveRepIndex={3} />
627
+ )
628
+ expect(await axe(container)).toHaveNoViolations()
629
+ })
630
+ })