@titan-design/react-ui 0.9.3 → 0.11.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 (120) hide show
  1. package/dist/bodymap.js +78 -24
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +78 -24
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +429 -11
  6. package/dist/index.d.ts +429 -11
  7. package/dist/index.js +2386 -885
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2306 -836
  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 +1900 -1609
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1901 -1610
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +129 -34
  20. package/dist/theme/index.d.ts +129 -34
  21. package/dist/theme/index.js +95 -19
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +92 -20
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +88 -24
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +88 -24
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  31. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  32. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  33. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  34. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  35. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  36. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  37. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  38. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  39. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  40. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  41. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  42. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  43. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  45. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  46. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  47. package/src/components/custom/Fatigue/README.md +93 -0
  48. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  49. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  51. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  52. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  53. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  54. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  55. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  56. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  57. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  58. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  59. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  60. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  61. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  62. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  63. package/src/components/custom/Fatigue/index.ts +41 -0
  64. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  65. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  66. package/src/components/custom/Workout/BaseBadge.tsx +5 -5
  67. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  68. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  69. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  71. package/src/components/custom/Workout/README.md +62 -16
  72. package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
  73. package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
  74. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  75. package/src/components/custom/Workout/SessionHeader.tsx +2 -2
  76. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  77. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  78. package/src/components/custom/Workout/SessionRail.tsx +55 -18
  79. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  80. package/src/components/custom/Workout/SetRow.tsx +9 -2
  81. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
  82. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
  83. package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
  84. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  85. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  86. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  87. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  88. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  89. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  90. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  91. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
  92. package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
  93. package/src/components/custom/Workout/index.ts +3 -0
  94. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  95. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  96. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  97. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  98. package/src/components/custom/index.ts +1 -0
  99. package/src/components/ui/surface/Surface.stories.tsx +195 -1
  100. package/src/components/ui/surface/Surface.test.tsx +148 -9
  101. package/src/components/ui/surface/Surface.tsx +37 -9
  102. package/src/components/ui/surface/SurfaceContext.ts +48 -8
  103. package/src/components/ui/surface/index.ts +1 -0
  104. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  105. package/src/test/raw-color-patterns.test.ts +71 -0
  106. package/src/theme/ColorPalettes.stories.tsx +871 -0
  107. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  108. package/src/theme/bar-paper.ts +59 -0
  109. package/src/theme/color-stories.coverage.test.ts +130 -0
  110. package/src/theme/color-story-kit.tsx +170 -0
  111. package/src/theme/config.ts +12 -0
  112. package/src/theme/elevation.ts +90 -67
  113. package/src/theme/global.css +35 -9
  114. package/src/theme/tokens/primitives.ts +40 -0
  115. package/src/theme/tokens/semantic.ts +63 -14
  116. package/tailwind.config.js +8 -0
  117. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  118. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  119. package/src/theme/ColorSystem.stories.tsx +0 -323
  120. package/src/theme/Colors.stories.tsx +0 -323
package/dist/pages.js CHANGED
@@ -36,17 +36,27 @@ var primitiveColors = {
36
36
  // Charcoal scale (dark backgrounds)
37
37
  charcoal: {
38
38
  0: "#6E6E6E",
39
- 100: "#4C4C4C",
40
39
  200: "#3C3C3C",
41
40
  300: "#2C2C2C",
42
41
  400: "#1F1F1F",
43
- 500: "#1C1C1C",
44
- 600: "#191919",
45
- 700: "#161616",
46
- 900: "#101010"
47
- },
42
+ 500: "#1C1C1C"},
48
43
  // Pure colors
49
44
  white: "#FFFFFF"};
