@titan-design/react-ui 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/index.d.mts +399 -4
  2. package/dist/index.d.ts +399 -4
  3. package/dist/index.js +2295 -895
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2279 -905
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  8. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +1974 -1740
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +1975 -1741
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +8 -3
  16. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  17. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  18. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  19. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  20. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  21. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  22. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  23. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  24. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  25. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  26. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  27. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  28. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  29. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  30. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  31. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  33. package/src/components/custom/Fatigue/README.md +93 -0
  34. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  35. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  36. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  37. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  38. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  39. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  40. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  41. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  42. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  43. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  44. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  45. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  46. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  47. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  48. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  49. package/src/components/custom/Fatigue/index.ts +41 -0
  50. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  51. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  52. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  56. package/src/components/custom/Workout/README.md +62 -16
  57. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  58. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  59. package/src/components/custom/Workout/SessionRail.tsx +50 -13
  60. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  61. package/src/components/custom/Workout/SetRow.tsx +9 -2
  62. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  63. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  64. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  65. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  66. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  67. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  68. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  69. package/src/components/custom/Workout/index.ts +3 -0
  70. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  71. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  72. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  73. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  74. package/src/components/custom/index.ts +1 -0
  75. package/src/test/raw-color-patterns.test.ts +71 -0
  76. package/src/theme/ColorPalettes.stories.tsx +871 -0
  77. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  78. package/src/theme/bar-paper.ts +59 -0
  79. package/src/theme/color-stories.coverage.test.ts +130 -0
  80. package/src/theme/color-story-kit.tsx +170 -0
  81. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  82. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  83. package/src/theme/ColorSystem.stories.tsx +0 -508
  84. package/src/theme/Colors.stories.tsx +0 -323
package/dist/index.js CHANGED
@@ -5379,7 +5379,7 @@ function ToastProvider({
5379
5379
  return id;
5380
5380
  }, [defaultDuration, maxToasts]);
5381
5381
  const removeToast = React25.useCallback((id) => {
5382
- setToasts((prev) => prev.filter((t17) => t17.id !== id));
5382
+ setToasts((prev) => prev.filter((t26) => t26.id !== id));
5383
5383
  }, []);
5384
5384
  const removeAllToasts = React25.useCallback(() => {
5385
5385
  setToasts([]);
@@ -8667,257 +8667,412 @@ function WorkoutPill({
8667
8667
  }
8668
8668
  return pill;
8669
8669
  }
8670
- var PULSE_HALF_MS = 950;
8671
- var PULSE_MIN_OPACITY = 0.45;
8672
- var SEGMENTED_BAR_GAP = 5;
8673
- function usePulse2(active) {
8674
- const [value] = React25.useState(() => new reactNative.Animated.Value(0));
8670
+
8671
+ // src/theme/bar-paper.ts
8672
+ function grainOpacityForColor(hex) {
8673
+ const h = hex.replace("#", "");
8674
+ const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
8675
+ const r = parseInt(full.slice(0, 2), 16);
8676
+ const g = parseInt(full.slice(2, 4), 16);
8677
+ const b = parseInt(full.slice(4, 6), 16);
8678
+ if ([r, g, b].some(Number.isNaN)) return 0.14;
8679
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
8680
+ return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
8681
+ }
8682
+ function barGrain(color) {
8683
+ const op = grainOpacityForColor(color).toFixed(3);
8684
+ return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='${op}'/%3E%3C/svg%3E")`;
8685
+ }
8686
+ function barPaper(color, flip = false) {
8687
+ return {
8688
+ backgroundImage: barGrain(color),
8689
+ boxShadow: flip ? "0 -6px 16px rgba(0,0,0,0.45)" : "inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)"
8690
+ };
8691
+ }
8692
+ var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
8693
+ var PEAK_OVERSHOOT = 1.12;
8694
+ function getReducedMotionPreference() {
8695
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8696
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8697
+ }
8698
+ function usePrefersReducedMotion() {
8699
+ const [reduced, setReduced] = React25.useState(getReducedMotionPreference);
8675
8700
  React25.useEffect(() => {
8676
- if (!active) return;
8677
- const loop = reactNative.Animated.loop(
8701
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
8702
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
8703
+ const handler = () => setReduced(mq.matches);
8704
+ handler();
8705
+ mq.addEventListener?.("change", handler);
8706
+ return () => mq.removeEventListener?.("change", handler);
8707
+ }, []);
8708
+ return reduced;
8709
+ }
8710
+ function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
8711
+ const prefersReducedMotion = usePrefersReducedMotion();
8712
+ const [liveGrowth] = React25.useState(() => new reactNative.Animated.Value(1));
8713
+ React25.useEffect(() => {
8714
+ if (liveRepIndex == null || liveVelocity == null) return;
8715
+ if (prefersReducedMotion) {
8716
+ liveGrowth.setValue(1);
8717
+ return;
8718
+ }
8719
+ liveGrowth.setValue(0);
8720
+ if (isNewPeak) {
8678
8721
  reactNative.Animated.sequence([
8679
- reactNative.Animated.timing(value, {
8680
- toValue: 1,
8681
- duration: PULSE_HALF_MS,
8682
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8722
+ reactNative.Animated.timing(liveGrowth, {
8723
+ toValue: PEAK_OVERSHOOT,
8724
+ duration: 220,
8725
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
8683
8726
  useNativeDriver: false
8684
8727
  }),
8685
- reactNative.Animated.timing(value, {
8686
- toValue: 0,
8687
- duration: PULSE_HALF_MS,
8688
- easing: reactNative.Easing.inOut(reactNative.Easing.ease),
8728
+ reactNative.Animated.spring(liveGrowth, {
8729
+ toValue: 1,
8730
+ friction: 5,
8731
+ tension: 140,
8689
8732
  useNativeDriver: false
8690
8733
  })
8691
- ])
8734
+ ]).start();
8735
+ } else {
8736
+ reactNative.Animated.timing(liveGrowth, {
8737
+ toValue: 1,
8738
+ duration: 300,
8739
+ easing: ANIMATION_EASING,
8740
+ useNativeDriver: false
8741
+ }).start();
8742
+ }
8743
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
8744
+ return liveGrowth;
8745
+ }
8746
+
8747
+ // src/components/custom/charts/SetBarChart.tsx
8748
+ function mixHex(a, b, t26) {
8749
+ const parse = (h) => {
8750
+ const s = h.replace("#", "");
8751
+ return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
8752
+ };
8753
+ const [ar, ag, ab] = parse(a);
8754
+ const [br, bg, bb] = parse(b);
8755
+ const ch = (x, y) => Math.round(x + (y - x) * t26).toString(16).padStart(2, "0");
8756
+ return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
8757
+ }
8758
+ var VARIABLE_FILL = primitiveRamps.cyan[900];
8759
+ var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8760
+ var FIXED_MAX_VALUE = 1.15;
8761
+ var PEAK_HEADROOM = 1.03;
8762
+ function scaleDenominator(scale, maxValue) {
8763
+ return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
8764
+ }
8765
+ function valueLabelFontSize(height) {
8766
+ return Math.round(Math.max(8, Math.min(12, height * 0.11)));
8767
+ }
8768
+ var SET_BAR_DEFAULT_HEIGHT = 220;
8769
+ var LABEL_ROW_HEIGHT = 16;
8770
+ var LABEL_GAP = 3;
8771
+ var GAP_RATIO = 0.08;
8772
+ var MIN_BAR_GAP = 2;
8773
+ var CHUNK_NOTCH_RATIO = 0.25;
8774
+ var CHUNK_NOTCH_MIN_PX = 10;
8775
+ var BAR_MAX_WIDTH = 120;
8776
+ var LABEL_MIN_BAR_WIDTH = 30;
8777
+ var DEFAULT_BAR_RADIUS = 5;
8778
+ var MIN_BAR_HEIGHT = 4;
8779
+ var FLAT_BAR_FRACTION = 0.5;
8780
+ var STUB_HEIGHT = 9;
8781
+ var REP_GAP = 2;
8782
+ function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
8783
+ const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
8784
+ const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
8785
+ const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
8786
+ return { barWidth, gap };
8787
+ }
8788
+ function isRep(slot) {
8789
+ return slot.kind === "rep";
8790
+ }
8791
+ function repIndexByCell(cells) {
8792
+ let count = 0;
8793
+ return cells.map((slot) => isRep(slot) ? count++ : -1);
8794
+ }
8795
+ function SetBarChart({
8796
+ slots,
8797
+ colorFor,
8798
+ height,
8799
+ scale = "peak",
8800
+ scaleMax,
8801
+ orientation = "up",
8802
+ liveRepIndex,
8803
+ isNewPeak = false,
8804
+ targetReps,
8805
+ gapRatio = GAP_RATIO,
8806
+ barRadius = DEFAULT_BAR_RADIUS,
8807
+ cornerStyle = "top",
8808
+ flat = false,
8809
+ showValueLabels: showValueLabelsProp = false,
8810
+ formatValue: formatValue3 = String,
8811
+ todoVariant = "solid",
8812
+ minColumns,
8813
+ renderReference,
8814
+ renderBarOverlay,
8815
+ expandProgress,
8816
+ hideBaseline = false,
8817
+ testID,
8818
+ testIDPrefix = "setbar",
8819
+ accessibilityLabel: accessibilityLabel2,
8820
+ label,
8821
+ className,
8822
+ viewProps = {}
8823
+ }) {
8824
+ const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
8825
+ const cells = targetPadded > slots.length ? [
8826
+ ...slots,
8827
+ ...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
8828
+ ] : slots;
8829
+ const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
8830
+ const best = repValues.length > 0 ? Math.max(...repValues) : 0;
8831
+ const flip = orientation === "down";
8832
+ const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
8833
+ const placeholderColor = useOnSurfaceColor("tertiary");
8834
+ const surface = useSurface();
8835
+ const surfaceBg = surfaceBackground(surface.level, surface.mode);
8836
+ const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
8837
+ const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
8838
+ const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
8839
+ const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
8840
+ const showValueLabels = showValueLabelsProp && !flat;
8841
+ const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
8842
+ const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
8843
+ const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
8844
+ const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
8845
+ const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
8846
+ const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
8847
+ const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
8848
+ const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
8849
+ const [plotW, setPlotW] = React25.useState(0);
8850
+ const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
8851
+ const totalCells = cells.length;
8852
+ const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
8853
+ const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
8854
+ const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
8855
+ const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
8856
+ const geometry = { scaleDenom, plotHeight, yOf, best, flip };
8857
+ const { style: externalStyle, ...restProps } = viewProps;
8858
+ const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
8859
+ const repIndices = repIndexByCell(cells);
8860
+ return /* @__PURE__ */ jsxs(
8861
+ reactNative.View,
8862
+ {
8863
+ className,
8864
+ style: [{ height, flexDirection: "row" }, externalStyle],
8865
+ testID,
8866
+ ...a11y,
8867
+ ...restProps,
8868
+ children: [
8869
+ label ? /* @__PURE__ */ jsx(
8870
+ ChartSideRail,
8871
+ {
8872
+ sections: [{ label, testID: `${testIDPrefix}-side-label` }],
8873
+ sectionExtent: height
8874
+ }
8875
+ ) : null,
8876
+ /* @__PURE__ */ jsxs(
8877
+ reactNative.View,
8878
+ {
8879
+ onLayout: onPlotLayout,
8880
+ style: {
8881
+ flex: 1,
8882
+ flexDirection: "row",
8883
+ alignItems: "flex-end",
8884
+ gap,
8885
+ position: "relative",
8886
+ borderBottomWidth: hideBaseline ? 0 : 2,
8887
+ borderBottomColor: placeholderColor,
8888
+ ...flipStyle
8889
+ },
8890
+ children: [
8891
+ renderReference?.(geometry),
8892
+ cells.map((slot, i) => {
8893
+ const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
8894
+ const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
8895
+ const column = {
8896
+ flex: 1,
8897
+ maxWidth: BAR_MAX_WIDTH,
8898
+ height: "100%",
8899
+ alignItems: "center",
8900
+ justifyContent: "flex-end",
8901
+ marginLeft: extraGap
8902
+ };
8903
+ if (!isRep(slot)) {
8904
+ return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
8905
+ slot.kind,
8906
+ barCorners,
8907
+ // Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
8908
+ flat ? compactBarHeight : STUB_HEIGHT,
8909
+ placeholderColor,
8910
+ testIDPrefix,
8911
+ todoVariant,
8912
+ solidTodoColor,
8913
+ emptyColor
8914
+ ) }, i);
8915
+ }
8916
+ const repIndex = repIndices[i];
8917
+ const value = slot.value ?? 0;
8918
+ const isLive = liveRepIndex === repIndex;
8919
+ const color = colorFor(value);
8920
+ const barHeightStyle = expandProgress ? expandProgress.interpolate({
8921
+ inputRange: [0, 1],
8922
+ outputRange: [flatBarHeight, valueBarHeight(value)]
8923
+ }) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
8924
+ const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
8925
+ const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
8926
+ return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
8927
+ ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
8928
+ // in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
8929
+ /* @__PURE__ */ jsx(
8930
+ reactNative.Animated.View,
8931
+ {
8932
+ style: {
8933
+ position: "absolute",
8934
+ bottom: labelBottom,
8935
+ left: 0,
8936
+ right: 0,
8937
+ alignItems: "center",
8938
+ ...expandProgress ? { opacity: expandProgress } : null
8939
+ },
8940
+ pointerEvents: "none",
8941
+ children: /* @__PURE__ */ jsx(
8942
+ reactNative.Text,
8943
+ {
8944
+ className: "text-text-secondary",
8945
+ style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
8946
+ testID: `${testIDPrefix}-label-${repIndex}`,
8947
+ children: formatValue3(value)
8948
+ }
8949
+ )
8950
+ }
8951
+ ),
8952
+ /* @__PURE__ */ jsx(
8953
+ reactNative.Animated.View,
8954
+ {
8955
+ style: [
8956
+ { width: "100%", height: barHeightStyle, backgroundColor: color },
8957
+ barCorners,
8958
+ barPaper(color, flip)
8959
+ ],
8960
+ testID: `${testIDPrefix}-bar-${repIndex}`
8961
+ }
8962
+ ),
8963
+ renderBarOverlay?.(repIndex, value)
8964
+ ] }, i);
8965
+ })
8966
+ ]
8967
+ }
8968
+ )
8969
+ ]
8970
+ }
8971
+ );
8972
+ }
8973
+ function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
8974
+ const base = {
8975
+ width: "100%",
8976
+ height: stubHeight,
8977
+ ...barCorners
8978
+ };
8979
+ if (kind === "variable") {
8980
+ return /* @__PURE__ */ jsx(
8981
+ reactNative.View,
8982
+ {
8983
+ style: { ...base, backgroundColor: VARIABLE_FILL },
8984
+ testID: `${testIDPrefix}-slot-variable`
8985
+ }
8692
8986
  );
8693
- loop.start();
8694
- return () => loop.stop();
8695
- }, [active, value]);
8696
- return value;
8987
+ }
8988
+ if (kind === "continue") {
8989
+ return /* @__PURE__ */ jsx(
8990
+ reactNative.View,
8991
+ {
8992
+ style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
8993
+ testID: `${testIDPrefix}-slot-continue`
8994
+ }
8995
+ );
8996
+ }
8997
+ if (kind === "empty") {
8998
+ return /* @__PURE__ */ jsx(
8999
+ reactNative.View,
9000
+ {
9001
+ style: { ...base, backgroundColor: emptyColor },
9002
+ testID: `${testIDPrefix}-slot-empty`
9003
+ }
9004
+ );
9005
+ }
9006
+ const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
9007
+ return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
8697
9008
  }
8698
- function SegmentedBar({
8699
- segments,
8700
- height = 8,
8701
- gap = SEGMENTED_BAR_GAP,
8702
- radius = 2,
8703
- marker = null,
8704
- segmentTestID,
8705
- style,
8706
- ...props
9009
+ function sideLabelFontSize(height) {
9010
+ return Math.round(Math.max(7, Math.min(13, height * 0.06)));
9011
+ }
9012
+ var SIDE_LABEL_INITIALS_BELOW = 70;
9013
+ function sideLabelText(label, extent) {
9014
+ if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
9015
+ return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
9016
+ }
9017
+ function ChartSideRail({
9018
+ sections,
9019
+ sectionExtent,
9020
+ variant = "hero"
8707
9021
  }) {
8708
- const pulse = usePulse2(segments.some((s) => s.pulse));
8709
- return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
8710
- segments.map((seg, i) => {
8711
- const fillStyle = {
8712
- width: `${(seg.fill ?? 1) * 100}%`,
8713
- height: "100%",
8714
- backgroundColor: seg.color,
8715
- opacity: seg.opacity
8716
- };
8717
- const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
8718
- return /* @__PURE__ */ jsx(
9022
+ if (!sections.some((s) => s.label)) return null;
9023
+ const isRail = variant === "rail";
9024
+ const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
9025
+ const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
9026
+ const textStyle = {
9027
+ maxWidth: "100%",
9028
+ textAlign: "center",
9029
+ fontSize,
9030
+ fontWeight: "700",
9031
+ letterSpacing: isRail || fontSize < 10 ? 0 : 2,
9032
+ textTransform: "uppercase"
9033
+ };
9034
+ return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
9035
+ reactNative.View,
9036
+ {
9037
+ style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
9038
+ children: s.label ? /* @__PURE__ */ jsx(
8719
9039
  reactNative.View,
8720
9040
  {
8721
9041
  style: {
8722
- flex: seg.weight ?? 1,
8723
- minWidth: 0,
8724
- height: "100%",
8725
- marginLeft: seg.leadingGap,
8726
- borderRadius: radius,
8727
- overflow: "hidden"
9042
+ width: labelLength,
9043
+ alignItems: "center",
9044
+ transform: [{ rotate: "-90deg" }]
8728
9045
  },
8729
- children: seg.pulse ? /* @__PURE__ */ jsx(
8730
- reactNative.Animated.View,
9046
+ children: /* @__PURE__ */ jsx(
9047
+ reactNative.Text,
8731
9048
  {
8732
- style: {
8733
- ...fillStyle,
8734
- ...seg.pulseColor ? {
8735
- backgroundColor: pulse.interpolate({
8736
- inputRange: [0, 1],
8737
- outputRange: seg.pulseColor
8738
- })
8739
- } : {
8740
- opacity: pulse.interpolate({
8741
- inputRange: [0, 1],
8742
- outputRange: [PULSE_MIN_OPACITY, 1]
8743
- })
8744
- }
8745
- },
8746
- accessibilityElementsHidden: true,
8747
- testID
9049
+ className: "text-text-tertiary",
9050
+ style: textStyle,
9051
+ numberOfLines: 1,
9052
+ testID: s.testID,
9053
+ children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
8748
9054
  }
8749
- ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
8750
- },
8751
- i
8752
- );
8753
- }),
8754
- marker ? /* @__PURE__ */ jsx(
8755
- reactNative.View,
8756
- {
8757
- style: {
8758
- position: "absolute",
8759
- left: `${marker.position * 100}%`,
8760
- top: 0,
8761
- bottom: 0,
8762
- width: 2,
8763
- backgroundColor: marker.color
8764
- },
8765
- accessibilityElementsHidden: true,
8766
- testID: "segmented-bar-marker"
8767
- }
8768
- ) : null
8769
- ] });
9055
+ )
9056
+ }
9057
+ ) : null
9058
+ },
9059
+ s.testID
9060
+ )) });
8770
9061
  }
