@procaaso/alphinity-ui-components 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Button: () => Button,
34
+ COLOR_PRESETS: () => COLOR_PRESETS,
34
35
  CardHeader: () => CardHeader,
35
36
  CardUnit: () => CardUnit,
36
37
  CardValue: () => CardValue,
@@ -44,6 +45,7 @@ __export(index_exports, {
44
45
  DisplayModeToggle: () => DisplayModeToggle,
45
46
  EquipmentIndicator: () => EquipmentIndicator,
46
47
  EquipmentIndicatorWithSparkline: () => EquipmentIndicatorWithSparkline,
48
+ FORCED_MARK_COLOR: () => FORCED_MARK_COLOR,
47
49
  FixedSVGContainer: () => FixedSVGContainer,
48
50
  FooterPanel: () => FooterPanel,
49
51
  FullscreenContainer: () => FullscreenContainer,
@@ -59,6 +61,7 @@ __export(index_exports, {
59
61
  PanelTabs: () => PanelTabs,
60
62
  RightPanel: () => RightPanel,
61
63
  RightToggleButton: () => RightToggleButton,
64
+ STATUS_LED_APPEARANCE: () => STATUS_LED_APPEARANCE,
62
65
  SVGArea: () => SVGArea,
63
66
  SVGLockedOverlay: () => SVGLockedOverlay,
64
67
  ScrollableSVGWrapper: () => ScrollableSVGWrapper,
@@ -69,6 +72,8 @@ __export(index_exports, {
69
72
  ThemeProvider: () => ThemeProvider,
70
73
  ThemeToggle: () => ThemeToggle,
71
74
  TrendLine: () => TrendLine,
75
+ UNKNOWN_MARK_COLOR: () => UNKNOWN_MARK_COLOR,
76
+ UNKNOWN_MARK_COLOR_DARK: () => UNKNOWN_MARK_COLOR_DARK,
72
77
  ValueEntry: () => ValueEntry,
73
78
  ZoomButton: () => ZoomButton,
74
79
  ZoomControls: () => ZoomControls,
@@ -78,6 +83,8 @@ __export(index_exports, {
78
83
  exampleDiagram: () => exampleDiagram,
79
84
  exportDiagram: () => exportDiagram,
80
85
  generateRoutePoints: () => generateRoutePoints,
86
+ getAppearanceDefaults: () => getAppearanceDefaults,
87
+ getAppearanceSchema: () => getAppearanceSchema,
81
88
  getAvailableSymbols: () => getAvailableSymbols,
82
89
  getPortWorldPosition: () => getPortWorldPosition,
83
90
  getSymbolDefinition: () => getSymbolDefinition,
@@ -85,9 +92,12 @@ __export(index_exports, {
85
92
  mockSymbolLibrary: () => mockSymbolLibrary,
86
93
  nodeHasPort: () => nodeHasPort,
87
94
  overlayStyles: () => overlayStyles,
95
+ readAppearanceValue: () => readAppearanceValue,
88
96
  registerSymbol: () => registerSymbol,
89
97
  registerSymbols: () => registerSymbols,
98
+ resolvePresetColor: () => resolvePresetColor,
90
99
  resolveStatusColor: () => resolveStatusColor,
100
+ useBooleanIndicators: () => useBooleanIndicators,
91
101
  useDeviceConfigs: () => useDeviceConfigs,
92
102
  useDeviceControls: () => useDeviceControls,
93
103
  useDrag: () => useDrag,
@@ -98,7 +108,8 @@ __export(index_exports, {
98
108
  useTelemetryStatus: () => useTelemetryStatus,
99
109
  useTheme: () => useTheme,
100
110
  useViewBox: () => useViewBox,
101
- validateDiagram: () => validateDiagram
111
+ validateDiagram: () => validateDiagram,
112
+ writeAppearanceValue: () => writeAppearanceValue
102
113
  });
103
114
  module.exports = __toCommonJS(index_exports);
104
115
 
@@ -278,6 +289,8 @@ var ValueEntry = ({
278
289
  inputType = "numeric",
279
290
  label,
280
291
  readOnly = false,
292
+ forced = false,
293
+ realValue,
281
294
  className = "",
282
295
  disabled,
283
296
  ...props
@@ -328,9 +341,14 @@ var ValueEntry = ({
328
341
  const unitClass = unit ? "ui-value-entry--with-unit" : "";
329
342
  const currentStatus = disabled || readOnly ? "disabled" : status;
330
343
  const statusVariantClass = currentStatus ? `ui-value-entry--${currentStatus}` : "";
344
+ const forcedClass = forced ? "ui-value-entry--forced" : "";
345
+ const hasRealValue = forced && realValue !== void 0;
331
346
  const displayText = isEditing ? displayValue : inputType === "numeric" ? Number(value).toFixed(precision) : String(value);
332
347
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ui-value-entry-container", children: [
333
- label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "ui-value-entry-label", children: label }),
348
+ label && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "ui-value-entry-label", children: [
349
+ label,
350
+ forced && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "ui-value-entry-forced-badge", "aria-label": "Forced", children: "F" })
351
+ ] }),
334
352
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ui-value-entry-wrapper", children: [
335
353
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
336
354
  "input",
@@ -343,19 +361,71 @@ var ValueEntry = ({
343
361
  onKeyDown: handleKeyDown,
344
362
  disabled,
345
363
  readOnly,
346
- className: `${baseClass} ${statusVariantClass} ${unitClass} ${className}`.trim(),
364
+ className: `${baseClass} ${statusVariantClass} ${forcedClass} ${unitClass} ${className}`.trim(),
347
365
  ...props
348
366
  }
349
367
  ),
350
368
  unit && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "ui-value-entry-unit", children: unit }),
351
369
  status === "alarm" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "ui-value-entry-badge ui-value-entry-badge--alarm" }),
352
- status === "warning" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "ui-value-entry-badge ui-value-entry-badge--warning" })
370
+ status === "warning" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "ui-value-entry-badge ui-value-entry-badge--warning" }),
371
+ !label && forced && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "ui-value-entry-forced-badge", "aria-label": "Forced", children: "F" })
372
+ ] }),
373
+ hasRealValue && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ui-value-entry-real-value", children: [
374
+ "REAL ",
375
+ String(realValue),
376
+ unit ? ` ${unit}` : ""
353
377
  ] })
354
378
  ] });
355
379
  };
356
380
 
357
381
  // src/components/ValueEntry/LegacyValueEntry.tsx
358
382
  var import_react3 = require("react");
383
+
384
+ // src/theme/statusColors.ts
385
+ var DEFAULT_STATUS_COLORS = {
386
+ normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
387
+ warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
388
+ alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
389
+ fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
390
+ off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
391
+ starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
392
+ stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
393
+ };
394
+ var FORCED_MARK_COLOR = {
395
+ fill: "#ede9fe",
396
+ stroke: "#5b21b6",
397
+ bg: "#ede9fe",
398
+ text: "#5b21b6",
399
+ border: "#7c3aed"
400
+ };
401
+ var UNKNOWN_MARK_COLOR = {
402
+ fill: "#ebebf5",
403
+ stroke: "#8f8fc2",
404
+ bg: "#ebebf5",
405
+ text: "#4f4f8a",
406
+ border: "#8f8fc2"
407
+ };
408
+ var UNKNOWN_MARK_COLOR_DARK = {
409
+ fill: "#23263a",
410
+ stroke: "#6f74a3",
411
+ bg: "#23263a",
412
+ text: "#b9bbdb",
413
+ border: "#6f74a3"
414
+ };
415
+ var STATUS_ALIASES = {
416
+ running: "normal",
417
+ stopped: "off"
418
+ };
419
+ function resolveStatusColor(status, customMap) {
420
+ const raw = (status || "off").toLowerCase();
421
+ const key = STATUS_ALIASES[raw] ?? raw;
422
+ const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
423
+ const overrides = customMap?.[key];
424
+ if (!overrides) return defaults;
425
+ return { ...defaults, ...overrides };
426
+ }
427
+
428
+ // src/components/ValueEntry/LegacyValueEntry.tsx
359
429
  var import_jsx_runtime4 = require("react/jsx-runtime");
360
430
  var LegacyValueEntry = ({
361
431
  value,
@@ -368,6 +438,8 @@ var LegacyValueEntry = ({
368
438
  inputType = "numeric",
369
439
  label,
370
440
  readOnly = false,
441
+ forced = false,
442
+ realValue,
371
443
  className = "",
372
444
  disabled,
373
445
  ...props
@@ -448,6 +520,27 @@ var LegacyValueEntry = ({
448
520
  }
449
521
  };
450
522
  const statusStyles = getStatusStyles();
523
+ const forcedBadgeStyle = {
524
+ display: "inline-flex",
525
+ alignItems: "center",
526
+ justifyContent: "center",
527
+ width: "1.1rem",
528
+ height: "1.1rem",
529
+ fontSize: "0.7rem",
530
+ fontWeight: 700,
531
+ color: FORCED_MARK_COLOR.text,
532
+ backgroundColor: FORCED_MARK_COLOR.bg,
533
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
534
+ borderRadius: "50%",
535
+ marginLeft: "8px",
536
+ lineHeight: 1
537
+ };
538
+ const realValueStyle = {
539
+ fontSize: "0.75rem",
540
+ fontWeight: 500,
541
+ color: "#94a3b8",
542
+ letterSpacing: "0.02em"
543
+ };
451
544
  const inputStyle = {
452
545
  width: "12rem",
453
546
  height: "4rem",
@@ -496,17 +589,22 @@ var LegacyValueEntry = ({
496
589
  gap: "0.75rem"
497
590
  },
498
591
  children: [
499
- label && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
592
+ (label || forced) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
500
593
  "label",
501
594
  {
502
595
  style: {
596
+ display: "flex",
597
+ alignItems: "center",
503
598
  fontSize: "0.875rem",
504
599
  fontWeight: "600",
505
600
  color: "#cbd5e1",
506
601
  textTransform: "uppercase",
507
602
  letterSpacing: "0.05em"
508
603
  },
509
- children: label
604
+ children: [
605
+ label,
606
+ forced && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: forcedBadgeStyle, "aria-label": "Forced", children: "F" })
607
+ ]
510
608
  }
511
609
  ),
512
610
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { position: "relative", display: "inline-flex", alignItems: "center" }, children: [
@@ -563,6 +661,11 @@ var LegacyValueEntry = ({
563
661
  }
564
662
  }
565
663
  )
664
+ ] }),
665
+ forced && realValue !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: realValueStyle, children: [
666
+ "REAL ",
667
+ String(realValue),
668
+ unit ? ` ${unit}` : ""
566
669
  ] })
567
670
  ]
568
671
  }
@@ -578,6 +681,9 @@ var ControlPanel = ({
578
681
  onModeChange,
579
682
  setpointValue,
580
683
  onSetpointChange,
684
+ currentSetpointValue,
685
+ currentSetpointForced = false,
686
+ currentSetpointRealValue,
581
687
  secondSetpointValue,
582
688
  onSecondSetpointChange,
583
689
  modeConfigs = {},
@@ -674,6 +780,21 @@ var ControlPanel = ({
674
780
  disabled: status === "disabled"
675
781
  }
676
782
  ) }),
783
+ currentSetpointValue !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
784
+ LegacyValueEntry,
785
+ {
786
+ value: currentSetpointValue,
787
+ onChange: () => {
788
+ },
789
+ label: `${currentLabel} (Current)`,
790
+ unit: currentUnit,
791
+ precision: currentPrecision,
792
+ status,
793
+ readOnly: true,
794
+ forced: currentSetpointForced,
795
+ realValue: currentSetpointRealValue
796
+ }
797
+ ) }),
677
798
  hasSecondSetpoint && onSecondSetpointChange && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
678
799
  LegacyValueEntry,
679
800
  {
@@ -764,9 +885,15 @@ var StatusIndicator = ({
764
885
  labelPosition = "right",
765
886
  shape = "circle",
766
887
  led = true,
888
+ visible = true,
889
+ forced = false,
890
+ realValue,
767
891
  className = "",
768
892
  ...props
769
893
  }) => {
894
+ if (!visible) {
895
+ return null;
896
+ }
770
897
  const baseClass = "ui-status-indicator";
771
898
  const statusClass = status ? `ui-status-indicator--${status}` : "";
772
899
  const sizeClass = size ? `ui-status-indicator--${size}` : "";
@@ -787,28 +914,43 @@ var StatusIndicator = ({
787
914
  return "ui-status-indicator-container ui-status-indicator-label-right";
788
915
  }
789
916
  };
917
+ const forcedClass = forced ? "ui-status-indicator--forced" : "";
918
+ const hasRealValue = forced && realValue !== void 0;
790
919
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: getContainerLayout(), children: [
791
920
  label && (labelPosition === "left" || labelPosition === "top") && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "ui-status-indicator-label", children: label }),
792
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
793
- "span",
794
- {
795
- role: "status",
796
- "aria-label": label || `Status: ${status}`,
797
- "aria-description": getStatusDescription(status),
798
- "data-status": status,
799
- className: `
800
- ${baseClass}
801
- ${statusClass}
802
- ${sizeClass}
803
- ${shapeClass}
804
- ${animationClass}
805
- ${ledClass}
806
- ${className}
807
- `.replace(/\s+/g, " ").trim(),
808
- ...props
809
- }
810
- ),
811
- label && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "ui-status-indicator-label", children: label })
921
+ hasRealValue && (labelPosition === "left" || labelPosition === "top") && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "ui-status-indicator-real-value", children: [
922
+ "REAL ",
923
+ realValue
924
+ ] }),
925
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: { position: "relative", display: "inline-flex" }, children: [
926
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
927
+ "span",
928
+ {
929
+ role: "status",
930
+ "aria-label": label || `Status: ${status}`,
931
+ "aria-description": getStatusDescription(status),
932
+ "data-status": status,
933
+ "data-forced": forced,
934
+ className: `
935
+ ${baseClass}
936
+ ${statusClass}
937
+ ${sizeClass}
938
+ ${shapeClass}
939
+ ${animationClass}
940
+ ${ledClass}
941
+ ${forcedClass}
942
+ ${className}
943
+ `.replace(/\s+/g, " ").trim(),
944
+ ...props
945
+ }
946
+ ),
947
+ forced && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "ui-status-indicator-forced-badge", "aria-label": "Forced", children: "F" })
948
+ ] }),
949
+ label && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "ui-status-indicator-label", children: label }),
950
+ hasRealValue && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "ui-status-indicator-real-value", children: [
951
+ "REAL ",
952
+ realValue
953
+ ] })
812
954
  ] });
