@cyvest/cyvest-vis 3.2.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.
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/components/CyvestGraph.tsx
2
- import { useState as useState2, useCallback as useCallback4 } from "react";
2
+ import { useState as useState2, useCallback as useCallback4, useMemo as useMemo8 } from "react";
3
3
 
4
4
  // src/components/ObservablesGraph.tsx
5
- import React3, { useMemo as useMemo2, useCallback as useCallback2, useState } from "react";
5
+ import React3, { useMemo as useMemo4, useCallback as useCallback2, useState, useRef as useRef2 } from "react";
6
6
  import {
7
7
  ReactFlow,
8
8
  ReactFlowProvider,
@@ -11,86 +11,28 @@ import {
11
11
  MiniMap,
12
12
  useNodesState,
13
13
  useEdgesState,
14
- ConnectionMode
14
+ ConnectionMode,
15
+ BackgroundVariant,
16
+ Panel
15
17
  } from "@xyflow/react";
16
18
  import "@xyflow/react/dist/style.css";
17
- import { getObservableGraph, findRootObservables } from "@cyvest/cyvest-js";
19
+ import { getObservableGraph } from "@cyvest/cyvest-js";
18
20
 
19
21
  // src/types.ts
20
22
  var DEFAULT_FORCE_CONFIG = {
21
23
  chargeStrength: -200,
22
24
  linkDistance: 80,
23
25
  centerStrength: 0.05,
24
- collisionRadius: 40,
26
+ collisionRadius: 45,
25
27
  iterations: 300
26
28
  };
27
29
 
28
30
  // src/components/ObservableNode.tsx
29
- import { memo } from "react";
31
+ import { memo, useMemo } from "react";
30
32
  import { Handle, Position } from "@xyflow/react";
31
33
 
32
34
  // src/utils/observables.ts
33
- var OBSERVABLE_EMOJI_MAP = {
34
- // Network
35
- "ipv4-addr": "\u{1F310}",
36
- "ipv6-addr": "\u{1F310}",
37
- "domain-name": "\u{1F3E0}",
38
- url: "\u{1F517}",
39
- "autonomous-system": "\u{1F30D}",
40
- "mac-addr": "\u{1F4F6}",
41
- // Email
42
- "email-addr": "\u{1F4E7}",
43
- "email-message": "\u2709\uFE0F",
44
- // File
45
- file: "\u{1F4C4}",
46
- "file-hash": "\u{1F510}",
47
- "file:hash:md5": "\u{1F510}",
48
- "file:hash:sha1": "\u{1F510}",
49
- "file:hash:sha256": "\u{1F510}",
50
- // User/Identity
51
- user: "\u{1F464}",
52
- "user-account": "\u{1F464}",
53
- identity: "\u{1FAAA}",
54
- // Process/System
55
- process: "\u2699\uFE0F",
56
- software: "\u{1F4BF}",
57
- "windows-registry-key": "\u{1F4DD}",
58
- // Threat Intelligence
59
- "threat-actor": "\u{1F479}",
60
- malware: "\u{1F9A0}",
61
- "attack-pattern": "\u2694\uFE0F",
62
- campaign: "\u{1F3AF}",
63
- indicator: "\u{1F6A8}",
64
- // Artifacts
65
- artifact: "\u{1F9EA}",
66
- certificate: "\u{1F4DC}",
67
- "x509-certificate": "\u{1F4DC}",
68
- // Default
69
- unknown: "\u2753"
70
- };
71
- function getObservableEmoji(observableType) {
72
- const normalized = observableType.toLowerCase().trim();
73
- return OBSERVABLE_EMOJI_MAP[normalized] ?? OBSERVABLE_EMOJI_MAP.unknown;
74
- }
75
- var OBSERVABLE_SHAPE_MAP = {
76
- // Domains get squares
77
- "domain-name": "square",
78
- // URLs get circles
79
- url: "circle",
80
- // IPs get triangles
81
- "ipv4-addr": "triangle",
82
- "ipv6-addr": "triangle",
83
- // Root/files get rectangles (default for root)
84
- file: "rectangle",
85
- "email-message": "rectangle"
86
- };
87
- function getObservableShape(observableType, isRoot) {
88
- if (isRoot) {
89
- return "rectangle";
90
- }
91
- const normalized = observableType.toLowerCase().trim();
92
- return OBSERVABLE_SHAPE_MAP[normalized] ?? "circle";
93
- }
35
+ import { getColorForLevel } from "@cyvest/cyvest-js";
94
36
  function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
95
37
  if (value.length <= maxLength) {
96
38
  return value;
@@ -102,213 +44,776 @@ function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
102
44
  return `${value.slice(0, maxLength - 1)}\u2026`;
103
45
  }
104
46
  function getLevelColor(level) {
105
- const colors = {
106
- NONE: "#6b7280",
107
- // gray-500
108
- TRUSTED: "#22c55e",
109
- // green-500
110
- INFO: "#3b82f6",
111
- // blue-500
112
- SAFE: "#22c55e",
113
- // green-500
114
- NOTABLE: "#eab308",
115
- // yellow-500
116
- SUSPICIOUS: "#f97316",
117
- // orange-500
118
- MALICIOUS: "#ef4444"
119
- // red-500
120
- };
121
- return colors[level] ?? colors.NONE;
47
+ return getColorForLevel(level);
48
+ }
49
+ function lightenHexColor(hex, amount) {
50
+ const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
51
+ if (normalized.length !== 6) {
52
+ return hex;
53
+ }
54
+ const r = parseInt(normalized.slice(0, 2), 16);
55
+ const g = parseInt(normalized.slice(2, 4), 16);
56
+ const b = parseInt(normalized.slice(4, 6), 16);
57
+ const mix = (channel) => Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
58
+ const toHex = (channel) => channel.toString(16).padStart(2, "0");
59
+ return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
122
60
  }
123
61
  function getLevelBackgroundColor(level) {
124
- const colors = {
125
- NONE: "#f3f4f6",
126
- // gray-100
127
- TRUSTED: "#dcfce7",
128
- // green-100
129
- INFO: "#dbeafe",
130
- // blue-100
131
- SAFE: "#dcfce7",
132
- // green-100
133
- NOTABLE: "#fef9c3",
134
- // yellow-100
135
- SUSPICIOUS: "#ffedd5",
136
- // orange-100
137
- MALICIOUS: "#fee2e2"
138
- // red-100
139
- };
140
- return colors[level] ?? colors.NONE;
62
+ return lightenHexColor(getLevelColor(level), 0.88);
141
63
  }
142
- var INVESTIGATION_NODE_EMOJI = {
143
- root: "\u{1F3AF}",
144
- check: "\u2705",
145
- container: "\u{1F4E6}"
64
+
65
+ // src/components/Icons.tsx
66
+ import { jsx, jsxs } from "react/jsx-runtime";
67
+ var defaultSize = 16;
68
+ var defaultColor = "currentColor";
69
+ var GlobeIcon = ({
70
+ size = defaultSize,
71
+ color = defaultColor,
72
+ className
73
+ }) => /* @__PURE__ */ jsxs(
74
+ "svg",
75
+ {
76
+ width: size,
77
+ height: size,
78
+ viewBox: "0 0 24 24",
79
+ fill: "none",
80
+ stroke: color,
81
+ strokeWidth: "2",
82
+ strokeLinecap: "round",
83
+ strokeLinejoin: "round",
84
+ className,
85
+ children: [
86
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
87
+ /* @__PURE__ */ jsx("path", { d: "M2 12h20" }),
88
+ /* @__PURE__ */ jsx("path", { d: "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" })
89
+ ]
90
+ }
91
+ );
92
+ var DomainIcon = ({
93
+ size = defaultSize,
94
+ color = defaultColor,
95
+ className
96
+ }) => /* @__PURE__ */ jsxs(
97
+ "svg",
98
+ {
99
+ width: size,
100
+ height: size,
101
+ viewBox: "0 0 24 24",
102
+ fill: "none",
103
+ stroke: color,
104
+ strokeWidth: "2",
105
+ strokeLinecap: "round",
106
+ strokeLinejoin: "round",
107
+ className,
108
+ children: [
109
+ /* @__PURE__ */ jsx("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
110
+ /* @__PURE__ */ jsx("polyline", { points: "9,22 9,12 15,12 15,22" })
111
+ ]
112
+ }
113
+ );
114
+ var LinkIcon = ({
115
+ size = defaultSize,
116
+ color = defaultColor,
117
+ className
118
+ }) => /* @__PURE__ */ jsxs(
119
+ "svg",
120
+ {
121
+ width: size,
122
+ height: size,
123
+ viewBox: "0 0 24 24",
124
+ fill: "none",
125
+ stroke: color,
126
+ strokeWidth: "2",
127
+ strokeLinecap: "round",
128
+ strokeLinejoin: "round",
129
+ className,
130
+ children: [
131
+ /* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
132
+ /* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
133
+ ]
134
+ }
135
+ );
136
+ var MailIcon = ({
137
+ size = defaultSize,
138
+ color = defaultColor,
139
+ className
140
+ }) => /* @__PURE__ */ jsxs(
141
+ "svg",
142
+ {
143
+ width: size,
144
+ height: size,
145
+ viewBox: "0 0 24 24",
146
+ fill: "none",
147
+ stroke: color,
148
+ strokeWidth: "2",
149
+ strokeLinecap: "round",
150
+ strokeLinejoin: "round",
151
+ className,
152
+ children: [
153
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
154
+ /* @__PURE__ */ jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
155
+ ]
156
+ }
157
+ );
158
+ var EnvelopeIcon = ({
159
+ size = defaultSize,
160
+ color = defaultColor,
161
+ className
162
+ }) => /* @__PURE__ */ jsxs(
163
+ "svg",
164
+ {
165
+ width: size,
166
+ height: size,
167
+ viewBox: "0 0 24 24",
168
+ fill: "none",
169
+ stroke: color,
170
+ strokeWidth: "2",
171
+ strokeLinecap: "round",
172
+ strokeLinejoin: "round",
173
+ className,
174
+ children: [
175
+ /* @__PURE__ */ jsx("path", { d: "M22 12h-6l-2 3h-4l-2-3H2" }),
176
+ /* @__PURE__ */ jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" })
177
+ ]
178
+ }
179
+ );
180
+ var FileIcon = ({
181
+ size = defaultSize,
182
+ color = defaultColor,
183
+ className
184
+ }) => /* @__PURE__ */ jsxs(
185
+ "svg",
186
+ {
187
+ width: size,
188
+ height: size,
189
+ viewBox: "0 0 24 24",
190
+ fill: "none",
191
+ stroke: color,
192
+ strokeWidth: "2",
193
+ strokeLinecap: "round",
194
+ strokeLinejoin: "round",
195
+ className,
196
+ children: [
197
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
198
+ /* @__PURE__ */ jsx("polyline", { points: "14,2 14,8 20,8" })
199
+ ]
200
+ }
201
+ );
202
+ var HashIcon = ({
203
+ size = defaultSize,
204
+ color = defaultColor,
205
+ className
206
+ }) => /* @__PURE__ */ jsxs(
207
+ "svg",
208
+ {
209
+ width: size,
210
+ height: size,
211
+ viewBox: "0 0 24 24",
212
+ fill: "none",
213
+ stroke: color,
214
+ strokeWidth: "2",
215
+ strokeLinecap: "round",
216
+ strokeLinejoin: "round",
217
+ className,
218
+ children: [
219
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "9", x2: "20", y2: "9" }),
220
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "15", x2: "20", y2: "15" }),
221
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "3", x2: "8", y2: "21" }),
222
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "3", x2: "14", y2: "21" })
223
+ ]
224
+ }
225
+ );
226
+ var UserIcon = ({
227
+ size = defaultSize,
228
+ color = defaultColor,
229
+ className
230
+ }) => /* @__PURE__ */ jsxs(
231
+ "svg",
232
+ {
233
+ width: size,
234
+ height: size,
235
+ viewBox: "0 0 24 24",
236
+ fill: "none",
237
+ stroke: color,
238
+ strokeWidth: "2",
239
+ strokeLinecap: "round",
240
+ strokeLinejoin: "round",
241
+ className,
242
+ children: [
243
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "5" }),
244
+ /* @__PURE__ */ jsx("path", { d: "M20 21a8 8 0 1 0-16 0" })
245
+ ]
246
+ }
247
+ );
248
+ var IdCardIcon = ({
249
+ size = defaultSize,
250
+ color = defaultColor,
251
+ className
252
+ }) => /* @__PURE__ */ jsxs(
253
+ "svg",
254
+ {
255
+ width: size,
256
+ height: size,
257
+ viewBox: "0 0 24 24",
258
+ fill: "none",
259
+ stroke: color,
260
+ strokeWidth: "2",
261
+ strokeLinecap: "round",
262
+ strokeLinejoin: "round",
263
+ className,
264
+ children: [
265
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "5", width: "20", height: "14", rx: "2" }),
266
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "12", r: "2" }),
267
+ /* @__PURE__ */ jsx("path", { d: "M14 10h4" }),
268
+ /* @__PURE__ */ jsx("path", { d: "M14 14h4" })
269
+ ]
270
+ }
271
+ );
272
+ var GearIcon = ({
273
+ size = defaultSize,
274
+ color = defaultColor,
275
+ className
276
+ }) => /* @__PURE__ */ jsxs(
277
+ "svg",
278
+ {
279
+ width: size,
280
+ height: size,
281
+ viewBox: "0 0 24 24",
282
+ fill: "none",
283
+ stroke: color,
284
+ strokeWidth: "2",
285
+ strokeLinecap: "round",
286
+ strokeLinejoin: "round",
287
+ className,
288
+ children: [
289
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
290
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
291
+ ]
292
+ }
293
+ );
294
+ var AppIcon = ({
295
+ size = defaultSize,
296
+ color = defaultColor,
297
+ className
298
+ }) => /* @__PURE__ */ jsxs(
299
+ "svg",
300
+ {
301
+ width: size,
302
+ height: size,
303
+ viewBox: "0 0 24 24",
304
+ fill: "none",
305
+ stroke: color,
306
+ strokeWidth: "2",
307
+ strokeLinecap: "round",
308
+ strokeLinejoin: "round",
309
+ className,
310
+ children: [
311
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
312
+ /* @__PURE__ */ jsx("path", { d: "M9 3v18" }),
313
+ /* @__PURE__ */ jsx("path", { d: "M3 9h18" })
314
+ ]
315
+ }
316
+ );
317
+ var RegistryIcon = ({
318
+ size = defaultSize,
319
+ color = defaultColor,
320
+ className
321
+ }) => /* @__PURE__ */ jsx(
322
+ "svg",
323
+ {
324
+ width: size,
325
+ height: size,
326
+ viewBox: "0 0 24 24",
327
+ fill: "none",
328
+ stroke: color,
329
+ strokeWidth: "2",
330
+ strokeLinecap: "round",
331
+ strokeLinejoin: "round",
332
+ className,
333
+ children: /* @__PURE__ */ jsx("path", { d: "m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4" })
334
+ }
335
+ );
336
+ var ThreatActorIcon = ({
337
+ size = defaultSize,
338
+ color = defaultColor,
339
+ className
340
+ }) => /* @__PURE__ */ jsxs(
341
+ "svg",
342
+ {
343
+ width: size,
344
+ height: size,
345
+ viewBox: "0 0 24 24",
346
+ fill: "none",
347
+ stroke: color,
348
+ strokeWidth: "2",
349
+ strokeLinecap: "round",
350
+ strokeLinejoin: "round",
351
+ className,
352
+ children: [
353
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "10", r: "7" }),
354
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.5", fill: color }),
355
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "9", r: "1.5", fill: color }),
356
+ /* @__PURE__ */ jsx("path", { d: "M9 17v-2" }),
357
+ /* @__PURE__ */ jsx("path", { d: "M12 17v-2" }),
358
+ /* @__PURE__ */ jsx("path", { d: "M15 17v-2" })
359
+ ]
360
+ }
361
+ );
362
+ var BugIcon = ({
363
+ size = defaultSize,
364
+ color = defaultColor,
365
+ className
366
+ }) => /* @__PURE__ */ jsxs(
367
+ "svg",
368
+ {
369
+ width: size,
370
+ height: size,
371
+ viewBox: "0 0 24 24",
372
+ fill: "none",
373
+ stroke: color,
374
+ strokeWidth: "2",
375
+ strokeLinecap: "round",
376
+ strokeLinejoin: "round",
377
+ className,
378
+ children: [
379
+ /* @__PURE__ */ jsx("path", { d: "m8 2 1.88 1.88" }),
380
+ /* @__PURE__ */ jsx("path", { d: "M14.12 3.88 16 2" }),
381
+ /* @__PURE__ */ jsx("path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }),
382
+ /* @__PURE__ */ jsx("path", { d: "M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" }),
383
+ /* @__PURE__ */ jsx("path", { d: "M12 20v-9" }),
384
+ /* @__PURE__ */ jsx("path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }),
385
+ /* @__PURE__ */ jsx("path", { d: "M6 13H2" }),
386
+ /* @__PURE__ */ jsx("path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }),
387
+ /* @__PURE__ */ jsx("path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }),
388
+ /* @__PURE__ */ jsx("path", { d: "M22 13h-4" }),
389
+ /* @__PURE__ */ jsx("path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" })
390
+ ]
391
+ }
392
+ );
393
+ var SwordIcon = ({
394
+ size = defaultSize,
395
+ color = defaultColor,
396
+ className
397
+ }) => /* @__PURE__ */ jsxs(
398
+ "svg",
399
+ {
400
+ width: size,
401
+ height: size,
402
+ viewBox: "0 0 24 24",
403
+ fill: "none",
404
+ stroke: color,
405
+ strokeWidth: "2",
406
+ strokeLinecap: "round",
407
+ strokeLinejoin: "round",
408
+ className,
409
+ children: [
410
+ /* @__PURE__ */ jsx("polyline", { points: "14.5,17.5 3,6 3,3 6,3 17.5,14.5" }),
411
+ /* @__PURE__ */ jsx("line", { x1: "13", y1: "19", x2: "19", y2: "13" }),
412
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "16", x2: "20", y2: "20" }),
413
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "21", x2: "21", y2: "19" })
414
+ ]
415
+ }
416
+ );
417
+ var TargetIcon = ({
418
+ size = defaultSize,
419
+ color = defaultColor,
420
+ className
421
+ }) => /* @__PURE__ */ jsxs(
422
+ "svg",
423
+ {
424
+ width: size,
425
+ height: size,
426
+ viewBox: "0 0 24 24",
427
+ fill: "none",
428
+ stroke: color,
429
+ strokeWidth: "2",
430
+ strokeLinecap: "round",
431
+ strokeLinejoin: "round",
432
+ className,
433
+ children: [
434
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
435
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "6" }),
436
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2" })
437
+ ]
438
+ }
439
+ );
440
+ var AlertIcon = ({
441
+ size = defaultSize,
442
+ color = defaultColor,
443
+ className
444
+ }) => /* @__PURE__ */ jsxs(
445
+ "svg",
446
+ {
447
+ width: size,
448
+ height: size,
449
+ viewBox: "0 0 24 24",
450
+ fill: "none",
451
+ stroke: color,
452
+ strokeWidth: "2",
453
+ strokeLinecap: "round",
454
+ strokeLinejoin: "round",
455
+ className,
456
+ children: [
457
+ /* @__PURE__ */ jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
458
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
459
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
460
+ ]
461
+ }
462
+ );
463
+ var FlaskIcon = ({
464
+ size = defaultSize,
465
+ color = defaultColor,
466
+ className
467
+ }) => /* @__PURE__ */ jsxs(
468
+ "svg",
469
+ {
470
+ width: size,
471
+ height: size,
472
+ viewBox: "0 0 24 24",
473
+ fill: "none",
474
+ stroke: color,
475
+ strokeWidth: "2",
476
+ strokeLinecap: "round",
477
+ strokeLinejoin: "round",
478
+ className,
479
+ children: [
480
+ /* @__PURE__ */ jsx("path", { d: "M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" }),
481
+ /* @__PURE__ */ jsx("path", { d: "M8.5 2h7" }),
482
+ /* @__PURE__ */ jsx("path", { d: "M7 16h10" })
483
+ ]
484
+ }
485
+ );
486
+ var CertificateIcon = ({
487
+ size = defaultSize,
488
+ color = defaultColor,
489
+ className
490
+ }) => /* @__PURE__ */ jsxs(
491
+ "svg",
492
+ {
493
+ width: size,
494
+ height: size,
495
+ viewBox: "0 0 24 24",
496
+ fill: "none",
497
+ stroke: color,
498
+ strokeWidth: "2",
499
+ strokeLinecap: "round",
500
+ strokeLinejoin: "round",
501
+ className,
502
+ children: [
503
+ /* @__PURE__ */ jsx("path", { d: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" }),
504
+ /* @__PURE__ */ jsx("path", { d: "M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Z" }),
505
+ /* @__PURE__ */ jsx("path", { d: "m9.5 15.5-3 3v3l3.5-1.5 3.5 1.5v-3l-3-3" })
506
+ ]
507
+ }
508
+ );
509
+ var WifiIcon = ({
510
+ size = defaultSize,
511
+ color = defaultColor,
512
+ className
513
+ }) => /* @__PURE__ */ jsxs(
514
+ "svg",
515
+ {
516
+ width: size,
517
+ height: size,
518
+ viewBox: "0 0 24 24",
519
+ fill: "none",
520
+ stroke: color,
521
+ strokeWidth: "2",
522
+ strokeLinecap: "round",
523
+ strokeLinejoin: "round",
524
+ className,
525
+ children: [
526
+ /* @__PURE__ */ jsx("path", { d: "M5 12.55a11 11 0 0 1 14.08 0" }),
527
+ /* @__PURE__ */ jsx("path", { d: "M1.42 9a16 16 0 0 1 21.16 0" }),
528
+ /* @__PURE__ */ jsx("path", { d: "M8.53 16.11a6 6 0 0 1 6.95 0" }),
529
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "20", x2: "12.01", y2: "20" })
530
+ ]
531
+ }
532
+ );
533
+ var WorldIcon = ({
534
+ size = defaultSize,
535
+ color = defaultColor,
536
+ className
537
+ }) => /* @__PURE__ */ jsxs(
538
+ "svg",
539
+ {
540
+ width: size,
541
+ height: size,
542
+ viewBox: "0 0 24 24",
543
+ fill: "none",
544
+ stroke: color,
545
+ strokeWidth: "2",
546
+ strokeLinecap: "round",
547
+ strokeLinejoin: "round",
548
+ className,
549
+ children: [
550
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
551
+ /* @__PURE__ */ jsx("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
552
+ /* @__PURE__ */ jsx("path", { d: "M2 12h20" })
553
+ ]
554
+ }
555
+ );
556
+ var QuestionIcon = ({
557
+ size = defaultSize,
558
+ color = defaultColor,
559
+ className
560
+ }) => /* @__PURE__ */ jsxs(
561
+ "svg",
562
+ {
563
+ width: size,
564
+ height: size,
565
+ viewBox: "0 0 24 24",
566
+ fill: "none",
567
+ stroke: color,
568
+ strokeWidth: "2",
569
+ strokeLinecap: "round",
570
+ strokeLinejoin: "round",
571
+ className,
572
+ children: [
573
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
574
+ /* @__PURE__ */ jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
575
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
576
+ ]
577
+ }
578
+ );
579
+ var CheckIcon = ({
580
+ size = defaultSize,
581
+ color = defaultColor,
582
+ className
583
+ }) => /* @__PURE__ */ jsx(
584
+ "svg",
585
+ {
586
+ width: size,
587
+ height: size,
588
+ viewBox: "0 0 24 24",
589
+ fill: "none",
590
+ stroke: color,
591
+ strokeWidth: "2",
592
+ strokeLinecap: "round",
593
+ strokeLinejoin: "round",
594
+ className,
595
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
596
+ }
597
+ );
598
+ var BoxIcon = ({
599
+ size = defaultSize,
600
+ color = defaultColor,
601
+ className
602
+ }) => /* @__PURE__ */ jsxs(
603
+ "svg",
604
+ {
605
+ width: size,
606
+ height: size,
607
+ viewBox: "0 0 24 24",
608
+ fill: "none",
609
+ stroke: color,
610
+ strokeWidth: "2",
611
+ strokeLinecap: "round",
612
+ strokeLinejoin: "round",
613
+ className,
614
+ children: [
615
+ /* @__PURE__ */ jsx("path", { d: "M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" }),
616
+ /* @__PURE__ */ jsx("path", { d: "m3.3 7 8.7 5 8.7-5" }),
617
+ /* @__PURE__ */ jsx("path", { d: "M12 22V12" })
618
+ ]
619
+ }
620
+ );
621
+ var CrosshairIcon = ({
622
+ size = defaultSize,
623
+ color = defaultColor,
624
+ className
625
+ }) => /* @__PURE__ */ jsxs(
626
+ "svg",
627
+ {
628
+ width: size,
629
+ height: size,
630
+ viewBox: "0 0 24 24",
631
+ fill: "none",
632
+ stroke: color,
633
+ strokeWidth: "2",
634
+ strokeLinecap: "round",
635
+ strokeLinejoin: "round",
636
+ className,
637
+ children: [
638
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
639
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "12", x2: "18", y2: "12" }),
640
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "12", x2: "2", y2: "12" }),
641
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "6", x2: "12", y2: "2" }),
642
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "22", x2: "12", y2: "18" })
643
+ ]
644
+ }
645
+ );
646
+ var OBSERVABLE_ICON_MAP = {
647
+ // Network
648
+ "ipv4-addr": GlobeIcon,
649
+ "ipv6-addr": GlobeIcon,
650
+ "domain-name": DomainIcon,
651
+ url: LinkIcon,
652
+ "autonomous-system": WorldIcon,
653
+ "mac-addr": WifiIcon,
654
+ // Email
655
+ "email-addr": MailIcon,
656
+ "email-message": EnvelopeIcon,
657
+ // File
658
+ file: FileIcon,
659
+ "file-hash": HashIcon,
660
+ "file:hash:md5": HashIcon,
661
+ "file:hash:sha1": HashIcon,
662
+ "file:hash:sha256": HashIcon,
663
+ // User/Identity
664
+ user: UserIcon,
665
+ "user-account": UserIcon,
666
+ identity: IdCardIcon,
667
+ // Process/System
668
+ process: GearIcon,
669
+ software: AppIcon,
670
+ "windows-registry-key": RegistryIcon,
671
+ // Threat Intelligence
672
+ "threat-actor": ThreatActorIcon,
673
+ malware: BugIcon,
674
+ "attack-pattern": SwordIcon,
675
+ campaign: TargetIcon,
676
+ indicator: AlertIcon,
677
+ // Artifacts
678
+ artifact: FlaskIcon,
679
+ certificate: CertificateIcon,
680
+ "x509-certificate": CertificateIcon,
681
+ // Default
682
+ unknown: QuestionIcon
683
+ };
684
+ var INVESTIGATION_ICON_MAP = {
685
+ root: CrosshairIcon,
686
+ check: CheckIcon,
687
+ container: BoxIcon
146
688
  };
