@wandelbots/wandelbots-js-react-components 2.34.2-pr.feature-robot-precondition-list.372.9dfd57e → 2.34.2

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 (65) hide show
  1. package/dist/components/CycleTimer.d.ts +16 -33
  2. package/dist/components/CycleTimer.d.ts.map +1 -1
  3. package/dist/components/ProgramControl.d.ts +2 -8
  4. package/dist/components/ProgramControl.d.ts.map +1 -1
  5. package/dist/components/ProgramStateIndicator.d.ts +1 -1
  6. package/dist/components/ProgramStateIndicator.d.ts.map +1 -1
  7. package/dist/components/robots/Robot.d.ts +2 -3
  8. package/dist/components/robots/Robot.d.ts.map +1 -1
  9. package/dist/icons/index.d.ts +0 -1
  10. package/dist/icons/index.d.ts.map +1 -1
  11. package/dist/index.cjs +50 -50
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +0 -10
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +9576 -11435
  16. package/dist/index.js.map +1 -1
  17. package/dist/themes/createDarkTheme.d.ts.map +1 -1
  18. package/package.json +1 -2
  19. package/src/components/CycleTimer.tsx +159 -384
  20. package/src/components/ProgramControl.tsx +12 -27
  21. package/src/components/ProgramStateIndicator.tsx +8 -25
  22. package/src/components/robots/Robot.tsx +2 -5
  23. package/src/i18n/locales/de/translations.json +1 -8
  24. package/src/i18n/locales/en/translations.json +1 -8
  25. package/src/icons/index.ts +0 -1
  26. package/src/index.ts +0 -14
  27. package/src/themes/createDarkTheme.ts +1 -75
  28. package/dist/components/AppHeader.d.ts +0 -34
  29. package/dist/components/AppHeader.d.ts.map +0 -1
  30. package/dist/components/DataGrid.d.ts +0 -66
  31. package/dist/components/DataGrid.d.ts.map +0 -1
  32. package/dist/components/LogPanel.d.ts +0 -38
  33. package/dist/components/LogPanel.d.ts.map +0 -1
  34. package/dist/components/LogStore.d.ts +0 -12
  35. package/dist/components/LogStore.d.ts.map +0 -1
  36. package/dist/components/LogViewer.d.ts +0 -46
  37. package/dist/components/LogViewer.d.ts.map +0 -1
  38. package/dist/components/RobotCard.d.ts +0 -103
  39. package/dist/components/RobotCard.d.ts.map +0 -1
  40. package/dist/components/RobotListItem.d.ts +0 -34
  41. package/dist/components/RobotListItem.d.ts.map +0 -1
  42. package/dist/components/RobotSetupReadinessIndicator.d.ts +0 -31
  43. package/dist/components/RobotSetupReadinessIndicator.d.ts.map +0 -1
  44. package/dist/components/RobotSetupReadinessIndicator.test.d.ts +0 -2
  45. package/dist/components/RobotSetupReadinessIndicator.test.d.ts.map +0 -1
  46. package/dist/components/TabBar.d.ts +0 -30
  47. package/dist/components/TabBar.d.ts.map +0 -1
  48. package/dist/components/robots/manufacturerHomePositions.d.ts +0 -21
  49. package/dist/components/robots/manufacturerHomePositions.d.ts.map +0 -1
  50. package/dist/icons/DropdownArrowIcon.d.ts +0 -3
  51. package/dist/icons/DropdownArrowIcon.d.ts.map +0 -1
  52. package/src/components/AppHeader.md +0 -84
  53. package/src/components/AppHeader.tsx +0 -199
  54. package/src/components/DataGrid.tsx +0 -659
  55. package/src/components/LogPanel.tsx +0 -69
  56. package/src/components/LogStore.ts +0 -44
  57. package/src/components/LogViewer.tsx +0 -370
  58. package/src/components/RobotCard.tsx +0 -568
  59. package/src/components/RobotListItem.tsx +0 -150
  60. package/src/components/RobotSetupReadinessIndicator.test.tsx +0 -60
  61. package/src/components/RobotSetupReadinessIndicator.tsx +0 -124
  62. package/src/components/TabBar.tsx +0 -144
  63. package/src/components/robots/manufacturerHomePositions.ts +0 -76
  64. package/src/icons/DropdownArrowIcon.tsx +0 -13
  65. package/src/icons/chevronDown.svg +0 -3
