@wandelbots/wandelbots-js-react-components 2.39.0 → 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.
Files changed (60) hide show
  1. package/dist/components/3d-viewport/collider/ColliderCollection.d.ts +11 -0
  2. package/dist/components/3d-viewport/collider/ColliderCollection.d.ts.map +1 -0
  3. package/dist/components/3d-viewport/collider/ColliderElement.d.ts +10 -0
  4. package/dist/components/3d-viewport/collider/ColliderElement.d.ts.map +1 -0
  5. package/dist/components/3d-viewport/collider/CollisionSceneRenderer.d.ts +9 -0
  6. package/dist/components/3d-viewport/collider/CollisionSceneRenderer.d.ts.map +1 -0
  7. package/dist/components/3d-viewport/collider/colliderShapeToBufferGeometry.d.ts +4 -0
  8. package/dist/components/3d-viewport/collider/colliderShapeToBufferGeometry.d.ts.map +1 -0
  9. package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -1
  10. package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -1
  11. package/dist/components/CycleTimer/index.d.ts +4 -5
  12. package/dist/components/CycleTimer/index.d.ts.map +1 -1
  13. package/dist/components/CycleTimer/types.d.ts +2 -3
  14. package/dist/components/CycleTimer/types.d.ts.map +1 -1
  15. package/dist/components/CycleTimer/useTimerLogic.d.ts +1 -2
  16. package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -1
  17. package/dist/components/Timer/Timer.d.ts +3 -0
  18. package/dist/components/Timer/Timer.d.ts.map +1 -0
  19. package/dist/components/Timer/TimerDefaultVariant.d.ts +10 -0
  20. package/dist/components/Timer/TimerDefaultVariant.d.ts.map +1 -0
  21. package/dist/components/Timer/TimerSmallVariant.d.ts +11 -0
  22. package/dist/components/Timer/TimerSmallVariant.d.ts.map +1 -0
  23. package/dist/components/Timer/index.d.ts +19 -0
  24. package/dist/components/Timer/index.d.ts.map +1 -0
  25. package/dist/components/Timer/types.d.ts +36 -0
  26. package/dist/components/Timer/types.d.ts.map +1 -0
  27. package/dist/components/Timer/useTimerAnimations.d.ts +11 -0
  28. package/dist/components/Timer/useTimerAnimations.d.ts.map +1 -0
  29. package/dist/components/Timer/useTimerLogic.d.ts +20 -0
  30. package/dist/components/Timer/useTimerLogic.d.ts.map +1 -0
  31. package/dist/components/Timer/utils.d.ts +9 -0
  32. package/dist/components/Timer/utils.d.ts.map +1 -0
  33. package/dist/index.cjs +47 -47
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5274 -4765
  38. package/dist/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/3d-viewport/collider/ColliderCollection.tsx +34 -0
  41. package/src/components/3d-viewport/collider/ColliderElement.tsx +36 -0
  42. package/src/components/3d-viewport/collider/CollisionSceneRenderer.tsx +26 -0
  43. package/src/components/3d-viewport/collider/colliderShapeToBufferGeometry.ts +48 -0
  44. package/src/components/CycleTimer/DefaultVariant.tsx +0 -2
  45. package/src/components/CycleTimer/SmallVariant.tsx +2 -5
  46. package/src/components/CycleTimer/index.tsx +4 -5
  47. package/src/components/CycleTimer/types.ts +1 -3
  48. package/src/components/CycleTimer/useTimerLogic.ts +27 -82
  49. package/src/components/CycleTimer/utils.ts +3 -3
  50. package/src/components/Timer/Timer.ts +2 -0
  51. package/src/components/Timer/TimerDefaultVariant.tsx +140 -0
  52. package/src/components/Timer/TimerSmallVariant.tsx +140 -0
  53. package/src/components/Timer/index.tsx +101 -0
  54. package/src/components/Timer/types.ts +38 -0
  55. package/src/components/Timer/useTimerAnimations.ts +94 -0
  56. package/src/components/Timer/useTimerLogic.ts +214 -0
  57. package/src/components/Timer/utils.ts +15 -0
  58. package/src/i18n/locales/de/translations.json +1 -0
  59. package/src/i18n/locales/en/translations.json +1 -0
  60. package/src/index.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.39.0",
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,
@@ -0,0 +1,34 @@
1
+ import type { ThreeElements } from "@react-three/fiber"
2
+ import type { Collider } from "@wandelbots/nova-api/v1"
3
+ import ColliderElement from "./ColliderElement"
4
+
5
+ export type MeshChildrenProvider = (
6
+ key: string,
7
+ collider: Collider,
8
+ ) => React.ReactNode
9
+
10
+ type ColliderCollectionProps = {
11
+ name?: string
12
+ colliders: Record<string, Collider>
13
+ meshChildrenProvider: MeshChildrenProvider
14
+ } & ThreeElements["group"]
15
+
16
+ export default function ColliderCollection({
17
+ name,
18
+ colliders,
19
+ meshChildrenProvider,
20
+ ...props
21
+ }: ColliderCollectionProps) {
22
+ return (
23
+ <group name={name} {...props}>
24
+ {Object.entries(colliders).map(([colliderKey, collider]) => (
25
+ <ColliderElement
26
+ key={colliderKey}
27
+ name={colliderKey}
28
+ collider={collider}
29
+ children={meshChildrenProvider(colliderKey, collider)}
30
+ />
31
+ ))}
32
+ </group>
33
+ )
34
+ }
@@ -0,0 +1,36 @@
1
+ import type { Collider } from "@wandelbots/nova-api/v1"
2
+ import type React from "react"
3
+ import * as THREE from "three"
4
+ import { colliderShapeToBufferGeometry } from "./colliderShapeToBufferGeometry"
5
+
6
+ type ColliderElementProps = {
7
+ name?: string
8
+ collider: Collider
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ export default function ColliderElement({
13
+ name,
14
+ collider,
15
+ children,
16
+ }: ColliderElementProps) {
17
+ const position = collider.pose?.position ?? [0, 0, 0]
18
+ const rotation = collider.pose?.orientation ?? [0, 0, 0]
19
+ if (collider.margin) {
20
+ console.warn(`${name} margin is not supported`)
21
+ }
22
+ return (
23
+ <mesh
24
+ name={name}
25
+ position={new THREE.Vector3(
26
+ position[0],
27
+ position[1],
28
+ position[2],
29
+ ).divideScalar(1000)}
30
+ rotation={new THREE.Euler(rotation[0], rotation[1], rotation[2], "XYZ")}
31
+ geometry={colliderShapeToBufferGeometry(collider.shape)}
32
+ >
33
+ {children}
34
+ </mesh>
35
+ )
36
+ }
@@ -0,0 +1,26 @@
1
+ import type { CollisionScene } from "@wandelbots/nova-api/v1"
2
+ import ColliderCollection, {
3
+ type MeshChildrenProvider,
4
+ } from "./ColliderCollection"
5
+
6
+ type CollisionSceneRendererProps = {
7
+ scene: CollisionScene
8
+ meshChildrenProvider: MeshChildrenProvider
9
+ }
10
+
11
+ export default function CollisionSceneRenderer({
12
+ scene,
13
+ meshChildrenProvider,
14
+ }: CollisionSceneRendererProps) {
15
+ const colliders = scene.colliders
16
+ return (
17
+ <group>
18
+ {colliders && (
19
+ <ColliderCollection
20
+ meshChildrenProvider={meshChildrenProvider}
21
+ colliders={colliders}
22
+ />
23
+ )}
24
+ </group>
25
+ )
26
+ }
@@ -0,0 +1,48 @@
1
+ import type { ColliderShape } from "@wandelbots/nova-api/v1"
2
+ import * as THREE from "three"
3
+ import { ConvexGeometry } from "three-stdlib"
4
+
5
+ export function colliderShapeToBufferGeometry(
6
+ shape: ColliderShape,
7
+ ): THREE.BufferGeometry {
8
+ const shapeType = shape.shape_type
9
+ switch (shapeType) {
10
+ case "convex_hull":
11
+ return new ConvexGeometry(
12
+ shape.vertices.map(
13
+ (vertex) =>
14
+ new THREE.Vector3(
15
+ vertex[0] / 1000,
16
+ vertex[1] / 1000,
17
+ vertex[2] / 1000,
18
+ ),
19
+ ),
20
+ )
21
+ case "box":
22
+ return new THREE.BoxGeometry(
23
+ shape.size_x / 1000,
24
+ shape.size_y / 1000,
25
+ shape.size_z / 1000,
26
+ )
27
+ case "sphere":
28
+ return new THREE.SphereGeometry(shape.radius / 1000)
29
+ case "capsule":
30
+ return new THREE.CapsuleGeometry(
31
+ shape.radius / 1000,
32
+ shape.cylinder_height / 1000,
33
+ )
34
+ case "cylinder":
35
+ return new THREE.CylinderGeometry(
36
+ shape.radius / 1000,
37
+ shape.radius / 1000,
38
+ shape.height / 1000,
39
+ )
40
+ case "rectangle": {
41
+ return new THREE.BoxGeometry(shape.size_x / 1000, shape.size_y / 1000, 0)
42
+ }
43
+ default: {
44
+ console.warn(`${shape.shape_type} is not supported`)
45
+ return new THREE.BufferGeometry()
46
+ }
47
+ }
48
+ }
@@ -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 && (currentState === "countup" || currentState === "idle")) {
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 that shows the remaining time of a cycle or counts up
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, countup, success
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/up and triggers callback when reaching zero (countdown only)
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 or simple text-only mode
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?: number, elapsedSeconds?: number) => void
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?: number, elapsedSeconds: number = 0) => {
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: newState,
126
- maxTime: maxTimeSeconds ?? null,
99
+ currentState: "countdown",
100
+ maxTime: maxTimeSeconds,
127
101
  isPausedState: false,
128
102
  }))
129
103
  pausedTimeRef.current = 0
130
104
 
131
- if (maxTimeSeconds !== undefined) {
132
- // Count-down mode
133
- const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
134
- const initialProgress =
135
- elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
136
-
137
- setTimerState((prev) => ({
138
- ...prev,
139
- remainingTime: remainingSeconds,
140
- currentProgress: initialProgress, // Immediately set progress
141
- }))
142
-
143
- progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
144
-
145
- if (remainingSeconds === 0) {
146
- setTimerState((prev) => ({ ...prev, isRunning: false }))
147
- startTimeRef.current = null
148
- if (onCycleEnd) {
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
- // Count-up mode
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" || currentState === "countup") {
29
- // Count-up modes: progress based on minute steps (0-100% per minute)
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" || currentState === "countup") {
48
+ if (currentState === "measuring") {
49
49
  return ((totalElapsed / 60) % 1) * 100
50
50
  }
51
51
 
@@ -0,0 +1,2 @@
1
+ export { Timer } from "./index"
2
+ export type { TimerControls, TimerProps } from "./types"
@@ -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
+ }