147
- function getInvestigationNodeEmoji(nodeType) {
148
- return INVESTIGATION_NODE_EMOJI[nodeType] ?? "\u2753";
689
+ function getObservableIcon(observableType) {
690
+ const normalized = observableType.toLowerCase().trim();
691
+ return OBSERVABLE_ICON_MAP[normalized] ?? OBSERVABLE_ICON_MAP.unknown;
692
+ }
693
+ function getInvestigationIcon(nodeType) {
694
+ return INVESTIGATION_ICON_MAP[nodeType] ?? QuestionIcon;
149
695
  }
150
696
 
151
697
  // src/components/ObservableNode.tsx
152
- import { jsx, jsxs } from "react/jsx-runtime";
153
- var NODE_SIZE = 28;
154
- var ROOT_NODE_SIZE = 36;
155
- function ObservableNodeComponent({
156
- data,
157
- selected
158
- }) {
698
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
699
+ var NODE_SIZE = 40;
700
+ var ROOT_NODE_WIDTH = 56;
701
+ var ROOT_NODE_HEIGHT = 40;
702
+ var ICON_SIZE = 18;
703
+ var ROOT_ICON_SIZE = 20;
704
+ var nodeStyles = {
705
+ container: {
706
+ display: "flex",
707
+ flexDirection: "column",
708
+ alignItems: "center",
709
+ cursor: "grab",
710
+ transition: "transform 0.1s ease-out"
711
+ },
712
+ shapeWrapper: {
713
+ position: "relative",
714
+ display: "flex",
715
+ alignItems: "center",
716
+ justifyContent: "center"
717
+ },
718
+ label: {
719
+ marginTop: 4,
720
+ fontSize: 10,
721
+ fontWeight: 500,
722
+ maxWidth: 80,
723
+ textAlign: "center",
724
+ overflow: "hidden",
725
+ textOverflow: "ellipsis",
726
+ whiteSpace: "nowrap",
727
+ fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
728
+ letterSpacing: "-0.01em",
729
+ lineHeight: 1.2
730
+ },
731
+ handle: {
732
+ position: "absolute",
733
+ top: "50%",
734
+ left: "50%",
735
+ transform: "translate(-50%, -50%)",
736
+ width: 1,
737
+ height: 1,
738
+ background: "transparent",
739
+ border: "none",
740
+ opacity: 0,
741
+ pointerEvents: "none"
742
+ }
743
+ };
744
+ function ObservableNodeComponent({ data, selected }) {
159
745
  const nodeData = data;
160
- const {
161
- label,
162
- emoji,
163
- shape,
164
- level,
165
- isRoot,
166
- whitelisted,
167
- fullValue
168
- } = nodeData;
169
- const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
746
+ const { label, level, isRoot, whitelisted, fullValue, observableType } = nodeData;
170
747
  const borderColor = getLevelColor(level);
171
748
  const backgroundColor = getLevelBackgroundColor(level);
172
- const getShapeStyle = () => {
173
- const baseStyle = {
174
- width: size,
175
- height: size,
749
+ const IconComponent = useMemo(() => {
750
+ if (isRoot) return CrosshairIcon;
751
+ return getObservableIcon(observableType);
752
+ }, [isRoot, observableType]);
753
+ const shapeStyle = useMemo(() => {
754
+ if (isRoot) {
755
+ return {
756
+ width: ROOT_NODE_WIDTH,
757
+ height: ROOT_NODE_HEIGHT,
758
+ borderRadius: ROOT_NODE_HEIGHT / 2,
759
+ display: "flex",
760
+ alignItems: "center",
761
+ justifyContent: "center",
762
+ backgroundColor,
763
+ border: `2.5px solid ${borderColor}`,
764
+ boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 8px rgba(0,0,0,0.08)",
765
+ opacity: whitelisted ? 0.5 : 1,
766
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
767
+ };
768
+ }
769
+ return {
770
+ width: NODE_SIZE,
771
+ height: NODE_SIZE,
772
+ borderRadius: "50%",
176
773
  display: "flex",
177
774
  alignItems: "center",
178
775
  justifyContent: "center",
179
776
  backgroundColor,
180
- border: `${selected ? 3 : 2}px solid ${borderColor}`,
777
+ border: `2px solid ${borderColor}`,
778
+ boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 6px rgba(0,0,0,0.08)",
181
779
  opacity: whitelisted ? 0.5 : 1,
182
- fontSize: isRoot ? 14 : 12
780
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
183
781
  };
184
- switch (shape) {
185
- case "square":
186
- return { ...baseStyle, borderRadius: 4 };
187
- case "circle":
188
- return { ...baseStyle, borderRadius: "50%" };
189
- case "triangle":
190
- return {
191
- ...baseStyle,
192
- borderRadius: 0,
193
- border: "none",
194
- background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
195
- clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
196
- position: "relative"
197
- };
198
- case "rectangle":
199
- default:
200
- return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
201
- }
202
- };
203
- const isTriangle = shape === "triangle";
204
- return /* @__PURE__ */ jsxs(
205
- "div",
206
- {
207
- className: "observable-node",
208
- style: {
209
- display: "flex",
210
- flexDirection: "column",
211
- alignItems: "center",
212
- cursor: "pointer"
213
- },
214
- children: [
215
- /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
216
- isTriangle ? (
217
- // Triangle using SVG
218
- /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 100 100", children: [
219
- /* @__PURE__ */ jsx(
220
- "polygon",
221
- {
222
- points: "50,10 90,90 10,90",
223
- fill: backgroundColor,
224
- stroke: borderColor,
225
- strokeWidth: selected ? 6 : 4,
226
- opacity: whitelisted ? 0.5 : 1
227
- }
228
- ),
229
- /* @__PURE__ */ jsx(
230
- "text",
231
- {
232
- x: "50",
233
- y: "65",
234
- textAnchor: "middle",
235
- fontSize: "32",
236
- dominantBaseline: "middle",
237
- children: emoji
238
- }
239
- )
240
- ] })
241
- ) : (
242
- // Other shapes using CSS
243
- /* @__PURE__ */ jsx("div", { style: getShapeStyle(), children: /* @__PURE__ */ jsx("span", { style: { userSelect: "none" }, children: emoji }) })
244
- ),
245
- /* @__PURE__ */ jsx(
246
- Handle,
247
- {
248
- type: "source",
249
- position: Position.Right,
250
- id: "source",
251
- style: {
252
- position: "absolute",
253
- top: "50%",
254
- left: "50%",
255
- transform: "translate(-50%, -50%)",
256
- width: 1,
257
- height: 1,
258
- background: "transparent",
259
- border: "none",
260
- opacity: 0
261
- }
262
- }
263
- ),
264
- /* @__PURE__ */ jsx(
265
- Handle,
266
- {
267
- type: "target",
268
- position: Position.Left,
269
- id: "target",
270
- style: {
271
- position: "absolute",
272
- top: "50%",
273
- left: "50%",
274
- transform: "translate(-50%, -50%)",
275
- width: 1,
276
- height: 1,
277
- background: "transparent",
278
- border: "none",
279
- opacity: 0
280
- }
281
- }
282
- )
283
- ] }),
284
- /* @__PURE__ */ jsx(
285
- "div",
286
- {
287
- style: {
288
- marginTop: 2,
289
- fontSize: 9,
290
- maxWidth: 70,
291
- textAlign: "center",
292
- overflow: "hidden",
293
- textOverflow: "ellipsis",
294
- whiteSpace: "nowrap",
295
- color: "#374151",
296
- fontFamily: "system-ui, sans-serif"
297
- },
298
- title: fullValue,
299
- children: label
300
- }
301
- )
302
- ]
303
- }
782
+ }, [isRoot, backgroundColor, borderColor, selected, whitelisted]);
783
+ const labelStyle = useMemo(
784
+ () => ({
785
+ ...nodeStyles.label,
786
+ color: whitelisted ? "#9ca3af" : "#374151"
787
+ }),
788
+ [whitelisted]
304
789
  );
