@aiready/components 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 { 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,7 +629,8 @@ 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
+ onTick
633
634
  } = options;
634
635
  const [nodes, setNodes] = useState(initialNodes);
635
636
  const [links, setLinks] = useState(initialLinks);
@@ -639,15 +640,22 @@ function useForceSimulation(initialNodes, initialLinks, options) {
639
640
  useEffect(() => {
640
641
  const nodesCopy = initialNodes.map((node) => ({ ...node }));
641
642
  const linksCopy = initialLinks.map((link) => ({ ...link }));
642
- const simulation = d32.forceSimulation(nodesCopy).force(
643
+ const simulation = d33.forceSimulation(nodesCopy).force(
643
644
  "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(
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(
646
647
  "collision",
647
- d32.forceCollide().radius(collisionRadius).strength(collisionStrength)
648
+ d33.forceCollide().radius((d) => {
649
+ const nodeSize = d && d.size ? d.size : 10;
650
+ return nodeSize + collisionRadius;
651
+ }).strength(collisionStrength)
648
652
  ).alphaDecay(alphaDecay).velocityDecay(velocityDecay);
649
653
  simulationRef.current = simulation;
650
654
  simulation.on("tick", () => {
655
+ try {
656
+ if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
657
+ } catch (e) {
658
+ }
651
659
  setNodes([...nodesCopy]);
652
660
  setLinks([...linksCopy]);
653
661
  setAlpha(simulation.alpha());
@@ -671,7 +679,8 @@ function useForceSimulation(initialNodes, initialLinks, options) {
671
679
  width,
672
680
  height,
673
681
  alphaDecay,
674
- velocityDecay
682
+ velocityDecay,
683
+ onTick
675
684
  ]);
676
685
  const restart = () => {
677
686
  if (simulationRef.current) {
@@ -685,13 +694,33 @@ function useForceSimulation(initialNodes, initialLinks, options) {
685
694
  setIsRunning(false);
686
695
  }
687
696
  };
697
+ const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });
698
+ const forcesEnabledRef = useRef(true);
699
+ const setForcesEnabled = (enabled) => {
700
+ const sim = simulationRef.current;
701
+ if (!sim) return;
702
+ if (forcesEnabledRef.current === enabled) return;
703
+ forcesEnabledRef.current = enabled;
704
+ try {
705
+ const charge = sim.force("charge");
706
+ if (charge && typeof charge.strength === "function") {
707
+ charge.strength(enabled ? originalForcesRef.current.charge : 0);
708
+ }
709
+ const link = sim.force("link");
710
+ if (link && typeof link.strength === "function") {
711
+ link.strength(enabled ? originalForcesRef.current.link : 0);
712
+ }
713
+ } catch (e) {
714
+ }
715
+ };
688
716
  return {
689
717
  nodes,
690
718
  links,
691
719
  restart,
692
720
  stop,
693
721
  isRunning,
694
- alpha
722
+ alpha,
723
+ setForcesEnabled
695
724
  };
696
725
  }
697
726
  function useDrag(simulation) {
@@ -717,211 +746,639 @@ function useDrag(simulation) {
717
746
  onDragEnd: dragEnded
718
747
  };
719
748
  }
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
740
- }) => {
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, {
749
+ var ForceDirectedGraph = forwardRef(
750
+ ({
751
+ nodes: initialNodes,
752
+ links: initialLinks,
745
753
  width,
746
754
  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);
755
+ simulationOptions,
756
+ enableZoom = true,
757
+ enableDrag = true,
758
+ onNodeClick,
759
+ onNodeHover,
760
+ onLinkClick,
761
+ selectedNodeId,
762
+ hoveredNodeId,
763
+ defaultNodeColor = "#69b3a2",
764
+ defaultNodeSize = 10,
765
+ defaultLinkColor = "#999",
766
+ defaultLinkWidth = 1,
767
+ showNodeLabels = true,
768
+ showLinkLabels = false,
769
+ className,
770
+ manualLayout = false,
771
+ onManualLayoutChange,
772
+ packageBounds
773
+ }, ref) => {
774
+ const svgRef = useRef(null);
775
+ const gRef = useRef(null);
776
+ const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
777
+ const dragNodeRef = useRef(null);
778
+ const dragActiveRef = useRef(false);
779
+ const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
780
+ const internalDragEnabledRef = useRef(enableDrag);
781
+ useEffect(() => {
782
+ internalDragEnabledRef.current = enableDrag;
783
+ }, [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;
794
+ }
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;
807
+ }
808
+ }
809
+ } catch (e) {
810
+ }
811
+ }
812
+ if (!effectiveBounds) return;
813
+ 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;
832
+ }
833
+ });
834
+ } catch (e) {
835
+ }
760
836
  };
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,
837
+ const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(initialNodes, initialLinks, {
819
838
  width,
820
839
  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 })
