@procaaso/alphinity-ui-components 1.10.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
@@ -283,6 +283,20 @@ var FORCED_MARK_COLOR = {
283
283
  text: "#5b21b6",
284
284
  border: "#7c3aed"
285
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
+ };
286
300
  var STATUS_ALIASES = {
287
301
  running: "normal",
288
302
  stopped: "off"
@@ -756,11 +770,15 @@ var StatusIndicator = ({
756
770
  labelPosition = "right",
757
771
  shape = "circle",
758
772
  led = true,
773
+ visible = true,
759
774
  forced = false,
760
775
  realValue,
761
776
  className = "",
762
777
  ...props
763
778
  }) => {
779
+ if (!visible) {
780
+ return null;
781
+ }
764
782
  const baseClass = "ui-status-indicator";
765
783
  const statusClass = status ? `ui-status-indicator--${status}` : "";
766
784
  const sizeClass = size ? `ui-status-indicator--${size}` : "";
@@ -3515,7 +3533,7 @@ var ThemeToggle = ({
3515
3533
  };
3516
3534
 
3517
3535
  // src/components/PIDCanvas/PIDCanvas.tsx
3518
- 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";
3519
3537
 
3520
3538
  // src/components/PIDCanvas/NodeRenderer.tsx
3521
3539
  import React9 from "react";
@@ -3546,6 +3564,25 @@ var mockSymbolLibrary = {
3546
3564
  tags: ["valve", "gate", "powered", "actuator"]
3547
3565
  }
3548
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
+ },
3549
3586
  "pump-positive-displacement": {
3550
3587
  id: "pump-positive-displacement",
3551
3588
  name: "Positive Displacement Pump",
@@ -4738,8 +4775,144 @@ function getAvailableSymbols() {
4738
4775
  return [...Object.keys(customSymbols), ...Object.keys(mockSymbolLibrary)];
4739
4776
  }
4740
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
+
4741
4913
  // src/components/PIDCanvas/NodeRenderer.tsx
4742
4914
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4915
+ var HATCH_SIZE = 6;
4743
4916
  function NodeRendererImpl({
4744
4917
  nodes: nodes2,
4745
4918
  selectedNodeIds,
@@ -4751,140 +4924,206 @@ function NodeRendererImpl({
4751
4924
  enableDrag = false,
4752
4925
  statusColorMap
4753
4926
  }) {
4754
- return /* @__PURE__ */ jsx16("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
4755
- const symbol = getSymbolDefinition(node.symbolId);
4756
- const isSelected = selectedNodeIds?.has(node.id) ?? false;
4757
- const isDragging = draggingNodeId === node.id;
4758
- if (!symbol) {
4759
- return /* @__PURE__ */ jsxs12("g", { children: [
4760
- /* @__PURE__ */ jsx16(
4761
- "circle",
4762
- {
4763
- cx: node.transform.x,
4764
- cy: node.transform.y,
4765
- r: "15",
4766
- fill: "#ff0000",
4767
- opacity: "0.3",
4768
- stroke: "#ff0000",
4769
- strokeWidth: "2"
4770
- }
4771
- ),
4772
- /* @__PURE__ */ jsx16(
4773
- "text",
4774
- {
4775
- x: node.transform.x,
4776
- y: node.transform.y,
4777
- textAnchor: "middle",
4778
- dominantBaseline: "middle",
4779
- fontSize: "10",
4780
- fill: "#ff0000",
4781
- children: "?"
4782
- }
4783
- )
4784
- ] }, node.id);
4785
- }
4786
- const transforms = [];
4787
- const effectiveX = isDragging ? node.transform.x + dragOffset.x : node.transform.x;
4788
- const effectiveY = isDragging ? node.transform.y + dragOffset.y : node.transform.y;
4789
- transforms.push(`translate(${effectiveX}, ${effectiveY})`);
4790
- if (node.transform.rotation) {
4791
- const centerX = symbol.viewBox.width / 2;
4792
- const centerY = symbol.viewBox.height / 2;
4793
- transforms.push(`rotate(${node.transform.rotation}, ${centerX}, ${centerY})`);
4794
- }
4795
- if (node.transform.scale && node.transform.scale !== 1) {
4796
- const centerX = symbol.viewBox.width / 2;
4797
- const centerY = symbol.viewBox.height / 2;
4798
- transforms.push(
4799
- `translate(${centerX}, ${centerY}) scale(${node.transform.scale}) translate(${-centerX}, ${-centerY})`
4800
- );
4801
- }
4802
- if (node.transform.flipX || node.transform.flipY) {
4803
- const scaleX = node.transform.flipX ? -1 : 1;
4804
- const scaleY = node.transform.flipY ? -1 : 1;
4805
- const centerX = symbol.viewBox.width / 2;
4806
- const centerY = symbol.viewBox.height / 2;
4807
- transforms.push(
4808
- `translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
4809
- );
4810
- }
4811
- const statusEntry = resolveStatusColor(node.status, statusColorMap);
4812
- const fillColor = node.customColors?.fill ?? statusEntry.fill;
4813
- const strokeColor = node.customColors?.stroke ?? statusEntry.stroke;
4814
- return /* @__PURE__ */ jsxs12(
4815
- "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",
4816
4932
  {
4817
- "data-node-id": node.id,
4818
- "data-symbol-id": node.symbolId,
4819
- "data-selected": isSelected,
4820
- "data-dragging": isDragging,
4821
- transform: transforms.join(" "),
4822
- style: {
4823
- "--symbol-fill": fillColor,
4824
- "--symbol-stroke": strokeColor,
4825
- cursor: enableDrag ? isDragging ? "grabbing" : "grab" : onNodeClick ? "pointer" : "default"
4826
- },
4827
- onClick: (e) => {
4828
- e.stopPropagation();
4829
- onNodeClick?.(node.id, e);
4830
- },
4831
- onDoubleClick: (e) => {
4832
- e.stopPropagation();
4833
- onNodeDoubleClick?.(node.id, e);
4834
- },
4835
- onMouseDown: (e) => {
4836
- if (enableDrag) {
4837
- onNodeDragStart?.(node.id, e);
4838
- }
4839
- },
4840
- onTouchStart: (e) => {
4841
- if (enableDrag) {
4842
- e.stopPropagation();
4843
- onNodeDragStart?.(node.id, e);
4844
- }
4845
- },
4933
+ id: hatchId,
4934
+ "data-testid": "position-unknown-pattern",
4935
+ patternUnits: "userSpaceOnUse",
4936
+ width: HATCH_SIZE,
4937
+ height: HATCH_SIZE,
4938
+ patternTransform: "rotate(45)",
4846
4939
  children: [
4847
- isSelected && /* @__PURE__ */ jsx16(
4848
- "rect",
4940
+ /* @__PURE__ */ jsx16("rect", { width: HATCH_SIZE, height: HATCH_SIZE, fill: UNKNOWN_MARK_COLOR.fill }),
4941
+ /* @__PURE__ */ jsx16(
4942
+ "line",
4849
4943
  {
4850
- x: "-5",
4851
- y: "-5",
4852
- width: symbol.viewBox.width + 10,
4853
- height: symbol.viewBox.height + 10,
4854
- fill: "none",
4855
- stroke: isDragging ? "#10b981" : "#3b82f6",
4856
- strokeWidth: isDragging ? "3" : "2",
4857
- strokeDasharray: "4 2",
4858
- rx: "2",
4859
- pointerEvents: "none"
4944
+ x1: "0",
4945
+ y1: "0",
4946
+ x2: "0",
4947
+ y2: HATCH_SIZE,
4948
+ stroke: UNKNOWN_MARK_COLOR.text,
4949
+ strokeWidth: "2"
4860
4950
  }
4861
- ),
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: [
4862
4961
  /* @__PURE__ */ jsx16(
4863
- "g",
4962
+ "circle",
4864
4963
  {
4865
- dangerouslySetInnerHTML: { __html: symbol.svgContent },
4866
- "data-symbol-content": symbol.id,
4867
- 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"
4868
4971
  }
4869
4972
  ),
4870
4973
  /* @__PURE__ */ jsx16(
4871
4974
  "text",
4872
4975
  {
4873
- x: symbol.viewBox.width / 2 + (node.labelOffset?.x ?? 0),
4874
- y: symbol.viewBox.height + 12 + (node.labelOffset?.y ?? 0),
4976
+ x: node.transform.x,
4977
+ y: node.transform.y,
4875
4978
  textAnchor: "middle",
4876
- fontSize: node.labelFontSize ?? 10,
4877
- fontWeight: "600",
4878
- fill: isSelected ? "#3b82f6" : "#1f2937",
4879
- 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,
4880
- children: node.label || node.id
4979
+ dominantBaseline: "middle",
4980
+ fontSize: "10",
4981
+ fill: "#ff0000",
4982
+ children: "?"
4881
4983
  }
4882
4984
  )
4883
- ]
4884
- },
4885
- node.id
4886
- );
4887
- }) });
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
+ ] });
4888
5127
  }
4889
5128
  var NodeRenderer = React9.memo(NodeRendererImpl);
4890
5129
 
@@ -6034,8 +6273,49 @@ function useTelemetryStatus(telemetry, diagram, setDiagram, options = {}) {
6034
6273
  }, [telemetry, diagram, setDiagram, enabled, mapStatus]);
6035
6274
  }
6036
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
+
6037
6317
  // src/diagram/hooks/useDrag.ts
6038
- 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";
6039
6319
  function useDrag(options = {}) {
6040
6320
  const {
6041
6321
  onDragStart,
@@ -6220,7 +6500,7 @@ function useDrag(options = {}) {
6220
6500
  },
6221
6501
  [screenToWorld, applySnap, onDragEnd]
6222
6502
  );
6223
- useEffect10(() => {
6503
+ useEffect11(() => {
6224
6504
  if (!isListening) return;
6225
6505
  document.addEventListener("mousemove", handleMouseMove);
6226
6506
  document.addEventListener("mouseup", handleMouseUp);
@@ -6289,7 +6569,7 @@ function useDrag(options = {}) {
6289
6569
  }
6290
6570
 
6291
6571
  // src/diagram/hooks/useKeyboardShortcuts.ts
6292
- import { useEffect as useEffect11 } from "react";
6572
+ import { useEffect as useEffect12 } from "react";
6293
6573
  function useKeyboardShortcuts(options) {
6294
6574
  const {
6295
6575
  onDelete,
@@ -6300,7 +6580,7 @@ function useKeyboardShortcuts(options) {
6300
6580
  onSelectAll,
6301
6581
  enabled = true
6302
6582
  } = options;
6303
- useEffect11(() => {
6583
+ useEffect12(() => {
6304
6584
  if (!enabled) return;
6305
6585
  const handleKeyDown = (event) => {
6306
6586
  const target = event.target;
@@ -6452,7 +6732,7 @@ function PIDCanvasImpl({
6452
6732
  const isUndoRedoAction = useRef6(false);
6453
6733
  const isInternalChange = useRef6(false);
6454
6734
  const lastInternalDiagram = useRef6(null);
6455
- useEffect12(() => {
6735
+ useEffect13(() => {
6456
6736
  const isExternal = !isInternalChange.current && diagram !== lastInternalDiagram.current;
6457
6737
  if (isExternal) {
6458
6738
  setLocalDiagram(diagram);
@@ -6497,7 +6777,7 @@ function PIDCanvasImpl({
6497
6777
  });
6498
6778
  return { minX, minY, maxX, maxY };
6499
6779
  }, [localDiagram.nodes]);
6500
- useEffect12(() => {
6780
+ useEffect13(() => {
6501
6781
  if (onMounted) {
6502
6782
  const controls = {
6503
6783
  fitToContent: (padding = 50) => {
@@ -6576,7 +6856,7 @@ function PIDCanvasImpl({
6576
6856
  isUndoRedoAction.current = false;
6577
6857
  }, 0);
6578
6858
  }, [localDiagram, onDiagramChange]);
6579
- useEffect12(() => {
6859
+ useEffect13(() => {
6580
6860
  const handleKeyDown = (e) => {
6581
6861
  const isCtrlOrCmd = e.ctrlKey || e.metaKey;
6582
6862
  if (isCtrlOrCmd && e.key === "z" && !e.shiftKey) {
@@ -6593,7 +6873,7 @@ function PIDCanvasImpl({
6593
6873
  window.addEventListener("keydown", handleKeyDown);
6594
6874
  return () => window.removeEventListener("keydown", handleKeyDown);
6595
6875
  }, [undo, redo]);
6596
- useEffect12(() => {
6876
+ useEffect13(() => {
6597
6877
  if (!features.dragNodes) return;
6598
6878
  const handleKeyDown = (e) => {
6599
6879
  if (selection.selectedNodeIds.size === 0 && !selectedWaypoint) return;
@@ -7130,6 +7410,7 @@ function PIDCanvasImpl({
7130
7410
  }
7131
7411
  ),
7132
7412
  telemetryEnabled && telemetry && /* @__PURE__ */ jsx20("g", { id: "layer-telemetry", children: displayDiagram.nodes.map((node) => {
7413
+ if (node.symbolId === "status-indicator") return null;
7133
7414
  const binding = telemetry.get(node.id);
7134
7415
  if (!binding) return null;
7135
7416
  const symbol = getSymbolDefinition(node.symbolId);
@@ -7497,6 +7778,8 @@ function NodeConfigPanel({
7497
7778
  onUpdateNode,
7498
7779
  onUpdateTelemetry,
7499
7780
  onAddTelemetry,
7781
+ onRemoveTelemetry,
7782
+ availableTags = [],
7500
7783
  onRemove,
7501
7784
  className = "",
7502
7785
  style = {}
@@ -7509,6 +7792,8 @@ function NodeConfigPanel({
7509
7792
  padding: "20px",
7510
7793
  ...style
7511
7794
  };
7795
+ const appearanceFields = getAppearanceSchema(node.symbolId);
7796
+ const showGenericTelemetry = appearanceFields.length === 0;
7512
7797
  return /* @__PURE__ */ jsxs18("div", { className, style: defaultStyle, children: [
7513
7798
  /* @__PURE__ */ jsxs18("h2", { style: { margin: "0 0 16px 0", fontSize: "1rem", fontWeight: 600 }, children: [
7514
7799
  "Configure: ",
@@ -7630,7 +7915,136 @@ function NodeConfigPanel({
7630
7915
  )
7631
7916
  ] })
7632
7917
  ] }),
7633
- /* @__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: [
7634
8048
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Telemetry Data" }),
7635
8049
  !binding ? /* @__PURE__ */ jsx22(
7636
8050
  "button",
@@ -7676,6 +8090,27 @@ function NodeConfigPanel({
7676
8090
  children: "+ Add Telemetry"
7677
8091
  }
7678
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
+ ),
7679
8114
  /* @__PURE__ */ jsx22("label", { style: { fontSize: "0.75rem", display: "block", marginBottom: "4px" }, children: "Initial Value:" }),
7680
8115
  /* @__PURE__ */ jsx22(
7681
8116
  "input",
@@ -7983,10 +8418,28 @@ function NodeConfigPanel({
7983
8418
  ] })
7984
8419
  ] }),
7985
8420
  " "
7986
- ] })
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
+ )
7987
8440
  ] })
7988
8441
  ] }),
7989
- binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
8442
+ showGenericTelemetry && binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
7990
8443
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Thresholds" }),
7991
8444
  ["highHigh", "high", "low", "lowLow"].map((key) => {
7992
8445
  const label = key === "highHigh" ? "High-High (\u2265)" : key === "high" ? "High (\u2265)" : key === "low" ? "Low (\u2264)" : "Low-Low (\u2264)";
@@ -8016,7 +8469,7 @@ function NodeConfigPanel({
8016
8469
  ] }, key);
8017
8470
  })
8018
8471
  ] }),
8019
- binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
8472
+ showGenericTelemetry && binding && /* @__PURE__ */ jsxs18("div", { style: { marginBottom: "20px" }, children: [
8020
8473
  /* @__PURE__ */ jsx22("h3", { style: { fontSize: "0.875rem", fontWeight: 600, marginBottom: "8px" }, children: "Status Colors" }),
8021
8474
  ["normal", "warning", "alarm", "fault", "off"].map((status) => {
8022
8475
  const defaultColors = {
@@ -8875,7 +9328,7 @@ function useDeviceConfigs(initialConfigs) {
8875
9328
  }
8876
9329
 
8877
9330
  // src/components/DeviceConfigPanel/DeviceConfigPanel.tsx
8878
- 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";
8879
9332
  import { Fragment as Fragment9, jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
8880
9333
  function DeviceConfigPanelImpl({
8881
9334
  binding,
@@ -8934,7 +9387,7 @@ function DeviceConfigPanelImpl({
8934
9387
  setCollapsedSections(collapsed);
8935
9388
  setBindingId(binding.nodeId);
8936
9389
  }
8937
- useEffect13(() => {
9390
+ useEffect14(() => {
8938
9391
  const currentTimestamp = binding?.state.lastSaved || 0;
8939
9392
  if (binding?.state.lastSaveResult && currentTimestamp !== lastResultTimestampRef.current) {
8940
9393
  lastResultTimestampRef.current = currentTimestamp;
@@ -9625,7 +10078,7 @@ function DeviceConfigPanelImpl({
9625
10078
  var DeviceConfigPanel = React15.memo(DeviceConfigPanelImpl);
9626
10079
 
9627
10080
  // src/diagram/hooks/useSimulation.ts
9628
- import { useEffect as useEffect14 } from "react";
10081
+ import { useEffect as useEffect15 } from "react";
9629
10082
  function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9630
10083
  const {
9631
10084
  interval = 2e3,
@@ -9636,7 +10089,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9636
10089
  historyLength = 10,
9637
10090
  generateValue
9638
10091
  } = options;
9639
- useEffect14(() => {
10092
+ useEffect15(() => {
9640
10093
  if (!enabled) return;
9641
10094
  const intervalId = setInterval(() => {
9642
10095
  const updates = [];
@@ -9686,6 +10139,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
9686
10139
  }
9687
10140
  export {
9688
10141
  Button,
10142
+ COLOR_PRESETS,
9689
10143
  CardHeader,
9690
10144
  CardUnit,
9691
10145
  CardValue,
@@ -9715,6 +10169,7 @@ export {
9715
10169
  PanelTabs,
9716
10170
  RightPanel,
9717
10171
  RightToggleButton,
10172
+ STATUS_LED_APPEARANCE,
9718
10173
  SVGArea,
9719
10174
  SVGLockedOverlay,
9720
10175
  ScrollableSVGWrapper,
@@ -9725,6 +10180,8 @@ export {
9725
10180
  ThemeProvider,
9726
10181
  ThemeToggle,
9727
10182
  TrendLine,
10183
+ UNKNOWN_MARK_COLOR,
10184
+ UNKNOWN_MARK_COLOR_DARK,
9728
10185
  ValueEntry,
9729
10186
  ZoomButton,
9730
10187
  ZoomControls,
@@ -9734,6 +10191,8 @@ export {
9734
10191
  exampleDiagram,
9735
10192
  exportDiagram,
9736
10193
  generateRoutePoints,
10194
+ getAppearanceDefaults,
10195
+ getAppearanceSchema,
9737
10196
  getAvailableSymbols,
9738
10197
  getPortWorldPosition,
9739
10198
  getSymbolDefinition,
@@ -9741,9 +10200,12 @@ export {
9741
10200
  mockSymbolLibrary,
9742
10201
  nodeHasPort,
9743
10202
  overlayStyles,
10203
+ readAppearanceValue,
9744
10204
  registerSymbol,
9745
10205
  registerSymbols,
10206
+ resolvePresetColor,
9746
10207
  resolveStatusColor,
10208
+ useBooleanIndicators,
9747
10209
  useDeviceConfigs,
9748
10210
  useDeviceControls,
9749
10211
  useDrag,
@@ -9754,6 +10216,7 @@ export {
9754
10216
  useTelemetryStatus,
9755
10217
  useTheme,
9756
10218
  useViewBox,
9757
- validateDiagram
10219
+ validateDiagram,
10220
+ writeAppearanceValue
9758
10221
  };
9759
10222
  //# sourceMappingURL=index.js.map