@procaaso/alphinity-ui-components 1.0.10 → 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,
@@ -1004,13 +1007,18 @@ function DeviceControlPanel({
1004
1007
  fontScale = 1,
1005
1008
  maxHeight,
1006
1009
  maxWidth,
1010
+ toastDuration = 3e3,
1007
1011
  onModeChange,
1008
1012
  onParameterChange,
1009
1013
  onStart,
1010
1014
  onStop,
1011
- onCustomAction
1015
+ onCustomAction,
1016
+ onOpenConfig,
1017
+ showConfigButton = true,
1018
+ modeSelectionStyle = "buttons"
1012
1019
  }) {
1013
1020
  const [localParameterValues, setLocalParameterValues] = (0, import_react5.useState)({});
1021
+ const [showToast, setShowToast] = (0, import_react5.useState)(true);
1014
1022
  (0, import_react5.useEffect)(() => {
1015
1023
  if (binding) {
1016
1024
  const currentParams2 = binding.parameters[binding.state.currentMode] || [];
@@ -1021,6 +1029,15 @@ function DeviceControlPanel({
1021
1029
  setLocalParameterValues(values);
1022
1030
  }
1023
1031
  }, [binding, binding?.state.currentMode]);
1032
+ (0, import_react5.useEffect)(() => {
1033
+ if (binding?.state.lastCommandResult) {
1034
+ setShowToast(true);
1035
+ const timer = setTimeout(() => {
1036
+ setShowToast(false);
1037
+ }, toastDuration);
1038
+ return () => clearTimeout(timer);
1039
+ }
1040
+ }, [binding?.state.lastCommandResult, toastDuration]);
1024
1041
  if (!binding) return null;
1025
1042
  const currentMode = binding.state.currentMode;
1026
1043
  const currentParams = binding.parameters[currentMode] || [];
@@ -1077,14 +1094,13 @@ function DeviceControlPanel({
1077
1094
  position: "fixed",
1078
1095
  ...position === "bottom" ? {
1079
1096
  bottom: 0,
1080
- ...align === "left" ? { left: 0, right: "auto" } : align === "right" ? { right: 0, left: "auto" } : { left: 0, right: 0, marginLeft: "auto", marginRight: "auto" },
1081
- animation: "slideUp 0.3s ease-out",
1082
- 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"
1083
1099
  } : {
1084
1100
  right: 0,
1085
1101
  top: 0,
1086
1102
  bottom: 0,
1087
- width: "400px",
1103
+ width: maxWidth || "400px",
1088
1104
  animation: "slideIn 0.3s ease-out"
1089
1105
  },
1090
1106
  backgroundColor,
@@ -1099,442 +1115,534 @@ function DeviceControlPanel({
1099
1115
  ...binding.display?.style
1100
1116
  };
1101
1117
  const titleText = binding.display?.title || binding.deviceName || binding.tag || binding.nodeId;
1102
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: panelStyle, className: binding.display?.className, children: [
1103
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1104
- "div",
1105
- {
1106
- style: {
1107
- display: "flex",
1108
- justifyContent: "space-between",
1109
- alignItems: "center",
1110
- marginBottom: "16px",
1111
- paddingBottom: "12px",
1112
- borderBottom: `2px solid ${borderColor}`
1113
- },
1114
- children: [
1115
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1118
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1119
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: panelStyle, className: binding.display?.className, children: [
1120
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1121
+ "div",
1122
+ {
1123
+ style: {
1124
+ display: "flex",
1125
+ justifyContent: "space-between",
1126
+ alignItems: "center",
1127
+ marginBottom: "16px",
1128
+ paddingBottom: "12px",
1129
+ borderBottom: `2px solid ${borderColor}`
1130
+ },
1131
+ children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1133
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1134
+ "h3",
1135
+ {
1136
+ style: {
1137
+ margin: 0,
1138
+ fontSize: `${fontSize.title}px`,
1139
+ fontWeight: 600,
1140
+ fontFamily: "Arial, sans-serif",
1141
+ letterSpacing: "0.5px",
1142
+ textTransform: "uppercase",
1143
+ color: textColor
1144
+ },
1145
+ children: titleText
1146
+ }
1147
+ ),
1148
+ binding.deviceType && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1149
+ "div",
1150
+ {
1151
+ style: {
1152
+ fontSize: "11px",
1153
+ color: mutedTextColor,
1154
+ marginTop: "4px",
1155
+ fontFamily: "Arial, sans-serif"
1156
+ },
1157
+ children: binding.deviceType
1158
+ }
1159
+ )
1160
+ ] }),
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
+ ] })
1203
+ ]
1204
+ }
1205
+ ),
1206
+ binding.state.status && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1207
+ "div",
1208
+ {
1209
+ style: {
1210
+ display: "flex",
1211
+ alignItems: "center",
1212
+ gap: "12px",
1213
+ padding: spacing.itemPadding,
1214
+ backgroundColor: surfaceColor,
1215
+ border: `1px solid ${borderColor}`,
1216
+ borderRadius: "6px",
1217
+ marginBottom: spacing.marginBottom
1218
+ },
1219
+ children: [
1116
1220
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1117
- "h3",
1118
- {
1119
- style: {
1120
- margin: 0,
1121
- fontSize: `${fontSize.title}px`,
1122
- fontWeight: 600,
1123
- fontFamily: "Arial, sans-serif",
1124
- letterSpacing: "0.5px",
1125
- textTransform: "uppercase",
1126
- color: textColor
1127
- },
1128
- children: titleText
1129
- }
1130
- ),
1131
- binding.deviceType && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1132
1221
  "div",
1133
1222
  {
1134
1223
  style: {
1135
- fontSize: "11px",
1136
- color: mutedTextColor,
1137
- marginTop: "4px",
1138
- fontFamily: "Arial, sans-serif"
1139
- },
1140
- children: binding.deviceType
1141
- }
1142
- )
1143
- ] }),
1144
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1145
- "button",
1146
- {
1147
- onClick: onClose,
1148
- style: {
1149
- width: touchSize * 0.7,
1150
- height: touchSize * 0.7,
1151
- backgroundColor: surfaceColor,
1152
- color: textColor,
1153
- border: `2px solid ${borderColor}`,
1154
- borderRadius: "2px",
1155
- fontSize: "18px",
1156
- cursor: "pointer",
1157
- fontWeight: "bold",
1158
- fontFamily: "Arial, sans-serif"
1159
- },
1160
- children: "\xD7"
1161
- }
1162
- )
1163
- ]
1164
- }
1165
- ),
1166
- binding.state.status && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1167
- "div",
1168
- {
1169
- style: {
1170
- display: "flex",
1171
- alignItems: "center",
1172
- gap: "12px",
1173
- padding: spacing.itemPadding,
1174
- backgroundColor: surfaceColor,
1175
- border: `1px solid ${borderColor}`,
1176
- borderRadius: "6px",
1177
- marginBottom: spacing.marginBottom
1178
- },
1179
- children: [
1180
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1181
- "div",
1182
- {
1183
- style: {
1184
- width: "10px",
1185
- height: "10px",
1186
- borderRadius: "50%",
1187
- backgroundColor: statusColor,
1188
- border: "1px solid #333"
1224
+ width: "10px",
1225
+ height: "10px",
1226
+ borderRadius: "50%",
1227
+ backgroundColor: statusColor,
1228
+ border: "1px solid #333"
1229
+ }
1189
1230
  }
1190
- }
1191
- ),
1192
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { flex: 1 }, children: [
1193
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1231
+ ),
1232
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { flex: 1 }, children: [
1233
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1234
+ "div",
1235
+ {
1236
+ style: {
1237
+ fontWeight: 600,
1238
+ textTransform: "uppercase",
1239
+ fontSize: `${fontSize.label}px`,
1240
+ fontFamily: "Arial, sans-serif",
1241
+ letterSpacing: "0.5px",
1242
+ color: textColor
1243
+ },
1244
+ children: binding.state.status
1245
+ }
1246
+ ),
1247
+ binding.state.statusMessage && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1248
+ "div",
1249
+ {
1250
+ style: {
1251
+ fontSize: "10px",
1252
+ color: mutedTextColor,
1253
+ marginTop: "2px",
1254
+ fontFamily: "Arial, sans-serif"
1255
+ },
1256
+ children: binding.state.statusMessage
1257
+ }
1258
+ )
1259
+ ] }),
1260
+ binding.state.isRunning !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1194
1261
  "div",