@@ -1,4 +1,4 @@
1
- import { Box, Fade, Typography, useTheme } from "@mui/material"
1
+ import { Box, Typography, useTheme } from "@mui/material"
2
2
  import { Gauge } from "@mui/x-charts/Gauge"
3
3
  import { observer } from "mobx-react-lite"
4
4
  import { useCallback, useEffect, useRef, useState } from "react"
@@ -9,13 +9,13 @@ import { useInterpolation } from "./utils/interpolation"
9
9
  export interface CycleTimerProps {
10
10
  /**
11
11
  * Callback that receives the timer control functions:
12
- * - `startNewCycle(maxTimeSeconds?, elapsedSeconds?)` - Start a new timer cycle (if maxTimeSeconds is omitted, runs as count-up timer)
12
+ * - `startNewCycle(maxTimeSeconds, elapsedSeconds?)` - Start a new timer cycle
13
13
  * - `pause()` - Pause the countdown while preserving remaining time
14
14
  * - `resume()` - Resume countdown from where it was paused
15
15
  * - `isPaused()` - Check current pause state
16
16
  */
17
17
  onCycleComplete: (controls: {
18
- startNewCycle: (maxTimeSeconds?: number, elapsedSeconds?: number) => void
18
+ startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
19
19
  pause: () => void
20
20
  resume: () => void
21
21
  isPaused: () => boolean
@@ -30,23 +30,18 @@ export interface CycleTimerProps {
30
30
  compact?: boolean
31
31
  /** Additional CSS classes */
32
32
  className?: string
33
- /** Whether the timer is in an error state (pauses timer and shows error styling) */
34
- hasError?: boolean
35
33
  }
36
34
 
37
35
  /**
38
- * A circular gauge timer component that shows the remaining time of a cycle or counts up
36
+ * A circular gauge timer component that shows the remaining time of a cycle
39
37
  *
40
38
  * Features:
41
39
  * - Circular gauge with 264px diameter and 40px thickness
42
- * - Two modes: count-down (with max time) or count-up (without max time)
43
- * - Count-down mode: shows remaining time prominently, counts down to zero
44
- * - Count-up mode: shows elapsed time, gauge progresses in minute steps
45
- * - Displays appropriate labels based on mode
46
- * - Automatically counts down/up and triggers callback when reaching zero (count-down only)
40
+ * - Shows remaining time prominently in the center (60px font)
41
+ * - Displays "remaining time" label at top and total time at bottom
42
+ * - Automatically counts down and triggers callback when reaching zero
47
43
  * - Full timer control: start, pause, resume functionality
48
44
  * - Support for starting with elapsed time (resume mid-cycle)
49
- * - Error state support: pauses timer and shows error styling (red color)
50
45
  * - Smooth spring-based progress animations for all state transitions
51
46
  * - Fully localized with i18next
52
47
  * - Material-UI theming integration
@@ -58,44 +53,32 @@ export interface CycleTimerProps {
58
53
  * @param variant - Visual variant: "default" (large gauge) or "small" (animated icon with text)
59
54
  * @param compact - For small variant: whether to hide remaining time details
60
55
  * @param className - Additional CSS classes
61
- * @param hasError - Whether the timer is in an error state (pauses timer and shows error styling)
62
56
  *
63
57
  * Usage:
64
58
  * ```tsx
65
- * // Count-down timer (with max time)
66
59
  * <CycleTimer
67
60
  * onCycleComplete={(controls) => {
68
- * // Start a 5-minute countdown cycle
61
+ * // Start a 5-minute cycle
69
62
  * controls.startNewCycle(300)
70
63
  *
71
64
  * // Or start a 5-minute cycle with 2 minutes already elapsed
72
65
  * controls.startNewCycle(300, 120)
73
- * }}
74
- * onCycleEnd={() => console.log('Cycle completed!')}
75
- * />
76
66
  *
77
- * // Count-up timer (no max time)
78
- * <CycleTimer
79
- * onCycleComplete={(controls) => {
80
- * // Start count-up timer
81
- * controls.startNewCycle()
67
+ * // Pause the timer
68
+ * controls.pause()
82
69
  *
83
- * // Or start count-up timer with some elapsed time
84
- * controls.startNewCycle(undefined, 120)
85
- * }}
86
- * />
70
+ * // Resume the timer
71
+ * controls.resume()
87
72
  *
88
- * // Timer with error state
89
- * <CycleTimer
90
- * hasError={errorCondition}
91
- * onCycleComplete={(controls) => {
92
- * controls.startNewCycle(300)
73
+ * // Check if paused
74
+ * const paused = controls.isPaused()
93
75
  * }}
76
+ * onCycleEnd={() => console.log('Cycle completed!')}
94
77
  * />
95
78
  * ```
96
79
  *
97
80
  * Control Functions:
98
- * - `startNewCycle(maxTimeSeconds?, elapsedSeconds?)` - Start a new timer cycle (omit maxTimeSeconds for count-up mode)
81
+ * - `startNewCycle(maxTimeSeconds, elapsedSeconds?)` - Start a new timer cycle
99
82
  * - `pause()` - Pause the countdown while preserving remaining time
100
83
  * - `resume()` - Resume countdown from where it was paused
101
84
  * - `isPaused()` - Check current pause state
@@ -109,29 +92,17 @@ export const CycleTimer = externalizeComponent(
109
92
  variant = "default",
110
93
  compact = false,
111
94
  className,
112
- hasError = false,
113
95
  }: CycleTimerProps) => {
114
96
  const theme = useTheme()
115
97
  const { t } = useTranslation()
116
98
  const [remainingTime, setRemainingTime] = useState(0)
117
- const [maxTime, setMaxTime] = useState<number | null>(null)
99
+ const [maxTime, setMaxTime] = useState(0)
118
100
  const [isRunning, setIsRunning] = useState(false)
119
101
  const [isPausedState, setIsPausedState] = useState(false)
120
102
  const [currentProgress, setCurrentProgress] = useState(0)
121
103
  const animationRef = useRef<number | null>(null)
122
104
  const startTimeRef = useRef<number | null>(null)
123
105
  const pausedTimeRef = useRef<number>(0)
124
- const [wasRunningBeforeError, setWasRunningBeforeError] = useState(false)
125
-
126
- // Brief animation states for pause and error visual feedback
127
- const [showPauseAnimation, setShowPauseAnimation] = useState(false)
128
- const [showErrorAnimation, setShowErrorAnimation] = useState(false)
129
- const pauseAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
130
- const errorAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
131
-
132
- // Track mode changes for fade transitions
133
- const [showLabels, setShowLabels] = useState(true)
134
- const prevMaxTimeRef = useRef<number | null | undefined>(undefined)
135
106
 
136
107
  // Spring-based interpolator for smooth gauge progress animations
137
108
  // Uses physics simulation to create natural, smooth transitions between progress values
@@ -143,78 +114,37 @@ export const CycleTimer = externalizeComponent(
143
114
  },
144
115
  })
145
116
 
146
- // Handle mode changes with fade transitions for labels only
147
- useEffect(() => {
148
- const currentIsCountUp = maxTime === null
149
- const prevMaxTime = prevMaxTimeRef.current
150
- const prevIsCountUp = prevMaxTime === null
151
-
152
- // Check if mode actually changed (not just first render)
153
- if (
154
- prevMaxTimeRef.current !== undefined &&
155
- prevIsCountUp !== currentIsCountUp
156
- ) {
157
- // Mode changed - labels will fade based on the Fade component conditions
158
- // We just need to ensure showLabels is true so Fade can control visibility
159
- setShowLabels(true)
160
- } else {
161
- // No mode change or first time - set initial state
162
- setShowLabels(true)
163
- }
164
-
165
- prevMaxTimeRef.current = maxTime
166
- }, [maxTime])
167
-
168
117
  const startNewCycle = useCallback(
169
- (maxTimeSeconds?: number, elapsedSeconds: number = 0) => {
170
- setMaxTime(maxTimeSeconds ?? null)
118
+ (maxTimeSeconds: number, elapsedSeconds: number = 0) => {
119
+ setMaxTime(maxTimeSeconds)
120
+ const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
121
+ setRemainingTime(remainingSeconds)
171
122
  setIsPausedState(false)
172
123
  pausedTimeRef.current = 0
173
124
 
174
- if (maxTimeSeconds !== undefined) {
175
- // Count-down mode: set remaining time
176
- const remainingSeconds = Math.max(
177
- 0,
178
- maxTimeSeconds - elapsedSeconds,
179
- )
180
- setRemainingTime(remainingSeconds)
181
-
182
- // Animate progress smoothly to starting position
183
- const initialProgress =
184
- elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
185
- if (elapsedSeconds === 0) {
186
- progressInterpolator.setTarget([0])
187
- } else {
188
- progressInterpolator.setTarget([initialProgress])
189
- }
190
-
191
- if (remainingSeconds === 0) {
192
- setIsRunning(false)
193
- startTimeRef.current = null
194
- // Trigger completion callback immediately if time is already up
195
- if (onCycleEnd) {
196
- setTimeout(() => onCycleEnd(), 0)
197
- }
198
- } else if (autoStart) {
199
- startTimeRef.current = Date.now() - elapsedSeconds * 1000
200
- setIsRunning(true)
201
- } else {
202
- startTimeRef.current = null
203
- }
125
+ // Animate progress smoothly to starting position
126
+ // For new cycles (no elapsed time), animate from current position to 0%
127
+ // For resumed cycles, animate to the appropriate progress percentage
128
+ const initialProgress =
129
+ elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
130
+ if (elapsedSeconds === 0) {
131
+ progressInterpolator.setTarget([0])
204
132
  } else {
205
- // Count-up mode: start from elapsed time
206
- setRemainingTime(elapsedSeconds)
207
-
208
- // For count-up mode, progress is based on minute steps
209
- const initialProgress = ((elapsedSeconds / 60) % 1) * 100
210
133
  progressInterpolator.setTarget([initialProgress])
134
+ }
211
135
 
212
- if (autoStart) {
213
- startTimeRef.current = Date.now() - elapsedSeconds * 1000
214
- setIsRunning(true)
215
- } else {
216
- startTimeRef.current = null
136
+ if (remainingSeconds === 0) {
137
+ setIsRunning(false)
138
+ startTimeRef.current = null
139
+ // Trigger completion callback immediately if time is already up
140
+ if (onCycleEnd) {
141
+ setTimeout(() => onCycleEnd(), 0)
217
142
  }
143
+ } else if (autoStart) {
144
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
145
+ setIsRunning(true)
146
+ } else {
147
+ startTimeRef.current = null
218
148
  }
219
149
  },
220
150
  [autoStart, onCycleEnd, progressInterpolator],
@@ -229,32 +159,11 @@ export const CycleTimer = externalizeComponent(
229
159
  // Calculate exact progress position and smoothly animate to it when pausing
230
160
  // This ensures the visual progress matches the actual elapsed time
231
161
  const totalElapsed = pausedTimeRef.current / 1000
232
-
233
- if (maxTime !== null) {
234
- // Count-down mode
235
- const exactProgress = Math.min(100, (totalElapsed / maxTime) * 100)
236
- progressInterpolator.setTarget([exactProgress])
237
- } else {
238
- // Count-up mode: progress based on minute steps
239
- const exactProgress = ((totalElapsed / 60) % 1) * 100
240
- progressInterpolator.setTarget([exactProgress])
241
- }
162
+ const exactProgress = Math.min(100, (totalElapsed / maxTime) * 100)
163
+ progressInterpolator.setTarget([exactProgress])
242
164
  }
243
165
  setIsRunning(false)
244
166
  setIsPausedState(true)
245
-
246
- // Trigger brief pause animation
247
- setShowPauseAnimation(true)
248
-
249
- // Clear any existing timeout
250
- if (pauseAnimationTimeoutRef.current) {
251
- clearTimeout(pauseAnimationTimeoutRef.current)
252
- }
253
-
254
- // Reset animation after longer duration
255
- pauseAnimationTimeoutRef.current = setTimeout(() => {
256
- setShowPauseAnimation(false)
257
- }, 800) // 800ms smooth animation
258
167
  }, [isRunning, maxTime, progressInterpolator])
259
168
 
260
169
  const resume = useCallback(() => {
@@ -269,49 +178,6 @@ export const CycleTimer = externalizeComponent(
269
178
  return isPausedState
270
179
  }, [isPausedState])
271
180
 
272
- // Handle error state changes
273
- useEffect(() => {
274
- if (hasError) {
275
- // Error occurred - pause timer if running and remember state
276
- if (isRunning && !isPausedState) {
277
- setWasRunningBeforeError(true)
278
- pause()
279
- }
280
-
281
- // Trigger brief error animation
282
- setShowErrorAnimation(true)
283
-
284
- // Clear any existing timeout
285
- if (errorAnimationTimeoutRef.current) {
286
- clearTimeout(errorAnimationTimeoutRef.current)
287
- }
288
-
289
- // Reset animation after longer duration
290
- errorAnimationTimeoutRef.current = setTimeout(() => {
291
- setShowErrorAnimation(false)
292
- }, 600) // 600ms smooth animation
293
- } else {
294
- // Error resolved - resume if was running before error
295
- if (wasRunningBeforeError && isPausedState) {
296
- setWasRunningBeforeError(false)
297
- resume()
298
- }
299
-
300
- // Clear error animation if error is resolved
301
- setShowErrorAnimation(false)
302
- if (errorAnimationTimeoutRef.current) {
303
- clearTimeout(errorAnimationTimeoutRef.current)
304
- }
305
- }
306
- }, [
307
- hasError,
308
- isRunning,
309
- isPausedState,
310
- wasRunningBeforeError,
311
- pause,
312
- resume,
313
- ])
314
-
315
181
  // Call onCycleComplete immediately to provide the timer control functions
316
182
  useEffect(() => {
317
183
  let isMounted = true
@@ -336,39 +202,30 @@ export const CycleTimer = externalizeComponent(
336
202
  if (isRunning) {
337
203
  // Single animation frame loop that handles both time updates and progress
338
204
  const updateTimer = () => {
339
- if (startTimeRef.current) {
205
+ if (startTimeRef.current && maxTime > 0) {
340
206
  const now = Date.now()
341
207
  const elapsed =
342
208
  (now - startTimeRef.current + pausedTimeRef.current) / 1000
343
-
344
- if (maxTime !== null) {
345
- // Count-down mode
346
- const remaining = Math.max(0, maxTime - elapsed)
347
- setRemainingTime(Math.ceil(remaining))
348
-
349
- // Smoothly animate progress based on elapsed time for fluid visual feedback
350
- const progress = Math.min(100, (elapsed / maxTime) * 100)
351
- progressInterpolator.setTarget([progress])
352
-
353
- if (remaining <= 0) {
354
- setIsRunning(false)
355
- startTimeRef.current = null
356
- setRemainingTime(0)
357
- // Animate to 100% completion with smooth spring transition
358
- progressInterpolator.setTarget([100])
359
- // Call onCycleEnd when timer reaches zero to notify about completion
360
- if (onCycleEnd) {
361
- setTimeout(() => onCycleEnd(), 0)
362
- }
363
- return
209
+ const remaining = Math.max(0, maxTime - elapsed)
210
+
211
+ // Update remaining time based on timestamp calculation
212
+ setRemainingTime(Math.ceil(remaining))
213
+
214
+ // Smoothly animate progress based on elapsed time for fluid visual feedback
215
+ const progress = Math.min(100, (elapsed / maxTime) * 100)
216
+ progressInterpolator.setTarget([progress])
217
+
218
+ if (remaining <= 0) {
219
+ setIsRunning(false)
220
+ startTimeRef.current = null
221
+ setRemainingTime(0)
222
+ // Animate to 100% completion with smooth spring transition
223
+ progressInterpolator.setTarget([100])
224
+ // Call onCycleEnd when timer reaches zero to notify about completion
225
+ if (onCycleEnd) {
226
+ setTimeout(() => onCycleEnd(), 0)
364
227
  }
365
- } else {
366
- // Count-up mode
367
- setRemainingTime(Math.floor(elapsed))
368
-
369
- // For count-up mode, progress completes every minute (0-100% per minute)
370
- const progress = ((elapsed / 60) % 1) * 100
371
- progressInterpolator.setTarget([progress])
228
+ return
372
229
  }
373
230
 
374
231
  // Continue animation loop while running
@@ -412,31 +269,12 @@ export const CycleTimer = externalizeComponent(
412
269
  }
413
270
  }, [progressInterpolator])
414
271
 
415
- // Cleanup animation timeouts on unmount
416
- useEffect(() => {
417
- return () => {
418
- if (pauseAnimationTimeoutRef.current) {
419
- clearTimeout(pauseAnimationTimeoutRef.current)
420
- }
421
- if (errorAnimationTimeoutRef.current) {
422
- clearTimeout(errorAnimationTimeoutRef.current)
423
- }
424
- }
425
- }, [])
426
-
427
272
  // Keep interpolator synchronized with static progress when timer is stopped
428
273
  // Ensures correct visual state when component initializes or timer stops
429
274
  useEffect(() => {
430
- if (!isRunning && !isPausedState) {
431
- if (maxTime !== null && maxTime > 0) {
432
- // Count-down mode
433
- const staticProgress = ((maxTime - remainingTime) / maxTime) * 100
434
- progressInterpolator.setTarget([staticProgress])
435
- } else if (maxTime === null) {
436
- // Count-up mode
437
- const staticProgress = ((remainingTime / 60) % 1) * 100
438
- progressInterpolator.setTarget([staticProgress])
439
- }
275
+ if (!isRunning && !isPausedState && maxTime > 0) {
276
+ const staticProgress = ((maxTime - remainingTime) / maxTime) * 100
277
+ progressInterpolator.setTarget([staticProgress])
440
278
  }
441
279
  }, [
442
280
  isRunning,
@@ -463,92 +301,92 @@ export const CycleTimer = externalizeComponent(
463
301
  sx={{
464
302
  display: "flex",
465
303
  alignItems: "center",
466
- m: 0,
467
- gap: 1, // 8px gap between circle and text
304
+ gap: 0.125, // Minimal gap - 1px
468
305
  }}
469
306
  >
470
- {/* Animated progress ring icon */}
307
+ {/* Animated progress gauge icon */}
471
308
  <Box
472
309
  sx={{
473
- width: 20,
474
- height: 20,
310
+ position: "relative",
311
+ width: 40,
312
+ height: 40,
475
313
  display: "flex",
476
314
  alignItems: "center",
477
315
  justifyContent: "center",
478
- opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
479
- transition: "opacity 0.5s ease-out",
316
+ borderRadius: "50%",
317
+ overflow: "visible",
480
318
  }}
481
319
  >
482
- <svg
483
- width="20"
484
- height="20"
485
- viewBox="0 0 20 20"
486
- style={{ transform: "rotate(-90deg)" }}
487
- role="img"
488
- aria-label="Timer progress"
489
- >
490
- {/* Background ring */}
491
- <circle
492
- cx="10"
493
- cy="10"
494
- r="8"
495
- fill="none"
496
- stroke={
497
- hasError
498
- ? theme.palette.error.light
499
- : theme.palette.success.main
500
- }
501
- strokeWidth="2"
502
- opacity={0.3}
503
- style={{
504
- transition: "stroke 0.5s ease-out",
505
- }}
506
- />
507
- {/* Progress ring */}
508
- <circle
509
- cx="10"
510
- cy="10"
511
- r="8"
512
- fill="none"
513
- stroke={
514
- hasError
515
- ? theme.palette.error.light
516
- : theme.palette.success.main
517
- }
518
- strokeWidth="2"
519
- strokeLinecap="round"
520
- strokeDasharray={`${2 * Math.PI * 8}`}
521
- strokeDashoffset={`${2 * Math.PI * 8 * (1 - progressValue / 100)}`}
522
- style={{
523
- transition:
524
- "stroke-dashoffset 0.1s ease-out, stroke 0.5s ease-out",
525
- }}
526
- />
527
- </svg>
320
+ <Gauge
321
+ width={40}
322
+ height={40}
323
+ value={progressValue}
324
+ valueMin={0}
325
+ valueMax={100}
326
+ innerRadius="70%"
327
+ outerRadius="95%"
328
+ skipAnimation={true}
329
+ sx={{
330
+ opacity: isPausedState ? 0.6 : 1,
331
+ transition: "opacity 0.2s ease",
332
+ [`& .MuiGauge-valueArc`]: {
333
+ fill: theme.palette.success.main,
334
+ },
335
+ [`& .MuiGauge-referenceArc`]: {
336
+ fill: theme.palette.success.main,
337
+ opacity: 0.3,
338
+ },
339
+ [`& .MuiGauge-valueText`]: {
340
+ display: "none",
341
+ },
342
+ [`& .MuiGauge-text`]: {
343
+ display: "none",
344
+ },
345
+ [`& text`]: {
346
+ display: "none",
347
+ },
348
+ // Hide any inner circle elements that might flash
349
+ [`& .MuiGauge-referenceArcBackground`]: {
350
+ display: "none",
351
+ },
352
+ [`& .MuiGauge-valueArcBackground`]: {
353
+ display: "none",
354
+ },
355
+ [`& circle`]: {
356
+ display: "none",
357
+ },
358
+ }}
359
+ />
360
+
361
+ {/* Inner circle overlay to prevent flashing */}
362
+ <Box
363
+ sx={{
364
+ position: "absolute",
365
+ top: "50%",
366
+ left: "50%",
367
+ transform: "translate(-50%, -50%)",
368
+ width: 13,
369
+ height: 13,
370
+ borderRadius: "50%",
371
+ backgroundColor: theme.palette.background?.paper || "white",
372
+ pointerEvents: "none",
373
+ }}
374
+ />
528
375
  </Box>
529
376
 
530
377
  {/* Timer text display */}
531
378
  <Typography
532
379
  variant="body2"
533
380
  sx={{
534
- color: hasError
535
- ? theme.palette.error.light
536
- : theme.palette.text.primary,
381
+ color: theme.palette.text.primary,
537
382
  fontSize: "14px",
538
- transition: "color 0.5s ease-out",
539
383
  }}
540
384
  >
541
- {hasError
542
- ? t("CycleTimer.Error.lb", "Error")
543
- : maxTime !== null
544
- ? // Count-down mode: show remaining time
545
- compact
546
- ? // Compact mode: show remaining time with "min." suffix
547
- `${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
548
- : // Full mode: show "remaining / of total min." format
549
- `${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`
550
- : // Count-up mode: show elapsed time only
551
- formatTime(remainingTime)}
385
+ {compact
386
+ ? // Compact mode: show only remaining time
387
+ formatTime(remainingTime)
388
+ : // Full mode: show "remaining / of total min." format
389
+ `${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`}
552
390
  </Typography>
553
391
  </Box>
554
392
  )
@@ -577,13 +415,10 @@ export const CycleTimer = externalizeComponent(
577
415
  outerRadius="90%"
578
416
  skipAnimation={true}
579
417
  sx={{
580
- opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
581
- transition: "opacity 0.5s ease-out",
418
+ opacity: isPausedState ? 0.6 : 1,
419
+ transition: "opacity 0.2s ease",
582
420
  [`& .MuiGauge-valueArc`]: {
583
- fill: hasError
584
- ? theme.palette.error.light
585
- : theme.palette.success.main,
586
- transition: "fill 0.5s ease-out",
421
+ fill: theme.palette.success.main,
587
422
  },
588
423
  [`& .MuiGauge-referenceArc`]: {
589
424
  fill: "white",
@@ -611,102 +446,42 @@ export const CycleTimer = externalizeComponent(
611
446
  gap: 1,
612
447
  }}
613
448
  >
614
- {/* "remaining time" label - always reserves space to prevent layout shift */}
615
- <Box
449
+ {/* "remaining time" label */}
450
+ <Typography
451
+ variant="body2"
616
452
  sx={{
617
- height: "16px", // Fixed height to prevent layout shift
618
- display: "flex",
619
- alignItems: "center",
620
- justifyContent: "center",
453
+ fontSize: "12px",
454
+ color: theme.palette.text.secondary,
621
455
  marginBottom: 0.5,
622
456
  }}
623
457
  >
624
- <Fade
625
- in={showLabels && maxTime !== null && !hasError}
626
- timeout={300}
627
- >
628
- <Typography
629
- variant="body2"
630
- sx={{
631
- fontSize: "12px",
632
- color: theme.palette.text.secondary,
633
- }}
634
- >
635
- {t("CycleTimer.RemainingTime.lb")}
636
- </Typography>
637
- </Fade>
638
- </Box>
458
+ {t("CycleTimer.RemainingTime.lb")}
459
+ </Typography>
639
460
 
640
- {/* Main timer display with error state transition */}
641
- <Box
461
+ {/* Main timer display */}
462
+ <Typography
463
+ variant="h1"
642
464
  sx={{
643
- position: "relative",
644
- height: "48px", // Fixed height to prevent layout shift
645
- display: "flex",
646
- alignItems: "center",
647
- justifyContent: "center",
465
+ fontSize: "48px",
466
+ fontWeight: 500,
467
+ color: theme.palette.text.primary,
468
+ lineHeight: 1,
648
469
  marginBottom: 0.5,
649
470
  }}
650
471
  >
651
- {/* Error text */}
652
- <Fade in={hasError} timeout={500}>
653
- <Typography
654
- variant="h3"
655
- sx={{
656
- position: "absolute",
657
- fontSize: "40px",
658
- fontWeight: 400,
659
- color: "#FFFFFF",
660
- lineHeight: "116.7%",
661
- }}
662
- >
663
- {t("CycleTimer.Error.lb", "Error")}
664
- </Typography>
665
- </Fade>
666
-
667
- {/* Normal timer text */}
668
- <Fade in={!hasError} timeout={500}>
669
- <Typography
670
- variant="h1"
671
- sx={{
672
- position: "absolute",
673
- fontSize: "48px",
674
- fontWeight: 500,
675
- color: theme.palette.text.primary,
676
- lineHeight: 1,
677
- }}
678
- >
679
- {formatTime(remainingTime)}
680
- </Typography>
681
- </Fade>
682
- </Box>
472
+ {formatTime(remainingTime)}
473
+ </Typography>
683
474
 
684
- {/* Total time display - always reserves space to prevent layout shift */}
685
- <Box
475
+ {/* Total time display */}
476
+ <Typography
477
+ variant="body2"
686
478
  sx={{
687
- height: "16px", // Fixed height to prevent layout shift
688
- display: "flex",
689
- alignItems: "center",
690
- justifyContent: "center",
479
+ fontSize: "12px",
480
+ color: theme.palette.text.secondary,
691
481
  }}
692
482
  >
693
- <Fade
694
- in={showLabels && maxTime !== null && !hasError}
695
- timeout={300}
696
- >
697
- <Typography
698
- variant="body2"
699
- sx={{
700
- fontSize: "12px",
701
- color: theme.palette.text.secondary,
702
- }}
703
- >
704
- {maxTime !== null
705
- ? t("CycleTimer.OfTime.lb", { time: formatTime(maxTime) })
706
- : ""}
707
- </Typography>
708
- </Fade>
709
- </Box>
483
+ {t("CycleTimer.OfTime.lb", { time: formatTime(maxTime) })}
484
+ </Typography>
710
485
  </Box>
711
486
  </Box>
712
487
  )