@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
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SetBar, velocityZoneColor, SET_STRIP_ZONES, SET_STRIP_VARIABLE_COLOR } from './SetBar'
5
+
6
+ /** The slot wrapper (carrier of the leading-gap margin) is a fill segment's parent. */
7
+ const slotOf = (fill: HTMLElement) => fill.parentElement as HTMLElement
8
+
9
+ describe('SetBar', () => {
10
+ describe('velocityZoneColor', () => {
11
+ it('maps velocity ratios to their zone pins', () => {
12
+ expect(velocityZoneColor(0.4)).toBe(SET_STRIP_ZONES.slow)
13
+ expect(velocityZoneColor(0.6)).toBe(SET_STRIP_ZONES.moderate)
14
+ expect(velocityZoneColor(0.85)).toBe(SET_STRIP_ZONES.fast)
15
+ expect(velocityZoneColor(1.2)).toBe(SET_STRIP_ZONES.fastest)
16
+ })
17
+ })
18
+
19
+ it('done: one colored fill segment per rep, no empties, no pulse', () => {
20
+ render(<SetBar set={{ status: 'done', velocities: [1, 0.8, 0.6, 0.4] }} />)
21
+ expect(screen.getAllByTestId('set-strip-fill')).toHaveLength(4)
22
+ expect(screen.queryByTestId('set-strip-empty')).not.toBeInTheDocument()
23
+ expect(screen.queryByTestId('set-strip-pulse')).not.toBeInTheDocument()
24
+ })
25
+
26
+ it('active: performed reps pulse, the remainder is greyed', () => {
27
+ render(<SetBar set={{ status: 'active', velocities: [0.9, 0.7, 0.5], planned: 8 }} />)
28
+ expect(screen.getAllByTestId('set-strip-pulse')).toHaveLength(3)
29
+ expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(5)
30
+ })
31
+
32
+ it('todo: a single grey bar, no fills or pulses', () => {
33
+ render(<SetBar set={{ status: 'todo', planned: 10 }} />)
34
+ expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(1)
35
+ expect(screen.queryByTestId('set-strip-fill')).not.toBeInTheDocument()
36
+ expect(screen.queryByTestId('set-strip-pulse')).not.toBeInTheDocument()
37
+ })
38
+
39
+ describe('range (variable rep-range)', () => {
40
+ it('renders range-max segments; committed-todo grey, variable-todo cyan', () => {
41
+ // floor 15, max 20, 12 done → 12 velocity fills · 3 grey (committed) · 5 cyan (variable)
42
+ render(<SetBar set={{ status: 'range', floor: 15, max: 20, doneVels: Array(12).fill(0.9) }} />)
43
+ expect(screen.getAllByTestId('set-strip-fill')).toHaveLength(12)
44
+ expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(3)
45
+ expect(screen.getAllByTestId('set-strip-variable')).toHaveLength(5)
46
+ })
47
+
48
+ it('counts a done rep past the floor as a real velocity rep (not variable)', () => {
49
+ // 17 done into the variable zone → 17 velocity fills, 3 cyan, 0 grey
50
+ render(<SetBar set={{ status: 'range', floor: 15, max: 20, doneVels: Array(17).fill(0.8) }} />)
51
+ expect(screen.getAllByTestId('set-strip-fill')).toHaveLength(17)
52
+ expect(screen.getAllByTestId('set-strip-variable')).toHaveLength(3)
53
+ expect(screen.queryByTestId('set-strip-empty')).not.toBeInTheDocument()
54
+ })
55
+
56
+ it('uses the variable/opportunity cyan pin for the variable-todo zone', () => {
57
+ render(<SetBar set={{ status: 'range', floor: 3, max: 4, doneVels: [] }} />)
58
+ expect(SET_STRIP_VARIABLE_COLOR).toBe('#0B3149') // cyan 900
59
+ expect(screen.getByTestId('set-strip-variable')).toHaveStyle({
60
+ backgroundColor: SET_STRIP_VARIABLE_COLOR,
61
+ })
62
+ })
63
+ })
64
+
65
+ describe('drop set', () => {
66
+ it('renders one fill per rep across sub-loads, split by 2px notches', () => {
67
+ render(<SetBar set={{ status: 'drop', subloads: [[0.85, 0.8, 0.7], [0.7, 0.6]] }} />)
68
+ const fills = screen.getAllByTestId('set-strip-fill')
69
+ expect(fills).toHaveLength(5)
70
+ expect(slotOf(fills[0])).not.toHaveStyle({ marginLeft: '2px' }) // first sub-load butts the edge
71
+ expect(slotOf(fills[3])).toHaveStyle({ marginLeft: '2px' }) // second sub-load opens with a notch
72
+ })
73
+ })
74
+
75
+ describe('myo-rep / rest-pause (done)', () => {
76
+ it('renders activation + clusters, split by 3px cluster gaps', () => {
77
+ render(<SetBar set={{ status: 'myo', activation: [0.75, 0.7, 0.65], clusters: [[0.6, 0.55], [0.5, 0.45]] }} />)
78
+ const fills = screen.getAllByTestId('set-strip-fill')
79
+ expect(fills).toHaveLength(7)
80
+ expect(slotOf(fills[3])).toHaveStyle({ marginLeft: '3px' }) // first cluster
81
+ expect(slotOf(fills[5])).toHaveStyle({ marginLeft: '3px' }) // second cluster
82
+ })
83
+ })
84
+
85
+ describe('myo-upcoming (planned, length unknown)', () => {
86
+ it('renders a grey activation chunk + a fading cyan clusters-to-failure trail', () => {
87
+ render(<SetBar set={{ status: 'myo-upcoming', activationLen: 4, clusterCount: 2 }} />)
88
+ expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(4) // grey activation
89
+ const trail = screen.getAllByTestId('set-strip-variable')
90
+ expect(trail).toHaveLength(10) // 2 clusters × 5
91
+ })
92
+
93
+ it('fades each expected cluster and opens the right edge with a cluster gap', () => {
94
+ render(<SetBar set={{ status: 'myo-upcoming', activationLen: 4, clusterCount: 2 }} />)
95
+ const trail = screen.getAllByTestId('set-strip-variable')
96
+ expect(trail[5]).toHaveStyle({ opacity: 0.6 }) // second cluster is recessive
97
+ expect(slotOf(trail[0])).toHaveStyle({ marginLeft: '3px' }) // trail opens after activation
98
+ expect(slotOf(trail[5])).toHaveStyle({ marginLeft: '3px' }) // each cluster is gapped
99
+ })
100
+
101
+ it('defaults to three expected clusters when clusterCount is omitted', () => {
102
+ render(<SetBar set={{ status: 'myo-upcoming', activationLen: 3 }} />)
103
+ expect(screen.getAllByTestId('set-strip-variable')).toHaveLength(15) // 3 × 5
104
+ })
105
+ })
106
+
107
+ it('defaults to an 8px height and honors the height prop', () => {
108
+ const { rerender } = render(<SetBar set={{ status: 'todo', planned: 3 }} />)
109
+ expect(screen.getByTestId('set-strip-set')).toHaveStyle({ height: '8px' })
110
+ rerender(<SetBar set={{ status: 'todo', planned: 3 }} height={4} />)
111
+ expect(screen.getByTestId('set-strip-set')).toHaveStyle({ height: '4px' })
112
+ })
113
+
114
+ describe('accessibility', () => {
115
+ it('has no accessibility violations', async () => {
116
+ const { container } = render(
117
+ <SetBar set={{ status: 'active', velocities: [0.9, 0.7], planned: 4 }} />
118
+ )
119
+ const results = await axe(container)
120
+ expect(results).toHaveNoViolations()
121
+ })
122
+ })
123
+ })
@@ -0,0 +1,171 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { primitiveColors, primitiveRamps } from '../../../theme/tokens/primitives'
3
+ import { SegmentedBar, type SegmentedBarSegment } from './SegmentedBar'
4
+
5
+ /**
6
+ * The four canonical rep-intensity zone pins (real titan ramp pins). A rep's mean
7
+ * velocity ratio maps to one of these; butted together they form a set's bar.
8
+ */
9
+ export const SET_STRIP_ZONES = {
10
+ slow: primitiveRamps.red[600],
11
+ moderate: primitiveRamps.orange[400],
12
+ fast: primitiveRamps.amber[300],
13
+ fastest: primitiveRamps.green[300],
14
+ } as const
15
+
16
+ /** Grey fill for planned-but-unperformed reps / upcoming sets (charcoal placeholder). */
17
+ const TODO_COLOR = primitiveColors.charcoal[300]
18
+
19
+ /**
20
+ * The learned "variable / unknown / opportunity" semantic (cyan-900, a real ramp
21
+ * pin): an unperformed rep in a range's variable zone, and the fading trail of a
22
+ * myo set whose cluster count isn't yet known.
23
+ */
24
+ export const SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900]
25
+
26
+ // Gap hierarchy — the chunk-size *pattern* carries set-type identity:
27
+ // butted reps (0) < NOTCH (drop sub-load) < CLUSTER (myo rest-pause) < SET_GAP (5, between sets).
28
+ /** Drop-set sub-load divider — a thin notch, narrower than a myo cluster gap. */
29
+ const NOTCH_GAP = 2
30
+ /** Myo rest-pause divider between the activation chunk and each mini-cluster. */
31
+ const CLUSTER_GAP = 3
32
+
33
+ /** myo-upcoming trail defaults: N fading expected clusters, each this many reps. */
34
+ const MYO_UPCOMING_CLUSTERS = 3
35
+ const MYO_UPCOMING_CLUSTER_LEN = 5
36
+ /** Per-cluster opacity decay along the "count unknown" trail (1 · 0.6 · 0.36 · …). */
37
+ const MYO_UPCOMING_FADE = 0.6
38
+
39
+ /**
40
+ * Active-set pulse range per zone: each segment eases between its pin's adjacent
41
+ * lighter/darker ramp steps, keeping the intensity reading while signalling "live".
42
+ */
43
+ const PULSE_RANGE: Record<string, [string, string]> = {
44
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
45
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
46
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
47
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]],
48
+ }
49
+
50
+ /** Map a per-rep mean-velocity ratio to its zone pin (fastest = green, slowest = red). */
51
+ export function velocityZoneColor(v: number): string {
52
+ if (v < 0.5) return SET_STRIP_ZONES.slow
53
+ if (v < 0.75) return SET_STRIP_ZONES.moderate
54
+ if (v < 1.0) return SET_STRIP_ZONES.fast
55
+ return SET_STRIP_ZONES.fastest
56
+ }
57
+
58
+ /**
59
+ * One set's data. Flat variants: `done` = every rep performed; `active` = reps-so-far
60
+ * performed against a planned count (remainder greyed, performed reps pulse); `todo` =
61
+ * a planned but unstarted set (solid grey bar). Set-type variants:
62
+ * - `range` — variable rep-range (e.g. 15–20): `max` segments; committed zone
63
+ * `0..floor` (done = velocity, todo = grey), variable zone `floor..max` (done =
64
+ * velocity, todo = the variable/opportunity cyan). `doneVels` may run past `floor`.
65
+ * - `drop` — one set, no rest: `subloads` butted internally, split by 2px notches.
66
+ * - `myo` — done rest-pause: `activation` chunk + `clusters`, split by 3px cluster gaps.
67
+ * - `myo-upcoming` — planned myo, length unknown: grey activation (`activationLen`) +
68
+ * a fading cyan "clusters-to-failure" trail with the right edge left open.
69
+ */
70
+ export type SetStripSet =
71
+ | { status: 'done'; velocities: number[] }
72
+ | { status: 'active'; velocities: number[]; planned: number }
73
+ | { status: 'todo'; planned: number }
74
+ | { status: 'range'; floor: number; max: number; doneVels: number[] }
75
+ | { status: 'drop'; subloads: number[][] }
76
+ | { status: 'myo'; activation: number[]; clusters: number[][] }
77
+ | { status: 'myo-upcoming'; activationLen: number; clusterCount?: number }
78
+
79
+ /** Flatten velocity chunks into butted segments, carving `gap` before each chunk but the first. */
80
+ function chunkedSegments(chunks: number[][], gap: number): SegmentedBarSegment[] {
81
+ return chunks.flatMap((vels, ci) =>
82
+ vels.map((v, ri) => ({
83
+ color: velocityZoneColor(v),
84
+ ...(ci > 0 && ri === 0 ? { leadingGap: gap } : {}),
85
+ }))
86
+ )
87
+ }
88
+
89
+ /** The variable rep-range's `max` segments: velocity where done, else grey (committed) / cyan (variable). */
90
+ function rangeSegments(floor: number, max: number, doneVels: number[]): SegmentedBarSegment[] {
91
+ return Array.from({ length: max }, (_, i) => {
92
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) }
93
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR }
94
+ })
95
+ }
96
+
97
+ /** Grey activation chunk + a fading, right-open cyan trail of expected rest-pause clusters. */
98
+ function myoUpcomingSegments(activationLen: number, clusterCount: number): SegmentedBarSegment[] {
99
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }))
100
+ const trail = Array.from({ length: clusterCount }, (_, ci) =>
101
+ Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_, ri) => ({
102
+ color: SET_STRIP_VARIABLE_COLOR,
103
+ opacity: MYO_UPCOMING_FADE ** ci,
104
+ ...(ri === 0 ? { leadingGap: CLUSTER_GAP } : {}),
105
+ }))
106
+ ).flat()
107
+ return [...activation, ...trail]
108
+ }
109
+
110
+ /** Map a set's performance state to the butted per-rep segments of its bar. */
111
+ function setSegments(set: SetStripSet): SegmentedBarSegment[] {
112
+ if (set.status === 'todo') {
113
+ return [{ color: TODO_COLOR }]
114
+ }
115
+ if (set.status === 'done') {
116
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }))
117
+ }
118
+ if (set.status === 'range') {
119
+ return rangeSegments(set.floor, set.max, set.doneVels)
120
+ }
121
+ if (set.status === 'drop') {
122
+ return chunkedSegments(set.subloads, NOTCH_GAP)
123
+ }
124
+ if (set.status === 'myo') {
125
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP)
126
+ }
127
+ if (set.status === 'myo-upcoming') {
128
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS)
129
+ }
130
+ const performed = set.velocities.map((v) => {
131
+ const color = velocityZoneColor(v)
132
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] }
133
+ })
134
+ const remaining = Math.max(0, set.planned - set.velocities.length)
135
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))]
136
+ }
137
+
138
+ /** Preserve SetBar's per-segment test hooks over the generic SegmentedBar fills. */
139
+ function setBarSegmentTestID(seg: SegmentedBarSegment): string {
140
+ if (seg.pulse) return 'set-strip-pulse'
141
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return 'set-strip-variable'
142
+ return seg.color === TODO_COLOR ? 'set-strip-empty' : 'set-strip-fill'
143
+ }
144
+
145
+ export interface SetBarProps {
146
+ /** One set's performance state (done / active / todo). */
147
+ set: SetStripSet
148
+ /** Bar height in px. Default 8. */
149
+ height?: number
150
+ }
151
+
152
+ /**
153
+ * ONE set's multi-coloured bar: the butted per-rep colour segments for a single
154
+ * set (`done` = velocity-coloured reps · `active` = performed reps pulsing + grey
155
+ * remainder · `todo` = a solid grey bar), composed over {@link SegmentedBar}.
156
+ * Fills its flex slot; {@link SetStrip} lays several side by side. Colours are the
157
+ * real titan ramp pins.
158
+ */
159
+ export function SetBar({ set, height = 8 }: SetBarProps) {
160
+ return (
161
+ <SegmentedBar
162
+ segments={setSegments(set)}
163
+ height={height}
164
+ gap={0}
165
+ radius={0}
166
+ segmentTestID={setBarSegmentTestID}
167
+ style={{ flex: 1, minWidth: 0, borderRadius: 2, overflow: 'hidden' }}
168
+ testID="set-strip-set"
169
+ />
170
+ )
171
+ }
@@ -1,25 +1,39 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View } from 'react-native'
3
3
  import { SetRow } from './SetRow'