1195
1262
  {
1196
1263
  style: {
1264
+ padding: "4px 10px",
1265
+ backgroundColor: binding.state.isRunning ? selectedColor : surfaceColor,
1266
+ border: `1px solid ${borderColor}`,
1267
+ borderRadius: "2px",
1268
+ fontSize: "10px",
1197
1269
  fontWeight: 600,
1198
- textTransform: "uppercase",
1199
- fontSize: `${fontSize.label}px`,
1200
1270
  fontFamily: "Arial, sans-serif",
1201
1271
  letterSpacing: "0.5px",
1202
1272
  color: textColor
1203
1273
  },
1204
- children: binding.state.status
1205
- }
1206
- ),
1207
- binding.state.statusMessage && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1208
- "div",
1209
- {
1210
- style: {
1211
- fontSize: "10px",
1212
- color: mutedTextColor,
1213
- marginTop: "2px",
1214
- fontFamily: "Arial, sans-serif"
1215
- },
1216
- children: binding.state.statusMessage
1274
+ children: binding.state.isRunning ? "RUNNING" : "STOPPED"
1217
1275
  }
1218
1276
  )
1219
- ] }),
1220
- binding.state.isRunning !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1221
- "div",
1222
- {
1223
- style: {
1224
- padding: "4px 10px",
1225
- backgroundColor: binding.state.isRunning ? selectedColor : surfaceColor,
1226
- border: `1px solid ${borderColor}`,
1227
- borderRadius: "2px",
1228
- fontSize: "10px",
1229
- fontWeight: 600,
1230
- fontFamily: "Arial, sans-serif",
1231
- letterSpacing: "0.5px",
1232
- color: textColor
1233
- },
1234
- children: binding.state.isRunning ? "RUNNING" : "STOPPED"
1235
- }
1236
- )
1237
- ]
1238
- }
1239
- ),
1240
- binding.display?.showModeSelector !== false && binding.modes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginBottom: "16px" }, children: [
1241
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1242
- "label",
1243
- {
1244
- style: {
1245
- display: "block",
1246
- marginBottom: "8px",
1247
- fontSize: "11px",
1248
- fontWeight: 600,
1249
- textTransform: "uppercase",
1250
- letterSpacing: "0.5px",
1251
- fontFamily: "Arial, sans-serif",
1252
- color: mutedTextColor
1253
- },
1254
- children: "Mode"
1277
+ ]
1255
1278
  }
