@wandelbots/wandelbots-js-react-components 2.32.0-pr.feature-robot-precondition-list.372.8c2beb0 → 2.32.0-pr.feature-robot-precondition-list.372.8ed54a6

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.32.0-pr.feature-robot-precondition-list.372.8c2beb0",
3
+ "version": "2.32.0-pr.feature-robot-precondition-list.372.8ed54a6",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -301,77 +301,56 @@ export const CycleTimer = externalizeComponent(
301
301
  sx={{
302
302
  display: "flex",
303
303
  alignItems: "center",
304
- gap: 0.125, // Minimal gap - 1px
304
+ m: 0,
305
+ gap: 1, // 8px gap between circle and text
305
306
  }}
306
307
  >
307
- {/* Animated progress gauge icon */}
308
+ {/* Animated progress ring icon */}
308
309
  <Box
309
310
  sx={{
310
- position: "relative",
311
- width: 40,
312
- height: 40,
311
+ width: 20,
312
+ height: 20,
313
313
  display: "flex",
314
314
  alignItems: "center",
315
315
  justifyContent: "center",
316
- borderRadius: "50%",
317
- overflow: "visible",
316
+ opacity: isPausedState ? 0.6 : 1,
317
+ transition: "opacity 0.2s ease",
318
318
  }}
319
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
- />
320
+ <svg
321
+ width="20"
322
+ height="20"
323
+ viewBox="0 0 20 20"
324
+ style={{ transform: "rotate(-90deg)" }}
325
+ role="img"
326
+ aria-label="Timer progress"
327
+ >
328
+ {/* Background ring */}
329
+ <circle
330
+ cx="10"
331
+ cy="10"
332
+ r="8"
333
+ fill="none"
334
+ stroke={theme.palette.success.main}
335
+ strokeWidth="2"
336
+ opacity={0.3}
337
+ />
338
+ {/* Progress ring */}
339
+ <circle
340
+ cx="10"
341
+ cy="10"
342
+ r="8"
343
+ fill="none"
344
+ stroke={theme.palette.success.main}
345
+ strokeWidth="2"
346
+ strokeLinecap="round"
347
+ strokeDasharray={`${2 * Math.PI * 8}`}
348
+ strokeDashoffset={`${2 * Math.PI * 8 * (1 - progressValue / 100)}`}
349
+ style={{
350
+ transition: "stroke-dashoffset 0.1s ease-out",
351
+ }}
352
+ />
353
+ </svg>
375
354
  </Box>
376
355
 
377
356
  {/* Timer text display */}
@@ -383,8 +362,8 @@ export const CycleTimer = externalizeComponent(
383
362
  }}
384
363
  >
