@titan-design/react-ui 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.d.mts +109 -12
  2. package/dist/index.d.ts +109 -12
  3. package/dist/index.js +648 -231
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +648 -232
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-8u_4WbL-.d.mts → pages-DCFBqp79.d.mts} +16 -7
  8. package/dist/{pages-DaWiBOGa.d.ts → pages-_fI3-x7Z.d.ts} +16 -7
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +495 -92
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +496 -93
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +90 -0
  17. package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
  18. package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
  19. package/src/components/custom/CircularTimer/index.ts +1 -0
  20. package/src/components/custom/Workout/FatigueMeter.stories.tsx +2 -2
  21. package/src/components/custom/Workout/FatigueMeter.test.tsx +12 -0
  22. package/src/components/custom/Workout/FatigueMeter.tsx +7 -3
  23. package/src/components/custom/Workout/README.md +47 -0
  24. package/src/components/custom/Workout/RestTimer.stories.tsx +132 -2
  25. package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
  26. package/src/components/custom/Workout/RestTimer.tsx +141 -50
  27. package/src/components/custom/Workout/TempoBar.stories.tsx +69 -0
  28. package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
  29. package/src/components/custom/Workout/TempoBar.tsx +107 -22
  30. package/src/components/custom/Workout/VelocityStrip.stories.tsx +152 -9
  31. package/src/components/custom/Workout/VelocityStrip.test.tsx +64 -0
  32. package/src/components/custom/Workout/VelocityStrip.tsx +287 -40
  33. package/src/components/custom/Workout/ZoneTrack.stories.tsx +2 -3
  34. package/src/components/custom/Workout/ZoneTrack.test.tsx +34 -0
  35. package/src/components/custom/Workout/ZoneTrack.tsx +76 -18
  36. package/src/components/custom/index.ts +1 -0
  37. package/src/components/ui/alert/Alert.stories.tsx +85 -2
  38. package/src/components/ui/alert/Alert.test.tsx +52 -0
  39. package/src/components/ui/alert/Alert.tsx +54 -20
  40. package/src/components/ui/progress/Progress.tsx +47 -22
package/dist/pages.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useMemo, useEffect, Fragment as Fragment$1, useSyncExternalStore, useCallback } from 'react';
2
- import { Easing, Platform, View, Text, Pressable, TextInput, Animated, ScrollView } from 'react-native';
2
+ import { Easing, Platform, View, Text, TextInput, Pressable, Animated, ScrollView } from 'react-native';
3
3
  import { Fragment, jsx as jsx$1, jsxs as jsxs$1 } from 'react/jsx-runtime';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
@@ -33,6 +33,8 @@ var primitiveColors = {
33
33
  900: "#111827"},
34
34
  // Charcoal scale (dark backgrounds)