4
+ import { SetTableHeader } from './SetTableHeader'
4
5
 
5
6
  const meta: Meta<typeof SetRow> = {
6
- title: 'Custom/Workout/SetRow',
7
+ title: 'Workout/SetRow',
7
8
  component: SetRow,
8
9
  tags: ['autodocs'],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ '**Molecule** — one row of the unified expanded exercise table (SET · REPS · LBS · RPE ' +
15
+ 'over a per-row velocity strip), as a lifecycle discriminated union on `state`: `done` ' +
16
+ '(logged), `live` (performing now — brightened, compact velocity-height spotlight), `todo` ' +
17
+ '(planned — muted grey stub). Done + upcoming share one muted treatment; only `live` is ' +
18
+ 'brightened. No PREV column. Composed by ' +
19
+ '[ExerciseCard](?path=/docs/workout-exercisecard--docs) (expanded).',
20
+ },
21
+ },
22
+ },
9
23
  argTypes: {
10
- mode: {
24
+ state: {
11
25
  control: 'select',
12
- options: ['active', 'completed', 'history'],
13
- description: 'Row display mode',
26
+ options: ['done', 'live', 'todo'],
27
+ description: 'Set lifecycle',
14
28
  },
15
29
  unit: {
16
30
  control: 'select',
17
31
  options: ['lbs', 'kg'],
18
32
  description: 'Weight unit',
19
33
  },
20
- isNextSet: {
21
- control: 'boolean',
22
- description: 'Whether this is the next set to perform',
34
+ setType: {
35
+ control: 'text',
36
+ description: 'Optional set-type marker shown in the SET cell (e.g. "W", "DROP")',
23
37
  },
24
38
  },
25
39
  }
@@ -27,139 +41,75 @@ const meta: Meta<typeof SetRow> = {
27
41
  export default meta
28
42
  type Story = StoryObj<typeof SetRow>
29
43
 
30
- export const CompletedWithVelocity: Story = {
44
+ export const Done: Story = {
31
45
  args: {
32
- mode: 'completed',
46
+ state: 'done',
33
47
  setNumber: 1,
48
+ unit: 'lbs',
34
49
  reps: 8,
35
50
  weight: 135,
36
- unit: 'lbs',
37
51
  rpe: 7.5,
38
- previous: { reps: 8, weight: 130 },
39
52
  velocities: [1.1, 0.95, 0.82, 0.68, 0.55, 0.48, 0.42, 0.38],
40
53
  },
41
54
  }
42
55
 
43
- export const ActiveWithTargets: Story = {
44
- args: {
45
- mode: 'active',
46
- setNumber: 3,
47
- reps: null,
48
- weight: null,
49
- unit: 'lbs',
50
- previous: { reps: 8, weight: 135 },
51
- targets: { reps: 10, weight: 150 },
52
- },
53
- }
54
-
55
- export const ActiveNextSet: Story = {
56
+ export const Live: Story = {
56
57
  args: {
57
- mode: 'active',
58
+ state: 'live',
58
59
  setNumber: 2,
59
- reps: null,
60
- weight: null,
61
60
  unit: 'lbs',
62
- isNextSet: true,
63
- targets: { reps: 8, weight: 135 },
64
- previous: { reps: 8, weight: 130 },
61
+ target: { reps: 10, weight: 150 },
62
+ reps: 5,
63
+ weight: 150,
64
+ velocities: [0.95, 0.9, 0.86, 0.8, 0.72],
65
65
  },
66
66
  }
67
67
 
68
- export const HistorySet: Story = {
68
+ export const Todo: Story = {
69
69
  args: {
70
- mode: 'history',
71
- setNumber: 1,
72
- reps: 10,
73
- weight: 185,
70
+ state: 'todo',
71
+ setNumber: 3,
74
72
  unit: 'lbs',
75
- rpe: 8,
76
- previous: { reps: 10, weight: 180 },
73
+ target: { reps: 10, weight: 150 },
77
74
  },
78
75
  }
79
76
 
80
77
  export const WithTypeBadge: Story = {
81
78
  args: {
82
- mode: 'completed',
79
+ state: 'done',
83
80
  setNumber: 1,
81
+ unit: 'lbs',
84
82
  reps: 5,
85
83
  weight: 95,
86
- unit: 'lbs',
87
84
  setType: 'W',
88
- previous: null,
85
+ velocities: [1.05, 0.98, 0.9, 0.84, 0.78],
89
86
  },
90
87
  }
91
88
 
92
- export const WithPrBadges: Story = {
93
- args: {
94
- mode: 'completed',
95
- setNumber: 3,
96
- reps: 8,
97
- weight: 225,
98
- unit: 'lbs',
99
- rpe: 9.5,
100
- previous: { reps: 8, weight: 215 },
101
- prBadges: [
102
- { type: 'e1rm', label: 'PR e1RM' },
103
- { type: 'weight', label: 'PR Weight' },
104
- ],
105
- },
106
- }
107
-
108
- export const WithRPE: Story = {
109
- args: {
110
- mode: 'completed',
111
- setNumber: 4,
112
- reps: 5,
113
- weight: 275,
114
- unit: 'lbs',
115
- rpe: 10,
116
- previous: { reps: 5, weight: 265 },
117
- },
118
- }
119
-
120
- export const AllVariants: Story = {
89
+ /** The lifecycle in context: header + done → live → todo, as the expanded card lays it out. */
90
+ export const TableInContext: Story = {
121
91
  render: () => (
122
- <View style={{ gap: 4, padding: 16, maxWidth: 400 }}>
92
+ <View style={{ padding: 16, maxWidth: 400 }}>
93
+ <SetTableHeader unit="lbs" showPrevious={false} />
123
94
  <SetRow
124
- mode="completed"
95
+ state="done"
125
96
  setNumber={1}
97
+ unit="lbs"
126
98
  reps={8}
127
99
  weight={135}
128
- unit="lbs"
129
- rpe={7}
130
- previous={{ reps: 8, weight: 130 }}
100
+ rpe={7.5}
131
101
  velocities={[1.1, 0.95, 0.82, 0.68, 0.55, 0.48, 0.42, 0.38]}
132
- prBadges={[{ type: 'e1rm', label: 'PR e1RM' }]}
133
102
  />
134
103
  <SetRow
135
- mode="completed"
104
+ state="live"
136
105
  setNumber={2}
137
- reps={8}
138
- weight={135}
139
- unit="lbs"
140
- rpe={8.5}
141
- previous={{ reps: 8, weight: 135 }}
142
- velocities={[0.95, 0.88, 0.78, 0.65, 0.52, 0.45, 0.40, 0.35]}
143
- />
144
- <SetRow
145
- mode="active"
146
- setNumber={3}
147
- reps={null}
148
- weight={null}
149
106
  unit="lbs"
150
- isNextSet
151
- targets={{ reps: 8, weight: 135 }}
152
- previous={{ reps: 8, weight: 135 }}
153
- />
154
- <SetRow
155
- mode="active"
156
- setNumber={4}
157
- reps={null}
158
- weight={null}
159
- unit="lbs"
160
- targets={{ reps: 8, weight: 135 }}
161
- previous={{ reps: 8, weight: 135 }}
107
+ target={{ reps: 8, weight: 135 }}
108
+ reps={5}
109
+ weight={135}
110
+ velocities={[0.95, 0.88, 0.78, 0.65, 0.52]}
162
111
  />
112
+ <SetRow state="todo" setNumber={3} unit="lbs" target={{ reps: 8, weight: 135 }} />
163
113
  </View>
164
114
  ),
165
115
  }