@aiready/components 0.1.3 → 0.1.5

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,4 +1,4 @@
1
- import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
1
+ import React, { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
2
2
  import * as d32 from 'd3';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
@@ -17,46 +17,183 @@ function useForceSimulation(initialNodes, initialLinks, options) {
17
17
  height,
18
18
  alphaDecay = 0.0228,
19
19
  velocityDecay = 0.4,
20
- onTick
20
+ alphaTarget = 0,
21
+ warmAlpha = 0.3,
22
+ alphaMin = 0.01,
23
+ // @ts-ignore allow extra option
24
+ stabilizeOnStop = true,
25
+ onTick,
26
+ // Optional throttle in milliseconds for tick updates (reduce React re-renders)
27
+ // Lower values = smoother but more CPU; default ~30ms (~33fps)
28
+ // @ts-ignore allow extra option
29
+ tickThrottleMs = 33,
30
+ // @ts-ignore allow extra option
31
+ maxSimulationTimeMs = 3e3
21
32
  } = options;
22
33
  const [nodes, setNodes] = useState(initialNodes);
23
34
  const [links, setLinks] = useState(initialLinks);
24
35
  const [isRunning, setIsRunning] = useState(false);
25
36
  const [alpha, setAlpha] = useState(1);
26
37
  const simulationRef = useRef(null);
38
+ const stopTimeoutRef = useRef(null);
39
+ const nodesKey = initialNodes.map((n) => n.id).join("|");
40
+ const linksKey = (initialLinks || []).map((l) => {
41
+ const s = typeof l.source === "string" ? l.source : l.source?.id;
42
+ const t = typeof l.target === "string" ? l.target : l.target?.id;
43
+ return `${s}->${t}:${l.type || ""}`;
44
+ }).join("|");
27
45
  useEffect(() => {
28
46
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
29
47
  const linksCopy = initialLinks.map((link) => ({ ...link }));
30
- const simulation = d32.forceSimulation(nodesCopy).force(
31
- "link",
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(
34
- "collision",
35
- d32.forceCollide().radius((d) => {
48
+ try {
49
+ nodesCopy.forEach((n, i) => {
50
+ const angle = i * 2 * Math.PI / nodesCopy.length;
51
+ const radius = Math.min(width, height) * 0.45;
52
+ n.x = width / 2 + radius * Math.cos(angle);
53
+ n.y = height / 2 + radius * Math.sin(angle);
54
+ n.vx = (Math.random() - 0.5) * 2;
55
+ n.vy = (Math.random() - 0.5) * 2;
56
+ });
57
+ } catch (e) {
58
+ nodesCopy.forEach((n) => {
59
+ n.x = Math.random() * width;
60
+ n.y = Math.random() * height;
61
+ n.vx = (Math.random() - 0.5) * 10;
62
+ n.vy = (Math.random() - 0.5) * 10;
63
+ });
64
+ }
65
+ const simulation = d32.forceSimulation(nodesCopy);
66
+ try {
67
+ const linkForce = d32.forceLink(linksCopy);
68
+ linkForce.id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength);
69
+ simulation.force("link", linkForce);
70
+ } catch (e) {
71
+ try {
72
+ simulation.force("link", d32.forceLink(linksCopy));
73
+ } catch (e2) {
74
+ }
75
+ }
76
+ try {
77
+ simulation.force("charge", d32.forceManyBody().strength(chargeStrength));
78
+ simulation.force("center", d32.forceCenter(width / 2, height / 2).strength(centerStrength));
79
+ const collide = d32.forceCollide().radius((d) => {
36
80
  const nodeSize = d && d.size ? d.size : 10;
37
81
  return nodeSize + collisionRadius;
38
- }).strength(collisionStrength)
39
- ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
82
+ }).strength(collisionStrength);
83
+ simulation.force("collision", collide);
84
+ simulation.force("x", d32.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5)));
85
+ simulation.force("y", d32.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
86
+ simulation.alphaDecay(alphaDecay);
87
+ simulation.velocityDecay(velocityDecay);
88
+ simulation.alphaMin(alphaMin);
89
+ try {
90
+ simulation.alphaTarget(alphaTarget);
91
+ } catch (e) {
92
+ }
93
+ try {
94
+ simulation.alpha(warmAlpha);
95
+ } catch (e) {
96
+ }
97
+ } catch (e) {
98
+ }
40
99
  simulationRef.current = simulation;
41
- simulation.on("tick", () => {
100
+ if (stopTimeoutRef.current != null) {
101
+ try {
102
+ globalThis.clearTimeout(stopTimeoutRef.current);
103
+ } catch (e) {
104
+ }
105
+ stopTimeoutRef.current = null;
106
+ }
107
+ if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
108
+ stopTimeoutRef.current = globalThis.setTimeout(() => {
109
+ try {
110
+ if (stabilizeOnStop) {
111
+ nodesCopy.forEach((n) => {
112
+ n.vx = 0;
113
+ n.vy = 0;
114
+ if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
115
+ if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
116
+ });
117
+ }
118
+ simulation.alpha(0);
119
+ simulation.stop();
120
+ } catch (e) {
121
+ }
122
+ setIsRunning(false);
123
+ setNodes([...nodesCopy]);
124
+ setLinks([...linksCopy]);
125
+ }, maxSimulationTimeMs);
126
+ }
127
+ let rafId = null;
128
+ let lastUpdate = 0;
129
+ const tickHandler = () => {
42
130
  try {
43
131
  if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
44
132
  } catch (e) {
45
133
  }
46
- setNodes([...nodesCopy]);
47
- setLinks([...linksCopy]);
48
- setAlpha(simulation.alpha());
49
- setIsRunning(simulation.alpha() > simulation.alphaMin());
50
- });
134
+ try {
135
+ if (simulation.alpha() <= alphaMin) {
136
+ try {
137
+ if (stabilizeOnStop) {
138
+ nodesCopy.forEach((n) => {
139
+ n.vx = 0;
140
+ n.vy = 0;
141
+ if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
142
+ if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
143
+ });
144
+ }
145
+ simulation.stop();
146
+ } catch (e) {
147
+ }
148
+ setAlpha(simulation.alpha());
149
+ setIsRunning(false);
150
+ setNodes([...nodesCopy]);
151
+ setLinks([...linksCopy]);
152
+ return;
153
+ }
154
+ } catch (e) {
155
+ }
156
+ const now = Date.now();
157
+ const shouldUpdate = now - lastUpdate >= tickThrottleMs;
158
+ if (rafId == null && shouldUpdate) {
159
+ rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
160
+ rafId = null;
161
+ lastUpdate = Date.now();
162
+ setNodes([...nodesCopy]);
163
+ setLinks([...linksCopy]);
164
+ setAlpha(simulation.alpha());
165
+ setIsRunning(simulation.alpha() > simulation.alphaMin());
166
+ });
167
+ }
168
+ };
169
+ simulation.on("tick", tickHandler);
51
170
  simulation.on("end", () => {
52
171
  setIsRunning(false);
53
172
  });
