@procaaso/alphinity-ui-components 1.1.3 → 1.4.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
@@ -72,6 +72,17 @@ function HMIPanel() {
72
72
  - `npm test` - Run tests with Vitest
73
73
  - `npm run lint` - Lint code with ESLint
74
74
  - `npm run typecheck` - Type check with TypeScript
75
+ - `npm run dev` - Run the demo playground on http://localhost:3010
76
+
77
+ ### Demo views
78
+
79
+ The demo (`npm run dev`) mounts one of three views, selected by a `?demo=` query param:
80
+
81
+ - `http://localhost:3010/` *(default)* - **Runtime**: full P&ID canvas with live telemetry, simulation, and device control/config panels
82
+ - `http://localhost:3010/?demo=single-node` - **Single-node HMI mockup** over the full plant SVG layout
83
+ - `http://localhost:3010/?demo=gallery` - **Component gallery** of individual widgets
84
+
85
+ > 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.
75
86
 
76
87
  ### Components
77
88
 
package/dist/index.cjs CHANGED
@@ -580,11 +580,12 @@ var ControlPanel = ({
580
580
  secondSetpointValue,
581
581
  onSecondSetpointChange,
582
582
  modeConfigs = {},
583
- defaultUnit = "Units",
583
+ defaultUnit,
584
584
  defaultLabel = "Setpoint",
585
- defaultMin = 0,
586
- defaultMax = 100,
587
- defaultPrecision = 2,
585
+ defaultMin,
586
+ defaultMax,
587
+ defaultPrecision,
588
+ scaling,
588
589
  status = "normal",
589
590
  collapsed = false,
590
591
  onCollapseChange,
@@ -606,11 +607,11 @@ var ControlPanel = ({
606
607
  onCollapseChange?.(!newExpanded);
607
608
  };
608
609
  const currentModeConfig = modeConfigs[selectedMode] || {};
609
- const currentUnit = currentModeConfig.unit || defaultUnit;
610
+ const currentUnit = currentModeConfig.unit || defaultUnit || scaling?.units || "Units";
610
611
  const currentLabel = currentModeConfig.label || defaultLabel;
611
- const currentMin = currentModeConfig.min ?? defaultMin;
612
- const currentMax = currentModeConfig.max ?? defaultMax;
613
- const currentPrecision = currentModeConfig.precision ?? defaultPrecision;
612
+ const currentMin = currentModeConfig.min ?? defaultMin ?? scaling?.euLow ?? 0;
613
+ const currentMax = currentModeConfig.max ?? defaultMax ?? scaling?.euHigh ?? 100;
614
+ const currentPrecision = currentModeConfig.precision ?? defaultPrecision ?? scaling?.decimals ?? 2;
614
615
  const hasSecondSetpoint = currentModeConfig.hasSecondSetpoint || false;
615
616
  const baseClass = "ui-control-panel";
616
617
  const statusClass = status ? `ui-control-panel--${status}` : "";
@@ -1037,8 +1038,11 @@ function DeviceControlPanel({
1037
1038
  toastDuration = 3e3,
1038
1039
  onModeChange,
1039
1040
  onParameterChange,
1041
+ onParameterActivate,
1040
1042
  onStart,
1041
1043
  onStop,
1044
+ startPending = false,
1045
+ stopPending = false,
1042
1046
  onCustomAction,
1043
1047
  onOpenConfig,
1044
1048
  showConfigButton = true,
@@ -1747,6 +1751,36 @@ function DeviceControlPanel({
1747
1751
  },
1748
1752
  children: param.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
1749
1753
  }
1754
+ ) : onParameterActivate && !param.readOnly ? (
1755
+ // Tappable value: defer editing to the consumer's editor (e.g. a
1756
+ // numpad dialog). Commit happens via onParameterChange.
1757
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1758
+ "button",
1759
+ {
1760
+ type: "button",
1761
+ onClick: () => onParameterActivate(param.id, localParameterValues[param.id] ?? param.value),
1762
+ style: {
1763
+ width: "100%",
1764
+ height: touchSize,
1765
+ display: "flex",
1766
+ alignItems: "center",
1767
+ backgroundColor: "#ffffff",
1768
+ color: textColor,
1769
+ border: `1px solid ${borderColor}`,
1770
+ borderRadius: "6px",
1771
+ padding: "0 12px",
1772
+ fontSize: `${fontSize.input}px`,
1773
+ fontFamily: "Arial, sans-serif",
1774
+ fontWeight: 600,
1775
+ textAlign: "left",
1776
+ cursor: "pointer"
1777
+ },
1778
+ children: [
1779
+ localParameterValues[param.id] ?? param.value,
1780
+ param.unit ? ` ${param.unit}` : ""
1781
+ ]
1782
+ }
1783
+ )
1750
1784
  ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1751
1785
  "input",
1752
1786
  {
@@ -1785,11 +1819,10 @@ function DeviceControlPanel({
1785
1819
  "button",
1786
1820
  {
1787
1821
  onClick: onStart,
1788
- disabled: binding.state.isRunning,
1789
1822
  style: {
1790
1823
  flex: 1,
1791
1824
  height: touchSize,
1792
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1825
+ backgroundColor: "#10b981",
1793
1826
  color: "#ffffff",
1794
1827
  border: "none",
1795
1828
  borderRadius: "6px",
@@ -1797,21 +1830,20 @@ function DeviceControlPanel({
1797
1830
  fontWeight: 700,
1798
1831
  fontFamily: "Arial, sans-serif",
1799
1832
  letterSpacing: "0.5px",
1800
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1801
- opacity: binding.state.isRunning ? 0.5 : 1
1833
+ cursor: "pointer",
1834
+ opacity: 1
1802
1835
  },
1803
- children: "START"
1836
+ children: startPending ? "STARTING\u2026" : "START"
1804
1837
  }
1805
1838
  ),
1806
1839
  activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1807
1840
  "button",
1808
1841
  {
1809
1842
  onClick: onStop,
1810
- disabled: !binding.state.isRunning,
1811
1843
  style: {
1812
1844
  flex: 1,
1813
1845
  height: touchSize,
1814
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1846
+ backgroundColor: "#ef4444",
1815
1847
  color: "#ffffff",
1816
1848
  border: "none",
1817
1849
  borderRadius: "6px",
@@ -1819,10 +1851,10 @@ function DeviceControlPanel({
1819
1851
  fontWeight: 700,
1820
1852
  fontFamily: "Arial, sans-serif",
1821
1853
  letterSpacing: "0.5px",
1822
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1823
- opacity: !binding.state.isRunning ? 0.5 : 1
1854
+ cursor: "pointer",
1855
+ opacity: 1
1824
1856
  },
1825
- children: "STOP"
1857
+ children: stopPending ? "STOPPING\u2026" : "STOP"
1826
1858
  }
1827
1859
  )
1828
1860
  ] }),
@@ -4457,12 +4489,13 @@ function DataOverlay({
4457
4489
  scale = 1,
4458
4490
  statusColorMap
4459
4491
  }) {
4460
- const { value, display, history } = telemetry;
4492
+ const { value, display, history, scaling } = telemetry;
4461
4493
  const showValue = display?.showValue ?? true;
4462
4494
  const showStatus = display?.showStatus ?? true;
4463
4495
  const showSparkline = display?.showSparkline ?? false;
4464
4496
  const label = display?.label;
4465
- const precision = display?.precision;
4497
+ const precision = scaling?.decimals ?? display?.precision;
4498
+ const unit = scaling?.units ?? value.unit;
4466
4499
  const sparklineColor = display?.sparklineColor;
4467
4500
  const textColor = display?.textColor;
4468
4501
  const backgroundColor = display?.backgroundColor;
@@ -4486,7 +4519,7 @@ function DataOverlay({
4486
4519
  const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
4487
4520
  const finalTextColor = textColor || colors.text;
4488
4521
  const formattedValue = formatValue(value.value);
4489
- const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
4522
+ const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
4490
4523
  const sparklinePath = (() => {
4491
4524
  if (!showSparkline || !history || history.length < 2) return "";
4492
4525
  const width = 70 * scale;
@@ -4494,8 +4527,9 @@ function DataOverlay({
4494
4527
  const padding = 5 * scale;
4495
4528
  const effectiveWidth = width - padding * 2;
4496
4529
  const effectiveHeight = height - padding * 2;
4497
- const min = Math.min(...history);
4498
- const max = Math.max(...history);
4530
+ const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
4531
+ const min = hasEuRange ? scaling.euLow : Math.min(...history);
4532
+ const max = hasEuRange ? scaling.euHigh : Math.max(...history);
4499
4533
  const range = max - min || 1;
4500
4534
  const points = history.map((val, i) => {
4501
4535
  const xPos = i / (history.length - 1) * effectiveWidth + padding;
@@ -7657,7 +7691,11 @@ function createControlBinding(nodeId, deviceConfig, actions) {
7657
7691
 
7658
7692
  // src/hooks/useDeviceControls.ts
7659
7693
  var import_react18 = require("react");
7660
- function useDeviceControls(initialBindings) {
7694
+ function commandKey(nodeId, kind, suffix) {
7695
+ return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
7696
+ }
7697
+ function useDeviceControls(initialBindings, options) {
7698
+ const { commandTimeoutMs } = options ?? {};
7661
7699
  const [controls, setControls] = (0, import_react18.useState)(() => {
7662
7700
  const map = /* @__PURE__ */ new Map();
7663
7701
  if (initialBindings) {
@@ -7667,6 +7705,11 @@ function useDeviceControls(initialBindings) {
7667
7705
  }
7668
7706
  return map;
7669
7707
  });
7708
+ const pendingRef = (0, import_react18.useRef)(/* @__PURE__ */ new Map());
7709
+ const [pendingCommands, setPendingCommands] = (0, import_react18.useState)(() => /* @__PURE__ */ new Set());
7710
+ const syncPending = (0, import_react18.useCallback)(() => {
7711
+ setPendingCommands(new Set(pendingRef.current.keys()));
7712
+ }, []);
7670
7713
  const updateControl = (0, import_react18.useCallback)((nodeId, updates) => {
7671
7714
  setControls((prev) => {
7672
7715
  const newMap = new Map(prev);
@@ -7756,121 +7799,112 @@ function useDeviceControls(initialBindings) {
7756
7799
  },
7757
7800
  [controls]
7758
7801
  );
7759
- const sendModeChange = (0, import_react18.useCallback)(
7760
- async (nodeId, mode) => {
7761
- const binding = controls.get(nodeId);
7762
- if (!binding) return;
7763
- updateDeviceState(nodeId, { currentMode: mode });
7764
- try {
7765
- await binding.actions.onModeChange?.(mode);
7766
- updateDeviceState(nodeId, {
7767
- lastCommandTime: Date.now(),
7768
- lastCommandResult: { success: true, message: `Mode changed to ${mode}` }
7769
- });
7770
- } catch (error) {
7771
- updateDeviceState(nodeId, {
7772
- currentMode: binding.state.currentMode,
7773
- lastCommandResult: {
7774
- success: false,
7775
- message: error instanceof Error ? error.message : "Mode change failed"
7776
- }
7777
- });
7802
+ const isCommandPending = (0, import_react18.useCallback)(
7803
+ (nodeId, kind) => {
7804
+ if (kind) {
7805
+ const prefix2 = `${nodeId}:${kind}`;
7806
+ for (const key of pendingCommands) {
7807
+ if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
7808
+ }
7809
+ return false;
7778
7810
  }
7811
+ const prefix = `${nodeId}:`;
7812
+ for (const key of pendingCommands) {
7813
+ if (key.startsWith(prefix)) return true;
7814
+ }
7815
+ return false;
7779
7816
  },
7780
- [controls, updateDeviceState]
7817
+ [pendingCommands]
7781
7818
  );
7782
- const sendParameterChange = (0, import_react18.useCallback)(
7783
- async (nodeId, parameterId, value) => {
7784
- const binding = controls.get(nodeId);
7785
- if (!binding) return;
7786
- updateParameter(nodeId, parameterId, value);
7787
- try {
7788
- await binding.actions.onParameterChange?.(parameterId, value);
7789
- updateDeviceState(nodeId, {
7790
- lastCommandTime: Date.now(),
7791
- lastCommandResult: { success: true, message: `Parameter ${parameterId} updated` }
7792
- });
7793
- } catch (error) {
7794
- updateDeviceState(nodeId, {
7795
- lastCommandResult: {
7796
- success: false,
7797
- message: error instanceof Error ? error.message : "Parameter change failed"
7798
- }
7799
- });
7800
- }
7819
+ const markPending = (0, import_react18.useCallback)(
7820
+ (key, delta) => {
7821
+ const next = (pendingRef.current.get(key) ?? 0) + delta;
7822
+ if (next > 0) pendingRef.current.set(key, next);
7823
+ else pendingRef.current.delete(key);
7824
+ syncPending();
7801
7825
  },
7802
- [controls, updateParameter, updateDeviceState]
7826
+ [syncPending]
7803
7827
  );
7804
- const sendStartCommand = (0, import_react18.useCallback)(
7805
- async (nodeId) => {
7828
+ const runCommand = (0, import_react18.useCallback)(
7829
+ async (nodeId, key, invoke, messages) => {
7806
7830
  const binding = controls.get(nodeId);
7807
7831
  if (!binding) return;
7808
- updateDeviceState(nodeId, { isRunning: true, status: "starting" });
7832
+ markPending(key, 1);
7833
+ let timer;
7809
7834
  try {
7810
- await binding.actions.onStart?.();
7835
+ const action = Promise.resolve(invoke(binding));
7836
+ if (commandTimeoutMs && commandTimeoutMs > 0) {
7837
+ await Promise.race([
7838
+ action,
7839
+ new Promise((_, reject) => {
7840
+ timer = setTimeout(
7841
+ () => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
7842
+ commandTimeoutMs
7843
+ );
7844
+ })
7845
+ ]);
7846
+ } else {
7847
+ await action;
7848
+ }
7811
7849
  updateDeviceState(nodeId, {
7812
- status: "normal",
7813
7850
  lastCommandTime: Date.now(),
7814
- lastCommandResult: { success: true, message: "Device started" }
7851
+ lastCommandResult: { success: true, message: messages.success }
7815
7852
  });
7816
7853
  } catch (error) {
7817
7854
  updateDeviceState(nodeId, {
7818
- isRunning: false,
7819
- status: "fault",
7855
+ lastCommandTime: Date.now(),
7820
7856
  lastCommandResult: {
7821
7857
  success: false,
7822
- message: error instanceof Error ? error.message : "Start failed"
7858
+ message: error instanceof Error ? error.message : messages.failure
7823
7859
  }
7824
7860
  });
7861
+ } finally {
7862
+ if (timer) clearTimeout(timer);
7863
+ markPending(key, -1);
7825
7864
  }
7826
7865
  },
7827
- [controls, updateDeviceState]
7866
+ [controls, updateDeviceState, markPending, commandTimeoutMs]
7867
+ );
7868
+ const sendModeChange = (0, import_react18.useCallback)(
7869
+ (nodeId, mode) => runCommand(
7870
+ nodeId,
7871
+ commandKey(nodeId, "mode"),
7872
+ (binding) => binding.actions.onModeChange?.(mode),
7873
+ { success: `Mode ${mode} command sent`, failure: "Mode change failed" }
7874
+ ),
7875
+ [runCommand]
7876
+ );
7877
+ const sendParameterChange = (0, import_react18.useCallback)(
7878
+ (nodeId, parameterId, value) => runCommand(
7879
+ nodeId,
7880
+ commandKey(nodeId, "parameter", parameterId),
7881
+ (binding) => binding.actions.onParameterChange?.(parameterId, value),
7882
+ { success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
7883
+ ),
7884
+ [runCommand]
7885
+ );
7886
+ const sendStartCommand = (0, import_react18.useCallback)(
7887
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
7888
+ success: "Start command sent",
7889
+ failure: "Start command failed"
7890
+ }),
7891
+ [runCommand]
7828
7892
  );
7829
7893
  const sendStopCommand = (0, import_react18.useCallback)(
7830
- async (nodeId) => {
7831
- const binding = controls.get(nodeId);
7832
- if (!binding) return;
7833
- updateDeviceState(nodeId, { isRunning: false, status: "stopping" });
7834
- try {
7835
- await binding.actions.onStop?.();
7836
- updateDeviceState(nodeId, {
7837
- status: "off",
7838
- lastCommandTime: Date.now(),
7839
- lastCommandResult: { success: true, message: "Device stopped" }
7840
- });
7841
- } catch (error) {
7842
- updateDeviceState(nodeId, {
7843
- isRunning: true,
7844
- status: "fault",
7845
- lastCommandResult: {
7846
- success: false,
7847
- message: error instanceof Error ? error.message : "Stop failed"
7848
- }
7849
- });
7850
- }
7851
- },
7852
- [controls, updateDeviceState]
7894
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
7895
+ success: "Stop command sent",
7896
+ failure: "Stop command failed"
7897
+ }),
7898
+ [runCommand]
7853
7899
  );
7854
7900
  const sendCustomAction = (0, import_react18.useCallback)(
7855
- async (nodeId, actionId) => {
7856
- const binding = controls.get(nodeId);
7857
- if (!binding) return;
7858
- try {
7859
- await binding.actions.onCustomAction?.(actionId);
7860
- updateDeviceState(nodeId, {
7861
- lastCommandTime: Date.now(),
7862
- lastCommandResult: { success: true, message: `Action ${actionId} executed` }
7863
- });
7864
- } catch (error) {
7865
- updateDeviceState(nodeId, {
7866
- lastCommandResult: {
7867
- success: false,
7868
- message: error instanceof Error ? error.message : "Action failed"
7869
- }
7870
- });
7871
- }
7872
- },
7873
- [controls, updateDeviceState]
7901
+ (nodeId, actionId) => runCommand(
7902
+ nodeId,
7903
+ commandKey(nodeId, "action", actionId),
7904
+ (binding) => binding.actions.onCustomAction?.(actionId),
7905
+ { success: `Action ${actionId} command sent`, failure: "Action failed" }
7906
+ ),
7907
+ [runCommand]
7874
7908
  );
7875
7909
  return {
7876
7910
  controls,
@@ -7881,6 +7915,10 @@ function useDeviceControls(initialBindings) {
7881
7915
  setControlBinding,
7882
7916
  removeControlBinding,
7883
7917
  getControlBinding,
7918
+ // In-flight command visibility (interaction state, not device state).
7919
+ // `isCommandPending` is the stable query surface; the raw key set is
7920
+ // intentionally not exported (its key format is an implementation detail).
7921
+ isCommandPending,
7884
7922
  // Command helpers
7885
7923
  sendModeChange,
7886
7924
  sendParameterChange,