790
+ return /* @__PURE__ */ jsxs2("div", { className: "observable-node", style: nodeStyles.container, children: [
791
+ /* @__PURE__ */ jsxs2("div", { style: nodeStyles.shapeWrapper, children: [
792
+ /* @__PURE__ */ jsx2("div", { style: shapeStyle, children: /* @__PURE__ */ jsx2(
793
+ IconComponent,
794
+ {
795
+ size: isRoot ? ROOT_ICON_SIZE : ICON_SIZE,
796
+ color: borderColor
797
+ }
798
+ ) }),
799
+ /* @__PURE__ */ jsx2(Handle, { type: "source", position: Position.Right, style: nodeStyles.handle }),
800
+ /* @__PURE__ */ jsx2(Handle, { type: "target", position: Position.Left, style: nodeStyles.handle })
801
+ ] }),
802
+ /* @__PURE__ */ jsx2("div", { style: labelStyle, title: fullValue, children: label })
803
+ ] });
305
804
  }
306
805
  var ObservableNode = memo(ObservableNodeComponent);
307
806
 
308
807
  // src/components/FloatingEdge.tsx
309
- import { memo as memo2 } from "react";
310
- import { BaseEdge, getStraightPath } from "@xyflow/react";
311
- import { jsx as jsx2 } from "react/jsx-runtime";
808
+ import { memo as memo2, useMemo as useMemo2 } from "react";
809
+ import { BaseEdge, getBezierPath } from "@xyflow/react";
810
+ import { jsx as jsx3 } from "react/jsx-runtime";
811
+ function getControlOffset(sourceX, sourceY, targetX, targetY) {
812
+ const dx = targetX - sourceX;
813
+ const dy = targetY - sourceY;
814
+ const distance = Math.sqrt(dx * dx + dy * dy);
815
+ return Math.min(Math.max(distance * 0.15, 20), 60);
816
+ }
312
817
  function FloatingEdgeComponent({
313
818
  id,
314
819
  sourceX,
@@ -316,24 +821,35 @@ function FloatingEdgeComponent({
316
821
  targetX,
317
822
  targetY,
318
823
  style,
319
- markerEnd
824
+ markerEnd,
825
+ selected
320
826
  }) {
321
- const [edgePath] = getStraightPath({
827
+ const offset = useMemo2(
828
+ () => getControlOffset(sourceX, sourceY, targetX, targetY),
829
+ [sourceX, sourceY, targetX, targetY]
830
+ );
831
+ const [edgePath] = getBezierPath({
322
832
  sourceX,
323
833
  sourceY,
324
834
  targetX,
325
- targetY
835
+ targetY,
836
+ curvature: 0.15
326
837
  });
327
- return /* @__PURE__ */ jsx2(
838
+ const edgeStyle = useMemo2(
839
+ () => ({
840
+ strokeWidth: selected ? 2.5 : 1.5,
841
+ stroke: selected ? "#3b82f6" : "#94a3b8",
842
+ transition: "stroke 0.15s ease, stroke-width 0.15s ease",
843
+ ...style
844
+ }),
845
+ [selected, style]
846
+ );
847
+ return /* @__PURE__ */ jsx3(
328
848
  BaseEdge,
329
849
  {
330
850
  id,
331
851
  path: edgePath,
332
- style: {
333
- strokeWidth: 1.5,
334
- stroke: "#94a3b8",
335
- ...style
336
- },
852
+ style: edgeStyle,
337
853
  markerEnd
338
854
  }
339
855
  );
@@ -341,7 +857,7 @@ function FloatingEdgeComponent({
341
857
  var FloatingEdge = memo2(FloatingEdgeComponent);
342
858
 
343
859
  // src/hooks/useForceLayout.ts
344
- import { useEffect, useRef, useCallback, useMemo } from "react";
860
+ import { useEffect, useRef, useCallback, useMemo as useMemo3 } from "react";
345
861
  import {
346
862
  forceSimulation,
347
863
  forceLink,
@@ -356,31 +872,42 @@ import {
356
872
  useNodesInitialized,
357
873
  useStore
358
874
  } from "@xyflow/react";
359
- var nodeCountSelector = (state) => state.nodeLookup.size;
875
+ var nodeIdsSelector = (state) => {
876
+ const ids = Array.from(state.nodeLookup.keys()).sort();
877
+ return ids.join(",");
878
+ };
360
879
  function useForceLayout(config = {}, rootNodeId) {
361
880
  const { getNodes, getEdges, setNodes } = useReactFlow();
362
881
  const nodesInitialized = useNodesInitialized();
363
- const nodeCount = useStore(nodeCountSelector);
364
- const forceConfig = useMemo(
882
+ const nodeIds = useStore(nodeIdsSelector);
883
+ const forceConfig = useMemo3(
365
884
  () => ({ ...DEFAULT_FORCE_CONFIG, ...config }),
366
885
  [config]
367
886
  );
368
887
  const simulationRef = useRef(null);
369
- const draggingNodeRef = useRef(null);
888
+ const draggingRef = useRef({ nodeId: null, active: false });
889
+ const nodePositionsRef = useRef(
890
+ /* @__PURE__ */ new Map()
891
+ );
892
+ const rafRef = useRef(null);
370
893
  useEffect(() => {
371
- if (!nodesInitialized || nodeCount === 0) {
894
+ if (!nodesInitialized || !nodeIds) {
372
895
  return;
373
896
  }
374
897
  const nodes = getNodes();
375
898
  const edges = getEdges();
899
+ if (nodes.length === 0) {
900
+ return;
901
+ }
376
902
  const simNodes = nodes.map((node) => {
377
903
  const existingNode = simulationRef.current?.nodes().find((n) => n.id === node.id);
904
+ const x = existingNode?.x ?? nodePositionsRef.current.get(node.id)?.x ?? node.position.x ?? Math.random() * 500 - 250;
905
+ const y = existingNode?.y ?? nodePositionsRef.current.get(node.id)?.y ?? node.position.y ?? Math.random() * 500 - 250;
378
906
  return {
379
907
  id: node.id,
380
- // Use existing simulation position or node position
381
- x: existingNode?.x ?? node.position.x ?? Math.random() * 500 - 250,
382
- y: existingNode?.y ?? node.position.y ?? Math.random() * 500 - 250,
383
- // Preserve fixed positions for dragged nodes
908
+ x,
909
+ y,
910
+ // Preserve fixed positions for dragged nodes or root
384
911
  fx: existingNode?.fx ?? null,
385
912
  fy: existingNode?.fy ?? null
386
913
  };
@@ -401,30 +928,33 @@ function useForceLayout(config = {}, rootNodeId) {
401
928
  if (simulationRef.current) {
402
929
  simulationRef.current.stop();
403
930
  }
931
+ if (rafRef.current) {
932
+ cancelAnimationFrame(rafRef.current);
933
+ rafRef.current = null;
934
+ }
404
935
  const simulation = forceSimulation(simNodes).force(
405
936
  "link",
406
- forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.5)
937
+ forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.4)
407
938
  ).force(
408
939
  "charge",
409
940
  forceManyBody().strength(forceConfig.chargeStrength)
410
- ).force(
411
- "center",
412
- forceCenter(0, 0).strength(forceConfig.centerStrength)
413
- ).force(
414
- "collision",
415
- forceCollide(forceConfig.collisionRadius)
416
- ).force(
417
- "x",
418
- forceX(0).strength(0.01)
419
- ).force(
420
- "y",
421
- forceY(0).strength(0.01)
422
- ).alphaDecay(0.02).velocityDecay(0.4);
423
- simulation.on("tick", () => {
941
+ ).force("center", forceCenter(0, 0).strength(forceConfig.centerStrength)).force("collision", forceCollide(forceConfig.collisionRadius)).force("x", forceX(0).strength(8e-3)).force("y", forceY(0).strength(8e-3)).alphaDecay(0.02).velocityDecay(0.35);
942
+ const updateNodes = () => {
943
+ if (draggingRef.current.active) {
944
+ rafRef.current = requestAnimationFrame(updateNodes);
945
+ return;
946
+ }
947
+ const simNodes2 = simulation.nodes();
948
+ for (const simNode of simNodes2) {
949
+ nodePositionsRef.current.set(simNode.id, { x: simNode.x, y: simNode.y });
950
+ }
424
951
  setNodes(
425
952
  (currentNodes) => currentNodes.map((node) => {
426
- const simNode = simulation.nodes().find((n) => n.id === node.id);
953
+ const simNode = simNodes2.find((n) => n.id === node.id);
427
954
  if (!simNode) return node;
955
+ const dx = Math.abs(node.position.x - simNode.x);
956
+ const dy = Math.abs(node.position.y - simNode.y);
957
+ if (dx < 0.1 && dy < 0.1) return node;
428
958
  return {
429
959
  ...node,
430
960
  position: {
@@ -434,14 +964,26 @@ function useForceLayout(config = {}, rootNodeId) {
434
964
  };
435
965
  })
436
966
  );
967
+ if (simulation.alpha() > 1e-3) {
968
+ rafRef.current = requestAnimationFrame(updateNodes);
969
+ }
970
+ };
971
+ simulation.on("tick", () => {
972
+ if (rafRef.current === null && simulation.alpha() > 1e-3) {
973
+ rafRef.current = requestAnimationFrame(updateNodes);
974
+ }
437
975
  });
438
976
  simulationRef.current = simulation;
439
977
  return () => {
440
978
  simulation.stop();
979
+ if (rafRef.current) {
980
+ cancelAnimationFrame(rafRef.current);
981
+ rafRef.current = null;
982
+ }
441
983
  };
442
984
  }, [
443
985
  nodesInitialized,
444
- nodeCount,
986
+ nodeIds,
445
987
  getNodes,
446
988
  getEdges,
447
989
  setNodes,
@@ -452,33 +994,34 @@ function useForceLayout(config = {}, rootNodeId) {
452
994
  (_, node) => {
453
995
  const simulation = simulationRef.current;
454
996
  if (!simulation) return;
455
- draggingNodeRef.current = node.id;
456
- simulation.alphaTarget(0.3).restart();
457
- const simNode = simulation.nodes().find((n) => n.id === node.id);
458
- if (simNode) {
459
- simNode.fx = simNode.x;
460
- simNode.fy = simNode.y;
461
- }
462
- },
463
- []
464
- );
465
- const onNodeDrag = useCallback(
466
- (_, node) => {
467
- const simulation = simulationRef.current;
468
- if (!simulation) return;
997
+ draggingRef.current = { nodeId: node.id, active: true };
469
998
  const simNode = simulation.nodes().find((n) => n.id === node.id);
470
999
  if (simNode) {
471
1000
  simNode.fx = node.position.x;
472
1001
  simNode.fy = node.position.y;
473
1002
  }
1003
+ simulation.alphaTarget(0.1).restart();
474
1004
  },
475
1005
  []
476
1006
  );
1007
+ const onNodeDrag = useCallback((_, node) => {
1008
+ const simulation = simulationRef.current;
1009
+ if (!simulation) return;
1010
+ const simNode = simulation.nodes().find((n) => n.id === node.id);
1011
+ if (simNode) {
1012
+ simNode.fx = node.position.x;
1013
+ simNode.fy = node.position.y;
1014
+ nodePositionsRef.current.set(node.id, {
1015
+ x: node.position.x,
1016
+ y: node.position.y
1017
+ });
1018
+ }
1019
+ }, []);
477
1020
  const onNodeDragStop = useCallback(
478
1021
  (_, node) => {
479
1022
  const simulation = simulationRef.current;
1023
+ draggingRef.current = { nodeId: null, active: false };
480
1024
  if (!simulation) return;
481
- draggingNodeRef.current = null;
482
1025
  simulation.alphaTarget(0);
483
1026
  if (node.id !== rootNodeId) {
484
1027
  const simNode = simulation.nodes().find((n) => n.id === node.id);
@@ -487,6 +1030,11 @@ function useForceLayout(config = {}, rootNodeId) {
487
1030
  simNode.fy = null;
488
1031
  }
489
1032
  }
1033
+ setTimeout(() => {
1034
+ if (simulationRef.current && !draggingRef.current.active) {
1035
+ simulationRef.current.alpha(0.1).restart();
1036
+ }
1037
+ }, 50);
490
1038
  },
491
1039
  [rootNodeId]
492
1040
  );
@@ -512,7 +1060,7 @@ function useForceLayout(config = {}, rootNodeId) {
512
1060
  forceCollide(updates.collisionRadius)
513
1061
  );
514
1062
  }
515
- simulation.alpha(0.5).restart();
1063
+ simulation.alpha(0.3).restart();
516
1064
  },
517
1065
  []
518
1066
  );
@@ -531,32 +1079,34 @@ function useForceLayout(config = {}, rootNodeId) {
531
1079
  }
532
1080
 
533
1081
  // src/components/ObservablesGraph.tsx
534
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1082
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
535
1083
  var nodeTypes = {
536
1084
  observable: ObservableNode
537
1085
  };
538
1086
  var edgeTypes = {
539
1087
  floating: FloatingEdge
540
1088
  };
1089
+ var defaultEdgeOptions = {
1090
+ type: "floating",
1091
+ style: { stroke: "#94a3b8", strokeWidth: 1.5 }
1092
+ };
541
1093
  function createObservableNodes(investigation, rootObservableIds) {
542
1094
  const graph = getObservableGraph(investigation);
543
1095
  return graph.nodes.map((graphNode, index) => {
544
1096
  const isRoot = rootObservableIds.has(graphNode.id);
545
- const shape = getObservableShape(graphNode.type, isRoot);
546
1097
  const nodeData = {
547
- label: truncateLabel(graphNode.value, 18),
1098
+ label: truncateLabel(graphNode.value, 16),
548
1099
  fullValue: graphNode.value,
549
1100
  observableType: graphNode.type,
550
1101
  level: graphNode.level,
551
1102
  score: graphNode.score,
552
- emoji: getObservableEmoji(graphNode.type),
553
- shape,
1103
+ shape: "circle",
554
1104
  isRoot,
555
1105
  whitelisted: graphNode.whitelisted,
556
1106
  internal: graphNode.internal
557
1107
  };
558
1108
  const angle = index / graph.nodes.length * 2 * Math.PI;
559
- const radius = isRoot ? 0 : 150;
1109
+ const radius = isRoot ? 0 : 180;
560
1110
  return {
561
1111
  id: graphNode.id,
562
1112
  type: "observable",
@@ -564,7 +1114,10 @@ function createObservableNodes(investigation, rootObservableIds) {
564
1114
  x: Math.cos(angle) * radius,
565
1115
  y: Math.sin(angle) * radius
566
1116
  },
567
- data: nodeData
1117
+ data: nodeData,
1118
+ // Enable selection for better UX
1119
+ selectable: true,
1120
+ draggable: true
568
1121
  };
569
1122
  });
570
1123
  }
@@ -581,100 +1134,160 @@ function createObservableEdges(investigation) {
581
1134
  target: graphEdge.target,
582
1135
  type: "floating",
583
1136
  data: edgeData,
1137
+ // Animated edges for a modern feel
1138
+ animated: false,
584
1139
  style: { stroke: "#94a3b8", strokeWidth: 1.5 }
585
1140
  };
586
1141
  });
587
1142
  }
588
1143
  var ForceControls = ({ config, onChange, onRestart }) => {
589
- return /* @__PURE__ */ jsxs2(
590
- "div",
591
- {
592
- style: {
593
- position: "absolute",
594
- top: 10,
595
- right: 10,
596
- background: "white",
597
- padding: 12,
598
- borderRadius: 8,
599
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
600
- fontSize: 12,
601
- fontFamily: "system-ui, sans-serif",
602
- zIndex: 10,
603
- minWidth: 160
604
- },
605
- children: [
606
- /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Force Layout" }),
607
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
608
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
609
- "Repulsion: ",
610
- config.chargeStrength
611
- ] }),
612
- /* @__PURE__ */ jsx3(
613
- "input",
614
- {
615
- type: "range",
616
- min: "-500",
617
- max: "-50",
618
- value: config.chargeStrength,
619
- onChange: (e) => onChange({ chargeStrength: Number(e.target.value) }),
620
- style: { width: "100%" }
621
- }
622
- )
1144
+ const [isExpanded, setIsExpanded] = useState(false);
1145
+ const panelStyle = {
1146
+ background: "rgba(255, 255, 255, 0.95)",
1147
+ backdropFilter: "blur(8px)",
1148
+ padding: isExpanded ? 14 : 10,
1149
+ borderRadius: 12,
1150
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
1151
+ fontSize: 12,
1152
+ fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1153
+ minWidth: isExpanded ? 180 : "auto",
1154
+ transition: "all 0.2s ease",
1155
+ border: "1px solid rgba(0,0,0,0.06)"
1156
+ };
1157
+ const headerStyle = {
1158
+ display: "flex",
1159
+ alignItems: "center",
1160
+ justifyContent: "space-between",
1161
+ gap: 8,
1162
+ cursor: "pointer"
1163
+ };
1164
+ const titleStyle = {
1165
+ fontWeight: 600,
1166
+ color: "#1f2937",
1167
+ fontSize: 12,
1168
+ letterSpacing: "-0.01em"
1169
+ };
1170
+ const toggleStyle = {
1171
+ background: "none",
1172
+ border: "none",
1173
+ cursor: "pointer",
1174
+ padding: 4,
1175
+ borderRadius: 4,
1176
+ color: "#6b7280",
1177
+ display: "flex",
1178
+ alignItems: "center",
1179
+ transition: "transform 0.2s ease",
1180
+ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)"
1181
+ };
1182
+ const sliderContainerStyle = {
1183
+ marginTop: 12,
1184
+ display: isExpanded ? "block" : "none"
1185
+ };
1186
+ const sliderLabelStyle = {
1187
+ display: "flex",
1188
+ justifyContent: "space-between",
1189
+ marginBottom: 4,
1190
+ color: "#4b5563",
1191
+ fontSize: 11
1192
+ };
1193
+ const sliderStyle = {
1194
+ width: "100%",
1195
+ height: 4,
1196
+ appearance: "none",
1197
+ background: "#e5e7eb",
1198
+ borderRadius: 2,
1199
+ outline: "none",
1200
+ cursor: "pointer"
1201
+ };
1202
+ const buttonStyle = {
1203
+ width: "100%",
1204
+ padding: "8px 12px",
1205
+ border: "none",
1206
+ borderRadius: 8,
1207
+ background: "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)",
1208
+ color: "white",
1209
+ cursor: "pointer",
1210
+ fontSize: 12,
1211
+ fontWeight: 500,
1212
+ marginTop: 12,
1213
+ transition: "transform 0.1s ease, box-shadow 0.1s ease",
1214
+ boxShadow: "0 2px 4px rgba(59, 130, 246, 0.3)"
1215
+ };
1216
+ return /* @__PURE__ */ jsxs3("div", { style: panelStyle, children: [
1217
+ /* @__PURE__ */ jsxs3("div", { style: headerStyle, onClick: () => setIsExpanded(!isExpanded), children: [
1218
+ /* @__PURE__ */ jsx4("span", { style: titleStyle, children: "\u26A1 Force Layout" }),
1219
+ /* @__PURE__ */ jsx4("button", { style: toggleStyle, children: /* @__PURE__ */ jsx4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx4("polyline", { points: "6 9 12 15 18 9" }) }) })
1220
+ ] }),
1221
+ /* @__PURE__ */ jsxs3("div", { style: sliderContainerStyle, children: [
1222
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 10 }, children: [
1223
+ /* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
1224
+ /* @__PURE__ */ jsx4("span", { children: "Repulsion" }),
1225
+ /* @__PURE__ */ jsx4("span", { children: config.chargeStrength })
623
1226
  ] }),
624
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
625
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
626
- "Link Distance: ",
627
- config.linkDistance
628
- ] }),
629
- /* @__PURE__ */ jsx3(
630
- "input",
631
- {
632
- type: "range",
633
- min: "30",
634
- max: "200",
635
- value: config.linkDistance,
636
- onChange: (e) => onChange({ linkDistance: Number(e.target.value) }),
637
- style: { width: "100%" }
638
- }
639
- )
1227
+ /* @__PURE__ */ jsx4(
1228
+ "input",
1229
+ {
1230
+ type: "range",
1231
+ min: "-500",
1232
+ max: "-50",
1233
+ value: config.chargeStrength,
1234
+ onChange: (e) => onChange({ chargeStrength: Number(e.target.value) }),
1235
+ style: sliderStyle
1236
+ }
1237
+ )
1238
+ ] }),
1239
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 10 }, children: [
1240
+ /* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
1241
+ /* @__PURE__ */ jsx4("span", { children: "Link Distance" }),
1242
+ /* @__PURE__ */ jsx4("span", { children: config.linkDistance })
640
1243
  ] }),
641
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
642
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
643
- "Collision: ",
644
- config.collisionRadius
645
- ] }),
646
- /* @__PURE__ */ jsx3(
647
- "input",
648
- {
649
- type: "range",
650
- min: "10",
651
- max: "80",
652
- value: config.collisionRadius,
653
- onChange: (e) => onChange({ collisionRadius: Number(e.target.value) }),
654
- style: { width: "100%" }
655
- }
656
- )
1244
+ /* @__PURE__ */ jsx4(
1245
+ "input",
1246
+ {
1247
+ type: "range",
1248
+ min: "30",
1249
+ max: "200",
1250
+ value: config.linkDistance,
1251
+ onChange: (e) => onChange({ linkDistance: Number(e.target.value) }),
1252
+ style: sliderStyle
1253
+ }
1254
+ )
1255
+ ] }),
1256
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 6 }, children: [
1257
+ /* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
1258
+ /* @__PURE__ */ jsx4("span", { children: "Collision" }),
1259
+ /* @__PURE__ */ jsx4("span", { children: config.collisionRadius })
657
1260
  ] }),
