@procaaso/alphinity-ui-components 1.0.11 → 1.0.12

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.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  ControlPanel: () => ControlPanel,
39
39
  DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
40
40
  DashboardCard: () => DashboardCard,
41
+ DeviceConfigPanel: () => DeviceConfigPanel,
41
42
  DeviceControlPanel: () => DeviceControlPanel,
42
43
  DisplayModeToggle: () => DisplayModeToggle,
43
44
  EquipmentIndicator: () => EquipmentIndicator,
@@ -70,6 +71,7 @@ __export(index_exports, {
70
71
  ZoomButton: () => ZoomButton,
71
72
  ZoomControls: () => ZoomControls,
72
73
  calculateThresholdStatus: () => calculateThresholdStatus,
74
+ createConfigBinding: () => createConfigBinding,
73
75
  createControlBinding: () => createControlBinding,
74
76
  exampleDiagram: () => exampleDiagram,
75
77
  exportDiagram: () => exportDiagram,
@@ -83,6 +85,7 @@ __export(index_exports, {
83
85
  overlayStyles: () => overlayStyles,
84
86
  registerSymbol: () => registerSymbol,
85
87
  registerSymbols: () => registerSymbols,
88
+ useDeviceConfigs: () => useDeviceConfigs,
86
89
  useDeviceControls: () => useDeviceControls,
87
90
  useDrag: () => useDrag,
88
91
  useKeyboardShortcuts: () => useKeyboardShortcuts,
@@ -1009,7 +1012,10 @@ function DeviceControlPanel({
1009
1012
  onParameterChange,
1010
1013
  onStart,
1011
1014
  onStop,
1012
- onCustomAction
1015
+ onCustomAction,
1016
+ onOpenConfig,
1017
+ showConfigButton = true,
1018
+ modeSelectionStyle = "buttons"
1013
1019
  }) {
1014
1020
  const [localParameterValues, setLocalParameterValues] = (0, import_react5.useState)({});
1015
1021
  const [showToast, setShowToast] = (0, import_react5.useState)(true);
@@ -1088,14 +1094,13 @@ function DeviceControlPanel({
1088
1094
  position: "fixed",
1089
1095
  ...position === "bottom" ? {
1090
1096
  bottom: 0,
1091
- ...align === "left" ? { left: 0, right: "auto" } : align === "right" ? { right: 0, left: "auto" } : { left: 0, right: 0, marginLeft: "auto", marginRight: "auto" },
1092
- animation: "slideUp 0.3s ease-out",
1093
- maxWidth
1097
+ ...align === "left" ? { left: 0, right: "auto", width: maxWidth } : align === "right" ? { right: 0, left: "auto", width: maxWidth } : { left: 0, right: 0, marginLeft: "auto", marginRight: "auto", maxWidth },
1098
+ animation: "slideUp 0.3s ease-out"
1094
1099
  } : {
1095
1100
  right: 0,
1096
1101
  top: 0,
1097
1102
  bottom: 0,
1098
- width: "400px",
1103
+ width: maxWidth || "400px",
1099
1104
  animation: "slideIn 0.3s ease-out"
1100
1105
  },
1101
1106
  backgroundColor,
@@ -1153,25 +1158,48 @@ function DeviceControlPanel({
1153
1158
  }
1154
1159
  )
1155
1160
  ] }),
1156
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1157
- "button",
1158
- {
1159
- onClick: onClose,
1160
- style: {
1161
- width: touchSize * 0.7,
1162
- height: touchSize * 0.7,
1163
- backgroundColor: surfaceColor,
1164
- color: textColor,
1165
- border: `2px solid ${borderColor}`,
1166
- borderRadius: "2px",
1167
- fontSize: "18px",
1168
- cursor: "pointer",
1169
- fontWeight: "bold",
1170
- fontFamily: "Arial, sans-serif"
1171
- },
1172
- children: "\xD7"
1173
- }
1174
- )
1161
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
1162
+ showConfigButton && onOpenConfig && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1163
+ "button",
1164
+ {
1165
+ onClick: onOpenConfig,
1166
+ title: "Open Configuration",
1167
+ style: {
1168
+ width: touchSize * 0.7,
1169
+ height: touchSize * 0.7,
1170
+ backgroundColor: surfaceColor,
1171
+ color: textColor,
1172
+ border: `2px solid ${borderColor}`,
1173
+ borderRadius: "2px",
1174
+ fontSize: "16px",
1175
+ cursor: "pointer",
1176
+ fontWeight: 600,
1177
+ fontFamily: "Arial, sans-serif",
1178
+ padding: "0"
1179
+ },
1180
+ children: "\u2699"
1181
+ }
1182
+ ),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1184
+ "button",
1185
+ {
1186
+ onClick: onClose,
1187
+ style: {
1188
+ width: touchSize * 0.7,
1189
+ height: touchSize * 0.7,
1190
+ backgroundColor: surfaceColor,
1191
+ color: textColor,
1192
+ border: `2px solid ${borderColor}`,
1193
+ borderRadius: "2px",
1194
+ fontSize: "18px",
1195
+ cursor: "pointer",
1196
+ fontWeight: "bold",
1197
+ fontFamily: "Arial, sans-serif"
1198
+ },
1199
+ children: "\xD7"
1200
+ }
1201
+ )
1202
+ ] })
1175
1203
  ]
1176
1204
  }
1177
1205
  ),
@@ -1266,7 +1294,30 @@ function DeviceControlPanel({
1266
1294
  children: "Mode"
1267
1295
  }
1268
1296
  ),
