@titan-design/react-ui 0.10.0 → 0.12.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 (190) hide show
  1. package/dist/bodymap.js +173 -343
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +173 -343
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +416 -18
  6. package/dist/index.d.ts +416 -18
  7. package/dist/index.js +2512 -1307
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2480 -1299
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1644 -1575
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1645 -1576
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +409 -415
  20. package/dist/theme/index.d.ts +409 -415
  21. package/dist/theme/index.js +234 -408
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +227 -399
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +141 -140
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +141 -140
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/arch/Architecture.stories.tsx +9 -9
  31. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  32. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  33. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  34. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  35. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  36. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  37. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  38. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  39. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  40. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  41. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  42. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  43. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  45. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  46. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  47. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  48. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  49. package/src/components/custom/Fatigue/README.md +93 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  51. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  52. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  53. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  54. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  55. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  56. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  57. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  58. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  59. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  60. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  61. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  62. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  63. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  64. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  65. package/src/components/custom/Fatigue/index.ts +41 -0
  66. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  67. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  68. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  69. package/src/components/custom/Table/Table.tsx +2 -2
  70. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  71. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  72. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  73. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  74. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  75. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  76. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  77. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  78. package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
  79. package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
  80. package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
  81. package/src/components/custom/Workout/InputBar.tsx +2 -2
  82. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  83. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  84. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  85. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  86. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  87. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  88. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  89. package/src/components/custom/Workout/README.md +62 -16
  90. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  91. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  92. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  93. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  94. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  95. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  96. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  97. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  98. package/src/components/custom/Workout/SessionRail.tsx +63 -18
  99. package/src/components/custom/Workout/SetBar.tsx +3 -3
  100. package/src/components/custom/Workout/SetRow.test.tsx +12 -9
  101. package/src/components/custom/Workout/SetRow.tsx +9 -2
  102. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  103. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  104. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  105. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  106. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  107. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  108. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  109. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  110. package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
  111. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  112. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  113. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  114. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  115. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  116. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  117. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  118. package/src/components/custom/Workout/index.ts +3 -0
  119. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  120. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  121. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  122. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  123. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  124. package/src/components/custom/index.ts +1 -0
  125. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  126. package/src/components/custom/stepper/Stepper.tsx +3 -3
  127. package/src/components/shell/DeviceMenu.tsx +2 -2
  128. package/src/components/shell/SideNav.tsx +1 -1
  129. package/src/components/shell/TopBar.tsx +1 -1
  130. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  131. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  132. package/src/components/ui/avatar/Avatar.tsx +2 -2
  133. package/src/components/ui/badge/Badge.tsx +2 -2
  134. package/src/components/ui/card/Card.tsx +3 -3
  135. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  136. package/src/components/ui/collapse/Collapse.tsx +1 -1
  137. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  138. package/src/components/ui/drawer/Drawer.tsx +3 -3
  139. package/src/components/ui/form-field/FormField.tsx +1 -1
  140. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  141. package/src/components/ui/menu/Menu.tsx +2 -2
  142. package/src/components/ui/pill/Pill.tsx +2 -2
  143. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  144. package/src/components/ui/popover/Popover.tsx +1 -1
  145. package/src/components/ui/progress/Progress.tsx +3 -3
  146. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  147. package/src/components/ui/radio/Radio.tsx +4 -4
  148. package/src/components/ui/select/Select.tsx +3 -3
  149. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  150. package/src/components/ui/surface/Surface.test.tsx +32 -19
  151. package/src/components/ui/surface/Surface.tsx +3 -3
  152. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  153. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  154. package/src/components/ui/switch/Switch.tsx +1 -1
  155. package/src/components/ui/tabs/Tabs.tsx +2 -2
  156. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  157. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  158. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  159. package/src/stories/PresetShowcase.stories.tsx +21 -21
  160. package/src/test/raw-color-patterns.test.ts +71 -0
  161. package/src/theme/ColorPalettes.stories.tsx +874 -0
  162. package/src/theme/ColorPrimitives.stories.tsx +348 -0
  163. package/src/theme/Depth.stories.tsx +312 -0
  164. package/src/theme/DepthCalibration.stories.tsx +655 -0
  165. package/src/theme/ThemeProvider.tsx +1 -1
  166. package/src/theme/barrel.ts +7 -14
  167. package/src/theme/color-stories.coverage.test.ts +125 -0
  168. package/src/theme/color-story-kit.tsx +170 -0
  169. package/src/theme/color-utils.ts +197 -0
  170. package/src/theme/config.ts +0 -8
  171. package/src/theme/depth-calibration.test.tsx +97 -0
  172. package/src/theme/elevation.test.ts +62 -1
  173. package/src/theme/elevation.ts +34 -8
  174. package/src/theme/global.css +47 -53
  175. package/src/theme/grey-ramp.test.ts +166 -0
  176. package/src/theme/materials.test.ts +106 -0
  177. package/src/theme/materials.ts +211 -0
  178. package/src/theme/presets/audiobook.ts +4 -4
  179. package/src/theme/presets/types.ts +3 -3
  180. package/src/theme/semantic-contrast.test.ts +83 -0
  181. package/src/theme/tokens/primitives.ts +62 -115
  182. package/src/theme/tokens/semantic.ts +130 -82
  183. package/src/utils/colors.ts +1 -1
  184. package/tailwind.config.js +2 -5
  185. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  186. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  187. package/src/theme/ColorSystem.stories.tsx +0 -508
  188. package/src/theme/Colors.stories.tsx +0 -323
  189. package/src/theme/shadows.stories.tsx +0 -523
  190. package/src/theme/shadows.ts +0 -439
package/dist/pages.js CHANGED
@@ -10,54 +10,37 @@ var tailwindMerge = require('tailwind-merge');
10
10
 
11
11
  // src/theme/tokens/primitives.ts
12
12
  var primitiveColors = {
13
- // Blue scale
14
- blue: {
15
- 400: "#60A5FA",
16
- 600: "#5048E5",
17
- 700: "#3832A0"},
18
- // Red scale
19
- red: {
20
- 500: "#EF4444",
21
- 600: "#D14343"},
22
- // Vivid red scale
23
- redVivid: {
24
- 400: "#FF6070",
25
- 500: "#FF4757",
26
- 700: "#C42539"},
27
- // Neutral/Gray scale
28
- neutral: {
29
- 50: "#FAFAFA",
30
- 100: "#F3F4F6",
31
- 300: "#D1D5DB",
32
- 400: "#9CA3AF",
33
- 500: "#6B7280",
34
- 600: "#4B5563",
35
- 900: "#111827"},
36
- // Charcoal scale (dark backgrounds)
37
- charcoal: {
38
- 0: "#6E6E6E",
39
- 100: "#4C4C4C",
40
- 200: "#3C3C3C",
41
- 300: "#2C2C2C",
42
- 400: "#1F1F1F",
43
- 500: "#1C1C1C"},
44
- // Pure colors
45
13
  white: "#FFFFFF"};
