@titan-design/react-ui 0.5.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 +241 -189
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +241 -189
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +1017 -130
- package/dist/index.d.ts +1017 -130
- package/dist/index.js +3058 -1090
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2992 -1091
- 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 +1797 -1292
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1797 -1292
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +464 -268
- package/dist/theme/index.d.ts +464 -268
- package/dist/theme/index.js +303 -191
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +295 -192
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +160 -143
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +160 -143
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +3 -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 +83 -7
- package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
- package/src/components/custom/DateTime/DateTime.tsx +55 -15
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
- package/src/components/custom/Gauge/Gauge.tsx +6 -3
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.tsx +3 -5
- 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/Treemap/Treemap.tsx +3 -5
- package/src/components/custom/Typography/Typography.stories.tsx +46 -28
- package/src/components/custom/Typography/Typography.test.tsx +13 -3
- package/src/components/custom/Typography/Typography.tsx +5 -0
- 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 +11 -16
- 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/BodyMap.tsx +6 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
- package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.tsx +7 -4
- 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 +6 -6
- 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/InputBar.tsx +2 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
- package/src/components/custom/Workout/IntensityBar.tsx +10 -6
- 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/MesoCard.tsx +8 -3
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
- 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/MuscleGroupChip.tsx +7 -4
- 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/PrBadge.tsx +5 -2
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +10 -7
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +17 -11
- 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 +170 -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/Sparkline.tsx +2 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
- package/src/components/custom/Workout/StatusDot.tsx +12 -9
- 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/StrengthTrendChart.tsx +9 -6
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.tsx +2 -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 +29 -36
- 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 +379 -144
- 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/WeekRow.tsx +3 -2
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.tsx +6 -2
- 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/WorkoutCard.tsx +5 -2
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
- 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/icons.tsx +6 -67
- 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/SvgIcon.tsx +50 -0
- package/src/components/icons/icons.stories.tsx +122 -0
- package/src/components/icons/icons.test.tsx +73 -0
- package/src/components/icons/icons.tsx +153 -0
- package/src/components/icons/index.ts +4 -0
- package/src/components/index.ts +6 -0
- package/src/components/shell/BrandLockup.stories.tsx +27 -0
- package/src/components/shell/BrandLockup.test.tsx +23 -0
- package/src/components/shell/BrandLockup.tsx +47 -0
- 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 +28 -0
- package/src/components/shell/DeviceIndicator.test.tsx +26 -0
- package/src/components/shell/DeviceIndicator.tsx +52 -0
- package/src/components/shell/DeviceMenu.stories.tsx +47 -0
- package/src/components/shell/DeviceMenu.test.tsx +40 -0
- package/src/components/shell/DeviceMenu.tsx +65 -0
- package/src/components/shell/DeviceRow.stories.tsx +38 -0
- package/src/components/shell/DeviceRow.test.tsx +37 -0
- package/src/components/shell/DeviceRow.tsx +58 -0
- 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 +116 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +27 -0
- package/src/components/shell/SessionStatePill.test.tsx +25 -0
- package/src/components/shell/SessionStatePill.tsx +46 -0
- 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 +49 -0
- package/src/components/shell/TopBar.test.tsx +36 -0
- package/src/components/shell/TopBar.tsx +89 -0
- package/src/components/shell/index.ts +13 -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 +38 -2
- package/src/components/ui/indicator/Indicator.test.tsx +20 -1
- package/src/components/ui/indicator/Indicator.tsx +50 -6
- package/src/components/ui/indicator/index.ts +1 -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/spinner/Spinner.tsx +3 -2
- 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/toolbar-button/ToolbarButton.tsx +2 -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 +323 -0
- package/src/theme/Colors.stories.tsx +74 -9
- package/src/theme/barrel.ts +1 -0
- package/src/theme/config.ts +20 -3
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/extracted-colors-dataviz.ts +21 -0
- package/src/theme/extracted-colors-ui.ts +20 -0
- package/src/theme/global.css +69 -53
- package/src/theme/gradients.test.ts +25 -0
- package/src/theme/gradients.ts +36 -0
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.test.ts +232 -0
- package/src/theme/tokens/primitives.ts +238 -87
- package/src/theme/tokens/semantic.ts +101 -81
- package/src/theme/workout-tokens.ts +26 -21
- package/src/utils/avatar-color.ts +3 -3
- package/tailwind.config.js +15 -3
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View } from 'react-native'
|
|
4
|
+
import { ZoneTrack, type ZoneTrackZone } from './ZoneTrack'
|
|
5
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
6
|
+
|
|
7
|
+
const { green, yellow, orange, red } = WORKOUT_TOKENS.scale
|
|
8
|
+
|
|
9
|
+
const FATIGUE_ZONES: ZoneTrackZone[] = [
|
|
10
|
+
{ upTo: 10, color: green },
|
|
11
|
+
{ upTo: 20, color: yellow },
|
|
12
|
+
{ upTo: 30, color: orange },
|
|
13
|
+
{ upTo: 40, color: red },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const FATIGUE_TICKS = [
|
|
17
|
+
{ value: 0, label: 'fresh' },
|
|
18
|
+
{ value: 10, label: 'VL10' },
|
|
19
|
+
{ value: 20, label: 'VL20' },
|
|
20
|
+
{ value: 30, label: 'VL30' },
|
|
21
|
+
{ value: 40, label: 'stop' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const meta: Meta<typeof ZoneTrack> = {
|
|
25
|
+
title: 'Workout/DataViz/ZoneTrack',
|
|
26
|
+
component: ZoneTrack,
|
|
27
|
+
tags: ['autodocs'],
|
|
28
|
+
parameters: {
|
|
29
|
+
docs: {
|
|
30
|
+
description: {
|
|
31
|
+
component:
|
|
32
|
+
'**Primitive.** The shared linear-gauge base: a pill track with an N-band zone ' +
|
|
33
|
+
'gradient, optional tick marks + labels, and a single value marker — a `needle` line ' +
|
|
34
|
+
'or a left-anchored `fill` (clip-reveal gradient, or solid trend colour). Used-by ↓ ' +
|
|
35
|
+
'[FatigueMeter](?path=/docs/workout-dataviz-fatiguemeter--docs); the target base for ' +
|
|
36
|
+
'TrainingLoadGauge (ACWR) + RpeCalibration (band + marker). Zone colours are literal ' +
|
|
37
|
+
'ramp-token hex.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
decorators: [
|
|
42
|
+
(Story) => (
|
|
43
|
+
<View style={{ width: 360, padding: 20, backgroundColor: '#131313' }}>
|
|
44
|
+
<Story />
|
|
45
|
+
</View>
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
export default meta
|
|
50
|
+
|
|
51
|
+
type Story = StoryObj<typeof ZoneTrack>
|
|
52
|
+
|
|
53
|
+
/** Needle over the fatigue gradient with the VL scale — the FatigueMeter shape. */
|
|
54
|
+
export const Needle: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
zones: FATIGUE_ZONES,
|
|
57
|
+
max: 40,
|
|
58
|
+
marker: { type: 'needle', value: 21 },
|
|
59
|
+
ticks: FATIGUE_TICKS,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fill marker without a colour clip-reveals the gradient up to the value. */
|
|
64
|
+
export const GradientFill: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
zones: FATIGUE_ZONES,
|
|
67
|
+
max: 40,
|
|
68
|
+
marker: { type: 'fill', value: 24 },
|
|
69
|
+
ticks: FATIGUE_TICKS,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fill marker with a colour paints a solid trend-coloured bar (FatigueGauge lineage). */
|
|
74
|
+
export const SolidTrendFill: Story = {
|
|
75
|
+
args: { zones: FATIGUE_ZONES, max: 40, marker: { type: 'fill', value: 24, color: orange } },
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A range highlight + needle — the RpeCalibration "predicted CI band vs actual" idiom. */
|
|
79
|
+
export const BandWithMarker: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
zones: [
|
|
82
|
+
{ upTo: 7, color: green },
|
|
83
|
+
{ upTo: 8.5, color: yellow },
|
|
84
|
+
{ upTo: 10, color: red },
|
|
85
|
+
],
|
|
86
|
+
min: 6,
|
|
87
|
+
max: 10,
|
|
88
|
+
band: { from: 7.2, to: 8.4, color: 'rgba(255,255,255,0.22)' },
|
|
89
|
+
marker: { type: 'needle', value: 8.1 },
|
|
90
|
+
ticks: [
|
|
91
|
+
{ value: 6, label: '6' },
|
|
92
|
+
{ value: 7, label: '7' },
|
|
93
|
+
{ value: 8, label: '8' },
|
|
94
|
+
{ value: 9, label: '9' },
|
|
95
|
+
{ value: 10, label: '10' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Bare gradient track — no marker, no ticks. */
|
|
101
|
+
export const BareTrack: Story = {
|
|
102
|
+
args: { zones: FATIGUE_ZONES, max: 40 },
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Chunky wall-glanceable density via a taller track. */
|
|
106
|
+
export const WallDensity: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
zones: FATIGUE_ZONES,
|
|
109
|
+
max: 40,
|
|
110
|
+
marker: { type: 'needle', value: 15 },
|
|
111
|
+
ticks: FATIGUE_TICKS,
|
|
112
|
+
trackHeight: 22,
|
|
113
|
+
needleOverhang: 9,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `marker.glow` haloes the whole track in the marker colour — a "sweet spot" / attention cue. */
|
|
118
|
+
export const Glow: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
zones: FATIGUE_ZONES,
|
|
121
|
+
max: 40,
|
|
122
|
+
marker: { type: 'fill', value: 20, color: green, glow: true },
|
|
123
|
+
trackHeight: 12,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Colored + emphasized ticks with tooltips — hover a label to expand the acronym / show the raw value. */
|
|
128
|
+
export const ColoredTooltipTicks: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
zones: FATIGUE_ZONES,
|
|
131
|
+
max: 40,
|
|
132
|
+
marker: { type: 'needle', value: 22 },
|
|
133
|
+
trackHeight: 12,
|
|
134
|
+
ticks: [
|
|
135
|
+
{ value: 10, label: 'VL10', tooltip: '10% velocity loss' },
|
|
136
|
+
{ value: 20, label: 'VL20', emphasized: true, tooltip: '20% velocity loss — threshold' },
|
|
137
|
+
{ value: 30, label: 'STOP', color: red, tooltip: '30% velocity loss — stop the set' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
}
|
|
@@ -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
|
+
}
|