658
- /* @__PURE__ */ jsx3(
659
- "button",
1261
+ /* @__PURE__ */ jsx4(
1262
+ "input",
660
1263
  {
661
- onClick: onRestart,
662
- style: {
663
- width: "100%",
664
- padding: "6px 12px",
665
- border: "none",
666
- borderRadius: 4,
667
- background: "#3b82f6",
668
- color: "white",
669
- cursor: "pointer",
670
- fontSize: 12
671
- },
672
- children: "Restart Simulation"
1264
+ type: "range",
1265
+ min: "10",
1266
+ max: "80",
1267
+ value: config.collisionRadius,
1268
+ onChange: (e) => onChange({ collisionRadius: Number(e.target.value) }),
1269
+ style: sliderStyle
673
1270
  }
674
1271
  )
675
- ]
676
- }
677
- );
1272
+ ] }),
1273
+ /* @__PURE__ */ jsx4(
1274
+ "button",
1275
+ {
1276
+ onClick: onRestart,
1277
+ style: buttonStyle,
1278
+ onMouseEnter: (e) => {
1279
+ e.currentTarget.style.transform = "translateY(-1px)";
1280
+ e.currentTarget.style.boxShadow = "0 4px 8px rgba(59, 130, 246, 0.4)";
1281
+ },
1282
+ onMouseLeave: (e) => {
1283
+ e.currentTarget.style.transform = "translateY(0)";
1284
+ e.currentTarget.style.boxShadow = "0 2px 4px rgba(59, 130, 246, 0.3)";
1285
+ },
1286
+ children: "Restart Simulation"
1287
+ }
1288
+ )
1289
+ ] })
1290
+ ] });
678
1291
  };