46
- var surfaceRampDark = {
47
- inset: "#13100D",
48
- // L*4.5 — sub-shell well / pressed
49
- background: "#1C1916",
50
- // L*9 — shell / frame
51
- base: "#252321",
52
- // L*13.5 — main content plane
53
- elevated: "#2C2A28",
54
- // L*17 — nav / rail
55
- raised: "#31302F",
56
- // L*20 — cards
57
- overlay: "#373635"
58
- // L*22.5 — hero / popover
14
+ var greyRamp = {
15
+ 50: "#F9F6F3",
16
+ // L*97.0 W6
17
+ 100: "#EDEAE7",
18
+ // L*92.8 W6
19
+ 200: "#D4D1CE",
20
+ // L*75.7 W6
21
+ 400: "#A29F9D",
22
+ // L*65.7 W5
23
+ 500: "#888684",
24
+ // L*56.0 W4
25
+ 600: "#72716F",
26
+ // L*47.7 W3
27
+ 700: "#5A5958",
28
+ // L*37.9 W2
29
+ 800: "#424140",
30
+ // L*27.6 W2 — generated bridge
31
+ 850: "#373635",
32
+ // L*22.7 W2 — surface overlay
33
+ 875: "#31302F",
34
+ // L*19.9 W2 — surface raised
35
+ 900: "#2C2A28",
36
+ // L*17.2 W4 — surface elevated
37
+ 925: "#252321",
38
+ // L*13.9 W4 — surface base
39
+ 950: "#1C1916",
40
+ // L*9.0 W6 — background base / shell
41
+ 975: "#100D0A"
42
+ // L*3.8 W6 — background frame / bezel
59
43
  };
60
- var backgroundFrameDark = "#100D0A";
61
44
  var primitiveRamps = {
62
45
  red: {
63
46
  50: "#FFF4F4",
@@ -100,9 +83,11 @@ var primitiveRamps = {
100
83
  blue: {
101
84
  50: "#EFF8FF",
102
85
  300: "#78C2FF",
86
+ 400: "#3CA8FF",
103
87
  500: "#2196F3",
104
88
  // pin
105
- 600: "#1072CB"},
89
+ 600: "#1072CB",
90
+ 700: "#135AA8"},
106
91
  magenta: {
107
92
  400: "#ED69C3",
108
93
  600: "#BA2996"}
@@ -182,275 +167,916 @@ var WORKOUT_TOKENS = {
182
167
  fill: "#3A3A3A"
183
168
  }};
184
169
 
185
- // src/utils/workout-format.ts
186
- function roundRpe(rpe) {
187
- return Math.round(rpe * 2) / 2;
188
- }
189
- function roundWeight(weight) {
190
- return Math.round(weight);
191
- }
192
- function formatVelocity(velocity) {
193
- return velocity.toFixed(2);
194
- }
195
- function roundTempo(tempo) {
196
- return tempo.map((v) => Math.round(v * 10) / 10);
197
- }
198
- function wrapJsx(fn) {
199
- return function(type, props, ...rest) {
200
- if (props && typeof props.className === "string" && props.className) {
201
- const cn2 = props.className;
202
- const cssStyle = { $$css: true, [cn2]: cn2 };
203
- const existing = props.style;
204
- props = {
205
- ...props,
206
- style: existing ? [cssStyle, existing] : cssStyle
207
- };
208
- delete props.className;
209
- }
210
- return fn(type, props, ...rest);
211
- };
170
+ // src/theme/tokens/semantic.ts
171
+ var semanticColorsLight = {
172
+ // Brand colors (brand-*)
173
+ "brand-primary": primitiveRamps.orange[400],
174
+ "brand-primary-light": primitiveRamps.orange[300],
175
+ "brand-primary-dark": primitiveRamps.orange[500],
176
+ "brand-primary-subtle": "rgba(255, 121, 0, 0.08)",
177
+ "brand-primary-hover": primitiveRamps.orange[500],
178
+ "brand-primary-active": primitiveRamps.orange[600],
179
+ "brand-secondary": primitiveRamps.cyan[600],
180
+ "brand-secondary-light": primitiveRamps.cyan[500],
181
+ "brand-secondary-dark": primitiveRamps.cyan[700],
182
+ "brand-secondary-subtle": "rgba(48, 123, 155, 0.08)",
183
+ "brand-secondary-hover": primitiveRamps.cyan[700],
184
+ "brand-secondary-active": primitiveRamps.cyan[800],
185
+ // Text on brand backgrounds (on-*)
186
+ "on-brand-primary": primitiveColors.white,
187
+ "on-brand-secondary": primitiveColors.white,
188
+ // Status colors (status-*)
189
+ "status-success": primitiveRamps.green[300],
190
+ "status-success-light": primitiveRamps.green[200],
191
+ "status-success-dark": primitiveRamps.green[600],
192
+ "status-success-subtle": primitiveRamps.green[50],
193
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
194
+ "status-live": primitiveRamps.green[300],
195
+ "status-live-muted": primitiveRamps.green[500],
196
+ "status-error": primitiveRamps.red[600],
197
+ "status-error-light": primitiveRamps.red[500],
198
+ "status-error-dark": primitiveRamps.red[700],
199
+ "status-error-subtle": primitiveRamps.red[50],
200
+ "status-error-vivid": primitiveRamps.red[600],
201
+ // vivid alert red
202
+ "status-error-vivid-light": primitiveRamps.red[500],
203
+ "status-error-vivid-dark": primitiveRamps.red[700],
204
+ "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
205
+ "status-warning": primitiveRamps.amber[300],
206
+ "status-warning-light": primitiveRamps.amber[200],
207
+ "status-warning-dark": primitiveRamps.amber[500],
208
+ "status-warning-subtle": primitiveRamps.amber[50],
209
+ "status-info": primitiveRamps.blue[500],
210
+ "status-info-light": primitiveRamps.blue[300],
211
+ "status-info-dark": primitiveRamps.blue[600],
212
+ "status-info-subtle": primitiveRamps.blue[50],
213
+ // Text on status backgrounds (on-status-*)
214
+ "on-status-success": primitiveColors.white,
215
+ "on-status-error": primitiveColors.white,
216
+ "on-status-warning": primitiveColors.white,
217
+ "on-status-info": primitiveColors.white,
218
+ // Result/outcome indicators (result-*)
219
+ "result-improve": "#4caf50",
220
+ // Green - positive outcome
221
+ "result-improve-light": "rgba(76, 175, 80, 0.12)",
222
+ "result-improve-dark": "#248a24",
223
+ "result-degrade": "#ef5350",
224
+ // Red - negative outcome
225
+ "result-degrade-light": "rgba(239, 83, 80, 0.12)",
226
+ "result-degrade-dark": "#b30000",
227
+ "result-inconclusive": "#9E9A97",
228
+ // Gray - no clear result
229
+ "result-inconclusive-light": "rgba(158, 154, 151, 0.12)",
230
+ "result-neutral": greyRamp[600],
231
+ // Neutral baseline
232
+ // Text on result backgrounds (on-result-*)
233
+ "on-result-improve": primitiveColors.white,
234
+ "on-result-degrade": primitiveColors.white,
235
+ "on-result-inconclusive": primitiveColors.white,
236
+ // Data visualization colors (data-*)
237
+ // First 10 colors from discrete rainbow optimized for charts
238
+ "data-1": discreteRainbow[9],
239
+ // Blue
240
+ "data-2": discreteRainbow[14],
241
+ // Green
242
+ "data-3": discreteRainbow[17],
243
+ // Yellow
244
+ "data-4": discreteRainbow[25],
245
+ // Red
246
+ "data-5": discreteRainbow[8],
247
+ // Purple
248
+ "data-6": discreteRainbow[20],
249
+ // Orange
250
+ "data-7": discreteRainbow[13],
251
+ // Light Blue
252
+ "data-8": discreteRainbow[15],
253
+ // Light Green
254
+ "data-9": discreteRainbow[3],
255
+ // Lavender
256
+ "data-10": discreteRainbow[22],
257
+ // Dark Orange
258
+ // Text colors (text-*)
259
+ "text-primary": "#121828",
260
+ "text-secondary": "#65748B",
261
+ "text-tertiary": greyRamp[400],
262
+ "text-disabled": "rgba(55, 65, 81, 0.48)",
263
+ "text-inverse": primitiveColors.white,
264
+ "text-link": primitiveRamps.blue[600],
265
+ "text-link-hover": primitiveRamps.blue[700],
266
+ // Surface colors (surface-*) - for elevated containers
267
+ "surface-base": primitiveColors.white,
268
+ "surface-elevated": greyRamp[50],
269
+ // slightly off-white for elevated cards
270
+ "surface-raised": greyRamp[100],
271
+ // light gray for raised cards
272
+ "surface-overlay": primitiveColors.white,
273
+ "surface-input": greyRamp[50],
274
+ // Input field background (filled variant)
275
+ // Background colors (background-*)
276
+ "background-base": "#EBEBEB",
277
+ "background-default": primitiveColors.white,
278
+ "background-subtle": greyRamp[50],
279
+ // Frame/bezel chrome — top bar + side nav shell, one step below
280
+ // `background-base`. Placeholder pairing for light mode, which is deferred.
281
+ "background-frame": greyRamp[400],
282
+ // Border colors (border-*)
283
+ "border-prominent": greyRamp[400],
284
+ // high-visibility divider
285
+ "border-focus": primitiveRamps.blue[600],
286
+ "border-input": greyRamp[200],
287
+ // Input field border
288
+ "border-input-hover": greyRamp[400],
289
+ // Input field border on hover
290
+ "border-input-focus": primitiveRamps.blue[600],
291
+ // Input field border on focus
292
+ "border-input-error": primitiveRamps.red[600],
293
+ // Input field border on error
294
+ // Alpha hairline separators (surface-independent — composite toward black on
295
+ // light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
296
+ "hairline-subtle": "rgba(0, 0, 0, 0.06)",
297
+ "hairline-default": "rgba(0, 0, 0, 0.09)",
298
+ "hairline-strong": "rgba(0, 0, 0, 0.14)",
299
+ // Interactive states (interactive-*)
300
+ "interactive-hover": "rgba(55, 65, 81, 0.04)",
301
+ "interactive-focus": "rgba(55, 65, 81, 0.12)",
302
+ "interactive-active": "rgba(55, 65, 81, 0.16)",
303
+ "interactive-selected": "rgba(55, 65, 81, 0.08)",
304
+ "interactive-disabled": "rgba(55, 65, 81, 0.12)",
305
+ "interactive-disabled-text": "rgba(55, 65, 81, 0.26)",
306
+ // Divider
307
+ "divider": "#E8E9EB",
308
+ // Avatar default
309
+ "avatar-background": greyRamp[600],
310
+ "avatar-text": primitiveColors.white
311
+ };
312
+ var semanticColorsDark = {
313
+ // Brand colors stay the same in dark mode
314
+ "brand-primary": primitiveRamps.orange[400],
315
+ "brand-primary-light": primitiveRamps.orange[300],
316
+ "brand-primary-dark": primitiveRamps.orange[500],
317
+ "brand-primary-subtle": "rgba(255, 121, 0, 0.12)",
318
+ "brand-primary-hover": primitiveRamps.orange[300],
319
+ "brand-primary-active": primitiveRamps.orange[200],
320
+ "brand-secondary": primitiveRamps.cyan[600],
321
+ "brand-secondary-light": primitiveRamps.cyan[500],
322
+ "brand-secondary-dark": primitiveRamps.cyan[700],
323
+ "brand-secondary-subtle": "rgba(48, 123, 155, 0.12)",
324
+ "brand-secondary-hover": primitiveRamps.cyan[500],
325
+ "brand-secondary-active": primitiveRamps.cyan[400],
326
+ // Text on brand backgrounds
327
+ "on-brand-primary": primitiveColors.white,
328
+ "on-brand-secondary": primitiveColors.white,
329
+ // Status colors
330
+ "status-success": primitiveRamps.green[300],
331
+ "status-success-light": primitiveRamps.green[200],
332
+ "status-success-dark": primitiveRamps.green[600],
333
+ "status-success-subtle": "rgba(46, 213, 115, 0.12)",
334
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
335
+ "status-live": primitiveRamps.green[300],
336
+ "status-live-muted": primitiveRamps.green[500],
337
+ "status-error": primitiveRamps.red[600],
338
+ "status-error-light": primitiveRamps.red[500],
339
+ "status-error-dark": primitiveRamps.red[700],
340
+ "status-error-subtle": "rgba(209, 67, 67, 0.12)",
341
+ "status-error-vivid": primitiveRamps.red[600],
342
+ "status-error-vivid-light": primitiveRamps.red[500],
343
+ "status-error-vivid-dark": primitiveRamps.red[700],
344
+ "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
345
+ "status-warning": primitiveRamps.amber[300],
346
+ "status-warning-light": primitiveRamps.amber[200],
347
+ "status-warning-dark": primitiveRamps.amber[500],
348
+ "status-warning-subtle": "rgba(249, 180, 21, 0.12)",
349
+ "status-info": primitiveRamps.blue[500],
350
+ "status-info-light": primitiveRamps.blue[300],
351
+ "status-info-dark": primitiveRamps.blue[600],
352
+ "status-info-subtle": "rgba(33, 150, 243, 0.12)",
353
+ // Text on status backgrounds
354
+ "on-status-success": primitiveColors.white,
355
+ "on-status-error": primitiveColors.white,
356
+ "on-status-warning": primitiveColors.white,
357
+ "on-status-info": primitiveColors.white,
358
+ // Result/outcome indicators (result-*)
359
+ "result-improve": "#4caf50",
360
+ "result-improve-light": "rgba(76, 175, 80, 0.16)",
361
+ "result-improve-dark": "#248a24",
362
+ "result-degrade": "#ef5350",
363
+ "result-degrade-light": "rgba(239, 83, 80, 0.16)",
364
+ "result-degrade-dark": "#b30000",
365
+ "result-inconclusive": "#9E9A97",
366
+ "result-inconclusive-light": "rgba(158, 154, 151, 0.16)",
367
+ "result-neutral": greyRamp[400],
368
+ // Text on result backgrounds
369
+ "on-result-improve": primitiveColors.white,
370
+ "on-result-degrade": primitiveColors.white,
371
+ "on-result-inconclusive": primitiveColors.white,
372
+ // Data visualization colors (same in dark mode for consistency)
373
+ "data-1": discreteRainbow[9],
374
+ "data-2": discreteRainbow[14],
375
+ "data-3": discreteRainbow[17],
376
+ "data-4": discreteRainbow[25],
377
+ "data-5": discreteRainbow[8],
378
+ "data-6": discreteRainbow[20],
379
+ "data-7": discreteRainbow[13],
380
+ "data-8": discreteRainbow[15],
381
+ "data-9": discreteRainbow[3],
382
+ "data-10": discreteRainbow[22],
383
+ // Text colors - inverted for dark mode
384
+ "text-primary": greyRamp[50],
385
+ "text-secondary": greyRamp[400],
386
+ // Deliberately ONE step lighter than the L*-nearest match (`greyRamp[600]`).
387
+ // The colour this replaced failed WCAG large-text outright on the three
388
+ // lightest planes — 2.96 / 2.72 / 2.49 against elevated / raised / overlay —
389
+ // and `600` carried that failure forward almost exactly (2.93 / 2.70 / 2.47).
390
+ // `500` clears 3:1 everywhere (3.32–5.34). Contrast beats colour fidelity for
391
+ // text roles; borders and surfaces still use strict L*-nearest. See
392
+ // semantic-contrast.test.ts, which fails if this is moved back down.
393
+ "text-tertiary": greyRamp[500],
394
+ "text-disabled": "rgba(255, 255, 255, 0.38)",
395
+ "text-inverse": greyRamp[950],
396
+ "text-link": "#828DF8",
397
+ "text-link-hover": primitiveRamps.blue[400],
398
+ // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
399
+ // S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
400
+ // `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
401
+ // and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
402
+ // tracks one plane above `surface-base`, same relative position as before the remap.
403
+ // `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
404
+ // so it takes the ramp's shell role. The deepest plane is `background-frame`,
405
+ // which is also the `SurfaceLevel` floor a pressed surface clamps at.
406
+ "surface-base": greyRamp[925],
407
+ // main surface (#252321, L*13.9)
408
+ "surface-elevated": greyRamp[900],
409
+ // elevated surface (#2C2A28, L*17.2 — nav/rail)
410
+ "surface-raised": greyRamp[875],
411
+ // raised surface (#31302F, L*19.9 — cards)
412
+ "surface-overlay": greyRamp[850],
413
+ // overlay surface (#373635, L*22.7 — hero/popover)
414
+ "surface-input": greyRamp[900],
415
+ // input surface (#2C2A28 — one plane above base)
416
+ // Background colors — same ramp, the frame/shell end of it.
417
+ "background-base": greyRamp[950],
418
+ // shell (#1C1916, L*9 — Surface level="background")
419
+ "background-default": greyRamp[925],
420
+ // main background (#252321, L*13.9 — matches surface-base)
421
+ "background-subtle": greyRamp[900],
422
+ // subtle background (#2C2A28, L*17.2 — matches surface-elevated)
423
+ // Frame/bezel chrome — the top bar + side nav shell, one step BELOW
424
+ // `background-base`. It used to be described as sitting OUTSIDE the ramp; it
425
+ // is simply the ramp's last step now, and the floor `<Surface pressed>` clamps at.
426
+ "background-frame": greyRamp[975],
427
+ // frame / bezel (#100D0A, L*3.8)
428
+ // Border colors — solid dark borders are RETIRED (TD-07.14). Not a preference — a structural
429
+ // consequence of one ramp. Every step from 850 to 975 is now a surface plane,
430
+ // so a solid border dark enough to read as a border necessarily equals some
431
+ // plane's fill, which is exactly the collision `surface.contract.test.ts` R2
432
+ // forbids. The only non-plane steps left in that band are 800 and 700, and
433
+ // borders there jump from L*~10-25 to L*~28-38 — a restyle, not a migration.
434
+ //
435
+ // So separation moves to the alpha hairlines, which dark mode already called
436
+ // "the primary separation cue". Being alpha, they composite against whatever
437
+ // plane they land on and cannot collide with any of them — the problem stops
438
+ // existing rather than being re-solved per plane. The three solid tokens were
439
+ // deleted outright; consumers point at the `hairline-*` family below.
440
+ //
441
+ // `border-prominent` stays SOLID: it is the one border meant to be seen
442
+ // outright (4 call sites, high-visibility dividers), and grey-800 is not a
443
+ // plane, so it keeps its job without collision.
444
+ "border-prominent": greyRamp[800],
445
+ // high-visibility divider
446
+ "border-focus": "#828DF8",
447
+ "border-input": greyRamp[700],
448
+ "border-input-hover": greyRamp[600],
449
+ "border-input-focus": "#828DF8",
450
+ "border-input-error": primitiveRamps.red[500],
451
+ // Alpha hairline separators — the primary separation cue (§4/S-2). Self-
452
+ // normalizing: composites toward white by ~the same amount on ANY plane, so
453
+ // one family works at every elevation instead of per-surface border tokens.
454
+ // Shadows are demoted to floating-overlay use only (not shipped as a fill
455
+ // separator here — see elevation.ts, follow-up S-4).
456
+ //
457
+ // RETUNED on the wall (VW-99, S-6). The original .06/.09/.14 were set at a
458
+ // desk. On the panel at ~3 m `subtle` rendered but was indiscernible, and
459
+ // `default` — the load-bearing one, with no solid-border fallback since
460
+ // TD-07.14 — went weak on the lightest plane. `strong` read cleanly with room
461
+ // to spare, which is what made raising the whole family viable.
462
+ //
463
+ // Each tier steps up onto the next MEASURED-GOOD rung rather than an
464
+ // interpolated one: the new `subtle` is the old `default`, and the new
465
+ // `default` is the old `strong` — a value just confirmed legible on all four
466
+ // planes. Spacing goes 1 : 1.56 : 2.33, against the original 1 : 1.5 : 2.33.
467
+ //
468
+ // RUN 2 (VW-99): all three tiers were seen on all four planes — the retune
469
+ // worked — but `subtle` was still "a bit hard to read", so the whole family
470
+ // took a further +.01. That is a comfort margin on a passing row, not a fix
471
+ // for a failing one, which is why it is a flat offset: the 1 : 1.56 : 2.33
472
+ // spacing that the run validated is preserved rather than re-derived.
473
+ //
474
+ // Keep these to TWO decimals. react-native-web quantises alpha to 8 bits, so
475
+ // a 3-decimal value silently rounds on the way to the DOM (.135 renders as
476
+ // .13) — the token would say one thing and paint another.
477
+ //
478
+ // DARK ONLY. The light family above composites toward BLACK on light planes,
479
+ // which this run says nothing about — do not mirror these numbers into it
480
+ // without its own verification.
481
+ "hairline-subtle": "rgba(255, 255, 255, 0.10)",
482
+ "hairline-default": "rgba(255, 255, 255, 0.15)",
483
+ "hairline-strong": "rgba(255, 255, 255, 0.22)",
484
+ // Interactive states
485
+ "interactive-hover": "rgba(255, 255, 255, 0.04)",
486
+ "interactive-focus": "rgba(255, 255, 255, 0.12)",
487
+ "interactive-active": "rgba(255, 255, 255, 0.16)",
488
+ "interactive-selected": "rgba(255, 255, 255, 0.08)",
489
+ "interactive-disabled": "rgba(255, 255, 255, 0.12)",
490
+ "interactive-disabled-text": "rgba(255, 255, 255, 0.26)",
491
+ // Divider
492
+ "divider": "rgba(255, 255, 255, 0.09)",
493
+ // Avatar default
494
+ "avatar-background": greyRamp[700],
495
+ "avatar-text": primitiveColors.white
496
+ };
497
+ function getSemanticColors(mode) {
498
+ return mode === "dark" ? semanticColorsDark : semanticColorsLight;
212
499
  }
213
- var jsx = wrapJsx(jsxRuntime.jsx);
214
- var jsxs = wrapJsx(jsxRuntime.jsxs);
215
500
 
216
- // src/components/custom/Workout/SegmentedBar.tsx
217
- var PULSE_HALF_MS = 950;
218
- var PULSE_MIN_OPACITY = 0.45;
219
- var SEGMENTED_BAR_GAP = 5;
220
- function usePulse(active) {
221
- const [value] = react.useState(() => new reactNative.Animated.Value(0));
222
- react.useEffect(() => {
223
- if (!active) return;
224
- const loop = reactNative.Animated.loop(
225
- reactNative.Animated.sequence([
226
- reactNative.Animated.timing(value, {
227
- toValue: 1,
228
- duration: PULSE_HALF_MS,
229
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
230
- useNativeDriver: false
231
- }),
232
- reactNative.Animated.timing(value, {
233
- toValue: 0,
234
- duration: PULSE_HALF_MS,
235
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
236
- useNativeDriver: false
237
- })
238
- ])
239
- );
240
- loop.start();
241
- return () => loop.stop();
242
- }, [active, value]);
243
- return value;
244
- }
245
- function SegmentedBar({
246
- segments,
247
- height = 8,
248
- gap = SEGMENTED_BAR_GAP,
249
- radius = 2,
250
- marker = null,
251
- segmentTestID,
252
- style,
253
- ...props
254
- }) {
255
- const pulse = usePulse(segments.some((s) => s.pulse));
256
- return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
257
- segments.map((seg, i) => {
258
- const fillStyle = {
259
- width: `${(seg.fill ?? 1) * 100}%`,
260
- height: "100%",
261
- backgroundColor: seg.color,
262
- opacity: seg.opacity
263
- };
264
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
265
- return /* @__PURE__ */ jsx(
266
- reactNative.View,
267
- {
268
- style: {
269
- flex: seg.weight ?? 1,
270
- minWidth: 0,
271
- height: "100%",
272
- marginLeft: seg.leadingGap,
273
- borderRadius: radius,
274
- overflow: "hidden"
275
- },
276
- children: seg.pulse ? /* @__PURE__ */ jsx(
277
- reactNative.Animated.View,
278
- {
279
- style: {
280
- ...fillStyle,
281
- ...seg.pulseColor ? {
282
- backgroundColor: pulse.interpolate({
283
- inputRange: [0, 1],
284
- outputRange: seg.pulseColor
285
- })
286
- } : {
287
- opacity: pulse.interpolate({
288
- inputRange: [0, 1],
289
- outputRange: [PULSE_MIN_OPACITY, 1]
290
- })
291
- }
292
- },
293
- accessibilityElementsHidden: true,
294
- testID
295
- }
296
- ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
297
- },
298
- i
299
- );
300
- }),
301
- marker ? /* @__PURE__ */ jsx(
302
- reactNative.View,
303
- {
304
- style: {
305
- position: "absolute",
306
- left: `${marker.position * 100}%`,
307
- top: 0,
308
- bottom: 0,
309
- width: 2,
310
- backgroundColor: marker.color
311
- },
312
- accessibilityElementsHidden: true,
313
- testID: "segmented-bar-marker"
314
- }
315
- ) : null
316
- ] });
501
+ // src/theme/color-utils.ts
502
+ function hexToRgb(hex) {
503
+ const cleaned = hex.replace("#", "");
504
+ let r, g, b;
505
+ if (cleaned.length === 3) {
506
+ r = parseInt(cleaned[0] + cleaned[0], 16);
507
+ g = parseInt(cleaned[1] + cleaned[1], 16);
508
+ b = parseInt(cleaned[2] + cleaned[2], 16);
509
+ } else if (cleaned.length === 6) {
510
+ r = parseInt(cleaned.slice(0, 2), 16);
511
+ g = parseInt(cleaned.slice(2, 4), 16);
512
+ b = parseInt(cleaned.slice(4, 6), 16);
513
+ } else {
514
+ return null;
515
+ }
516
+ return { r, g, b };
317
517
  }
318
-
319
- // src/components/custom/Workout/SetBar.tsx
320
- var SET_STRIP_ZONES = {
321
- slow: primitiveRamps.red[600],
322
- moderate: primitiveRamps.orange[400],
323
- fast: primitiveRamps.amber[300],
324
- fastest: primitiveRamps.green[300]
325
- };
326
- var TODO_COLOR = primitiveColors.charcoal[300];
327
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
328
- var NOTCH_GAP = 2;
329
- var CLUSTER_GAP = 3;
330
- var MYO_UPCOMING_CLUSTERS = 3;
331
- var MYO_UPCOMING_CLUSTER_LEN = 5;
332
- var MYO_UPCOMING_FADE = 0.6;
333
- var PULSE_RANGE = {
334
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
335
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
336
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
337
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
338
- };
339
- function velocityZoneColor(v) {
340
- if (v < 0.5) return SET_STRIP_ZONES.slow;
341
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
342
- if (v < 1) return SET_STRIP_ZONES.fast;
343
- return SET_STRIP_ZONES.fastest;
518
+ function rgbToHsv(r, g, b) {
519
+ const rNorm = r / 255;
520
+ const gNorm = g / 255;
521
+ const bNorm = b / 255;
522
+ const max = Math.max(rNorm, gNorm, bNorm);
523
+ const min = Math.min(rNorm, gNorm, bNorm);
524
+ const delta = max - min;
525
+ const v = max;
526
+ const s = max === 0 ? 0 : delta / max;
527
+ let h = 0;
528
+ if (delta !== 0) {
529
+ if (max === rNorm) {
530
+ h = 60 * ((gNorm - bNorm) / delta % 6);
531
+ } else if (max === gNorm) {
532
+ h = 60 * ((bNorm - rNorm) / delta + 2);
533
+ } else {
534
+ h = 60 * ((rNorm - gNorm) / delta + 4);
535
+ }
536
+ }
537
+ if (h < 0) h += 360;
538
+ return { h, s, v };
344
539
  }
345
- function chunkedSegments(chunks, gap) {
346
- return chunks.flatMap(
347
- (vels, ci) => vels.map((v, ri) => ({
348
- color: velocityZoneColor(v),
349
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
350
- }))
351
- );
540
+ function hsvToRgb(h, s, v) {
541
+ const c2 = v * s;
542
+ const x = c2 * (1 - Math.abs(h / 60 % 2 - 1));
543
+ const m = v - c2;
544
+ let rPrime, gPrime, bPrime;
545
+ if (h >= 0 && h < 60) {
546
+ rPrime = c2;
547
+ gPrime = x;
548
+ bPrime = 0;
549
+ } else if (h >= 60 && h < 120) {
550
+ rPrime = x;
551
+ gPrime = c2;
552
+ bPrime = 0;
553
+ } else if (h >= 120 && h < 180) {
554
+ rPrime = 0;
555
+ gPrime = c2;
556
+ bPrime = x;
557
+ } else if (h >= 180 && h < 240) {
558
+ rPrime = 0;
559
+ gPrime = x;
560
+ bPrime = c2;
561
+ } else if (h >= 240 && h < 300) {
562
+ rPrime = x;
563
+ gPrime = 0;
564
+ bPrime = c2;
565
+ } else {
566
+ rPrime = c2;
567
+ gPrime = 0;
568
+ bPrime = x;
569
+ }
570
+ return {
571
+ r: Math.round((rPrime + m) * 255),
572
+ g: Math.round((gPrime + m) * 255),
573
+ b: Math.round((bPrime + m) * 255)
574
+ };
352
575
  }
353
- function rangeSegments(floor, max, doneVels) {
354
- return Array.from({ length: max }, (_, i) => {
355
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
356
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
357
- });
576
+ function rgbToHex(r, g, b) {
577
+ const toHex = (n) => {
578
+ const clamped = Math.max(0, Math.min(255, Math.round(n)));
579
+ return clamped.toString(16).padStart(2, "0");
580
+ };
581
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
358
582
  }
359
- function myoUpcomingSegments(activationLen, clusterCount) {
360
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
361
- const trail = Array.from(
362
- { length: clusterCount },
363
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
364
- color: SET_STRIP_VARIABLE_COLOR,
365
- opacity: MYO_UPCOMING_FADE ** ci,
366
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
367
- }))
368
- ).flat();
369
- return [...activation, ...trail];
583
+ function lighten(hex, amount = 0.1) {
584
+ const rgb = hexToRgb(hex);
585
+ if (!rgb) return hex;
586
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
587
+ const newV = Math.min(1, hsv.v + amount);
588
+ const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
589
+ return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
370
590
  }
371
- function setSegments(set) {
372
- if (set.status === "todo") {
373
- return [{ color: TODO_COLOR }];
374
- }
375
- if (set.status === "done") {
376
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
591
+
592
+ // src/utils/colors.ts
593
+ function alpha(color, opacity) {
594
+ const clampedOpacity = Math.max(0, Math.min(1, opacity));
595
+ if (color.startsWith("#")) {
596
+ const hex = color.slice(1);
597
+ let r, g, b;
598
+ if (hex.length === 3) {
599
+ r = parseInt(hex[0] + hex[0], 16);
600
+ g = parseInt(hex[1] + hex[1], 16);
601
+ b = parseInt(hex[2] + hex[2], 16);
602
+ } else if (hex.length === 6) {
603
+ r = parseInt(hex.slice(0, 2), 16);
604
+ g = parseInt(hex.slice(2, 4), 16);
605
+ b = parseInt(hex.slice(4, 6), 16);
606
+ } else {
607
+ return color;
608
+ }
609
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
377
610
  }
378
- if (set.status === "range") {
379
- return rangeSegments(set.floor, set.max, set.doneVels);
611
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
612
+ if (rgbMatch) {
613
+ const [, r, g, b] = rgbMatch;
614
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
380
615
  }
381
- if (set.status === "drop") {
382
- return chunkedSegments(set.subloads, NOTCH_GAP);
616
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
617
+ if (rgbaMatch) {
618
+ const [, r, g, b] = rgbaMatch;
619
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
383
620
  }
384
- if (set.status === "myo") {
385
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
386
- }
387
- if (set.status === "myo-upcoming") {
388
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
389
- }
390
- const performed = set.velocities.map((v) => {
391
- const color = velocityZoneColor(v);
392
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
393
- });
394
- const remaining = Math.max(0, set.planned - set.velocities.length);
395
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
621
+ return color;
396
622
  }
397
- function setBarSegmentTestID(seg) {
398
- if (seg.pulse) return "set-strip-pulse";
399
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
400
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
623
+
624
+ // src/utils/workout-format.ts
625
+ function roundRpe(rpe) {
626
+ return Math.round(rpe * 2) / 2;
401
627
  }
402
- function SetBar({ set, height = 8 }) {
403
- return /* @__PURE__ */ jsx(
404
- SegmentedBar,
405
- {
406
- segments: setSegments(set),
407
- height,
408
- gap: 0,
409
- radius: 0,
410
- segmentTestID: setBarSegmentTestID,
411
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
412
- testID: "set-strip-set"
628
+ function roundWeight(weight) {
629
+ return Math.round(weight);
630
+ }
631
+ function formatVelocity(velocity) {
632
+ return velocity.toFixed(2);
633
+ }
634
+ function roundTempo(tempo) {
635
+ return tempo.map((v) => Math.round(v * 10) / 10);
636
+ }
637
+ function parseHex(hex) {
638
+ const h = hex.replace("#", "");
639
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
640
+ const rgb = [
641
+ parseInt(full.slice(0, 2), 16),
642
+ parseInt(full.slice(2, 4), 16),
643
+ parseInt(full.slice(4, 6), 16)
644
+ ];
645
+ return rgb.some(Number.isNaN) ? null : rgb;
646
+ }
647
+ function perceivedLuminance(hex) {
648
+ const rgb = parseHex(hex);
649
+ if (!rgb) return 0.5;
650
+ const [r, g, b] = rgb;
651
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
652
+ }
653
+ function grainOpacityForTone(baseColor) {
654
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * perceivedLuminance(baseColor)));
655
+ }
656
+ function grainForTone(baseColor) {
657
+ const op = grainOpacityForTone(baseColor).toFixed(3);
658
+ return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='${op}'/%3E%3C/svg%3E")`;
659
+ }
660
+ function barPaper(color, flip = false) {
661
+ return {
662
+ backgroundImage: grainForTone(color),
663
+ boxShadow: flip ? "0 -6px 16px rgba(0,0,0,0.45)" : "inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)"
664
+ };
665
+ }
666
+ var SURFACE_LEVEL_TOKEN = {
667
+ frame: "background-frame",
668
+ background: "background-base",
669
+ base: "surface-base",
670
+ elevated: "surface-elevated",
671
+ raised: "surface-raised",
672
+ overlay: "surface-overlay"
673
+ };
674
+ var ON_SURFACE_TOKEN = {
675
+ primary: "text-primary",
676
+ secondary: "text-secondary",
677
+ tertiary: "text-tertiary"
678
+ };
679
+ var DEFAULT_CONTEXT = { mode: "dark", level: "base" };
680
+ var SurfaceContext = react.createContext(DEFAULT_CONTEXT);
681
+ function useSurface() {
682
+ return react.useContext(SurfaceContext);
683
+ }
684
+ function useSurfaceMode() {
685
+ return react.useContext(SurfaceContext).mode;
686
+ }
687
+ function useOnSurfaceColor(role = "primary") {
688
+ return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
689
+ }
690
+ function surfaceBackground(level, mode) {
691
+ return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
692
+ }
693
+ var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
694
+ var PEAK_OVERSHOOT = 1.12;
695
+ function getReducedMotionPreference() {
696
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
697
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
698
+ }
699
+ function usePrefersReducedMotion() {
700
+ const [reduced, setReduced] = react.useState(getReducedMotionPreference);
701
+ react.useEffect(() => {
702
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
703
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
704
+ const handler = () => setReduced(mq.matches);
705
+ handler();
706
+ mq.addEventListener?.("change", handler);
707
+ return () => mq.removeEventListener?.("change", handler);
708
+ }, []);
709
+ return reduced;
710
+ }
711
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
712
+ const prefersReducedMotion = usePrefersReducedMotion();
713
+ const [liveGrowth] = react.useState(() => new reactNative.Animated.Value(1));
714
+ react.useEffect(() => {
715
+ if (liveRepIndex == null || liveVelocity == null) return;
716
+ if (prefersReducedMotion) {
717
+ liveGrowth.setValue(1);
718
+ return;
413
719
  }
414
- );
720
+ liveGrowth.setValue(0);
721
+ if (isNewPeak) {
722
+ reactNative.Animated.sequence([
723
+ reactNative.Animated.timing(liveGrowth, {
724
+ toValue: PEAK_OVERSHOOT,
725
+ duration: 220,
726
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
727
+ useNativeDriver: false
728
+ }),
729
+ reactNative.Animated.spring(liveGrowth, {
730
+ toValue: 1,
731
+ friction: 5,
732
+ tension: 140,
733
+ useNativeDriver: false
734
+ })
735
+ ]).start();
736
+ } else {
737
+ reactNative.Animated.timing(liveGrowth, {
738
+ toValue: 1,
739
+ duration: 300,
740
+ easing: ANIMATION_EASING,
741
+ useNativeDriver: false
742
+ }).start();
743
+ }
744
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
745
+ return liveGrowth;
415
746
  }
416
-
417
- // src/components/custom/Workout/SetStrip.tsx
418
- var SET_STRIP_GAP = 5;
419
- function setCategory(set) {
420
- switch (set.status) {
421
- case "done":
422
- case "drop":
423
- case "myo":
424
- return "done";
425
- case "active":
426
- return "active";
427
- case "range":
428
- return set.doneVels.length > 0 ? "active" : "upcoming";
429
- case "todo":
430
- case "myo-upcoming":
431
- return "upcoming";
432
- }
747
+ function wrapJsx(fn) {
748
+ return function(type, props, ...rest) {
749
+ if (props && typeof props.className === "string" && props.className) {
750
+ const cn2 = props.className;
751
+ const cssStyle = { $$css: true, [cn2]: cn2 };
752
+ const existing = props.style;
753
+ props = {
754
+ ...props,
755
+ style: existing ? [cssStyle, existing] : cssStyle
756
+ };
757
+ delete props.className;
758
+ }
759
+ return fn(type, props, ...rest);
760
+ };
433
761
  }
434
- function describeSets(sets) {
435
- const done = sets.filter((s) => setCategory(s) === "done").length;
436
- const active = sets.filter((s) => setCategory(s) === "active").length;
437
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
438
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
762
+ var jsx = wrapJsx(jsxRuntime.jsx);
763
+ var jsxs = wrapJsx(jsxRuntime.jsxs);
764
+
765
+ // src/components/custom/charts/SetBarChart.tsx
766
+ function mixHex(a, b, t12) {
767
+ const parse = (h) => {
768
+ const s = h.replace("#", "");
769
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
770
+ };
771
+ const [ar, ag, ab] = parse(a);
772
+ const [br, bg, bb] = parse(b);
773
+ const ch = (x, y) => Math.round(x + (y - x) * t12).toString(16).padStart(2, "0");
774
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
439
775
  }
440
- function SetStrip({ sets, height = 8, className, ...props }) {
441
- return /* @__PURE__ */ jsx(
776
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
777
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
778
+ var FIXED_MAX_VALUE = 1.15;
779
+ var PEAK_HEADROOM = 1.03;
780
+ function scaleDenominator(scale, maxValue) {
781
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
782
+ }
783
+ function valueLabelFontSize(height) {
784
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
785
+ }
786
+ var SET_BAR_DEFAULT_HEIGHT = 220;
787
+ var LABEL_ROW_HEIGHT = 16;
788
+ var LABEL_GAP = 3;
789
+ var GAP_RATIO = 0.08;
790
+ var MIN_BAR_GAP = 2;
791
+ var CHUNK_NOTCH_RATIO = 0.25;
792
+ var CHUNK_NOTCH_MIN_PX = 10;
793
+ var BAR_MAX_WIDTH = 120;
794
+ var LABEL_MIN_BAR_WIDTH = 30;
795
+ var DEFAULT_BAR_RADIUS = 5;
796
+ var MIN_BAR_HEIGHT = 4;
797
+ var FLAT_BAR_FRACTION = 0.5;
798
+ var STUB_HEIGHT = 9;
799
+ var REP_GAP = 2;
800
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
801
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
802
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
803
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
804
+ return { barWidth, gap };
805
+ }
806
+ function isRep(slot) {
807
+ return slot.kind === "rep";
808
+ }
809
+ function repIndexByCell(cells) {
810
+ let count = 0;
811
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
812
+ }
813
+ function SetBarChart({
814
+ slots,
815
+ colorFor,
816
+ height,
817
+ scale = "peak",
818
+ scaleMax,
819
+ orientation = "up",
820
+ liveRepIndex,
821
+ isNewPeak = false,
822
+ targetReps,
823
+ gapRatio = GAP_RATIO,
824
+ barRadius = DEFAULT_BAR_RADIUS,
825
+ cornerStyle = "top",
826
+ flat = false,
827
+ showValueLabels: showValueLabelsProp = false,
828
+ formatValue: formatValue2 = String,
829
+ todoVariant = "solid",
830
+ minColumns,
831
+ renderReference,
832
+ renderBarOverlay,
833
+ expandProgress,
834
+ hideBaseline = false,
835
+ testID,
836
+ testIDPrefix = "setbar",
837
+ accessibilityLabel: accessibilityLabel2,
838
+ label,
839
+ className,
840
+ viewProps = {}
841
+ }) {
842
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
843
+ const cells = targetPadded > slots.length ? [
844
+ ...slots,
845
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
846
+ ] : slots;
847
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
848
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
849
+ const flip = orientation === "down";
850
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
851
+ const placeholderColor = useOnSurfaceColor("tertiary");
852
+ const surface = useSurface();
853
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
854
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
855
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
856
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
857
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
858
+ const showValueLabels = showValueLabelsProp && !flat;
859
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
860
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
861
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
862
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
863
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
864
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
865
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
866
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
867
+ const [plotW, setPlotW] = react.useState(0);
868
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
869
+ const totalCells = cells.length;
870
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
871
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
872
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
873
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
874
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
875
+ const { style: externalStyle, ...restProps } = viewProps;
876
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
877
+ const repIndices = repIndexByCell(cells);
878
+ return /* @__PURE__ */ jsxs(
442
879
  reactNative.View,
443
880
  {
444
881
  className,
445
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
446
- accessibilityRole: "image",
447
- accessibilityLabel: describeSets(sets),
448
- testID: "set-strip",
449
- ...props,
450
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
882
+ style: [{ height, flexDirection: "row" }, externalStyle],
883
+ testID,
884
+ ...a11y,
885
+ ...restProps,
886
+ children: [
887
+ label ? /* @__PURE__ */ jsx(
888
+ ChartSideRail,
889
+ {
890
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
891
+ sectionExtent: height
892
+ }
893
+ ) : null,
894
+ /* @__PURE__ */ jsxs(
895
+ reactNative.View,
896
+ {
897
+ onLayout: onPlotLayout,
898
+ style: {
899
+ flex: 1,
900
+ flexDirection: "row",
901
+ alignItems: "flex-end",
902
+ gap,
903
+ position: "relative",
904
+ borderBottomWidth: hideBaseline ? 0 : 2,
905
+ borderBottomColor: placeholderColor,
906
+ ...flipStyle
907
+ },
908
+ children: [
909
+ renderReference?.(geometry),
910
+ cells.map((slot, i) => {
911
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
912
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
913
+ const column = {
914
+ flex: 1,
915
+ maxWidth: BAR_MAX_WIDTH,
916
+ height: "100%",
917
+ alignItems: "center",
918
+ justifyContent: "flex-end",
919
+ marginLeft: extraGap
920
+ };
921
+ if (!isRep(slot)) {
922
+ return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
923
+ slot.kind,
924
+ barCorners,
925
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
926
+ flat ? compactBarHeight : STUB_HEIGHT,
927
+ placeholderColor,
928
+ testIDPrefix,
929
+ todoVariant,
930
+ solidTodoColor,
931
+ emptyColor
932
+ ) }, i);
933
+ }
934
+ const repIndex = repIndices[i];
935
+ const value = slot.value ?? 0;
936
+ const isLive = liveRepIndex === repIndex;
937
+ const color = colorFor(value);
938
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
939
+ inputRange: [0, 1],
940
+ outputRange: [flatBarHeight, valueBarHeight(value)]
941
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
942
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
943
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
944
+ return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
945
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
946
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
947
+ /* @__PURE__ */ jsx(
948
+ reactNative.Animated.View,
949
+ {
950
+ style: {
951
+ position: "absolute",
952
+ bottom: labelBottom,
953
+ left: 0,
954
+ right: 0,
955
+ alignItems: "center",
956
+ ...expandProgress ? { opacity: expandProgress } : null
957
+ },
958
+ pointerEvents: "none",
959
+ children: /* @__PURE__ */ jsx(
960
+ reactNative.Text,
961
+ {
962
+ className: "text-text-secondary",
963
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
964
+ testID: `${testIDPrefix}-label-${repIndex}`,
965
+ children: formatValue2(value)
966
+ }
967
+ )
968
+ }
969
+ ),
970
+ /* @__PURE__ */ jsx(
971
+ reactNative.Animated.View,
972
+ {
973
+ style: [
974
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
975
+ barCorners,
976
+ barPaper(color, flip)
977
+ ],
978
+ testID: `${testIDPrefix}-bar-${repIndex}`
979
+ }
980
+ ),
981
+ renderBarOverlay?.(repIndex, value)
982
+ ] }, i);
983
+ })
984
+ ]
985
+ }
986
+ )
987
+ ]
451
988
  }
452
989
  );
453
990
  }
991
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
992
+ const base = {
993
+ width: "100%",
994
+ height: stubHeight,
995
+ ...barCorners
996
+ };
997
+ if (kind === "variable") {
998
+ return /* @__PURE__ */ jsx(
999
+ reactNative.View,
1000
+ {
1001
+ style: { ...base, backgroundColor: VARIABLE_FILL },
1002
+ testID: `${testIDPrefix}-slot-variable`
1003
+ }
1004
+ );
1005
+ }
1006
+ if (kind === "continue") {
1007
+ return /* @__PURE__ */ jsx(
1008
+ reactNative.View,
1009
+ {
1010
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
1011
+ testID: `${testIDPrefix}-slot-continue`
1012
+ }
1013
+ );
1014
+ }
1015
+ if (kind === "empty") {
1016
+ return /* @__PURE__ */ jsx(
1017
+ reactNative.View,
1018
+ {
1019
+ style: { ...base, backgroundColor: emptyColor },
1020
+ testID: `${testIDPrefix}-slot-empty`
1021
+ }
1022
+ );
1023
+ }
1024
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
1025
+ return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
1026
+ }
1027
+ function sideLabelFontSize(height) {
1028
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
1029
+ }
1030
+ var SIDE_LABEL_INITIALS_BELOW = 70;
1031
+ function sideLabelText(label, extent) {
1032
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
1033
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
1034
+ }
1035
+ function ChartSideRail({
1036
+ sections,
1037
+ sectionExtent,
1038
+ variant = "hero"
1039
+ }) {
1040
+ if (!sections.some((s) => s.label)) return null;
1041
+ const isRail = variant === "rail";
1042
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
1043
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
1044
+ const textStyle = {
1045
+ maxWidth: "100%",
1046
+ textAlign: "center",
1047
+ fontSize,
1048
+ fontWeight: "700",
1049
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
1050
+ textTransform: "uppercase"
1051
+ };
1052
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
1053
+ reactNative.View,
1054
+ {
1055
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
1056
+ children: s.label ? /* @__PURE__ */ jsx(
1057
+ reactNative.View,
1058
+ {
1059
+ style: {
1060
+ width: labelLength,
1061
+ alignItems: "center",
1062
+ transform: [{ rotate: "-90deg" }]
1063
+ },
1064
+ children: /* @__PURE__ */ jsx(
1065
+ reactNative.Text,
1066
+ {
1067
+ className: "text-text-tertiary",
1068
+ style: textStyle,
1069
+ numberOfLines: 1,
1070
+ testID: s.testID,
1071
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
1072
+ }
1073
+ )
1074
+ }
1075
+ ) : null
1076
+ },
1077
+ s.testID
1078
+ )) });
1079
+ }
454
1080
 
455
1081
  // src/components/custom/Workout/VelocityStrip.tsx
456
1082
  var VEL_COLORS = WORKOUT_TOKENS.scale;
@@ -496,6 +1122,18 @@ function calculateMeanVelocity(velocities) {
496
1122
  const sum = velocities.reduce((acc, v) => acc + v, 0);
497
1123
  return sum / velocities.length;
498
1124
  }
1125
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
1126
+ function getVelocityLossColor(lossPct) {
1127
+ const [t1, t22, t32] = VL_LOSS_THRESHOLDS;
1128
+ if (lossPct < t1) return VEL_COLORS.green;
1129
+ if (lossPct < t22) return VEL_COLORS.yellow;
1130
+ if (lossPct < t32) return VEL_COLORS.orange;
1131
+ return VEL_COLORS.red;
1132
+ }
1133
+ function velocityLossForRep(velocity, best) {
1134
+ if (best <= 0) return 0;
1135
+ return Math.max(0, Math.round((best - velocity) / best * 100));
1136
+ }
499
1137
  function classifyBand(velocity, bands) {
500
1138
  for (const band of bands) {
501
1139
  if (band.max === null || velocity < band.max) return band;
@@ -505,15 +1143,17 @@ function classifyBand(velocity, bands) {
505
1143
  function bandColor(band) {
506
1144
  return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
507
1145
  }
1146
+ function makeBarColorFor(zones) {
1147
+ const hasZones = zones != null && zones.length > 0;
1148
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
1149
+ }
508
1150
  function getLossStyle(loss) {
509
1151
  if (loss > 25) return { color: VEL_COLORS.red };
510
1152
  if (loss > 20) return { color: VEL_COLORS.orange };
511
1153
  return null;
512
1154
  }
513
- var REP_GAP = 2;
1155
+ var REP_GAP2 = 2;
514
1156
  var WIDE_GAP = 8;
515
- var TODO_COLOR2 = primitiveColors.charcoal[300];
516
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
517
1157
  function deriveDoneVelocities(set) {
518
1158
  switch (set.type) {
519
1159
  case "drop":
@@ -529,7 +1169,7 @@ function chunkedRepSlots(chunks) {
529
1169
  chunks.forEach((chunk) => {
530
1170
  chunk.forEach((velocity, ri) => {
531
1171
  const first = slots.length === 0;
532
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
1172
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
533
1173
  });
534
1174
  });
535
1175
  return slots;
@@ -543,7 +1183,7 @@ function buildSlots(set) {
543
1183
  return {
544
1184
  kind: done ? "rep" : "todo",
545
1185
  velocity: done ? set.velocities[i] : void 0,
546
- leadingGap: i === 0 ? 0 : REP_GAP
1186
+ leadingGap: i === 0 ? 0 : REP_GAP2
547
1187
  };
548
1188
  });
549
1189
  }
@@ -554,7 +1194,7 @@ function buildSlots(set) {
554
1194
  return {
555
1195
  kind,
556
1196
  velocity: kind === "rep" ? set.velocities[i] : void 0,
557
- leadingGap: i === 0 ? 0 : REP_GAP
1197
+ leadingGap: i === 0 ? 0 : REP_GAP2
558
1198
  };
559
1199
  });
560
1200
  }
@@ -562,16 +1202,16 @@ function buildSlots(set) {
562
1202
  const reps = set.velocities.map((velocity, i) => ({
563
1203
  kind: "rep",
564
1204
  velocity,
565
- leadingGap: i === 0 ? 0 : REP_GAP
1205
+ leadingGap: i === 0 ? 0 : REP_GAP2
566
1206
  }));
567
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
1207
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
568
1208
  return reps;
569
1209
  }
570
1210
  case "drop":
571
1211
  return chunkedRepSlots(set.subloads);
572
1212
  case "myo": {
573
1213
  const slots = chunkedRepSlots([set.activation, ...set.clusters]);
574
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
1214
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
575
1215
  return slots;
576
1216
  }
577
1217
  case "cluster": {
@@ -582,7 +1222,7 @@ function buildSlots(set) {
582
1222
  return {
583
1223
  kind: done ? "rep" : "todo",
584
1224
  velocity: done ? set.velocities[i] : void 0,
585
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
1225
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
586
1226
  };
587
1227
  });
588
1228
  }
@@ -604,253 +1244,148 @@ function setAccessibilityLabel(set, repCount) {
604
1244
  return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
605
1245
  }
606
1246
  }
607
- function slotAccessibilityLabel(slot, index) {
608
- switch (slot.kind) {
609
- case "rep":
610
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
611
- case "todo":
612
- return `Rep ${index + 1}: planned`;
613
- case "variable":
614
- return `Rep ${index + 1}: variable`;
615
- case "continue":
616
- return "Keep going";
617
- }
618
- }
619
- var EXPANDED_TODO_STUB_PCT = 16;
620
- var EXPANDED_ENCODED_PCT = 45;
621
1247
  var EXPANDED_HEIGHT = 60;
622
- var FIXED_MAX_VELOCITY = 1.15;
623
- var BARE_STUB_HEIGHT = 3;
624
- var HERO_HEIGHT = 220;
625
- var HERO_LABEL_HEADROOM = 20;
626
- var HERO_BAR_GAP = 8;
627
- var HERO_BAR_MAX_WIDTH = 120;
628
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
629
- var HERO_BAR_RADIUS = 5;
630
- var HERO_MIN_BAR_HEIGHT = 4;
631
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
632
- var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
633
- var PEAK_HEADROOM = 1.06;
634
- function scaleDenominator(scale, maxVelocity) {
635
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
636
- }
637
- function bareSlotHeight(slot, height, denom) {
638
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
639
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
640
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
1248
+ var COMPACT_HEIGHT = 8;
1249
+ var HERO_REFERENCE_COLOR = greyRamp[600];
1250
+ function DashedReferenceLine({
1251
+ anchor,
1252
+ offset,
1253
+ testID
1254
+ }) {
1255
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
1256
+ return /* @__PURE__ */ jsx(
1257
+ reactNative.View,
1258
+ {
1259
+ accessibilityElementsHidden: true,
1260
+ style: {
1261
+ position: "absolute",
1262
+ left: 0,
1263
+ right: 0,
1264
+ borderTopWidth: 1,
1265
+ borderStyle: "dashed",
1266
+ borderColor: HERO_REFERENCE_COLOR,
1267
+ pointerEvents: "none",
1268
+ ...edge
1269
+ },
1270
+ testID
1271
+ }
1272
+ );
641
1273
  }
642
- function getReducedMotionPreference() {
643
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
644
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
645
- }
646
- function usePrefersReducedMotion() {
647
- const [reduced, setReduced] = react.useState(getReducedMotionPreference);
648
- react.useEffect(() => {
649
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
650
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
651
- const handler = () => setReduced(mq.matches);
652
- handler();
653
- mq.addEventListener?.("change", handler);
654
- return () => mq.removeEventListener?.("change", handler);
655
- }, []);
656
- return reduced;
657
- }
658
- var ANIMATION_DURATION = 400;
659
- var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
660
- var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
661
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
662
- const prefersReducedMotion = usePrefersReducedMotion();
663
- const [liveScale] = react.useState(() => new reactNative.Animated.Value(1));
664
- react.useEffect(() => {
665
- if (!active || liveRepIndex == null || liveVelocity == null) return;
666
- if (prefersReducedMotion) {
667
- liveScale.setValue(1);
668
- return;
669
- }
670
- if (isNewPeak) {
671
- liveScale.setValue(1);
672
- reactNative.Animated.sequence([
673
- reactNative.Animated.timing(liveScale, {
674
- toValue: 1.25,
675
- duration: 150,
676
- easing: reactNative.Easing.out(reactNative.Easing.quad),
677
- useNativeDriver: true
678
- }),
679
- reactNative.Animated.spring(liveScale, {
680
- toValue: 1,
681
- friction: 3,
682
- tension: 140,
683
- useNativeDriver: true
684
- })
685
- ]).start();
686
- } else {
687
- liveScale.setValue(0.8);
688
- reactNative.Animated.timing(liveScale, {
689
- toValue: 1,
690
- duration: 300,
691
- easing: POP_EASING,
692
- useNativeDriver: true
693
- }).start();
694
- }
695
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
696
- return liveScale;
697
- }
698
- function HeroVelocityChart({
699
- doneVelocities,
700
- barColorFor,
1274
+ var BAND_LABEL_FONT = "monospace";
1275
+ var VL_LABEL_MIN_PLOT = 65;
1276
+ var VL_SEMANTIC = getSemanticColors("dark");
1277
+ function VelocityLossBands({
1278
+ best,
701
1279
  scaleDenom,
702
- referenceVelocity,
703
- liveRepIndex,
704
- isNewPeak,
705
- targetReps,
706
- height,
707
- className,
708
- viewProps
1280
+ plotHeight,
1281
+ flip = false
709
1282
  }) {
710
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
711
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
712
- const [plotW, setPlotW] = react.useState(0);
713
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
714
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
715
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
716
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
717
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
718
- const totalSlots = doneVelocities.length + pendingCount;
719
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
720
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
721
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
722
- const peakIndex = doneVelocities.reduce(
723
- (best, v, i) => v > doneVelocities[best] ? i : best,
724
- 0
1283
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
1284
+ const yOf = (v) => v / scaleDenom * plotHeight;
1285
+ const vl20 = best * 0.8;
1286
+ const vl30 = best * 0.7;
1287
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
1288
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
1289
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
1290
+ reactNative.View,
1291
+ {
1292
+ style: {
1293
+ position: "absolute",
1294
+ left: 0,
1295
+ right: 0,
1296
+ bottom: yOf(loV),
1297
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
1298
+ backgroundColor: color
1299
+ }
1300
+ }
725
1301
  );
726
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
727
- const repCount = doneVelocities.length;
728
- const total = Math.max(repCount, targetReps ?? repCount);
729
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
730
- const { style: externalStyle, ...restProps } = viewProps;
731
- return /* @__PURE__ */ jsx(
1302
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
732
1303
  reactNative.View,
733
1304
  {
734
- className,
735
- style: [{ height }, externalStyle],
736
- accessibilityRole: "image",
737
- accessibilityLabel: label,
738
- testID: "velocity-strip-hero",
739
- ...restProps,
740
- children: /* @__PURE__ */ jsxs(
741
- reactNative.View,
742
- {
743
- onLayout: onPlotLayout,
744
- style: {
745
- flex: 1,
746
- flexDirection: "row",
747
- alignItems: "flex-end",
748
- gap: heroGap,
749
- position: "relative",
750
- borderBottomWidth: 2,
751
- borderBottomColor: HERO_PENDING_COLOR
752
- },
753
- children: [
754
- referencePx > 0 && /* @__PURE__ */ jsx(
755
- reactNative.View,
756
- {
757
- accessibilityElementsHidden: true,
758
- style: {
759
- position: "absolute",
760
- left: 0,
761
- right: 0,
762
- bottom: referencePx,
763
- borderTopWidth: 1,
764
- borderStyle: "dashed",
765
- borderColor: HERO_REFERENCE_COLOR,
766
- pointerEvents: "none"
767
- },
768
- testID: "velocity-hero-reference"
769
- }
770
- ),
771
- doneVelocities.map((velocity, i) => {
772
- const isLive = liveRepIndex === i;
773
- return /* @__PURE__ */ jsxs(
774
- reactNative.View,
775
- {
776
- accessibilityElementsHidden: true,
777
- style: {
778
- flex: 1,
779
- maxWidth: HERO_BAR_MAX_WIDTH,
780
- height: "100%",
781
- alignItems: "center",
782
- justifyContent: "flex-end"
783
- },
784
- children: [
785
- showBarLabel(i) && /* @__PURE__ */ jsx(
786
- reactNative.Text,
787
- {
788
- className: "text-text-primary",
789
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
790
- testID: `velocity-label-${i}`,
791
- children: formatVelocity(velocity)
792
- }
793
- ),
794
- /* @__PURE__ */ jsx(
795
- reactNative.Animated.View,
796
- {
797
- style: [
798
- {
799
- width: "100%",
800
- height: barHeight(velocity),
801
- borderTopLeftRadius: HERO_BAR_RADIUS,
802
- borderTopRightRadius: HERO_BAR_RADIUS,
803
- backgroundColor: barColorFor(velocity)
804
- },
805
- isLive ? { transform: [{ scale: liveScale }] } : null
806
- ],
807
- testID: `velocity-bar-${i}`
808
- }
809
- )
810
- ]
811
- },
812
- i
813
- );
814
- }),
815
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
816
- reactNative.View,
817
- {
818
- accessibilityElementsHidden: true,
819
- style: {
820
- flex: 1,
821
- maxWidth: HERO_BAR_MAX_WIDTH,
822
- height: "100%",
823
- alignItems: "center",
824
- justifyContent: "flex-end"
825
- },
826
- children: /* @__PURE__ */ jsx(
827
- reactNative.View,
828
- {
829
- style: {
830
- width: "100%",
831
- height: HERO_MIN_BAR_HEIGHT * 3,
832
- borderWidth: 1,
833
- borderStyle: "dashed",
834
- borderColor: HERO_PENDING_COLOR,
835
- borderTopLeftRadius: HERO_BAR_RADIUS,
836
- borderTopRightRadius: HERO_BAR_RADIUS
837
- },
838
- testID: "velocity-slot-todo"
839
- }
840
- )
1305
+ style: {
1306
+ position: "absolute",
1307
+ left: 0,
1308
+ right: 0,
1309
+ bottom: yOf(v),
1310
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
1311
+ // band edge instead of floating up half a text-row (the label then centres on
1312
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
1313
+ height: 0,
1314
+ flexDirection: "row",
1315
+ alignItems: "center"
1316
+ },
1317
+ children: [
1318
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
1319
+ showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1320
+ /* @__PURE__ */ jsx(
1321
+ reactNative.Text,
1322
+ {
1323
+ style: {
1324
+ marginHorizontal: 6,
1325
+ fontSize: vlFont,
1326
+ fontWeight: "800",
1327
+ fontFamily: BAND_LABEL_FONT,
1328
+ color,
1329
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
841
1330
  },
842
- `pending-${i}`
843
- ))
844
- ]
845
- }
846
- )
1331
+ children: label
1332
+ }
1333
+ ),
1334
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
1335
+ ] }) : null
1336
+ ]
847
1337
  }
848
1338
  );
1339
+ return /* @__PURE__ */ jsxs(
1340
+ reactNative.View,
1341
+ {
1342
+ accessibilityElementsHidden: true,
1343
+ pointerEvents: "none",
1344
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
1345
+ testID: "velocity-loss-bands",
1346
+ children: [
1347
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
1348
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
1349
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
1350
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
1351
+ ]
1352
+ }
1353
+ );
1354
+ }
1355
+ function velocityReferenceOverlay(g, showLossBands) {
1356
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
1357
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1358
+ showLossBands && /* @__PURE__ */ jsx(
1359
+ VelocityLossBands,
1360
+ {
1361
+ best: g.best,
1362
+ scaleDenom: g.scaleDenom,
1363
+ plotHeight: g.plotHeight,
1364
+ flip: g.flip
1365
+ }
1366
+ ),
1367
+ referencePx > 0 && /* @__PURE__ */ jsx(
1368
+ DashedReferenceLine,
1369
+ {
1370
+ anchor: "bottom",
1371
+ offset: referencePx,
1372
+ testID: "velocity-hero-reference"
1373
+ }
1374
+ )
1375
+ ] });
849
1376
  }
