@wandelbots/wandelbots-js-react-components 2.27.1-pr.feature-add-program-control-component.367.8730636 → 2.27.1-pr.feature-add-cycle-timer.368.c18c8bd

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.27.1-pr.feature-add-program-control-component.367.8730636",
3
+ "version": "2.27.1-pr.feature-add-cycle-timer.368.c18c8bd",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -140,6 +140,7 @@
140
140
  },
141
141
  "dependencies": {
142
142
  "@monaco-editor/react": "^4.7.0",
143
+ "@mui/x-charts": "^8.9.0",
143
144
  "@shikijs/monaco": "^3.1.0",
144
145
  "i18next-browser-languagedetector": "^8.0.4",
145
146
  "lodash-es": "^4.17.21",
@@ -0,0 +1,292 @@
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
+
8
+ export interface CycleTimerProps {
9
+ /** Callback that receives the startNewCycle function for controlling the timer */
10
+ onCycleComplete: (startNewCycle: (maxTimeSeconds: number) => void) => void
11
+ /** Callback fired when a cycle actually completes (reaches zero) */
12
+ onCycleEnd?: () => void
13
+ /** Whether the timer should start automatically when maxTime is set */
14
+ autoStart?: boolean
15
+ /** Visual variant of the timer */
16
+ variant?: "default" | "small"
17
+ /** For small variant: whether to show remaining time details (compact hides them) */
18
+ compact?: boolean
19
+ /** Additional CSS classes */
20
+ className?: string
21
+ }
22
+
23
+ /**
24
+ * A circular gauge timer component that shows the remaining time of a cycle
25
+ *
26
+ * Features:
27
+ * - Circular gauge with 264px diameter and 40px thickness
28
+ * - Shows remaining time prominently in the center (60px font)
29
+ * - Displays "remaining time" label at top and total time at bottom
30
+ * - Automatically counts down and triggers callback when reaching zero
31
+ * - Fully localized with i18next
32
+ * - Material-UI theming integration
33
+ * - Small variant with animated progress icon (gauge border only) next to text
34
+ *
35
+ * @param onCycleComplete - Callback that receives the startNewCycle function for controlling the timer
36
+ * @param onCycleEnd - Optional callback fired when a cycle actually completes (reaches zero)
37
+ * @param autoStart - Whether to start timer automatically (default: true)
38
+ * @param variant - Visual variant: "default" (large gauge) or "small" (animated icon with text)
39
+ * @param compact - For small variant: whether to hide remaining time details
40
+ * @param className - Additional CSS classes
41
+ */
42
+ export const CycleTimer = externalizeComponent(
43
+ observer(
44
+ ({
45
+ onCycleComplete,
46
+ onCycleEnd,
47
+ autoStart = true,
48
+ variant = "default",
49
+ compact = false,
50
+ className,
51
+ }: CycleTimerProps) => {
52
+ const theme = useTheme()
53
+ const { t } = useTranslation()
54
+ const [remainingTime, setRemainingTime] = useState(0)
55
+ const [maxTime, setMaxTime] = useState(0)
56
+ const [isRunning, setIsRunning] = useState(false)
57
+ const intervalRef = useRef<NodeJS.Timeout | null>(null)
58
+
59
+ const startNewCycle = useCallback(
60
+ (maxTimeSeconds: number) => {
61
+ console.log(`Starting new cycle with ${maxTimeSeconds} seconds`)
62
+ setMaxTime(maxTimeSeconds)
63
+ setRemainingTime(maxTimeSeconds)
64
+ if (autoStart) {
65
+ console.log("Auto-start enabled, starting timer")
66
+ setIsRunning(true)
67
+ } else {
68
+ console.log("Auto-start disabled, timer set but not started")
69
+ }
70
+ },
71
+ [autoStart],
72
+ )
73
+
74
+ // Call onCycleComplete immediately to provide the startNewCycle function
75
+ useEffect(() => {
76
+ let isMounted = true
77
+ const timeoutId = setTimeout(() => {
78
+ if (isMounted) {
79
+ onCycleComplete(startNewCycle)
80
+ }
81
+ }, 0)
82
+
83
+ return () => {
84
+ isMounted = false
85
+ clearTimeout(timeoutId)
86
+ }
87
+ }, [onCycleComplete, startNewCycle])
88
+
89
+ useEffect(() => {
90
+ if (isRunning && remainingTime > 0) {
91
+ intervalRef.current = setInterval(() => {
92
+ setRemainingTime((prev) => {
93
+ if (prev <= 1) {
94
+ setIsRunning(false)
95
+ // Call onCycleEnd when timer reaches zero to notify about completion
96
+ if (onCycleEnd) {
97
+ setTimeout(() => onCycleEnd(), 0)
98
+ }
99
+ console.log("Cycle completed! Timer reached zero.")
100
+ return 0
101
+ }
102
+ return prev - 1
103
+ })
104
+ }, 1000)
105
+ } else {
106
+ if (intervalRef.current) {
107
+ clearInterval(intervalRef.current)
108
+ intervalRef.current = null
109
+ }
110
+ }
111
+
112
+ return () => {
113
+ if (intervalRef.current) {
114
+ clearInterval(intervalRef.current)
115
+ }
116
+ }
117
+ }, [isRunning, remainingTime, onCycleEnd])
118
+
119
+ const formatTime = (seconds: number): string => {
120
+ const minutes = Math.floor(seconds / 60)
121
+ const remainingSeconds = seconds % 60
122
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
123
+ }
124
+
125
+ const progressValue =
126
+ maxTime > 0 ? ((maxTime - remainingTime) / maxTime) * 100 : 0
127
+
128
+ // Small variant: horizontal layout with gauge icon and text
129
+ if (variant === "small") {
130
+ return (
131
+ <Box
132
+ className={className}
133
+ sx={{
134
+ display: "flex",
135
+ alignItems: "center",
136
+ gap: 0.125, // Minimal gap - 1px
137
+ }}
138
+ >
139
+ {/* Animated progress gauge icon */}
140
+ <Box
141
+ sx={{
142
+ position: "relative",
143
+ width: 40,
144
+ height: 40,
145
+ display: "flex",
146
+ alignItems: "center",
147
+ justifyContent: "center",
148
+ borderRadius: "50%",
149
+ overflow: "visible",
150
+ }}
151
+ >
152
+ <Gauge
153
+ width={40}
154
+ height={40}
155
+ value={progressValue}
156
+ valueMin={0}
157
+ valueMax={100}
158
+ innerRadius="70%"
159
+ outerRadius="95%"
160
+ sx={{
161
+ [`& .MuiGauge-valueArc`]: {
162
+ fill: theme.palette.success.main,
163
+ },
164
+ [`& .MuiGauge-referenceArc`]: {
165
+ fill: theme.palette.success.main,
166
+ opacity: 0.3,
167
+ },
168
+ [`& .MuiGauge-valueText`]: {
169
+ display: "none",
170
+ },
171
+ [`& .MuiGauge-text`]: {
172
+ display: "none",
173
+ },
174
+ [`& text`]: {
175
+ display: "none",
176
+ },
177
+ }}
178
+ />
179
+ </Box>
180
+
181
+ {/* Timer text display */}
182
+ <Typography
183
+ variant="body2"
184
+ sx={{
185
+ color: theme.palette.text.primary,
186
+ fontSize: "14px",
187
+ }}
188
+ >
189
+ {compact
190
+ ? // Compact mode: show only remaining time
191
+ formatTime(remainingTime)
192
+ : // Full mode: show "remaining / total min." format
193
+ `${formatTime(remainingTime)} / ${formatTime(maxTime)} ${t("CycleTimer.Min.lb")}`}
194
+ </Typography>
195
+ </Box>
196
+ )
197
+ }
198
+
199
+ // Default variant: large circular gauge with centered content
200
+ return (
201
+ <Box
202
+ className={className}
203
+ sx={{
204
+ position: "relative",
205
+ width: 264,
206
+ height: 264,
207
+ display: "flex",
208
+ alignItems: "center",
209
+ justifyContent: "center",
210
+ }}
211
+ >
212
+ <Gauge
213
+ width={264}
214
+ height={264}
215
+ value={progressValue}
216
+ valueMin={0}
217
+ valueMax={100}
218
+ innerRadius="71%"
219
+ outerRadius="90%"
220
+ sx={{
221
+ [`& .MuiGauge-valueArc`]: {
222
+ fill: theme.palette.success.main,
223
+ },
224
+ [`& .MuiGauge-referenceArc`]: {
225
+ fill: "white",
226
+ stroke: "transparent",
227
+ },
228
+ }}
229
+ />
230
+
231
+ {/* Center content overlay with timer information */}
232
+ <Box
233
+ sx={{
234
+ position: "absolute",
235
+ top: "50%",
236
+ left: "50%",
237
+ transform: "translate(-50%, -50%)",
238
+ width: 187, // 71% of 264 = ~187px inner radius
239
+ height: 187,
240
+ borderRadius: "50%",
241
+ backgroundColor: theme.palette.backgroundPaperElevation?.[8],
242
+ display: "flex",
243
+ flexDirection: "column",
244
+ alignItems: "center",
245
+ justifyContent: "center",
246
+ textAlign: "center",
247
+ gap: 1,
248
+ }}
249
+ >
250
+ {/* "remaining time" label */}
251
+ <Typography
252
+ variant="body2"
253
+ sx={{
254
+ fontSize: "12px",
255
+ color: theme.palette.text.secondary,
256
+ marginBottom: 0.5,
257
+ }}
258
+ >
259
+ {t("CycleTimer.RemainingTime.lb")}
260
+ </Typography>
261
+
262
+ {/* Main timer display */}
263
+ <Typography
264
+ variant="h1"
265
+ sx={{
266
+ fontSize: "48px",
267
+ fontWeight: 500,
268
+ color: theme.palette.text.primary,
269
+ lineHeight: 1,
270
+ marginBottom: 0.5,
271
+ }}
272
+ >
273
+ {formatTime(remainingTime)}
274
+ </Typography>
275
+
276
+ {/* Total time display */}
277
+ <Typography
278
+ variant="body2"
279
+ sx={{
280
+ fontSize: "12px",
281
+ color: theme.palette.text.secondary,
282
+ }}
283
+ >
284
+ {t("CycleTimer.Of.lb")} {formatTime(maxTime)}{" "}
285
+ {t("CycleTimer.Min.lb")}
286
+ </Typography>
287
+ </Box>
288
+ </Box>
289
+ )
290
+ },
291
+ ),
292
+ )
@@ -44,8 +44,7 @@
44
44
  "Jogging.Cartesian.bt": "Kartesisch",
45
45
  "Jogging.Joints.bt": "Gelenke",
46
46
  "Jogging.Velocity.bt": "Geschwindigkeit",
47
- "ProgramControl.Run.bt": "Starten",
48
- "ProgramControl.Resume.bt": "Fortsetzen",
49
- "ProgramControl.Pause.bt": "Pausieren",
50
- "ProgramControl.Stop.bt": "Stoppen"
47
+ "CycleTimer.RemainingTime.lb": "Verbleibende Zeit",
48
+ "CycleTimer.Of.lb": "von",
49
+ "CycleTimer.Min.lb": "min."
51
50
  }
