@procaaso/alphinity-ui-components 1.12.0 → 1.14.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/README.md CHANGED
@@ -1,98 +1,98 @@
1
- # @procaaso/alphinity-ui-components
2
-
3
- A TypeScript React component library for Alphinity branded systems.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @procaaso/alphinity-ui-components
9
- ```
10
-
11
- ## Usage
12
-
13
- ```tsx
14
- import { Button, ValueEntry, StatusIndicator } from '@procaaso/alphinity-ui-components';
15
- import { useState } from 'react';
16
-
17
- function HMIPanel() {
18
- const [temperature, setTemperature] = useState(72.5);
19
- const [pressure, setPressure] = useState(14.7);
20
- const [systemStatus, setSystemStatus] = useState('normal');
21
-
22
- return (
23
- <div className="p-4 space-y-4">
24
- {/* System status indicators */}
25
- <div className="flex gap-4 items-center">
26
- <StatusIndicator status="normal" label="Power" size="medium" led={true} />
27
- <StatusIndicator
28
- status="alarm"
29
- label="Temperature Alert"
30
- animation="pulse"
31
- shape="circle"
32
- />
33
- <StatusIndicator status="warning" label="Maintenance Due" animation="blink" />
34
- <StatusIndicator status="active" label="Process Running" size="large" />
35
- {/* Use "unknown" when the backend does not trust the value, such as a bad-quality
36
- OPC UA read. Do not use "fault": a fault means the device itself reports a problem. */}
37
- <StatusIndicator status="unknown" label="Flow Sensor" />
38
- </div>
39
-
40
- {/* Process control values */}
41
- <ValueEntry
42
- value={temperature}
43
- onChange={setTemperature}
44
- label="Temperature"
45
- unit="°C"
46
- min={0}
47
- max={100}
48
- precision={1}
49
- status="normal"
50
- />
51
-
52
- <ValueEntry
53
- value={pressure}
54
- onChange={setPressure}
55
- label="Pressure"
56
- unit="PSI"
57
- status="warning"
58
- readOnly
59
- />
60
-
61
- {/* Control buttons */}
62
- <Button variant="primary" onClick={() => console.log('Start process')}>
63
- Start
64
- </Button>
65
- </div>
66
- );
67
- }
68
- ```
69
-
70
- ## Development
71
-
72
- ### Scripts
73
-
74
- - `npm run build` - Build the library using tsup (ESM + CJS + DTS)
75
- - `npm test` - Run tests with Vitest
76
- - `npm run lint` - Lint code with ESLint
77
- - `npm run typecheck` - Type check with TypeScript
78
- - `npm run dev` - Run the demo playground on http://localhost:3010
79
-
80
- ### Demo views
81
-
82
- The demo (`npm run dev`) mounts one of three views, selected by a `?demo=` query param:
83
-
84
- - `http://localhost:3010/` *(default)* - **Runtime**: full P&ID canvas with live telemetry, simulation, and device control/config panels
85
- - `http://localhost:3010/?demo=single-node` - **Single-node HMI mockup** over the full plant SVG layout
86
- - `http://localhost:3010/?demo=gallery` - **Component gallery** of individual widgets
87
-
88
- > The demo is served, not opened as a file — `demo-dist/index.html` references assets by absolute path and will be blank if opened via `file://`. Use `npm run dev`, or `npx vite preview --config vite.demo.config.ts` for the built version.
89
-
90
- ### Components
91
-
92
- - **Button** - Control buttons with primary and secondary variants for industrial actions
93
- - **ValueEntry** - Numeric/text input with validation, units, status indicators, and precision control
94
- - **StatusIndicator** - LED-style status indicators with multiple states, animations, and customizable appearance
95
-
96
- ## Publishing
97
-
98
- This package is published to the npm registry at https://www.npmjs.com/package/@procaaso/alphinity-ui-components
1
+ # @procaaso/alphinity-ui-components
2
+
3
+ A TypeScript React component library for Alphinity branded systems.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @procaaso/alphinity-ui-components
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { Button, ValueEntry, StatusIndicator } from '@procaaso/alphinity-ui-components';
15
+ import { useState } from 'react';
16
+
17
+ function HMIPanel() {
18
+ const [temperature, setTemperature] = useState(72.5);
19
+ const [pressure, setPressure] = useState(14.7);
20
+ const [systemStatus, setSystemStatus] = useState('normal');
21
+
22
+ return (
23
+ <div className="p-4 space-y-4">
24
+ {/* System status indicators */}
25
+ <div className="flex gap-4 items-center">
26
+ <StatusIndicator status="normal" label="Power" size="medium" led={true} />
27
+ <StatusIndicator
28
+ status="alarm"
29
+ label="Temperature Alert"
30
+ animation="pulse"
31
+ shape="circle"
32
+ />
33
+ <StatusIndicator status="warning" label="Maintenance Due" animation="blink" />
34
+ <StatusIndicator status="active" label="Process Running" size="large" />
35
+ {/* Use "unknown" when the backend does not trust the value, such as a bad-quality
36
+ OPC UA read. Do not use "fault": a fault means the device itself reports a problem. */}
37
+ <StatusIndicator status="unknown" label="Flow Sensor" />
38
+ </div>
39
+
40
+ {/* Process control values */}
41
+ <ValueEntry
42
+ value={temperature}
43
+ onChange={setTemperature}
44
+ label="Temperature"
45
+ unit="°C"
46
+ min={0}
47
+ max={100}
48
+ precision={1}
49
+ status="normal"
50
+ />
51
+
52
+ <ValueEntry
53
+ value={pressure}
54
+ onChange={setPressure}
55
+ label="Pressure"
56
+ unit="PSI"
57
+ status="warning"
58
+ readOnly
59
+ />
60
+
61
+ {/* Control buttons */}
62
+ <Button variant="primary" onClick={() => console.log('Start process')}>
63
+ Start
64
+ </Button>
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ## Development
71
+
72
+ ### Scripts
73
+
74
+ - `npm run build` - Build the library using tsup (ESM + CJS + DTS)
75
+ - `npm test` - Run tests with Vitest
76
+ - `npm run lint` - Lint code with ESLint
77
+ - `npm run typecheck` - Type check with TypeScript
78
+ - `npm run dev` - Run the demo playground on http://localhost:3010
79
+
80
+ ### Demo views
81
+
82
+ The demo (`npm run dev`) mounts one of three views, selected by a `?demo=` query param:
83
+
84
+ - `http://localhost:3010/` *(default)* - **Runtime**: full P&ID canvas with live telemetry, simulation, and device control/config panels
85
+ - `http://localhost:3010/?demo=single-node` - **Single-node HMI mockup** over the full plant SVG layout
86
+ - `http://localhost:3010/?demo=gallery` - **Component gallery** of individual widgets
87
+
88
+ > The demo is served, not opened as a file — `demo-dist/index.html` references assets by absolute path and will be blank if opened via `file://`. Use `npm run dev`, or `npx vite preview --config vite.demo.config.ts` for the built version.
89
+
90
+ ### Components
91
+
92
+ - **Button** - Control buttons with primary and secondary variants for industrial actions
93
+ - **ValueEntry** - Numeric/text input with validation, units, status indicators, and precision control
94
+ - **StatusIndicator** - LED-style status indicators with multiple states, animations, and customizable appearance
95
+
96
+ ## Publishing
97
+
98
+ This package is published to the npm registry at https://www.npmjs.com/package/@procaaso/alphinity-ui-components
package/dist/index.cjs CHANGED
@@ -88,7 +88,9 @@ __export(index_exports, {
88
88
  generateRoutePoints: () => generateRoutePoints,
89
89
  getAppearanceDefaults: () => getAppearanceDefaults,
90
90
  getAppearanceSchema: () => getAppearanceSchema,
91
+ getAvailableBadgeGlyphs: () => getAvailableBadgeGlyphs,
91
92
  getAvailableSymbols: () => getAvailableSymbols,
93
+ getBadgeGlyph: () => getBadgeGlyph,
92
94
  getPortWorldPosition: () => getPortWorldPosition,
93
95
  getSymbolDefinition: () => getSymbolDefinition,
94
96
  importDiagram: () => importDiagram,
@@ -97,6 +99,8 @@ __export(index_exports, {
97
99
  normalizeDeviceStatus: () => normalizeDeviceStatus,
98
100
  overlayStyles: () => overlayStyles,
99
101
  readAppearanceValue: () => readAppearanceValue,
102
+ registerBadgeGlyph: () => registerBadgeGlyph,
103
+ registerBadgeGlyphs: () => registerBadgeGlyphs,
100
104
  registerSymbol: () => registerSymbol,
101
105
  registerSymbols: () => registerSymbols,
102
106
  resolvePresetColor: () => resolvePresetColor,
@@ -396,6 +400,13 @@ var ISA_UNKNOWN_DARK = {
396
400
  bg: "#23263a",
397
401
  border: "#6f74a3"
398
402
  };
403
+ var FORCED_MARK_COLOR = {
404
+ fill: "#ede9fe",
405
+ stroke: "#5b21b6",
406
+ bg: "#ede9fe",
407
+ text: "#5b21b6",
408
+ border: "#7c3aed"
409
+ };
399
410
  var DEFAULT_STATUS_COLORS = {
400
411
  normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
401
412
  warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
@@ -404,14 +415,8 @@ var DEFAULT_STATUS_COLORS = {
404
415
  off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
405
416
  starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
406
417
  stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
407
- unknown: { fill: ISA_UNKNOWN.bg, stroke: ISA_UNKNOWN.border, bg: ISA_UNKNOWN.bg, text: ISA_UNKNOWN.fg, border: ISA_UNKNOWN.border }
408
- };
409
- var FORCED_MARK_COLOR = {
410
- fill: "#ede9fe",
411
- stroke: "#5b21b6",
412
- bg: "#ede9fe",
413
- text: "#5b21b6",
414
- border: "#7c3aed"
418
+ unknown: { fill: ISA_UNKNOWN.bg, stroke: ISA_UNKNOWN.border, bg: ISA_UNKNOWN.bg, text: ISA_UNKNOWN.fg, border: ISA_UNKNOWN.border },
419
+ forced: FORCED_MARK_COLOR
415
420
  };
416
421
  var STATUS_ALIASES = {
417
422
  running: "normal",
@@ -1257,6 +1262,10 @@ var NodeControlsPanel = ({
1257
1262
  // src/components/DeviceControlPanel/DeviceControlPanel.tsx
1258
1263
  var import_react6 = __toESM(require("react"), 1);
1259
1264
  var import_jsx_runtime9 = require("react/jsx-runtime");
1265
+ var DISABLED_BUTTON_BG = "#e5e7eb";
1266
+ var DISABLED_BUTTON_TEXT = "#4b5563";
1267
+ var DISABLED_BUTTON_BORDER = "#6b7280";
1268
+ var DISABLED_BUTTON_HATCH = "repeating-linear-gradient(135deg, rgba(0,0,0,0.10) 0px, rgba(0,0,0,0.10) 4px, transparent 4px, transparent 10px)";
1260
1269
  function DeviceControlPanelImpl({
1261
1270
  binding,
1262
1271
  onClose,
@@ -1337,6 +1346,36 @@ function DeviceControlPanelImpl({
1337
1346
  const statusEntry = resolveStatusColor(binding.state.status, binding.theme?.statusColors);
1338
1347
  const statusColor = statusEntry.bg;
1339
1348
  const isUnknownStatus = normalizeDeviceStatus(binding.state.status) === "unknown";
1349
+ const startStopRowShown = binding.display?.showStartStop !== false && (activeCapabilities?.canStart === true || activeCapabilities?.canStop === true);
1350
+ const startEnabledNow = activeCapabilities?.canStart === true && activeCapabilities?.startEnabled !== false && !startPending;
1351
+ const stopEnabledNow = activeCapabilities?.canStop === true && activeCapabilities?.stopEnabled !== false;
1352
+ const startStopFontPx = Math.max(18, Math.round(18 * fontScale));
1353
+ const startStopButtonStyle = (enabled, enabledBg) => ({
1354
+ flex: 1,
1355
+ minWidth: 0,
1356
+ height: Math.max(touchSize, 56),
1357
+ borderRadius: "6px",
1358
+ fontSize: `${startStopFontPx}px`,
1359
+ fontWeight: 700,
1360
+ fontFamily: "Arial, sans-serif",
1361
+ letterSpacing: "0.5px",
1362
+ ...enabled ? {
1363
+ backgroundColor: enabledBg,
1364
+ color: "#ffffff",
1365
+ border: "2px solid transparent",
1366
+ cursor: "pointer",
1367
+ opacity: 1
1368
+ } : {
1369
+ // Disabled look that does not depend on colour (ISA-101): a dashed
1370
+ // border and a diagonal hatch. A grey fill alone is not enough.
1371
+ backgroundColor: DISABLED_BUTTON_BG,
1372
+ backgroundImage: DISABLED_BUTTON_HATCH,
1373
+ color: DISABLED_BUTTON_TEXT,
1374
+ border: `2px dashed ${DISABLED_BUTTON_BORDER}`,
1375
+ cursor: "not-allowed",
1376
+ opacity: 1
1377
+ }
1378
+ });
1340
1379
  const handleModeChange = (mode) => {
1341
1380
  onModeChange?.(mode);
1342
1381
  };
@@ -1522,6 +1561,7 @@ function DeviceControlPanelImpl({
1522
1561
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1523
1562
  "div",
1524
1563
  {
1564
+ "data-testid": "device-status-dot",
1525
1565
  style: {
1526
1566
  width: "10px",
1527
1567
  height: "10px",
@@ -2107,50 +2147,45 @@ function DeviceControlPanelImpl({
2107
2147
  param.id
2108
2148
  ))
2109
2149
  ] }),
2110
- binding.display?.showStartStop !== false && (activeCapabilities?.canStart || activeCapabilities?.canStop) && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: "flex", gap: spacing.gap, marginBottom: spacing.marginBottom }, children: [
2111
- activeCapabilities?.canStart && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2112
- "button",
2113
- {
2114
- onClick: onStart,
2115
- style: {
2116
- flex: 1,
2117
- height: touchSize,
2118
- backgroundColor: "#10b981",
2119
- color: "#ffffff",
2120
- border: "none",
2121
- borderRadius: "6px",
2122
- fontSize: "12px",
2123
- fontWeight: 700,
2124
- fontFamily: "Arial, sans-serif",
2125
- letterSpacing: "0.5px",
2126
- cursor: "pointer",
2127
- opacity: 1
2128
- },
2129
- children: startPending ? "STARTING\u2026" : "START"
2130
- }
2131
- ),
2132
- activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2133
- "button",
2134
- {
2135
- onClick: onStop,
2136
- style: {
2137
- flex: 1,
2138
- height: touchSize,
2139
- backgroundColor: "#ef4444",
2140
- color: "#ffffff",
2141
- border: "none",
2142
- borderRadius: "6px",
2143
- fontSize: "12px",
2144
- fontWeight: 700,
2145
- fontFamily: "Arial, sans-serif",
2146
- letterSpacing: "0.5px",
2147
- cursor: "pointer",
2148
- opacity: 1
2149
- },
2150
- children: stopPending ? "STOPPING\u2026" : "STOP"
2151
- }
2152
- )
2153
- ] }),
2150
+ startStopRowShown && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2151
+ "div",
2152
+ {
2153
+ "data-testid": "start-stop-row",
2154
+ style: { display: "flex", gap: spacing.gap, marginBottom: spacing.marginBottom },
2155
+ children: [
2156
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2157
+ "button",
2158
+ {
2159
+ type: "button",
2160
+ "data-control": "start",
2161
+ "data-enabled": startEnabledNow ? "true" : "false",
2162
+ onClick: () => {
2163
+ if (startEnabledNow) onStart?.();
2164
+ },
2165
+ disabled: !startEnabledNow,
2166
+ "aria-busy": startPending || void 0,
2167
+ style: startStopButtonStyle(startEnabledNow, "#10b981"),
2168
+ children: startPending ? "STARTING\u2026" : "START"
2169
+ }
2170
+ ),
2171
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2172
+ "button",
2173
+ {
2174
+ type: "button",
2175
+ "data-control": "stop",
2176
+ "data-enabled": stopEnabledNow ? "true" : "false",
2177
+ onClick: () => {
2178
+ if (stopEnabledNow) onStop?.();
2179
+ },
2180
+ disabled: !stopEnabledNow,
2181
+ "aria-busy": stopPending || void 0,
2182
+ style: startStopButtonStyle(stopEnabledNow, "#ef4444"),
2183
+ children: stopPending ? "STOPPING\u2026" : "STOP"
2184
+ }
2185
+ )
2186
+ ]
2187
+ }
2188
+ ),
2154
2189
  activeCapabilities?.customActions && activeCapabilities.customActions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: activeCapabilities.customActions.map((action) => {
2155
2190
  const actionDisabled = action.disabled === true;
2156
2191
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
@@ -5150,6 +5185,32 @@ function writeAppearanceValue(node, field, value) {
5150
5185
  return { metadata: { ...node.metadata, [field.key]: value } };
5151
5186
  }
5152
5187
 
5188
+ // src/diagram/badges/nodeBadgeGlyphs.ts
5189
+ var PROHIBIT_GLYPH = {
5190
+ id: "prohibit",
5191
+ svgContent: '<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2.4"/><line x1="5.6" y1="18.4" x2="18.4" y2="5.6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>'
5192
+ };
5193
+ var builtInBadgeGlyphs = {
5194
+ [PROHIBIT_GLYPH.id]: PROHIBIT_GLYPH
5195
+ };
5196
+ var customBadgeGlyphs = {};
5197
+ function registerBadgeGlyph(glyph) {
5198
+ customBadgeGlyphs[glyph.id] = glyph;
5199
+ }
5200
+ function registerBadgeGlyphs(glyphs) {
5201
+ if (Array.isArray(glyphs)) {
5202
+ glyphs.forEach((glyph) => registerBadgeGlyph(glyph));
5203
+ } else {
5204
+ Object.values(glyphs).forEach((glyph) => registerBadgeGlyph(glyph));
5205
+ }
5206
+ }
5207
+ function getBadgeGlyph(glyphId) {
5208
+ return customBadgeGlyphs[glyphId] ?? builtInBadgeGlyphs[glyphId];
5209
+ }
5210
+ function getAvailableBadgeGlyphs() {
5211
+ return [...Object.keys(customBadgeGlyphs), ...Object.keys(builtInBadgeGlyphs)];
5212
+ }
5213
+
5153
5214
  // src/components/PIDCanvas/NodeRenderer.tsx
5154
5215
  var import_jsx_runtime17 = require("react/jsx-runtime");
5155
5216
  var HATCH_TILE_SIZE = 7;
@@ -5159,6 +5220,75 @@ var HATCH_STRIPE_OPACITY = 85 / 255;
5159
5220
  function isUnknownNode(node) {
5160
5221
  return node.positionUnknown === true || normalizeDeviceStatus(node.status) === "unknown";
5161
5222
  }
5223
+ var DEFAULT_BADGE_CORNER = "top-right";
5224
+ var DEFAULT_BADGE_SIZE = 14;
5225
+ var DEFAULT_BADGE_COLOR = "#111827";
5226
+ var DEFAULT_BADGE_BACKGROUND = "#d1d5db";
5227
+ var BADGE_GLYPH_BOX = 24;
5228
+ var BADGE_BLINK_KEYFRAME_ID = "pid-node-badge-blink-keyframes";
5229
+ var BADGE_BLINK_KEYFRAME_NAME = "pid-node-badge-blink";
5230
+ function ensureBadgeBlinkKeyframes() {
5231
+ if (typeof document === "undefined") return;
5232
+ if (document.getElementById(BADGE_BLINK_KEYFRAME_ID)) return;
5233
+ const style = document.createElement("style");
5234
+ style.id = BADGE_BLINK_KEYFRAME_ID;
5235
+ style.textContent = `@keyframes ${BADGE_BLINK_KEYFRAME_NAME}{0%{opacity:1}50%{opacity:.15}100%{opacity:1}}`;
5236
+ document.head.appendChild(style);
5237
+ }
5238
+ function badgeCenter(corner, vb, size) {
5239
+ const margin = size / 4;
5240
+ switch (corner) {
5241
+ case "top-left":
5242
+ return { x: -margin, y: -margin };
5243
+ case "bottom-left":
5244
+ return { x: -margin, y: vb.height + margin };
5245
+ case "bottom-right":
5246
+ return { x: vb.width + margin, y: vb.height + margin };
5247
+ case "top-right":
5248
+ default:
5249
+ return { x: vb.width + margin, y: -margin };
5250
+ }
5251
+ }
5252
+ function renderNodeBadge(badge, vb, rotation) {
5253
+ const glyphDef = getBadgeGlyph(badge.glyph);
5254
+ const size = badge.size ?? DEFAULT_BADGE_SIZE;
5255
+ const color = badge.color ?? DEFAULT_BADGE_COLOR;
5256
+ const backgroundColor = badge.backgroundColor ?? DEFAULT_BADGE_BACKGROUND;
5257
+ const corner = badge.corner ?? DEFAULT_BADGE_CORNER;
5258
+ const { x: cx, y: cy } = badgeCenter(corner, vb, size);
5259
+ const radius = size / 2;
5260
+ const glyphScale = size / BADGE_GLYPH_BOX;
5261
+ let blinkStyle;
5262
+ if (badge.blink) {
5263
+ ensureBadgeBlinkKeyframes();
5264
+ blinkStyle = {
5265
+ animation: `${BADGE_BLINK_KEYFRAME_NAME} ${badge.blinkPeriodMs}ms ease-in-out infinite`
5266
+ };
5267
+ }
5268
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5269
+ "g",
5270
+ {
5271
+ "data-node-badge": badge.glyph,
5272
+ "data-badge-corner": corner,
5273
+ "data-badge-blink": badge.blink ? "true" : "false",
5274
+ "aria-label": badge.label,
5275
+ role: badge.label ? "img" : void 0,
5276
+ transform: rotation ? `translate(${cx}, ${cy}) rotate(${-rotation})` : `translate(${cx}, ${cy})`,
5277
+ style: blinkStyle,
5278
+ children: [
5279
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("circle", { r: radius, fill: backgroundColor }),
5280
+ glyphDef && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5281
+ "g",
5282
+ {
5283
+ color,
5284
+ transform: `scale(${glyphScale}) translate(${-BADGE_GLYPH_BOX / 2}, ${-BADGE_GLYPH_BOX / 2})`,
5285
+ dangerouslySetInnerHTML: { __html: glyphDef.svgContent }
5286
+ }
5287
+ )
5288
+ ]
5289
+ }
5290
+ );
5291
+ }
5162
5292
  function NodeRendererImpl({
5163
5293
  nodes: nodes2,
5164
5294
  selectedNodeIds,
@@ -5365,7 +5495,8 @@ function NodeRendererImpl({
5365
5495
  transform: node.transform.rotation ? `rotate(${-node.transform.rotation}, ${labelX}, ${labelY})` : void 0,
5366
5496
  children: node.label || node.id
5367
5497
  }
5368
- )
5498
+ ),
5499
+ node.badge && renderNodeBadge(node.badge, vb, node.transform.rotation)
5369
5500
  ]
5370
5501
  },
5371
5502
  node.id
@@ -10461,7 +10592,9 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
10461
10592
  generateRoutePoints,
10462
10593
  getAppearanceDefaults,
10463
10594
  getAppearanceSchema,
10595
+ getAvailableBadgeGlyphs,
10464
10596
  getAvailableSymbols,
10597
+ getBadgeGlyph,
10465
10598
  getPortWorldPosition,
10466
10599
  getSymbolDefinition,
10467
10600
  importDiagram,
@@ -10470,6 +10603,8 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
10470
10603
  normalizeDeviceStatus,
10471
10604
  overlayStyles,
10472
10605
  readAppearanceValue,
10606
+ registerBadgeGlyph,
10607
+ registerBadgeGlyphs,
10473
10608
  registerSymbol,
10474
10609
  registerSymbols,
10475
10610
  resolvePresetColor,