@aiready/components 0.1.0 → 0.1.4

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 React, { 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,39 +16,184 @@ 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
+ 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
20
32
  } = options;
21
33
  const [nodes, setNodes] = useState(initialNodes);
22
34
  const [links, setLinks] = useState(initialLinks);
23
35
  const [isRunning, setIsRunning] = useState(false);
24
36
  const [alpha, setAlpha] = useState(1);
25
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("|");
26
45
  useEffect(() => {
27
46
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
28
47
  const linksCopy = initialLinks.map((link) => ({ ...link }));
29
- const simulation = d3.forceSimulation(nodesCopy).force(
30
- "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(
33
- "collision",
34
- d3.forceCollide().radius(collisionRadius).strength(collisionStrength)
35
- ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
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) => {
80
+ const nodeSize = d && d.size ? d.size : 10;
81
+ return nodeSize + collisionRadius;
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
+ }
36
99
  simulationRef.current = simulation;
37
- simulation.on("tick", () => {
38
- setNodes([...nodesCopy]);
39
- setLinks([...linksCopy]);
40
- setAlpha(simulation.alpha());
41
- setIsRunning(simulation.alpha() > simulation.alphaMin());
42
- });
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 = () => {
130
+ try {
131
+ if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
132
+ } catch (e) {
133
+ }
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);
43
170
  simulation.on("end", () => {
44
171
  setIsRunning(false);
45
172
  });
46
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
+ }
47
192
  simulation.stop();
48
193
  };
49
194
  }, [
50
- initialNodes,
51
- initialLinks,
195
+ nodesKey,
196
+ linksKey,
52
197
  chargeStrength,
53
198
  linkDistance,
54
199
  linkStrength,
@@ -58,12 +203,38 @@ function useForceSimulation(initialNodes, initialLinks, options) {
58
203
  width,
59
204
  height,
60
205
  alphaDecay,
61
- velocityDecay
206
+ velocityDecay,
207
+ alphaTarget,
208
+ alphaMin,
209
+ stabilizeOnStop,
210
+ tickThrottleMs,
211
+ maxSimulationTimeMs
62
212
  ]);
63
213
  const restart = () => {
64
214
  if (simulationRef.current) {
65
- simulationRef.current.alpha(1).restart();
215
+ try {
216
+ simulationRef.current.alphaTarget(warmAlpha).restart();
217
+ } catch (e) {
218
+ simulationRef.current.restart();
219
+ }
66
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
+ }
67
238
  }
68
239
  };
69
240
  const stop = () => {
@@ -72,221 +243,699 @@ function useForceSimulation(initialNodes, initialLinks, options) {
72
243
  setIsRunning(false);
73
244
  }
74
245
  };
246
+ const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });
247
+ const forcesEnabledRef = useRef(true);
248
+ const setForcesEnabled = (enabled) => {
249
+ const sim = simulationRef.current;
250
+ if (!sim) return;
251
+ if (forcesEnabledRef.current === enabled) return;
252
+ forcesEnabledRef.current = enabled;
253
+ try {
254
+ const charge = sim.force("charge");
255
+ if (charge && typeof charge.strength === "function") {
256
+ charge.strength(enabled ? originalForcesRef.current.charge : 0);
257
+ }
258
+ const link = sim.force("link");
259
+ if (link && typeof link.strength === "function") {
260
+ link.strength(enabled ? originalForcesRef.current.link : 0);
261
+ }
262
+ } catch (e) {
263
+ }
264
+ };
75
265
  return {
76
266
  nodes,
77
267
  links,
78
268
  restart,
79
269
  stop,
80
270
  isRunning,
81
- alpha
271
+ alpha,
272
+ setForcesEnabled
82
273
  };
83
274
  }
