@titan-design/react-ui 0.2.7 → 0.4.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 (92) 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 +3014 -0
  6. package/dist/bodymap.js.map +1 -0
  7. package/dist/bodymap.mjs +1088 -0
  8. package/dist/bodymap.mjs.map +1 -0
  9. package/dist/chunk-2SISKTWM.mjs +1027 -0
  10. package/dist/chunk-2SISKTWM.mjs.map +1 -0
  11. package/dist/{chunk-5SSKGS6E.mjs → chunk-7XPB4NAO.mjs} +124 -4
  12. package/dist/chunk-7XPB4NAO.mjs.map +1 -0
  13. package/dist/{chunk-3VLOBS6O.mjs → chunk-SNVKL5WZ.mjs} +42 -5
  14. package/dist/chunk-SNVKL5WZ.mjs.map +1 -0
  15. package/dist/index.d.mts +416 -250
  16. package/dist/index.d.ts +416 -250
  17. package/dist/index.js +1567 -1123
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +1491 -2201
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/theme/index.d.mts +288 -2
  22. package/dist/theme/index.d.ts +288 -2
  23. package/dist/theme/index.js +159 -2
  24. package/dist/theme/index.js.map +1 -1
  25. package/dist/theme/index.mjs +2 -2
  26. package/dist/theme/tokens-css.d.mts +5 -3
  27. package/dist/theme/tokens-css.d.ts +5 -3
  28. package/dist/theme/tokens-css.js +318 -9
  29. package/dist/theme/tokens-css.js.map +1 -1
  30. package/dist/theme/tokens-css.mjs +1 -1
  31. package/dist/theme/tokens-css.mjs.map +1 -1
  32. package/package.json +10 -1
  33. package/src/bodymap.ts +37 -0
  34. package/src/components/custom/Table/Table.test.tsx +27 -0
  35. package/src/components/custom/Table/Table.tsx +33 -17
  36. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
  37. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
  38. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +442 -0
  39. package/src/components/custom/Workout/BodyMap.tsx +7 -9
  40. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +25 -29
  41. package/src/components/custom/Workout/CapacityBandChart.tsx +2 -3
  42. package/src/components/custom/Workout/ExerciseCard.test.tsx +24 -0
  43. package/src/components/custom/Workout/ExerciseCard.tsx +24 -26
  44. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
  45. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
  46. package/src/components/custom/Workout/ExerciseDetailPage.tsx +624 -0
  47. package/src/components/custom/Workout/InputBar.tsx +10 -12
  48. package/src/components/custom/Workout/MesoCard.tsx +2 -4
  49. package/src/components/custom/Workout/MesoProgressBar.test.tsx +12 -0
  50. package/src/components/custom/Workout/MesoProgressBar.tsx +5 -1
  51. package/src/components/custom/Workout/MesoStatusCard.tsx +17 -15
  52. package/src/components/custom/Workout/MuscleGroupChip.tsx +2 -4
  53. package/src/components/custom/Workout/PlaceholderStrip.tsx +2 -4
  54. package/src/components/custom/Workout/PrHistoryModal.tsx +12 -14
  55. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
  56. package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
  57. package/src/components/custom/Workout/ProgramPlanningPage.tsx +418 -0
  58. package/src/components/custom/Workout/ReadinessCheck.tsx +13 -12
  59. package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
  60. package/src/components/custom/Workout/RestTimer.tsx +12 -9
  61. package/src/components/custom/Workout/SetRow.tsx +16 -19
  62. package/src/components/custom/Workout/StatusDot.tsx +1 -0
  63. package/src/components/custom/Workout/StrengthTrendChart.tsx +15 -17
  64. package/src/components/custom/Workout/TempoBar.stories.tsx +95 -0
  65. package/src/components/custom/Workout/TempoBar.test.tsx +103 -0
  66. package/src/components/custom/Workout/TempoBar.tsx +215 -0
  67. package/src/components/custom/Workout/TempoDisplay.tsx +3 -1
  68. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
  69. package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
  70. package/src/components/custom/Workout/TrainingStatusPage.tsx +312 -0
  71. package/src/components/custom/Workout/VelocityStrip.test.tsx +10 -0
  72. package/src/components/custom/Workout/VelocityStrip.tsx +19 -16
  73. package/src/components/custom/Workout/WeekRow.tsx +2 -1
  74. package/src/components/custom/Workout/WorkoutCard.tsx +3 -6
  75. package/src/components/custom/Workout/index.ts +80 -32
  76. package/src/test/nativewind-stub.ts +13 -0
  77. package/src/theme/ThemeProvider.test.tsx +24 -0
  78. package/src/theme/ThemeProvider.tsx +50 -0
  79. package/src/theme/config.completeness.test.ts +77 -0
  80. package/src/theme/config.ts +151 -0
  81. package/src/theme/index.ts +2 -0
  82. package/src/theme/manifest/design-freeze.test.ts +155 -0
  83. package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
  84. package/src/theme/manifest/extract-css-properties.test.ts +112 -0
  85. package/src/theme/resolve-color.ts +19 -0
  86. package/src/theme/tokens-css.ts +5 -3
  87. package/src/theme/workout-tokens.ts +12 -14
  88. package/src/utils/index.ts +10 -0
  89. package/src/utils/workout-format.test.ts +81 -0
  90. package/src/utils/workout-format.ts +83 -0
  91. package/dist/chunk-3VLOBS6O.mjs.map +0 -1
  92. package/dist/chunk-5SSKGS6E.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
+ })