@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/dist/index.js CHANGED
@@ -477,11 +477,12 @@ var ControlPanel = ({
477
477
  secondSetpointValue,
478
478
  onSecondSetpointChange,
479
479
  modeConfigs = {},
480
- defaultUnit = "Units",
480
+ defaultUnit,
481
481
  defaultLabel = "Setpoint",
482
- defaultMin = 0,
483
- defaultMax = 100,
484
- defaultPrecision = 2,
482
+ defaultMin,
483
+ defaultMax,
484
+ defaultPrecision,
485
+ scaling,
485
486
  status = "normal",
486
487
  collapsed = false,
487
488
  onCollapseChange,
@@ -503,11 +504,11 @@ var ControlPanel = ({
503
504
  onCollapseChange?.(!newExpanded);
504
505
  };
505
506
  const currentModeConfig = modeConfigs[selectedMode] || {};
506
- const currentUnit = currentModeConfig.unit || defaultUnit;
507
+ const currentUnit = currentModeConfig.unit || defaultUnit || scaling?.units || "Units";
507
508
  const currentLabel = currentModeConfig.label || defaultLabel;
508
- const currentMin = currentModeConfig.min ?? defaultMin;
509
- const currentMax = currentModeConfig.max ?? defaultMax;
510
- const currentPrecision = currentModeConfig.precision ?? defaultPrecision;
509
+ const currentMin = currentModeConfig.min ?? defaultMin ?? scaling?.euLow ?? 0;
510
+ const currentMax = currentModeConfig.max ?? defaultMax ?? scaling?.euHigh ?? 100;
511
+ const currentPrecision = currentModeConfig.precision ?? defaultPrecision ?? scaling?.decimals ?? 2;
511
512
  const hasSecondSetpoint = currentModeConfig.hasSecondSetpoint || false;
512
513
  const baseClass = "ui-control-panel";
513
514
  const statusClass = status ? `ui-control-panel--${status}` : "";
@@ -934,8 +935,11 @@ function DeviceControlPanel({
934
935
  toastDuration = 3e3,
935
936
  onModeChange,
936
937
  onParameterChange,
938
+ onParameterActivate,
937
939
  onStart,
938
940
  onStop,
941
+ startPending = false,
942
+ stopPending = false,
939
943
  onCustomAction,
940
944
  onOpenConfig,
941
945
  showConfigButton = true,
@@ -1644,6 +1648,36 @@ function DeviceControlPanel({
1644
1648
  },
1645
1649
  children: param.options?.map((opt) => /* @__PURE__ */ jsx8("option", { value: opt.value, children: opt.label }, opt.value))
1646
1650
  }
1651
+ ) : onParameterActivate && !param.readOnly ? (
1652
+ // Tappable value: defer editing to the consumer's editor (e.g. a
1653
+ // numpad dialog). Commit happens via onParameterChange.
1654
+ /* @__PURE__ */ jsxs7(
1655
+ "button",
1656
+ {
1657
+ type: "button",
1658
+ onClick: () => onParameterActivate(param.id, localParameterValues[param.id] ?? param.value),
1659
+ style: {
1660
+ width: "100%",
1661
+ height: touchSize,
1662
+ display: "flex",
1663
+ alignItems: "center",
1664
+ backgroundColor: "#ffffff",
1665
+ color: textColor,
1666
+ border: `1px solid ${borderColor}`,
1667
+ borderRadius: "6px",
1668
+ padding: "0 12px",
1669
+ fontSize: `${fontSize.input}px`,
1670
+ fontFamily: "Arial, sans-serif",
1671
+ fontWeight: 600,
1672
+ textAlign: "left",
1673
+ cursor: "pointer"
1674
+ },
1675
+ children: [
1676
+ localParameterValues[param.id] ?? param.value,
1677
+ param.unit ? ` ${param.unit}` : ""
1678
+ ]
1679
+ }
1680
+ )
1647
1681
  ) : /* @__PURE__ */ jsx8(
1648
1682
  "input",
1649
1683
  {
@@ -1682,11 +1716,10 @@ function DeviceControlPanel({
1682
1716
  "button",
1683
1717
  {
1684
1718
  onClick: onStart,
1685
- disabled: binding.state.isRunning,
1686
1719
  style: {
1687
1720
  flex: 1,
1688
1721
  height: touchSize,
1689
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1722
+ backgroundColor: "#10b981",
1690
1723
  color: "#ffffff",
1691
1724
  border: "none",
1692
1725
  borderRadius: "6px",
@@ -1694,21 +1727,20 @@ function DeviceControlPanel({
1694
1727
  fontWeight: 700,
1695
1728
  fontFamily: "Arial, sans-serif",
1696
1729
  letterSpacing: "0.5px",
1697
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1698
- opacity: binding.state.isRunning ? 0.5 : 1
1730
+ cursor: "pointer",
1731
+ opacity: 1
1699
1732
  },
1700
- children: "START"
1733
+ children: startPending ? "STARTING\u2026" : "START"
1701
1734
  }
1702
1735
  ),
1703
1736
  activeCapabilities?.canStop && /* @__PURE__ */ jsx8(
1704
1737
  "button",
1705
1738
  {
1706
1739
  onClick: onStop,
1707
- disabled: !binding.state.isRunning,
1708
1740
  style: {
1709
1741
  flex: 1,
1710
1742
  height: touchSize,
1711
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1743
+ backgroundColor: "#ef4444",
1712
1744
  color: "#ffffff",
1713
1745
  border: "none",
1714
1746
  borderRadius: "6px",
@@ -1716,10 +1748,10 @@ function DeviceControlPanel({
1716
1748
  fontWeight: 700,
1717
1749
  fontFamily: "Arial, sans-serif",
1718
1750
  letterSpacing: "0.5px",
1719
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1720
- opacity: !binding.state.isRunning ? 0.5 : 1
1751
+ cursor: "pointer",
1752
+ opacity: 1
1721
1753
  },
1722
- children: "STOP"
1754
+ children: stopPending ? "STOPPING\u2026" : "STOP"
1723
1755
  }
1724
1756
  )
1725
1757
  ] }),
@@ -4354,12 +4386,13 @@ function DataOverlay({
4354
4386
  scale = 1,
4355
4387
  statusColorMap
4356
4388
  }) {
4357
- const { value, display, history } = telemetry;
4389
+ const { value, display, history, scaling } = telemetry;
4358
4390
  const showValue = display?.showValue ?? true;
4359
4391
  const showStatus = display?.showStatus ?? true;
4360
4392
  const showSparkline = display?.showSparkline ?? false;
4361
4393
  const label = display?.label;
4362
- const precision = display?.precision;
4394
+ const precision = scaling?.decimals ?? display?.precision;
4395
+ const unit = scaling?.units ?? value.unit;
4363
4396
  const sparklineColor = display?.sparklineColor;
4364
4397
  const textColor = display?.textColor;
4365
4398
  const backgroundColor = display?.backgroundColor;
@@ -4383,7 +4416,7 @@ function DataOverlay({
4383
4416
  const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
4384
4417
  const finalTextColor = textColor || colors.text;
4385
4418
  const formattedValue = formatValue(value.value);
4386
- const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
4419
+ const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
4387
4420
  const sparklinePath = (() => {
4388
4421
  if (!showSparkline || !history || history.length < 2) return "";
4389
4422
  const width = 70 * scale;
@@ -4391,8 +4424,9 @@ function DataOverlay({
4391
4424
  const padding = 5 * scale;
4392
4425
  const effectiveWidth = width - padding * 2;
4393
4426
  const effectiveHeight = height - padding * 2;
4394
- const min = Math.min(...history);
4395
- const max = Math.max(...history);
4427
+ const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
4428
+ const min = hasEuRange ? scaling.euLow : Math.min(...history);
4429
+ const max = hasEuRange ? scaling.euHigh : Math.max(...history);
4396
4430
  const range = max - min || 1;
4397
4431
  const points = history.map((val, i) => {
4398
4432
  const xPos = i / (history.length - 1) * effectiveWidth + padding;
@@ -7553,8 +7587,12 @@ function createControlBinding(nodeId, deviceConfig, actions) {
7553
7587
  }
7554
7588
 
7555
7589
  // src/hooks/useDeviceControls.ts
7556
- import { useState as useState15, useCallback as useCallback8 } from "react";
7557
- function useDeviceControls(initialBindings) {
7590
+ import { useState as useState15, useCallback as useCallback8, useRef as useRef6 } from "react";
7591
+ function commandKey(nodeId, kind, suffix) {
7592
+ return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
7593
+ }
7594
+ function useDeviceControls(initialBindings, options) {
7595
+ const { commandTimeoutMs } = options ?? {};
7558
7596
  const [controls, setControls] = useState15(() => {
7559
7597
  const map = /* @__PURE__ */ new Map();
7560
7598
  if (initialBindings) {
@@ -7564,6 +7602,11 @@ function useDeviceControls(initialBindings) {
7564
7602
  }
7565
7603
  return map;
7566
7604
  });
7605
+ const pendingRef = useRef6(/* @__PURE__ */ new Map());
7606
+ const [pendingCommands, setPendingCommands] = useState15(() => /* @__PURE__ */ new Set());
7607
+ const syncPending = useCallback8(() => {
7608
+ setPendingCommands(new Set(pendingRef.current.keys()));
7609
+ }, []);
7567
7610
  const updateControl = useCallback8((nodeId, updates) => {
7568
7611
  setControls((prev) => {
7569
7612
  const newMap = new Map(prev);
@@ -7653,121 +7696,112 @@ function useDeviceControls(initialBindings) {
7653
7696
  },
7654
7697
  [controls]
7655
7698
  );
7656
- const sendModeChange = useCallback8(
7657
- async (nodeId, mode) => {
7658
- const binding = controls.get(nodeId);
7659
- if (!binding) return;
7660
- updateDeviceState(nodeId, { currentMode: mode });
7661
- try {
7662
- await binding.actions.onModeChange?.(mode);
7663
- updateDeviceState(nodeId, {
7664
- lastCommandTime: Date.now(),
7665
- lastCommandResult: { success: true, message: `Mode changed to ${mode}` }
7666
- });
7667
- } catch (error) {
7668
- updateDeviceState(nodeId, {
7669
- currentMode: binding.state.currentMode,
7670
- lastCommandResult: {
7671
- success: false,
7672
- message: error instanceof Error ? error.message : "Mode change failed"
7673
- }
7674
- });
7699
+ const isCommandPending = useCallback8(
7700
+ (nodeId, kind) => {
7701
+ if (kind) {
7702
+ const prefix2 = `${nodeId}:${kind}`;
7703
+ for (const key of pendingCommands) {
7704
+ if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
7705
+ }
7706
+ return false;
7675
7707
  }
7708
+ const prefix = `${nodeId}:`;
7709
+ for (const key of pendingCommands) {
7710
+ if (key.startsWith(prefix)) return true;
7711
+ }
7712
+ return false;
7676
7713
  },
7677
- [controls, updateDeviceState]
7714
+ [pendingCommands]
7678
7715
  );
7679
- const sendParameterChange = useCallback8(
7680
- async (nodeId, parameterId, value) => {
7681
- const binding = controls.get(nodeId);
7682
- if (!binding) return;
7683
- updateParameter(nodeId, parameterId, value);
7684
- try {
7685
- await binding.actions.onParameterChange?.(parameterId, value);
7686
- updateDeviceState(nodeId, {
7687
- lastCommandTime: Date.now(),
7688
- lastCommandResult: { success: true, message: `Parameter ${parameterId} updated` }
7689
- });
7690
- } catch (error) {
7691
- updateDeviceState(nodeId, {
7692
- lastCommandResult: {
7693
- success: false,
7694
- message: error instanceof Error ? error.message : "Parameter change failed"
7695
- }
7696
- });
7697
- }
7716
+ const markPending = useCallback8(
7717
+ (key, delta) => {
7718
+ const next = (pendingRef.current.get(key) ?? 0) + delta;
7719
+ if (next > 0) pendingRef.current.set(key, next);
7720
+ else pendingRef.current.delete(key);
7721
+ syncPending();
7698
7722
  },
7699
- [controls, updateParameter, updateDeviceState]
7723
+ [syncPending]
7700
7724
  );
7701
- const sendStartCommand = useCallback8(
7702
- async (nodeId) => {
7725
+ const runCommand = useCallback8(
7726
+ async (nodeId, key, invoke, messages) => {
7703
7727
  const binding = controls.get(nodeId);
7704
7728
  if (!binding) return;
7705
- updateDeviceState(nodeId, { isRunning: true, status: "starting" });
7729
+ markPending(key, 1);
7730
+ let timer;
7706
7731
  try {
7707
- await binding.actions.onStart?.();
7732
+ const action = Promise.resolve(invoke(binding));
7733
+ if (commandTimeoutMs && commandTimeoutMs > 0) {
7734
+ await Promise.race([
7735
+ action,
7736
+ new Promise((_, reject) => {
7737
+ timer = setTimeout(
7738
+ () => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
7739
+ commandTimeoutMs
7740
+ );
7741
+ })
7742
+ ]);
7743
+ } else {
7744
+ await action;
7745
+ }
7708
7746
  updateDeviceState(nodeId, {
7709
- status: "normal",
7710
7747
  lastCommandTime: Date.now(),
7711
- lastCommandResult: { success: true, message: "Device started" }
7748
+ lastCommandResult: { success: true, message: messages.success }
7712
7749
  });
7713
7750
  } catch (error) {
7714
7751
  updateDeviceState(nodeId, {
7715
- isRunning: false,
7716
- status: "fault",
7752
+ lastCommandTime: Date.now(),
7717
7753
  lastCommandResult: {
7718
7754
  success: false,
7719
- message: error instanceof Error ? error.message : "Start failed"
7755
+ message: error instanceof Error ? error.message : messages.failure
7720
7756
  }
7721
7757
  });
7758
+ } finally {
7759
+ if (timer) clearTimeout(timer);
7760
+ markPending(key, -1);
7722
7761
  }
7723
7762
  },
7724
- [controls, updateDeviceState]
7763
+ [controls, updateDeviceState, markPending, commandTimeoutMs]
7764
+ );
7765
+ const sendModeChange = useCallback8(
7766
+ (nodeId, mode) => runCommand(
7767
+ nodeId,
7768
+ commandKey(nodeId, "mode"),
7769
+ (binding) => binding.actions.onModeChange?.(mode),
7770
+ { success: `Mode ${mode} command sent`, failure: "Mode change failed" }
7771
+ ),
7772
+ [runCommand]
7773
+ );
7774
+ const sendParameterChange = useCallback8(
7775
+ (nodeId, parameterId, value) => runCommand(
7776
+ nodeId,
7777
+ commandKey(nodeId, "parameter", parameterId),
7778
+ (binding) => binding.actions.onParameterChange?.(parameterId, value),
7779
+ { success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
7780
+ ),
7781
+ [runCommand]
7782
+ );
7783
+ const sendStartCommand = useCallback8(
7784
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
7785
+ success: "Start command sent",
7786
+ failure: "Start command failed"
7787
+ }),
7788
+ [runCommand]
7725
7789
  );
7726
7790
  const sendStopCommand = useCallback8(
7727
- async (nodeId) => {
7728
- const binding = controls.get(nodeId);
7729
- if (!binding) return;
7730
- updateDeviceState(nodeId, { isRunning: false, status: "stopping" });
7731
- try {
7732
- await binding.actions.onStop?.();
7733
- updateDeviceState(nodeId, {
7734
- status: "off",
7735
- lastCommandTime: Date.now(),
7736
- lastCommandResult: { success: true, message: "Device stopped" }
7737
- });
7738
- } catch (error) {
7739
- updateDeviceState(nodeId, {
7740
- isRunning: true,
7741
- status: "fault",
7742
- lastCommandResult: {
7743
- success: false,
7744
- message: error instanceof Error ? error.message : "Stop failed"
7745
- }
7746
- });
7747
- }
7748
- },
7749
- [controls, updateDeviceState]
7791
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
7792
+ success: "Stop command sent",
7793
+ failure: "Stop command failed"
7794
+ }),
7795
+ [runCommand]
7750
7796
  );
7751
7797
  const sendCustomAction = useCallback8(
7752
- async (nodeId, actionId) => {
7753
- const binding = controls.get(nodeId);
7754
- if (!binding) return;
7755
- try {
7756
- await binding.actions.onCustomAction?.(actionId);
7757
- updateDeviceState(nodeId, {
7758
- lastCommandTime: Date.now(),
7759
- lastCommandResult: { success: true, message: `Action ${actionId} executed` }
7760
- });
7761
- } catch (error) {
7762
- updateDeviceState(nodeId, {
7763
- lastCommandResult: {
7764
- success: false,
7765
- message: error instanceof Error ? error.message : "Action failed"
7766
- }
7767
- });
7768
- }
7769
- },
7770
- [controls, updateDeviceState]
7798
+ (nodeId, actionId) => runCommand(
7799
+ nodeId,
7800
+ commandKey(nodeId, "action", actionId),
7801
+ (binding) => binding.actions.onCustomAction?.(actionId),
7802
+ { success: `Action ${actionId} command sent`, failure: "Action failed" }
7803
+ ),
7804
+ [runCommand]
7771
7805
  );
7772
7806
  return {
7773
7807
  controls,
@@ -7778,6 +7812,10 @@ function useDeviceControls(initialBindings) {
7778
7812
  setControlBinding,
7779
7813
  removeControlBinding,
7780
7814
  getControlBinding,
7815
+ // In-flight command visibility (interaction state, not device state).
7816
+ // `isCommandPending` is the stable query surface; the raw key set is
7817
+ // intentionally not exported (its key format is an implementation detail).
7818
+ isCommandPending,
7781
7819
  // Command helpers
7782
7820
  sendModeChange,
7783
7821
  sendParameterChange,
@@ -8066,7 +8104,7 @@ function useDeviceConfigs(initialConfigs) {
8066
8104
  }
8067
8105
 
8068
8106
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8069
- import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef6 } from "react";
8107
+ import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef7 } from "react";
8070
8108
  import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
8071
8109
  function DeviceConfigPanel({
8072
8110
  binding,
@@ -8108,7 +8146,7 @@ function DeviceConfigPanel({
8108
8146
  });
8109
8147
  const [showToast, setShowToast] = useState17(true);
8110
8148
  const [bindingId, setBindingId] = useState17(binding?.nodeId || null);
8111
- const lastResultTimestampRef = useRef6(0);
8149
+ const lastResultTimestampRef = useRef7(0);
8112
8150
  if (binding && binding.nodeId !== bindingId) {
8113
8151
  const values = {};
8114
8152
  binding.parameters.forEach((param) => {