@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.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as React2 from 'react';
2
- import { useState, useEffect, useRef, 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';
6
6
  import { jsx, jsxs } from 'react/jsx-runtime';
7
- import * as d32 from 'd3';
7
+ import * as d33 from 'd3';
8
8
 
9
9
  // src/components/button.tsx
10
10
  function cn(...inputs) {
@@ -595,7 +595,7 @@ function useD3(renderFn, dependencies = []) {
595
595
  const ref = useRef(null);
596
596
  useEffect(() => {
597
597
  if (ref.current) {
598
- const selection = d32.select(ref.current);
598
+ const selection = d33.select(ref.current);
599
599
  renderFn(selection);
600
600
  }
601
601
  }, dependencies);
@@ -605,7 +605,7 @@ function useD3WithResize(renderFn, dependencies = []) {
605
605
  const ref = useRef(null);
606
606
  useEffect(() => {
607
607
  if (!ref.current) return;
608
- const selection = d32.select(ref.current);
608
+ const selection = d33.select(ref.current);
609
609
  const render = () => renderFn(selection);
610
610
  render();
611
611
  const resizeObserver = new ResizeObserver(() => {
@@ -629,39 +629,184 @@ function useForceSimulation(initialNodes, initialLinks, options) {
629
629
  width,
630
630
  height,
631
631
  alphaDecay = 0.0228,
632
- velocityDecay = 0.4
632
+ velocityDecay = 0.4,
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
633
645
  } = options;
634
646
  const [nodes, setNodes] = useState(initialNodes);
635
647
  const [links, setLinks] = useState(initialLinks);
636
648
  const [isRunning, setIsRunning] = useState(false);
637
649
  const [alpha, setAlpha] = useState(1);
638
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("|");
639
658
  useEffect(() => {
640
659
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
641
660
  const linksCopy = initialLinks.map((link) => ({ ...link }));
642
- const simulation = d32.forceSimulation(nodesCopy).force(
643
- "link",
644
- d32.forceLink(linksCopy).id((d) => d.id).distance(linkDistance).strength(linkStrength)
645
- ).force("charge", d32.forceManyBody().strength(chargeStrength)).force("center", d32.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
646
- "collision",
647
- d32.forceCollide().radius(collisionRadius).strength(collisionStrength)
648
- ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
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) => {
693
+ const nodeSize = d && d.size ? d.size : 10;
694
+ return nodeSize + collisionRadius;
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
+ }
649
712
  simulationRef.current = simulation;
650
- simulation.on("tick", () => {
651
- setNodes([...nodesCopy]);
652
- setLinks([...linksCopy]);
653
- setAlpha(simulation.alpha());
654
- setIsRunning(simulation.alpha() > simulation.alphaMin());
655
- });
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 = () => {
743
+ try {
744
+ if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
745
+ } catch (e) {
746
+ }
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);
656
783
  simulation.on("end", () => {
657
784
  setIsRunning(false);
658
785
  });
659
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
+ }
660
805
  simulation.stop();
661
806
  };
662
807
  }, [
663
- initialNodes,
664
- initialLinks,
808
+ nodesKey,
809
+ linksKey,
665
810
  chargeStrength,
666
811
  linkDistance,
667
812
  linkStrength,
@@ -671,12 +816,38 @@ function useForceSimulation(initialNodes, initialLinks, options) {
671
816
  width,
672
817
  height,
673
818
  alphaDecay,
674
- velocityDecay
819
+ velocityDecay,
820
+ alphaTarget,
821
+ alphaMin,
822
+ stabilizeOnStop,
823
+ tickThrottleMs,
824
+ maxSimulationTimeMs
675
825
  ]);
676
826
  const restart = () => {
677
827
  if (simulationRef.current) {
678
- simulationRef.current.alpha(1).restart();
828
+ try {
829
+ simulationRef.current.alphaTarget(warmAlpha).restart();
830
+ } catch (e) {
831
+ simulationRef.current.restart();
832
+ }
679
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
+ }
680
851
  }
681
852
  };
682
853
  const stop = () => {
@@ -685,13 +856,33 @@ function useForceSimulation(initialNodes, initialLinks, options) {
685
856
  setIsRunning(false);
686
857
  }
687
858
  };
859
+ const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });
860
+ const forcesEnabledRef = useRef(true);
861
+ const setForcesEnabled = (enabled) => {
862
+ const sim = simulationRef.current;
863
+ if (!sim) return;
864
+ if (forcesEnabledRef.current === enabled) return;
865
+ forcesEnabledRef.current = enabled;
866
+ try {
867
+ const charge = sim.force("charge");
868
+ if (charge && typeof charge.strength === "function") {
869
+ charge.strength(enabled ? originalForcesRef.current.charge : 0);
870
+ }
871
+ const link = sim.force("link");
872
+ if (link && typeof link.strength === "function") {
873
+ link.strength(enabled ? originalForcesRef.current.link : 0);
874
+ }
875
+ } catch (e) {
876
+ }
877
+ };
688
878
  return {
689
879
  nodes,
690
880
  links,
691
881
  restart,
692
882
  stop,
693
883
  isRunning,
694
- alpha
884
+ alpha,
885
+ setForcesEnabled
695
886
  };
696
887
  }
697
888
  function useDrag(simulation) {
@@ -717,211 +908,804 @@ function useDrag(simulation) {
717
908
  onDragEnd: dragEnded
718
909
  };
719
910
  }
720
- var ForceDirectedGraph = ({
721
- nodes: initialNodes,
722
- links: initialLinks,
723
- width,
724
- height,
725
- simulationOptions,
726
- enableZoom = true,
727
- enableDrag = true,
728
- onNodeClick,
729
- onNodeHover,
730
- onLinkClick,
731
- selectedNodeId,
732
- hoveredNodeId,
733
- defaultNodeColor = "#69b3a2",
734
- defaultNodeSize = 10,
735
- defaultLinkColor = "#999",
736
- defaultLinkWidth = 1,
737
- showNodeLabels = true,
738
- showLinkLabels = false,
739
- className
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
740
924
  }) => {
741
- const svgRef = useRef(null);
742
- const gRef = useRef(null);
743
- const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
744
- const { nodes, links, restart } = useForceSimulation(initialNodes, initialLinks, {
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;
1012
+ var ForceDirectedGraph = forwardRef(
1013
+ ({
1014
+ nodes: initialNodes,
1015
+ links: initialLinks,
745
1016
  width,
746
1017
  height,
747
- ...simulationOptions
748
- });
749
- useEffect(() => {
750
- if (!enableZoom || !svgRef.current || !gRef.current) return;
751
- const svg = d32.select(svgRef.current);
752
- const g = d32.select(gRef.current);
753
- const zoom2 = d32.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
754
- g.attr("transform", event.transform);
755
- setTransform(event.transform);
756
- });
757
- svg.call(zoom2);
758
- return () => {
759
- svg.on(".zoom", null);
1018
+ simulationOptions,
1019
+ enableZoom = true,
1020
+ enableDrag = true,
1021
+ onNodeClick,
1022
+ onNodeHover,
1023
+ onLinkClick,
1024
+ selectedNodeId,
1025
+ hoveredNodeId,
1026
+ defaultNodeColor = "#69b3a2",
1027
+ defaultNodeSize = 10,
1028
+ defaultLinkColor = "#999",
1029
+ defaultLinkWidth = 1,
1030
+ showNodeLabels = true,
1031
+ showLinkLabels = false,
1032
+ className,
1033
+ manualLayout = false,
1034
+ onManualLayoutChange,
1035
+ packageBounds
1036
+ }, ref) => {
1037
+ const svgRef = useRef(null);
1038
+ const gRef = useRef(null);
1039
+ const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
1040
+ const transformRef = useRef(transform);
1041
+ const dragNodeRef = useRef(null);
1042
+ const dragActiveRef = useRef(false);
1043
+ const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
1044
+ const internalDragEnabledRef = useRef(enableDrag);
1045
+ useEffect(() => {
1046
+ internalDragEnabledRef.current = enableDrag;
1047
+ }, [enableDrag]);
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;
1068
+ }
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;
1073
+ }
1074
+ });
1075
+ }
1076
+ } catch (e) {
1077
+ }
760
1078
  };
