@titan-design/react-ui 0.2.5 → 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/docs/WEB_SETUP.md +217 -0
- package/package.json +17 -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,222 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SetRow } from './SetRow'
|
|
5
|
+
|
|
6
|
+
const baseProps = {
|
|
7
|
+
mode: 'completed' as const,
|
|
8
|
+
setNumber: 1,
|
|
9
|
+
reps: 8,
|
|
10
|
+
weight: 135,
|
|
11
|
+
unit: 'lbs' as const,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('SetRow', () => {
|
|
15
|
+
describe('completed mode', () => {
|
|
16
|
+
it('renders set number, reps, weight', () => {
|
|
17
|
+
render(<SetRow {...baseProps} />)
|
|
18
|
+
expect(screen.getByTestId('set-row')).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByTestId('set-row-set-number')).toHaveTextContent('1')
|
|
20
|
+
expect(screen.getByTestId('set-row-reps')).toHaveTextContent('8')
|
|
21
|
+
expect(screen.getByTestId('set-row-weight')).toHaveTextContent('135')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('displays previous data when provided', () => {
|
|
25
|
+
render(<SetRow {...baseProps} previous={{ reps: 6, weight: 130 }} />)
|
|
26
|
+
expect(screen.getByTestId('set-row-previous')).toHaveTextContent(
|
|
27
|
+
'6 x 130',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('displays dash when no previous data', () => {
|
|
32
|
+
render(<SetRow {...baseProps} />)
|
|
33
|
+
expect(screen.getByTestId('set-row-previous')).toHaveTextContent('\u2014')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('applies reduced opacity for non-next completed sets', () => {
|
|
37
|
+
render(<SetRow {...baseProps} />)
|
|
38
|
+
const row = screen.getByTestId('set-row')
|
|
39
|
+
expect(row).toHaveStyle({ opacity: 0.55 })
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('active mode', () => {
|
|
44
|
+
it('renders target values when reps/weight are null', () => {
|
|
45
|
+
render(
|
|
46
|
+
<SetRow
|
|
47
|
+
mode="active"
|
|
48
|
+
setNumber={2}
|
|
49
|
+
reps={null}
|
|
50
|
+
weight={null}
|
|
51
|
+
unit="lbs"
|
|
52
|
+
targets={{ reps: 10, weight: 150 }}
|
|
53
|
+
/>,
|
|
54
|
+
)
|
|
55
|
+
expect(screen.getByTestId('set-row-target-reps')).toHaveTextContent('10')
|
|
56
|
+
expect(screen.getByTestId('set-row-target-weight')).toHaveTextContent(
|
|
57
|
+
'150',
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders dashes when no targets and values are null', () => {
|
|
62
|
+
render(
|
|
63
|
+
<SetRow mode="active" setNumber={2} reps={null} weight={null} unit="lbs" />,
|
|
64
|
+
)
|
|
65
|
+
expect(screen.getByTestId('set-row-reps')).toHaveTextContent('\u2014')
|
|
66
|
+
expect(screen.getByTestId('set-row-weight')).toHaveTextContent('\u2014')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('isNextSet highlighting', () => {
|
|
71
|
+
it('applies highlight styles for active isNextSet', () => {
|
|
72
|
+
render(
|
|
73
|
+
<SetRow
|
|
74
|
+
mode="active"
|
|
75
|
+
setNumber={1}
|
|
76
|
+
reps={null}
|
|
77
|
+
weight={null}
|
|
78
|
+
unit="lbs"
|
|
79
|
+
isNextSet
|
|
80
|
+
/>,
|
|
81
|
+
)
|
|
82
|
+
const row = screen.getByTestId('set-row')
|
|
83
|
+
const style = row.getAttribute('style') ?? ''
|
|
84
|
+
expect(style).toContain('background-color')
|
|
85
|
+
expect(style).toContain('border')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('does not apply highlight for non-next active sets', () => {
|
|
89
|
+
render(
|
|
90
|
+
<SetRow mode="active" setNumber={1} reps={null} weight={null} unit="lbs" />,
|
|
91
|
+
)
|
|
92
|
+
const row = screen.getByTestId('set-row')
|
|
93
|
+
expect(row).not.toHaveStyle({
|
|
94
|
+
backgroundColor: 'rgba(255,121,0,0.06)',
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('history mode', () => {
|
|
100
|
+
it('renders like completed mode', () => {
|
|
101
|
+
render(
|
|
102
|
+
<SetRow mode="history" setNumber={3} reps={5} weight={200} unit="kg" />,
|
|
103
|
+
)
|
|
104
|
+
expect(screen.getByTestId('set-row-reps')).toHaveTextContent('5')
|
|
105
|
+
expect(screen.getByTestId('set-row-weight')).toHaveTextContent('200')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('velocity strip', () => {
|
|
110
|
+
it('renders velocity strip when velocities provided', () => {
|
|
111
|
+
render(
|
|
112
|
+
<SetRow {...baseProps} velocities={[1.1, 0.95, 0.82]} />,
|
|
113
|
+
)
|
|
114
|
+
expect(screen.getByTestId('set-row-velocity-strip')).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not render velocity strip when no velocities', () => {
|
|
118
|
+
render(<SetRow {...baseProps} />)
|
|
119
|
+
expect(
|
|
120
|
+
screen.queryByTestId('set-row-velocity-strip'),
|
|
121
|
+
).not.toBeInTheDocument()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('passes onVelocityToggle to velocity strip', () => {
|
|
125
|
+
const onToggle = vi.fn()
|
|
126
|
+
render(
|
|
127
|
+
<SetRow
|
|
128
|
+
{...baseProps}
|
|
129
|
+
velocities={[1.1, 0.95]}
|
|
130
|
+
onVelocityToggle={onToggle}
|
|
131
|
+
/>,
|
|
132
|
+
)
|
|
133
|
+
const pressable = screen.getByTestId('velocity-strip-pressable')
|
|
134
|
+
fireEvent.click(pressable)
|
|
135
|
+
expect(onToggle).toHaveBeenCalledOnce()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('set type badge', () => {
|
|
140
|
+
it('renders set type badge instead of set number', () => {
|
|
141
|
+
render(<SetRow {...baseProps} setType="W" />)
|
|
142
|
+
expect(screen.getByTestId('set-row-type-badge')).toHaveTextContent('W')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('does not render type badge when setType is not provided', () => {
|
|
146
|
+
render(<SetRow {...baseProps} />)
|
|
147
|
+
expect(
|
|
148
|
+
screen.queryByTestId('set-row-type-badge'),
|
|
149
|
+
).not.toBeInTheDocument()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('PR badges', () => {
|
|
154
|
+
it('renders PR badges when provided', () => {
|
|
155
|
+
render(
|
|
156
|
+
<SetRow
|
|
157
|
+
{...baseProps}
|
|
158
|
+
prBadges={[{ type: 'e1rm', label: 'PR e1RM' }]}
|
|
159
|
+
/>,
|
|
160
|
+
)
|
|
161
|
+
expect(screen.getByTestId('set-row-pr-badges')).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('does not render PR badges container when no badges', () => {
|
|
165
|
+
render(<SetRow {...baseProps} />)
|
|
166
|
+
expect(
|
|
167
|
+
screen.queryByTestId('set-row-pr-badges'),
|
|
168
|
+
).not.toBeInTheDocument()
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('RPE', () => {
|
|
173
|
+
it('displays RPE value with color coding', () => {
|
|
174
|
+
render(<SetRow {...baseProps} rpe={9.5} />)
|
|
175
|
+
expect(screen.getByTestId('set-row-rpe')).toHaveTextContent('9.5')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('displays dash when RPE is null', () => {
|
|
179
|
+
render(<SetRow {...baseProps} rpe={null} />)
|
|
180
|
+
expect(screen.getByTestId('set-row-rpe')).toHaveTextContent('\u2014')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('accessibility', () => {
|
|
185
|
+
it('has correct accessibility label', () => {
|
|
186
|
+
render(<SetRow {...baseProps} />)
|
|
187
|
+
expect(
|
|
188
|
+
screen.getByLabelText('Set 1: 8 reps at 135 lbs'),
|
|
189
|
+
).toBeInTheDocument()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('has correct accessibility label with null values', () => {
|
|
193
|
+
render(
|
|
194
|
+
<SetRow mode="active" setNumber={2} reps={null} weight={null} unit="kg" />,
|
|
195
|
+
)
|
|
196
|
+
expect(
|
|
197
|
+
screen.getByLabelText('Set 2: no reps'),
|
|
198
|
+
).toBeInTheDocument()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('has no accessibility violations', async () => {
|
|
202
|
+
const { container } = render(<SetRow {...baseProps} />)
|
|
203
|
+
const results = await axe(container)
|
|
204
|
+
expect(results).toHaveNoViolations()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('has no accessibility violations with all features', async () => {
|
|
208
|
+
const { container } = render(
|
|
209
|
+
<SetRow
|
|
210
|
+
{...baseProps}
|
|
211
|
+
rpe={8}
|
|
212
|
+
velocities={[1.1, 0.95]}
|
|
213
|
+
setType="W"
|
|
214
|
+
prBadges={[{ type: 'e1rm', label: 'PR e1RM' }]}
|
|
215
|
+
previous={{ reps: 6, weight: 130 }}
|
|
216
|
+
/>,
|
|
217
|
+
)
|
|
218
|
+
const results = await axe(container)
|
|
219
|
+
expect(results).toHaveNoViolations()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { VelocityStrip } from './VelocityStrip'
|
|
4
|
+
import { PrBadge, type PRType } from './PrBadge'
|
|
5
|
+
|
|
6
|
+
export type SetRowMode = 'active' | 'completed' | 'history'
|
|
7
|
+
|
|
8
|
+
export interface SetRowProps {
|
|
9
|
+
mode: SetRowMode
|
|
10
|
+
setNumber: number
|
|
11
|
+
previous?: { reps: number; weight: number } | null
|
|
12
|
+
reps: number | null
|
|
13
|
+
weight: number | null
|
|
14
|
+
rpe?: number | null
|
|
15
|
+
unit: 'lbs' | 'kg'
|
|
16
|
+
velocities?: number[]
|
|
17
|
+
velocityExpanded?: boolean
|
|
18
|
+
onVelocityToggle?: () => void
|
|
19
|
+
setType?: string
|
|
20
|
+
prBadges?: Array<{ type: PRType; label: string }>
|
|
21
|
+
isNextSet?: boolean
|
|
22
|
+
targets?: { reps: number; weight: number }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getRPEColor(rpe: number): string {
|
|
26
|
+
if (rpe >= 9.5) return '#ff4757'
|
|
27
|
+
if (rpe >= 8.5) return '#ffa502'
|
|
28
|
+
if (rpe >= 7) return '#ffd43b'
|
|
29
|
+
return '#2ed573'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatAccessibilityLabel(
|
|
33
|
+
setNumber: number,
|
|
34
|
+
reps: number | null,
|
|
35
|
+
weight: number | null,
|
|
36
|
+
unit: string,
|
|
37
|
+
): string {
|
|
38
|
+
const repsText = reps != null ? `${reps} reps` : 'no reps'
|
|
39
|
+
const weightText = weight != null ? `at ${weight} ${unit}` : ''
|
|
40
|
+
return `Set ${setNumber}: ${repsText}${weightText ? ` ${weightText}` : ''}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function SetRow({
|
|
44
|
+
mode,
|
|
45
|
+
setNumber,
|
|
46
|
+
previous,
|
|
47
|
+
reps,
|
|
48
|
+
weight,
|
|
49
|
+
rpe,
|
|
50
|
+
unit,
|
|
51
|
+
velocities,
|
|
52
|
+
velocityExpanded,
|
|
53
|
+
onVelocityToggle,
|
|
54
|
+
setType,
|
|
55
|
+
prBadges,
|
|
56
|
+
isNextSet,
|
|
57
|
+
targets,
|
|
58
|
+
}: SetRowProps) {
|
|
59
|
+
const isActive = mode === 'active'
|
|
60
|
+
const isCompleted = mode === 'completed'
|
|
61
|
+
|
|
62
|
+
const rowStyle: Record<string, unknown> = {
|
|
63
|
+
flexDirection: 'row' as const,
|
|
64
|
+
alignItems: 'center' as const,
|
|
65
|
+
padding: 7,
|
|
66
|
+
paddingHorizontal: 8,
|
|
67
|
+
gap: 8,
|
|
68
|
+
borderRadius: 8,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isActive && isNextSet) {
|
|
72
|
+
rowStyle.backgroundColor = 'rgba(255,121,0,0.06)'
|
|
73
|
+
rowStyle.borderWidth = 1
|
|
74
|
+
rowStyle.borderColor = 'rgba(255,121,0,0.15)'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isCompleted && !isNextSet) {
|
|
78
|
+
rowStyle.opacity = 0.55
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View
|
|
83
|
+
style={rowStyle}
|
|
84
|
+
accessibilityLabel={formatAccessibilityLabel(
|
|
85
|
+
setNumber,
|
|
86
|
+
reps,
|
|
87
|
+
weight,
|
|
88
|
+
unit,
|
|
89
|
+
)}
|
|
90
|
+
testID="set-row"
|
|
91
|
+
>
|
|
92
|
+
<View
|
|
93
|
+
style={{ width: 36, alignItems: 'center', justifyContent: 'center' }}
|
|
94
|
+
testID="set-row-set-number"
|
|
95
|
+
>
|
|
96
|
+
{setType ? (
|
|
97
|
+
<Text
|
|
98
|
+
style={{
|
|
99
|
+
fontSize: 10,
|
|
100
|
+
fontWeight: '700',
|
|
101
|
+
fontFamily: 'Inter, sans-serif',
|
|
102
|
+
color: '#ffa502',
|
|
103
|
+
backgroundColor: 'rgba(255,165,2,0.12)',
|
|
104
|
+
paddingVertical: 1,
|
|
105
|
+
paddingHorizontal: 5,
|
|
106
|
+
borderRadius: 3,
|
|
107
|
+
overflow: 'hidden',
|
|
108
|
+
}}
|
|
109
|
+
testID="set-row-type-badge"
|
|
110
|
+
>
|
|
111
|
+
{setType}
|
|
112
|
+
</Text>
|
|
113
|
+
) : (
|
|
114
|
+
<Text
|
|
115
|
+
style={{
|
|
116
|
+
fontSize: 13,
|
|
117
|
+
fontWeight: '600',
|
|
118
|
+
fontFamily: 'Inter, sans-serif',
|
|
119
|
+
color: '#9CA3AF',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{setNumber}
|
|
123
|
+
</Text>
|
|
124
|
+
)}
|
|
125
|
+
</View>
|
|
126
|
+
|
|
127
|
+
<View
|
|
128
|
+
className="flex-1"
|
|
129
|
+
testID="set-row-previous"
|
|
130
|
+
>
|
|
131
|
+
<Text
|
|
132
|
+
style={{
|
|
133
|
+
fontSize: 12,
|
|
134
|
+
fontFamily: 'Inter, sans-serif',
|
|
135
|
+
color: '#9CA3AF',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{previous
|
|
139
|
+
? `${previous.reps} x ${previous.weight}`
|
|
140
|
+
: '\u2014'}
|
|
141
|
+
</Text>
|
|
142
|
+
</View>
|
|
143
|
+
|
|
144
|
+
<View
|
|
145
|
+
style={{ width: 44, alignItems: 'center', justifyContent: 'center' }}
|
|
146
|
+
testID="set-row-reps"
|
|
147
|
+
>
|
|
148
|
+
{isActive && reps == null && targets ? (
|
|
149
|
+
<Text
|
|
150
|
+
style={{
|
|
151
|
+
fontSize: 13,
|
|
152
|
+
fontStyle: 'italic',
|
|
153
|
+
fontFamily: 'Inter, sans-serif',
|
|
154
|
+
color: '#6B7280',
|
|
155
|
+
}}
|
|
156
|
+
testID="set-row-target-reps"
|
|
157
|
+
>
|
|
158
|
+
{targets.reps}
|
|
159
|
+
</Text>
|
|
160
|
+
) : (
|
|
161
|
+
<Text
|
|
162
|
+
style={{
|
|
163
|
+
fontSize: 14,
|
|
164
|
+
fontWeight: '600',
|
|
165
|
+
fontFamily: 'Inter, sans-serif',
|
|
166
|
+
color: reps != null ? '#F3F4F6' : '#6B7280',
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{reps != null ? reps : '\u2014'}
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
172
|
+
</View>
|
|
173
|
+
|
|
174
|
+
<View
|
|
175
|
+
style={{ width: 56, alignItems: 'center', justifyContent: 'center' }}
|
|
176
|
+
testID="set-row-weight"
|
|
177
|
+
>
|
|
178
|
+
{isActive && weight == null && targets ? (
|
|
179
|
+
<Text
|
|
180
|
+
style={{
|
|
181
|
+
fontSize: 13,
|
|
182
|
+
fontStyle: 'italic',
|
|
183
|
+
fontFamily: 'Inter, sans-serif',
|
|
184
|
+
color: '#6B7280',
|
|
185
|
+
}}
|
|
186
|
+
testID="set-row-target-weight"
|
|
187
|
+
>
|
|
188
|
+
{targets.weight}
|
|
189
|
+
</Text>
|
|
190
|
+
) : (
|
|
191
|
+
<Text
|
|
192
|
+
style={{
|
|
193
|
+
fontSize: 14,
|
|
194
|
+
fontWeight: '600',
|
|
195
|
+
fontFamily: 'Inter, sans-serif',
|
|
196
|
+
color: weight != null ? '#F3F4F6' : '#6B7280',
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{weight != null ? weight : '\u2014'}
|
|
200
|
+
</Text>
|
|
201
|
+
)}
|
|
202
|
+
</View>
|
|
203
|
+
|
|
204
|
+
<View
|
|
205
|
+
style={{ width: 36, alignItems: 'center', justifyContent: 'center' }}
|
|
206
|
+
testID="set-row-rpe"
|
|
207
|
+
>
|
|
208
|
+
<Text
|
|
209
|
+
style={{
|
|
210
|
+
fontSize: 13,
|
|
211
|
+
fontWeight: '600',
|
|
212
|
+
fontFamily: 'Inter, sans-serif',
|
|
213
|
+
color: rpe != null ? getRPEColor(rpe) : '#6B7280',
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{rpe != null ? rpe : '\u2014'}
|
|
217
|
+
</Text>
|
|
218
|
+
</View>
|
|
219
|
+
|
|
220
|
+
{prBadges && prBadges.length > 0 && (
|
|
221
|
+
<View
|
|
222
|
+
className="flex-row items-center"
|
|
223
|
+
style={{ gap: 4 }}
|
|
224
|
+
testID="set-row-pr-badges"
|
|
225
|
+
>
|
|
226
|
+
{prBadges.map((badge, i) => (
|
|
227
|
+
<PrBadge
|
|
228
|
+
key={i}
|
|
229
|
+
type={badge.type}
|
|
230
|
+
label={badge.label}
|
|
231
|
+
animate={false}
|
|
232
|
+
compact
|
|
233
|
+
/>
|
|
234
|
+
))}
|
|
235
|
+
</View>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{velocities && velocities.length > 0 && (
|
|
239
|
+
<View
|
|
240
|
+
style={{ position: 'absolute', bottom: 0, left: 8, right: 8 }}
|
|
241
|
+
testID="set-row-velocity-strip"
|
|
242
|
+
>
|
|
243
|
+
<VelocityStrip
|
|
244
|
+
velocities={velocities}
|
|
245
|
+
expanded={velocityExpanded}
|
|
246
|
+
onToggle={onVelocityToggle}
|
|
247
|
+
variant={onVelocityToggle ? 'full' : 'mini'}
|
|
248
|
+
/>
|
|
249
|
+
</View>
|
|
250
|
+
)}
|
|
251
|
+
</View>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Sparkline } from './Sparkline'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Sparkline> = {
|
|
6
|
+
title: 'Custom/Workout/Sparkline',
|
|
7
|
+
component: Sparkline,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
width: {
|
|
11
|
+
control: 'number',
|
|
12
|
+
description: 'Chart width in pixels',
|
|
13
|
+
},
|
|
14
|
+
height: {
|
|
15
|
+
control: 'number',
|
|
16
|
+
description: 'Chart height in pixels',
|
|
17
|
+
},
|
|
18
|
+
color: {
|
|
19
|
+
control: 'color',
|
|
20
|
+
description: 'Line and dot color',
|
|
21
|
+
},
|
|
22
|
+
showDots: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Show dots at data points',
|
|
25
|
+
},
|
|
26
|
+
highlightLast: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
description: 'Highlight the last data point with a larger dot',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default meta
|
|
34
|
+
type Story = StoryObj<typeof Sparkline>
|
|
35
|
+
|
|
36
|
+
const ascending = [10, 15, 22, 28, 35, 42, 50]
|
|
37
|
+
const descending = [50, 45, 38, 30, 22, 15, 10]
|
|
38
|
+
const uCurve = [40, 25, 15, 10, 12, 20, 35]
|
|
39
|
+
const flat = [20, 21, 19, 20, 21, 20, 19]
|
|
40
|
+
const volatile = [10, 35, 15, 45, 20, 40, 25]
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
data: ascending,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Ascending: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
data: ascending,
|
|
51
|
+
showDots: true,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Descending: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
data: descending,
|
|
58
|
+
showDots: true,
|
|
59
|
+
color: '#ff4757',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const UCurve: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
data: uCurve,
|
|
66
|
+
showDots: true,
|
|
67
|
+
color: '#2ed573',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Flat: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
data: flat,
|
|
74
|
+
showDots: true,
|
|
75
|
+
color: '#ffd43b',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const WithReferenceLines: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
data: ascending,
|
|
82
|
+
showDots: true,
|
|
83
|
+
referenceLines: [
|
|
84
|
+
{ value: 30, color: '#ff4757', dashed: true },
|
|
85
|
+
{ value: 45, color: '#2ed573', dashed: false },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const HighlightLast: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
data: ascending,
|
|
93
|
+
highlightLast: true,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const DotsAndHighlightLast: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
data: volatile,
|
|
100
|
+
showDots: true,
|
|
101
|
+
highlightLast: true,
|
|
102
|
+
color: '#ffa502',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const CustomSize: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
data: ascending,
|
|
109
|
+
width: 120,
|
|
110
|
+
height: 50,
|
|
111
|
+
showDots: true,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const AllShapes: Story = {
|
|
116
|
+
render: () => (
|
|
117
|
+
<View style={{ gap: 16, padding: 16 }}>
|
|
118
|
+
<Sparkline data={ascending} showDots highlightLast />
|
|
119
|
+
<Sparkline data={descending} showDots color="#ff4757" />
|
|
120
|
+
<Sparkline data={uCurve} showDots color="#2ed573" />
|
|
121
|
+
<Sparkline data={flat} showDots color="#ffd43b" />
|
|
122
|
+
<Sparkline data={volatile} showDots color="#ffa502" highlightLast />
|
|
123
|
+
</View>
|
|
124
|
+
),
|
|
125
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Sparkline } from './Sparkline'
|
|
5
|
+
|
|
6
|
+
const sampleData = [10, 25, 15, 30, 20]
|
|
7
|
+
|
|
8
|
+
describe('Sparkline', () => {
|
|
9
|
+
it('renders with data points', () => {
|
|
10
|
+
render(<Sparkline data={sampleData} />)
|
|
11
|
+
expect(screen.getByTestId('sparkline')).toBeInTheDocument()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('renders empty state for no data', () => {
|
|
15
|
+
render(<Sparkline data={[]} />)
|
|
16
|
+
expect(screen.getByTestId('sparkline-empty')).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders line segments between points', () => {
|
|
20
|
+
render(<Sparkline data={sampleData} />)
|
|
21
|
+
for (let i = 1; i < sampleData.length; i++) {
|
|
22
|
+
expect(
|
|
23
|
+
screen.getByTestId(`sparkline-segment-${i}`),
|
|
24
|
+
).toBeInTheDocument()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders dots when showDots is true', () => {
|
|
29
|
+
render(<Sparkline data={sampleData} showDots />)
|
|
30
|
+
for (let i = 0; i < sampleData.length; i++) {
|
|
31
|
+
expect(screen.getByTestId(`sparkline-dot-${i}`)).toBeInTheDocument()
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not render dots when showDots is false', () => {
|
|
36
|
+
render(<Sparkline data={sampleData} />)
|
|
37
|
+
expect(screen.queryByTestId('sparkline-dot-0')).not.toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('highlights last dot when highlightLast is true', () => {
|
|
41
|
+
render(<Sparkline data={sampleData} highlightLast />)
|
|
42
|
+
const lastIndex = sampleData.length - 1
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByTestId(`sparkline-dot-${lastIndex}`),
|
|
45
|
+
).toBeInTheDocument()
|
|
46
|
+
// Other dots should not be present (showDots is false)
|
|
47
|
+
expect(screen.queryByTestId('sparkline-dot-0')).not.toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders both regular dots and highlighted last', () => {
|
|
51
|
+
render(<Sparkline data={sampleData} showDots highlightLast />)
|
|
52
|
+
for (let i = 0; i < sampleData.length; i++) {
|
|
53
|
+
expect(screen.getByTestId(`sparkline-dot-${i}`)).toBeInTheDocument()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders reference lines', () => {
|
|
58
|
+
render(
|
|
59
|
+
<Sparkline
|
|
60
|
+
data={sampleData}
|
|
61
|
+
referenceLines={[
|
|
62
|
+
{ value: 20, color: '#ff0000' },
|
|
63
|
+
{ value: 25, color: '#00ff00', dashed: true },
|
|
64
|
+
]}
|
|
65
|
+
/>,
|
|
66
|
+
)
|
|
67
|
+
expect(screen.getByTestId('sparkline-reference-0')).toBeInTheDocument()
|
|
68
|
+
expect(screen.getByTestId('sparkline-reference-1')).toBeInTheDocument()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not render reference lines when not provided', () => {
|
|
72
|
+
render(<Sparkline data={sampleData} />)
|
|
73
|
+
expect(
|
|
74
|
+
screen.queryByTestId('sparkline-reference-0'),
|
|
75
|
+
).not.toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('uses default dimensions', () => {
|
|
79
|
+
render(<Sparkline data={sampleData} />)
|
|
80
|
+
const sparkline = screen.getByTestId('sparkline')
|
|
81
|
+
expect(sparkline).toHaveStyle({ width: '80px', height: '30px' })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('accepts custom dimensions', () => {
|
|
85
|
+
render(<Sparkline data={sampleData} width={120} height={50} />)
|
|
86
|
+
const sparkline = screen.getByTestId('sparkline')
|
|
87
|
+
expect(sparkline).toHaveStyle({ width: '120px', height: '50px' })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('sets accessibility label', () => {
|
|
91
|
+
render(<Sparkline data={sampleData} />)
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByLabelText('Sparkline chart with 5 data points'),
|
|
94
|
+
).toBeInTheDocument()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('sets empty accessibility label for no data', () => {
|
|
98
|
+
render(<Sparkline data={[]} />)
|
|
99
|
+
expect(
|
|
100
|
+
screen.getByLabelText('Sparkline chart, no data'),
|
|
101
|
+
).toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('accessibility', () => {
|
|
105
|
+
it('has no accessibility violations', async () => {
|
|
106
|
+
const { container } = render(
|
|
107
|
+
<Sparkline data={sampleData} showDots highlightLast />,
|
|
108
|
+
)
|
|
109
|
+
const results = await axe(container)
|
|
110
|
+
expect(results).toHaveNoViolations()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|