@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
package/dist/index.js CHANGED
@@ -82,6 +82,72 @@ function DumbbellIcon(props) {
82
82
  function StarIcon(props) {
83
83
  return /* @__PURE__ */ jsx(SvgIcon, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" }) });
84
84
  }
85
+ function ActivityIcon(props) {
86
+ return /* @__PURE__ */ jsx(SvgIcon, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M22 12h-4l-3 9L9 3l-3 9H2" }) });
87
+ }
88
+ function HistoryIcon(props) {
89
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
90
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
91
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" }),
92
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l4 2" })
93
+ ] });
94
+ }
95
+ function LayersIcon(props) {
96
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
97
+ /* @__PURE__ */ jsx("path", { d: "m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z" }),
98
+ /* @__PURE__ */ jsx("path", { d: "m6.08 9.5-3.49 1.59a1 1 0 0 0 0 1.81l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59" }),
99
+ /* @__PURE__ */ jsx("path", { d: "m6.08 14.5-3.49 1.59a1 1 0 0 0 0 1.81l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59" })
100
+ ] });
101
+ }
102
+ function PersonStandingIcon(props) {
103
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
104
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1" }),
105
+ /* @__PURE__ */ jsx("path", { d: "m9 20 3-6 3 6" }),
106
+ /* @__PURE__ */ jsx("path", { d: "m6 8 6 2 6-2" }),
107
+ /* @__PURE__ */ jsx("path", { d: "M12 10v4" })
108
+ ] });
109
+ }
110
+ function ScaleIcon(props) {
111
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
112
+ /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
113
+ /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
114
+ /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
115
+ /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
116
+ /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
117
+ ] });
118
+ }
119
+ function AlertTriangleIcon(props) {
120
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
121
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
122
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
123
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
124
+ ] });
125
+ }
126
+ function TrendingDownIcon(props) {
127
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
128
+ /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
129
+ /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
130
+ ] });
131
+ }
132
+ function CircleSlashIcon(props) {
133
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
134
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
135
+ /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
136
+ ] });
137
+ }
138
+ function AwardIcon(props) {
139
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
140
+ /* @__PURE__ */ jsx("path", { d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" }),
141
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
142
+ ] });
143
+ }
144
+ function InfoIcon(props) {
145
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
146
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
147
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
148
+ /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
149
+ ] });
150
+ }
85
151
  function cn(...inputs) {
86
152
  return tailwindMerge.twMerge(clsx.clsx(inputs));
87
153
  }
@@ -92,6 +158,7 @@ var statusColors = {
92
158
  subtle: "bg-status-success-subtle",
93
159
  outline: "border-2 border-status-success bg-transparent",
94
160
  solid: "bg-status-success",
161
+ border: "border-status-success",
95
162
  icon: "text-status-success",
96
163
  text: "text-status-success"
97
164
  },
@@ -99,6 +166,7 @@ var statusColors = {
99
166
  subtle: "bg-status-info-subtle",
100
167
  outline: "border-2 border-status-info bg-transparent",
101
168
  solid: "bg-status-info",
169
+ border: "border-status-info",
102
170
  icon: "text-status-info",
103
171
  text: "text-status-info"
104
172
  },
@@ -106,6 +174,7 @@ var statusColors = {
106
174
  subtle: "bg-status-warning-subtle",
107
175
  outline: "border-2 border-status-warning bg-transparent",
108
176
  solid: "bg-status-warning",
177
+ border: "border-status-warning",
109
178
  icon: "text-status-warning",
110
179
  text: "text-status-warning"
111
180
  },
@@ -113,6 +182,7 @@ var statusColors = {
113
182
  subtle: "bg-status-error-subtle",
114
183
  outline: "border-2 border-status-error bg-transparent",
115
184
  solid: "bg-status-error",
185
+ border: "border-status-error",
116
186
  icon: "text-status-error",
117
187
  text: "text-status-error"
118
188
  }
