@aiready/components 0.1.3 → 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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React2 from 'react';
2
- import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
2
+ import React2__default, { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
3
3
  import { cva } from 'class-variance-authority';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
@@ -630,46 +630,183 @@ function useForceSimulation(initialNodes, initialLinks, options) {
630
630
  height,
631
631
  alphaDecay = 0.0228,
632
632
  velocityDecay = 0.4,
633
- onTick
633
+ alphaTarget = 0,
634
+ warmAlpha = 0.3,
635
+ alphaMin = 0.01,
636
+ // @ts-ignore allow extra option
637
+ stabilizeOnStop = true,
638
+ onTick,
639
+ // Optional throttle in milliseconds for tick updates (reduce React re-renders)
640
+ // Lower values = smoother but more CPU; default ~30ms (~33fps)
641
+ // @ts-ignore allow extra option
642
+ tickThrottleMs = 33,
643
+ // @ts-ignore allow extra option
644
+ maxSimulationTimeMs = 3e3
634
645
  } = options;
635
646
  const [nodes, setNodes] = useState(initialNodes);
636
647
  const [links, setLinks] = useState(initialLinks);
637
648
  const [isRunning, setIsRunning] = useState(false);
638
649
  const [alpha, setAlpha] = useState(1);
639
650
  const simulationRef = useRef(null);
651
+ const stopTimeoutRef = useRef(null);
652
+ const nodesKey = initialNodes.map((n) => n.id).join("|");
653
+ const linksKey = (initialLinks || []).map((l) => {
654
+ const s = typeof l.source === "string" ? l.source : l.source?.id;
655
+ const t = typeof l.target === "string" ? l.target : l.target?.id;
656
+ return `${s}->${t}:${l.type || ""}`;
657
+ }).join("|");
640
658
  useEffect(() => {
641
659
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
642
660
  const linksCopy = initialLinks.map((link) => ({ ...link }));
643
- const simulation = d33.forceSimulation(nodesCopy).force(
644
- "link",
645
- d33.forceLink(linksCopy).id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength)
646
- ).force("charge", d33.forceManyBody().strength(chargeStrength)).force("center", d33.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
647
- "collision",
648
- d33.forceCollide().radius((d) => {
661
+ try {
662
+ nodesCopy.forEach((n, i) => {
663
+ const angle = i * 2 * Math.PI / nodesCopy.length;
664
+ const radius = Math.min(width, height) * 0.45;
665
+ n.x = width / 2 + radius * Math.cos(angle);
666
+ n.y = height / 2 + radius * Math.sin(angle);
667
+ n.vx = (Math.random() - 0.5) * 2;
668
+ n.vy = (Math.random() - 0.5) * 2;
669
+ });
670
+ } catch (e) {
671
+ nodesCopy.forEach((n) => {
672
+ n.x = Math.random() * width;
673
+ n.y = Math.random() * height;
674
+ n.vx = (Math.random() - 0.5) * 10;
675
+ n.vy = (Math.random() - 0.5) * 10;
676
+ });
677
+ }
678
+ const simulation = d33.forceSimulation(nodesCopy);
679
+ try {
680
+ const linkForce = d33.forceLink(linksCopy);
681
+ linkForce.id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength);
682
+ simulation.force("link", linkForce);
683
+ } catch (e) {
684
+ try {
685
+ simulation.force("link", d33.forceLink(linksCopy));
686
+ } catch (e2) {
687
+ }
688
+ }
689
+ try {
690
+ simulation.force("charge", d33.forceManyBody().strength(chargeStrength));
691
+ simulation.force("center", d33.forceCenter(width / 2, height / 2).strength(centerStrength));
692
+ const collide = d33.forceCollide().radius((d) => {
649
693
  const nodeSize = d && d.size ? d.size : 10;
650
694
  return nodeSize + collisionRadius;
651
- }).strength(collisionStrength)
652
- ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
695
+ }).strength(collisionStrength);
696
+ simulation.force("collision", collide);
697
+ simulation.force("x", d33.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5)));
698
+ simulation.force("y", d33.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
699
+ simulation.alphaDecay(alphaDecay);
700
+ simulation.velocityDecay(velocityDecay);
701
+ simulation.alphaMin(alphaMin);
702
+ try {
703
+ simulation.alphaTarget(alphaTarget);
704
+ } catch (e) {
705
+ }
706
+ try {
707
+ simulation.alpha(warmAlpha);
708
+ } catch (e) {
709
+ }
710
+ } catch (e) {
711
+ }
653
712
  simulationRef.current = simulation;
654
- simulation.on("tick", () => {
713
+ if (stopTimeoutRef.current != null) {
714
+ try {
715
+ globalThis.clearTimeout(stopTimeoutRef.current);
716
+ } catch (e) {
717
+ }
718
+ stopTimeoutRef.current = null;
719
+ }
720
+ if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
721
+ stopTimeoutRef.current = globalThis.setTimeout(() => {
722
+ try {
723
+ if (stabilizeOnStop) {
724
+ nodesCopy.forEach((n) => {
725
+ n.vx = 0;
726
+ n.vy = 0;
727
+ if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
728
+ if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
729
+ });
730
+ }
731
+ simulation.alpha(0);
732
+ simulation.stop();
733
+ } catch (e) {
734
+ }
735
+ setIsRunning(false);
736
+ setNodes([...nodesCopy]);
737
+ setLinks([...linksCopy]);
738
+ }, maxSimulationTimeMs);
739
+ }
740
+ let rafId = null;
741
+ let lastUpdate = 0;
742
+ const tickHandler = () => {
655
743
  try {
656
744
  if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
657
745
  } catch (e) {
658
746
  }
659
- setNodes([...nodesCopy]);
660
- setLinks([...linksCopy]);
661
- setAlpha(simulation.alpha());
662
- setIsRunning(simulation.alpha() > simulation.alphaMin());
663
- });
747
+ try {
748
+ if (simulation.alpha() <= alphaMin) {
749
+ try {
750
+ if (stabilizeOnStop) {
751
+ nodesCopy.forEach((n) => {
752
+ n.vx = 0;
753
+ n.vy = 0;
754
+ if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
755
+ if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
756
+ });
757
+ }
758
+ simulation.stop();
759
+ } catch (e) {
760
+ }
761
+ setAlpha(simulation.alpha());
762
+ setIsRunning(false);
763
+ setNodes([...nodesCopy]);
764
+ setLinks([...linksCopy]);
765
+ return;
766
+ }
767
+ } catch (e) {
768
+ }
769
+ const now = Date.now();
770
+ const shouldUpdate = now - lastUpdate >= tickThrottleMs;
771
+ if (rafId == null && shouldUpdate) {
772
+ rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
773
+ rafId = null;
774
+ lastUpdate = Date.now();
775
+ setNodes([...nodesCopy]);
776
+ setLinks([...linksCopy]);
777
+ setAlpha(simulation.alpha());
778
+ setIsRunning(simulation.alpha() > simulation.alphaMin());
779
+ });
780
+ }
781
+ };
782
+ simulation.on("tick", tickHandler);
664
783
  simulation.on("end", () => {
665
784
  setIsRunning(false);
666
785
  });
