@titan-design/react-ui 0.6.0 → 0.8.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 +956 -128
- package/dist/index.d.ts +956 -128
- package/dist/index.js +2873 -981
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2826 -982
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1659 -799
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1660 -800
- 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/CircularTimer/CircularTimer.stories.tsx +90 -0
- package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
- package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
- package/src/components/custom/CircularTimer/index.ts +1 -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 +85 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +86 -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 +193 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
- package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
- package/src/components/custom/Workout/RestTimer.tsx +155 -59
- 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 +70 -1
- package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
- package/src/components/custom/Workout/TempoBar.tsx +107 -22
- 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 +262 -56
- package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
- 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 +139 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +373 -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 +2 -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 +86 -3
- package/src/components/ui/alert/Alert.test.tsx +52 -0
- package/src/components/ui/alert/Alert.tsx +54 -20
- 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/progress/Progress.tsx +47 -22
- 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
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Pressable,
|
|
7
|
+
Animated,
|
|
8
|
+
Easing,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native'
|
|
4
12
|
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
13
|
+
import { primitiveColors, primitiveRamps, sequentialEffort } from '../../../theme/tokens/primitives'
|
|
5
14
|
import { formatVelocity } from '../../../utils/workout-format'
|
|
15
|
+
import { SET_STRIP_VARIABLE_COLOR } from './SetStrip'
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Structural velocity-zone band accepted from an upstream analytics source
|
|
@@ -32,8 +42,20 @@ export interface VelocityStripProps extends ViewProps {
|
|
|
32
42
|
* Drives both bar height and — in the default path — zone color. Callers must
|
|
33
43
|
* feed mean (not peak) concentric velocity so bar color, height, and the
|
|
34
44
|
* summary label all describe one metric.
|
|
45
|
+
*
|
|
46
|
+
* Optional: provide exactly one of `velocities` or {@link VelocityStripProps.set}.
|
|
47
|
+
* When neither is given the strip renders nothing.
|
|
35
48
|
*/
|
|
36
|
-
velocities
|
|
49
|
+
velocities?: number[]
|
|
50
|
+
/**
|
|
51
|
+
* Optional structured strength-training set descriptor (see {@link VelocitySet}).
|
|
52
|
+
* When present it supersedes `velocities`: the done-velocity array is derived
|
|
53
|
+
* from the set (so mean / loss / zone summaries still work) and the strip is
|
|
54
|
+
* drawn as a typed slot list — grey todo, cyan variable / continue windows and
|
|
55
|
+
* the wide-gap chunking that distinguishes drop / myo / cluster sets. Provide
|
|
56
|
+
* exactly one of `velocities` or `set`.
|
|
57
|
+
*/
|
|
58
|
+
set?: VelocitySet
|
|
37
59
|
/**
|
|
38
60
|
* Optional velocity-zone bands (shape-compatible with WA's
|
|
39
61
|
* `VelocityZones.bands`). When provided, bar color and the summary zone label
|
|
@@ -46,13 +68,45 @@ export interface VelocityStripProps extends ViewProps {
|
|
|
46
68
|
* Live mode: index of the most-recently-completed rep. That bar animates in
|
|
47
69
|
* with a "pop"; if it is also the current set peak (a new best), it "bounces"
|
|
48
70
|
* instead. Only the latest bar animates. Honors `prefers-reduced-motion`.
|
|
49
|
-
*
|
|
71
|
+
* Framed `expanded` chart only (interactive tap-to-expand use).
|
|
50
72
|
*/
|
|
51
73
|
liveRepIndex?: number
|
|
74
|
+
/**
|
|
75
|
+
* `expanded` variant: whether the chart is OPEN. Default true (the variant shows
|
|
76
|
+
* its chart). Toggle it (with {@link onToggle}) for the interactive tap-to-expand
|
|
77
|
+
* collapse↔chart animation; a static open chart (no `onToggle`) does not animate.
|
|
78
|
+
*/
|
|
52
79
|
expanded?: boolean
|
|
53
80
|
onToggle?: () => void
|
|
54
81
|
onRepPress?: (index: number, velocity: number) => void
|
|
55
|
-
|
|
82
|
+
/**
|
|
83
|
+
* `mini` — a flat 3px static strip (set-type aware). `expanded` — the velocity-
|
|
84
|
+
* HEIGHT bar chart (rounded tops), whose chrome is prop-driven: with
|
|
85
|
+
* {@link showNumbers} or {@link showInfo} on it's the framed chart (raised surface,
|
|
86
|
+
* padding, per-bar m/s labels, mean/loss info row, interactive collapse); with both
|
|
87
|
+
* off it's a bare strip — the active-set "spotlight" of {@link ExerciseCard}. `hero`
|
|
88
|
+
* — the across-the-room, single-set wall treatment: tall bars, a per-bar m/s value
|
|
89
|
+
* label, a dashed running-best reference line, and dashed placeholders for the reps
|
|
90
|
+
* still to come (see {@link targetReps}). Reuses the zone scale and the live-rep pop.
|
|
91
|
+
*/
|
|
92
|
+
variant?: 'mini' | 'expanded' | 'hero'
|
|
93
|
+
/** `expanded` / `hero` plot height in px (bars scale to this). Default 60 (`expanded`) / 220 (`hero`). */
|
|
94
|
+
height?: number
|
|
95
|
+
/**
|
|
96
|
+
* `hero` only: the set's planned rep count. Reps beyond {@link velocities} draw as
|
|
97
|
+
* dashed placeholder stubs so the wall reads "3 of 8 done" at a glance. Ignored
|
|
98
|
+
* (and no placeholders drawn) when absent or ≤ the performed-rep count.
|
|
99
|
+
*/
|
|
100
|
+
targetReps?: number
|
|
101
|
+
/**
|
|
102
|
+
* `expanded` / `hero` bar scaling. `peak` (default) scales to the set's own max
|
|
103
|
+
* (+15% headroom). `fixed` scales to a fixed velocity ceiling so bar heights read
|
|
104
|
+
* the same absolute velocity across sets (the spotlight).
|
|
105
|
+
*/
|
|
106
|
+
scale?: 'peak' | 'fixed'
|
|
107
|
+
/** `expanded` framed chart: per-bar m/s labels. Default true. */
|
|
108
|
+
showNumbers?: boolean
|
|
109
|
+
/** `expanded` framed chart: the mean/loss info row. Default true. */
|
|
56
110
|
showInfo?: boolean
|
|
57
111
|
className?: string
|
|
58
112
|
}
|
|
@@ -62,26 +116,25 @@ export interface VelocityStripProps extends ViewProps {
|
|
|
62
116
|
const VEL_COLORS = WORKOUT_TOKENS.scale
|
|
63
117
|
|
|
64
118
|
/**
|
|
65
|
-
* Map a velocity-zone id (WA's 5-band taxonomy) onto the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* stays red.
|
|
119
|
+
* Map a velocity-zone id (WA's 5-band taxonomy) directly onto the 6-stop
|
|
120
|
+
* `sequentialEffort` ramp. The default strip uses the 4-color {@link VEL_COLORS}
|
|
121
|
+
* scale (a subsample of the same ramp), but the 5-band taxonomy has one more
|
|
122
|
+
* level than that scale carries, so it samples 5 stops: the top four align with
|
|
123
|
+
* the default scale (speed/power/strengthSpeed/maximalStrength = green/gold/
|
|
124
|
+
* orange/red) and `grinding` takes the ramp's darker red. `maximalStrength` and
|
|
125
|
+
* `grinding` therefore stay DISTINCT — they no longer collapse onto one red for
|
|
126
|
+
* lack of a 5th hue. An exercise moving from default to profile-derived zones
|
|
127
|
+
* never shifts its fast-end colors.
|
|
75
128
|
*
|
|
76
129
|
* Unknown ids fall back to `green` (matching the historical default-path
|
|
77
130
|
* fallback), so a forward-compatible band id never renders an empty bar.
|
|
78
131
|
*/
|
|
79
|
-
const
|
|
80
|
-
speed:
|
|
81
|
-
power:
|
|
82
|
-
strengthSpeed:
|
|
83
|
-
maximalStrength:
|
|
84
|
-
grinding:
|
|
132
|
+
const bandIdToEffortColor: Record<string, string> = {
|
|
133
|
+
speed: sequentialEffort[0], // green-300
|
|
134
|
+
power: sequentialEffort[2], // amber-300 (gold)
|
|
135
|
+
strengthSpeed: sequentialEffort[3], // orange-400
|
|
136
|
+
maximalStrength: sequentialEffort[4], // red-600
|
|
137
|
+
grinding: sequentialEffort[5], // red-700 — the distinct 5th band
|
|
85
138
|
}
|
|
86
139
|
|
|
87
140
|
// --- Default (no-zones) scale ------------------------------------------------
|
|
@@ -131,7 +184,7 @@ export function calculateMeanVelocity(velocities: number[]): number {
|
|
|
131
184
|
/** Classify a velocity into its band (slow → fast, min inclusive / max exclusive). */
|
|
132
185
|
function classifyBand(
|
|
133
186
|
velocity: number,
|
|
134
|
-
bands: readonly VelocityZoneBandProp[]
|
|
187
|
+
bands: readonly VelocityZoneBandProp[]
|
|
135
188
|
): VelocityZoneBandProp | undefined {
|
|
136
189
|
for (const band of bands) {
|
|
137
190
|
if (band.max === null || velocity < band.max) return band
|
|
@@ -140,8 +193,7 @@ function classifyBand(
|
|
|
140
193
|
}
|
|
141
194
|
|
|
142
195
|
function bandColor(band: VelocityZoneBandProp): string {
|
|
143
|
-
|
|
144
|
-
return token ? VEL_COLORS[token] : VEL_COLORS.green
|
|
196
|
+
return bandIdToEffortColor[band.id] ?? VEL_COLORS.green
|
|
145
197
|
}
|
|
146
198
|
|
|
147
199
|
function getLossStyle(loss: number): Record<string, string> | null {
|
|
@@ -150,6 +202,219 @@ function getLossStyle(loss: number): Record<string, string> | null {
|
|
|
150
202
|
return null
|
|
151
203
|
}
|
|
152
204
|
|
|
205
|
+
// --- Set-type slot model -----------------------------------------------------
|
|
206
|
+
// The strength-training set vocabulary (operator-approved in the Set Modalities
|
|
207
|
+
// exploration): one strip encoding for every per-rep set type. A set resolves to
|
|
208
|
+
// an ordered list of slots; the slot `kind` picks the color and `leadingGap`
|
|
209
|
+
// encodes the chunk pattern (rep gap vs the wide notch that splits drop sub-loads
|
|
210
|
+
// / myo clusters / cluster intra-rests). Set-LEVEL types (pyramid / superset /
|
|
211
|
+
// tempo) belong to the heading, not the strip.
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A structured strength-training set. Supply as {@link VelocityStripProps.set} to
|
|
215
|
+
* render its set-type encoding; the done-velocity array (used for the mean / loss
|
|
216
|
+
* / zone summary) is derived from the type's velocity fields.
|
|
217
|
+
*/
|
|
218
|
+
export type VelocitySet =
|
|
219
|
+
/** Fixed reps at a fixed load: done reps colored + a grey todo remainder to `planned`. */
|
|
220
|
+
| { type: 'straight'; velocities: number[]; planned?: number }
|
|
221
|
+
/** Bounded reps: committed slots `0..floor`, then a cyan variable window `floor..max`. */
|
|
222
|
+
| { type: 'range'; velocities: number[]; floor: number; max: number }
|
|
223
|
+
/** As-many-reps-as-possible: done reps + a trailing cyan "continue" slot that never closes. */
|
|
224
|
+
| { type: 'amrap'; velocities: number[]; target?: number }
|
|
225
|
+
/** Load drops with no rest: each sub-load's reps, split by a WIDE notch gap. */
|
|
226
|
+
| { type: 'drop'; subloads: number[][] }
|
|
227
|
+
/** Rest-pause: an activation chunk + mini-clusters split by WIDE gaps; `open` adds a cyan continue. */
|
|
228
|
+
| { type: 'myo'; activation: number[]; clusters: number[][]; open?: boolean }
|
|
229
|
+
/** Fixed count broken by intra-set rests: reps grouped by `groupSize` with WIDE intra-rest gaps. */
|
|
230
|
+
| { type: 'cluster'; velocities: number[]; groupSize: number; planned?: number }
|
|
231
|
+
|
|
232
|
+
/** One drawn cell of the strip. `kind` picks the color; `leadingGap` the spacing before it. */
|
|
233
|
+
interface VelocitySlot {
|
|
234
|
+
kind: 'rep' | 'todo' | 'variable' | 'continue'
|
|
235
|
+
velocity?: number
|
|
236
|
+
leadingGap: number
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Butted reps carry this gap; chunk boundaries carry {@link WIDE_GAP}. */
|
|
240
|
+
const REP_GAP = 2
|
|
241
|
+
/**
|
|
242
|
+
* The between-group notch (drop sub-loads / myo clusters / cluster intra-rests): a
|
|
243
|
+
* fixed 8px total gap that reads as a group break without dominating a narrow strip.
|
|
244
|
+
* 4× the rep gap — enough to separate groups at session-rail scale AND wall scale
|
|
245
|
+
* without the notch ballooning at large widths. In the mini variant the container's
|
|
246
|
+
* 2px gap covers part of it, so per-slot margin adds the remaining 6px (2 + 6 = 8).
|
|
247
|
+
*/
|
|
248
|
+
const WIDE_GAP = 8
|
|
249
|
+
|
|
250
|
+
/** Grey fill for planned-but-unperformed reps (charcoal placeholder, literal hex). */
|
|
251
|
+
const TODO_COLOR = primitiveColors.charcoal[300]
|
|
252
|
+
/** The thin cyan-800 outline on the open-ended "continue" slot — reads as "keep going". */
|
|
253
|
+
const CONTINUE_OUTLINE = primitiveRamps.cyan[800]
|
|
254
|
+
|
|
255
|
+
/** The done-velocity array a set contributes to the mean / loss / zone summary. */
|
|
256
|
+
function deriveDoneVelocities(set: VelocitySet): number[] {
|
|
257
|
+
switch (set.type) {
|
|
258
|
+
case 'drop':
|
|
259
|
+
return set.subloads.flat()
|
|
260
|
+
case 'myo':
|
|
261
|
+
return [...set.activation, ...set.clusters.flat()]
|
|
262
|
+
default:
|
|
263
|
+
return set.velocities
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Flatten velocity chunks into rep slots, carving a WIDE gap before each chunk but the first. */
|
|
268
|
+
function chunkedRepSlots(chunks: number[][]): VelocitySlot[] {
|
|
269
|
+
const slots: VelocitySlot[] = []
|
|
270
|
+
chunks.forEach((chunk) => {
|
|
271
|
+
chunk.forEach((velocity, ri) => {
|
|
272
|
+
const first = slots.length === 0
|
|
273
|
+
slots.push({ kind: 'rep', velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP })
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
return slots
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Resolve a set to its ordered slot list — the exact vocabulary of the Set Modalities sheet. */
|
|
280
|
+
function buildSlots(set: VelocitySet): VelocitySlot[] {
|
|
281
|
+
switch (set.type) {
|
|
282
|
+
case 'straight': {
|
|
283
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length)
|
|
284
|
+
return Array.from({ length: total }, (_, i) => {
|
|
285
|
+
const done = i < set.velocities.length
|
|
286
|
+
return {
|
|
287
|
+
kind: done ? 'rep' : 'todo',
|
|
288
|
+
velocity: done ? set.velocities[i] : undefined,
|
|
289
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
case 'range': {
|
|
294
|
+
const total = Math.max(set.max, set.velocities.length)
|
|
295
|
+
return Array.from({ length: total }, (_, i) => {
|
|
296
|
+
const kind: VelocitySlot['kind'] =
|
|
297
|
+
i < set.velocities.length ? 'rep' : i < set.floor ? 'todo' : 'variable'
|
|
298
|
+
return {
|
|
299
|
+
kind,
|
|
300
|
+
velocity: kind === 'rep' ? set.velocities[i] : undefined,
|
|
301
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
case 'amrap': {
|
|
306
|
+
const reps = set.velocities.map<VelocitySlot>((velocity, i) => ({
|
|
307
|
+
kind: 'rep',
|
|
308
|
+
velocity,
|
|
309
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
310
|
+
}))
|
|
311
|
+
reps.push({ kind: 'continue', leadingGap: reps.length === 0 ? 0 : REP_GAP })
|
|
312
|
+
return reps
|
|
313
|
+
}
|
|
314
|
+
case 'drop':
|
|
315
|
+
return chunkedRepSlots(set.subloads)
|
|
316
|
+
case 'myo': {
|
|
317
|
+
const slots = chunkedRepSlots([set.activation, ...set.clusters])
|
|
318
|
+
if (set.open) slots.push({ kind: 'continue', leadingGap: slots.length === 0 ? 0 : REP_GAP })
|
|
319
|
+
return slots
|
|
320
|
+
}
|
|
321
|
+
case 'cluster': {
|
|
322
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length)
|
|
323
|
+
return Array.from({ length: total }, (_, i) => {
|
|
324
|
+
const done = i < set.velocities.length
|
|
325
|
+
const boundary = i > 0 && i % set.groupSize === 0
|
|
326
|
+
return {
|
|
327
|
+
kind: done ? 'rep' : 'todo',
|
|
328
|
+
velocity: done ? set.velocities[i] : undefined,
|
|
329
|
+
leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP,
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** A concise, set-type-aware summary for the strip's accessibility label. */
|
|
337
|
+
function setAccessibilityLabel(set: VelocitySet, repCount: number): string {
|
|
338
|
+
switch (set.type) {
|
|
339
|
+
case 'straight':
|
|
340
|
+
return `Velocity strip, straight set, ${repCount} reps`
|
|
341
|
+
case 'range':
|
|
342
|
+
return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`
|
|
343
|
+
case 'amrap':
|
|
344
|
+
return `Velocity strip, AMRAP set, ${repCount} reps and counting`
|
|
345
|
+
case 'drop':
|
|
346
|
+
return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`
|
|
347
|
+
case 'myo':
|
|
348
|
+
return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ', open' : ''}, ${repCount} reps`
|
|
349
|
+
case 'cluster':
|
|
350
|
+
return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Per-slot accessibility label for the expanded set-type bars. */
|
|
355
|
+
function slotAccessibilityLabel(slot: VelocitySlot, index: number): string {
|
|
356
|
+
switch (slot.kind) {
|
|
357
|
+
case 'rep':
|
|
358
|
+
return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`
|
|
359
|
+
case 'todo':
|
|
360
|
+
return `Rep ${index + 1}: planned`
|
|
361
|
+
case 'variable':
|
|
362
|
+
return `Rep ${index + 1}: variable`
|
|
363
|
+
case 'continue':
|
|
364
|
+
return 'Keep going'
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Straight-set expanded stub height for a planned (todo) rep, as a % of the plot. */
|
|
369
|
+
const EXPANDED_TODO_STUB_PCT = 16
|
|
370
|
+
/** Expanded height for advanced set-types (drop / myo / cluster / range / amrap): a short mini-style bar. */
|
|
371
|
+
const EXPANDED_ENCODED_PCT = 45
|
|
372
|
+
|
|
373
|
+
/** Default framed `expanded` chart height (px). */
|
|
374
|
+
const EXPANDED_HEIGHT = 60
|
|
375
|
+
/**
|
|
376
|
+
* The `scale="fixed"` velocity ceiling (m/s). `peak` scales to the set's own max
|
|
377
|
+
* (+15% headroom); `fixed` uses this constant so a bar's height reads the same
|
|
378
|
+
* absolute velocity across sets (the spotlight).
|
|
379
|
+
*/
|
|
380
|
+
const FIXED_MAX_VELOCITY = 1.15
|
|
381
|
+
/** Minimum bare-strip bar height (px) — a planned / variable / continue stub, or a near-zero rep. */
|
|
382
|
+
const BARE_STUB_HEIGHT = 3
|
|
383
|
+
|
|
384
|
+
// --- Hero variant ------------------------------------------------------------
|
|
385
|
+
// The across-the-room, single-set wall treatment. Absorbs the R2 "HeroVelocityBars"
|
|
386
|
+
// candidate into VelocityStrip: tall bars + per-bar value labels + a dashed
|
|
387
|
+
// running-best reference line + dashed placeholders for the reps still to come.
|
|
388
|
+
|
|
389
|
+
/** Default `hero` plot height (px) — tall enough to read a set's velocity shape across a room. */
|
|
390
|
+
const HERO_HEIGHT = 220
|
|
391
|
+
/** Vertical band reserved above the tallest bar for its value label (px). */
|
|
392
|
+
const HERO_LABEL_HEADROOM = 20
|
|
393
|
+
/** Gap between hero bars (px) — a group-notch-free even rhythm at wall scale. */
|
|
394
|
+
const HERO_BAR_GAP = 8
|
|
395
|
+
/** Cap on a single hero bar's width so a 2–3 rep set doesn't render slab-wide bars (px). */
|
|
396
|
+
const HERO_BAR_MAX_WIDTH = 52
|
|
397
|
+
/** Top-corner radius on hero bars (px). */
|
|
398
|
+
const HERO_BAR_RADIUS = 5
|
|
399
|
+
/** Minimum drawn height of a performed hero bar (px) — a near-zero rep still reads as a rep. */
|
|
400
|
+
const HERO_MIN_BAR_HEIGHT = 4
|
|
401
|
+
/** Dashed-stub border for a planned-but-unperformed hero rep (charcoal, reads on a dark wall). */
|
|
402
|
+
const HERO_PENDING_COLOR = primitiveColors.charcoal[100]
|
|
403
|
+
/** The dashed running-best reference line + its label (the lightest charcoal step). */
|
|
404
|
+
const HERO_REFERENCE_COLOR = primitiveColors.charcoal[0]
|
|
405
|
+
|
|
406
|
+
/** The bar-height scaling denominator for the given `scale` (guarded ≥ 0 by callers). */
|
|
407
|
+
function scaleDenominator(scale: 'peak' | 'fixed', maxVelocity: number): number {
|
|
408
|
+
return scale === 'fixed' ? FIXED_MAX_VELOCITY : maxVelocity * 1.15
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Bare-strip bar height (px): velocity-scaled for a performed rep, a short stub otherwise. */
|
|
412
|
+
function bareSlotHeight(slot: VelocitySlot, height: number, denom: number): number {
|
|
413
|
+
if (slot.kind !== 'rep' || denom <= 0) return BARE_STUB_HEIGHT
|
|
414
|
+
const ratio = Math.min(1, (slot.velocity ?? 0) / denom)
|
|
415
|
+
return Math.max(BARE_STUB_HEIGHT, ratio * height)
|
|
416
|
+
}
|
|
417
|
+
|
|
153
418
|
function getReducedMotionPreference(): boolean {
|
|
154
419
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
|
|
155
420
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
@@ -173,40 +438,22 @@ const ANIMATION_DURATION = 400
|
|
|
173
438
|
const ANIMATION_EASING = Easing.bezier(0.22, 1, 0.36, 1)
|
|
174
439
|
const POP_EASING = Easing.bezier(0.34, 1.56, 0.64, 1)
|
|
175
440
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const maxVelocity = Math.max(...velocities, 0)
|
|
189
|
-
const meanVelocity = calculateMeanVelocity(velocities)
|
|
190
|
-
const loss = calculateVelocityLoss(velocities)
|
|
191
|
-
|
|
192
|
-
const hasZones = zones != null && zones.length > 0
|
|
193
|
-
const barColorFor = (v: number): string =>
|
|
194
|
-
hasZones ? bandColor(classifyBand(v, zones)!) : zoneHexMap[getVelocityZoneColor(v)]
|
|
195
|
-
const meanZone = hasZones
|
|
196
|
-
? (classifyBand(meanVelocity, zones)?.label ?? '')
|
|
197
|
-
: getVelocityZoneName(meanVelocity)
|
|
198
|
-
|
|
441
|
+
/**
|
|
442
|
+
* The newest-rep entrance shared by the framed `expanded` chart and `hero`: a
|
|
443
|
+
* bounce when the rep is a new set peak, a pop otherwise. Honors reduced motion.
|
|
444
|
+
* Returns the scale `Animated.Value` for the live bar; inert while `active` is
|
|
445
|
+
* false or no live rep is present.
|
|
446
|
+
*/
|
|
447
|
+
function useLiveRepPop(
|
|
448
|
+
active: boolean,
|
|
449
|
+
liveRepIndex: number | undefined,
|
|
450
|
+
liveVelocity: number | undefined,
|
|
451
|
+
isNewPeak: boolean
|
|
452
|
+
): Animated.Value {
|
|
199
453
|
const prefersReducedMotion = usePrefersReducedMotion()
|
|
200
|
-
const [heightAnim] = useState(() => new Animated.Value(expanded ? 60 : 3))
|
|
201
|
-
const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
202
|
-
const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
203
454
|
const [liveScale] = useState(() => new Animated.Value(1))
|
|
204
|
-
|
|
205
|
-
// Newest-rep animation: pop for a normal rep, bounce when it sets a new peak.
|
|
206
|
-
const liveVelocity = liveRepIndex != null ? velocities[liveRepIndex] : undefined
|
|
207
|
-
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity
|
|
208
455
|
useEffect(() => {
|
|
209
|
-
if (
|
|
456
|
+
if (!active || liveRepIndex == null || liveVelocity == null) return
|
|
210
457
|
if (prefersReducedMotion) {
|
|
211
458
|
liveScale.setValue(1)
|
|
212
459
|
return
|
|
@@ -236,14 +483,254 @@ export function VelocityStrip({
|
|
|
236
483
|
useNativeDriver: true,
|
|
237
484
|
}).start()
|
|
238
485
|
}
|
|
239
|
-
}, [
|
|
486
|
+
}, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale])
|
|
487
|
+
return liveScale
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
interface HeroVelocityChartProps {
|
|
491
|
+
/** Performed per-rep mean concentric velocities (m/s). */
|
|
492
|
+
doneVelocities: number[]
|
|
493
|
+
/** Zone → color resolver, shared with the other variants (single-sources the scale). */
|
|
494
|
+
barColorFor: (velocity: number) => string
|
|
495
|
+
/** Bar-height scaling denominator (`peak` set-max or `fixed` ceiling). */
|
|
496
|
+
scaleDenom: number
|
|
497
|
+
/** Running-best velocity (the set peak) — drives the dashed reference line. */
|
|
498
|
+
referenceVelocity: number
|
|
499
|
+
liveRepIndex?: number
|
|
500
|
+
isNewPeak: boolean
|
|
501
|
+
targetReps?: number
|
|
502
|
+
height: number
|
|
503
|
+
className?: string
|
|
504
|
+
viewProps: ViewProps
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The `hero` render: the across-the-room, single-set wall treatment. Tall bars +
|
|
509
|
+
* a per-bar value label + a dashed running-best reference line + dashed placeholders
|
|
510
|
+
* for the reps still to come. Reuses {@link barColorFor} (zone scale) and the
|
|
511
|
+
* {@link useLiveRepPop} entrance so it stays consistent with the framed chart.
|
|
512
|
+
*/
|
|
513
|
+
function HeroVelocityChart({
|
|
514
|
+
doneVelocities,
|
|
515
|
+
barColorFor,
|
|
516
|
+
scaleDenom,
|
|
517
|
+
referenceVelocity,
|
|
518
|
+
liveRepIndex,
|
|
519
|
+
isNewPeak,
|
|
520
|
+
targetReps,
|
|
521
|
+
height,
|
|
522
|
+
className,
|
|
523
|
+
viewProps,
|
|
524
|
+
}: HeroVelocityChartProps) {
|
|
525
|
+
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : undefined
|
|
526
|
+
const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak)
|
|
527
|
+
|
|
528
|
+
// Reserve a band above the tallest bar for its value label so a peak bar never clips.
|
|
529
|
+
const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM)
|
|
530
|
+
const barHeight = (velocity: number): number =>
|
|
531
|
+
scaleDenom > 0
|
|
532
|
+
? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight)
|
|
533
|
+
: HERO_MIN_BAR_HEIGHT
|
|
534
|
+
|
|
535
|
+
const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length)
|
|
536
|
+
const referencePx =
|
|
537
|
+
referenceVelocity > 0 && scaleDenom > 0
|
|
538
|
+
? Math.min(plotHeight, (referenceVelocity / scaleDenom) * plotHeight)
|
|
539
|
+
: 0
|
|
540
|
+
|
|
541
|
+
const repCount = doneVelocities.length
|
|
542
|
+
const total = Math.max(repCount, targetReps ?? repCount)
|
|
543
|
+
const label =
|
|
544
|
+
referenceVelocity > 0
|
|
545
|
+
? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second`
|
|
546
|
+
: `Velocity chart, ${repCount} of ${total} reps`
|
|
547
|
+
|
|
548
|
+
const { style: externalStyle, ...restProps } = viewProps
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<View
|
|
552
|
+
className={className}
|
|
553
|
+
style={[{ height }, externalStyle]}
|
|
554
|
+
accessibilityRole="image"
|
|
555
|
+
accessibilityLabel={label}
|
|
556
|
+
testID="velocity-strip-hero"
|
|
557
|
+
{...restProps}
|
|
558
|
+
>
|
|
559
|
+
<View
|
|
560
|
+
style={{
|
|
561
|
+
flex: 1,
|
|
562
|
+
flexDirection: 'row',
|
|
563
|
+
alignItems: 'flex-end',
|
|
564
|
+
gap: HERO_BAR_GAP,
|
|
565
|
+
position: 'relative',
|
|
566
|
+
borderBottomWidth: 2,
|
|
567
|
+
borderBottomColor: HERO_PENDING_COLOR,
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
{/*
|
|
571
|
+
* Running-best reference line. Deliberately unlabeled: it sits at the tallest
|
|
572
|
+
* bar's top, and that bar already displays its velocity as its own value label,
|
|
573
|
+
* so a "best X.XX" tag would duplicate it — and would collide with that label
|
|
574
|
+
* whenever the peak bar is on the same side (declines peak-left, ends-on-best
|
|
575
|
+
* peak-right). The line alone reads as "how far has velocity fallen from best".
|
|
576
|
+
* The numeric best is in the container's accessibility label.
|
|
577
|
+
*/}
|
|
578
|
+
{referencePx > 0 && (
|
|
579
|
+
<View
|
|
580
|
+
accessibilityElementsHidden
|
|
581
|
+
style={{
|
|
582
|
+
position: 'absolute',
|
|
583
|
+
left: 0,
|
|
584
|
+
right: 0,
|
|
585
|
+
bottom: referencePx,
|
|
586
|
+
borderTopWidth: 1,
|
|
587
|
+
borderStyle: 'dashed',
|
|
588
|
+
borderColor: HERO_REFERENCE_COLOR,
|
|
589
|
+
pointerEvents: 'none',
|
|
590
|
+
}}
|
|
591
|
+
testID="velocity-hero-reference"
|
|
592
|
+
/>
|
|
593
|
+
)}
|
|
594
|
+
|
|
595
|
+
{doneVelocities.map((velocity, i) => {
|
|
596
|
+
const isLive = liveRepIndex === i
|
|
597
|
+
return (
|
|
598
|
+
<View
|
|
599
|
+
key={i}
|
|
600
|
+
accessibilityElementsHidden
|
|
601
|
+
style={{
|
|
602
|
+
flex: 1,
|
|
603
|
+
maxWidth: HERO_BAR_MAX_WIDTH,
|
|
604
|
+
height: '100%',
|
|
605
|
+
alignItems: 'center',
|
|
606
|
+
justifyContent: 'flex-end',
|
|
607
|
+
}}
|
|
608
|
+
>
|
|
609
|
+
<Text
|
|
610
|
+
className="text-text-primary"
|
|
611
|
+
style={{ fontSize: 12, fontWeight: '800', marginBottom: 4 }}
|
|
612
|
+
testID={`velocity-label-${i}`}
|
|
613
|
+
>
|
|
614
|
+
{formatVelocity(velocity)}
|
|
615
|
+
</Text>
|
|
616
|
+
<Animated.View
|
|
617
|
+
style={[
|
|
618
|
+
{
|
|
619
|
+
width: '100%',
|
|
620
|
+
height: barHeight(velocity),
|
|
621
|
+
borderTopLeftRadius: HERO_BAR_RADIUS,
|
|
622
|
+
borderTopRightRadius: HERO_BAR_RADIUS,
|
|
623
|
+
backgroundColor: barColorFor(velocity),
|
|
624
|
+
},
|
|
625
|
+
isLive ? { transform: [{ scale: liveScale }] } : null,
|
|
626
|
+
]}
|
|
627
|
+
testID={`velocity-bar-${i}`}
|
|
628
|
+
/>
|
|
629
|
+
</View>
|
|
630
|
+
)
|
|
631
|
+
})}
|
|
632
|
+
|
|
633
|
+
{Array.from({ length: pendingCount }, (_, i) => (
|
|
634
|
+
<View
|
|
635
|
+
key={`pending-${i}`}
|
|
636
|
+
accessibilityElementsHidden
|
|
637
|
+
style={{
|
|
638
|
+
flex: 1,
|
|
639
|
+
maxWidth: HERO_BAR_MAX_WIDTH,
|
|
640
|
+
height: '100%',
|
|
641
|
+
alignItems: 'center',
|
|
642
|
+
justifyContent: 'flex-end',
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
645
|
+
<View
|
|
646
|
+
style={{
|
|
647
|
+
width: '100%',
|
|
648
|
+
height: HERO_MIN_BAR_HEIGHT * 3,
|
|
649
|
+
borderWidth: 1,
|
|
650
|
+
borderStyle: 'dashed',
|
|
651
|
+
borderColor: HERO_PENDING_COLOR,
|
|
652
|
+
borderTopLeftRadius: HERO_BAR_RADIUS,
|
|
653
|
+
borderTopRightRadius: HERO_BAR_RADIUS,
|
|
654
|
+
}}
|
|
655
|
+
testID="velocity-slot-todo"
|
|
656
|
+
/>
|
|
657
|
+
</View>
|
|
658
|
+
))}
|
|
659
|
+
</View>
|
|
660
|
+
</View>
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function VelocityStrip({
|
|
665
|
+
velocities,
|
|
666
|
+
set,
|
|
667
|
+
zones,
|
|
668
|
+
liveRepIndex,
|
|
669
|
+
expanded = true,
|
|
670
|
+
onToggle,
|
|
671
|
+
onRepPress,
|
|
672
|
+
variant = 'expanded',
|
|
673
|
+
height = EXPANDED_HEIGHT,
|
|
674
|
+
targetReps,
|
|
675
|
+
scale = 'peak',
|
|
676
|
+
showNumbers = true,
|
|
677
|
+
showInfo = true,
|
|
678
|
+
className,
|
|
679
|
+
...props
|
|
680
|
+
}: VelocityStripProps) {
|
|
681
|
+
// A `set` descriptor derives its own done-velocity array; the legacy
|
|
682
|
+
// `velocities` path stays the source of truth otherwise. Every summary calc
|
|
683
|
+
// (mean / loss / zone) runs on this one array so the info row works either way.
|
|
684
|
+
const doneVelocities = set ? deriveDoneVelocities(set) : (velocities ?? [])
|
|
685
|
+
const slots: VelocitySlot[] = set
|
|
686
|
+
? buildSlots(set)
|
|
687
|
+
: doneVelocities.map((v, i) => ({
|
|
688
|
+
kind: 'rep',
|
|
689
|
+
velocity: v,
|
|
690
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
691
|
+
}))
|
|
692
|
+
|
|
693
|
+
const maxVelocity = Math.max(...doneVelocities, 0)
|
|
694
|
+
const meanVelocity = calculateMeanVelocity(doneVelocities)
|
|
695
|
+
const loss = calculateVelocityLoss(doneVelocities)
|
|
696
|
+
|
|
697
|
+
// The framed chart (raised box, labels, info) vs the bare spotlight strip is the
|
|
698
|
+
// only fork in the `expanded` variant — keyed by whether any chrome is requested.
|
|
699
|
+
const framed = showNumbers || showInfo
|
|
700
|
+
const scaleDenom = scaleDenominator(scale, maxVelocity)
|
|
701
|
+
|
|
702
|
+
const hasZones = zones != null && zones.length > 0
|
|
703
|
+
const barColorFor = (v: number): string =>
|
|
704
|
+
hasZones ? bandColor(classifyBand(v, zones)!) : zoneHexMap[getVelocityZoneColor(v)]
|
|
705
|
+
const slotColor = (slot: VelocitySlot): string => {
|
|
706
|
+
if (slot.kind === 'rep') return barColorFor(slot.velocity ?? 0)
|
|
707
|
+
if (slot.kind === 'todo') return TODO_COLOR
|
|
708
|
+
return SET_STRIP_VARIABLE_COLOR
|
|
709
|
+
}
|
|
710
|
+
const meanZone = hasZones
|
|
711
|
+
? (classifyBand(meanVelocity, zones)?.label ?? '')
|
|
712
|
+
: getVelocityZoneName(meanVelocity)
|
|
713
|
+
|
|
714
|
+
const [heightAnim] = useState(() => new Animated.Value(expanded ? height : 3))
|
|
715
|
+
const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
716
|
+
const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
717
|
+
|
|
718
|
+
// Newest-rep animation: pop for a normal rep, bounce when it sets a new peak.
|
|
719
|
+
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : undefined
|
|
720
|
+
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity
|
|
721
|
+
const liveScale = useLiveRepPop(
|
|
722
|
+
variant === 'expanded' && framed,
|
|
723
|
+
liveRepIndex,
|
|
724
|
+
liveVelocity,
|
|
725
|
+
isNewPeak
|
|
726
|
+
)
|
|
240
727
|
|
|
241
728
|
useEffect(() => {
|
|
242
|
-
if (variant
|
|
729
|
+
if (variant !== 'expanded' || !framed) return
|
|
243
730
|
|
|
244
731
|
Animated.parallel([
|
|
245
732
|
Animated.timing(heightAnim, {
|
|
246
|
-
toValue: expanded ?
|
|
733
|
+
toValue: expanded ? height : 3,
|
|
247
734
|
duration: ANIMATION_DURATION,
|
|
248
735
|
easing: ANIMATION_EASING,
|
|
249
736
|
useNativeDriver: false,
|
|
@@ -261,42 +748,168 @@ export function VelocityStrip({
|
|
|
261
748
|
useNativeDriver: false,
|
|
262
749
|
}),
|
|
263
750
|
]).start()
|
|
264
|
-
}, [expanded, variant, heightAnim, labelOpacity, infoOpacity])
|
|
751
|
+
}, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity])
|
|
752
|
+
|
|
753
|
+
// Nothing to draw: neither a legacy velocity array nor a set descriptor.
|
|
754
|
+
if (set == null && velocities == null) return null
|
|
755
|
+
|
|
756
|
+
const repCount = doneVelocities.length
|
|
757
|
+
const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`
|
|
758
|
+
|
|
759
|
+
if (variant === 'hero') {
|
|
760
|
+
// Hero's plot is far taller than the expanded chart; apply its own default when
|
|
761
|
+
// the caller left `height` at the shared 60px default.
|
|
762
|
+
const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height
|
|
763
|
+
return (
|
|
764
|
+
<HeroVelocityChart
|
|
765
|
+
doneVelocities={doneVelocities}
|
|
766
|
+
barColorFor={barColorFor}
|
|
767
|
+
scaleDenom={scaleDenom}
|
|
768
|
+
referenceVelocity={maxVelocity}
|
|
769
|
+
liveRepIndex={liveRepIndex}
|
|
770
|
+
isNewPeak={isNewPeak}
|
|
771
|
+
targetReps={targetReps}
|
|
772
|
+
height={heroHeight}
|
|
773
|
+
className={className}
|
|
774
|
+
viewProps={props}
|
|
775
|
+
/>
|
|
776
|
+
)
|
|
777
|
+
}
|
|
265
778
|
|
|
266
779
|
if (variant === 'mini') {
|
|
267
780
|
const { style: externalStyle, ...restProps } = props
|
|
781
|
+
// The container keeps the uniform REP_GAP (so a no-`set` strip is byte-identical
|
|
782
|
+
// to prior releases and holds HTML-parity); per-slot `marginLeft` carries only
|
|
783
|
+
// the EXTRA spacing for the WIDE notch that chunks drop / myo / cluster sets.
|
|
268
784
|
return (
|
|
269
785
|
<View
|
|
270
786
|
className={className}
|
|
271
|
-
style={[
|
|
787
|
+
style={[
|
|
788
|
+
{ flexDirection: 'row', height: 3, gap: REP_GAP, borderRadius: 2, overflow: 'hidden' },
|
|
789
|
+
externalStyle,
|
|
790
|
+
]}
|
|
272
791
|
accessibilityRole="image"
|
|
273
|
-
accessibilityLabel={
|
|
792
|
+
accessibilityLabel={miniLabel}
|
|
274
793
|
testID="velocity-strip-mini"
|
|
275
794
|
{...restProps}
|
|
276
795
|
>
|
|
277
|
-
{
|
|
796
|
+
{slots.map((slot, i) => (
|
|
278
797
|
<View
|
|
279
798
|
key={i}
|
|
280
799
|
style={{
|
|
281
800
|
flex: 1,
|
|
282
|
-
backgroundColor:
|
|
801
|
+
backgroundColor: slotColor(slot),
|
|
283
802
|
borderRadius: 1,
|
|
284
803
|
minWidth: 4,
|
|
285
804
|
height: '100%' as unknown as number,
|
|
805
|
+
// The container's uniform 2px gap covers rep spacing; a wide slot adds
|
|
806
|
+
// only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
|
|
807
|
+
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
808
|
+
...(slot.kind === 'continue'
|
|
809
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
810
|
+
: {}),
|
|
286
811
|
}}
|
|
287
812
|
accessibilityElementsHidden
|
|
288
|
-
testID={`velocity-bar-${i}`}
|
|
813
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
289
814
|
/>
|
|
290
815
|
))}
|
|
291
816
|
</View>
|
|
292
817
|
)
|
|
293
818
|
}
|
|
294
819
|
|
|
295
|
-
|
|
820
|
+
// Bare `expanded` strip (both chrome flags off): the velocity-HEIGHT spotlight —
|
|
821
|
+
// px bar heights, no raised box / labels / info row / animation. Set-type gaps carry
|
|
822
|
+
// over from the slot model exactly as in `mini` (container REP_GAP + per-slot extra
|
|
823
|
+
// for a WIDE notch); planned / variable / continue slots draw as stubs.
|
|
824
|
+
if (!framed) {
|
|
825
|
+
const { style: externalStyle, ...restProps } = props
|
|
826
|
+
return (
|
|
827
|
+
<View
|
|
828
|
+
className={className}
|
|
829
|
+
style={[
|
|
830
|
+
{ flexDirection: 'row', height, gap: REP_GAP, alignItems: 'flex-end' },
|
|
831
|
+
externalStyle,
|
|
832
|
+
]}
|
|
833
|
+
accessibilityRole="image"
|
|
834
|
+
accessibilityLabel={miniLabel}
|
|
835
|
+
testID="velocity-strip-compact"
|
|
836
|
+
{...restProps}
|
|
837
|
+
>
|
|
838
|
+
{slots.map((slot, i) => (
|
|
839
|
+
<View
|
|
840
|
+
key={i}
|
|
841
|
+
style={{
|
|
842
|
+
flex: 1,
|
|
843
|
+
minWidth: 4,
|
|
844
|
+
height: bareSlotHeight(slot, height, scaleDenom),
|
|
845
|
+
// Top-rounded to match the framed chart's bars (the "rounded tops from
|
|
846
|
+
// the numbers one"); square bottoms since bars sit on the baseline.
|
|
847
|
+
borderTopLeftRadius: 2,
|
|
848
|
+
borderTopRightRadius: 2,
|
|
849
|
+
backgroundColor: slotColor(slot),
|
|
850
|
+
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
851
|
+
...(slot.kind === 'continue'
|
|
852
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
853
|
+
: {}),
|
|
854
|
+
}}
|
|
855
|
+
accessibilityElementsHidden
|
|
856
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
857
|
+
/>
|
|
858
|
+
))}
|
|
859
|
+
</View>
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const stripLabel = set
|
|
864
|
+
? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? 'collapse' : 'expand'}`
|
|
865
|
+
: `Velocity chart for set, ${repCount} reps, tap to ${expanded ? 'collapse' : 'expand'}`
|
|
296
866
|
// When onToggle wraps the strip or individual reps are interactive, the container itself is not a button
|
|
297
867
|
const hasInteractiveContainer = onToggle != null
|
|
298
868
|
const hasInteractiveReps = onRepPress != null && expanded
|
|
299
869
|
|
|
870
|
+
// Expanded set-type bars: `straight` is the active-set spotlight (velocity-height
|
|
871
|
+
// done reps + short grey planned stubs); the advanced types render as a short,
|
|
872
|
+
// mini-style encoding (the gaps + colors carry identity, height is uniform).
|
|
873
|
+
const setBars =
|
|
874
|
+
set != null &&
|
|
875
|
+
slots.map((slot, i) => {
|
|
876
|
+
const isStraightRep = set.type === 'straight' && slot.kind === 'rep'
|
|
877
|
+
const heightPct = isStraightRep
|
|
878
|
+
? scaleDenom > 0
|
|
879
|
+
? Math.round(((slot.velocity ?? 0) / scaleDenom) * 100)
|
|
880
|
+
: 0
|
|
881
|
+
: set.type === 'straight'
|
|
882
|
+
? EXPANDED_TODO_STUB_PCT
|
|
883
|
+
: EXPANDED_ENCODED_PCT
|
|
884
|
+
return (
|
|
885
|
+
<View
|
|
886
|
+
key={i}
|
|
887
|
+
style={{
|
|
888
|
+
flex: 1,
|
|
889
|
+
height: '100%',
|
|
890
|
+
justifyContent: 'flex-end',
|
|
891
|
+
marginLeft: slot.leadingGap,
|
|
892
|
+
}}
|
|
893
|
+
>
|
|
894
|
+
<View
|
|
895
|
+
style={{
|
|
896
|
+
height: `${heightPct}%`,
|
|
897
|
+
minHeight: 2,
|
|
898
|
+
borderTopLeftRadius: 2,
|
|
899
|
+
borderTopRightRadius: 2,
|
|
900
|
+
backgroundColor: slotColor(slot),
|
|
901
|
+
...(slot.kind === 'continue'
|
|
902
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
903
|
+
: {}),
|
|
904
|
+
}}
|
|
905
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
906
|
+
accessibilityRole="image"
|
|
907
|
+
accessibilityLabel={slotAccessibilityLabel(slot, i)}
|
|
908
|
+
/>
|
|
909
|
+
</View>
|
|
910
|
+
)
|
|
911
|
+
})
|
|
912
|
+
|
|
300
913
|
const stripContent = (
|
|
301
914
|
<Animated.View
|
|
302
915
|
className={[className, expanded ? 'bg-surface-raised' : 'bg-transparent']
|
|
@@ -310,7 +923,9 @@ export function VelocityStrip({
|
|
|
310
923
|
borderRadius: expanded ? 6 : 2,
|
|
311
924
|
paddingTop: expanded ? 16 : 0,
|
|
312
925
|
paddingBottom: expanded ? 24 : 0,
|
|
313
|
-
|
|
926
|
+
// No horizontal inset — the framed chart's bars span full width, matching
|
|
927
|
+
// the bare spotlight and mini. Numbers/info sit in the vertical padding.
|
|
928
|
+
paddingHorizontal: 0,
|
|
314
929
|
paddingVertical: expanded ? undefined : 8,
|
|
315
930
|
overflow: expanded ? 'visible' : 'hidden',
|
|
316
931
|
},
|
|
@@ -320,86 +935,106 @@ export function VelocityStrip({
|
|
|
320
935
|
testID="velocity-strip"
|
|
321
936
|
{...props}
|
|
322
937
|
>
|
|
323
|
-
<View
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: ''
|
|
338
|
-
|
|
339
|
-
const barStyle: ViewStyle = expanded
|
|
340
|
-
? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: barBackground }
|
|
341
|
-
: {
|
|
342
|
-
minHeight: '100%' as unknown as number,
|
|
343
|
-
borderTopLeftRadius: 2,
|
|
344
|
-
borderTopRightRadius: 2,
|
|
345
|
-
borderBottomLeftRadius: 0,
|
|
346
|
-
borderBottomRightRadius: 0,
|
|
347
|
-
backgroundColor: barBackground,
|
|
348
|
-
}
|
|
938
|
+
<View style={{ flexDirection: 'row', flex: 1, gap: set ? 0 : 2, alignItems: 'flex-end' }}>
|
|
939
|
+
{set
|
|
940
|
+
? setBars
|
|
941
|
+
: doneVelocities.map((v, i) => {
|
|
942
|
+
const barBackground = barColorFor(v)
|
|
943
|
+
// Guard all-zero velocities (idle / pre-rep): a 0 denominator makes
|
|
944
|
+
// this 0 / 0 === NaN and emits height:'NaN%'. Flatten the bars instead.
|
|
945
|
+
const barHeightPct = scaleDenom > 0 ? Math.round((v / scaleDenom) * 100) : 0
|
|
946
|
+
const isLive = liveRepIndex === i
|
|
947
|
+
const liveLabelSuffix = isLive
|
|
948
|
+
? isNewPeak
|
|
949
|
+
? ', latest rep, new set peak'
|
|
950
|
+
: ', latest rep'
|
|
951
|
+
: ''
|
|
349
952
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
953
|
+
const barStyle: ViewStyle = expanded
|
|
954
|
+
? {
|
|
955
|
+
height: `${barHeightPct}%`,
|
|
956
|
+
minHeight: 2,
|
|
957
|
+
borderTopLeftRadius: 2,
|
|
958
|
+
borderTopRightRadius: 2,
|
|
959
|
+
backgroundColor: barBackground,
|
|
960
|
+
}
|
|
961
|
+
: {
|
|
962
|
+
minHeight: '100%' as unknown as number,
|
|
963
|
+
borderTopLeftRadius: 2,
|
|
964
|
+
borderTopRightRadius: 2,
|
|
965
|
+
borderBottomLeftRadius: 0,
|
|
966
|
+
borderBottomRightRadius: 0,
|
|
967
|
+
backgroundColor: barBackground,
|
|
968
|
+
}
|
|
365
969
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
)}
|
|
382
|
-
{barInner}
|
|
383
|
-
</View>
|
|
384
|
-
)
|
|
970
|
+
const barInner = isLive ? (
|
|
971
|
+
<Animated.View
|
|
972
|
+
style={[barStyle, { transform: [{ scale: liveScale }] }]}
|
|
973
|
+
testID={`velocity-bar-${i}`}
|
|
974
|
+
accessibilityRole="image"
|
|
975
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`}
|
|
976
|
+
/>
|
|
977
|
+
) : (
|
|
978
|
+
<View
|
|
979
|
+
style={barStyle}
|
|
980
|
+
testID={`velocity-bar-${i}`}
|
|
981
|
+
accessibilityRole="image"
|
|
982
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second`}
|
|
983
|
+
/>
|
|
984
|
+
)
|
|
385
985
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
986
|
+
const bar = (
|
|
987
|
+
<View
|
|
988
|
+
key={i}
|
|
989
|
+
style={{
|
|
990
|
+
flex: 1,
|
|
991
|
+
height: '100%',
|
|
992
|
+
justifyContent: 'flex-end',
|
|
993
|
+
position: 'relative',
|
|
994
|
+
}}
|
|
995
|
+
>
|
|
996
|
+
{expanded && showNumbers && (
|
|
997
|
+
<Animated.View
|
|
998
|
+
style={{
|
|
999
|
+
opacity: labelOpacity,
|
|
1000
|
+
alignItems: 'center',
|
|
1001
|
+
position: 'absolute',
|
|
1002
|
+
top: -13,
|
|
1003
|
+
left: 0,
|
|
1004
|
+
right: 0,
|
|
1005
|
+
}}
|
|
1006
|
+
accessibilityElementsHidden
|
|
1007
|
+
>
|
|
1008
|
+
<Text
|
|
1009
|
+
className="text-text-secondary"
|
|
1010
|
+
style={{ fontSize: 8, fontWeight: '600' }}
|
|
1011
|
+
testID={`velocity-label-${i}`}
|
|
1012
|
+
>
|
|
1013
|
+
{formatVelocity(v)}
|
|
1014
|
+
</Text>
|
|
1015
|
+
</Animated.View>
|
|
1016
|
+
)}
|
|
1017
|
+
{barInner}
|
|
1018
|
+
</View>
|
|
1019
|
+
)
|
|
400
1020
|
|
|
401
|
-
|
|
402
|
-
|
|
1021
|
+
if (onRepPress && expanded) {
|
|
1022
|
+
return (
|
|
1023
|
+
<Pressable
|
|
1024
|
+
key={i}
|
|
1025
|
+
style={{ flex: 1, height: '100%' }}
|
|
1026
|
+
onPress={() => onRepPress(i, v)}
|
|
1027
|
+
accessibilityRole="button"
|
|
1028
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`}
|
|
1029
|
+
testID={`velocity-bar-pressable-${i}`}
|
|
1030
|
+
>
|
|
1031
|
+
{bar}
|
|
1032
|
+
</Pressable>
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return bar
|
|
1037
|
+
})}
|
|
403
1038
|
</View>
|
|
404
1039
|
{expanded && showInfo && (
|
|
405
1040
|
<Animated.View
|