1256
1279
  ),
1257
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", gap: spacing.gap, flexWrap: "wrap" }, children: binding.modes.map((mode) => {
1258
- const isSelected = mode.value === currentMode;
1259
- const isEnabled = mode.enabled !== false;
1260
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1261
- "button",
1280
+ binding.display?.showModeSelector !== false && binding.modes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginBottom: "16px" }, children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1282
+ "label",
1262
1283
  {
1263
- onClick: () => isEnabled && handleModeChange(mode.value),
1264
- disabled: !isEnabled,
1265
1284
  style: {
1266
- flex: 1,
1267
- minWidth: "80px",
1285
+ display: "block",
1286
+ marginBottom: "8px",
1287
+ fontSize: "11px",
1288
+ fontWeight: 600,
1289
+ textTransform: "uppercase",
1290
+ letterSpacing: "0.5px",
1291
+ fontFamily: "Arial, sans-serif",
1292
+ color: mutedTextColor
1293
+ },
1294
+ children: "Mode"
1295
+ }
1296
+ ),
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%",
1268
1304
  height: touchSize,
1269
- backgroundColor: isSelected ? selectedColor : surfaceColor,
1270
- color: isSelected ? "#000000" : textColor,
1271
- border: `1px solid ${isSelected ? "#b4d0fe" : borderColor}`,
1305
+ backgroundColor: surfaceColor,
1306
+ color: textColor,
1307
+ border: `1px solid ${borderColor}`,
1272
1308
  borderRadius: "6px",
1273
1309
  fontSize: `${fontSize.label}px`,
1274
1310
  fontWeight: 600,
1275
1311
  fontFamily: "Arial, sans-serif",
1276
1312
  letterSpacing: "0.5px",
1277
1313
  textTransform: "uppercase",