8771
9062
 
8772
- // src/components/custom/Workout/SetBar.tsx
8773
- var SET_STRIP_ZONES = {
8774
- slow: primitiveRamps.red[600],
8775
- moderate: primitiveRamps.orange[400],
8776
- fast: primitiveRamps.amber[300],
8777
- fastest: primitiveRamps.green[300]
8778
- };
8779
- var TODO_COLOR = primitiveColors.charcoal[300];
8780
- var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
8781
- var NOTCH_GAP = 2;
8782
- var CLUSTER_GAP = 3;
8783
- var MYO_UPCOMING_CLUSTERS = 3;
8784
- var MYO_UPCOMING_CLUSTER_LEN = 5;
8785
- var MYO_UPCOMING_FADE = 0.6;
8786
- var PULSE_RANGE = {
8787
- [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
8788
- [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
8789
- [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
8790
- [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
8791
- };
8792
- function velocityZoneColor(v) {
8793
- if (v < 0.5) return SET_STRIP_ZONES.slow;
8794
- if (v < 0.75) return SET_STRIP_ZONES.moderate;
8795
- if (v < 1) return SET_STRIP_ZONES.fast;
8796
- return SET_STRIP_ZONES.fastest;
8797
- }
8798
- function chunkedSegments(chunks, gap) {
8799
- return chunks.flatMap(
8800
- (vels, ci) => vels.map((v, ri) => ({
8801
- color: velocityZoneColor(v),
8802
- ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
8803
- }))
8804
- );
8805
- }
8806
- function rangeSegments(floor, max, doneVels) {
8807
- return Array.from({ length: max }, (_, i) => {
8808
- if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
8809
- return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
8810
- });
8811
- }
8812
- function myoUpcomingSegments(activationLen, clusterCount) {
8813
- const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
8814
- const trail = Array.from(
8815
- { length: clusterCount },
8816
- (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
8817
- color: SET_STRIP_VARIABLE_COLOR,
8818
- opacity: MYO_UPCOMING_FADE ** ci,
8819
- ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
8820
- }))
8821
- ).flat();
8822
- return [...activation, ...trail];
8823
- }
8824
- function setSegments(set) {
8825
- if (set.status === "todo") {
8826
- return [{ color: TODO_COLOR }];
8827
- }
8828
- if (set.status === "done") {
8829
- return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
8830
- }
8831
- if (set.status === "range") {
8832
- return rangeSegments(set.floor, set.max, set.doneVels);
8833
- }
8834
- if (set.status === "drop") {
8835
- return chunkedSegments(set.subloads, NOTCH_GAP);
8836
- }
8837
- if (set.status === "myo") {
8838
- return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
8839
- }
8840
- if (set.status === "myo-upcoming") {
8841
- return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
8842
- }
8843
- const performed = set.velocities.map((v) => {
8844
- const color = velocityZoneColor(v);
8845
- return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
8846
- });
8847
- const remaining = Math.max(0, set.planned - set.velocities.length);
8848
- return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
8849
- }
8850
- function setBarSegmentTestID(seg) {
8851
- if (seg.pulse) return "set-strip-pulse";
8852
- if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
8853
- return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
8854
- }
8855
- function SetBar({ set, height = 8 }) {
8856
- return /* @__PURE__ */ jsx(
8857
- SegmentedBar,
8858
- {
8859
- segments: setSegments(set),
8860
- height,
8861
- gap: 0,
8862
- radius: 0,
8863
- segmentTestID: setBarSegmentTestID,
8864
- style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
8865
- testID: "set-strip-set"
8866
- }
8867
- );
8868
- }
8869
-
8870
- // src/components/custom/Workout/SetStrip.tsx
8871
- var SET_STRIP_GAP = 5;
8872
- function setCategory(set) {
8873
- switch (set.status) {
8874
- case "done":
8875
- case "drop":
8876
- case "myo":
8877
- return "done";
8878
- case "active":
8879
- return "active";
8880
- case "range":
8881
- return set.doneVels.length > 0 ? "active" : "upcoming";
8882
- case "todo":
8883
- case "myo-upcoming":
8884
- return "upcoming";
8885
- }
8886
- }
8887
- function describeSets(sets) {
8888
- const done = sets.filter((s) => setCategory(s) === "done").length;
8889
- const active = sets.filter((s) => setCategory(s) === "active").length;
8890
- const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
8891
- return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
8892
- }
8893
- function SetStrip({ sets, height = 8, className, ...props }) {
8894
- return /* @__PURE__ */ jsx(
8895
- reactNative.View,
8896
- {
8897
- className,
8898
- style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
8899
- accessibilityRole: "image",
8900
- accessibilityLabel: describeSets(sets),
8901
- testID: "set-strip",
8902
- ...props,
8903
- children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
8904
- }
8905
- );
8906
- }
8907
-
8908
- // src/components/custom/Workout/VelocityStrip.tsx
8909
- var VEL_COLORS = WORKOUT_TOKENS.scale;
8910
- var bandIdToEffortColor = {
8911
- speed: sequentialEffort[0],
8912
- // green-300
8913
- power: sequentialEffort[2],
8914
- // amber-300 (gold)
8915
- strengthSpeed: sequentialEffort[3],
8916
- // orange-400
8917
- maximalStrength: sequentialEffort[4],
8918
- // red-600
8919
- grinding: sequentialEffort[5]
8920
- // red-700 — the distinct 5th band
9063
+ // src/components/custom/Workout/VelocityStrip.tsx
9064
+ var VEL_COLORS = WORKOUT_TOKENS.scale;
9065
+ var bandIdToEffortColor = {
9066
+ speed: sequentialEffort[0],
9067
+ // green-300
9068
+ power: sequentialEffort[2],
9069
+ // amber-300 (gold)
9070
+ strengthSpeed: sequentialEffort[3],
9071
+ // orange-400
9072
+ maximalStrength: sequentialEffort[4],
9073
+ // red-600
9074
+ grinding: sequentialEffort[5]
9075
+ // red-700 the distinct 5th band
8921
9076
  };
8922
9077
  function getVelocityZoneColor(velocity) {
8923
9078
  if (velocity >= 1) return "vel-green";
@@ -8949,6 +9104,18 @@ function calculateMeanVelocity(velocities) {
8949
9104
  const sum = velocities.reduce((acc, v) => acc + v, 0);
8950
9105
  return sum / velocities.length;
8951
9106
  }
9107
+ var VL_LOSS_THRESHOLDS = [10, 20, 30];
9108
+ function getVelocityLossColor(lossPct) {
9109
+ const [t1, t26, t32] = VL_LOSS_THRESHOLDS;
9110
+ if (lossPct < t1) return VEL_COLORS.green;
9111
+ if (lossPct < t26) return VEL_COLORS.yellow;
9112
+ if (lossPct < t32) return VEL_COLORS.orange;
9113
+ return VEL_COLORS.red;
9114
+ }
9115
+ function velocityLossForRep(velocity, best) {
9116
+ if (best <= 0) return 0;
9117
+ return Math.max(0, Math.round((best - velocity) / best * 100));
9118
+ }
8952
9119
  function classifyBand(velocity, bands) {
8953
9120
  for (const band of bands) {
8954
9121
  if (band.max === null || velocity < band.max) return band;
@@ -8958,15 +9125,17 @@ function classifyBand(velocity, bands) {
8958
9125
  function bandColor(band) {
8959
9126
  return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
8960
9127
  }
9128
+ function makeBarColorFor(zones) {
9129
+ const hasZones = zones != null && zones.length > 0;
9130
+ return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9131
+ }
8961
9132
  function getLossStyle(loss) {
8962
9133
  if (loss > 25) return { color: VEL_COLORS.red };
8963
9134
  if (loss > 20) return { color: VEL_COLORS.orange };
8964
9135
  return null;
8965
9136
  }
8966
- var REP_GAP = 2;
9137
+ var REP_GAP2 = 2;
8967
9138
  var WIDE_GAP = 8;
8968
- var TODO_COLOR2 = primitiveColors.charcoal[300];
8969
- var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
8970
9139
  function deriveDoneVelocities(set) {
8971
9140
  switch (set.type) {
8972
9141
  case "drop":
@@ -8982,7 +9151,7 @@ function chunkedRepSlots(chunks) {
8982
9151
  chunks.forEach((chunk) => {
8983
9152
  chunk.forEach((velocity, ri) => {
8984
9153
  const first = slots.length === 0;
8985
- slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
9154
+ slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
8986
9155
  });
8987
9156
  });
8988
9157
  return slots;
@@ -8996,7 +9165,7 @@ function buildSlots(set) {
8996
9165
  return {
8997
9166
  kind: done ? "rep" : "todo",
8998
9167
  velocity: done ? set.velocities[i] : void 0,
8999
- leadingGap: i === 0 ? 0 : REP_GAP
9168
+ leadingGap: i === 0 ? 0 : REP_GAP2
9000
9169
  };
9001
9170
  });
9002
9171
  }
@@ -9007,7 +9176,7 @@ function buildSlots(set) {
9007
9176
  return {
9008
9177
  kind,
9009
9178
  velocity: kind === "rep" ? set.velocities[i] : void 0,
9010
- leadingGap: i === 0 ? 0 : REP_GAP
9179
+ leadingGap: i === 0 ? 0 : REP_GAP2
9011
9180
  };
9012
9181
  });
9013
9182
  }
@@ -9015,16 +9184,16 @@ function buildSlots(set) {
9015
9184
  const reps = set.velocities.map((velocity, i) => ({
9016
9185
  kind: "rep",
9017
9186
  velocity,
9018
- leadingGap: i === 0 ? 0 : REP_GAP
9187
+ leadingGap: i === 0 ? 0 : REP_GAP2
9019
9188
  }));
9020
- reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
9189
+ reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
9021
9190
  return reps;
9022
9191
  }
9023
9192
  case "drop":
9024
9193
  return chunkedRepSlots(set.subloads);
9025
9194
  case "myo": {
9026
9195
  const slots = chunkedRepSlots([set.activation, ...set.clusters]);
9027
- if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
9196
+ if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
9028
9197
  return slots;
9029
9198
  }
9030
9199
  case "cluster": {
@@ -9035,7 +9204,7 @@ function buildSlots(set) {
9035
9204
  return {
9036
9205
  kind: done ? "rep" : "todo",
9037
9206
  velocity: done ? set.velocities[i] : void 0,
9038
- leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
9207
+ leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
9039
9208
  };
9040
9209
  });
9041
9210
  }
@@ -9057,253 +9226,406 @@ function setAccessibilityLabel(set, repCount) {
9057
9226
  return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
9058
9227
  }
9059
9228
  }
9060
- function slotAccessibilityLabel(slot, index) {
9061
- switch (slot.kind) {
9062
- case "rep":
9063
- return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
9064
- case "todo":
9065
- return `Rep ${index + 1}: planned`;
9066
- case "variable":
9067
- return `Rep ${index + 1}: variable`;
9068
- case "continue":
9069
- return "Keep going";
9070
- }
9071
- }
9072
- var EXPANDED_TODO_STUB_PCT = 16;
9073
- var EXPANDED_ENCODED_PCT = 45;
9074
9229
  var EXPANDED_HEIGHT = 60;
9075
- var FIXED_MAX_VELOCITY = 1.15;
9076
- var BARE_STUB_HEIGHT = 3;
9077
- var HERO_HEIGHT = 220;
9078
- var HERO_LABEL_HEADROOM = 20;
9079
- var HERO_BAR_GAP = 8;
9080
- var HERO_BAR_MAX_WIDTH = 120;
9081
- var HERO_LABEL_MIN_BAR_WIDTH = 30;
9082
- var HERO_BAR_RADIUS = 5;
9083
- var HERO_MIN_BAR_HEIGHT = 4;
9084
- var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
9230
+ var COMPACT_HEIGHT = 8;
9085
9231
  var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
9086
- var PEAK_HEADROOM = 1.06;
9087
- function scaleDenominator(scale, maxVelocity) {
9088
- return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
9089
- }
9090
- function bareSlotHeight(slot, height, denom) {
9091
- if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
9092
- const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
9093
- return Math.max(BARE_STUB_HEIGHT, ratio * height);
9094
- }
9095
- function getReducedMotionPreference() {
9096
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
9097
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
9098
- }
9099
- function usePrefersReducedMotion() {
9100
- const [reduced, setReduced] = React25.useState(getReducedMotionPreference);
9101
- React25.useEffect(() => {
9102
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
9103
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
9104
- const handler = () => setReduced(mq.matches);
9105
- handler();
9106
- mq.addEventListener?.("change", handler);
9107
- return () => mq.removeEventListener?.("change", handler);
9108
- }, []);
9109
- return reduced;
9232
+ function DashedReferenceLine({
9233
+ anchor,
9234
+ offset,
9235
+ testID
9236
+ }) {
9237
+ const edge = anchor === "top" ? { top: offset } : { bottom: offset };
9238
+ return /* @__PURE__ */ jsx(
9239
+ reactNative.View,
9240
+ {
9241
+ accessibilityElementsHidden: true,
9242
+ style: {
9243
+ position: "absolute",
9244
+ left: 0,
9245
+ right: 0,
9246
+ borderTopWidth: 1,
9247
+ borderStyle: "dashed",
9248
+ borderColor: HERO_REFERENCE_COLOR,
9249
+ pointerEvents: "none",
9250
+ ...edge
9251
+ },
9252
+ testID
9253
+ }
9254
+ );
9110
9255
  }
9111
- var ANIMATION_DURATION = 400;
9112
- var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
9113
- var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
9114
- function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
9115
- const prefersReducedMotion = usePrefersReducedMotion();
9116
- const [liveScale] = React25.useState(() => new reactNative.Animated.Value(1));
9117
- React25.useEffect(() => {
9118
- if (!active || liveRepIndex == null || liveVelocity == null) return;
9119
- if (prefersReducedMotion) {
9120
- liveScale.setValue(1);
9121
- return;
9256
+ var BAND_LABEL_FONT = "monospace";
9257
+ var VL_LABEL_MIN_PLOT = 65;
9258
+ var VL_SEMANTIC = getSemanticColors("dark");
9259
+ function VelocityLossBands({
9260
+ best,
9261
+ scaleDenom,
9262
+ plotHeight,
9263
+ flip = false
9264
+ }) {
9265
+ if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
9266
+ const yOf = (v) => v / scaleDenom * plotHeight;
9267
+ const vl20 = best * 0.8;
9268
+ const vl30 = best * 0.7;
9269
+ const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
9270
+ const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
9271
+ const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
9272
+ reactNative.View,
9273
+ {
9274
+ style: {
9275
+ position: "absolute",
9276
+ left: 0,
9277
+ right: 0,
9278
+ bottom: yOf(loV),
9279
+ height: Math.max(0, yOf(hiV) - yOf(loV)),
9280
+ backgroundColor: color
9281
+ }
9122
9282
  }
9123
- if (isNewPeak) {
9124
- liveScale.setValue(1);
9125
- reactNative.Animated.sequence([
9126
- reactNative.Animated.timing(liveScale, {
9127
- toValue: 1.25,
9128
- duration: 150,
9129
- easing: reactNative.Easing.out(reactNative.Easing.quad),
9130
- useNativeDriver: true
9131
- }),
9132
- reactNative.Animated.spring(liveScale, {
9133
- toValue: 1,
9134
- friction: 3,
9135
- tension: 140,
9136
- useNativeDriver: true
9137
- })
9138
- ]).start();
9139
- } else {
9140
- liveScale.setValue(0.8);
9141
- reactNative.Animated.timing(liveScale, {
9142
- toValue: 1,
9143
- duration: 300,
9144
- easing: POP_EASING,
9145
- useNativeDriver: true
9146
- }).start();
9283
+ );
9284
+ const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
9285
+ reactNative.View,
9286
+ {
9287
+ style: {
9288
+ position: "absolute",
9289
+ left: 0,
9290
+ right: 0,
9291
+ bottom: yOf(v),
9292
+ // Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
9293
+ // band edge — instead of floating up half a text-row (the label then centres on
9294
+ // the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
9295
+ height: 0,
9296
+ flexDirection: "row",
9297
+ alignItems: "center"
9298
+ },
9299
+ children: [
9300
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
9301
+ showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9302
+ /* @__PURE__ */ jsx(
9303
+ reactNative.Text,
9304
+ {
9305
+ style: {
9306
+ marginHorizontal: 6,
9307
+ fontSize: vlFont,
9308
+ fontWeight: "800",
9309
+ fontFamily: BAND_LABEL_FONT,
9310
+ color,
9311
+ ...flip ? { transform: [{ scaleY: -1 }] } : null
9312
+ },
9313
+ children: label
9314
+ }
9315
+ ),
9316
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
9317
+ ] }) : null
9318
+ ]
9319
+ }
9320
+ );
9321
+ return /* @__PURE__ */ jsxs(
9322
+ reactNative.View,
9323
+ {
9324
+ accessibilityElementsHidden: true,
9325
+ pointerEvents: "none",
9326
+ style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
9327
+ testID: "velocity-loss-bands",
9328
+ children: [
9329
+ band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
9330
+ band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
9331
+ threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
9332
+ threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
9333
+ ]
9147
9334
  }
9148
- }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
9149
- return liveScale;
9335
+ );
9150
9336
  }
9151
- function HeroVelocityChart({
9152
- doneVelocities,
9153
- barColorFor,
9154
- scaleDenom,
9155
- referenceVelocity,
9156
- liveRepIndex,
9157
- isNewPeak,
9337
+ function velocityReferenceOverlay(g, showLossBands) {
9338
+ const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
9339
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9340
+ showLossBands && /* @__PURE__ */ jsx(
9341
+ VelocityLossBands,
9342
+ {
9343
+ best: g.best,
9344
+ scaleDenom: g.scaleDenom,
9345
+ plotHeight: g.plotHeight,
9346
+ flip: g.flip
9347
+ }
9348
+ ),
9349
+ referencePx > 0 && /* @__PURE__ */ jsx(
9350
+ DashedReferenceLine,
9351
+ {
9352
+ anchor: "bottom",
9353
+ offset: referencePx,
9354
+ testID: "velocity-hero-reference"
9355
+ }
9356
+ )
9357
+ ] });
9358
+ }
9359
+ var ANIMATION_DURATION = 400;
9360
+ var DUAL_HERO_HEIGHT = SET_BAR_DEFAULT_HEIGHT;
9361
+ var DUAL_RAIL_HEIGHT = 96;
9362
+ var DUAL_COMPACT_HEIGHT = 11.5;
9363
+ var DUAL_WING_GAP = 2;
9364
+ var COMPACT_FOLD_GAP = 1.5;
9365
+ var COMPACT_FOLD_HALF = 5;
9366
+ function streamDone(stream) {
9367
+ return stream.set ? deriveDoneVelocities(stream.set) : stream.velocities ?? [];
9368
+ }
9369
+ function streamNaturalSlots(stream, targetReps) {
9370
+ if (stream.set) {
9371
+ return buildSlots(stream.set).map((s) => ({
9372
+ kind: s.kind,
9373
+ value: s.velocity,
9374
+ leadingGap: s.leadingGap
9375
+ }));
9376
+ }
9377
+ const reps = (stream.velocities ?? []).map((v, i) => ({
9378
+ kind: "rep",
9379
+ value: v,
9380
+ leadingGap: i === 0 ? 0 : REP_GAP2
9381
+ }));
9382
+ const pad = Math.max(0, (targetReps ?? 0) - reps.length);
9383
+ return [...reps, ...Array.from({ length: pad }, () => ({ kind: "todo" }))];
9384
+ }
9385
+ function mergeColumnKind(l, r) {
9386
+ const kinds = [l?.kind, r?.kind];
9387
+ if (kinds.includes("variable")) return "variable";
9388
+ if (kinds.includes("continue")) return "continue";
9389
+ if (kinds.includes("rep")) return "rep";
9390
+ return "todo";
9391
+ }
9392
+ function sideColumnCell(slot, kind, leadingGap) {
9393
+ if (kind !== "rep") return { kind, leadingGap };
9394
+ return slot?.kind === "rep" ? { kind: "rep", value: slot.value, leadingGap } : { kind: "empty", leadingGap };
9395
+ }
9396
+ function alignDualSlots(left, right) {
9397
+ const n = Math.max(left.length, right.length);
9398
+ const L = [];
9399
+ const R = [];
9400
+ for (let i = 0; i < n; i++) {
9401
+ const lt = left[i];
9402
+ const rt = right[i];
9403
+ const kind = mergeColumnKind(lt, rt);
9404
+ const gap = Math.max(lt?.leadingGap ?? REP_GAP2, rt?.leadingGap ?? REP_GAP2);
9405
+ L.push(sideColumnCell(lt, kind, gap));
9406
+ R.push(sideColumnCell(rt, kind, gap));
9407
+ }
9408
+ return { left: L, right: R };
9409
+ }
9410
+ function DualVelocityHero({
9411
+ leftDone,
9412
+ rightDone,
9413
+ leftStream,
9414
+ rightStream,
9415
+ leftLabel,
9416
+ rightLabel,
9417
+ zones,
9418
+ barColor: barColor2,
9419
+ scale,
9158
9420
  targetReps,
9421
+ liveRepIndex,
9159
9422
  height,
9160
9423
  className,
9424
+ label,
9161
9425
  viewProps
9162
9426
  }) {
9163
- const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9164
- const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
9165
- const [plotW, setPlotW] = React25.useState(0);
9166
- const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
9167
- const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
9168
- const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
9169
- const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
9170
- const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
9171
- const totalSlots = doneVelocities.length + pendingCount;
9172
- const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
9173
- const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
9174
- const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
9175
- const peakIndex = doneVelocities.reduce(
9176
- (best, v, i) => v > doneVelocities[best] ? i : best,
9177
- 0
9178
- );
9179
- const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
9180
- const repCount = doneVelocities.length;
9181
- const total = Math.max(repCount, targetReps ?? repCount);
9182
- const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9427
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9428
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9429
+ const aligned = alignDualSlots(
9430
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9431
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9432
+ );
9183
9433
  const { style: externalStyle, ...restProps } = viewProps;
9184
- return /* @__PURE__ */ jsx(
9434
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9435
+ VelocityStrip,
9436
+ {
9437
+ variant: "hero",
9438
+ orientation,
9439
+ velocities: done,
9440
+ columnSlots: columns,
9441
+ scale,
9442
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9443
+ barColor: barColor2,
9444
+ zones,
9445
+ liveRepIndex,
9446
+ height: plotHalf,
9447
+ hideBaseline: true
9448
+ }
9449
+ );
9450
+ return /* @__PURE__ */ jsxs(
9185
9451
  reactNative.View,
9186
9452
  {
9187
9453
  className,
9188
- style: [{ height }, externalStyle],
9454
+ style: [{ height, flexDirection: "row" }, externalStyle],
9189
9455
  accessibilityRole: "image",
9190
9456
  accessibilityLabel: label,
9191
- testID: "velocity-strip-hero",
9457
+ testID: "dual-velocity-strip",
9192
9458
  ...restProps,
9193
- children: /* @__PURE__ */ jsxs(
9194
- reactNative.View,
9195
- {
9196
- onLayout: onPlotLayout,
9197
- style: {
9198
- flex: 1,
9199
- flexDirection: "row",
9200
- alignItems: "flex-end",
9201
- gap: heroGap,
9202
- position: "relative",
9203
- borderBottomWidth: 2,
9204
- borderBottomColor: HERO_PENDING_COLOR
9205
- },
9206
- children: [
9207
- referencePx > 0 && /* @__PURE__ */ jsx(
9208
- reactNative.View,
9209
- {
9210
- accessibilityElementsHidden: true,
9211
- style: {
9212
- position: "absolute",
9213
- left: 0,
9214
- right: 0,
9215
- bottom: referencePx,
9216
- borderTopWidth: 1,
9217
- borderStyle: "dashed",
9218
- borderColor: HERO_REFERENCE_COLOR,
9219
- pointerEvents: "none"
9220
- },
9221
- testID: "velocity-hero-reference"
9222
- }
9223
- ),
9224
- doneVelocities.map((velocity, i) => {
9225
- const isLive = liveRepIndex === i;
9226
- return /* @__PURE__ */ jsxs(
9227
- reactNative.View,
9228
- {
9229
- accessibilityElementsHidden: true,
9230
- style: {
9231
- flex: 1,
9232
- maxWidth: HERO_BAR_MAX_WIDTH,
9233
- height: "100%",
9234
- alignItems: "center",
9235
- justifyContent: "flex-end"
9236
- },
9237
- children: [
9238
- showBarLabel(i) && /* @__PURE__ */ jsx(
9239
- reactNative.Text,
9240
- {
9241
- className: "text-text-primary",
9242
- style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
9243
- testID: `velocity-label-${i}`,
9244
- children: formatVelocity(velocity)
9245
- }
9246
- ),
9247
- /* @__PURE__ */ jsx(
9248
- reactNative.Animated.View,
9249
- {
9250
- style: [
9251
- {
9252
- width: "100%",
9253
- height: barHeight(velocity),
9254
- borderTopLeftRadius: HERO_BAR_RADIUS,
9255
- borderTopRightRadius: HERO_BAR_RADIUS,
9256
- backgroundColor: barColorFor(velocity)
9257
- },
9258
- isLive ? { transform: [{ scale: liveScale }] } : null
9259
- ],
9260
- testID: `velocity-bar-${i}`
9261
- }
9262
- )
9263
- ]
9264
- },
9265
- i
9266
- );
9267
- }),
9268
- Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
9269
- reactNative.View,
9270
- {
9271
- accessibilityElementsHidden: true,
9272
- style: {
9273
- flex: 1,
9274
- maxWidth: HERO_BAR_MAX_WIDTH,
9275
- height: "100%",
9276
- alignItems: "center",
9277
- justifyContent: "flex-end"
9278
- },
9279
- children: /* @__PURE__ */ jsx(
9280
- reactNative.View,
9281
- {
9282
- style: {
9283
- width: "100%",
9284
- height: HERO_MIN_BAR_HEIGHT * 3,
9285
- borderWidth: 1,
9286
- borderStyle: "dashed",
9287
- borderColor: HERO_PENDING_COLOR,
9288
- borderTopLeftRadius: HERO_BAR_RADIUS,
9289
- borderTopRightRadius: HERO_BAR_RADIUS
9290
- },
9291
- testID: "velocity-slot-todo"
9292
- }
9293
- )
9294
- },
9295
- `pending-${i}`
9296
- ))
9297
- ]
9298
- }
9299
- )
9459
+ children: [
9460
+ /* @__PURE__ */ jsx(
9461
+ ChartSideRail,
9462
+ {
9463
+ sectionExtent: plotHalf,
9464
+ variant: "hero",
9465
+ sections: [
9466
+ { label: leftLabel, testID: "dual-velocity-side-label-L" },
9467
+ { label: rightLabel, testID: "dual-velocity-side-label-R" }
9468
+ ]
9469
+ }
9470
+ ),
9471
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1, gap: DUAL_WING_GAP }, children: [
9472
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9473
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9474
+ ] })
9475
+ ]
9476
+ }
9477
+ );
9478
+ }
9479
+ function DualVelocityRail({
9480
+ leftDone,
9481
+ rightDone,
9482
+ leftStream,
9483
+ rightStream,
9484
+ zones,
9485
+ barColor: barColor2,
9486
+ scale,
9487
+ targetReps,
9488
+ liveRepIndex,
9489
+ height,
9490
+ className,
9491
+ label,
9492
+ viewProps
9493
+ }) {
9494
+ const plotHalf = (height - DUAL_WING_GAP) / 2;
9495
+ const sharedMax = Math.max(...leftDone, ...rightDone, 0);
9496
+ const aligned = alignDualSlots(
9497
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9498
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9499
+ );
9500
+ const { style: externalStyle, ...restProps } = viewProps;
9501
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9502
+ VelocityStrip,
9503
+ {
9504
+ variant: "expanded",
9505
+ showNumbers: false,
9506
+ showInfo: false,
9507
+ orientation,
9508
+ velocities: done,
9509
+ columnSlots: columns,
9510
+ scale,
9511
+ scaleMax: scale === "fixed" ? void 0 : sharedMax,
9512
+ barColor: barColor2,
9513
+ zones,
9514
+ liveRepIndex,
9515
+ height: plotHalf
9516
+ }
9517
+ );
9518
+ return /* @__PURE__ */ jsxs(
9519
+ reactNative.View,
9520
+ {
9521
+ className,
9522
+ style: [{ height, flexDirection: "column", gap: DUAL_WING_GAP }, externalStyle],
9523
+ accessibilityRole: "image",
9524
+ accessibilityLabel: label,
9525
+ testID: "dual-velocity-strip",
9526
+ ...restProps,
9527
+ children: [
9528
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9529
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9530
+ ]
9531
+ }
9532
+ );
9533
+ }
9534
+ function DualVelocityCompactStrip({
9535
+ leftDone,
9536
+ rightDone,
9537
+ leftStream,
9538
+ rightStream,
9539
+ zones,
9540
+ barColor: barColor2,
9541
+ targetReps,
9542
+ height,
9543
+ className,
9544
+ label,
9545
+ viewProps
9546
+ }) {
9547
+ const half = height === DUAL_COMPACT_HEIGHT ? COMPACT_FOLD_HALF : (height - COMPACT_FOLD_GAP) / 2;
9548
+ const aligned = alignDualSlots(
9549
+ leftStream ? streamNaturalSlots(leftStream, targetReps) : [],
9550
+ rightStream ? streamNaturalSlots(rightStream, targetReps) : []
9551
+ );
9552
+ const { style: externalStyle, ...restProps } = viewProps;
9553
+ const wing = (orientation, columns, done) => /* @__PURE__ */ jsx(
9554
+ VelocityStrip,
9555
+ {
9556
+ variant: "compact",
9557
+ orientation,
9558
+ velocities: done,
9559
+ columnSlots: columns,
9560
+ barColor: barColor2,
9561
+ zones,
9562
+ height: half
9563
+ }
9564
+ );
9565
+ return /* @__PURE__ */ jsxs(
9566
+ reactNative.View,
9567
+ {
9568
+ className,
9569
+ style: [{ height, flexDirection: "column", gap: COMPACT_FOLD_GAP }, externalStyle],
9570
+ accessibilityRole: "image",
9571
+ accessibilityLabel: label,
9572
+ testID: "dual-velocity-strip",
9573
+ ...restProps,
9574
+ children: [
9575
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-up", children: wing("up", aligned.left, leftDone) }),
9576
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, testID: "dual-velocity-wing-down", children: wing("down", aligned.right, rightDone) })
9577
+ ]
9300
9578
  }
