@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,93 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import {
|
|
5
|
+
ExerciseIndicator,
|
|
6
|
+
resolveIndicator,
|
|
7
|
+
INDICATOR_PRECEDENCE,
|
|
8
|
+
type ExerciseIndicatorKind,
|
|
9
|
+
} from './ExerciseIndicator'
|
|
10
|
+
|
|
11
|
+
// tier color = titan status token, applied as the glyph's SVG stroke (literal hex):
|
|
12
|
+
// danger=status-error, warning=status-warning, success=status-success, info=status-info.
|
|
13
|
+
const kinds: { kind: ExerciseIndicatorKind; label: string; tier: string }[] = [
|
|
14
|
+
{ kind: 'imbalance', label: 'Left/right imbalance', tier: '#D14343' },
|
|
15
|
+
{ kind: 'overshoot', label: 'Load overshoot', tier: '#D14343' },
|
|
16
|
+
{ kind: 'velocity-loss', label: 'Velocity loss', tier: '#F9B415' },
|
|
17
|
+
{ kind: 'missed-reps', label: 'Missed reps', tier: '#F9B415' },
|
|
18
|
+
{ kind: 'pr', label: 'Personal record', tier: '#2ED573' },
|
|
19
|
+
{ kind: 'info', label: 'More info', tier: '#2196F3' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
describe('ExerciseIndicator', () => {
|
|
23
|
+
it.each(kinds)('renders the $kind chip glyph and accessible label', ({ kind, label }) => {
|
|
24
|
+
const { container } = render(<ExerciseIndicator kind={kind} />)
|
|
25
|
+
// glyph is an inline SVG (lucide-mirrored), not a text character
|
|
26
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
27
|
+
expect(screen.getByLabelText(label)).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it.each(kinds)('tints the $kind glyph with its tier color', ({ kind, tier }) => {
|
|
31
|
+
const { container } = render(<ExerciseIndicator kind={kind} />)
|
|
32
|
+
expect(container.querySelector('svg')!.getAttribute('stroke')).toBe(tier)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('renders a decorative (aria-hidden) glyph so the chip owns the label', () => {
|
|
36
|
+
const { container } = render(<ExerciseIndicator kind="pr" />)
|
|
37
|
+
expect(container.querySelector('svg')!.getAttribute('aria-hidden')).toBe('true')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('is presentational (no button role) without onPress', () => {
|
|
41
|
+
render(<ExerciseIndicator kind="pr" />)
|
|
42
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('becomes a button and fires onPress when interactive', () => {
|
|
46
|
+
const onPress = vi.fn()
|
|
47
|
+
render(<ExerciseIndicator kind="pr" onPress={onPress} />)
|
|
48
|
+
const button = screen.getByRole('button', { name: 'Personal record' })
|
|
49
|
+
fireEvent.click(button)
|
|
50
|
+
expect(onPress).toHaveBeenCalledOnce()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('accessibility', () => {
|
|
54
|
+
it.each(kinds)('has no accessibility violations for $kind', async ({ kind }) => {
|
|
55
|
+
const { container } = render(<ExerciseIndicator kind={kind} />)
|
|
56
|
+
const results = await axe(container)
|
|
57
|
+
expect(results).toHaveNoViolations()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('resolveIndicator', () => {
|
|
63
|
+
it('returns undefined when no signals are active', () => {
|
|
64
|
+
expect(resolveIndicator([])).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('returns the sole candidate unchanged', () => {
|
|
68
|
+
expect(resolveIndicator(['velocity-loss'])).toBe('velocity-loss')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('picks the highest-precedence kind regardless of input order', () => {
|
|
72
|
+
expect(resolveIndicator(['info', 'pr', 'overshoot'])).toBe('overshoot')
|
|
73
|
+
expect(resolveIndicator(['overshoot', 'pr', 'info'])).toBe('overshoot')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('lets imbalance (rank 1) win over every other signal', () => {
|
|
77
|
+
expect(resolveIndicator([...INDICATOR_PRECEDENCE])).toBe('imbalance')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('ranks the soft alerts above pr (alert > pr)', () => {
|
|
81
|
+
expect(resolveIndicator(['pr', 'missed-reps'])).toBe('missed-reps')
|
|
82
|
+
expect(resolveIndicator(['pr', 'velocity-loss'])).toBe('velocity-loss')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('falls to info only when it is the lowest active signal', () => {
|
|
86
|
+
expect(resolveIndicator(['info', 'pr'])).toBe('pr')
|
|
87
|
+
expect(resolveIndicator(['info'])).toBe('info')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('ignores duplicate candidates', () => {
|
|
91
|
+
expect(resolveIndicator(['pr', 'pr', 'info'])).toBe('pr')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import { View, Pressable, type ViewProps } from 'react-native'
|
|
4
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import {
|
|
6
|
+
ScaleIcon,
|
|
7
|
+
AlertTriangleIcon,
|
|
8
|
+
TrendingDownIcon,
|
|
9
|
+
CircleSlashIcon,
|
|
10
|
+
AwardIcon,
|
|
11
|
+
InfoIcon,
|
|
12
|
+
type IconProps,
|
|
13
|
+
} from '../../icons'
|
|
14
|
+
|
|
15
|
+
// Literal-hex status pins (the TempoBar pattern) — `resolveColor()` yields a
|
|
16
|
+
// `var(...)` under the RNW vitest alias, which breaks `toHaveStyle` in tests.
|
|
17
|
+
const t = getSemanticColors('dark')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The LOCKED 6-kind indicator taxonomy (TD-03.51). One precedence-ranked slot is
|
|
21
|
+
* ever shown per exercise, resolved by {@link resolveIndicator}. Glyph = the
|
|
22
|
+
* specific signal; color = the severity tier (danger/warning/success/info, bound
|
|
23
|
+
* to titan status tokens). Outlined (border + glyph, no fill) so a chip never
|
|
24
|
+
* collides with the filled velocity strip beside it.
|
|
25
|
+
*/
|
|
26
|
+
export type ExerciseIndicatorKind =
|
|
27
|
+
| 'imbalance'
|
|
28
|
+
| 'overshoot'
|
|
29
|
+
| 'velocity-loss'
|
|
30
|
+
| 'missed-reps'
|
|
31
|
+
| 'pr'
|
|
32
|
+
| 'info'
|
|
33
|
+
|
|
34
|
+
interface IndicatorConfig {
|
|
35
|
+
Icon: ComponentType<IconProps>
|
|
36
|
+
/** Tier color — a titan status semantic token (literal hex on `t`). */
|
|
37
|
+
color: string
|
|
38
|
+
/** Accessible description (also the intended tooltip subject). */
|
|
39
|
+
label: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const INDICATOR_CONFIG: Record<ExerciseIndicatorKind, IndicatorConfig> = {
|
|
43
|
+
imbalance: { Icon: ScaleIcon, color: t['status-error'], label: 'Left/right imbalance' },
|
|
44
|
+
overshoot: { Icon: AlertTriangleIcon, color: t['status-error'], label: 'Load overshoot' },
|
|
45
|
+
'velocity-loss': { Icon: TrendingDownIcon, color: t['status-warning'], label: 'Velocity loss' },
|
|
46
|
+
'missed-reps': { Icon: CircleSlashIcon, color: t['status-warning'], label: 'Missed reps' },
|
|
47
|
+
pr: { Icon: AwardIcon, color: t['status-success'], label: 'Personal record' },
|
|
48
|
+
info: { Icon: InfoIcon, color: t['status-info'], label: 'More info' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Precedence order (1=highest → last=lowest). A single chip is shown, so when
|
|
53
|
+
* several signals are true the highest-ranked wins. Alerts outrank `pr`; `info`
|
|
54
|
+
* is the floor. This ordering is LOCKED — do not reshuffle.
|
|
55
|
+
*/
|
|
56
|
+
export const INDICATOR_PRECEDENCE: readonly ExerciseIndicatorKind[] = [
|
|
57
|
+
'imbalance',
|
|
58
|
+
'overshoot',
|
|
59
|
+
'velocity-loss',
|
|
60
|
+
'missed-reps',
|
|
61
|
+
'pr',
|
|
62
|
+
'info',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Given the signals currently true for an exercise, pick the single
|
|
67
|
+
* highest-precedence kind to surface (or `undefined` when none apply). Order and
|
|
68
|
+
* duplicates in `candidates` don't matter — {@link INDICATOR_PRECEDENCE} decides.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveIndicator(
|
|
71
|
+
candidates: ExerciseIndicatorKind[],
|
|
72
|
+
): ExerciseIndicatorKind | undefined {
|
|
73
|
+
return INDICATOR_PRECEDENCE.find((kind) => candidates.includes(kind))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ExerciseIndicatorProps extends Omit<ViewProps, 'children'> {
|
|
77
|
+
kind: ExerciseIndicatorKind
|
|
78
|
+
/** Optional tap handler (surfaces detail — PR history / alert / info). */
|
|
79
|
+
onPress?: () => void
|
|
80
|
+
className?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A small circular outlined chip for an exercise heading. Presentational and
|
|
85
|
+
* static (chips are chrome — no pulse); the tap detail (tooltip/sheet) is wired
|
|
86
|
+
* by the consumer.
|
|
87
|
+
*/
|
|
88
|
+
export function ExerciseIndicator({ kind, onPress, className, ...props }: ExerciseIndicatorProps) {
|
|
89
|
+
const { Icon, color, label } = INDICATOR_CONFIG[kind]
|
|
90
|
+
|
|
91
|
+
const chip = (
|
|
92
|
+
<View
|
|
93
|
+
className={className}
|
|
94
|
+
style={{
|
|
95
|
+
width: 16,
|
|
96
|
+
height: 16,
|
|
97
|
+
borderRadius: 8,
|
|
98
|
+
borderWidth: 1,
|
|
99
|
+
borderColor: color,
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
}}
|
|
103
|
+
accessibilityRole="image"
|
|
104
|
+
accessibilityLabel={label}
|
|
105
|
+
testID="exercise-indicator"
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<Icon size={11} color={color} />
|
|
109
|
+
</View>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (onPress) {
|
|
113
|
+
return (
|
|
114
|
+
<Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel={label}>
|
|
115
|
+
{chip}
|
|
116
|
+
</Pressable>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return chip
|
|
120
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { FatigueMeter } from './FatigueMeter'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof FatigueMeter> = {
|
|
7
|
+
title: 'Workout/DataViz/FatigueMeter',
|
|
8
|
+
component: FatigueMeter,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
value: { control: { type: 'range', min: 0, max: 40, step: 1 } },
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'**Component.** The winning fatigue-visual lineage: a sliding needle over a fixed ' +
|
|
18
|
+
'green→gold→orange→red velocity-loss gradient with VL10/VL20/VL30/stop markers. ' +
|
|
19
|
+
'Composes the [ZoneTrack](?path=/docs/workout-dataviz-zonetrack--docs) primitive. ' +
|
|
20
|
+
'Prop-driven: `value` plus overridable `thresholds` / `max` / `zoneColors` / `labels`.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
(Story) => (
|
|
26
|
+
<View style={{ width: 360, padding: 20, backgroundColor: '#131313' }}>
|
|
27
|
+
<Story />
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
}
|
|
32
|
+
export default meta
|
|
33
|
+
|
|
34
|
+
type Story = StoryObj<typeof FatigueMeter>
|
|
35
|
+
|
|
36
|
+
/** Fresh — needle sits in the green zone. */
|
|
37
|
+
export const Fresh: Story = { args: { value: 5 } }
|
|
38
|
+
|
|
39
|
+
/** VL20 — needle crossing into the orange zone. */
|
|
40
|
+
export const Vl20: Story = { args: { value: 21 } }
|
|
41
|
+
|
|
42
|
+
/** Stop zone — deep into the red. */
|
|
43
|
+
export const StopZone: Story = { args: { value: 34 } }
|
|
44
|
+
|
|
45
|
+
/** Chunky wall-glanceable density via a taller track. */
|
|
46
|
+
export const WallDensity: Story = { args: { value: 21, trackHeight: 22 } }
|
|
47
|
+
|
|
48
|
+
/** Custom scale — a tighter 0..30 domain with earlier thresholds. */
|
|
49
|
+
export const CustomScale: Story = {
|
|
50
|
+
args: { value: 12, max: 30, thresholds: [8, 16, 24], labels: ['ok', 'w1', 'w2', 'w3', 'max'] },
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { FatigueMeter } from './FatigueMeter'
|
|
5
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
6
|
+
|
|
7
|
+
const { green, red } = WORKOUT_TOKENS.scale
|
|
8
|
+
|
|
9
|
+
describe('FatigueMeter', () => {
|
|
10
|
+
it('renders the four fatigue zone bands', () => {
|
|
11
|
+
render(<FatigueMeter value={0} />)
|
|
12
|
+
expect(screen.getAllByTestId('zone-track-band')).toHaveLength(4)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('colours the bands from the canonical effort scale', () => {
|
|
16
|
+
render(<FatigueMeter value={0} />)
|
|
17
|
+
const bands = screen.getAllByTestId('zone-track-band')
|
|
18
|
+
expect(bands[0]).toHaveStyle({ backgroundColor: green })
|
|
19
|
+
expect(bands[3]).toHaveStyle({ backgroundColor: red })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders the default VL scale labels', () => {
|
|
23
|
+
render(<FatigueMeter value={0} />)
|
|
24
|
+
for (const label of ['fresh', 'VL10', 'VL20', 'VL30', 'stop']) {
|
|
25
|
+
expect(screen.getByText(label)).toBeInTheDocument()
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('positions the needle at the value on the 0..40 loss scale', () => {
|
|
30
|
+
render(<FatigueMeter value={20} />)
|
|
31
|
+
expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '50%' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('clamps a value beyond the stop end to the track end', () => {
|
|
35
|
+
render(<FatigueMeter value={60} />)
|
|
36
|
+
expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '100%' })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reshapes the zones from custom thresholds + max', () => {
|
|
40
|
+
render(<FatigueMeter value={5} max={20} thresholds={[5, 10, 15]} />)
|
|
41
|
+
const bands = screen.getAllByTestId('zone-track-band')
|
|
42
|
+
expect(bands[0]).toHaveStyle({ flexGrow: 5 })
|
|
43
|
+
expect(bands[3]).toHaveStyle({ flexGrow: 5 })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('honours custom labels', () => {
|
|
47
|
+
render(<FatigueMeter value={5} labels={['A', 'B', 'C', 'D', 'E']} />)
|
|
48
|
+
expect(screen.getByText('A')).toBeInTheDocument()
|
|
49
|
+
expect(screen.getByText('E')).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('honours a needle colour override', () => {
|
|
53
|
+
render(<FatigueMeter value={10} needleColor="#FF00FF" />)
|
|
54
|
+
expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ backgroundColor: '#FF00FF' })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// RNW does not map accessibilityValue → aria-valuenow under jsdom (gotcha #11), so the
|
|
58
|
+
// loss value is verified via the needle position + the accessible name.
|
|
59
|
+
it('exposes the progressbar role with the loss value in its accessible name', () => {
|
|
60
|
+
render(<FatigueMeter value={17} />)
|
|
61
|
+
expect(
|
|
62
|
+
screen.getByRole('progressbar', { name: 'Fatigue: 17% velocity loss' })
|
|
63
|
+
).toBeInTheDocument()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('accessibility', () => {
|
|
67
|
+
it('has no accessibility violations', async () => {
|
|
68
|
+
const { container } = render(<FatigueMeter value={21} />)
|
|
69
|
+
const results = await axe(container)
|
|
70
|
+
expect(results).toHaveNoViolations()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import type { ViewProps } from 'react-native'
|
|
3
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
4
|
+
import { ZoneTrack, type ZoneTrackZone, type ZoneTrackTick } from './ZoneTrack'
|
|
5
|
+
|
|
6
|
+
const { green, yellow, orange, red } = WORKOUT_TOKENS.scale
|
|
7
|
+
|
|
8
|
+
/** Canonical fatigue band colours (fresh → stop) — the even effort-scale hue walk. */
|
|
9
|
+
const DEFAULT_ZONE_COLORS: [string, string, string, string] = [green, yellow, orange, red]
|
|
10
|
+
/** Zone boundaries in velocity-loss %, matching the VL10/VL20/VL30 coaching cues. */
|
|
11
|
+
const DEFAULT_THRESHOLDS: [number, number, number] = [10, 20, 30]
|
|
12
|
+
/** Scale labels at [min, ...thresholds, max]. */
|
|
13
|
+
const DEFAULT_LABELS = ['fresh', 'VL10', 'VL20', 'VL30', 'stop']
|
|
14
|
+
const DEFAULT_MAX = 40
|
|
15
|
+
|
|
16
|
+
export interface FatigueMeterProps extends ViewProps {
|
|
17
|
+
/** Current velocity-loss percentage (0 = fresh). Drives the needle position. */
|
|
18
|
+
value: number
|
|
19
|
+
/** Upper bound of the fatigue scale — the "stop" end, in loss %. Default 40. */
|
|
20
|
+
max?: number
|
|
21
|
+
/** Zone boundary thresholds (loss %): green→gold, gold→orange, orange→red. Default [10,20,30]. */
|
|
22
|
+
thresholds?: [number, number, number]
|
|
23
|
+
/** Zone band colours low→high (fresh→stop). Default the canonical effort scale. */
|
|
24
|
+
zoneColors?: [string, string, string, string]
|
|
25
|
+
/** Tick labels at [min, ...thresholds, max]. Default ['fresh','VL10','VL20','VL30','stop']. */
|
|
26
|
+
labels?: string[]
|
|
27
|
+
/** Track height in px. Default 14. */
|
|
28
|
+
trackHeight?: number
|
|
29
|
+
/** Needle colour. Default white. */
|
|
30
|
+
needleColor?: string
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A fatigue meter: a sliding needle over a fixed green→gold→orange→red velocity-loss
|
|
36
|
+
* gradient with VL10/VL20/VL30/stop scale markers. This is the winning fatigue-visual
|
|
37
|
+
* lineage (needle-over-zoned-gradient, three independent sources agreed) — it composes
|
|
38
|
+
* the shared {@link ZoneTrack} primitive, supplying the fatigue domain, zone bands from
|
|
39
|
+
* the effort-scale tokens, and the coaching-cue tick labels. Prop-driven: `value` plus
|
|
40
|
+
* overridable `thresholds` / `max` / `zoneColors` / `labels`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <FatigueMeter value={21} /> // needle in the VL20 (orange) zone
|
|
44
|
+
* <FatigueMeter value={8} max={40} thresholds={[10, 20, 30]} />
|
|
45
|
+
*/
|
|
46
|
+
export function FatigueMeter({
|
|
47
|
+
value,
|
|
48
|
+
max = DEFAULT_MAX,
|
|
49
|
+
thresholds = DEFAULT_THRESHOLDS,
|
|
50
|
+
zoneColors = DEFAULT_ZONE_COLORS,
|
|
51
|
+
labels = DEFAULT_LABELS,
|
|
52
|
+
trackHeight = 14,
|
|
53
|
+
needleColor,
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: FatigueMeterProps) {
|
|
57
|
+
const [t1, t2, t3] = thresholds
|
|
58
|
+
const zones: ZoneTrackZone[] = [
|
|
59
|
+
{ upTo: t1, color: zoneColors[0] },
|
|
60
|
+
{ upTo: t2, color: zoneColors[1] },
|
|
61
|
+
{ upTo: t3, color: zoneColors[2] },
|
|
62
|
+
{ upTo: max, color: zoneColors[3] },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const tickValues = [0, t1, t2, t3, max]
|
|
66
|
+
const ticks: ZoneTrackTick[] = tickValues.map((v, i) => ({ value: v, label: labels[i] }))
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ZoneTrack
|
|
70
|
+
className={className}
|
|
71
|
+
zones={zones}
|
|
72
|
+
min={0}
|
|
73
|
+
max={max}
|
|
74
|
+
trackHeight={trackHeight}
|
|
75
|
+
ticks={ticks}
|
|
76
|
+
marker={{ type: 'needle', value, color: needleColor }}
|
|
77
|
+
accessibilityLabel={`Fatigue: ${value}% velocity loss`}
|
|
78
|
+
testID="fatigue-meter"
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { View, Text, Pressable, TextInput } from 'react-native'
|
|
4
4
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
5
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
6
|
|
|
6
7
|
export interface InputBarProps {
|
|
7
8
|
exerciseName: string
|
|
@@ -17,7 +18,7 @@ export interface InputBarProps {
|
|
|
17
18
|
visible: boolean
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const BRAND_PRIMARY = '
|
|
21
|
+
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
21
22
|
const INPUT_CLASSNAME = 'bg-surface-raised border-border-strong text-text-primary'
|
|
22
23
|
|
|
23
24
|
const inputStyle = {
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native'
|
|
|
3
3
|
import { IntensityBar } from './IntensityBar'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof IntensityBar> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/DataViz/IntensityBar',
|
|
7
7
|
component: IntensityBar,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -44,19 +44,19 @@ describe('IntensityBar', () => {
|
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
describe('zone colors graded by true percentage', () => {
|
|
47
|
-
it('renders
|
|
47
|
+
it('renders green below the target ramp (< 75%)', () => {
|
|
48
48
|
render(<IntensityBar level={0.2} />)
|
|
49
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#
|
|
49
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
it('stays
|
|
52
|
+
it('stays green just under the amber boundary', () => {
|
|
53
53
|
render(<IntensityBar level={0.74} />)
|
|
54
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#
|
|
54
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
it('renders amber approaching the target (75-99%)', () => {
|
|
58
58
|
render(<IntensityBar level={0.75} />)
|
|
59
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#
|
|
59
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#F9B415' })
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('renders the brand target color exactly at 100%', () => {
|
|
@@ -71,12 +71,12 @@ describe('IntensityBar', () => {
|
|
|
71
71
|
|
|
72
72
|
it('renders over-2 deep red at 120%', () => {
|
|
73
73
|
render(<IntensityBar level={1.2} />)
|
|
74
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#
|
|
74
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A4221C' })
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
it('renders over-3 darkest red at 130%', () => {
|
|
78
78
|
render(<IntensityBar level={1.3} />)
|
|
79
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#
|
|
79
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7E1002' })
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
82
|
|
|
@@ -88,7 +88,7 @@ describe('IntensityBar', () => {
|
|
|
88
88
|
|
|
89
89
|
it('colors the bulge to match the over-target zone', () => {
|
|
90
90
|
render(<IntensityBar level={1.2} />)
|
|
91
|
-
expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#
|
|
91
|
+
expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A4221C' })
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it('does not render a bulge at or below target', () => {
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
type ViewStyle,
|
|
9
9
|
type DimensionValue,
|
|
10
10
|
} from 'react-native'
|
|
11
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
12
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
13
|
+
|
|
14
|
+
const t = getSemanticColors('dark')
|
|
11
15
|
|
|
12
16
|
export type IntensityBarOrientation = 'vertical' | 'horizontal'
|
|
13
17
|
|
|
@@ -34,12 +38,12 @@ const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(
|
|
|
34
38
|
|
|
35
39
|
// Zone colors graded by TRUE percentage (level * 100).
|
|
36
40
|
const ZONE = {
|
|
37
|
-
building: '
|
|
38
|
-
approaching: '
|
|
39
|
-
target: '
|
|
40
|
-
over1: '
|
|
41
|
-
over2:
|
|
42
|
-
over3:
|
|
41
|
+
building: t['status-success'], // below target ramp-up
|
|
42
|
+
approaching: t['status-warning'], // nearing target
|
|
43
|
+
target: t['brand-primary'], // exactly at target
|
|
44
|
+
over1: t['status-error'], // mild over-target
|
|
45
|
+
over2: WORKOUT_TOKENS.intensity.over2, // moderate over-target
|
|
46
|
+
over3: WORKOUT_TOKENS.intensity.over3, // severe over-target
|
|
43
47
|
} as const
|
|
44
48
|
|
|
45
49
|
type OverTier = 0 | 1 | 2 | 3
|