1377
+ var ANIMATION_DURATION = 400;
850
1378
  function VelocityStrip({
851
1379
  velocities,
852
1380
  set,
853
1381
  zones,
1382
+ barColor = "loss",
1383
+ showLossBands,
1384
+ orientation = "up",
1385
+ scaleMax,
1386
+ hideBaseline,
1387
+ columnSlots,
1388
+ label,
854
1389
  liveRepIndex,
855
1390
  expanded = true,
856
1391
  onToggle,
@@ -864,51 +1399,29 @@ function VelocityStrip({
864
1399
  className,
865
1400
  ...props
866
1401
  }) {
1402
+ const lossBandsOn = showLossBands ?? barColor === "loss";
867
1403
  const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
868
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
869
- kind: "rep",
870
- velocity: v,
871
- leadingGap: i === 0 ? 0 : REP_GAP
872
- }));
873
1404
  const maxVelocity = Math.max(...doneVelocities, 0);
874
1405
  const meanVelocity = calculateMeanVelocity(doneVelocities);
875
1406
  const loss = calculateVelocityLoss(doneVelocities);
876
1407
  const framed = showNumbers || showInfo;
877
- const scaleDenom = scaleDenominator(scale, maxVelocity);
878
1408
  const hasZones = zones != null && zones.length > 0;
