@titan-design/react-ui 0.10.0 → 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 (84) hide show
  1. package/dist/index.d.mts +399 -4
  2. package/dist/index.d.ts +399 -4
  3. package/dist/index.js +2295 -895
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2279 -905
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  8. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +1974 -1740
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +1975 -1741
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +8 -3
  16. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  17. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  18. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  19. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  20. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  21. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  22. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  23. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  24. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  25. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  26. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  27. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  28. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  29. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  30. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  31. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  33. package/src/components/custom/Fatigue/README.md +93 -0
  34. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  35. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  36. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  37. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  38. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  39. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  40. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  41. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  42. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  43. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  44. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  45. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  46. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  47. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  48. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  49. package/src/components/custom/Fatigue/index.ts +41 -0
  50. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  51. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  52. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  56. package/src/components/custom/Workout/README.md +62 -16
  57. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  58. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  59. package/src/components/custom/Workout/SessionRail.tsx +50 -13
  60. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  61. package/src/components/custom/Workout/SetRow.tsx +9 -2
  62. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  63. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  64. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  65. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  66. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  67. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  68. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  69. package/src/components/custom/Workout/index.ts +3 -0
  70. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  71. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  72. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  73. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  74. package/src/components/custom/index.ts +1 -0
  75. package/src/test/raw-color-patterns.test.ts +71 -0
  76. package/src/theme/ColorPalettes.stories.tsx +871 -0
  77. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  78. package/src/theme/bar-paper.ts +59 -0
  79. package/src/theme/color-stories.coverage.test.ts +130 -0
  80. package/src/theme/color-story-kit.tsx +170 -0
  81. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  82. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  83. package/src/theme/ColorSystem.stories.tsx +0 -508
  84. package/src/theme/Colors.stories.tsx +0 -323
