@titan-design/react-ui 0.2.7 → 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.
- package/dist/bodymap-BhG55xHM.d.mts +318 -0
- package/dist/bodymap-BhG55xHM.d.ts +318 -0
- package/dist/bodymap.d.mts +3 -0
- package/dist/bodymap.d.ts +3 -0
- package/dist/bodymap.js +2670 -0
- package/dist/bodymap.js.map +1 -0
- package/dist/bodymap.mjs +1101 -0
- package/dist/bodymap.mjs.map +1 -0
- package/dist/{chunk-5SSKGS6E.mjs → chunk-7XPB4NAO.mjs} +124 -4
- package/dist/chunk-7XPB4NAO.mjs.map +1 -0
- package/dist/{chunk-3VLOBS6O.mjs → chunk-P7TSQUN6.mjs} +3 -3
- package/dist/{chunk-3VLOBS6O.mjs.map → chunk-P7TSQUN6.mjs.map} +1 -1
- package/dist/chunk-TOD2WQBG.mjs +1048 -0
- package/dist/chunk-TOD2WQBG.mjs.map +1 -0
- package/dist/index.d.mts +375 -249
- package/dist/index.d.ts +375 -249
- package/dist/index.js +1259 -918
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1314 -2107
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +260 -0
- package/dist/theme/index.d.ts +260 -0
- package/dist/theme/index.js +122 -2
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -2
- package/dist/theme/tokens-css.d.mts +5 -3
- package/dist/theme/tokens-css.d.ts +5 -3
- package/dist/theme/tokens-css.js +318 -9
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +1 -1
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +10 -1
- package/src/bodymap.ts +37 -0
- package/src/components/custom/Table/Table.test.tsx +27 -0
- package/src/components/custom/Table/Table.tsx +33 -17
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +447 -0
- package/src/components/custom/Workout/BodyMap.tsx +5 -6
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +7 -8
- package/src/components/custom/Workout/CapacityBandChart.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.test.tsx +24 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +28 -15
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +630 -0
- package/src/components/custom/Workout/InputBar.tsx +6 -7
- package/src/components/custom/Workout/MesoCard.tsx +4 -2
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +12 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +5 -1
- package/src/components/custom/Workout/MesoStatusCard.tsx +5 -5
- package/src/components/custom/Workout/PlaceholderStrip.tsx +2 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +5 -5
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
- package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +422 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -8
- package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
- package/src/components/custom/Workout/RestTimer.tsx +11 -9
- package/src/components/custom/Workout/SetRow.tsx +13 -19
- package/src/components/custom/Workout/StatusDot.tsx +1 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +5 -5
- package/src/components/custom/Workout/TempoDisplay.tsx +3 -1
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
- package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
- package/src/components/custom/Workout/TrainingStatusPage.tsx +321 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +10 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +16 -15
- package/src/components/custom/Workout/WeekRow.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.tsx +5 -3
- package/src/components/custom/Workout/index.ts +71 -32
- package/src/theme/config.completeness.test.ts +77 -0
- package/src/theme/config.ts +151 -0
- package/src/theme/manifest/design-freeze.test.ts +155 -0
- package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
- package/src/theme/manifest/extract-css-properties.test.ts +112 -0
- package/src/theme/tokens-css.ts +5 -3
- package/src/theme/workout-tokens.ts +12 -14
- package/src/utils/index.ts +10 -0
- package/src/utils/workout-format.test.ts +81 -0
- package/src/utils/workout-format.ts +83 -0
- package/dist/chunk-5SSKGS6E.mjs.map +0 -1
|
@@ -66,9 +66,13 @@ function MesoSegment({ meso, isActive, onPress }: SegmentProps) {
|
|
|
66
66
|
? clampProgress(meso.currentWeek, meso.weekCount)
|
|
67
67
|
: 0
|
|
68
68
|
|
|
69
|
+
// weekCount drives the segment's flex width; guard 0 / negative so flex stays
|
|
70
|
+
// a valid positive value and the segment never collapses out of the layout.
|
|
71
|
+
const segmentFlex = Math.max(1, meso.weekCount)
|
|
72
|
+
|
|
69
73
|
return (
|
|
70
74
|
<Pressable
|
|
71
|
-
style={{ flex:
|
|
75
|
+
style={{ flex: segmentFlex }}
|
|
72
76
|
onPress={() => onPress(meso.id)}
|
|
73
77
|
onPressIn={handlePressIn}
|
|
74
78
|
onPressOut={handlePressOut}
|
|
@@ -7,9 +7,9 @@ import { StatusDot } from './StatusDot'
|
|
|
7
7
|
const BRAND_PRIMARY = '#FF7900'
|
|
8
8
|
const BRAND_PRIMARY_DARK = '#C45E00'
|
|
9
9
|
const BRAND_PRIMARY_LIGHT = '#FFB066'
|
|
10
|
-
const TEXT_PRIMARY = '
|
|
11
|
-
const TEXT_SECONDARY = '
|
|
12
|
-
const TEXT_TERTIARY = '
|
|
10
|
+
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
11
|
+
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
12
|
+
const TEXT_TERTIARY = 'var(--color-text-tertiary)'
|
|
13
13
|
|
|
14
14
|
const SUCCESS = '#14B8A6'
|
|
15
15
|
const WARNING = '#FFB020'
|
|
@@ -18,9 +18,9 @@ const ERROR = '#D14343'
|
|
|
18
18
|
/** Gradient stops for the 3px top accent: dark -> primary -> light (matches MesoCard). */
|
|
19
19
|
const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
|
|
20
20
|
|
|
21
|
-
/** Card surface gradient:
|
|
21
|
+
/** Card surface gradient: elevated -> raised at 135deg (theme-aware). */
|
|
22
22
|
const CARD_GRADIENT =
|
|
23
|
-
'linear-gradient(135deg,
|
|
23
|
+
'linear-gradient(135deg, var(--color-surface-elevated) 0%, var(--color-surface-raised) 100%)'
|
|
24
24
|
|
|
25
25
|
/** Gauge track gradient (teal -> amber -> red) at 0.25 alpha. */
|
|
26
26
|
const GAUGE_GRADIENT =
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { View, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
-
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
5
4
|
|
|
6
5
|
export interface PlaceholderStripProps extends ViewProps {
|
|
7
6
|
width?: number | string
|
|
@@ -26,7 +25,7 @@ function SingleStrip({
|
|
|
26
25
|
backgroundColor: 'transparent',
|
|
27
26
|
borderWidth: 1,
|
|
28
27
|
borderStyle: 'dashed',
|
|
29
|
-
borderColor:
|
|
28
|
+
borderColor: 'var(--color-border-strong)',
|
|
30
29
|
borderRadius: 1,
|
|
31
30
|
opacity: 0.5,
|
|
32
31
|
},
|
|
@@ -67,7 +66,7 @@ function SegmentedStrip({
|
|
|
67
66
|
backgroundColor: 'transparent',
|
|
68
67
|
borderWidth: 1,
|
|
69
68
|
borderStyle: 'dashed',
|
|
70
|
-
borderColor:
|
|
69
|
+
borderColor: 'var(--color-border-strong)',
|
|
71
70
|
borderRadius: 1,
|
|
72
71
|
minWidth: 4,
|
|
73
72
|
}}
|
|
@@ -6,11 +6,11 @@ import { Drawer, DrawerBody } from '../../ui/drawer'
|
|
|
6
6
|
const BRAND_PRIMARY = '#FF7900'
|
|
7
7
|
const RECENT_BORDER = 'rgba(255,176,32,0.3)'
|
|
8
8
|
const RECENT_GLOW = '0 0 12px rgba(255,176,32,0.06)'
|
|
9
|
-
const SURFACE_RAISED = '
|
|
10
|
-
const BORDER_DEFAULT = '
|
|
11
|
-
const TEXT_PRIMARY = '
|
|
12
|
-
const TEXT_SECONDARY = '
|
|
13
|
-
const TEXT_TERTIARY = '
|
|
9
|
+
const SURFACE_RAISED = 'var(--color-surface-raised)'
|
|
10
|
+
const BORDER_DEFAULT = 'var(--color-border-default)'
|
|
11
|
+
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
12
|
+
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
13
|
+
const TEXT_TERTIARY = 'var(--color-text-tertiary)'
|
|
14
14
|
|
|
15
15
|
export type PrRecordType = 'e1rm' | 'weight' | 'reps' | 'volume' | 'velocity'
|
|
16
16
|
|
|
@@ -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
|
+
})
|