84
275
  function cn(...inputs) {
85
276
  return twMerge(clsx(inputs));
86
277
  }
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
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
107
291
  }) => {
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, {
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;
379
+ var ForceDirectedGraph = forwardRef(
380
+ ({
381
+ nodes: initialNodes,
382
+ links: initialLinks,
112
383
  width,
113
384
  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);
385
+ simulationOptions,
386
+ enableZoom = true,
387
+ enableDrag = true,
388
+ onNodeClick,
389
+ onNodeHover,
390
+ onLinkClick,
391
+ selectedNodeId,
392
+ hoveredNodeId,
393
+ defaultNodeColor = "#69b3a2",
394
+ defaultNodeSize = 10,
395
+ defaultLinkColor = "#999",
396
+ defaultLinkWidth = 1,
397
+ showNodeLabels = true,
398
+ showLinkLabels = false,
399
+ className,
400
+ manualLayout = false,
401
+ onManualLayoutChange,
402
+ packageBounds
403
+ }, ref) => {
404
+ const svgRef = useRef(null);
405
+ const gRef = useRef(null);
406
+ const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
407
+ const transformRef = useRef(transform);
408
+ const dragNodeRef = useRef(null);
409
+ const dragActiveRef = useRef(false);
410
+ const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
411
+ const internalDragEnabledRef = useRef(enableDrag);
412
+ useEffect(() => {
413
+ internalDragEnabledRef.current = enableDrag;
414
+ }, [enableDrag]);
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;
435
+ }
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;
440
+ }
441
+ });
442
+ }
443
+ } catch (e) {
444
+ }
127
445
  };
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,
446
+ const { packageAreas, localPositions } = React.useMemo(() => {
447
+ try {
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;
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
+ });
487
+ });
488
+ return { packageAreas: packageAreas2, localPositions: localPositions2 };
489
+ } catch (e) {
490
+ return { packageAreas: {}, localPositions: {} };
491
+ }
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, {
186
505
  width,
187
506
  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 })
