@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,93 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import {
5
+ ExerciseIndicator,
6
+ resolveIndicator,
7
+ INDICATOR_PRECEDENCE,
8
+ type ExerciseIndicatorKind,
9
+ } from './ExerciseIndicator'
10
+
11
+ // tier color = titan status token, applied as the glyph's SVG stroke (literal hex):
12
+ // danger=status-error, warning=status-warning, success=status-success, info=status-info.
13
+ const kinds: { kind: ExerciseIndicatorKind; label: string; tier: string }[] = [
14
+ { kind: 'imbalance', label: 'Left/right imbalance', tier: '#D14343' },
15
+ { kind: 'overshoot', label: 'Load overshoot', tier: '#D14343' },
16
+ { kind: 'velocity-loss', label: 'Velocity loss', tier: '#F9B415' },
17
+ { kind: 'missed-reps', label: 'Missed reps', tier: '#F9B415' },
18
+ { kind: 'pr', label: 'Personal record', tier: '#2ED573' },
19
+ { kind: 'info', label: 'More info', tier: '#2196F3' },
20
+ ]
21
+
22
+ describe('ExerciseIndicator', () => {
23
+ it.each(kinds)('renders the $kind chip glyph and accessible label', ({ kind, label }) => {
24
+ const { container } = render(<ExerciseIndicator kind={kind} />)
25
+ // glyph is an inline SVG (lucide-mirrored), not a text character
26
+ expect(container.querySelector('svg')).toBeInTheDocument()
27
+ expect(screen.getByLabelText(label)).toBeInTheDocument()
28
+ })
29
+
30
+ it.each(kinds)('tints the $kind glyph with its tier color', ({ kind, tier }) => {
31
+ const { container } = render(<ExerciseIndicator kind={kind} />)
32
+ expect(container.querySelector('svg')!.getAttribute('stroke')).toBe(tier)
33
+ })
34
+
35
+ it('renders a decorative (aria-hidden) glyph so the chip owns the label', () => {
36
+ const { container } = render(<ExerciseIndicator kind="pr" />)
37
+ expect(container.querySelector('svg')!.getAttribute('aria-hidden')).toBe('true')
38
+ })
39
+
40
+ it('is presentational (no button role) without onPress', () => {
41
+ render(<ExerciseIndicator kind="pr" />)
42
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
43
+ })
44
+
45
+ it('becomes a button and fires onPress when interactive', () => {
46
+ const onPress = vi.fn()
47
+ render(<ExerciseIndicator kind="pr" onPress={onPress} />)
48
+ const button = screen.getByRole('button', { name: 'Personal record' })
49
+ fireEvent.click(button)
50
+ expect(onPress).toHaveBeenCalledOnce()
51
+ })
52
+
53
+ describe('accessibility', () => {
54
+ it.each(kinds)('has no accessibility violations for $kind', async ({ kind }) => {
55
+ const { container } = render(<ExerciseIndicator kind={kind} />)
56
+ const results = await axe(container)
57
+ expect(results).toHaveNoViolations()
58
+ })
59
+ })
60
+ })
61
+
62
+ describe('resolveIndicator', () => {
63
+ it('returns undefined when no signals are active', () => {
64
+ expect(resolveIndicator([])).toBeUndefined()
65
+ })
66
+
67
+ it('returns the sole candidate unchanged', () => {
68
+ expect(resolveIndicator(['velocity-loss'])).toBe('velocity-loss')
69
+ })
70
+
71
+ it('picks the highest-precedence kind regardless of input order', () => {
72
+ expect(resolveIndicator(['info', 'pr', 'overshoot'])).toBe('overshoot')
73
+ expect(resolveIndicator(['overshoot', 'pr', 'info'])).toBe('overshoot')
74
+ })
75
+
76
+ it('lets imbalance (rank 1) win over every other signal', () => {
77
+ expect(resolveIndicator([...INDICATOR_PRECEDENCE])).toBe('imbalance')
78
+ })
79
+
80
+ it('ranks the soft alerts above pr (alert > pr)', () => {
81
+ expect(resolveIndicator(['pr', 'missed-reps'])).toBe('missed-reps')
82
+ expect(resolveIndicator(['pr', 'velocity-loss'])).toBe('velocity-loss')
83
+ })
84
+
85
+ it('falls to info only when it is the lowest active signal', () => {
86
+ expect(resolveIndicator(['info', 'pr'])).toBe('pr')
87
+ expect(resolveIndicator(['info'])).toBe('info')
88
+ })
89
+
90
+ it('ignores duplicate candidates', () => {
91
+ expect(resolveIndicator(['pr', 'pr', 'info'])).toBe('pr')
92
+ })
93
+ })
@@ -0,0 +1,120 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import type { ComponentType } from 'react'
3
+ import { View, Pressable, type ViewProps } from 'react-native'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+ import {
6
+ ScaleIcon,
7
+ AlertTriangleIcon,
8
+ TrendingDownIcon,
9
+ CircleSlashIcon,
10
+ AwardIcon,
11
+ InfoIcon,
12
+ type IconProps,
13
+ } from '../../icons'
14
+
15
+ // Literal-hex status pins (the TempoBar pattern) — `resolveColor()` yields a
16
+ // `var(...)` under the RNW vitest alias, which breaks `toHaveStyle` in tests.
17
+ const t = getSemanticColors('dark')
18
+
19
+ /**
20
+ * The LOCKED 6-kind indicator taxonomy (TD-03.51). One precedence-ranked slot is
21
+ * ever shown per exercise, resolved by {@link resolveIndicator}. Glyph = the
22
+ * specific signal; color = the severity tier (danger/warning/success/info, bound
23
+ * to titan status tokens). Outlined (border + glyph, no fill) so a chip never
24
+ * collides with the filled velocity strip beside it.
25
+ */
26
+ export type ExerciseIndicatorKind =
27
+ | 'imbalance'
28
+ | 'overshoot'
29
+ | 'velocity-loss'
30
+ | 'missed-reps'
31
+ | 'pr'
32
+ | 'info'
33
+
34
+ interface IndicatorConfig {
35
+ Icon: ComponentType<IconProps>
36
+ /** Tier color — a titan status semantic token (literal hex on `t`). */
37
+ color: string
38
+ /** Accessible description (also the intended tooltip subject). */
39
+ label: string
40
+ }
41
+
42
+ const INDICATOR_CONFIG: Record<ExerciseIndicatorKind, IndicatorConfig> = {
43
+ imbalance: { Icon: ScaleIcon, color: t['status-error'], label: 'Left/right imbalance' },
44
+ overshoot: { Icon: AlertTriangleIcon, color: t['status-error'], label: 'Load overshoot' },
45
+ 'velocity-loss': { Icon: TrendingDownIcon, color: t['status-warning'], label: 'Velocity loss' },
46
+ 'missed-reps': { Icon: CircleSlashIcon, color: t['status-warning'], label: 'Missed reps' },
47
+ pr: { Icon: AwardIcon, color: t['status-success'], label: 'Personal record' },
48
+ info: { Icon: InfoIcon, color: t['status-info'], label: 'More info' },
49
+ }
50
+
51
+ /**
52
+ * Precedence order (1=highest → last=lowest). A single chip is shown, so when
53
+ * several signals are true the highest-ranked wins. Alerts outrank `pr`; `info`
54
+ * is the floor. This ordering is LOCKED — do not reshuffle.
55
+ */
56
+ export const INDICATOR_PRECEDENCE: readonly ExerciseIndicatorKind[] = [
57
+ 'imbalance',
58
+ 'overshoot',
59
+ 'velocity-loss',
60
+ 'missed-reps',
61
+ 'pr',
62
+ 'info',
63
+ ]
64
+
65
+ /**
66
+ * Given the signals currently true for an exercise, pick the single
67
+ * highest-precedence kind to surface (or `undefined` when none apply). Order and
68
+ * duplicates in `candidates` don't matter — {@link INDICATOR_PRECEDENCE} decides.
69
+ */
70
+ export function resolveIndicator(
71
+ candidates: ExerciseIndicatorKind[],
72
+ ): ExerciseIndicatorKind | undefined {
73
+ return INDICATOR_PRECEDENCE.find((kind) => candidates.includes(kind))
74
+ }
75
+
76
+ export interface ExerciseIndicatorProps extends Omit<ViewProps, 'children'> {
77
+ kind: ExerciseIndicatorKind
78
+ /** Optional tap handler (surfaces detail — PR history / alert / info). */
79
+ onPress?: () => void
80
+ className?: string
81
+ }
82
+
83
+ /**
84
+ * A small circular outlined chip for an exercise heading. Presentational and
85
+ * static (chips are chrome — no pulse); the tap detail (tooltip/sheet) is wired
86
+ * by the consumer.
87
+ */
88
+ export function ExerciseIndicator({ kind, onPress, className, ...props }: ExerciseIndicatorProps) {
89
+ const { Icon, color, label } = INDICATOR_CONFIG[kind]
90
+
91
+ const chip = (
92
+ <View
93
+ className={className}
94
+ style={{
95
+ width: 16,
96
+ height: 16,
97
+ borderRadius: 8,
98
+ borderWidth: 1,
99
+ borderColor: color,
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ }}
103
+ accessibilityRole="image"
104
+ accessibilityLabel={label}
105
+ testID="exercise-indicator"
106
+ {...props}
107
+ >
108
+ <Icon size={11} color={color} />
109
+ </View>
110
+ )
111
+
112
+ if (onPress) {
113
+ return (
114
+ <Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel={label}>
115
+ {chip}
116
+ </Pressable>
117
+ )
118
+ }
119
+ return chip
120
+ }
@@ -0,0 +1,51 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { View } from 'react-native'
4
+ import { FatigueMeter } from './FatigueMeter'
5
+
6
+ const meta: Meta<typeof FatigueMeter> = {
7
+ title: 'Workout/DataViz/FatigueMeter',
8
+ component: FatigueMeter,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ value: { control: { type: 'range', min: 0, max: 40, step: 1 } },
12
+ },
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component:
17
+ '**Component.** The winning fatigue-visual lineage: a sliding needle over a fixed ' +
18
+ 'green→gold→orange→red velocity-loss gradient with VL10/VL20/VL30/stop markers. ' +
19
+ 'Composes the [ZoneTrack](?path=/docs/workout-dataviz-zonetrack--docs) primitive. ' +
20
+ 'Prop-driven: `value` plus overridable `thresholds` / `max` / `zoneColors` / `labels`.',
21
+ },
22
+ },
23
+ },
24
+ decorators: [
25
+ (Story) => (
26
+ <View style={{ width: 360, padding: 20, backgroundColor: '#131313' }}>
27
+ <Story />
28
+ </View>
29
+ ),
30
+ ],
31
+ }
32
+ export default meta
33
+
34
+ type Story = StoryObj<typeof FatigueMeter>
35
+
36
+ /** Fresh — needle sits in the green zone. */
37
+ export const Fresh: Story = { args: { value: 5 } }
38
+
39
+ /** VL20 — needle crossing into the orange zone. */
40
+ export const Vl20: Story = { args: { value: 21 } }
41
+
42
+ /** Stop zone — deep into the red. */
43
+ export const StopZone: Story = { args: { value: 34 } }
44
+
45
+ /** Chunky wall-glanceable density via a taller track. */
46
+ export const WallDensity: Story = { args: { value: 21, trackHeight: 22 } }
47
+
48
+ /** Custom scale — a tighter 0..30 domain with earlier thresholds. */
49
+ export const CustomScale: Story = {
50
+ args: { value: 12, max: 30, thresholds: [8, 16, 24], labels: ['ok', 'w1', 'w2', 'w3', 'max'] },
51
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { FatigueMeter } from './FatigueMeter'
5
+ import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
6
+
7
+ const { green, red } = WORKOUT_TOKENS.scale
8
+
9
+ describe('FatigueMeter', () => {
10
+ it('renders the four fatigue zone bands', () => {
11
+ render(<FatigueMeter value={0} />)
12
+ expect(screen.getAllByTestId('zone-track-band')).toHaveLength(4)
13
+ })
14
+
15
+ it('colours the bands from the canonical effort scale', () => {
16
+ render(<FatigueMeter value={0} />)
17
+ const bands = screen.getAllByTestId('zone-track-band')
18
+ expect(bands[0]).toHaveStyle({ backgroundColor: green })
19
+ expect(bands[3]).toHaveStyle({ backgroundColor: red })
20
+ })
21
+
22
+ it('renders the default VL scale labels', () => {
23
+ render(<FatigueMeter value={0} />)
24
+ for (const label of ['fresh', 'VL10', 'VL20', 'VL30', 'stop']) {
25
+ expect(screen.getByText(label)).toBeInTheDocument()
26
+ }
27
+ })
28
+
29
+ it('positions the needle at the value on the 0..40 loss scale', () => {
30
+ render(<FatigueMeter value={20} />)
31
+ expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '50%' })
32
+ })
33
+
34
+ it('clamps a value beyond the stop end to the track end', () => {
35
+ render(<FatigueMeter value={60} />)
36
+ expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '100%' })
37
+ })
38
+
39
+ it('reshapes the zones from custom thresholds + max', () => {
40
+ render(<FatigueMeter value={5} max={20} thresholds={[5, 10, 15]} />)
41
+ const bands = screen.getAllByTestId('zone-track-band')
42
+ expect(bands[0]).toHaveStyle({ flexGrow: 5 })
43
+ expect(bands[3]).toHaveStyle({ flexGrow: 5 })
44
+ })
45
+
46
+ it('honours custom labels', () => {
47
+ render(<FatigueMeter value={5} labels={['A', 'B', 'C', 'D', 'E']} />)
48
+ expect(screen.getByText('A')).toBeInTheDocument()
49
+ expect(screen.getByText('E')).toBeInTheDocument()
50
+ })
51
+
52
+ it('honours a needle colour override', () => {
53
+ render(<FatigueMeter value={10} needleColor="#FF00FF" />)
54
+ expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ backgroundColor: '#FF00FF' })
55
+ })
56
+
57
+ // RNW does not map accessibilityValue → aria-valuenow under jsdom (gotcha #11), so the
58
+ // loss value is verified via the needle position + the accessible name.
59
+ it('exposes the progressbar role with the loss value in its accessible name', () => {
60
+ render(<FatigueMeter value={17} />)
61
+ expect(
62
+ screen.getByRole('progressbar', { name: 'Fatigue: 17% velocity loss' })
63
+ ).toBeInTheDocument()
64
+ })
65
+
66
+ describe('accessibility', () => {
67
+ it('has no accessibility violations', async () => {
68
+ const { container } = render(<FatigueMeter value={21} />)
69
+ const results = await axe(container)
70
+ expect(results).toHaveNoViolations()
71
+ })
72
+ })
73
+ })
@@ -0,0 +1,82 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import type { ViewProps } from 'react-native'
3
+ import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
4
+ import { ZoneTrack, type ZoneTrackZone, type ZoneTrackTick } from './ZoneTrack'
5
+
6
+ const { green, yellow, orange, red } = WORKOUT_TOKENS.scale
7
+
8
+ /** Canonical fatigue band colours (fresh → stop) — the even effort-scale hue walk. */
9
+ const DEFAULT_ZONE_COLORS: [string, string, string, string] = [green, yellow, orange, red]
10
+ /** Zone boundaries in velocity-loss %, matching the VL10/VL20/VL30 coaching cues. */
11
+ const DEFAULT_THRESHOLDS: [number, number, number] = [10, 20, 30]
12
+ /** Scale labels at [min, ...thresholds, max]. */
13
+ const DEFAULT_LABELS = ['fresh', 'VL10', 'VL20', 'VL30', 'stop']
14
+ const DEFAULT_MAX = 40
15
+
16
+ export interface FatigueMeterProps extends ViewProps {
17
+ /** Current velocity-loss percentage (0 = fresh). Drives the needle position. */
18
+ value: number
19
+ /** Upper bound of the fatigue scale — the "stop" end, in loss %. Default 40. */
20
+ max?: number
21
+ /** Zone boundary thresholds (loss %): green→gold, gold→orange, orange→red. Default [10,20,30]. */
22
+ thresholds?: [number, number, number]
23
+ /** Zone band colours low→high (fresh→stop). Default the canonical effort scale. */
24
+ zoneColors?: [string, string, string, string]
25
+ /** Tick labels at [min, ...thresholds, max]. Default ['fresh','VL10','VL20','VL30','stop']. */
26
+ labels?: string[]
27
+ /** Track height in px. Default 14. */
28
+ trackHeight?: number
29
+ /** Needle colour. Default white. */
30
+ needleColor?: string
31
+ className?: string
32
+ }
33
+
34
+ /**
35
+ * A fatigue meter: a sliding needle over a fixed green→gold→orange→red velocity-loss
36
+ * gradient with VL10/VL20/VL30/stop scale markers. This is the winning fatigue-visual
37
+ * lineage (needle-over-zoned-gradient, three independent sources agreed) — it composes
38
+ * the shared {@link ZoneTrack} primitive, supplying the fatigue domain, zone bands from
39
+ * the effort-scale tokens, and the coaching-cue tick labels. Prop-driven: `value` plus
40
+ * overridable `thresholds` / `max` / `zoneColors` / `labels`.
41
+ *
42
+ * @example
43
+ * <FatigueMeter value={21} /> // needle in the VL20 (orange) zone
44
+ * <FatigueMeter value={8} max={40} thresholds={[10, 20, 30]} />
45
+ */
46
+ export function FatigueMeter({
47
+ value,
48
+ max = DEFAULT_MAX,
49
+ thresholds = DEFAULT_THRESHOLDS,
50
+ zoneColors = DEFAULT_ZONE_COLORS,
51
+ labels = DEFAULT_LABELS,
52
+ trackHeight = 14,
53
+ needleColor,
54
+ className,
55
+ ...props
56
+ }: FatigueMeterProps) {
57
+ const [t1, t2, t3] = thresholds
58
+ const zones: ZoneTrackZone[] = [
59
+ { upTo: t1, color: zoneColors[0] },
60
+ { upTo: t2, color: zoneColors[1] },
61
+ { upTo: t3, color: zoneColors[2] },
62
+ { upTo: max, color: zoneColors[3] },
63
+ ]
64
+
65
+ const tickValues = [0, t1, t2, t3, max]
66
+ const ticks: ZoneTrackTick[] = tickValues.map((v, i) => ({ value: v, label: labels[i] }))
67
+
68
+ return (
69
+ <ZoneTrack
70
+ className={className}
71
+ zones={zones}
72
+ min={0}
73
+ max={max}
74
+ trackHeight={trackHeight}
75
+ ticks={ticks}
76
+ marker={{ type: 'needle', value, color: needleColor }}
77
+ accessibilityLabel={`Fatigue: ${value}% velocity loss`}
78
+ testID="fatigue-meter"
79
+ {...props}
80
+ />
81
+ )
82
+ }
@@ -4,7 +4,7 @@ import { View } from 'react-native'
4
4
  import { InputBar } from './InputBar'
