modviz 0.1.2

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.
Files changed (109) hide show
  1. package/README.md +261 -0
  2. package/dist/client/_shell.html +0 -0
  3. package/dist/client/android-chrome-192x192.png +0 -0
  4. package/dist/client/android-chrome-512x512.png +0 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/app-Sjrldkrg.css +2 -0
  7. package/dist/client/assets/button-aOWckyNs.js +1 -0
  8. package/dist/client/assets/check-C0EQe2S8.js +1 -0
  9. package/dist/client/assets/chevron-down-DrspihmT.js +1 -0
  10. package/dist/client/assets/chevron-right-DIJHr8AN.js +1 -0
  11. package/dist/client/assets/colors-CQoWjU5E.js +1 -0
  12. package/dist/client/assets/command-kkF7_wdz.js +45 -0
  13. package/dist/client/assets/compare-K6jVFsiI.js +1 -0
  14. package/dist/client/assets/compare-TOnoe1EP.js +2 -0
  15. package/dist/client/assets/configure-DnlSnhtN.js +1 -0
  16. package/dist/client/assets/explorer-C7NclVKg.js +2 -0
  17. package/dist/client/assets/explorer-Xu2X6XXF.js +1 -0
  18. package/dist/client/assets/external-link-B9eNA-li.js +1 -0
  19. package/dist/client/assets/flamegraph-CRVZSAlj.js +13 -0
  20. package/dist/client/assets/floating-ui.dom-DLIT5tPE.js +1 -0
  21. package/dist/client/assets/formatting-CiC0SYI8.js +1 -0
  22. package/dist/client/assets/graph-6Vr74V1k.js +2 -0
  23. package/dist/client/assets/graph-CVzypIGU.js +2 -0
  24. package/dist/client/assets/graph-command-menu-D2MoVT2B.js +4 -0
  25. package/dist/client/assets/graph-command-menu-VWiiW3qy.css +1 -0
  26. package/dist/client/assets/hierarchy-C8xxGb_u.js +2 -0
  27. package/dist/client/assets/hierarchy-iO7d4oSK.js +2 -0
  28. package/dist/client/assets/import-display-D-jRyyjM.js +5 -0
  29. package/dist/client/assets/imports-CPggnrs-.js +2 -0
  30. package/dist/client/assets/imports-CodbPyUJ.js +1 -0
  31. package/dist/client/assets/index-Dj_rhLdR.js +12 -0
  32. package/dist/client/assets/input-BCFMF0aR.js +1 -0
  33. package/dist/client/assets/jsx-runtime-DWSWI4JT.js +1 -0
  34. package/dist/client/assets/lazyRouteComponent-PTSyFp1J.js +1 -0
  35. package/dist/client/assets/loading-state-CyC_hrTF.js +1 -0
  36. package/dist/client/assets/modviz-data-BiRqoDI5.js +1 -0
  37. package/dist/client/assets/modviz-layout-Do93E-IB.js +1 -0
  38. package/dist/client/assets/modviz-sigma-Xl8qHaxK.js +312 -0
  39. package/dist/client/assets/portal-BgAm3V3j.js +1 -0
  40. package/dist/client/assets/routes-DBtN8hrZ.js +1 -0
  41. package/dist/client/assets/schemas-B4zfTepZ.js +39 -0
  42. package/dist/client/assets/search-BYHxNrYn.js +1 -0
  43. package/dist/client/assets/search-params-BaZRBvGI.js +1 -0
  44. package/dist/client/assets/setup-view-j1o0TuZz.js +1 -0
  45. package/dist/client/assets/summary-D703Zh3x.js +1 -0
  46. package/dist/client/assets/tooltip-B1VDU9HG.js +1 -0
  47. package/dist/client/assets/trace-B67CM5s2.js +2 -0
  48. package/dist/client/assets/trace-Bwwdw3AM.js +1 -0
  49. package/dist/client/assets/treemap-BZf2shzY.js +5 -0
  50. package/dist/client/assets/treemap-Csroy8Gy.js +2 -0
  51. package/dist/client/assets/utils-DkkZd0ys.js +1 -0
  52. package/dist/client/favicon-16x16.png +0 -0
  53. package/dist/client/favicon-32x32.png +0 -0
  54. package/dist/client/favicon.ico +0 -0
  55. package/dist/client/favicon.png +0 -0
  56. package/dist/client/site.webmanifest +19 -0
  57. package/dist/mod/cli-options.js +225 -0
  58. package/dist/mod/cli.js +519 -0
  59. package/dist/mod/index.js +3 -0
  60. package/dist/mod/llm-analysis.js +29 -0
  61. package/dist/mod/llm-output.js +742 -0
  62. package/dist/mod/module-graph-plugins.js +60 -0
  63. package/dist/mod/production-server.js +103 -0
  64. package/dist/mod/runtime-host.js +217 -0
  65. package/dist/mod/snapshot-history.js +73 -0
  66. package/dist/mod/types.js +3 -0
  67. package/dist/server/assets/__23tanstack-start-plugin-adapters-3QxJs4a0.js +5 -0
  68. package/dist/server/assets/_tanstack-start-manifest_v-DMytuIue.js +188 -0
  69. package/dist/server/assets/button-Bqnnid5i.js +41 -0
  70. package/dist/server/assets/colors-DhAxrYua.js +100 -0
  71. package/dist/server/assets/command-SdxShIbL.js +138 -0
  72. package/dist/server/assets/compare-BFMiiUsB.js +562 -0
  73. package/dist/server/assets/compare-CpOqTpYu.js +10 -0
  74. package/dist/server/assets/configure-Bvd45DTI.js +288 -0
  75. package/dist/server/assets/explorer-C7dODpSv.js +379 -0
  76. package/dist/server/assets/explorer-CpSb0JTa.js +20 -0
  77. package/dist/server/assets/flamegraph-CdW-VG6I.js +198 -0
  78. package/dist/server/assets/formatting-iDlL4tA-.js +4 -0
  79. package/dist/server/assets/graph-C1G9H5O4.js +438 -0
  80. package/dist/server/assets/graph-DAGFGioS.js +45 -0
  81. package/dist/server/assets/graph-command-menu-BV5GtOWx.js +249 -0
  82. package/dist/server/assets/hierarchy-B4K-Zfn9.js +16 -0
  83. package/dist/server/assets/hierarchy-BGpWSG-f.js +104 -0
  84. package/dist/server/assets/import-display-BVIOWcsm.js +124 -0
  85. package/dist/server/assets/imports-B6JBDl_h.js +379 -0
  86. package/dist/server/assets/imports-BGe5tZJT.js +28 -0
  87. package/dist/server/assets/input-C5r-hBix.js +19 -0
  88. package/dist/server/assets/loading-state-CrvCWTtw.js +23 -0
  89. package/dist/server/assets/modviz-data-CUyTorv0.js +197 -0
  90. package/dist/server/assets/modviz-layout-BAH2ogse.js +253 -0
  91. package/dist/server/assets/modviz-server-DoMlAyFW.js +195 -0
  92. package/dist/server/assets/modviz-sigma-XYxARWqd.js +1441 -0
  93. package/dist/server/assets/rolldown-runtime-rSIU-vHC.js +13 -0
  94. package/dist/server/assets/router-DYJ-zDbU.js +353 -0
  95. package/dist/server/assets/routes-DInCacpY.js +244 -0
  96. package/dist/server/assets/search-params-BNApPgkX.js +26 -0
  97. package/dist/server/assets/setup-view-DjI49Iqr.js +91 -0
  98. package/dist/server/assets/start-Ba3KII43.js +4 -0
  99. package/dist/server/assets/summary-z3lXkLCQ.js +208 -0
  100. package/dist/server/assets/tooltip-Ck0DDfF7.js +24 -0
  101. package/dist/server/assets/trace-ColKOf9g.js +16 -0
  102. package/dist/server/assets/trace-eVs-hIZO.js +578 -0
  103. package/dist/server/assets/treemap-BbZ9M4GF.js +17 -0
  104. package/dist/server/assets/treemap-CrgWFoCF.js +912 -0
  105. package/dist/server/assets/utils-BQZm0uva.js +8 -0
  106. package/dist/server/server.js +5259 -0
  107. package/dist/shared/modviz-compare.js +120 -0
  108. package/dist/shared/modviz-trace.js +244 -0
  109. package/package.json +135 -0
