@titan-design/react-ui 0.2.6 → 0.3.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 (191) hide show
  1. package/dist/bodymap-BhG55xHM.d.mts +318 -0
  2. package/dist/bodymap-BhG55xHM.d.ts +318 -0
  3. package/dist/bodymap.d.mts +3 -0
  4. package/dist/bodymap.d.ts +3 -0
  5. package/dist/bodymap.js +2670 -0
  6. package/dist/bodymap.js.map +1 -0
  7. package/dist/bodymap.mjs +1101 -0
  8. package/dist/bodymap.mjs.map +1 -0
  9. package/dist/{chunk-UXVRPOME.mjs → chunk-7XPB4NAO.mjs} +125 -785
  10. package/dist/chunk-7XPB4NAO.mjs.map +1 -0
  11. package/dist/chunk-P7TSQUN6.mjs +846 -0
  12. package/dist/chunk-P7TSQUN6.mjs.map +1 -0
  13. package/dist/chunk-TOD2WQBG.mjs +1048 -0
  14. package/dist/chunk-TOD2WQBG.mjs.map +1 -0
  15. package/dist/index.d.mts +1131 -4
  16. package/dist/index.d.ts +1131 -4
  17. package/dist/index.js +6396 -225
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +5572 -641
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/theme/index.d.mts +317 -1
  22. package/dist/theme/index.d.ts +317 -1
  23. package/dist/theme/index.js +185 -2
  24. package/dist/theme/index.js.map +1 -1
  25. package/dist/theme/index.mjs +2 -1
  26. package/dist/theme/tokens-css.d.mts +20 -0
  27. package/dist/theme/tokens-css.d.ts +20 -0
  28. package/dist/theme/tokens-css.js +598 -0
  29. package/dist/theme/tokens-css.js.map +1 -0
  30. package/dist/theme/tokens-css.mjs +24 -0
  31. package/dist/theme/tokens-css.mjs.map +1 -0
  32. package/package.json +25 -5
  33. package/src/bodymap.ts +37 -0
  34. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
  36. package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
  37. package/src/components/custom/Gauge/Gauge.tsx +167 -0
  38. package/src/components/custom/Gauge/index.ts +2 -0
  39. package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
  40. package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
  41. package/src/components/custom/Scatter/Scatter.tsx +260 -0
  42. package/src/components/custom/Scatter/index.ts +2 -0
  43. package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
  44. package/src/components/custom/Table/Table.test.tsx +27 -0
  45. package/src/components/custom/Table/Table.tsx +33 -17
  46. package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
  47. package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
  48. package/src/components/custom/Treemap/Treemap.tsx +192 -0
  49. package/src/components/custom/Treemap/index.ts +2 -0
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +447 -0
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
  54. package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
  55. package/src/components/custom/Workout/BaseBadge.tsx +113 -0
  56. package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
  57. package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
  58. package/src/components/custom/Workout/BodyMap.tsx +347 -0
  59. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
  60. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
  61. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +477 -0
  62. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
  63. package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
  64. package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
  65. package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
  66. package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
  67. package/src/components/custom/Workout/DeviationBar.tsx +78 -0
  68. package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
  69. package/src/components/custom/Workout/ExerciseCard.test.tsx +343 -0
  70. package/src/components/custom/Workout/ExerciseCard.tsx +442 -0
  71. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
  72. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
  73. package/src/components/custom/Workout/ExerciseDetailPage.tsx +630 -0
  74. package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
  75. package/src/components/custom/Workout/InputBar.test.tsx +116 -0
  76. package/src/components/custom/Workout/InputBar.tsx +162 -0
  77. package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
  78. package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
  79. package/src/components/custom/Workout/IntensityBar.tsx +166 -0
  80. package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
  81. package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
  82. package/src/components/custom/Workout/MesoCard.tsx +237 -0
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
  84. package/src/components/custom/Workout/MesoProgressBar.test.tsx +153 -0
  85. package/src/components/custom/Workout/MesoProgressBar.tsx +165 -0
  86. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
  87. package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
  88. package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
  89. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
  90. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
  91. package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
  92. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
  93. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
  94. package/src/components/custom/Workout/PlaceholderStrip.tsx +90 -0
  95. package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
  96. package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
  97. package/src/components/custom/Workout/PrBadge.tsx +103 -0
  98. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
  99. package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
  100. package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
  101. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
  102. package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
  103. package/src/components/custom/Workout/ProgramPlanningPage.tsx +422 -0
  104. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
  105. package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
  106. package/src/components/custom/Workout/ReadinessCheck.tsx +265 -0
  107. package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
  108. package/src/components/custom/Workout/RestTimer.tsx +15 -11
  109. package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
  110. package/src/components/custom/Workout/SetRow.test.tsx +222 -0
  111. package/src/components/custom/Workout/SetRow.tsx +247 -0
  112. package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
  113. package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
  114. package/src/components/custom/Workout/Sparkline.tsx +188 -0
  115. package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
  116. package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
  117. package/src/components/custom/Workout/StatusDot.tsx +160 -0
  118. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
  119. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
  120. package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
  121. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
  122. package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
  123. package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
  124. package/src/components/custom/Workout/TempoDisplay.tsx +209 -0
  125. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
  126. package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
  127. package/src/components/custom/Workout/TrainingStatusPage.tsx +321 -0
  128. package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
  129. package/src/components/custom/Workout/VelocityStrip.test.tsx +244 -0
  130. package/src/components/custom/Workout/VelocityStrip.tsx +288 -0
  131. package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
  132. package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
  133. package/src/components/custom/Workout/WeekRow.tsx +140 -0
  134. package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
  135. package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
  136. package/src/components/custom/Workout/WeightBadge.tsx +113 -0
  137. package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
  138. package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
  139. package/src/components/custom/Workout/WorkoutCard.tsx +228 -0
  140. package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
  141. package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
  142. package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
  143. package/src/components/custom/Workout/index.ts +152 -0
  144. package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
  145. package/src/components/custom/index.ts +3 -0
  146. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  147. package/src/components/custom/stepper/Stepper.tsx +2 -2
  148. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  149. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  150. package/src/components/ui/card/Card.tsx +1 -1
  151. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  152. package/src/components/ui/collapse/Collapse.tsx +1 -1
  153. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  154. package/src/components/ui/drawer/Drawer.tsx +3 -3
  155. package/src/components/ui/form-field/FormField.tsx +1 -1
  156. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  157. package/src/components/ui/menu/Menu.tsx +2 -2
  158. package/src/components/ui/pill/Pill.tsx +1 -1
  159. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  160. package/src/components/ui/popover/Popover.tsx +1 -1
  161. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  162. package/src/components/ui/section/Section.stories.tsx +4 -4
  163. package/src/components/ui/select/Select.tsx +1 -1
  164. package/src/components/ui/stack/Stack.stories.tsx +4 -4
  165. package/src/components/ui/tabs/Tabs.tsx +2 -2
  166. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
  168. package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
  169. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
  170. package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
  171. package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
  172. package/src/stories/PresetShowcase.stories.tsx +15 -15
  173. package/src/theme/config.completeness.test.ts +77 -0
  174. package/src/theme/config.ts +151 -0
  175. package/src/theme/index.ts +2 -1
  176. package/src/theme/manifest/css-property-manifest.example.json +79 -0
  177. package/src/theme/manifest/css-property-manifest.schema.json +93 -0
  178. package/src/theme/manifest/css-property-manifest.test.ts +81 -0
  179. package/src/theme/manifest/css-property-manifest.types.ts +49 -0
  180. package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
  181. package/src/theme/manifest/design-freeze.test.ts +155 -0
  182. package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
  183. package/src/theme/manifest/extract-css-properties.test.ts +112 -0
  184. package/src/theme/manifest/index.ts +11 -0
  185. package/src/theme/tokens-css.test.ts +28 -0
  186. package/src/theme/tokens-css.ts +36 -0
  187. package/src/theme/workout-tokens.ts +59 -0
  188. package/src/utils/index.ts +10 -0
  189. package/src/utils/workout-format.test.ts +81 -0
  190. package/src/utils/workout-format.ts +83 -0
  191. package/dist/chunk-UXVRPOME.mjs.map +0 -1
