@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.mjs CHANGED
@@ -2,7 +2,7 @@ import { jsx as jsx$1, jsxs as jsxs$1, Fragment } from 'react/jsx-runtime';
2
2
  import { Pressable, ActivityIndicator, Text, View, TextInput, Platform, StyleSheet, Easing, Image, Modal as Modal$1, ScrollView, Animated } from 'react-native';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
- import React25, { forwardRef, useState, createContext, useContext, useMemo, useRef, useEffect, useCallback, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
5
+ import React25, { forwardRef, useState, createContext, useContext, useMemo, useRef, useEffect, useCallback, useId, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
6
6
 
7
7
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
8
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -333,6 +333,21 @@ var primitiveColors = {
333
333
  black: "#000000",
334
334
  transparent: "transparent"
335
335
  };
336
+ var surfaceRampDark = {
337
+ inset: "#13100D",
338
+ // L*4.5 — sub-shell well / pressed
339
+ background: "#1C1916",
340
+ // L*9 — shell / frame
341
+ base: "#252321",
342
+ // L*13.5 — main content plane
343
+ elevated: "#2C2A28",
344
+ // L*17 — nav / rail
345
+ raised: "#31302F",
346
+ // L*20 — cards
347
+ overlay: "#373635"
348
+ // L*22.5 — hero / popover
349
+ };
350
+ var backgroundFrameDark = "#100D0A";
336
351
  var primitiveRamps = {
337
352
  red: {
338
353
  50: "#FFF4F4",
@@ -800,10 +815,19 @@ var semanticColorsLight = {
800
815
  "surface-overlay": primitiveColors.white,
801
816
  "surface-input": primitiveColors.neutral[50],
802
817
  // Input field background (filled variant)
818
+ // Deepest pressed pit (TD-surface-tokens, S-3) — light-mode counterpart of
819
+ // the dark ramp's `inset` plane; a placeholder pairing, not part of the
820
+ // dark-ramp investigation (surface.contract.test.ts is dark-mode only).
821
+ "surface-inset": primitiveColors.neutral[300],
803
822
  // Background colors (background-*)
804
823
  "background-base": "#EBEBEB",
805
824
  "background-default": primitiveColors.white,
806
825
  "background-subtle": primitiveColors.neutral[50],
826
+ // Frame/bezel chrome (TD-surface-tokens, S-3) — top bar + side nav shell,
827
+ // one step below `background-base` (darker still than `surface-inset`
828
+ // above, mirroring the dark-mode ordering). Placeholder pairing for light
829
+ // mode (see dark-mode note on `surface-inset` above).
830
+ "background-frame": primitiveColors.neutral[400],
807
831
  // Border colors (border-*)
808
832
  "border-default": "#E8E9EB",
809
833
  "border-subtle": primitiveColors.neutral[100],
@@ -819,6 +843,11 @@ var semanticColorsLight = {
819
843
  // Input field border on focus
820
844
  "border-input-error": primitiveColors.red[600],
821
845
  // Input field border on error
846
+ // Alpha hairline separators (surface-independent — composite toward black on
847
+ // light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
848
+ "hairline-subtle": "rgba(0, 0, 0, 0.06)",
849
+ "hairline-default": "rgba(0, 0, 0, 0.09)",
850
+ "hairline-strong": "rgba(0, 0, 0, 0.14)",
822
851
  // Interactive states (interactive-*)
823
852
  "interactive-hover": "rgba(55, 65, 81, 0.04)",
824
853
  "interactive-focus": "rgba(55, 65, 81, 0.12)",
@@ -911,25 +940,46 @@ var semanticColorsDark = {
911
940
  "text-inverse": primitiveColors.neutral[900],
912
941
  "text-link": "#828DF8",
913
942
  "text-link-hover": primitiveColors.blue[400],
914
- // Surface colors - dark backgrounds
915
- "surface-base": primitiveColors.charcoal[700],
916
- // main surface
917
- "surface-elevated": primitiveColors.charcoal[600],
918
- // elevated surface
919
- "surface-raised": primitiveColors.charcoal[500],
920
- // raised surface
921
- "surface-overlay": primitiveColors.charcoal[600],
922
- // overlay surface
923
- "surface-input": primitiveColors.charcoal[600],
924
- // input surface
925
- // Background colors
926
- "background-base": primitiveColors.charcoal[900],
927
- // darkest charcoal
928
- "background-default": primitiveColors.charcoal[700],
929
- // main background
930
- "background-subtle": primitiveColors.charcoal[500],
931
- // subtle background
932
- // Border colors
943
+ // Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
944
+ // S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
945
+ // `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
946
+ // and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
947
+ // tracks one plane above `surface-base`, same relative position as before the remap.
948
+ // `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
949
+ // so it takes the ramp's `background` (shell/frame) role, NOT `inset`. The ramp's
950
+ // deepest `inset` plane is now promoted to `surface-inset` below (S-3) — still no
951
+ // `SurfaceLevel` member for it (follow-up, alongside the elevation.ts -1/-2 pressed
952
+ // levels — parallel workstream, not this branch).
953
+ "surface-base": surfaceRampDark.base,
954
+ // main surface (#252321, L*13.5)
955
+ "surface-elevated": surfaceRampDark.elevated,
956
+ // elevated surface (#2C2A28, L*17 — nav/rail)
957
+ "surface-raised": surfaceRampDark.raised,
958
+ // raised surface (#31302F, L*20 — cards)
959
+ "surface-overlay": surfaceRampDark.overlay,
960
+ // overlay surface (#373635, L*22.5 — hero/popover)
961
+ "surface-input": surfaceRampDark.elevated,
962
+ // input surface (#2C2A28 — one plane above base)
963
+ "surface-inset": surfaceRampDark.inset,
964
+ // deepest pressed pit (#13100D, L*4.5 — sub-shell well)
965
+ // Background colors — same ramp, the frame/shell end of it.
966
+ "background-base": surfaceRampDark.background,
967
+ // shell / frame (#1C1916, L*9 — Surface level="background")
968
+ "background-default": surfaceRampDark.base,
969
+ // main background (#252321, L*13.5 — matches surface-base)
970
+ "background-subtle": surfaceRampDark.elevated,
971
+ // subtle background (#2C2A28, L*17 — matches surface-elevated)
972
+ // Frame/bezel chrome (S-3) — the top bar + side nav shell; one step BELOW
973
+ // `background-base`, darker even than `surface-inset` — it exits the content
974
+ // ramp entirely rather than sitting in it. See `backgroundFrameDark` in
975
+ // primitives.ts.
976
+ "background-frame": backgroundFrameDark,
977
+ // frame / bezel (#100D0A, L*3.79)
978
+ // Border colors — `border-subtle` (#1C1C1C) previously collided with
979
+ // `surface-raised` (also #1C1C1C, an invisible border on raised cards). The
980
+ // re-spaced ramp above moved `surface-raised` to #2D2C2B, so this value is now
981
+ // distinct from every surface hex without needing to change it — see
982
+ // `surface.contract.test.ts` R2/R4.
933
983
  "border-default": primitiveColors.charcoal[400],
934
984
  // default border
935
985
  "border-subtle": primitiveColors.charcoal[500],
@@ -943,6 +993,14 @@ var semanticColorsDark = {
943
993
  "border-input-hover": primitiveColors.neutral[500],
944
994
  "border-input-focus": "#828DF8",
945
995
  "border-input-error": primitiveColors.red[500],
996
+ // Alpha hairline separators — the primary separation cue (§4/S-2). Self-
997
+ // normalizing: composites toward white by ~the same amount on ANY plane, so
998
+ // one family works at every elevation instead of per-surface border tokens.
999
+ // Shadows are demoted to floating-overlay use only (not shipped as a fill
1000
+ // separator here — see elevation.ts, follow-up S-4).
1001
+ "hairline-subtle": "rgba(255, 255, 255, 0.06)",
1002
+ "hairline-default": "rgba(255, 255, 255, 0.09)",
1003
+ "hairline-strong": "rgba(255, 255, 255, 0.14)",
946
1004
  // Interactive states
947
1005
  "interactive-hover": "rgba(255, 255, 255, 0.04)",
948
1006
  "interactive-focus": "rgba(255, 255, 255, 0.12)",
@@ -1472,9 +1530,11 @@ var lightThemeCSSVars = {
1472
1530
  "--color-surface-base": semanticColorsLight["surface-base"],
1473
1531
  "--color-surface-elevated": semanticColorsLight["surface-elevated"],
1474
1532
  "--color-surface-raised": semanticColorsLight["surface-raised"],
1533
+ "--color-surface-inset": semanticColorsLight["surface-inset"],
1475
1534
  "--color-background-base": semanticColorsLight["background-base"],
1476
1535
  "--color-background-default": semanticColorsLight["background-default"],
1477
1536
  "--color-background-subtle": semanticColorsLight["background-subtle"],
1537
+ "--color-background-frame": semanticColorsLight["background-frame"],
1478
1538
  "--color-border-default": semanticColorsLight["border-default"],
1479
1539
  "--color-border-subtle": semanticColorsLight["border-subtle"],
1480
1540
  "--color-border-strong": semanticColorsLight["border-strong"],
@@ -1531,6 +1591,9 @@ var lightThemeCSSVars = {
1531
1591
  "--color-border-input-hover": semanticColorsLight["border-input-hover"],
1532
1592
  "--color-border-input-focus": semanticColorsLight["border-input-focus"],
1533
1593
  "--color-border-input-error": semanticColorsLight["border-input-error"],
1594
+ "--color-hairline-subtle": semanticColorsLight["hairline-subtle"],
1595
+ "--color-hairline-default": semanticColorsLight["hairline-default"],
1596
+ "--color-hairline-strong": semanticColorsLight["hairline-strong"],
1534
1597
  "--color-interactive-disabled-text": semanticColorsLight["interactive-disabled-text"],
1535
1598
  "--color-avatar-background": semanticColorsLight["avatar-background"],
1536
1599
  "--color-avatar-text": semanticColorsLight["avatar-text"],
@@ -1570,9 +1633,11 @@ var darkThemeCSSVars = {
1570
1633
  "--color-surface-base": semanticColorsDark["surface-base"],
1571
1634
  "--color-surface-elevated": semanticColorsDark["surface-elevated"],
1572
1635
  "--color-surface-raised": semanticColorsDark["surface-raised"],
1636
+ "--color-surface-inset": semanticColorsDark["surface-inset"],
1573
1637
  "--color-background-base": semanticColorsDark["background-base"],
1574
1638
  "--color-background-default": semanticColorsDark["background-default"],
1575
1639
  "--color-background-subtle": semanticColorsDark["background-subtle"],
1640
+ "--color-background-frame": semanticColorsDark["background-frame"],
1576
1641
  "--color-border-default": semanticColorsDark["border-default"],
1577
1642
  "--color-border-subtle": semanticColorsDark["border-subtle"],
1578
1643
  "--color-border-strong": semanticColorsDark["border-strong"],
@@ -1629,6 +1694,9 @@ var darkThemeCSSVars = {
1629
1694
  "--color-border-input-hover": semanticColorsDark["border-input-hover"],
1630
1695
  "--color-border-input-focus": semanticColorsDark["border-input-focus"],
1631
1696
  "--color-border-input-error": semanticColorsDark["border-input-error"],
1697
+ "--color-hairline-subtle": semanticColorsDark["hairline-subtle"],
1698
+ "--color-hairline-default": semanticColorsDark["hairline-default"],
1699
+ "--color-hairline-strong": semanticColorsDark["hairline-strong"],
1632
1700
  "--color-interactive-disabled-text": semanticColorsDark["interactive-disabled-text"],
1633
1701
  "--color-avatar-background": semanticColorsDark["avatar-background"],
1634
1702
  "--color-avatar-text": semanticColorsDark["avatar-text"],
@@ -2263,6 +2331,10 @@ function getElevationShadow(baseColor, level, theme, isHovered = false) {
2263
2331
  elevationShadowCache.set(cacheKey, result);
2264
2332
  return result;
2265
2333
  }
2334
+ var PRESSED_ELEVATION_LEVEL = -1;
2335
+ function getPressedRecessShadow(fillColor, theme) {
2336
+ return getElevationShadow(fillColor, PRESSED_ELEVATION_LEVEL, theme);
2337
+ }
2266
2338
  function getBaseSurfaceColor(theme) {
2267
2339
  return theme === "light" ? semanticColorsLight["surface-raised"] : semanticColorsDark["surface-elevated"];
2268
2340
  }
@@ -2565,6 +2637,7 @@ var SURFACE_LEVEL_TOKEN = {
2565
2637
  raised: "surface-raised",
2566
2638
  overlay: "surface-overlay"
2567
2639
  };
2640
+ var PRESSED_RAMP = ["inset", "background", "base", "elevated", "raised", "overlay"];
2568
2641
  var ON_SURFACE_TOKEN = {
2569
2642
  primary: "text-primary",
2570
2643
  secondary: "text-secondary",
@@ -2589,9 +2662,19 @@ function onSurfaceColors(mode) {
2589
2662
  function useOnSurfaceColor(role = "primary") {
2590
2663
  return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
2591
2664
  }
2665
+ function insetFloor(mode) {
2666
+ const colors = getSemanticColors(mode);
2667
+ return colors["surface-inset"] ?? surfaceRampDark.inset;
2668
+ }
2592
2669
  function surfaceBackground(level, mode) {
2670
+ if (level === "inset") return insetFloor(mode);
2593
2671
  return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
2594
2672
  }
2673
+ function pressedLevel(parent) {
2674
+ const index = PRESSED_RAMP.indexOf(parent);
2675
+ const down = Math.max(0, index - 1);
2676
+ return PRESSED_RAMP[down];
2677
+ }
2595
2678
  function getTheme() {
2596
2679
  if (typeof document === "undefined") return "dark";
2597
2680
  return document.documentElement.classList.contains("light") ? "light" : "dark";
@@ -5290,7 +5373,7 @@ function ToastProvider({
5290
5373
  return id;
5291
5374
  }, [defaultDuration, maxToasts]);
5292
5375
  const removeToast = useCallback((id) => {
5293
- setToasts((prev) => prev.filter((t16) => t16.id !== id));
5376
+ setToasts((prev) => prev.filter((t26) => t26.id !== id));
5294
5377
  }, []);
5295
5378
  const removeAllToasts = useCallback(() => {
5296
5379
  setToasts([]);
@@ -6043,6 +6126,7 @@ function IconBox({
6043
6126
  function Surface({
6044
6127
  elevation = 0,
6045
6128
  level,
6129
+ pressed = false,
6046
6130
  glowColor,
6047
6131
  glowIntensity,
6048
6132
  theme,
@@ -6056,13 +6140,24 @@ function Surface({
6056
6140
  const mode = theme ?? inherited.mode;
6057
6141
  const isPlane = level != null;
6058
6142
  const baseColor = getBaseSurfaceColor(mode);
6059
- const backgroundColor = isPlane ? surfaceBackground(level, mode) : getElevationSurface(baseColor, elevation, mode);
6060
- const shadowStyle = isPlane ? {} : getElevationShadow(baseColor, elevation, mode);
6143
+ const resolvedLevel = pressed ? pressedLevel(inherited.level) : level ?? inherited.level;
6144
+ let backgroundColor;
6145
+ let shadowStyle;
6146
+ if (pressed) {
6147
+ backgroundColor = surfaceBackground(resolvedLevel, mode);
6148
+ shadowStyle = getPressedRecessShadow(backgroundColor, mode);
6149
+ } else if (isPlane) {
6150
+ backgroundColor = surfaceBackground(level, mode);
6151
+ shadowStyle = {};
6152
+ } else {
6153
+ backgroundColor = getElevationSurface(baseColor, elevation, mode);
6154
+ shadowStyle = getElevationShadow(baseColor, elevation, mode);
6155
+ }
6061
6156
  const glowStyle = glowColor ? getGlowShadow(glowColor, glowIntensity) : {};
6062
- const applyRounded = rounded ?? !isPlane;
6157
+ const applyRounded = rounded ?? (pressed || !isPlane);
6063
6158
  const value = useMemo(
6064
- () => ({ mode, level: level ?? inherited.level }),
6065
- [mode, level, inherited.level]
6159
+ () => ({ mode, level: resolvedLevel }),
6160
+ [mode, resolvedLevel]
6066
6161
  );
6067
6162
  return /* @__PURE__ */ jsx(SurfaceContext.Provider, { value, children: /* @__PURE__ */ jsx(
6068
6163
  View,
@@ -7255,13 +7350,14 @@ function MetricGroup({
7255
7350
  )
7256
7351
  ] }, i)) });
7257
7352
  }
7353
+ var t = getSemanticColors("dark");
7258
7354
  var baseBadgeSizeConfig = {
7259
7355
  sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
7260
7356
  md: { fontSize: 10, paddingH: 8, paddingV: 2, iconSize: 12 },
7261
7357
  lg: { fontSize: 12, paddingH: 10, paddingV: 4, iconSize: 14 }
7262
7358
  };
7263
7359
  var variantColors = {
7264
- plain: { backgroundColor: "#1C1C1C", borderColor: "#1F1F1F" },
7360
+ plain: { backgroundColor: t["surface-raised"], borderColor: "#1F1F1F" },
7265
7361
  pr: {
7266
7362
  backgroundColor: "rgba(255, 121, 0, 0.12)",
7267
7363
  borderColor: "rgba(255, 121, 0, 0.3)"
@@ -7501,11 +7597,11 @@ function PrBadge({
7501
7597
  }
7502
7598
  return badge;
7503
7599
  }
7504
- var t = getSemanticColors("dark");
7600
+ var t2 = getSemanticColors("dark");
7505
7601
  var solidVariantColors = {
7506
- success: t["status-success"],
7507
- warning: t["status-warning"],
7508
- error: t["status-error"],
7602
+ success: t2["status-success"],
7603
+ warning: t2["status-warning"],
7604
+ error: t2["status-error"],
7509
7605
  neutral: "#6B7280"
7510
7606
  };
7511
7607
  var ringVariantStyles = {
@@ -7545,8 +7641,8 @@ var iconColors2 = {
7545
7641
  warning: "#6B4000",
7546
7642
  error: "#5C1A1A",
7547
7643
  neutral: "#D1D5DB",
7548
- "on-track": t["status-success"],
7549
- deviation: t["status-warning"],
7644
+ "on-track": t2["status-success"],
7645
+ deviation: t2["status-warning"],
7550
7646
  future: "#6B7280"
7551
7647
  };
7552
7648
  var sizeDimensions = {
@@ -7867,19 +7963,19 @@ function MetricCell({ children, color, fontSize = 11 }) {
7867
7963
  }
7868
7964
 
7869
7965
  // src/components/custom/Workout/TempoDisplay.tsx
7870
- var t2 = getSemanticColors("dark");
7966
+ var t3 = getSemanticColors("dark");
7871
7967
  var INTER = "Inter, sans-serif";
7872
7968
  var TEXT_TERTIARY = "#6B7280";
7873
- var STATUS_ERROR = t2["status-error"];
7874
- var STATUS_SUCCESS = t2["status-success"];
7875
- var STATUS_WARNING = t2["status-warning"];
7969
+ var STATUS_ERROR = t3["status-error"];
7970
+ var STATUS_SUCCESS = t3["status-success"];
7971
+ var STATUS_WARNING = t3["status-warning"];
7876
7972
  var ON_TARGET_MS = 100;
7877
7973
  function getTempoFillPct(elapsedMs, targetMs) {
7878
7974
  if (!targetMs) return 100;
7879
7975
  return Math.min(100, elapsedMs / targetMs * 100);
7880
7976
  }
7881
7977
  function activeNumberTone(elapsedMs, targetMs) {
7882
- if (targetMs == null) return t2["text-primary"];
7978
+ if (targetMs == null) return t3["text-primary"];
7883
7979
  const remainingMs = targetMs - elapsedMs;
7884
7980
  if (remainingMs > ON_TARGET_MS) return STATUS_WARNING;
7885
7981
  if (remainingMs >= -ON_TARGET_MS) return STATUS_SUCCESS;
@@ -8031,7 +8127,7 @@ function TempoDisplay({
8031
8127
  style: {
8032
8128
  flexDirection: "row",
8033
8129
  alignItems: "center",
8034
- backgroundColor: "#1C1C1C",
8130
+ backgroundColor: t3["surface-raised"],
8035
8131
  paddingHorizontal: chromePadX,
8036
8132
  paddingVertical: chromePadY,
8037
8133
  borderRadius: chromeRadius
@@ -8046,7 +8142,7 @@ function TempoDisplay({
8046
8142
  fontSize: labelFont,
8047
8143
  fontWeight: "500",
8048
8144
  // Live-muted green while a rep is running, tertiary at rest.
8049
- color: live ? t2["status-live-muted"] : TEXT_TERTIARY,
8145
+ color: live ? t3["status-live-muted"] : TEXT_TERTIARY,
8050
8146
  letterSpacing: 0.5,
8051
8147
  textTransform: "uppercase",
8052
8148
  marginRight: Math.round(fontSize * 0.5)
@@ -8104,7 +8200,7 @@ function TempoDisplay({
8104
8200
  View,
8105
8201
  {
8106
8202
  style: {
8107
- backgroundColor: "#191919",
8203
+ backgroundColor: t3["surface-overlay"],
8108
8204
  borderRadius: 6,
8109
8205
  paddingVertical: 8,
8110
8206
  paddingHorizontal: 12,
@@ -8157,13 +8253,13 @@ function TempoDisplay({
8157
8253
  }
8158
8254
  );
8159
8255
  }
8160
- var t3 = getSemanticColors("dark");
8256
+ var t4 = getSemanticColors("dark");
8161
8257
  function getDotColor(deviation) {
8162
8258
  const abs = Math.abs(deviation);
8163
- if (deviation < -0.3) return t3["status-success"];
8259
+ if (deviation < -0.3) return t4["status-success"];
8164
8260
  if (abs <= 0.3) return "#6B7280";
8165
- if (abs <= 0.7) return t3["status-warning"];
8166
- return t3["status-error"];
8261
+ if (abs <= 0.7) return t4["status-warning"];
8262
+ return t4["status-error"];
8167
8263
  }
8168
8264
  function getDeviationDescription(deviation) {
8169
8265
  if (deviation < -0.3) return "lighter than planned";
@@ -8231,19 +8327,19 @@ function DeviationBar({
8231
8327
  }
8232
8328
  );
8233
8329
  }
8234
- var t4 = getSemanticColors("dark");
8330
+ var t5 = getSemanticColors("dark");
8235
8331
  var TRACK_BG = "#333333";
8236
8332
  var LABEL_COLOR = "#6B7280";
8237
8333
  var TARGET_LINE_COLOR = "rgba(33, 150, 243, 0.5)";
8238
8334
  var AT_TARGET_GLOW = "0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)";
8239
8335
  var ZONE = {
8240
- building: t4["status-success"],
8336
+ building: t5["status-success"],
8241
8337
  // below target ramp-up
8242
- approaching: t4["status-warning"],
8338
+ approaching: t5["status-warning"],
8243
8339
  // nearing target
8244
- target: t4["brand-primary"],
8340
+ target: t5["brand-primary"],
8245
8341
  // exactly at target
8246
- over1: t4["status-error"],
8342
+ over1: t5["status-error"],
8247
8343
  // mild over-target
8248
8344
  over2: WORKOUT_TOKENS.intensity.over2,
8249
8345
  // moderate over-target
@@ -8412,12 +8508,12 @@ var MESO_ACCENT_GRADIENT_LIGHT = primitiveRamps.orange[300];
8412
8508
  var WORKOUT_PILL_DELOAD = primitiveRamps.magenta[600];
8413
8509
 
8414
8510
  // src/components/custom/Workout/WorkoutPill.tsx
8415
- var t5 = getSemanticColors("dark");
8511
+ var t6 = getSemanticColors("dark");
8416
8512
  var statusBgColors = {
8417
8513
  completed: "rgba(46,213,115,0.15)",
8418
8514
  current: "rgba(255,121,0,0.15)",
8419
8515
  next: "transparent",
8420
- upcoming: "#1C1C1C",
8516
+ upcoming: t6["surface-raised"],
8421
8517
  missed: "rgba(209,67,67,0.1)",
8422
8518
  deload: "rgba(186,41,150,0.15)"
8423
8519
  };
@@ -8430,9 +8526,9 @@ var statusBorderStyles = {
8430
8526
  deload: { borderWidth: 1, borderColor: "rgba(186,41,150,0.3)" }
8431
8527
  };
8432
8528
  var textColors = {
8433
- completed: t5["status-success"],
8434
- current: t5["brand-primary"],
8435
- next: t5["brand-primary"],
8529
+ completed: t6["status-success"],
8530
+ current: t6["brand-primary"],
8531
+ next: t6["brand-primary"],
8436
8532
  upcoming: "#6B7280",
8437
8533
  missed: "rgba(209,67,67,0.7)",
8438
8534
  deload: WORKOUT_PILL_DELOAD
@@ -8565,257 +8661,412 @@ function WorkoutPill({
8565
8661
  }
8566
8662
  return pill;
8567
8663
  }
8568
- var PULSE_HALF_MS = 950;
8569
- var PULSE_MIN_OPACITY = 0.45;
8570
- var SEGMENTED_BAR_GAP = 5;
8571
- function usePulse2(active) {
8572
- const [value] = useState(() => new Animated.Value(0));
8664
+
8665
+ // src/theme/bar-paper.ts
8666
+ function grainOpacityForColor(hex) {
8667
+ const h = hex.replace("#", "");
8668
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
8669
+ const r = parseInt(full.slice(0, 2), 16);
8670
+ const g = parseInt(full.slice(2, 4), 16);
8671
+ const b = parseInt(full.slice(4, 6), 16);
8672
+ if ([r, g, b].some(Number.isNaN)) return 0.14;
8673
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
8674
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
8675
+ }
8676
+ function barGrain(color) {
8677
+ const op = grainOpacityForColor(color).toFixed(3);
8678
+ 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")`;
8679
+ }
8680
+ function barPaper(color, flip = false) {
8681
+ return {
8682
+ backgroundImage: barGrain(color),
8683
+ 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)"
8684
+ };
8685
+ }
8686
+ var ANIMATION_EASING = Easing.bezier(0.22, 1, 0.36, 1);
8687
+ var PEAK_OVERSHOOT = 1.12;
8688
+ function getReducedMotionPreference() {
8689
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8690
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8691
+ }
8692
+ function usePrefersReducedMotion() {
8693
+ const [reduced, setReduced] = useState(getReducedMotionPreference);
8573
8694
  useEffect(() => {
8574
- if (!active) return;
8575
- const loop = Animated.loop(
8695
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
8696
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
8697
+ const handler = () => setReduced(mq.matches);
8698
+ handler();
8699
+ mq.addEventListener?.("change", handler);
8700
+ return () => mq.removeEventListener?.("change", handler);
8701
+ }, []);
8702
+ return reduced;
8703
+ }
8704
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
8705
+ const prefersReducedMotion = usePrefersReducedMotion();
8706
+ const [liveGrowth] = useState(() => new Animated.Value(1));
8707
+ useEffect(() => {
8708
+ if (liveRepIndex == null || liveVelocity == null) return;
8709
+ if (prefersReducedMotion) {
8710
+ liveGrowth.setValue(1);
8711
+ return;
8712
+ }
8713
+ liveGrowth.setValue(0);
8714
+ if (isNewPeak) {
8576
8715
  Animated.sequence([
8577
- Animated.timing(value, {
8578
- toValue: 1,
8579
- duration: PULSE_HALF_MS,
8580
- easing: Easing.inOut(Easing.ease),
8716
+ Animated.timing(liveGrowth, {
8717
+ toValue: PEAK_OVERSHOOT,
8718
+ duration: 220,
8719
+ easing: Easing.out(Easing.quad),
8581
8720
  useNativeDriver: false
8582
8721
  }),
8583
- Animated.timing(value, {
8584
- toValue: 0,
8585
- duration: PULSE_HALF_MS,
8586
- easing: Easing.inOut(Easing.ease),
8722
+ Animated.spring(liveGrowth, {
8723
+ toValue: 1,
8724
+ friction: 5,
8725
+ tension: 140,
8587
8726
  useNativeDriver: false
8588
8727
  })
8589
- ])
8590
- );
8591
- loop.start();
8592
- return () => loop.stop();
8593
- }, [active, value]);
8594
- return value;
8728
+ ]).start();
8729
+ } else {
8730
+ Animated.timing(liveGrowth, {
8731
+ toValue: 1,
8732
+ duration: 300,
8733
+ easing: ANIMATION_EASING,
8734
+ useNativeDriver: false
8735
+ }).start();
8736
+ }
8737
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
8738
+ return liveGrowth;
8595
8739
  }
8596
- function SegmentedBar({
8597
- segments,
8598
- height = 8,
8599
- gap = SEGMENTED_BAR_GAP,
8600
- radius = 2,
8601
- marker = null,
8602
- segmentTestID,
8603
- style,
8604
- ...props
8605
- }) {
8606
- const pulse = usePulse2(segments.some((s) => s.pulse));
8607
- return /* @__PURE__ */ jsxs(View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
8608
- segments.map((seg, i) => {
8609
- const fillStyle = {
8610
- width: `${(seg.fill ?? 1) * 100}%`,
8611
- height: "100%",
8612
- backgroundColor: seg.color,
8613
- opacity: seg.opacity
8614
- };
8615
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
8616
- return /* @__PURE__ */ jsx(
8740
+
8741
+ // src/components/custom/charts/SetBarChart.tsx
8742
+ function mixHex(a, b, t26) {
8743
+ const parse = (h) => {
8744
+ const s = h.replace("#", "");
8745
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
8746
+ };
8747
+ const [ar, ag, ab] = parse(a);
8748
+ const [br, bg, bb] = parse(b);
8749
+ const ch = (x, y) => Math.round(x + (y - x) * t26).toString(16).padStart(2, "0");
8750
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
8751
+ }
8752
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
8753
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8754
+ var FIXED_MAX_VALUE = 1.15;
8755
+ var PEAK_HEADROOM = 1.03;
8756
+ function scaleDenominator(scale, maxValue) {
8757
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
8758
+ }
8759
+ function valueLabelFontSize(height) {
8760
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
8761
+ }
8762
+ var SET_BAR_DEFAULT_HEIGHT = 220;
8763
+ var LABEL_ROW_HEIGHT = 16;
8764
+ var LABEL_GAP = 3;
8765
+ var GAP_RATIO = 0.08;
8766
+ var MIN_BAR_GAP = 2;
8767
+ var CHUNK_NOTCH_RATIO = 0.25;
8768
+ var CHUNK_NOTCH_MIN_PX = 10;
8769
+ var BAR_MAX_WIDTH = 120;
8770
+ var LABEL_MIN_BAR_WIDTH = 30;
8771
+ var DEFAULT_BAR_RADIUS = 5;
8772
+ var MIN_BAR_HEIGHT = 4;
8773
+ var FLAT_BAR_FRACTION = 0.5;
8774
+ var STUB_HEIGHT = 9;
8775
+ var REP_GAP = 2;
8776
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
8777
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
8778
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
8779
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
8780
+ return { barWidth, gap };
8781
+ }
8782
+ function isRep(slot) {
8783
+ return slot.kind === "rep";
8784
+ }
8785
+ function repIndexByCell(cells) {
8786
+ let count = 0;
8787
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
8788
+ }
8789
+ function SetBarChart({
8790
+ slots,
8791
+ colorFor,
8792
+ height,
8793
+ scale = "peak",
8794
+ scaleMax,
8795
+ orientation = "up",
8796
+ liveRepIndex,
8797
+ isNewPeak = false,
8798
+ targetReps,
8799
+ gapRatio = GAP_RATIO,
8800
+ barRadius = DEFAULT_BAR_RADIUS,
8801
+ cornerStyle = "top",
8802
+ flat = false,
8803
+ showValueLabels: showValueLabelsProp = false,
8804
+ formatValue: formatValue3 = String,
8805
+ todoVariant = "solid",
8806
+ minColumns,
8807
+ renderReference,
8808
+ renderBarOverlay,
8809
+ expandProgress,
8810
+ hideBaseline = false,
8811
+ testID,
8812
+ testIDPrefix = "setbar",
8813
+ accessibilityLabel: accessibilityLabel2,
8814
+ label,
8815
+ className,
8816
+ viewProps = {}
8817
+ }) {
8818
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
8819
+ const cells = targetPadded > slots.length ? [
8820
+ ...slots,
8821
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
8822
+ ] : slots;
8823
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
8824
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
8825
+ const flip = orientation === "down";
8826
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
8827
+ const placeholderColor = useOnSurfaceColor("tertiary");
8828
+ const surface = useSurface();
8829
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
8830
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
8831
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
8832
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
8833
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
8834
+ const showValueLabels = showValueLabelsProp && !flat;
8835
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
8836
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
8837
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
8838
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
8839
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
8840
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
8841
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
8842
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
8843
+ const [plotW, setPlotW] = useState(0);
8844
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
8845
+ const totalCells = cells.length;
8846
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
8847
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
8848
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
8849
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
8850
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
8851
+ const { style: externalStyle, ...restProps } = viewProps;
8852
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
8853
+ const repIndices = repIndexByCell(cells);
8854
+ return /* @__PURE__ */ jsxs(
8855
+ View,
8856
+ {
8857
+ className,
8858
+ style: [{ height, flexDirection: "row" }, externalStyle],
8859
+ testID,
8860
+ ...a11y,
8861
+ ...restProps,
8862
+ children: [
8863
+ label ? /* @__PURE__ */ jsx(
8864
+ ChartSideRail,
8865
+ {
8866
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
8867
+ sectionExtent: height
8868
+ }
8869
+ ) : null,
8870
+ /* @__PURE__ */ jsxs(
8871
+ View,
8872
+ {
8873
+ onLayout: onPlotLayout,
8874
+ style: {
8875
+ flex: 1,
8876
+ flexDirection: "row",
8877
+ alignItems: "flex-end",
8878
+ gap,
8879
+ position: "relative",
8880
+ borderBottomWidth: hideBaseline ? 0 : 2,
8881
+ borderBottomColor: placeholderColor,
8882
+ ...flipStyle
8883
+ },
8884
+ children: [
8885
+ renderReference?.(geometry),
8886
+ cells.map((slot, i) => {
8887
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
8888
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
8889
+ const column = {
8890
+ flex: 1,
8891
+ maxWidth: BAR_MAX_WIDTH,
8892
+ height: "100%",
8893
+ alignItems: "center",
8894
+ justifyContent: "flex-end",
8895
+ marginLeft: extraGap
8896
+ };
8897
+ if (!isRep(slot)) {
8898
+ return /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, style: column, children: renderStub(
8899
+ slot.kind,
8900
+ barCorners,
8901
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
8902
+ flat ? compactBarHeight : STUB_HEIGHT,
8903
+ placeholderColor,
8904
+ testIDPrefix,
8905
+ todoVariant,
8906
+ solidTodoColor,
8907
+ emptyColor
8908
+ ) }, i);
8909
+ }
8910
+ const repIndex = repIndices[i];
8911
+ const value = slot.value ?? 0;
8912
+ const isLive = liveRepIndex === repIndex;
8913
+ const color = colorFor(value);
8914
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
8915
+ inputRange: [0, 1],
8916
+ outputRange: [flatBarHeight, valueBarHeight(value)]
8917
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
8918
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
8919
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
8920
+ return /* @__PURE__ */ jsxs(View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
8921
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
8922
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
8923
+ /* @__PURE__ */ jsx(
8924
+ Animated.View,
8925
+ {
8926
+ style: {
8927
+ position: "absolute",
8928
+ bottom: labelBottom,
8929
+ left: 0,
8930
+ right: 0,
8931
+ alignItems: "center",
8932
+ ...expandProgress ? { opacity: expandProgress } : null
8933
+ },
8934
+ pointerEvents: "none",
8935
+ children: /* @__PURE__ */ jsx(
8936
+ Text,
8937
+ {
8938
+ className: "text-text-secondary",
8939
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
8940
+ testID: `${testIDPrefix}-label-${repIndex}`,
8941
+ children: formatValue3(value)
8942
+ }
8943
+ )
8944
+ }
8945
+ ),
8946
+ /* @__PURE__ */ jsx(
8947
+ Animated.View,
8948
+ {
8949
+ style: [
8950
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
8951
+ barCorners,
8952
+ barPaper(color, flip)
8953
+ ],
8954
+ testID: `${testIDPrefix}-bar-${repIndex}`
8955
+ }
8956
+ ),
8957
+ renderBarOverlay?.(repIndex, value)
8958
+ ] }, i);
8959
+ })
8960
+ ]
8961
+ }
8962
+ )
8963
+ ]
8964
+ }
8965
+ );
8966
+ }
8967
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
8968
+ const base = {
8969
+ width: "100%",
8970
+ height: stubHeight,
8971
+ ...barCorners
8972
+ };
8973
+ if (kind === "variable") {
8974
+ return /* @__PURE__ */ jsx(
8975
+ View,
8976
+ {
8977
+ style: { ...base, backgroundColor: VARIABLE_FILL },
8978
+ testID: `${testIDPrefix}-slot-variable`
8979
+ }
8980
+ );
8981
+ }
8982
+ if (kind === "continue") {
8983
+ return /* @__PURE__ */ jsx(
8984
+ View,
8985
+ {
8986
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
8987
+ testID: `${testIDPrefix}-slot-continue`
8988
+ }
8989
+ );
8990
+ }
8991
+ if (kind === "empty") {
8992
+ return /* @__PURE__ */ jsx(
8993
+ View,
8994
+ {
8995
+ style: { ...base, backgroundColor: emptyColor },
8996
+ testID: `${testIDPrefix}-slot-empty`
8997
+ }
8998
+ );
8999
+ }
9000
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
9001
+ return /* @__PURE__ */ jsx(View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
9002
+ }
9003
+ function sideLabelFontSize(height) {
9004
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
9005
+ }
9006
+ var SIDE_LABEL_INITIALS_BELOW = 70;
9007
+ function sideLabelText(label, extent) {
9008
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
9009
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
9010
+ }
9011
+ function ChartSideRail({
9012
+ sections,
9013
+ sectionExtent,
9014
+ variant = "hero"
9015
+ }) {
9016
+ if (!sections.some((s) => s.label)) return null;
9017
+ const isRail = variant === "rail";
9018
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
9019
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
9020
+ const textStyle = {
9021
+ maxWidth: "100%",
9022
+ textAlign: "center",
9023
+ fontSize,
9024
+ fontWeight: "700",
9025
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
9026
+ textTransform: "uppercase"
9027
+ };
9028
+ return /* @__PURE__ */ jsx(View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
9029
+ View,
9030
+ {
9031
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
9032
+ children: s.label ? /* @__PURE__ */ jsx(
8617
9033
  View,
8618
9034
  {
8619
9035
  style: {
8620
- flex: seg.weight ?? 1,
8621
- minWidth: 0,
8622
- height: "100%",
8623
- marginLeft: seg.leadingGap,
8624
- borderRadius: radius,
8625
- overflow: "hidden"
9036
+ width: labelLength,
9037
+ alignItems: "center",
9038
+ transform: [{ rotate: "-90deg" }]
8626
9039
  },
8627
- children: seg.pulse ? /* @__PURE__ */ jsx(
8628
- Animated.View,
9040
+ children: /* @__PURE__ */ jsx(
9041
+ Text,
8629
9042
  {
8630
- style: {
8631
- ...fillStyle,
8632
- ...seg.pulseColor ? {
8633
- backgroundColor: pulse.interpolate({
8634
- inputRange: [0, 1],
8635
- outputRange: seg.pulseColor
8636
- })
8637
- } : {
8638
- opacity: pulse.interpolate({
8639
- inputRange: [0, 1],
8640
- outputRange: [PULSE_MIN_OPACITY, 1]
8641
- })
8642
- }
8643
- },
8644
- accessibilityElementsHidden: true,
8645
- testID
9043
+ className: "text-text-tertiary",
9044
+ style: textStyle,
9045
+ numberOfLines: 1,
9046
+ testID: s.testID,
9047
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
8646
9048
  }
8647
- ) : /* @__PURE__ */ jsx(View, { style: fillStyle, accessibilityElementsHidden: true, testID })
8648
- },
8649
- i
8650
- );
8651
- }),
8652
- marker ? /* @__PURE__ */ jsx(
8653
- View,
8654
- {
8655
- style: {
8656
- position: "absolute",
8657
- left: `${marker.position * 100}%`,
8658
- top: 0,
8659
- bottom: 0,
8660
- width: 2,
8661
- backgroundColor: marker.color
8662
- },
8663
- accessibilityElementsHidden: true,
8664
- testID: "segmented-bar-marker"
8665
- }
8666
- ) : null
8667
- ] });
9049
+ )
9050
+ }
9051
+ ) : null
9052
+ },
9053
+ s.testID
9054
+ )) });
8668
9055
  }
8669
9056
 
8670
- // src/components/custom/Workout/SetBar.tsx
8671
- var SET_STRIP_ZONES = {
8672
- slow: primitiveRamps.red[600],
8673
- moderate: primitiveRamps.orange[400],
8674
- fast: primitiveRamps.amber[300],
8675
- fastest: primitiveRamps.green[300]
8676
- };
8677
- var TODO_COLOR = primitiveColors.charcoal[300];
8678
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
8679
- var NOTCH_GAP = 2;
8680
- var CLUSTER_GAP = 3;
8681
- var MYO_UPCOMING_CLUSTERS = 3;
8682
- var MYO_UPCOMING_CLUSTER_LEN = 5;
8683
- var MYO_UPCOMING_FADE = 0.6;
8684
- var PULSE_RANGE = {
8685
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
8686
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
8687
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
8688
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
8689
- };
8690
- function velocityZoneColor(v) {
8691
- if (v < 0.5) return SET_STRIP_ZONES.slow;
8692
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
8693
- if (v < 1) return SET_STRIP_ZONES.fast;
8694
- return SET_STRIP_ZONES.fastest;
8695
- }
8696
- function chunkedSegments(chunks, gap) {
8697
- return chunks.flatMap(
8698
- (vels, ci) => vels.map((v, ri) => ({
8699
- color: velocityZoneColor(v),
8700
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
8701
- }))
8702
- );
8703
- }
8704
- function rangeSegments(floor, max, doneVels) {
8705
- return Array.from({ length: max }, (_, i) => {
8706
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
8707
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
8708
- });
8709
- }
8710
- function myoUpcomingSegments(activationLen, clusterCount) {
8711
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
8712
- const trail = Array.from(
8713
- { length: clusterCount },
8714
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
8715
- color: SET_STRIP_VARIABLE_COLOR,
8716
- opacity: MYO_UPCOMING_FADE ** ci,
8717
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
8718
- }))
8719
- ).flat();
8720
- return [...activation, ...trail];
8721
- }
8722
- function setSegments(set) {
8723
- if (set.status === "todo") {
8724
- return [{ color: TODO_COLOR }];
8725
- }
8726
- if (set.status === "done") {
8727
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
8728
- }
8729
- if (set.status === "range") {
8730
- return rangeSegments(set.floor, set.max, set.doneVels);
8731
- }
8732
- if (set.status === "drop") {
8733
- return chunkedSegments(set.subloads, NOTCH_GAP);
8734
- }
8735
- if (set.status === "myo") {
8736
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
8737
- }
8738
- if (set.status === "myo-upcoming") {
8739
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
8740
- }
8741
- const performed = set.velocities.map((v) => {
8742
- const color = velocityZoneColor(v);
8743
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
8744
- });
8745
- const remaining = Math.max(0, set.planned - set.velocities.length);
8746
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
8747
- }
8748
- function setBarSegmentTestID(seg) {
8749
- if (seg.pulse) return "set-strip-pulse";
8750
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
8751
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
8752
- }
8753
- function SetBar({ set, height = 8 }) {
8754
- return /* @__PURE__ */ jsx(
8755
- SegmentedBar,
8756
- {
8757
- segments: setSegments(set),
8758
- height,
8759
- gap: 0,
8760
- radius: 0,
8761
- segmentTestID: setBarSegmentTestID,
8762
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
8763
- testID: "set-strip-set"
8764
- }
8765
- );
8766
- }
8767
-
8768
- // src/components/custom/Workout/SetStrip.tsx
8769
- var SET_STRIP_GAP = 5;
8770
- function setCategory(set) {
8771
- switch (set.status) {
8772
- case "done":
8773
- case "drop":
8774
- case "myo":
8775
- return "done";
8776
- case "active":
8777
- return "active";
8778
- case "range":
8779
- return set.doneVels.length > 0 ? "active" : "upcoming";
8780
- case "todo":
8781
- case "myo-upcoming":
8782
- return "upcoming";
8783
- }
8784
- }
8785
- function describeSets(sets) {
8786
- const done = sets.filter((s) => setCategory(s) === "done").length;
8787
- const active = sets.filter((s) => setCategory(s) === "active").length;
8788
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
8789
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
8790
- }
8791
- function SetStrip({ sets, height = 8, className, ...props }) {
8792
- return /* @__PURE__ */ jsx(
8793
- View,
8794
- {
8795
- className,
8796
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
8797
- accessibilityRole: "image",
8798
- accessibilityLabel: describeSets(sets),
8799
- testID: "set-strip",
8800
- ...props,
8801
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
8802
- }
8803
- );
8804
- }
8805
-
8806
- // src/components/custom/Workout/VelocityStrip.tsx
8807
- var VEL_COLORS = WORKOUT_TOKENS.scale;
8808
- var bandIdToEffortColor = {
8809
- speed: sequentialEffort[0],
8810
- // green-300
8811
- power: sequentialEffort[2],
8812
- // amber-300 (gold)
8813
- strengthSpeed: sequentialEffort[3],
8814
- // orange-400
8815
- maximalStrength: sequentialEffort[4],
8816
- // red-600
8817
- grinding: sequentialEffort[5]
8818
- // red-700 — the distinct 5th band
9057
+ // src/components/custom/Workout/VelocityStrip.tsx
9058
+ var VEL_COLORS = WORKOUT_TOKENS.scale;
9059
+ var bandIdToEffortColor = {
9060
+ speed: sequentialEffort[0],
9061
+ // green-300
9062
+ power: sequentialEffort[2],
9063
+ // amber-300 (gold)
9064
+ strengthSpeed: sequentialEffort[3],
9065
+ // orange-400
9066
+ maximalStrength: sequentialEffort[4],
9067
+ // red-600
9068
+ grinding: sequentialEffort[5]
9069
+ // red-700 the distinct 5th band
8819
9070
  };
8820
9071
  function getVelocityZoneColor(velocity) {
8821
9072
  if (velocity >= 1) return "vel-green";
@@ -8847,6 +9098,18 @@ function calculateMeanVelocity(velocities) {
8847
9098
  const sum = velocities.reduce((acc, v) => acc + v, 0);
8848
9099
  return sum / velocities.length;
8849
9100
  }
9101
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
9102
+ function getVelocityLossColor(lossPct) {
9103
+ const [t1, t26, t32] = VL_LOSS_THRESHOLDS;
9104
+ if (lossPct < t1) return VEL_COLORS.green;
9105
+ if (lossPct < t26) return VEL_COLORS.yellow;
9106
+ if (lossPct < t32) return VEL_COLORS.orange;
9107
+ return VEL_COLORS.red;
9108
+ }
9109
+ function velocityLossForRep(velocity, best) {
9110
+ if (best <= 0) return 0;
9111
+ return Math.max(0, Math.round((best - velocity) / best * 100));
9112
+ }
8850
9113
  function classifyBand(velocity, bands) {
8851
9114
  for (const band of bands) {
8852
9115
  if (band.max === null || velocity < band.max) return band;
@@ -8856,15 +9119,17 @@ function classifyBand(velocity, bands) {
8856
9119
  function bandColor(band) {
8857
9120
  return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
8858
9121
  }
9122
+ function makeBarColorFor(zones) {
9123
+ const hasZones = zones != null && zones.length > 0;
9124
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9125
+ }
8859
9126
  function getLossStyle(loss) {
8860
9127
  if (loss > 25) return { color: VEL_COLORS.red };
8861
9128
  if (loss > 20) return { color: VEL_COLORS.orange };
8862
9129
  return null;
8863
9130
  }
8864
- var REP_GAP = 2;
9131
+ var REP_GAP2 = 2;
8865
9132
  var WIDE_GAP = 8;
8866
- var TODO_COLOR2 = primitiveColors.charcoal[300];
8867
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8868
9133
  function deriveDoneVelocities(set) {
8869
9134
  switch (set.type) {
8870
9135
  case "drop":
@@ -8880,7 +9145,7 @@ function chunkedRepSlots(chunks) {
8880
9145
  chunks.forEach((chunk) => {
8881
9146
  chunk.forEach((velocity, ri) => {
8882
9147
  const first = slots.length === 0;
8883
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
9148
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
8884
9149
  });
8885
9150
  });
8886
9151
  return slots;
@@ -8894,7 +9159,7 @@ function buildSlots(set) {
8894
9159
  return {
8895
9160
  kind: done ? "rep" : "todo",
8896
9161
  velocity: done ? set.velocities[i] : void 0,
8897
- leadingGap: i === 0 ? 0 : REP_GAP
9162
+ leadingGap: i === 0 ? 0 : REP_GAP2
8898
9163
  };
8899
9164
  });
8900
9165
  }
@@ -8905,7 +9170,7 @@ function buildSlots(set) {
8905
9170
  return {
8906
9171
  kind,
8907
9172
  velocity: kind === "rep" ? set.velocities[i] : void 0,
8908
- leadingGap: i === 0 ? 0 : REP_GAP
9173
+ leadingGap: i === 0 ? 0 : REP_GAP2
8909
9174
  };
8910
9175
  });
8911
9176
  }
@@ -8913,16 +9178,16 @@ function buildSlots(set) {
8913
9178
  const reps = set.velocities.map((velocity, i) => ({
8914
9179
  kind: "rep",
8915
9180
  velocity,
8916
- leadingGap: i === 0 ? 0 : REP_GAP
9181
+ leadingGap: i === 0 ? 0 : REP_GAP2
8917
9182
  }));
8918
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
9183
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
8919
9184
  return reps;
8920
9185
  }
8921
9186
  case "drop":
8922
9187
  return chunkedRepSlots(set.subloads);
8923
9188
  case "myo": {
8924
9189
  const slots = chunkedRepSlots([set.activation, ...set.clusters]);
8925
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
9190
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
8926
9191
  return slots;
8927
9192
  }
8928
9193
  case "cluster": {
@@ -8933,7 +9198,7 @@ function buildSlots(set) {
8933
9198
  return {
8934
9199
  kind: done ? "rep" : "todo",
8935
9200
  velocity: done ? set.velocities[i] : void 0,
8936
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
9201
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
8937
9202
  };
8938
9203
  });
8939
9204
  }
@@ -8955,253 +9220,406 @@ function setAccessibilityLabel(set, repCount) {
8955
9220
  return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
8956
9221
  }
8957
9222
  }
8958
- function slotAccessibilityLabel(slot, index) {
8959
- switch (slot.kind) {
8960
- case "rep":
8961
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
8962
- case "todo":
8963
- return `Rep ${index + 1}: planned`;
8964
- case "variable":
8965
- return `Rep ${index + 1}: variable`;
8966
- case "continue":
8967
- return "Keep going";
8968
- }
8969
- }
8970
- var EXPANDED_TODO_STUB_PCT = 16;
8971
- var EXPANDED_ENCODED_PCT = 45;
8972
9223
  var EXPANDED_HEIGHT = 60;
8973
- var FIXED_MAX_VELOCITY = 1.15;
8974
- var BARE_STUB_HEIGHT = 3;
8975
- var HERO_HEIGHT = 220;
8976
- var HERO_LABEL_HEADROOM = 20;
8977
- var HERO_BAR_GAP = 8;
8978
- var HERO_BAR_MAX_WIDTH = 120;
8979
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
8980
- var HERO_BAR_RADIUS = 5;
8981
- var HERO_MIN_BAR_HEIGHT = 4;
8982
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
9224
+ var COMPACT_HEIGHT = 8;
8983
9225
  var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
8984
- var PEAK_HEADROOM = 1.06;
8985
- function scaleDenominator(scale, maxVelocity) {
8986
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
8987
- }
8988
- function bareSlotHeight(slot, height, denom) {
8989
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
8990
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
8991
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
8992
- }
8993
- function getReducedMotionPreference() {
8994
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8995
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8996
- }
8997
- function usePrefersReducedMotion() {
8998
- const [reduced, setReduced] = useState(getReducedMotionPreference);
8999
- useEffect(() => {
9000
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
9001
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
9002
- const handler = () => setReduced(mq.matches);
9003
- handler();
9004
- mq.addEventListener?.("change", handler);
9005
- return () => mq.removeEventListener?.("change", handler);
9006
- }, []);
9007
- return reduced;
9226
+ function DashedReferenceLine({
9227
+ anchor,
9228
+ offset,
9229
+ testID
9230
+ }) {
9231
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
9232
+ return /* @__PURE__ */ jsx(
9233
+ View,
9234
+ {
9235
+ accessibilityElementsHidden: true,
9236
+ style: {
9237
+ position: "absolute",
9238
+ left: 0,
9239
+ right: 0,
9240
+ borderTopWidth: 1,
9241
+ borderStyle: "dashed",
9242
+ borderColor: HERO_REFERENCE_COLOR,
9243
+ pointerEvents: "none",
9244
+ ...edge
9245
+ },
9246
+ testID
9247
+ }
9248
+ );
9008
9249
  }
9009
- var ANIMATION_DURATION = 400;
9010
- var ANIMATION_EASING = Easing.bezier(0.22, 1, 0.36, 1);
9011
- var POP_EASING = Easing.bezier(0.34, 1.56, 0.64, 1);
9012
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
9013
- const prefersReducedMotion = usePrefersReducedMotion();
9014
- const [liveScale] = useState(() => new Animated.Value(1));
9015
- useEffect(() => {
9016
- if (!active || liveRepIndex == null || liveVelocity == null) return;
9017
- if (prefersReducedMotion) {
9018
- liveScale.setValue(1);
9019
- return;
9250
+ var BAND_LABEL_FONT = "monospace";
9251
+ var VL_LABEL_MIN_PLOT = 65;
9252
+ var VL_SEMANTIC = getSemanticColors("dark");
9253
+ function VelocityLossBands({
9254
+ best,
9255
+ scaleDenom,
9256
+ plotHeight,
9257
+ flip = false
9258
+ }) {
9259
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
9260
+ const yOf = (v) => v / scaleDenom * plotHeight;
9261
+ const vl20 = best * 0.8;
9262
+ const vl30 = best * 0.7;
9263
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
9264
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
9265
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
9266
+ View,
9267
+ {
9268
+ style: {
9269
+ position: "absolute",
9270
+ left: 0,
9271
+ right: 0,
9272
+ bottom: yOf(loV),
9273
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
9274
+ backgroundColor: color
9275
+ }
9020
9276
  }
9021
- if (isNewPeak) {
9022
- liveScale.setValue(1);
9023
- Animated.sequence([
9024
- Animated.timing(liveScale, {
9025
- toValue: 1.25,
9026
- duration: 150,
9027
- easing: Easing.out(Easing.quad),
9028
- useNativeDriver: true
9029
- }),
9030
- Animated.spring(liveScale, {
9031
- toValue: 1,
9032
- friction: 3,
9033
- tension: 140,
9034
- useNativeDriver: true
9035
- })
9036
- ]).start();
9037
- } else {
9038
- liveScale.setValue(0.8);
9039
- Animated.timing(liveScale, {
9040
- toValue: 1,
9041
- duration: 300,
9042
- easing: POP_EASING,
9043
- useNativeDriver: true
9044
- }).start();
9277
+ );
9278
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
9279
+ View,
9280
+ {
9281
+ style: {
9282
+ position: "absolute",
9283
+ left: 0,
9284
+ right: 0,
9285
+ bottom: yOf(v),
9286
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
9287
+ // band edge — instead of floating up half a text-row (the label then centres on
9288
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
9289
+ height: 0,
9290
+ flexDirection: "row",
9291
+ alignItems: "center"
9292
+ },
9293
+ children: [
9294
+ /* @__PURE__ */ jsx(View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
9295
+ showLabels ? /* @__PURE__ */ jsxs(Fragment, { children: [
9296
+ /* @__PURE__ */ jsx(
9297
+ Text,
9298
+ {
9299
+ style: {
9300
+ marginHorizontal: 6,
9301
+ fontSize: vlFont,
9302
+ fontWeight: "800",
9303
+ fontFamily: BAND_LABEL_FONT,
9304
+ color,
9305
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
9306
+ },
9307
+ children: label
9308
+ }
9309
+ ),
9310
+ /* @__PURE__ */ jsx(View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
9311
+ ] }) : null
9312
+ ]
9313
+ }
9314
+ );
9315
+ return /* @__PURE__ */ jsxs(
9316
+ View,
9317
+ {
9318
+ accessibilityElementsHidden: true,
9319
+ pointerEvents: "none",
9320
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
9321
+ testID: "velocity-loss-bands",
9322
+ children: [
9323
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
9324
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
9325
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
9326
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
9327
+ ]
9045
9328
  }
9046
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
9047
- return liveScale;
9329
+ );
9048
9330
  }
9049
- function HeroVelocityChart({
9050
- doneVelocities,
9051
- barColorFor,
9052
- scaleDenom,
9053
- referenceVelocity,
9054
- liveRepIndex,
9055
- isNewPeak,
9331
+ function velocityReferenceOverlay(g, showLossBands) {
9332
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
9333
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
9334
+ showLossBands && /* @__PURE__ */ jsx(
9335
+ VelocityLossBands,
9336
+ {
9337
+ best: g.best,
9338
+ scaleDenom: g.scaleDenom,
9339
+ plotHeight: g.plotHeight,
9340
+ flip: g.flip
9341
+ }
9342
+ ),
9343
+ referencePx > 0 && /* @__PURE__ */ jsx(
9344
+ DashedReferenceLine,
9345
+ {
9346
+ anchor: "bottom",
9347
+ offset: referencePx,
9348
+ testID: "velocity-hero-reference"
9349
+ }
9350
+ )
9351
+ ] });
9352
+ }
9353
+ var ANIMATION_DURATION = 400;
9354
+ var DUAL_HERO_HEIGHT = SET_BAR_DEFAULT_HEIGHT;
9355
+ var DUAL_RAIL_HEIGHT = 96;
9356
+ var DUAL_COMPACT_HEIGHT = 11.5;
9357
+ var DUAL_WING_GAP = 2;
9358
+ var COMPACT_FOLD_GAP = 1.5;
9359
+ var COMPACT_FOLD_HALF = 5;
9360
+ function streamDone(stream) {
9361
+ return stream.set ? deriveDoneVelocities(stream.set) : stream.velocities ?? [];
9362
+ }
9363
+ function streamNaturalSlots(stream, targetReps) {
9364
+ if (stream.set) {
9365
+ return buildSlots(stream.set).map((s) => ({
9366
+ kind: s.kind,
9367
+ value: s.velocity,
9368
+ leadingGap: s.leadingGap
9369
+ }));
9370
+ }
9371
+ const reps = (stream.velocities ?? []).map((v, i) => ({
9372
+ kind: "rep",
9373
+ value: v,
9374
+ leadingGap: i === 0 ? 0 : REP_GAP2
9375
+ }));
9376
+ const pad = Math.max(0, (targetReps ?? 0) - reps.length);
9377
+ return [...reps, ...Array.from({ length: pad }, () => ({ kind: "todo" }))];
9378
+ }
9379
+ function mergeColumnKind(l, r) {
9380
+ const kinds = [l?.kind, r?.kind];
9381
+ if (kinds.includes("variable")) return "variable";
9382
+ if (kinds.includes("continue")) return "continue";
9383
+ if (kinds.includes("rep")) return "rep";
9384
+ return "todo";
9385
+ }
9386
+ function sideColumnCell(slot, kind, leadingGap) {
9387
+ if (kind !== "rep") return { kind, leadingGap };
9388
+ return slot?.kind === "rep" ? { kind: "rep", value: slot.value, leadingGap } : { kind: "empty", leadingGap };
9389
+ }
9390
+ function alignDualSlots(left, right) {
9391
+ const n = Math.max(left.length, right.length);
9392
+ const L = [];
9393
+ const R = [];
9394
+ for (let i = 0; i < n; i++) {
9395
+ const lt = left[i];
9396
+ const rt = right[i];
9397
+ const kind = mergeColumnKind(lt, rt);
9398
+ const gap = Math.max(lt?.leadingGap ?? REP_GAP2, rt?.leadingGap ?? REP_GAP2);
9399
+ L.push(sideColumnCell(lt, kind, gap));
9400
+ R.push(sideColumnCell(rt, kind, gap));
9401
+ }
9402
+ return { left: L, right: R };
9403
+ }
9404
+ function DualVelocityHero({
9405
+ leftDone,
9406
+ rightDone,
9407
+ leftStream,
9408
+ rightStream,
9409
+ leftLabel,
9410
+ rightLabel,
9411
+ zones,
9412
+ barColor: barColor2,
9413
+ scale,
9056
9414
  targetReps,
9415
+ liveRepIndex,
9057
9416
  height,
9058
9417
  className,
9418
+ label,
9059
9419
  viewProps
9060
9420
  }) {
9061
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9062
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
9063
- const [plotW, setPlotW] = useState(0);
9064
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
9065
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
9066
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
9067
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
9068
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
9069
- const totalSlots = doneVelocities.length + pendingCount;
9070
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
9071
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
9072
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
9073
- const peakIndex = doneVelocities.reduce(
9074
- (best, v, i) => v > doneVelocities[best] ? i : best,
9075
- 0
9076
- );
9077
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
9078
- const repCount = doneVelocities.length;
9079
- const total = Math.max(repCount, targetReps ?? repCount);
9080
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9421
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9422
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9423
+ const aligned = alignDualSlots(
9424
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9425
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9426
+ );
9081
9427
  const { style: externalStyle, ...restProps } = viewProps;
9082
- return /* @__PURE__ */ jsx(
9428
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9429
+ VelocityStrip,
9430
+ {
9431
+ variant: "hero",
9432
+ orientation,
9433
+ velocities: done,
9434
+ columnSlots: columns,
9435
+ scale,
9436
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9437
+ barColor: barColor2,
9438
+ zones,
9439
+ liveRepIndex,
9440
+ height: plotHalf,
9441
+ hideBaseline: true
9442
+ }
9443
+ );
9444
+ return /* @__PURE__ */ jsxs(
9083
9445
  View,
9084
9446
  {
9085
9447
  className,
9086
- style: [{ height }, externalStyle],
9448
+ style: [{ height, flexDirection: "row" }, externalStyle],
9087
9449
  accessibilityRole: "image",
9088
9450
  accessibilityLabel: label,
9089
- testID: "velocity-strip-hero",
9451
+ testID: "dual-velocity-strip",
9090
9452
  ...restProps,
9091
- children: /* @__PURE__ */ jsxs(
9092
- View,
9093
- {
9094
- onLayout: onPlotLayout,
9095
- style: {
9096
- flex: 1,
9097
- flexDirection: "row",
9098
- alignItems: "flex-end",
9099
- gap: heroGap,
9100
- position: "relative",
9101
- borderBottomWidth: 2,
9102
- borderBottomColor: HERO_PENDING_COLOR
9103
- },
9104
- children: [
9105
- referencePx > 0 && /* @__PURE__ */ jsx(
9106
- View,
9107
- {
9108
- accessibilityElementsHidden: true,
9109
- style: {
9110
- position: "absolute",
9111
- left: 0,
9112
- right: 0,
9113
- bottom: referencePx,
9114
- borderTopWidth: 1,
9115
- borderStyle: "dashed",
9116
- borderColor: HERO_REFERENCE_COLOR,
9117
- pointerEvents: "none"
9118
- },
9119
- testID: "velocity-hero-reference"
9120
- }
9121
- ),
9122
- doneVelocities.map((velocity, i) => {
9123
- const isLive = liveRepIndex === i;
9124
- return /* @__PURE__ */ jsxs(
9125
- View,
9126
- {
9127
- accessibilityElementsHidden: true,
9128
- style: {
9129
- flex: 1,
9130
- maxWidth: HERO_BAR_MAX_WIDTH,
9131
- height: "100%",
9132
- alignItems: "center",
9133
- justifyContent: "flex-end"
9134
- },
9135
- children: [
9136
- showBarLabel(i) && /* @__PURE__ */ jsx(
9137
- Text,
9138
- {
9139
- className: "text-text-primary",
9140
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
9141
- testID: `velocity-label-${i}`,
9142
- children: formatVelocity(velocity)
9143
- }
9144
- ),
9145
- /* @__PURE__ */ jsx(
9146
- Animated.View,
9147
- {
9148
- style: [
9149
- {
9150
- width: "100%",
9151
- height: barHeight(velocity),
9152
- borderTopLeftRadius: HERO_BAR_RADIUS,
9153
- borderTopRightRadius: HERO_BAR_RADIUS,
9154
- backgroundColor: barColorFor(velocity)
9155
- },
9156
- isLive ? { transform: [{ scale: liveScale }] } : null
9157
- ],
9158
- testID: `velocity-bar-${i}`
9159
- }
9160
- )
9161
- ]
9162
- },
9163
- i
9164
- );
9165
- }),
9166
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
9167
- View,
9168
- {
9169
- accessibilityElementsHidden: true,
9170
- style: {
9171
- flex: 1,
9172
- maxWidth: HERO_BAR_MAX_WIDTH,
9173
- height: "100%",
9174
- alignItems: "center",
9175
- justifyContent: "flex-end"
9176
- },
9177
- children: /* @__PURE__ */ jsx(
9178
- View,
9179
- {
9180
- style: {
9181
- width: "100%",
9182
- height: HERO_MIN_BAR_HEIGHT * 3,
9183
- borderWidth: 1,
9184
- borderStyle: "dashed",
9185
- borderColor: HERO_PENDING_COLOR,
9186
- borderTopLeftRadius: HERO_BAR_RADIUS,
9187
- borderTopRightRadius: HERO_BAR_RADIUS
9188
- },
9189
- testID: "velocity-slot-todo"
9190
- }
9191
- )
9192
- },
9193
- `pending-${i}`
9194
- ))
9195
- ]
9196
- }
9197
- )
9453
+ children: [
9454
+ /* @__PURE__ */ jsx(
9455
+ ChartSideRail,
9456
+ {
9457
+ sectionExtent: plotHalf,
9458
+ variant: "hero",
9459
+ sections: [
9460
+ { label: leftLabel, testID: "dual-velocity-side-label-L" },
9461
+ { label: rightLabel, testID: "dual-velocity-side-label-R" }
9462
+ ]
9463
+ }
9464
+ ),
9465
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, gap: DUAL_WING_GAP }, children: [
9466
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9467
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9468
+ ] })
9469
+ ]
9470
+ }
9471
+ );
9472
+ }
9473
+ function DualVelocityRail({
9474
+ leftDone,
9475
+ rightDone,
9476
+ leftStream,
9477
+ rightStream,
9478
+ zones,
9479
+ barColor: barColor2,
9480
+ scale,
9481
+ targetReps,
9482
+ liveRepIndex,
9483
+ height,
9484
+ className,
9485
+ label,
9486
+ viewProps
9487
+ }) {
9488
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9489
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9490
+ const aligned = alignDualSlots(
9491
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9492
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9493
+ );
9494
+ const { style: externalStyle, ...restProps } = viewProps;
9495
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9496
+ VelocityStrip,
9497
+ {
9498
+ variant: "expanded",
9499
+ showNumbers: false,
9500
+ showInfo: false,
9501
+ orientation,
9502
+ velocities: done,
9503
+ columnSlots: columns,
9504
+ scale,
9505
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9506
+ barColor: barColor2,
9507
+ zones,
9508
+ liveRepIndex,
9509
+ height: plotHalf
9510
+ }
9511
+ );
9512
+ return /* @__PURE__ */ jsxs(
9513
+ View,
9514
+ {
9515
+ className,
9516
+ style: [{ height, flexDirection: "column", gap: DUAL_WING_GAP }, externalStyle],
9517
+ accessibilityRole: "image",
9518
+ accessibilityLabel: label,
9519
+ testID: "dual-velocity-strip",
9520
+ ...restProps,
9521
+ children: [
9522
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9523
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9524
+ ]
9525
+ }
9526
+ );
9527
+ }
9528
+ function DualVelocityCompactStrip({
9529
+ leftDone,
9530
+ rightDone,
9531
+ leftStream,
9532
+ rightStream,
9533
+ zones,
9534
+ barColor: barColor2,
9535
+ targetReps,
9536
+ height,
9537
+ className,
9538
+ label,
9539
+ viewProps
9540
+ }) {
9541
+ const half = height === DUAL_COMPACT_HEIGHT ? COMPACT_FOLD_HALF : (height - COMPACT_FOLD_GAP) / 2;
9542
+ const aligned = alignDualSlots(
9543
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9544
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9545
+ );
9546
+ const { style: externalStyle, ...restProps } = viewProps;
9547
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9548
+ VelocityStrip,
9549
+ {
9550
+ variant: "compact",
9551
+ orientation,
9552
+ velocities: done,
9553
+ columnSlots: columns,
9554
+ barColor: barColor2,
9555
+ zones,
9556
+ height: half
9557
+ }
9558
+ );
9559
+ return /* @__PURE__ */ jsxs(
9560
+ View,
9561
+ {
9562
+ className,
9563
+ style: [{ height, flexDirection: "column", gap: COMPACT_FOLD_GAP }, externalStyle],
9564
+ accessibilityRole: "image",
9565
+ accessibilityLabel: label,
9566
+ testID: "dual-velocity-strip",
9567
+ ...restProps,
9568
+ children: [
9569
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9570
+ /* @__PURE__ */ jsx(View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9571
+ ]
9198
9572
  }
9199
9573
  );
9200
9574
  }
9575
+ function DualVelocityStrip({
9576
+ left,
9577
+ right,
9578
+ zones,
9579
+ barColor: barColor2 = "loss",
9580
+ targetReps,
9581
+ liveRepIndex,
9582
+ variant = "hero",
9583
+ scale = "peak",
9584
+ height,
9585
+ className,
9586
+ ...props
9587
+ }) {
9588
+ const resolvedHeight = height ?? (variant === "hero" ? DUAL_HERO_HEIGHT : variant === "compact" ? DUAL_COMPACT_HEIGHT : DUAL_RAIL_HEIGHT);
9589
+ const leftDone = streamDone(left);
9590
+ const rightDone = streamDone(right);
9591
+ 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`;
9592
+ const shared = {
9593
+ leftDone,
9594
+ rightDone,
9595
+ leftStream: left,
9596
+ rightStream: right,
9597
+ leftLabel: left.label,
9598
+ rightLabel: right.label,
9599
+ zones,
9600
+ barColor: barColor2,
9601
+ scale,
9602
+ targetReps,
9603
+ height: resolvedHeight,
9604
+ className,
9605
+ label,
9606
+ viewProps: props
9607
+ };
9608
+ if (variant === "rail") return /* @__PURE__ */ jsx(DualVelocityRail, { ...shared, liveRepIndex });
9609
+ if (variant === "compact") return /* @__PURE__ */ jsx(DualVelocityCompactStrip, { ...shared });
9610
+ return /* @__PURE__ */ jsx(DualVelocityHero, { ...shared, liveRepIndex });
9611
+ }
9201
9612
  function VelocityStrip({
9202
9613
  velocities,
9203
9614
  set,
9204
9615
  zones,
9616
+ barColor: barColor2 = "loss",
9617
+ showLossBands,
9618
+ orientation = "up",
9619
+ scaleMax,
9620
+ hideBaseline,
9621
+ columnSlots,
9622
+ label,
9205
9623
  liveRepIndex,
9206
9624
  expanded = true,
9207
9625
  onToggle,
@@ -9215,51 +9633,29 @@ function VelocityStrip({
9215
9633
  className,
9216
9634
  ...props
9217
9635
  }) {
9636
+ const lossBandsOn = showLossBands ?? barColor2 === "loss";
9218
9637
  const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
9219
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
9220
- kind: "rep",
9221
- velocity: v,
9222
- leadingGap: i === 0 ? 0 : REP_GAP
9223
- }));
9224
9638
  const maxVelocity = Math.max(...doneVelocities, 0);
9225
9639
  const meanVelocity = calculateMeanVelocity(doneVelocities);
9226
9640
  const loss = calculateVelocityLoss(doneVelocities);
9227
9641
  const framed = showNumbers || showInfo;
9228
- const scaleDenom = scaleDenominator(scale, maxVelocity);
9229
9642
  const hasZones = zones != null && zones.length > 0;
9230
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9231
- const slotColor = (slot) => {
9232
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
9233
- if (slot.kind === "todo") return TODO_COLOR2;
9234
- return SET_STRIP_VARIABLE_COLOR;
9235
- };
9643
+ const zoneColorFor = makeBarColorFor(zones);
9644
+ const barColorFor = (v) => barColor2 === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
9236
9645
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
9237
- const [heightAnim] = useState(() => new Animated.Value(expanded ? height : 3));
9238
- const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0));
9646
+ const [expandProgress] = useState(() => new Animated.Value(expanded ? 1 : 0));
9239
9647
  const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0));
9240
9648
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9241
9649
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
9242
- const liveScale = useLiveRepPop(
9243
- variant === "expanded" && framed,
9244
- liveRepIndex,
9245
- liveVelocity,
9246
- isNewPeak
9247
- );
9248
9650
  useEffect(() => {
9249
9651
  if (variant !== "expanded" || !framed) return;
9250
9652
  Animated.parallel([
9251
- Animated.timing(heightAnim, {
9252
- toValue: expanded ? height : 3,
9653
+ Animated.timing(expandProgress, {
9654
+ toValue: expanded ? 1 : 0,
9253
9655
  duration: ANIMATION_DURATION,
9254
9656
  easing: ANIMATION_EASING,
9255
9657
  useNativeDriver: false
9256
9658
  }),
9257
- Animated.timing(labelOpacity, {
9258
- toValue: expanded ? 1 : 0,
9259
- duration: 300,
9260
- delay: expanded ? 150 : 0,
9261
- useNativeDriver: false
9262
- }),
9263
9659
  Animated.timing(infoOpacity, {
9264
9660
  toValue: expanded ? 1 : 0,
9265
9661
  duration: 300,
@@ -9267,250 +9663,176 @@ function VelocityStrip({
9267
9663
  useNativeDriver: false
9268
9664
  })
9269
9665
  ]).start();
9270
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
9666
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
9271
9667
  if (set == null && velocities == null) return null;
9272
9668
  const repCount = doneVelocities.length;
9273
9669
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
9274
9670
  if (variant === "hero") {
9275
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
9671
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
9672
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9673
+ kind: s.kind,
9674
+ value: s.velocity,
9675
+ leadingGap: s.leadingGap
9676
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9677
+ const total = Math.max(repCount, targetReps ?? repCount);
9678
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9276
9679
  return /* @__PURE__ */ jsx(
9277
- HeroVelocityChart,
9680
+ SetBarChart,
9278
9681
  {
9279
- doneVelocities,
9280
- barColorFor,
9281
- scaleDenom,
9282
- referenceVelocity: maxVelocity,
9682
+ slots: heroSlots,
9683
+ colorFor: barColorFor,
9684
+ height: heroHeight,
9685
+ scale,
9686
+ scaleMax,
9687
+ orientation,
9283
9688
  liveRepIndex,
9284
9689
  isNewPeak,
9285
- targetReps,
9286
- height: heroHeight,
9690
+ targetReps: set || columnSlots ? void 0 : targetReps,
9691
+ label,
9692
+ showValueLabels: true,
9693
+ formatValue: formatVelocity,
9694
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
9695
+ hideBaseline: true,
9696
+ testID: "velocity-strip-hero",
9697
+ testIDPrefix: "velocity",
9698
+ accessibilityLabel: heroLabel,
9287
9699
  className,
9288
9700
  viewProps: props
9289
9701
  }
9290
9702
  );
9291
9703
  }
9292
- if (variant === "mini") {
9293
- const { style: externalStyle, ...restProps } = props;
9704
+ if (variant === "compact") {
9705
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9706
+ kind: s.kind,
9707
+ value: s.velocity,
9708
+ leadingGap: s.leadingGap
9709
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9710
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
9294
9711
  return /* @__PURE__ */ jsx(
9295
- View,
9712
+ SetBarChart,
9296
9713
  {
9297
- className,
9298
- style: [
9299
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
9300
- externalStyle
9301
- ],
9302
- accessibilityRole: "image",
9714
+ slots: compactSlots,
9715
+ colorFor: barColorFor,
9716
+ height: compactHeight,
9717
+ scale,
9718
+ scaleMax,
9719
+ orientation,
9720
+ flat: true,
9721
+ barRadius: 2,
9722
+ cornerStyle: "all",
9723
+ targetReps: set || columnSlots ? void 0 : targetReps,
9724
+ label,
9725
+ hideBaseline: true,
9726
+ testID: "velocity-strip-compact",
9727
+ testIDPrefix: "velocity",
9303
9728
  accessibilityLabel: miniLabel,
9304
- testID: "velocity-strip-mini",
9305
- ...restProps,
9306
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9307
- View,
9308
- {
9309
- style: {
9310
- flex: 1,
9311
- backgroundColor: slotColor(slot),
9312
- borderRadius: 1,
9313
- minWidth: 4,
9314
- height: "100%",
9315
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
9316
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
9317
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9318
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9319
- },
9320
- accessibilityElementsHidden: true,
9321
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9322
- },
9323
- i
9324
- ))
9729
+ className,
9730
+ viewProps: props
9325
9731
  }
9326
9732
  );
9327
9733
  }
9328
9734
  if (!framed) {
9329
- const { style: externalStyle, ...restProps } = props;
9735
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9736
+ kind: s.kind,
9737
+ value: s.velocity,
9738
+ leadingGap: s.leadingGap
9739
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9330
9740
  return /* @__PURE__ */ jsx(
9331
- View,
9741
+ SetBarChart,
9332
9742
  {
9333
- className,
9334
- style: [
9335
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
9336
- externalStyle
9337
- ],
9338
- accessibilityRole: "image",
9743
+ slots: spotlightSlots,
9744
+ colorFor: barColorFor,
9745
+ height,
9746
+ scale,
9747
+ scaleMax,
9748
+ orientation,
9749
+ liveRepIndex,
9750
+ isNewPeak,
9751
+ barRadius: 2,
9752
+ cornerStyle: "top",
9753
+ targetReps: set ? void 0 : targetReps,
9754
+ label,
9755
+ hideBaseline: true,
9756
+ testID: "velocity-strip-spotlight",
9757
+ testIDPrefix: "velocity",
9339
9758
  accessibilityLabel: miniLabel,
9340
- testID: "velocity-strip-compact",
9341
- ...restProps,
9342
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9343
- View,
9344
- {
9345
- style: {
9346
- flex: 1,
9347
- minWidth: 4,
9348
- height: bareSlotHeight(slot, height, scaleDenom),
9349
- // Top-rounded to match the framed chart's bars (the "rounded tops from
9350
- // the numbers one"); square bottoms since bars sit on the baseline.
9351
- borderTopLeftRadius: 2,
9352
- borderTopRightRadius: 2,
9353
- backgroundColor: slotColor(slot),
9354
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9355
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9356
- },
9357
- accessibilityElementsHidden: true,
9358
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9359
- },
9360
- i
9361
- ))
9759
+ className,
9760
+ viewProps: props
9362
9761
  }
9363
9762
  );
9364
9763
  }
9365
9764
  const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
9366
9765
  const hasInteractiveContainer = onToggle != null;
9367
9766
  const hasInteractiveReps = onRepPress != null && expanded;
9368
- const setBars = set != null && slots.map((slot, i) => {
9369
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
9370
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
9371
- return /* @__PURE__ */ jsx(
9372
- View,
9767
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
9768
+ kind: sl.kind,
9769
+ value: sl.velocity,
9770
+ leadingGap: sl.leadingGap
9771
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
9772
+ const needsBarOverlay = showNumbers || onRepPress != null;
9773
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(Fragment, { children: [
9774
+ showNumbers && /* @__PURE__ */ jsx(
9775
+ Animated.View,
9373
9776
  {
9374
9777
  style: {
9375
- flex: 1,
9376
- height: "100%",
9377
- justifyContent: "flex-end",
9378
- marginLeft: slot.leadingGap
9778
+ opacity: expandProgress,
9779
+ position: "absolute",
9780
+ top: -13,
9781
+ left: 0,
9782
+ right: 0,
9783
+ alignItems: "center"
9379
9784
  },
9785
+ accessibilityElementsHidden: true,
9786
+ pointerEvents: "none",
9380
9787
  children: /* @__PURE__ */ jsx(
9381
- View,
9788
+ Text,
9382
9789
  {
9383
- style: {
9384
- height: `${heightPct}%`,
9385
- minHeight: 2,
9386
- borderTopLeftRadius: 2,
9387
- borderTopRightRadius: 2,
9388
- backgroundColor: slotColor(slot),
9389
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9390
- },
9391
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
9392
- accessibilityRole: "image",
9393
- accessibilityLabel: slotAccessibilityLabel(slot, i)
9790
+ className: "text-text-secondary",
9791
+ style: { fontSize: 8, fontWeight: "600" },
9792
+ testID: `velocity-label-${repIndex}`,
9793
+ children: formatVelocity(value)
9394
9794
  }
9395
9795
  )
9396
- },
9397
- i
9398
- );
9399
- });
9796
+ }
9797
+ ),
9798
+ onRepPress && expanded && /* @__PURE__ */ jsx(
9799
+ Pressable,
9800
+ {
9801
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
9802
+ onPress: () => onRepPress(repIndex, value),
9803
+ accessibilityRole: "button",
9804
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
9805
+ testID: `velocity-bar-pressable-${repIndex}`
9806
+ }
9807
+ )
9808
+ ] }) : void 0;
9400
9809
  const stripContent = /* @__PURE__ */ jsxs(
9401
9810
  Animated.View,
9402
9811
  {
9403
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
9404
- style: [
9405
- {
9406
- height: heightAnim,
9407
- width: "100%",
9408
- gap: 2,
9409
- borderRadius: expanded ? 6 : 2,
9410
- paddingTop: expanded ? 16 : 0,
9411
- paddingBottom: expanded ? 24 : 0,
9412
- // No horizontal inset — the framed chart's bars span full width, matching
9413
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
9414
- paddingHorizontal: 0,
9415
- paddingVertical: expanded ? void 0 : 8,
9416
- overflow: expanded ? "visible" : "hidden"
9417
- }
9418
- ],
9812
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
9813
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
9419
9814
  accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
9420
9815
  accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
9421
9816
  testID: "velocity-strip",
9422
9817
  ...props,
9423
9818
  children: [
9424
- /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
9425
- const barBackground = barColorFor(v);
9426
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
9427
- const isLive = liveRepIndex === i;
9428
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
9429
- const barStyle = expanded ? {
9430
- height: `${barHeightPct}%`,
9431
- minHeight: 2,
9432
- borderTopLeftRadius: 2,
9433
- borderTopRightRadius: 2,
9434
- backgroundColor: barBackground
9435
- } : {
9436
- minHeight: "100%",
9437
- borderTopLeftRadius: 2,
9438
- borderTopRightRadius: 2,
9439
- borderBottomLeftRadius: 0,
9440
- borderBottomRightRadius: 0,
9441
- backgroundColor: barBackground
9442
- };
9443
- const barInner = isLive ? /* @__PURE__ */ jsx(
9444
- Animated.View,
9445
- {
9446
- style: [barStyle, { transform: [{ scale: liveScale }] }],
9447
- testID: `velocity-bar-${i}`,
9448
- accessibilityRole: "image",
9449
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
9450
- }
9451
- ) : /* @__PURE__ */ jsx(
9452
- View,
9453
- {
9454
- style: barStyle,
9455
- testID: `velocity-bar-${i}`,
9456
- accessibilityRole: "image",
9457
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
9458
- }
9459
- );
9460
- const bar = /* @__PURE__ */ jsxs(
9461
- View,
9462
- {
9463
- style: {
9464
- flex: 1,
9465
- height: "100%",
9466
- justifyContent: "flex-end",
9467
- position: "relative"
9468
- },
9469
- children: [
9470
- expanded && showNumbers && /* @__PURE__ */ jsx(
9471
- Animated.View,
9472
- {
9473
- style: {
9474
- opacity: labelOpacity,
9475
- alignItems: "center",
9476
- position: "absolute",
9477
- top: -13,
9478
- left: 0,
9479
- right: 0
9480
- },
9481
- accessibilityElementsHidden: true,
9482
- children: /* @__PURE__ */ jsx(
9483
- Text,
9484
- {
9485
- className: "text-text-secondary",
9486
- style: { fontSize: 8, fontWeight: "600" },
9487
- testID: `velocity-label-${i}`,
9488
- children: formatVelocity(v)
9489
- }
9490
- )
9491
- }
9492
- ),
9493
- barInner
9494
- ]
9495
- },
9496
- i
9497
- );
9498
- if (onRepPress && expanded) {
9499
- return /* @__PURE__ */ jsx(
9500
- Pressable,
9501
- {
9502
- style: { flex: 1, height: "100%" },
9503
- onPress: () => onRepPress(i, v),
9504
- accessibilityRole: "button",
9505
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
9506
- testID: `velocity-bar-pressable-${i}`,
9507
- children: bar
9508
- },
9509
- i
9510
- );
9511
- }
9512
- return bar;
9513
- }) }),
9819
+ /* @__PURE__ */ jsx(
9820
+ SetBarChart,
9821
+ {
9822
+ slots: framedSlots,
9823
+ colorFor: barColorFor,
9824
+ height,
9825
+ scale,
9826
+ scaleMax,
9827
+ expandProgress,
9828
+ renderBarOverlay: renderFramedBarOverlay,
9829
+ targetReps: set ? void 0 : targetReps,
9830
+ barRadius: 2,
9831
+ cornerStyle: "top",
9832
+ hideBaseline: true,
9833
+ testIDPrefix: "velocity"
9834
+ }
9835
+ ),
9514
9836
  expanded && showInfo && /* @__PURE__ */ jsxs(
9515
9837
  Animated.View,
9516
9838
  {
@@ -9518,10 +9840,8 @@ function VelocityStrip({
9518
9840
  flexDirection: "row",
9519
9841
  justifyContent: "space-between",
9520
9842
  opacity: infoOpacity,
9521
- position: "absolute",
9522
- bottom: 4,
9523
- left: 6,
9524
- right: 6
9843
+ marginTop: 6,
9844
+ paddingHorizontal: 6
9525
9845
  },
9526
9846
  testID: "velocity-info-row",
9527
9847
  children: [
@@ -9529,10 +9849,7 @@ function VelocityStrip({
9529
9849
  Text,
9530
9850
  {
9531
9851
  className: "text-text-secondary",
9532
- style: {
9533
- fontSize: 10,
9534
- fontFamily: "Inter, sans-serif"
9535
- },
9852
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
9536
9853
  children: [
9537
9854
  meanZone,
9538
9855
  " ",
@@ -9547,11 +9864,7 @@ function VelocityStrip({
9547
9864
  Text,
9548
9865
  {
9549
9866
  className: "text-text-secondary",
9550
- style: {
9551
- fontSize: 10,
9552
- fontFamily: "Inter, sans-serif",
9553
- ...getLossStyle(loss)
9554
- },
9867
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
9555
9868
  children: [
9556
9869
  "Loss: ",
9557
9870
  loss,
@@ -9579,13 +9892,13 @@ function VelocityStrip({
9579
9892
  }
9580
9893
  return stripContent;
9581
9894
  }
9582
- var t6 = getSemanticColors("dark");
9895
+ var t7 = getSemanticColors("dark");
9583
9896
  var dotColorMap = {
9584
9897
  untrained: "#2C2C2C",
9585
- behind: t6["brand-secondary"],
9586
- ontrack: t6["status-success"],
9587
- target: t6["brand-primary"],
9588
- over: t6["status-error"]
9898
+ behind: t7["brand-secondary"],
9899
+ ontrack: t7["status-success"],
9900
+ target: t7["brand-primary"],
9901
+ over: t7["status-error"]
9589
9902
  };
9590
9903
  function MuscleGroupChip({
9591
9904
  name,
@@ -9859,6 +10172,7 @@ function RowStrip({ set }) {
9859
10172
  showInfo: false,
9860
10173
  height: 24,
9861
10174
  scale: "fixed",
10175
+ liveRepIndex: set.velocities.length - 1,
9862
10176
  set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
9863
10177
  zones
9864
10178
  }
@@ -9868,7 +10182,9 @@ function RowStrip({ set }) {
9868
10182
  return /* @__PURE__ */ jsx(
9869
10183
  VelocityStrip,
9870
10184
  {
9871
- variant: "mini",
10185
+ variant: "compact",
10186
+ height: 8,
10187
+ hideBaseline: true,
9872
10188
  set: { type: "straight", velocities, planned: plannedReps(set) },
9873
10189
  zones
9874
10190
  }
@@ -10288,7 +10604,7 @@ function RestTimer({
10288
10604
  }
10289
10605
  );
10290
10606
  }
10291
- var t7 = getSemanticColors("dark");
10607
+ var t8 = getSemanticColors("dark");
10292
10608
  var TIMES = "\xD7";
10293
10609
  var AT = "@";
10294
10610
  function SetsRepsLoad({
@@ -10300,8 +10616,8 @@ function SetsRepsLoad({
10300
10616
  className,
10301
10617
  ...props
10302
10618
  }) {
10303
- const value = t7["text-primary"];
10304
- const sep = t7["text-tertiary"];
10619
+ const value = t8["text-primary"];
10620
+ const sep = t8["text-tertiary"];
10305
10621
  return /* @__PURE__ */ jsxs(
10306
10622
  View,
10307
10623
  {
@@ -10321,6 +10637,107 @@ function SetsRepsLoad({
10321
10637
  }
10322
10638
  );
10323
10639
  }
10640
+ var PULSE_HALF_MS = 950;
10641
+ var PULSE_MIN_OPACITY = 0.45;
10642
+ var SEGMENTED_BAR_GAP = 5;
10643
+ function usePulse2(active) {
10644
+ const [value] = useState(() => new Animated.Value(0));
10645
+ useEffect(() => {
10646
+ if (!active) return;
10647
+ const loop = Animated.loop(
10648
+ Animated.sequence([
10649
+ Animated.timing(value, {
10650
+ toValue: 1,
10651
+ duration: PULSE_HALF_MS,
10652
+ easing: Easing.inOut(Easing.ease),
10653
+ useNativeDriver: false
10654
+ }),
10655
+ Animated.timing(value, {
10656
+ toValue: 0,
10657
+ duration: PULSE_HALF_MS,
10658
+ easing: Easing.inOut(Easing.ease),
10659
+ useNativeDriver: false
10660
+ })
10661
+ ])
10662
+ );
10663
+ loop.start();
10664
+ return () => loop.stop();
10665
+ }, [active, value]);
10666
+ return value;
10667
+ }
10668
+ function SegmentedBar({
10669
+ segments,
10670
+ height = 8,
10671
+ gap = SEGMENTED_BAR_GAP,
10672
+ radius = 2,
10673
+ marker = null,
10674
+ segmentTestID,
10675
+ style,
10676
+ ...props
10677
+ }) {
10678
+ const pulse = usePulse2(segments.some((s) => s.pulse));
10679
+ return /* @__PURE__ */ jsxs(View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
10680
+ segments.map((seg, i) => {
10681
+ const fillStyle = {
10682
+ width: `${(seg.fill ?? 1) * 100}%`,
10683
+ height: "100%",
10684
+ backgroundColor: seg.color,
10685
+ opacity: seg.opacity
10686
+ };
10687
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
10688
+ return /* @__PURE__ */ jsx(
10689
+ View,
10690
+ {
10691
+ style: {
10692
+ flex: seg.weight ?? 1,
10693
+ minWidth: 0,
10694
+ height: "100%",
10695
+ marginLeft: seg.leadingGap,
10696
+ borderRadius: radius,
10697
+ overflow: "hidden"
10698
+ },
10699
+ children: seg.pulse ? /* @__PURE__ */ jsx(
10700
+ Animated.View,
10701
+ {
10702
+ style: {
10703
+ ...fillStyle,
10704
+ ...seg.pulseColor ? {
10705
+ backgroundColor: pulse.interpolate({
10706
+ inputRange: [0, 1],
10707
+ outputRange: seg.pulseColor
10708
+ })
10709
+ } : {
10710
+ opacity: pulse.interpolate({
10711
+ inputRange: [0, 1],
10712
+ outputRange: [PULSE_MIN_OPACITY, 1]
10713
+ })
10714
+ }
10715
+ },
10716
+ accessibilityElementsHidden: true,
10717
+ testID
10718
+ }
10719
+ ) : /* @__PURE__ */ jsx(View, { style: fillStyle, accessibilityElementsHidden: true, testID })
10720
+ },
10721
+ i
10722
+ );
10723
+ }),
10724
+ marker ? /* @__PURE__ */ jsx(
10725
+ View,
10726
+ {
10727
+ style: {
10728
+ position: "absolute",
10729
+ left: `${marker.position * 100}%`,
10730
+ top: 0,
10731
+ bottom: 0,
10732
+ width: 2,
10733
+ backgroundColor: marker.color
10734
+ },
10735
+ accessibilityElementsHidden: true,
10736
+ testID: "segmented-bar-marker"
10737
+ }
10738
+ ) : null
10739
+ ] });
10740
+ }
10324
10741
 
10325
10742
  // src/components/custom/Workout/paceTone.ts
10326
10743
  function paceTone(progress, target) {
@@ -10414,14 +10831,148 @@ function ScheduleTiles({ when, now, gap = 1, ...props }) {
10414
10831
  )
10415
10832
  ] });
10416
10833
  }
10417
- var t8 = getSemanticColors("dark");
10834
+
10835
+ // src/components/custom/Workout/SetBar.tsx
10836
+ var SET_STRIP_ZONES = {
10837
+ slow: primitiveRamps.red[600],
10838
+ moderate: primitiveRamps.orange[400],
10839
+ fast: primitiveRamps.amber[300],
10840
+ fastest: primitiveRamps.green[300]
10841
+ };
10842
+ var TODO_COLOR = primitiveColors.charcoal[300];
10843
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
10844
+ var NOTCH_GAP = 2;
10845
+ var CLUSTER_GAP = 3;
10846
+ var MYO_UPCOMING_CLUSTERS = 3;
10847
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
10848
+ var MYO_UPCOMING_FADE = 0.6;
10849
+ var PULSE_RANGE = {
10850
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
10851
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
10852
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
10853
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
10854
+ };
10855
+ function velocityZoneColor(v) {
10856
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
10857
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
10858
+ if (v < 1) return SET_STRIP_ZONES.fast;
10859
+ return SET_STRIP_ZONES.fastest;
10860
+ }
10861
+ function chunkedSegments(chunks, gap) {
10862
+ return chunks.flatMap(
10863
+ (vels, ci) => vels.map((v, ri) => ({
10864
+ color: velocityZoneColor(v),
10865
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
10866
+ }))
10867
+ );
10868
+ }
10869
+ function rangeSegments(floor, max, doneVels) {
10870
+ return Array.from({ length: max }, (_, i) => {
10871
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
10872
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
10873
+ });
10874
+ }
10875
+ function myoUpcomingSegments(activationLen, clusterCount) {
10876
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
10877
+ const trail = Array.from(
10878
+ { length: clusterCount },
10879
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
10880
+ color: SET_STRIP_VARIABLE_COLOR,
10881
+ opacity: MYO_UPCOMING_FADE ** ci,
10882
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
10883
+ }))
10884
+ ).flat();
10885
+ return [...activation, ...trail];
10886
+ }
10887
+ function setSegments(set) {
10888
+ if (set.status === "todo") {
10889
+ return [{ color: TODO_COLOR }];
10890
+ }
10891
+ if (set.status === "done") {
10892
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
10893
+ }
10894
+ if (set.status === "range") {
10895
+ return rangeSegments(set.floor, set.max, set.doneVels);
10896
+ }
10897
+ if (set.status === "drop") {
10898
+ return chunkedSegments(set.subloads, NOTCH_GAP);
10899
+ }
10900
+ if (set.status === "myo") {
10901
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
10902
+ }
10903
+ if (set.status === "myo-upcoming") {
10904
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
10905
+ }
10906
+ const performed = set.velocities.map((v) => {
10907
+ const color = velocityZoneColor(v);
10908
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
10909
+ });
10910
+ const remaining = Math.max(0, set.planned - set.velocities.length);
10911
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
10912
+ }
10913
+ function setBarSegmentTestID(seg) {
10914
+ if (seg.pulse) return "set-strip-pulse";
10915
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
10916
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
10917
+ }
10918
+ function SetBar({ set, height = 8 }) {
10919
+ return /* @__PURE__ */ jsx(
10920
+ SegmentedBar,
10921
+ {
10922
+ segments: setSegments(set),
10923
+ height,
10924
+ gap: 0,
10925
+ radius: 0,
10926
+ segmentTestID: setBarSegmentTestID,
10927
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
10928
+ testID: "set-strip-set"
10929
+ }
10930
+ );
10931
+ }
10932
+ var SET_STRIP_GAP = 5;
10933
+ function setCategory(set) {
10934
+ switch (set.status) {
10935
+ case "done":
10936
+ case "drop":
10937
+ case "myo":
10938
+ return "done";
10939
+ case "active":
10940
+ return "active";
10941
+ case "range":
10942
+ return set.doneVels.length > 0 ? "active" : "upcoming";
10943
+ case "todo":
10944
+ case "myo-upcoming":
10945
+ return "upcoming";
10946
+ }
10947
+ }
10948
+ function describeSets(sets) {
10949
+ const done = sets.filter((s) => setCategory(s) === "done").length;
10950
+ const active = sets.filter((s) => setCategory(s) === "active").length;
10951
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
10952
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
10953
+ }
10954
+ function SetStrip({ sets, height = 8, className, ...props }) {
10955
+ return /* @__PURE__ */ jsx(
10956
+ View,
10957
+ {
10958
+ className,
10959
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
10960
+ accessibilityRole: "image",
10961
+ accessibilityLabel: describeSets(sets),
10962
+ testID: "set-strip",
10963
+ ...props,
10964
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
10965
+ }
10966
+ );
10967
+ }
10968
+ var t9 = getSemanticColors("dark");
10418
10969
  var INDICATOR_CONFIG = {
10419
- imbalance: { Icon: ScaleIcon, color: t8["status-error"], label: "Left/right imbalance" },
10420
- overshoot: { Icon: AlertTriangleIcon, color: t8["status-error"], label: "Load overshoot" },
10421
- "velocity-loss": { Icon: TrendingDownIcon, color: t8["status-warning"], label: "Velocity loss" },
10422
- "missed-reps": { Icon: CircleSlashIcon, color: t8["status-warning"], label: "Missed reps" },
10423
- pr: { Icon: AwardIcon, color: t8["status-success"], label: "Personal record" },
10424
- info: { Icon: InfoIcon, color: t8["status-info"], label: "More info" }
10970
+ imbalance: { Icon: ScaleIcon, color: t9["status-error"], label: "Left/right imbalance" },
10971
+ overshoot: { Icon: AlertTriangleIcon, color: t9["status-error"], label: "Load overshoot" },
10972
+ "velocity-loss": { Icon: TrendingDownIcon, color: t9["status-warning"], label: "Velocity loss" },
10973
+ "missed-reps": { Icon: CircleSlashIcon, color: t9["status-warning"], label: "Missed reps" },
10974
+ pr: { Icon: AwardIcon, color: t9["status-success"], label: "Personal record" },
10975
+ info: { Icon: InfoIcon, color: t9["status-info"], label: "More info" }
10425
10976
  };
10426
10977
  var INDICATOR_PRECEDENCE = [
10427
10978
  "imbalance",
@@ -10716,7 +11267,9 @@ function CollapsedCard({
10716
11267
  {
10717
11268
  velocities,
10718
11269
  zones: velocityZones,
10719
- variant: "mini",
11270
+ variant: "compact",
11271
+ height: 8,
11272
+ hideBaseline: true,
10720
11273
  testID: `exercise-card-velocity-strip-${i}`
10721
11274
  },
10722
11275
  i
@@ -11015,6 +11568,7 @@ function SessionRail({
11015
11568
  next,
11016
11569
  stripHeight = 8,
11017
11570
  width = DEFAULT_WIDTH,
11571
+ expandedIndex,
11018
11572
  onExercisePress,
11019
11573
  className,
11020
11574
  style,
@@ -11055,11 +11609,28 @@ function SessionRail({
11055
11609
  borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
11056
11610
  borderBottomColor: DIVIDER
11057
11611
  },
11058
- children: /* @__PURE__ */ jsx(
11059
- ExerciseCardHeading,
11612
+ children: i === expandedIndex && ex.sets ? /* @__PURE__ */ jsx(
11613
+ ExerciseCard,
11060
11614
  {
11061
11615
  name: ex.name,
11062
- sets: ex.summary.sets,
11616
+ expanded: true,
11617
+ summary: {
11618
+ sets: ex.summary.sets,
11619
+ reps: ex.summary.reps,
11620
+ // ExerciseCard's summary takes a numeric load; a string load is
11621
+ // an unset/discovery weight, which has no expanded form yet.
11622
+ weight: typeof ex.summary.weight === "number" ? ex.summary.weight : 0,
11623
+ unit: ex.summary.unit
11624
+ },
11625
+ tempo: ex.tempo,
11626
+ sets: ex.sets,
11627
+ onExpandedChange: () => onExercisePress?.(ex, i)
11628
+ }
11629
+ ) : /* @__PURE__ */ jsx(
11630
+ ExerciseCardHeading,
11631
+ {
11632
+ name: ex.name,
11633
+ sets: ex.summary.sets,
11063
11634
  reps: ex.summary.reps,
11064
11635
  load: ex.summary.weight,
11065
11636
  unit: ex.summary.unit,
@@ -11083,7 +11654,7 @@ function SessionRail({
11083
11654
  var DEFAULTS = {
11084
11655
  label: "SS",
11085
11656
  color: getSemanticColors("dark")["brand-primary"],
11086
- bgBase: "#101010"
11657
+ bgBase: getSemanticColors("dark")["background-base"]
11087
11658
  };
11088
11659
  function SupersetWrapper({
11089
11660
  label = DEFAULTS.label,
@@ -11342,10 +11913,10 @@ function WeekRow({
11342
11913
  }
11343
11914
  );
11344
11915
  }
11345
- var t9 = getSemanticColors("dark");
11916
+ var t10 = getSemanticColors("dark");
11346
11917
  var COLORS = {
11347
- statusSuccess: t9["status-success"],
11348
- brandPrimary: t9["brand-primary"],
11918
+ statusSuccess: t10["status-success"],
11919
+ brandPrimary: t10["brand-primary"],
11349
11920
  borderDefault: "#1F1F1F",
11350
11921
  brandPrimarySubtle: "rgba(255,121,0,0.06)"
11351
11922
  };
@@ -11829,12 +12400,12 @@ function PrHistoryModal({
11829
12400
  }
11830
12401
  );
11831
12402
  }
11832
- var t10 = getSemanticColors("dark");
11833
- var BRAND_PRIMARY9 = t10["brand-primary"];
12403
+ var t11 = getSemanticColors("dark");
12404
+ var BRAND_PRIMARY9 = t11["brand-primary"];
11834
12405
  var BRAND_PRIMARY_SUBTLE = "rgba(255,121,0,0.12)";
11835
- var STATUS_SUCCESS2 = t10["status-success"];
11836
- var STATUS_WARNING2 = t10["status-warning"];
11837
- var STATUS_ERROR2 = t10["status-error"];
12406
+ var STATUS_SUCCESS2 = t11["status-success"];
12407
+ var STATUS_WARNING2 = t11["status-warning"];
12408
+ var STATUS_ERROR2 = t11["status-error"];
11838
12409
  var EMOJI_SETS = {
11839
12410
  sleep: ["\u{1F634}", "\u{1F610}", "\u{1F642}", "\u{1F60A}", "\u{1F929}"],
11840
12411
  soreness: ["\u{1F635}", "\u{1F623}", "\u{1F610}", "\u{1F642}", "\u{1F4AA}"],
@@ -12024,13 +12595,13 @@ function ReadinessCheck({
12024
12595
  )
12025
12596
  ] }) });
12026
12597
  }
12027
- var t11 = getSemanticColors("dark");
12028
- var BRAND_PRIMARY10 = t11["brand-primary"];
12598
+ var t12 = getSemanticColors("dark");
12599
+ var BRAND_PRIMARY10 = t12["brand-primary"];
12029
12600
  var BRAND_PRIMARY_DARK2 = MESO_ACCENT_GRADIENT_DARK;
12030
12601
  var BRAND_PRIMARY_LIGHT2 = MESO_ACCENT_GRADIENT_LIGHT;
12031
- var SUCCESS = t11["status-success"];
12032
- var WARNING = t11["status-warning"];
12033
- var ERROR = t11["status-error"];
12602
+ var SUCCESS = t12["status-success"];
12603
+ var WARNING = t12["status-warning"];
12604
+ var ERROR = t12["status-error"];
12034
12605
  var ACCENT_STOPS2 = [BRAND_PRIMARY_DARK2, BRAND_PRIMARY10, BRAND_PRIMARY_LIGHT2];
12035
12606
  var CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor("surface-elevated")} 0%, ${resolveColor("surface-raised")} 100%)`;
12036
12607
  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%)";
@@ -12401,8 +12972,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
12401
12972
  var PLOT_LEFT = 26;
12402
12973
  var TOOLTIP_WIDTH = 124;
12403
12974
  function timeOf(dateStr) {
12404
- const t16 = Date.parse(dateStr);
12405
- return Number.isNaN(t16) ? 0 : t16;
12975
+ const t26 = Date.parse(dateStr);
12976
+ return Number.isNaN(t26) ? 0 : t26;
12406
12977
  }
12407
12978
  function formatValue(value) {
12408
12979
  return `${Math.round(value)}`;
@@ -12891,10 +13462,10 @@ function StrengthTrendChart({
12891
13462
  }
12892
13463
  );
12893
13464
  }
12894
- var t12 = getSemanticColors("dark");
12895
- var STATUS_SUCCESS4 = t12["status-success"];
12896
- var STATUS_WARNING4 = t12["status-warning"];
12897
- var STATUS_INFO = t12["status-info"];
13465
+ var t13 = getSemanticColors("dark");
13466
+ var STATUS_SUCCESS4 = t13["status-success"];
13467
+ var STATUS_WARNING4 = t13["status-warning"];
13468
+ var STATUS_INFO = t13["status-info"];
12898
13469
  var DOT_BORDER = "#F3F4F6";
12899
13470
  var BAND_FILL = "rgba(46,213,115,0.1)";
12900
13471
  var BAND_EDGE = "rgba(46,213,115,0.45)";
@@ -12937,8 +13508,8 @@ function interpEdge(pixels, x, key) {
12937
13508
  const a = pixels[i];
12938
13509
  const b = pixels[i + 1];
12939
13510
  if (x >= a.x && x <= b.x) {
12940
- const t16 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
12941
- return a[key] + t16 * (b[key] - a[key]);
13511
+ const t26 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
13512
+ return a[key] + t26 * (b[key] - a[key]);
12942
13513
  }
12943
13514
  }
12944
13515
  return pixels[pixels.length - 1][key];
@@ -13268,13 +13839,13 @@ function CapacityBandChart({
13268
13839
  }
13269
13840
  );
13270
13841
  }
13271
- var t13 = getSemanticColors("dark");
13842
+ var t14 = getSemanticColors("dark");
13272
13843
  function glowShadow(color) {
13273
13844
  return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`;
13274
13845
  }
13275
13846
  var DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200];
13276
13847
  var DEFAULT_MARKER_COLOR2 = primitiveColors.white;
13277
- var TICK_COLOR = t13["text-tertiary"];
13848
+ var TICK_COLOR = t14["text-tertiary"];
13278
13849
  var ZONE_SIZES = {
13279
13850
  default: {
13280
13851
  trackHeight: 14,
@@ -13457,7 +14028,7 @@ function ZoneTrack({
13457
14028
  ),
13458
14029
  ticks?.map((tick, i) => {
13459
14030
  const emphasized = tick.emphasized === true;
13460
- const lineColor = tick.color ?? (emphasized ? t13["brand-primary"] : TICK_COLOR);
14031
+ const lineColor = tick.color ?? (emphasized ? t14["brand-primary"] : TICK_COLOR);
13461
14032
  const lineWidth = emphasized ? s.tickLineEmphasized : s.tickLineNormal;
13462
14033
  return /* @__PURE__ */ jsx(
13463
14034
  View,
@@ -13488,7 +14059,7 @@ function ZoneTrack({
13488
14059
  if (tick.label == null) return null;
13489
14060
  const emphasized = tick.emphasized === true;
13490
14061
  if (!showTickLabel(i, emphasized)) return null;
13491
- const labelColor = tick.color ?? (emphasized ? t13["brand-primary"] : TICK_COLOR);
14062
+ const labelColor = tick.color ?? (emphasized ? t14["brand-primary"] : TICK_COLOR);
13492
14063
  const labelNode = /* @__PURE__ */ jsx(
13493
14064
  Text,
13494
14065
  {
@@ -13544,14 +14115,14 @@ function FatigueMeter({
13544
14115
  className,
13545
14116
  ...props
13546
14117
  }) {
13547
- const [t1, t22, t32] = thresholds;
14118
+ const [t1, t26, t32] = thresholds;
13548
14119
  const zones = [
13549
14120
  { upTo: t1, color: zoneColors[0] },
13550
- { upTo: t22, color: zoneColors[1] },
14121
+ { upTo: t26, color: zoneColors[1] },
13551
14122
  { upTo: t32, color: zoneColors[2] },
13552
14123
  { upTo: max, color: zoneColors[3] }
13553
14124
  ];
13554
- const tickValues = [0, t1, t22, t32, max];
14125
+ const tickValues = [0, t1, t26, t32, max];
13555
14126
  const ticks = tickValues.map((v, i) => ({ value: v, label: labels[i] }));
13556
14127
  return /* @__PURE__ */ jsx(
13557
14128
  ZoneTrack,
@@ -13674,7 +14245,7 @@ function VolumeLandmarkBar({
13674
14245
  }
13675
14246
  );
13676
14247
  }
13677
- var t14 = getSemanticColors("dark");
14248
+ var t15 = getSemanticColors("dark");
13678
14249
  var statusToken = {
13679
14250
  productive: "status-success",
13680
14251
  threshold: "status-warning",
@@ -13691,7 +14262,7 @@ var defaultLabel = {
13691
14262
  stop: "Stop"
13692
14263
  };
13693
14264
  function statusPillColor(status) {
13694
- return t14[statusToken[status]];
14265
+ return t15[statusToken[status]];
13695
14266
  }
13696
14267
  function StatusPill2({ status, label, className, style, ...props }) {
13697
14268
  const color = statusPillColor(status);
@@ -13724,7 +14295,7 @@ function StatusPill2({ status, label, className, style, ...props }) {
13724
14295
  }
13725
14296
  );
13726
14297
  }
13727
- var t15 = getSemanticColors("dark");
14298
+ var t16 = getSemanticColors("dark");
13728
14299
  var categoryToken = {
13729
14300
  productive: null,
13730
14301
  threshold: "status-warning",
@@ -13737,7 +14308,7 @@ var floodOpacity = {
13737
14308
  };
13738
14309
  function liveAuraColor(category) {
13739
14310
  const token = categoryToken[category];
13740
- return token ? t15[token] : null;
14311
+ return token ? t16[token] : null;
13741
14312
  }
13742
14313
  function LiveAuraFrame({
13743
14314
  category,
@@ -13766,9 +14337,9 @@ function LiveAuraFrame({
13766
14337
  position: "relative",
13767
14338
  overflow: "hidden",
13768
14339
  borderRadius: 10,
13769
- backgroundColor: t15["background-base"],
14340
+ backgroundColor: t16["background-base"],
13770
14341
  borderWidth: 1,
13771
- borderColor: t15["border-default"]
14342
+ borderColor: t16["border-default"]
13772
14343
  },
13773
14344
  style
13774
14345
  ],
@@ -13789,6 +14360,905 @@ function LiveAuraFrame({
13789
14360
  }
13790
14361
  );
13791
14362
  }
14363
+ function VelocityHero({
14364
+ velocities,
14365
+ targetReps,
14366
+ liveRepIndex,
14367
+ width,
14368
+ height = 300
14369
+ }) {
14370
+ return /* @__PURE__ */ jsx(View, { testID: "velocity-hero", style: width != null ? { width, height } : { flex: 1, height }, children: /* @__PURE__ */ jsx(
14371
+ VelocityStrip,
14372
+ {
14373
+ variant: "hero",
14374
+ velocities,
14375
+ liveRepIndex: liveRepIndex ?? velocities.length - 1,
14376
+ targetReps,
14377
+ height,
14378
+ scale: "peak"
14379
+ }
14380
+ ) });
14381
+ }
14382
+
14383
+ // src/components/custom/Fatigue/fatigue-tokens.ts
14384
+ var t17 = getSemanticColors("dark");
14385
+ var FONT_HEAD = '"Space Grotesk", sans-serif';
14386
+ var FONT_UI = '"Nunito Sans", sans-serif';
14387
+ var FONT_MONO = "monospace";
14388
+ var TONE_COLOR = {
14389
+ ok: t17["status-success"],
14390
+ warn: t17["status-warning"],
14391
+ alarm: t17["status-error"]
14392
+ };
14393
+ var STATE_LABEL = {
14394
+ good: "Good",
14395
+ slowing: "Slowing",
14396
+ grinding: "Grinding",
14397
+ "form-breakdown": "Form breaking down"
14398
+ };
14399
+ var PHASE_AXIS_COLOR = {
14400
+ eccentric: primitiveRamps.magenta[800],
14401
+ concentric: primitiveRamps.cyan[800],
14402
+ hold: primitiveColors.charcoal[100],
14403
+ idle: primitiveColors.charcoal[200]
14404
+ };
14405
+ var PHASE_AXIS_BASE_COLOR = {
14406
+ eccentric: primitiveRamps.magenta[950],
14407
+ concentric: primitiveRamps.cyan[950],
14408
+ hold: primitiveColors.charcoal[300],
14409
+ idle: primitiveColors.charcoal[200]
14410
+ };
14411
+ var PACING_TONE = {
14412
+ ahead: primitiveRamps.amber[300],
14413
+ onPace: primitiveRamps.green[300],
14414
+ over: primitiveRamps.red[300]
14415
+ };
14416
+ var SILVER = primitiveColors.neutral[300];
14417
+ var RED_LIGHT = primitiveRamps.red[400];
14418
+ var RED_MID = primitiveRamps.red[600];
14419
+ var RED_DEEP = primitiveRamps.red[800];
14420
+ var GRIND_THRESHOLD = 0.35;
14421
+ var DRIFT_GREY = primitiveColors.neutral[600];
14422
+ function clamp012(v) {
14423
+ return Math.min(1, Math.max(0, v));
14424
+ }
14425
+ function hexToRgb2(hex) {
14426
+ const h = hex.replace("#", "");
14427
+ const f = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
14428
+ return [parseInt(f.slice(0, 2), 16), parseInt(f.slice(2, 4), 16), parseInt(f.slice(4, 6), 16)];
14429
+ }
14430
+ function mixHex2(a, b, ratio) {
14431
+ const [ar, ag, ab] = hexToRgb2(a);
14432
+ const [br, bg, bb] = hexToRgb2(b);
14433
+ const m = (x, y) => Math.round(x + (y - x) * clamp012(ratio));
14434
+ return `#${[m(ar, br), m(ag, bg), m(ab, bb)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
14435
+ }
14436
+ function ghostLineColor(tempoDeviation, grindSignature) {
14437
+ if (grindSignature >= GRIND_THRESHOLD) {
14438
+ const severity = clamp012((grindSignature - GRIND_THRESHOLD) / (1 - GRIND_THRESHOLD));
14439
+ return severity < 0.5 ? mixHex2(RED_LIGHT, RED_MID, severity * 2) : mixHex2(RED_MID, RED_DEEP, (severity - 0.5) * 2);
14440
+ }
14441
+ return mixHex2(SILVER, DRIFT_GREY, (tempoDeviation ?? 0) * 0.55);
14442
+ }
14443
+ function auraForVerdict(state) {
14444
+ if (state == null || state === "good") return "productive";
14445
+ if (state === "form-breakdown") return "stop";
14446
+ return "threshold";
14447
+ }
14448
+
14449
+ // src/components/custom/Fatigue/VerdictHero.tsx
14450
+ var t18 = getSemanticColors("dark");
14451
+ function formatRpe(rpe) {
14452
+ if (rpe == null) return "\u2014";
14453
+ return (Math.round(rpe * 2) / 2).toFixed(1);
14454
+ }
14455
+ function VerdictHero({ rpe, verdict }) {
14456
+ const tone = verdict ? TONE_COLOR[verdict.tone] : t18["text-tertiary"];
14457
+ const word = verdict ? STATE_LABEL[verdict.state] : "Warming up";
14458
+ return /* @__PURE__ */ jsxs(View, { style: { gap: 6 }, testID: "verdict-hero", children: [
14459
+ /* @__PURE__ */ jsx(
14460
+ Text,
14461
+ {
14462
+ style: {
14463
+ fontSize: 9,
14464
+ letterSpacing: 1.5,
14465
+ fontFamily: FONT_MONO,
14466
+ color: t18["text-tertiary"]
14467
+ },
14468
+ children: "FATIGUE"
14469
+ }
14470
+ ),
14471
+ /* @__PURE__ */ jsxs(View, { style: { gap: 2 }, children: [
14472
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-end", gap: 7 }, children: [
14473
+ /* @__PURE__ */ jsx(
14474
+ Text,
14475
+ {
14476
+ style: {
14477
+ fontSize: 62,
14478
+ fontWeight: "900",
14479
+ fontFamily: FONT_HEAD,
14480
+ color: tone,
14481
+ lineHeight: 60
14482
+ },
14483
+ accessibilityLabel: `RPE ${formatRpe(rpe)}`,
14484
+ children: formatRpe(rpe)
14485
+ }
14486
+ ),
14487
+ /* @__PURE__ */ jsx(
14488
+ Text,
14489
+ {
14490
+ style: {
14491
+ fontSize: 18,
14492
+ fontWeight: "800",
14493
+ color: tone,
14494
+ marginBottom: 9,
14495
+ fontFamily: FONT_HEAD
14496
+ },
14497
+ children: "RPE"
14498
+ }
14499
+ )
14500
+ ] }),
14501
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 17, fontWeight: "800", fontFamily: FONT_HEAD, color: tone }, children: word })
14502
+ ] })
14503
+ ] });
14504
+ }
14505
+ var t19 = getSemanticColors("dark");
14506
+ var TONE_VARIANT = {
14507
+ ok: "success",
14508
+ warn: "warning",
14509
+ alarm: "error"
14510
+ };
14511
+ var TONE_WORD = { ok: "ok", warn: "watch", alarm: "alarm" };
14512
+ function Light({
14513
+ label,
14514
+ tone,
14515
+ detail
14516
+ }) {
14517
+ const variant = tone ? TONE_VARIANT[tone] : "neutral";
14518
+ const word = tone ? TONE_WORD[tone] : "warming up";
14519
+ return /* @__PURE__ */ jsx(Tooltip, { label: `${detail} \xB7 ${word}`, placement: "bottom", children: /* @__PURE__ */ jsxs(
14520
+ View,
14521
+ {
14522
+ accessibilityLabel: `${detail}, ${word}`,
14523
+ style: { flexDirection: "row", alignItems: "center", gap: 5 },
14524
+ children: [
14525
+ /* @__PURE__ */ jsx(StatusDot, { variant, size: "sm", glow: true }),
14526
+ /* @__PURE__ */ jsx(
14527
+ Text,
14528
+ {
14529
+ style: {
14530
+ fontSize: 9,
14531
+ letterSpacing: 0.6,
14532
+ fontFamily: FONT_MONO,
14533
+ color: t19["text-secondary"]
14534
+ },
14535
+ children: label
14536
+ }
14537
+ )
14538
+ ]
14539
+ }
14540
+ ) });
14541
+ }
14542
+ function FatigueLights({ dimensions, spread = false }) {
14543
+ return /* @__PURE__ */ jsxs(
14544
+ View,
14545
+ {
14546
+ testID: "fatigue-lights",
14547
+ style: {
14548
+ flexDirection: "row",
14549
+ gap: spread ? 0 : 16,
14550
+ alignItems: "center",
14551
+ justifyContent: spread ? "space-between" : "flex-start"
14552
+ },
14553
+ children: [
14554
+ /* @__PURE__ */ jsx(Light, { label: "VEL", tone: dimensions?.velocityLoss ?? null, detail: "Velocity loss" }),
14555
+ /* @__PURE__ */ jsx(Light, { label: "ROM", tone: dimensions?.rom ?? null, detail: "ROM depth" }),
14556
+ /* @__PURE__ */ jsx(Light, { label: "TEMPO", tone: dimensions?.tempo ?? null, detail: "Tempo" })
14557
+ ]
14558
+ }
14559
+ );
14560
+ }
14561
+ function barColor(romM, working, short) {
14562
+ if (short != null && romM < short) return RED_DEEP;
14563
+ if (working != null && romM < working) return RED_LIGHT;
14564
+ return SILVER;
14565
+ }
14566
+ function romReferenceOverlay(g, workingStandardM, shortThresholdM) {
14567
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
14568
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14569
+ View,
14570
+ {
14571
+ pointerEvents: "none",
14572
+ style: {
14573
+ position: "absolute",
14574
+ left: 0,
14575
+ right: 0,
14576
+ bottom: 0,
14577
+ height: g.yOf(shortThresholdM),
14578
+ backgroundColor: alpha(RED_DEEP, 0.06)
14579
+ }
14580
+ }
14581
+ ),
14582
+ workingStandardM != null && /* @__PURE__ */ jsx(
14583
+ View,
14584
+ {
14585
+ pointerEvents: "none",
14586
+ style: {
14587
+ position: "absolute",
14588
+ left: 0,
14589
+ right: 0,
14590
+ bottom: g.yOf(workingStandardM),
14591
+ borderTopWidth: 1,
14592
+ borderStyle: "dashed",
14593
+ borderColor: alpha(SILVER, 0.4)
14594
+ }
14595
+ }
14596
+ ),
14597
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14598
+ View,
14599
+ {
14600
+ pointerEvents: "none",
14601
+ style: {
14602
+ position: "absolute",
14603
+ left: 0,
14604
+ right: 0,
14605
+ bottom: g.yOf(shortThresholdM),
14606
+ borderTopWidth: 1,
14607
+ borderStyle: "dashed",
14608
+ borderColor: alpha(RED_DEEP, 0.4)
14609
+ }
14610
+ }
14611
+ )
14612
+ ] });
14613
+ }
14614
+ function RomProgressionChart({
14615
+ points,
14616
+ workingStandardM,
14617
+ shortThresholdM,
14618
+ barHeight = 44,
14619
+ maxReps = 12,
14620
+ plannedReps: plannedReps2
14621
+ }) {
14622
+ const shown = points.slice(-maxReps);
14623
+ const slots = shown.map((p) => ({ kind: "rep", value: p.romM }));
14624
+ const scaleMax = Math.max(...shown.map((p) => p.romM), workingStandardM ?? 0, shortThresholdM ?? 0) || void 0;
14625
+ return /* @__PURE__ */ jsx(View, { testID: "rom-progression", children: /* @__PURE__ */ jsx(
14626
+ SetBarChart,
14627
+ {
14628
+ slots,
14629
+ colorFor: (romM) => barColor(romM, workingStandardM, shortThresholdM),
14630
+ height: barHeight,
14631
+ scaleMax,
14632
+ barRadius: 2,
14633
+ hideBaseline: true,
14634
+ targetReps: plannedReps2,
14635
+ renderReference: (g) => romReferenceOverlay(g, workingStandardM, shortThresholdM),
14636
+ testIDPrefix: "rom"
14637
+ }
14638
+ ) });
14639
+ }
14640
+
14641
+ // src/components/custom/Fatigue/tempo-pacing.ts
14642
+ var t20 = getSemanticColors("dark");
14643
+ var ON_TARGET_MS2 = 100;
14644
+ function phaseFillFraction(elapsedMs, targetMs) {
14645
+ if (targetMs == null || targetMs <= 0) return 1;
14646
+ return Math.min(1, Math.max(0, elapsedMs / targetMs));
14647
+ }
14648
+ function pacingTone(elapsedMs, targetMs) {
14649
+ if (targetMs == null || targetMs <= 0) return t20["text-primary"];
14650
+ const remainingMs = targetMs - elapsedMs;
14651
+ if (remainingMs > ON_TARGET_MS2) return PACING_TONE.ahead;
14652
+ if (remainingMs >= -ON_TARGET_MS2) return PACING_TONE.onPace;
14653
+ return PACING_TONE.over;
14654
+ }
14655
+ function prescribedSegments(tempo) {
14656
+ if (tempo == null) return [];
14657
+ const [ecc, pauseBottom, con, pauseTop] = tempo;
14658
+ const order = [
14659
+ ["eccentric", ecc],
14660
+ ["hold", pauseBottom],
14661
+ ["concentric", con],
14662
+ ["hold", pauseTop]
14663
+ ];
14664
+ const out = [];
14665
+ let t26 = 0;
14666
+ for (const [phase, seconds] of order) {
14667
+ const ms = seconds * 1e3;
14668
+ if (ms > 0) out.push({ phase, startMs: t26, endMs: t26 + ms });
14669
+ t26 += ms;
14670
+ }
14671
+ return out;
14672
+ }
14673
+ function phaseTargetsMs(phases, tempo) {
14674
+ if (tempo == null) return phases.map(() => null);
14675
+ const [eccS, pauseBottomS, conS, pauseTopS] = tempo;
14676
+ let seenConcentric = false;
14677
+ return phases.map((phase) => {
14678
+ switch (phase) {
14679
+ case "eccentric":
14680
+ return eccS * 1e3;
14681
+ case "concentric":
14682
+ seenConcentric = true;
14683
+ return conS * 1e3;
14684
+ case "hold":
14685
+ return (seenConcentric ? pauseTopS : pauseBottomS) * 1e3;
14686
+ default:
14687
+ return null;
14688
+ }
14689
+ });
14690
+ }
14691
+
14692
+ // src/components/custom/Fatigue/GhostBand.tsx
14693
+ var t21 = getSemanticColors("dark");
14694
+ var BAND_H = 16;
14695
+ var BAND_GAP = 4;
14696
+ var PHASE_LABEL = {
14697
+ eccentric: "ECC",
14698
+ concentric: "CON"
14699
+ };
14700
+ var LABEL_CH_PX = 7;
14701
+ var LABEL_PAD_PX = 6;
14702
+ var WELL_SHADE = primitiveColors.black;
14703
+ var WELL_FLOOR_LIGHT = primitiveColors.white;
14704
+ function labelFits(label, segW) {
14705
+ return segW >= label.length * LABEL_CH_PX + LABEL_PAD_PX;
14706
+ }
14707
+ function GhostBand({
14708
+ segments,
14709
+ x,
14710
+ top,
14711
+ height = BAND_H,
14712
+ showLabels = false,
14713
+ labelColor = t21["text-primary"],
14714
+ targetTempoSeconds = null,
14715
+ prescribed = false
14716
+ }) {
14717
+ const rawId = useId();
14718
+ const safeId = rawId.replace(/[^a-zA-Z0-9]/g, "");
14719
+ const clipId = `ghost-band-${safeId}`;
14720
+ const wellId = `ghost-band-well-${safeId}`;
14721
+ const drawn = segments.filter((seg) => x(seg.endMs) - x(seg.startMs) > 0);
14722
+ if (drawn.length === 0) return null;
14723
+ const bandLeft = x(drawn[0].startMs);
14724
+ const bandRight = x(drawn[drawn.length - 1].endMs);
14725
+ const bandW = bandRight - bandLeft;
14726
+ if (bandW <= 0) return null;
14727
+ const pacing = targetTempoSeconds != null && !prescribed;
14728
+ const targetsMs = phaseTargetsMs(
14729
+ drawn.map((seg) => seg.phase),
14730
+ targetTempoSeconds
14731
+ );
14732
+ const runs = drawn.map((seg, i) => {
14733
+ const left = x(seg.startMs);
14734
+ const right = i === drawn.length - 1 ? bandRight : x(drawn[i + 1].startMs);
14735
+ const width = Math.max(0, right - left);
14736
+ const targetMs = targetsMs[i];
14737
+ const elapsedMs = seg.endMs - seg.startMs;
14738
+ return {
14739
+ phase: seg.phase,
14740
+ left,
14741
+ width,
14742
+ // Prescribed → nothing performed, so nothing filled. No target at all → the run reads
14743
+ // complete rather than perpetually empty.
14744
+ fillWidth: prescribed ? 0 : pacing ? width * phaseFillFraction(elapsedMs, targetMs) : width,
14745
+ labelTone: pacing ? pacingTone(elapsedMs, targetMs) : labelColor
14746
+ };
14747
+ });
14748
+ return /* @__PURE__ */ jsxs("g", { children: [
14749
+ /* @__PURE__ */ jsxs("defs", { children: [
14750
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: bandLeft, y: top, width: bandW, height, rx: 2 }) }),
14751
+ /* @__PURE__ */ jsxs("linearGradient", { id: wellId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
14752
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: WELL_SHADE, stopOpacity: 0.6 }),
14753
+ /* @__PURE__ */ jsx("stop", { offset: "42%", stopColor: WELL_SHADE, stopOpacity: 0.12 }),
14754
+ /* @__PURE__ */ jsx("stop", { offset: "88%", stopColor: WELL_SHADE, stopOpacity: 0.02 }),
14755
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: WELL_FLOOR_LIGHT, stopOpacity: 0.07 })
14756
+ ] })
14757
+ ] }),
14758
+ /* @__PURE__ */ jsxs("g", { clipPath: `url(#${clipId})`, children: [
14759
+ /* @__PURE__ */ jsx(
14760
+ "rect",
14761
+ {
14762
+ x: bandLeft,
14763
+ y: top,
14764
+ width: bandW,
14765
+ height,
14766
+ fill: PHASE_AXIS_COLOR.idle,
14767
+ "data-testid": "ghost-band-floor"
14768
+ }
14769
+ ),
14770
+ runs.map((run, i) => /* @__PURE__ */ jsxs("g", { children: [
14771
+ /* @__PURE__ */ jsx(
14772
+ "rect",
14773
+ {
14774
+ x: run.left,
14775
+ y: top,
14776
+ width: run.width,
14777
+ height,
14778
+ fill: PHASE_AXIS_BASE_COLOR[run.phase],
14779
+ "data-testid": "ghost-band-base"
14780
+ }
14781
+ ),
14782
+ /* @__PURE__ */ jsx(
14783
+ "rect",
14784
+ {
14785
+ x: run.left,
14786
+ y: top,
14787
+ width: run.width,
14788
+ height,
14789
+ fill: `url(#${wellId})`,
14790
+ "data-testid": "ghost-band-well"
14791
+ }
14792
+ ),
14793
+ /* @__PURE__ */ jsx(
14794
+ "rect",
14795
+ {
14796
+ x: run.left,
14797
+ y: top,
14798
+ width: run.fillWidth,
14799
+ height,
14800
+ fill: PHASE_AXIS_COLOR[run.phase],
14801
+ "data-testid": "ghost-band-fill"
14802
+ }
14803
+ )
14804
+ ] }, i)),
14805
+ showLabels && runs.map((run, i) => {
14806
+ const label = PHASE_LABEL[run.phase];
14807
+ if (!label || !labelFits(label, run.width)) return null;
14808
+ return /* @__PURE__ */ jsx(
14809
+ "text",
14810
+ {
14811
+ x: run.left + run.width / 2,
14812
+ y: top + height / 2,
14813
+ textAnchor: "middle",
14814
+ dominantBaseline: "central",
14815
+ fill: run.labelTone,
14816
+ fontSize: 8,
14817
+ fontWeight: 800,
14818
+ letterSpacing: 1,
14819
+ fontFamily: FONT_UI,
14820
+ children: label
14821
+ },
14822
+ i
14823
+ );
14824
+ })
14825
+ ] })
14826
+ ] });
14827
+ }
14828
+ var t22 = getSemanticColors("dark");
14829
+ var PARCH = t22["text-primary"];
14830
+ function smoothPath(pts) {
14831
+ if (pts.length < 2) return pts.length ? `M ${pts[0][0]} ${pts[0][1]}` : "";
14832
+ let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
14833
+ for (let i = 0; i < pts.length - 1; i++) {
14834
+ const p0 = pts[i - 1] ?? pts[i];
14835
+ const p1 = pts[i];
14836
+ const p2 = pts[i + 1];
14837
+ const p3 = pts[i + 2] ?? p2;
14838
+ const c1x = p1[0] + (p2[0] - p0[0]) / 6;
14839
+ const c1y = p1[1] + (p2[1] - p0[1]) / 6;
14840
+ const c2x = p2[0] - (p3[0] - p1[0]) / 6;
14841
+ const c2y = p2[1] - (p3[1] - p1[1]) / 6;
14842
+ d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
14843
+ }
14844
+ return d;
14845
+ }
14846
+ var BLUR_STD_DEV = 2.6;
14847
+ function GhostBloom({
14848
+ current,
14849
+ ghosts = [],
14850
+ tint,
14851
+ baseline,
14852
+ orientation = "up",
14853
+ ghostStroke = (i) => alpha(PARCH, 0.1 + i * 0.015),
14854
+ lineWidth = 3.5
14855
+ }) {
14856
+ const rawId = useId();
14857
+ const blurId = `bloom-blur-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`;
14858
+ const toY = (mag) => orientation === "down" ? baseline + mag : baseline - mag;
14859
+ const toAbs = (pts) => pts.map(([px, mag]) => [px, toY(mag)]);
14860
+ const curD = smoothPath(toAbs(current));
14861
+ const ghostDs = ghosts.map((g) => smoothPath(toAbs(g)));
14862
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
14863
+ /* @__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 }) }) }),
14864
+ ghostDs.map((d, i) => /* @__PURE__ */ jsx(
14865
+ "path",
14866
+ {
14867
+ d,
14868
+ fill: "none",
14869
+ stroke: ghostStroke(i),
14870
+ strokeWidth: 1.5,
14871
+ strokeLinejoin: "round"
14872
+ },
14873
+ i
14874
+ )),
14875
+ /* @__PURE__ */ jsx(
14876
+ "path",
14877
+ {
14878
+ d: curD,
14879
+ fill: "none",
14880
+ stroke: alpha(primitiveColors.black, 0.3),
14881
+ strokeWidth: lineWidth + 3,
14882
+ strokeLinejoin: "round",
14883
+ strokeLinecap: "round",
14884
+ filter: `url(#${blurId})`
14885
+ }
14886
+ ),
14887
+ /* @__PURE__ */ jsx(
14888
+ "path",
14889
+ {
14890
+ d: curD,
14891
+ fill: "none",
14892
+ stroke: tint,
14893
+ strokeWidth: lineWidth,
14894
+ strokeLinejoin: "round",
14895
+ strokeLinecap: "round"
14896
+ }
14897
+ )
14898
+ ] });
14899
+ }
14900
+
14901
+ // src/components/custom/Fatigue/GhostSpark.tsx
14902
+ function GhostSpark({
14903
+ curves,
14904
+ width,
14905
+ height = 172,
14906
+ targetTempoSeconds = null
14907
+ }) {
14908
+ const w = width;
14909
+ const h = height;
14910
+ const padL = 12;
14911
+ const padR = 8;
14912
+ const padTop = 10;
14913
+ const padBot = 6;
14914
+ if (curves.length === 0) {
14915
+ const prescribed = prescribedSegments(targetTempoSeconds);
14916
+ if (prescribed.length === 0) {
14917
+ return /* @__PURE__ */ jsx(View, { testID: "ghost-spark", style: { width: w, height: h } });
14918
+ }
14919
+ const totalMs = prescribed[prescribed.length - 1].endMs;
14920
+ const bandTopEmpty = h - padBot - BAND_H;
14921
+ const xEmpty = (ms) => padL + ms / (totalMs * 1.04) * (w - padL - padR);
14922
+ return /* @__PURE__ */ jsx(View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsx("svg", { width: w, height: h, children: /* @__PURE__ */ jsx(
14923
+ GhostBand,
14924
+ {
14925
+ segments: prescribed,
14926
+ x: xEmpty,
14927
+ top: bandTopEmpty,
14928
+ showLabels: true,
14929
+ targetTempoSeconds,
14930
+ prescribed: true
14931
+ }
14932
+ ) }) });
14933
+ }
14934
+ const cur = curves[curves.length - 1];
14935
+ const allSamples = curves.flatMap((c) => c.samples);
14936
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
14937
+ const axisMaxT = Math.max(1, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0)) * 1.04;
14938
+ const bandBottom = h - padBot;
14939
+ const bandTop = bandBottom - BAND_H;
14940
+ const baseline = bandTop - BAND_GAP;
14941
+ const plotH = Math.max(1, baseline - padTop);
14942
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
14943
+ const mag = (v) => clamp012(v / vmax) * plotH;
14944
+ const lineTint = ghostLineColor(cur.tempoDeviation, cur.grindSignature);
14945
+ const curPts = cur.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
14946
+ const ghostPts = curves.slice(0, -1).map((c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]));
14947
+ return /* @__PURE__ */ jsx(View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
14948
+ /* @__PURE__ */ jsx(
14949
+ GhostBloom,
14950
+ {
14951
+ current: curPts,
14952
+ ghosts: ghostPts,
14953
+ tint: lineTint,
14954
+ baseline,
14955
+ orientation: "up"
14956
+ }
14957
+ ),
14958
+ /* @__PURE__ */ jsx(
14959
+ GhostBand,
14960
+ {
14961
+ segments: cur.phaseSegments,
14962
+ x,
14963
+ top: bandTop,
14964
+ showLabels: true,
14965
+ targetTempoSeconds
14966
+ }
14967
+ )
14968
+ ] }) });
14969
+ }
14970
+
14971
+ // src/components/custom/Fatigue/LiveFatigueCard.tsx
14972
+ var t23 = getSemanticColors("dark");
14973
+ var PAD = 18;
14974
+ var GHOST_GUTTER = 4;
14975
+ function LiveFatigueCard({ model, width = 318, height }) {
14976
+ const chartW = width - PAD * 2 - GHOST_GUTTER * 2;
14977
+ const chartH = height != null ? Math.round(Math.min(240, Math.max(168, height * 0.4))) : 172;
14978
+ return /* @__PURE__ */ jsxs(
14979
+ Surface,
14980
+ {
14981
+ level: "base",
14982
+ testID: "live-fatigue-card",
14983
+ style: {
14984
+ width,
14985
+ height,
14986
+ borderRadius: 14,
14987
+ padding: PAD,
14988
+ borderWidth: 1,
14989
+ borderColor: t23["hairline-default"],
14990
+ ...barPaper(t23["surface-base"])
14991
+ },
14992
+ children: [
14993
+ /* @__PURE__ */ jsxs(View, { style: { gap: 12 }, children: [
14994
+ /* @__PURE__ */ jsx(VerdictHero, { rpe: model.rpe, verdict: model.verdict }),
14995
+ /* @__PURE__ */ jsx(FatigueLights, { dimensions: model.verdict?.dimensions ?? null })
14996
+ ] }),
14997
+ /* @__PURE__ */ jsx(View, { style: { flex: 1, minHeight: 16 } }),
14998
+ /* @__PURE__ */ jsx(
14999
+ RomProgressionChart,
15000
+ {
15001
+ points: model.romProgression,
15002
+ workingStandardM: model.romWorkingStandardM,
15003
+ shortThresholdM: model.romShortThresholdM,
15004
+ plannedReps: model.plannedReps
15005
+ }
15006
+ ),
15007
+ /* @__PURE__ */ jsx(View, { style: { flex: 1, minHeight: 16 } }),
15008
+ /* @__PURE__ */ jsx(
15009
+ GhostSpark,
15010
+ {
15011
+ curves: model.velocityCurves,
15012
+ width: chartW,
15013
+ height: chartH,
15014
+ targetTempoSeconds: model.targetTempoSeconds
15015
+ }
15016
+ )
15017
+ ]
15018
+ }
15019
+ );
15020
+ }
15021
+
15022
+ // src/components/custom/Fatigue/LiveFatiguePanel.tsx
15023
+ var t24 = getSemanticColors("dark");
15024
+ function ExerciseHeaderLite({ header }) {
15025
+ return /* @__PURE__ */ jsxs(
15026
+ View,
15027
+ {
15028
+ style: {
15029
+ flexDirection: "row",
15030
+ alignItems: "flex-end",
15031
+ justifyContent: "space-between",
15032
+ paddingHorizontal: 24,
15033
+ paddingTop: 18,
15034
+ paddingBottom: 14,
15035
+ borderBottomWidth: 1,
15036
+ borderColor: alpha(t24["text-primary"], 0.08)
15037
+ },
15038
+ children: [
15039
+ /* @__PURE__ */ jsxs(View, { style: { gap: 2 }, children: [
15040
+ /* @__PURE__ */ jsx(
15041
+ Text,
15042
+ {
15043
+ style: {
15044
+ fontSize: 28,
15045
+ fontWeight: "700",
15046
+ fontFamily: FONT_HEAD,
15047
+ color: t24["text-primary"]
15048
+ },
15049
+ children: header.title
15050
+ }
15051
+ ),
15052
+ header.subtitle && /* @__PURE__ */ jsx(
15053
+ Text,
15054
+ {
15055
+ style: {
15056
+ fontSize: 13,
15057
+ fontWeight: "600",
15058
+ fontFamily: FONT_UI,
15059
+ color: t24["text-secondary"]
15060
+ },
15061
+ children: header.subtitle
15062
+ }
15063
+ )
15064
+ ] }),
15065
+ header.meta && /* @__PURE__ */ jsx(
15066
+ Text,
15067
+ {
15068
+ style: {
15069
+ fontSize: 13,
15070
+ fontWeight: "800",
15071
+ fontFamily: FONT_MONO,
15072
+ color: t24["text-tertiary"]
15073
+ },
15074
+ children: header.meta
15075
+ }
15076
+ )
15077
+ ]
15078
+ }
15079
+ );
15080
+ }
15081
+ function LiveFatiguePanel({
15082
+ model,
15083
+ velocity,
15084
+ header,
15085
+ aura,
15086
+ bodyHeight = 508,
15087
+ cardWidth = 318
15088
+ }) {
15089
+ const category = aura ?? auraForVerdict(model.verdict?.state ?? null);
15090
+ const heroH = bodyHeight - 26;
15091
+ return /* @__PURE__ */ jsx(
15092
+ LiveAuraFrame,
15093
+ {
15094
+ category,
15095
+ style: { borderRadius: 0, borderWidth: 0 },
15096
+ testID: "live-fatigue-panel",
15097
+ children: /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
15098
+ header && /* @__PURE__ */ jsx(ExerciseHeaderLite, { header }),
15099
+ /* @__PURE__ */ jsxs(View, { style: { padding: 24, flexDirection: "row", gap: 18, alignItems: "stretch" }, children: [
15100
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, gap: 8 }, children: [
15101
+ /* @__PURE__ */ jsx(
15102
+ Text,
15103
+ {
15104
+ style: {
15105
+ fontSize: 9,
15106
+ letterSpacing: 1.2,
15107
+ fontFamily: FONT_MONO,
15108
+ color: t24["text-tertiary"]
15109
+ },
15110
+ children: "VELOCITY \xB7 this set"
15111
+ }
15112
+ ),
15113
+ /* @__PURE__ */ jsx(
15114
+ VelocityHero,
15115
+ {
15116
+ velocities: velocity.velocities,
15117
+ targetReps: velocity.targetReps,
15118
+ liveRepIndex: velocity.liveRepIndex,
15119
+ height: heroH
15120
+ }
15121
+ )
15122
+ ] }),
15123
+ /* @__PURE__ */ jsx(LiveFatigueCard, { model, width: cardWidth, height: bodyHeight })
15124
+ ] })
15125
+ ] })
15126
+ }
15127
+ );
15128
+ }
15129
+ var t25 = getSemanticColors("dark");
15130
+ function phaseAt(segments, ms) {
15131
+ const hit = segments.find((s) => ms >= s.startMs && ms < s.endMs);
15132
+ return hit ? hit.phase : null;
15133
+ }
15134
+ function mergePhaseSegments(a, b) {
15135
+ if (a.length === 0) return b;
15136
+ if (b.length === 0) return a;
15137
+ const bounds = Array.from(new Set([...a, ...b].flatMap((s) => [s.startMs, s.endMs]))).sort(
15138
+ (p, q) => p - q
15139
+ );
15140
+ const out = [];
15141
+ for (let i = 0; i < bounds.length - 1; i++) {
15142
+ const startMs = bounds[i];
15143
+ const endMs = bounds[i + 1];
15144
+ if (endMs <= startMs) continue;
15145
+ const mid = (startMs + endMs) / 2;
15146
+ const pa = phaseAt(a, mid);
15147
+ const pb = phaseAt(b, mid);
15148
+ const phase = pa == null ? pb ?? "idle" : pb == null ? pa : pa === pb ? pa : "idle";
15149
+ const last = out[out.length - 1];
15150
+ if (last && last.phase === phase && last.endMs === startMs) last.endMs = endMs;
15151
+ else out.push({ phase, startMs, endMs });
15152
+ }
15153
+ return out;
15154
+ }
15155
+ function lastTMs(curves) {
15156
+ return Math.max(0, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0));
15157
+ }
15158
+ function DualGhostSpark({
15159
+ left,
15160
+ right,
15161
+ width,
15162
+ height = 232,
15163
+ leftLabel = "LEFT",
15164
+ rightLabel = "RIGHT",
15165
+ showDeviceLabels = true,
15166
+ targetTempoSeconds = null
15167
+ }) {
15168
+ const w = width;
15169
+ const h = height;
15170
+ const padL = 14;
15171
+ const padR = 14;
15172
+ const padTop = 22;
15173
+ const padBot = 22;
15174
+ if (left.length === 0 && right.length === 0) {
15175
+ return /* @__PURE__ */ jsx(View, { testID: "dual-ghost-spark", style: { width: w, height: h } });
15176
+ }
15177
+ const mid = padTop + (h - padTop - padBot) / 2;
15178
+ const bandTop = mid - BAND_H / 2;
15179
+ const baseUp = bandTop - BAND_GAP;
15180
+ const baseDown = mid + BAND_H / 2 + BAND_GAP;
15181
+ const wingH = Math.max(1, Math.min(baseUp - padTop, h - padBot - baseDown));
15182
+ const allSamples = [...left, ...right].flatMap((c) => c.samples);
15183
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
15184
+ const axisMaxT = Math.max(1, lastTMs(left), lastTMs(right)) * 1.04;
15185
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
15186
+ const mag = (v) => clamp012(v / vmax) * wingH;
15187
+ const toPts = (c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
15188
+ const wing = (curves) => {
15189
+ const cur = curves[curves.length - 1];
15190
+ return {
15191
+ cur,
15192
+ current: cur ? toPts(cur) : [],
15193
+ ghosts: curves.slice(0, -1).map(toPts),
15194
+ tint: cur ? ghostLineColor(cur.tempoDeviation, cur.grindSignature) : t25["text-tertiary"]
15195
+ };
15196
+ };
15197
+ const up = wing(left);
15198
+ const down = wing(right);
15199
+ const segments = mergePhaseSegments(up.cur?.phaseSegments ?? [], down.cur?.phaseSegments ?? []);
15200
+ return /* @__PURE__ */ jsx(View, { testID: "dual-ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
15201
+ up.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-left", children: /* @__PURE__ */ jsx(
15202
+ GhostBloom,
15203
+ {
15204
+ current: up.current,
15205
+ ghosts: up.ghosts,
15206
+ tint: up.tint,
15207
+ baseline: baseUp,
15208
+ orientation: "up"
15209
+ }
15210
+ ) }),
15211
+ down.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-right", children: /* @__PURE__ */ jsx(
15212
+ GhostBloom,
15213
+ {
15214
+ current: down.current,
15215
+ ghosts: down.ghosts,
15216
+ tint: down.tint,
15217
+ baseline: baseDown,
15218
+ orientation: "down"
15219
+ }
15220
+ ) }),
15221
+ /* @__PURE__ */ jsx(
15222
+ GhostBand,
15223
+ {
15224
+ segments,
15225
+ x,
15226
+ top: bandTop,
15227
+ showLabels: true,
15228
+ labelColor: alpha(t25["text-primary"], 0.92),
15229
+ targetTempoSeconds
15230
+ }
15231
+ ),
15232
+ showDeviceLabels && /* @__PURE__ */ jsxs(Fragment, { children: [
15233
+ /* @__PURE__ */ jsx(
15234
+ "text",
15235
+ {
15236
+ x: padL,
15237
+ y: padTop - 9,
15238
+ fontSize: 9,
15239
+ fontWeight: 800,
15240
+ letterSpacing: 1,
15241
+ fontFamily: FONT_UI,
15242
+ fill: t25["text-tertiary"],
15243
+ children: leftLabel
15244
+ }
15245
+ ),
15246
+ /* @__PURE__ */ jsx(
15247
+ "text",
15248
+ {
15249
+ x: padL,
15250
+ y: h - padBot + 15,
15251
+ fontSize: 9,
15252
+ fontWeight: 800,
15253
+ letterSpacing: 1,
15254
+ fontFamily: FONT_UI,
15255
+ fill: t25["text-tertiary"],
15256
+ children: rightLabel
15257
+ }
15258
+ )
15259
+ ] })
15260
+ ] }) });
15261
+ }
13792
15262
  var PALETTE = DATAVIZ_CATEGORICAL_PALETTE;
