@procaaso/alphinity-ui-components 1.1.3 → 1.3.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}` : "";
@@ -1039,6 +1040,8 @@ function DeviceControlPanel({
1039
1040
  onParameterChange,
1040
1041
  onStart,
1041
1042
  onStop,
1043
+ startPending = false,
1044
+ stopPending = false,
1042
1045
  onCustomAction,
1043
1046
  onOpenConfig,
1044
1047
  showConfigButton = true,
@@ -1785,11 +1788,10 @@ function DeviceControlPanel({
1785
1788
  "button",
1786
1789
  {
1787
1790
  onClick: onStart,
1788
- disabled: binding.state.isRunning,
1789
1791
  style: {
1790
1792
  flex: 1,
1791
1793
  height: touchSize,
1792
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1794
+ backgroundColor: "#10b981",
1793
1795
  color: "#ffffff",
1794
1796
  border: "none",
1795
1797
  borderRadius: "6px",
@@ -1797,21 +1799,20 @@ function DeviceControlPanel({
1797
1799
  fontWeight: 700,
1798
1800
  fontFamily: "Arial, sans-serif",
1799
1801
  letterSpacing: "0.5px",
1800
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1801
- opacity: binding.state.isRunning ? 0.5 : 1
1802
+ cursor: "pointer",
1803
+ opacity: 1
1802
1804
  },
1803
- children: "START"
1805
+ children: startPending ? "STARTING\u2026" : "START"
1804
1806
  }
1805
1807
  ),
1806
1808
  activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1807
1809
  "button",
1808
1810
  {
1809
1811
  onClick: onStop,
1810
- disabled: !binding.state.isRunning,
1811
1812
  style: {
1812
1813
  flex: 1,
1813
1814
  height: touchSize,
1814
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1815
+ backgroundColor: "#ef4444",
1815
1816
  color: "#ffffff",
1816
1817
  border: "none",
1817
1818
  borderRadius: "6px",
@@ -1819,10 +1820,10 @@ function DeviceControlPanel({
1819
1820
  fontWeight: 700,
1820
1821
  fontFamily: "Arial, sans-serif",
1821
1822
  letterSpacing: "0.5px",
1822
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1823
- opacity: !binding.state.isRunning ? 0.5 : 1
1823
+ cursor: "pointer",
1824
+ opacity: 1
1824
1825
  },
1825
- children: "STOP"
1826
+ children: stopPending ? "STOPPING\u2026" : "STOP"
1826
1827
  }
1827
1828
  )
1828
1829
  ] }),
@@ -4457,12 +4458,13 @@ function DataOverlay({
4457
4458
  scale = 1,
4458
4459
  statusColorMap
4459
4460
  }) {
4460
- const { value, display, history } = telemetry;
4461
+ const { value, display, history, scaling } = telemetry;
4461
4462
  const showValue = display?.showValue ?? true;
4462
4463
  const showStatus = display?.showStatus ?? true;
4463
4464
  const showSparkline = display?.showSparkline ?? false;
4464
4465
  const label = display?.label;
4465
- const precision = display?.precision;
4466
+ const precision = scaling?.decimals ?? display?.precision;
4467
+ const unit = scaling?.units ?? value.unit;
4466
4468
  const sparklineColor = display?.sparklineColor;
4467
4469
  const textColor = display?.textColor;
4468
4470
  const backgroundColor = display?.backgroundColor;
@@ -4486,7 +4488,7 @@ function DataOverlay({
4486
4488
  const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
4487
4489
  const finalTextColor = textColor || colors.text;
4488
4490
  const formattedValue = formatValue(value.value);
4489
- const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
4491
+ const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
4490
4492
  const sparklinePath = (() => {
4491
4493
  if (!showSparkline || !history || history.length < 2) return "";
4492
4494
  const width = 70 * scale;
@@ -4494,8 +4496,9 @@ function DataOverlay({
4494
4496
  const padding = 5 * scale;
4495
4497
  const effectiveWidth = width - padding * 2;
4496
4498
  const effectiveHeight = height - padding * 2;
4497
- const min = Math.min(...history);
4498
- const max = Math.max(...history);
4499
+ const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
4500
+ const min = hasEuRange ? scaling.euLow : Math.min(...history);
4501
+ const max = hasEuRange ? scaling.euHigh : Math.max(...history);
4499
4502
  const range = max - min || 1;
4500
4503
  const points = history.map((val, i) => {
4501
4504
  const xPos = i / (history.length - 1) * effectiveWidth + padding;
@@ -7657,7 +7660,11 @@ function createControlBinding(nodeId, deviceConfig, actions) {
7657
7660
 
7658
7661
  // src/hooks/useDeviceControls.ts
7659
7662
  var import_react18 = require("react");
7660
- function useDeviceControls(initialBindings) {
7663
+ function commandKey(nodeId, kind, suffix) {
7664
+ return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
7665
+ }
7666
+ function useDeviceControls(initialBindings, options) {
7667
+ const { commandTimeoutMs } = options ?? {};
7661
7668
  const [controls, setControls] = (0, import_react18.useState)(() => {
7662
7669
  const map = /* @__PURE__ */ new Map();
7663
7670
  if (initialBindings) {
@@ -7667,6 +7674,11 @@ function useDeviceControls(initialBindings) {
7667
7674
  }
7668
7675
  return map;
7669
7676
  });
7677
+ const pendingRef = (0, import_react18.useRef)(/* @__PURE__ */ new Map());
7678
+ const [pendingCommands, setPendingCommands] = (0, import_react18.useState)(() => /* @__PURE__ */ new Set());
7679
+ const syncPending = (0, import_react18.useCallback)(() => {
7680
+ setPendingCommands(new Set(pendingRef.current.keys()));
7681
+ }, []);
7670
7682
  const updateControl = (0, import_react18.useCallback)((nodeId, updates) => {
7671
7683
  setControls((prev) => {
7672
7684
  const newMap = new Map(prev);
@@ -7756,121 +7768,112 @@ function useDeviceControls(initialBindings) {
7756
7768
  },
7757
7769
  [controls]
7758
7770
  );
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
- });
7771
+ const isCommandPending = (0, import_react18.useCallback)(
7772
+ (nodeId, kind) => {
7773
+ if (kind) {
7774
+ const prefix2 = `${nodeId}:${kind}`;
7775
+ for (const key of pendingCommands) {
7776
+ if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
7777
+ }
7778
+ return false;
7779
+ }
7780
+ const prefix = `${nodeId}:`;
7781
+ for (const key of pendingCommands) {
7782
+ if (key.startsWith(prefix)) return true;
7778
7783
  }
7784
+ return false;
7779
7785
  },
7780
- [controls, updateDeviceState]
7786
+ [pendingCommands]
7781
7787
  );
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
- }
7788
+ const markPending = (0, import_react18.useCallback)(
7789
+ (key, delta) => {
7790
+ const next = (pendingRef.current.get(key) ?? 0) + delta;
7791
+ if (next > 0) pendingRef.current.set(key, next);
7792
+ else pendingRef.current.delete(key);
7793
+ syncPending();
7801
7794
  },
7802
- [controls, updateParameter, updateDeviceState]
7795
+ [syncPending]
7803
7796
  );
7804
- const sendStartCommand = (0, import_react18.useCallback)(
7805
- async (nodeId) => {
7797
+ const runCommand = (0, import_react18.useCallback)(
7798
+ async (nodeId, key, invoke, messages) => {
7806
7799
  const binding = controls.get(nodeId);
7807
7800
  if (!binding) return;
7808
- updateDeviceState(nodeId, { isRunning: true, status: "starting" });
7801
+ markPending(key, 1);
7802
+ let timer;
7809
7803
  try {
7810
- await binding.actions.onStart?.();
7804
+ const action = Promise.resolve(invoke(binding));
7805
+ if (commandTimeoutMs && commandTimeoutMs > 0) {
7806
+ await Promise.race([
7807
+ action,
7808
+ new Promise((_, reject) => {
7809
+ timer = setTimeout(
7810
+ () => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
7811
+ commandTimeoutMs
7812
+ );
7813
+ })
7814
+ ]);
7815
+ } else {
7816
+ await action;
7817
+ }
7811
7818
  updateDeviceState(nodeId, {
7812
- status: "normal",
7813
7819
  lastCommandTime: Date.now(),
7814
- lastCommandResult: { success: true, message: "Device started" }
7820
+ lastCommandResult: { success: true, message: messages.success }
7815
7821
  });
7816
7822
  } catch (error) {
7817
7823
  updateDeviceState(nodeId, {
7818
- isRunning: false,
7819
- status: "fault",
7824
+ lastCommandTime: Date.now(),
7820
7825
  lastCommandResult: {
7821
7826
  success: false,
7822
- message: error instanceof Error ? error.message : "Start failed"
7827
+ message: error instanceof Error ? error.message : messages.failure
7823
7828
  }
7824
7829
  });
7830
+ } finally {
7831
+ if (timer) clearTimeout(timer);
7832
+ markPending(key, -1);
7825
7833
  }
7826
7834
  },
7827
- [controls, updateDeviceState]
7835
+ [controls, updateDeviceState, markPending, commandTimeoutMs]
7836
+ );
7837
+ const sendModeChange = (0, import_react18.useCallback)(
7838
+ (nodeId, mode) => runCommand(
7839
+ nodeId,
7840
+ commandKey(nodeId, "mode"),
7841
+ (binding) => binding.actions.onModeChange?.(mode),
7842
+ { success: `Mode ${mode} command sent`, failure: "Mode change failed" }
7843
+ ),
7844
+ [runCommand]
7845
+ );
7846
+ const sendParameterChange = (0, import_react18.useCallback)(
7847
+ (nodeId, parameterId, value) => runCommand(
7848
+ nodeId,
7849
+ commandKey(nodeId, "parameter", parameterId),
7850
+ (binding) => binding.actions.onParameterChange?.(parameterId, value),
7851
+ { success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
7852
+ ),
7853
+ [runCommand]
7854
+ );
7855
+ const sendStartCommand = (0, import_react18.useCallback)(
7856
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
7857
+ success: "Start command sent",
7858
+ failure: "Start command failed"
7859
+ }),
7860
+ [runCommand]
7828
7861
  );
7829
7862
  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]
7863
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
7864
+ success: "Stop command sent",
7865
+ failure: "Stop command failed"
7866
+ }),
7867
+ [runCommand]
7853
7868
  );
7854
7869
  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]
7870
+ (nodeId, actionId) => runCommand(
7871
+ nodeId,
7872
+ commandKey(nodeId, "action", actionId),
7873
+ (binding) => binding.actions.onCustomAction?.(actionId),
7874
+ { success: `Action ${actionId} command sent`, failure: "Action failed" }
7875
+ ),
7876
+ [runCommand]
7874
7877
  );
7875
7878
  return {
7876
7879
  controls,
@@ -7881,6 +7884,10 @@ function useDeviceControls(initialBindings) {
7881
7884
  setControlBinding,
7882
7885
  removeControlBinding,
7883
7886
  getControlBinding,
7887
+ // In-flight command visibility (interaction state, not device state).
7888
+ // `isCommandPending` is the stable query surface; the raw key set is
7889
+ // intentionally not exported (its key format is an implementation detail).
7890
+ isCommandPending,
7884
7891
  // Command helpers
7885
7892
  sendModeChange,
7886
7893
  sendParameterChange,