@wandelbots/wandelbots-js-react-components 2.36.0 → 2.37.0-pr.feature-states-for-cycle-timer.379.4ca80c1
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/CycleTimer.d.ts +3 -0
- package/dist/components/CycleTimer/CycleTimer.d.ts.map +1 -0
- package/dist/components/CycleTimer/DefaultVariant.d.ts +10 -0
- package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -0
- package/dist/components/CycleTimer/SmallVariant.d.ts +11 -0
- package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -0
- package/dist/components/CycleTimer/index.d.ts +28 -0
- package/dist/components/CycleTimer/index.d.ts.map +1 -0
- package/dist/components/CycleTimer/types.d.ts +49 -0
- package/dist/components/CycleTimer/types.d.ts.map +1 -0
- package/dist/components/CycleTimer/useAnimations.d.ts +13 -0
- package/dist/components/CycleTimer/useAnimations.d.ts.map +1 -0
- package/dist/components/CycleTimer/useTimerLogic.d.ts +26 -0
- package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -0
- package/dist/components/CycleTimer/utils.d.ts +13 -0
- package/dist/components/CycleTimer/utils.d.ts.map +1 -0
- package/dist/components/CycleTimer.d.ts +2 -96
- package/dist/components/CycleTimer.d.ts.map +1 -1
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/jogging/PoseCartesianValues.d.ts +8 -4
- package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
- package/dist/components/jogging/PoseJointValues.d.ts +8 -4
- package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
- package/dist/index.cjs +50 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9215 -8857
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AppHeader.tsx +1 -1
- package/src/components/CycleTimer/CycleTimer.ts +6 -0
- package/src/components/CycleTimer/DefaultVariant.tsx +272 -0
- package/src/components/CycleTimer/SmallVariant.tsx +190 -0
- package/src/components/CycleTimer/index.tsx +143 -0
- package/src/components/CycleTimer/types.ts +58 -0
- package/src/components/CycleTimer/useAnimations.ts +154 -0
- package/src/components/CycleTimer/useTimerLogic.ts +377 -0
- package/src/components/CycleTimer/utils.ts +53 -0
- package/src/components/CycleTimer.tsx +6 -715
- package/src/components/ProgramControl.tsx +4 -4
- package/src/components/TabBar.tsx +8 -10
- package/src/components/jogging/PoseCartesianValues.tsx +67 -7
- package/src/components/jogging/PoseJointValues.tsx +68 -8
- package/src/i18n/locales/de/translations.json +4 -0
- package/src/i18n/locales/en/translations.json +4 -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.37.0-pr.feature-states-for-cycle-timer.379.4ca80c1",
|
|
4
4
|
"description": "React UI toolkit for building applications on top of the Wandelbots platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -85,7 +85,7 @@ export const AppHeader = externalizeComponent(
|
|
|
85
85
|
...sx,
|
|
86
86
|
}}
|
|
87
87
|
>
|
|
88
|
-
<Toolbar sx={{ minHeight: "
|
|
88
|
+
<Toolbar sx={{ minHeight: "62px !important" }}>
|
|
89
89
|
{/* App Icon */}
|
|
90
90
|
<Box sx={{ mr: 2, display: "flex", alignItems: "center" }}>
|
|
91
91
|
{appIcon}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Box, Fade, Typography, useTheme } from "@mui/material"
|
|
2
|
+
import { Gauge } from "@mui/x-charts/Gauge"
|
|
3
|
+
import { useTranslation } from "react-i18next"
|
|
4
|
+
import type { AnimationState, TimerState } from "./types"
|
|
5
|
+
import { formatTime } from "./utils"
|
|
6
|
+
|
|
7
|
+
interface DefaultVariantProps {
|
|
8
|
+
timerState: TimerState
|
|
9
|
+
animationState: AnimationState
|
|
10
|
+
hasError: boolean
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DefaultVariant = ({
|
|
15
|
+
timerState,
|
|
16
|
+
animationState,
|
|
17
|
+
hasError,
|
|
18
|
+
className,
|
|
19
|
+
}: DefaultVariantProps) => {
|
|
20
|
+
const { t } = useTranslation()
|
|
21
|
+
const theme = useTheme()
|
|
22
|
+
const { currentState, remainingTime, maxTime, currentProgress } = timerState
|
|
23
|
+
const {
|
|
24
|
+
showErrorAnimation,
|
|
25
|
+
showPauseAnimation,
|
|
26
|
+
showPulsatingText,
|
|
27
|
+
pulsatingFinished,
|
|
28
|
+
showLabels,
|
|
29
|
+
showMainText,
|
|
30
|
+
} = animationState
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Box
|
|
34
|
+
className={className}
|
|
35
|
+
sx={{
|
|
36
|
+
position: "relative",
|
|
37
|
+
width: 264,
|
|
38
|
+
height: 264,
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Gauge
|
|
45
|
+
width={264}
|
|
46
|
+
height={264}
|
|
47
|
+
value={currentState === "idle" ? 0 : currentProgress}
|
|
48
|
+
valueMin={0}
|
|
49
|
+
valueMax={100}
|
|
50
|
+
innerRadius={currentState === "idle" ? "75%" : "76%"}
|
|
51
|
+
outerRadius="90%"
|
|
52
|
+
skipAnimation={true}
|
|
53
|
+
text={() => ""}
|
|
54
|
+
sx={{
|
|
55
|
+
opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
|
|
56
|
+
transition: "opacity 0.5s ease-out",
|
|
57
|
+
[`& .MuiGauge-valueArc`]: {
|
|
58
|
+
fill: hasError
|
|
59
|
+
? theme.palette.error.light
|
|
60
|
+
: theme.palette.success.main,
|
|
61
|
+
transition: "fill 0.5s ease-out",
|
|
62
|
+
},
|
|
63
|
+
[`& .MuiGauge-referenceArc`]: {
|
|
64
|
+
fill: currentState === "idle" ? "#292B3F" : "white",
|
|
65
|
+
stroke: currentState === "idle" ? "#181927" : "transparent",
|
|
66
|
+
strokeWidth: currentState === "idle" ? 2 : 0,
|
|
67
|
+
transition:
|
|
68
|
+
"fill 0.5s ease-out, stroke 0.5s ease-out, stroke-width 0.5s ease-out",
|
|
69
|
+
},
|
|
70
|
+
[`& .MuiGauge-valueText`]: {
|
|
71
|
+
display: "none",
|
|
72
|
+
},
|
|
73
|
+
[`& .MuiGauge-text`]: {
|
|
74
|
+
display: "none",
|
|
75
|
+
},
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Center content overlay */}
|
|
80
|
+
<Box
|
|
81
|
+
sx={{
|
|
82
|
+
position: "absolute",
|
|
83
|
+
top: "50%",
|
|
84
|
+
left: "50%",
|
|
85
|
+
transform: "translate(-50%, -50%)",
|
|
86
|
+
width: 200,
|
|
87
|
+
height: 200,
|
|
88
|
+
borderRadius: "50%",
|
|
89
|
+
display: "flex",
|
|
90
|
+
flexDirection: "column",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
justifyContent: "center",
|
|
93
|
+
textAlign: "center",
|
|
94
|
+
gap: 1,
|
|
95
|
+
transition: "background-color 0.5s ease-out",
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{/* Top label */}
|
|
99
|
+
<Box
|
|
100
|
+
sx={{
|
|
101
|
+
height: "16px",
|
|
102
|
+
display: "flex",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
marginBottom: 0.5,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Fade
|
|
109
|
+
in={
|
|
110
|
+
showLabels &&
|
|
111
|
+
!hasError &&
|
|
112
|
+
currentState !== "idle" &&
|
|
113
|
+
currentState !== "countup" &&
|
|
114
|
+
currentState !== "success"
|
|
115
|
+
}
|
|
116
|
+
timeout={200}
|
|
117
|
+
>
|
|
118
|
+
<Typography
|
|
119
|
+
variant="body2"
|
|
120
|
+
sx={{
|
|
121
|
+
fontSize: "12px",
|
|
122
|
+
color:
|
|
123
|
+
currentState === "measured"
|
|
124
|
+
? pulsatingFinished
|
|
125
|
+
? theme.palette.text.secondary
|
|
126
|
+
: showPulsatingText
|
|
127
|
+
? theme.palette.success.main
|
|
128
|
+
: theme.palette.text.secondary
|
|
129
|
+
: theme.palette.text.secondary,
|
|
130
|
+
transition: "color 0.8s ease-in-out",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{currentState === "measuring"
|
|
134
|
+
? t("CycleTimer.CycleTime.lb", "Cycle Time")
|
|
135
|
+
: currentState === "measured"
|
|
136
|
+
? t("CycleTimer.CycleTime.lb", "Cycle Time")
|
|
137
|
+
: currentState === "countdown"
|
|
138
|
+
? t("CycleTimer.RemainingTime.lb", "Remaining Time")
|
|
139
|
+
: ""}
|
|
140
|
+
</Typography>
|
|
141
|
+
</Fade>
|
|
142
|
+
</Box>
|
|
143
|
+
|
|
144
|
+
{/* Main display */}
|
|
145
|
+
<Box
|
|
146
|
+
sx={{
|
|
147
|
+
position: "relative",
|
|
148
|
+
height: "48px",
|
|
149
|
+
display: "flex",
|
|
150
|
+
alignItems: "center",
|
|
151
|
+
justifyContent: "center",
|
|
152
|
+
marginBottom: 0.5,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{/* Idle state text */}
|
|
156
|
+
<Fade
|
|
157
|
+
in={showMainText && currentState === "idle" && !hasError}
|
|
158
|
+
timeout={200}
|
|
159
|
+
>
|
|
160
|
+
<Typography
|
|
161
|
+
variant="body2"
|
|
162
|
+
sx={{
|
|
163
|
+
position: "absolute",
|
|
164
|
+
fontSize: "12px",
|
|
165
|
+
fontWeight: 400,
|
|
166
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
167
|
+
lineHeight: "166%",
|
|
168
|
+
letterSpacing: "0.17px",
|
|
169
|
+
textAlign: "center",
|
|
170
|
+
width: "150px",
|
|
171
|
+
height: "20px",
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{t("CycleTimer.WaitingForCycle.lb", "Waiting for program cycle")}
|
|
175
|
+
</Typography>
|
|
176
|
+
</Fade>
|
|
177
|
+
|
|
178
|
+
{/* Error text */}
|
|
179
|
+
<Fade in={showMainText && hasError} timeout={200}>
|
|
180
|
+
<Typography
|
|
181
|
+
variant="h3"
|
|
182
|
+
sx={{
|
|
183
|
+
position: "absolute",
|
|
184
|
+
fontSize: "40px",
|
|
185
|
+
fontWeight: 400,
|
|
186
|
+
color: "#FFFFFF",
|
|
187
|
+
lineHeight: "116.7%",
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{t("CycleTimer.Error.lb", "Error")}
|
|
191
|
+
</Typography>
|
|
192
|
+
</Fade>
|
|
193
|
+
|
|
194
|
+
{/* Normal timer text */}
|
|
195
|
+
<Fade
|
|
196
|
+
in={
|
|
197
|
+
showMainText &&
|
|
198
|
+
!hasError &&
|
|
199
|
+
currentState !== "idle" &&
|
|
200
|
+
currentState !== "success"
|
|
201
|
+
}
|
|
202
|
+
timeout={200}
|
|
203
|
+
>
|
|
204
|
+
<Typography
|
|
205
|
+
variant="h1"
|
|
206
|
+
sx={{
|
|
207
|
+
position: "absolute",
|
|
208
|
+
fontSize: "48px",
|
|
209
|
+
fontWeight: 500,
|
|
210
|
+
color:
|
|
211
|
+
currentState === "measured"
|
|
212
|
+
? theme.palette.text.primary
|
|
213
|
+
: theme.palette.text.primary,
|
|
214
|
+
lineHeight: 1,
|
|
215
|
+
transition: "color 0.5s ease-out",
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{formatTime(remainingTime)}
|
|
219
|
+
</Typography>
|
|
220
|
+
</Fade>
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
{/* Bottom label */}
|
|
224
|
+
<Box
|
|
225
|
+
sx={{
|
|
226
|
+
height: "16px",
|
|
227
|
+
display: "flex",
|
|
228
|
+
alignItems: "center",
|
|
229
|
+
justifyContent: "center",
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<Fade
|
|
233
|
+
in={
|
|
234
|
+
showLabels &&
|
|
235
|
+
!hasError &&
|
|
236
|
+
currentState !== "idle" &&
|
|
237
|
+
currentState !== "countup" &&
|
|
238
|
+
currentState !== "success"
|
|
239
|
+
}
|
|
240
|
+
timeout={200}
|
|
241
|
+
>
|
|
242
|
+
<Typography
|
|
243
|
+
variant="body2"
|
|
244
|
+
sx={{
|
|
245
|
+
fontSize: "12px",
|
|
246
|
+
color:
|
|
247
|
+
currentState === "measured"
|
|
248
|
+
? pulsatingFinished
|
|
249
|
+
? theme.palette.text.secondary
|
|
250
|
+
: showPulsatingText
|
|
251
|
+
? theme.palette.success.main
|
|
252
|
+
: theme.palette.text.secondary
|
|
253
|
+
: theme.palette.text.secondary,
|
|
254
|
+
transition: "color 0.8s ease-in-out",
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{currentState === "measuring"
|
|
258
|
+
? t("CycleTimer.Measuring.lb", "measuring...")
|
|
259
|
+
: currentState === "measured"
|
|
260
|
+
? t("CycleTimer.Determined.lb", "determined")
|
|
261
|
+
: currentState === "countdown" && maxTime !== null
|
|
262
|
+
? t("CycleTimer.OfTime.lb", {
|
|
263
|
+
time: formatTime(maxTime),
|
|
264
|
+
})
|
|
265
|
+
: ""}
|
|
266
|
+
</Typography>
|
|
267
|
+
</Fade>
|
|
268
|
+
</Box>
|
|
269
|
+
</Box>
|
|
270
|
+
</Box>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Box, Typography, useTheme } from "@mui/material"
|
|
2
|
+
import { useTranslation } from "react-i18next"
|
|
3
|
+
import type { AnimationState, TimerState } from "./types"
|
|
4
|
+
import { formatTime } from "./utils"
|
|
5
|
+
|
|
6
|
+
interface SmallVariantProps {
|
|
7
|
+
timerState: TimerState
|
|
8
|
+
animationState: AnimationState
|
|
9
|
+
hasError: boolean
|
|
10
|
+
compact: boolean
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SmallVariant = ({
|
|
15
|
+
timerState,
|
|
16
|
+
animationState,
|
|
17
|
+
hasError,
|
|
18
|
+
compact,
|
|
19
|
+
className,
|
|
20
|
+
}: SmallVariantProps) => {
|
|
21
|
+
const { t } = useTranslation()
|
|
22
|
+
const theme = useTheme()
|
|
23
|
+
const { currentState, remainingTime, maxTime } = timerState
|
|
24
|
+
const {
|
|
25
|
+
showErrorAnimation,
|
|
26
|
+
showPauseAnimation,
|
|
27
|
+
showPulsatingText,
|
|
28
|
+
pulsatingFinished,
|
|
29
|
+
} = animationState
|
|
30
|
+
|
|
31
|
+
// Simple text-only mode for compact variant in certain states
|
|
32
|
+
if (compact && (currentState === "countup" || currentState === "idle")) {
|
|
33
|
+
return (
|
|
34
|
+
<Box
|
|
35
|
+
className={className}
|
|
36
|
+
sx={{
|
|
37
|
+
display: "flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
m: 0,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Typography
|
|
43
|
+
variant="body2"
|
|
44
|
+
sx={{
|
|
45
|
+
color: hasError
|
|
46
|
+
? theme.palette.error.light
|
|
47
|
+
: theme.palette.text.primary,
|
|
48
|
+
fontSize: "14px",
|
|
49
|
+
transition: "color 0.5s ease-out",
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{hasError
|
|
53
|
+
? t("CycleTimer.Error.lb", "Error")
|
|
54
|
+
: currentState === "idle"
|
|
55
|
+
? "0:00"
|
|
56
|
+
: formatTime(remainingTime)}
|
|
57
|
+
</Typography>
|
|
58
|
+
</Box>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Box
|
|
64
|
+
className={className}
|
|
65
|
+
sx={{
|
|
66
|
+
display: "flex",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
m: 0,
|
|
69
|
+
gap: 1,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{/* Animated progress ring icon */}
|
|
73
|
+
{!(
|
|
74
|
+
currentState === "countup" ||
|
|
75
|
+
(currentState === "idle" && compact)
|
|
76
|
+
) && (
|
|
77
|
+
<Box
|
|
78
|
+
sx={{
|
|
79
|
+
width: 20,
|
|
80
|
+
height: 20,
|
|
81
|
+
display: "flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
|
|
85
|
+
transition: "opacity 0.5s ease-out",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<svg
|
|
89
|
+
width="20"
|
|
90
|
+
height="20"
|
|
91
|
+
viewBox="0 0 20 20"
|
|
92
|
+
style={{ transform: "rotate(-90deg)" }}
|
|
93
|
+
role="img"
|
|
94
|
+
aria-label="Timer progress"
|
|
95
|
+
>
|
|
96
|
+
{/* Background ring */}
|
|
97
|
+
<circle
|
|
98
|
+
cx="10"
|
|
99
|
+
cy="10"
|
|
100
|
+
r="8"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke={
|
|
103
|
+
hasError
|
|
104
|
+
? theme.palette.error.light
|
|
105
|
+
: currentState === "measured"
|
|
106
|
+
? pulsatingFinished
|
|
107
|
+
? theme.palette.text.secondary
|
|
108
|
+
: showPulsatingText
|
|
109
|
+
? theme.palette.success.main
|
|
110
|
+
: theme.palette.text.secondary
|
|
111
|
+
: theme.palette.success.main
|
|
112
|
+
}
|
|
113
|
+
strokeWidth="2"
|
|
114
|
+
opacity={0.3}
|
|
115
|
+
style={{
|
|
116
|
+
transition: "stroke 0.8s ease-in-out",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
{/* Progress ring */}
|
|
120
|
+
<circle
|
|
121
|
+
cx="10"
|
|
122
|
+
cy="10"
|
|
123
|
+
r="8"
|
|
124
|
+
fill="none"
|
|
125
|
+
stroke={
|
|
126
|
+
hasError
|
|
127
|
+
? theme.palette.error.light
|
|
128
|
+
: currentState === "measured"
|
|
129
|
+
? pulsatingFinished
|
|
130
|
+
? theme.palette.text.secondary
|
|
131
|
+
: showPulsatingText
|
|
132
|
+
? theme.palette.success.main
|
|
133
|
+
: theme.palette.text.secondary
|
|
134
|
+
: theme.palette.success.main
|
|
135
|
+
}
|
|
136
|
+
strokeWidth="2"
|
|
137
|
+
strokeLinecap="round"
|
|
138
|
+
strokeDasharray={`${2 * Math.PI * 8}`}
|
|
139
|
+
strokeDashoffset={`${2 * Math.PI * 8 * (1 - (currentState === "idle" ? 0 : timerState.currentProgress) / 100)}`}
|
|
140
|
+
style={{
|
|
141
|
+
transition:
|
|
142
|
+
"stroke-dashoffset 0.1s ease-out, stroke 0.8s ease-in-out",
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
</svg>
|
|
146
|
+
</Box>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{/* Timer text display */}
|
|
150
|
+
<Typography
|
|
151
|
+
variant="body2"
|
|
152
|
+
sx={{
|
|
153
|
+
color: hasError
|
|
154
|
+
? theme.palette.error.light
|
|
155
|
+
: currentState === "idle"
|
|
156
|
+
? "rgba(255, 255, 255, 0.7)"
|
|
157
|
+
: currentState === "measured"
|
|
158
|
+
? pulsatingFinished
|
|
159
|
+
? theme.palette.text.secondary
|
|
160
|
+
: showPulsatingText
|
|
161
|
+
? theme.palette.success.main
|
|
162
|
+
: theme.palette.text.secondary
|
|
163
|
+
: theme.palette.text.primary,
|
|
164
|
+
fontSize: currentState === "idle" ? "12px" : "14px",
|
|
165
|
+
lineHeight: currentState === "idle" ? "166%" : "normal",
|
|
166
|
+
letterSpacing: currentState === "idle" ? "0.17px" : "normal",
|
|
167
|
+
transition: "color 0.8s ease-in-out, font-size 0.3s ease-out",
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{hasError
|
|
171
|
+
? t("CycleTimer.Error.lb", "Error")
|
|
172
|
+
: currentState === "idle"
|
|
173
|
+
? t("CycleTimer.WaitingForCycle.lb", "Waiting for program cycle")
|
|
174
|
+
: currentState === "measuring"
|
|
175
|
+
? compact
|
|
176
|
+
? `${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
|
|
177
|
+
: `${formatTime(remainingTime)} / ${t("CycleTimer.Measuring.lb", "measuring...")}`
|
|
178
|
+
: currentState === "measured"
|
|
179
|
+
? compact
|
|
180
|
+
? `${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
|
|
181
|
+
: `${formatTime(remainingTime)} / ${t("CycleTimer.Determined.lb", "determined")}`
|
|
182
|
+
: currentState === "countdown" && maxTime !== null
|
|
183
|
+
? compact
|
|
184
|
+
? `${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
|
|
185
|
+
: `${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`
|
|
186
|
+
: formatTime(remainingTime)}
|
|
187
|
+
</Typography>
|
|
188
|
+
</Box>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { observer } from "mobx-react-lite"
|
|
2
|
+
import { useEffect, useRef } from "react"
|
|
3
|
+
import { externalizeComponent } from "../../externalizeComponent"
|
|
4
|
+
import { DefaultVariant } from "./DefaultVariant"
|
|
5
|
+
import { SmallVariant } from "./SmallVariant"
|
|
6
|
+
import type { CycleTimerProps } from "./types"
|
|
7
|
+
import { useAnimations } from "./useAnimations"
|
|
8
|
+
import { useTimerLogic } from "./useTimerLogic"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A circular gauge timer component that shows the remaining time of a cycle or counts up
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Custom SVG circular gauge with 264px diameter and 40px thickness
|
|
15
|
+
* - Multiple states: idle, measuring, measured, countdown, countup, success
|
|
16
|
+
* - Idle state: shows "Waiting for program cycle" with transparent inner circle
|
|
17
|
+
* - Measuring state: counts up with "Cycle Time" / "measuring..." labels
|
|
18
|
+
* - Measured state: shows final time with "Cycle Time" / "determined" labels in pulsating green
|
|
19
|
+
* - Countdown mode: shows remaining time prominently, counts down to zero
|
|
20
|
+
* - Count-up mode: shows elapsed time without special labels
|
|
21
|
+
* - Success state: brief green flash after cycle completion
|
|
22
|
+
* - Displays appropriate labels based on state
|
|
23
|
+
* - Automatically counts down/up and triggers callback when reaching zero (countdown only)
|
|
24
|
+
* - Full timer control: start, pause, resume functionality
|
|
25
|
+
* - Support for starting with elapsed time (resume mid-cycle)
|
|
26
|
+
* - Error state support: pauses timer and shows error styling (red color)
|
|
27
|
+
* - Smooth fade transitions between different text states
|
|
28
|
+
* - Pulsating text animation for completed measuring state
|
|
29
|
+
* - Fully localized with i18next
|
|
30
|
+
* - Material-UI theming integration
|
|
31
|
+
* - Small variant with animated progress icon (gauge border only) next to text or simple text-only mode
|
|
32
|
+
*/
|
|
33
|
+
export const CycleTimer = externalizeComponent(
|
|
34
|
+
observer(
|
|
35
|
+
({
|
|
36
|
+
onCycleComplete,
|
|
37
|
+
onCycleEnd,
|
|
38
|
+
onMeasuringComplete,
|
|
39
|
+
autoStart = true,
|
|
40
|
+
variant = "default",
|
|
41
|
+
compact = false,
|
|
42
|
+
className,
|
|
43
|
+
hasError = false,
|
|
44
|
+
}: CycleTimerProps) => {
|
|
45
|
+
const prevStateRef = useRef<string | undefined>(undefined)
|
|
46
|
+
|
|
47
|
+
// Initialize animation hooks
|
|
48
|
+
const {
|
|
49
|
+
animationState,
|
|
50
|
+
triggerPauseAnimation,
|
|
51
|
+
triggerErrorAnimation,
|
|
52
|
+
clearErrorAnimation,
|
|
53
|
+
startPulsatingAnimation,
|
|
54
|
+
stopPulsatingAnimation,
|
|
55
|
+
triggerFadeTransition,
|
|
56
|
+
setInitialAnimationState,
|
|
57
|
+
cleanup,
|
|
58
|
+
} = useAnimations()
|
|
59
|
+
|
|
60
|
+
// Initialize timer logic
|
|
61
|
+
const { timerState, controls } = useTimerLogic({
|
|
62
|
+
autoStart,
|
|
63
|
+
onCycleEnd,
|
|
64
|
+
onMeasuringComplete,
|
|
65
|
+
hasError,
|
|
66
|
+
onPauseAnimation: triggerPauseAnimation,
|
|
67
|
+
onErrorAnimation: triggerErrorAnimation,
|
|
68
|
+
onClearErrorAnimation: clearErrorAnimation,
|
|
69
|
+
onStartPulsating: startPulsatingAnimation,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Handle state changes with fade transitions
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const prevState = prevStateRef.current
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
prevStateRef.current !== undefined &&
|
|
78
|
+
prevState !== timerState.currentState
|
|
79
|
+
) {
|
|
80
|
+
// Stop pulsating animation if leaving measured state
|
|
81
|
+
if (prevState === "measured") {
|
|
82
|
+
stopPulsatingAnimation()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Trigger fade transition
|
|
86
|
+
triggerFadeTransition()
|
|
87
|
+
} else {
|
|
88
|
+
// No state change or first time - set initial state
|
|
89
|
+
setInitialAnimationState()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
prevStateRef.current = timerState.currentState
|
|
93
|
+
}, [
|
|
94
|
+
timerState.currentState,
|
|
95
|
+
stopPulsatingAnimation,
|
|
96
|
+
triggerFadeTransition,
|
|
97
|
+
setInitialAnimationState,
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
// Provide controls to parent component
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
let isMounted = true
|
|
103
|
+
const timeoutId = setTimeout(() => {
|
|
104
|
+
if (isMounted) {
|
|
105
|
+
onCycleComplete(controls)
|
|
106
|
+
}
|
|
107
|
+
}, 0)
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
isMounted = false
|
|
111
|
+
clearTimeout(timeoutId)
|
|
112
|
+
}
|
|
113
|
+
}, [onCycleComplete, controls])
|
|
114
|
+
|
|
115
|
+
// Cleanup on unmount
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
return cleanup
|
|
118
|
+
}, [cleanup])
|
|
119
|
+
|
|
120
|
+
// Render appropriate variant
|
|
121
|
+
if (variant === "small") {
|
|
122
|
+
return (
|
|
123
|
+
<SmallVariant
|
|
124
|
+
timerState={timerState}
|
|
125
|
+
animationState={animationState}
|
|
126
|
+
hasError={hasError}
|
|
127
|
+
compact={compact}
|
|
128
|
+
className={className}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<DefaultVariant
|
|
135
|
+
timerState={timerState}
|
|
136
|
+
animationState={animationState}
|
|
137
|
+
hasError={hasError}
|
|
138
|
+
className={className}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type CycleTimerState =
|
|
2
|
+
| "idle" // Initial state showing "Waiting for program cycle"
|
|
3
|
+
| "measuring" // Counting up without max time, showing "Cycle Time" / "measuring..."
|
|
4
|
+
| "measured" // Completed measuring state showing "Cycle Time" / "determined" with pulsating green text
|
|
5
|
+
| "countdown" // Counting down with max time
|
|
6
|
+
| "countup" // Simple count up without special text
|
|
7
|
+
| "success" // Brief success state after cycle completion
|
|
8
|
+
|
|
9
|
+
export interface CycleTimerControls {
|
|
10
|
+
startNewCycle: (maxTimeSeconds?: number, elapsedSeconds?: number) => void
|
|
11
|
+
startMeasuring: (elapsedSeconds?: number) => void
|
|
12
|
+
startCountUp: (elapsedSeconds?: number) => void
|
|
13
|
+
setIdle: () => void
|
|
14
|
+
completeMeasuring: () => void
|
|
15
|
+
pause: () => void
|
|
16
|
+
resume: () => void
|
|
17
|
+
isPaused: () => boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CycleTimerProps {
|
|
21
|
+
/**
|
|
22
|
+
* Callback that receives the timer control functions
|
|
23
|
+
*/
|
|
24
|
+
onCycleComplete: (controls: CycleTimerControls) => void
|
|
25
|
+
/** Callback fired when a cycle actually completes (reaches zero) */
|
|
26
|
+
onCycleEnd?: () => void
|
|
27
|
+
/** Callback fired when measuring cycle completes */
|
|
28
|
+
onMeasuringComplete?: () => void
|
|
29
|
+
/** Whether the timer should start automatically when maxTime is set */
|
|
30
|
+
autoStart?: boolean
|
|
31
|
+
/** Visual variant of the timer */
|
|
32
|
+
variant?: "default" | "small"
|
|
33
|
+
/** For small variant: whether to show remaining time details (compact hides them) */
|
|
34
|
+
compact?: boolean
|
|
35
|
+
/** Additional CSS classes */
|
|
36
|
+
className?: string
|
|
37
|
+
/** Whether the timer is in an error state (pauses timer and shows error styling) */
|
|
38
|
+
hasError?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TimerState {
|
|
42
|
+
currentState: CycleTimerState
|
|
43
|
+
remainingTime: number
|
|
44
|
+
maxTime: number | null
|
|
45
|
+
isRunning: boolean
|
|
46
|
+
isPausedState: boolean
|
|
47
|
+
currentProgress: number
|
|
48
|
+
wasRunningBeforeError: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AnimationState {
|
|
52
|
+
showPauseAnimation: boolean
|
|
53
|
+
showErrorAnimation: boolean
|
|
54
|
+
showPulsatingText: boolean
|
|
55
|
+
pulsatingFinished: boolean
|
|
56
|
+
showLabels: boolean
|
|
57
|
+
showMainText: boolean
|
|
58
|
+
}
|