@cyvest/cyvest-vis 4.0.0 → 4.1.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.
@@ -1,169 +1,150 @@
1
1
  /**
2
2
  * Custom node component for the Observables Graph.
3
- * Renders nodes with different shapes based on observable type.
3
+ * Professional design with circular nodes and SVG icons.
4
4
  */
5
5
 
6
- import React, { memo } from "react";
6
+ import React, { memo, useMemo } from "react";
7
7
  import { Handle, Position, type NodeProps } from "@xyflow/react";
8
- import type { ObservableNodeData, ObservableShape } from "../types";
8
+ import type { ObservableNodeData } from "../types";
9
9
  import { getLevelColor, getLevelBackgroundColor } from "../utils/observables";
10
+ import { getObservableIcon, CrosshairIcon } from "./Icons";
10
11
 
11
12
  /**
12
- * Node size constants - keep small for nice force layout
13
+ * Node size constants
13
14
  */
14
- const NODE_SIZE = 28;
15
- const ROOT_NODE_SIZE = 36;
15
+ const NODE_SIZE = 40;
16
+ const ROOT_NODE_WIDTH = 56;
17
+ const ROOT_NODE_HEIGHT = 40;
18
+ const ICON_SIZE = 18;
19
+ const ROOT_ICON_SIZE = 20;
16
20
 
17
21
  /**
18
- * Observable node component with centered connection points.
22
+ * CSS styles for the node
19
23
  */
