@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.
Files changed (44) hide show
  1. package/dist/components/CycleTimer/CycleTimer.d.ts +3 -0
  2. package/dist/components/CycleTimer/CycleTimer.d.ts.map +1 -0
  3. package/dist/components/CycleTimer/DefaultVariant.d.ts +10 -0
  4. package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -0
  5. package/dist/components/CycleTimer/SmallVariant.d.ts +11 -0
  6. package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -0
  7. package/dist/components/CycleTimer/index.d.ts +28 -0
  8. package/dist/components/CycleTimer/index.d.ts.map +1 -0
  9. package/dist/components/CycleTimer/types.d.ts +49 -0
  10. package/dist/components/CycleTimer/types.d.ts.map +1 -0
  11. package/dist/components/CycleTimer/useAnimations.d.ts +13 -0
  12. package/dist/components/CycleTimer/useAnimations.d.ts.map +1 -0
  13. package/dist/components/CycleTimer/useTimerLogic.d.ts +26 -0
  14. package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -0
  15. package/dist/components/CycleTimer/utils.d.ts +13 -0
  16. package/dist/components/CycleTimer/utils.d.ts.map +1 -0
  17. package/dist/components/CycleTimer.d.ts +2 -96
  18. package/dist/components/CycleTimer.d.ts.map +1 -1
  19. package/dist/components/TabBar.d.ts.map +1 -1
  20. package/dist/components/jogging/PoseCartesianValues.d.ts +8 -4
  21. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  22. package/dist/components/jogging/PoseJointValues.d.ts +8 -4
  23. package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
  24. package/dist/index.cjs +50 -50
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +9215 -8857
  27. package/dist/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/AppHeader.tsx +1 -1
  30. package/src/components/CycleTimer/CycleTimer.ts +6 -0
  31. package/src/components/CycleTimer/DefaultVariant.tsx +272 -0
  32. package/src/components/CycleTimer/SmallVariant.tsx +190 -0
  33. package/src/components/CycleTimer/index.tsx +143 -0
  34. package/src/components/CycleTimer/types.ts +58 -0
  35. package/src/components/CycleTimer/useAnimations.ts +154 -0
  36. package/src/components/CycleTimer/useTimerLogic.ts +377 -0
  37. package/src/components/CycleTimer/utils.ts +53 -0
  38. package/src/components/CycleTimer.tsx +6 -715
  39. package/src/components/ProgramControl.tsx +4 -4
  40. package/src/components/TabBar.tsx +8 -10
  41. package/src/components/jogging/PoseCartesianValues.tsx +67 -7
  42. package/src/components/jogging/PoseJointValues.tsx +68 -8
  43. package/src/i18n/locales/de/translations.json +4 -0
  44. 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.36.0",
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: "64px !important" }}>
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,6 @@
1
+ export { CycleTimer } from "./index"
2
+ export type {
3
+ CycleTimerControls,
4
+ CycleTimerProps,
5
+ CycleTimerState,
6
+ } from "./types"
@@ -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
+ }