1278
- cursor: isEnabled ? "pointer" : "not-allowed",
1279
- opacity: isEnabled ? 1 : 0.4
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)(
1321
+ "div",
1322
+ {
1323
+ style: {
1324
+ display: "flex",
1325
+ gap: spacing.gap,
1326
+ overflowX: "auto",
1327
+ overflowY: "hidden",
1328
+ scrollbarWidth: "thin",
1329
+ scrollbarColor: `${borderColor} transparent`,
1330
+ paddingBottom: "4px",
1331
+ marginBottom: "-4px"
1332
+ },
1333
+ children: binding.modes.map((mode) => {
1334
+ const isSelected = mode.value === currentMode;
1335
+ const isEnabled = mode.enabled !== false;
1336
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1337
+ "button",
1338
+ {
1339
+ onClick: () => isEnabled && handleModeChange(mode.value),
1340
+ disabled: !isEnabled,
1341
+ style: {
1342
+ flex: "0 0 auto",
1343
+ minWidth: "90px",
1344
+ height: touchSize,
1345
+ backgroundColor: isSelected ? selectedColor : surfaceColor,
1346
+ color: isSelected ? "#000000" : textColor,
1347
+ border: `1px solid ${isSelected ? "#b4d0fe" : borderColor}`,
1348
+ borderRadius: "6px",
1349
+ fontSize: `${fontSize.label}px`,
1350
+ fontWeight: 600,
1351
+ fontFamily: "Arial, sans-serif",
1352
+ letterSpacing: "0.5px",
1353
+ textTransform: "uppercase",
1354
+ cursor: isEnabled ? "pointer" : "not-allowed",
1355
+ opacity: isEnabled ? 1 : 0.4,
1356
+ whiteSpace: "nowrap"
1357
+ },
1358
+ children: mode.label
1359
+ },
1360
+ mode.value
1361
+ );
1362
+ })
1363
+ }
1364
+ )
1365
+ ] }),
1366
+ currentParams.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginBottom: spacing.marginBottom }, children: [
1367
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1368
+ "label",
1369
+ {
1370
+ style: {
1371
+ display: "block",
1372
+ marginBottom: spacing.gap,
1373
+ fontSize: `${fontSize.label}px`,
1374
+ fontWeight: 600,
1375
+ textTransform: "uppercase",
1376
+ letterSpacing: "0.5px",
1377
+ fontFamily: "Arial, sans-serif",
1378
+ color: mutedTextColor
1379
+ },
1380
+ children: "Parameters"
1381
+ }
1382
+ ),
1383
+ currentParams.map((param) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1384
+ "div",
1385
+ {
1386
+ style: {
1387
+ marginBottom: spacing.gap,
1388
+ padding: spacing.itemPadding,
1389
+ backgroundColor: surfaceColor,
1390
+ border: `1px solid ${borderColor}`,
1391
+ borderRadius: "6px"
1280
1392
  },
1281
- children: mode.label
1393
+ children: [
1394
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1395
+ "div",
1396
+ {
1397
+ style: {
1398
+ display: "flex",
1399
+ justifyContent: "space-between",
1400
+ marginBottom: spacing.gap
1401
+ },
1402
+ children: [
1403
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1404
+ "span",
1405
+ {
1406
+ style: {
1407
+ fontSize: `${fontSize.label}px`,
1408
+ fontWeight: 600,
1409
+ textTransform: "uppercase",
1410
+ letterSpacing: "0.5px",
1411
+ fontFamily: "Arial, sans-serif",
1412
+ color: mutedTextColor
1413
+ },
1414
+ children: param.label
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1418
+ "span",
1419
+ {
1420
+ style: {
1421
+ fontSize: `${fontSize.value}px`,
1422
+ fontWeight: 700,
1423
+ fontFamily: "Arial, sans-serif",
1424
+ color: textColor
1425
+ },
1426
+ children: [
1427
+ localParameterValues[param.id] ?? param.value,
1428
+ " ",
1429
+ param.unit && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { color: mutedTextColor, fontSize: "10px", fontWeight: 600 }, children: param.unit })
1430
+ ]
1431
+ }
1432
+ )
1433
+ ]
1434
+ }
1435
+ ),
1436
+ param.type === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1437
+ "button",
1438
+ {
1439
+ onClick: () => {
1440
+ const newValue = !localParameterValues[param.id];
1441
+ handleParameterInput(param.id, newValue);
1442
+ handleParameterCommit(param.id, newValue);
1443
+ },
1444
+ disabled: param.readOnly,
1445
+ style: {
1446
+ width: "100%",
1447
+ height: touchSize,
1448
+ backgroundColor: localParameterValues[param.id] ? "#3f3f46" : surfaceColor,
1449
+ color: localParameterValues[param.id] ? textColor : mutedTextColor,
1450
+ border: `1px solid ${localParameterValues[param.id] ? "#52525b" : borderColor}`,
1451
+ borderRadius: "4px",
1452
+ fontSize: "12px",
1453
+ fontWeight: 600,
1454
+ fontFamily: "monospace",
1455
+ letterSpacing: "0.5px",
1456
+ cursor: param.readOnly ? "not-allowed" : "pointer",
1457
+ transition: "all 0.15s"
1458
+ },
1459
+ children: localParameterValues[param.id] ? "ON" : "OFF"
1460
+ }
1461
+ ) : param.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1462
+ "select",
1463
+ {
1464
+ value: localParameterValues[param.id] || param.value,
1465
+ onChange: (e) => {
1466
+ handleParameterInput(param.id, e.target.value);
1467
+ handleParameterCommit(param.id, e.target.value);
1468
+ },
1469
+ disabled: param.readOnly,
1470
+ style: {
1471
+ width: "100%",
1472
+ height: touchSize,
1473
+ backgroundColor: surfaceColor,
1474
+ color: textColor,
1475
+ border: `1px solid ${borderColor}`,
1476
+ borderRadius: "4px",
1477
+ padding: "0 12px",
1478
+ fontSize: "12px",
1479
+ fontFamily: "monospace",
1480
+ cursor: "pointer"
1481
+ },
1482
+ children: param.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
1483
+ }
1484
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1485
+ "input",
1486
+ {
1487
+ type: param.type === "number" ? "number" : "text",
1488
+ value: localParameterValues[param.id] ?? param.value,
1489
+ onChange: (e) => handleParameterInput(
1490
+ param.id,
1491
+ param.type === "number" ? +e.target.value : e.target.value
1492
+ ),
1493
+ onBlur: () => handleParameterCommit(param.id, localParameterValues[param.id]),
1494
+ disabled: param.readOnly,
1495
+ min: param.min,
1496
+ max: param.max,
1497
+ step: param.step,
1498
+ style: {
1499
+ width: "100%",
1500
+ height: touchSize,
1501
+ backgroundColor: "#ffffff",
1502
+ color: textColor,
1503
+ border: `1px solid ${borderColor}`,
1504
+ borderRadius: "6px",
1505
+ padding: "0 12px",
1506
+ fontSize: `${fontSize.input}px`,
1507
+ fontFamily: "Arial, sans-serif",
1508
+ fontWeight: 600
1509
+ }
1510
+ }
1511
+ )
1512
+ ]
1282
1513
  },