54
173
  return () => {
174
+ try {
175
+ simulation.on("tick", null);
176
+ } catch (e) {
177
+ }
178
+ if (stopTimeoutRef.current != null) {
179
+ try {
180
+ globalThis.clearTimeout(stopTimeoutRef.current);
181
+ } catch (e) {
182
+ }
183
+ stopTimeoutRef.current = null;
184
+ }
185
+ if (rafId != null) {
186
+ try {
187
+ (globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
188
+ } catch (e) {
189
+ }
190
+ rafId = null;
191
+ }
55
192
  simulation.stop();
56
193
  };
57
194
  }, [
58
- initialNodes,
59
- initialLinks,
195
+ nodesKey,
196
+ linksKey,
60
197
  chargeStrength,
61
198
  linkDistance,
62
199
  linkStrength,
@@ -67,12 +204,37 @@ function useForceSimulation(initialNodes, initialLinks, options) {
67
204
  height,
68
205
  alphaDecay,
69
206
  velocityDecay,
70
- onTick
207
+ alphaTarget,
208
+ alphaMin,
209
+ stabilizeOnStop,
210
+ tickThrottleMs,
211
+ maxSimulationTimeMs
71
212
  ]);
72
213
  const restart = () => {
73
214
  if (simulationRef.current) {
74
- simulationRef.current.alpha(1).restart();
215
+ try {
216
+ simulationRef.current.alphaTarget(warmAlpha).restart();
217
+ } catch (e) {
218
+ simulationRef.current.restart();
219
+ }
75
220
  setIsRunning(true);
221
+ if (stopTimeoutRef.current != null) {
222
+ try {
223
+ globalThis.clearTimeout(stopTimeoutRef.current);
224
+ } catch (e) {
225
+ }
226
+ stopTimeoutRef.current = null;
227
+ }
228
+ if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
229
+ stopTimeoutRef.current = globalThis.setTimeout(() => {
230
+ try {
231
+ simulationRef.current?.alpha(0);
232
+ simulationRef.current?.stop();
233
+ } catch (e) {
234
+ }
235
+ setIsRunning(false);
236
+ }, maxSimulationTimeMs);
237
+ }
76
238
  }