667
786
  return () => {
787
+ try {
788
+ simulation.on("tick", null);
789
+ } catch (e) {
790
+ }
791
+ if (stopTimeoutRef.current != null) {
792
+ try {
793
+ globalThis.clearTimeout(stopTimeoutRef.current);
794
+ } catch (e) {
795
+ }
796
+ stopTimeoutRef.current = null;
797
+ }
798
+ if (rafId != null) {
799
+ try {
800
+ (globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
801
+ } catch (e) {
802
+ }
803
+ rafId = null;
804
+ }
668
805
  simulation.stop();
669
806
  };
670
807
  }, [
671
- initialNodes,
672
- initialLinks,
808
+ nodesKey,
809
+ linksKey,
673
810
  chargeStrength,
674
811
  linkDistance,
675
812
  linkStrength,
@@ -680,12 +817,37 @@ function useForceSimulation(initialNodes, initialLinks, options) {
680
817
  height,
681
818
  alphaDecay,
682
819
  velocityDecay,
683
- onTick
820
+ alphaTarget,
821
+ alphaMin,
822
+ stabilizeOnStop,
823
+ tickThrottleMs,
824
+ maxSimulationTimeMs
684
825
  ]);
685
826
  const restart = () => {
686
827
  if (simulationRef.current) {
687
- simulationRef.current.alpha(1).restart();
828
+ try {
829
+ simulationRef.current.alphaTarget(warmAlpha).restart();
830
+ } catch (e) {
831
+ simulationRef.current.restart();
832
+ }
688
833
  setIsRunning(true);
834
+ if (stopTimeoutRef.current != null) {
835
+ try {
836
+ globalThis.clearTimeout(stopTimeoutRef.current);
837
+ } catch (e) {
838
+ }
839
+ stopTimeoutRef.current = null;
840
+ }
841
+ if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
842
+ stopTimeoutRef.current = globalThis.setTimeout(() => {
843
+ try {
844
+ simulationRef.current?.alpha(0);
845
+ simulationRef.current?.stop();
846
+ } catch (e) {
847
+ }
848
+ setIsRunning(false);
849
+ }, maxSimulationTimeMs);
850
+ }
689
851
  }