813
955
  };
814
956
 
@@ -936,6 +1078,9 @@ var NodeControlsPanel = ({
936
1078
  onModeChange: config.onModeChange,
937
1079
  setpointValue: config.setpoint,
938
1080
  onSetpointChange: config.onSetpointChange,
1081
+ currentSetpointValue: config.currentSetpoint,
1082
+ currentSetpointForced: config.currentSetpointForced,
1083
+ currentSetpointRealValue: config.currentSetpointRealValue,
939
1084
  secondSetpointValue: config.secondSetpoint,
940
1085
  onSecondSetpointChange: config.onSecondSetpointChange,
941
1086
  modeConfigs: config.modeConfigs,
@@ -1000,31 +1145,6 @@ var NodeControlsPanel = ({
1000
1145
 
1001
1146
  // src/components/DeviceControlPanel/DeviceControlPanel.tsx
1002
1147
  var import_react5 = __toESM(require("react"), 1);
1003
-
1004
- // src/theme/statusColors.ts
1005
- var DEFAULT_STATUS_COLORS = {
1006
- normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
1007
- warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
1008
- alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
1009
- fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
1010
- off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
1011
- starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
1012
- stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
1013
- };
1014
- var STATUS_ALIASES = {
1015
- running: "normal",
1016
- stopped: "off"
1017
- };
1018
- function resolveStatusColor(status, customMap) {
1019
- const raw = (status || "off").toLowerCase();
1020
- const key = STATUS_ALIASES[raw] ?? raw;
1021
- const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
1022
- const overrides = customMap?.[key];
1023
- if (!overrides) return defaults;
1024
- return { ...defaults, ...overrides };
1025
- }
1026
-
1027
- // src/components/DeviceControlPanel/DeviceControlPanel.tsx
1028
1148
  var import_jsx_runtime8 = require("react/jsx-runtime");
1029
1149
  function DeviceControlPanelImpl({
1030
1150
  binding,
@@ -1672,10 +1792,13 @@ function DeviceControlPanelImpl({
1672
1792
  marginBottom: spacing.gap
1673
1793
  },
1674
1794
  children: [
1675
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1795
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1676
1796
  "span",
1677
1797
  {
1678
1798
  style: {
1799
+ display: "flex",
1800
+ alignItems: "center",
1801
+ gap: "6px",
1679
1802
  fontSize: `${fontSize.label}px`,
1680
1803
  fontWeight: 600,
1681
1804
  textTransform: "uppercase",
@@ -1683,7 +1806,29 @@ function DeviceControlPanelImpl({
1683
1806
  fontFamily: "Arial, sans-serif",
1684
1807
  color: mutedTextColor
1685
1808
  },
1686
- children: param.label
1809
+ children: [
1810
+ param.label,
1811
+ param.forced?.active && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1812
+ "span",
1813
+ {
1814
+ "aria-label": "Forced",
1815
+ style: {
1816
+ display: "inline-flex",
1817
+ alignItems: "center",
1818
+ justifyContent: "center",
1819
+ width: "16px",
1820
+ height: "16px",
1821
+ fontSize: "10px",
1822
+ fontWeight: 700,
1823
+ color: FORCED_MARK_COLOR.text,
1824
+ backgroundColor: FORCED_MARK_COLOR.bg,
1825
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
1826
+ borderRadius: "50%"
1827
+ },
1828
+ children: "F"
1829
+ }
1830
+ )
1831
+ ]
1687
1832
  }
1688
1833
  ),
1689
1834
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
@@ -1705,6 +1850,27 @@ function DeviceControlPanelImpl({
1705
1850
  ]
1706
1851
  }
1707
1852
  ),
1853
+ param.forced?.active && param.forced.realValue !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1854
+ "div",
1855
+ {
1856
+ style: {
1857
+ display: "flex",
1858
+ justifyContent: "flex-end",
1859
+ marginTop: "-4px",
1860
+ marginBottom: spacing.gap,
1861
+ fontSize: "10px",
1862
+ fontWeight: 500,
1863
+ fontFamily: "Arial, sans-serif",
1864
+ color: mutedTextColor
1865
+ },
1866
+ children: [
1867
+ "REAL",
1868
+ " ",
1869
+ typeof param.forced.realValue === "boolean" ? param.forced.realValue ? "ON" : "OFF" : String(param.forced.realValue),
1870
+ param.unit ? ` ${param.unit}` : ""
1871
+ ]
1872
+ }
1873
+ ),
1708
1874
  param.type === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1709
1875
  "button",
1710
1876
  {
@@ -2803,19 +2969,38 @@ var CardUnit = (0, import_styled2.default)("div")({
2803
2969
 
2804
2970
  // src/components/layout/FullscreenWorkspace/FullscreenWorkspace.visuals.tsx
2805
2971
  var import_jsx_runtime12 = require("react/jsx-runtime");
2972
+ var forcedTagStyle = {
2973
+ position: "absolute",
2974
+ top: "-6px",
2975
+ right: "-6px",
2976
+ width: "14px",
2977
+ height: "14px",
2978
+ display: "flex",
2979
+ alignItems: "center",
2980
+ justifyContent: "center",
2981
+ fontSize: "9px",
2982
+ fontWeight: 700,
2983
+ color: FORCED_MARK_COLOR.text,
2984
+ backgroundColor: FORCED_MARK_COLOR.bg,
2985
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
2986
+ borderRadius: "50%",
2987
+ pointerEvents: "none"
2988
+ };
2806
2989
  var TrendLine = ({
2807
2990
  values,
2808
2991
  width = 60,
2809
2992
  height = 20,
2810
2993
  color = "#4fc3f7",
2811
2994
  strokeWidth = 1.5,
2812
- backgroundColor = "rgba(255, 255, 255, 0.1)"
2995
+ backgroundColor = "rgba(255, 255, 255, 0.1)",
2996
+ forced = false
2813
2997
  }) => {
2814
2998
  if (!values || values.length < 2) {
2815
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2999
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2816
3000
  "div",
2817
3001
  {
2818
3002
  style: {
3003
+ position: "relative",
2819
3004
  width,
2820
3005
  height,
2821
3006
  backgroundColor,
@@ -2824,7 +3009,10 @@ var TrendLine = ({
2824
3009
  alignItems: "center",
2825
3010
  justifyContent: "center"
2826
3011
  },
2827
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontSize: "8px", color: "#ccc" }, children: "No data" })
3012
+ children: [
3013
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontSize: "8px", color: "#ccc" }, children: "No data" }),
3014
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
3015
+ ]
2828
3016
  }
2829
3017
  );
2830
3018
  }
@@ -2836,7 +3024,7 @@ var TrendLine = ({
2836
3024
  const x = 4 + i / (values.length - 1) * (width - 8);
2837
3025
  return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
2838
3026
  }).join(" ");
2839
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3027
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2840
3028
  "div",
2841
3029
  {
2842
3030
  style: {
@@ -2845,30 +3033,41 @@ var TrendLine = ({
2845
3033
  backgroundColor,
2846
3034
  borderRadius: "2px",
2847
3035
  position: "relative",
2848
- overflow: "hidden"
3036
+ overflow: "visible"
2849
3037
  },
2850
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { width, height, style: { position: "absolute", top: 0, left: 0 }, children: [
2851
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2852
- "path",
3038
+ children: [
3039
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3040
+ "svg",
2853
3041
  {
2854
- d: pathData,
2855
- fill: "none",
2856
- stroke: color,
2857
- strokeWidth,
2858
- strokeLinecap: "round",
2859
- strokeLinejoin: "round"
3042
+ width,
3043
+ height,
3044
+ style: { position: "absolute", top: 0, left: 0, overflow: "hidden", borderRadius: "2px" },
3045
+ children: [
3046
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3047
+ "path",
3048
+ {
3049
+ d: pathData,
3050
+ fill: "none",
3051
+ stroke: color,
3052
+ strokeWidth,
3053
+ strokeLinecap: "round",
3054
+ strokeLinejoin: "round"
3055
+ }
3056
+ ),
3057
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3058
+ "circle",
3059
+ {
3060
+ cx: 4 + (width - 8),
3061
+ cy: normalizedValues[normalizedValues.length - 1],
3062
+ r: 1.5,
3063
+ fill: color
3064
+ }
3065
+ )
3066
+ ]
2860
3067
  }
2861
3068
  ),
2862
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2863
- "circle",
2864
- {
2865
- cx: 4 + (width - 8),
2866
- cy: normalizedValues[normalizedValues.length - 1],
2867
- r: 1.5,
2868
- fill: color
2869
- }
2870
- )
2871
- ] })
3069
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
3070
+ ]
2872
3071
  }
2873
3072
  );
2874
3073
  };
@@ -2884,7 +3083,9 @@ var CircularGauge = ({
2884
3083
  unit,
2885
3084
  label,
2886
3085
  size = 60,
2887
- color = "rgba(255, 255, 255, 0.9)"
3086
+ color = "rgba(255, 255, 255, 0.9)",
3087
+ forced = false,
3088
+ realValue
2888
3089
  }) => {
2889
3090
  const percentage = Math.min(value / max * 100, 100);
2890
3091
  const strokeWidth = 4;
@@ -2915,7 +3116,7 @@ var CircularGauge = ({
2915
3116
  height: size,
2916
3117
  borderRadius: "50%",
2917
3118
  backgroundColor,
2918
- border: `1px solid ${borderStrokeColor}`
3119
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : `1px solid ${borderStrokeColor}`
2919
3120
  },
2920
3121
  children: [
2921
3122
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { width: size, height: size, style: { transform: "rotate(-90deg)" }, children: [
@@ -2991,12 +3192,39 @@ var CircularGauge = ({
2991
3192
  },
2992
3193
  children: label
2993
3194
  }
2994
- )
3195
+ ),
3196
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3197
+ "span",
3198
+ {
3199
+ "aria-label": "Forced",
3200
+ style: {
3201
+ display: "inline-flex",
3202
+ alignItems: "center",
3203
+ justifyContent: "center",
3204
+ width: "14px",
3205
+ height: "14px",
3206
+ fontSize: "9px",
3207
+ fontWeight: 700,
3208
+ color: FORCED_MARK_COLOR.text,
3209
+ backgroundColor: FORCED_MARK_COLOR.bg,
3210
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
3211
+ borderRadius: "50%",
3212
+ marginTop: "2px"
3213
+ },
3214
+ children: "F"
3215
+ }
3216
+ ),
3217
+ forced && realValue !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { fontSize: "8px", opacity: 0.8, color: "white", marginTop: "1px" }, children: [
3218
+ "REAL ",
3219
+ realValue.toFixed(1),
3220
+ " ",
3221
+ unit
3222
+ ] })
2995
3223
  ]
2996
3224
  }
2997
3225
  );
2998
3226
  };
2999
- var Sparkline = ({ data, width, height, color }) => {
3227
+ var Sparkline = ({ data, width, height, color, forced = false }) => {
3000
3228
  if (!data || data.length < 2) {
3001
3229
  return null;
3002
3230
  }
@@ -3011,24 +3239,52 @@ var Sparkline = ({ data, width, height, color }) => {
3011
3239
  const y = height - 8 - (value - min) / range * (height - 8);
3012
3240
  return `${x + 4},${y + 4}`;
3013
3241
  }).join(" ");
3014
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3242
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3015
3243
  "div",
3016
3244
  {
3017
3245
  style: {
3246
+ position: "relative",
3018
3247
  width,
3019
3248
  height,
3020
3249
  backgroundColor: "rgba(0, 0, 0, 0.7)",
3021
- border: "1px solid rgba(255, 255, 255, 0.3)",
3250
+ border: forced ? `1px solid ${FORCED_MARK_COLOR.border}` : "1px solid rgba(255, 255, 255, 0.3)",
3022
3251
  borderRadius: "4px",
3023
3252
  display: "flex",
3024
3253
  alignItems: "center",
3025
3254
  justifyContent: "center",
3026
3255
  boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)"
3027
3256
  },
3028
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("svg", { width, height, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("polyline", { points, fill: "none", stroke: color, strokeWidth: "1.5", opacity: "0.9" }) })
3257
+ children: [
3258
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("svg", { width, height, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("polyline", { points, fill: "none", stroke: color, strokeWidth: "1.5", opacity: "0.9" }) }),
3259
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
3260
+ ]
3029
3261
  }
3030
3262
  );
3031
3263
  };
