@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.js CHANGED
@@ -174,6 +174,8 @@ var ValueEntry = ({
174
174
  inputType = "numeric",
175
175
  label,
176
176
  readOnly = false,
177
+ forced = false,
178
+ realValue,
177
179
  className = "",
178
180
  disabled,
179
181
  ...props
@@ -224,9 +226,14 @@ var ValueEntry = ({
224
226
  const unitClass = unit ? "ui-value-entry--with-unit" : "";
225
227
  const currentStatus = disabled || readOnly ? "disabled" : status;
226
228
  const statusVariantClass = currentStatus ? `ui-value-entry--${currentStatus}` : "";
229
+ const forcedClass = forced ? "ui-value-entry--forced" : "";
230
+ const hasRealValue = forced && realValue !== void 0;
227
231
  const displayText = isEditing ? displayValue : inputType === "numeric" ? Number(value).toFixed(precision) : String(value);
228
232
  return /* @__PURE__ */ jsxs2("div", { className: "ui-value-entry-container", children: [
229
- label && /* @__PURE__ */ jsx3("label", { className: "ui-value-entry-label", children: label }),
233
+ label && /* @__PURE__ */ jsxs2("label", { className: "ui-value-entry-label", children: [
234
+ label,
235
+ forced && /* @__PURE__ */ jsx3("span", { className: "ui-value-entry-forced-badge", "aria-label": "Forced", children: "F" })
236
+ ] }),
230
237
  /* @__PURE__ */ jsxs2("div", { className: "ui-value-entry-wrapper", children: [
231
238
  /* @__PURE__ */ jsx3(
232
239
  "input",
@@ -239,19 +246,71 @@ var ValueEntry = ({
239
246
  onKeyDown: handleKeyDown,
240
247
  disabled,
241
248
  readOnly,
242
- className: `${baseClass} ${statusVariantClass} ${unitClass} ${className}`.trim(),
249
+ className: `${baseClass} ${statusVariantClass} ${forcedClass} ${unitClass} ${className}`.trim(),
243
250
  ...props
244
251
  }
245
252
  ),
246
253
  unit && /* @__PURE__ */ jsx3("span", { className: "ui-value-entry-unit", children: unit }),
247
254
  status === "alarm" && /* @__PURE__ */ jsx3("div", { className: "ui-value-entry-badge ui-value-entry-badge--alarm" }),
248
- status === "warning" && /* @__PURE__ */ jsx3("div", { className: "ui-value-entry-badge ui-value-entry-badge--warning" })
255
+ status === "warning" && /* @__PURE__ */ jsx3("div", { className: "ui-value-entry-badge ui-value-entry-badge--warning" }),
256
+ !label && forced && /* @__PURE__ */ jsx3("span", { className: "ui-value-entry-forced-badge", "aria-label": "Forced", children: "F" })
257
+ ] }),
258
+ hasRealValue && /* @__PURE__ */ jsxs2("div", { className: "ui-value-entry-real-value", children: [
259
+ "REAL ",
260
+ String(realValue),
261
+ unit ? ` ${unit}` : ""
249
262
  ] })
250
263
  ] });
251
264
  };
252
265
 
253
266
  // src/components/ValueEntry/LegacyValueEntry.tsx
254
267
  import { useState as useState3, useCallback as useCallback2 } from "react";
268
+
269
+ // src/theme/statusColors.ts
270
+ var DEFAULT_STATUS_COLORS = {
271
+ normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
272
+ warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
273
+ alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
274
+ fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
275
+ off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
276
+ starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
277
+ stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
278
+ };
279
+ var FORCED_MARK_COLOR = {
280
+ fill: "#ede9fe",
281
+ stroke: "#5b21b6",
282
+ bg: "#ede9fe",
283
+ text: "#5b21b6",
284
+ border: "#7c3aed"
285
+ };
286
+ var UNKNOWN_MARK_COLOR = {
287
+ fill: "#ebebf5",
288
+ stroke: "#8f8fc2",
289
+ bg: "#ebebf5",
290
+ text: "#4f4f8a",
291
+ border: "#8f8fc2"
292
+ };
293
+ var UNKNOWN_MARK_COLOR_DARK = {
294
+ fill: "#23263a",
295
+ stroke: "#6f74a3",
296
+ bg: "#23263a",
297
+ text: "#b9bbdb",
298
+ border: "#6f74a3"
299
+ };
300
+ var STATUS_ALIASES = {
301
+ running: "normal",
302
+ stopped: "off"
303
+ };
304
+ function resolveStatusColor(status, customMap) {
305
+ const raw = (status || "off").toLowerCase();
306
+ const key = STATUS_ALIASES[raw] ?? raw;
307
+ const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
308
+ const overrides = customMap?.[key];
309
+ if (!overrides) return defaults;
310
+ return { ...defaults, ...overrides };
311
+ }
312
+
313
+ // src/components/ValueEntry/LegacyValueEntry.tsx
255
314
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
256
315
  var LegacyValueEntry = ({
257
316
  value,
@@ -264,6 +323,8 @@ var LegacyValueEntry = ({
264
323
  inputType = "numeric",
265
324
  label,
266
325
  readOnly = false,
326
+ forced = false,
327
+ realValue,
267
328
  className = "",
268
329
  disabled,
269
330
  ...props
@@ -344,6 +405,27 @@ var LegacyValueEntry = ({
344
405
  }
345
406
  };
346
407
  const statusStyles = getStatusStyles();
408
+ const forcedBadgeStyle = {
409
+ display: "inline-flex",
410
+ alignItems: "center",
411
+ justifyContent: "center",
412
+ width: "1.1rem",
413
+ height: "1.1rem",
414
+ fontSize: "0.7rem",
415
+ fontWeight: 700,
416
+ color: FORCED_MARK_COLOR.text,
417
+ backgroundColor: FORCED_MARK_COLOR.bg,
418
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
419
+ borderRadius: "50%",
420
+ marginLeft: "8px",
421
+ lineHeight: 1
422
+ };
423
+ const realValueStyle = {
424
+ fontSize: "0.75rem",
425
+ fontWeight: 500,
426
+ color: "#94a3b8",
427
+ letterSpacing: "0.02em"
428
+ };
347
429
  const inputStyle = {
348
430
  width: "12rem",
349
431
  height: "4rem",
@@ -392,17 +474,22 @@ var LegacyValueEntry = ({
392
474
  gap: "0.75rem"
393
475
  },
394
476
  children: [
395
- label && /* @__PURE__ */ jsx4(
477
+ (label || forced) && /* @__PURE__ */ jsxs3(
396
478
  "label",
397
479
  {
398
480
  style: {
481
+ display: "flex",
482
+ alignItems: "center",
399
483
  fontSize: "0.875rem",
400
484
  fontWeight: "600",
401
485
  color: "#cbd5e1",
402
486
  textTransform: "uppercase",
403
487
  letterSpacing: "0.05em"
404
488
  },
405
- children: label
489
+ children: [
490
+ label,
491
+ forced && /* @__PURE__ */ jsx4("span", { style: forcedBadgeStyle, "aria-label": "Forced", children: "F" })
492
+ ]
406
493
  }
407
494
  ),
408
495
  /* @__PURE__ */ jsxs3("div", { style: { position: "relative", display: "inline-flex", alignItems: "center" }, children: [
@@ -459,6 +546,11 @@ var LegacyValueEntry = ({
459
546
  }
460
547
  }
461
548
  )
549
+ ] }),
550
+ forced && realValue !== void 0 && /* @__PURE__ */ jsxs3("div", { style: realValueStyle, children: [
551
+ "REAL ",
552
+ String(realValue),
553
+ unit ? ` ${unit}` : ""
462
554
  ] })
463
555
  ]
464
556
  }
@@ -474,6 +566,9 @@ var ControlPanel = ({
474
566
  onModeChange,
475
567
  setpointValue,
476
568
  onSetpointChange,
569
+ currentSetpointValue,
570
+ currentSetpointForced = false,
571
+ currentSetpointRealValue,
477
572
  secondSetpointValue,
478
573
  onSecondSetpointChange,
479
574
  modeConfigs = {},
@@ -570,6 +665,21 @@ var ControlPanel = ({
570
665
  disabled: status === "disabled"
571
666
  }
572
667
  ) }),
668
+ currentSetpointValue !== void 0 && /* @__PURE__ */ jsx5("div", { children: /* @__PURE__ */ jsx5(
669
+ LegacyValueEntry,
670
+ {
671
+ value: currentSetpointValue,
672
+ onChange: () => {
673
+ },
674
+ label: `${currentLabel} (Current)`,
675
+ unit: currentUnit,
676
+ precision: currentPrecision,
677
+ status,
678
+ readOnly: true,
679
+ forced: currentSetpointForced,
680
+ realValue: currentSetpointRealValue
681
+ }
682
+ ) }),
573
683
  hasSecondSetpoint && onSecondSetpointChange && /* @__PURE__ */ jsx5("div", { children: /* @__PURE__ */ jsx5(
574
684
  LegacyValueEntry,
575
685
  {
@@ -660,9 +770,15 @@ var StatusIndicator = ({
660
770
  labelPosition = "right",
661
771
  shape = "circle",
662
772
  led = true,
773
+ visible = true,
774
+ forced = false,
775
+ realValue,
663
776
  className = "",
664
777
  ...props
665
778
  }) => {
779
+ if (!visible) {
780
+ return null;
781
+ }
666
782
  const baseClass = "ui-status-indicator";
667
783
  const statusClass = status ? `ui-status-indicator--${status}` : "";
668
784
  const sizeClass = size ? `ui-status-indicator--${size}` : "";
@@ -683,28 +799,43 @@ var StatusIndicator = ({
683
799
  return "ui-status-indicator-container ui-status-indicator-label-right";
684
800
  }
685
801
  };
802
+ const forcedClass = forced ? "ui-status-indicator--forced" : "";
803
+ const hasRealValue = forced && realValue !== void 0;
686
804
  return /* @__PURE__ */ jsxs5("span", { className: getContainerLayout(), children: [
687
805
  label && (labelPosition === "left" || labelPosition === "top") && /* @__PURE__ */ jsx6("span", { className: "ui-status-indicator-label", children: label }),
688
- /* @__PURE__ */ jsx6(
689
- "span",
690
- {
691
- role: "status",
692
- "aria-label": label || `Status: ${status}`,
693
- "aria-description": getStatusDescription(status),
694
- "data-status": status,
695
- className: `
696
- ${baseClass}
697
- ${statusClass}
698
- ${sizeClass}
699
- ${shapeClass}
700
- ${animationClass}
701
- ${ledClass}
702
- ${className}
703
- `.replace(/\s+/g, " ").trim(),
704
- ...props
705
- }
706
- ),
707
- label && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ jsx6("span", { className: "ui-status-indicator-label", children: label })
806
+ hasRealValue && (labelPosition === "left" || labelPosition === "top") && /* @__PURE__ */ jsxs5("span", { className: "ui-status-indicator-real-value", children: [
807
+ "REAL ",
808
+ realValue
809
+ ] }),
810
+ /* @__PURE__ */ jsxs5("span", { style: { position: "relative", display: "inline-flex" }, children: [
811
+ /* @__PURE__ */ jsx6(
812
+ "span",
813
+ {
814
+ role: "status",
815
+ "aria-label": label || `Status: ${status}`,
816
+ "aria-description": getStatusDescription(status),
817
+ "data-status": status,
818
+ "data-forced": forced,
819
+ className: `
820
+ ${baseClass}
821
+ ${statusClass}
822
+ ${sizeClass}
823
+ ${shapeClass}
824
+ ${animationClass}
825
+ ${ledClass}
826
+ ${forcedClass}
827
+ ${className}
828
+ `.replace(/\s+/g, " ").trim(),
829
+ ...props
830
+ }
831
+ ),
832
+ forced && /* @__PURE__ */ jsx6("span", { className: "ui-status-indicator-forced-badge", "aria-label": "Forced", children: "F" })
833
+ ] }),
834
+ label && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ jsx6("span", { className: "ui-status-indicator-label", children: label }),
835
+ hasRealValue && (labelPosition === "right" || labelPosition === "bottom") && /* @__PURE__ */ jsxs5("span", { className: "ui-status-indicator-real-value", children: [
836
+ "REAL ",
837
+ realValue
838
+ ] })
708
839
  ] });
709
840
  };
710
841
 
@@ -832,6 +963,9 @@ var NodeControlsPanel = ({
832
963
  onModeChange: config.onModeChange,
833
964
  setpointValue: config.setpoint,
834
965
  onSetpointChange: config.onSetpointChange,
966
+ currentSetpointValue: config.currentSetpoint,
967
+ currentSetpointForced: config.currentSetpointForced,
968
+ currentSetpointRealValue: config.currentSetpointRealValue,
835
969
  secondSetpointValue: config.secondSetpoint,
836
970
  onSecondSetpointChange: config.onSecondSetpointChange,
837
971
  modeConfigs: config.modeConfigs,
@@ -896,31 +1030,6 @@ var NodeControlsPanel = ({
896
1030
 
897
1031
  // src/components/DeviceControlPanel/DeviceControlPanel.tsx
898
1032
  import React5, { useState as useState5, useEffect as useEffect2 } from "react";
899
-
900
- // src/theme/statusColors.ts
901
- var DEFAULT_STATUS_COLORS = {
902
- normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
903
- warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
904
- alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
905
- fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
906
- off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
907
- starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
908
- stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
909
- };
910
- var STATUS_ALIASES = {
911
- running: "normal",
912
- stopped: "off"
913
- };
914
- function resolveStatusColor(status, customMap) {
915
- const raw = (status || "off").toLowerCase();
916
- const key = STATUS_ALIASES[raw] ?? raw;
917
- const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
918
- const overrides = customMap?.[key];
919
- if (!overrides) return defaults;
920
- return { ...defaults, ...overrides };
921
- }
922
-
923
- // src/components/DeviceControlPanel/DeviceControlPanel.tsx
924
1033
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
925
1034
  function DeviceControlPanelImpl({
926
1035
  binding,
@@ -1568,10 +1677,13 @@ function DeviceControlPanelImpl({
1568
1677
  marginBottom: spacing.gap
1569
1678
  },
1570
1679
  children: [
1571
- /* @__PURE__ */ jsx8(
1680
+ /* @__PURE__ */ jsxs7(
1572
1681
  "span",
1573
1682
  {
1574
1683
  style: {
1684
+ display: "flex",
1685
+ alignItems: "center",
1686
+ gap: "6px",
1575
1687
  fontSize: `${fontSize.label}px`,
1576
1688
  fontWeight: 600,
1577
1689
  textTransform: "uppercase",
@@ -1579,7 +1691,29 @@ function DeviceControlPanelImpl({
1579
1691
  fontFamily: "Arial, sans-serif",
1580
1692
  color: mutedTextColor
1581
1693
  },
1582
- children: param.label
1694
+ children: [
1695
+ param.label,
1696
+ param.forced?.active && /* @__PURE__ */ jsx8(
1697
+ "span",
1698
+ {
1699
+ "aria-label": "Forced",
1700
+ style: {
1701
+ display: "inline-flex",
1702
+ alignItems: "center",
1703
+ justifyContent: "center",
1704
+ width: "16px",
1705
+ height: "16px",
1706
+ fontSize: "10px",
1707
+ fontWeight: 700,
1708
+ color: FORCED_MARK_COLOR.text,
1709
+ backgroundColor: FORCED_MARK_COLOR.bg,
1710
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
1711
+ borderRadius: "50%"
1712
+ },
1713
+ children: "F"
1714
+ }
1715
+ )
1716
+ ]
1583
1717
  }
1584
1718
  ),
1585
1719
  /* @__PURE__ */ jsxs7(
@@ -1601,6 +1735,27 @@ function DeviceControlPanelImpl({
1601
1735
  ]
1602
1736
  }
1603
1737
  ),
1738
+ param.forced?.active && param.forced.realValue !== void 0 && /* @__PURE__ */ jsxs7(
1739
+ "div",
1740
+ {
1741
+ style: {
1742
+ display: "flex",
1743
+ justifyContent: "flex-end",
1744
+ marginTop: "-4px",
1745
+ marginBottom: spacing.gap,
1746
+ fontSize: "10px",
1747
+ fontWeight: 500,
1748
+ fontFamily: "Arial, sans-serif",
1749
+ color: mutedTextColor
1750
+ },
1751
+ children: [
1752
+ "REAL",
1753
+ " ",
1754
+ typeof param.forced.realValue === "boolean" ? param.forced.realValue ? "ON" : "OFF" : String(param.forced.realValue),
1755
+ param.unit ? ` ${param.unit}` : ""
1756
+ ]
1757
+ }
1758
+ ),
1604
1759
  param.type === "boolean" ? /* @__PURE__ */ jsx8(
1605
1760
  "button",
1606
1761
  {
@@ -2699,19 +2854,38 @@ var CardUnit = styled2("div")({
2699
2854
 
2700
2855
  // src/components/layout/FullscreenWorkspace/FullscreenWorkspace.visuals.tsx
2701
2856
  import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2857
+ var forcedTagStyle = {
2858
+ position: "absolute",
2859
+ top: "-6px",
2860
+ right: "-6px",
2861
+ width: "14px",
2862
+ height: "14px",
2863
+ display: "flex",
2864
+ alignItems: "center",
2865
+ justifyContent: "center",
2866
+ fontSize: "9px",
2867
+ fontWeight: 700,
2868
+ color: FORCED_MARK_COLOR.text,
2869
+ backgroundColor: FORCED_MARK_COLOR.bg,
2870
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
2871
+ borderRadius: "50%",
2872
+ pointerEvents: "none"
2873
+ };
2702
2874
  var TrendLine = ({
2703
2875
  values,
2704
2876
  width = 60,
2705
2877
  height = 20,
2706
2878
  color = "#4fc3f7",
2707
2879
  strokeWidth = 1.5,
2708
- backgroundColor = "rgba(255, 255, 255, 0.1)"
2880
+ backgroundColor = "rgba(255, 255, 255, 0.1)",
2881
+ forced = false
2709
2882
  }) => {
2710
2883
  if (!values || values.length < 2) {
2711
- return /* @__PURE__ */ jsx12(
2884
+ return /* @__PURE__ */ jsxs9(
2712
2885
  "div",
2713
2886
  {
2714
2887
  style: {
2888
+ position: "relative",
2715
2889
  width,
2716
2890
  height,
2717
2891
  backgroundColor,
@@ -2720,7 +2894,10 @@ var TrendLine = ({
2720
2894
  alignItems: "center",
2721
2895
  justifyContent: "center"
2722
2896
  },
2723
- children: /* @__PURE__ */ jsx12("span", { style: { fontSize: "8px", color: "#ccc" }, children: "No data" })
2897
+ children: [
2898
+ /* @__PURE__ */ jsx12("span", { style: { fontSize: "8px", color: "#ccc" }, children: "No data" }),
2899
+ forced && /* @__PURE__ */ jsx12("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
2900
+ ]
2724
2901
  }
2725
2902
  );
2726
2903
  }
@@ -2732,7 +2909,7 @@ var TrendLine = ({
2732
2909
  const x = 4 + i / (values.length - 1) * (width - 8);
2733
2910
  return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
2734
2911
  }).join(" ");
2735
- return /* @__PURE__ */ jsx12(
2912
+ return /* @__PURE__ */ jsxs9(
2736
2913
  "div",
2737
2914
  {
2738
2915
  style: {
@@ -2741,30 +2918,41 @@ var TrendLine = ({
2741
2918
  backgroundColor,
2742
2919
  borderRadius: "2px",
2743
2920
  position: "relative",
2744
- overflow: "hidden"
2921
+ overflow: "visible"
2745
2922
  },
2746
- children: /* @__PURE__ */ jsxs9("svg", { width, height, style: { position: "absolute", top: 0, left: 0 }, children: [
2747
- /* @__PURE__ */ jsx12(
2748
- "path",
2923
+ children: [
2924
+ /* @__PURE__ */ jsxs9(
2925
+ "svg",
2749
2926
  {
2750
- d: pathData,
2751
- fill: "none",
2752
- stroke: color,
2753
- strokeWidth,
2754
- strokeLinecap: "round",
2755
- strokeLinejoin: "round"
2927
+ width,
2928
+ height,
2929
+ style: { position: "absolute", top: 0, left: 0, overflow: "hidden", borderRadius: "2px" },
2930
+ children: [
2931
+ /* @__PURE__ */ jsx12(
2932
+ "path",
2933
+ {
2934
+ d: pathData,
2935
+ fill: "none",
2936
+ stroke: color,
2937
+ strokeWidth,
2938
+ strokeLinecap: "round",
2939
+ strokeLinejoin: "round"
2940
+ }
2941
+ ),
2942
+ /* @__PURE__ */ jsx12(
2943
+ "circle",
2944
+ {
2945
+ cx: 4 + (width - 8),
2946
+ cy: normalizedValues[normalizedValues.length - 1],
2947
+ r: 1.5,
2948
+ fill: color
2949
+ }
2950
+ )
2951
+ ]
2756
2952
  }
2757
2953
  ),
2758
- /* @__PURE__ */ jsx12(
2759
- "circle",
2760
- {
2761
- cx: 4 + (width - 8),
2762
- cy: normalizedValues[normalizedValues.length - 1],
2763
- r: 1.5,
2764
- fill: color
2765
- }
2766
- )
2767
- ] })
2954
+ forced && /* @__PURE__ */ jsx12("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
2955
+ ]
2768
2956
  }
2769
2957
  );
2770
2958
  };
@@ -2780,7 +2968,9 @@ var CircularGauge = ({
2780
2968
  unit,
2781
2969
  label,
2782
2970
  size = 60,
2783
- color = "rgba(255, 255, 255, 0.9)"
2971
+ color = "rgba(255, 255, 255, 0.9)",
2972
+ forced = false,
2973
+ realValue
2784
2974
  }) => {
2785
2975
  const percentage = Math.min(value / max * 100, 100);
2786
2976
  const strokeWidth = 4;
@@ -2811,7 +3001,7 @@ var CircularGauge = ({
2811
3001
  height: size,
2812
3002
  borderRadius: "50%",
2813
3003
  backgroundColor,
2814
- border: `1px solid ${borderStrokeColor}`
3004
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : `1px solid ${borderStrokeColor}`
2815
3005
  },
2816
3006
  children: [
2817
3007
  /* @__PURE__ */ jsxs9("svg", { width: size, height: size, style: { transform: "rotate(-90deg)" }, children: [
@@ -2887,12 +3077,39 @@ var CircularGauge = ({
2887
3077
  },
2888
3078
  children: label
2889
3079
  }
2890
- )
3080
+ ),
3081
+ forced && /* @__PURE__ */ jsx12(
3082
+ "span",
3083
+ {
3084
+ "aria-label": "Forced",
3085
+ style: {
3086
+ display: "inline-flex",
3087
+ alignItems: "center",
3088
+ justifyContent: "center",
3089
+ width: "14px",
3090
+ height: "14px",
3091
+ fontSize: "9px",
3092
+ fontWeight: 700,
3093
+ color: FORCED_MARK_COLOR.text,
3094
+ backgroundColor: FORCED_MARK_COLOR.bg,
3095
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
3096
+ borderRadius: "50%",
3097
+ marginTop: "2px"
3098
+ },
3099
+ children: "F"
3100
+ }
3101
+ ),
3102
+ forced && realValue !== void 0 && /* @__PURE__ */ jsxs9("div", { style: { fontSize: "8px", opacity: 0.8, color: "white", marginTop: "1px" }, children: [
3103
+ "REAL ",
3104
+ realValue.toFixed(1),
3105
+ " ",
3106
+ unit
3107
+ ] })
2891
3108
  ]
2892
3109
  }
2893
3110
  );
2894
3111
  };
2895
- var Sparkline = ({ data, width, height, color }) => {
3112
+ var Sparkline = ({ data, width, height, color, forced = false }) => {
2896
3113
  if (!data || data.length < 2) {
2897
3114
  return null;
2898
3115
  }
@@ -2907,24 +3124,52 @@ var Sparkline = ({ data, width, height, color }) => {
2907
3124
  const y = height - 8 - (value - min) / range * (height - 8);
2908
3125
  return `${x + 4},${y + 4}`;
2909
3126
  }).join(" ");
2910
- return /* @__PURE__ */ jsx12(
3127
+ return /* @__PURE__ */ jsxs9(
2911
3128
  "div",
2912
3129
  {
2913
3130
  style: {
3131
+ position: "relative",
2914
3132
  width,
2915
3133
  height,
2916
3134
  backgroundColor: "rgba(0, 0, 0, 0.7)",
2917
- border: "1px solid rgba(255, 255, 255, 0.3)",
3135
+ border: forced ? `1px solid ${FORCED_MARK_COLOR.border}` : "1px solid rgba(255, 255, 255, 0.3)",
2918
3136
  borderRadius: "4px",
2919
3137
  display: "flex",
2920
3138
  alignItems: "center",
2921
3139
  justifyContent: "center",
2922
3140
  boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)"
2923
3141
  },
2924
- children: /* @__PURE__ */ jsx12("svg", { width, height, children: /* @__PURE__ */ jsx12("polyline", { points, fill: "none", stroke: color, strokeWidth: "1.5", opacity: "0.9" }) })
3142
+ children: [
3143
+ /* @__PURE__ */ jsx12("svg", { width, height, children: /* @__PURE__ */ jsx12("polyline", { points, fill: "none", stroke: color, strokeWidth: "1.5", opacity: "0.9" }) }),
3144
+ forced && /* @__PURE__ */ jsx12("span", { style: forcedTagStyle, "aria-label": "Forced", children: "F" })
3145
+ ]
2925
3146
  }
2926
3147
  );
2927
3148
  };
3149
+ var EquipmentForcedBadge = () => /* @__PURE__ */ jsx12(
3150
+ "span",
3151
+ {
3152
+ "aria-label": "Forced",
3153
+ style: {
3154
+ position: "absolute",
3155
+ top: "-4px",
3156
+ right: "-4px",
3157
+ width: "11px",
3158
+ height: "11px",
3159
+ borderRadius: "50%",
3160
+ backgroundColor: FORCED_MARK_COLOR.bg,
3161
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
3162
+ color: FORCED_MARK_COLOR.text,
3163
+ fontSize: "7px",
3164
+ fontWeight: 700,
3165
+ display: "flex",
3166
+ alignItems: "center",
3167
+ justifyContent: "center",
3168
+ lineHeight: 1
3169
+ },
3170
+ children: "F"
3171
+ }
3172
+ );
2928
3173
  var EquipmentIndicatorWithSparkline = ({
2929
3174
  svgX,
2930
3175
  svgY,
@@ -2932,11 +3177,51 @@ var EquipmentIndicatorWithSparkline = ({
2932
3177
  label,
2933
3178
  size = 30,
2934
3179
  sparklineData,
2935
- sparklineOffset = { x: 40, y: 0 }
3180
+ sparklineOffset = { x: 40, y: 0 },
3181
+ forced = false
2936
3182
  }) => {
2937
3183
  const hasSparkline = sparklineData && sparklineData.length > 1 && Math.max(...sparklineData) !== Math.min(...sparklineData);
2938
3184
  return /* @__PURE__ */ jsxs9(Fragment3, { children: [
2939
- /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX, svgY, style: { transform: "translate(-50%, -50%)" }, children: /* @__PURE__ */ jsx12(
3185
+ /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX, svgY, style: { transform: "translate(-50%, -50%)" }, children: /* @__PURE__ */ jsxs9("div", { style: { position: "relative", width: size, height: size }, children: [
3186
+ /* @__PURE__ */ jsx12(
3187
+ "div",
3188
+ {
3189
+ style: {
3190
+ width: size,
3191
+ height: size,
3192
+ borderRadius: "50%",
3193
+ backgroundColor: color,
3194
+ display: "flex",
3195
+ alignItems: "center",
3196
+ justifyContent: "center",
3197
+ color: "white",
3198
+ fontSize: "7px",
3199
+ fontWeight: "700",
3200
+ textAlign: "center",
3201
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
3202
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : "2px solid rgba(255, 255, 255, 0.3)"
3203
+ },
3204
+ children: label
3205
+ }
3206
+ ),
3207
+ forced && /* @__PURE__ */ jsx12(EquipmentForcedBadge, {})
3208
+ ] }) }),
3209
+ hasSparkline && /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX: svgX + sparklineOffset.x, svgY: svgY + sparklineOffset.y, children: /* @__PURE__ */ jsx12(Sparkline, { data: sparklineData, width: 60, height: 20, color, forced }) })
3210
+ ] });
3211
+ };
3212
+ var EquipmentIndicator = ({
3213
+ svgX,
3214
+ svgY,
3215
+ color,
3216
+ label,
3217
+ size = 30,
3218
+ style,
3219
+ className,
3220
+ forced = false
3221
+ }) => {
3222
+ const overlayStyle = style !== void 0 ? { transform: "translate(-50%, -50%)", ...style } : { transform: "translate(-50%, -50%)" };
3223
+ return /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX, svgY, style: overlayStyle, className, children: /* @__PURE__ */ jsxs9("div", { style: { position: "relative", width: size, height: size }, children: [
3224
+ /* @__PURE__ */ jsx12(
2940
3225
  "div",
2941
3226
  {
2942
3227
  style: {
@@ -2952,45 +3237,13 @@ var EquipmentIndicatorWithSparkline = ({
2952
3237
  fontWeight: "700",
2953
3238
  textAlign: "center",
2954
3239
  boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
2955
- border: "2px solid rgba(255, 255, 255, 0.3)"
3240
+ border: forced ? `2px solid ${FORCED_MARK_COLOR.border}` : "2px solid rgba(255, 255, 255, 0.3)"
2956
3241
  },
2957
3242
  children: label
2958
3243
  }
2959
- ) }),
2960
- hasSparkline && /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX: svgX + sparklineOffset.x, svgY: svgY + sparklineOffset.y, children: /* @__PURE__ */ jsx12(Sparkline, { data: sparklineData, width: 60, height: 20, color }) })
2961
- ] });
2962
- };
2963
- var EquipmentIndicator = ({
2964
- svgX,
2965
- svgY,
2966
- color,
2967
- label,
2968
- size = 30,
2969
- style,
2970
- className
2971
- }) => {
2972
- const overlayStyle = style !== void 0 ? { transform: "translate(-50%, -50%)", ...style } : { transform: "translate(-50%, -50%)" };
2973
- return /* @__PURE__ */ jsx12(SVGLockedOverlay, { svgX, svgY, style: overlayStyle, className, children: /* @__PURE__ */ jsx12(
2974
- "div",
2975
- {
2976
- style: {
2977
- width: size,
2978
- height: size,
2979
- borderRadius: "50%",
2980
- backgroundColor: color,
2981
- display: "flex",
2982
- alignItems: "center",
2983
- justifyContent: "center",
2984
- color: "white",
2985
- fontSize: "7px",
2986
- fontWeight: "700",
2987
- textAlign: "center",
2988
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
2989
- border: "2px solid rgba(255, 255, 255, 0.3)"
2990
- },
2991
- children: label
2992
- }
2993
- ) });
3244
+ ),
3245
+ forced && /* @__PURE__ */ jsx12(EquipmentForcedBadge, {})
3246
+ ] }) });
2994
3247
  };
2995
3248
 
2996
3249
  // src/components/layout/FullscreenWorkspace/FullscreenWorkspace.tsx
@@ -3280,7 +3533,7 @@ var ThemeToggle = ({
3280
3533
  };
3281
3534
 
3282
3535
  // src/components/PIDCanvas/PIDCanvas.tsx
3283
- import React13, { useCallback as useCallback8, useState as useState14, useEffect as useEffect12, useRef as useRef6 } from "react";
3536
+ import React13, { useCallback as useCallback8, useState as useState14, useEffect as useEffect13, useRef as useRef6 } from "react";
3284
3537
 
3285
3538
  // src/components/PIDCanvas/NodeRenderer.tsx
3286
3539
  import React9 from "react";
@@ -3311,6 +3564,25 @@ var mockSymbolLibrary = {
3311
3564
  tags: ["valve", "gate", "powered", "actuator"]
3312
3565
  }
3313
3566
  },
3567
+ "status-indicator": {
3568
+ id: "status-indicator",
3569
+ name: "Status LED",
3570
+ category: "instrument",
3571
+ viewBox: { x: 0, y: 0, width: 24, height: 24 },
3572
+ ports: [],
3573
+ svgContent: `
3574
+ <rect x="2" y="2" width="20" height="20"
3575
+ rx="var(--led-rx, 50%)"
3576
+ fill="var(--symbol-fill, #64748b)"
3577
+ stroke="rgba(255,255,255,0.4)"
3578
+ stroke-width="var(--led-bezel, 0)"
3579
+ style="transform-box: fill-box; transform: rotate(var(--led-rotate, 0deg)); transform-origin: center; pointer-events: all;"/>
3580
+ `,
3581
+ metadata: {
3582
+ description: "Status LED indicator driven by telemetry status color",
3583
+ tags: ["status", "led", "indicator", "instrument"]
3584
+ }
3585
+ },
3314
3586
  "pump-positive-displacement": {
3315
3587
  id: "pump-positive-displacement",
3316
3588
  name: "Positive Displacement Pump",
@@ -4503,8 +4775,144 @@ function getAvailableSymbols() {
4503
4775
  return [...Object.keys(customSymbols), ...Object.keys(mockSymbolLibrary)];
4504
4776
  }
4505
4777
 
4778
+ // src/diagram/symbols/appearanceSchema.ts
4779
+ var COLOR_PRESETS = {
4780
+ green: "#22c55e",
4781
+ red: "#ef4444",
4782
+ amber: "#f59e0b",
4783
+ blue: "#3b82f6",
4784
+ grey: "#64748b"
4785
+ };
4786
+ function resolvePresetColor(name) {
4787
+ return name && COLOR_PRESETS[name] || COLOR_PRESETS.grey;
4788
+ }
4789
+ var COLOR_OPTIONS = Object.keys(COLOR_PRESETS);
4790
+ var STATUS_LED_APPEARANCE = [
4791
+ {
4792
+ key: "shape",
4793
+ kind: "select",
4794
+ label: "Shape",
4795
+ options: ["circle", "square", "diamond"],
4796
+ default: "circle"
4797
+ },
4798
+ {
4799
+ key: "size",
4800
+ kind: "select",
4801
+ label: "Size",
4802
+ target: "scale",
4803
+ options: ["small", "medium", "large"],
4804
+ default: "medium",
4805
+ valueMap: { small: 0.75, medium: 1, large: 1.5 }
4806
+ },
4807
+ { key: "led", kind: "boolean", label: "Bezel ring", default: true },
4808
+ { key: "glow", kind: "boolean", label: "Diffusion (glow)", default: false },
4809
+ {
4810
+ key: "animation",
4811
+ kind: "select",
4812
+ label: "Animation",
4813
+ options: ["none", "pulse", "blink"],
4814
+ default: "none"
4815
+ },
4816
+ {
4817
+ key: "labelPosition",
4818
+ kind: "select",
4819
+ label: "Label position",
4820
+ options: ["bottom", "top", "left", "right"],
4821
+ default: "bottom"
4822
+ },
4823
+ // Color binding: a named signal drives the on/off color.
4824
+ {
4825
+ key: "colorSource",
4826
+ kind: "tag",
4827
+ label: "Color source (tag)",
4828
+ default: "",
4829
+ help: "Signal/tag that drives color. True (non-zero) shows On color; false shows Off color. Blank = unbound (grey)."
4830
+ },
4831
+ {
4832
+ key: "onColor",
4833
+ kind: "select",
4834
+ label: "On color (true)",
4835
+ options: COLOR_OPTIONS,
4836
+ default: "green",
4837
+ help: "Color when the Color source is true."
4838
+ },
4839
+ {
4840
+ key: "offColor",
4841
+ kind: "select",
4842
+ label: "Off color (false)",
4843
+ options: COLOR_OPTIONS,
4844
+ default: "red",
4845
+ help: "Color when the Color source is false."
4846
+ },
4847
+ // Visibility binding: an independent named signal drives show/hide.
4848
+ {
4849
+ key: "visibleSource",
4850
+ kind: "tag",
4851
+ label: "Visibility source (tag)",
4852
+ default: "",
4853
+ help: "Signal/tag that drives visibility. True shows the LED, false hides it. Blank = always visible (use the Visible checkbox)."
4854
+ },
4855
+ {
4856
+ key: "visible",
4857
+ kind: "boolean",
4858
+ label: "Visible",
4859
+ target: "visible",
4860
+ default: true,
4861
+ help: "Static show/hide, used only when no Visibility source is set."
4862
+ }
4863
+ ];
4864
+ var APPEARANCE_REGISTRY = {
4865
+ "status-indicator": STATUS_LED_APPEARANCE
4866
+ };
4867
+ function getAppearanceSchema(symbolId) {
4868
+ return APPEARANCE_REGISTRY[symbolId] ?? [];
4869
+ }
4870
+ function getAppearanceDefaults(symbolId) {
4871
+ const out = {};
4872
+ for (const field of getAppearanceSchema(symbolId)) {
4873
+ if ((field.target ?? "metadata") === "metadata" && field.default !== void 0) {
4874
+ out[field.key] = field.default;
4875
+ }
4876
+ }
4877
+ return out;
4878
+ }
4879
+ function readAppearanceValue(node, field) {
4880
+ if (field.target === "visible") {
4881
+ return node.visible ?? field.default ?? true;
4882
+ }
4883
+ if (field.target === "scale") {
4884
+ const scale = node.transform.scale ?? 1;
4885
+ if (field.valueMap) {
4886
+ let bestOption = field.options?.[0] ?? String(scale);
4887
+ let bestDiff = Infinity;
4888
+ for (const [option, value2] of Object.entries(field.valueMap)) {
4889
+ const diff = Math.abs(value2 - scale);
4890
+ if (diff < bestDiff) {
4891
+ bestDiff = diff;
4892
+ bestOption = option;
4893
+ }
4894
+ }
4895
+ return bestOption;
4896
+ }
4897
+ return scale;
4898
+ }
4899
+ const value = node.metadata?.[field.key];
4900
+ return value ?? field.default;
4901
+ }
4902
+ function writeAppearanceValue(node, field, value) {
4903
+ if (field.target === "visible") {
4904
+ return { visible: Boolean(value) };
4905
+ }
4906
+ if (field.target === "scale") {
4907
+ const scale = field.valueMap ? field.valueMap[String(value)] ?? 1 : Number(value);
4908
+ return { transform: { ...node.transform, scale } };
4909
+ }
4910
+ return { metadata: { ...node.metadata, [field.key]: value } };
4911
+ }
4912
+
4506
4913
  // src/components/PIDCanvas/NodeRenderer.tsx
4507
4914
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4915
+ var HATCH_SIZE = 6;
4508
4916
  function NodeRendererImpl({
4509
4917
  nodes: nodes2,
4510
4918
  selectedNodeIds,
@@ -4516,140 +4924,206 @@ function NodeRendererImpl({
4516
4924
  enableDrag = false,
4517
4925
  statusColorMap
4518
4926
  }) {
4519
- return /* @__PURE__ */ jsx16("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
4520
- const symbol = getSymbolDefinition(node.symbolId);
4521
- const isSelected = selectedNodeIds?.has(node.id) ?? false;
4522
- const isDragging = draggingNodeId === node.id;
4523
- if (!symbol) {
4524
- return /* @__PURE__ */ jsxs12("g", { children: [
4525
- /* @__PURE__ */ jsx16(
4526
- "circle",
4527
- {
4528
- cx: node.transform.x,
4529
- cy: node.transform.y,
4530
- r: "15",
4531
- fill: "#ff0000",
4532
- opacity: "0.3",
4533
- stroke: "#ff0000",
4534
- strokeWidth: "2"
4535
- }
4536
- ),
4537
- /* @__PURE__ */ jsx16(
4538
- "text",
4539
- {
4540
- x: node.transform.x,
4541
- y: node.transform.y,
4542
- textAnchor: "middle",
4543
- dominantBaseline: "middle",
4544
- fontSize: "10",
4545
- fill: "#ff0000",
4546
- children: "?"
4547
- }
4548
- )
4549
- ] }, node.id);
4550
- }
4551
- const transforms = [];
4552
- const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
4553
- const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
4554
- transforms.push(`translate(${effectiveX}, ${effectiveY})`);
4555
- if (node.transform.rotation) {
4556
- const centerX = symbol.viewBox.width / 2;
4557
- const centerY = symbol.viewBox.height / 2;
4558
- transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
4559
- }
4560
- if (node.transform.scale && node.transform.scale !== 1) {
4561
- const centerX = symbol.viewBox.width / 2;
4562
- const centerY = symbol.viewBox.height / 2;
4563
- transforms.push(
4564
- `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
4565
- );
4566
- }
4567
- if (node.transform.flipX || node.transform.flipY) {
4568
- const scaleX = node.transform.flipX ? -1 : 1;
4569
- const scaleY = node.transform.flipY ? -1 : 1;
4570
- const centerX = symbol.viewBox.width / 2;
4571
- const centerY = symbol.viewBox.height / 2;
4572
- transforms.push(
4573
- `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
4574
- );
4575
- }
4576
- const statusEntry = resolveStatusColor(node.status, statusColorMap);
4577
- const fillColor = node.customColors?.fill ?? statusEntry.fill;
4578
- const strokeColor = node.customColors?.stroke ?? statusEntry.stroke;
4579
- return /* @__PURE__ */ jsxs12(
4580
- "g",
4927
+ const hatchId = `pid-unknown-hatch-${React9.useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
4928
+ const hasUnknown = nodes2.some((n) => n.positionUnknown === true && n.visible !== false);
4929
+ return /* @__PURE__ */ jsxs12("g", { id: "layer-nodes", children: [
4930
+ hasUnknown && /* @__PURE__ */ jsx16("defs", { children: /* @__PURE__ */ jsxs12(
4931
+ "pattern",
4581
4932
  {
4582
- "data-node-id": node.id,
4583
- "data-symbol-id": node.symbolId,
4584
- "data-selected": isSelected,
4585
- "data-dragging": isDragging,
4586
- transform: transforms.join(" "),
4587
- style: {
4588
- "--symbol-fill": fillColor,
4589
- "--symbol-stroke": strokeColor,
4590
- cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default"
4591
- },
4592
- onClick: (e) => {
4593
- e.stopPropagation();
4594
- onNodeClick?.(node.id, e);
4595
- },
4596
- onDoubleClick: (e) => {
4597
- e.stopPropagation();
4598
- onNodeDoubleClick?.(node.id, e);
4599
- },
4600
- onMouseDown: (e) => {
4601
- if (enableDrag) {
4602
- onNodeDragStart?.(node.id, e);
4603
- }
4604
- },
4605
- onTouchStart: (e) => {
4606
- if (enableDrag) {
4607
- e.stopPropagation();
4608
- onNodeDragStart?.(node.id, e);
4609
- }
4610
- },
4933
+ id: hatchId,
4934
+ "data-testid": "position-unknown-pattern",
4935
+ patternUnits: "userSpaceOnUse",
4936
+ width: HATCH_SIZE,
4937
+ height: HATCH_SIZE,
4938
+ patternTransform: "rotate(45)",
4611
4939
  children: [
4612
- isSelected && /* @__PURE__ */ jsx16(
4613
- "rect",
4940
+ /* @__PURE__ */ jsx16("rect", { width: HATCH_SIZE, height: HATCH_SIZE, fill: UNKNOWN_MARK_COLOR.fill }),
4941
+ /* @__PURE__ */ jsx16(
4942
+ "line",
4614
4943
  {
4615
- x: "-5",
4616
- y: "-5",
4617
- width: symbol.viewBox.width + 10,
4618
- height: symbol.viewBox.height + 10,
4619
- fill: "none",
4620
- stroke: isDragging ? "#10b981" : "#3b82f6",
4621
- strokeWidth: isDragging ? "3" : "2",
4622
- strokeDasharray: "4 2",
4623
- rx: "2",
4624
- pointerEvents: "none"
4944
+ x1: "0",
4945
+ y1: "0",
4946
+ x2: "0",
4947
+ y2: HATCH_SIZE,
4948
+ stroke: UNKNOWN_MARK_COLOR.text,
4949
+ strokeWidth: "2"
4625
4950
  }
4626
- ),
4951
+ )
4952
+ ]
4953
+ }
4954
+ ) }),
4955
+ nodes2.filter((node) => node.visible !== false).map((node) => {
4956
+ const symbol = getSymbolDefinition(node.symbolId);
4957
+ const isSelected = selectedNodeIds?.has(node.id) ?? false;
4958
+ const isDragging = draggingNodeId === node.id;
4959
+ if (!symbol) {
4960
+ return /* @__PURE__ */ jsxs12("g", { children: [
4627
4961
  /* @__PURE__ */ jsx16(
4628
- "g",
4962
+ "circle",
4629
4963
  {
4630
- dangerouslySetInnerHTML: { __html: symbol.svgContent },
4631
- "data-symbol-content": symbol.id,
4632
- style: { pointerEvents: "auto" }
4964
+ cx: node.transform.x,
4965
+ cy: node.transform.y,
4966
+ r: "15",
4967
+ fill: "#ff0000",
4968
+ opacity: "0.3",
4969
+ stroke: "#ff0000",
4970
+ strokeWidth: "2"
4633
4971
  }
4634
4972
  ),
4635
4973
  /* @__PURE__ */ jsx16(
4636
4974
  "text",
4637
4975
  {
4638
- x: symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0),
4639
- y: symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0),
4976
+ x: node.transform.x,
4977
+ y: node.transform.y,
4640
4978
  textAnchor: "middle",
4641
- fontSize: node.labelFontSize ?? 10,
4642
- fontWeight: "600",
4643
- fill: isSelected ? "#3b82f6" : "#1f2937",
4644
- 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,
4645
- children: node.label || node.id
4979
+ dominantBaseline: "middle",
4980
+ fontSize: "10",
4981
+ fill: "#ff0000",
4982
+ children: "?"
4646
4983
  }
4647
4984
  )
4648
- ]
4649
- },
4650
- node.id
4651
- );
4652
- }) });
4985
+ ] }, node.id);
4986
+ }
4987
+ const transforms = [];
4988
+ const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
4989
+ const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
4990
+ transforms.push(`translate(${effectiveX}, ${effectiveY})`);
4991
+ if (node.transform.rotation) {
4992
+ const centerX = symbol.viewBox.width / 2;
4993
+ const centerY = symbol.viewBox.height / 2;
4994
+ transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
4995
+ }
4996
+ if (node.transform.scale && node.transform.scale !== 1) {
4997
+ const centerX = symbol.viewBox.width / 2;
4998
+ const centerY = symbol.viewBox.height / 2;
4999
+ transforms.push(
5000
+ `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
5001
+ );
5002
+ }
5003
+ if (node.transform.flipX || node.transform.flipY) {
5004
+ const scaleX = node.transform.flipX ? -1 : 1;
5005
+ const scaleY = node.transform.flipY ? -1 : 1;
5006
+ const centerX = symbol.viewBox.width / 2;
5007
+ const centerY = symbol.viewBox.height / 2;
5008
+ transforms.push(
5009
+ `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
5010
+ );
5011
+ }
5012
+ const statusEntry = resolveStatusColor(node.status, statusColorMap);
5013
+ const isUnknown = node.positionUnknown === true;
5014
+ const fillColor = isUnknown ? `url(#${hatchId})` : node.customColors?.fill ?? statusEntry.fill;
5015
+ const strokeColor = isUnknown ? UNKNOWN_MARK_COLOR.stroke : node.customColors?.stroke ?? statusEntry.stroke;
5016
+ const appearance = { ...getAppearanceDefaults(node.symbolId), ...node.metadata ?? {} };
5017
+ const ledStyle = {};
5018
+ if (appearance.shape !== void 0) {
5019
+ ledStyle["--led-rx"] = appearance.shape === "circle" ? "50%" : "2";
5020
+ ledStyle["--led-rotate"] = appearance.shape === "diamond" ? "45deg" : "0deg";
5021
+ }
5022
+ if (appearance.led !== void 0) {
5023
+ ledStyle["--led-bezel"] = appearance.led ? "2" : "0";
5024
+ }
5025
+ if (appearance.glow) {
5026
+ ledStyle.filter = `drop-shadow(0 0 4px ${fillColor})`;
5027
+ }
5028
+ const ledAnimation = typeof appearance.animation === "string" && appearance.animation !== "none" ? appearance.animation : void 0;
5029
+ const vb = symbol.viewBox;
5030
+ const ox = node.labelOffset?.x ?? 0;
5031
+ const oy = node.labelOffset?.y ?? 0;
5032
+ const labelPos = typeof appearance.labelPosition === "string" ? appearance.labelPosition : "bottom";
5033
+ let labelX = vb.width / 2 + ox;
5034
+ let labelY = vb.height + 12 + oy;
5035
+ let labelAnchor = "middle";
5036
+ if (labelPos === "top") {
5037
+ labelX = vb.width / 2 + ox;
5038
+ labelY = -6 + oy;
5039
+ labelAnchor = "middle";
5040
+ } else if (labelPos === "left") {
5041
+ labelX = -6 + ox;
5042
+ labelY = vb.height / 2 + oy;
5043
+ labelAnchor = "end";
5044
+ } else if (labelPos === "right") {
5045
+ labelX = vb.width + 6 + ox;
5046
+ labelY = vb.height / 2 + oy;
5047
+ labelAnchor = "start";
5048
+ }
5049
+ return /* @__PURE__ */ jsxs12(
5050
+ "g",
5051
+ {
5052
+ "data-node-id": node.id,
5053
+ "data-symbol-id": node.symbolId,
5054
+ "data-selected": isSelected,
5055
+ "data-dragging": isDragging,
5056
+ "data-led-animation": ledAnimation,
5057
+ "data-position-unknown": isUnknown ? "true" : void 0,
5058
+ transform: transforms.join(" "),
5059
+ style: {
5060
+ "--symbol-fill": fillColor,
5061
+ "--symbol-stroke": strokeColor,
5062
+ cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default",
5063
+ ...ledStyle
5064
+ },
5065
+ onClick: (e) => {
5066
+ e.stopPropagation();
5067
+ onNodeClick?.(node.id, e);
5068
+ },
5069
+ onDoubleClick: (e) => {
5070
+ e.stopPropagation();
5071
+ onNodeDoubleClick?.(node.id, e);
5072
+ },
5073
+ onMouseDown: (e) => {
5074
+ if (enableDrag) {
5075
+ onNodeDragStart?.(node.id, e);
5076
+ }
5077
+ },
5078
+ onTouchStart: (e) => {
5079
+ if (enableDrag) {
5080
+ e.stopPropagation();
5081
+ onNodeDragStart?.(node.id, e);
5082
+ }
5083
+ },
5084
+ children: [
5085
+ isSelected && /* @__PURE__ */ jsx16(
5086
+ "rect",
5087
+ {
5088
+ x: "-5",
5089
+ y: "-5",
5090
+ width: symbol.viewBox.width + 10,
5091
+ height: symbol.viewBox.height + 10,
5092
+ fill: "none",
5093
+ stroke: isDragging ? "#10b981" : "#3b82f6",
5094
+ strokeWidth: isDragging ? "3" : "2",
5095
+ strokeDasharray: "4 2",
5096
+ rx: "2",
5097
+ pointerEvents: "none"
5098
+ }
5099
+ ),
5100
+ /* @__PURE__ */ jsx16(
5101
+ "g",
5102
+ {
5103
+ dangerouslySetInnerHTML: { __html: symbol.svgContent },
5104
+ "data-symbol-content": symbol.id,
5105
+ style: { pointerEvents: "auto" }
5106
+ }
5107
+ ),
5108
+ /* @__PURE__ */ jsx16(
5109
+ "text",
5110
+ {
5111
+ x: labelX,
5112
+ y: labelY,
5113
+ textAnchor: labelAnchor,
5114
+ fontSize: node.labelFontSize ?? 10,
5115
+ fontWeight: "600",
5116
+ fill: isSelected ? "#3b82f6" : "#1f2937",
5117
+ transform: node.transform.rotation ? `rotate(${-node.transform.rotation}, ${labelX}, ${labelY})` : void 0,
5118
+ children: node.label || node.id
5119
+ }
5120
+ )
5121
+ ]
5122
+ },
5123
+ node.id
5124
+ );
5125
+ })
5126
+ ] });
4653
5127
  }
4654
5128
  var NodeRenderer = React9.memo(NodeRendererImpl);
4655
5129
 
@@ -4870,7 +5344,9 @@ function DataOverlayImpl({
4870
5344
  scale = 1,
4871
5345
  statusColorMap
4872
5346
  }) {
4873
- const { value, display, history, scaling } = telemetry;
5347
+ const { value, display, history, scaling, forced } = telemetry;
5348
+ const isForced = forced?.active === true;
5349
+ const hasRealValue = isForced && forced?.realValue !== void 0;
4874
5350
  const showValue = display?.showValue ?? true;
4875
5351
  const showStatus = display?.showStatus ?? true;
4876
5352
  const showSparkline = display?.showSparkline ?? false;
@@ -4919,12 +5395,16 @@ function DataOverlayImpl({
4919
5395
  const finalTextColor = textColor || colors.text;
4920
5396
  const formattedValue = formatValue(value.value);
4921
5397
  const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
5398
+ const realDisplayText = hasRealValue ? unit ? `REAL ${formatValue(forced.realValue)} ${unit}` : `REAL ${formatValue(forced.realValue)}` : void 0;
5399
+ const borderColor = isForced ? FORCED_MARK_COLOR.border : colors.border;
5400
+ const borderWidth = (isForced ? 2 : 1) * scale;
4922
5401
  const hasSparkline = showSparkline && sparklinePath;
4923
- const overlayHeight = (label ? hasSparkline ? 55 : 30 : hasSparkline ? 47 : 22) * scale;
5402
+ const extraMarkRows = realDisplayText ? 1 : 0;
5403
+ const overlayHeight = (label ? hasSparkline ? 55 : 30 : hasSparkline ? 47 : 22) * scale + extraMarkRows * 9 * scale;
4924
5404
  const overlayWidth = 80 * scale;
4925
5405
  const displayX = x + offsetX;
4926
5406
  const displayY = y + offsetY;
4927
- return /* @__PURE__ */ jsxs14("g", { "data-overlay-node": nodeId, "data-status": value.status, children: [
5407
+ return /* @__PURE__ */ jsxs14("g", { "data-overlay-node": nodeId, "data-status": value.status, "data-forced": isForced, children: [
4928
5408
  /* @__PURE__ */ jsx18(
4929
5409
  "rect",
4930
5410
  {
@@ -4934,8 +5414,8 @@ function DataOverlayImpl({
4934
5414
  height: overlayHeight,
4935
5415
  rx: 4 * scale,
4936
5416
  fill: colors.bg,
4937
- stroke: colors.border,
4938
- strokeWidth: 1 * scale,
5417
+ stroke: borderColor,
5418
+ strokeWidth: borderWidth,
4939
5419
  opacity: "0.95",
4940
5420
  filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.2))"
4941
5421
  }
@@ -4984,6 +5464,48 @@ function DataOverlayImpl({
4984
5464
  ]
4985
5465
  }
4986
5466
  ),
5467
+ isForced && /* @__PURE__ */ jsxs14(
5468
+ "g",
5469
+ {
5470
+ transform: `translate(${displayX + overlayWidth / 2}, ${displayY - 25 * scale})`,
5471
+ "aria-label": "Forced",
5472
+ children: [
5473
+ /* @__PURE__ */ jsx18(
5474
+ "circle",
5475
+ {
5476
+ r: 7 * scale,
5477
+ fill: FORCED_MARK_COLOR.bg,
5478
+ stroke: FORCED_MARK_COLOR.border,
5479
+ strokeWidth: 1.5 * scale
5480
+ }
5481
+ ),
5482
+ /* @__PURE__ */ jsx18(
5483
+ "text",
5484
+ {
5485
+ textAnchor: "middle",
5486
+ dominantBaseline: "central",
5487
+ fontSize: 8 * scale,
5488
+ fontWeight: "700",
5489
+ fill: FORCED_MARK_COLOR.text,
5490
+ children: "F"
5491
+ }
5492
+ )
5493
+ ]
5494
+ }
5495
+ ),
5496
+ realDisplayText && /* @__PURE__ */ jsx18(
5497
+ "text",
5498
+ {
5499
+ x: displayX,
5500
+ y: displayY + (value.quality !== void 0 && value.quality < 100 ? 14 : 5) * scale,
5501
+ textAnchor: "middle",
5502
+ fontSize: 7 * scale,
5503
+ fontWeight: "500",
5504
+ fill: finalTextColor,
5505
+ opacity: "0.85",
5506
+ children: realDisplayText
5507
+ }
5508
+ ),
4987
5509
  hasSparkline && /* @__PURE__ */ jsx18("g", { transform: `translate(${displayX - 35 * scale}, ${displayY + (label ? 5 : 0) * scale})`, children: /* @__PURE__ */ jsx18(
4988
5510
  "path",
4989
5511
  {
@@ -5751,8 +6273,49 @@ function useTelemetryStatus(telemetry, diagram, setDiagram, options = {}) {
5751
6273
  }, [telemetry, diagram, setDiagram, enabled, mapStatus]);
5752
6274
  }
5753
6275
 
6276
+ // src/diagram/hooks/useBooleanIndicators.ts
6277
+ import { useEffect as useEffect10 } from "react";
6278
+ function useBooleanIndicators(telemetry, diagram, setDiagram, options = {}) {
6279
+ const { enabled = true, symbolId = "status-indicator" } = options;
6280
+ useEffect10(() => {
6281
+ if (!enabled) return;
6282
+ const tagIndex = /* @__PURE__ */ new Map();
6283
+ telemetry.forEach((binding) => {
6284
+ if (binding.tag) tagIndex.set(binding.tag, binding.value.value);
6285
+ });
6286
+ let needsUpdate = false;
6287
+ const updatedNodes = diagram.nodes.map((node) => {
6288
+ if (node.symbolId !== symbolId) return node;
6289
+ const meta = node.metadata ?? {};
6290
+ const colorSource = typeof meta.colorSource === "string" ? meta.colorSource : "";
6291
+ const visibleSource = typeof meta.visibleSource === "string" ? meta.visibleSource : "";
6292
+ let next = node;
6293
+ if (colorSource) {
6294
+ const isTrue = Boolean(tagIndex.get(colorSource));
6295
+ const fill = resolvePresetColor(
6296
+ String(isTrue ? meta.onColor ?? "green" : meta.offColor ?? "red")
6297
+ );
6298
+ if (node.customColors?.fill !== fill) {
6299
+ next = { ...next, customColors: { ...next.customColors, fill } };
6300
+ }
6301
+ }
6302
+ if (visibleSource) {
6303
+ const vis = Boolean(tagIndex.get(visibleSource));
6304
+ if ((next.visible ?? true) !== vis) {
6305
+ next = { ...next, visible: vis };
6306
+ }
6307
+ }
6308
+ if (next !== node) needsUpdate = true;
6309
+ return next;
6310
+ });
6311
+ if (needsUpdate) {
6312
+ setDiagram({ ...diagram, nodes: updatedNodes });
6313
+ }
6314
+ }, [telemetry, diagram, setDiagram, enabled, symbolId]);
6315
+ }
6316
+
5754
6317
  // src/diagram/hooks/useDrag.ts
5755
- import { useState as useState13, useCallback as useCallback7, useRef as useRef5, useEffect as useEffect10 } from "react";
6318
+ import { useState as useState13, useCallback as useCallback7, useRef as useRef5, useEffect as useEffect11 } from "react";
5756
6319
  function useDrag(options = {}) {
5757
6320
  const {
5758
6321
  onDragStart,
@@ -5937,7 +6500,7 @@ function useDrag(options = {}) {
5937
6500
  },
5938
6501
  [screenToWorld, applySnap, onDragEnd]
5939
6502
  );
5940
- useEffect10(() => {
6503
+ useEffect11(() => {
5941
6504
  if (!isListening) return;
5942
6505
  document.addEventListener("mousemove", handleMouseMove);
5943
6506
  document.addEventListener("mouseup", handleMouseUp);
@@ -6006,7 +6569,7 @@ function useDrag(options = {}) {
6006
6569
  }
6007
6570
 
6008
6571
  // src/diagram/hooks/useKeyboardShortcuts.ts
6009
- import { useEffect as useEffect11 } from "react";
6572
+ import { useEffect as useEffect12 } from "react";
6010
6573
  function useKeyboardShortcuts(options) {
6011
6574
  const {
6012
6575
  onDelete,
@@ -6017,7 +6580,7 @@ function useKeyboardShortcuts(options) {
6017
6580
  onSelectAll,
6018
6581
  enabled = true
6019
6582
  } = options;
6020
- useEffect11(() => {
6583
+ useEffect12(() => {
6021
6584
  if (!enabled) return;
6022
6585
  const handleKeyDown = (event) => {
6023
6586
  const target = event.target;
@@ -6169,7 +6732,7 @@ function PIDCanvasImpl({
6169
6732
  const isUndoRedoAction = useRef6(false);
6170
6733
  const isInternalChange = useRef6(false);
6171
6734
  const lastInternalDiagram = useRef6(null);
6172
- useEffect12(() => {
6735
+ useEffect13(() => {
6173
6736
  const isExternal = !isInternalChange.current && diagram !== lastInternalDiagram.current;
6174
6737
  if (isExternal) {
6175
6738
  setLocalDiagram(diagram);
@@ -6214,7 +6777,7 @@ function PIDCanvasImpl({
6214
6777
  });
6215
6778
  return { minX, minY, maxX, maxY };
6216
6779
  }, [localDiagram.nodes]);
6217
- useEffect12(() => {
6780
+ useEffect13(() => {
6218
6781
  if (onMounted) {
6219
6782
  const controls = {
6220
6783
  fitToContent: (padding = 50) => {
@@ -6293,7 +6856,7 @@ function PIDCanvasImpl({
6293
6856
  isUndoRedoAction.current = false;
6294
6857
  }, 0);
6295
6858
  }, [localDiagram, onDiagramChange]);
6296
- useEffect12(() => {
6859
+ useEffect13(() => {
6297
6860
  const handleKeyDown = (e) => {
6298
6861
  const isCtrlOrCmd = e.ctrlKey || e.metaKey;
6299
6862
  if (isCtrlOrCmd && e.key === "z" && !e.shiftKey) {
@@ -6310,7 +6873,7 @@ function PIDCanvasImpl({
6310
6873
  window.addEventListener("keydown", handleKeyDown);
6311
6874
  return () => window.removeEventListener("keydown", handleKeyDown);
6312
6875
  }, [undo, redo]);
6313
- useEffect12(() => {
6876
+ useEffect13(() => {
6314
6877
  if (!features.dragNodes) return;
6315
6878
  const handleKeyDown = (e) => {
6316
6879
  if (selection.selectedNodeIds.size === 0 && !selectedWaypoint) return;
@@ -6847,6 +7410,7 @@ function PIDCanvasImpl({
6847
7410
  }
6848
7411
  ),
6849
7412
  telemetryEnabled && telemetry && /* @__PURE__ */ jsx20("g", { id: "layer-telemetry", children: displayDiagram.nodes.map((node) => {
7413
+ if (node.symbolId === "status-indicator") return null;
6850
7414
  const binding = telemetry.get(node.id);
6851
7415
  if (!binding) return null;
6852
7416
  const symbol = getSymbolDefinition(node.symbolId);
@@ -7214,6 +7778,8 @@ function NodeConfigPanel({
7214
7778
  onUpdateNode,
7215
7779
  onUpdateTelemetry,
7216
7780
  onAddTelemetry,
7781
+ onRemoveTelemetry,
7782
+ availableTags = [],
7217
7783
  onRemove,
7218
7784
  className = "",
7219
7785
  style = {}
@@ -7226,6 +7792,8 @@ function NodeConfigPanel({
7226
7792
  padding: "20px",
7227
7793
  ...style
7228
7794
  };
7795
+ const appearanceFields = getAppearanceSchema(node.symbolId);
7796
+ const showGenericTelemetry = appearanceFields.length === 0;
7229
7797
  return /* @__PURE__ */ jsxs18("div", { className, style: defaultStyle, children: [
7230
7798
  /* @__PURE__ */ jsxs18("h2", { style: { margin: "0 0 16px 0", fontSize: "1rem", fontWeight: 600 }, children: [
7231
7799
  "Configure: ",
@@ -7347,7 +7915,136 @@ function NodeConfigPanel({
7347
7915
  )
7348
7916
  ] })
7349
7917
  ] }),
7350
- /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7918
+ appearanceFields.length > 0 && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7919
+ /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Appearance" }),
7920
+ appearanceFields.map((field) => {
7921
+ const value = readAppearanceValue(node, field);
7922
+ const fieldLabel = field.label ?? field.key;
7923
+ const inputStyle = {
7924
+ width: "100%",
7925
+ padding: "6px 8px",
7926
+ borderRadius: "4px",
7927
+ border: "1px solid #cbd5e1",
7928
+ fontSize: "0.875rem"
7929
+ };
7930
+ let control;
7931
+ if (field.kind === "boolean") {
7932
+ control = /* @__PURE__ */ jsxs18("label", { style: { fontSize: "0.75rem", display: "block" }, children: [
7933
+ /* @__PURE__ */ jsx22(
7934
+ "input",
7935
+ {
7936
+ type: "checkbox",
7937
+ checked: Boolean(value),
7938
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.checked)),
7939
+ style: { marginRight: "6px" }
7940
+ }
7941
+ ),
7942
+ fieldLabel
7943
+ ] });
7944
+ } else if (field.kind === "select") {
7945
+ control = /* @__PURE__ */ jsxs18(Fragment8, { children: [
7946
+ /* @__PURE__ */ jsxs18("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
7947
+ fieldLabel,
7948
+ ":"
7949
+ ] }),
7950
+ /* @__PURE__ */ jsx22(
7951
+ "select",
7952
+ {
7953
+ value: String(value),
7954
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
7955
+ style: inputStyle,
7956
+ children: field.options?.map((opt) => /* @__PURE__ */ jsx22("option", { value: opt, children: opt }, opt))
7957
+ }
7958
+ )
7959
+ ] });
7960
+ } else if (field.kind === "number") {
7961
+ control = /* @__PURE__ */ jsxs18(Fragment8, { children: [
7962
+ /* @__PURE__ */ jsxs18("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
7963
+ fieldLabel,
7964
+ ":"
7965
+ ] }),
7966
+ /* @__PURE__ */ jsx22(
7967
+ "input",
7968
+ {
7969
+ type: "number",
7970
+ value: Number(value),
7971
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, parseFloat(e.target.value) || 0)),
7972
+ style: inputStyle
7973
+ }
7974
+ )
7975
+ ] });
7976
+ } else if (field.kind === "tag") {
7977
+ const tagValue = String(value);
7978
+ const listId = `tags-${node.id}-${field.key}`;
7979
+ const known = tagValue.length > 0 && availableTags.includes(tagValue);
7980
+ const unknown = tagValue.length > 0 && !known;
7981
+ control = /* @__PURE__ */ jsxs18(Fragment8, { children: [
7982
+ /* @__PURE__ */ jsxs18("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
7983
+ fieldLabel,
7984
+ ":"
7985
+ ] }),
7986
+ /* @__PURE__ */ jsx22(
7987
+ "input",
7988
+ {
7989
+ type: "text",
7990
+ value: tagValue,
7991
+ list: listId,
7992
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
7993
+ placeholder: "type or pick a signal\u2026",
7994
+ style: {
7995
+ ...inputStyle,
7996
+ border: `1px solid ${unknown ? "#f59e0b" : known ? "#22c55e" : "#cbd5e1"}`
7997
+ }
7998
+ }
7999
+ ),
8000
+ /* @__PURE__ */ jsx22("datalist", { id: listId, children: availableTags.map((t) => /* @__PURE__ */ jsx22("option", { value: t }, t)) }),
8001
+ tagValue.length > 0 && /* @__PURE__ */ jsx22(
8002
+ "div",
8003
+ {
8004
+ style: {
8005
+ fontSize: "0.7rem",
8006
+ marginTop: "3px",
8007
+ color: known ? "#16a34a" : "#b45309"
8008
+ },
8009
+ children: known ? "\u2713 matched signal" : "\u26A0 no matching signal (reads as false until published)"
8010
+ }
8011
+ )
8012
+ ] });
8013
+ } else {
8014
+ control = /* @__PURE__ */ jsxs18(Fragment8, { children: [
8015
+ /* @__PURE__ */ jsxs18("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: [
8016
+ fieldLabel,
8017
+ ":"
8018
+ ] }),
8019
+ /* @__PURE__ */ jsx22(
8020
+ "input",
8021
+ {
8022
+ type: "text",
8023
+ value: String(value),
8024
+ onChange: (e) => onUpdateNode(writeAppearanceValue(node, field, e.target.value)),
8025
+ style: inputStyle
8026
+ }
8027
+ )
8028
+ ] });
8029
+ }
8030
+ return /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "10px" }, children: [
8031
+ control,
8032
+ field.help && /* @__PURE__ */ jsx22(
8033
+ "div",
8034
+ {
8035
+ style: {
8036
+ fontSize: "0.7rem",
8037
+ color: "#64748b",
8038
+ marginTop: "3px",
8039
+ lineHeight: 1.35
8040
+ },
8041
+ children: field.help
8042
+ }
8043
+ )
8044
+ ] }, field.key);
8045
+ })
8046
+ ] }),
8047
+ showGenericTelemetry && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7351
8048
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Telemetry Data" }),
7352
8049
  !binding ? /* @__PURE__ */ jsx22(
7353
8050
  "button",
@@ -7393,6 +8090,27 @@ function NodeConfigPanel({
7393
8090
  children: "+ Add Telemetry"
7394
8091
  }
7395
8092
  ) : /* @__PURE__ */ jsxs18(Fragment8, { children: [
8093
+ /* @__PURE__ */ jsx22("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Tag (attribute):" }),
8094
+ /* @__PURE__ */ jsx22(
8095
+ "input",
8096
+ {
8097
+ type: "text",
8098
+ value: binding.tag || "",
8099
+ onChange: (e) => onUpdateTelemetry({
8100
+ nodeId: node.id,
8101
+ tag: e.target.value
8102
+ }),
8103
+ placeholder: "e.g. pump.running",
8104
+ style: {
8105
+ width: "100%",
8106
+ padding: "6px 8px",
8107
+ borderRadius: "4px",
8108
+ border: "1px solid #cbd5e1",
8109
+ fontSize: "0.875rem",
8110
+ marginBottom: "8px"
8111
+ }
8112
+ }
8113
+ ),
7396
8114
  /* @__PURE__ */ jsx22("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Initial Value:" }),
7397
8115
  /* @__PURE__ */ jsx22(
7398
8116
  "input",
@@ -7700,10 +8418,28 @@ function NodeConfigPanel({
7700
8418
  ] })
7701
8419
  ] }),
7702
8420
  " "
7703
- ] })
8421
+ ] }),
8422
+ onRemoveTelemetry && /* @__PURE__ */ jsx22(
8423
+ "button",
8424
+ {
8425
+ onClick: onRemoveTelemetry,
8426
+ style: {
8427
+ width: "100%",
8428
+ padding: "6px 8px",
8429
+ marginTop: "8px",
8430
+ borderRadius: "4px",
8431
+ border: "1px solid #ef4444",
8432
+ background: "transparent",
8433
+ color: "#ef4444",
8434
+ fontSize: "0.8125rem",
8435
+ cursor: "pointer"
8436
+ },
8437
+ children: "Remove Telemetry"
8438
+ }
8439
+ )
7704
8440
  ] })
7705
8441
  ] }),
7706
- binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
8442
+ showGenericTelemetry && binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7707
8443
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Thresholds" }),
7708
8444
  ["highHigh", "high", "low", "lowLow"].map((key) => {
7709
8445
  const label = key === "highHigh" ? "High-High (\u2265)" : key === "high" ? "High (\u2265)" : key === "low" ? "Low (\u2264)" : "Low-Low (\u2264)";
@@ -7733,7 +8469,7 @@ function NodeConfigPanel({
7733
8469
  ] }, key);
7734
8470
  })
7735
8471
  ] }),
7736
- binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
8472
+ showGenericTelemetry && binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7737
8473
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Status Colors" }),
7738
8474
  ["normal", "warning", "alarm", "fault", "off"].map((status) => {
7739
8475
  const defaultColors = {
@@ -8592,7 +9328,7 @@ function useDeviceConfigs(initialConfigs) {
8592
9328
  }
8593
9329
 
8594
9330
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8595
- import React15, { useState as useState18, useEffect as useEffect13, useMemo, useRef as useRef8 } from "react";
9331
+ import React15, { useState as useState18, useEffect as useEffect14, useMemo, useRef as useRef8 } from "react";
8596
9332
  import { Fragment as Fragment9, jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
8597
9333
  function DeviceConfigPanelImpl({
8598
9334
  binding,
@@ -8651,7 +9387,7 @@ function DeviceConfigPanelImpl({
8651
9387
  setCollapsedSections(collapsed);
8652
9388
  setBindingId(binding.nodeId);
8653
9389
  }
8654
- useEffect13(() => {
9390
+ useEffect14(() => {
8655
9391
  const currentTimestamp = binding?.state.lastSaved || 0;
8656
9392
  if (binding?.state.lastSaveResult && currentTimestamp !== lastResultTimestampRef.current) {
8657
9393
  lastResultTimestampRef.current = currentTimestamp;
@@ -8786,7 +9522,49 @@ function DeviceConfigPanelImpl({
8786
9522
  " (",
8787
9523
  param.unit,
8788
9524
  ")"
8789
- ] })
9525
+ ] }),
9526
+ param.forced?.active && /* @__PURE__ */ jsx23(
9527
+ "span",
9528
+ {
9529
+ "aria-label": "Forced",
9530
+ style: {
9531
+ display: "inline-flex",
9532
+ alignItems: "center",
9533
+ justifyContent: "center",
9534
+ width: "16px",
9535
+ height: "16px",
9536
+ marginLeft: "6px",
9537
+ fontSize: "10px",
9538
+ fontWeight: 700,
9539
+ textTransform: "none",
9540
+ letterSpacing: "normal",
9541
+ color: FORCED_MARK_COLOR.text,
9542
+ backgroundColor: FORCED_MARK_COLOR.bg,
9543
+ border: `1px solid ${FORCED_MARK_COLOR.border}`,
9544
+ borderRadius: "50%",
9545
+ verticalAlign: "middle"
9546
+ },
9547
+ children: "F"
9548
+ }
9549
+ )
9550
+ ]
9551
+ }
9552
+ ),
9553
+ param.forced?.active && param.forced.realValue !== void 0 && /* @__PURE__ */ jsxs19(
9554
+ "div",
9555
+ {
9556
+ style: {
9557
+ fontSize: `${fontSize.description}px`,
9558
+ color: mutedTextColor,
9559
+ marginTop: "-2px",
9560
+ marginBottom: "6px",
9561
+ fontFamily: "Arial, sans-serif"
9562
+ },
9563
+ children: [
9564
+ "REAL",
9565
+ " ",
9566
+ typeof param.forced.realValue === "boolean" ? param.forced.realValue ? "ON" : "OFF" : String(param.forced.realValue),
9567
+ param.unit ? ` ${param.unit}` : ""
8790
9568
  ]
8791
9569
  }
8792
9570
  ),
@@ -9300,7 +10078,7 @@ function DeviceConfigPanelImpl({
9300
10078
  var DeviceConfigPanel = React15.memo(DeviceConfigPanelImpl);
9301
10079
 
9302
10080
  // src/diagram/hooks/useSimulation.ts
9303
- import { useEffect as useEffect14 } from "react";
10081
+ import { useEffect as useEffect15 } from "react";
9304
10082
  function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9305
10083
  const {
9306
10084
  interval = 2e3,
@@ -9311,7 +10089,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9311
10089
  historyLength = 10,
9312
10090
  generateValue
9313
10091
  } = options;
9314
- useEffect14(() => {
10092
+ useEffect15(() => {
9315
10093
  if (!enabled) return;
9316
10094
  const intervalId = setInterval(() => {
9317
10095
  const updates = [];
@@ -9361,6 +10139,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9361
10139
  }
9362
10140
  export {
9363
10141
  Button,
10142
+ COLOR_PRESETS,
9364
10143
  CardHeader,
9365
10144
  CardUnit,
9366
10145
  CardValue,
@@ -9374,6 +10153,7 @@ export {
9374
10153
  DisplayModeToggle,
9375
10154
  EquipmentIndicator,
9376
10155
  EquipmentIndicatorWithSparkline,
10156
+ FORCED_MARK_COLOR,
9377
10157
  FixedSVGContainer,
9378
10158
  FooterPanel,
9379
10159
  FullscreenContainer,
@@ -9389,6 +10169,7 @@ export {
9389
10169
  PanelTabs,
9390
10170
  RightPanel,
9391
10171
  RightToggleButton,
10172
+ STATUS_LED_APPEARANCE,
9392
10173
  SVGArea,
9393
10174
  SVGLockedOverlay,
9394
10175
  ScrollableSVGWrapper,
@@ -9399,6 +10180,8 @@ export {
9399
10180
  ThemeProvider,
9400
10181
  ThemeToggle,
9401
10182
  TrendLine,
10183
+ UNKNOWN_MARK_COLOR,
10184
+ UNKNOWN_MARK_COLOR_DARK,
9402
10185
  ValueEntry,
9403
10186
  ZoomButton,
9404
10187
  ZoomControls,
@@ -9408,6 +10191,8 @@ export {
9408
10191
  exampleDiagram,
9409
10192
  exportDiagram,
9410
10193
  generateRoutePoints,
10194
+ getAppearanceDefaults,
10195
+ getAppearanceSchema,
9411
10196
  getAvailableSymbols,
9412
10197
  getPortWorldPosition,
9413
10198
  getSymbolDefinition,
@@ -9415,9 +10200,12 @@ export {
9415
10200
  mockSymbolLibrary,
9416
10201
  nodeHasPort,
9417
10202
  overlayStyles,
10203
+ readAppearanceValue,
9418
10204
  registerSymbol,
9419
10205
  registerSymbols,
10206
+ resolvePresetColor,
9420
10207
  resolveStatusColor,
10208
+ useBooleanIndicators,
9421
10209
  useDeviceConfigs,
9422
10210
  useDeviceControls,
9423
10211
  useDrag,
@@ -9428,6 +10216,7 @@ export {
9428
10216
  useTelemetryStatus,
9429
10217
  useTheme,
9430
10218
  useViewBox,
9431
- validateDiagram
10219
+ validateDiagram,
10220
+ writeAppearanceValue
9432
10221
  };
9433
10222
  //# sourceMappingURL=index.js.map