1269
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1297
+ modeSelectionStyle === "dropdown" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1298
+ "select",
1299
+ {
1300
+ value: currentMode,
1301
+ onChange: (e) => handleModeChange(e.target.value),
1302
+ style: {
1303
+ width: "100%",
1304
+ height: touchSize,
1305
+ backgroundColor: surfaceColor,
1306
+ color: textColor,
1307
+ border: `1px solid ${borderColor}`,
1308
+ borderRadius: "6px",
1309
+ fontSize: `${fontSize.label}px`,
1310
+ fontWeight: 600,
1311
+ fontFamily: "Arial, sans-serif",
1312
+ letterSpacing: "0.5px",
1313
+ textTransform: "uppercase",
1314
+ paddingLeft: "12px",
1315
+ paddingRight: "12px",
1316
+ cursor: "pointer"
1317
+ },
1318
+ children: binding.modes.map((mode) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: mode.value, disabled: mode.enabled === false, children: mode.label }, mode.value))
1319
+ }
1320
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1270
1321
  "div",
1271
1322
  {
1272
1323
  style: {
@@ -7595,8 +7646,938 @@ function useDeviceControls(initialBindings) {
7595
7646
  };
7596
7647
  }
7597
7648
 
7598
- // src/diagram/hooks/useSimulation.ts
7649
+ // src/diagram/schema/DeviceConfigBinding.ts
7650
+ function createConfigBinding(nodeId, tag, parameters, actions, options) {
7651
+ return {
7652
+ nodeId,
7653
+ tag,
7654
+ deviceName: options?.deviceName,
7655
+ deviceType: options?.deviceType,
7656
+ sections: options?.sections,
7657
+ parameters,
7658
+ state: {
7659
+ isDirty: false,
7660
+ isSaving: false,
7661
+ errors: {}
7662
+ },
7663
+ actions,
7664
+ display: options?.display,
7665
+ theme: options?.theme,
7666
+ metadata: options?.metadata
7667
+ };
7668
+ }
7669
+
7670
+ // src/hooks/useDeviceConfigs.ts
7599
7671
  var import_react19 = require("react");
7672
+ function useDeviceConfigs(initialConfigs) {
7673
+ const [configs, setConfigs] = (0, import_react19.useState)(() => {
7674
+ const map = /* @__PURE__ */ new Map();
7675
+ if (initialConfigs) {
7676
+ initialConfigs.forEach((config) => {
7677
+ map.set(config.nodeId, config);
7678
+ });
7679
+ }
7680
+ return map;
7681
+ });
7682
+ const getConfig = (0, import_react19.useCallback)(
7683
+ (nodeId) => {
7684
+ return configs.get(nodeId);
7685
+ },
7686
+ [configs]
7687
+ );
7688
+ const updateConfig = (0, import_react19.useCallback)((nodeId, updates) => {
7689
+ setConfigs((prev) => {
7690
+ const newMap = new Map(prev);
7691
+ const existing = newMap.get(nodeId);
7692
+ if (existing) {
7693
+ newMap.set(nodeId, { ...existing, ...updates });
7694
+ } else {
7695
+ if ("tag" in updates && "parameters" in updates && "state" in updates && "actions" in updates) {
7696
+ newMap.set(nodeId, { nodeId, ...updates });
7697
+ }
7698
+ }
7699
+ return newMap;
7700
+ });
7701
+ }, []);
7702
+ const updateConfigState = (0, import_react19.useCallback)((nodeId, stateUpdates) => {
7703
+ setConfigs((prev) => {
7704
+ const newMap = new Map(prev);
7705
+ const existing = newMap.get(nodeId);
7706
+ if (existing) {
7707
+ newMap.set(nodeId, {
7708
+ ...existing,
7709
+ state: { ...existing.state, ...stateUpdates }
7710
+ });
7711
+ }
7712
+ return newMap;
7713
+ });
7714
+ }, []);
7715
+ const updateConfigBatch = (0, import_react19.useCallback)((updates) => {
7716
+ setConfigs((prev) => {
7717
+ const newMap = new Map(prev);
7718
+ Object.entries(updates).forEach(([nodeId, update]) => {
7719
+ const existing = newMap.get(nodeId);
7720
+ if (existing) {
7721
+ newMap.set(nodeId, { ...existing, ...update });
7722
+ }
7723
+ });
7724
+ return newMap;
7725
+ });
7726
+ }, []);
7727
+ const updateParameterValue = (0, import_react19.useCallback)((nodeId, parameterId, value) => {
7728
+ setConfigs((prev) => {
7729
+ const newMap = new Map(prev);
7730
+ const existing = newMap.get(nodeId);
7731
+ if (existing) {
7732
+ const updatedParameters = existing.parameters.map(
7733
+ (param) => param.id === parameterId ? { ...param, value } : param
7734
+ );
7735
+ newMap.set(nodeId, {
7736
+ ...existing,
7737
+ parameters: updatedParameters,
7738
+ state: { ...existing.state, isDirty: true }
7739
+ });
7740
+ }
7741
+ return newMap;
7742
+ });
7743
+ }, []);
7744
+ const applyConfig = (0, import_react19.useCallback)(
7745
+ async (nodeId) => {
7746
+ const config = configs.get(nodeId);
7747
+ if (!config || !config.actions.onApply) {
7748
+ return { success: false, message: "No apply handler defined" };
7749
+ }
7750
+ updateConfigState(nodeId, { isSaving: true });
7751
+ try {
7752
+ const values = config.parameters.reduce(
7753
+ (acc, param) => {
7754
+ acc[param.id] = param.value;
7755
+ return acc;
7756
+ },
7757
+ {}
7758
+ );
7759
+ const result = await config.actions.onApply(values);
7760
+ updateConfigState(nodeId, {
7761
+ isSaving: false,
7762
+ lastSaveResult: result,
7763
+ isDirty: result.success ? false : config.state.isDirty
7764
+ });
7765
+ return result;
7766
+ } catch (error) {
7767
+ const message = error instanceof Error ? error.message : "Apply failed";
7768
+ updateConfigState(nodeId, {
7769
+ isSaving: false,
7770
+ lastSaveResult: { success: false, message }
7771
+ });
7772
+ return { success: false, message };
7773
+ }
7774
+ },
7775
+ [configs, updateConfigState]
7776
+ );
7777
+ const saveConfig = (0, import_react19.useCallback)(
7778
+ async (nodeId) => {
7779
+ const config = configs.get(nodeId);
7780
+ if (!config || !config.actions.onSave) {
7781
+ return { success: false, message: "No save handler defined" };
7782
+ }
7783
+ updateConfigState(nodeId, { isSaving: true });
7784
+ try {
7785
+ const values = config.parameters.reduce(
7786
+ (acc, param) => {
7787
+ acc[param.id] = param.value;
7788
+ return acc;
7789
+ },
7790
+ {}
7791
+ );
7792
+ const result = await config.actions.onSave(values);
7793
+ updateConfigState(nodeId, {
7794
+ isSaving: false,
7795
+ lastSaved: result.success ? Date.now() : config.state.lastSaved,
7796
+ lastSaveResult: result,
7797
+ isDirty: result.success ? false : config.state.isDirty
7798
+ });
7799
+ return result;
7800
+ } catch (error) {
7801
+ const message = error instanceof Error ? error.message : "Save failed";
7802
+ updateConfigState(nodeId, {
7803
+ isSaving: false,
7804
+ lastSaveResult: { success: false, message }
7805
+ });
7806
+ return { success: false, message };
7807
+ }
7808
+ },
7809
+ [configs, updateConfigState]
7810
+ );
7811
+ const resetConfig = (0, import_react19.useCallback)(
7812
+ async (nodeId) => {
7813
+ const config = configs.get(nodeId);
7814
+ if (!config) {
7815
+ return { success: false, message: "Configuration not found" };
7816
+ }
7817
+ try {
7818
+ const resetParameters = config.parameters.map((param) => ({
7819
+ ...param,
7820
+ value: param.defaultValue !== void 0 ? param.defaultValue : param.value
7821
+ }));
7822
+ updateConfig(nodeId, {
7823
+ parameters: resetParameters,
7824
+ state: { ...config.state, isDirty: true }
7825
+ });
7826
+ if (config.actions.onReset) {
7827
+ const result = await config.actions.onReset();
7828
+ updateConfigState(nodeId, {
7829
+ lastSaveResult: result
7830
+ });
7831
+ return result;
7832
+ }
7833
+ return { success: true, message: "Reset to defaults" };
7834
+ } catch (error) {
7835
+ const message = error instanceof Error ? error.message : "Reset failed";
7836
+ return { success: false, message };
7837
+ }
7838
+ },
7839
+ [configs, updateConfig, updateConfigState]
7840
+ );
7841
+ const cancelConfig = (0, import_react19.useCallback)(
7842
+ (nodeId) => {
7843
+ const config = configs.get(nodeId);
7844
+ if (config?.actions.onCancel) {
7845
+ config.actions.onCancel();
7846
+ }
7847
+ },
7848
+ [configs]
7849
+ );
7850
+ const validateConfig = (0, import_react19.useCallback)(
7851
+ (nodeId) => {
7852
+ const config = configs.get(nodeId);
7853
+ if (!config) {
7854
+ return { valid: false, errors: { _general: "Configuration not found" } };
7855
+ }
7856
+ const errors = {};
7857
+ const values = config.parameters.reduce(
7858
+ (acc, param) => {
7859
+ acc[param.id] = param.value;
7860
+ return acc;
7861
+ },
7862
+ {}
7863
+ );
7864
+ config.parameters.forEach((param) => {
7865
+ if (param.required && (param.value === void 0 || param.value === null || param.value === "")) {
7866
+ errors[param.id] = `${param.label} is required`;
7867
+ return;
7868
+ }
7869
+ if (param.type === "number" && typeof param.value === "number") {
7870
+ if (param.min !== void 0 && param.value < param.min) {
7871
+ errors[param.id] = `${param.label} must be at least ${param.min}`;
7872
+ }
7873
+ if (param.max !== void 0 && param.value > param.max) {
7874
+ errors[param.id] = `${param.label} must be at most ${param.max}`;
7875
+ }
7876
+ }
7877
+ if (param.validate) {
7878
+ const result = param.validate(param.value);
7879
+ if (result !== true) {
7880
+ errors[param.id] = typeof result === "string" ? result : `${param.label} is invalid`;
7881
+ }
7882
+ }
7883
+ if (param.visibleWhen && !param.visibleWhen(values)) {
7884
+ delete errors[param.id];
7885
+ }
7886
+ });
7887
+ updateConfigState(nodeId, { errors });
7888
+ return { valid: Object.keys(errors).length === 0, errors };
7889
+ },
7890
+ [configs, updateConfigState]
7891
+ );
7892
+ const setConfig = (0, import_react19.useCallback)((config) => {
7893
+ setConfigs((prev) => {
7894
+ const newMap = new Map(prev);
7895
+ newMap.set(config.nodeId, config);
7896
+ return newMap;
7897
+ });
7898
+ }, []);
7899
+ const removeConfig = (0, import_react19.useCallback)((nodeId) => {
7900
+ setConfigs((prev) => {
7901
+ const newMap = new Map(prev);
7902
+ newMap.delete(nodeId);
7903
+ return newMap;
7904
+ });
7905
+ }, []);
7906
+ const clearConfigs = (0, import_react19.useCallback)(() => {
7907
+ setConfigs(/* @__PURE__ */ new Map());
7908
+ }, []);
7909
+ return {
7910
+ configs,
7911
+ getConfig,
7912
+ setConfig,
7913
+ removeConfig,
7914
+ clearConfigs,
7915
+ updateConfig,
7916
+ updateConfigState,
7917
+ updateConfigBatch,
7918
+ updateParameterValue,
7919
+ applyConfig,
7920
+ saveConfig,
7921
+ resetConfig,
7922
+ cancelConfig,
7923
+ validateConfig
7924
+ };
7925
+ }
7926
+
7927
+ // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
7928
+ var import_react20 = require("react");
7929
+ var import_jsx_runtime22 = require("react/jsx-runtime");
7930
+ function DeviceConfigPanel({
7931
+ binding,
7932
+ onClose,
7933
+ position = "bottom",
7934
+ align = "center",
7935
+ touchOptimized = true,
7936
+ compact = false,
7937
+ fontScale = 1,
7938
+ maxHeight,
7939
+ maxWidth,
7940
+ toastDuration = 3e3,
7941
+ onOpenControls,
7942
+ showControlsButton = true
7943
+ }) {
7944
+ const [localValues, setLocalValues] = (0, import_react20.useState)(() => {
7945
+ if (binding) {
7946
+ const values = {};
7947
+ binding.parameters.forEach((param) => {
7948
+ values[param.id] = param.value;
7949
+ });
7950
+ return values;
7951
+ }
7952
+ return {};
7953
+ });
7954
+ const [collapsedSections, setCollapsedSections] = (0, import_react20.useState)(() => {
7955
+ const collapsed = /* @__PURE__ */ new Set();
7956
+ if (binding?.sections) {
7957
+ binding.sections.forEach((section) => {
7958
+ if (section.collapsible && section.defaultCollapsed) {
7959
+ collapsed.add(section.id);
7960
+ }
7961
+ });
7962
+ }
7963
+ return collapsed;
7964
+ });
7965
+ const [showToast, setShowToast] = (0, import_react20.useState)(true);
7966
+ const [bindingId, setBindingId] = (0, import_react20.useState)(binding?.nodeId || null);
7967
+ const lastResultTimestampRef = (0, import_react20.useRef)(0);
7968
+ if (binding && binding.nodeId !== bindingId) {
7969
+ const values = {};
7970
+ binding.parameters.forEach((param) => {
7971
+ values[param.id] = param.value;
7972
+ });
7973
+ setLocalValues(values);
7974
+ const collapsed = /* @__PURE__ */ new Set();
7975
+ binding.sections?.forEach((section) => {
7976
+ if (section.collapsible && section.defaultCollapsed) {
7977
+ collapsed.add(section.id);
7978
+ }
7979
+ });
7980
+ setCollapsedSections(collapsed);
7981
+ setBindingId(binding.nodeId);
7982
+ }
7983
+ (0, import_react20.useEffect)(() => {
7984
+ const currentTimestamp = binding?.state.lastSaved || 0;
7985
+ if (binding?.state.lastSaveResult && currentTimestamp !== lastResultTimestampRef.current) {
7986
+ lastResultTimestampRef.current = currentTimestamp;
7987
+ const showTimer = setTimeout(() => {
7988
+ setShowToast(true);
7989
+ const hideTimer = setTimeout(() => {
7990
+ setShowToast(false);
7991
+ }, toastDuration);
7992
+ return () => clearTimeout(hideTimer);
7993
+ }, 0);
7994
+ return () => clearTimeout(showTimer);
7995
+ }
7996
+ }, [binding?.state.lastSaveResult, binding?.state.lastSaved, toastDuration]);
7997
+ const parametersBySection = (0, import_react20.useMemo)(() => {
7998
+ if (!binding) return { _unsectioned: [] };
7999
+ const groups = { _unsectioned: [] };
8000
+ binding.parameters.forEach((param) => {
8001
+ const sectionId = param.section || "_unsectioned";
8002
+ if (!groups[sectionId]) {
8003
+ groups[sectionId] = [];
8004
+ }
8005
+ groups[sectionId].push(param);
8006
+ });
8007
+ Object.keys(groups).forEach((sectionId) => {
8008
+ groups[sectionId].sort((a, b) => (a.order || 0) - (b.order || 0));
8009
+ });
8010
+ return groups;
8011
+ }, [binding]);
8012
+ const sortedSections = (0, import_react20.useMemo)(() => {
8013
+ if (!binding?.sections) return [];
8014
+ return [...binding.sections].sort((a, b) => (a.order || 0) - (b.order || 0));
8015
+ }, [binding]);
8016
+ if (!binding) return null;
8017
+ const touchSize = touchOptimized ? 56 : 40;
8018
+ const spacing = {
8019
+ padding: compact ? "12px" : "20px",
8020
+ gap: compact ? "6px" : "8px",
8021
+ marginBottom: compact ? "10px" : "16px",
8022
+ itemPadding: compact ? "8px 10px" : "10px 12px"
8023
+ };
8024
+ const fontSize = {
8025
+ title: Math.round(14 * fontScale),
8026
+ label: Math.round(11 * fontScale),
8027
+ value: Math.round(13 * fontScale),
8028
+ input: Math.round(13 * fontScale),
8029
+ button: Math.round(12 * fontScale),
8030
+ section: Math.round(12 * fontScale),
8031
+ description: Math.round(10 * fontScale)
8032
+ };
8033
+ const backgroundColor = binding.theme?.backgroundColor || "#edeeef";
8034
+ const surfaceColor = binding.theme?.surfaceColor || "#f5f6f7";
8035
+ const borderColor = binding.theme?.borderColor || "#d2d2d2";
8036
+ const textColor = binding.theme?.textColor || "#000000";
8037
+ const mutedTextColor = binding.theme?.mutedTextColor || "#666666";
8038
+ const accentColor = binding.theme?.accentColor || "#b4d0fe";
8039
+ const errorColor = binding.theme?.errorColor || "#ef4444";
8040
+ const successColor = binding.theme?.successColor || "#10b981";
8041
+ const handleValueChange = (parameterId, value) => {
8042
+ setLocalValues((prev) => ({ ...prev, [parameterId]: value }));
8043
+ };
8044
+ const handleApply = async () => {
8045
+ if (binding.actions.onApply) {
8046
+ await binding.actions.onApply(localValues);
8047
+ }
8048
+ };
8049
+ const handleSave = async () => {
8050
+ if (binding.actions.onSave) {
8051
+ await binding.actions.onSave(localValues);
8052
+ }
8053
+ };
8054
+ const handleReset = async () => {
8055
+ if (binding.display?.resetConfirmMessage) {
8056
+ if (!confirm(binding.display.resetConfirmMessage)) {
8057
+ return;
8058
+ }
8059
+ }
8060
+ if (binding.actions.onReset) {
8061
+ await binding.actions.onReset();
8062
+ }
8063
+ };
8064
+ const handleCancel = () => {
8065
+ if (binding.actions.onCancel) {
8066
+ binding.actions.onCancel();
8067
+ }
8068
+ onClose();
8069
+ };
8070
+ const toggleSection = (sectionId) => {
8071
+ setCollapsedSections((prev) => {
8072
+ const next = new Set(prev);
8073
+ if (next.has(sectionId)) {
8074
+ next.delete(sectionId);
8075
+ } else {
8076
+ next.add(sectionId);
8077
+ }
8078
+ return next;
8079
+ });
8080
+ };
8081
+ const renderParameter = (param) => {
8082
+ if (param.visibleWhen && !param.visibleWhen(localValues)) {
8083
+ return null;
8084
+ }
8085
+ const value = localValues[param.id];
8086
+ const error = binding.state.errors?.[param.id];
8087
+ const isDisabled = param.readOnly || param.enabled === false;
8088
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8089
+ "div",
8090
+ {
8091
+ style: {
8092
+ marginBottom: spacing.marginBottom
8093
+ },
8094
+ children: [
8095
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8096
+ "label",
8097
+ {
8098
+ style: {
8099
+ display: "block",
8100
+ fontSize: `${fontSize.label}px`,
8101
+ fontWeight: 600,
8102
+ color: textColor,
8103
+ marginBottom: "4px",
8104
+ fontFamily: "Arial, sans-serif",
8105
+ textTransform: "uppercase",
8106
+ letterSpacing: "0.5px"
8107
+ },
8108
+ children: [
8109
+ param.label,
8110
+ param.required && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { style: { color: errorColor }, children: " *" }),
8111
+ param.unit && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { style: { color: mutedTextColor, fontWeight: "normal" }, children: [
8112
+ " (",
8113
+ param.unit,
8114
+ ")"
8115
+ ] })
8116
+ ]
8117
+ }
8118
+ ),
8119
+ param.description && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8120
+ "div",
8121
+ {
8122
+ style: {
8123
+ fontSize: `${fontSize.description}px`,
8124
+ color: mutedTextColor,
8125
+ marginBottom: "6px",
8126
+ fontFamily: "Arial, sans-serif"
8127
+ },
8128
+ children: param.description
8129
+ }
8130
+ ),
8131
+ param.type === "number" && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8132
+ "input",
8133
+ {
8134
+ type: "number",
8135
+ value: value ?? "",
8136
+ onChange: (e) => handleValueChange(param.id, parseFloat(e.target.value)),
8137
+ min: param.min,
8138
+ max: param.max,
8139
+ step: param.step,
8140
+ disabled: isDisabled,
8141
+ style: {
8142
+ width: "100%",
8143
+ height: `${touchSize * 0.8}px`,
8144
+ padding: spacing.itemPadding,
8145
+ fontSize: `${fontSize.input}px`,
8146
+ fontFamily: "Arial, sans-serif",
8147
+ backgroundColor: isDisabled ? "#e5e7eb" : surfaceColor,
8148
+ color: textColor,
8149
+ border: `1px solid ${error ? errorColor : borderColor}`,
8150
+ borderRadius: "2px",
8151
+ outline: "none"
8152
+ }
8153
+ }
8154
+ ),
8155
+ param.type === "string" && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8156
+ "input",
8157
+ {
8158
+ type: "text",
8159
+ value: value ?? "",
8160
+ onChange: (e) => handleValueChange(param.id, e.target.value),
8161
+ disabled: isDisabled,
8162
+ style: {
8163
+ width: "100%",
8164
+ height: `${touchSize * 0.8}px`,
8165
+ padding: spacing.itemPadding,
8166
+ fontSize: `${fontSize.input}px`,
8167
+ fontFamily: "Arial, sans-serif",
8168
+ backgroundColor: isDisabled ? "#e5e7eb" : surfaceColor,
8169
+ color: textColor,
8170
+ border: `1px solid ${error ? errorColor : borderColor}`,
8171
+ borderRadius: "2px",
8172
+ outline: "none"
8173
+ }
8174
+ }
8175
+ ),
8176
+ param.type === "boolean" && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8177
+ "label",
8178
+ {
8179
+ style: {
8180
+ display: "flex",
8181
+ alignItems: "center",
8182
+ gap: "8px",
8183
+ cursor: isDisabled ? "not-allowed" : "pointer"
8184
+ },
8185
+ children: [
8186
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8187
+ "input",
8188
+ {
8189
+ type: "checkbox",
8190
+ checked: value ?? false,
8191
+ onChange: (e) => handleValueChange(param.id, e.target.checked),
8192
+ disabled: isDisabled,
8193
+ style: {
8194
+ width: "20px",
8195
+ height: "20px",
8196
+ cursor: isDisabled ? "not-allowed" : "pointer"
8197
+ }
8198
+ }
8199
+ ),
8200
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { style: { fontSize: `${fontSize.value}px`, color: textColor }, children: value ? "Enabled" : "Disabled" })
8201
+ ]
8202
+ }
8203
+ ),
8204
+ param.type === "select" && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8205
+ "select",
8206
+ {
8207
+ value: value ?? "",
8208
+ onChange: (e) => handleValueChange(param.id, e.target.value),
8209
+ disabled: isDisabled,
8210
+ style: {
8211
+ width: "100%",
8212
+ height: `${touchSize * 0.8}px`,
8213
+ padding: spacing.itemPadding,
8214
+ fontSize: `${fontSize.input}px`,
8215
+ fontFamily: "Arial, sans-serif",
8216
+ backgroundColor: isDisabled ? "#e5e7eb" : surfaceColor,
8217
+ color: textColor,
8218
+ border: `1px solid ${error ? errorColor : borderColor}`,
8219
+ borderRadius: "2px",
8220
+ outline: "none"
8221
+ },
8222
+ children: param.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("option", { value: option.value, children: option.label }, option.value))
8223
+ }
8224
+ ),
8225
+ error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8226
+ "div",
8227
+ {
8228
+ style: {
8229
+ fontSize: `${fontSize.description}px`,
8230
+ color: errorColor,
8231
+ marginTop: "4px",
8232
+ fontFamily: "Arial, sans-serif"
8233
+ },
8234
+ children: error
8235
+ }
8236
+ )
8237
+ ]
8238
+ },
8239
+ param.id
8240
+ );
8241
+ };
8242
+ const renderSection = (section) => {
8243
+ const params = parametersBySection[section.id] || [];
8244
+ const isCollapsed = collapsedSections.has(section.id);
8245
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8246
+ "div",
8247
+ {
8248
+ style: {
8249
+ marginBottom: spacing.marginBottom,
8250
+ border: `1px solid ${borderColor}`,
8251
+ borderRadius: "4px",
8252
+ backgroundColor: surfaceColor
8253
+ },
8254
+ children: [
8255
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8256
+ "div",
8257
+ {
8258
+ onClick: () => section.collapsible && toggleSection(section.id),
8259
+ style: {
8260
+ padding: spacing.itemPadding,
8261
+ borderBottom: isCollapsed ? "none" : `1px solid ${borderColor}`,
8262
+ cursor: section.collapsible ? "pointer" : "default",
8263
+ display: "flex",
8264
+ justifyContent: "space-between",
8265
+ alignItems: "center"
8266
+ },
8267
+ children: [
8268
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { children: [
8269
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8270
+ "div",
8271
+ {
8272
+ style: {
8273
+ fontSize: `${fontSize.section}px`,
8274
+ fontWeight: 600,
8275
+ color: textColor,
8276
+ fontFamily: "Arial, sans-serif",
8277
+ textTransform: "uppercase",
8278
+ letterSpacing: "0.5px"
8279
+ },
8280
+ children: [
8281
+ section.icon && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { style: { marginRight: "8px" }, children: section.icon }),
8282
+ section.title
8283
+ ]
8284
+ }
8285
+ ),
8286
+ section.description && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8287
+ "div",
8288
+ {
8289
+ style: {
8290
+ fontSize: `${fontSize.description}px`,
8291
+ color: mutedTextColor,
8292
+ marginTop: "4px",
8293
+ fontFamily: "Arial, sans-serif"
8294
+ },
8295
+ children: section.description
8296
+ }
8297
+ )
8298
+ ] }),
8299
+ section.collapsible && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { style: { fontSize: "16px", color: mutedTextColor }, children: isCollapsed ? "\u25BC" : "\u25B2" })
8300
+ ]
8301
+ }
8302
+ ),
8303
+ !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { padding: spacing.padding }, children: params.map(renderParameter) })
8304
+ ]
8305
+ },
8306
+ section.id
8307
+ );
8308
+ };
8309
+ const panelStyle = {
8310
+ position: "fixed",
8311
+ ...position === "bottom" ? {
8312
+ bottom: 0,
8313
+ ...align === "left" ? { left: 0, right: "auto", width: maxWidth } : align === "right" ? { right: 0, left: "auto", width: maxWidth } : { left: 0, right: 0, marginLeft: "auto", marginRight: "auto", maxWidth },
8314
+ animation: "slideUp 0.3s ease-out"
8315
+ } : {
8316
+ right: 0,
8317
+ top: 0,
8318
+ bottom: 0,
8319
+ width: maxWidth || "400px",
8320
+ animation: "slideIn 0.3s ease-out"
8321
+ },
8322
+ backgroundColor,
8323
+ color: textColor,
8324
+ borderRadius: position === "bottom" ? "8px 8px 0 0" : "8px 0 0 8px",
8325
+ boxShadow: "none",
8326
+ border: `2px solid ${borderColor}`,
8327
+ padding: spacing.padding,
8328
+ zIndex: 1e4,
8329
+ maxHeight: maxHeight || (position === "bottom" ? "70vh" : "100vh"),
8330
+ overflowY: "auto",
8331
+ display: "flex",
8332
+ flexDirection: "column",
8333
+ ...binding.display?.style
8334
+ };
8335
+ const titleText = binding.display?.title || binding.deviceName || binding.tag || binding.nodeId;
8336
+ const showApply = binding.display?.showApply !== false && binding.actions.onApply;
8337
+ const showSave = binding.display?.showSave !== false && binding.actions.onSave;
8338
+ const showReset = binding.display?.showReset !== false && binding.actions.onReset;
8339
+ const showCancel = binding.display?.showCancel !== false;
8340
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8341
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: panelStyle, className: binding.display?.className, children: [
8342
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8343
+ "div",
8344
+ {
8345
+ style: {
8346
+ display: "flex",
8347
+ justifyContent: "space-between",
8348
+ alignItems: "center",
8349
+ marginBottom: "16px",
8350
+ paddingBottom: "12px",
8351
+ borderBottom: `2px solid ${borderColor}`
8352
+ },
8353
+ children: [
8354
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { children: [
8355
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8356
+ "h3",
8357
+ {
8358
+ style: {
8359
+ margin: 0,
8360
+ fontSize: `${fontSize.title}px`,
8361
+ fontWeight: 600,
8362
+ fontFamily: "Arial, sans-serif",
8363
+ letterSpacing: "0.5px",
8364
+ textTransform: "uppercase",
8365
+ color: textColor
8366
+ },
8367
+ children: titleText
8368
+ }
8369
+ ),
8370
+ binding.display?.subtitle && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8371
+ "div",
8372
+ {
8373
+ style: {
8374
+ fontSize: `${fontSize.description}px`,
8375
+ color: mutedTextColor,
8376
+ marginTop: "4px",
8377
+ fontFamily: "Arial, sans-serif"
8378
+ },
8379
+ children: binding.display.subtitle
8380
+ }
8381
+ )
8382
+ ] }),
8383
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
8384
+ showControlsButton && onOpenControls && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8385
+ "button",
8386
+ {
8387
+ onClick: onOpenControls,
8388
+ title: "Open Controls",
8389
+ style: {
8390
+ minWidth: `${touchSize * 1.2}px`,
8391
+ height: touchSize * 0.7,
8392
+ backgroundColor: surfaceColor,
8393
+ color: textColor,
8394
+ border: `2px solid ${borderColor}`,
8395
+ borderRadius: "2px",
8396
+ fontSize: `${fontSize.button}px`,
8397
+ cursor: "pointer",
8398
+ fontWeight: 600,
8399
+ fontFamily: "Arial, sans-serif",
8400
+ padding: "0 8px",
8401
+ textTransform: "uppercase",
8402
+ letterSpacing: "0.5px"
8403
+ },
8404
+ children: "CTRL"
8405
+ }
8406
+ ),
8407
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8408
+ "button",
8409
+ {
8410
+ onClick: onClose,
8411
+ style: {
8412
+ width: touchSize * 0.7,
8413
+ height: touchSize * 0.7,
8414
+ backgroundColor: surfaceColor,
8415
+ color: textColor,
8416
+ border: `2px solid ${borderColor}`,
8417
+ borderRadius: "2px",
8418
+ fontSize: "18px",
8419
+ cursor: "pointer",
8420
+ fontWeight: "bold",
8421
+ fontFamily: "Arial, sans-serif"
8422
+ },
8423
+ children: "\xD7"
8424
+ }
8425
+ )
8426
+ ] })
8427
+ ]
8428
+ }
8429
+ ),
8430
+ binding.state.isDirty && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8431
+ "div",
8432
+ {
8433
+ style: {
8434
+ padding: spacing.itemPadding,
8435
+ backgroundColor: "#fff3cd",
8436
+ border: "1px solid #ffc107",
8437
+ borderRadius: "4px",
8438
+ marginBottom: spacing.marginBottom,
8439
+ fontSize: `${fontSize.description}px`,
8440
+ fontFamily: "Arial, sans-serif"
8441
+ },
8442
+ children: "Unsaved changes"
8443
+ }
8444
+ ),
8445
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { flex: 1, overflowY: "auto", marginBottom: spacing.marginBottom }, children: sortedSections.length > 0 ? sortedSections.map(renderSection) : (
8446
+ /* Render unsectioned parameters */
8447
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { children: parametersBySection._unsectioned?.map(renderParameter) })
8448
+ ) }),
8449
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
8450
+ "div",
8451
+ {
8452
+ style: {
8453
+ display: "flex",
8454
+ gap: spacing.gap,
8455
+ paddingTop: "12px",
8456
+ borderTop: `2px solid ${borderColor}`,
8457
+ flexWrap: "wrap"
8458
+ },
8459
+ children: [
8460
+ showApply && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8461
+ "button",
8462
+ {
8463
+ onClick: handleApply,
8464
+ disabled: binding.state.isSaving,
8465
+ style: {
8466
+ flex: 1,
8467
+ minWidth: "80px",
8468
+ height: `${touchSize}px`,
8469
+ backgroundColor: accentColor,
8470
+ color: "#000000",
8471
+ border: `2px solid ${borderColor}`,
8472
+ borderRadius: "2px",
8473
+ fontSize: `${fontSize.button}px`,
8474
+ fontWeight: 600,
8475
+ cursor: binding.state.isSaving ? "not-allowed" : "pointer",
8476
+ fontFamily: "Arial, sans-serif",
8477
+ textTransform: "uppercase",
8478
+ letterSpacing: "0.5px"
8479
+ },
8480
+ children: binding.state.isSaving ? "Applying..." : "Apply"
8481
+ }
8482
+ ),
8483
+ showSave && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8484
+ "button",
8485
+ {
8486
+ onClick: handleSave,
8487
+ disabled: binding.state.isSaving,
8488
+ style: {
8489
+ flex: 1,
8490
+ minWidth: "80px",
8491
+ height: `${touchSize}px`,
8492
+ backgroundColor: successColor,
8493
+ color: "#ffffff",
8494
+ border: `2px solid ${borderColor}`,
8495
+ borderRadius: "2px",
8496
+ fontSize: `${fontSize.button}px`,
8497
+ fontWeight: 600,
8498
+ cursor: binding.state.isSaving ? "not-allowed" : "pointer",
8499
+ fontFamily: "Arial, sans-serif",
8500
+ textTransform: "uppercase",
8501
+ letterSpacing: "0.5px"
8502
+ },
8503
+ children: binding.state.isSaving ? "Saving..." : "Save"
8504
+ }
8505
+ ),
8506
+ showReset && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8507
+ "button",
8508
+ {
8509
+ onClick: handleReset,
8510
+ disabled: binding.state.isSaving,
8511
+ style: {
8512
+ flex: 1,
8513
+ minWidth: "80px",
8514
+ height: `${touchSize}px`,
8515
+ backgroundColor: surfaceColor,
8516
+ color: textColor,
8517
+ border: `2px solid ${borderColor}`,
8518
+ borderRadius: "2px",
8519
+ fontSize: `${fontSize.button}px`,
8520
+ fontWeight: 600,
8521
+ cursor: binding.state.isSaving ? "not-allowed" : "pointer",
8522
+ fontFamily: "Arial, sans-serif",
8523
+ textTransform: "uppercase",
8524
+ letterSpacing: "0.5px"
8525
+ },
8526
+ children: "Reset"
8527
+ }
8528
+ ),
8529
+ showCancel && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8530
+ "button",
8531
+ {
8532
+ onClick: handleCancel,
8533
+ disabled: binding.state.isSaving,
8534
+ style: {
8535
+ flex: 1,
8536
+ minWidth: "80px",
8537
+ height: `${touchSize}px`,
8538
+ backgroundColor: surfaceColor,
8539
+ color: textColor,
8540
+ border: `2px solid ${borderColor}`,
8541
+ borderRadius: "2px",
8542
+ fontSize: `${fontSize.button}px`,
8543
+ fontWeight: 600,
8544
+ cursor: binding.state.isSaving ? "not-allowed" : "pointer",
8545
+ fontFamily: "Arial, sans-serif",
8546
+ textTransform: "uppercase",
8547
+ letterSpacing: "0.5px"
8548
+ },
8549
+ children: "Cancel"
8550
+ }
8551
+ )
8552
+ ]
8553
+ }
8554
+ )
8555
+ ] }),
8556
+ binding.state.lastSaveResult && showToast && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8557
+ "div",
8558
+ {
8559
+ style: {
8560
+ position: "fixed",
8561
+ bottom: "20px",
8562
+ ...align === "right" ? { left: "20px" } : { right: "20px" },
8563
+ backgroundColor: binding.state.lastSaveResult.success ? successColor : errorColor,
8564
+ color: "#ffffff",
8565
+ padding: "12px 20px",
8566
+ borderRadius: "4px",
8567
+ boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
8568
+ zIndex: 1e4,
8569
+ fontSize: `${fontSize.value}px`,
8570
+ fontFamily: "Arial, sans-serif",
8571
+ fontWeight: 600
8572
+ },
8573
+ children: binding.state.lastSaveResult.message || (binding.state.lastSaveResult.success ? "Success" : "Error")
8574
+ }
8575
+ )
8576
+ ] });
8577
+ }
8578
+
8579
+ // src/diagram/hooks/useSimulation.ts
8580
+ var import_react21 = require("react");
7600
8581
  function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7601
