@titan-design/react-ui 0.6.0 → 0.7.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.js +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +855 -124
- package/dist/index.d.ts +855 -124
- package/dist/index.js +2276 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -802
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1255 -798
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1255 -798
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +15 -10
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SetStrip, velocityZoneColor, SET_STRIP_ZONES, type SetStripSet } from './SetStrip'
|
|
5
|
+
|
|
6
|
+
describe('SetStrip', () => {
|
|
7
|
+
describe('velocityZoneColor', () => {
|
|
8
|
+
it('maps velocity ratios to their zone pins', () => {
|
|
9
|
+
expect(velocityZoneColor(0.4)).toBe(SET_STRIP_ZONES.slow)
|
|
10
|
+
expect(velocityZoneColor(0.6)).toBe(SET_STRIP_ZONES.moderate)
|
|
11
|
+
expect(velocityZoneColor(0.85)).toBe(SET_STRIP_ZONES.fast)
|
|
12
|
+
expect(velocityZoneColor(1.2)).toBe(SET_STRIP_ZONES.fastest)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('uses the real titan ramp pins (not ad-hoc hexes)', () => {
|
|
16
|
+
expect(SET_STRIP_ZONES.slow).toBe('#D14343') // red 600
|
|
17
|
+
expect(SET_STRIP_ZONES.moderate).toBe('#FF7900') // orange 400
|
|
18
|
+
expect(SET_STRIP_ZONES.fast).toBe('#F9B415') // amber 300
|
|
19
|
+
expect(SET_STRIP_ZONES.fastest).toBe('#2ED573') // green 300
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('renders one bar per set', () => {
|
|
24
|
+
const sets: SetStripSet[] = [
|
|
25
|
+
{ status: 'done', velocities: [1, 0.9] },
|
|
26
|
+
{ status: 'done', velocities: [0.8] },
|
|
27
|
+
]
|
|
28
|
+
render(<SetStrip sets={sets} />)
|
|
29
|
+
expect(screen.getAllByTestId('set-strip-set')).toHaveLength(2)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('set statuses', () => {
|
|
33
|
+
it('done: one colored fill segment per rep, no empties, no pulse', () => {
|
|
34
|
+
render(<SetStrip sets={[{ status: 'done', velocities: [1, 0.8, 0.6, 0.4] }]} />)
|
|
35
|
+
expect(screen.getAllByTestId('set-strip-fill')).toHaveLength(4)
|
|
36
|
+
expect(screen.queryByTestId('set-strip-empty')).not.toBeInTheDocument()
|
|
37
|
+
expect(screen.queryByTestId('set-strip-pulse')).not.toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('active: performed reps pulse, the remainder is greyed', () => {
|
|
41
|
+
render(<SetStrip sets={[{ status: 'active', velocities: [0.9, 0.7, 0.5], planned: 8 }]} />)
|
|
42
|
+
expect(screen.getAllByTestId('set-strip-pulse')).toHaveLength(3)
|
|
43
|
+
expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(5)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('todo: a single grey bar, no fills or pulses', () => {
|
|
47
|
+
render(<SetStrip sets={[{ status: 'todo', planned: 10 }]} />)
|
|
48
|
+
expect(screen.getAllByTestId('set-strip-empty')).toHaveLength(1)
|
|
49
|
+
expect(screen.queryByTestId('set-strip-fill')).not.toBeInTheDocument()
|
|
50
|
+
expect(screen.queryByTestId('set-strip-pulse')).not.toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('defaults to an 8px height and honors the height prop', () => {
|
|
55
|
+
const sets: SetStripSet[] = [{ status: 'todo', planned: 3 }]
|
|
56
|
+
const { rerender } = render(<SetStrip sets={sets} />)
|
|
57
|
+
expect(screen.getByTestId('set-strip')).toHaveStyle({ height: '8px' })
|
|
58
|
+
rerender(<SetStrip sets={sets} height={4} />)
|
|
59
|
+
expect(screen.getByTestId('set-strip')).toHaveStyle({ height: '4px' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('exposes a set-progress summary label', () => {
|
|
63
|
+
const sets: SetStripSet[] = [
|
|
64
|
+
{ status: 'done', velocities: [1] },
|
|
65
|
+
{ status: 'active', velocities: [0.9], planned: 3 },
|
|
66
|
+
{ status: 'todo', planned: 3 },
|
|
67
|
+
]
|
|
68
|
+
render(<SetStrip sets={sets} />)
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByLabelText('Set progress: 1 done, 1 in progress, 1 upcoming')
|
|
71
|
+
).toBeInTheDocument()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('categorises set-type variants in the progress summary', () => {
|
|
75
|
+
// drop + myo read as done · a started range reads in progress · myo-upcoming reads upcoming
|
|
76
|
+
const sets: SetStripSet[] = [
|
|
77
|
+
{ status: 'drop', subloads: [[0.8], [0.7]] },
|
|
78
|
+
{ status: 'myo', activation: [0.75], clusters: [[0.6]] },
|
|
79
|
+
{ status: 'range', floor: 15, max: 20, doneVels: [0.9, 0.85] },
|
|
80
|
+
{ status: 'myo-upcoming', activationLen: 12 },
|
|
81
|
+
]
|
|
82
|
+
render(<SetStrip sets={sets} />)
|
|
83
|
+
expect(
|
|
84
|
+
screen.getByLabelText('Set progress: 2 done, 1 in progress, 1 upcoming')
|
|
85
|
+
).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders one bar per set across mixed set-type variants', () => {
|
|
89
|
+
const sets: SetStripSet[] = [
|
|
90
|
+
{ status: 'drop', subloads: [[0.8, 0.7], [0.6]] },
|
|
91
|
+
{ status: 'range', floor: 3, max: 5, doneVels: [] },
|
|
92
|
+
]
|
|
93
|
+
render(<SetStrip sets={sets} />)
|
|
94
|
+
expect(screen.getAllByTestId('set-strip-set')).toHaveLength(2)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('accessibility', () => {
|
|
98
|
+
it('has no accessibility violations', async () => {
|
|
99
|
+
const sets: SetStripSet[] = [
|
|
100
|
+
{ status: 'done', velocities: [1, 0.8] },
|
|
101
|
+
{ status: 'active', velocities: [0.9], planned: 3 },
|
|
102
|
+
{ status: 'todo', planned: 3 },
|
|
103
|
+
]
|
|
104
|
+
const { container } = render(<SetStrip sets={sets} />)
|
|
105
|
+
const results = await axe(container)
|
|
106
|
+
expect(results).toHaveNoViolations()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, type ViewProps } from 'react-native'
|
|
3
|
+
import { SetBar, type SetStripSet } from './SetBar'
|
|
4
|
+
|
|
5
|
+
// Re-exported so the per-set colour vocabulary stays importable from either the
|
|
6
|
+
// molecule (SetStrip) or the atom (SetBar) it now lives in.
|
|
7
|
+
export {
|
|
8
|
+
SET_STRIP_ZONES,
|
|
9
|
+
SET_STRIP_VARIABLE_COLOR,
|
|
10
|
+
velocityZoneColor,
|
|
11
|
+
SetBar,
|
|
12
|
+
type SetBarProps,
|
|
13
|
+
} from './SetBar'
|
|
14
|
+
export type { SetStripSet } from './SetBar'
|
|
15
|
+
|
|
16
|
+
/** Gap between set bars — ~2× the (zero) rep gap, so sets read as discrete units. */
|
|
17
|
+
const SET_STRIP_GAP = 5
|
|
18
|
+
|
|
19
|
+
/** Collapse a set (flat or set-type) to its glance category for the progress summary. */
|
|
20
|
+
function setCategory(set: SetStripSet): 'done' | 'active' | 'upcoming' {
|
|
21
|
+
switch (set.status) {
|
|
22
|
+
case 'done':
|
|
23
|
+
case 'drop':
|
|
24
|
+
case 'myo':
|
|
25
|
+
return 'done'
|
|
26
|
+
case 'active':
|
|
27
|
+
return 'active'
|
|
28
|
+
case 'range':
|
|
29
|
+
return set.doneVels.length > 0 ? 'active' : 'upcoming'
|
|
30
|
+
case 'todo':
|
|
31
|
+
case 'myo-upcoming':
|
|
32
|
+
return 'upcoming'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describeSets(sets: SetStripSet[]): string {
|
|
37
|
+
const done = sets.filter((s) => setCategory(s) === 'done').length
|
|
38
|
+
const active = sets.filter((s) => setCategory(s) === 'active').length
|
|
39
|
+
const upcoming = sets.filter((s) => setCategory(s) === 'upcoming').length
|
|
40
|
+
return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SetStripProps extends ViewProps {
|
|
44
|
+
/** Per-set performance data, in set order. */
|
|
45
|
+
sets: SetStripSet[]
|
|
46
|
+
/** Bar height in px. Default 8. */
|
|
47
|
+
height?: number
|
|
48
|
+
className?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The per-set segmented performance strip: one continuous {@link SetBar} per set
|
|
53
|
+
* (rep intensities as butted color segments, no rep gaps), sets separated by a
|
|
54
|
+
* fixed gap. Fills its container width. Colors are the real titan ramp pins.
|
|
55
|
+
*/
|
|
56
|
+
export function SetStrip({ sets, height = 8, className, ...props }: SetStripProps) {
|
|
57
|
+
return (
|
|
58
|
+
<View
|
|
59
|
+
className={className}
|
|
60
|
+
style={{ flexDirection: 'row', width: '100%', height, gap: SET_STRIP_GAP }}
|
|
61
|
+
accessibilityRole="image"
|
|
62
|
+
accessibilityLabel={describeSets(sets)}
|
|
63
|
+
testID="set-strip"
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
{sets.map((set, i) => (
|
|
67
|
+
<SetBar key={i} set={set} height={height} />
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { SetTableHeader } from './SetTableHeader'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 🚧 WIP / placeholder — `SetTableHeader` (exported as `SetTableHeader`; the generic
|
|
7
|
+
* `TableHeader` name is taken by the DataViz `Table`). The expanded-set-table column
|
|
8
|
+
* header row (SET · PREV · REPS · LOAD · RPE) whose widths mirror `SetRow`. Extracted
|
|
9
|
+
* from `ExerciseCard`; the column set / widths are provisional pending the responsive
|
|
10
|
+
* unification (TD-03.56).
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof SetTableHeader> = {
|
|
13
|
+
title: 'Workout/SetTableHeader',
|
|
14
|
+
component: SetTableHeader,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'**Atom.** The expanded-set-table column header row; per-column widths mirror ' +
|
|
21
|
+
'[SetRow](?path=/docs/workout-setrow--docs) so headers align over the rows. ' +
|
|
22
|
+
'Used-by ↑ [ExerciseCard](?path=/docs/workout-exercisecard--docs) (expanded), which ' +
|
|
23
|
+
'renders it with `showPrevious={false}`. ' +
|
|
24
|
+
'Column set / widths provisional pending TD-03.56 responsive unification.',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
unit: { control: 'select', options: ['lbs', 'kg'] },
|
|
30
|
+
showPrevious: { control: 'boolean' },
|
|
31
|
+
},
|
|
32
|
+
decorators: [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<View style={{ width: 320, backgroundColor: '#131313' }}>
|
|
35
|
+
<Story />
|
|
36
|
+
</View>
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default meta
|
|
42
|
+
type Story = StoryObj<typeof SetTableHeader>
|
|
43
|
+
|
|
44
|
+
export const Lbs: Story = {
|
|
45
|
+
args: { unit: 'lbs' },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Kg: Story = {
|
|
49
|
+
args: { unit: 'kg' },
|
|
50
|
+
parameters: {
|
|
51
|
+
docs: { description: { story: 'The weight column reflects the `kg` unit.' } },
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const NoPrevious: Story = {
|
|
56
|
+
args: { unit: 'lbs', showPrevious: false },
|
|
57
|
+
parameters: {
|
|
58
|
+
docs: {
|
|
59
|
+
description: {
|
|
60
|
+
story:
|
|
61
|
+
'PREV dropped for rail density (`showPrevious={false}`); the column blanks to a flex ' +
|
|
62
|
+
'spacer so REPS/LOAD/RPE hold their positions. Pair with `SetRow`’s `showPrevious`.',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SetTableHeader } from './SetTableHeader'
|
|
5
|
+
|
|
6
|
+
describe('SetTableHeader', () => {
|
|
7
|
+
it('renders the five column labels with the lbs weight column by default', () => {
|
|
8
|
+
render(<SetTableHeader />)
|
|
9
|
+
const header = screen.getByTestId('table-header')
|
|
10
|
+
expect(header).toHaveTextContent('SET')
|
|
11
|
+
expect(header).toHaveTextContent('PREV')
|
|
12
|
+
expect(header).toHaveTextContent('REPS')
|
|
13
|
+
expect(header).toHaveTextContent('LBS')
|
|
14
|
+
expect(header).toHaveTextContent('RPE')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('drops the PREV column when showPrevious is false', () => {
|
|
18
|
+
render(<SetTableHeader showPrevious={false} />)
|
|
19
|
+
const header = screen.getByTestId('table-header')
|
|
20
|
+
expect(header).not.toHaveTextContent('PREV')
|
|
21
|
+
// the other columns remain
|
|
22
|
+
expect(header).toHaveTextContent('SET')
|
|
23
|
+
expect(header).toHaveTextContent('REPS')
|
|
24
|
+
expect(header).toHaveTextContent('RPE')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('shows the KG weight column when unit is kg', () => {
|
|
28
|
+
render(<SetTableHeader unit="kg" />)
|
|
29
|
+
const header = screen.getByTestId('table-header')
|
|
30
|
+
expect(header).toHaveTextContent('KG')
|
|
31
|
+
expect(header).not.toHaveTextContent('LBS')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('honors a custom testID so a host card can keep its own', () => {
|
|
35
|
+
render(<SetTableHeader testID="exercise-card-column-headers" />)
|
|
36
|
+
expect(screen.getByTestId('exercise-card-column-headers')).toBeInTheDocument()
|
|
37
|
+
expect(screen.queryByTestId('table-header')).not.toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('accessibility', () => {
|
|
41
|
+
it('has no accessibility violations', async () => {
|
|
42
|
+
const { container } = render(<SetTableHeader />)
|
|
43
|
+
const results = await axe(container)
|
|
44
|
+
expect(results).toHaveNoViolations()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
|
|
4
|
+
// One header column: its label and its fixed width (PREV is the lone flex column,
|
|
5
|
+
// `undefined`). Order + widths mirror SetRow's cells so headers align over rows.
|
|
6
|
+
type Column = { label: string; width: number | undefined }
|
|
7
|
+
|
|
8
|
+
export interface SetTableHeaderProps {
|
|
9
|
+
/** Weight-column label: LBS / KG. Default lbs. */
|
|
10
|
+
unit?: 'lbs' | 'kg'
|
|
11
|
+
/** Show the PREV (previous-best) column. Default true; false drops it for rail density. */
|
|
12
|
+
showPrevious?: boolean
|
|
13
|
+
/** Overridable so a host card can keep its own testID. Default "table-header". */
|
|
14
|
+
testID?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The previous-best column is the lone flex column (`undefined` width). When hidden
|
|
18
|
+
// it is dropped entirely (not blanked to a spacer) and the four fixed columns
|
|
19
|
+
// distribute across the full strip width via `justifyContent: space-between`.
|
|
20
|
+
const PREV_COLUMN: Column = { label: 'PREV', width: undefined }
|
|
21
|
+
|
|
22
|
+
/** Columns in order, with the weight column reflecting `unit`; PREV dropped when hidden. */
|
|
23
|
+
function buildColumns(unit: 'lbs' | 'kg', showPrevious: boolean): Column[] {
|
|
24
|
+
return [
|
|
25
|
+
{ label: 'SET', width: 36 },
|
|
26
|
+
...(showPrevious ? [PREV_COLUMN] : []),
|
|
27
|
+
{ label: 'REPS', width: 44 },
|
|
28
|
+
{ label: unit.toUpperCase(), width: 56 },
|
|
29
|
+
{ label: 'RPE', width: 36 },
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The expanded-set-table column-header row: SET · PREV · REPS · LOAD · RPE, with
|
|
35
|
+
* the weight column reflecting `unit`. `showPrevious={false}` drops PREV (rail
|
|
36
|
+
* density). Its per-column widths mirror {@link SetRow} so headers align over the
|
|
37
|
+
* rows. Extracted from {@link ExerciseCard}'s expanded state for reuse.
|
|
38
|
+
*/
|
|
39
|
+
export function SetTableHeader({
|
|
40
|
+
unit = 'lbs',
|
|
41
|
+
showPrevious = true,
|
|
42
|
+
testID = 'table-header',
|
|
43
|
+
}: SetTableHeaderProps) {
|
|
44
|
+
const columns = buildColumns(unit, showPrevious)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
className="flex-row"
|
|
49
|
+
style={{
|
|
50
|
+
paddingVertical: 8,
|
|
51
|
+
paddingHorizontal: 8,
|
|
52
|
+
paddingBottom: 4,
|
|
53
|
+
...(showPrevious ? {} : { justifyContent: 'space-between' }),
|
|
54
|
+
}}
|
|
55
|
+
testID={testID}
|
|
56
|
+
>
|
|
57
|
+
{columns.map(({ label, width }) => {
|
|
58
|
+
const isFlex = width === undefined
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
key={label}
|
|
63
|
+
style={{
|
|
64
|
+
...(isFlex ? { minWidth: 44 } : { width, flexShrink: 1 }),
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
gap: 8,
|
|
68
|
+
}}
|
|
69
|
+
{...(isFlex ? { className: 'flex-1' } : {})}
|
|
70
|
+
>
|
|
71
|
+
<Text
|
|
72
|
+
className="text-text-tertiary"
|
|
73
|
+
style={{
|
|
74
|
+
fontSize: 10,
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
fontFamily: 'Inter, sans-serif',
|
|
77
|
+
textTransform: 'uppercase',
|
|
78
|
+
letterSpacing: 0.8,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{label}
|
|
82
|
+
</Text>
|
|
83
|
+
</View>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</View>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { SetsRepsLoad } from './SetsRepsLoad'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `SetsRepsLoad` — the `sets × reps @ load` prescription line in the TempoDisplay
|
|
7
|
+
* visual language (Inter · 600 · letter-spacing 1 · value cells with muted
|
|
8
|
+
* separators).
|
|
9
|
+
*/
|
|
10
|
+
const meta: Meta<typeof SetsRepsLoad> = {
|
|
11
|
+
title: 'Workout/SetsRepsLoad',
|
|
12
|
+
component: SetsRepsLoad,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'**Atom.** The `sets × reps @ load` prescription line in the TempoDisplay visual ' +
|
|
19
|
+
'language. Composes [MetricCell](?path=/docs/workout-metriccell--docs) ' +
|
|
20
|
+
'(shared with [TempoDisplay](?path=/docs/workout-tempodisplay--docs)). ' +
|
|
21
|
+
'Used-by ↑ [ExerciseHeading](?path=/docs/workout-exerciseheading--docs).',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
decorators: [
|
|
26
|
+
(Story) => (
|
|
27
|
+
<View style={{ padding: 16, backgroundColor: '#131313' }}>
|
|
28
|
+
<Story />
|
|
29
|
+
</View>
|
|
30
|
+
),
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default meta
|
|
35
|
+
type Story = StoryObj<typeof SetsRepsLoad>
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: { sets: 5, reps: 8, load: 145, unit: 'lb' },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const RepRange: Story = {
|
|
42
|
+
args: { sets: 3, reps: '15-20', load: 40, unit: 'lb' },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Kilograms: Story = {
|
|
46
|
+
args: { sets: 4, reps: 6, load: 84, unit: 'kg' },
|
|
47
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SetsRepsLoad } from './SetsRepsLoad'
|
|
5
|
+
|
|
6
|
+
describe('SetsRepsLoad', () => {
|
|
7
|
+
it('renders the sets, reps and load values', () => {
|
|
8
|
+
render(<SetsRepsLoad sets={5} reps={8} load={145} />)
|
|
9
|
+
const line = screen.getByTestId('sets-reps-load')
|
|
10
|
+
expect(line).toHaveTextContent('5')
|
|
11
|
+
expect(line).toHaveTextContent('8')
|
|
12
|
+
expect(line).toHaveTextContent('145')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('renders the × and @ separators', () => {
|
|
16
|
+
render(<SetsRepsLoad sets={3} reps={10} load={90} />)
|
|
17
|
+
const line = screen.getByTestId('sets-reps-load')
|
|
18
|
+
expect(line).toHaveTextContent('×')
|
|
19
|
+
expect(line).toHaveTextContent('@')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('defaults the unit label to lb', () => {
|
|
23
|
+
render(<SetsRepsLoad sets={3} reps={10} load={90} />)
|
|
24
|
+
expect(screen.getByTestId('sets-reps-load')).toHaveTextContent('lb')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('accepts a custom unit label', () => {
|
|
28
|
+
render(<SetsRepsLoad sets={3} reps={10} load={40} unit="kg" />)
|
|
29
|
+
expect(screen.getByTestId('sets-reps-load')).toHaveTextContent('kg')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('supports rep ranges', () => {
|
|
33
|
+
render(<SetsRepsLoad sets={3} reps="15-20" load={40} />)
|
|
34
|
+
expect(screen.getByTestId('sets-reps-load')).toHaveTextContent('15-20')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('accessibility', () => {
|
|
38
|
+
it('has no accessibility violations', async () => {
|
|
39
|
+
const { container } = render(<SetsRepsLoad sets={5} reps={8} load={145} />)
|
|
40
|
+
const results = await axe(container)
|
|
41
|
+
expect(results).toHaveNoViolations()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, type ViewProps } from 'react-native'
|
|
3
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { MetricCell } from './metricText'
|
|
5
|
+
|
|
6
|
+
const t = getSemanticColors('dark')
|
|
7
|
+
|
|
8
|
+
const TIMES = '×' // × multiplication sign, padded by muted separators
|
|
9
|
+
const AT = '@'
|
|
10
|
+
|
|
11
|
+
export interface SetsRepsLoadProps extends ViewProps {
|
|
12
|
+
sets: number
|
|
13
|
+
reps: number | string
|
|
14
|
+
load: number
|
|
15
|
+
/** Displayed load unit label (e.g. "lb", "kg"). */
|
|
16
|
+
unit?: string
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The `sets × reps @ load` prescription line, in the TempoDisplay visual language
|
|
22
|
+
* (Inter · 600 · letter-spacing 1 · value cells with muted `×` / `@` separators).
|
|
23
|
+
* Shares the {@link MetricCell} primitive with TempoDisplay so the two stay in step.
|
|
24
|
+
*/
|
|
25
|
+
export function SetsRepsLoad({
|
|
26
|
+
sets,
|
|
27
|
+
reps,
|
|
28
|
+
load,
|
|
29
|
+
unit = 'lb',
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: SetsRepsLoadProps) {
|
|
33
|
+
const value = t['text-primary']
|
|
34
|
+
const sep = t['text-tertiary']
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
className={className}
|
|
39
|
+
style={{ flexDirection: 'row', alignItems: 'baseline' }}
|
|
40
|
+
accessibilityLabel={`${sets} sets of ${reps} reps at ${load} ${unit}`}
|
|
41
|
+
testID="sets-reps-load"
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<MetricCell color={value}>{sets}</MetricCell>
|
|
45
|
+
<MetricCell color={sep}>{` ${TIMES} `}</MetricCell>
|
|
46
|
+
<MetricCell color={value}>{reps}</MetricCell>
|
|
47
|
+
<MetricCell color={sep}>{` ${AT} `}</MetricCell>
|
|
48
|
+
<MetricCell color={value}>{load}</MetricCell>
|
|
49
|
+
<MetricCell color={sep}>{` ${unit}`}</MetricCell>
|
|
50
|
+
</View>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { StatusPill } from './StatusPill'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof StatusPill> = {
|
|
6
|
+
title: 'Workout/StatusPill',
|
|
7
|
+
component: StatusPill,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Auto-regulation verdict pill — a glowing status dot plus colored verdict text in a ' +
|
|
14
|
+
'tinted capsule. Composes `StatusDot`; tint, border, text, and dot all derive from the ' +
|
|
15
|
+
'same semantic status token (success / warning / error).',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
status: {
|
|
21
|
+
control: 'select',
|
|
22
|
+
options: ['productive', 'threshold', 'stop'],
|
|
23
|
+
description: 'Auto-regulation verdict',
|
|
24
|
+
},
|
|
25
|
+
label: {
|
|
26
|
+
control: 'text',
|
|
27
|
+
description: 'Overrides the default verdict text',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default meta
|
|
33
|
+
type Story = StoryObj<typeof StatusPill>
|
|
34
|
+
|
|
35
|
+
export const Productive: Story = {
|
|
36
|
+
args: { status: 'productive' },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Threshold: Story = {
|
|
40
|
+
args: { status: 'threshold' },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Stop: Story = {
|
|
44
|
+
args: { status: 'stop' },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const WithCustomLabel: Story = {
|
|
48
|
+
args: { status: 'stop', label: 'Stop · velocity loss 31%' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const AllStates: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<View style={{ gap: 10, alignItems: 'flex-start' }}>
|
|
54
|
+
<StatusPill status="productive" label="Productive · in the band" />
|
|
55
|
+
<StatusPill status="threshold" label="Threshold · VL20 reached" />
|
|
56
|
+
<StatusPill status="stop" label="Stop · velocity loss 31%" />
|
|
57
|
+
</View>
|
|
58
|
+
),
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { StatusPill, statusPillColor, type StatusPillStatus } from './StatusPill'
|
|
5
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
6
|
+
import { alpha } from '../../../utils/colors'
|
|
7
|
+
|
|
8
|
+
const t = getSemanticColors('dark')
|
|
9
|
+
|
|
10
|
+
const cases: Array<{
|
|
11
|
+
status: StatusPillStatus
|
|
12
|
+
token: 'status-success' | 'status-warning' | 'status-error'
|
|
13
|
+
label: string
|
|
14
|
+
}> = [
|
|
15
|
+
{ status: 'productive', token: 'status-success', label: 'Productive' },
|
|
16
|
+
{ status: 'threshold', token: 'status-warning', label: 'Threshold' },
|
|
17
|
+
{ status: 'stop', token: 'status-error', label: 'Stop' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
describe('StatusPill', () => {
|
|
21
|
+
it('renders the pill container', () => {
|
|
22
|
+
render(<StatusPill status="productive" />)
|
|
23
|
+
expect(screen.getByTestId('status-pill')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('composes a status dot', () => {
|
|
27
|
+
render(<StatusPill status="productive" />)
|
|
28
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('per-status mapping', () => {
|
|
32
|
+
cases.forEach(({ status, token, label }) => {
|
|
33
|
+
it(`maps ${status} to the ${token} color and default label`, () => {
|
|
34
|
+
render(<StatusPill status={status} />)
|
|
35
|
+
const text = screen.getByTestId('status-pill-label')
|
|
36
|
+
expect(text).toHaveTextContent(label)
|
|
37
|
+
expect(text).toHaveStyle({ color: t[token] })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it(`tints the ${status} capsule from the ${token} token`, () => {
|
|
41
|
+
render(<StatusPill status={status} />)
|
|
42
|
+
expect(screen.getByTestId('status-pill')).toHaveStyle({
|
|
43
|
+
backgroundColor: alpha(t[token], 0.14),
|
|
44
|
+
borderTopColor: alpha(t[token], 0.4),
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('exposes the resolved color via statusPillColor', () => {
|
|
51
|
+
expect(statusPillColor('stop')).toBe(t['status-error'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders a custom label override', () => {
|
|
55
|
+
render(<StatusPill status="stop" label="Stop · velocity loss 31%" />)
|
|
56
|
+
expect(screen.getByText('Stop · velocity loss 31%')).toBeInTheDocument()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('has no a11y violations', async () => {
|
|
60
|
+
const { container } = render(<StatusPill status="threshold" />)
|
|
61
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
62
|
+
})
|
|
63
|
+
})
|