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