45
+ var surfaceRampDark = {
46
+ inset: "#13100D",
47
+ // L*4.5 — sub-shell well / pressed
48
+ background: "#1C1916",
49
+ // L*9 — shell / frame
50
+ base: "#252321",
51
+ // L*13.5 — main content plane
52
+ elevated: "#2C2A28",
53
+ // L*17 — nav / rail
54
+ raised: "#31302F",
55
+ // L*20 — cards
56
+ overlay: "#373635"
57
+ // L*22.5 — hero / popover
58
+ };
59
+ var backgroundFrameDark = "#100D0A";
50
60
  var primitiveRamps = {
51
61
  red: {
52
62
  50: "#FFF4F4",
@@ -171,275 +181,1077 @@ var WORKOUT_TOKENS = {
171
181
  fill: "#3A3A3A"
172
182
  }};
173
183
 
174
- // src/utils/workout-format.ts
175
- function roundRpe(rpe) {
176
- return Math.round(rpe * 2) / 2;
177
- }
178
- function roundWeight(weight) {
179
- return Math.round(weight);
180
- }
181
- function formatVelocity(velocity) {
182
- return velocity.toFixed(2);
184
+ // src/theme/tokens/semantic.ts
185
+ var semanticColorsLight = {
186
+ // Brand colors (brand-*)
187
+ "brand-primary": primitiveRamps.orange[400],
188
+ "brand-primary-light": primitiveRamps.orange[300],
189
+ "brand-primary-dark": primitiveRamps.orange[500],
190
+ "brand-primary-subtle": "rgba(255, 121, 0, 0.08)",
191
+ "brand-primary-hover": primitiveRamps.orange[500],
192
+ "brand-primary-active": primitiveRamps.orange[600],
193
+ "brand-secondary": primitiveRamps.cyan[600],
194
+ "brand-secondary-light": primitiveRamps.cyan[500],
195
+ "brand-secondary-dark": primitiveRamps.cyan[700],
196
+ "brand-secondary-subtle": "rgba(48, 123, 155, 0.08)",
197
+ "brand-secondary-hover": primitiveRamps.cyan[700],
198
+ "brand-secondary-active": primitiveRamps.cyan[800],
199
+ // Text on brand backgrounds (on-*)
200
+ "on-brand-primary": primitiveColors.white,
201
+ "on-brand-secondary": primitiveColors.white,
202
+ // Status colors (status-*)
203
+ "status-success": primitiveRamps.green[300],
204
+ "status-success-light": primitiveRamps.green[200],
205
+ "status-success-dark": primitiveRamps.green[600],
206
+ "status-success-subtle": primitiveRamps.green[50],
207
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
208
+ "status-live": primitiveRamps.green[300],
209
+ "status-live-muted": primitiveRamps.green[500],
210
+ "status-error": primitiveRamps.red[600],
211
+ "status-error-light": primitiveRamps.red[500],
212
+ "status-error-dark": primitiveRamps.red[700],
213
+ "status-error-subtle": primitiveRamps.red[50],
214
+ "status-error-vivid": primitiveColors.redVivid[500],
215
+ // vivid alert red
216
+ "status-error-vivid-light": primitiveColors.redVivid[400],
217
+ "status-error-vivid-dark": primitiveColors.redVivid[700],
218
+ "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
219
+ "status-warning": primitiveRamps.amber[300],
220
+ "status-warning-light": primitiveRamps.amber[200],
221
+ "status-warning-dark": primitiveRamps.amber[500],
222
+ "status-warning-subtle": primitiveRamps.amber[50],
223
+ "status-info": primitiveRamps.blue[500],
224
+ "status-info-light": primitiveRamps.blue[300],
225
+ "status-info-dark": primitiveRamps.blue[600],
226
+ "status-info-subtle": primitiveRamps.blue[50],
227
+ // Text on status backgrounds (on-status-*)
228
+ "on-status-success": primitiveColors.white,
229
+ "on-status-error": primitiveColors.white,
230
+ "on-status-warning": primitiveColors.white,
231
+ "on-status-info": primitiveColors.white,
232
+ // Result/outcome indicators (result-*)
233
+ "result-improve": "#4caf50",
234
+ // Green - positive outcome
235
+ "result-improve-light": "rgba(76, 175, 80, 0.12)",
236
+ "result-improve-dark": "#248a24",
237
+ "result-degrade": "#ef5350",
238
+ // Red - negative outcome
239
+ "result-degrade-light": "rgba(239, 83, 80, 0.12)",
240
+ "result-degrade-dark": "#b30000",
241
+ "result-inconclusive": "#9E9A97",
242
+ // Gray - no clear result
243
+ "result-inconclusive-light": "rgba(158, 154, 151, 0.12)",
244
+ "result-neutral": primitiveColors.neutral[500],
245
+ // Neutral baseline
246
+ // Text on result backgrounds (on-result-*)
247
+ "on-result-improve": primitiveColors.white,
248
+ "on-result-degrade": primitiveColors.white,
249
+ "on-result-inconclusive": primitiveColors.white,
250
+ // Data visualization colors (data-*)
251
+ // First 10 colors from discrete rainbow optimized for charts
252
+ "data-1": discreteRainbow[9],
253
+ // Blue
254
+ "data-2": discreteRainbow[14],
255
+ // Green
256
+ "data-3": discreteRainbow[17],
257
+ // Yellow
258
+ "data-4": discreteRainbow[25],
259
+ // Red
260
+ "data-5": discreteRainbow[8],
261
+ // Purple
262
+ "data-6": discreteRainbow[20],
263
+ // Orange
264
+ "data-7": discreteRainbow[13],
265
+ // Light Blue
266
+ "data-8": discreteRainbow[15],
267
+ // Light Green
268
+ "data-9": discreteRainbow[3],
269
+ // Lavender
270
+ "data-10": discreteRainbow[22],
271
+ // Dark Orange
272
+ // Text colors (text-*)
273
+ "text-primary": "#121828",
274
+ "text-secondary": "#65748B",
275
+ "text-tertiary": primitiveColors.neutral[400],
276
+ "text-disabled": "rgba(55, 65, 81, 0.48)",
277
+ "text-inverse": primitiveColors.white,
278
+ "text-link": primitiveColors.blue[600],
279
+ "text-link-hover": primitiveColors.blue[700],
280
+ // Surface colors (surface-*) - for elevated containers
281
+ "surface-base": primitiveColors.white,
282
+ "surface-elevated": primitiveColors.neutral[50],
283
+ // slightly off-white for elevated cards
284
+ "surface-raised": primitiveColors.neutral[100],
285
+ // light gray for raised cards
286
+ "surface-overlay": primitiveColors.white,
287
+ "surface-input": primitiveColors.neutral[50],
288
+ // Input field background (filled variant)
289
+ // Deepest pressed pit (TD-surface-tokens, S-3) — light-mode counterpart of
290
+ // the dark ramp's `inset` plane; a placeholder pairing, not part of the
291
+ // dark-ramp investigation (surface.contract.test.ts is dark-mode only).
292
+ "surface-inset": primitiveColors.neutral[300],
293
+ // Background colors (background-*)
294
+ "background-base": "#EBEBEB",
295
+ "background-default": primitiveColors.white,
296
+ "background-subtle": primitiveColors.neutral[50],
297
+ // Frame/bezel chrome (TD-surface-tokens, S-3) — top bar + side nav shell,
298
+ // one step below `background-base` (darker still than `surface-inset`
299
+ // above, mirroring the dark-mode ordering). Placeholder pairing for light
300
+ // mode (see dark-mode note on `surface-inset` above).
301
+ "background-frame": primitiveColors.neutral[400],
302
+ // Border colors (border-*)
303
+ "border-default": "#E8E9EB",
304
+ "border-subtle": primitiveColors.neutral[100],
305
+ "border-strong": primitiveColors.neutral[300],
306
+ "border-prominent": primitiveColors.neutral[400],
307
+ // high-visibility divider
308
+ "border-focus": primitiveColors.blue[600],
309
+ "border-input": primitiveColors.neutral[300],
310
+ // Input field border
311
+ "border-input-hover": primitiveColors.neutral[400],
312
+ // Input field border on hover
313
+ "border-input-focus": primitiveColors.blue[600],
314
+ // Input field border on focus
315
+ "border-input-error": primitiveColors.red[600],
316
+ // Input field border on error
317
+ // Alpha hairline separators (surface-independent — composite toward black on
318
+ // light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
319
+ "hairline-subtle": "rgba(0, 0, 0, 0.06)",
320
+ "hairline-default": "rgba(0, 0, 0, 0.09)",
321
+ "hairline-strong": "rgba(0, 0, 0, 0.14)",
322
+ // Interactive states (interactive-*)
323
+ "interactive-hover": "rgba(55, 65, 81, 0.04)",
324
+ "interactive-focus": "rgba(55, 65, 81, 0.12)",
325
+ "interactive-active": "rgba(55, 65, 81, 0.16)",
326
+ "interactive-selected": "rgba(55, 65, 81, 0.08)",
327
+ "interactive-disabled": "rgba(55, 65, 81, 0.12)",
328
+ "interactive-disabled-text": "rgba(55, 65, 81, 0.26)",
329
+ // Divider
330
+ "divider": "#E8E9EB",
331
+ // Avatar default
332
+ "avatar-background": primitiveColors.neutral[500],
333
+ "avatar-text": primitiveColors.white
334
+ };
335
+ var semanticColorsDark = {
336
+ // Brand colors stay the same in dark mode
337
+ "brand-primary": primitiveRamps.orange[400],
338
+ "brand-primary-light": primitiveRamps.orange[300],
339
+ "brand-primary-dark": primitiveRamps.orange[500],
340
+ "brand-primary-subtle": "rgba(255, 121, 0, 0.12)",
341
+ "brand-primary-hover": primitiveRamps.orange[300],
342
+ "brand-primary-active": primitiveRamps.orange[200],
343
+ "brand-secondary": primitiveRamps.cyan[600],
344
+ "brand-secondary-light": primitiveRamps.cyan[500],
345
+ "brand-secondary-dark": primitiveRamps.cyan[700],
346
+ "brand-secondary-subtle": "rgba(48, 123, 155, 0.12)",
347
+ "brand-secondary-hover": primitiveRamps.cyan[500],
348
+ "brand-secondary-active": primitiveRamps.cyan[400],
349
+ // Text on brand backgrounds
350
+ "on-brand-primary": primitiveColors.white,
351
+ "on-brand-secondary": primitiveColors.white,
352
+ // Status colors
353
+ "status-success": primitiveRamps.green[300],
354
+ "status-success-light": primitiveRamps.green[200],
355
+ "status-success-dark": primitiveRamps.green[600],
356
+ "status-success-subtle": "rgba(46, 213, 115, 0.12)",
357
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
358
+ "status-live": primitiveRamps.green[300],
359
+ "status-live-muted": primitiveRamps.green[500],
360
+ "status-error": primitiveRamps.red[600],
361
+ "status-error-light": primitiveRamps.red[500],
362
+ "status-error-dark": primitiveRamps.red[700],
363
+ "status-error-subtle": "rgba(209, 67, 67, 0.12)",
364
+ "status-error-vivid": primitiveColors.redVivid[500],
365
+ "status-error-vivid-light": primitiveColors.redVivid[400],
366
+ "status-error-vivid-dark": primitiveColors.redVivid[700],
367
+ "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
368
+ "status-warning": primitiveRamps.amber[300],
369
+ "status-warning-light": primitiveRamps.amber[200],
370
+ "status-warning-dark": primitiveRamps.amber[500],
371
+ "status-warning-subtle": "rgba(249, 180, 21, 0.12)",
372
+ "status-info": primitiveRamps.blue[500],
373
+ "status-info-light": primitiveRamps.blue[300],
374
+ "status-info-dark": primitiveRamps.blue[600],
375
+ "status-info-subtle": "rgba(33, 150, 243, 0.12)",
376
+ // Text on status backgrounds
377
+ "on-status-success": primitiveColors.white,
378
+ "on-status-error": primitiveColors.white,
379
+ "on-status-warning": primitiveColors.white,
380
+ "on-status-info": primitiveColors.white,
381
+ // Result/outcome indicators (result-*)
382
+ "result-improve": "#4caf50",
383
+ "result-improve-light": "rgba(76, 175, 80, 0.16)",
384
+ "result-improve-dark": "#248a24",
385
+ "result-degrade": "#ef5350",
386
+ "result-degrade-light": "rgba(239, 83, 80, 0.16)",
387
+ "result-degrade-dark": "#b30000",
388
+ "result-inconclusive": "#9E9A97",
389
+ "result-inconclusive-light": "rgba(158, 154, 151, 0.16)",
390
+ "result-neutral": primitiveColors.neutral[400],
391
+ // Text on result backgrounds
392
+ "on-result-improve": primitiveColors.white,
393
+ "on-result-degrade": primitiveColors.white,
394
+ "on-result-inconclusive": primitiveColors.white,
395
+ // Data visualization colors (same in dark mode for consistency)
396
+ "data-1": discreteRainbow[9],
397
+ "data-2": discreteRainbow[14],
398
+ "data-3": discreteRainbow[17],
399
+ "data-4": discreteRainbow[25],
400
+ "data-5": discreteRainbow[8],
401
+ "data-6": discreteRainbow[20],
402
+ "data-7": discreteRainbow[13],
403
+ "data-8": discreteRainbow[15],
404
+ "data-9": discreteRainbow[3],
405
+ "data-10": discreteRainbow[22],
406
+ // Text colors - inverted for dark mode
407
+ "text-primary": primitiveColors.neutral[100],
408
+ "text-secondary": primitiveColors.neutral[400],
409
+ "text-tertiary": primitiveColors.neutral[500],
410
+ "text-disabled": "rgba(255, 255, 255, 0.38)",
411
+ "text-inverse": primitiveColors.neutral[900],
412
+ "text-link": "#828DF8",
413
+ "text-link-hover": primitiveColors.blue[400],
414
+ // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
415
+ // S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
416
+ // `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
417
+ // and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
418
+ // tracks one plane above `surface-base`, same relative position as before the remap.
419
+ // `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
420
+ // so it takes the ramp's `background` (shell/frame) role, NOT `inset`. The ramp's
421
+ // deepest `inset` plane is now promoted to `surface-inset` below (S-3) — still no
422
+ // `SurfaceLevel` member for it (follow-up, alongside the elevation.ts -1/-2 pressed
423
+ // levels — parallel workstream, not this branch).
424
+ "surface-base": surfaceRampDark.base,
425
+ // main surface (#252321, L*13.5)
426
+ "surface-elevated": surfaceRampDark.elevated,
427
+ // elevated surface (#2C2A28, L*17 — nav/rail)
428
+ "surface-raised": surfaceRampDark.raised,
429
+ // raised surface (#31302F, L*20 — cards)
430
+ "surface-overlay": surfaceRampDark.overlay,
431
+ // overlay surface (#373635, L*22.5 — hero/popover)
432
+ "surface-input": surfaceRampDark.elevated,
433
+ // input surface (#2C2A28 — one plane above base)
434
+ "surface-inset": surfaceRampDark.inset,
435
+ // deepest pressed pit (#13100D, L*4.5 — sub-shell well)
436
+ // Background colors — same ramp, the frame/shell end of it.
437
+ "background-base": surfaceRampDark.background,
438
+ // shell / frame (#1C1916, L*9 — Surface level="background")
439
+ "background-default": surfaceRampDark.base,
440
+ // main background (#252321, L*13.5 — matches surface-base)
441
+ "background-subtle": surfaceRampDark.elevated,
442
+ // subtle background (#2C2A28, L*17 — matches surface-elevated)
443
+ // Frame/bezel chrome (S-3) — the top bar + side nav shell; one step BELOW
444
+ // `background-base`, darker even than `surface-inset` — it exits the content
445
+ // ramp entirely rather than sitting in it. See `backgroundFrameDark` in
446
+ // primitives.ts.
447
+ "background-frame": backgroundFrameDark,
448
+ // frame / bezel (#100D0A, L*3.79)
449
+ // Border colors — `border-subtle` (#1C1C1C) previously collided with
450
+ // `surface-raised` (also #1C1C1C, an invisible border on raised cards). The
451
+ // re-spaced ramp above moved `surface-raised` to #2D2C2B, so this value is now
452
+ // distinct from every surface hex without needing to change it — see
453
+ // `surface.contract.test.ts` R2/R4.
454
+ "border-default": primitiveColors.charcoal[400],
455
+ // default border
456
+ "border-subtle": primitiveColors.charcoal[500],
457
+ // subtle border
458
+ "border-strong": primitiveColors.charcoal[300],
459
+ // strong border
460
+ "border-prominent": primitiveColors.charcoal[200],
461
+ // high-visibility divider
462
+ "border-focus": "#828DF8",
463
+ "border-input": primitiveColors.neutral[600],
464
+ "border-input-hover": primitiveColors.neutral[500],
465
+ "border-input-focus": "#828DF8",
466
+ "border-input-error": primitiveColors.red[500],
467
+ // Alpha hairline separators — the primary separation cue (§4/S-2). Self-
468
+ // normalizing: composites toward white by ~the same amount on ANY plane, so
469
+ // one family works at every elevation instead of per-surface border tokens.
470
+ // Shadows are demoted to floating-overlay use only (not shipped as a fill
471
+ // separator here — see elevation.ts, follow-up S-4).
472
+ "hairline-subtle": "rgba(255, 255, 255, 0.06)",
473
+ "hairline-default": "rgba(255, 255, 255, 0.09)",
474
+ "hairline-strong": "rgba(255, 255, 255, 0.14)",
475
+ // Interactive states
476
+ "interactive-hover": "rgba(255, 255, 255, 0.04)",
477
+ "interactive-focus": "rgba(255, 255, 255, 0.12)",
478
+ "interactive-active": "rgba(255, 255, 255, 0.16)",
479
+ "interactive-selected": "rgba(255, 255, 255, 0.08)",
480
+ "interactive-disabled": "rgba(255, 255, 255, 0.12)",
481
+ "interactive-disabled-text": "rgba(255, 255, 255, 0.26)",
482
+ // Divider
483
+ "divider": primitiveColors.charcoal[400],
484
+ // divider color
485
+ // Avatar default
486
+ "avatar-background": primitiveColors.neutral[600],
487
+ "avatar-text": primitiveColors.white
488
+ };
489
+ function getSemanticColors(mode) {
490
+ return mode === "dark" ? semanticColorsDark : semanticColorsLight;
183
491
  }
184
- function roundTempo(tempo) {
185
- return tempo.map((v) => Math.round(v * 10) / 10);
492
+ function hexToRgb(hex) {
493
+ const cleaned = hex.replace("#", "");
494
+ let r, g, b;
495
+ if (cleaned.length === 3) {
496
+ r = parseInt(cleaned[0] + cleaned[0], 16);
497
+ g = parseInt(cleaned[1] + cleaned[1], 16);
498
+ b = parseInt(cleaned[2] + cleaned[2], 16);
499
+ } else if (cleaned.length === 6) {
500
+ r = parseInt(cleaned.slice(0, 2), 16);
501
+ g = parseInt(cleaned.slice(2, 4), 16);
502
+ b = parseInt(cleaned.slice(4, 6), 16);
503
+ } else {
504
+ return null;
505
+ }
506
+ return { r, g, b };
186
507
  }
187
- function wrapJsx(fn) {
188
- return function(type, props, ...rest) {
189
- if (props && typeof props.className === "string" && props.className) {
190
- const cn2 = props.className;
191
- const cssStyle = { $$css: true, [cn2]: cn2 };
192
- const existing = props.style;
193
- props = {
194
- ...props,
195
- style: existing ? [cssStyle, existing] : cssStyle
196
- };
197
- delete props.className;
508
+ function rgbToHsv(r, g, b) {
509
+ const rNorm = r / 255;
510
+ const gNorm = g / 255;
511
+ const bNorm = b / 255;
512
+ const max = Math.max(rNorm, gNorm, bNorm);
513
+ const min = Math.min(rNorm, gNorm, bNorm);
514
+ const delta = max - min;
515
+ const v = max;
516
+ const s = max === 0 ? 0 : delta / max;
517
+ let h = 0;
518
+ if (delta !== 0) {
519
+ if (max === rNorm) {
520
+ h = 60 * ((gNorm - bNorm) / delta % 6);
521
+ } else if (max === gNorm) {
522
+ h = 60 * ((bNorm - rNorm) / delta + 2);
523
+ } else {
524
+ h = 60 * ((rNorm - gNorm) / delta + 4);
198
525
  }
199
- return fn(type, props, ...rest);
526
+ }
527
+ if (h < 0) h += 360;
528
+ return { h, s, v };
529
+ }
530
+ function hsvToRgb(h, s, v) {
531
+ const c = v * s;
532
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
533
+ const m = v - c;
534
+ let rPrime, gPrime, bPrime;
535
+ if (h >= 0 && h < 60) {
536
+ rPrime = c;
537
+ gPrime = x;
538
+ bPrime = 0;
539
+ } else if (h >= 60 && h < 120) {
540
+ rPrime = x;
541
+ gPrime = c;
542
+ bPrime = 0;
543
+ } else if (h >= 120 && h < 180) {
544
+ rPrime = 0;
545
+ gPrime = c;
546
+ bPrime = x;
547
+ } else if (h >= 180 && h < 240) {
548
+ rPrime = 0;
549
+ gPrime = x;
550
+ bPrime = c;
551
+ } else if (h >= 240 && h < 300) {
552
+ rPrime = x;
553
+ gPrime = 0;
554
+ bPrime = c;
555
+ } else {
556
+ rPrime = c;
557
+ gPrime = 0;
558
+ bPrime = x;
559
+ }
560
+ return {
561
+ r: Math.round((rPrime + m) * 255),
562
+ g: Math.round((gPrime + m) * 255),
563
+ b: Math.round((bPrime + m) * 255)
200
564
  };
201
565
  }
202
- var jsx = wrapJsx(jsxRuntime.jsx);
203
- var jsxs = wrapJsx(jsxRuntime.jsxs);
204
-
205
- // src/components/custom/Workout/SegmentedBar.tsx
206
- var PULSE_HALF_MS = 950;
207
- var PULSE_MIN_OPACITY = 0.45;
208
- var SEGMENTED_BAR_GAP = 5;
209
- function usePulse(active) {
210
- const [value] = react.useState(() => new reactNative.Animated.Value(0));
211
- react.useEffect(() => {
212
- if (!active) return;
213
- const loop = reactNative.Animated.loop(
214
- reactNative.Animated.sequence([
215
- reactNative.Animated.timing(value, {
216
- toValue: 1,
217
- duration: PULSE_HALF_MS,
218
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
219
- useNativeDriver: false
220
- }),
221
- reactNative.Animated.timing(value, {
222
- toValue: 0,
223
- duration: PULSE_HALF_MS,
224
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
225
- useNativeDriver: false
226
- })
227
- ])
228
- );
229
- loop.start();
230
- return () => loop.stop();
231
- }, [active, value]);
232
- return value;
566
+ function rgbToHex(r, g, b) {
567
+ const toHex = (n) => {
568
+ const clamped = Math.max(0, Math.min(255, Math.round(n)));
569
+ return clamped.toString(16).padStart(2, "0");
570
+ };
571
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
233
572
  }
234
- function SegmentedBar({
235
- segments,
236
- height = 8,
237
- gap = SEGMENTED_BAR_GAP,
238
- radius = 2,
239
- marker = null,
240
- segmentTestID,
241
- style,
242
- ...props
243
- }) {
244
- const pulse = usePulse(segments.some((s) => s.pulse));
245
- return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
246
- segments.map((seg, i) => {
247
- const fillStyle = {
248
- width: `${(seg.fill ?? 1) * 100}%`,
249
- height: "100%",
250
- backgroundColor: seg.color,
251
- opacity: seg.opacity
252
- };
253
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
254
- return /* @__PURE__ */ jsx(
255
- reactNative.View,
256
- {
257
- style: {
258
- flex: seg.weight ?? 1,
259
- minWidth: 0,
260
- height: "100%",
261
- marginLeft: seg.leadingGap,
262
- borderRadius: radius,
263
- overflow: "hidden"
264
- },
265
- children: seg.pulse ? /* @__PURE__ */ jsx(
266
- reactNative.Animated.View,
267
- {
268
- style: {
269
- ...fillStyle,
270
- ...seg.pulseColor ? {
271
- backgroundColor: pulse.interpolate({
272
- inputRange: [0, 1],
273
- outputRange: seg.pulseColor
274
- })
275
- } : {
276
- opacity: pulse.interpolate({
277
- inputRange: [0, 1],
278
- outputRange: [PULSE_MIN_OPACITY, 1]
279
- })
280
- }
281
- },
282
- accessibilityElementsHidden: true,
283
- testID
284
- }
285
- ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
286
- },
287
- i
288
- );
289
- }),
290
- marker ? /* @__PURE__ */ jsx(
291
- reactNative.View,
292
- {
293
- style: {
294
- position: "absolute",
295
- left: `${marker.position * 100}%`,
296
- top: 0,
297
- bottom: 0,
298
- width: 2,
299
- backgroundColor: marker.color
300
- },
301
- accessibilityElementsHidden: true,
302
- testID: "segmented-bar-marker"
303
- }
304
- ) : null
305
- ] });
573
+ function lighten(hex, amount = 0.1) {
574
+ const rgb = hexToRgb(hex);
575
+ if (!rgb) return hex;
576
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
577
+ const newV = Math.min(1, hsv.v + amount);
578
+ const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
579
+ return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
306
580
  }
307
-
308
- // src/components/custom/Workout/SetBar.tsx
309
- var SET_STRIP_ZONES = {
310
- slow: primitiveRamps.red[600],
311
- moderate: primitiveRamps.orange[400],
312
- fast: primitiveRamps.amber[300],
313
- fastest: primitiveRamps.green[300]
581
+ var shadowColors = {
582
+ // For charcoal surfaces (~#2C2C2C to #3C3C3C)
583
+ charcoal: {
584
+ dark: "rgba(0, 0, 0, 0.5)",
585
+ light: "rgba(255, 255, 255, 0.12)",
586
+ // brighter for crisp rim light
587
+ darker: "rgba(0, 0, 0, 0.6)"
588
+ },
589
+ // For neutral dark surfaces (~#1F2937)
590
+ neutralDark: {
591
+ dark: "rgba(0, 0, 0, 0.4)",
592
+ light: "rgba(255, 255, 255, 0.08)",
593
+ darker: "rgba(0, 0, 0, 0.5)"
594
+ },
595
+ // For light surfaces (white/light gray backgrounds)
596
+ neutralLight: {
597
+ dark: "rgba(0, 0, 0, 0.15)",
598
+ // Visible dark shadow for depth on light backgrounds
599
+ light: "rgba(255, 255, 255, 1)",
600
+ // Bright highlight for raised effect (visible on light gray)
601
+ darker: "rgba(0, 0, 0, 0.2)"
602
+ // Deeper shadow for pressed states
603
+ }
314
604
  };
315
- var TODO_COLOR = primitiveColors.charcoal[300];
316
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
317
- var NOTCH_GAP = 2;
318
- var CLUSTER_GAP = 3;
319
- var MYO_UPCOMING_CLUSTERS = 3;
320
- var MYO_UPCOMING_CLUSTER_LEN = 5;
321
- var MYO_UPCOMING_FADE = 0.6;
322
- var PULSE_RANGE = {
323
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
324
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
325
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
326
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
605
+ var intensityConfig = {
606
+ subtle: { offset: 1, blur: 2 },
607
+ // very light, barely visible
608
+ medium: { offset: 2, blur: 4 },
609
+ // noticeable but restrained
610
+ strong: { offset: 2, blur: 5 }
611
+ // slightly deeper blur, same offset as medium
327
612
  };
328
- function velocityZoneColor(v) {
329
- if (v < 0.5) return SET_STRIP_ZONES.slow;
330
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
331
- if (v < 1) return SET_STRIP_ZONES.fast;
332
- return SET_STRIP_ZONES.fastest;
333
- }
334
- function chunkedSegments(chunks, gap) {
335
- return chunks.flatMap(
336
- (vels, ci) => vels.map((v, ri) => ({
337
- color: velocityZoneColor(v),
338
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
339
- }))
340
- );
613
+ function createRaisedShadow(surface = "charcoal", intensity = "medium") {
614
+ const colors = shadowColors[surface];
615
+ const { offset, blur } = intensityConfig[intensity];
616
+ const rimBlur = 1;
617
+ return reactNative.Platform.select({
618
+ web: {
619
+ boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
620
+ },
621
+ default: {
622
+ // React Native shadow approximation (limited compared to web)
623
+ shadowColor: "#000",
624
+ shadowOffset: { width: offset, height: offset },
625
+ shadowOpacity: 0.3,
626
+ shadowRadius: blur / 2,
627
+ elevation: offset
628
+ }
629
+ });
341
630
  }
342
- function rangeSegments(floor, max, doneVels) {
343
- return Array.from({ length: max }, (_, i) => {
344
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
345
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
631
+ function createPressedShadow(surface = "charcoal", intensity = "medium") {
632
+ const colors = shadowColors[surface];
633
+ const { offset, blur } = intensityConfig[intensity];
634
+ const rimOffset = 1;
635
+ const rimBlur = 1;
636
+ return reactNative.Platform.select({
637
+ web: {
638
+ boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
639
+ },
640
+ default: {
641
+ // React Native doesn't support inset shadows, approximate with darker bg
642
+ shadowOpacity: 0,
643
+ elevation: 0
644
+ }
346
645
  });
347
646
  }
348
- function myoUpcomingSegments(activationLen, clusterCount) {
349
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
350
- const trail = Array.from(
351
- { length: clusterCount },
352
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
353
- color: SET_STRIP_VARIABLE_COLOR,
354
- opacity: MYO_UPCOMING_FADE ** ci,
355
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
356
- }))
357
- ).flat();
358
- return [...activation, ...trail];
647
+ function createFlatShadow() {
648
+ return reactNative.Platform.select({
649
+ web: {
650
+ boxShadow: "none"
651
+ },
652
+ default: {
653
+ shadowOpacity: 0,
654
+ elevation: 0
655
+ }
656
+ });
359
657
  }
360
- function setSegments(set) {
361
- if (set.status === "todo") {
362
- return [{ color: TODO_COLOR }];
363
- }
364
- if (set.status === "done") {
365
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
366
- }
367
- if (set.status === "range") {
368
- return rangeSegments(set.floor, set.max, set.doneVels);
658
+ function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
659
+ const colors = shadowColors[surface];
660
+ const { offset, blur } = intensityConfig[intensity];
661
+ const hoverOffset = Math.max(1, Math.round(offset / 2));
662
+ const hoverBlur = Math.max(1, Math.round(blur / 2));
663
+ const rimBlur = 1;
664
+ return reactNative.Platform.select({
665
+ web: {
666
+ // Keep same offsets as normal state for rim to prevent "shrinking" effect
667
+ boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
668
+ },
669
+ default: {
670
+ shadowColor: "#000",
671
+ shadowOffset: { width: hoverOffset, height: hoverOffset },
672
+ shadowOpacity: 0.2,
673
+ shadowRadius: hoverBlur / 2,
674
+ elevation: hoverOffset
675
+ }
676
+ });
677
+ }
678
+ function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
679
+ const colors = shadowColors[surface];
680
+ const { offset, blur } = intensityConfig[intensity];
681
+ const hoverOffset = Math.max(1, Math.round(offset / 2));
682
+ const hoverBlur = Math.max(1, Math.round(blur / 2));
683
+ const rimBlur = 1;
684
+ return reactNative.Platform.select({
685
+ web: {
686
+ // Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
687
+ boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
688
+ },
689
+ default: {
690
+ shadowOpacity: 0,
691
+ elevation: 0
692
+ }
693
+ });
694
+ }
695
+ ({
696
+ // Charcoal surface shadows (for dark toolbars, cards)
697
+ charcoal: {
698
+ raised: {
699
+ subtle: createRaisedShadow("charcoal", "subtle"),
700
+ medium: createRaisedShadow("charcoal", "medium"),
701
+ strong: createRaisedShadow("charcoal", "strong")
702
+ },
703
+ raisedHover: {
704
+ subtle: createRaisedHoverShadow("charcoal", "subtle"),
705
+ medium: createRaisedHoverShadow("charcoal", "medium"),
706
+ strong: createRaisedHoverShadow("charcoal", "strong")
707
+ },
708
+ pressed: {
709
+ subtle: createPressedShadow("charcoal", "subtle"),
710
+ medium: createPressedShadow("charcoal", "medium"),
711
+ strong: createPressedShadow("charcoal", "strong")
712
+ },
713
+ pressedHover: {
714
+ subtle: createPressedHoverShadow("charcoal", "subtle"),
715
+ medium: createPressedHoverShadow("charcoal", "medium"),
716
+ strong: createPressedHoverShadow("charcoal", "strong")
717
+ },
718
+ flat: createFlatShadow()
719
+ },
720
+ // Neutral dark surface shadows
721
+ neutralDark: {
722
+ raised: {
723
+ subtle: createRaisedShadow("neutralDark", "subtle"),
724
+ medium: createRaisedShadow("neutralDark", "medium"),
725
+ strong: createRaisedShadow("neutralDark", "strong")
726
+ },
727
+ raisedHover: {
728
+ subtle: createRaisedHoverShadow("neutralDark", "subtle"),
729
+ medium: createRaisedHoverShadow("neutralDark", "medium"),
730
+ strong: createRaisedHoverShadow("neutralDark", "strong")
731
+ },
732
+ pressed: {
733
+ subtle: createPressedShadow("neutralDark", "subtle"),
734
+ medium: createPressedShadow("neutralDark", "medium"),
735
+ strong: createPressedShadow("neutralDark", "strong")
736
+ },
737
+ pressedHover: {
738
+ subtle: createPressedHoverShadow("neutralDark", "subtle"),
739
+ medium: createPressedHoverShadow("neutralDark", "medium"),
740
+ strong: createPressedHoverShadow("neutralDark", "strong")
741
+ },
742
+ flat: createFlatShadow()
743
+ },
744
+ // Neutral light surface shadows
745
+ neutralLight: {
746
+ raised: {
747
+ subtle: createRaisedShadow("neutralLight", "subtle"),
748
+ medium: createRaisedShadow("neutralLight", "medium"),
749
+ strong: createRaisedShadow("neutralLight", "strong")
750
+ },
751
+ raisedHover: {
752
+ subtle: createRaisedHoverShadow("neutralLight", "subtle"),
753
+ medium: createRaisedHoverShadow("neutralLight", "medium"),
754
+ strong: createRaisedHoverShadow("neutralLight", "strong")
755
+ },
756
+ pressed: {
757
+ subtle: createPressedShadow("neutralLight", "subtle"),
758
+ medium: createPressedShadow("neutralLight", "medium"),
759
+ strong: createPressedShadow("neutralLight", "strong")
760
+ },
761
+ pressedHover: {
762
+ subtle: createPressedHoverShadow("neutralLight", "subtle"),
763
+ medium: createPressedHoverShadow("neutralLight", "medium"),
764
+ strong: createPressedHoverShadow("neutralLight", "strong")
765
+ },
766
+ flat: createFlatShadow()
369
767
  }
370
- if (set.status === "drop") {
371
- return chunkedSegments(set.subloads, NOTCH_GAP);
768
+ });
769
+
770
+ // src/utils/colors.ts
771
+ function alpha(color, opacity) {
772
+ const clampedOpacity = Math.max(0, Math.min(1, opacity));
773
+ if (color.startsWith("#")) {
774
+ const hex = color.slice(1);
775
+ let r, g, b;
776
+ if (hex.length === 3) {
777
+ r = parseInt(hex[0] + hex[0], 16);
778
+ g = parseInt(hex[1] + hex[1], 16);
779
+ b = parseInt(hex[2] + hex[2], 16);
780
+ } else if (hex.length === 6) {
781
+ r = parseInt(hex.slice(0, 2), 16);
782
+ g = parseInt(hex.slice(2, 4), 16);
783
+ b = parseInt(hex.slice(4, 6), 16);
784
+ } else {
785
+ return color;
786
+ }
787
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
372
788
  }
373
- if (set.status === "myo") {
374
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
789
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
790
+ if (rgbMatch) {
791
+ const [, r, g, b] = rgbMatch;
792
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
375
793
  }
376
- if (set.status === "myo-upcoming") {
377
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
794
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
795
+ if (rgbaMatch) {
796
+ const [, r, g, b] = rgbaMatch;
797
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
378
798
  }
379
- const performed = set.velocities.map((v) => {
380
- const color = velocityZoneColor(v);
381
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
382
- });
383
- const remaining = Math.max(0, set.planned - set.velocities.length);
384
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
799
+ return color;
385
800
  }
386
- function setBarSegmentTestID(seg) {
387
- if (seg.pulse) return "set-strip-pulse";
388
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
389
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
801
+
802
+ // src/utils/workout-format.ts
803
+ function roundRpe(rpe) {
804
+ return Math.round(rpe * 2) / 2;
390
805
  }
391
- function SetBar({ set, height = 8 }) {
392
- return /* @__PURE__ */ jsx(
393
- SegmentedBar,
394
- {
395
- segments: setSegments(set),
396
- height,
397
- gap: 0,
398
- radius: 0,
399
- segmentTestID: setBarSegmentTestID,
400
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
401
- testID: "set-strip-set"
402
- }
403
- );
806
+ function roundWeight(weight) {
807
+ return Math.round(weight);
808
+ }
809
+ function formatVelocity(velocity) {
810
+ return velocity.toFixed(2);
811
+ }
812
+ function roundTempo(tempo) {
813
+ return tempo.map((v) => Math.round(v * 10) / 10);
404
814
  }
405
815
 
406
- // src/components/custom/Workout/SetStrip.tsx
407
- var SET_STRIP_GAP = 5;
408
- function setCategory(set) {
409
- switch (set.status) {
410
- case "done":
411
- case "drop":
412
- case "myo":
413
- return "done";
414
- case "active":
415
- return "active";
416
- case "range":
417
- return set.doneVels.length > 0 ? "active" : "upcoming";
418
- case "todo":
419
- case "myo-upcoming":
420
- return "upcoming";
421
- }
816
+ // src/theme/bar-paper.ts
817
+ function grainOpacityForColor(hex) {
818
+ const h = hex.replace("#", "");
819
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
820
+ const r = parseInt(full.slice(0, 2), 16);
821
+ const g = parseInt(full.slice(2, 4), 16);
822
+ const b = parseInt(full.slice(4, 6), 16);
823
+ if ([r, g, b].some(Number.isNaN)) return 0.14;
824
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
825
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
826
+ }
827
+ function barGrain(color) {
828
+ const op = grainOpacityForColor(color).toFixed(3);
829
+ 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")`;
830
+ }
831
+ function barPaper(color, flip = false) {
832
+ return {
833
+ backgroundImage: barGrain(color),
834
+ 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)"
835
+ };
422
836
  }
423
- function describeSets(sets) {
424
- const done = sets.filter((s) => setCategory(s) === "done").length;
425
- const active = sets.filter((s) => setCategory(s) === "active").length;
426
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
427
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
837
+ var SURFACE_LEVEL_TOKEN = {
838
+ background: "background-base",
839
+ base: "surface-base",
840
+ elevated: "surface-elevated",
841
+ raised: "surface-raised",
842
+ overlay: "surface-overlay"
843
+ };
844
+ var ON_SURFACE_TOKEN = {
845
+ primary: "text-primary",
846
+ secondary: "text-secondary",
847
+ tertiary: "text-tertiary"
848
+ };
849
+ var DEFAULT_CONTEXT = { mode: "dark", level: "base" };
850
+ var SurfaceContext = react.createContext(DEFAULT_CONTEXT);
851
+ function useSurface() {
852
+ return react.useContext(SurfaceContext);
428
853
  }
429
- function SetStrip({ sets, height = 8, className, ...props }) {
430
- return /* @__PURE__ */ jsx(
854
+ function useSurfaceMode() {
855
+ return react.useContext(SurfaceContext).mode;
856
+ }
857
+ function useOnSurfaceColor(role = "primary") {
858
+ return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
859
+ }
860
+ function insetFloor(mode) {
861
+ const colors = getSemanticColors(mode);
862
+ return colors["surface-inset"] ?? surfaceRampDark.inset;
863
+ }
864
+ function surfaceBackground(level, mode) {
865
+ if (level === "inset") return insetFloor(mode);
866
+ return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
867
+ }
868
+ var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
869
+ var PEAK_OVERSHOOT = 1.12;
870
+ function getReducedMotionPreference() {
871
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
872
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
873
+ }
874
+ function usePrefersReducedMotion() {
875
+ const [reduced, setReduced] = react.useState(getReducedMotionPreference);
876
+ react.useEffect(() => {
877
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
878
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
879
+ const handler = () => setReduced(mq.matches);
880
+ handler();
881
+ mq.addEventListener?.("change", handler);
882
+ return () => mq.removeEventListener?.("change", handler);
883
+ }, []);
884
+ return reduced;
885
+ }
886
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
887
+ const prefersReducedMotion = usePrefersReducedMotion();
888
+ const [liveGrowth] = react.useState(() => new reactNative.Animated.Value(1));
889
+ react.useEffect(() => {
890
+ if (liveRepIndex == null || liveVelocity == null) return;
891
+ if (prefersReducedMotion) {
892
+ liveGrowth.setValue(1);
893
+ return;
894
+ }
895
+ liveGrowth.setValue(0);
896
+ if (isNewPeak) {
897
+ reactNative.Animated.sequence([
898
+ reactNative.Animated.timing(liveGrowth, {
899
+ toValue: PEAK_OVERSHOOT,
900
+ duration: 220,
901
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
902
+ useNativeDriver: false
903
+ }),
904
+ reactNative.Animated.spring(liveGrowth, {
905
+ toValue: 1,
906
+ friction: 5,
907
+ tension: 140,
908
+ useNativeDriver: false
909
+ })
910
+ ]).start();
911
+ } else {
912
+ reactNative.Animated.timing(liveGrowth, {
913
+ toValue: 1,
914
+ duration: 300,
915
+ easing: ANIMATION_EASING,
916
+ useNativeDriver: false
917
+ }).start();
918
+ }
919
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
920
+ return liveGrowth;
921
+ }
922
+ function wrapJsx(fn) {
923
+ return function(type, props, ...rest) {
924
+ if (props && typeof props.className === "string" && props.className) {
925
+ const cn2 = props.className;
926
+ const cssStyle = { $$css: true, [cn2]: cn2 };
927
+ const existing = props.style;
928
+ props = {
929
+ ...props,
930
+ style: existing ? [cssStyle, existing] : cssStyle
931
+ };
932
+ delete props.className;
933
+ }
934
+ return fn(type, props, ...rest);
935
+ };
936
+ }
937
+ var jsx = wrapJsx(jsxRuntime.jsx);
938
+ var jsxs = wrapJsx(jsxRuntime.jsxs);
939
+
940
+ // src/components/custom/charts/SetBarChart.tsx
941
+ function mixHex(a, b, t12) {
942
+ const parse = (h) => {
943
+ const s = h.replace("#", "");
944
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
945
+ };
946
+ const [ar, ag, ab] = parse(a);
947
+ const [br, bg, bb] = parse(b);
948
+ const ch = (x, y) => Math.round(x + (y - x) * t12).toString(16).padStart(2, "0");
949
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
950
+ }
951
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
952
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
953
+ var FIXED_MAX_VALUE = 1.15;
954
+ var PEAK_HEADROOM = 1.03;
955
+ function scaleDenominator(scale, maxValue) {
956
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
957
+ }
958
+ function valueLabelFontSize(height) {
959
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
960
+ }
961
+ var SET_BAR_DEFAULT_HEIGHT = 220;
962
+ var LABEL_ROW_HEIGHT = 16;
963
+ var LABEL_GAP = 3;
964
+ var GAP_RATIO = 0.08;
965
+ var MIN_BAR_GAP = 2;
966
+ var CHUNK_NOTCH_RATIO = 0.25;
967
+ var CHUNK_NOTCH_MIN_PX = 10;
968
+ var BAR_MAX_WIDTH = 120;
969
+ var LABEL_MIN_BAR_WIDTH = 30;
970
+ var DEFAULT_BAR_RADIUS = 5;
971
+ var MIN_BAR_HEIGHT = 4;
972
+ var FLAT_BAR_FRACTION = 0.5;
973
+ var STUB_HEIGHT = 9;
974
+ var REP_GAP = 2;
975
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
976
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
977
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
978
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
979
+ return { barWidth, gap };
980
+ }
981
+ function isRep(slot) {
982
+ return slot.kind === "rep";
983
+ }
984
+ function repIndexByCell(cells) {
985
+ let count = 0;
986
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
987
+ }
988
+ function SetBarChart({
989
+ slots,
990
+ colorFor,
991
+ height,
992
+ scale = "peak",
993
+ scaleMax,
994
+ orientation = "up",
995
+ liveRepIndex,
996
+ isNewPeak = false,
997
+ targetReps,
998
+ gapRatio = GAP_RATIO,
999
+ barRadius = DEFAULT_BAR_RADIUS,
1000
+ cornerStyle = "top",
1001
+ flat = false,
1002
+ showValueLabels: showValueLabelsProp = false,
1003
+ formatValue: formatValue2 = String,
1004
+ todoVariant = "solid",
1005
+ minColumns,
1006
+ renderReference,
1007
+ renderBarOverlay,
1008
+ expandProgress,
1009
+ hideBaseline = false,
1010
+ testID,
1011
+ testIDPrefix = "setbar",
1012
+ accessibilityLabel: accessibilityLabel2,
1013
+ label,
1014
+ className,
1015
+ viewProps = {}
1016
+ }) {
1017
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
1018
+ const cells = targetPadded > slots.length ? [
1019
+ ...slots,
1020
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
1021
+ ] : slots;
1022
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
1023
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
1024
+ const flip = orientation === "down";
1025
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
1026
+ const placeholderColor = useOnSurfaceColor("tertiary");
1027
+ const surface = useSurface();
1028
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
1029
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
1030
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
1031
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
1032
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
1033
+ const showValueLabels = showValueLabelsProp && !flat;
1034
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
1035
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
1036
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
1037
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
1038
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
1039
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
1040
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
1041
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
1042
+ const [plotW, setPlotW] = react.useState(0);
1043
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
1044
+ const totalCells = cells.length;
1045
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
1046
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
1047
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
1048
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
1049
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
1050
+ const { style: externalStyle, ...restProps } = viewProps;
1051
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
1052
+ const repIndices = repIndexByCell(cells);
1053
+ return /* @__PURE__ */ jsxs(
431
1054
  reactNative.View,
432
1055
  {
433
1056
  className,
434
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
435
- accessibilityRole: "image",
436
- accessibilityLabel: describeSets(sets),
437
- testID: "set-strip",
438
- ...props,
439
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
1057
+ style: [{ height, flexDirection: "row" }, externalStyle],
1058
+ testID,
1059
+ ...a11y,
1060
+ ...restProps,
1061
+ children: [
1062
+ label ? /* @__PURE__ */ jsx(
1063
+ ChartSideRail,
1064
+ {
1065
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
1066
+ sectionExtent: height
1067
+ }
1068
+ ) : null,
1069
+ /* @__PURE__ */ jsxs(
1070
+ reactNative.View,
1071
+ {
1072
+ onLayout: onPlotLayout,
1073
+ style: {
1074
+ flex: 1,
1075
+ flexDirection: "row",
1076
+ alignItems: "flex-end",
1077
+ gap,
1078
+ position: "relative",
1079
+ borderBottomWidth: hideBaseline ? 0 : 2,
1080
+ borderBottomColor: placeholderColor,
1081
+ ...flipStyle
1082
+ },
1083
+ children: [
1084
+ renderReference?.(geometry),
1085
+ cells.map((slot, i) => {
1086
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
1087
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
1088
+ const column = {
1089
+ flex: 1,
1090
+ maxWidth: BAR_MAX_WIDTH,
1091
+ height: "100%",
1092
+ alignItems: "center",
1093
+ justifyContent: "flex-end",
1094
+ marginLeft: extraGap
1095
+ };
1096
+ if (!isRep(slot)) {
1097
+ return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
1098
+ slot.kind,
1099
+ barCorners,
1100
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
1101
+ flat ? compactBarHeight : STUB_HEIGHT,
1102
+ placeholderColor,
1103
+ testIDPrefix,
1104
+ todoVariant,
1105
+ solidTodoColor,
1106
+ emptyColor
1107
+ ) }, i);
1108
+ }
1109
+ const repIndex = repIndices[i];
1110
+ const value = slot.value ?? 0;
1111
+ const isLive = liveRepIndex === repIndex;
1112
+ const color = colorFor(value);
1113
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
1114
+ inputRange: [0, 1],
1115
+ outputRange: [flatBarHeight, valueBarHeight(value)]
1116
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
1117
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
1118
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
1119
+ return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
1120
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
1121
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
1122
+ /* @__PURE__ */ jsx(
1123
+ reactNative.Animated.View,
1124
+ {
1125
+ style: {
1126
+ position: "absolute",
1127
+ bottom: labelBottom,
1128
+ left: 0,
1129
+ right: 0,
1130
+ alignItems: "center",
1131
+ ...expandProgress ? { opacity: expandProgress } : null
1132
+ },
1133
+ pointerEvents: "none",
1134
+ children: /* @__PURE__ */ jsx(
1135
+ reactNative.Text,
1136
+ {
1137
+ className: "text-text-secondary",
1138
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
1139
+ testID: `${testIDPrefix}-label-${repIndex}`,
1140
+ children: formatValue2(value)
1141
+ }
1142
+ )
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ jsx(
1146
+ reactNative.Animated.View,
1147
+ {
1148
+ style: [
1149
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
1150
+ barCorners,
1151
+ barPaper(color, flip)
1152
+ ],
1153
+ testID: `${testIDPrefix}-bar-${repIndex}`
1154
+ }
1155
+ ),
1156
+ renderBarOverlay?.(repIndex, value)
1157
+ ] }, i);
1158
+ })
1159
+ ]
1160
+ }
1161
+ )
1162
+ ]
440
1163
  }
441
1164
  );
442
1165
  }
1166
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
1167
+ const base = {
1168
+ width: "100%",
1169
+ height: stubHeight,
1170
+ ...barCorners
1171
+ };
1172
+ if (kind === "variable") {
1173
+ return /* @__PURE__ */ jsx(
1174
+ reactNative.View,
1175
+ {
1176
+ style: { ...base, backgroundColor: VARIABLE_FILL },
1177
+ testID: `${testIDPrefix}-slot-variable`
1178
+ }
1179
+ );
1180
+ }
1181
+ if (kind === "continue") {
1182
+ return /* @__PURE__ */ jsx(
1183
+ reactNative.View,
1184
+ {
1185
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
1186
+ testID: `${testIDPrefix}-slot-continue`
1187
+ }
1188
+ );
1189
+ }
1190
+ if (kind === "empty") {
1191
+ return /* @__PURE__ */ jsx(
1192
+ reactNative.View,
1193
+ {
1194
+ style: { ...base, backgroundColor: emptyColor },
1195
+ testID: `${testIDPrefix}-slot-empty`
1196
+ }
1197
+ );
1198
+ }
1199
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
1200
+ return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
1201
+ }
1202
+ function sideLabelFontSize(height) {
1203
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
1204
+ }
1205
+ var SIDE_LABEL_INITIALS_BELOW = 70;
1206
+ function sideLabelText(label, extent) {
1207
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
1208
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
1209
+ }
1210
+ function ChartSideRail({
1211
+ sections,
1212
+ sectionExtent,
1213
+ variant = "hero"
1214
+ }) {
1215
+ if (!sections.some((s) => s.label)) return null;
1216
+ const isRail = variant === "rail";
1217
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
1218
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
1219
+ const textStyle = {
1220
+ maxWidth: "100%",
1221
+ textAlign: "center",
1222
+ fontSize,
1223
+ fontWeight: "700",
1224
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
1225
+ textTransform: "uppercase"
1226
+ };
1227
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
1228
+ reactNative.View,
1229
+ {
1230
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
1231
+ children: s.label ? /* @__PURE__ */ jsx(
1232
+ reactNative.View,
1233
+ {
1234
+ style: {
1235
+ width: labelLength,
1236
+ alignItems: "center",
1237
+ transform: [{ rotate: "-90deg" }]
1238
+ },
1239
+ children: /* @__PURE__ */ jsx(
1240
+ reactNative.Text,
1241
+ {
1242
+ className: "text-text-tertiary",
1243
+ style: textStyle,
1244
+ numberOfLines: 1,
1245
+ testID: s.testID,
1246
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
1247
+ }
1248
+ )
1249
+ }
1250
+ ) : null
1251
+ },
1252
+ s.testID
1253
+ )) });
1254
+ }
443
1255
 
444
1256
  // src/components/custom/Workout/VelocityStrip.tsx
445
1257
  var VEL_COLORS = WORKOUT_TOKENS.scale;
@@ -485,6 +1297,18 @@ function calculateMeanVelocity(velocities) {
485
1297
  const sum = velocities.reduce((acc, v) => acc + v, 0);
486
1298
  return sum / velocities.length;
487
1299
  }
1300
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
1301
+ function getVelocityLossColor(lossPct) {
1302
+ const [t1, t22, t32] = VL_LOSS_THRESHOLDS;
1303
+ if (lossPct < t1) return VEL_COLORS.green;
1304
+ if (lossPct < t22) return VEL_COLORS.yellow;
1305
+ if (lossPct < t32) return VEL_COLORS.orange;
1306
+ return VEL_COLORS.red;
1307
+ }
1308
+ function velocityLossForRep(velocity, best) {
1309
+ if (best <= 0) return 0;
1310
+ return Math.max(0, Math.round((best - velocity) / best * 100));
1311
+ }
488
1312
  function classifyBand(velocity, bands) {
489
1313
  for (const band of bands) {
490
1314
  if (band.max === null || velocity < band.max) return band;
@@ -494,15 +1318,17 @@ function classifyBand(velocity, bands) {
494
1318
  function bandColor(band) {
495
1319
  return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
496
1320
  }
1321
+ function makeBarColorFor(zones) {
1322
+ const hasZones = zones != null && zones.length > 0;
1323
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
1324
+ }
497
1325
  function getLossStyle(loss) {
498
1326
  if (loss > 25) return { color: VEL_COLORS.red };
499
1327
  if (loss > 20) return { color: VEL_COLORS.orange };
500
1328
  return null;
501
1329
  }
502
- var REP_GAP = 2;
1330
+ var REP_GAP2 = 2;
503
1331
  var WIDE_GAP = 8;
504
- var TODO_COLOR2 = primitiveColors.charcoal[300];
505
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
506
1332
  function deriveDoneVelocities(set) {
507
1333
  switch (set.type) {
508
1334
  case "drop":
@@ -518,7 +1344,7 @@ function chunkedRepSlots(chunks) {
518
1344
  chunks.forEach((chunk) => {
519
1345
  chunk.forEach((velocity, ri) => {
520
1346
  const first = slots.length === 0;
521
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
1347
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
522
1348
  });
523
1349
  });
524
1350
  return slots;
@@ -532,7 +1358,7 @@ function buildSlots(set) {
532
1358
  return {
533
1359
  kind: done ? "rep" : "todo",
534
1360
  velocity: done ? set.velocities[i] : void 0,
535
- leadingGap: i === 0 ? 0 : REP_GAP
1361
+ leadingGap: i === 0 ? 0 : REP_GAP2
536
1362
  };
537
1363
  });
538
1364
  }
@@ -543,7 +1369,7 @@ function buildSlots(set) {
543
1369
  return {
544
1370
  kind,
545
1371
  velocity: kind === "rep" ? set.velocities[i] : void 0,
546
- leadingGap: i === 0 ? 0 : REP_GAP
1372
+ leadingGap: i === 0 ? 0 : REP_GAP2
547
1373
  };
548
1374
  });
549
1375
  }
@@ -551,16 +1377,16 @@ function buildSlots(set) {
551
1377
  const reps = set.velocities.map((velocity, i) => ({
552
1378
  kind: "rep",
553
1379
  velocity,
554
- leadingGap: i === 0 ? 0 : REP_GAP
1380
+ leadingGap: i === 0 ? 0 : REP_GAP2
555
1381
  }));
556
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
1382
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
557
1383
  return reps;
558
1384
  }
559
1385
  case "drop":
560
1386
  return chunkedRepSlots(set.subloads);
561
1387
  case "myo": {
562
1388
  const slots = chunkedRepSlots([set.activation, ...set.clusters]);
563
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
1389
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
564
1390
  return slots;
565
1391
  }
566
1392
  case "cluster": {
@@ -571,7 +1397,7 @@ function buildSlots(set) {
571
1397
  return {
572
1398
  kind: done ? "rep" : "todo",
573
1399
  velocity: done ? set.velocities[i] : void 0,
574
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
1400
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
575
1401
  };
576
1402
  });
577
1403
  }
@@ -593,253 +1419,148 @@ function setAccessibilityLabel(set, repCount) {
593
1419
  return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
594
1420
  }
595
1421
  }
596
- function slotAccessibilityLabel(slot, index) {
597
- switch (slot.kind) {
598
- case "rep":
599
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
600
- case "todo":
601
- return `Rep ${index + 1}: planned`;
602
- case "variable":
603
- return `Rep ${index + 1}: variable`;
604
- case "continue":
605
- return "Keep going";
606
- }
607
- }
608
- var EXPANDED_TODO_STUB_PCT = 16;
609
- var EXPANDED_ENCODED_PCT = 45;
610
1422
  var EXPANDED_HEIGHT = 60;
611
- var FIXED_MAX_VELOCITY = 1.15;
612
- var BARE_STUB_HEIGHT = 3;
613
- var HERO_HEIGHT = 220;
614
- var HERO_LABEL_HEADROOM = 20;
615
- var HERO_BAR_GAP = 8;
616
- var HERO_BAR_MAX_WIDTH = 120;
617
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
618
- var HERO_BAR_RADIUS = 5;
619
- var HERO_MIN_BAR_HEIGHT = 4;
620
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
1423
+ var COMPACT_HEIGHT = 8;
621
1424
  var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
622
- var PEAK_HEADROOM = 1.06;
623
- function scaleDenominator(scale, maxVelocity) {
624
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
625
- }
626
- function bareSlotHeight(slot, height, denom) {
627
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
628
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
629
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
630
- }
631
- function getReducedMotionPreference() {
632
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
633
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
634
- }
635
- function usePrefersReducedMotion() {
636
- const [reduced, setReduced] = react.useState(getReducedMotionPreference);
637
- react.useEffect(() => {
638
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
639
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
640
- const handler = () => setReduced(mq.matches);
641
- handler();
642
- mq.addEventListener?.("change", handler);
643
- return () => mq.removeEventListener?.("change", handler);
644
- }, []);
645
- return reduced;
646
- }
647
- var ANIMATION_DURATION = 400;
648
- var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
649
- var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
650
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
651
- const prefersReducedMotion = usePrefersReducedMotion();
652
- const [liveScale] = react.useState(() => new reactNative.Animated.Value(1));
653
- react.useEffect(() => {
654
- if (!active || liveRepIndex == null || liveVelocity == null) return;
655
- if (prefersReducedMotion) {
656
- liveScale.setValue(1);
657
- return;
658
- }
659
- if (isNewPeak) {
660
- liveScale.setValue(1);
661
- reactNative.Animated.sequence([
662
- reactNative.Animated.timing(liveScale, {
663
- toValue: 1.25,
664
- duration: 150,
665
- easing: reactNative.Easing.out(reactNative.Easing.quad),
666
- useNativeDriver: true
667
- }),
668
- reactNative.Animated.spring(liveScale, {
669
- toValue: 1,
670
- friction: 3,
671
- tension: 140,
672
- useNativeDriver: true
673
- })
674
- ]).start();
675
- } else {
676
- liveScale.setValue(0.8);
677
- reactNative.Animated.timing(liveScale, {
678
- toValue: 1,
679
- duration: 300,
680
- easing: POP_EASING,
681
- useNativeDriver: true
682
- }).start();
1425
+ function DashedReferenceLine({
1426
+ anchor,
1427
+ offset,
1428
+ testID
1429
+ }) {
1430
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
1431
+ return /* @__PURE__ */ jsx(
1432
+ reactNative.View,
1433
+ {
1434
+ accessibilityElementsHidden: true,
1435
+ style: {
1436
+ position: "absolute",
1437
+ left: 0,
1438
+ right: 0,
1439
+ borderTopWidth: 1,
1440
+ borderStyle: "dashed",
1441
+ borderColor: HERO_REFERENCE_COLOR,
1442
+ pointerEvents: "none",
1443
+ ...edge
1444
+ },
1445
+ testID
683
1446
  }
684
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
685
- return liveScale;
1447
+ );
686
1448
  }
687
- function HeroVelocityChart({
688
- doneVelocities,
689
- barColorFor,
1449
+ var BAND_LABEL_FONT = "monospace";
1450
+ var VL_LABEL_MIN_PLOT = 65;
1451
+ var VL_SEMANTIC = getSemanticColors("dark");
1452
+ function VelocityLossBands({
1453
+ best,
690
1454
  scaleDenom,
691
- referenceVelocity,
692
- liveRepIndex,
693
- isNewPeak,
694
- targetReps,
695
- height,
696
- className,
697
- viewProps
1455
+ plotHeight,
1456
+ flip = false
698
1457
  }) {
699
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
700
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
701
- const [plotW, setPlotW] = react.useState(0);
702
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
703
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
704
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
705
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
706
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
707
- const totalSlots = doneVelocities.length + pendingCount;
708
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
709
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
710
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
711
- const peakIndex = doneVelocities.reduce(
712
- (best, v, i) => v > doneVelocities[best] ? i : best,
713
- 0
714
- );
715
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
716
- const repCount = doneVelocities.length;
717
- const total = Math.max(repCount, targetReps ?? repCount);
718
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
719
- const { style: externalStyle, ...restProps } = viewProps;
720
- return /* @__PURE__ */ jsx(
1458
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
1459
+ const yOf = (v) => v / scaleDenom * plotHeight;
1460
+ const vl20 = best * 0.8;
1461
+ const vl30 = best * 0.7;
1462
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
1463
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
1464
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
721
1465
  reactNative.View,
722
1466
  {
723
- className,
724
- style: [{ height }, externalStyle],
725
- accessibilityRole: "image",
726
- accessibilityLabel: label,
727
- testID: "velocity-strip-hero",
728
- ...restProps,
729
- children: /* @__PURE__ */ jsxs(
730
- reactNative.View,
731
- {
732
- onLayout: onPlotLayout,
733
- style: {
734
- flex: 1,
735
- flexDirection: "row",
736
- alignItems: "flex-end",
737
- gap: heroGap,
738
- position: "relative",
739
- borderBottomWidth: 2,
740
- borderBottomColor: HERO_PENDING_COLOR
741
- },
742
- children: [
743
- referencePx > 0 && /* @__PURE__ */ jsx(
744
- reactNative.View,
745
- {
746
- accessibilityElementsHidden: true,
747
- style: {
748
- position: "absolute",
749
- left: 0,
750
- right: 0,
751
- bottom: referencePx,
752
- borderTopWidth: 1,
753
- borderStyle: "dashed",
754
- borderColor: HERO_REFERENCE_COLOR,
755
- pointerEvents: "none"
756
- },
757
- testID: "velocity-hero-reference"
758
- }
759
- ),
760
- doneVelocities.map((velocity, i) => {
761
- const isLive = liveRepIndex === i;
762
- return /* @__PURE__ */ jsxs(
763
- reactNative.View,
764
- {
765
- accessibilityElementsHidden: true,
766
- style: {
767
- flex: 1,
768
- maxWidth: HERO_BAR_MAX_WIDTH,
769
- height: "100%",
770
- alignItems: "center",
771
- justifyContent: "flex-end"
772
- },
773
- children: [
774
- showBarLabel(i) && /* @__PURE__ */ jsx(
775
- reactNative.Text,
776
- {
777
- className: "text-text-primary",
778
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
779
- testID: `velocity-label-${i}`,
780
- children: formatVelocity(velocity)
781
- }
782
- ),
783
- /* @__PURE__ */ jsx(
784
- reactNative.Animated.View,
785
- {
786
- style: [
787
- {
788
- width: "100%",
789
- height: barHeight(velocity),
790
- borderTopLeftRadius: HERO_BAR_RADIUS,
791
- borderTopRightRadius: HERO_BAR_RADIUS,
792
- backgroundColor: barColorFor(velocity)
793
- },
794
- isLive ? { transform: [{ scale: liveScale }] } : null
795
- ],
796
- testID: `velocity-bar-${i}`
797
- }
798
- )
799
- ]
800
- },
801
- i
802
- );
803
- }),
804
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
805
- reactNative.View,
806
- {
807
- accessibilityElementsHidden: true,
808
- style: {
809
- flex: 1,
810
- maxWidth: HERO_BAR_MAX_WIDTH,
811
- height: "100%",
812
- alignItems: "center",
813
- justifyContent: "flex-end"
814
- },
815
- children: /* @__PURE__ */ jsx(
816
- reactNative.View,
817
- {
818
- style: {
819
- width: "100%",
820
- height: HERO_MIN_BAR_HEIGHT * 3,
821
- borderWidth: 1,
822
- borderStyle: "dashed",
823
- borderColor: HERO_PENDING_COLOR,
824
- borderTopLeftRadius: HERO_BAR_RADIUS,
825
- borderTopRightRadius: HERO_BAR_RADIUS
826
- },
827
- testID: "velocity-slot-todo"
828
- }
829
- )
1467
+ style: {
1468
+ position: "absolute",
1469
+ left: 0,
1470
+ right: 0,
1471
+ bottom: yOf(loV),
1472
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
1473
+ backgroundColor: color
1474
+ }
1475
+ }
1476
+ );
1477
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
1478
+ reactNative.View,
1479
+ {
1480
+ style: {
1481
+ position: "absolute",
1482
+ left: 0,
1483
+ right: 0,
1484
+ bottom: yOf(v),
1485
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
1486
+ // band edge — instead of floating up half a text-row (the label then centres on
1487
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
1488
+ height: 0,
1489
+ flexDirection: "row",
1490
+ alignItems: "center"
1491
+ },
1492
+ children: [
1493
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
1494
+ showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1495
+ /* @__PURE__ */ jsx(
1496
+ reactNative.Text,
1497
+ {
1498
+ style: {
1499
+ marginHorizontal: 6,
1500
+ fontSize: vlFont,
1501
+ fontWeight: "800",
1502
+ fontFamily: BAND_LABEL_FONT,
1503
+ color,
1504
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
830
1505
  },
831
- `pending-${i}`
832
- ))
833
- ]
834
- }
835
- )
1506
+ children: label
1507
+ }
1508
+ ),
1509
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
1510
+ ] }) : null
1511
+ ]
836
1512
  }
837
1513
  );
1514
+ return /* @__PURE__ */ jsxs(
1515
+ reactNative.View,
1516
+ {
1517
+ accessibilityElementsHidden: true,
1518
+ pointerEvents: "none",
1519
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
1520
+ testID: "velocity-loss-bands",
1521
+ children: [
1522
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
1523
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
1524
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
1525
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
1526
+ ]
1527
+ }
1528
+ );
1529
+ }
1530
+ function velocityReferenceOverlay(g, showLossBands) {
1531
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
1532
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1533
+ showLossBands && /* @__PURE__ */ jsx(
1534
+ VelocityLossBands,
1535
+ {
1536
+ best: g.best,
1537
+ scaleDenom: g.scaleDenom,
1538
+ plotHeight: g.plotHeight,
1539
+ flip: g.flip
1540
+ }
1541
+ ),
1542
+ referencePx > 0 && /* @__PURE__ */ jsx(
1543
+ DashedReferenceLine,
1544
+ {
1545
+ anchor: "bottom",
1546
+ offset: referencePx,
1547
+ testID: "velocity-hero-reference"
1548
+ }
1549
+ )
1550
+ ] });
838
1551
  }
1552
+ var ANIMATION_DURATION = 400;
839
1553
  function VelocityStrip({
840
1554
  velocities,
841
1555
  set,
842
1556
  zones,
1557
+ barColor = "loss",
1558
+ showLossBands,
1559
+ orientation = "up",
1560
+ scaleMax,
1561
+ hideBaseline,
1562
+ columnSlots,
1563
+ label,
843
1564
  liveRepIndex,
844
1565
  expanded = true,
845
1566
  onToggle,
@@ -853,51 +1574,29 @@ function VelocityStrip({
853
1574
  className,
854
1575
  ...props
855
1576
  }) {
1577
+ const lossBandsOn = showLossBands ?? barColor === "loss";
856
1578
  const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
857
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
858
- kind: "rep",
859
- velocity: v,
860
- leadingGap: i === 0 ? 0 : REP_GAP
861
- }));
862
1579
  const maxVelocity = Math.max(...doneVelocities, 0);
863
1580
  const meanVelocity = calculateMeanVelocity(doneVelocities);
864
1581
  const loss = calculateVelocityLoss(doneVelocities);
865
1582
  const framed = showNumbers || showInfo;
866
- const scaleDenom = scaleDenominator(scale, maxVelocity);
867
1583
  const hasZones = zones != null && zones.length > 0;
868
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
869
- const slotColor = (slot) => {
870
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
871
- if (slot.kind === "todo") return TODO_COLOR2;
872
- return SET_STRIP_VARIABLE_COLOR;
873
- };
1584
+ const zoneColorFor = makeBarColorFor(zones);
1585
+ const barColorFor = (v) => barColor === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
874
1586
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
875
- const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
876
- const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
1587
+ const [expandProgress] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
877
1588
  const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
878
1589
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
879
1590
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
880
- const liveScale = useLiveRepPop(
881
- variant === "expanded" && framed,
882
- liveRepIndex,
883
- liveVelocity,
884
- isNewPeak
885
- );
886
1591
  react.useEffect(() => {
887
1592
  if (variant !== "expanded" || !framed) return;
888
1593
  reactNative.Animated.parallel([
889
- reactNative.Animated.timing(heightAnim, {
890
- toValue: expanded ? height : 3,
1594
+ reactNative.Animated.timing(expandProgress, {
1595
+ toValue: expanded ? 1 : 0,
891
1596
  duration: ANIMATION_DURATION,
892
1597
  easing: ANIMATION_EASING,
893
1598
  useNativeDriver: false
894
1599
  }),
895
- reactNative.Animated.timing(labelOpacity, {
896
- toValue: expanded ? 1 : 0,
897
- duration: 300,
898
- delay: expanded ? 150 : 0,
899
- useNativeDriver: false
900
- }),
901
1600
  reactNative.Animated.timing(infoOpacity, {
902
1601
  toValue: expanded ? 1 : 0,
903
1602
  duration: 300,
@@ -905,250 +1604,176 @@ function VelocityStrip({
905
1604
  useNativeDriver: false
906
1605
  })
907
1606
  ]).start();
908
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
1607
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
909
1608
  if (set == null && velocities == null) return null;
910
1609
  const repCount = doneVelocities.length;
911
1610
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
912
1611
  if (variant === "hero") {
913
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
1612
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
1613
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1614
+ kind: s.kind,
1615
+ value: s.velocity,
1616
+ leadingGap: s.leadingGap
1617
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1618
+ const total = Math.max(repCount, targetReps ?? repCount);
1619
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
914
1620
  return /* @__PURE__ */ jsx(
915
- HeroVelocityChart,
1621
+ SetBarChart,
916
1622
  {
917
- doneVelocities,
918
- barColorFor,
919
- scaleDenom,
920
- referenceVelocity: maxVelocity,
1623
+ slots: heroSlots,
1624
+ colorFor: barColorFor,
1625
+ height: heroHeight,
1626
+ scale,
1627
+ scaleMax,
1628
+ orientation,
921
1629
  liveRepIndex,
922
1630
  isNewPeak,
923
- targetReps,
924
- height: heroHeight,
1631
+ targetReps: set || columnSlots ? void 0 : targetReps,
1632
+ label,
1633
+ showValueLabels: true,
1634
+ formatValue: formatVelocity,
1635
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
1636
+ hideBaseline: true,
1637
+ testID: "velocity-strip-hero",
1638
+ testIDPrefix: "velocity",
1639
+ accessibilityLabel: heroLabel,
925
1640
  className,
926
1641
  viewProps: props
927
1642
  }
928
1643
  );
929
1644
  }
930
- if (variant === "mini") {
931
- const { style: externalStyle, ...restProps } = props;
1645
+ if (variant === "compact") {
1646
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1647
+ kind: s.kind,
1648
+ value: s.velocity,
1649
+ leadingGap: s.leadingGap
1650
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1651
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
932
1652
  return /* @__PURE__ */ jsx(
933
- reactNative.View,
1653
+ SetBarChart,
934
1654
  {
935
- className,
936
- style: [
937
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
938
- externalStyle
939
- ],
940
- accessibilityRole: "image",
1655
+ slots: compactSlots,
1656
+ colorFor: barColorFor,
1657
+ height: compactHeight,
1658
+ scale,
1659
+ scaleMax,
1660
+ orientation,
1661
+ flat: true,
1662
+ barRadius: 2,
1663
+ cornerStyle: "all",
1664
+ targetReps: set || columnSlots ? void 0 : targetReps,
1665
+ label,
1666
+ hideBaseline: true,
1667
+ testID: "velocity-strip-compact",
1668
+ testIDPrefix: "velocity",
941
1669
  accessibilityLabel: miniLabel,
942
- testID: "velocity-strip-mini",
943
- ...restProps,
944
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
945
- reactNative.View,
946
- {
947
- style: {
948
- flex: 1,
949
- backgroundColor: slotColor(slot),
950
- borderRadius: 1,
951
- minWidth: 4,
952
- height: "100%",
953
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
954
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
955
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
956
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
957
- },
958
- accessibilityElementsHidden: true,
959
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
960
- },
961
- i
962
- ))
1670
+ className,
1671
+ viewProps: props
963
1672
  }
964
1673
  );
965
1674
  }
966
1675
  if (!framed) {
967
- const { style: externalStyle, ...restProps } = props;
1676
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1677
+ kind: s.kind,
1678
+ value: s.velocity,
1679
+ leadingGap: s.leadingGap
1680
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
968
1681
  return /* @__PURE__ */ jsx(
969
- reactNative.View,
1682
+ SetBarChart,
970
1683
  {
971
- className,
972
- style: [
973
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
974
- externalStyle
975
- ],
976
- accessibilityRole: "image",
1684
+ slots: spotlightSlots,
1685
+ colorFor: barColorFor,
1686
+ height,
1687
+ scale,
1688
+ scaleMax,
1689
+ orientation,
1690
+ liveRepIndex,
1691
+ isNewPeak,
1692
+ barRadius: 2,
1693
+ cornerStyle: "top",
1694
+ targetReps: set ? void 0 : targetReps,
1695
+ label,
1696
+ hideBaseline: true,
1697
+ testID: "velocity-strip-spotlight",
1698
+ testIDPrefix: "velocity",
977
1699
  accessibilityLabel: miniLabel,
978
- testID: "velocity-strip-compact",
979
- ...restProps,
980
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
981
- reactNative.View,
982
- {
983
- style: {
984
- flex: 1,
985
- minWidth: 4,
986
- height: bareSlotHeight(slot, height, scaleDenom),
987
- // Top-rounded to match the framed chart's bars (the "rounded tops from
988
- // the numbers one"); square bottoms since bars sit on the baseline.
989
- borderTopLeftRadius: 2,
990
- borderTopRightRadius: 2,
991
- backgroundColor: slotColor(slot),
992
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
993
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
994
- },
995
- accessibilityElementsHidden: true,
996
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
997
- },
998
- i
999
- ))
1700
+ className,
1701
+ viewProps: props
1000
1702
  }
1001
1703
  );
1002
1704
  }
1003
1705
  const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
1004
1706
  const hasInteractiveContainer = onToggle != null;
1005
1707
  const hasInteractiveReps = onRepPress != null && expanded;
1006
- const setBars = set != null && slots.map((slot, i) => {
1007
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
1008
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
1009
- return /* @__PURE__ */ jsx(
1010
- reactNative.View,
1708
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
1709
+ kind: sl.kind,
1710
+ value: sl.velocity,
1711
+ leadingGap: sl.leadingGap
1712
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
1713
+ const needsBarOverlay = showNumbers || onRepPress != null;
1714
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1715
+ showNumbers && /* @__PURE__ */ jsx(
1716
+ reactNative.Animated.View,
1011
1717
  {
1012
1718
  style: {
1013
- flex: 1,
1014
- height: "100%",
1015
- justifyContent: "flex-end",
1016
- marginLeft: slot.leadingGap
1719
+ opacity: expandProgress,
1720
+ position: "absolute",
1721
+ top: -13,
1722
+ left: 0,
1723
+ right: 0,
1724
+ alignItems: "center"
1017
1725
  },
1726
+ accessibilityElementsHidden: true,
1727
+ pointerEvents: "none",
1018
1728
  children: /* @__PURE__ */ jsx(
1019
- reactNative.View,
1729
+ reactNative.Text,
1020
1730
  {
1021
- style: {
1022
- height: `${heightPct}%`,
1023
- minHeight: 2,
1024
- borderTopLeftRadius: 2,
1025
- borderTopRightRadius: 2,
1026
- backgroundColor: slotColor(slot),
1027
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1028
- },
1029
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
1030
- accessibilityRole: "image",
1031
- accessibilityLabel: slotAccessibilityLabel(slot, i)
1731
+ className: "text-text-secondary",
1732
+ style: { fontSize: 8, fontWeight: "600" },
1733
+ testID: `velocity-label-${repIndex}`,
1734
+ children: formatVelocity(value)
1032
1735
  }
1033
1736
  )
1034
- },
1035
- i
1036
- );
1037
- });
1737
+ }
1738
+ ),
1739
+ onRepPress && expanded && /* @__PURE__ */ jsx(
1740
+ reactNative.Pressable,
1741
+ {
1742
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
1743
+ onPress: () => onRepPress(repIndex, value),
1744
+ accessibilityRole: "button",
1745
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
1746
+ testID: `velocity-bar-pressable-${repIndex}`
1747
+ }
1748
+ )
1749
+ ] }) : void 0;
1038
1750
  const stripContent = /* @__PURE__ */ jsxs(
1039
1751
  reactNative.Animated.View,
1040
1752
  {
1041
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
1042
- style: [
1043
- {
1044
- height: heightAnim,
1045
- width: "100%",
1046
- gap: 2,
1047
- borderRadius: expanded ? 6 : 2,
1048
- paddingTop: expanded ? 16 : 0,
1049
- paddingBottom: expanded ? 24 : 0,
1050
- // No horizontal inset — the framed chart's bars span full width, matching
1051
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
1052
- paddingHorizontal: 0,
1053
- paddingVertical: expanded ? void 0 : 8,
1054
- overflow: expanded ? "visible" : "hidden"
1055
- }
1056
- ],
1753
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
1754
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
1057
1755
  accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
1058
1756
  accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
1059
1757
  testID: "velocity-strip",
1060
1758
  ...props,
1061
1759
  children: [
1062
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
1063
- const barBackground = barColorFor(v);
1064
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
1065
- const isLive = liveRepIndex === i;
1066
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
1067
- const barStyle = expanded ? {
1068
- height: `${barHeightPct}%`,
1069
- minHeight: 2,
1070
- borderTopLeftRadius: 2,
1071
- borderTopRightRadius: 2,
1072
- backgroundColor: barBackground
1073
- } : {
1074
- minHeight: "100%",
1075
- borderTopLeftRadius: 2,
1076
- borderTopRightRadius: 2,
1077
- borderBottomLeftRadius: 0,
1078
- borderBottomRightRadius: 0,
1079
- backgroundColor: barBackground
1080
- };
1081
- const barInner = isLive ? /* @__PURE__ */ jsx(
1082
- reactNative.Animated.View,
1083
- {
1084
- style: [barStyle, { transform: [{ scale: liveScale }] }],
1085
- testID: `velocity-bar-${i}`,
1086
- accessibilityRole: "image",
1087
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
1088
- }
1089
- ) : /* @__PURE__ */ jsx(
1090
- reactNative.View,
1091
- {
1092
- style: barStyle,
1093
- testID: `velocity-bar-${i}`,
1094
- accessibilityRole: "image",
1095
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
1096
- }
1097
- );
1098
- const bar = /* @__PURE__ */ jsxs(
1099
- reactNative.View,
1100
- {
1101
- style: {
1102
- flex: 1,
1103
- height: "100%",
1104
- justifyContent: "flex-end",
1105
- position: "relative"
1106
- },
1107
- children: [
1108
- expanded && showNumbers && /* @__PURE__ */ jsx(
1109
- reactNative.Animated.View,
1110
- {
1111
- style: {
1112
- opacity: labelOpacity,
1113
- alignItems: "center",
1114
- position: "absolute",
1115
- top: -13,
1116
- left: 0,
1117
- right: 0
1118
- },
1119
- accessibilityElementsHidden: true,
1120
- children: /* @__PURE__ */ jsx(
1121
- reactNative.Text,
1122
- {
1123
- className: "text-text-secondary",
1124
- style: { fontSize: 8, fontWeight: "600" },
1125
- testID: `velocity-label-${i}`,
1126
- children: formatVelocity(v)
1127
- }
1128
- )
1129
- }
1130
- ),
1131
- barInner
1132
- ]
1133
- },
1134
- i
1135
- );
1136
- if (onRepPress && expanded) {
1137
- return /* @__PURE__ */ jsx(
1138
- reactNative.Pressable,
1139
- {
1140
- style: { flex: 1, height: "100%" },
1141
- onPress: () => onRepPress(i, v),
1142
- accessibilityRole: "button",
1143
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
1144
- testID: `velocity-bar-pressable-${i}`,
1145
- children: bar
1146
- },
1147
- i
1148
- );
1760
+ /* @__PURE__ */ jsx(
1761
+ SetBarChart,
1762
+ {
1763
+ slots: framedSlots,
1764
+ colorFor: barColorFor,
1765
+ height,
1766
+ scale,
1767
+ scaleMax,
1768
+ expandProgress,
1769
+ renderBarOverlay: renderFramedBarOverlay,
1770
+ targetReps: set ? void 0 : targetReps,
1771
+ barRadius: 2,
1772
+ cornerStyle: "top",
1773
+ hideBaseline: true,
1774
+ testIDPrefix: "velocity"
1149
1775
  }
1150
- return bar;
1151
- }) }),
1776
+ ),
1152
1777
  expanded && showInfo && /* @__PURE__ */ jsxs(
1153
1778
  reactNative.Animated.View,
1154
1779
  {
@@ -1156,10 +1781,8 @@ function VelocityStrip({
1156
1781
  flexDirection: "row",
1157
1782
  justifyContent: "space-between",
1158
1783
  opacity: infoOpacity,
1159
- position: "absolute",
1160
- bottom: 4,
1161
- left: 6,
1162
- right: 6
1784
+ marginTop: 6,
1785
+ paddingHorizontal: 6
1163
1786
  },
1164
1787
  testID: "velocity-info-row",
1165
1788
  children: [
@@ -1167,10 +1790,7 @@ function VelocityStrip({
1167
1790
  reactNative.Text,
1168
1791
  {
1169
1792
  className: "text-text-secondary",
1170
- style: {
1171
- fontSize: 10,
1172
- fontFamily: "Inter, sans-serif"
1173
- },
1793
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
1174
1794
  children: [
1175
1795
  meanZone,
1176
1796
  " ",
@@ -1185,11 +1805,7 @@ function VelocityStrip({
1185
1805
  reactNative.Text,
1186
1806
  {
1187
1807
  className: "text-text-secondary",
1188
- style: {
1189
- fontSize: 10,
1190
- fontFamily: "Inter, sans-serif",
1191
- ...getLossStyle(loss)
1192
- },
1808
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
1193
1809
  children: [
1194
1810
  "Loss: ",
1195
1811
  loss,
@@ -1368,13 +1984,14 @@ function InfoIcon(props) {
1368
1984
  /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1369
1985
  ] });
1370
1986
  }
1987
+ var t = getSemanticColors("dark");
1371
1988
  var baseBadgeSizeConfig = {
1372
1989
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
1373
1990
  md: { fontSize: 10, paddingH: 8, paddingV: 2, iconSize: 12 },
1374
1991
  lg: { fontSize: 12, paddingH: 10, paddingV: 4, iconSize: 14 }
1375
1992
  };
1376
1993
  var variantColors = {
1377
- plain: { backgroundColor: "#1C1C1C", borderColor: "#1F1F1F" },
1994
+ plain: { backgroundColor: t["surface-raised"], borderColor: "#1F1F1F" },
1378
1995
  pr: {
1379
1996
  backgroundColor: "rgba(255, 121, 0, 0.12)",
1380
1997
  borderColor: "rgba(255, 121, 0, 0.3)"
@@ -1406,310 +2023,44 @@ function BaseBadge({
1406
2023
  useNativeDriver: true
1407
2024
  }).start();
1408
2025
  };
1409
- const badge = /* @__PURE__ */ jsxs(
1410
- reactNative.View,
1411
- {
1412
- className,
1413
- style: {
1414
- flexDirection: "row",
1415
- alignItems: "center",
1416
- gap: 3,
1417
- // borderRadius 2 is intentional per workout-tokens.ts (squared-off pill)
1418
- borderRadius: 2,
1419
- borderWidth: 1,
1420
- borderColor: colors.borderColor,
1421
- backgroundColor: colors.backgroundColor,
1422
- paddingHorizontal: config.paddingH,
1423
- paddingVertical: config.paddingV
1424
- },
1425
- ...props,
1426
- children: [
1427
- icon != null && /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "base-badge-icon", children: icon }),
1428
- children
1429
- ]
1430
- }
1431
- );
1432
- if (onPress) {
1433
- return /* @__PURE__ */ jsx(reactNative.Animated.View, { style: { transform: [{ scale: scaleAnim }] }, children: /* @__PURE__ */ jsx(
1434
- reactNative.Pressable,
1435
- {
1436
- onPress,
1437
- onPressIn: handlePressIn,
1438
- onPressOut: handlePressOut,
1439
- accessibilityRole: "button",
1440
- accessibilityLabel: props.accessibilityLabel,
1441
- testID: "base-badge-pressable",
1442
- children: badge
1443
- }
1444
- ) });
1445
- }
1446
- return badge;
1447
- }
1448
-
1449
- // src/theme/tokens/semantic.ts
1450
- var semanticColorsLight = {
1451
- // Brand colors (brand-*)
1452
- "brand-primary": primitiveRamps.orange[400],
1453
- "brand-primary-light": primitiveRamps.orange[300],
1454
- "brand-primary-dark": primitiveRamps.orange[500],
1455
- "brand-primary-subtle": "rgba(255, 121, 0, 0.08)",
1456
- "brand-primary-hover": primitiveRamps.orange[500],
1457
- "brand-primary-active": primitiveRamps.orange[600],
1458
- "brand-secondary": primitiveRamps.cyan[600],
1459
- "brand-secondary-light": primitiveRamps.cyan[500],
1460
- "brand-secondary-dark": primitiveRamps.cyan[700],
1461
- "brand-secondary-subtle": "rgba(48, 123, 155, 0.08)",
1462
- "brand-secondary-hover": primitiveRamps.cyan[700],
1463
- "brand-secondary-active": primitiveRamps.cyan[800],
1464
- // Text on brand backgrounds (on-*)
1465
- "on-brand-primary": primitiveColors.white,
1466
- "on-brand-secondary": primitiveColors.white,
1467
- // Status colors (status-*)
1468
- "status-success": primitiveRamps.green[300],
1469
- "status-success-light": primitiveRamps.green[200],
1470
- "status-success-dark": primitiveRamps.green[600],
1471
- "status-success-subtle": primitiveRamps.green[50],
1472
- // Live-session accent — its OWN role, decoupled from success so the two can diverge
1473
- "status-live": primitiveRamps.green[300],
1474
- "status-live-muted": primitiveRamps.green[500],
1475
- "status-error": primitiveRamps.red[600],
1476
- "status-error-light": primitiveRamps.red[500],
1477
- "status-error-dark": primitiveRamps.red[700],
1478
- "status-error-subtle": primitiveRamps.red[50],
1479
- "status-error-vivid": primitiveColors.redVivid[500],
1480
- // vivid alert red
1481
- "status-error-vivid-light": primitiveColors.redVivid[400],
1482
- "status-error-vivid-dark": primitiveColors.redVivid[700],
1483
- "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
1484
- "status-warning": primitiveRamps.amber[300],
1485
- "status-warning-light": primitiveRamps.amber[200],
1486
- "status-warning-dark": primitiveRamps.amber[500],
1487
- "status-warning-subtle": primitiveRamps.amber[50],
1488
- "status-info": primitiveRamps.blue[500],
1489
- "status-info-light": primitiveRamps.blue[300],
1490
- "status-info-dark": primitiveRamps.blue[600],
1491
- "status-info-subtle": primitiveRamps.blue[50],
1492
- // Text on status backgrounds (on-status-*)
1493
- "on-status-success": primitiveColors.white,
1494
- "on-status-error": primitiveColors.white,
1495
- "on-status-warning": primitiveColors.white,
1496
- "on-status-info": primitiveColors.white,
1497
- // Result/outcome indicators (result-*)
1498
- "result-improve": "#4caf50",
1499
- // Green - positive outcome
1500
- "result-improve-light": "rgba(76, 175, 80, 0.12)",
1501
- "result-improve-dark": "#248a24",
1502
- "result-degrade": "#ef5350",
1503
- // Red - negative outcome
1504
- "result-degrade-light": "rgba(239, 83, 80, 0.12)",
1505
- "result-degrade-dark": "#b30000",
1506
- "result-inconclusive": "#9E9A97",
1507
- // Gray - no clear result
1508
- "result-inconclusive-light": "rgba(158, 154, 151, 0.12)",
1509
- "result-neutral": primitiveColors.neutral[500],
1510
- // Neutral baseline
1511
- // Text on result backgrounds (on-result-*)
1512
- "on-result-improve": primitiveColors.white,
1513
- "on-result-degrade": primitiveColors.white,
1514
- "on-result-inconclusive": primitiveColors.white,
1515
- // Data visualization colors (data-*)
1516
- // First 10 colors from discrete rainbow optimized for charts
1517
- "data-1": discreteRainbow[9],
1518
- // Blue
1519
- "data-2": discreteRainbow[14],
1520
- // Green
1521
- "data-3": discreteRainbow[17],
1522
- // Yellow
1523
- "data-4": discreteRainbow[25],
1524
- // Red
1525
- "data-5": discreteRainbow[8],
1526
- // Purple
1527
- "data-6": discreteRainbow[20],
1528
- // Orange
1529
- "data-7": discreteRainbow[13],
1530
- // Light Blue
1531
- "data-8": discreteRainbow[15],
1532
- // Light Green
1533
- "data-9": discreteRainbow[3],
1534
- // Lavender
1535
- "data-10": discreteRainbow[22],
1536
- // Dark Orange
1537
- // Text colors (text-*)
1538
- "text-primary": "#121828",
1539
- "text-secondary": "#65748B",
1540
- "text-tertiary": primitiveColors.neutral[400],
1541
- "text-disabled": "rgba(55, 65, 81, 0.48)",
1542
- "text-inverse": primitiveColors.white,
1543
- "text-link": primitiveColors.blue[600],
1544
- "text-link-hover": primitiveColors.blue[700],
1545
- // Surface colors (surface-*) - for elevated containers
1546
- "surface-base": primitiveColors.white,
1547
- "surface-elevated": primitiveColors.neutral[50],
1548
- // slightly off-white for elevated cards
1549
- "surface-raised": primitiveColors.neutral[100],
1550
- // light gray for raised cards
1551
- "surface-overlay": primitiveColors.white,
1552
- "surface-input": primitiveColors.neutral[50],
1553
- // Input field background (filled variant)
1554
- // Background colors (background-*)
1555
- "background-base": "#EBEBEB",
1556
- "background-default": primitiveColors.white,
1557
- "background-subtle": primitiveColors.neutral[50],
1558
- // Border colors (border-*)
1559
- "border-default": "#E8E9EB",
1560
- "border-subtle": primitiveColors.neutral[100],
1561
- "border-strong": primitiveColors.neutral[300],
1562
- "border-prominent": primitiveColors.neutral[400],
1563
- // high-visibility divider
1564
- "border-focus": primitiveColors.blue[600],
1565
- "border-input": primitiveColors.neutral[300],
1566
- // Input field border
1567
- "border-input-hover": primitiveColors.neutral[400],
1568
- // Input field border on hover
1569
- "border-input-focus": primitiveColors.blue[600],
1570
- // Input field border on focus
1571
- "border-input-error": primitiveColors.red[600],
1572
- // Input field border on error
1573
- // Interactive states (interactive-*)
1574
- "interactive-hover": "rgba(55, 65, 81, 0.04)",
1575
- "interactive-focus": "rgba(55, 65, 81, 0.12)",
1576
- "interactive-active": "rgba(55, 65, 81, 0.16)",
1577
- "interactive-selected": "rgba(55, 65, 81, 0.08)",
1578
- "interactive-disabled": "rgba(55, 65, 81, 0.12)",
1579
- "interactive-disabled-text": "rgba(55, 65, 81, 0.26)",
1580
- // Divider
1581
- "divider": "#E8E9EB",
1582
- // Avatar default
1583
- "avatar-background": primitiveColors.neutral[500],
1584
- "avatar-text": primitiveColors.white
1585
- };
1586
- var semanticColorsDark = {
1587
- // Brand colors stay the same in dark mode
1588
- "brand-primary": primitiveRamps.orange[400],
1589
- "brand-primary-light": primitiveRamps.orange[300],
1590
- "brand-primary-dark": primitiveRamps.orange[500],
1591
- "brand-primary-subtle": "rgba(255, 121, 0, 0.12)",
1592
- "brand-primary-hover": primitiveRamps.orange[300],
1593
- "brand-primary-active": primitiveRamps.orange[200],
1594
- "brand-secondary": primitiveRamps.cyan[600],
1595
- "brand-secondary-light": primitiveRamps.cyan[500],
1596
- "brand-secondary-dark": primitiveRamps.cyan[700],
1597
- "brand-secondary-subtle": "rgba(48, 123, 155, 0.12)",
1598
- "brand-secondary-hover": primitiveRamps.cyan[500],
1599
- "brand-secondary-active": primitiveRamps.cyan[400],
1600
- // Text on brand backgrounds
1601
- "on-brand-primary": primitiveColors.white,
1602
- "on-brand-secondary": primitiveColors.white,
1603
- // Status colors
1604
- "status-success": primitiveRamps.green[300],
1605
- "status-success-light": primitiveRamps.green[200],
1606
- "status-success-dark": primitiveRamps.green[600],
1607
- "status-success-subtle": "rgba(46, 213, 115, 0.12)",
1608
- // Live-session accent — its OWN role, decoupled from success so the two can diverge
1609
- "status-live": primitiveRamps.green[300],
1610
- "status-live-muted": primitiveRamps.green[500],
1611
- "status-error": primitiveRamps.red[600],
1612
- "status-error-light": primitiveRamps.red[500],
1613
- "status-error-dark": primitiveRamps.red[700],
1614
- "status-error-subtle": "rgba(209, 67, 67, 0.12)",
1615
- "status-error-vivid": primitiveColors.redVivid[500],
1616
- "status-error-vivid-light": primitiveColors.redVivid[400],
1617
- "status-error-vivid-dark": primitiveColors.redVivid[700],
1618
- "status-error-vivid-subtle": "rgba(255, 71, 87, 0.12)",
1619
- "status-warning": primitiveRamps.amber[300],
1620
- "status-warning-light": primitiveRamps.amber[200],
1621
- "status-warning-dark": primitiveRamps.amber[500],
1622
- "status-warning-subtle": "rgba(249, 180, 21, 0.12)",
1623
- "status-info": primitiveRamps.blue[500],
1624
- "status-info-light": primitiveRamps.blue[300],
1625
- "status-info-dark": primitiveRamps.blue[600],
1626
- "status-info-subtle": "rgba(33, 150, 243, 0.12)",
1627
- // Text on status backgrounds
1628
- "on-status-success": primitiveColors.white,
1629
- "on-status-error": primitiveColors.white,
1630
- "on-status-warning": primitiveColors.white,
1631
- "on-status-info": primitiveColors.white,
1632
- // Result/outcome indicators (result-*)
1633
- "result-improve": "#4caf50",
1634
- "result-improve-light": "rgba(76, 175, 80, 0.16)",
1635
- "result-improve-dark": "#248a24",
1636
- "result-degrade": "#ef5350",
1637
- "result-degrade-light": "rgba(239, 83, 80, 0.16)",
1638
- "result-degrade-dark": "#b30000",
1639
- "result-inconclusive": "#9E9A97",
1640
- "result-inconclusive-light": "rgba(158, 154, 151, 0.16)",
1641
- "result-neutral": primitiveColors.neutral[400],
1642
- // Text on result backgrounds
1643
- "on-result-improve": primitiveColors.white,
1644
- "on-result-degrade": primitiveColors.white,
1645
- "on-result-inconclusive": primitiveColors.white,
1646
- // Data visualization colors (same in dark mode for consistency)
1647
- "data-1": discreteRainbow[9],
1648
- "data-2": discreteRainbow[14],
1649
- "data-3": discreteRainbow[17],
1650
- "data-4": discreteRainbow[25],
1651
- "data-5": discreteRainbow[8],
1652
- "data-6": discreteRainbow[20],
1653
- "data-7": discreteRainbow[13],
1654
- "data-8": discreteRainbow[15],
1655
- "data-9": discreteRainbow[3],
1656
- "data-10": discreteRainbow[22],
1657
- // Text colors - inverted for dark mode
1658
- "text-primary": primitiveColors.neutral[100],
1659
- "text-secondary": primitiveColors.neutral[400],
1660
- "text-tertiary": primitiveColors.neutral[500],
1661
- "text-disabled": "rgba(255, 255, 255, 0.38)",
1662
- "text-inverse": primitiveColors.neutral[900],
1663
- "text-link": "#828DF8",
1664
- "text-link-hover": primitiveColors.blue[400],
1665
- // Surface colors - dark backgrounds
1666
- "surface-base": primitiveColors.charcoal[700],
1667
- // main surface
1668
- "surface-elevated": primitiveColors.charcoal[600],
1669
- // elevated surface
1670
- "surface-raised": primitiveColors.charcoal[500],
1671
- // raised surface
1672
- "surface-overlay": primitiveColors.charcoal[600],
1673
- // overlay surface
1674
- "surface-input": primitiveColors.charcoal[600],
1675
- // input surface
1676
- // Background colors
1677
- "background-base": primitiveColors.charcoal[900],
1678
- // darkest charcoal
1679
- "background-default": primitiveColors.charcoal[700],
1680
- // main background
1681
- "background-subtle": primitiveColors.charcoal[500],
1682
- // subtle background
1683
- // Border colors
1684
- "border-default": primitiveColors.charcoal[400],
1685
- // default border
1686
- "border-subtle": primitiveColors.charcoal[500],
1687
- // subtle border
1688
- "border-strong": primitiveColors.charcoal[300],
1689
- // strong border
1690
- "border-prominent": primitiveColors.charcoal[200],
1691
- // high-visibility divider
1692
- "border-focus": "#828DF8",
1693
- "border-input": primitiveColors.neutral[600],
1694
- "border-input-hover": primitiveColors.neutral[500],
1695
- "border-input-focus": "#828DF8",
1696
- "border-input-error": primitiveColors.red[500],
1697
- // Interactive states
1698
- "interactive-hover": "rgba(255, 255, 255, 0.04)",
1699
- "interactive-focus": "rgba(255, 255, 255, 0.12)",
1700
- "interactive-active": "rgba(255, 255, 255, 0.16)",
1701
- "interactive-selected": "rgba(255, 255, 255, 0.08)",
1702
- "interactive-disabled": "rgba(255, 255, 255, 0.12)",
1703
- "interactive-disabled-text": "rgba(255, 255, 255, 0.26)",
1704
- // Divider
1705
- "divider": primitiveColors.charcoal[400],
1706
- // divider color
1707
- // Avatar default
1708
- "avatar-background": primitiveColors.neutral[600],
1709
- "avatar-text": primitiveColors.white
1710
- };
1711
- function getSemanticColors(mode) {
1712
- return mode === "dark" ? semanticColorsDark : semanticColorsLight;
2026
+ const badge = /* @__PURE__ */ jsxs(
2027
+ reactNative.View,
2028
+ {
2029
+ className,
2030
+ style: {
2031
+ flexDirection: "row",
2032
+ alignItems: "center",
2033
+ gap: 3,
2034
+ // borderRadius 2 is intentional per workout-tokens.ts (squared-off pill)
2035
+ borderRadius: 2,
2036
+ borderWidth: 1,
2037
+ borderColor: colors.borderColor,
2038
+ backgroundColor: colors.backgroundColor,
2039
+ paddingHorizontal: config.paddingH,
2040
+ paddingVertical: config.paddingV
2041
+ },
2042
+ ...props,
2043
+ children: [
2044
+ icon != null && /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "base-badge-icon", children: icon }),
2045
+ children
2046
+ ]
2047
+ }
2048
+ );
2049
+ if (onPress) {
2050
+ return /* @__PURE__ */ jsx(reactNative.Animated.View, { style: { transform: [{ scale: scaleAnim }] }, children: /* @__PURE__ */ jsx(
2051
+ reactNative.Pressable,
2052
+ {
2053
+ onPress,
2054
+ onPressIn: handlePressIn,
2055
+ onPressOut: handlePressOut,
2056
+ accessibilityRole: "button",
2057
+ accessibilityLabel: props.accessibilityLabel,
2058
+ testID: "base-badge-pressable",
2059
+ children: badge
2060
+ }
2061
+ ) });
2062
+ }
2063
+ return badge;
1713
2064
  }
1714
2065
 
1715
2066
  // src/components/custom/Workout/PrBadge.tsx
@@ -1762,563 +2113,255 @@ function PrBadge({
1762
2113
  ) : /* @__PURE__ */ jsx(
1763
2114
  BaseBadge,
1764
2115
  {
1765
- variant: "pr",
1766
- className,
1767
- accessibilityLabel: `Personal record: ${resolvedLabel}`,
1768
- ...props,
1769
- children: /* @__PURE__ */ jsxs(
1770
- reactNative.Text,
1771
- {
1772
- style: {
1773
- fontSize: 12,
1774
- color: BRAND_PRIMARY,
1775
- fontWeight: "700",
1776
- fontFamily: "Inter, sans-serif"
1777
- },
1778
- children: [
1779
- "\u2605",
1780
- " ",
1781
- resolvedLabel
1782
- ]
1783
- }
1784
- )
1785
- }
1786
- );
1787
- if (animate) {
1788
- return /* @__PURE__ */ jsx(
1789
- reactNative.Animated.View,
1790
- {
1791
- style: { transform: [{ scale }], opacity },
1792
- testID: "pr-badge-animated",
1793
- children: badge
1794
- }
1795
- );
1796
- }
1797
- return badge;
1798
- }
1799
- var DARK = getSemanticColors("dark");
1800
- var TEXT_ACTIVE = DARK["text-primary"];
1801
- var TEXT_MUTED = DARK["text-secondary"];
1802
- var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
1803
- var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
1804
- var typeBadge = {
1805
- fontSize: 10,
1806
- fontWeight: "700",
1807
- fontFamily: "Inter, sans-serif",
1808
- color: WORKOUT_TOKENS.scale.orange,
1809
- backgroundColor: "rgba(255,165,2,0.12)",
1810
- paddingVertical: 1,
1811
- paddingHorizontal: 5,
1812
- borderRadius: 3,
1813
- overflow: "hidden"
1814
- };
1815
- function displayReps(set) {
1816
- return set.state === "done" ? set.reps : set.target.reps;
1817
- }
1818
- function displayWeight(set) {
1819
- return set.state === "done" ? set.weight : set.target.weight;
1820
- }
1821
- function plannedReps(set) {
1822
- if (set.state === "done") return set.velocities.length;
1823
- if (set.state === "live") return set.target.reps;
1824
- return set.planned ?? set.target.reps;
1825
- }
1826
- function accessibilityLabel(set) {
1827
- const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
1828
- if (set.state === "live") return `${base}, in progress`;
1829
- if (set.state === "todo") return `${base}, upcoming`;
1830
- return base;
1831
- }
1832
- function Cell({
1833
- width,
1834
- children,
1835
- testID
1836
- }) {
1837
- return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
1838
- }
1839
- function RowStrip({ set }) {
1840
- const zones = set.velocityZones;
1841
- if (set.state === "live") {
1842
- return /* @__PURE__ */ jsx(
1843
- VelocityStrip,
1844
- {
1845
- variant: "expanded",
1846
- showNumbers: false,
1847
- showInfo: false,
1848
- height: 24,
1849
- scale: "fixed",
1850
- set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
1851
- zones
1852
- }
1853
- );
1854
- }
1855
- const velocities = set.state === "done" ? set.velocities : [];
1856
- return /* @__PURE__ */ jsx(
1857
- VelocityStrip,
1858
- {
1859
- variant: "mini",
1860
- set: { type: "straight", velocities, planned: plannedReps(set) },
1861
- zones
1862
- }
1863
- );
1864
- }
1865
- function SetRow(set) {
1866
- const live = set.state === "live";
1867
- const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
1868
- const rpe = set.state === "todo" ? null : set.rpe;
1869
- return /* @__PURE__ */ jsxs(
1870
- reactNative.View,
1871
- {
1872
- style: { paddingVertical: 6, paddingHorizontal: 8 },
1873
- accessibilityLabel: accessibilityLabel(set),
1874
- testID: "set-row",
1875
- children: [
1876
- /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
1877
- /* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
1878
- /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
1879
- /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
1880
- /* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
1881
- ] }),
1882
- /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
1883
- ]
1884
- }
1885
- );
1886
- }
1887
- var PREV_COLUMN = { label: "PREV", width: void 0 };
1888
- function buildColumns(unit, showPrevious) {
1889
- return [
1890
- { label: "SET", width: 36 },
1891
- ...showPrevious ? [PREV_COLUMN] : [],
1892
- { label: "REPS", width: 44 },
1893
- { label: unit.toUpperCase(), width: 56 },
1894
- { label: "RPE", width: 36 }
1895
- ];
1896
- }
1897
- function SetTableHeader({
1898
- unit = "lbs",
1899
- showPrevious = true,
1900
- testID = "table-header"
1901
- }) {
1902
- const columns = buildColumns(unit, showPrevious);
1903
- return /* @__PURE__ */ jsx(
1904
- reactNative.View,
1905
- {
1906
- className: "flex-row",
1907
- style: {
1908
- paddingVertical: 8,
1909
- paddingHorizontal: 8,
1910
- paddingBottom: 4,
1911
- ...showPrevious ? {} : { justifyContent: "space-between" }
1912
- },
1913
- testID,
1914
- children: columns.map(({ label, width }) => {
1915
- const isFlex = width === void 0;
1916
- return /* @__PURE__ */ jsx(
1917
- reactNative.View,
1918
- {
1919
- style: {
1920
- ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
1921
- alignItems: "center",
1922
- justifyContent: "center",
1923
- gap: 8
1924
- },
1925
- ...isFlex ? { className: "flex-1" } : {},
1926
- children: /* @__PURE__ */ jsx(
1927
- reactNative.Text,
1928
- {
1929
- className: "text-text-tertiary",
1930
- style: {
1931
- fontSize: 10,
1932
- fontWeight: "600",
1933
- fontFamily: "Inter, sans-serif",
1934
- textTransform: "uppercase",
1935
- letterSpacing: 0.8
1936
- },
1937
- children: label
1938
- }
1939
- )
1940
- },
1941
- label
1942
- );
1943
- })
1944
- }
1945
- );
1946
- }
1947
- var METRIC_FONT = "Inter, sans-serif";
1948
- function MetricCell({ children, color, fontSize = 11 }) {
1949
- return /* @__PURE__ */ jsx(
1950
- reactNative.Text,
1951
- {
1952
- style: {
1953
- fontFamily: METRIC_FONT,
1954
- fontSize,
1955
- color,
1956
- fontWeight: "600",
1957
- letterSpacing: 1
1958
- },
1959
- children
1960
- }
1961
- );
1962
- }
1963
-
1964
- // src/components/custom/Workout/SetsRepsLoad.tsx
1965
- var t = getSemanticColors("dark");
1966
- var TIMES = "\xD7";
1967
- var AT = "@";
1968
- function SetsRepsLoad({
1969
- sets,
1970
- reps,
1971
- load,
1972
- unit = "lb",
1973
- fontSize,
1974
- className,
1975
- ...props
1976
- }) {
1977
- const value = t["text-primary"];
1978
- const sep = t["text-tertiary"];
1979
- return /* @__PURE__ */ jsxs(
1980
- reactNative.View,
1981
- {
1982
- className,
1983
- style: { flexDirection: "row", alignItems: "baseline" },
1984
- accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
1985
- testID: "sets-reps-load",
2116
+ variant: "pr",
2117
+ className,
2118
+ accessibilityLabel: `Personal record: ${resolvedLabel}`,
1986
2119
  ...props,
1987
- children: [
1988
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: sets }),
1989
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${TIMES} ` }),
1990
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: reps }),
1991
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
1992
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
1993
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
1994
- ]
2120
+ children: /* @__PURE__ */ jsxs(
2121
+ reactNative.Text,
2122
+ {
2123
+ style: {
2124
+ fontSize: 12,
2125
+ color: BRAND_PRIMARY,
2126
+ fontWeight: "700",
2127
+ fontFamily: "Inter, sans-serif"
2128
+ },
2129
+ children: [
2130
+ "\u2605",
2131
+ " ",
2132
+ resolvedLabel
2133
+ ]
2134
+ }
2135
+ )
1995
2136
  }
1996
2137
  );
1997
- }
1998
- function hexToRgb(hex) {
1999
- const cleaned = hex.replace("#", "");
2000
- let r, g, b;
2001
- if (cleaned.length === 3) {
2002
- r = parseInt(cleaned[0] + cleaned[0], 16);
2003
- g = parseInt(cleaned[1] + cleaned[1], 16);
2004
- b = parseInt(cleaned[2] + cleaned[2], 16);
2005
- } else if (cleaned.length === 6) {
2006
- r = parseInt(cleaned.slice(0, 2), 16);
2007
- g = parseInt(cleaned.slice(2, 4), 16);
2008
- b = parseInt(cleaned.slice(4, 6), 16);
2009
- } else {
2010
- return null;
2138
+ if (animate) {
2139
+ return /* @__PURE__ */ jsx(
2140
+ reactNative.Animated.View,
2141
+ {
2142
+ style: { transform: [{ scale }], opacity },
2143
+ testID: "pr-badge-animated",
2144
+ children: badge
2145
+ }
2146
+ );
2011
2147
  }
2012
- return { r, g, b };
2148
+ return badge;
2013
2149
  }
2014
- function rgbToHsv(r, g, b) {
2015
- const rNorm = r / 255;
2016
- const gNorm = g / 255;
2017
- const bNorm = b / 255;
2018
- const max = Math.max(rNorm, gNorm, bNorm);
2019
- const min = Math.min(rNorm, gNorm, bNorm);
2020
- const delta = max - min;
2021
- const v = max;
2022
- const s = max === 0 ? 0 : delta / max;
2023
- let h = 0;
2024
- if (delta !== 0) {
2025
- if (max === rNorm) {
2026
- h = 60 * ((gNorm - bNorm) / delta % 6);
2027
- } else if (max === gNorm) {
2028
- h = 60 * ((bNorm - rNorm) / delta + 2);
2029
- } else {
2030
- h = 60 * ((rNorm - gNorm) / delta + 4);
2031
- }
2032
- }
2033
- if (h < 0) h += 360;
2034
- return { h, s, v };
2150
+ var DARK = getSemanticColors("dark");
2151
+ var TEXT_ACTIVE = DARK["text-primary"];
2152
+ var TEXT_MUTED = DARK["text-secondary"];
2153
+ var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
2154
+ var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
2155
+ var typeBadge = {
2156
+ fontSize: 10,
2157
+ fontWeight: "700",
2158
+ fontFamily: "Inter, sans-serif",
2159
+ color: WORKOUT_TOKENS.scale.orange,
2160
+ backgroundColor: "rgba(255,165,2,0.12)",
2161
+ paddingVertical: 1,
2162
+ paddingHorizontal: 5,
2163
+ borderRadius: 3,
2164
+ overflow: "hidden"
2165
+ };
2166
+ function displayReps(set) {
2167
+ return set.state === "done" ? set.reps : set.target.reps;
2035
2168
  }
2036
- function hsvToRgb(h, s, v) {
2037
- const c = v * s;
2038
- const x = c * (1 - Math.abs(h / 60 % 2 - 1));
2039
- const m = v - c;
2040
- let rPrime, gPrime, bPrime;
2041
- if (h >= 0 && h < 60) {
2042
- rPrime = c;
2043
- gPrime = x;
2044
- bPrime = 0;
2045
- } else if (h >= 60 && h < 120) {
2046
- rPrime = x;
2047
- gPrime = c;
2048
- bPrime = 0;
2049
- } else if (h >= 120 && h < 180) {
2050
- rPrime = 0;
2051
- gPrime = c;
2052
- bPrime = x;
2053
- } else if (h >= 180 && h < 240) {
2054
- rPrime = 0;
2055
- gPrime = x;
2056
- bPrime = c;
2057
- } else if (h >= 240 && h < 300) {
2058
- rPrime = x;
2059
- gPrime = 0;
2060
- bPrime = c;
2061
- } else {
2062
- rPrime = c;
2063
- gPrime = 0;
2064
- bPrime = x;
2065
- }
2066
- return {
2067
- r: Math.round((rPrime + m) * 255),
2068
- g: Math.round((gPrime + m) * 255),
2069
- b: Math.round((bPrime + m) * 255)
2070
- };
2169
+ function displayWeight(set) {
2170
+ return set.state === "done" ? set.weight : set.target.weight;
2071
2171
  }
2072
- function rgbToHex(r, g, b) {
2073
- const toHex = (n) => {
2074
- const clamped = Math.max(0, Math.min(255, Math.round(n)));
2075
- return clamped.toString(16).padStart(2, "0");
2076
- };
2077
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2172
+ function plannedReps(set) {
2173
+ if (set.state === "done") return set.velocities.length;
2174
+ if (set.state === "live") return set.target.reps;
2175
+ return set.planned ?? set.target.reps;
2078
2176
  }
2079
- function lighten(hex, amount = 0.1) {
2080
- const rgb = hexToRgb(hex);
2081
- if (!rgb) return hex;
2082
- const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
2083
- const newV = Math.min(1, hsv.v + amount);
2084
- const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
2085
- return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
2177
+ function accessibilityLabel(set) {
2178
+ const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
2179
+ if (set.state === "live") return `${base}, in progress`;
2180
+ if (set.state === "todo") return `${base}, upcoming`;
2181
+ return base;
2086
2182
  }
2087
- var shadowColors = {
2088
- // For charcoal surfaces (~#2C2C2C to #3C3C3C)
2089
- charcoal: {
2090
- dark: "rgba(0, 0, 0, 0.5)",
2091
- light: "rgba(255, 255, 255, 0.12)",
2092
- // brighter for crisp rim light
2093
- darker: "rgba(0, 0, 0, 0.6)"
2094
- },
2095
- // For neutral dark surfaces (~#1F2937)
2096
- neutralDark: {
2097
- dark: "rgba(0, 0, 0, 0.4)",
2098
- light: "rgba(255, 255, 255, 0.08)",
2099
- darker: "rgba(0, 0, 0, 0.5)"
2100
- },
2101
- // For light surfaces (white/light gray backgrounds)
2102
- neutralLight: {
2103
- dark: "rgba(0, 0, 0, 0.15)",
2104
- // Visible dark shadow for depth on light backgrounds
2105
- light: "rgba(255, 255, 255, 1)",
2106
- // Bright highlight for raised effect (visible on light gray)
2107
- darker: "rgba(0, 0, 0, 0.2)"
2108
- // Deeper shadow for pressed states
2183
+ function Cell({
2184
+ width,
2185
+ children,
2186
+ testID
2187
+ }) {
2188
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
2189
+ }
2190
+ function RowStrip({ set }) {
2191
+ const zones = set.velocityZones;
2192
+ if (set.state === "live") {
2193
+ return /* @__PURE__ */ jsx(
2194
+ VelocityStrip,
2195
+ {
2196
+ variant: "expanded",
2197
+ showNumbers: false,
2198
+ showInfo: false,
2199
+ height: 24,
2200
+ scale: "fixed",
2201
+ liveRepIndex: set.velocities.length - 1,
2202
+ set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
2203
+ zones
2204
+ }
2205
+ );
2109
2206
  }
2110
- };
2111
- var intensityConfig = {
2112
- subtle: { offset: 1, blur: 2 },
2113
- // very light, barely visible
2114
- medium: { offset: 2, blur: 4 },
2115
- // noticeable but restrained
2116
- strong: { offset: 2, blur: 5 }
2117
- // slightly deeper blur, same offset as medium
2118
- };
2119
- function createRaisedShadow(surface = "charcoal", intensity = "medium") {
2120
- const colors = shadowColors[surface];
2121
- const { offset, blur } = intensityConfig[intensity];
2122
- const rimBlur = 1;
2123
- return reactNative.Platform.select({
2124
- web: {
2125
- boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2126
- },
2127
- default: {
2128
- // React Native shadow approximation (limited compared to web)
2129
- shadowColor: "#000",
2130
- shadowOffset: { width: offset, height: offset },
2131
- shadowOpacity: 0.3,
2132
- shadowRadius: blur / 2,
2133
- elevation: offset
2207
+ const velocities = set.state === "done" ? set.velocities : [];
2208
+ return /* @__PURE__ */ jsx(
2209
+ VelocityStrip,
2210
+ {
2211
+ variant: "compact",
2212
+ height: 8,
2213
+ hideBaseline: true,
2214
+ set: { type: "straight", velocities, planned: plannedReps(set) },
2215
+ zones
2134
2216
  }
2135
- });
2217
+ );
2136
2218
  }
2137
- function createPressedShadow(surface = "charcoal", intensity = "medium") {
2138
- const colors = shadowColors[surface];
2139
- const { offset, blur } = intensityConfig[intensity];
2140
- const rimOffset = 1;
2141
- const rimBlur = 1;
2142
- return reactNative.Platform.select({
2143
- web: {
2144
- boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
2145
- },
2146
- default: {
2147
- // React Native doesn't support inset shadows, approximate with darker bg
2148
- shadowOpacity: 0,
2149
- elevation: 0
2219
+ function SetRow(set) {
2220
+ const live = set.state === "live";
2221
+ const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
2222
+ const rpe = set.state === "todo" ? null : set.rpe;
2223
+ return /* @__PURE__ */ jsxs(
2224
+ reactNative.View,
2225
+ {
2226
+ style: { paddingVertical: 6, paddingHorizontal: 8 },
2227
+ accessibilityLabel: accessibilityLabel(set),
2228
+ testID: "set-row",
2229
+ children: [
2230
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
2231
+ /* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
2232
+ /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
2233
+ /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
2234
+ /* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
2235
+ ] }),
2236
+ /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
2237
+ ]
2150
2238
  }
2151
- });
2239
+ );
2152
2240
  }
2153
- function createFlatShadow() {
2154
- return reactNative.Platform.select({
2155
- web: {
2156
- boxShadow: "none"
2157
- },
2158
- default: {
2159
- shadowOpacity: 0,
2160
- elevation: 0
2161
- }
2162
- });
2241
+ var PREV_COLUMN = { label: "PREV", width: void 0 };
2242
+ function buildColumns(unit, showPrevious) {
2243
+ return [
2244
+ { label: "SET", width: 36 },
2245
+ ...showPrevious ? [PREV_COLUMN] : [],
2246
+ { label: "REPS", width: 44 },
2247
+ { label: unit.toUpperCase(), width: 56 },
2248
+ { label: "RPE", width: 36 }
2249
+ ];
2163
2250
  }
2164
- function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
2165
- const colors = shadowColors[surface];
2166
- const { offset, blur } = intensityConfig[intensity];
2167
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2168
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2169
- const rimBlur = 1;
2170
- return reactNative.Platform.select({
2171
- web: {
2172
- // Keep same offsets as normal state for rim to prevent "shrinking" effect
2173
- boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2174
- },
2175
- default: {
2176
- shadowColor: "#000",
2177
- shadowOffset: { width: hoverOffset, height: hoverOffset },
2178
- shadowOpacity: 0.2,
2179
- shadowRadius: hoverBlur / 2,
2180
- elevation: hoverOffset
2251
+ function SetTableHeader({
2252
+ unit = "lbs",
2253
+ showPrevious = true,
2254
+ testID = "table-header"
2255
+ }) {
2256
+ const columns = buildColumns(unit, showPrevious);
2257
+ return /* @__PURE__ */ jsx(
2258
+ reactNative.View,
2259
+ {
2260
+ className: "flex-row",
2261
+ style: {
2262
+ paddingVertical: 8,
2263
+ paddingHorizontal: 8,
2264
+ paddingBottom: 4,
2265
+ ...showPrevious ? {} : { justifyContent: "space-between" }
2266
+ },
2267
+ testID,
2268
+ children: columns.map(({ label, width }) => {
2269
+ const isFlex = width === void 0;
2270
+ return /* @__PURE__ */ jsx(
2271
+ reactNative.View,
2272
+ {
2273
+ style: {
2274
+ ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
2275
+ alignItems: "center",
2276
+ justifyContent: "center",
2277
+ gap: 8
2278
+ },
2279
+ ...isFlex ? { className: "flex-1" } : {},
2280
+ children: /* @__PURE__ */ jsx(
2281
+ reactNative.Text,
2282
+ {
2283
+ className: "text-text-tertiary",
2284
+ style: {
2285
+ fontSize: 10,
2286
+ fontWeight: "600",
2287
+ fontFamily: "Inter, sans-serif",
2288
+ textTransform: "uppercase",
2289
+ letterSpacing: 0.8
2290
+ },
2291
+ children: label
2292
+ }
2293
+ )
2294
+ },
2295
+ label
2296
+ );
2297
+ })
2181
2298
  }
2182
- });
2299
+ );
2183
2300
  }
2184
- function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
2185
- const colors = shadowColors[surface];
2186
- const { offset, blur } = intensityConfig[intensity];
2187
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2188
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2189
- const rimBlur = 1;
2190
- return reactNative.Platform.select({
2191
- web: {
2192
- // Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
2193
- boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
2194
- },
2195
- default: {
2196
- shadowOpacity: 0,
2197
- elevation: 0
2301
+ var METRIC_FONT = "Inter, sans-serif";
2302
+ function MetricCell({ children, color, fontSize = 11 }) {
2303
+ return /* @__PURE__ */ jsx(
2304
+ reactNative.Text,
2305
+ {
2306
+ style: {
2307
+ fontFamily: METRIC_FONT,
2308
+ fontSize,
2309
+ color,
2310
+ fontWeight: "600",
2311
+ letterSpacing: 1
2312
+ },
2313
+ children
2198
2314
  }
2199
- });
2315
+ );
2200
2316
  }
2201
- ({
2202
- // Charcoal surface shadows (for dark toolbars, cards)
2203
- charcoal: {
2204
- raised: {
2205
- subtle: createRaisedShadow("charcoal", "subtle"),
2206
- medium: createRaisedShadow("charcoal", "medium"),
2207
- strong: createRaisedShadow("charcoal", "strong")
2208
- },
2209
- raisedHover: {
2210
- subtle: createRaisedHoverShadow("charcoal", "subtle"),
2211
- medium: createRaisedHoverShadow("charcoal", "medium"),
2212
- strong: createRaisedHoverShadow("charcoal", "strong")
2213
- },
2214
- pressed: {
2215
- subtle: createPressedShadow("charcoal", "subtle"),
2216
- medium: createPressedShadow("charcoal", "medium"),
2217
- strong: createPressedShadow("charcoal", "strong")
2218
- },
2219
- pressedHover: {
2220
- subtle: createPressedHoverShadow("charcoal", "subtle"),
2221
- medium: createPressedHoverShadow("charcoal", "medium"),
2222
- strong: createPressedHoverShadow("charcoal", "strong")
2223
- },
2224
- flat: createFlatShadow()
2225
- },
2226
- // Neutral dark surface shadows
2227
- neutralDark: {
2228
- raised: {
2229
- subtle: createRaisedShadow("neutralDark", "subtle"),
2230
- medium: createRaisedShadow("neutralDark", "medium"),
2231
- strong: createRaisedShadow("neutralDark", "strong")
2232
- },
2233
- raisedHover: {
2234
- subtle: createRaisedHoverShadow("neutralDark", "subtle"),
2235
- medium: createRaisedHoverShadow("neutralDark", "medium"),
2236
- strong: createRaisedHoverShadow("neutralDark", "strong")
2237
- },
2238
- pressed: {
2239
- subtle: createPressedShadow("neutralDark", "subtle"),
2240
- medium: createPressedShadow("neutralDark", "medium"),
2241
- strong: createPressedShadow("neutralDark", "strong")
2242
- },
2243
- pressedHover: {
2244
- subtle: createPressedHoverShadow("neutralDark", "subtle"),
2245
- medium: createPressedHoverShadow("neutralDark", "medium"),
2246
- strong: createPressedHoverShadow("neutralDark", "strong")
2247
- },
2248
- flat: createFlatShadow()
2249
- },
2250
- // Neutral light surface shadows
2251
- neutralLight: {
2252
- raised: {
2253
- subtle: createRaisedShadow("neutralLight", "subtle"),
2254
- medium: createRaisedShadow("neutralLight", "medium"),
2255
- strong: createRaisedShadow("neutralLight", "strong")
2256
- },
2257
- raisedHover: {
2258
- subtle: createRaisedHoverShadow("neutralLight", "subtle"),
2259
- medium: createRaisedHoverShadow("neutralLight", "medium"),
2260
- strong: createRaisedHoverShadow("neutralLight", "strong")
2261
- },
2262
- pressed: {
2263
- subtle: createPressedShadow("neutralLight", "subtle"),
2264
- medium: createPressedShadow("neutralLight", "medium"),
2265
- strong: createPressedShadow("neutralLight", "strong")
2266
- },
2267
- pressedHover: {
2268
- subtle: createPressedHoverShadow("neutralLight", "subtle"),
2269
- medium: createPressedHoverShadow("neutralLight", "medium"),
2270
- strong: createPressedHoverShadow("neutralLight", "strong")
2271
- },
2272
- flat: createFlatShadow()
2273
- }
2274
- });
2275
2317
 
2276
- // src/utils/colors.ts
2277
- function alpha(color, opacity) {
2278
- const clampedOpacity = Math.max(0, Math.min(1, opacity));
2279
- if (color.startsWith("#")) {
2280
- const hex = color.slice(1);
2281
- let r, g, b;
2282
- if (hex.length === 3) {
2283
- r = parseInt(hex[0] + hex[0], 16);
2284
- g = parseInt(hex[1] + hex[1], 16);
2285
- b = parseInt(hex[2] + hex[2], 16);
2286
- } else if (hex.length === 6) {
2287
- r = parseInt(hex.slice(0, 2), 16);
2288
- g = parseInt(hex.slice(2, 4), 16);
2289
- b = parseInt(hex.slice(4, 6), 16);
2290
- } else {
2291
- return color;
2318
+ // src/components/custom/Workout/SetsRepsLoad.tsx
2319
+ var t2 = getSemanticColors("dark");
2320
+ var TIMES = "\xD7";
2321
+ var AT = "@";
2322
+ function SetsRepsLoad({
2323
+ sets,
2324
+ reps,
2325
+ load,
2326
+ unit = "lb",
2327
+ fontSize,
2328
+ className,
2329
+ ...props
2330
+ }) {
2331
+ const value = t2["text-primary"];
2332
+ const sep = t2["text-tertiary"];
2333
+ return /* @__PURE__ */ jsxs(
2334
+ reactNative.View,
2335
+ {
2336
+ className,
2337
+ style: { flexDirection: "row", alignItems: "baseline" },
2338
+ accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
2339
+ testID: "sets-reps-load",
2340
+ ...props,
2341
+ children: [
2342
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: sets }),
2343
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${TIMES} ` }),
2344
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: reps }),
2345
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
2346
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
2347
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
2348
+ ]
2292
2349
  }
2293
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2294
- }
2295
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
2296
- if (rgbMatch) {
2297
- const [, r, g, b] = rgbMatch;
2298
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2299
- }
2300
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
2301
- if (rgbaMatch) {
2302
- const [, r, g, b] = rgbaMatch;
2303
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2304
- }
2305
- return color;
2350
+ );
2306
2351
  }
2307
-
2308
- // src/components/custom/Workout/TempoDisplay.tsx
2309
- var t2 = getSemanticColors("dark");
2352
+ var t3 = getSemanticColors("dark");
2310
2353
  var INTER = "Inter, sans-serif";
2311
2354
  var TEXT_TERTIARY = "#6B7280";
2312
- var STATUS_ERROR = t2["status-error"];
2313
- var STATUS_SUCCESS = t2["status-success"];
2314
- var STATUS_WARNING = t2["status-warning"];
2355
+ var STATUS_ERROR = t3["status-error"];
2356
+ var STATUS_SUCCESS = t3["status-success"];
2357
+ var STATUS_WARNING = t3["status-warning"];
2315
2358
  var ON_TARGET_MS = 100;
2316
2359
  function getTempoFillPct(elapsedMs, targetMs) {
2317
2360
  if (!targetMs) return 100;
2318
2361
  return Math.min(100, elapsedMs / targetMs * 100);
2319
2362
  }
2320
2363
  function activeNumberTone(elapsedMs, targetMs) {
2321
- if (targetMs == null) return t2["text-primary"];
2364
+ if (targetMs == null) return t3["text-primary"];
2322
2365
  const remainingMs = targetMs - elapsedMs;
2323
2366
  if (remainingMs > ON_TARGET_MS) return STATUS_WARNING;
2324
2367
  if (remainingMs >= -ON_TARGET_MS) return STATUS_SUCCESS;
@@ -2470,7 +2513,7 @@ function TempoDisplay({
2470
2513
  style: {
2471
2514
  flexDirection: "row",
2472
2515
  alignItems: "center",
2473
- backgroundColor: "#1C1C1C",
2516
+ backgroundColor: t3["surface-raised"],
2474
2517
  paddingHorizontal: chromePadX,
2475
2518
  paddingVertical: chromePadY,
2476
2519
  borderRadius: chromeRadius
@@ -2485,7 +2528,7 @@ function TempoDisplay({
2485
2528
  fontSize: labelFont,
2486
2529
  fontWeight: "500",
2487
2530
  // Live-muted green while a rep is running, tertiary at rest.
2488
- color: live ? t2["status-live-muted"] : TEXT_TERTIARY,
2531
+ color: live ? t3["status-live-muted"] : TEXT_TERTIARY,
2489
2532
  letterSpacing: 0.5,
2490
2533
  textTransform: "uppercase",
2491
2534
  marginRight: Math.round(fontSize * 0.5)
@@ -2543,7 +2586,7 @@ function TempoDisplay({
2543
2586
  reactNative.View,
2544
2587
  {
2545
2588
  style: {
2546
- backgroundColor: "#191919",
2589
+ backgroundColor: t3["surface-overlay"],
2547
2590
  borderRadius: 6,
2548
2591
  paddingVertical: 8,
2549
2592
  paddingHorizontal: 12,
@@ -2596,14 +2639,14 @@ function TempoDisplay({
2596
2639
  }
2597
2640
  );
2598
2641
  }
2599
- var t3 = getSemanticColors("dark");
2642
+ var t4 = getSemanticColors("dark");
2600
2643
  var INDICATOR_CONFIG = {
2601
- imbalance: { Icon: ScaleIcon, color: t3["status-error"], label: "Left/right imbalance" },
2602
- overshoot: { Icon: AlertTriangleIcon, color: t3["status-error"], label: "Load overshoot" },
2603
- "velocity-loss": { Icon: TrendingDownIcon, color: t3["status-warning"], label: "Velocity loss" },
2604
- "missed-reps": { Icon: CircleSlashIcon, color: t3["status-warning"], label: "Missed reps" },
2605
- pr: { Icon: AwardIcon, color: t3["status-success"], label: "Personal record" },
2606
- info: { Icon: InfoIcon, color: t3["status-info"], label: "More info" }
2644
+ imbalance: { Icon: ScaleIcon, color: t4["status-error"], label: "Left/right imbalance" },
2645
+ overshoot: { Icon: AlertTriangleIcon, color: t4["status-error"], label: "Load overshoot" },
2646
+ "velocity-loss": { Icon: TrendingDownIcon, color: t4["status-warning"], label: "Velocity loss" },
2647
+ "missed-reps": { Icon: CircleSlashIcon, color: t4["status-warning"], label: "Missed reps" },
2648
+ pr: { Icon: AwardIcon, color: t4["status-success"], label: "Personal record" },
2649
+ info: { Icon: InfoIcon, color: t4["status-info"], label: "More info" }
2607
2650
  };
2608
2651
  function ExerciseIndicator({ kind, onPress, className, ...props }) {
2609
2652
  const { Icon, color, label } = INDICATOR_CONFIG[kind];
@@ -2689,6 +2732,243 @@ function ExerciseHeading({
2689
2732
  )
2690
2733
  ] });
2691
2734
  }
2735
+ var PULSE_HALF_MS = 950;
2736
+ var PULSE_MIN_OPACITY = 0.45;
2737
+ var SEGMENTED_BAR_GAP = 5;
2738
+ function usePulse(active) {
2739
+ const [value] = react.useState(() => new reactNative.Animated.Value(0));
2740
+ react.useEffect(() => {
2741
+ if (!active) return;
2742
+ const loop = reactNative.Animated.loop(
2743
+ reactNative.Animated.sequence([
2744
+ reactNative.Animated.timing(value, {
2745
+ toValue: 1,
2746
+ duration: PULSE_HALF_MS,
2747
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2748
+ useNativeDriver: false
2749
+ }),
2750
+ reactNative.Animated.timing(value, {
2751
+ toValue: 0,
2752
+ duration: PULSE_HALF_MS,
2753
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2754
+ useNativeDriver: false
2755
+ })
2756
+ ])
2757
+ );
2758
+ loop.start();
2759
+ return () => loop.stop();
2760
+ }, [active, value]);
2761
+ return value;
2762
+ }
2763
+ function SegmentedBar({
2764
+ segments,
2765
+ height = 8,
2766
+ gap = SEGMENTED_BAR_GAP,
2767
+ radius = 2,
2768
+ marker = null,
2769
+ segmentTestID,
2770
+ style,
2771
+ ...props
2772
+ }) {
2773
+ const pulse = usePulse(segments.some((s) => s.pulse));
2774
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
2775
+ segments.map((seg, i) => {
2776
+ const fillStyle = {
2777
+ width: `${(seg.fill ?? 1) * 100}%`,
2778
+ height: "100%",
2779
+ backgroundColor: seg.color,
2780
+ opacity: seg.opacity
2781
+ };
2782
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
2783
+ return /* @__PURE__ */ jsx(
2784
+ reactNative.View,
2785
+ {
2786
+ style: {
2787
+ flex: seg.weight ?? 1,
2788
+ minWidth: 0,
2789
+ height: "100%",
2790
+ marginLeft: seg.leadingGap,
2791
+ borderRadius: radius,
2792
+ overflow: "hidden"
2793
+ },
2794
+ children: seg.pulse ? /* @__PURE__ */ jsx(
2795
+ reactNative.Animated.View,
2796
+ {
2797
+ style: {
2798
+ ...fillStyle,
2799
+ ...seg.pulseColor ? {
2800
+ backgroundColor: pulse.interpolate({
2801
+ inputRange: [0, 1],
2802
+ outputRange: seg.pulseColor
2803
+ })
2804
+ } : {
2805
+ opacity: pulse.interpolate({
2806
+ inputRange: [0, 1],
2807
+ outputRange: [PULSE_MIN_OPACITY, 1]
2808
+ })
2809
+ }
2810
+ },
2811
+ accessibilityElementsHidden: true,
2812
+ testID
2813
+ }
2814
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
2815
+ },
2816
+ i
2817
+ );
2818
+ }),
2819
+ marker ? /* @__PURE__ */ jsx(
2820
+ reactNative.View,
2821
+ {
2822
+ style: {
2823
+ position: "absolute",
2824
+ left: `${marker.position * 100}%`,
2825
+ top: 0,
2826
+ bottom: 0,
2827
+ width: 2,
2828
+ backgroundColor: marker.color
2829
+ },
2830
+ accessibilityElementsHidden: true,
2831
+ testID: "segmented-bar-marker"
2832
+ }
2833
+ ) : null
2834
+ ] });
2835
+ }
2836
+
2837
+ // src/components/custom/Workout/SetBar.tsx
2838
+ var SET_STRIP_ZONES = {
2839
+ slow: primitiveRamps.red[600],
2840
+ moderate: primitiveRamps.orange[400],
2841
+ fast: primitiveRamps.amber[300],
2842
+ fastest: primitiveRamps.green[300]
2843
+ };
2844
+ var TODO_COLOR = primitiveColors.charcoal[300];
2845
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
2846
+ var NOTCH_GAP = 2;
2847
+ var CLUSTER_GAP = 3;
2848
+ var MYO_UPCOMING_CLUSTERS = 3;
2849
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
2850
+ var MYO_UPCOMING_FADE = 0.6;
2851
+ var PULSE_RANGE = {
2852
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
2853
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
2854
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
2855
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
2856
+ };
2857
+ function velocityZoneColor(v) {
2858
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
2859
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
2860
+ if (v < 1) return SET_STRIP_ZONES.fast;
2861
+ return SET_STRIP_ZONES.fastest;
2862
+ }
2863
+ function chunkedSegments(chunks, gap) {
2864
+ return chunks.flatMap(
2865
+ (vels, ci) => vels.map((v, ri) => ({
2866
+ color: velocityZoneColor(v),
2867
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
2868
+ }))
2869
+ );
2870
+ }
2871
+ function rangeSegments(floor, max, doneVels) {
2872
+ return Array.from({ length: max }, (_, i) => {
2873
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
2874
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
2875
+ });
2876
+ }
2877
+ function myoUpcomingSegments(activationLen, clusterCount) {
2878
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
2879
+ const trail = Array.from(
2880
+ { length: clusterCount },
2881
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
2882
+ color: SET_STRIP_VARIABLE_COLOR,
2883
+ opacity: MYO_UPCOMING_FADE ** ci,
2884
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
2885
+ }))
2886
+ ).flat();
2887
+ return [...activation, ...trail];
2888
+ }
2889
+ function setSegments(set) {
2890
+ if (set.status === "todo") {
2891
+ return [{ color: TODO_COLOR }];
2892
+ }
2893
+ if (set.status === "done") {
2894
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
2895
+ }
2896
+ if (set.status === "range") {
2897
+ return rangeSegments(set.floor, set.max, set.doneVels);
2898
+ }
2899
+ if (set.status === "drop") {
2900
+ return chunkedSegments(set.subloads, NOTCH_GAP);
2901
+ }
2902
+ if (set.status === "myo") {
2903
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
2904
+ }
2905
+ if (set.status === "myo-upcoming") {
2906
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
2907
+ }
2908
+ const performed = set.velocities.map((v) => {
2909
+ const color = velocityZoneColor(v);
2910
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
2911
+ });
2912
+ const remaining = Math.max(0, set.planned - set.velocities.length);
2913
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
2914
+ }
2915
+ function setBarSegmentTestID(seg) {
2916
+ if (seg.pulse) return "set-strip-pulse";
2917
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
2918
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
2919
+ }
2920
+ function SetBar({ set, height = 8 }) {
2921
+ return /* @__PURE__ */ jsx(
2922
+ SegmentedBar,
2923
+ {
2924
+ segments: setSegments(set),
2925
+ height,
2926
+ gap: 0,
2927
+ radius: 0,
2928
+ segmentTestID: setBarSegmentTestID,
2929
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
2930
+ testID: "set-strip-set"
2931
+ }
2932
+ );
2933
+ }
2934
+
2935
+ // src/components/custom/Workout/SetStrip.tsx
2936
+ var SET_STRIP_GAP = 5;
2937
+ function setCategory(set) {
2938
+ switch (set.status) {
2939
+ case "done":
2940
+ case "drop":
2941
+ case "myo":
2942
+ return "done";
2943
+ case "active":
2944
+ return "active";
2945
+ case "range":
2946
+ return set.doneVels.length > 0 ? "active" : "upcoming";
2947
+ case "todo":
2948
+ case "myo-upcoming":
2949
+ return "upcoming";
2950
+ }
2951
+ }
2952
+ function describeSets(sets) {
2953
+ const done = sets.filter((s) => setCategory(s) === "done").length;
2954
+ const active = sets.filter((s) => setCategory(s) === "active").length;
2955
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
2956
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
2957
+ }
2958
+ function SetStrip({ sets, height = 8, className, ...props }) {
2959
+ return /* @__PURE__ */ jsx(
2960
+ reactNative.View,
2961
+ {
2962
+ className,
2963
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
2964
+ accessibilityRole: "image",
2965
+ accessibilityLabel: describeSets(sets),
2966
+ testID: "set-strip",
2967
+ ...props,
2968
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
2969
+ }
2970
+ );
2971
+ }
2692
2972
 
2693
2973
  // src/components/custom/Workout/ExerciseCardHeading.tsx
2694
2974
  function ExerciseCardHeading({
@@ -2831,7 +3111,9 @@ function CollapsedCard({
2831
3111
  {
2832
3112
  velocities,
2833
3113
  zones: velocityZones,
2834
- variant: "mini",
3114
+ variant: "compact",
3115
+ height: 8,
3116
+ hideBaseline: true,
2835
3117
  testID: `exercise-card-velocity-strip-${i}`
2836
3118
  },
2837
3119
  i
@@ -3619,7 +3901,7 @@ function RestTimer({
3619
3901
  var DEFAULTS = {
3620
3902
  label: "SS",
3621
3903
  color: getSemanticColors("dark")["brand-primary"],
3622
- bgBase: "#101010"
3904
+ bgBase: getSemanticColors("dark")["background-base"]
3623
3905
  };
3624
3906
  function SupersetWrapper({
3625
3907
  label = DEFAULTS.label,
@@ -4401,11 +4683,11 @@ function CardContent({ children, className }) {
4401
4683
  function CardFooter({ children, className }) {
4402
4684
  return /* @__PURE__ */ jsx(reactNative.View, { className: cn("px-6 py-4 flex-row items-center gap-2", className), children });
4403
4685
  }
4404
- var t4 = getSemanticColors("dark");
4686
+ var t5 = getSemanticColors("dark");
4405
4687
  var solidVariantColors = {
4406
- success: t4["status-success"],
4407
- warning: t4["status-warning"],
4408
- error: t4["status-error"],
4688
+ success: t5["status-success"],
4689
+ warning: t5["status-warning"],
4690
+ error: t5["status-error"],
4409
4691
  neutral: "#6B7280"
4410
4692
  };
4411
4693
  var ringVariantStyles = {
@@ -4445,8 +4727,8 @@ var iconColors = {
4445
4727
  warning: "#6B4000",
4446
4728
  error: "#5C1A1A",
4447
4729
  neutral: "#D1D5DB",
4448
- "on-track": t4["status-success"],
4449
- deviation: t4["status-warning"],
4730
+ "on-track": t5["status-success"],
4731
+ deviation: t5["status-warning"],
4450
4732
  future: "#6B7280"
4451
4733
  };
4452
4734
  var sizeDimensions = {
@@ -4532,13 +4814,13 @@ var MESO_ACCENT_GRADIENT_LIGHT = primitiveRamps.orange[300];
4532
4814
  var WORKOUT_PILL_DELOAD = primitiveRamps.magenta[600];
4533
4815
 
4534
4816
  // src/components/custom/Workout/MesoStatusCard.tsx
4535
- var t5 = getSemanticColors("dark");
4536
- var BRAND_PRIMARY5 = t5["brand-primary"];
4817
+ var t6 = getSemanticColors("dark");
4818
+ var BRAND_PRIMARY5 = t6["brand-primary"];
4537
4819
  var BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK;
4538
4820
  var BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT;
4539
- var SUCCESS = t5["status-success"];
4540
- var WARNING = t5["status-warning"];
4541
- var ERROR = t5["status-error"];
4821
+ var SUCCESS = t6["status-success"];
4822
+ var WARNING = t6["status-warning"];
4823
+ var ERROR = t6["status-error"];
4542
4824
  var ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY5, BRAND_PRIMARY_LIGHT];
4543
4825
  var CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor("surface-elevated")} 0%, ${resolveColor("surface-raised")} 100%)`;
4544
4826
  var GAUGE_GRADIENT = "linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(249,180,21,0.25) 50%, rgba(209,67,67,0.25) 100%)";
@@ -4909,8 +5191,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
4909
5191
  var PLOT_LEFT = 26;
4910
5192
  var TOOLTIP_WIDTH = 124;
4911
5193
  function timeOf(dateStr) {
4912
- const t11 = Date.parse(dateStr);
4913
- return Number.isNaN(t11) ? 0 : t11;
5194
+ const t12 = Date.parse(dateStr);
5195
+ return Number.isNaN(t12) ? 0 : t12;
4914
5196
  }
4915
5197
  function formatValue(value) {
4916
5198
  return `${Math.round(value)}`;
@@ -5399,10 +5681,10 @@ function StrengthTrendChart({
5399
5681
  }
5400
5682
  );
5401
5683
  }
5402
- var t6 = getSemanticColors("dark");
5403
- var STATUS_SUCCESS3 = t6["status-success"];
5404
- var STATUS_WARNING3 = t6["status-warning"];
5405
- var STATUS_INFO = t6["status-info"];
5684
+ var t7 = getSemanticColors("dark");
5685
+ var STATUS_SUCCESS3 = t7["status-success"];
5686
+ var STATUS_WARNING3 = t7["status-warning"];
5687
+ var STATUS_INFO = t7["status-info"];
5406
5688
  var DOT_BORDER = "#F3F4F6";
5407
5689
  var BAND_FILL = "rgba(46,213,115,0.1)";
5408
5690
  var BAND_EDGE = "rgba(46,213,115,0.45)";
@@ -5445,8 +5727,8 @@ function interpEdge(pixels, x, key) {
5445
5727
  const a = pixels[i];
5446
5728
  const b = pixels[i + 1];
5447
5729
  if (x >= a.x && x <= b.x) {
5448
- const t11 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
5449
- return a[key] + t11 * (b[key] - a[key]);
5730
+ const t12 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
5731
+ return a[key] + t12 * (b[key] - a[key]);
5450
5732
  }
5451
5733
  }
5452
5734
  return pixels[pixels.length - 1][key];
@@ -6000,7 +6282,16 @@ function VbtBreakdown({
6000
6282
  children: set.label
6001
6283
  }
6002
6284
  ),
6003
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(VelocityStrip, { velocities: set.velocities, zones, variant: "mini" }) }),
6285
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(
6286
+ VelocityStrip,
6287
+ {
6288
+ velocities: set.velocities,
6289
+ zones,
6290
+ variant: "compact",
6291
+ height: 8,
6292
+ hideBaseline: true
6293
+ }
6294
+ ) }),
6004
6295
  /* @__PURE__ */ jsx(
6005
6296
  reactNative.Text,
6006
6297
  {
@@ -6485,12 +6776,12 @@ function Badge({
6485
6776
  }
6486
6777
  );
6487
6778
  }
6488
- var t7 = getSemanticColors("dark");
6779
+ var t8 = getSemanticColors("dark");
6489
6780
  var statusBgColors = {
6490
6781
  completed: "rgba(46,213,115,0.15)",
6491
6782
  current: "rgba(255,121,0,0.15)",
6492
6783
  next: "transparent",
6493
- upcoming: "#1C1C1C",
6784
+ upcoming: t8["surface-raised"],
6494
6785
  missed: "rgba(209,67,67,0.1)",
6495
6786
  deload: "rgba(186,41,150,0.15)"
6496
6787
  };
@@ -6503,9 +6794,9 @@ var statusBorderStyles = {
6503
6794
  deload: { borderWidth: 1, borderColor: "rgba(186,41,150,0.3)" }
6504
6795
  };
6505
6796
  var textColors = {
6506
- completed: t7["status-success"],
6507
- current: t7["brand-primary"],
6508
- next: t7["brand-primary"],
6797
+ completed: t8["status-success"],
6798
+ current: t8["brand-primary"],
6799
+ next: t8["brand-primary"],
6509
6800
  upcoming: "#6B7280",
6510
6801
  missed: "rgba(209,67,67,0.7)",
6511
6802
  deload: WORKOUT_PILL_DELOAD
@@ -6638,19 +6929,19 @@ function WorkoutPill({
6638
6929
  }
6639
6930
  return pill;
6640
6931
  }
6641
- var t8 = getSemanticColors("dark");
6932
+ var t9 = getSemanticColors("dark");
6642
6933
  var TRACK_BG = "#333333";
6643
6934
  var LABEL_COLOR = "#6B7280";
6644
6935
  var TARGET_LINE_COLOR = "rgba(33, 150, 243, 0.5)";
6645
6936
  var AT_TARGET_GLOW = "0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)";
6646
6937
  var ZONE = {
6647
- building: t8["status-success"],
6938
+ building: t9["status-success"],
6648
6939
  // below target ramp-up
6649
- approaching: t8["status-warning"],
6940
+ approaching: t9["status-warning"],
6650
6941
  // nearing target
6651
- target: t8["brand-primary"],
6942
+ target: t9["brand-primary"],
6652
6943
  // exactly at target
6653
- over1: t8["status-error"],
6944
+ over1: t9["status-error"],
6654
6945
  // mild over-target
6655
6946
  over2: WORKOUT_TOKENS.intensity.over2,
6656
6947
  // moderate over-target
@@ -7073,13 +7364,13 @@ function MesoCard({
7073
7364
  }
7074
7365
  ) });
7075
7366
  }
7076
- var t9 = getSemanticColors("dark");
7367
+ var t10 = getSemanticColors("dark");
7077
7368
  var dotColorMap = {
7078
7369
  untrained: "#2C2C2C",
7079
- behind: t9["brand-secondary"],
7080
- ontrack: t9["status-success"],
7081
- target: t9["brand-primary"],
7082
- over: t9["status-error"]
7370
+ behind: t10["brand-secondary"],
7371
+ ontrack: t10["status-success"],
7372
+ target: t10["brand-primary"],
7373
+ over: t10["status-error"]
7083
7374
  };
7084
7375
  function MuscleGroupChip({
7085
7376
  name,
@@ -7153,10 +7444,10 @@ function MuscleGroupChip({
7153
7444
  }
7154
7445
 
7155
7446
  // src/components/custom/Workout/WorkoutCard.tsx
7156
- var t10 = getSemanticColors("dark");
7447
+ var t11 = getSemanticColors("dark");
7157
7448
  var COLORS = {
7158
- statusSuccess: t10["status-success"],
7159
- brandPrimary: t10["brand-primary"],
7449
+ statusSuccess: t11["status-success"],
7450
+ brandPrimary: t11["brand-primary"],
7160
7451
  borderDefault: "#1F1F1F",
7161
7452
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
7162
7453
  };