@wandelbots/wandelbots-js-react-components 2.32.0 → 2.33.0-pr.feature-robot-precondition-list.372.3ad6c62

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 (45) hide show
  1. package/dist/components/CycleTimer.d.ts.map +1 -1
  2. package/dist/components/DataGrid.d.ts +61 -0
  3. package/dist/components/DataGrid.d.ts.map +1 -0
  4. package/dist/components/LogPanel.d.ts +38 -0
  5. package/dist/components/LogPanel.d.ts.map +1 -0
  6. package/dist/components/LogStore.d.ts +11 -0
  7. package/dist/components/LogStore.d.ts.map +1 -0
  8. package/dist/components/LogViewer.d.ts +26 -0
  9. package/dist/components/LogViewer.d.ts.map +1 -0
  10. package/dist/components/ProgramControl.d.ts +6 -1
  11. package/dist/components/ProgramControl.d.ts.map +1 -1
  12. package/dist/components/ProgramStateIndicator.d.ts +1 -1
  13. package/dist/components/ProgramStateIndicator.d.ts.map +1 -1
  14. package/dist/components/RobotCard.d.ts +84 -0
  15. package/dist/components/RobotCard.d.ts.map +1 -0
  16. package/dist/components/RobotListItem.d.ts +34 -0
  17. package/dist/components/RobotListItem.d.ts.map +1 -0
  18. package/dist/components/RobotSetupReadinessIndicator.d.ts +31 -0
  19. package/dist/components/RobotSetupReadinessIndicator.d.ts.map +1 -0
  20. package/dist/components/RobotSetupReadinessIndicator.test.d.ts +2 -0
  21. package/dist/components/RobotSetupReadinessIndicator.test.d.ts.map +1 -0
  22. package/dist/components/robots/Robot.d.ts +3 -2
  23. package/dist/components/robots/Robot.d.ts.map +1 -1
  24. package/dist/index.cjs +49 -49
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8288 -7119
  29. package/dist/index.js.map +1 -1
  30. package/package.json +2 -1
  31. package/src/components/CycleTimer.tsx +43 -64
  32. package/src/components/DataGrid.tsx +454 -0
  33. package/src/components/LogPanel.tsx +69 -0
  34. package/src/components/LogStore.ts +40 -0
  35. package/src/components/LogViewer.tsx +311 -0
  36. package/src/components/ProgramControl.tsx +20 -10
  37. package/src/components/ProgramStateIndicator.tsx +20 -8
  38. package/src/components/RobotCard.tsx +576 -0
  39. package/src/components/RobotListItem.tsx +152 -0
  40. package/src/components/RobotSetupReadinessIndicator.test.tsx +60 -0
  41. package/src/components/RobotSetupReadinessIndicator.tsx +124 -0
  42. package/src/components/robots/Robot.tsx +5 -2
  43. package/src/i18n/locales/de/translations.json +6 -1
  44. package/src/i18n/locales/en/translations.json +6 -1
  45. package/src/index.ts +7 -0
