@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,200 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { ZoneTrack, type ZoneTrackZone } from './ZoneTrack'
|
|
5
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
6
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
7
|
+
|
|
8
|
+
const { green, yellow, orange, red } = WORKOUT_TOKENS.scale
|
|
9
|
+
const t = getSemanticColors('dark')
|
|
10
|
+
|
|
11
|
+
const ZONES: ZoneTrackZone[] = [
|
|
12
|
+
{ upTo: 10, color: green },
|
|
13
|
+
{ upTo: 20, color: yellow },
|
|
14
|
+
{ upTo: 30, color: orange },
|
|
15
|
+
{ upTo: 40, color: red },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
describe('ZoneTrack', () => {
|
|
19
|
+
it('renders one band per zone', () => {
|
|
20
|
+
render(<ZoneTrack zones={ZONES} max={40} />)
|
|
21
|
+
expect(screen.getAllByTestId('zone-track-band')).toHaveLength(4)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('colours each band from the supplied zone token (literal hex, not a var ref)', () => {
|
|
25
|
+
render(<ZoneTrack zones={ZONES} max={40} />)
|
|
26
|
+
const bands = screen.getAllByTestId('zone-track-band')
|
|
27
|
+
expect(bands[0]).toHaveStyle({ backgroundColor: green })
|
|
28
|
+
expect(bands[3]).toHaveStyle({ backgroundColor: red })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('weights each band by its domain span', () => {
|
|
32
|
+
render(
|
|
33
|
+
<ZoneTrack
|
|
34
|
+
zones={[
|
|
35
|
+
{ upTo: 30, color: green },
|
|
36
|
+
{ upTo: 40, color: red },
|
|
37
|
+
]}
|
|
38
|
+
max={40}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
const bands = screen.getAllByTestId('zone-track-band')
|
|
42
|
+
expect(bands[0]).toHaveStyle({ flexGrow: 30 })
|
|
43
|
+
expect(bands[1]).toHaveStyle({ flexGrow: 10 })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders no marker when none is supplied', () => {
|
|
47
|
+
render(<ZoneTrack zones={ZONES} max={40} />)
|
|
48
|
+
expect(screen.queryByTestId('zone-track-needle')).not.toBeInTheDocument()
|
|
49
|
+
expect(screen.queryByTestId('zone-track-fill')).not.toBeInTheDocument()
|
|
50
|
+
expect(screen.queryByTestId('zone-track-unfilled')).not.toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('positions a needle marker at the value fraction of the domain', () => {
|
|
54
|
+
render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'needle', value: 10 }} />)
|
|
55
|
+
expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '25%' })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('clamps a needle past the max to the track end', () => {
|
|
59
|
+
render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'needle', value: 100 }} />)
|
|
60
|
+
expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '100%' })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('clip-reveals the gradient for a fill marker without a colour', () => {
|
|
64
|
+
render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 20 }} />)
|
|
65
|
+
expect(screen.getByTestId('zone-track-unfilled')).toHaveStyle({ left: '50%' })
|
|
66
|
+
expect(screen.queryByTestId('zone-track-fill')).not.toBeInTheDocument()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('draws a solid trend fill when the fill marker has a colour', () => {
|
|
70
|
+
render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 30, color: red }} />)
|
|
71
|
+
const fill = screen.getByTestId('zone-track-fill')
|
|
72
|
+
expect(fill).toHaveStyle({ width: '75%', backgroundColor: red })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('renders a tick with its label per entry', () => {
|
|
76
|
+
render(
|
|
77
|
+
<ZoneTrack
|
|
78
|
+
zones={ZONES}
|
|
79
|
+
max={40}
|
|
80
|
+
ticks={[
|
|
81
|
+
{ value: 0, label: 'fresh' },
|
|
82
|
+
{ value: 40, label: 'stop' },
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
expect(screen.getAllByTestId('zone-track-tick')).toHaveLength(2)
|
|
87
|
+
expect(screen.getByText('fresh')).toBeInTheDocument()
|
|
88
|
+
expect(screen.getByText('stop')).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('draws a range highlight only when a band is supplied', () => {
|
|
92
|
+
const { rerender } = render(<ZoneTrack zones={ZONES} max={40} />)
|
|
93
|
+
expect(screen.queryByTestId('zone-track-band-highlight')).not.toBeInTheDocument()
|
|
94
|
+
rerender(
|
|
95
|
+
<ZoneTrack
|
|
96
|
+
zones={ZONES}
|
|
97
|
+
max={40}
|
|
98
|
+
band={{ from: 10, to: 20, color: 'rgba(255,255,255,0.2)' }}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
expect(screen.getByTestId('zone-track-band-highlight')).toHaveStyle({
|
|
102
|
+
left: '25%',
|
|
103
|
+
width: '25%',
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// RNW does not map accessibilityValue → aria-valuenow under jsdom (gotcha #11), so the
|
|
108
|
+
// value is verified via the marker position + the accessible name, not aria-valuenow.
|
|
109
|
+
it('exposes the progressbar role with an accessible name', () => {
|
|
110
|
+
render(
|
|
111
|
+
<ZoneTrack
|
|
112
|
+
zones={ZONES}
|
|
113
|
+
max={40}
|
|
114
|
+
marker={{ type: 'needle', value: 12 }}
|
|
115
|
+
accessibilityLabel="Fatigue meter"
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
expect(screen.getByRole('progressbar', { name: 'Fatigue meter' })).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('accessibility', () => {
|
|
122
|
+
it('has no accessibility violations', async () => {
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<ZoneTrack
|
|
125
|
+
zones={ZONES}
|
|
126
|
+
max={40}
|
|
127
|
+
marker={{ type: 'needle', value: 21 }}
|
|
128
|
+
ticks={[
|
|
129
|
+
{ value: 0, label: 'fresh' },
|
|
130
|
+
{ value: 40, label: 'stop' },
|
|
131
|
+
]}
|
|
132
|
+
accessibilityLabel="Fatigue meter"
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
const results = await axe(container)
|
|
136
|
+
expect(results).toHaveNoViolations()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('glow', () => {
|
|
141
|
+
it('haloes the track when the marker has glow', () => {
|
|
142
|
+
render(
|
|
143
|
+
<ZoneTrack
|
|
144
|
+
zones={ZONES}
|
|
145
|
+
max={40}
|
|
146
|
+
marker={{ type: 'fill', value: 24, color: orange, glow: true }}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
expect((screen.getByTestId('zone-track-track') as HTMLElement).style.boxShadow).not.toBe('')
|
|
150
|
+
})
|
|
151
|
+
it('has no glow by default', () => {
|
|
152
|
+
render(
|
|
153
|
+
<ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 24, color: orange }} />
|
|
154
|
+
)
|
|
155
|
+
expect((screen.getByTestId('zone-track-track') as HTMLElement).style.boxShadow).toBe('')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('ticks (colored lines + labels + tooltip)', () => {
|
|
160
|
+
it('renders a line and label per tick', () => {
|
|
161
|
+
render(
|
|
162
|
+
<ZoneTrack
|
|
163
|
+
zones={ZONES}
|
|
164
|
+
max={40}
|
|
165
|
+
ticks={[
|
|
166
|
+
{ value: 10, label: 'A' },
|
|
167
|
+
{ value: 30, label: 'B' },
|
|
168
|
+
]}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
expect(screen.getAllByTestId('zone-track-tick-line')).toHaveLength(2)
|
|
172
|
+
expect(screen.getAllByTestId('zone-track-tick-label').map((e) => e.textContent)).toEqual([
|
|
173
|
+
'A',
|
|
174
|
+
'B',
|
|
175
|
+
])
|
|
176
|
+
})
|
|
177
|
+
it('colours an emphasized tick line with the brand accent', () => {
|
|
178
|
+
render(
|
|
179
|
+
<ZoneTrack zones={ZONES} max={40} ticks={[{ value: 20, label: 'T', emphasized: true }]} />
|
|
180
|
+
)
|
|
181
|
+
expect(screen.getByTestId('zone-track-tick-line')).toHaveStyle({
|
|
182
|
+
backgroundColor: t['brand-primary'],
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
it('applies a per-tick color override to the line', () => {
|
|
186
|
+
render(<ZoneTrack zones={ZONES} max={40} ticks={[{ value: 20, label: 'X', color: red }]} />)
|
|
187
|
+
expect(screen.getByTestId('zone-track-tick-line')).toHaveStyle({ backgroundColor: red })
|
|
188
|
+
})
|
|
189
|
+
it('renders a tooltip label trigger', () => {
|
|
190
|
+
render(
|
|
191
|
+
<ZoneTrack
|
|
192
|
+
zones={ZONES}
|
|
193
|
+
max={40}
|
|
194
|
+
ticks={[{ value: 20, label: 'MAV', tooltip: 'Maximum Adaptive Volume' }]}
|
|
195
|
+
/>
|
|
196
|
+
)
|
|
197
|
+
expect(screen.getByText('MAV')).toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text, type ViewProps, type DimensionValue } from 'react-native'
|
|
3
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { primitiveColors } from '../../../theme/tokens/primitives'
|
|
5
|
+
import { alpha } from '../../../utils/colors'
|
|
6
|
+
import { Tooltip } from '../../ui/tooltip/Tooltip'
|
|
7
|
+
|
|
8
|
+
const t = getSemanticColors('dark')
|
|
9
|
+
|
|
10
|
+
/** Soft two-stop glow in `color`, for a marker's optional `glow`. */
|
|
11
|
+
function glowShadow(color: string): string {
|
|
12
|
+
return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Muted, un-reached track colour — a charcoal step, matches the IntensityBar track family. */
|
|
16
|
+
const DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200]
|
|
17
|
+
/** Default needle / fill-marker colour. */
|
|
18
|
+
const DEFAULT_MARKER_COLOR = primitiveColors.white
|
|
19
|
+
/** Tick mark + tick label colour. */
|
|
20
|
+
const TICK_COLOR = t['text-tertiary']
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TRACK_HEIGHT = 14
|
|
23
|
+
const DEFAULT_NEEDLE_OVERHANG = 6
|
|
24
|
+
const NEEDLE_WIDTH = 4
|
|
25
|
+
|
|
26
|
+
/** A single colour band of the zone gradient, spanning `[prev.upTo, upTo]` of the domain. */
|
|
27
|
+
export interface ZoneTrackZone {
|
|
28
|
+
/** Upper bound of this band in domain units. The last band's `upTo` should reach `max`. */
|
|
29
|
+
upTo: number
|
|
30
|
+
/** Band fill colour — a literal hex (RNW-safe), sourced from a ramp/effort-scale token. */
|
|
31
|
+
color: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A tick at `value`: a colored line over the track + an optional compact label
|
|
36
|
+
* below, with an optional tooltip (to expand an acronym / show the raw value so
|
|
37
|
+
* the label footer can stay light). Multiple ticks are supported.
|
|
38
|
+
*/
|
|
39
|
+
export interface ZoneTrackTick {
|
|
40
|
+
/** Position of the tick in domain units. */
|
|
41
|
+
value: number
|
|
42
|
+
/** Optional compact label rendered under the tick. */
|
|
43
|
+
label?: string
|
|
44
|
+
/** Line + label colour. Defaults to a muted tick colour (or brand when `emphasized`). */
|
|
45
|
+
color?: string
|
|
46
|
+
/** Emphasize this tick (thicker line, brand colour by default) — e.g. a target landmark. */
|
|
47
|
+
emphasized?: boolean
|
|
48
|
+
/** Tooltip content shown on hover (web) / long-press (native) — expand the acronym, show the raw value. */
|
|
49
|
+
tooltip?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The value marker sitting on the track.
|
|
54
|
+
* - `needle`: a vertical line at `value`, overhanging the track (FatigueMeter / TrainingLoadGauge).
|
|
55
|
+
* - `fill`: a left-anchored fill from `min` to `value`. Omit `color` to clip-reveal the zone
|
|
56
|
+
* gradient up to `value`; supply `color` for a solid trend-coloured fill (FatigueGauge-style).
|
|
57
|
+
*/
|
|
58
|
+
export type ZoneTrackMarker =
|
|
59
|
+
| { type: 'needle'; value: number; color?: string; glow?: boolean }
|
|
60
|
+
| { type: 'fill'; value: number; color?: string; glow?: boolean }
|
|
61
|
+
|
|
62
|
+
/** An optional translucent range highlight (e.g. RpeCalibration's predicted CI band). */
|
|
63
|
+
export interface ZoneTrackBand {
|
|
64
|
+
/** Range start in domain units. */
|
|
65
|
+
from: number
|
|
66
|
+
/** Range end in domain units. */
|
|
67
|
+
to: number
|
|
68
|
+
/** Highlight colour (typically a translucent token). */
|
|
69
|
+
color: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ZoneTrackProps extends ViewProps {
|
|
73
|
+
/** Ordered zone bands, laid left→right; each covers `[previous.upTo, upTo]` of the domain. */
|
|
74
|
+
zones: ZoneTrackZone[]
|
|
75
|
+
/** Domain maximum (right edge). */
|
|
76
|
+
max: number
|
|
77
|
+
/** Domain minimum (left edge). Default 0. */
|
|
78
|
+
min?: number
|
|
79
|
+
/** The value marker — a needle line or a left-anchored fill. Omit for a bare gradient track. */
|
|
80
|
+
marker?: ZoneTrackMarker | null
|
|
81
|
+
/** Optional tick marks with labels below the track. */
|
|
82
|
+
ticks?: ZoneTrackTick[]
|
|
83
|
+
/** Optional translucent range highlight drawn over the gradient. */
|
|
84
|
+
band?: ZoneTrackBand | null
|
|
85
|
+
/** Track height in px. Default 14. */
|
|
86
|
+
trackHeight?: number
|
|
87
|
+
/** How far a needle marker overhangs the track top + bottom, in px. Default 6. */
|
|
88
|
+
needleOverhang?: number
|
|
89
|
+
/** Muted colour of the track behind / beyond the fill. Default a charcoal step. */
|
|
90
|
+
trackColor?: string
|
|
91
|
+
className?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Map a domain value to a clamped 0..1 fraction of the track width. */
|
|
95
|
+
function fraction(value: number, min: number, max: number): number {
|
|
96
|
+
if (max <= min) return 0
|
|
97
|
+
return Math.max(0, Math.min(1, (value - min) / (max - min)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pct(frac: number): DimensionValue {
|
|
101
|
+
return `${frac * 100}%`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Low-level gauge primitive: a horizontal pill track carrying an N-band zone gradient,
|
|
106
|
+
* optional tick marks with labels, and a single value marker (a needle line or a
|
|
107
|
+
* left-anchored fill). The zone bands are weighted by their domain span, so thresholds
|
|
108
|
+
* need not be evenly spaced. This is the shared visual base for the linear gauge family
|
|
109
|
+
* (FatigueMeter needle, TrainingLoadGauge zone bar, RpeCalibration band + marker) — pass
|
|
110
|
+
* the domain (`min`/`max`), the `zones`, and a `marker`; colours are literal hex from the
|
|
111
|
+
* ramp tokens (never `var()` refs, so they survive the RNW/vitest alias).
|
|
112
|
+
*/
|
|
113
|
+
export function ZoneTrack({
|
|
114
|
+
zones,
|
|
115
|
+
max,
|
|
116
|
+
min = 0,
|
|
117
|
+
marker = null,
|
|
118
|
+
ticks,
|
|
119
|
+
band = null,
|
|
120
|
+
trackHeight = DEFAULT_TRACK_HEIGHT,
|
|
121
|
+
needleOverhang = DEFAULT_NEEDLE_OVERHANG,
|
|
122
|
+
trackColor = DEFAULT_TRACK_COLOR,
|
|
123
|
+
className,
|
|
124
|
+
style,
|
|
125
|
+
accessibilityLabel,
|
|
126
|
+
...props
|
|
127
|
+
}: ZoneTrackProps) {
|
|
128
|
+
const isNeedle = marker?.type === 'needle'
|
|
129
|
+
const markerHeight = isNeedle ? trackHeight + needleOverhang * 2 : trackHeight
|
|
130
|
+
const markerFrac = marker != null ? fraction(marker.value, min, max) : 0
|
|
131
|
+
|
|
132
|
+
const bands = zones.map((zone, i) => {
|
|
133
|
+
const prev = i === 0 ? min : zones[i - 1].upTo
|
|
134
|
+
const start = Math.max(min, Math.min(max, prev))
|
|
135
|
+
const end = Math.max(min, Math.min(max, zone.upTo))
|
|
136
|
+
return { color: zone.color, weight: Math.max(0, end - start) }
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View
|
|
141
|
+
className={className}
|
|
142
|
+
style={[{ width: '100%' }, style]}
|
|
143
|
+
accessibilityRole="progressbar"
|
|
144
|
+
accessibilityValue={marker != null ? { min, max, now: marker.value } : { min, max, now: min }}
|
|
145
|
+
accessibilityLabel={accessibilityLabel ?? `Zone track: ${marker?.value ?? min}`}
|
|
146
|
+
testID="zone-track"
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
<View style={{ position: 'relative', height: markerHeight }}>
|
|
150
|
+
<View
|
|
151
|
+
testID="zone-track-track"
|
|
152
|
+
accessibilityElementsHidden
|
|
153
|
+
style={{
|
|
154
|
+
position: 'absolute',
|
|
155
|
+
top: (markerHeight - trackHeight) / 2,
|
|
156
|
+
left: 0,
|
|
157
|
+
right: 0,
|
|
158
|
+
height: trackHeight,
|
|
159
|
+
borderRadius: trackHeight / 2,
|
|
160
|
+
backgroundColor: trackColor,
|
|
161
|
+
overflow: 'hidden',
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
// The pill's own box-shadow renders outside its overflow:hidden, so
|
|
164
|
+
// `glow` haloes the whole track in the marker colour.
|
|
165
|
+
...(marker?.glow
|
|
166
|
+
? { boxShadow: glowShadow(marker.color ?? DEFAULT_MARKER_COLOR) }
|
|
167
|
+
: null),
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{bands.map((b, i) => (
|
|
171
|
+
<View
|
|
172
|
+
key={i}
|
|
173
|
+
testID="zone-track-band"
|
|
174
|
+
style={{
|
|
175
|
+
flexGrow: b.weight,
|
|
176
|
+
flexShrink: 1,
|
|
177
|
+
flexBasis: 0,
|
|
178
|
+
minWidth: 0,
|
|
179
|
+
height: '100%',
|
|
180
|
+
backgroundColor: b.color,
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
))}
|
|
184
|
+
|
|
185
|
+
{band != null && (
|
|
186
|
+
<View
|
|
187
|
+
testID="zone-track-band-highlight"
|
|
188
|
+
style={{
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
top: 0,
|
|
191
|
+
bottom: 0,
|
|
192
|
+
left: pct(fraction(band.from, min, max)),
|
|
193
|
+
width: pct(fraction(band.to, min, max) - fraction(band.from, min, max)),
|
|
194
|
+
backgroundColor: band.color,
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{marker?.type === 'fill' && (
|
|
200
|
+
<View
|
|
201
|
+
testID="zone-track-unfilled"
|
|
202
|
+
style={{
|
|
203
|
+
position: 'absolute',
|
|
204
|
+
top: 0,
|
|
205
|
+
bottom: 0,
|
|
206
|
+
left: pct(markerFrac),
|
|
207
|
+
right: 0,
|
|
208
|
+
backgroundColor: trackColor,
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
{marker?.type === 'fill' && marker.color != null && (
|
|
213
|
+
<View
|
|
214
|
+
testID="zone-track-fill"
|
|
215
|
+
style={{
|
|
216
|
+
position: 'absolute',
|
|
217
|
+
top: 0,
|
|
218
|
+
bottom: 0,
|
|
219
|
+
left: 0,
|
|
220
|
+
width: pct(markerFrac),
|
|
221
|
+
backgroundColor: marker.color,
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</View>
|
|
226
|
+
|
|
227
|
+
{isNeedle && (
|
|
228
|
+
<View
|
|
229
|
+
testID="zone-track-needle"
|
|
230
|
+
style={{
|
|
231
|
+
position: 'absolute',
|
|
232
|
+
top: 0,
|
|
233
|
+
left: pct(markerFrac),
|
|
234
|
+
width: NEEDLE_WIDTH,
|
|
235
|
+
height: markerHeight,
|
|
236
|
+
borderRadius: NEEDLE_WIDTH / 2,
|
|
237
|
+
backgroundColor: marker.color ?? DEFAULT_MARKER_COLOR,
|
|
238
|
+
transform: [{ translateX: -NEEDLE_WIDTH / 2 }],
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Colored tick lines over the track (decorative; the labels below carry the a11y). */}
|
|
244
|
+
{ticks?.map((tick, i) => {
|
|
245
|
+
const emphasized = tick.emphasized === true
|
|
246
|
+
const lineColor = tick.color ?? (emphasized ? t['brand-primary'] : TICK_COLOR)
|
|
247
|
+
const lineWidth = emphasized ? 2 : 1.5
|
|
248
|
+
return (
|
|
249
|
+
<View
|
|
250
|
+
key={`line-${i}`}
|
|
251
|
+
testID="zone-track-tick-line"
|
|
252
|
+
accessibilityElementsHidden
|
|
253
|
+
style={{
|
|
254
|
+
position: 'absolute',
|
|
255
|
+
top: (markerHeight - trackHeight) / 2 - 2,
|
|
256
|
+
left: pct(fraction(tick.value, min, max)),
|
|
257
|
+
width: lineWidth,
|
|
258
|
+
height: trackHeight + 4,
|
|
259
|
+
borderRadius: lineWidth / 2,
|
|
260
|
+
backgroundColor: lineColor,
|
|
261
|
+
transform: [{ translateX: -lineWidth / 2 }],
|
|
262
|
+
zIndex: 3,
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
)
|
|
266
|
+
})}
|
|
267
|
+
</View>
|
|
268
|
+
|
|
269
|
+
{ticks != null && ticks.length > 0 && (
|
|
270
|
+
<View style={{ position: 'relative', height: 16, marginTop: 5 }}>
|
|
271
|
+
{ticks.map((tick, i) => {
|
|
272
|
+
if (tick.label == null) return null
|
|
273
|
+
const emphasized = tick.emphasized === true
|
|
274
|
+
const labelColor = tick.color ?? (emphasized ? t['brand-primary'] : TICK_COLOR)
|
|
275
|
+
const labelNode = (
|
|
276
|
+
<Text
|
|
277
|
+
testID="zone-track-tick-label"
|
|
278
|
+
style={{
|
|
279
|
+
fontSize: 9,
|
|
280
|
+
letterSpacing: 0.5,
|
|
281
|
+
color: labelColor,
|
|
282
|
+
fontFamily: 'monospace',
|
|
283
|
+
fontWeight: emphasized ? '700' : '400',
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
{tick.label}
|
|
287
|
+
</Text>
|
|
288
|
+
)
|
|
289
|
+
return (
|
|
290
|
+
<View
|
|
291
|
+
key={`label-${i}`}
|
|
292
|
+
testID="zone-track-tick"
|
|
293
|
+
style={{
|
|
294
|
+
position: 'absolute',
|
|
295
|
+
left: pct(fraction(tick.value, min, max)),
|
|
296
|
+
transform: [{ translateX: -14 }],
|
|
297
|
+
width: 28,
|
|
298
|
+
alignItems: 'center',
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{tick.tooltip != null ? (
|
|
302
|
+
<Tooltip label={tick.tooltip} placement="top">
|
|
303
|
+
{labelNode}
|
|
304
|
+
</Tooltip>
|
|
305
|
+
) : (
|
|
306
|
+
labelNode
|
|
307
|
+
)}
|
|
308
|
+
</View>
|
|
309
|
+
)
|
|
310
|
+
})}
|
|
311
|
+
</View>
|
|
312
|
+
)}
|
|
313
|
+
</View>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
@@ -29,6 +29,7 @@ export { WorkoutPill, type WorkoutPillProps, type WorkoutPillStatus } from './Wo
|
|
|
29
29
|
export {
|
|
30
30
|
VelocityStrip,
|
|
31
31
|
type VelocityStripProps,
|
|
32
|
+
type VelocitySet,
|
|
32
33
|
type VelocityZoneBandProp,
|
|
33
34
|
getVelocityZoneColor,
|
|
34
35
|
getVelocityZoneName,
|
|
@@ -37,10 +38,46 @@ export {
|
|
|
37
38
|
} from './VelocityStrip'
|
|
38
39
|
export { MuscleGroupChip, type MuscleGroupChipProps, type VolumeStatus } from './MuscleGroupChip'
|
|
39
40
|
export { Sparkline, type SparklineProps } from './Sparkline'
|
|
40
|
-
export { SetRow, type SetRowProps, type
|
|
41
|
+
export { SetRow, type SetRowProps, type SetRowState, type SetRowUnit } from './SetRow'
|
|
41
42
|
export { InputBar, type InputBarProps } from './InputBar'
|
|
42
43
|
export { RestTimer, type RestTimerProps } from './RestTimer'
|
|
43
|
-
export {
|
|
44
|
+
export { MetricCell, type MetricCellProps, METRIC_FONT } from './metricText'
|
|
45
|
+
export { SetsRepsLoad, type SetsRepsLoadProps } from './SetsRepsLoad'
|
|
46
|
+
export { SegmentedBar, type SegmentedBarProps, type SegmentedBarSegment } from './SegmentedBar'
|
|
47
|
+
export { paceTone, paceToneColor, type PaceTone } from './paceTone'
|
|
48
|
+
export {
|
|
49
|
+
SegmentedProgressBar,
|
|
50
|
+
type SegmentedProgressBarProps,
|
|
51
|
+
type SegmentedProgressBarSegment,
|
|
52
|
+
} from './SegmentedProgressBar'
|
|
53
|
+
export { MetricTiles, type MetricTilesProps, type MetricTileData } from './MetricTiles'
|
|
54
|
+
export { ScheduleTiles, type ScheduleTilesProps } from './ScheduleTiles'
|
|
55
|
+
export { SetBar, type SetBarProps } from './SetBar'
|
|
56
|
+
export {
|
|
57
|
+
SetStrip,
|
|
58
|
+
type SetStripProps,
|
|
59
|
+
type SetStripSet,
|
|
60
|
+
SET_STRIP_ZONES,
|
|
61
|
+
SET_STRIP_VARIABLE_COLOR,
|
|
62
|
+
velocityZoneColor,
|
|
63
|
+
} from './SetStrip'
|
|
64
|
+
export {
|
|
65
|
+
ExerciseIndicator,
|
|
66
|
+
resolveIndicator,
|
|
67
|
+
INDICATOR_PRECEDENCE,
|
|
68
|
+
type ExerciseIndicatorProps,
|
|
69
|
+
type ExerciseIndicatorKind,
|
|
70
|
+
} from './ExerciseIndicator'
|
|
71
|
+
export { ExerciseHeading, type ExerciseHeadingProps } from './ExerciseHeading'
|
|
72
|
+
export { ExerciseCardHeading, type ExerciseCardHeadingProps } from './ExerciseCardHeading'
|
|
73
|
+
export { ExerciseCard, type ExerciseCardProps } from './ExerciseCard'
|
|
74
|
+
export { SetTableHeader, type SetTableHeaderProps } from './SetTableHeader'
|
|
75
|
+
export {
|
|
76
|
+
SessionHeader,
|
|
77
|
+
type SessionHeaderProps,
|
|
78
|
+
type SessionHeaderPlanEntry,
|
|
79
|
+
} from './SessionHeader'
|
|
80
|
+
export { SessionRail, type SessionRailProps, type SessionRailExercise } from './SessionRail'
|
|
44
81
|
export { SupersetWrapper, type SupersetWrapperProps } from './SupersetWrapper'
|
|
45
82
|
export {
|
|
46
83
|
MesoProgressBar,
|
|
@@ -152,3 +189,19 @@ export type {
|
|
|
152
189
|
WorkoutProgress,
|
|
153
190
|
WorkoutGroup,
|
|
154
191
|
} from './ActiveWorkoutPage'
|
|
192
|
+
export { ZoneTrack } from './ZoneTrack'
|
|
193
|
+
export type {
|
|
194
|
+
ZoneTrackProps,
|
|
195
|
+
ZoneTrackZone,
|
|
196
|
+
ZoneTrackTick,
|
|
197
|
+
ZoneTrackMarker,
|
|
198
|
+
ZoneTrackBand,
|
|
199
|
+
} from './ZoneTrack'
|
|
200
|
+
export { FatigueMeter } from './FatigueMeter'
|
|
201
|
+
export type { FatigueMeterProps } from './FatigueMeter'
|
|
202
|
+
export { VolumeLandmarkBar } from './VolumeLandmarkBar'
|
|
203
|
+
export type { VolumeLandmarkBarProps, VolumeZone } from './VolumeLandmarkBar'
|
|
204
|
+
export { StatusPill, statusPillColor } from './StatusPill'
|
|
205
|
+
export type { StatusPillProps, StatusPillStatus } from './StatusPill'
|
|
206
|
+
export { LiveAuraFrame, liveAuraColor } from './LiveAuraFrame'
|
|
207
|
+
export type { LiveAuraFrameProps, LiveAuraCategory } from './LiveAuraFrame'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { MetricCell } from './metricText'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `MetricCell` — the shared "segmented metric" typography primitive (Inter · 600 ·
|
|
7
|
+
* letter-spacing 1). Value cells take a primary color, separators a muted one. The
|
|
8
|
+
* single source of the value/separator look shared by SetsRepsLoad and TempoDisplay.
|
|
9
|
+
*/
|
|
10
|
+
const meta: Meta<typeof MetricCell> = {
|
|
11
|
+
title: 'Workout/MetricCell',
|
|
12
|
+
component: MetricCell,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'**Primitive.** The shared Inter·600·letter-spacing-1 value/separator cell. ' +
|
|
19
|
+
'Extracted so the two prescription readouts read as one visual language instead of ' +
|
|
20
|
+
'drifting copies. Used-by ↑ ' +
|
|
21
|
+
'[SetsRepsLoad](?path=/docs/workout-setsrepsload--docs) + ' +
|
|
22
|
+
'[TempoDisplay](?path=/docs/workout-tempodisplay--docs).',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
argTypes: {
|
|
27
|
+
color: { control: 'color' },
|
|
28
|
+
fontSize: { control: { type: 'range', min: 9, max: 24, step: 1 } },
|
|
29
|
+
},
|
|
30
|
+
decorators: [
|
|
31
|
+
(Story) => (
|
|
32
|
+
<View style={{ padding: 20, backgroundColor: '#131313' }}>
|
|
33
|
+
<Story />
|
|
34
|
+
</View>
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default meta
|
|
40
|
+
type Story = StoryObj<typeof MetricCell>
|
|
41
|
+
|
|
42
|
+
export const ValueCell: Story = {
|
|
43
|
+
args: { children: '145', color: '#F3F4F6', fontSize: 11 },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Separator: Story = {
|
|
47
|
+
args: { children: '×', color: '#6B7280', fontSize: 11 },
|
|
48
|
+
parameters: { docs: { description: { story: 'A muted separator cell (× / @ / -).' } } },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const InContext: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
|
54
|
+
<MetricCell color="#F3F4F6">5</MetricCell>
|
|
55
|
+
<MetricCell color="#6B7280"> × </MetricCell>
|
|
56
|
+
<MetricCell color="#F3F4F6">8</MetricCell>
|
|
57
|
+
<MetricCell color="#6B7280"> @ </MetricCell>
|
|
58
|
+
<MetricCell color="#F3F4F6">145</MetricCell>
|
|
59
|
+
<MetricCell color="#6B7280"> lb</MetricCell>
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
parameters: {
|
|
63
|
+
docs: {
|
|
64
|
+
description: {
|
|
65
|
+
story: 'Value + separator cells composed into the sets × reps @ load language.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { Text } from 'react-native'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared "segmented metric" typography primitive — the Inter · 600 · letter-spacing 1
|
|
7
|
+
* value/separator cell used by both {@link TempoDisplay} (tempo digits + dashes) and
|
|
8
|
+
* {@link SetsRepsLoad} (sets × reps @ load). Extracted so the two read as one visual
|
|
9
|
+
* language instead of drifting copies.
|
|
10
|
+
*/
|
|
11
|
+
export const METRIC_FONT = 'Inter, sans-serif'
|
|
12
|
+
|
|
13
|
+
export interface MetricCellProps {
|
|
14
|
+
children: ReactNode
|
|
15
|
+
/** Cell text color — value cells take a primary color, separators a muted one. */
|
|
16
|
+
color: string
|
|
17
|
+
fontSize?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MetricCell({ children, color, fontSize = 11 }: MetricCellProps) {
|
|
21
|
+
return (
|
|
22
|
+
<Text
|
|
23
|
+
style={{
|
|
24
|
+
fontFamily: METRIC_FONT,
|
|
25
|
+
fontSize,
|
|
26
|
+
color,
|
|
27
|
+
fontWeight: '600',
|
|
28
|
+
letterSpacing: 1,
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</Text>
|
|
33
|
+
)
|
|
34
|
+
}
|