@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.
Files changed (258) hide show
  1. package/dist/bodymap.js +12 -7
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +12 -7
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +956 -128
  6. package/dist/index.d.ts +956 -128
  7. package/dist/index.js +2873 -981
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2826 -982
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1659 -799
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1660 -800
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +16 -82
  20. package/dist/theme/index.d.ts +16 -82
  21. package/dist/theme/index.js +13 -115
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +13 -115
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +14 -6
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +14 -6
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/arch/Architecture.stories.tsx +870 -0
  31. package/src/arch/annotations.json +48 -0
  32. package/src/arch/arch-graph.json +4301 -0
  33. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +90 -0
  34. package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
  35. package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
  36. package/src/components/custom/CircularTimer/index.ts +1 -0
  37. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  38. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  39. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  40. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  41. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  42. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  43. package/src/components/custom/Table/Table.stories.tsx +1 -1
  44. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  45. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  46. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  47. package/src/components/custom/TimerReadout/index.ts +1 -0
  48. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  49. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  54. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  55. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  56. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  57. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  58. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  59. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  60. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  61. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  62. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  63. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  64. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  65. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  66. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  67. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  68. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  69. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  70. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  71. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  72. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  73. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  74. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  75. package/src/components/custom/Workout/FatigueMeter.test.tsx +85 -0
  76. package/src/components/custom/Workout/FatigueMeter.tsx +86 -0
  77. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  78. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  79. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  80. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  81. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  82. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  84. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  85. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  86. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  87. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  88. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  89. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  90. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  91. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  92. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  93. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  94. package/src/components/custom/Workout/README.md +193 -4
  95. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  96. package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
  97. package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
  98. package/src/components/custom/Workout/RestTimer.tsx +155 -59
  99. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  100. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  101. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  102. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  103. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  104. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  105. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  106. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  107. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  108. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  109. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  110. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  111. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  112. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  113. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  114. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  115. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  116. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  117. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  118. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  119. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  120. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  121. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  122. package/src/components/custom/Workout/SetBar.tsx +171 -0
  123. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  124. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  125. package/src/components/custom/Workout/SetRow.tsx +169 -232
  126. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  127. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  128. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  129. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  130. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  131. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  132. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  133. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  134. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  135. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  136. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  137. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  138. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  139. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  140. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  141. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  142. package/src/components/custom/Workout/TempoBar.stories.tsx +70 -1
  143. package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
  144. package/src/components/custom/Workout/TempoBar.tsx +107 -22
  145. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  146. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  147. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  148. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  149. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  150. package/src/components/custom/Workout/VelocityStrip.stories.tsx +262 -56
  151. package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
  152. package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
  153. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  154. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  155. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  156. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  157. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  158. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  159. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  160. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  161. package/src/components/custom/Workout/ZoneTrack.stories.tsx +139 -0
  162. package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
  163. package/src/components/custom/Workout/ZoneTrack.tsx +373 -0
  164. package/src/components/custom/Workout/index.ts +55 -2
  165. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  166. package/src/components/custom/Workout/metricText.tsx +34 -0
  167. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  168. package/src/components/custom/Workout/paceTone.ts +27 -0
  169. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  170. package/src/components/custom/index.ts +2 -0
  171. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  172. package/src/components/icons/icons.stories.tsx +67 -2
  173. package/src/components/icons/icons.test.tsx +32 -2
  174. package/src/components/icons/icons.tsx +108 -0
  175. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  176. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  177. package/src/components/shell/DashboardShell.test.tsx +42 -0
  178. package/src/components/shell/DashboardShell.tsx +74 -0
  179. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  180. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  181. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  182. package/src/components/shell/NavItem.stories.tsx +52 -0
  183. package/src/components/shell/NavItem.test.tsx +35 -0
  184. package/src/components/shell/NavItem.tsx +75 -0
  185. package/src/components/shell/README.md +30 -0
  186. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  187. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  188. package/src/components/shell/SessionStatePill.tsx +1 -1
  189. package/src/components/shell/SideNav.stories.tsx +81 -0
  190. package/src/components/shell/SideNav.test.tsx +51 -0
  191. package/src/components/shell/SideNav.tsx +74 -0
  192. package/src/components/shell/TopBar.stories.tsx +5 -5
  193. package/src/components/shell/index.ts +5 -0
  194. package/src/components/ui/alert/Alert.stories.tsx +86 -3
  195. package/src/components/ui/alert/Alert.test.tsx +52 -0
  196. package/src/components/ui/alert/Alert.tsx +54 -20
  197. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  198. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  199. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  200. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  201. package/src/components/ui/button/Button.stories.tsx +1 -1
  202. package/src/components/ui/card/Card.stories.tsx +1 -1
  203. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  204. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  205. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  206. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  207. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  208. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  209. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  210. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  211. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  212. package/src/components/ui/index.ts +1 -0
  213. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  214. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  215. package/src/components/ui/indicator/Indicator.tsx +6 -1
  216. package/src/components/ui/input/Input.stories.tsx +1 -1
  217. package/src/components/ui/link/Link.stories.tsx +1 -1
  218. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  219. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  220. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  221. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  222. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  223. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  224. package/src/components/ui/progress/Progress.tsx +47 -22
  225. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  226. package/src/components/ui/section/Section.stories.tsx +1 -1
  227. package/src/components/ui/select/Select.stories.tsx +1 -1
  228. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  229. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  230. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  231. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  232. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  233. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  234. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  235. package/src/components/ui/tile/Tile.test.tsx +48 -0
  236. package/src/components/ui/tile/Tile.tsx +64 -0
  237. package/src/components/ui/tile/index.ts +2 -0
  238. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  239. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  240. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  241. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  242. package/src/hooks/index.ts +2 -0
  243. package/src/hooks/useTimer.test.ts +150 -0
  244. package/src/hooks/useTimer.ts +77 -0
  245. package/src/index.ts +3 -0
  246. package/src/pages.ts +1 -1
  247. package/src/stories/PresetShowcase.stories.tsx +1 -1
  248. package/src/stories/ThemePresets.stories.tsx +1 -1
  249. package/src/theme/ColorSystem.stories.tsx +1 -1
  250. package/src/theme/Colors.stories.tsx +5 -8
  251. package/src/theme/config.ts +4 -0
  252. package/src/theme/elevation.stories.tsx +1 -1
  253. package/src/theme/global.css +5 -1
  254. package/src/theme/shadows.stories.tsx +1 -1
  255. package/src/theme/tokens/primitives.ts +11 -107
  256. package/src/theme/tokens/semantic.ts +8 -0
  257. package/src/theme/workout-tokens.ts +11 -7
  258. package/tailwind.config.js +5 -0
@@ -1,12 +1,31 @@
1
- import React from 'react'
2
- import type { Meta, StoryObj } from '@storybook/react-vite'
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: 'Custom/Workout/RestTimer',
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
- const remainingMs = Math.max(0, totalSeconds * 1000 - elapsedMs)
31
- const remainingSec = Math.ceil(remainingMs / 1000)
32
- const minutes = Math.floor(remainingSec / 60)
33
- const seconds = remainingSec % 60
34
- const timeDisplay = `${minutes}:${String(seconds).padStart(2, '0')}`
35
- // A zero-duration timer is already complete; guard the 0 / 0 === NaN case
36
- // that would otherwise emit width:'NaN%'.
37
- const totalMs = totalSeconds * 1000
38
- const progressPct = totalMs > 0 ? Math.min(100, (elapsedMs / totalMs) * 100) : 100
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
  }