@@ -126,6 +196,8 @@ var defaultIcons = {
126
196
  function Alert({
127
197
  status = "info",
128
198
  variant = "subtle",
199
+ size = "default",
200
+ message,
129
201
  icon,
130
202
  showIcon = true,
131
203
  onClose,
@@ -135,28 +207,43 @@ function Alert({
135
207
  }) {
136
208
  const colors = statusColors[status];
137
209
  const isSolid = variant === "solid";
210
+ const isCompact = size === "compact";
138
211
  return /* @__PURE__ */ jsxs(
139
212
  reactNative.View,
140
213
  {
141
214
  accessibilityRole: "alert",
142
215
  className: cn(
143
- "flex-row items-start p-4 rounded-lg",
216
+ isCompact ? "flex-row items-center px-3 py-2 rounded-lg" : "flex-row items-start p-4 rounded-lg",
144
217
  colors[variant],
218
+ // A compact `subtle` pill gets a hairline status border so it reads as a
219
+ // defined cue on a dark wall (the CueFlag look); other variants carry their own.
220
+ isCompact && variant === "subtle" && cn("border", colors.border),
145
221
  className
146
222
  ),
147
223
  ...props,
148
224
  children: [
149
- showIcon && /* @__PURE__ */ jsx(reactNative.View, { className: "mr-3 mt-0.5", children: icon || /* @__PURE__ */ jsx(
225
+ showIcon && /* @__PURE__ */ jsx(reactNative.View, { className: isCompact ? "mr-2" : "mr-3 mt-0.5", children: icon || /* @__PURE__ */ jsx(
150
226
  reactNative.Text,
151
227
  {
152
228
  className: cn(
153
- "text-lg font-bold",
229
+ "font-bold",
230
+ isCompact ? "text-base" : "text-lg",
154
231
  isSolid ? "text-white" : colors.icon
155
232
  ),
156
233
  children: defaultIcons[status]
157
234
  }
158
235
  ) }),
159
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", children }),
236
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-1", children: [
237
+ message != null && /* @__PURE__ */ jsx(
238
+ reactNative.Text,
239
+ {
240
+ className: cn("text-sm font-semibold", isSolid ? "text-white" : colors.text),
241
+ testID: "alert-message",
242
+ children: message
243
+ }
244
+ ),
245
+ children
246
+ ] }),
160
247
  onClose && /* @__PURE__ */ jsx(
161
248
  reactNative.Pressable,
162
249
  {
@@ -180,7 +267,7 @@ function AlertDescription({ children, className }) {
180
267
 
181
268
  // src/theme/tokens/primitives.ts
182
269
  var primitiveColors = {
183
- // Blue scale (primary brand)
270
+ // Blue scale
184
271
  blue: {
185
272
  50: "#EFF6FF",
186
273
  100: "#DBEAFE",
@@ -189,124 +276,36 @@ var primitiveColors = {
189
276
  400: "#60A5FA",
190
277
  500: "#3B82F6",
191
278
  600: "#5048E5",
192
- // Primary main
193
279
  700: "#3832A0",
194
- // Primary dark
195
280
  800: "#1E40AF",
196
281
  900: "#1E3A8A"
197
282
  },
198
- // Green scale (secondary brand)
199
- green: {
200
- 50: "#ECFDF5",
201
- 100: "#D1FAE5",
202
- 200: "#A7F3D0",
203
- 300: "#6EE7B7",
204
- 400: "#3FC79A",
205
- // Secondary light
206
- 500: "#10B981",
207
- // Secondary main
208
- 600: "#059669",
209
- 700: "#0B815A",
210
- // Secondary dark
211
- 800: "#065F46",
212
- 900: "#064E3B"
213
- },
214
- // Teal scale (success)
215
- teal: {
216
- 50: "#F0FDFA",
217
- 100: "#CCFBF1",
218
- 200: "#99F6E4",
219
- 300: "#5EEAD4",
220
- 400: "#43C6B7",
221
- // Success light
222
- 500: "#14B8A6",
223
- // Success main
224
- 600: "#0D9488",
225
- 700: "#0E8074",
226
- // Success dark
227
- 800: "#115E59",
228
- 900: "#134E4A"
229
- },
230
- // Red scale (error)
283
+ // Red scale
231
284
  red: {
232
285
  50: "#FEF2F2",
233
286
  100: "#FEE2E2",
234
287
  200: "#FECACA",
235
288
  300: "#FCA5A5",
236
289
  400: "#DA6868",
237
- // Error light
238
290
  500: "#EF4444",
239
291
  600: "#D14343",
240
- // Error main
241
292
  700: "#922E2E",
242
- // Error dark
243
293
  800: "#991B1B",
244
294
  900: "#7F1D1D"
245
295
  },
246
- // Vivid green scale (live/go success)
247
- greenVivid: {
248
- 50: "#E9FBF0",
249
- 100: "#C8F7DA",
250
- 200: "#9FF0BF",
251
- 300: "#6BE79D",
252
- 400: "#47DE84",
253
- // Success vivid light
254
- 500: "#2ED573",
255
- // Success vivid main
256
- 600: "#22B85F",
257
- 700: "#1A9950",
258
- // Success vivid dark
259
- 800: "#157A40",
260
- 900: "#105C30"
261
- },
262
- // Vivid red scale (alert)
296
+ // Vivid red scale
263
297
  redVivid: {
264
298
  50: "#FFECEE",
265
299
  100: "#FFD6DB",
266
300
  200: "#FFB3BC",
267
301
  300: "#FF8593",
268
302
  400: "#FF6070",
269
- // Error vivid light
270
303
  500: "#FF4757",
271
- // Error vivid main
272
304
  600: "#E63548",
273
305
  700: "#C42539",
274
- // Error vivid dark
275
306
  800: "#9E1C2C",
276
307
  900: "#7A1520"
277
308
  },
278
- // Amber scale (warning)
279
- amber: {
280
- 50: "#FFFBEB",
281
- 100: "#FEF3C7",
282
- 200: "#FDE68A",
283
- 300: "#FCD34D",
284
- 400: "#FFBF4C",
285
- // Warning light
286
- 500: "#FFB020",
287
- // Warning main
288
- 600: "#B27B16",
289
- // Warning dark
290
- 700: "#B45309",
291
- 800: "#92400E",
292
- 900: "#78350F"
293
- },
294
- // Sky/Blue scale (info)
295
- sky: {
296
- 50: "#F0F9FF",
297
- 100: "#E0F2FE",
298
- 200: "#BAE6FD",
299
- 300: "#7DD3FC",
300
- 400: "#64B6F7",
301
- // Info light
302
- 500: "#2196F3",
303
- // Info main
304
- 600: "#0284C7",
305
- 700: "#0B79D0",
306
- // Info dark
307
- 800: "#075985",
308
- 900: "#0C4A6E"
309
- },
310
309
  // Neutral/Gray scale
311
310
  neutral: {
312
311
  50: "#FAFAFA",
@@ -321,18 +320,6 @@ var primitiveColors = {
321
320
  900: "#111827",
322
321
  950: "#030712"
323
322
  },
324
- // Orange scale (accent/brand color)
325
- accent: {
326
- 100: "#FFA860",
327
- 200: "#FF9630",
328
- 300: "#FF8500",
329
- 400: "#FF7900",
330
- 500: "#F56C00",
331
- 600: "#E06D10",
332
- 700: "#D0620C",
333
- 800: "#B75500",
334
- 900: "#A34900"
335
- },
336
323
  // Charcoal scale (dark backgrounds)
337
324
  charcoal: {
338
325
  0: "#6E6E6E",
@@ -347,18 +334,6 @@ var primitiveColors = {
347
334
  800: "#131313",
348
335
  900: "#101010"
349
336
  },
350
- // Steel scale (cool accent)
351
- steel: {
352
- 100: "#8AA4B8",
353
- 200: "#7894A8",
354
- 300: "#678498",
355
- 400: "#557488",
356
- 500: "#406D87",
357
- 600: "#39617A",
358
- 700: "#32556D",
359
- 800: "#243D53",
360
- 900: "#1D3146"
361
- },
362
337
  // Pure colors
363
338
  white: "#FFFFFF",
364
339
  black: "#000000",
@@ -749,6 +724,9 @@ var semanticColorsLight = {
749
724
  "status-success-light": primitiveRamps.green[200],
750
725
  "status-success-dark": primitiveRamps.green[600],
751
726
  "status-success-subtle": primitiveRamps.green[50],
727
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
728
+ "status-live": primitiveRamps.green[300],
729
+ "status-live-muted": primitiveRamps.green[500],
752
730
  "status-error": primitiveRamps.red[600],
753
731
  "status-error-light": primitiveRamps.red[500],
754
732
  "status-error-dark": primitiveRamps.red[700],
@@ -882,6 +860,9 @@ var semanticColorsDark = {
882
860
  "status-success-light": primitiveRamps.green[200],
883
861
  "status-success-dark": primitiveRamps.green[600],
884
862
  "status-success-subtle": "rgba(46, 213, 115, 0.12)",
863
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
864
+ "status-live": primitiveRamps.green[300],
865
+ "status-live-muted": primitiveRamps.green[500],
885
866
  "status-error": primitiveRamps.red[600],
886
867
  "status-error-light": primitiveRamps.red[500],
887
868
  "status-error-dark": primitiveRamps.red[700],
@@ -1476,6 +1457,8 @@ var lightThemeCSSVars = {
1476
1457
  "--color-on-brand-secondary": semanticColorsLight["on-brand-secondary"],
1477
1458
  "--color-status-success": semanticColorsLight["status-success"],
1478
1459
  "--color-status-success-subtle": semanticColorsLight["status-success-subtle"],
1460
+ "--color-status-live": semanticColorsLight["status-live"],
1461
+ "--color-status-live-muted": semanticColorsLight["status-live-muted"],
1479
1462
  "--color-status-error": semanticColorsLight["status-error"],
1480
1463
  "--color-status-error-subtle": semanticColorsLight["status-error-subtle"],
1481
1464
  "--color-status-warning": semanticColorsLight["status-warning"],
@@ -1572,6 +1555,8 @@ var darkThemeCSSVars = {
1572
1555
  "--color-on-brand-secondary": semanticColorsDark["on-brand-secondary"],
1573
1556
  "--color-status-success": semanticColorsDark["status-success"],
1574
1557
  "--color-status-success-subtle": semanticColorsDark["status-success-subtle"],
1558
+ "--color-status-live": semanticColorsDark["status-live"],
1559
+ "--color-status-live-muted": semanticColorsDark["status-live-muted"],
1575
1560
  "--color-status-error": semanticColorsDark["status-error"],
1576
1561
  "--color-status-error-subtle": semanticColorsDark["status-error-subtle"],
1577
1562
  "--color-status-warning": semanticColorsDark["status-warning"],
@@ -2789,6 +2774,7 @@ var colorStyles = {
2789
2774
  default: "bg-text-tertiary",
2790
2775
  primary: "bg-brand-primary",
2791
2776
  success: "bg-status-success",
2777
+ live: "bg-status-live",
2792
2778
  error: "bg-status-error",
2793
2779
  warning: "bg-status-warning",
2794
2780
  info: "bg-status-info",
@@ -2845,7 +2831,7 @@ function Indicator({
2845
2831
  colorClass,
2846
2832
  pulseMode === "opacity" && "animate-pulse",
2847
2833
  ring && "border-2 border-background-base",
2848
- glow && !customColor && color === "success" && "shadow-[0_0_6px_rgba(46,213,115,0.6)]",
2834
+ glow && !customColor && (color === "success" || color === "live") && "shadow-[0_0_6px_rgba(46,213,115,0.6)]",
2849
2835
  glow && !customColor && color === "error" && "shadow-[0_0_6px_rgba(239,68,68,0.6)]",
2850
2836
  glow && !customColor && color === "warning" && "shadow-[0_0_6px_rgba(245,158,11,0.6)]",
2851
2837
  glow && !customColor && color === "primary" && "shadow-[0_0_6px_rgba(255,121,0,0.6)]",
@@ -5107,6 +5093,8 @@ function CircularProgress({
5107
5093
  color = "primary",
5108
5094
  showValue = false,
5109
5095
  formatValue: formatValue3 = (v, m) => `${Math.round(v / m * 100)}%`,
5096
+ children,
5097
+ fill = false,
5110
5098
  className,
5111
5099
  ...props
5112
5100
  }) {
@@ -5115,11 +5103,12 @@ function CircularProgress({
5115
5103
  const circumference = 2 * Math.PI * radius;
5116
5104
  const strokeDashoffset = circumference - percentage / 100 * circumference;
5117
5105
  const center = size / 2;
5106
+ const boxStyle = fill ? { width: "100%", aspectRatio: 1 } : { width: size, height: size };
5118
5107
  return /* @__PURE__ */ jsxs(
5119
5108
  reactNative.View,
5120
5109
  {
5121
5110
  className: cn("items-center justify-center", className),
5122
- style: { width: size, height: size },
5111
+ style: boxStyle,
5123
5112
  accessibilityRole: "progressbar",
5124
5113
  accessibilityValue: {
5125
5114
  min: 0,
@@ -5128,52 +5117,56 @@ function CircularProgress({
5128
5117
  },
5129
5118
  ...props,
5130
5119
  children: [
5131
- /* @__PURE__ */ jsx(
5120
+ /* @__PURE__ */ jsx(reactNative.View, { className: cn(isIndeterminate && "animate-spin"), style: boxStyle, children: /* @__PURE__ */ jsxs(
5121
+ "svg",
5122
+ {
5123
+ width: "100%",
5124
+ height: "100%",
5125
+ viewBox: `0 0 ${size} ${size}`,
5126
+ style: { position: "absolute" },
5127
+ children: [
5128
+ /* @__PURE__ */ jsx(
5129
+ "circle",
5130
+ {
5131
+ cx: center,
5132
+ cy: center,
5133
+ r: radius,
5134
+ fill: "none",
5135
+ stroke: "var(--color-border-default)",
5136
+ strokeWidth
5137
+ }
5138
+ ),
5139
+ /* @__PURE__ */ jsx(
5140
+ "circle",
5141
+ {
5142
+ cx: center,
5143
+ cy: center,
5144
+ r: radius,
5145
+ fill: "none",
5146
+ stroke: colorVarMap[color],
5147
+ strokeWidth,
5148
+ strokeLinecap: "round",
5149
+ strokeDasharray: circumference,
5150
+ strokeDashoffset: isIndeterminate ? circumference * 0.75 : strokeDashoffset,
5151
+ transform: `rotate(-90 ${center} ${center})`,
5152
+ style: { transition: "stroke-dashoffset 300ms cubic-bezier(0.22, 1, 0.36, 1)" }
5153
+ }
5154
+ )
5155
+ ]
5156
+ }
5157
+ ) }),
5158
+ children != null ? /* @__PURE__ */ jsx(
5132
5159
  reactNative.View,
5133
5160
  {
5134
- className: cn(isIndeterminate && "animate-spin"),
5135
- style: { width: size, height: size },
5136
- children: /* @__PURE__ */ jsxs(
5137
- "svg",
5138
- {
5139
- width: size,
5140
- height: size,
5141
- viewBox: `0 0 ${size} ${size}`,
5142
- style: { position: "absolute" },
5143
- children: [
5144
- /* @__PURE__ */ jsx(
5145
- "circle",
5146
- {
5147
- cx: center,
5148
- cy: center,
5149
- r: radius,
5150
- fill: "none",
5151
- stroke: "var(--color-border-default)",
5152
- strokeWidth
5153
- }
5154
- ),
5155
- /* @__PURE__ */ jsx(
5156
- "circle",
5157
- {
5158
- cx: center,
5159
- cy: center,
5160
- r: radius,
5161
- fill: "none",
5162
- stroke: colorVarMap[color],
5163
- strokeWidth,
5164
- strokeLinecap: "round",
5165
- strokeDasharray: circumference,
5166
- strokeDashoffset: isIndeterminate ? circumference * 0.75 : strokeDashoffset,
5167
- transform: `rotate(-90 ${center} ${center})`,
5168
- style: { transition: "stroke-dashoffset 300ms cubic-bezier(0.22, 1, 0.36, 1)" }
5169
- }
5170
- )
5171
- ]
5172
- }
5173
- )
5161
+ style: {
5162
+ position: "absolute",
5163
+ alignItems: "center",
5164
+ justifyContent: "center",
5165
+ pointerEvents: "none"
5166
+ },
5167
+ children
5174
5168
  }
5175
- ),
5176
- showValue && !isIndeterminate && /* @__PURE__ */ jsx(
5169
+ ) : showValue && !isIndeterminate && /* @__PURE__ */ jsx(
5177
5170
  reactNative.Text,
5178
5171
  {
5179
5172
  className: "text-xs font-semibold text-text-primary absolute",
@@ -5269,7 +5262,7 @@ function ToastProvider({
5269
5262
  return id;
5270
5263
  }, [defaultDuration, maxToasts]);
5271
5264
  const removeToast = React24.useCallback((id) => {
5272
- setToasts((prev) => prev.filter((t12) => t12.id !== id));
5265
+ setToasts((prev) => prev.filter((t18) => t18.id !== id));
5273
5266
  }, []);
5274
5267
  const removeAllToasts = React24.useCallback(() => {
5275
5268
  setToasts([]);
@@ -6138,6 +6131,50 @@ function Pill({
6138
6131
  }
6139
6132
  return /* @__PURE__ */ jsx(reactNative.View, { className: containerClasses, ...props, children: content });
6140
6133
  }
6134
+ function Tile({
6135
+ label,
6136
+ value,
6137
+ valueColor,
6138
+ align = "center",
6139
+ className,
6140
+ ...props
6141
+ }) {
6142
+ const isCenter = align === "center";
6143
+ return /* @__PURE__ */ jsxs(
6144
+ reactNative.View,
6145
+ {
6146
+ className: cn(
6147
+ "flex-1 rounded-md bg-surface-raised px-1.5 py-2",
6148
+ isCenter ? "items-center" : "items-start",
6149
+ className
6150
+ ),
6151
+ ...props,
6152
+ children: [
6153
+ /* @__PURE__ */ jsx(
6154
+ reactNative.Text,
6155
+ {
6156
+ className: cn(
6157
+ "text-[10px] font-bold uppercase tracking-wider text-text-tertiary",
6158
+ isCenter ? "text-center" : "text-left"
6159
+ ),
6160
+ children: label
6161
+ }
6162
+ ),
6163
+ /* @__PURE__ */ jsx(
6164
+ reactNative.Text,
6165
+ {
6166
+ className: cn(
6167
+ "mt-0.5 font-mono text-sm font-bold text-text-primary",
6168
+ isCenter ? "text-center" : "text-left"
6169
+ ),
6170
+ style: valueColor ? { color: valueColor } : void 0,
6171
+ children: value
6172
+ }
6173
+ )
6174
+ ]
6175
+ }
6176
+ );
6177
+ }
6141
6178
  var variantStyles4 = {
6142
6179
  h1: "font-heading text-5xl font-bold leading-tight",
6143
6180
  h2: "font-heading text-4xl font-bold leading-tight",
@@ -6997,6 +7034,143 @@ function formatDateTime(value, format = "datetime", isUTC = false, fallback = "-
6997
7034
  }
6998
7035
  return formatDate(value, format, isUTC);
6999
7036
  }
7037
+ function formatDuration(ms) {
7038
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1e3));
7039
+ const minutes = Math.floor(totalSeconds / 60);
7040
+ const seconds = totalSeconds % 60;
7041
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
7042
+ }
7043
+ function useTimer(options = {}) {
7044
+ const {
7045
+ mode = "up",
7046
+ durationMs,
7047
+ elapsedMs: controlledElapsedMs,
7048
+ running = false,
7049
+ autoTick = false,
7050
+ tickMs = 1e3
7051
+ } = options;
7052
+ const isControlled = controlledElapsedMs != null;
7053
+ const [internalElapsedMs, setInternalElapsedMs] = React24.useState(0);
7054
+ React24.useEffect(() => {
7055
+ if (isControlled || !autoTick || !running) return;
7056
+ const id = setInterval(() => {
7057
+ setInternalElapsedMs((prev) => prev + tickMs);
7058
+ }, tickMs);
7059
+ return () => clearInterval(id);
7060
+ }, [isControlled, autoTick, running, tickMs]);
7061
+ const elapsedMs = isControlled ? controlledElapsedMs : internalElapsedMs;
7062
+ const hasDuration = durationMs != null && durationMs > 0;
7063
+ const remainingMs = durationMs != null ? Math.max(0, durationMs - elapsedMs) : 0;
7064
+ let progress;
7065
+ if (hasDuration) {
7066
+ progress = Math.min(1, Math.max(0, elapsedMs / durationMs));
7067
+ } else {
7068
+ progress = mode === "down" ? 1 : 0;
7069
+ }
7070
+ const label = formatDuration(mode === "down" ? remainingMs : elapsedMs);
7071
+ const done = durationMs != null && elapsedMs >= durationMs;
7072
+ return { elapsedMs, remainingMs, progress, label, done };
7073
+ }
7074
+
7075
+ // src/components/custom/TimerReadout/TimerReadout.tsx
7076
+ var semantic = getSemanticColors("dark");
7077
+ var LIVE_GREEN = primitiveRamps.green[500];
7078
+ var SECONDARY = semantic["text-secondary"];
7079
+ var TERTIARY = semantic["text-tertiary"];
7080
+ var DEFAULT_GLYPH = "\u23F1";
7081
+ function TimerReadout({
7082
+ mode = "up",
7083
+ elapsedMs,
7084
+ durationMs,
7085
+ running = false,
7086
+ autoTick = false,
7087
+ showTotal = false,
7088
+ glyph = DEFAULT_GLYPH,
7089
+ className,
7090
+ ...rest
7091
+ }) {
7092
+ const { label } = useTimer({ mode, durationMs, elapsedMs, running, autoTick });
7093
+ const currentColor = running ? LIVE_GREEN : SECONDARY;
7094
+ const total = showTotal && durationMs != null ? formatDuration(durationMs) : null;
7095
+ return /* @__PURE__ */ jsx(
7096
+ reactNative.View,
7097
+ {
7098
+ accessibilityRole: "timer",
7099
+ accessibilityLabel: total != null ? `${label} of ${total}` : label,
7100
+ className: cn("flex-row items-baseline justify-end", className),
7101
+ ...rest,
7102
+ children: /* @__PURE__ */ jsxs(Typography, { variant: "mono", style: { fontSize: 11, textAlign: "right" }, children: [
7103
+ /* @__PURE__ */ jsx(reactNative.Text, { testID: "timer-readout-glyph", style: { color: currentColor, marginRight: 3 }, children: glyph }),
7104
+ /* @__PURE__ */ jsx(reactNative.Text, { testID: "timer-readout-current", style: { color: currentColor }, children: label }),
7105
+ total != null ? /* @__PURE__ */ jsx(reactNative.Text, { testID: "timer-readout-total", style: { color: TERTIARY }, children: ` / ${total}` }) : null
7106
+ ] })
7107
+ }
7108
+ );
7109
+ }
7110
+ function CircularTimer({
7111
+ durationMs,
7112
+ elapsedMs,
7113
+ mode = "down",
7114
+ size = 160,
7115
+ strokeWidth = 8,
7116
+ fill = false,
7117
+ color = "primary",
7118
+ doneLabel,
7119
+ doneColor = "success",
7120
+ controls,
7121
+ accessibilityLabel: accessibilityLabel2,
7122
+ testID
7123
+ }) {
7124
+ const { remainingMs, progress, label, done } = useTimer({ mode, durationMs, elapsedMs });
7125
+ const isDone = done && mode === "down";
7126
+ const showDone = isDone && doneLabel != null;
7127
+ const remainingFrac = durationMs > 0 ? Math.max(0, Math.min(1, remainingMs / durationMs)) : 0;
7128
+ const arc = mode === "down" ? showDone ? 1 : remainingFrac : progress;
7129
+ const ringColor = showDone ? doneColor : color;
7130
+ const remainingSec = Math.ceil(Math.max(0, remainingMs) / 1e3);
7131
+ const a11y = accessibilityLabel2 ?? (isDone ? "Timer complete" : `${remainingSec} seconds remaining`);
7132
+ return /* @__PURE__ */ jsxs(
7133
+ reactNative.View,
7134
+ {
7135
+ style: { alignItems: "center", gap: 16 },
7136
+ accessibilityRole: "timer",
7137
+ accessibilityLabel: a11y,
7138
+ testID: testID ?? "circular-timer",
7139
+ children: [
7140
+ /* @__PURE__ */ jsx(
7141
+ CircularProgress,
7142
+ {
7143
+ value: arc,
7144
+ max: 1,
7145
+ size,
7146
+ strokeWidth,
7147
+ fill,
7148
+ color: ringColor,
7149
+ accessibilityLabel: "Timer ring",
7150
+ testID: "circular-timer-ring",
7151
+ children: /* @__PURE__ */ jsx(
7152
+ reactNative.Text,
7153
+ {
7154
+ className: showDone ? void 0 : "text-text-primary",
7155
+ style: {
7156
+ fontSize: Math.round(size * (showDone ? 0.28 : 0.24)),
7157
+ fontFamily: '"Space Grotesk", sans-serif',
7158
+ fontWeight: "800",
7159
+ fontVariant: ["tabular-nums"],
7160
+ letterSpacing: showDone ? 1 : -1,
7161
+ ...showDone ? { color: colorVarMap[doneColor] } : null
7162
+ },
7163
+ testID: "circular-timer-label",
7164
+ children: showDone ? doneLabel : label
7165
+ }
7166
+ )
7167
+ }
7168
+ ),
7169
+ controls
7170
+ ]
7171
+ }
7172
+ );
7173
+ }
7000
7174
  var sizeConfig2 = {
7001
7175
  sm: { value: "text-lg font-bold", label: "text-xs", unit: "text-xs" },
7002
7176
  md: { value: "text-2xl font-bold", label: "text-xs", unit: "text-sm" },
@@ -7426,15 +7600,22 @@ var WORKOUT_TOKENS = {
7426
7600
  // Canonical 4-band performance scale — the single source for BOTH the
7427
7601
  // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
7428
7602
  // is intentionally inverted between the two consumers: for velocity, green =
7429
- // fastest/best; for RPE, green = easiest. This is the [0,1,2,4] subsample of
7430
- // the canonical `sequentialEffort` primitive. The `orange` key is the band
7431
- // label. Consumed inline (RN).
7603
+ // fastest/best; for RPE, green = easiest. This is the [0,2,3,4] subsample of
7604
+ // the canonical `sequentialEffort` primitive: an EVEN hue walk across the ramp
7605
+ // (green gold → orange → red). The earlier [0,1,2,4] spent two of its four
7606
+ // bands on adjacent yellows (amber-200 + amber-300) and skipped orange, reading
7607
+ // yellow-heavy with an abrupt jump to red; sampling amber-300 + orange-400
7608
+ // instead gives four cleanly separable bands that hold at 3px strip scale.
7609
+ // Consumed inline (RN).
7432
7610
  scale: {
7433
7611
  green: sequentialEffort[0],
7434
- yellow: sequentialEffort[1],
7435
- orange: sequentialEffort[2],
7436
- // gold band
7612
+ // green-300
7613
+ yellow: sequentialEffort[2],
7614
+ // amber-300 (gold)
7615
+ orange: sequentialEffort[3],
7616
+ // orange-400 (true orange)
7437
7617
  red: sequentialEffort[4]
7618
+ // red-600
7438
7619
  },
7439
7620
  // BodyMap volume heatmap — the canonical `divergingScale` (under → optimal →
7440
7621
  // over): a true diverging shape with a light green center, cool-blue under-
@@ -7559,8 +7740,8 @@ function roundTempo(tempo) {
7559
7740
  return tempo.map((v) => Math.round(v * 10) / 10);
7560
7741
  }
7561
7742
  function formatSignedPct(ratio) {
7562
- const pct = Math.round(ratio * 100);
7563
- return `${pct > 0 ? "+" : ""}${pct}%`;
7743
+ const pct2 = Math.round(ratio * 100);
7744
+ return `${pct2 > 0 ? "+" : ""}${pct2}%`;
7564
7745
  }
7565
7746
  function formatPrescription(p) {
7566
7747
  if (p == null) return null;
@@ -7640,6 +7821,11 @@ var TEMPO_PACING = {
7640
7821
  behindThresholdPct: 0.15,
7641
7822
  minPhaseDurationMs: 500
7642
7823
  };
7824
+ function formatTempoDelta(remainingMs) {
7825
+ const s = remainingMs / 1e3;
7826
+ const rounded = Math.abs(s) < 0.05 ? 0 : s;
7827
+ return rounded.toFixed(1);
7828
+ }
7643
7829
  function getTempoPacingState(elapsedMs, targetMs) {
7644
7830
  if (!targetMs || targetMs < TEMPO_PACING.minPhaseDurationMs) return "none";
7645
7831
  const ratio = elapsedMs / targetMs;
@@ -7652,24 +7838,33 @@ function getTempoFillPct(elapsedMs, targetMs) {
7652
7838
  }
7653
7839
  var PHASE_ORDER = ["concentric", "hold", "eccentric"];
7654
7840
  var PHASE_CONFIG = {
7655
- concentric: { label: "Con", color: t2["status-success"], flex: 2 },
7656
- hold: { label: "Hold", color: t2["brand-primary"], flex: 1 },
7657
- eccentric: { label: "Ecc", color: t2["status-warning"], flex: 3 }
7841
+ concentric: { label: "Con", wallLabel: "Concentric", color: t2["status-success"], flex: 2 },
7842
+ hold: { label: "Hold", wallLabel: "Hold", color: t2["brand-primary"], flex: 1 },
7843
+ eccentric: { label: "Ecc", wallLabel: "Eccentric", color: t2["status-warning"], flex: 3 }
7844
+ };
7845
+ var TEMPO_SIZES = {
7846
+ default: { labelMargin: 4, labelFont: 10, barGap: 2, barHeight: 20, radius: 4, segFont: 12 },
7847
+ wall: { labelMargin: 8, labelFont: 15, barGap: 4, barHeight: 40, radius: 6, segFont: 22 }
7658
7848
  };
7659
7849
  function TempoBar({
7660
7850
  activePhase,
7661
7851
  phaseElapsedMs,
7662
7852
  completed,
7663
7853
  target,
7854
+ size = "default",
7855
+ activeDisplay = "time",
7664
7856
  className,
7665
7857
  ...props
7666
7858
  }) {
7859
+ const s = TEMPO_SIZES[size];
7860
+ const [display, setDisplay] = React24.useState(activeDisplay);
7861
+ const toggleDisplay = () => setDisplay((d) => d === "time" ? "delta" : "time");
7667
7862
  return /* @__PURE__ */ jsxs(reactNative.View, { className, testID: "tempo-bar", ...props, children: [
7668
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", marginBottom: 4 }, children: PHASE_ORDER.map((phase) => {
7863
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", marginBottom: s.labelMargin }, children: PHASE_ORDER.map((phase) => {
7669
7864
  const config = PHASE_CONFIG[phase];
7670
- return /* @__PURE__ */ jsx(reactNative.View, { style: { flex: config.flex, alignItems: "center" }, children: /* @__PURE__ */ jsx(reactNative.Text, { style: { fontSize: 10, color: t2["text-disabled"] }, children: config.label }) }, phase);
7865
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { flex: config.flex, alignItems: "center" }, children: /* @__PURE__ */ jsx(reactNative.Text, { numberOfLines: 1, style: { fontSize: s.labelFont, color: t2["text-disabled"] }, children: size === "wall" ? config.wallLabel : config.label }) }, phase);
7671
7866
  }) }),
7672
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", gap: 2, height: 20 }, children: PHASE_ORDER.map((phase) => {
7867
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", gap: s.barGap, height: s.barHeight }, children: PHASE_ORDER.map((phase) => {
7673
7868
  const config = PHASE_CONFIG[phase];
7674
7869
  const isActive = activePhase === phase;
7675
7870
  const completedMs = completed?.[phase];
@@ -7681,7 +7876,7 @@ function TempoBar({
7681
7876
  style: {
7682
7877
  flex: config.flex,
7683
7878
  backgroundColor: alpha("#ffffff", 0.06),
7684
- borderRadius: 4,
7879
+ borderRadius: s.radius,
7685
7880
  overflow: "hidden"
7686
7881
  },
7687
7882
  children: isActive ? /* @__PURE__ */ jsx(
@@ -7689,9 +7884,21 @@ function TempoBar({
7689
7884
  {
7690
7885
  config,
7691
7886
  phaseElapsedMs,
7692
- targetMs
7887
+ targetMs,
7888
+ radius: s.radius,
7889
+ font: s.segFont,
7890
+ display,
7891
+ onToggleDisplay: toggleDisplay
7892
+ }
7893
+ ) : completedMs != null ? /* @__PURE__ */ jsx(
7894
+ CompletedSegment,
7895
+ {
7896
+ config,
7897
+ completedMs,
7898
+ targetMs,
7899
+ font: s.segFont
7693
7900
  }
7694
- ) : completedMs != null ? /* @__PURE__ */ jsx(CompletedSegment, { config, completedMs, targetMs }) : null
7901
+ ) : null
7695
7902
  },
7696
7903
  phase
7697
7904
  );
@@ -7701,49 +7908,65 @@ function TempoBar({
7701
7908
  function ActiveSegment({
7702
7909
  config,
7703
7910
  phaseElapsedMs,
7704
- targetMs
7911
+ targetMs,
7912
+ radius,
7913
+ font,
7914
+ display,
7915
+ onToggleDisplay
7705
7916
  }) {
7706
7917
  const pacing = getTempoPacingState(phaseElapsedMs, targetMs);
7707
7918
  const barColor = pacing === "behind" ? t2["status-error"] : config.color;
7708
7919
  const fillPct = getTempoFillPct(phaseElapsedMs, targetMs);
7709
- const label = targetMs ? `${formatDuration(phaseElapsedMs)} / ${formatDuration(targetMs)}` : formatDuration(phaseElapsedMs);
7710
- return /* @__PURE__ */ jsxs(reactNative.View, { style: { height: "100%", position: "relative" }, testID: `tempo-segment-active-${config.label}`, children: [
7711
- /* @__PURE__ */ jsx(
7712
- reactNative.View,
7713
- {
7714
- style: {
7715
- position: "absolute",
7716
- left: 0,
7717
- top: 0,
7718
- bottom: 0,
7719
- width: `${fillPct}%`,
7720
- backgroundColor: alpha(barColor, 0.3),
7721
- borderRadius: 4
7722
- }
7723
- }
7724
- ),
7725
- /* @__PURE__ */ jsx(
7726
- reactNative.View,
7727
- {
7728
- style: {
7729
- height: "100%",
7730
- flexDirection: "row",
7731
- alignItems: "center",
7732
- justifyContent: "center"
7733
- },
7734
- children: /* @__PURE__ */ jsx(reactNative.Text, { style: { fontSize: 12, fontWeight: "700", color: barColor }, children: label })
7735
- }
7736
- )
7737
- ] });
7920
+ const label = display === "delta" && targetMs != null ? formatTempoDelta(targetMs - phaseElapsedMs) : targetMs != null ? `${formatDuration2(phaseElapsedMs)} / ${formatDuration2(targetMs)}` : formatDuration2(phaseElapsedMs);
7921
+ return /* @__PURE__ */ jsxs(
7922
+ reactNative.Pressable,
7923
+ {
7924
+ onPress: onToggleDisplay,
7925
+ style: { height: "100%", position: "relative" },
7926
+ accessibilityRole: "button",
7927
+ accessibilityLabel: `Tempo readout: ${display === "delta" ? "delta" : "time"} \u2014 tap to switch`,
7928
+ testID: `tempo-segment-active-${config.label}`,
7929
+ children: [
7930
+ /* @__PURE__ */ jsx(
7931
+ reactNative.View,
7932
+ {
7933
+ style: {
7934
+ position: "absolute",
7935
+ left: 0,
7936
+ top: 0,
7937
+ bottom: 0,
7938
+ width: `${fillPct}%`,
7939
+ backgroundColor: alpha(barColor, 0.3),
7940
+ borderRadius: radius
7941
+ }
7942
+ }
7943
+ ),
7944
+ /* @__PURE__ */ jsx(
7945
+ reactNative.View,
7946
+ {
7947
+ style: {
7948
+ height: "100%",
7949
+ flexDirection: "row",
7950
+ alignItems: "center",
7951
+ justifyContent: "center"
7952
+ },
7953
+ children: /* @__PURE__ */ jsx(reactNative.Text, { style: { fontSize: font, fontWeight: "700", color: barColor }, children: label })
7954
+ }
7955
+ )
7956
+ ]
7957
+ }
7958
+ );
7738
7959
  }
7739
7960
  function CompletedSegment({
7740
7961
  config,
7741
7962
  completedMs,
7742
- targetMs
7963
+ targetMs,
7964
+ font
7743
7965
  }) {
7744
7966
  const pacing = getTempoPacingState(completedMs, targetMs);
7745
7967
  const hitTarget = pacing !== "behind";
7746
7968
  const indicator = targetMs && targetMs >= TEMPO_PACING.minPhaseDurationMs ? hitTarget ? " \u2713" : " \u2717" : "";
7969
+ const cueColor = hitTarget ? config.color : t2["status-error"];
7747
7970
  return /* @__PURE__ */ jsx(
7748
7971
  reactNative.View,
7749
7972
  {
@@ -7752,20 +7975,46 @@ function CompletedSegment({
7752
7975
  flexDirection: "row",
7753
7976
  alignItems: "center",
7754
7977
  justifyContent: "center",
7755
- backgroundColor: alpha(config.color, 0.15)
7978
+ backgroundColor: alpha(cueColor, 0.15)
7756
7979
  },
7757
7980
  testID: `tempo-segment-completed-${config.label}`,
7758
- children: /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 12, fontWeight: "500", color: alpha(config.color, 0.7) }, children: [
7759
- formatDuration(completedMs),
7760
- indicator
7761
- ] })
7981
+ children: /* @__PURE__ */ jsxs(
7982
+ reactNative.Text,
7983
+ {
7984
+ style: {
7985
+ fontSize: font,
7986
+ fontWeight: "500",
7987
+ color: hitTarget ? alpha(config.color, 0.7) : cueColor
7988
+ },
7989
+ children: [
7990
+ formatDuration2(completedMs),
7991
+ indicator
7992
+ ]
7993
+ }
7994
+ )
7762
7995
  }
7763
7996
  );
7764
7997
  }
7765
- function formatDuration(ms) {
7998
+ function formatDuration2(ms) {
7766
7999
  const s = ms / 1e3;
7767
8000
  return s < 10 ? s.toFixed(1) : Math.round(s).toString();
7768
8001
  }
8002
+ var METRIC_FONT = "Inter, sans-serif";
8003
+ function MetricCell({ children, color, fontSize = 11 }) {
8004
+ return /* @__PURE__ */ jsx(
8005
+ reactNative.Text,
8006
+ {
8007
+ style: {
8008
+ fontFamily: METRIC_FONT,
8009
+ fontSize,
8010
+ color,
8011
+ fontWeight: "600",
8012
+ letterSpacing: 1
8013
+ },
8014
+ children
8015
+ }
8016
+ );
8017
+ }
7769
8018
 
7770
8019
  // src/components/custom/Workout/TempoDisplay.tsx
7771
8020
  var t3 = getSemanticColors("dark");
@@ -7784,34 +8033,10 @@ function TempoValue({
7784
8033
  color,
7785
8034
  fontSize
7786
8035
  }) {
7787
- return /* @__PURE__ */ jsx(
7788
- reactNative.Text,
7789
- {
7790
- style: {
7791
- fontFamily: INTER,
7792
- fontSize,
7793
- color,
7794
- fontWeight: "600",
7795
- letterSpacing: 1
7796
- },
7797
- children: value
7798
- }
7799
- );
8036
+ return /* @__PURE__ */ jsx(MetricCell, { color, fontSize, children: value });
7800
8037
  }
7801
8038
  function TempoSeparator({ color, fontSize }) {
7802
- return /* @__PURE__ */ jsx(
7803
- reactNative.Text,
7804
- {
7805
- style: {
7806
- fontFamily: INTER,
7807
- fontSize,
7808
- color,
7809
- fontWeight: "600",
7810
- letterSpacing: 1
7811
- },
7812
- children: "-"
7813
- }
7814
- );
8039
+ return /* @__PURE__ */ jsx(MetricCell, { color, fontSize, children: "-" });
7815
8040
  }
7816
8041
  var LIVE_PHASES = [
7817
8042
  { key: "eccentric", color: phaseColors.eccentric },
@@ -7873,6 +8098,7 @@ function TempoDisplay({
7873
8098
  tempo,
7874
8099
  size = "md",
7875
8100
  colored = true,
8101
+ showLabel = true,
7876
8102
  showInfo = true,
7877
8103
  live,
7878
8104
  onPress,
@@ -7906,7 +8132,7 @@ function TempoDisplay({
7906
8132
  },
7907
8133
  ...props,
7908
8134
  children: [
7909
- /* @__PURE__ */ jsx(
8135
+ showLabel && /* @__PURE__ */ jsx(
7910
8136
  reactNative.Text,
7911
8137
  {
7912
8138
  style: {
@@ -8134,12 +8360,12 @@ var ZONE = {
8134
8360
  // severe over-target
8135
8361
  };
8136
8362
  var BULGE_SIZE = { 1: 8, 2: 10, 3: 11 };
8137
- function zoneForPct(pct) {
8138
- if (pct >= 125) return { color: ZONE.over3, over: 3 };
8139
- if (pct >= 115) return { color: ZONE.over2, over: 2 };
8140
- if (pct > 100) return { color: ZONE.over1, over: 1 };
8141
- if (pct === 100) return { color: ZONE.target, over: 0 };
8142
- if (pct >= 75) return { color: ZONE.approaching, over: 0 };
8363
+ function zoneForPct(pct2) {
8364
+ if (pct2 >= 125) return { color: ZONE.over3, over: 3 };
8365
+ if (pct2 >= 115) return { color: ZONE.over2, over: 2 };
8366
+ if (pct2 > 100) return { color: ZONE.over1, over: 1 };
8367
+ if (pct2 === 100) return { color: ZONE.target, over: 0 };
8368
+ if (pct2 >= 75) return { color: ZONE.approaching, over: 0 };
8143
8369
  return { color: ZONE.building, over: 0 };
8144
8370
  }
8145
8371
  function clampFill(value) {
@@ -8147,9 +8373,9 @@ function clampFill(value) {
8147
8373
  if (value > 1) return 1;
8148
8374
  return value;
8149
8375
  }
8150
- function isAtTarget(pct, threshold) {
8376
+ function isAtTarget(pct2, threshold) {
8151
8377
  if (threshold == null) return false;
8152
- return Math.abs(pct - Math.round(threshold * 100)) <= 5;
8378
+ return Math.abs(pct2 - Math.round(threshold * 100)) <= 5;
8153
8379
  }
8154
8380
  function IntensityBar({
8155
8381
  level,
@@ -8163,9 +8389,9 @@ function IntensityBar({
8163
8389
  }) {
8164
8390
  const isVertical = orientation === "vertical";
8165
8391
  const fillLevel = clampFill(level);
8166
- const pct = Math.round(Math.max(0, level) * 100);
8167
- const zone = zoneForPct(pct);
8168
- const atTarget = isAtTarget(pct, threshold);
8392
+ const pct2 = Math.round(Math.max(0, level) * 100);
8393
+ const zone = zoneForPct(pct2);
8394
+ const atTarget = isAtTarget(pct2, threshold);
8169
8395
  const [fillAnim] = React24.useState(() => new reactNative.Animated.Value(fillLevel));
8170
8396
  React24.useEffect(() => {
8171
8397
  const animation = reactNative.Animated.timing(fillAnim, {
@@ -8216,8 +8442,8 @@ function IntensityBar({
8216
8442
  className,
8217
8443
  style: { alignItems: "center" },
8218
8444
  accessibilityRole: "progressbar",
8219
- accessibilityValue: { min: 0, max: 100, now: pct },
8220
- accessibilityLabel: `Intensity level: ${pct}%`,
8445
+ accessibilityValue: { min: 0, max: 100, now: pct2 },
8446
+ accessibilityLabel: `Intensity level: ${pct2}%`,
8221
8447
  testID: "intensity-bar",
8222
8448
  ...props,
8223
8449
  children: [
@@ -8263,7 +8489,7 @@ function IntensityBar({
8263
8489
  accessibilityElementsHidden: true,
8264
8490
  testID: "intensity-label",
8265
8491
  children: [
8266
- pct,
8492
+ pct2,
8267
8493
  "%"
8268
8494
  ]
8269
8495
  }
@@ -8448,13 +8674,257 @@ function WorkoutPill({
8448
8674
  }
8449
8675
  return pill;
8450
8676
  }
8677
+ var PULSE_HALF_MS = 950;
8678
+ var PULSE_MIN_OPACITY = 0.45;
8679
+ var SEGMENTED_BAR_GAP = 5;
8680
+ function usePulse2(active) {
8681
+ const [value] = React24.useState(() => new reactNative.Animated.Value(0));
8682
+ React24.useEffect(() => {
8683
+ if (!active) return;
8684
+ const loop = reactNative.Animated.loop(
8685
+ reactNative.Animated.sequence([
8686
+ reactNative.Animated.timing(value, {
8687
+ toValue: 1,
8688
+ duration: PULSE_HALF_MS,
8689
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8690
+ useNativeDriver: false
8691
+ }),
8692
+ reactNative.Animated.timing(value, {
8693
+ toValue: 0,
8694
+ duration: PULSE_HALF_MS,
8695
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8696
+ useNativeDriver: false
8697
+ })
8698
+ ])
8699
+ );
8700
+ loop.start();
8701
+ return () => loop.stop();
8702
+ }, [active, value]);
8703
+ return value;
8704
+ }
8705
+ function SegmentedBar({
8706
+ segments,
8707
+ height = 8,
8708
+ gap = SEGMENTED_BAR_GAP,
8709
+ radius = 2,
8710
+ marker = null,
8711
+ segmentTestID,
8712
+ style,
8713
+ ...props
8714
+ }) {
8715
+ const pulse = usePulse2(segments.some((s) => s.pulse));
8716
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
8717
+ segments.map((seg, i) => {
8718
+ const fillStyle = {
8719
+ width: `${(seg.fill ?? 1) * 100}%`,
8720
+ height: "100%",
8721
+ backgroundColor: seg.color,
8722
+ opacity: seg.opacity
8723
+ };
8724
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
8725
+ return /* @__PURE__ */ jsx(
8726
+ reactNative.View,
8727
+ {
8728
+ style: {
8729
+ flex: seg.weight ?? 1,
8730
+ minWidth: 0,
8731
+ height: "100%",
8732
+ marginLeft: seg.leadingGap,
8733
+ borderRadius: radius,
8734
+ overflow: "hidden"
8735
+ },
8736
+ children: seg.pulse ? /* @__PURE__ */ jsx(
8737
+ reactNative.Animated.View,
8738
+ {
8739
+ style: {
8740
+ ...fillStyle,
8741
+ ...seg.pulseColor ? {
8742
+ backgroundColor: pulse.interpolate({
8743
+ inputRange: [0, 1],
8744
+ outputRange: seg.pulseColor
8745
+ })
8746
+ } : {
8747
+ opacity: pulse.interpolate({
8748
+ inputRange: [0, 1],
8749
+ outputRange: [PULSE_MIN_OPACITY, 1]
8750
+ })
8751
+ }
8752
+ },
8753
+ accessibilityElementsHidden: true,
8754
+ testID
8755
+ }
8756
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
8757
+ },
8758
+ i
8759
+ );
8760
+ }),
8761
+ marker ? /* @__PURE__ */ jsx(
8762
+ reactNative.View,
8763
+ {
8764
+ style: {
8765
+ position: "absolute",
8766
+ left: `${marker.position * 100}%`,
8767
+ top: 0,
8768
+ bottom: 0,
8769
+ width: 2,
8770
+ backgroundColor: marker.color
8771
+ },
8772
+ accessibilityElementsHidden: true,
8773
+ testID: "segmented-bar-marker"
8774
+ }
8775
+ ) : null
8776
+ ] });
8777
+ }
8778
+
8779
+ // src/components/custom/Workout/SetBar.tsx
8780
+ var SET_STRIP_ZONES = {
8781
+ slow: primitiveRamps.red[600],
8782
+ moderate: primitiveRamps.orange[400],
8783
+ fast: primitiveRamps.amber[300],
8784
+ fastest: primitiveRamps.green[300]
8785
+ };
8786
+ var TODO_COLOR = primitiveColors.charcoal[300];
8787
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
8788
+ var NOTCH_GAP = 2;
8789
+ var CLUSTER_GAP = 3;
8790
+ var MYO_UPCOMING_CLUSTERS = 3;
8791
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
8792
+ var MYO_UPCOMING_FADE = 0.6;
8793
+ var PULSE_RANGE = {
8794
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
8795
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
8796
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
8797
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
8798
+ };
8799
+ function velocityZoneColor(v) {
8800
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
8801
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
8802
+ if (v < 1) return SET_STRIP_ZONES.fast;
8803
+ return SET_STRIP_ZONES.fastest;
8804
+ }
8805
+ function chunkedSegments(chunks, gap) {
8806
+ return chunks.flatMap(
8807
+ (vels, ci) => vels.map((v, ri) => ({
8808
+ color: velocityZoneColor(v),
8809
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
8810
+ }))
8811
+ );
8812
+ }
8813
+ function rangeSegments(floor, max, doneVels) {
8814
+ return Array.from({ length: max }, (_, i) => {
8815
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
8816
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
8817
+ });
8818
+ }
8819
+ function myoUpcomingSegments(activationLen, clusterCount) {
8820
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
8821
+ const trail = Array.from(
8822
+ { length: clusterCount },
8823
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
8824
+ color: SET_STRIP_VARIABLE_COLOR,
8825
+ opacity: MYO_UPCOMING_FADE ** ci,
8826
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
8827
+ }))
8828
+ ).flat();
8829
+ return [...activation, ...trail];
8830
+ }
8831
+ function setSegments(set) {
8832
+ if (set.status === "todo") {
8833
+ return [{ color: TODO_COLOR }];
8834
+ }
8835
+ if (set.status === "done") {
8836
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
8837
+ }
8838
+ if (set.status === "range") {
8839
+ return rangeSegments(set.floor, set.max, set.doneVels);
8840
+ }
8841
+ if (set.status === "drop") {
8842
+ return chunkedSegments(set.subloads, NOTCH_GAP);
8843
+ }
8844
+ if (set.status === "myo") {
8845
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
8846
+ }
8847
+ if (set.status === "myo-upcoming") {
8848
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
8849
+ }
8850
+ const performed = set.velocities.map((v) => {
8851
+ const color = velocityZoneColor(v);
8852
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
8853
+ });
8854
+ const remaining = Math.max(0, set.planned - set.velocities.length);
8855
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
8856
+ }
8857
+ function setBarSegmentTestID(seg) {
8858
+ if (seg.pulse) return "set-strip-pulse";
8859
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
8860
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
8861
+ }
8862
+ function SetBar({ set, height = 8 }) {
8863
+ return /* @__PURE__ */ jsx(
8864
+ SegmentedBar,
8865
+ {
8866
+ segments: setSegments(set),
8867
+ height,
8868
+ gap: 0,
8869
+ radius: 0,
8870
+ segmentTestID: setBarSegmentTestID,
8871
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
8872
+ testID: "set-strip-set"
8873
+ }
8874
+ );
8875
+ }
8876
+
8877
+ // src/components/custom/Workout/SetStrip.tsx
8878
+ var SET_STRIP_GAP = 5;
8879
+ function setCategory(set) {
8880
+ switch (set.status) {
8881
+ case "done":
8882
+ case "drop":
8883
+ case "myo":
8884
+ return "done";
8885
+ case "active":
8886
+ return "active";
8887
+ case "range":
8888
+ return set.doneVels.length > 0 ? "active" : "upcoming";
8889
+ case "todo":
8890
+ case "myo-upcoming":
8891
+ return "upcoming";
8892
+ }
8893
+ }
8894
+ function describeSets(sets) {
8895
+ const done = sets.filter((s) => setCategory(s) === "done").length;
8896
+ const active = sets.filter((s) => setCategory(s) === "active").length;
8897
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
8898
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
8899
+ }
8900
+ function SetStrip({ sets, height = 8, className, ...props }) {
8901
+ return /* @__PURE__ */ jsx(
8902
+ reactNative.View,
8903
+ {
8904
+ className,
8905
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
8906
+ accessibilityRole: "image",
8907
+ accessibilityLabel: describeSets(sets),
8908
+ testID: "set-strip",
8909
+ ...props,
8910
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
8911
+ }
8912
+ );
8913
+ }
8914
+
8915
+ // src/components/custom/Workout/VelocityStrip.tsx
8451
8916
  var VEL_COLORS = WORKOUT_TOKENS.scale;
8452
- var zoneIdToScaleToken = {
8453
- speed: "green",
8454
- power: "yellow",
8455
- strengthSpeed: "orange",
8456
- maximalStrength: "red",
8457
- grinding: "red"
8917
+ var bandIdToEffortColor = {
8918
+ speed: sequentialEffort[0],
8919
+ // green-300
8920
+ power: sequentialEffort[2],
8921
+ // amber-300 (gold)
8922
+ strengthSpeed: sequentialEffort[3],
8923
+ // orange-400
8924
+ maximalStrength: sequentialEffort[4],
8925
+ // red-600
8926
+ grinding: sequentialEffort[5]
8927
+ // red-700 — the distinct 5th band
8458
8928
  };
8459
8929
  function getVelocityZoneColor(velocity) {
8460
8930
  if (velocity >= 1) return "vel-green";
@@ -8493,14 +8963,140 @@ function classifyBand(velocity, bands) {
8493
8963
  return bands[bands.length - 1];
8494
8964
  }
8495
8965
  function bandColor(band) {
8496
- const token = zoneIdToScaleToken[band.id];
8497
- return token ? VEL_COLORS[token] : VEL_COLORS.green;
8966
+ return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
8498
8967
  }
8499
8968
  function getLossStyle(loss) {
8500
8969
  if (loss > 25) return { color: VEL_COLORS.red };
8501
8970
  if (loss > 20) return { color: VEL_COLORS.orange };
8502
8971
  return null;
8503
8972
  }
8973
+ var REP_GAP = 2;
8974
+ var WIDE_GAP = 8;
8975
+ var TODO_COLOR2 = primitiveColors.charcoal[300];
8976
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8977
+ function deriveDoneVelocities(set) {
8978
+ switch (set.type) {
8979
+ case "drop":
8980
+ return set.subloads.flat();
8981
+ case "myo":
8982
+ return [...set.activation, ...set.clusters.flat()];
8983
+ default:
8984
+ return set.velocities;
8985
+ }
8986
+ }
8987
+ function chunkedRepSlots(chunks) {
8988
+ const slots = [];
8989
+ chunks.forEach((chunk) => {
8990
+ chunk.forEach((velocity, ri) => {
8991
+ const first = slots.length === 0;
8992
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
8993
+ });
8994
+ });
8995
+ return slots;
8996
+ }
8997
+ function buildSlots(set) {
8998
+ switch (set.type) {
8999
+ case "straight": {
9000
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
9001
+ return Array.from({ length: total }, (_, i) => {
9002
+ const done = i < set.velocities.length;
9003
+ return {
9004
+ kind: done ? "rep" : "todo",
9005
+ velocity: done ? set.velocities[i] : void 0,
9006
+ leadingGap: i === 0 ? 0 : REP_GAP
9007
+ };
9008
+ });
9009
+ }
9010
+ case "range": {
9011
+ const total = Math.max(set.max, set.velocities.length);
9012
+ return Array.from({ length: total }, (_, i) => {
9013
+ const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
9014
+ return {
9015
+ kind,
9016
+ velocity: kind === "rep" ? set.velocities[i] : void 0,
9017
+ leadingGap: i === 0 ? 0 : REP_GAP
9018
+ };
9019
+ });
9020
+ }
9021
+ case "amrap": {
9022
+ const reps = set.velocities.map((velocity, i) => ({
9023
+ kind: "rep",
9024
+ velocity,
9025
+ leadingGap: i === 0 ? 0 : REP_GAP
9026
+ }));
9027
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
9028
+ return reps;
9029
+ }
9030
+ case "drop":
9031
+ return chunkedRepSlots(set.subloads);
9032
+ case "myo": {
9033
+ const slots = chunkedRepSlots([set.activation, ...set.clusters]);
9034
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
9035
+ return slots;
9036
+ }
9037
+ case "cluster": {
9038
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
9039
+ return Array.from({ length: total }, (_, i) => {
9040
+ const done = i < set.velocities.length;
9041
+ const boundary = i > 0 && i % set.groupSize === 0;
9042
+ return {
9043
+ kind: done ? "rep" : "todo",
9044
+ velocity: done ? set.velocities[i] : void 0,
9045
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
9046
+ };
9047
+ });
9048
+ }
9049
+ }
9050
+ }
9051
+ function setAccessibilityLabel(set, repCount) {
9052
+ switch (set.type) {
9053
+ case "straight":
9054
+ return `Velocity strip, straight set, ${repCount} reps`;
9055
+ case "range":
9056
+ return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
9057
+ case "amrap":
9058
+ return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
9059
+ case "drop":
9060
+ return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
9061
+ case "myo":
9062
+ return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
9063
+ case "cluster":
9064
+ return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
9065
+ }
9066
+ }
9067
+ function slotAccessibilityLabel(slot, index) {
9068
+ switch (slot.kind) {
9069
+ case "rep":
9070
+ return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
9071
+ case "todo":
9072
+ return `Rep ${index + 1}: planned`;
9073
+ case "variable":
9074
+ return `Rep ${index + 1}: variable`;
9075
+ case "continue":
9076
+ return "Keep going";
9077
+ }
9078
+ }
9079
+ var EXPANDED_TODO_STUB_PCT = 16;
9080
+ var EXPANDED_ENCODED_PCT = 45;
9081
+ var EXPANDED_HEIGHT = 60;
9082
+ var FIXED_MAX_VELOCITY = 1.15;
9083
+ var BARE_STUB_HEIGHT = 3;
9084
+ var HERO_HEIGHT = 220;
9085
+ var HERO_LABEL_HEADROOM = 20;
9086
+ var HERO_BAR_GAP = 8;
9087
+ var HERO_BAR_MAX_WIDTH = 52;
9088
+ var HERO_BAR_RADIUS = 5;
9089
+ var HERO_MIN_BAR_HEIGHT = 4;
9090
+ var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
9091
+ var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
9092
+ function scaleDenominator(scale, maxVelocity) {
9093
+ return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * 1.15;
9094
+ }
9095
+ function bareSlotHeight(slot, height, denom) {
9096
+ if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
9097
+ const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
9098
+ return Math.max(BARE_STUB_HEIGHT, ratio * height);
9099
+ }
8504
9100
  function getReducedMotionPreference() {
8505
9101
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8506
9102
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -8520,33 +9116,11 @@ function usePrefersReducedMotion() {
8520
9116
  var ANIMATION_DURATION = 400;
8521
9117
  var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
8522
9118
  var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
8523
- function VelocityStrip({
8524
- velocities,
8525
- zones,
8526
- liveRepIndex,
8527
- expanded = false,
8528
- onToggle,
8529
- onRepPress,
8530
- variant = "full",
8531
- showInfo = true,
8532
- className,
8533
- ...props
8534
- }) {
8535
- const maxVelocity = Math.max(...velocities, 0);
8536
- const meanVelocity = calculateMeanVelocity(velocities);
8537
- const loss = calculateVelocityLoss(velocities);
8538
- const hasZones = zones != null && zones.length > 0;
8539
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
8540
- const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
9119
+ function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
8541
9120
  const prefersReducedMotion = usePrefersReducedMotion();
8542
- const [heightAnim] = React24.useState(() => new reactNative.Animated.Value(expanded ? 60 : 3));
8543
- const [labelOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
8544
- const [infoOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
8545
9121
  const [liveScale] = React24.useState(() => new reactNative.Animated.Value(1));
8546
- const liveVelocity = liveRepIndex != null ? velocities[liveRepIndex] : void 0;
8547
- const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
8548
9122
  React24.useEffect(() => {
8549
- if (variant === "mini" || liveRepIndex == null || liveVelocity == null) return;
9123
+ if (!active || liveRepIndex == null || liveVelocity == null) return;
8550
9124
  if (prefersReducedMotion) {
8551
9125
  liveScale.setValue(1);
8552
9126
  return;
@@ -8576,12 +9150,201 @@ function VelocityStrip({
8576
9150
  useNativeDriver: true
8577
9151
  }).start();
8578
9152
  }
8579
- }, [variant, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
9153
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
9154
+ return liveScale;
9155
+ }
9156
+ function HeroVelocityChart({
9157
+ doneVelocities,
9158
+ barColorFor,
9159
+ scaleDenom,
9160
+ referenceVelocity,
9161
+ liveRepIndex,
9162
+ isNewPeak,
9163
+ targetReps,
9164
+ height,
9165
+ className,
9166
+ viewProps
9167
+ }) {
9168
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9169
+ const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
9170
+ const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
9171
+ const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
9172
+ const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
9173
+ const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
9174
+ const repCount = doneVelocities.length;
9175
+ const total = Math.max(repCount, targetReps ?? repCount);
9176
+ const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9177
+ const { style: externalStyle, ...restProps } = viewProps;
9178
+ return /* @__PURE__ */ jsx(
9179
+ reactNative.View,
9180
+ {
9181
+ className,
9182
+ style: [{ height }, externalStyle],
9183
+ accessibilityRole: "image",
9184
+ accessibilityLabel: label,
9185
+ testID: "velocity-strip-hero",
9186
+ ...restProps,
9187
+ children: /* @__PURE__ */ jsxs(
9188
+ reactNative.View,
9189
+ {
9190
+ style: {
9191
+ flex: 1,
9192
+ flexDirection: "row",
9193
+ alignItems: "flex-end",
9194
+ gap: HERO_BAR_GAP,
9195
+ position: "relative",
9196
+ borderBottomWidth: 2,
9197
+ borderBottomColor: HERO_PENDING_COLOR
9198
+ },
9199
+ children: [
9200
+ referencePx > 0 && /* @__PURE__ */ jsx(
9201
+ reactNative.View,
9202
+ {
9203
+ accessibilityElementsHidden: true,
9204
+ style: {
9205
+ position: "absolute",
9206
+ left: 0,
9207
+ right: 0,
9208
+ bottom: referencePx,
9209
+ borderTopWidth: 1,
9210
+ borderStyle: "dashed",
9211
+ borderColor: HERO_REFERENCE_COLOR,
9212
+ pointerEvents: "none"
9213
+ },
9214
+ testID: "velocity-hero-reference"
9215
+ }
9216
+ ),
9217
+ doneVelocities.map((velocity, i) => {
9218
+ const isLive = liveRepIndex === i;
9219
+ return /* @__PURE__ */ jsxs(
9220
+ reactNative.View,
9221
+ {
9222
+ accessibilityElementsHidden: true,
9223
+ style: {
9224
+ flex: 1,
9225
+ maxWidth: HERO_BAR_MAX_WIDTH,
9226
+ height: "100%",
9227
+ alignItems: "center",
9228
+ justifyContent: "flex-end"
9229
+ },
9230
+ children: [
9231
+ /* @__PURE__ */ jsx(
9232
+ reactNative.Text,
9233
+ {
9234
+ className: "text-text-primary",
9235
+ style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
9236
+ testID: `velocity-label-${i}`,
9237
+ children: formatVelocity(velocity)
9238
+ }
9239
+ ),
9240
+ /* @__PURE__ */ jsx(
9241
+ reactNative.Animated.View,
9242
+ {
9243
+ style: [
9244
+ {
9245
+ width: "100%",
9246
+ height: barHeight(velocity),
9247
+ borderTopLeftRadius: HERO_BAR_RADIUS,
9248
+ borderTopRightRadius: HERO_BAR_RADIUS,
9249
+ backgroundColor: barColorFor(velocity)
9250
+ },
9251
+ isLive ? { transform: [{ scale: liveScale }] } : null
9252
+ ],
9253
+ testID: `velocity-bar-${i}`
9254
+ }
9255
+ )
9256
+ ]
9257
+ },
9258
+ i
9259
+ );
9260
+ }),
9261
+ Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
9262
+ reactNative.View,
9263
+ {
9264
+ accessibilityElementsHidden: true,
9265
+ style: {
9266
+ flex: 1,
9267
+ maxWidth: HERO_BAR_MAX_WIDTH,
9268
+ height: "100%",
9269
+ alignItems: "center",
9270
+ justifyContent: "flex-end"
9271
+ },
9272
+ children: /* @__PURE__ */ jsx(
9273
+ reactNative.View,
9274
+ {
9275
+ style: {
9276
+ width: "100%",
9277
+ height: HERO_MIN_BAR_HEIGHT * 3,
9278
+ borderWidth: 1,
9279
+ borderStyle: "dashed",
9280
+ borderColor: HERO_PENDING_COLOR,
9281
+ borderTopLeftRadius: HERO_BAR_RADIUS,
9282
+ borderTopRightRadius: HERO_BAR_RADIUS
9283
+ },
9284
+ testID: "velocity-slot-todo"
9285
+ }
9286
+ )
9287
+ },
9288
+ `pending-${i}`
9289
+ ))
9290
+ ]
9291
+ }
9292
+ )
9293
+ }
9294
+ );
9295
+ }
9296
+ function VelocityStrip({
9297
+ velocities,
9298
+ set,
9299
+ zones,
9300
+ liveRepIndex,
9301
+ expanded = true,
9302
+ onToggle,
9303
+ onRepPress,
9304
+ variant = "expanded",
9305
+ height = EXPANDED_HEIGHT,
9306
+ targetReps,
9307
+ scale = "peak",
9308
+ showNumbers = true,
9309
+ showInfo = true,
9310
+ className,
9311
+ ...props
9312
+ }) {
9313
+ const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
9314
+ const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
9315
+ kind: "rep",
9316
+ velocity: v,
9317
+ leadingGap: i === 0 ? 0 : REP_GAP
9318
+ }));
9319
+ const maxVelocity = Math.max(...doneVelocities, 0);
9320
+ const meanVelocity = calculateMeanVelocity(doneVelocities);
9321
+ const loss = calculateVelocityLoss(doneVelocities);
9322
+ const framed = showNumbers || showInfo;
9323
+ const scaleDenom = scaleDenominator(scale, maxVelocity);
9324
+ const hasZones = zones != null && zones.length > 0;
9325
+ const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9326
+ const slotColor = (slot) => {
9327
+ if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
9328
+ if (slot.kind === "todo") return TODO_COLOR2;
9329
+ return SET_STRIP_VARIABLE_COLOR;
9330
+ };
9331
+ const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
9332
+ const [heightAnim] = React24.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
9333
+ const [labelOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9334
+ const [infoOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9335
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9336
+ const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
9337
+ const liveScale = useLiveRepPop(
9338
+ variant === "expanded" && framed,
9339
+ liveRepIndex,
9340
+ liveVelocity,
9341
+ isNewPeak
9342
+ );
8580
9343
  React24.useEffect(() => {
8581
- if (variant === "mini") return;
9344
+ if (variant !== "expanded" || !framed) return;
8582
9345
  reactNative.Animated.parallel([
8583
9346
  reactNative.Animated.timing(heightAnim, {
8584
- toValue: expanded ? 60 : 3,
9347
+ toValue: expanded ? height : 3,
8585
9348
  duration: ANIMATION_DURATION,
8586
9349
  easing: ANIMATION_EASING,
8587
9350
  useNativeDriver: false
@@ -8599,101 +9362,219 @@ function VelocityStrip({
8599
9362
  useNativeDriver: false
8600
9363
  })
8601
9364
  ]).start();
8602
- }, [expanded, variant, heightAnim, labelOpacity, infoOpacity]);
9365
+ }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
9366
+ if (set == null && velocities == null) return null;
9367
+ const repCount = doneVelocities.length;
9368
+ const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
9369
+ if (variant === "hero") {
9370
+ const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
9371
+ return /* @__PURE__ */ jsx(
9372
+ HeroVelocityChart,
9373
+ {
9374
+ doneVelocities,
9375
+ barColorFor,
9376
+ scaleDenom,
9377
+ referenceVelocity: maxVelocity,
9378
+ liveRepIndex,
9379
+ isNewPeak,
9380
+ targetReps,
9381
+ height: heroHeight,
9382
+ className,
9383
+ viewProps: props
9384
+ }
9385
+ );
9386
+ }
8603
9387
  if (variant === "mini") {
8604
9388
  const { style: externalStyle, ...restProps } = props;
8605
9389
  return /* @__PURE__ */ jsx(
8606
9390
  reactNative.View,
8607
9391
  {
8608
9392
  className,
8609
- style: [{ flexDirection: "row", height: 3, gap: 2, borderRadius: 2, overflow: "hidden" }, externalStyle],
9393
+ style: [
9394
+ { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
9395
+ externalStyle
9396
+ ],
8610
9397
  accessibilityRole: "image",
8611
- accessibilityLabel: `Velocity strip, ${velocities.length} reps`,
9398
+ accessibilityLabel: miniLabel,
8612
9399
  testID: "velocity-strip-mini",
8613
9400
  ...restProps,
8614
- children: velocities.map((v, i) => /* @__PURE__ */ jsx(
9401
+ children: slots.map((slot, i) => /* @__PURE__ */ jsx(
8615
9402
  reactNative.View,
8616
9403
  {
8617
9404
  style: {
8618
9405
  flex: 1,
8619
- backgroundColor: barColorFor(v),
9406
+ backgroundColor: slotColor(slot),
8620
9407
  borderRadius: 1,
8621
9408
  minWidth: 4,
8622
- height: "100%"
9409
+ height: "100%",
9410
+ // The container's uniform 2px gap covers rep spacing; a wide slot adds
9411
+ // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
9412
+ marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9413
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
8623
9414
  },
8624
9415
  accessibilityElementsHidden: true,
8625
- testID: `velocity-bar-${i}`
9416
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
8626
9417
  },
8627
9418
  i
8628
9419
  ))
8629
9420
  }
8630
9421
  );
8631
9422
  }
8632
- const stripLabel = `Velocity chart for set, ${velocities.length} reps, tap to ${expanded ? "collapse" : "expand"}`;
8633
- const hasInteractiveContainer = onToggle != null;
8634
- const hasInteractiveReps = onRepPress != null && expanded;
8635
- const stripContent = /* @__PURE__ */ jsxs(
8636
- reactNative.Animated.View,
8637
- {
8638
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
8639
- style: [
8640
- {
8641
- height: heightAnim,
8642
- width: "100%",
8643
- gap: 2,
8644
- borderRadius: expanded ? 6 : 2,
8645
- paddingTop: expanded ? 16 : 0,
8646
- paddingBottom: expanded ? 24 : 0,
8647
- paddingHorizontal: expanded ? 6 : 0,
8648
- paddingVertical: expanded ? void 0 : 8,
8649
- overflow: expanded ? "visible" : "hidden"
8650
- }
8651
- ],
8652
- accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
8653
- accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
8654
- testID: "velocity-strip",
8655
- ...props,
8656
- children: [
8657
- /* @__PURE__ */ jsx(
9423
+ if (!framed) {
9424
+ const { style: externalStyle, ...restProps } = props;
9425
+ return /* @__PURE__ */ jsx(
9426
+ reactNative.View,
9427
+ {
9428
+ className,
9429
+ style: [
9430
+ { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
9431
+ externalStyle
9432
+ ],
9433
+ accessibilityRole: "image",
9434
+ accessibilityLabel: miniLabel,
9435
+ testID: "velocity-strip-compact",
9436
+ ...restProps,
9437
+ children: slots.map((slot, i) => /* @__PURE__ */ jsx(
8658
9438
  reactNative.View,
8659
9439
  {
8660
- style: { flexDirection: "row", flex: 1, gap: 2, alignItems: "flex-end" },
8661
- children: velocities.map((v, i) => {
8662
- const barBackground = barColorFor(v);
8663
- const barHeightPct = maxVelocity > 0 ? Math.round(v / (maxVelocity * 1.15) * 100) : 0;
8664
- const isLive = liveRepIndex === i;
8665
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
8666
- const barStyle = expanded ? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: barBackground } : {
8667
- minHeight: "100%",
8668
- borderTopLeftRadius: 2,
8669
- borderTopRightRadius: 2,
8670
- borderBottomLeftRadius: 0,
8671
- borderBottomRightRadius: 0,
8672
- backgroundColor: barBackground
8673
- };
8674
- const barInner = isLive ? /* @__PURE__ */ jsx(
8675
- reactNative.Animated.View,
8676
- {
8677
- style: [barStyle, { transform: [{ scale: liveScale }] }],
8678
- testID: `velocity-bar-${i}`,
8679
- accessibilityRole: "image",
8680
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
8681
- }
8682
- ) : /* @__PURE__ */ jsx(
8683
- reactNative.View,
8684
- {
8685
- style: barStyle,
8686
- testID: `velocity-bar-${i}`,
8687
- accessibilityRole: "image",
8688
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
8689
- }
8690
- );
8691
- const bar = /* @__PURE__ */ jsxs(
8692
- reactNative.View,
8693
- {
8694
- style: { flex: 1, height: "100%", justifyContent: "flex-end", position: "relative" },
8695
- children: [
8696
- expanded && /* @__PURE__ */ jsx(reactNative.Animated.View, { style: { opacity: labelOpacity, alignItems: "center", position: "absolute", top: -13, left: 0, right: 0 }, accessibilityElementsHidden: true, children: /* @__PURE__ */ jsx(
9440
+ style: {
9441
+ flex: 1,
9442
+ minWidth: 4,
9443
+ height: bareSlotHeight(slot, height, scaleDenom),
9444
+ // Top-rounded to match the framed chart's bars (the "rounded tops from
9445
+ // the numbers one"); square bottoms since bars sit on the baseline.
9446
+ borderTopLeftRadius: 2,
9447
+ borderTopRightRadius: 2,
9448
+ backgroundColor: slotColor(slot),
9449
+ marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9450
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9451
+ },
9452
+ accessibilityElementsHidden: true,
9453
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9454
+ },
9455
+ i
9456
+ ))
9457
+ }
9458
+ );
9459
+ }
9460
+ const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
9461
+ const hasInteractiveContainer = onToggle != null;
9462
+ const hasInteractiveReps = onRepPress != null && expanded;
9463
+ const setBars = set != null && slots.map((slot, i) => {
9464
+ const isStraightRep = set.type === "straight" && slot.kind === "rep";
9465
+ const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
9466
+ return /* @__PURE__ */ jsx(
9467
+ reactNative.View,
9468
+ {
9469
+ style: {
9470
+ flex: 1,
9471
+ height: "100%",
9472
+ justifyContent: "flex-end",
9473
+ marginLeft: slot.leadingGap
9474
+ },
9475
+ children: /* @__PURE__ */ jsx(
9476
+ reactNative.View,
9477
+ {
9478
+ style: {
9479
+ height: `${heightPct}%`,
9480
+ minHeight: 2,
9481
+ borderTopLeftRadius: 2,
9482
+ borderTopRightRadius: 2,
9483
+ backgroundColor: slotColor(slot),
9484
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9485
+ },
9486
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
9487
+ accessibilityRole: "image",
9488
+ accessibilityLabel: slotAccessibilityLabel(slot, i)
9489
+ }
9490
+ )
9491
+ },
9492
+ i
9493
+ );
9494
+ });
9495
+ const stripContent = /* @__PURE__ */ jsxs(
9496
+ reactNative.Animated.View,
9497
+ {
9498
+ className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
9499
+ style: [
9500
+ {
9501
+ height: heightAnim,
9502
+ width: "100%",
9503
+ gap: 2,
9504
+ borderRadius: expanded ? 6 : 2,
9505
+ paddingTop: expanded ? 16 : 0,
9506
+ paddingBottom: expanded ? 24 : 0,
9507
+ // No horizontal inset — the framed chart's bars span full width, matching
9508
+ // the bare spotlight and mini. Numbers/info sit in the vertical padding.
9509
+ paddingHorizontal: 0,
9510
+ paddingVertical: expanded ? void 0 : 8,
9511
+ overflow: expanded ? "visible" : "hidden"
9512
+ }
9513
+ ],
9514
+ accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
9515
+ accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
9516
+ testID: "velocity-strip",
9517
+ ...props,
9518
+ children: [
9519
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
9520
+ const barBackground = barColorFor(v);
9521
+ const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
9522
+ const isLive = liveRepIndex === i;
9523
+ const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
9524
+ const barStyle = expanded ? {
9525
+ height: `${barHeightPct}%`,
9526
+ minHeight: 2,
9527
+ borderTopLeftRadius: 2,
9528
+ borderTopRightRadius: 2,
9529
+ backgroundColor: barBackground
9530
+ } : {
9531
+ minHeight: "100%",
9532
+ borderTopLeftRadius: 2,
9533
+ borderTopRightRadius: 2,
9534
+ borderBottomLeftRadius: 0,
9535
+ borderBottomRightRadius: 0,
9536
+ backgroundColor: barBackground
9537
+ };
9538
+ const barInner = isLive ? /* @__PURE__ */ jsx(
9539
+ reactNative.Animated.View,
9540
+ {
9541
+ style: [barStyle, { transform: [{ scale: liveScale }] }],
9542
+ testID: `velocity-bar-${i}`,
9543
+ accessibilityRole: "image",
9544
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
9545
+ }
9546
+ ) : /* @__PURE__ */ jsx(
9547
+ reactNative.View,
9548
+ {
9549
+ style: barStyle,
9550
+ testID: `velocity-bar-${i}`,
9551
+ accessibilityRole: "image",
9552
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
9553
+ }
9554
+ );
9555
+ const bar = /* @__PURE__ */ jsxs(
9556
+ reactNative.View,
9557
+ {
9558
+ style: {
9559
+ flex: 1,
9560
+ height: "100%",
9561
+ justifyContent: "flex-end",
9562
+ position: "relative"
9563
+ },
9564
+ children: [
9565
+ expanded && showNumbers && /* @__PURE__ */ jsx(
9566
+ reactNative.Animated.View,
9567
+ {
9568
+ style: {
9569
+ opacity: labelOpacity,
9570
+ alignItems: "center",
9571
+ position: "absolute",
9572
+ top: -13,
9573
+ left: 0,
9574
+ right: 0
9575
+ },
9576
+ accessibilityElementsHidden: true,
9577
+ children: /* @__PURE__ */ jsx(
8697
9578
  reactNative.Text,
8698
9579
  {
8699
9580
  className: "text-text-secondary",
@@ -8701,30 +9582,30 @@ function VelocityStrip({
8701
9582
  testID: `velocity-label-${i}`,
8702
9583
  children: formatVelocity(v)
8703
9584
  }
8704
- ) }),
8705
- barInner
8706
- ]
8707
- },
8708
- i
8709
- );
8710
- if (onRepPress && expanded) {
8711
- return /* @__PURE__ */ jsx(
8712
- reactNative.Pressable,
8713
- {
8714
- style: { flex: 1, height: "100%" },
8715
- onPress: () => onRepPress(i, v),
8716
- accessibilityRole: "button",
8717
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
8718
- testID: `velocity-bar-pressable-${i}`,
8719
- children: bar
8720
- },
8721
- i
8722
- );
8723
- }
8724
- return bar;
8725
- })
9585
+ )
9586
+ }
9587
+ ),
9588
+ barInner
9589
+ ]
9590
+ },
9591
+ i
9592
+ );
9593
+ if (onRepPress && expanded) {
9594
+ return /* @__PURE__ */ jsx(
9595
+ reactNative.Pressable,
9596
+ {
9597
+ style: { flex: 1, height: "100%" },
9598
+ onPress: () => onRepPress(i, v),
9599
+ accessibilityRole: "button",
9600
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
9601
+ testID: `velocity-bar-pressable-${i}`,
9602
+ children: bar
9603
+ },
9604
+ i
9605
+ );
8726
9606
  }
8727
- ),
9607
+ return bar;
9608
+ }) }),
8728
9609
  expanded && showInfo && /* @__PURE__ */ jsxs(
8729
9610
  reactNative.Animated.View,
8730
9611
  {
@@ -9022,233 +9903,90 @@ function Sparkline({
9022
9903
  }
9023
9904
  );
9024
9905
  }
9025
- function formatAccessibilityLabel(setNumber, reps, weight, unit) {
9026
- const repsText = reps != null ? `${reps} reps` : "no reps";
9027
- const weightText = weight != null ? `at ${roundWeight(weight)} ${unit}` : "";
9028
- return `Set ${setNumber}: ${repsText}${weightText ? ` ${weightText}` : ""}`;
9906
+ var DARK = getSemanticColors("dark");
9907
+ var TEXT_ACTIVE = DARK["text-primary"];
9908
+ var TEXT_MUTED = DARK["text-secondary"];
9909
+ var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
9910
+ var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
9911
+ var typeBadge = {
9912
+ fontSize: 10,
9913
+ fontWeight: "700",
9914
+ fontFamily: "Inter, sans-serif",
9915
+ color: WORKOUT_TOKENS.scale.orange,
9916
+ backgroundColor: "rgba(255,165,2,0.12)",
9917
+ paddingVertical: 1,
9918
+ paddingHorizontal: 5,
9919
+ borderRadius: 3,
9920
+ overflow: "hidden"
9921
+ };
9922
+ function displayReps(set) {
9923
+ return set.state === "done" ? set.reps : set.target.reps;
9924
+ }
9925
+ function displayWeight(set) {
9926
+ return set.state === "done" ? set.weight : set.target.weight;
9927
+ }
9928
+ function plannedReps(set) {
9929
+ if (set.state === "done") return set.velocities.length;
9930
+ if (set.state === "live") return set.target.reps;
9931
+ return set.planned ?? set.target.reps;
9932
+ }
9933
+ function accessibilityLabel(set) {
9934
+ const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
9935
+ if (set.state === "live") return `${base}, in progress`;
9936
+ if (set.state === "todo") return `${base}, upcoming`;
9937
+ return base;
9938
+ }
9939
+ function Cell({
9940
+ width,
9941
+ children,
9942
+ testID
9943
+ }) {
9944
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
9029
9945
  }
9030
- function SetRow({
9031
- mode,
9032
- setNumber,
9033
- previous,
9034
- reps,
9035
- weight,
9036
- rpe,
9037
- unit,
9038
- velocities,
9039
- velocityZones,
9040
- velocityLiveRepIndex,
9041
- velocityExpanded,
9042
- onVelocityToggle,
9043
- setType,
9044
- prBadges,
9045
- isNextSet,
9046
- targets
9047
- }) {
9048
- const isActive = mode === "active";
9049
- const isCompleted = mode === "completed";
9050
- const rowStyle = {
9051
- flexDirection: "row",
9052
- alignItems: "center",
9053
- padding: 7,
9054
- paddingHorizontal: 8,
9055
- gap: 8,
9056
- borderRadius: 8
9057
- };
9058
- if (isActive && isNextSet) {
9059
- rowStyle.backgroundColor = "rgba(255,121,0,0.06)";
9060
- rowStyle.borderWidth = 1;
9061
- rowStyle.borderColor = "rgba(255,121,0,0.15)";
9062
- }
9063
- if (isCompleted && !isNextSet) {
9064
- rowStyle.opacity = 0.55;
9946
+ function RowStrip({ set }) {
9947
+ const zones = set.velocityZones;
9948
+ if (set.state === "live") {
9949
+ return /* @__PURE__ */ jsx(
9950
+ VelocityStrip,
9951
+ {
9952
+ variant: "expanded",
9953
+ showNumbers: false,
9954
+ showInfo: false,
9955
+ height: 24,
9956
+ scale: "fixed",
9957
+ set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
9958
+ zones
9959
+ }
9960
+ );
9065
9961
  }
9962
+ const velocities = set.state === "done" ? set.velocities : [];
9963
+ return /* @__PURE__ */ jsx(
9964
+ VelocityStrip,
9965
+ {
9966
+ variant: "mini",
9967
+ set: { type: "straight", velocities, planned: plannedReps(set) },
9968
+ zones
9969
+ }
9970
+ );
9971
+ }
9972
+ function SetRow(set) {
9973
+ const live = set.state === "live";
9974
+ const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
9975
+ const rpe = set.state === "todo" ? null : set.rpe;
9066
9976
  return /* @__PURE__ */ jsxs(
9067
9977
  reactNative.View,
9068
9978
  {
9069
- style: rowStyle,
9070
- accessibilityLabel: formatAccessibilityLabel(
9071
- setNumber,
9072
- reps,
9073
- weight,
9074
- unit
9075
- ),
9979
+ style: { paddingVertical: 6, paddingHorizontal: 8 },
9980
+ accessibilityLabel: accessibilityLabel(set),
9076
9981
  testID: "set-row",
9077
9982
  children: [
9078
- /* @__PURE__ */ jsx(
9079
- reactNative.View,
9080
- {
9081
- style: { width: 36, alignItems: "center", justifyContent: "center" },
9082
- testID: "set-row-set-number",
9083
- children: setType ? /* @__PURE__ */ jsx(
9084
- reactNative.Text,
9085
- {
9086
- style: {
9087
- fontSize: 10,
9088
- fontWeight: "700",
9089
- fontFamily: "Inter, sans-serif",
9090
- color: WORKOUT_TOKENS.scale.orange,
9091
- backgroundColor: "rgba(255,165,2,0.12)",
9092
- paddingVertical: 1,
9093
- paddingHorizontal: 5,
9094
- borderRadius: 3,
9095
- overflow: "hidden"
9096
- },
9097
- testID: "set-row-type-badge",
9098
- children: setType
9099
- }
9100
- ) : /* @__PURE__ */ jsx(
9101
- reactNative.Text,
9102
- {
9103
- className: "text-text-secondary",
9104
- style: {
9105
- fontSize: 13,
9106
- fontWeight: "600",
9107
- fontFamily: "Inter, sans-serif"
9108
- },
9109
- children: setNumber
9110
- }
9111
- )
9112
- }
9113
- ),
9114
- /* @__PURE__ */ jsx(
9115
- reactNative.View,
9116
- {
9117
- className: "flex-1",
9118
- testID: "set-row-previous",
9119
- children: /* @__PURE__ */ jsx(
9120
- reactNative.Text,
9121
- {
9122
- className: "text-text-secondary",
9123
- style: {
9124
- fontSize: 12,
9125
- fontFamily: "Inter, sans-serif"
9126
- },
9127
- children: previous ? `${previous.reps} x ${roundWeight(previous.weight)}` : "\u2014"
9128
- }
9129
- )
9130
- }
9131
- ),
9132
- /* @__PURE__ */ jsx(
9133
- reactNative.View,
9134
- {
9135
- style: { width: 44, alignItems: "center", justifyContent: "center" },
9136
- testID: "set-row-reps",
9137
- children: isActive && reps == null && targets ? /* @__PURE__ */ jsx(
9138
- reactNative.Text,
9139
- {
9140
- className: "text-text-tertiary",
9141
- style: {
9142
- fontSize: 13,
9143
- fontStyle: "italic",
9144
- fontFamily: "Inter, sans-serif"
9145
- },
9146
- testID: "set-row-target-reps",
9147
- children: targets.reps
9148
- }
9149
- ) : /* @__PURE__ */ jsx(
9150
- reactNative.Text,
9151
- {
9152
- className: reps != null ? "text-text-primary" : "text-text-tertiary",
9153
- style: {
9154
- fontSize: 14,
9155
- fontWeight: "600",
9156
- fontFamily: "Inter, sans-serif"
9157
- },
9158
- children: reps != null ? reps : "\u2014"
9159
- }
9160
- )
9161
- }
9162
- ),
9163
- /* @__PURE__ */ jsx(
9164
- reactNative.View,
9165
- {
9166
- style: { width: 56, alignItems: "center", justifyContent: "center" },
9167
- testID: "set-row-weight",
9168
- children: isActive && weight == null && targets ? /* @__PURE__ */ jsx(
9169
- reactNative.Text,
9170
- {
9171
- className: "text-text-tertiary",
9172
- style: {
9173
- fontSize: 13,
9174
- fontStyle: "italic",
9175
- fontFamily: "Inter, sans-serif"
9176
- },
9177
- testID: "set-row-target-weight",
9178
- children: roundWeight(targets.weight)
9179
- }
9180
- ) : /* @__PURE__ */ jsx(
9181
- reactNative.Text,
9182
- {
9183
- className: weight != null ? "text-text-primary" : "text-text-tertiary",
9184
- style: {
9185
- fontSize: 14,
9186
- fontWeight: "600",
9187
- fontFamily: "Inter, sans-serif"
9188
- },
9189
- children: weight != null ? roundWeight(weight) : "\u2014"
9190
- }
9191
- )
9192
- }
9193
- ),
9194
- /* @__PURE__ */ jsx(
9195
- reactNative.View,
9196
- {
9197
- style: { width: 36, alignItems: "center", justifyContent: "center" },
9198
- testID: "set-row-rpe",
9199
- children: /* @__PURE__ */ jsx(
9200
- reactNative.Text,
9201
- {
9202
- className: rpe != null ? void 0 : "text-text-tertiary",
9203
- style: {
9204
- fontSize: 13,
9205
- fontWeight: "600",
9206
- fontFamily: "Inter, sans-serif",
9207
- // rpeColor returns a concrete hex (native-safe); only the tertiary
9208
- // fallback needs the className token.
9209
- color: rpe != null ? rpeColor(rpe) : void 0
9210
- },
9211
- children: rpe != null ? roundRpe(rpe) : "\u2014"
9212
- }
9213
- )
9214
- }
9215
- ),
9216
- prBadges && prBadges.length > 0 && /* @__PURE__ */ jsx(
9217
- reactNative.View,
9218
- {
9219
- className: "flex-row items-center",
9220
- style: { gap: 4 },
9221
- testID: "set-row-pr-badges",
9222
- children: prBadges.map((badge, i) => /* @__PURE__ */ jsx(
9223
- PrBadge,
9224
- {
9225
- type: badge.type,
9226
- label: badge.label,
9227
- animate: false,
9228
- compact: true
9229
- },
9230
- i
9231
- ))
9232
- }
9233
- ),
9234
- velocities && velocities.length > 0 && /* @__PURE__ */ jsx(
9235
- reactNative.View,
9236
- {
9237
- style: { position: "absolute", bottom: 0, left: 8, right: 8 },
9238
- testID: "set-row-velocity-strip",
9239
- children: /* @__PURE__ */ jsx(
9240
- VelocityStrip,
9241
- {
9242
- velocities,
9243
- zones: velocityZones,
9244
- liveRepIndex: velocityLiveRepIndex,
9245
- expanded: velocityExpanded,
9246
- onToggle: onVelocityToggle,
9247
- variant: onVelocityToggle ? "full" : "mini"
9248
- }
9249
- )
9250
- }
9251
- )
9983
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
9984
+ /* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
9985
+ /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
9986
+ /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
9987
+ /* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
9988
+ ] }),
9989
+ /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
9252
9990
  ]
9253
9991
  }
9254
9992
  );
@@ -9410,23 +10148,139 @@ function InputBar({
9410
10148
  );
9411
10149
  }
9412
10150
  var BRAND_PRIMARY4 = getSemanticColors("dark")["brand-primary"];
9413
- function RestTimer({
9414
- totalSeconds,
9415
- elapsedMs,
10151
+ var RING_DEFAULT_SIZE = 180;
10152
+ function RestActions({ onAddTime, onSkip }) {
10153
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
10154
+ /* @__PURE__ */ jsx(
10155
+ reactNative.Pressable,
10156
+ {
10157
+ onPress: onAddTime,
10158
+ style: {
10159
+ backgroundColor: "rgba(255,255,255,0.06)",
10160
+ paddingVertical: 8,
10161
+ paddingHorizontal: 20,
10162
+ borderRadius: 8
10163
+ },
10164
+ accessibilityRole: "button",
10165
+ accessibilityLabel: "Add 30 seconds",
10166
+ testID: "rest-timer-add-time",
10167
+ children: /* @__PURE__ */ jsx(
10168
+ reactNative.Text,
10169
+ {
10170
+ className: "text-text-secondary",
10171
+ style: { fontSize: 11, fontFamily: "Inter, sans-serif", fontWeight: "600" },
10172
+ children: "+30s"
10173
+ }
10174
+ )
10175
+ }
10176
+ ),
10177
+ /* @__PURE__ */ jsx(
10178
+ reactNative.Pressable,
10179
+ {
10180
+ onPress: onSkip,
10181
+ style: {
10182
+ backgroundColor: "rgba(255,121,0,0.12)",
10183
+ paddingVertical: 8,
10184
+ paddingHorizontal: 20,
10185
+ borderRadius: 8
10186
+ },
10187
+ accessibilityRole: "button",
10188
+ accessibilityLabel: "Skip rest",
10189
+ testID: "rest-timer-skip",
10190
+ children: /* @__PURE__ */ jsx(
10191
+ reactNative.Text,
10192
+ {
10193
+ style: {
10194
+ fontSize: 11,
10195
+ fontFamily: "Inter, sans-serif",
10196
+ fontWeight: "600",
10197
+ color: BRAND_PRIMARY4
10198
+ },
10199
+ children: "Skip"
10200
+ }
10201
+ )
10202
+ }
10203
+ )
10204
+ ] });
10205
+ }
10206
+ function RestTimerRing({
10207
+ totalSeconds,
10208
+ elapsedMs,
10209
+ remainingSec,
10210
+ done,
10211
+ size,
10212
+ nextSetInfo,
10213
+ displayOnly,
10214
+ onSkip,
10215
+ onAddTime
10216
+ }) {
10217
+ const a11yLabel = done ? "Rest complete, next set ready" : `Rest timer, ${Math.max(0, remainingSec)} seconds remaining`;
10218
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { alignItems: "center", gap: 16 }, testID: "rest-timer", children: [
10219
+ /* @__PURE__ */ jsx(
10220
+ CircularTimer,
10221
+ {
10222
+ durationMs: totalSeconds * 1e3,
10223
+ elapsedMs,
10224
+ mode: "down",
10225
+ size,
10226
+ doneLabel: "GO",
10227
+ doneColor: "success",
10228
+ accessibilityLabel: a11yLabel,
10229
+ controls: !displayOnly ? /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip }) : void 0,
10230
+ testID: "rest-timer-ring"
10231
+ }
10232
+ ),
10233
+ nextSetInfo != null && /* @__PURE__ */ jsx(
10234
+ reactNative.Text,
10235
+ {
10236
+ className: "text-text-tertiary",
10237
+ style: { fontSize: 13, fontFamily: "Inter, sans-serif" },
10238
+ testID: "rest-timer-next-set",
10239
+ children: nextSetInfo
10240
+ }
10241
+ )
10242
+ ] });
10243
+ }
10244
+ function RestTimer({
10245
+ totalSeconds,
10246
+ elapsedMs,
9416
10247
  onSkip,
9417
10248
  onAddTime,
9418
10249
  nextSetInfo,
9419
10250
  visible,
9420
- displayOnly = false
10251
+ displayOnly = false,
10252
+ variant = "bar",
10253
+ size = RING_DEFAULT_SIZE
9421
10254
  }) {
9422
- if (!visible) return null;
9423
- const remainingMs = Math.max(0, totalSeconds * 1e3 - elapsedMs);
10255
+ const {
10256
+ remainingMs,
10257
+ progress,
10258
+ label: timeDisplay,
10259
+ done
10260
+ } = useTimer({
10261
+ mode: "down",
10262
+ durationMs: totalSeconds * 1e3,
10263
+ elapsedMs
10264
+ });
9424
10265
  const remainingSec = Math.ceil(remainingMs / 1e3);
9425
- const minutes = Math.floor(remainingSec / 60);
9426
- const seconds = remainingSec % 60;
9427
- const timeDisplay = `${minutes}:${String(seconds).padStart(2, "0")}`;
9428
- const totalMs = totalSeconds * 1e3;
9429
- const progressPct = totalMs > 0 ? Math.min(100, elapsedMs / totalMs * 100) : 100;
10266
+ const progressPct = progress * 100;
10267
+ if (!visible) return null;
10268
+ if (variant === "ring") {
10269
+ return /* @__PURE__ */ jsx(
10270
+ RestTimerRing,
10271
+ {
10272
+ totalSeconds,
10273
+ elapsedMs,
10274
+ remainingSec,
10275
+ done,
10276
+ size,
10277
+ nextSetInfo,
10278
+ displayOnly,
10279
+ onSkip,
10280
+ onAddTime
10281
+ }
10282
+ );
10283
+ }
9430
10284
  return /* @__PURE__ */ jsxs(
9431
10285
  reactNative.View,
9432
10286
  {
@@ -9524,69 +10378,318 @@ function RestTimer({
9524
10378
  )
9525
10379
  }
9526
10380
  ),
9527
- !displayOnly && /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
10381
+ !displayOnly && /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip })
10382
+ ]
10383
+ }
10384
+ );
10385
+ }
10386
+ var t8 = getSemanticColors("dark");
10387
+ var TIMES = "\xD7";
10388
+ var AT = "@";
10389
+ function SetsRepsLoad({
10390
+ sets,
10391
+ reps,
10392
+ load,
10393
+ unit = "lb",
10394
+ className,
10395
+ ...props
10396
+ }) {
10397
+ const value = t8["text-primary"];
10398
+ const sep = t8["text-tertiary"];
10399
+ return /* @__PURE__ */ jsxs(
10400
+ reactNative.View,
10401
+ {
10402
+ className,
10403
+ style: { flexDirection: "row", alignItems: "baseline" },
10404
+ accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
10405
+ testID: "sets-reps-load",
10406
+ ...props,
10407
+ children: [
10408
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: sets }),
10409
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${TIMES} ` }),
10410
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: reps }),
10411
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${AT} ` }),
10412
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: load }),
10413
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${unit}` })
10414
+ ]
10415
+ }
10416
+ );
10417
+ }
10418
+
10419
+ // src/components/custom/Workout/paceTone.ts
10420
+ function paceTone(progress, target) {
10421
+ if (target === void 0) return "neutral";
10422
+ return progress >= target ? "ahead" : "behind";
10423
+ }
10424
+ var PACE_TONE_COLORS = {
10425
+ ahead: getSemanticColors("dark")["status-success"],
10426
+ behind: getSemanticColors("dark")["status-warning"],
10427
+ neutral: primitiveRamps.cyan[400]
10428
+ };
10429
+ function paceToneColor(tone) {
10430
+ return PACE_TONE_COLORS[tone];
10431
+ }
10432
+
10433
+ // src/components/custom/Workout/SegmentedProgressBar.tsx
10434
+ var DEFAULT_MARKER_COLOR = primitiveRamps.green[500];
10435
+ function SegmentedProgressBar({
10436
+ segments,
10437
+ value,
10438
+ target,
10439
+ markerColor = DEFAULT_MARKER_COLOR,
10440
+ color,
10441
+ height,
10442
+ gap,
10443
+ ...props
10444
+ }) {
10445
+ const total = segments.reduce((sum, s) => sum + s.weight, 0);
10446
+ const fillColor = color ?? paceToneColor(paceTone(total > 0 ? value / total : 0, target));
10447
+ const barSegments = segments.map((seg, i) => {
10448
+ const cumulativeBefore = segments.slice(0, i).reduce((sum, s) => sum + s.weight, 0);
10449
+ const fill = Math.max(0, Math.min(1, (value - cumulativeBefore) / seg.weight));
10450
+ return { weight: seg.weight, fill, color: fillColor };
10451
+ });
10452
+ return /* @__PURE__ */ jsx(
10453
+ SegmentedBar,
10454
+ {
10455
+ segments: barSegments,
10456
+ marker: target != null ? { position: target, color: markerColor } : null,
10457
+ height,
10458
+ gap,
10459
+ ...props
10460
+ }
10461
+ );
10462
+ }
10463
+
10464
+ // src/components/custom/Workout/MetricTiles.tsx
10465
+ function MetricTiles({ metrics, gap = 1, ...props }) {
10466
+ return /* @__PURE__ */ jsx(HStack, { gap, align: "stretch", ...props, children: metrics.map((metric) => /* @__PURE__ */ jsx(
10467
+ Tile,
10468
+ {
10469
+ label: metric.label,
10470
+ value: metric.value,
10471
+ valueColor: metric.valueColor
10472
+ },
10473
+ metric.label
10474
+ )) });
10475
+ }
10476
+ var DAY_MS = 24 * 60 * 60 * 1e3;
10477
+ var SOON_ACCENT = primitiveRamps.orange[400];
10478
+ function compactUntil(ms) {
10479
+ if (ms <= 0) return "now";
10480
+ const min = Math.round(ms / 6e4);
10481
+ if (min < 60) return `${min}m`;
10482
+ const hr = Math.round(min / 60);
10483
+ if (hr < 24) return `${hr}h`;
10484
+ const d = Math.round(hr / 24);
10485
+ if (d < 7) return `${d}d`;
10486
+ return `${Math.round(d / 7)}w`;
10487
+ }
10488
+ function ScheduleTiles({ when, now, gap = 1, ...props }) {
10489
+ const [mountNow] = React24.useState(() => Date.now());
10490
+ const reference = now ?? mountNow;
10491
+ const whenMs = when instanceof Date ? when.getTime() : when;
10492
+ const soon = whenMs - reference < DAY_MS;
10493
+ const time = new Date(whenMs).toLocaleTimeString(void 0, {
10494
+ hour: "numeric",
10495
+ minute: "2-digit",
10496
+ hour12: true
10497
+ });
10498
+ return /* @__PURE__ */ jsxs(HStack, { gap, ...props, children: [
10499
+ /* @__PURE__ */ jsx(Tile, { label: "Date", value: formatDateTime(when, "short") }),
10500
+ /* @__PURE__ */ jsx(Tile, { label: "Time", value: time }),
10501
+ /* @__PURE__ */ jsx(
10502
+ Tile,
10503
+ {
10504
+ label: "Until",
10505
+ value: compactUntil(whenMs - reference),
10506
+ valueColor: soon ? SOON_ACCENT : void 0
10507
+ }
10508
+ )
10509
+ ] });
10510
+ }
10511
+ var t9 = getSemanticColors("dark");
10512
+ var INDICATOR_CONFIG = {
10513
+ imbalance: { Icon: ScaleIcon, color: t9["status-error"], label: "Left/right imbalance" },
10514
+ overshoot: { Icon: AlertTriangleIcon, color: t9["status-error"], label: "Load overshoot" },
10515
+ "velocity-loss": { Icon: TrendingDownIcon, color: t9["status-warning"], label: "Velocity loss" },
10516
+ "missed-reps": { Icon: CircleSlashIcon, color: t9["status-warning"], label: "Missed reps" },
10517
+ pr: { Icon: AwardIcon, color: t9["status-success"], label: "Personal record" },
10518
+ info: { Icon: InfoIcon, color: t9["status-info"], label: "More info" }
10519
+ };
10520
+ var INDICATOR_PRECEDENCE = [
10521
+ "imbalance",
10522
+ "overshoot",
10523
+ "velocity-loss",
10524
+ "missed-reps",
10525
+ "pr",
10526
+ "info"
10527
+ ];
10528
+ function resolveIndicator(candidates) {
10529
+ return INDICATOR_PRECEDENCE.find((kind) => candidates.includes(kind));
10530
+ }
10531
+ function ExerciseIndicator({ kind, onPress, className, ...props }) {
10532
+ const { Icon, color, label } = INDICATOR_CONFIG[kind];
10533
+ const chip = /* @__PURE__ */ jsx(
10534
+ reactNative.View,
10535
+ {
10536
+ className,
10537
+ style: {
10538
+ width: 16,
10539
+ height: 16,
10540
+ borderRadius: 8,
10541
+ borderWidth: 1,
10542
+ borderColor: color,
10543
+ alignItems: "center",
10544
+ justifyContent: "center"
10545
+ },
10546
+ accessibilityRole: "image",
10547
+ accessibilityLabel: label,
10548
+ testID: "exercise-indicator",
10549
+ ...props,
10550
+ children: /* @__PURE__ */ jsx(Icon, { size: 11, color })
10551
+ }
10552
+ );
10553
+ if (onPress) {
10554
+ return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, children: chip });
10555
+ }
10556
+ return chip;
10557
+ }
10558
+ function headingLabel(name, sets, reps, load, unit) {
10559
+ return `${name}, ${sets}\xD7${reps} @ ${roundWeight(load)} ${unit}`;
10560
+ }
10561
+ function ExerciseHeading({
10562
+ name,
10563
+ sets,
10564
+ reps,
10565
+ load,
10566
+ unit = "lbs",
10567
+ tempo,
10568
+ indicator,
10569
+ dimmed,
10570
+ onPress
10571
+ }) {
10572
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { opacity: dimmed ? 0.55 : 1 }, testID: "exercise-heading", children: [
10573
+ /* @__PURE__ */ jsxs(
10574
+ reactNative.Pressable,
10575
+ {
10576
+ onPress,
10577
+ accessibilityRole: "button",
10578
+ accessibilityLabel: headingLabel(name, sets, reps, load, unit),
10579
+ className: "flex-row items-center",
10580
+ style: { gap: 8 },
10581
+ testID: "exercise-card-header",
10582
+ children: [
9528
10583
  /* @__PURE__ */ jsx(
9529
- reactNative.Pressable,
10584
+ reactNative.Text,
9530
10585
  {
9531
- onPress: onAddTime,
9532
- style: {
9533
- backgroundColor: "rgba(255,255,255,0.06)",
9534
- paddingVertical: 8,
9535
- paddingHorizontal: 20,
9536
- borderRadius: 8
9537
- },
9538
- accessibilityRole: "button",
9539
- accessibilityLabel: "Add 30 seconds",
9540
- testID: "rest-timer-add-time",
9541
- children: /* @__PURE__ */ jsx(
9542
- reactNative.Text,
9543
- {
9544
- className: "text-text-secondary",
9545
- style: {
9546
- fontSize: 11,
9547
- fontFamily: "Inter, sans-serif",
9548
- fontWeight: "600"
9549
- },
9550
- children: "+30s"
9551
- }
9552
- )
10586
+ className: "text-text-primary",
10587
+ style: { fontSize: 14, fontFamily: '"Space Grotesk", sans-serif', fontWeight: "700" },
10588
+ testID: "exercise-card-name",
10589
+ children: name
9553
10590
  }
9554
10591
  ),
9555
- /* @__PURE__ */ jsx(
9556
- reactNative.Pressable,
9557
- {
9558
- onPress: onSkip,
9559
- style: {
9560
- backgroundColor: "rgba(255,121,0,0.12)",
9561
- paddingVertical: 8,
9562
- paddingHorizontal: 20,
9563
- borderRadius: 8
9564
- },
9565
- accessibilityRole: "button",
9566
- accessibilityLabel: "Skip rest",
9567
- testID: "rest-timer-skip",
9568
- children: /* @__PURE__ */ jsx(
9569
- reactNative.Text,
9570
- {
9571
- style: {
9572
- fontSize: 11,
9573
- fontFamily: "Inter, sans-serif",
9574
- fontWeight: "600",
9575
- color: BRAND_PRIMARY4
9576
- },
9577
- children: "Skip"
9578
- }
9579
- )
9580
- }
9581
- )
9582
- ] })
10592
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
10593
+ indicator && /* @__PURE__ */ jsx(ExerciseIndicator, { kind: indicator })
10594
+ ]
10595
+ }
10596
+ ),
10597
+ /* @__PURE__ */ jsxs(
10598
+ reactNative.View,
10599
+ {
10600
+ className: "flex-row items-center",
10601
+ style: { gap: 8, marginTop: 1 },
10602
+ testID: "exercise-card-summary",
10603
+ children: [
10604
+ /* @__PURE__ */ jsx(SetsRepsLoad, { sets, reps, load, unit }),
10605
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
10606
+ tempo && /* @__PURE__ */ jsx(TempoDisplay, { tempo, size: "sm", showLabel: false, showInfo: false })
10607
+ ]
10608
+ }
10609
+ )
10610
+ ] });
10611
+ }
10612
+ function ExerciseCardHeading({
10613
+ setStates,
10614
+ stripHeight = 8,
10615
+ dimmed,
10616
+ testID = "exercise-card",
10617
+ ...heading
10618
+ }) {
10619
+ return /* @__PURE__ */ jsxs(
10620
+ reactNative.View,
10621
+ {
10622
+ style: { paddingVertical: 9, paddingHorizontal: 12, opacity: dimmed ? 0.55 : 1 },
10623
+ testID,
10624
+ children: [
10625
+ /* @__PURE__ */ jsx(ExerciseHeading, { ...heading }),
10626
+ setStates.length > 0 && /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: 7 }, testID: "exercise-card-strip", children: /* @__PURE__ */ jsx(SetStrip, { sets: setStates, height: stripHeight }) })
9583
10627
  ]
9584
10628
  }
9585
10629
  );
9586
10630
  }
9587
- function buildColumnHeaders(unit) {
9588
- return ["SET", "PREV", "REPS", unit.toUpperCase(), "RPE"];
10631
+ var PREV_COLUMN = { label: "PREV", width: void 0 };
10632
+ function buildColumns(unit, showPrevious) {
10633
+ return [
10634
+ { label: "SET", width: 36 },
10635
+ ...showPrevious ? [PREV_COLUMN] : [],
10636
+ { label: "REPS", width: 44 },
10637
+ { label: unit.toUpperCase(), width: 56 },
10638
+ { label: "RPE", width: 36 }
10639
+ ];
9589
10640
  }
10641
+ function SetTableHeader({
10642
+ unit = "lbs",
10643
+ showPrevious = true,
10644
+ testID = "table-header"
10645
+ }) {
10646
+ const columns = buildColumns(unit, showPrevious);
10647
+ return /* @__PURE__ */ jsx(
10648
+ reactNative.View,
10649
+ {
10650
+ className: "flex-row",
10651
+ style: {
10652
+ paddingVertical: 8,
10653
+ paddingHorizontal: 8,
10654
+ paddingBottom: 4,
10655
+ ...showPrevious ? {} : { justifyContent: "space-between" }
10656
+ },
10657
+ testID,
10658
+ children: columns.map(({ label, width }) => {
10659
+ const isFlex = width === void 0;
10660
+ return /* @__PURE__ */ jsx(
10661
+ reactNative.View,
10662
+ {
10663
+ style: {
10664
+ ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
10665
+ alignItems: "center",
10666
+ justifyContent: "center",
10667
+ gap: 8
10668
+ },
10669
+ ...isFlex ? { className: "flex-1" } : {},
10670
+ children: /* @__PURE__ */ jsx(
10671
+ reactNative.Text,
10672
+ {
10673
+ className: "text-text-tertiary",
10674
+ style: {
10675
+ fontSize: 10,
10676
+ fontWeight: "600",
10677
+ fontFamily: "Inter, sans-serif",
10678
+ textTransform: "uppercase",
10679
+ letterSpacing: 0.8
10680
+ },
10681
+ children: label
10682
+ }
10683
+ )
10684
+ },
10685
+ label
10686
+ );
10687
+ })
10688
+ }
10689
+ );
10690
+ }
10691
+
10692
+ // src/components/custom/Workout/ExerciseCard.tsx
9590
10693
  function getSupersetBorderRadius(position) {
9591
10694
  switch (position) {
9592
10695
  case "first":
@@ -9629,7 +10732,7 @@ function formatSummary(summary) {
9629
10732
  if (!summary) return "";
9630
10733
  return `${summary.sets}\xD7${summary.reps} @ ${roundWeight(summary.weight)} ${summary.unit}`;
9631
10734
  }
9632
- function formatAccessibilityLabel2(name, summary, prescription) {
10735
+ function formatAccessibilityLabel(name, summary, prescription) {
9633
10736
  if (summary) return `${name}, ${formatSummary(summary)}`;
9634
10737
  if (prescription) return `${name}, ${prescription}`;
9635
10738
  return name;
@@ -9638,7 +10741,6 @@ function CollapsedCard({
9638
10741
  name,
9639
10742
  onToggle,
9640
10743
  summary,
9641
- e1rm,
9642
10744
  isPR,
9643
10745
  setVelocities,
9644
10746
  velocityZones,
@@ -9657,7 +10759,7 @@ function CollapsedCard({
9657
10759
  onPressIn: () => setPressed(true),
9658
10760
  onPressOut: () => setPressed(false),
9659
10761
  accessibilityRole: "button",
9660
- accessibilityLabel: formatAccessibilityLabel2(name, summary, void 0),
10762
+ accessibilityLabel: formatAccessibilityLabel(name, summary, void 0),
9661
10763
  testID: "exercise-card",
9662
10764
  children: /* @__PURE__ */ jsxs(
9663
10765
  reactNative.View,
@@ -9671,220 +10773,131 @@ function CollapsedCard({
9671
10773
  ...supersetMargin
9672
10774
  },
9673
10775
  children: [
9674
- /* @__PURE__ */ jsxs(
9675
- reactNative.View,
9676
- {
9677
- className: "flex-row items-center",
9678
- testID: "exercise-card-header",
9679
- children: [
9680
- /* @__PURE__ */ jsx(
9681
- reactNative.Text,
9682
- {
9683
- className: "text-text-primary",
9684
- style: {
9685
- fontSize: 14,
9686
- fontFamily: '"Space Grotesk", sans-serif',
9687
- fontWeight: "700"
9688
- },
9689
- testID: "exercise-card-name",
9690
- children: name
9691
- }
9692
- ),
9693
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
9694
- summary && /* @__PURE__ */ jsx(
9695
- reactNative.Text,
9696
- {
9697
- className: "text-text-secondary",
9698
- style: {
9699
- fontSize: 12,
9700
- fontFamily: "Inter, sans-serif",
9701
- marginRight: 8
9702
- },
9703
- testID: "exercise-card-summary",
9704
- children: formatSummary(summary)
9705
- }
9706
- ),
9707
- e1rm && /* @__PURE__ */ jsx(
9708
- WeightBadge,
9709
- {
9710
- value: roundWeight(e1rm.value),
9711
- unit: e1rm.unit,
9712
- size: "sm",
9713
- showIcon: false,
9714
- testID: "exercise-card-e1rm"
9715
- }
9716
- ),
9717
- isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
9718
- ]
9719
- }
9720
- ),
9721
- (completedSets > 0 || remaining > 0) && /* @__PURE__ */ jsxs(
9722
- reactNative.View,
9723
- {
9724
- className: "flex-row",
9725
- style: { marginTop: 6, gap: 4 },
9726
- testID: "exercise-card-strips",
9727
- children: [
9728
- setVelocities?.map((velocities, i) => /* @__PURE__ */ jsx(
9729
- VelocityStrip,
9730
- {
9731
- velocities,
9732
- zones: velocityZones,
9733
- variant: "mini",
9734
- testID: `exercise-card-velocity-strip-${i}`
9735
- },
9736
- i
9737
- )),
9738
- Array.from({ length: remaining }, (_, i) => /* @__PURE__ */ jsx(
9739
- PlaceholderStrip,
9740
- {
9741
- testID: `exercise-card-placeholder-${i}`
9742
- },
9743
- `placeholder-${i}`
9744
- ))
9745
- ]
9746
- }
9747
- )
10776
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", testID: "exercise-card-header", children: [
10777
+ /* @__PURE__ */ jsx(
10778
+ reactNative.Text,
10779
+ {
10780
+ className: "text-text-primary",
10781
+ style: {
10782
+ fontSize: 14,
10783
+ fontFamily: '"Space Grotesk", sans-serif',
10784
+ fontWeight: "700"
10785
+ },
10786
+ testID: "exercise-card-name",
10787
+ children: name
10788
+ }
10789
+ ),
10790
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
10791
+ summary && /* @__PURE__ */ jsx(
10792
+ reactNative.Text,
10793
+ {
10794
+ className: "text-text-secondary",
10795
+ style: {
10796
+ fontSize: 12,
10797
+ fontFamily: "Inter, sans-serif",
10798
+ marginRight: 8
10799
+ },
10800
+ testID: "exercise-card-summary",
10801
+ children: formatSummary(summary)
10802
+ }
10803
+ ),
10804
+ isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
10805
+ ] }),
10806
+ (completedSets > 0 || remaining > 0) && /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row", style: { marginTop: 6, gap: 4 }, testID: "exercise-card-strips", children: [
10807
+ setVelocities?.map((velocities, i) => /* @__PURE__ */ jsx(
10808
+ VelocityStrip,
10809
+ {
10810
+ velocities,
10811
+ zones: velocityZones,
10812
+ variant: "mini",
10813
+ testID: `exercise-card-velocity-strip-${i}`
10814
+ },
10815
+ i
10816
+ )),
10817
+ Array.from({ length: remaining }, (_, i) => /* @__PURE__ */ jsx(
10818
+ PlaceholderStrip,
10819
+ {
10820
+ testID: `exercise-card-placeholder-${i}`
10821
+ },
10822
+ `placeholder-${i}`
10823
+ ))
10824
+ ] })
9748
10825
  ]
9749
10826
  }
9750
10827
  )
9751
10828
  }
9752
10829
  );
9753
10830
  }
10831
+ var DARK2 = getSemanticColors("dark");
10832
+ var BODY_DIVIDER = DARK2["border-subtle"];
10833
+ function deriveHeaderSetStates(sets) {
10834
+ return sets.map((set) => {
10835
+ switch (set.state) {
10836
+ case "live":
10837
+ return { status: "active", velocities: set.velocities, planned: set.target.reps };
10838
+ case "done":
10839
+ return { status: "done", velocities: set.velocities };
10840
+ case "todo":
10841
+ return { status: "todo", planned: set.planned ?? set.target.reps };
10842
+ }
10843
+ });
10844
+ }
9754
10845
  function ExpandedCard({
9755
10846
  name,
9756
10847
  onToggle,
9757
10848
  summary,
9758
- e1rm,
9759
10849
  isPR,
9760
10850
  sets,
9761
10851
  tempo,
10852
+ indicator,
10853
+ setStates,
10854
+ stripHeight = 8,
10855
+ velocityZones,
9762
10856
  supersetPosition
9763
10857
  }) {
9764
10858
  const borderRadius = getSupersetBorderRadius(supersetPosition);
9765
10859
  const supersetMargin = getSupersetMargin(supersetPosition);
9766
10860
  const unit = summary?.unit ?? sets?.[0]?.unit ?? "lbs";
9767
- const columnHeaders = buildColumnHeaders(unit);
10861
+ const headerStates = setStates ?? (sets ? deriveHeaderSetStates(sets) : []);
10862
+ const headerIndicator = indicator ?? (isPR ? "pr" : void 0);
9768
10863
  return /* @__PURE__ */ jsxs(
9769
- reactNative.View,
9770
- {
9771
- className: "bg-surface-elevated border-border",
9772
- style: {
9773
- borderWidth: 1,
9774
- ...borderRadius,
9775
- ...supersetMargin
9776
- },
9777
- testID: "exercise-card",
9778
- children: [
9779
- /* @__PURE__ */ jsx(
9780
- reactNative.Pressable,
9781
- {
9782
- onPress: onToggle,
9783
- accessibilityRole: "button",
9784
- accessibilityLabel: formatAccessibilityLabel2(name, summary, void 0),
9785
- "aria-expanded": true,
9786
- testID: "exercise-card-header",
9787
- children: /* @__PURE__ */ jsxs(
9788
- reactNative.View,
9789
- {
9790
- className: "flex-row items-center",
9791
- style: {
9792
- padding: 12,
9793
- paddingHorizontal: 14,
9794
- paddingBottom: 10,
9795
- borderBottomWidth: 1,
9796
- borderBottomColor: resolveColor("border-default")
9797
- },
9798
- children: [
9799
- /* @__PURE__ */ jsx(
9800
- reactNative.Text,
9801
- {
9802
- className: "text-text-primary",
9803
- style: {
9804
- fontSize: 14,
9805
- fontFamily: '"Space Grotesk", sans-serif',
9806
- fontWeight: "700"
9807
- },
9808
- testID: "exercise-card-name",
9809
- children: name
9810
- }
9811
- ),
9812
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
9813
- summary && /* @__PURE__ */ jsx(
9814
- reactNative.Text,
9815
- {
9816
- className: "text-text-secondary",
9817
- style: {
9818
- fontSize: 12,
9819
- fontFamily: "Inter, sans-serif",
9820
- marginRight: 8
9821
- },
9822
- testID: "exercise-card-summary",
9823
- children: formatSummary(summary)
9824
- }
9825
- ),
9826
- e1rm && /* @__PURE__ */ jsx(
9827
- WeightBadge,
9828
- {
9829
- value: roundWeight(e1rm.value),
9830
- unit: e1rm.unit,
9831
- size: "sm",
9832
- showIcon: false,
9833
- testID: "exercise-card-e1rm"
9834
- }
9835
- ),
9836
- isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
9837
- ]
9838
- }
9839
- )
10864
+ reactNative.View,
10865
+ {
10866
+ className: "bg-surface-elevated border-border",
10867
+ style: {
10868
+ borderWidth: 1,
10869
+ ...borderRadius,
10870
+ ...supersetMargin
10871
+ },
10872
+ testID: "exercise-card",
10873
+ children: [
10874
+ /* @__PURE__ */ jsx(
10875
+ ExerciseCardHeading,
10876
+ {
10877
+ name,
10878
+ sets: summary?.sets ?? sets?.length ?? 0,
10879
+ reps: summary?.reps ?? 0,
10880
+ load: summary?.weight ?? 0,
10881
+ unit,
10882
+ tempo,
10883
+ indicator: headerIndicator,
10884
+ setStates: headerStates,
10885
+ stripHeight,
10886
+ onPress: onToggle,
10887
+ testID: "exercise-card-heading"
9840
10888
  }
9841
10889
  ),
9842
- tempo && /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: 8, paddingHorizontal: 14 }, testID: "exercise-card-tempo", children: /* @__PURE__ */ jsx(TempoDisplay, { tempo, size: "sm" }) }),
9843
- /* @__PURE__ */ jsx(
10890
+ /* @__PURE__ */ jsxs(
9844
10891
  reactNative.View,
9845
10892
  {
9846
- className: "flex-row",
9847
- style: {
9848
- paddingVertical: 8,
9849
- paddingHorizontal: 8,
9850
- paddingBottom: 4
9851
- },
9852
- testID: "exercise-card-column-headers",
9853
- children: columnHeaders.map((header, i) => {
9854
- const widths = [36, void 0, 44, 56, 36];
9855
- const width = widths[i];
9856
- const isFlex = width === void 0;
9857
- return /* @__PURE__ */ jsx(
9858
- reactNative.View,
9859
- {
9860
- style: {
9861
- ...isFlex ? {} : { width },
9862
- alignItems: "center",
9863
- justifyContent: "center",
9864
- gap: 8
9865
- },
9866
- ...isFlex ? { className: "flex-1" } : {},
9867
- children: /* @__PURE__ */ jsx(
9868
- reactNative.Text,
9869
- {
9870
- className: "text-text-tertiary",
9871
- style: {
9872
- fontSize: 10,
9873
- fontWeight: "600",
9874
- fontFamily: "Inter, sans-serif",
9875
- textTransform: "uppercase",
9876
- letterSpacing: 0.8
9877
- },
9878
- children: header
9879
- }
9880
- )
9881
- },
9882
- header
9883
- );
9884
- })
10893
+ style: { borderTopWidth: 1, borderTopColor: BODY_DIVIDER, paddingBottom: 6 },
10894
+ testID: "exercise-card-body",
10895
+ children: [
10896
+ /* @__PURE__ */ jsx(SetTableHeader, { unit, showPrevious: false, testID: "exercise-card-column-headers" }),
10897
+ sets && /* @__PURE__ */ jsx(reactNative.View, { testID: "exercise-card-sets", children: sets.map((set, i) => /* @__PURE__ */ jsx(SetRow, { ...set, velocityZones: set.velocityZones ?? velocityZones }, i)) })
10898
+ ]
9885
10899
  }
9886
- ),
9887
- sets && /* @__PURE__ */ jsx(reactNative.View, { testID: "exercise-card-sets", children: sets.map((setProps, i) => /* @__PURE__ */ jsx(SetRow, { ...setProps }, i)) })
10900
+ )
9888
10901
  ]
9889
10902
  }
9890
10903
  );
@@ -9893,11 +10906,12 @@ function UpcomingCard({
9893
10906
  name,
9894
10907
  prescription,
9895
10908
  previousBest,
9896
- supersetPosition
10909
+ supersetPosition,
10910
+ onToggle
9897
10911
  }) {
9898
10912
  const borderRadius = getSupersetBorderRadius(supersetPosition);
9899
10913
  const supersetMargin = getSupersetMargin(supersetPosition);
9900
- return /* @__PURE__ */ jsx(
10914
+ return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress: onToggle, accessibilityRole: "button", children: /* @__PURE__ */ jsx(
9901
10915
  reactNative.View,
9902
10916
  {
9903
10917
  style: {
@@ -9907,7 +10921,7 @@ function UpcomingCard({
9907
10921
  ...borderRadius,
9908
10922
  ...supersetMargin
9909
10923
  },
9910
- accessibilityLabel: formatAccessibilityLabel2(name, void 0, prescription),
10924
+ accessibilityLabel: formatAccessibilityLabel(name, void 0, prescription),
9911
10925
  testID: "exercise-card",
9912
10926
  children: /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", children: [
9913
10927
  /* @__PURE__ */ jsx(
@@ -9915,6 +10929,7 @@ function UpcomingCard({
9915
10929
  {
9916
10930
  className: "text-text-primary",
9917
10931
  style: {
10932
+ flexShrink: 0,
9918
10933
  fontSize: 14,
9919
10934
  fontFamily: '"Space Grotesk", sans-serif',
9920
10935
  fontWeight: "700"
@@ -9927,7 +10942,9 @@ function UpcomingCard({
9927
10942
  reactNative.Text,
9928
10943
  {
9929
10944
  className: "text-text-secondary",
10945
+ numberOfLines: 1,
9930
10946
  style: {
10947
+ flexShrink: 1,
9931
10948
  fontSize: 12,
9932
10949
  fontFamily: "Inter, sans-serif",
9933
10950
  marginLeft: 8
@@ -9936,12 +10953,14 @@ function UpcomingCard({
9936
10953
  children: prescription
9937
10954
  }
9938
10955
  ),
9939
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
10956
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { minWidth: 8 } }),
9940
10957
  previousBest && /* @__PURE__ */ jsx(
9941
10958
  reactNative.Text,
9942
10959
  {
9943
10960
  className: "text-text-tertiary",
10961
+ numberOfLines: 1,
9944
10962
  style: {
10963
+ flexShrink: 1,
9945
10964
  fontSize: 11,
9946
10965
  fontFamily: "Inter, sans-serif"
9947
10966
  },
@@ -9951,17 +10970,208 @@ function UpcomingCard({
9951
10970
  )
9952
10971
  ] })
9953
10972
  }
10973
+ ) });
10974
+ }
10975
+ function ExerciseCard({
10976
+ upcoming,
10977
+ expanded,
10978
+ defaultExpanded,
10979
+ onExpandedChange,
10980
+ ...rest
10981
+ }) {
10982
+ const [internalExpanded, setInternalExpanded] = React24.useState(defaultExpanded ?? false);
10983
+ const isControlled = expanded !== void 0;
10984
+ const isExpanded = isControlled ? expanded : internalExpanded;
10985
+ const onToggle = () => {
10986
+ const next = !isExpanded;
10987
+ onExpandedChange?.(next);
10988
+ if (!isControlled) setInternalExpanded(next);
10989
+ };
10990
+ if (upcoming) return /* @__PURE__ */ jsx(UpcomingCard, { ...rest, onToggle });
10991
+ return isExpanded ? /* @__PURE__ */ jsx(ExpandedCard, { ...rest, onToggle }) : /* @__PURE__ */ jsx(CollapsedCard, { ...rest, onToggle });
10992
+ }
10993
+ var t10 = getSemanticColors("dark");
10994
+ var RAISED = primitiveColors.charcoal[400];
10995
+ var BAR_HEIGHT = 9;
10996
+ var BAR_GAP = 3;
10997
+ function SessionHeader({
10998
+ title,
10999
+ plan,
11000
+ setsDone = 0,
11001
+ elapsedMs,
11002
+ budgetMs,
11003
+ running = true,
11004
+ metrics = [],
11005
+ next,
11006
+ className,
11007
+ style,
11008
+ ...props
11009
+ }) {
11010
+ const upcoming = next != null;
11011
+ const totalSets = plan.reduce((sum, e) => sum + e.sets, 0);
11012
+ const hasPace = !upcoming && budgetMs != null && elapsedMs != null;
11013
+ const target = hasPace ? elapsedMs / budgetMs : void 0;
11014
+ const setsLabel = upcoming ? `${totalSets} sets` : `${Math.floor(setsDone)}/${totalSets} sets`;
11015
+ const setsLabelColor = upcoming ? t10["text-secondary"] : paceToneColor(paceTone(totalSets > 0 ? setsDone / totalSets : 0, target));
11016
+ return /* @__PURE__ */ jsxs(
11017
+ reactNative.View,
11018
+ {
11019
+ className,
11020
+ style: [
11021
+ {
11022
+ backgroundColor: RAISED,
11023
+ paddingTop: 11,
11024
+ paddingRight: 12,
11025
+ paddingBottom: 12,
11026
+ paddingLeft: 12,
11027
+ zIndex: 2
11028
+ },
11029
+ style
11030
+ ],
11031
+ testID: "session-rail-header",
11032
+ ...props,
11033
+ children: [
11034
+ /* @__PURE__ */ jsx(
11035
+ reactNative.Text,
11036
+ {
11037
+ accessibilityRole: "header",
11038
+ style: {
11039
+ fontSize: 16,
11040
+ lineHeight: 18,
11041
+ fontWeight: "700",
11042
+ fontFamily: '"Space Grotesk", sans-serif',
11043
+ color: t10["text-primary"],
11044
+ marginBottom: 10
11045
+ },
11046
+ testID: "session-rail-title",
11047
+ children: title
11048
+ }
11049
+ ),
11050
+ upcoming ? /* @__PURE__ */ jsx(ScheduleTiles, { when: next }) : /* @__PURE__ */ jsx(MetricTiles, { metrics }),
11051
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { marginTop: 10 }, children: [
11052
+ /* @__PURE__ */ jsx(
11053
+ SegmentedProgressBar,
11054
+ {
11055
+ segments: plan.map((e) => ({ weight: e.sets })),
11056
+ value: upcoming ? 0 : setsDone,
11057
+ target,
11058
+ height: BAR_HEIGHT,
11059
+ gap: BAR_GAP
11060
+ }
11061
+ ),
11062
+ /* @__PURE__ */ jsxs(
11063
+ reactNative.View,
11064
+ {
11065
+ style: {
11066
+ flexDirection: "row",
11067
+ justifyContent: "space-between",
11068
+ alignItems: "baseline",
11069
+ marginTop: 5
11070
+ },
11071
+ children: [
11072
+ /* @__PURE__ */ jsx(
11073
+ Typography,
11074
+ {
11075
+ variant: "mono",
11076
+ style: { fontSize: 10, color: setsLabelColor },
11077
+ testID: "session-rail-sets",
11078
+ children: setsLabel
11079
+ }
11080
+ ),
11081
+ /* @__PURE__ */ jsx(
11082
+ TimerReadout,
11083
+ {
11084
+ mode: "up",
11085
+ elapsedMs: upcoming ? budgetMs : elapsedMs,
11086
+ durationMs: budgetMs,
11087
+ running: running && !upcoming,
11088
+ showTotal: budgetMs != null && !upcoming
11089
+ }
11090
+ )
11091
+ ]
11092
+ }
11093
+ )
11094
+ ] })
11095
+ ]
11096
+ }
9954
11097
  );
9955
11098
  }
9956
- function ExerciseCard(props) {
9957
- switch (props.state) {
9958
- case "collapsed":
9959
- return /* @__PURE__ */ jsx(CollapsedCard, { ...props });
9960
- case "expanded":
9961
- return /* @__PURE__ */ jsx(ExpandedCard, { ...props });
9962
- case "upcoming":
9963
- return /* @__PURE__ */ jsx(UpcomingCard, { ...props });
9964
- }
11099
+ var INSET = primitiveColors.charcoal[800];
11100
+ var DIVIDER = primitiveColors.charcoal[500];
11101
+ var DEFAULT_WIDTH = 246;
11102
+ function SessionRail({
11103
+ title,
11104
+ exercises,
11105
+ setsDone,
11106
+ elapsedMs,
11107
+ budgetMs,
11108
+ running,
11109
+ metrics,
11110
+ next,
11111
+ stripHeight = 8,
11112
+ width = DEFAULT_WIDTH,
11113
+ onExercisePress,
11114
+ className,
11115
+ style,
11116
+ ...props
11117
+ }) {
11118
+ return /* @__PURE__ */ jsxs(
11119
+ reactNative.View,
11120
+ {
11121
+ className,
11122
+ style: [{ width, flexDirection: "column", backgroundColor: INSET }, style],
11123
+ testID: "session-rail",
11124
+ ...props,
11125
+ children: [
11126
+ /* @__PURE__ */ jsx(
11127
+ SessionHeader,
11128
+ {
11129
+ title,
11130
+ plan: exercises.map((ex) => ({ name: ex.name, sets: ex.summary.sets })),
11131
+ setsDone,
11132
+ elapsedMs,
11133
+ budgetMs,
11134
+ running,
11135
+ metrics,
11136
+ next
11137
+ }
11138
+ ),
11139
+ /* @__PURE__ */ jsx(
11140
+ reactNative.View,
11141
+ {
11142
+ style: [{ flex: 1, backgroundColor: INSET }, neumorphicShadows.charcoal.pressed.subtle],
11143
+ testID: "session-rail-list",
11144
+ children: exercises.map((ex, i) => /* @__PURE__ */ jsx(
11145
+ reactNative.View,
11146
+ {
11147
+ style: {
11148
+ borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
11149
+ borderBottomColor: DIVIDER
11150
+ },
11151
+ children: /* @__PURE__ */ jsx(
11152
+ ExerciseCardHeading,
11153
+ {
11154
+ name: ex.name,
11155
+ sets: ex.summary.sets,
11156
+ reps: ex.summary.reps,
11157
+ load: ex.summary.weight,
11158
+ unit: ex.summary.unit,
11159
+ tempo: ex.tempo,
11160
+ indicator: ex.indicator,
11161
+ setStates: ex.setStates,
11162
+ stripHeight,
11163
+ dimmed: ex.upcoming,
11164
+ onPress: () => onExercisePress?.(ex, i)
11165
+ }
11166
+ )
11167
+ },
11168
+ ex.id ?? i
11169
+ ))
11170
+ }
11171
+ )
11172
+ ]
11173
+ }
11174
+ );
9965
11175
  }
9966
11176
  var DEFAULTS = {
9967
11177
  label: "SS",
@@ -10225,10 +11435,10 @@ function WeekRow({
10225
11435
  }
10226
11436
  );
10227
11437
  }
10228
- var t8 = getSemanticColors("dark");
11438
+ var t11 = getSemanticColors("dark");
10229
11439
  var COLORS = {
10230
- statusSuccess: t8["status-success"],
10231
- brandPrimary: t8["brand-primary"],
11440
+ statusSuccess: t11["status-success"],
11441
+ brandPrimary: t11["brand-primary"],
10232
11442
  borderDefault: "#1F1F1F",
10233
11443
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
10234
11444
  };
@@ -10712,12 +11922,12 @@ function PrHistoryModal({
10712
11922
  }
10713
11923
  );
10714
11924
  }
10715
- var t9 = getSemanticColors("dark");
10716
- var BRAND_PRIMARY9 = t9["brand-primary"];
11925
+ var t12 = getSemanticColors("dark");
11926
+ var BRAND_PRIMARY9 = t12["brand-primary"];
10717
11927
  var BRAND_PRIMARY_SUBTLE = "rgba(255,121,0,0.12)";
10718
- var STATUS_SUCCESS = t9["status-success"];
10719
- var STATUS_WARNING = t9["status-warning"];
10720
- var STATUS_ERROR2 = t9["status-error"];
11928
+ var STATUS_SUCCESS = t12["status-success"];
11929
+ var STATUS_WARNING = t12["status-warning"];
11930
+ var STATUS_ERROR2 = t12["status-error"];
10721
11931
  var EMOJI_SETS = {
10722
11932
  sleep: ["\u{1F634}", "\u{1F610}", "\u{1F642}", "\u{1F60A}", "\u{1F929}"],
10723
11933
  soreness: ["\u{1F635}", "\u{1F623}", "\u{1F610}", "\u{1F642}", "\u{1F4AA}"],
@@ -10907,13 +12117,13 @@ function ReadinessCheck({
10907
12117
  )
10908
12118
  ] }) });
10909
12119
  }
10910
- var t10 = getSemanticColors("dark");
10911
- var BRAND_PRIMARY10 = t10["brand-primary"];
12120
+ var t13 = getSemanticColors("dark");
12121
+ var BRAND_PRIMARY10 = t13["brand-primary"];
10912
12122
  var BRAND_PRIMARY_DARK2 = MESO_ACCENT_GRADIENT_DARK;
10913
12123
  var BRAND_PRIMARY_LIGHT2 = MESO_ACCENT_GRADIENT_LIGHT;
10914
- var SUCCESS = t10["status-success"];
10915
- var WARNING = t10["status-warning"];
10916
- var ERROR = t10["status-error"];
12124
+ var SUCCESS = t13["status-success"];
12125
+ var WARNING = t13["status-warning"];
12126
+ var ERROR = t13["status-error"];
10917
12127
  var ACCENT_STOPS2 = [BRAND_PRIMARY_DARK2, BRAND_PRIMARY10, BRAND_PRIMARY_LIGHT2];
10918
12128
  var CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor("surface-elevated")} 0%, ${resolveColor("surface-raised")} 100%)`;
10919
12129
  var GAUGE_GRADIENT = "linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(249,180,21,0.25) 50%, rgba(209,67,67,0.25) 100%)";
@@ -10985,7 +12195,7 @@ function StatusPill({ badge }) {
10985
12195
  }
10986
12196
  );
10987
12197
  }
10988
- function MetricCell({ metric }) {
12198
+ function MetricCell2({ metric }) {
10989
12199
  return /* @__PURE__ */ jsxs(reactNative.View, { style: { width: "48%" }, testID: "meso-status-card-metric", children: [
10990
12200
  /* @__PURE__ */ jsx(
10991
12201
  reactNative.Text,
@@ -11203,7 +12413,7 @@ function MesoStatusCard({
11203
12413
  className: "flex-row flex-wrap",
11204
12414
  style: { gap: 8 },
11205
12415
  testID: "meso-status-card-metrics",
11206
- children: metrics.map((metric, index) => /* @__PURE__ */ jsx(MetricCell, { metric }, `${metric.label}-${index}`))
12416
+ children: metrics.map((metric, index) => /* @__PURE__ */ jsx(MetricCell2, { metric }, `${metric.label}-${index}`))
11207
12417
  }
11208
12418
  ),
11209
12419
  gauges.length > 0 && /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 12 }, testID: "meso-status-card-gauges", children: gauges.map((gauge, index) => /* @__PURE__ */ jsx(Gauge, { gauge }, `${gauge.label}-${index}`)) }),
@@ -11284,8 +12494,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
11284
12494
  var PLOT_LEFT = 26;
11285
12495
  var TOOLTIP_WIDTH = 124;
11286
12496
  function timeOf(dateStr) {
11287
- const t12 = Date.parse(dateStr);
11288
- return Number.isNaN(t12) ? 0 : t12;
12497
+ const t18 = Date.parse(dateStr);
12498
+ return Number.isNaN(t18) ? 0 : t18;
11289
12499
  }
11290
12500
  function formatValue(value) {
11291
12501
  return `${Math.round(value)}`;
@@ -11774,10 +12984,10 @@ function StrengthTrendChart({
11774
12984
  }
11775
12985
  );
11776
12986
  }
11777
- var t11 = getSemanticColors("dark");
11778
- var STATUS_SUCCESS3 = t11["status-success"];
11779
- var STATUS_WARNING3 = t11["status-warning"];
11780
- var STATUS_INFO = t11["status-info"];
12987
+ var t14 = getSemanticColors("dark");
12988
+ var STATUS_SUCCESS3 = t14["status-success"];
12989
+ var STATUS_WARNING3 = t14["status-warning"];
12990
+ var STATUS_INFO = t14["status-info"];
11781
12991
  var DOT_BORDER = "#F3F4F6";
11782
12992
  var BAND_FILL = "rgba(46,213,115,0.1)";
11783
12993
  var BAND_EDGE = "rgba(46,213,115,0.45)";
@@ -11820,13 +13030,13 @@ function interpEdge(pixels, x, key) {
11820
13030
  const a = pixels[i];
11821
13031
  const b = pixels[i + 1];
11822
13032
  if (x >= a.x && x <= b.x) {
11823
- const t12 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
11824
- return a[key] + t12 * (b[key] - a[key]);
13033
+ const t18 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
13034
+ return a[key] + t18 * (b[key] - a[key]);
11825
13035
  }
11826
13036
  }
11827
13037
  return pixels[pixels.length - 1][key];
11828
13038
  }
11829
- function buildColumns(pixels, step) {
13039
+ function buildColumns2(pixels, step) {
11830
13040
  if (pixels.length < 2) return [];
11831
13041
  const start = pixels[0].x;
11832
13042
  const end = pixels[pixels.length - 1].x;
@@ -11938,7 +13148,7 @@ function CapacityBandChart({
11938
13148
  }
11939
13149
  const { toX, toY } = scale;
11940
13150
  const bandPixels = toPixels(band, toX, toY);
11941
- const columns = buildColumns(bandPixels, COLUMN_STEP);
13151
+ const columns = buildColumns2(bandPixels, COLUMN_STEP);
11942
13152
  const topEdge = buildEdges(bandPixels, "yHigh");
11943
13153
  const bottomEdge = buildEdges(bandPixels, "yLow");
11944
13154
  const lastBandPoint = band[band.length - 1];
@@ -12013,8 +13223,8 @@ function CapacityBandChart({
12013
13223
  testID: "capacity-band-chart-reveal",
12014
13224
  children: /* @__PURE__ */ jsxs(reactNative.View, { style: { width, height, position: "relative" }, children: [
12015
13225
  projection && /* @__PURE__ */ jsxs(reactNative.View, { testID: "capacity-band-chart-projection", children: [
12016
- renderColumns(buildColumns(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
12017
- renderColumns(buildColumns(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
13226
+ renderColumns(buildColumns2(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
13227
+ renderColumns(buildColumns2(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
12018
13228
  renderEdges(buildEdges(trainingPixels, "yHigh"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
12019
13229
  renderEdges(buildEdges(trainingPixels, "yLow"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
12020
13230
  renderEdges(buildEdges(restPixels, "yHigh"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
@@ -12081,72 +13291,585 @@ function CapacityBandChart({
12081
13291
  return /* @__PURE__ */ jsx(
12082
13292
  reactNative.Text,
12083
13293
  {
12084
- className: "text-text-tertiary",
13294
+ className: "text-text-tertiary",
13295
+ style: {
13296
+ position: "absolute",
13297
+ left: toX(point.date) - 14,
13298
+ top: height - PADDING_BOTTOM + 2,
13299
+ width: 28,
13300
+ textAlign: "center",
13301
+ fontSize: 10,
13302
+ fontFamily: "Inter, sans-serif"
13303
+ },
13304
+ accessibilityElementsHidden: true,
13305
+ importantForAccessibility: "no-hide-descendants",
13306
+ testID: "capacity-band-chart-x-label",
13307
+ children: formatDate2(point.date)
13308
+ },
13309
+ `x-${point.date}`
13310
+ );
13311
+ })
13312
+ ]
13313
+ }
13314
+ ),
13315
+ workouts.map((workout, i) => {
13316
+ const cx = toX(workout.date);
13317
+ const cy = toY(workout.load);
13318
+ const color = DOT_COLORS[workout.status];
13319
+ const dotStyle = {
13320
+ width: DOT_SIZE,
13321
+ height: DOT_SIZE,
13322
+ borderRadius: DOT_SIZE / 2,
13323
+ backgroundColor: color,
13324
+ borderWidth: 2,
13325
+ borderColor: DOT_BORDER
13326
+ };
13327
+ const label = `Workout on ${formatDate2(workout.date)}, load ${workout.load}, ${STATUS_PHRASES[workout.status]}`;
13328
+ return /* @__PURE__ */ jsx(
13329
+ reactNative.Animated.View,
13330
+ {
13331
+ style: {
13332
+ position: "absolute",
13333
+ left: cx - DOT_SIZE / 2,
13334
+ top: cy - DOT_SIZE / 2,
13335
+ transform: [{ scale: dotAnims[i] ?? 1 }]
13336
+ },
13337
+ testID: "capacity-band-chart-dot-wrapper",
13338
+ children: onWorkoutPress ? /* @__PURE__ */ jsx(
13339
+ reactNative.Pressable,
13340
+ {
13341
+ onPress: () => onWorkoutPress(workout),
13342
+ accessibilityRole: "button",
13343
+ accessibilityLabel: label,
13344
+ style: dotStyle,
13345
+ testID: "capacity-band-chart-dot"
13346
+ }
13347
+ ) : /* @__PURE__ */ jsx(
13348
+ reactNative.View,
13349
+ {
13350
+ accessibilityElementsHidden: true,
13351
+ importantForAccessibility: "no-hide-descendants",
13352
+ style: dotStyle,
13353
+ testID: "capacity-band-chart-dot"
13354
+ }
13355
+ )
13356
+ },
13357
+ `dot-${workout.date}-${i}`
13358
+ );
13359
+ })
13360
+ ]
13361
+ }
13362
+ );
13363
+ }
13364
+ var t15 = getSemanticColors("dark");
13365
+ function glowShadow(color) {
13366
+ return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`;
13367
+ }
13368
+ var DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200];
13369
+ var DEFAULT_MARKER_COLOR2 = primitiveColors.white;
13370
+ var TICK_COLOR = t15["text-tertiary"];
13371
+ var ZONE_SIZES = {
13372
+ default: {
13373
+ trackHeight: 14,
13374
+ needleOverhang: 6,
13375
+ needleWidth: 4,
13376
+ tickLineNormal: 1.5,
13377
+ tickLineEmphasized: 2,
13378
+ tickLineHeightPad: 4,
13379
+ tickLineTopPad: 2,
13380
+ labelRowHeight: 16,
13381
+ labelMarginTop: 5,
13382
+ tickFont: 9,
13383
+ tickLetterSpacing: 0.5,
13384
+ tickCellWidth: 28
13385
+ },
13386
+ wall: {
13387
+ trackHeight: 24,
13388
+ needleOverhang: 9,
13389
+ needleWidth: 7,
13390
+ tickLineNormal: 2.5,
13391
+ tickLineEmphasized: 3.5,
13392
+ tickLineHeightPad: 6,
13393
+ tickLineTopPad: 3,
13394
+ labelRowHeight: 24,
13395
+ labelMarginTop: 8,
13396
+ tickFont: 14,
13397
+ tickLetterSpacing: 0.5,
13398
+ tickCellWidth: 48
13399
+ }
13400
+ };
13401
+ function fraction(value, min, max) {
13402
+ if (max <= min) return 0;
13403
+ return Math.max(0, Math.min(1, (value - min) / (max - min)));
13404
+ }
13405
+ function pct(frac) {
13406
+ return `${frac * 100}%`;
13407
+ }
13408
+ function ZoneTrack({
13409
+ zones,
13410
+ max,
13411
+ min = 0,
13412
+ marker = null,
13413
+ ticks,
13414
+ band = null,
13415
+ size = "default",
13416
+ trackHeight: trackHeightProp,
13417
+ needleOverhang: needleOverhangProp,
13418
+ trackColor = DEFAULT_TRACK_COLOR,
13419
+ className,
13420
+ style,
13421
+ accessibilityLabel: accessibilityLabel2,
13422
+ ...props
13423
+ }) {
13424
+ const s = ZONE_SIZES[size];
13425
+ const trackHeight = trackHeightProp ?? s.trackHeight;
13426
+ const needleOverhang = needleOverhangProp ?? s.needleOverhang;
13427
+ const isNeedle = marker?.type === "needle";
13428
+ const markerHeight = isNeedle ? trackHeight + needleOverhang * 2 : trackHeight;
13429
+ const markerFrac = marker != null ? fraction(marker.value, min, max) : 0;
13430
+ const bands = zones.map((zone, i) => {
13431
+ const prev = i === 0 ? min : zones[i - 1].upTo;
13432
+ const start = Math.max(min, Math.min(max, prev));
13433
+ const end = Math.max(min, Math.min(max, zone.upTo));
13434
+ return { color: zone.color, weight: Math.max(0, end - start) };
13435
+ });
13436
+ return /* @__PURE__ */ jsxs(
13437
+ reactNative.View,
13438
+ {
13439
+ className,
13440
+ style: [{ width: "100%" }, style],
13441
+ accessibilityRole: "progressbar",
13442
+ accessibilityValue: marker != null ? { min, max, now: marker.value } : { min, max, now: min },
13443
+ accessibilityLabel: accessibilityLabel2 ?? `Zone track: ${marker?.value ?? min}`,
13444
+ testID: "zone-track",
13445
+ ...props,
13446
+ children: [
13447
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { position: "relative", height: markerHeight }, children: [
13448
+ /* @__PURE__ */ jsxs(
13449
+ reactNative.View,
13450
+ {
13451
+ testID: "zone-track-track",
13452
+ accessibilityElementsHidden: true,
13453
+ style: {
13454
+ position: "absolute",
13455
+ top: (markerHeight - trackHeight) / 2,
13456
+ left: 0,
13457
+ right: 0,
13458
+ height: trackHeight,
13459
+ borderRadius: trackHeight / 2,
13460
+ backgroundColor: trackColor,
13461
+ overflow: "hidden",
13462
+ flexDirection: "row",
13463
+ // The pill's own box-shadow renders outside its overflow:hidden, so
13464
+ // `glow` haloes the whole track in the marker colour.
13465
+ ...marker?.glow ? { boxShadow: glowShadow(marker.color ?? DEFAULT_MARKER_COLOR2) } : null
13466
+ },
13467
+ children: [
13468
+ bands.map((b, i) => /* @__PURE__ */ jsx(
13469
+ reactNative.View,
13470
+ {
13471
+ testID: "zone-track-band",
13472
+ style: {
13473
+ flexGrow: b.weight,
13474
+ flexShrink: 1,
13475
+ flexBasis: 0,
13476
+ minWidth: 0,
13477
+ height: "100%",
13478
+ backgroundColor: b.color
13479
+ }
13480
+ },
13481
+ i
13482
+ )),
13483
+ band != null && /* @__PURE__ */ jsx(
13484
+ reactNative.View,
13485
+ {
13486
+ testID: "zone-track-band-highlight",
13487
+ style: {
13488
+ position: "absolute",
13489
+ top: 0,
13490
+ bottom: 0,
13491
+ left: pct(fraction(band.from, min, max)),
13492
+ width: pct(fraction(band.to, min, max) - fraction(band.from, min, max)),
13493
+ backgroundColor: band.color
13494
+ }
13495
+ }
13496
+ ),
13497
+ marker?.type === "fill" && /* @__PURE__ */ jsx(
13498
+ reactNative.View,
13499
+ {
13500
+ testID: "zone-track-unfilled",
13501
+ style: {
13502
+ position: "absolute",
13503
+ top: 0,
13504
+ bottom: 0,
13505
+ left: pct(markerFrac),
13506
+ right: 0,
13507
+ backgroundColor: trackColor
13508
+ }
13509
+ }
13510
+ ),
13511
+ marker?.type === "fill" && marker.color != null && /* @__PURE__ */ jsx(
13512
+ reactNative.View,
13513
+ {
13514
+ testID: "zone-track-fill",
12085
13515
  style: {
12086
13516
  position: "absolute",
12087
- left: toX(point.date) - 14,
12088
- top: height - PADDING_BOTTOM + 2,
12089
- width: 28,
12090
- textAlign: "center",
12091
- fontSize: 10,
12092
- fontFamily: "Inter, sans-serif"
12093
- },
12094
- accessibilityElementsHidden: true,
12095
- importantForAccessibility: "no-hide-descendants",
12096
- testID: "capacity-band-chart-x-label",
12097
- children: formatDate2(point.date)
12098
- },
12099
- `x-${point.date}`
12100
- );
12101
- })
12102
- ]
12103
- }
12104
- ),
12105
- workouts.map((workout, i) => {
12106
- const cx = toX(workout.date);
12107
- const cy = toY(workout.load);
12108
- const color = DOT_COLORS[workout.status];
12109
- const dotStyle = {
12110
- width: DOT_SIZE,
12111
- height: DOT_SIZE,
12112
- borderRadius: DOT_SIZE / 2,
12113
- backgroundColor: color,
12114
- borderWidth: 2,
12115
- borderColor: DOT_BORDER
12116
- };
12117
- const label = `Workout on ${formatDate2(workout.date)}, load ${workout.load}, ${STATUS_PHRASES[workout.status]}`;
12118
- return /* @__PURE__ */ jsx(
12119
- reactNative.Animated.View,
13517
+ top: 0,
13518
+ bottom: 0,
13519
+ left: 0,
13520
+ width: pct(markerFrac),
13521
+ backgroundColor: marker.color
13522
+ }
13523
+ }
13524
+ )
13525
+ ]
13526
+ }
13527
+ ),
13528
+ isNeedle && /* @__PURE__ */ jsx(
13529
+ reactNative.View,
12120
13530
  {
13531
+ testID: "zone-track-needle",
12121
13532
  style: {
12122
13533
  position: "absolute",
12123
- left: cx - DOT_SIZE / 2,
12124
- top: cy - DOT_SIZE / 2,
12125
- transform: [{ scale: dotAnims[i] ?? 1 }]
13534
+ top: 0,
13535
+ left: pct(markerFrac),
13536
+ width: s.needleWidth,
13537
+ height: markerHeight,
13538
+ borderRadius: s.needleWidth / 2,
13539
+ backgroundColor: marker.color ?? DEFAULT_MARKER_COLOR2,
13540
+ transform: [{ translateX: -s.needleWidth / 2 }]
13541
+ }
13542
+ }
13543
+ ),
13544
+ ticks?.map((tick, i) => {
13545
+ const emphasized = tick.emphasized === true;
13546
+ const lineColor = tick.color ?? (emphasized ? t15["brand-primary"] : TICK_COLOR);
13547
+ const lineWidth = emphasized ? s.tickLineEmphasized : s.tickLineNormal;
13548
+ return /* @__PURE__ */ jsx(
13549
+ reactNative.View,
13550
+ {
13551
+ testID: "zone-track-tick-line",
13552
+ accessibilityElementsHidden: true,
13553
+ style: {
13554
+ position: "absolute",
13555
+ top: (markerHeight - trackHeight) / 2 - s.tickLineTopPad,
13556
+ left: pct(fraction(tick.value, min, max)),
13557
+ width: lineWidth,
13558
+ height: trackHeight + s.tickLineHeightPad,
13559
+ borderRadius: lineWidth / 2,
13560
+ backgroundColor: lineColor,
13561
+ transform: [{ translateX: -lineWidth / 2 }],
13562
+ zIndex: 3
13563
+ }
12126
13564
  },
12127
- testID: "capacity-band-chart-dot-wrapper",
12128
- children: onWorkoutPress ? /* @__PURE__ */ jsx(
12129
- reactNative.Pressable,
13565
+ `line-${i}`
13566
+ );
13567
+ })
13568
+ ] }),
13569
+ ticks != null && ticks.length > 0 && /* @__PURE__ */ jsx(
13570
+ reactNative.View,
13571
+ {
13572
+ style: { position: "relative", height: s.labelRowHeight, marginTop: s.labelMarginTop },
13573
+ children: ticks.map((tick, i) => {
13574
+ if (tick.label == null) return null;
13575
+ const emphasized = tick.emphasized === true;
13576
+ const labelColor = tick.color ?? (emphasized ? t15["brand-primary"] : TICK_COLOR);
13577
+ const labelNode = /* @__PURE__ */ jsx(
13578
+ reactNative.Text,
12130
13579
  {
12131
- onPress: () => onWorkoutPress(workout),
12132
- accessibilityRole: "button",
12133
- accessibilityLabel: label,
12134
- style: dotStyle,
12135
- testID: "capacity-band-chart-dot"
13580
+ testID: "zone-track-tick-label",
13581
+ style: {
13582
+ fontSize: s.tickFont,
13583
+ letterSpacing: s.tickLetterSpacing,
13584
+ color: labelColor,
13585
+ fontFamily: "monospace",
13586
+ fontWeight: emphasized ? "700" : "400"
13587
+ },
13588
+ children: tick.label
12136
13589
  }
12137
- ) : /* @__PURE__ */ jsx(
13590
+ );
13591
+ return /* @__PURE__ */ jsx(
12138
13592
  reactNative.View,
12139
13593
  {
12140
- accessibilityElementsHidden: true,
12141
- importantForAccessibility: "no-hide-descendants",
12142
- style: dotStyle,
12143
- testID: "capacity-band-chart-dot"
12144
- }
12145
- )
13594
+ testID: "zone-track-tick",
13595
+ style: {
13596
+ position: "absolute",
13597
+ left: pct(fraction(tick.value, min, max)),
13598
+ transform: [{ translateX: -s.tickCellWidth / 2 }],
13599
+ width: s.tickCellWidth,
13600
+ alignItems: "center"
13601
+ },
13602
+ children: tick.tooltip != null ? /* @__PURE__ */ jsx(Tooltip, { label: tick.tooltip, placement: "top", children: labelNode }) : labelNode
13603
+ },
13604
+ `label-${i}`
13605
+ );
13606
+ })
13607
+ }
13608
+ )
13609
+ ]
13610
+ }
13611
+ );
13612
+ }
13613
+
13614
+ // src/components/custom/Workout/FatigueMeter.tsx
13615
+ var { green, yellow, orange, red } = WORKOUT_TOKENS.scale;
13616
+ var DEFAULT_ZONE_COLORS = [green, yellow, orange, red];
13617
+ var DEFAULT_THRESHOLDS = [10, 20, 30];
13618
+ var DEFAULT_LABELS = ["fresh", "VL10", "VL20", "VL30", "stop"];
13619
+ var DEFAULT_MAX = 40;
13620
+ function FatigueMeter({
13621
+ value,
13622
+ max = DEFAULT_MAX,
13623
+ thresholds = DEFAULT_THRESHOLDS,
13624
+ zoneColors = DEFAULT_ZONE_COLORS,
13625
+ labels = DEFAULT_LABELS,
13626
+ size = "default",
13627
+ trackHeight,
13628
+ needleColor,
13629
+ className,
13630
+ ...props
13631
+ }) {
13632
+ const [t1, t22, t32] = thresholds;
13633
+ const zones = [
13634
+ { upTo: t1, color: zoneColors[0] },
13635
+ { upTo: t22, color: zoneColors[1] },
13636
+ { upTo: t32, color: zoneColors[2] },
13637
+ { upTo: max, color: zoneColors[3] }
13638
+ ];
13639
+ const tickValues = [0, t1, t22, t32, max];
13640
+ const ticks = tickValues.map((v, i) => ({ value: v, label: labels[i] }));
13641
+ return /* @__PURE__ */ jsx(
13642
+ ZoneTrack,
13643
+ {
13644
+ className,
13645
+ zones,
13646
+ min: 0,
13647
+ max,
13648
+ size,
13649
+ trackHeight,
13650
+ ticks,
13651
+ marker: { type: "needle", value, color: needleColor },
13652
+ accessibilityLabel: `Fatigue: ${value}% velocity loss`,
13653
+ testID: "fatigue-meter",
13654
+ ...props
13655
+ }
13656
+ );
13657
+ }
13658
+ var HEAT = WORKOUT_TOKENS.heatmap;
13659
+ var NEUTRAL_TRACK = primitiveColors.charcoal[200];
13660
+ var MONO = "monospace";
13661
+ var HEAT_BY_ZONE = {
13662
+ under: HEAT.under,
13663
+ maintenance: HEAT.maintenance,
13664
+ productive: HEAT.productive,
13665
+ approaching: HEAT.approaching,
13666
+ over: HEAT.over
13667
+ };
13668
+ var ZONE_DESCRIPTION = {
13669
+ under: "below MEV",
13670
+ maintenance: "building toward MAV",
13671
+ productive: "in the productive zone",
13672
+ approaching: "approaching MRV",
13673
+ over: "over MRV"
13674
+ };
13675
+ var LANDMARK_NAME = {
13676
+ MEV: "Minimum Effective Volume",
13677
+ MAV: "Maximum Adaptive Volume",
13678
+ MRV: "Maximum Recoverable Volume"
13679
+ };
13680
+ function zoneForSets(sets, { mev, mav, mrv }) {
13681
+ if (sets < mev) return "under";
13682
+ if (sets < mav) return "maintenance";
13683
+ if (sets >= mrv) return "over";
13684
+ const midpoint = (mav + mrv) / 2;
13685
+ return sets < midpoint ? "productive" : "approaching";
13686
+ }
13687
+ function VolumeLandmarkBar({
13688
+ muscle,
13689
+ currentSets,
13690
+ landmarks,
13691
+ width = 220,
13692
+ trackHeight = 10,
13693
+ scaleMax,
13694
+ className,
13695
+ style,
13696
+ ...props
13697
+ }) {
13698
+ const { mev, mav, mrv } = landmarks;
13699
+ const max = scaleMax ?? mrv * 1.2;
13700
+ const zone = zoneForSets(currentSets, landmarks);
13701
+ const fillColor = HEAT_BY_ZONE[zone];
13702
+ const pct2 = mav > 0 ? Math.round(currentSets / mav * 100) : 0;
13703
+ return /* @__PURE__ */ jsxs(
13704
+ reactNative.View,
13705
+ {
13706
+ className,
13707
+ style: [{ width, gap: 3 }, style],
13708
+ testID: "volume-landmark-bar",
13709
+ ...props,
13710
+ children: [
13711
+ /* @__PURE__ */ jsx(
13712
+ DataRow,
13713
+ {
13714
+ label: muscle,
13715
+ value: /* @__PURE__ */ jsxs(
13716
+ reactNative.Text,
13717
+ {
13718
+ testID: "volume-landmark-pct",
13719
+ style: { fontSize: 14, fontWeight: "700", fontFamily: MONO, color: fillColor },
13720
+ children: [
13721
+ pct2,
13722
+ "%"
13723
+ ]
13724
+ }
13725
+ ),
13726
+ className: "py-0",
13727
+ testID: "volume-landmark-header"
13728
+ }
13729
+ ),
13730
+ /* @__PURE__ */ jsx(
13731
+ ZoneTrack,
13732
+ {
13733
+ zones: [{ upTo: max, color: NEUTRAL_TRACK }],
13734
+ max,
13735
+ marker: {
13736
+ type: "fill",
13737
+ value: currentSets,
13738
+ color: fillColor,
13739
+ // Glow when in the optimal productive band — the "sweet spot" cue.
13740
+ glow: zone === "productive"
12146
13741
  },
12147
- `dot-${workout.date}-${i}`
12148
- );
12149
- })
13742
+ trackColor: NEUTRAL_TRACK,
13743
+ trackHeight,
13744
+ ticks: [
13745
+ { value: mev, label: "MEV", tooltip: `${LANDMARK_NAME.MEV} \xB7 ${mev} sets/wk` },
13746
+ {
13747
+ value: mav,
13748
+ label: "MAV",
13749
+ emphasized: true,
13750
+ tooltip: `${LANDMARK_NAME.MAV} (target) \xB7 ${mav} sets/wk`
13751
+ },
13752
+ { value: mrv, label: "MRV", tooltip: `${LANDMARK_NAME.MRV} \xB7 ${mrv} sets/wk` }
13753
+ ],
13754
+ accessibilityLabel: `${muscle} weekly volume: ${currentSets} sets, ${pct2}% of MAV target, ${ZONE_DESCRIPTION[zone]}`,
13755
+ testID: "volume-landmark-track"
13756
+ }
13757
+ )
13758
+ ]
13759
+ }
13760
+ );
13761
+ }
13762
+ var t16 = getSemanticColors("dark");
13763
+ var statusToken = {
13764
+ productive: "status-success",
13765
+ threshold: "status-warning",
13766
+ stop: "status-error"
13767
+ };
13768
+ var statusDotVariant = {
13769
+ productive: "success",
13770
+ threshold: "warning",
13771
+ stop: "error"
13772
+ };
13773
+ var defaultLabel = {
13774
+ productive: "Productive",
13775
+ threshold: "Threshold",
13776
+ stop: "Stop"
13777
+ };
13778
+ function statusPillColor(status) {
13779
+ return t16[statusToken[status]];
13780
+ }
13781
+ function StatusPill2({ status, label, className, style, ...props }) {
13782
+ const color = statusPillColor(status);
13783
+ const text = label ?? defaultLabel[status];
13784
+ return /* @__PURE__ */ jsxs(
13785
+ reactNative.View,
13786
+ {
13787
+ testID: "status-pill",
13788
+ className,
13789
+ style: [
13790
+ {
13791
+ flexDirection: "row",
13792
+ alignItems: "center",
13793
+ gap: 7,
13794
+ alignSelf: "flex-start",
13795
+ paddingVertical: 7,
13796
+ paddingHorizontal: 13,
13797
+ borderRadius: 9,
13798
+ backgroundColor: alpha(color, 0.14),
13799
+ borderWidth: 1,
13800
+ borderColor: alpha(color, 0.4)
13801
+ },
13802
+ style
13803
+ ],
13804
+ ...props,
13805
+ children: [
13806
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, children: /* @__PURE__ */ jsx(StatusDot, { variant: statusDotVariant[status], glow: true }) }),
13807
+ /* @__PURE__ */ jsx(reactNative.Text, { testID: "status-pill-label", style: { fontSize: 13, fontWeight: "800", color }, children: text })
13808
+ ]
13809
+ }
13810
+ );
13811
+ }
13812
+ var t17 = getSemanticColors("dark");
13813
+ var categoryToken = {
13814
+ productive: null,
13815
+ threshold: "status-warning",
13816
+ stop: "status-error"
13817
+ };
13818
+ var floodOpacity = {
13819
+ productive: 0,
13820
+ threshold: 0.12,
13821
+ stop: 0.18
13822
+ };
13823
+ function liveAuraColor(category) {
13824
+ const token = categoryToken[category];
13825
+ return token ? t17[token] : null;
13826
+ }
13827
+ function LiveAuraFrame({
13828
+ category,
13829
+ pulse = true,
13830
+ className,
13831
+ children,
13832
+ style,
13833
+ ...props
13834
+ }) {
13835
+ const color = liveAuraColor(category);
13836
+ const tint = color ? alpha(color, floodOpacity[category]) : "transparent";
13837
+ const floodStyle = {
13838
+ ...reactNative.StyleSheet.absoluteFillObject,
13839
+ ...reactNative.Platform.OS === "web" ? (
13840
+ // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
13841
+ { backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
13842
+ ) : { backgroundColor: tint }
13843
+ };
13844
+ return /* @__PURE__ */ jsxs(
13845
+ reactNative.View,
13846
+ {
13847
+ testID: "live-aura-frame",
13848
+ className,
13849
+ style: [
13850
+ {
13851
+ position: "relative",
13852
+ overflow: "hidden",
13853
+ borderRadius: 10,
13854
+ backgroundColor: t17["background-base"],
13855
+ borderWidth: 1,
13856
+ borderColor: t17["border-default"]
13857
+ },
13858
+ style
13859
+ ],
13860
+ ...props,
13861
+ children: [
13862
+ color && /* @__PURE__ */ jsx(
13863
+ reactNative.View,
13864
+ {
13865
+ testID: "live-aura-flood",
13866
+ accessibilityElementsHidden: true,
13867
+ pointerEvents: "none",
13868
+ className: category === "stop" && pulse ? "animate-pulse" : void 0,
13869
+ style: floodStyle
13870
+ }
13871
+ ),
13872
+ children
12150
13873
  ]
12151
13874
  }
12152
13875
  );
@@ -12357,28 +14080,28 @@ function Scatter({
12357
14080
  testID: "scatter-canvas",
12358
14081
  style: { position: "absolute", top: 0, left: 0, width, height, overflow: "hidden" },
12359
14082
  children: [
12360
- ticksOf(yd, TICK_COUNT).map((t12, i) => {
12361
- const y = toY(t12);
14083
+ ticksOf(yd, TICK_COUNT).map((t18, i) => {
14084
+ const y = toY(t18);
12362
14085
  return /* @__PURE__ */ jsx(
12363
14086
  reactNative.View,
12364
14087
  {
12365
14088
  accessibilityElementsHidden: true,
12366
14089
  testID: "scatter-gridline-y",
12367
14090
  style: { position: "absolute", left: PLOT_LEFT2, width: innerW, top: y, height: 1, backgroundColor: GRID_LINE2 },
12368
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t12) })
14091
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t18) })
12369
14092
  },
12370
14093
  `y-${i}`
12371
14094
  );
12372
14095
  }),
12373
- ticksOf(xd, TICK_COUNT).map((t12, i) => {
12374
- const x = toX(t12);
14096
+ ticksOf(xd, TICK_COUNT).map((t18, i) => {
14097
+ const x = toX(t18);
12375
14098
  return /* @__PURE__ */ jsx(
12376
14099
  reactNative.View,
12377
14100
  {
12378
14101
  accessibilityElementsHidden: true,
12379
14102
  testID: "scatter-gridline-x",
12380
14103
  style: { position: "absolute", top: PLOT_TOP, height: innerH, left: x, width: 1, backgroundColor: GRID_LINE2 },
12381
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t12) })
14104
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t18) })
12382
14105
  },
12383
14106
  `x-${i}`
12384
14107
  );
@@ -12443,7 +14166,7 @@ var STATUS_SUCCESS4 = sem2["status-success"];
12443
14166
  var STATUS_WARNING4 = sem2["status-warning"];
12444
14167
  var STATUS_ERROR4 = sem2["status-error"];
12445
14168
  var TRACK = "rgba(255,255,255,0.08)";
12446
- var DEFAULT_THRESHOLDS = [
14169
+ var DEFAULT_THRESHOLDS2 = [
12447
14170
  { value: 0, color: STATUS_ERROR4 },
12448
14171
  { value: 60, color: STATUS_WARNING4 },
12449
14172
  { value: 80, color: STATUS_SUCCESS4 }
@@ -12458,8 +14181,8 @@ function clamp012(n) {
12458
14181
  function formatValue2(v) {
12459
14182
  return Number.isInteger(v) ? `${v}` : v.toFixed(1);
12460
14183
  }
12461
- function bandColor2(fraction, bands, min, max) {
12462
- const value = min + fraction * (max - min);
14184
+ function bandColor2(fraction2, bands, min, max) {
14185
+ const value = min + fraction2 * (max - min);
12463
14186
  const sorted = [...bands].sort((a, b) => a.value - b.value);
12464
14187
  let color = sorted[0]?.color ?? STATUS_SUCCESS4;
12465
14188
  for (const band of sorted) {
@@ -12471,15 +14194,15 @@ function buildTicks(fill, size, activeColor) {
12471
14194
  const center = size / 2;
12472
14195
  const radius = center - size * 0.08;
12473
14196
  return Array.from({ length: SEGMENTS }, (_, i) => {
12474
- const fraction = (i + 0.5) / SEGMENTS;
12475
- const deg = ARC_START + fraction * ARC_SWEEP;
14197
+ const fraction2 = (i + 0.5) / SEGMENTS;
14198
+ const deg = ARC_START + fraction2 * ARC_SWEEP;
12476
14199
  const rad = deg * Math.PI / 180;
12477
14200
  return {
12478
14201
  key: i,
12479
14202
  left: center + radius * Math.cos(rad),
12480
14203
  top: center + radius * Math.sin(rad),
12481
14204
  rotate: `${deg - 90}deg`,
12482
- color: fraction <= fill ? activeColor(fraction) : TRACK
14205
+ color: fraction2 <= fill ? activeColor(fraction2) : TRACK
12483
14206
  };
12484
14207
  });
12485
14208
  }
@@ -12490,7 +14213,7 @@ function Gauge2({
12490
14213
  size = 160,
12491
14214
  label,
12492
14215
  unit,
12493
- thresholds = DEFAULT_THRESHOLDS,
14216
+ thresholds = DEFAULT_THRESHOLDS2,
12494
14217
  color,
12495
14218
  className,
12496
14219
  ...props
@@ -12513,23 +14236,23 @@ function Gauge2({
12513
14236
  testID: "gauge",
12514
14237
  ...props,
12515
14238
  children: [
12516
- ticks.map((t12) => /* @__PURE__ */ jsx(
14239
+ ticks.map((t18) => /* @__PURE__ */ jsx(
12517
14240
  reactNative.View,
12518
14241
  {
12519
14242
  accessibilityElementsHidden: true,
12520
14243
  testID: "gauge-segment",
12521
14244
  style: {
12522
14245
  position: "absolute",
12523
- left: t12.left - tickThickness / 2,
12524
- top: t12.top - tickLength / 2,
14246
+ left: t18.left - tickThickness / 2,
14247
+ top: t18.top - tickLength / 2,
12525
14248
  width: tickThickness,
12526
14249
  height: tickLength,
12527
14250
  borderRadius: tickThickness / 2,
12528
- backgroundColor: t12.color,
12529
- transform: [{ rotate: t12.rotate }]
14251
+ backgroundColor: t18.color,
14252
+ transform: [{ rotate: t18.rotate }]
12530
14253
  }
12531
14254
  },
12532
- t12.key
14255
+ t18.key
12533
14256
  )),
12534
14257
  /* @__PURE__ */ jsxs(reactNative.View, { style: { position: "absolute", top: 0, left: 0, width: size, height: size }, className: "items-center justify-center", children: [
12535
14258
  /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-baseline gap-0.5", children: [
@@ -12575,7 +14298,7 @@ function BrandLockup({
12575
14298
  }
12576
14299
  var stateConfig = {
12577
14300
  // live = vivid green with an expanding ring; rest = solid amber (no pulse — operator); idle is dim.
12578
- live: { label: "LIVE", color: "success", pulse: "ping", textColor: "primary" },
14301
+ live: { label: "LIVE", color: "live", pulse: "ping", textColor: "primary" },
12579
14302
  rest: { label: "REST", color: "warning", pulse: false, textColor: "primary" },
12580
14303
  idle: { label: "IDLE", color: "default", pulse: false, textColor: "secondary" }
12581
14304
  };
@@ -12739,6 +14462,127 @@ function TopBar({
12739
14462
  }
12740
14463
  );
12741
14464
  }
14465
+ function NavItem({
14466
+ icon,
14467
+ label,
14468
+ active = false,
14469
+ live = false,
14470
+ onPress,
14471
+ className
14472
+ }) {
14473
+ const glyphColor = active ? "text-brand-primary" : "text-text-tertiary";
14474
+ const labelColor = active ? "text-brand-primary" : live ? "text-status-live-muted" : "text-text-tertiary";
14475
+ return /* @__PURE__ */ jsxs(
14476
+ reactNative.Pressable,
14477
+ {
14478
+ accessibilityRole: "tab",
14479
+ accessibilityState: { selected: active },
14480
+ accessibilityLabel: label,
14481
+ onPress,
14482
+ className: cn("relative h-[46px] w-[60px] items-center justify-center", className),
14483
+ children: [
14484
+ active ? /* @__PURE__ */ jsx(
14485
+ reactNative.View,
14486
+ {
14487
+ testID: "nav-item-accent",
14488
+ className: "absolute left-0 top-[13px] bottom-[13px] w-[3px] rounded-r-[3px] bg-brand-primary"
14489
+ }
14490
+ ) : null,
14491
+ /* @__PURE__ */ jsxs(
14492
+ reactNative.View,
14493
+ {
14494
+ className: cn(
14495
+ "h-[46px] w-[46px] items-center justify-center gap-[3px] rounded-[11px]",
14496
+ glyphColor
14497
+ ),
14498
+ children: [
14499
+ icon,
14500
+ /* @__PURE__ */ jsx(
14501
+ Typography,
14502
+ {
14503
+ variant: "button",
14504
+ color: "inherit",
14505
+ className: cn("text-[8.5px] font-bold uppercase tracking-[0.4px]", labelColor),
14506
+ children: label
14507
+ }
14508
+ )
14509
+ ]
14510
+ }
14511
+ )
14512
+ ]
14513
+ }
14514
+ );
14515
+ }
14516
+ var defaultNavItems = [
14517
+ { key: "live", label: "Live", icon: /* @__PURE__ */ jsx(ActivityIcon, { size: 20, color: "currentColor" }) },
14518
+ { key: "review", label: "Review", icon: /* @__PURE__ */ jsx(HistoryIcon, { size: 20, color: "currentColor" }) },
14519
+ { key: "program", label: "Plan", icon: /* @__PURE__ */ jsx(LayersIcon, { size: 20, color: "currentColor" }) },
14520
+ { key: "body", label: "Body", icon: /* @__PURE__ */ jsx(PersonStandingIcon, { size: 20, color: "currentColor" }) }
14521
+ ];
14522
+ function SideNav({
14523
+ items = defaultNavItems,
14524
+ activeKey,
14525
+ onNavigate,
14526
+ liveKey = null,
14527
+ className
14528
+ }) {
14529
+ return /* @__PURE__ */ jsx(
14530
+ reactNative.View,
14531
+ {
14532
+ accessibilityRole: "tablist",
14533
+ className: cn(
14534
+ "w-[60px] items-center gap-[6px] border-r border-border bg-background-base py-3",
14535
+ className
14536
+ ),
14537
+ children: items.map((item) => /* @__PURE__ */ jsx(
14538
+ NavItem,
14539
+ {
14540
+ icon: item.icon,
14541
+ label: item.label,
14542
+ active: item.key === activeKey,
14543
+ live: item.key === liveKey && item.key !== activeKey,
14544
+ onPress: () => onNavigate?.(item.key)
14545
+ },
14546
+ item.key
14547
+ ))
14548
+ }
14549
+ );
14550
+ }
14551
+ var DEMO_DEVICES = [
14552
+ { id: "Voltra-A3F2", nickname: "Left Cable", slot: "L", state: "connected" },
14553
+ { id: "Voltra-9B1C", nickname: "Right Cable", slot: "R", state: "connected" }
14554
+ ];
14555
+ function ContentPlaceholder() {
14556
+ return /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1 items-center justify-center", children: /* @__PURE__ */ jsx(reactNative.Text, { className: "font-heading text-sm text-text-tertiary", children: "main content region" }) });
14557
+ }
14558
+ function DashboardShell({
14559
+ activeKey = "live",
14560
+ navItems,
14561
+ liveKey = null,
14562
+ state = "live",
14563
+ devices = DEMO_DEVICES,
14564
+ subtitle = "wall dashboard",
14565
+ onNavigate,
14566
+ onSelectDevice,
14567
+ children,
14568
+ className
14569
+ }) {
14570
+ return /* @__PURE__ */ jsxs(reactNative.View, { className: cn("flex-1 flex-row bg-surface-base", className), children: [
14571
+ /* @__PURE__ */ jsx(SideNav, { activeKey, items: navItems, liveKey, onNavigate }),
14572
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-1", children: [
14573
+ /* @__PURE__ */ jsx(
14574
+ TopBar,
14575
+ {
14576
+ state,
14577
+ devices,
14578
+ subtitle,
14579
+ onSelectDevice
14580
+ }
14581
+ ),
14582
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", children: children ?? /* @__PURE__ */ jsx(ContentPlaceholder, {}) })
14583
+ ] })
14584
+ ] });
14585
+ }
12742
14586
 
12743
14587
  // src/utils/form.ts
12744
14588
  function getFieldValidationProps(state) {
@@ -12832,13 +14676,16 @@ exports.Accordion = Accordion;
12832
14676
  exports.AccordionButton = AccordionButton;
12833
14677
  exports.AccordionItem = AccordionItem;
12834
14678
  exports.AccordionPanel = AccordionPanel;
14679
+ exports.ActivityIcon = ActivityIcon;
12835
14680
  exports.Alert = Alert;
12836
14681
  exports.AlertDescription = AlertDescription;
12837
14682
  exports.AlertTitle = AlertTitle;
14683
+ exports.AlertTriangleIcon = AlertTriangleIcon;
12838
14684
  exports.Autocomplete = Autocomplete;
12839
14685
  exports.Avatar = Avatar;
12840
14686
  exports.AvatarBadge = AvatarBadge;
12841
14687
  exports.AvatarGroup = AvatarGroup;
14688
+ exports.AwardIcon = AwardIcon;
12842
14689
  exports.Badge = Badge;
12843
14690
  exports.BadgeText = BadgeText;
12844
14691
  exports.BaseBadge = BaseBadge;
@@ -12863,10 +14710,13 @@ exports.CardTitle = CardTitle;
12863
14710
  exports.Checkbox = Checkbox;
12864
14711
  exports.CheckboxGroup = CheckboxGroup;
12865
14712
  exports.Chip = Chip;
14713
+ exports.CircleSlashIcon = CircleSlashIcon;
12866
14714
  exports.CircularProgress = CircularProgress;
14715
+ exports.CircularTimer = CircularTimer;
12867
14716
  exports.Collapse = Collapse;
12868
14717
  exports.CollapseButton = CollapseButton;
12869
14718
  exports.CollapseContent = CollapseContent;
14719
+ exports.DashboardShell = DashboardShell;
12870
14720
  exports.DataRow = DataRow;
12871
14721
  exports.DateTime = DateTime;
12872
14722
  exports.DeviationBar = DeviationBar;
@@ -12881,6 +14731,10 @@ exports.DrawerHeader = DrawerHeader;
12881
14731
  exports.DumbbellIcon = DumbbellIcon;
12882
14732
  exports.EmptyState = EmptyState;
12883
14733
  exports.ExerciseCard = ExerciseCard;
14734
+ exports.ExerciseCardHeading = ExerciseCardHeading;
14735
+ exports.ExerciseHeading = ExerciseHeading;
14736
+ exports.ExerciseIndicator = ExerciseIndicator;
14737
+ exports.FatigueMeter = FatigueMeter;
12884
14738
  exports.FormActions = FormActions;
12885
14739
  exports.FormField = FormField;
12886
14740
  exports.FormRow = FormRow;
@@ -12889,20 +14743,26 @@ exports.Gauge = Gauge2;
12889
14743
  exports.HStack = HStack;
12890
14744
  exports.Heading = Heading;
12891
14745
  exports.HelpTip = HelpTip;
14746
+ exports.HistoryIcon = HistoryIcon;
14747
+ exports.INDICATOR_PRECEDENCE = INDICATOR_PRECEDENCE;
12892
14748
  exports.IconBox = IconBox;
12893
14749
  exports.Indicator = Indicator;
14750
+ exports.InfoIcon = InfoIcon;
12894
14751
  exports.Input = Input;
12895
14752
  exports.InputBar = InputBar;
12896
14753
  exports.InputGroup = InputGroup;
12897
14754
  exports.IntensityBar = IntensityBar;
12898
14755
  exports.Label = Label;
12899
14756
  exports.LabelWithHelp = LabelWithHelp;
14757
+ exports.LayersIcon = LayersIcon;
12900
14758
  exports.Link = Link;
12901
14759
  exports.ListItem = ListItem;
12902
14760
  exports.ListItemContent = ListItemContent;
12903
14761
  exports.ListItemDivider = ListItemDivider;
12904
14762
  exports.ListItemIcon = ListItemIcon;
12905
14763
  exports.ListItemTrailing = ListItemTrailing;
14764
+ exports.LiveAuraFrame = LiveAuraFrame;
14765
+ exports.METRIC_FONT = METRIC_FONT;
12906
14766
  exports.Menu = Menu;
12907
14767
  exports.MenuDivider = MenuDivider;
12908
14768
  exports.MenuGroup = MenuGroup;
@@ -12913,7 +14773,9 @@ exports.MesoCard = MesoCard;
12913
14773
  exports.MesoProgressBar = MesoProgressBar;
12914
14774
  exports.MesoStatusCard = MesoStatusCard;
12915
14775
  exports.Metric = Metric;
14776
+ exports.MetricCell = MetricCell;
12916
14777
  exports.MetricGroup = MetricGroup;
14778
+ exports.MetricTiles = MetricTiles;
12917
14779
  exports.Modal = Modal;
12918
14780
  exports.ModalBody = ModalBody;
12919
14781
  exports.ModalCloseButton = ModalCloseButton;
@@ -12922,9 +14784,11 @@ exports.ModalFooter = ModalFooter;
12922
14784
  exports.ModalHeader = ModalHeader;
12923
14785
  exports.ModalTitle = ModalTitle;
12924
14786
  exports.MuscleGroupChip = MuscleGroupChip;
14787
+ exports.NavItem = NavItem;
12925
14788
  exports.Overline = Overline;
12926
14789
  exports.Paragraph = Paragraph;
12927
14790
  exports.PasswordInput = PasswordInput;
14791
+ exports.PersonStandingIcon = PersonStandingIcon;
12928
14792
  exports.Pill = Pill;
12929
14793
  exports.PlaceholderStrip = PlaceholderStrip;
12930
14794
  exports.Popover = Popover;
@@ -12939,13 +14803,26 @@ exports.Radio = Radio;
12939
14803
  exports.RadioGroup = RadioGroup;
12940
14804
  exports.ReadinessCheck = ReadinessCheck;
12941
14805
  exports.RestTimer = RestTimer;
14806
+ exports.SET_STRIP_VARIABLE_COLOR = SET_STRIP_VARIABLE_COLOR;
14807
+ exports.SET_STRIP_ZONES = SET_STRIP_ZONES;
14808
+ exports.ScaleIcon = ScaleIcon;
12942
14809
  exports.Scatter = Scatter;
14810
+ exports.ScheduleTiles = ScheduleTiles;
12943
14811
  exports.Section = Section;
12944
14812
  exports.SectionContent = SectionContent;
12945
14813
  exports.SectionHeader = SectionHeader;
14814
+ exports.SegmentedBar = SegmentedBar;
14815
+ exports.SegmentedProgressBar = SegmentedProgressBar;
12946
14816
  exports.Select = Select;
14817
+ exports.SessionHeader = SessionHeader;
14818
+ exports.SessionRail = SessionRail;
12947
14819
  exports.SessionStatePill = SessionStatePill;
14820
+ exports.SetBar = SetBar;
12948
14821
  exports.SetRow = SetRow;
14822
+ exports.SetStrip = SetStrip;
14823
+ exports.SetTableHeader = SetTableHeader;
14824
+ exports.SetsRepsLoad = SetsRepsLoad;
14825
+ exports.SideNav = SideNav;
12949
14826
  exports.Sidebar = Sidebar;
12950
14827
  exports.SidebarContent = SidebarContent;
12951
14828
  exports.SidebarDivider = SidebarDivider;
@@ -12963,6 +14840,7 @@ exports.Spinner = Spinner;
12963
14840
  exports.Stack = Stack;
12964
14841
  exports.StarIcon = StarIcon;
12965
14842
  exports.StatusDot = StatusDot;
14843
+ exports.StatusPill = StatusPill2;
12966
14844
  exports.StepContent = StepContent;
12967
14845
  exports.StepIndicator = StepIndicator;
12968
14846
  exports.StepLabel = StepLabel;
@@ -12988,6 +14866,8 @@ exports.TableRow = TableRow;
12988
14866
  exports.Tabs = Tabs;
12989
14867
  exports.TempoBar = TempoBar;
12990
14868
  exports.TempoDisplay = TempoDisplay;
14869
+ exports.Tile = Tile;
14870
+ exports.TimerReadout = TimerReadout;
12991
14871
  exports.Toast = Toast;
12992
14872
  exports.ToastProvider = ToastProvider;
12993
14873
  exports.ToolbarButton = ToolbarButton;
@@ -12995,14 +14875,17 @@ exports.ToolbarButtonGroup = ToolbarButtonGroup;
12995
14875
  exports.Tooltip = Tooltip;
12996
14876
  exports.TopBar = TopBar;
12997
14877
  exports.Treemap = Treemap;
14878
+ exports.TrendingDownIcon = TrendingDownIcon;
12998
14879
  exports.Typography = Typography;
12999
14880
  exports.VStack = VStack;
13000
14881
  exports.VelocityStrip = VelocityStrip;
13001
14882
  exports.VoltrasMark = VoltrasMark;
14883
+ exports.VolumeLandmarkBar = VolumeLandmarkBar;
13002
14884
  exports.WeekRow = WeekRow;
13003
14885
  exports.WeightBadge = WeightBadge;
13004
14886
  exports.WorkoutCard = WorkoutCard;
13005
14887
  exports.WorkoutPill = WorkoutPill;
14888
+ exports.ZoneTrack = ZoneTrack;
13006
14889
  exports.alpha = alpha;
13007
14890
  exports.applyThemePreset = applyThemePreset;
13008
14891
  exports.audiobookPreset = audiobookPreset;
@@ -13022,11 +14905,13 @@ exports.createRaisedHoverShadow = createRaisedHoverShadow;
13022
14905
  exports.createRaisedShadow = createRaisedShadow;
13023
14906
  exports.darkThemeCSSVars = darkThemeCSSVars;
13024
14907
  exports.darken = darken;
14908
+ exports.defaultNavItems = defaultNavItems;
13025
14909
  exports.defaultPreset = defaultPreset;
13026
14910
  exports.discreteRainbow = discreteRainbow;
13027
14911
  exports.divergingScale = divergingScale;
13028
14912
  exports.elevationSystem = elevationSystem;
13029
14913
  exports.formatDateTime = formatDateTime;
14914
+ exports.formatDuration = formatDuration;
13030
14915
  exports.formatPrescription = formatPrescription;
13031
14916
  exports.formatSignedPct = formatSignedPct;
13032
14917
  exports.formatVelocity = formatVelocity;
@@ -13063,7 +14948,10 @@ exports.isValidEmail = isValidEmail;
13063
14948
  exports.lightThemeCSSVars = lightThemeCSSVars;
13064
14949
  exports.lighten = lighten;
13065
14950
  exports.linearGradient = linearGradient;
14951
+ exports.liveAuraColor = liveAuraColor;
13066
14952
  exports.neumorphicShadows = neumorphicShadows;
14953
+ exports.paceTone = paceTone;
14954
+ exports.paceToneColor = paceToneColor;
13067
14955
  exports.primitiveBorderRadius = primitiveBorderRadius;
13068
14956
  exports.primitiveBreakpoints = primitiveBreakpoints;
13069
14957
  exports.primitiveColors = primitiveColors;
@@ -13073,6 +14961,7 @@ exports.primitiveSpacing = primitiveSpacing;
13073
14961
  exports.primitiveTypography = primitiveTypography;
13074
14962
  exports.primitiveZIndex = primitiveZIndex;
13075
14963
  exports.resolveColor = resolveColor;
14964
+ exports.resolveIndicator = resolveIndicator;
13076
14965
  exports.rgbToHex = rgbToHex;
13077
14966
  exports.rgbToHsv = rgbToHsv;
13078
14967
  exports.roundRpe = roundRpe;
@@ -13084,11 +14973,14 @@ exports.semanticColorsLight = semanticColorsLight;
13084
14973
  exports.semanticTypography = semanticTypography;
13085
14974
  exports.sequentialEffort = sequentialEffort;
13086
14975
  exports.shadowColors = shadowColors;
14976
+ exports.statusPillColor = statusPillColor;
13087
14977
  exports.surfaceGradient = surfaceGradient;
13088
14978
  exports.useTable = useTable;
14979
+ exports.useTimer = useTimer;
13089
14980
  exports.useToast = useToast;
13090
14981
  exports.useToolbarButton = useToolbarButton;
13091
14982
  exports.validateCssPropertyManifest = validateCssPropertyManifest;
13092
14983
  exports.validationRules = validationRules;
14984
+ exports.velocityZoneColor = velocityZoneColor;
13093
14985
  //# sourceMappingURL=index.js.map
13094
14986
  //# sourceMappingURL=index.js.map