9301
9579
  );
9302
9580
  }
9581
+ function DualVelocityStrip({
9582
+ left,
9583
+ right,
9584
+ zones,
9585
+ barColor: barColor2 = "loss",
9586
+ targetReps,
9587
+ liveRepIndex,
9588
+ variant = "hero",
9589
+ scale = "peak",
9590
+ height,
9591
+ className,
9592
+ ...props
9593
+ }) {
9594
+ const resolvedHeight = height ?? (variant === "hero" ? DUAL_HERO_HEIGHT : variant === "compact" ? DUAL_COMPACT_HEIGHT : DUAL_RAIL_HEIGHT);
9595
+ const leftDone = streamDone(left);
9596
+ const rightDone = streamDone(right);
9597
+ const label = `Dual velocity chart, left ${leftDone.length} of ${Math.max(leftDone.length, targetReps ?? leftDone.length)} reps, right ${rightDone.length} of ${Math.max(rightDone.length, targetReps ?? rightDone.length)} reps`;
9598
+ const shared = {
9599
+ leftDone,
9600
+ rightDone,
9601
+ leftStream: left,
9602
+ rightStream: right,
9603
+ leftLabel: left.label,
9604
+ rightLabel: right.label,
9605
+ zones,
9606
+ barColor: barColor2,
9607
+ scale,
9608
+ targetReps,
9609
+ height: resolvedHeight,
9610
+ className,
9611
+ label,
9612
+ viewProps: props
9613
+ };
9614
+ if (variant === "rail") return /* @__PURE__ */ jsx(DualVelocityRail, { ...shared, liveRepIndex });
9615
+ if (variant === "compact") return /* @__PURE__ */ jsx(DualVelocityCompactStrip, { ...shared });
9616
+ return /* @__PURE__ */ jsx(DualVelocityHero, { ...shared, liveRepIndex });
9617
+ }
9303
9618
  function VelocityStrip({
9304
9619
  velocities,
9305
9620
  set,
9306
9621
  zones,
9622
+ barColor: barColor2 = "loss",
9623
+ showLossBands,
9624
+ orientation = "up",
9625
+ scaleMax,
9626
+ hideBaseline,
9627
+ columnSlots,
9628
+ label,
9307
9629
  liveRepIndex,
9308
9630
  expanded = true,
9309
9631
  onToggle,
@@ -9317,51 +9639,29 @@ function VelocityStrip({
9317
9639
  className,
9318
9640
  ...props
9319
9641
  }) {
9642
+ const lossBandsOn = showLossBands ?? barColor2 === "loss";
9320
9643
  const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
9321
- const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
9322
- kind: "rep",
9323
- velocity: v,
9324
- leadingGap: i === 0 ? 0 : REP_GAP
9325
- }));
9326
9644
  const maxVelocity = Math.max(...doneVelocities, 0);
9327
9645
  const meanVelocity = calculateMeanVelocity(doneVelocities);
9328
9646
  const loss = calculateVelocityLoss(doneVelocities);
9329
9647
  const framed = showNumbers || showInfo;
9330
- const scaleDenom = scaleDenominator(scale, maxVelocity);
9331
9648
  const hasZones = zones != null && zones.length > 0;
9332
- const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
9333
- const slotColor = (slot) => {
9334
- if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
9335
- if (slot.kind === "todo") return TODO_COLOR2;
9336
- return SET_STRIP_VARIABLE_COLOR;
9337
- };
9649
+ const zoneColorFor = makeBarColorFor(zones);
9650
+ const barColorFor = (v) => barColor2 === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
9338
9651
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
9339
- const [heightAnim] = React25.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
9340
- const [labelOpacity] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9652
+ const [expandProgress] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9341
9653
  const [infoOpacity] = React25.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
