@wandelbots/wandelbots-js-react-components 2.29.0 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CycleTimer.d.ts +80 -0
- package/dist/components/CycleTimer.d.ts.map +1 -0
- package/dist/components/jogging/JoggingStore.d.ts +1 -1
- package/dist/index.cjs +82 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16741 -8698
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/CycleTimer.tsx +490 -0
- package/src/i18n/locales/de/translations.json +3 -0
- package/src/i18n/locales/en/translations.json +3 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wandelbots/wandelbots-js-react-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.0",
|
|
4
4
|
"description": "React UI toolkit for building applications on top of the Wandelbots platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
"dependencies": {
|
|
140
140
|
"@monaco-editor/react": "^4.7.0",
|
|
141
|
+
"@mui/x-charts": "^8.9.0",
|
|
141
142
|
"@shikijs/monaco": "^3.1.0",
|
|
142
143
|
"i18next-browser-languagedetector": "^8.0.4",
|
|
143
144
|
"lodash-es": "^4.17.21",
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { Box, Typography, useTheme } from "@mui/material"
|
|
2
|
+
import { Gauge } from "@mui/x-charts/Gauge"
|
|
3
|
+
import { observer } from "mobx-react-lite"
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
5
|
+
import { useTranslation } from "react-i18next"
|
|
6
|
+
import { externalizeComponent } from "../externalizeComponent"
|
|
7
|
+
import { useInterpolation } from "./utils/interpolation"
|
|
8
|
+
|
|
9
|
+
export interface CycleTimerProps {
|
|
10
|
+
/**
|
|
11
|
+
* Callback that receives the timer control functions:
|
|
12
|
+
* - `startNewCycle(maxTimeSeconds, elapsedSeconds?)` - Start a new timer cycle
|
|
13
|
+
* - `pause()` - Pause the countdown while preserving remaining time
|
|
14
|
+
* - `resume()` - Resume countdown from where it was paused
|
|
15
|
+
* - `isPaused()` - Check current pause state
|
|
16
|
+
*/
|
|
17
|
+
onCycleComplete: (controls: {
|
|
18
|
+
startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
|
|
19
|
+
pause: () => void
|
|
20
|
+
resume: () => void
|
|
21
|
+
isPaused: () => boolean
|
|
22
|
+
}) => void
|
|
23
|
+
/** Callback fired when a cycle actually completes (reaches zero) */
|
|
24
|
+
onCycleEnd?: () => void
|
|
25
|
+
/** Whether the timer should start automatically when maxTime is set */
|
|
26
|
+
autoStart?: boolean
|
|
27
|
+
/** Visual variant of the timer */
|
|
28
|
+
variant?: "default" | "small"
|
|
29
|
+
/** For small variant: whether to show remaining time details (compact hides them) */
|
|
30
|
+
compact?: boolean
|
|
31
|
+
/** Additional CSS classes */
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A circular gauge timer component that shows the remaining time of a cycle
|
|
37
|
+
*
|
|
38
|
+
* Features:
|
|
39
|
+
* - Circular gauge with 264px diameter and 40px thickness
|
|
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
|
|
43
|
+
* - Full timer control: start, pause, resume functionality
|
|
44
|
+
* - Support for starting with elapsed time (resume mid-cycle)
|
|
45
|
+
* - Smooth spring-based progress animations for all state transitions
|
|
46
|
+
* - Fully localized with i18next
|
|
47
|
+
* - Material-UI theming integration
|
|
48
|
+
* - Small variant with animated progress icon (gauge border only) next to text
|
|
49
|
+
*
|
|
50
|
+
* @param onCycleComplete - Callback that receives timer control functions
|
|
51
|
+
* @param onCycleEnd - Optional callback fired when a cycle actually completes (reaches zero)
|
|
52
|
+
* @param autoStart - Whether to start timer automatically (default: true)
|
|
53
|
+
* @param variant - Visual variant: "default" (large gauge) or "small" (animated icon with text)
|
|
54
|
+
* @param compact - For small variant: whether to hide remaining time details
|
|
55
|
+
* @param className - Additional CSS classes
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <CycleTimer
|
|
60
|
+
* onCycleComplete={(controls) => {
|
|
61
|
+
* // Start a 5-minute cycle
|
|
62
|
+
* controls.startNewCycle(300)
|
|
63
|
+
*
|
|
64
|
+
* // Or start a 5-minute cycle with 2 minutes already elapsed
|
|
65
|
+
* controls.startNewCycle(300, 120)
|
|
66
|
+
*
|
|
67
|
+
* // Pause the timer
|
|
68
|
+
* controls.pause()
|
|
69
|
+
*
|
|
70
|
+
* // Resume the timer
|
|
71
|
+
* controls.resume()
|
|
72
|
+
*
|
|
73
|
+
* // Check if paused
|
|
74
|
+
* const paused = controls.isPaused()
|
|
75
|
+
* }}
|
|
76
|
+
* onCycleEnd={() => console.log('Cycle completed!')}
|
|
77
|
+
* />
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Control Functions:
|
|
81
|
+
* - `startNewCycle(maxTimeSeconds, elapsedSeconds?)` - Start a new timer cycle
|
|
82
|
+
* - `pause()` - Pause the countdown while preserving remaining time
|
|
83
|
+
* - `resume()` - Resume countdown from where it was paused
|
|
84
|
+
* - `isPaused()` - Check current pause state
|
|
85
|
+
*/
|
|
86
|
+
export const CycleTimer = externalizeComponent(
|
|
87
|
+
observer(
|
|
88
|
+
({
|
|
89
|
+
onCycleComplete,
|
|
90
|
+
onCycleEnd,
|
|
91
|
+
autoStart = true,
|
|
92
|
+
variant = "default",
|
|
93
|
+
compact = false,
|
|
94
|
+
className,
|
|
95
|
+
}: CycleTimerProps) => {
|
|
96
|
+
const theme = useTheme()
|
|
97
|
+
const { t } = useTranslation()
|
|
98
|
+
const [remainingTime, setRemainingTime] = useState(0)
|
|
99
|
+
const [maxTime, setMaxTime] = useState(0)
|
|
100
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
101
|
+
const [isPausedState, setIsPausedState] = useState(false)
|
|
102
|
+
const [currentProgress, setCurrentProgress] = useState(0)
|
|
103
|
+
const animationRef = useRef<number | null>(null)
|
|
104
|
+
const startTimeRef = useRef<number | null>(null)
|
|
105
|
+
const pausedTimeRef = useRef<number>(0)
|
|
106
|
+
|
|
107
|
+
// Spring-based interpolator for smooth gauge progress animations
|
|
108
|
+
// Uses physics simulation to create natural, smooth transitions between progress values
|
|
109
|
+
const [progressInterpolator] = useInterpolation([0], {
|
|
110
|
+
tension: 80, // Higher values = faster, more responsive animations
|
|
111
|
+
friction: 18, // Higher values = more damping, less bouncy animations
|
|
112
|
+
onChange: ([progress]) => {
|
|
113
|
+
setCurrentProgress(progress)
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const startNewCycle = useCallback(
|
|
118
|
+
(maxTimeSeconds: number, elapsedSeconds: number = 0) => {
|
|
119
|
+
setMaxTime(maxTimeSeconds)
|
|
120
|
+
const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
|
|
121
|
+
setRemainingTime(remainingSeconds)
|
|
122
|
+
setIsPausedState(false)
|
|
123
|
+
pausedTimeRef.current = 0
|
|
124
|
+
|
|
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])
|
|
132
|
+
} else {
|
|
133
|
+
progressInterpolator.setTarget([initialProgress])
|
|
134
|
+
}
|
|
135
|
+
|
|
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)
|
|
142
|
+
}
|
|
143
|
+
} else if (autoStart) {
|
|
144
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
145
|
+
setIsRunning(true)
|
|
146
|
+
} else {
|
|
147
|
+
startTimeRef.current = null
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[autoStart, onCycleEnd, progressInterpolator],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const pause = useCallback(() => {
|
|
154
|
+
if (startTimeRef.current && isRunning) {
|
|
155
|
+
const now = Date.now()
|
|
156
|
+
const additionalElapsed = now - startTimeRef.current
|
|
157
|
+
pausedTimeRef.current += additionalElapsed
|
|
158
|
+
|
|
159
|
+
// Calculate exact progress position and smoothly animate to it when pausing
|
|
160
|
+
// This ensures the visual progress matches the actual elapsed time
|
|
161
|
+
const totalElapsed = pausedTimeRef.current / 1000
|
|
162
|
+
const exactProgress = Math.min(100, (totalElapsed / maxTime) * 100)
|
|
163
|
+
progressInterpolator.setTarget([exactProgress])
|
|
164
|
+
}
|
|
165
|
+
setIsRunning(false)
|
|
166
|
+
setIsPausedState(true)
|
|
167
|
+
}, [isRunning, maxTime, progressInterpolator])
|
|
168
|
+
|
|
169
|
+
const resume = useCallback(() => {
|
|
170
|
+
if (isPausedState && remainingTime > 0) {
|
|
171
|
+
startTimeRef.current = Date.now()
|
|
172
|
+
setIsRunning(true)
|
|
173
|
+
setIsPausedState(false)
|
|
174
|
+
}
|
|
175
|
+
}, [isPausedState, remainingTime])
|
|
176
|
+
|
|
177
|
+
const isPaused = useCallback(() => {
|
|
178
|
+
return isPausedState
|
|
179
|
+
}, [isPausedState])
|
|
180
|
+
|
|
181
|
+
// Call onCycleComplete immediately to provide the timer control functions
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
let isMounted = true
|
|
184
|
+
const timeoutId = setTimeout(() => {
|
|
185
|
+
if (isMounted) {
|
|
186
|
+
onCycleComplete({
|
|
187
|
+
startNewCycle,
|
|
188
|
+
pause,
|
|
189
|
+
resume,
|
|
190
|
+
isPaused,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}, 0)
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
isMounted = false
|
|
197
|
+
clearTimeout(timeoutId)
|
|
198
|
+
}
|
|
199
|
+
}, [onCycleComplete, startNewCycle, pause, resume, isPaused])
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (isRunning) {
|
|
203
|
+
// Single animation frame loop that handles both time updates and progress
|
|
204
|
+
const updateTimer = () => {
|
|
205
|
+
if (startTimeRef.current && maxTime > 0) {
|
|
206
|
+
const now = Date.now()
|
|
207
|
+
const elapsed =
|
|
208
|
+
(now - startTimeRef.current + pausedTimeRef.current) / 1000
|
|
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)
|
|
227
|
+
}
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Continue animation loop while running
|
|
232
|
+
if (isRunning) {
|
|
233
|
+
animationRef.current = requestAnimationFrame(updateTimer)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
animationRef.current = requestAnimationFrame(updateTimer)
|
|
239
|
+
} else {
|
|
240
|
+
if (animationRef.current) {
|
|
241
|
+
cancelAnimationFrame(animationRef.current)
|
|
242
|
+
animationRef.current = null
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return () => {
|
|
247
|
+
if (animationRef.current) {
|
|
248
|
+
cancelAnimationFrame(animationRef.current)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}, [isRunning, onCycleEnd, maxTime, progressInterpolator])
|
|
252
|
+
|
|
253
|
+
// Dedicated animation loop for spring physics interpolation
|
|
254
|
+
// Runs at 60fps to ensure smooth progress animations independent of timer updates
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
let interpolationAnimationId: number | null = null
|
|
257
|
+
|
|
258
|
+
const animateInterpolation = () => {
|
|
259
|
+
progressInterpolator.update(1 / 60) // 60fps interpolation
|
|
260
|
+
interpolationAnimationId = requestAnimationFrame(animateInterpolation)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interpolationAnimationId = requestAnimationFrame(animateInterpolation)
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
if (interpolationAnimationId) {
|
|
267
|
+
cancelAnimationFrame(interpolationAnimationId)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}, [progressInterpolator])
|
|
271
|
+
|
|
272
|
+
// Keep interpolator synchronized with static progress when timer is stopped
|
|
273
|
+
// Ensures correct visual state when component initializes or timer stops
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!isRunning && !isPausedState && maxTime > 0) {
|
|
276
|
+
const staticProgress = ((maxTime - remainingTime) / maxTime) * 100
|
|
277
|
+
progressInterpolator.setTarget([staticProgress])
|
|
278
|
+
}
|
|
279
|
+
}, [
|
|
280
|
+
isRunning,
|
|
281
|
+
isPausedState,
|
|
282
|
+
maxTime,
|
|
283
|
+
remainingTime,
|
|
284
|
+
progressInterpolator,
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
const formatTime = (seconds: number): string => {
|
|
288
|
+
const minutes = Math.floor(seconds / 60)
|
|
289
|
+
const remainingSeconds = seconds % 60
|
|
290
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Use interpolated progress value for smooth gauge animations
|
|
294
|
+
const progressValue = currentProgress
|
|
295
|
+
|
|
296
|
+
// Small variant: horizontal layout with gauge icon and text
|
|
297
|
+
if (variant === "small") {
|
|
298
|
+
return (
|
|
299
|
+
<Box
|
|
300
|
+
className={className}
|
|
301
|
+
sx={{
|
|
302
|
+
display: "flex",
|
|
303
|
+
alignItems: "center",
|
|
304
|
+
gap: 0.125, // Minimal gap - 1px
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
{/* Animated progress gauge icon */}
|
|
308
|
+
<Box
|
|
309
|
+
sx={{
|
|
310
|
+
position: "relative",
|
|
311
|
+
width: 40,
|
|
312
|
+
height: 40,
|
|
313
|
+
display: "flex",
|
|
314
|
+
alignItems: "center",
|
|
315
|
+
justifyContent: "center",
|
|
316
|
+
borderRadius: "50%",
|
|
317
|
+
overflow: "visible",
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
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
|
+
/>
|
|
375
|
+
</Box>
|
|
376
|
+
|
|
377
|
+
{/* Timer text display */}
|
|
378
|
+
<Typography
|
|
379
|
+
variant="body2"
|
|
380
|
+
sx={{
|
|
381
|
+
color: theme.palette.text.primary,
|
|
382
|
+
fontSize: "14px",
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
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) })}`}
|
|
390
|
+
</Typography>
|
|
391
|
+
</Box>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Default variant: large circular gauge with centered content
|
|
396
|
+
return (
|
|
397
|
+
<Box
|
|
398
|
+
className={className}
|
|
399
|
+
sx={{
|
|
400
|
+
position: "relative",
|
|
401
|
+
width: 264,
|
|
402
|
+
height: 264,
|
|
403
|
+
display: "flex",
|
|
404
|
+
alignItems: "center",
|
|
405
|
+
justifyContent: "center",
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
<Gauge
|
|
409
|
+
width={264}
|
|
410
|
+
height={264}
|
|
411
|
+
value={progressValue}
|
|
412
|
+
valueMin={0}
|
|
413
|
+
valueMax={100}
|
|
414
|
+
innerRadius="71%"
|
|
415
|
+
outerRadius="90%"
|
|
416
|
+
skipAnimation={true}
|
|
417
|
+
sx={{
|
|
418
|
+
opacity: isPausedState ? 0.6 : 1,
|
|
419
|
+
transition: "opacity 0.2s ease",
|
|
420
|
+
[`& .MuiGauge-valueArc`]: {
|
|
421
|
+
fill: theme.palette.success.main,
|
|
422
|
+
},
|
|
423
|
+
[`& .MuiGauge-referenceArc`]: {
|
|
424
|
+
fill: "white",
|
|
425
|
+
stroke: "transparent",
|
|
426
|
+
},
|
|
427
|
+
}}
|
|
428
|
+
/>
|
|
429
|
+
|
|
430
|
+
{/* Center content overlay with timer information */}
|
|
431
|
+
<Box
|
|
432
|
+
sx={{
|
|
433
|
+
position: "absolute",
|
|
434
|
+
top: "50%",
|
|
435
|
+
left: "50%",
|
|
436
|
+
transform: "translate(-50%, -50%)",
|
|
437
|
+
width: 187, // 71% of 264 = ~187px inner radius
|
|
438
|
+
height: 187,
|
|
439
|
+
borderRadius: "50%",
|
|
440
|
+
backgroundColor: theme.palette.backgroundPaperElevation?.[8],
|
|
441
|
+
display: "flex",
|
|
442
|
+
flexDirection: "column",
|
|
443
|
+
alignItems: "center",
|
|
444
|
+
justifyContent: "center",
|
|
445
|
+
textAlign: "center",
|
|
446
|
+
gap: 1,
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
{/* "remaining time" label */}
|
|
450
|
+
<Typography
|
|
451
|
+
variant="body2"
|
|
452
|
+
sx={{
|
|
453
|
+
fontSize: "12px",
|
|
454
|
+
color: theme.palette.text.secondary,
|
|
455
|
+
marginBottom: 0.5,
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
{t("CycleTimer.RemainingTime.lb")}
|
|
459
|
+
</Typography>
|
|
460
|
+
|
|
461
|
+
{/* Main timer display */}
|
|
462
|
+
<Typography
|
|
463
|
+
variant="h1"
|
|
464
|
+
sx={{
|
|
465
|
+
fontSize: "48px",
|
|
466
|
+
fontWeight: 500,
|
|
467
|
+
color: theme.palette.text.primary,
|
|
468
|
+
lineHeight: 1,
|
|
469
|
+
marginBottom: 0.5,
|
|
470
|
+
}}
|
|
471
|
+
>
|
|
472
|
+
{formatTime(remainingTime)}
|
|
473
|
+
</Typography>
|
|
474
|
+
|
|
475
|
+
{/* Total time display */}
|
|
476
|
+
<Typography
|
|
477
|
+
variant="body2"
|
|
478
|
+
sx={{
|
|
479
|
+
fontSize: "12px",
|
|
480
|
+
color: theme.palette.text.secondary,
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
{t("CycleTimer.OfTime.lb", { time: formatTime(maxTime) })}
|
|
484
|
+
</Typography>
|
|
485
|
+
</Box>
|
|
486
|
+
</Box>
|
|
487
|
+
)
|
|
488
|
+
},
|
|
489
|
+
),
|
|
490
|
+
)
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
"Jogging.Cartesian.bt": "Kartesisch",
|
|
45
45
|
"Jogging.Joints.bt": "Gelenke",
|
|
46
46
|
"Jogging.Velocity.bt": "Geschwindigkeit",
|
|
47
|
+
"CycleTimer.RemainingTime.lb": "Verbleibende Zeit",
|
|
48
|
+
"CycleTimer.OfTime.lb": "von {{time}} min.",
|
|
49
|
+
"CycleTimer.Time.lb": "{{time}} min.",
|
|
47
50
|
"ProgramControl.Start.bt": "Start",
|
|
48
51
|
"ProgramControl.Resume.bt": "Weiter",
|
|
49
52
|
"ProgramControl.Pause.bt": "Pause",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"Jogging.Cartesian.bt": "Cartesian",
|
|
46
46
|
"Jogging.Joints.bt": "Joints",
|
|
47
47
|
"Jogging.Velocity.bt": "Velocity",
|
|
48
|
+
"CycleTimer.RemainingTime.lb": "Time remaining",
|
|
49
|
+
"CycleTimer.OfTime.lb": "of {{time}} min.",
|
|
50
|
+
"CycleTimer.Time.lb": "{{time}} min.",
|
|
48
51
|
"ProgramControl.Start.bt": "Start",
|
|
49
52
|
"ProgramControl.Resume.bt": "Resume",
|
|
50
53
|
"ProgramControl.Pause.bt": "Pause",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./components/3d-viewport/PresetEnvironment"
|
|
2
2
|
export * from "./components/3d-viewport/SafetyZonesRenderer"
|
|
3
3
|
export * from "./components/3d-viewport/TrajectoryRenderer"
|
|
4
|
+
export * from "./components/CycleTimer"
|
|
4
5
|
export * from "./components/jogging/JoggingCartesianAxisControl"
|
|
5
6
|
export * from "./components/jogging/JoggingJointRotationControl"
|
|
6
7
|
export * from "./components/jogging/JoggingPanel"
|