@procaaso/alphinity-ui-components 1.1.2 → 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/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}` : "";
@@ -895,6 +896,31 @@ var NodeControlsPanel = ({
895
896
 
896
897
  // src/components/DeviceControlPanel/DeviceControlPanel.tsx
897
898
  import { useState as useState5, useEffect as useEffect2 } from "react";
899
+
900
+ // src/theme/statusColors.ts
901
+ var DEFAULT_STATUS_COLORS = {
902
+ normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
903
+ warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
904
+ alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
905
+ fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
906
+ off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
907
+ starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
908
+ stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
909
+ };
910
+ var STATUS_ALIASES = {
911
+ running: "normal",
912
+ stopped: "off"
913
+ };
914
+ function resolveStatusColor(status, customMap) {
915
+ const raw = (status || "off").toLowerCase();
916
+ const key = STATUS_ALIASES[raw] ?? raw;
917
+ const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
918
+ const overrides = customMap?.[key];
919
+ if (!overrides) return defaults;
920
+ return { ...defaults, ...overrides };
921
+ }
922
+
923
+ // src/components/DeviceControlPanel/DeviceControlPanel.tsx
898
924
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
899
925
  function DeviceControlPanel({
900
926
  binding,
@@ -911,6 +937,8 @@ function DeviceControlPanel({
911
937
  onParameterChange,
912
938
  onStart,
913
939
  onStop,
940
+ startPending = false,
941
+ stopPending = false,
914
942
  onCustomAction,
915
943
  onOpenConfig,
916
944
  showConfigButton = true,
@@ -969,23 +997,8 @@ function DeviceControlPanel({
969
997
  const textColor = binding.theme?.textColor || "#000000";
970
998
  const mutedTextColor = "#666666";
971
999
  const selectedColor = "#b4d0fe";
972
- const statusColors = {
973
- normal: binding.theme?.statusColors?.normal || "#888888",
974
- // Gray (no alarm)
975
- warning: binding.theme?.statusColors?.warning || "#FFD700",
976
- // Yellow
977
- alarm: binding.theme?.statusColors?.alarm || "#FF0000",
978
- // Red
979
- fault: binding.theme?.statusColors?.fault || "#FF0000",
980
- // Red
981
- off: binding.theme?.statusColors?.off || "#666666",
982
- // Dark gray
983
- starting: binding.theme?.statusColors?.starting || "#90EE90",
984
- // Light green
985
- stopping: binding.theme?.statusColors?.stopping || "#FFD700"
986
- // Yellow
987
- };
988
- const statusColor = statusColors[binding.state.status || "normal"];
1000
+ const statusEntry = resolveStatusColor(binding.state.status, binding.theme?.statusColors);
1001
+ const statusColor = statusEntry.bg;
989
1002
  const handleModeChange = (mode) => {
990
1003
  onModeChange?.(mode);
991
1004
  };
@@ -1272,7 +1285,10 @@ function DeviceControlPanel({
1272
1285
  /* @__PURE__ */ jsxs7(
1273
1286
  "div",
1274
1287
  {
1275
- onClick: () => setShowModeModal(true),
1288
+ onClick: () => {
1289
+ setPendingMode(currentMode);
1290
+ setShowModeModal(true);
1291
+ },
1276
1292
  style: {
1277
1293
  padding: "12px",
1278
1294
  backgroundColor: surfaceColor,
@@ -1669,11 +1685,10 @@ function DeviceControlPanel({
1669
1685
  "button",
1670
1686
  {
1671
1687
  onClick: onStart,
1672
- disabled: binding.state.isRunning,
1673
1688
  style: {
1674
1689
  flex: 1,
1675
1690
  height: touchSize,
1676
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1691
+ backgroundColor: "#10b981",
1677
1692
  color: "#ffffff",
1678
1693
  border: "none",
1679
1694
  borderRadius: "6px",
@@ -1681,21 +1696,20 @@ function DeviceControlPanel({
1681
1696
  fontWeight: 700,
1682
1697
  fontFamily: "Arial, sans-serif",
1683
1698
  letterSpacing: "0.5px",
1684
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1685
- opacity: binding.state.isRunning ? 0.5 : 1
1699
+ cursor: "pointer",
1700
+ opacity: 1
1686
1701
  },
1687
- children: "START"
1702
+ children: startPending ? "STARTING\u2026" : "START"
1688
1703
  }
1689
1704
  ),
1690
1705
  activeCapabilities?.canStop && /* @__PURE__ */ jsx8(
1691
1706
  "button",
1692
1707
  {
1693
1708
  onClick: onStop,
1694
- disabled: !binding.state.isRunning,
1695
1709
  style: {
1696
1710
  flex: 1,
1697
1711
  height: touchSize,
1698
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1712
+ backgroundColor: "#ef4444",
1699
1713
  color: "#ffffff",
1700
1714
  border: "none",
1701
1715
  borderRadius: "6px",
@@ -1703,10 +1717,10 @@ function DeviceControlPanel({
1703
1717
  fontWeight: 700,
1704
1718
  fontFamily: "Arial, sans-serif",
1705
1719
  letterSpacing: "0.5px",
1706
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1707
- opacity: !binding.state.isRunning ? 0.5 : 1
1720
+ cursor: "pointer",
1721
+ opacity: 1
1708
1722
  },
1709
- children: "STOP"
1723
+ children: stopPending ? "STOPPING\u2026" : "STOP"
1710
1724
  }
1711
1725
  )
1712
1726
  ] }),
@@ -4012,7 +4026,8 @@ function NodeRenderer({
4012
4026
  onNodeDragStart,
4013
4027
  draggingNodeId,
4014
4028
  dragOffset = { x: 0, y: 0 },
4015
- enableDrag = false
4029
+ enableDrag = false,
4030
+ statusColorMap
4016
4031
  }) {
4017
4032
  return /* @__PURE__ */ jsx15("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
4018
4033
  const symbol = getSymbolDefinition(node.symbolId);
@@ -4071,24 +4086,9 @@ function NodeRenderer({
4071
4086
  `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