9342
9654
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
9343
9655
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
9344
- const liveScale = useLiveRepPop(
9345
- variant === "expanded" && framed,
9346
- liveRepIndex,
9347
- liveVelocity,
9348
- isNewPeak
9349
- );
9350
9656
  React25.useEffect(() => {
9351
9657
  if (variant !== "expanded" || !framed) return;
9352
9658
  reactNative.Animated.parallel([
9353
- reactNative.Animated.timing(heightAnim, {
9354
- toValue: expanded ? height : 3,
9659
+ reactNative.Animated.timing(expandProgress, {
9660
+ toValue: expanded ? 1 : 0,
9355
9661
  duration: ANIMATION_DURATION,
9356
9662
  easing: ANIMATION_EASING,
9357
9663
  useNativeDriver: false
9358
9664
  }),
9359
- reactNative.Animated.timing(labelOpacity, {
9360
- toValue: expanded ? 1 : 0,
9361
- duration: 300,
9362
- delay: expanded ? 150 : 0,
9363
- useNativeDriver: false
9364
- }),
9365
9665
  reactNative.Animated.timing(infoOpacity, {
9366
9666
  toValue: expanded ? 1 : 0,
9367
9667
  duration: 300,
@@ -9369,250 +9669,176 @@ function VelocityStrip({
9369
9669
  useNativeDriver: false
9370
9670
  })
9371
9671
  ]).start();
9372
- }, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
9672
+ }, [expanded, variant, framed, expandProgress, infoOpacity]);
9373
9673
  if (set == null && velocities == null) return null;
9374
9674
  const repCount = doneVelocities.length;
9375
9675
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
9376
9676
  if (variant === "hero") {
9377
- const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
9677
+ const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
9678
+ const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9679
+ kind: s.kind,
9680
+ value: s.velocity,
9681
+ leadingGap: s.leadingGap
9682
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9683
+ const total = Math.max(repCount, targetReps ?? repCount);
9684
+ const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
9378
9685
  return /* @__PURE__ */ jsx(
9379
- HeroVelocityChart,
9686
+ SetBarChart,
9380
9687
  {
9381
- doneVelocities,
9382
- barColorFor,
9383
- scaleDenom,
9384
- referenceVelocity: maxVelocity,
9688
+ slots: heroSlots,
9689
+ colorFor: barColorFor,
9690
+ height: heroHeight,
9691
+ scale,
9692
+ scaleMax,
9693
+ orientation,
9385
9694
  liveRepIndex,
9386
9695
  isNewPeak,
9387
- targetReps,
9388
- height: heroHeight,
9696
+ targetReps: set || columnSlots ? void 0 : targetReps,
9697
+ label,
9698
+ showValueLabels: true,
9699
+ formatValue: formatVelocity,
9700
+ renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
9701
+ hideBaseline: true,
9702
+ testID: "velocity-strip-hero",
9703
+ testIDPrefix: "velocity",
9704
+ accessibilityLabel: heroLabel,
9389
9705
  className,
9390
9706
  viewProps: props
9391
9707
  }
9392
9708
  );
9393
9709
  }
9394
- if (variant === "mini") {
9395
- const { style: externalStyle, ...restProps } = props;
9710
+ if (variant === "compact") {
9711
+ const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9712
+ kind: s.kind,
9713
+ value: s.velocity,
9714
+ leadingGap: s.leadingGap
9715
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9716
+ const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
9396
9717
  return /* @__PURE__ */ jsx(
9397
- reactNative.View,
9718
+ SetBarChart,
9398
9719
  {
9399
- className,
9400
- style: [
9401
- { flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
9402
- externalStyle
9403
- ],
9404
- accessibilityRole: "image",
9720
+ slots: compactSlots,
9721
+ colorFor: barColorFor,
9722
+ height: compactHeight,
9723
+ scale,
9724
+ scaleMax,
9725
+ orientation,
9726
+ flat: true,
9727
+ barRadius: 2,
9728
+ cornerStyle: "all",
9729
+ targetReps: set || columnSlots ? void 0 : targetReps,
9730
+ label,
9731
+ hideBaseline: true,
9732
+ testID: "velocity-strip-compact",
9733
+ testIDPrefix: "velocity",
9405
9734
  accessibilityLabel: miniLabel,
9406
- testID: "velocity-strip-mini",
9407
- ...restProps,
9408
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9409
- reactNative.View,
9410
- {
9411
- style: {
9412
- flex: 1,
9413
- backgroundColor: slotColor(slot),
9414
- borderRadius: 1,
9415
- minWidth: 4,
9416
- height: "100%",
9417
- // The container's uniform 2px gap covers rep spacing; a wide slot adds
9418
- // only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
9419
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9420
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9421
- },
9422
- accessibilityElementsHidden: true,
9423
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9424
- },
9425
- i
9426
- ))
9735
+ className,
9736
+ viewProps: props
9427
9737
  }
9428
9738
  );
9429
9739
  }
9430
9740
  if (!framed) {
9431
- const { style: externalStyle, ...restProps } = props;
9741
+ const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
9742
+ kind: s.kind,
9743
+ value: s.velocity,
9744
+ leadingGap: s.leadingGap
9745
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
9432
9746
  return /* @__PURE__ */ jsx(
9433
- reactNative.View,
9747
+ SetBarChart,
9434
9748
  {
9435
- className,
9436
- style: [
9437
- { flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
9438
- externalStyle
9439
- ],
9440
- accessibilityRole: "image",
9749
+ slots: spotlightSlots,
9750
+ colorFor: barColorFor,
9751
+ height,
9752
+ scale,
9753
+ scaleMax,
9754
+ orientation,
9755
+ liveRepIndex,
9756
+ isNewPeak,
9757
+ barRadius: 2,
9758
+ cornerStyle: "top",
9759
+ targetReps: set ? void 0 : targetReps,
9760
+ label,
9761
+ hideBaseline: true,
9762
+ testID: "velocity-strip-spotlight",
9763
+ testIDPrefix: "velocity",
9441
9764
  accessibilityLabel: miniLabel,
9442
- testID: "velocity-strip-compact",
9443
- ...restProps,
9444
- children: slots.map((slot, i) => /* @__PURE__ */ jsx(
9445
- reactNative.View,
9446
- {
9447
- style: {
9448
- flex: 1,
9449
- minWidth: 4,
9450
- height: bareSlotHeight(slot, height, scaleDenom),
9451
- // Top-rounded to match the framed chart's bars (the "rounded tops from
9452
- // the numbers one"); square bottoms since bars sit on the baseline.
9453
- borderTopLeftRadius: 2,
9454
- borderTopRightRadius: 2,
9455
- backgroundColor: slotColor(slot),
9456
- marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
9457
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9458
- },
9459
- accessibilityElementsHidden: true,
9460
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
9461
- },
9462
- i
9463
- ))
9765
+ className,
9766
+ viewProps: props
9464
9767
  }
9465
9768
  );
9466
9769
  }
9467
9770
  const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
9468
9771
  const hasInteractiveContainer = onToggle != null;
9469
9772
  const hasInteractiveReps = onRepPress != null && expanded;
9470
- const setBars = set != null && slots.map((slot, i) => {
9471
- const isStraightRep = set.type === "straight" && slot.kind === "rep";
9472
- const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
9473
- return /* @__PURE__ */ jsx(
9474
- reactNative.View,
9773
+ const framedSlots = set ? buildSlots(set).map((sl) => ({
9774
+ kind: sl.kind,
9775
+ value: sl.velocity,
9776
+ leadingGap: sl.leadingGap
9777
+ })) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
9778
+ const needsBarOverlay = showNumbers || onRepPress != null;
9779
+ const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
9780
+ showNumbers && /* @__PURE__ */ jsx(
9781
+ reactNative.Animated.View,
9475
9782
  {
9476
9783
  style: {
9477
- flex: 1,
9478
- height: "100%",
9479
- justifyContent: "flex-end",
9480
- marginLeft: slot.leadingGap
9784
+ opacity: expandProgress,
9785
+ position: "absolute",
9786
+ top: -13,
9787
+ left: 0,
9788
+ right: 0,
9789
+ alignItems: "center"
9481
9790
  },
9791
+ accessibilityElementsHidden: true,
9792
+ pointerEvents: "none",
9482
9793
  children: /* @__PURE__ */ jsx(
9483
- reactNative.View,
9794
+ reactNative.Text,
9484
9795
  {
9485
- style: {
9486
- height: `${heightPct}%`,
9487
- minHeight: 2,
9488
- borderTopLeftRadius: 2,
9489
- borderTopRightRadius: 2,
9490
- backgroundColor: slotColor(slot),
9491
- ...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
9492
- },
9493
- testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
9494
- accessibilityRole: "image",
9495
- accessibilityLabel: slotAccessibilityLabel(slot, i)
9796
+ className: "text-text-secondary",
9797
+ style: { fontSize: 8, fontWeight: "600" },
9798
+ testID: `velocity-label-${repIndex}`,
9799
+ children: formatVelocity(value)
9496
9800
  }
9497
9801
  )
9498
- },
9499
- i
9500
- );
9501
- });
9802
+ }
9803
+ ),
9804
+ onRepPress && expanded && /* @__PURE__ */ jsx(
9805
+ reactNative.Pressable,
9806
+ {
9807
+ style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
9808
+ onPress: () => onRepPress(repIndex, value),
9809
+ accessibilityRole: "button",
9810
+ accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
9811
+ testID: `velocity-bar-pressable-${repIndex}`
9812
+ }
9813
+ )
9814
+ ] }) : void 0;
9502
9815
  const stripContent = /* @__PURE__ */ jsxs(
9503
9816
  reactNative.Animated.View,
9504
9817
  {
9505
- className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
9506
- style: [
9507
- {
9508
- height: heightAnim,
9509
- width: "100%",
9510
- gap: 2,
9511
- borderRadius: expanded ? 6 : 2,
9512
- paddingTop: expanded ? 16 : 0,
9513
- paddingBottom: expanded ? 24 : 0,
9514
- // No horizontal inset — the framed chart's bars span full width, matching
9515
- // the bare spotlight and mini. Numbers/info sit in the vertical padding.
9516
- paddingHorizontal: 0,
9517
- paddingVertical: expanded ? void 0 : 8,
9518
- overflow: expanded ? "visible" : "hidden"
9519
- }
9520
- ],
9818
+ className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
9819
+ style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
9521
9820
  accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
9522
9821
  accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
9523
9822
  testID: "velocity-strip",
9524
9823
  ...props,
9525
9824
  children: [
9526
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
9527
- const barBackground = barColorFor(v);
9528
- const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
9529
- const isLive = liveRepIndex === i;
9530
- const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
9531
- const barStyle = expanded ? {
9532
- height: `${barHeightPct}%`,
9533
- minHeight: 2,
9534
- borderTopLeftRadius: 2,
9535
- borderTopRightRadius: 2,
9536
- backgroundColor: barBackground
9537
- } : {
9538
- minHeight: "100%",
9539
- borderTopLeftRadius: 2,
9540
- borderTopRightRadius: 2,
9541
- borderBottomLeftRadius: 0,
9542
- borderBottomRightRadius: 0,
9543
- backgroundColor: barBackground
9544
- };
9545
- const barInner = isLive ? /* @__PURE__ */ jsx(
9546
- reactNative.Animated.View,
9547
- {
9548
- style: [barStyle, { transform: [{ scale: liveScale }] }],
9549
- testID: `velocity-bar-${i}`,
9550
- accessibilityRole: "image",
9551
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
9552
- }
9553
- ) : /* @__PURE__ */ jsx(
9554
- reactNative.View,
9555
- {
9556
- style: barStyle,
9557
- testID: `velocity-bar-${i}`,
9558
- accessibilityRole: "image",
9559
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
9560
- }
9561
- );
9562
- const bar = /* @__PURE__ */ jsxs(
9563
- reactNative.View,
9564
- {
9565
- style: {
9566
- flex: 1,
9567
- height: "100%",
9568
- justifyContent: "flex-end",
9569
- position: "relative"
9570
- },
9571
- children: [
9572
- expanded && showNumbers && /* @__PURE__ */ jsx(
9573
- reactNative.Animated.View,
9574
- {
9575
- style: {
9576
- opacity: labelOpacity,
9577
- alignItems: "center",
9578
- position: "absolute",
9579
- top: -13,
9580
- left: 0,
9581
- right: 0
9582
- },
9583
- accessibilityElementsHidden: true,
9584
- children: /* @__PURE__ */ jsx(
9585
- reactNative.Text,
9586
- {
9587
- className: "text-text-secondary",
9588
- style: { fontSize: 8, fontWeight: "600" },
9589
- testID: `velocity-label-${i}`,
9590
- children: formatVelocity(v)
9591
- }
9592
- )
9593
- }
9594
- ),
9595
- barInner
9596
- ]
9597
- },
9598
- i
9599
- );
9600
- if (onRepPress && expanded) {
9601
- return /* @__PURE__ */ jsx(
9602
- reactNative.Pressable,
9603
- {
9604
- style: { flex: 1, height: "100%" },
9605
- onPress: () => onRepPress(i, v),
9606
- accessibilityRole: "button",
9607
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
9608
- testID: `velocity-bar-pressable-${i}`,
9609
- children: bar
9610
- },
9611
- i
9612
- );
9825
+ /* @__PURE__ */ jsx(
9826
+ SetBarChart,
9827
+ {
9828
+ slots: framedSlots,
9829
+ colorFor: barColorFor,
9830
+ height,
9831
+ scale,
9832
+ scaleMax,
9833
+ expandProgress,
9834
+ renderBarOverlay: renderFramedBarOverlay,
9835
+ targetReps: set ? void 0 : targetReps,
9836
+ barRadius: 2,
9837
+ cornerStyle: "top",
9838
+ hideBaseline: true,
9839
+ testIDPrefix: "velocity"
9613
9840
  }
9614
- return bar;
9615
- }) }),
9841
+ ),
9616
9842
  expanded && showInfo && /* @__PURE__ */ jsxs(
9617
9843
  reactNative.Animated.View,
9618
9844
  {
@@ -9620,10 +9846,8 @@ function VelocityStrip({
9620
9846
  flexDirection: "row",
9621
9847
  justifyContent: "space-between",
9622
9848
  opacity: infoOpacity,
9623
- position: "absolute",
9624
- bottom: 4,
9625
- left: 6,
9626
- right: 6
9849
+ marginTop: 6,
9850
+ paddingHorizontal: 6
9627
9851
  },
9628
9852
  testID: "velocity-info-row",
9629
9853
  children: [
@@ -9631,10 +9855,7 @@ function VelocityStrip({
9631
9855
  reactNative.Text,
9632
9856
  {
9633
9857
  className: "text-text-secondary",
9634
- style: {
9635
- fontSize: 10,
9636
- fontFamily: "Inter, sans-serif"
9637
- },
9858
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
9638
9859
  children: [
9639
9860
  meanZone,
9640
9861
  " ",
@@ -9649,11 +9870,7 @@ function VelocityStrip({
9649
9870
  reactNative.Text,
9650
9871
  {
9651
9872
  className: "text-text-secondary",
9652
- style: {
9653
- fontSize: 10,
9654
- fontFamily: "Inter, sans-serif",
9655
- ...getLossStyle(loss)
9656
- },
9873
+ style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
9657
9874
  children: [
9658
9875
  "Loss: ",
9659
9876
  loss,
@@ -9961,6 +10178,7 @@ function RowStrip({ set }) {
9961
10178
  showInfo: false,
9962
10179
  height: 24,
9963
10180
  scale: "fixed",
10181
+ liveRepIndex: set.velocities.length - 1,
9964
10182
  set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
9965
10183
  zones
9966
10184
  }
@@ -9970,7 +10188,9 @@ function RowStrip({ set }) {
9970
10188
  return /* @__PURE__ */ jsx(
9971
10189
  VelocityStrip,
9972
10190
  {
9973
- variant: "mini",
10191
+ variant: "compact",
10192
+ height: 8,
10193
+ hideBaseline: true,
9974
10194
  set: { type: "straight", velocities, planned: plannedReps(set) },
9975
10195
  zones
9976
10196
  }
@@ -10423,7 +10643,108 @@ function SetsRepsLoad({
10423
10643
  }
10424
10644
  );
10425
10645
  }
10426
-
10646
+ var PULSE_HALF_MS = 950;
10647
+ var PULSE_MIN_OPACITY = 0.45;
10648
+ var SEGMENTED_BAR_GAP = 5;
10649
+ function usePulse2(active) {
10650
+ const [value] = React25.useState(() => new reactNative.Animated.Value(0));
10651
+ React25.useEffect(() => {
10652
+ if (!active) return;
10653
+ const loop = reactNative.Animated.loop(
10654
+ reactNative.Animated.sequence([
10655
+ reactNative.Animated.timing(value, {
10656
+ toValue: 1,
10657
+ duration: PULSE_HALF_MS,
10658
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
10659
+ useNativeDriver: false
10660
+ }),
10661
+ reactNative.Animated.timing(value, {
10662
+ toValue: 0,
10663
+ duration: PULSE_HALF_MS,
10664
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
10665
+ useNativeDriver: false
10666
+ })
10667
+ ])
10668
+ );
10669
+ loop.start();
10670
+ return () => loop.stop();
10671
+ }, [active, value]);
10672
+ return value;
10673
+ }
10674
+ function SegmentedBar({
10675
+ segments,
10676
+ height = 8,
10677
+ gap = SEGMENTED_BAR_GAP,
10678
+ radius = 2,
10679
+ marker = null,
10680
+ segmentTestID,
10681
+ style,
10682
+ ...props
10683
+ }) {
10684
+ const pulse = usePulse2(segments.some((s) => s.pulse));
10685
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
10686
+ segments.map((seg, i) => {
10687
+ const fillStyle = {
10688
+ width: `${(seg.fill ?? 1) * 100}%`,
10689
+ height: "100%",
10690
+ backgroundColor: seg.color,
10691
+ opacity: seg.opacity
10692
+ };
10693
+ const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
10694
+ return /* @__PURE__ */ jsx(
10695
+ reactNative.View,
10696
+ {
10697
+ style: {
10698
+ flex: seg.weight ?? 1,
10699
+ minWidth: 0,
10700
+ height: "100%",
10701
+ marginLeft: seg.leadingGap,
10702
+ borderRadius: radius,
10703
+ overflow: "hidden"
10704
+ },
10705
+ children: seg.pulse ? /* @__PURE__ */ jsx(
10706
+ reactNative.Animated.View,
10707
+ {
10708
+ style: {
10709
+ ...fillStyle,
10710
+ ...seg.pulseColor ? {
10711
+ backgroundColor: pulse.interpolate({
10712
+ inputRange: [0, 1],
10713
+ outputRange: seg.pulseColor
10714
+ })
10715
+ } : {
10716
+ opacity: pulse.interpolate({
10717
+ inputRange: [0, 1],
10718
+ outputRange: [PULSE_MIN_OPACITY, 1]
10719
+ })
10720
+ }
10721
+ },
10722
+ accessibilityElementsHidden: true,
10723
+ testID
10724
+ }
10725
+ ) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
10726
+ },
10727
+ i
10728
+ );
10729
+ }),
10730
+ marker ? /* @__PURE__ */ jsx(
10731
+ reactNative.View,
10732
+ {
10733
+ style: {
10734
+ position: "absolute",
10735
+ left: `${marker.position * 100}%`,
10736
+ top: 0,
10737
+ bottom: 0,
10738
+ width: 2,
10739
+ backgroundColor: marker.color
10740
+ },
10741
+ accessibilityElementsHidden: true,
10742
+ testID: "segmented-bar-marker"
10743
+ }
10744
+ ) : null
10745
+ ] });
10746
+ }
10747
+
10427
10748
  // src/components/custom/Workout/paceTone.ts
10428
10749
  function paceTone(progress, target) {
10429
10750
  if (target === void 0) return "neutral";
@@ -10516,6 +10837,140 @@ function ScheduleTiles({ when, now, gap = 1, ...props }) {
10516
10837
  )
10517
10838
  ] });
10518
10839
  }
10840
+
10841
+ // src/components/custom/Workout/SetBar.tsx
10842
+ var SET_STRIP_ZONES = {
10843
+ slow: primitiveRamps.red[600],
10844
+ moderate: primitiveRamps.orange[400],
10845
+ fast: primitiveRamps.amber[300],
10846
+ fastest: primitiveRamps.green[300]
10847
+ };
10848
+ var TODO_COLOR = primitiveColors.charcoal[300];
10849
+ var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
10850
+ var NOTCH_GAP = 2;
10851
+ var CLUSTER_GAP = 3;
10852
+ var MYO_UPCOMING_CLUSTERS = 3;
10853
+ var MYO_UPCOMING_CLUSTER_LEN = 5;
10854
+ var MYO_UPCOMING_FADE = 0.6;
10855
+ var PULSE_RANGE = {
10856
+ [SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
10857
+ [SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
10858
+ [SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
10859
+ [SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
10860
+ };
10861
+ function velocityZoneColor(v) {
10862
+ if (v < 0.5) return SET_STRIP_ZONES.slow;
10863
+ if (v < 0.75) return SET_STRIP_ZONES.moderate;
10864
+ if (v < 1) return SET_STRIP_ZONES.fast;
10865
+ return SET_STRIP_ZONES.fastest;
10866
+ }
10867
+ function chunkedSegments(chunks, gap) {
10868
+ return chunks.flatMap(
10869
+ (vels, ci) => vels.map((v, ri) => ({
10870
+ color: velocityZoneColor(v),
10871
+ ...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
10872
+ }))
10873
+ );
10874
+ }
10875
+ function rangeSegments(floor, max, doneVels) {
10876
+ return Array.from({ length: max }, (_, i) => {
10877
+ if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
10878
+ return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
10879
+ });
10880
+ }
10881
+ function myoUpcomingSegments(activationLen, clusterCount) {
10882
+ const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
10883
+ const trail = Array.from(
10884
+ { length: clusterCount },
10885
+ (_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
10886
+ color: SET_STRIP_VARIABLE_COLOR,
10887
+ opacity: MYO_UPCOMING_FADE ** ci,
10888
+ ...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
10889
+ }))
10890
+ ).flat();
10891
+ return [...activation, ...trail];
10892
+ }
10893
+ function setSegments(set) {
10894
+ if (set.status === "todo") {
10895
+ return [{ color: TODO_COLOR }];
10896
+ }
10897
+ if (set.status === "done") {
10898
+ return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
10899
+ }
10900
+ if (set.status === "range") {
10901
+ return rangeSegments(set.floor, set.max, set.doneVels);
10902
+ }
10903
+ if (set.status === "drop") {
10904
+ return chunkedSegments(set.subloads, NOTCH_GAP);
10905
+ }
10906
+ if (set.status === "myo") {
10907
+ return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
10908
+ }
10909
+ if (set.status === "myo-upcoming") {
10910
+ return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
10911
+ }
10912
+ const performed = set.velocities.map((v) => {
10913
+ const color = velocityZoneColor(v);
10914
+ return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
10915
+ });
10916
+ const remaining = Math.max(0, set.planned - set.velocities.length);
10917
+ return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
10918
+ }
10919
+ function setBarSegmentTestID(seg) {
10920
+ if (seg.pulse) return "set-strip-pulse";
10921
+ if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
10922
+ return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
10923
+ }
10924
+ function SetBar({ set, height = 8 }) {
10925
+ return /* @__PURE__ */ jsx(
10926
+ SegmentedBar,
10927
+ {
10928
+ segments: setSegments(set),
10929
+ height,
10930
+ gap: 0,
10931
+ radius: 0,
10932
+ segmentTestID: setBarSegmentTestID,
10933
+ style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
10934
+ testID: "set-strip-set"
10935
+ }
10936
+ );
10937
+ }
10938
+ var SET_STRIP_GAP = 5;
10939
+ function setCategory(set) {
10940
+ switch (set.status) {
10941
+ case "done":
10942
+ case "drop":
10943
+ case "myo":
10944
+ return "done";
10945
+ case "active":
10946
+ return "active";
10947
+ case "range":
10948
+ return set.doneVels.length > 0 ? "active" : "upcoming";
10949
+ case "todo":
10950
+ case "myo-upcoming":
10951
+ return "upcoming";
10952
+ }
10953
+ }
10954
+ function describeSets(sets) {
10955
+ const done = sets.filter((s) => setCategory(s) === "done").length;
10956
+ const active = sets.filter((s) => setCategory(s) === "active").length;
10957
+ const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
10958
+ return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
10959
+ }
10960
+ function SetStrip({ sets, height = 8, className, ...props }) {
10961
+ return /* @__PURE__ */ jsx(
10962
+ reactNative.View,
10963
+ {
10964
+ className,
10965
+ style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
10966
+ accessibilityRole: "image",
10967
+ accessibilityLabel: describeSets(sets),
10968
+ testID: "set-strip",
10969
+ ...props,
10970
+ children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
10971
+ }
10972
+ );
10973
+ }
10519
10974
  var t9 = getSemanticColors("dark");
10520
10975
  var INDICATOR_CONFIG = {
10521
10976
  imbalance: { Icon: ScaleIcon, color: t9["status-error"], label: "Left/right imbalance" },
@@ -10818,7 +11273,9 @@ function CollapsedCard({
10818
11273
  {
10819
11274
  velocities,
10820
11275
  zones: velocityZones,
10821
- variant: "mini",
11276
+ variant: "compact",
11277
+ height: 8,
11278
+ hideBaseline: true,
10822
11279
  testID: `exercise-card-velocity-strip-${i}`
10823
11280
  },
10824
11281
  i
@@ -11117,6 +11574,7 @@ function SessionRail({
11117
11574
  next,
11118
11575
  stripHeight = 8,
11119
11576
  width = DEFAULT_WIDTH,
11577
+ expandedIndex,
11120
11578
  onExercisePress,
11121
11579
  className,
11122
11580
  style,
@@ -11157,7 +11615,24 @@ function SessionRail({
11157
11615
  borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
11158
11616
  borderBottomColor: DIVIDER
11159
11617
  },
11160
- children: /* @__PURE__ */ jsx(
11618
+ children: i === expandedIndex && ex.sets ? /* @__PURE__ */ jsx(
11619
+ ExerciseCard,
11620
+ {
11621
+ name: ex.name,
11622
+ expanded: true,
11623
+ summary: {
11624
+ sets: ex.summary.sets,
11625
+ reps: ex.summary.reps,
11626
+ // ExerciseCard's summary takes a numeric load; a string load is
11627
+ // an unset/discovery weight, which has no expanded form yet.
11628
+ weight: typeof ex.summary.weight === "number" ? ex.summary.weight : 0,
11629
+ unit: ex.summary.unit
11630
+ },
11631
+ tempo: ex.tempo,
11632
+ sets: ex.sets,
11633
+ onExpandedChange: () => onExercisePress?.(ex, i)
11634
+ }
11635
+ ) : /* @__PURE__ */ jsx(
11161
11636
  ExerciseCardHeading,
11162
11637
  {
11163
11638
  name: ex.name,
@@ -12503,8 +12978,8 @@ var ERROR_PILL_BORDER = "rgba(209,67,67,0.20)";
12503
12978
  var PLOT_LEFT = 26;
12504
12979
  var TOOLTIP_WIDTH = 124;
12505
12980
  function timeOf(dateStr) {
12506
- const t17 = Date.parse(dateStr);
12507
- return Number.isNaN(t17) ? 0 : t17;
12981
+ const t26 = Date.parse(dateStr);
12982
+ return Number.isNaN(t26) ? 0 : t26;
12508
12983
  }
12509
12984
  function formatValue(value) {
12510
12985
  return `${Math.round(value)}`;
@@ -13039,8 +13514,8 @@ function interpEdge(pixels, x, key) {
13039
13514
  const a = pixels[i];
13040
13515
  const b = pixels[i + 1];
13041
13516
  if (x >= a.x && x <= b.x) {
13042
- const t17 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
13043
- return a[key] + t17 * (b[key] - a[key]);
13517
+ const t26 = b.x === a.x ? 0 : (x - a.x) / (b.x - a.x);
13518
+ return a[key] + t26 * (b[key] - a[key]);
13044
13519
  }
13045
13520
  }
13046
13521
  return pixels[pixels.length - 1][key];
@@ -13646,14 +14121,14 @@ function FatigueMeter({
13646
14121
  className,
13647
14122
  ...props
13648
14123
  }) {
13649
- const [t1, t22, t32] = thresholds;
14124
+ const [t1, t26, t32] = thresholds;
13650
14125
  const zones = [
13651
14126
  { upTo: t1, color: zoneColors[0] },
13652
- { upTo: t22, color: zoneColors[1] },
14127
+ { upTo: t26, color: zoneColors[1] },
13653
14128
  { upTo: t32, color: zoneColors[2] },
13654
14129
  { upTo: max, color: zoneColors[3] }
13655
14130
  ];
13656
- const tickValues = [0, t1, t22, t32, max];
14131
+ const tickValues = [0, t1, t26, t32, max];
13657
14132
  const ticks = tickValues.map((v, i) => ({ value: v, label: labels[i] }));
13658
14133
  return /* @__PURE__ */ jsx(
13659
14134
  ZoneTrack,
@@ -13701,196 +14176,1095 @@ function zoneForSets(sets, { mev, mav, mrv }) {
13701
14176
  const midpoint = (mav + mrv) / 2;
13702
14177
  return sets < midpoint ? "productive" : "approaching";
13703
14178
  }
13704
- function VolumeLandmarkBar({
13705
- muscle,
13706
- currentSets,
13707
- landmarks,
13708
- width = 220,
13709
- trackHeight = 10,
13710
- scaleMax,
13711
- className,
13712
- style,
13713
- ...props
14179
+ function VolumeLandmarkBar({
14180
+ muscle,
14181
+ currentSets,
14182
+ landmarks,
14183
+ width = 220,
14184
+ trackHeight = 10,
14185
+ scaleMax,
14186
+ className,
14187
+ style,
14188
+ ...props
14189
+ }) {
14190
+ const { mev, mav, mrv } = landmarks;
14191
+ const max = scaleMax ?? mrv * 1.2;
14192
+ const zone = zoneForSets(currentSets, landmarks);
14193
+ const fillColor = HEAT_BY_ZONE[zone];
14194
+ const pct2 = mav > 0 ? Math.round(currentSets / mav * 100) : 0;
14195
+ return /* @__PURE__ */ jsxs(
14196
+ reactNative.View,
14197
+ {
14198
+ className,
14199
+ style: [{ width, gap: 3 }, style],
14200
+ testID: "volume-landmark-bar",
14201
+ ...props,
14202
+ children: [
14203
+ /* @__PURE__ */ jsx(
14204
+ DataRow,
14205
+ {
14206
+ label: muscle,
14207
+ value: /* @__PURE__ */ jsxs(
14208
+ reactNative.Text,
14209
+ {
14210
+ testID: "volume-landmark-pct",
14211
+ style: { fontSize: 14, fontWeight: "700", fontFamily: MONO2, color: fillColor },
14212
+ children: [
14213
+ pct2,
14214
+ "%"
14215
+ ]
14216
+ }
14217
+ ),
14218
+ className: "py-0",
14219
+ testID: "volume-landmark-header"
14220
+ }
14221
+ ),
14222
+ /* @__PURE__ */ jsx(
14223
+ ZoneTrack,
14224
+ {
14225
+ zones: [{ upTo: max, color: NEUTRAL_TRACK }],
14226
+ max,
14227
+ marker: {
14228
+ type: "fill",
14229
+ value: currentSets,
14230
+ color: fillColor,
14231
+ // Glow when in the optimal productive band — the "sweet spot" cue.
14232
+ glow: zone === "productive"
14233
+ },
14234
+ trackColor: NEUTRAL_TRACK,
14235
+ trackHeight,
14236
+ ticks: [
14237
+ { value: mev, label: "MEV", tooltip: `${LANDMARK_NAME.MEV} \xB7 ${mev} sets/wk` },
14238
+ {
14239
+ value: mav,
14240
+ label: "MAV",
14241
+ emphasized: true,
14242
+ tooltip: `${LANDMARK_NAME.MAV} (target) \xB7 ${mav} sets/wk`
14243
+ },
14244
+ { value: mrv, label: "MRV", tooltip: `${LANDMARK_NAME.MRV} \xB7 ${mrv} sets/wk` }
14245
+ ],
14246
+ accessibilityLabel: `${muscle} weekly volume: ${currentSets} sets, ${pct2}% of MAV target, ${ZONE_DESCRIPTION[zone]}`,
14247
+ testID: "volume-landmark-track"
14248
+ }
14249
+ )
14250
+ ]
14251
+ }
14252
+ );
14253
+ }
14254
+ var t15 = getSemanticColors("dark");
14255
+ var statusToken = {
14256
+ productive: "status-success",
14257
+ threshold: "status-warning",
14258
+ stop: "status-error"
14259
+ };
14260
+ var statusDotVariant = {
14261
+ productive: "success",
14262
+ threshold: "warning",
14263
+ stop: "error"
14264
+ };
14265
+ var defaultLabel = {
14266
+ productive: "Productive",
14267
+ threshold: "Threshold",
14268
+ stop: "Stop"
14269
+ };
14270
+ function statusPillColor(status) {
14271
+ return t15[statusToken[status]];
14272
+ }
14273
+ function StatusPill2({ status, label, className, style, ...props }) {
14274
+ const color = statusPillColor(status);
14275
+ const text = label ?? defaultLabel[status];
14276
+ return /* @__PURE__ */ jsxs(
14277
+ reactNative.View,
14278
+ {
14279
+ testID: "status-pill",
14280
+ className,
14281
+ style: [
14282
+ {
14283
+ flexDirection: "row",
14284
+ alignItems: "center",
14285
+ gap: 7,
14286
+ alignSelf: "flex-start",
14287
+ paddingVertical: 7,
14288
+ paddingHorizontal: 13,
14289
+ borderRadius: 9,
14290
+ backgroundColor: alpha(color, 0.14),
14291
+ borderWidth: 1,
14292
+ borderColor: alpha(color, 0.4)
14293
+ },
14294
+ style
14295
+ ],
14296
+ ...props,
14297
+ children: [
14298
+ /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, children: /* @__PURE__ */ jsx(StatusDot, { variant: statusDotVariant[status], glow: true }) }),
14299
+ /* @__PURE__ */ jsx(reactNative.Text, { testID: "status-pill-label", style: { fontSize: 13, fontWeight: "800", color }, children: text })
14300
+ ]
14301
+ }
14302
+ );
14303
+ }
14304
+ var t16 = getSemanticColors("dark");
14305
+ var categoryToken = {
14306
+ productive: null,
14307
+ threshold: "status-warning",
14308
+ stop: "status-error"
14309
+ };
14310
+ var floodOpacity = {
14311
+ productive: 0,
14312
+ threshold: 0.12,
14313
+ stop: 0.18
14314
+ };
14315
+ function liveAuraColor(category) {
14316
+ const token = categoryToken[category];
14317
+ return token ? t16[token] : null;
14318
+ }
14319
+ function LiveAuraFrame({
14320
+ category,
14321
+ pulse = true,
14322
+ className,
14323
+ children,
14324
+ style,
14325
+ ...props
14326
+ }) {
14327
+ const color = liveAuraColor(category);
14328
+ const tint = color ? alpha(color, floodOpacity[category]) : "transparent";
14329
+ const floodStyle = {
14330
+ ...reactNative.StyleSheet.absoluteFillObject,
14331
+ ...reactNative.Platform.OS === "web" ? (
14332
+ // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
14333
+ { backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
14334
+ ) : { backgroundColor: tint }
14335
+ };
14336
+ return /* @__PURE__ */ jsxs(
14337
+ reactNative.View,
14338
+ {
14339
+ testID: "live-aura-frame",
14340
+ className,
14341
+ style: [
14342
+ {
14343
+ position: "relative",
14344
+ overflow: "hidden",
14345
+ borderRadius: 10,
14346
+ backgroundColor: t16["background-base"],
14347
+ borderWidth: 1,
14348
+ borderColor: t16["border-default"]
14349
+ },
14350
+ style
14351
+ ],
14352
+ ...props,
14353
+ children: [
14354
+ color && /* @__PURE__ */ jsx(
14355
+ reactNative.View,
14356
+ {
14357
+ testID: "live-aura-flood",
14358
+ accessibilityElementsHidden: true,
14359
+ pointerEvents: "none",
14360
+ className: category === "stop" && pulse ? "animate-pulse" : void 0,
14361
+ style: floodStyle
14362
+ }
14363
+ ),
14364
+ children
14365
+ ]
14366
+ }
14367
+ );
14368
+ }
14369
+ function VelocityHero({
14370
+ velocities,
14371
+ targetReps,
14372
+ liveRepIndex,
14373
+ width,
14374
+ height = 300
14375
+ }) {
14376
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "velocity-hero", style: width != null ? { width, height } : { flex: 1, height }, children: /* @__PURE__ */ jsx(
14377
+ VelocityStrip,
14378
+ {
14379
+ variant: "hero",
14380
+ velocities,
14381
+ liveRepIndex: liveRepIndex ?? velocities.length - 1,
14382
+ targetReps,
14383
+ height,
14384
+ scale: "peak"
14385
+ }
14386
+ ) });
14387
+ }
14388
+
14389
+ // src/components/custom/Fatigue/fatigue-tokens.ts
14390
+ var t17 = getSemanticColors("dark");
14391
+ var FONT_HEAD = '"Space Grotesk", sans-serif';
14392
+ var FONT_UI = '"Nunito Sans", sans-serif';
14393
+ var FONT_MONO = "monospace";
14394
+ var TONE_COLOR = {
14395
+ ok: t17["status-success"],
14396
+ warn: t17["status-warning"],
14397
+ alarm: t17["status-error"]
14398
+ };
14399
+ var STATE_LABEL = {
14400
+ good: "Good",
14401
+ slowing: "Slowing",
14402
+ grinding: "Grinding",
14403
+ "form-breakdown": "Form breaking down"
14404
+ };
14405
+ var PHASE_AXIS_COLOR = {
14406
+ eccentric: primitiveRamps.magenta[800],
14407
+ concentric: primitiveRamps.cyan[800],
14408
+ hold: primitiveColors.charcoal[100],
14409
+ idle: primitiveColors.charcoal[200]
14410
+ };
14411
+ var PHASE_AXIS_BASE_COLOR = {
14412
+ eccentric: primitiveRamps.magenta[950],
14413
+ concentric: primitiveRamps.cyan[950],
14414
+ hold: primitiveColors.charcoal[300],
14415
+ idle: primitiveColors.charcoal[200]
14416
+ };
14417
+ var PACING_TONE = {
14418
+ ahead: primitiveRamps.amber[300],
14419
+ onPace: primitiveRamps.green[300],
14420
+ over: primitiveRamps.red[300]
14421
+ };
14422
+ var SILVER = primitiveColors.neutral[300];
14423
+ var RED_LIGHT = primitiveRamps.red[400];
14424
+ var RED_MID = primitiveRamps.red[600];
14425
+ var RED_DEEP = primitiveRamps.red[800];
14426
+ var GRIND_THRESHOLD = 0.35;
14427
+ var DRIFT_GREY = primitiveColors.neutral[600];
14428
+ function clamp012(v) {
14429
+ return Math.min(1, Math.max(0, v));
14430
+ }
14431
+ function hexToRgb2(hex) {
14432
+ const h = hex.replace("#", "");
14433
+ const f = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
14434
+ return [parseInt(f.slice(0, 2), 16), parseInt(f.slice(2, 4), 16), parseInt(f.slice(4, 6), 16)];
14435
+ }
14436
+ function mixHex2(a, b, ratio) {
14437
+ const [ar, ag, ab] = hexToRgb2(a);
14438
+ const [br, bg, bb] = hexToRgb2(b);
14439
+ const m = (x, y) => Math.round(x + (y - x) * clamp012(ratio));
14440
+ return `#${[m(ar, br), m(ag, bg), m(ab, bb)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
14441
+ }
14442
+ function ghostLineColor(tempoDeviation, grindSignature) {
14443
+ if (grindSignature >= GRIND_THRESHOLD) {
14444
+ const severity = clamp012((grindSignature - GRIND_THRESHOLD) / (1 - GRIND_THRESHOLD));
14445
+ return severity < 0.5 ? mixHex2(RED_LIGHT, RED_MID, severity * 2) : mixHex2(RED_MID, RED_DEEP, (severity - 0.5) * 2);
14446
+ }
14447
+ return mixHex2(SILVER, DRIFT_GREY, (tempoDeviation ?? 0) * 0.55);
14448
+ }
14449
+ function auraForVerdict(state) {
14450
+ if (state == null || state === "good") return "productive";
14451
+ if (state === "form-breakdown") return "stop";
14452
+ return "threshold";
14453
+ }
14454
+
14455
+ // src/components/custom/Fatigue/VerdictHero.tsx
14456
+ var t18 = getSemanticColors("dark");
14457
+ function formatRpe(rpe) {
14458
+ if (rpe == null) return "\u2014";
14459
+ return (Math.round(rpe * 2) / 2).toFixed(1);
14460
+ }
14461
+ function VerdictHero({ rpe, verdict }) {
14462
+ const tone = verdict ? TONE_COLOR[verdict.tone] : t18["text-tertiary"];
14463
+ const word = verdict ? STATE_LABEL[verdict.state] : "Warming up";
14464
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 6 }, testID: "verdict-hero", children: [
14465
+ /* @__PURE__ */ jsx(
14466
+ reactNative.Text,
14467
+ {
14468
+ style: {
14469
+ fontSize: 9,
14470
+ letterSpacing: 1.5,
14471
+ fontFamily: FONT_MONO,
14472
+ color: t18["text-tertiary"]
14473
+ },
14474
+ children: "FATIGUE"
14475
+ }
14476
+ ),
14477
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 2 }, children: [
14478
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", alignItems: "flex-end", gap: 7 }, children: [
14479
+ /* @__PURE__ */ jsx(
14480
+ reactNative.Text,
14481
+ {
14482
+ style: {
14483
+ fontSize: 62,
14484
+ fontWeight: "900",
14485
+ fontFamily: FONT_HEAD,
14486
+ color: tone,
14487
+ lineHeight: 60
14488
+ },
14489
+ accessibilityLabel: `RPE ${formatRpe(rpe)}`,
14490
+ children: formatRpe(rpe)
14491
+ }
14492
+ ),
14493
+ /* @__PURE__ */ jsx(
14494
+ reactNative.Text,
14495
+ {
14496
+ style: {
14497
+ fontSize: 18,
14498
+ fontWeight: "800",
14499
+ color: tone,
14500
+ marginBottom: 9,
14501
+ fontFamily: FONT_HEAD
14502
+ },
14503
+ children: "RPE"
14504
+ }
14505
+ )
14506
+ ] }),
14507
+ /* @__PURE__ */ jsx(reactNative.Text, { style: { fontSize: 17, fontWeight: "800", fontFamily: FONT_HEAD, color: tone }, children: word })
14508
+ ] })
14509
+ ] });
14510
+ }
14511
+ var t19 = getSemanticColors("dark");
14512
+ var TONE_VARIANT = {
14513
+ ok: "success",
14514
+ warn: "warning",
14515
+ alarm: "error"
14516
+ };
14517
+ var TONE_WORD = { ok: "ok", warn: "watch", alarm: "alarm" };
14518
+ function Light({
14519
+ label,
14520
+ tone,
14521
+ detail
14522
+ }) {
14523
+ const variant = tone ? TONE_VARIANT[tone] : "neutral";
14524
+ const word = tone ? TONE_WORD[tone] : "warming up";
14525
+ return /* @__PURE__ */ jsx(Tooltip, { label: `${detail} \xB7 ${word}`, placement: "bottom", children: /* @__PURE__ */ jsxs(
14526
+ reactNative.View,
14527
+ {
14528
+ accessibilityLabel: `${detail}, ${word}`,
14529
+ style: { flexDirection: "row", alignItems: "center", gap: 5 },
14530
+ children: [
14531
+ /* @__PURE__ */ jsx(StatusDot, { variant, size: "sm", glow: true }),
14532
+ /* @__PURE__ */ jsx(
14533
+ reactNative.Text,
14534
+ {
14535
+ style: {
14536
+ fontSize: 9,
14537
+ letterSpacing: 0.6,
14538
+ fontFamily: FONT_MONO,
14539
+ color: t19["text-secondary"]
14540
+ },
14541
+ children: label
14542
+ }
14543
+ )
14544
+ ]
14545
+ }
14546
+ ) });
14547
+ }
14548
+ function FatigueLights({ dimensions, spread = false }) {
14549
+ return /* @__PURE__ */ jsxs(
14550
+ reactNative.View,
14551
+ {
14552
+ testID: "fatigue-lights",
14553
+ style: {
14554
+ flexDirection: "row",
14555
+ gap: spread ? 0 : 16,
14556
+ alignItems: "center",
14557
+ justifyContent: spread ? "space-between" : "flex-start"
14558
+ },
14559
+ children: [
14560
+ /* @__PURE__ */ jsx(Light, { label: "VEL", tone: dimensions?.velocityLoss ?? null, detail: "Velocity loss" }),
14561
+ /* @__PURE__ */ jsx(Light, { label: "ROM", tone: dimensions?.rom ?? null, detail: "ROM depth" }),
14562
+ /* @__PURE__ */ jsx(Light, { label: "TEMPO", tone: dimensions?.tempo ?? null, detail: "Tempo" })
14563
+ ]
14564
+ }
14565
+ );
14566
+ }
14567
+ function barColor(romM, working, short) {
14568
+ if (short != null && romM < short) return RED_DEEP;
14569
+ if (working != null && romM < working) return RED_LIGHT;
14570
+ return SILVER;
14571
+ }
14572
+ function romReferenceOverlay(g, workingStandardM, shortThresholdM) {
14573
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
14574
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14575
+ reactNative.View,
14576
+ {
14577
+ pointerEvents: "none",
14578
+ style: {
14579
+ position: "absolute",
14580
+ left: 0,
14581
+ right: 0,
14582
+ bottom: 0,
14583
+ height: g.yOf(shortThresholdM),
14584
+ backgroundColor: alpha(RED_DEEP, 0.06)
14585
+ }
14586
+ }
14587
+ ),
14588
+ workingStandardM != null && /* @__PURE__ */ jsx(
14589
+ reactNative.View,
14590
+ {
14591
+ pointerEvents: "none",
14592
+ style: {
14593
+ position: "absolute",
14594
+ left: 0,
14595
+ right: 0,
14596
+ bottom: g.yOf(workingStandardM),
14597
+ borderTopWidth: 1,
14598
+ borderStyle: "dashed",
14599
+ borderColor: alpha(SILVER, 0.4)
14600
+ }
14601
+ }
14602
+ ),
14603
+ shortThresholdM != null && /* @__PURE__ */ jsx(
14604
+ reactNative.View,
14605
+ {
14606
+ pointerEvents: "none",
14607
+ style: {
14608
+ position: "absolute",
14609
+ left: 0,
14610
+ right: 0,
14611
+ bottom: g.yOf(shortThresholdM),
14612
+ borderTopWidth: 1,
14613
+ borderStyle: "dashed",
14614
+ borderColor: alpha(RED_DEEP, 0.4)
14615
+ }
14616
+ }
14617
+ )
14618
+ ] });
14619
+ }
14620
+ function RomProgressionChart({
14621
+ points,
14622
+ workingStandardM,
14623
+ shortThresholdM,
14624
+ barHeight = 44,
14625
+ maxReps = 12,
14626
+ plannedReps: plannedReps2
14627
+ }) {
14628
+ const shown = points.slice(-maxReps);
14629
+ const slots = shown.map((p) => ({ kind: "rep", value: p.romM }));
14630
+ const scaleMax = Math.max(...shown.map((p) => p.romM), workingStandardM ?? 0, shortThresholdM ?? 0) || void 0;
14631
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "rom-progression", children: /* @__PURE__ */ jsx(
14632
+ SetBarChart,
14633
+ {
14634
+ slots,
14635
+ colorFor: (romM) => barColor(romM, workingStandardM, shortThresholdM),
14636
+ height: barHeight,
14637
+ scaleMax,
14638
+ barRadius: 2,
14639
+ hideBaseline: true,
14640
+ targetReps: plannedReps2,
14641
+ renderReference: (g) => romReferenceOverlay(g, workingStandardM, shortThresholdM),
14642
+ testIDPrefix: "rom"
14643
+ }
14644
+ ) });
14645
+ }
14646
+
14647
+ // src/components/custom/Fatigue/tempo-pacing.ts
14648
+ var t20 = getSemanticColors("dark");
14649
+ var ON_TARGET_MS2 = 100;
14650
+ function phaseFillFraction(elapsedMs, targetMs) {
14651
+ if (targetMs == null || targetMs <= 0) return 1;
14652
+ return Math.min(1, Math.max(0, elapsedMs / targetMs));
14653
+ }
14654
+ function pacingTone(elapsedMs, targetMs) {
14655
+ if (targetMs == null || targetMs <= 0) return t20["text-primary"];
14656
+ const remainingMs = targetMs - elapsedMs;
14657
+ if (remainingMs > ON_TARGET_MS2) return PACING_TONE.ahead;
14658
+ if (remainingMs >= -ON_TARGET_MS2) return PACING_TONE.onPace;
14659
+ return PACING_TONE.over;
14660
+ }
14661
+ function prescribedSegments(tempo) {
14662
+ if (tempo == null) return [];
14663
+ const [ecc, pauseBottom, con, pauseTop] = tempo;
14664
+ const order = [
14665
+ ["eccentric", ecc],
14666
+ ["hold", pauseBottom],
14667
+ ["concentric", con],
14668
+ ["hold", pauseTop]
14669
+ ];
14670
+ const out = [];
14671
+ let t26 = 0;
14672
+ for (const [phase, seconds] of order) {
14673
+ const ms = seconds * 1e3;
14674
+ if (ms > 0) out.push({ phase, startMs: t26, endMs: t26 + ms });
14675
+ t26 += ms;
14676
+ }
14677
+ return out;
14678
+ }
14679
+ function phaseTargetsMs(phases, tempo) {
14680
+ if (tempo == null) return phases.map(() => null);
14681
+ const [eccS, pauseBottomS, conS, pauseTopS] = tempo;
14682
+ let seenConcentric = false;
14683
+ return phases.map((phase) => {
14684
+ switch (phase) {
14685
+ case "eccentric":
14686
+ return eccS * 1e3;
14687
+ case "concentric":
14688
+ seenConcentric = true;
14689
+ return conS * 1e3;
14690
+ case "hold":
14691
+ return (seenConcentric ? pauseTopS : pauseBottomS) * 1e3;
14692
+ default:
14693
+ return null;
14694
+ }
14695
+ });
14696
+ }
14697
+
14698
+ // src/components/custom/Fatigue/GhostBand.tsx
14699
+ var t21 = getSemanticColors("dark");
14700
+ var BAND_H = 16;
14701
+ var BAND_GAP = 4;
14702
+ var PHASE_LABEL = {
14703
+ eccentric: "ECC",
14704
+ concentric: "CON"
14705
+ };
14706
+ var LABEL_CH_PX = 7;
14707
+ var LABEL_PAD_PX = 6;
14708
+ var WELL_SHADE = primitiveColors.black;
14709
+ var WELL_FLOOR_LIGHT = primitiveColors.white;
14710
+ function labelFits(label, segW) {
14711
+ return segW >= label.length * LABEL_CH_PX + LABEL_PAD_PX;
14712
+ }
14713
+ function GhostBand({
14714
+ segments,
14715
+ x,
14716
+ top,
14717
+ height = BAND_H,
14718
+ showLabels = false,
14719
+ labelColor = t21["text-primary"],
14720
+ targetTempoSeconds = null,
14721
+ prescribed = false
14722
+ }) {
14723
+ const rawId = React25.useId();
14724
+ const safeId = rawId.replace(/[^a-zA-Z0-9]/g, "");
14725
+ const clipId = `ghost-band-${safeId}`;
14726
+ const wellId = `ghost-band-well-${safeId}`;
14727
+ const drawn = segments.filter((seg) => x(seg.endMs) - x(seg.startMs) > 0);
14728
+ if (drawn.length === 0) return null;
14729
+ const bandLeft = x(drawn[0].startMs);
14730
+ const bandRight = x(drawn[drawn.length - 1].endMs);
14731
+ const bandW = bandRight - bandLeft;
14732
+ if (bandW <= 0) return null;
14733
+ const pacing = targetTempoSeconds != null && !prescribed;
14734
+ const targetsMs = phaseTargetsMs(
14735
+ drawn.map((seg) => seg.phase),
14736
+ targetTempoSeconds
14737
+ );
14738
+ const runs = drawn.map((seg, i) => {
14739
+ const left = x(seg.startMs);
14740
+ const right = i === drawn.length - 1 ? bandRight : x(drawn[i + 1].startMs);
14741
+ const width = Math.max(0, right - left);
14742
+ const targetMs = targetsMs[i];
14743
+ const elapsedMs = seg.endMs - seg.startMs;
14744
+ return {
14745
+ phase: seg.phase,
14746
+ left,
14747
+ width,
14748
+ // Prescribed → nothing performed, so nothing filled. No target at all → the run reads
14749
+ // complete rather than perpetually empty.
14750
+ fillWidth: prescribed ? 0 : pacing ? width * phaseFillFraction(elapsedMs, targetMs) : width,
14751
+ labelTone: pacing ? pacingTone(elapsedMs, targetMs) : labelColor
14752
+ };
14753
+ });
14754
+ return /* @__PURE__ */ jsxs("g", { children: [
14755
+ /* @__PURE__ */ jsxs("defs", { children: [
14756
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: bandLeft, y: top, width: bandW, height, rx: 2 }) }),
14757
+ /* @__PURE__ */ jsxs("linearGradient", { id: wellId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
14758
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: WELL_SHADE, stopOpacity: 0.6 }),
14759
+ /* @__PURE__ */ jsx("stop", { offset: "42%", stopColor: WELL_SHADE, stopOpacity: 0.12 }),
14760
+ /* @__PURE__ */ jsx("stop", { offset: "88%", stopColor: WELL_SHADE, stopOpacity: 0.02 }),
14761
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: WELL_FLOOR_LIGHT, stopOpacity: 0.07 })
14762
+ ] })
14763
+ ] }),
14764
+ /* @__PURE__ */ jsxs("g", { clipPath: `url(#${clipId})`, children: [
14765
+ /* @__PURE__ */ jsx(
14766
+ "rect",
14767
+ {
14768
+ x: bandLeft,
14769
+ y: top,
14770
+ width: bandW,
14771
+ height,
14772
+ fill: PHASE_AXIS_COLOR.idle,
14773
+ "data-testid": "ghost-band-floor"
14774
+ }
14775
+ ),
14776
+ runs.map((run, i) => /* @__PURE__ */ jsxs("g", { children: [
14777
+ /* @__PURE__ */ jsx(
14778
+ "rect",
14779
+ {
14780
+ x: run.left,
14781
+ y: top,
14782
+ width: run.width,
14783
+ height,
14784
+ fill: PHASE_AXIS_BASE_COLOR[run.phase],
14785
+ "data-testid": "ghost-band-base"
14786
+ }
14787
+ ),
14788
+ /* @__PURE__ */ jsx(
14789
+ "rect",
14790
+ {
14791
+ x: run.left,
14792
+ y: top,
14793
+ width: run.width,
14794
+ height,
14795
+ fill: `url(#${wellId})`,
14796
+ "data-testid": "ghost-band-well"
14797
+ }
14798
+ ),
14799
+ /* @__PURE__ */ jsx(
14800
+ "rect",
14801
+ {
14802
+ x: run.left,
14803
+ y: top,
14804
+ width: run.fillWidth,
14805
+ height,
14806
+ fill: PHASE_AXIS_COLOR[run.phase],
14807
+ "data-testid": "ghost-band-fill"
14808
+ }
14809
+ )
14810
+ ] }, i)),
14811
+ showLabels && runs.map((run, i) => {
14812
+ const label = PHASE_LABEL[run.phase];
14813
+ if (!label || !labelFits(label, run.width)) return null;
14814
+ return /* @__PURE__ */ jsx(
14815
+ "text",
14816
+ {
14817
+ x: run.left + run.width / 2,
14818
+ y: top + height / 2,
14819
+ textAnchor: "middle",
14820
+ dominantBaseline: "central",
14821
+ fill: run.labelTone,
14822
+ fontSize: 8,
14823
+ fontWeight: 800,
14824
+ letterSpacing: 1,
14825
+ fontFamily: FONT_UI,
14826
+ children: label
14827
+ },
14828
+ i
14829
+ );
14830
+ })
14831
+ ] })
14832
+ ] });
14833
+ }
14834
+ var t22 = getSemanticColors("dark");
14835
+ var PARCH = t22["text-primary"];
14836
+ function smoothPath(pts) {
14837
+ if (pts.length < 2) return pts.length ? `M ${pts[0][0]} ${pts[0][1]}` : "";
14838
+ let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
14839
+ for (let i = 0; i < pts.length - 1; i++) {
14840
+ const p0 = pts[i - 1] ?? pts[i];
14841
+ const p1 = pts[i];
14842
+ const p2 = pts[i + 1];
14843
+ const p3 = pts[i + 2] ?? p2;
14844
+ const c1x = p1[0] + (p2[0] - p0[0]) / 6;
14845
+ const c1y = p1[1] + (p2[1] - p0[1]) / 6;
14846
+ const c2x = p2[0] - (p3[0] - p1[0]) / 6;
14847
+ const c2y = p2[1] - (p3[1] - p1[1]) / 6;
14848
+ d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
14849
+ }
14850
+ return d;
14851
+ }
14852
+ var BLUR_STD_DEV = 2.6;
14853
+ function GhostBloom({
14854
+ current,
14855
+ ghosts = [],
14856
+ tint,
14857
+ baseline,
14858
+ orientation = "up",
14859
+ ghostStroke = (i) => alpha(PARCH, 0.1 + i * 0.015),
14860
+ lineWidth = 3.5
14861
+ }) {
14862
+ const rawId = React25.useId();
14863
+ const blurId = `bloom-blur-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`;
14864
+ const toY = (mag) => orientation === "down" ? baseline + mag : baseline - mag;
14865
+ const toAbs = (pts) => pts.map(([px, mag]) => [px, toY(mag)]);
14866
+ const curD = smoothPath(toAbs(current));
14867
+ const ghostDs = ghosts.map((g) => smoothPath(toAbs(g)));
14868
+ return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
14869
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("filter", { id: blurId, x: "-50%", y: "-50%", width: "200%", height: "200%", children: /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: BLUR_STD_DEV }) }) }),
14870
+ ghostDs.map((d, i) => /* @__PURE__ */ jsx(
14871
+ "path",
14872
+ {
14873
+ d,
14874
+ fill: "none",
14875
+ stroke: ghostStroke(i),
14876
+ strokeWidth: 1.5,
14877
+ strokeLinejoin: "round"
14878
+ },
14879
+ i
14880
+ )),
14881
+ /* @__PURE__ */ jsx(
14882
+ "path",
14883
+ {
14884
+ d: curD,
14885
+ fill: "none",
14886
+ stroke: alpha(primitiveColors.black, 0.3),
14887
+ strokeWidth: lineWidth + 3,
14888
+ strokeLinejoin: "round",
14889
+ strokeLinecap: "round",
14890
+ filter: `url(#${blurId})`
14891
+ }
14892
+ ),
14893
+ /* @__PURE__ */ jsx(
14894
+ "path",
14895
+ {
14896
+ d: curD,
14897
+ fill: "none",
14898
+ stroke: tint,
14899
+ strokeWidth: lineWidth,
14900
+ strokeLinejoin: "round",
14901
+ strokeLinecap: "round"
14902
+ }
14903
+ )
14904
+ ] });
14905
+ }
14906
+
14907
+ // src/components/custom/Fatigue/GhostSpark.tsx
14908
+ function GhostSpark({
14909
+ curves,
14910
+ width,
14911
+ height = 172,
14912
+ targetTempoSeconds = null
13714
14913
  }) {
13715
- const { mev, mav, mrv } = landmarks;
13716
- const max = scaleMax ?? mrv * 1.2;
13717
- const zone = zoneForSets(currentSets, landmarks);
13718
- const fillColor = HEAT_BY_ZONE[zone];
13719
- const pct2 = mav > 0 ? Math.round(currentSets / mav * 100) : 0;
14914
+ const w = width;
14915
+ const h = height;
14916
+ const padL = 12;
14917
+ const padR = 8;
14918
+ const padTop = 10;
14919
+ const padBot = 6;
14920
+ if (curves.length === 0) {
14921
+ const prescribed = prescribedSegments(targetTempoSeconds);
14922
+ if (prescribed.length === 0) {
14923
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { width: w, height: h } });
14924
+ }
14925
+ const totalMs = prescribed[prescribed.length - 1].endMs;
14926
+ const bandTopEmpty = h - padBot - BAND_H;
14927
+ const xEmpty = (ms) => padL + ms / (totalMs * 1.04) * (w - padL - padR);
14928
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsx("svg", { width: w, height: h, children: /* @__PURE__ */ jsx(
14929
+ GhostBand,
14930
+ {
14931
+ segments: prescribed,
14932
+ x: xEmpty,
14933
+ top: bandTopEmpty,
14934
+ showLabels: true,
14935
+ targetTempoSeconds,
14936
+ prescribed: true
14937
+ }
14938
+ ) }) });
14939
+ }
14940
+ const cur = curves[curves.length - 1];
14941
+ const allSamples = curves.flatMap((c) => c.samples);
14942
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
14943
+ const axisMaxT = Math.max(1, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0)) * 1.04;
14944
+ const bandBottom = h - padBot;
14945
+ const bandTop = bandBottom - BAND_H;
14946
+ const baseline = bandTop - BAND_GAP;
14947
+ const plotH = Math.max(1, baseline - padTop);
14948
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
14949
+ const mag = (v) => clamp012(v / vmax) * plotH;
14950
+ const lineTint = ghostLineColor(cur.tempoDeviation, cur.grindSignature);
14951
+ const curPts = cur.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
14952
+ const ghostPts = curves.slice(0, -1).map((c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]));
14953
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
14954
+ /* @__PURE__ */ jsx(
14955
+ GhostBloom,
14956
+ {
14957
+ current: curPts,
14958
+ ghosts: ghostPts,
14959
+ tint: lineTint,
14960
+ baseline,
14961
+ orientation: "up"
14962
+ }
14963
+ ),
14964
+ /* @__PURE__ */ jsx(
14965
+ GhostBand,
14966
+ {
14967
+ segments: cur.phaseSegments,
14968
+ x,
14969
+ top: bandTop,
14970
+ showLabels: true,
14971
+ targetTempoSeconds
14972
+ }
14973
+ )
14974
+ ] }) });
14975
+ }
14976
+
14977
+ // src/components/custom/Fatigue/LiveFatigueCard.tsx
14978
+ var t23 = getSemanticColors("dark");
14979
+ var PAD = 18;
14980
+ var GHOST_GUTTER = 4;
14981
+ function LiveFatigueCard({ model, width = 318, height }) {
14982
+ const chartW = width - PAD * 2 - GHOST_GUTTER * 2;
14983
+ const chartH = height != null ? Math.round(Math.min(240, Math.max(168, height * 0.4))) : 172;
13720
14984
  return /* @__PURE__ */ jsxs(
13721
- reactNative.View,
14985
+ Surface,
13722
14986
  {
13723
- className,
13724
- style: [{ width, gap: 3 }, style],
13725
- testID: "volume-landmark-bar",
13726
- ...props,
14987
+ level: "base",
14988
+ testID: "live-fatigue-card",
14989
+ style: {
14990
+ width,
14991
+ height,
14992
+ borderRadius: 14,
14993
+ padding: PAD,
14994
+ borderWidth: 1,
14995
+ borderColor: t23["hairline-default"],
14996
+ ...barPaper(t23["surface-base"])
14997
+ },
13727
14998
  children: [
14999
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 12 }, children: [
15000
+ /* @__PURE__ */ jsx(VerdictHero, { rpe: model.rpe, verdict: model.verdict }),
15001
+ /* @__PURE__ */ jsx(FatigueLights, { dimensions: model.verdict?.dimensions ?? null })
15002
+ ] }),
15003
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, minHeight: 16 } }),
13728
15004
  /* @__PURE__ */ jsx(
13729
- DataRow,
15005
+ RomProgressionChart,
13730
15006
  {
13731
- label: muscle,
13732
- value: /* @__PURE__ */ jsxs(
13733
- reactNative.Text,
13734
- {
13735
- testID: "volume-landmark-pct",
13736
- style: { fontSize: 14, fontWeight: "700", fontFamily: MONO2, color: fillColor },
13737
- children: [
13738
- pct2,
13739
- "%"
13740
- ]
13741
- }
13742
- ),
13743
- className: "py-0",
13744
- testID: "volume-landmark-header"
15007
+ points: model.romProgression,
15008
+ workingStandardM: model.romWorkingStandardM,
15009
+ shortThresholdM: model.romShortThresholdM,
15010
+ plannedReps: model.plannedReps
13745
15011
  }
13746
15012
  ),