385
364
  {compact
386
- ? // Compact mode: show only remaining time
387
- formatTime(remainingTime)
365
+ ? // Compact mode: show remaining time with "min." suffix
366
+ `${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
388
367
  : // Full mode: show "remaining / of total min." format
389
368
  `${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`}
390
369
  </Typography>
@@ -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,
@@ -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
  />
@@ -0,0 +1,507 @@
1
+ import { Box, Button, Card, Divider, Typography, useTheme } from "@mui/material"
2
+ import { Bounds, useBounds } from "@react-three/drei"
3
+ import { Canvas } from "@react-three/fiber"
4
+ import type { DHParameter } from "@wandelbots/nova-api/v1"
5
+ import type {
6
+ ConnectedMotionGroup,
7
+ RobotControllerStateOperationModeEnum,
8
+ RobotControllerStateSafetyStateEnum,
9
+ } from "@wandelbots/nova-js/v1"
10
+ import { observer } from "mobx-react-lite"
11
+ import { useCallback, useEffect, useRef, useState } from "react"
12
+ import { useTranslation } from "react-i18next"
13
+ import type { Group } from "three"
14
+ import { externalizeComponent } from "../externalizeComponent"
15
+ import { PresetEnvironment } from "./3d-viewport/PresetEnvironment"
16
+ import { CycleTimer } from "./CycleTimer"
17
+ import type { ProgramState } from "./ProgramControl"
18
+ import { ProgramStateIndicator } from "./ProgramStateIndicator"
19
+ import { SupportedRobot } from "./robots/SupportedRobot"
20
+
21
+ // Component to refresh bounds when model renders
22
+ function BoundsRefresher({
23
+ modelRenderTrigger,
24
+ children,
25
+ }: {
26
+ modelRenderTrigger?: number
27
+ children: React.ReactNode
28
+ }) {
29
+ const api = useBounds()
30
+
31
+ useEffect(() => {
32
+ if (modelRenderTrigger && modelRenderTrigger > 0) {
33
+ // Small delay to ensure the model is fully rendered
34
+ const timer = setTimeout(() => {
35
+ api.refresh().fit()
36
+ }, 100)
37
+ return () => clearTimeout(timer)
38
+ }
39
+ }, [modelRenderTrigger, api])
40
+
41
+ return <>{children}</>
42
+ }
43
+
44
+ export interface RobotCardProps {
45
+ /** Name of the robot displayed at the top */
46
+ robotName: string
47
+ /** Current program state */
48
+ programState: ProgramState
49
+ /** Current safety state of the robot controller */
50
+ safetyState: RobotControllerStateSafetyStateEnum
51
+ /** Current operation mode of the robot controller */
52
+ operationMode: RobotControllerStateOperationModeEnum
53
+ /** Whether the "Drive to Home" button should be enabled */
54
+ driveToHomeEnabled?: boolean
55
+ /** Callback fired when "Drive to Home" button is pressed */
56
+ onDriveToHomePress?: () => void
57
+ /** Callback fired when "Drive to Home" button is released */
58
+ onDriveToHomeRelease?: () => void
59
+ /** Connected motion group for the robot */
60
+ connectedMotionGroup: ConnectedMotionGroup
61
+ /** Custom robot component to render (optional, defaults to SupportedRobot) */
62
+ robotComponent?: React.ComponentType<{
63
+ rapidlyChangingMotionState: ConnectedMotionGroup["rapidlyChangingMotionState"]
64
+ modelFromController: string
65
+ dhParameters: DHParameter[]
66
+ flangeRef?: React.Ref<Group>
67
+ postModelRender?: () => void
68
+ transparentColor?: string
69
+ getModel?: (modelFromController: string) => string
70
+ }>
71
+ /** Custom cycle timer component (optional, defaults to CycleTimer) */
72
+ cycleTimerComponent?: React.ComponentType<{
73
+ variant?: "default" | "small"
74
+ compact?: boolean
75
+ onCycleComplete: (controls: {
76
+ startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
77
+ pause: () => void
78
+ resume: () => void
79
+ isPaused: () => boolean
80
+ }) => void
81
+ onCycleEnd?: () => void
82
+ autoStart?: boolean
83
+ className?: string
84
+ }>
85
+ /** Additional CSS class name */
86
+ className?: string
87
+ }
88
+
89
+ /**
90
+ * A responsive card component that displays a 3D robot with states and controls.
91
+ * The card automatically adapts to its container's size and aspect ratio.
92
+ *
93
+ * Features:
94
+ * - Fully responsive Material-UI Card that adapts to container dimensions
95
+ * - Automatic layout switching based on aspect ratio:
96
+ * - Portrait mode: Vertical layout with robot in center
97
+ * - Landscape mode: Horizontal layout with robot on left, content on right (left-aligned)
98
+ * - Minimum size constraints (300px width, 400px height in portrait, 250px height in landscape) for usability
99
+ * - Robot name displayed in Typography h6 at top-left
100
+ * - Program state indicator below the name
101
+ * - Auto-fitting 3D robot model that scales with container size
102
+ * - Compact cycle time component with small variant
103
+ * - Transparent gray divider line
104
+ * - "Drive to Home" button with press-and-hold functionality
105
+ * - Localization support via react-i18next
106
+ * - Material-UI theming integration
107
+ */
108
+ export const RobotCard = externalizeComponent(
109
+ observer(
110
+ ({
111
+ robotName,
112
+ programState,
113
+ safetyState,
114
+ operationMode,
115
+ driveToHomeEnabled = false,
116
+ onDriveToHomePress,
117
+ onDriveToHomeRelease,
118
+ connectedMotionGroup,
119
+ robotComponent: RobotComponent = SupportedRobot,
120
+ cycleTimerComponent: CycleTimerComponent = CycleTimer,
121
+ className,
122
+ }: RobotCardProps) => {
123
+ const theme = useTheme()
124
+ const { t } = useTranslation()
125
+ const [isDriveToHomePressed, setIsDriveToHomePressed] = useState(false)
126
+ const driveButtonRef = useRef<HTMLButtonElement>(null)
127
+ const robotRef = useRef<Group>(null)
128
+ const cardRef = useRef<HTMLDivElement>(null)
129
+ const [isLandscape, setIsLandscape] = useState(false)
130
+ const [modelRenderTrigger, setModelRenderTrigger] = useState(0)
131
+
132
+ // Hook to detect aspect ratio changes
133
+ useEffect(() => {
134
+ const checkAspectRatio = () => {
135
+ if (cardRef.current) {
136
+ const { offsetWidth, offsetHeight } = cardRef.current
137
+ setIsLandscape(offsetWidth > offsetHeight)
138
+ }
139
+ }
140
+
141
+ // Initial check
142
+ checkAspectRatio()
143
+
144
+ // Set up ResizeObserver to watch for size changes
145
+ const resizeObserver = new ResizeObserver(checkAspectRatio)
146
+ if (cardRef.current) {
147
+ resizeObserver.observe(cardRef.current)
148
+ }
149
+
150
+ return () => {
151
+ resizeObserver.disconnect()
152
+ }
153
+ }, [])
154
+
155
+ const handleModelRender = useCallback(() => {
156
+ // Trigger bounds refresh when model renders
157
+ setModelRenderTrigger((prev) => prev + 1)
158
+ }, [])
159
+
160
+ const handleDriveToHomeMouseDown = useCallback(() => {
161
+ if (!driveToHomeEnabled || !onDriveToHomePress) return
162
+ setIsDriveToHomePressed(true)
163
+ onDriveToHomePress()
164
+ }, [driveToHomeEnabled, onDriveToHomePress])
165
+
166
+ const handleDriveToHomeMouseUp = useCallback(() => {
167
+ if (!driveToHomeEnabled || !onDriveToHomeRelease) return
168
+ setIsDriveToHomePressed(false)
169
+ onDriveToHomeRelease()
170
+ }, [driveToHomeEnabled, onDriveToHomeRelease])
171
+
172
+ const handleDriveToHomeMouseLeave = useCallback(() => {
173
+ if (isDriveToHomePressed && onDriveToHomeRelease) {
174
+ setIsDriveToHomePressed(false)
175
+ onDriveToHomeRelease()
176
+ }
177
+ }, [isDriveToHomePressed, onDriveToHomeRelease])
178
+
179
+ // Mock cycle timer controls for now
180
+ const handleCycleComplete = useCallback(
181
+ (_controls: {
182
+ startNewCycle: (
183
+ maxTimeSeconds: number,
184
+ elapsedSeconds?: number,
185
+ ) => void
186
+ pause: () => void
187
+ resume: () => void
188
+ isPaused: () => boolean
189
+ }) => {
190
+ // TODO: Implement cycle timer integration if needed
191
+ // Controls are available here for future integration
192
+ },
193
+ [],
194
+ )
195
+
196
+ return (
197
+ <Card
198
+ ref={cardRef}
199
+ className={className}
200
+ sx={{
201
+ width: "100%",
202
+ height: "100%",
203
+ display: "flex",
204
+ flexDirection: isLandscape ? "row" : "column",
205
+ position: "relative",
206
+ overflow: "hidden",
207
+ minWidth: 300,
208
+ minHeight: isLandscape ? 300 : 400,
209
+ background: "var(--background-paper-elevation-8, #292B3F)",
210
+ border:
211
+ "1px solid var(--secondary-_states-outlinedBorder, #FFFFFF1F)",
212
+ borderRadius: "18px",
213
+ boxShadow: "none",
214
+ }}
215
+ >
216
+ {isLandscape ? (
217
+ <>
218
+ {/* Landscape Layout: Robot on left, content on right */}
219
+ <Box
220
+ sx={{
221
+ flex: "0 0 50%",
222
+ position: "relative",
223
+ height: "100%",
224
+ minHeight: "100%",
225
+ maxHeight: "100%",
226
+ borderRadius: 1,
227
+ m: 2,
228
+ mr: 1,
229
+ overflow: "hidden", // Prevent content from affecting container size
230
+ }}
231
+ >
232
+ <Canvas
233
+ camera={{
234
+ position: [2, 2, 2],
235
+ fov: 45,
236
+ }}
237
+ shadows
238
+ style={{
239
+ borderRadius: theme.shape.borderRadius,
240
+ width: "100%",
241
+ height: "100%",
242
+ background: "transparent",
243
+ position: "absolute",
244
+ top: 0,
245
+ left: 0,
246
+ }}
247
+ dpr={[1, 2]}
248
+ gl={{ alpha: true, antialias: true }}
249
+ >
250
+ <PresetEnvironment />
251
+ <Bounds fit clip observe margin={1}>
252
+ <BoundsRefresher modelRenderTrigger={modelRenderTrigger}>
253
+ <group ref={robotRef}>
254
+ <RobotComponent
255
+ rapidlyChangingMotionState={
256
+ connectedMotionGroup.rapidlyChangingMotionState
257
+ }
258
+ modelFromController={
259
+ connectedMotionGroup.modelFromController || ""
260
+ }
261
+ dhParameters={connectedMotionGroup.dhParameters || []}
262
+ postModelRender={handleModelRender}
263
+ />
264
+ </group>
265
+ </BoundsRefresher>
266
+ </Bounds>
267
+ </Canvas>
268
+ </Box>
269
+
270
+ {/* Content container on right */}
271
+ <Box
272
+ sx={{
273
+ flex: "1",
274
+ display: "flex",
275
+ flexDirection: "column",
276
+ justifyContent: "flex-start",
277
+ }}
278
+ >
279
+ {/* Header section with robot name and program state */}
280
+ <Box
281
+ sx={{
282
+ p: 3,
283
+ pb: 2,
284
+ textAlign: "left",
285
+ }}
286
+ >
287
+ <Typography variant="h6" component="h2" sx={{ mb: 1 }}>
288
+ {robotName}
289
+ </Typography>
290
+ <ProgramStateIndicator
291
+ programState={programState}
292
+ safetyState={safetyState}
293
+ operationMode={operationMode}
294
+ />
295
+ </Box>
296
+
297
+ {/* Bottom section with runtime, cycle time, and button */}
298
+ <Box
299
+ sx={{
300
+ p: 3,
301
+ pt: 0,
302
+ flex: "1",
303
+ display: "flex",
304
+ flexDirection: "column",
305
+ justifyContent: "space-between",
306
+ }}
307
+ >
308
+ <Box>
309
+ {/* Runtime display */}
310
+ <Typography
311
+ variant="body1"
312
+ sx={{
313
+ mb: 0,
314
+ color: "var(--text-secondary, #FFFFFFB2)",
315
+ textAlign: "left",
316
+ }}
317
+ >
318
+ {t("RobotCard.Runtime.lb")}
319
+ </Typography>
320
+
321
+ {/* Compact cycle time component directly below runtime */}
322
+ <Box sx={{ textAlign: "left" }}>
323
+ <CycleTimerComponent
324
+ variant="small"
325
+ compact
326
+ onCycleComplete={handleCycleComplete}
327
+ />
328
+ </Box>
329
+
330
+ {/* Divider */}
331
+ <Divider
332
+ sx={{
333
+ mt: 2,
334
+ mb: 2,
335
+ borderColor: theme.palette.divider,
336
+ opacity: 0.5,
337
+ }}
338
+ />
339
+ </Box>
340
+
341
+ <Box sx={{ mt: "auto" }}>
342
+ {/* Drive to Home button with some space */}
343
+ <Box
344
+ sx={{
345
+ display: "flex",
346
+ justifyContent: "flex-start",
347
+ mt: 2,
348
+ mb: 2,
349
+ }}
350
+ >
351
+ <Button
352
+ ref={driveButtonRef}
353
+ variant="contained"
354
+ color="secondary"
355
+ size="small"
356
+ disabled={true}
357
+ onMouseDown={handleDriveToHomeMouseDown}
358
+ onMouseUp={handleDriveToHomeMouseUp}
359
+ onMouseLeave={handleDriveToHomeMouseLeave}
360
+ onTouchStart={handleDriveToHomeMouseDown}
361
+ onTouchEnd={handleDriveToHomeMouseUp}
362
+ sx={{
363
+ textTransform: "none",
364
+ px: 1.5,
365
+ py: 0.5,
366
+ }}
367
+ >
368
+ {t("RobotCard.DriveToHome.bt")}
369
+ </Button>
370
+ </Box>
371
+ </Box>
372
+ </Box>
373
+ </Box>
374
+ </>
375
+ ) : (
376
+ <>
377
+ {/* Portrait Layout: Header, Robot, Footer */}
378
+
379
+ {/* Header section with robot name and program state */}
380
+ <Box sx={{ p: 3, pb: 1 }}>
381
+ <Typography variant="h6" component="h2" sx={{ mb: 1 }}>
382
+ {robotName}
383
+ </Typography>
384
+ <ProgramStateIndicator
385
+ programState={programState}
386
+ safetyState={safetyState}
387
+ operationMode={operationMode}
388
+ />
389
+ </Box>
390
+
391
+ {/* 3D Robot viewport in center */}
392
+ <Box
393
+ sx={{
394
+ flex: 1,
395
+ position: "relative",
396
+ minHeight: 200,
397
+ borderRadius: 1,
398
+ mx: 3,
399
+ mb: 1,
400
+ overflow: "hidden", // Prevent content from affecting container size
401
+ }}
402
+ >
403
+ <Canvas
404
+ camera={{
405
+ position: [2, 2, 2],
406
+ fov: 45,
407
+ }}
408
+ shadows
409
+ style={{
410
+ borderRadius: theme.shape.borderRadius,
411
+ width: "100%",
412
+ height: "100%",
413
+ background: "transparent",
414
+ position: "absolute",
415
+ top: 0,
416
+ left: 0,
417
+ }}
418
+ dpr={[1, 2]}
419
+ gl={{ alpha: true, antialias: true }}
420
+ >
421
+ <PresetEnvironment />
422
+ <Bounds fit clip observe margin={1.2}>
423
+ <BoundsRefresher modelRenderTrigger={modelRenderTrigger}>
424
+ <group ref={robotRef}>
425
+ <RobotComponent
426
+ rapidlyChangingMotionState={
427
+ connectedMotionGroup.rapidlyChangingMotionState
428
+ }
429
+ modelFromController={
430
+ connectedMotionGroup.modelFromController || ""
431
+ }
432
+ dhParameters={connectedMotionGroup.dhParameters || []}
433
+ postModelRender={handleModelRender}
434
+ />
435
+ </group>
436
+ </BoundsRefresher>
437
+ </Bounds>
438
+ </Canvas>
439
+ </Box>
440
+
441
+ {/* Bottom section with runtime, cycle time, and button */}
442
+ <Box sx={{ p: 3, pt: 0 }}>
443
+ {/* Runtime display */}
444
+ <Typography
445
+ variant="body1"
446
+ sx={{
447
+ mb: 0,
448
+ color: "var(--text-secondary, #FFFFFFB2)",
449
+ }}
450
+ >
451
+ {t("RobotCard.Runtime.lb")}
452
+ </Typography>
453
+
454
+ {/* Compact cycle time component directly below runtime */}
455
+ <CycleTimerComponent
456
+ variant="small"
457
+ compact
458
+ onCycleComplete={handleCycleComplete}
459
+ />
460
+
461
+ {/* Divider */}
462
+ <Divider
463
+ sx={{
464
+ mt: 2,
465
+ mb: 2,
466
+ borderColor: theme.palette.divider,
467
+ opacity: 0.5,
468
+ }}
469
+ />
470
+
471
+ {/* Drive to Home button with some space */}
472
+ <Box
473
+ sx={{
474
+ display: "flex",
475
+ justifyContent: "flex-start",
476
+ mt: 5,
477
+ mb: 2,
478
+ }}
479
+ >
480
+ <Button
481
+ ref={driveButtonRef}
482
+ variant="contained"
483
+ color="secondary"
484
+ size="small"
485
+ disabled={true}
486
+ onMouseDown={handleDriveToHomeMouseDown}
487
+ onMouseUp={handleDriveToHomeMouseUp}
488
+ onMouseLeave={handleDriveToHomeMouseLeave}
489
+ onTouchStart={handleDriveToHomeMouseDown}
490
+ onTouchEnd={handleDriveToHomeMouseUp}
491
+ sx={{
492
+ textTransform: "none",
493
+ px: 1.5,
494
+ py: 0.5,
495
+ }}
496
+ >
497
+ {t("RobotCard.DriveToHome.bt")}
498
+ </Button>
499
+ </Box>
500
+ </Box>
501
+ </>
502
+ )}
503
+ </Card>
504
+ )
505
+ },
506
+ ),
507
+ )
@@ -61,7 +61,6 @@ export const RobotListItem = externalizeComponent(
61
61
  border:
62
62
  "1px solid var(--secondary-_states-outlinedBorder, #FFFFFF1F)",
63
63
  background: "var(--background-paper-elevation-8, #292B3E)",
64
- width: 732,
65
64
  height: 80,
66
65
  minHeight: "80px",
67
66
  borderRadius: "8px",