@@ -0,0 +1,40 @@
1
+ import { action, makeObservable, observable } from "mobx"
2
+ import type { LogLevel, LogMessage } from "./LogViewer"
3
+
4
+ export class LogStore {
5
+ messages: LogMessage[] = []
6
+
7
+ constructor() {
8
+ makeObservable(this, {
9
+ messages: observable,
10
+ addMessage: action,
11
+ clearMessages: action,
12
+ })
13
+ }
14
+
15
+ addMessage = (message: string, level: LogLevel = "info") => {
16
+ const logMessage: LogMessage = {
17
+ id: Math.random().toString(36).substring(2, 11),
18
+ timestamp: new Date(),
19
+ message,
20
+ level,
21
+ }
22
+ this.messages.push(logMessage)
23
+ }
24
+
25
+ clearMessages = () => {
26
+ this.messages = []
27
+ }
28
+
29
+ addInfo = (message: string) => {
30
+ this.addMessage(message, "info")
31
+ }
32
+
33
+ addWarning = (message: string) => {
34
+ this.addMessage(message, "warning")
35
+ }
36
+
37
+ addError = (message: string) => {
38
+ this.addMessage(message, "error")
39
+ }
40
+ }
@@ -0,0 +1,311 @@
1
+ import {
2
+ ContentCopy,
3
+ DescriptionOutlined as DocumentIcon,
4
+ ExpandLess,
5
+ ExpandMore,
6
+ } from "@mui/icons-material"
7
+ import type { SxProps } from "@mui/material"
8
+ import {
9
+ Box,
10
+ Button,
11
+ IconButton,
12
+ Paper,
13
+ Typography,
14
+ useTheme,
15
+ } from "@mui/material"
16
+ import { observer } from "mobx-react-lite"
17
+ import { useEffect, useRef, useState } from "react"
18
+ import { externalizeComponent } from "../externalizeComponent"
19
+
20
+ export type LogLevel = "info" | "error" | "warning"
21
+
22
+ export type LogMessage = {
23
+ id: string
24
+ timestamp: Date
25
+ message: string
26
+ level: LogLevel
27
+ }
28
+
29
+ export type LogViewerProps = {
30
+ /** Log messages to display */
31
+ messages: LogMessage[]
32
+ /** Callback when clear button is clicked */
33
+ onClear?: () => void
34
+ /** Height of the component */
35
+ height?: string | number
36
+ /** Additional styles */
37
+ sx?: SxProps
38
+ }
39
+
40
+ /**
41
+ * A log viewer component that displays timestamped log messages with different levels.
42
+ * Features a header with document icon and clear button, and scrollable message area.
43
+ */
44
+ export const LogViewer = externalizeComponent(
45
+ observer((props: LogViewerProps) => {
46
+ const { messages = [], onClear, height = 400, sx } = props
47
+ const theme = useTheme()
48
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
49
+
50
+ // Auto-scroll to bottom when new messages are added
51
+ useEffect(() => {
52
+ if (messages.length === 0) return
53
+
54
+ const scrollContainer = scrollContainerRef.current
55
+ if (!scrollContainer) return
56
+
57
+ // Use a timeout to scroll after the DOM updates
58
+ const timeoutId = setTimeout(() => {
59
+ // Scroll the container to the bottom, not the entire browser
60
+ scrollContainer.scrollTop = scrollContainer.scrollHeight
61
+ }, 10)
62
+
63
+ return () => clearTimeout(timeoutId)
64
+ }, [messages.length])
65
+
66
+ const formatTimestamp = (timestamp: Date) => {
67
+ return timestamp.toLocaleTimeString("en-US", {
68
+ hour12: false,
69
+ hour: "2-digit",
70
+ minute: "2-digit",
71
+ second: "2-digit",
72
+ })
73
+ }
74
+
75
+ const getMessageColor = (level: LogLevel) => {
76
+ switch (level) {
77
+ case "error":
78
+ return theme.palette.error.main
79
+ case "warning":
80
+ return theme.palette.warning.main
81
+ case "info":
82
+ default:
83
+ return theme.palette.text.secondary
84
+ }
85
+ }
86
+
87
+ // Component for individual log messages with expand/copy functionality
88
+ const LogMessage = ({ message }: { message: LogMessage }) => {
89
+ const [isExpanded, setIsExpanded] = useState(false)
90
+ const [copyTooltip, setCopyTooltip] = useState(false)
91
+ const [isHovered, setIsHovered] = useState(false)
92
+ const isLongMessage = message.message.length > 150
93
+
94
+ const handleCopy = async () => {
95
+ try {
96
+ await navigator.clipboard.writeText(message.message)
97
+ setCopyTooltip(true)
98
+ setTimeout(() => setCopyTooltip(false), 2000)
99
+ } catch (err) {
100
+ console.error("Failed to copy message:", err)
101
+ }
102
+ }
103
+
104
+ const displayMessage =
105
+ isLongMessage && !isExpanded
106
+ ? message.message.substring(0, 150) + "..."
107
+ : message.message
108
+
109
+ return (
110
+ <Box
111
+ key={message.id}
112
+ onMouseEnter={() => setIsHovered(true)}
113
+ onMouseLeave={() => setIsHovered(false)}
114
+ sx={{
115
+ display: "flex",
116
+ gap: 1,
117
+ fontFamily: "monospace",
118
+ flexDirection: "column",
119
+ "&:hover": {
120
+ backgroundColor: theme.palette.action.hover,
121
+ },
122
+ borderRadius: "4px",
123
+ padding: "2px 4px",
124
+ margin: "-2px -4px",
125
+ }}
126
+ >
127
+ <Box sx={{ display: "flex", gap: 1 }}>
128
+ {/* Timestamp */}
129
+ <Typography
130
+ component="span"
131
+ sx={{
132
+ fontWeight: 400,
133
+ fontSize: "12px",
134
+ lineHeight: "18px",
135
+ letterSpacing: "0.4px",
136
+ color: theme.palette.text.disabled,
137
+ whiteSpace: "nowrap",
138
+ flexShrink: 0,
139
+ }}
140
+ >
141
+ [{formatTimestamp(message.timestamp)}]
142
+ </Typography>
143
+
144
+ {/* Message */}
145
+ <Typography
146
+ component="span"
147
+ sx={{
148
+ fontWeight: 400,
149
+ fontSize: "12px",
150
+ lineHeight: "18px",
151
+ letterSpacing: "0.4px",
152
+ color: getMessageColor(message.level),
153
+ wordBreak: "break-word",
154
+ overflowWrap: "anywhere",
155
+ hyphens: "auto",
156
+ flex: 1,
157
+ whiteSpace: "pre-wrap",
158
+ }}
159
+ >
160
+ {displayMessage}
161
+ </Typography>
162
+
163
+ {/* Action buttons - only visible on hover */}
164
+ <Box
165
+ sx={{
166
+ display: "flex",
167
+ alignItems: "flex-start",
168
+ gap: 0.5,
169
+ opacity: isHovered ? 1 : 0,
170
+ transition: "opacity 0.2s ease-in-out",
171
+ visibility: isHovered ? "visible" : "hidden",
172
+ }}
173
+ >
174
+ <IconButton
175
+ size="small"
176
+ onClick={handleCopy}
177
+ sx={{
178
+ padding: "2px",
179
+ color: theme.palette.text.secondary,
180
+ "&:hover": {
181
+ backgroundColor: theme.palette.action.hover,
182
+ },
183
+ }}
184
+ title={copyTooltip ? "Copied!" : "Copy message"}
185
+ >
186
+ <ContentCopy sx={{ fontSize: 12 }} />
187
+ </IconButton>
188
+
189
+ {isLongMessage && (
190
+ <IconButton
191
+ size="small"
192
+ onClick={() => setIsExpanded(!isExpanded)}
193
+ sx={{
194
+ padding: "2px",
195
+ color: theme.palette.text.secondary,
196
+ "&:hover": {
197
+ backgroundColor: theme.palette.action.hover,
198
+ },
199
+ }}
200
+ title={isExpanded ? "Collapse" : "Expand"}
201
+ >
202
+ {isExpanded ? (
203
+ <ExpandLess sx={{ fontSize: 12 }} />
204
+ ) : (
205
+ <ExpandMore sx={{ fontSize: 12 }} />
206
+ )}
207
+ </IconButton>
208
+ )}
209
+ </Box>
210
+ </Box>
211
+ </Box>
212
+ )
213
+ }
214
+
215
+ return (
216
+ <Paper
217
+ sx={{
218
+ backgroundColor:
219
+ theme.palette.backgroundPaperElevation?.[2] || "#171927",
220
+ backgroundImage: "none", // Override any gradient from elevation
221
+ height,
222
+ display: "flex",
223
+ flexDirection: "column",
224
+ overflow: "hidden",
225
+ ...sx,
226
+ }}
227
+ >
228
+ {/* Header */}
229
+ <Box
230
+ sx={{
231
+ display: "flex",
232
+ alignItems: "center",
233
+ justifyContent: "space-between",
234
+ padding: "12px 16px",
235
+ }}
236
+ >
237
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
238
+ <DocumentIcon
239
+ sx={{
240
+ fontSize: 16,
241
+ color: theme.palette.action.active,
242
+ opacity: 0.56,
243
+ }}
244
+ />
245
+ <Typography
246
+ sx={{
247
+ fontWeight: 500,
248
+ fontSize: "14px",
249
+ lineHeight: "143%",
250
+ letterSpacing: "0.17px",
251
+ color: theme.palette.text.primary,
252
+ }}
253
+ >
254
+ Log
255
+ </Typography>
256
+ </Box>
257
+ <Button
258
+ onClick={onClear}
259
+ variant="text"
260
+ sx={{
261
+ fontWeight: 500,
262
+ fontSize: "13px",
263
+ lineHeight: "22px",
264
+ letterSpacing: "0.46px",
265
+ color: theme.palette.primary.main,
266
+ textTransform: "none",
267
+ minWidth: "auto",
268
+ padding: "4px 8px",
269
+ "&:hover": {
270
+ backgroundColor: theme.palette.primary.main + "14", // 8% opacity
271
+ },
272
+ }}
273
+ >
274
+ Clear
275
+ </Button>
276
+ </Box>
277
+
278
+ {/* Messages Container */}
279
+ <Box
280
+ ref={scrollContainerRef}
281
+ sx={{
282
+ flex: 1,
283
+ overflow: "auto",
284
+ padding: "8px 16px",
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ gap: "2px",
288
+ }}
289
+ >
290
+ {messages.length === 0 ? (
291
+ <Typography
292
+ sx={{
293
+ color: theme.palette.text.disabled,
294
+ fontSize: "12px",
295
+ fontStyle: "italic",
296
+ textAlign: "center",
297
+ marginTop: 2,
298
+ }}
299
+ >
300
+ No log messages
301
+ </Typography>
302
+ ) : (
303
+ messages.map((message) => (
304
+ <LogMessage key={message.id} message={message} />
305
+ ))
306
+ )}
307
+ </Box>
308
+ </Paper>
309
+ )
310
+ }),
311
+ )
@@ -4,7 +4,12 @@ import { observer } from "mobx-react-lite"
4
4
  import { useTranslation } from "react-i18next"
