@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,12 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
3
|
import { RestTimer } from './RestTimer'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof RestTimer> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/RestTimer',
|
|
7
7
|
component: RestTimer,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Controlled rest countdown (presentational off `useTimer`; pass `totalSeconds` + ' +
|
|
14
|
+
'`elapsedMs`). Two variants: `bar` (default) — the compact linear card (REST label + ' +
|
|
15
|
+
'mm:ss + progress bar + controls), the mobile bottom-bar treatment; and `ring` — the ' +
|
|
16
|
+
'across-the-room wall treatment: a draining countdown ring (composes `CircularProgress`) ' +
|
|
17
|
+
'with the mm:ss countdown in its center, flipping to a full `success` ring reading "GO" ' +
|
|
18
|
+
'when rest is up. `displayOnly` hides the +30s/Skip controls in either variant; `size` ' +
|
|
19
|
+
'sets the ring diameter.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
9
23
|
argTypes: {
|
|
24
|
+
variant: {
|
|
25
|
+
control: 'inline-radio',
|
|
26
|
+
options: ['bar', 'ring'],
|
|
27
|
+
description: 'bar (compact linear card) or ring (wall countdown)',
|
|
28
|
+
},
|
|
10
29
|
totalSeconds: {
|
|
11
30
|
control: 'number',
|
|
12
31
|
description: 'Total rest duration in seconds',
|
|
@@ -15,6 +34,10 @@ const meta: Meta<typeof RestTimer> = {
|
|
|
15
34
|
control: 'number',
|
|
16
35
|
description: 'Elapsed time in milliseconds',
|
|
17
36
|
},
|
|
37
|
+
size: {
|
|
38
|
+
control: { type: 'number', min: 96, max: 280, step: 4 },
|
|
39
|
+
description: 'ring diameter in px (default 180)',
|
|
40
|
+
},
|
|
18
41
|
visible: {
|
|
19
42
|
control: 'boolean',
|
|
20
43
|
description: 'Whether the timer is visible',
|
|
@@ -69,3 +92,110 @@ export const JustStarted: Story = {
|
|
|
69
92
|
visible: true,
|
|
70
93
|
},
|
|
71
94
|
}
|
|
95
|
+
|
|
96
|
+
// --- Ring variant ------------------------------------------------------------
|
|
97
|
+
// The across-the-room wall rest treatment. Rendered on the north-star wall
|
|
98
|
+
// background so the draining ring reads the way it will live.
|
|
99
|
+
|
|
100
|
+
/** Wall-background frame for the ring stories (mirrors the north-star rest page). */
|
|
101
|
+
const ringDecorator: Decorator = (Story) => (
|
|
102
|
+
<View style={{ width: 420, padding: 32, alignItems: 'center', backgroundColor: '#0E0E0E' }}>
|
|
103
|
+
<Text
|
|
104
|
+
style={{
|
|
105
|
+
color: '#5A5A5A',
|
|
106
|
+
fontSize: 10,
|
|
107
|
+
fontWeight: '700',
|
|
108
|
+
letterSpacing: 1,
|
|
109
|
+
marginBottom: 20,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
REST · UNTIL NEXT SET · (organism chrome — not the component)
|
|
113
|
+
</Text>
|
|
114
|
+
<Story />
|
|
115
|
+
</View>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/** Controls-driven ring: flip `totalSeconds` / `elapsedMs` / `size` / `displayOnly`. */
|
|
119
|
+
export const RingPlayground: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
variant: 'ring',
|
|
122
|
+
totalSeconds: 120,
|
|
123
|
+
elapsedMs: 47000,
|
|
124
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
125
|
+
onSkip: () => {},
|
|
126
|
+
onAddTime: () => {},
|
|
127
|
+
visible: true,
|
|
128
|
+
},
|
|
129
|
+
decorators: [ringDecorator],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Just started: the ring is nearly full and draining. */
|
|
133
|
+
export const RingJustStarted: Story = {
|
|
134
|
+
args: {
|
|
135
|
+
variant: 'ring',
|
|
136
|
+
totalSeconds: 120,
|
|
137
|
+
elapsedMs: 8000,
|
|
138
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
139
|
+
onSkip: () => {},
|
|
140
|
+
onAddTime: () => {},
|
|
141
|
+
visible: true,
|
|
142
|
+
},
|
|
143
|
+
decorators: [ringDecorator],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Midway: half the rest has drained. */
|
|
147
|
+
export const RingMidway: Story = {
|
|
148
|
+
args: {
|
|
149
|
+
variant: 'ring',
|
|
150
|
+
totalSeconds: 120,
|
|
151
|
+
elapsedMs: 60000,
|
|
152
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
153
|
+
onSkip: () => {},
|
|
154
|
+
onAddTime: () => {},
|
|
155
|
+
visible: true,
|
|
156
|
+
},
|
|
157
|
+
decorators: [ringDecorator],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Almost done: a sliver of ring left. */
|
|
161
|
+
export const RingAlmostDone: Story = {
|
|
162
|
+
args: {
|
|
163
|
+
variant: 'ring',
|
|
164
|
+
totalSeconds: 120,
|
|
165
|
+
elapsedMs: 112000,
|
|
166
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
167
|
+
onSkip: () => {},
|
|
168
|
+
onAddTime: () => {},
|
|
169
|
+
visible: true,
|
|
170
|
+
},
|
|
171
|
+
decorators: [ringDecorator],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Rest is up: the ring empties and flips to success (go time). */
|
|
175
|
+
export const RingDone: Story = {
|
|
176
|
+
args: {
|
|
177
|
+
variant: 'ring',
|
|
178
|
+
totalSeconds: 120,
|
|
179
|
+
elapsedMs: 120000,
|
|
180
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
181
|
+
onSkip: () => {},
|
|
182
|
+
onAddTime: () => {},
|
|
183
|
+
visible: true,
|
|
184
|
+
},
|
|
185
|
+
decorators: [ringDecorator],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Display-only ring (poll mode): the ring + next-set line, no controls. */
|
|
189
|
+
export const RingDisplayOnly: Story = {
|
|
190
|
+
args: {
|
|
191
|
+
variant: 'ring',
|
|
192
|
+
totalSeconds: 120,
|
|
193
|
+
elapsedMs: 47000,
|
|
194
|
+
nextSetInfo: 'Next · Bench Press · set 3 of 4',
|
|
195
|
+
displayOnly: true,
|
|
196
|
+
onSkip: () => {},
|
|
197
|
+
onAddTime: () => {},
|
|
198
|
+
visible: true,
|
|
199
|
+
},
|
|
200
|
+
decorators: [ringDecorator],
|
|
201
|
+
}
|
|
@@ -184,3 +184,76 @@ describe('RestTimer zero-duration timer (NaN guard)', () => {
|
|
|
184
184
|
expect(container.innerHTML).not.toContain('NaN')
|
|
185
185
|
})
|
|
186
186
|
})
|
|
187
|
+
|
|
188
|
+
describe('RestTimer ring variant', () => {
|
|
189
|
+
const ringProps = { ...defaultProps, variant: 'ring' as const }
|
|
190
|
+
|
|
191
|
+
it('renders the ring (composed CircularProgress) instead of the linear bar', () => {
|
|
192
|
+
render(<RestTimer {...ringProps} />)
|
|
193
|
+
expect(screen.getByTestId('rest-timer-ring')).toBeInTheDocument()
|
|
194
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
195
|
+
expect(screen.queryByTestId('rest-timer-progress-track')).not.toBeInTheDocument()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('shows the mm:ss countdown in the ring center while resting', () => {
|
|
199
|
+
render(<RestTimer {...ringProps} totalSeconds={120} elapsedMs={60000} />)
|
|
200
|
+
expect(screen.getByTestId('circular-timer-label')).toHaveTextContent('1:00')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('shows GO (not a time) when rest is complete', () => {
|
|
204
|
+
render(<RestTimer {...ringProps} totalSeconds={120} elapsedMs={120000} />)
|
|
205
|
+
expect(screen.getByTestId('circular-timer-label')).toHaveTextContent('GO')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('labels the completed ring as rest complete', () => {
|
|
209
|
+
render(<RestTimer {...ringProps} totalSeconds={120} elapsedMs={125000} />)
|
|
210
|
+
expect(screen.getByLabelText('Rest complete, next set ready')).toBeInTheDocument()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('labels the resting ring with seconds remaining', () => {
|
|
214
|
+
render(<RestTimer {...ringProps} totalSeconds={120} elapsedMs={60000} />)
|
|
215
|
+
expect(screen.getByLabelText('Rest timer, 60 seconds remaining')).toBeInTheDocument()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('renders the next-set line and controls by default', () => {
|
|
219
|
+
render(<RestTimer {...ringProps} nextSetInfo="Next · Bench · set 3 of 4" />)
|
|
220
|
+
expect(screen.getByTestId('rest-timer-next-set')).toHaveTextContent('Next · Bench · set 3 of 4')
|
|
221
|
+
expect(screen.getByTestId('rest-timer-add-time')).toBeInTheDocument()
|
|
222
|
+
expect(screen.getByTestId('rest-timer-skip')).toBeInTheDocument()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('hides the controls in displayOnly mode (keeps ring + next-set)', () => {
|
|
226
|
+
render(<RestTimer {...ringProps} nextSetInfo="Next" displayOnly />)
|
|
227
|
+
expect(screen.getByTestId('rest-timer-ring')).toBeInTheDocument()
|
|
228
|
+
expect(screen.getByTestId('rest-timer-next-set')).toBeInTheDocument()
|
|
229
|
+
expect(screen.queryByTestId('rest-timer-skip')).not.toBeInTheDocument()
|
|
230
|
+
expect(screen.queryByTestId('rest-timer-add-time')).not.toBeInTheDocument()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('fires onSkip / onAddTime from the ring controls', () => {
|
|
234
|
+
const onSkip = vi.fn()
|
|
235
|
+
const onAddTime = vi.fn()
|
|
236
|
+
render(<RestTimer {...ringProps} onSkip={onSkip} onAddTime={onAddTime} />)
|
|
237
|
+
fireEvent.click(screen.getByTestId('rest-timer-skip'))
|
|
238
|
+
fireEvent.click(screen.getByTestId('rest-timer-add-time'))
|
|
239
|
+
expect(onSkip).toHaveBeenCalledOnce()
|
|
240
|
+
expect(onAddTime).toHaveBeenCalledOnce()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('respects visible=false', () => {
|
|
244
|
+
render(<RestTimer {...ringProps} visible={false} />)
|
|
245
|
+
expect(screen.queryByTestId('rest-timer-ring')).not.toBeInTheDocument()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('emits no NaN when totalSeconds is 0', () => {
|
|
249
|
+
const { container } = render(<RestTimer {...ringProps} totalSeconds={0} elapsedMs={0} />)
|
|
250
|
+
expect(container.innerHTML).not.toContain('NaN')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('has no accessibility violations', async () => {
|
|
254
|
+
const { container } = render(
|
|
255
|
+
<RestTimer {...ringProps} nextSetInfo="Next · Bench · set 3 of 4" />
|
|
256
|
+
)
|
|
257
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { View, Text, Pressable } from 'react-native'
|
|
3
3
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
4
4
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { useTimer } from '../../../hooks/useTimer'
|
|
6
|
+
import { CircularTimer } from '../CircularTimer/CircularTimer'
|
|
5
7
|
|
|
6
8
|
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
7
9
|
|
|
10
|
+
/** Default `ring` diameter (px) — the across-the-room wall rest treatment. */
|
|
11
|
+
const RING_DEFAULT_SIZE = 180
|
|
12
|
+
|
|
8
13
|
export interface RestTimerProps {
|
|
9
14
|
totalSeconds: number
|
|
10
15
|
elapsedMs: number
|
|
@@ -14,6 +19,123 @@ export interface RestTimerProps {
|
|
|
14
19
|
visible: boolean
|
|
15
20
|
/** Passive/poll-only mode: hides the Skip and +30s controls, keeps the countdown, progress bar, and next-set line. */
|
|
16
21
|
displayOnly?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* `bar` (default) — the compact linear card (REST label + mm:ss + progress bar +
|
|
24
|
+
* controls), the mobile bottom-bar treatment. `ring` — the across-the-room wall
|
|
25
|
+
* treatment: a draining countdown ring (composes {@link CircularProgress}) with a
|
|
26
|
+
* big remaining-seconds numeral, the next-set line, and the same controls.
|
|
27
|
+
*/
|
|
28
|
+
variant?: 'bar' | 'ring'
|
|
29
|
+
/** `ring` diameter in px. Default 180. */
|
|
30
|
+
size?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The +30s / Skip control row — shared by both variants (hidden in `displayOnly`). */
|
|
34
|
+
function RestActions({ onAddTime, onSkip }: { onAddTime: () => void; onSkip: () => void }) {
|
|
35
|
+
return (
|
|
36
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
37
|
+
<Pressable
|
|
38
|
+
onPress={onAddTime}
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
41
|
+
paddingVertical: 8,
|
|
42
|
+
paddingHorizontal: 20,
|
|
43
|
+
borderRadius: 8,
|
|
44
|
+
}}
|
|
45
|
+
accessibilityRole="button"
|
|
46
|
+
accessibilityLabel="Add 30 seconds"
|
|
47
|
+
testID="rest-timer-add-time"
|
|
48
|
+
>
|
|
49
|
+
<Text
|
|
50
|
+
className="text-text-secondary"
|
|
51
|
+
style={{ fontSize: 11, fontFamily: 'Inter, sans-serif', fontWeight: '600' }}
|
|
52
|
+
>
|
|
53
|
+
+30s
|
|
54
|
+
</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
<Pressable
|
|
57
|
+
onPress={onSkip}
|
|
58
|
+
style={{
|
|
59
|
+
backgroundColor: 'rgba(255,121,0,0.12)',
|
|
60
|
+
paddingVertical: 8,
|
|
61
|
+
paddingHorizontal: 20,
|
|
62
|
+
borderRadius: 8,
|
|
63
|
+
}}
|
|
64
|
+
accessibilityRole="button"
|
|
65
|
+
accessibilityLabel="Skip rest"
|
|
66
|
+
testID="rest-timer-skip"
|
|
67
|
+
>
|
|
68
|
+
<Text
|
|
69
|
+
style={{
|
|
70
|
+
fontSize: 11,
|
|
71
|
+
fontFamily: 'Inter, sans-serif',
|
|
72
|
+
fontWeight: '600',
|
|
73
|
+
color: BRAND_PRIMARY,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
Skip
|
|
77
|
+
</Text>
|
|
78
|
+
</Pressable>
|
|
79
|
+
</View>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface RestTimerRingProps {
|
|
84
|
+
totalSeconds: number
|
|
85
|
+
elapsedMs: number
|
|
86
|
+
remainingSec: number
|
|
87
|
+
done: boolean
|
|
88
|
+
size: number
|
|
89
|
+
nextSetInfo?: string
|
|
90
|
+
displayOnly: boolean
|
|
91
|
+
onSkip: () => void
|
|
92
|
+
onAddTime: () => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The `ring` render: the wall rest treatment. Composes the generic {@link CircularTimer}
|
|
97
|
+
* (which owns the arc + mm:ss readout + the fill-green "GO" done state), passing the
|
|
98
|
+
* shared {@link RestActions} as its controls, and adds the rest-specific next-set footer
|
|
99
|
+
* caption below. Rest-domain chrome only — the timer mechanics live in `CircularTimer`.
|
|
100
|
+
*/
|
|
101
|
+
function RestTimerRing({
|
|
102
|
+
totalSeconds,
|
|
103
|
+
elapsedMs,
|
|
104
|
+
remainingSec,
|
|
105
|
+
done,
|
|
106
|
+
size,
|
|
107
|
+
nextSetInfo,
|
|
108
|
+
displayOnly,
|
|
109
|
+
onSkip,
|
|
110
|
+
onAddTime,
|
|
111
|
+
}: RestTimerRingProps) {
|
|
112
|
+
const a11yLabel = done
|
|
113
|
+
? 'Rest complete, next set ready'
|
|
114
|
+
: `Rest timer, ${Math.max(0, remainingSec)} seconds remaining`
|
|
115
|
+
return (
|
|
116
|
+
<View style={{ alignItems: 'center', gap: 16 }} testID="rest-timer">
|
|
117
|
+
<CircularTimer
|
|
118
|
+
durationMs={totalSeconds * 1000}
|
|
119
|
+
elapsedMs={elapsedMs}
|
|
120
|
+
mode="down"
|
|
121
|
+
size={size}
|
|
122
|
+
doneLabel="GO"
|
|
123
|
+
doneColor="success"
|
|
124
|
+
accessibilityLabel={a11yLabel}
|
|
125
|
+
controls={!displayOnly ? <RestActions onAddTime={onAddTime} onSkip={onSkip} /> : undefined}
|
|
126
|
+
testID="rest-timer-ring"
|
|
127
|
+
/>
|
|
128
|
+
{nextSetInfo != null && (
|
|
129
|
+
<Text
|
|
130
|
+
className="text-text-tertiary"
|
|
131
|
+
style={{ fontSize: 13, fontFamily: 'Inter, sans-serif' }}
|
|
132
|
+
testID="rest-timer-next-set"
|
|
133
|
+
>
|
|
134
|
+
{nextSetInfo}
|
|
135
|
+
</Text>
|
|
136
|
+
)}
|
|
137
|
+
</View>
|
|
138
|
+
)
|
|
17
139
|
}
|
|
18
140
|
|
|
19
141
|
export function RestTimer({
|
|
@@ -24,18 +146,41 @@ export function RestTimer({
|
|
|
24
146
|
nextSetInfo,
|
|
25
147
|
visible,
|
|
26
148
|
displayOnly = false,
|
|
149
|
+
variant = 'bar',
|
|
150
|
+
size = RING_DEFAULT_SIZE,
|
|
27
151
|
}: RestTimerProps) {
|
|
152
|
+
// useTimer owns the countdown math (remaining/progress/mm:ss); a zero-duration
|
|
153
|
+
// timer is complete, which keeps the width out of the 0/0 === NaN case.
|
|
154
|
+
const {
|
|
155
|
+
remainingMs,
|
|
156
|
+
progress,
|
|
157
|
+
label: timeDisplay,
|
|
158
|
+
done,
|
|
159
|
+
} = useTimer({
|
|
160
|
+
mode: 'down',
|
|
161
|
+
durationMs: totalSeconds * 1000,
|
|
162
|
+
elapsedMs,
|
|
163
|
+
})
|
|
164
|
+
const remainingSec = Math.ceil(remainingMs / 1000)
|
|
165
|
+
const progressPct = progress * 100
|
|
166
|
+
|
|
28
167
|
if (!visible) return null
|
|
29
168
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
169
|
+
if (variant === 'ring') {
|
|
170
|
+
return (
|
|
171
|
+
<RestTimerRing
|
|
172
|
+
totalSeconds={totalSeconds}
|
|
173
|
+
elapsedMs={elapsedMs}
|
|
174
|
+
remainingSec={remainingSec}
|
|
175
|
+
done={done}
|
|
176
|
+
size={size}
|
|
177
|
+
nextSetInfo={nextSetInfo}
|
|
178
|
+
displayOnly={displayOnly}
|
|
179
|
+
onSkip={onSkip}
|
|
180
|
+
onAddTime={onAddTime}
|
|
181
|
+
/>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
39
184
|
|
|
40
185
|
return (
|
|
41
186
|
<View
|
|
@@ -128,56 +273,7 @@ export function RestTimer({
|
|
|
128
273
|
</View>
|
|
129
274
|
|
|
130
275
|
{/* Actions row — hidden in display-only (poll-only) mode */}
|
|
131
|
-
{!displayOnly &&
|
|
132
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
133
|
-
<Pressable
|
|
134
|
-
onPress={onAddTime}
|
|
135
|
-
style={{
|
|
136
|
-
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
137
|
-
paddingVertical: 8,
|
|
138
|
-
paddingHorizontal: 20,
|
|
139
|
-
borderRadius: 8,
|
|
140
|
-
}}
|
|
141
|
-
accessibilityRole="button"
|
|
142
|
-
accessibilityLabel="Add 30 seconds"
|
|
143
|
-
testID="rest-timer-add-time"
|
|
144
|
-
>
|
|
145
|
-
<Text
|
|
146
|
-
className="text-text-secondary"
|
|
147
|
-
style={{
|
|
148
|
-
fontSize: 11,
|
|
149
|
-
fontFamily: 'Inter, sans-serif',
|
|
150
|
-
fontWeight: '600',
|
|
151
|
-
}}
|
|
152
|
-
>
|
|
153
|
-
+30s
|
|
154
|
-
</Text>
|
|
155
|
-
</Pressable>
|
|
156
|
-
<Pressable
|
|
157
|
-
onPress={onSkip}
|
|
158
|
-
style={{
|
|
159
|
-
backgroundColor: 'rgba(255,121,0,0.12)',
|
|
160
|
-
paddingVertical: 8,
|
|
161
|
-
paddingHorizontal: 20,
|
|
162
|
-
borderRadius: 8,
|
|
163
|
-
}}
|
|
164
|
-
accessibilityRole="button"
|
|
165
|
-
accessibilityLabel="Skip rest"
|
|
166
|
-
testID="rest-timer-skip"
|
|
167
|
-
>
|
|
168
|
-
<Text
|
|
169
|
-
style={{
|
|
170
|
-
fontSize: 11,
|
|
171
|
-
fontFamily: 'Inter, sans-serif',
|
|
172
|
-
fontWeight: '600',
|
|
173
|
-
color: BRAND_PRIMARY,
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
176
|
-
Skip
|
|
177
|
-
</Text>
|
|
178
|
-
</Pressable>
|
|
179
|
-
</View>
|
|
180
|
-
)}
|
|
276
|
+
{!displayOnly && <RestActions onAddTime={onAddTime} onSkip={onSkip} />}
|
|
181
277
|
</View>
|
|
182
278
|
)
|
|
183
279
|
}
|