@@ -0,0 +1,1441 @@
1
+ import { t as Flamegraph } from "./flamegraph-CdW-VG6I.js";
2
+ import { a as PopoverTrigger, c as hoveredClusterNameAtom, i as PopoverContent, l as isNodeDetailsOpenAtom, n as GraphCommandMenuDialog, o as currentNodeIdAtom, r as Popover, s as highlightedNodeIdAtom, u as selectedNodeIdsAtom } from "./graph-command-menu-BV5GtOWx.js";
3
+ import { t as cn } from "./utils-BQZm0uva.js";
4
+ import { t as Button } from "./button-Bqnnid5i.js";
5
+ import { t as Input } from "./input-C5r-hBix.js";
6
+ import { i as TooltipTrigger, r as TooltipContent, t as Tooltip } from "./tooltip-Ck0DDfF7.js";
7
+ import { c as getExternalPackageName, l as getNodeGroupingLabel } from "./modviz-data-CUyTorv0.js";
8
+ import { n as getRandom, t as colors } from "./colors-DhAxrYua.js";
9
+ import { t as LoadingState } from "./loading-state-CrvCWTtw.js";
10
+ import { a as CommandInput, i as CommandGroup, o as CommandItem, r as CommandEmpty, s as CommandList, t as Command } from "./command-SdxShIbL.js";
11
+ import * as React from "react";
12
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
+ import { Link } from "@tanstack/react-router";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ import { ControlsContainer, FullScreenControl, SigmaContainer, ZoomControl, useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from "@react-sigma/core";
16
+ import { MiniMap } from "@react-sigma/minimap";
17
+ import { fitViewportToNodes } from "@sigma/utils";
18
+ import { CheckIcon, ChevronDownIcon, ChevronRight, ChevronsUpDown, File, LocateFixed, RotateCcw } from "lucide-react";
19
+ import { Dialog } from "@ark-ui/react/dialog";
20
+ import { Portal } from "@ark-ui/react/portal";
21
+ import { Tabs } from "@ark-ui/react/tabs";
22
+ import { useAtom } from "@xstate/store/react";
23
+ import { LuArrowDownFromLine, LuArrowRight, LuArrowUpToLine, LuBug, LuInfinity, LuMaximize, LuMinimize, LuOctagonMinus } from "react-icons/lu";
24
+ import { useFilter } from "@ark-ui/react/locale";
25
+ import { TreeView, createTreeCollection, useTreeView } from "@ark-ui/react/tree-view";
26
+ import { ark } from "@ark-ui/react/factory";
27
+ import { Select, createListCollection, selectAnatomy } from "@ark-ui/react/select";
28
+ import { useWorkerLayoutForceAtlas2 } from "@react-sigma/layout-forceatlas2";
29
+ import { useLayoutNoverlap } from "@react-sigma/layout-noverlap";
30
+ import { DirectedGraph } from "graphology";
31
+ //#region src/components/tree-view/basic.tsx
32
+ var TreeNodeName = (props) => {
33
+ const { node } = props;
34
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
35
+ /* @__PURE__ */ jsxs(Tooltip, {
36
+ lazyMount: true,
37
+ children: [/* @__PURE__ */ jsx(TooltipTrigger, { children: /* @__PURE__ */ jsxs("span", {
38
+ className: cn("font-medium whitespace-nowrap", node.original.isBarrelFile && "underline decoration-amber-600 decoration-2", props.isCircular && "text-red-400"),
39
+ children: [
40
+ (node.original.isBarrelFile || props.isCircular) && "⚠️ ",
41
+ node.name,
42
+ node.original.isBarrelFile && ` (barrel ${node.original.exports.length} exports)`,
43
+ props.isCircular && " (circular)",
44
+ props.hasReachedMaxDepth && " (max depth reached)"
45
+ ]
46
+ }) }), /* @__PURE__ */ jsx(TooltipContent, {
47
+ className: "max-w-[auto]",
48
+ children: props.visited?.size ? /* @__PURE__ */ jsxs("div", {
49
+ className: "flex flex-col gap-2",
50
+ children: [
51
+ /* @__PURE__ */ jsx("span", { children: "Imported by:" }),
52
+ [...props.visited].map((nodeId, index) => /* @__PURE__ */ jsxs("span", {
53
+ className: cn(nodeId === node.id && "text-red-400"),
54
+ style: { paddingLeft: `${index * 10}px` },
55
+ children: [
56
+ "->",
57
+ " ",
58
+ nodeId
59
+ ]
60
+ }, nodeId)),
61
+ /* @__PURE__ */ jsxs("span", {
62
+ className: "text-red-400",
63
+ style: { paddingLeft: `${[...props.visited].length * 10}px` },
64
+ children: [
65
+ "->",
66
+ " ",
67
+ node.id
68
+ ]
69
+ })
70
+ ]
71
+ }) : node.id
72
+ })]
73
+ }),
74
+ props.focusedNodeId !== node.id && /* @__PURE__ */ jsx(Button, {
75
+ className: "ml-auto",
76
+ variant: "ghost",
77
+ size: "icon",
78
+ onClickCapture: (e) => {
79
+ e.stopPropagation();
80
+ currentNodeIdAtom.set(node.id);
81
+ isNodeDetailsOpenAtom.set(true);
82
+ },
83
+ children: /* @__PURE__ */ jsx(LuArrowRight, { className: "text-slate-400" })
84
+ }),
85
+ /* @__PURE__ */ jsx(Button, {
86
+ className: "ml-auto",
87
+ variant: "ghost",
88
+ size: "icon",
89
+ onClickCapture: (e) => {
90
+ e.stopPropagation();
91
+ console.log(node);
92
+ },
93
+ children: /* @__PURE__ */ jsx(LuBug, { className: "text-slate-400" })
94
+ })
95
+ ] });
96
+ };
97
+ var TreeNode = (props) => {
98
+ const { node, indexPath } = props;
99
+ return /* @__PURE__ */ jsx(TreeView.NodeProvider, {
100
+ node,
101
+ indexPath,
102
+ children: node.children?.length ? /* @__PURE__ */ jsxs(TreeView.Branch, {
103
+ onClick: () => console.log(indexPath, node),
104
+ children: [/* @__PURE__ */ jsxs(TreeView.BranchControl, {
105
+ className: "group flex w-full items-center gap-2 rounded-lg py-0.5 text-sm font-medium whitespace-nowrap transition-all duration-200 hover:bg-slate-50 dark:hover:bg-slate-800/50 data-[state=open]:text-slate-900 dark:data-[state=open]:text-slate-100",
106
+ children: [/* @__PURE__ */ jsx(TreeView.BranchIndicator, {
107
+ className: "flex h-4 w-4 shrink-0 items-center justify-center",
108
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5 text-slate-400 transition-transform duration-200 group-data-[state=open]:rotate-90 group-data-[state=open]:text-slate-600" })
109
+ }), /* @__PURE__ */ jsx(TreeView.BranchText, {
110
+ className: "flex items-center gap-2.5 text-slate-700 dark:text-slate-300 group-data-[state=open]:text-slate-900 dark:group-data-[state=open]:text-slate-100",
111
+ children: /* @__PURE__ */ jsx(TreeNodeName, {
112
+ node,
113
+ focusedNodeId: props.focusedNodeId
114
+ })
115
+ })]
116
+ }), /* @__PURE__ */ jsxs(TreeView.BranchContent, {
117
+ className: "ml-6 border-l border-slate-200 pl-2 dark:border-slate-700/60",
118
+ children: [/* @__PURE__ */ jsx(TreeView.BranchIndentGuide, {}), node.children.map((child, index) => props.visited.has(child.id) ? /* @__PURE__ */ jsxs(TreeView.Item, {
119
+ onClick: () => console.log([...indexPath, index], child),
120
+ className: "group flex w-full items-center gap-2 rounded-lg py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 hover:bg-slate-50 dark:hover:bg-slate-800/50 data-[selected]:bg-blue-50 dark:data-[selected]:bg-blue-900/30 data-[selected]:text-blue-700 dark:data-[selected]:text-blue-300 data-[selected]:shadow-sm data-[selected]:ring-1 data-[selected]:ring-blue-200 dark:data-[selected]:ring-blue-800/30",
121
+ children: [/* @__PURE__ */ jsx("div", {
122
+ className: "flex h-4 w-4 shrink-0 items-center justify-center",
123
+ children: /* @__PURE__ */ jsx("div", { className: "h-1.5 w-1.5 rounded-full bg-slate-300 dark:bg-slate-600 group-data-[selected]:bg-blue-500" })
124
+ }), /* @__PURE__ */ jsxs(TreeView.ItemText, {
125
+ className: "flex items-center gap-2.5 text-slate-600 dark:text-slate-400 group-data-[selected]:text-blue-700 dark:group-data-[selected]:text-blue-300",
126
+ children: [/* @__PURE__ */ jsx(LuInfinity, { className: "h-4 w-4 text-slate-400 group-data-[selected]:text-blue-500" }), /* @__PURE__ */ jsx(TreeNodeName, {
127
+ node: child,
128
+ isCircular: true,
129
+ visited: props.visited,
130
+ focusedNodeId: props.focusedNodeId
131
+ })]
132
+ })]
133
+ }, child.id) : props.currentDepth >= props.maxDepth ? /* @__PURE__ */ jsxs(TreeView.Item, {
134
+ onClick: () => console.log([...indexPath, index], child),
135
+ className: "group flex w-full items-center gap-2 rounded-lg py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 hover:bg-slate-50 dark:hover:bg-slate-800/50 data-[selected]:bg-blue-50 dark:data-[selected]:bg-blue-900/30 data-[selected]:text-blue-700 dark:data-[selected]:text-blue-300 data-[selected]:shadow-sm data-[selected]:ring-1 data-[selected]:ring-blue-200 dark:data-[selected]:ring-blue-800/30",
136
+ children: [/* @__PURE__ */ jsx("div", {
137
+ className: "flex h-4 w-4 shrink-0 items-center justify-center",
138
+ children: /* @__PURE__ */ jsx("div", { className: "h-1.5 w-1.5 rounded-full bg-slate-300 dark:bg-slate-600 group-data-[selected]:bg-blue-500" })
139
+ }), /* @__PURE__ */ jsxs(TreeView.ItemText, {
140
+ className: "flex items-center gap-2.5 text-slate-600 dark:text-slate-400 group-data-[selected]:text-blue-700 dark:group-data-[selected]:text-blue-300",
141
+ children: [/* @__PURE__ */ jsx(LuOctagonMinus, { className: "h-4 w-4 text-slate-400 group-data-[selected]:text-blue-500" }), /* @__PURE__ */ jsx(TreeNodeName, {
142
+ node: child,
143
+ hasReachedMaxDepth: true,
144
+ focusedNodeId: props.focusedNodeId
145
+ })]
146
+ })]
147
+ }, child.id) : /* @__PURE__ */ jsx(TreeNode, {
148
+ node: child,
149
+ indexPath: [...indexPath, index],
150
+ visited: new Set([...props.visited, child.id]),
151
+ currentDepth: props.currentDepth + 1,
152
+ maxDepth: props.maxDepth,
153
+ focusedNodeId: props.focusedNodeId
154
+ }, child.id))]
155
+ })]
156
+ }) : /* @__PURE__ */ jsxs(TreeView.Item, {
157
+ onClick: () => console.log(indexPath, node),
158
+ className: "group flex w-full items-center gap-2 rounded-lg py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 hover:bg-slate-50 dark:hover:bg-slate-800/50 data-[selected]:bg-blue-50 dark:data-[selected]:bg-blue-900/30 data-[selected]:text-blue-700 dark:data-[selected]:text-blue-300 data-[selected]:shadow-sm data-[selected]:ring-1 data-[selected]:ring-blue-200 dark:data-[selected]:ring-blue-800/30",
159
+ children: [/* @__PURE__ */ jsx("div", {
160
+ className: "flex h-4 w-4 shrink-0 items-center justify-center",
161
+ children: /* @__PURE__ */ jsx("div", { className: "h-1.5 w-1.5 rounded-full bg-slate-300 dark:bg-slate-600 group-data-[selected]:bg-blue-500" })
162
+ }), /* @__PURE__ */ jsxs(TreeView.ItemText, {
163
+ className: "flex items-center gap-2.5 text-slate-600 dark:text-slate-400 group-data-[selected]:text-blue-700 dark:group-data-[selected]:text-blue-300",
164
+ children: [/* @__PURE__ */ jsx(File, { className: "h-4 w-4 text-slate-400 group-data-[selected]:text-blue-500" }), /* @__PURE__ */ jsx(TreeNodeName, {
165
+ node,
166
+ focusedNodeId: props.focusedNodeId
167
+ })]
168
+ })]
169
+ })
170
+ }, node.id);
171
+ };
172
+ function TreeViewBasic(props) {
173
+ const { contains } = useFilter({ sensitivity: "base" });
174
+ const [collection, setCollection] = useState(props.initialCollection);
175
+ const filter = (value) => {
176
+ setCollection(value.length > 0 ? collection.filter((node) => contains(node.name, value)) : props.initialCollection);
177
+ };
178
+ const treeView = useTreeView({
179
+ id: props.entryNodeId,
180
+ collection,
181
+ defaultExpandedValue: props.defaultExpandedValue ?? [props.entryNodeId],
182
+ selectedValue: []
183
+ });
184
+ const flattened = treeView.collection.flatten();
185
+ const visible = treeView.getVisibleNodes().map(({ node }) => node.id);
186
+ const flattenedUniqueNodeIds = new Set(flattened.map((node) => node.id));
187
+ const isAllExpanded = treeView.expandedValue.length === flattenedUniqueNodeIds.size;
188
+ return /* @__PURE__ */ jsxs("div", {
189
+ className: "w-full h-full min-h-0 flex flex-col gap-2",
190
+ children: [/* @__PURE__ */ jsxs("div", {
191
+ className: "flex gap-2",
192
+ children: [/* @__PURE__ */ jsx(Input, {
193
+ placeholder: "Search",
194
+ onChange: (e) => filter(e.target.value)
195
+ }), /* @__PURE__ */ jsx("div", {
196
+ className: "ml-auto",
197
+ children: isAllExpanded ? /* @__PURE__ */ jsx(Button, {
198
+ variant: "outline",
199
+ className: "px-2 py-1",
200
+ onClick: () => treeView.collapse(),
201
+ children: /* @__PURE__ */ jsx(LuArrowUpToLine, {
202
+ className: "h-4 w-4 text-slate-400",
203
+ children: /* @__PURE__ */ jsx("span", {
204
+ className: "sr-only",
205
+ children: "Collapse all"
206
+ })
207
+ })
208
+ }) : /* @__PURE__ */ jsx(Button, {
209
+ variant: "outline",
210
+ className: "px-2 py-1",
211
+ onClick: () => flattened.length <= 500 ? treeView.expand() : visible.length <= 500 ? treeView.expand(visible) : treeView.expand(visible.slice(0, 250)),
212
+ children: /* @__PURE__ */ jsx(LuArrowDownFromLine, {
213
+ className: "h-4 w-4 text-slate-400",
214
+ children: /* @__PURE__ */ jsx("span", {
215
+ className: "sr-only",
216
+ children: "Expand all"
217
+ })
218
+ })
219
+ })
220
+ })]
221
+ }), /* @__PURE__ */ jsx("div", {
222
+ className: "w-full h-full min-h-0 overflow-auto",
223
+ children: /* @__PURE__ */ jsx(TreeView.RootProvider, {
224
+ value: treeView,
225
+ lazyMount: true,
226
+ unmountOnExit: true,
227
+ children: /* @__PURE__ */ jsx(TreeView.Tree, { children: collection.rootNode.children?.map((node, index) => /* @__PURE__ */ jsx(TreeNode, {
228
+ node,
229
+ indexPath: [index],
230
+ visited: new Set([node.id]),
231
+ currentDepth: 0,
232
+ maxDepth: 100,
233
+ focusedNodeId: props.entryNodeId
234
+ }, node.id)) })
235
+ })
236
+ })]
237
+ });
238
+ }
239
+ //#endregion
240
+ //#region src/components/tree-view/map-modviz-output-to-tree-collection.ts
241
+ function mapModvizOutputToImporteesTreeCollection(output, entryNodeId) {
242
+ const entryNode = output.nodes.find((node) => node.path === entryNodeId);
243
+ if (!entryNode) return null;
244
+ const rootNode = {
245
+ id: entryNodeId,
246
+ name: entryNode.name,
247
+ children: [],
248
+ original: entryNode
249
+ };
250
+ const allVisited = /* @__PURE__ */ new Set();
251
+ const stack = [{
252
+ nodePath: entryNodeId,
253
+ parent: rootNode,
254
+ visited: new Set([entryNodeId])
255
+ }];
256
+ const root = stack[0].parent;
257
+ while (stack.length > 0) {
258
+ const { nodePath, parent, visited } = stack.pop();
259
+ allVisited.add(nodePath);
260
+ const node = output.nodes.find((node) => node.path === nodePath);
261
+ if (!node) continue;
262
+ const currentTreeNode = {
263
+ id: node.path,
264
+ name: node.path.split("/").slice(-2).join("/"),
265
+ children: [],
266
+ original: node
267
+ };
268
+ if (parent.children) parent.children.push(currentTreeNode);
269
+ node.importees.forEach((importee) => {
270
+ if (visited.has(importee)) {
271
+ const importeeNode = output.nodes.find((node) => node.path === importee);
272
+ if (!importeeNode) return;
273
+ const importeeTreeNode = {
274
+ id: importeeNode.path,
275
+ name: importeeNode.path.split("/").slice(-2).join("/"),
276
+ children: [],
277
+ original: importeeNode
278
+ };
279
+ currentTreeNode.children.push(importeeTreeNode);
280
+ return;
281
+ }
282
+ visited.add(importee);
283
+ stack.push({
284
+ nodePath: importee,
285
+ parent: currentTreeNode,
286
+ visited: new Set([...visited, importee])
287
+ });
288
+ });
289
+ }
290
+ return {
291
+ visited: allVisited,
292
+ collection: createTreeCollection({
293
+ nodeToValue: (node) => node.id,
294
+ nodeToString: (node) => node.name,
295
+ rootNode: root
296
+ })
297
+ };
298
+ }
299
+ function mapModvizOutputToImportsChainTreeCollection(output, currentNodeId, direction = "from-current-node-to-entrypoint") {
300
+ const entryNode = output.nodes.find((node) => node.path === currentNodeId);
301
+ if (!entryNode) return null;
302
+ const chain = entryNode.chain.at(0) ?? [];
303
+ const importChain = direction === "from-entrypoint-to-current-node" ? chain : chain.reverse();
304
+ const rootId = importChain[0];
305
+ if (!rootId) return null;
306
+ const root = output.nodes.find((node) => node.path === rootId);
307
+ if (!root) return null;
308
+ const rootNode = {
309
+ id: rootId,
310
+ name: root.path.split("/").slice(-2).join("/"),
311
+ children: [],
312
+ original: root
313
+ };
314
+ let currentParent = rootNode;
315
+ for (const nodePath of importChain) {
316
+ const node = output.nodes.find((node) => node.path === nodePath);
317
+ if (!node) continue;
318
+ const treeNode = {
319
+ id: node.path,
320
+ name: node.path.split("/").slice(-2).join("/"),
321
+ children: [],
322
+ original: node
323
+ };
324
+ currentParent.children.push(treeNode);
325
+ currentParent = treeNode;
326
+ }
327
+ return createTreeCollection({
328
+ nodeToValue: (node) => node.id,
329
+ nodeToString: (node) => node.name,
330
+ rootNode
331
+ });
332
+ }
333
+ //#endregion
334
+ //#region src/components/ui/select.tsx
335
+ var parts = selectAnatomy.extendWith("separator").build();
336
+ var SelectComponent = React.forwardRef((props, ref) => /* @__PURE__ */ jsx(Select.Root, {
337
+ ref,
338
+ ...props
339
+ }));
340
+ SelectComponent.displayName = "Select";
341
+ var Select$1 = SelectComponent;
342
+ var SelectClearTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.ClearTrigger, {
343
+ ref,
344
+ className: cn("absolute end-0 top-0 flex size-9 items-center justify-center rounded-md border border-transparent text-muted-foreground/80 outline-none transition-[color,box-shadow] hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", className),
345
+ ...props
346
+ }));
347
+ SelectClearTrigger.displayName = "SelectClearTrigger";
348
+ var SelectContent = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.Positioner, { children: /* @__PURE__ */ jsx(Select.Content, {
349
+ ref,
350
+ className: cn("relative w-full min-w-32 overflow-hidden rounded-md border border-input bg-popover p-1 text-popover-foreground shadow-lg", "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2 data-[placement=left]:-translate-x-1 data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2 data-[placement=top]:-translate-y-1 data-[placement=right]:translate-x-1 data-[placement=bottom]:translate-y-1", className),
351
+ ...props
352
+ }) }));
353
+ SelectContent.displayName = "SelectContent";
354
+ Select.Context;
355
+ var SelectControl = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.Control, {
356
+ ref,
357
+ className: cn("relative flex min-h-[38px] rounded-md border border-input text-sm outline-none transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40", className),
358
+ ...props
359
+ }));
360
+ SelectControl.displayName = "SelectControl";
361
+ var SelectIndicator = React.forwardRef((props, ref) => /* @__PURE__ */ jsx(Select.Indicator, {
362
+ ref,
363
+ ...props,
364
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 shrink-0 in-aria-invalid:text-destructive/80 text-muted-foreground/80" })
365
+ }));
366
+ SelectIndicator.displayName = "SelectIndicator";
367
+ var SelectItem = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(Select.Item, {
368
+ ref,
369
+ className: cn("relative flex w-full cursor-default select-none items-center rounded py-1.5 ps-8 pe-2 text-sm outline-hidden data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50", className),
370
+ ...props,
371
+ children: [/* @__PURE__ */ jsx("span", {
372
+ className: "absolute start-2 flex size-3.5 items-center justify-center",
373
+ children: /* @__PURE__ */ jsx(Select.ItemIndicator, { children: /* @__PURE__ */ jsx(CheckIcon, { size: 16 }) })
374
+ }), /* @__PURE__ */ jsx(Select.ItemText, { children })]
375
+ }));
376
+ SelectItem.displayName = "SelectItem";
377
+ Select.ItemContext;
378
+ Select.ItemGroup;
379
+ var SelectItemGroupLabel = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.ItemGroupLabel, {
380
+ ref,
381
+ className: cn("py-1.5 ps-8 pe-2 font-medium text-muted-foreground text-xs", className),
382
+ ...props
383
+ }));
384
+ SelectItemGroupLabel.displayName = "SelectItemGroupLabel";
385
+ Select.ItemText;
386
+ var SelectLabel = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.Label, {
387
+ ref,
388
+ className: cn("select-none font-medium text-foreground text-sm leading-4 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50", className),
389
+ ...props
390
+ }));
391
+ SelectLabel.displayName = "SelectLabel";
392
+ var SelectList = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.List, {
393
+ ref,
394
+ className: cn("max-h-[min(24rem,var(--available-height))] overflow-y-auto", className),
395
+ ...props
396
+ }));
397
+ SelectList.displayName = "SelectList";
398
+ Select.RootProvider;
399
+ var SelectSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(ark.hr, {
400
+ ref,
401
+ ...parts.separator.attrs,
402
+ className: cn("-mx-1 my-1 h-px bg-border", className),
403
+ ...props
404
+ }));
405
+ SelectSeparator.displayName = "SelectSeparator";
406
+ var SelectTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(Select.Trigger, {
407
+ ref,
408
+ className: cn("flex flex-1 items-center justify-between gap-1 bg-transparent px-3 py-2 outline-none outline-hidden placeholder:text-muted-foreground/70 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 data-[placeholder-shown]:text-muted-foreground", className),
409
+ ...props
410
+ }));
411
+ SelectTrigger.displayName = "SelectTrigger";
412
+ var SelectValueText = Select.ValueText;
413
+ //#endregion
414
+ //#region src/components/dialog/dialog.tsx
415
+ function NodeDetailsModal(props) {
416
+ const isOpened = useAtom(isNodeDetailsOpenAtom);
417
+ const currentNodeId = useAtom(currentNodeIdAtom);
418
+ const node = currentNodeId ? props.output.nodes.find((candidate) => candidate.path === currentNodeId) : null;
419
+ return /* @__PURE__ */ jsx(Dialog.Root, {
420
+ open: Boolean(isOpened && node),
421
+ onOpenChange: (details) => {
422
+ if (!details.open) isNodeDetailsOpenAtom.set(false);
423
+ },
424
+ lazyMount: true,
425
+ children: /* @__PURE__ */ jsxs(Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: "data-[state=open]:animate-backdrop-in data-[state=closed]:animate-backdrop-out fixed inset-0 z-50 bg-black/50 backdrop-blur-xs" }), /* @__PURE__ */ jsx(Dialog.Positioner, {
426
+ className: "fixed inset-0 z-50 flex items-center justify-center p-4",
427
+ children: node && /* @__PURE__ */ jsx(NodeDetailsModalContent, {
428
+ node,
429
+ output: props.output,
430
+ entryNodeId: props.output.metadata.entrypoints.at(0)
431
+ })
432
+ })] })
433
+ });
434
+ }
435
+ var NodeDetailsModalContent = (props) => {
436
+ const [isMaximized, setIsMaximized] = useState(false);
437
+ const [viewMode, setViewMode] = useState("tree-search");
438
+ return /* @__PURE__ */ jsxs(Dialog.Content, {
439
+ className: "data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out relative w-full max-w-4xl h-[80vh] rounded-lg bg-white dark:bg-gray-900 shadow-lg flex flex-col",
440
+ style: {
441
+ width: isMaximized ? "95vw" : void 0,
442
+ maxWidth: isMaximized ? "95vw" : void 0,
443
+ height: isMaximized ? "95vh" : void 0,
444
+ maxHeight: isMaximized ? "95vh" : void 0
445
+ },
446
+ children: [
447
+ /* @__PURE__ */ jsxs(Dialog.Title, {
448
+ className: "relative flex gap-2 items-center text-lg font-semibold text-gray-900 dark:text-white p-4 border-b border-gray-200 dark:border-gray-700",
449
+ children: [
450
+ /* @__PURE__ */ jsx("span", { children: "Current node:" }),
451
+ /* @__PURE__ */ jsx("span", { children: props.node.path }),
452
+ /* @__PURE__ */ jsx("div", {
453
+ className: "ml-auto",
454
+ children: /* @__PURE__ */ jsx(Button, {
455
+ variant: "outline",
456
+ className: "px-2 py-1",
457
+ onClick: () => setIsMaximized((isMaximized) => !isMaximized),
458
+ children: isMaximized ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LuMinimize, { className: "h-4 w-4 text-slate-400" }), /* @__PURE__ */ jsx("span", {
459
+ className: "sr-only",
460
+ children: "Minimize"
461
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LuMaximize, { className: "h-4 w-4 text-slate-400" }), /* @__PURE__ */ jsx("span", {
462
+ className: "sr-only",
463
+ children: "Maximize"
464
+ })] })
465
+ })
466
+ })
467
+ ]
468
+ }),
469
+ /* @__PURE__ */ jsx("div", {
470
+ className: "bg-white dark:bg-gray-800 w-full h-full min-h-0 px-4 py-6 rounded-xl flex flex-col",
471
+ children: /* @__PURE__ */ jsxs(Tabs.Root, {
472
+ className: "w-full h-full min-h-0 overflow-hidden flex flex-col",
473
+ defaultValue: "transitive-imports",
474
+ children: [
475
+ /* @__PURE__ */ jsxs("div", {
476
+ className: "flex w-full",
477
+ children: [/* @__PURE__ */ jsxs(Tabs.List, {
478
+ className: "flex gap-1 p-1 bg-gray-100 rounded-lg dark:bg-gray-700 mb-2",
479
+ children: [/* @__PURE__ */ jsx(Tabs.Trigger, {
480
+ value: "transitive-imports",
481
+ className: "w-fit px-4 py-2 text-sm font-medium text-gray-600 rounded-md transition-all data-selected:bg-white data-selected:text-gray-900 data-selected:shadow-sm dark:text-gray-300 dark:data-selected:bg-gray-800 dark:data-selected:text-gray-100",
482
+ children: "Transitive imports"
483
+ }), /* @__PURE__ */ jsx(Tabs.Trigger, {
484
+ value: "imports-chain",
485
+ className: "w-fit px-4 py-2 text-sm font-medium text-gray-600 rounded-md transition-all data-selected:bg-white data-selected:text-gray-900 data-selected:shadow-sm dark:text-gray-300 dark:data-selected:bg-gray-800 dark:data-selected:text-gray-100",
486
+ children: "Imports chains"
487
+ })]
488
+ }), /* @__PURE__ */ jsx("div", {
489
+ className: "relative z-50 ml-auto px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-300",
490
+ children: /* @__PURE__ */ jsxs(Select$1, {
491
+ className: "min-w-64 *:not-first:mt-2",
492
+ value: [viewMode],
493
+ onValueChange: (details) => setViewMode(details.value.at(0)),
494
+ collection: viewModeCollection,
495
+ positioning: { sameWidth: true },
496
+ children: [/* @__PURE__ */ jsx(SelectControl, { children: /* @__PURE__ */ jsxs(SelectTrigger, { children: [/* @__PURE__ */ jsx(SelectValueText, { placeholder: "View mode" }), /* @__PURE__ */ jsx(SelectIndicator, {})] }) }), /* @__PURE__ */ jsx(SelectContent, { children: viewModeCollection.items.map((item) => /* @__PURE__ */ jsx(SelectItem, {
497
+ item,
498
+ children: item.label
499
+ }, item.value)) })]
500
+ })
501
+ })]
502
+ }),
503
+ /* @__PURE__ */ jsxs(Tabs.Content, {
504
+ value: "transitive-imports",
505
+ className: "w-full h-full min-h-0 text-gray-600 dark:text-gray-300",
506
+ children: [viewMode === "tree-search" && /* @__PURE__ */ jsx(TransitiveImportsTab, {
507
+ node: props.node,
508
+ output: props.output
509
+ }), viewMode === "flamegraph" && /* @__PURE__ */ jsx(Flamegraph, {
510
+ output: props.output,
511
+ entryNodeId: props.node.path
512
+ })]
513
+ }),
514
+ /* @__PURE__ */ jsx(Tabs.Content, {
515
+ value: "imports-chain",
516
+ className: "w-full h-full min-h-0 text-gray-600 dark:text-gray-300",
517
+ children: /* @__PURE__ */ jsx(ImportsChainTab, {
518
+ node: props.node,
519
+ output: props.output
520
+ })
521
+ })
522
+ ]
523
+ })
524
+ }),
525
+ /* @__PURE__ */ jsxs("div", {
526
+ className: "flex items-center justify-between text-xs p-2 border-t border-gray-200 dark:border-gray-700",
527
+ children: [/* @__PURE__ */ jsxs("div", {
528
+ className: "flex flex-col gap-1",
529
+ children: [/* @__PURE__ */ jsxs("div", {
530
+ className: "flex gap-3",
531
+ children: ["Current node: ", /* @__PURE__ */ jsx("span", { children: props.node.path })]
532
+ }), /* @__PURE__ */ jsxs("div", {
533
+ className: "flex gap-3",
534
+ children: ["Entrypoint: ", /* @__PURE__ */ jsx("span", { children: props.entryNodeId })]
535
+ })]
536
+ }), /* @__PURE__ */ jsx(Link, {
537
+ to: "/trace",
538
+ search: getNodeTraceSearch(props.node),
539
+ onClick: () => isNodeDetailsOpenAtom.set(false),
540
+ className: "rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-sky-50 hover:border-sky-300 hover:text-sky-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-sky-500/60 dark:hover:bg-sky-500/10 dark:hover:text-sky-200",
541
+ children: "Trace this node →"
542
+ })]
543
+ })
544
+ ]
545
+ });
546
+ };
547
+ var getNodeTraceSearch = (node) => {
548
+ if (node.path.includes("node_modules")) return {
549
+ package: getExternalPackageName(node) ?? node.path,
550
+ node: "",
551
+ limit: 25
552
+ };
553
+ return {
554
+ node: node.path,
555
+ package: "",
556
+ limit: 25
557
+ };
558
+ };
559
+ var TransitiveImportsTab = (props) => {
560
+ const initialCollection = useMemo(() => mapModvizOutputToImporteesTreeCollection(props.output, props.node.path), [props.output, props.node.path]);
561
+ if (!initialCollection) return;
562
+ const transitiveImports = initialCollection.visited;
563
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
564
+ className: "h-full min-h-0 flex flex-col p-2 gap-2",
565
+ children: [/* @__PURE__ */ jsxs("span", {
566
+ onClick: () => console.log(props.node, initialCollection.collection.rootNode, initialCollection.visited),
567
+ children: [transitiveImports.size, " file imported (transitively) from current node"]
568
+ }), /* @__PURE__ */ jsx(TreeViewBasic, {
569
+ entryNodeId: props.node.path,
570
+ initialCollection: initialCollection.collection
571
+ }, props.node.path)]
572
+ }) });
573
+ };
574
+ var ImportsChainTab = (props) => {
575
+ const [direction, setDirection] = useState("from-entrypoint-to-current-node");
576
+ const initialCollection = useMemo(() => mapModvizOutputToImportsChainTreeCollection(props.output, props.node.path, direction), [
577
+ props.output,
578
+ props.node.path,
579
+ direction
580
+ ]);
581
+ if (!initialCollection) return;
582
+ const chain = props.output.nodes.find((node) => node.path === props.node.path).chain.at(0) ?? [];
583
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
584
+ className: "h-full min-h-0 flex flex-col p-2 gap-2",
585
+ children: [/* @__PURE__ */ jsxs("div", {
586
+ className: "flex gap-2 items-center",
587
+ children: [/* @__PURE__ */ jsxs("span", {
588
+ onClick: () => console.log(props.node, initialCollection.rootNode, chain),
589
+ children: [/* @__PURE__ */ jsxs("span", { children: [
590
+ "Reached",
591
+ " ",
592
+ direction === "from-entrypoint-to-current-node" ? "current node" : "entrypoint",
593
+ " ",
594
+ "after"
595
+ ] }), /* @__PURE__ */ jsxs("span", { children: [
596
+ " ",
597
+ chain.length,
598
+ " imports"
599
+ ] })]
600
+ }), /* @__PURE__ */ jsx("div", {
601
+ className: "ml-auto",
602
+ children: /* @__PURE__ */ jsx(ImportsChainDirection, {
603
+ value: direction,
604
+ onValueChange: setDirection
605
+ })
606
+ })]
607
+ }), /* @__PURE__ */ jsx(TreeViewBasic, {
608
+ entryNodeId: props.node.path,
609
+ initialCollection,
610
+ defaultExpandedValue: chain
611
+ }, props.node.path + direction)]
612
+ }) });
613
+ };
614
+ var viewModeCollection = createListCollection({ items: [{
615
+ value: "tree-search",
616
+ label: "Tree search"
617
+ }, {
618
+ value: "flamegraph",
619
+ label: "Flamegraph"
620
+ }] });
621
+ var directionCollection = createListCollection({ items: [{
622
+ value: "from-entrypoint-to-current-node",
623
+ label: "From entrypoint to current node"
624
+ }, {
625
+ value: "from-current-node-to-entrypoint",
626
+ label: "From current node to entrypoint"
627
+ }] });
628
+ function ImportsChainDirection(props) {
629
+ return /* @__PURE__ */ jsxs(Select$1, {
630
+ className: "min-w-64 *:not-first:mt-2",
631
+ value: [props.value],
632
+ onValueChange: (details) => props.onValueChange(details.value.at(0)),
633
+ collection: directionCollection,
634
+ positioning: { sameWidth: true },
635
+ children: [/* @__PURE__ */ jsx(SelectControl, { children: /* @__PURE__ */ jsxs(SelectTrigger, { children: [/* @__PURE__ */ jsx(SelectValueText, { placeholder: "Direction" }), /* @__PURE__ */ jsx(SelectIndicator, {})] }) }), /* @__PURE__ */ jsx(SelectContent, { children: directionCollection.items.map((item) => /* @__PURE__ */ jsx(SelectItem, {
636
+ item,
637
+ children: item.label
638
+ }, item.value)) })]
639
+ });
640
+ }
641
+ //#endregion
642
+ //#region src/components/graph/common/clamp.ts
643
+ var clamp = (min, max, value) => {
644
+ return Math.min(Math.max(min, value), max);
645
+ };
646
+ //#endregion
647
+ //#region src/components/graph/common/use-create-graph.ts
648
+ var useCreateGraph = (props) => {
649
+ const workspacePackageNames = useMemo(() => new Set(props.packages.map((pkg) => pkg.name)), [props.packages]);
650
+ const clusterAnchors = useMemo(() => {
651
+ const spreadBase = Math.max(26, Math.sqrt(props.nodes.length) * .9);
652
+ const groups = Array.from(new Set(props.nodes.map((node) => getNodeGroupingLabel(node, workspacePackageNames, props.externalGrouping ?? "combined")))).sort((left, right) => left.localeCompare(right));
653
+ return new Map(groups.map((groupLabel, index) => {
654
+ const angle = index * 2.399963229728653;
655
+ const radius = spreadBase * Math.sqrt(index + 1);
656
+ return [groupLabel, {
657
+ x: Math.cos(angle) * radius,
658
+ y: Math.sin(angle) * radius
659
+ }];
660
+ }));
661
+ }, [
662
+ props.externalGrouping,
663
+ props.nodes,
664
+ workspacePackageNames
665
+ ]);
666
+ const clusterColors = useMemo(() => {
667
+ const colorMap = /* @__PURE__ */ new Map();
668
+ const clusters = /* @__PURE__ */ new Set();
669
+ props.nodes.forEach((node) => {
670
+ clusters.add(getNodeGroupingLabel(node, workspacePackageNames, props.externalGrouping ?? "combined"));
671
+ });
672
+ let index = 0;
673
+ clusters.forEach((cluster) => {
674
+ colorMap.set(cluster, colors.list[++index] ?? colors.deterministic(cluster));
675
+ });
676
+ return colorMap;
677
+ }, [
678
+ props.externalGrouping,
679
+ props.nodes,
680
+ workspacePackageNames
681
+ ]);
682
+ const edges = useMemo(() => {
683
+ const ids = /* @__PURE__ */ new Set();
684
+ props.nodes.forEach((node) => {
685
+ node.importedBy.forEach((importee) => {
686
+ ids.add(`${importee}->${node.path}`);
687
+ });
688
+ });
689
+ return {
690
+ ids,
691
+ list: Array.from(ids).map((edge) => {
692
+ const [source, target] = edge.split("->");
693
+ return {
694
+ source,
695
+ target
696
+ };
697
+ })
698
+ };
699
+ }, [props.nodes]);
700
+ return useCallback(() => {
701
+ const graph = new DirectedGraph();
702
+ const nodesMap = /* @__PURE__ */ new Map();
703
+ const nodeSizeScale = props.layoutSettings?.nodeSizeScale ?? 1;
704
+ props.nodes.forEach((node) => {
705
+ nodesMap.set(node.path, node);
706
+ const groupLabel = getNodeGroupingLabel(node, workspacePackageNames, props.externalGrouping ?? "combined");
707
+ const anchor = clusterAnchors.get(groupLabel) ?? {
708
+ x: 0,
709
+ y: 0
710
+ };
711
+ const clusterRadius = Math.max(12, Math.min(36, Math.sqrt(node.importedBy.length + 1) * 6));
712
+ const jitterX = (getRandom() - .5) * clusterRadius;
713
+ const jitterY = (getRandom() - .5) * clusterRadius;
714
+ const isEntry = props.entryNode === node.path;
715
+ const x = isEntry ? 0 : anchor.x + jitterX;
716
+ const y = isEntry ? 0 : anchor.y + jitterY;
717
+ const label = node.package && node.isBarrelFile ? `${node.package?.name}/${node.name}` : node.name;
718
+ graph.addNode(node.path, {
719
+ x,
720
+ y,
721
+ label,
722
+ originalLabel: label,
723
+ modType: node.type,
724
+ cluster: groupLabel,
725
+ clusterPath: node.package?.path ?? groupLabel,
726
+ color: clusterColors.get(groupLabel) ?? colors.default,
727
+ size: clamp(4, 18 * nodeSizeScale, Math.max(4, node.importedBy.length * nodeSizeScale)),
728
+ highlighted: false
729
+ });
730
+ });
731
+ edges.list.forEach((edge) => {
732
+ if (nodesMap.has(edge.source) && nodesMap.has(edge.target)) {
733
+ const sourceGroup = getNodeGroupingLabel(nodesMap.get(edge.source), workspacePackageNames, props.externalGrouping ?? "combined");
734
+ graph.addEdge(edge.source, edge.target, {
735
+ label: edge.source,
736
+ color: clusterColors.get(sourceGroup) ?? colors.default
737
+ });
738
+ }
739
+ });
740
+ return graph;
741
+ }, [
742
+ clusterAnchors,
743
+ clusterColors,
744
+ edges.list,
745
+ props.entryNode,
746
+ props.externalGrouping,
747
+ props.layoutSettings?.nodeSizeScale,
748
+ props.nodes,
749
+ workspacePackageNames
750
+ ]);
751
+ };
752
+ //#endregion
753
+ //#region src/components/graph/common/use-graph-settings.ts
754
+ var useGraphSettings = () => {
755
+ const sigma = useSigma();
756
+ const setSettings = useSetSettings();
757
+ const registerEvents = useRegisterEvents();
758
+ const hoveredClusterName = useAtom(hoveredClusterNameAtom);
759
+ const selectedNodeIds = useAtom(selectedNodeIdsAtom);
760
+ const selectedNodeSet = useMemo(() => new Set(selectedNodeIds), [selectedNodeIds]);
761
+ const selectedVisibleNodeSet = useMemo(() => {
762
+ if (selectedNodeSet.size === 0) return /* @__PURE__ */ new Set();
763
+ const graph = sigma.getGraph();
764
+ const visible = new Set(selectedNodeIds);
765
+ for (const selectedNodeId of selectedNodeIds) {
766
+ if (!graph.hasNode(selectedNodeId)) continue;
767
+ for (const neighborId of graph.neighbors(selectedNodeId)) visible.add(neighborId);
768
+ }
769
+ return visible;
770
+ }, [
771
+ selectedNodeIds,
772
+ selectedNodeSet.size,
773
+ sigma
774
+ ]);
775
+ const [hoveredNodeId, setHoveredNodeId] = useState(null);
776
+ useEffect(() => {
777
+ registerEvents({
778
+ enterNode: (event) => setHoveredNodeId(event.node),
779
+ leaveNode: () => setHoveredNodeId(null),
780
+ clickNode: (event) => {
781
+ currentNodeIdAtom.set(event.node);
782
+ highlightedNodeIdAtom.set(null);
783
+ selectedNodeIdsAtom.set((previous) => previous.includes(event.node) ? previous.filter((nodeId) => nodeId !== event.node) : [...previous, event.node]);
784
+ },
785
+ downStage: () => {
786
+ highlightedNodeIdAtom.set(null);
787
+ },
788
+ mousedown: () => {
789
+ if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox());
790
+ }
791
+ });
792
+ }, [registerEvents, sigma]);
793
+ useEffect(() => {
794
+ setSettings({
795
+ autoCenter: true,
796
+ autoRescale: true,
797
+ zoomDuration: 150,
798
+ renderLabels: Boolean(hoveredNodeId || hoveredClusterName || selectedNodeSet.size > 0),
799
+ hideLabelsOnMove: true,
800
+ labelRenderedSizeThreshold: 8,
801
+ nodeReducer: (nodeId, node) => {
802
+ const graph = sigma.getGraph();
803
+ const isHoveredNode = nodeId === hoveredNodeId;
804
+ const updated = {
805
+ ...node,
806
+ highlighted: node.highlighted || false
807
+ };
808
+ if (selectedNodeSet.size > 0) {
809
+ if (selectedNodeSet.has(nodeId)) {
810
+ updated.label = node.label;
811
+ updated.highlighted = true;
812
+ updated.size = node.size + clamp(2, 8, node.size * .15);
813
+ } else if (selectedVisibleNodeSet.has(nodeId)) {
814
+ updated.label = node.label;
815
+ updated.highlighted = true;
816
+ } else {
817
+ updated.color = colors.default;
818
+ updated.label = isHoveredNode ? node.label : "";
819
+ updated.highlighted = isHoveredNode;
820
+ }
821
+ if (isHoveredNode) {
822
+ updated.label = node.label;
823
+ updated.highlighted = true;
824
+ updated.size = node.size + clamp(4, 10, node.size * .25);
825
+ }
826
+ return updated;
827
+ }
828
+ if (hoveredNodeId && graph.hasNode(hoveredNodeId)) {
829
+ if (nodeId === hoveredNodeId) {
830
+ updated.label = node.label;
831
+ updated.size = node.size + clamp(4, 10, node.size * .25);
832
+ }
833
+ if (nodeId === hoveredNodeId || graph.neighbors(hoveredNodeId).includes(nodeId)) {
834
+ updated.label = node.label;
835
+ updated.highlighted = true;
836
+ } else {
837
+ updated.color = colors.default;
838
+ updated.highlighted = false;
839
+ updated.label = "";
840
+ }
841
+ } else if (hoveredClusterName) if (node.cluster === hoveredClusterName) {
842
+ updated.label = node.label;
843
+ updated.highlighted = true;
844
+ } else {
845
+ updated.color = colors.default;
846
+ updated.highlighted = false;
847
+ updated.label = "";
848
+ }
849
+ return updated;
850
+ },
851
+ edgeReducer: (edgeId, edge) => {
852
+ const graph = sigma.getGraph();
853
+ const updated = {
854
+ ...edge,
855
+ hidden: true
856
+ };
857
+ if (selectedNodeSet.size > 0) {
858
+ const [source, target] = graph.extremities(edgeId);
859
+ if (selectedNodeSet.has(source) || selectedNodeSet.has(target)) {
860
+ updated.hidden = false;
861
+ updated.color = graph.getNodeAttribute(source, "color");
862
+ }
863
+ return updated;
864
+ }
865
+ if (hoveredNodeId && graph.extremities(edgeId).includes(hoveredNodeId)) {
866
+ const activeNode = graph.getNodeAttributes(hoveredNodeId);
867
+ updated.hidden = false;
868
+ updated.color = activeNode.color;
869
+ } else if (hoveredClusterName) {
870
+ const [source, target] = graph.extremities(edgeId);
871
+ const sourceNode = graph.getNodeAttributes(source);
872
+ const targetNode = graph.getNodeAttributes(target);
873
+ if (sourceNode.cluster === hoveredClusterName && targetNode.cluster === hoveredClusterName) {
874
+ updated.hidden = false;
875
+ updated.color = sourceNode.color;
876
+ }
877
+ }
878
+ return updated;
879
+ }
880
+ });
881
+ }, [
882
+ hoveredClusterName,
883
+ hoveredNodeId,
884
+ selectedNodeSet,
885
+ selectedVisibleNodeSet,
886
+ setSettings,
887
+ sigma
888
+ ]);
889
+ };
890
+ //#endregion
891
+ //#region src/components/graph/common/render-sigma-graph.tsx
892
+ var SigmaGraph = (props) => {
893
+ const layout = useWorkerLayoutForceAtlas2(useMemo(() => ({
894
+ ...props.layout,
895
+ settings: {
896
+ barnesHutOptimize: true,
897
+ slowDown: 8,
898
+ ...props.layout?.settings,
899
+ gravity: props.layoutSettings.gravity,
900
+ scalingRatio: props.layoutSettings.scalingRatio,
901
+ strongGravityMode: props.layoutSettings.strongGravityMode,
902
+ linLogMode: props.layoutSettings.linLogMode,
903
+ adjustSizes: props.layoutSettings.adjustSizes,
904
+ outboundAttractionDistribution: props.layoutSettings.outboundAttractionDistribution
905
+ }
906
+ }), [
907
+ props.layout,
908
+ props.layoutSettings.adjustSizes,
909
+ props.layoutSettings.gravity,
910
+ props.layoutSettings.linLogMode,
911
+ props.layoutSettings.outboundAttractionDistribution,
912
+ props.layoutSettings.scalingRatio,
913
+ props.layoutSettings.strongGravityMode
914
+ ]));
915
+ const noverlap = useLayoutNoverlap({
916
+ maxIterations: Math.min(240, Math.max(80, Math.round(props.layoutSettings.iterations / 2))),
917
+ settings: {
918
+ expansion: 1.2,
919
+ gridSize: 40,
920
+ margin: 6,
921
+ ratio: 1.4,
922
+ speed: 2
923
+ }
924
+ });
925
+ const createGraph = useCreateGraph(props);
926
+ const loadGraph = useLoadGraph();
927
+ const stopTimerRef = useRef(null);
928
+ const hasLoadedGraphRef = useRef(false);
929
+ const animationFrameRef = useRef(null);
930
+ const activeRunIdRef = useRef(0);
931
+ useGraphSettings();
932
+ const applyNoverlapSpacing = (runId) => {
933
+ if (activeRunIdRef.current !== runId) return;
934
+ noverlap.assign();
935
+ };
936
+ const stopLayoutAfterDelay = (delayMs, runId) => {
937
+ if (stopTimerRef.current != null) window.clearTimeout(stopTimerRef.current);
938
+ stopTimerRef.current = window.setTimeout(() => {
939
+ if (activeRunIdRef.current !== runId) return;
940
+ layout.stop();
941
+ applyNoverlapSpacing(runId);
942
+ stopTimerRef.current = null;
943
+ props.onBusyChange?.(false);
944
+ }, delayMs);
945
+ };
946
+ const cancelActiveUpdate = () => {
947
+ activeRunIdRef.current += 1;
948
+ if (animationFrameRef.current != null) {
949
+ window.cancelAnimationFrame(animationFrameRef.current);
950
+ animationFrameRef.current = null;
951
+ }
952
+ if (stopTimerRef.current != null) {
953
+ window.clearTimeout(stopTimerRef.current);
954
+ stopTimerRef.current = null;
955
+ }
956
+ layout.stop();
957
+ props.onBusyChange?.(false);
958
+ };
959
+ useEffect(() => {
960
+ const runId = activeRunIdRef.current + 1;
961
+ activeRunIdRef.current = runId;
962
+ animationFrameRef.current = window.requestAnimationFrame(() => {
963
+ animationFrameRef.current = null;
964
+ if (activeRunIdRef.current !== runId) return;
965
+ props.onBusyChange?.(true);
966
+ const graph = createGraph();
967
+ if (activeRunIdRef.current !== runId) return;
968
+ loadGraph(graph);
969
+ hasLoadedGraphRef.current = true;
970
+ layout.stop();
971
+ layout.start();
972
+ stopLayoutAfterDelay(Math.max(900, props.layoutSettings.iterations * 12), runId);
973
+ });
974
+ return () => {
975
+ if (activeRunIdRef.current === runId) cancelActiveUpdate();
976
+ };
977
+ }, [
978
+ createGraph,
979
+ layout.start,
980
+ layout.stop,
981
+ loadGraph,
982
+ props.layoutSettings.iterations,
983
+ props.onBusyChange
984
+ ]);
985
+ useEffect(() => {
986
+ if (!hasLoadedGraphRef.current) return;
987
+ props.onBusyChange?.(true);
988
+ layout.stop();
989
+ layout.start();
990
+ stopLayoutAfterDelay(Math.max(900, props.layoutSettings.iterations * 12), activeRunIdRef.current);
991
+ return () => {
992
+ if (stopTimerRef.current != null) {
993
+ window.clearTimeout(stopTimerRef.current);
994
+ stopTimerRef.current = null;
995
+ }
996
+ layout.stop();
997
+ props.onBusyChange?.(false);
998
+ };
999
+ }, [
1000
+ layout.start,
1001
+ layout.stop,
1002
+ props.layoutSettings.adjustSizes,
1003
+ props.layoutSettings.gravity,
1004
+ props.layoutSettings.iterations,
1005
+ props.layoutSettings.linLogMode,
1006
+ props.layoutSettings.outboundAttractionDistribution,
1007
+ props.layoutSettings.scalingRatio,
1008
+ props.layoutSettings.strongGravityMode,
1009
+ props.onBusyChange
1010
+ ]);
1011
+ useEffect(() => {
1012
+ if (!props.cancelNonce) return;
1013
+ cancelActiveUpdate();
1014
+ }, [
1015
+ props.cancelNonce,
1016
+ layout.stop,
1017
+ props.onBusyChange
1018
+ ]);
1019
+ useEffect(() => {
1020
+ return () => {
1021
+ cancelActiveUpdate();
1022
+ layout.stop();
1023
+ layout.kill();
1024
+ };
1025
+ }, [layout.kill]);
1026
+ return null;
1027
+ };
1028
+ //#endregion
1029
+ //#region src/utils/infer-paths-label.ts
1030
+ /**
1031
+ * Infers a label name for a group of files based on common directory patterns.
1032
+ * Analyzes file paths to find the most significant common directory segment.
1033
+ *
1034
+ * @param filePaths - Array of file paths to analyze
1035
+ * @returns The inferred group label or undefined if no clear pattern is found
1036
+ *
1037
+ * @example
1038
+ * inferGroupLabel([
1039
+ * "apps/backend/src/commitments/use-cases/compute-timeline.use-case.ts",
1040
+ * "apps/backend/src/commitments/use-cases/export-commitments.use-case.ts",
1041
+ * "apps/backend/src/commitments/mappers/commitment.mapper.ts"
1042
+ * ]); // Returns "commitments"
1043
+ */
1044
+ function inferPathsLabel(filePaths) {
1045
+ if (!filePaths.length) return;
1046
+ const allSegments = filePaths.map((path) => {
1047
+ return path.split("/").filter((segment) => segment && segment !== "src" && segment !== "lib" && segment !== "dist" && segment !== "build" && !segment.includes("."));
1048
+ });
1049
+ const segmentFrequency = /* @__PURE__ */ new Map();
1050
+ allSegments.forEach((segments) => {
1051
+ segments.forEach((segment, index) => {
1052
+ const current = segmentFrequency.get(segment) || {
1053
+ count: 0,
1054
+ positions: []
1055
+ };
1056
+ current.count++;
1057
+ current.positions.push(index);
1058
+ segmentFrequency.set(segment, current);
1059
+ });
1060
+ });
1061
+ const genericNames = new Set([
1062
+ "apps",
1063
+ "packages",
1064
+ "node_modules",
1065
+ ".pnpm",
1066
+ "src",
1067
+ "modules",
1068
+ "components",
1069
+ "utils",
1070
+ "helpers",
1071
+ "shared",
1072
+ "common",
1073
+ "core",
1074
+ "base",
1075
+ "types",
1076
+ "interfaces",
1077
+ "backend",
1078
+ "frontend",
1079
+ "client",
1080
+ "server",
1081
+ "api",
1082
+ "web"
1083
+ ]);
1084
+ let bestCandidate = {
1085
+ segment: "",
1086
+ score: 0
1087
+ };
1088
+ for (const [segment, data] of segmentFrequency) {
1089
+ if (genericNames.has(segment)) continue;
1090
+ const frequencyRatio = data.count / filePaths.length;
1091
+ const avgPosition = data.positions.reduce((sum, pos) => sum + pos, 0) / data.positions.length;
1092
+ const positionConsistency = 1 - (Math.max(...data.positions) - Math.min(...data.positions)) / 10;
1093
+ const score = frequencyRatio * .6 + positionConsistency * .4 - avgPosition * .1;
1094
+ if (score > bestCandidate.score && frequencyRatio >= .5) bestCandidate = {
1095
+ segment,
1096
+ score
1097
+ };
1098
+ }
1099
+ if (!bestCandidate.segment) {
1100
+ const pathSegments = findLongestCommonPath(filePaths).split("/").filter((s) => s && !s.includes("."));
1101
+ for (let i = pathSegments.length - 1; i >= 0; i--) if (!genericNames.has(pathSegments[i])) {
1102
+ bestCandidate.segment = pathSegments[i];
1103
+ break;
1104
+ }
1105
+ }
1106
+ return bestCandidate.segment || void 0;
1107
+ }
1108
+ /**
1109
+ * Finds the longest common path prefix among multiple file paths
1110
+ */
1111
+ function findLongestCommonPath(paths) {
1112
+ if (paths.length === 0) return "";
1113
+ if (paths.length === 1) return paths[0];
1114
+ const segments = paths.map((path) => path.split("/"));
1115
+ const minLength = Math.min(...segments.map((s) => s.length));
1116
+ let commonLength = 0;
1117
+ for (let i = 0; i < minLength; i++) {
1118
+ const segment = segments[0][i];
1119
+ if (segments.every((s) => s[i] === segment)) commonLength = i + 1;
1120
+ else break;
1121
+ }
1122
+ return segments[0].slice(0, commonLength).join("/");
1123
+ }
1124
+ //#endregion
1125
+ //#region src/components/graph/modviz-sigma.tsx
1126
+ var SIGMA_CONTAINER_SETTINGS = { allowInvalidContainer: true };
1127
+ var ModvizSigma = (props) => {
1128
+ const [sigma, setSigma] = useState(null);
1129
+ return /* @__PURE__ */ jsxs(SigmaContainer, {
1130
+ ref: setSigma,
1131
+ className: "relative h-full w-full",
1132
+ settings: SIGMA_CONTAINER_SETTINGS,
1133
+ children: [
1134
+ /* @__PURE__ */ jsx(SigmaGraph, {
1135
+ entryNode: props.entryNode,
1136
+ externalGrouping: props.externalGrouping,
1137
+ layoutSettings: props.layoutSettings,
1138
+ packages: props.packages,
1139
+ nodes: props.nodes,
1140
+ cancelNonce: props.cancelNonce,
1141
+ onBusyChange: props.onBusyChange
1142
+ }),
1143
+ props.isBusy ? /* @__PURE__ */ jsx("div", {
1144
+ className: "absolute inset-0 z-20 flex items-center justify-center bg-white/72 backdrop-blur-[1.5px] dark:bg-slate-950/72",
1145
+ children: /* @__PURE__ */ jsxs("div", {
1146
+ className: "pointer-events-auto flex min-w-[18rem] flex-col items-center gap-4 rounded-3xl border border-slate-200/80 bg-white/90 px-6 py-5 shadow-[0_18px_48px_-32px_rgba(15,23,42,0.65)] dark:border-slate-800 dark:bg-slate-950/90",
1147
+ children: [/* @__PURE__ */ jsx(LoadingState, {
1148
+ label: "Updating graph…",
1149
+ description: "Rebuilding the dependency map and running the layout in the background.",
1150
+ className: "min-h-0"
1151
+ }), props.onCancelUpdate ? /* @__PURE__ */ jsx(Button, {
1152
+ variant: "outline",
1153
+ size: "sm",
1154
+ onClick: props.onCancelUpdate,
1155
+ children: "Cancel update"
1156
+ }) : null]
1157
+ })
1158
+ }) : null,
1159
+ sigma && /* @__PURE__ */ jsx(WithGraph, {
1160
+ hideClusterLabels: props.layoutSettings.hideClusterLabels,
1161
+ output: props.output,
1162
+ sigma,
1163
+ nodes: props.nodes,
1164
+ externalGrouping: props.externalGrouping,
1165
+ entryNode: props.entryNode
1166
+ })
1167
+ ]
1168
+ });
1169
+ };
1170
+ var WithGraph = (props) => {
1171
+ const sigma = props.sigma;
1172
+ const clusterMap = useClusterMap(sigma);
1173
+ const clusterList = useClusterList(clusterMap);
1174
+ const externalNodeIds = useMemo(() => props.nodes.filter((node) => node.path.includes("node_modules")).map((node) => node.path), [props.nodes]);
1175
+ const visibleClusters = useMemo(() => clusterList.filter((cluster) => cluster.nodes.length > 5), [clusterList]);
1176
+ useClusterLabelLayer(sigma, clusterMap, props.hideClusterLabels);
1177
+ const graph = sigma.getGraph();
1178
+ const nodes = graph.nodes();
1179
+ const focusNodes = (nodeIds) => {
1180
+ if (nodeIds.length === 0) return;
1181
+ fitViewportToNodes(sigma, nodeIds, { animate: true });
1182
+ };
1183
+ const focusCluster = (clusterName) => {
1184
+ focusNodes(graph.filterNodes((_, attrs) => attrs.cluster === clusterName));
1185
+ };
1186
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1187
+ /* @__PURE__ */ jsxs(ControlsContainer, {
1188
+ position: "bottom-right",
1189
+ className: "mb-6",
1190
+ children: [/* @__PURE__ */ jsx(ZoomControl, {}), /* @__PURE__ */ jsx(FullScreenControl, {})]
1191
+ }),
1192
+ /* @__PURE__ */ jsx(ControlsContainer, {
1193
+ position: "top-left",
1194
+ className: "z-10!",
1195
+ children: /* @__PURE__ */ jsxs("div", {
1196
+ className: "flex w-[18rem] flex-col gap-2 p-2 text-xs",
1197
+ children: [
1198
+ /* @__PURE__ */ jsxs("div", {
1199
+ className: "flex items-center gap-2",
1200
+ children: [
1201
+ /* @__PURE__ */ jsx(SidebarIconButton, {
1202
+ label: `Reset view (${nodes.length} nodes)`,
1203
+ onClick: () => focusNodes(nodes),
1204
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4" })
1205
+ }),
1206
+ props.entryNode ? /* @__PURE__ */ jsx(SidebarIconButton, {
1207
+ label: "Focus entrypoint",
1208
+ onClick: () => focusNodes(props.entryNode ? [props.entryNode] : []),
1209
+ children: /* @__PURE__ */ jsx(LocateFixed, { className: "size-4" })
1210
+ }) : null,
1211
+ /* @__PURE__ */ jsx("div", {
1212
+ className: "min-w-0 flex-1",
1213
+ children: /* @__PURE__ */ jsx(ClusterCombobox, {
1214
+ clusters: visibleClusters,
1215
+ onFocusCluster: focusCluster
1216
+ })
1217
+ })
1218
+ ]
1219
+ }),
1220
+ props.externalGrouping === "package" && externalNodeIds.length ? /* @__PURE__ */ jsxs("button", {
1221
+ className: "flex items-center gap-2 rounded-md bg-white px-2 py-1 hover:bg-gray-100",
1222
+ onClick: () => focusNodes(externalNodeIds),
1223
+ children: [
1224
+ "node_modules (",
1225
+ externalNodeIds.length,
1226
+ ")"
1227
+ ]
1228
+ }) : null,
1229
+ /* @__PURE__ */ jsx("div", {
1230
+ className: "max-h-[300px] overflow-auto",
1231
+ children: /* @__PURE__ */ jsx("div", {
1232
+ className: "flex flex-col gap-2 pr-1",
1233
+ children: visibleClusters.map((cluster) => {
1234
+ return /* @__PURE__ */ jsxs("button", {
1235
+ title: cluster.path,
1236
+ className: "flex items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-gray-100",
1237
+ onClick: () => focusCluster(cluster.name),
1238
+ onMouseEnter: () => {
1239
+ hoveredClusterNameAtom.set(cluster.name);
1240
+ },
1241
+ onMouseLeave: () => {
1242
+ hoveredClusterNameAtom.set(null);
1243
+ },
1244
+ children: [
1245
+ /* @__PURE__ */ jsx("div", {
1246
+ className: "w-2 h-2",
1247
+ style: { backgroundColor: cluster.color }
1248
+ }),
1249
+ cluster.inferredName || cluster.name,
1250
+ "(",
1251
+ cluster.nodes.length,
1252
+ ")"
1253
+ ]
1254
+ }, cluster.name);
1255
+ })
1256
+ })
1257
+ })
1258
+ ]
1259
+ })
1260
+ }),
1261
+ /* @__PURE__ */ jsx(GraphCommandMenuDialog, {
1262
+ nodes: props.nodes,
1263
+ onHighlight: (value) => {
1264
+ if (!value) return highlightedNodeIdAtom.set(null);
1265
+ if (!props.nodes.find((node) => node.path === value)) return;
1266
+ highlightedNodeIdAtom.set(value);
1267
+ },
1268
+ onSelect: (value) => {
1269
+ highlightedNodeIdAtom.set(null);
1270
+ if (!value) return currentNodeIdAtom.set(null);
1271
+ if (!props.nodes.find((node) => node.path === value)) return;
1272
+ currentNodeIdAtom.set(value);
1273
+ selectedNodeIdsAtom.set((prev) => prev.includes(value) ? prev.filter((id) => id !== value) : [...prev, value]);
1274
+ }
1275
+ }),
1276
+ /* @__PURE__ */ jsx(ControlsContainer, {
1277
+ position: "bottom-left",
1278
+ children: /* @__PURE__ */ jsx(MiniMap, {
1279
+ width: "100px",
1280
+ height: "100px"
1281
+ })
1282
+ }),
1283
+ /* @__PURE__ */ jsx(NodeDetailsModal, { output: props.output })
1284
+ ] });
1285
+ };
1286
+ function SidebarIconButton(props) {
1287
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1288
+ asChild: true,
1289
+ children: /* @__PURE__ */ jsx(Button, {
1290
+ size: "icon",
1291
+ variant: "outline",
1292
+ onClick: props.onClick,
1293
+ "aria-label": props.label,
1294
+ children: props.children
1295
+ })
1296
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: props.label })] });
1297
+ }
1298
+ function ClusterCombobox(props) {
1299
+ const [open, setOpen] = useState(false);
1300
+ return /* @__PURE__ */ jsxs(Popover, {
1301
+ open,
1302
+ onOpenChange: setOpen,
1303
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
1304
+ asChild: true,
1305
+ children: /* @__PURE__ */ jsxs(Button, {
1306
+ variant: "outline",
1307
+ role: "combobox",
1308
+ "aria-expanded": open,
1309
+ className: "w-full justify-between",
1310
+ children: ["Jump to cluster", /* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-4 opacity-50" })]
1311
+ })
1312
+ }), /* @__PURE__ */ jsx(PopoverContent, {
1313
+ className: "w-[20rem] p-0",
1314
+ align: "start",
1315
+ children: /* @__PURE__ */ jsxs(Command, { children: [/* @__PURE__ */ jsx(CommandInput, { placeholder: "Search cluster groups..." }), /* @__PURE__ */ jsxs(CommandList, { children: [/* @__PURE__ */ jsx(CommandEmpty, { children: "No cluster found." }), /* @__PURE__ */ jsx(CommandGroup, {
1316
+ heading: "Cluster groups",
1317
+ children: props.clusters.map((cluster) => /* @__PURE__ */ jsxs(CommandItem, {
1318
+ value: `${cluster.inferredName || cluster.name} ${cluster.name} ${cluster.path}`,
1319
+ onMouseEnter: () => hoveredClusterNameAtom.set(cluster.name),
1320
+ onMouseLeave: () => hoveredClusterNameAtom.set(null),
1321
+ onSelect: () => {
1322
+ props.onFocusCluster(cluster.name);
1323
+ setOpen(false);
1324
+ },
1325
+ children: [/* @__PURE__ */ jsx("div", {
1326
+ className: "size-2 rounded-sm",
1327
+ style: { backgroundColor: cluster.color }
1328
+ }), /* @__PURE__ */ jsxs("div", {
1329
+ className: "flex min-w-0 flex-1 items-center justify-between gap-3",
1330
+ children: [/* @__PURE__ */ jsx("span", {
1331
+ className: "truncate",
1332
+ children: cluster.inferredName || cluster.name
1333
+ }), /* @__PURE__ */ jsx("span", {
1334
+ className: "shrink-0 text-xs text-slate-500",
1335
+ children: cluster.nodes.length
1336
+ })]
1337
+ })]
1338
+ }, cluster.name))
1339
+ })] })] })
1340
+ })]
1341
+ });
1342
+ }
1343
+ var useClusterMap = (sigma) => {
1344
+ const [clusterMap, setClusterMap] = useState(() => /* @__PURE__ */ new Map());
1345
+ useEffect(() => {
1346
+ const listener = () => {
1347
+ const map = /* @__PURE__ */ new Map();
1348
+ map.clear();
1349
+ sigma.getGraph().forEachNode((nodeId, attrs) => {
1350
+ if (!attrs.cluster) return;
1351
+ const cluster = map.get(attrs.cluster);
1352
+ if (cluster) {
1353
+ cluster.nodes.push(nodeId);
1354
+ cluster.positions.push({
1355
+ x: attrs.x,
1356
+ y: attrs.y
1357
+ });
1358
+ cluster.isExternal = cluster.isExternal || attrs.modType === "external";
1359
+ return;
1360
+ }
1361
+ map.set(attrs.cluster, {
1362
+ name: attrs.cluster,
1363
+ isExternal: attrs.modType === "external",
1364
+ path: attrs.clusterPath ?? "",
1365
+ nodes: [nodeId],
1366
+ positions: [{
1367
+ x: attrs.x,
1368
+ y: attrs.y
1369
+ }],
1370
+ color: attrs.color
1371
+ });
1372
+ });
1373
+ map.forEach((cluster) => {
1374
+ if (!cluster.isExternal) {
1375
+ const pathsLabel = inferPathsLabel(cluster.nodes.map((path) => path.replace(cluster.path, "")));
1376
+ if (pathsLabel) cluster.inferredName = pathsLabel;
1377
+ }
1378
+ cluster.x = cluster.positions.reduce((acc, p) => acc + p.x, 0) / cluster.positions.length;
1379
+ cluster.y = cluster.positions.reduce((acc, p) => acc + p.y, 0) / cluster.positions.length;
1380
+ });
1381
+ setClusterMap(map);
1382
+ };
1383
+ sigma.addListener("afterProcess", listener);
1384
+ return () => {
1385
+ sigma.removeListener("afterProcess", listener);
1386
+ };
1387
+ }, [sigma]);
1388
+ return clusterMap;
1389
+ };
1390
+ var useClusterList = (clusterMap) => {
1391
+ return useMemo(() => {
1392
+ return Array.from(clusterMap.entries()).sort((a, b) => {
1393
+ const res = b[1].nodes.length - a[1].nodes.length;
1394
+ return res !== 0 ? res : a[0].localeCompare(b[0]);
1395
+ }).map(([_name, cluster]) => cluster);
1396
+ }, [clusterMap]);
1397
+ };
1398
+ var useClusterLabelLayer = (sigma, clusterMap, hideClusterLabels) => {
1399
+ useEffect(() => {
1400
+ const visibleClusters = Array.from(clusterMap.values()).filter((cluster) => cluster.nodes.length >= 8).sort((left, right) => right.nodes.length - left.nodes.length).slice(0, 24);
1401
+ let layer = document.getElementById("cluster-label-layers");
1402
+ const hasLayer = Boolean(layer);
1403
+ if (!hasLayer) layer = document.createElement("div");
1404
+ layer.id = "cluster-label-layers";
1405
+ layer.dataset.hidden = hideClusterLabels ? "true" : void 0;
1406
+ if (hideClusterLabels) layer.dataset.hidden = "true";
1407
+ else delete layer.dataset.hidden;
1408
+ layer.className = "absolute top-0 left-0 w-full h-full pointer-events-none overflow-hidden data-hidden:hidden";
1409
+ let clusterLabelsDoms = "";
1410
+ visibleClusters.forEach((cluster) => {
1411
+ const viewportPos = sigma.graphToViewport(cluster);
1412
+ const fontSize = clamp(14, 28, 10 + Math.log2(cluster.nodes.length) * 3);
1413
+ clusterLabelsDoms += `<div id='${cluster.name}' class="absolute -translate-1/2 -translate-y-1/2 text-shadow-md font-semibold whitespace-nowrap" style="top:${viewportPos.y}px;left:${viewportPos.x}px;color:${cluster.color};font-size:${fontSize}px;background:color-mix(in srgb, white 78%, transparent);padding:2px 8px;border-radius:999px;backdrop-filter:blur(2px);opacity:0.92">${cluster.inferredName || cluster.name}</div>`;
1414
+ });
1415
+ layer.innerHTML = clusterLabelsDoms;
1416
+ if (!hasLayer) {
1417
+ const container = sigma.getContainer();
1418
+ container.insertBefore(layer, container.querySelector(".sigma-hovers"));
1419
+ }
1420
+ const listener = () => {
1421
+ visibleClusters.forEach((cluster) => {
1422
+ const clusterLabel = document.getElementById(cluster.name);
1423
+ if (clusterLabel) {
1424
+ const viewportPos = sigma.graphToViewport(cluster);
1425
+ clusterLabel.style.top = `${viewportPos.y}px`;
1426
+ clusterLabel.style.left = `${viewportPos.x}px`;
1427
+ }
1428
+ });
1429
+ };
1430
+ sigma.on("afterRender", listener);
1431
+ return () => {
1432
+ sigma.off("afterRender", listener);
1433
+ };
1434
+ }, [
1435
+ clusterMap,
1436
+ hideClusterLabels,
1437
+ sigma
1438
+ ]);
1439
+ };
1440
+ //#endregion
1441
+ export { ModvizSigma };