13793
15263
  function scaleValue(v, scale) {
13794
15264
  if (v <= 0) return 0;
@@ -13877,7 +15347,7 @@ function Treemap({
13877
15347
  const rects = squarify(weighted, width, height);
13878
15348
  return /* @__PURE__ */ jsx(View, { className: cn("relative", className), style: { width, height }, ...props, children: rects.map((r) => {
13879
15349
  const selected = r.datum.id === selectedId;
13880
- const labelFits = r.w > 54 && r.h > 22;
15350
+ const labelFits2 = r.w > 54 && r.h > 22;
13881
15351
  const pressable = r.datum.id !== "__more__";
13882
15352
  return /* @__PURE__ */ jsx(
13883
15353
  Pressable,
@@ -13899,7 +15369,7 @@ function Treemap({
13899
15369
  padding: 4,
13900
15370
  overflow: "hidden"
13901
15371
  },
13902
- children: labelFits && /* @__PURE__ */ jsx(Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
15372
+ children: labelFits2 && /* @__PURE__ */ jsx(Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
13903
15373
  },
13904
15374
  r.datum.id
13905
15375
  );
@@ -13995,28 +15465,28 @@ function Scatter({
13995
15465
  testID: "scatter-canvas",
13996
15466
  style: { position: "absolute", top: 0, left: 0, width, height, overflow: "hidden" },
13997
15467
  children: [
13998
- ticksOf(yd, TICK_COUNT).map((t16, i) => {
13999
- const y = toY(t16);
15468
+ ticksOf(yd, TICK_COUNT).map((t26, i) => {
15469
+ const y = toY(t26);
14000
15470
  return /* @__PURE__ */ jsx(
14001
15471
  View,
14002
15472
  {
14003
15473
  accessibilityElementsHidden: true,
14004
15474
  testID: "scatter-gridline-y",
14005
15475
  style: { position: "absolute", left: PLOT_LEFT2, width: innerW, top: y, height: 1, backgroundColor: GRID_LINE2 },
14006
- children: /* @__PURE__ */ jsx(Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t16) })
15476
+ children: /* @__PURE__ */ jsx(Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t26) })
14007
15477
  },
14008
15478
  `y-${i}`
14009
15479
  );
14010
15480
  }),
14011
- ticksOf(xd, TICK_COUNT).map((t16, i) => {
14012
- const x = toX(t16);
15481
+ ticksOf(xd, TICK_COUNT).map((t26, i) => {
15482
+ const x = toX(t26);
14013
15483
  return /* @__PURE__ */ jsx(
14014
15484
  View,
14015
15485
  {
14016
15486
  accessibilityElementsHidden: true,
14017
15487
  testID: "scatter-gridline-x",
14018
15488
  style: { position: "absolute", top: PLOT_TOP, height: innerH, left: x, width: 1, backgroundColor: GRID_LINE2 },
14019
- children: /* @__PURE__ */ jsx(Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t16) })
15489
+ children: /* @__PURE__ */ jsx(Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t26) })
14020
15490
  },
14021
15491
  `x-${i}`
14022
15492
  );
@@ -14089,7 +15559,7 @@ var DEFAULT_THRESHOLDS2 = [
14089
15559
  var ARC_START = 135;
14090
15560
  var ARC_SWEEP = 270;
14091
15561
  var SEGMENTS = 40;
14092
- function clamp012(n) {
15562
+ function clamp013(n) {
14093
15563
  if (Number.isNaN(n)) return 0;
14094
15564
  return Math.max(0, Math.min(1, n));
14095
15565
  }
@@ -14134,7 +15604,7 @@ function Gauge2({
14134
15604
  ...props
14135
15605
  }) {
14136
15606
  const span = max - min || 1;
14137
- const fill = clamp012((value - min) / span);
15607
+ const fill = clamp013((value - min) / span);
14138
15608
  const activeColor = (f) => color ?? bandColor2(f, thresholds, min, max);
14139
15609
  const ticks = buildTicks(fill, size, activeColor);
14140
15610
  const displayColor = color ?? bandColor2(fill, thresholds, min, max);
@@ -14151,23 +15621,23 @@ function Gauge2({
14151
15621
  testID: "gauge",
14152
15622
  ...props,
14153
15623
  children: [
14154
- ticks.map((t16) => /* @__PURE__ */ jsx(
15624
+ ticks.map((t26) => /* @__PURE__ */ jsx(
14155
15625
  View,
14156
15626
  {
14157
15627
  accessibilityElementsHidden: true,
14158
15628
  testID: "gauge-segment",
14159
15629
  style: {
14160
15630
  position: "absolute",
14161
- left: t16.left - tickThickness / 2,
14162
- top: t16.top - tickLength / 2,
15631
+ left: t26.left - tickThickness / 2,
15632
+ top: t26.top - tickLength / 2,
14163
15633
  width: tickThickness,
14164
15634
  height: tickLength,
14165
15635
  borderRadius: tickThickness / 2,
14166
- backgroundColor: t16.color,
14167
- transform: [{ rotate: t16.rotate }]
15636
+ backgroundColor: t26.color,
15637
+ transform: [{ rotate: t26.rotate }]
14168
15638
  }
14169
15639
  },
14170
- t16.key
15640
+ t26.key
14171
15641
  )),
14172
15642
  /* @__PURE__ */ jsxs(View, { style: { position: "absolute", top: 0, left: 0, width: size, height: size }, className: "items-center justify-center", children: [
14173
15643
  /* @__PURE__ */ jsxs(View, { className: "flex-row items-baseline gap-0.5", children: [
@@ -14587,6 +16057,6 @@ function composeValidators(...validators) {
14587
16057
  };
14588
16058
  }
14589
16059
 
14590
- export { Accordion, AccordionButton, AccordionItem, AccordionPanel, ActivityIcon, Alert, AlertDescription, AlertTitle, AlertTriangleIcon, Autocomplete, Avatar, AvatarBadge, AvatarGroup, AwardIcon, Badge, BadgeText, BaseBadge, BluetoothIcon, BrandLockup, BreadcrumbItem, Breadcrumbs, Button, ButtonIcon, ButtonSpinner, ButtonText, CATEGORICAL_CVD_SAFE_MAX, CapacityBandChart, Caption, Card, CardContent, CardDescription, CardFooter, CardHeader, CardSkeleton, CardTitle, Checkbox, CheckboxGroup, Chip, CircleSlashIcon, CircularProgress, CircularTimer, Collapse, CollapseButton, CollapseContent, DashboardShell, DataRow, DateTime, DeviationBar, DeviceIndicator, DeviceMenu, DeviceRow, Divider, Drawer, DrawerBody, DrawerFooter, DrawerHeader, DumbbellIcon, EmptyState, ExerciseCard, ExerciseCardHeading, ExerciseHeading, ExerciseIndicator, FatigueMeter, FormActions, FormField, FormRow, FormSection, Gauge2 as Gauge, HStack, Heading, HelpTip, HistoryIcon, INDICATOR_PRECEDENCE, IconBox, Indicator, InfoIcon, Input, InputBar, InputGroup, IntensityBar, Label, LabelWithHelp, LayersIcon, Link, ListItem, ListItemContent, ListItemDivider, ListItemIcon, ListItemTrailing, LiveAuraFrame, METRIC_FONT, Menu, MenuDivider, MenuGroup, MenuItem, MenuList, MenuTrigger, MesoCard, MesoProgressBar, MesoStatusCard, Metric, MetricCell, MetricGroup, MetricTiles, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalTitle, MuscleGroupChip, NavItem, Overline, Paragraph, PasswordInput, PersonStandingIcon, Pill, PlaceholderStrip, Popover, PopoverCloseButton, PopoverContent, PopoverTrigger, PrBadge, PrHistoryModal, Progress, ProgressSteps, Radio, RadioGroup, ReadinessCheck, RestTimer, SET_STRIP_VARIABLE_COLOR, SET_STRIP_ZONES, SURFACE_LEVEL_TOKEN, ScaleIcon, Scatter, ScheduleTiles, Section, SectionContent, SectionHeader, SegmentedBar, SegmentedProgressBar, Select, SessionHeader, SessionRail, SessionStatePill, SetBar, SetRow, SetStrip, SetTableHeader, SetsRepsLoad, SideNav, Sidebar, SidebarContent, SidebarDivider, SidebarFooter, SidebarHeader, SidebarItem, SidebarSection, Skeleton, SkeletonCard, SkeletonCircle, SkeletonListItem, SkeletonText, Sparkline, Spinner, Stack, StarIcon, StatusDot, StatusPill2 as StatusPill, StepContent, StepIndicator, StepLabel, Stepper, Step as StepperStep, StrengthTrendChart, SupersetWrapper, Surface, SurfaceContext, SvgIcon, Switch, Tab, TabList, TabPanel, TabPanels, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TablePagination, TableRow, Tabs, TempoDisplay, Tile, TimerReadout, Toast, ToastProvider, ToolbarButton, ToolbarButtonGroup, Tooltip, TopBar, Treemap, TrendingDownIcon, Typography, VStack, VelocityStrip, VoltrasMark, VolumeLandmarkBar, WeekRow, WeightBadge, WorkoutCard, WorkoutPill, ZoneTrack, alpha, applyThemePreset, audiobookPreset, bestTextColor, buttonSizes, calculateMeanVelocity, calculateVelocityLoss, categoricalPalette, cn, componentElevationRanges, composeValidators, createFieldId, createFlatShadow, createPressedHoverShadow, createPressedShadow, createRaisedHoverShadow, createRaisedShadow, darkThemeCSSVars, darken, defaultNavItems, defaultPreset, discreteRainbow, divergingScale, elevationSystem, formatDateTime, formatDuration, formatPrescription, formatSignedPct, formatVelocity, getBaseSurfaceColor, getCategoricalColor, getContrastText, getDiscreteRainbowColor, getElevationConfig, getElevationShadow, getElevationSurface, getFieldAriaProps, getFieldValidationProps, getGlowShadow, getHoverColors, getLuminance, getResultColor, getSemanticColors, getStatusColor, getThemeCSSVars, getValidatedElevation, getVelocityZoneColor, getVelocityZoneName, gluestackConfig, hasMaxLength, hasMinLength, hexToRgb, hsvToRgb, isCssPropertyManifest, isDark, isEmpty, isValidEmail, lightThemeCSSVars, lighten, linearGradient, liveAuraColor, neumorphicShadows, onSurfaceColors, paceTone, paceToneColor, primitiveBorderRadius, primitiveBreakpoints, primitiveColors, primitiveRamps, primitiveShadows, primitiveSpacing, primitiveTypography, primitiveZIndex, resolveColor, resolveIndicator, rgbToHex, rgbToHsv, roundRpe, roundTempo, roundWeight, rpeColor, semanticColorsDark, semanticColorsLight, semanticTypography, sequentialEffort, shadowColors, statusPillColor, surfaceBackground, surfaceGradient, useOnSurfaceColor, useSurface, useSurfaceMode, useTable, useTimer, useToast, useToolbarButton, validateCssPropertyManifest, validationRules, velocityZoneColor };
16060
+ export { Accordion, AccordionButton, AccordionItem, AccordionPanel, ActivityIcon, Alert, AlertDescription, AlertTitle, AlertTriangleIcon, Autocomplete, Avatar, AvatarBadge, AvatarGroup, AwardIcon, BAND_GAP, BAND_H, Badge, BadgeText, BaseBadge, BluetoothIcon, BrandLockup, BreadcrumbItem, Breadcrumbs, Button, ButtonIcon, ButtonSpinner, ButtonText, CATEGORICAL_CVD_SAFE_MAX, CapacityBandChart, Caption, Card, CardContent, CardDescription, CardFooter, CardHeader, CardSkeleton, CardTitle, Checkbox, CheckboxGroup, Chip, CircleSlashIcon, CircularProgress, CircularTimer, Collapse, CollapseButton, CollapseContent, DRIFT_GREY, DashboardShell, DataRow, DateTime, DeviationBar, DeviceIndicator, DeviceMenu, DeviceRow, Divider, Drawer, DrawerBody, DrawerFooter, DrawerHeader, DualGhostSpark, DualVelocityStrip, DumbbellIcon, EmptyState, ExerciseCard, ExerciseCardHeading, ExerciseHeading, ExerciseIndicator, FatigueLights, FatigueMeter, FormActions, FormField, FormRow, FormSection, GRIND_THRESHOLD, Gauge2 as Gauge, GhostBand, GhostBloom, GhostSpark, HStack, Heading, HelpTip, HistoryIcon, INDICATOR_PRECEDENCE, IconBox, Indicator, InfoIcon, Input, InputBar, InputGroup, IntensityBar, Label, LabelWithHelp, LayersIcon, Link, ListItem, ListItemContent, ListItemDivider, ListItemIcon, ListItemTrailing, LiveAuraFrame, LiveFatigueCard, LiveFatiguePanel, METRIC_FONT, Menu, MenuDivider, MenuGroup, MenuItem, MenuList, MenuTrigger, MesoCard, MesoProgressBar, MesoStatusCard, Metric, MetricCell, MetricGroup, MetricTiles, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalTitle, MuscleGroupChip, NavItem, Overline, PHASE_AXIS_COLOR, PRESSED_ELEVATION_LEVEL, Paragraph, PasswordInput, PersonStandingIcon, Pill, PlaceholderStrip, Popover, PopoverCloseButton, PopoverContent, PopoverTrigger, PrBadge, PrHistoryModal, Progress, ProgressSteps, RED_DEEP, RED_LIGHT, RED_MID, Radio, RadioGroup, ReadinessCheck, RestTimer, RomProgressionChart, SET_STRIP_VARIABLE_COLOR, SET_STRIP_ZONES, SILVER, STATE_LABEL, SURFACE_LEVEL_TOKEN, ScaleIcon, Scatter, ScheduleTiles, Section, SectionContent, SectionHeader, SegmentedBar, SegmentedProgressBar, Select, SessionHeader, SessionRail, SessionStatePill, SetBar, SetRow, SetStrip, SetTableHeader, SetsRepsLoad, SideNav, Sidebar, SidebarContent, SidebarDivider, SidebarFooter, SidebarHeader, SidebarItem, SidebarSection, Skeleton, SkeletonCard, SkeletonCircle, SkeletonListItem, SkeletonText, Sparkline, Spinner, Stack, StarIcon, StatusDot, StatusPill2 as StatusPill, StepContent, StepIndicator, StepLabel, Stepper, Step as StepperStep, StrengthTrendChart, SupersetWrapper, Surface, SurfaceContext, SvgIcon, Switch, TONE_COLOR, Tab, TabList, TabPanel, TabPanels, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TablePagination, TableRow, Tabs, TempoDisplay, Tile, TimerReadout, Toast, ToastProvider, ToolbarButton, ToolbarButtonGroup, Tooltip, TopBar, Treemap, TrendingDownIcon, Typography, VStack, VelocityHero, VelocityStrip, VerdictHero, VoltrasMark, VolumeLandmarkBar, WeekRow, WeightBadge, WorkoutCard, WorkoutPill, ZoneTrack, alpha, applyThemePreset, audiobookPreset, auraForVerdict, backgroundFrameDark, bestTextColor, buttonSizes, calculateMeanVelocity, calculateVelocityLoss, categoricalPalette, cn, componentElevationRanges, composeValidators, createFieldId, createFlatShadow, createPressedHoverShadow, createPressedShadow, createRaisedHoverShadow, createRaisedShadow, darkThemeCSSVars, darken, defaultNavItems, defaultPreset, discreteRainbow, divergingScale, elevationSystem, formatDateTime, formatDuration, formatPrescription, formatSignedPct, formatVelocity, getBaseSurfaceColor, getCategoricalColor, getContrastText, getDiscreteRainbowColor, getElevationConfig, getElevationShadow, getElevationSurface, getFieldAriaProps, getFieldValidationProps, getGlowShadow, getHoverColors, getLuminance, getPressedRecessShadow, getResultColor, getSemanticColors, getStatusColor, getThemeCSSVars, getValidatedElevation, getVelocityZoneColor, getVelocityZoneName, ghostLineColor, gluestackConfig, hasMaxLength, hasMinLength, hexToRgb, hsvToRgb, isCssPropertyManifest, isDark, isEmpty, isValidEmail, lightThemeCSSVars, lighten, linearGradient, liveAuraColor, mergePhaseSegments, neumorphicShadows, onSurfaceColors, paceTone, paceToneColor, pressedLevel, primitiveBorderRadius, primitiveBreakpoints, primitiveColors, primitiveRamps, primitiveShadows, primitiveSpacing, primitiveTypography, primitiveZIndex, resolveColor, resolveIndicator, rgbToHex, rgbToHsv, roundRpe, roundTempo, roundWeight, rpeColor, semanticColorsDark, semanticColorsLight, semanticTypography, sequentialEffort, shadowColors, smoothPath, statusPillColor, surfaceBackground, surfaceGradient, surfaceRampDark, useOnSurfaceColor, useSurface, useSurfaceMode, useTable, useTimer, useToast, useToolbarButton, validateCssPropertyManifest, validationRules, velocityZoneColor };
14591
16061
  //# sourceMappingURL=index.mjs.map
14592
16062
  //# sourceMappingURL=index.mjs.map