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