@titan-design/react-ui 0.9.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/bodymap.js +78 -24
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +78 -24
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +429 -11
  6. package/dist/index.d.ts +429 -11
  7. package/dist/index.js +2386 -885
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2306 -836
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1900 -1609
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1901 -1610
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +129 -34
  20. package/dist/theme/index.d.ts +129 -34
  21. package/dist/theme/index.js +95 -19
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +92 -20
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +88 -24
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +88 -24
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  31. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  32. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  33. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  34. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  35. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  36. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  37. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  38. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  39. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  40. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  41. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  42. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  43. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  45. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  46. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  47. package/src/components/custom/Fatigue/README.md +93 -0
  48. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  49. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  51. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  52. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  53. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  54. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  55. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  56. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  57. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  58. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  59. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  60. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  61. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  62. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  63. package/src/components/custom/Fatigue/index.ts +41 -0
  64. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  65. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  66. package/src/components/custom/Workout/BaseBadge.tsx +5 -5
  67. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  68. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  69. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  71. package/src/components/custom/Workout/README.md +62 -16
  72. package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
  73. package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
  74. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  75. package/src/components/custom/Workout/SessionHeader.tsx +2 -2
  76. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  77. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  78. package/src/components/custom/Workout/SessionRail.tsx +55 -18
  79. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  80. package/src/components/custom/Workout/SetRow.tsx +9 -2
  81. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
  82. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
  83. package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
  84. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  85. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  86. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  87. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  88. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  89. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  90. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  91. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
  92. package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
  93. package/src/components/custom/Workout/index.ts +3 -0
  94. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  95. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  96. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  97. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  98. package/src/components/custom/index.ts +1 -0
  99. package/src/components/ui/surface/Surface.stories.tsx +195 -1
  100. package/src/components/ui/surface/Surface.test.tsx +148 -9
  101. package/src/components/ui/surface/Surface.tsx +37 -9
  102. package/src/components/ui/surface/SurfaceContext.ts +48 -8
  103. package/src/components/ui/surface/index.ts +1 -0
  104. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  105. package/src/test/raw-color-patterns.test.ts +71 -0
  106. package/src/theme/ColorPalettes.stories.tsx +871 -0
  107. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  108. package/src/theme/bar-paper.ts +59 -0
  109. package/src/theme/color-stories.coverage.test.ts +130 -0
  110. package/src/theme/color-story-kit.tsx +170 -0
  111. package/src/theme/config.ts +12 -0
  112. package/src/theme/elevation.ts +90 -67
  113. package/src/theme/global.css +35 -9
  114. package/src/theme/tokens/primitives.ts +40 -0
  115. package/src/theme/tokens/semantic.ts +63 -14
  116. package/tailwind.config.js +8 -0
  117. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  118. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  119. package/src/theme/ColorSystem.stories.tsx +0 -323
  120. package/src/theme/Colors.stories.tsx +0 -323
package/dist/index.js CHANGED
@@ -339,6 +339,21 @@ var primitiveColors = {
339
339
  black: "#000000",
340
340
  transparent: "transparent"
341
341
  };