15013
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, minHeight: 16 } }),
13747
15014
  /* @__PURE__ */ jsx(
13748
- ZoneTrack,
15015
+ GhostSpark,
13749
15016
  {
13750
- zones: [{ upTo: max, color: NEUTRAL_TRACK }],
13751
- max,
13752
- marker: {
13753
- type: "fill",
13754
- value: currentSets,
13755
- color: fillColor,
13756
- // Glow when in the optimal productive band — the "sweet spot" cue.
13757
- glow: zone === "productive"
13758
- },
13759
- trackColor: NEUTRAL_TRACK,
13760
- trackHeight,
13761
- ticks: [
13762
- { value: mev, label: "MEV", tooltip: `${LANDMARK_NAME.MEV} \xB7 ${mev} sets/wk` },
13763
- {
13764
- value: mav,
13765
- label: "MAV",
13766
- emphasized: true,
13767
- tooltip: `${LANDMARK_NAME.MAV} (target) \xB7 ${mav} sets/wk`
13768
- },
13769
- { value: mrv, label: "MRV", tooltip: `${LANDMARK_NAME.MRV} \xB7 ${mrv} sets/wk` }
13770
- ],
13771
- accessibilityLabel: `${muscle} weekly volume: ${currentSets} sets, ${pct2}% of MAV target, ${ZONE_DESCRIPTION[zone]}`,
13772
- testID: "volume-landmark-track"
15017
+ curves: model.velocityCurves,
15018
+ width: chartW,
15019
+ height: chartH,
15020
+ targetTempoSeconds: model.targetTempoSeconds
13773
15021
  }
13774
15022
  )
