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