package/dist/pages.js CHANGED
@@ -36,7 +36,6 @@ var primitiveColors = {
36
36
  // Charcoal scale (dark backgrounds)
37
37
  charcoal: {
38
38
  0: "#6E6E6E",
39
- 100: "#4C4C4C",
40
39
  200: "#3C3C3C",
41
40
  300: "#2C2C2C",
42
41
  400: "#1F1F1F",
@@ -182,1204 +181,6 @@ var WORKOUT_TOKENS = {
182
181
  fill: "#3A3A3A"
183
182
  }};
184
183
 
185
- // src/utils/workout-format.ts
186
- function roundRpe(rpe) {
187
- return Math.round(rpe * 2) / 2;
188
- }
189
- function roundWeight(weight) {
190
- return Math.round(weight);
191
- }
192
- function formatVelocity(velocity) {
193
- return velocity.toFixed(2);
194
- }
195
- function roundTempo(tempo) {
196
- return tempo.map((v) => Math.round(v * 10) / 10);
197
- }
198
- function wrapJsx(fn) {
199
- return function(type, props, ...rest) {
200
- if (props && typeof props.className === "string" && props.className) {
201
- const cn2 = props.className;
202
- const cssStyle = { $$css: true, [cn2]: cn2 };
203
- const existing = props.style;
204
- props = {
205
- ...props,
206
- style: existing ? [cssStyle, existing] : cssStyle
207
- };
208
- delete props.className;
209
- }
210
- return fn(type, props, ...rest);
211
- };
212
- }
213
- var jsx = wrapJsx(jsxRuntime.jsx);
214
- var jsxs = wrapJsx(jsxRuntime.jsxs);
215
-
216
- // src/components/custom/Workout/SegmentedBar.tsx
217
- var PULSE_HALF_MS = 950;
218
- var PULSE_MIN_OPACITY = 0.45;
219
- var SEGMENTED_BAR_GAP = 5;
220
- function usePulse(active) {
221
- const [value] = react.useState(() => new reactNative.Animated.Value(0));
222
- react.useEffect(() => {
223
- if (!active) return;
224
- const loop = reactNative.Animated.loop(
225
- reactNative.Animated.sequence([
226
- reactNative.Animated.timing(value, {
227
- toValue: 1,
228
- duration: PULSE_HALF_MS,
229
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
230
- useNativeDriver: false
231
- }),
232
- reactNative.Animated.timing(value, {
233
- toValue: 0,
234
- duration: PULSE_HALF_MS,
235
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
236
- useNativeDriver: false
237
- })
238
- ])
239
- );
240
- loop.start();
241
- return () => loop.stop();
242
- }, [active, value]);
243
- return value;
244
- }
245
- function SegmentedBar({
246
- segments,
247
- height = 8,
248
- gap = SEGMENTED_BAR_GAP,
249
- radius = 2,
250
- marker = null,
251
- segmentTestID,
252
- style,
253
- ...props
254
- }) {
255
- const pulse = usePulse(segments.some((s) => s.pulse));
256
- return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
257
- segments.map((seg, i) => {
258
- const fillStyle = {
259
- width: `${(seg.fill ?? 1) * 100}%`,
260
- height: "100%",
261
- backgroundColor: seg.color,
262
- opacity: seg.opacity
263
- };
264
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
265
- return /* @__PURE__ */ jsx(
266
- reactNative.View,
267
- {
268
- style: {
269
- flex: seg.weight ?? 1,
270
- minWidth: 0,
271
- height: "100%",
272
- marginLeft: seg.leadingGap,
273
- borderRadius: radius,
274
- overflow: "hidden"
275
- },
276
- children: seg.pulse ? /* @__PURE__ */ jsx(
277
- reactNative.Animated.View,
278
- {
279
- style: {
280
- ...fillStyle,
281
- ...seg.pulseColor ? {
282
- backgroundColor: pulse.interpolate({
283
- inputRange: [0, 1],
284
- outputRange: seg.pulseColor
285
- })
286
- } : {
287
- opacity: pulse.interpolate({
288
- inputRange: [0, 1],
289
- outputRange: [PULSE_MIN_OPACITY, 1]
290
- })
291
- }
292
- },
293
- accessibilityElementsHidden: true,
294
- testID
295
- }
296
- ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
297
- },
298
- i
299
- );
300
- }),
301
- marker ? /* @__PURE__ */ jsx(
302
- reactNative.View,
303
- {
304
- style: {
305
- position: "absolute",
306
- left: `${marker.position * 100}%`,
307
- top: 0,
308
- bottom: 0,
309
- width: 2,
310
- backgroundColor: marker.color
311
- },
312
- accessibilityElementsHidden: true,
313
- testID: "segmented-bar-marker"
314
- }
315
- ) : null
316
- ] });
317
- }
318
-
319
- // src/components/custom/Workout/SetBar.tsx
320
- var SET_STRIP_ZONES = {
321
- slow: primitiveRamps.red[600],
322
- moderate: primitiveRamps.orange[400],
323
- fast: primitiveRamps.amber[300],
324
- fastest: primitiveRamps.green[300]
325
- };
326
- var TODO_COLOR = primitiveColors.charcoal[300];
327
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
328
- var NOTCH_GAP = 2;
329
- var CLUSTER_GAP = 3;
330
- var MYO_UPCOMING_CLUSTERS = 3;
331
- var MYO_UPCOMING_CLUSTER_LEN = 5;
332
- var MYO_UPCOMING_FADE = 0.6;
333
- var PULSE_RANGE = {
334
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
335
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
336
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
337
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
338
- };
339
- function velocityZoneColor(v) {
340
- if (v < 0.5) return SET_STRIP_ZONES.slow;
341
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
342
- if (v < 1) return SET_STRIP_ZONES.fast;
343
- return SET_STRIP_ZONES.fastest;
344
- }
345
- function chunkedSegments(chunks, gap) {
346
- return chunks.flatMap(
347
- (vels, ci) => vels.map((v, ri) => ({
348
- color: velocityZoneColor(v),
349
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
350
- }))
351
- );
352
- }
353
- function rangeSegments(floor, max, doneVels) {
354
- return Array.from({ length: max }, (_, i) => {
355
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
356
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
357
- });
358
- }
359
- function myoUpcomingSegments(activationLen, clusterCount) {
360
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
361
- const trail = Array.from(
362
- { length: clusterCount },
363
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
364
- color: SET_STRIP_VARIABLE_COLOR,
365
- opacity: MYO_UPCOMING_FADE ** ci,
366
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
367
- }))
368
- ).flat();
369
- return [...activation, ...trail];
370
- }
371
- function setSegments(set) {
372
- if (set.status === "todo") {
373
- return [{ color: TODO_COLOR }];
374
- }
375
- if (set.status === "done") {
376
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
377
- }
378
- if (set.status === "range") {
379
- return rangeSegments(set.floor, set.max, set.doneVels);
380
- }
381
- if (set.status === "drop") {
382
- return chunkedSegments(set.subloads, NOTCH_GAP);
383
- }
384
- if (set.status === "myo") {
385
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
386
- }
387
- if (set.status === "myo-upcoming") {
388
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
389
- }
390
- const performed = set.velocities.map((v) => {
391
- const color = velocityZoneColor(v);
392
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
393
- });
394
- const remaining = Math.max(0, set.planned - set.velocities.length);
395
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
396
- }
397
- function setBarSegmentTestID(seg) {
398
- if (seg.pulse) return "set-strip-pulse";
399
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
400
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
401
- }
402
- function SetBar({ set, height = 8 }) {
403
- return /* @__PURE__ */ jsx(
404
- SegmentedBar,
405
- {
406
- segments: setSegments(set),
407
- height,
408
- gap: 0,
409
- radius: 0,
410
- segmentTestID: setBarSegmentTestID,
411
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
412
- testID: "set-strip-set"
413
- }
414
- );
415
- }
416
-
417
- // src/components/custom/Workout/SetStrip.tsx
418
- var SET_STRIP_GAP = 5;
419
- function setCategory(set) {
420
- switch (set.status) {
421
- case "done":
422
- case "drop":
423
- case "myo":
424
- return "done";
425
- case "active":
426
- return "active";
427
- case "range":
428
- return set.doneVels.length > 0 ? "active" : "upcoming";
429
- case "todo":
430
- case "myo-upcoming":
431
- return "upcoming";
432
- }
433
- }
434
- function describeSets(sets) {
435
- const done = sets.filter((s) => setCategory(s) === "done").length;
436
- const active = sets.filter((s) => setCategory(s) === "active").length;
437
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
438
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
439
- }
440
- function SetStrip({ sets, height = 8, className, ...props }) {
441
- return /* @__PURE__ */ jsx(
442
- reactNative.View,
443
- {
444
- className,
445
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
446
- accessibilityRole: "image",
447
- accessibilityLabel: describeSets(sets),
448
- testID: "set-strip",
449
- ...props,
450
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
451
- }
452
- );
453
- }
454
-
455
- // src/components/custom/Workout/VelocityStrip.tsx
456
- var VEL_COLORS = WORKOUT_TOKENS.scale;
457
- var bandIdToEffortColor = {
458
- speed: sequentialEffort[0],
459
- // green-300
460
- power: sequentialEffort[2],
461
- // amber-300 (gold)
462
- strengthSpeed: sequentialEffort[3],
463
- // orange-400
464
- maximalStrength: sequentialEffort[4],
465
- // red-600
466
- grinding: sequentialEffort[5]
467
- // red-700 — the distinct 5th band
468
- };
469
- function getVelocityZoneColor(velocity) {
470
- if (velocity >= 1) return "vel-green";
471
- if (velocity >= 0.75) return "vel-yellow";
472
- if (velocity >= 0.5) return "vel-orange";
473
- return "vel-red";
474
- }
475
- function getVelocityZoneName(velocity) {
476
- if (velocity >= 1) return "Speed";
477
- if (velocity >= 0.75) return "Power";
478
- if (velocity >= 0.5) return "Strength-Speed";
479
- return "Strength";
480
- }
481
- var zoneHexMap = {
482
- "vel-green": VEL_COLORS.green,
483
- "vel-yellow": VEL_COLORS.yellow,
484
- "vel-orange": VEL_COLORS.orange,
485
- "vel-red": VEL_COLORS.red
486
- };
487
- function calculateVelocityLoss(velocities) {
488
- if (velocities.length < 2) return 0;
489
- const best = Math.max(...velocities);
490
- if (best <= 0) return 0;
491
- const last = velocities[velocities.length - 1];
492
- return Math.max(0, Math.round((best - last) / best * 100));
493
- }
494
- function calculateMeanVelocity(velocities) {
495
- if (velocities.length === 0) return 0;
496
- const sum = velocities.reduce((acc, v) => acc + v, 0);
497
- return sum / velocities.length;
498
- }
499
- function classifyBand(velocity, bands) {
500
- for (const band of bands) {
501
- if (band.max === null || velocity < band.max) return band;
502
- }
503
- return bands[bands.length - 1];
504
- }
505
- function bandColor(band) {
506
- return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
507
- }
508
- function getLossStyle(loss) {
509
- if (loss > 25) return { color: VEL_COLORS.red };
510
- if (loss > 20) return { color: VEL_COLORS.orange };
511
- return null;
512
- }
513
- var REP_GAP = 2;
514
- var WIDE_GAP = 8;
515
- var TODO_COLOR2 = primitiveColors.charcoal[300];
516
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
517
- function deriveDoneVelocities(set) {
518
- switch (set.type) {
519
- case "drop":
520
- return set.subloads.flat();
521
- case "myo":
522
- return [...set.activation, ...set.clusters.flat()];
523
- default:
524
- return set.velocities;
525
- }
526
- }
527
- function chunkedRepSlots(chunks) {
528
- const slots = [];
529
- chunks.forEach((chunk) => {
530
- chunk.forEach((velocity, ri) => {
531
- const first = slots.length === 0;
532
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
533
- });
534
- });
535
- return slots;
536
- }
537
- function buildSlots(set) {
538
- switch (set.type) {
539
- case "straight": {
540
- const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
541
- return Array.from({ length: total }, (_, i) => {
542
- const done = i < set.velocities.length;
543
- return {
544
- kind: done ? "rep" : "todo",
545
- velocity: done ? set.velocities[i] : void 0,
546
- leadingGap: i === 0 ? 0 : REP_GAP
547
- };
548
- });
549
- }
550
- case "range": {
551
- const total = Math.max(set.max, set.velocities.length);
552
- return Array.from({ length: total }, (_, i) => {
553
- const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
554
- return {
555
- kind,
556
- velocity: kind === "rep" ? set.velocities[i] : void 0,
557
- leadingGap: i === 0 ? 0 : REP_GAP
558
- };
559
- });
560
- }
561
- case "amrap": {
562
- const reps = set.velocities.map((velocity, i) => ({
563
- kind: "rep",
564
- velocity,
565
- leadingGap: i === 0 ? 0 : REP_GAP
566
- }));
567
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
568
- return reps;
569
- }
570
- case "drop":
571
- return chunkedRepSlots(set.subloads);
572
- case "myo": {
573
- const slots = chunkedRepSlots([set.activation, ...set.clusters]);
574
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
575
- return slots;
576
- }
577
- case "cluster": {
578
- const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
579
- return Array.from({ length: total }, (_, i) => {
580
- const done = i < set.velocities.length;
581
- const boundary = i > 0 && i % set.groupSize === 0;
582
- return {
583
- kind: done ? "rep" : "todo",
584
- velocity: done ? set.velocities[i] : void 0,
585
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
586
- };
587
- });
588
- }
589
- }
590
- }
591
- function setAccessibilityLabel(set, repCount) {
592
- switch (set.type) {
593
- case "straight":
594
- return `Velocity strip, straight set, ${repCount} reps`;
595
- case "range":
596
- return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
597
- case "amrap":
598
- return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
599
- case "drop":
600
- return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
601
- case "myo":
602
- return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
603
- case "cluster":
604
- return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
605
- }
606
- }
607
- function slotAccessibilityLabel(slot, index) {
608
- switch (slot.kind) {
609
- case "rep":
610
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
611
- case "todo":
612
- return `Rep ${index + 1}: planned`;
613
- case "variable":
614
- return `Rep ${index + 1}: variable`;
615
- case "continue":
616
- return "Keep going";
617
- }
618
- }
619
- var EXPANDED_TODO_STUB_PCT = 16;
620
- var EXPANDED_ENCODED_PCT = 45;
621
- var EXPANDED_HEIGHT = 60;
622
- var FIXED_MAX_VELOCITY = 1.15;
623
- var BARE_STUB_HEIGHT = 3;
624
- var HERO_HEIGHT = 220;
625
- var HERO_LABEL_HEADROOM = 20;
626
- var HERO_BAR_GAP = 8;
627
- var HERO_BAR_MAX_WIDTH = 120;
628
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
629
- var HERO_BAR_RADIUS = 5;
630
- var HERO_MIN_BAR_HEIGHT = 4;
631
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
632
- var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
633
- var PEAK_HEADROOM = 1.06;
634
- function scaleDenominator(scale, maxVelocity) {
635
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
636
- }
637
- function bareSlotHeight(slot, height, denom) {
638
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
639
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
640
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
641
- }
642
- function getReducedMotionPreference() {
643
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
644
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
645
- }
646
- function usePrefersReducedMotion() {
647
- const [reduced, setReduced] = react.useState(getReducedMotionPreference);
648
- react.useEffect(() => {
649
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
650
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
651
- const handler = () => setReduced(mq.matches);
652
- handler();
653
- mq.addEventListener?.("change", handler);
654
- return () => mq.removeEventListener?.("change", handler);
655
- }, []);
656
- return reduced;
657
- }
658
- var ANIMATION_DURATION = 400;
659
- var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
660
- var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
661
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
662
- const prefersReducedMotion = usePrefersReducedMotion();
663
- const [liveScale] = react.useState(() => new reactNative.Animated.Value(1));
664
- react.useEffect(() => {
665
- if (!active || liveRepIndex == null || liveVelocity == null) return;
666
- if (prefersReducedMotion) {
667
- liveScale.setValue(1);
668
- return;
669
- }
670
- if (isNewPeak) {
671
- liveScale.setValue(1);
672
- reactNative.Animated.sequence([
673
- reactNative.Animated.timing(liveScale, {
674
- toValue: 1.25,
675
- duration: 150,
676
- easing: reactNative.Easing.out(reactNative.Easing.quad),
677
- useNativeDriver: true
678
- }),
679
- reactNative.Animated.spring(liveScale, {
680
- toValue: 1,
681
- friction: 3,
682
- tension: 140,
683
- useNativeDriver: true
684
- })
685
- ]).start();
686
- } else {
687
- liveScale.setValue(0.8);
688
- reactNative.Animated.timing(liveScale, {
689
- toValue: 1,
690
- duration: 300,
691
- easing: POP_EASING,
692
- useNativeDriver: true
693
- }).start();
694
- }
695
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
696
- return liveScale;
697
- }
698
- function HeroVelocityChart({
699
- doneVelocities,
700
- barColorFor,
701
- scaleDenom,
702
- referenceVelocity,
703
- liveRepIndex,
704
- isNewPeak,
705
- targetReps,
706
- height,
707
- className,
708
- viewProps
709
- }) {
710
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
711
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
712
- const [plotW, setPlotW] = react.useState(0);
713
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
714
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
715
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
716
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
717
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
718
- const totalSlots = doneVelocities.length + pendingCount;
719
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
720
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
721
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
722
- const peakIndex = doneVelocities.reduce(
723
- (best, v, i) => v > doneVelocities[best] ? i : best,
724
- 0
725
- );
726
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
727
- const repCount = doneVelocities.length;
728
- const total = Math.max(repCount, targetReps ?? repCount);
729
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
730
- const { style: externalStyle, ...restProps } = viewProps;
731
- return /* @__PURE__ */ jsx(
732
- reactNative.View,
733
- {
734
- className,
735
- style: [{ height }, externalStyle],
736
- accessibilityRole: "image",
737
- accessibilityLabel: label,
738
- testID: "velocity-strip-hero",
739
- ...restProps,
740
- children: /* @__PURE__ */ jsxs(
741
- reactNative.View,
742
- {
743
- onLayout: onPlotLayout,
744
- style: {
745
- flex: 1,
746
- flexDirection: "row",
747
- alignItems: "flex-end",
748
- gap: heroGap,
749
- position: "relative",
750
- borderBottomWidth: 2,
751
- borderBottomColor: HERO_PENDING_COLOR
752
- },
753
- children: [
754
- referencePx > 0 && /* @__PURE__ */ jsx(
755
- reactNative.View,
756
- {
757
- accessibilityElementsHidden: true,
758
- style: {
759
- position: "absolute",
760
- left: 0,
761
- right: 0,
762
- bottom: referencePx,
763
- borderTopWidth: 1,
764
- borderStyle: "dashed",
765
- borderColor: HERO_REFERENCE_COLOR,
766
- pointerEvents: "none"
767
- },
768
- testID: "velocity-hero-reference"
769
- }
770
- ),
771
- doneVelocities.map((velocity, i) => {
772
- const isLive = liveRepIndex === i;
773
- return /* @__PURE__ */ jsxs(
774
- reactNative.View,
775
- {
776
- accessibilityElementsHidden: true,
777
- style: {
778
- flex: 1,
779
- maxWidth: HERO_BAR_MAX_WIDTH,
780
- height: "100%",
781
- alignItems: "center",
782
- justifyContent: "flex-end"
783
- },
784
- children: [
785
- showBarLabel(i) && /* @__PURE__ */ jsx(
786
- reactNative.Text,
787
- {
788
- className: "text-text-primary",
789
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
790
- testID: `velocity-label-${i}`,
791
- children: formatVelocity(velocity)
792
- }
793
- ),
794
- /* @__PURE__ */ jsx(
795
- reactNative.Animated.View,
796
- {
797
- style: [
798
- {
799
- width: "100%",
800
- height: barHeight(velocity),
801
- borderTopLeftRadius: HERO_BAR_RADIUS,
802
- borderTopRightRadius: HERO_BAR_RADIUS,
803
- backgroundColor: barColorFor(velocity)
804
- },
805
- isLive ? { transform: [{ scale: liveScale }] } : null
806
- ],
807
- testID: `velocity-bar-${i}`
808
- }
809
- )
810
- ]
811
- },
812
- i
813
- );
814
- }),
815
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
816
- reactNative.View,
817
- {
818
- accessibilityElementsHidden: true,
819
- style: {
820
- flex: 1,
821
- maxWidth: HERO_BAR_MAX_WIDTH,
822
- height: "100%",
823
- alignItems: "center",
824
- justifyContent: "flex-end"
825
- },
826
- children: /* @__PURE__ */ jsx(
827
- reactNative.View,
828
- {
829
- style: {
830
- width: "100%",
831
- height: HERO_MIN_BAR_HEIGHT * 3,
832
- borderWidth: 1,
833
- borderStyle: "dashed",
834
- borderColor: HERO_PENDING_COLOR,
835
- borderTopLeftRadius: HERO_BAR_RADIUS,
836
- borderTopRightRadius: HERO_BAR_RADIUS
837
- },
838
- testID: "velocity-slot-todo"
839
- }
840
- )
841
- },
842
- `pending-${i}`
843
- ))
844
- ]
845
- }
846
- )
847
- }
848
- );
849
- }
850
- function VelocityStrip({
851
- velocities,
852
- set,
853
- zones,
854
- liveRepIndex,
855
- expanded = true,
856
- onToggle,
857
- onRepPress,
858
- variant = "expanded",
859
- height = EXPANDED_HEIGHT,
860
- targetReps,
861
- scale = "peak",
862
- showNumbers = true,
863
- showInfo = true,
864
- className,
865
- ...props
866
- }) {
867
- const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
868
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
869
- kind: "rep",
870
- velocity: v,
871
- leadingGap: i === 0 ? 0 : REP_GAP
872
- }));
873
- const maxVelocity = Math.max(...doneVelocities, 0);
874
- const meanVelocity = calculateMeanVelocity(doneVelocities);
875
- const loss = calculateVelocityLoss(doneVelocities);
876
- const framed = showNumbers || showInfo;
877
- const scaleDenom = scaleDenominator(scale, maxVelocity);
878
- const hasZones = zones != null && zones.length > 0;
879
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
880
- const slotColor = (slot) => {
881
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
882
- if (slot.kind === "todo") return TODO_COLOR2;
883
- return SET_STRIP_VARIABLE_COLOR;
884
- };
885
- const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
886
- const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
887
- const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
888
- const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
889
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
890
- const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
891
- const liveScale = useLiveRepPop(
892
- variant === "expanded" && framed,
893
- liveRepIndex,
894
- liveVelocity,
895
- isNewPeak
896
- );
897
- react.useEffect(() => {
898
- if (variant !== "expanded" || !framed) return;
899
- reactNative.Animated.parallel([
900
- reactNative.Animated.timing(heightAnim, {
901
- toValue: expanded ? height : 3,
902
- duration: ANIMATION_DURATION,
903
- easing: ANIMATION_EASING,
904
- useNativeDriver: false
905
- }),
906
- reactNative.Animated.timing(labelOpacity, {
907
- toValue: expanded ? 1 : 0,
908
- duration: 300,
909
- delay: expanded ? 150 : 0,
910
- useNativeDriver: false
911
- }),
912
- reactNative.Animated.timing(infoOpacity, {
913
- toValue: expanded ? 1 : 0,
914
- duration: 300,
915
- delay: expanded ? 200 : 0,
916
- useNativeDriver: false
917
- })
918
- ]).start();
919
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
920
- if (set == null && velocities == null) return null;
921
- const repCount = doneVelocities.length;
922
- const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
923
- if (variant === "hero") {
924
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
925
- return /* @__PURE__ */ jsx(
926
- HeroVelocityChart,
927
- {
928
- doneVelocities,
929
- barColorFor,
930
- scaleDenom,
931
- referenceVelocity: maxVelocity,
932
- liveRepIndex,
933
- isNewPeak,
934
- targetReps,
935
- height: heroHeight,
936
- className,
937
- viewProps: props
938
- }
939
- );
940
- }
941
- if (variant === "mini") {
942
- const { style: externalStyle, ...restProps } = props;
943
- return /* @__PURE__ */ jsx(
944
- reactNative.View,
945
- {
946
- className,
947
- style: [
948
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
949
- externalStyle
950
- ],
951
- accessibilityRole: "image",
952
- accessibilityLabel: miniLabel,
953
- testID: "velocity-strip-mini",
954
- ...restProps,
955
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
956
- reactNative.View,
957
- {
958
- style: {
959
- flex: 1,
960
- backgroundColor: slotColor(slot),
961
- borderRadius: 1,
962
- minWidth: 4,
963
- height: "100%",
964
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
965
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
966
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
967
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
968
- },
969
- accessibilityElementsHidden: true,
970
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
971
- },
972
- i
973
- ))
974
- }
975
- );
976
- }
977
- if (!framed) {
978
- const { style: externalStyle, ...restProps } = props;
979
- return /* @__PURE__ */ jsx(
980
- reactNative.View,
981
- {
982
- className,
983
- style: [
984
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
985
- externalStyle
986
- ],
987
- accessibilityRole: "image",
988
- accessibilityLabel: miniLabel,
989
- testID: "velocity-strip-compact",
990
- ...restProps,
991
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
992
- reactNative.View,
993
- {
994
- style: {
995
- flex: 1,
996
- minWidth: 4,
997
- height: bareSlotHeight(slot, height, scaleDenom),
998
- // Top-rounded to match the framed chart's bars (the "rounded tops from
999
- // the numbers one"); square bottoms since bars sit on the baseline.
1000
- borderTopLeftRadius: 2,
1001
- borderTopRightRadius: 2,
1002
- backgroundColor: slotColor(slot),
1003
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
1004
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1005
- },
1006
- accessibilityElementsHidden: true,
1007
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
1008
- },
1009
- i
1010
- ))
1011
- }
1012
- );
1013
- }
1014
- const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
1015
- const hasInteractiveContainer = onToggle != null;
1016
- const hasInteractiveReps = onRepPress != null && expanded;
1017
- const setBars = set != null && slots.map((slot, i) => {
1018
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
1019
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
1020
- return /* @__PURE__ */ jsx(
1021
- reactNative.View,
1022
- {
1023
- style: {
1024
- flex: 1,
1025
- height: "100%",
1026
- justifyContent: "flex-end",
1027
- marginLeft: slot.leadingGap
1028
- },
1029
- children: /* @__PURE__ */ jsx(
1030
- reactNative.View,
1031
- {
1032
- style: {
1033
- height: `${heightPct}%`,
1034
- minHeight: 2,
1035
- borderTopLeftRadius: 2,
1036
- borderTopRightRadius: 2,
1037
- backgroundColor: slotColor(slot),
1038
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
1039
- },
1040
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
1041
- accessibilityRole: "image",
1042
- accessibilityLabel: slotAccessibilityLabel(slot, i)
1043
- }
1044
- )
1045
- },
1046
- i
1047
- );
1048
- });
1049
- const stripContent = /* @__PURE__ */ jsxs(
1050
- reactNative.Animated.View,
1051
- {
1052
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
1053
- style: [
1054
- {
1055
- height: heightAnim,
1056
- width: "100%",
1057
- gap: 2,
1058
- borderRadius: expanded ? 6 : 2,
1059
- paddingTop: expanded ? 16 : 0,
1060
- paddingBottom: expanded ? 24 : 0,
1061
- // No horizontal inset — the framed chart's bars span full width, matching
1062
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
1063
- paddingHorizontal: 0,
1064
- paddingVertical: expanded ? void 0 : 8,
1065
- overflow: expanded ? "visible" : "hidden"
1066
- }
1067
- ],
1068
- accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
1069
- accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
1070
- testID: "velocity-strip",
1071
- ...props,
1072
- children: [
1073
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
1074
- const barBackground = barColorFor(v);
1075
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
1076
- const isLive = liveRepIndex === i;
1077
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
1078
- const barStyle = expanded ? {
1079
- height: `${barHeightPct}%`,
1080
- minHeight: 2,
1081
- borderTopLeftRadius: 2,
1082
- borderTopRightRadius: 2,
1083
- backgroundColor: barBackground
1084
- } : {
1085
- minHeight: "100%",
1086
- borderTopLeftRadius: 2,
1087
- borderTopRightRadius: 2,
1088
- borderBottomLeftRadius: 0,
1089
- borderBottomRightRadius: 0,
1090
- backgroundColor: barBackground
1091
- };
1092
- const barInner = isLive ? /* @__PURE__ */ jsx(
1093
- reactNative.Animated.View,
1094
- {
1095
- style: [barStyle, { transform: [{ scale: liveScale }] }],
1096
- testID: `velocity-bar-${i}`,
1097
- accessibilityRole: "image",
1098
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
1099
- }
1100
- ) : /* @__PURE__ */ jsx(
1101
- reactNative.View,
1102
- {
1103
- style: barStyle,
1104
- testID: `velocity-bar-${i}`,
1105
- accessibilityRole: "image",
1106
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
1107
- }
1108
- );
1109
- const bar = /* @__PURE__ */ jsxs(
1110
- reactNative.View,
1111
- {
1112
- style: {
1113
- flex: 1,
1114
- height: "100%",
1115
- justifyContent: "flex-end",
1116
- position: "relative"
1117
- },
1118
- children: [
1119
- expanded && showNumbers && /* @__PURE__ */ jsx(
1120
- reactNative.Animated.View,
1121
- {
1122
- style: {
1123
- opacity: labelOpacity,
1124
- alignItems: "center",
1125
- position: "absolute",
1126
- top: -13,
1127
- left: 0,
1128
- right: 0
1129
- },
1130
- accessibilityElementsHidden: true,
1131
- children: /* @__PURE__ */ jsx(
1132
- reactNative.Text,
1133
- {
1134
- className: "text-text-secondary",
1135
- style: { fontSize: 8, fontWeight: "600" },
1136
- testID: `velocity-label-${i}`,
1137
- children: formatVelocity(v)
1138
- }
1139
- )
1140
- }
1141
- ),
1142
- barInner
1143
- ]
1144
- },
1145
- i
1146
- );
1147
- if (onRepPress && expanded) {
1148
- return /* @__PURE__ */ jsx(
1149
- reactNative.Pressable,
1150
- {
1151
- style: { flex: 1, height: "100%" },
1152
- onPress: () => onRepPress(i, v),
1153
- accessibilityRole: "button",
1154
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
1155
- testID: `velocity-bar-pressable-${i}`,
1156
- children: bar
1157
- },
1158
- i
1159
- );
1160
- }
1161
- return bar;
1162
- }) }),
1163
- expanded && showInfo && /* @__PURE__ */ jsxs(
1164
- reactNative.Animated.View,
1165
- {
1166
- style: {
1167
- flexDirection: "row",
1168
- justifyContent: "space-between",
1169
- opacity: infoOpacity,
1170
- position: "absolute",
1171
- bottom: 4,
1172
- left: 6,
1173
- right: 6
1174
- },
1175
- testID: "velocity-info-row",
1176
- children: [
1177
- /* @__PURE__ */ jsxs(
1178
- reactNative.Text,
1179
- {
1180
- className: "text-text-secondary",
1181
- style: {
1182
- fontSize: 10,
1183
- fontFamily: "Inter, sans-serif"
1184
- },
1185
- children: [
1186
- meanZone,
1187
- " ",
1188
- "\xB7",
1189
- " ",
1190
- formatVelocity(meanVelocity),
1191
- " m/s"
1192
- ]
1193
- }
1194
- ),
1195
- /* @__PURE__ */ jsxs(
1196
- reactNative.Text,
1197
- {
1198
- className: "text-text-secondary",
1199
- style: {
1200
- fontSize: 10,
1201
- fontFamily: "Inter, sans-serif",
1202
- ...getLossStyle(loss)
1203
- },
1204
- children: [
1205
- "Loss: ",
1206
- loss,
1207
- "%"
1208
- ]
1209
- }
1210
- )
1211
- ]
1212
- }
1213
- )
1214
- ]
1215
- }
1216
- );
1217
- if (onToggle) {
1218
- return /* @__PURE__ */ jsx(
1219
- reactNative.Pressable,
1220
- {
1221
- onPress: onToggle,
1222
- accessibilityRole: "button",
1223
- accessibilityLabel: stripLabel,
1224
- testID: "velocity-strip-pressable",
1225
- children: stripContent
1226
- }
1227
- );
1228
- }
1229
- return stripContent;
1230
- }
1231
- function cn(...inputs) {
1232
- return tailwindMerge.twMerge(clsx.clsx(inputs));
1233
- }
1234
-
1235
- // src/components/custom/Workout/PlaceholderStrip.tsx
1236
- function SingleStrip({
1237
- width,
1238
- className,
1239
- ...props
1240
- }) {
1241
- return /* @__PURE__ */ jsx(
1242
- reactNative.View,
1243
- {
1244
- className,
1245
- style: [
1246
- {
1247
- height: 3,
1248
- backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1249
- borderRadius: 2,
1250
- opacity: 0.5
1251
- },
1252
- width != null ? { width } : { flex: 1 }
1253
- ],
1254
- accessibilityRole: "image",
1255
- accessibilityLabel: "Planned set, not yet completed",
1256
- testID: "placeholder-strip",
1257
- ...props
1258
- }
1259
- );
1260
- }
1261
- function SegmentedStrip({
1262
- segments = 3,
1263
- width,
1264
- className,
1265
- ...props
1266
- }) {
1267
- return /* @__PURE__ */ jsx(
1268
- reactNative.View,
1269
- {
1270
- className: cn("flex-row", className),
1271
- style: [
1272
- { height: 3, gap: 2, opacity: 0.5 },
1273
- width != null ? { width } : { flex: 1 }
1274
- ],
1275
- accessibilityRole: "image",
1276
- accessibilityLabel: "Planned set, not yet completed",
1277
- testID: "placeholder-strip",
1278
- ...props,
1279
- children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
1280
- reactNative.View,
1281
- {
1282
- style: {
1283
- flex: 1,
1284
- height: 3,
1285
- backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1286
- borderRadius: 1,
1287
- minWidth: 4
1288
- },
1289
- accessibilityElementsHidden: true,
1290
- testID: "placeholder-segment"
1291
- },
1292
- i
1293
- ))
1294
- }
1295
- );
1296
- }
1297
- function PlaceholderStrip({
1298
- mode = "single",
1299
- segments,
1300
- ...props
1301
- }) {
1302
- if (mode === "segmented") {
1303
- return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
1304
- }
1305
- return /* @__PURE__ */ jsx(SingleStrip, { ...props });
1306
- }
1307
-
1308
- // src/components/icons/SvgIcon.tsx
1309
- function SvgIcon({
1310
- size = 24,
1311
- color = "currentColor",
1312
- strokeWidth = 2,
1313
- fill = "none",
1314
- title,
1315
- children
1316
- }) {
1317
- return /* @__PURE__ */ jsxs(
1318
- "svg",
1319
- {
1320
- width: size,
1321
- height: size,
1322
- viewBox: "0 0 24 24",
1323
- fill,
1324
- stroke: color,
1325
- strokeWidth,
1326
- strokeLinecap: "round",
1327
- strokeLinejoin: "round",
1328
- ...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
1329
- children: [
1330
- title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
1331
- children
1332
- ]
1333
- }
1334
- );
1335
- }
1336
-
1337
- // src/components/icons/icons.tsx
1338
- function StarIcon(props) {
1339
- return /* @__PURE__ */ jsx(SvgIcon, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" }) });
1340
- }
1341
- function ScaleIcon(props) {
1342
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1343
- /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1344
- /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1345
- /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
1346
- /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
1347
- /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
1348
- ] });
1349
- }
1350
- function AlertTriangleIcon(props) {
1351
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1352
- /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
1353
- /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1354
- /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1355
- ] });
1356
- }
1357
- function TrendingDownIcon(props) {
1358
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1359
- /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
1360
- /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
1361
- ] });
1362
- }
1363
- function CircleSlashIcon(props) {
1364
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1365
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1366
- /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
1367
- ] });
1368
- }
1369
- function AwardIcon(props) {
1370
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1371
- /* @__PURE__ */ jsx("path", { d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" }),
1372
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
1373
- ] });
1374
- }
1375
- function InfoIcon(props) {
1376
- return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1377
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1378
- /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
1379
- /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1380
- ] });
1381
- }
1382
-
1383
184
  // src/theme/tokens/semantic.ts