679
1292
  var ObservablesGraphInner = ({
680
1293
  initialNodes,
@@ -692,11 +1305,13 @@ var ObservablesGraphInner = ({
692
1305
  ...DEFAULT_FORCE_CONFIG,
693
1306
  ...initialForceConfig
694
1307
  });
1308
+ const initialFitDone = useRef2(false);
695
1309
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
696
1310
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
697
1311
  React3.useEffect(() => {
698
1312
  setNodes(initialNodes);
699
1313
  setEdges(initialEdges);
1314
+ initialFitDone.current = false;
700
1315
  }, [initialNodes, initialEdges, setNodes, setEdges]);
701
1316
  const {
702
1317
  onNodeDragStart,
@@ -728,71 +1343,114 @@ var ObservablesGraphInner = ({
728
1343
  const data = node.data;
729
1344
  return getLevelColor(data.level);
730
1345
  }, []);
731
- return /* @__PURE__ */ jsxs2(
732
- "div",
1346
+ const containerStyle = useMemo4(
1347
+ () => ({
1348
+ width,
1349
+ height,
1350
+ position: "relative",
1351
+ background: "linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%)"
1352
+ }),
1353
+ [width, height]
1354
+ );
1355
+ return /* @__PURE__ */ jsx4("div", { className, style: containerStyle, children: /* @__PURE__ */ jsxs3(
1356
+ ReactFlow,
733
1357
  {
734
- className,
735
- style: {
736
- width,
737
- height,
738
- position: "relative"
739
- },
1358
+ nodes,
1359
+ edges,
1360
+ onNodesChange,
1361
+ onEdgesChange,
1362
+ onNodeClick: handleNodeClick,
1363
+ onNodeDoubleClick: handleNodeDoubleClick,
1364
+ onNodeDragStart,
1365
+ onNodeDrag,
1366
+ onNodeDragStop,
1367
+ nodeTypes,
1368
+ edgeTypes,
1369
+ defaultEdgeOptions,
1370
+ connectionMode: ConnectionMode.Loose,
1371
+ fitView: true,
1372
+ fitViewOptions: { padding: 0.4, maxZoom: 1.5 },
1373
+ minZoom: 0.1,
1374
+ maxZoom: 2.5,
1375
+ proOptions: { hideAttribution: true },
1376
+ nodesDraggable: true,
1377
+ nodesConnectable: false,
1378
+ elementsSelectable: true,
1379
+ selectNodesOnDrag: false,
1380
+ panOnDrag: true,
1381
+ zoomOnScroll: true,
1382
+ zoomOnPinch: true,
1383
+ panOnScroll: false,
740
1384
  children: [
741
- /* @__PURE__ */ jsxs2(
742
- ReactFlow,
1385
+ /* @__PURE__ */ jsx4(
1386
+ Background,
743
1387
  {
744
- nodes,
745
- edges,
746
- onNodesChange,
747
- onEdgesChange,
748
- onNodeClick: handleNodeClick,
749
- onNodeDoubleClick: handleNodeDoubleClick,
750
- onNodeDragStart,
751
- onNodeDrag,
752
- onNodeDragStop,
753
- nodeTypes,
754
- edgeTypes,
755
- connectionMode: ConnectionMode.Loose,
756
- fitView: true,
757
- fitViewOptions: { padding: 0.3 },
758
- minZoom: 0.1,
759
- maxZoom: 2,
760
- proOptions: { hideAttribution: true },
761
- children: [
762
- /* @__PURE__ */ jsx3(Background, {}),
763
- /* @__PURE__ */ jsx3(Controls, {}),
764
- /* @__PURE__ */ jsx3(MiniMap, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
765
- ]
1388
+ variant: BackgroundVariant.Dots,
1389
+ gap: 24,
1390
+ size: 1,
1391
+ color: "#d1d5db"
766
1392
  }
767
1393
  ),
768
- showControls && /* @__PURE__ */ jsx3(
1394
+ /* @__PURE__ */ jsx4(
1395
+ Controls,
1396
+ {
1397
+ showInteractive: false,
1398
+ style: {
1399
+ borderRadius: 10,
1400
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
1401
+ border: "1px solid rgba(0,0,0,0.06)"
1402
+ }
1403
+ }
1404
+ ),
1405
+ /* @__PURE__ */ jsx4(
1406
+ MiniMap,
1407
+ {
1408
+ nodeColor: miniMapNodeColor,
1409
+ zoomable: true,
1410
+ pannable: true,
1411
+ style: {
1412
+ borderRadius: 10,
1413
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
1414
+ border: "1px solid rgba(0,0,0,0.06)",
1415
+ background: "rgba(255,255,255,0.9)"
1416
+ },
1417
+ maskColor: "rgba(0,0,0,0.08)"
1418
+ }
1419
+ ),
1420
+ showControls && /* @__PURE__ */ jsx4(Panel, { position: "top-right", children: /* @__PURE__ */ jsx4(
769
1421
  ForceControls,
770
1422
  {
771
1423
  config: forceConfig,
772
1424
  onChange: handleConfigChange,
773
1425
  onRestart: restartSimulation
774
1426
  }
775
- )
1427
+ ) })
776
1428
  ]
777
1429
  }
778
- );
1430
+ ) });
779
1431
  };
780
1432
  var ObservablesGraph = (props) => {
781
1433
  const { investigation } = props;
782
- const rootObservables = useMemo2(() => {
783
- const roots = findRootObservables(investigation);
784
- return new Set(roots.map((r) => r.key));
785
- }, [investigation]);
786
- const primaryRootId = useMemo2(() => {
787
- const roots = findRootObservables(investigation);
788
- return roots.length > 0 ? roots[0].key : void 0;
1434
+ const { rootKeys, primaryRootId } = useMemo4(() => {
1435
+ const rootType = investigation.data_extraction.root_type;
1436
+ if (!rootType) {
1437
+ return { rootKeys: /* @__PURE__ */ new Set(), primaryRootId: void 0 };
1438
+ }
1439
+ const normalizedRootType = rootType.toLowerCase().trim();
1440
+ const rootsByType = Object.values(investigation.observables).filter(
1441
+ (obs) => obs.type.toLowerCase() === normalizedRootType
1442
+ );
1443
+ return {
1444
+ rootKeys: new Set(rootsByType.map((obs) => obs.key)),
1445
+ primaryRootId: rootsByType[0]?.key
1446
+ };
789
1447
  }, [investigation]);
790
- const { initialNodes, initialEdges } = useMemo2(() => {
791
- const nodes = createObservableNodes(investigation, rootObservables);
1448
+ const { initialNodes, initialEdges } = useMemo4(() => {
1449
+ const nodes = createObservableNodes(investigation, rootKeys);
792
1450
  const edges = createObservableEdges(investigation);
793
1451
  return { initialNodes: nodes, initialEdges: edges };
794
- }, [investigation, rootObservables]);
795
- return /* @__PURE__ */ jsx3(ReactFlowProvider, { children: /* @__PURE__ */ jsx3(
1452
+ }, [investigation, rootKeys]);
1453
+ return /* @__PURE__ */ jsx4(ReactFlowProvider, { children: /* @__PURE__ */ jsx4(
796
1454
  ObservablesGraphInner,
797
1455
  {
798
1456
  ...props,
@@ -804,160 +1462,142 @@ var ObservablesGraph = (props) => {
804
1462
  };
805
1463
 
806
1464
  // src/components/InvestigationGraph.tsx
807
- import React5, { useMemo as useMemo4, useCallback as useCallback3 } from "react";
1465
+ import React5, { useMemo as useMemo7, useCallback as useCallback3 } from "react";
808
1466
  import {
809
1467
  ReactFlow as ReactFlow2,
810
1468
  Background as Background2,
811
1469
  Controls as Controls2,
812
1470
  MiniMap as MiniMap2,
813
1471
  useNodesState as useNodesState2,
814
- useEdgesState as useEdgesState2
1472
+ useEdgesState as useEdgesState2,
1473
+ BackgroundVariant as BackgroundVariant2,
1474
+ MarkerType
815
1475
  } from "@xyflow/react";
816
1476
  import "@xyflow/react/dist/style.css";
817
- import { findRootObservables as findRootObservables2 } from "@cyvest/cyvest-js";
818
1477
 
819
1478
  // src/components/InvestigationNode.tsx
820
- import { memo as memo3 } from "react";
1479
+ import { memo as memo3, useMemo as useMemo5 } from "react";
821
1480
  import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
822
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
823
- function InvestigationNodeComponent({
824
- data,
825
- selected
826
- }) {
1481
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1482
+ var NODE_CONFIG = {
1483
+ root: {
1484
+ minWidth: 140,
1485
+ padding: "10px 18px",
1486
+ borderRadius: 20,
1487
+ fontWeight: 600,
1488
+ fontSize: 13,
1489
+ iconSize: 18,
1490
+ showIcon: true,
1491
+ alignCenter: true
1492
+ },
1493
+ check: {
1494
+ minWidth: 140,
1495
+ padding: "8px 14px",
1496
+ borderRadius: 8,
1497
+ fontWeight: 500,
1498
+ fontSize: 12,
1499
+ iconSize: 14,
1500
+ showIcon: false,
1501
+ // No icon for checks
1502
+ alignCenter: false
1503
+ // Left-aligned
1504
+ },
1505
+ container: {
1506
+ minWidth: 120,
1507
+ padding: "8px 14px",
1508
+ borderRadius: 16,
1509
+ fontWeight: 500,
1510
+ fontSize: 12,
1511
+ iconSize: 16,
1512
+ showIcon: true,
1513
+ alignCenter: true
1514
+ }
1515
+ };
1516
+ function InvestigationNodeComponent({ data, selected }) {
827
1517
  const nodeData = data;
828
- const {
829
- label,
830
- emoji,
831
- nodeType,
832
- level,
833
- description
834
- } = nodeData;
1518
+ const { label, nodeType, level, description } = nodeData;
835
1519
  const borderColor = getLevelColor(level);
836
1520
  const backgroundColor = getLevelBackgroundColor(level);
837
- const getNodeStyle = () => {
838
- switch (nodeType) {
839
- case "root":
840
- return {
841
- minWidth: 120,
842
- padding: "8px 16px",
843
- borderRadius: 8,
844
- fontWeight: 600
845
- };
846
- case "check":
847
- return {
848
- minWidth: 100,
849
- padding: "6px 12px",
850
- borderRadius: 4,
851
- fontWeight: 400
852
- };
853
- case "container":
854
- return {
855
- minWidth: 100,
856
- padding: "6px 12px",
857
- borderRadius: 12,
858
- fontWeight: 400
859
- };
860
- default:
861
- return {
862
- minWidth: 80,
863
- padding: "6px 12px",
864
- borderRadius: 4,
865
- fontWeight: 400
866
- };
867
- }
868
- };
869
- const style = getNodeStyle();
870
- return /* @__PURE__ */ jsxs3(
871
- "div",
872
- {
873
- className: "investigation-node",
874
- style: {
875
- ...style,
876
- display: "flex",
877
- flexDirection: "column",
878
- alignItems: "center",
879
- backgroundColor,
880
- border: `${selected ? 3 : 2}px solid ${borderColor}`,
881
- cursor: "pointer",
882
- fontFamily: "system-ui, sans-serif"
883
- },
884
- children: [
885
- /* @__PURE__ */ jsxs3(
886
- "div",
887
- {
888
- style: {
889
- display: "flex",
890
- alignItems: "center",
891
- gap: 6
892
- },
893
- children: [
894
- /* @__PURE__ */ jsx4("span", { style: { fontSize: 14 }, children: emoji }),
895
- /* @__PURE__ */ jsx4(
896
- "span",
897
- {
898
- style: {
899
- fontSize: 12,
900
- fontWeight: style.fontWeight,
901
- maxWidth: 150,
902
- overflow: "hidden",
903
- textOverflow: "ellipsis",
904
- whiteSpace: "nowrap"
905
- },
906
- title: label,
907
- children: label
908
- }
909
- )
910
- ]
911
- }
912
- ),
913
- description && /* @__PURE__ */ jsx4(
914
- "div",
915
- {
916
- style: {
917
- marginTop: 4,
918
- fontSize: 10,
919
- color: "#6b7280",
920
- maxWidth: 140,
921
- overflow: "hidden",
922
- textOverflow: "ellipsis",
923
- whiteSpace: "nowrap"
924
- },
925
- title: description,
926
- children: description
927
- }
928
- ),
929
- /* @__PURE__ */ jsx4(
930
- Handle2,
931
- {
932
- type: "target",
933
- position: Position2.Left,
934
- style: {
935
- width: 8,
936
- height: 8,
937
- background: borderColor
938
- }
939
- }
940
- ),
941
- /* @__PURE__ */ jsx4(
942
- Handle2,
943
- {
944
- type: "source",
945
- position: Position2.Right,
946
- style: {
947
- width: 8,
948
- height: 8,
949
- background: borderColor
950
- }
951
- }
952
- )
953
- ]
954
- }
1521
+ const config = NODE_CONFIG[nodeType] || NODE_CONFIG.check;
1522
+ const IconComponent = useMemo5(
1523
+ () => getInvestigationIcon(nodeType),
1524
+ [nodeType]
955
1525
  );
1526
+ const nodeStyle = useMemo5(
1527
+ () => ({
1528
+ minWidth: config.minWidth,
1529
+ padding: config.padding,
1530
+ borderRadius: config.borderRadius,
1531
+ display: "flex",
1532
+ flexDirection: "column",
1533
+ alignItems: config.alignCenter ? "center" : "flex-start",
1534
+ backgroundColor,
1535
+ border: `2px solid ${borderColor}`,
1536
+ boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 8px rgba(0,0,0,0.08)",
1537
+ cursor: "pointer",
1538
+ fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1539
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
1540
+ }),
1541
+ [config, backgroundColor, borderColor, selected]
1542
+ );
1543
+ const headerStyle = useMemo5(
1544
+ () => ({
1545
+ display: "flex",
1546
+ alignItems: "center",
1547
+ gap: 8,
1548
+ width: config.alignCenter ? "auto" : "100%"
1549
+ }),
1550
+ [config.alignCenter]
1551
+ );
1552
+ const labelStyle = useMemo5(
1553
+ () => ({
1554
+ fontSize: config.fontSize,
1555
+ fontWeight: config.fontWeight,
1556
+ maxWidth: 180,
1557
+ overflow: "hidden",
1558
+ textOverflow: "ellipsis",
1559
+ whiteSpace: "nowrap",
1560
+ color: "#1f2937",
1561
+ letterSpacing: "-0.01em"
1562
+ }),
1563
+ [config]
1564
+ );
1565
+ const descriptionStyle = useMemo5(
1566
+ () => ({
1567
+ marginTop: 4,
1568
+ fontSize: 10,
1569
+ color: "#6b7280",
1570
+ maxWidth: 170,
1571
+ overflow: "hidden",
1572
+ textOverflow: "ellipsis",
1573
+ whiteSpace: "nowrap",
1574
+ lineHeight: 1.3,
1575
+ width: "100%",
1576
+ textAlign: config.alignCenter ? "center" : "left"
1577
+ }),
1578
+ [config.alignCenter]
1579
+ );
1580
+ const handleStyle = {
1581
+ width: 1,
1582
+ height: 1,
1583
+ background: "transparent",
1584
+ border: "none",
1585
+ opacity: 0
1586
+ };
1587
+ return /* @__PURE__ */ jsxs4("div", { className: "investigation-node", style: nodeStyle, children: [
1588
+ /* @__PURE__ */ jsxs4("div", { style: headerStyle, children: [
1589
+ config.showIcon && /* @__PURE__ */ jsx5(IconComponent, { size: config.iconSize, color: borderColor }),
1590
+ /* @__PURE__ */ jsx5("span", { style: labelStyle, title: label, children: label })
1591
+ ] }),
1592
+ description && /* @__PURE__ */ jsx5("div", { style: descriptionStyle, title: description, children: description }),
1593
+ /* @__PURE__ */ jsx5(Handle2, { type: "target", position: Position2.Left, style: handleStyle }),
1594
+ /* @__PURE__ */ jsx5(Handle2, { type: "source", position: Position2.Right, style: handleStyle })
1595
+ ] });
956
1596
  }
957
1597
  var InvestigationNode = memo3(InvestigationNodeComponent);
958
1598
 
959
1599
  // src/hooks/useDagreLayout.ts
960
- import { useMemo as useMemo3 } from "react";
1600
+ import { useMemo as useMemo6 } from "react";
961
1601
  import Dagre from "@dagrejs/dagre";
962
1602
  var DEFAULT_OPTIONS = {
963
1603
  direction: "LR",
@@ -1003,10 +1643,23 @@ function computeDagreLayout(nodes, edges, options = {}) {
1003
1643
  }
1004
1644
 
1005
1645
  // src/components/InvestigationGraph.tsx
1006
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1646
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1007
1647
  var nodeTypes2 = {
1008
1648
  investigation: InvestigationNode
1009
1649
  };
1650
+ var defaultEdgeOptions2 = {
1651
+ type: "smoothstep",
1652
+ style: {
1653
+ stroke: "#94a3b8",
1654
+ strokeWidth: 1.5
1655
+ },
1656
+ markerEnd: {
1657
+ type: MarkerType.ArrowClosed,
1658
+ width: 16,
1659
+ height: 16,
1660
+ color: "#94a3b8"
1661
+ }
1662
+ };
1010
1663
  function flattenContainers(containers) {
1011
1664
  const result = [];
1012
1665
  for (const container of Object.values(containers)) {
@@ -1020,24 +1673,36 @@ function flattenContainers(containers) {
1020
1673
  function createInvestigationGraph(investigation) {
1021
1674
  const nodes = [];
1022
1675
  const edges = [];
1023
- const roots = findRootObservables2(investigation);
1024
- const primaryRoot = roots.length > 0 ? roots[0] : null;
1025
- const rootKey = primaryRoot?.key ?? "investigation-root";
1026
- const rootValue = primaryRoot?.value ?? "Investigation";
1676
+ const rootType = investigation.data_extraction.root_type;
1677
+ const normalizedRootType = rootType?.toLowerCase().trim();
1678
+ const rootsByType = normalizedRootType ? Object.values(investigation.observables).filter(
1679
+ (obs) => obs.type.toLowerCase() === normalizedRootType
1680
+ ) : [];
1681
+ const primaryRoot = rootsByType[0] ?? null;
1682
+ const rootKey = primaryRoot?.key ?? investigation.investigation_id;
1683
+ const rootValue = primaryRoot?.value ?? investigation.investigation_name ?? investigation.investigation_id;
1027
1684
  const rootLevel = primaryRoot?.level ?? investigation.level;
1028
1685
  const rootNodeData = {
1029
1686
  label: truncateLabel(rootValue, 24),
1030
1687
  nodeType: "root",
1031
1688
  level: rootLevel,
1032
- score: primaryRoot?.score ?? investigation.score,
1033
- emoji: getInvestigationNodeEmoji("root")
1689
+ score: primaryRoot?.score ?? investigation.score
1034
1690
  };
1035
1691
  nodes.push({
1036
1692
  id: rootKey,
1037
1693
  type: "investigation",
1038
1694
  position: { x: 0, y: 0 },
1039
- data: rootNodeData
1695
+ data: rootNodeData,
1696
+ selectable: true,
1697
+ draggable: true
1040
1698
  });
1699
+ const allContainers = flattenContainers(investigation.containers);
1700
+ const checksInContainers = /* @__PURE__ */ new Set();
1701
+ for (const container of allContainers) {
1702
+ for (const checkKey of container.checks) {
1703
+ checksInContainers.add(checkKey);
1704
+ }
1705
+ }
1041
1706
  const allChecks = [];
1042
1707
  for (const checksForKey of Object.values(investigation.checks)) {
1043
1708
  allChecks.push(...checksForKey);
@@ -1051,43 +1716,51 @@ function createInvestigationGraph(investigation) {
1051
1716
  nodeType: "check",
1052
1717
  level: check.level,
1053
1718
  score: check.score,
1054
- description: truncateLabel(check.description, 30),
1055
- emoji: getInvestigationNodeEmoji("check")
1719
+ description: truncateLabel(check.description, 30)
1056
1720
  };
1057
1721
  nodes.push({
1058
1722
  id: `check-${check.key}`,
1059
1723
  type: "investigation",
1060
1724
  position: { x: 0, y: 0 },
1061
- data: checkNodeData
1062
- });
1063
- edges.push({
1064
- id: `edge-root-${check.key}`,
1065
- source: rootKey,
1066
- target: `check-${check.key}`,
1067
- type: "default"
1725
+ data: checkNodeData,
1726
+ selectable: true,
1727
+ draggable: true
1068
1728
  });
1729
+ if (!checksInContainers.has(check.key)) {
1730
+ edges.push({
1731
+ id: `edge-root-${check.key}`,
1732
+ source: rootKey,
1733
+ target: `check-${check.key}`,
1734
+ type: "smoothstep",
1735
+ animated: false
1736
+ });
1737
+ }
1069
1738
  }
1070
- const allContainers = flattenContainers(investigation.containers);
1071
1739
  for (const container of allContainers) {
1072
1740
  const containerNodeData = {
1073
- label: truncateLabel(container.path.split("/").pop() ?? container.path, 20),
1741
+ label: truncateLabel(
1742
+ container.path.split("/").pop() ?? container.path,
1743
+ 20
1744
+ ),
1074
1745
  nodeType: "container",
1075
1746
  level: container.aggregated_level,
1076
1747
  score: container.aggregated_score,
1077
- path: container.path,
1078
- emoji: getInvestigationNodeEmoji("container")
1748
+ path: container.path
1079
1749
  };
1080
1750
  nodes.push({
1081
1751
  id: `container-${container.key}`,
1082
1752
  type: "investigation",
1083
1753
  position: { x: 0, y: 0 },
1084
- data: containerNodeData
1754
+ data: containerNodeData,
1755
+ selectable: true,
1756
+ draggable: true
1085
1757
  });
1086
1758
  edges.push({
1087
1759
  id: `edge-root-container-${container.key}`,
1088
1760
  source: rootKey,
1089
1761
  target: `container-${container.key}`,
1090
- type: "default"
1762
+ type: "smoothstep",
1763
+ animated: false
1091
1764
  });
1092
1765
  for (const checkKey of container.checks) {
1093
1766
  if (seenCheckIds.has(checkKey)) {
@@ -1095,8 +1768,8 @@ function createInvestigationGraph(investigation) {
1095
1768
  id: `edge-container-check-${container.key}-${checkKey}`,
1096
1769
  source: `container-${container.key}`,
1097
1770
  target: `check-${checkKey}`,
1098
- type: "default",
1099
- style: { strokeDasharray: "5,5" }
1771
+ type: "smoothstep",
1772
+ animated: false
1100
1773
  });
1101
1774
  }
1102
1775
  }
@@ -1110,15 +1783,15 @@ var InvestigationGraph = ({
1110
1783
  onNodeClick,
1111
1784
  className
1112
1785
  }) => {
1113
- const { initialNodes, initialEdges } = useMemo4(() => {
1786
+ const { initialNodes, initialEdges } = useMemo7(() => {
1114
1787
  const { nodes: nodes2, edges: edges2 } = createInvestigationGraph(investigation);
1115
1788
  return { initialNodes: nodes2, initialEdges: edges2 };
1116
1789
  }, [investigation]);
1117
- const { nodes: layoutNodes, edges: layoutEdges } = useMemo4(() => {
1790
+ const { nodes: layoutNodes, edges: layoutEdges } = useMemo7(() => {
1118
1791
  return computeDagreLayout(initialNodes, initialEdges, {
1119
1792
  direction: "LR",
1120
- nodeSpacing: 30,
1121
- rankSpacing: 120
1793
+ nodeSpacing: 40,
1794
+ rankSpacing: 140
1122
1795
  });
1123
1796
  }, [initialNodes, initialEdges]);
1124
1797
  const [nodes, setNodes, onNodesChange] = useNodesState2(layoutNodes);
@@ -1138,97 +1811,200 @@ var InvestigationGraph = ({
1138
1811
  const data = node.data;
1139
1812
  return getLevelColor(data.level);
1140
1813
  }, []);
1141
- return /* @__PURE__ */ jsx5(
1142
- "div",
1143
- {
1144
- className,
1145
- style: {
1146
- width,
1147
- height,
1148
- position: "relative"
1149
- },
1150
- children: /* @__PURE__ */ jsxs4(
1151
- ReactFlow2,
1152
- {
1153
- nodes,
1154
- edges,
1155
- onNodesChange,
1156
- onEdgesChange,
1157
- onNodeClick: handleNodeClick,
1158
- nodeTypes: nodeTypes2,
1159
- fitView: true,
1160
- fitViewOptions: { padding: 0.2 },
1161
- minZoom: 0.1,
1162
- maxZoom: 2,
1163
- proOptions: { hideAttribution: true },
1164
- children: [
1165
- /* @__PURE__ */ jsx5(Background2, {}),
1166
- /* @__PURE__ */ jsx5(Controls2, {}),
1167
- /* @__PURE__ */ jsx5(MiniMap2, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
1168
- ]
1169
- }
1170
- )
1171
- }
1814
+ const containerStyle = useMemo7(
1815
+ () => ({
1816
+ width,
1817
+ height,
1818
+ position: "relative",
1819
+ background: "linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%)"
1820
+ }),
1821
+ [width, height]
1172
1822
  );
1173
- };
1174
-
1175
- // src/components/CyvestGraph.tsx
1176
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1177
- var ViewToggle = ({ currentView, onChange }) => {
1178
- return /* @__PURE__ */ jsxs5(
1179
- "div",
1823
+ return /* @__PURE__ */ jsx6("div", { className, style: containerStyle, children: /* @__PURE__ */ jsxs5(
1824
+ ReactFlow2,
1180
1825
  {
1181
- style: {
1182
- position: "absolute",
1183
- top: 10,
1184
- left: 10,
1185
- display: "flex",
1186
- gap: 4,
1187
- background: "white",
1188
- padding: 4,
1189
- borderRadius: 8,
1190
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
1191
- zIndex: 10,
1192
- fontFamily: "system-ui, sans-serif"
1193
- },
1826
+ nodes,
1827
+ edges,
1828
+ onNodesChange,
1829
+ onEdgesChange,
1830
+ onNodeClick: handleNodeClick,
1831
+ nodeTypes: nodeTypes2,
1832
+ defaultEdgeOptions: defaultEdgeOptions2,
1833
+ fitView: true,
1834
+ fitViewOptions: { padding: 0.3, maxZoom: 1.5 },
1835
+ minZoom: 0.1,
1836
+ maxZoom: 2.5,
1837
+ proOptions: { hideAttribution: true },
1838
+ nodesDraggable: true,
1839
+ nodesConnectable: false,
1840
+ elementsSelectable: true,
1841
+ selectNodesOnDrag: false,
1842
+ panOnDrag: true,
1843
+ zoomOnScroll: true,
1844
+ zoomOnPinch: true,
1845
+ panOnScroll: false,
1194
1846
  children: [
1195
1847
  /* @__PURE__ */ jsx6(
1196
- "button",
1848
+ Background2,
1849
+ {
1850
+ variant: BackgroundVariant2.Dots,
1851
+ gap: 24,
1852
+ size: 1,
1853
+ color: "#d1d5db"
1854
+ }
1855
+ ),
1856
+ /* @__PURE__ */ jsx6(
1857
+ Controls2,
1197
1858
  {
1198
- onClick: () => onChange("observables"),
1859
+ showInteractive: false,
1199
1860
  style: {
1200
- padding: "6px 12px",
1201
- border: "none",
1202
- borderRadius: 4,
1203
- cursor: "pointer",
1204
- fontSize: 12,
1205
- fontWeight: currentView === "observables" ? 600 : 400,
1206
- background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
1207
- color: currentView === "observables" ? "white" : "#374151"
1208
- },
1209
- children: "Observables"
1861
+ borderRadius: 10,
1862
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
1863
+ border: "1px solid rgba(0,0,0,0.06)"
1864
+ }
1210
1865
  }
1211
1866
  ),
1212
1867
  /* @__PURE__ */ jsx6(
1213
- "button",
1868
+ MiniMap2,
1214
1869
  {
1215
- onClick: () => onChange("investigation"),
1870
+ nodeColor: miniMapNodeColor,
1871
+ zoomable: true,
1872
+ pannable: true,
1216
1873
  style: {
1217
- padding: "6px 12px",
1218
- border: "none",
1219
- borderRadius: 4,
1220
- cursor: "pointer",
1221
- fontSize: 12,
1222
- fontWeight: currentView === "investigation" ? 600 : 400,
1223
- background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
1224
- color: currentView === "investigation" ? "white" : "#374151"
1874
+ borderRadius: 10,
1875
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
1876
+ border: "1px solid rgba(0,0,0,0.06)",
1877
+ background: "rgba(255,255,255,0.9)"
1225
1878
  },
1226
- children: "Investigation"
1879
+ maskColor: "rgba(0,0,0,0.08)"
1227
1880
  }
1228
1881
  )
1229
1882
  ]
1230
1883
  }
1884
+ ) });
1885
+ };
1886
+
1887
+ // src/components/CyvestGraph.tsx
1888
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1889
+ var ViewToggle = ({ currentView, onChange }) => {
1890
+ const containerStyle = useMemo8(
1891
+ () => ({
1892
+ position: "absolute",
1893
+ top: 12,
1894
+ left: 12,
1895
+ display: "flex",
1896
+ gap: 2,
1897
+ background: "rgba(255, 255, 255, 0.95)",
1898
+ backdropFilter: "blur(8px)",
1899
+ padding: 4,
1900
+ borderRadius: 10,
1901
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
1902
+ zIndex: 10,
1903
+ fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1904
+ border: "1px solid rgba(0,0,0,0.06)"
1905
+ }),
1906
+ []
1907
+ );
1908
+ const getButtonStyle = useCallback4(
1909
+ (isActive) => ({
1910
+ padding: "8px 14px",
1911
+ border: "none",
1912
+ borderRadius: 7,
1913
+ cursor: "pointer",
1914
+ fontSize: 12,
1915
+ fontWeight: isActive ? 600 : 500,
1916
+ background: isActive ? "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)" : "transparent",
1917
+ color: isActive ? "white" : "#4b5563",
1918
+ transition: "all 0.15s ease",
1919
+ letterSpacing: "-0.01em"
1920
+ }),
1921
+ []
1231
1922
  );
1923
+ return /* @__PURE__ */ jsxs6("div", { style: containerStyle, children: [
1924
+ /* @__PURE__ */ jsx7(
1925
+ "button",
1926
+ {
1927
+ onClick: () => onChange("observables"),
1928
+ style: getButtonStyle(currentView === "observables"),
1929
+ onMouseEnter: (e) => {
1930
+ if (currentView !== "observables") {
1931
+ e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
1932
+ e.currentTarget.style.color = "#3b82f6";
1933
+ }
1934
+ },
1935
+ onMouseLeave: (e) => {
1936
+ if (currentView !== "observables") {
1937
+ e.currentTarget.style.background = "transparent";
1938
+ e.currentTarget.style.color = "#4b5563";
1939
+ }
1940
+ },
1941
+ children: /* @__PURE__ */ jsxs6("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
1942
+ /* @__PURE__ */ jsxs6(
1943
+ "svg",
1944
+ {
1945
+ width: "14",
1946
+ height: "14",
1947
+ viewBox: "0 0 24 24",
1948
+ fill: "none",
1949
+ stroke: "currentColor",
1950
+ strokeWidth: "2",
1951
+ strokeLinecap: "round",
1952
+ strokeLinejoin: "round",
1953
+ children: [
1954
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "3" }),
1955
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "10" }),
1956
+ /* @__PURE__ */ jsx7("line", { x1: "12", y1: "2", x2: "12", y2: "4" }),
1957
+ /* @__PURE__ */ jsx7("line", { x1: "12", y1: "20", x2: "12", y2: "22" }),
1958
+ /* @__PURE__ */ jsx7("line", { x1: "2", y1: "12", x2: "4", y2: "12" }),
1959
+ /* @__PURE__ */ jsx7("line", { x1: "20", y1: "12", x2: "22", y2: "12" })
1960
+ ]
1961
+ }
1962
+ ),
1963
+ "Observables"
1964
+ ] })
1965
+ }
1966
+ ),
1967
+ /* @__PURE__ */ jsx7(
1968
+ "button",
1969
+ {
1970
+ onClick: () => onChange("investigation"),
1971
+ style: getButtonStyle(currentView === "investigation"),
1972
+ onMouseEnter: (e) => {
1973
+ if (currentView !== "investigation") {
1974
+ e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
1975
+ e.currentTarget.style.color = "#3b82f6";
1976
+ }
1977
+ },
1978
+ onMouseLeave: (e) => {
1979
+ if (currentView !== "investigation") {
1980
+ e.currentTarget.style.background = "transparent";
1981
+ e.currentTarget.style.color = "#4b5563";
1982
+ }
1983
+ },
1984
+ children: /* @__PURE__ */ jsxs6("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
1985
+ /* @__PURE__ */ jsxs6(
1986
+ "svg",
1987
+ {
1988
+ width: "14",
1989
+ height: "14",
1990
+ viewBox: "0 0 24 24",
1991
+ fill: "none",
1992
+ stroke: "currentColor",
1993
+ strokeWidth: "2",
1994
+ strokeLinecap: "round",
1995
+ strokeLinejoin: "round",
1996
+ children: [
1997
+ /* @__PURE__ */ jsx7("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1998
+ /* @__PURE__ */ jsx7("path", { d: "M9 3v18" }),
1999
+ /* @__PURE__ */ jsx7("path", { d: "M3 9h18" })
2000
+ ]
2001
+ }
2002
+ ),
2003
+ "Investigation"
2004
+ ] })
2005
+ }
2006
+ )
2007
+ ] });
1232
2008
  };
1233
2009
  var CyvestGraph = ({
1234
2010
  investigation,
@@ -1239,46 +2015,52 @@ var CyvestGraph = ({
1239
2015
  className,
1240
2016
  showViewToggle = true
1241
2017
  }) => {
1242
- const [view, setView] = useState2(initialView);
2018
+ const [view, setView] = useState2(
2019
+ initialView
2020
+ );
1243
2021
  const handleNodeClick = useCallback4(
1244
2022
  (nodeId, _nodeType) => {
1245
2023
  onNodeClick?.(nodeId);
1246
2024
  },
1247
2025
  [onNodeClick]
1248
2026
  );
1249
- return /* @__PURE__ */ jsxs5(
1250
- "div",
1251
- {
1252
- className,
1253
- style: {
1254
- width,
1255
- height,
1256
- position: "relative"
1257
- },
1258
- children: [
1259
- showViewToggle && /* @__PURE__ */ jsx6(ViewToggle, { currentView: view, onChange: setView }),
1260
- view === "observables" ? /* @__PURE__ */ jsx6(
1261
- ObservablesGraph,
1262
- {
1263
- investigation,
1264
- height: "100%",
1265
- width: "100%",
1266
- onNodeClick: handleNodeClick,
1267
- showControls: true
1268
- }
1269
- ) : /* @__PURE__ */ jsx6(
1270
- InvestigationGraph,
1271
- {
1272
- investigation,
1273
- height: "100%",
1274
- width: "100%",
1275
- onNodeClick: handleNodeClick
1276
- }
1277
- )
1278
- ]
1279
- }
2027
+ const containerStyle = useMemo8(
2028
+ () => ({
2029
+ width,
2030
+ height,
2031
+ position: "relative"
2032
+ }),
2033
+ [width, height]
1280
2034
  );
2035
+ return /* @__PURE__ */ jsxs6("div", { className, style: containerStyle, children: [
2036
+ showViewToggle && /* @__PURE__ */ jsx7(ViewToggle, { currentView: view, onChange: setView }),
2037
+ view === "observables" ? /* @__PURE__ */ jsx7(
2038
+ ObservablesGraph,
2039
+ {
2040
+ investigation,
2041
+ height: "100%",
2042
+ width: "100%",
2043
+ onNodeClick: handleNodeClick,
2044
+ showControls: true
2045
+ }
2046
+ ) : /* @__PURE__ */ jsx7(
2047
+ InvestigationGraph,
2048
+ {
2049
+ investigation,
2050
+ height: "100%",
2051
+ width: "100%",
2052
+ onNodeClick: handleNodeClick
2053
+ }
2054
+ )
2055
+ ] });
1281
2056
  };
1282
2057
  export {
1283
- CyvestGraph
2058
+ CyvestGraph,
2059
+ DEFAULT_FORCE_CONFIG,
2060
+ INVESTIGATION_ICON_MAP,
2061
+ InvestigationGraph,
2062
+ OBSERVABLE_ICON_MAP,
2063
+ ObservablesGraph,
2064
+ getInvestigationIcon,
2065
+ getObservableIcon
1284
2066
  };