35
35
  charcoal: {
36
+ 0: "#6E6E6E",
37
+ 100: "#4C4C4C",
36
38
  200: "#3C3C3C",
37
39
  300: "#2C2C2C",
38
40
  400: "#1F1F1F",
@@ -604,6 +606,14 @@ var EXPANDED_ENCODED_PCT = 45;
604
606
  var EXPANDED_HEIGHT = 60;
605
607
  var FIXED_MAX_VELOCITY = 1.15;
606
608
  var BARE_STUB_HEIGHT = 3;
609
+ var HERO_HEIGHT = 220;
610
+ var HERO_LABEL_HEADROOM = 20;
611
+ var HERO_BAR_GAP = 8;
612
+ var HERO_BAR_MAX_WIDTH = 52;
613
+ var HERO_BAR_RADIUS = 5;
614
+ var HERO_MIN_BAR_HEIGHT = 4;
615
+ var HERO_PENDING_COLOR = primitiveColors.charcoal[100];
616
+ var HERO_REFERENCE_COLOR = primitiveColors.charcoal[0];
607
617
  function scaleDenominator(scale, maxVelocity) {
608
618
  return scale === "fixed" ? FIXED_MAX_VELOCITY : maxVelocity * 1.15;
609
619
  }
@@ -631,6 +641,183 @@ function usePrefersReducedMotion() {
631
641
  var ANIMATION_DURATION = 400;
632
642
  var ANIMATION_EASING = Easing.bezier(0.22, 1, 0.36, 1);
633
643
  var POP_EASING = Easing.bezier(0.34, 1.56, 0.64, 1);
644
+ function useLiveRepPop(active, liveRepIndex, liveVelocity, isNewPeak) {
645
+ const prefersReducedMotion = usePrefersReducedMotion();
646
+ const [liveScale] = useState(() => new Animated.Value(1));
647
+ useEffect(() => {
648
+ if (!active || liveRepIndex == null || liveVelocity == null) return;
649
+ if (prefersReducedMotion) {
650
+ liveScale.setValue(1);
651
+ return;
652
+ }
653
+ if (isNewPeak) {
654
+ liveScale.setValue(1);
655
+ Animated.sequence([
656
+ Animated.timing(liveScale, {
657
+ toValue: 1.25,
658
+ duration: 150,
659
+ easing: Easing.out(Easing.quad),
660
+ useNativeDriver: true
661
+ }),
662
+ Animated.spring(liveScale, {
663
+ toValue: 1,
664
+ friction: 3,
665
+ tension: 140,
666
+ useNativeDriver: true
667
+ })
668
+ ]).start();
669
+ } else {
670
+ liveScale.setValue(0.8);
671
+ Animated.timing(liveScale, {
672
+ toValue: 1,
673
+ duration: 300,
674
+ easing: POP_EASING,
675
+ useNativeDriver: true
676
+ }).start();
677
+ }
678
+ }, [active, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
679
+ return liveScale;
680
+ }
681
+ function HeroVelocityChart({
682
+ doneVelocities,
683
+ barColorFor,
684
+ scaleDenom,
685
+ referenceVelocity,
686
+ liveRepIndex,
687
+ isNewPeak,
688
+ targetReps,
689
+ height,
690
+ className,
691
+ viewProps
692
+ }) {
693
+ const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
694
+ const liveScale = useLiveRepPop(true, liveRepIndex, liveVelocity, isNewPeak);
695
+ const plotHeight = Math.max(0, height - HERO_LABEL_HEADROOM);
696
+ const barHeight = (velocity) => scaleDenom > 0 ? Math.max(HERO_MIN_BAR_HEIGHT, Math.min(1, velocity / scaleDenom) * plotHeight) : HERO_MIN_BAR_HEIGHT;
697
+ const pendingCount = Math.max(0, (targetReps ?? 0) - doneVelocities.length);
698
+ const referencePx = referenceVelocity > 0 && scaleDenom > 0 ? Math.min(plotHeight, referenceVelocity / scaleDenom * plotHeight) : 0;
699
+ const repCount = doneVelocities.length;
700
+ const total = Math.max(repCount, targetReps ?? repCount);
701
+ const label = referenceVelocity > 0 ? `Velocity chart, ${repCount} of ${total} reps, best ${formatVelocity(referenceVelocity)} meters per second` : `Velocity chart, ${repCount} of ${total} reps`;
702
+ const { style: externalStyle, ...restProps } = viewProps;
703
+ return /* @__PURE__ */ jsx(
704
+ View,
705
+ {
706
+ className,
707
+ style: [{ height }, externalStyle],
708
+ accessibilityRole: "image",
709
+ accessibilityLabel: label,
710
+ testID: "velocity-strip-hero",
711
+ ...restProps,
712
+ children: /* @__PURE__ */ jsxs(
713
+ View,
714
+ {
715
+ style: {
716
+ flex: 1,
717
+ flexDirection: "row",
718
+ alignItems: "flex-end",
719
+ gap: HERO_BAR_GAP,
720
+ position: "relative",
721
+ borderBottomWidth: 2,
722
+ borderBottomColor: HERO_PENDING_COLOR
723
+ },
724
+ children: [
725
+ referencePx > 0 && /* @__PURE__ */ jsx(
726
+ View,
727
+ {
728
+ accessibilityElementsHidden: true,
729
+ style: {
730
+ position: "absolute",
731
+ left: 0,
732
+ right: 0,
733
+ bottom: referencePx,
734
+ borderTopWidth: 1,
735
+ borderStyle: "dashed",
736
+ borderColor: HERO_REFERENCE_COLOR,
737
+ pointerEvents: "none"
738
+ },
739
+ testID: "velocity-hero-reference"
740
+ }
741
+ ),
742
+ doneVelocities.map((velocity, i) => {
743
+ const isLive = liveRepIndex === i;
744
+ return /* @__PURE__ */ jsxs(
745
+ View,
746
+ {
747
+ accessibilityElementsHidden: true,
748
+ style: {
749
+ flex: 1,
750
+ maxWidth: HERO_BAR_MAX_WIDTH,
751
+ height: "100%",
752
+ alignItems: "center",
753
+ justifyContent: "flex-end"
754
+ },
755
+ children: [
756
+ /* @__PURE__ */ jsx(
757
+ Text,
758
+ {
759
+ className: "text-text-primary",
760
+ style: { fontSize: 12, fontWeight: "800", marginBottom: 4 },
761
+ testID: `velocity-label-${i}`,
762
+ children: formatVelocity(velocity)
763
+ }
764
+ ),
765
+ /* @__PURE__ */ jsx(
766
+ Animated.View,
767
+ {
768
+ style: [
769
+ {
770
+ width: "100%",
771
+ height: barHeight(velocity),
772
+ borderTopLeftRadius: HERO_BAR_RADIUS,
773
+ borderTopRightRadius: HERO_BAR_RADIUS,
774
+ backgroundColor: barColorFor(velocity)
775
+ },
776
+ isLive ? { transform: [{ scale: liveScale }] } : null
777
+ ],
778
+ testID: `velocity-bar-${i}`
779
+ }
780
+ )
781
+ ]
782
+ },
783
+ i
784
+ );
785
+ }),
786
+ Array.from({ length: pendingCount }, (_, i) => /* @__PURE__ */ jsx(
787
+ View,
788
+ {
789
+ accessibilityElementsHidden: true,
790
+ style: {
791
+ flex: 1,
792
+ maxWidth: HERO_BAR_MAX_WIDTH,
793
+ height: "100%",
794
+ alignItems: "center",
795
+ justifyContent: "flex-end"
796
+ },
797
+ children: /* @__PURE__ */ jsx(
798
+ View,
799
+ {
800
+ style: {
801
+ width: "100%",
802
+ height: HERO_MIN_BAR_HEIGHT * 3,
803
+ borderWidth: 1,
804
+ borderStyle: "dashed",
805
+ borderColor: HERO_PENDING_COLOR,
806
+ borderTopLeftRadius: HERO_BAR_RADIUS,
807
+ borderTopRightRadius: HERO_BAR_RADIUS
808
+ },
809
+ testID: "velocity-slot-todo"
810
+ }
811
+ )
812
+ },
813
+ `pending-${i}`
814
+ ))
815
+ ]
816
+ }
817
+ )
818
+ }
819
+ );
820
+ }
634
821
  function VelocityStrip({
635
822
  velocities,
636
823
  set,
@@ -641,6 +828,7 @@ function VelocityStrip({
641
828
  onRepPress,
642
829
  variant = "expanded",
643
830
  height = EXPANDED_HEIGHT,
831
+ targetReps,
644
832
  scale = "peak",
645
833
  showNumbers = true,
646
834
  showInfo = true,
@@ -666,45 +854,17 @@ function VelocityStrip({
666
854
  return SET_STRIP_VARIABLE_COLOR;
667
855
  };
668
856
  const meanZone = hasZones ? classifyBand(meanVelocity, zones)?.label ?? "" : getVelocityZoneName(meanVelocity);
669
- const prefersReducedMotion = usePrefersReducedMotion();
670
857
  const [heightAnim] = useState(() => new Animated.Value(expanded ? height : 3));
671
858
  const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0));
672
859
  const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0));
673
- const [liveScale] = useState(() => new Animated.Value(1));
674
860
  const liveVelocity = liveRepIndex != null ? doneVelocities[liveRepIndex] : void 0;
675
861
  const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity;
676
- useEffect(() => {
677
- if (variant !== "expanded" || !framed || liveRepIndex == null || liveVelocity == null) return;
678
- if (prefersReducedMotion) {
679
- liveScale.setValue(1);
680
- return;
681
- }
682
- if (isNewPeak) {
683
- liveScale.setValue(1);
684
- Animated.sequence([
685
- Animated.timing(liveScale, {
686
- toValue: 1.25,
687
- duration: 150,
688
- easing: Easing.out(Easing.quad),
689
- useNativeDriver: true
690
- }),
691
- Animated.spring(liveScale, {
692
- toValue: 1,
693
- friction: 3,
694
- tension: 140,
695
- useNativeDriver: true
696
- })
697
- ]).start();
698
- } else {
699
- liveScale.setValue(0.8);
700
- Animated.timing(liveScale, {
701
- toValue: 1,
702
- duration: 300,
703
- easing: POP_EASING,
704
- useNativeDriver: true
705
- }).start();
706
- }
707
- }, [variant, framed, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale]);
862
+ const liveScale = useLiveRepPop(
863
+ variant === "expanded" && framed,
864
+ liveRepIndex,
865
+ liveVelocity,
866
+ isNewPeak
867
+ );
708
868
  useEffect(() => {
709
869
  if (variant !== "expanded" || !framed) return;
710
870
  Animated.parallel([
@@ -731,6 +891,24 @@ function VelocityStrip({
731
891
  if (set == null && velocities == null) return null;
732
892
  const repCount = doneVelocities.length;
733
893
  const miniLabel = set ? setAccessibilityLabel(set, repCount) : `Velocity strip, ${repCount} reps`;
894
+ if (variant === "hero") {
895
+ const heroHeight = height === EXPANDED_HEIGHT ? HERO_HEIGHT : height;
896
+ return /* @__PURE__ */ jsx(
897
+ HeroVelocityChart,
898
+ {
899
+ doneVelocities,
900
+ barColorFor,
901
+ scaleDenom,
902
+ referenceVelocity: maxVelocity,
903
+ liveRepIndex,
904
+ isNewPeak,
905
+ targetReps,
906
+ height: heroHeight,
907
+ className,
908
+ viewProps: props
909
+ }
910
+ );
911
+ }
734
912
  if (variant === "mini") {
735
913
  const { style: externalStyle, ...restProps } = props;
736
914
  return /* @__PURE__ */ jsx(
@@ -2991,9 +3169,270 @@ function useTimer(options = {}) {
2991
3169
  const done = durationMs != null && elapsedMs >= durationMs;
2992
3170
  return { elapsedMs, remainingMs, progress, label, done };
2993
3171
  }
3172
+ var colorVarMap = {
3173
+ primary: "var(--color-brand-primary)",
3174
+ secondary: "var(--color-brand-secondary)",
3175
+ success: "var(--color-status-success)",
3176
+ error: "var(--color-status-error)",
3177
+ warning: "var(--color-status-warning)",
3178
+ info: "var(--color-status-info)"
3179
+ };
3180
+ function CircularProgress({
3181
+ value = 0,
3182
+ max = 100,
3183
+ isIndeterminate = false,
3184
+ size = 48,
3185
+ strokeWidth = 4,
3186
+ color = "primary",
3187
+ showValue = false,
3188
+ formatValue: formatValue2 = (v, m) => `${Math.round(v / m * 100)}%`,
3189
+ children,
3190
+ fill = false,
3191
+ className,
3192
+ ...props
3193
+ }) {
3194
+ const percentage = Math.min(Math.max(value / max * 100, 0), 100);
3195
+ const radius = (size - strokeWidth) / 2;
3196
+ const circumference = 2 * Math.PI * radius;
3197
+ const strokeDashoffset = circumference - percentage / 100 * circumference;
3198
+ const center = size / 2;
3199
+ const boxStyle = fill ? { width: "100%", aspectRatio: 1 } : { width: size, height: size };
3200
+ return /* @__PURE__ */ jsxs(
3201
+ View,
3202
+ {
3203
+ className: cn("items-center justify-center", className),
3204
+ style: boxStyle,
3205
+ accessibilityRole: "progressbar",
3206
+ accessibilityValue: {
3207
+ min: 0,
3208
+ max,
3209
+ now: isIndeterminate ? void 0 : value
3210
+ },
3211
+ ...props,
3212
+ children: [
3213
+ /* @__PURE__ */ jsx(View, { className: cn(isIndeterminate && "animate-spin"), style: boxStyle, children: /* @__PURE__ */ jsxs(
3214
+ "svg",
3215
+ {
3216
+ width: "100%",
3217
+ height: "100%",
3218
+ viewBox: `0 0 ${size} ${size}`,
3219
+ style: { position: "absolute" },
3220
+ children: [
3221
+ /* @__PURE__ */ jsx(
3222
+ "circle",
3223
+ {
3224
+ cx: center,
3225
+ cy: center,
3226
+ r: radius,
3227
+ fill: "none",
3228
+ stroke: "var(--color-border-default)",
3229
+ strokeWidth
3230
+ }
3231
+ ),
3232
+ /* @__PURE__ */ jsx(
3233
+ "circle",
3234
+ {
3235
+ cx: center,
3236
+ cy: center,
3237
+ r: radius,
3238
+ fill: "none",
3239
+ stroke: colorVarMap[color],
3240
+ strokeWidth,
3241
+ strokeLinecap: "round",
3242
+ strokeDasharray: circumference,
3243
+ strokeDashoffset: isIndeterminate ? circumference * 0.75 : strokeDashoffset,
3244
+ transform: `rotate(-90 ${center} ${center})`,
3245
+ style: { transition: "stroke-dashoffset 300ms cubic-bezier(0.22, 1, 0.36, 1)" }
3246
+ }
3247
+ )
3248
+ ]
3249
+ }
3250
+ ) }),
3251
+ children != null ? /* @__PURE__ */ jsx(
3252
+ View,
3253
+ {
3254
+ style: {
3255
+ position: "absolute",
3256
+ alignItems: "center",
3257
+ justifyContent: "center",
3258
+ pointerEvents: "none"
3259
+ },
3260
+ children
3261
+ }
3262
+ ) : showValue && !isIndeterminate && /* @__PURE__ */ jsx(
3263
+ Text,
3264
+ {
3265
+ className: "text-xs font-semibold text-text-primary absolute",
3266
+ style: { fontSize: Math.max(size * 0.22, 10) },
3267
+ children: formatValue2(value, max)
3268
+ }
3269
+ )
3270
+ ]
3271
+ }
3272
+ );
3273
+ }
3274
+
3275
+ // src/components/custom/CircularTimer/CircularTimer.tsx
3276
+ function CircularTimer({
3277
+ durationMs,
3278
+ elapsedMs,
3279
+ mode = "down",
3280
+ size = 160,
3281
+ strokeWidth = 8,
3282
+ fill = false,
3283
+ color = "primary",
3284
+ doneLabel,
3285
+ doneColor = "success",
3286
+ controls,
3287
+ accessibilityLabel: accessibilityLabel2,
3288
+ testID
3289
+ }) {
3290
+ const { remainingMs, progress, label, done } = useTimer({ mode, durationMs, elapsedMs });
3291
+ const isDone = done && mode === "down";
3292
+ const showDone = isDone && doneLabel != null;
3293
+ const remainingFrac = durationMs > 0 ? Math.max(0, Math.min(1, remainingMs / durationMs)) : 0;
3294
+ const arc = mode === "down" ? showDone ? 1 : remainingFrac : progress;
3295
+ const ringColor = showDone ? doneColor : color;
3296
+ const remainingSec = Math.ceil(Math.max(0, remainingMs) / 1e3);
3297
+ const a11y = accessibilityLabel2 ?? (isDone ? "Timer complete" : `${remainingSec} seconds remaining`);
3298
+ return /* @__PURE__ */ jsxs(
3299
+ View,
3300
+ {
3301
+ style: { alignItems: "center", gap: 16 },
3302
+ accessibilityRole: "timer",
3303
+ accessibilityLabel: a11y,
3304
+ testID: testID ?? "circular-timer",
3305
+ children: [
3306
+ /* @__PURE__ */ jsx(
3307
+ CircularProgress,
3308
+ {
3309
+ value: arc,
3310
+ max: 1,
3311
+ size,
3312
+ strokeWidth,
3313
+ fill,
3314
+ color: ringColor,
3315
+ accessibilityLabel: "Timer ring",
3316
+ testID: "circular-timer-ring",
3317
+ children: /* @__PURE__ */ jsx(
3318
+ Text,
3319
+ {
3320
+ className: showDone ? void 0 : "text-text-primary",
3321
+ style: {
3322
+ fontSize: Math.round(size * (showDone ? 0.28 : 0.24)),
3323
+ fontFamily: '"Space Grotesk", sans-serif',
3324
+ fontWeight: "800",
3325
+ fontVariant: ["tabular-nums"],
3326
+ letterSpacing: showDone ? 1 : -1,
3327
+ ...showDone ? { color: colorVarMap[doneColor] } : null
3328
+ },
3329
+ testID: "circular-timer-label",
3330
+ children: showDone ? doneLabel : label
3331
+ }
3332
+ )
3333
+ }
3334
+ ),
3335
+ controls
3336
+ ]
3337
+ }
3338
+ );
3339
+ }
2994
3340
 
2995
3341
  // src/components/custom/Workout/RestTimer.tsx
2996
3342
  var BRAND_PRIMARY3 = getSemanticColors("dark")["brand-primary"];
3343
+ var RING_DEFAULT_SIZE = 180;
3344
+ function RestActions({ onAddTime, onSkip }) {
3345
+ return /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: 8 }, children: [
3346
+ /* @__PURE__ */ jsx(
3347
+ Pressable,
3348
+ {
3349
+ onPress: onAddTime,
3350
+ style: {
3351
+ backgroundColor: "rgba(255,255,255,0.06)",
3352
+ paddingVertical: 8,
3353
+ paddingHorizontal: 20,
3354
+ borderRadius: 8
3355
+ },
3356
+ accessibilityRole: "button",
3357
+ accessibilityLabel: "Add 30 seconds",
3358
+ testID: "rest-timer-add-time",
3359
+ children: /* @__PURE__ */ jsx(
3360
+ Text,
3361
+ {
3362
+ className: "text-text-secondary",
3363
+ style: { fontSize: 11, fontFamily: "Inter, sans-serif", fontWeight: "600" },
3364
+ children: "+30s"
3365
+ }
3366
+ )
3367
+ }
3368
+ ),
3369
+ /* @__PURE__ */ jsx(
3370
+ Pressable,
3371
+ {
3372
+ onPress: onSkip,
3373
+ style: {
3374
+ backgroundColor: "rgba(255,121,0,0.12)",
3375
+ paddingVertical: 8,
3376
+ paddingHorizontal: 20,
3377
+ borderRadius: 8
3378
+ },
3379
+ accessibilityRole: "button",
3380
+ accessibilityLabel: "Skip rest",
3381
+ testID: "rest-timer-skip",
3382
+ children: /* @__PURE__ */ jsx(
3383
+ Text,
3384
+ {
3385
+ style: {
3386
+ fontSize: 11,
3387
+ fontFamily: "Inter, sans-serif",
3388
+ fontWeight: "600",
3389
+ color: BRAND_PRIMARY3
3390
+ },
3391
+ children: "Skip"
3392
+ }
3393
+ )
3394
+ }
3395
+ )
3396
+ ] });
3397
+ }
3398
+ function RestTimerRing({
3399
+ totalSeconds,
3400
+ elapsedMs,
3401
+ remainingSec,
3402
+ done,
3403
+ size,
3404
+ nextSetInfo,
3405
+ displayOnly,
3406
+ onSkip,
3407
+ onAddTime
3408
+ }) {
3409
+ const a11yLabel = done ? "Rest complete, next set ready" : `Rest timer, ${Math.max(0, remainingSec)} seconds remaining`;
3410
+ return /* @__PURE__ */ jsxs(View, { style: { alignItems: "center", gap: 16 }, testID: "rest-timer", children: [
3411
+ /* @__PURE__ */ jsx(
3412
+ CircularTimer,
3413
+ {
3414
+ durationMs: totalSeconds * 1e3,
3415
+ elapsedMs,
3416
+ mode: "down",
3417
+ size,
3418
+ doneLabel: "GO",
3419
+ doneColor: "success",
3420
+ accessibilityLabel: a11yLabel,
3421
+ controls: !displayOnly ? /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip }) : void 0,
3422
+ testID: "rest-timer-ring"
3423
+ }
3424
+ ),
3425
+ nextSetInfo != null && /* @__PURE__ */ jsx(
3426
+ Text,
3427
+ {
3428
+ className: "text-text-tertiary",
3429
+ style: { fontSize: 13, fontFamily: "Inter, sans-serif" },
3430
+ testID: "rest-timer-next-set",
3431
+ children: nextSetInfo
3432
+ }
3433
+ )
3434
+ ] });
3435
+ }
2997
3436
  function RestTimer({
2998
3437
  totalSeconds,
2999
3438
  elapsedMs,
@@ -3001,12 +3440,15 @@ function RestTimer({
3001
3440
  onAddTime,
3002
3441
  nextSetInfo,
3003
3442
  visible,
3004
- displayOnly = false
3443
+ displayOnly = false,
3444
+ variant = "bar",
3445
+ size = RING_DEFAULT_SIZE
3005
3446
  }) {
3006
3447
  const {
3007
3448
  remainingMs,
3008
3449
  progress,
3009
- label: timeDisplay
3450
+ label: timeDisplay,
3451
+ done
3010
3452
  } = useTimer({
3011
3453
  mode: "down",
3012
3454
  durationMs: totalSeconds * 1e3,
@@ -3015,6 +3457,22 @@ function RestTimer({
3015
3457
  const remainingSec = Math.ceil(remainingMs / 1e3);
3016
3458
  const progressPct = progress * 100;
3017
3459
  if (!visible) return null;
3460
+ if (variant === "ring") {
3461
+ return /* @__PURE__ */ jsx(
3462
+ RestTimerRing,
3463
+ {
3464
+ totalSeconds,
3465
+ elapsedMs,
3466
+ remainingSec,
3467
+ done,
3468
+ size,
3469
+ nextSetInfo,
3470
+ displayOnly,
3471
+ onSkip,
3472
+ onAddTime
3473
+ }
3474
+ );
3475
+ }
3018
3476
  return /* @__PURE__ */ jsxs(
3019
3477
  View,
3020
3478
  {
@@ -3112,62 +3570,7 @@ function RestTimer({
3112
3570
  )
3113
3571
  }
3114
3572
  ),
3115
- !displayOnly && /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: 8 }, children: [
3116
- /* @__PURE__ */ jsx(
3117
- Pressable,
3118
- {
3119
- onPress: onAddTime,
3120
- style: {
3121
- backgroundColor: "rgba(255,255,255,0.06)",
3122
- paddingVertical: 8,
3123
- paddingHorizontal: 20,
3124
- borderRadius: 8
3125
- },
3126
- accessibilityRole: "button",
3127
- accessibilityLabel: "Add 30 seconds",
3128
- testID: "rest-timer-add-time",
3129
- children: /* @__PURE__ */ jsx(
3130
- Text,
3131
- {
3132
- className: "text-text-secondary",
3133
- style: {
3134
- fontSize: 11,
3135
- fontFamily: "Inter, sans-serif",
3136
- fontWeight: "600"
3137
- },
3138
- children: "+30s"
3139
- }
3140
- )
3141
- }
3142
- ),
3143
- /* @__PURE__ */ jsx(
3144
- Pressable,
3145
- {
3146
- onPress: onSkip,
3147
- style: {
3148
- backgroundColor: "rgba(255,121,0,0.12)",
3149
- paddingVertical: 8,
3150
- paddingHorizontal: 20,
3151
- borderRadius: 8
3152
- },
3153
- accessibilityRole: "button",
3154
- accessibilityLabel: "Skip rest",
3155
- testID: "rest-timer-skip",
3156
- children: /* @__PURE__ */ jsx(
3157
- Text,
3158
- {
3159
- style: {
3160
- fontSize: 11,
3161
- fontFamily: "Inter, sans-serif",
3162
- fontWeight: "600",
3163
- color: BRAND_PRIMARY3
3164
- },
3165
- children: "Skip"
3166
- }
3167
- )
3168
- }
3169
- )
3170
- ] })
3573
+ !displayOnly && /* @__PURE__ */ jsx(RestActions, { onAddTime, onSkip })
3171
3574
  ]
3172
3575
  }
3173
3576
  );