840
+ chargeStrength: manualLayout ? 0 : void 0,
841
+ onTick,
842
+ ...simulationOptions
843
+ });
844
+ useEffect(() => {
845
+ if (!packageBounds) return;
846
+ try {
847
+ restart();
848
+ } catch (e) {
849
+ }
850
+ }, [packageBounds, restart]);
851
+ useEffect(() => {
852
+ try {
853
+ if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
854
+ else setForcesEnabled(true);
855
+ } catch (e) {
856
+ }
857
+ }, [manualLayout, pinnedNodes, setForcesEnabled]);
858
+ useImperativeHandle(
859
+ ref,
860
+ () => ({
861
+ pinAll: () => {
862
+ const newPinned = /* @__PURE__ */ new Set();
863
+ nodes.forEach((node) => {
864
+ node.fx = node.x;
865
+ node.fy = node.y;
866
+ newPinned.add(node.id);
867
+ });
868
+ setPinnedNodes(newPinned);
869
+ restart();
870
+ },
871
+ unpinAll: () => {
872
+ nodes.forEach((node) => {
873
+ node.fx = null;
874
+ node.fy = null;
875
+ });
876
+ setPinnedNodes(/* @__PURE__ */ new Set());
877
+ restart();
878
+ },
879
+ resetLayout: () => {
880
+ nodes.forEach((node) => {
881
+ node.fx = null;
882
+ node.fy = null;
883
+ });
884
+ setPinnedNodes(/* @__PURE__ */ new Set());
885
+ restart();
886
+ },
887
+ fitView: () => {
888
+ if (!svgRef.current || !nodes.length) return;
889
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
890
+ nodes.forEach((node) => {
891
+ if (node.x !== void 0 && node.y !== void 0) {
892
+ const size = node.size || 10;
893
+ minX = Math.min(minX, node.x - size);
894
+ maxX = Math.max(maxX, node.x + size);
895
+ minY = Math.min(minY, node.y - size);
896
+ maxY = Math.max(maxY, node.y + size);
897
+ }
898
+ });
899
+ if (!isFinite(minX)) return;
900
+ const padding = 40;
901
+ const nodeWidth = maxX - minX;
902
+ const nodeHeight = maxY - minY;
903
+ const scale = Math.min(
904
+ (width - padding * 2) / nodeWidth,
905
+ (height - padding * 2) / nodeHeight,
906
+ 10
907
+ );
908
+ const centerX = (minX + maxX) / 2;
909
+ const centerY = (minY + maxY) / 2;
910
+ const x = width / 2 - centerX * scale;
911
+ const y = height / 2 - centerY * scale;
912
+ if (gRef.current && svgRef.current) {
913
+ const svg = d33.select(svgRef.current);
914
+ const newTransform = d33.zoomIdentity.translate(x, y).scale(scale);
915
+ svg.transition().duration(300).call(d33.zoom().transform, newTransform);
916
+ setTransform(newTransform);
834
917
  }
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: [
918
+ },
919
+ getPinnedNodes: () => Array.from(pinnedNodes),
920
+ setDragMode: (enabled) => {
921
+ internalDragEnabledRef.current = enabled;
922
+ }
923
+ }),
924
+ [nodes, pinnedNodes, restart, width, height]
925
+ );
926
+ useEffect(() => {
927
+ try {
928
+ if (typeof onManualLayoutChange === "function") onManualLayoutChange(manualLayout);
929
+ } catch (e) {
930
+ }
931
+ }, [manualLayout, onManualLayoutChange]);
932
+ useEffect(() => {
933
+ if (!enableZoom || !svgRef.current || !gRef.current) return;
934
+ const svg = d33.select(svgRef.current);
935
+ const g = d33.select(gRef.current);
936
+ const zoom2 = d33.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
937
+ g.attr("transform", event.transform);
938
+ setTransform(event.transform);
939
+ });
940
+ svg.call(zoom2);
941
+ return () => {
942
+ svg.on(".zoom", null);
943
+ };
944
+ }, [enableZoom]);
945
+ const handleDragStart = useCallback(
946
+ (event, node) => {
947
+ if (!enableDrag) return;
948
+ event.preventDefault();
949
+ event.stopPropagation();
950
+ dragActiveRef.current = true;
951
+ dragNodeRef.current = node;
952
+ node.fx = node.x;
953
+ node.fy = node.y;
954
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
955
+ try {
956
+ stop();
957
+ } catch (e) {
958
+ }
959
+ },
960
+ [enableDrag, restart]
961
+ );
962
+ useEffect(() => {
963
+ if (!enableDrag) return;
964
+ const handleWindowMove = (event) => {
965
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
966
+ const svg = svgRef.current;
967
+ if (!svg) return;
968
+ 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;
971
+ dragNodeRef.current.fx = x;
972
+ dragNodeRef.current.fy = y;
973
+ };
974
+ const handleWindowUp = () => {
975
+ if (!dragActiveRef.current) return;
976
+ try {
977
+ setForcesEnabled(true);
978
+ restart();
979
+ } catch (e) {
980
+ }
981
+ dragNodeRef.current = null;
982
+ dragActiveRef.current = false;
983
+ };
984
+ const handleWindowLeave = (event) => {
985
+ if (event.relatedTarget === null) handleWindowUp();
986
+ };
987
+ window.addEventListener("mousemove", handleWindowMove);
988
+ window.addEventListener("mouseup", handleWindowUp);
989
+ window.addEventListener("mouseout", handleWindowLeave);
990
+ window.addEventListener("blur", handleWindowUp);
991
+ return () => {
992
+ window.removeEventListener("mousemove", handleWindowMove);
993
+ window.removeEventListener("mouseup", handleWindowUp);
994
+ window.removeEventListener("mouseout", handleWindowLeave);
995
+ window.removeEventListener("blur", handleWindowUp);
996
+ };
997
+ }, [enableDrag, transform]);
998
+ useEffect(() => {
999
+ if (!gRef.current || !enableDrag) return;
1000
+ const g = d33.select(gRef.current);
1001
+ const dragBehavior = d33.drag().on("start", function(event) {
1002
+ try {
1003
+ const target = event.sourceEvent && event.sourceEvent.target || event.target;
1004
+ const grp = target.closest?.("g.node");
1005
+ const id = grp?.getAttribute("data-id");
1006
+ if (!id) return;
1007
+ const node = nodes.find((n) => n.id === id);
1008
+ if (!node) return;
1009
+ if (!internalDragEnabledRef.current) return;
1010
+ if (!event.active) restart();
1011
+ dragActiveRef.current = true;
1012
+ dragNodeRef.current = node;
1013
+ node.fx = node.x;
1014
+ node.fy = node.y;
1015
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
1016
+ } catch (e) {
1017
+ }
1018
+ }).on("drag", function(event) {
1019
+ if (!dragActiveRef.current || !dragNodeRef.current) return;
1020
+ const svg = svgRef.current;
1021
+ if (!svg) return;
1022
+ const rect = svg.getBoundingClientRect();
1023
+ const x = (event.sourceEvent.clientX - rect.left - transform.x) / transform.k;
1024
+ const y = (event.sourceEvent.clientY - rect.top - transform.y) / transform.k;
1025
+ dragNodeRef.current.fx = x;
1026
+ dragNodeRef.current.fy = y;
1027
+ }).on("end", function() {
1028
+ try {
1029
+ setForcesEnabled(true);
1030
+ restart();
1031
+ } catch (e) {
1032
+ }
1033
+ dragNodeRef.current = null;
1034
+ dragActiveRef.current = false;
1035
+ });
1036
+ try {
1037
+ g.selectAll("g.node").call(dragBehavior);
1038
+ } catch (e) {
1039
+ }
1040
+ return () => {
1041
+ try {
1042
+ g.selectAll("g.node").on(".drag", null);
1043
+ } catch (e) {
1044
+ }
1045
+ };
1046
+ }, [gRef, enableDrag, nodes, transform, restart]);
1047
+ const handleNodeClick = useCallback(
1048
+ (node) => {
1049
+ onNodeClick?.(node);
1050
+ },
1051
+ [onNodeClick]
1052
+ );
1053
+ const handleNodeDoubleClick = useCallback(
1054
+ (event, node) => {
1055
+ event.stopPropagation();
1056
+ if (!enableDrag) return;
1057
+ if (node.fx === null || node.fx === void 0) {
1058
+ node.fx = node.x;
1059
+ node.fy = node.y;
1060
+ setPinnedNodes((prev) => /* @__PURE__ */ new Set([...prev, node.id]));
1061
+ } else {
1062
+ node.fx = null;
1063
+ node.fy = null;
1064
+ setPinnedNodes((prev) => {
1065
+ const next = new Set(prev);
1066
+ next.delete(node.id);
1067
+ return next;
1068
+ });
1069
+ }
1070
+ restart();
1071
+ },
1072
+ [enableDrag, restart]
1073
+ );
1074
+ const handleCanvasDoubleClick = useCallback(() => {
1075
+ nodes.forEach((node) => {
1076
+ node.fx = null;
1077
+ node.fy = null;
1078
+ });
1079
+ setPinnedNodes(/* @__PURE__ */ new Set());
1080
+ restart();
1081
+ }, [nodes, restart]);
1082
+ const handleNodeMouseEnter = useCallback(
1083
+ (node) => {
1084
+ onNodeHover?.(node);
1085
+ },
1086
+ [onNodeHover]
1087
+ );
1088
+ const handleNodeMouseLeave = useCallback(() => {
1089
+ onNodeHover?.(null);
1090
+ }, [onNodeHover]);
1091
+ const handleLinkClick = useCallback(
1092
+ (link) => {
1093
+ onLinkClick?.(link);
1094
+ },
1095
+ [onLinkClick]
1096
+ );
1097
+ return /* @__PURE__ */ jsxs(
1098
+ "svg",
1099
+ {
1100
+ ref: svgRef,
1101
+ width,
1102
+ height,
1103
+ className: cn("bg-white dark:bg-gray-900", className),
1104
+ onDoubleClick: handleCanvasDoubleClick,
1105
+ children: [
1106
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
1107
+ "marker",
1108
+ {
1109
+ id: "arrow",
1110
+ viewBox: "0 0 10 10",
1111
+ refX: "20",
1112
+ refY: "5",
1113
+ markerWidth: "6",
1114
+ markerHeight: "6",
1115
+ orient: "auto",
1116
+ children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: defaultLinkColor })
1117
+ }
1118
+ ) }),
1119
+ /* @__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
+ }),
1212
+ 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
1213
  /* @__PURE__ */ jsx(
843
- "line",
1214
+ "circle",
844
1215
  {
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)
1216
+ cx: b.x,
1217
+ cy: b.y,
1218
+ r: b.r,
1219
+ fill: "rgba(148,163,184,0.06)",
1220
+ stroke: "#475569",
1221
+ strokeWidth: 2,
1222
+ strokeDasharray: "6 6",
1223
+ opacity: 0.9
854
1224
  }
