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

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 +54 -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 +48 -48
  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 +8321 -7160
  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 +85 -0
  34. package/src/components/LogStore.ts +40 -0
  35. package/src/components/LogViewer.tsx +297 -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,297 @@
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 { Box, Button, IconButton, Paper, Typography } from "@mui/material"
9
+ import { observer } from "mobx-react-lite"
10
+ import { useEffect, useRef, useState } from "react"
11
+ import { externalizeComponent } from "../externalizeComponent"
12
+
13
+ export type LogLevel = "info" | "error" | "warning"
14
+
15
+ export type LogMessage = {
16
+ id: string
17
+ timestamp: Date
18
+ message: string
19
+ level: LogLevel
20
+ }
21
+
22
+ export type LogViewerProps = {
23
+ /** Log messages to display */
24
+ messages: LogMessage[]
25
+ /** Callback when clear button is clicked */
26
+ onClear?: () => void
27
+ /** Height of the component */
28
+ height?: string | number
29
+ /** Additional styles */
30
+ sx?: SxProps
31
+ }
32
+
33
+ /**
34
+ * A log viewer component that displays timestamped log messages with different levels.
35
+ * Features a header with document icon and clear button, and scrollable message area.
36
+ */
37
+ export const LogViewer = externalizeComponent(
38
+ observer((props: LogViewerProps) => {
39
+ const { messages = [], onClear, height = 400, sx } = props
40
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
41
+
42
+ // Auto-scroll to bottom when new messages are added
43
+ useEffect(() => {
44
+ if (messages.length === 0) return
45
+
46
+ const scrollContainer = scrollContainerRef.current
47
+ if (!scrollContainer) return
48
+
49
+ // Use a timeout to scroll after the DOM updates
50
+ const timeoutId = setTimeout(() => {
51
+ // Scroll the container to the bottom, not the entire browser
52
+ scrollContainer.scrollTop = scrollContainer.scrollHeight
53
+ }, 10)
54
+
55
+ return () => clearTimeout(timeoutId)
56
+ }, [messages.length])
57
+
58
+ const formatTimestamp = (timestamp: Date) => {
59
+ return timestamp.toLocaleTimeString("en-US", {
60
+ hour12: false,
61
+ hour: "2-digit",
62
+ minute: "2-digit",
63
+ second: "2-digit",
64
+ })
65
+ }
66
+
67
+ const getMessageColor = (level: LogLevel) => {
68
+ switch (level) {
69
+ case "error":
70
+ return "var(--error-main, #EF5350)"
71
+ case "warning":
72
+ return "var(--warning-main, #FF9800)"
73
+ case "info":
74
+ default:
75
+ return "var(--text-secondary, #FFFFFFB2)"
76
+ }
77
+ }
78
+
79
+ // Component for individual log messages with expand/copy functionality
80
+ const LogMessage = ({ message }: { message: LogMessage }) => {
81
+ const [isExpanded, setIsExpanded] = useState(false)
82
+ const [copyTooltip, setCopyTooltip] = useState(false)
83
+ const [isHovered, setIsHovered] = useState(false)
84
+ const isLongMessage = message.message.length > 150
85
+
86
+ const handleCopy = async () => {
87
+ try {
88
+ await navigator.clipboard.writeText(message.message)
89
+ setCopyTooltip(true)
90
+ setTimeout(() => setCopyTooltip(false), 2000)
91
+ } catch (err) {
92
+ console.error("Failed to copy message:", err)
93
+ }
94
+ }
95
+
96
+ const displayMessage =
97
+ isLongMessage && !isExpanded
98
+ ? message.message.substring(0, 150) + "..."
99
+ : message.message
100
+
101
+ return (
102
+ <Box
103
+ key={message.id}
104
+ onMouseEnter={() => setIsHovered(true)}
105
+ onMouseLeave={() => setIsHovered(false)}
106
+ sx={{
107
+ display: "flex",
108
+ gap: 1,
109
+ fontFamily: "monospace",
110
+ flexDirection: "column",
111
+ "&:hover": {
112
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
113
+ },
114
+ borderRadius: "4px",
115
+ padding: "2px 4px",
116
+ margin: "-2px -4px",
117
+ }}
118
+ >
119
+ <Box sx={{ display: "flex", gap: 1 }}>
120
+ {/* Timestamp */}
121
+ <Typography
122
+ component="span"
123
+ sx={{
124
+ fontWeight: 400,
125
+ fontSize: "12px",
126
+ lineHeight: "18px",
127
+ letterSpacing: "0.4px",
128
+ color: "var(--text-disabled, #FFFFFF61)",
129
+ whiteSpace: "nowrap",
130
+ flexShrink: 0,
131
+ }}
132
+ >
133
+ [{formatTimestamp(message.timestamp)}]
134
+ </Typography>
135
+
136
+ {/* Message */}
137
+ <Typography
138
+ component="span"
139
+ sx={{
140
+ fontWeight: 400,
141
+ fontSize: "12px",
142
+ lineHeight: "18px",
143
+ letterSpacing: "0.4px",
144
+ color: getMessageColor(message.level),
145
+ wordBreak: "break-word",
146
+ overflowWrap: "anywhere",
147
+ hyphens: "auto",
148
+ flex: 1,
149
+ whiteSpace: "pre-wrap",
150
+ }}
151
+ >
152
+ {displayMessage}
153
+ </Typography>
154
+
155
+ {/* Action buttons - only visible on hover */}
156
+ <Box
157
+ sx={{
158
+ display: "flex",
159
+ alignItems: "flex-start",
160
+ gap: 0.5,
161
+ opacity: isHovered ? 1 : 0,
162
+ transition: "opacity 0.2s ease-in-out",
163
+ visibility: isHovered ? "visible" : "hidden",
164
+ }}
165
+ >
166
+ <IconButton
167
+ size="small"
168
+ onClick={handleCopy}
169
+ sx={{
170
+ padding: "2px",
171
+ color: "var(--text-secondary, #FFFFFFB2)",
172
+ "&:hover": {
173
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
174
+ },
175
+ }}
176
+ title={copyTooltip ? "Copied!" : "Copy message"}
177
+ >
178
+ <ContentCopy sx={{ fontSize: 12 }} />
179
+ </IconButton>
180
+
181
+ {isLongMessage && (
182
+ <IconButton
183
+ size="small"
184
+ onClick={() => setIsExpanded(!isExpanded)}
185
+ sx={{
186
+ padding: "2px",
187
+ color: "var(--text-secondary, #FFFFFFB2)",
188
+ "&:hover": {
189
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
190
+ },
191
+ }}
192
+ title={isExpanded ? "Collapse" : "Expand"}
193
+ >
194
+ {isExpanded ? (
195
+ <ExpandLess sx={{ fontSize: 12 }} />
196
+ ) : (
197
+ <ExpandMore sx={{ fontSize: 12 }} />
198
+ )}
199
+ </IconButton>
200
+ )}
201
+ </Box>
202
+ </Box>
203
+ </Box>
204
+ )
205
+ }
206
+
207
+ return (
208
+ <Paper
209
+ sx={{
210
+ background: "var(--background-paper-elevation-2, #171927)",
211
+ height,
212
+ display: "flex",
213
+ flexDirection: "column",
214
+ overflow: "hidden",
215
+ ...sx,
216
+ }}
217
+ >
218
+ {/* Header */}
219
+ <Box
220
+ sx={{
221
+ display: "flex",
222
+ alignItems: "center",
223
+ justifyContent: "space-between",
224
+ padding: "12px 16px",
225
+ }}
226
+ >
227
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
228
+ <DocumentIcon
229
+ sx={{ fontSize: 16, color: "var(--text-secondary, #FFFFFFB2)" }}
230
+ />
231
+ <Typography
232
+ sx={{
233
+ fontWeight: 500,
234
+ fontSize: "14px",
235
+ lineHeight: "143%",
236
+ letterSpacing: "0.17px",
237
+ color: "var(--text-primary, #FFFFFF)",
238
+ }}
239
+ >
240
+ Log
241
+ </Typography>
242
+ </Box>
243
+ <Button
244
+ onClick={onClear}
245
+ variant="text"
246
+ sx={{
247
+ fontWeight: 500,
248
+ fontSize: "13px",
249
+ lineHeight: "22px",
250
+ letterSpacing: "0.46px",
251
+ color: "var(--primary-main, #8E56FC)",
252
+ textTransform: "none",
253
+ minWidth: "auto",
254
+ padding: "4px 8px",
255
+ "&:hover": {
256
+ backgroundColor: "rgba(142, 86, 252, 0.08)",
257
+ },
258
+ }}
259
+ >
260
+ Clear
261
+ </Button>
262
+ </Box>
263
+
264
+ {/* Messages Container */}
265
+ <Box
266
+ ref={scrollContainerRef}
267
+ sx={{
268
+ flex: 1,
269
+ overflow: "auto",
270
+ padding: "8px 16px",
271
+ display: "flex",
272
+ flexDirection: "column",
273
+ gap: "2px",
274
+ }}
275
+ >
276
+ {messages.length === 0 ? (
277
+ <Typography
278
+ sx={{
279
+ color: "var(--text-disabled, #FFFFFF61)",
280
+ fontSize: "12px",
281
+ fontStyle: "italic",
282
+ textAlign: "center",
283
+ marginTop: 2,
284
+ }}
285
+ >
286
+ No log messages
287
+ </Typography>
288
+ ) : (
289
+ messages.map((message) => (
290
+ <LogMessage key={message.id} message={message} />
291
+ ))
292
+ )}
293
+ </Box>
294
+ </Paper>
295
+ )
296
+ }),
297
+ )
@@ -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
  />