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