@titan-design/react-ui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +399 -4
- package/dist/index.d.ts +399 -4
- package/dist/index.js +2295 -895
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2279 -905
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
- package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1974 -1740
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1975 -1741
- package/dist/pages.mjs.map +1 -1
- package/package.json +8 -3
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
- package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
- package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
- package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
- package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
- package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
- package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
- package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
- package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
- package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
- package/src/components/custom/Fatigue/README.md +93 -0
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
- package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
- package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
- package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
- package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
- package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
- package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
- package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
- package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
- package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
- package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
- package/src/components/custom/Fatigue/index.ts +41 -0
- package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
- package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
- package/src/components/custom/Workout/README.md +62 -16
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +50 -13
- package/src/components/custom/Workout/SetRow.test.tsx +6 -6
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
- package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- package/src/components/custom/Workout/index.ts +3 -0
- package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
- package/src/components/custom/charts/SetBarChart.tsx +651 -0
- package/src/components/custom/charts/live-rep-growth.ts +91 -0
- package/src/components/custom/index.ts +1 -0
- package/src/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +871 -0
- package/src/theme/ColorPrimitives.stories.tsx +300 -0
- package/src/theme/bar-paper.ts +59 -0
- package/src/theme/color-stories.coverage.test.ts +130 -0
- package/src/theme/color-story-kit.tsx +170 -0
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
- package/src/theme/ColorSystem.stories.tsx +0 -508
- package/src/theme/Colors.stories.tsx +0 -323
package/dist/pages.js
CHANGED
|
@@ -36,7 +36,6 @@ var primitiveColors = {
|
|
|
36
36
|
// Charcoal scale (dark backgrounds)
|
|
37
37
|
charcoal: {
|
|
38
38
|
0: "#6E6E6E",
|
|
39
|
-
100: "#4C4C4C",
|
|
40
39
|
200: "#3C3C3C",
|
|
41
40
|
300: "#2C2C2C",
|
|
42
41
|
400: "#1F1F1F",
|
|
@@ -182,1204 +181,6 @@ var WORKOUT_TOKENS = {
|
|
|
182
181
|
fill: "#3A3A3A"
|
|
183
182
|
}};
|
|
184
183
|
|
|
185
|
-
// src/utils/workout-format.ts
|
|
186
|
-
function roundRpe(rpe) {
|
|
187
|
-
return Math.round(rpe * 2) / 2;
|
|
188
|
-
}
|
|
189
|
-
function roundWeight(weight) {
|
|
190
|
-
return Math.round(weight);
|
|
191
|
-
}
|
|
192
|
-
function formatVelocity(velocity) {
|
|
193
|
-
return velocity.toFixed(2);
|
|
194
|
-
}
|
|
195
|
-
function roundTempo(tempo) {
|
|
196
|
-
return tempo.map((v) => Math.round(v * 10) / 10);
|
|
197
|
-
}
|
|
198
|
-
function wrapJsx(fn) {
|
|
199
|
-
return function(type, props, ...rest) {
|
|
200
|
-
if (props && typeof props.className === "string" && props.className) {
|
|
201
|
-
const cn2 = props.className;
|
|
202
|
-
const cssStyle = { $$css: true, [cn2]: cn2 };
|
|
203
|
-
const existing = props.style;
|
|
204
|
-
props = {
|
|
205
|
-
...props,
|
|
206
|
-
style: existing ? [cssStyle, existing] : cssStyle
|
|
207
|
-
};
|
|
208
|
-
delete props.className;
|
|
209
|
-
}
|
|
210
|
-
return fn(type, props, ...rest);
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
var jsx = wrapJsx(jsxRuntime.jsx);
|
|
214
|
-
var jsxs = wrapJsx(jsxRuntime.jsxs);
|
|
215
|
-
|
|
216
|
-
// src/components/custom/Workout/SegmentedBar.tsx
|
|
217
|
-
var PULSE_HALF_MS = 950;
|
|
218
|
-
var PULSE_MIN_OPACITY = 0.45;
|
|
219
|
-
var SEGMENTED_BAR_GAP = 5;
|
|
220
|
-
function usePulse(active) {
|
|
221
|
-
const [value] = react.useState(() => new reactNative.Animated.Value(0));
|
|
222
|
-
react.useEffect(() => {
|
|
223
|
-
if (!active) return;
|
|
224
|
-
const loop = reactNative.Animated.loop(
|
|
225
|
-
reactNative.Animated.sequence([
|
|
226
|
-
reactNative.Animated.timing(value, {
|
|
227
|
-
toValue: 1,
|
|
228
|
-
duration: PULSE_HALF_MS,
|
|
229
|
-
easing: reactNative.Easing.inOut(reactNative.Easing.ease),
|
|
230
|
-
useNativeDriver: false
|
|
231
|
-
}),
|
|
232
|
-
reactNative.Animated.timing(value, {
|
|
233
|
-
toValue: 0,
|
|
234
|
-
duration: PULSE_HALF_MS,
|
|
235
|
-
easing: reactNative.Easing.inOut(reactNative.Easing.ease),
|
|
236
|
-
useNativeDriver: false
|
|
237
|
-
})
|
|
238
|
-
])
|
|
239
|
-
);
|
|
240
|
-
loop.start();
|
|
241
|
-
return () => loop.stop();
|
|
242
|
-
}, [active, value]);
|
|
243
|
-
return value;
|
|
244
|
-
}
|
|
245
|
-
function SegmentedBar({
|
|
246
|
-
segments,
|
|
247
|
-
height = 8,
|
|
248
|
-
gap = SEGMENTED_BAR_GAP,
|
|
249
|
-
radius = 2,
|
|
250
|
-
marker = null,
|
|
251
|
-
segmentTestID,
|
|
252
|
-
style,
|
|
253
|
-
...props
|
|
254
|
-
}) {
|
|
255
|
-
const pulse = usePulse(segments.some((s) => s.pulse));
|
|
256
|
-
return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
|
|
257
|
-
segments.map((seg, i) => {
|
|
258
|
-
const fillStyle = {
|
|
259
|
-
width: `${(seg.fill ?? 1) * 100}%`,
|
|
260
|
-
height: "100%",
|
|
261
|
-
backgroundColor: seg.color,
|
|
262
|
-
opacity: seg.opacity
|
|
263
|
-
};
|
|
264
|
-
const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
|
|
265
|
-
return /* @__PURE__ */ jsx(
|
|
266
|
-
reactNative.View,
|
|
267
|
-
{
|
|
268
|
-
style: {
|
|
269
|
-
flex: seg.weight ?? 1,
|
|
270
|
-
minWidth: 0,
|
|
271
|
-
height: "100%",
|
|
272
|
-
marginLeft: seg.leadingGap,
|
|
273
|
-
borderRadius: radius,
|
|
274
|
-
overflow: "hidden"
|
|
275
|
-
},
|
|
276
|
-
children: seg.pulse ? /* @__PURE__ */ jsx(
|
|
277
|
-
reactNative.Animated.View,
|
|
278
|
-
{
|
|
279
|
-
style: {
|
|
280
|
-
...fillStyle,
|
|
281
|
-
...seg.pulseColor ? {
|
|
282
|
-
backgroundColor: pulse.interpolate({
|
|
283
|
-
inputRange: [0, 1],
|
|
284
|
-
outputRange: seg.pulseColor
|
|
285
|
-
})
|
|
286
|
-
} : {
|
|
287
|
-
opacity: pulse.interpolate({
|
|
288
|
-
inputRange: [0, 1],
|
|
289
|
-
outputRange: [PULSE_MIN_OPACITY, 1]
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
accessibilityElementsHidden: true,
|
|
294
|
-
testID
|
|
295
|
-
}
|
|
296
|
-
) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
|
|
297
|
-
},
|
|
298
|
-
i
|
|
299
|
-
);
|
|
300
|
-
}),
|
|
301
|
-
marker ? /* @__PURE__ */ jsx(
|
|
302
|
-
reactNative.View,
|
|
303
|
-
{
|
|
304
|
-
style: {
|
|
305
|
-
position: "absolute",
|
|
306
|
-
left: `${marker.position * 100}%`,
|
|
307
|
-
top: 0,
|
|
308
|
-
bottom: 0,
|
|
309
|
-
width: 2,
|
|
310
|
-
backgroundColor: marker.color
|
|
311
|
-
},
|
|
312
|
-
accessibilityElementsHidden: true,
|
|
313
|
-
testID: "segmented-bar-marker"
|
|
314
|
-
}
|
|
315
|
-
) : null
|
|
316
|
-
] });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// src/components/custom/Workout/SetBar.tsx
|
|
320
|
-
var SET_STRIP_ZONES = {
|
|
321
|
-
slow: primitiveRamps.red[600],
|
|
322
|
-
moderate: primitiveRamps.orange[400],
|
|
323
|
-
fast: primitiveRamps.amber[300],
|
|
324
|
-
fastest: primitiveRamps.green[300]
|
|
325
|
-
};
|
|
326
|
-
var TODO_COLOR = primitiveColors.charcoal[300];
|
|
327
|
-
var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
|
|
328
|
-
var NOTCH_GAP = 2;
|
|
329
|
-
var CLUSTER_GAP = 3;
|
|
330
|
-
var MYO_UPCOMING_CLUSTERS = 3;
|
|
331
|
-
var MYO_UPCOMING_CLUSTER_LEN = 5;
|
|
332
|
-
var MYO_UPCOMING_FADE = 0.6;
|
|
333
|
-
var PULSE_RANGE = {
|
|
334
|
-
[SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
|
|
335
|
-
[SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
|
|
336
|
-
[SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
|
|
337
|
-
[SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
|
|
338
|
-
};
|
|
339
|
-
function velocityZoneColor(v) {
|
|
340
|
-
if (v < 0.5) return SET_STRIP_ZONES.slow;
|
|
341
|
-
if (v < 0.75) return SET_STRIP_ZONES.moderate;
|
|
342
|
-
if (v < 1) return SET_STRIP_ZONES.fast;
|
|
343
|
-
return SET_STRIP_ZONES.fastest;
|
|
344
|
-
}
|
|
345
|
-
function chunkedSegments(chunks, gap) {
|
|
346
|
-
return chunks.flatMap(
|
|
347
|
-
(vels, ci) => vels.map((v, ri) => ({
|
|
348
|
-
color: velocityZoneColor(v),
|
|
349
|
-
...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
|
|
350
|
-
}))
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
function rangeSegments(floor, max, doneVels) {
|
|
354
|
-
return Array.from({ length: max }, (_, i) => {
|
|
355
|
-
if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
|
|
356
|
-
return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
function myoUpcomingSegments(activationLen, clusterCount) {
|
|
360
|
-
const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
|
|
361
|
-
const trail = Array.from(
|
|
362
|
-
{ length: clusterCount },
|
|
363
|
-
(_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
|
|
364
|
-
color: SET_STRIP_VARIABLE_COLOR,
|
|
365
|
-
opacity: MYO_UPCOMING_FADE ** ci,
|
|
366
|
-
...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
|
|
367
|
-
}))
|
|
368
|
-
).flat();
|
|
369
|
-
return [...activation, ...trail];
|
|
370
|
-
}
|
|
371
|
-
function setSegments(set) {
|
|
372
|
-
if (set.status === "todo") {
|
|
373
|
-
return [{ color: TODO_COLOR }];
|
|
374
|
-
}
|
|
375
|
-
if (set.status === "done") {
|
|
376
|
-
return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
|
|
377
|
-
}
|
|
378
|
-
if (set.status === "range") {
|
|
379
|
-
return rangeSegments(set.floor, set.max, set.doneVels);
|
|
380
|
-
}
|
|
381
|
-
if (set.status === "drop") {
|
|
382
|
-
return chunkedSegments(set.subloads, NOTCH_GAP);
|
|
383
|
-
}
|
|
384
|
-
if (set.status === "myo") {
|
|
385
|
-
return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
|
|
386
|
-
}
|
|
387
|
-
if (set.status === "myo-upcoming") {
|
|
388
|
-
return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
|
|
389
|
-
}
|
|
390
|
-
const performed = set.velocities.map((v) => {
|
|
391
|
-
const color = velocityZoneColor(v);
|
|
392
|
-
return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
|
|
393
|
-
});
|
|
394
|
-
const remaining = Math.max(0, set.planned - set.velocities.length);
|
|
395
|
-
return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
|
|
396
|
-
}
|
|
397
|
-
function setBarSegmentTestID(seg) {
|
|
398
|
-
if (seg.pulse) return "set-strip-pulse";
|
|
399
|
-
if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
|
|
400
|
-
return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
|
|
401
|
-
}
|
|
402
|
-
function SetBar({ set, height = 8 }) {
|
|
403
|
-
return /* @__PURE__ */ jsx(
|
|
404
|
-
SegmentedBar,
|
|
405
|
-
{
|
|
406
|
-
segments: setSegments(set),
|
|
407
|
-
height,
|
|
408
|
-
gap: 0,
|
|
409
|
-
radius: 0,
|
|
410
|
-
segmentTestID: setBarSegmentTestID,
|
|
411
|
-
style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
|
|
412
|
-
testID: "set-strip-set"
|
|
413
|
-
}
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/components/custom/Workout/SetStrip.tsx
|
|
418
|
-
var SET_STRIP_GAP = 5;
|
|
419
|
-
function setCategory(set) {
|
|
420
|
-
switch (set.status) {
|
|
421
|
-
case "done":
|
|
422
|
-
case "drop":
|
|
423
|
-
case "myo":
|
|
424
|
-
return "done";
|
|
425
|
-
case "active":
|
|
426
|
-
return "active";
|
|
427
|
-
case "range":
|
|
428
|
-
return set.doneVels.length > 0 ? "active" : "upcoming";
|
|
429
|
-
case "todo":
|
|
430
|
-
case "myo-upcoming":
|
|
431
|
-
return "upcoming";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
function describeSets(sets) {
|
|
435
|
-
const done = sets.filter((s) => setCategory(s) === "done").length;
|
|
436
|
-
const active = sets.filter((s) => setCategory(s) === "active").length;
|
|
437
|
-
const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
|
|
438
|
-
return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
|
|
439
|
-
}
|
|
440
|
-
function SetStrip({ sets, height = 8, className, ...props }) {
|
|
441
|
-
return /* @__PURE__ */ jsx(
|
|
442
|
-
reactNative.View,
|
|
443
|
-
{
|
|
444
|
-
className,
|
|
445
|
-
style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
|
|
446
|
-
accessibilityRole: "image",
|
|
447
|
-
accessibilityLabel: describeSets(sets),
|
|
448
|
-
testID: "set-strip",
|
|
449
|
-
...props,
|
|
450
|
-
children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
|
|
451
|
-
}
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// src/components/custom/Workout/VelocityStrip.tsx
|
|
456
|
-
var VEL_COLORS = WORKOUT_TOKENS.scale;
|
|
457
|
-
var bandIdToEffortColor = {
|
|
458
|
-
speed: sequentialEffort[0],
|
|
459
|
-
// green-300
|
|
460
|
-
power: sequentialEffort[2],
|
|
461
|
-
// amber-300 (gold)
|
|
462
|
-
strengthSpeed: sequentialEffort[3],
|
|
463
|
-
// orange-400
|
|
464
|
-
maximalStrength: sequentialEffort[4],
|
|
465
|
-
// red-600
|
|
466
|
-
grinding: sequentialEffort[5]
|
|
467
|
-
// red-700 — the distinct 5th band
|
|
468
|
-
};
|
|
469
|
-
function getVelocityZoneColor(velocity) {
|
|
470
|
-
if (velocity >= 1) return "vel-green";
|
|
471
|
-
if (velocity >= 0.75) return "vel-yellow";
|
|
472
|
-
if (velocity >= 0.5) return "vel-orange";
|
|
473
|
-
return "vel-red";
|
|
474
|
-
}
|
|
475
|
-
function getVelocityZoneName(velocity) {
|
|
476
|
-
if (velocity >= 1) return "Speed";
|
|
477
|
-
if (velocity >= 0.75) return "Power";
|
|
478
|
-
if (velocity >= 0.5) return "Strength-Speed";
|
|
479
|
-
return "Strength";
|
|
480
|
-
}
|
|
481
|
-
var zoneHexMap = {
|
|
482
|
-
"vel-green": VEL_COLORS.green,
|
|
483
|
-
"vel-yellow": VEL_COLORS.yellow,
|
|
484
|
-
"vel-orange": VEL_COLORS.orange,
|
|
485
|
-
"vel-red": VEL_COLORS.red
|
|
486
|
-
};
|
|
487
|
-
function calculateVelocityLoss(velocities) {
|
|
488
|
-
if (velocities.length < 2) return 0;
|
|
489
|
-
const best = Math.max(...velocities);
|
|
490
|
-
if (best <= 0) return 0;
|
|
491
|
-
const last = velocities[velocities.length - 1];
|
|
492
|
-
return Math.max(0, Math.round((best - last) / best * 100));
|
|
493
|
-
}
|
|
494
|
-
function calculateMeanVelocity(velocities) {
|
|
495
|
-
if (velocities.length === 0) return 0;
|
|
496
|
-
const sum = velocities.reduce((acc, v) => acc + v, 0);
|
|
497
|
-
return sum / velocities.length;
|
|
498
|
-
}
|
|
499
|
-
function classifyBand(velocity, bands) {
|
|
500
|
-
for (const band of bands) {
|
|
501
|
-
if (band.max === null || velocity < band.max) return band;
|
|
502
|
-
}
|
|
503
|
-
return bands[bands.length - 1];
|
|
504
|
-
}
|
|
505
|
-
function bandColor(band) {
|
|
506
|
-
return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
|
|
507
|
-
}
|
|
508
|
-
function getLossStyle(loss) {
|
|
509
|
-
if (loss > 25) return { color: VEL_COLORS.red };
|
|
510
|
-
if (loss > 20) return { color: VEL_COLORS.orange };
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
var REP_GAP = 2;
|
|
514
|
-
var WIDE_GAP = 8;
|
|
515
|
-
var TODO_COLOR2 = primitiveColors.charcoal[300];
|
|
516
|
-
var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
|
|
517
|
-
function deriveDoneVelocities(set) {
|
|
518
|
-
switch (set.type) {
|
|
519
|
-
case "drop":
|
|
520
|
-
return set.subloads.flat();
|
|
521
|
-
case "myo":
|
|
522
|
-
return [...set.activation, ...set.clusters.flat()];
|
|
523
|
-
default:
|
|
524
|
-
return set.velocities;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function chunkedRepSlots(chunks) {
|
|
528
|
-
const slots = [];
|
|
529
|
-
chunks.forEach((chunk) => {
|
|
530
|
-
chunk.forEach((velocity, ri) => {
|
|
531
|
-
const first = slots.length === 0;
|
|
532
|
-
slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP });
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
return slots;
|
|
536
|
-
}
|
|
537
|
-
function buildSlots(set) {
|
|
538
|
-
switch (set.type) {
|
|
539
|
-
case "straight": {
|
|
540
|
-
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
|
|
541
|
-
return Array.from({ length: total }, (_, i) => {
|
|
542
|
-
const done = i < set.velocities.length;
|
|
543
|
-
return {
|
|
544
|
-
kind: done ? "rep" : "todo",
|
|
545
|
-
velocity: done ? set.velocities[i] : void 0,
|
|
546
|
-
leadingGap: i === 0 ? 0 : REP_GAP
|
|
547
|
-
};
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
case "range": {
|
|
551
|
-
const total = Math.max(set.max, set.velocities.length);
|
|
552
|
-
return Array.from({ length: total }, (_, i) => {
|
|
553
|
-
const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
|
|
554
|
-
return {
|
|
555
|
-
kind,
|
|
556
|
-
velocity: kind === "rep" ? set.velocities[i] : void 0,
|
|
557
|
-
leadingGap: i === 0 ? 0 : REP_GAP
|
|
558
|
-
};
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
case "amrap": {
|
|
562
|
-
const reps = set.velocities.map((velocity, i) => ({
|
|
563
|
-
kind: "rep",
|
|
564
|
-
velocity,
|
|
565
|
-
leadingGap: i === 0 ? 0 : REP_GAP
|
|
566
|
-
}));
|
|
567
|
-
reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP });
|
|
568
|
-
return reps;
|
|
569
|
-
}
|
|
570
|
-
case "drop":
|
|
571
|
-
return chunkedRepSlots(set.subloads);
|
|
572
|
-
case "myo": {
|
|
573
|
-
const slots = chunkedRepSlots([set.activation, ...set.clusters]);
|
|
574
|
-
if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP });
|
|
575
|
-
return slots;
|
|
576
|
-
}
|
|
577
|
-
case "cluster": {
|
|
578
|
-
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
|
|
579
|
-
return Array.from({ length: total }, (_, i) => {
|
|
580
|
-
const done = i < set.velocities.length;
|
|
581
|
-
const boundary = i > 0 && i % set.groupSize === 0;
|
|
582
|
-
return {
|
|
583
|
-
kind: done ? "rep" : "todo",
|
|
584
|
-
velocity: done ? set.velocities[i] : void 0,
|
|
585
|
-
leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP
|
|
586
|
-
};
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function setAccessibilityLabel(set, repCount) {
|
|
592
|
-
switch (set.type) {
|
|
593
|
-
case "straight":
|
|
594
|
-
return `Velocity strip, straight set, ${repCount} reps`;
|
|
595
|
-
case "range":
|
|
596
|
-
return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
|
|
597
|
-
case "amrap":
|
|
598
|
-
return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
|
|
599
|
-
case "drop":
|
|
600
|
-
return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
|
|
601
|
-
case "myo":
|
|
602
|
-
return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
|
|
603
|
-
case "cluster":
|
|
604
|
-
return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function slotAccessibilityLabel(slot, index) {
|
|
608
|
-
switch (slot.kind) {
|
|
609
|
-
case "rep":
|
|
610
|
-
return `Rep ${index + 1}: ${formatVelocity(slot.velocity ?? 0)} meters per second`;
|
|
611
|
-
case "todo":
|
|
612
|
-
return `Rep ${index + 1}: planned`;
|
|
613
|
-
case "variable":
|
|
614
|
-
return `Rep ${index + 1}: variable`;
|
|
615
|
-
case "continue":
|
|
616
|
-
return "Keep going";
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
var EXPANDED_TODO_STUB_PCT = 16;
|
|
620
|
-
var EXPANDED_ENCODED_PCT = 45;
|
|
621
|
-
var EXPANDED_HEIGHT = 60;
|
|
622
|
-
var FIXED_MAX_VELOCITY = 1.15;
|
|
623
|
-
var BARE_STUB_HEIGHT = 3;
|
|
624
|
-
var HERO_HEIGHT = 220;
|
|
625
|
-
var HERO_LABEL_HEADROOM = 20;
|
|
626
|
-
var HERO_BAR_GAP = 8;
|
|
627
|
-
var HERO_BAR_MAX_WIDTH = 120;
|
|
628
|
-
var HERO_LABEL_MIN_BAR_WIDTH = 30;
|
|
629
|
-
var HERO_BAR_RADIUS = 5;
|
|
630
|
-
var HERO_MIN_BAR_HEIGHT = 4;
|
|
631
|
-
var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
|
|
632
|
-
var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
|
|
633
|
-
var PEAK_HEADROOM = 1.06;
|
|
634
|
-
function scaleDenominator(scale, maxVelocity) {
|
|
635
|
-
return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * PEAK_HEADROOM;
|
|
636
|
-
}
|
|
637
|
-
function bareSlotHeight(slot, height, denom) {
|
|
638
|
-
if (slot.kind !== "rep" || denom <= 0) return BARE_STUB_HEIGHT;
|
|
639
|
-
const ratio = Math.min(1, (slot.velocity ?? 0) / denom);
|
|
640
|
-
return Math.max(BARE_STUB_HEIGHT, ratio * height);
|
|
641
|
-
}
|
|
642
|
-
function getReducedMotionPreference() {
|
|
643
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
644
|
-
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
645
|
-
}
|
|
646
|
-
function usePrefersReducedMotion() {
|
|
647
|
-
const [reduced, setReduced] = react.useState(getReducedMotionPreference);
|
|
648
|
-
react.useEffect(() => {
|
|
649
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
650
|
-
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
651
|
-
const handler = () => setReduced(mq.matches);
|
|
652
|
-
handler();
|
|
653
|
-
mq.addEventListener?.("change", handler);
|
|
654
|
-
return () => mq.removeEventListener?.("change", handler);
|
|
655
|
-
}, []);
|
|
656
|
-
return reduced;
|
|
657
|
-
}
|
|
658
|
-
var ANIMATION_DURATION = 400;
|
|
659
|
-
var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
|
|
660
|
-
var POP_EASING = reactNative.Easing.bezier(0.34, 1.56, 0.64, 1);
|
|
661
|
-
function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
|
|
662
|
-
const prefersReducedMotion = usePrefersReducedMotion();
|
|
663
|
-
const [liveScale] = react.useState(() => new reactNative.Animated.Value(1));
|
|
664
|
-
react.useEffect(() => {
|
|
665
|
-
if (!active || liveRepIndex == null || liveVelocity == null) return;
|
|
666
|
-
if (prefersReducedMotion) {
|
|
667
|
-
liveScale.setValue(1);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
if (isNewPeak) {
|
|
671
|
-
liveScale.setValue(1);
|
|
672
|
-
reactNative.Animated.sequence([
|
|
673
|
-
reactNative.Animated.timing(liveScale, {
|
|
674
|
-
toValue: 1.25,
|
|
675
|
-
duration: 150,
|
|
676
|
-
easing: reactNative.Easing.out(reactNative.Easing.quad),
|
|
677
|
-
useNativeDriver: true
|
|
678
|
-
}),
|
|
679
|
-
reactNative.Animated.spring(liveScale, {
|
|
680
|
-
toValue: 1,
|
|
681
|
-
friction: 3,
|
|
682
|
-
tension: 140,
|
|
683
|
-
useNativeDriver: true
|
|
684
|
-
})
|
|
685
|
-
]).start();
|
|
686
|
-
} else {
|
|
687
|
-
liveScale.setValue(0.8);
|
|
688
|
-
reactNative.Animated.timing(liveScale, {
|
|
689
|
-
toValue: 1,
|
|
690
|
-
duration: 300,
|
|
691
|
-
easing: POP_EASING,
|
|
692
|
-
useNativeDriver: true
|
|
693
|
-
}).start();
|
|
694
|
-
}
|
|
695
|
-
}, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
|
|
696
|
-
return liveScale;
|
|
697
|
-
}
|
|
698
|
-
function HeroVelocityChart({
|
|
699
|
-
doneVelocities,
|
|
700
|
-
barColorFor,
|
|
701
|
-
scaleDenom,
|
|
702
|
-
referenceVelocity,
|
|
703
|
-
liveRepIndex,
|
|
704
|
-
isNewPeak,
|
|
705
|
-
targetReps,
|
|
706
|
-
height,
|
|
707
|
-
className,
|
|
708
|
-
viewProps
|
|
709
|
-
}) {
|
|
710
|
-
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
|
|
711
|
-
const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
|
|
712
|
-
const [plotW, setPlotW] = react.useState(0);
|
|
713
|
-
const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
|
|
714
|
-
const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
|
|
715
|
-
const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
|
|
716
|
-
const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
|
|
717
|
-
const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
|
|
718
|
-
const totalSlots = doneVelocities.length + pendingCount;
|
|
719
|
-
const barWidth = plotW > 0 && totalSlots > 0 ? Math.min(HERO_BAR_MAX_WIDTH, (plotW - HERO_BAR_GAP * (totalSlots - 1)) / totalSlots) : HERO_BAR_MAX_WIDTH;
|
|
720
|
-
const labelsCrowded = plotW > 0 && barWidth < HERO_LABEL_MIN_BAR_WIDTH;
|
|
721
|
-
const heroGap = plotW === 0 ? HERO_BAR_GAP : barWidth < 16 ? 2 : barWidth < 28 ? 4 : HERO_BAR_GAP;
|
|
722
|
-
const peakIndex = doneVelocities.reduce(
|
|
723
|
-
(best, v, i) => v > doneVelocities[best] ? i : best,
|
|
724
|
-
0
|
|
725
|
-
);
|
|
726
|
-
const showBarLabel = (i) => !labelsCrowded || i === peakIndex || i === liveRepIndex;
|
|
727
|
-
const repCount = doneVelocities.length;
|
|
728
|
-
const total = Math.max(repCount, targetReps ?? repCount);
|
|
729
|
-
const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
|
|
730
|
-
const { style: externalStyle, ...restProps } = viewProps;
|
|
731
|
-
return /* @__PURE__ */ jsx(
|
|
732
|
-
reactNative.View,
|
|
733
|
-
{
|
|
734
|
-
className,
|
|
735
|
-
style: [{ height }, externalStyle],
|
|
736
|
-
accessibilityRole: "image",
|
|
737
|
-
accessibilityLabel: label,
|
|
738
|
-
testID: "velocity-strip-hero",
|
|
739
|
-
...restProps,
|
|
740
|
-
children: /* @__PURE__ */ jsxs(
|
|
741
|
-
reactNative.View,
|
|
742
|
-
{
|
|
743
|
-
onLayout: onPlotLayout,
|
|
744
|
-
style: {
|
|
745
|
-
flex: 1,
|
|
746
|
-
flexDirection: "row",
|
|
747
|
-
alignItems: "flex-end",
|
|
748
|
-
gap: heroGap,
|
|
749
|
-
position: "relative",
|
|
750
|
-
borderBottomWidth: 2,
|
|
751
|
-
borderBottomColor: HERO_PENDING_COLOR
|
|
752
|
-
},
|
|
753
|
-
children: [
|
|
754
|
-
referencePx > 0 && /* @__PURE__ */ jsx(
|
|
755
|
-
reactNative.View,
|
|
756
|
-
{
|
|
757
|
-
accessibilityElementsHidden: true,
|
|
758
|
-
style: {
|
|
759
|
-
position: "absolute",
|
|
760
|
-
left: 0,
|
|
761
|
-
right: 0,
|
|
762
|
-
bottom: referencePx,
|
|
763
|
-
borderTopWidth: 1,
|
|
764
|
-
borderStyle: "dashed",
|
|
765
|
-
borderColor: HERO_REFERENCE_COLOR,
|
|
766
|
-
pointerEvents: "none"
|
|
767
|
-
},
|
|
768
|
-
testID: "velocity-hero-reference"
|
|
769
|
-
}
|
|
770
|
-
),
|
|
771
|
-
doneVelocities.map((velocity, i) => {
|
|
772
|
-
const isLive = liveRepIndex === i;
|
|
773
|
-
return /* @__PURE__ */ jsxs(
|
|
774
|
-
reactNative.View,
|
|
775
|
-
{
|
|
776
|
-
accessibilityElementsHidden: true,
|
|
777
|
-
style: {
|
|
778
|
-
flex: 1,
|
|
779
|
-
maxWidth: HERO_BAR_MAX_WIDTH,
|
|
780
|
-
height: "100%",
|
|
781
|
-
alignItems: "center",
|
|
782
|
-
justifyContent: "flex-end"
|
|
783
|
-
},
|
|
784
|
-
children: [
|
|
785
|
-
showBarLabel(i) && /* @__PURE__ */ jsx(
|
|
786
|
-
reactNative.Text,
|
|
787
|
-
{
|
|
788
|
-
className: "text-text-primary",
|
|
789
|
-
style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
|
|
790
|
-
testID: `velocity-label-${i}`,
|
|
791
|
-
children: formatVelocity(velocity)
|
|
792
|
-
}
|
|
793
|
-
),
|
|
794
|
-
/* @__PURE__ */ jsx(
|
|
795
|
-
reactNative.Animated.View,
|
|
796
|
-
{
|
|
797
|
-
style: [
|
|
798
|
-
{
|
|
799
|
-
width: "100%",
|
|
800
|
-
height: barHeight(velocity),
|
|
801
|
-
borderTopLeftRadius: HERO_BAR_RADIUS,
|
|
802
|
-
borderTopRightRadius: HERO_BAR_RADIUS,
|
|
803
|
-
backgroundColor: barColorFor(velocity)
|
|
804
|
-
},
|
|
805
|
-
isLive ? { transform: [{ scale: liveScale }] } : null
|
|
806
|
-
],
|
|
807
|
-
testID: `velocity-bar-${i}`
|
|
808
|
-
}
|
|
809
|
-
)
|
|
810
|
-
]
|
|
811
|
-
},
|
|
812
|
-
i
|
|
813
|
-
);
|
|
814
|
-
}),
|
|
815
|
-
Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
|
|
816
|
-
reactNative.View,
|
|
817
|
-
{
|
|
818
|
-
accessibilityElementsHidden: true,
|
|
819
|
-
style: {
|
|
820
|
-
flex: 1,
|
|
821
|
-
maxWidth: HERO_BAR_MAX_WIDTH,
|
|
822
|
-
height: "100%",
|
|
823
|
-
alignItems: "center",
|
|
824
|
-
justifyContent: "flex-end"
|
|
825
|
-
},
|
|
826
|
-
children: /* @__PURE__ */ jsx(
|
|
827
|
-
reactNative.View,
|
|
828
|
-
{
|
|
829
|
-
style: {
|
|
830
|
-
width: "100%",
|
|
831
|
-
height: HERO_MIN_BAR_HEIGHT * 3,
|
|
832
|
-
borderWidth: 1,
|
|
833
|
-
borderStyle: "dashed",
|
|
834
|
-
borderColor: HERO_PENDING_COLOR,
|
|
835
|
-
borderTopLeftRadius: HERO_BAR_RADIUS,
|
|
836
|
-
borderTopRightRadius: HERO_BAR_RADIUS
|
|
837
|
-
},
|
|
838
|
-
testID: "velocity-slot-todo"
|
|
839
|
-
}
|
|
840
|
-
)
|
|
841
|
-
},
|
|
842
|
-
`pending-${i}`
|
|
843
|
-
))
|
|
844
|
-
]
|
|
845
|
-
}
|
|
846
|
-
)
|
|
847
|
-
}
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
function VelocityStrip({
|
|
851
|
-
velocities,
|
|
852
|
-
set,
|
|
853
|
-
zones,
|
|
854
|
-
liveRepIndex,
|
|
855
|
-
expanded = true,
|
|
856
|
-
onToggle,
|
|
857
|
-
onRepPress,
|
|
858
|
-
variant = "expanded",
|
|
859
|
-
height = EXPANDED_HEIGHT,
|
|
860
|
-
targetReps,
|
|
861
|
-
scale = "peak",
|
|
862
|
-
showNumbers = true,
|
|
863
|
-
showInfo = true,
|
|
864
|
-
className,
|
|
865
|
-
...props
|
|
866
|
-
}) {
|
|
867
|
-
const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
|
|
868
|
-
const slots = set ? buildSlots(set) : doneVelocities.map((v, i) => ({
|
|
869
|
-
kind: "rep",
|
|
870
|
-
velocity: v,
|
|
871
|
-
leadingGap: i === 0 ? 0 : REP_GAP
|
|
872
|
-
}));
|
|
873
|
-
const maxVelocity = Math.max(...doneVelocities, 0);
|
|
874
|
-
const meanVelocity = calculateMeanVelocity(doneVelocities);
|
|
875
|
-
const loss = calculateVelocityLoss(doneVelocities);
|
|
876
|
-
const framed = showNumbers || showInfo;
|
|
877
|
-
const scaleDenom = scaleDenominator(scale, maxVelocity);
|
|
878
|
-
const hasZones = zones != null && zones.length > 0;
|
|
879
|
-
const barColorFor = (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
|
|
880
|
-
const slotColor = (slot) => {
|
|
881
|
-
if (slot.kind === "rep") return barColorFor(slot.velocity ?? 0);
|
|
882
|
-
if (slot.kind === "todo") return TODO_COLOR2;
|
|
883
|
-
return SET_STRIP_VARIABLE_COLOR;
|
|
884
|
-
};
|
|
885
|
-
const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
|
|
886
|
-
const [heightAnim] = react.useState(() => new reactNative.Animated.Value(expanded ? height : 3));
|
|
887
|
-
const [labelOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
|
|
888
|
-
const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
|
|
889
|
-
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
|
|
890
|
-
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
|
|
891
|
-
const liveScale = useLiveRepPop(
|
|
892
|
-
variant === "expanded" && framed,
|
|
893
|
-
liveRepIndex,
|
|
894
|
-
liveVelocity,
|
|
895
|
-
isNewPeak
|
|
896
|
-
);
|
|
897
|
-
react.useEffect(() => {
|
|
898
|
-
if (variant !== "expanded" || !framed) return;
|
|
899
|
-
reactNative.Animated.parallel([
|
|
900
|
-
reactNative.Animated.timing(heightAnim, {
|
|
901
|
-
toValue: expanded ? height : 3,
|
|
902
|
-
duration: ANIMATION_DURATION,
|
|
903
|
-
easing: ANIMATION_EASING,
|
|
904
|
-
useNativeDriver: false
|
|
905
|
-
}),
|
|
906
|
-
reactNative.Animated.timing(labelOpacity, {
|
|
907
|
-
toValue: expanded ? 1 : 0,
|
|
908
|
-
duration: 300,
|
|
909
|
-
delay: expanded ? 150 : 0,
|
|
910
|
-
useNativeDriver: false
|
|
911
|
-
}),
|
|
912
|
-
reactNative.Animated.timing(infoOpacity, {
|
|
913
|
-
toValue: expanded ? 1 : 0,
|
|
914
|
-
duration: 300,
|
|
915
|
-
delay: expanded ? 200 : 0,
|
|
916
|
-
useNativeDriver: false
|
|
917
|
-
})
|
|
918
|
-
]).start();
|
|
919
|
-
}, [expanded, variant, framed, height, heightAnim, labelOpacity, infoOpacity]);
|
|
920
|
-
if (set == null && velocities == null) return null;
|
|
921
|
-
const repCount = doneVelocities.length;
|
|
922
|
-
const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
|
|
923
|
-
if (variant === "hero") {
|
|
924
|
-
const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
|
|
925
|
-
return /* @__PURE__ */ jsx(
|
|
926
|
-
HeroVelocityChart,
|
|
927
|
-
{
|
|
928
|
-
doneVelocities,
|
|
929
|
-
barColorFor,
|
|
930
|
-
scaleDenom,
|
|
931
|
-
referenceVelocity: maxVelocity,
|
|
932
|
-
liveRepIndex,
|
|
933
|
-
isNewPeak,
|
|
934
|
-
targetReps,
|
|
935
|
-
height: heroHeight,
|
|
936
|
-
className,
|
|
937
|
-
viewProps: props
|
|
938
|
-
}
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
if (variant === "mini") {
|
|
942
|
-
const { style: externalStyle, ...restProps } = props;
|
|
943
|
-
return /* @__PURE__ */ jsx(
|
|
944
|
-
reactNative.View,
|
|
945
|
-
{
|
|
946
|
-
className,
|
|
947
|
-
style: [
|
|
948
|
-
{ flexDirection: "row", height: 3, gap: REP_GAP, borderRadius: 2, overflow: "hidden" },
|
|
949
|
-
externalStyle
|
|
950
|
-
],
|
|
951
|
-
accessibilityRole: "image",
|
|
952
|
-
accessibilityLabel: miniLabel,
|
|
953
|
-
testID: "velocity-strip-mini",
|
|
954
|
-
...restProps,
|
|
955
|
-
children: slots.map((slot, i) => /* @__PURE__ */ jsx(
|
|
956
|
-
reactNative.View,
|
|
957
|
-
{
|
|
958
|
-
style: {
|
|
959
|
-
flex: 1,
|
|
960
|
-
backgroundColor: slotColor(slot),
|
|
961
|
-
borderRadius: 1,
|
|
962
|
-
minWidth: 4,
|
|
963
|
-
height: "100%",
|
|
964
|
-
// The container's uniform 2px gap covers rep spacing; a wide slot adds
|
|
965
|
-
// only the EXTRA (WIDE_GAP − REP_GAP) so the notch totals WIDE_GAP.
|
|
966
|
-
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
967
|
-
...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
|
|
968
|
-
},
|
|
969
|
-
accessibilityElementsHidden: true,
|
|
970
|
-
testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
|
|
971
|
-
},
|
|
972
|
-
i
|
|
973
|
-
))
|
|
974
|
-
}
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
if (!framed) {
|
|
978
|
-
const { style: externalStyle, ...restProps } = props;
|
|
979
|
-
return /* @__PURE__ */ jsx(
|
|
980
|
-
reactNative.View,
|
|
981
|
-
{
|
|
982
|
-
className,
|
|
983
|
-
style: [
|
|
984
|
-
{ flexDirection: "row", height, gap: REP_GAP, alignItems: "flex-end" },
|
|
985
|
-
externalStyle
|
|
986
|
-
],
|
|
987
|
-
accessibilityRole: "image",
|
|
988
|
-
accessibilityLabel: miniLabel,
|
|
989
|
-
testID: "velocity-strip-compact",
|
|
990
|
-
...restProps,
|
|
991
|
-
children: slots.map((slot, i) => /* @__PURE__ */ jsx(
|
|
992
|
-
reactNative.View,
|
|
993
|
-
{
|
|
994
|
-
style: {
|
|
995
|
-
flex: 1,
|
|
996
|
-
minWidth: 4,
|
|
997
|
-
height: bareSlotHeight(slot, height, scaleDenom),
|
|
998
|
-
// Top-rounded to match the framed chart's bars (the "rounded tops from
|
|
999
|
-
// the numbers one"); square bottoms since bars sit on the baseline.
|
|
1000
|
-
borderTopLeftRadius: 2,
|
|
1001
|
-
borderTopRightRadius: 2,
|
|
1002
|
-
backgroundColor: slotColor(slot),
|
|
1003
|
-
marginLeft: Math.max(0, slot.leadingGap - REP_GAP),
|
|
1004
|
-
...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
|
|
1005
|
-
},
|
|
1006
|
-
accessibilityElementsHidden: true,
|
|
1007
|
-
testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`
|
|
1008
|
-
},
|
|
1009
|
-
i
|
|
1010
|
-
))
|
|
1011
|
-
}
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
|
|
1015
|
-
const hasInteractiveContainer = onToggle != null;
|
|
1016
|
-
const hasInteractiveReps = onRepPress != null && expanded;
|
|
1017
|
-
const setBars = set != null && slots.map((slot, i) => {
|
|
1018
|
-
const isStraightRep = set.type === "straight" && slot.kind === "rep";
|
|
1019
|
-
const heightPct = isStraightRep ? scaleDenom > 0 ? Math.round((slot.velocity ?? 0) / scaleDenom * 100) : 0 : set.type === "straight" ? EXPANDED_TODO_STUB_PCT : EXPANDED_ENCODED_PCT;
|
|
1020
|
-
return /* @__PURE__ */ jsx(
|
|
1021
|
-
reactNative.View,
|
|
1022
|
-
{
|
|
1023
|
-
style: {
|
|
1024
|
-
flex: 1,
|
|
1025
|
-
height: "100%",
|
|
1026
|
-
justifyContent: "flex-end",
|
|
1027
|
-
marginLeft: slot.leadingGap
|
|
1028
|
-
},
|
|
1029
|
-
children: /* @__PURE__ */ jsx(
|
|
1030
|
-
reactNative.View,
|
|
1031
|
-
{
|
|
1032
|
-
style: {
|
|
1033
|
-
height: `${heightPct}%`,
|
|
1034
|
-
minHeight: 2,
|
|
1035
|
-
borderTopLeftRadius: 2,
|
|
1036
|
-
borderTopRightRadius: 2,
|
|
1037
|
-
backgroundColor: slotColor(slot),
|
|
1038
|
-
...slot.kind === "continue" ? { borderWidth: 1, borderColor: CONTINUE_OUTLINE } : {}
|
|
1039
|
-
},
|
|
1040
|
-
testID: slot.kind === "rep" ? `velocity-bar-${i}` : `velocity-slot-${slot.kind}`,
|
|
1041
|
-
accessibilityRole: "image",
|
|
1042
|
-
accessibilityLabel: slotAccessibilityLabel(slot, i)
|
|
1043
|
-
}
|
|
1044
|
-
)
|
|
1045
|
-
},
|
|
1046
|
-
i
|
|
1047
|
-
);
|
|
1048
|
-
});
|
|
1049
|
-
const stripContent = /* @__PURE__ */ jsxs(
|
|
1050
|
-
reactNative.Animated.View,
|
|
1051
|
-
{
|
|
1052
|
-
className: [className, expanded ? "bg-surface-raised" : "bg-transparent"].filter(Boolean).join(" "),
|
|
1053
|
-
style: [
|
|
1054
|
-
{
|
|
1055
|
-
height: heightAnim,
|
|
1056
|
-
width: "100%",
|
|
1057
|
-
gap: 2,
|
|
1058
|
-
borderRadius: expanded ? 6 : 2,
|
|
1059
|
-
paddingTop: expanded ? 16 : 0,
|
|
1060
|
-
paddingBottom: expanded ? 24 : 0,
|
|
1061
|
-
// No horizontal inset — the framed chart's bars span full width, matching
|
|
1062
|
-
// the bare spotlight and mini. Numbers/info sit in the vertical padding.
|
|
1063
|
-
paddingHorizontal: 0,
|
|
1064
|
-
paddingVertical: expanded ? void 0 : 8,
|
|
1065
|
-
overflow: expanded ? "visible" : "hidden"
|
|
1066
|
-
}
|
|
1067
|
-
],
|
|
1068
|
-
accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
|
|
1069
|
-
accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
|
|
1070
|
-
testID: "velocity-strip",
|
|
1071
|
-
...props,
|
|
1072
|
-
children: [
|
|
1073
|
-
/* @__PURE__ */ jsx(reactNative.View, { style: { flexDirection: "row", flex: 1, gap: set ? 0 : 2, alignItems: "flex-end" }, children: set ? setBars : doneVelocities.map((v, i) => {
|
|
1074
|
-
const barBackground = barColorFor(v);
|
|
1075
|
-
const barHeightPct = scaleDenom > 0 ? Math.round(v / scaleDenom * 100) : 0;
|
|
1076
|
-
const isLive = liveRepIndex === i;
|
|
1077
|
-
const liveLabelSuffix = isLive ? isNewPeak ? ", latest rep, new set peak" : ", latest rep" : "";
|
|
1078
|
-
const barStyle = expanded ? {
|
|
1079
|
-
height: `${barHeightPct}%`,
|
|
1080
|
-
minHeight: 2,
|
|
1081
|
-
borderTopLeftRadius: 2,
|
|
1082
|
-
borderTopRightRadius: 2,
|
|
1083
|
-
backgroundColor: barBackground
|
|
1084
|
-
} : {
|
|
1085
|
-
minHeight: "100%",
|
|
1086
|
-
borderTopLeftRadius: 2,
|
|
1087
|
-
borderTopRightRadius: 2,
|
|
1088
|
-
borderBottomLeftRadius: 0,
|
|
1089
|
-
borderBottomRightRadius: 0,
|
|
1090
|
-
backgroundColor: barBackground
|
|
1091
|
-
};
|
|
1092
|
-
const barInner = isLive ? /* @__PURE__ */ jsx(
|
|
1093
|
-
reactNative.Animated.View,
|
|
1094
|
-
{
|
|
1095
|
-
style: [barStyle, { transform: [{ scale: liveScale }] }],
|
|
1096
|
-
testID: `velocity-bar-${i}`,
|
|
1097
|
-
accessibilityRole: "image",
|
|
1098
|
-
accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`
|
|
1099
|
-
}
|
|
1100
|
-
) : /* @__PURE__ */ jsx(
|
|
1101
|
-
reactNative.View,
|
|
1102
|
-
{
|
|
1103
|
-
style: barStyle,
|
|
1104
|
-
testID: `velocity-bar-${i}`,
|
|
1105
|
-
accessibilityRole: "image",
|
|
1106
|
-
accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second`
|
|
1107
|
-
}
|
|
1108
|
-
);
|
|
1109
|
-
const bar = /* @__PURE__ */ jsxs(
|
|
1110
|
-
reactNative.View,
|
|
1111
|
-
{
|
|
1112
|
-
style: {
|
|
1113
|
-
flex: 1,
|
|
1114
|
-
height: "100%",
|
|
1115
|
-
justifyContent: "flex-end",
|
|
1116
|
-
position: "relative"
|
|
1117
|
-
},
|
|
1118
|
-
children: [
|
|
1119
|
-
expanded && showNumbers && /* @__PURE__ */ jsx(
|
|
1120
|
-
reactNative.Animated.View,
|
|
1121
|
-
{
|
|
1122
|
-
style: {
|
|
1123
|
-
opacity: labelOpacity,
|
|
1124
|
-
alignItems: "center",
|
|
1125
|
-
position: "absolute",
|
|
1126
|
-
top: -13,
|
|
1127
|
-
left: 0,
|
|
1128
|
-
right: 0
|
|
1129
|
-
},
|
|
1130
|
-
accessibilityElementsHidden: true,
|
|
1131
|
-
children: /* @__PURE__ */ jsx(
|
|
1132
|
-
reactNative.Text,
|
|
1133
|
-
{
|
|
1134
|
-
className: "text-text-secondary",
|
|
1135
|
-
style: { fontSize: 8, fontWeight: "600" },
|
|
1136
|
-
testID: `velocity-label-${i}`,
|
|
1137
|
-
children: formatVelocity(v)
|
|
1138
|
-
}
|
|
1139
|
-
)
|
|
1140
|
-
}
|
|
1141
|
-
),
|
|
1142
|
-
barInner
|
|
1143
|
-
]
|
|
1144
|
-
},
|
|
1145
|
-
i
|
|
1146
|
-
);
|
|
1147
|
-
if (onRepPress && expanded) {
|
|
1148
|
-
return /* @__PURE__ */ jsx(
|
|
1149
|
-
reactNative.Pressable,
|
|
1150
|
-
{
|
|
1151
|
-
style: { flex: 1, height: "100%" },
|
|
1152
|
-
onPress: () => onRepPress(i, v),
|
|
1153
|
-
accessibilityRole: "button",
|
|
1154
|
-
accessibilityLabel: `Rep ${i + 1}: ${formatVelocity(v)} meters per second, tap for details`,
|
|
1155
|
-
testID: `velocity-bar-pressable-${i}`,
|
|
1156
|
-
children: bar
|
|
1157
|
-
},
|
|
1158
|
-
i
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
return bar;
|
|
1162
|
-
}) }),
|
|
1163
|
-
expanded && showInfo && /* @__PURE__ */ jsxs(
|
|
1164
|
-
reactNative.Animated.View,
|
|
1165
|
-
{
|
|
1166
|
-
style: {
|
|
1167
|
-
flexDirection: "row",
|
|
1168
|
-
justifyContent: "space-between",
|
|
1169
|
-
opacity: infoOpacity,
|
|
1170
|
-
position: "absolute",
|
|
1171
|
-
bottom: 4,
|
|
1172
|
-
left: 6,
|
|
1173
|
-
right: 6
|
|
1174
|
-
},
|
|
1175
|
-
testID: "velocity-info-row",
|
|
1176
|
-
children: [
|
|
1177
|
-
/* @__PURE__ */ jsxs(
|
|
1178
|
-
reactNative.Text,
|
|
1179
|
-
{
|
|
1180
|
-
className: "text-text-secondary",
|
|
1181
|
-
style: {
|
|
1182
|
-
fontSize: 10,
|
|
1183
|
-
fontFamily: "Inter, sans-serif"
|
|
1184
|
-
},
|
|
1185
|
-
children: [
|
|
1186
|
-
meanZone,
|
|
1187
|
-
" ",
|
|
1188
|
-
"\xB7",
|
|
1189
|
-
" ",
|
|
1190
|
-
formatVelocity(meanVelocity),
|
|
1191
|
-
" m/s"
|
|
1192
|
-
]
|
|
1193
|
-
}
|
|
1194
|
-
),
|
|
1195
|
-
/* @__PURE__ */ jsxs(
|
|
1196
|
-
reactNative.Text,
|
|
1197
|
-
{
|
|
1198
|
-
className: "text-text-secondary",
|
|
1199
|
-
style: {
|
|
1200
|
-
fontSize: 10,
|
|
1201
|
-
fontFamily: "Inter, sans-serif",
|
|
1202
|
-
...getLossStyle(loss)
|
|
1203
|
-
},
|
|
1204
|
-
children: [
|
|
1205
|
-
"Loss: ",
|
|
1206
|
-
loss,
|
|
1207
|
-
"%"
|
|
1208
|
-
]
|
|
1209
|
-
}
|
|
1210
|
-
)
|
|
1211
|
-
]
|
|
1212
|
-
}
|
|
1213
|
-
)
|
|
1214
|
-
]
|
|
1215
|
-
}
|
|
1216
|
-
);
|
|
1217
|
-
if (onToggle) {
|
|
1218
|
-
return /* @__PURE__ */ jsx(
|
|
1219
|
-
reactNative.Pressable,
|
|
1220
|
-
{
|
|
1221
|
-
onPress: onToggle,
|
|
1222
|
-
accessibilityRole: "button",
|
|
1223
|
-
accessibilityLabel: stripLabel,
|
|
1224
|
-
testID: "velocity-strip-pressable",
|
|
1225
|
-
children: stripContent
|
|
1226
|
-
}
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
return stripContent;
|
|
1230
|
-
}
|
|
1231
|
-
function cn(...inputs) {
|
|
1232
|
-
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// src/components/custom/Workout/PlaceholderStrip.tsx
|
|
1236
|
-
function SingleStrip({
|
|
1237
|
-
width,
|
|
1238
|
-
className,
|
|
1239
|
-
...props
|
|
1240
|
-
}) {
|
|
1241
|
-
return /* @__PURE__ */ jsx(
|
|
1242
|
-
reactNative.View,
|
|
1243
|
-
{
|
|
1244
|
-
className,
|
|
1245
|
-
style: [
|
|
1246
|
-
{
|
|
1247
|
-
height: 3,
|
|
1248
|
-
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
1249
|
-
borderRadius: 2,
|
|
1250
|
-
opacity: 0.5
|
|
1251
|
-
},
|
|
1252
|
-
width != null ? { width } : { flex: 1 }
|
|
1253
|
-
],
|
|
1254
|
-
accessibilityRole: "image",
|
|
1255
|
-
accessibilityLabel: "Planned set, not yet completed",
|
|
1256
|
-
testID: "placeholder-strip",
|
|
1257
|
-
...props
|
|
1258
|
-
}
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
function SegmentedStrip({
|
|
1262
|
-
segments = 3,
|
|
1263
|
-
width,
|
|
1264
|
-
className,
|
|
1265
|
-
...props
|
|
1266
|
-
}) {
|
|
1267
|
-
return /* @__PURE__ */ jsx(
|
|
1268
|
-
reactNative.View,
|
|
1269
|
-
{
|
|
1270
|
-
className: cn("flex-row", className),
|
|
1271
|
-
style: [
|
|
1272
|
-
{ height: 3, gap: 2, opacity: 0.5 },
|
|
1273
|
-
width != null ? { width } : { flex: 1 }
|
|
1274
|
-
],
|
|
1275
|
-
accessibilityRole: "image",
|
|
1276
|
-
accessibilityLabel: "Planned set, not yet completed",
|
|
1277
|
-
testID: "placeholder-strip",
|
|
1278
|
-
...props,
|
|
1279
|
-
children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
|
|
1280
|
-
reactNative.View,
|
|
1281
|
-
{
|
|
1282
|
-
style: {
|
|
1283
|
-
flex: 1,
|
|
1284
|
-
height: 3,
|
|
1285
|
-
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
1286
|
-
borderRadius: 1,
|
|
1287
|
-
minWidth: 4
|
|
1288
|
-
},
|
|
1289
|
-
accessibilityElementsHidden: true,
|
|
1290
|
-
testID: "placeholder-segment"
|
|
1291
|
-
},
|
|
1292
|
-
i
|
|
1293
|
-
))
|
|
1294
|
-
}
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
function PlaceholderStrip({
|
|
1298
|
-
mode = "single",
|
|
1299
|
-
segments,
|
|
1300
|
-
...props
|
|
1301
|
-
}) {
|
|
1302
|
-
if (mode === "segmented") {
|
|
1303
|
-
return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
|
|
1304
|
-
}
|
|
1305
|
-
return /* @__PURE__ */ jsx(SingleStrip, { ...props });
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// src/components/icons/SvgIcon.tsx
|
|
1309
|
-
function SvgIcon({
|
|
1310
|
-
size = 24,
|
|
1311
|
-
color = "currentColor",
|
|
1312
|
-
strokeWidth = 2,
|
|
1313
|
-
fill = "none",
|
|
1314
|
-
title,
|
|
1315
|
-
children
|
|
1316
|
-
}) {
|
|
1317
|
-
return /* @__PURE__ */ jsxs(
|
|
1318
|
-
"svg",
|
|
1319
|
-
{
|
|
1320
|
-
width: size,
|
|
1321
|
-
height: size,
|
|
1322
|
-
viewBox: "0 0 24 24",
|
|
1323
|
-
fill,
|
|
1324
|
-
stroke: color,
|
|
1325
|
-
strokeWidth,
|
|
1326
|
-
strokeLinecap: "round",
|
|
1327
|
-
strokeLinejoin: "round",
|
|
1328
|
-
...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
|
|
1329
|
-
children: [
|
|
1330
|
-
title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
|
|
1331
|
-
children
|
|
1332
|
-
]
|
|
1333
|
-
}
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// src/components/icons/icons.tsx
|
|
1338
|
-
function StarIcon(props) {
|
|
1339
|
-
return /* @__PURE__ */ jsx(SvgIcon, { ...props, 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" }) });
|
|
1340
|
-
}
|
|
1341
|
-
function ScaleIcon(props) {
|
|
1342
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1343
|
-
/* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
|
|
1344
|
-
/* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
|
|
1345
|
-
/* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
|
|
1346
|
-
/* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
|
|
1347
|
-
/* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
|
|
1348
|
-
] });
|
|
1349
|
-
}
|
|
1350
|
-
function AlertTriangleIcon(props) {
|
|
1351
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1352
|
-
/* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
|
|
1353
|
-
/* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
|
|
1354
|
-
/* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
|
|
1355
|
-
] });
|
|
1356
|
-
}
|
|
1357
|
-
function TrendingDownIcon(props) {
|
|
1358
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1359
|
-
/* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
|
|
1360
|
-
/* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
|
|
1361
|
-
] });
|
|
1362
|
-
}
|
|
1363
|
-
function CircleSlashIcon(props) {
|
|
1364
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1365
|
-
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1366
|
-
/* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
|
|
1367
|
-
] });
|
|
1368
|
-
}
|
|
1369
|
-
function AwardIcon(props) {
|
|
1370
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1371
|
-
/* @__PURE__ */ jsx("path", { d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" }),
|
|
1372
|
-
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
|
|
1373
|
-
] });
|
|
1374
|
-
}
|
|
1375
|
-
function InfoIcon(props) {
|
|
1376
|
-
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1377
|
-
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1378
|
-
/* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
|
|
1379
|
-
/* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
|
|
1380
|
-
] });
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
184
|
// src/theme/tokens/semantic.ts
|
|
1384
185
|
var semanticColorsLight = {
|
|
1385
186
|
// Brand colors (brand-*)
|
|
@@ -1685,11 +486,1504 @@ var semanticColorsDark = {
|
|
|
1685
486
|
"avatar-background": primitiveColors.neutral[600],
|
|
1686
487
|
"avatar-text": primitiveColors.white
|
|
1687
488
|
};
|
|
1688
|
-
function getSemanticColors(mode) {
|
|
1689
|
-
return mode === "dark" ? semanticColorsDark : semanticColorsLight;
|
|
489
|
+
function getSemanticColors(mode) {
|
|
490
|
+
return mode === "dark" ? semanticColorsDark : semanticColorsLight;
|
|
491
|
+
}
|
|
492
|
+
function hexToRgb(hex) {
|
|
493
|
+
const cleaned = hex.replace("#", "");
|
|
494
|
+
let r, g, b;
|
|
495
|
+
if (cleaned.length === 3) {
|
|
496
|
+
r = parseInt(cleaned[0] + cleaned[0], 16);
|
|
497
|
+
g = parseInt(cleaned[1] + cleaned[1], 16);
|
|
498
|
+
b = parseInt(cleaned[2] + cleaned[2], 16);
|
|
499
|
+
} else if (cleaned.length === 6) {
|
|
500
|
+
r = parseInt(cleaned.slice(0, 2), 16);
|
|
501
|
+
g = parseInt(cleaned.slice(2, 4), 16);
|
|
502
|
+
b = parseInt(cleaned.slice(4, 6), 16);
|
|
503
|
+
} else {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return { r, g, b };
|
|
507
|
+
}
|
|
508
|
+
function rgbToHsv(r, g, b) {
|
|
509
|
+
const rNorm = r / 255;
|
|
510
|
+
const gNorm = g / 255;
|
|
511
|
+
const bNorm = b / 255;
|
|
512
|
+
const max = Math.max(rNorm, gNorm, bNorm);
|
|
513
|
+
const min = Math.min(rNorm, gNorm, bNorm);
|
|
514
|
+
const delta = max - min;
|
|
515
|
+
const v = max;
|
|
516
|
+
const s = max === 0 ? 0 : delta / max;
|
|
517
|
+
let h = 0;
|
|
518
|
+
if (delta !== 0) {
|
|
519
|
+
if (max === rNorm) {
|
|
520
|
+
h = 60 * ((gNorm - bNorm) / delta % 6);
|
|
521
|
+
} else if (max === gNorm) {
|
|
522
|
+
h = 60 * ((bNorm - rNorm) / delta + 2);
|
|
523
|
+
} else {
|
|
524
|
+
h = 60 * ((rNorm - gNorm) / delta + 4);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (h < 0) h += 360;
|
|
528
|
+
return { h, s, v };
|
|
529
|
+
}
|
|
530
|
+
function hsvToRgb(h, s, v) {
|
|
531
|
+
const c = v * s;
|
|
532
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
533
|
+
const m = v - c;
|
|
534
|
+
let rPrime, gPrime, bPrime;
|
|
535
|
+
if (h >= 0 && h < 60) {
|
|
536
|
+
rPrime = c;
|
|
537
|
+
gPrime = x;
|
|
538
|
+
bPrime = 0;
|
|
539
|
+
} else if (h >= 60 && h < 120) {
|
|
540
|
+
rPrime = x;
|
|
541
|
+
gPrime = c;
|
|
542
|
+
bPrime = 0;
|
|
543
|
+
} else if (h >= 120 && h < 180) {
|
|
544
|
+
rPrime = 0;
|
|
545
|
+
gPrime = c;
|
|
546
|
+
bPrime = x;
|
|
547
|
+
} else if (h >= 180 && h < 240) {
|
|
548
|
+
rPrime = 0;
|
|
549
|
+
gPrime = x;
|
|
550
|
+
bPrime = c;
|
|
551
|
+
} else if (h >= 240 && h < 300) {
|
|
552
|
+
rPrime = x;
|
|
553
|
+
gPrime = 0;
|
|
554
|
+
bPrime = c;
|
|
555
|
+
} else {
|
|
556
|
+
rPrime = c;
|
|
557
|
+
gPrime = 0;
|
|
558
|
+
bPrime = x;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
r: Math.round((rPrime + m) * 255),
|
|
562
|
+
g: Math.round((gPrime + m) * 255),
|
|
563
|
+
b: Math.round((bPrime + m) * 255)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function rgbToHex(r, g, b) {
|
|
567
|
+
const toHex = (n) => {
|
|
568
|
+
const clamped = Math.max(0, Math.min(255, Math.round(n)));
|
|
569
|
+
return clamped.toString(16).padStart(2, "0");
|
|
570
|
+
};
|
|
571
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
572
|
+
}
|
|
573
|
+
function lighten(hex, amount = 0.1) {
|
|
574
|
+
const rgb = hexToRgb(hex);
|
|
575
|
+
if (!rgb) return hex;
|
|
576
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
577
|
+
const newV = Math.min(1, hsv.v + amount);
|
|
578
|
+
const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
|
|
579
|
+
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
580
|
+
}
|
|
581
|
+
var shadowColors = {
|
|
582
|
+
// For charcoal surfaces (~#2C2C2C to #3C3C3C)
|
|
583
|
+
charcoal: {
|
|
584
|
+
dark: "rgba(0, 0, 0, 0.5)",
|
|
585
|
+
light: "rgba(255, 255, 255, 0.12)",
|
|
586
|
+
// brighter for crisp rim light
|
|
587
|
+
darker: "rgba(0, 0, 0, 0.6)"
|
|
588
|
+
},
|
|
589
|
+
// For neutral dark surfaces (~#1F2937)
|
|
590
|
+
neutralDark: {
|
|
591
|
+
dark: "rgba(0, 0, 0, 0.4)",
|
|
592
|
+
light: "rgba(255, 255, 255, 0.08)",
|
|
593
|
+
darker: "rgba(0, 0, 0, 0.5)"
|
|
594
|
+
},
|
|
595
|
+
// For light surfaces (white/light gray backgrounds)
|
|
596
|
+
neutralLight: {
|
|
597
|
+
dark: "rgba(0, 0, 0, 0.15)",
|
|
598
|
+
// Visible dark shadow for depth on light backgrounds
|
|
599
|
+
light: "rgba(255, 255, 255, 1)",
|
|
600
|
+
// Bright highlight for raised effect (visible on light gray)
|
|
601
|
+
darker: "rgba(0, 0, 0, 0.2)"
|
|
602
|
+
// Deeper shadow for pressed states
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
var intensityConfig = {
|
|
606
|
+
subtle: { offset: 1, blur: 2 },
|
|
607
|
+
// very light, barely visible
|
|
608
|
+
medium: { offset: 2, blur: 4 },
|
|
609
|
+
// noticeable but restrained
|
|
610
|
+
strong: { offset: 2, blur: 5 }
|
|
611
|
+
// slightly deeper blur, same offset as medium
|
|
612
|
+
};
|
|
613
|
+
function createRaisedShadow(surface = "charcoal", intensity = "medium") {
|
|
614
|
+
const colors = shadowColors[surface];
|
|
615
|
+
const { offset, blur } = intensityConfig[intensity];
|
|
616
|
+
const rimBlur = 1;
|
|
617
|
+
return reactNative.Platform.select({
|
|
618
|
+
web: {
|
|
619
|
+
boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
|
|
620
|
+
},
|
|
621
|
+
default: {
|
|
622
|
+
// React Native shadow approximation (limited compared to web)
|
|
623
|
+
shadowColor: "#000",
|
|
624
|
+
shadowOffset: { width: offset, height: offset },
|
|
625
|
+
shadowOpacity: 0.3,
|
|
626
|
+
shadowRadius: blur / 2,
|
|
627
|
+
elevation: offset
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function createPressedShadow(surface = "charcoal", intensity = "medium") {
|
|
632
|
+
const colors = shadowColors[surface];
|
|
633
|
+
const { offset, blur } = intensityConfig[intensity];
|
|
634
|
+
const rimOffset = 1;
|
|
635
|
+
const rimBlur = 1;
|
|
636
|
+
return reactNative.Platform.select({
|
|
637
|
+
web: {
|
|
638
|
+
boxShadow: `inset ${offset}px ${offset}px ${blur}px ${colors.darker}, inset -${rimOffset}px -${rimOffset}px ${rimBlur}px ${colors.light}`
|
|
639
|
+
},
|
|
640
|
+
default: {
|
|
641
|
+
// React Native doesn't support inset shadows, approximate with darker bg
|
|
642
|
+
shadowOpacity: 0,
|
|
643
|
+
elevation: 0
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function createFlatShadow() {
|
|
648
|
+
return reactNative.Platform.select({
|
|
649
|
+
web: {
|
|
650
|
+
boxShadow: "none"
|
|
651
|
+
},
|
|
652
|
+
default: {
|
|
653
|
+
shadowOpacity: 0,
|
|
654
|
+
elevation: 0
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
function createRaisedHoverShadow(surface = "charcoal", intensity = "medium") {
|
|
659
|
+
const colors = shadowColors[surface];
|
|
660
|
+
const { offset, blur } = intensityConfig[intensity];
|
|
661
|
+
const hoverOffset = Math.max(1, Math.round(offset / 2));
|
|
662
|
+
const hoverBlur = Math.max(1, Math.round(blur / 2));
|
|
663
|
+
const rimBlur = 1;
|
|
664
|
+
return reactNative.Platform.select({
|
|
665
|
+
web: {
|
|
666
|
+
// Keep same offsets as normal state for rim to prevent "shrinking" effect
|
|
667
|
+
boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
|
|
668
|
+
},
|
|
669
|
+
default: {
|
|
670
|
+
shadowColor: "#000",
|
|
671
|
+
shadowOffset: { width: hoverOffset, height: hoverOffset },
|
|
672
|
+
shadowOpacity: 0.2,
|
|
673
|
+
shadowRadius: hoverBlur / 2,
|
|
674
|
+
elevation: hoverOffset
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function createPressedHoverShadow(surface = "charcoal", intensity = "medium") {
|
|
679
|
+
const colors = shadowColors[surface];
|
|
680
|
+
const { offset, blur } = intensityConfig[intensity];
|
|
681
|
+
const hoverOffset = Math.max(1, Math.round(offset / 2));
|
|
682
|
+
const hoverBlur = Math.max(1, Math.round(blur / 2));
|
|
683
|
+
const rimBlur = 1;
|
|
684
|
+
return reactNative.Platform.select({
|
|
685
|
+
web: {
|
|
686
|
+
// Keep rim offset at 1px (same as pressed normal) to prevent "shrinking" effect
|
|
687
|
+
boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${colors.darker}, inset -1px -1px ${rimBlur}px ${colors.light}`
|
|
688
|
+
},
|
|
689
|
+
default: {
|
|
690
|
+
shadowOpacity: 0,
|
|
691
|
+
elevation: 0
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
({
|
|
696
|
+
// Charcoal surface shadows (for dark toolbars, cards)
|
|
697
|
+
charcoal: {
|
|
698
|
+
raised: {
|
|
699
|
+
subtle: createRaisedShadow("charcoal", "subtle"),
|
|
700
|
+
medium: createRaisedShadow("charcoal", "medium"),
|
|
701
|
+
strong: createRaisedShadow("charcoal", "strong")
|
|
702
|
+
},
|
|
703
|
+
raisedHover: {
|
|
704
|
+
subtle: createRaisedHoverShadow("charcoal", "subtle"),
|
|
705
|
+
medium: createRaisedHoverShadow("charcoal", "medium"),
|
|
706
|
+
strong: createRaisedHoverShadow("charcoal", "strong")
|
|
707
|
+
},
|
|
708
|
+
pressed: {
|
|
709
|
+
subtle: createPressedShadow("charcoal", "subtle"),
|
|
710
|
+
medium: createPressedShadow("charcoal", "medium"),
|
|
711
|
+
strong: createPressedShadow("charcoal", "strong")
|
|
712
|
+
},
|
|
713
|
+
pressedHover: {
|
|
714
|
+
subtle: createPressedHoverShadow("charcoal", "subtle"),
|
|
715
|
+
medium: createPressedHoverShadow("charcoal", "medium"),
|
|
716
|
+
strong: createPressedHoverShadow("charcoal", "strong")
|
|
717
|
+
},
|
|
718
|
+
flat: createFlatShadow()
|
|
719
|
+
},
|
|
720
|
+
// Neutral dark surface shadows
|
|
721
|
+
neutralDark: {
|
|
722
|
+
raised: {
|
|
723
|
+
subtle: createRaisedShadow("neutralDark", "subtle"),
|
|
724
|
+
medium: createRaisedShadow("neutralDark", "medium"),
|
|
725
|
+
strong: createRaisedShadow("neutralDark", "strong")
|
|
726
|
+
},
|
|
727
|
+
raisedHover: {
|
|
728
|
+
subtle: createRaisedHoverShadow("neutralDark", "subtle"),
|
|
729
|
+
medium: createRaisedHoverShadow("neutralDark", "medium"),
|
|
730
|
+
strong: createRaisedHoverShadow("neutralDark", "strong")
|
|
731
|
+
},
|
|
732
|
+
pressed: {
|
|
733
|
+
subtle: createPressedShadow("neutralDark", "subtle"),
|
|
734
|
+
medium: createPressedShadow("neutralDark", "medium"),
|
|
735
|
+
strong: createPressedShadow("neutralDark", "strong")
|
|
736
|
+
},
|
|
737
|
+
pressedHover: {
|
|
738
|
+
subtle: createPressedHoverShadow("neutralDark", "subtle"),
|
|
739
|
+
medium: createPressedHoverShadow("neutralDark", "medium"),
|
|
740
|
+
strong: createPressedHoverShadow("neutralDark", "strong")
|
|
741
|
+
},
|
|
742
|
+
flat: createFlatShadow()
|
|
743
|
+
},
|
|
744
|
+
// Neutral light surface shadows
|
|
745
|
+
neutralLight: {
|
|
746
|
+
raised: {
|
|
747
|
+
subtle: createRaisedShadow("neutralLight", "subtle"),
|
|
748
|
+
medium: createRaisedShadow("neutralLight", "medium"),
|
|
749
|
+
strong: createRaisedShadow("neutralLight", "strong")
|
|
750
|
+
},
|
|
751
|
+
raisedHover: {
|
|
752
|
+
subtle: createRaisedHoverShadow("neutralLight", "subtle"),
|
|
753
|
+
medium: createRaisedHoverShadow("neutralLight", "medium"),
|
|
754
|
+
strong: createRaisedHoverShadow("neutralLight", "strong")
|
|
755
|
+
},
|
|
756
|
+
pressed: {
|
|
757
|
+
subtle: createPressedShadow("neutralLight", "subtle"),
|
|
758
|
+
medium: createPressedShadow("neutralLight", "medium"),
|
|
759
|
+
strong: createPressedShadow("neutralLight", "strong")
|
|
760
|
+
},
|
|
761
|
+
pressedHover: {
|
|
762
|
+
subtle: createPressedHoverShadow("neutralLight", "subtle"),
|
|
763
|
+
medium: createPressedHoverShadow("neutralLight", "medium"),
|
|
764
|
+
strong: createPressedHoverShadow("neutralLight", "strong")
|
|
765
|
+
},
|
|
766
|
+
flat: createFlatShadow()
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/utils/colors.ts
|
|
771
|
+
function alpha(color, opacity) {
|
|
772
|
+
const clampedOpacity = Math.max(0, Math.min(1, opacity));
|
|
773
|
+
if (color.startsWith("#")) {
|
|
774
|
+
const hex = color.slice(1);
|
|
775
|
+
let r, g, b;
|
|
776
|
+
if (hex.length === 3) {
|
|
777
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
778
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
779
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
780
|
+
} else if (hex.length === 6) {
|
|
781
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
782
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
783
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
784
|
+
} else {
|
|
785
|
+
return color;
|
|
786
|
+
}
|
|
787
|
+
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
|
|
788
|
+
}
|
|
789
|
+
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
790
|
+
if (rgbMatch) {
|
|
791
|
+
const [, r, g, b] = rgbMatch;
|
|
792
|
+
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
|
|
793
|
+
}
|
|
794
|
+
const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
|
|
795
|
+
if (rgbaMatch) {
|
|
796
|
+
const [, r, g, b] = rgbaMatch;
|
|
797
|
+
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
|
|
798
|
+
}
|
|
799
|
+
return color;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/utils/workout-format.ts
|
|
803
|
+
function roundRpe(rpe) {
|
|
804
|
+
return Math.round(rpe * 2) / 2;
|
|
805
|
+
}
|
|
806
|
+
function roundWeight(weight) {
|
|
807
|
+
return Math.round(weight);
|
|
808
|
+
}
|
|
809
|
+
function formatVelocity(velocity) {
|
|
810
|
+
return velocity.toFixed(2);
|
|
811
|
+
}
|
|
812
|
+
function roundTempo(tempo) {
|
|
813
|
+
return tempo.map((v) => Math.round(v * 10) / 10);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/theme/bar-paper.ts
|
|
817
|
+
function grainOpacityForColor(hex) {
|
|
818
|
+
const h = hex.replace("#", "");
|
|
819
|
+
const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;
|
|
820
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
821
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
822
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
823
|
+
if ([r, g, b].some(Number.isNaN)) return 0.14;
|
|
824
|
+
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
825
|
+
return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * lum));
|
|
826
|
+
}
|
|
827
|
+
function barGrain(color) {
|
|
828
|
+
const op = grainOpacityForColor(color).toFixed(3);
|
|
829
|
+
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='${op}'/%3E%3C/svg%3E")`;
|
|
830
|
+
}
|
|
831
|
+
function barPaper(color, flip = false) {
|
|
832
|
+
return {
|
|
833
|
+
backgroundImage: barGrain(color),
|
|
834
|
+
boxShadow: flip ? "0 -6px 16px rgba(0,0,0,0.45)" : "inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)"
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
var SURFACE_LEVEL_TOKEN = {
|
|
838
|
+
background: "background-base",
|
|
839
|
+
base: "surface-base",
|
|
840
|
+
elevated: "surface-elevated",
|
|
841
|
+
raised: "surface-raised",
|
|
842
|
+
overlay: "surface-overlay"
|
|
843
|
+
};
|
|
844
|
+
var ON_SURFACE_TOKEN = {
|
|
845
|
+
primary: "text-primary",
|
|
846
|
+
secondary: "text-secondary",
|
|
847
|
+
tertiary: "text-tertiary"
|
|
848
|
+
};
|
|
849
|
+
var DEFAULT_CONTEXT = { mode: "dark", level: "base" };
|
|
850
|
+
var SurfaceContext = react.createContext(DEFAULT_CONTEXT);
|
|
851
|
+
function useSurface() {
|
|
852
|
+
return react.useContext(SurfaceContext);
|
|
853
|
+
}
|
|
854
|
+
function useSurfaceMode() {
|
|
855
|
+
return react.useContext(SurfaceContext).mode;
|
|
856
|
+
}
|
|
857
|
+
function useOnSurfaceColor(role = "primary") {
|
|
858
|
+
return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]];
|
|
859
|
+
}
|
|
860
|
+
function insetFloor(mode) {
|
|
861
|
+
const colors = getSemanticColors(mode);
|
|
862
|
+
return colors["surface-inset"] ?? surfaceRampDark.inset;
|
|
863
|
+
}
|
|
864
|
+
function surfaceBackground(level, mode) {
|
|
865
|
+
if (level === "inset") return insetFloor(mode);
|
|
866
|
+
return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]];
|
|
867
|
+
}
|
|
868
|
+
var ANIMATION_EASING = reactNative.Easing.bezier(0.22, 1, 0.36, 1);
|
|
869
|
+
var PEAK_OVERSHOOT = 1.12;
|
|
870
|
+
function getReducedMotionPreference() {
|
|
871
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
872
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
873
|
+
}
|
|
874
|
+
function usePrefersReducedMotion() {
|
|
875
|
+
const [reduced, setReduced] = react.useState(getReducedMotionPreference);
|
|
876
|
+
react.useEffect(() => {
|
|
877
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
878
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
879
|
+
const handler = () => setReduced(mq.matches);
|
|
880
|
+
handler();
|
|
881
|
+
mq.addEventListener?.("change", handler);
|
|
882
|
+
return () => mq.removeEventListener?.("change", handler);
|
|
883
|
+
}, []);
|
|
884
|
+
return reduced;
|
|
885
|
+
}
|
|
886
|
+
function useLiveRepGrowth(active, liveRepIndex, liveVelocity, isNewPeak) {
|
|
887
|
+
const prefersReducedMotion = usePrefersReducedMotion();
|
|
888
|
+
const [liveGrowth] = react.useState(() => new reactNative.Animated.Value(1));
|
|
889
|
+
react.useEffect(() => {
|
|
890
|
+
if (liveRepIndex == null || liveVelocity == null) return;
|
|
891
|
+
if (prefersReducedMotion) {
|
|
892
|
+
liveGrowth.setValue(1);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
liveGrowth.setValue(0);
|
|
896
|
+
if (isNewPeak) {
|
|
897
|
+
reactNative.Animated.sequence([
|
|
898
|
+
reactNative.Animated.timing(liveGrowth, {
|
|
899
|
+
toValue: PEAK_OVERSHOOT,
|
|
900
|
+
duration: 220,
|
|
901
|
+
easing: reactNative.Easing.out(reactNative.Easing.quad),
|
|
902
|
+
useNativeDriver: false
|
|
903
|
+
}),
|
|
904
|
+
reactNative.Animated.spring(liveGrowth, {
|
|
905
|
+
toValue: 1,
|
|
906
|
+
friction: 5,
|
|
907
|
+
tension: 140,
|
|
908
|
+
useNativeDriver: false
|
|
909
|
+
})
|
|
910
|
+
]).start();
|
|
911
|
+
} else {
|
|
912
|
+
reactNative.Animated.timing(liveGrowth, {
|
|
913
|
+
toValue: 1,
|
|
914
|
+
duration: 300,
|
|
915
|
+
easing: ANIMATION_EASING,
|
|
916
|
+
useNativeDriver: false
|
|
917
|
+
}).start();
|
|
918
|
+
}
|
|
919
|
+
}, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveGrowth]);
|
|
920
|
+
return liveGrowth;
|
|
921
|
+
}
|
|
922
|
+
function wrapJsx(fn) {
|
|
923
|
+
return function(type, props, ...rest) {
|
|
924
|
+
if (props && typeof props.className === "string" && props.className) {
|
|
925
|
+
const cn2 = props.className;
|
|
926
|
+
const cssStyle = { $$css: true, [cn2]: cn2 };
|
|
927
|
+
const existing = props.style;
|
|
928
|
+
props = {
|
|
929
|
+
...props,
|
|
930
|
+
style: existing ? [cssStyle, existing] : cssStyle
|
|
931
|
+
};
|
|
932
|
+
delete props.className;
|
|
933
|
+
}
|
|
934
|
+
return fn(type, props, ...rest);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
var jsx = wrapJsx(jsxRuntime.jsx);
|
|
938
|
+
var jsxs = wrapJsx(jsxRuntime.jsxs);
|
|
939
|
+
|
|
940
|
+
// src/components/custom/charts/SetBarChart.tsx
|
|
941
|
+
function mixHex(a, b, t12) {
|
|
942
|
+
const parse = (h) => {
|
|
943
|
+
const s = h.replace("#", "");
|
|
944
|
+
return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
|
|
945
|
+
};
|
|
946
|
+
const [ar, ag, ab] = parse(a);
|
|
947
|
+
const [br, bg, bb] = parse(b);
|
|
948
|
+
const ch = (x, y) => Math.round(x + (y - x) * t12).toString(16).padStart(2, "0");
|
|
949
|
+
return `#${ch(ar, br)}${ch(ag, bg)}${ch(ab, bb)}`;
|
|
950
|
+
}
|
|
951
|
+
var VARIABLE_FILL = primitiveRamps.cyan[900];
|
|
952
|
+
var CONTINUE_OUTLINE = primitiveRamps.cyan[800];
|
|
953
|
+
var FIXED_MAX_VALUE = 1.15;
|
|
954
|
+
var PEAK_HEADROOM = 1.03;
|
|
955
|
+
function scaleDenominator(scale, maxValue) {
|
|
956
|
+
return scale === "fixed" ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM;
|
|
957
|
+
}
|
|
958
|
+
function valueLabelFontSize(height) {
|
|
959
|
+
return Math.round(Math.max(8, Math.min(12, height * 0.11)));
|
|
960
|
+
}
|
|
961
|
+
var SET_BAR_DEFAULT_HEIGHT = 220;
|
|
962
|
+
var LABEL_ROW_HEIGHT = 16;
|
|
963
|
+
var LABEL_GAP = 3;
|
|
964
|
+
var GAP_RATIO = 0.08;
|
|
965
|
+
var MIN_BAR_GAP = 2;
|
|
966
|
+
var CHUNK_NOTCH_RATIO = 0.25;
|
|
967
|
+
var CHUNK_NOTCH_MIN_PX = 10;
|
|
968
|
+
var BAR_MAX_WIDTH = 120;
|
|
969
|
+
var LABEL_MIN_BAR_WIDTH = 30;
|
|
970
|
+
var DEFAULT_BAR_RADIUS = 5;
|
|
971
|
+
var MIN_BAR_HEIGHT = 4;
|
|
972
|
+
var FLAT_BAR_FRACTION = 0.5;
|
|
973
|
+
var STUB_HEIGHT = 9;
|
|
974
|
+
var REP_GAP = 2;
|
|
975
|
+
function computeBarLayout(plotWidth, count, gapRatio = GAP_RATIO) {
|
|
976
|
+
const rawBarWidth = plotWidth > 0 && count > 0 ? plotWidth / (count + (count - 1) * gapRatio) : BAR_MAX_WIDTH;
|
|
977
|
+
const barWidth = Math.min(BAR_MAX_WIDTH, rawBarWidth);
|
|
978
|
+
const gap = plotWidth === 0 ? MIN_BAR_GAP : Math.max(MIN_BAR_GAP, gapRatio * barWidth);
|
|
979
|
+
return { barWidth, gap };
|
|
980
|
+
}
|
|
981
|
+
function isRep(slot) {
|
|
982
|
+
return slot.kind === "rep";
|
|
983
|
+
}
|
|
984
|
+
function repIndexByCell(cells) {
|
|
985
|
+
let count = 0;
|
|
986
|
+
return cells.map((slot) => isRep(slot) ? count++ : -1);
|
|
987
|
+
}
|
|
988
|
+
function SetBarChart({
|
|
989
|
+
slots,
|
|
990
|
+
colorFor,
|
|
991
|
+
height,
|
|
992
|
+
scale = "peak",
|
|
993
|
+
scaleMax,
|
|
994
|
+
orientation = "up",
|
|
995
|
+
liveRepIndex,
|
|
996
|
+
isNewPeak = false,
|
|
997
|
+
targetReps,
|
|
998
|
+
gapRatio = GAP_RATIO,
|
|
999
|
+
barRadius = DEFAULT_BAR_RADIUS,
|
|
1000
|
+
cornerStyle = "top",
|
|
1001
|
+
flat = false,
|
|
1002
|
+
showValueLabels: showValueLabelsProp = false,
|
|
1003
|
+
formatValue: formatValue2 = String,
|
|
1004
|
+
todoVariant = "solid",
|
|
1005
|
+
minColumns,
|
|
1006
|
+
renderReference,
|
|
1007
|
+
renderBarOverlay,
|
|
1008
|
+
expandProgress,
|
|
1009
|
+
hideBaseline = false,
|
|
1010
|
+
testID,
|
|
1011
|
+
testIDPrefix = "setbar",
|
|
1012
|
+
accessibilityLabel: accessibilityLabel2,
|
|
1013
|
+
label,
|
|
1014
|
+
className,
|
|
1015
|
+
viewProps = {}
|
|
1016
|
+
}) {
|
|
1017
|
+
const targetPadded = Math.max(slots.length, targetReps ?? 0, minColumns ?? 0);
|
|
1018
|
+
const cells = targetPadded > slots.length ? [
|
|
1019
|
+
...slots,
|
|
1020
|
+
...Array.from({ length: targetPadded - slots.length }, () => ({ kind: "todo" }))
|
|
1021
|
+
] : slots;
|
|
1022
|
+
const repValues = cells.filter(isRep).map((s) => s.value ?? 0);
|
|
1023
|
+
const best = repValues.length > 0 ? Math.max(...repValues) : 0;
|
|
1024
|
+
const flip = orientation === "down";
|
|
1025
|
+
const flipStyle = flip ? { transform: [{ scaleY: -1 }] } : null;
|
|
1026
|
+
const placeholderColor = useOnSurfaceColor("tertiary");
|
|
1027
|
+
const surface = useSurface();
|
|
1028
|
+
const surfaceBg = surfaceBackground(surface.level, surface.mode);
|
|
1029
|
+
const solidTodoColor = mixHex(surfaceBg, placeholderColor, 0.55);
|
|
1030
|
+
const emptyColor = mixHex(surfaceBg, placeholderColor, 0.28);
|
|
1031
|
+
const barCorners = cornerStyle === "all" ? { borderRadius: barRadius } : { borderTopLeftRadius: barRadius, borderTopRightRadius: barRadius };
|
|
1032
|
+
const scaleDenom = scaleMax != null && scaleMax > 0 ? scaleMax * PEAK_HEADROOM : scaleDenominator(scale, best);
|
|
1033
|
+
const showValueLabels = showValueLabelsProp && !flat;
|
|
1034
|
+
const plotHeight = Math.max(0, height - (showValueLabels ? LABEL_ROW_HEIGHT + LABEL_GAP : 0));
|
|
1035
|
+
const yOf = (value) => scaleDenom > 0 ? value / scaleDenom * plotHeight : 0;
|
|
1036
|
+
const flatBarHeight = Math.max(MIN_BAR_HEIGHT, FLAT_BAR_FRACTION * plotHeight);
|
|
1037
|
+
const compactBarHeight = Math.max(MIN_BAR_HEIGHT, plotHeight);
|
|
1038
|
+
const valueBarHeight = (value) => scaleDenom > 0 ? Math.max(MIN_BAR_HEIGHT, Math.min(1, value / scaleDenom) * plotHeight) : MIN_BAR_HEIGHT;
|
|
1039
|
+
const barHeight = (value) => flat ? compactBarHeight : valueBarHeight(value);
|
|
1040
|
+
const liveValue = liveRepIndex != null && liveRepIndex < repValues.length ? repValues[liveRepIndex] : void 0;
|
|
1041
|
+
const liveGrowth = useLiveRepGrowth(true, liveRepIndex, liveValue, isNewPeak);
|
|
1042
|
+
const [plotW, setPlotW] = react.useState(0);
|
|
1043
|
+
const onPlotLayout = (e) => setPlotW(e.nativeEvent.layout.width);
|
|
1044
|
+
const totalCells = cells.length;
|
|
1045
|
+
const { barWidth, gap } = computeBarLayout(plotW, totalCells, gapRatio);
|
|
1046
|
+
const labelsCrowded = plotW > 0 && barWidth < LABEL_MIN_BAR_WIDTH;
|
|
1047
|
+
const peakRepIndex = repValues.reduce((b, v, i) => v > repValues[b] ? i : b, 0);
|
|
1048
|
+
const showBarLabel = (repIndex) => !labelsCrowded || repIndex === peakRepIndex || repIndex === liveRepIndex;
|
|
1049
|
+
const geometry = { scaleDenom, plotHeight, yOf, best, flip };
|
|
1050
|
+
const { style: externalStyle, ...restProps } = viewProps;
|
|
1051
|
+
const a11y = accessibilityLabel2 != null ? { accessibilityRole: "image", accessibilityLabel: accessibilityLabel2 } : {};
|
|
1052
|
+
const repIndices = repIndexByCell(cells);
|
|
1053
|
+
return /* @__PURE__ */ jsxs(
|
|
1054
|
+
reactNative.View,
|
|
1055
|
+
{
|
|
1056
|
+
className,
|
|
1057
|
+
style: [{ height, flexDirection: "row" }, externalStyle],
|
|
1058
|
+
testID,
|
|
1059
|
+
...a11y,
|
|
1060
|
+
...restProps,
|
|
1061
|
+
children: [
|
|
1062
|
+
label ? /* @__PURE__ */ jsx(
|
|
1063
|
+
ChartSideRail,
|
|
1064
|
+
{
|
|
1065
|
+
sections: [{ label, testID: `${testIDPrefix}-side-label` }],
|
|
1066
|
+
sectionExtent: height
|
|
1067
|
+
}
|
|
1068
|
+
) : null,
|
|
1069
|
+
/* @__PURE__ */ jsxs(
|
|
1070
|
+
reactNative.View,
|
|
1071
|
+
{
|
|
1072
|
+
onLayout: onPlotLayout,
|
|
1073
|
+
style: {
|
|
1074
|
+
flex: 1,
|
|
1075
|
+
flexDirection: "row",
|
|
1076
|
+
alignItems: "flex-end",
|
|
1077
|
+
gap,
|
|
1078
|
+
position: "relative",
|
|
1079
|
+
borderBottomWidth: hideBaseline ? 0 : 2,
|
|
1080
|
+
borderBottomColor: placeholderColor,
|
|
1081
|
+
...flipStyle
|
|
1082
|
+
},
|
|
1083
|
+
children: [
|
|
1084
|
+
renderReference?.(geometry),
|
|
1085
|
+
cells.map((slot, i) => {
|
|
1086
|
+
const isChunkBoundary = (slot.leadingGap ?? REP_GAP) > REP_GAP;
|
|
1087
|
+
const extraGap = isChunkBoundary ? Math.max(CHUNK_NOTCH_MIN_PX, CHUNK_NOTCH_RATIO * barWidth) : 0;
|
|
1088
|
+
const column = {
|
|
1089
|
+
flex: 1,
|
|
1090
|
+
maxWidth: BAR_MAX_WIDTH,
|
|
1091
|
+
height: "100%",
|
|
1092
|
+
alignItems: "center",
|
|
1093
|
+
justifyContent: "flex-end",
|
|
1094
|
+
marginLeft: extraGap
|
|
1095
|
+
};
|
|
1096
|
+
if (!isRep(slot)) {
|
|
1097
|
+
return /* @__PURE__ */ jsx(reactNative.View, { accessibilityElementsHidden: true, style: column, children: renderStub(
|
|
1098
|
+
slot.kind,
|
|
1099
|
+
barCorners,
|
|
1100
|
+
// Flat (compact) stubs fill the plot like the compact bars; else the fixed short stub.
|
|
1101
|
+
flat ? compactBarHeight : STUB_HEIGHT,
|
|
1102
|
+
placeholderColor,
|
|
1103
|
+
testIDPrefix,
|
|
1104
|
+
todoVariant,
|
|
1105
|
+
solidTodoColor,
|
|
1106
|
+
emptyColor
|
|
1107
|
+
) }, i);
|
|
1108
|
+
}
|
|
1109
|
+
const repIndex = repIndices[i];
|
|
1110
|
+
const value = slot.value ?? 0;
|
|
1111
|
+
const isLive = liveRepIndex === repIndex;
|
|
1112
|
+
const color = colorFor(value);
|
|
1113
|
+
const barHeightStyle = expandProgress ? expandProgress.interpolate({
|
|
1114
|
+
inputRange: [0, 1],
|
|
1115
|
+
outputRange: [flatBarHeight, valueBarHeight(value)]
|
|
1116
|
+
}) : isLive ? liveGrowth.interpolate({ inputRange: [0, 1], outputRange: [0, barHeight(value)] }) : barHeight(value);
|
|
1117
|
+
const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex);
|
|
1118
|
+
const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP;
|
|
1119
|
+
return /* @__PURE__ */ jsxs(reactNative.View, { accessibilityElementsHidden: renderBarOverlay == null, style: column, children: [
|
|
1120
|
+
ownLabel && // All labels pin to one aligned row JUST ABOVE THE PEAK BAR (not the container top),
|
|
1121
|
+
// in the muted on-surface-secondary tone — so the row hugs the bars, no floating void.
|
|
1122
|
+
/* @__PURE__ */ jsx(
|
|
1123
|
+
reactNative.Animated.View,
|
|
1124
|
+
{
|
|
1125
|
+
style: {
|
|
1126
|
+
position: "absolute",
|
|
1127
|
+
bottom: labelBottom,
|
|
1128
|
+
left: 0,
|
|
1129
|
+
right: 0,
|
|
1130
|
+
alignItems: "center",
|
|
1131
|
+
...expandProgress ? { opacity: expandProgress } : null
|
|
1132
|
+
},
|
|
1133
|
+
pointerEvents: "none",
|
|
1134
|
+
children: /* @__PURE__ */ jsx(
|
|
1135
|
+
reactNative.Text,
|
|
1136
|
+
{
|
|
1137
|
+
className: "text-text-secondary",
|
|
1138
|
+
style: [{ fontSize: valueLabelFontSize(height), fontWeight: "700" }, flipStyle],
|
|
1139
|
+
testID: `${testIDPrefix}-label-${repIndex}`,
|
|
1140
|
+
children: formatValue2(value)
|
|
1141
|
+
}
|
|
1142
|
+
)
|
|
1143
|
+
}
|
|
1144
|
+
),
|
|
1145
|
+
/* @__PURE__ */ jsx(
|
|
1146
|
+
reactNative.Animated.View,
|
|
1147
|
+
{
|
|
1148
|
+
style: [
|
|
1149
|
+
{ width: "100%", height: barHeightStyle, backgroundColor: color },
|
|
1150
|
+
barCorners,
|
|
1151
|
+
barPaper(color, flip)
|
|
1152
|
+
],
|
|
1153
|
+
testID: `${testIDPrefix}-bar-${repIndex}`
|
|
1154
|
+
}
|
|
1155
|
+
),
|
|
1156
|
+
renderBarOverlay?.(repIndex, value)
|
|
1157
|
+
] }, i);
|
|
1158
|
+
})
|
|
1159
|
+
]
|
|
1160
|
+
}
|
|
1161
|
+
)
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
function renderStub(kind, barCorners, stubHeight, placeholderColor, testIDPrefix, todoVariant, solidTodoColor, emptyColor) {
|
|
1167
|
+
const base = {
|
|
1168
|
+
width: "100%",
|
|
1169
|
+
height: stubHeight,
|
|
1170
|
+
...barCorners
|
|
1171
|
+
};
|
|
1172
|
+
if (kind === "variable") {
|
|
1173
|
+
return /* @__PURE__ */ jsx(
|
|
1174
|
+
reactNative.View,
|
|
1175
|
+
{
|
|
1176
|
+
style: { ...base, backgroundColor: VARIABLE_FILL },
|
|
1177
|
+
testID: `${testIDPrefix}-slot-variable`
|
|
1178
|
+
}
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (kind === "continue") {
|
|
1182
|
+
return /* @__PURE__ */ jsx(
|
|
1183
|
+
reactNative.View,
|
|
1184
|
+
{
|
|
1185
|
+
style: { ...base, borderWidth: 1, borderColor: CONTINUE_OUTLINE },
|
|
1186
|
+
testID: `${testIDPrefix}-slot-continue`
|
|
1187
|
+
}
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (kind === "empty") {
|
|
1191
|
+
return /* @__PURE__ */ jsx(
|
|
1192
|
+
reactNative.View,
|
|
1193
|
+
{
|
|
1194
|
+
style: { ...base, backgroundColor: emptyColor },
|
|
1195
|
+
testID: `${testIDPrefix}-slot-empty`
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const todoStyle = todoVariant === "solid" ? { ...base, backgroundColor: solidTodoColor } : { ...base, borderWidth: 1, borderStyle: "dashed", borderColor: placeholderColor };
|
|
1200
|
+
return /* @__PURE__ */ jsx(reactNative.View, { style: todoStyle, testID: `${testIDPrefix}-slot-todo` });
|
|
1201
|
+
}
|
|
1202
|
+
function sideLabelFontSize(height) {
|
|
1203
|
+
return Math.round(Math.max(7, Math.min(13, height * 0.06)));
|
|
1204
|
+
}
|
|
1205
|
+
var SIDE_LABEL_INITIALS_BELOW = 70;
|
|
1206
|
+
function sideLabelText(label, extent) {
|
|
1207
|
+
if (extent >= SIDE_LABEL_INITIALS_BELOW) return label;
|
|
1208
|
+
return label.trim().split(/\s+/).map((word) => word.charAt(0)).join(" ").toUpperCase();
|
|
1209
|
+
}
|
|
1210
|
+
function ChartSideRail({
|
|
1211
|
+
sections,
|
|
1212
|
+
sectionExtent,
|
|
1213
|
+
variant = "hero"
|
|
1214
|
+
}) {
|
|
1215
|
+
if (!sections.some((s) => s.label)) return null;
|
|
1216
|
+
const isRail = variant === "rail";
|
|
1217
|
+
const labelLength = Math.max(0, sectionExtent - (isRail ? 0 : 6));
|
|
1218
|
+
const fontSize = isRail ? 7 : sideLabelFontSize(sectionExtent);
|
|
1219
|
+
const textStyle = {
|
|
1220
|
+
maxWidth: "100%",
|
|
1221
|
+
textAlign: "center",
|
|
1222
|
+
fontSize,
|
|
1223
|
+
fontWeight: "700",
|
|
1224
|
+
letterSpacing: isRail || fontSize < 10 ? 0 : 2,
|
|
1225
|
+
textTransform: "uppercase"
|
|
1226
|
+
};
|
|
1227
|
+
return /* @__PURE__ */ jsx(reactNative.View, { style: { width: isRail ? 18 : 34 }, accessibilityElementsHidden: true, children: sections.map((s) => /* @__PURE__ */ jsx(
|
|
1228
|
+
reactNative.View,
|
|
1229
|
+
{
|
|
1230
|
+
style: { height: sectionExtent, alignItems: "center", justifyContent: "center" },
|
|
1231
|
+
children: s.label ? /* @__PURE__ */ jsx(
|
|
1232
|
+
reactNative.View,
|
|
1233
|
+
{
|
|
1234
|
+
style: {
|
|
1235
|
+
width: labelLength,
|
|
1236
|
+
alignItems: "center",
|
|
1237
|
+
transform: [{ rotate: "-90deg" }]
|
|
1238
|
+
},
|
|
1239
|
+
children: /* @__PURE__ */ jsx(
|
|
1240
|
+
reactNative.Text,
|
|
1241
|
+
{
|
|
1242
|
+
className: "text-text-tertiary",
|
|
1243
|
+
style: textStyle,
|
|
1244
|
+
numberOfLines: 1,
|
|
1245
|
+
testID: s.testID,
|
|
1246
|
+
children: isRail ? s.label : sideLabelText(s.label, sectionExtent)
|
|
1247
|
+
}
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
) : null
|
|
1251
|
+
},
|
|
1252
|
+
s.testID
|
|
1253
|
+
)) });
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/components/custom/Workout/VelocityStrip.tsx
|
|
1257
|
+
var VEL_COLORS = WORKOUT_TOKENS.scale;
|
|
1258
|
+
var bandIdToEffortColor = {
|
|
1259
|
+
speed: sequentialEffort[0],
|
|
1260
|
+
// green-300
|
|
1261
|
+
power: sequentialEffort[2],
|
|
1262
|
+
// amber-300 (gold)
|
|
1263
|
+
strengthSpeed: sequentialEffort[3],
|
|
1264
|
+
// orange-400
|
|
1265
|
+
maximalStrength: sequentialEffort[4],
|
|
1266
|
+
// red-600
|
|
1267
|
+
grinding: sequentialEffort[5]
|
|
1268
|
+
// red-700 — the distinct 5th band
|
|
1269
|
+
};
|
|
1270
|
+
function getVelocityZoneColor(velocity) {
|
|
1271
|
+
if (velocity >= 1) return "vel-green";
|
|
1272
|
+
if (velocity >= 0.75) return "vel-yellow";
|
|
1273
|
+
if (velocity >= 0.5) return "vel-orange";
|
|
1274
|
+
return "vel-red";
|
|
1275
|
+
}
|
|
1276
|
+
function getVelocityZoneName(velocity) {
|
|
1277
|
+
if (velocity >= 1) return "Speed";
|
|
1278
|
+
if (velocity >= 0.75) return "Power";
|
|
1279
|
+
if (velocity >= 0.5) return "Strength-Speed";
|
|
1280
|
+
return "Strength";
|
|
1281
|
+
}
|
|
1282
|
+
var zoneHexMap = {
|
|
1283
|
+
"vel-green": VEL_COLORS.green,
|
|
1284
|
+
"vel-yellow": VEL_COLORS.yellow,
|
|
1285
|
+
"vel-orange": VEL_COLORS.orange,
|
|
1286
|
+
"vel-red": VEL_COLORS.red
|
|
1287
|
+
};
|
|
1288
|
+
function calculateVelocityLoss(velocities) {
|
|
1289
|
+
if (velocities.length < 2) return 0;
|
|
1290
|
+
const best = Math.max(...velocities);
|
|
1291
|
+
if (best <= 0) return 0;
|
|
1292
|
+
const last = velocities[velocities.length - 1];
|
|
1293
|
+
return Math.max(0, Math.round((best - last) / best * 100));
|
|
1294
|
+
}
|
|
1295
|
+
function calculateMeanVelocity(velocities) {
|
|
1296
|
+
if (velocities.length === 0) return 0;
|
|
1297
|
+
const sum = velocities.reduce((acc, v) => acc + v, 0);
|
|
1298
|
+
return sum / velocities.length;
|
|
1299
|
+
}
|
|
1300
|
+
var VL_LOSS_THRESHOLDS = [10, 20, 30];
|
|
1301
|
+
function getVelocityLossColor(lossPct) {
|
|
1302
|
+
const [t1, t22, t32] = VL_LOSS_THRESHOLDS;
|
|
1303
|
+
if (lossPct < t1) return VEL_COLORS.green;
|
|
1304
|
+
if (lossPct < t22) return VEL_COLORS.yellow;
|
|
1305
|
+
if (lossPct < t32) return VEL_COLORS.orange;
|
|
1306
|
+
return VEL_COLORS.red;
|
|
1307
|
+
}
|
|
1308
|
+
function velocityLossForRep(velocity, best) {
|
|
1309
|
+
if (best <= 0) return 0;
|
|
1310
|
+
return Math.max(0, Math.round((best - velocity) / best * 100));
|
|
1311
|
+
}
|
|
1312
|
+
function classifyBand(velocity, bands) {
|
|
1313
|
+
for (const band of bands) {
|
|
1314
|
+
if (band.max === null || velocity < band.max) return band;
|
|
1315
|
+
}
|
|
1316
|
+
return bands[bands.length - 1];
|
|
1317
|
+
}
|
|
1318
|
+
function bandColor(band) {
|
|
1319
|
+
return bandIdToEffortColor[band.id] ?? VEL_COLORS.green;
|
|
1320
|
+
}
|
|
1321
|
+
function makeBarColorFor(zones) {
|
|
1322
|
+
const hasZones = zones != null && zones.length > 0;
|
|
1323
|
+
return (v) => hasZones ? bandColor(classifyBand(v, zones)) : zoneHexMap[getVelocityZoneColor(v)];
|
|
1324
|
+
}
|
|
1325
|
+
function getLossStyle(loss) {
|
|
1326
|
+
if (loss > 25) return { color: VEL_COLORS.red };
|
|
1327
|
+
if (loss > 20) return { color: VEL_COLORS.orange };
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
var REP_GAP2 = 2;
|
|
1331
|
+
var WIDE_GAP = 8;
|
|
1332
|
+
function deriveDoneVelocities(set) {
|
|
1333
|
+
switch (set.type) {
|
|
1334
|
+
case "drop":
|
|
1335
|
+
return set.subloads.flat();
|
|
1336
|
+
case "myo":
|
|
1337
|
+
return [...set.activation, ...set.clusters.flat()];
|
|
1338
|
+
default:
|
|
1339
|
+
return set.velocities;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function chunkedRepSlots(chunks) {
|
|
1343
|
+
const slots = [];
|
|
1344
|
+
chunks.forEach((chunk) => {
|
|
1345
|
+
chunk.forEach((velocity, ri) => {
|
|
1346
|
+
const first = slots.length === 0;
|
|
1347
|
+
slots.push({ kind: "rep", velocity, leadingGap: first ? 0 : ri === 0 ? WIDE_GAP : REP_GAP2 });
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
return slots;
|
|
1351
|
+
}
|
|
1352
|
+
function buildSlots(set) {
|
|
1353
|
+
switch (set.type) {
|
|
1354
|
+
case "straight": {
|
|
1355
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
|
|
1356
|
+
return Array.from({ length: total }, (_, i) => {
|
|
1357
|
+
const done = i < set.velocities.length;
|
|
1358
|
+
return {
|
|
1359
|
+
kind: done ? "rep" : "todo",
|
|
1360
|
+
velocity: done ? set.velocities[i] : void 0,
|
|
1361
|
+
leadingGap: i === 0 ? 0 : REP_GAP2
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
case "range": {
|
|
1366
|
+
const total = Math.max(set.max, set.velocities.length);
|
|
1367
|
+
return Array.from({ length: total }, (_, i) => {
|
|
1368
|
+
const kind = i < set.velocities.length ? "rep" : i < set.floor ? "todo" : "variable";
|
|
1369
|
+
return {
|
|
1370
|
+
kind,
|
|
1371
|
+
velocity: kind === "rep" ? set.velocities[i] : void 0,
|
|
1372
|
+
leadingGap: i === 0 ? 0 : REP_GAP2
|
|
1373
|
+
};
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
case "amrap": {
|
|
1377
|
+
const reps = set.velocities.map((velocity, i) => ({
|
|
1378
|
+
kind: "rep",
|
|
1379
|
+
velocity,
|
|
1380
|
+
leadingGap: i === 0 ? 0 : REP_GAP2
|
|
1381
|
+
}));
|
|
1382
|
+
reps.push({ kind: "continue", leadingGap: reps.length === 0 ? 0 : REP_GAP2 });
|
|
1383
|
+
return reps;
|
|
1384
|
+
}
|
|
1385
|
+
case "drop":
|
|
1386
|
+
return chunkedRepSlots(set.subloads);
|
|
1387
|
+
case "myo": {
|
|
1388
|
+
const slots = chunkedRepSlots([set.activation, ...set.clusters]);
|
|
1389
|
+
if (set.open) slots.push({ kind: "continue", leadingGap: slots.length === 0 ? 0 : REP_GAP2 });
|
|
1390
|
+
return slots;
|
|
1391
|
+
}
|
|
1392
|
+
case "cluster": {
|
|
1393
|
+
const total = Math.max(set.velocities.length, set.planned ?? set.velocities.length);
|
|
1394
|
+
return Array.from({ length: total }, (_, i) => {
|
|
1395
|
+
const done = i < set.velocities.length;
|
|
1396
|
+
const boundary = i > 0 && i % set.groupSize === 0;
|
|
1397
|
+
return {
|
|
1398
|
+
kind: done ? "rep" : "todo",
|
|
1399
|
+
velocity: done ? set.velocities[i] : void 0,
|
|
1400
|
+
leadingGap: i === 0 ? 0 : boundary ? WIDE_GAP : REP_GAP2
|
|
1401
|
+
};
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function setAccessibilityLabel(set, repCount) {
|
|
1407
|
+
switch (set.type) {
|
|
1408
|
+
case "straight":
|
|
1409
|
+
return `Velocity strip, straight set, ${repCount} reps`;
|
|
1410
|
+
case "range":
|
|
1411
|
+
return `Velocity strip, rep-range set, floor ${set.floor} to ${set.max}, ${repCount} reps done`;
|
|
1412
|
+
case "amrap":
|
|
1413
|
+
return `Velocity strip, AMRAP set, ${repCount} reps and counting`;
|
|
1414
|
+
case "drop":
|
|
1415
|
+
return `Velocity strip, drop set, ${set.subloads.length} loads, ${repCount} reps`;
|
|
1416
|
+
case "myo":
|
|
1417
|
+
return `Velocity strip, myo-reps set, ${set.clusters.length} clusters${set.open ? ", open" : ""}, ${repCount} reps`;
|
|
1418
|
+
case "cluster":
|
|
1419
|
+
return `Velocity strip, cluster set, groups of ${set.groupSize}, ${repCount} reps`;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
var EXPANDED_HEIGHT = 60;
|
|
1423
|
+
var COMPACT_HEIGHT = 8;
|
|
1424
|
+
var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
|
|
1425
|
+
function DashedReferenceLine({
|
|
1426
|
+
anchor,
|
|
1427
|
+
offset,
|
|
1428
|
+
testID
|
|
1429
|
+
}) {
|
|
1430
|
+
const edge = anchor === "top" ? { top: offset } : { bottom: offset };
|
|
1431
|
+
return /* @__PURE__ */ jsx(
|
|
1432
|
+
reactNative.View,
|
|
1433
|
+
{
|
|
1434
|
+
accessibilityElementsHidden: true,
|
|
1435
|
+
style: {
|
|
1436
|
+
position: "absolute",
|
|
1437
|
+
left: 0,
|
|
1438
|
+
right: 0,
|
|
1439
|
+
borderTopWidth: 1,
|
|
1440
|
+
borderStyle: "dashed",
|
|
1441
|
+
borderColor: HERO_REFERENCE_COLOR,
|
|
1442
|
+
pointerEvents: "none",
|
|
1443
|
+
...edge
|
|
1444
|
+
},
|
|
1445
|
+
testID
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
var BAND_LABEL_FONT = "monospace";
|
|
1450
|
+
var VL_LABEL_MIN_PLOT = 65;
|
|
1451
|
+
var VL_SEMANTIC = getSemanticColors("dark");
|
|
1452
|
+
function VelocityLossBands({
|
|
1453
|
+
best,
|
|
1454
|
+
scaleDenom,
|
|
1455
|
+
plotHeight,
|
|
1456
|
+
flip = false
|
|
1457
|
+
}) {
|
|
1458
|
+
if (best <= 0 || scaleDenom <= 0 || plotHeight <= 0) return null;
|
|
1459
|
+
const yOf = (v) => v / scaleDenom * plotHeight;
|
|
1460
|
+
const vl20 = best * 0.8;
|
|
1461
|
+
const vl30 = best * 0.7;
|
|
1462
|
+
const vlFont = Math.round(Math.max(7, Math.min(9, plotHeight * 0.05)));
|
|
1463
|
+
const showLabels = plotHeight >= VL_LABEL_MIN_PLOT;
|
|
1464
|
+
const band = (loV, hiV, color) => /* @__PURE__ */ jsx(
|
|
1465
|
+
reactNative.View,
|
|
1466
|
+
{
|
|
1467
|
+
style: {
|
|
1468
|
+
position: "absolute",
|
|
1469
|
+
left: 0,
|
|
1470
|
+
right: 0,
|
|
1471
|
+
bottom: yOf(loV),
|
|
1472
|
+
height: Math.max(0, yOf(hiV) - yOf(loV)),
|
|
1473
|
+
backgroundColor: color
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
);
|
|
1477
|
+
const threshold = (v, color, label) => /* @__PURE__ */ jsxs(
|
|
1478
|
+
reactNative.View,
|
|
1479
|
+
{
|
|
1480
|
+
style: {
|
|
1481
|
+
position: "absolute",
|
|
1482
|
+
left: 0,
|
|
1483
|
+
right: 0,
|
|
1484
|
+
bottom: yOf(v),
|
|
1485
|
+
// Collapse to zero height so the dashed border lands EXACTLY on `yOf(v)` — the
|
|
1486
|
+
// band edge — instead of floating up half a text-row (the label then centres on
|
|
1487
|
+
// the line, breaking it). Matches DashedReferenceLine's bare-border anchoring.
|
|
1488
|
+
height: 0,
|
|
1489
|
+
flexDirection: "row",
|
|
1490
|
+
alignItems: "center"
|
|
1491
|
+
},
|
|
1492
|
+
children: [
|
|
1493
|
+
/* @__PURE__ */ jsx(reactNative.View, { style: { flex: 9, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } }),
|
|
1494
|
+
showLabels ? /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
|
|
1495
|
+
/* @__PURE__ */ jsx(
|
|
1496
|
+
reactNative.Text,
|
|
1497
|
+
{
|
|
1498
|
+
style: {
|
|
1499
|
+
marginHorizontal: 6,
|
|
1500
|
+
fontSize: vlFont,
|
|
1501
|
+
fontWeight: "800",
|
|
1502
|
+
fontFamily: BAND_LABEL_FONT,
|
|
1503
|
+
color,
|
|
1504
|
+
...flip ? { transform: [{ scaleY: -1 }] } : null
|
|
1505
|
+
},
|
|
1506
|
+
children: label
|
|
1507
|
+
}
|
|
1508
|
+
),
|
|
1509
|
+
/* @__PURE__ */ jsx(reactNative.View, { style: { flex: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: color } })
|
|
1510
|
+
] }) : null
|
|
1511
|
+
]
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
return /* @__PURE__ */ jsxs(
|
|
1515
|
+
reactNative.View,
|
|
1516
|
+
{
|
|
1517
|
+
accessibilityElementsHidden: true,
|
|
1518
|
+
pointerEvents: "none",
|
|
1519
|
+
style: { position: "absolute", left: 0, right: 0, bottom: 0, top: 0 },
|
|
1520
|
+
testID: "velocity-loss-bands",
|
|
1521
|
+
children: [
|
|
1522
|
+
band(0, vl30, alpha(VL_SEMANTIC["status-error"], 0.09)),
|
|
1523
|
+
band(vl30, vl20, alpha(VL_SEMANTIC["status-warning"], 0.08)),
|
|
1524
|
+
threshold(vl20, alpha(VL_SEMANTIC["status-warning"], 0.75), "VL 20%"),
|
|
1525
|
+
threshold(vl30, alpha(VL_SEMANTIC["status-error"], 0.75), "VL 30%")
|
|
1526
|
+
]
|
|
1527
|
+
}
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
function velocityReferenceOverlay(g, showLossBands) {
|
|
1531
|
+
const referencePx = g.best > 0 && g.scaleDenom > 0 ? Math.min(g.plotHeight, g.yOf(g.best)) : 0;
|
|
1532
|
+
return /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
|
|
1533
|
+
showLossBands && /* @__PURE__ */ jsx(
|
|
1534
|
+
VelocityLossBands,
|
|
1535
|
+
{
|
|
1536
|
+
best: g.best,
|
|
1537
|
+
scaleDenom: g.scaleDenom,
|
|
1538
|
+
plotHeight: g.plotHeight,
|
|
1539
|
+
flip: g.flip
|
|
1540
|
+
}
|
|
1541
|
+
),
|
|
1542
|
+
referencePx > 0 && /* @__PURE__ */ jsx(
|
|
1543
|
+
DashedReferenceLine,
|
|
1544
|
+
{
|
|
1545
|
+
anchor: "bottom",
|
|
1546
|
+
offset: referencePx,
|
|
1547
|
+
testID: "velocity-hero-reference"
|
|
1548
|
+
}
|
|
1549
|
+
)
|
|
1550
|
+
] });
|
|
1551
|
+
}
|
|
1552
|
+
var ANIMATION_DURATION = 400;
|
|
1553
|
+
function VelocityStrip({
|
|
1554
|
+
velocities,
|
|
1555
|
+
set,
|
|
1556
|
+
zones,
|
|
1557
|
+
barColor = "loss",
|
|
1558
|
+
showLossBands,
|
|
1559
|
+
orientation = "up",
|
|
1560
|
+
scaleMax,
|
|
1561
|
+
hideBaseline,
|
|
1562
|
+
columnSlots,
|
|
1563
|
+
label,
|
|
1564
|
+
liveRepIndex,
|
|
1565
|
+
expanded = true,
|
|
1566
|
+
onToggle,
|
|
1567
|
+
onRepPress,
|
|
1568
|
+
variant = "expanded",
|
|
1569
|
+
height = EXPANDED_HEIGHT,
|
|
1570
|
+
targetReps,
|
|
1571
|
+
scale = "peak",
|
|
1572
|
+
showNumbers = true,
|
|
1573
|
+
showInfo = true,
|
|
1574
|
+
className,
|
|
1575
|
+
...props
|
|
1576
|
+
}) {
|
|
1577
|
+
const lossBandsOn = showLossBands ?? barColor === "loss";
|
|
1578
|
+
const doneVelocities = set ? deriveDoneVelocities(set) : velocities ?? [];
|
|
1579
|
+
const maxVelocity = Math.max(...doneVelocities, 0);
|
|
1580
|
+
const meanVelocity = calculateMeanVelocity(doneVelocities);
|
|
1581
|
+
const loss = calculateVelocityLoss(doneVelocities);
|
|
1582
|
+
const framed = showNumbers || showInfo;
|
|
1583
|
+
const hasZones = zones != null && zones.length > 0;
|
|
1584
|
+
const zoneColorFor = makeBarColorFor(zones);
|
|
1585
|
+
const barColorFor = (v) => barColor === "loss" ? getVelocityLossColor(velocityLossForRep(v, maxVelocity)) : zoneColorFor(v);
|
|
1586
|
+
const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
|
|
1587
|
+
const [expandProgress] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
|
|
1588
|
+
const [infoOpacity] = react.useState(() => new reactNative.Animated.Value(expanded ? 1 : 0));
|
|
1589
|
+
const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
|
|
1590
|
+
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
|
|
1591
|
+
react.useEffect(() => {
|
|
1592
|
+
if (variant !== "expanded" || !framed) return;
|
|
1593
|
+
reactNative.Animated.parallel([
|
|
1594
|
+
reactNative.Animated.timing(expandProgress, {
|
|
1595
|
+
toValue: expanded ? 1 : 0,
|
|
1596
|
+
duration: ANIMATION_DURATION,
|
|
1597
|
+
easing: ANIMATION_EASING,
|
|
1598
|
+
useNativeDriver: false
|
|
1599
|
+
}),
|
|
1600
|
+
reactNative.Animated.timing(infoOpacity, {
|
|
1601
|
+
toValue: expanded ? 1 : 0,
|
|
1602
|
+
duration: 300,
|
|
1603
|
+
delay: expanded ? 200 : 0,
|
|
1604
|
+
useNativeDriver: false
|
|
1605
|
+
})
|
|
1606
|
+
]).start();
|
|
1607
|
+
}, [expanded, variant, framed, expandProgress, infoOpacity]);
|
|
1608
|
+
if (set == null && velocities == null) return null;
|
|
1609
|
+
const repCount = doneVelocities.length;
|
|
1610
|
+
const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
|
|
1611
|
+
if (variant === "hero") {
|
|
1612
|
+
const heroHeight = height === EXPANDED_HEIGHT && columnSlots == null ? SET_BAR_DEFAULT_HEIGHT : height;
|
|
1613
|
+
const heroSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
|
|
1614
|
+
kind: s.kind,
|
|
1615
|
+
value: s.velocity,
|
|
1616
|
+
leadingGap: s.leadingGap
|
|
1617
|
+
})) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
|
|
1618
|
+
const total = Math.max(repCount, targetReps ?? repCount);
|
|
1619
|
+
const heroLabel = maxVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(maxVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
|
|
1620
|
+
return /* @__PURE__ */ jsx(
|
|
1621
|
+
SetBarChart,
|
|
1622
|
+
{
|
|
1623
|
+
slots: heroSlots,
|
|
1624
|
+
colorFor: barColorFor,
|
|
1625
|
+
height: heroHeight,
|
|
1626
|
+
scale,
|
|
1627
|
+
scaleMax,
|
|
1628
|
+
orientation,
|
|
1629
|
+
liveRepIndex,
|
|
1630
|
+
isNewPeak,
|
|
1631
|
+
targetReps: set || columnSlots ? void 0 : targetReps,
|
|
1632
|
+
label,
|
|
1633
|
+
showValueLabels: true,
|
|
1634
|
+
formatValue: formatVelocity,
|
|
1635
|
+
renderReference: (g) => velocityReferenceOverlay(g, lossBandsOn),
|
|
1636
|
+
hideBaseline: true,
|
|
1637
|
+
testID: "velocity-strip-hero",
|
|
1638
|
+
testIDPrefix: "velocity",
|
|
1639
|
+
accessibilityLabel: heroLabel,
|
|
1640
|
+
className,
|
|
1641
|
+
viewProps: props
|
|
1642
|
+
}
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
if (variant === "compact") {
|
|
1646
|
+
const compactSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
|
|
1647
|
+
kind: s.kind,
|
|
1648
|
+
value: s.velocity,
|
|
1649
|
+
leadingGap: s.leadingGap
|
|
1650
|
+
})) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
|
|
1651
|
+
const compactHeight = height === EXPANDED_HEIGHT ? COMPACT_HEIGHT : height;
|
|
1652
|
+
return /* @__PURE__ */ jsx(
|
|
1653
|
+
SetBarChart,
|
|
1654
|
+
{
|
|
1655
|
+
slots: compactSlots,
|
|
1656
|
+
colorFor: barColorFor,
|
|
1657
|
+
height: compactHeight,
|
|
1658
|
+
scale,
|
|
1659
|
+
scaleMax,
|
|
1660
|
+
orientation,
|
|
1661
|
+
flat: true,
|
|
1662
|
+
barRadius: 2,
|
|
1663
|
+
cornerStyle: "all",
|
|
1664
|
+
targetReps: set || columnSlots ? void 0 : targetReps,
|
|
1665
|
+
label,
|
|
1666
|
+
hideBaseline: true,
|
|
1667
|
+
testID: "velocity-strip-compact",
|
|
1668
|
+
testIDPrefix: "velocity",
|
|
1669
|
+
accessibilityLabel: miniLabel,
|
|
1670
|
+
className,
|
|
1671
|
+
viewProps: props
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
if (!framed) {
|
|
1676
|
+
const spotlightSlots = columnSlots ?? (set ? buildSlots(set).map((s) => ({
|
|
1677
|
+
kind: s.kind,
|
|
1678
|
+
value: s.velocity,
|
|
1679
|
+
leadingGap: s.leadingGap
|
|
1680
|
+
})) : doneVelocities.map((v) => ({ kind: "rep", value: v })));
|
|
1681
|
+
return /* @__PURE__ */ jsx(
|
|
1682
|
+
SetBarChart,
|
|
1683
|
+
{
|
|
1684
|
+
slots: spotlightSlots,
|
|
1685
|
+
colorFor: barColorFor,
|
|
1686
|
+
height,
|
|
1687
|
+
scale,
|
|
1688
|
+
scaleMax,
|
|
1689
|
+
orientation,
|
|
1690
|
+
liveRepIndex,
|
|
1691
|
+
isNewPeak,
|
|
1692
|
+
barRadius: 2,
|
|
1693
|
+
cornerStyle: "top",
|
|
1694
|
+
targetReps: set ? void 0 : targetReps,
|
|
1695
|
+
label,
|
|
1696
|
+
hideBaseline: true,
|
|
1697
|
+
testID: "velocity-strip-spotlight",
|
|
1698
|
+
testIDPrefix: "velocity",
|
|
1699
|
+
accessibilityLabel: miniLabel,
|
|
1700
|
+
className,
|
|
1701
|
+
viewProps: props
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
const stripLabel = set ? `${setAccessibilityLabel(set, repCount)}, tap to ${expanded ? "collapse" : "expand"}` : `Velocity chart for set, ${repCount} reps, tap to ${expanded ? "collapse" : "expand"}`;
|
|
1706
|
+
const hasInteractiveContainer = onToggle != null;
|
|
1707
|
+
const hasInteractiveReps = onRepPress != null && expanded;
|
|
1708
|
+
const framedSlots = set ? buildSlots(set).map((sl) => ({
|
|
1709
|
+
kind: sl.kind,
|
|
1710
|
+
value: sl.velocity,
|
|
1711
|
+
leadingGap: sl.leadingGap
|
|
1712
|
+
})) : doneVelocities.map((v) => ({ kind: "rep", value: v }));
|
|
1713
|
+
const needsBarOverlay = showNumbers || onRepPress != null;
|
|
1714
|
+
const renderFramedBarOverlay = needsBarOverlay ? (repIndex, value) => /* @__PURE__ */ jsxs(jsxRuntime.Fragment, { children: [
|
|
1715
|
+
showNumbers && /* @__PURE__ */ jsx(
|
|
1716
|
+
reactNative.Animated.View,
|
|
1717
|
+
{
|
|
1718
|
+
style: {
|
|
1719
|
+
opacity: expandProgress,
|
|
1720
|
+
position: "absolute",
|
|
1721
|
+
top: -13,
|
|
1722
|
+
left: 0,
|
|
1723
|
+
right: 0,
|
|
1724
|
+
alignItems: "center"
|
|
1725
|
+
},
|
|
1726
|
+
accessibilityElementsHidden: true,
|
|
1727
|
+
pointerEvents: "none",
|
|
1728
|
+
children: /* @__PURE__ */ jsx(
|
|
1729
|
+
reactNative.Text,
|
|
1730
|
+
{
|
|
1731
|
+
className: "text-text-secondary",
|
|
1732
|
+
style: { fontSize: 8, fontWeight: "600" },
|
|
1733
|
+
testID: `velocity-label-${repIndex}`,
|
|
1734
|
+
children: formatVelocity(value)
|
|
1735
|
+
}
|
|
1736
|
+
)
|
|
1737
|
+
}
|
|
1738
|
+
),
|
|
1739
|
+
onRepPress && expanded && /* @__PURE__ */ jsx(
|
|
1740
|
+
reactNative.Pressable,
|
|
1741
|
+
{
|
|
1742
|
+
style: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 },
|
|
1743
|
+
onPress: () => onRepPress(repIndex, value),
|
|
1744
|
+
accessibilityRole: "button",
|
|
1745
|
+
accessibilityLabel: `Rep ${repIndex + 1}: ${formatVelocity(value)} meters per second, tap for details`,
|
|
1746
|
+
testID: `velocity-bar-pressable-${repIndex}`
|
|
1747
|
+
}
|
|
1748
|
+
)
|
|
1749
|
+
] }) : void 0;
|
|
1750
|
+
const stripContent = /* @__PURE__ */ jsxs(
|
|
1751
|
+
reactNative.Animated.View,
|
|
1752
|
+
{
|
|
1753
|
+
className: [className, "bg-surface-raised"].filter(Boolean).join(" "),
|
|
1754
|
+
style: { width: "100%", borderRadius: 6, paddingTop: 16, paddingBottom: showInfo ? 8 : 4 },
|
|
1755
|
+
accessibilityRole: hasInteractiveContainer || hasInteractiveReps ? "none" : "button",
|
|
1756
|
+
accessibilityLabel: hasInteractiveContainer || hasInteractiveReps ? void 0 : stripLabel,
|
|
1757
|
+
testID: "velocity-strip",
|
|
1758
|
+
...props,
|
|
1759
|
+
children: [
|
|
1760
|
+
/* @__PURE__ */ jsx(
|
|
1761
|
+
SetBarChart,
|
|
1762
|
+
{
|
|
1763
|
+
slots: framedSlots,
|
|
1764
|
+
colorFor: barColorFor,
|
|
1765
|
+
height,
|
|
1766
|
+
scale,
|
|
1767
|
+
scaleMax,
|
|
1768
|
+
expandProgress,
|
|
1769
|
+
renderBarOverlay: renderFramedBarOverlay,
|
|
1770
|
+
targetReps: set ? void 0 : targetReps,
|
|
1771
|
+
barRadius: 2,
|
|
1772
|
+
cornerStyle: "top",
|
|
1773
|
+
hideBaseline: true,
|
|
1774
|
+
testIDPrefix: "velocity"
|
|
1775
|
+
}
|
|
1776
|
+
),
|
|
1777
|
+
expanded && showInfo && /* @__PURE__ */ jsxs(
|
|
1778
|
+
reactNative.Animated.View,
|
|
1779
|
+
{
|
|
1780
|
+
style: {
|
|
1781
|
+
flexDirection: "row",
|
|
1782
|
+
justifyContent: "space-between",
|
|
1783
|
+
opacity: infoOpacity,
|
|
1784
|
+
marginTop: 6,
|
|
1785
|
+
paddingHorizontal: 6
|
|
1786
|
+
},
|
|
1787
|
+
testID: "velocity-info-row",
|
|
1788
|
+
children: [
|
|
1789
|
+
/* @__PURE__ */ jsxs(
|
|
1790
|
+
reactNative.Text,
|
|
1791
|
+
{
|
|
1792
|
+
className: "text-text-secondary",
|
|
1793
|
+
style: { fontSize: 10, fontFamily: "Inter, sans-serif" },
|
|
1794
|
+
children: [
|
|
1795
|
+
meanZone,
|
|
1796
|
+
" ",
|
|
1797
|
+
"\xB7",
|
|
1798
|
+
" ",
|
|
1799
|
+
formatVelocity(meanVelocity),
|
|
1800
|
+
" m/s"
|
|
1801
|
+
]
|
|
1802
|
+
}
|
|
1803
|
+
),
|
|
1804
|
+
/* @__PURE__ */ jsxs(
|
|
1805
|
+
reactNative.Text,
|
|
1806
|
+
{
|
|
1807
|
+
className: "text-text-secondary",
|
|
1808
|
+
style: { fontSize: 10, fontFamily: "Inter, sans-serif", ...getLossStyle(loss) },
|
|
1809
|
+
children: [
|
|
1810
|
+
"Loss: ",
|
|
1811
|
+
loss,
|
|
1812
|
+
"%"
|
|
1813
|
+
]
|
|
1814
|
+
}
|
|
1815
|
+
)
|
|
1816
|
+
]
|
|
1817
|
+
}
|
|
1818
|
+
)
|
|
1819
|
+
]
|
|
1820
|
+
}
|
|
1821
|
+
);
|
|
1822
|
+
if (onToggle) {
|
|
1823
|
+
return /* @__PURE__ */ jsx(
|
|
1824
|
+
reactNative.Pressable,
|
|
1825
|
+
{
|
|
1826
|
+
onPress: onToggle,
|
|
1827
|
+
accessibilityRole: "button",
|
|
1828
|
+
accessibilityLabel: stripLabel,
|
|
1829
|
+
testID: "velocity-strip-pressable",
|
|
1830
|
+
children: stripContent
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
return stripContent;
|
|
1835
|
+
}
|
|
1836
|
+
function cn(...inputs) {
|
|
1837
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/components/custom/Workout/PlaceholderStrip.tsx
|
|
1841
|
+
function SingleStrip({
|
|
1842
|
+
width,
|
|
1843
|
+
className,
|
|
1844
|
+
...props
|
|
1845
|
+
}) {
|
|
1846
|
+
return /* @__PURE__ */ jsx(
|
|
1847
|
+
reactNative.View,
|
|
1848
|
+
{
|
|
1849
|
+
className,
|
|
1850
|
+
style: [
|
|
1851
|
+
{
|
|
1852
|
+
height: 3,
|
|
1853
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
1854
|
+
borderRadius: 2,
|
|
1855
|
+
opacity: 0.5
|
|
1856
|
+
},
|
|
1857
|
+
width != null ? { width } : { flex: 1 }
|
|
1858
|
+
],
|
|
1859
|
+
accessibilityRole: "image",
|
|
1860
|
+
accessibilityLabel: "Planned set, not yet completed",
|
|
1861
|
+
testID: "placeholder-strip",
|
|
1862
|
+
...props
|
|
1863
|
+
}
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
function SegmentedStrip({
|
|
1867
|
+
segments = 3,
|
|
1868
|
+
width,
|
|
1869
|
+
className,
|
|
1870
|
+
...props
|
|
1871
|
+
}) {
|
|
1872
|
+
return /* @__PURE__ */ jsx(
|
|
1873
|
+
reactNative.View,
|
|
1874
|
+
{
|
|
1875
|
+
className: cn("flex-row", className),
|
|
1876
|
+
style: [
|
|
1877
|
+
{ height: 3, gap: 2, opacity: 0.5 },
|
|
1878
|
+
width != null ? { width } : { flex: 1 }
|
|
1879
|
+
],
|
|
1880
|
+
accessibilityRole: "image",
|
|
1881
|
+
accessibilityLabel: "Planned set, not yet completed",
|
|
1882
|
+
testID: "placeholder-strip",
|
|
1883
|
+
...props,
|
|
1884
|
+
children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
|
|
1885
|
+
reactNative.View,
|
|
1886
|
+
{
|
|
1887
|
+
style: {
|
|
1888
|
+
flex: 1,
|
|
1889
|
+
height: 3,
|
|
1890
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
1891
|
+
borderRadius: 1,
|
|
1892
|
+
minWidth: 4
|
|
1893
|
+
},
|
|
1894
|
+
accessibilityElementsHidden: true,
|
|
1895
|
+
testID: "placeholder-segment"
|
|
1896
|
+
},
|
|
1897
|
+
i
|
|
1898
|
+
))
|
|
1899
|
+
}
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
function PlaceholderStrip({
|
|
1903
|
+
mode = "single",
|
|
1904
|
+
segments,
|
|
1905
|
+
...props
|
|
1906
|
+
}) {
|
|
1907
|
+
if (mode === "segmented") {
|
|
1908
|
+
return /* @__PURE__ */ jsx(SegmentedStrip, { segments, ...props });
|
|
1909
|
+
}
|
|
1910
|
+
return /* @__PURE__ */ jsx(SingleStrip, { ...props });
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/components/icons/SvgIcon.tsx
|
|
1914
|
+
function SvgIcon({
|
|
1915
|
+
size = 24,
|
|
1916
|
+
color = "currentColor",
|
|
1917
|
+
strokeWidth = 2,
|
|
1918
|
+
fill = "none",
|
|
1919
|
+
title,
|
|
1920
|
+
children
|
|
1921
|
+
}) {
|
|
1922
|
+
return /* @__PURE__ */ jsxs(
|
|
1923
|
+
"svg",
|
|
1924
|
+
{
|
|
1925
|
+
width: size,
|
|
1926
|
+
height: size,
|
|
1927
|
+
viewBox: "0 0 24 24",
|
|
1928
|
+
fill,
|
|
1929
|
+
stroke: color,
|
|
1930
|
+
strokeWidth,
|
|
1931
|
+
strokeLinecap: "round",
|
|
1932
|
+
strokeLinejoin: "round",
|
|
1933
|
+
...title ? { role: "img", "aria-label": title } : { "aria-hidden": true },
|
|
1934
|
+
children: [
|
|
1935
|
+
title ? /* @__PURE__ */ jsx("title", { children: title }) : null,
|
|
1936
|
+
children
|
|
1937
|
+
]
|
|
1938
|
+
}
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/components/icons/icons.tsx
|
|
1943
|
+
function StarIcon(props) {
|
|
1944
|
+
return /* @__PURE__ */ jsx(SvgIcon, { ...props, 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" }) });
|
|
1945
|
+
}
|
|
1946
|
+
function ScaleIcon(props) {
|
|
1947
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1948
|
+
/* @__PURE__ */ jsx("path", { d: "m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
|
|
1949
|
+
/* @__PURE__ */ jsx("path", { d: "m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" }),
|
|
1950
|
+
/* @__PURE__ */ jsx("path", { d: "M7 21h10" }),
|
|
1951
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3v18" }),
|
|
1952
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" })
|
|
1953
|
+
] });
|
|
1954
|
+
}
|
|
1955
|
+
function AlertTriangleIcon(props) {
|
|
1956
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1957
|
+
/* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
|
|
1958
|
+
/* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
|
|
1959
|
+
/* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
|
|
1960
|
+
] });
|
|
1961
|
+
}
|
|
1962
|
+
function TrendingDownIcon(props) {
|
|
1963
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1964
|
+
/* @__PURE__ */ jsx("path", { d: "M16 17h6v-6" }),
|
|
1965
|
+
/* @__PURE__ */ jsx("path", { d: "m22 17-8.5-8.5-5 5L2 7" })
|
|
1966
|
+
] });
|
|
1967
|
+
}
|
|
1968
|
+
function CircleSlashIcon(props) {
|
|
1969
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1970
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1971
|
+
/* @__PURE__ */ jsx("path", { d: "m9 15 6-6" })
|
|
1972
|
+
] });
|
|
1973
|
+
}
|
|
1974
|
+
function AwardIcon(props) {
|
|
1975
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1976
|
+
/* @__PURE__ */ jsx("path", { d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" }),
|
|
1977
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "6" })
|
|
1978
|
+
] });
|
|
1979
|
+
}
|
|
1980
|
+
function InfoIcon(props) {
|
|
1981
|
+
return /* @__PURE__ */ jsxs(SvgIcon, { ...props, children: [
|
|
1982
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1983
|
+
/* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
|
|
1984
|
+
/* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
|
|
1985
|
+
] });
|
|
1690
1986
|
}
|
|
1691
|
-
|
|
1692
|
-
// src/components/custom/Workout/BaseBadge.tsx
|
|
1693
1987
|
var t = getSemanticColors("dark");
|
|
1694
1988
|
var baseBadgeSizeConfig = {
|
|
1695
1989
|
sm: { fontSize: 9, paddingH: 6, paddingV: 2, iconSize: 10 },
|
|
@@ -1804,565 +2098,257 @@ function PrBadge({
|
|
|
1804
2098
|
easing: reactNative.Easing.bezier(0.34, 1.56, 0.64, 1),
|
|
1805
2099
|
useNativeDriver: true
|
|
1806
2100
|
})
|
|
1807
|
-
]).start();
|
|
1808
|
-
}, [animate, scale, opacity]);
|
|
1809
|
-
const badge = compact ? /* @__PURE__ */ jsx(
|
|
1810
|
-
reactNative.View,
|
|
1811
|
-
{
|
|
1812
|
-
className,
|
|
1813
|
-
accessibilityRole: "image",
|
|
1814
|
-
accessibilityLabel: `Personal record: ${resolvedLabel}`,
|
|
1815
|
-
testID: "pr-badge-star",
|
|
1816
|
-
...props,
|
|
1817
|
-
children: /* @__PURE__ */ jsx(StarIcon, { size: 14, color: BRAND_PRIMARY, fill: BRAND_PRIMARY, strokeWidth: 2 })
|
|
1818
|
-
}
|
|
1819
|
-
) : /* @__PURE__ */ jsx(
|
|
1820
|
-
BaseBadge,
|
|
1821
|
-
{
|
|
1822
|
-
variant: "pr",
|
|
1823
|
-
className,
|
|
1824
|
-
accessibilityLabel: `Personal record: ${resolvedLabel}`,
|
|
1825
|
-
...props,
|
|
1826
|
-
children: /* @__PURE__ */ jsxs(
|
|
1827
|
-
reactNative.Text,
|
|
1828
|
-
{
|
|
1829
|
-
style: {
|
|
1830
|
-
fontSize: 12,
|
|
1831
|
-
color: BRAND_PRIMARY,
|
|
1832
|
-
fontWeight: "700",
|
|
1833
|
-
fontFamily: "Inter, sans-serif"
|
|
1834
|
-
},
|
|
1835
|
-
children: [
|
|
1836
|
-
"\u2605",
|
|
1837
|
-
" ",
|
|
1838
|
-
resolvedLabel
|
|
1839
|
-
]
|
|
1840
|
-
}
|
|
1841
|
-
)
|
|
1842
|
-
}
|
|
1843
|
-
);
|
|
1844
|
-
if (animate) {
|
|
1845
|
-
return /* @__PURE__ */ jsx(
|
|
1846
|
-
reactNative.Animated.View,
|
|
1847
|
-
{
|
|
1848
|
-
style: { transform: [{ scale }], opacity },
|
|
1849
|
-
testID: "pr-badge-animated",
|
|
1850
|
-
children: badge
|
|
1851
|
-
}
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
return badge;
|
|
1855
|
-
}
|
|
1856
|
-
var DARK = getSemanticColors("dark");
|
|
1857
|
-
var TEXT_ACTIVE = DARK["text-primary"];
|
|
1858
|
-
var TEXT_MUTED = DARK["text-secondary"];
|
|
1859
|
-
var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
|
|
1860
|
-
var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
|
|
1861
|
-
var typeBadge = {
|
|
1862
|
-
fontSize: 10,
|
|
1863
|
-
fontWeight: "700",
|
|
1864
|
-
fontFamily: "Inter, sans-serif",
|
|
1865
|
-
color: WORKOUT_TOKENS.scale.orange,
|
|
1866
|
-
backgroundColor: "rgba(255,165,2,0.12)",
|
|
1867
|
-
paddingVertical: 1,
|
|
1868
|
-
paddingHorizontal: 5,
|
|
1869
|
-
borderRadius: 3,
|
|
1870
|
-
overflow: "hidden"
|
|
1871
|
-
};
|
|
1872
|
-
function displayReps(set) {
|
|
1873
|
-
return set.state === "done" ? set.reps : set.target.reps;
|
|
1874
|
-
}
|
|
1875
|
-
function displayWeight(set) {
|
|
1876
|
-
return set.state === "done" ? set.weight : set.target.weight;
|
|
1877
|
-
}
|
|
1878
|
-
function plannedReps(set) {
|
|
1879
|
-
if (set.state === "done") return set.velocities.length;
|
|
1880
|
-
if (set.state === "live") return set.target.reps;
|
|
1881
|
-
return set.planned ?? set.target.reps;
|
|
1882
|
-
}
|
|
1883
|
-
function accessibilityLabel(set) {
|
|
1884
|
-
const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
|
|
1885
|
-
if (set.state === "live") return `${base}, in progress`;
|
|
1886
|
-
if (set.state === "todo") return `${base}, upcoming`;
|
|
1887
|
-
return base;
|
|
1888
|
-
}
|
|
1889
|
-
function Cell({
|
|
1890
|
-
width,
|
|
1891
|
-
children,
|
|
1892
|
-
testID
|
|
1893
|
-
}) {
|
|
1894
|
-
return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
|
|
1895
|
-
}
|
|
1896
|
-
function RowStrip({ set }) {
|
|
1897
|
-
const zones = set.velocityZones;
|
|
1898
|
-
if (set.state === "live") {
|
|
1899
|
-
return /* @__PURE__ */ jsx(
|
|
1900
|
-
VelocityStrip,
|
|
1901
|
-
{
|
|
1902
|
-
variant: "expanded",
|
|
1903
|
-
showNumbers: false,
|
|
1904
|
-
showInfo: false,
|
|
1905
|
-
height: 24,
|
|
1906
|
-
scale: "fixed",
|
|
1907
|
-
set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
|
|
1908
|
-
zones
|
|
1909
|
-
}
|
|
1910
|
-
);
|
|
1911
|
-
}
|
|
1912
|
-
const velocities = set.state === "done" ? set.velocities : [];
|
|
1913
|
-
return /* @__PURE__ */ jsx(
|
|
1914
|
-
VelocityStrip,
|
|
1915
|
-
{
|
|
1916
|
-
variant: "mini",
|
|
1917
|
-
set: { type: "straight", velocities, planned: plannedReps(set) },
|
|
1918
|
-
zones
|
|
1919
|
-
}
|
|
1920
|
-
);
|
|
1921
|
-
}
|
|
1922
|
-
function SetRow(set) {
|
|
1923
|
-
const live = set.state === "live";
|
|
1924
|
-
const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
|
|
1925
|
-
const rpe = set.state === "todo" ? null : set.rpe;
|
|
1926
|
-
return /* @__PURE__ */ jsxs(
|
|
1927
|
-
reactNative.View,
|
|
1928
|
-
{
|
|
1929
|
-
style: { paddingVertical: 6, paddingHorizontal: 8 },
|
|
1930
|
-
accessibilityLabel: accessibilityLabel(set),
|
|
1931
|
-
testID: "set-row",
|
|
1932
|
-
children: [
|
|
1933
|
-
/* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
|
|
1934
|
-
/* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
|
|
1935
|
-
/* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
|
|
1936
|
-
/* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
|
|
1937
|
-
/* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
|
|
1938
|
-
] }),
|
|
1939
|
-
/* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
|
|
1940
|
-
]
|
|
1941
|
-
}
|
|
1942
|
-
);
|
|
1943
|
-
}
|
|
1944
|
-
var PREV_COLUMN = { label: "PREV", width: void 0 };
|
|
1945
|
-
function buildColumns(unit, showPrevious) {
|
|
1946
|
-
return [
|
|
1947
|
-
{ label: "SET", width: 36 },
|
|
1948
|
-
...showPrevious ? [PREV_COLUMN] : [],
|
|
1949
|
-
{ label: "REPS", width: 44 },
|
|
1950
|
-
{ label: unit.toUpperCase(), width: 56 },
|
|
1951
|
-
{ label: "RPE", width: 36 }
|
|
1952
|
-
];
|
|
1953
|
-
}
|
|
1954
|
-
function SetTableHeader({
|
|
1955
|
-
unit = "lbs",
|
|
1956
|
-
showPrevious = true,
|
|
1957
|
-
testID = "table-header"
|
|
1958
|
-
}) {
|
|
1959
|
-
const columns = buildColumns(unit, showPrevious);
|
|
1960
|
-
return /* @__PURE__ */ jsx(
|
|
1961
|
-
reactNative.View,
|
|
1962
|
-
{
|
|
1963
|
-
className: "flex-row",
|
|
1964
|
-
style: {
|
|
1965
|
-
paddingVertical: 8,
|
|
1966
|
-
paddingHorizontal: 8,
|
|
1967
|
-
paddingBottom: 4,
|
|
1968
|
-
...showPrevious ? {} : { justifyContent: "space-between" }
|
|
1969
|
-
},
|
|
1970
|
-
testID,
|
|
1971
|
-
children: columns.map(({ label, width }) => {
|
|
1972
|
-
const isFlex = width === void 0;
|
|
1973
|
-
return /* @__PURE__ */ jsx(
|
|
1974
|
-
reactNative.View,
|
|
1975
|
-
{
|
|
1976
|
-
style: {
|
|
1977
|
-
...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
|
|
1978
|
-
alignItems: "center",
|
|
1979
|
-
justifyContent: "center",
|
|
1980
|
-
gap: 8
|
|
1981
|
-
},
|
|
1982
|
-
...isFlex ? { className: "flex-1" } : {},
|
|
1983
|
-
children: /* @__PURE__ */ jsx(
|
|
1984
|
-
reactNative.Text,
|
|
1985
|
-
{
|
|
1986
|
-
className: "text-text-tertiary",
|
|
1987
|
-
style: {
|
|
1988
|
-
fontSize: 10,
|
|
1989
|
-
fontWeight: "600",
|
|
1990
|
-
fontFamily: "Inter, sans-serif",
|
|
1991
|
-
textTransform: "uppercase",
|
|
1992
|
-
letterSpacing: 0.8
|
|
1993
|
-
},
|
|
1994
|
-
children: label
|
|
1995
|
-
}
|
|
1996
|
-
)
|
|
1997
|
-
},
|
|
1998
|
-
label
|
|
1999
|
-
);
|
|
2000
|
-
})
|
|
2001
|
-
}
|
|
2002
|
-
);
|
|
2003
|
-
}
|
|
2004
|
-
var METRIC_FONT = "Inter, sans-serif";
|
|
2005
|
-
function MetricCell({ children, color, fontSize = 11 }) {
|
|
2006
|
-
return /* @__PURE__ */ jsx(
|
|
2007
|
-
reactNative.Text,
|
|
2008
|
-
{
|
|
2009
|
-
style: {
|
|
2010
|
-
fontFamily: METRIC_FONT,
|
|
2011
|
-
fontSize,
|
|
2012
|
-
color,
|
|
2013
|
-
fontWeight: "600",
|
|
2014
|
-
letterSpacing: 1
|
|
2015
|
-
},
|
|
2016
|
-
children
|
|
2017
|
-
}
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// src/components/custom/Workout/SetsRepsLoad.tsx
|
|
2022
|
-
var t2 = getSemanticColors("dark");
|
|
2023
|
-
var TIMES = "\xD7";
|
|
2024
|
-
var AT = "@";
|
|
2025
|
-
function SetsRepsLoad({
|
|
2026
|
-
sets,
|
|
2027
|
-
reps,
|
|
2028
|
-
load,
|
|
2029
|
-
unit = "lb",
|
|
2030
|
-
fontSize,
|
|
2031
|
-
className,
|
|
2032
|
-
...props
|
|
2033
|
-
}) {
|
|
2034
|
-
const value = t2["text-primary"];
|
|
2035
|
-
const sep = t2["text-tertiary"];
|
|
2036
|
-
return /* @__PURE__ */ jsxs(
|
|
2101
|
+
]).start();
|
|
2102
|
+
}, [animate, scale, opacity]);
|
|
2103
|
+
const badge = compact ? /* @__PURE__ */ jsx(
|
|
2037
2104
|
reactNative.View,
|
|
2038
2105
|
{
|
|
2039
2106
|
className,
|
|
2040
|
-
|
|
2041
|
-
accessibilityLabel:
|
|
2042
|
-
testID: "
|
|
2107
|
+
accessibilityRole: "image",
|
|
2108
|
+
accessibilityLabel: `Personal record: ${resolvedLabel}`,
|
|
2109
|
+
testID: "pr-badge-star",
|
|
2043
2110
|
...props,
|
|
2044
|
-
children:
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2111
|
+
children: /* @__PURE__ */ jsx(StarIcon, { size: 14, color: BRAND_PRIMARY, fill: BRAND_PRIMARY, strokeWidth: 2 })
|
|
2112
|
+
}
|
|
2113
|
+
) : /* @__PURE__ */ jsx(
|
|
2114
|
+
BaseBadge,
|
|
2115
|
+
{
|
|
2116
|
+
variant: "pr",
|
|
2117
|
+
className,
|
|
2118
|
+
accessibilityLabel: `Personal record: ${resolvedLabel}`,
|
|
2119
|
+
...props,
|
|
2120
|
+
children: /* @__PURE__ */ jsxs(
|
|
2121
|
+
reactNative.Text,
|
|
2122
|
+
{
|
|
2123
|
+
style: {
|
|
2124
|
+
fontSize: 12,
|
|
2125
|
+
color: BRAND_PRIMARY,
|
|
2126
|
+
fontWeight: "700",
|
|
2127
|
+
fontFamily: "Inter, sans-serif"
|
|
2128
|
+
},
|
|
2129
|
+
children: [
|
|
2130
|
+
"\u2605",
|
|
2131
|
+
" ",
|
|
2132
|
+
resolvedLabel
|
|
2133
|
+
]
|
|
2134
|
+
}
|
|
2135
|
+
)
|
|
2052
2136
|
}
|
|
2053
2137
|
);
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
r = parseInt(cleaned.slice(0, 2), 16);
|
|
2064
|
-
g = parseInt(cleaned.slice(2, 4), 16);
|
|
2065
|
-
b = parseInt(cleaned.slice(4, 6), 16);
|
|
2066
|
-
} else {
|
|
2067
|
-
return null;
|
|
2138
|
+
if (animate) {
|
|
2139
|
+
return /* @__PURE__ */ jsx(
|
|
2140
|
+
reactNative.Animated.View,
|
|
2141
|
+
{
|
|
2142
|
+
style: { transform: [{ scale }], opacity },
|
|
2143
|
+
testID: "pr-badge-animated",
|
|
2144
|
+
children: badge
|
|
2145
|
+
}
|
|
2146
|
+
);
|
|
2068
2147
|
}
|
|
2069
|
-
return
|
|
2148
|
+
return badge;
|
|
2070
2149
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
}
|
|
2090
|
-
if (h < 0) h += 360;
|
|
2091
|
-
return { h, s, v };
|
|
2150
|
+
var DARK = getSemanticColors("dark");
|
|
2151
|
+
var TEXT_ACTIVE = DARK["text-primary"];
|
|
2152
|
+
var TEXT_MUTED = DARK["text-secondary"];
|
|
2153
|
+
var COL = { set: 36, reps: 44, load: 56, rpe: 36 };
|
|
2154
|
+
var cellText = { fontFamily: "Inter, sans-serif", fontSize: 13, fontWeight: "600" };
|
|
2155
|
+
var typeBadge = {
|
|
2156
|
+
fontSize: 10,
|
|
2157
|
+
fontWeight: "700",
|
|
2158
|
+
fontFamily: "Inter, sans-serif",
|
|
2159
|
+
color: WORKOUT_TOKENS.scale.orange,
|
|
2160
|
+
backgroundColor: "rgba(255,165,2,0.12)",
|
|
2161
|
+
paddingVertical: 1,
|
|
2162
|
+
paddingHorizontal: 5,
|
|
2163
|
+
borderRadius: 3,
|
|
2164
|
+
overflow: "hidden"
|
|
2165
|
+
};
|
|
2166
|
+
function displayReps(set) {
|
|
2167
|
+
return set.state === "done" ? set.reps : set.target.reps;
|
|
2092
2168
|
}
|
|
2093
|
-
function
|
|
2094
|
-
|
|
2095
|
-
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
2096
|
-
const m = v - c;
|
|
2097
|
-
let rPrime, gPrime, bPrime;
|
|
2098
|
-
if (h >= 0 && h < 60) {
|
|
2099
|
-
rPrime = c;
|
|
2100
|
-
gPrime = x;
|
|
2101
|
-
bPrime = 0;
|
|
2102
|
-
} else if (h >= 60 && h < 120) {
|
|
2103
|
-
rPrime = x;
|
|
2104
|
-
gPrime = c;
|
|
2105
|
-
bPrime = 0;
|
|
2106
|
-
} else if (h >= 120 && h < 180) {
|
|
2107
|
-
rPrime = 0;
|
|
2108
|
-
gPrime = c;
|
|
2109
|
-
bPrime = x;
|
|
2110
|
-
} else if (h >= 180 && h < 240) {
|
|
2111
|
-
rPrime = 0;
|
|
2112
|
-
gPrime = x;
|
|
2113
|
-
bPrime = c;
|
|
2114
|
-
} else if (h >= 240 && h < 300) {
|
|
2115
|
-
rPrime = x;
|
|
2116
|
-
gPrime = 0;
|
|
2117
|
-
bPrime = c;
|
|
2118
|
-
} else {
|
|
2119
|
-
rPrime = c;
|
|
2120
|
-
gPrime = 0;
|
|
2121
|
-
bPrime = x;
|
|
2122
|
-
}
|
|
2123
|
-
return {
|
|
2124
|
-
r: Math.round((rPrime + m) * 255),
|
|
2125
|
-
g: Math.round((gPrime + m) * 255),
|
|
2126
|
-
b: Math.round((bPrime + m) * 255)
|
|
2127
|
-
};
|
|
2169
|
+
function displayWeight(set) {
|
|
2170
|
+
return set.state === "done" ? set.weight : set.target.weight;
|
|
2128
2171
|
}
|
|
2129
|
-
function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
};
|
|
2134
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
2172
|
+
function plannedReps(set) {
|
|
2173
|
+
if (set.state === "done") return set.velocities.length;
|
|
2174
|
+
if (set.state === "live") return set.target.reps;
|
|
2175
|
+
return set.planned ?? set.target.reps;
|
|
2135
2176
|
}
|
|
2136
|
-
function
|
|
2137
|
-
const
|
|
2138
|
-
if (
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
const newRgb = hsvToRgb(hsv.h, hsv.s, newV);
|
|
2142
|
-
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
2177
|
+
function accessibilityLabel(set) {
|
|
2178
|
+
const base = `Set ${set.setNumber}: ${displayReps(set)} reps at ${roundWeight(displayWeight(set))} ${set.unit}`;
|
|
2179
|
+
if (set.state === "live") return `${base}, in progress`;
|
|
2180
|
+
if (set.state === "todo") return `${base}, upcoming`;
|
|
2181
|
+
return base;
|
|
2143
2182
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
darker: "rgba(0, 0, 0, 0.6)"
|
|
2151
|
-
},
|
|
2152
|
-
// For neutral dark surfaces (~#1F2937)
|
|
2153
|
-
neutralDark: {
|
|
2154
|
-
dark: "rgba(0, 0, 0, 0.4)",
|
|
2155
|
-
light: "rgba(255, 255, 255, 0.08)",
|
|
2156
|
-
darker: "rgba(0, 0, 0, 0.5)"
|
|
2157
|
-
},
|
|
2158
|
-
// For light surfaces (white/light gray backgrounds)
|
|
2159
|
-
neutralLight: {
|
|
2160
|
-
dark: "rgba(0, 0, 0, 0.15)",
|
|
2161
|
-
// Visible dark shadow for depth on light backgrounds
|
|
2162
|
-
light: "rgba(255, 255, 255, 1)",
|
|
2163
|
-
// Bright highlight for raised effect (visible on light gray)
|
|
2164
|
-
darker: "rgba(0, 0, 0, 0.2)"
|
|
2165
|
-
// Deeper shadow for pressed states
|
|
2166
|
-
}
|
|
2167
|
-
};
|
|
2168
|
-
var intensityConfig = {
|
|
2169
|
-
subtle: { offset: 1, blur: 2 },
|
|
2170
|
-
// very light, barely visible
|
|
2171
|
-
medium: { offset: 2, blur: 4 },
|
|
2172
|
-
// noticeable but restrained
|
|
2173
|
-
strong: { offset: 2, blur: 5 }
|
|
2174
|
-
// slightly deeper blur, same offset as medium
|
|
2175
|
-
};
|
|
2176
|
-
function createRaisedShadow(surface = "charcoal", intensity = "medium") {
|
|
2177
|
-
const colors = shadowColors[surface];
|
|
2178
|
-
const { offset, blur } = intensityConfig[intensity];
|
|
2179
|
-
const rimBlur = 1;
|
|
2180
|
-
return reactNative.Platform.select({
|
|
2181
|
-
web: {
|
|
2182
|
-
boxShadow: `${offset}px ${offset}px ${blur}px ${colors.dark}, -${offset}px -${offset}px ${rimBlur}px ${colors.light}`
|
|
2183
|
-
},
|
|
2184
|
-
default: {
|
|
2185
|
-
// React Native shadow approximation (limited compared to web)
|
|
2186
|
-
shadowColor: "#000",
|
|
2187
|
-
shadowOffset: { width: offset, height: offset },
|
|
2188
|
-
shadowOpacity: 0.3,
|
|
2189
|
-
shadowRadius: blur / 2,
|
|
2190
|
-
elevation: offset
|
|
2191
|
-
}
|
|
2192
|
-
});
|
|
2183
|
+
function Cell({
|
|
2184
|
+
width,
|
|
2185
|
+
children,
|
|
2186
|
+
testID
|
|
2187
|
+
}) {
|
|
2188
|
+
return /* @__PURE__ */ jsx(reactNative.View, { style: { width, alignItems: "center", justifyContent: "center" }, testID, children });
|
|
2193
2189
|
}
|
|
2194
|
-
function
|
|
2195
|
-
const
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2190
|
+
function RowStrip({ set }) {
|
|
2191
|
+
const zones = set.velocityZones;
|
|
2192
|
+
if (set.state === "live") {
|
|
2193
|
+
return /* @__PURE__ */ jsx(
|
|
2194
|
+
VelocityStrip,
|
|
2195
|
+
{
|
|
2196
|
+
variant: "expanded",
|
|
2197
|
+
showNumbers: false,
|
|
2198
|
+
showInfo: false,
|
|
2199
|
+
height: 24,
|
|
2200
|
+
scale: "fixed",
|
|
2201
|
+
liveRepIndex: set.velocities.length - 1,
|
|
2202
|
+
set: { type: "straight", velocities: set.velocities, planned: set.target.reps },
|
|
2203
|
+
zones
|
|
2204
|
+
}
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
const velocities = set.state === "done" ? set.velocities : [];
|
|
2208
|
+
return /* @__PURE__ */ jsx(
|
|
2209
|
+
VelocityStrip,
|
|
2210
|
+
{
|
|
2211
|
+
variant: "compact",
|
|
2212
|
+
height: 8,
|
|
2213
|
+
hideBaseline: true,
|
|
2214
|
+
set: { type: "straight", velocities, planned: plannedReps(set) },
|
|
2215
|
+
zones
|
|
2207
2216
|
}
|
|
2208
|
-
|
|
2217
|
+
);
|
|
2209
2218
|
}
|
|
2210
|
-
function
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2219
|
+
function SetRow(set) {
|
|
2220
|
+
const live = set.state === "live";
|
|
2221
|
+
const valueColor = live ? TEXT_ACTIVE : TEXT_MUTED;
|
|
2222
|
+
const rpe = set.state === "todo" ? null : set.rpe;
|
|
2223
|
+
return /* @__PURE__ */ jsxs(
|
|
2224
|
+
reactNative.View,
|
|
2225
|
+
{
|
|
2226
|
+
style: { paddingVertical: 6, paddingHorizontal: 8 },
|
|
2227
|
+
accessibilityLabel: accessibilityLabel(set),
|
|
2228
|
+
testID: "set-row",
|
|
2229
|
+
children: [
|
|
2230
|
+
/* @__PURE__ */ jsxs(reactNative.View, { className: "flex-row items-center", style: { justifyContent: "space-between" }, children: [
|
|
2231
|
+
/* @__PURE__ */ jsx(Cell, { width: COL.set, testID: "set-row-set-number", children: set.setType ? /* @__PURE__ */ jsx(reactNative.Text, { style: typeBadge, testID: "set-row-type-badge", children: set.setType }) : /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor, fontWeight: live ? "700" : "600" }, children: set.setNumber }) }),
|
|
2232
|
+
/* @__PURE__ */ jsx(Cell, { width: COL.reps, testID: "set-row-reps", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: displayReps(set) }) }),
|
|
2233
|
+
/* @__PURE__ */ jsx(Cell, { width: COL.load, testID: "set-row-weight", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: valueColor }, children: roundWeight(displayWeight(set)) }) }),
|
|
2234
|
+
/* @__PURE__ */ jsx(Cell, { width: COL.rpe, testID: "set-row-rpe", children: /* @__PURE__ */ jsx(reactNative.Text, { style: { ...cellText, color: TEXT_MUTED }, children: rpe != null ? roundRpe(rpe) : "\u2014" }) })
|
|
2235
|
+
] }),
|
|
2236
|
+
/* @__PURE__ */ jsx(reactNative.View, { style: { marginTop: live ? 6 : 4 }, testID: "set-row-strip", children: /* @__PURE__ */ jsx(RowStrip, { set }) })
|
|
2237
|
+
]
|
|
2218
2238
|
}
|
|
2219
|
-
|
|
2239
|
+
);
|
|
2220
2240
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2241
|
+
var PREV_COLUMN = { label: "PREV", width: void 0 };
|
|
2242
|
+
function buildColumns(unit, showPrevious) {
|
|
2243
|
+
return [
|
|
2244
|
+
{ label: "SET", width: 36 },
|
|
2245
|
+
...showPrevious ? [PREV_COLUMN] : [],
|
|
2246
|
+
{ label: "REPS", width: 44 },
|
|
2247
|
+
{ label: unit.toUpperCase(), width: 56 },
|
|
2248
|
+
{ label: "RPE", width: 36 }
|
|
2249
|
+
];
|
|
2250
|
+
}
|
|
2251
|
+
function SetTableHeader({
|
|
2252
|
+
unit = "lbs",
|
|
2253
|
+
showPrevious = true,
|
|
2254
|
+
testID = "table-header"
|
|
2255
|
+
}) {
|
|
2256
|
+
const columns = buildColumns(unit, showPrevious);
|
|
2257
|
+
return /* @__PURE__ */ jsx(
|
|
2258
|
+
reactNative.View,
|
|
2259
|
+
{
|
|
2260
|
+
className: "flex-row",
|
|
2261
|
+
style: {
|
|
2262
|
+
paddingVertical: 8,
|
|
2263
|
+
paddingHorizontal: 8,
|
|
2264
|
+
paddingBottom: 4,
|
|
2265
|
+
...showPrevious ? {} : { justifyContent: "space-between" }
|
|
2266
|
+
},
|
|
2267
|
+
testID,
|
|
2268
|
+
children: columns.map(({ label, width }) => {
|
|
2269
|
+
const isFlex = width === void 0;
|
|
2270
|
+
return /* @__PURE__ */ jsx(
|
|
2271
|
+
reactNative.View,
|
|
2272
|
+
{
|
|
2273
|
+
style: {
|
|
2274
|
+
...isFlex ? { minWidth: 44 } : { width, flexShrink: 1 },
|
|
2275
|
+
alignItems: "center",
|
|
2276
|
+
justifyContent: "center",
|
|
2277
|
+
gap: 8
|
|
2278
|
+
},
|
|
2279
|
+
...isFlex ? { className: "flex-1" } : {},
|
|
2280
|
+
children: /* @__PURE__ */ jsx(
|
|
2281
|
+
reactNative.Text,
|
|
2282
|
+
{
|
|
2283
|
+
className: "text-text-tertiary",
|
|
2284
|
+
style: {
|
|
2285
|
+
fontSize: 10,
|
|
2286
|
+
fontWeight: "600",
|
|
2287
|
+
fontFamily: "Inter, sans-serif",
|
|
2288
|
+
textTransform: "uppercase",
|
|
2289
|
+
letterSpacing: 0.8
|
|
2290
|
+
},
|
|
2291
|
+
children: label
|
|
2292
|
+
}
|
|
2293
|
+
)
|
|
2294
|
+
},
|
|
2295
|
+
label
|
|
2296
|
+
);
|
|
2297
|
+
})
|
|
2238
2298
|
}
|
|
2239
|
-
|
|
2299
|
+
);
|
|
2240
2300
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
elevation: 0
|
|
2301
|
+
var METRIC_FONT = "Inter, sans-serif";
|
|
2302
|
+
function MetricCell({ children, color, fontSize = 11 }) {
|
|
2303
|
+
return /* @__PURE__ */ jsx(
|
|
2304
|
+
reactNative.Text,
|
|
2305
|
+
{
|
|
2306
|
+
style: {
|
|
2307
|
+
fontFamily: METRIC_FONT,
|
|
2308
|
+
fontSize,
|
|
2309
|
+
color,
|
|
2310
|
+
fontWeight: "600",
|
|
2311
|
+
letterSpacing: 1
|
|
2312
|
+
},
|
|
2313
|
+
children
|
|
2255
2314
|
}
|
|
2256
|
-
|
|
2315
|
+
);
|
|
2257
2316
|
}
|
|
2258
|
-
({
|
|
2259
|
-
// Charcoal surface shadows (for dark toolbars, cards)
|
|
2260
|
-
charcoal: {
|
|
2261
|
-
raised: {
|
|
2262
|
-
subtle: createRaisedShadow("charcoal", "subtle"),
|
|
2263
|
-
medium: createRaisedShadow("charcoal", "medium"),
|
|
2264
|
-
strong: createRaisedShadow("charcoal", "strong")
|
|
2265
|
-
},
|
|
2266
|
-
raisedHover: {
|
|
2267
|
-
subtle: createRaisedHoverShadow("charcoal", "subtle"),
|
|
2268
|
-
medium: createRaisedHoverShadow("charcoal", "medium"),
|
|
2269
|
-
strong: createRaisedHoverShadow("charcoal", "strong")
|
|
2270
|
-
},
|
|
2271
|
-
pressed: {
|
|
2272
|
-
subtle: createPressedShadow("charcoal", "subtle"),
|
|
2273
|
-
medium: createPressedShadow("charcoal", "medium"),
|
|
2274
|
-
strong: createPressedShadow("charcoal", "strong")
|
|
2275
|
-
},
|
|
2276
|
-
pressedHover: {
|
|
2277
|
-
subtle: createPressedHoverShadow("charcoal", "subtle"),
|
|
2278
|
-
medium: createPressedHoverShadow("charcoal", "medium"),
|
|
2279
|
-
strong: createPressedHoverShadow("charcoal", "strong")
|
|
2280
|
-
},
|
|
2281
|
-
flat: createFlatShadow()
|
|
2282
|
-
},
|
|
2283
|
-
// Neutral dark surface shadows
|
|
2284
|
-
neutralDark: {
|
|
2285
|
-
raised: {
|
|
2286
|
-
subtle: createRaisedShadow("neutralDark", "subtle"),
|
|
2287
|
-
medium: createRaisedShadow("neutralDark", "medium"),
|
|
2288
|
-
strong: createRaisedShadow("neutralDark", "strong")
|
|
2289
|
-
},
|
|
2290
|
-
raisedHover: {
|
|
2291
|
-
subtle: createRaisedHoverShadow("neutralDark", "subtle"),
|
|
2292
|
-
medium: createRaisedHoverShadow("neutralDark", "medium"),
|
|
2293
|
-
strong: createRaisedHoverShadow("neutralDark", "strong")
|
|
2294
|
-
},
|
|
2295
|
-
pressed: {
|
|
2296
|
-
subtle: createPressedShadow("neutralDark", "subtle"),
|
|
2297
|
-
medium: createPressedShadow("neutralDark", "medium"),
|
|
2298
|
-
strong: createPressedShadow("neutralDark", "strong")
|
|
2299
|
-
},
|
|
2300
|
-
pressedHover: {
|
|
2301
|
-
subtle: createPressedHoverShadow("neutralDark", "subtle"),
|
|
2302
|
-
medium: createPressedHoverShadow("neutralDark", "medium"),
|
|
2303
|
-
strong: createPressedHoverShadow("neutralDark", "strong")
|
|
2304
|
-
},
|
|
2305
|
-
flat: createFlatShadow()
|
|
2306
|
-
},
|
|
2307
|
-
// Neutral light surface shadows
|
|
2308
|
-
neutralLight: {
|
|
2309
|
-
raised: {
|
|
2310
|
-
subtle: createRaisedShadow("neutralLight", "subtle"),
|
|
2311
|
-
medium: createRaisedShadow("neutralLight", "medium"),
|
|
2312
|
-
strong: createRaisedShadow("neutralLight", "strong")
|
|
2313
|
-
},
|
|
2314
|
-
raisedHover: {
|
|
2315
|
-
subtle: createRaisedHoverShadow("neutralLight", "subtle"),
|
|
2316
|
-
medium: createRaisedHoverShadow("neutralLight", "medium"),
|
|
2317
|
-
strong: createRaisedHoverShadow("neutralLight", "strong")
|
|
2318
|
-
},
|
|
2319
|
-
pressed: {
|
|
2320
|
-
subtle: createPressedShadow("neutralLight", "subtle"),
|
|
2321
|
-
medium: createPressedShadow("neutralLight", "medium"),
|
|
2322
|
-
strong: createPressedShadow("neutralLight", "strong")
|
|
2323
|
-
},
|
|
2324
|
-
pressedHover: {
|
|
2325
|
-
subtle: createPressedHoverShadow("neutralLight", "subtle"),
|
|
2326
|
-
medium: createPressedHoverShadow("neutralLight", "medium"),
|
|
2327
|
-
strong: createPressedHoverShadow("neutralLight", "strong")
|
|
2328
|
-
},
|
|
2329
|
-
flat: createFlatShadow()
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2332
2317
|
|
|
2333
|
-
// src/
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2318
|
+
// src/components/custom/Workout/SetsRepsLoad.tsx
|
|
2319
|
+
var t2 = getSemanticColors("dark");
|
|
2320
|
+
var TIMES = "\xD7";
|
|
2321
|
+
var AT = "@";
|
|
2322
|
+
function SetsRepsLoad({
|
|
2323
|
+
sets,
|
|
2324
|
+
reps,
|
|
2325
|
+
load,
|
|
2326
|
+
unit = "lb",
|
|
2327
|
+
fontSize,
|
|
2328
|
+
className,
|
|
2329
|
+
...props
|
|
2330
|
+
}) {
|
|
2331
|
+
const value = t2["text-primary"];
|
|
2332
|
+
const sep = t2["text-tertiary"];
|
|
2333
|
+
return /* @__PURE__ */ jsxs(
|
|
2334
|
+
reactNative.View,
|
|
2335
|
+
{
|
|
2336
|
+
className,
|
|
2337
|
+
style: { flexDirection: "row", alignItems: "baseline" },
|
|
2338
|
+
accessibilityLabel: `${sets} sets of ${reps} reps at ${load} ${unit}`,
|
|
2339
|
+
testID: "sets-reps-load",
|
|
2340
|
+
...props,
|
|
2341
|
+
children: [
|
|
2342
|
+
/* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: sets }),
|
|
2343
|
+
/* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${TIMES} ` }),
|
|
2344
|
+
/* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: reps }),
|
|
2345
|
+
/* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${AT} ` }),
|
|
2346
|
+
/* @__PURE__ */ jsx(MetricCell, { color: value, fontSize, children: load }),
|
|
2347
|
+
/* @__PURE__ */ jsx(MetricCell, { color: sep, fontSize, children: ` ${unit}` })
|
|
2348
|
+
]
|
|
2349
2349
|
}
|
|
2350
|
-
|
|
2351
|
-
}
|
|
2352
|
-
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
2353
|
-
if (rgbMatch) {
|
|
2354
|
-
const [, r, g, b] = rgbMatch;
|
|
2355
|
-
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
|
|
2356
|
-
}
|
|
2357
|
-
const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/);
|
|
2358
|
-
if (rgbaMatch) {
|
|
2359
|
-
const [, r, g, b] = rgbaMatch;
|
|
2360
|
-
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
|
|
2361
|
-
}
|
|
2362
|
-
return color;
|
|
2350
|
+
);
|
|
2363
2351
|
}
|
|
2364
|
-
|
|
2365
|
-
// src/components/custom/Workout/TempoDisplay.tsx
|
|
2366
2352
|
var t3 = getSemanticColors("dark");
|
|
2367
2353
|
var INTER = "Inter, sans-serif";
|
|
2368
2354
|
var TEXT_TERTIARY = "#6B7280";
|
|
@@ -2746,6 +2732,243 @@ function ExerciseHeading({
|
|
|
2746
2732
|
)
|
|
2747
2733
|
] });
|
|
2748
2734
|
}
|
|
2735
|
+
var PULSE_HALF_MS = 950;
|
|
2736
|
+
var PULSE_MIN_OPACITY = 0.45;
|
|
2737
|
+
var SEGMENTED_BAR_GAP = 5;
|
|
2738
|
+
function usePulse(active) {
|
|
2739
|
+
const [value] = react.useState(() => new reactNative.Animated.Value(0));
|
|
2740
|
+
react.useEffect(() => {
|
|
2741
|
+
if (!active) return;
|
|
2742
|
+
const loop = reactNative.Animated.loop(
|
|
2743
|
+
reactNative.Animated.sequence([
|
|
2744
|
+
reactNative.Animated.timing(value, {
|
|
2745
|
+
toValue: 1,
|
|
2746
|
+
duration: PULSE_HALF_MS,
|
|
2747
|
+
easing: reactNative.Easing.inOut(reactNative.Easing.ease),
|
|
2748
|
+
useNativeDriver: false
|
|
2749
|
+
}),
|
|
2750
|
+
reactNative.Animated.timing(value, {
|
|
2751
|
+
toValue: 0,
|
|
2752
|
+
duration: PULSE_HALF_MS,
|
|
2753
|
+
easing: reactNative.Easing.inOut(reactNative.Easing.ease),
|
|
2754
|
+
useNativeDriver: false
|
|
2755
|
+
})
|
|
2756
|
+
])
|
|
2757
|
+
);
|
|
2758
|
+
loop.start();
|
|
2759
|
+
return () => loop.stop();
|
|
2760
|
+
}, [active, value]);
|
|
2761
|
+
return value;
|
|
2762
|
+
}
|
|
2763
|
+
function SegmentedBar({
|
|
2764
|
+
segments,
|
|
2765
|
+
height = 8,
|
|
2766
|
+
gap = SEGMENTED_BAR_GAP,
|
|
2767
|
+
radius = 2,
|
|
2768
|
+
marker = null,
|
|
2769
|
+
segmentTestID,
|
|
2770
|
+
style,
|
|
2771
|
+
...props
|
|
2772
|
+
}) {
|
|
2773
|
+
const pulse = usePulse(segments.some((s) => s.pulse));
|
|
2774
|
+
return /* @__PURE__ */ jsxs(reactNative.View, { style: [{ flexDirection: "row", height, gap, position: "relative" }, style], ...props, children: [
|
|
2775
|
+
segments.map((seg, i) => {
|
|
2776
|
+
const fillStyle = {
|
|
2777
|
+
width: `${(seg.fill ?? 1) * 100}%`,
|
|
2778
|
+
height: "100%",
|
|
2779
|
+
backgroundColor: seg.color,
|
|
2780
|
+
opacity: seg.opacity
|
|
2781
|
+
};
|
|
2782
|
+
const testID = segmentTestID?.(seg, i) ?? "segmented-bar-segment";
|
|
2783
|
+
return /* @__PURE__ */ jsx(
|
|
2784
|
+
reactNative.View,
|
|
2785
|
+
{
|
|
2786
|
+
style: {
|
|
2787
|
+
flex: seg.weight ?? 1,
|
|
2788
|
+
minWidth: 0,
|
|
2789
|
+
height: "100%",
|
|
2790
|
+
marginLeft: seg.leadingGap,
|
|
2791
|
+
borderRadius: radius,
|
|
2792
|
+
overflow: "hidden"
|
|
2793
|
+
},
|
|
2794
|
+
children: seg.pulse ? /* @__PURE__ */ jsx(
|
|
2795
|
+
reactNative.Animated.View,
|
|
2796
|
+
{
|
|
2797
|
+
style: {
|
|
2798
|
+
...fillStyle,
|
|
2799
|
+
...seg.pulseColor ? {
|
|
2800
|
+
backgroundColor: pulse.interpolate({
|
|
2801
|
+
inputRange: [0, 1],
|
|
2802
|
+
outputRange: seg.pulseColor
|
|
2803
|
+
})
|
|
2804
|
+
} : {
|
|
2805
|
+
opacity: pulse.interpolate({
|
|
2806
|
+
inputRange: [0, 1],
|
|
2807
|
+
outputRange: [PULSE_MIN_OPACITY, 1]
|
|
2808
|
+
})
|
|
2809
|
+
}
|
|
2810
|
+
},
|
|
2811
|
+
accessibilityElementsHidden: true,
|
|
2812
|
+
testID
|
|
2813
|
+
}
|
|
2814
|
+
) : /* @__PURE__ */ jsx(reactNative.View, { style: fillStyle, accessibilityElementsHidden: true, testID })
|
|
2815
|
+
},
|
|
2816
|
+
i
|
|
2817
|
+
);
|
|
2818
|
+
}),
|
|
2819
|
+
marker ? /* @__PURE__ */ jsx(
|
|
2820
|
+
reactNative.View,
|
|
2821
|
+
{
|
|
2822
|
+
style: {
|
|
2823
|
+
position: "absolute",
|
|
2824
|
+
left: `${marker.position * 100}%`,
|
|
2825
|
+
top: 0,
|
|
2826
|
+
bottom: 0,
|
|
2827
|
+
width: 2,
|
|
2828
|
+
backgroundColor: marker.color
|
|
2829
|
+
},
|
|
2830
|
+
accessibilityElementsHidden: true,
|
|
2831
|
+
testID: "segmented-bar-marker"
|
|
2832
|
+
}
|
|
2833
|
+
) : null
|
|
2834
|
+
] });
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// src/components/custom/Workout/SetBar.tsx
|
|
2838
|
+
var SET_STRIP_ZONES = {
|
|
2839
|
+
slow: primitiveRamps.red[600],
|
|
2840
|
+
moderate: primitiveRamps.orange[400],
|
|
2841
|
+
fast: primitiveRamps.amber[300],
|
|
2842
|
+
fastest: primitiveRamps.green[300]
|
|
2843
|
+
};
|
|
2844
|
+
var TODO_COLOR = primitiveColors.charcoal[300];
|
|
2845
|
+
var SET_STRIP_VARIABLE_COLOR = primitiveRamps.cyan[900];
|
|
2846
|
+
var NOTCH_GAP = 2;
|
|
2847
|
+
var CLUSTER_GAP = 3;
|
|
2848
|
+
var MYO_UPCOMING_CLUSTERS = 3;
|
|
2849
|
+
var MYO_UPCOMING_CLUSTER_LEN = 5;
|
|
2850
|
+
var MYO_UPCOMING_FADE = 0.6;
|
|
2851
|
+
var PULSE_RANGE = {
|
|
2852
|
+
[SET_STRIP_ZONES.slow]: [primitiveRamps.red[500], primitiveRamps.red[700]],
|
|
2853
|
+
[SET_STRIP_ZONES.moderate]: [primitiveRamps.orange[300], primitiveRamps.orange[500]],
|
|
2854
|
+
[SET_STRIP_ZONES.fast]: [primitiveRamps.amber[200], primitiveRamps.amber[400]],
|
|
2855
|
+
[SET_STRIP_ZONES.fastest]: [primitiveRamps.green[200], primitiveRamps.green[400]]
|
|
2856
|
+
};
|
|
2857
|
+
function velocityZoneColor(v) {
|
|
2858
|
+
if (v < 0.5) return SET_STRIP_ZONES.slow;
|
|
2859
|
+
if (v < 0.75) return SET_STRIP_ZONES.moderate;
|
|
2860
|
+
if (v < 1) return SET_STRIP_ZONES.fast;
|
|
2861
|
+
return SET_STRIP_ZONES.fastest;
|
|
2862
|
+
}
|
|
2863
|
+
function chunkedSegments(chunks, gap) {
|
|
2864
|
+
return chunks.flatMap(
|
|
2865
|
+
(vels, ci) => vels.map((v, ri) => ({
|
|
2866
|
+
color: velocityZoneColor(v),
|
|
2867
|
+
...ci > 0 && ri === 0 ? { leadingGap: gap } : {}
|
|
2868
|
+
}))
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
function rangeSegments(floor, max, doneVels) {
|
|
2872
|
+
return Array.from({ length: max }, (_, i) => {
|
|
2873
|
+
if (i < doneVels.length) return { color: velocityZoneColor(doneVels[i]) };
|
|
2874
|
+
return { color: i < floor ? TODO_COLOR : SET_STRIP_VARIABLE_COLOR };
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
function myoUpcomingSegments(activationLen, clusterCount) {
|
|
2878
|
+
const activation = Array.from({ length: activationLen }, () => ({ color: TODO_COLOR }));
|
|
2879
|
+
const trail = Array.from(
|
|
2880
|
+
{ length: clusterCount },
|
|
2881
|
+
(_, ci) => Array.from({ length: MYO_UPCOMING_CLUSTER_LEN }, (_2, ri) => ({
|
|
2882
|
+
color: SET_STRIP_VARIABLE_COLOR,
|
|
2883
|
+
opacity: MYO_UPCOMING_FADE ** ci,
|
|
2884
|
+
...ri === 0 ? { leadingGap: CLUSTER_GAP } : {}
|
|
2885
|
+
}))
|
|
2886
|
+
).flat();
|
|
2887
|
+
return [...activation, ...trail];
|
|
2888
|
+
}
|
|
2889
|
+
function setSegments(set) {
|
|
2890
|
+
if (set.status === "todo") {
|
|
2891
|
+
return [{ color: TODO_COLOR }];
|
|
2892
|
+
}
|
|
2893
|
+
if (set.status === "done") {
|
|
2894
|
+
return set.velocities.map((v) => ({ color: velocityZoneColor(v) }));
|
|
2895
|
+
}
|
|
2896
|
+
if (set.status === "range") {
|
|
2897
|
+
return rangeSegments(set.floor, set.max, set.doneVels);
|
|
2898
|
+
}
|
|
2899
|
+
if (set.status === "drop") {
|
|
2900
|
+
return chunkedSegments(set.subloads, NOTCH_GAP);
|
|
2901
|
+
}
|
|
2902
|
+
if (set.status === "myo") {
|
|
2903
|
+
return chunkedSegments([set.activation, ...set.clusters], CLUSTER_GAP);
|
|
2904
|
+
}
|
|
2905
|
+
if (set.status === "myo-upcoming") {
|
|
2906
|
+
return myoUpcomingSegments(set.activationLen, set.clusterCount ?? MYO_UPCOMING_CLUSTERS);
|
|
2907
|
+
}
|
|
2908
|
+
const performed = set.velocities.map((v) => {
|
|
2909
|
+
const color = velocityZoneColor(v);
|
|
2910
|
+
return { color, pulse: true, pulseColor: PULSE_RANGE[color] };
|
|
2911
|
+
});
|
|
2912
|
+
const remaining = Math.max(0, set.planned - set.velocities.length);
|
|
2913
|
+
return [...performed, ...Array.from({ length: remaining }, () => ({ color: TODO_COLOR }))];
|
|
2914
|
+
}
|
|
2915
|
+
function setBarSegmentTestID(seg) {
|
|
2916
|
+
if (seg.pulse) return "set-strip-pulse";
|
|
2917
|
+
if (seg.color === SET_STRIP_VARIABLE_COLOR) return "set-strip-variable";
|
|
2918
|
+
return seg.color === TODO_COLOR ? "set-strip-empty" : "set-strip-fill";
|
|
2919
|
+
}
|
|
2920
|
+
function SetBar({ set, height = 8 }) {
|
|
2921
|
+
return /* @__PURE__ */ jsx(
|
|
2922
|
+
SegmentedBar,
|
|
2923
|
+
{
|
|
2924
|
+
segments: setSegments(set),
|
|
2925
|
+
height,
|
|
2926
|
+
gap: 0,
|
|
2927
|
+
radius: 0,
|
|
2928
|
+
segmentTestID: setBarSegmentTestID,
|
|
2929
|
+
style: { flex: 1, minWidth: 0, borderRadius: 2, overflow: "hidden" },
|
|
2930
|
+
testID: "set-strip-set"
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// src/components/custom/Workout/SetStrip.tsx
|
|
2936
|
+
var SET_STRIP_GAP = 5;
|
|
2937
|
+
function setCategory(set) {
|
|
2938
|
+
switch (set.status) {
|
|
2939
|
+
case "done":
|
|
2940
|
+
case "drop":
|
|
2941
|
+
case "myo":
|
|
2942
|
+
return "done";
|
|
2943
|
+
case "active":
|
|
2944
|
+
return "active";
|
|
2945
|
+
case "range":
|
|
2946
|
+
return set.doneVels.length > 0 ? "active" : "upcoming";
|
|
2947
|
+
case "todo":
|
|
2948
|
+
case "myo-upcoming":
|
|
2949
|
+
return "upcoming";
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
function describeSets(sets) {
|
|
2953
|
+
const done = sets.filter((s) => setCategory(s) === "done").length;
|
|
2954
|
+
const active = sets.filter((s) => setCategory(s) === "active").length;
|
|
2955
|
+
const upcoming = sets.filter((s) => setCategory(s) === "upcoming").length;
|
|
2956
|
+
return `Set progress: ${done} done, ${active} in progress, ${upcoming} upcoming`;
|
|
2957
|
+
}
|
|
2958
|
+
function SetStrip({ sets, height = 8, className, ...props }) {
|
|
2959
|
+
return /* @__PURE__ */ jsx(
|
|
2960
|
+
reactNative.View,
|
|
2961
|
+
{
|
|
2962
|
+
className,
|
|
2963
|
+
style: { flexDirection: "row", width: "100%", height, gap: SET_STRIP_GAP },
|
|
2964
|
+
accessibilityRole: "image",
|
|
2965
|
+
accessibilityLabel: describeSets(sets),
|
|
2966
|
+
testID: "set-strip",
|
|
2967
|
+
...props,
|
|
2968
|
+
children: sets.map((set, i) => /* @__PURE__ */ jsx(SetBar, { set, height }, i))
|
|
2969
|
+
}
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2749
2972
|
|
|
2750
2973
|
// src/components/custom/Workout/ExerciseCardHeading.tsx
|
|
2751
2974
|
function ExerciseCardHeading({
|
|
@@ -2888,7 +3111,9 @@ function CollapsedCard({
|
|
|
2888
3111
|
{
|
|
2889
3112
|
velocities,
|
|
2890
3113
|
zones: velocityZones,
|
|
2891
|
-
variant: "
|
|
3114
|
+
variant: "compact",
|
|
3115
|
+
height: 8,
|
|
3116
|
+
hideBaseline: true,
|
|
2892
3117
|
testID: `exercise-card-velocity-strip-${i}`
|
|
2893
3118
|
},
|
|
2894
3119
|
i
|
|
@@ -6057,7 +6282,16 @@ function VbtBreakdown({
|
|
|
6057
6282
|
children: set.label
|
|
6058
6283
|
}
|
|
6059
6284
|
),
|
|
6060
|
-
/* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(
|
|
6285
|
+
/* @__PURE__ */ jsx(reactNative.View, { className: "flex-1", style: { height: 8, justifyContent: "center" }, children: /* @__PURE__ */ jsx(
|
|
6286
|
+
VelocityStrip,
|
|
6287
|
+
{
|
|
6288
|
+
velocities: set.velocities,
|
|
6289
|
+
zones,
|
|
6290
|
+
variant: "compact",
|
|
6291
|
+
height: 8,
|
|
6292
|
+
hideBaseline: true
|
|
6293
|
+
}
|
|
6294
|
+
) }),
|
|
6061
6295
|
/* @__PURE__ */ jsx(
|
|
6062
6296
|
reactNative.Text,
|
|
6063
6297
|
{
|