761
- }, [enableZoom]);
762
- const handleDragStart = useCallback(
763
- (event, node) => {
764
- if (!enableDrag) return;
765
- event.stopPropagation();
766
- node.fx = node.x;
767
- node.fy = node.y;
768
- restart();
769
- },
770
- [enableDrag, restart]
771
- );
772
- const handleDrag = useCallback(
773
- (event, node) => {
774
- if (!enableDrag) return;
775
- const svg = svgRef.current;
776
- if (!svg) return;
777
- const rect = svg.getBoundingClientRect();
778
- const x = (event.clientX - rect.left - transform.x) / transform.k;
779
- const y = (event.clientY - rect.top - transform.y) / transform.k;
780
- node.fx = x;
781
- node.fy = y;
782
- },
783
- [enableDrag, transform]
784
- );
785
- const handleDragEnd = useCallback(
786
- (event, node) => {
787
- if (!enableDrag) return;
788
- event.stopPropagation();
789
- node.fx = null;
790
- node.fy = null;
791
- },
792
- [enableDrag]
793
- );
794
- const handleNodeClick = useCallback(
795
- (node) => {
796
- onNodeClick?.(node);
797
- },
798
- [onNodeClick]
799
- );
800
- const handleNodeMouseEnter = useCallback(
801
- (node) => {
802
- onNodeHover?.(node);
803
- },
804
- [onNodeHover]
805
- );
806
- const handleNodeMouseLeave = useCallback(() => {
807
- onNodeHover?.(null);
808
- }, [onNodeHover]);
809
- const handleLinkClick = useCallback(
810
- (link) => {
811
- onLinkClick?.(link);
812
- },
813
- [onLinkClick]
814
- );
815
- return /* @__PURE__ */ jsxs(
816
- "svg",
817
- {
818
- ref: svgRef,
1079
+ const { packageAreas, localPositions } = React2__default.useMemo(() => {
1080
+ try {
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;
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
+ });
1120
+ });
1121
+ return { packageAreas: packageAreas2, localPositions: localPositions2 };
1122
+ } catch (e) {
1123
+ return { packageAreas: {}, localPositions: {} };
1124
+ }
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, {
819
1138
  width,
820
1139
  height,
821
- className: cn("bg-white dark:bg-gray-900", className),
822
- children: [
823
- /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
824
- "marker",
825
- {
826
- id: "arrow",
827
- viewBox: "0 0 10 10",
828
- refX: "20",
829
- refY: "5",
830
- markerWidth: "6",
831
- markerHeight: "6",
832
- orient: "auto",
833
- children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
1140
+ chargeStrength: manualLayout ? 0 : void 0,
1141
+ onTick,
1142
+ ...simulationOptions
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
+ }
834
1185
  }
835
- ) }),
836
- /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
837
- links.map((link, i) => {
838
- const source = link.source;
839
- const target = link.target;
840
- if (!source.x || !source.y || !target.x || !target.y) return null;
841
- return /* @__PURE__ */ jsxs("g", { children: [
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]);
1212
+ useEffect(() => {
1213
+ if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
1214
+ try {
1215
+ restart();
1216
+ } catch (e) {
1217
+ }
1218
+ }, [packageBounds, clusterBounds, packageAreas, restart]);
1219
+ useEffect(() => {
1220
+ try {
1221
+ if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
1222
+ else setForcesEnabled(true);
1223
+ } catch (e) {
1224
+ }
1225
+ }, [manualLayout, pinnedNodes, setForcesEnabled]);
1226
+ useImperativeHandle(
1227
+ ref,
1228
+ () => ({
1229
+ pinAll: () => {
1230
+ const newPinned = /* @__PURE__ */ new Set();
1231
+ nodes.forEach((node) => {
1232
+ node.fx = node.x;
1233
+ node.fy = node.y;
1234
+ newPinned.add(node.id);
1235
+ });
1236
+ setPinnedNodes(newPinned);
1237
+ restart();
1238
+ },
1239
+ unpinAll: () => {
1240
+ nodes.forEach((node) => {
1241
+ node.fx = null;
1242
+ node.fy = null;
1243
+ });
1244
+ setPinnedNodes(/* @__PURE__ */ new Set());
1245
+ restart();
1246
+ },
1247
+ resetLayout: () => {
1248
+ nodes.forEach((node) => {
1249
+ node.fx = null;
1250
+ node.fy = null;
1251
+ });
1252
+ setPinnedNodes(/* @__PURE__ */ new Set());
1253
+ restart();
1254
+ },
1255
+ fitView: () => {
1256
+ if (!svgRef.current || !nodes.length) return;
1257
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1258
+ nodes.forEach((node) => {
1259
+ if (node.x !== void 0 && node.y !== void 0) {
1260
+ const size = node.size || 10;
1261
+ minX = Math.min(minX, node.x - size);
1262
+ maxX = Math.max(maxX, node.x + size);
1263
+ minY = Math.min(minY, node.y - size);
1264
+ maxY = Math.max(maxY, node.y + size);
1265
+ }
1266
+ });
1267
+ if (!isFinite(minX)) return;
1268
+ const padding = 40;
1269
+ const nodeWidth = maxX - minX;
1270
+ const nodeHeight = maxY - minY;
1271
+ const scale = Math.min(
1272
+ (width - padding * 2) / nodeWidth,
1273
+ (height - padding * 2) / nodeHeight,
1274
+ 10
1275
+ );
1276
+ const centerX = (minX + maxX) / 2;
1277
+ const centerY = (minY + maxY) / 2;
1278
+ const x = width / 2 - centerX * scale;
1279
+ const y = height / 2 - centerY * scale;
1280
+ if (gRef.current && svgRef.current) {
1281
+ const svg = d33.select(svgRef.current);
1282
+ const newTransform = d33.zoomIdentity.translate(x, y).scale(scale);
1283
+ svg.transition().duration(300).call(d33.zoom().transform, newTransform);
1284
+ setTransform(newTransform);
1285
+ }
1286
+ },
1287
+ getPinnedNodes: () => Array.from(pinnedNodes),
1288
+ setDragMode: (enabled) => {
1289
+ internalDragEnabledRef.current = enabled;
1290
+ }
1291
+ }),
1292
+ [nodes, pinnedNodes, restart, width, height]
1293
+ );
1294
+ useEffect(() => {
1295
+ try {
1296
+ if (typeof onManualLayoutChange === "function") onManualLayoutChange(manualLayout);
1297
+ } catch (e) {
1298
+ }
1299
+ }, [manualLayout, onManualLayoutChange]);
1300
+ useEffect(() => {
1301
+ if (!enableZoom || !svgRef.current || !gRef.current) return;
1302
+ const svg = d33.select(svgRef.current);
1303
+ const g = d33.select(gRef.current);
1304
+ const zoom2 = d33.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
1305
+ g.attr("transform", event.transform);
1306
+ transformRef.current = event.transform;
1307
+ setTransform(event.transform);
1308
+ });
1309
+ svg.call(zoom2);
1310
+ return () => {
1311
+ svg.on(".zoom", null);
1312
+ };
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]);
1334
+ const handleDragStart = useCallback(
1335
+ (event, node) => {
1336
+ if (!enableDrag) return;
1337
+ event.preventDefault();
1338
+ event.stopPropagation();
1339
+ dragActiveRef.current = true;
1340
+ dragNodeRef.current = node;
1341
+ node.fx = node.x;
1342
+ node.fy = node.y;
1343
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
1344
+ try {
1345
+ stop();
1346
+ } catch (e) {
1347
+ }
1348
+ },
1349
+ [enableDrag, restart]
1350
+ );
1351
+ useEffect(() => {
1352
+ if (!enableDrag) return;
1353
+ const handleWindowMove = (event) => {
1354
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
1355
+ const svg = svgRef.current;
1356
+ if (!svg) return;
1357
+ const rect = svg.getBoundingClientRect();
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;
1361
+ dragNodeRef.current.fx = x;
1362
+ dragNodeRef.current.fy = y;
1363
+ };
1364
+ const handleWindowUp = () => {
1365
+ if (!dragActiveRef.current) return;
1366
+ try {
1367
+ setForcesEnabled(true);
1368
+ restart();
1369
+ } catch (e) {
1370
+ }
1371
+ dragNodeRef.current = null;
1372
+ dragActiveRef.current = false;
1373
+ };
1374
+ const handleWindowLeave = (event) => {
1375
+ if (event.relatedTarget === null) handleWindowUp();
1376
+ };
1377
+ window.addEventListener("mousemove", handleWindowMove);
1378
+ window.addEventListener("mouseup", handleWindowUp);
1379
+ window.addEventListener("mouseout", handleWindowLeave);
1380
+ window.addEventListener("blur", handleWindowUp);
1381
+ return () => {
1382
+ window.removeEventListener("mousemove", handleWindowMove);
1383
+ window.removeEventListener("mouseup", handleWindowUp);
1384
+ window.removeEventListener("mouseout", handleWindowLeave);
1385
+ window.removeEventListener("blur", handleWindowUp);
1386
+ };
1387
+ }, [enableDrag]);
1388
+ useEffect(() => {
1389
+ if (!gRef.current || !enableDrag) return;
1390
+ const g = d33.select(gRef.current);
1391
+ const dragBehavior = d33.drag().on("start", function(event) {
1392
+ try {
1393
+ const target = event.sourceEvent && event.sourceEvent.target || event.target;
1394
+ const grp = target.closest?.("g.node");
1395
+ const id = grp?.getAttribute("data-id");
1396
+ if (!id) return;
1397
+ const node = nodes.find((n) => n.id === id);
1398
+ if (!node) return;
1399
+ if (!internalDragEnabledRef.current) return;
1400
+ if (!event.active) restart();
1401
+ dragActiveRef.current = true;
1402
+ dragNodeRef.current = node;
1403
+ node.fx = node.x;
1404
+ node.fy = node.y;
1405
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
1406
+ } catch (e) {
1407
+ }
1408
+ }).on("drag", function(event) {
1409
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
1410
+ const svg = svgRef.current;
1411
+ if (!svg) return;
1412
+ const rect = svg.getBoundingClientRect();
1413
+ const x = (event.sourceEvent.clientX - rect.left - transform.x) / transform.k;
1414
+ const y = (event.sourceEvent.clientY - rect.top - transform.y) / transform.k;
1415
+ dragNodeRef.current.fx = x;
1416
+ dragNodeRef.current.fy = y;
1417
+ }).on("end", function() {
1418
+ try {
1419
+ setForcesEnabled(true);
1420
+ restart();
1421
+ } catch (e) {
1422
+ }
1423
+ dragNodeRef.current = null;
1424
+ dragActiveRef.current = false;
1425
+ });
1426
+ try {
1427
+ g.selectAll("g.node").call(dragBehavior);
1428
+ } catch (e) {
1429
+ }
1430
+ return () => {
1431
+ try {
1432
+ g.selectAll("g.node").on(".drag", null);
1433
+ } catch (e) {
1434
+ }
1435
+ };
1436
+ }, [gRef, enableDrag, nodes, transform, restart]);
1437
+ const handleNodeClick = useCallback(
1438
+ (node) => {
1439
+ onNodeClick?.(node);
1440
+ },
1441
+ [onNodeClick]
1442
+ );
1443
+ const handleNodeDoubleClick = useCallback(
1444
+ (event, node) => {
1445
+ event.stopPropagation();
1446
+ if (!enableDrag) return;
1447
+ if (node.fx === null || node.fx === void 0) {
1448
+ node.fx = node.x;
1449
+ node.fy = node.y;
1450
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
1451
+ } else {
1452
+ node.fx = null;
1453
+ node.fy = null;
1454
+ setPinnedNodes((prev) => {
1455
+ const next = new Set(prev);
1456
+ next.delete(node.id);
1457
+ return next;
1458
+ });
1459
+ }
1460
+ restart();
1461
+ },
1462
+ [enableDrag, restart]
1463
+ );
1464
+ const handleCanvasDoubleClick = useCallback(() => {
1465
+ nodes.forEach((node) => {
1466
+ node.fx = null;
1467
+ node.fy = null;
1468
+ });
1469
+ setPinnedNodes(/* @__PURE__ */ new Set());
1470
+ restart();
1471
+ }, [nodes, restart]);
1472
+ const handleNodeMouseEnter = useCallback(
1473
+ (node) => {
1474
+ onNodeHover?.(node);
1475
+ },
1476
+ [onNodeHover]
1477
+ );
1478
+ const handleNodeMouseLeave = useCallback(() => {
1479
+ onNodeHover?.(null);
1480
+ }, [onNodeHover]);
1481
+ const handleLinkClick = useCallback(
1482
+ (link) => {
1483
+ onLinkClick?.(link);
1484
+ },
1485
+ [onLinkClick]
1486
+ );
1487
+ return /* @__PURE__ */ jsxs(
1488
+ "svg",
1489
+ {
1490
+ ref: svgRef,
1491
+ width,
1492
+ height,
1493
+ className: cn("bg-white dark:bg-gray-900", className),
1494
+ onDoubleClick: handleCanvasDoubleClick,
1495
+ children: [
1496
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
1497
+ "marker",
1498
+ {
1499
+ id: "arrow",
1500
+ viewBox: "0 0 10 10",
1501
+ refX: "20",
1502
+ refY: "5",
1503
+ markerWidth: "6",
1504
+ markerHeight: "6",
1505
+ orient: "auto",
1506
+ children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
1507
+ }
1508
+ ) }),
1509
+ /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
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
+ )),
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: [
842
1540
  /* @__PURE__ */ jsx(
843
- "line",
1541
+ "circle",
844
1542
  {
845
- x1: source.x,
846
- y1: source.y,
847
- x2: target.x,
848
- y2: target.y,
849
- stroke: link.color || defaultLinkColor,
850
- strokeWidth: link.width || defaultLinkWidth,
851
- opacity: 0.6,
852
- className: "cursor-pointer transition-opacity hover:opacity-100",
853
- onClick: () => handleLinkClick(link)
1543
+ cx: b.x,
1544
+ cy: b.y,
1545
+ r: b.r,
1546
+ fill: "rgba(148,163,184,0.06)",
1547
+ stroke: "#475569",
1548
+ strokeWidth: 2,
1549
+ strokeDasharray: "6 6",
1550
+ opacity: 0.9
854
1551
  }
855
1552
  ),
856
- showLinkLabels && link.label && /* @__PURE__ */ jsx(
1553
+ /* @__PURE__ */ jsx(
857
1554
  "text",
858
1555
  {
859
- x: (source.x + target.x) / 2,
860
- y: (source.y + target.y) / 2,
861
- fill: "#666",
862
- fontSize: "10",
1556
+ x: b.x,
1557
+ y: Math.max(12, b.y - b.r + 14),
1558
+ fill: "#475569",
1559
+ fontSize: 11,
863
1560
  textAnchor: "middle",
864
- dominantBaseline: "middle",
865
1561
  pointerEvents: "none",
866
- children: link.label
1562
+ children: pid.replace(/^pkg:/, "")
867
1563
  }
868
1564
  )
869
- ] }, `link-${i}`);
870
- }),
871
- nodes.map((node) => {
872
- if (!node.x || !node.y) return null;
873
- const isSelected = selectedNodeId === node.id;
874
- const isHovered = hoveredNodeId === node.id;
875
- const nodeSize = node.size || defaultNodeSize;
876
- const nodeColor = node.color || defaultNodeColor;
877
- return /* @__PURE__ */ jsxs(
878
- "g",
1565
+ ] }, pid)) })
1566
+ ] })
1567
+ ]
1568
+ }
1569
+ );
1570
+ }
1571
+ );
1572
+ ForceDirectedGraph.displayName = "ForceDirectedGraph";
1573
+ var GraphControls = ({
1574
+ dragEnabled = true,
1575
+ onDragToggle,
1576
+ manualLayout = false,
1577
+ onManualLayoutToggle,
1578
+ onPinAll,
1579
+ onUnpinAll,
1580
+ onReset,
1581
+ onFitView,
1582
+ pinnedCount = 0,
1583
+ totalNodes = 0,
1584
+ visible = true,
1585
+ position = "top-left",
1586
+ className
1587
+ }) => {
1588
+ if (!visible) return null;
1589
+ const positionClasses = {
1590
+ "top-left": "top-4 left-4",
1591
+ "top-right": "top-4 right-4",
1592
+ "bottom-left": "bottom-4 left-4",
1593
+ "bottom-right": "bottom-4 right-4"
1594
+ };
1595
+ const ControlButton = ({ onClick, active = false, icon, label, disabled = false }) => /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
1596
+ /* @__PURE__ */ jsx(
1597
+ "button",
1598
+ {
1599
+ onClick,
1600
+ disabled,
1601
+ className: cn(
1602
+ "p-2 rounded-lg transition-all duration-200",
1603
+ active ? "bg-blue-500 text-white shadow-md hover:bg-blue-600" : "bg-gray-100 text-gray-700 hover:bg-gray-200",
1604
+ disabled && "opacity-50 cursor-not-allowed hover:bg-gray-100",
1605
+ "dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-blue-600"
1606
+ ),
1607
+ title: label,
1608
+ children: /* @__PURE__ */ jsx("span", { className: "text-lg", children: icon })
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsx("div", { className: "absolute left-full ml-2 px-2 py-1 bg-gray-900 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50", children: label })
1612
+ ] });
1613
+ return /* @__PURE__ */ jsxs(
1614
+ "div",
1615
+ {
1616
+ className: cn(
1617
+ "fixed z-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-2 border border-gray-200 dark:border-gray-700",
1618
+ positionClasses[position],
1619
+ className
1620
+ ),
1621
+ children: [
1622
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1623
+ /* @__PURE__ */ jsx(
1624
+ ControlButton,
1625
+ {
1626
+ onClick: () => onDragToggle?.(!dragEnabled),
1627
+ active: dragEnabled,
1628
+ icon: "\u270B",
1629
+ label: dragEnabled ? "Drag enabled" : "Drag disabled"
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ jsx(
1633
+ ControlButton,
1634
+ {
1635
+ onClick: () => onManualLayoutToggle?.(!manualLayout),
1636
+ active: manualLayout,
1637
+ icon: "\u{1F527}",
1638
+ label: manualLayout ? "Manual layout: ON (drag freely)" : "Manual layout: OFF (forces active)"
1639
+ }
1640
+ ),
1641
+ /* @__PURE__ */ jsx("div", { className: "w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" }),
1642
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1643
+ /* @__PURE__ */ jsx(
1644
+ ControlButton,
879
1645
  {
880
- transform: `translate(${node.x},${node.y})`,
881
- className: "cursor-pointer",
882
- onClick: () => handleNodeClick(node),
883
- onMouseEnter: () => handleNodeMouseEnter(node),
884
- onMouseLeave: handleNodeMouseLeave,
885
- onMouseDown: (e) => handleDragStart(e, node),
886
- onMouseMove: (e) => handleDrag(e, node),
887
- onMouseUp: (e) => handleDragEnd(e, node),
888
- children: [
889
- /* @__PURE__ */ jsx(
890
- "circle",
891
- {
892
- r: nodeSize,
893
- fill: nodeColor,
894
- stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
895
- strokeWidth: isSelected ? 3 : 2,
896
- opacity: isHovered || isSelected ? 1 : 0.9,
897
- className: "transition-all"
898
- }
899
- ),
900
- showNodeLabels && node.label && /* @__PURE__ */ jsx(
901
- "text",
902
- {
903
- y: nodeSize + 15,
904
- fill: "#333",
905
- fontSize: "12",
906
- textAnchor: "middle",
907
- dominantBaseline: "middle",
908
- pointerEvents: "none",
909
- className: "select-none",
910
- children: node.label
911
- }
912
- )
913
- ]
914
- },
915
- node.id
916
- );
917
- })
1646
+ onClick: () => onPinAll?.(),
1647
+ disabled: totalNodes === 0,
1648
+ icon: "\u{1F4CC}",
1649
+ label: `Pin all nodes (${totalNodes})`
1650
+ }
1651
+ ),
1652
+ /* @__PURE__ */ jsx(
1653
+ ControlButton,
1654
+ {
1655
+ onClick: () => onUnpinAll?.(),
1656
+ disabled: pinnedCount === 0,
1657
+ icon: "\u{1F4CD}",
1658
+ label: `Unpin all (${pinnedCount} pinned)`
1659
+ }
1660
+ )
1661
+ ] }),
1662
+ /* @__PURE__ */ jsx("div", { className: "w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" }),
1663
+ /* @__PURE__ */ jsx(
1664
+ ControlButton,
1665
+ {
1666
+ onClick: () => onFitView?.(),
1667
+ disabled: totalNodes === 0,
1668
+ icon: "\u{1F3AF}",
1669
+ label: "Fit all nodes in view"
1670
+ }
1671
+ ),
1672
+ /* @__PURE__ */ jsx(
1673
+ ControlButton,
1674
+ {
1675
+ onClick: () => onReset?.(),
1676
+ disabled: totalNodes === 0,
1677
+ icon: "\u21BA",
1678
+ label: "Reset to auto-layout"
1679
+ }
1680
+ )
1681
+ ] }),
1682
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-600 dark:text-gray-400", children: [
1683
+ /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
1684
+ /* @__PURE__ */ jsx("strong", { children: "Nodes:" }),
1685
+ " ",
1686
+ totalNodes
1687
+ ] }),
1688
+ pinnedCount > 0 && /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
1689
+ /* @__PURE__ */ jsx("strong", { children: "Pinned:" }),
1690
+ " ",
1691
+ pinnedCount
1692
+ ] }),
1693
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 text-gray-500 dark:text-gray-500 leading-snug", children: [
1694
+ /* @__PURE__ */ jsx("strong", { children: "Tips:" }),
1695
+ /* @__PURE__ */ jsxs("ul", { className: "mt-1 ml-1 space-y-0.5", children: [
1696
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Drag nodes to reposition" }),
1697
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Double-click to pin/unpin" }),
1698
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Double-click canvas to unpin all" }),
1699
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Scroll to zoom" })
1700
+ ] })
1701
+ ] })
918
1702
  ] })