1384
185
  var semanticColorsLight = {
1385
186
  // Brand colors (brand-*)
@@ -1685,11 +486,1504 @@ var semanticColorsDark = {
1685
486
  "avatar-background": primitiveColors.neutral[600],
1686
487
  "avatar-text": primitiveColors.white
1687
488
  };
1688
- function getSemanticColors(mode) {
1689
- return mode === "dark" ? semanticColorsDark : semanticColorsLight;
489
+ function getSemanticColors(mode) {
490
+ return mode === "dark" ? semanticColorsDark : semanticColorsLight;
491
+ }
492
+ function hexToRgb(hex) {
493
+ const cleaned = hex.replace("#", "");
494
+ let r, g, b;
495
+ if (cleaned.length === 3) {
496
+ r = parseInt(cleaned[0] + cleaned[0], 16);
497
+ g = parseInt(cleaned[1] + cleaned[1], 16);
498
+ b = parseInt(cleaned[2] + cleaned[2], 16);
499
+ } else if (cleaned.length === 6) {
500
+ r = parseInt(cleaned.slice(0, 2), 16);
501
+ g = parseInt(cleaned.slice(2, 4), 16);
502
+ b = parseInt(cleaned.slice(4, 6), 16);
503
+ } else {
504
+ return null;
505
+ }
506
+ return { r, g, b };
507
+ }
508
+ function rgbToHsv(r, g, b) {
509
+ const rNorm = r / 255;
510
+ const gNorm = g / 255;
511
+ const bNorm = b / 255;
512
+ const max = Math.max(rNorm, gNorm, bNorm);
513
+ const min = Math.min(rNorm, gNorm, bNorm);
514
+ const delta = max - min;
515
+ const v = max;
516
+ const s = max === 0 ? 0 : delta / max;
517
+ let h = 0;
518
+ if (delta !== 0) {
519
+ if (max === rNorm) {
520
+ h = 60 * ((gNorm - bNorm) / delta % 6);
521
+ } else if (max === gNorm) {
522
+ h = 60 * ((bNorm - rNorm) / delta + 2);
523
+ } else {
524
+ h = 60 * ((rNorm - gNorm) / delta + 4);
525
+ }
526
+ }
527
+ if (h < 0) h += 360;
528
+ return { h, s, v };
529
+ }
530
+ function hsvToRgb(h, s, v) {
531
+ const c = v * s;
532
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
533
+ const m = v - c;
534
+ let rPrime, gPrime, bPrime;
535
+ if (h >= 0 && h < 60) {
536
+ rPrime = c;
537
+ gPrime = x;
538
+ bPrime = 0;
539
+ } else if (h >= 60 && h < 120) {
540
+ rPrime = x;
541
+ gPrime = c;
542
+ bPrime = 0;
543
+ } else if (h >= 120 && h < 180) {
544
+ rPrime = 0;
545
+ gPrime = c;
546
+ bPrime = x;
547
+ } else if (h >= 180 && h < 240) {
548
+ rPrime = 0;
549
+ gPrime = x;
550
+ bPrime = c;
551
+ } else if (h >= 240 && h < 300) {
552
+ rPrime = x;
553
+ gPrime = 0;
554
+ bPrime = c;
555
+ } else {
556
+ rPrime = c;
557
+ gPrime = 0;
558
+ bPrime = x;
559
+ }
560
+ return {
561
+ r: Math.round((rPrime + m) * 255),
562
+ g: Math.round((gPrime + m) * 255),
563
+ b: Math.round((bPrime + m) * 255)
564
+ };
565
+ }
566
+ function rgbToHex(r, g, b) {
567
+ const toHex = (n) => {
568
+ const clamped = Math.max(0, Math.min(255, Math.round(n)));
569
+ return clamped.toString(16).padStart(2, "0");
570
+ };
571
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
572
+ }
573
+ function lighten(hex, amount = 0.1) {
574
+ const rgb = hexToRgb(hex);
575
+ if (!rgb) return hex;
576
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
577
+ const newV = Math.min(1, hsv.v + amount);
578
+ const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
579
+ return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
580
+ }
581
+ var shadowColors = {
582
+ // For charcoal surfaces (~#2C2C2C to #3C3C3C)
583
+ charcoal: {
584
+ dark: "rgba(0, 0, 0, 0.5)",
585
+ light: "rgba(255, 255, 255, 0.12)",
586
+ // brighter for crisp rim light
587
+ darker: "rgba(0, 0, 0, 0.6)"
588
+ },
589
+ // For neutral dark surfaces (~#1F2937)
590
+ neutralDark: {
591
+ dark: "rgba(0, 0, 0, 0.4)",
592
+ light: "rgba(255, 255, 255, 0.08)",
593
+ darker: "rgba(0, 0, 0, 0.5)"
594
+ },
595
+ // For light surfaces (white/light gray backgrounds)
596
+ neutralLight: {
597
+ dark: "rgba(0, 0, 0, 0.15)",
598
+ // Visible dark shadow for depth on light backgrounds
599
+ light: "rgba(255, 255, 255, 1)",
600
+ // Bright highlight for raised effect (visible on light gray)
601
+ darker: "rgba(0, 0, 0, 0.2)"
602
+ // Deeper shadow for pressed states
603
+ }
604
+ };
605
+ var intensityConfig = {
606
+ subtle: { offset: 1, blur: 2 },
607
+ // very light, barely visible
608
+ medium: { offset: 2, blur: 4 },
609
+ // noticeable but restrained
610
+ strong: { offset: 2, blur: 5 }
611
+ // slightly deeper blur, same offset as medium
612
+ };
613
+ function createRaisedShadow(surface = "charcoal", intensity = "medium") {
614
+ const colors = shadowColors[surface];
615
+ const { offset, blur } = intensityConfig[intensity];
616
+ const rimBlur = 1;
617
+ return reactNative.Platform.select({
618
+ web: {
619
+ boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
620
+ },
621
+ default: {
622
+ // React Native shadow approximation (limited compared to web)
623
+ shadowColor: "#000",
624
+ shadowOffset: { width: offset, height: offset },
625
+ shadowOpacity: 0.3,
626
+ shadowRadius: blur / 2,
627
+ elevation: offset
628
+ }
629
+ });
630
+ }
631
+ function createPressedShadow(surface = "charcoal", intensity = "medium") {
632
+ const colors = shadowColors[surface];
633
+ const { offset, blur } = intensityConfig[intensity];
634
+ const rimOffset = 1;
635
+ const rimBlur = 1;
636
+ return reactNative.Platform.select({
637
+ web: {
638
+ boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
639
+ },
640
+ default: {
641
+ // React Native doesn't support inset shadows, approximate with darker bg
642
+ shadowOpacity: 0,
643
+ elevation: 0
644
+ }
645
+ });
646
+ }
647
+ function createFlatShadow() {
648
+ return reactNative.Platform.select({
649
+ web: {
650
+ boxShadow: "none"
651
+ },
652
+ default: {
653
+ shadowOpacity: 0,
654
+ elevation: 0
655
+ }
656
+ });
657
+ }
658
+ function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
659
+ const colors = shadowColors[surface];
660
+ const { offset, blur } = intensityConfig[intensity];
661
+ const hoverOffset = Math.max(1, Math.round(offset / 2));
662
+ const hoverBlur = Math.max(1, Math.round(blur / 2));
663
+ const rimBlur = 1;
664
+ return reactNative.Platform.select({
665
+ web: {
666
+ // Keep same offsets as normal state for rim to prevent "shrinking" effect
667
+ boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
668
+ },
669
+ default: {
670
+ shadowColor: "#000",
671
+ shadowOffset: { width: hoverOffset, height: hoverOffset },
672
+ shadowOpacity: 0.2,
673
+ shadowRadius: hoverBlur / 2,
674
+ elevation: hoverOffset
675
+ }
676
+ });
677
+ }
678
+ function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
679
+ const colors = shadowColors[surface];
680
+ const { offset, blur } = intensityConfig[intensity];
681
+ const hoverOffset = Math.max(1, Math.round(offset / 2));
682
+ const hoverBlur = Math.max(1, Math.round(blur / 2));
683
+ const rimBlur = 1;
684
+ return reactNative.Platform.select({
685
+ web: {
686
+ // Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
687
+ boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
688
+ },
689
+ default: {
690
+ shadowOpacity: 0,
691
+ elevation: 0
692
+ }
693
+ });
694
+ }
695
+ ({
696
+ // Charcoal surface shadows (for dark toolbars, cards)
697
+ charcoal: {
698
+ raised: {
699
+ subtle: createRaisedShadow("charcoal", "subtle"),
700
+ medium: createRaisedShadow("charcoal", "medium"),
701
+ strong: createRaisedShadow("charcoal", "strong")
702
+ },
703
+ raisedHover: {
704
+ subtle: createRaisedHoverShadow("charcoal", "subtle"),
705
+ medium: createRaisedHoverShadow("charcoal", "medium"),
706
+ strong: createRaisedHoverShadow("charcoal", "strong")
707
+ },
708
+ pressed: {
709
+ subtle: createPressedShadow("charcoal", "subtle"),
710
+ medium: createPressedShadow("charcoal", "medium"),
711
+ strong: createPressedShadow("charcoal", "strong")
712
+ },
713
+ pressedHover: {
714
+ subtle: createPressedHoverShadow("charcoal", "subtle"),
715
+ medium: createPressedHoverShadow("charcoal", "medium"),
716
+ strong: createPressedHoverShadow("charcoal", "strong")
717
+ },
718
+ flat: createFlatShadow()
719
+ },
720
+ // Neutral dark surface shadows
721
+ neutralDark: {
722
+ raised: {
723
+ subtle: createRaisedShadow("neutralDark", "subtle"),
724
+ medium: createRaisedShadow("neutralDark", "medium"),
725
+ strong: createRaisedShadow("neutralDark", "strong")
726
+ },
727
+ raisedHover: {
728
+ subtle: createRaisedHoverShadow("neutralDark", "subtle"),
729
+ medium: createRaisedHoverShadow("neutralDark", "medium"),
730
+ strong: createRaisedHoverShadow("neutralDark", "strong")
731
+ },
732
+ pressed: {
733
+ subtle: createPressedShadow("neutralDark", "subtle"),
734
+ medium: createPressedShadow("neutralDark", "medium"),
735
+ strong: createPressedShadow("neutralDark", "strong")
736
+ },
737
+ pressedHover: {
738
+ subtle: createPressedHoverShadow("neutralDark", "subtle"),
739
+ medium: createPressedHoverShadow("neutralDark", "medium"),
740
+ strong: createPressedHoverShadow("neutralDark", "strong")
741
+ },
742
+ flat: createFlatShadow()
743
+ },
744
+ // Neutral light surface shadows
745
+ neutralLight: {
746
+ raised: {
747
+ subtle: createRaisedShadow("neutralLight", "subtle"),
748
+ medium: createRaisedShadow("neutralLight", "medium"),
749
+ strong: createRaisedShadow("neutralLight", "strong")
750
+ },
751
+ raisedHover: {
752
+ subtle: createRaisedHoverShadow("neutralLight", "subtle"),
753
+ medium: createRaisedHoverShadow("neutralLight", "medium"),
754
+ strong: createRaisedHoverShadow("neutralLight", "strong")
755
+ },
756
+ pressed: {
757
+ subtle: createPressedShadow("neutralLight", "subtle"),
758
+ medium: createPressedShadow("neutralLight", "medium"),
759
+ strong: createPressedShadow("neutralLight", "strong")
760
+ },
761
+ pressedHover: {
762
+ subtle: createPressedHoverShadow("neutralLight", "subtle"),
763
+ medium: createPressedHoverShadow("neutralLight", "medium"),
764
+ strong: createPressedHoverShadow("neutralLight", "strong")
765
+ },
766
+ flat: createFlatShadow()
767
+ }
768
+ });
769
+
770
+ // src/utils/colors.ts
771
+ function alpha(color, opacity) {
772
+ const clampedOpacity = Math.max(0, Math.min(1, opacity));
773
+ if (color.startsWith("#")) {
774
+ const hex = color.slice(1);
775
+ let r, g, b;
776
+ if (hex.length === 3) {
777
+ r = parseInt(hex[0] + hex[0], 16);
778
+ g = parseInt(hex[1] + hex[1], 16);
779
+ b = parseInt(hex[2] + hex[2], 16);
780
+ } else if (hex.length === 6) {
781
+ r = parseInt(hex.slice(0, 2), 16);
782
+ g = parseInt(hex.slice(2, 4), 16);
783
+ b = parseInt(hex.slice(4, 6), 16);
784
+ } else {
785
+ return color;
786
+ }
787
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
788
+ }
789
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
790
+ if (rgbMatch) {
791
+ const [, r, g, b] = rgbMatch;
792
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
793
+ }
794
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
795
+ if (rgbaMatch) {
796
+ const [, r, g, b] = rgbaMatch;
797
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
798
+ }
799
+ return color;
800
+ }
801
+
802
+ // src/utils/workout-format.ts
803
+ function roundRpe(rpe) {
804
+ return Math.round(rpe * 2) / 2;
805
+ }
806
+ function roundWeight(weight) {
807
+ return Math.round(weight);
808
+ }
809
+ function formatVelocity(velocity) {
810
+ return velocity.toFixed(2);
811
+ }
812
+ function roundTempo(tempo) {
813
+ return tempo.map((v) => Math.round(v * 10) / 10);
814
+ }
815
+
816
+ // src/theme/bar-paper.ts
817
+ function grainOpacityForColor(hex) {
818
+ const h = hex.replace("#", "");
819
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
820
+ const r = parseInt(full.slice(0, 2), 16);
821
+ const g = parseInt(full.slice(2, 4), 16);
822
+ const b = parseInt(full.slice(4, 6), 16);
823
+ if ([r, g, b].some(Number.isNaN)) return 0.14;
824
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
825
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
826
+ }
827
+ function barGrain(color) {
828
+ const op = grainOpacityForColor(color).toFixed(3);
829
+ return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='${op}'/%3E%3C/svg%3E")`;
830
+ }
831
+ function barPaper(color, flip = false) {
832
+ return {
833
+ backgroundImage: barGrain(color),
834
+ boxShadow: flip ? "0 -6px 16px rgba(0,0,0,0.45)" : "inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)"
835
+ };
836
+ }
837
+ var SURFACE_LEVEL_TOKEN = {
838
+ background: "background-base",
839
+ base: "surface-base",
840
+ elevated: "surface-elevated",
841
+ raised: "surface-raised",
842
+ overlay: "surface-overlay"
843
+ };
844
+ var ON_SURFACE_TOKEN = {
845
+ primary: "text-primary",
846
+ secondary: "text-secondary",
847
+ tertiary: "text-tertiary"
848
+ };
849
+ var DEFAULT_CONTEXT = { mode: "dark", level: "base" };
850
+ var SurfaceContext = react.createContext(DEFAULT_CONTEXT);
851
+ function useSurface() {
852
+ return react.useContext(SurfaceContext);
853
+ }
854
+ function useSurfaceMode() {
855
+ return react.useContext(SurfaceContext).mode;
856
+ }
857
+ function useOnSurfaceColor(role = "primary") {
858
+ return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
859
+ }
860
+ function insetFloor(mode) {
861
+ const colors = getSemanticColors(mode);
862
+ return colors["surface-inset"] ?? surfaceRampDark.inset;
863
+ }
864
+ function surfaceBackground(level, mode) {
865
+ if (level === "inset") return insetFloor(mode);
866
+ return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
867
+ }
868
+ var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
869
+ var PEAK_OVERSHOOT = 1.12;
870
+ function getReducedMotionPreference() {
871
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
872
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
873
+ }
874
+ function usePrefersReducedMotion() {
875
+ const [reduced, setReduced] = react.useState(getReducedMotionPreference);
876
+ react.useEffect(() => {
877
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
878
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
879
+ const handler = () => setReduced(mq.matches);
880
+ handler();
881
+ mq.addEventListener?.("change", handler);
882
+ return () => mq.removeEventListener?.("change", handler);
883
+ }, []);
884
+ return reduced;
885
+ }
886
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
887
+ const prefersReducedMotion = usePrefersReducedMotion();
888
+ const [liveGrowth] = react.useState(() => new reactNative.Animated.Value(1));
889
+ react.useEffect(() => {
890
+ if (liveRepIndex == null || liveVelocity == null) return;
891
+ if (prefersReducedMotion) {
892
+ liveGrowth.setValue(1);
893
+ return;
894
+ }
895
+ liveGrowth.setValue(0);
896
+ if (isNewPeak) {
897
+ reactNative.Animated.sequence([
898
+ reactNative.Animated.timing(liveGrowth, {
899
+ toValue: PEAK_OVERSHOOT,
900
+ duration: 220,
901
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
902
+ useNativeDriver: false
903
+ }),
904
+ reactNative.Animated.spring(liveGrowth, {
905
+ toValue: 1,
906
+ friction: 5,
907
+ tension: 140,
908
+ useNativeDriver: false
909
+ })
910
+ ]).start();
911
+ } else {
912
+ reactNative.Animated.timing(liveGrowth, {
913
+ toValue: 1,
914
+ duration: 300,
915
+ easing: ANIMATION_EASING,
916
+ useNativeDriver: false
917
+ }).start();
918
+ }
919
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
920
+ return liveGrowth;
921
+ }
922
+ function wrapJsx(fn) {
923
+ return function(type, props, ...rest) {
924
+ if (props && typeof props.className === "string" && props.className) {
925
+ const cn2 = props.className;
926
+ const cssStyle = { $$css: true, [cn2]: cn2 };
927
+ const existing = props.style;
928
+ props = {
929
+ ...props,
930
+ style: existing ? [cssStyle, existing] : cssStyle
931
+ };
932
+ delete props.className;
933
+ }
934
+ return fn(type, props, ...rest);
935
+ };
936
+ }
937
+ var jsx = wrapJsx(jsxRuntime.jsx);
938
+ var jsxs = wrapJsx(jsxRuntime.jsxs);
939
+
940
+ // src/components/custom/charts/SetBarChart.tsx
941
+ function mixHex(a, b, t12) {
942
+ const parse = (h) => {
943
+ const s = h.replace("#", "");
944
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
945
+ };
946
+ const [ar, ag, ab] = parse(a);
947
+ const [br, bg, bb] = parse(b);
948
+ const ch = (x, y) => Math.round(x + (y - x) * t12).toString(16).padStart(2, "0");
949
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
950
+ }
951
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
952
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
953
+ var FIXED_MAX_VALUE = 1.15;
954
+ var PEAK_HEADROOM = 1.03;
955
+ function scaleDenominator(scale, maxValue) {
956
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
957
+ }
958
+ function valueLabelFontSize(height) {
959
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
960
+ }
961
+ var SET_BAR_DEFAULT_HEIGHT = 220;
962
+ var LABEL_ROW_HEIGHT = 16;
963
+ var LABEL_GAP = 3;
964
+ var GAP_RATIO = 0.08;
965
+ var MIN_BAR_GAP = 2;
966
+ var CHUNK_NOTCH_RATIO = 0.25;
967
+ var CHUNK_NOTCH_MIN_PX = 10;
968
+ var BAR_MAX_WIDTH = 120;
969
+ var LABEL_MIN_BAR_WIDTH = 30;
970
+ var DEFAULT_BAR_RADIUS = 5;
971
+ var MIN_BAR_HEIGHT = 4;
972
+ var FLAT_BAR_FRACTION = 0.5;
973
+ var STUB_HEIGHT = 9;
974
+ var REP_GAP = 2;
975
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
976
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
977
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
978
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
979
+ return { barWidth, gap };
980
+ }
981
+ function isRep(slot) {
982
+ return slot.kind === "rep";
983
+ }
984
+ function repIndexByCell(cells) {
985
+ let count = 0;
986
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
987
+ }
988
+ function SetBarChart({
989
+ slots,
990
+ colorFor,
991
+ height,
992
+ scale = "peak",
993
+ scaleMax,
994
+ orientation = "up",
995
+ liveRepIndex,
996
+ isNewPeak = false,
997
+ targetReps,
998
+ gapRatio = GAP_RATIO,
999
+ barRadius = DEFAULT_BAR_RADIUS,
1000
+ cornerStyle = "top",
1001
+ flat = false,
1002
+ showValueLabels: showValueLabelsProp = false,
1003
+ formatValue: formatValue2 = String,
1004
+ todoVariant = "solid",
1005
+ minColumns,
1006
+ renderReference,
1007
+ renderBarOverlay,
1008
+ expandProgress,
1009
+ hideBaseline = false,
1010
+ testID,
1011
+ testIDPrefix = "setbar",
1012
+ accessibilityLabel: accessibilityLabel2,
1013
+ label,
1014
+ className,
1015
+ viewProps = {}
1016
+ }) {
1017
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
1018
+ const cells = targetPadded > slots.length ? [
1019
+ ...slots,
1020
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
1021
+ ] : slots;
1022
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
1023
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
1024
+ const flip = orientation === "down";
1025
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
1026
+ const placeholderColor = useOnSurfaceColor("tertiary");
1027
+ const surface = useSurface();
1028
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
1029
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
1030
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
1031
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
1032
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
1033
+ const showValueLabels = showValueLabelsProp && !flat;
1034
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
1035
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
1036
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
1037
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
1038
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
1039
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
1040
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
1041
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
1042
+ const [plotW, setPlotW] = react.useState(0);
1043
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
1044
+ const totalCells = cells.length;
1045
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
1046
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
1047
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
1048
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
1049
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
1050
+ const { style: externalStyle, ...restProps } = viewProps;
1051
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
1052
+ const repIndices = repIndexByCell(cells);
1053
+ return /* @__PURE__ */ jsxs(
1054
+ reactNative.View,
1055
+ {
1056
+ className,
1057
+ style: [{ height, flexDirection: "row" }, externalStyle],
1058
+ testID,
1059
+ ...a11y,
1060
+ ...restProps,
1061
+ children: [
1062
+ label ? /* @__PURE__ */ jsx(
1063
+ ChartSideRail,
1064
+ {
1065
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
1066
+ sectionExtent: height
1067
+ }
1068
+ ) : null,
1069
+ /* @__PURE__ */ jsxs(
1070
+ reactNative.View,
1071
+ {
1072
+ onLayout: onPlotLayout,
1073
+ style: {
1074
+ flex: 1,
1075
+ flexDirection: "row",
1076
+ alignItems: "flex-end",
1077
+ gap,
1078
+ position: "relative",
1079
+ borderBottomWidth: hideBaseline ? 0 : 2,
1080
+ borderBottomColor: placeholderColor,
1081
+ ...flipStyle
1082
+ },
1083
+ children: [
1084
+ renderReference?.(geometry),
1085
+ cells.map((slot, i) => {
1086
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
1087
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
1088
+ const column = {
1089
+ flex: 1,
1090
+ maxWidth: BAR_MAX_WIDTH,
1091
+ height: "100%",
1092
+ alignItems: "center",
1093
+ justifyContent: "flex-end",
1094
+ marginLeft: extraGap
1095
+ };
1096
+ if (!isRep(slot)) {
1097
+ return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
1098
+ slot.kind,
1099
+ barCorners,
1100
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
1101
+ flat ? compactBarHeight : STUB_HEIGHT,
1102
+ placeholderColor,
1103
+ testIDPrefix,
1104
+ todoVariant,
1105
+ solidTodoColor,
1106
+ emptyColor
1107
+ ) }, i);
1108
+ }
1109
+ const repIndex = repIndices[i];
1110
+ const value = slot.value ?? 0;
1111
+ const isLive = liveRepIndex === repIndex;
1112
+ const color = colorFor(value);
1113
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
1114
+ inputRange: [0, 1],
1115
+ outputRange: [flatBarHeight, valueBarHeight(value)]
1116
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
1117
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
1118
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
1119
+ return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
1120
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
1121
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
1122
+ /* @__PURE__ */ jsx(
1123
+ reactNative.Animated.View,
1124
+ {
1125
+ style: {
1126
+ position: "absolute",
1127
+ bottom: labelBottom,
1128
+ left: 0,
1129
+ right: 0,
1130
+ alignItems: "center",
1131
+ ...expandProgress ? { opacity: expandProgress } : null
1132
+ },
1133
+ pointerEvents: "none",
1134
+ children: /* @__PURE__ */ jsx(
1135
+ reactNative.Text,
1136
+ {
1137
+ className: "text-text-secondary",
1138
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
1139
+ testID: `${testIDPrefix}-label-${repIndex}`,
1140
+ children: formatValue2(value)
1141
+ }
1142
+ )
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ jsx(
1146
+ reactNative.Animated.View,
1147
+ {
1148
+ style: [
1149
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
1150
+ barCorners,
1151
+ barPaper(color, flip)
1152
+ ],
1153
+ testID: `${testIDPrefix}-bar-${repIndex}`
1154
+ }
1155
+ ),
1156
+ renderBarOverlay?.(repIndex, value)
1157
+ ] }, i);
1158
+ })
1159
+ ]
1160
+ }
1161
+ )
1162
+ ]
1163
+ }
1164
+ );
1165
+ }
1166
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
1167
+ const base = {
1168
+ width: "100%",
1169
+ height: stubHeight,
1170
+ ...barCorners
1171
+ };
1172
+ if (kind === "variable") {
1173
+ return /* @__PURE__ */ jsx(
1174
+ reactNative.View,
1175
+ {
1176
+ style: { ...base, backgroundColor: VARIABLE_FILL },
1177
+ testID: `${testIDPrefix}-slot-variable`
1178
+ }
1179
+ );
1180
+ }
1181
+ if (kind === "continue") {
1182
+ return /* @__PURE__ */ jsx(
1183
+ reactNative.View,
1184
+ {
1185
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
1186
+ testID: `${testIDPrefix}-slot-continue`
1187
+ }
1188
+ );
1189
+ }
1190
+ if (kind === "empty") {
1191
+ return /* @__PURE__ */ jsx(
1192
+ reactNative.View,
1193
+ {
1194
+ style: { ...base, backgroundColor: emptyColor },
1195
+ testID: `${testIDPrefix}-slot-empty`
1196
+ }
1197
+ );
1198
+ }
1199
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
1200
+ return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
1201
+ }
1202
+ function sideLabelFontSize(height) {
1203
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
1204
+ }
1205
+ var SIDE_LABEL_INITIALS_BELOW = 70;
1206
+ function sideLabelText(label, extent) {
1207
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
1208
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
1209
+ }
1210
+ function ChartSideRail({
1211
+ sections,
1212
+ sectionExtent,
1213
+ variant = "hero"
1214
+ }) {
1215
+ if (!sections.some((s) => s.label)) return null;
1216
+ const isRail = variant === "rail";
1217
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
1218
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
1219
+ const textStyle = {
1220
+ maxWidth: "100%",
1221
+ textAlign: "center",
1222
+ fontSize,
1223
+ fontWeight: "700",
1224
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
1225
+ textTransform: "uppercase"
1226
+ };
1227
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
1228
+ reactNative.View,
1229
+ {
1230
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
1231
+ children: s.label ? /* @__PURE__ */ jsx(
1232
+ reactNative.View,
1233
+ {
1234
+ style: {
1235
+ width: labelLength,
1236
+ alignItems: "center",
1237
+ transform: [{ rotate: "-90deg" }]
1238
+ },
1239
+ children: /* @__PURE__ */ jsx(
1240
+ reactNative.Text,
1241
+ {
1242
+ className: "text-text-tertiary",
1243
+ style: textStyle,
1244
+ numberOfLines: 1,
1245
+ testID: s.testID,
1246
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
1247
+ }
1248
+ )
1249
+ }
1250
+ ) : null
1251
+ },
1252
+ s.testID
1253
+ )) });
1254
+ }
1255
+
1256
+ // src/components/custom/Workout/VelocityStrip.tsx
1257
+ var VEL_COLORS = WORKOUT_TOKENS.scale;
1258
+ var bandIdToEffortColor = {
1259
+ speed: sequentialEffort[0],
1260
+ // green-300
1261
+ power: sequentialEffort[2],
1262
+ // amber-300 (gold)
1263
+ strengthSpeed: sequentialEffort[3],
1264
+ // orange-400
1265
+ maximalStrength: sequentialEffort[4],
1266
+ // red-600
1267
+ grinding: sequentialEffort[5]
1268
+ // red-700 — the distinct 5th band
1269
+ };
1270
+ function getVelocityZoneColor(velocity) {
1271
+ if (velocity >= 1) return "vel-green";
1272
+ if (velocity >= 0.75) return "vel-yellow";
1273
+ if (velocity >= 0.5) return "vel-orange";
1274
+ return "vel-red";
1275
+ }
1276
+ function getVelocityZoneName(velocity) {
1277
+ if (velocity >= 1) return "Speed";
1278
+ if (velocity >= 0.75) return "Power";
1279
+ if (velocity >= 0.5) return "Strength-Speed";
1280
+ return "Strength";
1281
+ }
1282
+ var zoneHexMap = {
1283
+ "vel-green": VEL_COLORS.green,
1284
+ "vel-yellow": VEL_COLORS.yellow,
1285
+ "vel-orange": VEL_COLORS.orange,
1286
+ "vel-red": VEL_COLORS.red
1287
+ };
1288
+ function calculateVelocityLoss(velocities) {
1289
+ if (velocities.length < 2) return 0;
1290
+ const best = Math.max(...velocities);
1291
+ if (best <= 0) return 0;
1292
+ const last = velocities[velocities.length - 1];
1293
+ return Math.max(0, Math.round((best - last) / best * 100));
1294
+ }
1295
+ function calculateMeanVelocity(velocities) {
1296
+ if (velocities.length === 0) return 0;
1297
+ const sum = velocities.reduce((acc, v) => acc + v, 0);
1298
+ return sum / velocities.length;
1299
+ }
1300
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
1301
+ function getVelocityLossColor(lossPct) {
1302
+ const [t1, t22, t32] = VL_LOSS_THRESHOLDS;
1303
+ if (lossPct < t1) return VEL_COLORS.green;
1304
+ if (lossPct < t22) return VEL_COLORS.yellow;
1305
+ if (lossPct < t32) return VEL_COLORS.orange;
1306
+ return VEL_COLORS.red;
1307
+ }
1308
+ function velocityLossForRep(velocity, best) {
1309
+ if (best <= 0) return 0;
1310
+ return Math.max(0, Math.round((best - velocity) / best * 100));
1311
+ }
1312
+ function classifyBand(velocity, bands) {
1313
+ for (const band of bands) {
1314
+ if (band.max === null || velocity < band.max) return band;
1315
+ }
1316
+ return bands[bands.length - 1];
1317
+ }
1318
+ function bandColor(band) {
1319
+ return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
1320
+ }
1321
+ function makeBarColorFor(zones) {
1322
+ const hasZones = zones != null && zones.length > 0;
1323
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
1324
+ }
1325
+ function getLossStyle(loss) {
1326
+ if (loss > 25) return { color: VEL_COLORS.red };
1327
+ if (loss > 20) return { color: VEL_COLORS.orange };
1328
+ return null;
1329
+ }
1330
+ var REP_GAP2 = 2;
1331
+ var WIDE_GAP = 8;
1332
+ function deriveDoneVelocities(set) {
1333
+ switch (set.type) {
1334
+ case "drop":
1335
+ return set.subloads.flat();
1336
+ case "myo":
1337
+ return [...set.activation, ...set.clusters.flat()];
1338
+ default:
1339
+ return set.velocities;
1340
+ }
1341
+ }
1342
+ function chunkedRepSlots(chunks) {
1343
+ const slots = [];
1344
+ chunks.forEach((chunk) => {
1345
+ chunk.forEach((velocity, ri) => {
1346
+ const first = slots.length === 0;
1347
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
1348
+ });
1349
+ });
1350
+ return slots;
1351
+ }
1352
+ function buildSlots(set) {
1353
+ switch (set.type) {
1354
+ case "straight": {
1355
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
1356
+ return Array.from({ length: total }, (_, i) => {
1357
+ const done = i < set.velocities.length;
1358
+ return {
1359
+ kind: done ? "rep" : "todo",
1360
+ velocity: done ? set.velocities[i] : void 0,
1361
+ leadingGap: i === 0 ? 0 : REP_GAP2
1362
+ };
1363
+ });
1364
+ }
1365
+ case "range": {
1366
+ const total = Math.max(set.max, set.velocities.length);
1367
+ return Array.from({ length: total }, (_, i) => {
1368
+ const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
1369
+ return {
1370
+ kind,
1371
+ velocity: kind === "rep" ? set.velocities[i] : void 0,
1372
+ leadingGap: i === 0 ? 0 : REP_GAP2
1373
+ };
1374
+ });
1375
+ }
1376
+ case "amrap": {
1377
+ const reps = set.velocities.map((velocity, i) => ({
1378
+ kind: "rep",
1379
+ velocity,
1380
+ leadingGap: i === 0 ? 0 : REP_GAP2
1381
+ }));
1382
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
1383
+ return reps;
1384
+ }
1385
+ case "drop":
1386
+ return chunkedRepSlots(set.subloads);
1387
+ case "myo": {
1388
+ const slots = chunkedRepSlots([set.activation, ...set.clusters]);
1389
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
1390
+ return slots;
1391
+ }
1392
+ case "cluster": {
1393
+ const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
1394
+ return Array.from({ length: total }, (_, i) => {
1395
+ const done = i < set.velocities.length;
1396
+ const boundary = i > 0 && i % set.groupSize === 0;
1397
+ return {
1398
+ kind: done ? "rep" : "todo",
1399
+ velocity: done ? set.velocities[i] : void 0,
1400
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
1401
+ };
1402
+ });
1403
+ }
1404
+ }
1405
+ }
1406
+ function setAccessibilityLabel(set, repCount) {
1407
+ switch (set.type) {
1408
+ case "straight":
1409
+ return `Velocity strip, straight set, ${repCount} reps`;
1410
+ case "range":
1411
+ return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
1412
+ case "amrap":
1413
+ return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
1414
+ case "drop":
1415
+ return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
1416
+ case "myo":
1417
+ return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
1418
+ case "cluster":
1419
+ return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
1420
+ }
1421
+ }
1422
+ var EXPANDED_HEIGHT = 60;
1423
+ var COMPACT_HEIGHT = 8;
1424
+ var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
1425
+ function DashedReferenceLine({
1426
+ anchor,
1427
+ offset,
1428
+ testID
1429
+ }) {
1430
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
1431
+ return /* @__PURE__ */ jsx(
1432
+ reactNative.View,
1433
+ {
1434
+ accessibilityElementsHidden: true,
1435
+ style: {
1436
+ position: "absolute",
1437
+ left: 0,
1438
+ right: 0,
1439
+ borderTopWidth: 1,
1440
+ borderStyle: "dashed",
1441
+ borderColor: HERO_REFERENCE_COLOR,
1442
+ pointerEvents: "none",
1443
+ ...edge
1444
+ },
1445
+ testID
1446
+ }
1447
+ );
1448
+ }
1449
+ var BAND_LABEL_FONT = "monospace";
1450
+ var VL_LABEL_MIN_PLOT = 65;
1451
+ var VL_SEMANTIC = getSemanticColors("dark");
1452
+ function VelocityLossBands({
1453
+ best,
1454
+ scaleDenom,
1455
+ plotHeight,
1456
+ flip = false
1457
+ }) {
1458
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
1459
+ const yOf = (v) => v / scaleDenom * plotHeight;
1460
+ const vl20 = best * 0.8;
1461
+ const vl30 = best * 0.7;
1462
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
1463
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
1464
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
1465
+ reactNative.View,
1466
+ {
1467
+ style: {
1468
+ position: "absolute",
1469
+ left: 0,
1470
+ right: 0,
1471
+ bottom: yOf(loV),
1472
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
1473
+ backgroundColor: color
1474
+ }
1475
+ }
1476
+ );
1477
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
1478
+ reactNative.View,
1479
+ {
1480
+ style: {
1481
+ position: "absolute",
1482
+ left: 0,
1483
+ right: 0,
1484
+ bottom: yOf(v),
1485
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
1486
+ // band edge — instead of floating up half a text-row (the label then centres on
1487
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
1488
+ height: 0,
1489
+ flexDirection: "row",
1490
+ alignItems: "center"
1491
+ },
1492
+ children: [
1493
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
1494
+ showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1495
+ /* @__PURE__ */ jsx(
1496
+ reactNative.Text,
1497
+ {
1498
+ style: {
1499
+ marginHorizontal: 6,
1500
+ fontSize: vlFont,
1501
+ fontWeight: "800",
1502
+ fontFamily: BAND_LABEL_FONT,
1503
+ color,
1504
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
1505
+ },
1506
+ children: label
1507
+ }
1508
+ ),
1509
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
1510
+ ] }) : null
1511
+ ]
1512
+ }
1513
+ );
1514
+ return /* @__PURE__ */ jsxs(
1515
+ reactNative.View,
1516
+ {
1517
+ accessibilityElementsHidden: true,
1518
+ pointerEvents: "none",
1519
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
1520
+ testID: "velocity-loss-bands",
1521
+ children: [
1522
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
1523
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
1524
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
1525
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
1526
+ ]
1527
+ }
1528
+ );
1529
+ }
1530
+ function velocityReferenceOverlay(g, showLossBands) {
1531
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
1532
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1533
+ showLossBands && /* @__PURE__ */ jsx(
1534
+ VelocityLossBands,
1535
+ {
1536
+ best: g.best,
1537
+ scaleDenom: g.scaleDenom,
1538
+ plotHeight: g.plotHeight,
1539
+ flip: g.flip
1540
+ }
1541
+ ),
1542
+ referencePx > 0 && /* @__PURE__ */ jsx(
1543
+ DashedReferenceLine,
1544
+ {
1545
+ anchor: "bottom",
1546
+ offset: referencePx,
1547
+ testID: "velocity-hero-reference"
1548
+ }
1549
+ )
1550
+ ] });
1551
+ }
1552
+ var ANIMATION_DURATION = 400;
1553
+ function VelocityStrip({
1554
+ velocities,
1555
+ set,
1556
+ zones,
1557
+ barColor = "loss",
1558
+ showLossBands,
1559
+ orientation = "up",
1560
+ scaleMax,
1561
+ hideBaseline,
1562
+ columnSlots,
1563
+ label,
1564
+ liveRepIndex,
1565
+ expanded = true,
1566
+ onToggle,
1567
+ onRepPress,
1568
+ variant = "expanded",
1569
+ height = EXPANDED_HEIGHT,
1570
+ targetReps,
1571
+ scale = "peak",
1572
+ showNumbers = true,
1573
+ showInfo = true,
1574
+ className,
1575
+ ...props
1576
+ }) {
1577
+ const lossBandsOn = showLossBands ?? barColor === "loss";
1578
+ const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
1579
+ const maxVelocity = Math.max(...doneVelocities, 0);
1580
+ const meanVelocity = calculateMeanVelocity(doneVelocities);
1581
+ const loss = calculateVelocityLoss(doneVelocities);
1582
+ const framed = showNumbers || showInfo;
1583
+ const hasZones = zones != null && zones.length > 0;
1584
+ const zoneColorFor = makeBarColorFor(zones);
1585
+ const barColorFor = (v) => barColor === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
1586
+ const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
1587
+ const [expandProgress] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
1588
+ const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
1589
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
1590
+ const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
1591
+ react.useEffect(() => {
1592
+ if (variant !== "expanded" || !framed) return;
1593
+ reactNative.Animated.parallel([
1594
+ reactNative.Animated.timing(expandProgress, {
1595
+ toValue: expanded ? 1 : 0,
1596
+ duration: ANIMATION_DURATION,
1597
+ easing: ANIMATION_EASING,
1598
+ useNativeDriver: false
1599
+ }),
1600
+ reactNative.Animated.timing(infoOpacity, {
1601
+ toValue: expanded ? 1 : 0,
1602
+ duration: 300,
1603
+ delay: expanded ? 200 : 0,
1604
+ useNativeDriver: false
1605
+ })
1606
+ ]).start();
1607
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
1608
+ if (set == null && velocities == null) return null;
1609
+ const repCount = doneVelocities.length;
1610
+ const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
1611
+ if (variant === "hero") {
1612
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
1613
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1614
+ kind: s.kind,
1615
+ value: s.velocity,
1616
+ leadingGap: s.leadingGap
1617
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1618
+ const total = Math.max(repCount, targetReps ?? repCount);
1619
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
1620
+ return /* @__PURE__ */ jsx(
1621
+ SetBarChart,
1622
+ {
1623
+ slots: heroSlots,
1624
+ colorFor: barColorFor,
1625
+ height: heroHeight,
1626
+ scale,
1627
+ scaleMax,
1628
+ orientation,
1629
+ liveRepIndex,
1630
+ isNewPeak,
1631
+ targetReps: set || columnSlots ? void 0 : targetReps,
1632
+ label,
1633
+ showValueLabels: true,
1634
+ formatValue: formatVelocity,
1635
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
1636
+ hideBaseline: true,
1637
+ testID: "velocity-strip-hero",
1638
+ testIDPrefix: "velocity",
1639
+ accessibilityLabel: heroLabel,
1640
+ className,
1641
+ viewProps: props
1642
+ }
1643
+ );
1644
+ }
1645
+ if (variant === "compact") {
1646
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1647
+ kind: s.kind,
1648
+ value: s.velocity,
1649
+ leadingGap: s.leadingGap
1650
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1651
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
1652
+ return /* @__PURE__ */ jsx(
1653
+ SetBarChart,
1654
+ {
1655
+ slots: compactSlots,
1656
+ colorFor: barColorFor,
1657
+ height: compactHeight,
1658
+ scale,
1659
+ scaleMax,
1660
+ orientation,
1661
+ flat: true,
1662
+ barRadius: 2,
1663
+ cornerStyle: "all",
1664
+ targetReps: set || columnSlots ? void 0 : targetReps,
1665
+ label,
1666
+ hideBaseline: true,
1667
+ testID: "velocity-strip-compact",
1668
+ testIDPrefix: "velocity",
1669
+ accessibilityLabel: miniLabel,
1670
+ className,
1671
+ viewProps: props
1672
+ }
1673
+ );
1674
+ }
1675
+ if (!framed) {
1676
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
1677
+ kind: s.kind,
1678
+ value: s.velocity,
1679
+ leadingGap: s.leadingGap
1680
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
1681
+ return /* @__PURE__ */ jsx(
1682
+ SetBarChart,
1683
+ {
1684
+ slots: spotlightSlots,
1685
+ colorFor: barColorFor,
1686
+ height,
1687
+ scale,
1688
+ scaleMax,
1689
+ orientation,
1690
+ liveRepIndex,
1691
+ isNewPeak,
1692
+ barRadius: 2,
1693
+ cornerStyle: "top",
1694
+ targetReps: set ? void 0 : targetReps,
1695
+ label,
1696
+ hideBaseline: true,
1697
+ testID: "velocity-strip-spotlight",
1698
+ testIDPrefix: "velocity",
1699
+ accessibilityLabel: miniLabel,
1700
+ className,
1701
+ viewProps: props
1702
+ }
1703
+ );
1704
+ }
1705
+ const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
1706
+ const hasInteractiveContainer = onToggle != null;
1707
+ const hasInteractiveReps = onRepPress != null && expanded;
1708
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
1709
+ kind: sl.kind,
1710
+ value: sl.velocity,
1711
+ leadingGap: sl.leadingGap
1712
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
1713
+ const needsBarOverlay = showNumbers || onRepPress != null;
1714
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
1715
+ showNumbers && /* @__PURE__ */ jsx(
1716
+ reactNative.Animated.View,
1717
+ {
1718
+ style: {
1719
+ opacity: expandProgress,
1720
+ position: "absolute",
1721
+ top: -13,
1722
+ left: 0,
1723
+ right: 0,
1724
+ alignItems: "center"
1725
+ },
1726
+ accessibilityElementsHidden: true,
1727
+ pointerEvents: "none",
1728
+ children: /* @__PURE__ */ jsx(
1729
+ reactNative.Text,
1730
+ {
1731
+ className: "text-text-secondary",
1732
+ style: { fontSize: 8, fontWeight: "600" },
1733
+ testID: `velocity-label-${repIndex}`,
1734
+ children: formatVelocity(value)
1735
+ }
1736
+ )
1737
+ }
1738
+ ),
1739
+ onRepPress && expanded && /* @__PURE__ */ jsx(
1740
+ reactNative.Pressable,
1741
+ {
1742
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
1743
+ onPress: () => onRepPress(repIndex, value),
1744
+ accessibilityRole: "button",
1745
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
1746
+ testID: `velocity-bar-pressable-${repIndex}`
1747
+ }
1748
+ )
1749
+ ] }) : void 0;
1750
+ const stripContent = /* @__PURE__ */ jsxs(
1751
+ reactNative.Animated.View,
1752
+ {
1753
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
1754
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
1755
+ accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
1756
+ accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
1757
+ testID: "velocity-strip",
1758
+ ...props,
1759
+ children: [
1760
+ /* @__PURE__ */ jsx(
1761
+ SetBarChart,
1762
+ {
1763
+ slots: framedSlots,
1764
+ colorFor: barColorFor,
1765
+ height,
1766
+ scale,
1767
+ scaleMax,
1768
+ expandProgress,
1769
+ renderBarOverlay: renderFramedBarOverlay,
1770
+ targetReps: set ? void 0 : targetReps,
1771
+ barRadius: 2,
1772
+ cornerStyle: "top",
1773
+ hideBaseline: true,
1774
+ testIDPrefix: "velocity"
1775
+ }
1776
+ ),
1777
+ expanded && showInfo && /* @__PURE__ */ jsxs(
1778
+ reactNative.Animated.View,
1779
+ {
1780
+ style: {
1781
+ flexDirection: "row",
1782
+ justifyContent: "space-between",
1783
+ opacity: infoOpacity,
1784
+ marginTop: 6,
1785
+ paddingHorizontal: 6
1786
+ },
1787
+ testID: "velocity-info-row",
1788
+ children: [
1789
+ /* @__PURE__ */ jsxs(
1790
+ reactNative.Text,
1791
+ {
1792
+ className: "text-text-secondary",
1793
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
1794
+ children: [
1795
+ meanZone,
1796
+ " ",
1797
+ "\xB7",
1798
+ " ",
1799
+ formatVelocity(meanVelocity),
1800
+ " m/s"
1801
+ ]
1802
+ }
1803
+ ),
1804
+ /* @__PURE__ */ jsxs(
1805
+ reactNative.Text,
1806
+ {
1807
+ className: "text-text-secondary",
1808
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
1809
+ children: [
1810
+ "Loss: ",
1811
+ loss,
1812
+ "%"
1813
+ ]
1814
+ }
1815
+ )
1816
+ ]
1817
+ }
1818
+ )
1819
+ ]
1820
+ }
1821
+ );
1822
+ if (onToggle) {
1823
+ return /* @__PURE__ */ jsx(
1824
+ reactNative.Pressable,
1825
+ {
1826
+ onPress: onToggle,
1827
+ accessibilityRole: "button",
1828
+ accessibilityLabel: stripLabel,
1829
+ testID: "velocity-strip-pressable",
1830
+ children: stripContent
1831
+ }
1832
+ );
1833
+ }
1834
+ return stripContent;
1835
+ }
1836
+ function cn(...inputs) {
1837
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
1838
+ }
1839
+
1840
+ // src/components/custom/Workout/PlaceholderStrip.tsx
1841
+ function SingleStrip({
1842
+ width,
1843
+ className,
1844
+ ...props
1845
+ }) {
1846
+ return /* @__PURE__ */ jsx(
1847
+ reactNative.View,
1848
+ {
1849
+ className,
1850
+ style: [
1851
+ {
1852
+ height: 3,
1853
+ backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1854
+ borderRadius: 2,
1855
+ opacity: 0.5
1856
+ },
1857
+ width != null ? { width } : { flex: 1 }
1858
+ ],
1859
+ accessibilityRole: "image",
1860
+ accessibilityLabel: "Planned set, not yet completed",
1861
+ testID: "placeholder-strip",
1862
+ ...props
1863
+ }
1864
+ );
1865
+ }
1866
+ function SegmentedStrip({
1867
+ segments = 3,
1868
+ width,
1869
+ className,
1870
+ ...props
1871
+ }) {
1872
+ return /* @__PURE__ */ jsx(
1873
+ reactNative.View,
1874
+ {
1875
+ className: cn("flex-row", className),
1876
+ style: [
1877
+ { height: 3, gap: 2, opacity: 0.5 },
1878
+ width != null ? { width } : { flex: 1 }
1879
+ ],
1880
+ accessibilityRole: "image",
1881
+ accessibilityLabel: "Planned set, not yet completed",
1882
+ testID: "placeholder-strip",
1883
+ ...props,
1884
+ children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
1885
+ reactNative.View,
1886
+ {
1887
+ style: {
1888
+ flex: 1,
1889
+ height: 3,
1890
+ backgroundColor: WORKOUT_TOKENS.placeholder.fill,
1891
+ borderRadius: 1,
1892
+ minWidth: 4
1893
+ },
1894
+ accessibilityElementsHidden: true,
1895
+ testID: "placeholder-segment"
1896
+ },
1897
+ i
1898
+ ))
1899
+ }
1900
+ );
1901
+ }
1902
+ function PlaceholderStrip({
1903
+ mode = "single",
1904
+ segments,
1905
+ ...props
1906
+ }) {
1907
+ if (mode === "segmented") {
1908
+ return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
1909
+ }
1910
+ return /* @__PURE__ */ jsx(SingleStrip, { ...props });
1911
+ }
1912
+
1913
+ // src/components/icons/SvgIcon.tsx
1914
+ function SvgIcon({
1915
+ size = 24,
1916
+ color = "currentColor",
1917
+ strokeWidth = 2,
1918
+ fill = "none",
1919
+ title,
1920
+ children
1921
+ }) {
1922
+ return /* @__PURE__ */ jsxs(
1923
+ "svg",
1924
+ {
1925
+ width: size,
1926
+ height: size,
1927
+ viewBox: "0 0 24 24",
1928
+ fill,
1929
+ stroke: color,
1930
+ strokeWidth,
1931
+ strokeLinecap: "round",
1932
+ strokeLinejoin: "round",
1933
+ ...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
1934
+ children: [
1935
+ title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
1936
+ children
1937
+ ]
1938
+ }
1939
+ );
1940
+ }
1941
+
1942
+ // src/components/icons/icons.tsx
1943
+ function StarIcon(props) {
1944
+ return /* @__PURE__ */ jsx(SvgIcon, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" }) });
1945
+ }
1946
+ function ScaleIcon(props) {
1947
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1948
+ /* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1949
+ /* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
1950
+ /* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
1951
+ /* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
1952
+ /* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
1953
+ ] });
1954
+ }
1955
+ function AlertTriangleIcon(props) {
1956
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1957
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
1958
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
1959
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
1960
+ ] });
1961
+ }
1962
+ function TrendingDownIcon(props) {
1963
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1964
+ /* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
1965
+ /* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
1966
+ ] });
1967
+ }
1968
+ function CircleSlashIcon(props) {
1969
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1970
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1971
+ /* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
1972
+ ] });
1973
+ }
1974
+ function AwardIcon(props) {
1975
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1976
+ /* @__PURE__ */ jsx("path", { d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" }),
1977
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
1978
+ ] });
1979
+ }
1980
+ function InfoIcon(props) {
1981
+ return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
1982
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
1983
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
1984
+ /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
1985
+ ] });
1690
1986
  }
