@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/pages.js CHANGED
@@ -10,21 +10,18 @@ var tailwindMerge = require('tailwind-merge');
10
10
 
11
11
  // src/theme/tokens/primitives.ts
12
12
  var primitiveColors = {
13
- // Blue scale (primary brand)
13
+ // Blue scale
14
14
  blue: {
15
15
  400: "#60A5FA",
16
16
  600: "#5048E5",
17
- // Primary main
18
17
  700: "#3832A0"},
19
- // Red scale (error)
18
+ // Red scale
20
19
  red: {
21
- // Error light
22
20
  500: "#EF4444",
23
21
  600: "#D14343"},
24
- // Vivid red scale (alert)
22
+ // Vivid red scale
25
23
  redVivid: {
26
24
  400: "#FF6070",
27
- // Error vivid light
28
25
  500: "#FF4757",
29
26
  700: "#C42539"},
30
27
  // Neutral/Gray scale
@@ -38,6 +35,8 @@ var primitiveColors = {
38
35
  900: "#111827"},
39
36
  // Charcoal scale (dark backgrounds)
40
37
  charcoal: {
38
+ 0: "#6E6E6E",
39
+ 100: "#4C4C4C",
41
40
  200: "#3C3C3C",
42
41
  300: "#2C2C2C",
43
42
  400: "#1F1F1F",
@@ -67,11 +66,16 @@ var primitiveRamps = {
67
66
  50: "#FFF7DD",
68
67
  200: "#FFD352",
69
68
  300: "#F9B415",
69
+ // pin
70
+ 400: "#E08C00",
70
71
  500: "#C27400"},
71
72
  green: {
72
73
  50: "#E3FFEE",
73
74
  200: "#58F69E",
74
75
  300: "#2ED573",
76
+ // pin
77
+ 400: "#21C05D",
78
+ 500: "#22A444",
75
79
  600: "#298732"},
76
80
  cyan: {
77
81
  // pin
@@ -79,7 +83,8 @@ var primitiveRamps = {
79
83
  500: "#2697B7",
80
84
  600: "#307B9B",
81
85
  700: "#2A617F",
82
- 800: "#22465F"},
86
+ 800: "#22465F",
87
+ 900: "#0B3149"},
83
88
  blue: {
84
89
  50: "#EFF8FF",
85
90
  300: "#78C2FF",
@@ -134,15 +139,22 @@ var WORKOUT_TOKENS = {
134
139
  // Canonical 4-band performance scale — the single source for BOTH the
135
140
  // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
136
141
  // is intentionally inverted between the two consumers: for velocity, green =
137
- // fastest/best; for RPE, green = easiest. This is the [0,1,2,4] subsample of
138
- // the canonical `sequentialEffort` primitive. The `orange` key is the band
139
- // label. Consumed inline (RN).
142
+ // fastest/best; for RPE, green = easiest. This is the [0,2,3,4] subsample of
143
+ // the canonical `sequentialEffort` primitive: an EVEN hue walk across the ramp
144
+ // (green gold → orange → red). The earlier [0,1,2,4] spent two of its four
145
+ // bands on adjacent yellows (amber-200 + amber-300) and skipped orange, reading
146
+ // yellow-heavy with an abrupt jump to red; sampling amber-300 + orange-400
147
+ // instead gives four cleanly separable bands that hold at 3px strip scale.
148
+ // Consumed inline (RN).
140
149
  scale: {
141
150
  green: sequentialEffort[0],
142
- yellow: sequentialEffort[1],
143
- orange: sequentialEffort[2],
144
- // gold band
151
+ // green-300
152
+ yellow: sequentialEffort[2],
153
+ // amber-300 (gold)
154
+ orange: sequentialEffort[3],
155
+ // orange-400 (true orange)
145
156
  red: sequentialEffort[4]
157
+ // red-600
146
158
  },
147
159
  // Surface and border colors were removed here: those flip with the theme, so
148
160
  // components use the CSS custom properties (var(--color-surface-*/border-*))
@@ -161,12 +173,6 @@ var WORKOUT_TOKENS = {
161
173
  function roundRpe(rpe) {
162
174
  return Math.round(rpe * 2) / 2;
163
175
  }
164
- function rpeColor(rpe) {
165
- if (rpe >= 9.5) return WORKOUT_TOKENS.scale.red;
166
- if (rpe >= 8.5) return WORKOUT_TOKENS.scale.orange;
167
- if (rpe >= 7) return WORKOUT_TOKENS.scale.yellow;
168
- return WORKOUT_TOKENS.scale.green;
169
- }
170
176
  function roundWeight(weight) {
171
177
  return Math.round(weight);
172
178
  }
@@ -194,14 +200,258 @@ function wrapJsx(fn) {
194
200
  var jsx = wrapJsx(jsxRuntime.jsx);
195
201
  var jsxs = wrapJsx(jsxRuntime.jsxs);
196
202
 
203
+ // src/components/custom/Workout/SegmentedBar.tsx
204
+ var PULSE_HALF_MS = 950;
205
+ var PULSE_MIN_OPACITY = 0.45;
206
+ var SEGMENTED_BAR_GAP = 5;
207
+ function usePulse(active) {
208
+ const [value] = react.useState(() => new reactNative.Animated.Value(0));
209
+ react.useEffect(() => {
210
+ if (!active) return;
211
+ const loop = reactNative.Animated.loop(
212
+ reactNative.Animated.sequence([
213
+ reactNative.Animated.timing(value, {
214
+ toValue: 1,
215
+ duration: PULSE_HALF_MS,
216
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
217
+ useNativeDriver: false
218
+ }),
219
+ reactNative.Animated.timing(value, {
220
+ toValue: 0,
221
+ duration: PULSE_HALF_MS,
222
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
223
+ useNativeDriver: false
224
+ })
225
+ ])
226
+ );
227
+ loop.start();
228
+ return () => loop.stop();
229
+ }, [active, value]);
230
+ return value;
231
+ }
232
+ function SegmentedBar({
233
+ segments,
234
+ height = 8,
235
+ gap = SEGMENTED_BAR_GAP,
236
+ radius = 2,
237
+ marker = null,
238
+ segmentTestID,
239
+ style,
240
+ ...props
241
+ }) {
242
+ const pulse = usePulse(segments.some((s) => s.pulse));
243
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
244
+ segments.map((seg, i) => {
245
+ const fillStyle = {
246
+ width: `${(seg.fill ?? 1) * 100}%`,
247
+ height: "100%",
248
+ backgroundColor: seg.color,
249
+ opacity: seg.opacity
250
+ };
251
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
252
+ return /* @__PURE__ */ jsx(
253
+ reactNative.View,
254
+ {
255
+ style: {
256
+ flex: seg.weight ?? 1,
257
+ minWidth: 0,
258
+ height: "100%",
259
+ marginLeft: seg.leadingGap,
260
+ borderRadius: radius,
261
+ overflow: "hidden"
262
+ },
263
+ children: seg.pulse ? /* @__PURE__ */ jsx(
264
+ reactNative.Animated.View,
265
+ {
266
+ style: {
267
+ ...fillStyle,
268
+ ...seg.pulseColor ? {
269
+ backgroundColor: pulse.interpolate({
270
+ inputRange: [0, 1],
271
+ outputRange: seg.pulseColor
272
+ })
273
+ } : {
274
+ opacity: pulse.interpolate({
275
+ inputRange: [0, 1],
276
+ outputRange: [PULSE_MIN_OPACITY, 1]
277
+ })
278
+ }
279
+ },
280
+ accessibilityElementsHidden: true,
281
+ testID
282
+ }
283
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
284
+ },
285
+ i
286
+ );
287
+ }),
288
+ marker ? /* @__PURE__ */ jsx(
289
+ reactNative.View,
290
+ {
291
+ style: {
292
+ position: "absolute",
293
+ left: `${marker.position * 100}%`,
294
+ top: 0,
295
+ bottom: 0,
296
+ width: 2,
297
+ backgroundColor: marker.color
298
+ },
299
+ accessibilityElementsHidden: true,
300
+ testID: "segmented-bar-marker"
301
+ }
302
+ ) : null
303
+ ] });
304
+ }
305
+
306
+ // src/components/custom/Workout/SetBar.tsx
307
+ var SET_STRIP_ZONES = {
308
+ slow: primitiveRamps.red[600],
309
+ moderate: primitiveRamps.orange[400],
310
+ fast: primitiveRamps.amber[300],
311
+ fastest: primitiveRamps.green[300]
312
+ };
313
+ var TODO_COLOR = primitiveColors.charcoal[300];
314
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
315
+ var NOTCH_GAP = 2;
316
+ var CLUSTER_GAP = 3;
317
+ var MYO_UPCOMING_CLUSTERS = 3;
318
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
319
+ var MYO_UPCOMING_FADE = 0.6;
320
+ var PULSE_RANGE = {
321
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
322
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
323
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
324
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
325
+ };
326
+ function velocityZoneColor(v) {
327
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
328
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
329
+ if (v < 1) return SET_STRIP_ZONES.fast;
330
+ return SET_STRIP_ZONES.fastest;
331
+ }
332
+ function chunkedSegments(chunks, gap) {
333
+ return chunks.flatMap(
334
+ (vels, ci) => vels.map((v, ri) => ({
335
+ color: velocityZoneColor(v),
336
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
337
+ }))
338
+ );
339
+ }
340
+ function rangeSegments(floor, max, doneVels) {
341
+ return Array.from({ length: max }, (_, i) => {
342
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
343
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
344
+ });
345
+ }
346
+ function myoUpcomingSegments(activationLen, clusterCount) {
347
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
348
+ const trail = Array.from(
349
+ { length: clusterCount },
350
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
351
+ color: SET_STRIP_VARIABLE_COLOR,
352
+ opacity: MYO_UPCOMING_FADE ** ci,
353
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
354
+ }))
355
+ ).flat();
356
+ return [...activation, ...trail];
357
+ }
358
+ function setSegments(set) {
359
+ if (set.status === "todo") {
360
+ return [{ color: TODO_COLOR }];
361
+ }
362
+ if (set.status === "done") {
363
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
364
+ }
365
+ if (set.status === "range") {
366
+ return rangeSegments(set.floor, set.max, set.doneVels);
367
+ }
368
+ if (set.status === "drop") {
369
+ return chunkedSegments(set.subloads, NOTCH_GAP);
370
+ }
371
+ if (set.status === "myo") {
372
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
373
+ }
374
+ if (set.status === "myo-upcoming") {
375
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
376
+ }
377
+ const performed = set.velocities.map((v) => {
378
+ const color = velocityZoneColor(v);
379
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
380
+ });
381
+ const remaining = Math.max(0, set.planned - set.velocities.length);
382
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
383
+ }
384
+ function setBarSegmentTestID(seg) {
385
+ if (seg.pulse) return "set-strip-pulse";
386
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
387
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
388
+ }
389
+ function SetBar({ set, height = 8 }) {
390
+ return /* @__PURE__ */ jsx(
391
+ SegmentedBar,
392
+ {
393
+ segments: setSegments(set),
394
+ height,
395
+ gap: 0,
396
+ radius: 0,
397
+ segmentTestID: setBarSegmentTestID,
398
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
399
+ testID: "set-strip-set"
400
+ }
401
+ );
402
+ }
403
+
404
+ // src/components/custom/Workout/SetStrip.tsx
405
+ var SET_STRIP_GAP = 5;
406
+ function setCategory(set) {
407
+ switch (set.status) {
408
+ case "done":
409
+ case "drop":
410
+ case "myo":
411
+ return "done";
412
+ case "active":
413
+ return "active";
414
+ case "range":
415
+ return set.doneVels.length > 0 ? "active" : "upcoming";
416
+ case "todo":
417
+ case "myo-upcoming":
418
+ return "upcoming";
419
+ }
420
+ }
421
+ function describeSets(sets) {
422
+ const done = sets.filter((s) => setCategory(s) === "done").length;
423
+ const active = sets.filter((s) => setCategory(s) === "active").length;
424
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
425
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
426
+ }
427
+ function SetStrip({ sets, height = 8, className, ...props }) {
428
+ return /* @__PURE__ */ jsx(
429
+ reactNative.View,
430
+ {
431
+ className,
432
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
433
+ accessibilityRole: "image",
434
+ accessibilityLabel: describeSets(sets),
435
+ testID: "set-strip",
436
+ ...props,
437
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
438
+ }
439
+ );
440
+ }
441
+
197
442
  // src/components/custom/Workout/VelocityStrip.tsx
198
443
  var VEL_COLORS = WORKOUT_TOKENS.scale;
199
- var zoneIdToScaleToken = {
200
- speed: "green",
201
- power: "yellow",
202
- strengthSpeed: "orange",
203
- maximalStrength: "red",
204
- grinding: "red"
444
+ var bandIdToEffortColor = {
445
+ speed: sequentialEffort[0],
446
+ // green-300
447
+ power: sequentialEffort[2],
448
+ // amber-300 (gold)
449
+ strengthSpeed: sequentialEffort[3],
450
+ // orange-400
451
+ maximalStrength: sequentialEffort[4],
452
+ // red-600
453
+ grinding: sequentialEffort[5]
454
+ // red-700 — the distinct 5th band
205
455
  };
206
456
  function getVelocityZoneColor(velocity) {
207
457
  if (velocity >= 1) return "vel-green";
@@ -240,14 +490,140 @@ function classifyBand(velocity, bands) {
240
490
  return bands[bands.length - 1];
241
491
  }
242
492
  function bandColor(band) {
243
- const token = zoneIdToScaleToken[band.id];
244
- return token ? VEL_COLORS[token] : VEL_COLORS.green;
493
+ return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
245
494
  }
246
495
  function getLossStyle(loss) {
247
496
  if (loss > 25) return { color: VEL_COLORS.red };
248
497
  if (loss > 20) return { color: VEL_COLORS.orange };
249
498
  return null;
250
499
  }
500
+ var REP_GAP = 2;
501
+ var WIDE_GAP = 8;
502
+ var TODO_COLOR2 = primitiveColors.charcoal[300];
503
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
504
+ function deriveDoneVelocities(set) {
505
+ switch (set.type) {
506
+ case "drop":
507
+ return set.subloads.flat();
508
+ case "myo":
509
+ return [...set.activation, ...set.clusters.flat()];
510
+ default:
511
+ return set.velocities;
512
+ }
513
+ }
514
+ function chunkedRepSlots(chunks) {
515
+ const slots = [];
516
+ chunks.forEach((chunk) => {
517
+ chunk.forEach((velocity, ri) => {
518
+ const first = slots.length === 0;
519
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
520
+ });
521
+ });
522
+ return slots;
523
+ }
524
+ function buildSlots(set) {
525
+ switch (set.type) {
526
+ case "straight": {
527
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
528
+ return Array.from({ length: total }, (_, i) => {
529
+ const done = i < set.velocities.length;
530
+ return {
531
+ kind: done ? "rep" : "todo",
532
+ velocity: done ? set.velocities[i] : void 0,
533
+ leadingGap: i === 0 ? 0 : REP_GAP
534
+ };
535
+ });
536
+ }
537
+ case "range": {
538
+ const total = Math.max(set.max, set.velocities.length);
539
+ return Array.from({ length: total }, (_, i) => {
540
+ const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
541
+ return {
542
+ kind,
543
+ velocity: kind === "rep" ? set.velocities[i] : void 0,
544
+ leadingGap: i === 0 ? 0 : REP_GAP
545
+ };
546
+ });
547
+ }
548
+ case "amrap": {
549
+ const reps = set.velocities.map((velocity, i) => ({
550
+ kind: "rep",
551
+ velocity,
552
+ leadingGap: i === 0 ? 0 : REP_GAP
553
+ }));
554
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
555
+ return reps;
556
+ }
557
+ case "drop":
558
+ return chunkedRepSlots(set.subloads);
559
+ case "myo": {
560
+ const slots = chunkedRepSlots([set.activation, ...set.clusters]);
561
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
562
+ return slots;
563
+ }
564
+ case "cluster": {
565
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
566
+ return Array.from({ length: total }, (_, i) => {
567
+ const done = i < set.velocities.length;
568
+ const boundary = i > 0 && i % set.groupSize === 0;
569
+ return {
570
+ kind: done ? "rep" : "todo",
571
+ velocity: done ? set.velocities[i] : void 0,
572
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
573
+ };
574
+ });
575
+ }
576
+ }
577
+ }
578
+ function setAccessibilityLabel(set, repCount) {
579
+ switch (set.type) {
580
+ case "straight":
581
+ return `Velocity strip, straight set, ${repCount} reps`;
582
+ case "range":
583
+ return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
584
+ case "amrap":
585
+ return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
586
+ case "drop":
587
+ return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
588
+ case "myo":
589
+ return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
590
+ case "cluster":
591
+ return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
592
+ }
593
+ }
594
+ function slotAccessibilityLabel(slot, index) {
595
+ switch (slot.kind) {
596
+ case "rep":
597
+ return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
598
+ case "todo":
599
+ return `Rep ${index + 1}: planned`;
600
+ case "variable":
601
+ return `Rep ${index + 1}: variable`;
602
+ case "continue":
603
+ return "Keep going";
604
+ }
605
+ }
606
+ var EXPANDED_TODO_STUB_PCT = 16;
607
+ var EXPANDED_ENCODED_PCT = 45;
608
+ var EXPANDED_HEIGHT = 60;
609
+ var FIXED_MAX_VELOCITY = 1.15;
610
+ var BARE_STUB_HEIGHT = 3;
611
+ var HERO_HEIGHT = 220;
612
+ var HERO_LABEL_HEADROOM = 20;
613
+ var HERO_BAR_GAP = 8;
614
+ var HERO_BAR_MAX_WIDTH = 52;
615
+ var HERO_BAR_RADIUS = 5;
616
+ var HERO_MIN_BAR_HEIGHT = 4;
617
+ var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
618
+ var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
619
+ function scaleDenominator(scale, maxVelocity) {
620
+ return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * 1.15;
621
+ }
622
+ function bareSlotHeight(slot, height, denom) {
623
+ if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
624
+ const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
625
+ return Math.max(BARE_STUB_HEIGHT, ratio * height);
626
+ }
251
627
  function getReducedMotionPreference() {
252
628
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
253
629
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -267,33 +643,11 @@ function usePrefersReducedMotion() {
267
643
  var ANIMATION_DURATION = 400;
268
644
  var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
269
645
  var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
270
- function VelocityStrip({
271
- velocities,
272
- zones,
273
- liveRepIndex,
274
- expanded = false,
275
- onToggle,
276
- onRepPress,
277
- variant = "full",
278
- showInfo = true,
279
- className,
280
- ...props
281
- }) {
282
- const maxVelocity = Math.max(...velocities, 0);
283
- const meanVelocity = calculateMeanVelocity(velocities);
284
- const loss = calculateVelocityLoss(velocities);
285
- const hasZones = zones != null && zones.length > 0;
286
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
287
- const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
646
+ function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
288
647
  const prefersReducedMotion = usePrefersReducedMotion();
289
- const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? 60 : 3));
290
- const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
291
- const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
292
648
  const [liveScale] = react.useState(() => new reactNative.Animated.Value(1));
293
- const liveVelocity = liveRepIndex != null ? velocities[liveRepIndex] : void 0;
294
- const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
295
649
  react.useEffect(() => {
296
- if (variant === "mini" || liveRepIndex == null || liveVelocity == null) return;
650
+ if (!active || liveRepIndex == null || liveVelocity == null) return;
297
651
  if (prefersReducedMotion) {
298
652
  liveScale.setValue(1);
299
653
  return;
@@ -323,12 +677,201 @@ function VelocityStrip({
323
677
  useNativeDriver: true
324
678
  }).start();
325
679
  }
326
- }, [variant, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
680
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
681
+ return liveScale;
682
+ }
683
+ function HeroVelocityChart({
684
+ doneVelocities,
685
+ barColorFor,
686
+ scaleDenom,
687
+ referenceVelocity,
688
+ liveRepIndex,
689
+ isNewPeak,
690
+ targetReps,
691
+ height,
692
+ className,
693
+ viewProps
694
+ }) {
695
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
696
+ const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
697
+ const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
698
+ const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
699
+ const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
700
+ const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
701
+ const repCount = doneVelocities.length;
702
+ const total = Math.max(repCount, targetReps ?? repCount);
703
+ const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
704
+ const { style: externalStyle, ...restProps } = viewProps;
705
+ return /* @__PURE__ */ jsx(
706
+ reactNative.View,
707
+ {
708
+ className,
709
+ style: [{ height }, externalStyle],
710
+ accessibilityRole: "image",
711
+ accessibilityLabel: label,
712
+ testID: "velocity-strip-hero",
713
+ ...restProps,
714
+ children: /* @__PURE__ */ jsxs(
715
+ reactNative.View,
716
+ {
717
+ style: {
718
+ flex: 1,
719
+ flexDirection: "row",
720
+ alignItems: "flex-end",
721
+ gap: HERO_BAR_GAP,
722
+ position: "relative",
723
+ borderBottomWidth: 2,
724
+ borderBottomColor: HERO_PENDING_COLOR
725
+ },
726
+ children: [
727
+ referencePx > 0 && /* @__PURE__ */ jsx(
728
+ reactNative.View,
729
+ {
730
+ accessibilityElementsHidden: true,
731
+ style: {
732
+ position: "absolute",
733
+ left: 0,
734
+ right: 0,
735
+ bottom: referencePx,
736
+ borderTopWidth: 1,
737
+ borderStyle: "dashed",
738
+ borderColor: HERO_REFERENCE_COLOR,
739
+ pointerEvents: "none"
740
+ },
741
+ testID: "velocity-hero-reference"
742
+ }
743
+ ),
744
+ doneVelocities.map((velocity, i) => {
745
+ const isLive = liveRepIndex === i;
746
+ return /* @__PURE__ */ jsxs(
747
+ reactNative.View,
748
+ {
749
+ accessibilityElementsHidden: true,
750
+ style: {
751
+ flex: 1,
752
+ maxWidth: HERO_BAR_MAX_WIDTH,
753
+ height: "100%",
754
+ alignItems: "center",
755
+ justifyContent: "flex-end"
756
+ },
757
+ children: [
758
+ /* @__PURE__ */ jsx(
759
+ reactNative.Text,
760
+ {
761
+ className: "text-text-primary",
762
+ style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
763
+ testID: `velocity-label-${i}`,
764
+ children: formatVelocity(velocity)
765
+ }
766
+ ),
767
+ /* @__PURE__ */ jsx(
768
+ reactNative.Animated.View,
769
+ {
770
+ style: [
771
+ {
772
+ width: "100%",
773
+ height: barHeight(velocity),
774
+ borderTopLeftRadius: HERO_BAR_RADIUS,
775
+ borderTopRightRadius: HERO_BAR_RADIUS,
776
+ backgroundColor: barColorFor(velocity)
777
+ },
778
+ isLive ? { transform: [{ scale: liveScale }] } : null
779
+ ],
780
+ testID: `velocity-bar-${i}`
781
+ }
782
+ )
783
+ ]
784
+ },
785
+ i
786
+ );
787
+ }),
788
+ Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
789
+ reactNative.View,
790
+ {
791
+ accessibilityElementsHidden: true,
792
+ style: {
793
+ flex: 1,
794
+ maxWidth: HERO_BAR_MAX_WIDTH,
795
+ height: "100%",
796
+ alignItems: "center",
797
+ justifyContent: "flex-end"
798
+ },
799
+ children: /* @__PURE__ */ jsx(
800
+ reactNative.View,
801
+ {
802
+ style: {
803
+ width: "100%",
804
+ height: HERO_MIN_BAR_HEIGHT * 3,
805
+ borderWidth: 1,
806
+ borderStyle: "dashed",
807
+ borderColor: HERO_PENDING_COLOR,
808
+ borderTopLeftRadius: HERO_BAR_RADIUS,
809
+ borderTopRightRadius: HERO_BAR_RADIUS
810
+ },
811
+ testID: "velocity-slot-todo"
812
+ }
813
+ )
814
+ },
815
+ `pending-${i}`
816
+ ))
817
+ ]
818
+ }
819
+ )
820
+ }
821
+ );
822
+ }
823
+ function VelocityStrip({
824
+ velocities,
825
+ set,
826
+ zones,
827
+ liveRepIndex,
828
+ expanded = true,
829
+ onToggle,
830
+ onRepPress,
831
+ variant = "expanded",
832
+ height = EXPANDED_HEIGHT,
833
+ targetReps,
834
+ scale = "peak",
835
+ showNumbers = true,
836
+ showInfo = true,
837
+ className,
838
+ ...props
839
+ }) {
840
+ const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
841
+ const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
842
+ kind: "rep",
843
+ velocity: v,
844
+ leadingGap: i === 0 ? 0 : REP_GAP
845
+ }));
846
+ const maxVelocity = Math.max(...doneVelocities, 0);
847
+ const meanVelocity = calculateMeanVelocity(doneVelocities);
848
+ const loss = calculateVelocityLoss(doneVelocities);
849
+ const framed = showNumbers || showInfo;
850
+ const scaleDenom = scaleDenominator(scale, maxVelocity);
851
+ const hasZones = zones != null && zones.length > 0;
852
+ const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
853
+ const slotColor = (slot) => {
854
+ if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
855
+ if (slot.kind === "todo") return TODO_COLOR2;
856
+ return SET_STRIP_VARIABLE_COLOR;
857
+ };
858
+ const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
859
+ const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
860
+ const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
861
+ const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
862
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
863
+ const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
864
+ const liveScale = useLiveRepPop(
865
+ variant === "expanded" && framed,
866
+ liveRepIndex,
867
+ liveVelocity,
868
+ isNewPeak
869
+ );
327
870
  react.useEffect(() => {
328
- if (variant === "mini") return;
871
+ if (variant !== "expanded" || !framed) return;
329
872
  reactNative.Animated.parallel([
330
873
  reactNative.Animated.timing(heightAnim, {
331
- toValue: expanded ? 60 : 3,
874
+ toValue: expanded ? height : 3,
332
875
  duration: ANIMATION_DURATION,
333
876
  easing: ANIMATION_EASING,
334
877
  useNativeDriver: false
@@ -346,39 +889,136 @@ function VelocityStrip({
346
889
  useNativeDriver: false
347
890
  })
348
891
  ]).start();
349
- }, [expanded, variant, heightAnim, labelOpacity, infoOpacity]);
892
+ }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
893
+ if (set == null && velocities == null) return null;
894
+ const repCount = doneVelocities.length;
895
+ const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
896
+ if (variant === "hero") {
897
+ const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
898
+ return /* @__PURE__ */ jsx(
899
+ HeroVelocityChart,
900
+ {
901
+ doneVelocities,
902
+ barColorFor,
903
+ scaleDenom,
904
+ referenceVelocity: maxVelocity,
905
+ liveRepIndex,
906
+ isNewPeak,
907
+ targetReps,
908
+ height: heroHeight,
909
+ className,
910
+ viewProps: props
911
+ }
912
+ );
913
+ }
350
914
  if (variant === "mini") {
351
915
  const { style: externalStyle, ...restProps } = props;
352
916
  return /* @__PURE__ */ jsx(
353
917
  reactNative.View,
354
918
  {
355
919
  className,
356
- style: [{ flexDirection: "row", height: 3, gap: 2, borderRadius: 2, overflow: "hidden" }, externalStyle],
920
+ style: [
921
+ { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
922
+ externalStyle
923
+ ],
357
924
  accessibilityRole: "image",
358
- accessibilityLabel: `Velocity strip, ${velocities.length} reps`,
925
+ accessibilityLabel: miniLabel,
359
926
  testID: "velocity-strip-mini",
360
927
  ...restProps,
361
- children: velocities.map((v, i) => /* @__PURE__ */ jsx(
928
+ children: slots.map((slot, i) => /* @__PURE__ */ jsx(
362
929
  reactNative.View,
363
930
  {
364
931
  style: {
365
932
  flex: 1,
366
- backgroundColor: barColorFor(v),
933
+ backgroundColor: slotColor(slot),
367
934
  borderRadius: 1,
368
935
  minWidth: 4,
369
- height: "100%"
936
+ height: "100%",
937
+ // The container's uniform 2px gap covers rep spacing; a wide slot adds
938
+ // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
939
+ marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
940
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
370
941
  },
371
942
  accessibilityElementsHidden: true,
372
- testID: `velocity-bar-${i}`
943
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
373
944
  },
374
945
  i
375
946
  ))
376
947
  }
377
948
  );
378
949
  }
379
- const stripLabel = `Velocity chart for set, ${velocities.length} reps, tap to ${expanded ? "collapse" : "expand"}`;
950
+ if (!framed) {
951
+ const { style: externalStyle, ...restProps } = props;
952
+ return /* @__PURE__ */ jsx(
953
+ reactNative.View,
954
+ {
955
+ className,
956
+ style: [
957
+ { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
958
+ externalStyle
959
+ ],
960
+ accessibilityRole: "image",
961
+ accessibilityLabel: miniLabel,
962
+ testID: "velocity-strip-compact",
963
+ ...restProps,
964
+ children: slots.map((slot, i) => /* @__PURE__ */ jsx(
965
+ reactNative.View,
966
+ {
967
+ style: {
968
+ flex: 1,
969
+ minWidth: 4,
970
+ height: bareSlotHeight(slot, height, scaleDenom),
971
+ // Top-rounded to match the framed chart's bars (the "rounded tops from
972
+ // the numbers one"); square bottoms since bars sit on the baseline.
973
+ borderTopLeftRadius: 2,
974
+ borderTopRightRadius: 2,
975
+ backgroundColor: slotColor(slot),
976
+ marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
977
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
978
+ },
979
+ accessibilityElementsHidden: true,
980
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
981
+ },
982
+ i
983
+ ))
984
+ }
985
+ );
986
+ }
987
+ const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
380
988
  const hasInteractiveContainer = onToggle != null;
381
989
  const hasInteractiveReps = onRepPress != null && expanded;
990
+ const setBars = set != null && slots.map((slot, i) => {
991
+ const isStraightRep = set.type === "straight" && slot.kind === "rep";
992
+ const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
993
+ return /* @__PURE__ */ jsx(
994
+ reactNative.View,
995
+ {
996
+ style: {
997
+ flex: 1,
998
+ height: "100%",
999
+ justifyContent: "flex-end",
1000
+ marginLeft: slot.leadingGap
1001
+ },
1002
+ children: /* @__PURE__ */ jsx(
1003
+ reactNative.View,
1004
+ {
1005
+ style: {
1006
+ height: `${heightPct}%`,
1007
+ minHeight: 2,
1008
+ borderTopLeftRadius: 2,
1009
+ borderTopRightRadius: 2,
1010
+ backgroundColor: slotColor(slot),
1011
+ ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1012
+ },
1013
+ testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
1014
+ accessibilityRole: "image",
1015
+ accessibilityLabel: slotAccessibilityLabel(slot, i)
1016
+ }
1017
+ )
1018
+ },
1019
+ i
1020
+ );
1021
+ });
382
1022
  const stripContent = /* @__PURE__ */ jsxs(
383
1023
  reactNative.Animated.View,
384
1024
  {
@@ -391,7 +1031,9 @@ function VelocityStrip({
391
1031
  borderRadius: expanded ? 6 : 2,
392
1032
  paddingTop: expanded ? 16 : 0,
393
1033
  paddingBottom: expanded ? 24 : 0,
394
- paddingHorizontal: expanded ? 6 : 0,
1034
+ // No horizontal inset the framed chart's bars span full width, matching
1035
+ // the bare spotlight and mini. Numbers/info sit in the vertical padding.
1036
+ paddingHorizontal: 0,
395
1037
  paddingVertical: expanded ? void 0 : 8,
396
1038
  overflow: expanded ? "visible" : "hidden"
397
1039
  }
@@ -401,46 +1043,65 @@ function VelocityStrip({
401
1043
  testID: "velocity-strip",
402
1044
  ...props,
403
1045
  children: [
404
- /* @__PURE__ */ jsx(
405
- reactNative.View,
406
- {
407
- style: { flexDirection: "row", flex: 1, gap: 2, alignItems: "flex-end" },
408
- children: velocities.map((v, i) => {
409
- const barBackground = barColorFor(v);
410
- const barHeightPct = maxVelocity > 0 ? Math.round(v / (maxVelocity * 1.15) * 100) : 0;
411
- const isLive = liveRepIndex === i;
412
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
413
- const barStyle = expanded ? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: barBackground } : {
414
- minHeight: "100%",
415
- borderTopLeftRadius: 2,
416
- borderTopRightRadius: 2,
417
- borderBottomLeftRadius: 0,
418
- borderBottomRightRadius: 0,
419
- backgroundColor: barBackground
420
- };
421
- const barInner = isLive ? /* @__PURE__ */ jsx(
422
- reactNative.Animated.View,
423
- {
424
- style: [barStyle, { transform: [{ scale: liveScale }] }],
425
- testID: `velocity-bar-${i}`,
426
- accessibilityRole: "image",
427
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
428
- }
429
- ) : /* @__PURE__ */ jsx(
430
- reactNative.View,
431
- {
432
- style: barStyle,
433
- testID: `velocity-bar-${i}`,
434
- accessibilityRole: "image",
435
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
436
- }
437
- );
438
- const bar = /* @__PURE__ */ jsxs(
439
- reactNative.View,
440
- {
441
- style: { flex: 1, height: "100%", justifyContent: "flex-end", position: "relative" },
442
- children: [
443
- expanded && /* @__PURE__ */ jsx(reactNative.Animated.View, { style: { opacity: labelOpacity, alignItems: "center", position: "absolute", top: -13, left: 0, right: 0 }, accessibilityElementsHidden: true, children: /* @__PURE__ */ jsx(
1046
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
1047
+ const barBackground = barColorFor(v);
1048
+ const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
1049
+ const isLive = liveRepIndex === i;
1050
+ const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
1051
+ const barStyle = expanded ? {
1052
+ height: `${barHeightPct}%`,
1053
+ minHeight: 2,
1054
+ borderTopLeftRadius: 2,
1055
+ borderTopRightRadius: 2,
1056
+ backgroundColor: barBackground
1057
+ } : {
1058
+ minHeight: "100%",
1059
+ borderTopLeftRadius: 2,
1060
+ borderTopRightRadius: 2,
1061
+ borderBottomLeftRadius: 0,
1062
+ borderBottomRightRadius: 0,
1063
+ backgroundColor: barBackground
1064
+ };
1065
+ const barInner = isLive ? /* @__PURE__ */ jsx(
1066
+ reactNative.Animated.View,
1067
+ {
1068
+ style: [barStyle, { transform: [{ scale: liveScale }] }],
1069
+ testID: `velocity-bar-${i}`,
1070
+ accessibilityRole: "image",
1071
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
1072
+ }
1073
+ ) : /* @__PURE__ */ jsx(
1074
+ reactNative.View,
1075
+ {
1076
+ style: barStyle,
1077
+ testID: `velocity-bar-${i}`,
1078
+ accessibilityRole: "image",
1079
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
1080
+ }
1081
+ );
1082
+ const bar = /* @__PURE__ */ jsxs(
1083
+ reactNative.View,
1084
+ {
1085
+ style: {
1086
+ flex: 1,
1087
+ height: "100%",
1088
+ justifyContent: "flex-end",
1089
+ position: "relative"
1090
+ },
1091
+ children: [
1092
+ expanded && showNumbers && /* @__PURE__ */ jsx(
1093
+ reactNative.Animated.View,
1094
+ {
1095
+ style: {
1096
+ opacity: labelOpacity,
1097
+ alignItems: "center",
1098
+ position: "absolute",
1099
+ top: -13,
1100
+ left: 0,
1101
+ right: 0
1102
+ },
1103
+ accessibilityElementsHidden: true,
1104
+ children: /* @__PURE__ */ jsx(
444
1105
  reactNative.Text,
445
1106
  {
446
1107
  className: "text-text-secondary",
@@ -448,30 +1109,30 @@ function VelocityStrip({
448
1109
  testID: `velocity-label-${i}`,
449
1110
  children: formatVelocity(v)
450
1111
  }
451
- ) }),
452
- barInner
453
- ]
454
- },
455
- i
456
- );
457
- if (onRepPress && expanded) {
458
- return /* @__PURE__ */ jsx(
459
- reactNative.Pressable,
460
- {
461
- style: { flex: 1, height: "100%" },
462
- onPress: () => onRepPress(i, v),
463
- accessibilityRole: "button",
464
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
465
- testID: `velocity-bar-pressable-${i}`,
466
- children: bar
467
- },
468
- i
469
- );
470
- }
471
- return bar;
472
- })
1112
+ )
1113
+ }
1114
+ ),
1115
+ barInner
1116
+ ]
1117
+ },
1118
+ i
1119
+ );
1120
+ if (onRepPress && expanded) {
1121
+ return /* @__PURE__ */ jsx(
1122
+ reactNative.Pressable,
1123
+ {
1124
+ style: { flex: 1, height: "100%" },
1125
+ onPress: () => onRepPress(i, v),
1126
+ accessibilityRole: "button",
1127
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
1128
+ testID: `velocity-bar-pressable-${i}`,
1129
+ children: bar
1130
+ },
1131
+ i
1132
+ );
473
1133
  }
474
- ),
1134
+ return bar;
1135
+ }) }),
475
1136
  expanded && showInfo && /* @__PURE__ */ jsxs(
476
1137
  reactNative.Animated.View,
477
1138
  {
@@ -647,17 +1308,49 @@ function SvgIcon({
647
1308
  }
648
1309
 
649
1310
  // src/components/icons/icons.tsx
650
- function DumbbellIcon(props) {
1311
+ function StarIcon(props) {
1312
+ 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" }) });
1313
+ }
1314
+ function ScaleIcon(props) {
651
1315
  return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
652
- /* @__PURE__ */ jsx("path", { d: "M14.4 14.4 9.6 9.6" }),
653
- /* @__PURE__ */ jsx("path", { d: "M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z" }),
654
- /* @__PURE__ */ jsx("path", { d: "m21.5 21.5-1.4-1.4" }),
655
- /* @__PURE__ */ jsx("path", { d: "M3.9 3.9 2.5 2.5" }),
656
- /* @__PURE__ */ jsx("path", { d: "M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z" })
1316
+ /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1317
+ /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1318
+ /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
1319
+ /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
1320
+ /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
657
1321
  ] });
658
1322
  }
659
- function StarIcon(props) {
660
- 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" }) });
1323
+ function AlertTriangleIcon(props) {
1324
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1325
+ /* @__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" }),
1326
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1327
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1328
+ ] });
1329
+ }
1330
+ function TrendingDownIcon(props) {
1331
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1332
+ /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
1333
+ /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
1334
+ ] });
1335
+ }
1336
+ function CircleSlashIcon(props) {
1337
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1338
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1339
+ /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
1340
+ ] });
1341
+ }
1342
+ function AwardIcon(props) {
1343
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1344
+ /* @__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" }),
1345
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
1346
+ ] });
1347
+ }
1348
+ function InfoIcon(props) {
1349
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1350
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1351
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
1352
+ /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1353
+ ] });
661
1354
  }
662
1355
  var baseBadgeSizeConfig = {
663
1356
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
@@ -760,6 +1453,9 @@ var semanticColorsLight = {
760
1453
  "status-success-light": primitiveRamps.green[200],
761
1454
  "status-success-dark": primitiveRamps.green[600],
762
1455
  "status-success-subtle": primitiveRamps.green[50],
1456
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
1457
+ "status-live": primitiveRamps.green[300],
1458
+ "status-live-muted": primitiveRamps.green[500],
763
1459
  "status-error": primitiveRamps.red[600],
764
1460
  "status-error-light": primitiveRamps.red[500],
765
1461
  "status-error-dark": primitiveRamps.red[700],
@@ -893,6 +1589,9 @@ var semanticColorsDark = {
893
1589
  "status-success-light": primitiveRamps.green[200],
894
1590
  "status-success-dark": primitiveRamps.green[600],
895
1591
  "status-success-subtle": "rgba(46, 213, 115, 0.12)",
1592
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
1593
+ "status-live": primitiveRamps.green[300],
1594
+ "status-live-muted": primitiveRamps.green[500],
896
1595
  "status-error": primitiveRamps.red[600],
897
1596
  "status-error-light": primitiveRamps.red[500],
898
1597
  "status-error-dark": primitiveRamps.red[700],
@@ -997,93 +1696,7 @@ function getSemanticColors(mode) {
997
1696
  return mode === "dark" ? semanticColorsDark : semanticColorsLight;
998
1697
  }
999
1698
 
1000
- // src/components/custom/Workout/WeightBadge.tsx
1001
- function WeightBadge({
1002
- value,
1003
- unit = "lbs",
1004
- reps,
1005
- delta,
1006
- isPr,
1007
- showIcon = true,
1008
- size = "md",
1009
- onPress,
1010
- className,
1011
- ...props
1012
- }) {
1013
- const config = baseBadgeSizeConfig[size];
1014
- const textColor = isPr ? getSemanticColors("dark")["brand-primary"] : "#9CA3AF";
1015
- const iconColor = textColor;
1016
- const repMaxClause = reps != null ? `${reps} rep max` : "Estimated one rep max";
1017
- const deltaLabel = delta != null ? `, ${delta >= 0 ? "+" : ""}${delta}% change` : "";
1018
- const fullLabel = `${repMaxClause}: ${value} ${unit}${deltaLabel}`;
1019
- return /* @__PURE__ */ jsxs(
1020
- BaseBadge,
1021
- {
1022
- variant: isPr ? "pr" : "plain",
1023
- size,
1024
- onPress,
1025
- className,
1026
- icon: showIcon ? /* @__PURE__ */ jsx(DumbbellIcon, { size: config.iconSize, color: iconColor, strokeWidth: 2 }) : void 0,
1027
- accessibilityLabel: fullLabel,
1028
- testID: "weight-badge",
1029
- ...props,
1030
- children: [
1031
- /* @__PURE__ */ jsxs(
1032
- reactNative.Text,
1033
- {
1034
- style: {
1035
- fontFamily: '"Space Grotesk", sans-serif',
1036
- fontWeight: "600",
1037
- fontSize: config.fontSize,
1038
- color: textColor
1039
- },
1040
- children: [
1041
- isPr ? "\u2733 " : "",
1042
- value,
1043
- " ",
1044
- unit
1045
- ]
1046
- }
1047
- ),
1048
- reps != null && /* @__PURE__ */ jsxs(
1049
- reactNative.Text,
1050
- {
1051
- style: {
1052
- fontFamily: '"Space Grotesk", sans-serif',
1053
- fontWeight: "600",
1054
- fontSize: config.fontSize,
1055
- marginLeft: 2,
1056
- color: textColor,
1057
- opacity: 0.7
1058
- },
1059
- testID: "weight-badge-repmax",
1060
- children: [
1061
- reps,
1062
- "RM"
1063
- ]
1064
- }
1065
- ),
1066
- delta != null && /* @__PURE__ */ jsxs(
1067
- reactNative.Text,
1068
- {
1069
- style: {
1070
- fontFamily: '"Space Grotesk", sans-serif',
1071
- fontSize: config.fontSize,
1072
- marginLeft: 4,
1073
- color: delta >= 0 ? getSemanticColors("dark")["result-improve"] : getSemanticColors("dark")["result-degrade"]
1074
- },
1075
- testID: "weight-badge-delta",
1076
- children: [
1077
- delta >= 0 ? "+" : "",
1078
- delta,
1079
- "%"
1080
- ]
1081
- }
1082
- )
1083
- ]
1084
- }
1085
- );
1086
- }
1699
+ // src/components/custom/Workout/PrBadge.tsx
1087
1700
  var BRAND_PRIMARY = getSemanticColors("dark")["brand-primary"];
1088
1701
  var typeLabels = {
1089
1702
  e1rm: "PR e1RM",
@@ -1167,6 +1780,204 @@ function PrBadge({
1167
1780
  }
1168
1781
  return badge;
1169
1782
  }
1783
+ var DARK = getSemanticColors("dark");
1784
+ var TEXT_ACTIVE = DARK["text-primary"];
1785
+ var TEXT_MUTED = DARK["text-secondary"];
1786
+ var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
1787
+ var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
1788
+ var typeBadge = {
1789
+ fontSize: 10,
1790
+ fontWeight: "700",
1791
+ fontFamily: "Inter, sans-serif",
1792
+ color: WORKOUT_TOKENS.scale.orange,
1793
+ backgroundColor: "rgba(255,165,2,0.12)",
1794
+ paddingVertical: 1,
1795
+ paddingHorizontal: 5,
1796
+ borderRadius: 3,
1797
+ overflow: "hidden"
1798
+ };
1799
+ function displayReps(set) {
1800
+ return set.state === "done" ? set.reps : set.target.reps;
1801
+ }
1802
+ function displayWeight(set) {
1803
+ return set.state === "done" ? set.weight : set.target.weight;
1804
+ }
1805
+ function plannedReps(set) {
1806
+ if (set.state === "done") return set.velocities.length;
1807
+ if (set.state === "live") return set.target.reps;
1808
+ return set.planned ?? set.target.reps;
1809
+ }
1810
+ function accessibilityLabel(set) {
1811
+ const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
1812
+ if (set.state === "live") return `${base}, in progress`;
1813
+ if (set.state === "todo") return `${base}, upcoming`;
1814
+ return base;
1815
+ }
1816
+ function Cell({
1817
+ width,
1818
+ children,
1819
+ testID
1820
+ }) {
1821
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
1822
+ }
1823
+ function RowStrip({ set }) {
1824
+ const zones = set.velocityZones;
1825
+ if (set.state === "live") {
1826
+ return /* @__PURE__ */ jsx(
1827
+ VelocityStrip,
1828
+ {
1829
+ variant: "expanded",
1830
+ showNumbers: false,
1831
+ showInfo: false,
1832
+ height: 24,
1833
+ scale: "fixed",
1834
+ set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
1835
+ zones
1836
+ }
1837
+ );
1838
+ }
1839
+ const velocities = set.state === "done" ? set.velocities : [];
1840
+ return /* @__PURE__ */ jsx(
1841
+ VelocityStrip,
1842
+ {
1843
+ variant: "mini",
1844
+ set: { type: "straight", velocities, planned: plannedReps(set) },
1845
+ zones
1846
+ }
1847
+ );
1848
+ }
1849
+ function SetRow(set) {
1850
+ const live = set.state === "live";
1851
+ const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
1852
+ const rpe = set.state === "todo" ? null : set.rpe;
1853
+ return /* @__PURE__ */ jsxs(
1854
+ reactNative.View,
1855
+ {
1856
+ style: { paddingVertical: 6, paddingHorizontal: 8 },
1857
+ accessibilityLabel: accessibilityLabel(set),
1858
+ testID: "set-row",
1859
+ children: [
1860
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
1861
+ /* @__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 }) }),
1862
+ /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
1863
+ /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
1864
+ /* @__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" }) })
1865
+ ] }),
1866
+ /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
1867
+ ]
1868
+ }
1869
+ );
1870
+ }
1871
+ var PREV_COLUMN = { label: "PREV", width: void 0 };
1872
+ function buildColumns(unit, showPrevious) {
1873
+ return [
1874
+ { label: "SET", width: 36 },
1875
+ ...showPrevious ? [PREV_COLUMN] : [],
1876
+ { label: "REPS", width: 44 },
1877
+ { label: unit.toUpperCase(), width: 56 },
1878
+ { label: "RPE", width: 36 }
1879
+ ];
1880
+ }
1881
+ function SetTableHeader({
1882
+ unit = "lbs",
1883
+ showPrevious = true,
1884
+ testID = "table-header"
1885
+ }) {
1886
+ const columns = buildColumns(unit, showPrevious);
1887
+ return /* @__PURE__ */ jsx(
1888
+ reactNative.View,
1889
+ {
1890
+ className: "flex-row",
1891
+ style: {
1892
+ paddingVertical: 8,
1893
+ paddingHorizontal: 8,
1894
+ paddingBottom: 4,
1895
+ ...showPrevious ? {} : { justifyContent: "space-between" }
1896
+ },
1897
+ testID,
1898
+ children: columns.map(({ label, width }) => {
1899
+ const isFlex = width === void 0;
1900
+ return /* @__PURE__ */ jsx(
1901
+ reactNative.View,
1902
+ {
1903
+ style: {
1904
+ ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
1905
+ alignItems: "center",
1906
+ justifyContent: "center",
1907
+ gap: 8
1908
+ },
1909
+ ...isFlex ? { className: "flex-1" } : {},
1910
+ children: /* @__PURE__ */ jsx(
1911
+ reactNative.Text,
1912
+ {
1913
+ className: "text-text-tertiary",
1914
+ style: {
1915
+ fontSize: 10,
1916
+ fontWeight: "600",
1917
+ fontFamily: "Inter, sans-serif",
1918
+ textTransform: "uppercase",
1919
+ letterSpacing: 0.8
1920
+ },
1921
+ children: label
1922
+ }
1923
+ )
1924
+ },
1925
+ label
1926
+ );
1927
+ })
1928
+ }
1929
+ );
1930
+ }
1931
+ var METRIC_FONT = "Inter, sans-serif";
1932
+ function MetricCell({ children, color, fontSize = 11 }) {
1933
+ return /* @__PURE__ */ jsx(
1934
+ reactNative.Text,
1935
+ {
1936
+ style: {
1937
+ fontFamily: METRIC_FONT,
1938
+ fontSize,
1939
+ color,
1940
+ fontWeight: "600",
1941
+ letterSpacing: 1
1942
+ },
1943
+ children
1944
+ }
1945
+ );
1946
+ }
1947
+
1948
+ // src/components/custom/Workout/SetsRepsLoad.tsx
1949
+ var t = getSemanticColors("dark");
1950
+ var TIMES = "\xD7";
1951
+ var AT = "@";
1952
+ function SetsRepsLoad({
1953
+ sets,
1954
+ reps,
1955
+ load,
1956
+ unit = "lb",
1957
+ className,
1958
+ ...props
1959
+ }) {
1960
+ const value = t["text-primary"];
1961
+ const sep = t["text-tertiary"];
1962
+ return /* @__PURE__ */ jsxs(
1963
+ reactNative.View,
1964
+ {
1965
+ className,
1966
+ style: { flexDirection: "row", alignItems: "baseline" },
1967
+ accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
1968
+ testID: "sets-reps-load",
1969
+ ...props,
1970
+ children: [
1971
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: sets }),
1972
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${TIMES} ` }),
1973
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: reps }),
1974
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${AT} ` }),
1975
+ /* @__PURE__ */ jsx(MetricCell, { color: value, children: load }),
1976
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, children: ` ${unit}` })
1977
+ ]
1978
+ }
1979
+ );
1980
+ }
1170
1981
  function hexToRgb(hex) {
1171
1982
  const cleaned = hex.replace("#", "");
1172
1983
  let r, g, b;
@@ -1476,7 +2287,7 @@ function alpha(color, opacity) {
1476
2287
  }
1477
2288
  return color;
1478
2289
  }
1479
- var t = getSemanticColors("dark");
2290
+ var t2 = getSemanticColors("dark");
1480
2291
  var TEMPO_PACING = {
1481
2292
  behindThresholdPct: 0.15,
1482
2293
  minPhaseDurationMs: 500
@@ -1492,20 +2303,20 @@ function getTempoFillPct(elapsedMs, targetMs) {
1492
2303
  return Math.min(100, elapsedMs / targetMs * 100);
1493
2304
  }
1494
2305
  ({
1495
- concentric: { color: t["status-success"]},
1496
- hold: { color: t["brand-primary"]},
1497
- eccentric: { color: t["status-warning"]}
2306
+ concentric: { color: t2["status-success"]},
2307
+ hold: { color: t2["brand-primary"]},
2308
+ eccentric: { color: t2["status-warning"]}
1498
2309
  });
1499
2310
 
1500
2311
  // src/components/custom/Workout/TempoDisplay.tsx
1501
- var t2 = getSemanticColors("dark");
2312
+ var t3 = getSemanticColors("dark");
1502
2313
  var INTER = "Inter, sans-serif";
1503
2314
  var TEXT_TERTIARY = "#6B7280";
1504
- var STATUS_ERROR = t2["status-error"];
2315
+ var STATUS_ERROR = t3["status-error"];
1505
2316
  var phaseColors = {
1506
- eccentric: t2["status-warning"],
2317
+ eccentric: t3["status-warning"],
1507
2318
  pauseBottom: TEXT_TERTIARY,
1508
- concentric: t2["status-success"],
2319
+ concentric: t3["status-success"],
1509
2320
  pauseTop: TEXT_TERTIARY,
1510
2321
  dash: TEXT_TERTIARY
1511
2322
  };
@@ -1514,34 +2325,10 @@ function TempoValue({
1514
2325
  color,
1515
2326
  fontSize
1516
2327
  }) {
1517
- return /* @__PURE__ */ jsx(
1518
- reactNative.Text,
1519
- {
1520
- style: {
1521
- fontFamily: INTER,
1522
- fontSize,
1523
- color,
1524
- fontWeight: "600",
1525
- letterSpacing: 1
1526
- },
1527
- children: value
1528
- }
1529
- );
2328
+ return /* @__PURE__ */ jsx(MetricCell, { color, fontSize, children: value });
1530
2329
  }
1531
2330
  function TempoSeparator({ color, fontSize }) {
1532
- return /* @__PURE__ */ jsx(
1533
- reactNative.Text,
1534
- {
1535
- style: {
1536
- fontFamily: INTER,
1537
- fontSize,
1538
- color,
1539
- fontWeight: "600",
1540
- letterSpacing: 1
1541
- },
1542
- children: "-"
1543
- }
1544
- );
2331
+ return /* @__PURE__ */ jsx(MetricCell, { color, fontSize, children: "-" });
1545
2332
  }
1546
2333
  var LIVE_PHASES = [
1547
2334
  { key: "eccentric", color: phaseColors.eccentric },
@@ -1603,6 +2390,7 @@ function TempoDisplay({
1603
2390
  tempo,
1604
2391
  size = "md",
1605
2392
  colored = true,
2393
+ showLabel = true,
1606
2394
  showInfo = true,
1607
2395
  live,
1608
2396
  onPress,
@@ -1636,7 +2424,7 @@ function TempoDisplay({
1636
2424
  },
1637
2425
  ...props,
1638
2426
  children: [
1639
- /* @__PURE__ */ jsx(
2427
+ showLabel && /* @__PURE__ */ jsx(
1640
2428
  reactNative.Text,
1641
2429
  {
1642
2430
  style: {
@@ -1770,245 +2558,121 @@ function TempoDisplay({
1770
2558
  }
1771
2559
  );
1772
2560
  }
1773
- function formatAccessibilityLabel(setNumber, reps, weight, unit) {
1774
- const repsText = reps != null ? `${reps} reps` : "no reps";
1775
- const weightText = weight != null ? `at ${roundWeight(weight)} ${unit}` : "";
1776
- return `Set ${setNumber}: ${repsText}${weightText ? ` ${weightText}` : ""}`;
2561
+ var t4 = getSemanticColors("dark");
2562
+ var INDICATOR_CONFIG = {
2563
+ imbalance: { Icon: ScaleIcon, color: t4["status-error"], label: "Left/right imbalance" },
2564
+ overshoot: { Icon: AlertTriangleIcon, color: t4["status-error"], label: "Load overshoot" },
2565
+ "velocity-loss": { Icon: TrendingDownIcon, color: t4["status-warning"], label: "Velocity loss" },
2566
+ "missed-reps": { Icon: CircleSlashIcon, color: t4["status-warning"], label: "Missed reps" },
2567
+ pr: { Icon: AwardIcon, color: t4["status-success"], label: "Personal record" },
2568
+ info: { Icon: InfoIcon, color: t4["status-info"], label: "More info" }
2569
+ };
2570
+ function ExerciseIndicator({ kind, onPress, className, ...props }) {
2571
+ const { Icon, color, label } = INDICATOR_CONFIG[kind];
2572
+ const chip = /* @__PURE__ */ jsx(
2573
+ reactNative.View,
2574
+ {
2575
+ className,
2576
+ style: {
2577
+ width: 16,
2578
+ height: 16,
2579
+ borderRadius: 8,
2580
+ borderWidth: 1,
2581
+ borderColor: color,
2582
+ alignItems: "center",
2583
+ justifyContent: "center"
2584
+ },
2585
+ accessibilityRole: "image",
2586
+ accessibilityLabel: label,
2587
+ testID: "exercise-indicator",
2588
+ ...props,
2589
+ children: /* @__PURE__ */ jsx(Icon, { size: 11, color })
2590
+ }
2591
+ );
2592
+ if (onPress) {
2593
+ return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, children: chip });
2594
+ }
2595
+ return chip;
1777
2596
  }
1778
- function SetRow({
1779
- mode,
1780
- setNumber,
1781
- previous,
2597
+
2598
+ // src/components/custom/Workout/ExerciseHeading.tsx
2599
+ function headingLabel(name, sets, reps, load, unit) {
2600
+ return `${name}, ${sets}\xD7${reps} @ ${roundWeight(load)} ${unit}`;
2601
+ }
2602
+ function ExerciseHeading({
2603
+ name,
2604
+ sets,
1782
2605
  reps,
1783
- weight,
1784
- rpe,
1785
- unit,
1786
- velocities,
1787
- velocityZones,
1788
- velocityLiveRepIndex,
1789
- velocityExpanded,
1790
- onVelocityToggle,
1791
- setType,
1792
- prBadges,
1793
- isNextSet,
1794
- targets
2606
+ load,
2607
+ unit = "lbs",
2608
+ tempo,
2609
+ indicator,
2610
+ dimmed,
2611
+ onPress
2612
+ }) {
2613
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { opacity: dimmed ? 0.55 : 1 }, testID: "exercise-heading", children: [
2614
+ /* @__PURE__ */ jsxs(
2615
+ reactNative.Pressable,
2616
+ {
2617
+ onPress,
2618
+ accessibilityRole: "button",
2619
+ accessibilityLabel: headingLabel(name, sets, reps, load, unit),
2620
+ className: "flex-row items-center",
2621
+ style: { gap: 8 },
2622
+ testID: "exercise-card-header",
2623
+ children: [
2624
+ /* @__PURE__ */ jsx(
2625
+ reactNative.Text,
2626
+ {
2627
+ className: "text-text-primary",
2628
+ style: { fontSize: 14, fontFamily: '"Space Grotesk", sans-serif', fontWeight: "700" },
2629
+ testID: "exercise-card-name",
2630
+ children: name
2631
+ }
2632
+ ),
2633
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2634
+ indicator && /* @__PURE__ */ jsx(ExerciseIndicator, { kind: indicator })
2635
+ ]
2636
+ }
2637
+ ),
2638
+ /* @__PURE__ */ jsxs(
2639
+ reactNative.View,
2640
+ {
2641
+ className: "flex-row items-center",
2642
+ style: { gap: 8, marginTop: 1 },
2643
+ testID: "exercise-card-summary",
2644
+ children: [
2645
+ /* @__PURE__ */ jsx(SetsRepsLoad, { sets, reps, load, unit }),
2646
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2647
+ tempo && /* @__PURE__ */ jsx(TempoDisplay, { tempo, size: "sm", showLabel: false, showInfo: false })
2648
+ ]
2649
+ }
2650
+ )
2651
+ ] });
2652
+ }
2653
+
2654
+ // src/components/custom/Workout/ExerciseCardHeading.tsx
2655
+ function ExerciseCardHeading({
2656
+ setStates,
2657
+ stripHeight = 8,
2658
+ dimmed,
2659
+ testID = "exercise-card",
2660
+ ...heading
1795
2661
  }) {
1796
- const isActive = mode === "active";
1797
- const isCompleted = mode === "completed";
1798
- const rowStyle = {
1799
- flexDirection: "row",
1800
- alignItems: "center",
1801
- padding: 7,
1802
- paddingHorizontal: 8,
1803
- gap: 8,
1804
- borderRadius: 8
1805
- };
1806
- if (isActive && isNextSet) {
1807
- rowStyle.backgroundColor = "rgba(255,121,0,0.06)";
1808
- rowStyle.borderWidth = 1;
1809
- rowStyle.borderColor = "rgba(255,121,0,0.15)";
1810
- }
1811
- if (isCompleted && !isNextSet) {
1812
- rowStyle.opacity = 0.55;
1813
- }
1814
2662
  return /* @__PURE__ */ jsxs(
1815
2663
  reactNative.View,
1816
2664
  {
1817
- style: rowStyle,
1818
- accessibilityLabel: formatAccessibilityLabel(
1819
- setNumber,
1820
- reps,
1821
- weight,
1822
- unit
1823
- ),
1824
- testID: "set-row",
2665
+ style: { paddingVertical: 9, paddingHorizontal: 12, opacity: dimmed ? 0.55 : 1 },
2666
+ testID,
1825
2667
  children: [
1826
- /* @__PURE__ */ jsx(
1827
- reactNative.View,
1828
- {
1829
- style: { width: 36, alignItems: "center", justifyContent: "center" },
1830
- testID: "set-row-set-number",
1831
- children: setType ? /* @__PURE__ */ jsx(
1832
- reactNative.Text,
1833
- {
1834
- style: {
1835
- fontSize: 10,
1836
- fontWeight: "700",
1837
- fontFamily: "Inter, sans-serif",
1838
- color: WORKOUT_TOKENS.scale.orange,
1839
- backgroundColor: "rgba(255,165,2,0.12)",
1840
- paddingVertical: 1,
1841
- paddingHorizontal: 5,
1842
- borderRadius: 3,
1843
- overflow: "hidden"
1844
- },
1845
- testID: "set-row-type-badge",
1846
- children: setType
1847
- }
1848
- ) : /* @__PURE__ */ jsx(
1849
- reactNative.Text,
1850
- {
1851
- className: "text-text-secondary",
1852
- style: {
1853
- fontSize: 13,
1854
- fontWeight: "600",
1855
- fontFamily: "Inter, sans-serif"
1856
- },
1857
- children: setNumber
1858
- }
1859
- )
1860
- }
1861
- ),
1862
- /* @__PURE__ */ jsx(
1863
- reactNative.View,
1864
- {
1865
- className: "flex-1",
1866
- testID: "set-row-previous",
1867
- children: /* @__PURE__ */ jsx(
1868
- reactNative.Text,
1869
- {
1870
- className: "text-text-secondary",
1871
- style: {
1872
- fontSize: 12,
1873
- fontFamily: "Inter, sans-serif"
1874
- },
1875
- children: previous ? `${previous.reps} x ${roundWeight(previous.weight)}` : "\u2014"
1876
- }
1877
- )
1878
- }
1879
- ),
1880
- /* @__PURE__ */ jsx(
1881
- reactNative.View,
1882
- {
1883
- style: { width: 44, alignItems: "center", justifyContent: "center" },
1884
- testID: "set-row-reps",
1885
- children: isActive && reps == null && targets ? /* @__PURE__ */ jsx(
1886
- reactNative.Text,
1887
- {
1888
- className: "text-text-tertiary",
1889
- style: {
1890
- fontSize: 13,
1891
- fontStyle: "italic",
1892
- fontFamily: "Inter, sans-serif"
1893
- },
1894
- testID: "set-row-target-reps",
1895
- children: targets.reps
1896
- }
1897
- ) : /* @__PURE__ */ jsx(
1898
- reactNative.Text,
1899
- {
1900
- className: reps != null ? "text-text-primary" : "text-text-tertiary",
1901
- style: {
1902
- fontSize: 14,
1903
- fontWeight: "600",
1904
- fontFamily: "Inter, sans-serif"
1905
- },
1906
- children: reps != null ? reps : "\u2014"
1907
- }
1908
- )
1909
- }
1910
- ),
1911
- /* @__PURE__ */ jsx(
1912
- reactNative.View,
1913
- {
1914
- style: { width: 56, alignItems: "center", justifyContent: "center" },
1915
- testID: "set-row-weight",
1916
- children: isActive && weight == null && targets ? /* @__PURE__ */ jsx(
1917
- reactNative.Text,
1918
- {
1919
- className: "text-text-tertiary",
1920
- style: {
1921
- fontSize: 13,
1922
- fontStyle: "italic",
1923
- fontFamily: "Inter, sans-serif"
1924
- },
1925
- testID: "set-row-target-weight",
1926
- children: roundWeight(targets.weight)
1927
- }
1928
- ) : /* @__PURE__ */ jsx(
1929
- reactNative.Text,
1930
- {
1931
- className: weight != null ? "text-text-primary" : "text-text-tertiary",
1932
- style: {
1933
- fontSize: 14,
1934
- fontWeight: "600",
1935
- fontFamily: "Inter, sans-serif"
1936
- },
1937
- children: weight != null ? roundWeight(weight) : "\u2014"
1938
- }
1939
- )
1940
- }
1941
- ),
1942
- /* @__PURE__ */ jsx(
1943
- reactNative.View,
1944
- {
1945
- style: { width: 36, alignItems: "center", justifyContent: "center" },
1946
- testID: "set-row-rpe",
1947
- children: /* @__PURE__ */ jsx(
1948
- reactNative.Text,
1949
- {
1950
- className: rpe != null ? void 0 : "text-text-tertiary",
1951
- style: {
1952
- fontSize: 13,
1953
- fontWeight: "600",
1954
- fontFamily: "Inter, sans-serif",
1955
- // rpeColor returns a concrete hex (native-safe); only the tertiary
1956
- // fallback needs the className token.
1957
- color: rpe != null ? rpeColor(rpe) : void 0
1958
- },
1959
- children: rpe != null ? roundRpe(rpe) : "\u2014"
1960
- }
1961
- )
1962
- }
1963
- ),
1964
- prBadges && prBadges.length > 0 && /* @__PURE__ */ jsx(
1965
- reactNative.View,
1966
- {
1967
- className: "flex-row items-center",
1968
- style: { gap: 4 },
1969
- testID: "set-row-pr-badges",
1970
- children: prBadges.map((badge, i) => /* @__PURE__ */ jsx(
1971
- PrBadge,
1972
- {
1973
- type: badge.type,
1974
- label: badge.label,
1975
- animate: false,
1976
- compact: true
1977
- },
1978
- i
1979
- ))
1980
- }
1981
- ),
1982
- velocities && velocities.length > 0 && /* @__PURE__ */ jsx(
1983
- reactNative.View,
1984
- {
1985
- style: { position: "absolute", bottom: 0, left: 8, right: 8 },
1986
- testID: "set-row-velocity-strip",
1987
- children: /* @__PURE__ */ jsx(
1988
- VelocityStrip,
1989
- {
1990
- velocities,
1991
- zones: velocityZones,
1992
- liveRepIndex: velocityLiveRepIndex,
1993
- expanded: velocityExpanded,
1994
- onToggle: onVelocityToggle,
1995
- variant: onVelocityToggle ? "full" : "mini"
1996
- }
1997
- )
1998
- }
1999
- )
2668
+ /* @__PURE__ */ jsx(ExerciseHeading, { ...heading }),
2669
+ setStates.length > 0 && /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: 7 }, testID: "exercise-card-strip", children: /* @__PURE__ */ jsx(SetStrip, { sets: setStates, height: stripHeight }) })
2000
2670
  ]
2001
2671
  }
2002
2672
  );
2003
2673
  }
2004
- function resolveColor(token, mode = "dark") {
2005
- return reactNative.Platform.OS === "web" ? `var(--color-${token})` : getSemanticColors(mode)[token];
2006
- }
2007
2674
 
2008
2675
  // src/components/custom/Workout/ExerciseCard.tsx
2009
- function buildColumnHeaders(unit) {
2010
- return ["SET", "PREV", "REPS", unit.toUpperCase(), "RPE"];
2011
- }
2012
2676
  function getSupersetBorderRadius(position) {
2013
2677
  switch (position) {
2014
2678
  case "first":
@@ -2051,7 +2715,7 @@ function formatSummary(summary) {
2051
2715
  if (!summary) return "";
2052
2716
  return `${summary.sets}\xD7${summary.reps} @ ${roundWeight(summary.weight)} ${summary.unit}`;
2053
2717
  }
2054
- function formatAccessibilityLabel2(name, summary, prescription) {
2718
+ function formatAccessibilityLabel(name, summary, prescription) {
2055
2719
  if (summary) return `${name}, ${formatSummary(summary)}`;
2056
2720
  if (prescription) return `${name}, ${prescription}`;
2057
2721
  return name;
@@ -2060,7 +2724,6 @@ function CollapsedCard({
2060
2724
  name,
2061
2725
  onToggle,
2062
2726
  summary,
2063
- e1rm,
2064
2727
  isPR,
2065
2728
  setVelocities,
2066
2729
  velocityZones,
@@ -2079,7 +2742,7 @@ function CollapsedCard({
2079
2742
  onPressIn: () => setPressed(true),
2080
2743
  onPressOut: () => setPressed(false),
2081
2744
  accessibilityRole: "button",
2082
- accessibilityLabel: formatAccessibilityLabel2(name, summary, void 0),
2745
+ accessibilityLabel: formatAccessibilityLabel(name, summary, void 0),
2083
2746
  testID: "exercise-card",
2084
2747
  children: /* @__PURE__ */ jsxs(
2085
2748
  reactNative.View,
@@ -2093,100 +2756,93 @@ function CollapsedCard({
2093
2756
  ...supersetMargin
2094
2757
  },
2095
2758
  children: [
2096
- /* @__PURE__ */ jsxs(
2097
- reactNative.View,
2098
- {
2099
- className: "flex-row items-center",
2100
- testID: "exercise-card-header",
2101
- children: [
2102
- /* @__PURE__ */ jsx(
2103
- reactNative.Text,
2104
- {
2105
- className: "text-text-primary",
2106
- style: {
2107
- fontSize: 14,
2108
- fontFamily: '"Space Grotesk", sans-serif',
2109
- fontWeight: "700"
2110
- },
2111
- testID: "exercise-card-name",
2112
- children: name
2113
- }
2114
- ),
2115
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2116
- summary && /* @__PURE__ */ jsx(
2117
- reactNative.Text,
2118
- {
2119
- className: "text-text-secondary",
2120
- style: {
2121
- fontSize: 12,
2122
- fontFamily: "Inter, sans-serif",
2123
- marginRight: 8
2124
- },
2125
- testID: "exercise-card-summary",
2126
- children: formatSummary(summary)
2127
- }
2128
- ),
2129
- e1rm && /* @__PURE__ */ jsx(
2130
- WeightBadge,
2131
- {
2132
- value: roundWeight(e1rm.value),
2133
- unit: e1rm.unit,
2134
- size: "sm",
2135
- showIcon: false,
2136
- testID: "exercise-card-e1rm"
2137
- }
2138
- ),
2139
- isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
2140
- ]
2141
- }
2142
- ),
2143
- (completedSets > 0 || remaining > 0) && /* @__PURE__ */ jsxs(
2144
- reactNative.View,
2145
- {
2146
- className: "flex-row",
2147
- style: { marginTop: 6, gap: 4 },
2148
- testID: "exercise-card-strips",
2149
- children: [
2150
- setVelocities?.map((velocities, i) => /* @__PURE__ */ jsx(
2151
- VelocityStrip,
2152
- {
2153
- velocities,
2154
- zones: velocityZones,
2155
- variant: "mini",
2156
- testID: `exercise-card-velocity-strip-${i}`
2157
- },
2158
- i
2159
- )),
2160
- Array.from({ length: remaining }, (_, i) => /* @__PURE__ */ jsx(
2161
- PlaceholderStrip,
2162
- {
2163
- testID: `exercise-card-placeholder-${i}`
2164
- },
2165
- `placeholder-${i}`
2166
- ))
2167
- ]
2168
- }
2169
- )
2759
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", testID: "exercise-card-header", children: [
2760
+ /* @__PURE__ */ jsx(
2761
+ reactNative.Text,
2762
+ {
2763
+ className: "text-text-primary",
2764
+ style: {
2765
+ fontSize: 14,
2766
+ fontFamily: '"Space Grotesk", sans-serif',
2767
+ fontWeight: "700"
2768
+ },
2769
+ testID: "exercise-card-name",
2770
+ children: name
2771
+ }
2772
+ ),
2773
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2774
+ summary && /* @__PURE__ */ jsx(
2775
+ reactNative.Text,
2776
+ {
2777
+ className: "text-text-secondary",
2778
+ style: {
2779
+ fontSize: 12,
2780
+ fontFamily: "Inter, sans-serif",
2781
+ marginRight: 8
2782
+ },
2783
+ testID: "exercise-card-summary",
2784
+ children: formatSummary(summary)
2785
+ }
2786
+ ),
2787
+ isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
2788
+ ] }),
2789
+ (completedSets > 0 || remaining > 0) && /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row", style: { marginTop: 6, gap: 4 }, testID: "exercise-card-strips", children: [
2790
+ setVelocities?.map((velocities, i) => /* @__PURE__ */ jsx(
2791
+ VelocityStrip,
2792
+ {
2793
+ velocities,
2794
+ zones: velocityZones,
2795
+ variant: "mini",
2796
+ testID: `exercise-card-velocity-strip-${i}`
2797
+ },
2798
+ i
2799
+ )),
2800
+ Array.from({ length: remaining }, (_, i) => /* @__PURE__ */ jsx(
2801
+ PlaceholderStrip,
2802
+ {
2803
+ testID: `exercise-card-placeholder-${i}`
2804
+ },
2805
+ `placeholder-${i}`
2806
+ ))
2807
+ ] })
2170
2808
  ]
2171
2809
  }
2172
2810
  )
2173
2811
  }
2174
2812
  );
2175
2813
  }
2814
+ var DARK2 = getSemanticColors("dark");
2815
+ var BODY_DIVIDER = DARK2["border-subtle"];
2816
+ function deriveHeaderSetStates(sets) {
2817
+ return sets.map((set) => {
2818
+ switch (set.state) {
2819
+ case "live":
2820
+ return { status: "active", velocities: set.velocities, planned: set.target.reps };
2821
+ case "done":
2822
+ return { status: "done", velocities: set.velocities };
2823
+ case "todo":
2824
+ return { status: "todo", planned: set.planned ?? set.target.reps };
2825
+ }
2826
+ });
2827
+ }
2176
2828
  function ExpandedCard({
2177
2829
  name,
2178
2830
  onToggle,
2179
2831
  summary,
2180
- e1rm,
2181
2832
  isPR,
2182
2833
  sets,
2183
2834
  tempo,
2835
+ indicator,
2836
+ setStates,
2837
+ stripHeight = 8,
2838
+ velocityZones,
2184
2839
  supersetPosition
2185
2840
  }) {
2186
2841
  const borderRadius = getSupersetBorderRadius(supersetPosition);
2187
2842
  const supersetMargin = getSupersetMargin(supersetPosition);
2188
2843
  const unit = summary?.unit ?? sets?.[0]?.unit ?? "lbs";
2189
- const columnHeaders = buildColumnHeaders(unit);
2844
+ const headerStates = setStates ?? (sets ? deriveHeaderSetStates(sets) : []);
2845
+ const headerIndicator = indicator ?? (isPR ? "pr" : void 0);
2190
2846
  return /* @__PURE__ */ jsxs(
2191
2847
  reactNative.View,
2192
2848
  {
@@ -2199,114 +2855,32 @@ function ExpandedCard({
2199
2855
  testID: "exercise-card",
2200
2856
  children: [
2201
2857
  /* @__PURE__ */ jsx(
2202
- reactNative.Pressable,
2858
+ ExerciseCardHeading,
2203
2859
  {
2860
+ name,
2861
+ sets: summary?.sets ?? sets?.length ?? 0,
2862
+ reps: summary?.reps ?? 0,
2863
+ load: summary?.weight ?? 0,
2864
+ unit,
2865
+ tempo,
2866
+ indicator: headerIndicator,
2867
+ setStates: headerStates,
2868
+ stripHeight,
2204
2869
  onPress: onToggle,
2205
- accessibilityRole: "button",
2206
- accessibilityLabel: formatAccessibilityLabel2(name, summary, void 0),
2207
- "aria-expanded": true,
2208
- testID: "exercise-card-header",
2209
- children: /* @__PURE__ */ jsxs(
2210
- reactNative.View,
2211
- {
2212
- className: "flex-row items-center",
2213
- style: {
2214
- padding: 12,
2215
- paddingHorizontal: 14,
2216
- paddingBottom: 10,
2217
- borderBottomWidth: 1,
2218
- borderBottomColor: resolveColor("border-default")
2219
- },
2220
- children: [
2221
- /* @__PURE__ */ jsx(
2222
- reactNative.Text,
2223
- {
2224
- className: "text-text-primary",
2225
- style: {
2226
- fontSize: 14,
2227
- fontFamily: '"Space Grotesk", sans-serif',
2228
- fontWeight: "700"
2229
- },
2230
- testID: "exercise-card-name",
2231
- children: name
2232
- }
2233
- ),
2234
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2235
- summary && /* @__PURE__ */ jsx(
2236
- reactNative.Text,
2237
- {
2238
- className: "text-text-secondary",
2239
- style: {
2240
- fontSize: 12,
2241
- fontFamily: "Inter, sans-serif",
2242
- marginRight: 8
2243
- },
2244
- testID: "exercise-card-summary",
2245
- children: formatSummary(summary)
2246
- }
2247
- ),
2248
- e1rm && /* @__PURE__ */ jsx(
2249
- WeightBadge,
2250
- {
2251
- value: roundWeight(e1rm.value),
2252
- unit: e1rm.unit,
2253
- size: "sm",
2254
- showIcon: false,
2255
- testID: "exercise-card-e1rm"
2256
- }
2257
- ),
2258
- isPR && /* @__PURE__ */ jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsx(PrBadge, { type: "e1rm", compact: true, animate: false }) })
2259
- ]
2260
- }
2261
- )
2870
+ testID: "exercise-card-heading"
2262
2871
  }
2263
2872
  ),
2264
- tempo && /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: 8, paddingHorizontal: 14 }, testID: "exercise-card-tempo", children: /* @__PURE__ */ jsx(TempoDisplay, { tempo, size: "sm" }) }),
2265
- /* @__PURE__ */ jsx(
2873
+ /* @__PURE__ */ jsxs(
2266
2874
  reactNative.View,
2267
2875
  {
2268
- className: "flex-row",
2269
- style: {
2270
- paddingVertical: 8,
2271
- paddingHorizontal: 8,
2272
- paddingBottom: 4
2273
- },
2274
- testID: "exercise-card-column-headers",
2275
- children: columnHeaders.map((header, i) => {
2276
- const widths = [36, void 0, 44, 56, 36];
2277
- const width = widths[i];
2278
- const isFlex = width === void 0;
2279
- return /* @__PURE__ */ jsx(
2280
- reactNative.View,
2281
- {
2282
- style: {
2283
- ...isFlex ? {} : { width },
2284
- alignItems: "center",
2285
- justifyContent: "center",
2286
- gap: 8
2287
- },
2288
- ...isFlex ? { className: "flex-1" } : {},
2289
- children: /* @__PURE__ */ jsx(
2290
- reactNative.Text,
2291
- {
2292
- className: "text-text-tertiary",
2293
- style: {
2294
- fontSize: 10,
2295
- fontWeight: "600",
2296
- fontFamily: "Inter, sans-serif",
2297
- textTransform: "uppercase",
2298
- letterSpacing: 0.8
2299
- },
2300
- children: header
2301
- }
2302
- )
2303
- },
2304
- header
2305
- );
2306
- })
2876
+ style: { borderTopWidth: 1, borderTopColor: BODY_DIVIDER, paddingBottom: 6 },
2877
+ testID: "exercise-card-body",
2878
+ children: [
2879
+ /* @__PURE__ */ jsx(SetTableHeader, { unit, showPrevious: false, testID: "exercise-card-column-headers" }),
2880
+ sets && /* @__PURE__ */ jsx(reactNative.View, { testID: "exercise-card-sets", children: sets.map((set, i) => /* @__PURE__ */ jsx(SetRow, { ...set, velocityZones: set.velocityZones ?? velocityZones }, i)) })
2881
+ ]
2307
2882
  }
2308
- ),
2309
- sets && /* @__PURE__ */ jsx(reactNative.View, { testID: "exercise-card-sets", children: sets.map((setProps, i) => /* @__PURE__ */ jsx(SetRow, { ...setProps }, i)) })
2883
+ )
2310
2884
  ]
2311
2885
  }
2312
2886
  );
@@ -2315,11 +2889,12 @@ function UpcomingCard({
2315
2889
  name,
2316
2890
  prescription,
2317
2891
  previousBest,
2318
- supersetPosition
2892
+ supersetPosition,
2893
+ onToggle
2319
2894
  }) {
2320
2895
  const borderRadius = getSupersetBorderRadius(supersetPosition);
2321
2896
  const supersetMargin = getSupersetMargin(supersetPosition);
2322
- return /* @__PURE__ */ jsx(
2897
+ return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress: onToggle, accessibilityRole: "button", children: /* @__PURE__ */ jsx(
2323
2898
  reactNative.View,
2324
2899
  {
2325
2900
  style: {
@@ -2329,7 +2904,7 @@ function UpcomingCard({
2329
2904
  ...borderRadius,
2330
2905
  ...supersetMargin
2331
2906
  },
2332
- accessibilityLabel: formatAccessibilityLabel2(name, void 0, prescription),
2907
+ accessibilityLabel: formatAccessibilityLabel(name, void 0, prescription),
2333
2908
  testID: "exercise-card",
2334
2909
  children: /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", children: [
2335
2910
  /* @__PURE__ */ jsx(
@@ -2337,6 +2912,7 @@ function UpcomingCard({
2337
2912
  {
2338
2913
  className: "text-text-primary",
2339
2914
  style: {
2915
+ flexShrink: 0,
2340
2916
  fontSize: 14,
2341
2917
  fontFamily: '"Space Grotesk", sans-serif',
2342
2918
  fontWeight: "700"
@@ -2349,7 +2925,9 @@ function UpcomingCard({
2349
2925
  reactNative.Text,
2350
2926
  {
2351
2927
  className: "text-text-secondary",
2928
+ numberOfLines: 1,
2352
2929
  style: {
2930
+ flexShrink: 1,
2353
2931
  fontSize: 12,
2354
2932
  fontFamily: "Inter, sans-serif",
2355
2933
  marginLeft: 8
@@ -2358,12 +2936,14 @@ function UpcomingCard({
2358
2936
  children: prescription
2359
2937
  }
2360
2938
  ),
2361
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1" }),
2939
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { minWidth: 8 } }),
2362
2940
  previousBest && /* @__PURE__ */ jsx(
2363
2941
  reactNative.Text,
2364
2942
  {
2365
2943
  className: "text-text-tertiary",
2944
+ numberOfLines: 1,
2366
2945
  style: {
2946
+ flexShrink: 1,
2367
2947
  fontSize: 11,
2368
2948
  fontFamily: "Inter, sans-serif"
2369
2949
  },
@@ -2373,18 +2953,31 @@ function UpcomingCard({
2373
2953
  )
2374
2954
  ] })
2375
2955
  }
2376
- );
2956
+ ) });
2377
2957
  }
2378
- function ExerciseCard(props) {
2379
- switch (props.state) {
2380
- case "collapsed":
2381
- return /* @__PURE__ */ jsx(CollapsedCard, { ...props });
2382
- case "expanded":
2383
- return /* @__PURE__ */ jsx(ExpandedCard, { ...props });
2384
- case "upcoming":
2385
- return /* @__PURE__ */ jsx(UpcomingCard, { ...props });
2386
- }
2958
+ function ExerciseCard({
2959
+ upcoming,
2960
+ expanded,
2961
+ defaultExpanded,
2962
+ onExpandedChange,
2963
+ ...rest
2964
+ }) {
2965
+ const [internalExpanded, setInternalExpanded] = react.useState(defaultExpanded ?? false);
2966
+ const isControlled = expanded !== void 0;
2967
+ const isExpanded = isControlled ? expanded : internalExpanded;
2968
+ const onToggle = () => {
2969
+ const next = !isExpanded;
2970
+ onExpandedChange?.(next);
2971
+ if (!isControlled) setInternalExpanded(next);
2972
+ };
2973
+ if (upcoming) return /* @__PURE__ */ jsx(UpcomingCard, { ...rest, onToggle });
2974
+ return isExpanded ? /* @__PURE__ */ jsx(ExpandedCard, { ...rest, onToggle }) : /* @__PURE__ */ jsx(CollapsedCard, { ...rest, onToggle });
2975
+ }
2976
+ function resolveColor(token, mode = "dark") {
2977
+ return reactNative.Platform.OS === "web" ? `var(--color-${token})` : getSemanticColors(mode)[token];
2387
2978
  }
2979
+
2980
+ // src/components/custom/Workout/InputBar.tsx
2388
2981
  var BRAND_PRIMARY2 = getSemanticColors("dark")["brand-primary"];
2389
2982
  var INPUT_CLASSNAME = "bg-surface-raised border-border-strong text-text-primary";
2390
2983
  var inputStyle = {
@@ -2541,7 +3134,307 @@ function InputBar({
2541
3134
  }
2542
3135
  );
2543
3136
  }
3137
+ function formatDuration(ms) {
3138
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1e3));
3139
+ const minutes = Math.floor(totalSeconds / 60);
3140
+ const seconds = totalSeconds % 60;
3141
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
3142
+ }
3143
+ function useTimer(options = {}) {
3144
+ const {
3145
+ mode = "up",
3146
+ durationMs,
3147
+ elapsedMs: controlledElapsedMs,
3148
+ running = false,
3149
+ autoTick = false,
3150
+ tickMs = 1e3
3151
+ } = options;
3152
+ const isControlled = controlledElapsedMs != null;
3153
+ const [internalElapsedMs, setInternalElapsedMs] = react.useState(0);
3154
+ react.useEffect(() => {
3155
+ if (isControlled || !autoTick || !running) return;
3156
+ const id = setInterval(() => {
3157
+ setInternalElapsedMs((prev) => prev + tickMs);
3158
+ }, tickMs);
3159
+ return () => clearInterval(id);
3160
+ }, [isControlled, autoTick, running, tickMs]);
3161
+ const elapsedMs = isControlled ? controlledElapsedMs : internalElapsedMs;
3162
+ const hasDuration = durationMs != null && durationMs > 0;
3163
+ const remainingMs = durationMs != null ? Math.max(0, durationMs - elapsedMs) : 0;
3164
+ let progress;
3165
+ if (hasDuration) {
3166
+ progress = Math.min(1, Math.max(0, elapsedMs / durationMs));
3167
+ } else {
3168
+ progress = mode === "down" ? 1 : 0;
3169
+ }
3170
+ const label = formatDuration(mode === "down" ? remainingMs : elapsedMs);
3171
+ const done = durationMs != null && elapsedMs >= durationMs;
3172
+ return { elapsedMs, remainingMs, progress, label, done };
3173
+ }
3174
+ var colorVarMap = {
3175
+ primary: "var(--color-brand-primary)",
3176
+ secondary: "var(--color-brand-secondary)",
3177
+ success: "var(--color-status-success)",
3178
+ error: "var(--color-status-error)",
3179
+ warning: "var(--color-status-warning)",
3180
+ info: "var(--color-status-info)"
3181
+ };
3182
+ function CircularProgress({
3183
+ value = 0,
3184
+ max = 100,
3185
+ isIndeterminate = false,
3186
+ size = 48,
3187
+ strokeWidth = 4,
3188
+ color = "primary",
3189
+ showValue = false,
3190
+ formatValue: formatValue2 = (v, m) => `${Math.round(v / m * 100)}%`,
3191
+ children,
3192
+ fill = false,
3193
+ className,
3194
+ ...props
3195
+ }) {
3196
+ const percentage = Math.min(Math.max(value / max * 100, 0), 100);
3197
+ const radius = (size - strokeWidth) / 2;
3198
+ const circumference = 2 * Math.PI * radius;
3199
+ const strokeDashoffset = circumference - percentage / 100 * circumference;
3200
+ const center = size / 2;
3201
+ const boxStyle = fill ? { width: "100%", aspectRatio: 1 } : { width: size, height: size };
3202
+ return /* @__PURE__ */ jsxs(
3203
+ reactNative.View,
3204
+ {
3205
+ className: cn("items-center justify-center", className),
3206
+ style: boxStyle,
3207
+ accessibilityRole: "progressbar",
3208
+ accessibilityValue: {
3209
+ min: 0,
3210
+ max,
3211
+ now: isIndeterminate ? void 0 : value
3212
+ },
3213
+ ...props,
3214
+ children: [
3215
+ /* @__PURE__ */ jsx(reactNative.View, { className: cn(isIndeterminate && "animate-spin"), style: boxStyle, children: /* @__PURE__ */ jsxs(
3216
+ "svg",
3217
+ {
3218
+ width: "100%",
3219
+ height: "100%",
3220
+ viewBox: `0 0 ${size} ${size}`,
3221
+ style: { position: "absolute" },
3222
+ children: [
3223
+ /* @__PURE__ */ jsx(
3224
+ "circle",
3225
+ {
3226
+ cx: center,
3227
+ cy: center,
3228
+ r: radius,
3229
+ fill: "none",
3230
+ stroke: "var(--color-border-default)",
3231
+ strokeWidth
3232
+ }
3233
+ ),
3234
+ /* @__PURE__ */ jsx(
3235
+ "circle",
3236
+ {
3237
+ cx: center,
3238
+ cy: center,
3239
+ r: radius,
3240
+ fill: "none",
3241
+ stroke: colorVarMap[color],
3242
+ strokeWidth,
3243
+ strokeLinecap: "round",
3244
+ strokeDasharray: circumference,
3245
+ strokeDashoffset: isIndeterminate ? circumference * 0.75 : strokeDashoffset,
3246
+ transform: `rotate(-90 ${center} ${center})`,
3247
+ style: { transition: "stroke-dashoffset 300ms cubic-bezier(0.22, 1, 0.36, 1)" }
3248
+ }
3249
+ )
3250
+ ]
3251
+ }
3252
+ ) }),
3253
+ children != null ? /* @__PURE__ */ jsx(
3254
+ reactNative.View,
3255
+ {
3256
+ style: {
3257
+ position: "absolute",
3258
+ alignItems: "center",
3259
+ justifyContent: "center",
3260
+ pointerEvents: "none"
3261
+ },
3262
+ children
3263
+ }
3264
+ ) : showValue && !isIndeterminate && /* @__PURE__ */ jsx(
3265
+ reactNative.Text,
3266
+ {
3267
+ className: "text-xs font-semibold text-text-primary absolute",
3268
+ style: { fontSize: Math.max(size * 0.22, 10) },
3269
+ children: formatValue2(value, max)
3270
+ }
3271
+ )
3272
+ ]
3273
+ }
3274
+ );
3275
+ }
3276
+
3277
+ // src/components/custom/CircularTimer/CircularTimer.tsx
3278
+ function CircularTimer({
3279
+ durationMs,
3280
+ elapsedMs,
3281
+ mode = "down",
3282
+ size = 160,
3283
+ strokeWidth = 8,
3284
+ fill = false,
3285
+ color = "primary",
3286
+ doneLabel,
3287
+ doneColor = "success",
3288
+ controls,
3289
+ accessibilityLabel: accessibilityLabel2,
3290
+ testID
3291
+ }) {
3292
+ const { remainingMs, progress, label, done } = useTimer({ mode, durationMs, elapsedMs });
3293
+ const isDone = done && mode === "down";
3294
+ const showDone = isDone && doneLabel != null;
3295
+ const remainingFrac = durationMs > 0 ? Math.max(0, Math.min(1, remainingMs / durationMs)) : 0;
3296
+ const arc = mode === "down" ? showDone ? 1 : remainingFrac : progress;
3297
+ const ringColor = showDone ? doneColor : color;
3298
+ const remainingSec = Math.ceil(Math.max(0, remainingMs) / 1e3);
3299
+ const a11y = accessibilityLabel2 ?? (isDone ? "Timer complete" : `${remainingSec} seconds remaining`);
3300
+ return /* @__PURE__ */ jsxs(
3301
+ reactNative.View,
3302
+ {
3303
+ style: { alignItems: "center", gap: 16 },
3304
+ accessibilityRole: "timer",
3305
+ accessibilityLabel: a11y,
3306
+ testID: testID ?? "circular-timer",
3307
+ children: [
3308
+ /* @__PURE__ */ jsx(
3309
+ CircularProgress,
3310
+ {
3311
+ value: arc,
3312
+ max: 1,
3313
+ size,
3314
+ strokeWidth,
3315
+ fill,
3316
+ color: ringColor,
3317
+ accessibilityLabel: "Timer ring",
3318
+ testID: "circular-timer-ring",
3319
+ children: /* @__PURE__ */ jsx(
3320
+ reactNative.Text,
3321
+ {
3322
+ className: showDone ? void 0 : "text-text-primary",
3323
+ style: {
3324
+ fontSize: Math.round(size * (showDone ? 0.28 : 0.24)),
3325
+ fontFamily: '"Space Grotesk", sans-serif',
3326
+ fontWeight: "800",
3327
+ fontVariant: ["tabular-nums"],
3328
+ letterSpacing: showDone ? 1 : -1,
3329
+ ...showDone ? { color: colorVarMap[doneColor] } : null
3330
+ },
3331
+ testID: "circular-timer-label",
3332
+ children: showDone ? doneLabel : label
3333
+ }
3334
+ )
3335
+ }
3336
+ ),
3337
+ controls
3338
+ ]
3339
+ }
3340
+ );
3341
+ }
3342
+
3343
+ // src/components/custom/Workout/RestTimer.tsx
2544
3344
  var BRAND_PRIMARY3 = getSemanticColors("dark")["brand-primary"];
3345
+ var RING_DEFAULT_SIZE = 180;
3346
+ function RestActions({ onAddTime, onSkip }) {
3347
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
3348
+ /* @__PURE__ */ jsx(
3349
+ reactNative.Pressable,
3350
+ {
3351
+ onPress: onAddTime,
3352
+ style: {
3353
+ backgroundColor: "rgba(255,255,255,0.06)",
3354
+ paddingVertical: 8,
3355
+ paddingHorizontal: 20,
3356
+ borderRadius: 8
3357
+ },
3358
+ accessibilityRole: "button",
3359
+ accessibilityLabel: "Add 30 seconds",
3360
+ testID: "rest-timer-add-time",
3361
+ children: /* @__PURE__ */ jsx(
3362
+ reactNative.Text,
3363
+ {
3364
+ className: "text-text-secondary",
3365
+ style: { fontSize: 11, fontFamily: "Inter, sans-serif", fontWeight: "600" },
3366
+ children: "+30s"
3367
+ }
3368
+ )
3369
+ }
3370
+ ),
3371
+ /* @__PURE__ */ jsx(
3372
+ reactNative.Pressable,
3373
+ {
3374
+ onPress: onSkip,
3375
+ style: {
3376
+ backgroundColor: "rgba(255,121,0,0.12)",
3377
+ paddingVertical: 8,
3378
+ paddingHorizontal: 20,
3379
+ borderRadius: 8
3380
+ },
3381
+ accessibilityRole: "button",
3382
+ accessibilityLabel: "Skip rest",
3383
+ testID: "rest-timer-skip",
3384
+ children: /* @__PURE__ */ jsx(
3385
+ reactNative.Text,
3386
+ {
3387
+ style: {
3388
+ fontSize: 11,
3389
+ fontFamily: "Inter, sans-serif",
3390
+ fontWeight: "600",
3391
+ color: BRAND_PRIMARY3
3392
+ },
3393
+ children: "Skip"
3394
+ }
3395
+ )
3396
+ }
3397
+ )
3398
+ ] });
3399
+ }
3400
+ function RestTimerRing({
3401
+ totalSeconds,
3402
+ elapsedMs,
3403
+ remainingSec,
3404
+ done,
3405
+ size,
3406
+ nextSetInfo,
3407
+ displayOnly,
3408
+ onSkip,
3409
+ onAddTime
3410
+ }) {
3411
+ const a11yLabel = done ? "Rest complete, next set ready" : `Rest timer, ${Math.max(0, remainingSec)} seconds remaining`;
3412
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { alignItems: "center", gap: 16 }, testID: "rest-timer", children: [
3413
+ /* @__PURE__ */ jsx(
3414
+ CircularTimer,
3415
+ {
3416
+ durationMs: totalSeconds * 1e3,
3417
+ elapsedMs,
3418
+ mode: "down",
3419
+ size,
3420
+ doneLabel: "GO",
3421
+ doneColor: "success",
3422
+ accessibilityLabel: a11yLabel,
3423
+ controls: !displayOnly ? /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip }) : void 0,
3424
+ testID: "rest-timer-ring"
3425
+ }
3426
+ ),
3427
+ nextSetInfo != null && /* @__PURE__ */ jsx(
3428
+ reactNative.Text,
3429
+ {
3430
+ className: "text-text-tertiary",
3431
+ style: { fontSize: 13, fontFamily: "Inter, sans-serif" },
3432
+ testID: "rest-timer-next-set",
3433
+ children: nextSetInfo
3434
+ }
3435
+ )
3436
+ ] });
3437
+ }
2545
3438
  function RestTimer({
2546
3439
  totalSeconds,
2547
3440
  elapsedMs,
@@ -2549,16 +3442,39 @@ function RestTimer({
2549
3442
  onAddTime,
2550
3443
  nextSetInfo,
2551
3444
  visible,
2552
- displayOnly = false
3445
+ displayOnly = false,
3446
+ variant = "bar",
3447
+ size = RING_DEFAULT_SIZE
2553
3448
  }) {
2554
- if (!visible) return null;
2555
- const remainingMs = Math.max(0, totalSeconds * 1e3 - elapsedMs);
3449
+ const {
3450
+ remainingMs,
3451
+ progress,
3452
+ label: timeDisplay,
3453
+ done
3454
+ } = useTimer({
3455
+ mode: "down",
3456
+ durationMs: totalSeconds * 1e3,
3457
+ elapsedMs
3458
+ });
2556
3459
  const remainingSec = Math.ceil(remainingMs / 1e3);
2557
- const minutes = Math.floor(remainingSec / 60);
2558
- const seconds = remainingSec % 60;
2559
- const timeDisplay = `${minutes}:${String(seconds).padStart(2, "0")}`;
2560
- const totalMs = totalSeconds * 1e3;
2561
- const progressPct = totalMs > 0 ? Math.min(100, elapsedMs / totalMs * 100) : 100;
3460
+ const progressPct = progress * 100;
3461
+ if (!visible) return null;
3462
+ if (variant === "ring") {
3463
+ return /* @__PURE__ */ jsx(
3464
+ RestTimerRing,
3465
+ {
3466
+ totalSeconds,
3467
+ elapsedMs,
3468
+ remainingSec,
3469
+ done,
3470
+ size,
3471
+ nextSetInfo,
3472
+ displayOnly,
3473
+ onSkip,
3474
+ onAddTime
3475
+ }
3476
+ );
3477
+ }
2562
3478
  return /* @__PURE__ */ jsxs(
2563
3479
  reactNative.View,
2564
3480
  {
@@ -2656,62 +3572,7 @@ function RestTimer({
2656
3572
  )
2657
3573
  }
2658
3574
  ),
2659
- !displayOnly && /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
2660
- /* @__PURE__ */ jsx(
2661
- reactNative.Pressable,
2662
- {
2663
- onPress: onAddTime,
2664
- style: {
2665
- backgroundColor: "rgba(255,255,255,0.06)",
2666
- paddingVertical: 8,
2667
- paddingHorizontal: 20,
2668
- borderRadius: 8
2669
- },
2670
- accessibilityRole: "button",
2671
- accessibilityLabel: "Add 30 seconds",
2672
- testID: "rest-timer-add-time",
2673
- children: /* @__PURE__ */ jsx(
2674
- reactNative.Text,
2675
- {
2676
- className: "text-text-secondary",
2677
- style: {
2678
- fontSize: 11,
2679
- fontFamily: "Inter, sans-serif",
2680
- fontWeight: "600"
2681
- },
2682
- children: "+30s"
2683
- }
2684
- )
2685
- }
2686
- ),
2687
- /* @__PURE__ */ jsx(
2688
- reactNative.Pressable,
2689
- {
2690
- onPress: onSkip,
2691
- style: {
2692
- backgroundColor: "rgba(255,121,0,0.12)",
2693
- paddingVertical: 8,
2694
- paddingHorizontal: 20,
2695
- borderRadius: 8
2696
- },
2697
- accessibilityRole: "button",
2698
- accessibilityLabel: "Skip rest",
2699
- testID: "rest-timer-skip",
2700
- children: /* @__PURE__ */ jsx(
2701
- reactNative.Text,
2702
- {
2703
- style: {
2704
- fontSize: 11,
2705
- fontFamily: "Inter, sans-serif",
2706
- fontWeight: "600",
2707
- color: BRAND_PRIMARY3
2708
- },
2709
- children: "Skip"
2710
- }
2711
- )
2712
- }
2713
- )
2714
- ] })
3575
+ !displayOnly && /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip })
2715
3576
  ]
2716
3577
  }
2717
3578
  );
@@ -2797,23 +3658,22 @@ function deriveWorkoutProgress(exercises) {
2797
3658
  function findActiveExercise(exercises) {
2798
3659
  return exercises.find((exercise) => exercise.status === "active") ?? null;
2799
3660
  }
2800
- function statusToCardState(status, focused) {
2801
- if (status === "upcoming") return "upcoming";
2802
- return focused ? "expanded" : "collapsed";
3661
+ function isUpcomingExercise(status) {
3662
+ return status === "upcoming";
2803
3663
  }
2804
3664
  function toActiveCardProps(exercise, focused, onToggle, supersetPosition = null, supersetColor) {
2805
- const state = statusToCardState(exercise.status, focused);
3665
+ const upcoming = isUpcomingExercise(exercise.status);
2806
3666
  return {
2807
3667
  name: exercise.name,
2808
- state,
2809
- onToggle,
3668
+ upcoming,
3669
+ expanded: focused,
3670
+ onExpandedChange: onToggle,
2810
3671
  summary: exercise.summary,
2811
- e1rm: exercise.e1rm,
2812
3672
  isPR: exercise.isPR,
2813
3673
  tempo: exercise.tempo,
2814
3674
  setVelocities: exercise.setVelocities,
2815
3675
  totalPlannedSets: exercise.totalPlannedSets,
2816
- sets: state === "expanded" ? exercise.sets : void 0,
3676
+ sets: focused ? exercise.sets : void 0,
2817
3677
  prescription: exercise.prescription,
2818
3678
  previousBest: exercise.previousBest,
2819
3679
  supersetPosition,
@@ -3502,11 +4362,11 @@ function CardContent({ children, className }) {
3502
4362
  function CardFooter({ children, className }) {
3503
4363
  return /* @__PURE__ */ jsx(reactNative.View, { className: cn("px-6 py-4 flex-row items-center gap-2", className), children });
3504
4364
  }
3505
- var t3 = getSemanticColors("dark");
4365
+ var t5 = getSemanticColors("dark");
3506
4366
  var solidVariantColors = {
3507
- success: t3["status-success"],
3508
- warning: t3["status-warning"],
3509
- error: t3["status-error"],
4367
+ success: t5["status-success"],
4368
+ warning: t5["status-warning"],
4369
+ error: t5["status-error"],
3510
4370
  neutral: "#6B7280"
3511
4371
  };
3512
4372
  var ringVariantStyles = {
@@ -3546,8 +4406,8 @@ var iconColors = {
3546
4406
  warning: "#6B4000",
3547
4407
  error: "#5C1A1A",
3548
4408
  neutral: "#D1D5DB",
3549
- "on-track": t3["status-success"],
3550
- deviation: t3["status-warning"],
4409
+ "on-track": t5["status-success"],
4410
+ deviation: t5["status-warning"],
3551
4411
  future: "#6B7280"
3552
4412
  };
3553
4413
  var sizeDimensions = {
@@ -3633,13 +4493,13 @@ var MESO_ACCENT_GRADIENT_LIGHT = primitiveRamps.orange[300];
3633
4493
  var WORKOUT_PILL_DELOAD = primitiveRamps.magenta[600];
3634
4494
 
3635
4495
  // src/components/custom/Workout/MesoStatusCard.tsx
3636
- var t4 = getSemanticColors("dark");
3637
- var BRAND_PRIMARY5 = t4["brand-primary"];
4496
+ var t6 = getSemanticColors("dark");
4497
+ var BRAND_PRIMARY5 = t6["brand-primary"];
3638
4498
  var BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK;
3639
4499
  var BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT;
3640
- var SUCCESS = t4["status-success"];
3641
- var WARNING = t4["status-warning"];
3642
- var ERROR = t4["status-error"];
4500
+ var SUCCESS = t6["status-success"];
4501
+ var WARNING = t6["status-warning"];
4502
+ var ERROR = t6["status-error"];
3643
4503
  var ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY5, BRAND_PRIMARY_LIGHT];
3644
4504
  var CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor("surface-elevated")} 0%, ${resolveColor("surface-raised")} 100%)`;
3645
4505
  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%)";
@@ -3711,7 +4571,7 @@ function StatusPill({ badge }) {
3711
4571
  }
3712
4572
  );
3713
4573
  }
3714
- function MetricCell({ metric }) {
4574
+ function MetricCell2({ metric }) {
3715
4575
  return /* @__PURE__ */ jsxs(reactNative.View, { style: { width: "48%" }, testID: "meso-status-card-metric", children: [
3716
4576
  /* @__PURE__ */ jsx(
3717
4577
  reactNative.Text,
@@ -3929,7 +4789,7 @@ function MesoStatusCard({
3929
4789
  className: "flex-row flex-wrap",
3930
4790
  style: { gap: 8 },
3931
4791
  testID: "meso-status-card-metrics",
3932
- children: metrics.map((metric, index) => /* @__PURE__ */ jsx(MetricCell, { metric }, `${metric.label}-${index}`))
4792
+ children: metrics.map((metric, index) => /* @__PURE__ */ jsx(MetricCell2, { metric }, `${metric.label}-${index}`))
3933
4793
  }
3934
4794
  ),
3935
4795
  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}`)) }),
@@ -4010,8 +4870,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
4010
4870
  var PLOT_LEFT = 26;
4011
4871
  var TOOLTIP_WIDTH = 124;
4012
4872
  function timeOf(dateStr) {
4013
- const t10 = Date.parse(dateStr);
4014
- return Number.isNaN(t10) ? 0 : t10;
4873
+ const t12 = Date.parse(dateStr);
4874
+ return Number.isNaN(t12) ? 0 : t12;
4015
4875
  }
4016
4876
  function formatValue(value) {
4017
4877
  return `${Math.round(value)}`;
@@ -4500,10 +5360,10 @@ function StrengthTrendChart({
4500
5360
  }
4501
5361
  );
4502
5362
  }
4503
- var t5 = getSemanticColors("dark");
4504
- var STATUS_SUCCESS2 = t5["status-success"];
4505
- var STATUS_WARNING2 = t5["status-warning"];
4506
- var STATUS_INFO = t5["status-info"];
5363
+ var t7 = getSemanticColors("dark");
5364
+ var STATUS_SUCCESS2 = t7["status-success"];
5365
+ var STATUS_WARNING2 = t7["status-warning"];
5366
+ var STATUS_INFO = t7["status-info"];
4507
5367
  var DOT_BORDER = "#F3F4F6";
4508
5368
  var BAND_FILL = "rgba(46,213,115,0.1)";
4509
5369
  var BAND_EDGE = "rgba(46,213,115,0.45)";
@@ -4546,13 +5406,13 @@ function interpEdge(pixels, x, key) {
4546
5406
  const a = pixels[i];
4547
5407
  const b = pixels[i + 1];
4548
5408
  if (x >= a.x && x <= b.x) {
4549
- const t10 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
4550
- return a[key] + t10 * (b[key] - a[key]);
5409
+ const t12 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
5410
+ return a[key] + t12 * (b[key] - a[key]);
4551
5411
  }
4552
5412
  }
4553
5413
  return pixels[pixels.length - 1][key];
4554
5414
  }
4555
- function buildColumns(pixels, step) {
5415
+ function buildColumns2(pixels, step) {
4556
5416
  if (pixels.length < 2) return [];
4557
5417
  const start = pixels[0].x;
4558
5418
  const end = pixels[pixels.length - 1].x;
@@ -4664,7 +5524,7 @@ function CapacityBandChart({
4664
5524
  }
4665
5525
  const { toX, toY } = scale;
4666
5526
  const bandPixels = toPixels(band, toX, toY);
4667
- const columns = buildColumns(bandPixels, COLUMN_STEP);
5527
+ const columns = buildColumns2(bandPixels, COLUMN_STEP);
4668
5528
  const topEdge = buildEdges(bandPixels, "yHigh");
4669
5529
  const bottomEdge = buildEdges(bandPixels, "yLow");
4670
5530
  const lastBandPoint = band[band.length - 1];
@@ -4739,8 +5599,8 @@ function CapacityBandChart({
4739
5599
  testID: "capacity-band-chart-reveal",
4740
5600
  children: /* @__PURE__ */ jsxs(reactNative.View, { style: { width, height, position: "relative" }, children: [
4741
5601
  projection && /* @__PURE__ */ jsxs(reactNative.View, { testID: "capacity-band-chart-projection", children: [
4742
- renderColumns(buildColumns(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
4743
- renderColumns(buildColumns(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
5602
+ renderColumns(buildColumns2(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
5603
+ renderColumns(buildColumns2(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
4744
5604
  renderEdges(buildEdges(trainingPixels, "yHigh"), STATUS_SUCCESS2, true, "capacity-band-chart-projection-training"),
4745
5605
  renderEdges(buildEdges(trainingPixels, "yLow"), STATUS_SUCCESS2, true, "capacity-band-chart-projection-training"),
4746
5606
  renderEdges(buildEdges(restPixels, "yHigh"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
@@ -4901,10 +5761,9 @@ function deriveExerciseStats(entries) {
4901
5761
  function toExerciseCardProps(entry, expanded, onToggle) {
4902
5762
  return {
4903
5763
  name: entry.title,
4904
- state: expanded ? "expanded" : "collapsed",
4905
- onToggle,
5764
+ expanded,
5765
+ onExpandedChange: onToggle,
4906
5766
  summary: entry.summary,
4907
- e1rm: entry.e1rm,
4908
5767
  isPR: entry.isPR,
4909
5768
  tempo: entry.tempo,
4910
5769
  sets: expanded ? entry.sets : void 0
@@ -5448,6 +6307,7 @@ var colorStyles = {
5448
6307
  default: "bg-text-tertiary",
5449
6308
  primary: "bg-brand-primary",
5450
6309
  success: "bg-status-success",
6310
+ live: "bg-status-live",
5451
6311
  error: "bg-status-error",
5452
6312
  warning: "bg-status-warning",
5453
6313
  info: "bg-status-info",
@@ -5504,7 +6364,7 @@ function Indicator({
5504
6364
  colorClass,
5505
6365
  pulseMode === "opacity" && "animate-pulse",
5506
6366
  ring && "border-2 border-background-base",
5507
- glow && !customColor && color === "success" && "shadow-[0_0_6px_rgba(46,213,115,0.6)]",
6367
+ glow && !customColor && (color === "success" || color === "live") && "shadow-[0_0_6px_rgba(46,213,115,0.6)]",
5508
6368
  glow && !customColor && color === "error" && "shadow-[0_0_6px_rgba(239,68,68,0.6)]",
5509
6369
  glow && !customColor && color === "warning" && "shadow-[0_0_6px_rgba(245,158,11,0.6)]",
5510
6370
  glow && !customColor && color === "primary" && "shadow-[0_0_6px_rgba(255,121,0,0.6)]",
@@ -5586,7 +6446,7 @@ function Badge({
5586
6446
  }
5587
6447
  );
5588
6448
  }
5589
- var t6 = getSemanticColors("dark");
6449
+ var t8 = getSemanticColors("dark");
5590
6450
  var statusBgColors = {
5591
6451
  completed: "rgba(46,213,115,0.15)",
5592
6452
  current: "rgba(255,121,0,0.15)",
@@ -5604,14 +6464,14 @@ var statusBorderStyles = {
5604
6464
  deload: { borderWidth: 1, borderColor: "rgba(186,41,150,0.3)" }
5605
6465
  };
5606
6466
  var textColors = {
5607
- completed: t6["status-success"],
5608
- current: t6["brand-primary"],
5609
- next: t6["brand-primary"],
6467
+ completed: t8["status-success"],
6468
+ current: t8["brand-primary"],
6469
+ next: t8["brand-primary"],
5610
6470
  upcoming: "#6B7280",
5611
6471
  missed: "rgba(209,67,67,0.7)",
5612
6472
  deload: WORKOUT_PILL_DELOAD
5613
6473
  };
5614
- function usePulse(enabled) {
6474
+ function usePulse2(enabled) {
5615
6475
  const [opacity] = react.useState(() => new reactNative.Animated.Value(1));
5616
6476
  react.useEffect(() => {
5617
6477
  if (!enabled) {
@@ -5649,7 +6509,7 @@ function WorkoutPill({
5649
6509
  ...props
5650
6510
  }) {
5651
6511
  const shouldPulse = pulse ?? status === "current";
5652
- const pulseOpacity = usePulse(shouldPulse);
6512
+ const pulseOpacity = usePulse2(shouldPulse);
5653
6513
  const isCompleted = status === "completed";
5654
6514
  const isMissed = status === "missed";
5655
6515
  const pill = /* @__PURE__ */ jsx(
@@ -5739,19 +6599,19 @@ function WorkoutPill({
5739
6599
  }
5740
6600
  return pill;
5741
6601
  }
5742
- var t7 = getSemanticColors("dark");
6602
+ var t9 = getSemanticColors("dark");
5743
6603
  var TRACK_BG = "#333333";
5744
6604
  var LABEL_COLOR = "#6B7280";
5745
6605
  var TARGET_LINE_COLOR = "rgba(33, 150, 243, 0.5)";
5746
6606
  var AT_TARGET_GLOW = "0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)";
5747
6607
  var ZONE = {
5748
- building: t7["status-success"],
6608
+ building: t9["status-success"],
5749
6609
  // below target ramp-up
5750
- approaching: t7["status-warning"],
6610
+ approaching: t9["status-warning"],
5751
6611
  // nearing target
5752
- target: t7["brand-primary"],
6612
+ target: t9["brand-primary"],
5753
6613
  // exactly at target
5754
- over1: t7["status-error"],
6614
+ over1: t9["status-error"],
5755
6615
  // mild over-target
5756
6616
  over2: WORKOUT_TOKENS.intensity.over2,
5757
6617
  // moderate over-target
@@ -6174,13 +7034,13 @@ function MesoCard({
6174
7034
  }
6175
7035
  ) });
6176
7036
  }
6177
- var t8 = getSemanticColors("dark");
7037
+ var t10 = getSemanticColors("dark");
6178
7038
  var dotColorMap = {
6179
7039
  untrained: "#2C2C2C",
6180
- behind: t8["brand-secondary"],
6181
- ontrack: t8["status-success"],
6182
- target: t8["brand-primary"],
6183
- over: t8["status-error"]
7040
+ behind: t10["brand-secondary"],
7041
+ ontrack: t10["status-success"],
7042
+ target: t10["brand-primary"],
7043
+ over: t10["status-error"]
6184
7044
  };
6185
7045
  function MuscleGroupChip({
6186
7046
  name,
@@ -6254,10 +7114,10 @@ function MuscleGroupChip({
6254
7114
  }
6255
7115
 
6256
7116
  // src/components/custom/Workout/WorkoutCard.tsx
6257
- var t9 = getSemanticColors("dark");
7117
+ var t11 = getSemanticColors("dark");
6258
7118
  var COLORS = {
6259
- statusSuccess: t9["status-success"],
6260
- brandPrimary: t9["brand-primary"],
7119
+ statusSuccess: t11["status-success"],
7120
+ brandPrimary: t11["brand-primary"],
6261
7121
  borderDefault: "#1F1F1F",
6262
7122
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
6263
7123
  };
@@ -6541,8 +7401,8 @@ function WeekLevel({ week, onSelectWorkout }) {
6541
7401
  function WorkoutLevel({ workout, expandedExercise, onToggleExercise }) {
6542
7402
  const exercises = workout.exercises.map((exercise, index) => ({
6543
7403
  ...exercise,
6544
- state: exercise.state === "upcoming" ? exercise.state : index === expandedExercise ? "expanded" : "collapsed",
6545
- onToggle: () => onToggleExercise(index)
7404
+ expanded: index === expandedExercise,
7405
+ onExpandedChange: () => onToggleExercise(index)
6546
7406
  }));
6547
7407
  return /* @__PURE__ */ jsx(reactNative.View, { testID: "program-planning-page-workout-level", children: /* @__PURE__ */ jsx(
6548
7408
  WorkoutCard,
@@ -6685,7 +7545,7 @@ exports.findMeso = findMeso;
6685
7545
  exports.findWeek = findWeek;
6686
7546
  exports.findWorkout = findWorkout;
6687
7547
  exports.groupExercises = groupExercises;
6688
- exports.statusToCardState = statusToCardState;
7548
+ exports.isUpcomingExercise = isUpcomingExercise;
6689
7549
  exports.summarizeVbt = summarizeVbt;
6690
7550
  exports.toActiveCardProps = toActiveCardProps;
6691
7551
  exports.toExerciseCardProps = toExerciseCardProps;