@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 +11 -0
- package/dist/index.cjs +154 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -100
- package/dist/index.d.ts +175 -100
- package/dist/index.js +157 -119
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
583
|
+
defaultUnit,
|
|
584
584
|
defaultLabel = "Setpoint",
|
|
585
|
-
defaultMin
|
|
586
|
-
defaultMax
|
|
587
|
-
defaultPrecision
|
|
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:
|
|
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:
|
|
1801
|
-
opacity:
|
|
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:
|
|
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:
|
|
1823
|
-
opacity:
|
|
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 =
|
|
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
|
|
4498
|
-
const
|
|
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
|
|
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
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
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
|
-
[
|
|
7817
|
+
[pendingCommands]
|
|
7781
7818
|
);
|
|
7782
|
-
const
|
|
7783
|
-
|
|
7784
|
-
const
|
|
7785
|
-
if (
|
|
7786
|
-
|
|
7787
|
-
|
|
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
|
-
[
|
|
7826
|
+
[syncPending]
|
|
7803
7827
|
);
|
|
7804
|
-
const
|
|
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
|
-
|
|
7832
|
+
markPending(key, 1);
|
|
7833
|
+
let timer;
|
|
7809
7834
|
try {
|
|
7810
|
-
|
|
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:
|
|
7851
|
+
lastCommandResult: { success: true, message: messages.success }
|
|
7815
7852
|
});
|
|
7816
7853
|
} catch (error) {
|
|
7817
7854
|
updateDeviceState(nodeId, {
|
|
7818
|
-
|
|
7819
|
-
status: "fault",
|
|
7855
|
+
lastCommandTime: Date.now(),
|
|
7820
7856
|
lastCommandResult: {
|
|
7821
7857
|
success: false,
|
|
7822
|
-
message: error instanceof Error ? error.message :
|
|
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
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
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
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
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,
|