@wandelbots/wandelbots-js-react-components 2.39.0-pr.feature-collision-scene-renderer.382.c9d9121 → 2.40.0-pr.feature-seperate-timer.383.c9c6f2b
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/components/CycleTimer/DefaultVariant.d.ts.map +1 -1
- package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -1
- package/dist/components/CycleTimer/index.d.ts +4 -5
- package/dist/components/CycleTimer/index.d.ts.map +1 -1
- package/dist/components/CycleTimer/types.d.ts +2 -3
- package/dist/components/CycleTimer/types.d.ts.map +1 -1
- package/dist/components/CycleTimer/useTimerLogic.d.ts +1 -2
- package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -1
- package/dist/components/Timer/Timer.d.ts +3 -0
- package/dist/components/Timer/Timer.d.ts.map +1 -0
- package/dist/components/Timer/TimerDefaultVariant.d.ts +10 -0
- package/dist/components/Timer/TimerDefaultVariant.d.ts.map +1 -0
- package/dist/components/Timer/TimerSmallVariant.d.ts +11 -0
- package/dist/components/Timer/TimerSmallVariant.d.ts.map +1 -0
- package/dist/components/Timer/index.d.ts +19 -0
- package/dist/components/Timer/index.d.ts.map +1 -0
- package/dist/components/Timer/types.d.ts +36 -0
- package/dist/components/Timer/types.d.ts.map +1 -0
- package/dist/components/Timer/useTimerAnimations.d.ts +11 -0
- package/dist/components/Timer/useTimerAnimations.d.ts.map +1 -0
- package/dist/components/Timer/useTimerLogic.d.ts +20 -0
- package/dist/components/Timer/useTimerLogic.d.ts.map +1 -0
- package/dist/components/Timer/utils.d.ts +9 -0
- package/dist/components/Timer/utils.d.ts.map +1 -0
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5228 -4808
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CycleTimer/DefaultVariant.tsx +0 -2
- package/src/components/CycleTimer/SmallVariant.tsx +2 -5
- package/src/components/CycleTimer/index.tsx +4 -5
- package/src/components/CycleTimer/types.ts +1 -3
- package/src/components/CycleTimer/useTimerLogic.ts +27 -82
- package/src/components/CycleTimer/utils.ts +3 -3
- package/src/components/Timer/Timer.ts +2 -0
- package/src/components/Timer/TimerDefaultVariant.tsx +140 -0
- package/src/components/Timer/TimerSmallVariant.tsx +140 -0
- package/src/components/Timer/index.tsx +101 -0
- package/src/components/Timer/types.ts +38 -0
- package/src/components/Timer/useTimerAnimations.ts +94 -0
- package/src/components/Timer/useTimerLogic.ts +214 -0
- package/src/components/Timer/utils.ts +15 -0
- package/src/i18n/locales/de/translations.json +1 -0
- package/src/i18n/locales/en/translations.json +1 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wandelbots/wandelbots-js-react-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.0-pr.feature-seperate-timer.383.c9c6f2b",
|
|
4
4
|
"description": "React UI toolkit for building applications on top of the Wandelbots platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -114,7 +114,6 @@ export const DefaultVariant = ({
|
|
|
114
114
|
showLabels &&
|
|
115
115
|
!hasError &&
|
|
116
116
|
currentState !== "idle" &&
|
|
117
|
-
currentState !== "countup" &&
|
|
118
117
|
currentState !== "success"
|
|
119
118
|
}
|
|
120
119
|
timeout={300}
|
|
@@ -277,7 +276,6 @@ export const DefaultVariant = ({
|
|
|
277
276
|
showLabels &&
|
|
278
277
|
!hasError &&
|
|
279
278
|
currentState !== "idle" &&
|
|
280
|
-
currentState !== "countup" &&
|
|
281
279
|
currentState !== "success"
|
|
282
280
|
}
|
|
283
281
|
timeout={300}
|
|
@@ -31,7 +31,7 @@ export const SmallVariant = ({
|
|
|
31
31
|
} = animationState
|
|
32
32
|
|
|
33
33
|
// Simple text-only mode for compact variant in certain states
|
|
34
|
-
if (compact &&
|
|
34
|
+
if (compact && currentState === "idle") {
|
|
35
35
|
return (
|
|
36
36
|
<Box
|
|
37
37
|
className={className}
|
|
@@ -72,10 +72,7 @@ export const SmallVariant = ({
|
|
|
72
72
|
}}
|
|
73
73
|
>
|
|
74
74
|
{/* Animated progress ring icon */}
|
|
75
|
-
{!(
|
|
76
|
-
currentState === "countup" ||
|
|
77
|
-
(currentState === "idle" && compact)
|
|
78
|
-
) && (
|
|
75
|
+
{!(currentState === "idle" && compact) && (
|
|
79
76
|
<Box
|
|
80
77
|
sx={{
|
|
81
78
|
width: 20,
|
|
@@ -8,19 +8,18 @@ import { useAnimations } from "./useAnimations"
|
|
|
8
8
|
import { useTimerLogic } from "./useTimerLogic"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* A circular gauge timer component
|
|
11
|
+
* A circular gauge timer component for cycle-specific timing operations
|
|
12
12
|
*
|
|
13
13
|
* Features:
|
|
14
14
|
* - Custom SVG circular gauge with 264px diameter and 40px thickness
|
|
15
|
-
* - Multiple states: idle, measuring, measured, countdown,
|
|
15
|
+
* - Multiple states: idle, measuring, measured, countdown, success
|
|
16
16
|
* - Idle state: shows "Waiting for program cycle" with transparent inner circle
|
|
17
17
|
* - Measuring state: counts up with "Cycle Time" / "measuring..." labels
|
|
18
18
|
* - Measured state: shows final time with "Cycle Time" / "determined" labels in pulsating green
|
|
19
19
|
* - Countdown mode: shows remaining time prominently, counts down to zero
|
|
20
|
-
* - Count-up mode: shows elapsed time without special labels
|
|
21
20
|
* - Success state: brief green flash after cycle completion
|
|
22
21
|
* - Displays appropriate labels based on state
|
|
23
|
-
* - Automatically counts down
|
|
22
|
+
* - Automatically counts down and triggers callback when reaching zero
|
|
24
23
|
* - Full timer control: start, pause, resume functionality
|
|
25
24
|
* - Support for starting with elapsed time (resume mid-cycle)
|
|
26
25
|
* - Error state support: pauses timer and shows error styling (red color)
|
|
@@ -28,7 +27,7 @@ import { useTimerLogic } from "./useTimerLogic"
|
|
|
28
27
|
* - Pulsating text animation for completed measuring state
|
|
29
28
|
* - Fully localized with i18next
|
|
30
29
|
* - Material-UI theming integration
|
|
31
|
-
* - Small variant with animated progress icon (gauge border only) next to text
|
|
30
|
+
* - Small variant with animated progress icon (gauge border only) next to text
|
|
32
31
|
*/
|
|
33
32
|
export const CycleTimer = externalizeComponent(
|
|
34
33
|
observer(
|
|
@@ -3,13 +3,11 @@ export type CycleTimerState =
|
|
|
3
3
|
| "measuring" // Counting up without max time, showing "Cycle Time" / "measuring..."
|
|
4
4
|
| "measured" // Completed measuring state showing "Cycle Time" / "determined" with pulsating green text
|
|
5
5
|
| "countdown" // Counting down with max time
|
|
6
|
-
| "countup" // Simple count up without special text
|
|
7
6
|
| "success" // Brief success state after cycle completion
|
|
8
7
|
|
|
9
8
|
export interface CycleTimerControls {
|
|
10
|
-
startNewCycle: (maxTimeSeconds
|
|
9
|
+
startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
|
|
11
10
|
startMeasuring: (elapsedSeconds?: number) => void
|
|
12
|
-
startCountUp: (elapsedSeconds?: number) => void
|
|
13
11
|
setIdle: () => void
|
|
14
12
|
completeMeasuring: () => void
|
|
15
13
|
pause: () => void
|
|
@@ -88,93 +88,46 @@ export const useTimerLogic = ({
|
|
|
88
88
|
[autoStart, progressInterpolator],
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
const startCountUp = useCallback(
|
|
92
|
-
(elapsedSeconds: number = 0) => {
|
|
93
|
-
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
94
|
-
setTimerState((prev) => ({
|
|
95
|
-
...prev,
|
|
96
|
-
currentState: "countup",
|
|
97
|
-
maxTime: null,
|
|
98
|
-
remainingTime: elapsedSeconds,
|
|
99
|
-
isPausedState: false,
|
|
100
|
-
currentProgress: initialProgress, // Immediately set progress
|
|
101
|
-
}))
|
|
102
|
-
pausedTimeRef.current = 0
|
|
103
|
-
|
|
104
|
-
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
105
|
-
|
|
106
|
-
if (autoStart) {
|
|
107
|
-
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
108
|
-
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
109
|
-
} else {
|
|
110
|
-
startTimeRef.current = null
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
[autoStart, progressInterpolator],
|
|
114
|
-
)
|
|
115
|
-
|
|
116
91
|
const startNewCycle = useCallback(
|
|
117
|
-
(maxTimeSeconds
|
|
92
|
+
(maxTimeSeconds: number, elapsedSeconds: number = 0) => {
|
|
118
93
|
// Stop any running timer first to prevent conflicts
|
|
119
94
|
setTimerState((prev) => ({ ...prev, isRunning: false }))
|
|
120
95
|
startTimeRef.current = null
|
|
121
96
|
|
|
122
|
-
const newState = maxTimeSeconds !== undefined ? "countdown" : "countup"
|
|
123
97
|
setTimerState((prev) => ({
|
|
124
98
|
...prev,
|
|
125
|
-
currentState:
|
|
126
|
-
maxTime: maxTimeSeconds
|
|
99
|
+
currentState: "countdown",
|
|
100
|
+
maxTime: maxTimeSeconds,
|
|
127
101
|
isPausedState: false,
|
|
128
102
|
}))
|
|
129
103
|
pausedTimeRef.current = 0
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
queueMicrotask(() => onCycleEnd())
|
|
150
|
-
}
|
|
151
|
-
} else if (autoStart) {
|
|
152
|
-
setTimeout(() => {
|
|
153
|
-
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
154
|
-
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
155
|
-
}, 0)
|
|
156
|
-
} else {
|
|
157
|
-
startTimeRef.current = null
|
|
105
|
+
// Count-down mode
|
|
106
|
+
const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
|
|
107
|
+
const initialProgress =
|
|
108
|
+
elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
|
|
109
|
+
|
|
110
|
+
setTimerState((prev) => ({
|
|
111
|
+
...prev,
|
|
112
|
+
remainingTime: remainingSeconds,
|
|
113
|
+
currentProgress: initialProgress, // Immediately set progress
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
117
|
+
|
|
118
|
+
if (remainingSeconds === 0) {
|
|
119
|
+
setTimerState((prev) => ({ ...prev, isRunning: false }))
|
|
120
|
+
startTimeRef.current = null
|
|
121
|
+
if (onCycleEnd) {
|
|
122
|
+
queueMicrotask(() => onCycleEnd())
|
|
158
123
|
}
|
|
124
|
+
} else if (autoStart) {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
127
|
+
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
128
|
+
}, 0)
|
|
159
129
|
} else {
|
|
160
|
-
|
|
161
|
-
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
162
|
-
setTimerState((prev) => ({
|
|
163
|
-
...prev,
|
|
164
|
-
remainingTime: elapsedSeconds,
|
|
165
|
-
currentProgress: initialProgress, // Immediately set progress
|
|
166
|
-
}))
|
|
167
|
-
|
|
168
|
-
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
169
|
-
|
|
170
|
-
if (autoStart) {
|
|
171
|
-
setTimeout(() => {
|
|
172
|
-
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
173
|
-
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
174
|
-
}, 0)
|
|
175
|
-
} else {
|
|
176
|
-
startTimeRef.current = null
|
|
177
|
-
}
|
|
130
|
+
startTimeRef.current = null
|
|
178
131
|
}
|
|
179
132
|
},
|
|
180
133
|
[autoStart, onCycleEnd, progressInterpolator],
|
|
@@ -316,13 +269,6 @@ export const useTimerLogic = ({
|
|
|
316
269
|
}))
|
|
317
270
|
const progress = ((elapsed / 60) % 1) * 100
|
|
318
271
|
progressInterpolator.setTarget([progress])
|
|
319
|
-
} else if (timerState.currentState === "countup") {
|
|
320
|
-
setTimerState((prev) => ({
|
|
321
|
-
...prev,
|
|
322
|
-
remainingTime: Math.floor(elapsed),
|
|
323
|
-
}))
|
|
324
|
-
const progress = ((elapsed / 60) % 1) * 100
|
|
325
|
-
progressInterpolator.setTarget([progress])
|
|
326
272
|
}
|
|
327
273
|
|
|
328
274
|
if (timerState.isRunning) {
|
|
@@ -375,7 +321,6 @@ export const useTimerLogic = ({
|
|
|
375
321
|
controls: {
|
|
376
322
|
startNewCycle,
|
|
377
323
|
startMeasuring,
|
|
378
|
-
startCountUp,
|
|
379
324
|
setIdle,
|
|
380
325
|
completeMeasuring,
|
|
381
326
|
pause,
|
|
@@ -25,8 +25,8 @@ export const calculateProgress = (
|
|
|
25
25
|
return Math.min(100, (elapsed / maxTime) * 100)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (currentState === "measuring"
|
|
29
|
-
//
|
|
28
|
+
if (currentState === "measuring") {
|
|
29
|
+
// Measuring mode: progress based on minute steps (0-100% per minute)
|
|
30
30
|
return ((remainingTime / 60) % 1) * 100
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -45,7 +45,7 @@ export const calculateExactProgress = (
|
|
|
45
45
|
return Math.min(100, (totalElapsed / maxTime) * 100)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (currentState === "measuring"
|
|
48
|
+
if (currentState === "measuring") {
|
|
49
49
|
return ((totalElapsed / 60) % 1) * 100
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Box, Fade, Typography } from "@mui/material"
|
|
2
|
+
import { Gauge } from "@mui/x-charts"
|
|
3
|
+
import { useTheme } from "@mui/material/styles"
|
|
4
|
+
import { useTranslation } from "react-i18next"
|
|
5
|
+
import type { TimerState, TimerAnimationState } from "./types"
|
|
6
|
+
import { formatTime } from "./utils"
|
|
7
|
+
|
|
8
|
+
interface TimerDefaultVariantProps {
|
|
9
|
+
timerState: TimerState
|
|
10
|
+
animationState: TimerAnimationState
|
|
11
|
+
hasError: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TimerDefaultVariant = ({
|
|
16
|
+
timerState,
|
|
17
|
+
animationState,
|
|
18
|
+
hasError,
|
|
19
|
+
className,
|
|
20
|
+
}: TimerDefaultVariantProps) => {
|
|
21
|
+
const { t } = useTranslation()
|
|
22
|
+
const theme = useTheme()
|
|
23
|
+
const { elapsedTime, currentProgress } = timerState
|
|
24
|
+
const { showErrorAnimation, showPauseAnimation, showMainText } = animationState
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Box
|
|
28
|
+
className={className}
|
|
29
|
+
sx={{
|
|
30
|
+
position: "relative",
|
|
31
|
+
width: 264,
|
|
32
|
+
height: 264,
|
|
33
|
+
display: "flex",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center",
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Gauge
|
|
39
|
+
width={264}
|
|
40
|
+
height={264}
|
|
41
|
+
value={currentProgress}
|
|
42
|
+
valueMin={0}
|
|
43
|
+
valueMax={100}
|
|
44
|
+
innerRadius="85%"
|
|
45
|
+
outerRadius="100%"
|
|
46
|
+
margin={0}
|
|
47
|
+
skipAnimation={true}
|
|
48
|
+
text={() => ""}
|
|
49
|
+
sx={{
|
|
50
|
+
opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
|
|
51
|
+
transition: "opacity 0.5s ease-out",
|
|
52
|
+
[`& .MuiGauge-valueArc`]: {
|
|
53
|
+
fill: hasError
|
|
54
|
+
? theme.palette.error.light
|
|
55
|
+
: theme.palette.success.main,
|
|
56
|
+
transition: "fill 0.5s ease-out",
|
|
57
|
+
},
|
|
58
|
+
[`& .MuiGauge-referenceArc`]: {
|
|
59
|
+
fill: "#171927",
|
|
60
|
+
stroke: "transparent",
|
|
61
|
+
strokeWidth: 0,
|
|
62
|
+
transition:
|
|
63
|
+
"fill 0.5s ease-out, stroke 0.5s ease-out, stroke-width 0.5s ease-out",
|
|
64
|
+
},
|
|
65
|
+
[`& .MuiGauge-valueText`]: {
|
|
66
|
+
display: "none",
|
|
67
|
+
},
|
|
68
|
+
[`& .MuiGauge-text`]: {
|
|
69
|
+
display: "none",
|
|
70
|
+
},
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
{/* Center content overlay */}
|
|
75
|
+
<Box
|
|
76
|
+
sx={{
|
|
77
|
+
position: "absolute",
|
|
78
|
+
top: "50%",
|
|
79
|
+
left: "50%",
|
|
80
|
+
transform: "translate(-50%, -50%)",
|
|
81
|
+
width: 225,
|
|
82
|
+
height: 225,
|
|
83
|
+
borderRadius: "50%",
|
|
84
|
+
backgroundColor: "#292B3F",
|
|
85
|
+
display: "flex",
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
textAlign: "center",
|
|
90
|
+
gap: 1,
|
|
91
|
+
transition: "background-color 0.5s ease-out",
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{/* Main display */}
|
|
95
|
+
<Box
|
|
96
|
+
sx={{
|
|
97
|
+
position: "relative",
|
|
98
|
+
height: "48px",
|
|
99
|
+
display: "flex",
|
|
100
|
+
alignItems: "center",
|
|
101
|
+
justifyContent: "center",
|
|
102
|
+
marginBottom: 0.5,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{/* Error text */}
|
|
106
|
+
<Fade in={showMainText && hasError} timeout={200}>
|
|
107
|
+
<Typography
|
|
108
|
+
variant="h6"
|
|
109
|
+
sx={{
|
|
110
|
+
position: "absolute",
|
|
111
|
+
fontSize: "16px",
|
|
112
|
+
fontWeight: 500,
|
|
113
|
+
color: theme.palette.error.light,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{t("timer.error")}
|
|
117
|
+
</Typography>
|
|
118
|
+
</Fade>
|
|
119
|
+
|
|
120
|
+
{/* Timer display */}
|
|
121
|
+
<Fade in={showMainText && !hasError} timeout={300}>
|
|
122
|
+
<Typography
|
|
123
|
+
variant="h1"
|
|
124
|
+
sx={{
|
|
125
|
+
position: "absolute",
|
|
126
|
+
fontSize: "48px",
|
|
127
|
+
fontWeight: 500,
|
|
128
|
+
color: theme.palette.text.primary,
|
|
129
|
+
lineHeight: 1,
|
|
130
|
+
letterSpacing: "-0.5px",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{formatTime(elapsedTime)}
|
|
134
|
+
</Typography>
|
|
135
|
+
</Fade>
|
|
136
|
+
</Box>
|
|
137
|
+
</Box>
|
|
138
|
+
</Box>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -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
|
+
}
|