@titan-design/react-ui 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/bodymap.js +241 -189
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +241 -189
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +1017 -130
  6. package/dist/index.d.ts +1017 -130
  7. package/dist/index.js +3058 -1090
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2992 -1091
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1797 -1292
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1797 -1292
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +464 -268
  20. package/dist/theme/index.d.ts +464 -268
  21. package/dist/theme/index.js +303 -191
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +295 -192
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +160 -143
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +160 -143
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +3 -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/DateTime/DateTime.stories.tsx +83 -7
  34. package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
  35. package/src/components/custom/DateTime/DateTime.tsx +55 -15
  36. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  37. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  38. package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
  39. package/src/components/custom/Gauge/Gauge.tsx +6 -3
  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/Scatter/Scatter.tsx +3 -5
  43. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  44. package/src/components/custom/Table/Table.stories.tsx +1 -1
  45. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  46. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  47. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  48. package/src/components/custom/TimerReadout/index.ts +1 -0
  49. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  50. package/src/components/custom/Treemap/Treemap.tsx +3 -5
  51. package/src/components/custom/Typography/Typography.stories.tsx +46 -28
  52. package/src/components/custom/Typography/Typography.test.tsx +13 -3
  53. package/src/components/custom/Typography/Typography.tsx +5 -0
  54. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  55. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  56. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +11 -16
  57. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  58. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  59. package/src/components/custom/Workout/BodyMap.tsx +6 -3
  60. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  61. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
  62. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  63. package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
  64. package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
  65. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  66. package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
  67. package/src/components/custom/Workout/DeviationBar.tsx +7 -4
  68. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  69. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  70. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  71. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  72. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  73. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  74. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  75. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  76. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  77. package/src/components/custom/Workout/ExerciseDetailPage.tsx +6 -6
  78. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  79. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  80. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  81. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  82. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  83. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  84. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  85. package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
  86. package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
  87. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  88. package/src/components/custom/Workout/InputBar.tsx +2 -1
  89. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  90. package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
  91. package/src/components/custom/Workout/IntensityBar.tsx +10 -6
  92. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  93. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  94. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  95. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  96. package/src/components/custom/Workout/MesoCard.tsx +8 -3
  97. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  98. package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
  99. package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
  100. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  101. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
  102. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  103. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  104. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  105. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  106. package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
  107. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  108. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  109. package/src/components/custom/Workout/PrBadge.tsx +5 -2
  110. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  111. package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
  112. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  113. package/src/components/custom/Workout/ProgramPlanningPage.tsx +10 -7
  114. package/src/components/custom/Workout/README.md +146 -4
  115. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  116. package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
  117. package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
  118. package/src/components/custom/Workout/RestTimer.tsx +17 -11
  119. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  120. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  121. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  122. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  123. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  124. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  125. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  126. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  127. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  128. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  129. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  130. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  131. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  132. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  133. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  134. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  135. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  136. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  137. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  138. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  139. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  140. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  141. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  142. package/src/components/custom/Workout/SetBar.tsx +171 -0
  143. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  144. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  145. package/src/components/custom/Workout/SetRow.tsx +170 -232
  146. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  147. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  148. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  149. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  150. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  151. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  152. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  153. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  154. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  155. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  156. package/src/components/custom/Workout/Sparkline.tsx +2 -1
  157. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  158. package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
  159. package/src/components/custom/Workout/StatusDot.tsx +12 -9
  160. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  161. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  162. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  163. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  164. package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
  165. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  166. package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
  167. package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
  168. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  169. package/src/components/custom/Workout/TempoDisplay.tsx +29 -36
  170. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  171. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  172. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  173. package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
  174. package/src/components/custom/Workout/VelocityStrip.test.tsx +379 -144
  175. package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
  176. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  177. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  178. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  179. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  180. package/src/components/custom/Workout/WeekRow.tsx +3 -2
  181. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  182. package/src/components/custom/Workout/WeightBadge.tsx +6 -2
  183. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  184. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  185. package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
  186. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  187. package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
  188. package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
  189. package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
  190. package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
  191. package/src/components/custom/Workout/icons.tsx +6 -67
  192. package/src/components/custom/Workout/index.ts +55 -2
  193. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  194. package/src/components/custom/Workout/metricText.tsx +34 -0
  195. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  196. package/src/components/custom/Workout/paceTone.ts +27 -0
  197. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  198. package/src/components/custom/index.ts +1 -0
  199. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  200. package/src/components/icons/SvgIcon.tsx +50 -0
  201. package/src/components/icons/icons.stories.tsx +122 -0
  202. package/src/components/icons/icons.test.tsx +73 -0
  203. package/src/components/icons/icons.tsx +153 -0
  204. package/src/components/icons/index.ts +4 -0
  205. package/src/components/index.ts +6 -0
  206. package/src/components/shell/BrandLockup.stories.tsx +27 -0
  207. package/src/components/shell/BrandLockup.test.tsx +23 -0
  208. package/src/components/shell/BrandLockup.tsx +47 -0
  209. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  210. package/src/components/shell/DashboardShell.test.tsx +42 -0
  211. package/src/components/shell/DashboardShell.tsx +74 -0
  212. package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
  213. package/src/components/shell/DeviceIndicator.test.tsx +26 -0
  214. package/src/components/shell/DeviceIndicator.tsx +52 -0
  215. package/src/components/shell/DeviceMenu.stories.tsx +47 -0
  216. package/src/components/shell/DeviceMenu.test.tsx +40 -0
  217. package/src/components/shell/DeviceMenu.tsx +65 -0
  218. package/src/components/shell/DeviceRow.stories.tsx +38 -0
  219. package/src/components/shell/DeviceRow.test.tsx +37 -0
  220. package/src/components/shell/DeviceRow.tsx +58 -0
  221. package/src/components/shell/NavItem.stories.tsx +52 -0
  222. package/src/components/shell/NavItem.test.tsx +35 -0
  223. package/src/components/shell/NavItem.tsx +75 -0
  224. package/src/components/shell/README.md +116 -0
  225. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  226. package/src/components/shell/SessionStatePill.stories.tsx +27 -0
  227. package/src/components/shell/SessionStatePill.test.tsx +25 -0
  228. package/src/components/shell/SessionStatePill.tsx +46 -0
  229. package/src/components/shell/SideNav.stories.tsx +81 -0
  230. package/src/components/shell/SideNav.test.tsx +51 -0
  231. package/src/components/shell/SideNav.tsx +74 -0
  232. package/src/components/shell/TopBar.stories.tsx +49 -0
  233. package/src/components/shell/TopBar.test.tsx +36 -0
  234. package/src/components/shell/TopBar.tsx +89 -0
  235. package/src/components/shell/index.ts +13 -0
  236. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  237. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  238. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  239. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  240. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  241. package/src/components/ui/button/Button.stories.tsx +1 -1
  242. package/src/components/ui/card/Card.stories.tsx +1 -1
  243. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  244. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  245. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  246. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  247. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  248. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  249. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  250. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  251. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  252. package/src/components/ui/index.ts +1 -0
  253. package/src/components/ui/indicator/Indicator.stories.tsx +38 -2
  254. package/src/components/ui/indicator/Indicator.test.tsx +20 -1
  255. package/src/components/ui/indicator/Indicator.tsx +50 -6
  256. package/src/components/ui/indicator/index.ts +1 -1
  257. package/src/components/ui/input/Input.stories.tsx +1 -1
  258. package/src/components/ui/link/Link.stories.tsx +1 -1
  259. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  260. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  261. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  262. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  263. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  264. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  265. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  266. package/src/components/ui/section/Section.stories.tsx +1 -1
  267. package/src/components/ui/select/Select.stories.tsx +1 -1
  268. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  269. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  270. package/src/components/ui/spinner/Spinner.tsx +3 -2
  271. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  272. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  273. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  274. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  275. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  276. package/src/components/ui/tile/Tile.test.tsx +48 -0
  277. package/src/components/ui/tile/Tile.tsx +64 -0
  278. package/src/components/ui/tile/index.ts +2 -0
  279. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  280. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  281. package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
  282. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  283. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  284. package/src/hooks/index.ts +2 -0
  285. package/src/hooks/useTimer.test.ts +150 -0
  286. package/src/hooks/useTimer.ts +77 -0
  287. package/src/index.ts +3 -0
  288. package/src/pages.ts +1 -1
  289. package/src/stories/PresetShowcase.stories.tsx +1 -1
  290. package/src/stories/ThemePresets.stories.tsx +1 -1
  291. package/src/theme/ColorSystem.stories.tsx +323 -0
  292. package/src/theme/Colors.stories.tsx +74 -9
  293. package/src/theme/barrel.ts +1 -0
  294. package/src/theme/config.ts +20 -3
  295. package/src/theme/elevation.stories.tsx +1 -1
  296. package/src/theme/extracted-colors-dataviz.ts +21 -0
  297. package/src/theme/extracted-colors-ui.ts +20 -0
  298. package/src/theme/global.css +69 -53
  299. package/src/theme/gradients.test.ts +25 -0
  300. package/src/theme/gradients.ts +36 -0
  301. package/src/theme/shadows.stories.tsx +1 -1
  302. package/src/theme/tokens/primitives.test.ts +232 -0
  303. package/src/theme/tokens/primitives.ts +238 -87
  304. package/src/theme/tokens/semantic.ts +101 -81
  305. package/src/theme/workout-tokens.ts +26 -21
  306. package/src/utils/avatar-color.ts +3 -3
  307. package/tailwind.config.js +15 -3
