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