3264
+ var EquipmentForcedBadge = () => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3265
+ "span",
3266
+ {
3267
+ "aria-label": "Forced",
3268
+ style: {
3269
+ position: "absolute",
3270
+ top: "-4px",
3271
+ right: "-4px",
3272
+ width: "11px",
3273
+ height: "11px",
3274
+ borderRadius: "50%",
3275
+ backgroundColor: FORCED_MARK_COLOR.bg,
3276
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
3277
+ color: FORCED_MARK_COLOR.text,
3278
+ fontSize: "7px",
3279
+ fontWeight: 700,
3280
+ display: "flex",
3281
+ alignItems: "center",
3282
+ justifyContent: "center",
3283
+ lineHeight: 1
3284
+ },
3285
+ children: "F"
3286
+ }
3287
+ );
3032
3288
  var EquipmentIndicatorWithSparkline = ({
3033
3289
  svgX,
3034
3290
  svgY,
@@ -3036,11 +3292,51 @@ var EquipmentIndicatorWithSparkline = ({
3036
3292
  label,
3037
3293
  size = 30,
3038
3294
  sparklineData,
3039
- sparklineOffset = { x: 40, y: 0 }
3295
+ sparklineOffset = { x: 40, y: 0 },
3296
+ forced = false
3040
3297
  }) => {
3041
3298
  const hasSparkline = sparklineData && sparklineData.length > 1 && Math.max(...sparklineData) !== Math.min(...sparklineData);
3042
3299
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
3043
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX, svgY, style: { transform: "translate(-50%, -50%)" }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3300
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX, svgY, style: { transform: "translate(-50%, -50%)" }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { position: "relative", width: size, height: size }, children: [
3301
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3302
+ "div",
3303
+ {
3304
+ style: {
3305
+ width: size,
3306
+ height: size,
3307
+ borderRadius: "50%",
3308
+ backgroundColor: color,
3309
+ display: "flex",
3310
+ alignItems: "center",
3311
+ justifyContent: "center",
3312
+ color: "white",
3313
+ fontSize: "7px",
3314
+ fontWeight: "700",
3315
+ textAlign: "center",
3316
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
3317
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : "2px solid rgba(255, 255, 255, 0.3)"
3318
+ },
3319
+ children: label
3320
+ }
3321
+ ),
3322
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EquipmentForcedBadge, {})
3323
+ ] }) }),
3324
+ hasSparkline && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX: svgX + sparklineOffset.x, svgY: svgY + sparklineOffset.y, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Sparkline, { data: sparklineData, width: 60, height: 20, color, forced }) })
3325
+ ] });
3326
+ };
3327
+ var EquipmentIndicator = ({
3328
+ svgX,
3329
+ svgY,
3330
+ color,
3331
+ label,
3332
+ size = 30,
3333
+ style,
3334
+ className,
3335
+ forced = false
3336
+ }) => {
3337
+ const overlayStyle = style !== void 0 ? { transform: "translate(-50%, -50%)", ...style } : { transform: "translate(-50%, -50%)" };
3338
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX, svgY, style: overlayStyle, className, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { position: "relative", width: size, height: size }, children: [
3339
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3044
3340
  "div",
3045
3341
  {
3046
3342
  style: {
@@ -3056,45 +3352,13 @@ var EquipmentIndicatorWithSparkline = ({
3056
3352
  fontWeight: "700",
3057
3353
  textAlign: "center",
3058
3354
  boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
3059
- border: "2px solid rgba(255, 255, 255, 0.3)"
3355
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : "2px solid rgba(255, 255, 255, 0.3)"
3060
3356
  },
3061
3357
  children: label
3062
3358
  }
3063
- ) }),
3064
- hasSparkline && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX: svgX + sparklineOffset.x, svgY: svgY + sparklineOffset.y, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Sparkline, { data: sparklineData, width: 60, height: 20, color }) })
3065
- ] });
3066
- };
3067
- var EquipmentIndicator = ({
3068
- svgX,
3069
- svgY,
3070
- color,
3071
- label,
3072
- size = 30,
3073
- style,
3074
- className
3075
- }) => {
3076
- const overlayStyle = style !== void 0 ? { transform: "translate(-50%, -50%)", ...style } : { transform: "translate(-50%, -50%)" };
3077
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SVGLockedOverlay, { svgX, svgY, style: overlayStyle, className, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3078
- "div",
3079
- {
3080
- style: {
3081
- width: size,
3082
- height: size,
3083
- borderRadius: "50%",
3084
- backgroundColor: color,
3085
- display: "flex",
3086
- alignItems: "center",
3087
- justifyContent: "center",
3088
- color: "white",
3089
- fontSize: "7px",
3090
- fontWeight: "700",
3091
- textAlign: "center",
3092
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
3093
- border: "2px solid rgba(255, 255, 255, 0.3)"
3094
- },
3095
- children: label
3096
- }
3097
- ) });
3359
+ ),
3360
+ forced && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EquipmentForcedBadge, {})
3361
+ ] }) });
3098
3362
  };
3099
3363
 
3100
3364
  // src/components/layout/FullscreenWorkspace/FullscreenWorkspace.tsx
@@ -3384,7 +3648,7 @@ var ThemeToggle = ({
3384
3648
  };
3385
3649
 
3386
3650
  // src/components/PIDCanvas/PIDCanvas.tsx
3387
- var import_react20 = __toESM(require("react"), 1);
3651
+ var import_react21 = __toESM(require("react"), 1);
3388
3652
 
3389
3653
  // src/components/PIDCanvas/NodeRenderer.tsx
3390
3654
  var import_react10 = __toESM(require("react"), 1);
@@ -3415,6 +3679,25 @@ var mockSymbolLibrary = {
3415
3679
  tags: ["valve", "gate", "powered", "actuator"]
3416
3680
  }
3417
3681
  },
3682
+ "status-indicator": {
3683
+ id: "status-indicator",
3684
+ name: "Status LED",
3685
+ category: "instrument",
3686
+ viewBox: { x: 0, y: 0, width: 24, height: 24 },
3687
+ ports: [],
3688
+ svgContent: `
3689
+ <rect x="2" y="2" width="20" height="20"
3690
+ rx="var(--led-rx, 50%)"
3691
+ fill="var(--symbol-fill, #64748b)"
3692
+ stroke="rgba(255,255,255,0.4)"
3693
+ stroke-width="var(--led-bezel, 0)"
3694
+ style="transform-box: fill-box; transform: rotate(var(--led-rotate, 0deg)); transform-origin: center; pointer-events: all;"/>
3695
+ `,
3696
+ metadata: {
3697
+ description: "Status LED indicator driven by telemetry status color",
3698
+ tags: ["status", "led", "indicator", "instrument"]
3699
+ }
3700
+ },
3418
3701
  "pump-positive-displacement": {
3419
3702
  id: "pump-positive-displacement",
3420
3703
  name: "Positive Displacement Pump",
@@ -4607,8 +4890,144 @@ function getAvailableSymbols() {
4607
4890
  return [...Object.keys(customSymbols), ...Object.keys(mockSymbolLibrary)];
4608
4891
  }
4609
4892
 
4893
+ // src/diagram/symbols/appearanceSchema.ts
4894
+ var COLOR_PRESETS = {
4895
+ green: "#22c55e",
4896
+ red: "#ef4444",
4897
+ amber: "#f59e0b",
4898
+ blue: "#3b82f6",
4899
+ grey: "#64748b"
4900
+ };
4901
+ function resolvePresetColor(name) {
4902
+ return name && COLOR_PRESETS[name] || COLOR_PRESETS.grey;
4903
+ }
4904
+ var COLOR_OPTIONS = Object.keys(COLOR_PRESETS);
4905
+ var STATUS_LED_APPEARANCE = [
4906
+ {
4907
+ key: "shape",
4908
+ kind: "select",
4909
+ label: "Shape",
4910
+ options: ["circle", "square", "diamond"],
4911
+ default: "circle"
4912
+ },
4913
+ {
4914
+ key: "size",
4915
+ kind: "select",
4916
+ label: "Size",
4917
+ target: "scale",
4918
+ options: ["small", "medium", "large"],
4919
+ default: "medium",
4920
+ valueMap: { small: 0.75, medium: 1, large: 1.5 }
4921
+ },
4922
+ { key: "led", kind: "boolean", label: "Bezel ring", default: true },
4923
+ { key: "glow", kind: "boolean", label: "Diffusion (glow)", default: false },
4924
+ {
4925
+ key: "animation",
4926
+ kind: "select",
4927
+ label: "Animation",
4928
+ options: ["none", "pulse", "blink"],
4929
+ default: "none"
4930
+ },
4931
+ {
4932
+ key: "labelPosition",
4933
+ kind: "select",
4934
+ label: "Label position",
4935
+ options: ["bottom", "top", "left", "right"],
4936
+ default: "bottom"
4937
+ },
4938
+ // Color binding: a named signal drives the on/off color.
4939
+ {
4940
+ key: "colorSource",
4941
+ kind: "tag",
4942
+ label: "Color source (tag)",
4943
+ default: "",
4944
+ help: "Signal/tag that drives color. True (non-zero) shows On color; false shows Off color. Blank = unbound (grey)."
4945
+ },
4946
+ {
4947
+ key: "onColor",
4948
+ kind: "select",
4949
+ label: "On color (true)",
4950
+ options: COLOR_OPTIONS,
4951
+ default: "green",
4952
+ help: "Color when the Color source is true."
4953
+ },
4954
+ {
4955
+ key: "offColor",
4956
+ kind: "select",
4957
+ label: "Off color (false)",
4958
+ options: COLOR_OPTIONS,
4959
+ default: "red",
4960
+ help: "Color when the Color source is false."
4961
+ },
4962
+ // Visibility binding: an independent named signal drives show/hide.
4963
+ {
4964
+ key: "visibleSource",
4965
+ kind: "tag",
4966
+ label: "Visibility source (tag)",
4967
+ default: "",
4968
+ help: "Signal/tag that drives visibility. True shows the LED, false hides it. Blank = always visible (use the Visible checkbox)."
4969
+ },
4970
+ {
4971
+ key: "visible",
4972
+ kind: "boolean",
4973
+ label: "Visible",
4974
+ target: "visible",
4975
+ default: true,
4976
+ help: "Static show/hide, used only when no Visibility source is set."
4977
+ }
4978
+ ];
4979
+ var APPEARANCE_REGISTRY = {
4980
+ "status-indicator": STATUS_LED_APPEARANCE
4981
+ };
4982
+ function getAppearanceSchema(symbolId) {
4983
+ return APPEARANCE_REGISTRY[symbolId] ?? [];
4984
+ }
4985
+ function getAppearanceDefaults(symbolId) {
4986
+ const out = {};
4987
+ for (const field of getAppearanceSchema(symbolId)) {
4988
+ if ((field.target ?? "metadata") === "metadata" && field.default !== void 0) {
4989
+ out[field.key] = field.default;
4990
+ }
4991
+ }
4992
+ return out;
4993
+ }
4994
+ function readAppearanceValue(node, field) {
4995
+ if (field.target === "visible") {
4996
+ return node.visible ?? field.default ?? true;
4997
+ }
4998
+ if (field.target === "scale") {
4999
+ const scale = node.transform.scale ?? 1;
5000
+ if (field.valueMap) {
5001
+ let bestOption = field.options?.[0] ?? String(scale);
5002
+ let bestDiff = Infinity;
5003
+ for (const [option, value2] of Object.entries(field.valueMap)) {
5004
+ const diff = Math.abs(value2 - scale);
5005
+ if (diff < bestDiff) {
5006
+ bestDiff = diff;
5007
+ bestOption = option;
5008
+ }
5009
+ }
5010
+ return bestOption;
5011
+ }
5012
+ return scale;
5013
+ }
5014
+ const value = node.metadata?.[field.key];
5015
+ return value ?? field.default;
5016
+ }
5017
+ function writeAppearanceValue(node, field, value) {
5018
+ if (field.target === "visible") {
5019
+ return { visible: Boolean(value) };
5020
+ }
5021
+ if (field.target === "scale") {
5022
+ const scale = field.valueMap ? field.valueMap[String(value)] ?? 1 : Number(value);
5023
+ return { transform: { ...node.transform, scale } };
5024
+ }
5025
+ return { metadata: { ...node.metadata, [field.key]: value } };
5026
+ }
5027
+
4610
5028
  // src/components/PIDCanvas/NodeRenderer.tsx
4611
5029
  var import_jsx_runtime16 = require("react/jsx-runtime");
5030
+ var HATCH_SIZE = 6;
4612
5031
  function NodeRendererImpl({
4613
5032
  nodes: nodes2,
4614
5033
  selectedNodeIds,
@@ -4620,140 +5039,206 @@ function NodeRendererImpl({
4620
5039
  enableDrag = false,
4621
5040
  statusColorMap
4622
5041
  }) {
4623
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
4624
- const symbol = getSymbolDefinition(node.symbolId);
4625
- const isSelected = selectedNodeIds?.has(node.id) ?? false;
4626
- const isDragging = draggingNodeId === node.id;
4627
- if (!symbol) {
4628
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("g", { children: [
4629
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4630
- "circle",
4631
- {
4632
- cx: node.transform.x,
4633
- cy: node.transform.y,
4634
- r: "15",
4635
- fill: "#ff0000",
4636
- opacity: "0.3",
4637
- stroke: "#ff0000",
4638
- strokeWidth: "2"
4639
- }
4640
- ),
4641
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4642
- "text",
4643
- {
4644
- x: node.transform.x,
4645
- y: node.transform.y,
4646
- textAnchor: "middle",
4647
- dominantBaseline: "middle",
4648
- fontSize: "10",
4649
- fill: "#ff0000",
4650
- children: "?"
4651
- }
4652
- )
4653
- ] }, node.id);
4654
- }
4655
- const transforms = [];
4656
- const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
4657
- const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
4658
- transforms.push(`translate(${effectiveX}, ${effectiveY})`);
4659
- if (node.transform.rotation) {
4660
- const centerX = symbol.viewBox.width / 2;
4661
- const centerY = symbol.viewBox.height / 2;
4662
- transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
4663
- }
4664
- if (node.transform.scale && node.transform.scale !== 1) {
4665
- const centerX = symbol.viewBox.width / 2;
4666
- const centerY = symbol.viewBox.height / 2;
4667
- transforms.push(
4668
- `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
4669
- );
4670
- }
4671
- if (node.transform.flipX || node.transform.flipY) {
4672
- const scaleX = node.transform.flipX ? -1 : 1;
4673
- const scaleY = node.transform.flipY ? -1 : 1;
4674
- const centerX = symbol.viewBox.width / 2;
4675
- const centerY = symbol.viewBox.height / 2;
4676
- transforms.push(
4677
- `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
4678
- );
4679
- }
4680
- const statusEntry = resolveStatusColor(node.status, statusColorMap);
4681
- const fillColor = node.customColors?.fill ?? statusEntry.fill;
4682
- const strokeColor = node.customColors?.stroke ?? statusEntry.stroke;
4683
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4684
- "g",
5042
+ const hatchId = `pid-unknown-hatch-${import_react10.default.useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
5043
+ const hasUnknown = nodes2.some((n) => n.positionUnknown === true && n.visible !== false);
5044
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("g", { id: "layer-nodes", children: [
5045
+ hasUnknown && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5046
+ "pattern",
4685
5047
  {
4686
- "data-node-id": node.id,
4687
- "data-symbol-id": node.symbolId,
4688
- "data-selected": isSelected,
4689
- "data-dragging": isDragging,
4690
- transform: transforms.join(" "),
4691
- style: {
4692
- "--symbol-fill": fillColor,
4693
- "--symbol-stroke": strokeColor,
4694
- cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default"
4695
- },
4696
- onClick: (e) => {
4697
- e.stopPropagation();
4698
- onNodeClick?.(node.id, e);
4699
- },
4700
- onDoubleClick: (e) => {
4701
- e.stopPropagation();
4702
- onNodeDoubleClick?.(node.id, e);
4703
- },
4704
- onMouseDown: (e) => {
4705
- if (enableDrag) {
4706
- onNodeDragStart?.(node.id, e);
4707
- }
4708
- },
4709
- onTouchStart: (e) => {
4710
- if (enableDrag) {
4711
- e.stopPropagation();
4712
- onNodeDragStart?.(node.id, e);
4713
- }
4714
- },
5048
+ id: hatchId,
5049
+ "data-testid": "position-unknown-pattern",
5050
+ patternUnits: "userSpaceOnUse",
5051
+ width: HATCH_SIZE,
5052
+ height: HATCH_SIZE,
5053
+ patternTransform: "rotate(45)",
4715
5054
  children: [
4716
- isSelected && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4717
- "rect",
5055
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("rect", { width: HATCH_SIZE, height: HATCH_SIZE, fill: UNKNOWN_MARK_COLOR.fill }),
5056
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5057
+ "line",
4718
5058
  {
4719
- x: "-5",
4720
- y: "-5",
4721
- width: symbol.viewBox.width + 10,
4722
- height: symbol.viewBox.height + 10,
4723
- fill: "none",
4724
- stroke: isDragging ? "#10b981" : "#3b82f6",
4725
- strokeWidth: isDragging ? "3" : "2",
4726
- strokeDasharray: "4 2",
4727
- rx: "2",
4728
- pointerEvents: "none"
5059
+ x1: "0",
5060
+ y1: "0",
5061
+ x2: "0",
5062
+ y2: HATCH_SIZE,
5063
+ stroke: UNKNOWN_MARK_COLOR.text,
5064
+ strokeWidth: "2"
4729
5065
  }
4730
- ),
5066
+ )
5067
+ ]
5068
+ }
5069
+ ) }),
5070
+ nodes2.filter((node) => node.visible !== false).map((node) => {
5071
+ const symbol = getSymbolDefinition(node.symbolId);
5072
+ const isSelected = selectedNodeIds?.has(node.id) ?? false;
5073
+ const isDragging = draggingNodeId === node.id;
5074
+ if (!symbol) {
5075
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("g", { children: [
4731
5076
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4732
- "g",
5077
+ "circle",
4733
5078
  {
4734
- dangerouslySetInnerHTML: { __html: symbol.svgContent },
4735
- "data-symbol-content": symbol.id,
4736
- style: { pointerEvents: "auto" }
5079
+ cx: node.transform.x,
5080
+ cy: node.transform.y,
5081
+ r: "15",
5082
+ fill: "#ff0000",
5083
+ opacity: "0.3",
5084
+ stroke: "#ff0000",
5085
+ strokeWidth: "2"
4737
5086
  }
4738
5087
  ),
4739
5088
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4740
5089
  "text",
4741
5090
  {
4742
- x: symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0),
4743
- y: symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0),
5091
+ x: node.transform.x,
5092
+ y: node.transform.y,
4744
5093
  textAnchor: "middle",
4745
- fontSize: node.labelFontSize ?? 10,
4746
- fontWeight: "600",
4747
- fill: isSelected ? "#3b82f6" : "#1f2937",
4748
- transform: node.transform.rotation ? `rotate(${-node.transform.rotation}, ${symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0)}, ${symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0)})` : void 0,
4749
- children: node.label || node.id
5094
+ dominantBaseline: "middle",
5095
+ fontSize: "10",
5096
+ fill: "#ff0000",
5097
+ children: "?"
4750
5098
  }
4751
5099
  )
4752
- ]
4753
- },
4754
- node.id
4755
- );
4756
- }) });
5100
+ ] }, node.id);
5101
+ }
5102
+ const transforms = [];
5103
+ const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
5104
+ const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
5105
+ transforms.push(`translate(${effectiveX}, ${effectiveY})`);
5106
+ if (node.transform.rotation) {
5107
+ const centerX = symbol.viewBox.width / 2;
5108
+ const centerY = symbol.viewBox.height / 2;
5109
+ transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
5110
+ }
5111
+ if (node.transform.scale && node.transform.scale !== 1) {
5112
+ const centerX = symbol.viewBox.width / 2;
5113
+ const centerY = symbol.viewBox.height / 2;
5114
+ transforms.push(
5115
+ `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
5116
+ );
5117
+ }
5118
+ if (node.transform.flipX || node.transform.flipY) {
5119
+ const scaleX = node.transform.flipX ? -1 : 1;
5120
+ const scaleY = node.transform.flipY ? -1 : 1;
5121
+ const centerX = symbol.viewBox.width / 2;
5122
+ const centerY = symbol.viewBox.height / 2;
5123
+ transforms.push(
5124
+ `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
5125
+ );
5126
+ }
5127
+ const statusEntry = resolveStatusColor(node.status, statusColorMap);
5128
+ const isUnknown = node.positionUnknown === true;
5129
+ const fillColor = isUnknown ? `url(#${hatchId})` : node.customColors?.fill ?? statusEntry.fill;
5130
+ const strokeColor = isUnknown ? UNKNOWN_MARK_COLOR.stroke : node.customColors?.stroke ?? statusEntry.stroke;
5131
+ const appearance = { ...getAppearanceDefaults(node.symbolId), ...node.metadata ?? {} };
5132
+ const ledStyle = {};
5133
+ if (appearance.shape !== void 0) {
5134
+ ledStyle["--led-rx"] = appearance.shape === "circle" ? "50%" : "2";
5135
+ ledStyle["--led-rotate"] = appearance.shape === "diamond" ? "45deg" : "0deg";
5136
+ }
5137
+ if (appearance.led !== void 0) {
5138
+ ledStyle["--led-bezel"] = appearance.led ? "2" : "0";
5139
+ }
5140
+ if (appearance.glow) {
5141
+ ledStyle.filter = `drop-shadow(0 0 4px ${fillColor})`;
5142
+ }
5143
+ const ledAnimation = typeof appearance.animation === "string" && appearance.animation !== "none" ? appearance.animation : void 0;
5144
+ const vb = symbol.viewBox;
5145
+ const ox = node.labelOffset?.x ?? 0;
5146
+ const oy = node.labelOffset?.y ?? 0;
5147
+ const labelPos = typeof appearance.labelPosition === "string" ? appearance.labelPosition : "bottom";
5148
+ let labelX = vb.width / 2 + ox;
5149
+ let labelY = vb.height + 12 + oy;
5150
+ let labelAnchor = "middle";
5151
+ if (labelPos === "top") {
5152
+ labelX = vb.width / 2 + ox;
5153
+ labelY = -6 + oy;
5154
+ labelAnchor = "middle";
5155
+ } else if (labelPos === "left") {
5156
+ labelX = -6 + ox;
5157
+ labelY = vb.height / 2 + oy;
5158
+ labelAnchor = "end";
5159
+ } else if (labelPos === "right") {
5160
+ labelX = vb.width + 6 + ox;
5161
+ labelY = vb.height / 2 + oy;
5162
+ labelAnchor = "start";
5163
+ }
5164
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5165
+ "g",
5166
+ {
5167
+ "data-node-id": node.id,
5168
+ "data-symbol-id": node.symbolId,
5169
+ "data-selected": isSelected,
5170
+ "data-dragging": isDragging,
5171
+ "data-led-animation": ledAnimation,
5172
+ "data-position-unknown": isUnknown ? "true" : void 0,
5173
+ transform: transforms.join(" "),
5174
+ style: {
5175
+ "--symbol-fill": fillColor,
5176
+ "--symbol-stroke": strokeColor,
5177
+ cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default",
5178
+ ...ledStyle
5179
+ },
5180
+ onClick: (e) => {
5181
+ e.stopPropagation();
5182
+ onNodeClick?.(node.id, e);
5183
+ },
5184
+ onDoubleClick: (e) => {
5185
+ e.stopPropagation();
5186
+ onNodeDoubleClick?.(node.id, e);
5187
+ },
5188
+ onMouseDown: (e) => {
5189
+ if (enableDrag) {
5190
+ onNodeDragStart?.(node.id, e);
5191
+ }
5192
+ },
5193
+ onTouchStart: (e) => {
5194
+ if (enableDrag) {
5195
+ e.stopPropagation();
5196
+ onNodeDragStart?.(node.id, e);
5197
+ }
5198
+ },
5199
+ children: [
5200
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5201
+ "rect",
5202
+ {
5203
+ x: "-5",
5204
+ y: "-5",
5205
+ width: symbol.viewBox.width + 10,
5206
+ height: symbol.viewBox.height + 10,
5207
+ fill: "none",
5208
+ stroke: isDragging ? "#10b981" : "#3b82f6",
5209
+ strokeWidth: isDragging ? "3" : "2",
5210
+ strokeDasharray: "4 2",
5211
+ rx: "2",
5212
+ pointerEvents: "none"
5213
+ }
5214
+ ),
5215
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5216
+ "g",
5217
+ {
5218
+ dangerouslySetInnerHTML: { __html: symbol.svgContent },
5219
+ "data-symbol-content": symbol.id,
5220
+ style: { pointerEvents: "auto" }
5221
+ }
5222
+ ),
5223
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5224
+ "text",
5225
+ {
5226
+ x: labelX,
5227
+ y: labelY,
5228
+ textAnchor: labelAnchor,
5229
+ fontSize: node.labelFontSize ?? 10,
5230
+ fontWeight: "600",
5231
+ fill: isSelected ? "#3b82f6" : "#1f2937",
5232
+ transform: node.transform.rotation ? `rotate(${-node.transform.rotation}, ${labelX}, ${labelY})` : void 0,
5233
+ children: node.label || node.id
5234
+ }
5235
+ )
5236
+ ]
5237
+ },
5238
+ node.id
5239
+ );
5240
+ })
5241
+ ] });
4757
5242
  }
