@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { alpha } from '../../../utils/colors'
|
|
5
|
+
import { StatusDot, type StatusDotVariant } from './StatusDot'
|
|
6
|
+
|
|
7
|
+
const t = getSemanticColors('dark')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Auto-regulation verdict — the human-readable read-once summary of a set's
|
|
11
|
+
* velocity-loss state.
|
|
12
|
+
* - `productive` — in the band, keep going (success)
|
|
13
|
+
* - `threshold` — VL20 reached, approaching fatigue (warning)
|
|
14
|
+
* - `stop` — VL28+, terminate the set (error)
|
|
15
|
+
*/
|
|
16
|
+
export type StatusPillStatus = 'productive' | 'threshold' | 'stop'
|
|
17
|
+
|
|
18
|
+
export interface StatusPillProps extends ViewProps {
|
|
19
|
+
status: StatusPillStatus
|
|
20
|
+
/** Overrides the default verdict text for the status. */
|
|
21
|
+
label?: string
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const statusToken: Record<StatusPillStatus, 'status-success' | 'status-warning' | 'status-error'> =
|
|
26
|
+
{
|
|
27
|
+
productive: 'status-success',
|
|
28
|
+
threshold: 'status-warning',
|
|
29
|
+
stop: 'status-error',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const statusDotVariant: Record<StatusPillStatus, StatusDotVariant> = {
|
|
33
|
+
productive: 'success',
|
|
34
|
+
threshold: 'warning',
|
|
35
|
+
stop: 'error',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultLabel: Record<StatusPillStatus, string> = {
|
|
39
|
+
productive: 'Productive',
|
|
40
|
+
threshold: 'Threshold',
|
|
41
|
+
stop: 'Stop',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolved semantic color for a verdict status. */
|
|
45
|
+
export function statusPillColor(status: StatusPillStatus): string {
|
|
46
|
+
return t[statusToken[status]]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verdict pill: a glowing status dot + colored verdict text, in a tinted,
|
|
51
|
+
* bordered capsule. Composes {@link StatusDot} for the dot so the glow/color
|
|
52
|
+
* stays consistent with the atom. Tint and border are derived from the same
|
|
53
|
+
* semantic token via `alpha()` — no per-status raw hex.
|
|
54
|
+
*/
|
|
55
|
+
export function StatusPill({ status, label, className, style, ...props }: StatusPillProps) {
|
|
56
|
+
const color = statusPillColor(status)
|
|
57
|
+
const text = label ?? defaultLabel[status]
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View
|
|
61
|
+
testID="status-pill"
|
|
62
|
+
className={className}
|
|
63
|
+
style={[
|
|
64
|
+
{
|
|
65
|
+
flexDirection: 'row',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: 7,
|
|
68
|
+
alignSelf: 'flex-start',
|
|
69
|
+
paddingVertical: 7,
|
|
70
|
+
paddingHorizontal: 13,
|
|
71
|
+
borderRadius: 9,
|
|
72
|
+
backgroundColor: alpha(color, 0.14),
|
|
73
|
+
borderWidth: 1,
|
|
74
|
+
borderColor: alpha(color, 0.4),
|
|
75
|
+
},
|
|
76
|
+
style,
|
|
77
|
+
]}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<View accessibilityElementsHidden>
|
|
81
|
+
<StatusDot variant={statusDotVariant[status]} glow />
|
|
82
|
+
</View>
|
|
83
|
+
<Text testID="status-pill-label" style={{ fontSize: 13, fontWeight: '800', color }}>
|
|
84
|
+
{text}
|
|
85
|
+
</Text>
|
|
86
|
+
</View>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -32,7 +32,7 @@ const decline: StrengthTrendDataPoint[] = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
const meta: Meta<typeof StrengthTrendChart> = {
|
|
35
|
-
title: '
|
|
35
|
+
title: 'Workout/DataViz/StrengthTrendChart',
|
|
36
36
|
component: StrengthTrendChart,
|
|
37
37
|
tags: ['autodocs'],
|
|
38
38
|
argTypes: {
|
|
@@ -4,7 +4,7 @@ import { SupersetWrapper } from './SupersetWrapper'
|
|
|
4
4
|
import { ExerciseCard } from './ExerciseCard'
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof SupersetWrapper> = {
|
|
7
|
-
title: '
|
|
7
|
+
title: 'Workout/SupersetWrapper',
|
|
8
8
|
component: SupersetWrapper,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
10
|
decorators: [
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native'
|
|
|
3
3
|
import { TempoBar } from './TempoBar'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof TempoBar> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/TempoBar',
|
|
7
7
|
component: TempoBar,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -15,6 +15,16 @@ const meta: Meta<typeof TempoBar> = {
|
|
|
15
15
|
phaseElapsedMs: { control: 'number', description: 'Elapsed ms in the active phase' },
|
|
16
16
|
completed: { control: 'object', description: 'Completed phase durations (ms)' },
|
|
17
17
|
target: { control: 'object', description: 'Per-phase target durations (seconds)' },
|
|
18
|
+
size: {
|
|
19
|
+
control: 'inline-radio',
|
|
20
|
+
options: ['default', 'wall'],
|
|
21
|
+
description: 'Density: default (compact) or wall (across-the-room dashboard scale)',
|
|
22
|
+
},
|
|
23
|
+
activeDisplay: {
|
|
24
|
+
control: 'inline-radio',
|
|
25
|
+
options: ['time', 'delta'],
|
|
26
|
+
description: 'Active readout: time (elapsed/target) or delta (countdown). Tap to toggle.',
|
|
27
|
+
},
|
|
18
28
|
},
|
|
19
29
|
decorators: [
|
|
20
30
|
(Story) => (
|
|
@@ -93,3 +103,62 @@ export const AllStates: Story = {
|
|
|
93
103
|
</View>
|
|
94
104
|
),
|
|
95
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The across-the-room wall scale — bigger bar, full phase words, and colour-coded
|
|
109
|
+
* pacing (a missed target reads red). Here the eccentric ran long → red.
|
|
110
|
+
*/
|
|
111
|
+
export const Wall: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
activePhase: null,
|
|
114
|
+
phaseElapsedMs: 0,
|
|
115
|
+
completed: { concentric: 950, hold: 1000, eccentric: 4200 },
|
|
116
|
+
target: TARGET,
|
|
117
|
+
size: 'wall',
|
|
118
|
+
},
|
|
119
|
+
decorators: [
|
|
120
|
+
(Story) => (
|
|
121
|
+
<View style={{ width: 460, padding: 28, backgroundColor: '#0E0E0E' }}>
|
|
122
|
+
<Story />
|
|
123
|
+
</View>
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Wall, mid-rep — the eccentric is active, showing the delta countdown (1.5s to target). Tap to switch to elapsed/target. */
|
|
129
|
+
export const WallActiveDelta: Story = {
|
|
130
|
+
args: {
|
|
131
|
+
activePhase: 'eccentric',
|
|
132
|
+
phaseElapsedMs: 1500,
|
|
133
|
+
completed: { concentric: 950, hold: 1000 },
|
|
134
|
+
target: TARGET,
|
|
135
|
+
size: 'wall',
|
|
136
|
+
activeDisplay: 'delta',
|
|
137
|
+
},
|
|
138
|
+
decorators: [
|
|
139
|
+
(Story) => (
|
|
140
|
+
<View style={{ width: 460, padding: 28, backgroundColor: '#0E0E0E' }}>
|
|
141
|
+
<Story />
|
|
142
|
+
</View>
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Wall, over target — the eccentric ran 1.2s past its target, so the delta is negative and red. */
|
|
148
|
+
export const WallActiveOver: Story = {
|
|
149
|
+
args: {
|
|
150
|
+
activePhase: 'eccentric',
|
|
151
|
+
phaseElapsedMs: 4200,
|
|
152
|
+
completed: { concentric: 950, hold: 1000 },
|
|
153
|
+
target: TARGET,
|
|
154
|
+
size: 'wall',
|
|
155
|
+
activeDisplay: 'delta',
|
|
156
|
+
},
|
|
157
|
+
decorators: [
|
|
158
|
+
(Story) => (
|
|
159
|
+
<View style={{ width: 460, padding: 28, backgroundColor: '#0E0E0E' }}>
|
|
160
|
+
<Story />
|
|
161
|
+
</View>
|
|
162
|
+
),
|
|
163
|
+
],
|
|
164
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
3
|
import { axe } from 'jest-axe'
|
|
4
4
|
import {
|
|
5
5
|
TempoBar,
|
|
6
6
|
getTempoPacingState,
|
|
7
7
|
getTempoFillPct,
|
|
8
|
+
formatTempoDelta,
|
|
8
9
|
TEMPO_PACING,
|
|
9
10
|
} from './TempoBar'
|
|
11
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
10
12
|
|
|
13
|
+
const t = getSemanticColors('dark')
|
|
11
14
|
const TARGET = { concentric: 1, hold: 1, eccentric: 3 }
|
|
12
15
|
|
|
13
16
|
describe('getTempoPacingState', () => {
|
|
@@ -70,7 +73,7 @@ describe('TempoBar', () => {
|
|
|
70
73
|
phaseElapsedMs={100}
|
|
71
74
|
completed={{ concentric: 950 }}
|
|
72
75
|
target={TARGET}
|
|
73
|
-
|
|
76
|
+
/>
|
|
74
77
|
)
|
|
75
78
|
expect(screen.getByTestId('tempo-segment-completed-Con')).toBeInTheDocument()
|
|
76
79
|
expect(screen.getByText('0.9 ✓')).toBeInTheDocument()
|
|
@@ -83,7 +86,7 @@ describe('TempoBar', () => {
|
|
|
83
86
|
phaseElapsedMs={100}
|
|
84
87
|
completed={{ eccentric: 4000 }}
|
|
85
88
|
target={TARGET}
|
|
86
|
-
|
|
89
|
+
/>
|
|
87
90
|
)
|
|
88
91
|
// eccentric target 3s, 4s elapsed => behind => cross
|
|
89
92
|
expect(screen.getByText('4.0 ✗')).toBeInTheDocument()
|
|
@@ -96,8 +99,120 @@ describe('TempoBar', () => {
|
|
|
96
99
|
phaseElapsedMs={1500}
|
|
97
100
|
completed={{ concentric: 900, hold: 1000 }}
|
|
98
101
|
target={TARGET}
|
|
99
|
-
|
|
102
|
+
/>
|
|
100
103
|
)
|
|
101
104
|
expect(await axe(container)).toHaveNoViolations()
|
|
102
105
|
})
|
|
103
106
|
})
|
|
107
|
+
|
|
108
|
+
describe('TempoBar size="wall"', () => {
|
|
109
|
+
const TARGET_W = { concentric: 1, hold: 1, eccentric: 3 }
|
|
110
|
+
|
|
111
|
+
it('scales the active label up at wall density', () => {
|
|
112
|
+
render(<TempoBar activePhase="concentric" phaseElapsedMs={600} target={TARGET_W} size="wall" />)
|
|
113
|
+
expect(screen.getByText('0.6 / 1.0')).toHaveStyle({ fontSize: 22 })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps the compact label size by default', () => {
|
|
117
|
+
render(<TempoBar activePhase="concentric" phaseElapsedMs={600} target={TARGET_W} />)
|
|
118
|
+
expect(screen.getByText('0.6 / 1.0')).toHaveStyle({ fontSize: 12 })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('preserves the segment testIDs at wall density', () => {
|
|
122
|
+
render(
|
|
123
|
+
<TempoBar
|
|
124
|
+
activePhase="eccentric"
|
|
125
|
+
phaseElapsedMs={1500}
|
|
126
|
+
completed={{ concentric: 900 }}
|
|
127
|
+
target={TARGET_W}
|
|
128
|
+
size="wall"
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
expect(screen.getByTestId('tempo-segment-active-Ecc')).toBeInTheDocument()
|
|
132
|
+
expect(screen.getByTestId('tempo-segment-completed-Con')).toBeInTheDocument()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('spells out the full phase words at wall density', () => {
|
|
136
|
+
render(<TempoBar activePhase={null} phaseElapsedMs={0} target={TARGET_W} size="wall" />)
|
|
137
|
+
expect(screen.getByText('Concentric')).toBeInTheDocument()
|
|
138
|
+
expect(screen.getByText('Eccentric')).toBeInTheDocument()
|
|
139
|
+
expect(screen.queryByText('Con')).not.toBeInTheDocument()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('formatTempoDelta', () => {
|
|
144
|
+
it('shows remaining time to target as a positive value', () => {
|
|
145
|
+
expect(formatTempoDelta(1500)).toBe('1.5')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('shows over-target time as negative', () => {
|
|
149
|
+
expect(formatTempoDelta(-1200)).toBe('-1.2')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('shows 0.0 at the target (no -0.0)', () => {
|
|
153
|
+
expect(formatTempoDelta(0)).toBe('0.0')
|
|
154
|
+
expect(formatTempoDelta(-10)).toBe('0.0')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('TempoBar activeDisplay (delta countdown + toggle)', () => {
|
|
159
|
+
it('counts the remaining time down to the target in delta mode', () => {
|
|
160
|
+
render(
|
|
161
|
+
<TempoBar
|
|
162
|
+
activePhase="eccentric"
|
|
163
|
+
phaseElapsedMs={1800}
|
|
164
|
+
target={TARGET}
|
|
165
|
+
activeDisplay="delta"
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
// eccentric target 3.0s, elapsed 1.8s → 1.2s remaining
|
|
169
|
+
expect(screen.getByText('1.2')).toBeInTheDocument()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('goes negative once the phase runs past its target', () => {
|
|
173
|
+
render(
|
|
174
|
+
<TempoBar
|
|
175
|
+
activePhase="eccentric"
|
|
176
|
+
phaseElapsedMs={4200}
|
|
177
|
+
target={TARGET}
|
|
178
|
+
activeDisplay="delta"
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
expect(screen.getByText('-1.2')).toBeInTheDocument()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('tapping the active segment toggles time ↔ delta', () => {
|
|
185
|
+
render(
|
|
186
|
+
<TempoBar
|
|
187
|
+
activePhase="concentric"
|
|
188
|
+
phaseElapsedMs={600}
|
|
189
|
+
target={TARGET}
|
|
190
|
+
activeDisplay="time"
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
expect(screen.getByText('0.6 / 1.0')).toBeInTheDocument()
|
|
194
|
+
fireEvent.click(screen.getByTestId('tempo-segment-active-Con'))
|
|
195
|
+
// concentric target 1.0s, elapsed 0.6s → 0.4s remaining
|
|
196
|
+
expect(screen.getByText('0.4')).toBeInTheDocument()
|
|
197
|
+
expect(screen.queryByText('0.6 / 1.0')).not.toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('defaults to the time readout', () => {
|
|
201
|
+
render(<TempoBar activePhase="concentric" phaseElapsedMs={600} target={TARGET} />)
|
|
202
|
+
expect(screen.getByText('0.6 / 1.0')).toBeInTheDocument()
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('TempoBar completed pacing colour', () => {
|
|
207
|
+
it('colours a missed completed phase red', () => {
|
|
208
|
+
render(
|
|
209
|
+
<TempoBar
|
|
210
|
+
activePhase="hold"
|
|
211
|
+
phaseElapsedMs={100}
|
|
212
|
+
completed={{ eccentric: 4000 }}
|
|
213
|
+
target={TARGET}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
expect(screen.getByText('4.0 ✗')).toHaveStyle({ color: t['status-error'] })
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
4
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
5
|
import { alpha } from '../../../utils/colors'
|
|
5
6
|
|
|
@@ -19,15 +20,23 @@ export const TEMPO_PACING = {
|
|
|
19
20
|
|
|
20
21
|
export type TempoPacingState = 'none' | 'on-pace' | 'behind'
|
|
21
22
|
|
|
23
|
+
/** Active-phase readout mode: elapsed/target `time`, or a `delta` countdown to the target. */
|
|
24
|
+
export type TempoActiveDisplay = 'time' | 'delta'
|
|
25
|
+
|
|
26
|
+
/** Signed seconds to one decimal — the delta countdown (`+` remaining, `-` over target). */
|
|
27
|
+
export function formatTempoDelta(remainingMs: number): string {
|
|
28
|
+
const s = remainingMs / 1000
|
|
29
|
+
// Guard `-0.0`; show a leading minus only once past target.
|
|
30
|
+
const rounded = Math.abs(s) < 0.05 ? 0 : s
|
|
31
|
+
return rounded.toFixed(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* Classify how a phase's elapsed time tracks against its target duration.
|
|
24
36
|
* `none` when there is no meaningful target; `behind` once elapsed exceeds the
|
|
25
37
|
* target by more than the behind threshold; `on-pace` otherwise.
|
|
26
38
|
*/
|
|
27
|
-
export function getTempoPacingState(
|
|
28
|
-
elapsedMs: number,
|
|
29
|
-
targetMs: number | null,
|
|
30
|
-
): TempoPacingState {
|
|
39
|
+
export function getTempoPacingState(elapsedMs: number, targetMs: number | null): TempoPacingState {
|
|
31
40
|
if (!targetMs || targetMs < TEMPO_PACING.minPhaseDurationMs) return 'none'
|
|
32
41
|
const ratio = elapsedMs / targetMs
|
|
33
42
|
if (ratio > 1 + TEMPO_PACING.behindThresholdPct) return 'behind'
|
|
@@ -41,7 +50,10 @@ export function getTempoFillPct(elapsedMs: number, targetMs: number | null): num
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface PhaseConfig {
|
|
53
|
+
/** Compact label + the stable testID suffix. */
|
|
44
54
|
label: string
|
|
55
|
+
/** Full phase word, spelled out at `wall` density where there's room to read it. */
|
|
56
|
+
wallLabel: string
|
|
45
57
|
color: string
|
|
46
58
|
flex: number
|
|
47
59
|
}
|
|
@@ -49,9 +61,27 @@ interface PhaseConfig {
|
|
|
49
61
|
const PHASE_ORDER: TempoPhaseKey[] = ['concentric', 'hold', 'eccentric']
|
|
50
62
|
|
|
51
63
|
const PHASE_CONFIG: Record<TempoPhaseKey, PhaseConfig> = {
|
|
52
|
-
concentric: { label: 'Con', color: t['status-success'], flex: 2 },
|
|
53
|
-
hold: { label: 'Hold', color: t['brand-primary'], flex: 1 },
|
|
54
|
-
eccentric: { label: 'Ecc', color: t['status-warning'], flex: 3 },
|
|
64
|
+
concentric: { label: 'Con', wallLabel: 'Concentric', color: t['status-success'], flex: 2 },
|
|
65
|
+
hold: { label: 'Hold', wallLabel: 'Hold', color: t['brand-primary'], flex: 1 },
|
|
66
|
+
eccentric: { label: 'Ecc', wallLabel: 'Eccentric', color: t['status-warning'], flex: 3 },
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Density. `default` — the compact card/rail treatment. `wall` — the across-the-room dashboard scale. */
|
|
70
|
+
export type TempoBarSize = 'default' | 'wall'
|
|
71
|
+
|
|
72
|
+
interface TempoBarSizing {
|
|
73
|
+
labelMargin: number
|
|
74
|
+
labelFont: number
|
|
75
|
+
barGap: number
|
|
76
|
+
barHeight: number
|
|
77
|
+
radius: number
|
|
78
|
+
segFont: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Per-density magnitudes. `default` reproduces the original compact values exactly. */
|
|
82
|
+
const TEMPO_SIZES: Record<TempoBarSize, TempoBarSizing> = {
|
|
83
|
+
default: { labelMargin: 4, labelFont: 10, barGap: 2, barHeight: 20, radius: 4, segFont: 12 },
|
|
84
|
+
wall: { labelMargin: 8, labelFont: 15, barGap: 4, barHeight: 40, radius: 6, segFont: 22 },
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
export interface TempoBarProps extends ViewProps {
|
|
@@ -63,6 +93,14 @@ export interface TempoBarProps extends ViewProps {
|
|
|
63
93
|
completed?: Partial<Record<TempoPhaseKey, number>>
|
|
64
94
|
/** Optional per-phase target durations (seconds) for pacing feedback. */
|
|
65
95
|
target?: Partial<Record<TempoPhaseKey, number>>
|
|
96
|
+
/** Density: `default` (compact) or `wall` (the across-the-room dashboard scale). */
|
|
97
|
+
size?: TempoBarSize
|
|
98
|
+
/**
|
|
99
|
+
* Active-phase readout. `time` (default) — `elapsed / target`. `delta` — a countdown
|
|
100
|
+
* of the remaining time to target (`1.5` → `0.0` → `-1.2` once over), colour-coded like
|
|
101
|
+
* the bar. Tapping the active segment toggles between the two at runtime.
|
|
102
|
+
*/
|
|
103
|
+
activeDisplay?: TempoActiveDisplay
|
|
66
104
|
className?: string
|
|
67
105
|
}
|
|
68
106
|
|
|
@@ -79,25 +117,33 @@ export function TempoBar({
|
|
|
79
117
|
phaseElapsedMs,
|
|
80
118
|
completed,
|
|
81
119
|
target,
|
|
120
|
+
size = 'default',
|
|
121
|
+
activeDisplay = 'time',
|
|
82
122
|
className,
|
|
83
123
|
...props
|
|
84
124
|
}: TempoBarProps) {
|
|
125
|
+
const s = TEMPO_SIZES[size]
|
|
126
|
+
// Runtime toggle of the active readout, seeded from `activeDisplay`.
|
|
127
|
+
const [display, setDisplay] = useState<TempoActiveDisplay>(activeDisplay)
|
|
128
|
+
const toggleDisplay = () => setDisplay((d) => (d === 'time' ? 'delta' : 'time'))
|
|
85
129
|
return (
|
|
86
130
|
<View className={className} testID="tempo-bar" {...props}>
|
|
87
131
|
{/* Phase labels */}
|
|
88
|
-
<View style={{ flexDirection: 'row', marginBottom:
|
|
132
|
+
<View style={{ flexDirection: 'row', marginBottom: s.labelMargin }}>
|
|
89
133
|
{PHASE_ORDER.map((phase) => {
|
|
90
134
|
const config = PHASE_CONFIG[phase]
|
|
91
135
|
return (
|
|
92
136
|
<View key={phase} style={{ flex: config.flex, alignItems: 'center' }}>
|
|
93
|
-
<Text style={{ fontSize:
|
|
137
|
+
<Text numberOfLines={1} style={{ fontSize: s.labelFont, color: t['text-disabled'] }}>
|
|
138
|
+
{size === 'wall' ? config.wallLabel : config.label}
|
|
139
|
+
</Text>
|
|
94
140
|
</View>
|
|
95
141
|
)
|
|
96
142
|
})}
|
|
97
143
|
</View>
|
|
98
144
|
|
|
99
145
|
{/* Segmented bar */}
|
|
100
|
-
<View style={{ flexDirection: 'row', gap:
|
|
146
|
+
<View style={{ flexDirection: 'row', gap: s.barGap, height: s.barHeight }}>
|
|
101
147
|
{PHASE_ORDER.map((phase) => {
|
|
102
148
|
const config = PHASE_CONFIG[phase]
|
|
103
149
|
const isActive = activePhase === phase
|
|
@@ -111,7 +157,7 @@ export function TempoBar({
|
|
|
111
157
|
style={{
|
|
112
158
|
flex: config.flex,
|
|
113
159
|
backgroundColor: alpha('#ffffff', 0.06),
|
|
114
|
-
borderRadius:
|
|
160
|
+
borderRadius: s.radius,
|
|
115
161
|
overflow: 'hidden',
|
|
116
162
|
}}
|
|
117
163
|
>
|
|
@@ -120,9 +166,18 @@ export function TempoBar({
|
|
|
120
166
|
config={config}
|
|
121
167
|
phaseElapsedMs={phaseElapsedMs}
|
|
122
168
|
targetMs={targetMs}
|
|
169
|
+
radius={s.radius}
|
|
170
|
+
font={s.segFont}
|
|
171
|
+
display={display}
|
|
172
|
+
onToggleDisplay={toggleDisplay}
|
|
123
173
|
/>
|
|
124
174
|
) : completedMs != null ? (
|
|
125
|
-
<CompletedSegment
|
|
175
|
+
<CompletedSegment
|
|
176
|
+
config={config}
|
|
177
|
+
completedMs={completedMs}
|
|
178
|
+
targetMs={targetMs}
|
|
179
|
+
font={s.segFont}
|
|
180
|
+
/>
|
|
126
181
|
) : null}
|
|
127
182
|
</View>
|
|
128
183
|
)
|
|
@@ -136,21 +191,40 @@ function ActiveSegment({
|
|
|
136
191
|
config,
|
|
137
192
|
phaseElapsedMs,
|
|
138
193
|
targetMs,
|
|
194
|
+
radius,
|
|
195
|
+
font,
|
|
196
|
+
display,
|
|
197
|
+
onToggleDisplay,
|
|
139
198
|
}: {
|
|
140
199
|
config: PhaseConfig
|
|
141
200
|
phaseElapsedMs: number
|
|
142
201
|
targetMs: number | null
|
|
202
|
+
radius: number
|
|
203
|
+
font: number
|
|
204
|
+
display: TempoActiveDisplay
|
|
205
|
+
onToggleDisplay: () => void
|
|
143
206
|
}) {
|
|
144
207
|
const pacing = getTempoPacingState(phaseElapsedMs, targetMs)
|
|
145
208
|
const barColor = pacing === 'behind' ? t['status-error'] : config.color
|
|
146
209
|
const fillPct = getTempoFillPct(phaseElapsedMs, targetMs)
|
|
147
210
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
211
|
+
// `delta`: count the remaining time to target down to 0.0, then negative once over.
|
|
212
|
+
// Falls back to elapsed when there's no target. `time`: the elapsed / target readout.
|
|
213
|
+
const label =
|
|
214
|
+
display === 'delta' && targetMs != null
|
|
215
|
+
? formatTempoDelta(targetMs - phaseElapsedMs)
|
|
216
|
+
: targetMs != null
|
|
217
|
+
? `${formatDuration(phaseElapsedMs)} / ${formatDuration(targetMs)}`
|
|
218
|
+
: formatDuration(phaseElapsedMs)
|
|
151
219
|
|
|
152
220
|
return (
|
|
153
|
-
<
|
|
221
|
+
<Pressable
|
|
222
|
+
onPress={onToggleDisplay}
|
|
223
|
+
style={{ height: '100%', position: 'relative' }}
|
|
224
|
+
accessibilityRole="button"
|
|
225
|
+
accessibilityLabel={`Tempo readout: ${display === 'delta' ? 'delta' : 'time'} — tap to switch`}
|
|
226
|
+
testID={`tempo-segment-active-${config.label}`}
|
|
227
|
+
>
|
|
154
228
|
<View
|
|
155
229
|
style={{
|
|
156
230
|
position: 'absolute',
|
|
@@ -159,7 +233,7 @@ function ActiveSegment({
|
|
|
159
233
|
bottom: 0,
|
|
160
234
|
width: `${fillPct}%`,
|
|
161
235
|
backgroundColor: alpha(barColor, 0.3),
|
|
162
|
-
borderRadius:
|
|
236
|
+
borderRadius: radius,
|
|
163
237
|
}}
|
|
164
238
|
/>
|
|
165
239
|
<View
|
|
@@ -170,9 +244,9 @@ function ActiveSegment({
|
|
|
170
244
|
justifyContent: 'center',
|
|
171
245
|
}}
|
|
172
246
|
>
|
|
173
|
-
<Text style={{ fontSize:
|
|
247
|
+
<Text style={{ fontSize: font, fontWeight: '700', color: barColor }}>{label}</Text>
|
|
174
248
|
</View>
|
|
175
|
-
</
|
|
249
|
+
</Pressable>
|
|
176
250
|
)
|
|
177
251
|
}
|
|
178
252
|
|
|
@@ -180,15 +254,20 @@ function CompletedSegment({
|
|
|
180
254
|
config,
|
|
181
255
|
completedMs,
|
|
182
256
|
targetMs,
|
|
257
|
+
font,
|
|
183
258
|
}: {
|
|
184
259
|
config: PhaseConfig
|
|
185
260
|
completedMs: number
|
|
186
261
|
targetMs: number | null
|
|
262
|
+
font: number
|
|
187
263
|
}) {
|
|
188
264
|
const pacing = getTempoPacingState(completedMs, targetMs)
|
|
189
265
|
const hitTarget = pacing !== 'behind'
|
|
190
266
|
const indicator =
|
|
191
267
|
targetMs && targetMs >= TEMPO_PACING.minPhaseDurationMs ? (hitTarget ? ' ✓' : ' ✗') : ''
|
|
268
|
+
// A missed target reads by COLOUR first (red fill + red duration), the ✓/✗ second —
|
|
269
|
+
// glanceable across a room; a hit keeps the phase's own colour.
|
|
270
|
+
const cueColor = hitTarget ? config.color : t['status-error']
|
|
192
271
|
|
|
193
272
|
return (
|
|
194
273
|
<View
|
|
@@ -197,11 +276,17 @@ function CompletedSegment({
|
|
|
197
276
|
flexDirection: 'row',
|
|
198
277
|
alignItems: 'center',
|
|
199
278
|
justifyContent: 'center',
|
|
200
|
-
backgroundColor: alpha(
|
|
279
|
+
backgroundColor: alpha(cueColor, 0.15),
|
|
201
280
|
}}
|
|
202
281
|
testID={`tempo-segment-completed-${config.label}`}
|
|
203
282
|
>
|
|
204
|
-
<Text
|
|
283
|
+
<Text
|
|
284
|
+
style={{
|
|
285
|
+
fontSize: font,
|
|
286
|
+
fontWeight: '500',
|
|
287
|
+
color: hitTarget ? alpha(config.color, 0.7) : cueColor,
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
205
290
|
{formatDuration(completedMs)}
|
|
206
291
|
{indicator}
|
|
207
292
|
</Text>
|
|
@@ -5,9 +5,20 @@ import { TempoDisplay, type TempoLivePhase, type TempoLiveState } from './TempoD
|
|
|
5
5
|
|
|
6
6
|
// tempo = [eccentric, pauseBottom, concentric, pauseTop]
|
|
7
7
|
const meta: Meta<typeof TempoDisplay> = {
|
|
8
|
-
title: '
|
|
8
|
+
title: 'Workout/TempoDisplay',
|
|
9
9
|
component: TempoDisplay,
|
|
10
10
|
tags: ['autodocs'],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'**Molecule.** The eccentric/pause/concentric/pause tempo display. Composes ' +
|
|
16
|
+
'[MetricCell](?path=/docs/workout-metriccell--docs) ' +
|
|
17
|
+
'(shared value/separator cell). Used-by ↑ ' +
|
|
18
|
+
'[ExerciseHeading](?path=/docs/workout-exerciseheading--docs) (`showLabel={false}`).',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
11
22
|
argTypes: {
|
|
12
23
|
tempo: {
|
|
13
24
|
control: 'object',
|