1283
- mode.value
1284
- );
1285
- }) })
1286
- ] }),
1287
- currentParams.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginBottom: spacing.marginBottom }, children: [
1288
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1289
- "label",
1514
+ param.id
1515
+ ))
1516
+ ] }),
1517
+ binding.display?.showStartStop !== false && (activeCapabilities?.canStart || activeCapabilities?.canStop) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: spacing.gap, marginBottom: spacing.marginBottom }, children: [
1518
+ activeCapabilities?.canStart && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1519
+ "button",
1520
+ {
1521
+ onClick: onStart,
1522
+ disabled: binding.state.isRunning,
1523
+ style: {
1524
+ flex: 1,
1525
+ height: touchSize,
1526
+ backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1527
+ color: "#ffffff",
1528
+ border: "none",
1529
+ borderRadius: "6px",
1530
+ fontSize: "12px",
1531
+ fontWeight: 700,
1532
+ fontFamily: "Arial, sans-serif",
1533
+ letterSpacing: "0.5px",
1534
+ cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1535
+ opacity: binding.state.isRunning ? 0.5 : 1
1536
+ },
1537
+ children: "START"
1538
+ }
1539
+ ),
1540
+ activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1541
+ "button",
1542
+ {
1543
+ onClick: onStop,
1544
+ disabled: !binding.state.isRunning,
1545
+ style: {
1546
+ flex: 1,
1547
+ height: touchSize,
1548
+ backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1549
+ color: "#ffffff",
1550
+ border: "none",
1551
+ borderRadius: "6px",
1552
+ fontSize: "12px",
1553
+ fontWeight: 700,
1554
+ fontFamily: "Arial, sans-serif",
1555
+ letterSpacing: "0.5px",
1556
+ cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1557
+ opacity: !binding.state.isRunning ? 0.5 : 1
1558
+ },
1559
+ children: "STOP"
1560
+ }
1561
+ )
1562
+ ] }),
1563
+ activeCapabilities?.customActions && activeCapabilities.customActions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: activeCapabilities.customActions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1564
+ "button",
1290
1565
  {
1566
+ onClick: () => onCustomAction?.(action.id),
1291
1567
  style: {
1292
- display: "block",
1293
- marginBottom: spacing.gap,
1294
- fontSize: `${fontSize.label}px`,
1568
+ flex: 1,
1569
+ minWidth: "120px",
1570
+ height: touchSize,
1571
+ backgroundColor: surfaceColor,
1572
+ color: textColor,
1573
+ border: `1px solid ${borderColor}`,
1574
+ borderRadius: "6px",
1575
+ fontSize: "11px",
1295
1576
  fontWeight: 600,
1296
- textTransform: "uppercase",
1577
+ fontFamily: "monospace",
1297
1578
  letterSpacing: "0.5px",
1298
- fontFamily: "Arial, sans-serif",
1299
- color: mutedTextColor
1300
- },
1301
- children: "Parameters"
1302
- }
1303
- ),
1304
- currentParams.map((param) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1305
- "div",
1306
- {
1307
- style: {
1308
- marginBottom: spacing.gap,
1309
- padding: spacing.itemPadding,
1310
- backgroundColor: surfaceColor,
1311
- border: `1px solid ${borderColor}`,
1312
- borderRadius: "6px"
1579
+ textTransform: "uppercase",
1580
+ cursor: "pointer",
1581
+ transition: "all 0.15s"
1313
1582
  },
1314
1583
  children: [
1315
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1316
- "div",
1317
- {
1318
- style: {
1319
- display: "flex",
1320
- justifyContent: "space-between",
1321
- marginBottom: spacing.gap
1322
- },
1323
- children: [
1324
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1325
- "span",
1326
- {
1327
- style: {
1328
- fontSize: `${fontSize.label}px`,
1329
- fontWeight: 600,
1330
- textTransform: "uppercase",
1331
- letterSpacing: "0.5px",
1332
- fontFamily: "Arial, sans-serif",
1333
- color: mutedTextColor
1334
- },
1335
- children: param.label
1336
- }
1337
- ),
1338
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1339
- "span",
1340
- {
1341
- style: {
1342
- fontSize: `${fontSize.value}px`,
1343
- fontWeight: 700,
1344
- fontFamily: "Arial, sans-serif",
1345
- color: textColor
1346
- },
1347
- children: [
1348
- localParameterValues[param.id] ?? param.value,
1349
- " ",
1350
- param.unit && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { color: mutedTextColor, fontSize: "10px", fontWeight: 600 }, children: param.unit })
1351
- ]
1352
- }
1353
- )
1354
- ]
1355
- }
1356
- ),
1357
- param.type === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1358
- "button",
1359
- {
1360
- onClick: () => {
1361
- const newValue = !localParameterValues[param.id];
1362
- handleParameterInput(param.id, newValue);
1363
- handleParameterCommit(param.id, newValue);
1364
- },
1365
- disabled: param.readOnly,
1366
- style: {
1367
- width: "100%",
1368
- height: touchSize,
1369
- backgroundColor: localParameterValues[param.id] ? "#3f3f46" : surfaceColor,
1370
- color: localParameterValues[param.id] ? textColor : mutedTextColor,
1371
- border: `1px solid ${localParameterValues[param.id] ? "#52525b" : borderColor}`,
1372
- borderRadius: "4px",
1373
- fontSize: "12px",
1374
- fontWeight: 600,
1375
- fontFamily: "monospace",
1376
- letterSpacing: "0.5px",
1377
- cursor: param.readOnly ? "not-allowed" : "pointer",
1378
- transition: "all 0.15s"
1379
- },
1380
- children: localParameterValues[param.id] ? "ON" : "OFF"
1381
- }
1382
- ) : param.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1383
- "select",
1384
- {
1385
- value: localParameterValues[param.id] || param.value,
1386
- onChange: (e) => {
1387
- handleParameterInput(param.id, e.target.value);
1388
- handleParameterCommit(param.id, e.target.value);
1389
- },
1390
- disabled: param.readOnly,
1391
- style: {
1392
- width: "100%",
1393
- height: touchSize,
1394
- backgroundColor: surfaceColor,
1395
- color: textColor,
1396
- border: `1px solid ${borderColor}`,
1397
- borderRadius: "4px",
1398
- padding: "0 12px",
1399
- fontSize: "12px",
1400
- fontFamily: "monospace",
1401
- cursor: "pointer"
1402
- },
1403
- children: param.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
1404
- }
1405
- ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1406
- "input",
1407
- {
1408
- type: param.type === "number" ? "number" : "text",
1409
- value: localParameterValues[param.id] ?? param.value,
1410
- onChange: (e) => handleParameterInput(
1411
- param.id,
1412
- param.type === "number" ? +e.target.value : e.target.value
1413
- ),
1414
- onBlur: () => handleParameterCommit(param.id, localParameterValues[param.id]),
1415
- disabled: param.readOnly,
1416
- min: param.min,
1417
- max: param.max,
1418
- step: param.step,
1419
- style: {
1420
- width: "100%",
1421
- height: touchSize,
1422
- backgroundColor: "#ffffff",
1423
- color: textColor,
1424
- border: `1px solid ${borderColor}`,
1425
- borderRadius: "6px",
1426
- padding: "0 12px",
1427
- fontSize: `${fontSize.input}px`,
1428
- fontFamily: "Arial, sans-serif",
1429
- fontWeight: 600
1430
- }
1431
- }
1432
- )
1584
+ action.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { marginRight: "6px" }, children: action.icon }),
1585
+ action.label
1433
1586
  ]