@@ -0,0 +1,153 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { LiveAuraFrame } from './LiveAuraFrame'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+
6
+ const t = getSemanticColors('dark')
7
+
8
+ /** A representative live-workout hero so the full-surface flood reads against real content. */
9
+ function LiveHero({ caption }: { caption: string }) {
10
+ return (
11
+ <View style={{ flex: 1, padding: 24, justifyContent: 'space-between' }}>
12
+ <View>
13
+ <Text
14
+ style={{
15
+ fontSize: 11,
16
+ fontWeight: '800',
17
+ letterSpacing: 1.2,
18
+ textTransform: 'uppercase',
19
+ color: t['text-tertiary'],
20
+ }}
21
+ >
22
+ Cable Row · Set 4 / 5
23
+ </Text>
24
+ </View>
25
+
26
+ <View style={{ alignItems: 'center' }}>
27
+ <Text
28
+ style={{ fontSize: 96, fontWeight: '800', lineHeight: 100, color: t['text-primary'] }}
29
+ >
30
+ 8
31
+ </Text>
32
+ <Text
33
+ style={{
34
+ fontSize: 12,
35
+ letterSpacing: 2,
36
+ textTransform: 'uppercase',
37
+ color: t['text-tertiary'],
38
+ }}
39
+ >
40
+ reps
41
+ </Text>
42
+ </View>
43
+
44
+ <View>
45
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 12 }}>
46
+ {[
47
+ ['0.42', 'm/s'],
48
+ ['8.5', 'RPE'],
49
+ ['−18%', 'vel loss'],
50
+ ].map(([v, l]) => (
51
+ <View key={l} style={{ alignItems: 'center' }}>
52
+ <Text style={{ fontSize: 20, fontWeight: '700', color: t['text-primary'] }}>{v}</Text>
53
+ <Text
54
+ style={{
55
+ fontSize: 9,
56
+ letterSpacing: 1,
57
+ textTransform: 'uppercase',
58
+ color: t['text-tertiary'],
59
+ }}
60
+ >
61
+ {l}
62
+ </Text>
63
+ </View>
64
+ ))}
65
+ </View>
66
+ <Text style={{ fontSize: 12, color: t['text-secondary'], textAlign: 'center' }}>
67
+ {caption}
68
+ </Text>
69
+ </View>
70
+ </View>
71
+ )
72
+ }
73
+
74
+ const PANEL = { width: 360, height: 620, borderRadius: 24, overflow: 'hidden' as const }
75
+
76
+ const meta: Meta<typeof LiveAuraFrame> = {
77
+ title: 'Workout/LiveAuraFrame',
78
+ component: LiveAuraFrame,
79
+ tags: ['autodocs'],
80
+ parameters: {
81
+ layout: 'fullscreen',
82
+ docs: {
83
+ description: {
84
+ component:
85
+ 'Full-surface color-flood frame tied to a coaching category. Wraps content on a charcoal ' +
86
+ 'base and layers a radial wash — amber at threshold (VL20), red at stop (VL28+, pulsing) — ' +
87
+ 'derived from the semantic warning / error tokens via `alpha()`. Children pass through. ' +
88
+ 'Rendered here at live-screen size so the flood reads (it is meant to tint a whole panel, ' +
89
+ 'not a card).',
90
+ },
91
+ },
92
+ },
93
+ argTypes: {
94
+ category: {
95
+ control: 'select',
96
+ options: ['productive', 'threshold', 'stop'],
97
+ description: 'Coaching-category flood level',
98
+ },
99
+ pulse: { control: 'boolean', description: 'Pulse the flood on stop' },
100
+ },
101
+ decorators: [
102
+ (Story) => (
103
+ <View
104
+ style={{
105
+ minHeight: 700,
106
+ backgroundColor: '#070707',
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ padding: 32,
110
+ }}
111
+ >
112
+ <Story />
113
+ </View>
114
+ ),
115
+ ],
116
+ render: (args) => (
117
+ <LiveAuraFrame {...args} style={PANEL}>
118
+ <LiveHero
119
+ caption={
120
+ args.category === 'stop'
121
+ ? 'red flood — VL28+, stop the set'
122
+ : args.category === 'threshold'
123
+ ? 'amber flood — VL20 threshold reached'
124
+ : 'no flood — productive, quiet background'
125
+ }
126
+ />
127
+ </LiveAuraFrame>
128
+ ),
129
+ }
130
+
131
+ export default meta
132
+ type Story = StoryObj<typeof LiveAuraFrame>
133
+
134
+ export const Productive: Story = { args: { category: 'productive' } }
135
+ export const Threshold: Story = { args: { category: 'threshold' } }
136
+ export const Stop: Story = { args: { category: 'stop' } }
137
+
138
+ /** All three flood levels side by side at live-screen size. */
139
+ export const AllStates: Story = {
140
+ render: () => (
141
+ <View style={{ flexDirection: 'row', gap: 20, flexWrap: 'wrap', justifyContent: 'center' }}>
142
+ <LiveAuraFrame category="productive" style={PANEL}>
143
+ <LiveHero caption="no flood — productive" />
144
+ </LiveAuraFrame>
145
+ <LiveAuraFrame category="threshold" style={PANEL}>
146
+ <LiveHero caption="amber flood — VL20 threshold" />
147
+ </LiveAuraFrame>
148
+ <LiveAuraFrame category="stop" pulse={false} style={PANEL}>
149
+ <LiveHero caption="red flood — VL28+, stop" />
150
+ </LiveAuraFrame>
151
+ </View>
152
+ ),
153
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Text } from 'react-native'
5
+ import { LiveAuraFrame, liveAuraColor, type LiveAuraCategory } from './LiveAuraFrame'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
+ import { alpha } from '../../../utils/colors'
8
+
9
+ const t = getSemanticColors('dark')
10
+
11
+ describe('LiveAuraFrame', () => {
12
+ it('renders the frame and passes children through', () => {
13
+ render(
14
+ <LiveAuraFrame category="productive">
15
+ <Text>Cable Row</Text>
16
+ </LiveAuraFrame>
17
+ )
18
+ expect(screen.getByTestId('live-aura-frame')).toBeInTheDocument()
19
+ expect(screen.getByText('Cable Row')).toBeInTheDocument()
20
+ })
21
+
22
+ it('sits on the charcoal base surface', () => {
23
+ render(<LiveAuraFrame category="productive" />)
24
+ expect(screen.getByTestId('live-aura-frame')).toHaveStyle({
25
+ backgroundColor: t['background-base'],
26
+ })
27
+ })
28
+
29
+ it('renders no flood overlay for the productive (quiet) category', () => {
30
+ render(<LiveAuraFrame category="productive" />)
31
+ expect(screen.queryByTestId('live-aura-flood')).not.toBeInTheDocument()
32
+ })
33
+
34
+ const floods: Array<{
35
+ category: LiveAuraCategory
36
+ token: 'status-warning' | 'status-error'
37
+ opacity: number
38
+ }> = [
39
+ { category: 'threshold', token: 'status-warning', opacity: 0.12 },
40
+ { category: 'stop', token: 'status-error', opacity: 0.18 },
41
+ ]
42
+
43
+ floods.forEach(({ category, token, opacity }) => {
44
+ it(`floods ${category} with a ${token}-tinted radial wash that fades to transparent`, () => {
45
+ render(<LiveAuraFrame category={category} />)
46
+ // Web (the target): a top-down radial gradient tinted with the token color,
47
+ // fading to transparent — matches R2's .aura-* CSS, NOT a flat surface fill.
48
+ const flood = screen.getByTestId('live-aura-flood') as HTMLElement
49
+ expect(flood.style.backgroundImage).toContain(alpha(t[token], opacity))
50
+ expect(flood.style.backgroundImage).toContain('transparent')
51
+ expect(flood.style.backgroundColor).toBe('')
52
+ })
53
+ })
54
+
55
+ it('exposes the resolved flood color via liveAuraColor', () => {
56
+ expect(liveAuraColor('productive')).toBeNull()
57
+ expect(liveAuraColor('threshold')).toBe(t['status-warning'])
58
+ expect(liveAuraColor('stop')).toBe(t['status-error'])
59
+ })
60
+
61
+ it('renders the flood for stop when pulse is disabled', () => {
62
+ render(<LiveAuraFrame category="stop" pulse={false} />)
63
+ expect(screen.getByTestId('live-aura-flood')).toBeInTheDocument()
64
+ })
65
+
66
+ it('has no a11y violations', async () => {
67
+ const { container } = render(
68
+ <LiveAuraFrame category="stop">
69
+ <Text>Set 4/5</Text>
70
+ </LiveAuraFrame>
71
+ )
72
+ expect(await axe(container)).toHaveNoViolations()
73
+ })
74
+ })
@@ -0,0 +1,105 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, StyleSheet, Platform, type ViewProps, type ViewStyle } from 'react-native'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+ import { alpha } from '../../../utils/colors'
5
+
6
+ const t = getSemanticColors('dark')
7
+
8
+ /**
9
+ * Coaching-category flood level for the live surface. Mirrors {@link StatusPill}:
10
+ * - `productive` — quiet, no flood
11
+ * - `threshold` — amber wash at VL20
12
+ * - `stop` — red wash at VL28+ (optionally pulsing)
13
+ */
14
+ export type LiveAuraCategory = 'productive' | 'threshold' | 'stop'
15
+
16
+ export interface LiveAuraFrameProps extends ViewProps {
17
+ category: LiveAuraCategory
18
+ /**
19
+ * Pulse the flood on `stop`. Uses the nativewind `animate-pulse` utility so
20
+ * it stays static-friendly under test (className→style, no live clock).
21
+ * Defaults to true.
22
+ */
23
+ pulse?: boolean
24
+ className?: string
25
+ children?: React.ReactNode
26
+ }
27
+
28
+ const categoryToken: Record<LiveAuraCategory, 'status-warning' | 'status-error' | null> = {
29
+ productive: null,
30
+ threshold: 'status-warning',
31
+ stop: 'status-error',
32
+ }
33
+
34
+ const floodOpacity: Record<LiveAuraCategory, number> = {
35
+ productive: 0,
36
+ threshold: 0.12,
37
+ stop: 0.18,
38
+ }
39
+
40
+ /** Resolved flood color for a category, or `null` when there is no flood. */
41
+ export function liveAuraColor(category: LiveAuraCategory): string | null {
42
+ const token = categoryToken[category]
43
+ return token ? t[token] : null
44
+ }
45
+
46
+ /**
47
+ * Full-surface color-flood frame tied to a coaching category. Wraps `children`
48
+ * on a charcoal base and layers a radial wash (amber at threshold, red at stop)
49
+ * as a decorative, pointer-transparent overlay. The wash uses a solid
50
+ * `backgroundColor` fallback (native) plus a web-only `backgroundImage` radial
51
+ * gradient; both derive from the same semantic token via `alpha()`.
52
+ */
53
+ export function LiveAuraFrame({
54
+ category,
55
+ pulse = true,
56
+ className,
57
+ children,
58
+ style,
59
+ ...props
60
+ }: LiveAuraFrameProps) {
61
+ const color = liveAuraColor(category)
62
+ const tint = color ? alpha(color, floodOpacity[category]) : 'transparent'
63
+
64
+ // Match R2's canonical aura: a top-down radial wash that FADES TO TRANSPARENT
65
+ // by ~62%, so the charcoal base shows through the lower panel — NOT a flat
66
+ // full-surface tint. On web (the target: MCP dashboard + Storybook) render the
67
+ // gradient only; native has no gradient support, so fall back to a solid tint.
68
+ const floodStyle = {
69
+ ...StyleSheet.absoluteFillObject,
70
+ ...(Platform.OS === 'web'
71
+ ? // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
72
+ { backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
73
+ : { backgroundColor: tint }),
74
+ } as ViewStyle
75
+
76
+ return (
77
+ <View
78
+ testID="live-aura-frame"
79
+ className={className}
80
+ style={[
81
+ {
82
+ position: 'relative',
83
+ overflow: 'hidden',
84
+ borderRadius: 10,
85
+ backgroundColor: t['background-base'],
86
+ borderWidth: 1,
87
+ borderColor: t['border-default'],
88
+ },
89
+ style,
90
+ ]}
91
+ {...props}
92
+ >
93
+ {color && (
94
+ <View
95
+ testID="live-aura-flood"
96
+ accessibilityElementsHidden
97
+ pointerEvents="none"
98
+ className={category === 'stop' && pulse ? 'animate-pulse' : undefined}
99
+ style={floodStyle}
100
+ />
101
+ )}
102
+ {children}
103
+ </View>
104
+ )
105
+ }
@@ -48,7 +48,7 @@ const weeks: WeekRowProps[] = [
48
48
  ]
49
49
 
50
50
  const meta: Meta<typeof MesoCard> = {
51
- title: 'Custom/Workout/MesoCard',
51
+ title: 'Workout/MesoCard',
52
52
  component: MesoCard,
53
53
  tags: ['autodocs'],
54
54
  argTypes: {
@@ -4,10 +4,15 @@ import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-n
4
4
  import { Card } from '../../ui/card'
5
5
  import { Badge } from '../../ui/badge'
6
6
  import { WeekRow, type WeekRowProps } from './WeekRow'
7
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
8
+ import {
9
+ MESO_ACCENT_GRADIENT_DARK,
10
+ MESO_ACCENT_GRADIENT_LIGHT,
11
+ } from '../../../theme/extracted-colors-dataviz'
7
12
 
8
- const BRAND_PRIMARY = '#FF7900'
9
- const BRAND_PRIMARY_DARK = '#C45E00'
10
- const BRAND_PRIMARY_LIGHT = '#FFB066'
13
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
14
+ const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
15
+ const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
11
16
  const BORDER_DEFAULT = '#1F1F1F'
12
17
 
13
18
  /** Gradient stops for the 3px top accent: dark -> primary -> light. */
@@ -4,7 +4,7 @@ import { View } from 'react-native'
4
4
  import { MesoProgressBar, type Meso } from './MesoProgressBar'
5
5
 
6
6
  const meta: Meta<typeof MesoProgressBar> = {
7
- title: 'Custom/Workout/MesoProgressBar',
7
+ title: 'Workout/MesoProgressBar',
8
8
  component: MesoProgressBar,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -40,7 +40,7 @@ describe('MesoProgressBar', () => {
40
40
  it('applies completed color at 0.3 alpha', () => {
41
41
  render(<MesoProgressBar mesos={mesos} activeMesoId={null} onMesoPress={vi.fn()} />)
42
42
  expect(screen.getByTestId('meso-segment-inner-m1')).toHaveStyle({
43
- backgroundColor: 'rgba(20,184,166,0.3)',
43
+ backgroundColor: 'rgba(46,213,115,0.3)',
44
44
  })
45
45
  })
46
46
 
@@ -1,8 +1,9 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { useState } from 'react'
3
3
  import { View, Pressable, Animated, type ViewProps } from 'react-native'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
5
 
5
- const BRAND_PRIMARY = '#FF7900'
6
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
6
7
 
7
8
  export type MesoStatus = 'completed' | 'current' | 'upcoming'
8
9
 
@@ -26,7 +27,7 @@ export interface MesoProgressBarProps extends ViewProps {
26
27
  }
27
28
 
28
29
  const segmentBgColors: Record<MesoStatus, string> = {
29
- completed: 'rgba(20,184,166,0.3)',
30
+ completed: 'rgba(46,213,115,0.3)',
30
31
  current: 'rgba(255,121,0,0.5)',
31
32
  upcoming: 'rgba(255,255,255,0.06)',
32
33
  }
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { MesoStatusCard } from './MesoStatusCard'
4
4
 
5
5
  const meta: Meta<typeof MesoStatusCard> = {
6
- title: 'Custom/Workout/MesoStatusCard',
6
+ title: 'Workout/MesoStatusCard',
7
7
  component: MesoStatusCard,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -4,14 +4,21 @@ import { View, Text, type ViewProps, type ViewStyle } from 'react-native'
4
4
  import { Card } from '../../ui/card'
5
5
  import { StatusDot } from './StatusDot'
6
6
  import { resolveColor } from '../../../theme/resolve-color'
7
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
8
+ import {
9
+ MESO_ACCENT_GRADIENT_DARK,
10
+ MESO_ACCENT_GRADIENT_LIGHT,
11
+ } from '../../../theme/extracted-colors-dataviz'
7
12
 
8
- const BRAND_PRIMARY = '#FF7900'
9
- const BRAND_PRIMARY_DARK = '#C45E00'
10
- const BRAND_PRIMARY_LIGHT = '#FFB066'
13
+ const t = getSemanticColors('dark')
11
14
 
12
- const SUCCESS = '#14B8A6'
13
- const WARNING = '#FFB020'
14
- const ERROR = '#D14343'
15
+ const BRAND_PRIMARY = t['brand-primary']
16
+ const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
17
+ const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
18
+
19
+ const SUCCESS = t['status-success']
20
+ const WARNING = t['status-warning']
21
+ const ERROR = t['status-error']
15
22
 
16
23
  /** Gradient stops for the 3px top accent: dark -> primary -> light (matches MesoCard). */
17
24
  const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
@@ -25,7 +32,7 @@ const CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor('surface-elevated'
25
32
 
26
33
  /** Gauge track gradient (teal -> amber -> red) at 0.25 alpha. */
27
34
  const GAUGE_GRADIENT =
28
- 'linear-gradient(90deg, rgba(20,184,166,0.25) 0%, rgba(255,176,32,0.25) 50%, rgba(209,67,67,0.25) 100%)'
35
+ 'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(249,180,21,0.25) 50%, rgba(209,67,67,0.25) 100%)'
29
36
 
30
37
  export type MesoStatusBadgeVariant = 'success' | 'warning' | 'error'
31
38
 
@@ -33,8 +40,8 @@ const STATUS_VARIANTS: Record<
33
40
  MesoStatusBadgeVariant,
34
41
  { bg: string; border: string; text: string }
35
42
  > = {
36
- success: { bg: 'rgba(20,184,166,0.15)', border: 'rgba(20,184,166,0.3)', text: SUCCESS },
37
- warning: { bg: 'rgba(255,176,32,0.15)', border: 'rgba(255,176,32,0.3)', text: WARNING },
43
+ success: { bg: 'rgba(46,213,115,0.15)', border: 'rgba(46,213,115,0.3)', text: SUCCESS },
44
+ warning: { bg: 'rgba(249,180,21,0.15)', border: 'rgba(249,180,21,0.3)', text: WARNING },
38
45
  error: { bg: 'rgba(209,67,67,0.15)', border: 'rgba(209,67,67,0.25)', text: ERROR },
39
46
  }
40
47
 
@@ -400,9 +407,9 @@ export function MesoStatusCard({
400
407
  {coaching != null && (
401
408
  <View
402
409
  style={{
403
- backgroundColor: 'rgba(255,176,32,0.06)',
410
+ backgroundColor: 'rgba(249,180,21,0.06)',
404
411
  borderWidth: 1,
405
- borderColor: 'rgba(255,176,32,0.15)',
412
+ borderColor: 'rgba(249,180,21,0.15)',
406
413
  borderRadius: 8,
407
414
  paddingVertical: 10,
408
415
  paddingHorizontal: 12,
@@ -427,9 +434,9 @@ export function MesoStatusCard({
427
434
  className="flex-row items-center"
428
435
  style={{
429
436
  gap: 8,
430
- backgroundColor: 'rgba(20,184,166,0.06)',
437
+ backgroundColor: 'rgba(46,213,115,0.06)',
431
438
  borderWidth: 1,
432
- borderColor: 'rgba(20,184,166,0.2)',
439
+ borderColor: 'rgba(46,213,115,0.2)',
433
440
  borderRadius: 8,
434
441
  paddingVertical: 10,
435
442
  paddingHorizontal: 12,
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { MetricTiles } from './MetricTiles'
4
+ import { getSemanticColors } from '../../../theme'
5
+
6
+ const AMBER = getSemanticColors('dark')['status-warning']
7
+
8
+ const meta: Meta<typeof MetricTiles> = {
9
+ title: 'Workout/MetricTiles',
10
+ component: MetricTiles,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ '**Molecule.** An HStack of [Tile](?path=/docs/components-atoms-tile--docs). Used-by ↑ SessionHeader.',
17
+ },
18
+ },
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <View style={{ width: 246 }}>
23
+ <Story />
24
+ </View>
25
+ ),
26
+ ],
27
+ }
28
+
29
+ export default meta
30
+ type Story = StoryObj<typeof MetricTiles>
31
+
32
+ export const SessionStats: Story = {
33
+ args: {
34
+ metrics: [
35
+ { label: 'Volume', value: '76%' },
36
+ { label: 'Load', value: '7.3k' },
37
+ { label: 'Fatigue', value: 'MOD', valueColor: AMBER },
38
+ ],
39
+ },
40
+ }
41
+
42
+ export const TwoTiles: Story = {
43
+ args: {
44
+ metrics: [
45
+ { label: 'Time', value: '6:30 PM' },
46
+ { label: 'Until', value: '5h', valueColor: AMBER },
47
+ ],
48
+ },
49
+ }
50
+
51
+ export const FourTiles: Story = {
52
+ args: {
53
+ metrics: [
54
+ { label: 'Sets', value: '7/12' },
55
+ { label: 'Volume', value: '76%' },
56
+ { label: 'Load', value: '7.3k' },
57
+ { label: 'Fatigue', value: 'MOD', valueColor: AMBER },
58
+ ],
59
+ },
60
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { MetricTiles } from './MetricTiles'
5
+
6
+ const STATS = [
7
+ { label: 'Volume', value: '76%' },
8
+ { label: 'Load', value: '7.3k' },
9
+ { label: 'Fatigue', value: 'MOD', valueColor: '#F5A623' },
10
+ ]
11
+
12
+ describe('MetricTiles', () => {
13
+ it('renders a tile per metric', () => {
14
+ render(<MetricTiles metrics={STATS} />)
15
+ expect(screen.getByText('Volume')).toBeInTheDocument()
16
+ expect(screen.getByText('76%')).toBeInTheDocument()
17
+ expect(screen.getByText('Load')).toBeInTheDocument()
18
+ expect(screen.getByText('7.3k')).toBeInTheDocument()
19
+ expect(screen.getByText('Fatigue')).toBeInTheDocument()
20
+ expect(screen.getByText('MOD')).toBeInTheDocument()
21
+ })
22
+
23
+ it('passes valueColor through to the tile value', () => {
24
+ render(<MetricTiles metrics={STATS} />)
25
+ expect(screen.getByText('MOD')).toHaveStyle({ color: '#F5A623' })
26
+ })
27
+
28
+ it('does not tint values without a valueColor', () => {
29
+ render(<MetricTiles metrics={STATS} />)
30
+ expect(screen.getByText('76%').style.color).toBe('')
31
+ })
32
+
33
+ it('reflows an arbitrary count of metrics', () => {
34
+ render(
35
+ <MetricTiles
36
+ metrics={[
37
+ { label: 'Time', value: '6:30 PM' },
38
+ { label: 'Until', value: '5h' },
39
+ ]}
40
+ />
41
+ )
42
+ expect(screen.getByText('6:30 PM')).toBeInTheDocument()
43
+ expect(screen.getByText('5h')).toBeInTheDocument()
44
+ })
45
+
46
+ it('passes additional props through', () => {
47
+ render(<MetricTiles metrics={STATS} testID="metric-tiles" />)
48
+ expect(screen.getByText('Volume')).toBeInTheDocument()
49
+ })
50
+
51
+ describe('accessibility', () => {
52
+ it('has no accessibility violations', async () => {
53
+ const { container } = render(<MetricTiles metrics={STATS} />)
54
+ const results = await axe(container)
55
+ expect(results).toHaveNoViolations()
56
+ })
57
+ })
58
+ })
@@ -0,0 +1,48 @@
1
+ import { type ViewProps } from 'react-native'
2
+ import { HStack } from '../../ui/stack'
3
+ import { Tile } from '../../ui/tile'
4
+
5
+ export interface MetricTileData {
6
+ /** Uppercase micro-label shown above the value */
7
+ label: string
8
+ /** Primary value */
9
+ value: string
10
+ /** Tints the value (status/accent hex); defaults to primary text */
11
+ valueColor?: string
12
+ }
13
+
14
+ export interface MetricTilesProps extends ViewProps {
15
+ /** Stat cells rendered left-to-right; typically three */
16
+ metrics: MetricTileData[]
17
+ /** Space between tiles (Stack gap scale, default 1 ≈ 4px) */
18
+ gap?: 0 | 1 | 2 | 3 | 4
19
+ }
20
+
21
+ /**
22
+ * MetricTiles — a row of equal-width stat tiles.
23
+ *
24
+ * A thin, presentational HStack of {@link Tile}. Each tile flexes to fill its
25
+ * share so the row reflows gracefully in a responsive rail. Used by SessionHeader
26
+ * for Volume / Load / Fatigue (and Date / Time / Until when upcoming).
27
+ *
28
+ * @example
29
+ * <MetricTiles metrics={[
30
+ * { label: 'Volume', value: '76%' },
31
+ * { label: 'Load', value: '7.3k' },
32
+ * { label: 'Fatigue', value: 'MOD', valueColor: amber },
33
+ * ]} />
34
+ */
35
+ export function MetricTiles({ metrics, gap = 1, ...props }: MetricTilesProps) {
36
+ return (
37
+ <HStack gap={gap} align="stretch" {...props}>
38
+ {metrics.map((metric) => (
39
+ <Tile
40
+ key={metric.label}
41
+ label={metric.label}
42
+ value={metric.value}
43
+ valueColor={metric.valueColor}
44
+ />
45
+ ))}
46
+ </HStack>
47
+ )
48
+ }
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { MuscleGroupChip, type VolumeStatus } from './MuscleGroupChip'
4
4
 
5
5
  const meta: Meta<typeof MuscleGroupChip> = {
6
- title: 'Custom/Workout/MuscleGroupChip',
6
+ title: 'Components/Atoms/MuscleGroupChip',
7
7
  component: MuscleGroupChip,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {