@wandelbots/wandelbots-js-react-components 2.44.0 → 2.45.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 (59) hide show
  1. package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -1
  2. package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -1
  3. package/dist/components/CycleTimer/index.d.ts +4 -5
  4. package/dist/components/CycleTimer/index.d.ts.map +1 -1
  5. package/dist/components/CycleTimer/types.d.ts +2 -3
  6. package/dist/components/CycleTimer/types.d.ts.map +1 -1
  7. package/dist/components/CycleTimer/useTimerLogic.d.ts +1 -2
  8. package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -1
  9. package/dist/components/ProgramControl.d.ts +7 -1
  10. package/dist/components/ProgramControl.d.ts.map +1 -1
  11. package/dist/components/ProgramStateIndicator.d.ts.map +1 -1
  12. package/dist/components/Timer/Timer.d.ts +3 -0
  13. package/dist/components/Timer/Timer.d.ts.map +1 -0
  14. package/dist/components/Timer/TimerDefaultVariant.d.ts +10 -0
  15. package/dist/components/Timer/TimerDefaultVariant.d.ts.map +1 -0
  16. package/dist/components/Timer/TimerSmallVariant.d.ts +11 -0
  17. package/dist/components/Timer/TimerSmallVariant.d.ts.map +1 -0
  18. package/dist/components/Timer/index.d.ts +19 -0
  19. package/dist/components/Timer/index.d.ts.map +1 -0
  20. package/dist/components/Timer/types.d.ts +36 -0
  21. package/dist/components/Timer/types.d.ts.map +1 -0
  22. package/dist/components/Timer/useTimerAnimations.d.ts +11 -0
  23. package/dist/components/Timer/useTimerAnimations.d.ts.map +1 -0
  24. package/dist/components/Timer/useTimerLogic.d.ts +20 -0
  25. package/dist/components/Timer/useTimerLogic.d.ts.map +1 -0
  26. package/dist/components/Timer/utils.d.ts +9 -0
  27. package/dist/components/Timer/utils.d.ts.map +1 -0
  28. package/dist/components/jogging/PoseCartesianValues.d.ts +3 -5
  29. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  30. package/dist/components/jogging/PoseJointValues.d.ts +3 -5
  31. package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
  32. package/dist/index.cjs +48 -48
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6342 -5942
  37. package/dist/index.js.map +1 -1
  38. package/package.json +5 -5
  39. package/src/components/CycleTimer/DefaultVariant.tsx +0 -2
  40. package/src/components/CycleTimer/SmallVariant.tsx +2 -5
  41. package/src/components/CycleTimer/index.tsx +4 -5
  42. package/src/components/CycleTimer/types.ts +1 -3
  43. package/src/components/CycleTimer/useTimerLogic.ts +40 -96
  44. package/src/components/CycleTimer/utils.ts +3 -3
  45. package/src/components/ProgramControl.tsx +31 -3
  46. package/src/components/ProgramStateIndicator.tsx +31 -1
  47. package/src/components/Timer/Timer.ts +2 -0
  48. package/src/components/Timer/TimerDefaultVariant.tsx +140 -0
  49. package/src/components/Timer/TimerSmallVariant.tsx +140 -0
  50. package/src/components/Timer/index.tsx +101 -0
  51. package/src/components/Timer/types.ts +38 -0
  52. package/src/components/Timer/useTimerAnimations.ts +94 -0
  53. package/src/components/Timer/useTimerLogic.ts +214 -0
  54. package/src/components/Timer/utils.ts +15 -0
  55. package/src/components/jogging/PoseCartesianValues.tsx +16 -82
  56. package/src/components/jogging/PoseJointValues.tsx +16 -82
  57. package/src/i18n/locales/de/translations.json +7 -0
  58. package/src/i18n/locales/en/translations.json +7 -0
  59. package/src/index.ts +1 -0