77
239
  };
78
240
  const stop = () => {
@@ -113,6 +275,107 @@ function useForceSimulation(initialNodes, initialLinks, options) {
113
275
  function cn(...inputs) {
114
276
  return twMerge(clsx(inputs));
115
277
  }
278
+ var NodeItem = ({
279
+ node,
280
+ isSelected,
281
+ isHovered,
282
+ pinned,
283
+ defaultNodeSize,
284
+ defaultNodeColor,
285
+ showLabel = true,
286
+ onClick,
287
+ onDoubleClick,
288
+ onMouseEnter,
289
+ onMouseLeave,
290
+ onMouseDown
291
+ }) => {
292
+ const nodeSize = node.size || defaultNodeSize;
293
+ const nodeColor = node.color || defaultNodeColor;
294
+ const x = node.x ?? 0;
295
+ const y = node.y ?? 0;
296
+ return /* @__PURE__ */ jsxs(
297
+ "g",
298
+ {
299
+ className: "cursor-pointer node",
300
+ "data-id": node.id,
301
+ transform: `translate(${x},${y})`,
302
+ onClick: () => onClick?.(node),
303
+ onDoubleClick: (e) => onDoubleClick?.(e, node),
304
+ onMouseEnter: () => onMouseEnter?.(node),
305
+ onMouseLeave: () => onMouseLeave?.(),
306
+ onMouseDown: (e) => onMouseDown?.(e, node),
307
+ children: [
308
+ /* @__PURE__ */ jsx(
309
+ "circle",
310
+ {
311
+ r: nodeSize,
312
+ fill: nodeColor,
313
+ stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
314
+ strokeWidth: pinned ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
315
+ opacity: isHovered || isSelected ? 1 : 0.9
316
+ }
317
+ ),
318
+ pinned && /* @__PURE__ */ jsx("circle", { r: nodeSize + 4, fill: "none", stroke: "#ff6b6b", strokeWidth: 1, opacity: 0.5, className: "pointer-events-none" }),
319
+ showLabel && node.label && /* @__PURE__ */ jsx("text", { y: nodeSize + 15, fill: "#333", fontSize: "12", textAnchor: "middle", dominantBaseline: "middle", pointerEvents: "none", className: "select-none", children: node.label })
320
+ ]
321
+ },
322
+ node.id
323
+ );
324
+ };
325
+ var NodeItem_default = NodeItem;
326
+ var LinkItem = ({ link, onClick, defaultWidth, showLabel = true, nodes = [] }) => {
327
+ const src = link.source?.id ?? (typeof link.source === "string" ? link.source : void 0);
328
+ const tgt = link.target?.id ?? (typeof link.target === "string" ? link.target : void 0);
329
+ const getNodePosition = (nodeOrId) => {
330
+ if (typeof nodeOrId === "object" && nodeOrId !== null) {
331
+ const node = nodeOrId;
332
+ return { x: node.x ?? 0, y: node.y ?? 0 };
333
+ } else if (typeof nodeOrId === "string") {
334
+ const found = nodes.find((n) => n.id === nodeOrId);
335
+ if (found) return { x: found.x ?? 0, y: found.y ?? 0 };
336
+ }
337
+ return null;
338
+ };
339
+ const sourcePos = getNodePosition(link.source);
340
+ const targetPos = getNodePosition(link.target);
341
+ if (!sourcePos || !targetPos) {
342
+ return null;
343
+ }
344
+ const midX = (sourcePos.x + targetPos.x) / 2;
345
+ const midY = (sourcePos.y + targetPos.y) / 2;
346
+ return /* @__PURE__ */ jsxs("g", { children: [
347
+ /* @__PURE__ */ jsx(
348
+ "line",
349
+ {
350
+ x1: sourcePos.x,
351
+ y1: sourcePos.y,
352
+ x2: targetPos.x,
353
+ y2: targetPos.y,
354
+ "data-source": src,
355
+ "data-target": tgt,
356
+ stroke: link.color,
357
+ strokeWidth: link.width ?? defaultWidth ?? 1,
358
+ opacity: 0.6,
359
+ className: "cursor-pointer transition-opacity hover:opacity-100",
360
+ onClick: () => onClick?.(link)
361
+ }
362
+ ),
363
+ showLabel && link.label && /* @__PURE__ */ jsx(
364
+ "text",
365
+ {
366
+ x: midX,
367
+ y: midY,
368
+ fill: "#666",
369
+ fontSize: "10",
370
+ textAnchor: "middle",
371
+ dominantBaseline: "middle",
372
+ pointerEvents: "none",
373
+ children: link.label
374
+ }
375
+ )
376
+ ] });
377
+ };
378
+ var LinkItem_default = LinkItem;
116
379
  var ForceDirectedGraph = forwardRef(
117
380
  ({
118
381
  nodes: initialNodes,
@@ -141,6 +404,7 @@ var ForceDirectedGraph = forwardRef(
141
404
  const svgRef = useRef(null);
142
405
  const gRef = useRef(null);
143
406
  const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
407
+ const transformRef = useRef(transform);
144
408
  const dragNodeRef = useRef(null);
145
409
  const dragActiveRef = useRef(false);
146
410
  const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
@@ -148,73 +412,177 @@ var ForceDirectedGraph = forwardRef(
148
412
  useEffect(() => {
149
413
  internalDragEnabledRef.current = enableDrag;
150
414
  }, [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;
415
+ const onTick = (_nodesCopy, _linksCopy, _sim) => {
416
+ try {
417
+ const boundsToUse = clusterBounds?.bounds ?? packageBounds;
418
+ const nodeClusterMap = clusterBounds?.nodeToCluster ?? {};
419
+ if (boundsToUse) {
420
+ Object.values(nodesById).forEach((n) => {
421
+ if (!n) return;
422
+ const group = n.group ?? n.packageGroup;
423
+ const clusterKey = nodeClusterMap[n.id];
424
+ const key = clusterKey ?? (group ? `pkg:${group}` : void 0);
425
+ if (!key) return;
426
+ const center = boundsToUse[key];
427
+ if (!center) return;
428
+ const dx = center.x - (n.x ?? 0);
429
+ const dy = center.y - (n.y ?? 0);
430
+ const dist = Math.sqrt(dx * dx + dy * dy);
431
+ const pullStrength = Math.min(0.5, 0.15 * (dist / (center.r || 200)) + 0.06);
432
+ if (!isNaN(pullStrength) && isFinite(pullStrength)) {
433
+ n.vx = (n.vx ?? 0) + dx / (dist || 1) * pullStrength;
434
+ n.vy = (n.vy ?? 0) + dy / (dist || 1) * pullStrength;
161
435
  }
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;
436
+ if (center.r && dist > center.r) {
437
+ const excess = (dist - center.r) / (dist || 1);
438
+ n.vx = (n.vx ?? 0) - dx * 0.02 * excess;
439
+ n.vy = (n.vy ?? 0) - dy * 0.02 * excess;
174
440
  }
175
- }
176
- } catch (e) {
441
+ });
177
442
  }
443
+ } catch (e) {
178
444
  }
179
- if (!effectiveBounds) return;
445
+ };
446
+ const { packageAreas, localPositions } = React.useMemo(() => {
180
447
  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;
448
+ if (!initialNodes || !initialNodes.length) return { packageAreas: {}, localPositions: {} };
449
+ const groups = /* @__PURE__ */ new Map();
450
+ initialNodes.forEach((n) => {
451
+ const key = n.packageGroup || n.group || "root";
452
+ if (!groups.has(key)) groups.set(key, []);
453
+ groups.get(key).push(n);
454
+ });
455
+ const groupKeys = Array.from(groups.keys());
456
+ const children = groupKeys.map((k) => ({ name: k, value: Math.max(1, groups.get(k).length) }));
457
+ const root = d32.hierarchy({ children });
458
+ root.sum((d) => d.value);
459
+ const pack2 = d32.pack().size([width, height]).padding(Math.max(20, Math.min(width, height) * 0.03));
460
+ const packed = pack2(root);
461
+ const packageAreas2 = {};
462
+ if (packed.children) {
463
+ packed.children.forEach((c) => {
464
+ const name = c.data.name;
465
+ packageAreas2[name] = { x: c.x, y: c.y, r: Math.max(40, c.r) };
466
+ });
467
+ }
468
+ const localPositions2 = {};
469
+ groups.forEach((nodesInGroup, _key) => {
470
+ if (!nodesInGroup || nodesInGroup.length === 0) return;
471
+ const localNodes = nodesInGroup.map((n) => ({ id: n.id, x: Math.random() * 10 - 5, y: Math.random() * 10 - 5, size: n.size || 10 }));
472
+ const localLinks = (initialLinks || []).filter((l) => {
473
+ const s = typeof l.source === "string" ? l.source : l.source && l.source.id;
474
+ const t = typeof l.target === "string" ? l.target : l.target && l.target.id;
475
+ return localNodes.some((ln) => ln.id === s) && localNodes.some((ln) => ln.id === t);
476
+ }).map((l) => ({ source: typeof l.source === "string" ? l.source : l.source.id, target: typeof l.target === "string" ? l.target : l.target.id }));
477
+ if (localNodes.length === 1) {
478
+ localPositions2[localNodes[0].id] = { x: 0, y: 0 };
479
+ return;
199
480
  }
481
+ const sim = d32.forceSimulation(localNodes).force("link", d32.forceLink(localLinks).id((d) => d.id).distance(30).strength(0.8)).force("charge", d32.forceManyBody().strength(-15)).force("collide", d32.forceCollide((d) => (d.size || 10) + 6).iterations(2)).stop();
482
+ const ticks = 300;
483
+ for (let i = 0; i < ticks; i++) sim.tick();
484
+ localNodes.forEach((ln) => {
485
+ localPositions2[ln.id] = { x: ln.x ?? 0, y: ln.y ?? 0 };
486
+ });
200
487
  });
488
+ return { packageAreas: packageAreas2, localPositions: localPositions2 };
201
489
  } catch (e) {
490
+ return { packageAreas: {}, localPositions: {} };
202
491
  }
203
- };
204
- const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(initialNodes, initialLinks, {
492
+ }, [initialNodes, initialLinks, width, height]);
493
+ const seededNodes = React.useMemo(() => {
494
+ if (!initialNodes || !Object.keys(packageAreas || {}).length) return initialNodes;
495
+ return initialNodes.map((n) => {
496
+ const key = n.packageGroup || n.group || "root";
497
+ const area = packageAreas[key];
498
+ const lp = localPositions[n.id];
499
+ if (!area || !lp) return n;
500
+ const scale = Math.max(0.5, area.r * 0.6 / (Math.max(1, Math.sqrt(lp.x * lp.x + lp.y * lp.y)) || 1));
501
+ return { ...n, x: area.x + lp.x * scale, y: area.y + lp.y * scale };
502
+ });
503
+ }, [initialNodes, packageAreas, localPositions]);
504
+ const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(seededNodes || initialNodes, initialLinks, {
205
505
  width,
206
506
  height,
207
507
  chargeStrength: manualLayout ? 0 : void 0,
208
508
  onTick,
209
509
  ...simulationOptions
210
510
  });
511
+ const nodesById = React.useMemo(() => {
512
+ const m = {};
513
+ (nodes || []).forEach((n) => {
514
+ if (n && n.id) m[n.id] = n;
515
+ });
516
+ return m;
517
+ }, [nodes]);
518
+ const clusterBounds = React.useMemo(() => {
519
+ try {
520
+ if (!links || !nodes) return null;
521
+ const nodeIds = new Set(nodes.map((n) => n.id));
522
+ const adj = /* @__PURE__ */ new Map();
523
+ nodes.forEach((n) => adj.set(n.id, /* @__PURE__ */ new Set()));
524
+ links.forEach((l) => {
525
+ const type = l.type || "reference";
526
+ if (type !== "dependency") return;
527
+ const s = typeof l.source === "string" ? l.source : l.source && l.source.id || null;
528
+ const t = typeof l.target === "string" ? l.target : l.target && l.target.id || null;
529
+ if (!s || !t) return;
530
+ if (!nodeIds.has(s) || !nodeIds.has(t)) return;
531
+ adj.get(s)?.add(t);
532
+ adj.get(t)?.add(s);
533
+ });
534
+ const visited = /* @__PURE__ */ new Set();
535
+ const comps = [];
536
+ for (const nid of nodeIds) {
537
+ if (visited.has(nid)) continue;
538
+ const stack = [nid];
539
+ const comp = [];
540
+ visited.add(nid);
541
+ while (stack.length) {
542
+ const cur = stack.pop();
543
+ comp.push(cur);
544
+ const neigh = adj.get(cur);
545
+ if (!neigh) continue;
546
+ for (const nb of neigh) {
547
+ if (!visited.has(nb)) {
548
+ visited.add(nb);
549
+ stack.push(nb);
550
+ }
551
+ }
552
+ }
553
+ comps.push(comp);
554
+ }
555
+ if (comps.length <= 1) return null;
556
+ const children = comps.map((c, i) => ({ name: String(i), value: Math.max(1, c.length) }));
557
+ d32.hierarchy({ children }).sum((d) => d.value).sort((a, b) => b.value - a.value);
558
+ const num = comps.length;
559
+ const cx = width / 2;
560
+ const cy = height / 2;
561
+ const base = Math.max(width, height);
562
+ const circleRadius = base * Math.max(30, num * 20, Math.sqrt(num) * 12);
563
+ const map = {};
564
+ comps.forEach((c, i) => {
565
+ const angle = 2 * Math.PI * i / num;
566
+ const x = cx + Math.cos(angle) * circleRadius;
567
+ const y = cy + Math.sin(angle) * circleRadius;
568
+ const sizeBias = Math.sqrt(Math.max(1, c.length));
569
+ const r = Math.max(200, 100 * sizeBias);
570
+ map[`cluster:${i}`] = { x, y, r };
571
+ });
572
+ const nodeToCluster = {};
573
+ comps.forEach((c, i) => c.forEach((nid) => nodeToCluster[nid] = `cluster:${i}`));
574
+ return { bounds: map, nodeToCluster };
575
+ } catch (e) {
576
+ return null;
577
+ }
578
+ }, [nodes, links, width, height]);
211
579
  useEffect(() => {
212
- if (!packageBounds) return;
580
+ if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
213
581
  try {
214
582
  restart();
215
583
  } catch (e) {
216
584
  }
217
- }, [packageBounds, restart]);
585
+ }, [packageBounds, clusterBounds, packageAreas, restart]);
218
586
  useEffect(() => {
219
587
  try {
220
588
  if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
@@ -302,6 +670,7 @@ var ForceDirectedGraph = forwardRef(
302
670
  const g = d32.select(gRef.current);
303
671
  const zoom2 = d32.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
304
672
  g.attr("transform", event.transform);
673
+ transformRef.current = event.transform;
305
674
  setTransform(event.transform);
306
675
  });
307
676
  svg.call(zoom2);
@@ -309,6 +678,26 @@ var ForceDirectedGraph = forwardRef(
309
678
  svg.on(".zoom", null);
310
679
  };
311
680
  }, [enableZoom]);
681
+ useEffect(() => {
682
+ if (!gRef.current) return;
683
+ try {
684
+ const g = d32.select(gRef.current);
685
+ g.selectAll("g.node").each(function() {
686
+ const datum = d32.select(this).datum();
687
+ if (!datum) return;
688
+ d32.select(this).attr("transform", `translate(${datum.x || 0},${datum.y || 0})`);
689
+ });
690
+ g.selectAll("line").each(function() {
691
+ const l = d32.select(this).datum();
692
+ if (!l) return;
693
+ const s = typeof l.source === "object" ? l.source : nodes.find((n) => n.id === l.source) || l.source;
694
+ const t = typeof l.target === "object" ? l.target : nodes.find((n) => n.id === l.target) || l.target;
695
+ if (!s || !t) return;
696
+ d32.select(this).attr("x1", s.x).attr("y1", s.y).attr("x2", t.x).attr("y2", t.y);
697
+ });
698
+ } catch (e) {
699
+ }
700
+ }, [nodes, links]);
312
701
  const handleDragStart = useCallback(
313
702
  (event, node) => {
314
703
  if (!enableDrag) return;
@@ -333,8 +722,9 @@ var ForceDirectedGraph = forwardRef(
333
722
  const svg = svgRef.current;
334
723
  if (!svg) return;
335
724
  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;
725
+ const t = transformRef.current;
726
+ const x = (event.clientX - rect.left - t.x) / t.k;
727
+ const y = (event.clientY - rect.top - t.y) / t.k;
338
728
  dragNodeRef.current.fx = x;
339
729
  dragNodeRef.current.fy = y;
340
730
  };
@@ -361,7 +751,7 @@ var ForceDirectedGraph = forwardRef(
361
751
  window.removeEventListener("mouseout", handleWindowLeave);
362
752
  window.removeEventListener("blur", handleWindowUp);
363
753
  };
364
- }, [enableDrag, transform]);
754
+ }, [enableDrag]);
365
755
  useEffect(() => {
366
756
  if (!gRef.current || !enableDrag) return;
367
757
  const g = d32.select(gRef.current);
@@ -484,98 +874,35 @@ var ForceDirectedGraph = forwardRef(
484
874
  }
485
875
  ) }),
486
876
  /* @__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
- }),
877
+ links.map((link, i) => /* @__PURE__ */ jsx(
878
+ LinkItem_default,
879
+ {
880
+ link,
881
+ onClick: handleLinkClick,
882
+ defaultWidth: defaultLinkWidth,
883
+ showLabel: showLinkLabels,
884
+ nodes
885
+ },
886
+ `link-${i}`
887
+ )),
888
+ nodes.map((node) => /* @__PURE__ */ jsx(
889
+ NodeItem_default,
890
+ {
891
+ node,
892
+ isSelected: selectedNodeId === node.id,
893
+ isHovered: hoveredNodeId === node.id,
894
+ pinned: pinnedNodes.has(node.id),
895
+ defaultNodeSize,
896
+ defaultNodeColor,
897
+ showLabel: showNodeLabels,
898
+ onClick: handleNodeClick,
899
+ onDoubleClick: handleNodeDoubleClick,
900
+ onMouseEnter: handleNodeMouseEnter,
901
+ onMouseLeave: handleNodeMouseLeave,
902
+ onMouseDown: handleDragStart
903
+ },
904
+ node.id
905
+ )),
579
906
  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: [
580
907
  /* @__PURE__ */ jsx(
581
908
  "circle",