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