@@ -0,0 +1,165 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { SetRow } from './SetRow'
4
+
5
+ const meta: Meta<typeof SetRow> = {
6
+ title: 'Custom/Workout/SetRow',
7
+ component: SetRow,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ mode: {
11
+ control: 'select',
12
+ options: ['active', 'completed', 'history'],
13
+ description: 'Row display mode',
14
+ },
15
+ unit: {
16
+ control: 'select',
17
+ options: ['lbs', 'kg'],
18
+ description: 'Weight unit',
19
+ },
20
+ isNextSet: {
21
+ control: 'boolean',
22
+ description: 'Whether this is the next set to perform',
23
+ },
24
+ },
25
+ }
26
+
27
+ export default meta
28
+ type Story = StoryObj<typeof SetRow>
29
+
30
+ export const CompletedWithVelocity: Story = {
31
+ args: {
32
+ mode: 'completed',
33
+ setNumber: 1,
34
+ reps: 8,
35
+ weight: 135,
36
+ unit: 'lbs',
37
+ rpe: 7.5,
38
+ previous: { reps: 8, weight: 130 },
39
+ velocities: [1.1, 0.95, 0.82, 0.68, 0.55, 0.48, 0.42, 0.38],
40
+ },
41
+ }
42
+
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
+ args: {
57
+ mode: 'active',
58
+ setNumber: 2,
59
+ reps: null,
60
+ weight: null,
61
+ unit: 'lbs',
62
+ isNextSet: true,
63
+ targets: { reps: 8, weight: 135 },
64
+ previous: { reps: 8, weight: 130 },
65
+ },
66
+ }
67
+
68
+ export const HistorySet: Story = {
69
+ args: {
70
+ mode: 'history',
71
+ setNumber: 1,
72
+ reps: 10,
73
+ weight: 185,
74
+ unit: 'lbs',
75
+ rpe: 8,
76
+ previous: { reps: 10, weight: 180 },
77
+ },
78
+ }
79
+
80
+ export const WithTypeBadge: Story = {
81
+ args: {
82
+ mode: 'completed',
83
+ setNumber: 1,
84
+ reps: 5,
85
+ weight: 95,
86
+ unit: 'lbs',
87
+ setType: 'W',
88
+ previous: null,
89
+ },
90
+ }
91
+
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 = {
121
+ render: () => (
122
+ <View style={{ gap: 4, padding: 16, maxWidth: 400 }}>
123
+ <SetRow
124
+ mode="completed"
125
+ setNumber={1}
126
+ reps={8}
127
+ weight={135}
128
+ unit="lbs"
129
+ rpe={7}
130
+ previous={{ reps: 8, weight: 130 }}
131
+ 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
+ />
134
+ <SetRow
135
+ mode="completed"
136
+ 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
+ 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 }}
162
+ />
163
+ </View>
164
+ ),
165
+ }
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SetRow } from './SetRow'
5
+
6
+ const baseProps = {
7
+ mode: 'completed' as const,
8
+ setNumber: 1,
9
+ reps: 8,
10
+ weight: 135,
11
+ unit: 'lbs' as const,
12
+ }
13
+
14
+ describe('SetRow', () => {
15
+ describe('completed mode', () => {
16
+ it('renders set number, reps, weight', () => {
17
+ render(<SetRow {...baseProps} />)
18
+ expect(screen.getByTestId('set-row')).toBeInTheDocument()
19
+ expect(screen.getByTestId('set-row-set-number')).toHaveTextContent('1')
20
+ expect(screen.getByTestId('set-row-reps')).toHaveTextContent('8')
21
+ expect(screen.getByTestId('set-row-weight')).toHaveTextContent('135')
22
+ })
23
+
24
+ it('displays previous data when provided', () => {
25
+ render(<SetRow {...baseProps} previous={{ reps: 6, weight: 130 }} />)
26
+ expect(screen.getByTestId('set-row-previous')).toHaveTextContent(
27
+ '6 x 130',
28
+ )
29
+ })
30
+
31
+ it('displays dash when no previous data', () => {
32
+ render(<SetRow {...baseProps} />)
33
+ expect(screen.getByTestId('set-row-previous')).toHaveTextContent('\u2014')
34
+ })
35
+
36
+ it('applies reduced opacity for non-next completed sets', () => {
37
+ render(<SetRow {...baseProps} />)
38
+ const row = screen.getByTestId('set-row')
39
+ expect(row).toHaveStyle({ opacity: 0.55 })
40
+ })
41
+ })
42
+
43
+ describe('active mode', () => {
44
+ it('renders target values when reps/weight are null', () => {
45
+ render(
46
+ <SetRow
47
+ mode="active"
48
+ setNumber={2}
49
+ reps={null}
50
+ weight={null}
51
+ unit="lbs"
52
+ targets={{ reps: 10, weight: 150 }}
53
+ />,
54
+ )
55
+ expect(screen.getByTestId('set-row-target-reps')).toHaveTextContent('10')
56
+ expect(screen.getByTestId('set-row-target-weight')).toHaveTextContent(
57
+ '150',
58
+ )
59
+ })
60
+
61
+ it('renders dashes when no targets and values are null', () => {
62
+ render(
63
+ <SetRow mode="active" setNumber={2} reps={null} weight={null} unit="lbs" />,
64
+ )
65
+ expect(screen.getByTestId('set-row-reps')).toHaveTextContent('\u2014')
66
+ expect(screen.getByTestId('set-row-weight')).toHaveTextContent('\u2014')
67
+ })
68
+ })
69
+
70
+ describe('isNextSet highlighting', () => {
71
+ it('applies highlight styles for active isNextSet', () => {
72
+ render(
73
+ <SetRow
74
+ mode="active"
75
+ setNumber={1}
76
+ reps={null}
77
+ weight={null}
78
+ unit="lbs"
79
+ isNextSet
80
+ />,
81
+ )
82
+ const row = screen.getByTestId('set-row')
83
+ const style = row.getAttribute('style') ?? ''
84
+ expect(style).toContain('background-color')
85
+ expect(style).toContain('border')
86
+ })
87
+
88
+ it('does not apply highlight for non-next active sets', () => {
89
+ render(
90
+ <SetRow mode="active" setNumber={1} reps={null} weight={null} unit="lbs" />,
91
+ )
92
+ const row = screen.getByTestId('set-row')
93
+ expect(row).not.toHaveStyle({
94
+ backgroundColor: 'rgba(255,121,0,0.06)',
95
+ })
96
+ })
97
+ })
98
+
99
+ describe('history mode', () => {
100
+ it('renders like completed mode', () => {
101
+ render(
102
+ <SetRow mode="history" setNumber={3} reps={5} weight={200} unit="kg" />,
103
+ )
104
+ expect(screen.getByTestId('set-row-reps')).toHaveTextContent('5')
105
+ expect(screen.getByTestId('set-row-weight')).toHaveTextContent('200')
106
+ })
107
+ })
108
+
109
+ describe('velocity strip', () => {
110
+ it('renders velocity strip when velocities provided', () => {
111
+ render(
112
+ <SetRow {...baseProps} velocities={[1.1, 0.95, 0.82]} />,
113
+ )
114
+ expect(screen.getByTestId('set-row-velocity-strip')).toBeInTheDocument()
115
+ })
116
+
117
+ it('does not render velocity strip when no velocities', () => {
118
+ render(<SetRow {...baseProps} />)
119
+ expect(
120
+ screen.queryByTestId('set-row-velocity-strip'),
121
+ ).not.toBeInTheDocument()
122
+ })
123
+
124
+ it('passes onVelocityToggle to velocity strip', () => {
125
+ const onToggle = vi.fn()
126
+ render(
127
+ <SetRow
128
+ {...baseProps}
129
+ velocities={[1.1, 0.95]}
130
+ onVelocityToggle={onToggle}
131
+ />,
132
+ )
133
+ const pressable = screen.getByTestId('velocity-strip-pressable')
134
+ fireEvent.click(pressable)
135
+ expect(onToggle).toHaveBeenCalledOnce()
136
+ })
137
+ })
138
+
139
+ describe('set type badge', () => {
140
+ it('renders set type badge instead of set number', () => {
141
+ render(<SetRow {...baseProps} setType="W" />)
142
+ expect(screen.getByTestId('set-row-type-badge')).toHaveTextContent('W')
143
+ })
144
+
145
+ it('does not render type badge when setType is not provided', () => {
146
+ render(<SetRow {...baseProps} />)
147
+ expect(
148
+ screen.queryByTestId('set-row-type-badge'),
149
+ ).not.toBeInTheDocument()
150
+ })
151
+ })
152
+
153
+ describe('PR badges', () => {
154
+ it('renders PR badges when provided', () => {
155
+ render(
156
+ <SetRow
157
+ {...baseProps}
158
+ prBadges={[{ type: 'e1rm', label: 'PR e1RM' }]}
159
+ />,
160
+ )
161
+ expect(screen.getByTestId('set-row-pr-badges')).toBeInTheDocument()
162
+ })
163
+
164
+ it('does not render PR badges container when no badges', () => {
165
+ render(<SetRow {...baseProps} />)
166
+ expect(
167
+ screen.queryByTestId('set-row-pr-badges'),
168
+ ).not.toBeInTheDocument()
169
+ })
170
+ })
171
+
172
+ describe('RPE', () => {
173
+ it('displays RPE value with color coding', () => {
174
+ render(<SetRow {...baseProps} rpe={9.5} />)
175
+ expect(screen.getByTestId('set-row-rpe')).toHaveTextContent('9.5')
176
+ })
177
+
178
+ it('displays dash when RPE is null', () => {
179
+ render(<SetRow {...baseProps} rpe={null} />)
180
+ expect(screen.getByTestId('set-row-rpe')).toHaveTextContent('\u2014')
181
+ })
182
+ })
183
+
184
+ describe('accessibility', () => {
185
+ it('has correct accessibility label', () => {
186
+ render(<SetRow {...baseProps} />)
187
+ expect(
188
+ screen.getByLabelText('Set 1: 8 reps at 135 lbs'),
189
+ ).toBeInTheDocument()
190
+ })
191
+
192
+ it('has correct accessibility label with null values', () => {
193
+ render(
194
+ <SetRow mode="active" setNumber={2} reps={null} weight={null} unit="kg" />,
195
+ )
196
+ expect(
197
+ screen.getByLabelText('Set 2: no reps'),
198
+ ).toBeInTheDocument()
199
+ })
200
+
201
+ it('has no accessibility violations', async () => {
202
+ const { container } = render(<SetRow {...baseProps} />)
203
+ const results = await axe(container)
204
+ expect(results).toHaveNoViolations()
205
+ })
206
+
207
+ it('has no accessibility violations with all features', async () => {
208
+ const { container } = render(
209
+ <SetRow
210
+ {...baseProps}
211
+ rpe={8}
212
+ velocities={[1.1, 0.95]}
213
+ setType="W"
214
+ prBadges={[{ type: 'e1rm', label: 'PR e1RM' }]}
215
+ previous={{ reps: 6, weight: 130 }}
216
+ />,
217
+ )
218
+ const results = await axe(container)
219
+ expect(results).toHaveNoViolations()
220
+ })
221
+ })
222
+ })
@@ -0,0 +1,247 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, Text } from 'react-native'
3
+ import { VelocityStrip } from './VelocityStrip'
4
+ import { PrBadge, type PRType } from './PrBadge'
5
+ import { roundRpe, rpeColor, roundWeight } from '../../../utils/workout-format'
6
+
7
+ export type SetRowMode = 'active' | 'completed' | 'history'
8
+
9
+ export interface SetRowProps {
10
+ mode: SetRowMode
11
+ setNumber: number
12
+ previous?: { reps: number; weight: number } | null
13
+ reps: number | null
14
+ weight: number | null
15
+ rpe?: number | null
16
+ unit: 'lbs' | 'kg'
17
+ velocities?: number[]
18
+ velocityExpanded?: boolean
19
+ onVelocityToggle?: () => void
20
+ setType?: string
21
+ prBadges?: Array<{ type: PRType; label: string }>
22
+ isNextSet?: boolean
23
+ targets?: { reps: number; weight: number }
24
+ }
25
+
26
+ function formatAccessibilityLabel(
27
+ setNumber: number,
28
+ reps: number | null,
29
+ weight: number | null,
30
+ unit: string,
31
+ ): string {
32
+ const repsText = reps != null ? `${reps} reps` : 'no reps'
33
+ const weightText = weight != null ? `at ${roundWeight(weight)} ${unit}` : ''
34
+ return `Set ${setNumber}: ${repsText}${weightText ? ` ${weightText}` : ''}`
35
+ }
36
+
37
+ export function SetRow({
38
+ mode,
39
+ setNumber,
40
+ previous,
41
+ reps,
42
+ weight,
43
+ rpe,
44
+ unit,
45
+ velocities,
46
+ velocityExpanded,
47
+ onVelocityToggle,
48
+ setType,
49
+ prBadges,
50
+ isNextSet,
51
+ targets,
52
+ }: SetRowProps) {
53
+ const isActive = mode === 'active'
54
+ const isCompleted = mode === 'completed'
55
+
56
+ const rowStyle: Record<string, unknown> = {
57
+ flexDirection: 'row' as const,
58
+ alignItems: 'center' as const,
59
+ padding: 7,
60
+ paddingHorizontal: 8,
61
+ gap: 8,
62
+ borderRadius: 8,
63
+ }
64
+
65
+ if (isActive && isNextSet) {
66
+ rowStyle.backgroundColor = 'rgba(255,121,0,0.06)'
67
+ rowStyle.borderWidth = 1
68
+ rowStyle.borderColor = 'rgba(255,121,0,0.15)'
69
+ }
70
+
71
+ if (isCompleted && !isNextSet) {
72
+ rowStyle.opacity = 0.55
73
+ }
74
+
75
+ return (
76
+ <View
77
+ style={rowStyle}
78
+ accessibilityLabel={formatAccessibilityLabel(
79
+ setNumber,
80
+ reps,
81
+ weight,
82
+ unit,
83
+ )}
84
+ testID="set-row"
85
+ >
86
+ <View
87
+ style={{ width: 36, alignItems: 'center', justifyContent: 'center' }}
88
+ testID="set-row-set-number"
89
+ >
90
+ {setType ? (
91
+ <Text
92
+ style={{
93
+ fontSize: 10,
94
+ fontWeight: '700',
95
+ fontFamily: 'Inter, sans-serif',
96
+ color: '#ffa502',
97
+ backgroundColor: 'rgba(255,165,2,0.12)',
98
+ paddingVertical: 1,
99
+ paddingHorizontal: 5,
100
+ borderRadius: 3,
101
+ overflow: 'hidden',
102
+ }}
103
+ testID="set-row-type-badge"
104
+ >
105
+ {setType}
106
+ </Text>
107
+ ) : (
108
+ <Text
109
+ style={{
110
+ fontSize: 13,
111
+ fontWeight: '600',
112
+ fontFamily: 'Inter, sans-serif',
113
+ color: 'var(--color-text-secondary)',
114
+ }}
115
+ >
116
+ {setNumber}
117
+ </Text>
118
+ )}
119
+ </View>
120
+
121
+ <View
122
+ className="flex-1"
123
+ testID="set-row-previous"
124
+ >
125
+ <Text
126
+ style={{
127
+ fontSize: 12,
128
+ fontFamily: 'Inter, sans-serif',
129
+ color: 'var(--color-text-secondary)',
130
+ }}
131
+ >
132
+ {previous
133
+ ? `${previous.reps} x ${roundWeight(previous.weight)}`
134
+ : '\u2014'}
135
+ </Text>
136
+ </View>
137
+
138
+ <View
139
+ style={{ width: 44, alignItems: 'center', justifyContent: 'center' }}
140
+ testID="set-row-reps"
141
+ >
142
+ {isActive && reps == null && targets ? (
143
+ <Text
144
+ style={{
145
+ fontSize: 13,
146
+ fontStyle: 'italic',
147
+ fontFamily: 'Inter, sans-serif',
148
+ color: 'var(--color-text-tertiary)',
149
+ }}
150
+ testID="set-row-target-reps"
151
+ >
152
+ {targets.reps}
153
+ </Text>
154
+ ) : (
155
+ <Text
156
+ style={{
157
+ fontSize: 14,
158
+ fontWeight: '600',
159
+ fontFamily: 'Inter, sans-serif',
160
+ color: reps != null ? 'var(--color-text-primary)' : 'var(--color-text-tertiary)',
161
+ }}
162
+ >
163
+ {reps != null ? reps : '\u2014'}
164
+ </Text>
165
+ )}
166
+ </View>
167
+
168
+ <View
169
+ style={{ width: 56, alignItems: 'center', justifyContent: 'center' }}
170
+ testID="set-row-weight"
171
+ >
172
+ {isActive && weight == null && targets ? (
173
+ <Text
174
+ style={{
175
+ fontSize: 13,
176
+ fontStyle: 'italic',
177
+ fontFamily: 'Inter, sans-serif',
178
+ color: 'var(--color-text-tertiary)',
179
+ }}
180
+ testID="set-row-target-weight"
181
+ >
182
+ {roundWeight(targets.weight)}
183
+ </Text>
184
+ ) : (
185
+ <Text
186
+ style={{
187
+ fontSize: 14,
188
+ fontWeight: '600',
189
+ fontFamily: 'Inter, sans-serif',
190
+ color: weight != null ? 'var(--color-text-primary)' : 'var(--color-text-tertiary)',
191
+ }}
192
+ >
193
+ {weight != null ? roundWeight(weight) : '\u2014'}
194
+ </Text>
195
+ )}
196
+ </View>
197
+
198
+ <View
199
+ style={{ width: 36, alignItems: 'center', justifyContent: 'center' }}
200
+ testID="set-row-rpe"
201
+ >
202
+ <Text
203
+ style={{
204
+ fontSize: 13,
205
+ fontWeight: '600',
206
+ fontFamily: 'Inter, sans-serif',
207
+ color: rpe != null ? rpeColor(rpe) : 'var(--color-text-tertiary)',
208
+ }}
209
+ >
210
+ {rpe != null ? roundRpe(rpe) : '\u2014'}
211
+ </Text>
212
+ </View>
213
+
214
+ {prBadges && prBadges.length > 0 && (
215
+ <View
216
+ className="flex-row items-center"
217
+ style={{ gap: 4 }}
218
+ testID="set-row-pr-badges"
219
+ >
220
+ {prBadges.map((badge, i) => (
221
+ <PrBadge
222
+ key={i}
223
+ type={badge.type}
224
+ label={badge.label}
225
+ animate={false}
226
+ compact
227
+ />
228
+ ))}
229
+ </View>
230
+ )}
231
+
232
+ {velocities && velocities.length > 0 && (
233
+ <View
234
+ style={{ position: 'absolute', bottom: 0, left: 8, right: 8 }}
235
+ testID="set-row-velocity-strip"
236
+ >
237
+ <VelocityStrip
238
+ velocities={velocities}
239
+ expanded={velocityExpanded}
240
+ onToggle={onVelocityToggle}
241
+ variant={onVelocityToggle ? 'full' : 'mini'}
242
+ />
243
+ </View>
244
+ )}
245
+ </View>
246
+ )
247
+ }