@@ -0,0 +1,140 @@
1
+ import { Box, Typography } from "@mui/material"
2
+ import { useTheme } from "@mui/material/styles"
3
+ import { useTranslation } from "react-i18next"
4
+ import type { TimerState, TimerAnimationState } from "./types"
5
+ import { formatTime } from "./utils"
6
+
7
+ interface TimerSmallVariantProps {
8
+ timerState: TimerState
9
+ animationState: TimerAnimationState
10
+ hasError: boolean
11
+ compact: boolean
12
+ className?: string
13
+ }
14
+
15
+ export const TimerSmallVariant = ({
16
+ timerState,
17
+ animationState,
18
+ hasError,
19
+ compact,
20
+ className,
21
+ }: TimerSmallVariantProps) => {
22
+ const { t } = useTranslation()
23
+ const theme = useTheme()
24
+ const { elapsedTime, currentProgress } = timerState
25
+ const { showErrorAnimation, showPauseAnimation } = animationState
26
+
27
+ // Simple text-only mode for compact variant
28
+ if (compact) {
29
+ return (
30
+ <Box
31
+ className={className}
32
+ sx={{
33
+ display: "flex",
34
+ alignItems: "center",
35
+ m: 0,
36
+ }}
37
+ >
38
+ <Typography
39
+ variant="body2"
40
+ sx={{
41
+ color: hasError
42
+ ? theme.palette.error.light
43
+ : theme.palette.text.primary,
44
+ fontSize: "14px",
45
+ transition: "color 0.5s ease-out",
46
+ }}
47
+ >
48
+ {hasError ? t("timer.error") : formatTime(elapsedTime)}
49
+ </Typography>
50
+ </Box>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <Box
56
+ className={className}
57
+ sx={{
58
+ display: "flex",
59
+ alignItems: "center",
60
+ m: 0,
61
+ gap: 1,
62
+ }}
63
+ >
64
+ {/* Animated progress ring icon */}
65
+ <Box
66
+ sx={{
67
+ width: 20,
68
+ height: 20,
69
+ display: "flex",
70
+ alignItems: "center",
71
+ justifyContent: "center",
72
+ opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
73
+ transition: "opacity 0.5s ease-out",
74
+ }}
75
+ >
76
+ <svg
77
+ width="20"
78
+ height="20"
79
+ viewBox="0 0 20 20"
80
+ style={{ transform: "rotate(-90deg)" }}
81
+ role="img"
82
+ aria-label="Timer progress"
83
+ >
84
+ <circle
85
+ cx="10"
86
+ cy="10"
87
+ r="8"
88
+ fill="none"
89
+ stroke={
90
+ hasError
91
+ ? theme.palette.error.light
92
+ : theme.palette.success.main
93
+ }
94
+ strokeWidth="2"
95
+ opacity={0.3}
96
+ style={{
97
+ transition: "stroke 0.8s ease-in-out, opacity 2s ease-in-out",
98
+ }}
99
+ />
100
+ <circle
101
+ cx="10"
102
+ cy="10"
103
+ r="8"
104
+ fill="none"
105
+ stroke={
106
+ hasError
107
+ ? theme.palette.error.light
108
+ : theme.palette.success.main
109
+ }
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ strokeDasharray={`${2 * Math.PI * 8}`}
113
+ strokeDashoffset={`${2 * Math.PI * 8 * (1 - currentProgress / 100)}`}
114
+ style={{
115
+ transition:
116
+ "stroke-dashoffset 0.1s ease-out, stroke 0.8s ease-in-out, opacity 2s ease-in-out",
117
+ }}
118
+ />
119
+ </svg>
120
+ </Box>
121
+
122
+ {/* Timer text display */}
123
+ <Typography
124
+ variant="body2"
125
+ sx={{
126
+ color: hasError
127
+ ? theme.palette.error.light
128
+ : theme.palette.text.primary,
129
+ fontSize: "14px",
130
+ lineHeight: "normal",
131
+ letterSpacing: "normal",
132
+ transition:
133
+ "color 0.8s ease-in-out, font-size 0.3s ease-out, opacity 2s ease-in-out",
134
+ }}
135
+ >
136
+ {hasError ? t("timer.error") : formatTime(elapsedTime)}
137
+ </Typography>
138
+ </Box>
139
+ )
140
+ }
@@ -0,0 +1,101 @@
1
+ import { observer } from "mobx-react-lite"
2
+ import { useEffect } from "react"
3
+ import { externalizeComponent } from "../../externalizeComponent"
4
+ import { TimerDefaultVariant } from "./TimerDefaultVariant"
5
+ import { TimerSmallVariant } from "./TimerSmallVariant"
6
+ import type { TimerProps } from "./types"
7
+ import { useTimerAnimations } from "./useTimerAnimations"
8
+ import { useTimerLogic } from "./useTimerLogic"
9
+
10
+ /**
11
+ * A simple count-up timer component with visual progress indication
12
+ *
13
+ * Features:
14
+ * - Count-up timer that tracks elapsed time
15
+ * - Visual progress gauge that cycles every minute
16
+ * - Two display variants: large circular gauge (default) or small icon with text
17
+ * - Full timer control: start, pause, resume, reset functionality
18
+ * - Support for starting with elapsed time (resume mid-session)
19
+ * - Error state support: pauses timer and shows error styling
20
+ * - Smooth progress animations with spring physics
21
+ * - Fully localized with i18next
22
+ * - Material-UI theming integration
23
+ */
24
+ export const Timer = externalizeComponent(
25
+ observer(
26
+ ({
27
+ onTimerReady,
28
+ autoStart = true,
29
+ variant = "default",
30
+ compact = false,
31
+ className,
32
+ hasError = false,
33
+ }: TimerProps) => {
34
+ // Initialize animation hooks
35
+ const {
36
+ animationState,
37
+ triggerPauseAnimation,
38
+ triggerErrorAnimation,
39
+ clearErrorAnimation,
40
+ setInitialAnimationState,
41
+ cleanup,
42
+ } = useTimerAnimations()
43
+
44
+ // Initialize timer logic
45
+ const { timerState, controls } = useTimerLogic({
46
+ autoStart,
47
+ hasError,
48
+ onPauseAnimation: triggerPauseAnimation,
49
+ onErrorAnimation: triggerErrorAnimation,
50
+ onClearErrorAnimation: clearErrorAnimation,
51
+ })
52
+
53
+ // Set initial animation state
54
+ useEffect(() => {
55
+ setInitialAnimationState()
56
+ }, [setInitialAnimationState])
57
+
58
+ // Provide controls to parent component
59
+ useEffect(() => {
60
+ let isMounted = true
61
+ const timeoutId = setTimeout(() => {
62
+ if (isMounted) {
63
+ onTimerReady(controls)
64
+ }
65
+ }, 0)
66
+
67
+ return () => {
68
+ isMounted = false
69
+ clearTimeout(timeoutId)
70
+ }
71
+ }, [onTimerReady, controls])
72
+
73
+ // Cleanup on unmount
74
+ useEffect(() => {
75
+ return cleanup
76
+ }, [cleanup])
77
+
78
+ // Render appropriate variant
79
+ if (variant === "small") {
80
+ return (
81
+ <TimerSmallVariant
82
+ timerState={timerState}
83
+ animationState={animationState}
84
+ hasError={hasError}
85
+ compact={compact}
86
+ className={className}
87
+ />
88
+ )
89
+ }
90
+
91
+ return (
92
+ <TimerDefaultVariant
93
+ timerState={timerState}
94
+ animationState={animationState}
95
+ hasError={hasError}
96
+ className={className}
97
+ />
98
+ )
99
+ },
100
+ ),
101
+ )
@@ -0,0 +1,38 @@
1
+ export interface TimerControls {
2
+ start: (elapsedSeconds?: number) => void
3
+ pause: () => void
4
+ resume: () => void
5
+ reset: () => void
6
+ isPaused: () => boolean
7
+ }
8
+
9
+ export interface TimerProps {
10
+ /**
11
+ * Callback that receives the timer control functions
12
+ */
13
+ onTimerReady: (controls: TimerControls) => void
14
+ /** Whether the timer should start automatically when initialized */
15
+ autoStart?: boolean
16
+ /** Visual variant of the timer */
17
+ variant?: "default" | "small"
18
+ /** For small variant: whether to show compact display */
19
+ compact?: boolean
20
+ /** Additional CSS classes */
21
+ className?: string
22
+ /** Whether the timer is in an error state (pauses timer and shows error styling) */
23
+ hasError?: boolean
24
+ }
25
+
26
+ export interface TimerState {
27
+ elapsedTime: number
28
+ isRunning: boolean
29
+ isPausedState: boolean
30
+ currentProgress: number
31
+ wasRunningBeforeError: boolean
32
+ }
33
+
34
+ export interface TimerAnimationState {
35
+ showPauseAnimation: boolean
36
+ showErrorAnimation: boolean
37
+ showMainText: boolean
38
+ }
@@ -0,0 +1,94 @@
1
+ import { useCallback, useRef, useState } from "react"
2
+ import type { TimerAnimationState } from "./types"
3
+
4
+ export const useTimerAnimations = () => {
5
+ const [animationState, setAnimationState] = useState<TimerAnimationState>({
6
+ showPauseAnimation: false,
7
+ showErrorAnimation: false,
8
+ showMainText: true,
9
+ })
10
+
11
+ // Refs for managing timeouts
12
+ const pauseAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
13
+ const errorAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
14
+ const fadeTimeoutRef = useRef<NodeJS.Timeout | null>(null)
15
+
16
+ const triggerPauseAnimation = useCallback(() => {
17
+ setAnimationState((prev) => ({ ...prev, showPauseAnimation: true }))
18
+
19
+ if (pauseAnimationTimeoutRef.current) {
20
+ clearTimeout(pauseAnimationTimeoutRef.current)
21
+ }
22
+
23
+ pauseAnimationTimeoutRef.current = setTimeout(() => {
24
+ setAnimationState((prev) => ({ ...prev, showPauseAnimation: false }))
25
+ }, 800)
26
+ }, [])
27
+
28
+ const triggerErrorAnimation = useCallback(() => {
29
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: true }))
30
+
31
+ if (errorAnimationTimeoutRef.current) {
32
+ clearTimeout(errorAnimationTimeoutRef.current)
33
+ }
34
+
35
+ errorAnimationTimeoutRef.current = setTimeout(() => {
36
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: false }))
37
+ }, 600)
38
+ }, [])
39
+
40
+ const clearErrorAnimation = useCallback(() => {
41
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: false }))
42
+ if (errorAnimationTimeoutRef.current) {
43
+ clearTimeout(errorAnimationTimeoutRef.current)
44
+ }
45
+ }, [])
46
+
47
+ const triggerFadeTransition = useCallback(() => {
48
+ setAnimationState((prev) => ({
49
+ ...prev,
50
+ showMainText: false,
51
+ }))
52
+
53
+ if (fadeTimeoutRef.current) {
54
+ clearTimeout(fadeTimeoutRef.current)
55
+ }
56
+
57
+ fadeTimeoutRef.current = setTimeout(() => {
58
+ setAnimationState((prev) => ({
59
+ ...prev,
60
+ showMainText: true,
61
+ }))
62
+ }, 200)
63
+ }, [])
64
+
65
+ const setInitialAnimationState = useCallback(() => {
66
+ setAnimationState((prev) => ({
67
+ ...prev,
68
+ showMainText: true,
69
+ }))
70
+ }, [])
71
+
72
+ // Cleanup function
73
+ const cleanup = useCallback(() => {
74
+ if (pauseAnimationTimeoutRef.current) {
75
+ clearTimeout(pauseAnimationTimeoutRef.current)
76
+ }
77
+ if (errorAnimationTimeoutRef.current) {
78
+ clearTimeout(errorAnimationTimeoutRef.current)
79
+ }
80
+ if (fadeTimeoutRef.current) {
81
+ clearTimeout(fadeTimeoutRef.current)
82
+ }
83
+ }, [])
84
+
85
+ return {
86
+ animationState,
87
+ triggerPauseAnimation,
88
+ triggerErrorAnimation,
89
+ clearErrorAnimation,
90
+ triggerFadeTransition,
91
+ setInitialAnimationState,
92
+ cleanup,
93
+ }
94
+ }
@@ -0,0 +1,214 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+ import { useInterpolation } from "../utils/interpolation"
3
+ import type { TimerState } from "./types"
4
+
5
+ interface UseTimerLogicProps {
6
+ autoStart: boolean
7
+ hasError: boolean
8
+ onPauseAnimation: () => void
9
+ onErrorAnimation: () => void
10
+ onClearErrorAnimation: () => void
11
+ }
12
+
13
+ export const useTimerLogic = ({
14
+ autoStart,
15
+ hasError,
16
+ onPauseAnimation,
17
+ onErrorAnimation,
18
+ onClearErrorAnimation,
19
+ }: UseTimerLogicProps) => {
20
+ const [timerState, setTimerState] = useState<TimerState>({
21
+ elapsedTime: 0,
22
+ isRunning: false,
23
+ isPausedState: false,
24
+ currentProgress: 0,
25
+ wasRunningBeforeError: false,
26
+ })
27
+
28
+ // Timer-related refs
29
+ const animationRef = useRef<number | null>(null)
30
+ const startTimeRef = useRef<number | null>(null)
31
+ const pausedTimeRef = useRef<number>(0)
32
+ const lastProgressRef = useRef<number>(0)
33
+
34
+ // Spring-based interpolator for smooth gauge progress animations
35
+ const [progressInterpolator] = useInterpolation([0], {
36
+ tension: 80,
37
+ friction: 18,
38
+ onChange: ([progress]) => {
39
+ setTimerState((prev) => ({ ...prev, currentProgress: progress }))
40
+ },
41
+ })
42
+
43
+ const start = useCallback(
44
+ (elapsedSeconds: number = 0) => {
45
+ const initialProgress = ((elapsedSeconds / 60) % 1) * 100
46
+ setTimerState((prev) => ({
47
+ ...prev,
48
+ elapsedTime: elapsedSeconds,
49
+ isPausedState: false,
50
+ currentProgress: initialProgress,
51
+ }))
52
+ pausedTimeRef.current = 0
53
+ lastProgressRef.current = initialProgress
54
+
55
+ progressInterpolator.setImmediate([initialProgress])
56
+
57
+ if (autoStart) {
58
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
59
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
60
+ } else {
61
+ startTimeRef.current = null
62
+ }
63
+ },
64
+ [autoStart, progressInterpolator],
65
+ )
66
+
67
+ const pause = useCallback(() => {
68
+ if (startTimeRef.current && timerState.isRunning) {
69
+ const now = Date.now()
70
+ const totalElapsed = (now - startTimeRef.current) / 1000 + pausedTimeRef.current
71
+ const currentProgress = ((totalElapsed / 60) % 1) * 100
72
+ progressInterpolator.setTarget([currentProgress])
73
+
74
+ setTimerState((prev) => ({
75
+ ...prev,
76
+ elapsedTime: Math.floor(totalElapsed),
77
+ }))
78
+ }
79
+
80
+ setTimerState((prev) => ({
81
+ ...prev,
82
+ isRunning: false,
83
+ isPausedState: true,
84
+ }))
85
+ onPauseAnimation()
86
+ }, [
87
+ timerState.isRunning,
88
+ progressInterpolator,
89
+ onPauseAnimation,
90
+ ])
91
+
92
+ const resume = useCallback(() => {
93
+ if (timerState.isPausedState) {
94
+ pausedTimeRef.current = timerState.elapsedTime
95
+ startTimeRef.current = Date.now()
96
+ setTimerState((prev) => ({
97
+ ...prev,
98
+ isRunning: true,
99
+ isPausedState: false,
100
+ }))
101
+ }
102
+ }, [timerState.isPausedState, timerState.elapsedTime])
103
+
104
+ const reset = useCallback(() => {
105
+ setTimerState((prev) => ({
106
+ ...prev,
107
+ elapsedTime: 0,
108
+ isRunning: false,
109
+ isPausedState: false,
110
+ currentProgress: 0,
111
+ }))
112
+ pausedTimeRef.current = 0
113
+ startTimeRef.current = null
114
+ lastProgressRef.current = 0
115
+ progressInterpolator.setImmediate([0])
116
+ }, [progressInterpolator])
117
+
118
+ const isPaused = useCallback(() => {
119
+ return timerState.isPausedState
120
+ }, [timerState.isPausedState])
121
+
122
+ // Handle error state changes
123
+ useEffect(() => {
124
+ if (hasError) {
125
+ if (timerState.isRunning) {
126
+ setTimerState((prev) => ({ ...prev, wasRunningBeforeError: true }))
127
+ pause()
128
+ }
129
+ onErrorAnimation()
130
+ } else {
131
+ if (timerState.wasRunningBeforeError && !timerState.isRunning) {
132
+ setTimerState((prev) => ({ ...prev, wasRunningBeforeError: false }))
133
+ resume()
134
+ }
135
+ onClearErrorAnimation()
136
+ }
137
+ }, [
138
+ hasError,
139
+ timerState.isRunning,
140
+ timerState.wasRunningBeforeError,
141
+ pause,
142
+ resume,
143
+ onErrorAnimation,
144
+ onClearErrorAnimation,
145
+ ])
146
+
147
+ // Main timer loop
148
+ useEffect(() => {
149
+ if (timerState.isRunning) {
150
+ const updateTimer = () => {
151
+ if (startTimeRef.current) {
152
+ const now = Date.now()
153
+ const totalElapsed = (now - startTimeRef.current) / 1000 + pausedTimeRef.current
154
+ const currentProgress = ((totalElapsed / 60) % 1) * 100
155
+
156
+ setTimerState((prev) => ({
157
+ ...prev,
158
+ elapsedTime: Math.floor(totalElapsed),
159
+ }))
160
+
161
+ // Only update progress interpolator if progress changed significantly
162
+ const progressDiff = Math.abs(currentProgress - lastProgressRef.current)
163
+ if (progressDiff > 0.1) {
164
+ progressInterpolator.setTarget([currentProgress])
165
+ lastProgressRef.current = currentProgress
166
+ }
167
+ }
168
+ animationRef.current = requestAnimationFrame(updateTimer)
169
+ }
170
+ animationRef.current = requestAnimationFrame(updateTimer)
171
+ } else {
172
+ if (animationRef.current) {
173
+ cancelAnimationFrame(animationRef.current)
174
+ animationRef.current = null
175
+ }
176
+ }
177
+
178
+ return () => {
179
+ if (animationRef.current) {
180
+ cancelAnimationFrame(animationRef.current)
181
+ animationRef.current = null
182
+ }
183
+ }
184
+ }, [timerState.isRunning, progressInterpolator])
185
+
186
+ // Interpolation animation loop
187
+ useEffect(() => {
188
+ let interpolationAnimationId: number | null = null
189
+
190
+ const animateInterpolation = () => {
191
+ progressInterpolator.update()
192
+ interpolationAnimationId = requestAnimationFrame(animateInterpolation)
193
+ }
194
+
195
+ interpolationAnimationId = requestAnimationFrame(animateInterpolation)
196
+
197
+ return () => {
198
+ if (interpolationAnimationId) {
199
+ cancelAnimationFrame(interpolationAnimationId)
200
+ }
201
+ }
202
+ }, [progressInterpolator])
203
+
204
+ return {
205
+ timerState,
206
+ controls: {
207
+ start,
208
+ pause,
209
+ resume,
210
+ reset,
211
+ isPaused,
212
+ },
213
+ }
214
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Formats time in seconds to MM:SS format
3
+ */
4
+ export const formatTime = (seconds: number): string => {
5
+ const minutes = Math.floor(seconds / 60)
6
+ const remainingSeconds = seconds % 60
7
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
8
+ }
9
+
10
+ /**
11
+ * Calculates progress percentage for timer (minute-based cycles)
12
+ */
13
+ export const calculateTimerProgress = (elapsedTime: number): number => {
14
+ return ((elapsedTime / 60) % 1) * 100
15
+ }