1434
1587
  },
1435
- param.id
1436
- ))
1437
- ] }),
1438
- binding.display?.showStartStop !== false && (activeCapabilities?.canStart || activeCapabilities?.canStop) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: spacing.gap, marginBottom: spacing.marginBottom }, children: [
1439
- activeCapabilities?.canStart && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1440
- "button",
1441
- {
1442
- onClick: onStart,
1443
- disabled: binding.state.isRunning,
1444
- style: {
1445
- flex: 1,
1446
- height: touchSize,
1447
- backgroundColor: binding.state.isRunning ? "#a3a3a3" : "#10b981",
1448
- color: "#ffffff",
1449
- border: "none",
1450
- borderRadius: "6px",
1451
- fontSize: "12px",
1452
- fontWeight: 700,
1453
- fontFamily: "Arial, sans-serif",
1454
- letterSpacing: "0.5px",
1455
- cursor: binding.state.isRunning ? "not-allowed" : "pointer",
1456
- opacity: binding.state.isRunning ? 0.5 : 1
1457
- },
1458
- children: "START"
1459
- }
1460
- ),
1461
- activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1462
- "button",
1463
- {
1464
- onClick: onStop,
1465
- disabled: !binding.state.isRunning,
1466
- style: {
1467
- flex: 1,
1468
- height: touchSize,
1469
- backgroundColor: !binding.state.isRunning ? "#a3a3a3" : "#ef4444",
1470
- color: "#ffffff",
1471
- border: "none",
1472
- borderRadius: "6px",
1473
- fontSize: "12px",
1474
- fontWeight: 700,
1475
- fontFamily: "Arial, sans-serif",
1476
- letterSpacing: "0.5px",
1477
- cursor: !binding.state.isRunning ? "not-allowed" : "pointer",
1478
- opacity: !binding.state.isRunning ? 0.5 : 1
1479
- },
1480
- children: "STOP"
1481
- }
1482
- )
1588
+ action.id
1589
+ )) })
1483
1590
  ] }),
