@titan-design/react-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/MesoStatusCard-CQk71hSi.d.mts +77 -0
  2. package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
  3. package/dist/bodymap.d.mts +246 -3
  4. package/dist/bodymap.d.ts +246 -3
  5. package/dist/bodymap.mjs +1911 -6
  6. package/dist/bodymap.mjs.map +1 -1
  7. package/dist/index.d.mts +31 -738
  8. package/dist/index.d.ts +31 -738
  9. package/dist/index.js +894 -1692
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +10287 -8203
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/pages-D7Jlssvp.d.ts +791 -0
  14. package/dist/pages-MO0JCySL.d.mts +791 -0
  15. package/dist/pages.d.mts +4 -0
  16. package/dist/pages.d.ts +4 -0
  17. package/dist/pages.js +6646 -0
  18. package/dist/pages.js.map +1 -0
  19. package/dist/pages.mjs +6626 -0
  20. package/dist/pages.mjs.map +1 -0
  21. package/dist/theme/index.d.mts +12 -12
  22. package/dist/theme/index.d.ts +12 -12
  23. package/dist/theme/index.js +30 -30
  24. package/dist/theme/index.js.map +1 -1
  25. package/dist/theme/index.mjs +1880 -2
  26. package/dist/theme/index.mjs.map +1 -1
  27. package/dist/theme/tokens-css.mjs +573 -1
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +9 -7
  30. package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
  31. package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
  32. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  33. package/src/components/custom/Workout/ExerciseCard.tsx +5 -1
  34. package/src/components/custom/Workout/ExerciseDetailPage.tsx +17 -4
  35. package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
  36. package/src/components/custom/Workout/IntensityBar.tsx +109 -58
  37. package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -9
  38. package/src/components/custom/Workout/PrBadge.tsx +3 -3
  39. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  40. package/src/components/custom/Workout/README.md +50 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
  42. package/src/components/custom/Workout/RestTimer.tsx +48 -43
  43. package/src/components/custom/Workout/SetRow.tsx +10 -1
  44. package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
  45. package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
  46. package/src/components/custom/Workout/TempoDisplay.tsx +104 -1
  47. package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
  48. package/src/components/custom/Workout/VelocityStrip.tsx +203 -38
  49. package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
  50. package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
  51. package/src/components/custom/Workout/WeightBadge.tsx +2 -2
  52. package/src/components/custom/Workout/icons.tsx +68 -0
  53. package/src/components/custom/Workout/index.ts +38 -47
  54. package/src/index.ts +3 -2
  55. package/src/pages.ts +61 -0
  56. package/src/test/stories-smoke.test.tsx +50 -0
  57. package/src/theme/barrel.ts +53 -0
  58. package/src/theme/index.ts +8 -43
  59. package/src/theme/native-theming-guard.test.ts +117 -0
  60. package/dist/bodymap-BhG55xHM.d.mts +0 -318
  61. package/dist/bodymap-BhG55xHM.d.ts +0 -318
  62. package/dist/chunk-2SISKTWM.mjs +0 -1027
  63. package/dist/chunk-2SISKTWM.mjs.map +0 -1
  64. package/dist/chunk-7XPB4NAO.mjs +0 -1013
  65. package/dist/chunk-7XPB4NAO.mjs.map +0 -1
  66. package/dist/chunk-SNVKL5WZ.mjs +0 -883
  67. package/dist/chunk-SNVKL5WZ.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -5,8 +5,6 @@ var clsx = require('clsx');
5
5
  var tailwindMerge = require('tailwind-merge');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var React24 = require('react');
8
- var nativewind = require('nativewind');
9
- var lucideReact = require('lucide-react');
10
8
 
11
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
10
 