20
- function ObservableNodeComponent({
21
- data,
22
- selected,
23
- }: NodeProps) {
24
+ const nodeStyles = {
25
+ container: {
26
+ display: "flex",
27
+ flexDirection: "column" as const,
28
+ alignItems: "center",
29
+ cursor: "grab",
30
+ transition: "transform 0.1s ease-out",
31
+ },
32
+ shapeWrapper: {
33
+ position: "relative" as const,
34
+ display: "flex",
35
+ alignItems: "center",
36
+ justifyContent: "center",
37
+ },
38
+ label: {
39
+ marginTop: 4,
40
+ fontSize: 10,
41
+ fontWeight: 500,
42
+ maxWidth: 80,
43
+ textAlign: "center" as const,
44
+ overflow: "hidden",
45
+ textOverflow: "ellipsis",
46
+ whiteSpace: "nowrap" as const,
47
+ fontFamily:
48
+ "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
49
+ letterSpacing: "-0.01em",
50
+ lineHeight: 1.2,
51
+ },
52
+ handle: {
53
+ position: "absolute" as const,
54
+ top: "50%",
55
+ left: "50%",
56
+ transform: "translate(-50%, -50%)",
57
+ width: 1,
58
+ height: 1,
59
+ background: "transparent",
60
+ border: "none",
61
+ opacity: 0,
62
+ pointerEvents: "none" as const,
63
+ },
64
+ };
65
+
66
+ /**
67
+ * Observable node component with professional circular design.
68
+ */
69
+ function ObservableNodeComponent({ data, selected }: NodeProps) {
24
70
  const nodeData = data as unknown as ObservableNodeData;
25
- const {
26
- label,
27
- emoji,
28
- shape,
29
- level,
30
- isRoot,
31
- whitelisted,
32
- fullValue,
33
- } = nodeData;
71
+ const { label, level, isRoot, whitelisted, fullValue, observableType } =
72
+ nodeData;
34
73
 
35
- const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
36
74
  const borderColor = getLevelColor(level);
37
75
  const backgroundColor = getLevelBackgroundColor(level);
38
76
 
39
- // Get shape styles
40
- const getShapeStyle = (): React.CSSProperties => {
41
- const baseStyle: React.CSSProperties = {
42
- width: size,
43
- height: size,
77
+ // Get the appropriate icon component
78
+ const IconComponent = useMemo(() => {
79
+ if (isRoot) return CrosshairIcon;
80
+ return getObservableIcon(observableType);
81
+ }, [isRoot, observableType]);
82
+
83
+ // Memoize styles to prevent recalculation
84
+ const shapeStyle = useMemo(() => {
85
+ if (isRoot) {
86
+ // Root node is a rounded rectangle (pill shape)
87
+ return {
88
+ width: ROOT_NODE_WIDTH,
89
+ height: ROOT_NODE_HEIGHT,
90
+ borderRadius: ROOT_NODE_HEIGHT / 2,
91
+ display: "flex",
92
+ alignItems: "center",
93
+ justifyContent: "center",
94
+ backgroundColor,
95
+ border: `2.5px solid ${borderColor}`,
96
+ boxShadow: selected
97
+ ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)`
98
+ : "0 2px 8px rgba(0,0,0,0.08)",
99
+ opacity: whitelisted ? 0.5 : 1,
100
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out",
101
+ };
102
+ }
103
+
104
+ // All other nodes are circles
105
+ return {
106
+ width: NODE_SIZE,
107
+ height: NODE_SIZE,
108
+ borderRadius: "50%",
44
109
  display: "flex",
45
110
  alignItems: "center",
46
111
  justifyContent: "center",
47
112
  backgroundColor,
48
- border: `${selected ? 3 : 2}px solid ${borderColor}`,
113
+ border: `2px solid ${borderColor}`,
114
+ boxShadow: selected
115
+ ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)`
116
+ : "0 2px 6px rgba(0,0,0,0.08)",
49
117
  opacity: whitelisted ? 0.5 : 1,
50
- fontSize: isRoot ? 14 : 12,
118
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out",
51
119
  };
120
+ }, [isRoot, backgroundColor, borderColor, selected, whitelisted]);
52
121
 
53
- switch (shape) {
54
- case "square":
55
- return { ...baseStyle, borderRadius: 4 };
56
- case "circle":
57
- return { ...baseStyle, borderRadius: "50%" };
58
- case "triangle":
59
- // Use clip-path for triangle
60
- return {
61
- ...baseStyle,
62
- borderRadius: 0,
63
- border: "none",
64
- background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
65
- clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
66
- position: "relative",
67
- };
68
- case "rectangle":
69
- default:
70
- return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
71
- }
72
- };
73
-
74
- // For triangle, we need a different approach
75
- const isTriangle = shape === "triangle";
122
+ const labelStyle = useMemo(
123
+ () => ({
124
+ ...nodeStyles.label,
125
+ color: whitelisted ? "#9ca3af" : "#374151",
126
+ }),
127
+ [whitelisted]
128
+ );
76
129
 
77
130
  return (
78
- <div
79
- className="observable-node"
80
- style={{
81
- display: "flex",
82
- flexDirection: "column",
83
- alignItems: "center",
84
- cursor: "pointer",
85
- }}
86
- >
131
+ <div className="observable-node" style={nodeStyles.container}>
87
132
  {/* Shape container */}
88
- <div style={{ position: "relative" }}>
89
- {isTriangle ? (
90
- // Triangle using SVG
91
- <svg width={size} height={size} viewBox="0 0 100 100">
92
- <polygon
93
- points="50,10 90,90 10,90"
94
- fill={backgroundColor}
95
- stroke={borderColor}
96
- strokeWidth={selected ? 6 : 4}
97
- opacity={whitelisted ? 0.5 : 1}
98
- />
99
- <text
100
- x="50"
101
- y="65"
102
- textAnchor="middle"
103
- fontSize="32"
104
- dominantBaseline="middle"
105
- >
106
- {emoji}
107
- </text>
108
- </svg>
109
- ) : (
110
- // Other shapes using CSS
111
- <div style={getShapeStyle()}>
112
- <span style={{ userSelect: "none" }}>{emoji}</span>
113
- </div>
114
- )}
133
+ <div style={nodeStyles.shapeWrapper}>
134
+ <div style={shapeStyle}>
135
+ <IconComponent
136
+ size={isRoot ? ROOT_ICON_SIZE : ICON_SIZE}
137
+ color={borderColor}
138
+ />
139
+ </div>
115
140
 
116
- {/* Center handle for source connections */}
117
- <Handle
118
- type="source"
119
- position={Position.Right}
120
- id="source"
121
- style={{
122
- position: "absolute",
123
- top: "50%",
124
- left: "50%",
125
- transform: "translate(-50%, -50%)",
126
- width: 1,
127
- height: 1,
128
- background: "transparent",
129
- border: "none",
130
- opacity: 0,
131
- }}
132
- />
133
- {/* Center handle for target connections */}
134
- <Handle
135
- type="target"
136
- position={Position.Left}
137
- id="target"
138
- style={{
139
- position: "absolute",
140
- top: "50%",
141
- left: "50%",
142
- transform: "translate(-50%, -50%)",
143
- width: 1,
144
- height: 1,
145
- background: "transparent",
146
- border: "none",
147
- opacity: 0,
148
- }}
149
- />
141
+ {/* Hidden handles for edge connections - centered */}
142
+ <Handle type="source" position={Position.Right} style={nodeStyles.handle} />
143
+ <Handle type="target" position={Position.Left} style={nodeStyles.handle} />
150
144
  </div>
151
145
 
152
146
  {/* Label below the shape */}
153
- <div
154
- style={{
155
- marginTop: 2,
156
- fontSize: 9,
157
- maxWidth: 70,
158
- textAlign: "center",
159
- overflow: "hidden",
160
- textOverflow: "ellipsis",
161
- whiteSpace: "nowrap",
162
- color: "#374151",
163
- fontFamily: "system-ui, sans-serif",
164
- }}
165
- title={fullValue}
166
- >
147
+ <div style={labelStyle} title={fullValue}>
167
148
  {label}
168
149
  </div>
169
150
  </div>