@titan-design/react-ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodymap.js +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +855 -124
- package/dist/index.d.ts +855 -124
- package/dist/index.js +2276 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -802
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1255 -798
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1255 -798
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +15 -10
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -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,36 @@ 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}.
|
|
88
|
+
*/
|
|
89
|
+
variant?: 'mini' | 'expanded'
|
|
90
|
+
/** `expanded` plot height in px (bars scale to this). Default 60. */
|
|
91
|
+
height?: number
|
|
92
|
+
/**
|
|
93
|
+
* `expanded` bar scaling. `peak` (default) scales to the set's own max (+15%
|
|
94
|
+
* headroom). `fixed` scales to a fixed velocity ceiling so bar heights read the
|
|
95
|
+
* same absolute velocity across sets (the spotlight).
|
|
96
|
+
*/
|
|
97
|
+
scale?: 'peak' | 'fixed'
|
|
98
|
+
/** `expanded` framed chart: per-bar m/s labels. Default true. */
|
|
99
|
+
showNumbers?: boolean
|
|
100
|
+
/** `expanded` framed chart: the mean/loss info row. Default true. */
|
|
56
101
|
showInfo?: boolean
|
|
57
102
|
className?: string
|
|
58
103
|
}
|
|
@@ -62,26 +107,25 @@ export interface VelocityStripProps extends ViewProps {
|
|
|
62
107
|
const VEL_COLORS = WORKOUT_TOKENS.scale
|
|
63
108
|
|
|
64
109
|
/**
|
|
65
|
-
* Map a velocity-zone id (WA's 5-band taxonomy) onto the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* stays red.
|
|
110
|
+
* Map a velocity-zone id (WA's 5-band taxonomy) directly onto the 6-stop
|
|
111
|
+
* `sequentialEffort` ramp. The default strip uses the 4-color {@link VEL_COLORS}
|
|
112
|
+
* scale (a subsample of the same ramp), but the 5-band taxonomy has one more
|
|
113
|
+
* level than that scale carries, so it samples 5 stops: the top four align with
|
|
114
|
+
* the default scale (speed/power/strengthSpeed/maximalStrength = green/gold/
|
|
115
|
+
* orange/red) and `grinding` takes the ramp's darker red. `maximalStrength` and
|
|
116
|
+
* `grinding` therefore stay DISTINCT — they no longer collapse onto one red for
|
|
117
|
+
* lack of a 5th hue. An exercise moving from default to profile-derived zones
|
|
118
|
+
* never shifts its fast-end colors.
|
|
75
119
|
*
|
|
76
120
|
* Unknown ids fall back to `green` (matching the historical default-path
|
|
77
121
|
* fallback), so a forward-compatible band id never renders an empty bar.
|
|
78
122
|
*/
|
|
79
|
-
const
|
|
80
|
-
speed:
|
|
81
|
-
power:
|
|
82
|
-
strengthSpeed:
|
|
83
|
-
maximalStrength:
|
|
84
|
-
grinding:
|
|
123
|
+
const bandIdToEffortColor: Record<string, string> = {
|
|
124
|
+
speed: sequentialEffort[0], // green-300
|
|
125
|
+
power: sequentialEffort[2], // amber-300 (gold)
|
|
126
|
+
strengthSpeed: sequentialEffort[3], // orange-400
|
|
127
|
+
maximalStrength: sequentialEffort[4], // red-600
|
|
128
|
+
grinding: sequentialEffort[5], // red-700 — the distinct 5th band
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
// --- Default (no-zones) scale ------------------------------------------------
|
|
@@ -131,7 +175,7 @@ export function calculateMeanVelocity(velocities: number[]): number {
|
|
|
131
175
|
/** Classify a velocity into its band (slow → fast, min inclusive / max exclusive). */
|
|
132
176
|
function classifyBand(
|
|
133
177
|
velocity: number,
|
|
134
|
-
bands: readonly VelocityZoneBandProp[]
|
|
178
|
+
bands: readonly VelocityZoneBandProp[]
|
|
135
179
|
): VelocityZoneBandProp | undefined {
|
|
136
180
|
for (const band of bands) {
|
|
137
181
|
if (band.max === null || velocity < band.max) return band
|
|
@@ -140,8 +184,7 @@ function classifyBand(
|
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
function bandColor(band: VelocityZoneBandProp): string {
|
|
143
|
-
|
|
144
|
-
return token ? VEL_COLORS[token] : VEL_COLORS.green
|
|
187
|
+
return bandIdToEffortColor[band.id] ?? VEL_COLORS.green
|
|
145
188
|
}
|
|
146
189
|
|
|
147
190
|
function getLossStyle(loss: number): Record<string, string> | null {
|
|
@@ -150,6 +193,197 @@ function getLossStyle(loss: number): Record<string, string> | null {
|
|
|
150
193
|
return null
|
|
151
194
|
}
|
|
152
195
|
|
|
196
|
+
// --- Set-type slot model -----------------------------------------------------
|
|
197
|
+
// The strength-training set vocabulary (operator-approved in the Set Modalities
|
|
198
|
+
// exploration): one strip encoding for every per-rep set type. A set resolves to
|
|
199
|
+
// an ordered list of slots; the slot `kind` picks the color and `leadingGap`
|
|
200
|
+
// encodes the chunk pattern (rep gap vs the wide notch that splits drop sub-loads
|
|
201
|
+
// / myo clusters / cluster intra-rests). Set-LEVEL types (pyramid / superset /
|
|
202
|
+
// tempo) belong to the heading, not the strip.
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A structured strength-training set. Supply as {@link VelocityStripProps.set} to
|
|
206
|
+
* render its set-type encoding; the done-velocity array (used for the mean / loss
|
|
207
|
+
* / zone summary) is derived from the type's velocity fields.
|
|
208
|
+
*/
|
|
209
|
+
export type VelocitySet =
|
|
210
|
+
/** Fixed reps at a fixed load: done reps colored + a grey todo remainder to `planned`. */
|
|
211
|
+
| { type: 'straight'; velocities: number[]; planned?: number }
|
|
212
|
+
/** Bounded reps: committed slots `0..floor`, then a cyan variable window `floor..max`. */
|
|
213
|
+
| { type: 'range'; velocities: number[]; floor: number; max: number }
|
|
214
|
+
/** As-many-reps-as-possible: done reps + a trailing cyan "continue" slot that never closes. */
|
|
215
|
+
| { type: 'amrap'; velocities: number[]; target?: number }
|
|
216
|
+
/** Load drops with no rest: each sub-load's reps, split by a WIDE notch gap. */
|
|
217
|
+
| { type: 'drop'; subloads: number[][] }
|
|
218
|
+
/** Rest-pause: an activation chunk + mini-clusters split by WIDE gaps; `open` adds a cyan continue. */
|
|
219
|
+
| { type: 'myo'; activation: number[]; clusters: number[][]; open?: boolean }
|
|
220
|
+
/** Fixed count broken by intra-set rests: reps grouped by `groupSize` with WIDE intra-rest gaps. */
|
|
221
|
+
| { type: 'cluster'; velocities: number[]; groupSize: number; planned?: number }
|
|
222
|
+
|
|
223
|
+
/** One drawn cell of the strip. `kind` picks the color; `leadingGap` the spacing before it. */
|
|
224
|
+
interface VelocitySlot {
|
|
225
|
+
kind: 'rep' | 'todo' | 'variable' | 'continue'
|
|
226
|
+
velocity?: number
|
|
227
|
+
leadingGap: number
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Butted reps carry this gap; chunk boundaries carry {@link WIDE_GAP}. */
|
|
231
|
+
const REP_GAP = 2
|
|
232
|
+
/**
|
|
233
|
+
* The between-group notch (drop sub-loads / myo clusters / cluster intra-rests): a
|
|
234
|
+
* fixed 8px total gap that reads as a group break without dominating a narrow strip.
|
|
235
|
+
* 4× the rep gap — enough to separate groups at session-rail scale AND wall scale
|
|
236
|
+
* without the notch ballooning at large widths. In the mini variant the container's
|
|
237
|
+
* 2px gap covers part of it, so per-slot margin adds the remaining 6px (2 + 6 = 8).
|
|
238
|
+
*/
|
|
239
|
+
const WIDE_GAP = 8
|
|
240
|
+
|
|
241
|
+
/** Grey fill for planned-but-unperformed reps (charcoal placeholder, literal hex). */
|
|
242
|
+
const TODO_COLOR = primitiveColors.charcoal[300]
|
|
243
|
+
/** The thin cyan-800 outline on the open-ended "continue" slot — reads as "keep going". */
|
|
244
|
+
const CONTINUE_OUTLINE = primitiveRamps.cyan[800]
|
|
245
|
+
|
|
246
|
+
/** The done-velocity array a set contributes to the mean / loss / zone summary. */
|
|
247
|
+
function deriveDoneVelocities(set: VelocitySet): number[] {
|
|
248
|
+
switch (set.type) {
|
|
249
|
+
case 'drop':
|
|
250
|
+
return set.subloads.flat()
|
|
251
|
+
case 'myo':
|
|
252
|
+
return [...set.activation, ...set.clusters.flat()]
|
|
253
|
+
default:
|
|
254
|
+
return set.velocities
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Flatten velocity chunks into rep slots, carving a WIDE gap before each chunk but the first. */
|
|
259
|
+
function chunkedRepSlots(chunks: number[][]): VelocitySlot[] {
|
|
260
|
+
const slots: VelocitySlot[] = []
|
|
261
|
+
chunks.forEach((chunk) => {
|
|
262
|
+
chunk.forEach((velocity, ri) => {
|
|
263
|
+
const first = slots.length === 0
|
|
264
|
+
slots.push({ kind: 'rep', velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP })
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
return slots
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Resolve a set to its ordered slot list — the exact vocabulary of the Set Modalities sheet. */
|
|
271
|
+
function buildSlots(set: VelocitySet): VelocitySlot[] {
|
|
272
|
+
switch (set.type) {
|
|
273
|
+
case 'straight': {
|
|
274
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length)
|
|
275
|
+
return Array.from({ length: total }, (_, i) => {
|
|
276
|
+
const done = i < set.velocities.length
|
|
277
|
+
return {
|
|
278
|
+
kind: done ? 'rep' : 'todo',
|
|
279
|
+
velocity: done ? set.velocities[i] : undefined,
|
|
280
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
case 'range': {
|
|
285
|
+
const total = Math.max(set.max, set.velocities.length)
|
|
286
|
+
return Array.from({ length: total }, (_, i) => {
|
|
287
|
+
const kind: VelocitySlot['kind'] =
|
|
288
|
+
i < set.velocities.length ? 'rep' : i < set.floor ? 'todo' : 'variable'
|
|
289
|
+
return {
|
|
290
|
+
kind,
|
|
291
|
+
velocity: kind === 'rep' ? set.velocities[i] : undefined,
|
|
292
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
case 'amrap': {
|
|
297
|
+
const reps = set.velocities.map<VelocitySlot>((velocity, i) => ({
|
|
298
|
+
kind: 'rep',
|
|
299
|
+
velocity,
|
|
300
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
301
|
+
}))
|
|
302
|
+
reps.push({ kind: 'continue', leadingGap: reps.length === 0 ? 0 : REP_GAP })
|
|
303
|
+
return reps
|
|
304
|
+
}
|
|
305
|
+
case 'drop':
|
|
306
|
+
return chunkedRepSlots(set.subloads)
|
|
307
|
+
case 'myo': {
|
|
308
|
+
const slots = chunkedRepSlots([set.activation, ...set.clusters])
|
|
309
|
+
if (set.open) slots.push({ kind: 'continue', leadingGap: slots.length === 0 ? 0 : REP_GAP })
|
|
310
|
+
return slots
|
|
311
|
+
}
|
|
312
|
+
case 'cluster': {
|
|
313
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length)
|
|
314
|
+
return Array.from({ length: total }, (_, i) => {
|
|
315
|
+
const done = i < set.velocities.length
|
|
316
|
+
const boundary = i > 0 && i % set.groupSize === 0
|
|
317
|
+
return {
|
|
318
|
+
kind: done ? 'rep' : 'todo',
|
|
319
|
+
velocity: done ? set.velocities[i] : undefined,
|
|
320
|
+
leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP,
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A concise, set-type-aware summary for the strip's accessibility label. */
|
|
328
|
+
function setAccessibilityLabel(set: VelocitySet, repCount: number): string {
|
|
329
|
+
switch (set.type) {
|
|
330
|
+
case 'straight':
|
|
331
|
+
return `Velocity strip, straight set, ${repCount} reps`
|
|
332
|
+
case 'range':
|
|
333
|
+
return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`
|
|
334
|
+
case 'amrap':
|
|
335
|
+
return `Velocity strip, AMRAP set, ${repCount} reps and counting`
|
|
336
|
+
case 'drop':
|
|
337
|
+
return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`
|
|
338
|
+
case 'myo':
|
|
339
|
+
return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ', open' : ''}, ${repCount} reps`
|
|
340
|
+
case 'cluster':
|
|
341
|
+
return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Per-slot accessibility label for the expanded set-type bars. */
|
|
346
|
+
function slotAccessibilityLabel(slot: VelocitySlot, index: number): string {
|
|
347
|
+
switch (slot.kind) {
|
|
348
|
+
case 'rep':
|
|
349
|
+
return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`
|
|
350
|
+
case 'todo':
|
|
351
|
+
return `Rep ${index + 1}: planned`
|
|
352
|
+
case 'variable':
|
|
353
|
+
return `Rep ${index + 1}: variable`
|
|
354
|
+
case 'continue':
|
|
355
|
+
return 'Keep going'
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Straight-set expanded stub height for a planned (todo) rep, as a % of the plot. */
|
|
360
|
+
const EXPANDED_TODO_STUB_PCT = 16
|
|
361
|
+
/** Expanded height for advanced set-types (drop / myo / cluster / range / amrap): a short mini-style bar. */
|
|
362
|
+
const EXPANDED_ENCODED_PCT = 45
|
|
363
|
+
|
|
364
|
+
/** Default framed `expanded` chart height (px). */
|
|
365
|
+
const EXPANDED_HEIGHT = 60
|
|
366
|
+
/**
|
|
367
|
+
* The `scale="fixed"` velocity ceiling (m/s). `peak` scales to the set's own max
|
|
368
|
+
* (+15% headroom); `fixed` uses this constant so a bar's height reads the same
|
|
369
|
+
* absolute velocity across sets (the spotlight).
|
|
370
|
+
*/
|
|
371
|
+
const FIXED_MAX_VELOCITY = 1.15
|
|
372
|
+
/** Minimum bare-strip bar height (px) — a planned / variable / continue stub, or a near-zero rep. */
|
|
373
|
+
const BARE_STUB_HEIGHT = 3
|
|
374
|
+
|
|
375
|
+
/** The bar-height scaling denominator for the given `scale` (guarded ≥ 0 by callers). */
|
|
376
|
+
function scaleDenominator(scale: 'peak' | 'fixed', maxVelocity: number): number {
|
|
377
|
+
return scale === 'fixed' ? FIXED_MAX_VELOCITY : maxVelocity * 1.15
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Bare-strip bar height (px): velocity-scaled for a performed rep, a short stub otherwise. */
|
|
381
|
+
function bareSlotHeight(slot: VelocitySlot, height: number, denom: number): number {
|
|
382
|
+
if (slot.kind !== 'rep' || denom <= 0) return BARE_STUB_HEIGHT
|
|
383
|
+
const ratio = Math.min(1, (slot.velocity ?? 0) / denom)
|
|
384
|
+
return Math.max(BARE_STUB_HEIGHT, ratio * height)
|
|
385
|
+
}
|
|
386
|
+
|
|
153
387
|
function getReducedMotionPreference(): boolean {
|
|
154
388
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
|
|
155
389
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
@@ -175,38 +409,64 @@ const POP_EASING = Easing.bezier(0.34, 1.56, 0.64, 1)
|
|
|
175
409
|
|
|
176
410
|
export function VelocityStrip({
|
|
177
411
|
velocities,
|
|
412
|
+
set,
|
|
178
413
|
zones,
|
|
179
414
|
liveRepIndex,
|
|
180
|
-
expanded =
|
|
415
|
+
expanded = true,
|
|
181
416
|
onToggle,
|
|
182
417
|
onRepPress,
|
|
183
|
-
variant = '
|
|
418
|
+
variant = 'expanded',
|
|
419
|
+
height = EXPANDED_HEIGHT,
|
|
420
|
+
scale = 'peak',
|
|
421
|
+
showNumbers = true,
|
|
184
422
|
showInfo = true,
|
|
185
423
|
className,
|
|
186
424
|
...props
|
|
187
425
|
}: VelocityStripProps) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
426
|
+
// A `set` descriptor derives its own done-velocity array; the legacy
|
|
427
|
+
// `velocities` path stays the source of truth otherwise. Every summary calc
|
|
428
|
+
// (mean / loss / zone) runs on this one array so the info row works either way.
|
|
429
|
+
const doneVelocities = set ? deriveDoneVelocities(set) : (velocities ?? [])
|
|
430
|
+
const slots: VelocitySlot[] = set
|
|
431
|
+
? buildSlots(set)
|
|
432
|
+
: doneVelocities.map((v, i) => ({
|
|
433
|
+
kind: 'rep',
|
|
434
|
+
velocity: v,
|
|
435
|
+
leadingGap: i === 0 ? 0 : REP_GAP,
|
|
436
|
+
}))
|
|
437
|
+
|
|
438
|
+
const maxVelocity = Math.max(...doneVelocities, 0)
|
|
439
|
+
const meanVelocity = calculateMeanVelocity(doneVelocities)
|
|
440
|
+
const loss = calculateVelocityLoss(doneVelocities)
|
|
441
|
+
|
|
442
|
+
// The framed chart (raised box, labels, info) vs the bare spotlight strip is the
|
|
443
|
+
// only fork in the `expanded` variant — keyed by whether any chrome is requested.
|
|
444
|
+
const framed = showNumbers || showInfo
|
|
445
|
+
const scaleDenom = scaleDenominator(scale, maxVelocity)
|
|
191
446
|
|
|
192
447
|
const hasZones = zones != null && zones.length > 0
|
|
193
448
|
const barColorFor = (v: number): string =>
|
|
194
449
|
hasZones ? bandColor(classifyBand(v, zones)!) : zoneHexMap[getVelocityZoneColor(v)]
|
|
450
|
+
const slotColor = (slot: VelocitySlot): string => {
|
|
451
|
+
if (slot.kind === 'rep') return barColorFor(slot.velocity ?? 0)
|
|
452
|
+
if (slot.kind === 'todo') return TODO_COLOR
|
|
453
|
+
return SET_STRIP_VARIABLE_COLOR
|
|
454
|
+
}
|
|
195
455
|
const meanZone = hasZones
|
|
196
456
|
? (classifyBand(meanVelocity, zones)?.label ?? '')
|
|
197
457
|
: getVelocityZoneName(meanVelocity)
|
|
198
458
|
|
|
199
459
|
const prefersReducedMotion = usePrefersReducedMotion()
|
|
200
|
-
const [heightAnim] = useState(() => new Animated.Value(expanded ?
|
|
460
|
+
const [heightAnim] = useState(() => new Animated.Value(expanded ? height : 3))
|
|
201
461
|
const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
202
462
|
const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
203
463
|
const [liveScale] = useState(() => new Animated.Value(1))
|
|
204
464
|
|
|
205
465
|
// Newest-rep animation: pop for a normal rep, bounce when it sets a new peak.
|
|
206
|
-
const liveVelocity = liveRepIndex != null ?
|
|
466
|
+
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : undefined
|
|
207
467
|
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity
|
|
208
468
|
useEffect(() => {
|
|
209
|
-
if (variant
|
|
469
|
+
if (variant !== 'expanded' || !framed || liveRepIndex == null || liveVelocity == null) return
|
|
210
470
|
if (prefersReducedMotion) {
|
|
211
471
|
liveScale.setValue(1)
|
|
212
472
|
return
|
|
@@ -236,14 +496,14 @@ export function VelocityStrip({
|
|
|
236
496
|
useNativeDriver: true,
|
|
237
497
|
}).start()
|
|
238
498
|
}
|
|
239
|
-
}, [variant, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale])
|
|
499
|
+
}, [variant, framed, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale])
|
|
240
500
|
|
|
241
501
|
useEffect(() => {
|
|
242
|
-
if (variant
|
|
502
|
+
if (variant !== 'expanded' || !framed) return
|
|
243
503
|
|
|
244
504
|
Animated.parallel([
|
|
245
505
|
Animated.timing(heightAnim, {
|
|
246
|
-
toValue: expanded ?
|
|
506
|
+
toValue: expanded ? height : 3,
|
|
247
507
|
duration: ANIMATION_DURATION,
|
|
248
508
|
easing: ANIMATION_EASING,
|
|
249
509
|
useNativeDriver: false,
|
|
@@ -261,42 +521,148 @@ export function VelocityStrip({
|
|
|
261
521
|
useNativeDriver: false,
|
|
262
522
|
}),
|
|
263
523
|
]).start()
|
|
264
|
-
}, [expanded, variant, heightAnim, labelOpacity, infoOpacity])
|
|
524
|
+
}, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity])
|
|
525
|
+
|
|
526
|
+
// Nothing to draw: neither a legacy velocity array nor a set descriptor.
|
|
527
|
+
if (set == null && velocities == null) return null
|
|
528
|
+
|
|
529
|
+
const repCount = doneVelocities.length
|
|
530
|
+
const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`
|
|
265
531
|
|
|
266
532
|
if (variant === 'mini') {
|
|
267
533
|
const { style: externalStyle, ...restProps } = props
|
|
534
|
+
// The container keeps the uniform REP_GAP (so a no-`set` strip is byte-identical
|
|
535
|
+
// to prior releases and holds HTML-parity); per-slot `marginLeft` carries only
|
|
536
|
+
// the EXTRA spacing for the WIDE notch that chunks drop / myo / cluster sets.
|
|
268
537
|
return (
|
|
269
538
|
<View
|
|
270
539
|
className={className}
|
|
271
|
-
style={[
|
|
540
|
+
style={[
|
|
541
|
+
{ flexDirection: 'row', height: 3, gap: REP_GAP, borderRadius: 2, overflow: 'hidden' },
|
|
542
|
+
externalStyle,
|
|
543
|
+
]}
|
|
272
544
|
accessibilityRole="image"
|
|
273
|
-
accessibilityLabel={
|
|
545
|
+
accessibilityLabel={miniLabel}
|
|
274
546
|
testID="velocity-strip-mini"
|
|
275
547
|
{...restProps}
|
|
276
548
|
>
|
|
277
|
-
{
|
|
549
|
+
{slots.map((slot, i) => (
|
|
278
550
|
<View
|
|
279
551
|
key={i}
|
|
280
552
|
style={{
|
|
281
553
|
flex: 1,
|
|
282
|
-
backgroundColor:
|
|
554
|
+
backgroundColor: slotColor(slot),
|
|
283
555
|
borderRadius: 1,
|
|
284
556
|
minWidth: 4,
|
|
285
557
|
height: '100%' as unknown as number,
|
|
558
|
+
// The container's uniform 2px gap covers rep spacing; a wide slot adds
|
|
559
|
+
// only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
|
|
560
|
+
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
561
|
+
...(slot.kind === 'continue'
|
|
562
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
563
|
+
: {}),
|
|
286
564
|
}}
|
|
287
565
|
accessibilityElementsHidden
|
|
288
|
-
testID={`velocity-bar-${i}`}
|
|
566
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
289
567
|
/>
|
|
290
568
|
))}
|
|
291
569
|
</View>
|
|
292
570
|
)
|
|
293
571
|
}
|
|
294
572
|
|
|
295
|
-
|
|
573
|
+
// Bare `expanded` strip (both chrome flags off): the velocity-HEIGHT spotlight —
|
|
574
|
+
// px bar heights, no raised box / labels / info row / animation. Set-type gaps carry
|
|
575
|
+
// over from the slot model exactly as in `mini` (container REP_GAP + per-slot extra
|
|
576
|
+
// for a WIDE notch); planned / variable / continue slots draw as stubs.
|
|
577
|
+
if (!framed) {
|
|
578
|
+
const { style: externalStyle, ...restProps } = props
|
|
579
|
+
return (
|
|
580
|
+
<View
|
|
581
|
+
className={className}
|
|
582
|
+
style={[
|
|
583
|
+
{ flexDirection: 'row', height, gap: REP_GAP, alignItems: 'flex-end' },
|
|
584
|
+
externalStyle,
|
|
585
|
+
]}
|
|
586
|
+
accessibilityRole="image"
|
|
587
|
+
accessibilityLabel={miniLabel}
|
|
588
|
+
testID="velocity-strip-compact"
|
|
589
|
+
{...restProps}
|
|
590
|
+
>
|
|
591
|
+
{slots.map((slot, i) => (
|
|
592
|
+
<View
|
|
593
|
+
key={i}
|
|
594
|
+
style={{
|
|
595
|
+
flex: 1,
|
|
596
|
+
minWidth: 4,
|
|
597
|
+
height: bareSlotHeight(slot, height, scaleDenom),
|
|
598
|
+
// Top-rounded to match the framed chart's bars (the "rounded tops from
|
|
599
|
+
// the numbers one"); square bottoms since bars sit on the baseline.
|
|
600
|
+
borderTopLeftRadius: 2,
|
|
601
|
+
borderTopRightRadius: 2,
|
|
602
|
+
backgroundColor: slotColor(slot),
|
|
603
|
+
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
604
|
+
...(slot.kind === 'continue'
|
|
605
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
606
|
+
: {}),
|
|
607
|
+
}}
|
|
608
|
+
accessibilityElementsHidden
|
|
609
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
610
|
+
/>
|
|
611
|
+
))}
|
|
612
|
+
</View>
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const stripLabel = set
|
|
617
|
+
? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? 'collapse' : 'expand'}`
|
|
618
|
+
: `Velocity chart for set, ${repCount} reps, tap to ${expanded ? 'collapse' : 'expand'}`
|
|
296
619
|
// When onToggle wraps the strip or individual reps are interactive, the container itself is not a button
|
|
297
620
|
const hasInteractiveContainer = onToggle != null
|
|
298
621
|
const hasInteractiveReps = onRepPress != null && expanded
|
|
299
622
|
|
|
623
|
+
// Expanded set-type bars: `straight` is the active-set spotlight (velocity-height
|
|
624
|
+
// done reps + short grey planned stubs); the advanced types render as a short,
|
|
625
|
+
// mini-style encoding (the gaps + colors carry identity, height is uniform).
|
|
626
|
+
const setBars =
|
|
627
|
+
set != null &&
|
|
628
|
+
slots.map((slot, i) => {
|
|
629
|
+
const isStraightRep = set.type === 'straight' && slot.kind === 'rep'
|
|
630
|
+
const heightPct = isStraightRep
|
|
631
|
+
? scaleDenom > 0
|
|
632
|
+
? Math.round(((slot.velocity ?? 0) / scaleDenom) * 100)
|
|
633
|
+
: 0
|
|
634
|
+
: set.type === 'straight'
|
|
635
|
+
? EXPANDED_TODO_STUB_PCT
|
|
636
|
+
: EXPANDED_ENCODED_PCT
|
|
637
|
+
return (
|
|
638
|
+
<View
|
|
639
|
+
key={i}
|
|
640
|
+
style={{
|
|
641
|
+
flex: 1,
|
|
642
|
+
height: '100%',
|
|
643
|
+
justifyContent: 'flex-end',
|
|
644
|
+
marginLeft: slot.leadingGap,
|
|
645
|
+
}}
|
|
646
|
+
>
|
|
647
|
+
<View
|
|
648
|
+
style={{
|
|
649
|
+
height: `${heightPct}%`,
|
|
650
|
+
minHeight: 2,
|
|
651
|
+
borderTopLeftRadius: 2,
|
|
652
|
+
borderTopRightRadius: 2,
|
|
653
|
+
backgroundColor: slotColor(slot),
|
|
654
|
+
...(slot.kind === 'continue'
|
|
655
|
+
? { borderWidth: 1, borderColor: CONTINUE_OUTLINE }
|
|
656
|
+
: {}),
|
|
657
|
+
}}
|
|
658
|
+
testID={slot.kind === 'rep' ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`}
|
|
659
|
+
accessibilityRole="image"
|
|
660
|
+
accessibilityLabel={slotAccessibilityLabel(slot, i)}
|
|
661
|
+
/>
|
|
662
|
+
</View>
|
|
663
|
+
)
|
|
664
|
+
})
|
|
665
|
+
|
|
300
666
|
const stripContent = (
|
|
301
667
|
<Animated.View
|
|
302
668
|
className={[className, expanded ? 'bg-surface-raised' : 'bg-transparent']
|
|
@@ -310,7 +676,9 @@ export function VelocityStrip({
|
|
|
310
676
|
borderRadius: expanded ? 6 : 2,
|
|
311
677
|
paddingTop: expanded ? 16 : 0,
|
|
312
678
|
paddingBottom: expanded ? 24 : 0,
|
|
313
|
-
|
|
679
|
+
// No horizontal inset — the framed chart's bars span full width, matching
|
|
680
|
+
// the bare spotlight and mini. Numbers/info sit in the vertical padding.
|
|
681
|
+
paddingHorizontal: 0,
|
|
314
682
|
paddingVertical: expanded ? undefined : 8,
|
|
315
683
|
overflow: expanded ? 'visible' : 'hidden',
|
|
316
684
|
},
|
|
@@ -320,86 +688,106 @@ export function VelocityStrip({
|
|
|
320
688
|
testID="velocity-strip"
|
|
321
689
|
{...props}
|
|
322
690
|
>
|
|
323
|
-
<View
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
691
|
+
<View style={{ flexDirection: 'row', flex: 1, gap: set ? 0 : 2, alignItems: 'flex-end' }}>
|
|
692
|
+
{set
|
|
693
|
+
? setBars
|
|
694
|
+
: doneVelocities.map((v, i) => {
|
|
695
|
+
const barBackground = barColorFor(v)
|
|
696
|
+
// Guard all-zero velocities (idle / pre-rep): a 0 denominator makes
|
|
697
|
+
// this 0 / 0 === NaN and emits height:'NaN%'. Flatten the bars instead.
|
|
698
|
+
const barHeightPct = scaleDenom > 0 ? Math.round((v / scaleDenom) * 100) : 0
|
|
699
|
+
const isLive = liveRepIndex === i
|
|
700
|
+
const liveLabelSuffix = isLive
|
|
701
|
+
? isNewPeak
|
|
702
|
+
? ', latest rep, new set peak'
|
|
703
|
+
: ', latest rep'
|
|
704
|
+
: ''
|
|
705
|
+
|
|
706
|
+
const barStyle: ViewStyle = expanded
|
|
707
|
+
? {
|
|
708
|
+
height: `${barHeightPct}%`,
|
|
709
|
+
minHeight: 2,
|
|
710
|
+
borderTopLeftRadius: 2,
|
|
711
|
+
borderTopRightRadius: 2,
|
|
712
|
+
backgroundColor: barBackground,
|
|
713
|
+
}
|
|
714
|
+
: {
|
|
715
|
+
minHeight: '100%' as unknown as number,
|
|
716
|
+
borderTopLeftRadius: 2,
|
|
717
|
+
borderTopRightRadius: 2,
|
|
718
|
+
borderBottomLeftRadius: 0,
|
|
719
|
+
borderBottomRightRadius: 0,
|
|
720
|
+
backgroundColor: barBackground,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const barInner = isLive ? (
|
|
724
|
+
<Animated.View
|
|
725
|
+
style={[barStyle, { transform: [{ scale: liveScale }] }]}
|
|
726
|
+
testID={`velocity-bar-${i}`}
|
|
727
|
+
accessibilityRole="image"
|
|
728
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`}
|
|
729
|
+
/>
|
|
730
|
+
) : (
|
|
731
|
+
<View
|
|
732
|
+
style={barStyle}
|
|
733
|
+
testID={`velocity-bar-${i}`}
|
|
734
|
+
accessibilityRole="image"
|
|
735
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second`}
|
|
736
|
+
/>
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
const bar = (
|
|
740
|
+
<View
|
|
741
|
+
key={i}
|
|
742
|
+
style={{
|
|
743
|
+
flex: 1,
|
|
744
|
+
height: '100%',
|
|
745
|
+
justifyContent: 'flex-end',
|
|
746
|
+
position: 'relative',
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
{expanded && showNumbers && (
|
|
750
|
+
<Animated.View
|
|
751
|
+
style={{
|
|
752
|
+
opacity: labelOpacity,
|
|
753
|
+
alignItems: 'center',
|
|
754
|
+
position: 'absolute',
|
|
755
|
+
top: -13,
|
|
756
|
+
left: 0,
|
|
757
|
+
right: 0,
|
|
758
|
+
}}
|
|
759
|
+
accessibilityElementsHidden
|
|
760
|
+
>
|
|
761
|
+
<Text
|
|
762
|
+
className="text-text-secondary"
|
|
763
|
+
style={{ fontSize: 8, fontWeight: '600' }}
|
|
764
|
+
testID={`velocity-label-${i}`}
|
|
765
|
+
>
|
|
766
|
+
{formatVelocity(v)}
|
|
767
|
+
</Text>
|
|
768
|
+
</Animated.View>
|
|
769
|
+
)}
|
|
770
|
+
{barInner}
|
|
771
|
+
</View>
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if (onRepPress && expanded) {
|
|
775
|
+
return (
|
|
776
|
+
<Pressable
|
|
777
|
+
key={i}
|
|
778
|
+
style={{ flex: 1, height: '100%' }}
|
|
779
|
+
onPress={() => onRepPress(i, v)}
|
|
780
|
+
accessibilityRole="button"
|
|
781
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`}
|
|
782
|
+
testID={`velocity-bar-pressable-${i}`}
|
|
783
|
+
>
|
|
784
|
+
{bar}
|
|
785
|
+
</Pressable>
|
|
786
|
+
)
|
|
348
787
|
}
|
|
349
788
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
style={[barStyle, { transform: [{ scale: liveScale }] }]}
|
|
353
|
-
testID={`velocity-bar-${i}`}
|
|
354
|
-
accessibilityRole="image"
|
|
355
|
-
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`}
|
|
356
|
-
/>
|
|
357
|
-
) : (
|
|
358
|
-
<View
|
|
359
|
-
style={barStyle}
|
|
360
|
-
testID={`velocity-bar-${i}`}
|
|
361
|
-
accessibilityRole="image"
|
|
362
|
-
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second`}
|
|
363
|
-
/>
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
const bar = (
|
|
367
|
-
<View
|
|
368
|
-
key={i}
|
|
369
|
-
style={{ flex: 1, height: '100%', justifyContent: 'flex-end', position: 'relative' }}
|
|
370
|
-
>
|
|
371
|
-
{expanded && (
|
|
372
|
-
<Animated.View style={{ opacity: labelOpacity, alignItems: 'center', position: 'absolute', top: -13, left: 0, right: 0 }} accessibilityElementsHidden>
|
|
373
|
-
<Text
|
|
374
|
-
className="text-text-secondary"
|
|
375
|
-
style={{ fontSize: 8, fontWeight: '600' }}
|
|
376
|
-
testID={`velocity-label-${i}`}
|
|
377
|
-
>
|
|
378
|
-
{formatVelocity(v)}
|
|
379
|
-
</Text>
|
|
380
|
-
</Animated.View>
|
|
381
|
-
)}
|
|
382
|
-
{barInner}
|
|
383
|
-
</View>
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
if (onRepPress && expanded) {
|
|
387
|
-
return (
|
|
388
|
-
<Pressable
|
|
389
|
-
key={i}
|
|
390
|
-
style={{ flex: 1, height: '100%' }}
|
|
391
|
-
onPress={() => onRepPress(i, v)}
|
|
392
|
-
accessibilityRole="button"
|
|
393
|
-
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`}
|
|
394
|
-
testID={`velocity-bar-pressable-${i}`}
|
|
395
|
-
>
|
|
396
|
-
{bar}
|
|
397
|
-
</Pressable>
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return bar
|
|
402
|
-
})}
|
|
789
|
+
return bar
|
|
790
|
+
})}
|
|
403
791
|
</View>
|
|
404
792
|
{expanded && showInfo && (
|
|
405
793
|
<Animated.View
|