@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/IMPLEMENTATION_GUIDE.md +1351 -1351
- package/README.md +98 -98
- package/dist/index.cjs +188 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -20
- package/dist/index.d.ts +140 -20
- package/dist/index.js +184 -53
- package/dist/index.js.map +1 -1
- package/package.json +85 -85
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
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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,
|