4758
5243
  var NodeRenderer = import_react10.default.memo(NodeRendererImpl);
4759
5244
 
@@ -4974,7 +5459,9 @@ function DataOverlayImpl({
4974
5459
  scale = 1,
4975
5460
  statusColorMap
4976
5461
  }) {
4977
- const { value, display, history, scaling } = telemetry;
5462
+ const { value, display, history, scaling, forced } = telemetry;
5463
+ const isForced = forced?.active === true;
5464
+ const hasRealValue = isForced && forced?.realValue !== void 0;
4978
5465
  const showValue = display?.showValue ?? true;
4979
5466
  const showStatus = display?.showStatus ?? true;
4980
5467
  const showSparkline = display?.showSparkline ?? false;
@@ -5023,12 +5510,16 @@ function DataOverlayImpl({
5023
5510
  const finalTextColor = textColor || colors.text;
5024
5511
  const formattedValue = formatValue(value.value);
5025
5512
  const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
5513
+ const realDisplayText = hasRealValue ? unit ? `REAL ${formatValue(forced.realValue)} ${unit}` : `REAL ${formatValue(forced.realValue)}` : void 0;
5514
+ const borderColor = isForced ? FORCED_MARK_COLOR.border : colors.border;
5515
+ const borderWidth = (isForced ? 2 : 1) * scale;
5026
5516
  const hasSparkline = showSparkline && sparklinePath;
5027
- const overlayHeight = (label ? hasSparkline ? 55 : 30 : hasSparkline ? 47 : 22) * scale;
5517
+ const extraMarkRows = realDisplayText ? 1 : 0;
5518
+ const overlayHeight = (label ? hasSparkline ? 55 : 30 : hasSparkline ? 47 : 22) * scale + extraMarkRows * 9 * scale;
5028
5519
  const overlayWidth = 80 * scale;
5029
5520
  const displayX = x + offsetX;
5030
5521
  const displayY = y + offsetY;
5031
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("g", { "data-overlay-node": nodeId, "data-status": value.status, children: [
5522
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("g", { "data-overlay-node": nodeId, "data-status": value.status, "data-forced": isForced, children: [
5032
5523
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5033
5524
  "rect",
5034
5525
  {
@@ -5038,8 +5529,8 @@ function DataOverlayImpl({
5038
5529
  height: overlayHeight,
5039
5530
  rx: 4 * scale,
5040
5531
  fill: colors.bg,
5041
- stroke: colors.border,
5042
- strokeWidth: 1 * scale,
5532
+ stroke: borderColor,
5533
+ strokeWidth: borderWidth,
5043
5534
  opacity: "0.95",
5044
5535
  filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.2))"
5045
5536
  }
@@ -5088,6 +5579,48 @@ function DataOverlayImpl({
5088
5579
  ]
5089
5580
  }
5090
5581
  ),
5582
+ isForced && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5583
+ "g",
5584
+ {
5585
+ transform: `translate(${displayX + overlayWidth / 2}, ${displayY - 25 * scale})`,
5586
+ "aria-label": "Forced",
5587
+ children: [
5588
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5589
+ "circle",
5590
+ {
5591
+ r: 7 * scale,
5592
+ fill: FORCED_MARK_COLOR.bg,
5593
+ stroke: FORCED_MARK_COLOR.border,
5594
+ strokeWidth: 1.5 * scale
5595
+ }
5596
+ ),
5597
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5598
+ "text",
5599
+ {
5600
+ textAnchor: "middle",
5601
+ dominantBaseline: "central",
5602
+ fontSize: 8 * scale,
5603
+ fontWeight: "700",
5604
+ fill: FORCED_MARK_COLOR.text,
5605
+ children: "F"
5606
+ }
5607
+ )
5608
+ ]
5609
+ }
5610
+ ),
5611
+ realDisplayText && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5612
+ "text",
5613
+ {
5614
+ x: displayX,
5615
+ y: displayY + (value.quality !== void 0 && value.quality < 100 ? 14 : 5) * scale,
5616
+ textAnchor: "middle",
5617
+ fontSize: 7 * scale,
5618
+ fontWeight: "500",
5619
+ fill: finalTextColor,
5620
+ opacity: "0.85",
5621
+ children: realDisplayText
5622
+ }
5623
+ ),
5091
5624
  hasSparkline && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("g", { transform: `translate(${displayX - 35 * scale}, ${displayY + (label ? 5 : 0) * scale})`, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5092
5625
  "path",
5093
5626
  {
@@ -5855,8 +6388,49 @@ function useTelemetryStatus(telemetry, diagram, setDiagram, options = {}) {
5855
6388
  }, [telemetry, diagram, setDiagram, enabled, mapStatus]);
5856
6389
  }
5857
6390
 
5858
- // src/diagram/hooks/useDrag.ts
6391
+ // src/diagram/hooks/useBooleanIndicators.ts
5859
6392
  var import_react18 = require("react");
6393
+ function useBooleanIndicators(telemetry, diagram, setDiagram, options = {}) {
6394
+ const { enabled = true, symbolId = "status-indicator" } = options;
6395
+ (0, import_react18.useEffect)(() => {
6396
+ if (!enabled) return;
6397
+ const tagIndex = /* @__PURE__ */ new Map();
6398
+ telemetry.forEach((binding) => {
6399
+ if (binding.tag) tagIndex.set(binding.tag, binding.value.value);
6400
+ });
6401
+ let needsUpdate = false;
6402
+ const updatedNodes = diagram.nodes.map((node) => {
6403
+ if (node.symbolId !== symbolId) return node;
6404
+ const meta = node.metadata ?? {};
6405
+ const colorSource = typeof meta.colorSource === "string" ? meta.colorSource : "";
6406
+ const visibleSource = typeof meta.visibleSource === "string" ? meta.visibleSource : "";
6407
+ let next = node;
6408
+ if (colorSource) {
6409
+ const isTrue = Boolean(tagIndex.get(colorSource));
6410
+ const fill = resolvePresetColor(
6411
+ String(isTrue ? meta.onColor ?? "green" : meta.offColor ?? "red")
6412
+ );
6413
+ if (node.customColors?.fill !== fill) {
6414
+ next = { ...next, customColors: { ...next.customColors, fill } };
6415
+ }
6416
+ }
6417
+ if (visibleSource) {
6418
+ const vis = Boolean(tagIndex.get(visibleSource));
6419
+ if ((next.visible ?? true) !== vis) {
6420
+ next = { ...next, visible: vis };
6421
+ }
6422
+ }
6423
+ if (next !== node) needsUpdate = true;
6424
+ return next;
6425
+ });
6426
+ if (needsUpdate) {
6427
+ setDiagram({ ...diagram, nodes: updatedNodes });
6428
+ }
6429
+ }, [telemetry, diagram, setDiagram, enabled, symbolId]);
6430
+ }
6431
+
6432
+ // src/diagram/hooks/useDrag.ts
6433
+ var import_react19 = require("react");
5860
6434
  function useDrag(options = {}) {
5861
6435
  const {
5862
6436
  onDragStart,
@@ -5866,23 +6440,23 @@ function useDrag(options = {}) {
5866
6440
  dragThreshold = 3,
5867
6441
  screenToWorld = (x, y) => ({ x, y })
5868
6442
  } = options;
5869
- const [dragState, setDragState] = (0, import_react18.useState)({
6443
+ const [dragState, setDragState] = (0, import_react19.useState)({
5870
6444
  isDragging: false,
5871
6445
  startPosition: null,
5872
6446
  offset: { x: 0, y: 0 },
5873
6447
  draggedId: null
5874
6448
  });
5875
- const [isListening, setIsListening] = (0, import_react18.useState)(false);
5876
- const dragStartRef = (0, import_react18.useRef)(null);
5877
- const hasMovedRef = (0, import_react18.useRef)(false);
5878
- const applySnap = (0, import_react18.useCallback)(
6449
+ const [isListening, setIsListening] = (0, import_react19.useState)(false);
6450
+ const dragStartRef = (0, import_react19.useRef)(null);
6451
+ const hasMovedRef = (0, import_react19.useRef)(false);
6452
+ const applySnap = (0, import_react19.useCallback)(
5879
6453
  (value) => {
5880
6454
  if (snapToGrid <= 0) return value;
5881
6455
  return Math.round(value / snapToGrid) * snapToGrid;
5882
6456
  },
5883
6457
  [snapToGrid]
5884
6458
  );
5885
- const handleMouseMove = (0, import_react18.useCallback)(
6459
+ const handleMouseMove = (0, import_react19.useCallback)(
5886
6460
  (event) => {
5887
6461
  if (!dragStartRef.current) return;
5888
6462
  const { id, screenStart: _screenStart, itemPosition, clickOffset } = dragStartRef.current;
@@ -5923,7 +6497,7 @@ function useDrag(options = {}) {
5923
6497
  },
5924
6498
  [screenToWorld, dragThreshold, applySnap, onDragStart, onDragMove]
5925
6499
  );
5926
- const handleMouseUp = (0, import_react18.useCallback)(
6500
+ const handleMouseUp = (0, import_react19.useCallback)(
5927
6501
  (event) => {
5928
6502
  if (!dragStartRef.current || !hasMovedRef.current) {
5929
6503
  dragStartRef.current = null;
@@ -5960,7 +6534,7 @@ function useDrag(options = {}) {
5960
6534
  },
5961
6535
  [screenToWorld, applySnap, onDragEnd]
5962
6536
  );
5963
- const handleTouchMove = (0, import_react18.useCallback)(
6537
+ const handleTouchMove = (0, import_react19.useCallback)(
5964
6538
  (event) => {
5965
6539
  if (!dragStartRef.current || event.touches.length !== 1) return;
5966
6540
  const touch = event.touches[0];
@@ -6003,7 +6577,7 @@ function useDrag(options = {}) {
6003
6577
  },
6004
6578
  [screenToWorld, dragThreshold, applySnap, onDragStart, onDragMove]
6005
6579
  );
6006
- const handleTouchEnd = (0, import_react18.useCallback)(
6580
+ const handleTouchEnd = (0, import_react19.useCallback)(
6007
6581
  (event) => {
6008
6582
  if (!dragStartRef.current || !hasMovedRef.current) {
6009
6583
  dragStartRef.current = null;
@@ -6041,7 +6615,7 @@ function useDrag(options = {}) {
6041
6615
  },
6042
6616
  [screenToWorld, applySnap, onDragEnd]
6043
6617
  );
6044
- (0, import_react18.useEffect)(() => {
6618
+ (0, import_react19.useEffect)(() => {
6045
6619
  if (!isListening) return;
6046
6620
  document.addEventListener("mousemove", handleMouseMove);
6047
6621
  document.addEventListener("mouseup", handleMouseUp);
@@ -6056,7 +6630,7 @@ function useDrag(options = {}) {
6056
6630
  document.removeEventListener("touchcancel", handleTouchEnd);
6057
6631
  };
6058
6632
  }, [isListening, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
6059
- const startDrag = (0, import_react18.useCallback)(
6633
+ const startDrag = (0, import_react19.useCallback)(
6060
6634
  (id, event, itemPosition) => {
6061
6635
  event.stopPropagation();
6062
6636
  let clientX;
@@ -6086,7 +6660,7 @@ function useDrag(options = {}) {
6086
6660
  },
6087
6661
  [screenToWorld]
6088
6662
  );
6089
- const cancelDrag = (0, import_react18.useCallback)(() => {
6663
+ const cancelDrag = (0, import_react19.useCallback)(() => {
6090
6664
  dragStartRef.current = null;
6091
6665
  hasMovedRef.current = false;
6092
6666
  setIsListening(false);
@@ -6097,7 +6671,7 @@ function useDrag(options = {}) {
6097
6671
  draggedId: null
6098
6672
  });
6099
6673
  }, []);
6100
- const isDraggingItem = (0, import_react18.useCallback)(
6674
+ const isDraggingItem = (0, import_react19.useCallback)(
6101
6675
  (id) => dragState.isDragging && dragState.draggedId === id,
6102
6676
  [dragState.isDragging, dragState.draggedId]
6103
6677
  );
@@ -6110,7 +6684,7 @@ function useDrag(options = {}) {
6110
6684
  }
6111
6685
 
6112
6686
  // src/diagram/hooks/useKeyboardShortcuts.ts
6113
- var import_react19 = require("react");
6687
+ var import_react20 = require("react");
6114
6688
  function useKeyboardShortcuts(options) {
6115
6689
  const {
6116
6690
  onDelete,
@@ -6121,7 +6695,7 @@ function useKeyboardShortcuts(options) {
6121
6695
  onSelectAll,
6122
6696
  enabled = true
6123
6697
  } = options;
6124
- (0, import_react19.useEffect)(() => {
6698
+ (0, import_react20.useEffect)(() => {
6125
6699
  if (!enabled) return;
6126
6700
  const handleKeyDown = (event) => {
6127
6701
  const target = event.target;
@@ -6259,21 +6833,21 @@ function PIDCanvasImpl({
6259
6833
  statusColorMap,
6260
6834
  ...props
6261
6835
  }) {
6262
- const [localDiagram, setLocalDiagram] = (0, import_react20.useState)(diagram);
6263
- const [dragOverPosition, setDragOverPosition] = (0, import_react20.useState)(null);
6264
- const [isConnecting, setIsConnecting] = (0, import_react20.useState)(false);
6265
- const [connectionSource, setConnectionSource] = (0, import_react20.useState)(null);
6266
- const [connectionSourcePort, setConnectionSourcePort] = (0, import_react20.useState)(null);
6267
- const [connectionCursor, setConnectionCursor] = (0, import_react20.useState)(null);
6268
- const [hoveredNode, setHoveredNode] = (0, import_react20.useState)(null);
6269
- const [hoveredPort, setHoveredPort] = (0, import_react20.useState)(null);
6270
- const [selectedWaypoint, setSelectedWaypoint] = (0, import_react20.useState)(null);
6271
- const historyStack = (0, import_react20.useRef)([]);
6272
- const redoStack = (0, import_react20.useRef)([]);
6273
- const isUndoRedoAction = (0, import_react20.useRef)(false);
6274
- const isInternalChange = (0, import_react20.useRef)(false);
6275
- const lastInternalDiagram = (0, import_react20.useRef)(null);
6276
- (0, import_react20.useEffect)(() => {
6836
+ const [localDiagram, setLocalDiagram] = (0, import_react21.useState)(diagram);
6837
+ const [dragOverPosition, setDragOverPosition] = (0, import_react21.useState)(null);
6838
+ const [isConnecting, setIsConnecting] = (0, import_react21.useState)(false);
6839
+ const [connectionSource, setConnectionSource] = (0, import_react21.useState)(null);
6840
+ const [connectionSourcePort, setConnectionSourcePort] = (0, import_react21.useState)(null);
6841
+ const [connectionCursor, setConnectionCursor] = (0, import_react21.useState)(null);
6842
+ const [hoveredNode, setHoveredNode] = (0, import_react21.useState)(null);
6843
+ const [hoveredPort, setHoveredPort] = (0, import_react21.useState)(null);
6844
+ const [selectedWaypoint, setSelectedWaypoint] = (0, import_react21.useState)(null);
6845
+ const historyStack = (0, import_react21.useRef)([]);
6846
+ const redoStack = (0, import_react21.useRef)([]);
6847
+ const isUndoRedoAction = (0, import_react21.useRef)(false);
6848
+ const isInternalChange = (0, import_react21.useRef)(false);
6849
+ const lastInternalDiagram = (0, import_react21.useRef)(null);
6850
+ (0, import_react21.useEffect)(() => {
6277
6851
  const isExternal = !isInternalChange.current && diagram !== lastInternalDiagram.current;
6278
6852
  if (isExternal) {
6279
6853
  setLocalDiagram(diagram);
@@ -6295,7 +6869,7 @@ function PIDCanvasImpl({
6295
6869
  enablePan: features.pan ?? true,
6296
6870
  enableZoom: features.zoom ?? true
6297
6871
  });
6298
- const calculateContentBounds = (0, import_react20.useCallback)(() => {
6872
+ const calculateContentBounds = (0, import_react21.useCallback)(() => {
6299
6873
  const nodes2 = localDiagram.nodes.filter((n) => n.visible !== false);
6300
6874
  if (nodes2.length === 0) {
6301
6875
  return null;
@@ -6318,7 +6892,7 @@ function PIDCanvasImpl({
6318
6892
  });
6319
6893
  return { minX, minY, maxX, maxY };
6320
6894
  }, [localDiagram.nodes]);
6321
- (0, import_react20.useEffect)(() => {
6895
+ (0, import_react21.useEffect)(() => {
6322
6896
  if (onMounted) {
6323
6897
  const controls = {
6324
6898
  fitToContent: (padding = 50) => {
@@ -6359,8 +6933,8 @@ function PIDCanvasImpl({
6359
6933
  });
6360
6934
  const dragEnabled = features.dragNodes ?? false;
6361
6935
  const selectionEnabled = features.selection ?? false;
6362
- const containerRef = (0, import_react20.useRef)(null);
6363
- const pushToHistory = (0, import_react20.useCallback)((currentDiagram) => {
6936
+ const containerRef = (0, import_react21.useRef)(null);
6937
+ const pushToHistory = (0, import_react21.useCallback)((currentDiagram) => {
6364
6938
  if (isUndoRedoAction.current) return;
6365
6939
  const snapshot = JSON.parse(JSON.stringify(currentDiagram));
6366
6940
  historyStack.current.push(snapshot);
@@ -6369,7 +6943,7 @@ function PIDCanvasImpl({
6369
6943
  }
6370
6944
  redoStack.current = [];
6371
6945
  }, []);
6372
- const undo = (0, import_react20.useCallback)(() => {
6946
+ const undo = (0, import_react21.useCallback)(() => {
6373
6947
  if (historyStack.current.length === 0) return;
6374
6948
  isUndoRedoAction.current = true;
6375
6949
  const currentSnapshot = JSON.parse(JSON.stringify(localDiagram));
@@ -6383,7 +6957,7 @@ function PIDCanvasImpl({
6383
6957
  isUndoRedoAction.current = false;
6384
6958
  }, 0);
6385
6959
  }, [localDiagram, onDiagramChange]);
6386
- const redo = (0, import_react20.useCallback)(() => {
6960
+ const redo = (0, import_react21.useCallback)(() => {
6387
6961
  if (redoStack.current.length === 0) return;
6388
6962
  isUndoRedoAction.current = true;
6389
6963
  const currentSnapshot = JSON.parse(JSON.stringify(localDiagram));
@@ -6397,7 +6971,7 @@ function PIDCanvasImpl({
6397
6971
  isUndoRedoAction.current = false;
6398
6972
  }, 0);
6399
6973
  }, [localDiagram, onDiagramChange]);
6400
- (0, import_react20.useEffect)(() => {
6974
+ (0, import_react21.useEffect)(() => {
6401
6975
  const handleKeyDown = (e) => {
6402
6976
  const isCtrlOrCmd = e.ctrlKey || e.metaKey;
6403
6977
  if (isCtrlOrCmd && e.key === "z" && !e.shiftKey) {
@@ -6414,7 +6988,7 @@ function PIDCanvasImpl({
6414
6988
  window.addEventListener("keydown", handleKeyDown);
6415
6989
  return () => window.removeEventListener("keydown", handleKeyDown);
6416
6990
  }, [undo, redo]);
6417
- (0, import_react20.useEffect)(() => {
6991
+ (0, import_react21.useEffect)(() => {
6418
6992
  if (!features.dragNodes) return;
6419
6993
  const handleKeyDown = (e) => {
6420
6994
  if (selection.selectedNodeIds.size === 0 && !selectedWaypoint) return;
@@ -6499,7 +7073,7 @@ function PIDCanvasImpl({
6499
7073
  onNodeMove,
6500
7074
  pushToHistory
6501
7075
  ]);
6502
- const handlePositionChange = (0, import_react20.useCallback)(
7076
+ const handlePositionChange = (0, import_react21.useCallback)(
6503
7077
  (nodeId, x, y) => {
6504
7078
  pushToHistory(localDiagram);
6505
7079
  const updatedNodes = localDiagram.nodes.map(
@@ -6521,7 +7095,7 @@ function PIDCanvasImpl({
6521
7095
  },
6522
7096
  [localDiagram, onDiagramChange, onNodeMove, pushToHistory]
6523
7097
  );
6524
- const handleWaypointPositionChange = (0, import_react20.useCallback)(
7098
+ const handleWaypointPositionChange = (0, import_react21.useCallback)(
6525
7099
  (pipeId, pointIndex, x, y) => {
6526
7100
  pushToHistory(localDiagram);
6527
7101
  const updatedPipes = localDiagram.pipes.map((pipe) => {
@@ -6539,7 +7113,7 @@ function PIDCanvasImpl({
6539
7113
  },
6540
7114
  [localDiagram, onDiagramChange, pushToHistory]
6541
7115
  );
6542
- const handleDragEnd = (0, import_react20.useCallback)(
7116
+ const handleDragEnd = (0, import_react21.useCallback)(
6543
7117
  (nodeId, offset, finalPosition) => {
6544
7118
  const dragDistance = Math.sqrt(offset.x ** 2 + offset.y ** 2);
6545
7119
  if (dragDistance < 2) {
@@ -6620,7 +7194,7 @@ function PIDCanvasImpl({
6620
7194
  onDiagramChange?.(updatedDiagram);
6621
7195
  }
6622
7196
  });
6623
- const handleNodeDragStart = (0, import_react20.useCallback)(
7197
+ const handleNodeDragStart = (0, import_react21.useCallback)(
6624
7198
  (nodeId, event) => {
6625
7199
  const node = localDiagram.nodes.find((n) => n.id === nodeId);
6626
7200
  if (!node) return;
@@ -6628,7 +7202,7 @@ function PIDCanvasImpl({
6628
7202
  },
6629
7203
  [localDiagram.nodes, startDrag]
6630
7204
  );
6631
- const handleWaypointClick = (0, import_react20.useCallback)(
7205
+ const handleWaypointClick = (0, import_react21.useCallback)(
6632
7206
  (pipeId, pointIndex) => {
6633
7207
  setSelectedWaypoint({ pipeId, pointIndex });
6634
7208
  if (selectionEnabled) {
@@ -6637,7 +7211,7 @@ function PIDCanvasImpl({
6637
7211
  },
6638
7212
  [selectionEnabled, clearSelection]
6639
7213
  );
6640
- const handleWaypointDragStart = (0, import_react20.useCallback)(
7214
+ const handleWaypointDragStart = (0, import_react21.useCallback)(
6641
7215
  (pipeId, pointIndex, event) => {
6642
7216
  const pipe = localDiagram.pipes.find((p) => p.id === pipeId);
6643
7217
  if (!pipe || pointIndex >= pipe.routePoints.length) return;
@@ -6646,7 +7220,7 @@ function PIDCanvasImpl({
6646
7220
  },
6647
7221
  [localDiagram.pipes, startWaypointDrag]
6648
7222
  );
6649
- const handlePipeSegmentClick = (0, import_react20.useCallback)(
7223
+ const handlePipeSegmentClick = (0, import_react21.useCallback)(
6650
7224
  (pipeId, position, segmentIndex) => {
6651
7225
  pushToHistory(localDiagram);
6652
7226
  const updatedPipes = localDiagram.pipes.map((pipe) => {
@@ -6665,7 +7239,7 @@ function PIDCanvasImpl({
6665
7239
  [localDiagram, onDiagramChange, pushToHistory]
6666
7240
  );
6667
7241
  const telemetryEnabled = features.telemetry ?? false;
6668
- const handleDelete = (0, import_react20.useCallback)(() => {
7242
+ const handleDelete = (0, import_react21.useCallback)(() => {
6669
7243
  if (!selectionEnabled) return;
6670
7244
  const { selectedNodeIds, selectedPipeIds } = selection;
6671
7245
  if (selectedNodeIds.size === 0 && selectedPipeIds.size === 0) return;
@@ -6694,17 +7268,17 @@ function PIDCanvasImpl({
6694
7268
  onDelete,
6695
7269
  pushToHistory
6696
7270
  ]);
6697
- const handleCopy = (0, import_react20.useCallback)(() => {
7271
+ const handleCopy = (0, import_react21.useCallback)(() => {
6698
7272
  if (!selectionEnabled) return;
6699
7273
  const { selectedNodeIds, selectedPipeIds } = selection;
6700
7274
  if (selectedNodeIds.size === 0 && selectedPipeIds.size === 0) return;
6701
7275
  onItemsCopied?.(Array.from(selectedNodeIds), Array.from(selectedPipeIds));
6702
7276
  }, [selection, selectionEnabled, onItemsCopied]);
6703
- const handlePaste = (0, import_react20.useCallback)(() => {
7277
+ const handlePaste = (0, import_react21.useCallback)(() => {
6704
7278
  if (!selectionEnabled) return;
6705
7279
  onPaste?.();
6706
7280
  }, [selectionEnabled, onPaste]);
6707
- const handleNodeDoubleClick = (0, import_react20.useCallback)(
7281
+ const handleNodeDoubleClick = (0, import_react21.useCallback)(
6708
7282
  (nodeId, event) => {
6709
7283
  if (!selectionEnabled) return;
6710
7284
  console.log("Node double-clicked:", nodeId);
@@ -6718,7 +7292,7 @@ function PIDCanvasImpl({
6718
7292
  onCopy: handleCopy,
6719
7293
  onPaste: handlePaste
6720
7294
  });
6721
- const handleNodeClick = (0, import_react20.useCallback)(
7295
+ const handleNodeClick = (0, import_react21.useCallback)(
6722
7296
  (nodeId, event) => {
6723
7297
  const connectionModeEnabled = features.connectionMode ?? false;
6724
7298
  if (connectionModeEnabled && isConnecting) {
@@ -6760,7 +7334,7 @@ function PIDCanvasImpl({
6760
7334
  onNodeClick
6761
7335
  ]
6762
7336
  );
6763
- const handlePipeClick = (0, import_react20.useCallback)(
7337
+ const handlePipeClick = (0, import_react21.useCallback)(
6764
7338
  (pipeId, event) => {
6765
7339
  if (selectionEnabled) {
6766
7340
  selectPipe(pipeId, event.ctrlKey || event.metaKey);
@@ -6779,7 +7353,7 @@ function PIDCanvasImpl({
6779
7353
  }
6780
7354
  setSelectedWaypoint(null);
6781
7355
  };
6782
- const handleDragOver = (0, import_react20.useCallback)(
7356
+ const handleDragOver = (0, import_react21.useCallback)(
6783
7357
  (event) => {
6784
7358
  if (!features.allowSymbolDrop) return;
6785
7359
  event.preventDefault();
@@ -6790,7 +7364,7 @@ function PIDCanvasImpl({
6790
7364
  },
6791
7365
  [features.allowSymbolDrop, screenToWorld, svgRef]
6792
7366
  );
6793
- const handleDrop = (0, import_react20.useCallback)(
7367
+ const handleDrop = (0, import_react21.useCallback)(
6794
7368
  (event) => {
6795
7369
  if (!features.allowSymbolDrop) return;
6796
7370
  event.preventDefault();
@@ -6803,10 +7377,10 @@ function PIDCanvasImpl({
6803
7377
  },
6804
7378
  [features.allowSymbolDrop, screenToWorld, onSymbolDrop, svgRef]
6805
7379
  );
6806
- const handleDragLeave = (0, import_react20.useCallback)(() => {
7380
+ const handleDragLeave = (0, import_react21.useCallback)(() => {
6807
7381
  setDragOverPosition(null);
6808
7382
  }, []);
6809
- const handleMouseMove = (0, import_react20.useCallback)(
7383
+ const handleMouseMove = (0, import_react21.useCallback)(
6810
7384
  (event) => {
6811
7385
  const connectionModeEnabled = features.connectionMode ?? false;
6812
7386
  if (connectionModeEnabled) {
@@ -6951,6 +7525,7 @@ function PIDCanvasImpl({
6951
7525
  }
6952
7526
  ),
6953
7527
  telemetryEnabled && telemetry && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("g", { id: "layer-telemetry", children: displayDiagram.nodes.map((node) => {
7528
+ if (node.symbolId === "status-indicator") return null;
6954
7529
  const binding = telemetry.get(node.id);
6955
7530
  if (!binding) return null;
6956
7531
  const symbol = getSymbolDefinition(node.symbolId);
@@ -7116,10 +7691,10 @@ function PIDCanvasImpl({
7116
7691
  }
7117
7692
  );
7118
7693
  }
7119
- var PIDCanvas = import_react20.default.memo(PIDCanvasImpl);
7694
+ var PIDCanvas = import_react21.default.memo(PIDCanvasImpl);
7120
7695
 
7121
7696
  // src/components/SymbolLibrary/SymbolLibrary.tsx
7122
- var import_react21 = require("react");
7697
+ var import_react22 = require("react");
7123
7698
  var import_jsx_runtime21 = require("react/jsx-runtime");
7124
7699
  function SymbolLibrary({
7125
7700
  className = "",
@@ -7127,8 +7702,8 @@ function SymbolLibrary({
7127
7702
  showCategories = true,
7128
7703
  categories = ["Valves", "Pumps", "Tanks", "Instruments", "Displays"]
7129
7704
  }) {
7130
- const [selectedCategory, setSelectedCategory] = (0, import_react21.useState)("all");
7131
- const [searchQuery, setSearchQuery] = (0, import_react21.useState)("");
7705
+ const [selectedCategory, setSelectedCategory] = (0, import_react22.useState)("all");
7706
+ const [searchQuery, setSearchQuery] = (0, import_react22.useState)("");
7132
7707
  const symbolIds = getAvailableSymbols();
7133
7708
  const allSymbols = symbolIds.map((id) => getSymbolDefinition(id)).filter((symbol) => symbol !== void 0);
7134
7709
  const categoryMap = {
@@ -7318,6 +7893,8 @@ function NodeConfigPanel({
7318
7893
  onUpdateNode,
7319
7894
  onUpdateTelemetry,
7320
7895
  onAddTelemetry,
7896
+ onRemoveTelemetry,
7897
+ availableTags = [],
7321
7898
  onRemove,
7322
7899
  className = "",
7323
7900
  style = {}
@@ -7330,6 +7907,8 @@ function NodeConfigPanel({
7330
7907
  padding: "20px",
7331
7908
  ...style
7332
7909
  };
7910
+ const appearanceFields = getAppearanceSchema(node.symbolId);
7911
+ const showGenericTelemetry = appearanceFields.length === 0;
7333
7912
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className, style: defaultStyle, children: [
7334
7913
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("h2", { style: { margin: "0 0 16px 0", fontSize: "1rem", fontWeight: 600 }, children: [
7335
7914
  "Configure: ",
@@ -7451,7 +8030,136 @@ function NodeConfigPanel({
7451
8030
  )
7452
8031
  ] })
7453
8032
  ] }),
7454
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
8033
+ appearanceFields.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
8034
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Appearance" }),
8035
+ appearanceFields.map((field) => {
8036
+ const value = readAppearanceValue(node, field);
8037
+ const fieldLabel = field.label ?? field.key;
8038
+ const inputStyle = {
8039
+ width: "100%",
8040
+ padding: "6px 8px",
8041
+ borderRadius: "4px",
8042
+ border: "1px solid #cbd5e1",
8043
+ fontSize: "0.875rem"
8044
+ };
8045
+ let control;
8046
+ if (field.kind === "boolean") {
8047
+ control = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { style: { fontSize: "0.75rem", display: "block" }, children: [
8048
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8049
+ "input",
8050
+ {
8051
+ type: "checkbox",
8052
+ checked: Boolean(value),
8053
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.checked)),
8054
+ style: { marginRight: "6px" }
8055
+ }
8056
+ ),
8057
+ fieldLabel
8058
+ ] });
8059
+ } else if (field.kind === "select") {
8060
+ control = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8061
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
8062
+ fieldLabel,
8063
+ ":"
8064
+ ] }),
8065
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8066
+ "select",
8067
+ {
8068
+ value: String(value),
8069
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
8070
+ style: inputStyle,
8071
+ children: field.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("option", { value: opt, children: opt }, opt))
8072
+ }
8073
+ )
8074
+ ] });
8075
+ } else if (field.kind === "number") {
8076
+ control = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8077
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
8078
+ fieldLabel,
8079
+ ":"
8080
+ ] }),
8081
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8082
+ "input",
8083
+ {
8084
+ type: "number",
8085
+ value: Number(value),
8086
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, parseFloat(e.target.value) || 0)),
8087
+ style: inputStyle
8088
+ }
8089
+ )
8090
+ ] });
8091
+ } else if (field.kind === "tag") {
8092
+ const tagValue = String(value);
8093
+ const listId = `tags-${node.id}-${field.key}`;
8094
+ const known = tagValue.length > 0 && availableTags.includes(tagValue);
8095
+ const unknown = tagValue.length > 0 && !known;
8096
+ control = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8097
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
8098
+ fieldLabel,
8099
+ ":"
8100
+ ] }),
8101
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8102
+ "input",
8103
+ {
8104
+ type: "text",
8105
+ value: tagValue,
8106
+ list: listId,
8107
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
8108
+ placeholder: "type or pick a signal\u2026",
8109
+ style: {
8110
+ ...inputStyle,
8111
+ border: `1px solid ${unknown ? "#f59e0b" : known ? "#22c55e" : "#cbd5e1"}`
8112
+ }
8113
+ }
8114
+ ),
8115
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("datalist", { id: listId, children: availableTags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("option", { value: t }, t)) }),
8116
+ tagValue.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8117
+ "div",
8118
+ {
8119
+ style: {
8120
+ fontSize: "0.7rem",
8121
+ marginTop: "3px",
8122
+ color: known ? "#16a34a" : "#b45309"
8123
+ },
8124
+ children: known ? "\u2713 matched signal" : "\u26A0 no matching signal (reads as false until published)"
8125
+ }
8126
+ )
8127
+ ] });
8128
+ } else {
8129
+ control = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8130
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
8131
+ fieldLabel,
8132
+ ":"
8133
+ ] }),
8134
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8135
+ "input",
8136
+ {
8137
+ type: "text",
8138
+ value: String(value),
8139
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
8140
+ style: inputStyle
8141
+ }
8142
+ )
8143
+ ] });
8144
+ }
8145
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "10px" }, children: [
8146
+ control,
8147
+ field.help && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8148
+ "div",
8149
+ {
8150
+ style: {
8151
+ fontSize: "0.7rem",
8152
+ color: "#64748b",
8153
+ marginTop: "3px",
8154
+ lineHeight: 1.35
8155
+ },
8156
+ children: field.help
8157
+ }
8158
+ )
8159
+ ] }, field.key);
8160
+ })
8161
+ ] }),
8162
+ showGenericTelemetry && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
7455
8163
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Telemetry Data" }),
7456
8164
  !binding ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7457
8165
  "button",
@@ -7497,6 +8205,27 @@ function NodeConfigPanel({
7497
8205
  children: "+ Add Telemetry"
7498
8206
  }
7499
8207
  ) : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
8208
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Tag (attribute):" }),
8209
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8210
+ "input",
8211
+ {
8212
+ type: "text",
8213
+ value: binding.tag || "",
8214
+ onChange: (e) => onUpdateTelemetry({
8215
+ nodeId: node.id,
8216
+ tag: e.target.value
8217
+ }),
8218
+ placeholder: "e.g. pump.running",
8219
+ style: {
8220
+ width: "100%",
8221
+ padding: "6px 8px",
8222
+ borderRadius: "4px",
8223
+ border: "1px solid #cbd5e1",
8224
+ fontSize: "0.875rem",
8225
+ marginBottom: "8px"
8226
+ }
8227
+ }
8228
+ ),
7500
8229
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Initial Value:" }),
7501
8230
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7502
8231
  "input",
@@ -7804,10 +8533,28 @@ function NodeConfigPanel({
7804
8533
  ] })
7805
8534
  ] }),
7806
8535
  " "
7807
- ] })
8536
+ ] }),
8537
+ onRemoveTelemetry && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8538
+ "button",
8539
+ {
8540
+ onClick: onRemoveTelemetry,
8541
+ style: {
8542
+ width: "100%",
8543
+ padding: "6px 8px",
8544
+ marginTop: "8px",
8545
+ borderRadius: "4px",
8546
+ border: "1px solid #ef4444",
8547
+ background: "transparent",
8548
+ color: "#ef4444",
8549
+ fontSize: "0.8125rem",
8550
+ cursor: "pointer"
8551
+ },
8552
+ children: "Remove Telemetry"
8553
+ }
8554
+ )
7808
8555
  ] })
7809
8556
  ] }),
7810
- binding && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
8557
+ showGenericTelemetry && binding && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
7811
8558
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Thresholds" }),
7812
8559
  ["highHigh", "high", "low", "lowLow"].map((key) => {
7813
8560
  const label = key === "highHigh" ? "High-High (\u2265)" : key === "high" ? "High (\u2265)" : key === "low" ? "Low (\u2264)" : "Low-Low (\u2264)";
@@ -7837,7 +8584,7 @@ function NodeConfigPanel({
7837
8584
  ] }, key);
7838
8585
  })
7839
8586
  ] }),
7840
- binding && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
8587
+ showGenericTelemetry && binding && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { marginBottom: "20px" }, children: [
7841
8588
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Status Colors" }),
7842
8589
  ["normal", "warning", "alarm", "fault", "off"].map((status) => {
7843
8590
  const defaultColors = {
@@ -8179,13 +8926,13 @@ function createControlBinding(nodeId, deviceConfig, actions) {
8179
8926
  }
8180
8927
 
8181
8928
  // src/hooks/useDeviceControls.ts
8182
- var import_react22 = require("react");
8929
+ var import_react23 = require("react");
8183
8930
  function commandKey(nodeId, kind, suffix) {
8184
8931
  return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
8185
8932
  }
8186
8933
  function useDeviceControls(initialBindings, options) {
8187
8934
  const { commandTimeoutMs } = options ?? {};
8188
- const [controls, setControls] = (0, import_react22.useState)(() => {
8935
+ const [controls, setControls] = (0, import_react23.useState)(() => {
8189
8936
  const map = /* @__PURE__ */ new Map();
8190
8937
  if (initialBindings) {
8191
8938
  initialBindings.forEach((binding) => {
@@ -8194,12 +8941,12 @@ function useDeviceControls(initialBindings, options) {
8194
8941
  }
8195
8942
  return map;
8196
8943
  });
8197
- const pendingRef = (0, import_react22.useRef)(/* @__PURE__ */ new Map());
8198
- const [pendingCommands, setPendingCommands] = (0, import_react22.useState)(() => /* @__PURE__ */ new Set());
8199
- const syncPending = (0, import_react22.useCallback)(() => {
8944
+ const pendingRef = (0, import_react23.useRef)(/* @__PURE__ */ new Map());
8945
+ const [pendingCommands, setPendingCommands] = (0, import_react23.useState)(() => /* @__PURE__ */ new Set());
8946
+ const syncPending = (0, import_react23.useCallback)(() => {
8200
8947
  setPendingCommands(new Set(pendingRef.current.keys()));
8201
8948
  }, []);
8202
- const updateControl = (0, import_react22.useCallback)((nodeId, updates) => {
8949
+ const updateControl = (0, import_react23.useCallback)((nodeId, updates) => {
8203
8950
  setControls((prev) => {
8204
8951
  const newMap = new Map(prev);
8205
8952
  const existing = newMap.get(nodeId);
@@ -8213,7 +8960,7 @@ function useDeviceControls(initialBindings, options) {
8213
8960
  return newMap;
8214
8961
  });
8215
8962
  }, []);
8216
- const updateControlBatch = (0, import_react22.useCallback)((updates) => {
8963
+ const updateControlBatch = (0, import_react23.useCallback)((updates) => {
8217
8964
  setControls((prev) => {
8218
8965
  const newMap = new Map(prev);
8219
8966
  Object.entries(updates).forEach(([nodeId, update]) => {
@@ -8225,7 +8972,7 @@ function useDeviceControls(initialBindings, options) {
8225
8972
  return newMap;
8226
8973
  });
8227
8974
  }, []);
8228
- const updateDeviceState = (0, import_react22.useCallback)(
8975
+ const updateDeviceState = (0, import_react23.useCallback)(
8229
8976
  (nodeId, stateUpdates) => {
8230
8977
  setControls((prev) => {
8231
8978
  const newMap = new Map(prev);
@@ -8241,7 +8988,7 @@ function useDeviceControls(initialBindings, options) {
8241
8988
  },
8242
8989
  []
8243
8990
  );
8244
- const updateParameter = (0, import_react22.useCallback)(
8991
+ const updateParameter = (0, import_react23.useCallback)(
8245
8992
  (nodeId, parameterId, value) => {
8246
8993
  setControls((prev) => {
8247
8994
  const newMap = new Map(prev);
@@ -8268,27 +9015,27 @@ function useDeviceControls(initialBindings, options) {
8268
9015
  },
8269
9016
  []
8270
9017
  );
8271
- const setControlBinding = (0, import_react22.useCallback)((binding) => {
9018
+ const setControlBinding = (0, import_react23.useCallback)((binding) => {
8272
9019
  setControls((prev) => {
8273
9020
  const newMap = new Map(prev);
8274
9021
  newMap.set(binding.nodeId, binding);
8275
9022
  return newMap;
8276
9023
  });
8277
9024
  }, []);
8278
- const removeControlBinding = (0, import_react22.useCallback)((nodeId) => {
9025
+ const removeControlBinding = (0, import_react23.useCallback)((nodeId) => {
8279
9026
  setControls((prev) => {
8280
9027
  const newMap = new Map(prev);
8281
9028
  newMap.delete(nodeId);
8282
9029
  return newMap;
8283
9030
  });
8284
9031
  }, []);
8285
- const getControlBinding = (0, import_react22.useCallback)(
9032
+ const getControlBinding = (0, import_react23.useCallback)(
8286
9033
  (nodeId) => {
8287
9034
  return controls.get(nodeId);
8288
9035
  },
8289
9036
  [controls]
8290
9037
  );
8291
- const isCommandPending = (0, import_react22.useCallback)(
9038
+ const isCommandPending = (0, import_react23.useCallback)(
8292
9039
  (nodeId, kind) => {
8293
9040
  if (kind) {
8294
9041
  const prefix2 = `${nodeId}:${kind}`;
@@ -8305,7 +9052,7 @@ function useDeviceControls(initialBindings, options) {
8305
9052
  },
8306
9053
  [pendingCommands]
8307
9054
  );
8308
- const markPending = (0, import_react22.useCallback)(
9055
+ const markPending = (0, import_react23.useCallback)(
8309
9056
  (key, delta) => {
8310
9057
  const next = (pendingRef.current.get(key) ?? 0) + delta;
8311
9058
  if (next > 0) pendingRef.current.set(key, next);
@@ -8314,7 +9061,7 @@ function useDeviceControls(initialBindings, options) {
8314
9061
  },
8315
9062
  [syncPending]
8316
9063
  );
8317
- const runCommand = (0, import_react22.useCallback)(
9064
+ const runCommand = (0, import_react23.useCallback)(
8318
9065
  async (nodeId, key, invoke, messages) => {
8319
9066
  const binding = controls.get(nodeId);
8320
9067
  if (!binding) return;
@@ -8354,7 +9101,7 @@ function useDeviceControls(initialBindings, options) {
8354
9101
  },
8355
9102
  [controls, updateDeviceState, markPending, commandTimeoutMs]
8356
9103
  );
8357
- const sendModeChange = (0, import_react22.useCallback)(
9104
+ const sendModeChange = (0, import_react23.useCallback)(
8358
9105
  (nodeId, mode) => runCommand(
8359
9106
  nodeId,
8360
9107
  commandKey(nodeId, "mode"),
@@ -8363,7 +9110,7 @@ function useDeviceControls(initialBindings, options) {
8363
9110
  ),
8364
9111
  [runCommand]
8365
9112
  );
8366
- const sendParameterChange = (0, import_react22.useCallback)(
9113
+ const sendParameterChange = (0, import_react23.useCallback)(
8367
9114
  (nodeId, parameterId, value) => runCommand(
8368
9115
  nodeId,
8369
9116
  commandKey(nodeId, "parameter", parameterId),
@@ -8372,21 +9119,21 @@ function useDeviceControls(initialBindings, options) {
8372
9119
  ),
8373
9120
  [runCommand]
8374
9121
  );
8375
- const sendStartCommand = (0, import_react22.useCallback)(
9122
+ const sendStartCommand = (0, import_react23.useCallback)(
8376
9123
  (nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
8377
9124
  success: "Start command sent",
8378
9125
  failure: "Start command failed"
8379
9126
  }),
8380
9127
  [runCommand]
8381
9128
  );
8382
- const sendStopCommand = (0, import_react22.useCallback)(
9129
+ const sendStopCommand = (0, import_react23.useCallback)(
8383
9130
  (nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
8384
9131
  success: "Stop command sent",
8385
9132
  failure: "Stop command failed"
8386
9133
  }),
8387
9134
  [runCommand]
8388
9135
  );
8389
- const sendCustomAction = (0, import_react22.useCallback)(
9136
+ const sendCustomAction = (0, import_react23.useCallback)(
8390
9137
  (nodeId, actionId) => runCommand(
8391
9138
  nodeId,
8392
9139
  commandKey(nodeId, "action", actionId),
@@ -8439,9 +9186,9 @@ function createConfigBinding(nodeId, tag, parameters, actions, options) {
8439
9186
  }
8440
9187
 
8441
9188
  // src/hooks/useDeviceConfigs.ts
8442
- var import_react23 = require("react");
9189
+ var import_react24 = require("react");
8443
9190
  function useDeviceConfigs(initialConfigs) {
8444
- const [configs, setConfigs] = (0, import_react23.useState)(() => {
9191
+ const [configs, setConfigs] = (0, import_react24.useState)(() => {
8445
9192
  const map = /* @__PURE__ */ new Map();
8446
9193
  if (initialConfigs) {
8447
9194
  initialConfigs.forEach((config) => {
@@ -8450,13 +9197,13 @@ function useDeviceConfigs(initialConfigs) {
8450
9197
  }
8451
9198
  return map;
8452
9199
  });
8453
- const getConfig = (0, import_react23.useCallback)(
9200
+ const getConfig = (0, import_react24.useCallback)(
8454
9201
  (nodeId) => {
8455
9202
  return configs.get(nodeId);
8456
9203
  },
8457
9204
  [configs]
8458
9205
  );
8459
- const updateConfig = (0, import_react23.useCallback)((nodeId, updates) => {
9206
+ const updateConfig = (0, import_react24.useCallback)((nodeId, updates) => {
8460
9207
  setConfigs((prev) => {
8461
9208
  const newMap = new Map(prev);
8462
9209
  const existing = newMap.get(nodeId);
@@ -8470,7 +9217,7 @@ function useDeviceConfigs(initialConfigs) {
8470
9217
  return newMap;
8471
9218
  });
8472
9219
  }, []);
8473
- const updateConfigState = (0, import_react23.useCallback)((nodeId, stateUpdates) => {
9220
+ const updateConfigState = (0, import_react24.useCallback)((nodeId, stateUpdates) => {
8474
9221
  setConfigs((prev) => {
8475
9222
  const newMap = new Map(prev);
8476
9223
  const existing = newMap.get(nodeId);
@@ -8483,7 +9230,7 @@ function useDeviceConfigs(initialConfigs) {
8483
9230
  return newMap;
8484
9231
  });
8485
9232
  }, []);
8486
- const updateConfigBatch = (0, import_react23.useCallback)((updates) => {
9233
+ const updateConfigBatch = (0, import_react24.useCallback)((updates) => {
8487
9234
  setConfigs((prev) => {
8488
9235
  const newMap = new Map(prev);
8489
9236
  Object.entries(updates).forEach(([nodeId, update]) => {
@@ -8495,7 +9242,7 @@ function useDeviceConfigs(initialConfigs) {
8495
9242
  return newMap;
8496
9243
  });
8497
9244
  }, []);
8498
- const updateParameterValue = (0, import_react23.useCallback)((nodeId, parameterId, value) => {
9245
+ const updateParameterValue = (0, import_react24.useCallback)((nodeId, parameterId, value) => {
8499
9246
  setConfigs((prev) => {
8500
9247
  const newMap = new Map(prev);
8501
9248
  const existing = newMap.get(nodeId);
@@ -8512,7 +9259,7 @@ function useDeviceConfigs(initialConfigs) {
8512
9259
  return newMap;
8513
9260
  });
8514
9261
  }, []);
8515
- const applyConfig = (0, import_react23.useCallback)(
9262
+ const applyConfig = (0, import_react24.useCallback)(
8516
9263
  async (nodeId) => {
8517
9264
  const config = configs.get(nodeId);
8518
9265
  if (!config || !config.actions.onApply) {
@@ -8545,7 +9292,7 @@ function useDeviceConfigs(initialConfigs) {
8545
9292
  },
8546
9293
  [configs, updateConfigState]
8547
9294
  );
8548
- const saveConfig = (0, import_react23.useCallback)(
9295
+ const saveConfig = (0, import_react24.useCallback)(
8549
9296
  async (nodeId) => {
8550
9297
  const config = configs.get(nodeId);
8551
9298
  if (!config || !config.actions.onSave) {
@@ -8579,7 +9326,7 @@ function useDeviceConfigs(initialConfigs) {
8579
9326
  },
8580
9327
  [configs, updateConfigState]
8581
9328
  );
8582
- const resetConfig = (0, import_react23.useCallback)(
9329
+ const resetConfig = (0, import_react24.useCallback)(
8583
9330
  async (nodeId) => {
8584
9331
  const config = configs.get(nodeId);
8585
9332
  if (!config) {
@@ -8609,7 +9356,7 @@ function useDeviceConfigs(initialConfigs) {
8609
9356
  },
8610
9357
  [configs, updateConfig, updateConfigState]
8611
9358
  );
8612
- const cancelConfig = (0, import_react23.useCallback)(
9359
+ const cancelConfig = (0, import_react24.useCallback)(
8613
9360
  (nodeId) => {
8614
9361
  const config = configs.get(nodeId);
8615
9362
  if (config?.actions.onCancel) {
@@ -8618,7 +9365,7 @@ function useDeviceConfigs(initialConfigs) {
8618
9365
  },
8619
9366
  [configs]
8620
9367
  );
8621
- const validateConfig = (0, import_react23.useCallback)(
9368
+ const validateConfig = (0, import_react24.useCallback)(
8622
9369
  (nodeId) => {
8623
9370
  const config = configs.get(nodeId);
8624
9371
  if (!config) {
@@ -8660,21 +9407,21 @@ function useDeviceConfigs(initialConfigs) {
8660
9407
  },
8661
9408
  [configs, updateConfigState]
8662
9409
  );
8663
- const setConfig = (0, import_react23.useCallback)((config) => {
9410
+ const setConfig = (0, import_react24.useCallback)((config) => {
8664
9411
  setConfigs((prev) => {
8665
9412
  const newMap = new Map(prev);
8666
9413
  newMap.set(config.nodeId, config);
8667
9414
  return newMap;
8668
9415
  });
8669
9416
  }, []);
8670
- const removeConfig = (0, import_react23.useCallback)((nodeId) => {
9417
+ const removeConfig = (0, import_react24.useCallback)((nodeId) => {
8671
9418
  setConfigs((prev) => {
8672
9419
  const newMap = new Map(prev);
8673
9420
  newMap.delete(nodeId);
8674
9421
  return newMap;
8675
9422
  });
8676
9423
  }, []);
8677
- const clearConfigs = (0, import_react23.useCallback)(() => {
9424
+ const clearConfigs = (0, import_react24.useCallback)(() => {
8678
9425
  setConfigs(/* @__PURE__ */ new Map());
8679
9426
  }, []);
8680
9427
  return {
@@ -8696,7 +9443,7 @@ function useDeviceConfigs(initialConfigs) {
8696
9443
  }
8697
9444
 
8698
9445
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8699
- var import_react24 = __toESM(require("react"), 1);
9446
+ var import_react25 = __toESM(require("react"), 1);
8700
9447
  var import_jsx_runtime23 = require("react/jsx-runtime");
8701
9448
  function DeviceConfigPanelImpl({
8702
9449
  binding,
@@ -8716,7 +9463,7 @@ function DeviceConfigPanelImpl({
8716
9463
  showBuiltInToast = true,
8717
9464
  embedded = false
8718
9465
  }) {
8719
- const [localValues, setLocalValues] = (0, import_react24.useState)(() => {
9466
+ const [localValues, setLocalValues] = (0, import_react25.useState)(() => {
8720
9467
  if (binding) {
8721
9468
  const values = {};
8722
9469
  binding.parameters.forEach((param) => {
@@ -8726,7 +9473,7 @@ function DeviceConfigPanelImpl({
8726
9473
  }
8727
9474
  return {};
8728
9475
  });
8729
- const [collapsedSections, setCollapsedSections] = (0, import_react24.useState)(() => {
9476
+ const [collapsedSections, setCollapsedSections] = (0, import_react25.useState)(() => {
8730
9477
  const collapsed = /* @__PURE__ */ new Set();
8731
9478
  if (binding?.sections) {
8732
9479
  binding.sections.forEach((section) => {
@@ -8737,9 +9484,9 @@ function DeviceConfigPanelImpl({
8737
9484
  }
8738
9485
  return collapsed;
8739
9486
  });
8740
- const [showToast, setShowToast] = (0, import_react24.useState)(true);
8741
- const [bindingId, setBindingId] = (0, import_react24.useState)(binding?.nodeId || null);
8742
- const lastResultTimestampRef = (0, import_react24.useRef)(0);
9487
+ const [showToast, setShowToast] = (0, import_react25.useState)(true);
9488
+ const [bindingId, setBindingId] = (0, import_react25.useState)(binding?.nodeId || null);
9489
+ const lastResultTimestampRef = (0, import_react25.useRef)(0);
8743
9490
  if (binding && binding.nodeId !== bindingId) {
8744
9491
  const values = {};
8745
9492
  binding.parameters.forEach((param) => {
@@ -8755,7 +9502,7 @@ function DeviceConfigPanelImpl({
8755
9502
  setCollapsedSections(collapsed);
8756
9503
  setBindingId(binding.nodeId);
8757
9504
  }
8758
- (0, import_react24.useEffect)(() => {
9505
+ (0, import_react25.useEffect)(() => {
8759
9506
  const currentTimestamp = binding?.state.lastSaved || 0;
8760
9507
  if (binding?.state.lastSaveResult && currentTimestamp !== lastResultTimestampRef.current) {
8761
9508
  lastResultTimestampRef.current = currentTimestamp;
@@ -8772,7 +9519,7 @@ function DeviceConfigPanelImpl({
8772
9519
  };
8773
9520
  }
8774
9521
  }, [binding?.state.lastSaveResult, binding?.state.lastSaved, toastDuration]);
8775
- const parametersBySection = (0, import_react24.useMemo)(() => {
9522
+ const parametersBySection = (0, import_react25.useMemo)(() => {
8776
9523
  if (!binding) return { _unsectioned: [] };
8777
9524
  const groups = { _unsectioned: [] };
8778
9525
  binding.parameters.forEach((param) => {
@@ -8787,7 +9534,7 @@ function DeviceConfigPanelImpl({
8787
9534
  });
8788
9535
  return groups;
8789
9536
  }, [binding]);
8790
- const sortedSections = (0, import_react24.useMemo)(() => {
9537
+ const sortedSections = (0, import_react25.useMemo)(() => {
8791
9538
  if (!binding?.sections) return [];
8792
9539
  return [...binding.sections].sort((a, b) => (a.order || 0) - (b.order || 0));
8793
9540
  }, [binding]);
@@ -8890,7 +9637,49 @@ function DeviceConfigPanelImpl({
8890
9637
  " (",
8891
9638
  param.unit,
8892
9639
  ")"
8893
- ] })
9640
+ ] }),
9641
+ param.forced?.active && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
9642
+ "span",
9643
+ {
9644
+ "aria-label": "Forced",
9645
+ style: {
9646
+ display: "inline-flex",
9647
+ alignItems: "center",
9648
+ justifyContent: "center",
9649
+ width: "16px",
9650
+ height: "16px",
9651
+ marginLeft: "6px",
9652
+ fontSize: "10px",
9653
+ fontWeight: 700,
9654
+ textTransform: "none",
9655
+ letterSpacing: "normal",
9656
+ color: FORCED_MARK_COLOR.text,
9657
+ backgroundColor: FORCED_MARK_COLOR.bg,
9658
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
9659
+ borderRadius: "50%",
9660
+ verticalAlign: "middle"
9661
+ },
9662
+ children: "F"
9663
+ }
9664
+ )
9665
+ ]
9666
+ }
9667
+ ),
9668
+ param.forced?.active && param.forced.realValue !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
9669
+ "div",
9670
+ {
9671
+ style: {
9672
+ fontSize: `${fontSize.description}px`,
9673
+ color: mutedTextColor,
9674
+ marginTop: "-2px",
9675
+ marginBottom: "6px",
9676
+ fontFamily: "Arial, sans-serif"
9677
+ },
9678
+ children: [
9679
+ "REAL",
9680
+ " ",
9681
+ typeof param.forced.realValue === "boolean" ? param.forced.realValue ? "ON" : "OFF" : String(param.forced.realValue),
9682
+ param.unit ? ` ${param.unit}` : ""
8894
9683
  ]
8895
9684
  }
8896
9685
  ),
@@ -9401,10 +10190,10 @@ function DeviceConfigPanelImpl({
9401
10190
  )
9402
10191
  ] });
9403
10192
  }
9404
- var DeviceConfigPanel = import_react24.default.memo(DeviceConfigPanelImpl);
10193
+ var DeviceConfigPanel = import_react25.default.memo(DeviceConfigPanelImpl);
9405
10194
 
9406
10195
  // src/diagram/hooks/useSimulation.ts
9407
- var import_react25 = require("react");
10196
+ var import_react26 = require("react");
9408
10197
  function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9409
10198
  const {
9410
10199
  interval = 2e3,
@@ -9415,7 +10204,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9415
10204
  historyLength = 10,
9416
10205
  generateValue
9417
10206
  } = options;
9418
- (0, import_react25.useEffect)(() => {
10207
+ (0, import_react26.useEffect)(() => {
9419
10208
  if (!enabled) return;
9420
10209
  const intervalId = setInterval(() => {
9421
10210
  const updates = [];
@@ -9466,6 +10255,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9466
10255
  // Annotate the CommonJS export names for ESM import in node:
9467
10256
  0 && (module.exports = {
9468
10257
  Button,
10258
+ COLOR_PRESETS,
9469
10259
  CardHeader,
9470
10260
  CardUnit,
9471
10261
  CardValue,
@@ -9479,6 +10269,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9479
10269
  DisplayModeToggle,
9480
10270
  EquipmentIndicator,
9481
10271
  EquipmentIndicatorWithSparkline,
10272
+ FORCED_MARK_COLOR,
9482
10273
  FixedSVGContainer,
9483
10274
  FooterPanel,
9484
10275
  FullscreenContainer,
@@ -9494,6 +10285,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9494
10285
  PanelTabs,
9495
10286
  RightPanel,
9496
10287
  RightToggleButton,
10288
+ STATUS_LED_APPEARANCE,
9497
10289
  SVGArea,
9498
10290
  SVGLockedOverlay,
9499
10291
  ScrollableSVGWrapper,
@@ -9504,6 +10296,8 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9504
10296
  ThemeProvider,
9505
10297
  ThemeToggle,
9506
10298
  TrendLine,
10299
+ UNKNOWN_MARK_COLOR,
10300
+ UNKNOWN_MARK_COLOR_DARK,
9507
10301
  ValueEntry,
9508
10302
  ZoomButton,
9509
10303
  ZoomControls,
@@ -9513,6 +10307,8 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9513
10307
  exampleDiagram,
9514
10308
  exportDiagram,
9515
10309
  generateRoutePoints,
10310
+ getAppearanceDefaults,
10311
+ getAppearanceSchema,
9516
10312
  getAvailableSymbols,
9517
10313
  getPortWorldPosition,
9518
10314
  getSymbolDefinition,
@@ -9520,9 +10316,12 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9520
10316
  mockSymbolLibrary,
9521
10317
  nodeHasPort,
9522
10318
  overlayStyles,
10319
+ readAppearanceValue,
9523
10320
  registerSymbol,
9524
10321
  registerSymbols,
10322
+ resolvePresetColor,
9525
10323
  resolveStatusColor,
10324
+ useBooleanIndicators,
9526
10325
  useDeviceConfigs,
9527
10326
  useDeviceControls,
9528
10327
  useDrag,
@@ -9533,6 +10332,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9533
10332
  useTelemetryStatus,
9534
10333
  useTheme,
9535
10334
  useViewBox,
9536
- validateDiagram
10335
+ validateDiagram,
10336
+ writeAppearanceValue
9537
10337
  });
9538
10338
  //# sourceMappingURL=index.cjs.map