4072
4087
  );
4073
4088
  }
4074
- const getStatusColor = (status) => {
4075
- switch (status) {
4076
- case "running":
4077
- return "#22c55e";
4078
- // Green
4079
- case "alarm":
4080
- return "#ef4444";
4081
- // Red
4082
- case "warning":
4083
- return "#f59e0b";
4084
- // Orange
4085
- case "stopped":
4086
- default:
4087
- return "#edeeef";
4088
- }
4089
- };
4090
- const fillColor = node.customColors?.fill ?? getStatusColor(node.status);
4091
- const strokeColor = node.customColors?.stroke ?? "rgb(0, 0, 0)";
4089
+ const statusEntry = resolveStatusColor(node.status, statusColorMap);
4090
+ const fillColor = node.customColors?.fill ?? statusEntry.fill;
4091
+ const strokeColor = node.customColors?.stroke ?? statusEntry.stroke;
4092
4092
  return /* @__PURE__ */ jsxs11(
4093
4093
  "g",
4094
4094
  {
@@ -4352,14 +4352,16 @@ function DataOverlay({
4352
4352
  x,
4353
4353
  y,
4354
4354
  telemetry,
4355
- scale = 1
4355
+ scale = 1,
4356
+ statusColorMap
4356
4357
  }) {
4357
- const { value, display, history } = telemetry;
4358
+ const { value, display, history, scaling } = telemetry;
4358
4359
  const showValue = display?.showValue ?? true;
4359
4360
  const showStatus = display?.showStatus ?? true;
4360
4361
  const showSparkline = display?.showSparkline ?? false;
4361
4362
  const label = display?.label;
4362
- const precision = display?.precision;
4363
+ const precision = scaling?.decimals ?? display?.precision;
4364
+ const unit = scaling?.units ?? value.unit;
4363
4365
  const sparklineColor = display?.sparklineColor;
4364
4366
  const textColor = display?.textColor;
4365
4367
  const backgroundColor = display?.backgroundColor;
@@ -4377,25 +4379,13 @@ function DataOverlay({
4377
4379
  }
4378
4380
  return val.toString();
4379
4381
  };
4380
- const defaultStatusColors = {
4381
- normal: { bg: "#10b981", text: "#ffffff", border: "#059669" },
4382
- warning: { bg: "#f59e0b", text: "#ffffff", border: "#d97706" },
4383
- alarm: { bg: "#ef4444", text: "#ffffff", border: "#dc2626" },
4384
- fault: { bg: "#dc2626", text: "#ffffff", border: "#b91c1c" },
4385
- off: { bg: "#6b7280", text: "#ffffff", border: "#4b5563" }
4386
- };
4387
- const getStatusColors = () => {
4388
- const customColor = telemetry.statusColors?.[value.status];
4389
- if (customColor) {
4390
- return { bg: customColor, text: "#ffffff", border: customColor };
4391
- }
4392
- return defaultStatusColors[value.status] || defaultStatusColors.normal;
4393
- };
4394
- const statusColors = getStatusColors();
4382
+ const resolvedEntry = resolveStatusColor(value.status, statusColorMap);
4383
+ const customColor = telemetry.statusColors?.[value.status];
4384
+ const statusColors = customColor ? { bg: customColor, text: "#ffffff", border: customColor } : { bg: resolvedEntry.bg, text: resolvedEntry.text, border: resolvedEntry.border };
4395
4385
  const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
4396
4386
  const finalTextColor = textColor || colors.text;
4397
4387
  const formattedValue = formatValue(value.value);
4398
- const displayText = value.unit ? `${formattedValue} ${value.unit}` : formattedValue;
4388
+ const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
4399
4389
  const sparklinePath = (() => {
4400
4390
  if (!showSparkline || !history || history.length < 2) return "";
4401
4391
  const width = 70 * scale;
@@ -4403,8 +4393,9 @@ function DataOverlay({
4403
4393
  const padding = 5 * scale;
4404
4394
  const effectiveWidth = width - padding * 2;
4405
4395
  const effectiveHeight = height - padding * 2;
4406
- const min = Math.min(...history);
4407
- const max = Math.max(...history);
4396
+ const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
4397
+ const min = hasEuRange ? scaling.euLow : Math.min(...history);
4398
+ const max = hasEuRange ? scaling.euHigh : Math.max(...history);
4408
4399
  const range = max - min || 1;
4409
4400
  const points = history.map((val, i) => {
4410
4401
  const xPos = i / (history.length - 1) * effectiveWidth + padding;
@@ -5662,6 +5653,7 @@ function PIDCanvas({
5662
5653
  onPipeCreated,
5663
5654
  onMounted,
5664
5655
  telemetry,
5656
+ statusColorMap,
5665
5657
  ...props
5666
5658
  }) {
5667
5659
  const [localDiagram, setLocalDiagram] = useState13(diagram);
@@ -6315,7 +6307,8 @@ function PIDCanvas({
6315
6307
  enableDrag: dragEnabled && !isConnecting,
6316
6308
  onNodeDragStart: dragEnabled && !isConnecting ? handleNodeDragStart : void 0,
6317
6309
  draggingNodeId: dragState.draggedId,
6318
- dragOffset: dragState.offset
6310
+ dragOffset: dragState.offset,
6311
+ statusColorMap
6319
6312
  }
6320
6313
  ),
6321
6314
  (features.editPipeRoutes ?? false) && /* @__PURE__ */ jsx19(
@@ -6349,7 +6342,8 @@ function PIDCanvas({
6349
6342
  x: overlayX,
6350
6343
  y: overlayY,
6351
6344
  telemetry: binding,
6352
- scale: features.telemetryScale ?? 1
6345
+ scale: features.telemetryScale ?? 1,
6346
+ statusColorMap
6353
6347
  },
6354
6348
  `telemetry-${node.id}`
6355
6349
  );
@@ -7562,8 +7556,12 @@ function createControlBinding(nodeId, deviceConfig, actions) {
7562
7556
  }
7563
7557
 
7564
7558
  // src/hooks/useDeviceControls.ts
7565
- import { useState as useState15, useCallback as useCallback8 } from "react";
7566
- function useDeviceControls(initialBindings) {
7559
+ import { useState as useState15, useCallback as useCallback8, useRef as useRef6 } from "react";
7560
+ function commandKey(nodeId, kind, suffix) {
7561
+ return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
7562
+ }
7563
+ function useDeviceControls(initialBindings, options) {
7564
+ const { commandTimeoutMs } = options ?? {};
7567
7565
  const [controls, setControls] = useState15(() => {
7568
7566
  const map = /* @__PURE__ */ new Map();
7569
7567
  if (initialBindings) {
@@ -7573,6 +7571,11 @@ function useDeviceControls(initialBindings) {
7573
7571
  }
7574
7572
  return map;
7575
7573
  });
7574
+ const pendingRef = useRef6(/* @__PURE__ */ new Map());
7575
+ const [pendingCommands, setPendingCommands] = useState15(() => /* @__PURE__ */ new Set());
7576
+ const syncPending = useCallback8(() => {
7577
+ setPendingCommands(new Set(pendingRef.current.keys()));
7578
+ }, []);
7576
7579
  const updateControl = useCallback8((nodeId, updates) => {
7577
7580
  setControls((prev) => {
7578
7581
  const newMap = new Map(prev);
@@ -7662,121 +7665,112 @@ function useDeviceControls(initialBindings) {
7662
7665
  },
7663
7666
  [controls]
7664
7667
  );
7665
- const sendModeChange = useCallback8(
7666
- async (nodeId, mode) => {
7667
- const binding = controls.get(nodeId);
7668
- if (!binding) return;
7669
- updateDeviceState(nodeId, { currentMode: mode });
7670
- try {
7671
- await binding.actions.onModeChange?.(mode);
7672
- updateDeviceState(nodeId, {
7673
- lastCommandTime: Date.now(),
7674
- lastCommandResult: { success: true, message: `Mode changed to ${mode}` }
7675
- });
7676
- } catch (error) {
7677
- updateDeviceState(nodeId, {
7678
- currentMode: binding.state.currentMode,
7679
- lastCommandResult: {
7680
- success: false,
7681
- message: error instanceof Error ? error.message : "Mode change failed"
7682
- }
7683
- });
7668
+ const isCommandPending = useCallback8(
7669
+ (nodeId, kind) => {
7670
+ if (kind) {
7671
+ const prefix2 = `${nodeId}:${kind}`;
7672
+ for (const key of pendingCommands) {
7673
+ if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
7674
+ }
7675
+ return false;
7684
7676
  }
7677
+ const prefix = `${nodeId}:`;
7678
+ for (const key of pendingCommands) {
7679
+ if (key.startsWith(prefix)) return true;
7680
+ }
7681
+ return false;
7685
7682
  },
7686
- [controls, updateDeviceState]
7683
+ [pendingCommands]
7687
7684
  );
7688
- const sendParameterChange = useCallback8(
7689
- async (nodeId, parameterId, value) => {
7690
- const binding = controls.get(nodeId);
7691
- if (!binding) return;
7692
- updateParameter(nodeId, parameterId, value);
7693
- try {
7694
- await binding.actions.onParameterChange?.(parameterId, value);
7695
- updateDeviceState(nodeId, {
7696
- lastCommandTime: Date.now(),
7697
- lastCommandResult: { success: true, message: `Parameter ${parameterId} updated` }
7698
- });
7699
- } catch (error) {
7700
- updateDeviceState(nodeId, {
7701
- lastCommandResult: {
7702
- success: false,
7703
- message: error instanceof Error ? error.message : "Parameter change failed"
7704
- }
7705
- });
7706
- }
7685
+ const markPending = useCallback8(
7686
+ (key, delta) => {
7687
+ const next = (pendingRef.current.get(key) ?? 0) + delta;
7688
+ if (next > 0) pendingRef.current.set(key, next);
7689
+ else pendingRef.current.delete(key);
7690
+ syncPending();
7707
7691
  },
7708
- [controls, updateParameter, updateDeviceState]
7692
+ [syncPending]
7709
7693
  );
7710
- const sendStartCommand = useCallback8(
7711
- async (nodeId) => {
7694
+ const runCommand = useCallback8(
7695
+ async (nodeId, key, invoke, messages) => {
7712
7696
  const binding = controls.get(nodeId);
7713
7697
  if (!binding) return;
7714
- updateDeviceState(nodeId, { isRunning: true, status: "starting" });
7698
+ markPending(key, 1);
7699
+ let timer;
7715
7700
  try {
7716
- await binding.actions.onStart?.();
7701
+ const action = Promise.resolve(invoke(binding));
7702
+ if (commandTimeoutMs && commandTimeoutMs > 0) {
7703
+ await Promise.race([
7704
+ action,
7705
+ new Promise((_, reject) => {
7706
+ timer = setTimeout(
7707
+ () => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
7708
+ commandTimeoutMs
7709
+ );
7710
+ })
7711
+ ]);
7712
+ } else {
7713
+ await action;
7714
+ }
7717
7715
  updateDeviceState(nodeId, {
7718
- status: "normal",
7719
7716
  lastCommandTime: Date.now(),
7720
- lastCommandResult: { success: true, message: "Device started" }
7717
+ lastCommandResult: { success: true, message: messages.success }
7721
7718
  });
7722
7719
  } catch (error) {
7723
7720
  updateDeviceState(nodeId, {
7724
- isRunning: false,
7725
- status: "fault",
7721
+ lastCommandTime: Date.now(),
7726
7722
  lastCommandResult: {
7727
7723
  success: false,
7728
- message: error instanceof Error ? error.message : "Start failed"
7724
+ message: error instanceof Error ? error.message : messages.failure
7729
7725
  }
7730
7726
  });
7727
+ } finally {
7728
+ if (timer) clearTimeout(timer);
7729
+ markPending(key, -1);
7731
7730
  }
7732
7731
  },
7733
- [controls, updateDeviceState]
7732
+ [controls, updateDeviceState, markPending, commandTimeoutMs]
7733
+ );
7734
+ const sendModeChange = useCallback8(
7735
+ (nodeId, mode) => runCommand(
7736
+ nodeId,
7737
+ commandKey(nodeId, "mode"),
7738
+ (binding) => binding.actions.onModeChange?.(mode),
7739
+ { success: `Mode ${mode} command sent`, failure: "Mode change failed" }
7740
+ ),
7741
+ [runCommand]
7742
+ );
7743
+ const sendParameterChange = useCallback8(
7744
+ (nodeId, parameterId, value) => runCommand(
7745
+ nodeId,
7746
+ commandKey(nodeId, "parameter", parameterId),
7747
+ (binding) => binding.actions.onParameterChange?.(parameterId, value),
7748
+ { success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
7749
+ ),
7750
+ [runCommand]
7751
+ );
7752
+ const sendStartCommand = useCallback8(
7753
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
7754
+ success: "Start command sent",
7755
+ failure: "Start command failed"
7756
+ }),
7757
+ [runCommand]
7734
7758
  );
7735
7759
  const sendStopCommand = useCallback8(
7736
- async (nodeId) => {
7737
- const binding = controls.get(nodeId);
7738
- if (!binding) return;
7739
- updateDeviceState(nodeId, { isRunning: false, status: "stopping" });
7740
- try {
7741
- await binding.actions.onStop?.();
7742
- updateDeviceState(nodeId, {
7743
- status: "off",
7744
- lastCommandTime: Date.now(),
7745
- lastCommandResult: { success: true, message: "Device stopped" }
7746
- });
7747
- } catch (error) {
7748
- updateDeviceState(nodeId, {
7749
- isRunning: true,
7750
- status: "fault",
7751
- lastCommandResult: {
7752
- success: false,
7753
- message: error instanceof Error ? error.message : "Stop failed"
7754
- }
7755
- });
7756
- }
7757
- },
7758
- [controls, updateDeviceState]
7760
+ (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
7761
+ success: "Stop command sent",
7762
+ failure: "Stop command failed"
7763
+ }),
7764
+ [runCommand]
7759
7765
  );
7760
7766
  const sendCustomAction = useCallback8(
7761
- async (nodeId, actionId) => {
7762
- const binding = controls.get(nodeId);
7763
- if (!binding) return;
7764
- try {
7765
- await binding.actions.onCustomAction?.(actionId);
7766
- updateDeviceState(nodeId, {
7767
- lastCommandTime: Date.now(),
7768
- lastCommandResult: { success: true, message: `Action ${actionId} executed` }
7769
- });
7770
- } catch (error) {
7771
- updateDeviceState(nodeId, {
7772
- lastCommandResult: {
7773
- success: false,
7774
- message: error instanceof Error ? error.message : "Action failed"
7775
- }
7776
- });
7777
- }
7778
- },
7779
- [controls, updateDeviceState]
7767
+ (nodeId, actionId) => runCommand(
7768
+ nodeId,
7769
+ commandKey(nodeId, "action", actionId),
7770
+ (binding) => binding.actions.onCustomAction?.(actionId),
7771
+ { success: `Action ${actionId} command sent`, failure: "Action failed" }
7772
+ ),
7773
+ [runCommand]
7780
7774
  );
7781
7775
  return {
7782
7776
  controls,
@@ -7787,6 +7781,10 @@ function useDeviceControls(initialBindings) {
7787
7781
  setControlBinding,
7788
7782
  removeControlBinding,
7789
7783
  getControlBinding,
7784
+ // In-flight command visibility (interaction state, not device state).
7785
+ // `isCommandPending` is the stable query surface; the raw key set is
7786
+ // intentionally not exported (its key format is an implementation detail).
7787
+ isCommandPending,
7790
7788
  // Command helpers
7791
7789
  sendModeChange,
7792
7790
  sendParameterChange,
@@ -8075,7 +8073,7 @@ function useDeviceConfigs(initialConfigs) {
8075
8073
  }
8076
8074
 
8077
8075
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8078
- import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef6 } from "react";
8076
+ import { useState as useState17, useEffect as useEffect12, useMemo, useRef as useRef7 } from "react";
8079
8077
  import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
8080
8078
  function DeviceConfigPanel({
8081
8079
  binding,
@@ -8117,7 +8115,7 @@ function DeviceConfigPanel({
8117
8115
  });
8118
8116
  const [showToast, setShowToast] = useState17(true);
8119
8117
  const [bindingId, setBindingId] = useState17(binding?.nodeId || null);
8120
- const lastResultTimestampRef = useRef6(0);
8118
+ const lastResultTimestampRef = useRef7(0);
8121
8119
  if (binding && binding.nodeId !== bindingId) {
8122
8120
  const values = {};
8123
8121
  binding.parameters.forEach((param) => {
@@ -8844,6 +8842,7 @@ export {
8844
8842
  CardValue,
8845
8843
  CircularGauge,
8846
8844
  ControlPanel,
8845
+ DEFAULT_STATUS_COLORS,
8847
8846
  DEFAULT_THRESHOLDS,
8848
8847
  DashboardCard,
8849
8848
  DeviceConfigPanel,
@@ -8893,6 +8892,7 @@ export {
8893
8892
  overlayStyles,
8894
8893
  registerSymbol,
8895
8894
  registerSymbols,
8895
+ resolveStatusColor,
8896
8896
  useDeviceConfigs,
8897
8897
  useDeviceControls,
8898
8898
  useDrag,