507
+ chargeStrength: manualLayout ? 0 : void 0,
508
+ onTick,
509
+ ...simulationOptions
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
+ }
201
552
  }
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: [
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]);
579
+ useEffect(() => {
580
+ if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
581
+ try {
582
+ restart();
583
+ } catch (e) {
584
+ }
585
+ }, [packageBounds, clusterBounds, packageAreas, restart]);
586
+ useEffect(() => {
587
+ try {
588
+ if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
589
+ else setForcesEnabled(true);
590
+ } catch (e) {
591
+ }
592
+ }, [manualLayout, pinnedNodes, setForcesEnabled]);
593
+ useImperativeHandle(
594
+ ref,
595
+ () => ({
596
+ pinAll: () => {
597
+ const newPinned = /* @__PURE__ */ new Set();
598
+ nodes.forEach((node) => {
599
+ node.fx = node.x;
600
+ node.fy = node.y;
601
+ newPinned.add(node.id);
602
+ });
603
+ setPinnedNodes(newPinned);
604
+ restart();
605
+ },
606
+ unpinAll: () => {
607
+ nodes.forEach((node) => {
608
+ node.fx = null;
609
+ node.fy = null;
610
+ });
611
+ setPinnedNodes(/* @__PURE__ */ new Set());
612
+ restart();
613
+ },
614
+ resetLayout: () => {
615
+ nodes.forEach((node) => {
616
+ node.fx = null;
617
+ node.fy = null;
618
+ });
619
+ setPinnedNodes(/* @__PURE__ */ new Set());
620
+ restart();
621
+ },
622
+ fitView: () => {
623
+ if (!svgRef.current || !nodes.length) return;
624
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
625
+ nodes.forEach((node) => {
626
+ if (node.x !== void 0 && node.y !== void 0) {
627
+ const size = node.size || 10;
628
+ minX = Math.min(minX, node.x - size);
629
+ maxX = Math.max(maxX, node.x + size);
630
+ minY = Math.min(minY, node.y - size);
631
+ maxY = Math.max(maxY, node.y + size);
632
+ }
633
+ });
634
+ if (!isFinite(minX)) return;
635
+ const padding = 40;
636
+ const nodeWidth = maxX - minX;
637
+ const nodeHeight = maxY - minY;
638
+ const scale = Math.min(
639
+ (width - padding * 2) / nodeWidth,
640
+ (height - padding * 2) / nodeHeight,
641
+ 10
642
+ );
643
+ const centerX = (minX + maxX) / 2;
644
+ const centerY = (minY + maxY) / 2;
645
+ const x = width / 2 - centerX * scale;
646
+ const y = height / 2 - centerY * scale;
647
+ if (gRef.current && svgRef.current) {
648
+ const svg = d32.select(svgRef.current);
649
+ const newTransform = d32.zoomIdentity.translate(x, y).scale(scale);
650
+ svg.transition().duration(300).call(d32.zoom().transform, newTransform);
651
+ setTransform(newTransform);
652
+ }
653
+ },
654
+ getPinnedNodes: () => Array.from(pinnedNodes),
655
+ setDragMode: (enabled) => {
656
+ internalDragEnabledRef.current = enabled;
657
+ }
658
+ }),
659
+ [nodes, pinnedNodes, restart, width, height]
660
+ );
661
+ useEffect(() => {
662
+ try {
663
+ if (typeof onManualLayoutChange === "function") onManualLayoutChange(manualLayout);
664
+ } catch (e) {
665
+ }
666
+ }, [manualLayout, onManualLayoutChange]);
667
+ useEffect(() => {
668
+ if (!enableZoom || !svgRef.current || !gRef.current) return;
669
+ const svg = d32.select(svgRef.current);
670
+ const g = d32.select(gRef.current);
671
+ const zoom2 = d32.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
672
+ g.attr("transform", event.transform);
673
+ transformRef.current = event.transform;
674
+ setTransform(event.transform);
675
+ });
676
+ svg.call(zoom2);
677
+ return () => {
678
+ svg.on(".zoom", null);
679
+ };
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]);
701
+ const handleDragStart = useCallback(
702
+ (event, node) => {
703
+ if (!enableDrag) return;
704
+ event.preventDefault();
705
+ event.stopPropagation();
706
+ dragActiveRef.current = true;
707
+ dragNodeRef.current = node;
708
+ node.fx = node.x;
709
+ node.fy = node.y;
710
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
711
+ try {
712
+ stop();
713
+ } catch (e) {
714
+ }
715
+ },
716
+ [enableDrag, restart]
717
+ );
718
+ useEffect(() => {
719
+ if (!enableDrag) return;
720
+ const handleWindowMove = (event) => {
721
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
722
+ const svg = svgRef.current;
723
+ if (!svg) return;
724
+ const rect = svg.getBoundingClientRect();
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;
728
+ dragNodeRef.current.fx = x;
729
+ dragNodeRef.current.fy = y;
730
+ };
731
+ const handleWindowUp = () => {
732
+ if (!dragActiveRef.current) return;
733
+ try {
734
+ setForcesEnabled(true);
735
+ restart();
736
+ } catch (e) {
737
+ }
738
+ dragNodeRef.current = null;
739
+ dragActiveRef.current = false;
740
+ };
741
+ const handleWindowLeave = (event) => {
742
+ if (event.relatedTarget === null) handleWindowUp();
743
+ };
744
+ window.addEventListener("mousemove", handleWindowMove);
745
+ window.addEventListener("mouseup", handleWindowUp);
746
+ window.addEventListener("mouseout", handleWindowLeave);
747
+ window.addEventListener("blur", handleWindowUp);
748
+ return () => {
749
+ window.removeEventListener("mousemove", handleWindowMove);
750
+ window.removeEventListener("mouseup", handleWindowUp);
751
+ window.removeEventListener("mouseout", handleWindowLeave);
752
+ window.removeEventListener("blur", handleWindowUp);
753
+ };
754
+ }, [enableDrag]);
755
+ useEffect(() => {
756
+ if (!gRef.current || !enableDrag) return;
757
+ const g = d32.select(gRef.current);
758
+ const dragBehavior = d32.drag().on("start", function(event) {
759
+ try {
760
+ const target = event.sourceEvent && event.sourceEvent.target || event.target;
761
+ const grp = target.closest?.("g.node");
762
+ const id = grp?.getAttribute("data-id");
763
+ if (!id) return;
764
+ const node = nodes.find((n) => n.id === id);
765
+ if (!node) return;
766
+ if (!internalDragEnabledRef.current) return;
767
+ if (!event.active) restart();
768
+ dragActiveRef.current = true;
769
+ dragNodeRef.current = node;
770
+ node.fx = node.x;
771
+ node.fy = node.y;
772
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
773
+ } catch (e) {
774
+ }
775
+ }).on("drag", function(event) {
776
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
777
+ const svg = svgRef.current;
778
+ if (!svg) return;
779
+ const rect = svg.getBoundingClientRect();
780
+ const x = (event.sourceEvent.clientX - rect.left - transform.x) / transform.k;
781
+ const y = (event.sourceEvent.clientY - rect.top - transform.y) / transform.k;
782
+ dragNodeRef.current.fx = x;
783
+ dragNodeRef.current.fy = y;
784
+ }).on("end", function() {
785
+ try {
786
+ setForcesEnabled(true);
787
+ restart();
788
+ } catch (e) {
789
+ }
790
+ dragNodeRef.current = null;
791
+ dragActiveRef.current = false;
792
+ });
793
+ try {
794
+ g.selectAll("g.node").call(dragBehavior);
795
+ } catch (e) {
796
+ }
797
+ return () => {
798
+ try {
799
+ g.selectAll("g.node").on(".drag", null);
800
+ } catch (e) {
801
+ }
802
+ };
803
+ }, [gRef, enableDrag, nodes, transform, restart]);
804
+ const handleNodeClick = useCallback(
805
+ (node) => {
806
+ onNodeClick?.(node);
807
+ },
808
+ [onNodeClick]
809
+ );
810
+ const handleNodeDoubleClick = useCallback(
811
+ (event, node) => {
812
+ event.stopPropagation();
813
+ if (!enableDrag) return;
814
+ if (node.fx === null || node.fx === void 0) {
815
+ node.fx = node.x;
816
+ node.fy = node.y;
817
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
818
+ } else {
819
+ node.fx = null;
820
+ node.fy = null;
821
+ setPinnedNodes((prev) => {
822
+ const next = new Set(prev);
823
+ next.delete(node.id);
824
+ return next;
825
+ });
826
+ }
827
+ restart();
828
+ },
829
+ [enableDrag, restart]
830
+ );
831
+ const handleCanvasDoubleClick = useCallback(() => {
832
+ nodes.forEach((node) => {
833
+ node.fx = null;
834
+ node.fy = null;
835
+ });
836
+ setPinnedNodes(/* @__PURE__ */ new Set());
837
+ restart();
838
+ }, [nodes, restart]);
839
+ const handleNodeMouseEnter = useCallback(
840
+ (node) => {
841
+ onNodeHover?.(node);
842
+ },
843
+ [onNodeHover]
844
+ );
845
+ const handleNodeMouseLeave = useCallback(() => {
846
+ onNodeHover?.(null);
847
+ }, [onNodeHover]);
848
+ const handleLinkClick = useCallback(
849
+ (link) => {
850
+ onLinkClick?.(link);
851
+ },
852
+ [onLinkClick]
853
+ );
854
+ return /* @__PURE__ */ jsxs(
855
+ "svg",
856
+ {
857
+ ref: svgRef,
858
+ width,
859
+ height,
860
+ className: cn("bg-white dark:bg-gray-900", className),
861
+ onDoubleClick: handleCanvasDoubleClick,
862
+ children: [
863
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
864
+ "marker",
865
+ {
866
+ id: "arrow",
867
+ viewBox: "0 0 10 10",
868
+ refX: "20",
869
+ refY: "5",
870
+ markerWidth: "6",
871
+ markerHeight: "6",
872
+ orient: "auto",
873
+ children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
874
+ }
875
+ ) }),
876
+ /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
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
+ )),
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: [
209
907
  /* @__PURE__ */ jsx(
210
- "line",
908
+ "circle",
211
909
  {
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)
910
+ cx: b.x,
911
+ cy: b.y,
912
+ r: b.r,
913
+ fill: "rgba(148,163,184,0.06)",
914
+ stroke: "#475569",
915
+ strokeWidth: 2,
916
+ strokeDasharray: "6 6",
917
+ opacity: 0.9
221
918
  }
222
919
  ),
223
- showLinkLabels && link.label && /* @__PURE__ */ jsx(
920
+ /* @__PURE__ */ jsx(
224
921
  "text",
225
922
  {
226
- x: (source.x + target.x) / 2,
227
- y: (source.y + target.y) / 2,
228
- fill: "#666",
229
- fontSize: "10",
923
+ x: b.x,
924
+ y: Math.max(12, b.y - b.r + 14),
925
+ fill: "#475569",
926
+ fontSize: 11,
230
927
  textAnchor: "middle",
231
- dominantBaseline: "middle",
232
928
  pointerEvents: "none",
233
- children: link.label
929
+ children: pid.replace(/^pkg:/, "")
234
930
  }
235
931
  )
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
- };
932
+ ] }, pid)) })
933
+ ] })
934
+ ]
935
+ }
936
+ );
937
+ }
938
+ );
290
939
  ForceDirectedGraph.displayName = "ForceDirectedGraph";
291
940
 
292
941
  export { ForceDirectedGraph };