879
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
880
- const slotColor = (slot) => {
881
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
882
- if (slot.kind === "todo") return TODO_COLOR2;
883
- return SET_STRIP_VARIABLE_COLOR;
884
- };
1409
+ const zoneColorFor = makeBarColorFor(zones);
1410
+ const barColorFor = (v) => barColor === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
885
1411
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
886
- const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
887
- const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
1412
+ const [expandProgress] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
888
1413
  const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
889
1414
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
890
1415
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
891
- const liveScale = useLiveRepPop(
892
- variant === "expanded" && framed,
893
- liveRepIndex,
894
- liveVelocity,
895
- isNewPeak
896
- );
897
1416
  react.useEffect(() => {
898
1417
  if (variant !== "expanded" || !framed) return;
899
1418
  reactNative.Animated.parallel([
900
- reactNative.Animated.timing(heightAnim, {
901
- toValue: expanded ? height : 3,
1419
+ reactNative.Animated.timing(expandProgress, {
1420
+ toValue: expanded ? 1 : 0,
902
1421
  duration: ANIMATION_DURATION,
903
1422
  easing: ANIMATION_EASING,
904
1423
  useNativeDriver: false
905
1424
  }),
906
- reactNative.Animated.timing(labelOpacity, {
907
- toValue: expanded ? 1 : 0,
908
- duration: 300,
909
- delay: expanded ? 150 : 0,
910
- useNativeDriver: false
911
- }),
912
1425
  reactNative.Animated.timing(infoOpacity, {
913
1426
  toValue: expanded ? 1 : 0,
914
1427
  duration: 300,
@@ -916,250 +1429,176 @@ function VelocityStrip({
916
1429
  useNativeDriver: false
917
1430
  })
918
1431
  ]).start();
919
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
1432
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
920
1433
  if (set == null && velocities == null) return null;
921
1434
  const repCount = doneVelocities.length;
922
1435
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
923
1436
  if (variant === "hero") {
924
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
1437
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
1438
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1439
+ kind: s.kind,
1440
+ value: s.velocity,
1441
+ leadingGap: s.leadingGap
1442
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1443
+ const total = Math.max(repCount, targetReps ?? repCount);
1444
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
925
1445
  return /* @__PURE__ */ jsx(
926
- HeroVelocityChart,
1446
+ SetBarChart,
927
1447
  {
928
- doneVelocities,
929
- barColorFor,
930
- scaleDenom,
931
- referenceVelocity: maxVelocity,
1448
+ slots: heroSlots,
1449
+ colorFor: barColorFor,
1450
+ height: heroHeight,
1451
+ scale,
1452
+ scaleMax,
1453
+ orientation,
932
1454
  liveRepIndex,
933
1455
  isNewPeak,
934
- targetReps,
935
- height: heroHeight,
1456
+ targetReps: set || columnSlots ? void 0 : targetReps,
1457
+ label,
1458
+ showValueLabels: true,
1459
+ formatValue: formatVelocity,
1460
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
1461
+ hideBaseline: true,
1462
+ testID: "velocity-strip-hero",
1463
+ testIDPrefix: "velocity",
1464
+ accessibilityLabel: heroLabel,
936
1465
  className,
937
1466
  viewProps: props
938
1467
  }
939
1468
  );
940
1469
  }
941
- if (variant === "mini") {
942
- const { style: externalStyle, ...restProps } = props;
1470
+ if (variant === "compact") {
1471
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1472
+ kind: s.kind,
1473
+ value: s.velocity,
1474
+ leadingGap: s.leadingGap
1475
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1476
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
943
1477
  return /* @__PURE__ */ jsx(
944
- reactNative.View,
1478
+ SetBarChart,
945
1479
  {
946
- className,
947
- style: [
948
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
949
- externalStyle
950
- ],
951
- accessibilityRole: "image",
1480
+ slots: compactSlots,
1481
+ colorFor: barColorFor,
1482
+ height: compactHeight,
1483
+ scale,
1484
+ scaleMax,
1485
+ orientation,
1486
+ flat: true,
1487
+ barRadius: 2,
1488
+ cornerStyle: "all",
1489
+ targetReps: set || columnSlots ? void 0 : targetReps,
1490
+ label,
1491
+ hideBaseline: true,
1492
+ testID: "velocity-strip-compact",
1493
+ testIDPrefix: "velocity",
952
1494
  accessibilityLabel: miniLabel,
953
- testID: "velocity-strip-mini",
954
- ...restProps,
955
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
956
- reactNative.View,
957
- {
958
- style: {
959
- flex: 1,
960
- backgroundColor: slotColor(slot),
961
- borderRadius: 1,
962
- minWidth: 4,
963
- height: "100%",
964
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
965
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
966
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
967
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
968
- },
969
- accessibilityElementsHidden: true,
970
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
971
- },
972
- i
973
- ))
1495
+ className,
1496
+ viewProps: props
974
1497
  }
975
1498
  );
976
1499
  }
977
1500
  if (!framed) {
978
- const { style: externalStyle, ...restProps } = props;
1501
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1502
+ kind: s.kind,
1503
+ value: s.velocity,
1504
+ leadingGap: s.leadingGap
1505
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
979
1506
  return /* @__PURE__ */ jsx(
980
- reactNative.View,
1507
+ SetBarChart,
981
1508
  {
982
- className,
983
- style: [
984
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
985
- externalStyle
986
- ],
987
- accessibilityRole: "image",
1509
+ slots: spotlightSlots,
1510
+ colorFor: barColorFor,
1511
+ height,
1512
+ scale,
1513
+ scaleMax,
1514
+ orientation,
1515
+ liveRepIndex,
1516
+ isNewPeak,
1517
+ barRadius: 2,
1518
+ cornerStyle: "top",
1519
+ targetReps: set ? void 0 : targetReps,
1520
+ label,
1521
+ hideBaseline: true,
1522
+ testID: "velocity-strip-spotlight",
1523
+ testIDPrefix: "velocity",
988
1524
  accessibilityLabel: miniLabel,
989
- testID: "velocity-strip-compact",
990
- ...restProps,
991
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
992
- reactNative.View,
993
- {
994
- style: {
995
- flex: 1,
996
- minWidth: 4,
997
- height: bareSlotHeight(slot, height, scaleDenom),
998
- // Top-rounded to match the framed chart's bars (the "rounded tops from
999
- // the numbers one"); square bottoms since bars sit on the baseline.
1000
- borderTopLeftRadius: 2,
1001
- borderTopRightRadius: 2,
1002
- backgroundColor: slotColor(slot),
1003
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
1004
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1005
- },
1006
- accessibilityElementsHidden: true,
1007
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
1008
- },
1009
- i
1010
- ))
1525
+ className,
1526
+ viewProps: props
1011
1527
  }
1012
1528
  );
1013
1529
  }
1014
1530
  const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
1015
1531
  const hasInteractiveContainer = onToggle != null;
1016
1532
  const hasInteractiveReps = onRepPress != null && expanded;
1017
- const setBars = set != null && slots.map((slot, i) => {
1018
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
1019
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
1020
- return /* @__PURE__ */ jsx(
1021
- reactNative.View,
1533
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
1534
+ kind: sl.kind,
1535
+ value: sl.velocity,
1536
+ leadingGap: sl.leadingGap
1537
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
1538
+ const needsBarOverlay = showNumbers || onRepPress != null;
1539
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1540
+ showNumbers && /* @__PURE__ */ jsx(
1541
+ reactNative.Animated.View,
1022
1542
  {
1023
1543
  style: {
1024
- flex: 1,
1025
- height: "100%",
1026
- justifyContent: "flex-end",
1027
- marginLeft: slot.leadingGap
1544
+ opacity: expandProgress,
1545
+ position: "absolute",
1546
+ top: -13,
1547
+ left: 0,
1548
+ right: 0,
1549
+ alignItems: "center"
1028
1550
  },
1551
+ accessibilityElementsHidden: true,
1552
+ pointerEvents: "none",
1029
1553
  children: /* @__PURE__ */ jsx(
1030
- reactNative.View,
1554
+ reactNative.Text,
1031
1555
  {
1032
- style: {
1033
- height: `${heightPct}%`,
1034
- minHeight: 2,
1035
- borderTopLeftRadius: 2,
1036
- borderTopRightRadius: 2,
1037
- backgroundColor: slotColor(slot),
1038
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1039
- },
1040
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
1041
- accessibilityRole: "image",
1042
- accessibilityLabel: slotAccessibilityLabel(slot, i)
1556
+ className: "text-text-secondary",
1557
+ style: { fontSize: 8, fontWeight: "600" },
1558
+ testID: `velocity-label-${repIndex}`,
1559
+ children: formatVelocity(value)
1043
1560
  }
1044
1561
  )
1045
- },
1046
- i
1047
- );
1048
- });
1562
+ }
1563
+ ),
1564
+ onRepPress && expanded && /* @__PURE__ */ jsx(
1565
+ reactNative.Pressable,
1566
+ {
1567
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
1568
+ onPress: () => onRepPress(repIndex, value),
1569
+ accessibilityRole: "button",
1570
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
1571
+ testID: `velocity-bar-pressable-${repIndex}`
1572
+ }
1573
+ )
1574
+ ] }) : void 0;
1049
1575
  const stripContent = /* @__PURE__ */ jsxs(
1050
1576
  reactNative.Animated.View,
1051
1577
  {
1052
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
1053
- style: [
1054
- {
1055
- height: heightAnim,
1056
- width: "100%",
1057
- gap: 2,
1058
- borderRadius: expanded ? 6 : 2,
1059
- paddingTop: expanded ? 16 : 0,
1060
- paddingBottom: expanded ? 24 : 0,
1061
- // No horizontal inset — the framed chart's bars span full width, matching
1062
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
1063
- paddingHorizontal: 0,
1064
- paddingVertical: expanded ? void 0 : 8,
1065
- overflow: expanded ? "visible" : "hidden"
1066
- }
1067
- ],
1578
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
1579
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
1068
1580
  accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
1069
1581
  accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
1070
1582
  testID: "velocity-strip",
1071
1583
  ...props,
1072
1584
  children: [
1073
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
1074
- const barBackground = barColorFor(v);
1075
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
1076
- const isLive = liveRepIndex === i;
1077
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
1078
- const barStyle = expanded ? {
1079
- height: `${barHeightPct}%`,
1080
- minHeight: 2,
1081
- borderTopLeftRadius: 2,
1082
- borderTopRightRadius: 2,
1083
- backgroundColor: barBackground
1084
- } : {
1085
- minHeight: "100%",
1086
- borderTopLeftRadius: 2,
1087
- borderTopRightRadius: 2,
1088
- borderBottomLeftRadius: 0,
1089
- borderBottomRightRadius: 0,
1090
- backgroundColor: barBackground
1091
- };
1092
- const barInner = isLive ? /* @__PURE__ */ jsx(
1093
- reactNative.Animated.View,
1094
- {
1095
- style: [barStyle, { transform: [{ scale: liveScale }] }],
1096
- testID: `velocity-bar-${i}`,
1097
- accessibilityRole: "image",
1098
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
1099
- }
1100
- ) : /* @__PURE__ */ jsx(
1101
- reactNative.View,
1102
- {
1103
- style: barStyle,
1104
- testID: `velocity-bar-${i}`,
1105
- accessibilityRole: "image",
1106
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
1107
- }
1108
- );
1109
- const bar = /* @__PURE__ */ jsxs(
1110
- reactNative.View,
1111
- {
1112
- style: {
1113
- flex: 1,
1114
- height: "100%",
1115
- justifyContent: "flex-end",
1116
- position: "relative"
1117
- },
1118
- children: [
1119
- expanded && showNumbers && /* @__PURE__ */ jsx(
1120
- reactNative.Animated.View,
1121
- {
1122
- style: {
1123
- opacity: labelOpacity,
1124
- alignItems: "center",
1125
- position: "absolute",
1126
- top: -13,
1127
- left: 0,
1128
- right: 0
1129
- },
1130
- accessibilityElementsHidden: true,
1131
- children: /* @__PURE__ */ jsx(
1132
- reactNative.Text,
1133
- {
1134
- className: "text-text-secondary",
1135
- style: { fontSize: 8, fontWeight: "600" },
1136
- testID: `velocity-label-${i}`,
1137
- children: formatVelocity(v)
1138
- }
1139
- )
1140
- }
1141
- ),
1142
- barInner
1143
- ]
1144
- },
1145
- i
1146
- );
1147
- if (onRepPress && expanded) {
1148
- return /* @__PURE__ */ jsx(
1149
- reactNative.Pressable,
1150
- {
1151
- style: { flex: 1, height: "100%" },
1152
- onPress: () => onRepPress(i, v),
1153
- accessibilityRole: "button",
1154
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
1155
- testID: `velocity-bar-pressable-${i}`,
1156
- children: bar
1157
- },
1158
- i
1159
- );
1585
+ /* @__PURE__ */ jsx(
1586
+ SetBarChart,
1587
+ {
1588
+ slots: framedSlots,
1589
+ colorFor: barColorFor,
1590
+ height,
1591
+ scale,
1592
+ scaleMax,
1593
+ expandProgress,
1594
+ renderBarOverlay: renderFramedBarOverlay,
1595
+ targetReps: set ? void 0 : targetReps,
1596
+ barRadius: 2,
1597
+ cornerStyle: "top",
1598
+ hideBaseline: true,
1599
+ testIDPrefix: "velocity"
1160
1600
  }
1161
- return bar;
1162
- }) }),
1601
+ ),
1163
1602
  expanded && showInfo && /* @__PURE__ */ jsxs(
1164
1603
  reactNative.Animated.View,
1165
1604
  {
@@ -1167,10 +1606,8 @@ function VelocityStrip({
1167
1606
  flexDirection: "row",
1168
1607
  justifyContent: "space-between",
1169
1608
  opacity: infoOpacity,
1170
- position: "absolute",
1171
- bottom: 4,
1172
- left: 6,
1173
- right: 6
1609
+ marginTop: 6,
1610
+ paddingHorizontal: 6
1174
1611
  },
1175
1612
  testID: "velocity-info-row",
1176
1613
  children: [
@@ -1178,10 +1615,7 @@ function VelocityStrip({
1178
1615
  reactNative.Text,
1179
1616
  {
1180
1617
  className: "text-text-secondary",
1181
- style: {
1182
- fontSize: 10,
1183
- fontFamily: "Inter, sans-serif"
1184
- },
1618
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
1185
1619
  children: [
1186
1620
  meanZone,
1187
1621
  " ",
@@ -1196,11 +1630,7 @@ function VelocityStrip({
1196
1630
  reactNative.Text,
1197
1631
  {
1198
1632
  className: "text-text-secondary",
1199
- style: {
1200
- fontSize: 10,
1201
- fontFamily: "Inter, sans-serif",
1202
- ...getLossStyle(loss)
1203
- },
1633
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
1204
1634
  children: [
1205
1635
  "Loss: ",
1206
1636
  loss,
@@ -1262,434 +1692,123 @@ function SegmentedStrip({
1262
1692
  segments = 3,
1263
1693
  width,
1264
1694
  className,
1265
- ...props
1266
- }) {
1267
- return /* @__PURE__ */ jsx(
1268
- reactNative.View,
1269
- {
1270
- className: cn("flex-row", className),
1271
- style: [
1272
- { height: 3, gap: 2, opacity: 0.5 },
1273
- width != null ? { width } : { flex: 1 }
1274
- ],
1275
- accessibilityRole: "image",
1276
- accessibilityLabel: "Planned set, not yet completed",
1277
- testID: "placeholder-strip",
1278
- ...props,
1279
- children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
1280
- reactNative.View,
1281
- {
1282
- style: {
1283
- flex: 1,
1284
- height: 3,
1285
- backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1286
- borderRadius: 1,
1287
- minWidth: 4
1288
- },
1289
- accessibilityElementsHidden: true,
1290
- testID: "placeholder-segment"
1291
- },
1292
- i
1293
- ))
1294
- }
1295
- );
1296
- }
1297
- function PlaceholderStrip({
1298
- mode = "single",
1299
- segments,
1300
- ...props
1301
- }) {
1302
- if (mode === "segmented") {
1303
- return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
1304
- }
1305
- return /* @__PURE__ */ jsx(SingleStrip, { ...props });
1306
- }
1307
-
1308
- // src/components/icons/SvgIcon.tsx
1309
- function SvgIcon({
1310
- size = 24,
1311
- color = "currentColor",
1312
- strokeWidth = 2,
1313
- fill = "none",
1314
- title,
1315
- children
1316
- }) {
1317
- return /* @__PURE__ */ jsxs(
1318
- "svg",
1319
- {
1320
- width: size,
1321
- height: size,
1322
- viewBox: "0 0 24 24",
1323
- fill,
1324
- stroke: color,
1325
- strokeWidth,
1326
- strokeLinecap: "round",
1327
- strokeLinejoin: "round",
1328
- ...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
1329
- children: [
1330
- title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
1331
- children
1332
- ]
1333
- }
1334
- );
1335
- }
1336
-
1337
- // src/components/icons/icons.tsx
1338
- function StarIcon(props) {
1339
- 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" }) });
1340
- }
1341
- function ScaleIcon(props) {
1342
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1343
- /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1344
- /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1345
- /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
1346
- /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
1347
- /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
1348
- ] });
1349
- }
1350
- function AlertTriangleIcon(props) {
1351
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1352
- /* @__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" }),
1353
- /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1354
- /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1355
- ] });
1356
- }
1357
- function TrendingDownIcon(props) {
1358
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1359
- /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
1360
- /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
1361
- ] });
1362
- }
1363
- function CircleSlashIcon(props) {
1364
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1365
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1366
- /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
1367
- ] });
1368
- }
1369
- function AwardIcon(props) {
1370
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1371
- /* @__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" }),
1372
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
1373
- ] });
1374
- }
1375
- function InfoIcon(props) {
1376
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1377
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1378
- /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
1379
- /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1380
- ] });
1381
- }
1382
-
1383
- // src/theme/tokens/semantic.ts
1384
- var semanticColorsLight = {
1385
- // Brand colors (brand-*)
1386
- "brand-primary": primitiveRamps.orange[400],
1387
- "brand-primary-light": primitiveRamps.orange[300],
1388
- "brand-primary-dark": primitiveRamps.orange[500],
1389
- "brand-primary-subtle": "rgba(255, 121, 0, 0.08)",
1390
- "brand-primary-hover": primitiveRamps.orange[500],
1391
- "brand-primary-active": primitiveRamps.orange[600],
1392
- "brand-secondary": primitiveRamps.cyan[600],
1393
- "brand-secondary-light": primitiveRamps.cyan[500],
1394
- "brand-secondary-dark": primitiveRamps.cyan[700],
1395
- "brand-secondary-subtle": "rgba(48, 123, 155, 0.08)",
1396
- "brand-secondary-hover": primitiveRamps.cyan[700],
1397
- "brand-secondary-active": primitiveRamps.cyan[800],
1398
- // Text on brand backgrounds (on-*)
1399
- "on-brand-primary": primitiveColors.white,
1400
- "on-brand-secondary": primitiveColors.white,
1401
- // Status colors (status-*)
1402
- "status-success": primitiveRamps.green[300],
1403
- "status-success-light": primitiveRamps.green[200],
1404
- "status-success-dark": primitiveRamps.green[600],
1405
- "status-success-subtle": primitiveRamps.green[50],
1406
- // Live-session accent — its OWN role, decoupled from success so the two can diverge
1407
- "status-live": primitiveRamps.green[300],
1408
- "status-live-muted": primitiveRamps.green[500],
1409
- "status-error": primitiveRamps.red[600],
1410
- "status-error-light": primitiveRamps.red[500],
1411
- "status-error-dark": primitiveRamps.red[700],
1412
- "status-error-subtle": primitiveRamps.red[50],
1413
- "status-error-vivid": primitiveColors.redVivid[500],
1414
- // vivid alert red
1415
- "status-error-vivid-light": primitiveColors.redVivid[400],
1416
- "status-error-vivid-dark": primitiveColors.redVivid[700],
1417
- "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
1418
- "status-warning": primitiveRamps.amber[300],
1419
- "status-warning-light": primitiveRamps.amber[200],
1420
- "status-warning-dark": primitiveRamps.amber[500],
1421
- "status-warning-subtle": primitiveRamps.amber[50],
1422
- "status-info": primitiveRamps.blue[500],
1423
- "status-info-light": primitiveRamps.blue[300],
1424
- "status-info-dark": primitiveRamps.blue[600],
1425
- "status-info-subtle": primitiveRamps.blue[50],
1426
- // Text on status backgrounds (on-status-*)
1427
- "on-status-success": primitiveColors.white,
1428
- "on-status-error": primitiveColors.white,
1429
- "on-status-warning": primitiveColors.white,
1430
- "on-status-info": primitiveColors.white,
1431
- // Result/outcome indicators (result-*)
1432
- "result-improve": "#4caf50",
1433
- // Green - positive outcome
1434
- "result-improve-light": "rgba(76, 175, 80, 0.12)",
1435
- "result-improve-dark": "#248a24",
1436
- "result-degrade": "#ef5350",
1437
- // Red - negative outcome
1438
- "result-degrade-light": "rgba(239, 83, 80, 0.12)",
1439
- "result-degrade-dark": "#b30000",
1440
- "result-inconclusive": "#9E9A97",
1441
- // Gray - no clear result
1442
- "result-inconclusive-light": "rgba(158, 154, 151, 0.12)",
1443
- "result-neutral": primitiveColors.neutral[500],
1444
- // Neutral baseline
1445
- // Text on result backgrounds (on-result-*)
1446
- "on-result-improve": primitiveColors.white,
1447
- "on-result-degrade": primitiveColors.white,
1448
- "on-result-inconclusive": primitiveColors.white,
1449
- // Data visualization colors (data-*)
1450
- // First 10 colors from discrete rainbow optimized for charts
1451
- "data-1": discreteRainbow[9],
1452
- // Blue
1453
- "data-2": discreteRainbow[14],
1454
- // Green
1455
- "data-3": discreteRainbow[17],
1456
- // Yellow
1457
- "data-4": discreteRainbow[25],
1458
- // Red
1459
- "data-5": discreteRainbow[8],
1460
- // Purple
1461
- "data-6": discreteRainbow[20],
1462
- // Orange
1463
- "data-7": discreteRainbow[13],
1464
- // Light Blue
1465
- "data-8": discreteRainbow[15],
1466
- // Light Green
1467
- "data-9": discreteRainbow[3],
1468
- // Lavender
1469
- "data-10": discreteRainbow[22],
1470
- // Dark Orange
1471
- // Text colors (text-*)
1472
- "text-primary": "#121828",
1473
- "text-secondary": "#65748B",
1474
- "text-tertiary": primitiveColors.neutral[400],
1475
- "text-disabled": "rgba(55, 65, 81, 0.48)",
1476
- "text-inverse": primitiveColors.white,
1477
- "text-link": primitiveColors.blue[600],
1478
- "text-link-hover": primitiveColors.blue[700],
1479
- // Surface colors (surface-*) - for elevated containers
1480
- "surface-base": primitiveColors.white,
1481
- "surface-elevated": primitiveColors.neutral[50],
1482
- // slightly off-white for elevated cards
1483
- "surface-raised": primitiveColors.neutral[100],
1484
- // light gray for raised cards
1485
- "surface-overlay": primitiveColors.white,
1486
- "surface-input": primitiveColors.neutral[50],
1487
- // Input field background (filled variant)
1488
- // Deepest pressed pit (TD-surface-tokens, S-3) — light-mode counterpart of
1489
- // the dark ramp's `inset` plane; a placeholder pairing, not part of the
1490
- // dark-ramp investigation (surface.contract.test.ts is dark-mode only).
1491
- "surface-inset": primitiveColors.neutral[300],
1492
- // Background colors (background-*)
1493
- "background-base": "#EBEBEB",
1494
- "background-default": primitiveColors.white,
1495
- "background-subtle": primitiveColors.neutral[50],
1496
- // Frame/bezel chrome (TD-surface-tokens, S-3) — top bar + side nav shell,
1497
- // one step below `background-base` (darker still than `surface-inset`
1498
- // above, mirroring the dark-mode ordering). Placeholder pairing for light
1499
- // mode (see dark-mode note on `surface-inset` above).
1500
- "background-frame": primitiveColors.neutral[400],
1501
- // Border colors (border-*)
1502
- "border-default": "#E8E9EB",
1503
- "border-subtle": primitiveColors.neutral[100],
1504
- "border-strong": primitiveColors.neutral[300],
1505
- "border-prominent": primitiveColors.neutral[400],
1506
- // high-visibility divider
1507
- "border-focus": primitiveColors.blue[600],
1508
- "border-input": primitiveColors.neutral[300],
1509
- // Input field border
1510
- "border-input-hover": primitiveColors.neutral[400],
1511
- // Input field border on hover
1512
- "border-input-focus": primitiveColors.blue[600],
1513
- // Input field border on focus
1514
- "border-input-error": primitiveColors.red[600],
1515
- // Input field border on error
1516
- // Alpha hairline separators (surface-independent — composite toward black on
1517
- // light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
1518
- "hairline-subtle": "rgba(0, 0, 0, 0.06)",
1519
- "hairline-default": "rgba(0, 0, 0, 0.09)",
1520
- "hairline-strong": "rgba(0, 0, 0, 0.14)",
1521
- // Interactive states (interactive-*)
1522
- "interactive-hover": "rgba(55, 65, 81, 0.04)",
1523
- "interactive-focus": "rgba(55, 65, 81, 0.12)",
1524
- "interactive-active": "rgba(55, 65, 81, 0.16)",
1525
- "interactive-selected": "rgba(55, 65, 81, 0.08)",
1526
- "interactive-disabled": "rgba(55, 65, 81, 0.12)",
1527
- "interactive-disabled-text": "rgba(55, 65, 81, 0.26)",
1528
- // Divider
1529
- "divider": "#E8E9EB",
1530
- // Avatar default
1531
- "avatar-background": primitiveColors.neutral[500],
1532
- "avatar-text": primitiveColors.white
1533
- };
1534
- var semanticColorsDark = {
1535
- // Brand colors stay the same in dark mode
1536
- "brand-primary": primitiveRamps.orange[400],
1537
- "brand-primary-light": primitiveRamps.orange[300],
1538
- "brand-primary-dark": primitiveRamps.orange[500],
1539
- "brand-primary-subtle": "rgba(255, 121, 0, 0.12)",
1540
- "brand-primary-hover": primitiveRamps.orange[300],
1541
- "brand-primary-active": primitiveRamps.orange[200],
1542
- "brand-secondary": primitiveRamps.cyan[600],
1543
- "brand-secondary-light": primitiveRamps.cyan[500],
1544
- "brand-secondary-dark": primitiveRamps.cyan[700],
1545
- "brand-secondary-subtle": "rgba(48, 123, 155, 0.12)",
1546
- "brand-secondary-hover": primitiveRamps.cyan[500],
1547
- "brand-secondary-active": primitiveRamps.cyan[400],
1548
- // Text on brand backgrounds
1549
- "on-brand-primary": primitiveColors.white,
1550
- "on-brand-secondary": primitiveColors.white,
1551
- // Status colors
1552
- "status-success": primitiveRamps.green[300],
1553
- "status-success-light": primitiveRamps.green[200],
1554
- "status-success-dark": primitiveRamps.green[600],
1555
- "status-success-subtle": "rgba(46, 213, 115, 0.12)",
1556
- // Live-session accent — its OWN role, decoupled from success so the two can diverge
1557
- "status-live": primitiveRamps.green[300],
1558
- "status-live-muted": primitiveRamps.green[500],
1559
- "status-error": primitiveRamps.red[600],
1560
- "status-error-light": primitiveRamps.red[500],
1561
- "status-error-dark": primitiveRamps.red[700],
1562
- "status-error-subtle": "rgba(209, 67, 67, 0.12)",
1563
- "status-error-vivid": primitiveColors.redVivid[500],
1564
- "status-error-vivid-light": primitiveColors.redVivid[400],
1565
- "status-error-vivid-dark": primitiveColors.redVivid[700],
1566
- "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
1567
- "status-warning": primitiveRamps.amber[300],
1568
- "status-warning-light": primitiveRamps.amber[200],
1569
- "status-warning-dark": primitiveRamps.amber[500],
1570
- "status-warning-subtle": "rgba(249, 180, 21, 0.12)",
1571
- "status-info": primitiveRamps.blue[500],
1572
- "status-info-light": primitiveRamps.blue[300],
1573
- "status-info-dark": primitiveRamps.blue[600],
1574
- "status-info-subtle": "rgba(33, 150, 243, 0.12)",
1575
- // Text on status backgrounds
1576
- "on-status-success": primitiveColors.white,
1577
- "on-status-error": primitiveColors.white,
1578
- "on-status-warning": primitiveColors.white,
1579
- "on-status-info": primitiveColors.white,
1580
- // Result/outcome indicators (result-*)
1581
- "result-improve": "#4caf50",
1582
- "result-improve-light": "rgba(76, 175, 80, 0.16)",
1583
- "result-improve-dark": "#248a24",
1584
- "result-degrade": "#ef5350",
1585
- "result-degrade-light": "rgba(239, 83, 80, 0.16)",
1586
- "result-degrade-dark": "#b30000",
1587
- "result-inconclusive": "#9E9A97",
1588
- "result-inconclusive-light": "rgba(158, 154, 151, 0.16)",
1589
- "result-neutral": primitiveColors.neutral[400],
1590
- // Text on result backgrounds
1591
- "on-result-improve": primitiveColors.white,
1592
- "on-result-degrade": primitiveColors.white,
1593
- "on-result-inconclusive": primitiveColors.white,
1594
- // Data visualization colors (same in dark mode for consistency)
1595
- "data-1": discreteRainbow[9],
1596
- "data-2": discreteRainbow[14],
1597
- "data-3": discreteRainbow[17],
1598
- "data-4": discreteRainbow[25],
1599
- "data-5": discreteRainbow[8],
1600
- "data-6": discreteRainbow[20],
1601
- "data-7": discreteRainbow[13],
1602
- "data-8": discreteRainbow[15],
1603
- "data-9": discreteRainbow[3],
1604
- "data-10": discreteRainbow[22],
1605
- // Text colors - inverted for dark mode
1606
- "text-primary": primitiveColors.neutral[100],
1607
- "text-secondary": primitiveColors.neutral[400],
1608
- "text-tertiary": primitiveColors.neutral[500],
1609
- "text-disabled": "rgba(255, 255, 255, 0.38)",
1610
- "text-inverse": primitiveColors.neutral[900],
1611
- "text-link": "#828DF8",
1612
- "text-link-hover": primitiveColors.blue[400],
1613
- // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
1614
- // S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
1615
- // `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
1616
- // and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
1617
- // tracks one plane above `surface-base`, same relative position as before the remap.
1618
- // `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
1619
- // so it takes the ramp's `background` (shell/frame) role, NOT `inset`. The ramp's
1620
- // deepest `inset` plane is now promoted to `surface-inset` below (S-3) — still no
1621
- // `SurfaceLevel` member for it (follow-up, alongside the elevation.ts -1/-2 pressed
1622
- // levels — parallel workstream, not this branch).
1623
- "surface-base": surfaceRampDark.base,
1624
- // main surface (#252321, L*13.5)
1625
- "surface-elevated": surfaceRampDark.elevated,
1626
- // elevated surface (#2C2A28, L*17 — nav/rail)
1627
- "surface-raised": surfaceRampDark.raised,
1628
- // raised surface (#31302F, L*20 — cards)
1629
- "surface-overlay": surfaceRampDark.overlay,
1630
- // overlay surface (#373635, L*22.5 — hero/popover)
1631
- "surface-input": surfaceRampDark.elevated,
1632
- // input surface (#2C2A28 — one plane above base)
1633
- "surface-inset": surfaceRampDark.inset,
1634
- // deepest pressed pit (#13100D, L*4.5 — sub-shell well)
1635
- // Background colors — same ramp, the frame/shell end of it.
1636
- "background-base": surfaceRampDark.background,
1637
- // shell / frame (#1C1916, L*9 — Surface level="background")
1638
- "background-default": surfaceRampDark.base,
1639
- // main background (#252321, L*13.5 — matches surface-base)
1640
- "background-subtle": surfaceRampDark.elevated,
1641
- // subtle background (#2C2A28, L*17 — matches surface-elevated)
1642
- // Frame/bezel chrome (S-3) — the top bar + side nav shell; one step BELOW
1643
- // `background-base`, darker even than `surface-inset` — it exits the content
1644
- // ramp entirely rather than sitting in it. See `backgroundFrameDark` in
1645
- // primitives.ts.
1646
- "background-frame": backgroundFrameDark,
1647
- // frame / bezel (#100D0A, L*3.79)
1648
- // Border colors — `border-subtle` (#1C1C1C) previously collided with
1649
- // `surface-raised` (also #1C1C1C, an invisible border on raised cards). The
1650
- // re-spaced ramp above moved `surface-raised` to #2D2C2B, so this value is now
1651
- // distinct from every surface hex without needing to change it — see
1652
- // `surface.contract.test.ts` R2/R4.
1653
- "border-default": primitiveColors.charcoal[400],
1654
- // default border
1655
- "border-subtle": primitiveColors.charcoal[500],
1656
- // subtle border
1657
- "border-strong": primitiveColors.charcoal[300],
1658
- // strong border
1659
- "border-prominent": primitiveColors.charcoal[200],
1660
- // high-visibility divider
1661
- "border-focus": "#828DF8",
1662
- "border-input": primitiveColors.neutral[600],
1663
- "border-input-hover": primitiveColors.neutral[500],
1664
- "border-input-focus": "#828DF8",
1665
- "border-input-error": primitiveColors.red[500],
1666
- // Alpha hairline separators — the primary separation cue (§4/S-2). Self-
1667
- // normalizing: composites toward white by ~the same amount on ANY plane, so
1668
- // one family works at every elevation instead of per-surface border tokens.
1669
- // Shadows are demoted to floating-overlay use only (not shipped as a fill
1670
- // separator here — see elevation.ts, follow-up S-4).
1671
- "hairline-subtle": "rgba(255, 255, 255, 0.06)",
1672
- "hairline-default": "rgba(255, 255, 255, 0.09)",
1673
- "hairline-strong": "rgba(255, 255, 255, 0.14)",
1674
- // Interactive states
1675
- "interactive-hover": "rgba(255, 255, 255, 0.04)",
1676
- "interactive-focus": "rgba(255, 255, 255, 0.12)",
1677
- "interactive-active": "rgba(255, 255, 255, 0.16)",
1678
- "interactive-selected": "rgba(255, 255, 255, 0.08)",
1679
- "interactive-disabled": "rgba(255, 255, 255, 0.12)",
1680
- "interactive-disabled-text": "rgba(255, 255, 255, 0.26)",
1681
- // Divider
1682
- "divider": primitiveColors.charcoal[400],
1683
- // divider color
1684
- // Avatar default
1685
- "avatar-background": primitiveColors.neutral[600],
1686
- "avatar-text": primitiveColors.white
1687
- };
1688
- function getSemanticColors(mode) {
1689
- return mode === "dark" ? semanticColorsDark : semanticColorsLight;
1695
+ ...props
1696
+ }) {
1697
+ return /* @__PURE__ */ jsx(
1698
+ reactNative.View,
1699
+ {
1700
+ className: cn("flex-row", className),
1701
+ style: [
1702
+ { height: 3, gap: 2, opacity: 0.5 },
1703
+ width != null ? { width } : { flex: 1 }
1704
+ ],
1705
+ accessibilityRole: "image",
1706
+ accessibilityLabel: "Planned set, not yet completed",
1707
+ testID: "placeholder-strip",
1708
+ ...props,
1709
+ children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
1710
+ reactNative.View,
1711
+ {
1712
+ style: {
1713
+ flex: 1,
1714
+ height: 3,
1715
+ backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1716
+ borderRadius: 1,
1717
+ minWidth: 4
1718
+ },
1719
+ accessibilityElementsHidden: true,
1720
+ testID: "placeholder-segment"
1721
+ },
1722
+ i
1723
+ ))
1724
+ }
1725
+ );
1726
+ }
1727
+ function PlaceholderStrip({
1728
+ mode = "single",
1729
+ segments,
1730
+ ...props
1731
+ }) {
1732
+ if (mode === "segmented") {
1733
+ return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
1734
+ }
1735
+ return /* @__PURE__ */ jsx(SingleStrip, { ...props });
1736
+ }
1737
+
1738
+ // src/components/icons/SvgIcon.tsx
1739
+ function SvgIcon({
1740
+ size = 24,
1741
+ color = "currentColor",
1742
+ strokeWidth = 2,
1743
+ fill = "none",
1744
+ title,
1745
+ children
1746
+ }) {
1747
+ return /* @__PURE__ */ jsxs(
1748
+ "svg",
1749
+ {
1750
+ width: size,
1751
+ height: size,
1752
+ viewBox: "0 0 24 24",
1753
+ fill,
1754
+ stroke: color,
1755
+ strokeWidth,
1756
+ strokeLinecap: "round",
1757
+ strokeLinejoin: "round",
1758
+ ...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
1759
+ children: [
1760
+ title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
1761
+ children
1762
+ ]
1763
+ }
1764
+ );
1690
1765
  }
1691
1766
 
1692
- // src/components/custom/Workout/BaseBadge.tsx
1767
+ // src/components/icons/icons.tsx
1768
+ function StarIcon(props) {
1769
+ 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" }) });
1770
+ }
1771
+ function ScaleIcon(props) {
1772
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1773
+ /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1774
+ /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1775
+ /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
1776
+ /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
1777
+ /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
1778
+ ] });
1779
+ }
1780
+ function AlertTriangleIcon(props) {
1781
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1782
+ /* @__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" }),
1783
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1784
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1785
+ ] });
1786
+ }
1787
+ function TrendingDownIcon(props) {
1788
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1789
+ /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
1790
+ /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
1791
+ ] });
1792
+ }
1793
+ function CircleSlashIcon(props) {
1794
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1795
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1796
+ /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
1797
+ ] });
1798
+ }
1799
+ function AwardIcon(props) {
1800
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1801
+ /* @__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" }),
1802
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
1803
+ ] });
1804
+ }
1805
+ function InfoIcon(props) {
1806
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1807
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1808
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
1809
+ /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1810
+ ] });
1811
+ }
1693
1812
  var t = getSemanticColors("dark");
1694
1813
  var baseBadgeSizeConfig = {
1695
1814
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
@@ -1697,7 +1816,7 @@ var baseBadgeSizeConfig = {
1697
1816
  lg: { fontSize: 12, paddingH: 10, paddingV: 4, iconSize: 14 }
1698
1817
  };
1699
1818
  var variantColors = {
1700
- plain: { backgroundColor: t["surface-raised"], borderColor: "#1F1F1F" },
1819
+ plain: { backgroundColor: t["surface-raised"], borderColor: t["hairline-default"] },
1701
1820
  pr: {
1702
1821
  backgroundColor: "rgba(255, 121, 0, 0.12)",
1703
1822
  borderColor: "rgba(255, 121, 0, 0.3)"
@@ -1904,6 +2023,7 @@ function RowStrip({ set }) {
1904
2023
  showInfo: false,
1905
2024
  height: 24,
1906
2025
  scale: "fixed",
2026
+ liveRepIndex: set.velocities.length - 1,
1907
2027
  set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
1908
2028
  zones
1909
2029
  }
@@ -1913,7 +2033,9 @@ function RowStrip({ set }) {
1913
2033
  return /* @__PURE__ */ jsx(
1914
2034
  VelocityStrip,
1915
2035
  {
1916
- variant: "mini",
2036
+ variant: "compact",
2037
+ height: 8,
2038
+ hideBaseline: true,
1917
2039
  set: { type: "straight", velocities, planned: plannedReps(set) },
1918
2040
  zones
1919
2041
  }
@@ -2048,321 +2170,10 @@ function SetsRepsLoad({
2048
2170
  /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
2049
2171
  /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
2050
2172
  /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
2051
- ]
2052
- }
2053
- );
2054
- }
2055
- function hexToRgb(hex) {
2056
- const cleaned = hex.replace("#", "");
2057
- let r, g, b;
2058
- if (cleaned.length === 3) {
2059
- r = parseInt(cleaned[0] + cleaned[0], 16);
2060
- g = parseInt(cleaned[1] + cleaned[1], 16);
2061
- b = parseInt(cleaned[2] + cleaned[2], 16);
2062
- } else if (cleaned.length === 6) {
2063
- r = parseInt(cleaned.slice(0, 2), 16);
2064
- g = parseInt(cleaned.slice(2, 4), 16);
2065
- b = parseInt(cleaned.slice(4, 6), 16);
2066
- } else {
2067
- return null;
2068
- }
2069
- return { r, g, b };
2070
- }
2071
- function rgbToHsv(r, g, b) {
2072
- const rNorm = r / 255;
2073
- const gNorm = g / 255;
2074
- const bNorm = b / 255;
2075
- const max = Math.max(rNorm, gNorm, bNorm);
2076
- const min = Math.min(rNorm, gNorm, bNorm);
2077
- const delta = max - min;
2078
- const v = max;
2079
- const s = max === 0 ? 0 : delta / max;
2080
- let h = 0;
2081
- if (delta !== 0) {
2082
- if (max === rNorm) {
2083
- h = 60 * ((gNorm - bNorm) / delta % 6);
2084
- } else if (max === gNorm) {
2085
- h = 60 * ((bNorm - rNorm) / delta + 2);
2086
- } else {
2087
- h = 60 * ((rNorm - gNorm) / delta + 4);
2088
- }
2089
- }
2090
- if (h < 0) h += 360;
2091
- return { h, s, v };
2092
- }
2093
- function hsvToRgb(h, s, v) {
2094
- const c = v * s;
2095
- const x = c * (1 - Math.abs(h / 60 % 2 - 1));
2096
- const m = v - c;
2097
- let rPrime, gPrime, bPrime;
2098
- if (h >= 0 && h < 60) {
2099
- rPrime = c;
2100
- gPrime = x;
2101
- bPrime = 0;
2102
- } else if (h >= 60 && h < 120) {
2103
- rPrime = x;
2104
- gPrime = c;
2105
- bPrime = 0;
2106
- } else if (h >= 120 && h < 180) {
2107
- rPrime = 0;
2108
- gPrime = c;
2109
- bPrime = x;
2110
- } else if (h >= 180 && h < 240) {
2111
- rPrime = 0;
2112
- gPrime = x;
2113
- bPrime = c;
2114
- } else if (h >= 240 && h < 300) {
2115
- rPrime = x;
2116
- gPrime = 0;
2117
- bPrime = c;
2118
- } else {
2119
- rPrime = c;
2120
- gPrime = 0;
2121
- bPrime = x;
2122
- }
2123
- return {
2124
- r: Math.round((rPrime + m) * 255),
2125
- g: Math.round((gPrime + m) * 255),
2126
- b: Math.round((bPrime + m) * 255)
2127
- };
2128
- }
2129
- function rgbToHex(r, g, b) {
2130
- const toHex = (n) => {
2131
- const clamped = Math.max(0, Math.min(255, Math.round(n)));
2132
- return clamped.toString(16).padStart(2, "0");
2133
- };
2134
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2135
- }
2136
- function lighten(hex, amount = 0.1) {
2137
- const rgb = hexToRgb(hex);
2138
- if (!rgb) return hex;
2139
- const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
2140
- const newV = Math.min(1, hsv.v + amount);
2141
- const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
2142
- return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
2143
- }
2144
- var shadowColors = {
2145
- // For charcoal surfaces (~#2C2C2C to #3C3C3C)
2146
- charcoal: {
2147
- dark: "rgba(0, 0, 0, 0.5)",
2148
- light: "rgba(255, 255, 255, 0.12)",
2149
- // brighter for crisp rim light
2150
- darker: "rgba(0, 0, 0, 0.6)"
2151
- },
2152
- // For neutral dark surfaces (~#1F2937)
2153
- neutralDark: {
2154
- dark: "rgba(0, 0, 0, 0.4)",
2155
- light: "rgba(255, 255, 255, 0.08)",
2156
- darker: "rgba(0, 0, 0, 0.5)"
2157
- },
2158
- // For light surfaces (white/light gray backgrounds)
2159
- neutralLight: {
2160
- dark: "rgba(0, 0, 0, 0.15)",
2161
- // Visible dark shadow for depth on light backgrounds
2162
- light: "rgba(255, 255, 255, 1)",
2163
- // Bright highlight for raised effect (visible on light gray)
2164
- darker: "rgba(0, 0, 0, 0.2)"
2165
- // Deeper shadow for pressed states
2166
- }
2167
- };
2168
- var intensityConfig = {
2169
- subtle: { offset: 1, blur: 2 },
2170
- // very light, barely visible
2171
- medium: { offset: 2, blur: 4 },
2172
- // noticeable but restrained
2173
- strong: { offset: 2, blur: 5 }
2174
- // slightly deeper blur, same offset as medium
2175
- };
2176
- function createRaisedShadow(surface = "charcoal", intensity = "medium") {
2177
- const colors = shadowColors[surface];
2178
- const { offset, blur } = intensityConfig[intensity];
2179
- const rimBlur = 1;
2180
- return reactNative.Platform.select({
2181
- web: {
2182
- boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2183
- },
2184
- default: {
2185
- // React Native shadow approximation (limited compared to web)
2186
- shadowColor: "#000",
2187
- shadowOffset: { width: offset, height: offset },
2188
- shadowOpacity: 0.3,
2189
- shadowRadius: blur / 2,
2190
- elevation: offset
2191
- }
2192
- });
2193
- }
2194
- function createPressedShadow(surface = "charcoal", intensity = "medium") {
2195
- const colors = shadowColors[surface];
2196
- const { offset, blur } = intensityConfig[intensity];
2197
- const rimOffset = 1;
2198
- const rimBlur = 1;
2199
- return reactNative.Platform.select({
2200
- web: {
2201
- boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
2202
- },
2203
- default: {
2204
- // React Native doesn't support inset shadows, approximate with darker bg
2205
- shadowOpacity: 0,
2206
- elevation: 0
2207
- }
2208
- });
2209
- }
2210
- function createFlatShadow() {
2211
- return reactNative.Platform.select({
2212
- web: {
2213
- boxShadow: "none"
2214
- },
2215
- default: {
2216
- shadowOpacity: 0,
2217
- elevation: 0
2218
- }
2219
- });
2220
- }
2221
- function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
2222
- const colors = shadowColors[surface];
2223
- const { offset, blur } = intensityConfig[intensity];
2224
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2225
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2226
- const rimBlur = 1;
2227
- return reactNative.Platform.select({
2228
- web: {
2229
- // Keep same offsets as normal state for rim to prevent "shrinking" effect
2230
- boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2231
- },
2232
- default: {
2233
- shadowColor: "#000",
2234
- shadowOffset: { width: hoverOffset, height: hoverOffset },
2235
- shadowOpacity: 0.2,
2236
- shadowRadius: hoverBlur / 2,
2237
- elevation: hoverOffset
2238
- }
2239
- });
2240
- }
2241
- function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
2242
- const colors = shadowColors[surface];
2243
- const { offset, blur } = intensityConfig[intensity];
2244
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2245
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2246
- const rimBlur = 1;
2247
- return reactNative.Platform.select({
2248
- web: {
2249
- // Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
2250
- boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
2251
- },
2252
- default: {
2253
- shadowOpacity: 0,
2254
- elevation: 0
2255
- }
2256
- });
2257
- }
2258
- ({
2259
- // Charcoal surface shadows (for dark toolbars, cards)
2260
- charcoal: {
2261
- raised: {
2262
- subtle: createRaisedShadow("charcoal", "subtle"),
2263
- medium: createRaisedShadow("charcoal", "medium"),
2264
- strong: createRaisedShadow("charcoal", "strong")
2265
- },
2266
- raisedHover: {
2267
- subtle: createRaisedHoverShadow("charcoal", "subtle"),
2268
- medium: createRaisedHoverShadow("charcoal", "medium"),
2269
- strong: createRaisedHoverShadow("charcoal", "strong")
2270
- },
2271
- pressed: {
2272
- subtle: createPressedShadow("charcoal", "subtle"),
2273
- medium: createPressedShadow("charcoal", "medium"),
2274
- strong: createPressedShadow("charcoal", "strong")
2275
- },
2276
- pressedHover: {
2277
- subtle: createPressedHoverShadow("charcoal", "subtle"),
2278
- medium: createPressedHoverShadow("charcoal", "medium"),
2279
- strong: createPressedHoverShadow("charcoal", "strong")
2280
- },
2281
- flat: createFlatShadow()
2282
- },
2283
- // Neutral dark surface shadows
2284
- neutralDark: {
2285
- raised: {
2286
- subtle: createRaisedShadow("neutralDark", "subtle"),
2287
- medium: createRaisedShadow("neutralDark", "medium"),
2288
- strong: createRaisedShadow("neutralDark", "strong")
2289
- },
2290
- raisedHover: {
2291
- subtle: createRaisedHoverShadow("neutralDark", "subtle"),
2292
- medium: createRaisedHoverShadow("neutralDark", "medium"),
2293
- strong: createRaisedHoverShadow("neutralDark", "strong")
2294
- },
2295
- pressed: {
2296
- subtle: createPressedShadow("neutralDark", "subtle"),
2297
- medium: createPressedShadow("neutralDark", "medium"),
2298
- strong: createPressedShadow("neutralDark", "strong")
2299
- },
2300
- pressedHover: {
2301
- subtle: createPressedHoverShadow("neutralDark", "subtle"),
2302
- medium: createPressedHoverShadow("neutralDark", "medium"),
2303
- strong: createPressedHoverShadow("neutralDark", "strong")
2304
- },
2305
- flat: createFlatShadow()
2306
- },
2307
- // Neutral light surface shadows
2308
- neutralLight: {
2309
- raised: {
2310
- subtle: createRaisedShadow("neutralLight", "subtle"),
2311
- medium: createRaisedShadow("neutralLight", "medium"),
2312
- strong: createRaisedShadow("neutralLight", "strong")
2313
- },
2314
- raisedHover: {
2315
- subtle: createRaisedHoverShadow("neutralLight", "subtle"),
2316
- medium: createRaisedHoverShadow("neutralLight", "medium"),
2317
- strong: createRaisedHoverShadow("neutralLight", "strong")
2318
- },
2319
- pressed: {
2320
- subtle: createPressedShadow("neutralLight", "subtle"),
2321
- medium: createPressedShadow("neutralLight", "medium"),
2322
- strong: createPressedShadow("neutralLight", "strong")
2323
- },
2324
- pressedHover: {
2325
- subtle: createPressedHoverShadow("neutralLight", "subtle"),
2326
- medium: createPressedHoverShadow("neutralLight", "medium"),
2327
- strong: createPressedHoverShadow("neutralLight", "strong")
2328
- },
2329
- flat: createFlatShadow()
2330
- }
2331
- });
2332
-
2333
- // src/utils/colors.ts
2334
- function alpha(color, opacity) {
2335
- const clampedOpacity = Math.max(0, Math.min(1, opacity));
2336
- if (color.startsWith("#")) {
2337
- const hex = color.slice(1);
2338
- let r, g, b;
2339
- if (hex.length === 3) {
2340
- r = parseInt(hex[0] + hex[0], 16);
2341
- g = parseInt(hex[1] + hex[1], 16);
2342
- b = parseInt(hex[2] + hex[2], 16);
2343
- } else if (hex.length === 6) {
2344
- r = parseInt(hex.slice(0, 2), 16);
2345
- g = parseInt(hex.slice(2, 4), 16);
2346
- b = parseInt(hex.slice(4, 6), 16);
2347
- } else {
2348
- return color;
2173
+ ]
2349
2174
  }
2350
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2351
- }
2352
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
2353
- if (rgbMatch) {
2354
- const [, r, g, b] = rgbMatch;
2355
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2356
- }
2357
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
2358
- if (rgbaMatch) {
2359
- const [, r, g, b] = rgbaMatch;
2360
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2361
- }
2362
- return color;
2175
+ );
2363
2176
  }