919
1703
  ]
920
1704
  }
921
1705
  );
922
1706
  };
923
- ForceDirectedGraph.displayName = "ForceDirectedGraph";
1707
+ GraphControls.displayName = "GraphControls";
924
1708
 
925
- export { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Container, ForceDirectedGraph, Grid, Input, Label, RadioGroup, Select, Separator, Stack, Switch, Textarea, badgeVariants, buttonVariants, chartColors, cn, domainColors, formatCompactNumber, formatDate, formatDateTime, formatDecimal, formatDuration, formatFileSize, formatMetric, formatNumber, formatPercentage, formatRange, formatRelativeTime, getDomainColor, getSeverityColor, hexToRgba, severityColors, useD3, useD3WithResize, useDebounce, useDrag, useForceSimulation };
1709
+ export { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Container, ForceDirectedGraph, GraphControls, Grid, Input, Label, RadioGroup, Select, Separator, Stack, Switch, Textarea, badgeVariants, buttonVariants, chartColors, cn, domainColors, formatCompactNumber, formatDate, formatDateTime, formatDecimal, formatDuration, formatFileSize, formatMetric, formatNumber, formatPercentage, formatRange, formatRelativeTime, getDomainColor, getSeverityColor, hexToRgba, severityColors, useD3, useD3WithResize, useDebounce, useDrag, useForceSimulation };
926
1710
  //# sourceMappingURL=index.js.map
927
1711
  //# sourceMappingURL=index.js.map