@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,257 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { ProgramPlanningPage, type PlanMeso } from './ProgramPlanningPage'
3
+ import type { ExerciseCardProps } from './ExerciseCard'
4
+ import type { WorkoutMuscleGroup } from './WorkoutCard'
5
+
6
+ const upperMuscles: WorkoutMuscleGroup[] = [
7
+ { group: 'chest', label: 'Chest', volumeStatus: 'productive' },
8
+ { group: 'front-delts', label: 'Front Delts', volumeStatus: 'maintenance' },
9
+ { group: 'triceps', label: 'Triceps', volumeStatus: 'productive' },
10
+ ]
11
+
12
+ const lowerMuscles: WorkoutMuscleGroup[] = [
13
+ { group: 'quads', label: 'Quads', volumeStatus: 'over' },
14
+ { group: 'glutes', label: 'Glutes', volumeStatus: 'productive' },
15
+ { group: 'hamstrings', label: 'Hamstrings', volumeStatus: 'maintenance' },
16
+ ]
17
+
18
+ const upperExercises: ExerciseCardProps[] = [
19
+ {
20
+ name: 'Barbell Bench Press',
21
+ state: 'collapsed',
22
+ onToggle: () => {},
23
+ summary: { sets: 4, reps: 8, weight: 185, unit: 'lbs' },
24
+ e1rm: { value: 235, unit: 'lbs' },
25
+ isPR: true,
26
+ setVelocities: [
27
+ [1.05, 0.94, 0.82, 0.74],
28
+ [0.98, 0.9, 0.8, 0.71],
29
+ ],
30
+ totalPlannedSets: 4,
31
+ },
32
+ {
33
+ name: 'Incline Dumbbell Press',
34
+ state: 'collapsed',
35
+ onToggle: () => {},
36
+ summary: { sets: 3, reps: 10, weight: 70, unit: 'lbs' },
37
+ e1rm: { value: 92, unit: 'lbs' },
38
+ totalPlannedSets: 3,
39
+ },
40
+ {
41
+ name: 'Cable Triceps Pushdown',
42
+ state: 'upcoming',
43
+ onToggle: () => {},
44
+ prescription: '3×12-15 @ RPE 8',
45
+ previousBest: '50 lbs × 14',
46
+ },
47
+ ]
48
+
49
+ const lowerExercises: ExerciseCardProps[] = [
50
+ {
51
+ name: 'Back Squat',
52
+ state: 'collapsed',
53
+ onToggle: () => {},
54
+ summary: { sets: 5, reps: 5, weight: 275, unit: 'lbs' },
55
+ e1rm: { value: 335, unit: 'lbs' },
56
+ totalPlannedSets: 5,
57
+ },
58
+ {
59
+ name: 'Romanian Deadlift',
60
+ state: 'upcoming',
61
+ onToggle: () => {},
62
+ prescription: '3×8-10 @ RPE 8',
63
+ previousBest: '225 lbs × 9',
64
+ },
65
+ {
66
+ name: 'Leg Press',
67
+ state: 'upcoming',
68
+ onToggle: () => {},
69
+ prescription: '3×12 @ RPE 9',
70
+ previousBest: '450 lbs × 12',
71
+ },
72
+ ]
73
+
74
+ function upperWorkout(
75
+ id: string,
76
+ date: string,
77
+ status: 'completed' | 'today' | 'upcoming'
78
+ ): PlanMeso['weeks'][number]['workouts'][number] {
79
+ return {
80
+ id,
81
+ name: 'Upper A',
82
+ date,
83
+ duration: '52 min',
84
+ status,
85
+ muscleGroups: upperMuscles,
86
+ totalSets: 18,
87
+ totalVolume: 21400,
88
+ unit: 'lbs',
89
+ exercises: upperExercises,
90
+ }
91
+ }
92
+
93
+ function lowerWorkout(
94
+ id: string,
95
+ date: string,
96
+ status: 'completed' | 'today' | 'upcoming'
97
+ ): PlanMeso['weeks'][number]['workouts'][number] {
98
+ return {
99
+ id,
100
+ name: 'Lower A',
101
+ date,
102
+ duration: '58 min',
103
+ status,
104
+ muscleGroups: lowerMuscles,
105
+ totalSets: 16,
106
+ totalVolume: 28800,
107
+ unit: 'lbs',
108
+ exercises: lowerExercises,
109
+ }
110
+ }
111
+
112
+ const accumulation: PlanMeso = {
113
+ id: 'm1',
114
+ name: 'Accumulation',
115
+ goal: 'Hypertrophy',
116
+ split: 'Upper/Lower',
117
+ weekRange: 'Weeks 1-4',
118
+ weekCount: 4,
119
+ currentWeek: 3,
120
+ status: 'current',
121
+ volumeHeatmap: [
122
+ { group: 'chest', percentage: 72 },
123
+ { group: 'back', percentage: 85 },
124
+ { group: 'legs', percentage: 90 },
125
+ { group: 'shoulders', percentage: 55 },
126
+ { group: 'arms', percentage: 60 },
127
+ ],
128
+ weeks: [
129
+ {
130
+ weekNumber: 1,
131
+ intensityLevel: 0.5,
132
+ intensityThreshold: 0.85,
133
+ workouts: [
134
+ upperWorkout('m1w1-u', 'Mon, Jun 2', 'completed'),
135
+ lowerWorkout('m1w1-l', 'Wed, Jun 4', 'completed'),
136
+ ],
137
+ },
138
+ {
139
+ weekNumber: 2,
140
+ intensityLevel: 0.62,
141
+ intensityThreshold: 0.85,
142
+ workouts: [
143
+ upperWorkout('m1w2-u', 'Mon, Jun 9', 'completed'),
144
+ lowerWorkout('m1w2-l', 'Wed, Jun 11', 'completed'),
145
+ ],
146
+ },
147
+ {
148
+ weekNumber: 3,
149
+ intensityLevel: 0.74,
150
+ intensityThreshold: 0.85,
151
+ isCurrent: true,
152
+ workouts: [
153
+ upperWorkout('m1w3-u', 'Mon, Jun 16', 'today'),
154
+ lowerWorkout('m1w3-l', 'Wed, Jun 18', 'upcoming'),
155
+ ],
156
+ },
157
+ {
158
+ weekNumber: 4,
159
+ intensityLevel: 0.35,
160
+ intensityThreshold: 0.85,
161
+ isDeload: true,
162
+ workouts: [
163
+ upperWorkout('m1w4-u', 'Mon, Jun 23', 'upcoming'),
164
+ lowerWorkout('m1w4-l', 'Wed, Jun 25', 'upcoming'),
165
+ ],
166
+ },
167
+ ],
168
+ }
169
+
170
+ const intensification: PlanMeso = {
171
+ id: 'm2',
172
+ name: 'Intensification',
173
+ goal: 'Strength',
174
+ split: 'Push/Pull/Legs',
175
+ weekRange: 'Weeks 5-7',
176
+ weekCount: 3,
177
+ status: 'upcoming',
178
+ volumeHeatmap: [
179
+ { group: 'chest', percentage: 60 },
180
+ { group: 'back', percentage: 70 },
181
+ { group: 'legs', percentage: 80 },
182
+ { group: 'shoulders', percentage: 50 },
183
+ { group: 'arms', percentage: 45 },
184
+ ],
185
+ weeks: [
186
+ {
187
+ weekNumber: 1,
188
+ intensityLevel: 0.78,
189
+ intensityThreshold: 0.9,
190
+ workouts: [upperWorkout('m2w1-u', 'Mon, Jun 30', 'upcoming')],
191
+ },
192
+ {
193
+ weekNumber: 2,
194
+ intensityLevel: 0.86,
195
+ intensityThreshold: 0.9,
196
+ workouts: [lowerWorkout('m2w2-l', 'Mon, Jul 7', 'upcoming')],
197
+ },
198
+ {
199
+ weekNumber: 3,
200
+ intensityLevel: 0.92,
201
+ intensityThreshold: 0.9,
202
+ workouts: [upperWorkout('m2w3-u', 'Mon, Jul 14', 'upcoming')],
203
+ },
204
+ ],
205
+ }
206
+
207
+ const peak: PlanMeso = {
208
+ id: 'm3',
209
+ name: 'Peak',
210
+ goal: 'Peaking',
211
+ split: 'Full Body',
212
+ weekRange: 'Weeks 8-9',
213
+ weekCount: 2,
214
+ status: 'upcoming',
215
+ weeks: [
216
+ {
217
+ weekNumber: 1,
218
+ intensityLevel: 0.95,
219
+ intensityThreshold: 0.95,
220
+ workouts: [upperWorkout('m3w1-u', 'Mon, Jul 21', 'upcoming')],
221
+ },
222
+ {
223
+ weekNumber: 2,
224
+ intensityLevel: 0.6,
225
+ intensityThreshold: 0.95,
226
+ isDeload: true,
227
+ workouts: [lowerWorkout('m3w2-l', 'Mon, Jul 28', 'upcoming')],
228
+ },
229
+ ],
230
+ }
231
+
232
+ const accumulationDone: PlanMeso = {
233
+ ...accumulation,
234
+ id: 'm0',
235
+ name: 'Base',
236
+ status: 'completed',
237
+ weekRange: 'Weeks -3-0',
238
+ }
239
+
240
+ const mesos: PlanMeso[] = [accumulationDone, accumulation, intensification, peak]
241
+
242
+ const meta: Meta<typeof ProgramPlanningPage> = {
243
+ title: 'Custom/Workout/ProgramPlanningPage',
244
+ component: ProgramPlanningPage,
245
+ parameters: { layout: 'fullscreen' },
246
+ tags: ['autodocs'],
247
+ args: { title: 'Program Plan', mesos },
248
+ }
249
+
250
+ export default meta
251
+ type Story = StoryObj<typeof ProgramPlanningPage>
252
+
253
+ export const Default: Story = {}
254
+
255
+ export const SingleMeso: Story = {
256
+ args: { mesos: [accumulation] },
257
+ }
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import {
4
+ ProgramPlanningPage,
5
+ deriveNavLevel,
6
+ toProgressBarMesos,
7
+ findMeso,
8
+ findWeek,
9
+ findWorkout,
10
+ buildBreadcrumbs,
11
+ type PlanMeso,
12
+ } from './ProgramPlanningPage'
13
+
14
+ const workout = (id: string, status: 'completed' | 'today' | 'upcoming') => ({
15
+ id,
16
+ name: `Workout ${id}`,
17
+ date: 'Mon, Jun 2',
18
+ status,
19
+ muscleGroups: [{ group: 'chest', label: 'Chest' as const }],
20
+ totalSets: 12,
21
+ unit: 'lbs' as const,
22
+ exercises: [
23
+ {
24
+ name: 'Bench Press',
25
+ state: 'collapsed' as const,
26
+ onToggle: () => {},
27
+ summary: { sets: 3, reps: 8, weight: 185, unit: 'lbs' as const },
28
+ },
29
+ { name: 'Row', state: 'upcoming' as const, onToggle: () => {}, prescription: '3×10' },
30
+ ],
31
+ })
32
+
33
+ const mesos: PlanMeso[] = [
34
+ {
35
+ id: 'm1',
36
+ name: 'Accumulation',
37
+ goal: 'Hypertrophy',
38
+ split: 'Upper/Lower',
39
+ weekRange: 'Weeks 1-4',
40
+ weekCount: 2,
41
+ currentWeek: 1,
42
+ status: 'current',
43
+ weeks: [
44
+ {
45
+ weekNumber: 1,
46
+ intensityLevel: 0.5,
47
+ isCurrent: true,
48
+ workouts: [workout('w1a', 'today'), workout('w1b', 'upcoming')],
49
+ },
50
+ { weekNumber: 2, intensityLevel: 0.7, workouts: [workout('w2a', 'upcoming')] },
51
+ ],
52
+ },
53
+ {
54
+ id: 'm2',
55
+ name: 'Peak',
56
+ goal: 'Peaking',
57
+ split: 'Full Body',
58
+ weekRange: 'Weeks 5-6',
59
+ weekCount: 1,
60
+ status: 'upcoming',
61
+ weeks: [{ weekNumber: 1, intensityLevel: 0.9, workouts: [workout('w3a', 'upcoming')] }],
62
+ },
63
+ ]
64
+
65
+ describe('deriveNavLevel', () => {
66
+ it('returns the deepest level indicated by the selection cursor', () => {
67
+ expect(deriveNavLevel({ weekNumber: null, workoutId: null })).toBe('meso')
68
+ expect(deriveNavLevel({ weekNumber: 1, workoutId: null })).toBe('week')
69
+ expect(deriveNavLevel({ weekNumber: 1, workoutId: 'w1a' })).toBe('workout')
70
+ })
71
+ })
72
+
73
+ describe('toProgressBarMesos', () => {
74
+ it('projects only the fields MesoProgressBar needs', () => {
75
+ expect(toProgressBarMesos(mesos)).toEqual([
76
+ { id: 'm1', name: 'Accumulation', weekCount: 2, status: 'current', currentWeek: 1 },
77
+ { id: 'm2', name: 'Peak', weekCount: 1, status: 'upcoming', currentWeek: undefined },
78
+ ])
79
+ })
80
+ })
81
+
82
+ describe('find helpers', () => {
83
+ it('locates a meso, week, and workout down the tree', () => {
84
+ const meso = findMeso(mesos, 'm1')
85
+ expect(meso?.name).toBe('Accumulation')
86
+ const week = findWeek(meso, 1)
87
+ expect(week?.workouts).toHaveLength(2)
88
+ expect(findWorkout(week, 'w1b')?.name).toBe('Workout w1b')
89
+ })
90
+
91
+ it('returns null for missing ids or null inputs', () => {
92
+ expect(findMeso(mesos, 'nope')).toBeNull()
93
+ expect(findMeso(mesos, null)).toBeNull()
94
+ expect(findWeek(null, 1)).toBeNull()
95
+ expect(findWorkout(findWeek(findMeso(mesos, 'm1'), 1), 'nope')).toBeNull()
96
+ })
97
+ })
98
+
99
+ describe('buildBreadcrumbs', () => {
100
+ it('grows the trail with drill depth', () => {
101
+ const meso = mesos[0]
102
+ expect(buildBreadcrumbs(meso, null, null).map((c) => c.label)).toEqual(['Accumulation'])
103
+ expect(buildBreadcrumbs(meso, 1, null).map((c) => c.label)).toEqual(['Accumulation', 'Week 1'])
104
+ const wo = findWorkout(findWeek(meso, 1), 'w1a')
105
+ expect(buildBreadcrumbs(meso, 1, wo).map((c) => c.level)).toEqual(['meso', 'week', 'workout'])
106
+ })
107
+ })
108
+
109
+ describe('ProgramPlanningPage', () => {
110
+ it('renders the progress bar and starts at the meso level', () => {
111
+ render(<ProgramPlanningPage mesos={mesos} />)
112
+ expect(screen.getByTestId('program-planning-page')).toBeInTheDocument()
113
+ expect(screen.getByTestId('meso-progress-bar')).toBeInTheDocument()
114
+ expect(screen.getByTestId('program-planning-page-meso-level')).toBeInTheDocument()
115
+ expect(screen.getByTestId('program-planning-page-crumb-meso')).toHaveTextContent('Accumulation')
116
+ })
117
+
118
+ it('drills meso -> week -> workout and rewinds via breadcrumbs', () => {
119
+ render(<ProgramPlanningPage mesos={mesos} />)
120
+
121
+ // Meso level: tapping a week's workout pill drills into that week.
122
+ fireEvent.click(screen.getAllByTestId('workout-pill-pressable')[0])
123
+ expect(screen.getByTestId('program-planning-page-week-level')).toBeInTheDocument()
124
+ expect(screen.getByTestId('program-planning-page-crumb-week')).toHaveTextContent('Week 1')
125
+
126
+ // Week level: the week's two workouts render as cards; open the first.
127
+ expect(screen.getAllByTestId('workout-card')).toHaveLength(2)
128
+ fireEvent.click(screen.getAllByTestId('workout-card-toggle')[0])
129
+ expect(screen.getByTestId('program-planning-page-workout-level')).toBeInTheDocument()
130
+ expect(screen.getByTestId('workout-card-exercises')).toBeInTheDocument()
131
+
132
+ // Breadcrumb rewinds back to the meso overview.
133
+ fireEvent.click(screen.getByTestId('program-planning-page-crumb-meso'))
134
+ expect(screen.getByTestId('program-planning-page-meso-level')).toBeInTheDocument()
135
+ })
136
+
137
+ it('switches the active meso when a progress-bar segment is pressed', () => {
138
+ render(<ProgramPlanningPage mesos={mesos} />)
139
+ fireEvent.click(screen.getByTestId('meso-segment-m2'))
140
+ expect(screen.getByTestId('program-planning-page-crumb-meso')).toHaveTextContent('Peak')
141
+ })
142
+ })