@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
@@ -7,6 +7,7 @@ import {
7
7
  getVelocityZoneName,
8
8
  calculateVelocityLoss,
9
9
  calculateMeanVelocity,
10
+ getVelocityLossColor,
10
11
  } from './VelocityStrip'
11
12
 
12
13
  const sampleVelocities = [1.1, 0.95, 0.82, 0.68, 0.45]
@@ -29,7 +30,7 @@ describe('VelocityStrip expanded (framed chart)', () => {
29
30
  ).toBeInTheDocument()
30
31
  })
31
32
 
32
- it('renders the 3px collapsed strip when expanded is false', () => {
33
+ it('collapses to the flat state (tap-to-expand, no info row) when expanded is false', () => {
33
34
  render(<VelocityStrip velocities={sampleVelocities} expanded={false} />)
34
35
  expect(
35
36
  screen.getByLabelText('Velocity chart for set, 5 reps, tap to expand')
@@ -78,17 +79,28 @@ describe('VelocityStrip expanded (framed chart)', () => {
78
79
  })
79
80
 
80
81
  describe('scale', () => {
81
- it('peak (default) scales the tallest rep to nearly full height', () => {
82
- // maxV=1.0 -> denom 1.06 (peak headroom) -> 1.0/1.06 = 94%.
83
- render(<VelocityStrip velocities={[1.0, 0.5]} showInfo={false} />)
84
- expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '94%' })
82
+ // Folded onto SetBarChart: bars are px value-height (not %); the bare spotlight has static
83
+ // heights (no expand animation), so we assert the taller-rep-reads-taller scaling relationship.
84
+ const barPx = (id: string) => parseFloat(getComputedStyle(screen.getByTestId(id)).height)
85
+ const bareScale = {
86
+ variant: 'expanded' as const,
87
+ showNumbers: false,
88
+ showInfo: false,
89
+ height: 60,
90
+ }
91
+
92
+ it('peak (default) scales the tallest rep taller than a smaller one', () => {
93
+ render(<VelocityStrip {...bareScale} velocities={[1.0, 0.5]} />)
94
+ expect(barPx('velocity-bar-0')).toBeGreaterThan(barPx('velocity-bar-1'))
95
+ // peak: 1.0 fills nearly the whole 60px plot.
96
+ expect(barPx('velocity-bar-0')).toBeGreaterThan(52)
85
97
  })
86
98
 
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%' })
99
+ it('fixed scales against a ceiling above the set max (bars shorter than peak)', () => {
100
+ render(<VelocityStrip {...bareScale} scale="fixed" velocities={[1.0, 0.5]} />)
101
+ // fixed ceiling 1.15 > 1.0, so the tallest bar no longer fills the plot; 0.5 is ~half of 1.0.
102
+ expect(barPx('velocity-bar-0')).toBeLessThan(56)
103
+ expect(barPx('velocity-bar-1')).toBeLessThan(barPx('velocity-bar-0'))
92
104
  })
93
105
  })
94
106
 
@@ -116,31 +128,31 @@ describe('VelocityStrip expanded (framed chart)', () => {
116
128
  })
117
129
  })
118
130
 
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()
131
+ describe('VelocityStrip compact variant (flat resting strip)', () => {
132
+ it('renders the flat compact strip', () => {
133
+ render(<VelocityStrip velocities={sampleVelocities} variant="compact" />)
134
+ expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
123
135
  })
124
136
 
125
137
  it('does not respond to press', () => {
126
- render(<VelocityStrip velocities={sampleVelocities} variant="mini" onToggle={() => {}} />)
138
+ render(<VelocityStrip velocities={sampleVelocities} variant="compact" onToggle={() => {}} />)
127
139
  expect(screen.queryByTestId('velocity-strip-pressable')).not.toBeInTheDocument()
128
140
  })
129
141
 
130
142
  it('renders one bar per rep', () => {
131
- render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
143
+ render(<VelocityStrip velocities={sampleVelocities} variant="compact" />)
132
144
  for (let i = 0; i < sampleVelocities.length; i++) {
133
145
  expect(screen.getByTestId(`velocity-bar-${i}`)).toBeInTheDocument()
134
146
  }
135
147
  })
136
148
 
137
149
  it('has no accessibility violations', async () => {
138
- const { container } = render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
150
+ const { container } = render(<VelocityStrip velocities={sampleVelocities} variant="compact" />)
139
151
  expect(await axe(container)).toHaveNoViolations()
140
152
  })
141
153
 
142
- it('does not apply live marking', () => {
143
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} variant="mini" />)
154
+ it('does not apply live marking (compact is the resting form)', () => {
155
+ render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} variant="compact" />)
144
156
  expect(screen.queryByLabelText(/latest rep/)).not.toBeInTheDocument()
145
157
  })
146
158
  })
@@ -216,6 +228,94 @@ describe('calculateMeanVelocity', () => {
216
228
  })
217
229
  })