342
+ var surfaceRampDark = {
343
+ inset: "#13100D",
344
+ // L*4.5 — sub-shell well / pressed
345
+ background: "#1C1916",
346
+ // L*9 — shell / frame
347
+ base: "#252321",
348
+ // L*13.5 — main content plane
349
+ elevated: "#2C2A28",
350
+ // L*17 — nav / rail
351
+ raised: "#31302F",
352
+ // L*20 — cards
353
+ overlay: "#373635"
354
+ // L*22.5 — hero / popover
355
+ };
356
+ var backgroundFrameDark = "#100D0A";
342
357
  var primitiveRamps = {
343
358
  red: {
344
359
  50: "#FFF4F4",
@@ -806,10 +821,19 @@ var semanticColorsLight = {
806
821
  "surface-overlay": primitiveColors.white,
807
822
  "surface-input": primitiveColors.neutral[50],
808
823
  // Input field background (filled variant)
824
+ // Deepest pressed pit (TD-surface-tokens, S-3) — light-mode counterpart of
825
+ // the dark ramp's `inset` plane; a placeholder pairing, not part of the
826
+ // dark-ramp investigation (surface.contract.test.ts is dark-mode only).
827
+ "surface-inset": primitiveColors.neutral[300],
809
828
  // Background colors (background-*)
810
829
  "background-base": "#EBEBEB",
811
830
  "background-default": primitiveColors.white,
812
831
  "background-subtle": primitiveColors.neutral[50],
832
+ // Frame/bezel chrome (TD-surface-tokens, S-3) — top bar + side nav shell,
833
+ // one step below `background-base` (darker still than `surface-inset`
834
+ // above, mirroring the dark-mode ordering). Placeholder pairing for light
835
+ // mode (see dark-mode note on `surface-inset` above).
836
+ "background-frame": primitiveColors.neutral[400],
813
837
  // Border colors (border-*)
814
838
  "border-default": "#E8E9EB",
815
839
  "border-subtle": primitiveColors.neutral[100],
@@ -825,6 +849,11 @@ var semanticColorsLight = {
825
849
  // Input field border on focus
826
850
  "border-input-error": primitiveColors.red[600],
827
851
  // Input field border on error
852
+ // Alpha hairline separators (surface-independent — composite toward black on
853
+ // light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
854
+ "hairline-subtle": "rgba(0, 0, 0, 0.06)",
855
+ "hairline-default": "rgba(0, 0, 0, 0.09)",
856
+ "hairline-strong": "rgba(0, 0, 0, 0.14)",
828
857
  // Interactive states (interactive-*)
829
858
  "interactive-hover": "rgba(55, 65, 81, 0.04)",
830
859
  "interactive-focus": "rgba(55, 65, 81, 0.12)",
@@ -917,25 +946,46 @@ var semanticColorsDark = {
917
946
  "text-inverse": primitiveColors.neutral[900],
918
947
  "text-link": "#828DF8",
919
948
  "text-link-hover": primitiveColors.blue[400],
920
- // Surface colors - dark backgrounds
921
- "surface-base": primitiveColors.charcoal[700],
922
- // main surface
923
- "surface-elevated": primitiveColors.charcoal[600],
924
- // elevated surface
925
- "surface-raised": primitiveColors.charcoal[500],
926
- // raised surface
927
- "surface-overlay": primitiveColors.charcoal[600],
928
- // overlay surface
929
- "surface-input": primitiveColors.charcoal[600],
930
- // input surface
931
- // Background colors
932
- "background-base": primitiveColors.charcoal[900],
933
- // darkest charcoal
934
- "background-default": primitiveColors.charcoal[700],
935
- // main background
936
- "background-subtle": primitiveColors.charcoal[500],
937
- // subtle background
938
- // Border colors
949
+ // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
950
+ // S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
951
+ // `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
952
+ // and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
953
+ // tracks one plane above `surface-base`, same relative position as before the remap.
954
+ // `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
955
+ // so it takes the ramp's `background` (shell/frame) role, NOT `inset`. The ramp's
956
+ // deepest `inset` plane is now promoted to `surface-inset` below (S-3) — still no
957
+ // `SurfaceLevel` member for it (follow-up, alongside the elevation.ts -1/-2 pressed
958
+ // levels — parallel workstream, not this branch).
959
+ "surface-base": surfaceRampDark.base,
960
+ // main surface (#252321, L*13.5)
961
+ "surface-elevated": surfaceRampDark.elevated,
962
+ // elevated surface (#2C2A28, L*17 — nav/rail)
963
+ "surface-raised": surfaceRampDark.raised,
964
+ // raised surface (#31302F, L*20 — cards)
965
+ "surface-overlay": surfaceRampDark.overlay,
966
+ // overlay surface (#373635, L*22.5 — hero/popover)
967
+ "surface-input": surfaceRampDark.elevated,
968
+ // input surface (#2C2A28 — one plane above base)
969
+ "surface-inset": surfaceRampDark.inset,
970
+ // deepest pressed pit (#13100D, L*4.5 — sub-shell well)
971
+ // Background colors — same ramp, the frame/shell end of it.
972
+ "background-base": surfaceRampDark.background,
973
+ // shell / frame (#1C1916, L*9 — Surface level="background")
974
+ "background-default": surfaceRampDark.base,
975
+ // main background (#252321, L*13.5 — matches surface-base)
976
+ "background-subtle": surfaceRampDark.elevated,
977
+ // subtle background (#2C2A28, L*17 — matches surface-elevated)
978
+ // Frame/bezel chrome (S-3) — the top bar + side nav shell; one step BELOW
979
+ // `background-base`, darker even than `surface-inset` — it exits the content
980
+ // ramp entirely rather than sitting in it. See `backgroundFrameDark` in
981
+ // primitives.ts.
982
+ "background-frame": backgroundFrameDark,
983
+ // frame / bezel (#100D0A, L*3.79)
984
+ // Border colors — `border-subtle` (#1C1C1C) previously collided with
985
+ // `surface-raised` (also #1C1C1C, an invisible border on raised cards). The
986
+ // re-spaced ramp above moved `surface-raised` to #2D2C2B, so this value is now
987
+ // distinct from every surface hex without needing to change it — see
988
+ // `surface.contract.test.ts` R2/R4.
939
989
  "border-default": primitiveColors.charcoal[400],
940
990
  // default border
941
991
  "border-subtle": primitiveColors.charcoal[500],
@@ -949,6 +999,14 @@ var semanticColorsDark = {
949
999
  "border-input-hover": primitiveColors.neutral[500],
950
1000
  "border-input-focus": "#828DF8",
951
1001
  "border-input-error": primitiveColors.red[500],
1002
+ // Alpha hairline separators — the primary separation cue (§4/S-2). Self-
1003
+ // normalizing: composites toward white by ~the same amount on ANY plane, so
1004
+ // one family works at every elevation instead of per-surface border tokens.
1005
+ // Shadows are demoted to floating-overlay use only (not shipped as a fill
1006
+ // separator here — see elevation.ts, follow-up S-4).
1007
+ "hairline-subtle": "rgba(255, 255, 255, 0.06)",
1008
+ "hairline-default": "rgba(255, 255, 255, 0.09)",
1009
+ "hairline-strong": "rgba(255, 255, 255, 0.14)",
952
1010
  // Interactive states
953
1011
  "interactive-hover": "rgba(255, 255, 255, 0.04)",
954
1012
  "interactive-focus": "rgba(255, 255, 255, 0.12)",
@@ -1478,9 +1536,11 @@ var lightThemeCSSVars = {
1478
1536
  "--color-surface-base": semanticColorsLight["surface-base"],
1479
1537
  "--color-surface-elevated": semanticColorsLight["surface-elevated"],
1480
1538
  "--color-surface-raised": semanticColorsLight["surface-raised"],
1539
+ "--color-surface-inset": semanticColorsLight["surface-inset"],
1481
1540
  "--color-background-base": semanticColorsLight["background-base"],
1482
1541
  "--color-background-default": semanticColorsLight["background-default"],
1483
1542
  "--color-background-subtle": semanticColorsLight["background-subtle"],
1543
+ "--color-background-frame": semanticColorsLight["background-frame"],
1484
1544
  "--color-border-default": semanticColorsLight["border-default"],
1485
1545
  "--color-border-subtle": semanticColorsLight["border-subtle"],
1486
1546
  "--color-border-strong": semanticColorsLight["border-strong"],
@@ -1537,6 +1597,9 @@ var lightThemeCSSVars = {
1537
1597
  "--color-border-input-hover": semanticColorsLight["border-input-hover"],
1538
1598
  "--color-border-input-focus": semanticColorsLight["border-input-focus"],
1539
1599
  "--color-border-input-error": semanticColorsLight["border-input-error"],
1600
+ "--color-hairline-subtle": semanticColorsLight["hairline-subtle"],
1601
+ "--color-hairline-default": semanticColorsLight["hairline-default"],
1602
+ "--color-hairline-strong": semanticColorsLight["hairline-strong"],
1540
1603
  "--color-interactive-disabled-text": semanticColorsLight["interactive-disabled-text"],
1541
1604
  "--color-avatar-background": semanticColorsLight["avatar-background"],
1542
1605
  "--color-avatar-text": semanticColorsLight["avatar-text"],
@@ -1576,9 +1639,11 @@ var darkThemeCSSVars = {
1576
1639
  "--color-surface-base": semanticColorsDark["surface-base"],
1577
1640
  "--color-surface-elevated": semanticColorsDark["surface-elevated"],
1578
1641
  "--color-surface-raised": semanticColorsDark["surface-raised"],
1642
+ "--color-surface-inset": semanticColorsDark["surface-inset"],
1579
1643
  "--color-background-base": semanticColorsDark["background-base"],
1580
1644
  "--color-background-default": semanticColorsDark["background-default"],
1581
1645
  "--color-background-subtle": semanticColorsDark["background-subtle"],
1646
+ "--color-background-frame": semanticColorsDark["background-frame"],
1582
1647
  "--color-border-default": semanticColorsDark["border-default"],
1583
1648
  "--color-border-subtle": semanticColorsDark["border-subtle"],
1584
1649
  "--color-border-strong": semanticColorsDark["border-strong"],
@@ -1635,6 +1700,9 @@ var darkThemeCSSVars = {
1635
1700
  "--color-border-input-hover": semanticColorsDark["border-input-hover"],
1636
1701
  "--color-border-input-focus": semanticColorsDark["border-input-focus"],
1637
1702
  "--color-border-input-error": semanticColorsDark["border-input-error"],
1703
+ "--color-hairline-subtle": semanticColorsDark["hairline-subtle"],
1704
+ "--color-hairline-default": semanticColorsDark["hairline-default"],
1705
+ "--color-hairline-strong": semanticColorsDark["hairline-strong"],
1638
1706
  "--color-interactive-disabled-text": semanticColorsDark["interactive-disabled-text"],
1639
1707
  "--color-avatar-background": semanticColorsDark["avatar-background"],
1640
1708
  "--color-avatar-text": semanticColorsDark["avatar-text"],
@@ -2269,6 +2337,10 @@ function getElevationShadow(baseColor, level, theme, isHovered = false) {
2269
2337
  elevationShadowCache.set(cacheKey, result);
2270
2338
  return result;
2271
2339
  }
2340
+ var PRESSED_ELEVATION_LEVEL = -1;
2341
+ function getPressedRecessShadow(fillColor, theme) {
2342
+ return getElevationShadow(fillColor, PRESSED_ELEVATION_LEVEL, theme);
2343
+ }
2272
2344
  function getBaseSurfaceColor(theme) {
2273
2345
  return theme === "light" ? semanticColorsLight["surface-raised"] : semanticColorsDark["surface-elevated"];
2274
2346
  }
@@ -2571,6 +2643,7 @@ var SURFACE_LEVEL_TOKEN = {
2571
2643
  raised: "surface-raised",
2572
2644
  overlay: "surface-overlay"
2573
2645
  };
2646
+ var PRESSED_RAMP = ["inset", "background", "base", "elevated", "raised", "overlay"];
2574
2647
  var ON_SURFACE_TOKEN = {
2575
2648
  primary: "text-primary",
2576
2649
  secondary: "text-secondary",
@@ -2595,9 +2668,19 @@ function onSurfaceColors(mode) {
2595
2668
  function useOnSurfaceColor(role = "primary") {
2596
2669
  return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
2597
2670
  }
2671
+ function insetFloor(mode) {
2672
+ const colors = getSemanticColors(mode);
2673
+ return colors["surface-inset"] ?? surfaceRampDark.inset;
2674
+ }
2598
2675
  function surfaceBackground(level, mode) {
2676
+ if (level === "inset") return insetFloor(mode);
2599
2677
  return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
2600
2678
  }
2679
+ function pressedLevel(parent) {
2680
+ const index = PRESSED_RAMP.indexOf(parent);
2681
+ const down = Math.max(0, index - 1);
2682
+ return PRESSED_RAMP[down];
2683
+ }
2601
2684
  function getTheme() {
2602
2685
  if (typeof document === "undefined") return "dark";
2603
2686
  return document.documentElement.classList.contains("light") ? "light" : "dark";
@@ -5296,7 +5379,7 @@ function ToastProvider({
5296
5379
  return id;
5297
5380
  }, [defaultDuration, maxToasts]);
5298
5381
  const removeToast = React25.useCallback((id) => {
5299
- setToasts((prev) => prev.filter((t16) => t16.id !== id));
5382
+ setToasts((prev) => prev.filter((t26) => t26.id !== id));
5300
5383
  }, []);
5301
5384
  const removeAllToasts = React25.useCallback(() => {
5302
5385
  setToasts([]);
@@ -6049,6 +6132,7 @@ function IconBox({
6049
6132
  function Surface({
6050
6133
  elevation = 0,
6051
6134
  level,
6135
+ pressed = false,
6052
6136
  glowColor,
6053
6137
  glowIntensity,
6054
6138
  theme,
@@ -6062,13 +6146,24 @@ function Surface({
6062
6146
  const mode = theme ?? inherited.mode;
6063
6147
  const isPlane = level != null;
6064
6148
  const baseColor = getBaseSurfaceColor(mode);
6065
- const backgroundColor = isPlane ? surfaceBackground(level, mode) : getElevationSurface(baseColor, elevation, mode);
6066
- const shadowStyle = isPlane ? {} : getElevationShadow(baseColor, elevation, mode);
6149
+ const resolvedLevel = pressed ? pressedLevel(inherited.level) : level ?? inherited.level;
6150
+ let backgroundColor;
6151
+ let shadowStyle;
6152
+ if (pressed) {
6153
+ backgroundColor = surfaceBackground(resolvedLevel, mode);
6154
+ shadowStyle = getPressedRecessShadow(backgroundColor, mode);
6155
+ } else if (isPlane) {
6156
+ backgroundColor = surfaceBackground(level, mode);
6157
+ shadowStyle = {};
6158
+ } else {
6159
+ backgroundColor = getElevationSurface(baseColor, elevation, mode);
6160
+ shadowStyle = getElevationShadow(baseColor, elevation, mode);
6161
+ }
6067
6162
  const glowStyle = glowColor ? getGlowShadow(glowColor, glowIntensity) : {};
6068
- const applyRounded = rounded ?? !isPlane;
6163
+ const applyRounded = rounded ?? (pressed || !isPlane);
6069
6164
  const value = React25.useMemo(
6070
- () => ({ mode, level: level ?? inherited.level }),
6071
- [mode, level, inherited.level]
6165
+ () => ({ mode, level: resolvedLevel }),
6166
+ [mode, resolvedLevel]
6072
6167
  );
6073
6168
  return /* @__PURE__ */ jsx(SurfaceContext.Provider, { value, children: /* @__PURE__ */ jsx(
6074
6169
  reactNative.View,
@@ -7261,13 +7356,14 @@ function MetricGroup({
7261
7356
  )
7262
7357
  ] }, i)) });
7263
7358
  }
7359
+ var t = getSemanticColors("dark");
7264
7360
  var baseBadgeSizeConfig = {
7265
7361
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
7266
7362
  md: { fontSize: 10, paddingH: 8, paddingV: 2, iconSize: 12 },
7267
7363
  lg: { fontSize: 12, paddingH: 10, paddingV: 4, iconSize: 14 }
7268
7364
  };
7269
7365
  var variantColors = {
7270
- plain: { backgroundColor: "#1C1C1C", borderColor: "#1F1F1F" },
7366
+ plain: { backgroundColor: t["surface-raised"], borderColor: "#1F1F1F" },
7271
7367
  pr: {
7272
7368
  backgroundColor: "rgba(255, 121, 0, 0.12)",
7273
7369
  borderColor: "rgba(255, 121, 0, 0.3)"
@@ -7507,11 +7603,11 @@ function PrBadge({
7507
7603
  }
7508
7604
  return badge;
7509
7605
  }
7510
- var t = getSemanticColors("dark");
7606
+ var t2 = getSemanticColors("dark");
7511
7607
  var solidVariantColors = {
7512
- success: t["status-success"],
7513
- warning: t["status-warning"],
7514
- error: t["status-error"],
7608
+ success: t2["status-success"],
7609
+ warning: t2["status-warning"],
7610
+ error: t2["status-error"],
7515
7611
  neutral: "#6B7280"
7516
7612
  };
7517
7613
  var ringVariantStyles = {
@@ -7551,8 +7647,8 @@ var iconColors2 = {
7551
7647
  warning: "#6B4000",
7552
7648
  error: "#5C1A1A",
7553
7649
  neutral: "#D1D5DB",
7554
- "on-track": t["status-success"],
7555
- deviation: t["status-warning"],
7650
+ "on-track": t2["status-success"],
7651
+ deviation: t2["status-warning"],
7556
7652
  future: "#6B7280"
7557
7653
  };
7558
7654
  var sizeDimensions = {
@@ -7873,19 +7969,19 @@ function MetricCell({ children, color, fontSize = 11 }) {
7873
7969
  }
7874
7970
 
7875
7971
  // src/components/custom/Workout/TempoDisplay.tsx
7876
- var t2 = getSemanticColors("dark");
7972
+ var t3 = getSemanticColors("dark");
7877
7973
  var INTER = "Inter, sans-serif";
7878
7974
  var TEXT_TERTIARY = "#6B7280";
7879
- var STATUS_ERROR = t2["status-error"];
7880
- var STATUS_SUCCESS = t2["status-success"];
7881
- var STATUS_WARNING = t2["status-warning"];
7975
+ var STATUS_ERROR = t3["status-error"];
7976
+ var STATUS_SUCCESS = t3["status-success"];
7977
+ var STATUS_WARNING = t3["status-warning"];
7882
7978
  var ON_TARGET_MS = 100;
7883
7979
  function getTempoFillPct(elapsedMs, targetMs) {
7884
7980
  if (!targetMs) return 100;
7885
7981
  return Math.min(100, elapsedMs / targetMs * 100);
7886
7982
  }
7887
7983
  function activeNumberTone(elapsedMs, targetMs) {
7888
- if (targetMs == null) return t2["text-primary"];
7984
+ if (targetMs == null) return t3["text-primary"];
7889
7985
  const remainingMs = targetMs - elapsedMs;
7890
7986
  if (remainingMs > ON_TARGET_MS) return STATUS_WARNING;
7891
7987
  if (remainingMs >= -ON_TARGET_MS) return STATUS_SUCCESS;
@@ -8037,7 +8133,7 @@ function TempoDisplay({
8037
8133
  style: {
8038
8134
  flexDirection: "row",
8039
8135
  alignItems: "center",
8040
- backgroundColor: "#1C1C1C",
8136
+ backgroundColor: t3["surface-raised"],
8041
8137
  paddingHorizontal: chromePadX,
8042
8138
  paddingVertical: chromePadY,
8043
8139
  borderRadius: chromeRadius
@@ -8052,7 +8148,7 @@ function TempoDisplay({
8052
8148
  fontSize: labelFont,
8053
8149
  fontWeight: "500",
8054
8150
  // Live-muted green while a rep is running, tertiary at rest.
8055
- color: live ? t2["status-live-muted"] : TEXT_TERTIARY,
8151
+ color: live ? t3["status-live-muted"] : TEXT_TERTIARY,
8056
8152
  letterSpacing: 0.5,
8057
8153
  textTransform: "uppercase",
8058
8154
  marginRight: Math.round(fontSize * 0.5)
@@ -8110,7 +8206,7 @@ function TempoDisplay({
8110
8206
  reactNative.View,
8111
8207
  {
8112
8208
  style: {
8113
- backgroundColor: "#191919",
8209
+ backgroundColor: t3["surface-overlay"],
8114
8210
  borderRadius: 6,
8115
8211
  paddingVertical: 8,
8116
8212
  paddingHorizontal: 12,
@@ -8163,13 +8259,13 @@ function TempoDisplay({
8163
8259
  }
8164
8260
  );
8165
8261
  }
8166
- var t3 = getSemanticColors("dark");
8262
+ var t4 = getSemanticColors("dark");
8167
8263
  function getDotColor(deviation) {
8168
8264
  const abs = Math.abs(deviation);
8169
- if (deviation < -0.3) return t3["status-success"];
8265
+ if (deviation < -0.3) return t4["status-success"];
8170
8266
  if (abs <= 0.3) return "#6B7280";
8171
- if (abs <= 0.7) return t3["status-warning"];
8172
- return t3["status-error"];
8267
+ if (abs <= 0.7) return t4["status-warning"];
8268
+ return t4["status-error"];
8173
8269
  }
8174
8270
  function getDeviationDescription(deviation) {
8175
8271
  if (deviation < -0.3) return "lighter than planned";
@@ -8237,19 +8333,19 @@ function DeviationBar({
8237
8333
  }
8238
8334
  );
8239
8335
  }
8240
- var t4 = getSemanticColors("dark");
8336
+ var t5 = getSemanticColors("dark");
8241
8337
  var TRACK_BG = "#333333";
8242
8338
  var LABEL_COLOR = "#6B7280";
8243
8339
  var TARGET_LINE_COLOR = "rgba(33, 150, 243, 0.5)";
8244
8340
  var AT_TARGET_GLOW = "0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)";
8245
8341
  var ZONE = {
8246
- building: t4["status-success"],
8342
+ building: t5["status-success"],
8247
8343
  // below target ramp-up
8248
- approaching: t4["status-warning"],
8344
+ approaching: t5["status-warning"],
8249
8345
  // nearing target
8250
- target: t4["brand-primary"],
8346
+ target: t5["brand-primary"],
8251
8347
  // exactly at target
8252
- over1: t4["status-error"],
8348
+ over1: t5["status-error"],
8253
8349
  // mild over-target
8254
8350
  over2: WORKOUT_TOKENS.intensity.over2,
8255
8351
  // moderate over-target
@@ -8418,12 +8514,12 @@ var MESO_ACCENT_GRADIENT_LIGHT = primitiveRamps.orange[300];
8418
8514
  var WORKOUT_PILL_DELOAD = primitiveRamps.magenta[600];
8419
8515
 
8420
8516
  // src/components/custom/Workout/WorkoutPill.tsx
8421
- var t5 = getSemanticColors("dark");
8517
+ var t6 = getSemanticColors("dark");
8422
8518
  var statusBgColors = {
8423
8519
  completed: "rgba(46,213,115,0.15)",
8424
8520
  current: "rgba(255,121,0,0.15)",
8425
8521
  next: "transparent",
8426
- upcoming: "#1C1C1C",
8522
+ upcoming: t6["surface-raised"],
8427
8523
  missed: "rgba(209,67,67,0.1)",
8428
8524
  deload: "rgba(186,41,150,0.15)"
8429
8525
  };
@@ -8436,9 +8532,9 @@ var statusBorderStyles = {
8436
8532
  deload: { borderWidth: 1, borderColor: "rgba(186,41,150,0.3)" }
8437
8533
  };
8438
8534
  var textColors = {
8439
- completed: t5["status-success"],
8440
- current: t5["brand-primary"],
8441
- next: t5["brand-primary"],
8535
+ completed: t6["status-success"],
8536
+ current: t6["brand-primary"],
8537
+ next: t6["brand-primary"],
8442
8538
  upcoming: "#6B7280",
8443
8539
  missed: "rgba(209,67,67,0.7)",
8444
8540
  deload: WORKOUT_PILL_DELOAD
@@ -8571,257 +8667,412 @@ function WorkoutPill({
8571
8667
  }
8572
8668
  return pill;
8573
8669
  }
8574
- var PULSE_HALF_MS = 950;
8575
- var PULSE_MIN_OPACITY = 0.45;
8576
- var SEGMENTED_BAR_GAP = 5;
8577
- function usePulse2(active) {
8578
- const [value] = React25.useState(() => new reactNative.Animated.Value(0));
8670
+
8671
+ // src/theme/bar-paper.ts
8672
+ function grainOpacityForColor(hex) {
8673
+ const h = hex.replace("#", "");
8674
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
8675
+ const r = parseInt(full.slice(0, 2), 16);
8676
+ const g = parseInt(full.slice(2, 4), 16);
8677
+ const b = parseInt(full.slice(4, 6), 16);
8678
+ if ([r, g, b].some(Number.isNaN)) return 0.14;
8679
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
8680
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
8681
+ }
8682
+ function barGrain(color) {
8683
+ const op = grainOpacityForColor(color).toFixed(3);
8684
+ 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")`;
8685
+ }
8686
+ function barPaper(color, flip = false) {
8687
+ return {
8688
+ backgroundImage: barGrain(color),
8689
+ 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)"
8690
+ };
8691
+ }
8692
+ var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
8693
+ var PEAK_OVERSHOOT = 1.12;
8694
+ function getReducedMotionPreference() {
8695
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8696
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8697
+ }
8698
+ function usePrefersReducedMotion() {
8699
+ const [reduced, setReduced] = React25.useState(getReducedMotionPreference);
8579
8700
  React25.useEffect(() => {
8580
- if (!active) return;
8581
- const loop = reactNative.Animated.loop(
8701
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
8702
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
8703
+ const handler = () => setReduced(mq.matches);
8704
+ handler();
8705
+ mq.addEventListener?.("change", handler);
8706
+ return () => mq.removeEventListener?.("change", handler);
8707
+ }, []);
8708
+ return reduced;
8709
+ }
8710
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
8711
+ const prefersReducedMotion = usePrefersReducedMotion();
8712
+ const [liveGrowth] = React25.useState(() => new reactNative.Animated.Value(1));
8713
+ React25.useEffect(() => {
8714
+ if (liveRepIndex == null || liveVelocity == null) return;
8715
+ if (prefersReducedMotion) {
8716
+ liveGrowth.setValue(1);
8717
+ return;
8718
+ }
8719
+ liveGrowth.setValue(0);
8720
+ if (isNewPeak) {
8582
8721
  reactNative.Animated.sequence([
8583
- reactNative.Animated.timing(value, {
8584
- toValue: 1,
8585
- duration: PULSE_HALF_MS,
8586
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8722
+ reactNative.Animated.timing(liveGrowth, {
8723
+ toValue: PEAK_OVERSHOOT,
8724
+ duration: 220,
8725
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
8587
8726
  useNativeDriver: false
8588
8727
  }),
8589
- reactNative.Animated.timing(value, {
8590
- toValue: 0,
8591
- duration: PULSE_HALF_MS,
8592
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8728
+ reactNative.Animated.spring(liveGrowth, {
8729
+ toValue: 1,
8730
+ friction: 5,
8731
+ tension: 140,
8593
8732
  useNativeDriver: false
8594
8733
  })
8595
- ])
8734
+ ]).start();
8735
+ } else {
8736
+ reactNative.Animated.timing(liveGrowth, {
8737
+ toValue: 1,
8738
+ duration: 300,
8739
+ easing: ANIMATION_EASING,
8740
+ useNativeDriver: false
8741
+ }).start();
8742
+ }
8743
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
8744
+ return liveGrowth;
8745
+ }
8746
+
8747
+ // src/components/custom/charts/SetBarChart.tsx
8748
+ function mixHex(a, b, t26) {
8749
+ const parse = (h) => {
8750
+ const s = h.replace("#", "");
8751
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
8752
+ };
8753
+ const [ar, ag, ab] = parse(a);
8754
+ const [br, bg, bb] = parse(b);
8755
+ const ch = (x, y) => Math.round(x + (y - x) * t26).toString(16).padStart(2, "0");
8756
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
8757
+ }
8758
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
8759
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8760
+ var FIXED_MAX_VALUE = 1.15;
8761
+ var PEAK_HEADROOM = 1.03;
8762
+ function scaleDenominator(scale, maxValue) {
8763
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
8764
+ }
8765
+ function valueLabelFontSize(height) {
8766
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
8767
+ }
8768
+ var SET_BAR_DEFAULT_HEIGHT = 220;
8769
+ var LABEL_ROW_HEIGHT = 16;
8770
+ var LABEL_GAP = 3;
8771
+ var GAP_RATIO = 0.08;
8772
+ var MIN_BAR_GAP = 2;
8773
+ var CHUNK_NOTCH_RATIO = 0.25;
8774
+ var CHUNK_NOTCH_MIN_PX = 10;
8775
+ var BAR_MAX_WIDTH = 120;
8776
+ var LABEL_MIN_BAR_WIDTH = 30;
8777
+ var DEFAULT_BAR_RADIUS = 5;
8778
+ var MIN_BAR_HEIGHT = 4;
8779
+ var FLAT_BAR_FRACTION = 0.5;
8780
+ var STUB_HEIGHT = 9;
8781
+ var REP_GAP = 2;
8782
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
8783
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
8784
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
8785
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
8786
+ return { barWidth, gap };
8787
+ }
8788
+ function isRep(slot) {
8789
+ return slot.kind === "rep";
8790
+ }
8791
+ function repIndexByCell(cells) {
8792
+ let count = 0;
8793
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
8794
+ }
8795
+ function SetBarChart({
8796
+ slots,
8797
+ colorFor,
8798
+ height,
8799
+ scale = "peak",
8800
+ scaleMax,
8801
+ orientation = "up",
8802
+ liveRepIndex,
8803
+ isNewPeak = false,
8804
+ targetReps,
8805
+ gapRatio = GAP_RATIO,
8806
+ barRadius = DEFAULT_BAR_RADIUS,
8807
+ cornerStyle = "top",
8808
+ flat = false,
8809
+ showValueLabels: showValueLabelsProp = false,
8810
+ formatValue: formatValue3 = String,
8811
+ todoVariant = "solid",
8812
+ minColumns,
8813
+ renderReference,
8814
+ renderBarOverlay,
8815
+ expandProgress,
8816
+ hideBaseline = false,
8817
+ testID,
8818
+ testIDPrefix = "setbar",
8819
+ accessibilityLabel: accessibilityLabel2,
8820
+ label,
8821
+ className,
8822
+ viewProps = {}
8823
+ }) {
8824
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
8825
+ const cells = targetPadded > slots.length ? [
8826
+ ...slots,
8827
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
8828
+ ] : slots;
8829
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
8830
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
8831
+ const flip = orientation === "down";
8832
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
8833
+ const placeholderColor = useOnSurfaceColor("tertiary");
8834
+ const surface = useSurface();
8835
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
8836
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
8837
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
8838
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
8839
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
8840
+ const showValueLabels = showValueLabelsProp && !flat;
8841
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
8842
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
8843
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
8844
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
8845
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
8846
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
8847
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
8848
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
8849
+ const [plotW, setPlotW] = React25.useState(0);
8850
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
8851
+ const totalCells = cells.length;
8852
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
8853
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
8854
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
8855
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
8856
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
8857
+ const { style: externalStyle, ...restProps } = viewProps;
8858
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
8859
+ const repIndices = repIndexByCell(cells);
8860
+ return /* @__PURE__ */ jsxs(
8861
+ reactNative.View,
8862
+ {
8863
+ className,
8864
+ style: [{ height, flexDirection: "row" }, externalStyle],
8865
+ testID,
8866
+ ...a11y,
8867
+ ...restProps,
8868
+ children: [
8869
+ label ? /* @__PURE__ */ jsx(
8870
+ ChartSideRail,
8871
+ {
8872
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
8873
+ sectionExtent: height
8874
+ }
8875
+ ) : null,
8876
+ /* @__PURE__ */ jsxs(
8877
+ reactNative.View,
8878
+ {
8879
+ onLayout: onPlotLayout,
8880
+ style: {
8881
+ flex: 1,
8882
+ flexDirection: "row",
8883
+ alignItems: "flex-end",
8884
+ gap,
8885
+ position: "relative",
8886
+ borderBottomWidth: hideBaseline ? 0 : 2,
8887
+ borderBottomColor: placeholderColor,
8888
+ ...flipStyle
8889
+ },
8890
+ children: [
8891
+ renderReference?.(geometry),
8892
+ cells.map((slot, i) => {
8893
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
8894
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
8895
+ const column = {
8896
+ flex: 1,
8897
+ maxWidth: BAR_MAX_WIDTH,
8898
+ height: "100%",
8899
+ alignItems: "center",
8900
+ justifyContent: "flex-end",
8901
+ marginLeft: extraGap
8902
+ };
8903
+ if (!isRep(slot)) {
8904
+ return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
8905
+ slot.kind,
8906
+ barCorners,
8907
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
8908
+ flat ? compactBarHeight : STUB_HEIGHT,
8909
+ placeholderColor,
8910
+ testIDPrefix,
8911
+ todoVariant,
8912
+ solidTodoColor,
8913
+ emptyColor
8914
+ ) }, i);
8915
+ }
8916
+ const repIndex = repIndices[i];
8917
+ const value = slot.value ?? 0;
8918
+ const isLive = liveRepIndex === repIndex;
8919
+ const color = colorFor(value);
8920
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
8921
+ inputRange: [0, 1],
8922
+ outputRange: [flatBarHeight, valueBarHeight(value)]
8923
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
8924
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
8925
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
8926
+ return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
8927
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
8928
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
8929
+ /* @__PURE__ */ jsx(
8930
+ reactNative.Animated.View,
8931
+ {
8932
+ style: {
8933
+ position: "absolute",
8934
+ bottom: labelBottom,
8935
+ left: 0,
8936
+ right: 0,
8937
+ alignItems: "center",
8938
+ ...expandProgress ? { opacity: expandProgress } : null
8939
+ },
8940
+ pointerEvents: "none",
8941
+ children: /* @__PURE__ */ jsx(
8942
+ reactNative.Text,
8943
+ {
8944
+ className: "text-text-secondary",
8945
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
8946
+ testID: `${testIDPrefix}-label-${repIndex}`,
8947
+ children: formatValue3(value)
8948
+ }
8949
+ )
8950
+ }
8951
+ ),
8952
+ /* @__PURE__ */ jsx(
8953
+ reactNative.Animated.View,
8954
+ {
8955
+ style: [
8956
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
8957
+ barCorners,
8958
+ barPaper(color, flip)
8959
+ ],
8960
+ testID: `${testIDPrefix}-bar-${repIndex}`
8961
+ }
8962
+ ),
8963
+ renderBarOverlay?.(repIndex, value)
8964
+ ] }, i);
8965
+ })
8966
+ ]
8967
+ }
8968
+ )
8969
+ ]
8970
+ }
8971
+ );
8972
+ }
8973
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
8974
+ const base = {
8975
+ width: "100%",
8976
+ height: stubHeight,
8977
+ ...barCorners
8978
+ };
8979
+ if (kind === "variable") {
8980
+ return /* @__PURE__ */ jsx(
8981
+ reactNative.View,
8982
+ {
8983
+ style: { ...base, backgroundColor: VARIABLE_FILL },
8984
+ testID: `${testIDPrefix}-slot-variable`
8985
+ }
8596
8986
  );
8597
- loop.start();
8598
- return () => loop.stop();
8599
- }, [active, value]);
8600
- return value;
8987
+ }
8988
+ if (kind === "continue") {
8989
+ return /* @__PURE__ */ jsx(
8990
+ reactNative.View,
8991
+ {
8992
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
8993
+ testID: `${testIDPrefix}-slot-continue`
8994
+ }
8995
+ );
8996
+ }
8997
+ if (kind === "empty") {
8998
+ return /* @__PURE__ */ jsx(
8999
+ reactNative.View,
9000
+ {
9001
+ style: { ...base, backgroundColor: emptyColor },
9002
+ testID: `${testIDPrefix}-slot-empty`
9003
+ }
9004
+ );
9005
+ }
9006
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
9007
+ return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
8601
9008
  }
8602
- function SegmentedBar({
8603
- segments,
8604
- height = 8,
8605
- gap = SEGMENTED_BAR_GAP,
8606
- radius = 2,
8607
- marker = null,
8608
- segmentTestID,
8609
- style,
8610
- ...props
9009
+ function sideLabelFontSize(height) {
9010
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
9011
+ }
9012
+ var SIDE_LABEL_INITIALS_BELOW = 70;
9013
+ function sideLabelText(label, extent) {
9014
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
9015
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
9016
+ }
9017
+ function ChartSideRail({
9018
+ sections,
9019
+ sectionExtent,
9020
+ variant = "hero"
8611
9021
  }) {
8612
- const pulse = usePulse2(segments.some((s) => s.pulse));
8613
- return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
8614
- segments.map((seg, i) => {
8615
- const fillStyle = {
8616
- width: `${(seg.fill ?? 1) * 100}%`,
8617
- height: "100%",
8618
- backgroundColor: seg.color,
8619
- opacity: seg.opacity
8620
- };
8621
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
8622
- return /* @__PURE__ */ jsx(
9022
+ if (!sections.some((s) => s.label)) return null;
9023
+ const isRail = variant === "rail";
9024
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
9025
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
9026
+ const textStyle = {
9027
+ maxWidth: "100%",
9028
+ textAlign: "center",
9029
+ fontSize,
9030
+ fontWeight: "700",
9031
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
9032
+ textTransform: "uppercase"
9033
+ };
9034
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
9035
+ reactNative.View,
9036
+ {
9037
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
9038
+ children: s.label ? /* @__PURE__ */ jsx(
8623
9039
  reactNative.View,
8624
9040
  {
8625
9041
  style: {
8626
- flex: seg.weight ?? 1,
8627
- minWidth: 0,
8628
- height: "100%",
8629
- marginLeft: seg.leadingGap,
8630
- borderRadius: radius,
8631
- overflow: "hidden"
9042
+ width: labelLength,
9043
+ alignItems: "center",
9044
+ transform: [{ rotate: "-90deg" }]
8632
9045
  },
8633
- children: seg.pulse ? /* @__PURE__ */ jsx(
8634
- reactNative.Animated.View,
9046
+ children: /* @__PURE__ */ jsx(
9047
+ reactNative.Text,
8635
9048
  {
8636
- style: {
8637
- ...fillStyle,
8638
- ...seg.pulseColor ? {
8639
- backgroundColor: pulse.interpolate({
8640
- inputRange: [0, 1],
8641
- outputRange: seg.pulseColor
8642
- })
8643
- } : {
8644
- opacity: pulse.interpolate({
8645
- inputRange: [0, 1],
8646
- outputRange: [PULSE_MIN_OPACITY, 1]
8647
- })
8648
- }
8649
- },
8650
- accessibilityElementsHidden: true,
8651
- testID
9049
+ className: "text-text-tertiary",
9050
+ style: textStyle,
9051
+ numberOfLines: 1,
9052
+ testID: s.testID,
9053
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
8652
9054
  }
8653
- ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
8654
- },
8655
- i
8656
- );
8657
- }),
8658
- marker ? /* @__PURE__ */ jsx(
8659
- reactNative.View,
8660
- {
8661
- style: {
8662
- position: "absolute",
8663
- left: `${marker.position * 100}%`,
8664
- top: 0,
8665
- bottom: 0,
8666
- width: 2,
8667
- backgroundColor: marker.color
8668
- },
8669
- accessibilityElementsHidden: true,
8670
- testID: "segmented-bar-marker"
8671
- }
8672
- ) : null
8673
- ] });
9055
+ )
9056
+ }
9057
+ ) : null
9058
+ },
9059
+ s.testID
9060
+ )) });
8674
9061
  }
8675
9062
 
8676
- // src/components/custom/Workout/SetBar.tsx
8677
- var SET_STRIP_ZONES = {
8678
- slow: primitiveRamps.red[600],
8679
- moderate: primitiveRamps.orange[400],
8680
- fast: primitiveRamps.amber[300],
8681
- fastest: primitiveRamps.green[300]
8682
- };
8683
- var TODO_COLOR = primitiveColors.charcoal[300];
8684
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
8685
- var NOTCH_GAP = 2;
8686
- var CLUSTER_GAP = 3;
8687
- var MYO_UPCOMING_CLUSTERS = 3;
8688
- var MYO_UPCOMING_CLUSTER_LEN = 5;
8689
- var MYO_UPCOMING_FADE = 0.6;
8690
- var PULSE_RANGE = {
8691
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
8692
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
8693
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
8694
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
8695
- };
8696
- function velocityZoneColor(v) {
8697
- if (v < 0.5) return SET_STRIP_ZONES.slow;
8698
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
8699
- if (v < 1) return SET_STRIP_ZONES.fast;
8700
- return SET_STRIP_ZONES.fastest;
8701
- }
8702
- function chunkedSegments(chunks, gap) {
8703
- return chunks.flatMap(
8704
- (vels, ci) => vels.map((v, ri) => ({
8705
- color: velocityZoneColor(v),
8706
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
8707
- }))
8708
- );
8709
- }
8710
- function rangeSegments(floor, max, doneVels) {
8711
- return Array.from({ length: max }, (_, i) => {
8712
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
8713
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
8714
- });
8715
- }
8716
- function myoUpcomingSegments(activationLen, clusterCount) {
8717
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
8718
- const trail = Array.from(
8719
- { length: clusterCount },
8720
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
8721
- color: SET_STRIP_VARIABLE_COLOR,
8722
- opacity: MYO_UPCOMING_FADE ** ci,
8723
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
8724
- }))
8725
- ).flat();
8726
- return [...activation, ...trail];
8727
- }
8728
- function setSegments(set) {
8729
- if (set.status === "todo") {
8730
- return [{ color: TODO_COLOR }];
8731
- }
8732
- if (set.status === "done") {
8733
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
8734
- }
8735
- if (set.status === "range") {
8736
- return rangeSegments(set.floor, set.max, set.doneVels);
8737
- }
8738
- if (set.status === "drop") {
8739
- return chunkedSegments(set.subloads, NOTCH_GAP);
8740
- }
8741
- if (set.status === "myo") {
8742
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
8743
- }
8744
- if (set.status === "myo-upcoming") {
8745
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
8746
- }
8747
- const performed = set.velocities.map((v) => {
8748
- const color = velocityZoneColor(v);
8749
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
8750
- });
8751
- const remaining = Math.max(0, set.planned - set.velocities.length);
8752
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
8753
- }
8754
- function setBarSegmentTestID(seg) {
8755
- if (seg.pulse) return "set-strip-pulse";
8756
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
8757
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
8758
- }
8759
- function SetBar({ set, height = 8 }) {
8760
- return /* @__PURE__ */ jsx(
8761
- SegmentedBar,
8762
- {
8763
- segments: setSegments(set),
8764
- height,
8765
- gap: 0,
8766
- radius: 0,
8767
- segmentTestID: setBarSegmentTestID,
8768
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
8769
- testID: "set-strip-set"
8770
- }
8771
- );
8772
- }
8773
-
8774
- // src/components/custom/Workout/SetStrip.tsx
8775
- var SET_STRIP_GAP = 5;
8776
- function setCategory(set) {
8777
- switch (set.status) {
8778
- case "done":
8779
- case "drop":
8780
- case "myo":
8781
- return "done";
8782
- case "active":
8783
- return "active";
8784
- case "range":
8785
- return set.doneVels.length > 0 ? "active" : "upcoming";
8786
- case "todo":
8787
- case "myo-upcoming":
8788
- return "upcoming";
8789
- }
8790
- }
8791
- function describeSets(sets) {
8792
- const done = sets.filter((s) => setCategory(s) === "done").length;
8793
- const active = sets.filter((s) => setCategory(s) === "active").length;
8794
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
8795
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
8796
- }
8797
- function SetStrip({ sets, height = 8, className, ...props }) {
8798
- return /* @__PURE__ */ jsx(
8799
- reactNative.View,
8800
- {
8801
- className,
8802
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
8803
- accessibilityRole: "image",
8804
- accessibilityLabel: describeSets(sets),
8805
- testID: "set-strip",
8806
- ...props,
8807
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
8808
- }
8809
- );
8810
- }
8811
-
8812
- // src/components/custom/Workout/VelocityStrip.tsx
8813
- var VEL_COLORS = WORKOUT_TOKENS.scale;
8814
- var bandIdToEffortColor = {
8815
- speed: sequentialEffort[0],
8816
- // green-300
8817
- power: sequentialEffort[2],
8818
- // amber-300 (gold)
8819
- strengthSpeed: sequentialEffort[3],
8820
- // orange-400
8821
- maximalStrength: sequentialEffort[4],
8822
- // red-600
8823
- grinding: sequentialEffort[5]
8824
- // red-700 — the distinct 5th band
9063
+ // src/components/custom/Workout/VelocityStrip.tsx
9064
+ var VEL_COLORS = WORKOUT_TOKENS.scale;
9065
+ var bandIdToEffortColor = {
9066
+ speed: sequentialEffort[0],
9067
+ // green-300
9068
+ power: sequentialEffort[2],
9069
+ // amber-300 (gold)
9070
+ strengthSpeed: sequentialEffort[3],
9071
+ // orange-400
9072
+ maximalStrength: sequentialEffort[4],
9073
+ // red-600
9074
+ grinding: sequentialEffort[5]
9075
+ // red-700 the distinct 5th band
8825
9076
  };
8826
9077
  function getVelocityZoneColor(velocity) {
8827
9078
  if (velocity >= 1) return "vel-green";
@@ -8853,6 +9104,18 @@ function calculateMeanVelocity(velocities) {
8853
9104
  const sum = velocities.reduce((acc, v) => acc + v, 0);
8854
9105
  return sum / velocities.length;
8855
9106
  }
9107
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
9108
+ function getVelocityLossColor(lossPct) {
9109
+ const [t1, t26, t32] = VL_LOSS_THRESHOLDS;
9110
+ if (lossPct < t1) return VEL_COLORS.green;
9111
+ if (lossPct < t26) return VEL_COLORS.yellow;
9112
+ if (lossPct < t32) return VEL_COLORS.orange;
9113
+ return VEL_COLORS.red;
9114
+ }
9115
+ function velocityLossForRep(velocity, best) {
9116
+ if (best <= 0) return 0;
9117
+ return Math.max(0, Math.round((best - velocity) / best * 100));
9118
+ }
8856
9119
  function classifyBand(velocity, bands) {
8857
9120
  for (const band of bands) {
8858
9121
  if (band.max === null || velocity < band.max) return band;
@@ -8862,15 +9125,17 @@ function classifyBand(velocity, bands) {
8862
9125
  function bandColor(band) {
8863
9126
  return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
8864
9127
  }
9128
+ function makeBarColorFor(zones) {
9129
+ const hasZones = zones != null && zones.length > 0;
9130
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9131
+ }
8865
9132
  function getLossStyle(loss) {
8866
9133
  if (loss > 25) return { color: VEL_COLORS.red };
8867
9134
  if (loss > 20) return { color: VEL_COLORS.orange };
8868
9135
  return null;
8869
9136
  }
8870
- var REP_GAP = 2;
9137
+ var REP_GAP2 = 2;
8871
9138
  var WIDE_GAP = 8;
8872
- var TODO_COLOR2 = primitiveColors.charcoal[300];
8873
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8874
9139
  function deriveDoneVelocities(set) {
8875
9140
  switch (set.type) {
8876
9141
  case "drop":
@@ -8886,7 +9151,7 @@ function chunkedRepSlots(chunks) {
8886
9151
  chunks.forEach((chunk) => {
8887
9152
  chunk.forEach((velocity, ri) => {
8888
9153
  const first = slots.length === 0;
8889
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
9154
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
8890
9155
  });
8891
9156
  });
8892
9157
  return slots;
@@ -8900,7 +9165,7 @@ function buildSlots(set) {
8900
9165
  return {
8901
9166
  kind: done ? "rep" : "todo",
8902
9167
  velocity: done ? set.velocities[i] : void 0,
8903
- leadingGap: i === 0 ? 0 : REP_GAP
9168
+ leadingGap: i === 0 ? 0 : REP_GAP2
8904
9169
  };
8905
9170
  });
8906
9171
  }
@@ -8911,7 +9176,7 @@ function buildSlots(set) {
8911
9176
  return {
8912
9177
  kind,
8913
9178
  velocity: kind === "rep" ? set.velocities[i] : void 0,
8914
- leadingGap: i === 0 ? 0 : REP_GAP
9179
+ leadingGap: i === 0 ? 0 : REP_GAP2
8915
9180
  };
8916
9181
  });
8917
9182
  }
@@ -8919,16 +9184,16 @@ function buildSlots(set) {
8919
9184
  const reps = set.velocities.map((velocity, i) => ({
8920
9185
  kind: "rep",
8921
9186
  velocity,
8922
- leadingGap: i === 0 ? 0 : REP_GAP
9187
+ leadingGap: i === 0 ? 0 : REP_GAP2
8923
9188
  }));
8924
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
9189
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
8925
9190
  return reps;
8926
9191
  }
8927
9192
  case "drop":
8928
9193
  return chunkedRepSlots(set.subloads);
8929
9194
  case "myo": {
8930
9195
  const slots = chunkedRepSlots([set.activation, ...set.clusters]);
8931
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
9196
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
8932
9197
  return slots;
8933
9198
  }
8934
9199
  case "cluster": {
@@ -8939,7 +9204,7 @@ function buildSlots(set) {
8939
9204
  return {
8940
9205
  kind: done ? "rep" : "todo",
8941
9206
  velocity: done ? set.velocities[i] : void 0,
8942
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
9207
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
8943
9208
  };
8944
9209
  });
8945
9210
  }
@@ -8961,253 +9226,406 @@ function setAccessibilityLabel(set, repCount) {
8961
9226
  return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
8962
9227
  }
8963
9228
  }
8964
- function slotAccessibilityLabel(slot, index) {
8965
- switch (slot.kind) {
8966
- case "rep":
8967
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
8968
- case "todo":
8969
- return `Rep ${index + 1}: planned`;
8970
- case "variable":
8971
- return `Rep ${index + 1}: variable`;
8972
- case "continue":
8973
- return "Keep going";
8974
- }
8975
- }
8976
- var EXPANDED_TODO_STUB_PCT = 16;
8977
- var EXPANDED_ENCODED_PCT = 45;
8978
9229
  var EXPANDED_HEIGHT = 60;
8979
- var FIXED_MAX_VELOCITY = 1.15;
8980
- var BARE_STUB_HEIGHT = 3;
8981
- var HERO_HEIGHT = 220;
8982
- var HERO_LABEL_HEADROOM = 20;
8983
- var HERO_BAR_GAP = 8;
8984
- var HERO_BAR_MAX_WIDTH = 120;
8985
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
8986
- var HERO_BAR_RADIUS = 5;
8987
- var HERO_MIN_BAR_HEIGHT = 4;
8988
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
9230
+ var COMPACT_HEIGHT = 8;
8989
9231
  var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
8990
- var PEAK_HEADROOM = 1.06;
8991
- function scaleDenominator(scale, maxVelocity) {
8992
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
8993
- }
8994
- function bareSlotHeight(slot, height, denom) {
8995
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
8996
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
8997
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
8998
- }
8999
- function getReducedMotionPreference() {
9000
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
9001
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
9002
- }
9003
- function usePrefersReducedMotion() {
9004
- const [reduced, setReduced] = React25.useState(getReducedMotionPreference);
9005
- React25.useEffect(() => {
9006
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
9007
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
9008
- const handler = () => setReduced(mq.matches);
9009
- handler();
9010
- mq.addEventListener?.("change", handler);
9011
- return () => mq.removeEventListener?.("change", handler);
9012
- }, []);
9013
- return reduced;
9232
+ function DashedReferenceLine({
9233
+ anchor,
9234
+ offset,
9235
+ testID
9236
+ }) {
9237
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
9238
+ return /* @__PURE__ */ jsx(
9239
+ reactNative.View,
9240
+ {
9241
+ accessibilityElementsHidden: true,
9242
+ style: {
9243
+ position: "absolute",
9244
+ left: 0,
9245
+ right: 0,
9246
+ borderTopWidth: 1,
9247
+ borderStyle: "dashed",
9248
+ borderColor: HERO_REFERENCE_COLOR,
9249
+ pointerEvents: "none",
9250
+ ...edge
9251
+ },
9252
+ testID
9253
+ }
9254
+ );
9014
9255
  }
9015
- var ANIMATION_DURATION = 400;
9016
- var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
9017
- var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
9018
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
9019
- const prefersReducedMotion = usePrefersReducedMotion();
9020
- const [liveScale] = React25.useState(() => new reactNative.Animated.Value(1));
9021
- React25.useEffect(() => {
9022
- if (!active || liveRepIndex == null || liveVelocity == null) return;
9023
- if (prefersReducedMotion) {
9024
- liveScale.setValue(1);
9025
- return;
9256
+ var BAND_LABEL_FONT = "monospace";
9257
+ var VL_LABEL_MIN_PLOT = 65;
9258
+ var VL_SEMANTIC = getSemanticColors("dark");
9259
+ function VelocityLossBands({
9260
+ best,
9261
+ scaleDenom,
9262
+ plotHeight,
9263
+ flip = false
9264
+ }) {
9265
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
9266
+ const yOf = (v) => v / scaleDenom * plotHeight;
9267
+ const vl20 = best * 0.8;
9268
+ const vl30 = best * 0.7;
9269
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
9270
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
9271
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
9272
+ reactNative.View,
9273
+ {
9274
+ style: {
9275
+ position: "absolute",
9276
+ left: 0,
9277
+ right: 0,
9278
+ bottom: yOf(loV),
9279
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
9280
+ backgroundColor: color
9281
+ }
9026
9282
  }
9027
- if (isNewPeak) {
9028
- liveScale.setValue(1);
9029
- reactNative.Animated.sequence([
9030
- reactNative.Animated.timing(liveScale, {
9031
- toValue: 1.25,
9032
- duration: 150,
9033
- easing: reactNative.Easing.out(reactNative.Easing.quad),
9034
- useNativeDriver: true
9035
- }),
9036
- reactNative.Animated.spring(liveScale, {
9037
- toValue: 1,
9038
- friction: 3,
9039
- tension: 140,
9040
- useNativeDriver: true
9041
- })
9042
- ]).start();
9043
- } else {
9044
- liveScale.setValue(0.8);
9045
- reactNative.Animated.timing(liveScale, {
9046
- toValue: 1,
9047
- duration: 300,
9048
- easing: POP_EASING,
9049
- useNativeDriver: true
9050
- }).start();
9283
+ );
9284
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
9285
+ reactNative.View,
9286
+ {
9287
+ style: {
9288
+ position: "absolute",
9289
+ left: 0,
9290
+ right: 0,
9291
+ bottom: yOf(v),
9292
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
9293
+ // band edge — instead of floating up half a text-row (the label then centres on
9294
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
9295
+ height: 0,
9296
+ flexDirection: "row",
9297
+ alignItems: "center"
9298
+ },
9299
+ children: [
9300
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
9301
+ showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9302
+ /* @__PURE__ */ jsx(
9303
+ reactNative.Text,
9304
+ {
9305
+ style: {
9306
+ marginHorizontal: 6,
9307
+ fontSize: vlFont,
9308
+ fontWeight: "800",
9309
+ fontFamily: BAND_LABEL_FONT,
9310
+ color,
9311
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
9312
+ },
9313
+ children: label
9314
+ }
9315
+ ),
9316
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
9317
+ ] }) : null
9318
+ ]
9319
+ }
9320
+ );
9321
+ return /* @__PURE__ */ jsxs(
9322
+ reactNative.View,
9323
+ {
9324
+ accessibilityElementsHidden: true,
9325
+ pointerEvents: "none",
9326
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
9327
+ testID: "velocity-loss-bands",
9328
+ children: [
9329
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
9330
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
9331
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
9332
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
9333
+ ]
9051
9334
  }
9052
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
9053
- return liveScale;
9335
+ );
9054
9336
  }
9055
- function HeroVelocityChart({
9056
- doneVelocities,
9057
- barColorFor,
9058
- scaleDenom,
9059
- referenceVelocity,
9060
- liveRepIndex,
9061
- isNewPeak,
9337
+ function velocityReferenceOverlay(g, showLossBands) {
9338
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
9339
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9340
+ showLossBands && /* @__PURE__ */ jsx(
9341
+ VelocityLossBands,
9342
+ {
9343
+ best: g.best,
9344
+ scaleDenom: g.scaleDenom,
9345
+ plotHeight: g.plotHeight,
9346
+ flip: g.flip
9347
+ }
9348
+ ),
9349
+ referencePx > 0 && /* @__PURE__ */ jsx(
9350
+ DashedReferenceLine,
9351
+ {
9352
+ anchor: "bottom",
9353
+ offset: referencePx,
9354
+ testID: "velocity-hero-reference"
9355
+ }
9356
+ )
9357
+ ] });
9358
+ }
9359
+ var ANIMATION_DURATION = 400;
9360
+ var DUAL_HERO_HEIGHT = SET_BAR_DEFAULT_HEIGHT;
9361
+ var DUAL_RAIL_HEIGHT = 96;
9362
+ var DUAL_COMPACT_HEIGHT = 11.5;
9363
+ var DUAL_WING_GAP = 2;
9364
+ var COMPACT_FOLD_GAP = 1.5;
9365
+ var COMPACT_FOLD_HALF = 5;
9366
+ function streamDone(stream) {
9367
+ return stream.set ? deriveDoneVelocities(stream.set) : stream.velocities ?? [];
9368
+ }
9369
+ function streamNaturalSlots(stream, targetReps) {
9370
+ if (stream.set) {
9371
+ return buildSlots(stream.set).map((s) => ({
9372
+ kind: s.kind,
9373
+ value: s.velocity,
9374
+ leadingGap: s.leadingGap
9375
+ }));
9376
+ }
9377
+ const reps = (stream.velocities ?? []).map((v, i) => ({
9378
+ kind: "rep",
9379
+ value: v,
9380
+ leadingGap: i === 0 ? 0 : REP_GAP2
9381
+ }));
9382
+ const pad = Math.max(0, (targetReps ?? 0) - reps.length);
9383
+ return [...reps, ...Array.from({ length: pad }, () => ({ kind: "todo" }))];
9384
+ }
9385
+ function mergeColumnKind(l, r) {
9386
+ const kinds = [l?.kind, r?.kind];
9387
+ if (kinds.includes("variable")) return "variable";
9388
+ if (kinds.includes("continue")) return "continue";
9389
+ if (kinds.includes("rep")) return "rep";
9390
+ return "todo";
9391
+ }
9392
+ function sideColumnCell(slot, kind, leadingGap) {
9393
+ if (kind !== "rep") return { kind, leadingGap };
9394
+ return slot?.kind === "rep" ? { kind: "rep", value: slot.value, leadingGap } : { kind: "empty", leadingGap };
9395
+ }
9396
+ function alignDualSlots(left, right) {
9397
+ const n = Math.max(left.length, right.length);
9398
+ const L = [];
9399
+ const R = [];
9400
+ for (let i = 0; i < n; i++) {
9401
+ const lt = left[i];
9402
+ const rt = right[i];
9403
+ const kind = mergeColumnKind(lt, rt);
9404
+ const gap = Math.max(lt?.leadingGap ?? REP_GAP2, rt?.leadingGap ?? REP_GAP2);
9405
+ L.push(sideColumnCell(lt, kind, gap));
9406
+ R.push(sideColumnCell(rt, kind, gap));
9407
+ }
9408
+ return { left: L, right: R };
9409
+ }
9410
+ function DualVelocityHero({
9411
+ leftDone,
9412
+ rightDone,
9413
+ leftStream,
9414
+ rightStream,
9415
+ leftLabel,
9416
+ rightLabel,
9417
+ zones,
9418
+ barColor: barColor2,
9419
+ scale,
9062
9420
  targetReps,
9421
+ liveRepIndex,
9063
9422
  height,
9064
9423
  className,
9424
+ label,
9065
9425
  viewProps
9066
9426
  }) {
9067
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9068
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
9069
- const [plotW, setPlotW] = React25.useState(0);
9070
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
9071
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
9072
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
9073
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
9074
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
9075
- const totalSlots = doneVelocities.length + pendingCount;
9076
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
9077
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
9078
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
9079
- const peakIndex = doneVelocities.reduce(
9080
- (best, v, i) => v > doneVelocities[best] ? i : best,
9081
- 0
9082
- );
9083
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
9084
- const repCount = doneVelocities.length;
9085
- const total = Math.max(repCount, targetReps ?? repCount);
9086
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9427
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9428
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9429
+ const aligned = alignDualSlots(
9430
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9431
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9432
+ );
9087
9433
  const { style: externalStyle, ...restProps } = viewProps;
9088
- return /* @__PURE__ */ jsx(
9434
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9435
+ VelocityStrip,
9436
+ {
9437
+ variant: "hero",
9438
+ orientation,
9439
+ velocities: done,
9440
+ columnSlots: columns,
9441
+ scale,
9442
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9443
+ barColor: barColor2,
9444
+ zones,
9445
+ liveRepIndex,
9446
+ height: plotHalf,
9447
+ hideBaseline: true
9448
+ }
9449
+ );
9450
+ return /* @__PURE__ */ jsxs(
9089
9451
  reactNative.View,
9090
9452
  {
9091
9453
  className,
9092
- style: [{ height }, externalStyle],
9454
+ style: [{ height, flexDirection: "row" }, externalStyle],
9093
9455
  accessibilityRole: "image",
9094
9456
  accessibilityLabel: label,
9095
- testID: "velocity-strip-hero",
9457
+ testID: "dual-velocity-strip",
9096
9458
  ...restProps,
9097
- children: /* @__PURE__ */ jsxs(
9098
- reactNative.View,
9099
- {
9100
- onLayout: onPlotLayout,
9101
- style: {
9102
- flex: 1,
9103
- flexDirection: "row",
9104
- alignItems: "flex-end",
9105
- gap: heroGap,
9106
- position: "relative",
9107
- borderBottomWidth: 2,
9108
- borderBottomColor: HERO_PENDING_COLOR
9109
- },
9110
- children: [
9111
- referencePx > 0 && /* @__PURE__ */ jsx(
9112
- reactNative.View,
9113
- {
9114
- accessibilityElementsHidden: true,
9115
- style: {
9116
- position: "absolute",
9117
- left: 0,
9118
- right: 0,
9119
- bottom: referencePx,
9120
- borderTopWidth: 1,
9121
- borderStyle: "dashed",
9122
- borderColor: HERO_REFERENCE_COLOR,
9123
- pointerEvents: "none"
9124
- },
9125
- testID: "velocity-hero-reference"
9126
- }
9127
- ),
9128
- doneVelocities.map((velocity, i) => {
9129
- const isLive = liveRepIndex === i;
9130
- return /* @__PURE__ */ jsxs(
9131
- reactNative.View,
9132
- {
9133
- accessibilityElementsHidden: true,
9134
- style: {
9135
- flex: 1,
9136
- maxWidth: HERO_BAR_MAX_WIDTH,
9137
- height: "100%",
9138
- alignItems: "center",
9139
- justifyContent: "flex-end"
9140
- },
9141
- children: [
9142
- showBarLabel(i) && /* @__PURE__ */ jsx(
9143
- reactNative.Text,
9144
- {
9145
- className: "text-text-primary",
9146
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
9147
- testID: `velocity-label-${i}`,
9148
- children: formatVelocity(velocity)
9149
- }
9150
- ),
9151
- /* @__PURE__ */ jsx(
9152
- reactNative.Animated.View,
9153
- {
9154
- style: [
9155
- {
9156
- width: "100%",
9157
- height: barHeight(velocity),
9158
- borderTopLeftRadius: HERO_BAR_RADIUS,
9159
- borderTopRightRadius: HERO_BAR_RADIUS,
9160
- backgroundColor: barColorFor(velocity)
9161
- },
9162
- isLive ? { transform: [{ scale: liveScale }] } : null
9163
- ],
9164
- testID: `velocity-bar-${i}`
9165
- }
9166
- )
9167
- ]
9168
- },
9169
- i
9170
- );
9171
- }),
9172
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
9173
- reactNative.View,
9174
- {
9175
- accessibilityElementsHidden: true,
9176
- style: {
9177
- flex: 1,
9178
- maxWidth: HERO_BAR_MAX_WIDTH,
9179
- height: "100%",
9180
- alignItems: "center",
9181
- justifyContent: "flex-end"
9182
- },
9183
- children: /* @__PURE__ */ jsx(
9184
- reactNative.View,
9185
- {
9186
- style: {
9187
- width: "100%",
9188
- height: HERO_MIN_BAR_HEIGHT * 3,
9189
- borderWidth: 1,
9190
- borderStyle: "dashed",
9191
- borderColor: HERO_PENDING_COLOR,
9192
- borderTopLeftRadius: HERO_BAR_RADIUS,
9193
- borderTopRightRadius: HERO_BAR_RADIUS
9194
- },
9195
- testID: "velocity-slot-todo"
9196
- }
9197
- )
9198
- },
9199
- `pending-${i}`
9200
- ))
9201
- ]
9202
- }
9203
- )
9459
+ children: [
9460
+ /* @__PURE__ */ jsx(
9461
+ ChartSideRail,
9462
+ {
9463
+ sectionExtent: plotHalf,
9464
+ variant: "hero",
9465
+ sections: [
9466
+ { label: leftLabel, testID: "dual-velocity-side-label-L" },
9467
+ { label: rightLabel, testID: "dual-velocity-side-label-R" }
9468
+ ]
9469
+ }
9470
+ ),
9471
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1, gap: DUAL_WING_GAP }, children: [
9472
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9473
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9474
+ ] })
9475
+ ]
9476
+ }
9477
+ );
9478
+ }
9479
+ function DualVelocityRail({
9480
+ leftDone,
9481
+ rightDone,
9482
+ leftStream,
9483
+ rightStream,
9484
+ zones,
9485
+ barColor: barColor2,
9486
+ scale,
9487
+ targetReps,
9488
+ liveRepIndex,
9489
+ height,
9490
+ className,
9491
+ label,
9492
+ viewProps
9493
+ }) {
9494
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9495
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9496
+ const aligned = alignDualSlots(
9497
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9498
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9499
+ );
9500
+ const { style: externalStyle, ...restProps } = viewProps;
9501
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9502
+ VelocityStrip,
9503
+ {
9504
+ variant: "expanded",
9505
+ showNumbers: false,
9506
+ showInfo: false,
9507
+ orientation,
9508
+ velocities: done,
9509
+ columnSlots: columns,
9510
+ scale,
9511
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9512
+ barColor: barColor2,
9513
+ zones,
9514
+ liveRepIndex,
9515
+ height: plotHalf
9516
+ }
9517
+ );
9518
+ return /* @__PURE__ */ jsxs(
9519
+ reactNative.View,
9520
+ {
9521
+ className,
9522
+ style: [{ height, flexDirection: "column", gap: DUAL_WING_GAP }, externalStyle],
9523
+ accessibilityRole: "image",
9524
+ accessibilityLabel: label,
9525
+ testID: "dual-velocity-strip",
9526
+ ...restProps,
9527
+ children: [
9528
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9529
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9530
+ ]
9531
+ }
9532
+ );
9533
+ }
9534
+ function DualVelocityCompactStrip({
9535
+ leftDone,
9536
+ rightDone,
9537
+ leftStream,
9538
+ rightStream,
9539
+ zones,
9540
+ barColor: barColor2,
9541
+ targetReps,
9542
+ height,
9543
+ className,
9544
+ label,
9545
+ viewProps
9546
+ }) {
9547
+ const half = height === DUAL_COMPACT_HEIGHT ? COMPACT_FOLD_HALF : (height - COMPACT_FOLD_GAP) / 2;
9548
+ const aligned = alignDualSlots(
9549
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9550
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9551
+ );
9552
+ const { style: externalStyle, ...restProps } = viewProps;
9553
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9554
+ VelocityStrip,
9555
+ {
9556
+ variant: "compact",
9557
+ orientation,
9558
+ velocities: done,
9559
+ columnSlots: columns,
9560
+ barColor: barColor2,
9561
+ zones,
9562
+ height: half
9563
+ }
9564
+ );
9565
+ return /* @__PURE__ */ jsxs(
9566
+ reactNative.View,
9567
+ {
9568
+ className,
9569
+ style: [{ height, flexDirection: "column", gap: COMPACT_FOLD_GAP }, externalStyle],
9570
+ accessibilityRole: "image",
9571
+ accessibilityLabel: label,
9572
+ testID: "dual-velocity-strip",
9573
+ ...restProps,
9574
+ children: [
9575
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9576
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9577
+ ]
9204
9578
  }
9205
9579
  );
9206
9580
  }
9581
+ function DualVelocityStrip({
9582
+ left,
9583
+ right,
9584
+ zones,
9585
+ barColor: barColor2 = "loss",
9586
+ targetReps,
9587
+ liveRepIndex,
9588
+ variant = "hero",
9589
+ scale = "peak",
9590
+ height,
9591
+ className,
9592
+ ...props
9593
+ }) {
9594
+ const resolvedHeight = height ?? (variant === "hero" ? DUAL_HERO_HEIGHT : variant === "compact" ? DUAL_COMPACT_HEIGHT : DUAL_RAIL_HEIGHT);
9595
+ const leftDone = streamDone(left);
9596
+ const rightDone = streamDone(right);
9597
+ const label = `Dual velocity chart, left ${leftDone.length} of ${Math.max(leftDone.length, targetReps ?? leftDone.length)} reps, right ${rightDone.length} of ${Math.max(rightDone.length, targetReps ?? rightDone.length)} reps`;
9598
+ const shared = {
9599
+ leftDone,
9600
+ rightDone,
9601
+ leftStream: left,
9602
+ rightStream: right,
9603
+ leftLabel: left.label,
9604
+ rightLabel: right.label,
9605
+ zones,
9606
+ barColor: barColor2,
9607
+ scale,
9608
+ targetReps,
9609
+ height: resolvedHeight,
9610
+ className,
9611
+ label,
9612
+ viewProps: props
9613
+ };
9614
+ if (variant === "rail") return /* @__PURE__ */ jsx(DualVelocityRail, { ...shared, liveRepIndex });
9615
+ if (variant === "compact") return /* @__PURE__ */ jsx(DualVelocityCompactStrip, { ...shared });
9616
+ return /* @__PURE__ */ jsx(DualVelocityHero, { ...shared, liveRepIndex });
9617
+ }
9207
9618
  function VelocityStrip({
9208
9619
  velocities,
9209
9620
  set,
9210
9621
  zones,
9622
+ barColor: barColor2 = "loss",
9623
+ showLossBands,
9624
+ orientation = "up",
9625
+ scaleMax,
9626
+ hideBaseline,
9627
+ columnSlots,
9628
+ label,
9211
9629
  liveRepIndex,
9212
9630
  expanded = true,
9213
9631
  onToggle,
@@ -9221,51 +9639,29 @@ function VelocityStrip({
9221
9639
  className,
9222
9640
  ...props
9223
9641
  }) {
9642
+ const lossBandsOn = showLossBands ?? barColor2 === "loss";
9224
9643
  const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
9225
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
9226
- kind: "rep",
9227
- velocity: v,
9228
- leadingGap: i === 0 ? 0 : REP_GAP
9229
- }));
9230
9644
  const maxVelocity = Math.max(...doneVelocities, 0);
9231
9645
  const meanVelocity = calculateMeanVelocity(doneVelocities);
9232
9646
  const loss = calculateVelocityLoss(doneVelocities);
9233
9647
  const framed = showNumbers || showInfo;
9234
- const scaleDenom = scaleDenominator(scale, maxVelocity);
9235
9648
  const hasZones = zones != null && zones.length > 0;
9236
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9237
- const slotColor = (slot) => {
9238
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
9239
- if (slot.kind === "todo") return TODO_COLOR2;
9240
- return SET_STRIP_VARIABLE_COLOR;
9241
- };
9649
+ const zoneColorFor = makeBarColorFor(zones);
9650
+ const barColorFor = (v) => barColor2 === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
9242
9651
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
9243
- const [heightAnim] = React25.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
9244
- const [labelOpacity] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9652
+ const [expandProgress] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9245
9653
  const [infoOpacity] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9246
9654
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9247
9655
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
9248
- const liveScale = useLiveRepPop(
9249
- variant === "expanded" && framed,
9250
- liveRepIndex,
9251
- liveVelocity,
9252
- isNewPeak
9253
- );
9254
9656
  React25.useEffect(() => {
9255
9657
  if (variant !== "expanded" || !framed) return;
9256
9658
  reactNative.Animated.parallel([
9257
- reactNative.Animated.timing(heightAnim, {
9258
- toValue: expanded ? height : 3,
9659
+ reactNative.Animated.timing(expandProgress, {
9660
+ toValue: expanded ? 1 : 0,
9259
9661
  duration: ANIMATION_DURATION,
9260
9662
  easing: ANIMATION_EASING,
9261
9663
  useNativeDriver: false
9262
9664
  }),
9263
- reactNative.Animated.timing(labelOpacity, {
9264
- toValue: expanded ? 1 : 0,
9265
- duration: 300,
9266
- delay: expanded ? 150 : 0,
9267
- useNativeDriver: false
9268
- }),
9269
9665
  reactNative.Animated.timing(infoOpacity, {
9270
9666
  toValue: expanded ? 1 : 0,
9271
9667
  duration: 300,
@@ -9273,250 +9669,176 @@ function VelocityStrip({
9273
9669
  useNativeDriver: false
9274
9670
  })
9275
9671
  ]).start();
9276
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
9672
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
9277
9673
  if (set == null && velocities == null) return null;
9278
9674
  const repCount = doneVelocities.length;
9279
9675
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
9280
9676
  if (variant === "hero") {
9281
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
9677
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
9678
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9679
+ kind: s.kind,
9680
+ value: s.velocity,
9681
+ leadingGap: s.leadingGap
9682
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9683
+ const total = Math.max(repCount, targetReps ?? repCount);
9684
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9282
9685
  return /* @__PURE__ */ jsx(
9283
- HeroVelocityChart,
9686
+ SetBarChart,
9284
9687
  {
9285
- doneVelocities,
9286
- barColorFor,
9287
- scaleDenom,
9288
- referenceVelocity: maxVelocity,
9688
+ slots: heroSlots,
9689
+ colorFor: barColorFor,
9690
+ height: heroHeight,
9691
+ scale,
9692
+ scaleMax,
9693
+ orientation,
9289
9694
  liveRepIndex,
9290
9695
  isNewPeak,
9291
- targetReps,
9292
- height: heroHeight,
9696
+ targetReps: set || columnSlots ? void 0 : targetReps,
9697
+ label,
9698
+ showValueLabels: true,
9699
+ formatValue: formatVelocity,
9700
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
9701
+ hideBaseline: true,
9702
+ testID: "velocity-strip-hero",
9703
+ testIDPrefix: "velocity",
9704
+ accessibilityLabel: heroLabel,
9293
9705
  className,
9294
9706
  viewProps: props
9295
9707
  }
9296
9708
  );
9297
9709
  }
9298
- if (variant === "mini") {
9299
- const { style: externalStyle, ...restProps } = props;
9710
+ if (variant === "compact") {
9711
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9712
+ kind: s.kind,
9713
+ value: s.velocity,
9714
+ leadingGap: s.leadingGap
9715
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9716
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
9300
9717
  return /* @__PURE__ */ jsx(
9301
- reactNative.View,
9718
+ SetBarChart,
9302
9719
  {
9303
- className,
9304
- style: [
9305
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
9306
- externalStyle
9307
- ],
9308
- accessibilityRole: "image",
9720
+ slots: compactSlots,
9721
+ colorFor: barColorFor,
9722
+ height: compactHeight,
9723
+ scale,
9724
+ scaleMax,
9725
+ orientation,
9726
+ flat: true,
9727
+ barRadius: 2,
9728
+ cornerStyle: "all",
9729
+ targetReps: set || columnSlots ? void 0 : targetReps,
9730
+ label,
9731
+ hideBaseline: true,
9732
+ testID: "velocity-strip-compact",
9733
+ testIDPrefix: "velocity",
9309
9734
  accessibilityLabel: miniLabel,
9310
- testID: "velocity-strip-mini",
9311
- ...restProps,
9312
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9313
- reactNative.View,
9314
- {
9315
- style: {
9316
- flex: 1,
9317
- backgroundColor: slotColor(slot),
9318
- borderRadius: 1,
9319
- minWidth: 4,
9320
- height: "100%",
9321
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
9322
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
9323
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9324
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9325
- },
9326
- accessibilityElementsHidden: true,
9327
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9328
- },
9329
- i
9330
- ))
9735
+ className,
9736
+ viewProps: props
9331
9737
  }
9332
9738
  );
9333
9739
  }
9334
9740
  if (!framed) {
9335
- const { style: externalStyle, ...restProps } = props;
9741
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9742
+ kind: s.kind,
9743
+ value: s.velocity,
9744
+ leadingGap: s.leadingGap
9745
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9336
9746
  return /* @__PURE__ */ jsx(
9337
- reactNative.View,
9747
+ SetBarChart,
9338
9748
  {
9339
- className,
9340
- style: [
9341
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
9342
- externalStyle
9343
- ],
9344
- accessibilityRole: "image",
9749
+ slots: spotlightSlots,
9750
+ colorFor: barColorFor,
9751
+ height,
9752
+ scale,
9753
+ scaleMax,
9754
+ orientation,
9755
+ liveRepIndex,
9756
+ isNewPeak,
9757
+ barRadius: 2,
9758
+ cornerStyle: "top",
9759
+ targetReps: set ? void 0 : targetReps,
9760
+ label,
9761
+ hideBaseline: true,
9762
+ testID: "velocity-strip-spotlight",
9763
+ testIDPrefix: "velocity",
9345
9764
  accessibilityLabel: miniLabel,
9346
- testID: "velocity-strip-compact",
9347
- ...restProps,
9348
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9349
- reactNative.View,
9350
- {
9351
- style: {
9352
- flex: 1,
9353
- minWidth: 4,
9354
- height: bareSlotHeight(slot, height, scaleDenom),
9355
- // Top-rounded to match the framed chart's bars (the "rounded tops from
9356
- // the numbers one"); square bottoms since bars sit on the baseline.
9357
- borderTopLeftRadius: 2,
9358
- borderTopRightRadius: 2,
9359
- backgroundColor: slotColor(slot),
9360
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9361
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9362
- },
9363
- accessibilityElementsHidden: true,
9364
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9365
- },
9366
- i
9367
- ))
9765
+ className,
9766
+ viewProps: props
9368
9767
  }
9369
9768
  );
9370
9769
  }
9371
9770
  const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
9372
9771
  const hasInteractiveContainer = onToggle != null;
9373
9772
  const hasInteractiveReps = onRepPress != null && expanded;
9374
- const setBars = set != null && slots.map((slot, i) => {
9375
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
9376
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
9377
- return /* @__PURE__ */ jsx(
9378
- reactNative.View,
9773
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
9774
+ kind: sl.kind,
9775
+ value: sl.velocity,
9776
+ leadingGap: sl.leadingGap
9777
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
9778
+ const needsBarOverlay = showNumbers || onRepPress != null;
9779
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9780
+ showNumbers && /* @__PURE__ */ jsx(
9781
+ reactNative.Animated.View,
9379
9782
  {
9380
9783
  style: {
9381
- flex: 1,
9382
- height: "100%",
9383
- justifyContent: "flex-end",
9384
- marginLeft: slot.leadingGap
9784
+ opacity: expandProgress,
9785
+ position: "absolute",
9786
+ top: -13,
9787
+ left: 0,
9788
+ right: 0,
9789
+ alignItems: "center"
9385
9790
  },
9791
+ accessibilityElementsHidden: true,
9792
+ pointerEvents: "none",
9386
9793
  children: /* @__PURE__ */ jsx(
9387
- reactNative.View,
9794
+ reactNative.Text,
9388
9795
  {
9389
- style: {
9390
- height: `${heightPct}%`,
9391
- minHeight: 2,
9392
- borderTopLeftRadius: 2,
9393
- borderTopRightRadius: 2,
9394
- backgroundColor: slotColor(slot),
9395
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9396
- },
9397
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
9398
- accessibilityRole: "image",
9399
- accessibilityLabel: slotAccessibilityLabel(slot, i)
9796
+ className: "text-text-secondary",
9797
+ style: { fontSize: 8, fontWeight: "600" },
9798
+ testID: `velocity-label-${repIndex}`,
9799
+ children: formatVelocity(value)
9400
9800
  }
9401
9801
  )
9402
- },
9403
- i
9404
- );
9405
- });
9802
+ }
9803
+ ),
9804
+ onRepPress && expanded && /* @__PURE__ */ jsx(
9805
+ reactNative.Pressable,
9806
+ {
9807
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
9808
+ onPress: () => onRepPress(repIndex, value),
9809
+ accessibilityRole: "button",
9810
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
9811
+ testID: `velocity-bar-pressable-${repIndex}`
9812
+ }
9813
+ )
9814
+ ] }) : void 0;
9406
9815
  const stripContent = /* @__PURE__ */ jsxs(
9407
9816
  reactNative.Animated.View,
9408
9817
  {
9409
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
9410
- style: [
9411
- {
9412
- height: heightAnim,
9413
- width: "100%",
9414
- gap: 2,
9415
- borderRadius: expanded ? 6 : 2,
9416
- paddingTop: expanded ? 16 : 0,
9417
- paddingBottom: expanded ? 24 : 0,
9418
- // No horizontal inset — the framed chart's bars span full width, matching
9419
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
9420
- paddingHorizontal: 0,
9421
- paddingVertical: expanded ? void 0 : 8,
9422
- overflow: expanded ? "visible" : "hidden"
9423
- }
9424
- ],
9818
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
9819
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
9425
9820
  accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
9426
9821
  accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
9427
9822
  testID: "velocity-strip",
9428
9823
  ...props,
9429
9824
  children: [
9430
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
9431
- const barBackground = barColorFor(v);
9432
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
9433
- const isLive = liveRepIndex === i;
9434
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
9435
- const barStyle = expanded ? {
9436
- height: `${barHeightPct}%`,
9437
- minHeight: 2,
9438
- borderTopLeftRadius: 2,
9439
- borderTopRightRadius: 2,
9440
- backgroundColor: barBackground
9441
- } : {
9442
- minHeight: "100%",
9443
- borderTopLeftRadius: 2,
9444
- borderTopRightRadius: 2,
9445
- borderBottomLeftRadius: 0,
9446
- borderBottomRightRadius: 0,
9447
- backgroundColor: barBackground
9448
- };
9449
- const barInner = isLive ? /* @__PURE__ */ jsx(
9450
- reactNative.Animated.View,
9451
- {
9452
- style: [barStyle, { transform: [{ scale: liveScale }] }],
9453
- testID: `velocity-bar-${i}`,
9454
- accessibilityRole: "image",
9455
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
9456
- }
9457
- ) : /* @__PURE__ */ jsx(
9458
- reactNative.View,
9459
- {
9460
- style: barStyle,
9461
- testID: `velocity-bar-${i}`,
9462
- accessibilityRole: "image",
9463
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
9464
- }
9465
- );
9466
- const bar = /* @__PURE__ */ jsxs(
9467
- reactNative.View,
9468
- {
9469
- style: {
9470
- flex: 1,
9471
- height: "100%",
9472
- justifyContent: "flex-end",
9473
- position: "relative"
9474
- },
9475
- children: [
9476
- expanded && showNumbers && /* @__PURE__ */ jsx(
9477
- reactNative.Animated.View,
9478
- {
9479
- style: {
9480
- opacity: labelOpacity,
9481
- alignItems: "center",
9482
- position: "absolute",
9483
- top: -13,
9484
- left: 0,
9485
- right: 0
9486
- },
9487
- accessibilityElementsHidden: true,
9488
- children: /* @__PURE__ */ jsx(
9489
- reactNative.Text,
9490
- {
9491
- className: "text-text-secondary",
9492
- style: { fontSize: 8, fontWeight: "600" },
9493
- testID: `velocity-label-${i}`,
9494
- children: formatVelocity(v)
9495
- }
9496
- )
9497
- }
9498
- ),
9499
- barInner
9500
- ]
9501
- },
9502
- i
9503
- );
9504
- if (onRepPress && expanded) {
9505
- return /* @__PURE__ */ jsx(
9506
- reactNative.Pressable,
9507
- {
9508
- style: { flex: 1, height: "100%" },
9509
- onPress: () => onRepPress(i, v),
9510
- accessibilityRole: "button",
9511
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
9512
- testID: `velocity-bar-pressable-${i}`,
9513
- children: bar
9514
- },
9515
- i
9516
- );
9825
+ /* @__PURE__ */ jsx(
9826
+ SetBarChart,
9827
+ {
9828
+ slots: framedSlots,
9829
+ colorFor: barColorFor,
9830
+ height,
9831
+ scale,
9832
+ scaleMax,
9833
+ expandProgress,
9834
+ renderBarOverlay: renderFramedBarOverlay,
9835
+ targetReps: set ? void 0 : targetReps,
9836
+ barRadius: 2,
9837
+ cornerStyle: "top",
9838
+ hideBaseline: true,
9839
+ testIDPrefix: "velocity"
9517
9840
  }
9518
- return bar;
9519
- }) }),
9841
+ ),
9520
9842
  expanded && showInfo && /* @__PURE__ */ jsxs(
9521
9843
  reactNative.Animated.View,
9522
9844
  {
@@ -9524,10 +9846,8 @@ function VelocityStrip({
9524
9846
  flexDirection: "row",
9525
9847
  justifyContent: "space-between",
9526
9848
  opacity: infoOpacity,
9527
- position: "absolute",
9528
- bottom: 4,
9529
- left: 6,
9530
- right: 6
9849
+ marginTop: 6,
9850
+ paddingHorizontal: 6
9531
9851
  },
9532
9852
  testID: "velocity-info-row",
9533
9853
  children: [
@@ -9535,10 +9855,7 @@ function VelocityStrip({
9535
9855
  reactNative.Text,
9536
9856
  {
9537
9857
  className: "text-text-secondary",
9538
- style: {
9539
- fontSize: 10,
9540
- fontFamily: "Inter, sans-serif"
9541
- },
9858
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
9542
9859
  children: [
9543
9860
  meanZone,
9544
9861
  " ",
@@ -9553,11 +9870,7 @@ function VelocityStrip({
9553
9870
  reactNative.Text,
9554
9871
  {
9555
9872
  className: "text-text-secondary",
9556
- style: {
9557
- fontSize: 10,
9558
- fontFamily: "Inter, sans-serif",
9559
- ...getLossStyle(loss)
9560
- },
9873
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
9561
9874
  children: [
9562
9875
  "Loss: ",
9563
9876
  loss,
@@ -9585,13 +9898,13 @@ function VelocityStrip({
9585
9898
  }
9586
9899
  return stripContent;
9587
9900
  }
9588
- var t6 = getSemanticColors("dark");
9901
+ var t7 = getSemanticColors("dark");
9589
9902
  var dotColorMap = {
9590
9903
  untrained: "#2C2C2C",
9591
- behind: t6["brand-secondary"],
9592
- ontrack: t6["status-success"],
9593
- target: t6["brand-primary"],
9594
- over: t6["status-error"]
9904
+ behind: t7["brand-secondary"],
9905
+ ontrack: t7["status-success"],
9906
+ target: t7["brand-primary"],
9907
+ over: t7["status-error"]
9595
9908
  };
9596
9909
  function MuscleGroupChip({
9597
9910
  name,
@@ -9865,6 +10178,7 @@ function RowStrip({ set }) {
9865
10178
  showInfo: false,
9866
10179
  height: 24,
9867
10180
  scale: "fixed",
10181
+ liveRepIndex: set.velocities.length - 1,
9868
10182
  set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
9869
10183
  zones
9870
10184
  }
@@ -9874,7 +10188,9 @@ function RowStrip({ set }) {
9874
10188
  return /* @__PURE__ */ jsx(
9875
10189
  VelocityStrip,
9876
10190
  {
9877
- variant: "mini",
10191
+ variant: "compact",
10192
+ height: 8,
10193
+ hideBaseline: true,
9878
10194
  set: { type: "straight", velocities, planned: plannedReps(set) },
9879
10195
  zones
9880
10196
  }
@@ -10294,7 +10610,7 @@ function RestTimer({
10294
10610
  }
10295
10611
  );
10296
10612
  }
10297
- var t7 = getSemanticColors("dark");
10613
+ var t8 = getSemanticColors("dark");
10298
10614
  var TIMES = "\xD7";
10299
10615
  var AT = "@";
10300
10616
  function SetsRepsLoad({
@@ -10306,8 +10622,8 @@ function SetsRepsLoad({
10306
10622
  className,
10307
10623
  ...props
10308
10624
  }) {
10309
- const value = t7["text-primary"];
10310
- const sep = t7["text-tertiary"];
10625
+ const value = t8["text-primary"];
10626
+ const sep = t8["text-tertiary"];
10311
10627
  return /* @__PURE__ */ jsxs(
10312
10628
  reactNative.View,
10313
10629
  {
@@ -10327,6 +10643,107 @@ function SetsRepsLoad({
10327
10643
  }
10328
10644
  );
10329
10645
  }
10646
+ var PULSE_HALF_MS = 950;
10647
+ var PULSE_MIN_OPACITY = 0.45;
10648
+ var SEGMENTED_BAR_GAP = 5;
10649
+ function usePulse2(active) {
10650
+ const [value] = React25.useState(() => new reactNative.Animated.Value(0));
10651
+ React25.useEffect(() => {
10652
+ if (!active) return;
10653
+ const loop = reactNative.Animated.loop(
10654
+ reactNative.Animated.sequence([
10655
+ reactNative.Animated.timing(value, {
10656
+ toValue: 1,
10657
+ duration: PULSE_HALF_MS,
10658
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
10659
+ useNativeDriver: false
10660
+ }),
10661
+ reactNative.Animated.timing(value, {
10662
+ toValue: 0,
10663
+ duration: PULSE_HALF_MS,
10664
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
10665
+ useNativeDriver: false
10666
+ })
10667
+ ])
10668
+ );
10669
+ loop.start();
10670
+ return () => loop.stop();
10671
+ }, [active, value]);
10672
+ return value;
10673
+ }
10674
+ function SegmentedBar({
10675
+ segments,
10676
+ height = 8,
10677
+ gap = SEGMENTED_BAR_GAP,
10678
+ radius = 2,
10679
+ marker = null,
10680
+ segmentTestID,
10681
+ style,
10682
+ ...props
10683
+ }) {
10684
+ const pulse = usePulse2(segments.some((s) => s.pulse));
10685
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
10686
+ segments.map((seg, i) => {
10687
+ const fillStyle = {
10688
+ width: `${(seg.fill ?? 1) * 100}%`,
10689
+ height: "100%",
10690
+ backgroundColor: seg.color,
10691
+ opacity: seg.opacity
10692
+ };
10693
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
10694
+ return /* @__PURE__ */ jsx(
10695
+ reactNative.View,
10696
+ {
10697
+ style: {
10698
+ flex: seg.weight ?? 1,
10699
+ minWidth: 0,
10700
+ height: "100%",
10701
+ marginLeft: seg.leadingGap,
10702
+ borderRadius: radius,
10703
+ overflow: "hidden"
10704
+ },
10705
+ children: seg.pulse ? /* @__PURE__ */ jsx(
10706
+ reactNative.Animated.View,
10707
+ {
10708
+ style: {
10709
+ ...fillStyle,
10710
+ ...seg.pulseColor ? {
10711
+ backgroundColor: pulse.interpolate({
10712
+ inputRange: [0, 1],
10713
+ outputRange: seg.pulseColor
10714
+ })
10715
+ } : {
10716
+ opacity: pulse.interpolate({
10717
+ inputRange: [0, 1],
10718
+ outputRange: [PULSE_MIN_OPACITY, 1]
10719
+ })
10720
+ }
10721
+ },
10722
+ accessibilityElementsHidden: true,
10723
+ testID
10724
+ }
10725
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
10726
+ },
10727
+ i
10728
+ );
10729
+ }),
10730
+ marker ? /* @__PURE__ */ jsx(
10731
+ reactNative.View,
10732
+ {
10733
+ style: {
10734
+ position: "absolute",
10735
+ left: `${marker.position * 100}%`,
10736
+ top: 0,
10737
+ bottom: 0,
10738
+ width: 2,
10739
+ backgroundColor: marker.color
10740
+ },
10741
+ accessibilityElementsHidden: true,
10742
+ testID: "segmented-bar-marker"
10743
+ }
10744
+ ) : null
10745
+ ] });
10746
+ }
10330
10747
 
10331
10748
  // src/components/custom/Workout/paceTone.ts
10332
10749
  function paceTone(progress, target) {
@@ -10420,75 +10837,209 @@ function ScheduleTiles({ when, now, gap = 1, ...props }) {
10420
10837
  )
10421
10838
  ] });
10422
10839
  }
10423
- var t8 = getSemanticColors("dark");
10424
- var INDICATOR_CONFIG = {
10425
- imbalance: { Icon: ScaleIcon, color: t8["status-error"], label: "Left/right imbalance" },
10426
- overshoot: { Icon: AlertTriangleIcon, color: t8["status-error"], label: "Load overshoot" },
10427
- "velocity-loss": { Icon: TrendingDownIcon, color: t8["status-warning"], label: "Velocity loss" },
10428
- "missed-reps": { Icon: CircleSlashIcon, color: t8["status-warning"], label: "Missed reps" },
10429
- pr: { Icon: AwardIcon, color: t8["status-success"], label: "Personal record" },
10430
- info: { Icon: InfoIcon, color: t8["status-info"], label: "More info" }
10840
+
10841
+ // src/components/custom/Workout/SetBar.tsx
10842
+ var SET_STRIP_ZONES = {
10843
+ slow: primitiveRamps.red[600],
10844
+ moderate: primitiveRamps.orange[400],
10845
+ fast: primitiveRamps.amber[300],
10846
+ fastest: primitiveRamps.green[300]
10431
10847
  };
10432
- var INDICATOR_PRECEDENCE = [
10433
- "imbalance",
10434
- "overshoot",
10435
- "velocity-loss",
10436
- "missed-reps",
10437
- "pr",
10438
- "info"
10439
- ];
10440
- function resolveIndicator(candidates) {
10441
- return INDICATOR_PRECEDENCE.find((kind) => candidates.includes(kind));
10848
+ var TODO_COLOR = primitiveColors.charcoal[300];
10849
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
10850
+ var NOTCH_GAP = 2;
10851
+ var CLUSTER_GAP = 3;
10852
+ var MYO_UPCOMING_CLUSTERS = 3;
10853
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
10854
+ var MYO_UPCOMING_FADE = 0.6;
10855
+ var PULSE_RANGE = {
10856
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
10857
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
10858
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
10859
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
10860
+ };
10861
+ function velocityZoneColor(v) {
10862
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
10863
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
10864
+ if (v < 1) return SET_STRIP_ZONES.fast;
10865
+ return SET_STRIP_ZONES.fastest;
10442
10866
  }
10443
- function ExerciseIndicator({ kind, onPress, className, ...props }) {
10444
- const { Icon, color, label } = INDICATOR_CONFIG[kind];
10445
- const chip = /* @__PURE__ */ jsx(
10446
- reactNative.View,
10447
- {
10448
- className,
10449
- style: {
10450
- width: 16,
10451
- height: 16,
10452
- borderRadius: 8,
10453
- borderWidth: 1,
10454
- borderColor: color,
10455
- alignItems: "center",
10456
- justifyContent: "center"
10457
- },
10458
- accessibilityRole: "image",
10459
- accessibilityLabel: label,
10460
- testID: "exercise-indicator",
10461
- ...props,
10462
- children: /* @__PURE__ */ jsx(Icon, { size: 11, color })
10463
- }
10867
+ function chunkedSegments(chunks, gap) {
10868
+ return chunks.flatMap(
10869
+ (vels, ci) => vels.map((v, ri) => ({
10870
+ color: velocityZoneColor(v),
10871
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
10872
+ }))
10464
10873
  );
10465
- if (onPress) {
10466
- return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, children: chip });
10874
+ }
10875
+ function rangeSegments(floor, max, doneVels) {
10876
+ return Array.from({ length: max }, (_, i) => {
10877
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
10878
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
10879
+ });
10880
+ }
10881
+ function myoUpcomingSegments(activationLen, clusterCount) {
10882
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
10883
+ const trail = Array.from(
10884
+ { length: clusterCount },
10885
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
10886
+ color: SET_STRIP_VARIABLE_COLOR,
10887
+ opacity: MYO_UPCOMING_FADE ** ci,
10888
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
10889
+ }))
10890
+ ).flat();
10891
+ return [...activation, ...trail];
10892
+ }
10893
+ function setSegments(set) {
10894
+ if (set.status === "todo") {
10895
+ return [{ color: TODO_COLOR }];
10467
10896
  }
10468
- return chip;
10897
+ if (set.status === "done") {
10898
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
10899
+ }
10900
+ if (set.status === "range") {
10901
+ return rangeSegments(set.floor, set.max, set.doneVels);
10902
+ }
10903
+ if (set.status === "drop") {
10904
+ return chunkedSegments(set.subloads, NOTCH_GAP);
10905
+ }
10906
+ if (set.status === "myo") {
10907
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
10908
+ }
10909
+ if (set.status === "myo-upcoming") {
10910
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
10911
+ }
10912
+ const performed = set.velocities.map((v) => {
10913
+ const color = velocityZoneColor(v);
10914
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
10915
+ });
10916
+ const remaining = Math.max(0, set.planned - set.velocities.length);
10917
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
10469
10918
  }
10470
- function headingLabel(name, sets, reps, load, unit) {
10471
- const loadLabel = typeof load === "number" ? roundWeight(load) : load;
10472
- return `${name}, ${sets}\xD7${reps} @ ${loadLabel} ${unit}`;
10919
+ function setBarSegmentTestID(seg) {
10920
+ if (seg.pulse) return "set-strip-pulse";
10921
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
10922
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
10473
10923
  }
10474
- function ExerciseHeading({
10475
- name,
10476
- sets,
10477
- reps,
10478
- load,
10479
- unit = "lbs",
10480
- tempo,
10481
- indicator,
10482
- dimmed,
10483
- onPress
10484
- }) {
10485
- return /* @__PURE__ */ jsxs(reactNative.View, { style: { opacity: dimmed ? 0.55 : 1 }, testID: "exercise-heading", children: [
10486
- /* @__PURE__ */ jsxs(
10487
- reactNative.Pressable,
10488
- {
10489
- onPress,
10490
- accessibilityRole: "button",
10491
- accessibilityLabel: headingLabel(name, sets, reps, load, unit),
10924
+ function SetBar({ set, height = 8 }) {
10925
+ return /* @__PURE__ */ jsx(
10926
+ SegmentedBar,
10927
+ {
10928
+ segments: setSegments(set),
10929
+ height,
10930
+ gap: 0,
10931
+ radius: 0,
10932
+ segmentTestID: setBarSegmentTestID,
10933
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
10934
+ testID: "set-strip-set"
10935
+ }
10936
+ );
10937
+ }
10938
+ var SET_STRIP_GAP = 5;
10939
+ function setCategory(set) {
10940
+ switch (set.status) {
10941
+ case "done":
10942
+ case "drop":
10943
+ case "myo":
10944
+ return "done";
10945
+ case "active":
10946
+ return "active";
10947
+ case "range":
10948
+ return set.doneVels.length > 0 ? "active" : "upcoming";
10949
+ case "todo":
10950
+ case "myo-upcoming":
10951
+ return "upcoming";
10952
+ }
10953
+ }
10954
+ function describeSets(sets) {
10955
+ const done = sets.filter((s) => setCategory(s) === "done").length;
10956
+ const active = sets.filter((s) => setCategory(s) === "active").length;
10957
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
10958
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
10959
+ }
10960
+ function SetStrip({ sets, height = 8, className, ...props }) {
10961
+ return /* @__PURE__ */ jsx(
10962
+ reactNative.View,
10963
+ {
10964
+ className,
10965
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
10966
+ accessibilityRole: "image",
10967
+ accessibilityLabel: describeSets(sets),
10968
+ testID: "set-strip",
10969
+ ...props,
10970
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
10971
+ }
10972
+ );
10973
+ }
10974
+ var t9 = getSemanticColors("dark");
10975
+ var INDICATOR_CONFIG = {
10976
+ imbalance: { Icon: ScaleIcon, color: t9["status-error"], label: "Left/right imbalance" },
10977
+ overshoot: { Icon: AlertTriangleIcon, color: t9["status-error"], label: "Load overshoot" },
10978
+ "velocity-loss": { Icon: TrendingDownIcon, color: t9["status-warning"], label: "Velocity loss" },
10979
+ "missed-reps": { Icon: CircleSlashIcon, color: t9["status-warning"], label: "Missed reps" },
10980
+ pr: { Icon: AwardIcon, color: t9["status-success"], label: "Personal record" },
10981
+ info: { Icon: InfoIcon, color: t9["status-info"], label: "More info" }
10982
+ };
10983
+ var INDICATOR_PRECEDENCE = [
10984
+ "imbalance",
10985
+ "overshoot",
10986
+ "velocity-loss",
10987
+ "missed-reps",
10988
+ "pr",
10989
+ "info"
10990
+ ];
10991
+ function resolveIndicator(candidates) {
10992
+ return INDICATOR_PRECEDENCE.find((kind) => candidates.includes(kind));
10993
+ }
10994
+ function ExerciseIndicator({ kind, onPress, className, ...props }) {
10995
+ const { Icon, color, label } = INDICATOR_CONFIG[kind];
10996
+ const chip = /* @__PURE__ */ jsx(
10997
+ reactNative.View,
10998
+ {
10999
+ className,
11000
+ style: {
11001
+ width: 16,
11002
+ height: 16,
11003
+ borderRadius: 8,
11004
+ borderWidth: 1,
11005
+ borderColor: color,
11006
+ alignItems: "center",
11007
+ justifyContent: "center"
11008
+ },
11009
+ accessibilityRole: "image",
11010
+ accessibilityLabel: label,
11011
+ testID: "exercise-indicator",
11012
+ ...props,
11013
+ children: /* @__PURE__ */ jsx(Icon, { size: 11, color })
11014
+ }
11015
+ );
11016
+ if (onPress) {
11017
+ return /* @__PURE__ */ jsx(reactNative.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, children: chip });
11018
+ }
11019
+ return chip;
11020
+ }
11021
+ function headingLabel(name, sets, reps, load, unit) {
11022
+ const loadLabel = typeof load === "number" ? roundWeight(load) : load;
11023
+ return `${name}, ${sets}\xD7${reps} @ ${loadLabel} ${unit}`;
11024
+ }
11025
+ function ExerciseHeading({
11026
+ name,
11027
+ sets,
11028
+ reps,
11029
+ load,
11030
+ unit = "lbs",
11031
+ tempo,
11032
+ indicator,
11033
+ dimmed,
11034
+ onPress
11035
+ }) {
11036
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { opacity: dimmed ? 0.55 : 1 }, testID: "exercise-heading", children: [
11037
+ /* @__PURE__ */ jsxs(
11038
+ reactNative.Pressable,
11039
+ {
11040
+ onPress,
11041
+ accessibilityRole: "button",
11042
+ accessibilityLabel: headingLabel(name, sets, reps, load, unit),
10492
11043
  className: "flex-row items-center",
10493
11044
  style: { gap: 8 },
10494
11045
  testID: "exercise-card-header",
@@ -10722,7 +11273,9 @@ function CollapsedCard({
10722
11273
  {
10723
11274
  velocities,
10724
11275
  zones: velocityZones,
10725
- variant: "mini",
11276
+ variant: "compact",
11277
+ height: 8,
11278
+ hideBaseline: true,
10726
11279
  testID: `exercise-card-velocity-strip-${i}`
10727
11280
  },
10728
11281
  i
@@ -11021,6 +11574,7 @@ function SessionRail({
11021
11574
  next,
11022
11575
  stripHeight = 8,
11023
11576
  width = DEFAULT_WIDTH,
11577
+ expandedIndex,
11024
11578
  onExercisePress,
11025
11579
  className,
11026
11580
  style,
@@ -11061,7 +11615,24 @@ function SessionRail({
11061
11615
  borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
11062
11616
  borderBottomColor: DIVIDER
11063
11617
  },
11064
- children: /* @__PURE__ */ jsx(
11618
+ children: i === expandedIndex && ex.sets ? /* @__PURE__ */ jsx(
11619
+ ExerciseCard,
11620
+ {
11621
+ name: ex.name,
11622
+ expanded: true,
11623
+ summary: {
11624
+ sets: ex.summary.sets,
11625
+ reps: ex.summary.reps,
11626
+ // ExerciseCard's summary takes a numeric load; a string load is
11627
+ // an unset/discovery weight, which has no expanded form yet.
11628
+ weight: typeof ex.summary.weight === "number" ? ex.summary.weight : 0,
11629
+ unit: ex.summary.unit
11630
+ },
11631
+ tempo: ex.tempo,
11632
+ sets: ex.sets,
11633
+ onExpandedChange: () => onExercisePress?.(ex, i)
11634
+ }
11635
+ ) : /* @__PURE__ */ jsx(
11065
11636
  ExerciseCardHeading,
11066
11637
  {
11067
11638
  name: ex.name,
@@ -11089,7 +11660,7 @@ function SessionRail({
11089
11660
  var DEFAULTS = {
11090
11661
  label: "SS",
11091
11662
  color: getSemanticColors("dark")["brand-primary"],
11092
- bgBase: "#101010"
11663
+ bgBase: getSemanticColors("dark")["background-base"]
11093
11664
  };
11094
11665
  function SupersetWrapper({
11095
11666
  label = DEFAULTS.label,
@@ -11348,10 +11919,10 @@ function WeekRow({
11348
11919
  }
11349
11920
  );
11350
11921
  }
11351
- var t9 = getSemanticColors("dark");
11922
+ var t10 = getSemanticColors("dark");
11352
11923
  var COLORS = {
11353
- statusSuccess: t9["status-success"],
11354
- brandPrimary: t9["brand-primary"],
11924
+ statusSuccess: t10["status-success"],
11925
+ brandPrimary: t10["brand-primary"],
11355
11926
  borderDefault: "#1F1F1F",
11356
11927
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
11357
11928
  };
@@ -11835,12 +12406,12 @@ function PrHistoryModal({
11835
12406
  }
11836
12407
  );
11837
12408
  }
11838
- var t10 = getSemanticColors("dark");
11839
- var BRAND_PRIMARY9 = t10["brand-primary"];
12409
+ var t11 = getSemanticColors("dark");
12410
+ var BRAND_PRIMARY9 = t11["brand-primary"];
11840
12411
  var BRAND_PRIMARY_SUBTLE = "rgba(255,121,0,0.12)";
11841
- var STATUS_SUCCESS2 = t10["status-success"];
11842
- var STATUS_WARNING2 = t10["status-warning"];
11843
- var STATUS_ERROR2 = t10["status-error"];
12412
+ var STATUS_SUCCESS2 = t11["status-success"];
12413
+ var STATUS_WARNING2 = t11["status-warning"];
12414
+ var STATUS_ERROR2 = t11["status-error"];
11844
12415
  var EMOJI_SETS = {
11845
12416
  sleep: ["\u{1F634}", "\u{1F610}", "\u{1F642}", "\u{1F60A}", "\u{1F929}"],
11846
12417
  soreness: ["\u{1F635}", "\u{1F623}", "\u{1F610}", "\u{1F642}", "\u{1F4AA}"],
@@ -12030,13 +12601,13 @@ function ReadinessCheck({
12030
12601
  )
12031
12602
  ] }) });
12032
12603
  }
12033
- var t11 = getSemanticColors("dark");
12034
- var BRAND_PRIMARY10 = t11["brand-primary"];
12604
+ var t12 = getSemanticColors("dark");
12605
+ var BRAND_PRIMARY10 = t12["brand-primary"];
12035
12606
  var BRAND_PRIMARY_DARK2 = MESO_ACCENT_GRADIENT_DARK;
12036
12607
  var BRAND_PRIMARY_LIGHT2 = MESO_ACCENT_GRADIENT_LIGHT;
12037
- var SUCCESS = t11["status-success"];
12038
- var WARNING = t11["status-warning"];
12039
- var ERROR = t11["status-error"];
12608
+ var SUCCESS = t12["status-success"];
12609
+ var WARNING = t12["status-warning"];
12610
+ var ERROR = t12["status-error"];
12040
12611
  var ACCENT_STOPS2 = [BRAND_PRIMARY_DARK2, BRAND_PRIMARY10, BRAND_PRIMARY_LIGHT2];
12041
12612
  var CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor("surface-elevated")} 0%, ${resolveColor("surface-raised")} 100%)`;
12042
12613
  var GAUGE_GRADIENT = "linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(249,180,21,0.25) 50%, rgba(209,67,67,0.25) 100%)";
@@ -12407,8 +12978,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
12407
12978
  var PLOT_LEFT = 26;
12408
12979
  var TOOLTIP_WIDTH = 124;
12409
12980
  function timeOf(dateStr) {
12410
- const t16 = Date.parse(dateStr);
12411
- return Number.isNaN(t16) ? 0 : t16;
12981
+ const t26 = Date.parse(dateStr);
12982
+ return Number.isNaN(t26) ? 0 : t26;
12412
12983
  }
12413
12984
  function formatValue(value) {
12414
12985
  return `${Math.round(value)}`;
@@ -12897,10 +13468,10 @@ function StrengthTrendChart({
12897
13468
  }
12898
13469
  );
12899
13470
  }
12900
- var t12 = getSemanticColors("dark");
12901
- var STATUS_SUCCESS4 = t12["status-success"];
12902
- var STATUS_WARNING4 = t12["status-warning"];
12903
- var STATUS_INFO = t12["status-info"];
13471
+ var t13 = getSemanticColors("dark");
13472
+ var STATUS_SUCCESS4 = t13["status-success"];
13473
+ var STATUS_WARNING4 = t13["status-warning"];
13474
+ var STATUS_INFO = t13["status-info"];
12904
13475
  var DOT_BORDER = "#F3F4F6";
12905
13476
  var BAND_FILL = "rgba(46,213,115,0.1)";
12906
13477
  var BAND_EDGE = "rgba(46,213,115,0.45)";
@@ -12943,8 +13514,8 @@ function interpEdge(pixels, x, key) {
12943
13514
  const a = pixels[i];
12944
13515
  const b = pixels[i + 1];
12945
13516
  if (x >= a.x && x <= b.x) {
12946
- const t16 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
12947
- return a[key] + t16 * (b[key] - a[key]);
13517
+ const t26 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
13518
+ return a[key] + t26 * (b[key] - a[key]);
12948
13519
  }
12949
13520
  }
12950
13521
  return pixels[pixels.length - 1][key];
@@ -13274,13 +13845,13 @@ function CapacityBandChart({
13274
13845
  }
13275
13846
  );
13276
13847
  }
13277
- var t13 = getSemanticColors("dark");
13848
+ var t14 = getSemanticColors("dark");
13278
13849
  function glowShadow(color) {
13279
13850
  return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`;
13280
13851
  }
13281
13852
  var DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200];
13282
13853
  var DEFAULT_MARKER_COLOR2 = primitiveColors.white;
13283
- var TICK_COLOR = t13["text-tertiary"];
13854
+ var TICK_COLOR = t14["text-tertiary"];
13284
13855
  var ZONE_SIZES = {
13285
13856
  default: {
13286
13857
  trackHeight: 14,
@@ -13463,7 +14034,7 @@ function ZoneTrack({
13463
14034
  ),
13464
14035
  ticks?.map((tick, i) => {
13465
14036
  const emphasized = tick.emphasized === true;
13466
- const lineColor = tick.color ?? (emphasized ? t13["brand-primary"] : TICK_COLOR);
14037
+ const lineColor = tick.color ?? (emphasized ? t14["brand-primary"] : TICK_COLOR);
13467
14038
  const lineWidth = emphasized ? s.tickLineEmphasized : s.tickLineNormal;
13468
14039
  return /* @__PURE__ */ jsx(
13469
14040
  reactNative.View,
@@ -13494,7 +14065,7 @@ function ZoneTrack({
13494
14065
  if (tick.label == null) return null;
13495
14066
  const emphasized = tick.emphasized === true;
13496
14067
  if (!showTickLabel(i, emphasized)) return null;
13497
- const labelColor = tick.color ?? (emphasized ? t13["brand-primary"] : TICK_COLOR);
14068
+ const labelColor = tick.color ?? (emphasized ? t14["brand-primary"] : TICK_COLOR);
13498
14069
  const labelNode = /* @__PURE__ */ jsx(
13499
14070
  reactNative.Text,
13500
14071
  {
@@ -13550,14 +14121,14 @@ function FatigueMeter({
13550
14121
  className,
13551
14122
  ...props
13552
14123
  }) {
13553
- const [t1, t22, t32] = thresholds;
14124
+ const [t1, t26, t32] = thresholds;
13554
14125
  const zones = [
13555
14126
  { upTo: t1, color: zoneColors[0] },
13556
- { upTo: t22, color: zoneColors[1] },
14127
+ { upTo: t26, color: zoneColors[1] },
13557
14128
  { upTo: t32, color: zoneColors[2] },
13558
14129
  { upTo: max, color: zoneColors[3] }
13559
14130
  ];
13560
- const tickValues = [0, t1, t22, t32, max];
14131
+ const tickValues = [0, t1, t26, t32, max];
13561
14132
  const ticks = tickValues.map((v, i) => ({ value: v, label: labels[i] }));
13562
14133
  return /* @__PURE__ */ jsx(
13563
14134
  ZoneTrack,
@@ -13680,7 +14251,7 @@ function VolumeLandmarkBar({
13680
14251
  }
13681
14252
  );
13682
14253
  }
13683
- var t14 = getSemanticColors("dark");
14254
+ var t15 = getSemanticColors("dark");
13684
14255
  var statusToken = {
13685
14256
  productive: "status-success",
13686
14257
  threshold: "status-warning",
@@ -13697,7 +14268,7 @@ var defaultLabel = {
13697
14268
  stop: "Stop"
13698
14269
  };
13699
14270
  function statusPillColor(status) {
13700
- return t14[statusToken[status]];
14271
+ return t15[statusToken[status]];
13701
14272
  }
13702
14273
  function StatusPill2({ status, label, className, style, ...props }) {
13703
14274
  const color = statusPillColor(status);
@@ -13730,7 +14301,7 @@ function StatusPill2({ status, label, className, style, ...props }) {
13730
14301
  }
13731
14302
  );
13732
14303
  }
13733
- var t15 = getSemanticColors("dark");
14304
+ var t16 = getSemanticColors("dark");
13734
14305
  var categoryToken = {
13735
14306
  productive: null,
13736
14307
  threshold: "status-warning",
@@ -13743,7 +14314,7 @@ var floodOpacity = {
13743
14314
  };
13744
14315
  function liveAuraColor(category) {
13745
14316
  const token = categoryToken[category];
13746
- return token ? t15[token] : null;
14317
+ return token ? t16[token] : null;
13747
14318
  }
13748
14319
  function LiveAuraFrame({
13749
14320
  category,
@@ -13772,9 +14343,9 @@ function LiveAuraFrame({
13772
14343
  position: "relative",
13773
14344
  overflow: "hidden",
13774
14345
  borderRadius: 10,
13775
- backgroundColor: t15["background-base"],
14346
+ backgroundColor: t16["background-base"],
13776
14347
  borderWidth: 1,
13777
- borderColor: t15["border-default"]
14348
+ borderColor: t16["border-default"]
13778
14349
  },
13779
14350
  style
13780
14351
  ],
@@ -13795,6 +14366,905 @@ function LiveAuraFrame({
13795
14366
  }
13796
14367
  );
13797
14368
  }
14369
+ function VelocityHero({
14370
+ velocities,
14371
+ targetReps,
14372
+ liveRepIndex,
14373
+ width,
14374
+ height = 300
14375
+ }) {
14376
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "velocity-hero", style: width != null ? { width, height } : { flex: 1, height }, children: /* @__PURE__ */ jsx(
14377
+ VelocityStrip,
14378
+ {
14379
+ variant: "hero",
14380
+ velocities,
14381
+ liveRepIndex: liveRepIndex ?? velocities.length - 1,
14382
+ targetReps,
14383
+ height,
14384
+ scale: "peak"
14385
+ }
14386
+ ) });
14387
+ }
14388
+
14389
+ // src/components/custom/Fatigue/fatigue-tokens.ts
14390
+ var t17 = getSemanticColors("dark");
14391
+ var FONT_HEAD = '"Space Grotesk", sans-serif';
14392
+ var FONT_UI = '"Nunito Sans", sans-serif';
14393
+ var FONT_MONO = "monospace";
14394
+ var TONE_COLOR = {
14395
+ ok: t17["status-success"],
14396
+ warn: t17["status-warning"],
14397
+ alarm: t17["status-error"]
14398
+ };
14399
+ var STATE_LABEL = {
14400
+ good: "Good",
14401
+ slowing: "Slowing",
14402
+ grinding: "Grinding",
14403
+ "form-breakdown": "Form breaking down"
14404
+ };
14405
+ var PHASE_AXIS_COLOR = {
14406
+ eccentric: primitiveRamps.magenta[800],
14407
+ concentric: primitiveRamps.cyan[800],
14408
+ hold: primitiveColors.charcoal[100],
14409
+ idle: primitiveColors.charcoal[200]
14410
+ };
14411
+ var PHASE_AXIS_BASE_COLOR = {
14412
+ eccentric: primitiveRamps.magenta[950],
14413
+ concentric: primitiveRamps.cyan[950],
14414
+ hold: primitiveColors.charcoal[300],
14415
+ idle: primitiveColors.charcoal[200]
14416
+ };
14417
+ var PACING_TONE = {
14418
+ ahead: primitiveRamps.amber[300],
14419
+ onPace: primitiveRamps.green[300],
14420
+ over: primitiveRamps.red[300]
14421
+ };
14422
+ var SILVER = primitiveColors.neutral[300];
14423
+ var RED_LIGHT = primitiveRamps.red[400];
14424
+ var RED_MID = primitiveRamps.red[600];
14425
+ var RED_DEEP = primitiveRamps.red[800];
14426
+ var GRIND_THRESHOLD = 0.35;
14427
+ var DRIFT_GREY = primitiveColors.neutral[600];
14428
+ function clamp012(v) {
14429
+ return Math.min(1, Math.max(0, v));
14430
+ }
14431
+ function hexToRgb2(hex) {
14432
+ const h = hex.replace("#", "");
14433
+ const f = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
14434
+ return [parseInt(f.slice(0, 2), 16), parseInt(f.slice(2, 4), 16), parseInt(f.slice(4, 6), 16)];
14435
+ }
14436
+ function mixHex2(a, b, ratio) {
14437
+ const [ar, ag, ab] = hexToRgb2(a);
14438
+ const [br, bg, bb] = hexToRgb2(b);
14439
+ const m = (x, y) => Math.round(x + (y - x) * clamp012(ratio));
14440
+ return `#${[m(ar, br), m(ag, bg), m(ab, bb)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
14441
+ }
14442
+ function ghostLineColor(tempoDeviation, grindSignature) {
14443
+ if (grindSignature >= GRIND_THRESHOLD) {
14444
+ const severity = clamp012((grindSignature - GRIND_THRESHOLD) / (1 - GRIND_THRESHOLD));
14445
+ return severity < 0.5 ? mixHex2(RED_LIGHT, RED_MID, severity * 2) : mixHex2(RED_MID, RED_DEEP, (severity - 0.5) * 2);
14446
+ }
14447
+ return mixHex2(SILVER, DRIFT_GREY, (tempoDeviation ?? 0) * 0.55);
14448
+ }
14449
+ function auraForVerdict(state) {
14450
+ if (state == null || state === "good") return "productive";
14451
+ if (state === "form-breakdown") return "stop";
14452
+ return "threshold";
14453
+ }
14454
+
14455
+ // src/components/custom/Fatigue/VerdictHero.tsx
14456
+ var t18 = getSemanticColors("dark");
14457
+ function formatRpe(rpe) {
14458
+ if (rpe == null) return "\u2014";
14459
+ return (Math.round(rpe * 2) / 2).toFixed(1);
14460
+ }
14461
+ function VerdictHero({ rpe, verdict }) {
14462
+ const tone = verdict ? TONE_COLOR[verdict.tone] : t18["text-tertiary"];
14463
+ const word = verdict ? STATE_LABEL[verdict.state] : "Warming up";
14464
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 6 }, testID: "verdict-hero", children: [
14465
+ /* @__PURE__ */ jsx(
14466
+ reactNative.Text,
14467
+ {
14468
+ style: {
14469
+ fontSize: 9,
14470
+ letterSpacing: 1.5,
14471
+ fontFamily: FONT_MONO,
14472
+ color: t18["text-tertiary"]
14473
+ },
14474
+ children: "FATIGUE"
14475
+ }
14476
+ ),
14477
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 2 }, children: [
14478
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", alignItems: "flex-end", gap: 7 }, children: [
14479
+ /* @__PURE__ */ jsx(
14480
+ reactNative.Text,
14481
+ {
14482
+ style: {
14483
+ fontSize: 62,
14484
+ fontWeight: "900",
14485
+ fontFamily: FONT_HEAD,
14486
+ color: tone,
14487
+ lineHeight: 60
14488
+ },
14489
+ accessibilityLabel: `RPE ${formatRpe(rpe)}`,
14490
+ children: formatRpe(rpe)
14491
+ }
14492
+ ),
14493
+ /* @__PURE__ */ jsx(
14494
+ reactNative.Text,
14495
+ {
14496
+ style: {
14497
+ fontSize: 18,
14498
+ fontWeight: "800",
14499
+ color: tone,
14500
+ marginBottom: 9,
14501
+ fontFamily: FONT_HEAD
14502
+ },
14503
+ children: "RPE"
14504
+ }
14505
+ )
14506
+ ] }),
14507
+ /* @__PURE__ */ jsx(reactNative.Text, { style: { fontSize: 17, fontWeight: "800", fontFamily: FONT_HEAD, color: tone }, children: word })
14508
+ ] })
14509
+ ] });
14510
+ }
14511
+ var t19 = getSemanticColors("dark");
14512
+ var TONE_VARIANT = {
14513
+ ok: "success",
14514
+ warn: "warning",
14515
+ alarm: "error"
14516
+ };
14517
+ var TONE_WORD = { ok: "ok", warn: "watch", alarm: "alarm" };
14518
+ function Light({
14519
+ label,
14520
+ tone,
14521
+ detail
14522
+ }) {
14523
+ const variant = tone ? TONE_VARIANT[tone] : "neutral";
14524
+ const word = tone ? TONE_WORD[tone] : "warming up";
14525
+ return /* @__PURE__ */ jsx(Tooltip, { label: `${detail} \xB7 ${word}`, placement: "bottom", children: /* @__PURE__ */ jsxs(
14526
+ reactNative.View,
14527
+ {
14528
+ accessibilityLabel: `${detail}, ${word}`,
14529
+ style: { flexDirection: "row", alignItems: "center", gap: 5 },
14530
+ children: [
14531
+ /* @__PURE__ */ jsx(StatusDot, { variant, size: "sm", glow: true }),
14532
+ /* @__PURE__ */ jsx(
14533
+ reactNative.Text,
14534
+ {
14535
+ style: {
14536
+ fontSize: 9,
14537
+ letterSpacing: 0.6,
14538
+ fontFamily: FONT_MONO,
14539
+ color: t19["text-secondary"]
14540
+ },
14541
+ children: label
14542
+ }
14543
+ )
14544
+ ]
14545
+ }
14546
+ ) });
14547
+ }
14548
+ function FatigueLights({ dimensions, spread = false }) {
14549
+ return /* @__PURE__ */ jsxs(
14550
+ reactNative.View,
14551
+ {
14552
+ testID: "fatigue-lights",
14553
+ style: {
14554
+ flexDirection: "row",
14555
+ gap: spread ? 0 : 16,
14556
+ alignItems: "center",
14557
+ justifyContent: spread ? "space-between" : "flex-start"
14558
+ },
14559
+ children: [
14560
+ /* @__PURE__ */ jsx(Light, { label: "VEL", tone: dimensions?.velocityLoss ?? null, detail: "Velocity loss" }),
14561
+ /* @__PURE__ */ jsx(Light, { label: "ROM", tone: dimensions?.rom ?? null, detail: "ROM depth" }),
14562
+ /* @__PURE__ */ jsx(Light, { label: "TEMPO", tone: dimensions?.tempo ?? null, detail: "Tempo" })
14563
+ ]
14564
+ }
14565
+ );
14566
+ }
14567
+ function barColor(romM, working, short) {
14568
+ if (short != null && romM < short) return RED_DEEP;
14569
+ if (working != null && romM < working) return RED_LIGHT;
14570
+ return SILVER;
14571
+ }
14572
+ function romReferenceOverlay(g, workingStandardM, shortThresholdM) {
14573
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
14574
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14575
+ reactNative.View,
14576
+ {
14577
+ pointerEvents: "none",
14578
+ style: {
14579
+ position: "absolute",
14580
+ left: 0,
14581
+ right: 0,
14582
+ bottom: 0,
14583
+ height: g.yOf(shortThresholdM),
14584
+ backgroundColor: alpha(RED_DEEP, 0.06)
14585
+ }
14586
+ }
14587
+ ),
14588
+ workingStandardM != null && /* @__PURE__ */ jsx(
14589
+ reactNative.View,
14590
+ {
14591
+ pointerEvents: "none",
14592
+ style: {
14593
+ position: "absolute",
14594
+ left: 0,
14595
+ right: 0,
14596
+ bottom: g.yOf(workingStandardM),
14597
+ borderTopWidth: 1,
14598
+ borderStyle: "dashed",
14599
+ borderColor: alpha(SILVER, 0.4)
14600
+ }
14601
+ }
14602
+ ),
14603
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14604
+ reactNative.View,
14605
+ {
14606
+ pointerEvents: "none",
14607
+ style: {
14608
+ position: "absolute",
14609
+ left: 0,
14610
+ right: 0,
14611
+ bottom: g.yOf(shortThresholdM),
14612
+ borderTopWidth: 1,
14613
+ borderStyle: "dashed",
14614
+ borderColor: alpha(RED_DEEP, 0.4)
14615
+ }
14616
+ }
14617
+ )
14618
+ ] });
14619
+ }
14620
+ function RomProgressionChart({
14621
+ points,
14622
+ workingStandardM,
14623
+ shortThresholdM,
14624
+ barHeight = 44,
14625
+ maxReps = 12,
14626
+ plannedReps: plannedReps2
14627
+ }) {
14628
+ const shown = points.slice(-maxReps);
14629
+ const slots = shown.map((p) => ({ kind: "rep", value: p.romM }));
14630
+ const scaleMax = Math.max(...shown.map((p) => p.romM), workingStandardM ?? 0, shortThresholdM ?? 0) || void 0;
14631
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "rom-progression", children: /* @__PURE__ */ jsx(
14632
+ SetBarChart,
14633
+ {
14634
+ slots,
14635
+ colorFor: (romM) => barColor(romM, workingStandardM, shortThresholdM),
14636
+ height: barHeight,
14637
+ scaleMax,
14638
+ barRadius: 2,
14639
+ hideBaseline: true,
14640
+ targetReps: plannedReps2,
14641
+ renderReference: (g) => romReferenceOverlay(g, workingStandardM, shortThresholdM),
14642
+ testIDPrefix: "rom"
14643
+ }
14644
+ ) });
14645
+ }
14646
+
14647
+ // src/components/custom/Fatigue/tempo-pacing.ts
14648
+ var t20 = getSemanticColors("dark");
14649
+ var ON_TARGET_MS2 = 100;
14650
+ function phaseFillFraction(elapsedMs, targetMs) {
14651
+ if (targetMs == null || targetMs <= 0) return 1;
14652
+ return Math.min(1, Math.max(0, elapsedMs / targetMs));
14653
+ }
14654
+ function pacingTone(elapsedMs, targetMs) {
14655
+ if (targetMs == null || targetMs <= 0) return t20["text-primary"];
14656
+ const remainingMs = targetMs - elapsedMs;
14657
+ if (remainingMs > ON_TARGET_MS2) return PACING_TONE.ahead;
14658
+ if (remainingMs >= -ON_TARGET_MS2) return PACING_TONE.onPace;
14659
+ return PACING_TONE.over;
14660
+ }
14661
+ function prescribedSegments(tempo) {
14662
+ if (tempo == null) return [];
14663
+ const [ecc, pauseBottom, con, pauseTop] = tempo;
14664
+ const order = [
14665
+ ["eccentric", ecc],
14666
+ ["hold", pauseBottom],
14667
+ ["concentric", con],
14668
+ ["hold", pauseTop]
14669
+ ];
14670
+ const out = [];
14671
+ let t26 = 0;
14672
+ for (const [phase, seconds] of order) {
14673
+ const ms = seconds * 1e3;
14674
+ if (ms > 0) out.push({ phase, startMs: t26, endMs: t26 + ms });
14675
+ t26 += ms;
14676
+ }
14677
+ return out;
14678
+ }
14679
+ function phaseTargetsMs(phases, tempo) {
14680
+ if (tempo == null) return phases.map(() => null);
14681
+ const [eccS, pauseBottomS, conS, pauseTopS] = tempo;
14682
+ let seenConcentric = false;
14683
+ return phases.map((phase) => {
14684
+ switch (phase) {
14685
+ case "eccentric":
14686
+ return eccS * 1e3;
14687
+ case "concentric":
14688
+ seenConcentric = true;
14689
+ return conS * 1e3;
14690
+ case "hold":
14691
+ return (seenConcentric ? pauseTopS : pauseBottomS) * 1e3;
14692
+ default:
14693
+ return null;
14694
+ }
14695
+ });
14696
+ }
14697
+
14698
+ // src/components/custom/Fatigue/GhostBand.tsx
14699
+ var t21 = getSemanticColors("dark");
14700
+ var BAND_H = 16;
14701
+ var BAND_GAP = 4;
14702
+ var PHASE_LABEL = {
14703
+ eccentric: "ECC",
14704
+ concentric: "CON"
14705
+ };
14706
+ var LABEL_CH_PX = 7;
14707
+ var LABEL_PAD_PX = 6;
14708
+ var WELL_SHADE = primitiveColors.black;
14709
+ var WELL_FLOOR_LIGHT = primitiveColors.white;
14710
+ function labelFits(label, segW) {
14711
+ return segW >= label.length * LABEL_CH_PX + LABEL_PAD_PX;
14712
+ }
14713
+ function GhostBand({
14714
+ segments,
14715
+ x,
14716
+ top,
14717
+ height = BAND_H,
14718
+ showLabels = false,
14719
+ labelColor = t21["text-primary"],
14720
+ targetTempoSeconds = null,
14721
+ prescribed = false
14722
+ }) {
14723
+ const rawId = React25.useId();
14724
+ const safeId = rawId.replace(/[^a-zA-Z0-9]/g, "");
14725
+ const clipId = `ghost-band-${safeId}`;
14726
+ const wellId = `ghost-band-well-${safeId}`;
14727
+ const drawn = segments.filter((seg) => x(seg.endMs) - x(seg.startMs) > 0);
14728
+ if (drawn.length === 0) return null;
14729
+ const bandLeft = x(drawn[0].startMs);
14730
+ const bandRight = x(drawn[drawn.length - 1].endMs);
14731
+ const bandW = bandRight - bandLeft;
14732
+ if (bandW <= 0) return null;
14733
+ const pacing = targetTempoSeconds != null && !prescribed;
14734
+ const targetsMs = phaseTargetsMs(
14735
+ drawn.map((seg) => seg.phase),
14736
+ targetTempoSeconds
14737
+ );
14738
+ const runs = drawn.map((seg, i) => {
14739
+ const left = x(seg.startMs);
14740
+ const right = i === drawn.length - 1 ? bandRight : x(drawn[i + 1].startMs);
14741
+ const width = Math.max(0, right - left);
14742
+ const targetMs = targetsMs[i];
14743
+ const elapsedMs = seg.endMs - seg.startMs;
14744
+ return {
14745
+ phase: seg.phase,
14746
+ left,
14747
+ width,
14748
+ // Prescribed → nothing performed, so nothing filled. No target at all → the run reads
14749
+ // complete rather than perpetually empty.
14750
+ fillWidth: prescribed ? 0 : pacing ? width * phaseFillFraction(elapsedMs, targetMs) : width,
14751
+ labelTone: pacing ? pacingTone(elapsedMs, targetMs) : labelColor
14752
+ };
14753
+ });
14754
+ return /* @__PURE__ */ jsxs("g", { children: [
14755
+ /* @__PURE__ */ jsxs("defs", { children: [
14756
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: bandLeft, y: top, width: bandW, height, rx: 2 }) }),
14757
+ /* @__PURE__ */ jsxs("linearGradient", { id: wellId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
14758
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: WELL_SHADE, stopOpacity: 0.6 }),
14759
+ /* @__PURE__ */ jsx("stop", { offset: "42%", stopColor: WELL_SHADE, stopOpacity: 0.12 }),
14760
+ /* @__PURE__ */ jsx("stop", { offset: "88%", stopColor: WELL_SHADE, stopOpacity: 0.02 }),
14761
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: WELL_FLOOR_LIGHT, stopOpacity: 0.07 })
14762
+ ] })
14763
+ ] }),
14764
+ /* @__PURE__ */ jsxs("g", { clipPath: `url(#${clipId})`, children: [
14765
+ /* @__PURE__ */ jsx(
14766
+ "rect",
14767
+ {
14768
+ x: bandLeft,
14769
+ y: top,
14770
+ width: bandW,
14771
+ height,
14772
+ fill: PHASE_AXIS_COLOR.idle,
14773
+ "data-testid": "ghost-band-floor"
14774
+ }
14775
+ ),
14776
+ runs.map((run, i) => /* @__PURE__ */ jsxs("g", { children: [
14777
+ /* @__PURE__ */ jsx(
14778
+ "rect",
14779
+ {
14780
+ x: run.left,
14781
+ y: top,
14782
+ width: run.width,
14783
+ height,
14784
+ fill: PHASE_AXIS_BASE_COLOR[run.phase],
14785
+ "data-testid": "ghost-band-base"
14786
+ }
14787
+ ),
14788
+ /* @__PURE__ */ jsx(
14789
+ "rect",
14790
+ {
14791
+ x: run.left,
14792
+ y: top,
14793
+ width: run.width,
14794
+ height,
14795
+ fill: `url(#${wellId})`,
14796
+ "data-testid": "ghost-band-well"
14797
+ }
14798
+ ),
14799
+ /* @__PURE__ */ jsx(
14800
+ "rect",
14801
+ {
14802
+ x: run.left,
14803
+ y: top,
14804
+ width: run.fillWidth,
14805
+ height,
14806
+ fill: PHASE_AXIS_COLOR[run.phase],
14807
+ "data-testid": "ghost-band-fill"
14808
+ }
14809
+ )
14810
+ ] }, i)),
14811
+ showLabels && runs.map((run, i) => {
14812
+ const label = PHASE_LABEL[run.phase];
14813
+ if (!label || !labelFits(label, run.width)) return null;
14814
+ return /* @__PURE__ */ jsx(
14815
+ "text",
14816
+ {
14817
+ x: run.left + run.width / 2,
14818
+ y: top + height / 2,
14819
+ textAnchor: "middle",
14820
+ dominantBaseline: "central",
14821
+ fill: run.labelTone,
14822
+ fontSize: 8,
14823
+ fontWeight: 800,
14824
+ letterSpacing: 1,
14825
+ fontFamily: FONT_UI,
14826
+ children: label
14827
+ },
14828
+ i
14829
+ );
14830
+ })
14831
+ ] })
14832
+ ] });
14833
+ }
14834
+ var t22 = getSemanticColors("dark");
14835
+ var PARCH = t22["text-primary"];
14836
+ function smoothPath(pts) {
14837
+ if (pts.length < 2) return pts.length ? `M ${pts[0][0]} ${pts[0][1]}` : "";
14838
+ let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
14839
+ for (let i = 0; i < pts.length - 1; i++) {
14840
+ const p0 = pts[i - 1] ?? pts[i];
14841
+ const p1 = pts[i];
14842
+ const p2 = pts[i + 1];
14843
+ const p3 = pts[i + 2] ?? p2;
14844
+ const c1x = p1[0] + (p2[0] - p0[0]) / 6;
14845
+ const c1y = p1[1] + (p2[1] - p0[1]) / 6;
14846
+ const c2x = p2[0] - (p3[0] - p1[0]) / 6;
14847
+ const c2y = p2[1] - (p3[1] - p1[1]) / 6;
14848
+ d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
14849
+ }
14850
+ return d;
14851
+ }
14852
+ var BLUR_STD_DEV = 2.6;
14853
+ function GhostBloom({
14854
+ current,
14855
+ ghosts = [],
14856
+ tint,
14857
+ baseline,
14858
+ orientation = "up",
14859
+ ghostStroke = (i) => alpha(PARCH, 0.1 + i * 0.015),
14860
+ lineWidth = 3.5
14861
+ }) {
14862
+ const rawId = React25.useId();
14863
+ const blurId = `bloom-blur-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`;
14864
+ const toY = (mag) => orientation === "down" ? baseline + mag : baseline - mag;
14865
+ const toAbs = (pts) => pts.map(([px, mag]) => [px, toY(mag)]);
14866
+ const curD = smoothPath(toAbs(current));
14867
+ const ghostDs = ghosts.map((g) => smoothPath(toAbs(g)));
14868
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
14869
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("filter", { id: blurId, x: "-50%", y: "-50%", width: "200%", height: "200%", children: /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: BLUR_STD_DEV }) }) }),
14870
+ ghostDs.map((d, i) => /* @__PURE__ */ jsx(
14871
+ "path",
14872
+ {
14873
+ d,
14874
+ fill: "none",
14875
+ stroke: ghostStroke(i),
14876
+ strokeWidth: 1.5,
14877
+ strokeLinejoin: "round"
14878
+ },
14879
+ i
14880
+ )),
14881
+ /* @__PURE__ */ jsx(
14882
+ "path",
14883
+ {
14884
+ d: curD,
14885
+ fill: "none",
14886
+ stroke: alpha(primitiveColors.black, 0.3),
14887
+ strokeWidth: lineWidth + 3,
14888
+ strokeLinejoin: "round",
14889
+ strokeLinecap: "round",
14890
+ filter: `url(#${blurId})`
14891
+ }
14892
+ ),
14893
+ /* @__PURE__ */ jsx(
14894
+ "path",
14895
+ {
14896
+ d: curD,
14897
+ fill: "none",
14898
+ stroke: tint,
14899
+ strokeWidth: lineWidth,
14900
+ strokeLinejoin: "round",
14901
+ strokeLinecap: "round"
14902
+ }
14903
+ )
14904
+ ] });
14905
+ }
14906
+
14907
+ // src/components/custom/Fatigue/GhostSpark.tsx
14908
+ function GhostSpark({
14909
+ curves,
14910
+ width,
14911
+ height = 172,
14912
+ targetTempoSeconds = null
14913
+ }) {
14914
+ const w = width;
14915
+ const h = height;
14916
+ const padL = 12;
14917
+ const padR = 8;
14918
+ const padTop = 10;
14919
+ const padBot = 6;
14920
+ if (curves.length === 0) {
14921
+ const prescribed = prescribedSegments(targetTempoSeconds);
14922
+ if (prescribed.length === 0) {
14923
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { width: w, height: h } });
14924
+ }
14925
+ const totalMs = prescribed[prescribed.length - 1].endMs;
14926
+ const bandTopEmpty = h - padBot - BAND_H;
14927
+ const xEmpty = (ms) => padL + ms / (totalMs * 1.04) * (w - padL - padR);
14928
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsx("svg", { width: w, height: h, children: /* @__PURE__ */ jsx(
14929
+ GhostBand,
14930
+ {
14931
+ segments: prescribed,
14932
+ x: xEmpty,
14933
+ top: bandTopEmpty,
14934
+ showLabels: true,
14935
+ targetTempoSeconds,
14936
+ prescribed: true
14937
+ }
14938
+ ) }) });
14939
+ }
14940
+ const cur = curves[curves.length - 1];
14941
+ const allSamples = curves.flatMap((c) => c.samples);
14942
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
14943
+ const axisMaxT = Math.max(1, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0)) * 1.04;
14944
+ const bandBottom = h - padBot;
14945
+ const bandTop = bandBottom - BAND_H;
14946
+ const baseline = bandTop - BAND_GAP;
14947
+ const plotH = Math.max(1, baseline - padTop);
14948
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
14949
+ const mag = (v) => clamp012(v / vmax) * plotH;
14950
+ const lineTint = ghostLineColor(cur.tempoDeviation, cur.grindSignature);
14951
+ const curPts = cur.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
14952
+ const ghostPts = curves.slice(0, -1).map((c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]));
14953
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
14954
+ /* @__PURE__ */ jsx(
14955
+ GhostBloom,
14956
+ {
14957
+ current: curPts,
14958
+ ghosts: ghostPts,
14959
+ tint: lineTint,
14960
+ baseline,
14961
+ orientation: "up"
14962
+ }
14963
+ ),
14964
+ /* @__PURE__ */ jsx(
14965
+ GhostBand,
14966
+ {
14967
+ segments: cur.phaseSegments,
14968
+ x,
14969
+ top: bandTop,
14970
+ showLabels: true,
14971
+ targetTempoSeconds
14972
+ }
14973
+ )
14974
+ ] }) });
14975
+ }
14976
+
14977
+ // src/components/custom/Fatigue/LiveFatigueCard.tsx
14978
+ var t23 = getSemanticColors("dark");
14979
+ var PAD = 18;
14980
+ var GHOST_GUTTER = 4;
14981
+ function LiveFatigueCard({ model, width = 318, height }) {
14982
+ const chartW = width - PAD * 2 - GHOST_GUTTER * 2;
14983
+ const chartH = height != null ? Math.round(Math.min(240, Math.max(168, height * 0.4))) : 172;
14984
+ return /* @__PURE__ */ jsxs(
14985
+ Surface,
14986
+ {
14987
+ level: "base",
14988
+ testID: "live-fatigue-card",
14989
+ style: {
14990
+ width,
14991
+ height,
14992
+ borderRadius: 14,
14993
+ padding: PAD,
14994
+ borderWidth: 1,
14995
+ borderColor: t23["hairline-default"],
14996
+ ...barPaper(t23["surface-base"])
14997
+ },
14998
+ children: [
14999
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 12 }, children: [
15000
+ /* @__PURE__ */ jsx(VerdictHero, { rpe: model.rpe, verdict: model.verdict }),
15001
+ /* @__PURE__ */ jsx(FatigueLights, { dimensions: model.verdict?.dimensions ?? null })
15002
+ ] }),
15003
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, minHeight: 16 } }),
15004
+ /* @__PURE__ */ jsx(
15005
+ RomProgressionChart,
15006
+ {
15007
+ points: model.romProgression,
15008
+ workingStandardM: model.romWorkingStandardM,
15009
+ shortThresholdM: model.romShortThresholdM,
15010
+ plannedReps: model.plannedReps
15011
+ }
15012
+ ),
15013
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, minHeight: 16 } }),
15014
+ /* @__PURE__ */ jsx(
15015
+ GhostSpark,
15016
+ {
15017
+ curves: model.velocityCurves,
15018
+ width: chartW,
15019
+ height: chartH,
15020
+ targetTempoSeconds: model.targetTempoSeconds
15021
+ }
15022
+ )
15023
+ ]
15024
+ }
15025
+ );
15026
+ }
15027
+
15028
+ // src/components/custom/Fatigue/LiveFatiguePanel.tsx
15029
+ var t24 = getSemanticColors("dark");
15030
+ function ExerciseHeaderLite({ header }) {
15031
+ return /* @__PURE__ */ jsxs(
15032
+ reactNative.View,
15033
+ {
15034
+ style: {
15035
+ flexDirection: "row",
15036
+ alignItems: "flex-end",
15037
+ justifyContent: "space-between",
15038
+ paddingHorizontal: 24,
15039
+ paddingTop: 18,
15040
+ paddingBottom: 14,
15041
+ borderBottomWidth: 1,
15042
+ borderColor: alpha(t24["text-primary"], 0.08)
15043
+ },
15044
+ children: [
15045
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 2 }, children: [
15046
+ /* @__PURE__ */ jsx(
15047
+ reactNative.Text,
15048
+ {
15049
+ style: {
15050
+ fontSize: 28,
15051
+ fontWeight: "700",
15052
+ fontFamily: FONT_HEAD,
15053
+ color: t24["text-primary"]
15054
+ },
15055
+ children: header.title
15056
+ }
15057
+ ),
15058
+ header.subtitle && /* @__PURE__ */ jsx(
15059
+ reactNative.Text,
15060
+ {
15061
+ style: {
15062
+ fontSize: 13,
15063
+ fontWeight: "600",
15064
+ fontFamily: FONT_UI,
15065
+ color: t24["text-secondary"]
15066
+ },
15067
+ children: header.subtitle
15068
+ }
15069
+ )
15070
+ ] }),
15071
+ header.meta && /* @__PURE__ */ jsx(
15072
+ reactNative.Text,
15073
+ {
15074
+ style: {
15075
+ fontSize: 13,
15076
+ fontWeight: "800",
15077
+ fontFamily: FONT_MONO,
15078
+ color: t24["text-tertiary"]
15079
+ },
15080
+ children: header.meta
15081
+ }
15082
+ )
15083
+ ]
15084
+ }
15085
+ );
15086
+ }
15087
+ function LiveFatiguePanel({
15088
+ model,
15089
+ velocity,
15090
+ header,
15091
+ aura,
15092
+ bodyHeight = 508,
15093
+ cardWidth = 318
15094
+ }) {
15095
+ const category = aura ?? auraForVerdict(model.verdict?.state ?? null);
15096
+ const heroH = bodyHeight - 26;
15097
+ return /* @__PURE__ */ jsx(
15098
+ LiveAuraFrame,
15099
+ {
15100
+ category,
15101
+ style: { borderRadius: 0, borderWidth: 0 },
15102
+ testID: "live-fatigue-panel",
15103
+ children: /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1 }, children: [
15104
+ header && /* @__PURE__ */ jsx(ExerciseHeaderLite, { header }),
15105
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { padding: 24, flexDirection: "row", gap: 18, alignItems: "stretch" }, children: [
15106
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1, gap: 8 }, children: [
15107
+ /* @__PURE__ */ jsx(
15108
+ reactNative.Text,
15109
+ {
15110
+ style: {
15111
+ fontSize: 9,
15112
+ letterSpacing: 1.2,
15113
+ fontFamily: FONT_MONO,
15114
+ color: t24["text-tertiary"]
15115
+ },
15116
+ children: "VELOCITY \xB7 this set"
15117
+ }
15118
+ ),
15119
+ /* @__PURE__ */ jsx(
15120
+ VelocityHero,
15121
+ {
15122
+ velocities: velocity.velocities,
15123
+ targetReps: velocity.targetReps,
15124
+ liveRepIndex: velocity.liveRepIndex,
15125
+ height: heroH
15126
+ }
15127
+ )
15128
+ ] }),
15129
+ /* @__PURE__ */ jsx(LiveFatigueCard, { model, width: cardWidth, height: bodyHeight })
15130
+ ] })
15131
+ ] })
15132
+ }
15133
+ );
15134
+ }
15135
+ var t25 = getSemanticColors("dark");
15136
+ function phaseAt(segments, ms) {
15137
+ const hit = segments.find((s) => ms >= s.startMs && ms < s.endMs);
15138
+ return hit ? hit.phase : null;
15139
+ }
15140
+ function mergePhaseSegments(a, b) {
15141
+ if (a.length === 0) return b;
15142
+ if (b.length === 0) return a;
15143
+ const bounds = Array.from(new Set([...a, ...b].flatMap((s) => [s.startMs, s.endMs]))).sort(
15144
+ (p, q) => p - q
15145
+ );
15146
+ const out = [];
15147
+ for (let i = 0; i < bounds.length - 1; i++) {
15148
+ const startMs = bounds[i];
15149
+ const endMs = bounds[i + 1];
15150
+ if (endMs <= startMs) continue;
15151
+ const mid = (startMs + endMs) / 2;
15152
+ const pa = phaseAt(a, mid);
15153
+ const pb = phaseAt(b, mid);
15154
+ const phase = pa == null ? pb ?? "idle" : pb == null ? pa : pa === pb ? pa : "idle";
15155
+ const last = out[out.length - 1];
15156
+ if (last && last.phase === phase && last.endMs === startMs) last.endMs = endMs;
15157
+ else out.push({ phase, startMs, endMs });
15158
+ }
15159
+ return out;
15160
+ }
15161
+ function lastTMs(curves) {
15162
+ return Math.max(0, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0));
15163
+ }
15164
+ function DualGhostSpark({
15165
+ left,
15166
+ right,
15167
+ width,
15168
+ height = 232,
15169
+ leftLabel = "LEFT",
15170
+ rightLabel = "RIGHT",
15171
+ showDeviceLabels = true,
15172
+ targetTempoSeconds = null
15173
+ }) {
15174
+ const w = width;
15175
+ const h = height;
15176
+ const padL = 14;
15177
+ const padR = 14;
15178
+ const padTop = 22;
15179
+ const padBot = 22;
15180
+ if (left.length === 0 && right.length === 0) {
15181
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "dual-ghost-spark", style: { width: w, height: h } });
15182
+ }
15183
+ const mid = padTop + (h - padTop - padBot) / 2;
15184
+ const bandTop = mid - BAND_H / 2;
15185
+ const baseUp = bandTop - BAND_GAP;
15186
+ const baseDown = mid + BAND_H / 2 + BAND_GAP;
15187
+ const wingH = Math.max(1, Math.min(baseUp - padTop, h - padBot - baseDown));
15188
+ const allSamples = [...left, ...right].flatMap((c) => c.samples);
15189
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
15190
+ const axisMaxT = Math.max(1, lastTMs(left), lastTMs(right)) * 1.04;
15191
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
15192
+ const mag = (v) => clamp012(v / vmax) * wingH;
15193
+ const toPts = (c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
15194
+ const wing = (curves) => {
15195
+ const cur = curves[curves.length - 1];
15196
+ return {
15197
+ cur,
15198
+ current: cur ? toPts(cur) : [],
15199
+ ghosts: curves.slice(0, -1).map(toPts),
15200
+ tint: cur ? ghostLineColor(cur.tempoDeviation, cur.grindSignature) : t25["text-tertiary"]
15201
+ };
15202
+ };
15203
+ const up = wing(left);
15204
+ const down = wing(right);
15205
+ const segments = mergePhaseSegments(up.cur?.phaseSegments ?? [], down.cur?.phaseSegments ?? []);
15206
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "dual-ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
15207
+ up.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-left", children: /* @__PURE__ */ jsx(
15208
+ GhostBloom,
15209
+ {
15210
+ current: up.current,
15211
+ ghosts: up.ghosts,
15212
+ tint: up.tint,
15213
+ baseline: baseUp,
15214
+ orientation: "up"
15215
+ }
15216
+ ) }),
15217
+ down.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-right", children: /* @__PURE__ */ jsx(
15218
+ GhostBloom,
15219
+ {
15220
+ current: down.current,
15221
+ ghosts: down.ghosts,
15222
+ tint: down.tint,
15223
+ baseline: baseDown,
15224
+ orientation: "down"
15225
+ }
15226
+ ) }),
15227
+ /* @__PURE__ */ jsx(
15228
+ GhostBand,
15229
+ {
15230
+ segments,
15231
+ x,
15232
+ top: bandTop,
15233
+ showLabels: true,
15234
+ labelColor: alpha(t25["text-primary"], 0.92),
15235
+ targetTempoSeconds
15236
+ }
15237
+ ),
15238
+ showDeviceLabels && /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
15239
+ /* @__PURE__ */ jsx(
15240
+ "text",
15241
+ {
15242
+ x: padL,
15243
+ y: padTop - 9,
15244
+ fontSize: 9,
15245
+ fontWeight: 800,
15246
+ letterSpacing: 1,
15247
+ fontFamily: FONT_UI,
15248
+ fill: t25["text-tertiary"],
15249
+ children: leftLabel
15250
+ }
15251
+ ),
15252
+ /* @__PURE__ */ jsx(
15253
+ "text",
15254
+ {
15255
+ x: padL,
15256
+ y: h - padBot + 15,
15257
+ fontSize: 9,
15258
+ fontWeight: 800,
15259
+ letterSpacing: 1,
15260
+ fontFamily: FONT_UI,
15261
+ fill: t25["text-tertiary"],
15262
+ children: rightLabel
15263
+ }
15264
+ )
15265
+ ] })
15266
+ ] }) });
15267
+ }
13798
15268
  var PALETTE = DATAVIZ_CATEGORICAL_PALETTE;
