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