5
5
 
6
6
  const meta: Meta<typeof InputBar> = {
7
- title: 'Custom/Workout/InputBar',
7
+ title: 'Workout/InputBar',
8
8
  component: InputBar,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -2,6 +2,7 @@
2
2
  import React from 'react'
3
3
  import { View, Text, Pressable, TextInput } from 'react-native'
4
4
  import { resolveColor } from '../../../theme/resolve-color'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
6
 
6
7
  export interface InputBarProps {
7
8
  exerciseName: string
@@ -17,7 +18,7 @@ export interface InputBarProps {
17
18
  visible: boolean
18
19
  }
19
20
 
20
- const BRAND_PRIMARY = '#FF7900'
21
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
21
22
  const INPUT_CLASSNAME = 'bg-surface-raised border-border-strong text-text-primary'
22
23
 
23
24
  const inputStyle = {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { IntensityBar } from './IntensityBar'
4
4
 
5
5
  const meta: Meta<typeof IntensityBar> = {
6
- title: 'Custom/Workout/IntensityBar',
6
+ title: 'Workout/DataViz/IntensityBar',
7
7
  component: IntensityBar,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -44,19 +44,19 @@ describe('IntensityBar', () => {
44
44
  })
45
45
 
46
46
  describe('zone colors graded by true percentage', () => {
47
- it('renders teal below the target ramp (< 75%)', () => {
47
+ it('renders green below the target ramp (< 75%)', () => {
48
48
  render(<IntensityBar level={0.2} />)
49
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
49
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
50
50
  })
51
51
 
52
- it('stays teal just under the amber boundary', () => {
52
+ it('stays green just under the amber boundary', () => {
53
53
  render(<IntensityBar level={0.74} />)
54
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
54
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
55
55
  })
56
56
 
57
57
  it('renders amber approaching the target (75-99%)', () => {
58
58
  render(<IntensityBar level={0.75} />)
59
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#FFB020' })
59
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#F9B415' })
60
60
  })
61
61
 
62
62
  it('renders the brand target color exactly at 100%', () => {
@@ -71,12 +71,12 @@ describe('IntensityBar', () => {
71
71
 
72
72
  it('renders over-2 deep red at 120%', () => {
73
73
  render(<IntensityBar level={1.2} />)
74
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A62626' })
74
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A4221C' })
75
75
  })
76
76
 
77
77
  it('renders over-3 darkest red at 130%', () => {
78
78
  render(<IntensityBar level={1.3} />)
79
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7A1C1C' })
79
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7E1002' })
80
80
  })
81
81
  })
82
82
 
@@ -88,7 +88,7 @@ describe('IntensityBar', () => {
88
88
 
89
89
  it('colors the bulge to match the over-target zone', () => {
90
90
  render(<IntensityBar level={1.2} />)
91
- expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A62626' })
91
+ expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A4221C' })
92
92
  })
93
93
 
94
94
  it('does not render a bulge at or below target', () => {
@@ -8,6 +8,10 @@ import {
8
8
  type ViewStyle,
9
9
  type DimensionValue,
10
10
  } from 'react-native'
11
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
12
+ import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
13
+
14
+ const t = getSemanticColors('dark')
11
15
 
12
16
  export type IntensityBarOrientation = 'vertical' | 'horizontal'
13
17
 
@@ -34,12 +38,12 @@ const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(
34
38
 
35
39
  // Zone colors graded by TRUE percentage (level * 100).
36
40
  const ZONE = {
37
- building: '#14B8A6', // teal — below target ramp-up
38
- approaching: '#FFB020', // amber — nearing target
39
- target: '#FF7900', // brand — exactly at target
40
- over1: '#D14343', // red — mild over-target
41
- over2: '#A62626', // deep red — moderate over-target
42
- over3: '#7A1C1C', // darkest red — severe over-target
41
+ building: t['status-success'], // below target ramp-up
42
+ approaching: t['status-warning'], // nearing target
43
+ target: t['brand-primary'], // exactly at target
44
+ over1: t['status-error'], // mild over-target
45
+ over2: WORKOUT_TOKENS.intensity.over2, // moderate over-target
46
+ over3: WORKOUT_TOKENS.intensity.over3, // severe over-target
43
47
  } as const
44
48
 
45
49
  type OverTier = 0 | 1 | 2 | 3