855
1225
  ),
856
- showLinkLabels && link.label && /* @__PURE__ */ jsx(
1226
+ /* @__PURE__ */ jsx(
857
1227
  "text",
858
1228
  {
859
- x: (source.x + target.x) / 2,
860
- y: (source.y + target.y) / 2,
861
- fill: "#666",
862
- fontSize: "10",
1229
+ x: b.x,
1230
+ y: Math.max(12, b.y - b.r + 14),
1231
+ fill: "#475569",
1232
+ fontSize: 11,
863
1233
  textAnchor: "middle",
864
- dominantBaseline: "middle",
865
1234
  pointerEvents: "none",
866
- children: link.label
1235
+ children: pid.replace(/^pkg:/, "")
867
1236
  }
868
1237
  )
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",
1238
+ ] }, pid)) })
1239
+ ] })
1240
+ ]
1241
+ }
1242
+ );
1243
+ }
1244
+ );
1245
+ ForceDirectedGraph.displayName = "ForceDirectedGraph";
1246
+ var GraphControls = ({
1247
+ dragEnabled = true,
1248
+ onDragToggle,
1249
+ manualLayout = false,
1250
+ onManualLayoutToggle,
1251
+ onPinAll,
1252
+ onUnpinAll,
1253
+ onReset,
1254
+ onFitView,
1255
+ pinnedCount = 0,
1256
+ totalNodes = 0,
1257
+ visible = true,
1258
+ position = "top-left",
1259
+ className
1260
+ }) => {
1261
+ if (!visible) return null;
1262
+ const positionClasses = {
1263
+ "top-left": "top-4 left-4",
1264
+ "top-right": "top-4 right-4",
1265
+ "bottom-left": "bottom-4 left-4",
1266
+ "bottom-right": "bottom-4 right-4"
1267
+ };
1268
+ const ControlButton = ({ onClick, active = false, icon, label, disabled = false }) => /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
1269
+ /* @__PURE__ */ jsx(
1270
+ "button",
1271
+ {
1272
+ onClick,
1273
+ disabled,
1274
+ className: cn(
1275
+ "p-2 rounded-lg transition-all duration-200",
1276
+ active ? "bg-blue-500 text-white shadow-md hover:bg-blue-600" : "bg-gray-100 text-gray-700 hover:bg-gray-200",
1277
+ disabled && "opacity-50 cursor-not-allowed hover:bg-gray-100",
1278
+ "dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-blue-600"
1279
+ ),
1280
+ title: label,
1281
+ children: /* @__PURE__ */ jsx("span", { className: "text-lg", children: icon })
1282
+ }
1283
+ ),
1284
+ /* @__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 })
1285
+ ] });
1286
+ return /* @__PURE__ */ jsxs(
1287
+ "div",
1288
+ {
1289
+ className: cn(
1290
+ "fixed z-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-2 border border-gray-200 dark:border-gray-700",
1291
+ positionClasses[position],
1292
+ className
1293
+ ),
1294
+ children: [
1295
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1296
+ /* @__PURE__ */ jsx(
1297
+ ControlButton,
1298
+ {
1299
+ onClick: () => onDragToggle?.(!dragEnabled),
1300
+ active: dragEnabled,
1301
+ icon: "\u270B",
1302
+ label: dragEnabled ? "Drag enabled" : "Drag disabled"
1303
+ }
1304
+ ),
1305
+ /* @__PURE__ */ jsx(
1306
+ ControlButton,
1307
+ {
1308
+ onClick: () => onManualLayoutToggle?.(!manualLayout),
1309
+ active: manualLayout,
1310
+ icon: "\u{1F527}",
1311
+ label: manualLayout ? "Manual layout: ON (drag freely)" : "Manual layout: OFF (forces active)"
1312
+ }
1313
+ ),
1314
+ /* @__PURE__ */ jsx("div", { className: "w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" }),
1315
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1316
+ /* @__PURE__ */ jsx(
1317
+ ControlButton,
1318
+ {
1319
+ onClick: () => onPinAll?.(),
1320
+ disabled: totalNodes === 0,
1321
+ icon: "\u{1F4CC}",
1322
+ label: `Pin all nodes (${totalNodes})`
1323
+ }
1324
+ ),
1325
+ /* @__PURE__ */ jsx(
1326
+ ControlButton,
879
1327
  {
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
- })
1328
+ onClick: () => onUnpinAll?.(),
1329
+ disabled: pinnedCount === 0,
1330
+ icon: "\u{1F4CD}",
1331
+ label: `Unpin all (${pinnedCount} pinned)`
1332
+ }
1333
+ )
1334
+ ] }),
1335
+ /* @__PURE__ */ jsx("div", { className: "w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" }),
1336
+ /* @__PURE__ */ jsx(
1337
+ ControlButton,
1338
+ {
1339
+ onClick: () => onFitView?.(),
1340
+ disabled: totalNodes === 0,
1341
+ icon: "\u{1F3AF}",
1342
+ label: "Fit all nodes in view"
1343
+ }
1344
+ ),
1345
+ /* @__PURE__ */ jsx(
1346
+ ControlButton,
1347
+ {
1348
+ onClick: () => onReset?.(),
1349
+ disabled: totalNodes === 0,
1350
+ icon: "\u21BA",
1351
+ label: "Reset to auto-layout"
1352
+ }
1353
+ )
1354
+ ] }),
1355
+ /* @__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: [
1356
+ /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
1357
+ /* @__PURE__ */ jsx("strong", { children: "Nodes:" }),
1358
+ " ",
1359
+ totalNodes
1360
+ ] }),
1361
+ pinnedCount > 0 && /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
1362
+ /* @__PURE__ */ jsx("strong", { children: "Pinned:" }),
1363
+ " ",
1364
+ pinnedCount
1365
+ ] }),
1366
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 text-gray-500 dark:text-gray-500 leading-snug", children: [
1367
+ /* @__PURE__ */ jsx("strong", { children: "Tips:" }),
1368
+ /* @__PURE__ */ jsxs("ul", { className: "mt-1 ml-1 space-y-0.5", children: [
1369
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Drag nodes to reposition" }),
1370
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Double-click to pin/unpin" }),
1371
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Double-click canvas to unpin all" }),
1372
+ /* @__PURE__ */ jsx("li", { children: "\u2022 Scroll to zoom" })
1373
+ ] })
1374
+ ] })
918
1375
  ] })
919
1376
  ]
920
1377
  }
921
1378
  );
922
1379
  };
923
- ForceDirectedGraph.displayName = "ForceDirectedGraph";
1380
+ GraphControls.displayName = "GraphControls";
924
1381
 
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 };
1382
+ 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
1383
  //# sourceMappingURL=index.js.map
927
1384
  //# sourceMappingURL=index.js.map