218
230
 
231
+ // Literal hex for the four VL-band tones (green-300 / amber-300 / orange-400 / red-600),
232
+ // the SAME stops FatigueMeter's VL10/VL20/VL30 default thresholds use.
233
+ const VL_GREEN = '#2ED573'
234
+ const VL_YELLOW = '#F9B415'
235
+ const VL_ORANGE = '#FF7900'
236
+ const VL_RED = '#D14343'
237
+
238
+ describe('getVelocityLossColor', () => {
239
+ it('reads green below the VL10 threshold', () => {
240
+ expect(getVelocityLossColor(0)).toBe(VL_GREEN)
241
+ expect(getVelocityLossColor(9.9)).toBe(VL_GREEN)
242
+ })
243
+
244
+ it('reads gold/amber from VL10 up to (not including) VL20', () => {
245
+ expect(getVelocityLossColor(10)).toBe(VL_YELLOW)
246
+ expect(getVelocityLossColor(19.9)).toBe(VL_YELLOW)
247
+ })
248
+
249
+ it('reads orange from VL20 up to (not including) VL30', () => {
250
+ expect(getVelocityLossColor(20)).toBe(VL_ORANGE)
251
+ expect(getVelocityLossColor(29.9)).toBe(VL_ORANGE)
252
+ })
253
+
254
+ it('reads red at and past VL30', () => {
255
+ expect(getVelocityLossColor(30)).toBe(VL_RED)
256
+ expect(getVelocityLossColor(100)).toBe(VL_RED)
257
+ })
258
+ })
259
+
260
+ describe('VelocityStrip barColor prop', () => {
261
+ // Best rep is 1.0 m/s, so per-rep loss lands exactly on the VL10/20/30 boundaries:
262
+ // 0%, 10%, 20%, 30% loss — one rep in each band.
263
+ const decliningSet = [1.0, 0.9, 0.8, 0.7]
264
+
265
+ it('defaults to loss coloring (the landed default is barColor="loss")', () => {
266
+ render(<VelocityStrip velocities={decliningSet} variant="compact" />)
267
+ // Loss from the set's own best (1.0): 0% / 10% / 20% / 30% -> green / yellow / orange / red.
268
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: VL_GREEN })
269
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: VL_YELLOW })
270
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ backgroundColor: VL_ORANGE })
271
+ expect(screen.getByTestId('velocity-bar-3')).toHaveStyle({ backgroundColor: VL_RED })
272
+ })
273
+
274
+ it('explicit barColor="zone" matches the default', () => {
275
+ render(<VelocityStrip velocities={decliningSet} variant="compact" barColor="zone" />)
276
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: VL_YELLOW })
277
+ })
278
+
279
+ it('barColor="loss" colors each bar green→red by loss from the set\'s own best', () => {
280
+ render(<VelocityStrip velocities={decliningSet} variant="compact" barColor="loss" />)
281
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: VL_GREEN })
282
+ expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: VL_YELLOW })
283
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ backgroundColor: VL_ORANGE })
284
+ expect(screen.getByTestId('velocity-bar-3')).toHaveStyle({ backgroundColor: VL_RED })
285
+ })
286
+
287
+ it('barColor="loss" reads a slow-but-still-best rep as green (loss-relative, not absolute)', () => {
288
+ // 0.3 m/s would be "vel-red" under the absolute zone scale, but as the set's
289
+ // (single) best rep its loss is 0 — loss-relative coloring reads it green.
290
+ render(<VelocityStrip velocities={[0.3]} variant="compact" barColor="loss" />)
291
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: VL_GREEN })
292
+ })
293
+
294
+ it('barColor="loss" ignores a supplied zones prop (it is a separate scale)', () => {
295
+ render(
296
+ <VelocityStrip
297
+ velocities={decliningSet}
298
+ variant="compact"
299
+ barColor="loss"
300
+ zones={compoundBands}
301
+ />
302
+ )
303
+ expect(screen.getByTestId('velocity-bar-3')).toHaveStyle({ backgroundColor: VL_RED })
304
+ })
305
+
306
+ it('barColor="loss" guards all-zero velocities to green (no NaN/best<=0)', () => {
307
+ render(<VelocityStrip velocities={[0, 0, 0]} variant="compact" barColor="loss" />)
308
+ expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: VL_GREEN })
309
+ expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ backgroundColor: VL_GREEN })
310
+ })
311
+
312
+ it('renders no bars (and does not crash) for an empty velocities array', () => {
313
+ render(<VelocityStrip velocities={[]} variant="compact" barColor="loss" />)
314
+ expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
315
+ expect(screen.queryAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(0)
316
+ })
317
+ })
318
+
219
319
  // WA-shaped 5-band zone set (compound movement-class defaults, mean m/s).