13775
15023
  ]
13776
15024
  }
13777
15025
  );
13778
15026
  }
13779
- var t15 = getSemanticColors("dark");
13780
- var statusToken = {
13781
- productive: "status-success",
13782
- threshold: "status-warning",
13783
- stop: "status-error"
13784
- };
13785
- var statusDotVariant = {
13786
- productive: "success",
13787
- threshold: "warning",
13788
- stop: "error"
13789
- };
13790
- var defaultLabel = {
13791
- productive: "Productive",
13792
- threshold: "Threshold",
13793
- stop: "Stop"
13794
- };
13795
- function statusPillColor(status) {
13796
- return t15[statusToken[status]];
13797
- }
13798
- function StatusPill2({ status, label, className, style, ...props }) {
13799
- const color = statusPillColor(status);
13800
- const text = label ?? defaultLabel[status];
15027
+
15028
+ // src/components/custom/Fatigue/LiveFatiguePanel.tsx
15029
+ var t24 = getSemanticColors("dark");
15030
+ function ExerciseHeaderLite({ header }) {
13801
15031
  return /* @__PURE__ */ jsxs(
13802
15032
  reactNative.View,
13803
15033
  {
13804
- testID: "status-pill",
13805
- className,
13806
- style: [
13807
- {
13808
- flexDirection: "row",
13809
- alignItems: "center",
13810
- gap: 7,
13811
- alignSelf: "flex-start",
13812
- paddingVertical: 7,
13813
- paddingHorizontal: 13,
13814
- borderRadius: 9,
13815
- backgroundColor: alpha(color, 0.14),
13816
- borderWidth: 1,
13817
- borderColor: alpha(color, 0.4)
13818
- },
13819
- style
13820
- ],
13821
- ...props,
15034
+ style: {
15035
+ flexDirection: "row",
15036
+ alignItems: "flex-end",
15037
+ justifyContent: "space-between",
15038
+ paddingHorizontal: 24,
15039
+ paddingTop: 18,
15040
+ paddingBottom: 14,
15041
+ borderBottomWidth: 1,
15042
+ borderColor: alpha(t24["text-primary"], 0.08)
15043
+ },
13822
15044
  children: [
13823
- /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, children: /* @__PURE__ */ jsx(StatusDot, { variant: statusDotVariant[status], glow: true }) }),
13824
- /* @__PURE__ */ jsx(reactNative.Text, { testID: "status-pill-label", style: { fontSize: 13, fontWeight: "800", color }, children: text })
15045
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 2 }, children: [
15046
+ /* @__PURE__ */ jsx(
15047
+ reactNative.Text,
15048
+ {
15049
+ style: {
15050
+ fontSize: 28,
15051
+ fontWeight: "700",
15052
+ fontFamily: FONT_HEAD,
15053
+ color: t24["text-primary"]
15054
+ },
15055
+ children: header.title
15056
+ }
15057
+ ),
15058
+ header.subtitle && /* @__PURE__ */ jsx(
15059
+ reactNative.Text,
15060
+ {
15061
+ style: {
15062
+ fontSize: 13,
15063
+ fontWeight: "600",
15064
+ fontFamily: FONT_UI,
15065
+ color: t24["text-secondary"]
15066
+ },
15067
+ children: header.subtitle
15068
+ }
15069
+ )
15070
+ ] }),
15071
+ header.meta && /* @__PURE__ */ jsx(
15072
+ reactNative.Text,
15073
+ {
15074
+ style: {
15075
+ fontSize: 13,
15076
+ fontWeight: "800",
15077
+ fontFamily: FONT_MONO,
15078
+ color: t24["text-tertiary"]
15079
+ },
15080
+ children: header.meta
15081
+ }
15082
+ )
13825
15083
  ]