2364
-
2365
- // src/components/custom/Workout/TempoDisplay.tsx
2366
2177
  var t3 = getSemanticColors("dark");
2367
2178
  var INTER = "Inter, sans-serif";
2368
2179
  var TEXT_TERTIARY = "#6B7280";
@@ -2444,7 +2255,7 @@ function LiveTempoCell({
2444
2255
  alignItems: "center",
2445
2256
  justifyContent: "center"
2446
2257
  };
2447
- const num = (c, text) => /* @__PURE__ */ jsx(reactNative.Text, { style: { fontFamily: MONO, fontSize, color: c, fontWeight: "700" }, children: text });
2258
+ const num = (c2, text) => /* @__PURE__ */ jsx(reactNative.Text, { style: { fontFamily: MONO, fontSize, color: c2, fontWeight: "700" }, children: text });
2448
2259
  if (status === "upcoming") {
2449
2260
  const targetText = targetMs != null ? (targetMs / 1e3).toFixed(1) : String(value);
2450
2261
  return /* @__PURE__ */ jsx(reactNative.View, { style: wrap, children: num(color, targetText) });
@@ -2746,6 +2557,243 @@ function ExerciseHeading({
2746
2557
  )
2747
2558
  ] });
2748
2559
  }
2560
+ var PULSE_HALF_MS = 950;
2561
+ var PULSE_MIN_OPACITY = 0.45;
2562
+ var SEGMENTED_BAR_GAP = 5;
2563
+ function usePulse(active) {
2564
+ const [value] = react.useState(() => new reactNative.Animated.Value(0));
2565
+ react.useEffect(() => {
2566
+ if (!active) return;
2567
+ const loop = reactNative.Animated.loop(
2568
+ reactNative.Animated.sequence([
2569
+ reactNative.Animated.timing(value, {
2570
+ toValue: 1,
2571
+ duration: PULSE_HALF_MS,
2572
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2573
+ useNativeDriver: false
2574
+ }),
2575
+ reactNative.Animated.timing(value, {
2576
+ toValue: 0,
2577
+ duration: PULSE_HALF_MS,
2578
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2579
+ useNativeDriver: false
2580
+ })
2581
+ ])
2582
+ );
2583
+ loop.start();
2584
+ return () => loop.stop();
2585
+ }, [active, value]);
2586
+ return value;
2587
+ }
2588
+ function SegmentedBar({
2589
+ segments,
2590
+ height = 8,
2591
+ gap = SEGMENTED_BAR_GAP,
2592
+ radius = 2,
2593
+ marker = null,
2594
+ segmentTestID,
2595
+ style,
2596
+ ...props
2597
+ }) {
2598
+ const pulse = usePulse(segments.some((s) => s.pulse));
2599
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
2600
+ segments.map((seg, i) => {
2601
+ const fillStyle = {
2602
+ width: `${(seg.fill ?? 1) * 100}%`,
2603
+ height: "100%",
2604
+ backgroundColor: seg.color,
2605
+ opacity: seg.opacity
2606
+ };
2607
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
2608
+ return /* @__PURE__ */ jsx(
2609
+ reactNative.View,
2610
+ {
2611
+ style: {
2612
+ flex: seg.weight ?? 1,
2613
+ minWidth: 0,
2614
+ height: "100%",
2615
+ marginLeft: seg.leadingGap,
2616
+ borderRadius: radius,
2617
+ overflow: "hidden"
2618
+ },
2619
+ children: seg.pulse ? /* @__PURE__ */ jsx(
2620
+ reactNative.Animated.View,
2621
+ {
2622
+ style: {
2623
+ ...fillStyle,
2624
+ ...seg.pulseColor ? {
2625
+ backgroundColor: pulse.interpolate({
2626
+ inputRange: [0, 1],
2627
+ outputRange: seg.pulseColor
2628
+ })
2629
+ } : {
2630
+ opacity: pulse.interpolate({
2631
+ inputRange: [0, 1],
2632
+ outputRange: [PULSE_MIN_OPACITY, 1]
2633
+ })
2634
+ }
2635
+ },
2636
+ accessibilityElementsHidden: true,
2637
+ testID
2638
+ }
2639
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
2640
+ },
2641
+ i
2642
+ );
2643
+ }),
2644
+ marker ? /* @__PURE__ */ jsx(
2645
+ reactNative.View,
2646
+ {
2647
+ style: {
2648
+ position: "absolute",
2649
+ left: `${marker.position * 100}%`,
2650
+ top: 0,
2651
+ bottom: 0,
2652
+ width: 2,
2653
+ backgroundColor: marker.color
2654
+ },
2655
+ accessibilityElementsHidden: true,
2656
+ testID: "segmented-bar-marker"
2657
+ }
2658
+ ) : null
2659
+ ] });
2660
+ }
2661
+
2662
+ // src/components/custom/Workout/SetBar.tsx
2663
+ var SET_STRIP_ZONES = {
2664
+ slow: primitiveRamps.red[600],
2665
+ moderate: primitiveRamps.orange[400],
2666
+ fast: primitiveRamps.amber[300],
2667
+ fastest: primitiveRamps.green[300]
2668
+ };
2669
+ var TODO_COLOR = greyRamp[900];
2670
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
2671
+ var NOTCH_GAP = 2;
2672
+ var CLUSTER_GAP = 3;
2673
+ var MYO_UPCOMING_CLUSTERS = 3;
2674
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
2675
+ var MYO_UPCOMING_FADE = 0.6;
2676
+ var PULSE_RANGE = {
2677
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
2678
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
2679
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
2680
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
2681
+ };
2682
+ function velocityZoneColor(v) {
2683
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
2684
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
2685
+ if (v < 1) return SET_STRIP_ZONES.fast;
2686
+ return SET_STRIP_ZONES.fastest;
2687
+ }
2688
+ function chunkedSegments(chunks, gap) {
2689
+ return chunks.flatMap(
2690
+ (vels, ci) => vels.map((v, ri) => ({
2691
+ color: velocityZoneColor(v),
2692
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
2693
+ }))
2694
+ );
2695
+ }
2696
+ function rangeSegments(floor, max, doneVels) {
2697
+ return Array.from({ length: max }, (_, i) => {
2698
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
2699
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
2700
+ });
2701
+ }
2702
+ function myoUpcomingSegments(activationLen, clusterCount) {
2703
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
2704
+ const trail = Array.from(
2705
+ { length: clusterCount },
2706
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
2707
+ color: SET_STRIP_VARIABLE_COLOR,
2708
+ opacity: MYO_UPCOMING_FADE ** ci,
2709
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
2710
+ }))
2711
+ ).flat();
2712
+ return [...activation, ...trail];
2713
+ }
2714
+ function setSegments(set) {
2715
+ if (set.status === "todo") {
2716
+ return [{ color: TODO_COLOR }];
2717
+ }
2718
+ if (set.status === "done") {
2719
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
2720
+ }
2721
+ if (set.status === "range") {
2722
+ return rangeSegments(set.floor, set.max, set.doneVels);
2723
+ }
2724
+ if (set.status === "drop") {
2725
+ return chunkedSegments(set.subloads, NOTCH_GAP);
2726
+ }
2727
+ if (set.status === "myo") {
2728
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
2729
+ }
2730
+ if (set.status === "myo-upcoming") {
2731
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
2732
+ }
2733
+ const performed = set.velocities.map((v) => {
2734
+ const color = velocityZoneColor(v);
2735
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
2736
+ });
2737
+ const remaining = Math.max(0, set.planned - set.velocities.length);
2738
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
2739
+ }
2740
+ function setBarSegmentTestID(seg) {
2741
+ if (seg.pulse) return "set-strip-pulse";
2742
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
2743
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
2744
+ }
2745
+ function SetBar({ set, height = 8 }) {
2746
+ return /* @__PURE__ */ jsx(
2747
+ SegmentedBar,
2748
+ {
2749
+ segments: setSegments(set),
2750
+ height,
2751
+ gap: 0,
2752
+ radius: 0,
2753
+ segmentTestID: setBarSegmentTestID,
2754
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
2755
+ testID: "set-strip-set"
2756
+ }
2757
+ );
2758
+ }
2759
+
2760
+ // src/components/custom/Workout/SetStrip.tsx
2761
+ var SET_STRIP_GAP = 5;
2762
+ function setCategory(set) {
2763
+ switch (set.status) {
2764
+ case "done":
2765
+ case "drop":
2766
+ case "myo":
2767
+ return "done";
2768
+ case "active":
2769
+ return "active";
2770
+ case "range":
2771
+ return set.doneVels.length > 0 ? "active" : "upcoming";
2772
+ case "todo":
2773
+ case "myo-upcoming":
2774
+ return "upcoming";
2775
+ }
2776
+ }
2777
+ function describeSets(sets) {
2778
+ const done = sets.filter((s) => setCategory(s) === "done").length;
2779
+ const active = sets.filter((s) => setCategory(s) === "active").length;
2780
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
2781
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
2782
+ }
2783
+ function SetStrip({ sets, height = 8, className, ...props }) {
2784
+ return /* @__PURE__ */ jsx(
2785
+ reactNative.View,
2786
+ {
2787
+ className,
2788
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
2789
+ accessibilityRole: "image",
2790
+ accessibilityLabel: describeSets(sets),
2791
+ testID: "set-strip",
2792
+ ...props,
2793
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
2794
+ }
2795
+ );
2796
+ }
2749
2797
 