5
5
  import { externalizeComponent } from "../externalizeComponent"
6
6
 
7
- export type ProgramState = "idle" | "running" | "paused" | "stopping"
7
+ export enum ProgramState {
8
+ IDLE = "idle",
9
+ RUNNING = "running",
10
+ PAUSED = "paused",
11
+ STOPPING = "stopping",
12
+ }
8
13
 
9
14
  export interface ProgramControlProps {
10
15
  /** The current state of the program control */
@@ -70,22 +75,24 @@ export const ProgramControl = externalizeComponent(
70
75
  const getButtonConfigs = (): ButtonConfig[] => {
71
76
  const baseConfigs: Record<string, ButtonConfig> = {
72
77
  run: {
73
- enabled: state === "idle" || state === "paused",
78
+ enabled:
79
+ state === ProgramState.IDLE || state === ProgramState.PAUSED,
74
80
  label:
75
- state === "paused"
81
+ state === ProgramState.PAUSED
76
82
  ? t("ProgramControl.Resume.bt")
77
83
  : t("ProgramControl.Start.bt"),
78
84
  color: theme.palette.success.main,
79
85
  onClick: onRun,
80
86
  },
81
87
  pause: {
82
- enabled: state === "running",
88
+ enabled: state === ProgramState.RUNNING,
83
89
  label: t("ProgramControl.Pause.bt"),
84
90
  color: "#FFFFFF33",
85
91
  onClick: onPause || (() => {}),
86
92
  },
87
93
  stop: {
88
- enabled: state === "running" || state === "paused",
94
+ enabled:
95
+ state === ProgramState.RUNNING || state === ProgramState.PAUSED,
89
96
  label: t("ProgramControl.Stop.bt"),
90
97
  color: theme.palette.error.main,
91
98
  onClick: onStop,
@@ -157,7 +164,7 @@ export const ProgramControl = externalizeComponent(
157
164
  variant="contained"
158
165
  disabled={
159
166
  !config.enabled ||
160
- (state === "stopping" && !requiresManualReset)
167
+ (state === ProgramState.STOPPING && !requiresManualReset)
161
168
  }
162
169
  onClick={config.onClick}
163
170
  sx={{
@@ -167,14 +174,17 @@ export const ProgramControl = externalizeComponent(
167
174
  backgroundColor: config.color,
168
175
  opacity:
169
176
  config.enabled &&
170
- !(state === "stopping" && !requiresManualReset)
177
+ !(state === ProgramState.STOPPING && !requiresManualReset)
171
178
  ? 1
172
179
  : 0.3,
173
180
  "&:hover": {
174
181
  backgroundColor: config.color,
175
182
  opacity:
176
183
  config.enabled &&
177
- !(state === "stopping" && !requiresManualReset)
184
+ !(
185
+ state === ProgramState.STOPPING &&
186
+ !requiresManualReset
187
+ )
178
188
  ? 0.8
179
189
  : 0.3,
180
190
  },
@@ -194,13 +204,13 @@ export const ProgramControl = externalizeComponent(
194
204
  sx={{
195
205
  color:
196
206
  config.enabled &&
197
- !(state === "stopping" && !requiresManualReset)
207
+ !(state === ProgramState.STOPPING && !requiresManualReset)
198
208
  ? config.color
199
209
  : theme.palette.text.disabled,
200
210
  textAlign: "center",
201
211
  opacity:
202
212
  config.enabled &&
203
- !(state === "stopping" && !requiresManualReset)
213
+ !(state === ProgramState.STOPPING && !requiresManualReset)
204
214
  ? 1
205
215
  : 0.3,
206
216
  }}
@@ -1,4 +1,4 @@
1
- import { Chip, useTheme } from "@mui/material"
1
+ import { Chip, Typography, useTheme } from "@mui/material"
2
2
  import type {
3
3
  RobotControllerStateOperationModeEnum,
4
4
  RobotControllerStateSafetyStateEnum,
@@ -6,7 +6,7 @@ import type {
6
6
  import { observer } from "mobx-react-lite"
7
7
  import { useTranslation } from "react-i18next"
8
8
  import { externalizeComponent } from "../externalizeComponent"
9
- import type { ProgramState } from "./ProgramControl"
9
+ import { ProgramState } from "./ProgramControl"
10
10
 
11
11
  export interface ProgramStateIndicatorProps {
12
12
  /** The current state of the program */
@@ -77,22 +77,22 @@ export const ProgramStateIndicator = externalizeComponent(
77
77
  // For normal safety states, check program state
78
78
  if (safetyState === "SAFETY_STATE_NORMAL") {
79
79
  switch (programState) {
80
- case "running":
80
+ case ProgramState.RUNNING:
81
81
  return {
82
82
  label: t("ProgramStateIndicator.Running.lb"),
83
83
  color: theme.palette.success.main,
84
84
  }
85
- case "paused":
85
+ case ProgramState.PAUSED:
86
86
  return {
87
87
  label: t("ProgramStateIndicator.Paused.lb"),
88
88
  color: theme.palette.grey[600],
89
89
  }
90
- case "stopping":
90
+ case ProgramState.STOPPING:
91
91
  return {
92
92
  label: t("ProgramStateIndicator.Stopped.lb"),
93
93
  color: theme.palette.error.main,
94
94
  }
95
- case "idle":
95
+ case ProgramState.IDLE:
96
96
  default:
97
97
  return {
98
98
  label: t("ProgramStateIndicator.Ready.lb"),
@@ -131,14 +131,26 @@ export const ProgramStateIndicator = externalizeComponent(
131
131
  return (
132
132
  <Chip
133
133
  className={className}
134
- label={fullLabel}
134
+ label={
135
+ <Typography
136
+ variant="body2"
137
+ sx={{
138
+ fontSize: "0.75rem", // Smaller than body2
139
+ lineHeight: 1.2,
140
+ }}
141
+ >
142
+ {fullLabel}
143
+ </Typography>
144
+ }
135
145
  variant="filled"
136
146
  sx={{
137
147
  backgroundColor: color,
138
148
  color: theme.palette.getContrastText(color),
139
149
  fontWeight: 500,
150
+ height: "auto",
140
151
  "& .MuiChip-label": {
141
- paddingX: 2,
152
+ paddingX: 1.5,
153
+ paddingY: 0.5,
142
154
  },
143
155
  }}
144
156
  />