8582
  const {
7602
8583
  interval = 2e3,
@@ -7607,7 +8588,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7607
8588
  historyLength = 10,
7608
8589
  generateValue
7609
8590
  } = options;
7610
- (0, import_react19.useEffect)(() => {
8591
+ (0, import_react21.useEffect)(() => {
7611
8592
  if (!enabled) return;
7612
8593
  const intervalId = setInterval(() => {
7613
8594
  const updates = [];
@@ -7665,6 +8646,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7665
8646
  ControlPanel,
7666
8647
  DEFAULT_THRESHOLDS,
7667
8648
  DashboardCard,
8649
+ DeviceConfigPanel,
7668
8650
  DeviceControlPanel,
7669
8651
  DisplayModeToggle,
7670
8652
  EquipmentIndicator,
@@ -7697,6 +8679,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7697
8679
  ZoomButton,
7698
8680
  ZoomControls,
7699
8681
  calculateThresholdStatus,
8682
+ createConfigBinding,
7700
8683
  createControlBinding,
7701
8684
  exampleDiagram,
7702
8685
  exportDiagram,
@@ -7710,6 +8693,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7710
8693
  overlayStyles,
7711
8694
  registerSymbol,
7712
8695
  registerSymbols,
8696
+ useDeviceConfigs,
7713
8697
  useDeviceControls,
7714
8698
  useDrag,
7715
8699
  useKeyboardShortcuts,