@aiready/components 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { useRef, useState, useEffect, useCallback } from 'react';
2
- import * as d3 from 'd3';
1
+ import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
2
+ import * as d32 from 'd3';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -16,7 +16,8 @@ function useForceSimulation(initialNodes, initialLinks, options) {
16
16
  width,
17
17
  height,
18
18
  alphaDecay = 0.0228,
19
- velocityDecay = 0.4
19
+ velocityDecay = 0.4,
20
+ onTick
20
21
  } = options;
21
22
  const [nodes, setNodes] = useState(initialNodes);
22
23
  const [links, setLinks] = useState(initialLinks);
@@ -26,15 +27,22 @@ function useForceSimulation(initialNodes, initialLinks, options) {
26
27
  useEffect(() => {
27
28
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
28
29
  const linksCopy = initialLinks.map((link) => ({ ...link }));
29
- const simulation = d3.forceSimulation(nodesCopy).force(
30
+ const simulation = d32.forceSimulation(nodesCopy).force(
30
31
  "link",
31
- d3.forceLink(linksCopy).id((d) => d.id).distance(linkDistance).strength(linkStrength)
32
- ).force("charge", d3.forceManyBody().strength(chargeStrength)).force("center", d3.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
32
+ d32.forceLink(linksCopy).id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength)
33
+ ).force("charge", d32.forceManyBody().strength(chargeStrength)).force("center", d32.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
33
34
  "collision",
34
- d3.forceCollide().radius(collisionRadius).strength(collisionStrength)
35
+ d32.forceCollide().radius((d) => {
36
+ const nodeSize = d && d.size ? d.size : 10;
37
+ return nodeSize + collisionRadius;
38
+ }).strength(collisionStrength)
35
39
  ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
36
40
  simulationRef.current = simulation;
37
41
  simulation.on("tick", () => {
42
+ try {
43
+ if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
44
+ } catch (e) {
45
+ }
38
46
  setNodes([...nodesCopy]);
39
47
  setLinks([...linksCopy]);
40
48
  setAlpha(simulation.alpha());
@@ -58,7 +66,8 @@ function useForceSimulation(initialNodes, initialLinks, options) {
58
66
  width,
59
67
  height,
60
68
  alphaDecay,
61
- velocityDecay
69
+ velocityDecay,
70
+ onTick
62
71
  ]);
63
72
  const restart = () => {
64
73
  if (simulationRef.current) {
@@ -72,221 +81,534 @@ function useForceSimulation(initialNodes, initialLinks, options) {
72
81
  setIsRunning(false);
73
82
  }
74
83
  };
84
+ const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });
85
+ const forcesEnabledRef = useRef(true);
86
+ const setForcesEnabled = (enabled) => {
87
+ const sim = simulationRef.current;
88
+ if (!sim) return;
89
+ if (forcesEnabledRef.current === enabled) return;
90
+ forcesEnabledRef.current = enabled;
91
+ try {
92
+ const charge = sim.force("charge");
93
+ if (charge && typeof charge.strength === "function") {
94
+ charge.strength(enabled ? originalForcesRef.current.charge : 0);
95
+ }
96
+ const link = sim.force("link");
97
+ if (link && typeof link.strength === "function") {
98
+ link.strength(enabled ? originalForcesRef.current.link : 0);
99
+ }
100
+ } catch (e) {
101
+ }
102
+ };
75
103
  return {
76
104
  nodes,
77
105
  links,
78
106
  restart,
79
107
  stop,
80
108
  isRunning,
81
- alpha
109
+ alpha,
110
+ setForcesEnabled
82
111
  };
83
112
  }
84
113
  function cn(...inputs) {
85
114
  return twMerge(clsx(inputs));
86
115
  }
87
- var ForceDirectedGraph = ({
88
- nodes: initialNodes,
89
- links: initialLinks,
90
- width,
91
- height,
92
- simulationOptions,
93
- enableZoom = true,
94
- enableDrag = true,
95
- onNodeClick,
96
- onNodeHover,
97
- onLinkClick,
98
- selectedNodeId,
99
- hoveredNodeId,
100
- defaultNodeColor = "#69b3a2",
101
- defaultNodeSize = 10,
102
- defaultLinkColor = "#999",
103
- defaultLinkWidth = 1,
104
- showNodeLabels = true,
105
- showLinkLabels = false,
106
- className
107
- }) => {
108
- const svgRef = useRef(null);
109
- const gRef = useRef(null);
110
- const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
111
- const { nodes, links, restart } = useForceSimulation(initialNodes, initialLinks, {
116
+ var ForceDirectedGraph = forwardRef(
117
+ ({
118
+ nodes: initialNodes,
119
+ links: initialLinks,
112
120
  width,
113
121
  height,
114
- ...simulationOptions
115
- });
116
- useEffect(() => {
117
- if (!enableZoom || !svgRef.current || !gRef.current) return;
118
- const svg = d3.select(svgRef.current);
119
- const g = d3.select(gRef.current);
120
- const zoom2 = d3.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
121
- g.attr("transform", event.transform);
122
- setTransform(event.transform);
123
- });
124
- svg.call(zoom2);
125
- return () => {
126
- svg.on(".zoom", null);
122
+ simulationOptions,
123
+ enableZoom = true,
124
+ enableDrag = true,
125
+ onNodeClick,
126
+ onNodeHover,
127
+ onLinkClick,
128
+ selectedNodeId,
129
+ hoveredNodeId,
130
+ defaultNodeColor = "#69b3a2",
131
+ defaultNodeSize = 10,
132
+ defaultLinkColor = "#999",
133
+ defaultLinkWidth = 1,
134
+ showNodeLabels = true,
135
+ showLinkLabels = false,
136
+ className,
137
+ manualLayout = false,
138
+ onManualLayoutChange,
139
+ packageBounds
140
+ }, ref) => {
141
+ const svgRef = useRef(null);
142
+ const gRef = useRef(null);
143
+ const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
144
+ const dragNodeRef = useRef(null);
145
+ const dragActiveRef = useRef(false);
146
+ const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
147
+ const internalDragEnabledRef = useRef(enableDrag);
148
+ useEffect(() => {
149
+ internalDragEnabledRef.current = enableDrag;
150
+ }, [enableDrag]);
151
+ const onTick = (nodesCopy, _linksCopy, _sim) => {
152
+ const bounds = packageBounds && Object.keys(packageBounds).length ? packageBounds : void 0;
153
+ let effectiveBounds = bounds;
154
+ if (!effectiveBounds) {
155
+ try {
156
+ const counts = {};
157
+ (initialNodes || []).forEach((n) => {
158
+ if (n && n.kind === "file") {
159
+ const g = n.packageGroup || "root";
160
+ counts[g] = (counts[g] || 0) + 1;
161
+ }
162
+ });
163
+ const children = Object.keys(counts).map((k) => ({ name: k, value: counts[k] }));
164
+ if (children.length > 0) {
165
+ const root = d32.hierarchy({ children }).sum((d) => d.value);
166
+ const pack2 = d32.pack().size([width, height]).padding(30);
167
+ const packed = pack2(root);
168
+ const map = {};
169
+ if (packed.children) {
170
+ packed.children.forEach((c) => {
171
+ map[`pkg:${c.data.name}`] = { x: c.x, y: c.y, r: c.r * 0.95 };
172
+ });
173
+ effectiveBounds = map;
174
+ }
175
+ }
176
+ } catch (e) {
177
+ }
178
+ }
179
+ if (!effectiveBounds) return;
180
+ try {
181
+ Object.values(nodesCopy).forEach((n) => {
182
+ if (!n) return;
183
+ if (n.kind === "package") return;
184
+ const pkg = n.packageGroup;
185
+ if (!pkg) return;
186
+ const bound = effectiveBounds[`pkg:${pkg}`];
187
+ if (!bound) return;
188
+ const margin = (n.size || 10) + 12;
189
+ const dx = (n.x || 0) - bound.x;
190
+ const dy = (n.y || 0) - bound.y;
191
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1e-4;
192
+ const maxDist = Math.max(1, bound.r - margin);
193
+ if (dist > maxDist) {
194
+ const desiredX = bound.x + dx * (maxDist / dist);
195
+ const desiredY = bound.y + dy * (maxDist / dist);
196
+ const softness = 0.08;
197
+ n.vx = (n.vx || 0) + (desiredX - n.x) * softness;
198
+ n.vy = (n.vy || 0) + (desiredY - n.y) * softness;
199
+ }
200
+ });
201
+ } catch (e) {
202
+ }
127
203
  };
128
- }, [enableZoom]);
129
- const handleDragStart = useCallback(
130
- (event, node) => {
131
- if (!enableDrag) return;
132
- event.stopPropagation();
133
- node.fx = node.x;
134
- node.fy = node.y;
135
- restart();
136
- },
137
- [enableDrag, restart]
138
- );
139
- const handleDrag = useCallback(
140
- (event, node) => {
141
- if (!enableDrag) return;
142
- const svg = svgRef.current;
143
- if (!svg) return;
144
- const rect = svg.getBoundingClientRect();
145
- const x = (event.clientX - rect.left - transform.x) / transform.k;
146
- const y = (event.clientY - rect.top - transform.y) / transform.k;
147
- node.fx = x;
148
- node.fy = y;
149
- },
150
- [enableDrag, transform]
151
- );
152
- const handleDragEnd = useCallback(
153
- (event, node) => {
154
- if (!enableDrag) return;
155
- event.stopPropagation();
156
- node.fx = null;
157
- node.fy = null;
158
- },
159
- [enableDrag]
160
- );
161
- const handleNodeClick = useCallback(
162
- (node) => {
163
- onNodeClick?.(node);
164
- },
165
- [onNodeClick]
166
- );
167
- const handleNodeMouseEnter = useCallback(
168
- (node) => {
169
- onNodeHover?.(node);
170
- },
171
- [onNodeHover]
172
- );
173
- const handleNodeMouseLeave = useCallback(() => {
174
- onNodeHover?.(null);
175
- }, [onNodeHover]);
176
- const handleLinkClick = useCallback(
177
- (link) => {
178
- onLinkClick?.(link);
179
- },
180
- [onLinkClick]
181
- );
182
- return /* @__PURE__ */ jsxs(
183
- "svg",
184
- {
185
- ref: svgRef,
204
+ const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(initialNodes, initialLinks, {
186
205
  width,
187
206
  height,
188
- className: cn("bg-white dark:bg-gray-900", className),
189
- children: [
190
- /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
191
- "marker",
192
- {
193
- id: "arrow",
194
- viewBox: "0 0 10 10",
195
- refX: "20",
196
- refY: "5",
197
- markerWidth: "6",
198
- markerHeight: "6",
199
- orient: "auto",
200
- children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
207
+ chargeStrength: manualLayout ? 0 : void 0,
208
+ onTick,
209
+ ...simulationOptions
210
+ });
211
+ useEffect(() => {
212
+ if (!packageBounds) return;
213
+ try {
214
+ restart();
215
+ } catch (e) {
216
+ }
217
+ }, [packageBounds, restart]);
218
+ useEffect(() => {
219
+ try {
220
+ if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
221
+ else setForcesEnabled(true);
222
+ } catch (e) {
223
+ }
224
+ }, [manualLayout, pinnedNodes, setForcesEnabled]);
225
+ useImperativeHandle(
226
+ ref,
227
+ () => ({
228
+ pinAll: () => {
229
+ const newPinned = /* @__PURE__ */ new Set();
230
+ nodes.forEach((node) => {
231
+ node.fx = node.x;
232
+ node.fy = node.y;
233
+ newPinned.add(node.id);
234
+ });
235
+ setPinnedNodes(newPinned);
236
+ restart();
237
+ },
238
+ unpinAll: () => {
239
+ nodes.forEach((node) => {
240
+ node.fx = null;
241
+ node.fy = null;
242
+ });
243
+ setPinnedNodes(/* @__PURE__ */ new Set());
244
+ restart();
245
+ },
246
+ resetLayout: () => {
247
+ nodes.forEach((node) => {
248
+ node.fx = null;
249
+ node.fy = null;
250
+ });
251
+ setPinnedNodes(/* @__PURE__ */ new Set());
252
+ restart();
253
+ },
254
+ fitView: () => {
255
+ if (!svgRef.current || !nodes.length) return;
256
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
257
+ nodes.forEach((node) => {
258
+ if (node.x !== void 0 && node.y !== void 0) {
259
+ const size = node.size || 10;
260
+ minX = Math.min(minX, node.x - size);
261
+ maxX = Math.max(maxX, node.x + size);
262
+ minY = Math.min(minY, node.y - size);
263
+ maxY = Math.max(maxY, node.y + size);
264
+ }
265
+ });
266
+ if (!isFinite(minX)) return;
267
+ const padding = 40;
268
+ const nodeWidth = maxX - minX;
269
+ const nodeHeight = maxY - minY;
270
+ const scale = Math.min(
271
+ (width - padding * 2) / nodeWidth,
272
+ (height - padding * 2) / nodeHeight,
273
+ 10
274
+ );
275
+ const centerX = (minX + maxX) / 2;
276
+ const centerY = (minY + maxY) / 2;
277
+ const x = width / 2 - centerX * scale;
278
+ const y = height / 2 - centerY * scale;
279
+ if (gRef.current && svgRef.current) {
280
+ const svg = d32.select(svgRef.current);
281
+ const newTransform = d32.zoomIdentity.translate(x, y).scale(scale);
282
+ svg.transition().duration(300).call(d32.zoom().transform, newTransform);
283
+ setTransform(newTransform);
201
284
  }
202
- ) }),
203
- /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
204
- links.map((link, i) => {
205
- const source = link.source;
206
- const target = link.target;
207
- if (!source.x || !source.y || !target.x || !target.y) return null;
208
- return /* @__PURE__ */ jsxs("g", { children: [
285
+ },
286
+ getPinnedNodes: () => Array.from(pinnedNodes),
287
+ setDragMode: (enabled) => {
288
+ internalDragEnabledRef.current = enabled;
289
+ }
290
+ }),
291
+ [nodes, pinnedNodes, restart, width, height]
292
+ );
293
+ useEffect(() => {
294
+ try {
295
+ if (typeof onManualLayoutChange === "function") onManualLayoutChange(manualLayout);
296
+ } catch (e) {
297
+ }
298
+ }, [manualLayout, onManualLayoutChange]);
299
+ useEffect(() => {
300
+ if (!enableZoom || !svgRef.current || !gRef.current) return;
301
+ const svg = d32.select(svgRef.current);
302
+ const g = d32.select(gRef.current);
303
+ const zoom2 = d32.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
304
+ g.attr("transform", event.transform);
305
+ setTransform(event.transform);
306
+ });
307
+ svg.call(zoom2);
308
+ return () => {
309
+ svg.on(".zoom", null);
310
+ };
311
+ }, [enableZoom]);
312
+ const handleDragStart = useCallback(
313
+ (event, node) => {
314
+ if (!enableDrag) return;
315
+ event.preventDefault();
316
+ event.stopPropagation();
317
+ dragActiveRef.current = true;
318
+ dragNodeRef.current = node;
319
+ node.fx = node.x;
320
+ node.fy = node.y;
321
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
322
+ try {
323
+ stop();
324
+ } catch (e) {
325
+ }
326
+ },
327
+ [enableDrag, restart]
328
+ );
329
+ useEffect(() => {
330
+ if (!enableDrag) return;
331
+ const handleWindowMove = (event) => {
332
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
333
+ const svg = svgRef.current;
334
+ if (!svg) return;
335
+ const rect = svg.getBoundingClientRect();
336
+ const x = (event.clientX - rect.left - transform.x) / transform.k;
337
+ const y = (event.clientY - rect.top - transform.y) / transform.k;
338
+ dragNodeRef.current.fx = x;
339
+ dragNodeRef.current.fy = y;
340
+ };
341
+ const handleWindowUp = () => {
342
+ if (!dragActiveRef.current) return;
343
+ try {
344
+ setForcesEnabled(true);
345
+ restart();
346
+ } catch (e) {
347
+ }
348
+ dragNodeRef.current = null;
349
+ dragActiveRef.current = false;
350
+ };
351
+ const handleWindowLeave = (event) => {
352
+ if (event.relatedTarget === null) handleWindowUp();
353
+ };
354
+ window.addEventListener("mousemove", handleWindowMove);
355
+ window.addEventListener("mouseup", handleWindowUp);
356
+ window.addEventListener("mouseout", handleWindowLeave);
357
+ window.addEventListener("blur", handleWindowUp);
358
+ return () => {
359
+ window.removeEventListener("mousemove", handleWindowMove);
360
+ window.removeEventListener("mouseup", handleWindowUp);
361
+ window.removeEventListener("mouseout", handleWindowLeave);
362
+ window.removeEventListener("blur", handleWindowUp);
363
+ };
364
+ }, [enableDrag, transform]);
365
+ useEffect(() => {
366
+ if (!gRef.current || !enableDrag) return;
367
+ const g = d32.select(gRef.current);
368
+ const dragBehavior = d32.drag().on("start", function(event) {
369
+ try {
370
+ const target = event.sourceEvent && event.sourceEvent.target || event.target;
371
+ const grp = target.closest?.("g.node");
372
+ const id = grp?.getAttribute("data-id");
373
+ if (!id) return;
374
+ const node = nodes.find((n) => n.id === id);
375
+ if (!node) return;
376
+ if (!internalDragEnabledRef.current) return;
377
+ if (!event.active) restart();
378
+ dragActiveRef.current = true;
379
+ dragNodeRef.current = node;
380
+ node.fx = node.x;
381
+ node.fy = node.y;
382
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
383
+ } catch (e) {
384
+ }
385
+ }).on("drag", function(event) {
386
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
387
+ const svg = svgRef.current;
388
+ if (!svg) return;
389
+ const rect = svg.getBoundingClientRect();
390
+ const x = (event.sourceEvent.clientX - rect.left - transform.x) / transform.k;
391
+ const y = (event.sourceEvent.clientY - rect.top - transform.y) / transform.k;
392
+ dragNodeRef.current.fx = x;
393
+ dragNodeRef.current.fy = y;
394
+ }).on("end", function() {
395
+ try {
396
+ setForcesEnabled(true);
397
+ restart();
398
+ } catch (e) {
399
+ }
400
+ dragNodeRef.current = null;
401
+ dragActiveRef.current = false;
402
+ });
403
+ try {
404
+ g.selectAll("g.node").call(dragBehavior);
405
+ } catch (e) {
406
+ }
407
+ return () => {
408
+ try {
409
+ g.selectAll("g.node").on(".drag", null);
410
+ } catch (e) {
411
+ }
412
+ };
413
+ }, [gRef, enableDrag, nodes, transform, restart]);
414
+ const handleNodeClick = useCallback(
415
+ (node) => {
416
+ onNodeClick?.(node);
417
+ },
418
+ [onNodeClick]
419
+ );
420
+ const handleNodeDoubleClick = useCallback(
421
+ (event, node) => {
422
+ event.stopPropagation();
423
+ if (!enableDrag) return;
424
+ if (node.fx === null || node.fx === void 0) {
425
+ node.fx = node.x;
426
+ node.fy = node.y;
427
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
428
+ } else {
429
+ node.fx = null;
430
+ node.fy = null;
431
+ setPinnedNodes((prev) => {
432
+ const next = new Set(prev);
433
+ next.delete(node.id);
434
+ return next;
435
+ });
436
+ }
437
+ restart();
438
+ },
439
+ [enableDrag, restart]
440
+ );
441
+ const handleCanvasDoubleClick = useCallback(() => {
442
+ nodes.forEach((node) => {
443
+ node.fx = null;
444
+ node.fy = null;
445
+ });
446
+ setPinnedNodes(/* @__PURE__ */ new Set());
447
+ restart();
448
+ }, [nodes, restart]);
449
+ const handleNodeMouseEnter = useCallback(
450
+ (node) => {
451
+ onNodeHover?.(node);
452
+ },
453
+ [onNodeHover]
454
+ );
455
+ const handleNodeMouseLeave = useCallback(() => {
456
+ onNodeHover?.(null);
457
+ }, [onNodeHover]);
458
+ const handleLinkClick = useCallback(
459
+ (link) => {
460
+ onLinkClick?.(link);
461
+ },
462
+ [onLinkClick]
463
+ );
464
+ return /* @__PURE__ */ jsxs(
465
+ "svg",
466
+ {
467
+ ref: svgRef,
468
+ width,
469
+ height,
470
+ className: cn("bg-white dark:bg-gray-900", className),
471
+ onDoubleClick: handleCanvasDoubleClick,
472
+ children: [
473
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
474
+ "marker",
475
+ {
476
+ id: "arrow",
477
+ viewBox: "0 0 10 10",
478
+ refX: "20",
479
+ refY: "5",
480
+ markerWidth: "6",
481
+ markerHeight: "6",
482
+ orient: "auto",
483
+ children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
484
+ }
485
+ ) }),
486
+ /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
487
+ links.map((link, i) => {
488
+ const source = link.source;
489
+ const target = link.target;
490
+ if (source.x == null || source.y == null || target.x == null || target.y == null) return null;
491
+ return /* @__PURE__ */ jsxs("g", { children: [
492
+ /* @__PURE__ */ jsx(
493
+ "line",
494
+ {
495
+ x1: source.x,
496
+ y1: source.y,
497
+ x2: target.x,
498
+ y2: target.y,
499
+ stroke: link.color || defaultLinkColor,
500
+ strokeWidth: link.width || defaultLinkWidth,
501
+ opacity: 0.6,
502
+ className: "cursor-pointer transition-opacity hover:opacity-100",
503
+ onClick: () => handleLinkClick(link)
504
+ }
505
+ ),
506
+ showLinkLabels && link.label && /* @__PURE__ */ jsx(
507
+ "text",
508
+ {
509
+ x: (source.x + target.x) / 2,
510
+ y: (source.y + target.y) / 2,
511
+ fill: "#666",
512
+ fontSize: "10",
513
+ textAnchor: "middle",
514
+ dominantBaseline: "middle",
515
+ pointerEvents: "none",
516
+ children: link.label
517
+ }
518
+ )
519
+ ] }, `link-${i}`);
520
+ }),
521
+ nodes.map((node) => {
522
+ if (node.x == null || node.y == null) return null;
523
+ const isSelected = selectedNodeId === node.id;
524
+ const isHovered = hoveredNodeId === node.id;
525
+ const nodeSize = node.size || defaultNodeSize;
526
+ const nodeColor = node.color || defaultNodeColor;
527
+ return /* @__PURE__ */ jsxs(
528
+ "g",
529
+ {
530
+ transform: `translate(${node.x},${node.y})`,
531
+ className: "cursor-pointer node",
532
+ "data-id": node.id,
533
+ onClick: () => handleNodeClick(node),
534
+ onDoubleClick: (event) => handleNodeDoubleClick(event, node),
535
+ onMouseEnter: () => handleNodeMouseEnter(node),
536
+ onMouseLeave: handleNodeMouseLeave,
537
+ onMouseDown: (e) => handleDragStart(e, node),
538
+ children: [
539
+ /* @__PURE__ */ jsx(
540
+ "circle",
541
+ {
542
+ r: nodeSize,
543
+ fill: nodeColor,
544
+ stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
545
+ strokeWidth: pinnedNodes.has(node.id) ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
546
+ opacity: isHovered || isSelected ? 1 : 0.9,
547
+ className: "transition-all"
548
+ }
549
+ ),
550
+ pinnedNodes.has(node.id) && /* @__PURE__ */ jsx(
551
+ "circle",
552
+ {
553
+ r: nodeSize + 4,
554
+ fill: "none",
555
+ stroke: "#ff6b6b",
556
+ strokeWidth: 1,
557
+ opacity: 0.5,
558
+ className: "pointer-events-none"
559
+ }
560
+ ),
561
+ showNodeLabels && node.label && /* @__PURE__ */ jsx(
562
+ "text",
563
+ {
564
+ y: nodeSize + 15,
565
+ fill: "#333",
566
+ fontSize: "12",
567
+ textAnchor: "middle",
568
+ dominantBaseline: "middle",
569
+ pointerEvents: "none",
570
+ className: "select-none",
571
+ children: node.label
572
+ }
573
+ )
574
+ ]
575
+ },
576
+ node.id
577
+ );
578
+ }),
579
+ packageBounds && Object.keys(packageBounds).length > 0 && /* @__PURE__ */ jsx("g", { className: "package-boundaries", pointerEvents: "none", children: Object.entries(packageBounds).map(([pid, b]) => /* @__PURE__ */ jsxs("g", { children: [
209
580
  /* @__PURE__ */ jsx(
210
- "line",
581
+ "circle",
211
582
  {
212
- x1: source.x,
213
- y1: source.y,
214
- x2: target.x,
215
- y2: target.y,
216
- stroke: link.color || defaultLinkColor,
217
- strokeWidth: link.width || defaultLinkWidth,
218
- opacity: 0.6,
219
- className: "cursor-pointer transition-opacity hover:opacity-100",
220
- onClick: () => handleLinkClick(link)
583
+ cx: b.x,
584
+ cy: b.y,
585
+ r: b.r,
586
+ fill: "rgba(148,163,184,0.06)",
587
+ stroke: "#475569",
588
+ strokeWidth: 2,
589
+ strokeDasharray: "6 6",
590
+ opacity: 0.9
221
591
  }
222
592
  ),
223
- showLinkLabels && link.label && /* @__PURE__ */ jsx(
593
+ /* @__PURE__ */ jsx(
224
594
  "text",
225
595
  {
226
- x: (source.x + target.x) / 2,
227
- y: (source.y + target.y) / 2,
228
- fill: "#666",
229
- fontSize: "10",
596
+ x: b.x,
597
+ y: Math.max(12, b.y - b.r + 14),
598
+ fill: "#475569",
599
+ fontSize: 11,
230
600
  textAnchor: "middle",
231
- dominantBaseline: "middle",
232
601
  pointerEvents: "none",
233
- children: link.label
602
+ children: pid.replace(/^pkg:/, "")
234
603
  }
235
604
  )
236
- ] }, `link-${i}`);
237
- }),
238
- nodes.map((node) => {
239
- if (!node.x || !node.y) return null;
240
- const isSelected = selectedNodeId === node.id;
241
- const isHovered = hoveredNodeId === node.id;
242
- const nodeSize = node.size || defaultNodeSize;
243
- const nodeColor = node.color || defaultNodeColor;
244
- return /* @__PURE__ */ jsxs(
245
- "g",
246
- {
247
- transform: `translate(${node.x},${node.y})`,
248
- className: "cursor-pointer",
249
- onClick: () => handleNodeClick(node),
250
- onMouseEnter: () => handleNodeMouseEnter(node),
251
- onMouseLeave: handleNodeMouseLeave,
252
- onMouseDown: (e) => handleDragStart(e, node),
253
- onMouseMove: (e) => handleDrag(e, node),
254
- onMouseUp: (e) => handleDragEnd(e, node),
255
- children: [
256
- /* @__PURE__ */ jsx(
257
- "circle",
258
- {
259
- r: nodeSize,
260
- fill: nodeColor,
261
- stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
262
- strokeWidth: isSelected ? 3 : 2,
263
- opacity: isHovered || isSelected ? 1 : 0.9,
264
- className: "transition-all"
265
- }
266
- ),
267
- showNodeLabels && node.label && /* @__PURE__ */ jsx(
268
- "text",
269
- {
270
- y: nodeSize + 15,
271
- fill: "#333",
272
- fontSize: "12",
273
- textAnchor: "middle",
274
- dominantBaseline: "middle",
275
- pointerEvents: "none",
276
- className: "select-none",
277
- children: node.label
278
- }
279
- )
280
- ]
281
- },
282
- node.id
283
- );
284
- })
285
- ] })
286
- ]
287
- }
288
- );
289
- };
605
+ ] }, pid)) })
606
+ ] })
607
+ ]
608
+ }
609
+ );
610
+ }
611
+ );
290
612
  ForceDirectedGraph.displayName = "ForceDirectedGraph";
291
613
 
292
614
  export { ForceDirectedGraph };