13826
15084
  }
13827
15085
  );
13828
15086
  }
13829
- var t16 = getSemanticColors("dark");
13830
- var categoryToken = {
13831
- productive: null,
13832
- threshold: "status-warning",
13833
- stop: "status-error"
13834
- };
13835
- var floodOpacity = {
13836
- productive: 0,
13837
- threshold: 0.12,
13838
- stop: 0.18
13839
- };
13840
- function liveAuraColor(category) {
13841
- const token = categoryToken[category];
13842
- return token ? t16[token] : null;
13843
- }
13844
- function LiveAuraFrame({
13845
- category,
13846
- pulse = true,
13847
- className,
13848
- children,
13849
- style,
13850
- ...props
15087
+ function LiveFatiguePanel({
15088
+ model,
15089
+ velocity,
15090
+ header,
15091
+ aura,
15092
+ bodyHeight = 508,
15093
+ cardWidth = 318
13851
15094
  }) {
13852
- const color = liveAuraColor(category);
13853
- const tint = color ? alpha(color, floodOpacity[category]) : "transparent";
13854
- const floodStyle = {
13855
- ...reactNative.StyleSheet.absoluteFillObject,
13856
- ...reactNative.Platform.OS === "web" ? (
13857
- // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
13858
- { backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
13859
- ) : { backgroundColor: tint }
13860
- };
13861
- return /* @__PURE__ */ jsxs(
13862
- reactNative.View,
15095
+ const category = aura ?? auraForVerdict(model.verdict?.state ?? null);
15096
+ const heroH = bodyHeight - 26;
15097
+ return /* @__PURE__ */ jsx(
15098
+ LiveAuraFrame,
13863
15099
  {
13864
- testID: "live-aura-frame",
13865
- className,
13866
- style: [
13867
- {
13868
- position: "relative",
13869
- overflow: "hidden",
13870
- borderRadius: 10,
13871
- backgroundColor: t16["background-base"],
13872
- borderWidth: 1,
13873
- borderColor: t16["border-default"]
13874
- },
13875
- style
13876
- ],
13877
- ...props,
13878
- children: [
13879
- color && /* @__PURE__ */ jsx(
13880
- reactNative.View,
13881
- {
13882
- testID: "live-aura-flood",
13883
- accessibilityElementsHidden: true,
13884
- pointerEvents: "none",
13885
- className: category === "stop" && pulse ? "animate-pulse" : void 0,
13886
- style: floodStyle
13887
- }
13888
- ),
13889
- children
13890
- ]
15100
+ category,
15101
+ style: { borderRadius: 0, borderWidth: 0 },
15102
+ testID: "live-fatigue-panel",
15103
+ children: /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1 }, children: [
15104
+ header && /* @__PURE__ */ jsx(ExerciseHeaderLite, { header }),
15105
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { padding: 24, flexDirection: "row", gap: 18, alignItems: "stretch" }, children: [
15106
+ /* @__PURE__ */ jsxs(reactNative.View, { style: { flex: 1, gap: 8 }, children: [
15107
+ /* @__PURE__ */ jsx(
15108
+ reactNative.Text,
15109
+ {
15110
+ style: {
15111
+ fontSize: 9,
15112
+ letterSpacing: 1.2,
15113
+ fontFamily: FONT_MONO,
15114
+ color: t24["text-tertiary"]
15115
+ },
15116
+ children: "VELOCITY \xB7 this set"
15117
+ }
15118
+ ),
15119
+ /* @__PURE__ */ jsx(
15120
+ VelocityHero,
15121
+ {
15122
+ velocities: velocity.velocities,
15123
+ targetReps: velocity.targetReps,
15124
+ liveRepIndex: velocity.liveRepIndex,
15125
+ height: heroH
15126
+ }
15127
+ )
15128
+ ] }),
15129
+ /* @__PURE__ */ jsx(LiveFatigueCard, { model, width: cardWidth, height: bodyHeight })
15130
+ ] })
15131
+ ] })
13891
15132
  }
13892
15133
  );
13893
15134
  }