1484
- activeCapabilities?.customActions && activeCapabilities.customActions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: activeCapabilities.customActions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1485
- "button",
1486
- {
1487
- onClick: () => onCustomAction?.(action.id),
1488
- style: {
1489
- flex: 1,
1490
- minWidth: "120px",
1491
- height: touchSize,
1492
- backgroundColor: surfaceColor,
1493
- color: textColor,
1494
- border: `1px solid ${borderColor}`,
1495
- borderRadius: "6px",
1496
- fontSize: "11px",
1497
- fontWeight: 600,
1498
- fontFamily: "monospace",
1499
- letterSpacing: "0.5px",
1500
- textTransform: "uppercase",
1501
- cursor: "pointer",
1502
- transition: "all 0.15s"
1503
- },
1504
- children: [
1505
- action.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { marginRight: "6px" }, children: action.icon }),
1506
- action.label
1507
- ]
1508
- },
1509
- action.id
1510
- )) }),
1511
- binding.state.lastCommandResult && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1591
+ binding.state.lastCommandResult && showToast && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1512
1592
  "div",
1513
1593
  {
1514
1594
  style: {
1515
- marginTop: "16px",
1516
- padding: "10px 12px",
1517
- backgroundColor: surfaceColor,
1518
- border: `1px solid ${binding.state.lastCommandResult.success ? "#52525b" : borderColor}`,
1519
- borderRadius: "4px",
1520
- fontSize: "11px",
1521
- fontFamily: "monospace",
1522
- color: binding.state.lastCommandResult.success ? textColor : mutedTextColor
1595
+ position: "fixed",
1596
+ bottom: "20px",
1597
+ ...position === "bottom" && align === "right" ? { left: "20px", right: "auto" } : { right: "20px", left: "auto" },
1598
+ // Default to right
1599
+ maxWidth: "400px",
1600
+ padding: "12px 16px",
1601
+ backgroundColor: binding.state.lastCommandResult.success ? "#10b981" : "#ef4444",
1602
+ color: "#ffffff",
1603
+ border: "none",
1604
+ borderRadius: "8px",
1605
+ fontSize: `${fontSize.label}px`,
1606
+ fontFamily: "Arial, sans-serif",
1607
+ fontWeight: 600,
1608
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1609
+ zIndex: 1e4,
1610
+ animation: position === "bottom" && align === "right" ? "slideInToastLeft 0.3s ease-out" : "slideInToast 0.3s ease-out"
1523
1611
  },
1524
1612
  children: binding.state.lastCommandResult.message
1525
1613
  }
