@wandelbots/wandelbots-js-react-components 2.34.0 → 2.34.1-pr.feature-robot-precondition-list.372.a71f99a
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/AppHeader.d.ts +34 -0
- package/dist/components/AppHeader.d.ts.map +1 -0
- package/dist/components/CycleTimer.d.ts +21 -16
- package/dist/components/CycleTimer.d.ts.map +1 -1
- package/dist/components/DataGrid.d.ts +66 -0
- package/dist/components/DataGrid.d.ts.map +1 -0
- package/dist/components/LogPanel.d.ts +38 -0
- package/dist/components/LogPanel.d.ts.map +1 -0
- package/dist/components/LogStore.d.ts +12 -0
- package/dist/components/LogStore.d.ts.map +1 -0
- package/dist/components/LogViewer.d.ts +46 -0
- package/dist/components/LogViewer.d.ts.map +1 -0
- package/dist/components/ProgramControl.d.ts +8 -2
- package/dist/components/ProgramControl.d.ts.map +1 -1
- package/dist/components/ProgramStateIndicator.d.ts +1 -1
- package/dist/components/ProgramStateIndicator.d.ts.map +1 -1
- package/dist/components/RobotCard.d.ts +100 -0
- package/dist/components/RobotCard.d.ts.map +1 -0
- package/dist/components/RobotListItem.d.ts +34 -0
- package/dist/components/RobotListItem.d.ts.map +1 -0
- package/dist/components/RobotSetupReadinessIndicator.d.ts +31 -0
- package/dist/components/RobotSetupReadinessIndicator.d.ts.map +1 -0
- package/dist/components/RobotSetupReadinessIndicator.test.d.ts +2 -0
- package/dist/components/RobotSetupReadinessIndicator.test.d.ts.map +1 -0
- package/dist/components/TabBar.d.ts +30 -0
- package/dist/components/TabBar.d.ts.map +1 -0
- package/dist/components/robots/Robot.d.ts +3 -2
- package/dist/components/robots/Robot.d.ts.map +1 -1
- package/dist/components/robots/manufacturerHomePositions.d.ts +21 -0
- package/dist/components/robots/manufacturerHomePositions.d.ts.map +1 -0
- package/dist/icons/DropdownArrowIcon.d.ts +3 -0
- package/dist/icons/DropdownArrowIcon.d.ts.map +1 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/index.cjs +49 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11166 -9377
- package/dist/index.js.map +1 -1
- package/dist/themes/createDarkTheme.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/components/AppHeader.md +84 -0
- package/src/components/AppHeader.tsx +199 -0
- package/src/components/CycleTimer.tsx +229 -148
- package/src/components/DataGrid.tsx +659 -0
- package/src/components/LogPanel.tsx +69 -0
- package/src/components/LogStore.ts +44 -0
- package/src/components/LogViewer.tsx +370 -0
- package/src/components/ProgramControl.tsx +27 -12
- package/src/components/ProgramStateIndicator.tsx +25 -8
- package/src/components/RobotCard.tsx +559 -0
- package/src/components/RobotListItem.tsx +150 -0
- package/src/components/RobotSetupReadinessIndicator.test.tsx +60 -0
- package/src/components/RobotSetupReadinessIndicator.tsx +124 -0
- package/src/components/TabBar.tsx +144 -0
- package/src/components/robots/Robot.tsx +5 -2
- package/src/components/robots/manufacturerHomePositions.ts +76 -0
- package/src/i18n/locales/de/translations.json +7 -1
- package/src/i18n/locales/en/translations.json +7 -1
- package/src/icons/DropdownArrowIcon.tsx +13 -0
- package/src/icons/chevronDown.svg +3 -0
- package/src/icons/index.ts +1 -0
- package/src/index.ts +14 -0
- package/src/themes/createDarkTheme.ts +75 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Typography, useTheme } from "@mui/material"
|
|
1
|
+
import { Box, Fade, 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
|
|
12
|
+
* - `startNewCycle(maxTimeSeconds?, elapsedSeconds?)` - Start a new timer cycle (if maxTimeSeconds is omitted, runs as count-up timer)
|
|
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
|
|
18
|
+
startNewCycle: (maxTimeSeconds?: number, elapsedSeconds?: number) => void
|
|
19
19
|
pause: () => void
|
|
20
20
|
resume: () => void
|
|
21
21
|
isPaused: () => boolean
|
|
@@ -33,13 +33,15 @@ export interface CycleTimerProps {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* A circular gauge timer component that shows the remaining time of a cycle
|
|
36
|
+
* A circular gauge timer component that shows the remaining time of a cycle or counts up
|
|
37
37
|
*
|
|
38
38
|
* Features:
|
|
39
39
|
* - Circular gauge with 264px diameter and 40px thickness
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
40
|
+
* - Two modes: count-down (with max time) or count-up (without max time)
|
|
41
|
+
* - Count-down mode: shows remaining time prominently, counts down to zero
|
|
42
|
+
* - Count-up mode: shows elapsed time, gauge progresses in minute steps
|
|
43
|
+
* - Displays appropriate labels based on mode
|
|
44
|
+
* - Automatically counts down/up and triggers callback when reaching zero (count-down only)
|
|
43
45
|
* - Full timer control: start, pause, resume functionality
|
|
44
46
|
* - Support for starting with elapsed time (resume mid-cycle)
|
|
45
47
|
* - Smooth spring-based progress animations for all state transitions
|
|
@@ -56,29 +58,32 @@ export interface CycleTimerProps {
|
|
|
56
58
|
*
|
|
57
59
|
* Usage:
|
|
58
60
|
* ```tsx
|
|
61
|
+
* // Count-down timer (with max time)
|
|
59
62
|
* <CycleTimer
|
|
60
63
|
* onCycleComplete={(controls) => {
|
|
61
|
-
* // Start a 5-minute cycle
|
|
64
|
+
* // Start a 5-minute countdown cycle
|
|
62
65
|
* controls.startNewCycle(300)
|
|
63
66
|
*
|
|
64
67
|
* // Or start a 5-minute cycle with 2 minutes already elapsed
|
|
65
68
|
* controls.startNewCycle(300, 120)
|
|
69
|
+
* }}
|
|
70
|
+
* onCycleEnd={() => console.log('Cycle completed!')}
|
|
71
|
+
* />
|
|
66
72
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* //
|
|
71
|
-
* controls.
|
|
73
|
+
* // Count-up timer (no max time)
|
|
74
|
+
* <CycleTimer
|
|
75
|
+
* onCycleComplete={(controls) => {
|
|
76
|
+
* // Start count-up timer
|
|
77
|
+
* controls.startNewCycle()
|
|
72
78
|
*
|
|
73
|
-
* //
|
|
74
|
-
*
|
|
79
|
+
* // Or start count-up timer with some elapsed time
|
|
80
|
+
* controls.startNewCycle(undefined, 120)
|
|
75
81
|
* }}
|
|
76
|
-
* onCycleEnd={() => console.log('Cycle completed!')}
|
|
77
82
|
* />
|
|
78
83
|
* ```
|
|
79
84
|
*
|
|
80
85
|
* Control Functions:
|
|
81
|
-
* - `startNewCycle(maxTimeSeconds
|
|
86
|
+
* - `startNewCycle(maxTimeSeconds?, elapsedSeconds?)` - Start a new timer cycle (omit maxTimeSeconds for count-up mode)
|
|
82
87
|
* - `pause()` - Pause the countdown while preserving remaining time
|
|
83
88
|
* - `resume()` - Resume countdown from where it was paused
|
|
84
89
|
* - `isPaused()` - Check current pause state
|
|
@@ -96,7 +101,7 @@ export const CycleTimer = externalizeComponent(
|
|
|
96
101
|
const theme = useTheme()
|
|
97
102
|
const { t } = useTranslation()
|
|
98
103
|
const [remainingTime, setRemainingTime] = useState(0)
|
|
99
|
-
const [maxTime, setMaxTime] = useState(
|
|
104
|
+
const [maxTime, setMaxTime] = useState<number | null>(null)
|
|
100
105
|
const [isRunning, setIsRunning] = useState(false)
|
|
101
106
|
const [isPausedState, setIsPausedState] = useState(false)
|
|
102
107
|
const [currentProgress, setCurrentProgress] = useState(0)
|
|
@@ -104,6 +109,10 @@ export const CycleTimer = externalizeComponent(
|
|
|
104
109
|
const startTimeRef = useRef<number | null>(null)
|
|
105
110
|
const pausedTimeRef = useRef<number>(0)
|
|
106
111
|
|
|
112
|
+
// Track mode changes for fade transitions
|
|
113
|
+
const [showLabels, setShowLabels] = useState(true)
|
|
114
|
+
const prevMaxTimeRef = useRef<number | null | undefined>(undefined)
|
|
115
|
+
|
|
107
116
|
// Spring-based interpolator for smooth gauge progress animations
|
|
108
117
|
// Uses physics simulation to create natural, smooth transitions between progress values
|
|
109
118
|
const [progressInterpolator] = useInterpolation([0], {
|
|
@@ -114,37 +123,78 @@ export const CycleTimer = externalizeComponent(
|
|
|
114
123
|
},
|
|
115
124
|
})
|
|
116
125
|
|
|
126
|
+
// Handle mode changes with fade transitions for labels only
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const currentIsCountUp = maxTime === null
|
|
129
|
+
const prevMaxTime = prevMaxTimeRef.current
|
|
130
|
+
const prevIsCountUp = prevMaxTime === null
|
|
131
|
+
|
|
132
|
+
// Check if mode actually changed (not just first render)
|
|
133
|
+
if (
|
|
134
|
+
prevMaxTimeRef.current !== undefined &&
|
|
135
|
+
prevIsCountUp !== currentIsCountUp
|
|
136
|
+
) {
|
|
137
|
+
// Mode changed - labels will fade based on the Fade component conditions
|
|
138
|
+
// We just need to ensure showLabels is true so Fade can control visibility
|
|
139
|
+
setShowLabels(true)
|
|
140
|
+
} else {
|
|
141
|
+
// No mode change or first time - set initial state
|
|
142
|
+
setShowLabels(true)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
prevMaxTimeRef.current = maxTime
|
|
146
|
+
}, [maxTime])
|
|
147
|
+
|
|
117
148
|
const startNewCycle = useCallback(
|
|
118
|
-
(maxTimeSeconds
|
|
119
|
-
setMaxTime(maxTimeSeconds)
|
|
120
|
-
const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
|
|
121
|
-
setRemainingTime(remainingSeconds)
|
|
149
|
+
(maxTimeSeconds?: number, elapsedSeconds: number = 0) => {
|
|
150
|
+
setMaxTime(maxTimeSeconds ?? null)
|
|
122
151
|
setIsPausedState(false)
|
|
123
152
|
pausedTimeRef.current = 0
|
|
124
153
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
if (maxTimeSeconds !== undefined) {
|
|
155
|
+
// Count-down mode: set remaining time
|
|
156
|
+
const remainingSeconds = Math.max(
|
|
157
|
+
0,
|
|
158
|
+
maxTimeSeconds - elapsedSeconds,
|
|
159
|
+
)
|
|
160
|
+
setRemainingTime(remainingSeconds)
|
|
161
|
+
|
|
162
|
+
// Animate progress smoothly to starting position
|
|
163
|
+
const initialProgress =
|
|
164
|
+
elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
|
|
165
|
+
if (elapsedSeconds === 0) {
|
|
166
|
+
progressInterpolator.setTarget([0])
|
|
167
|
+
} else {
|
|
168
|
+
progressInterpolator.setTarget([initialProgress])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (remainingSeconds === 0) {
|
|
172
|
+
setIsRunning(false)
|
|
173
|
+
startTimeRef.current = null
|
|
174
|
+
// Trigger completion callback immediately if time is already up
|
|
175
|
+
if (onCycleEnd) {
|
|
176
|
+
setTimeout(() => onCycleEnd(), 0)
|
|
177
|
+
}
|
|
178
|
+
} else if (autoStart) {
|
|
179
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
180
|
+
setIsRunning(true)
|
|
181
|
+
} else {
|
|
182
|
+
startTimeRef.current = null
|
|
183
|
+
}
|
|
132
184
|
} else {
|
|
185
|
+
// Count-up mode: start from elapsed time
|
|
186
|
+
setRemainingTime(elapsedSeconds)
|
|
187
|
+
|
|
188
|
+
// For count-up mode, progress is based on minute steps
|
|
189
|
+
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
133
190
|
progressInterpolator.setTarget([initialProgress])
|
|
134
|
-
}
|
|
135
191
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
setTimeout(() => onCycleEnd(), 0)
|
|
192
|
+
if (autoStart) {
|
|
193
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
194
|
+
setIsRunning(true)
|
|
195
|
+
} else {
|
|
196
|
+
startTimeRef.current = null
|
|
142
197
|
}
|
|
143
|
-
} else if (autoStart) {
|
|
144
|
-
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
145
|
-
setIsRunning(true)
|
|
146
|
-
} else {
|
|
147
|
-
startTimeRef.current = null
|
|
148
198
|
}
|
|
149
199
|
},
|
|
150
200
|
[autoStart, onCycleEnd, progressInterpolator],
|
|
@@ -159,8 +209,16 @@ export const CycleTimer = externalizeComponent(
|
|
|
159
209
|
// Calculate exact progress position and smoothly animate to it when pausing
|
|
160
210
|
// This ensures the visual progress matches the actual elapsed time
|
|
161
211
|
const totalElapsed = pausedTimeRef.current / 1000
|
|
162
|
-
|
|
163
|
-
|
|
212
|
+
|
|
213
|
+
if (maxTime !== null) {
|
|
214
|
+
// Count-down mode
|
|
215
|
+
const exactProgress = Math.min(100, (totalElapsed / maxTime) * 100)
|
|
216
|
+
progressInterpolator.setTarget([exactProgress])
|
|
217
|
+
} else {
|
|
218
|
+
// Count-up mode: progress based on minute steps
|
|
219
|
+
const exactProgress = ((totalElapsed / 60) % 1) * 100
|
|
220
|
+
progressInterpolator.setTarget([exactProgress])
|
|
221
|
+
}
|
|
164
222
|
}
|
|
165
223
|
setIsRunning(false)
|
|
166
224
|
setIsPausedState(true)
|
|
@@ -202,30 +260,39 @@ export const CycleTimer = externalizeComponent(
|
|
|
202
260
|
if (isRunning) {
|
|
203
261
|
// Single animation frame loop that handles both time updates and progress
|
|
204
262
|
const updateTimer = () => {
|
|
205
|
-
if (startTimeRef.current
|
|
263
|
+
if (startTimeRef.current) {
|
|
206
264
|
const now = Date.now()
|
|
207
265
|
const elapsed =
|
|
208
266
|
(now - startTimeRef.current + pausedTimeRef.current) / 1000
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
267
|
+
|
|
268
|
+
if (maxTime !== null) {
|
|
269
|
+
// Count-down mode
|
|
270
|
+
const remaining = Math.max(0, maxTime - elapsed)
|
|
271
|
+
setRemainingTime(Math.ceil(remaining))
|
|
272
|
+
|
|
273
|
+
// Smoothly animate progress based on elapsed time for fluid visual feedback
|
|
274
|
+
const progress = Math.min(100, (elapsed / maxTime) * 100)
|
|
275
|
+
progressInterpolator.setTarget([progress])
|
|
276
|
+
|
|
277
|
+
if (remaining <= 0) {
|
|
278
|
+
setIsRunning(false)
|
|
279
|
+
startTimeRef.current = null
|
|
280
|
+
setRemainingTime(0)
|
|
281
|
+
// Animate to 100% completion with smooth spring transition
|
|
282
|
+
progressInterpolator.setTarget([100])
|
|
283
|
+
// Call onCycleEnd when timer reaches zero to notify about completion
|
|
284
|
+
if (onCycleEnd) {
|
|
285
|
+
setTimeout(() => onCycleEnd(), 0)
|
|
286
|
+
}
|
|
287
|
+
return
|
|
227
288
|
}
|
|
228
|
-
|
|
289
|
+
} else {
|
|
290
|
+
// Count-up mode
|
|
291
|
+
setRemainingTime(Math.floor(elapsed))
|
|
292
|
+
|
|
293
|
+
// For count-up mode, progress completes every minute (0-100% per minute)
|
|
294
|
+
const progress = ((elapsed / 60) % 1) * 100
|
|
295
|
+
progressInterpolator.setTarget([progress])
|
|
229
296
|
}
|
|
230
297
|
|
|
231
298
|
// Continue animation loop while running
|
|
@@ -272,9 +339,16 @@ export const CycleTimer = externalizeComponent(
|
|
|
272
339
|
// Keep interpolator synchronized with static progress when timer is stopped
|
|
273
340
|
// Ensures correct visual state when component initializes or timer stops
|
|
274
341
|
useEffect(() => {
|
|
275
|
-
if (!isRunning && !isPausedState
|
|
276
|
-
|
|
277
|
-
|
|
342
|
+
if (!isRunning && !isPausedState) {
|
|
343
|
+
if (maxTime !== null && maxTime > 0) {
|
|
344
|
+
// Count-down mode
|
|
345
|
+
const staticProgress = ((maxTime - remainingTime) / maxTime) * 100
|
|
346
|
+
progressInterpolator.setTarget([staticProgress])
|
|
347
|
+
} else if (maxTime === null) {
|
|
348
|
+
// Count-up mode
|
|
349
|
+
const staticProgress = ((remainingTime / 60) % 1) * 100
|
|
350
|
+
progressInterpolator.setTarget([staticProgress])
|
|
351
|
+
}
|
|
278
352
|
}
|
|
279
353
|
}, [
|
|
280
354
|
isRunning,
|
|
@@ -301,77 +375,56 @@ export const CycleTimer = externalizeComponent(
|
|
|
301
375
|
sx={{
|
|
302
376
|
display: "flex",
|
|
303
377
|
alignItems: "center",
|
|
304
|
-
|
|
378
|
+
m: 0,
|
|
379
|
+
gap: 1, // 8px gap between circle and text
|
|
305
380
|
}}
|
|
306
381
|
>
|
|
307
|
-
{/* Animated progress
|
|
382
|
+
{/* Animated progress ring icon */}
|
|
308
383
|
<Box
|
|
309
384
|
sx={{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
height: 40,
|
|
385
|
+
width: 20,
|
|
386
|
+
height: 20,
|
|
313
387
|
display: "flex",
|
|
314
388
|
alignItems: "center",
|
|
315
389
|
justifyContent: "center",
|
|
316
|
-
|
|
317
|
-
|
|
390
|
+
opacity: isPausedState ? 0.6 : 1,
|
|
391
|
+
transition: "opacity 0.2s ease",
|
|
318
392
|
}}
|
|
319
393
|
>
|
|
320
|
-
<
|
|
321
|
-
width=
|
|
322
|
-
height=
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
/>
|
|
394
|
+
<svg
|
|
395
|
+
width="20"
|
|
396
|
+
height="20"
|
|
397
|
+
viewBox="0 0 20 20"
|
|
398
|
+
style={{ transform: "rotate(-90deg)" }}
|
|
399
|
+
role="img"
|
|
400
|
+
aria-label="Timer progress"
|
|
401
|
+
>
|
|
402
|
+
{/* Background ring */}
|
|
403
|
+
<circle
|
|
404
|
+
cx="10"
|
|
405
|
+
cy="10"
|
|
406
|
+
r="8"
|
|
407
|
+
fill="none"
|
|
408
|
+
stroke={theme.palette.success.main}
|
|
409
|
+
strokeWidth="2"
|
|
410
|
+
opacity={0.3}
|
|
411
|
+
/>
|
|
412
|
+
{/* Progress ring */}
|
|
413
|
+
<circle
|
|
414
|
+
cx="10"
|
|
415
|
+
cy="10"
|
|
416
|
+
r="8"
|
|
417
|
+
fill="none"
|
|
418
|
+
stroke={theme.palette.success.main}
|
|
419
|
+
strokeWidth="2"
|
|
420
|
+
strokeLinecap="round"
|
|
421
|
+
strokeDasharray={`${2 * Math.PI * 8}`}
|
|
422
|
+
strokeDashoffset={`${2 * Math.PI * 8 * (1 - progressValue / 100)}`}
|
|
423
|
+
style={{
|
|
424
|
+
transition: "stroke-dashoffset 0.1s ease-out",
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
</svg>
|
|
375
428
|
</Box>
|
|
376
429
|
|
|
377
430
|
{/* Timer text display */}
|
|
@@ -382,11 +435,15 @@ export const CycleTimer = externalizeComponent(
|
|
|
382
435
|
fontSize: "14px",
|
|
383
436
|
}}
|
|
384
437
|
>
|
|
385
|
-
{
|
|
386
|
-
? //
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
438
|
+
{maxTime !== null
|
|
439
|
+
? // Count-down mode: show remaining time
|
|
440
|
+
compact
|
|
441
|
+
? // Compact mode: show remaining time with "min." suffix
|
|
442
|
+
`${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
|
|
443
|
+
: // Full mode: show "remaining / of total min." format
|
|
444
|
+
`${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`
|
|
445
|
+
: // Count-up mode: show elapsed time only
|
|
446
|
+
formatTime(remainingTime)}
|
|
390
447
|
</Typography>
|
|
391
448
|
</Box>
|
|
392
449
|
)
|
|
@@ -446,19 +503,30 @@ export const CycleTimer = externalizeComponent(
|
|
|
446
503
|
gap: 1,
|
|
447
504
|
}}
|
|
448
505
|
>
|
|
449
|
-
{/* "remaining time" label */}
|
|
450
|
-
<
|
|
451
|
-
variant="body2"
|
|
506
|
+
{/* "remaining time" label - always reserves space to prevent layout shift */}
|
|
507
|
+
<Box
|
|
452
508
|
sx={{
|
|
453
|
-
|
|
454
|
-
|
|
509
|
+
height: "16px", // Fixed height to prevent layout shift
|
|
510
|
+
display: "flex",
|
|
511
|
+
alignItems: "center",
|
|
512
|
+
justifyContent: "center",
|
|
455
513
|
marginBottom: 0.5,
|
|
456
514
|
}}
|
|
457
515
|
>
|
|
458
|
-
{
|
|
459
|
-
|
|
516
|
+
<Fade in={showLabels && maxTime !== null} timeout={300}>
|
|
517
|
+
<Typography
|
|
518
|
+
variant="body2"
|
|
519
|
+
sx={{
|
|
520
|
+
fontSize: "12px",
|
|
521
|
+
color: theme.palette.text.secondary,
|
|
522
|
+
}}
|
|
523
|
+
>
|
|
524
|
+
{t("CycleTimer.RemainingTime.lb")}
|
|
525
|
+
</Typography>
|
|
526
|
+
</Fade>
|
|
527
|
+
</Box>
|
|
460
528
|
|
|
461
|
-
{/* Main timer display */}
|
|
529
|
+
{/* Main timer display - never fades, always visible */}
|
|
462
530
|
<Typography
|
|
463
531
|
variant="h1"
|
|
464
532
|
sx={{
|
|
@@ -472,16 +540,29 @@ export const CycleTimer = externalizeComponent(
|
|
|
472
540
|
{formatTime(remainingTime)}
|
|
473
541
|
</Typography>
|
|
474
542
|
|
|
475
|
-
{/* Total time display */}
|
|
476
|
-
<
|
|
477
|
-
variant="body2"
|
|
543
|
+
{/* Total time display - always reserves space to prevent layout shift */}
|
|
544
|
+
<Box
|
|
478
545
|
sx={{
|
|
479
|
-
|
|
480
|
-
|
|
546
|
+
height: "16px", // Fixed height to prevent layout shift
|
|
547
|
+
display: "flex",
|
|
548
|
+
alignItems: "center",
|
|
549
|
+
justifyContent: "center",
|
|
481
550
|
}}
|
|
482
551
|
>
|
|
483
|
-
|
|
484
|
-
|
|
552
|
+
<Fade in={showLabels && maxTime !== null} timeout={300}>
|
|
553
|
+
<Typography
|
|
554
|
+
variant="body2"
|
|
555
|
+
sx={{
|
|
556
|
+
fontSize: "12px",
|
|
557
|
+
color: theme.palette.text.secondary,
|
|
558
|
+
}}
|
|
559
|
+
>
|
|
560
|
+
{maxTime !== null
|
|
561
|
+
? t("CycleTimer.OfTime.lb", { time: formatTime(maxTime) })
|
|
562
|
+
: ""}
|
|
563
|
+
</Typography>
|
|
564
|
+
</Fade>
|
|
565
|
+
</Box>
|
|
485
566
|
</Box>
|
|
486
567
|
</Box>
|
|
487
568
|
)
|