@cyvest/cyvest-vis 4.0.0 → 4.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyvest/cyvest-vis",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "@dagrejs/dagre": "^1.1.8",
14
14
  "@xyflow/react": "^12.10.0",
15
15
  "d3-force": "^3.0.0",
16
- "@cyvest/cyvest-js": "4.0.0"
16
+ "@cyvest/cyvest-js": "4.2.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/d3-force": "^3.0.10",
@@ -2,63 +2,128 @@
2
2
  * CyvestGraph component - combined view with toggle between observables and investigation views.
3
3
  */
4
4
 
5
- import React, { useState, useCallback } from "react";
5
+ import React, { useState, useCallback, useMemo } from "react";
6
6
  import type { CyvestGraphProps, InvestigationNodeType } from "../types";
7
7
  import { ObservablesGraph } from "./ObservablesGraph";
8
8
  import { InvestigationGraph } from "./InvestigationGraph";
9
9
 
10
10
  /**
11
- * View toggle button component.
11
+ * View toggle button component with modern design.
12
12
  */
13
13
  const ViewToggle: React.FC<{
14
14
  currentView: "observables" | "investigation";
15
15
  onChange: (view: "observables" | "investigation") => void;
16
16
  }> = ({ currentView, onChange }) => {
17
+ const containerStyle: React.CSSProperties = useMemo(
18
+ () => ({
19
+ position: "absolute",
20
+ top: 12,
21
+ left: 12,
22
+ display: "flex",
23
+ gap: 2,
24
+ background: "rgba(255, 255, 255, 0.95)",
25
+ backdropFilter: "blur(8px)",
26
+ padding: 4,
27
+ borderRadius: 10,
28
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
29
+ zIndex: 10,
30
+ fontFamily:
31
+ "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
32
+ border: "1px solid rgba(0,0,0,0.06)",
33
+ }),
34
+ []
35
+ );
36
+
37
+ const getButtonStyle = useCallback(
38
+ (isActive: boolean): React.CSSProperties => ({
39
+ padding: "8px 14px",
40
+ border: "none",
41
+ borderRadius: 7,
42
+ cursor: "pointer",
43
+ fontSize: 12,
44
+ fontWeight: isActive ? 600 : 500,
45
+ background: isActive
46
+ ? "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)"
47
+ : "transparent",
48
+ color: isActive ? "white" : "#4b5563",
49
+ transition: "all 0.15s ease",
50
+ letterSpacing: "-0.01em",
51
+ }),
52
+ []
53
+ );
54
+
17
55
  return (
18
- <div
19
- style={{
20
- position: "absolute",
21
- top: 10,
22
- left: 10,
23
- display: "flex",
24
- gap: 4,
25
- background: "white",
26
- padding: 4,
27
- borderRadius: 8,
28
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
29
- zIndex: 10,
30
- fontFamily: "system-ui, sans-serif",
31
- }}
32
- >
56
+ <div style={containerStyle}>
33
57
  <button
34
58
  onClick={() => onChange("observables")}
35
- style={{
36
- padding: "6px 12px",
37
- border: "none",
38
- borderRadius: 4,
39
- cursor: "pointer",
40
- fontSize: 12,
41
- fontWeight: currentView === "observables" ? 600 : 400,
42
- background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
43
- color: currentView === "observables" ? "white" : "#374151",
59
+ style={getButtonStyle(currentView === "observables")}
60
+ onMouseEnter={(e) => {
61
+ if (currentView !== "observables") {
62
+ e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
63
+ e.currentTarget.style.color = "#3b82f6";
64
+ }
65
+ }}
66
+ onMouseLeave={(e) => {
67
+ if (currentView !== "observables") {
68
+ e.currentTarget.style.background = "transparent";
69
+ e.currentTarget.style.color = "#4b5563";
70
+ }
44
71
  }}
45
72
  >
46
- Observables
73
+ <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
74
+ <svg
75
+ width="14"
76
+ height="14"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth="2"
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ >
84
+ <circle cx="12" cy="12" r="3" />
85
+ <circle cx="12" cy="12" r="10" />
86
+ <line x1="12" y1="2" x2="12" y2="4" />
87
+ <line x1="12" y1="20" x2="12" y2="22" />
88
+ <line x1="2" y1="12" x2="4" y2="12" />
89
+ <line x1="20" y1="12" x2="22" y2="12" />
90
+ </svg>
91
+ Observables
92
+ </span>
47
93
  </button>
48
94
  <button
49
95
  onClick={() => onChange("investigation")}
50
- style={{
51
- padding: "6px 12px",
52
- border: "none",
53
- borderRadius: 4,
54
- cursor: "pointer",
55
- fontSize: 12,
56
- fontWeight: currentView === "investigation" ? 600 : 400,
57
- background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
58
- color: currentView === "investigation" ? "white" : "#374151",
96
+ style={getButtonStyle(currentView === "investigation")}
97
+ onMouseEnter={(e) => {
98
+ if (currentView !== "investigation") {
99
+ e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
100
+ e.currentTarget.style.color = "#3b82f6";
101
+ }
102
+ }}
103
+ onMouseLeave={(e) => {
104
+ if (currentView !== "investigation") {
105
+ e.currentTarget.style.background = "transparent";
106
+ e.currentTarget.style.color = "#4b5563";
107
+ }
59
108
  }}
60
109
  >
61
- Investigation
110
+ <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
111
+ <svg
112
+ width="14"
113
+ height="14"
114
+ viewBox="0 0 24 24"
115
+ fill="none"
116
+ stroke="currentColor"
117
+ strokeWidth="2"
118
+ strokeLinecap="round"
119
+ strokeLinejoin="round"
120
+ >
121
+ <rect x="3" y="3" width="18" height="18" rx="2" />
122
+ <path d="M9 3v18" />
123
+ <path d="M3 9h18" />
124
+ </svg>
125
+ Investigation
126
+ </span>
62
127
  </button>
63
128
  </div>
64
129
  );
@@ -76,7 +141,9 @@ export const CyvestGraph: React.FC<CyvestGraphProps> = ({
76
141
  className,
77
142
  showViewToggle = true,
78
143
  }) => {
79
- const [view, setView] = useState<"observables" | "investigation">(initialView);
144
+ const [view, setView] = useState<"observables" | "investigation">(
145
+ initialView
146
+ );
80
147
 
81
148
  const handleNodeClick = useCallback(
82
149
  (nodeId: string, _nodeType?: InvestigationNodeType) => {
@@ -85,15 +152,17 @@ export const CyvestGraph: React.FC<CyvestGraphProps> = ({
85
152
  [onNodeClick]
86
153
  );
87
154
 
155
+ const containerStyle: React.CSSProperties = useMemo(
156
+ () => ({
157
+ width,
158
+ height,
159
+ position: "relative",
160
+ }),
161
+ [width, height]
162
+ );
163
+
88
164
  return (
89
- <div
90
- className={className}
91
- style={{
92
- width,
93
- height,
94
- position: "relative",
95
- }}
96
- >
165
+ <div className={className} style={containerStyle}>
97
166
  {showViewToggle && <ViewToggle currentView={view} onChange={setView} />}
98
167
 
99
168
  {view === "observables" ? (
@@ -1,14 +1,31 @@
1
1
  /**
2
2
  * Floating Edge component for use with force-directed layout.
3
- * Uses simple straight lines that connect to node edges.
3
+ * Uses smooth bezier curves that connect to node centers.
4
4
  */
5
5
 
6
- import React, { memo } from "react";
7
- import { BaseEdge, getStraightPath, type EdgeProps } from "@xyflow/react";
6
+ import React, { memo, useMemo } from "react";
7
+ import { BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
8
8
 
9
9
  /**
10
- * Floating edge component that uses straight lines.
11
- * React Flow passes sourceX, sourceY, targetX, targetY based on node positions.
10
+ * Calculate control point offset based on distance.
11
+ * Longer edges get more curve for better visibility.
12
+ */
13
+ function getControlOffset(
14
+ sourceX: number,
15
+ sourceY: number,
16
+ targetX: number,
17
+ targetY: number
18
+ ): number {
19
+ const dx = targetX - sourceX;
20
+ const dy = targetY - sourceY;
21
+ const distance = Math.sqrt(dx * dx + dy * dy);
22
+ // Scale curve intensity with distance, capped
23
+ return Math.min(Math.max(distance * 0.15, 20), 60);
24
+ }
25
+
26
+ /**
27
+ * Floating edge component with smooth bezier curves.
28
+ * The curve adapts to the edge length for optimal visibility.
12
29
  */
13
30
  function FloatingEdgeComponent({
14
31
  id,
@@ -18,23 +35,36 @@ function FloatingEdgeComponent({
18
35
  targetY,
19
36
  style,
20
37
  markerEnd,
38
+ selected,
21
39
  }: EdgeProps) {
22
- const [edgePath] = getStraightPath({
40
+ const offset = useMemo(
41
+ () => getControlOffset(sourceX, sourceY, targetX, targetY),
42
+ [sourceX, sourceY, targetX, targetY]
43
+ );
44
+
45
+ const [edgePath] = getBezierPath({
23
46
  sourceX,
24
47
  sourceY,
25
48
  targetX,
26
49
  targetY,
50
+ curvature: 0.15,
27
51
  });
28
52
 
53
+ const edgeStyle = useMemo(
54
+ () => ({
55
+ strokeWidth: selected ? 2.5 : 1.5,
56
+ stroke: selected ? "#3b82f6" : "#94a3b8",
57
+ transition: "stroke 0.15s ease, stroke-width 0.15s ease",
58
+ ...style,
59
+ }),
60
+ [selected, style]
61
+ );
62
+
29
63
  return (
30
64
  <BaseEdge
31
65
  id={id}
32
66
  path={edgePath}
33
- style={{
34
- strokeWidth: 1.5,
35
- stroke: "#94a3b8",
36
- ...style,
37
- }}
67
+ style={edgeStyle}
38
68
  markerEnd={markerEnd}
39
69
  />
40
70
  );