@procaaso/alphinity-ui-components 1.13.0 → 1.15.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
@@ -400,6 +400,13 @@ var ISA_UNKNOWN_DARK = {
400
400
  bg: "#23263a",
401
401
  border: "#6f74a3"
402
402
  };
403
+ var FORCED_MARK_COLOR = {
404
+ fill: "#ede9fe",
405
+ stroke: "#5b21b6",
406
+ bg: "#ede9fe",
407
+ text: "#5b21b6",
408
+ border: "#7c3aed"
409
+ };
403
410
  var DEFAULT_STATUS_COLORS = {
404
411
  normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
405
412
  warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
@@ -408,14 +415,8 @@ var DEFAULT_STATUS_COLORS = {
408
415
  off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
409
416
  starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
410
417
  stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
411
- unknown: { fill: ISA_UNKNOWN.bg, stroke: ISA_UNKNOWN.border, bg: ISA_UNKNOWN.bg, text: ISA_UNKNOWN.fg, border: ISA_UNKNOWN.border }
412
- };
413
- var FORCED_MARK_COLOR = {
414
- fill: "#ede9fe",
415
- stroke: "#5b21b6",
416
- bg: "#ede9fe",
417
- text: "#5b21b6",
418
- 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
419
420
  };
420
421
  var STATUS_ALIASES = {
421
422
  running: "normal",
@@ -1261,6 +1262,10 @@ var NodeControlsPanel = ({
1261
1262
  // src/components/DeviceControlPanel/DeviceControlPanel.tsx
1262
1263
  var import_react6 = __toESM(require("react"), 1);
1263
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)";
1264
1269
  function DeviceControlPanelImpl({
1265
1270
  binding,
1266
1271
  onClose,
@@ -1341,6 +1346,36 @@ function DeviceControlPanelImpl({
1341
1346
  const statusEntry = resolveStatusColor(binding.state.status, binding.theme?.statusColors);
1342
1347
  const statusColor = statusEntry.bg;
1343
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
+ });
1344
1379
  const handleModeChange = (mode) => {
1345
1380
  onModeChange?.(mode);
1346
1381
  };
@@ -1526,6 +1561,7 @@ function DeviceControlPanelImpl({
1526
1561
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1527
1562
  "div",
1528
1563
  {
1564
+ "data-testid": "device-status-dot",
1529
1565
  style: {
1530
1566
  width: "10px",
1531
1567
  height: "10px",
@@ -2111,50 +2147,45 @@ function DeviceControlPanelImpl({
2111
2147
  param.id
2112
2148
  ))
2113
2149
  ] }),
2114
- 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: [
2115
- activeCapabilities?.canStart && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2116
- "button",
2117
- {
2118
- onClick: onStart,
2119
- style: {
2120
- flex: 1,
2121
- height: touchSize,
2122
- backgroundColor: "#10b981",
2123
- color: "#ffffff",
2124
- border: "none",
2125
- borderRadius: "6px",
2126
- fontSize: "12px",
2127
- fontWeight: 700,
2128
- fontFamily: "Arial, sans-serif",
2129
- letterSpacing: "0.5px",
2130
- cursor: "pointer",
2131
- opacity: 1
2132
- },
2133
- children: startPending ? "STARTING\u2026" : "START"
2134
- }
2135
- ),
2136
- activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2137
- "button",
2138
- {
2139
- onClick: onStop,
2140
- style: {
2141
- flex: 1,
2142
- height: touchSize,
2143
- backgroundColor: "#ef4444",
2144
- color: "#ffffff",
2145
- border: "none",
2146
- borderRadius: "6px",
2147
- fontSize: "12px",
2148
- fontWeight: 700,
2149
- fontFamily: "Arial, sans-serif",
2150
- letterSpacing: "0.5px",
2151
- cursor: "pointer",
2152
- opacity: 1
2153
- },
2154
- children: stopPending ? "STOPPING\u2026" : "STOP"
2155
- }
2156
- )
2157
- ] }),
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
+ ),
2158
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) => {
2159
2190
  const actionDisabled = action.disabled === true;
2160
2191
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(