220
320
  const compoundBands = [
221
321
  { id: 'grinding', label: 'Grinding', min: 0, max: 0.35 },
@@ -226,14 +326,29 @@ const compoundBands = [
226
326
  ] as const
227
327
 
228
328
  describe('VelocityStrip zones prop', () => {
329
+ // The band mapping is the `zone` scale; the landed default is `loss`, so these pass `barColor="zone"`.
229
330
  it('colors bars from the supplied bands (mini)', () => {
230
- render(<VelocityStrip velocities={[1.1, 0.45]} zones={compoundBands} variant="mini" />)
331
+ render(
332
+ <VelocityStrip
333
+ velocities={[1.1, 0.45]}
334
+ zones={compoundBands}
335
+ variant="compact"
336
+ barColor="zone"
337
+ />
338
+ )
231
339
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
232
340
  expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#D14343' })
233
341
  })
234
342
 
235
343
  it('gives grinding and maximalStrength DISTINCT reds (5-band, no collapse)', () => {
236
- render(<VelocityStrip velocities={[0.45, 0.2]} zones={compoundBands} variant="mini" />)
344
+ render(
345
+ <VelocityStrip
346
+ velocities={[0.45, 0.2]}
347
+ zones={compoundBands}
348
+ variant="compact"
349
+ barColor="zone"
350
+ />
351
+ )
237
352
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#D14343' })
238
353
  expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#A4221C' })
239
354
  })
@@ -244,7 +359,7 @@ describe('VelocityStrip zones prop', () => {
244
359
  })
245
360
 
246
361
  it('falls back to the default scale when zones is absent', () => {
247
- render(<VelocityStrip velocities={[1.1]} variant="mini" />)
362
+ render(<VelocityStrip velocities={[1.1]} variant="compact" />)
248
363
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
249
364
  })
250
365
  })
@@ -257,16 +372,17 @@ describe('VelocityStrip live mode', () => {
257
372
  else delete (window as { matchMedia?: unknown }).matchMedia
258
373
  })
259
374
 
260
- it('marks the latest rep bar in its accessibility label', () => {
261
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={3} />)
262
- expect(screen.getByLabelText(/Rep 4: 0\.68 meters per second, latest rep$/)).toBeInTheDocument()
375
+ // The bars are now SetBarChart's (a11y-hidden; live-grow is the visual marking via `liveRepIndex`).
376
+ const spotlight = { variant: 'expanded' as const, showNumbers: false, showInfo: false }
377
+
378
+ it('renders the newest rep in the live spotlight', () => {
379
+ render(<VelocityStrip {...spotlight} velocities={sampleVelocities} liveRepIndex={3} />)
380
+ expect(screen.getByTestId('velocity-bar-3')).toBeInTheDocument()
263
381
  })
264
382
 
265
- it('flags a new set peak on the latest bar', () => {
266
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} />)
267
- expect(
268
- screen.getByLabelText(/Rep 1: 1\.10 meters per second, latest rep, new set peak$/)
269
- ).toBeInTheDocument()
383
+ it('renders a new-set-peak live rep without crashing', () => {
384
+ render(<VelocityStrip {...spotlight} velocities={sampleVelocities} liveRepIndex={0} />)
385
+ expect(screen.getByTestId('velocity-bar-0')).toBeInTheDocument()
270
386
  })
271
387
 
272
388
  it('renders without animation when prefers-reduced-motion is set', () => {
@@ -275,48 +391,59 @@ describe('VelocityStrip live mode', () => {
275
391
  addEventListener: vi.fn(),
276
392
  removeEventListener: vi.fn(),
277
393
  }) as unknown as typeof window.matchMedia
278
- render(<VelocityStrip velocities={sampleVelocities} liveRepIndex={0} />)
394
+ render(<VelocityStrip {...spotlight} velocities={sampleVelocities} liveRepIndex={0} />)
279
395
  expect(screen.getByTestId('velocity-bar-0')).toBeInTheDocument()
280
396
  })
281
397
  })