1526
1614
  ),
1527
1615
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("style", { children: `
1528
- @keyframes slideUp {
1529
- from { transform: translateY(100%); }
1530
- to { transform: translateY(0); }
1616
+ @keyframes slideInToast {
1617
+ from {
1618
+ opacity: 0;
1619
+ transform: translateX(20px);
1620
+ }
1621
+ to {
1622
+ opacity: 1;
1623
+ transform: translateX(0);
1624
+ }
1625
+ }
1626
+ @keyframes slideInToastLeft {
1627
+ from {
1628
+ opacity: 0;
1629
+ transform: translateX(-20px);
1531
1630
  }
1532
- @keyframes slideIn {
1533
- from { transform: translateX(100%); }
1534
- to { transform: translateX(0); }
1631
+ to {
1632
+ opacity: 1;
1633
+ transform: translateX(0);
1535
1634
  }
1536
- ` })
1537
- ] }) });
1635
+ }
1636
+ @keyframes slideUp {
1637
+ from { transform: translateY(100%); }
1638
+ to { transform: translateY(0); }
1639
+ }
1640
+ @keyframes slideIn {
1641
+ from { transform: translateX(100%); }
1642
+ to { transform: translateX(0); }
1643
+ }
1644
+ ` })
1645
+ ] });
1538
1646
  }
1539
1647
 
1540
1648
  // src/components/layout/FullscreenWorkspace/FullscreenWorkspace.tsx
@@ -7538,8 +7646,938 @@ function useDeviceControls(initialBindings) {
7538
7646
  };
7539
7647
  }
7540
7648
 
7541
- // 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
7542
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");
7543
8581
  function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7544
8582
  const {
7545
8583
  interval = 2e3,
@@ -7550,7 +8588,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7550
8588
  historyLength = 10,
7551
8589
  generateValue
7552
8590
  } = options;
7553
- (0, import_react19.useEffect)(() => {
8591
+ (0, import_react21.useEffect)(() => {
7554
8592
  if (!enabled) return;
7555
8593
  const intervalId = setInterval(() => {
7556
8594
  const updates = [];
@@ -7608,6 +8646,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7608
8646
  ControlPanel,
7609
8647
  DEFAULT_THRESHOLDS,
7610
8648
  DashboardCard,
8649
+ DeviceConfigPanel,
7611
8650
  DeviceControlPanel,
7612
8651
  DisplayModeToggle,
7613
8652
  EquipmentIndicator,
@@ -7640,6 +8679,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7640
8679
  ZoomButton,
7641
8680
  ZoomControls,
7642
8681
  calculateThresholdStatus,
8682
+ createConfigBinding,
7643
8683
  createControlBinding,
7644
8684
  exampleDiagram,
7645
8685
  exportDiagram,
@@ -7653,6 +8693,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
7653
8693
  overlayStyles,
7654
8694
  registerSymbol,
7655
8695
  registerSymbols,
8696
+ useDeviceConfigs,
7656
8697
  useDeviceControls,
7657
8698
  useDrag,
7658
8699
  useKeyboardShortcuts,