15135
+ var t25 = getSemanticColors("dark");
15136
+ function phaseAt(segments, ms) {
15137
+ const hit = segments.find((s) => ms >= s.startMs && ms < s.endMs);
15138
+ return hit ? hit.phase : null;
15139
+ }
15140
+ function mergePhaseSegments(a, b) {
15141
+ if (a.length === 0) return b;
15142
+ if (b.length === 0) return a;
15143
+ const bounds = Array.from(new Set([...a, ...b].flatMap((s) => [s.startMs, s.endMs]))).sort(
15144
+ (p, q) => p - q
15145
+ );
15146
+ const out = [];
15147
+ for (let i = 0; i < bounds.length - 1; i++) {
15148
+ const startMs = bounds[i];
15149
+ const endMs = bounds[i + 1];
15150
+ if (endMs <= startMs) continue;
15151
+ const mid = (startMs + endMs) / 2;
15152
+ const pa = phaseAt(a, mid);
15153
+ const pb = phaseAt(b, mid);
15154
+ const phase = pa == null ? pb ?? "idle" : pb == null ? pa : pa === pb ? pa : "idle";
15155
+ const last = out[out.length - 1];
15156
+ if (last && last.phase === phase && last.endMs === startMs) last.endMs = endMs;
15157
+ else out.push({ phase, startMs, endMs });
15158
+ }
15159
+ return out;
15160
+ }
15161
+ function lastTMs(curves) {
15162
+ return Math.max(0, ...curves.map((c) => c.samples[c.samples.length - 1]?.tMs ?? 0));
15163
+ }
15164
+ function DualGhostSpark({
15165
+ left,
15166
+ right,
15167
+ width,
15168
+ height = 232,
15169
+ leftLabel = "LEFT",
15170
+ rightLabel = "RIGHT",
15171
+ showDeviceLabels = true,
15172
+ targetTempoSeconds = null
15173
+ }) {
15174
+ const w = width;
15175
+ const h = height;
15176
+ const padL = 14;
15177
+ const padR = 14;
15178
+ const padTop = 22;
15179
+ const padBot = 22;
15180
+ if (left.length === 0 && right.length === 0) {
15181
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "dual-ghost-spark", style: { width: w, height: h } });
15182
+ }
15183
+ const mid = padTop + (h - padTop - padBot) / 2;
15184
+ const bandTop = mid - BAND_H / 2;
15185
+ const baseUp = bandTop - BAND_GAP;
15186
+ const baseDown = mid + BAND_H / 2 + BAND_GAP;
15187
+ const wingH = Math.max(1, Math.min(baseUp - padTop, h - padBot - baseDown));
15188
+ const allSamples = [...left, ...right].flatMap((c) => c.samples);
15189
+ const vmax = Math.max(0.01, ...allSamples.map((s) => s.velocityMps)) * 1.06;
15190
+ const axisMaxT = Math.max(1, lastTMs(left), lastTMs(right)) * 1.04;
15191
+ const x = (ms) => padL + ms / axisMaxT * (w - padL - padR);
15192
+ const mag = (v) => clamp012(v / vmax) * wingH;
15193
+ const toPts = (c) => c.samples.map((s) => [x(s.tMs), mag(s.velocityMps)]);
15194
+ const wing = (curves) => {
15195
+ const cur = curves[curves.length - 1];
15196
+ return {
15197
+ cur,
15198
+ current: cur ? toPts(cur) : [],
15199
+ ghosts: curves.slice(0, -1).map(toPts),
15200
+ tint: cur ? ghostLineColor(cur.tempoDeviation, cur.grindSignature) : t25["text-tertiary"]
15201
+ };
15202
+ };
15203
+ const up = wing(left);
15204
+ const down = wing(right);
15205
+ const segments = mergePhaseSegments(up.cur?.phaseSegments ?? [], down.cur?.phaseSegments ?? []);
15206
+ return /* @__PURE__ */ jsx(reactNative.View, { testID: "dual-ghost-spark", style: { paddingHorizontal: 4 }, children: /* @__PURE__ */ jsxs("svg", { width: w, height: h, children: [
15207
+ up.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-left", children: /* @__PURE__ */ jsx(
15208
+ GhostBloom,
15209
+ {
15210
+ current: up.current,
15211
+ ghosts: up.ghosts,
15212
+ tint: up.tint,
15213
+ baseline: baseUp,
15214
+ orientation: "up"
15215
+ }
15216
+ ) }),
15217
+ down.current.length > 0 && /* @__PURE__ */ jsx("g", { "data-testid": "dual-ghost-bloom-right", children: /* @__PURE__ */ jsx(
15218
+ GhostBloom,
15219
+ {
15220
+ current: down.current,
15221
+ ghosts: down.ghosts,
15222
+ tint: down.tint,
15223
+ baseline: baseDown,
15224
+ orientation: "down"
15225
+ }
15226
+ ) }),
15227
+ /* @__PURE__ */ jsx(
15228
+ GhostBand,
15229
+ {
15230
+ segments,
15231
+ x,
15232
+ top: bandTop,
15233
+ showLabels: true,
15234
+ labelColor: alpha(t25["text-primary"], 0.92),
15235
+ targetTempoSeconds
15236
+ }
15237
+ ),
15238
+ showDeviceLabels && /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
15239
+ /* @__PURE__ */ jsx(
15240
+ "text",
15241
+ {
15242
+ x: padL,
15243
+ y: padTop - 9,
15244
+ fontSize: 9,
15245
+ fontWeight: 800,
15246
+ letterSpacing: 1,
15247
+ fontFamily: FONT_UI,
15248
+ fill: t25["text-tertiary"],
15249
+ children: leftLabel
15250
+ }
15251
+ ),
15252
+ /* @__PURE__ */ jsx(
15253
+ "text",
15254
+ {
15255
+ x: padL,
15256
+ y: h - padBot + 15,
15257
+ fontSize: 9,
15258
+ fontWeight: 800,
15259
+ letterSpacing: 1,
15260
+ fontFamily: FONT_UI,
15261
+ fill: t25["text-tertiary"],
15262
+ children: rightLabel
15263
+ }
15264
+ )
15265
+ ] })
15266
+ ] }) });
15267
+ }
13894
15268
  var PALETTE = DATAVIZ_CATEGORICAL_PALETTE;
13895
15269
  function scaleValue(v, scale) {
13896
15270
  if (v <= 0) return 0;
@@ -13979,7 +15353,7 @@ function Treemap({
13979
15353
  const rects = squarify(weighted, width, height);
13980
15354
  return /* @__PURE__ */ jsx(reactNative.View, { className: cn("relative", className), style: { width, height }, ...props, children: rects.map((r) => {
13981
15355
  const selected = r.datum.id === selectedId;
13982
- const labelFits = r.w > 54 && r.h > 22;
15356
+ const labelFits2 = r.w > 54 && r.h > 22;
13983
15357
  const pressable = r.datum.id !== "__more__";
13984
15358
  return /* @__PURE__ */ jsx(
13985
15359
  reactNative.Pressable,
@@ -14001,7 +15375,7 @@ function Treemap({
14001
15375
  padding: 4,
14002
15376
  overflow: "hidden"
14003
15377
  },
14004
- children: labelFits && /* @__PURE__ */ jsx(reactNative.Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
15378
+ children: labelFits2 && /* @__PURE__ */ jsx(reactNative.Text, { numberOfLines: 2, className: "text-[10px] font-semibold text-[#0b0b0b]", children: r.datum.label ?? r.datum.id })
14005
15379
  },
14006
15380
  r.datum.id
14007
15381
  );
@@ -14097,28 +15471,28 @@ function Scatter({
14097
15471
  testID: "scatter-canvas",
14098
15472
  style: { position: "absolute", top: 0, left: 0, width, height, overflow: "hidden" },
14099
15473
  children: [
14100
- ticksOf(yd, TICK_COUNT).map((t17, i) => {
14101
- const y = toY(t17);
15474
+ ticksOf(yd, TICK_COUNT).map((t26, i) => {
15475
+ const y = toY(t26);
14102
15476
  return /* @__PURE__ */ jsx(
14103
15477
  reactNative.View,
14104
15478
  {
14105
15479
  accessibilityElementsHidden: true,
14106
15480
  testID: "scatter-gridline-y",
14107
15481
  style: { position: "absolute", left: PLOT_LEFT2, width: innerW, top: y, height: 1, backgroundColor: GRID_LINE2 },
14108
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t17) })
15482
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", left: -PLOT_LEFT2, top: -6, width: PLOT_LEFT2 - 6, textAlign: "right" }, children: formatTick(t26) })
14109
15483
  },
14110
15484
  `y-${i}`
14111
15485
  );
14112
15486
  }),
14113
- ticksOf(xd, TICK_COUNT).map((t17, i) => {
14114
- const x = toX(t17);
15487
+ ticksOf(xd, TICK_COUNT).map((t26, i) => {
15488
+ const x = toX(t26);
14115
15489
  return /* @__PURE__ */ jsx(
14116
15490
  reactNative.View,
14117
15491
  {
14118
15492
  accessibilityElementsHidden: true,
14119
15493
  testID: "scatter-gridline-x",
14120
15494
  style: { position: "absolute", top: PLOT_TOP, height: innerH, left: x, width: 1, backgroundColor: GRID_LINE2 },
14121
- children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t17) })
15495
+ children: /* @__PURE__ */ jsx(reactNative.Text, { className: "text-[9px] text-text-tertiary", style: { position: "absolute", top: innerH + 4, left: -16, width: 32, textAlign: "center" }, children: formatTick(t26) })
14122
15496
  },
14123
15497
  `x-${i}`
14124
15498
  );
@@ -14191,7 +15565,7 @@ var DEFAULT_THRESHOLDS2 = [
14191
15565
  var ARC_START = 135;
14192
15566
  var ARC_SWEEP = 270;
14193
15567
  var SEGMENTS = 40;
14194
- function clamp012(n) {
15568
+ function clamp013(n) {
14195
15569
  if (Number.isNaN(n)) return 0;
14196
15570
  return Math.max(0, Math.min(1, n));
14197
15571
  }
@@ -14236,7 +15610,7 @@ function Gauge2({
14236
15610
  ...props
14237
15611
  }) {
14238
15612
  const span = max - min || 1;
14239
- const fill = clamp012((value - min) / span);
15613
+ const fill = clamp013((value - min) / span);
14240
15614
  const activeColor = (f) => color ?? bandColor2(f, thresholds, min, max);
14241
15615
  const ticks = buildTicks(fill, size, activeColor);
14242
15616
  const displayColor = color ?? bandColor2(fill, thresholds, min, max);
@@ -14253,23 +15627,23 @@ function Gauge2({
14253
15627
  testID: "gauge",
14254
15628
  ...props,
14255
15629
  children: [
14256
- ticks.map((t17) => /* @__PURE__ */ jsx(
15630
+ ticks.map((t26) => /* @__PURE__ */ jsx(
14257
15631
  reactNative.View,
14258
15632
  {
14259
15633
  accessibilityElementsHidden: true,
14260
15634
  testID: "gauge-segment",
14261
15635
  style: {
14262
15636
  position: "absolute",
14263
- left: t17.left - tickThickness / 2,
14264
- top: t17.top - tickLength / 2,
15637
+ left: t26.left - tickThickness / 2,
15638
+ top: t26.top - tickLength / 2,
14265
15639
  width: tickThickness,
14266
15640
  height: tickLength,
14267
15641
  borderRadius: tickThickness / 2,
14268
- backgroundColor: t17.color,
14269
- transform: [{ rotate: t17.rotate }]
15642
+ backgroundColor: t26.color,
15643
+ transform: [{ rotate: t26.rotate }]
14270
15644
  }
14271
15645
  },
14272
- t17.key
15646
+ t26.key
14273
15647
  )),
14274
15648
  /* @__PURE__ */ jsxs(reactNative.View, { style: { position: "absolute", top: 0, left: 0, width: size, height: size }, className: "items-center justify-center", children: [
14275
15649
  /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-baseline gap-0.5", children: [
@@ -14703,6 +16077,8 @@ exports.Avatar = Avatar;
14703
16077
  exports.AvatarBadge = AvatarBadge;
14704
16078
  exports.AvatarGroup = AvatarGroup;
14705
16079
  exports.AwardIcon = AwardIcon;
16080
+ exports.BAND_GAP = BAND_GAP;
16081
+ exports.BAND_H = BAND_H;
14706
16082
  exports.Badge = Badge;
14707
16083
  exports.BadgeText = BadgeText;
14708
16084
  exports.BaseBadge = BaseBadge;
@@ -14733,6 +16109,7 @@ exports.CircularTimer = CircularTimer;
14733
16109
  exports.Collapse = Collapse;
14734
16110
  exports.CollapseButton = CollapseButton;
14735
16111
  exports.CollapseContent = CollapseContent;
16112
+ exports.DRIFT_GREY = DRIFT_GREY;
14736
16113
  exports.DashboardShell = DashboardShell;
14737
16114
  exports.DataRow = DataRow;
14738
16115
  exports.DateTime = DateTime;
@@ -14745,18 +16122,25 @@ exports.Drawer = Drawer;
14745
16122
  exports.DrawerBody = DrawerBody;
14746
16123
  exports.DrawerFooter = DrawerFooter;
14747
16124
  exports.DrawerHeader = DrawerHeader;
16125
+ exports.DualGhostSpark = DualGhostSpark;
16126
+ exports.DualVelocityStrip = DualVelocityStrip;
14748
16127
  exports.DumbbellIcon = DumbbellIcon;
14749
16128
  exports.EmptyState = EmptyState;
14750
16129
  exports.ExerciseCard = ExerciseCard;
14751
16130
  exports.ExerciseCardHeading = ExerciseCardHeading;
14752
16131
  exports.ExerciseHeading = ExerciseHeading;
14753
16132
  exports.ExerciseIndicator = ExerciseIndicator;
16133
+ exports.FatigueLights = FatigueLights;
14754
16134
  exports.FatigueMeter = FatigueMeter;
14755
16135
  exports.FormActions = FormActions;
14756
16136
  exports.FormField = FormField;
14757
16137
  exports.FormRow = FormRow;
14758
16138
  exports.FormSection = FormSection;
16139
+ exports.GRIND_THRESHOLD = GRIND_THRESHOLD;
14759
16140
  exports.Gauge = Gauge2;
16141
+ exports.GhostBand = GhostBand;
16142
+ exports.GhostBloom = GhostBloom;
16143
+ exports.GhostSpark = GhostSpark;
14760
16144
  exports.HStack = HStack;
14761
16145
  exports.Heading = Heading;
14762
16146
  exports.HelpTip = HelpTip;
@@ -14779,6 +16163,8 @@ exports.ListItemDivider = ListItemDivider;
14779
16163
  exports.ListItemIcon = ListItemIcon;
14780
16164
  exports.ListItemTrailing = ListItemTrailing;
14781
16165
  exports.LiveAuraFrame = LiveAuraFrame;
16166
+ exports.LiveFatigueCard = LiveFatigueCard;
16167
+ exports.LiveFatiguePanel = LiveFatiguePanel;
14782
16168
  exports.METRIC_FONT = METRIC_FONT;
14783
16169
  exports.Menu = Menu;
14784
16170
  exports.MenuDivider = MenuDivider;
@@ -14803,6 +16189,7 @@ exports.ModalTitle = ModalTitle;
14803
16189
  exports.MuscleGroupChip = MuscleGroupChip;
14804
16190
  exports.NavItem = NavItem;
14805
16191
  exports.Overline = Overline;
16192
+ exports.PHASE_AXIS_COLOR = PHASE_AXIS_COLOR;
14806
16193
  exports.PRESSED_ELEVATION_LEVEL = PRESSED_ELEVATION_LEVEL;
14807
16194
  exports.Paragraph = Paragraph;
14808
16195
  exports.PasswordInput = PasswordInput;
@@ -14817,12 +16204,18 @@ exports.PrBadge = PrBadge;
14817
16204
  exports.PrHistoryModal = PrHistoryModal;
14818
16205
  exports.Progress = Progress;
14819
16206
  exports.ProgressSteps = ProgressSteps;
16207
+ exports.RED_DEEP = RED_DEEP;
16208
+ exports.RED_LIGHT = RED_LIGHT;
16209
+ exports.RED_MID = RED_MID;
14820
16210
  exports.Radio = Radio;
14821
16211
  exports.RadioGroup = RadioGroup;
14822
16212
  exports.ReadinessCheck = ReadinessCheck;
14823
16213
  exports.RestTimer = RestTimer;
16214
+ exports.RomProgressionChart = RomProgressionChart;
14824
16215
  exports.SET_STRIP_VARIABLE_COLOR = SET_STRIP_VARIABLE_COLOR;
14825
16216
  exports.SET_STRIP_ZONES = SET_STRIP_ZONES;
16217
+ exports.SILVER = SILVER;
16218
+ exports.STATE_LABEL = STATE_LABEL;
14826
16219
  exports.SURFACE_LEVEL_TOKEN = SURFACE_LEVEL_TOKEN;
14827
16220
  exports.ScaleIcon = ScaleIcon;
14828
16221
  exports.Scatter = Scatter;
@@ -14871,6 +16264,7 @@ exports.Surface = Surface;
14871
16264
  exports.SurfaceContext = SurfaceContext;
14872
16265
  exports.SvgIcon = SvgIcon;
14873
16266
  exports.Switch = Switch;
16267
+ exports.TONE_COLOR = TONE_COLOR;
14874
16268
  exports.Tab = Tab;
14875
16269
  exports.TabList = TabList;
14876
16270
  exports.TabPanel = TabPanel;
@@ -14896,7 +16290,9 @@ exports.Treemap = Treemap;
14896
16290
  exports.TrendingDownIcon = TrendingDownIcon;
14897
16291
  exports.Typography = Typography;
14898
16292
  exports.VStack = VStack;
16293
+ exports.VelocityHero = VelocityHero;
14899
16294
  exports.VelocityStrip = VelocityStrip;
16295
+ exports.VerdictHero = VerdictHero;
14900
16296
  exports.VoltrasMark = VoltrasMark;
14901
16297
  exports.VolumeLandmarkBar = VolumeLandmarkBar;
14902
16298
  exports.WeekRow = WeekRow;
@@ -14907,6 +16303,7 @@ exports.ZoneTrack = ZoneTrack;
14907
16303
  exports.alpha = alpha;
14908
16304
  exports.applyThemePreset = applyThemePreset;
14909
16305
  exports.audiobookPreset = audiobookPreset;
16306
+ exports.auraForVerdict = auraForVerdict;
14910
16307
  exports.backgroundFrameDark = backgroundFrameDark;
14911
16308
  exports.bestTextColor = bestTextColor;
14912
16309
  exports.buttonSizes = buttonSizes;
@@ -14954,6 +16351,7 @@ exports.getThemeCSSVars = getThemeCSSVars;
14954
16351
  exports.getValidatedElevation = getValidatedElevation;
14955
16352
  exports.getVelocityZoneColor = getVelocityZoneColor;
14956
16353
  exports.getVelocityZoneName = getVelocityZoneName;
16354
+ exports.ghostLineColor = ghostLineColor;
14957
16355
  exports.gluestackConfig = gluestackConfig;
14958
16356
  exports.hasMaxLength = hasMaxLength;
14959
16357
  exports.hasMinLength = hasMinLength;
@@ -14967,6 +16365,7 @@ exports.lightThemeCSSVars = lightThemeCSSVars;
14967
16365
  exports.lighten = lighten;
14968
16366
  exports.linearGradient = linearGradient;
14969
16367
  exports.liveAuraColor = liveAuraColor;
16368
+ exports.mergePhaseSegments = mergePhaseSegments;
14970
16369
  exports.neumorphicShadows = neumorphicShadows;
14971
16370
  exports.onSurfaceColors = onSurfaceColors;
14972
16371
  exports.paceTone = paceTone;
@@ -14993,6 +16392,7 @@ exports.semanticColorsLight = semanticColorsLight;
14993
16392
  exports.semanticTypography = semanticTypography;
14994
16393
  exports.sequentialEffort = sequentialEffort;
14995
16394
  exports.shadowColors = shadowColors;
16395
+ exports.smoothPath = smoothPath;
14996
16396
  exports.statusPillColor = statusPillColor;
14997
16397
  exports.surfaceBackground = surfaceBackground;
14998
16398
  exports.surfaceGradient = surfaceGradient;