282
398
 
283
399
  describe('VelocityStrip all-zero velocities (NaN guard)', () => {
284
- it('emits a valid 0% bar height (not NaN) when every velocity is zero', () => {
285
- render(<VelocityStrip velocities={[0, 0, 0]} showInfo={false} />)
286
- expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ height: '0%' })
287
- expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ height: '0%' })
400
+ it('emits a valid (non-NaN) bar height when every velocity is zero', () => {
401
+ render(
402
+ <VelocityStrip
403
+ velocities={[0, 0, 0]}
404
+ variant="expanded"
405
+ showNumbers={false}
406
+ showInfo={false}
407
+ />
408
+ )
409
+ const h = getComputedStyle(screen.getByTestId('velocity-bar-0')).height
410
+ expect(h).not.toContain('NaN')
411
+ expect(parseFloat(h)).toBeGreaterThanOrEqual(0)
288
412
  })
289
413
  })
290
414
 
291
415
  // Literal-hex slot colors (must match the component's tokens exactly).
292
- const TODO_GREY = '#2C2C2C'
416
+ // TODO_SOLID = the LANDED surface-relative solid to-do tone (base/dark plane blended toward the
417
+ // on-surface neutral) — replaced the old fixed grey #2C2C2C. Solid fill, not a dashed outline.
418
+ // Pinned rather than recomputed on purpose: deriving it here would just mirror
419
+ // SetBarChart's mixHex and stop catching a change to the blend itself.
420
+ // = mixHex(grey-925, text-tertiary, 0.55); moved with the warm ramp (TD-07.14).
421
+ const TODO_SOLID = '#5B5957'
293
422
  const VARIABLE_CYAN = '#0B3149'
294
423
  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', () => {
