@titan-design/react-ui 0.2.6 → 0.2.7
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/chunk-3VLOBS6O.mjs +846 -0
- package/dist/chunk-3VLOBS6O.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-5SSKGS6E.mjs} +3 -783
- package/dist/chunk-5SSKGS6E.mjs.map +1 -0
- package/dist/index.d.mts +1005 -4
- package/dist/index.d.ts +1005 -4
- package/dist/index.js +6043 -213
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5901 -177
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +57 -1
- package/dist/theme/index.d.ts +57 -1
- package/dist/theme/index.js +63 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +18 -0
- package/dist/theme/tokens-css.d.ts +18 -0
- package/dist/theme/tokens-css.js +289 -0
- package/dist/theme/tokens-css.js.map +1 -0
- package/dist/theme/tokens-css.mjs +24 -0
- package/dist/theme/tokens-css.mjs.map +1 -0
- package/package.json +16 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
- package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
- package/src/components/custom/Gauge/Gauge.tsx +167 -0
- package/src/components/custom/Gauge/index.ts +2 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
- package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
- package/src/components/custom/Scatter/Scatter.tsx +260 -0
- package/src/components/custom/Scatter/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
- package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
- package/src/components/custom/Treemap/Treemap.tsx +192 -0
- package/src/components/custom/Treemap/index.ts +2 -0
- package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
- package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
- package/src/components/custom/Workout/BaseBadge.tsx +113 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
- package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
- package/src/components/custom/Workout/BodyMap.tsx +348 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +478 -0
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
- package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
- package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
- package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
- package/src/components/custom/Workout/DeviationBar.tsx +78 -0
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +319 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +429 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
- package/src/components/custom/Workout/InputBar.test.tsx +116 -0
- package/src/components/custom/Workout/InputBar.tsx +163 -0
- package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
- package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
- package/src/components/custom/Workout/IntensityBar.tsx +166 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
- package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
- package/src/components/custom/Workout/MesoCard.tsx +235 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +141 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +161 -0
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
- package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
- package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
- package/src/components/custom/Workout/PlaceholderStrip.tsx +91 -0
- package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
- package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
- package/src/components/custom/Workout/PrBadge.tsx +103 -0
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
- package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +266 -0
- package/src/components/custom/Workout/RestTimer.tsx +8 -6
- package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
- package/src/components/custom/Workout/SetRow.test.tsx +222 -0
- package/src/components/custom/Workout/SetRow.tsx +253 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
- package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
- package/src/components/custom/Workout/Sparkline.tsx +188 -0
- package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
- package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
- package/src/components/custom/Workout/StatusDot.tsx +159 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
- package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +207 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +234 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +287 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
- package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
- package/src/components/custom/Workout/WeekRow.tsx +140 -0
- package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
- package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
- package/src/components/custom/Workout/WeightBadge.tsx +113 -0
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
- package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +226 -0
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
- package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
- package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
- package/src/components/custom/Workout/index.ts +113 -0
- package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +2 -2
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/card/Card.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +4 -4
- package/src/components/ui/select/Select.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +4 -4
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
- package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
- package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
- package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
- package/src/stories/PresetShowcase.stories.tsx +15 -15
- package/src/theme/index.ts +2 -1
- package/src/theme/manifest/css-property-manifest.example.json +79 -0
- package/src/theme/manifest/css-property-manifest.schema.json +93 -0
- package/src/theme/manifest/css-property-manifest.test.ts +81 -0
- package/src/theme/manifest/css-property-manifest.types.ts +49 -0
- package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
- package/src/theme/manifest/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +34 -0
- package/src/theme/workout-tokens.ts +61 -0
- package/dist/chunk-UXVRPOME.mjs.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { View, Text, Pressable } from 'react-native'
|
|
4
|
+
import {
|
|
5
|
+
BodyMapDetailPanel,
|
|
6
|
+
type ContributingExercise,
|
|
7
|
+
type UpcomingExercise,
|
|
8
|
+
} from './BodyMapDetailPanel'
|
|
9
|
+
import { MuscleGroup } from './muscleTaxonomy'
|
|
10
|
+
|
|
11
|
+
const contributing: ContributingExercise[] = [
|
|
12
|
+
{ name: 'Barbell Bench Press', sets: 4, contributionWeight: 1 },
|
|
13
|
+
{ name: 'Incline Dumbbell Press', sets: 3, contributionWeight: 1 },
|
|
14
|
+
{ name: 'Cable Fly', sets: 3, contributionWeight: 0.75 },
|
|
15
|
+
{ name: 'Overhead Press', sets: 2, contributionWeight: 0.33 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const upcoming: UpcomingExercise[] = [
|
|
19
|
+
{ name: 'Dips', workoutName: 'Push B', sets: 3 },
|
|
20
|
+
{ name: 'Machine Chest Press', workoutName: 'Push B', sets: 3 },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const meta: Meta<typeof BodyMapDetailPanel> = {
|
|
24
|
+
title: 'Custom/Workout/BodyMapDetailPanel',
|
|
25
|
+
component: BodyMapDetailPanel,
|
|
26
|
+
tags: ['autodocs'],
|
|
27
|
+
decorators: [
|
|
28
|
+
(Story) => (
|
|
29
|
+
<View style={{ position: 'relative', height: 640, width: 380, backgroundColor: '#0E0E0E' }}>
|
|
30
|
+
<Story />
|
|
31
|
+
</View>
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
argTypes: {
|
|
35
|
+
displayName: { control: 'text', description: 'Human-readable muscle name' },
|
|
36
|
+
weeklySets: { control: 'number', description: 'Weekly effective sets logged' },
|
|
37
|
+
volumeStatus: {
|
|
38
|
+
control: 'select',
|
|
39
|
+
options: ['under', 'maintenance', 'productive', 'over'],
|
|
40
|
+
description: 'Volume status relative to landmarks',
|
|
41
|
+
},
|
|
42
|
+
isOpen: { control: 'boolean', description: 'Whether the bottom sheet is visible' },
|
|
43
|
+
lastTrained: { control: 'text', description: 'Last trained label' },
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default meta
|
|
48
|
+
type Story = StoryObj<typeof BodyMapDetailPanel>
|
|
49
|
+
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
muscleGroup: MuscleGroup.CHEST,
|
|
53
|
+
displayName: 'Chest',
|
|
54
|
+
weeklySets: 14,
|
|
55
|
+
landmarks: { mev: 8, mav: 14, mrv: 20 },
|
|
56
|
+
volumeStatus: 'productive',
|
|
57
|
+
lastTrained: '2 days ago',
|
|
58
|
+
weeklyHistory: [8, 10, 12, 11, 13, 14],
|
|
59
|
+
contributingExercises: contributing,
|
|
60
|
+
upcomingExercises: upcoming,
|
|
61
|
+
isOpen: true,
|
|
62
|
+
onClose: () => {},
|
|
63
|
+
onViewExercises: () => {},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const UnderTrained: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
...Default.args,
|
|
70
|
+
displayName: 'Rear Delts',
|
|
71
|
+
muscleGroup: MuscleGroup.REAR_DELTS,
|
|
72
|
+
weeklySets: 4,
|
|
73
|
+
landmarks: { mev: 6, mav: 12, mrv: 18 },
|
|
74
|
+
volumeStatus: 'under',
|
|
75
|
+
weeklyHistory: [2, 3, 3, 4],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const Maintenance: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
...Default.args,
|
|
82
|
+
displayName: 'Biceps',
|
|
83
|
+
muscleGroup: MuscleGroup.BICEPS,
|
|
84
|
+
weeklySets: 8,
|
|
85
|
+
landmarks: { mev: 4, mav: 10, mrv: 18 },
|
|
86
|
+
volumeStatus: 'maintenance',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const Productive: Story = {
|
|
91
|
+
args: { ...Default.args },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const OverReaching: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
...Default.args,
|
|
97
|
+
displayName: 'Quads',
|
|
98
|
+
muscleGroup: MuscleGroup.QUADS,
|
|
99
|
+
weeklySets: 20,
|
|
100
|
+
landmarks: { mev: 6, mav: 12, mrv: 18 },
|
|
101
|
+
volumeStatus: 'over',
|
|
102
|
+
weeklyHistory: [12, 14, 16, 18, 20],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const Minimal: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
muscleGroup: MuscleGroup.CALVES,
|
|
109
|
+
displayName: 'Calves',
|
|
110
|
+
weeklySets: 10,
|
|
111
|
+
landmarks: { mev: 6, mav: 10, mrv: 16 },
|
|
112
|
+
volumeStatus: 'maintenance',
|
|
113
|
+
isOpen: true,
|
|
114
|
+
onClose: () => {},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const WithoutHistory: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
...Default.args,
|
|
121
|
+
weeklyHistory: undefined,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const WithoutUpcoming: Story = {
|
|
126
|
+
args: {
|
|
127
|
+
...Default.args,
|
|
128
|
+
upcomingExercises: [],
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function InteractiveBodyMapDetailPanel() {
|
|
133
|
+
const [open, setOpen] = useState(false)
|
|
134
|
+
return (
|
|
135
|
+
<View style={{ position: 'relative', height: 640, width: 380, backgroundColor: '#0E0E0E', padding: 16 }}>
|
|
136
|
+
<Pressable
|
|
137
|
+
onPress={() => setOpen(true)}
|
|
138
|
+
accessibilityRole="button"
|
|
139
|
+
style={{
|
|
140
|
+
alignSelf: 'flex-start',
|
|
141
|
+
backgroundColor: '#FF7900',
|
|
142
|
+
paddingVertical: 10,
|
|
143
|
+
paddingHorizontal: 20,
|
|
144
|
+
borderRadius: 8,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<Text style={{ color: '#FFFFFF', fontWeight: '700', fontFamily: 'Inter, sans-serif' }}>
|
|
148
|
+
Tap Chest
|
|
149
|
+
</Text>
|
|
150
|
+
</Pressable>
|
|
151
|
+
<BodyMapDetailPanel
|
|
152
|
+
muscleGroup={MuscleGroup.CHEST}
|
|
153
|
+
displayName="Chest"
|
|
154
|
+
weeklySets={14}
|
|
155
|
+
landmarks={{ mev: 8, mav: 14, mrv: 20 }}
|
|
156
|
+
volumeStatus="productive"
|
|
157
|
+
lastTrained="2 days ago"
|
|
158
|
+
weeklyHistory={[8, 10, 12, 11, 13, 14]}
|
|
159
|
+
contributingExercises={contributing}
|
|
160
|
+
upcomingExercises={upcoming}
|
|
161
|
+
isOpen={open}
|
|
162
|
+
onClose={() => setOpen(false)}
|
|
163
|
+
onViewExercises={() => {}}
|
|
164
|
+
/>
|
|
165
|
+
</View>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const Interactive: Story = {
|
|
170
|
+
render: () => <InteractiveBodyMapDetailPanel />,
|
|
171
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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 {
|
|
5
|
+
BodyMapDetailPanel,
|
|
6
|
+
type ContributingExercise,
|
|
7
|
+
type UpcomingExercise,
|
|
8
|
+
} from './BodyMapDetailPanel'
|
|
9
|
+
import { MuscleGroup } from './muscleTaxonomy'
|
|
10
|
+
|
|
11
|
+
const contributing: ContributingExercise[] = [
|
|
12
|
+
{ name: 'Barbell Bench Press', sets: 4, contributionWeight: 1 },
|
|
13
|
+
{ name: 'Cable Fly', sets: 3, contributionWeight: 0.75 },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const upcoming: UpcomingExercise[] = [
|
|
17
|
+
{ name: 'Dips', workoutName: 'Push B', sets: 3 },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const baseProps = {
|
|
21
|
+
muscleGroup: MuscleGroup.CHEST,
|
|
22
|
+
displayName: 'Chest',
|
|
23
|
+
weeklySets: 14,
|
|
24
|
+
landmarks: { mev: 8, mav: 14, mrv: 20 },
|
|
25
|
+
volumeStatus: 'productive' as const,
|
|
26
|
+
lastTrained: '2 days ago',
|
|
27
|
+
weeklyHistory: [8, 10, 12, 14],
|
|
28
|
+
contributingExercises: contributing,
|
|
29
|
+
upcomingExercises: upcoming,
|
|
30
|
+
isOpen: true,
|
|
31
|
+
onClose: vi.fn(),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('BodyMapDetailPanel', () => {
|
|
35
|
+
describe('rendering', () => {
|
|
36
|
+
it('renders the panel with the muscle title when open', () => {
|
|
37
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
38
|
+
expect(screen.getByTestId('body-map-detail-panel')).toBeInTheDocument()
|
|
39
|
+
expect(screen.getByTestId('body-map-detail-panel-title')).toHaveTextContent('Chest')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('does not render when closed', () => {
|
|
43
|
+
render(<BodyMapDetailPanel {...baseProps} isOpen={false} />)
|
|
44
|
+
expect(screen.queryByTestId('body-map-detail-panel')).not.toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('supports the `visible` alias for visibility', () => {
|
|
48
|
+
const { isOpen: _isOpen, ...rest } = baseProps
|
|
49
|
+
render(<BodyMapDetailPanel {...rest} visible />)
|
|
50
|
+
expect(screen.getByTestId('body-map-detail-panel')).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders the drag handle, status badge, and last-trained line', () => {
|
|
54
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
55
|
+
expect(screen.getByTestId('body-map-detail-panel-handle')).toBeInTheDocument()
|
|
56
|
+
expect(screen.getByTestId('body-map-detail-panel-status-badge')).toHaveTextContent(
|
|
57
|
+
'productive',
|
|
58
|
+
)
|
|
59
|
+
expect(screen.getByTestId('body-map-detail-panel-last-trained')).toHaveTextContent(
|
|
60
|
+
'Last trained 2 days ago',
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('shows the weekly set count and MRV suffix', () => {
|
|
65
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
66
|
+
expect(screen.getByTestId('body-map-detail-panel-set-count')).toHaveTextContent('14')
|
|
67
|
+
expect(screen.getByTestId('body-map-detail-panel-mrv-suffix')).toHaveTextContent('/ 20 MRV')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('volume bar', () => {
|
|
72
|
+
it('positions the marker at the midpoint when sets sit between MEV and MRV', () => {
|
|
73
|
+
// mev 8, mrv 20, sets 14 -> (14-8)/(20-8) = 0.5
|
|
74
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
75
|
+
const marker = screen.getByTestId('body-map-detail-panel-volume-marker')
|
|
76
|
+
expect(marker.getAttribute('style') ?? '').toContain('left: 50%')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('clamps the marker to the left edge below MEV', () => {
|
|
80
|
+
render(<BodyMapDetailPanel {...baseProps} weeklySets={2} />)
|
|
81
|
+
const marker = screen.getByTestId('body-map-detail-panel-volume-marker')
|
|
82
|
+
expect(marker.getAttribute('style') ?? '').toContain('left: 0%')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('clamps the marker to the right edge at or above MRV', () => {
|
|
86
|
+
render(<BodyMapDetailPanel {...baseProps} weeklySets={24} />)
|
|
87
|
+
const marker = screen.getByTestId('body-map-detail-panel-volume-marker')
|
|
88
|
+
expect(marker.getAttribute('style') ?? '').toContain('left: 100%')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('exposes the volume bar as a progressbar reflecting sets and landmarks', () => {
|
|
92
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
93
|
+
const bar = screen.getByTestId('body-map-detail-panel-volume-bar')
|
|
94
|
+
expect(bar).toHaveAttribute('aria-valuenow', '14')
|
|
95
|
+
expect(bar).toHaveAttribute('aria-valuemin', '8')
|
|
96
|
+
expect(bar).toHaveAttribute('aria-valuemax', '20')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('lists', () => {
|
|
101
|
+
it('renders contributing exercises with sets and contribution', () => {
|
|
102
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
103
|
+
expect(screen.getByTestId('body-map-detail-panel-contributing-0-name')).toHaveTextContent(
|
|
104
|
+
'Barbell Bench Press',
|
|
105
|
+
)
|
|
106
|
+
expect(screen.getByTestId('body-map-detail-panel-contributing-0-detail')).toHaveTextContent(
|
|
107
|
+
'4 sets · 100%',
|
|
108
|
+
)
|
|
109
|
+
expect(screen.getByTestId('body-map-detail-panel-contributing-1-detail')).toHaveTextContent(
|
|
110
|
+
'3 sets · 75%',
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('renders upcoming exercises with workout name and sets', () => {
|
|
115
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
116
|
+
expect(screen.getByTestId('body-map-detail-panel-upcoming-0-name')).toHaveTextContent('Dips')
|
|
117
|
+
expect(screen.getByTestId('body-map-detail-panel-upcoming-0-detail')).toHaveTextContent(
|
|
118
|
+
'Push B · 3 sets',
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders the volume sparkline when history is supplied', () => {
|
|
123
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
124
|
+
expect(screen.getByTestId('body-map-detail-panel-sparkline')).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('does not crash and omits sections for empty optional arrays', () => {
|
|
128
|
+
render(
|
|
129
|
+
<BodyMapDetailPanel
|
|
130
|
+
{...baseProps}
|
|
131
|
+
contributingExercises={[]}
|
|
132
|
+
upcomingExercises={[]}
|
|
133
|
+
weeklyHistory={[]}
|
|
134
|
+
/>,
|
|
135
|
+
)
|
|
136
|
+
expect(screen.getByTestId('body-map-detail-panel')).toBeInTheDocument()
|
|
137
|
+
expect(screen.queryByTestId('body-map-detail-panel-contributing')).not.toBeInTheDocument()
|
|
138
|
+
expect(screen.queryByTestId('body-map-detail-panel-upcoming')).not.toBeInTheDocument()
|
|
139
|
+
expect(screen.queryByTestId('body-map-detail-panel-sparkline')).not.toBeInTheDocument()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('omits the last-trained line and view-exercises button when not provided', () => {
|
|
143
|
+
const { lastTrained: _lt, ...rest } = baseProps
|
|
144
|
+
render(<BodyMapDetailPanel {...rest} />)
|
|
145
|
+
expect(screen.queryByTestId('body-map-detail-panel-last-trained')).not.toBeInTheDocument()
|
|
146
|
+
expect(screen.queryByTestId('body-map-detail-panel-view-exercises')).not.toBeInTheDocument()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('interaction', () => {
|
|
151
|
+
it('calls onClose when the backdrop is pressed', () => {
|
|
152
|
+
const onClose = vi.fn()
|
|
153
|
+
render(<BodyMapDetailPanel {...baseProps} onClose={onClose} />)
|
|
154
|
+
fireEvent.click(screen.getByTestId('body-map-detail-panel-backdrop'))
|
|
155
|
+
expect(onClose).toHaveBeenCalledOnce()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('calls onClose when the handle is pressed', () => {
|
|
159
|
+
const onClose = vi.fn()
|
|
160
|
+
render(<BodyMapDetailPanel {...baseProps} onClose={onClose} />)
|
|
161
|
+
fireEvent.click(screen.getByTestId('body-map-detail-panel-handle'))
|
|
162
|
+
expect(onClose).toHaveBeenCalledOnce()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('calls onClose when the close button is pressed', () => {
|
|
166
|
+
const onClose = vi.fn()
|
|
167
|
+
render(<BodyMapDetailPanel {...baseProps} onClose={onClose} />)
|
|
168
|
+
fireEvent.click(screen.getByTestId('body-map-detail-panel-close'))
|
|
169
|
+
expect(onClose).toHaveBeenCalledOnce()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('calls onViewExercises when the view button is pressed', () => {
|
|
173
|
+
const onViewExercises = vi.fn()
|
|
174
|
+
render(<BodyMapDetailPanel {...baseProps} onViewExercises={onViewExercises} />)
|
|
175
|
+
fireEvent.click(screen.getByTestId('body-map-detail-panel-view-exercises'))
|
|
176
|
+
expect(onViewExercises).toHaveBeenCalledOnce()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('accessibility', () => {
|
|
181
|
+
it('exposes a dialog role labelled with the muscle', () => {
|
|
182
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
183
|
+
const dialog = screen.getByRole('dialog')
|
|
184
|
+
expect(dialog).toHaveAttribute('aria-label', 'Chest volume details')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('labels the close affordances', () => {
|
|
188
|
+
render(<BodyMapDetailPanel {...baseProps} />)
|
|
189
|
+
expect(screen.getAllByLabelText('Close Chest details').length).toBeGreaterThan(0)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('has no accessibility violations with full content', async () => {
|
|
193
|
+
const { container } = render(
|
|
194
|
+
<BodyMapDetailPanel {...baseProps} onViewExercises={vi.fn()} />,
|
|
195
|
+
)
|
|
196
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('has no accessibility violations for each volume status', async () => {
|
|
200
|
+
const statuses = ['under', 'maintenance', 'productive', 'over'] as const
|
|
201
|
+
for (const volumeStatus of statuses) {
|
|
202
|
+
const { container, unmount } = render(
|
|
203
|
+
<BodyMapDetailPanel {...baseProps} volumeStatus={volumeStatus} />,
|
|
204
|
+
)
|
|
205
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
206
|
+
unmount()
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('has no accessibility violations with only required props', async () => {
|
|
211
|
+
const { container } = render(
|
|
212
|
+
<BodyMapDetailPanel
|
|
213
|
+
muscleGroup={MuscleGroup.CALVES}
|
|
214
|
+
displayName="Calves"
|
|
215
|
+
weeklySets={10}
|
|
216
|
+
landmarks={{ mev: 6, mav: 10, mrv: 16 }}
|
|
217
|
+
volumeStatus="maintenance"
|
|
218
|
+
isOpen
|
|
219
|
+
onClose={vi.fn()}
|
|
220
|
+
/>,
|
|
221
|
+
)
|
|
222
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|