@@ -45,8 +45,7 @@
45
45
  "Jogging.Cartesian.bt": "Cartesian",
46
46
  "Jogging.Joints.bt": "Joints",
47
47
  "Jogging.Velocity.bt": "Velocity",
48
- "ProgramControl.Run.bt": "Run",
49
- "ProgramControl.Resume.bt": "Resume",
50
- "ProgramControl.Pause.bt": "Pause",
51
- "ProgramControl.Stop.bt": "Stop"
48
+ "CycleTimer.RemainingTime.lb": "remaining time",
49
+ "CycleTimer.Of.lb": "of",
50
+ "CycleTimer.Min.lb": "min."
52
51
  }
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"
@@ -9,7 +10,6 @@ export * from "./components/jogging/PoseCartesianValues"
9
10
  export * from "./components/jogging/PoseJointValues"
10
11
  export * from "./components/LoadingCover"
11
12
  export * from "./components/modal/NoMotionGroupModal"
12
- export * from "./components/ProgramControl"
13
13
  export * from "./components/robots/AxisConfig"
14
14
  export * from "./components/robots/Robot"
15
15
  export { defaultGetModel } from "./components/robots/robotModelLogic"
@@ -1,43 +0,0 @@
1
- export type ProgramState = "idle" | "running" | "paused" | "stopping";
2
- export interface ProgramControlProps {
3
- /** The current state of the program control */
4
- state: ProgramState;
5
- /** Callback fired when the run/resume button is clicked */
6
- onRun: () => void;
7
- /** Callback fired when the pause button is clicked (only available in 'with_pause' variant) */
8
- onPause?: () => void;
9
- /** Callback fired when the stop button is clicked */
10
- onStop: () => void;
11
- /**
12
- * Function to reset the component from 'stopping' state back to 'idle'.
13
- * This must be called manually by the user when requiresManualReset is true.
14
- */
15
- onReset?: () => void;
16
- /**
17
- * When true, the component will stay in 'stopping' state until onReset is called manually.
18
- * When false (default), auto-resets to 'idle' after 2 seconds.
19
- */
20
- requiresManualReset?: boolean;
21
- /**
22
- * Variant of the component:
23
- * - 'with_pause': Shows run/pause/stop buttons (default)
24
- * - 'without_pause': Shows only run/stop buttons
25
- */
26
- variant?: "with_pause" | "without_pause";
27
- /** Additional CSS class name */
28
- className?: string;
29
- }
30
- /**
31
- * A control component for program execution with run, pause, and stop functionality.
32
- *
33
- * Features:
34
- * - State machine with idle, running, paused, and stopping states
35
- * - Two variants: with_pause (3 buttons) and without_pause (2 buttons)
36
- * - Optional manual reset functionality
37
- * - Responsive design with 110px circular buttons
38
- * - Material-UI theming integration
39
- */
40
- export declare const ProgramControl: (({ state, onRun, onPause, onStop, onReset, requiresManualReset, variant, className, }: ProgramControlProps) => import("react/jsx-runtime").JSX.Element) & {
41
- displayName: string;
42
- };
43
- //# sourceMappingURL=ProgramControl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ProgramControl.d.ts","sourceRoot":"","sources":["../../src/components/ProgramControl.tsx"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAA;AAErE,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,KAAK,EAAE,YAAY,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,qDAAqD;IACrD,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,YAAY,GAAG,eAAe,CAAA;IACxC,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AASD;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0FAWpB,mBAAmB;;CAuJzB,CAAA"}
@@ -1,217 +0,0 @@
1
- import { Pause, PlayArrow, Stop } from "@mui/icons-material"
2
- import { Box, Button, Typography, useTheme } from "@mui/material"
3
- import { observer } from "mobx-react-lite"
4
- import { useTranslation } from "react-i18next"
5
- import { externalizeComponent } from "../externalizeComponent"
6
-
7
- export type ProgramState = "idle" | "running" | "paused" | "stopping"
8
-
9
- export interface ProgramControlProps {
10
- /** The current state of the program control */
11
- state: ProgramState
12
- /** Callback fired when the run/resume button is clicked */
13
- onRun: () => void
14
- /** Callback fired when the pause button is clicked (only available in 'with_pause' variant) */
15
- onPause?: () => void
16
- /** Callback fired when the stop button is clicked */
17
- onStop: () => void
18
- /**
19
- * Function to reset the component from 'stopping' state back to 'idle'.
20
- * This must be called manually by the user when requiresManualReset is true.
21
- */
22
- onReset?: () => void
23
- /**
24
- * When true, the component will stay in 'stopping' state until onReset is called manually.
25
- * When false (default), auto-resets to 'idle' after 2 seconds.
26
- */
27
- requiresManualReset?: boolean
28
- /**
29
- * Variant of the component:
30
- * - 'with_pause': Shows run/pause/stop buttons (default)
31
- * - 'without_pause': Shows only run/stop buttons
32
- */
33
- variant?: "with_pause" | "without_pause"
34
- /** Additional CSS class name */
35
- className?: string
36
- }
37
-
38
- interface ButtonConfig {
39
- enabled: boolean
40
- label: string
41
- color: string
42
- onClick: () => void
43
- }
44
-
45
- /**
46
- * A control component for program execution with run, pause, and stop functionality.
47
- *
48
- * Features:
49
- * - State machine with idle, running, paused, and stopping states
50
- * - Two variants: with_pause (3 buttons) and without_pause (2 buttons)
51
- * - Optional manual reset functionality
52
- * - Responsive design with 110px circular buttons
53
- * - Material-UI theming integration
54
- */
55
- export const ProgramControl = externalizeComponent(
56
- observer(
57
- ({
58
- state,
59
- onRun,
60
- onPause,
61
- onStop,
62
- onReset,
63
- requiresManualReset = false,
64
- variant = "with_pause",
65
- className,
66
- }: ProgramControlProps) => {
67
- const theme = useTheme()
68
- const { t } = useTranslation()
69
-
70
- const getButtonConfigs = (): ButtonConfig[] => {
71
- const baseConfigs: Record<string, ButtonConfig> = {
72
- run: {
73
- enabled: state === "idle" || state === "paused",
74
- label:
75
- state === "paused"
76
- ? t("ProgramControl.Resume.bt")
77
- : t("ProgramControl.Run.bt"),
78
- color: theme.palette.success.main,
79
- onClick: onRun,
80
- },
81
- pause: {
82
- enabled: state === "running",
83
- label: t("ProgramControl.Pause.bt"),
84
- color: "#FFFFFF33",
85
- onClick: onPause || (() => {}),
86
- },
87
- stop: {
88
- enabled: state === "running" || state === "paused",
89
- label: t("ProgramControl.Stop.bt"),
90
- color: theme.palette.error.main,
91
- onClick: onStop,
92
- },
93
- }
94
-
95
- if (variant === "without_pause") {
96
- return [baseConfigs.run, baseConfigs.stop]
97
- }
98
-
99
- return [baseConfigs.run, baseConfigs.pause, baseConfigs.stop]
100
- }
101
-
102
- const getButtonIcon = (index: number) => {
103
- const iconProps = { sx: { fontSize: "55px" } }
104
-
105
- if (variant === "without_pause") {
106
- return index === 0 ? (
107
- <PlayArrow {...iconProps} />
108
- ) : (
109
- <Stop {...iconProps} />
110
- )
111
- }
112
-
113
- switch (index) {
114
- case 0:
115
- return <PlayArrow {...iconProps} />
116
- case 1:
117
- return <Pause {...iconProps} />
118
- case 2:
119
- return <Stop {...iconProps} />
120
- default:
121
- return null
122
- }
123
- }
124
-
125
- const buttonConfigs = getButtonConfigs()
126
-
127
- return (
128
- <Box
129
- className={className}
130
- sx={{
131
- display: "flex",
132
- flexDirection: "column",
133
- alignItems: "center",
134
- gap: 2,
135
- }}
136
- >
137
- <Box
138
- sx={{
139
- display: "flex",
140
- gap: "40px",
141
- flexWrap: "wrap",
142
- justifyContent: "center",
143
- alignItems: "center",
144
- }}
145
- >
146
- {buttonConfigs.map((config, index) => (
147
- <Box
148
- key={config.label}
149
- sx={{
150
- display: "flex",
151
- flexDirection: "column",
152
- alignItems: "center",
153
- gap: 1,
154
- }}
155
- >
156
- <Button
157
- variant="contained"
158
- disabled={
159
- !config.enabled ||
160
- (state === "stopping" && !requiresManualReset)
161
- }
162
- onClick={config.onClick}
163
- sx={{
164
- width: "110px",
165
- height: "110px",
166
- borderRadius: "110px",
167
- backgroundColor: config.color,
168
- opacity:
169
- config.enabled &&
170
- !(state === "stopping" && !requiresManualReset)
171
- ? 1
172
- : 0.3,
173
- "&:hover": {
174
- backgroundColor: config.color,
175
- opacity:
176
- config.enabled &&
177
- !(state === "stopping" && !requiresManualReset)
178
- ? 0.8
179
- : 0.3,
180
- },
181
- "&:disabled": {
182
- backgroundColor: config.color,
183
- opacity: 0.3,
184
- },
185
- minWidth: "110px",
186
- flexShrink: 0,
187
- }}
188
- >
189
- {getButtonIcon(index)}
190
- </Button>
191
-
192
- <Typography
193
- variant="body1"
194
- sx={{
195
- color:
196
- config.enabled &&
197
- !(state === "stopping" && !requiresManualReset)
198
- ? config.color
199
- : theme.palette.text.disabled,
200
- textAlign: "center",
201
- opacity:
202
- config.enabled &&
203
- !(state === "stopping" && !requiresManualReset)
204
- ? 1
205
- : 0.3,
206
- }}
207
- >
208
- {config.label}
209
- </Typography>
210
- </Box>
211
- ))}
212
- </Box>
213
- </Box>
214
- )
215
- },
216
- ),
217
- )