424
+ describe('VelocityStrip set-type modes (compact)', () => {
425
+ // compact composes SetBarChart, so the per-slot geometry (WIDE chunk-notch px, container gap) is
426
+ // covered in SetBarChart.test; here we verify VelocityStrip maps each set type to the right slots.
427
+ it('straight: done reps colored + solid todo remainder to the planned count', () => {
301
428
  render(
302
429
  <VelocityStrip
303
430
  set={{ type: 'straight', velocities: [1.1, 0.9, 0.8], planned: 5 }}
304
- variant="mini"
431
+ variant="compact"
305
432
  />
306
433
  )
307
434
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(3)
308
435
  expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(2)
309
436
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
310
437
  expect(screen.getAllByTestId('velocity-slot-todo')[0]).toHaveStyle({
311
- backgroundColor: TODO_GREY,
438
+ backgroundColor: TODO_SOLID,
312
439
  })
313
440
  })
314
441
 
315
- it('range: committed grey todo + a cyan variable window floor..max', () => {
442
+ it('range: committed reps + solid floor todo + a cyan variable window floor..max', () => {
316
443
  render(
317
444
  <VelocityStrip
318
445
  set={{ type: 'range', velocities: [1.1, 1.0, 0.9, 0.85, 0.8, 0.7], floor: 8, max: 12 }}
319
- variant="mini"
446
+ variant="compact"
320
447
  />
321
448
  )
322
449
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(6)
@@ -327,15 +454,15 @@ describe('VelocityStrip set-type modes (mini)', () => {
327
454
  })
328
455
  })
329
456
 
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" />)
457
+ it('amrap: done reps + one trailing cyan-outline continue slot', () => {
458
+ render(<VelocityStrip set={{ type: 'amrap', velocities: [1.1, 1.0, 0.9] }} variant="compact" />)
332
459
  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 })
460
+ expect(screen.getByTestId('velocity-slot-continue')).toHaveStyle({
461
+ borderTopColor: CONTINUE_OUTLINE,
462
+ })
336
463
  })
337
464
 
338
- it('drop: sub-loads split by a WIDE gap before each load after the first', () => {
465
+ it('drop: one bar per rep across the sub-loads', () => {
339
466
  render(
340
467
  <VelocityStrip
341
468
  set={{
@@ -346,17 +473,13 @@ describe('VelocityStrip set-type modes (mini)', () => {
346
473
  [0.6, 0.5],
347
474
  ],
348
475
  }}
349
- variant="mini"
476
+ variant="compact"
350
477
  />
351
478
  )
352
479
  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
480
  })
358
481
 
359
- it('myo: activation + clusters split by WIDE gaps, open adds a cyan continue', () => {
482
+ it('myo: activation + clusters as reps, open adds a cyan continue', () => {
360
483
  render(
361
484
  <VelocityStrip
362
485
  set={{
@@ -368,16 +491,14 @@ describe('VelocityStrip set-type modes (mini)', () => {
368
491
  ],
369
492
  open: true,
370
493
  }}
371
- variant="mini"
494
+ variant="compact"
372
495
  />
373
496
  )
374
497
  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
498
  expect(screen.getByTestId('velocity-slot-continue')).toBeInTheDocument()
378
499
  })
379
500
 
380
- it('cluster: fixed count grouped by WIDE intra-rest gaps, grey planned remainder', () => {
501
+ it('cluster: fixed count + solid planned remainder', () => {
381
502
  render(
382
503
  <VelocityStrip
383
504
  set={{
@@ -386,22 +507,19 @@ describe('VelocityStrip set-type modes (mini)', () => {
386
507
  groupSize: 2,
387
508
  planned: 8,
388
509
  }}
389
- variant="mini"
510
+ variant="compact"
390
511
  />
391
512
  )
392
513
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(5)
393
514
  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
515
  })
397
516
 
398
- it('back-compat: a velocities-only mini strip is unchanged (rep colors, rep gaps only)', () => {
399
- render(<VelocityStrip velocities={sampleVelocities} variant="mini" />)
517
+ it('back-compat: a velocities-only compact strip renders rep bars, no todo', () => {
518
+ render(<VelocityStrip velocities={sampleVelocities} variant="compact" />)
400
519
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(sampleVelocities.length)
401
520
  expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
402
- expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ marginLeft: '0px' })
403
521
  expect(screen.getByTestId('velocity-bar-0')).toHaveStyle({ backgroundColor: '#2ED573' })
404
- expect(screen.getByTestId('velocity-strip-mini')).toHaveStyle({ gap: CONTAINER_GAP_PX })
522
+ expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
405
523
  })
406
524
 
407
525
  it('derives the summary from a set descriptor (mean / loss info row)', () => {
@@ -410,14 +528,14 @@ describe('VelocityStrip set-type modes (mini)', () => {
410
528
  })
411
529
 
412
530
  it('labels the strip per set type', () => {
413
- render(<VelocityStrip set={{ type: 'amrap', velocities: [1.0, 0.9] }} variant="mini" />)
531
+ render(<VelocityStrip set={{ type: 'amrap', velocities: [1.0, 0.9] }} variant="compact" />)
414
532
  expect(
415
533
  screen.getByLabelText('Velocity strip, AMRAP set, 2 reps and counting')
416
534
  ).toBeInTheDocument()
417
535
  })
418
536
 
419
537
  it('renders nothing when neither velocities nor set is provided', () => {
420
- const { container } = render(<VelocityStrip variant="mini" />)
538
+ const { container } = render(<VelocityStrip variant="compact" />)
421
539
  expect(container.firstChild).toBeNull()
422
540
  })
423
541
  })
@@ -428,7 +546,7 @@ describe('VelocityStrip set-type modes (expanded framed)', () => {
428
546
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(2)
429
547
  expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(2)
430
548
  expect(screen.getAllByTestId('velocity-slot-todo')[0]).toHaveStyle({
431
- backgroundColor: TODO_GREY,
549
+ backgroundColor: TODO_SOLID,
432
550
  })
433
551
  })
434
552
 
@@ -459,8 +577,8 @@ describe('VelocityStrip expanded bare strip (spotlight: numbers + info off)', ()
459
577
  set={{ type: 'straight', velocities: [1.0, 0.8], planned: 2 }}
460
578
  />
461
579
  )
462
- expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
463
- expect(screen.queryByTestId('velocity-strip-mini')).not.toBeInTheDocument()
580
+ expect(screen.getByTestId('velocity-strip-spotlight')).toBeInTheDocument()
581
+ expect(screen.queryByTestId('velocity-strip-compact')).not.toBeInTheDocument()
464
582
  expect(screen.queryByTestId('velocity-strip')).not.toBeInTheDocument()
465
583
  })
466
584
 
@@ -500,7 +618,8 @@ describe('VelocityStrip expanded bare strip (spotlight: numbers + info off)', ()
500
618
  expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(1)
501
619
  const stubs = screen.getAllByTestId('velocity-slot-todo')
502
620
  expect(stubs).toHaveLength(2)
503
- expect(stubs[0]).toHaveStyle({ height: '3px', backgroundColor: TODO_GREY })
621
+ // Folded onto SetBarChart: the solid to-do stub is the shared 9px height + surface-relative tone.
622
+ expect(stubs[0]).toHaveStyle({ height: '9px', backgroundColor: TODO_SOLID })
504
623
  })
505
624
 
506
625
  it('colors performed reps from the default effort scale', () => {
@@ -514,7 +633,7 @@ describe('VelocityStrip expanded bare strip (spotlight: numbers + info off)', ()
514
633
  expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ backgroundColor: '#D14343' })
515
634
  })
516
635
 
517
- it('carries the set-type WIDE-gap chunk encoding (drop sub-loads)', () => {
636
+ it('carries the set-type chunk encoding (drop sub-loads) as one bar per rep', () => {
518
637
  render(
519
638
  <VelocityStrip
520
639
  {...bareProps}
@@ -527,8 +646,9 @@ describe('VelocityStrip expanded bare strip (spotlight: numbers + info off)', ()
527
646
  }}
528
647
  />
529
648
  )
530
- expect(screen.getByTestId('velocity-bar-1')).toHaveStyle({ marginLeft: REP_SLOT_ML })
531
- expect(screen.getByTestId('velocity-bar-2')).toHaveStyle({ marginLeft: WIDE_SLOT_ML })
649
+ // Folded onto SetBarChart: 4 rep bars; the WIDE chunk-notch (now a proportional column margin)
650
+ // is covered in SetBarChart.test.
651
+ expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(4)
532
652
  })
533
653
 
534
654
  it('has no chrome: no per-bar labels, no info row, no pressable', () => {
@@ -576,7 +696,7 @@ describe('VelocityStrip hero variant', () => {
576
696
  expect(screen.getByTestId('velocity-label-0')).toHaveTextContent('0.90')
577
697
  })
578
698
 
579
- it('draws a dashed placeholder for each rep still to come (target − done)', () => {
699
+ it('draws a to-do placeholder for each rep still to come (target − done)', () => {
580
700
  render(<VelocityStrip velocities={heroSet} variant="hero" targetReps={8} />)
581
701
  expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(4)
582
702
  })