13799
15269
  function scaleValue(v, scale) {
13800
15270
  if (v <= 0) return 0;
@@ -13883,7 +15353,7 @@ function Treemap({
13883
15353
  const rects = squarify(weighted, width, height);
13884
15354
  return /* @__PURE__ */ jsx(reactNative.View, { className: cn("relative", className), style: { width, height }, ...props, children: rects.map((r) => {
13885
15355
  const selected = r.datum.id === selectedId;
13886
- const labelFits = r.w > 54 && r.h > 22;
15356
+ const labelFits2 = r.w > 54 && r.h > 22;
13887
15357
  const pressable = r.datum.id !== "__more__";
13888
15358
  return /* @__PURE__ */ jsx(
13889
15359
  reactNative.Pressable,
@@ -13905,7 +15375,7 @@ function Treemap({
13905
15375
  padding: 4,
13906
15376
  overflow: "hidden"
13907
15377
  },
13908
- children: labelFits && /* @__PURE__ */ jsx(reactNative.Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
15378
+ children: labelFits2 && /* @__PURE__ */ jsx(reactNative.Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
13909
15379
  },
13910
15380
  r.datum.id
13911
15381
  );
@@ -14001,28 +15471,28 @@ function Scatter({
14001
15471
  testID: "scatter-canvas",
14002
15472
  style: { position: "absolute", top: 0, left: 0, width, height, overflow: "hidden" },
14003
15473
  children: [
14004
- ticksOf(yd, TICK_COUNT).map((t16, i) => {
14005
- const y = toY(t16);
15474
+ ticksOf(yd, TICK_COUNT).map((t26, i) => {
15475
+ const y = toY(t26);
14006
15476
  return /* @__PURE__ */ jsx(
14007
15477
  reactNative.View,
14008
15478
  {
14009
15479
  accessibilityElementsHidden: true,
14010
15480
  testID: "scatter-gridline-y",
14011
15481
  style: { position: "absolute", left: PLOT_LEFT2, width: innerW, top: y, height: 1, backgroundColor: GRID_LINE2 },
14012
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t16) })
15482
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t26) })
14013
15483
  },
14014
15484
  `y-${i}`
14015
15485
  );
14016
15486
  }),
14017
- ticksOf(xd, TICK_COUNT).map((t16, i) => {
14018
- const x = toX(t16);
15487
+ ticksOf(xd, TICK_COUNT).map((t26, i) => {
15488
+ const x = toX(t26);
14019
15489
  return /* @__PURE__ */ jsx(
14020
15490
  reactNative.View,
14021
15491
  {
14022
15492
  accessibilityElementsHidden: true,
14023
15493
  testID: "scatter-gridline-x",
14024
15494
  style: { position: "absolute", top: PLOT_TOP, height: innerH, left: x, width: 1, backgroundColor: GRID_LINE2 },
14025
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t16) })
15495
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t26) })
14026
15496
  },
14027
15497
  `x-${i}`
14028
15498
  );
@@ -14095,7 +15565,7 @@ var DEFAULT_THRESHOLDS2 = [
14095
15565
  var ARC_START = 135;
14096
15566
  var ARC_SWEEP = 270;
14097
15567
  var SEGMENTS = 40;
14098
- function clamp012(n) {
15568
+ function clamp013(n) {
14099
15569
  if (Number.isNaN(n)) return 0;
14100
15570
  return Math.max(0, Math.min(1, n));
14101
15571
  }
@@ -14140,7 +15610,7 @@ function Gauge2({
14140
15610
  ...props
14141
15611
  }) {
14142
15612
  const span = max - min || 1;
14143
- const fill = clamp012((value - min) / span);
15613
+ const fill = clamp013((value - min) / span);
14144
15614
  const activeColor = (f) => color ?? bandColor2(f, thresholds, min, max);
14145
15615
  const ticks = buildTicks(fill, size, activeColor);
14146
15616
  const displayColor = color ?? bandColor2(fill, thresholds, min, max);
@@ -14157,23 +15627,23 @@ function Gauge2({
14157
15627
  testID: "gauge",
14158
15628
  ...props,
14159
15629
  children: [
14160
- ticks.map((t16) => /* @__PURE__ */ jsx(
15630
+ ticks.map((t26) => /* @__PURE__ */ jsx(
14161
15631
  reactNative.View,
14162
15632
  {
14163
15633
  accessibilityElementsHidden: true,
14164
15634
  testID: "gauge-segment",
14165
15635
  style: {
14166
15636
  position: "absolute",
14167
- left: t16.left - tickThickness / 2,
14168
- top: t16.top - tickLength / 2,
15637
+ left: t26.left - tickThickness / 2,
15638
+ top: t26.top - tickLength / 2,
14169
15639
  width: tickThickness,
14170
15640
  height: tickLength,
14171
15641
  borderRadius: tickThickness / 2,
14172
- backgroundColor: t16.color,
14173
- transform: [{ rotate: t16.rotate }]
15642
+ backgroundColor: t26.color,
15643
+ transform: [{ rotate: t26.rotate }]
14174
15644
  }
14175
15645
  },
14176
- t16.key
15646
+ t26.key
14177
15647
  )),
14178
15648
  /* @__PURE__ */ jsxs(reactNative.View, { style: { position: "absolute", top: 0, left: 0, width: size, height: size }, className: "items-center justify-center", children: [
14179
15649
  /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-baseline gap-0.5", children: [
@@ -14607,6 +16077,8 @@ exports.Avatar = Avatar;
14607
16077
  exports.AvatarBadge = AvatarBadge;
14608
16078
  exports.AvatarGroup = AvatarGroup;
14609
16079
  exports.AwardIcon = AwardIcon;
16080
+ exports.BAND_GAP = BAND_GAP;
16081
+ exports.BAND_H = BAND_H;
14610
16082
  exports.Badge = Badge;
14611
16083
  exports.BadgeText = BadgeText;
14612
16084
  exports.BaseBadge = BaseBadge;
@@ -14637,6 +16109,7 @@ exports.CircularTimer = CircularTimer;
14637
16109
  exports.Collapse = Collapse;
14638
16110
  exports.CollapseButton = CollapseButton;
14639
16111
  exports.CollapseContent = CollapseContent;
16112
+ exports.DRIFT_GREY = DRIFT_GREY;
14640
16113
  exports.DashboardShell = DashboardShell;
14641
16114
  exports.DataRow = DataRow;
14642
16115
  exports.DateTime = DateTime;
@@ -14649,18 +16122,25 @@ exports.Drawer = Drawer;
14649
16122
  exports.DrawerBody = DrawerBody;
14650
16123
  exports.DrawerFooter = DrawerFooter;
14651
16124
  exports.DrawerHeader = DrawerHeader;
16125
+ exports.DualGhostSpark = DualGhostSpark;
16126
+ exports.DualVelocityStrip = DualVelocityStrip;
14652
16127
  exports.DumbbellIcon = DumbbellIcon;
14653
16128
  exports.EmptyState = EmptyState;
14654
16129
  exports.ExerciseCard = ExerciseCard;
14655
16130
  exports.ExerciseCardHeading = ExerciseCardHeading;
14656
16131
  exports.ExerciseHeading = ExerciseHeading;
14657
16132
  exports.ExerciseIndicator = ExerciseIndicator;
16133
+ exports.FatigueLights = FatigueLights;
14658
16134
  exports.FatigueMeter = FatigueMeter;
14659
16135
  exports.FormActions = FormActions;
14660
16136
  exports.FormField = FormField;
14661
16137
  exports.FormRow = FormRow;
14662
16138
  exports.FormSection = FormSection;
16139
+ exports.GRIND_THRESHOLD = GRIND_THRESHOLD;
14663
16140
  exports.Gauge = Gauge2;
16141
+ exports.GhostBand = GhostBand;
16142
+ exports.GhostBloom = GhostBloom;
16143
+ exports.GhostSpark = GhostSpark;
14664
16144
  exports.HStack = HStack;
14665
16145
  exports.Heading = Heading;
14666
16146
  exports.HelpTip = HelpTip;
@@ -14683,6 +16163,8 @@ exports.ListItemDivider = ListItemDivider;
14683
16163
  exports.ListItemIcon = ListItemIcon;
14684
16164
  exports.ListItemTrailing = ListItemTrailing;
14685
16165
  exports.LiveAuraFrame = LiveAuraFrame;
16166
+ exports.LiveFatigueCard = LiveFatigueCard;
16167
+ exports.LiveFatiguePanel = LiveFatiguePanel;
14686
16168
  exports.METRIC_FONT = METRIC_FONT;
14687
16169
  exports.Menu = Menu;
14688
16170
  exports.MenuDivider = MenuDivider;
@@ -14707,6 +16189,8 @@ exports.ModalTitle = ModalTitle;
14707
16189
  exports.MuscleGroupChip = MuscleGroupChip;
14708
16190
  exports.NavItem = NavItem;
14709
16191
  exports.Overline = Overline;
16192
+ exports.PHASE_AXIS_COLOR = PHASE_AXIS_COLOR;
16193
+ exports.PRESSED_ELEVATION_LEVEL = PRESSED_ELEVATION_LEVEL;
14710
16194
  exports.Paragraph = Paragraph;
14711
16195
  exports.PasswordInput = PasswordInput;
14712
16196
  exports.PersonStandingIcon = PersonStandingIcon;
@@ -14720,12 +16204,18 @@ exports.PrBadge = PrBadge;
14720
16204
  exports.PrHistoryModal = PrHistoryModal;
14721
16205
  exports.Progress = Progress;
14722
16206
  exports.ProgressSteps = ProgressSteps;
16207
+ exports.RED_DEEP = RED_DEEP;
16208
+ exports.RED_LIGHT = RED_LIGHT;
16209
+ exports.RED_MID = RED_MID;
14723
16210
  exports.Radio = Radio;
14724
16211
  exports.RadioGroup = RadioGroup;
14725
16212
  exports.ReadinessCheck = ReadinessCheck;
14726
16213
  exports.RestTimer = RestTimer;
16214
+ exports.RomProgressionChart = RomProgressionChart;
14727
16215
  exports.SET_STRIP_VARIABLE_COLOR = SET_STRIP_VARIABLE_COLOR;
14728
16216
  exports.SET_STRIP_ZONES = SET_STRIP_ZONES;
16217
+ exports.SILVER = SILVER;
16218
+ exports.STATE_LABEL = STATE_LABEL;
14729
16219
  exports.SURFACE_LEVEL_TOKEN = SURFACE_LEVEL_TOKEN;
14730
16220
  exports.ScaleIcon = ScaleIcon;
14731
16221
  exports.Scatter = Scatter;
@@ -14774,6 +16264,7 @@ exports.Surface = Surface;
14774
16264
  exports.SurfaceContext = SurfaceContext;
14775
16265
  exports.SvgIcon = SvgIcon;
14776
16266
  exports.Switch = Switch;
16267
+ exports.TONE_COLOR = TONE_COLOR;
14777
16268
  exports.Tab = Tab;
14778
16269
  exports.TabList = TabList;
14779
16270
  exports.TabPanel = TabPanel;
@@ -14799,7 +16290,9 @@ exports.Treemap = Treemap;
14799
16290
  exports.TrendingDownIcon = TrendingDownIcon;
14800
16291
  exports.Typography = Typography;
14801
16292
  exports.VStack = VStack;
16293
+ exports.VelocityHero = VelocityHero;
14802
16294
  exports.VelocityStrip = VelocityStrip;
16295
+ exports.VerdictHero = VerdictHero;
14803
16296
  exports.VoltrasMark = VoltrasMark;
14804
16297
  exports.VolumeLandmarkBar = VolumeLandmarkBar;
14805
16298
  exports.WeekRow = WeekRow;
@@ -14810,6 +16303,8 @@ exports.ZoneTrack = ZoneTrack;
14810
16303
  exports.alpha = alpha;
14811
16304
  exports.applyThemePreset = applyThemePreset;
14812
16305
  exports.audiobookPreset = audiobookPreset;
16306
+ exports.auraForVerdict = auraForVerdict;
16307
+ exports.backgroundFrameDark = backgroundFrameDark;
14813
16308
  exports.bestTextColor = bestTextColor;
14814
16309
  exports.buttonSizes = buttonSizes;
14815
16310
  exports.calculateMeanVelocity = calculateMeanVelocity;
@@ -14848,6 +16343,7 @@ exports.getFieldValidationProps = getFieldValidationProps;
14848
16343
  exports.getGlowShadow = getGlowShadow;
14849
16344
  exports.getHoverColors = getHoverColors;
14850
16345
  exports.getLuminance = getLuminance;
16346
+ exports.getPressedRecessShadow = getPressedRecessShadow;
14851
16347
  exports.getResultColor = getResultColor;
14852
16348
  exports.getSemanticColors = getSemanticColors;
14853
16349
  exports.getStatusColor = getStatusColor;
@@ -14855,6 +16351,7 @@ exports.getThemeCSSVars = getThemeCSSVars;
14855
16351
  exports.getValidatedElevation = getValidatedElevation;
14856
16352
  exports.getVelocityZoneColor = getVelocityZoneColor;
14857
16353
  exports.getVelocityZoneName = getVelocityZoneName;
16354
+ exports.ghostLineColor = ghostLineColor;
14858
16355
  exports.gluestackConfig = gluestackConfig;
14859
16356
  exports.hasMaxLength = hasMaxLength;
14860
16357
  exports.hasMinLength = hasMinLength;
@@ -14868,10 +16365,12 @@ exports.lightThemeCSSVars = lightThemeCSSVars;
14868
16365
  exports.lighten = lighten;
14869
16366
  exports.linearGradient = linearGradient;
14870
16367
  exports.liveAuraColor = liveAuraColor;
16368
+ exports.mergePhaseSegments = mergePhaseSegments;
14871
16369
  exports.neumorphicShadows = neumorphicShadows;
14872
16370
  exports.onSurfaceColors = onSurfaceColors;
14873
16371
  exports.paceTone = paceTone;
14874
16372
  exports.paceToneColor = paceToneColor;
16373
+ exports.pressedLevel = pressedLevel;
14875
16374
  exports.primitiveBorderRadius = primitiveBorderRadius;
14876
16375
  exports.primitiveBreakpoints = primitiveBreakpoints;
14877
16376
  exports.primitiveColors = primitiveColors;
@@ -14893,9 +16392,11 @@ exports.semanticColorsLight = semanticColorsLight;
14893
16392
  exports.semanticTypography = semanticTypography;
14894
16393
  exports.sequentialEffort = sequentialEffort;
14895
16394
  exports.shadowColors = shadowColors;
16395
+ exports.smoothPath = smoothPath;
14896
16396
  exports.statusPillColor = statusPillColor;
14897
16397
  exports.surfaceBackground = surfaceBackground;
14898
16398
  exports.surfaceGradient = surfaceGradient;
16399
+ exports.surfaceRampDark = surfaceRampDark;
14899
16400
  exports.useOnSurfaceColor = useOnSurfaceColor;
14900
16401
  exports.useSurface = useSurface;
14901
16402
  exports.useSurfaceMode = useSurfaceMode;