@@ -2161,18 +2159,6 @@ function validateCssPropertyManifest(candidate) {
2161
2159
  function isCssPropertyManifest(candidate) {
2162
2160
  return validateCssPropertyManifest(candidate).valid;
2163
2161
  }
2164
- var MODE_VARS = {
2165
- dark: nativewind.vars(darkThemeCSSVars),
2166
- light: nativewind.vars(lightThemeCSSVars)
2167
- };
2168
- function ThemeProvider({
2169
- mode = "dark",
2170
- style,
2171
- children,
2172
- ...props
2173
- }) {
2174
- return /* @__PURE__ */ jsx(reactNative.View, { style: [MODE_VARS[mode], { flex: 1 }, style], ...props, children });
2175
- }
2176
2162
  function resolveColor(token, mode = "dark") {
2177
2163
  return reactNative.Platform.OS === "web" ? `var(--color-${token})` : getSemanticColors(mode)[token];
2178
2164
  }
@@ -6768,7 +6754,7 @@ var variantColors = {
6768
6754
  plain: { backgroundColor: "#1C1C1C", borderColor: "#1F1F1F" },
6769
6755
  pr: {
6770
6756
  backgroundColor: "rgba(255, 121, 0, 0.12)",
6771
- borderColor: "rgba(255, 121, 0, 0.12)"
6757
+ borderColor: "rgba(255, 121, 0, 0.3)"
6772
6758
  }
6773
6759
  };
6774
6760
  function BaseBadge({
@@ -6836,6 +6822,58 @@ function BaseBadge({
6836
6822
  }
6837
6823
  return badge;
6838
6824
  }
6825
+
6826
+ // src/components/custom/Workout/icons.tsx
6827
+ function DumbbellIcon({
6828
+ size = 24,
6829
+ color = "currentColor",
6830
+ fill = "none",
6831
+ strokeWidth = 2
6832
+ }) {
6833
+ return /* @__PURE__ */ jsxs(
6834
+ "svg",
6835
+ {
6836
+ width: size,
6837
+ height: size,
6838
+ viewBox: "0 0 24 24",
6839
+ fill,
6840
+ stroke: color,
6841
+ strokeWidth,
6842
+ strokeLinecap: "round",
6843
+ strokeLinejoin: "round",
6844
+ children: [
6845
+ /* @__PURE__ */ jsx("path", { d: "M14.4 14.4 9.6 9.6" }),
6846
+ /* @__PURE__ */ jsx("path", { d: "M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z" }),
6847
+ /* @__PURE__ */ jsx("path", { d: "m21.5 21.5-1.4-1.4" }),
6848
+ /* @__PURE__ */ jsx("path", { d: "M3.9 3.9 2.5 2.5" }),
6849
+ /* @__PURE__ */ jsx("path", { d: "M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z" })
6850
+ ]
6851
+ }
6852
+ );
6853
+ }
6854
+ function StarIcon({
6855
+ size = 24,
6856
+ color = "currentColor",
6857
+ fill = "none",
6858
+ strokeWidth = 2
6859
+ }) {
6860
+ return /* @__PURE__ */ jsx(
6861
+ "svg",
6862
+ {
6863
+ width: size,
6864
+ height: size,
6865
+ viewBox: "0 0 24 24",
6866
+ fill,
6867
+ stroke: color,
6868
+ strokeWidth,
6869
+ strokeLinecap: "round",
6870
+ strokeLinejoin: "round",
6871
+ children: /* @__PURE__ */ jsx("path", { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" })
6872
+ }
6873
+ );
6874
+ }
6875
+
6876
+ // src/components/custom/Workout/WeightBadge.tsx
6839
6877
  function WeightBadge({
6840
6878
  value,
6841
6879
  unit = "lbs",
@@ -6861,7 +6899,7 @@ function WeightBadge({
6861
6899
  size,
6862
6900
  onPress,
6863
6901
  className,
6864
- icon: showIcon ? /* @__PURE__ */ jsx(lucideReact.Dumbbell, { size: config.iconSize, color: iconColor, strokeWidth: 2 }) : void 0,
6902
+ icon: showIcon ? /* @__PURE__ */ jsx(DumbbellIcon, { size: config.iconSize, color: iconColor, strokeWidth: 2 }) : void 0,
6865
6903
  accessibilityLabel: fullLabel,
6866
6904
  testID: "weight-badge",
6867
6905
  ...props,
@@ -6965,7 +7003,7 @@ function PrBadge({
6965
7003
  accessibilityLabel: `Personal record: ${resolvedLabel}`,
6966
7004
  testID: "pr-badge-star",
6967
7005
  ...props,
6968
- children: /* @__PURE__ */ jsx(lucideReact.Star, { size: 14, color: "#FF7900", fill: "#FF7900", strokeWidth: 2 })
7006
+ children: /* @__PURE__ */ jsx(StarIcon, { size: 14, color: "#FF7900", fill: "#FF7900", strokeWidth: 2 })
6969
7007
  }
6970
7008
  ) : /* @__PURE__ */ jsx(
6971
7009
  BaseBadge,
@@ -6978,7 +7016,7 @@ function PrBadge({
6978
7016
  reactNative.Text,
6979
7017
  {
6980
7018
  style: {
6981
- fontSize: 10,
7019
+ fontSize: 12,
6982
7020
  color: "#FF7900",
6983
7021
  fontWeight: "700",
6984
7022
  fontFamily: "Inter, sans-serif"
@@ -7129,6 +7167,29 @@ function StatusDot({
7129
7167
  }
7130
7168
  return /* @__PURE__ */ jsx(reactNative.View, { className, ...props, children: dot });
7131
7169
  }
7170
+
7171
+ // src/theme/workout-tokens.ts
7172
+ var WORKOUT_TOKENS = {
7173
+ // Canonical 4-band performance scale — the single source for BOTH the
7174
+ // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
7175
+ // is intentionally inverted between the two consumers: for velocity, green =
7176
+ // fastest/best; for RPE, green = easiest. Each component maps its own
7177
+ // thresholds onto these four colors. Kept as vivid data-viz values (distinct
7178
+ // from the muted status tokens) and theme-independent, so — like the heatmap
7179
+ // scale below — they are consumed inline as a JS import rather than CSS vars.
7180
+ // A JS import also resolves on native RN, unlike var(--…) strings.
7181
+ scale: {
7182
+ green: "#2ed573",
7183
+ yellow: "#ffd43b",
7184
+ orange: "#ffa502",
7185
+ red: "#ff4757"
7186
+ },
7187
+ // Placeholder strip
7188
+ placeholder: {
7189
+ fill: "#3A3A3A"
7190
+ }};
7191
+
7192
+ // src/components/custom/Workout/PlaceholderStrip.tsx
7132
7193
  function SingleStrip({
7133
7194
  width,
7134
7195
  className,
@@ -7137,14 +7198,12 @@ function SingleStrip({
7137
7198
  return /* @__PURE__ */ jsx(
7138
7199
  reactNative.View,
7139
7200
  {
7140
- className: cn("border-border-strong", className),
7201
+ className,
7141
7202
  style: [
7142
7203
  {
7143
7204
  height: 3,
7144
- backgroundColor: "transparent",
7145
- borderWidth: 1,
7146
- borderStyle: "dashed",
7147
- borderRadius: 1,
7205
+ backgroundColor: WORKOUT_TOKENS.placeholder.fill,
7206
+ borderRadius: 2,
7148
7207
  opacity: 0.5
7149
7208
  },
7150
7209
  width != null ? { width } : { flex: 1 }
@@ -7177,13 +7236,10 @@ function SegmentedStrip({
7177
7236
  children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
7178
7237
  reactNative.View,
7179
7238
  {
7180
- className: "border-border-strong",
7181
7239
  style: {
7182
7240
  flex: 1,
7183
7241
  height: 3,
7184
- backgroundColor: "transparent",
7185
- borderWidth: 1,
7186
- borderStyle: "dashed",
7242
+ backgroundColor: WORKOUT_TOKENS.placeholder.fill,
7187
7243
  borderRadius: 1,
7188
7244
  minWidth: 4
7189
7245
  },
@@ -7206,23 +7262,6 @@ function PlaceholderStrip({
7206
7262
  return /* @__PURE__ */ jsx(SingleStrip, { ...props });
7207
7263
  }
7208
7264
 
7209
- // src/theme/workout-tokens.ts
7210
- var WORKOUT_TOKENS = {
7211
- // Canonical 4-band performance scale — the single source for BOTH the
7212
- // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
7213
- // is intentionally inverted between the two consumers: for velocity, green =
7214
- // fastest/best; for RPE, green = easiest. Each component maps its own
7215
- // thresholds onto these four colors. Kept as vivid data-viz values (distinct
7216
- // from the muted status tokens) and theme-independent, so — like the heatmap
7217
- // scale below — they are consumed inline as a JS import rather than CSS vars.
7218
- // A JS import also resolves on native RN, unlike var(--…) strings.
7219
- scale: {
7220
- green: "#2ed573",
7221
- yellow: "#ffd43b",
7222
- orange: "#ffa502",
7223
- red: "#ff4757"
7224
- }};
7225
-
7226
7265
  // src/utils/workout-format.ts
7227
7266
  function roundRpe(rpe) {
7228
7267
  return Math.round(rpe * 2) / 2;
@@ -7261,302 +7300,90 @@ function formatPrescription(p) {
7261
7300
  return full.length > 0 ? full : null;
7262
7301
  }
7263
7302
 
7264
- // src/components/custom/Workout/TempoDisplay.tsx
7265
- var INTER = "Inter, sans-serif";
7266
- var TEXT_TERTIARY = "#6B7280";
7267
- var phaseColors = {
7268
- eccentric: "#FFB020",
7269
- // status-warning (amber)
7270
- pauseBottom: TEXT_TERTIARY,
7271
- concentric: "#14B8A6",
7272
- // status-success (teal)
7273
- pauseTop: TEXT_TERTIARY,
7274
- dash: TEXT_TERTIARY
7275
- };
7276
- function TempoValue({
7277
- value,
7278
- color,
7279
- fontSize
7280
- }) {
7281
- return /* @__PURE__ */ jsx(
7282
- reactNative.Text,
7283
- {
7284
- style: {
7285
- fontFamily: INTER,
7286
- fontSize,
7287
- color,
7288
- fontWeight: "600",
7289
- letterSpacing: 1
7290
- },
7291
- children: value
7292
- }
7293
- );
7303
+ // src/utils/colors.ts
7304
+ function getStatusColor(status) {
7305
+ return `var(--color-status-${status})`;
7294
7306
  }
7295
- function TempoSeparator({ color, fontSize }) {
7296
- return /* @__PURE__ */ jsx(
7297
- reactNative.Text,
7298
- {
7299
- style: {
7300
- fontFamily: INTER,
7301
- fontSize,
7302
- color,
7303
- fontWeight: "600",
7304
- letterSpacing: 1
7305
- },
7306
- children: "-"
7307
+ function getResultColor(result) {
7308
+ return `var(--color-result-${result})`;
7309
+ }
7310
+ function alpha(color, opacity) {
7311
+ const clampedOpacity = Math.max(0, Math.min(1, opacity));
7312
+ if (color.startsWith("#")) {
7313
+ const hex = color.slice(1);
7314
+ let r, g, b;
7315
+ if (hex.length === 3) {
7316
+ r = parseInt(hex[0] + hex[0], 16);
7317
+ g = parseInt(hex[1] + hex[1], 16);
7318
+ b = parseInt(hex[2] + hex[2], 16);
7319
+ } else if (hex.length === 6) {
7320
+ r = parseInt(hex.slice(0, 2), 16);
7321
+ g = parseInt(hex.slice(2, 4), 16);
7322
+ b = parseInt(hex.slice(4, 6), 16);
7323
+ } else {
7324
+ return color;
7307
7325
  }
7308
- );
7326
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7327
+ }
7328
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
7329
+ if (rgbMatch) {
7330
+ const [, r, g, b] = rgbMatch;
7331
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7332
+ }
7333
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
7334
+ if (rgbaMatch) {
7335
+ const [, r, g, b] = rgbaMatch;
7336
+ return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7337
+ }
7338
+ return color;
7309
7339
  }
7310
- function TempoDisplay({
7311
- tempo,
7312
- size = "md",
7313
- colored = true,
7314
- showInfo = true,
7315
- onPress,
7316
- className,
7317
- ...props
7318
- }) {
7319
- const [showTooltip, setShowTooltip] = React24.useState(false);
7320
- const [eccentric, pauseBottom, concentric, pauseTop] = roundTempo(tempo);
7321
- const isSm = size === "sm";
7322
- const fontSize = isSm ? 9 : 11;
7323
- const monoColor = TEXT_TERTIARY;
7324
- const handlePress = React24.useCallback(() => {
7325
- if (onPress) {
7326
- onPress();
7327
- }
7328
- if (showInfo) {
7329
- setShowTooltip((prev) => !prev);
7330
- }
7331
- }, [onPress, showInfo]);
7332
- const content = /* @__PURE__ */ jsxs(
7333
- reactNative.View,
7334
- {
7335
- className,
7336
- style: {
7337
- flexDirection: "row",
7338
- alignItems: "center",
7339
- backgroundColor: "#1C1C1C",
7340
- paddingHorizontal: isSm ? 6 : 8,
7341
- paddingVertical: 3,
7342
- borderRadius: 4
7343
- },
7344
- ...props,
7345
- children: [
7346
- /* @__PURE__ */ jsx(
7347
- reactNative.Text,
7348
- {
7349
- style: {
7350
- fontFamily: INTER,
7351
- fontSize: 9,
7352
- fontWeight: "500",
7353
- color: TEXT_TERTIARY,
7354
- letterSpacing: 0.5,
7355
- textTransform: "uppercase",
7356
- marginRight: 6
7357
- },
7358
- children: "TEMPO"
7359
- }
7360
- ),
7361
- /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row" }, testID: "tempo-value", children: colored ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
7362
- /* @__PURE__ */ jsx(TempoValue, { value: eccentric, color: phaseColors.eccentric, fontSize }),
7363
- /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7364
- /* @__PURE__ */ jsx(TempoValue, { value: pauseBottom, color: phaseColors.pauseBottom, fontSize }),
7365
- /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7366
- /* @__PURE__ */ jsx(TempoValue, { value: concentric, color: phaseColors.concentric, fontSize }),
7367
- /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7368
- /* @__PURE__ */ jsx(TempoValue, { value: pauseTop, color: phaseColors.pauseTop, fontSize })
7369
- ] }) : /* @__PURE__ */ jsxs(
7370
- reactNative.Text,
7371
- {
7372
- style: {
7373
- fontFamily: INTER,
7374
- fontSize,
7375
- color: monoColor,
7376
- fontWeight: "600",
7377
- letterSpacing: 1
7378
- },
7379
- children: [
7380
- eccentric,
7381
- "-",
7382
- pauseBottom,
7383
- "-",
7384
- concentric,
7385
- "-",
7386
- pauseTop
7387
- ]
7388
- }
7389
- ) })
7390
- ]
7391
- }
7392
- );
7393
- return /* @__PURE__ */ jsxs(
7394
- reactNative.Pressable,
7395
- {
7396
- accessibilityLabel: `Tempo: ${eccentric} second eccentric, ${pauseBottom} second pause, ${concentric} second concentric, ${pauseTop} second pause`,
7397
- accessibilityRole: "button",
7398
- onPress: handlePress,
7399
- testID: "tempo-display",
7400
- children: [
7401
- content,
7402
- showTooltip && /* @__PURE__ */ jsxs(
7403
- reactNative.View,
7404
- {
7405
- style: {
7406
- position: "absolute",
7407
- bottom: "100%",
7408
- left: "50%",
7409
- transform: [{ translateX: "-50%" }],
7410
- marginBottom: 8,
7411
- zIndex: 20,
7412
- alignItems: "center"
7413
- },
7414
- testID: "tempo-tooltip",
7415
- children: [
7416
- /* @__PURE__ */ jsxs(
7417
- reactNative.View,
7418
- {
7419
- style: {
7420
- backgroundColor: "#191919",
7421
- borderRadius: 6,
7422
- paddingVertical: 8,
7423
- paddingHorizontal: 12,
7424
- borderWidth: 1,
7425
- borderColor: "#2C2C2C"
7426
- },
7427
- children: [
7428
- /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7429
- "Eccentric: ",
7430
- eccentric,
7431
- "s"
7432
- ] }),
7433
- /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7434
- "Pause (bottom): ",
7435
- pauseBottom,
7436
- "s"
7437
- ] }),
7438
- /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7439
- "Concentric: ",
7440
- concentric,
7441
- "s"
7442
- ] }),
7443
- /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7444
- "Pause (top): ",
7445
- pauseTop,
7446
- "s"
7447
- ] })
7448
- ]
7449
- }
7450
- ),
7451
- /* @__PURE__ */ jsx(
7452
- reactNative.View,
7453
- {
7454
- style: {
7455
- width: 0,
7456
- height: 0,
7457
- borderLeftWidth: 5,
7458
- borderRightWidth: 5,
7459
- borderTopWidth: 5,
7460
- borderLeftColor: "transparent",
7461
- borderRightColor: "transparent",
7462
- borderTopColor: "#2C2C2C"
7463
- }
7464
- }
7465
- )
7466
- ]
7467
- }
7468
- )
7469
- ]
7470
- }
7471
- );
7472
- }
7473
-
7474
- // src/utils/colors.ts
7475
- function getStatusColor(status) {
7476
- return `var(--color-status-${status})`;
7477
- }
7478
- function getResultColor(result) {
7479
- return `var(--color-result-${result})`;
7480
- }
7481
- function alpha(color, opacity) {
7482
- const clampedOpacity = Math.max(0, Math.min(1, opacity));
7483
- if (color.startsWith("#")) {
7484
- const hex = color.slice(1);
7485
- let r, g, b;
7486
- if (hex.length === 3) {
7487
- r = parseInt(hex[0] + hex[0], 16);
7488
- g = parseInt(hex[1] + hex[1], 16);
7489
- b = parseInt(hex[2] + hex[2], 16);
7490
- } else if (hex.length === 6) {
7491
- r = parseInt(hex.slice(0, 2), 16);
7492
- g = parseInt(hex.slice(2, 4), 16);
7493
- b = parseInt(hex.slice(4, 6), 16);
7494
- } else {
7495
- return color;
7496
- }
7497
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7498
- }
7499
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
7500
- if (rgbMatch) {
7501
- const [, r, g, b] = rgbMatch;
7502
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7503
- }
7504
- const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
7505
- if (rgbaMatch) {
7506
- const [, r, g, b] = rgbaMatch;
7507
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
7508
- }
7509
- return color;
7510
- }
7511
- function getLuminance(color) {
7512
- if (!color.startsWith("#")) return "dark";
7513
- const hex = color.slice(1);
7514
- let r, g, b;
7515
- if (hex.length === 3) {
7516
- r = parseInt(hex[0] + hex[0], 16);
7517
- g = parseInt(hex[1] + hex[1], 16);
7518
- b = parseInt(hex[2] + hex[2], 16);
7519
- } else if (hex.length === 6) {
7520
- r = parseInt(hex.slice(0, 2), 16);
7521
- g = parseInt(hex.slice(2, 4), 16);
7522
- b = parseInt(hex.slice(4, 6), 16);
7523
- } else {
7524
- return "dark";
7525
- }
7526
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
7527
- return luminance > 0.5 ? "light" : "dark";
7528
- }
7529
- function getContrastText(backgroundColor) {
7530
- return getLuminance(backgroundColor) === "light" ? "#000000" : "#FFFFFF";
7531
- }
7532
-
7533
- // src/components/custom/Workout/TempoBar.tsx
7534
- var t = getSemanticColors("dark");
7535
- var TEMPO_PACING = {
7536
- behindThresholdPct: 0.15,
7537
- minPhaseDurationMs: 500
7538
- };
7539
- function getTempoPacingState(elapsedMs, targetMs) {
7540
- if (!targetMs || targetMs < TEMPO_PACING.minPhaseDurationMs) return "none";
7541
- const ratio = elapsedMs / targetMs;
7542
- if (ratio > 1 + TEMPO_PACING.behindThresholdPct) return "behind";
7543
- return "on-pace";
7544
- }
7545
- function getTempoFillPct(elapsedMs, targetMs) {
7546
- if (!targetMs) return 100;
7547
- return Math.min(100, elapsedMs / targetMs * 100);
7548
- }
7549
- var PHASE_ORDER = ["concentric", "hold", "eccentric"];
7550
- var PHASE_CONFIG = {
7551
- concentric: { label: "Con", color: t["status-success"], flex: 2 },
7552
- hold: { label: "Hold", color: t["brand-primary"], flex: 1 },
7553
- eccentric: { label: "Ecc", color: t["status-warning"], flex: 3 }
7554
- };
7555
- function TempoBar({
7556
- activePhase,
7557
- phaseElapsedMs,
7558
- completed,
7559
- target,
7340
+ function getLuminance(color) {
7341
+ if (!color.startsWith("#")) return "dark";
7342
+ const hex = color.slice(1);
7343
+ let r, g, b;
7344
+ if (hex.length === 3) {
7345
+ r = parseInt(hex[0] + hex[0], 16);
7346
+ g = parseInt(hex[1] + hex[1], 16);
7347
+ b = parseInt(hex[2] + hex[2], 16);
7348
+ } else if (hex.length === 6) {
7349
+ r = parseInt(hex.slice(0, 2), 16);
7350
+ g = parseInt(hex.slice(2, 4), 16);
7351
+ b = parseInt(hex.slice(4, 6), 16);
7352
+ } else {
7353
+ return "dark";
7354
+ }
7355
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
7356
+ return luminance > 0.5 ? "light" : "dark";
7357
+ }
7358
+ function getContrastText(backgroundColor) {
7359
+ return getLuminance(backgroundColor) === "light" ? "#000000" : "#FFFFFF";
7360
+ }
7361
+ var t = getSemanticColors("dark");
7362
+ var TEMPO_PACING = {
7363
+ behindThresholdPct: 0.15,
7364
+ minPhaseDurationMs: 500
7365
+ };
7366
+ function getTempoPacingState(elapsedMs, targetMs) {
7367
+ if (!targetMs || targetMs < TEMPO_PACING.minPhaseDurationMs) return "none";
7368
+ const ratio = elapsedMs / targetMs;
7369
+ if (ratio > 1 + TEMPO_PACING.behindThresholdPct) return "behind";
7370
+ return "on-pace";
7371
+ }
7372
+ function getTempoFillPct(elapsedMs, targetMs) {
7373
+ if (!targetMs) return 100;
7374
+ return Math.min(100, elapsedMs / targetMs * 100);
7375
+ }
7376
+ var PHASE_ORDER = ["concentric", "hold", "eccentric"];
7377
+ var PHASE_CONFIG = {
7378
+ concentric: { label: "Con", color: t["status-success"], flex: 2 },
7379
+ hold: { label: "Hold", color: t["brand-primary"], flex: 1 },
7380
+ eccentric: { label: "Ecc", color: t["status-warning"], flex: 3 }
7381
+ };
7382
+ function TempoBar({
7383
+ activePhase,
7384
+ phaseElapsedMs,
7385
+ completed,
7386
+ target,
7560
7387
  className,
7561
7388
  ...props
7562
7389
  }) {
@@ -7662,31 +7489,306 @@ function formatDuration(ms) {
7662
7489
  const s = ms / 1e3;
7663
7490
  return s < 10 ? s.toFixed(1) : Math.round(s).toString();
7664
7491
  }
7665
- function getDotColor(deviation) {
7666
- const abs = Math.abs(deviation);
7667
- if (deviation < -0.3) return "#14B8A6";
7668
- if (abs <= 0.3) return "#6B7280";
7669
- if (abs <= 0.7) return "#FFB020";
7670
- return "#D14343";
7492
+
7493
+ // src/components/custom/Workout/TempoDisplay.tsx
7494
+ var INTER = "Inter, sans-serif";
7495
+ var TEXT_TERTIARY = "#6B7280";
7496
+ var STATUS_ERROR = "#D14343";
7497
+ var phaseColors = {
7498
+ eccentric: "#FFB020",
7499
+ // status-warning (amber)
7500
+ pauseBottom: TEXT_TERTIARY,
7501
+ concentric: "#14B8A6",
7502
+ // status-success (teal)
7503
+ pauseTop: TEXT_TERTIARY,
7504
+ dash: TEXT_TERTIARY
7505
+ };
7506
+ function TempoValue({
7507
+ value,
7508
+ color,
7509
+ fontSize
7510
+ }) {
7511
+ return /* @__PURE__ */ jsx(
7512
+ reactNative.Text,
7513
+ {
7514
+ style: {
7515
+ fontFamily: INTER,
7516
+ fontSize,
7517
+ color,
7518
+ fontWeight: "600",
7519
+ letterSpacing: 1
7520
+ },
7521
+ children: value
7522
+ }
7523
+ );
7671
7524
  }
7672
- function getDeviationDescription(deviation) {
7673
- if (deviation < -0.3) return "lighter than planned";
7674
- if (deviation > 0.3) return "harder than planned";
7675
- return "on plan";
7525
+ function TempoSeparator({ color, fontSize }) {
7526
+ return /* @__PURE__ */ jsx(
7527
+ reactNative.Text,
7528
+ {
7529
+ style: {
7530
+ fontFamily: INTER,
7531
+ fontSize,
7532
+ color,
7533
+ fontWeight: "600",
7534
+ letterSpacing: 1
7535
+ },
7536
+ children: "-"
7537
+ }
7538
+ );
7676
7539
  }
7677
- function DeviationBar({
7678
- deviation,
7679
- width,
7680
- className,
7681
- ...props
7540
+ var LIVE_PHASES = [
7541
+ { key: "eccentric", color: phaseColors.eccentric },
7542
+ { key: "pauseBottom", color: phaseColors.pauseBottom },
7543
+ { key: "concentric", color: phaseColors.concentric },
7544
+ { key: "pauseTop", color: phaseColors.pauseTop }
7545
+ ];
7546
+ function LiveTempoCell({
7547
+ value,
7548
+ color,
7549
+ isActive,
7550
+ phaseElapsedMs,
7551
+ fontSize
7682
7552
  }) {
7683
- const clamped = Math.max(-1, Math.min(1, deviation));
7684
- const resolvedWidth = width ?? 40;
7685
- const dotPosition = (clamped + 1) / 2 * resolvedWidth;
7686
- const dotSize = 8;
7687
- const trackHeight = 6;
7688
- const containerHeight = 10;
7689
- const valueNow = Math.round(clamped * 100);
7553
+ if (!isActive) {
7554
+ return /* @__PURE__ */ jsx(TempoValue, { value, color: alpha(color, 0.35), fontSize });
7555
+ }
7556
+ const targetMs = value > 0 ? value * 1e3 : null;
7557
+ const barColor = getTempoPacingState(phaseElapsedMs, targetMs) === "behind" ? STATUS_ERROR : color;
7558
+ const fillPct = getTempoFillPct(phaseElapsedMs, targetMs);
7559
+ return /* @__PURE__ */ jsxs(reactNative.View, { style: { position: "relative" }, testID: "tempo-live-active", children: [
7560
+ /* @__PURE__ */ jsx(
7561
+ reactNative.View,
7562
+ {
7563
+ style: {
7564
+ position: "absolute",
7565
+ left: 0,
7566
+ top: 0,
7567
+ bottom: 0,
7568
+ width: `${fillPct}%`,
7569
+ backgroundColor: alpha(barColor, 0.3),
7570
+ borderRadius: 2
7571
+ }
7572
+ }
7573
+ ),
7574
+ /* @__PURE__ */ jsx(TempoValue, { value, color: barColor, fontSize })
7575
+ ] });
7576
+ }
7577
+ function LiveTempoRow({
7578
+ values,
7579
+ live,
7580
+ fontSize
7581
+ }) {
7582
+ return /* @__PURE__ */ jsx(jsxRuntime.Fragment, { children: LIVE_PHASES.map((phase, i) => /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row" }, children: [
7583
+ i > 0 && /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7584
+ /* @__PURE__ */ jsx(
7585
+ LiveTempoCell,
7586
+ {
7587
+ value: values[i],
7588
+ color: phase.color,
7589
+ isActive: live.activePhase === phase.key,
7590
+ phaseElapsedMs: live.phaseElapsedMs,
7591
+ fontSize
7592
+ }
7593
+ )
7594
+ ] }, phase.key)) });
7595
+ }
7596
+ function TempoDisplay({
7597
+ tempo,
7598
+ size = "md",
7599
+ colored = true,
7600
+ showInfo = true,
7601
+ live,
7602
+ onPress,
7603
+ className,
7604
+ ...props
7605
+ }) {
7606
+ const [showTooltip, setShowTooltip] = React24.useState(false);
7607
+ const [eccentric, pauseBottom, concentric, pauseTop] = roundTempo(tempo);
7608
+ const isSm = size === "sm";
7609
+ const fontSize = isSm ? 9 : 11;
7610
+ const monoColor = TEXT_TERTIARY;
7611
+ const handlePress = React24.useCallback(() => {
7612
+ if (onPress) {
7613
+ onPress();
7614
+ }
7615
+ if (showInfo) {
7616
+ setShowTooltip((prev) => !prev);
7617
+ }
7618
+ }, [onPress, showInfo]);
7619
+ const content = /* @__PURE__ */ jsxs(
7620
+ reactNative.View,
7621
+ {
7622
+ className,
7623
+ style: {
7624
+ flexDirection: "row",
7625
+ alignItems: "center",
7626
+ backgroundColor: "#1C1C1C",
7627
+ paddingHorizontal: isSm ? 6 : 8,
7628
+ paddingVertical: 3,
7629
+ borderRadius: 4
7630
+ },
7631
+ ...props,
7632
+ children: [
7633
+ /* @__PURE__ */ jsx(
7634
+ reactNative.Text,
7635
+ {
7636
+ style: {
7637
+ fontFamily: INTER,
7638
+ fontSize: 9,
7639
+ fontWeight: "500",
7640
+ color: TEXT_TERTIARY,
7641
+ letterSpacing: 0.5,
7642
+ textTransform: "uppercase",
7643
+ marginRight: 6
7644
+ },
7645
+ children: "TEMPO"
7646
+ }
7647
+ ),
7648
+ /* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row" }, testID: "tempo-value", children: live ? /* @__PURE__ */ jsx(
7649
+ LiveTempoRow,
7650
+ {
7651
+ values: [eccentric, pauseBottom, concentric, pauseTop],
7652
+ live,
7653
+ fontSize
7654
+ }
7655
+ ) : colored ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
7656
+ /* @__PURE__ */ jsx(TempoValue, { value: eccentric, color: phaseColors.eccentric, fontSize }),
7657
+ /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7658
+ /* @__PURE__ */ jsx(TempoValue, { value: pauseBottom, color: phaseColors.pauseBottom, fontSize }),
7659
+ /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7660
+ /* @__PURE__ */ jsx(TempoValue, { value: concentric, color: phaseColors.concentric, fontSize }),
7661
+ /* @__PURE__ */ jsx(TempoSeparator, { color: phaseColors.dash, fontSize }),
7662
+ /* @__PURE__ */ jsx(TempoValue, { value: pauseTop, color: phaseColors.pauseTop, fontSize })
7663
+ ] }) : /* @__PURE__ */ jsxs(
7664
+ reactNative.Text,
7665
+ {
7666
+ style: {
7667
+ fontFamily: INTER,
7668
+ fontSize,
7669
+ color: monoColor,
7670
+ fontWeight: "600",
7671
+ letterSpacing: 1
7672
+ },
7673
+ children: [
7674
+ eccentric,
7675
+ "-",
7676
+ pauseBottom,
7677
+ "-",
7678
+ concentric,
7679
+ "-",
7680
+ pauseTop
7681
+ ]
7682
+ }
7683
+ ) })
7684
+ ]
7685
+ }
7686
+ );
7687
+ return /* @__PURE__ */ jsxs(
7688
+ reactNative.Pressable,
7689
+ {
7690
+ accessibilityLabel: `Tempo: ${eccentric} second eccentric, ${pauseBottom} second pause, ${concentric} second concentric, ${pauseTop} second pause`,
7691
+ accessibilityRole: "button",
7692
+ onPress: handlePress,
7693
+ testID: "tempo-display",
7694
+ children: [
7695
+ content,
7696
+ showTooltip && /* @__PURE__ */ jsxs(
7697
+ reactNative.View,
7698
+ {
7699
+ style: {
7700
+ position: "absolute",
7701
+ bottom: "100%",
7702
+ left: "50%",
7703
+ transform: [{ translateX: "-50%" }],
7704
+ marginBottom: 8,
7705
+ zIndex: 20,
7706
+ alignItems: "center"
7707
+ },
7708
+ testID: "tempo-tooltip",
7709
+ children: [
7710
+ /* @__PURE__ */ jsxs(
7711
+ reactNative.View,
7712
+ {
7713
+ style: {
7714
+ backgroundColor: "#191919",
7715
+ borderRadius: 6,
7716
+ paddingVertical: 8,
7717
+ paddingHorizontal: 12,
7718
+ borderWidth: 1,
7719
+ borderColor: "#2C2C2C"
7720
+ },
7721
+ children: [
7722
+ /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7723
+ "Eccentric: ",
7724
+ eccentric,
7725
+ "s"
7726
+ ] }),
7727
+ /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7728
+ "Pause (bottom): ",
7729
+ pauseBottom,
7730
+ "s"
7731
+ ] }),
7732
+ /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7733
+ "Concentric: ",
7734
+ concentric,
7735
+ "s"
7736
+ ] }),
7737
+ /* @__PURE__ */ jsxs(reactNative.Text, { style: { fontSize: 10, lineHeight: 16, color: "#9CA3AF", fontFamily: INTER }, children: [
7738
+ "Pause (top): ",
7739
+ pauseTop,
7740
+ "s"
7741
+ ] })
7742
+ ]
7743
+ }
7744
+ ),
7745
+ /* @__PURE__ */ jsx(
7746
+ reactNative.View,
7747
+ {
7748
+ style: {
7749
+ width: 0,
7750
+ height: 0,
7751
+ borderLeftWidth: 5,
7752
+ borderRightWidth: 5,
7753
+ borderTopWidth: 5,
7754
+ borderLeftColor: "transparent",
7755
+ borderRightColor: "transparent",
7756
+ borderTopColor: "#2C2C2C"
7757
+ }
7758
+ }
7759
+ )
7760
+ ]
7761
+ }
7762
+ )
7763
+ ]
7764
+ }
7765
+ );
7766
+ }
7767
+ function getDotColor(deviation) {
7768
+ const abs = Math.abs(deviation);
7769
+ if (deviation < -0.3) return "#14B8A6";
7770
+ if (abs <= 0.3) return "#6B7280";
7771
+ if (abs <= 0.7) return "#FFB020";
7772
+ return "#D14343";
7773
+ }
7774
+ function getDeviationDescription(deviation) {
7775
+ if (deviation < -0.3) return "lighter than planned";
7776
+ if (deviation > 0.3) return "harder than planned";
7777
+ return "on plan";
7778
+ }
7779
+ function DeviationBar({
7780
+ deviation,
7781
+ width,
7782
+ className,
7783
+ ...props
7784
+ }) {
7785
+ const clamped = Math.max(-1, Math.min(1, deviation));
7786
+ const resolvedWidth = width ?? 40;
7787
+ const dotPosition = (clamped + 1) / 2 * resolvedWidth;
7788
+ const dotSize = 8;
7789
+ const trackHeight = 6;
7790
+ const containerHeight = 10;
7791
+ const valueNow = Math.round(clamped * 100);
7690
7792
  return /* @__PURE__ */ jsxs(
7691
7793
  reactNative.View,
7692
7794
  {
@@ -7735,19 +7837,42 @@ function DeviationBar({
7735
7837
  }
7736
7838
  );
7737
7839
  }
7738
- var TRACK_BG = "rgba(255,255,255,0.06)";
7739
- var THRESHOLD_COLOR = "#FFFFFF";
7840
+ var TRACK_BG = "#333333";
7740
7841
  var LABEL_COLOR = "#6B7280";
7741
- function getZoneColor(level) {
7742
- if (level >= 0.7) return "#D14343";
7743
- if (level >= 0.4) return "#FFB020";
7744
- return "#14B8A6";
7745
- }
7746
- function clamp01(value) {
7842
+ var TARGET_LINE_COLOR = "rgba(33, 150, 243, 0.5)";
7843
+ var AT_TARGET_GLOW = "0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)";
7844
+ var ZONE = {
7845
+ building: "#14B8A6",
7846
+ // teal — below target ramp-up
7847
+ approaching: "#FFB020",
7848
+ // amber — nearing target
7849
+ target: "#FF7900",
7850
+ // brand — exactly at target
7851
+ over1: "#D14343",
7852
+ // red — mild over-target
7853
+ over2: "#A62626",
7854
+ // deep red — moderate over-target
7855
+ over3: "#7A1C1C"
7856
+ // darkest red — severe over-target
7857
+ };
7858
+ var BULGE_SIZE = { 1: 8, 2: 10, 3: 11 };
7859
+ function zoneForPct(pct) {
7860
+ if (pct >= 125) return { color: ZONE.over3, over: 3 };
7861
+ if (pct >= 115) return { color: ZONE.over2, over: 2 };
7862
+ if (pct > 100) return { color: ZONE.over1, over: 1 };
7863
+ if (pct === 100) return { color: ZONE.target, over: 0 };
7864
+ if (pct >= 75) return { color: ZONE.approaching, over: 0 };
7865
+ return { color: ZONE.building, over: 0 };
7866
+ }
7867
+ function clampFill(value) {
7747
7868
  if (value < 0) return 0;
7748
7869
  if (value > 1) return 1;
7749
7870
  return value;
7750
7871
  }
7872
+ function isAtTarget(pct, threshold) {
7873
+ if (threshold == null) return false;
7874
+ return Math.abs(pct - Math.round(threshold * 100)) <= 5;
7875
+ }
7751
7876
  function IntensityBar({
7752
7877
  level,
7753
7878
  threshold,
@@ -7759,62 +7884,62 @@ function IntensityBar({
7759
7884
  ...props
7760
7885
  }) {
7761
7886
  const isVertical = orientation === "vertical";
7762
- const clampedLevel = clamp01(level);
7763
- const percentage = Math.round(clampedLevel * 100);
7764
- const zoneColor = getZoneColor(clampedLevel);
7765
- const [fillAnim] = React24.useState(() => new reactNative.Animated.Value(clampedLevel));
7887
+ const fillLevel = clampFill(level);
7888
+ const pct = Math.round(Math.max(0, level) * 100);
7889
+ const zone = zoneForPct(pct);
7890
+ const atTarget = isAtTarget(pct, threshold);
7891
+ const [fillAnim] = React24.useState(() => new reactNative.Animated.Value(fillLevel));
7766
7892
  React24.useEffect(() => {
7767
7893
  const animation = reactNative.Animated.timing(fillAnim, {
7768
- toValue: clampedLevel,
7894
+ toValue: fillLevel,
7769
7895
  duration: 250,
7770
7896
  useNativeDriver: false
7771
7897
  });
7772
7898
  animation.start();
7773
7899
  return () => animation.stop();
7774
- }, [clampedLevel, fillAnim]);
7900
+ }, [fillLevel, fillAnim]);
7775
7901
  const fillSizePercent = fillAnim.interpolate({
7776
7902
  inputRange: [0, 1],
7777
7903
  outputRange: ["0%", "100%"]
7778
7904
  });
7779
7905
  const trackStyle = isVertical ? { width: thickness, height: size } : { width: size, height: thickness };
7780
- const fillStyle = isVertical ? {
7781
- position: "absolute",
7782
- bottom: 0,
7783
- left: 0,
7784
- right: 0,
7785
- height: fillSizePercent
7786
- } : {
7906
+ const fillStyle = isVertical ? { position: "absolute", bottom: 0, left: 0, right: 0, height: fillSizePercent } : { position: "absolute", top: 0, bottom: 0, left: 0, width: fillSizePercent };
7907
+ const bulgeSize = zone.over === 0 ? 0 : BULGE_SIZE[zone.over];
7908
+ const bulgeStyle = {
7787
7909
  position: "absolute",
7788
- top: 0,
7789
- bottom: 0,
7790
- left: 0,
7791
- width: fillSizePercent
7910
+ width: bulgeSize,
7911
+ height: bulgeSize,
7912
+ borderRadius: bulgeSize / 2,
7913
+ backgroundColor: zone.color,
7914
+ zIndex: 1,
7915
+ ...isVertical ? { top: 0, left: "50%", transform: [{ translateX: -bulgeSize / 2 }] } : { right: 0, top: "50%", transform: [{ translateY: -bulgeSize / 2 }] }
7792
7916
  };
7793
- const thresholdLineStyle = threshold != null ? isVertical ? {
7917
+ const targetPct = `${clampFill(threshold ?? 0) * 100}%`;
7918
+ const targetLineStyle = isVertical ? {
7794
7919
  position: "absolute",
7795
- bottom: `${clamp01(threshold) * 100}%`,
7796
- left: -2,
7797
- right: -2,
7798
- height: 1,
7799
- backgroundColor: THRESHOLD_COLOR,
7920
+ bottom: targetPct,
7921
+ left: -3,
7922
+ right: -3,
7923
+ height: 1.5,
7924
+ backgroundColor: TARGET_LINE_COLOR,
7800
7925
  zIndex: 4
7801
7926
  } : {
7802
7927
  position: "absolute",
7803
- left: `${clamp01(threshold) * 100}%`,
7804
- top: -2,
7805
- bottom: -2,
7806
- width: 1,
7807
- backgroundColor: THRESHOLD_COLOR,
7928
+ left: targetPct,
7929
+ top: -3,
7930
+ bottom: -3,
7931
+ width: 1.5,
7932
+ backgroundColor: TARGET_LINE_COLOR,
7808
7933
  zIndex: 4
7809
- } : void 0;
7934
+ };
7810
7935
  return /* @__PURE__ */ jsxs(
7811
7936
  reactNative.View,
7812
7937
  {
7813
7938
  className,
7814
7939
  style: { alignItems: "center" },
7815
7940
  accessibilityRole: "progressbar",
7816
- accessibilityValue: { min: 0, max: 100, now: percentage },
7817
- accessibilityLabel: `Intensity level: ${percentage}%`,
7941
+ accessibilityValue: { min: 0, max: 100, now: pct },
7942
+ accessibilityLabel: `Intensity level: ${pct}%`,
7818
7943
  testID: "intensity-bar",
7819
7944
  ...props,
7820
7945
  children: [
@@ -7836,13 +7961,32 @@ function IntensityBar({
7836
7961
  style: {
7837
7962
  ...fillStyle,
7838
7963
  borderRadius: 3,
7839
- backgroundColor: zoneColor,
7840
- zIndex: 2
7964
+ backgroundColor: zone.color,
7965
+ zIndex: 2,
7966
+ ...atTarget ? { boxShadow: AT_TARGET_GLOW } : null
7841
7967
  },
7842
7968
  testID: "intensity-fill"
7843
7969
  }
7844
7970
  ),
7845
- threshold != null && /* @__PURE__ */ jsx(reactNative.View, { style: thresholdLineStyle, testID: "intensity-threshold" })
7971
+ zone.over !== 0 && /* @__PURE__ */ jsx(reactNative.View, { style: bulgeStyle, testID: "intensity-bulge" }),
7972
+ threshold != null && /* @__PURE__ */ jsx(reactNative.View, { style: targetLineStyle, testID: "intensity-target" })
7973
+ ]
7974
+ }
7975
+ ),
7976
+ /* @__PURE__ */ jsxs(
7977
+ reactNative.Text,
7978
+ {
7979
+ style: {
7980
+ fontSize: 8,
7981
+ color: LABEL_COLOR,
7982
+ fontFamily: '"Nunito Sans", sans-serif',
7983
+ marginTop: 6
7984
+ },
7985
+ accessibilityElementsHidden: true,
7986
+ testID: "intensity-label",
7987
+ children: [
7988
+ pct,
7989
+ "%"
7846
7990
  ]
7847
7991
  }
7848
7992
  ),
@@ -7854,7 +7998,7 @@ function IntensityBar({
7854
7998
  fontWeight: "500",
7855
7999
  color: LABEL_COLOR,
7856
8000
  fontFamily: "Inter, sans-serif",
7857
- marginTop: 6
8001
+ marginTop: 2
7858
8002
  },
7859
8003
  accessibilityElementsHidden: true,
7860
8004
  testID: "intensity-threshold-label",
@@ -8018,6 +8162,13 @@ function WorkoutPill({
8018
8162
  return pill;
8019
8163
  }
8020
8164
  var VEL_COLORS = WORKOUT_TOKENS.scale;
8165
+ var zoneIdToScaleToken = {
8166
+ speed: "green",
8167
+ power: "yellow",
8168
+ strengthSpeed: "orange",
8169
+ maximalStrength: "red",
8170
+ grinding: "red"
8171
+ };
8021
8172
  function getVelocityZoneColor(velocity) {
8022
8173
  if (velocity >= 1) return "vel-green";
8023
8174
  if (velocity >= 0.75) return "vel-yellow";
@@ -8030,47 +8181,62 @@ function getVelocityZoneName(velocity) {
8030
8181
  if (velocity >= 0.5) return "Strength-Speed";
8031
8182
  return "Strength";
8032
8183
  }
8184
+ var zoneHexMap = {
8185
+ "vel-green": VEL_COLORS.green,
8186
+ "vel-yellow": VEL_COLORS.yellow,
8187
+ "vel-orange": VEL_COLORS.orange,
8188
+ "vel-red": VEL_COLORS.red
8189
+ };
8033
8190
  function calculateVelocityLoss(velocities) {
8034
8191
  if (velocities.length < 2) return 0;
8035
- const first = velocities[0];
8192
+ const best = Math.max(...velocities);
8193
+ if (best <= 0) return 0;
8036
8194
  const last = velocities[velocities.length - 1];
8037
- if (first === 0) return 0;
8038
- return Math.round((first - last) / first * 100);
8195
+ return Math.max(0, Math.round((best - last) / best * 100));
8039
8196
  }
8040
8197
  function calculateMeanVelocity(velocities) {
8041
8198
  if (velocities.length === 0) return 0;
8042
8199
  const sum = velocities.reduce((acc, v) => acc + v, 0);
8043
8200
  return sum / velocities.length;
8044
8201
  }
8045
- function getBarColor(zone) {
8046
- switch (zone) {
8047
- case "vel-green":
8048
- return VEL_COLORS.green;
8049
- case "vel-yellow":
8050
- return VEL_COLORS.yellow;
8051
- case "vel-orange":
8052
- return VEL_COLORS.orange;
8053
- case "vel-red":
8054
- return VEL_COLORS.red;
8055
- default:
8056
- return VEL_COLORS.green;
8202
+ function classifyBand(velocity, bands) {
8203
+ for (const band of bands) {
8204
+ if (band.max === null || velocity < band.max) return band;
8057
8205
  }
8206
+ return bands[bands.length - 1];
8207
+ }
8208
+ function bandColor(band) {
8209
+ const token = zoneIdToScaleToken[band.id];
8210
+ return token ? VEL_COLORS[token] : VEL_COLORS.green;
8058
8211
  }
8059
- var zoneHexMap = {
8060
- "vel-green": VEL_COLORS.green,
8061
- "vel-yellow": VEL_COLORS.yellow,
8062
- "vel-orange": VEL_COLORS.orange,
8063
- "vel-red": VEL_COLORS.red
8064
- };
8065
8212
  function getLossStyle(loss) {
8066
8213
  if (loss > 25) return { color: VEL_COLORS.red };
8067
8214
  if (loss > 20) return { color: VEL_COLORS.orange };
8068
8215
  return null;
8069
8216
  }
8217
+ function getReducedMotionPreference() {
8218
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
8219
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8220
+ }
8221
+ function usePrefersReducedMotion() {
8222
+ const [reduced, setReduced] = React24.useState(getReducedMotionPreference);
8223
+ React24.useEffect(() => {
8224
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
8225
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
8226
+ const handler = () => setReduced(mq.matches);
8227
+ handler();
8228
+ mq.addEventListener?.("change", handler);
8229
+ return () => mq.removeEventListener?.("change", handler);
8230
+ }, []);
8231
+ return reduced;
8232
+ }
8070
8233
  var ANIMATION_DURATION = 400;
8071
8234
  var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
8235
+ var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
8072
8236
  function VelocityStrip({
8073
8237
  velocities,
8238
+ zones,
8239
+ liveRepIndex,
8074
8240
  expanded = false,
8075
8241
  onToggle,
8076
8242
  onRepPress,
@@ -8082,10 +8248,48 @@ function VelocityStrip({
8082
8248
  const maxVelocity = Math.max(...velocities, 0);
8083
8249
  const meanVelocity = calculateMeanVelocity(velocities);
8084
8250
  const loss = calculateVelocityLoss(velocities);
8085
- const meanZone = getVelocityZoneName(meanVelocity);
8251
+ const hasZones = zones != null && zones.length > 0;
8252
+ const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
8253
+ const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
8254
+ const prefersReducedMotion = usePrefersReducedMotion();
8086
8255
  const [heightAnim] = React24.useState(() => new reactNative.Animated.Value(expanded ? 60 : 3));
8087
8256
  const [labelOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
8088
8257
  const [infoOpacity] = React24.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
8258
+ const [liveScale] = React24.useState(() => new reactNative.Animated.Value(1));
8259
+ const liveVelocity = liveRepIndex != null ? velocities[liveRepIndex] : void 0;
8260
+ const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
8261
+ React24.useEffect(() => {
8262
+ if (variant === "mini" || liveRepIndex == null || liveVelocity == null) return;
8263
+ if (prefersReducedMotion) {
8264
+ liveScale.setValue(1);
8265
+ return;
8266
+ }
8267
+ if (isNewPeak) {
8268
+ liveScale.setValue(1);
8269
+ reactNative.Animated.sequence([
8270
+ reactNative.Animated.timing(liveScale, {
8271
+ toValue: 1.25,
8272
+ duration: 150,
8273
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
8274
+ useNativeDriver: true
8275
+ }),
8276
+ reactNative.Animated.spring(liveScale, {
8277
+ toValue: 1,
8278
+ friction: 3,
8279
+ tension: 140,
8280
+ useNativeDriver: true
8281
+ })
8282
+ ]).start();
8283
+ } else {
8284
+ liveScale.setValue(0.8);
8285
+ reactNative.Animated.timing(liveScale, {
8286
+ toValue: 1,
8287
+ duration: 300,
8288
+ easing: POP_EASING,
8289
+ useNativeDriver: true
8290
+ }).start();
8291
+ }
8292
+ }, [variant, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
8089
8293
  React24.useEffect(() => {
8090
8294
  if (variant === "mini") return;
8091
8295
  reactNative.Animated.parallel([
@@ -8125,7 +8329,7 @@ function VelocityStrip({
8125
8329
  {
8126
8330
  style: {
8127
8331
  flex: 1,
8128
- backgroundColor: getBarColor(getVelocityZoneColor(v)),
8332
+ backgroundColor: barColorFor(v),
8129
8333
  borderRadius: 1,
8130
8334
  minWidth: 4,
8131
8335
  height: "100%"
@@ -8168,8 +8372,35 @@ function VelocityStrip({
8168
8372
  {
8169
8373
  style: { flexDirection: "row", flex: 1, gap: 2, alignItems: "flex-end" },
8170
8374
  children: velocities.map((v, i) => {
8171
- const zone = getVelocityZoneColor(v);
8375
+ const barBackground = barColorFor(v);
8172
8376
  const barHeightPct = maxVelocity > 0 ? Math.round(v / (maxVelocity * 1.15) * 100) : 0;
8377
+ const isLive = liveRepIndex === i;
8378
+ const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
8379
+ const barStyle = expanded ? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: barBackground } : {
8380
+ minHeight: "100%",
8381
+ borderTopLeftRadius: 2,
8382
+ borderTopRightRadius: 2,
8383
+ borderBottomLeftRadius: 0,
8384
+ borderBottomRightRadius: 0,
8385
+ backgroundColor: barBackground
8386
+ };
8387
+ const barInner = isLive ? /* @__PURE__ */ jsx(
8388
+ reactNative.Animated.View,
8389
+ {
8390
+ style: [barStyle, { transform: [{ scale: liveScale }] }],
8391
+ testID: `velocity-bar-${i}`,
8392
+ accessibilityRole: "image",
8393
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
8394
+ }
8395
+ ) : /* @__PURE__ */ jsx(
8396
+ reactNative.View,
8397
+ {
8398
+ style: barStyle,
8399
+ testID: `velocity-bar-${i}`,
8400
+ accessibilityRole: "image",
8401
+ accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
8402
+ }
8403
+ );
8173
8404
  const bar = /* @__PURE__ */ jsxs(
8174
8405
  reactNative.View,
8175
8406
  {
@@ -8184,22 +8415,7 @@ function VelocityStrip({
8184
8415
  children: formatVelocity(v)
8185
8416
  }
8186
8417
  ) }),
8187
- /* @__PURE__ */ jsx(
8188
- reactNative.View,
8189
- {
8190
- style: expanded ? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: zoneHexMap[zone] } : {
8191
- minHeight: "100%",
8192
- borderTopLeftRadius: 2,
8193
- borderTopRightRadius: 2,
8194
- borderBottomLeftRadius: 0,
8195
- borderBottomRightRadius: 0,
8196
- backgroundColor: zoneHexMap[zone]
8197
- },
8198
- testID: `velocity-bar-${i}`,
8199
- accessibilityRole: "image",
8200
- accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
8201
- }
8202
- )
8418
+ barInner
8203
8419
  ]
8204
8420
  },
8205
8421
  i
@@ -8532,6 +8748,8 @@ function SetRow({
8532
8748
  rpe,
8533
8749
  unit,
8534
8750
  velocities,
8751
+ velocityZones,
8752
+ velocityLiveRepIndex,
8535
8753
  velocityExpanded,
8536
8754
  onVelocityToggle,
8537
8755
  setType,
@@ -8734,6 +8952,8 @@ function SetRow({
8734
8952
  VelocityStrip,
8735
8953
  {
8736
8954
  velocities,
8955
+ zones: velocityZones,
8956
+ liveRepIndex: velocityLiveRepIndex,
8737
8957
  expanded: velocityExpanded,
8738
8958
  onToggle: onVelocityToggle,
8739
8959
  variant: onVelocityToggle ? "full" : "mini"
@@ -8908,7 +9128,8 @@ function RestTimer({
8908
9128
  onSkip,
8909
9129
  onAddTime,
8910
9130
  nextSetInfo,
8911
- visible
9131
+ visible,
9132
+ displayOnly = false
8912
9133
  }) {
8913
9134
  if (!visible) return null;
8914
9135
  const remainingMs = Math.max(0, totalSeconds * 1e3 - elapsedMs);
@@ -9015,7 +9236,7 @@ function RestTimer({
9015
9236
  )
9016
9237
  }
9017
9238
  ),
9018
- /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
9239
+ !displayOnly && /* @__PURE__ */ jsxs(reactNative.View, { style: { flexDirection: "row", gap: 8 }, children: [
9019
9240
  /* @__PURE__ */ jsx(
9020
9241
  reactNative.Pressable,
9021
9242
  {
@@ -9132,6 +9353,7 @@ function CollapsedCard({
9132
9353
  e1rm,
9133
9354
  isPR,
9134
9355
  setVelocities,
9356
+ velocityZones,
9135
9357
  totalPlannedSets,
9136
9358
  supersetPosition
9137
9359
  }) {
@@ -9219,6 +9441,7 @@ function CollapsedCard({
9219
9441
  VelocityStrip,
9220
9442
  {
9221
9443
  velocities,
9444
+ zones: velocityZones,
9222
9445
  variant: "mini",
9223
9446
  testID: `exercise-card-velocity-strip-${i}`
9224
9447
  },
@@ -10056,7 +10279,7 @@ function PrRecordRow({ record, index }) {
10056
10279
  },
10057
10280
  testID: `pr-history-modal-record-${index}`,
10058
10281
  children: [
10059
- /* @__PURE__ */ jsx(lucideReact.Star, { size: 16, color: BRAND_PRIMARY7, fill: BRAND_PRIMARY7, strokeWidth: 2 }),
10282
+ /* @__PURE__ */ jsx(StarIcon, { size: 16, color: BRAND_PRIMARY7, fill: BRAND_PRIMARY7, strokeWidth: 2 }),
10060
10283
  /* @__PURE__ */ jsx(
10061
10284
  reactNative.Text,
10062
10285
  {
@@ -10204,7 +10427,7 @@ var BRAND_PRIMARY8 = "#FF7900";
10204
10427
  var BRAND_PRIMARY_SUBTLE = "rgba(255,121,0,0.12)";
10205
10428
  var STATUS_SUCCESS = "#14B8A6";
10206
10429
  var STATUS_WARNING = "#FFB020";
10207
- var STATUS_ERROR = "#D14343";
10430
+ var STATUS_ERROR2 = "#D14343";
10208
10431
  var EMOJI_SETS = {
10209
10432
  sleep: ["\u{1F634}", "\u{1F610}", "\u{1F642}", "\u{1F60A}", "\u{1F929}"],
10210
10433
  soreness: ["\u{1F635}", "\u{1F623}", "\u{1F610}", "\u{1F642}", "\u{1F4AA}"],
@@ -10213,7 +10436,7 @@ var EMOJI_SETS = {
10213
10436
  };
10214
10437
  var DEFAULT_EMOJI_SET = ["\u{1F61E}", "\u{1F615}", "\u{1F610}", "\u{1F642}", "\u{1F604}"];
10215
10438
  function scoreColor(score) {
10216
- if (score < 40) return STATUS_ERROR;
10439
+ if (score < 40) return STATUS_ERROR2;
10217
10440
  if (score < 70) return STATUS_WARNING;
10218
10441
  return STATUS_SUCCESS;
10219
10442
  }
@@ -10408,7 +10631,7 @@ var STATUS_VARIANTS = {
10408
10631
  warning: { bg: "rgba(255,176,32,0.15)", border: "rgba(255,176,32,0.3)", text: WARNING },
10409
10632
  error: { bg: "rgba(209,67,67,0.15)", border: "rgba(209,67,67,0.25)", text: ERROR }
10410
10633
  };
10411
- function clamp012(value) {
10634
+ function clamp01(value) {
10412
10635
  if (!Number.isFinite(value)) return 0;
10413
10636
  if (value < 0) return 0;
10414
10637
  if (value > 1) return 1;
@@ -10503,7 +10726,7 @@ function MetricCell({ metric }) {
10503
10726
  ] });
10504
10727
  }
10505
10728
  function Gauge({ gauge }) {
10506
- const level = clamp012(gauge.level);
10729
+ const level = clamp01(gauge.level);
10507
10730
  const percentage = Math.round(level * 100);
10508
10731
  const markerColor = getGaugeZoneColor(level);
10509
10732
  return /* @__PURE__ */ jsxs(
@@ -10759,7 +10982,7 @@ function MesoStatusCard({
10759
10982
  var BRAND_PRIMARY10 = "#FF7900";
10760
10983
  var PROJECTION_LINE_COLOR = resolveColor("text-tertiary");
10761
10984
  var STATUS_SUCCESS2 = "#14B8A6";
10762
- var STATUS_ERROR2 = "#D14343";
10985
+ var STATUS_ERROR3 = "#D14343";
10763
10986
  var STATUS_WARNING2 = "#FFB020";
10764
10987
  var GRID_LINE = "rgba(255,255,255,0.06)";
10765
10988
  var SUCCESS_PILL_BG = "rgba(20,184,166,0.10)";
@@ -11159,7 +11382,7 @@ function StrengthTrendChart({
11159
11382
  marginTop: 1,
11160
11383
  fontSize: 10,
11161
11384
  fontFamily: "Inter, sans-serif",
11162
- color: selectedCoord.point.e1rm - geometry.actual[selected - 1].point.e1rm >= 0 ? STATUS_SUCCESS2 : STATUS_ERROR2
11385
+ color: selectedCoord.point.e1rm - geometry.actual[selected - 1].point.e1rm >= 0 ? STATUS_SUCCESS2 : STATUS_ERROR3
11163
11386
  },
11164
11387
  children: `${selectedCoord.point.e1rm - geometry.actual[selected - 1].point.e1rm >= 0 ? "+" : "-"}${Math.abs(
11165
11388
  selectedCoord.point.e1rm - geometry.actual[selected - 1].point.e1rm
@@ -11215,7 +11438,7 @@ function StrengthTrendChart({
11215
11438
  fontSize: 11,
11216
11439
  fontFamily: "Inter, sans-serif",
11217
11440
  fontWeight: "600",
11218
- color: trend.positive ? STATUS_SUCCESS2 : STATUS_ERROR2
11441
+ color: trend.positive ? STATUS_SUCCESS2 : STATUS_ERROR3
11219
11442
  },
11220
11443
  children: trendText
11221
11444
  }
@@ -11407,1231 +11630,230 @@ function CapacityBandChart({
11407
11630
  const toY2 = (value) => PADDING_TOP + (1 - (value - domainMin) / vRange) * plotHeight;
11408
11631
  return { toX: toX2, toY: toY2 };
11409
11632
  }, [band, workouts, projection, plotWidth, plotHeight]);
11410
- if (band.length === 0) {
11411
- return /* @__PURE__ */ jsx(
11412
- reactNative.View,
11413
- {
11414
- style: { width, height },
11415
- className,
11416
- accessibilityRole: "image",
11417
- accessibilityLabel: "Training capacity band chart, no data",
11418
- testID: "capacity-band-chart-empty",
11419
- ...props
11420
- }
11421
- );
11422
- }
11423
- const { toX, toY } = scale;
11424
- const bandPixels = toPixels(band, toX, toY);
11425
- const columns = buildColumns(bandPixels, COLUMN_STEP);
11426
- const topEdge = buildEdges(bandPixels, "yHigh");
11427
- const bottomEdge = buildEdges(bandPixels, "yLow");
11428
- const lastBandPoint = band[band.length - 1];
11429
- const trainingPixels = projection ? toPixels([lastBandPoint, ...projection.withTraining], toX, toY) : [];
11430
- const restPixels = projection ? toPixels([lastBandPoint, ...projection.withRest], toX, toY) : [];
11431
- const labelStride = Math.max(1, Math.ceil(band.length / 5));
11432
- const revealWidth = reveal.interpolate({ inputRange: [0, 1], outputRange: [0, width] });
11433
- const renderColumns = (cols, color, key) => cols.map((col, i) => /* @__PURE__ */ jsx(
11434
- reactNative.View,
11435
- {
11436
- style: {
11437
- position: "absolute",
11438
- left: col.x,
11439
- top: col.top,
11440
- width: COLUMN_STEP + 0.5,
11441
- height: col.height,
11442
- backgroundColor: color
11443
- },
11444
- accessibilityElementsHidden: true,
11445
- importantForAccessibility: "no-hide-descendants",
11446
- testID: key
11447
- },
11448
- `${key}-${i}`
11449
- ));
11450
- const renderEdges = (segments, color, dashed, key) => segments.map((seg, i) => /* @__PURE__ */ jsx(
11451
- reactNative.View,
11452
- {
11453
- style: {
11454
- position: "absolute",
11455
- left: seg.left,
11456
- top: seg.top,
11457
- width: seg.length,
11458
- height: dashed ? 0 : 1.5,
11459
- borderTopWidth: dashed ? 1 : 0,
11460
- borderTopColor: dashed ? color : void 0,
11461
- borderStyle: dashed ? "dashed" : "solid",
11462
- backgroundColor: dashed ? void 0 : color,
11463
- opacity: dashed ? 0.7 : 1,
11464
- transformOrigin: "0 0",
11465
- transform: [{ rotate: `${seg.angle}deg` }]
11466
- },
11467
- accessibilityElementsHidden: true,
11468
- importantForAccessibility: "no-hide-descendants",
11469
- testID: key
11470
- },
11471
- `${key}-${i}`
11472
- ));
11473
- return /* @__PURE__ */ jsxs(
11474
- reactNative.View,
11475
- {
11476
- style: { width, height, position: "relative" },
11477
- className,
11478
- testID: "capacity-band-chart-root",
11479
- ...props,
11480
- children: [
11481
- /* @__PURE__ */ jsxs(
11482
- reactNative.View,
11483
- {
11484
- style: { position: "absolute", top: 0, left: 0, width, height },
11485
- accessibilityRole: "image",
11486
- accessibilityLabel: `Training capacity band chart. Current capacity: ${currentStatus(
11487
- workouts
11488
- )}. ${workouts.length} workout${workouts.length === 1 ? "" : "s"} shown.`,
11489
- testID: "capacity-band-chart",
11490
- children: [
11491
- /* @__PURE__ */ jsx(
11492
- reactNative.Animated.View,
11493
- {
11494
- style: { position: "absolute", top: 0, left: 0, height, width: revealWidth, overflow: "hidden" },
11495
- accessibilityElementsHidden: true,
11496
- importantForAccessibility: "no-hide-descendants",
11497
- testID: "capacity-band-chart-reveal",
11498
- children: /* @__PURE__ */ jsxs(reactNative.View, { style: { width, height, position: "relative" }, children: [
11499
- projection && /* @__PURE__ */ jsxs(reactNative.View, { testID: "capacity-band-chart-projection", children: [
11500
- renderColumns(buildColumns(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
11501
- renderColumns(buildColumns(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
11502
- renderEdges(buildEdges(trainingPixels, "yHigh"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
11503
- renderEdges(buildEdges(trainingPixels, "yLow"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
11504
- renderEdges(buildEdges(restPixels, "yHigh"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
11505
- renderEdges(buildEdges(restPixels, "yLow"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
11506
- /* @__PURE__ */ jsx(
11507
- reactNative.Text,
11508
- {
11509
- style: {
11510
- position: "absolute",
11511
- left: trainingPixels[trainingPixels.length - 1]?.x + 2,
11512
- top: trainingPixels[trainingPixels.length - 1]?.yHigh - 12,
11513
- fontSize: 9,
11514
- fontFamily: "Inter, sans-serif",
11515
- color: STATUS_SUCCESS3
11516
- },
11517
- testID: "capacity-band-chart-projection-training-label",
11518
- children: "Training"
11519
- }
11520
- ),
11521
- /* @__PURE__ */ jsx(
11522
- reactNative.Text,
11523
- {
11524
- style: {
11525
- position: "absolute",
11526
- left: restPixels[restPixels.length - 1]?.x + 2,
11527
- top: restPixels[restPixels.length - 1]?.yLow + 2,
11528
- fontSize: 9,
11529
- fontFamily: "Inter, sans-serif",
11530
- color: STATUS_INFO
11531
- },
11532
- testID: "capacity-band-chart-projection-rest-label",
11533
- children: "Rest"
11534
- }
11535
- )
11536
- ] }),
11537
- /* @__PURE__ */ jsx(reactNative.View, { testID: "capacity-band-chart-band", children: renderColumns(columns, BAND_FILL, "capacity-band-chart-band-cell") }),
11538
- renderEdges(topEdge, BAND_EDGE, false, "capacity-band-chart-edge-top"),
11539
- renderEdges(bottomEdge, BAND_EDGE, false, "capacity-band-chart-edge-bottom")
11540
- ] })
11541
- }
11542
- ),
11543
- /* @__PURE__ */ jsx(
11544
- reactNative.Text,
11545
- {
11546
- className: "text-text-tertiary",
11547
- style: {
11548
- position: "absolute",
11549
- left: PADDING_LEFT / 2 - 14,
11550
- top: PADDING_TOP + plotHeight / 2 - 7,
11551
- width: 32,
11552
- textAlign: "center",
11553
- fontSize: 9,
11554
- fontFamily: "Inter, sans-serif",
11555
- transform: [{ rotate: "-90deg" }]
11556
- },
11557
- accessibilityElementsHidden: true,
11558
- importantForAccessibility: "no-hide-descendants",
11559
- testID: "capacity-band-chart-y-axis-label",
11560
- children: "Load"
11561
- }
11562
- ),
11563
- band.map((point, i) => {
11564
- if (i % labelStride !== 0 && i !== band.length - 1) return null;
11565
- return /* @__PURE__ */ jsx(
11566
- reactNative.Text,
11567
- {
11568
- className: "text-text-tertiary",
11569
- style: {
11570
- position: "absolute",
11571
- left: toX(point.date) - 14,
11572
- top: height - PADDING_BOTTOM + 2,
11573
- width: 28,
11574
- textAlign: "center",
11575
- fontSize: 10,
11576
- fontFamily: "Inter, sans-serif"
11577
- },
11578
- accessibilityElementsHidden: true,
11579
- importantForAccessibility: "no-hide-descendants",
11580
- testID: "capacity-band-chart-x-label",
11581
- children: formatDate2(point.date)
11582
- },
11583
- `x-${point.date}`
11584
- );
11585
- })
11586
- ]
11587
- }
11588
- ),
11589
- workouts.map((workout, i) => {
11590
- const cx = toX(workout.date);
11591
- const cy = toY(workout.load);
11592
- const color = DOT_COLORS[workout.status];
11593
- const dotStyle = {
11594
- width: DOT_SIZE,
11595
- height: DOT_SIZE,
11596
- borderRadius: DOT_SIZE / 2,
11597
- backgroundColor: color,
11598
- borderWidth: 2,
11599
- borderColor: DOT_BORDER
11600
- };
11601
- const label = `Workout on ${formatDate2(workout.date)}, load ${workout.load}, ${STATUS_PHRASES[workout.status]}`;
11602
- return /* @__PURE__ */ jsx(
11603
- reactNative.Animated.View,
11604
- {
11605
- style: {
11606
- position: "absolute",
11607
- left: cx - DOT_SIZE / 2,
11608
- top: cy - DOT_SIZE / 2,
11609
- transform: [{ scale: dotAnims[i] ?? 1 }]
11610
- },
11611
- testID: "capacity-band-chart-dot-wrapper",
11612
- children: onWorkoutPress ? /* @__PURE__ */ jsx(
11613
- reactNative.Pressable,
11614
- {
11615
- onPress: () => onWorkoutPress(workout),
11616
- accessibilityRole: "button",
11617
- accessibilityLabel: label,
11618
- style: dotStyle,
11619
- testID: "capacity-band-chart-dot"
11620
- }
11621
- ) : /* @__PURE__ */ jsx(
11622
- reactNative.View,
11623
- {
11624
- accessibilityElementsHidden: true,
11625
- importantForAccessibility: "no-hide-descendants",
11626
- style: dotStyle,
11627
- testID: "capacity-band-chart-dot"
11628
- }
11629
- )
11630
- },
11631
- `dot-${workout.date}-${i}`
11632
- );
11633
- })
11634
- ]
11635
- }
11636
- );
11637
- }
11638
- var BRAND_PRIMARY11 = "#FF7900";
11639
- var WORKOUT_PILL_STATUS = {
11640
- completed: "completed",
11641
- today: "current",
11642
- upcoming: "upcoming"
11643
- };
11644
- function deriveNavLevel(selection) {
11645
- if (selection.workoutId != null) return "workout";
11646
- if (selection.weekNumber != null) return "week";
11647
- return "meso";
11648
- }
11649
- function toProgressBarMesos(mesos) {
11650
- return mesos.map(({ id, name, weekCount, status, currentWeek }) => ({
11651
- id,
11652
- name,
11653
- weekCount,
11654
- status,
11655
- currentWeek
11656
- }));
11657
- }
11658
- function findMeso(mesos, id) {
11659
- if (id == null) return null;
11660
- return mesos.find((meso) => meso.id === id) ?? null;
11661
- }
11662
- function findWeek(meso, weekNumber) {
11663
- if (meso == null || weekNumber == null) return null;
11664
- return meso.weeks.find((week) => week.weekNumber === weekNumber) ?? null;
11665
- }
11666
- function findWorkout(week, workoutId) {
11667
- if (week == null || workoutId == null) return null;
11668
- return week.workouts.find((workout) => workout.id === workoutId) ?? null;
11669
- }
11670
- function buildBreadcrumbs(meso, weekNumber, workout) {
11671
- const crumbs = [{ key: "meso", label: meso.name, level: "meso" }];
11672
- if (weekNumber != null) {
11673
- crumbs.push({ key: "week", label: `Week ${weekNumber}`, level: "week" });
11674
- }
11675
- if (workout != null) {
11676
- crumbs.push({ key: "workout", label: workout.name, level: "workout" });
11677
- }
11678
- return crumbs;
11679
- }
11680
- function Breadcrumbs2({ crumbs, onNavigate }) {
11681
- return /* @__PURE__ */ jsx(
11682
- reactNative.View,
11683
- {
11684
- className: "flex-row items-center flex-wrap",
11685
- style: { gap: 4 },
11686
- testID: "program-planning-page-breadcrumbs",
11687
- children: crumbs.map((crumb, index) => {
11688
- const isLast = index === crumbs.length - 1;
11689
- return /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { gap: 4 }, children: [
11690
- index > 0 && /* @__PURE__ */ jsx(reactNative.Text, { className: "text-text-tertiary", style: { fontSize: 12 }, children: "\u203A" }),
11691
- /* @__PURE__ */ jsx(
11692
- reactNative.Pressable,
11693
- {
11694
- onPress: () => onNavigate(crumb.level),
11695
- disabled: isLast,
11696
- accessibilityRole: "button",
11697
- testID: `program-planning-page-crumb-${crumb.key}`,
11698
- children: /* @__PURE__ */ jsx(
11699
- reactNative.Text,
11700
- {
11701
- className: isLast ? "text-text-primary" : void 0,
11702
- style: {
11703
- fontSize: 12,
11704
- fontFamily: "Inter, sans-serif",
11705
- fontWeight: isLast ? "700" : "500",
11706
- color: isLast ? void 0 : BRAND_PRIMARY11
11707
- },
11708
- children: crumb.label
11709
- }
11710
- )
11711
- }
11712
- )
11713
- ] }, crumb.key);
11714
- })
11715
- }
11716
- );
11717
- }
11718
- function toWeekRow(week, meso, onSelectWeek) {
11719
- const workouts = week.workouts.map((workout) => ({
11720
- name: workout.name,
11721
- status: WORKOUT_PILL_STATUS[workout.status],
11722
- onPress: () => onSelectWeek(week.weekNumber)
11723
- }));
11724
- return {
11725
- weekNumber: week.weekNumber,
11726
- totalWeeks: meso.weekCount,
11727
- workouts,
11728
- intensityLevel: week.intensityLevel,
11729
- intensityThreshold: week.intensityThreshold,
11730
- isCurrent: week.isCurrent ?? week.weekNumber === meso.currentWeek,
11731
- isDeload: week.isDeload
11732
- };
11733
- }
11734
- function MesoLevel({ mesos, activeMesoId, onSelectMeso, onSelectWeek }) {
11735
- return /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 12 }, testID: "program-planning-page-meso-level", children: mesos.map((meso) => {
11736
- const expanded = meso.id === activeMesoId;
11737
- return /* @__PURE__ */ jsx(
11738
- MesoCard,
11739
- {
11740
- name: meso.name,
11741
- goal: meso.goal,
11742
- split: meso.split,
11743
- weekRange: meso.weekRange,
11744
- currentWeek: meso.currentWeek,
11745
- totalWeeks: meso.weekCount,
11746
- weeks: expanded ? meso.weeks.map((week) => toWeekRow(week, meso, onSelectWeek)) : [],
11747
- volumeHeatmap: meso.volumeHeatmap,
11748
- expanded,
11749
- onToggle: () => onSelectMeso(meso.id),
11750
- highlighted: expanded
11751
- },
11752
- meso.id
11753
- );
11754
- }) });
11755
- }
11756
- function WeekLevel({ week, onSelectWorkout }) {
11757
- return /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 10 }, testID: "program-planning-page-week-level", children: week.workouts.map((workout) => /* @__PURE__ */ jsx(
11758
- WorkoutCard,
11759
- {
11760
- name: workout.name,
11761
- date: workout.date,
11762
- duration: workout.duration,
11763
- status: workout.status,
11764
- muscleGroups: workout.muscleGroups,
11765
- totalSets: workout.totalSets,
11766
- totalVolume: workout.totalVolume,
11767
- unit: workout.unit,
11768
- exercises: workout.exercises,
11769
- expanded: false,
11770
- onToggle: () => onSelectWorkout(workout.id)
11771
- },
11772
- workout.id
11773
- )) });
11774
- }
11775
- function WorkoutLevel({ workout, expandedExercise, onToggleExercise }) {
11776
- const exercises = workout.exercises.map((exercise, index) => ({
11777
- ...exercise,
11778
- state: exercise.state === "upcoming" ? exercise.state : index === expandedExercise ? "expanded" : "collapsed",
11779
- onToggle: () => onToggleExercise(index)
11780
- }));
11781
- return /* @__PURE__ */ jsx(reactNative.View, { testID: "program-planning-page-workout-level", children: /* @__PURE__ */ jsx(
11782
- WorkoutCard,
11783
- {
11784
- name: workout.name,
11785
- date: workout.date,
11786
- duration: workout.duration,
11787
- status: workout.status,
11788
- muscleGroups: workout.muscleGroups,
11789
- totalSets: workout.totalSets,
11790
- totalVolume: workout.totalVolume,
11791
- unit: workout.unit,
11792
- exercises,
11793
- expanded: true,
11794
- onToggle: () => void 0
11795
- }
11796
- ) });
11797
- }
11798
- function ProgramPlanningPage({
11799
- title = "Program Plan",
11800
- mesos,
11801
- className,
11802
- ...props
11803
- }) {
11804
- const [activeMesoId, setActiveMesoId] = React24.useState(mesos[0]?.id ?? null);
11805
- const [selectedWeek, setSelectedWeek] = React24.useState(null);
11806
- const [selectedWorkoutId, setSelectedWorkoutId] = React24.useState(null);
11807
- const [expandedExercise, setExpandedExercise] = React24.useState(null);
11808
- const progressMesos = React24.useMemo(() => toProgressBarMesos(mesos), [mesos]);
11809
- const activeMeso = findMeso(mesos, activeMesoId);
11810
- const level = deriveNavLevel({ weekNumber: selectedWeek, workoutId: selectedWorkoutId });
11811
- const activeWeek = findWeek(activeMeso, selectedWeek);
11812
- const activeWorkout = findWorkout(activeWeek, selectedWorkoutId);
11813
- const selectMeso = (id) => {
11814
- setActiveMesoId(id);
11815
- setSelectedWeek(null);
11816
- setSelectedWorkoutId(null);
11817
- };
11818
- const selectWeek = (weekNumber) => {
11819
- setSelectedWeek(weekNumber);
11820
- setSelectedWorkoutId(null);
11821
- setExpandedExercise(null);
11822
- };
11823
- const selectWorkout = (id) => {
11824
- setSelectedWorkoutId(id);
11825
- setExpandedExercise(null);
11826
- };
11827
- const toggleExercise = (index) => setExpandedExercise((current) => current === index ? null : index);
11828
- const navigateTo = (target) => {
11829
- if (target === "meso") selectMeso(activeMesoId ?? mesos[0]?.id);
11830
- else if (target === "week" && selectedWeek != null) selectWeek(selectedWeek);
11831
- };
11832
- return /* @__PURE__ */ jsx(
11833
- reactNative.View,
11834
- {
11835
- className: cn(className, "bg-background-base"),
11836
- style: { width: 390 },
11837
- accessibilityRole: "main",
11838
- "aria-label": title,
11839
- testID: "program-planning-page",
11840
- ...props,
11841
- children: /* @__PURE__ */ jsxs(reactNative.View, { style: { padding: 16, gap: 14 }, testID: "program-planning-page-content", children: [
11842
- /* @__PURE__ */ jsx(
11843
- reactNative.Text,
11844
- {
11845
- accessibilityRole: "header",
11846
- className: "text-text-primary",
11847
- style: {
11848
- fontSize: 20,
11849
- fontFamily: '"Space Grotesk", sans-serif',
11850
- fontWeight: "700"
11851
- },
11852
- testID: "program-planning-page-title",
11853
- children: title
11854
- }
11855
- ),
11856
- /* @__PURE__ */ jsx(reactNative.View, { style: { marginHorizontal: -16 }, children: /* @__PURE__ */ jsx(
11857
- MesoProgressBar,
11858
- {
11859
- mesos: progressMesos,
11860
- activeMesoId,
11861
- onMesoPress: selectMeso
11862
- }
11863
- ) }),
11864
- activeMeso != null && /* @__PURE__ */ jsx(
11865
- Breadcrumbs2,
11866
- {
11867
- crumbs: buildBreadcrumbs(activeMeso, selectedWeek, activeWorkout),
11868
- onNavigate: navigateTo
11869
- }
11870
- ),
11871
- /* @__PURE__ */ jsxs(
11872
- reactNative.View,
11873
- {
11874
- className: "bg-surface-elevated border-border",
11875
- style: {
11876
- borderWidth: 1,
11877
- borderRadius: 12,
11878
- padding: 12
11879
- },
11880
- testID: "program-planning-page-body",
11881
- children: [
11882
- level === "meso" && /* @__PURE__ */ jsx(
11883
- MesoLevel,
11884
- {
11885
- mesos,
11886
- activeMesoId,
11887
- onSelectMeso: selectMeso,
11888
- onSelectWeek: selectWeek
11889
- }
11890
- ),
11891
- level === "week" && activeWeek != null && /* @__PURE__ */ jsx(WeekLevel, { week: activeWeek, onSelectWorkout: selectWorkout }),
11892
- level === "workout" && activeWorkout != null && /* @__PURE__ */ jsx(
11893
- WorkoutLevel,
11894
- {
11895
- workout: activeWorkout,
11896
- expandedExercise,
11897
- onToggleExercise: toggleExercise
11898
- }
11899
- )
11900
- ]
11901
- }
11902
- )
11903
- ] })
11904
- }
11905
- );
11906
- }
11907
- var BRAND_PRIMARY12 = "#FF7900";
11908
- var CHART_WIDTH = 326;
11909
- var CHART_HEIGHT = 150;
11910
- var EXERCISE_DETAIL_TABS = [
11911
- { key: "progress", label: "Progress" },
11912
- { key: "history", label: "History" },
11913
- { key: "advanced", label: "Advanced" }
11914
- ];
11915
- function deriveExerciseStats(entries) {
11916
- let bestE1rm = null;
11917
- let prCount = 0;
11918
- for (const entry of entries) {
11919
- if (entry.e1rm != null && (bestE1rm == null || entry.e1rm.value > bestE1rm)) {
11920
- bestE1rm = entry.e1rm.value;
11921
- }
11922
- if (entry.isPR) prCount += 1;
11923
- }
11924
- return { sessions: entries.length, bestE1rm, prCount };
11925
- }
11926
- function toExerciseCardProps(entry, expanded, onToggle) {
11927
- return {
11928
- name: entry.title,
11929
- state: expanded ? "expanded" : "collapsed",
11930
- onToggle,
11931
- summary: entry.summary,
11932
- e1rm: entry.e1rm,
11933
- isPR: entry.isPR,
11934
- tempo: entry.tempo,
11935
- sets: expanded ? entry.sets : void 0
11936
- };
11937
- }
11938
- function summarizeVbt(sets) {
11939
- const allReps = sets.flatMap((set) => set.velocities);
11940
- const meanVelocity = calculateMeanVelocity(allReps);
11941
- let velocityLoss = 0;
11942
- for (const set of sets) {
11943
- velocityLoss = Math.max(velocityLoss, calculateVelocityLoss(set.velocities));
11944
- }
11945
- return { meanVelocity, velocityLoss };
11946
- }
11947
- function TabBar({ active, onSelect }) {
11948
- return /* @__PURE__ */ jsx(
11949
- reactNative.View,
11950
- {
11951
- className: "flex-row",
11952
- style: {
11953
- borderBottomWidth: 1,
11954
- borderBottomColor: resolveColor("border-default")
11955
- },
11956
- accessibilityRole: "tablist",
11957
- testID: "exercise-detail-page-tabs",
11958
- children: EXERCISE_DETAIL_TABS.map(({ key, label }) => {
11959
- const isActive = key === active;
11960
- return /* @__PURE__ */ jsx(
11961
- reactNative.Pressable,
11962
- {
11963
- onPress: () => onSelect(key),
11964
- accessibilityRole: "tab",
11965
- accessibilityState: { selected: isActive },
11966
- style: {
11967
- flex: 1,
11968
- alignItems: "center",
11969
- paddingVertical: 10,
11970
- borderBottomWidth: 2,
11971
- borderBottomColor: isActive ? BRAND_PRIMARY12 : "transparent"
11972
- },
11973
- testID: `exercise-detail-page-tab-${key}`,
11974
- children: /* @__PURE__ */ jsx(
11975
- reactNative.Text,
11976
- {
11977
- className: isActive ? "text-text-primary" : "text-text-tertiary",
11978
- style: {
11979
- fontSize: 13,
11980
- fontFamily: "Inter, sans-serif",
11981
- fontWeight: isActive ? "700" : "500"
11982
- },
11983
- children: label
11984
- }
11985
- )
11986
- },
11987
- key
11988
- );
11989
- })
11990
- }
11991
- );
11992
- }
11993
- var STAT_CARDS = [
11994
- { key: "sessions", label: "Sessions" },
11995
- { key: "bestE1rm", label: "Best e1RM" },
11996
- { key: "prCount", label: "PRs" }
11997
- ];
11998
- function StatStrip({ stats, unit }) {
11999
- return /* @__PURE__ */ jsx(reactNative.View, { className: "flex-row", style: { gap: 8 }, testID: "exercise-detail-page-stats", children: STAT_CARDS.map(({ key, label }) => {
12000
- const raw = stats[key];
12001
- const value = key === "bestE1rm" ? raw == null ? "\u2014" : `${raw} ${unit}` : `${raw}`;
12002
- return /* @__PURE__ */ jsxs(
12003
- reactNative.View,
12004
- {
12005
- className: "bg-surface-elevated border-border",
12006
- style: {
12007
- flex: 1,
12008
- padding: 12,
12009
- borderRadius: 10,
12010
- borderWidth: 1
12011
- },
12012
- testID: `exercise-detail-page-stat-${key}`,
12013
- children: [
12014
- /* @__PURE__ */ jsx(
12015
- reactNative.Text,
12016
- {
12017
- className: "text-text-primary",
12018
- style: {
12019
- fontSize: 18,
12020
- fontFamily: '"Space Grotesk", sans-serif',
12021
- fontWeight: "700"
12022
- },
12023
- children: value
12024
- }
12025
- ),
12026
- /* @__PURE__ */ jsx(
12027
- reactNative.Text,
12028
- {
12029
- className: "text-text-tertiary",
12030
- style: {
12031
- marginTop: 2,
12032
- fontSize: 10,
12033
- fontFamily: "Inter, sans-serif",
12034
- fontWeight: "600",
12035
- letterSpacing: 0.4,
12036
- textTransform: "uppercase"
12037
- },
12038
- children: label
12039
- }
12040
- )
12041
- ]
12042
- },
12043
- key
12044
- );
12045
- }) });
12046
- }
12047
- function SectionCard({
12048
- title,
12049
- children,
12050
- testID
12051
- }) {
12052
- return /* @__PURE__ */ jsxs(
12053
- reactNative.View,
12054
- {
12055
- className: "bg-surface-elevated border-border",
12056
- style: {
12057
- borderWidth: 1,
12058
- borderRadius: 12,
12059
- padding: 12,
12060
- gap: 10
12061
- },
12062
- testID,
12063
- children: [
12064
- /* @__PURE__ */ jsx(
12065
- reactNative.Text,
12066
- {
12067
- className: "text-text-tertiary",
12068
- style: {
12069
- fontSize: 11,
12070
- fontFamily: "Inter, sans-serif",
12071
- fontWeight: "600",
12072
- letterSpacing: 0.6,
12073
- textTransform: "uppercase"
12074
- },
12075
- children: title
12076
- }
12077
- ),
12078
- children
12079
- ]
12080
- }
12081
- );
12082
- }
12083
- function EntryList({ entries, expandedId, onToggle, emptyLabel, testID }) {
12084
- if (entries.length === 0) {
12085
- return /* @__PURE__ */ jsx(
12086
- reactNative.Text,
12087
- {
12088
- className: "text-text-tertiary",
12089
- style: { fontSize: 12, fontFamily: "Inter, sans-serif" },
12090
- testID: `${testID}-empty`,
12091
- children: emptyLabel
12092
- }
12093
- );
12094
- }
12095
- return /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 8 }, testID, children: entries.map((entry) => /* @__PURE__ */ jsx(
12096
- ExerciseCard,
12097
- {
12098
- ...toExerciseCardProps(entry, entry.id === expandedId, () => onToggle(entry.id))
12099
- },
12100
- entry.id
12101
- )) });
12102
- }
12103
- function VbtBreakdown({ sets }) {
12104
- return /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 10 }, testID: "exercise-detail-page-vbt-breakdown", children: sets.map((set) => {
12105
- const mean = calculateMeanVelocity(set.velocities);
12106
- const loss = calculateVelocityLoss(set.velocities);
12107
- return /* @__PURE__ */ jsxs(
12108
- reactNative.View,
12109
- {
12110
- className: "flex-row items-center",
12111
- style: { gap: 10 },
12112
- testID: "exercise-detail-page-vbt-set",
12113
- children: [
12114
- /* @__PURE__ */ jsx(
12115
- reactNative.Text,
12116
- {
12117
- className: "text-text-secondary",
12118
- style: {
12119
- width: 44,
12120
- fontSize: 11,
12121
- fontFamily: "Inter, sans-serif",
12122
- fontWeight: "600"
12123
- },
12124
- children: set.label
12125
- }
12126
- ),
12127
- /* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(VelocityStrip, { velocities: set.velocities, variant: "mini" }) }),
12128
- /* @__PURE__ */ jsx(
12129
- reactNative.Text,
12130
- {
12131
- className: "text-text-tertiary",
12132
- style: {
12133
- fontSize: 11,
12134
- fontFamily: "Inter, sans-serif",
12135
- fontVariant: ["tabular-nums"]
12136
- },
12137
- children: `${mean.toFixed(2)} \xB7 -${loss}%`
12138
- }
12139
- )
12140
- ]
12141
- },
12142
- set.label
12143
- );
12144
- }) });
12145
- }
12146
- function VbtSummaryRow({ summary }) {
12147
- const cells = [
12148
- { label: "Mean Velocity", value: `${summary.meanVelocity.toFixed(2)} m/s` },
12149
- { label: "Peak Vel. Loss", value: `-${summary.velocityLoss}%` }
12150
- ];
12151
- return /* @__PURE__ */ jsx(reactNative.View, { className: "flex-row", style: { gap: 8 }, testID: "exercise-detail-page-vbt-summary", children: cells.map((cell) => /* @__PURE__ */ jsxs(
12152
- reactNative.View,
12153
- {
12154
- className: "bg-surface-elevated border-border",
12155
- style: {
12156
- flex: 1,
12157
- padding: 12,
12158
- borderRadius: 10,
12159
- borderWidth: 1
12160
- },
12161
- children: [
12162
- /* @__PURE__ */ jsx(
12163
- reactNative.Text,
12164
- {
12165
- className: "text-text-primary",
12166
- style: {
12167
- fontSize: 16,
12168
- fontFamily: '"Space Grotesk", sans-serif',
12169
- fontWeight: "700"
12170
- },
12171
- children: cell.value
12172
- }
12173
- ),
12174
- /* @__PURE__ */ jsx(
12175
- reactNative.Text,
12176
- {
12177
- className: "text-text-tertiary",
12178
- style: {
12179
- marginTop: 2,
12180
- fontSize: 10,
12181
- fontFamily: "Inter, sans-serif",
12182
- fontWeight: "600",
12183
- letterSpacing: 0.4,
12184
- textTransform: "uppercase"
12185
- },
12186
- children: cell.label
12187
- }
12188
- )
12189
- ]
12190
- },
12191
- cell.label
12192
- )) });
12193
- }
12194
- function ExerciseDetailPage({
12195
- exercise,
12196
- meso,
12197
- trend,
12198
- progression,
12199
- history,
12200
- vbt,
12201
- className,
12202
- ...props
12203
- }) {
12204
- const [tab, setTab] = React24.useState("progress");
12205
- const [expandedProgressId, setExpandedProgressId] = React24.useState(null);
12206
- const [expandedHistoryId, setExpandedHistoryId] = React24.useState(null);
12207
- const stats = React24.useMemo(() => deriveExerciseStats(history), [history]);
12208
- const vbtSummary = React24.useMemo(() => summarizeVbt(vbt.sets), [vbt.sets]);
12209
- const toggle = (setId) => (id) => setId((current) => current === id ? null : id);
12210
- return /* @__PURE__ */ jsx(
12211
- reactNative.View,
12212
- {
12213
- className: cn("bg-background-base", className),
12214
- style: { width: 390 },
12215
- accessibilityRole: "main",
12216
- "aria-label": exercise.name,
12217
- testID: "exercise-detail-page",
12218
- ...props,
12219
- children: /* @__PURE__ */ jsxs(reactNative.View, { style: { padding: 16, gap: 14 }, testID: "exercise-detail-page-content", children: [
12220
- /* @__PURE__ */ jsxs(
12221
- reactNative.View,
12222
- {
12223
- className: "flex-row items-start justify-between",
12224
- style: { gap: 8 },
12225
- testID: "exercise-detail-page-header",
12226
- children: [
12227
- /* @__PURE__ */ jsxs(reactNative.View, { style: { flexShrink: 1 }, children: [
12228
- /* @__PURE__ */ jsx(
12229
- reactNative.Text,
12230
- {
12231
- accessibilityRole: "header",
12232
- className: "text-text-primary",
12233
- style: {
12234
- fontSize: 20,
12235
- fontFamily: '"Space Grotesk", sans-serif',
12236
- fontWeight: "700"
12237
- },
12238
- testID: "exercise-detail-page-title",
12239
- children: exercise.name
12240
- }
12241
- ),
12242
- /* @__PURE__ */ jsx(
12243
- reactNative.Text,
12244
- {
12245
- className: "text-text-secondary",
12246
- style: {
12247
- marginTop: 2,
12248
- fontSize: 12,
12249
- fontFamily: "Inter, sans-serif"
12250
- },
12251
- testID: "exercise-detail-page-subtitle",
12252
- children: exercise.subtitle
12253
- }
12254
- )
12255
- ] }),
12256
- exercise.currentE1rm != null && /* @__PURE__ */ jsxs(
12257
- reactNative.View,
12258
- {
12259
- style: {
12260
- paddingVertical: 4,
12261
- paddingHorizontal: 10,
12262
- borderRadius: 6,
12263
- backgroundColor: "rgba(255,121,0,0.10)",
12264
- borderWidth: 1,
12265
- borderColor: "rgba(255,121,0,0.25)"
12266
- },
12267
- testID: "exercise-detail-page-e1rm",
12268
- children: [
12269
- /* @__PURE__ */ jsx(
12270
- reactNative.Text,
12271
- {
12272
- className: "text-text-tertiary",
12273
- style: {
12274
- fontSize: 9,
12275
- fontFamily: "Inter, sans-serif",
12276
- fontWeight: "600",
12277
- letterSpacing: 0.4,
12278
- textTransform: "uppercase"
12279
- },
12280
- children: "e1RM"
12281
- }
12282
- ),
12283
- /* @__PURE__ */ jsx(
12284
- reactNative.Text,
12285
- {
12286
- style: {
12287
- fontSize: 15,
12288
- fontFamily: '"Space Grotesk", sans-serif',
12289
- fontWeight: "700",
12290
- color: BRAND_PRIMARY12
12291
- },
12292
- children: `${exercise.currentE1rm} ${exercise.unit}`
12293
- }
12294
- )
12295
- ]
12296
- }
12297
- )
12298
- ]
12299
- }
12300
- ),
12301
- /* @__PURE__ */ jsx(TabBar, { active: tab, onSelect: setTab }),
12302
- tab === "progress" && /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 14 }, testID: "exercise-detail-page-panel-progress", children: [
12303
- /* @__PURE__ */ jsx(MesoStatusCard, { ...meso }),
12304
- /* @__PURE__ */ jsx(StatStrip, { stats, unit: exercise.unit }),
12305
- /* @__PURE__ */ jsx(SectionCard, { title: "Strength Trend", testID: "exercise-detail-page-trend", children: /* @__PURE__ */ jsx(
12306
- StrengthTrendChart,
12307
- {
12308
- data: trend.data,
12309
- projection: trend.projection,
12310
- mesoBoundaries: trend.mesoBoundaries,
12311
- width: CHART_WIDTH,
12312
- height: CHART_HEIGHT,
12313
- unit: exercise.unit,
12314
- animateOnMount: false
12315
- }
12316
- ) }),
12317
- /* @__PURE__ */ jsx(SectionCard, { title: "Week-by-Week", testID: "exercise-detail-page-progression", children: /* @__PURE__ */ jsx(
12318
- EntryList,
12319
- {
12320
- entries: progression,
12321
- expandedId: expandedProgressId,
12322
- onToggle: toggle(setExpandedProgressId),
12323
- emptyLabel: "No progression logged yet",
12324
- testID: "exercise-detail-page-progression-list"
12325
- }
12326
- ) })
12327
- ] }),
12328
- tab === "history" && /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 14 }, testID: "exercise-detail-page-panel-history", children: /* @__PURE__ */ jsx(SectionCard, { title: "All Sessions", testID: "exercise-detail-page-history", children: /* @__PURE__ */ jsx(
12329
- EntryList,
12330
- {
12331
- entries: history,
12332
- expandedId: expandedHistoryId,
12333
- onToggle: toggle(setExpandedHistoryId),
12334
- emptyLabel: "No sessions logged yet",
12335
- testID: "exercise-detail-page-history-list"
12336
- }
12337
- ) }) }),
12338
- tab === "advanced" && /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 14 }, testID: "exercise-detail-page-panel-advanced", children: [
12339
- /* @__PURE__ */ jsx(SectionCard, { title: "Capacity Band", testID: "exercise-detail-page-capacity", children: /* @__PURE__ */ jsx(
12340
- CapacityBandChart,
12341
- {
12342
- band: vbt.band,
12343
- workouts: vbt.workouts,
12344
- projection: vbt.projection,
12345
- width: CHART_WIDTH,
12346
- height: CHART_HEIGHT
12347
- }
12348
- ) }),
12349
- /* @__PURE__ */ jsx(VbtSummaryRow, { summary: vbtSummary }),
12350
- /* @__PURE__ */ jsx(SectionCard, { title: "Velocity Breakdown", testID: "exercise-detail-page-velocity", children: /* @__PURE__ */ jsx(VbtBreakdown, { sets: vbt.sets }) })
12351
- ] })
12352
- ] })
12353
- }
12354
- );
12355
- }
12356
- var BRAND_PRIMARY13 = "#FF7900";
12357
- function countCompletedSets(exercise) {
12358
- if (exercise.setVelocities != null) return exercise.setVelocities.length;
12359
- if (exercise.status === "completed") return exercise.totalPlannedSets;
12360
- return 0;
12361
- }
12362
- function deriveWorkoutProgress(exercises) {
12363
- let completedExercises = 0;
12364
- let completedSets = 0;
12365
- let totalSets = 0;
12366
- for (const exercise of exercises) {
12367
- if (exercise.status === "completed") completedExercises += 1;
12368
- completedSets += countCompletedSets(exercise);
12369
- totalSets += exercise.totalPlannedSets;
12370
- }
12371
- return {
12372
- completedExercises,
12373
- totalExercises: exercises.length,
12374
- completedSets,
12375
- totalSets,
12376
- fraction: totalSets === 0 ? 0 : completedSets / totalSets
12377
- };
12378
- }
12379
- function findActiveExercise(exercises) {
12380
- return exercises.find((exercise) => exercise.status === "active") ?? null;
12381
- }
12382
- function statusToCardState(status, focused) {
12383
- if (status === "upcoming") return "upcoming";
12384
- return focused ? "expanded" : "collapsed";
12385
- }
12386
- function toActiveCardProps(exercise, focused, onToggle, supersetPosition = null, supersetColor) {
12387
- const state = statusToCardState(exercise.status, focused);
12388
- return {
12389
- name: exercise.name,
12390
- state,
12391
- onToggle,
12392
- summary: exercise.summary,
12393
- e1rm: exercise.e1rm,
12394
- isPR: exercise.isPR,
12395
- tempo: exercise.tempo,
12396
- setVelocities: exercise.setVelocities,
12397
- totalPlannedSets: exercise.totalPlannedSets,
12398
- sets: state === "expanded" ? exercise.sets : void 0,
12399
- prescription: exercise.prescription,
12400
- previousBest: exercise.previousBest,
12401
- supersetPosition,
12402
- supersetColor
12403
- };
12404
- }
12405
- function groupExercises(exercises, supersets) {
12406
- const byId = new Map(supersets.map((superset) => [superset.id, superset]));
12407
- const groups = [];
12408
- for (const exercise of exercises) {
12409
- const superset = exercise.supersetId != null ? byId.get(exercise.supersetId) : void 0;
12410
- const last = groups[groups.length - 1];
12411
- if (superset != null && last?.type === "superset" && last.superset.id === superset.id) {
12412
- last.exercises.push(exercise);
12413
- } else if (superset != null) {
12414
- groups.push({ type: "superset", superset, exercises: [exercise] });
12415
- } else {
12416
- groups.push({ type: "single", exercise });
12417
- }
12418
- }
12419
- return groups;
12420
- }
12421
- function supersetPositionAt(index, length) {
12422
- if (length === 1) return null;
12423
- if (index === 0) return "first";
12424
- if (index === length - 1) return "last";
12425
- return "middle";
12426
- }
12427
- function WorkoutHeader({
12428
- title,
12429
- subtitle,
12430
- progress
12431
- }) {
12432
- return /* @__PURE__ */ jsxs(reactNative.View, { style: { gap: 10 }, testID: "active-workout-page-header", children: [
12433
- /* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-start justify-between", style: { gap: 8 }, children: [
12434
- /* @__PURE__ */ jsxs(reactNative.View, { style: { flexShrink: 1 }, children: [
12435
- /* @__PURE__ */ jsx(
12436
- reactNative.Text,
12437
- {
12438
- accessibilityRole: "header",
12439
- className: "text-text-primary",
12440
- style: {
12441
- fontSize: 20,
12442
- fontFamily: '"Space Grotesk", sans-serif',
12443
- fontWeight: "700"
12444
- },
12445
- testID: "active-workout-page-title",
12446
- children: title
12447
- }
12448
- ),
12449
- subtitle != null && /* @__PURE__ */ jsx(
12450
- reactNative.Text,
12451
- {
12452
- className: "text-text-secondary",
12453
- style: {
12454
- marginTop: 2,
12455
- fontSize: 12,
12456
- fontFamily: "Inter, sans-serif"
12457
- },
12458
- testID: "active-workout-page-subtitle",
12459
- children: subtitle
12460
- }
12461
- )
12462
- ] }),
12463
- /* @__PURE__ */ jsxs(reactNative.View, { style: { alignItems: "flex-end" }, testID: "active-workout-page-progress-count", children: [
12464
- /* @__PURE__ */ jsx(
12465
- reactNative.Text,
12466
- {
12467
- style: {
12468
- fontSize: 15,
12469
- fontFamily: '"Space Grotesk", sans-serif',
12470
- fontWeight: "700",
12471
- color: BRAND_PRIMARY13,
12472
- fontVariant: ["tabular-nums"]
12473
- },
12474
- children: `${progress.completedExercises}/${progress.totalExercises}`
12475
- }
12476
- ),
12477
- /* @__PURE__ */ jsx(
12478
- reactNative.Text,
12479
- {
12480
- className: "text-text-tertiary",
12481
- style: {
12482
- fontSize: 10,
12483
- fontFamily: "Inter, sans-serif",
12484
- fontWeight: "600",
12485
- letterSpacing: 0.4,
12486
- textTransform: "uppercase"
12487
- },
12488
- children: `${progress.completedSets}/${progress.totalSets} sets`
12489
- }
12490
- )
12491
- ] })
12492
- ] }),
12493
- /* @__PURE__ */ jsx(
11633
+ if (band.length === 0) {
11634
+ return /* @__PURE__ */ jsx(
12494
11635
  reactNative.View,
12495
11636
  {
12496
- className: "bg-border",
12497
- style: { height: 4, borderRadius: 2 },
12498
- testID: "active-workout-page-progress-track",
12499
- children: /* @__PURE__ */ jsx(
12500
- reactNative.View,
12501
- {
12502
- style: {
12503
- height: "100%",
12504
- width: `${Math.round(progress.fraction * 100)}%`,
12505
- backgroundColor: BRAND_PRIMARY13,
12506
- borderRadius: 2
12507
- },
12508
- testID: "active-workout-page-progress-fill"
12509
- }
12510
- )
11637
+ style: { width, height },
11638
+ className,
11639
+ accessibilityRole: "image",
11640
+ accessibilityLabel: "Training capacity band chart, no data",
11641
+ testID: "capacity-band-chart-empty",
11642
+ ...props
12511
11643
  }
12512
- )
12513
- ] });
12514
- }
12515
- function ExerciseList({ groups, focusedId, onToggle }) {
12516
- return /* @__PURE__ */ jsx(reactNative.View, { style: { gap: 10 }, testID: "active-workout-page-exercises", children: groups.map((group) => {
12517
- if (group.type === "single") {
12518
- return /* @__PURE__ */ jsx(
12519
- reactNative.View,
12520
- {
12521
- className: "bg-surface-elevated border-border",
12522
- style: {
12523
- borderWidth: 1,
12524
- borderRadius: 12
12525
- },
12526
- children: /* @__PURE__ */ jsx(
12527
- ExerciseCard,
12528
- {
12529
- ...toActiveCardProps(
12530
- group.exercise,
12531
- group.exercise.id === focusedId,
12532
- () => onToggle(group.exercise.id)
12533
- )
12534
- }
12535
- )
12536
- },
12537
- group.exercise.id
12538
- );
12539
- }
12540
- return /* @__PURE__ */ jsx(
12541
- SupersetWrapper,
12542
- {
12543
- label: group.superset.label,
12544
- color: group.superset.color,
12545
- children: group.exercises.map((exercise, index) => /* @__PURE__ */ jsx(
12546
- ExerciseCard,
12547
- {
12548
- ...toActiveCardProps(
12549
- exercise,
12550
- exercise.id === focusedId,
12551
- () => onToggle(exercise.id),
12552
- supersetPositionAt(index, group.exercises.length),
12553
- group.superset.color
12554
- )
12555
- },
12556
- exercise.id
12557
- ))
12558
- },
12559
- group.superset.id
12560
11644
  );
12561
- }) });
12562
- }
12563
- function ActiveWorkoutPage({
12564
- title = "Active Workout",
12565
- subtitle,
12566
- exercises,
12567
- supersets = [],
12568
- input,
12569
- rest,
12570
- initialResting = false,
12571
- className,
12572
- ...props
12573
- }) {
12574
- const active = findActiveExercise(exercises);
12575
- const [focusedId, setFocusedId] = React24.useState(active?.id ?? null);
12576
- const [resting, setResting] = React24.useState(initialResting);
12577
- const [reps, setReps] = React24.useState(input?.reps ?? "");
12578
- const [weight, setWeight] = React24.useState(input?.weight ?? "");
12579
- const progress = React24.useMemo(() => deriveWorkoutProgress(exercises), [exercises]);
12580
- const groups = React24.useMemo(() => groupExercises(exercises, supersets), [exercises, supersets]);
12581
- const toggleFocus = (id) => setFocusedId((current) => current === id ? null : id);
12582
- const nextSetNumber = active != null ? countCompletedSets(active) + 1 : 1;
12583
- const canRecord = reps.trim() !== "" && weight.trim() !== "";
12584
- const showRest = resting && rest != null;
12585
- const showInput = !showRest && active != null;
11645
+ }
11646
+ const { toX, toY } = scale;
11647
+ const bandPixels = toPixels(band, toX, toY);
11648
+ const columns = buildColumns(bandPixels, COLUMN_STEP);
11649
+ const topEdge = buildEdges(bandPixels, "yHigh");
11650
+ const bottomEdge = buildEdges(bandPixels, "yLow");
11651
+ const lastBandPoint = band[band.length - 1];
11652
+ const trainingPixels = projection ? toPixels([lastBandPoint, ...projection.withTraining], toX, toY) : [];
11653
+ const restPixels = projection ? toPixels([lastBandPoint, ...projection.withRest], toX, toY) : [];
11654
+ const labelStride = Math.max(1, Math.ceil(band.length / 5));
11655
+ const revealWidth = reveal.interpolate({ inputRange: [0, 1], outputRange: [0, width] });
11656
+ const renderColumns = (cols, color, key) => cols.map((col, i) => /* @__PURE__ */ jsx(
11657
+ reactNative.View,
11658
+ {
11659
+ style: {
11660
+ position: "absolute",
11661
+ left: col.x,
11662
+ top: col.top,
11663
+ width: COLUMN_STEP + 0.5,
11664
+ height: col.height,
11665
+ backgroundColor: color
11666
+ },
11667
+ accessibilityElementsHidden: true,
11668
+ importantForAccessibility: "no-hide-descendants",
11669
+ testID: key
11670
+ },
11671
+ `${key}-${i}`
11672
+ ));
11673
+ const renderEdges = (segments, color, dashed, key) => segments.map((seg, i) => /* @__PURE__ */ jsx(
11674
+ reactNative.View,
11675
+ {
11676
+ style: {
11677
+ position: "absolute",
11678
+ left: seg.left,
11679
+ top: seg.top,
11680
+ width: seg.length,
11681
+ height: dashed ? 0 : 1.5,
11682
+ borderTopWidth: dashed ? 1 : 0,
11683
+ borderTopColor: dashed ? color : void 0,
11684
+ borderStyle: dashed ? "dashed" : "solid",
11685
+ backgroundColor: dashed ? void 0 : color,
11686
+ opacity: dashed ? 0.7 : 1,
11687
+ transformOrigin: "0 0",
11688
+ transform: [{ rotate: `${seg.angle}deg` }]
11689
+ },
11690
+ accessibilityElementsHidden: true,
11691
+ importantForAccessibility: "no-hide-descendants",
11692
+ testID: key
11693
+ },
11694
+ `${key}-${i}`
11695
+ ));
12586
11696
  return /* @__PURE__ */ jsxs(
12587
11697
  reactNative.View,
12588
11698
  {
12589
- className: ["bg-background-base", className].filter(Boolean).join(" "),
12590
- style: { position: "relative", width: 390 },
12591
- accessibilityRole: "main",
12592
- "aria-label": title,
12593
- testID: "active-workout-page",
11699
+ style: { width, height, position: "relative" },
11700
+ className,
11701
+ testID: "capacity-band-chart-root",
12594
11702
  ...props,
12595
11703
  children: [
12596
- /* @__PURE__ */ jsxs(reactNative.View, { style: { padding: 16, gap: 14 }, testID: "active-workout-page-content", children: [
12597
- /* @__PURE__ */ jsx(WorkoutHeader, { title, subtitle, progress }),
12598
- /* @__PURE__ */ jsx(ExerciseList, { groups, focusedId, onToggle: toggleFocus })
12599
- ] }),
12600
- (showRest || showInput) && /* @__PURE__ */ jsx(
11704
+ /* @__PURE__ */ jsxs(
12601
11705
  reactNative.View,
12602
11706
  {
12603
- style: { borderTopWidth: 1, borderTopColor: resolveColor("border-default") },
12604
- testID: "active-workout-page-bottom-bar",
12605
- children: showRest && rest != null ? /* @__PURE__ */ jsx(
12606
- RestTimer,
12607
- {
12608
- totalSeconds: rest.totalSeconds,
12609
- elapsedMs: rest.elapsedMs,
12610
- nextSetInfo: rest.nextSetInfo,
12611
- onSkip: () => setResting(false),
12612
- onAddTime: () => void 0,
12613
- visible: true
12614
- }
12615
- ) : active != null && /* @__PURE__ */ jsx(
12616
- InputBar,
12617
- {
12618
- exerciseName: active.name,
12619
- setNumber: nextSetNumber,
12620
- totalSets: active.totalPlannedSets,
12621
- reps,
12622
- weight,
12623
- unit: active.unit,
12624
- onRepsChange: setReps,
12625
- onWeightChange: setWeight,
12626
- onRecord: () => {
12627
- if (rest != null) setResting(true);
12628
- },
12629
- canRecord,
12630
- visible: true
12631
- }
12632
- )
11707
+ style: { position: "absolute", top: 0, left: 0, width, height },
11708
+ accessibilityRole: "image",
11709
+ accessibilityLabel: `Training capacity band chart. Current capacity: ${currentStatus(
11710
+ workouts
11711
+ )}. ${workouts.length} workout${workouts.length === 1 ? "" : "s"} shown.`,
11712
+ testID: "capacity-band-chart",
11713
+ children: [
11714
+ /* @__PURE__ */ jsx(
11715
+ reactNative.Animated.View,
11716
+ {
11717
+ style: { position: "absolute", top: 0, left: 0, height, width: revealWidth, overflow: "hidden" },
11718
+ accessibilityElementsHidden: true,
11719
+ importantForAccessibility: "no-hide-descendants",
11720
+ testID: "capacity-band-chart-reveal",
11721
+ children: /* @__PURE__ */ jsxs(reactNative.View, { style: { width, height, position: "relative" }, children: [
11722
+ projection && /* @__PURE__ */ jsxs(reactNative.View, { testID: "capacity-band-chart-projection", children: [
11723
+ renderColumns(buildColumns(trainingPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
11724
+ renderColumns(buildColumns(restPixels, COLUMN_STEP), PROJECTION_FILL, "capacity-band-chart-projection-fill"),
11725
+ renderEdges(buildEdges(trainingPixels, "yHigh"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
11726
+ renderEdges(buildEdges(trainingPixels, "yLow"), STATUS_SUCCESS3, true, "capacity-band-chart-projection-training"),
11727
+ renderEdges(buildEdges(restPixels, "yHigh"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
11728
+ renderEdges(buildEdges(restPixels, "yLow"), STATUS_INFO, true, "capacity-band-chart-projection-rest"),
11729
+ /* @__PURE__ */ jsx(
11730
+ reactNative.Text,
11731
+ {
11732
+ style: {
11733
+ position: "absolute",
11734
+ left: trainingPixels[trainingPixels.length - 1]?.x + 2,
11735
+ top: trainingPixels[trainingPixels.length - 1]?.yHigh - 12,
11736
+ fontSize: 9,
11737
+ fontFamily: "Inter, sans-serif",
11738
+ color: STATUS_SUCCESS3
11739
+ },
11740
+ testID: "capacity-band-chart-projection-training-label",
11741
+ children: "Training"
11742
+ }
11743
+ ),
11744
+ /* @__PURE__ */ jsx(
11745
+ reactNative.Text,
11746
+ {
11747
+ style: {
11748
+ position: "absolute",
11749
+ left: restPixels[restPixels.length - 1]?.x + 2,
11750
+ top: restPixels[restPixels.length - 1]?.yLow + 2,
11751
+ fontSize: 9,
11752
+ fontFamily: "Inter, sans-serif",
11753
+ color: STATUS_INFO
11754
+ },
11755
+ testID: "capacity-band-chart-projection-rest-label",
11756
+ children: "Rest"
11757
+ }
11758
+ )
11759
+ ] }),
11760
+ /* @__PURE__ */ jsx(reactNative.View, { testID: "capacity-band-chart-band", children: renderColumns(columns, BAND_FILL, "capacity-band-chart-band-cell") }),
11761
+ renderEdges(topEdge, BAND_EDGE, false, "capacity-band-chart-edge-top"),
11762
+ renderEdges(bottomEdge, BAND_EDGE, false, "capacity-band-chart-edge-bottom")
11763
+ ] })
11764
+ }
11765
+ ),
11766
+ /* @__PURE__ */ jsx(
11767
+ reactNative.Text,
11768
+ {
11769
+ className: "text-text-tertiary",
11770
+ style: {
11771
+ position: "absolute",
11772
+ left: PADDING_LEFT / 2 - 14,
11773
+ top: PADDING_TOP + plotHeight / 2 - 7,
11774
+ width: 32,
11775
+ textAlign: "center",
11776
+ fontSize: 9,
11777
+ fontFamily: "Inter, sans-serif",
11778
+ transform: [{ rotate: "-90deg" }]
11779
+ },
11780
+ accessibilityElementsHidden: true,
11781
+ importantForAccessibility: "no-hide-descendants",
11782
+ testID: "capacity-band-chart-y-axis-label",
11783
+ children: "Load"
11784
+ }
11785
+ ),
11786
+ band.map((point, i) => {
11787
+ if (i % labelStride !== 0 && i !== band.length - 1) return null;
11788
+ return /* @__PURE__ */ jsx(
11789
+ reactNative.Text,
11790
+ {
11791
+ className: "text-text-tertiary",
11792
+ style: {
11793
+ position: "absolute",
11794
+ left: toX(point.date) - 14,
11795
+ top: height - PADDING_BOTTOM + 2,
11796
+ width: 28,
11797
+ textAlign: "center",
11798
+ fontSize: 10,
11799
+ fontFamily: "Inter, sans-serif"
11800
+ },
11801
+ accessibilityElementsHidden: true,
11802
+ importantForAccessibility: "no-hide-descendants",
11803
+ testID: "capacity-band-chart-x-label",
11804
+ children: formatDate2(point.date)
11805
+ },
11806
+ `x-${point.date}`
11807
+ );
11808
+ })
11809
+ ]
12633
11810
  }
12634
- )
11811
+ ),
11812
+ workouts.map((workout, i) => {
11813
+ const cx = toX(workout.date);
11814
+ const cy = toY(workout.load);
11815
+ const color = DOT_COLORS[workout.status];
11816
+ const dotStyle = {
11817
+ width: DOT_SIZE,
11818
+ height: DOT_SIZE,
11819
+ borderRadius: DOT_SIZE / 2,
11820
+ backgroundColor: color,
11821
+ borderWidth: 2,
11822
+ borderColor: DOT_BORDER
11823
+ };
11824
+ const label = `Workout on ${formatDate2(workout.date)}, load ${workout.load}, ${STATUS_PHRASES[workout.status]}`;
11825
+ return /* @__PURE__ */ jsx(
11826
+ reactNative.Animated.View,
11827
+ {
11828
+ style: {
11829
+ position: "absolute",
11830
+ left: cx - DOT_SIZE / 2,
11831
+ top: cy - DOT_SIZE / 2,
11832
+ transform: [{ scale: dotAnims[i] ?? 1 }]
11833
+ },
11834
+ testID: "capacity-band-chart-dot-wrapper",
11835
+ children: onWorkoutPress ? /* @__PURE__ */ jsx(
11836
+ reactNative.Pressable,
11837
+ {
11838
+ onPress: () => onWorkoutPress(workout),
11839
+ accessibilityRole: "button",
11840
+ accessibilityLabel: label,
11841
+ style: dotStyle,
11842
+ testID: "capacity-band-chart-dot"
11843
+ }
11844
+ ) : /* @__PURE__ */ jsx(
11845
+ reactNative.View,
11846
+ {
11847
+ accessibilityElementsHidden: true,
11848
+ importantForAccessibility: "no-hide-descendants",
11849
+ style: dotStyle,
11850
+ testID: "capacity-band-chart-dot"
11851
+ }
11852
+ )
11853
+ },
11854
+ `dot-${workout.date}-${i}`
11855
+ );
11856
+ })
12635
11857
  ]
12636
11858
  }
12637
11859
  );
@@ -12943,24 +12165,24 @@ function Scatter({
12943
12165
  }
12944
12166
  var STATUS_SUCCESS4 = "#14B8A6";
12945
12167
  var STATUS_WARNING4 = "#FFB020";
12946
- var STATUS_ERROR3 = "#D14343";
12168
+ var STATUS_ERROR4 = "#D14343";
12947
12169
  var TRACK = "rgba(255,255,255,0.08)";
12948
12170
  var DEFAULT_THRESHOLDS = [
12949
- { value: 0, color: STATUS_ERROR3 },
12171
+ { value: 0, color: STATUS_ERROR4 },
12950
12172
  { value: 60, color: STATUS_WARNING4 },
12951
12173
  { value: 80, color: STATUS_SUCCESS4 }
12952
12174
  ];
12953
12175
  var ARC_START = 135;
12954
12176
  var ARC_SWEEP = 270;
12955
12177
  var SEGMENTS = 40;
12956
- function clamp013(n) {
12178
+ function clamp012(n) {
12957
12179
  if (Number.isNaN(n)) return 0;
12958
12180
  return Math.max(0, Math.min(1, n));
12959
12181
  }
12960
12182
  function formatValue2(v) {
12961
12183
  return Number.isInteger(v) ? `${v}` : v.toFixed(1);
12962
12184
  }
12963
- function bandColor(fraction, bands, min, max) {
12185
+ function bandColor2(fraction, bands, min, max) {
12964
12186
  const value = min + fraction * (max - min);
12965
12187
  const sorted = [...bands].sort((a, b) => a.value - b.value);
12966
12188
  let color = sorted[0]?.color ?? STATUS_SUCCESS4;
@@ -12998,10 +12220,10 @@ function Gauge2({
12998
12220
  ...props
12999
12221
  }) {
13000
12222
  const span = max - min || 1;
13001
- const fill = clamp013((value - min) / span);
13002
- const activeColor = (f) => color ?? bandColor(f, thresholds, min, max);
12223
+ const fill = clamp012((value - min) / span);
12224
+ const activeColor = (f) => color ?? bandColor2(f, thresholds, min, max);
13003
12225
  const ticks = buildTicks(fill, size, activeColor);
13004
- const displayColor = color ?? bandColor(fill, thresholds, min, max);
12226
+ const displayColor = color ?? bandColor2(fill, thresholds, min, max);
13005
12227
  const tickLength = size * 0.11;
13006
12228
  const tickThickness = Math.max(2, size * 0.9 / SEGMENTS);
13007
12229
  const ariaLabel = `${label ? `${label}: ` : ""}${formatValue2(value)}${unit ?? ""} of ${formatValue2(max)}`;
@@ -13137,7 +12359,6 @@ exports.Accordion = Accordion;
13137
12359
  exports.AccordionButton = AccordionButton;
13138
12360
  exports.AccordionItem = AccordionItem;
13139
12361
  exports.AccordionPanel = AccordionPanel;
13140
- exports.ActiveWorkoutPage = ActiveWorkoutPage;
13141
12362
  exports.Alert = Alert;
13142
12363
  exports.AlertDescription = AlertDescription;
13143
12364
  exports.AlertTitle = AlertTitle;
@@ -13178,10 +12399,8 @@ exports.Drawer = Drawer;
13178
12399
  exports.DrawerBody = DrawerBody;
13179
12400
  exports.DrawerFooter = DrawerFooter;
13180
12401
  exports.DrawerHeader = DrawerHeader;
13181
- exports.EXERCISE_DETAIL_TABS = EXERCISE_DETAIL_TABS;
13182
12402
  exports.EmptyState = EmptyState;
13183
12403
  exports.ExerciseCard = ExerciseCard;
13184
- exports.ExerciseDetailPage = ExerciseDetailPage;
13185
12404
  exports.FormActions = FormActions;
13186
12405
  exports.FormField = FormField;
13187
12406
  exports.FormRow = FormRow;
@@ -13234,7 +12453,6 @@ exports.PopoverContent = PopoverContent;
13234
12453
  exports.PopoverTrigger = PopoverTrigger;
13235
12454
  exports.PrBadge = PrBadge;
13236
12455
  exports.PrHistoryModal = PrHistoryModal;
13237
- exports.ProgramPlanningPage = ProgramPlanningPage;
13238
12456
  exports.Progress = Progress;
13239
12457
  exports.ProgressSteps = ProgressSteps;
13240
12458
  exports.Radio = Radio;
@@ -13287,7 +12505,6 @@ exports.TableRow = TableRow;
13287
12505
  exports.Tabs = Tabs;
13288
12506
  exports.TempoBar = TempoBar;
13289
12507
  exports.TempoDisplay = TempoDisplay;
13290
- exports.ThemeProvider = ThemeProvider;
13291
12508
  exports.Toast = Toast;
13292
12509
  exports.ToastProvider = ToastProvider;
13293
12510
  exports.ToolbarButton = ToolbarButton;
@@ -13304,14 +12521,12 @@ exports.WorkoutPill = WorkoutPill;
13304
12521
  exports.alpha = alpha;
13305
12522
  exports.applyThemePreset = applyThemePreset;
13306
12523
  exports.audiobookPreset = audiobookPreset;
13307
- exports.buildBreadcrumbs = buildBreadcrumbs;
13308
12524
  exports.buttonSizes = buttonSizes;
13309
12525
  exports.calculateMeanVelocity = calculateMeanVelocity;
13310
12526
  exports.calculateVelocityLoss = calculateVelocityLoss;
13311
12527
  exports.cn = cn;
13312
12528
  exports.componentElevationRanges = componentElevationRanges;
13313
12529
  exports.composeValidators = composeValidators;
13314
- exports.countCompletedSets = countCompletedSets;
13315
12530
  exports.createFieldId = createFieldId;
13316
12531
  exports.createFlatShadow = createFlatShadow;
13317
12532
  exports.createPressedHoverShadow = createPressedHoverShadow;
@@ -13321,15 +12536,8 @@ exports.createRaisedShadow = createRaisedShadow;
13321
12536
  exports.darkThemeCSSVars = darkThemeCSSVars;
13322
12537
  exports.darken = darken;
13323
12538
  exports.defaultPreset = defaultPreset;
13324
- exports.deriveExerciseStats = deriveExerciseStats;
13325
- exports.deriveNavLevel = deriveNavLevel;
13326
- exports.deriveWorkoutProgress = deriveWorkoutProgress;
13327
12539
  exports.discreteRainbow = discreteRainbow;
13328
12540
  exports.elevationSystem = elevationSystem;
13329
- exports.findActiveExercise = findActiveExercise;
13330
- exports.findMeso = findMeso;
13331
- exports.findWeek = findWeek;
13332
- exports.findWorkout = findWorkout;
13333
12541
  exports.formatDateTime = formatDateTime;
13334
12542
  exports.formatPrescription = formatPrescription;
13335
12543
  exports.formatSignedPct = formatSignedPct;
@@ -13355,7 +12563,6 @@ exports.getValidatedElevation = getValidatedElevation;
13355
12563
  exports.getVelocityZoneColor = getVelocityZoneColor;
13356
12564
  exports.getVelocityZoneName = getVelocityZoneName;
13357
12565
  exports.gluestackConfig = gluestackConfig;
13358
- exports.groupExercises = groupExercises;
13359
12566
  exports.hasMaxLength = hasMaxLength;
13360
12567
  exports.hasMinLength = hasMinLength;
13361
12568
  exports.hexToRgb = hexToRgb;
@@ -13385,11 +12592,6 @@ exports.semanticColorsDark = semanticColorsDark;
13385
12592
  exports.semanticColorsLight = semanticColorsLight;
13386
12593
  exports.semanticTypography = semanticTypography;
13387
12594
  exports.shadowColors = shadowColors;
13388
- exports.statusToCardState = statusToCardState;
13389
- exports.summarizeVbt = summarizeVbt;
13390
- exports.toActiveCardProps = toActiveCardProps;
13391
- exports.toExerciseCardProps = toExerciseCardProps;
13392
- exports.toProgressBarMesos = toProgressBarMesos;
13393
12595
  exports.useTable = useTable;
13394
12596
  exports.useToast = useToast;
13395
12597
  exports.useToolbarButton = useToolbarButton;