2750
2798
  // src/components/custom/Workout/ExerciseCardHeading.tsx
2751
2799
  function ExerciseCardHeading({
@@ -2888,7 +2936,9 @@ function CollapsedCard({
2888
2936
  {
2889
2937
  velocities,
2890
2938
  zones: velocityZones,
2891
- variant: "mini",
2939
+ variant: "compact",
2940
+ height: 8,
2941
+ hideBaseline: true,
2892
2942
  testID: `exercise-card-velocity-strip-${i}`
2893
2943
  },
2894
2944
  i
@@ -2908,7 +2958,7 @@ function CollapsedCard({
2908
2958
  );
2909
2959
  }
2910
2960
  var DARK2 = getSemanticColors("dark");
2911
- var BODY_DIVIDER = DARK2["border-subtle"];
2961
+ var BODY_DIVIDER = DARK2["hairline-subtle"];
2912
2962
  function deriveHeaderSetStates(sets) {
2913
2963
  return sets.map((set) => {
2914
2964
  switch (set.state) {
@@ -2942,7 +2992,7 @@ function ExpandedCard({
2942
2992
  return /* @__PURE__ */ jsxs(
2943
2993
  reactNative.View,
2944
2994
  {
2945
- className: "bg-surface-elevated border-border",
2995
+ className: "bg-surface-elevated border-hairline",
2946
2996
  style: {
2947
2997
  borderWidth: 1,
2948
2998
  ...borderRadius,
@@ -3075,7 +3125,7 @@ function resolveColor(token, mode = "dark") {
3075
3125
 
3076
3126
  // src/components/custom/Workout/InputBar.tsx
3077
3127
  var BRAND_PRIMARY2 = getSemanticColors("dark")["brand-primary"];
3078
- var INPUT_CLASSNAME = "bg-surface-raised border-border-strong text-text-primary";
3128
+ var INPUT_CLASSNAME = "bg-surface-raised border-hairline-strong text-text-primary";
3079
3129
  var inputStyle = {
3080
3130
  fontSize: 14,
3081
3131
  fontWeight: "600",
@@ -3108,7 +3158,7 @@ function InputBar({
3108
3158
  style: {
3109
3159
  width: "100%",
3110
3160
  borderTopWidth: 1,
3111
- borderTopColor: resolveColor("border-default"),
3161
+ borderTopColor: resolveColor("hairline-default"),
3112
3162
  paddingTop: 10,
3113
3163
  paddingHorizontal: 16,
3114
3164
  paddingBottom: 12,
@@ -3323,7 +3373,7 @@ function CircularProgress({
3323
3373
  cy: center,
3324
3374
  r: radius,
3325
3375
  fill: "none",
3326
- stroke: "var(--color-border-default)",
3376
+ stroke: "var(--color-hairline-default)",
3327
3377
  strokeWidth
3328
3378
  }
3329
3379
  ),
@@ -3578,7 +3628,7 @@ function RestTimer({
3578
3628
  style: {
3579
3629
  width: "100%",
3580
3630
  borderTopWidth: 1,
3581
- borderTopColor: resolveColor("border-default"),
3631
+ borderTopColor: resolveColor("hairline-default"),
3582
3632
  paddingVertical: 12,
3583
3633
  paddingHorizontal: 16
3584
3634
  },
@@ -3647,7 +3697,7 @@ function RestTimer({
3647
3697
  /* @__PURE__ */ jsx(
3648
3698
  reactNative.View,
3649
3699
  {
3650
- className: "bg-border",
3700
+ className: "bg-hairline",
3651
3701
  style: {
3652
3702
  height: 3,
3653
3703
  borderRadius: 2,
@@ -3867,7 +3917,7 @@ function WorkoutHeader({
3867
3917
  /* @__PURE__ */ jsx(
3868
3918
  reactNative.View,
3869
3919
  {
3870
- className: "bg-border",
3920
+ className: "bg-hairline",
3871
3921
  style: { height: 4, borderRadius: 2 },
3872
3922
  testID: "active-workout-page-progress-track",
3873
3923
  children: /* @__PURE__ */ jsx(
@@ -3892,7 +3942,7 @@ function ExerciseList({ groups, focusedId, onToggle }) {
3892
3942
  return /* @__PURE__ */ jsx(
3893
3943
  reactNative.View,
3894
3944
  {
3895
- className: "bg-surface-elevated border-border",
3945
+ className: "bg-surface-elevated border-hairline",
3896
3946
  style: {
3897
3947
  borderWidth: 1,
3898
3948
  borderRadius: 12
@@ -3974,7 +4024,7 @@ function ActiveWorkoutPage({
3974
4024
  (showRest || showInput) && /* @__PURE__ */ jsx(
3975
4025
  reactNative.View,
3976
4026
  {
3977
- style: { borderTopWidth: 1, borderTopColor: resolveColor("border-default") },
4027
+ style: { borderTopWidth: 1, borderTopColor: resolveColor("hairline-default") },
3978
4028
  testID: "active-workout-page-bottom-bar",
3979
4029
  children: showRest && rest != null ? /* @__PURE__ */ jsx(
3980
4030
  RestTimer,
@@ -4038,7 +4088,13 @@ var elevationSystem = {
4038
4088
  // Level 1: Subtle elevation (outline cards, subtle containers)
4039
4089
  [1]: {
4040
4090
  name: "subtle",
4041
- colorAdjustment: 0.01,
4091
+ // The whole ladder was re-spaced in TD-07.16. It used to run
4092
+ // 0.01/0.02/0.04/0.06/0.08, where the first step was ~2 RGB values — under
4093
+ // the perceptual floor, so level 1 existed in the type system and nowhere on
4094
+ // screen. With drop-shadow now gated off below FLOATING_ELEVATION_MIN, TONE
4095
+ // is the only thing separating 0-3, so every step has to actually land.
4096
+ // Even 0.025 spacing, verified in elevation.test.ts as ΔL* > 1 per step.
4097
+ colorAdjustment: 0.025,
4042
4098
  shadowIntensity: "subtle",
4043
4099
  shadowStyle: "raised",
4044
4100
  description: "Subtle elevation (outline cards, subtle containers)"
@@ -4046,7 +4102,7 @@ var elevationSystem = {
4046
4102
  // Level 2: Standard elevation (default cards, buttons)
4047
4103
  [2]: {
4048
4104
  name: "standard",
4049
- colorAdjustment: 0.02,
4105
+ colorAdjustment: 0.05,
4050
4106
  shadowIntensity: "medium",
4051
4107
  shadowStyle: "raised",
4052
4108
  description: "Standard elevation (default cards, raised buttons)"
@@ -4054,7 +4110,7 @@ var elevationSystem = {
4054
4110
  // Level 3: Prominent elevation (important cards, modals)
4055
4111
  [3]: {
4056
4112
  name: "prominent",
4057
- colorAdjustment: 0.04,
4113
+ colorAdjustment: 0.075,
4058
4114
  shadowIntensity: "strong",
4059
4115
  shadowStyle: "raised",
4060
4116
  description: "Prominent elevation (important cards, modals, dropdowns)"
@@ -4062,7 +4118,7 @@ var elevationSystem = {
4062
4118
  // Level 4: High elevation (overlays, popovers)
4063
4119
  [4]: {
4064
4120
  name: "high",
4065
- colorAdjustment: 0.06,
4121
+ colorAdjustment: 0.1,
4066
4122
  shadowIntensity: "strong",
4067
4123
  shadowStyle: "raised",
4068
4124
  description: "High elevation (overlays, popovers, tooltips)"
@@ -4070,7 +4126,7 @@ var elevationSystem = {
4070
4126
  // Level 5: Floating (toasts, floating action buttons)
4071
4127
  [5]: {
4072
4128
  name: "floating",
4073
- colorAdjustment: 0.08,
4129
+ colorAdjustment: 0.13,
4074
4130
  shadowIntensity: "strong",
4075
4131
  shadowStyle: "raised",
4076
4132
  description: "Floating elevation (toasts, floating action buttons, highest z-index)"
@@ -4186,12 +4242,12 @@ function calculateElevationShadow(baseColor, level, theme, isHovered = false) {
4186
4242
  const elevatedSurfaceColor = calculateElevationSurface(baseColor, level);
4187
4243
  const rimLightColor = calculateRimLightColor(baseColor, level);
4188
4244
  const darkShadowColors = calculateDarkShadowColors(elevatedSurfaceColor);
4189
- const intensityConfig2 = {
4245
+ const intensityConfig = {
4190
4246
  subtle: { offset: 1, blur: 2 },
4191
4247
  medium: { offset: 2, blur: 4 },
4192
4248
  strong: { offset: 2, blur: 5 }
4193
4249
  };
4194
- const { offset, blur } = intensityConfig2[config.shadowIntensity];
4250
+ const { offset, blur } = intensityConfig[config.shadowIntensity];
4195
4251
  const rimConfig = getRimLightConfig();
4196
4252
  if (config.shadowStyle === "pressed") {
4197
4253
  if (isHovered) {
@@ -4247,12 +4303,13 @@ function calculateElevationShadow(baseColor, level, theme, isHovered = false) {
4247
4303
  });
4248
4304
  }
4249
4305
  }
4306
+ var FLOATING_ELEVATION_MIN = 4;
4250
4307
  function getElevationShadow(baseColor, level, theme, isHovered = false) {
4251
4308
  const cacheKey = getShadowCacheKey(baseColor, level, theme, isHovered);
4252
4309
  if (elevationShadowCache.has(cacheKey)) {
4253
4310
  return elevationShadowCache.get(cacheKey);
4254
4311
  }
4255
- const result = calculateElevationShadow(baseColor, level, theme, isHovered);
4312
+ const result = level > 0 && level < FLOATING_ELEVATION_MIN ? {} : calculateElevationShadow(baseColor, level, theme, isHovered);
4256
4313
  if (elevationShadowCache.size >= MAX_SHADOW_CACHE_SIZE) {
4257
4314
  const firstKey = elevationShadowCache.keys().next().value;
4258
4315
  elevationShadowCache.delete(firstKey);
@@ -4338,12 +4395,12 @@ function useTheme() {
4338
4395
  var variantStyles = {
4339
4396
  elevated: "",
4340
4397
  // Will be set dynamically via elevation system
4341
- outline: "border-2 border-border-strong",
4398
+ outline: "border-2 border-hairline-strong",
4342
4399
  // Thicker border with stronger contrast
4343
4400
  filled: "",
4344
4401
  // Will be set dynamically via elevation system
4345
- accent: "border border-border",
4346
- subtle: "border border-border-subtle"
4402
+ accent: "border border-hairline",
4403
+ subtle: "border border-hairline-subtle"
4347
4404
  };
4348
4405
  function Card({
4349
4406
  variant = "elevated",
@@ -4463,7 +4520,7 @@ var solidVariantColors = {
4463
4520
  success: t5["status-success"],
4464
4521
  warning: t5["status-warning"],
4465
4522
  error: t5["status-error"],
4466
- neutral: "#6B7280"
4523
+ neutral: greyRamp[500]
4467
4524
  };
4468
4525
  var ringVariantStyles = {
4469
4526
  "on-track": {
@@ -4501,10 +4558,10 @@ var iconColors = {
4501
4558
  success: "#0A5C52",
4502
4559
  warning: "#6B4000",
4503
4560
  error: "#5C1A1A",
4504
- neutral: "#D1D5DB",
4561
+ neutral: greyRamp[200],
4505
4562
  "on-track": t5["status-success"],
4506
4563
  deviation: t5["status-warning"],
4507
- future: "#6B7280"
4564
+ future: greyRamp[500]
4508
4565
  };
4509
4566
  var sizeDimensions = {
4510
4567
  sm: { size: 8, showIcon: false },
@@ -4573,7 +4630,7 @@ function StatusDot({
4573
4630
  /* @__PURE__ */ jsx(
4574
4631
  reactNative.Text,
4575
4632
  {
4576
- style: { fontSize: 12, lineHeight: 16, color: "#9CA3AF" },
4633
+ style: { fontSize: 12, lineHeight: 16, color: greyRamp[400] },
4577
4634
  accessibilityElementsHidden: true,
4578
4635
  children: label
4579
4636
  }
@@ -5046,11 +5103,11 @@ function ChartLine({
5046
5103
  dashed,
5047
5104
  testID
5048
5105
  }) {
5049
- return /* @__PURE__ */ jsx(jsxRuntime.Fragment, { children: coords.map((c, i) => {
5106
+ return /* @__PURE__ */ jsx(jsxRuntime.Fragment, { children: coords.map((c2, i) => {
5050
5107
  if (i === 0) return null;
5051
5108
  const prev = coords[i - 1];
5052
- const dx = c.x - prev.x;
5053
- const dy = c.y - prev.y;
5109
+ const dx = c2.x - prev.x;
5110
+ const dy = c2.y - prev.y;
5054
5111
  const length = Math.sqrt(dx * dx + dy * dy);
5055
5112
  const angle = Math.atan2(dy, dx) * (180 / Math.PI);
5056
5113
  return /* @__PURE__ */ jsx(
@@ -5253,21 +5310,21 @@ function StrengthTrendChart({
5253
5310
  }
5254
5311
  ),
5255
5312
  geometry.actual.map(
5256
- (c) => c.point.isPR ? /* @__PURE__ */ jsx(
5313
+ (c2) => c2.point.isPR ? /* @__PURE__ */ jsx(
5257
5314
  reactNative.Text,
5258
5315
  {
5259
5316
  accessibilityElementsHidden: true,
5260
5317
  testID: "strength-trend-chart-pr-star",
5261
5318
  style: {
5262
5319
  position: "absolute",
5263
- left: c.x - 7,
5264
- top: c.y - 20,
5320
+ left: c2.x - 7,
5321
+ top: c2.y - 20,
5265
5322
  fontSize: 12,
5266
5323
  color: STATUS_WARNING2
5267
5324
  },
5268
5325
  children: "\u2605"
5269
5326
  },
5270
- `star-${c.index}`
5327
+ `star-${c2.index}`
5271
5328
  ) : null
5272
5329
  )
5273
5330
  ]
@@ -5278,29 +5335,29 @@ function StrengthTrendChart({
5278
5335
  {
5279
5336
  style: { position: "absolute", top: 0, left: 0, width, height },
5280
5337
  testID: "strength-trend-chart-points",
5281
- children: geometry.actual.map((c) => {
5282
- const label = c.point.sessionLabel ?? c.point.date;
5338
+ children: geometry.actual.map((c2) => {
5339
+ const label = c2.point.sessionLabel ?? c2.point.date;
5283
5340
  return /* @__PURE__ */ jsx(
5284
5341
  reactNative.Pressable,
5285
5342
  {
5286
5343
  testID: "strength-trend-chart-point",
5287
5344
  accessibilityRole: "button",
5288
- accessibilityLabel: `${label}: ${formatValue(c.point.e1rm)} ${unit}${c.point.isPR ? ", personal record" : ""}`,
5345
+ accessibilityLabel: `${label}: ${formatValue(c2.point.e1rm)} ${unit}${c2.point.isPR ? ", personal record" : ""}`,
5289
5346
  onPress: () => {
5290
- setSelected(c.index);
5291
- onPointPress?.(c.point);
5347
+ setSelected(c2.index);
5348
+ onPointPress?.(c2.point);
5292
5349
  },
5293
5350
  style: {
5294
5351
  position: "absolute",
5295
- left: c.x - 4,
5296
- top: c.y - 4,
5352
+ left: c2.x - 4,
5353
+ top: c2.y - 4,
5297
5354
  width: 8,
5298
5355
  height: 8,
5299
5356
  borderRadius: 1,
5300
5357
  backgroundColor: BRAND_PRIMARY6
5301
5358
  }
5302
5359
  },
5303
- `point-${c.index}`
5360
+ `point-${c2.index}`
5304
5361
  );
5305
5362
  })
5306
5363
  }
@@ -5310,7 +5367,7 @@ function StrengthTrendChart({
5310
5367
  {
5311
5368
  testID: "strength-trend-chart-tooltip",
5312
5369
  accessibilityElementsHidden: true,
5313
- className: "bg-surface-elevated border-border-strong",
5370
+ className: "bg-surface-elevated border-hairline-strong",
5314
5371
  style: {
5315
5372
  position: "absolute",
5316
5373
  left: Math.max(
@@ -5881,7 +5938,7 @@ function TabBar({ active, onSelect }) {
5881
5938
  className: "flex-row",
5882
5939
  style: {
5883
5940
  borderBottomWidth: 1,
5884
- borderBottomColor: resolveColor("border-default")
5941
+ borderBottomColor: resolveColor("hairline-default")
5885
5942
  },
5886
5943
  accessibilityRole: "tablist",
5887
5944
  testID: "exercise-detail-page-tabs",
@@ -5932,7 +5989,7 @@ function StatStrip({ stats, unit }) {
5932
5989
  return /* @__PURE__ */ jsxs(
5933
5990
  reactNative.View,
5934
5991
  {
5935
- className: "bg-surface-elevated border-border",
5992
+ className: "bg-surface-elevated border-hairline",
5936
5993
  style: {
5937
5994
  flex: 1,
5938
5995
  padding: 12,
@@ -5982,7 +6039,7 @@ function SectionCard({
5982
6039
  return /* @__PURE__ */ jsxs(
5983
6040
  reactNative.View,
5984
6041
  {
5985
- className: "bg-surface-elevated border-border",
6042
+ className: "bg-surface-elevated border-hairline",
5986
6043
  style: {
5987
6044
  borderWidth: 1,
5988
6045
  borderRadius: 12,
@@ -6057,7 +6114,16 @@ function VbtBreakdown({
6057
6114
  children: set.label
6058
6115
  }
6059
6116
  ),
6060
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(VelocityStrip, { velocities: set.velocities, zones, variant: "mini" }) }),
6117
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(
6118
+ VelocityStrip,
6119
+ {
6120
+ velocities: set.velocities,
6121
+ zones,
6122
+ variant: "compact",
6123
+ height: 8,
6124
+ hideBaseline: true
6125
+ }
6126
+ ) }),
6061
6127
  /* @__PURE__ */ jsx(
6062
6128
  reactNative.Text,
6063
6129
  {
@@ -6084,7 +6150,7 @@ function VbtSummaryRow({ summary }) {
6084
6150
  return /* @__PURE__ */ jsx(reactNative.View, { className: "flex-row", style: { gap: 8 }, testID: "exercise-detail-page-vbt-summary", children: cells.map((cell) => /* @__PURE__ */ jsxs(
6085
6151
  reactNative.View,
6086
6152
  {
6087
- className: "bg-surface-elevated border-border",
6153
+ className: "bg-surface-elevated border-hairline",
6088
6154
  style: {
6089
6155
  flex: 1,
6090
6156
  padding: 12,
@@ -6476,7 +6542,7 @@ function Indicator({
6476
6542
  // src/components/ui/badge/Badge.tsx
6477
6543
  var variantColorStyles = {
6478
6544
  solid: {
6479
- default: "bg-border-strong text-text-inverse",
6545
+ default: "bg-hairline-strong text-text-inverse",
6480
6546
  primary: "bg-brand-primary text-text-inverse",
6481
6547
  secondary: "bg-brand-secondary text-text-inverse",
6482
6548
  success: "bg-status-success text-text-inverse",
@@ -6494,7 +6560,7 @@ var variantColorStyles = {
6494
6560
  info: "bg-status-info-subtle text-status-info"
6495
6561
  },
6496
6562
  outline: {
6497
- default: "border border-border-strong text-text-secondary",
6563
+ default: "border border-hairline-strong text-text-secondary",
6498
6564
  primary: "border border-brand-primary text-brand-primary",
6499
6565
  secondary: "border border-brand-secondary text-brand-secondary",
6500
6566
  success: "border border-status-success text-status-success",
@@ -6542,6 +6608,7 @@ function Badge({
6542
6608
  }
6543
6609
  );
6544
6610
  }
6611
+ var HAIRLINE_DEFAULT = getSemanticColors("dark")["hairline-default"];
6545
6612
  var t8 = getSemanticColors("dark");
6546
6613
  var statusBgColors = {
6547
6614
  completed: "rgba(46,213,115,0.15)",
@@ -6555,7 +6622,7 @@ var statusBorderStyles = {
6555
6622
  completed: { borderWidth: 1, borderColor: "rgba(46,213,115,0.3)" },
6556
6623
  current: { borderWidth: 1, borderColor: "rgba(255,121,0,0.3)" },
6557
6624
  next: { borderWidth: 1, borderColor: "rgba(255,121,0,0.4)" },
6558
- upcoming: { borderWidth: 1, borderColor: "#1F1F1F" },
6625
+ upcoming: { borderWidth: 1, borderColor: HAIRLINE_DEFAULT },
6559
6626
  missed: { borderWidth: 1, borderColor: "rgba(209,67,67,0.25)" },
6560
6627
  deload: { borderWidth: 1, borderColor: "rgba(186,41,150,0.3)" }
6561
6628
  };
@@ -6563,7 +6630,7 @@ var textColors = {
6563
6630
  completed: t8["status-success"],
6564
6631
  current: t8["brand-primary"],
6565
6632
  next: t8["brand-primary"],
6566
- upcoming: "#6B7280",
6633
+ upcoming: greyRamp[500],
6567
6634
  missed: "rgba(209,67,67,0.7)",
6568
6635
  deload: WORKOUT_PILL_DELOAD
6569
6636
  };
@@ -6974,10 +7041,11 @@ function WeekRow({
6974
7041
  }
6975
7042
 
6976
7043
  // src/components/custom/Workout/MesoCard.tsx
7044
+ var HAIRLINE_DEFAULT2 = getSemanticColors("dark")["hairline-default"];
6977
7045
  var BRAND_PRIMARY10 = getSemanticColors("dark")["brand-primary"];
6978
7046
  var BRAND_PRIMARY_DARK2 = MESO_ACCENT_GRADIENT_DARK;
6979
7047
  var BRAND_PRIMARY_LIGHT2 = MESO_ACCENT_GRADIENT_LIGHT;
6980
- var BORDER_DEFAULT = "#1F1F1F";
7048
+ var BORDER_DEFAULT = HAIRLINE_DEFAULT2;
6981
7049
  var ACCENT_STOPS2 = [BRAND_PRIMARY_DARK2, BRAND_PRIMARY10, BRAND_PRIMARY_LIGHT2];
6982
7050
  function heatmapColor(percentage) {
6983
7051
  const clamped = Math.max(0, Math.min(100, percentage));
@@ -7132,7 +7200,7 @@ function MesoCard({
7132
7200
  }
7133
7201
  var t10 = getSemanticColors("dark");
7134
7202
  var dotColorMap = {
7135
- untrained: "#2C2C2C",
7203
+ untrained: greyRamp[900],
7136
7204
  behind: t10["brand-secondary"],
7137
7205
  ontrack: t10["status-success"],
7138
7206
  target: t10["brand-primary"],
@@ -7145,12 +7213,12 @@ function MuscleGroupChip({
7145
7213
  className,
7146
7214
  ...props
7147
7215
  }) {
7148
- const dotColor = volumeStatus ? dotColorMap[volumeStatus] : "#2C2C2C";
7216
+ const dotColor = volumeStatus ? dotColorMap[volumeStatus] : greyRamp[900];
7149
7217
  const statusLabel = volumeStatus ?? "no status";
7150
7218
  const content = /* @__PURE__ */ jsxs(
7151
7219
  reactNative.View,
7152
7220
  {
7153
- className: ["bg-surface-raised border-border", className].filter(Boolean).join(" "),
7221
+ className: ["bg-surface-raised border-hairline", className].filter(Boolean).join(" "),
7154
7222
  style: {
7155
7223
  flexDirection: "row",
7156
7224
  alignItems: "center",
@@ -7210,11 +7278,12 @@ function MuscleGroupChip({
7210
7278
  }
7211
7279
 
7212
7280
  // src/components/custom/Workout/WorkoutCard.tsx
7281
+ var HAIRLINE_DEFAULT3 = getSemanticColors("dark")["hairline-default"];
7213
7282
  var t11 = getSemanticColors("dark");
7214
7283
  var COLORS = {
7215
7284
  statusSuccess: t11["status-success"],
7216
7285
  brandPrimary: t11["brand-primary"],
7217
- borderDefault: "#1F1F1F",
7286
+ borderDefault: HAIRLINE_DEFAULT3,
7218
7287
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
7219
7288
  };
7220
7289
  var statusBorderColors = {
@@ -7593,7 +7662,7 @@ function ProgramPlanningPage({
7593
7662
  /* @__PURE__ */ jsxs(
7594
7663
  reactNative.View,
7595
7664
  {
7596
- className: "bg-surface-elevated border-border",
7665
+ className: "bg-surface-elevated border-hairline",
7597
7666
  style: {
7598
7667
  borderWidth: 1,
7599
7668
  borderRadius: 12,