1691
-
1692
- // src/components/custom/Workout/BaseBadge.tsx
1693
1987
  var t = getSemanticColors("dark");
1694
1988
  var baseBadgeSizeConfig = {
1695
1989
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
@@ -1804,565 +2098,257 @@ function PrBadge({
1804
2098
  easing: reactNative.Easing.bezier(0.34, 1.56, 0.64, 1),
1805
2099
  useNativeDriver: true
1806
2100
  })
1807
- ]).start();
1808
- }, [animate, scale, opacity]);
1809
- const badge = compact ? /* @__PURE__ */ jsx(
1810
- reactNative.View,
1811
- {
1812
- className,
1813
- accessibilityRole: "image",
1814
- accessibilityLabel: `Personal record: ${resolvedLabel}`,
1815
- testID: "pr-badge-star",
1816
- ...props,
1817
- children: /* @__PURE__ */ jsx(StarIcon, { size: 14, color: BRAND_PRIMARY, fill: BRAND_PRIMARY, strokeWidth: 2 })
1818
- }
1819
- ) : /* @__PURE__ */ jsx(
1820
- BaseBadge,
1821
- {
1822
- variant: "pr",
1823
- className,
1824
- accessibilityLabel: `Personal record: ${resolvedLabel}`,
1825
- ...props,
1826
- children: /* @__PURE__ */ jsxs(
1827
- reactNative.Text,
1828
- {
1829
- style: {
1830
- fontSize: 12,
1831
- color: BRAND_PRIMARY,
1832
- fontWeight: "700",
1833
- fontFamily: "Inter, sans-serif"
1834
- },
1835
- children: [
1836
- "\u2605",
1837
- " ",
1838
- resolvedLabel
1839
- ]
1840
- }
1841
- )
1842
- }
1843
- );
1844
- if (animate) {
1845
- return /* @__PURE__ */ jsx(
1846
- reactNative.Animated.View,
1847
- {
1848
- style: { transform: [{ scale }], opacity },
1849
- testID: "pr-badge-animated",
1850
- children: badge
1851
- }
1852
- );
1853
- }
1854
- return badge;
1855
- }
1856
- var DARK = getSemanticColors("dark");
1857
- var TEXT_ACTIVE = DARK["text-primary"];
1858
- var TEXT_MUTED = DARK["text-secondary"];
1859
- var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
1860
- var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
1861
- var typeBadge = {
1862
- fontSize: 10,
1863
- fontWeight: "700",
1864
- fontFamily: "Inter, sans-serif",
1865
- color: WORKOUT_TOKENS.scale.orange,
1866
- backgroundColor: "rgba(255,165,2,0.12)",
1867
- paddingVertical: 1,
1868
- paddingHorizontal: 5,
1869
- borderRadius: 3,
1870
- overflow: "hidden"
1871
- };
1872
- function displayReps(set) {
1873
- return set.state === "done" ? set.reps : set.target.reps;
1874
- }
1875
- function displayWeight(set) {
1876
- return set.state === "done" ? set.weight : set.target.weight;
1877
- }
1878
- function plannedReps(set) {
1879
- if (set.state === "done") return set.velocities.length;
1880
- if (set.state === "live") return set.target.reps;
1881
- return set.planned ?? set.target.reps;
1882
- }
1883
- function accessibilityLabel(set) {
1884
- const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
1885
- if (set.state === "live") return `${base}, in progress`;
1886
- if (set.state === "todo") return `${base}, upcoming`;
1887
- return base;
1888
- }
1889
- function Cell({
1890
- width,
1891
- children,
1892
- testID
1893
- }) {
1894
- return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
1895
- }
1896
- function RowStrip({ set }) {
1897
- const zones = set.velocityZones;
1898
- if (set.state === "live") {
1899
- return /* @__PURE__ */ jsx(
1900
- VelocityStrip,
1901
- {
1902
- variant: "expanded",
1903
- showNumbers: false,
1904
- showInfo: false,
1905
- height: 24,
1906
- scale: "fixed",
1907
- set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
1908
- zones
1909
- }
1910
- );
1911
- }
1912
- const velocities = set.state === "done" ? set.velocities : [];
1913
- return /* @__PURE__ */ jsx(
1914
- VelocityStrip,
1915
- {
1916
- variant: "mini",
1917
- set: { type: "straight", velocities, planned: plannedReps(set) },
1918
- zones
1919
- }
1920
- );
1921
- }
1922
- function SetRow(set) {
1923
- const live = set.state === "live";
1924
- const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
1925
- const rpe = set.state === "todo" ? null : set.rpe;
1926
- return /* @__PURE__ */ jsxs(
1927
- reactNative.View,
1928
- {
1929
- style: { paddingVertical: 6, paddingHorizontal: 8 },
1930
- accessibilityLabel: accessibilityLabel(set),
1931
- testID: "set-row",
1932
- children: [
1933
- /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
1934
- /* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
1935
- /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
1936
- /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
1937
- /* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
1938
- ] }),
1939
- /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
1940
- ]
1941
- }
1942
- );
1943
- }
1944
- var PREV_COLUMN = { label: "PREV", width: void 0 };
1945
- function buildColumns(unit, showPrevious) {
1946
- return [
1947
- { label: "SET", width: 36 },
1948
- ...showPrevious ? [PREV_COLUMN] : [],
1949
- { label: "REPS", width: 44 },
1950
- { label: unit.toUpperCase(), width: 56 },
1951
- { label: "RPE", width: 36 }
1952
- ];
1953
- }
1954
- function SetTableHeader({
1955
- unit = "lbs",
1956
- showPrevious = true,
1957
- testID = "table-header"
1958
- }) {
1959
- const columns = buildColumns(unit, showPrevious);
1960
- return /* @__PURE__ */ jsx(
1961
- reactNative.View,
1962
- {
1963
- className: "flex-row",
1964
- style: {
1965
- paddingVertical: 8,
1966
- paddingHorizontal: 8,
1967
- paddingBottom: 4,
1968
- ...showPrevious ? {} : { justifyContent: "space-between" }
1969
- },
1970
- testID,
1971
- children: columns.map(({ label, width }) => {
1972
- const isFlex = width === void 0;
1973
- return /* @__PURE__ */ jsx(
1974
- reactNative.View,
1975
- {
1976
- style: {
1977
- ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
1978
- alignItems: "center",
1979
- justifyContent: "center",
1980
- gap: 8
1981
- },
1982
- ...isFlex ? { className: "flex-1" } : {},
1983
- children: /* @__PURE__ */ jsx(
1984
- reactNative.Text,
1985
- {
1986
- className: "text-text-tertiary",
1987
- style: {
1988
- fontSize: 10,
1989
- fontWeight: "600",
1990
- fontFamily: "Inter, sans-serif",
1991
- textTransform: "uppercase",
1992
- letterSpacing: 0.8
1993
- },
1994
- children: label
1995
- }
1996
- )
1997
- },
1998
- label
1999
- );
2000
- })
2001
- }
2002
- );
2003
- }
2004
- var METRIC_FONT = "Inter, sans-serif";
2005
- function MetricCell({ children, color, fontSize = 11 }) {
2006
- return /* @__PURE__ */ jsx(
2007
- reactNative.Text,
2008
- {
2009
- style: {
2010
- fontFamily: METRIC_FONT,
2011
- fontSize,
2012
- color,
2013
- fontWeight: "600",
2014
- letterSpacing: 1
2015
- },
2016
- children
2017
- }
2018
- );
2019
- }
2020
-
2021
- // src/components/custom/Workout/SetsRepsLoad.tsx
2022
- var t2 = getSemanticColors("dark");
2023
- var TIMES = "\xD7";
2024
- var AT = "@";
2025
- function SetsRepsLoad({
2026
- sets,
2027
- reps,
2028
- load,
2029
- unit = "lb",
2030
- fontSize,
2031
- className,
2032
- ...props
2033
- }) {
2034
- const value = t2["text-primary"];
2035
- const sep = t2["text-tertiary"];
2036
- return /* @__PURE__ */ jsxs(
2101
+ ]).start();
2102
+ }, [animate, scale, opacity]);
2103
+ const badge = compact ? /* @__PURE__ */ jsx(
2037
2104
  reactNative.View,
2038
2105
  {
2039
2106
  className,
2040
- style: { flexDirection: "row", alignItems: "baseline" },
2041
- accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
2042
- testID: "sets-reps-load",
2107
+ accessibilityRole: "image",
2108
+ accessibilityLabel: `Personal record: ${resolvedLabel}`,
2109
+ testID: "pr-badge-star",
2043
2110
  ...props,
2044
- children: [
2045
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: sets }),
2046
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${TIMES} ` }),
2047
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: reps }),
2048
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
2049
- /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
2050
- /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
2051
- ]
2111
+ children: /* @__PURE__ */ jsx(StarIcon, { size: 14, color: BRAND_PRIMARY, fill: BRAND_PRIMARY, strokeWidth: 2 })
2112
+ }
2113
+ ) : /* @__PURE__ */ jsx(
2114
+ BaseBadge,
2115
+ {
2116
+ variant: "pr",
2117
+ className,
2118
+ accessibilityLabel: `Personal record: ${resolvedLabel}`,
2119
+ ...props,
2120
+ children: /* @__PURE__ */ jsxs(
2121
+ reactNative.Text,
2122
+ {
2123
+ style: {
2124
+ fontSize: 12,
2125
+ color: BRAND_PRIMARY,
2126
+ fontWeight: "700",
2127
+ fontFamily: "Inter, sans-serif"
2128
+ },
2129
+ children: [
2130
+ "\u2605",
2131
+ " ",
2132
+ resolvedLabel
2133
+ ]
2134
+ }
2135
+ )
2052
2136
  }
2053
2137
  );
2054
- }
2055
- function hexToRgb(hex) {
2056
- const cleaned = hex.replace("#", "");
2057
- let r, g, b;
2058
- if (cleaned.length === 3) {
2059
- r = parseInt(cleaned[0] + cleaned[0], 16);
2060
- g = parseInt(cleaned[1] + cleaned[1], 16);
2061
- b = parseInt(cleaned[2] + cleaned[2], 16);
2062
- } else if (cleaned.length === 6) {
2063
- r = parseInt(cleaned.slice(0, 2), 16);
2064
- g = parseInt(cleaned.slice(2, 4), 16);
2065
- b = parseInt(cleaned.slice(4, 6), 16);
2066
- } else {
2067
- return null;
2138
+ if (animate) {
2139
+ return /* @__PURE__ */ jsx(
2140
+ reactNative.Animated.View,
2141
+ {
2142
+ style: { transform: [{ scale }], opacity },
2143
+ testID: "pr-badge-animated",
2144
+ children: badge
2145
+ }
2146
+ );
2068
2147
  }
2069
- return { r, g, b };
2148
+ return badge;
2070
2149
  }
2071
- function rgbToHsv(r, g, b) {
2072
- const rNorm = r / 255;
2073
- const gNorm = g / 255;
2074
- const bNorm = b / 255;
2075
- const max = Math.max(rNorm, gNorm, bNorm);
2076
- const min = Math.min(rNorm, gNorm, bNorm);
2077
- const delta = max - min;
2078
- const v = max;
2079
- const s = max === 0 ? 0 : delta / max;
2080
- let h = 0;
2081
- if (delta !== 0) {
2082
- if (max === rNorm) {
2083
- h = 60 * ((gNorm - bNorm) / delta % 6);
2084
- } else if (max === gNorm) {
2085
- h = 60 * ((bNorm - rNorm) / delta + 2);
2086
- } else {
2087
- h = 60 * ((rNorm - gNorm) / delta + 4);
2088
- }
2089
- }
2090
- if (h < 0) h += 360;
2091
- return { h, s, v };
2150
+ var DARK = getSemanticColors("dark");
2151
+ var TEXT_ACTIVE = DARK["text-primary"];
2152
+ var TEXT_MUTED = DARK["text-secondary"];
2153
+ var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
2154
+ var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
2155
+ var typeBadge = {
2156
+ fontSize: 10,
2157
+ fontWeight: "700",
2158
+ fontFamily: "Inter, sans-serif",
2159
+ color: WORKOUT_TOKENS.scale.orange,
2160
+ backgroundColor: "rgba(255,165,2,0.12)",
2161
+ paddingVertical: 1,
2162
+ paddingHorizontal: 5,
2163
+ borderRadius: 3,
2164
+ overflow: "hidden"
2165
+ };
2166
+ function displayReps(set) {
2167
+ return set.state === "done" ? set.reps : set.target.reps;
2092
2168
  }
2093
- function hsvToRgb(h, s, v) {
2094
- const c = v * s;
2095
- const x = c * (1 - Math.abs(h / 60 % 2 - 1));
2096
- const m = v - c;
2097
- let rPrime, gPrime, bPrime;
2098
- if (h >= 0 && h < 60) {
2099
- rPrime = c;
2100
- gPrime = x;
2101
- bPrime = 0;
2102
- } else if (h >= 60 && h < 120) {
2103
- rPrime = x;
2104
- gPrime = c;
2105
- bPrime = 0;
2106
- } else if (h >= 120 && h < 180) {
2107
- rPrime = 0;
2108
- gPrime = c;
2109
- bPrime = x;
2110
- } else if (h >= 180 && h < 240) {
2111
- rPrime = 0;
2112
- gPrime = x;
2113
- bPrime = c;
2114
- } else if (h >= 240 && h < 300) {
2115
- rPrime = x;
2116
- gPrime = 0;
2117
- bPrime = c;
2118
- } else {
2119
- rPrime = c;
2120
- gPrime = 0;
2121
- bPrime = x;
2122
- }
2123
- return {
2124
- r: Math.round((rPrime + m) * 255),
2125
- g: Math.round((gPrime + m) * 255),
2126
- b: Math.round((bPrime + m) * 255)
2127
- };
2169
+ function displayWeight(set) {
2170
+ return set.state === "done" ? set.weight : set.target.weight;
2128
2171
  }
2129
- function rgbToHex(r, g, b) {
2130
- const toHex = (n) => {
2131
- const clamped = Math.max(0, Math.min(255, Math.round(n)));
2132
- return clamped.toString(16).padStart(2, "0");
2133
- };
2134
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2172
+ function plannedReps(set) {
2173
+ if (set.state === "done") return set.velocities.length;
2174
+ if (set.state === "live") return set.target.reps;
2175
+ return set.planned ?? set.target.reps;
2135
2176
  }
2136
- function lighten(hex, amount = 0.1) {
2137
- const rgb = hexToRgb(hex);
2138
- if (!rgb) return hex;
2139
- const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
2140
- const newV = Math.min(1, hsv.v + amount);
2141
- const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
2142
- return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
2177
+ function accessibilityLabel(set) {
2178
+ const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
2179
+ if (set.state === "live") return `${base}, in progress`;
2180
+ if (set.state === "todo") return `${base}, upcoming`;
2181
+ return base;
2143
2182
  }
2144
- var shadowColors = {
2145
- // For charcoal surfaces (~#2C2C2C to #3C3C3C)
2146
- charcoal: {
2147
- dark: "rgba(0, 0, 0, 0.5)",
2148
- light: "rgba(255, 255, 255, 0.12)",
2149
- // brighter for crisp rim light
2150
- darker: "rgba(0, 0, 0, 0.6)"
2151
- },
2152
- // For neutral dark surfaces (~#1F2937)
2153
- neutralDark: {
2154
- dark: "rgba(0, 0, 0, 0.4)",
2155
- light: "rgba(255, 255, 255, 0.08)",
2156
- darker: "rgba(0, 0, 0, 0.5)"
2157
- },
2158
- // For light surfaces (white/light gray backgrounds)
2159
- neutralLight: {
2160
- dark: "rgba(0, 0, 0, 0.15)",
2161
- // Visible dark shadow for depth on light backgrounds
2162
- light: "rgba(255, 255, 255, 1)",
2163
- // Bright highlight for raised effect (visible on light gray)
2164
- darker: "rgba(0, 0, 0, 0.2)"
2165
- // Deeper shadow for pressed states
2166
- }
2167
- };
2168
- var intensityConfig = {
2169
- subtle: { offset: 1, blur: 2 },
2170
- // very light, barely visible
2171
- medium: { offset: 2, blur: 4 },
2172
- // noticeable but restrained
2173
- strong: { offset: 2, blur: 5 }
2174
- // slightly deeper blur, same offset as medium
2175
- };
2176
- function createRaisedShadow(surface = "charcoal", intensity = "medium") {
2177
- const colors = shadowColors[surface];
2178
- const { offset, blur } = intensityConfig[intensity];
2179
- const rimBlur = 1;
2180
- return reactNative.Platform.select({
2181
- web: {
2182
- boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2183
- },
2184
- default: {
2185
- // React Native shadow approximation (limited compared to web)
2186
- shadowColor: "#000",
2187
- shadowOffset: { width: offset, height: offset },
2188
- shadowOpacity: 0.3,
2189
- shadowRadius: blur / 2,
2190
- elevation: offset
2191
- }
2192
- });
2183
+ function Cell({
2184
+ width,
2185
+ children,
2186
+ testID
2187
+ }) {
2188
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
2193
2189
  }
2194
- function createPressedShadow(surface = "charcoal", intensity = "medium") {
2195
- const colors = shadowColors[surface];
2196
- const { offset, blur } = intensityConfig[intensity];
2197
- const rimOffset = 1;
2198
- const rimBlur = 1;
2199
- return reactNative.Platform.select({
2200
- web: {
2201
- boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
2202
- },
2203
- default: {
2204
- // React Native doesn't support inset shadows, approximate with darker bg
2205
- shadowOpacity: 0,
2206
- elevation: 0
2190
+ function RowStrip({ set }) {
2191
+ const zones = set.velocityZones;
2192
+ if (set.state === "live") {
2193
+ return /* @__PURE__ */ jsx(
2194
+ VelocityStrip,
2195
+ {
2196
+ variant: "expanded",
2197
+ showNumbers: false,
2198
+ showInfo: false,
2199
+ height: 24,
2200
+ scale: "fixed",
2201
+ liveRepIndex: set.velocities.length - 1,
2202
+ set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
2203
+ zones
2204
+ }
2205
+ );
2206
+ }
2207
+ const velocities = set.state === "done" ? set.velocities : [];
2208
+ return /* @__PURE__ */ jsx(
2209
+ VelocityStrip,
2210
+ {
2211
+ variant: "compact",
2212
+ height: 8,
2213
+ hideBaseline: true,
2214
+ set: { type: "straight", velocities, planned: plannedReps(set) },
2215
+ zones
2207
2216
  }
2208
- });
2217
+ );
2209
2218
  }
2210
- function createFlatShadow() {
2211
- return reactNative.Platform.select({
2212
- web: {
2213
- boxShadow: "none"
2214
- },
2215
- default: {
2216
- shadowOpacity: 0,
2217
- elevation: 0
2219
+ function SetRow(set) {
2220
+ const live = set.state === "live";
2221
+ const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
2222
+ const rpe = set.state === "todo" ? null : set.rpe;
2223
+ return /* @__PURE__ */ jsxs(
2224
+ reactNative.View,
2225
+ {
2226
+ style: { paddingVertical: 6, paddingHorizontal: 8 },
2227
+ accessibilityLabel: accessibilityLabel(set),
2228
+ testID: "set-row",
2229
+ children: [
2230
+ /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
2231
+ /* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
2232
+ /* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
2233
+ /* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
2234
+ /* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
2235
+ ] }),
2236
+ /* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
2237
+ ]
2218
2238
  }
2219
- });
2239
+ );
2220
2240
  }
2221
- function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
2222
- const colors = shadowColors[surface];
2223
- const { offset, blur } = intensityConfig[intensity];
2224
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2225
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2226
- const rimBlur = 1;
2227
- return reactNative.Platform.select({
2228
- web: {
2229
- // Keep same offsets as normal state for rim to prevent "shrinking" effect
2230
- boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
2231
- },
2232
- default: {
2233
- shadowColor: "#000",
2234
- shadowOffset: { width: hoverOffset, height: hoverOffset },
2235
- shadowOpacity: 0.2,
2236
- shadowRadius: hoverBlur / 2,
2237
- elevation: hoverOffset
2241
+ var PREV_COLUMN = { label: "PREV", width: void 0 };
2242
+ function buildColumns(unit, showPrevious) {
2243
+ return [
2244
+ { label: "SET", width: 36 },
2245
+ ...showPrevious ? [PREV_COLUMN] : [],
2246
+ { label: "REPS", width: 44 },
2247
+ { label: unit.toUpperCase(), width: 56 },
2248
+ { label: "RPE", width: 36 }
2249
+ ];
2250
+ }
2251
+ function SetTableHeader({
2252
+ unit = "lbs",
2253
+ showPrevious = true,
2254
+ testID = "table-header"
2255
+ }) {
2256
+ const columns = buildColumns(unit, showPrevious);
2257
+ return /* @__PURE__ */ jsx(
2258
+ reactNative.View,
2259
+ {
2260
+ className: "flex-row",
2261
+ style: {
2262
+ paddingVertical: 8,
2263
+ paddingHorizontal: 8,
2264
+ paddingBottom: 4,
2265
+ ...showPrevious ? {} : { justifyContent: "space-between" }
2266
+ },
2267
+ testID,
2268
+ children: columns.map(({ label, width }) => {
2269
+ const isFlex = width === void 0;
2270
+ return /* @__PURE__ */ jsx(
2271
+ reactNative.View,
2272
+ {
2273
+ style: {
2274
+ ...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
2275
+ alignItems: "center",
2276
+ justifyContent: "center",
2277
+ gap: 8
2278
+ },
2279
+ ...isFlex ? { className: "flex-1" } : {},
2280
+ children: /* @__PURE__ */ jsx(
2281
+ reactNative.Text,
2282
+ {
2283
+ className: "text-text-tertiary",
2284
+ style: {
2285
+ fontSize: 10,
2286
+ fontWeight: "600",
2287
+ fontFamily: "Inter, sans-serif",
2288
+ textTransform: "uppercase",
2289
+ letterSpacing: 0.8
2290
+ },
2291
+ children: label
2292
+ }
2293
+ )
2294
+ },
2295
+ label
2296
+ );
2297
+ })
2238
2298
  }
2239
- });
2299
+ );
2240
2300
  }
2241
- function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
2242
- const colors = shadowColors[surface];
2243
- const { offset, blur } = intensityConfig[intensity];
2244
- const hoverOffset = Math.max(1, Math.round(offset / 2));
2245
- const hoverBlur = Math.max(1, Math.round(blur / 2));
2246
- const rimBlur = 1;
2247
- return reactNative.Platform.select({
2248
- web: {
2249
- // Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
2250
- boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
2251
- },
2252
- default: {
2253
- shadowOpacity: 0,
2254
- elevation: 0
2301
+ var METRIC_FONT = "Inter, sans-serif";
2302
+ function MetricCell({ children, color, fontSize = 11 }) {
2303
+ return /* @__PURE__ */ jsx(
2304
+ reactNative.Text,
2305
+ {
2306
+ style: {
2307
+ fontFamily: METRIC_FONT,
2308
+ fontSize,
2309
+ color,
2310
+ fontWeight: "600",
2311
+ letterSpacing: 1
2312
+ },
2313
+ children
2255
2314
  }
2256
- });
2315
+ );
2257
2316
  }
2258
- ({
2259
- // Charcoal surface shadows (for dark toolbars, cards)
2260
- charcoal: {
2261
- raised: {
2262
- subtle: createRaisedShadow("charcoal", "subtle"),
2263
- medium: createRaisedShadow("charcoal", "medium"),
2264
- strong: createRaisedShadow("charcoal", "strong")
2265
- },
2266
- raisedHover: {
2267
- subtle: createRaisedHoverShadow("charcoal", "subtle"),
2268
- medium: createRaisedHoverShadow("charcoal", "medium"),
2269
- strong: createRaisedHoverShadow("charcoal", "strong")
2270
- },
2271
- pressed: {
2272
- subtle: createPressedShadow("charcoal", "subtle"),
2273
- medium: createPressedShadow("charcoal", "medium"),
2274
- strong: createPressedShadow("charcoal", "strong")
2275
- },
2276
- pressedHover: {
2277
- subtle: createPressedHoverShadow("charcoal", "subtle"),
2278
- medium: createPressedHoverShadow("charcoal", "medium"),
2279
- strong: createPressedHoverShadow("charcoal", "strong")
2280
- },
2281
- flat: createFlatShadow()
2282
- },
2283
- // Neutral dark surface shadows
2284
- neutralDark: {
2285
- raised: {
2286
- subtle: createRaisedShadow("neutralDark", "subtle"),
2287
- medium: createRaisedShadow("neutralDark", "medium"),
2288
- strong: createRaisedShadow("neutralDark", "strong")
2289
- },
2290
- raisedHover: {
2291
- subtle: createRaisedHoverShadow("neutralDark", "subtle"),
2292
- medium: createRaisedHoverShadow("neutralDark", "medium"),
2293
- strong: createRaisedHoverShadow("neutralDark", "strong")
2294
- },
2295
- pressed: {
2296
- subtle: createPressedShadow("neutralDark", "subtle"),
2297
- medium: createPressedShadow("neutralDark", "medium"),
2298
- strong: createPressedShadow("neutralDark", "strong")
2299
- },
2300
- pressedHover: {
2301
- subtle: createPressedHoverShadow("neutralDark", "subtle"),
2302
- medium: createPressedHoverShadow("neutralDark", "medium"),
2303
- strong: createPressedHoverShadow("neutralDark", "strong")
2304
- },
2305
- flat: createFlatShadow()
2306
- },
2307
- // Neutral light surface shadows
2308
- neutralLight: {
2309
- raised: {
2310
- subtle: createRaisedShadow("neutralLight", "subtle"),
2311
- medium: createRaisedShadow("neutralLight", "medium"),
2312
- strong: createRaisedShadow("neutralLight", "strong")
2313
- },
2314
- raisedHover: {
2315
- subtle: createRaisedHoverShadow("neutralLight", "subtle"),
2316
- medium: createRaisedHoverShadow("neutralLight", "medium"),
2317
- strong: createRaisedHoverShadow("neutralLight", "strong")
2318
- },
2319
- pressed: {
2320
- subtle: createPressedShadow("neutralLight", "subtle"),
2321
- medium: createPressedShadow("neutralLight", "medium"),
2322
- strong: createPressedShadow("neutralLight", "strong")
2323
- },
2324
- pressedHover: {
2325
- subtle: createPressedHoverShadow("neutralLight", "subtle"),
2326
- medium: createPressedHoverShadow("neutralLight", "medium"),
2327
- strong: createPressedHoverShadow("neutralLight", "strong")
2328
- },
2329
- flat: createFlatShadow()
2330
- }
2331
- });
2332
2317
 
2333
- // src/utils/colors.ts
2334
- function alpha(color, opacity) {
2335
- const clampedOpacity = Math.max(0, Math.min(1, opacity));
2336
- if (color.startsWith("#")) {
2337
- const hex = color.slice(1);
2338
- let r, g, b;
2339
- if (hex.length === 3) {
2340
- r = parseInt(hex[0] + hex[0], 16);
2341
- g = parseInt(hex[1] + hex[1], 16);
2342
- b = parseInt(hex[2] + hex[2], 16);
2343
- } else if (hex.length === 6) {
2344
- r = parseInt(hex.slice(0, 2), 16);
2345
- g = parseInt(hex.slice(2, 4), 16);
2346
- b = parseInt(hex.slice(4, 6), 16);
2347
- } else {
2348
- return color;
2318
+ // src/components/custom/Workout/SetsRepsLoad.tsx
2319
+ var t2 = getSemanticColors("dark");
2320
+ var TIMES = "\xD7";
2321
+ var AT = "@";
2322
+ function SetsRepsLoad({
2323
+ sets,
2324
+ reps,
2325
+ load,
2326
+ unit = "lb",
2327
+ fontSize,
2328
+ className,
2329
+ ...props
2330
+ }) {
2331
+ const value = t2["text-primary"];
2332
+ const sep = t2["text-tertiary"];
2333
+ return /* @__PURE__ */ jsxs(
2334
+ reactNative.View,
2335
+ {
2336
+ className,
2337
+ style: { flexDirection: "row", alignItems: "baseline" },
2338
+ accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
2339
+ testID: "sets-reps-load",
2340
+ ...props,
2341
+ children: [
2342
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: sets }),
2343
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${TIMES} ` }),
2344
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: reps }),
2345
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
2346
+ /* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
2347
+ /* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
2348
+ ]
2349
2349
  }
2350
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2351
- }
2352
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
2353
- if (rgbMatch) {
2354
- const [, r, g, b] = rgbMatch;
2355
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2356
- }
2357
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
2358
- if (rgbaMatch) {
2359
- const [, r, g, b] = rgbaMatch;
2360
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
2361
- }
2362
- return color;
2350
+ );
2363
2351
  }
2364
-
2365
- // src/components/custom/Workout/TempoDisplay.tsx
2366
2352
  var t3 = getSemanticColors("dark");
2367
2353
  var INTER = "Inter, sans-serif";
2368
2354
  var TEXT_TERTIARY = "#6B7280";
@@ -2746,6 +2732,243 @@ function ExerciseHeading({
2746
2732
  )
2747
2733
  ] });
2748
2734
  }
2735
+ var PULSE_HALF_MS = 950;
2736
+ var PULSE_MIN_OPACITY = 0.45;
2737
+ var SEGMENTED_BAR_GAP = 5;
2738
+ function usePulse(active) {
2739
+ const [value] = react.useState(() => new reactNative.Animated.Value(0));
2740
+ react.useEffect(() => {
2741
+ if (!active) return;
2742
+ const loop = reactNative.Animated.loop(
2743
+ reactNative.Animated.sequence([
2744
+ reactNative.Animated.timing(value, {
2745
+ toValue: 1,
2746
+ duration: PULSE_HALF_MS,
2747
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2748
+ useNativeDriver: false
2749
+ }),
2750
+ reactNative.Animated.timing(value, {
2751
+ toValue: 0,
2752
+ duration: PULSE_HALF_MS,
2753
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2754
+ useNativeDriver: false
2755
+ })
2756
+ ])
2757
+ );
2758
+ loop.start();
2759
+ return () => loop.stop();
2760
+ }, [active, value]);
2761
+ return value;
2762
+ }
2763
+ function SegmentedBar({
2764
+ segments,
2765
+ height = 8,
2766
+ gap = SEGMENTED_BAR_GAP,
2767
+ radius = 2,
2768
+ marker = null,
2769
+ segmentTestID,
2770
+ style,
2771
+ ...props
2772
+ }) {
2773
+ const pulse = usePulse(segments.some((s) => s.pulse));
2774
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
2775
+ segments.map((seg, i) => {
2776
+ const fillStyle = {
2777
+ width: `${(seg.fill ?? 1) * 100}%`,
2778
+ height: "100%",
2779
+ backgroundColor: seg.color,
2780
+ opacity: seg.opacity
2781
+ };
2782
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
2783
+ return /* @__PURE__ */ jsx(
2784
+ reactNative.View,
2785
+ {
2786
+ style: {
2787
+ flex: seg.weight ?? 1,
2788
+ minWidth: 0,
2789
+ height: "100%",
2790
+ marginLeft: seg.leadingGap,
2791
+ borderRadius: radius,
2792
+ overflow: "hidden"
2793
+ },
2794
+ children: seg.pulse ? /* @__PURE__ */ jsx(
2795
+ reactNative.Animated.View,
2796
+ {
2797
+ style: {
2798
+ ...fillStyle,
2799
+ ...seg.pulseColor ? {
2800
+ backgroundColor: pulse.interpolate({
2801
+ inputRange: [0, 1],
2802
+ outputRange: seg.pulseColor
2803
+ })
2804
+ } : {
2805
+ opacity: pulse.interpolate({
2806
+ inputRange: [0, 1],
2807
+ outputRange: [PULSE_MIN_OPACITY, 1]
2808
+ })
2809
+ }
2810
+ },
2811
+ accessibilityElementsHidden: true,
2812
+ testID
2813
+ }
2814
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
2815
+ },
2816
+ i
2817
+ );
2818
+ }),
2819
+ marker ? /* @__PURE__ */ jsx(
2820
+ reactNative.View,
2821
+ {
2822
+ style: {
2823
+ position: "absolute",
2824
+ left: `${marker.position * 100}%`,
2825
+ top: 0,
2826
+ bottom: 0,
2827
+ width: 2,
2828
+ backgroundColor: marker.color
2829
+ },
2830
+ accessibilityElementsHidden: true,
2831
+ testID: "segmented-bar-marker"
2832
+ }
2833
+ ) : null
2834
+ ] });
2835
+ }
2836
+
2837
+ // src/components/custom/Workout/SetBar.tsx
2838
+ var SET_STRIP_ZONES = {
2839
+ slow: primitiveRamps.red[600],
2840
+ moderate: primitiveRamps.orange[400],
2841
+ fast: primitiveRamps.amber[300],
2842
+ fastest: primitiveRamps.green[300]
2843
+ };
2844
+ var TODO_COLOR = primitiveColors.charcoal[300];
2845
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
2846
+ var NOTCH_GAP = 2;
2847
+ var CLUSTER_GAP = 3;
2848
+ var MYO_UPCOMING_CLUSTERS = 3;
2849
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
2850
+ var MYO_UPCOMING_FADE = 0.6;
2851
+ var PULSE_RANGE = {
2852
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
2853
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
2854
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
2855
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
2856
+ };
2857
+ function velocityZoneColor(v) {
2858
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
2859
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
2860
+ if (v < 1) return SET_STRIP_ZONES.fast;
2861
+ return SET_STRIP_ZONES.fastest;
2862
+ }
2863
+ function chunkedSegments(chunks, gap) {
2864
+ return chunks.flatMap(
2865
+ (vels, ci) => vels.map((v, ri) => ({
2866
+ color: velocityZoneColor(v),
2867
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
2868
+ }))
2869
+ );
2870
+ }
2871
+ function rangeSegments(floor, max, doneVels) {
2872
+ return Array.from({ length: max }, (_, i) => {
2873
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
2874
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
2875
+ });
2876
+ }
2877
+ function myoUpcomingSegments(activationLen, clusterCount) {
2878
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
2879
+ const trail = Array.from(
2880
+ { length: clusterCount },
2881
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
2882
+ color: SET_STRIP_VARIABLE_COLOR,
2883
+ opacity: MYO_UPCOMING_FADE ** ci,
2884
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
2885
+ }))
2886
+ ).flat();
2887
+ return [...activation, ...trail];
2888
+ }
2889
+ function setSegments(set) {
2890
+ if (set.status === "todo") {
2891
+ return [{ color: TODO_COLOR }];
2892
+ }
2893
+ if (set.status === "done") {
2894
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
2895
+ }
2896
+ if (set.status === "range") {
2897
+ return rangeSegments(set.floor, set.max, set.doneVels);
2898
+ }
2899
+ if (set.status === "drop") {
2900
+ return chunkedSegments(set.subloads, NOTCH_GAP);
2901
+ }
2902
+ if (set.status === "myo") {
2903
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
2904
+ }
2905
+ if (set.status === "myo-upcoming") {
2906
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
2907
+ }
2908
+ const performed = set.velocities.map((v) => {
2909
+ const color = velocityZoneColor(v);
2910
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
2911
+ });
2912
+ const remaining = Math.max(0, set.planned - set.velocities.length);
2913
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
2914
+ }
2915
+ function setBarSegmentTestID(seg) {
2916
+ if (seg.pulse) return "set-strip-pulse";
2917
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
2918
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
2919
+ }
2920
+ function SetBar({ set, height = 8 }) {
2921
+ return /* @__PURE__ */ jsx(
2922
+ SegmentedBar,
2923
+ {
2924
+ segments: setSegments(set),
2925
+ height,
2926
+ gap: 0,
2927
+ radius: 0,
2928
+ segmentTestID: setBarSegmentTestID,
2929
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
2930
+ testID: "set-strip-set"
2931
+ }
2932
+ );
2933
+ }
2934
+
2935
+ // src/components/custom/Workout/SetStrip.tsx
2936
+ var SET_STRIP_GAP = 5;
2937
+ function setCategory(set) {
2938
+ switch (set.status) {
2939
+ case "done":
2940
+ case "drop":
2941
+ case "myo":
2942
+ return "done";
2943
+ case "active":
2944
+ return "active";
2945
+ case "range":
2946
+ return set.doneVels.length > 0 ? "active" : "upcoming";
2947
+ case "todo":
2948
+ case "myo-upcoming":
2949
+ return "upcoming";
2950
+ }
2951
+ }
2952
+ function describeSets(sets) {
2953
+ const done = sets.filter((s) => setCategory(s) === "done").length;
2954
+ const active = sets.filter((s) => setCategory(s) === "active").length;
2955
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
2956
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
2957
+ }
2958
+ function SetStrip({ sets, height = 8, className, ...props }) {
2959
+ return /* @__PURE__ */ jsx(
2960
+ reactNative.View,
2961
+ {
2962
+ className,
2963
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
2964
+ accessibilityRole: "image",
2965
+ accessibilityLabel: describeSets(sets),
2966
+ testID: "set-strip",
2967
+ ...props,
2968
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
2969
+ }
2970
+ );
2971
+ }
2749
2972
 
2750
2973
  // src/components/custom/Workout/ExerciseCardHeading.tsx
2751
2974
  function ExerciseCardHeading({
@@ -2888,7 +3111,9 @@ function CollapsedCard({
2888
3111
  {
2889
3112
  velocities,
2890
3113
  zones: velocityZones,
2891
- variant: "mini",
3114
+ variant: "compact",
3115
+ height: 8,
3116
+ hideBaseline: true,
2892
3117
  testID: `exercise-card-velocity-strip-${i}`
2893
3118
  },
2894
3119
  i
@@ -6057,7 +6282,16 @@ function VbtBreakdown({
6057
6282
  children: set.label
6058
6283
  }
6059
6284
  ),
6060
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(VelocityStrip, { velocities: set.velocities, zones, variant: "mini" }) }),
6285
+ /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(
6286
+ VelocityStrip,
6287
+ {
6288
+ velocities: set.velocities,
6289
+ zones,
6290
+ variant: "compact",
6291
+ height: 8,
6292
+ hideBaseline: true
6293
+ }
6294
+ ) }),
6061
6295
  /* @__PURE__ */ jsx(
6062
6296
  reactNative.Text,
6063
6297
  {