690
852
  };
691
853
  const stop = () => {
@@ -746,6 +908,107 @@ function useDrag(simulation) {
746
908
  onDragEnd: dragEnded
747
909
  };
748
910
  }
911
+ var NodeItem = ({
912
+ node,
913
+ isSelected,
914
+ isHovered,
915
+ pinned,
916
+ defaultNodeSize,
917
+ defaultNodeColor,
918
+ showLabel = true,
919
+ onClick,
920
+ onDoubleClick,
921
+ onMouseEnter,
922
+ onMouseLeave,
923
+ onMouseDown
924
+ }) => {
925
+ const nodeSize = node.size || defaultNodeSize;
926
+ const nodeColor = node.color || defaultNodeColor;
927
+ const x = node.x ?? 0;
928
+ const y = node.y ?? 0;
929
+ return /* @__PURE__ */ jsxs(
930
+ "g",
931
+ {
932
+ className: "cursor-pointer node",
933
+ "data-id": node.id,
934
+ transform: `translate(${x},${y})`,
935
+ onClick: () => onClick?.(node),
936
+ onDoubleClick: (e) => onDoubleClick?.(e, node),
937
+ onMouseEnter: () => onMouseEnter?.(node),
938
+ onMouseLeave: () => onMouseLeave?.(),
939
+ onMouseDown: (e) => onMouseDown?.(e, node),
940
+ children: [
941
+ /* @__PURE__ */ jsx(
942
+ "circle",
943
+ {
944
+ r: nodeSize,
945
+ fill: nodeColor,
946
+ stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
947
+ strokeWidth: pinned ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
948
+ opacity: isHovered || isSelected ? 1 : 0.9
949
+ }
950
+ ),
951
+ pinned && /* @__PURE__ */ jsx("circle", { r: nodeSize + 4, fill: "none", stroke: "#ff6b6b", strokeWidth: 1, opacity: 0.5, className: "pointer-events-none" }),
952
+ 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 })
953
+ ]
954
+ },
955
+ node.id
956
+ );
957
+ };
958
+ var NodeItem_default = NodeItem;
959
+ var LinkItem = ({ link, onClick, defaultWidth, showLabel = true, nodes = [] }) => {
960
+ const src = link.source?.id ?? (typeof link.source === "string" ? link.source : void 0);
961
+ const tgt = link.target?.id ?? (typeof link.target === "string" ? link.target : void 0);
962
+ const getNodePosition = (nodeOrId) => {
963
+ if (typeof nodeOrId === "object" && nodeOrId !== null) {
964
+ const node = nodeOrId;
965
+ return { x: node.x ?? 0, y: node.y ?? 0 };
966
+ } else if (typeof nodeOrId === "string") {
967
+ const found = nodes.find((n) => n.id === nodeOrId);
968
+ if (found) return { x: found.x ?? 0, y: found.y ?? 0 };
969
+ }
970
+ return null;
971
+ };
972
+ const sourcePos = getNodePosition(link.source);
973
+ const targetPos = getNodePosition(link.target);
974
+ if (!sourcePos || !targetPos) {
975
+ return null;
976
+ }
977
+ const midX = (sourcePos.x + targetPos.x) / 2;
978
+ const midY = (sourcePos.y + targetPos.y) / 2;
979
+ return /* @__PURE__ */ jsxs("g", { children: [
980
+ /* @__PURE__ */ jsx(
981
+ "line",
982
+ {
983
+ x1: sourcePos.x,
984
+ y1: sourcePos.y,
985
+ x2: targetPos.x,
986
+ y2: targetPos.y,
987
+ "data-source": src,
988
+ "data-target": tgt,
989
+ stroke: link.color,
990
+ strokeWidth: link.width ?? defaultWidth ?? 1,
991
+ opacity: 0.6,
992
+ className: "cursor-pointer transition-opacity hover:opacity-100",
993
+ onClick: () => onClick?.(link)
994
+ }
995
+ ),
996
+ showLabel && link.label && /* @__PURE__ */ jsx(
997
+ "text",
998
+ {
999
+ x: midX,
1000
+ y: midY,
1001
+ fill: "#666",
1002
+ fontSize: "10",
1003
+ textAnchor: "middle",
1004
+ dominantBaseline: "middle",
1005
+ pointerEvents: "none",
1006
+ children: link.label
1007
+ }
1008
+ )
1009
+ ] });
1010
+ };
1011
+ var LinkItem_default = LinkItem;
749
1012
  var ForceDirectedGraph = forwardRef(
750
1013
  ({
751
1014
  nodes: initialNodes,
@@ -774,6 +1037,7 @@ var ForceDirectedGraph = forwardRef(
774
1037
  const svgRef = useRef(null);
775
1038
  const gRef = useRef(null);
776
1039
  const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
1040
+ const transformRef = useRef(transform);
777
1041
  const dragNodeRef = useRef(null);
778
1042
  const dragActiveRef = useRef(false);
779
1043
  const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
@@ -781,73 +1045,177 @@ var ForceDirectedGraph = forwardRef(
781
1045
  useEffect(() => {
782
1046
  internalDragEnabledRef.current = enableDrag;
783
1047
  }, [enableDrag]);
784
- const onTick = (nodesCopy, _linksCopy, _sim) => {
785
- const bounds = packageBounds && Object.keys(packageBounds).length ? packageBounds : void 0;
786
- let effectiveBounds = bounds;
787
- if (!effectiveBounds) {
788
- try {
789
- const counts = {};
790
- (initialNodes || []).forEach((n) => {
791
- if (n && n.kind === "file") {
792
- const g = n.packageGroup || "root";
793
- counts[g] = (counts[g] || 0) + 1;
1048
+ const onTick = (_nodesCopy, _linksCopy, _sim) => {
1049
+ try {
1050
+ const boundsToUse = clusterBounds?.bounds ?? packageBounds;
1051
+ const nodeClusterMap = clusterBounds?.nodeToCluster ?? {};
1052
+ if (boundsToUse) {
1053
+ Object.values(nodesById).forEach((n) => {
1054
+ if (!n) return;
1055
+ const group = n.group ?? n.packageGroup;
1056
+ const clusterKey = nodeClusterMap[n.id];
1057
+ const key = clusterKey ?? (group ? `pkg:${group}` : void 0);
1058
+ if (!key) return;
1059
+ const center = boundsToUse[key];
1060
+ if (!center) return;
1061
+ const dx = center.x - (n.x ?? 0);
1062
+ const dy = center.y - (n.y ?? 0);
1063
+ const dist = Math.sqrt(dx * dx + dy * dy);
1064
+ const pullStrength = Math.min(0.5, 0.15 * (dist / (center.r || 200)) + 0.06);
1065
+ if (!isNaN(pullStrength) && isFinite(pullStrength)) {
1066
+ n.vx = (n.vx ?? 0) + dx / (dist || 1) * pullStrength;
1067
+ n.vy = (n.vy ?? 0) + dy / (dist || 1) * pullStrength;
794
1068
  }
795
- });
796
- const children = Object.keys(counts).map((k) => ({ name: k, value: counts[k] }));
797
- if (children.length > 0) {
798
- const root = d33.hierarchy({ children }).sum((d) => d.value);
799
- const pack2 = d33.pack().size([width, height]).padding(30);
800
- const packed = pack2(root);
801
- const map = {};
802
- if (packed.children) {
803
- packed.children.forEach((c) => {
804
- map[`pkg:${c.data.name}`] = { x: c.x, y: c.y, r: c.r * 0.95 };
805
- });
806
- effectiveBounds = map;
1069
+ if (center.r && dist > center.r) {
1070
+ const excess = (dist - center.r) / (dist || 1);
1071
+ n.vx = (n.vx ?? 0) - dx * 0.02 * excess;
1072
+ n.vy = (n.vy ?? 0) - dy * 0.02 * excess;
807
1073
  }
808
- }
809
- } catch (e) {
1074
+ });
810
1075
  }
1076
+ } catch (e) {
811
1077
  }
812
- if (!effectiveBounds) return;
1078
+ };
1079
+ const { packageAreas, localPositions } = React2__default.useMemo(() => {
813
1080
  try {
814
- Object.values(nodesCopy).forEach((n) => {
815
- if (!n) return;
816
- if (n.kind === "package") return;
817
- const pkg = n.packageGroup;
818
- if (!pkg) return;
819
- const bound = effectiveBounds[`pkg:${pkg}`];
820
- if (!bound) return;
821
- const margin = (n.size || 10) + 12;
822
- const dx = (n.x || 0) - bound.x;
823
- const dy = (n.y || 0) - bound.y;
824
- const dist = Math.sqrt(dx * dx + dy * dy) || 1e-4;
825
- const maxDist = Math.max(1, bound.r - margin);
826
- if (dist > maxDist) {
827
- const desiredX = bound.x + dx * (maxDist / dist);
828
- const desiredY = bound.y + dy * (maxDist / dist);
829
- const softness = 0.08;
830
- n.vx = (n.vx || 0) + (desiredX - n.x) * softness;
831
- n.vy = (n.vy || 0) + (desiredY - n.y) * softness;
1081
+ if (!initialNodes || !initialNodes.length) return { packageAreas: {}, localPositions: {} };
1082
+ const groups = /* @__PURE__ */ new Map();
1083
+ initialNodes.forEach((n) => {
1084
+ const key = n.packageGroup || n.group || "root";
1085
+ if (!groups.has(key)) groups.set(key, []);
1086
+ groups.get(key).push(n);
1087
+ });
1088
+ const groupKeys = Array.from(groups.keys());
1089
+ const children = groupKeys.map((k) => ({ name: k, value: Math.max(1, groups.get(k).length) }));
1090
+ const root = d33.hierarchy({ children });
1091
+ root.sum((d) => d.value);
1092
+ const pack2 = d33.pack().size([width, height]).padding(Math.max(20, Math.min(width, height) * 0.03));
1093
+ const packed = pack2(root);
1094
+ const packageAreas2 = {};
1095
+ if (packed.children) {
1096
+ packed.children.forEach((c) => {
1097
+ const name = c.data.name;
1098
+ packageAreas2[name] = { x: c.x, y: c.y, r: Math.max(40, c.r) };
1099
+ });
1100
+ }
1101
+ const localPositions2 = {};
1102
+ groups.forEach((nodesInGroup, _key) => {
1103
+ if (!nodesInGroup || nodesInGroup.length === 0) return;
1104
+ const localNodes = nodesInGroup.map((n) => ({ id: n.id, x: Math.random() * 10 - 5, y: Math.random() * 10 - 5, size: n.size || 10 }));
1105
+ const localLinks = (initialLinks || []).filter((l) => {
1106
+ const s = typeof l.source === "string" ? l.source : l.source && l.source.id;
1107
+ const t = typeof l.target === "string" ? l.target : l.target && l.target.id;
1108
+ return localNodes.some((ln) => ln.id === s) && localNodes.some((ln) => ln.id === t);
1109
+ }).map((l) => ({ source: typeof l.source === "string" ? l.source : l.source.id, target: typeof l.target === "string" ? l.target : l.target.id }));
1110
+ if (localNodes.length === 1) {
1111
+ localPositions2[localNodes[0].id] = { x: 0, y: 0 };
1112
+ return;
832
1113
  }
1114
+ const sim = d33.forceSimulation(localNodes).force("link", d33.forceLink(localLinks).id((d) => d.id).distance(30).strength(0.8)).force("charge", d33.forceManyBody().strength(-15)).force("collide", d33.forceCollide((d) => (d.size || 10) + 6).iterations(2)).stop();
1115
+ const ticks = 300;
1116
+ for (let i = 0; i < ticks; i++) sim.tick();
1117
+ localNodes.forEach((ln) => {
1118
+ localPositions2[ln.id] = { x: ln.x ?? 0, y: ln.y ?? 0 };
1119
+ });
833
1120
  });
1121
+ return { packageAreas: packageAreas2, localPositions: localPositions2 };
834
1122
  } catch (e) {
1123
+ return { packageAreas: {}, localPositions: {} };
835
1124
  }
836
- };
837
- const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(initialNodes, initialLinks, {
1125
+ }, [initialNodes, initialLinks, width, height]);
1126
+ const seededNodes = React2__default.useMemo(() => {
1127
+ if (!initialNodes || !Object.keys(packageAreas || {}).length) return initialNodes;
1128
+ return initialNodes.map((n) => {
1129
+ const key = n.packageGroup || n.group || "root";
1130
+ const area = packageAreas[key];
1131
+ const lp = localPositions[n.id];
1132
+ if (!area || !lp) return n;
1133
+ const scale = Math.max(0.5, area.r * 0.6 / (Math.max(1, Math.sqrt(lp.x * lp.x + lp.y * lp.y)) || 1));
1134
+ return { ...n, x: area.x + lp.x * scale, y: area.y + lp.y * scale };
1135
+ });
1136
+ }, [initialNodes, packageAreas, localPositions]);
1137
+ const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(seededNodes || initialNodes, initialLinks, {
838
1138
  width,
839
1139
  height,
840
1140
  chargeStrength: manualLayout ? 0 : void 0,
841
1141
  onTick,
842
1142
  ...simulationOptions
843
1143
  });
1144
+ const nodesById = React2__default.useMemo(() => {
1145
+ const m = {};
1146
+ (nodes || []).forEach((n) => {
1147
+ if (n && n.id) m[n.id] = n;
1148
+ });
1149
+ return m;
1150
+ }, [nodes]);
1151
+ const clusterBounds = React2__default.useMemo(() => {
1152
+ try {
1153
+ if (!links || !nodes) return null;
1154
+ const nodeIds = new Set(nodes.map((n) => n.id));
1155
+ const adj = /* @__PURE__ */ new Map();
1156
+ nodes.forEach((n) => adj.set(n.id, /* @__PURE__ */ new Set()));
1157
+ links.forEach((l) => {
1158
+ const type = l.type || "reference";
1159
+ if (type !== "dependency") return;
1160
+ const s = typeof l.source === "string" ? l.source : l.source && l.source.id || null;
1161
+ const t = typeof l.target === "string" ? l.target : l.target && l.target.id || null;
1162
+ if (!s || !t) return;
1163
+ if (!nodeIds.has(s) || !nodeIds.has(t)) return;
1164
+ adj.get(s)?.add(t);
1165
+ adj.get(t)?.add(s);
1166
+ });
1167
+ const visited = /* @__PURE__ */ new Set();
1168
+ const comps = [];
1169
+ for (const nid of nodeIds) {
1170
+ if (visited.has(nid)) continue;
1171
+ const stack = [nid];
1172
+ const comp = [];
1173
+ visited.add(nid);
1174
+ while (stack.length) {
1175
+ const cur = stack.pop();
1176
+ comp.push(cur);
1177
+ const neigh = adj.get(cur);
1178
+ if (!neigh) continue;
1179
+ for (const nb of neigh) {
1180
+ if (!visited.has(nb)) {
1181
+ visited.add(nb);
1182
+ stack.push(nb);
1183
+ }
1184
+ }
1185
+ }
1186
+ comps.push(comp);
1187
+ }
1188
+ if (comps.length <= 1) return null;
1189
+ const children = comps.map((c, i) => ({ name: String(i), value: Math.max(1, c.length) }));
1190
+ d33.hierarchy({ children }).sum((d) => d.value).sort((a, b) => b.value - a.value);
1191
+ const num = comps.length;
1192
+ const cx = width / 2;
1193
+ const cy = height / 2;
1194
+ const base = Math.max(width, height);
1195
+ const circleRadius = base * Math.max(30, num * 20, Math.sqrt(num) * 12);
1196
+ const map = {};
1197
+ comps.forEach((c, i) => {
1198
+ const angle = 2 * Math.PI * i / num;
1199
+ const x = cx + Math.cos(angle) * circleRadius;
1200
+ const y = cy + Math.sin(angle) * circleRadius;
1201
+ const sizeBias = Math.sqrt(Math.max(1, c.length));
1202
+ const r = Math.max(200, 100 * sizeBias);
1203
+ map[`cluster:${i}`] = { x, y, r };
1204
+ });
1205
+ const nodeToCluster = {};
1206
+ comps.forEach((c, i) => c.forEach((nid) => nodeToCluster[nid] = `cluster:${i}`));
1207
+ return { bounds: map, nodeToCluster };
1208
+ } catch (e) {
1209
+ return null;
1210
+ }
1211
+ }, [nodes, links, width, height]);
844
1212
  useEffect(() => {
845
- if (!packageBounds) return;
1213
+ if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
846
1214
  try {
847
1215
  restart();
848
1216
  } catch (e) {
849
1217
  }
850
- }, [packageBounds, restart]);
1218
+ }, [packageBounds, clusterBounds, packageAreas, restart]);
851
1219
  useEffect(() => {
852
1220
  try {
853
1221
  if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
@@ -935,6 +1303,7 @@ var ForceDirectedGraph = forwardRef(
935
1303
  const g = d33.select(gRef.current);
936
1304
  const zoom2 = d33.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
937
1305
  g.attr("transform", event.transform);
1306
+ transformRef.current = event.transform;
938
1307
  setTransform(event.transform);
939
1308
  });
940
1309
  svg.call(zoom2);
@@ -942,6 +1311,26 @@ var ForceDirectedGraph = forwardRef(
942
1311
  svg.on(".zoom", null);
943
1312
  };
944
1313
  }, [enableZoom]);
1314
+ useEffect(() => {
1315
+ if (!gRef.current) return;
1316
+ try {
1317
+ const g = d33.select(gRef.current);
1318
+ g.selectAll("g.node").each(function() {
1319
+ const datum = d33.select(this).datum();
1320
+ if (!datum) return;
1321
+ d33.select(this).attr("transform", `translate(${datum.x || 0},${datum.y || 0})`);
1322
+ });
1323
+ g.selectAll("line").each(function() {
1324
+ const l = d33.select(this).datum();
1325
+ if (!l) return;
1326
+ const s = typeof l.source === "object" ? l.source : nodes.find((n) => n.id === l.source) || l.source;
1327
+ const t = typeof l.target === "object" ? l.target : nodes.find((n) => n.id === l.target) || l.target;
1328
+ if (!s || !t) return;
1329
+ d33.select(this).attr("x1", s.x).attr("y1", s.y).attr("x2", t.x).attr("y2", t.y);
1330
+ });
1331
+ } catch (e) {
1332
+ }
1333
+ }, [nodes, links]);
945
1334
  const handleDragStart = useCallback(
946
1335
  (event, node) => {
947
1336
  if (!enableDrag) return;
@@ -966,8 +1355,9 @@ var ForceDirectedGraph = forwardRef(
966
1355
  const svg = svgRef.current;
967
1356
  if (!svg) return;
968
1357
  const rect = svg.getBoundingClientRect();
969
- const x = (event.clientX - rect.left - transform.x) / transform.k;
970
- const y = (event.clientY - rect.top - transform.y) / transform.k;
1358
+ const t = transformRef.current;
1359
+ const x = (event.clientX - rect.left - t.x) / t.k;
1360
+ const y = (event.clientY - rect.top - t.y) / t.k;
971
1361
  dragNodeRef.current.fx = x;
972
1362
  dragNodeRef.current.fy = y;
973
1363
  };
@@ -994,7 +1384,7 @@ var ForceDirectedGraph = forwardRef(
994
1384
  window.removeEventListener("mouseout", handleWindowLeave);
995
1385
  window.removeEventListener("blur", handleWindowUp);
996
1386
  };
997
- }, [enableDrag, transform]);
1387
+ }, [enableDrag]);
998
1388
  useEffect(() => {
999
1389
  if (!gRef.current || !enableDrag) return;
1000
1390
  const g = d33.select(gRef.current);
@@ -1117,98 +1507,35 @@ var ForceDirectedGraph = forwardRef(
1117
1507
  }
1118
1508
  ) }),
1119
1509
  /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
1120
- links.map((link, i) => {
1121
- const source = link.source;
1122
- const target = link.target;
1123
- if (source.x == null || source.y == null || target.x == null || target.y == null) return null;
1124
- return /* @__PURE__ */ jsxs("g", { children: [
1125
- /* @__PURE__ */ jsx(
1126
- "line",
1127
- {
1128
- x1: source.x,
1129
- y1: source.y,
1130
- x2: target.x,
1131
- y2: target.y,
1132
- stroke: link.color || defaultLinkColor,
1133
- strokeWidth: link.width || defaultLinkWidth,
1134
- opacity: 0.6,
1135
- className: "cursor-pointer transition-opacity hover:opacity-100",
1136
- onClick: () => handleLinkClick(link)
1137
- }
1138
- ),
1139
- showLinkLabels && link.label && /* @__PURE__ */ jsx(
1140
- "text",
1141
- {
1142
- x: (source.x + target.x) / 2,
1143
- y: (source.y + target.y) / 2,
1144
- fill: "#666",
1145
- fontSize: "10",
1146
- textAnchor: "middle",
1147
- dominantBaseline: "middle",
1148
- pointerEvents: "none",
1149
- children: link.label
1150
- }
1151
- )
1152
- ] }, `link-${i}`);
1153
- }),
1154
- nodes.map((node) => {
1155
- if (node.x == null || node.y == null) return null;
1156
- const isSelected = selectedNodeId === node.id;
1157
- const isHovered = hoveredNodeId === node.id;
1158
- const nodeSize = node.size || defaultNodeSize;
1159
- const nodeColor = node.color || defaultNodeColor;
1160
- return /* @__PURE__ */ jsxs(
1161
- "g",
1162
- {
1163
- transform: `translate(${node.x},${node.y})`,
1164
- className: "cursor-pointer node",
1165
- "data-id": node.id,
1166
- onClick: () => handleNodeClick(node),
1167
- onDoubleClick: (event) => handleNodeDoubleClick(event, node),
1168
- onMouseEnter: () => handleNodeMouseEnter(node),
1169
- onMouseLeave: handleNodeMouseLeave,
1170
- onMouseDown: (e) => handleDragStart(e, node),
1171
- children: [
1172
- /* @__PURE__ */ jsx(
1173
- "circle",
1174
- {
1175
- r: nodeSize,
1176
- fill: nodeColor,
1177
- stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
1178
- strokeWidth: pinnedNodes.has(node.id) ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
1179
- opacity: isHovered || isSelected ? 1 : 0.9,
1180
- className: "transition-all"
1181
- }
1182
- ),
1183
- pinnedNodes.has(node.id) && /* @__PURE__ */ jsx(
1184
- "circle",
1185
- {
1186
- r: nodeSize + 4,
1187
- fill: "none",
1188
- stroke: "#ff6b6b",
1189
- strokeWidth: 1,
1190
- opacity: 0.5,
1191
- className: "pointer-events-none"
1192
- }
1193
- ),
1194
- showNodeLabels && node.label && /* @__PURE__ */ jsx(
1195
- "text",
1196
- {
1197
- y: nodeSize + 15,
1198
- fill: "#333",
1199
- fontSize: "12",
1200
- textAnchor: "middle",
1201
- dominantBaseline: "middle",
1202
- pointerEvents: "none",
1203
- className: "select-none",
1204
- children: node.label
1205
- }
1206
- )
1207
- ]
1208
- },
1209
- node.id
1210
- );
1211
- }),
1510
+ links.map((link, i) => /* @__PURE__ */ jsx(
1511
+ LinkItem_default,
1512
+ {
1513
+ link,
1514
+ onClick: handleLinkClick,
1515
+ defaultWidth: defaultLinkWidth,
1516
+ showLabel: showLinkLabels,
1517
+ nodes
1518
+ },
1519
+ `link-${i}`
1520
+ )),
1521
+ nodes.map((node) => /* @__PURE__ */ jsx(
1522
+ NodeItem_default,
1523
+ {
1524
+ node,
1525
+ isSelected: selectedNodeId === node.id,
1526
+ isHovered: hoveredNodeId === node.id,
1527
+ pinned: pinnedNodes.has(node.id),
1528
+ defaultNodeSize,
1529
+ defaultNodeColor,
1530
+ showLabel: showNodeLabels,
1531
+ onClick: handleNodeClick,
1532
+ onDoubleClick: handleNodeDoubleClick,
1533
+ onMouseEnter: handleNodeMouseEnter,
1534
+ onMouseLeave: handleNodeMouseLeave,
1535
+ onMouseDown: handleDragStart
1536
+ },
1537
+ node.id
1538
+ )),
1212
1539
  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: [
1213
1540
  /* @__PURE__ */ jsx(
1214
1541
  "circle",