@n-dx/web 0.1.0

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 (671) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +5 -0
  3. package/build.js +243 -0
  4. package/dist/cli/index.d.ts +8 -0
  5. package/dist/cli/index.js +50 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/landing/index.html +325 -0
  8. package/dist/landing/landing.d.ts +38 -0
  9. package/dist/landing/landing.js +299 -0
  10. package/dist/landing/landing.js.map +1 -0
  11. package/dist/public.d.ts +55 -0
  12. package/dist/public.js +51 -0
  13. package/dist/public.js.map +1 -0
  14. package/dist/schema/features.d.ts +28 -0
  15. package/dist/schema/features.js +9 -0
  16. package/dist/schema/features.js.map +1 -0
  17. package/dist/schema/v1.d.ts +317 -0
  18. package/dist/schema/v1.js +3 -0
  19. package/dist/schema/v1.js.map +1 -0
  20. package/dist/server/aggregation-cache.d.ts +105 -0
  21. package/dist/server/aggregation-cache.js +163 -0
  22. package/dist/server/aggregation-cache.js.map +1 -0
  23. package/dist/server/concurrent-execution-metrics.d.ts +103 -0
  24. package/dist/server/concurrent-execution-metrics.js +253 -0
  25. package/dist/server/concurrent-execution-metrics.js.map +1 -0
  26. package/dist/server/domain-gateway.d.ts +18 -0
  27. package/dist/server/domain-gateway.js +19 -0
  28. package/dist/server/domain-gateway.js.map +1 -0
  29. package/dist/server/incremental-task-usage.d.ts +92 -0
  30. package/dist/server/incremental-task-usage.js +251 -0
  31. package/dist/server/incremental-task-usage.js.map +1 -0
  32. package/dist/server/index.d.ts +39 -0
  33. package/dist/server/index.js +36 -0
  34. package/dist/server/index.js.map +1 -0
  35. package/dist/server/port.d.ts +96 -0
  36. package/dist/server/port.js +134 -0
  37. package/dist/server/port.js.map +1 -0
  38. package/dist/server/pr-markdown-refresh-diagnostics.d.ts +59 -0
  39. package/dist/server/pr-markdown-refresh-diagnostics.js +429 -0
  40. package/dist/server/pr-markdown-refresh-diagnostics.js.map +1 -0
  41. package/dist/server/prd-io.d.ts +40 -0
  42. package/dist/server/prd-io.js +66 -0
  43. package/dist/server/prd-io.js.map +1 -0
  44. package/dist/server/process-memory-tracker.d.ts +79 -0
  45. package/dist/server/process-memory-tracker.js +194 -0
  46. package/dist/server/process-memory-tracker.js.map +1 -0
  47. package/dist/server/register-scheduler.d.ts +53 -0
  48. package/dist/server/register-scheduler.js +36 -0
  49. package/dist/server/register-scheduler.js.map +1 -0
  50. package/dist/server/rex-gateway.d.ts +39 -0
  51. package/dist/server/rex-gateway.js +47 -0
  52. package/dist/server/rex-gateway.js.map +1 -0
  53. package/dist/server/routes-adaptive.d.ts +21 -0
  54. package/dist/server/routes-adaptive.js +659 -0
  55. package/dist/server/routes-adaptive.js.map +1 -0
  56. package/dist/server/routes-config.d.ts +47 -0
  57. package/dist/server/routes-config.js +222 -0
  58. package/dist/server/routes-config.js.map +1 -0
  59. package/dist/server/routes-data.d.ts +14 -0
  60. package/dist/server/routes-data.js +129 -0
  61. package/dist/server/routes-data.js.map +1 -0
  62. package/dist/server/routes-features.d.ts +14 -0
  63. package/dist/server/routes-features.js +245 -0
  64. package/dist/server/routes-features.js.map +1 -0
  65. package/dist/server/routes-hench.d.ts +116 -0
  66. package/dist/server/routes-hench.js +2016 -0
  67. package/dist/server/routes-hench.js.map +1 -0
  68. package/dist/server/routes-integrations.d.ts +23 -0
  69. package/dist/server/routes-integrations.js +277 -0
  70. package/dist/server/routes-integrations.js.map +1 -0
  71. package/dist/server/routes-mcp.d.ts +31 -0
  72. package/dist/server/routes-mcp.js +175 -0
  73. package/dist/server/routes-mcp.js.map +1 -0
  74. package/dist/server/routes-notion.d.ts +23 -0
  75. package/dist/server/routes-notion.js +723 -0
  76. package/dist/server/routes-notion.js.map +1 -0
  77. package/dist/server/routes-project.d.ts +47 -0
  78. package/dist/server/routes-project.js +128 -0
  79. package/dist/server/routes-project.js.map +1 -0
  80. package/dist/server/routes-rex/analysis.d.ts +11 -0
  81. package/dist/server/routes-rex/analysis.js +583 -0
  82. package/dist/server/routes-rex/analysis.js.map +1 -0
  83. package/dist/server/routes-rex/execution.d.ts +37 -0
  84. package/dist/server/routes-rex/execution.js +355 -0
  85. package/dist/server/routes-rex/execution.js.map +1 -0
  86. package/dist/server/routes-rex/health.d.ts +8 -0
  87. package/dist/server/routes-rex/health.js +136 -0
  88. package/dist/server/routes-rex/health.js.map +1 -0
  89. package/dist/server/routes-rex/index.d.ts +35 -0
  90. package/dist/server/routes-rex/index.js +72 -0
  91. package/dist/server/routes-rex/index.js.map +1 -0
  92. package/dist/server/routes-rex/items.d.ts +8 -0
  93. package/dist/server/routes-rex/items.js +359 -0
  94. package/dist/server/routes-rex/items.js.map +1 -0
  95. package/dist/server/routes-rex/prune.d.ts +11 -0
  96. package/dist/server/routes-rex/prune.js +379 -0
  97. package/dist/server/routes-rex/prune.js.map +1 -0
  98. package/dist/server/routes-rex/reads.d.ts +9 -0
  99. package/dist/server/routes-rex/reads.js +119 -0
  100. package/dist/server/routes-rex/reads.js.map +1 -0
  101. package/dist/server/routes-rex/requirements.d.ts +10 -0
  102. package/dist/server/routes-rex/requirements.js +408 -0
  103. package/dist/server/routes-rex/requirements.js.map +1 -0
  104. package/dist/server/routes-rex/shared.d.ts +37 -0
  105. package/dist/server/routes-rex/shared.js +73 -0
  106. package/dist/server/routes-rex/shared.js.map +1 -0
  107. package/dist/server/routes-search.d.ts +26 -0
  108. package/dist/server/routes-search.js +82 -0
  109. package/dist/server/routes-search.js.map +1 -0
  110. package/dist/server/routes-sourcevision.d.ts +18 -0
  111. package/dist/server/routes-sourcevision.js +444 -0
  112. package/dist/server/routes-sourcevision.js.map +1 -0
  113. package/dist/server/routes-static.d.ts +20 -0
  114. package/dist/server/routes-static.js +168 -0
  115. package/dist/server/routes-static.js.map +1 -0
  116. package/dist/server/routes-status.d.ts +58 -0
  117. package/dist/server/routes-status.js +191 -0
  118. package/dist/server/routes-status.js.map +1 -0
  119. package/dist/server/routes-token-usage.d.ts +63 -0
  120. package/dist/server/routes-token-usage.js +720 -0
  121. package/dist/server/routes-token-usage.js.map +1 -0
  122. package/dist/server/routes-validation.d.ts +10 -0
  123. package/dist/server/routes-validation.js +365 -0
  124. package/dist/server/routes-validation.js.map +1 -0
  125. package/dist/server/routes-workflow.d.ts +15 -0
  126. package/dist/server/routes-workflow.js +498 -0
  127. package/dist/server/routes-workflow.js.map +1 -0
  128. package/dist/server/search-index.d.ts +111 -0
  129. package/dist/server/search-index.js +348 -0
  130. package/dist/server/search-index.js.map +1 -0
  131. package/dist/server/shared-types.d.ts +65 -0
  132. package/dist/server/shared-types.js +31 -0
  133. package/dist/server/shared-types.js.map +1 -0
  134. package/dist/server/start.d.ts +68 -0
  135. package/dist/server/start.js +568 -0
  136. package/dist/server/start.js.map +1 -0
  137. package/dist/server/task-usage.d.ts +17 -0
  138. package/dist/server/task-usage.js +20 -0
  139. package/dist/server/task-usage.js.map +1 -0
  140. package/dist/server/types.d.ts +27 -0
  141. package/dist/server/types.js +26 -0
  142. package/dist/server/types.js.map +1 -0
  143. package/dist/server/usage-cleanup-scheduler.d.ts +107 -0
  144. package/dist/server/usage-cleanup-scheduler.js +232 -0
  145. package/dist/server/usage-cleanup-scheduler.js.map +1 -0
  146. package/dist/server/websocket.d.ts +131 -0
  147. package/dist/server/websocket.js +512 -0
  148. package/dist/server/websocket.js.map +1 -0
  149. package/dist/shared/data-files.d.ts +17 -0
  150. package/dist/shared/data-files.js +18 -0
  151. package/dist/shared/data-files.js.map +1 -0
  152. package/dist/shared/index.d.ts +9 -0
  153. package/dist/shared/index.js +9 -0
  154. package/dist/shared/index.js.map +1 -0
  155. package/dist/shared/view-id.d.ts +8 -0
  156. package/dist/shared/view-id.js +9 -0
  157. package/dist/shared/view-id.js.map +1 -0
  158. package/dist/viewer/Hench-F.png +0 -0
  159. package/dist/viewer/Rex-F.png +0 -0
  160. package/dist/viewer/SourceVision-F.png +0 -0
  161. package/dist/viewer/SourceVision.png +0 -0
  162. package/dist/viewer/api.d.ts +19 -0
  163. package/dist/viewer/api.js +17 -0
  164. package/dist/viewer/api.js.map +1 -0
  165. package/dist/viewer/bootstrap.d.ts +13 -0
  166. package/dist/viewer/bootstrap.js +40 -0
  167. package/dist/viewer/bootstrap.js.map +1 -0
  168. package/dist/viewer/components/active-tasks-panel.d.ts +36 -0
  169. package/dist/viewer/components/active-tasks-panel.js +183 -0
  170. package/dist/viewer/components/active-tasks-panel.js.map +1 -0
  171. package/dist/viewer/components/breadcrumb.d.ts +21 -0
  172. package/dist/viewer/components/breadcrumb.js +117 -0
  173. package/dist/viewer/components/breadcrumb.js.map +1 -0
  174. package/dist/viewer/components/concurrency-panel.d.ts +18 -0
  175. package/dist/viewer/components/concurrency-panel.js +175 -0
  176. package/dist/viewer/components/concurrency-panel.js.map +1 -0
  177. package/dist/viewer/components/config-footer.d.ts +11 -0
  178. package/dist/viewer/components/config-footer.js +132 -0
  179. package/dist/viewer/components/config-footer.js.map +1 -0
  180. package/dist/viewer/components/constants.d.ts +10 -0
  181. package/dist/viewer/components/constants.js +11 -0
  182. package/dist/viewer/components/constants.js.map +1 -0
  183. package/dist/viewer/components/copy-link-button.d.ts +30 -0
  184. package/dist/viewer/components/copy-link-button.js +74 -0
  185. package/dist/viewer/components/copy-link-button.js.map +1 -0
  186. package/dist/viewer/components/crash-recovery-banner.d.ts +28 -0
  187. package/dist/viewer/components/crash-recovery-banner.js +47 -0
  188. package/dist/viewer/components/crash-recovery-banner.js.map +1 -0
  189. package/dist/viewer/components/data-display/collapsible-section.d.ts +14 -0
  190. package/dist/viewer/components/data-display/collapsible-section.js +66 -0
  191. package/dist/viewer/components/data-display/collapsible-section.js.map +1 -0
  192. package/dist/viewer/components/data-display/findings-list.d.ts +13 -0
  193. package/dist/viewer/components/data-display/findings-list.js +136 -0
  194. package/dist/viewer/components/data-display/findings-list.js.map +1 -0
  195. package/dist/viewer/components/data-display/health-gauge.d.ts +37 -0
  196. package/dist/viewer/components/data-display/health-gauge.js +62 -0
  197. package/dist/viewer/components/data-display/health-gauge.js.map +1 -0
  198. package/dist/viewer/components/data-display/mini-charts.d.ts +36 -0
  199. package/dist/viewer/components/data-display/mini-charts.js +124 -0
  200. package/dist/viewer/components/data-display/mini-charts.js.map +1 -0
  201. package/dist/viewer/components/data-display/tree-view.d.ts +18 -0
  202. package/dist/viewer/components/data-display/tree-view.js +107 -0
  203. package/dist/viewer/components/data-display/tree-view.js.map +1 -0
  204. package/dist/viewer/components/data-display/zone-map.d.ts +28 -0
  205. package/dist/viewer/components/data-display/zone-map.js +197 -0
  206. package/dist/viewer/components/data-display/zone-map.js.map +1 -0
  207. package/dist/viewer/components/degradation-banner.d.ts +28 -0
  208. package/dist/viewer/components/degradation-banner.js +47 -0
  209. package/dist/viewer/components/degradation-banner.js.map +1 -0
  210. package/dist/viewer/components/detail-panel.d.ts +12 -0
  211. package/dist/viewer/components/detail-panel.js +166 -0
  212. package/dist/viewer/components/detail-panel.js.map +1 -0
  213. package/dist/viewer/components/elapsed-time.d.ts +38 -0
  214. package/dist/viewer/components/elapsed-time.js +35 -0
  215. package/dist/viewer/components/elapsed-time.js.map +1 -0
  216. package/dist/viewer/components/faq.d.ts +12 -0
  217. package/dist/viewer/components/faq.js +237 -0
  218. package/dist/viewer/components/faq.js.map +1 -0
  219. package/dist/viewer/components/favicon.d.ts +30 -0
  220. package/dist/viewer/components/favicon.js +90 -0
  221. package/dist/viewer/components/favicon.js.map +1 -0
  222. package/dist/viewer/components/guide.d.ts +5 -0
  223. package/dist/viewer/components/guide.js +116 -0
  224. package/dist/viewer/components/guide.js.map +1 -0
  225. package/dist/viewer/components/index.d.ts +37 -0
  226. package/dist/viewer/components/index.js +46 -0
  227. package/dist/viewer/components/index.js.map +1 -0
  228. package/dist/viewer/components/logos.d.ts +42 -0
  229. package/dist/viewer/components/logos.js +46 -0
  230. package/dist/viewer/components/logos.js.map +1 -0
  231. package/dist/viewer/components/memory-panel.d.ts +19 -0
  232. package/dist/viewer/components/memory-panel.js +181 -0
  233. package/dist/viewer/components/memory-panel.js.map +1 -0
  234. package/dist/viewer/components/memory-warning.d.ts +23 -0
  235. package/dist/viewer/components/memory-warning.js +42 -0
  236. package/dist/viewer/components/memory-warning.js.map +1 -0
  237. package/dist/viewer/components/notion-schema-wizard.d.ts +16 -0
  238. package/dist/viewer/components/notion-schema-wizard.js +263 -0
  239. package/dist/viewer/components/notion-schema-wizard.js.map +1 -0
  240. package/dist/viewer/components/polling-suspension-indicator.d.ts +24 -0
  241. package/dist/viewer/components/polling-suspension-indicator.js +31 -0
  242. package/dist/viewer/components/polling-suspension-indicator.js.map +1 -0
  243. package/dist/viewer/components/prd-tree/add-item-form.d.ts +31 -0
  244. package/dist/viewer/components/prd-tree/add-item-form.js +231 -0
  245. package/dist/viewer/components/prd-tree/add-item-form.js.map +1 -0
  246. package/dist/viewer/components/prd-tree/analyze-panel.d.ts +13 -0
  247. package/dist/viewer/components/prd-tree/analyze-panel.js +245 -0
  248. package/dist/viewer/components/prd-tree/analyze-panel.js.map +1 -0
  249. package/dist/viewer/components/prd-tree/batch-import-panel.d.ts +34 -0
  250. package/dist/viewer/components/prd-tree/batch-import-panel.js +415 -0
  251. package/dist/viewer/components/prd-tree/batch-import-panel.js.map +1 -0
  252. package/dist/viewer/components/prd-tree/bulk-actions.d.ts +19 -0
  253. package/dist/viewer/components/prd-tree/bulk-actions.js +90 -0
  254. package/dist/viewer/components/prd-tree/bulk-actions.js.map +1 -0
  255. package/dist/viewer/components/prd-tree/compute.d.ts +32 -0
  256. package/dist/viewer/components/prd-tree/compute.js +129 -0
  257. package/dist/viewer/components/prd-tree/compute.js.map +1 -0
  258. package/dist/viewer/components/prd-tree/delete-confirmation.d.ts +23 -0
  259. package/dist/viewer/components/prd-tree/delete-confirmation.js +83 -0
  260. package/dist/viewer/components/prd-tree/delete-confirmation.js.map +1 -0
  261. package/dist/viewer/components/prd-tree/execution-panel.d.ts +14 -0
  262. package/dist/viewer/components/prd-tree/execution-panel.js +291 -0
  263. package/dist/viewer/components/prd-tree/execution-panel.js.map +1 -0
  264. package/dist/viewer/components/prd-tree/facet-filter.d.ts +40 -0
  265. package/dist/viewer/components/prd-tree/facet-filter.js +188 -0
  266. package/dist/viewer/components/prd-tree/facet-filter.js.map +1 -0
  267. package/dist/viewer/components/prd-tree/index.d.ts +24 -0
  268. package/dist/viewer/components/prd-tree/index.js +16 -0
  269. package/dist/viewer/components/prd-tree/index.js.map +1 -0
  270. package/dist/viewer/components/prd-tree/inline-add-form.d.ts +32 -0
  271. package/dist/viewer/components/prd-tree/inline-add-form.js +175 -0
  272. package/dist/viewer/components/prd-tree/inline-add-form.js.map +1 -0
  273. package/dist/viewer/components/prd-tree/inline-status-picker.d.ts +31 -0
  274. package/dist/viewer/components/prd-tree/inline-status-picker.js +110 -0
  275. package/dist/viewer/components/prd-tree/inline-status-picker.js.map +1 -0
  276. package/dist/viewer/components/prd-tree/lazy-children.d.ts +40 -0
  277. package/dist/viewer/components/prd-tree/lazy-children.js +73 -0
  278. package/dist/viewer/components/prd-tree/lazy-children.js.map +1 -0
  279. package/dist/viewer/components/prd-tree/levels.d.ts +31 -0
  280. package/dist/viewer/components/prd-tree/levels.js +72 -0
  281. package/dist/viewer/components/prd-tree/levels.js.map +1 -0
  282. package/dist/viewer/components/prd-tree/listener-lifecycle.d.ts +105 -0
  283. package/dist/viewer/components/prd-tree/listener-lifecycle.js +173 -0
  284. package/dist/viewer/components/prd-tree/listener-lifecycle.js.map +1 -0
  285. package/dist/viewer/components/prd-tree/merge-preview.d.ts +25 -0
  286. package/dist/viewer/components/prd-tree/merge-preview.js +181 -0
  287. package/dist/viewer/components/prd-tree/merge-preview.js.map +1 -0
  288. package/dist/viewer/components/prd-tree/prd-tree.d.ts +91 -0
  289. package/dist/viewer/components/prd-tree/prd-tree.js +565 -0
  290. package/dist/viewer/components/prd-tree/prd-tree.js.map +1 -0
  291. package/dist/viewer/components/prd-tree/proposal-editor.d.ts +44 -0
  292. package/dist/viewer/components/prd-tree/proposal-editor.js +438 -0
  293. package/dist/viewer/components/prd-tree/proposal-editor.js.map +1 -0
  294. package/dist/viewer/components/prd-tree/prune-confirmation.d.ts +25 -0
  295. package/dist/viewer/components/prd-tree/prune-confirmation.js +336 -0
  296. package/dist/viewer/components/prd-tree/prune-confirmation.js.map +1 -0
  297. package/dist/viewer/components/prd-tree/prune-diff-tree.d.ts +39 -0
  298. package/dist/viewer/components/prd-tree/prune-diff-tree.js +319 -0
  299. package/dist/viewer/components/prd-tree/prune-diff-tree.js.map +1 -0
  300. package/dist/viewer/components/prd-tree/reorganize-panel.d.ts +16 -0
  301. package/dist/viewer/components/prd-tree/reorganize-panel.js +213 -0
  302. package/dist/viewer/components/prd-tree/reorganize-panel.js.map +1 -0
  303. package/dist/viewer/components/prd-tree/smart-add-input.d.ts +19 -0
  304. package/dist/viewer/components/prd-tree/smart-add-input.js +383 -0
  305. package/dist/viewer/components/prd-tree/smart-add-input.js.map +1 -0
  306. package/dist/viewer/components/prd-tree/status-filter.d.ts +40 -0
  307. package/dist/viewer/components/prd-tree/status-filter.js +131 -0
  308. package/dist/viewer/components/prd-tree/status-filter.js.map +1 -0
  309. package/dist/viewer/components/prd-tree/task-detail.d.ts +41 -0
  310. package/dist/viewer/components/prd-tree/task-detail.js +1205 -0
  311. package/dist/viewer/components/prd-tree/task-detail.js.map +1 -0
  312. package/dist/viewer/components/prd-tree/task-utilization.d.ts +7 -0
  313. package/dist/viewer/components/prd-tree/task-utilization.js +25 -0
  314. package/dist/viewer/components/prd-tree/task-utilization.js.map +1 -0
  315. package/dist/viewer/components/prd-tree/tree-differ.d.ts +59 -0
  316. package/dist/viewer/components/prd-tree/tree-differ.js +200 -0
  317. package/dist/viewer/components/prd-tree/tree-differ.js.map +1 -0
  318. package/dist/viewer/components/prd-tree/tree-event-delegate.d.ts +70 -0
  319. package/dist/viewer/components/prd-tree/tree-event-delegate.js +176 -0
  320. package/dist/viewer/components/prd-tree/tree-event-delegate.js.map +1 -0
  321. package/dist/viewer/components/prd-tree/tree-search.d.ts +65 -0
  322. package/dist/viewer/components/prd-tree/tree-search.js +178 -0
  323. package/dist/viewer/components/prd-tree/tree-search.js.map +1 -0
  324. package/dist/viewer/components/prd-tree/tree-utils.d.ts +38 -0
  325. package/dist/viewer/components/prd-tree/tree-utils.js +107 -0
  326. package/dist/viewer/components/prd-tree/tree-utils.js.map +1 -0
  327. package/dist/viewer/components/prd-tree/types.d.ts +93 -0
  328. package/dist/viewer/components/prd-tree/types.js +16 -0
  329. package/dist/viewer/components/prd-tree/types.js.map +1 -0
  330. package/dist/viewer/components/prd-tree/virtual-scroll.d.ts +119 -0
  331. package/dist/viewer/components/prd-tree/virtual-scroll.js +169 -0
  332. package/dist/viewer/components/prd-tree/virtual-scroll.js.map +1 -0
  333. package/dist/viewer/components/progressive-loader.d.ts +114 -0
  334. package/dist/viewer/components/progressive-loader.js +225 -0
  335. package/dist/viewer/components/progressive-loader.js.map +1 -0
  336. package/dist/viewer/components/refresh-queue-status.d.ts +20 -0
  337. package/dist/viewer/components/refresh-queue-status.js +65 -0
  338. package/dist/viewer/components/refresh-queue-status.js.map +1 -0
  339. package/dist/viewer/components/rex-task-link.d.ts +50 -0
  340. package/dist/viewer/components/rex-task-link.js +218 -0
  341. package/dist/viewer/components/rex-task-link.js.map +1 -0
  342. package/dist/viewer/components/search-filter.d.ts +20 -0
  343. package/dist/viewer/components/search-filter.js +28 -0
  344. package/dist/viewer/components/search-filter.js.map +1 -0
  345. package/dist/viewer/components/search-overlay.d.ts +31 -0
  346. package/dist/viewer/components/search-overlay.js +472 -0
  347. package/dist/viewer/components/search-overlay.js.map +1 -0
  348. package/dist/viewer/components/sidebar.d.ts +18 -0
  349. package/dist/viewer/components/sidebar.js +357 -0
  350. package/dist/viewer/components/sidebar.js.map +1 -0
  351. package/dist/viewer/components/status-indicators.d.ts +63 -0
  352. package/dist/viewer/components/status-indicators.js +136 -0
  353. package/dist/viewer/components/status-indicators.js.map +1 -0
  354. package/dist/viewer/components/theme-toggle.d.ts +8 -0
  355. package/dist/viewer/components/theme-toggle.js +28 -0
  356. package/dist/viewer/components/theme-toggle.js.map +1 -0
  357. package/dist/viewer/components/throttle-controls.d.ts +18 -0
  358. package/dist/viewer/components/throttle-controls.js +304 -0
  359. package/dist/viewer/components/throttle-controls.js.map +1 -0
  360. package/dist/viewer/components/ws-health-panel.d.ts +18 -0
  361. package/dist/viewer/components/ws-health-panel.js +250 -0
  362. package/dist/viewer/components/ws-health-panel.js.map +1 -0
  363. package/dist/viewer/components/zone-slideout.d.ts +17 -0
  364. package/dist/viewer/components/zone-slideout.js +162 -0
  365. package/dist/viewer/components/zone-slideout.js.map +1 -0
  366. package/dist/viewer/crash/crash-detector.d.ts +69 -0
  367. package/dist/viewer/crash/crash-detector.js +239 -0
  368. package/dist/viewer/crash/crash-detector.js.map +1 -0
  369. package/dist/viewer/crash/index.d.ts +7 -0
  370. package/dist/viewer/crash/index.js +8 -0
  371. package/dist/viewer/crash/index.js.map +1 -0
  372. package/dist/viewer/deployed-mode.d.ts +37 -0
  373. package/dist/viewer/deployed-mode.js +94 -0
  374. package/dist/viewer/deployed-mode.js.map +1 -0
  375. package/dist/viewer/external.d.ts +17 -0
  376. package/dist/viewer/external.js +17 -0
  377. package/dist/viewer/external.js.map +1 -0
  378. package/dist/viewer/graph/index.d.ts +9 -0
  379. package/dist/viewer/graph/index.js +12 -0
  380. package/dist/viewer/graph/index.js.map +1 -0
  381. package/dist/viewer/graph/physics.d.ts +96 -0
  382. package/dist/viewer/graph/physics.js +366 -0
  383. package/dist/viewer/graph/physics.js.map +1 -0
  384. package/dist/viewer/graph/renderer.d.ts +184 -0
  385. package/dist/viewer/graph/renderer.js +1438 -0
  386. package/dist/viewer/graph/renderer.js.map +1 -0
  387. package/dist/viewer/hooks/index.d.ts +27 -0
  388. package/dist/viewer/hooks/index.js +28 -0
  389. package/dist/viewer/hooks/index.js.map +1 -0
  390. package/dist/viewer/hooks/use-app-data.d.ts +31 -0
  391. package/dist/viewer/hooks/use-app-data.js +152 -0
  392. package/dist/viewer/hooks/use-app-data.js.map +1 -0
  393. package/dist/viewer/hooks/use-crash-recovery.d.ts +50 -0
  394. package/dist/viewer/hooks/use-crash-recovery.js +76 -0
  395. package/dist/viewer/hooks/use-crash-recovery.js.map +1 -0
  396. package/dist/viewer/hooks/use-delete-actions.d.ts +48 -0
  397. package/dist/viewer/hooks/use-delete-actions.js +103 -0
  398. package/dist/viewer/hooks/use-delete-actions.js.map +1 -0
  399. package/dist/viewer/hooks/use-dom-performance-monitor.d.ts +68 -0
  400. package/dist/viewer/hooks/use-dom-performance-monitor.js +71 -0
  401. package/dist/viewer/hooks/use-dom-performance-monitor.js.map +1 -0
  402. package/dist/viewer/hooks/use-facet-state.d.ts +32 -0
  403. package/dist/viewer/hooks/use-facet-state.js +119 -0
  404. package/dist/viewer/hooks/use-facet-state.js.map +1 -0
  405. package/dist/viewer/hooks/use-feature-toggle.d.ts +18 -0
  406. package/dist/viewer/hooks/use-feature-toggle.js +57 -0
  407. package/dist/viewer/hooks/use-feature-toggle.js.map +1 -0
  408. package/dist/viewer/hooks/use-file-edges.d.ts +23 -0
  409. package/dist/viewer/hooks/use-file-edges.js +221 -0
  410. package/dist/viewer/hooks/use-file-edges.js.map +1 -0
  411. package/dist/viewer/hooks/use-graceful-degradation.d.ts +30 -0
  412. package/dist/viewer/hooks/use-graceful-degradation.js +45 -0
  413. package/dist/viewer/hooks/use-graceful-degradation.js.map +1 -0
  414. package/dist/viewer/hooks/use-item-selection.d.ts +49 -0
  415. package/dist/viewer/hooks/use-item-selection.js +117 -0
  416. package/dist/viewer/hooks/use-item-selection.js.map +1 -0
  417. package/dist/viewer/hooks/use-memory-monitor.d.ts +39 -0
  418. package/dist/viewer/hooks/use-memory-monitor.js +73 -0
  419. package/dist/viewer/hooks/use-memory-monitor.js.map +1 -0
  420. package/dist/viewer/hooks/use-pan-zoom.d.ts +33 -0
  421. package/dist/viewer/hooks/use-pan-zoom.js +110 -0
  422. package/dist/viewer/hooks/use-pan-zoom.js.map +1 -0
  423. package/dist/viewer/hooks/use-persistent-filter.d.ts +24 -0
  424. package/dist/viewer/hooks/use-persistent-filter.js +37 -0
  425. package/dist/viewer/hooks/use-persistent-filter.js.map +1 -0
  426. package/dist/viewer/hooks/use-polling-suspension.d.ts +32 -0
  427. package/dist/viewer/hooks/use-polling-suspension.js +41 -0
  428. package/dist/viewer/hooks/use-polling-suspension.js.map +1 -0
  429. package/dist/viewer/hooks/use-polling.d.ts +39 -0
  430. package/dist/viewer/hooks/use-polling.js +55 -0
  431. package/dist/viewer/hooks/use-polling.js.map +1 -0
  432. package/dist/viewer/hooks/use-prd-actions.d.ts +126 -0
  433. package/dist/viewer/hooks/use-prd-actions.js +250 -0
  434. package/dist/viewer/hooks/use-prd-actions.js.map +1 -0
  435. package/dist/viewer/hooks/use-prd-data.d.ts +45 -0
  436. package/dist/viewer/hooks/use-prd-data.js +159 -0
  437. package/dist/viewer/hooks/use-prd-data.js.map +1 -0
  438. package/dist/viewer/hooks/use-prd-deep-link.d.ts +45 -0
  439. package/dist/viewer/hooks/use-prd-deep-link.js +60 -0
  440. package/dist/viewer/hooks/use-prd-deep-link.js.map +1 -0
  441. package/dist/viewer/hooks/use-prd-websocket.d.ts +47 -0
  442. package/dist/viewer/hooks/use-prd-websocket.js +139 -0
  443. package/dist/viewer/hooks/use-prd-websocket.js.map +1 -0
  444. package/dist/viewer/hooks/use-project-metadata.d.ts +25 -0
  445. package/dist/viewer/hooks/use-project-metadata.js +55 -0
  446. package/dist/viewer/hooks/use-project-metadata.js.map +1 -0
  447. package/dist/viewer/hooks/use-project-status.d.ts +60 -0
  448. package/dist/viewer/hooks/use-project-status.js +133 -0
  449. package/dist/viewer/hooks/use-project-status.js.map +1 -0
  450. package/dist/viewer/hooks/use-refresh-throttle.d.ts +45 -0
  451. package/dist/viewer/hooks/use-refresh-throttle.js +52 -0
  452. package/dist/viewer/hooks/use-refresh-throttle.js.map +1 -0
  453. package/dist/viewer/hooks/use-route-state.d.ts +18 -0
  454. package/dist/viewer/hooks/use-route-state.js +115 -0
  455. package/dist/viewer/hooks/use-route-state.js.map +1 -0
  456. package/dist/viewer/hooks/use-subzone-edges.d.ts +21 -0
  457. package/dist/viewer/hooks/use-subzone-edges.js +147 -0
  458. package/dist/viewer/hooks/use-subzone-edges.js.map +1 -0
  459. package/dist/viewer/hooks/use-tab-visibility.d.ts +31 -0
  460. package/dist/viewer/hooks/use-tab-visibility.js +43 -0
  461. package/dist/viewer/hooks/use-tab-visibility.js.map +1 -0
  462. package/dist/viewer/hooks/use-tick.d.ts +43 -0
  463. package/dist/viewer/hooks/use-tick.js +76 -0
  464. package/dist/viewer/hooks/use-tick.js.map +1 -0
  465. package/dist/viewer/hooks/use-toast.d.ts +24 -0
  466. package/dist/viewer/hooks/use-toast.js +26 -0
  467. package/dist/viewer/hooks/use-toast.js.map +1 -0
  468. package/dist/viewer/hooks/use-zone-drag.d.ts +30 -0
  469. package/dist/viewer/hooks/use-zone-drag.js +60 -0
  470. package/dist/viewer/hooks/use-zone-drag.js.map +1 -0
  471. package/dist/viewer/index.html +36 -0
  472. package/dist/viewer/loader.d.ts +33 -0
  473. package/dist/viewer/loader.js +195 -0
  474. package/dist/viewer/loader.js.map +1 -0
  475. package/dist/viewer/main.d.ts +1 -0
  476. package/dist/viewer/main.js +121 -0
  477. package/dist/viewer/main.js.map +1 -0
  478. package/dist/viewer/messaging/call-rate-limiter.d.ts +50 -0
  479. package/dist/viewer/messaging/call-rate-limiter.js +103 -0
  480. package/dist/viewer/messaging/call-rate-limiter.js.map +1 -0
  481. package/dist/viewer/messaging/fetch-pipeline.d.ts +58 -0
  482. package/dist/viewer/messaging/fetch-pipeline.js +58 -0
  483. package/dist/viewer/messaging/fetch-pipeline.js.map +1 -0
  484. package/dist/viewer/messaging/index.d.ts +43 -0
  485. package/dist/viewer/messaging/index.js +46 -0
  486. package/dist/viewer/messaging/index.js.map +1 -0
  487. package/dist/viewer/messaging/message-coalescer.d.ts +96 -0
  488. package/dist/viewer/messaging/message-coalescer.js +121 -0
  489. package/dist/viewer/messaging/message-coalescer.js.map +1 -0
  490. package/dist/viewer/messaging/message-throttle.d.ts +95 -0
  491. package/dist/viewer/messaging/message-throttle.js +147 -0
  492. package/dist/viewer/messaging/message-throttle.js.map +1 -0
  493. package/dist/viewer/messaging/request-dedup.d.ts +43 -0
  494. package/dist/viewer/messaging/request-dedup.js +55 -0
  495. package/dist/viewer/messaging/request-dedup.js.map +1 -0
  496. package/dist/viewer/messaging/ws-pipeline.d.ts +85 -0
  497. package/dist/viewer/messaging/ws-pipeline.js +68 -0
  498. package/dist/viewer/messaging/ws-pipeline.js.map +1 -0
  499. package/dist/viewer/n-dx.png +0 -0
  500. package/dist/viewer/performance/dom-performance-monitor.d.ts +157 -0
  501. package/dist/viewer/performance/dom-performance-monitor.js +341 -0
  502. package/dist/viewer/performance/dom-performance-monitor.js.map +1 -0
  503. package/dist/viewer/performance/dom-update-gate.d.ts +122 -0
  504. package/dist/viewer/performance/dom-update-gate.js +229 -0
  505. package/dist/viewer/performance/dom-update-gate.js.map +1 -0
  506. package/dist/viewer/performance/graceful-degradation.d.ts +73 -0
  507. package/dist/viewer/performance/graceful-degradation.js +152 -0
  508. package/dist/viewer/performance/graceful-degradation.js.map +1 -0
  509. package/dist/viewer/performance/index.d.ts +14 -0
  510. package/dist/viewer/performance/index.js +20 -0
  511. package/dist/viewer/performance/index.js.map +1 -0
  512. package/dist/viewer/performance/memory-monitor.d.ts +78 -0
  513. package/dist/viewer/performance/memory-monitor.js +218 -0
  514. package/dist/viewer/performance/memory-monitor.js.map +1 -0
  515. package/dist/viewer/performance/refresh-throttle.d.ts +90 -0
  516. package/dist/viewer/performance/refresh-throttle.js +266 -0
  517. package/dist/viewer/performance/refresh-throttle.js.map +1 -0
  518. package/dist/viewer/performance/response-buffer-gate.d.ts +108 -0
  519. package/dist/viewer/performance/response-buffer-gate.js +170 -0
  520. package/dist/viewer/performance/response-buffer-gate.js.map +1 -0
  521. package/dist/viewer/performance/update-batcher.d.ts +79 -0
  522. package/dist/viewer/performance/update-batcher.js +119 -0
  523. package/dist/viewer/performance/update-batcher.js.map +1 -0
  524. package/dist/viewer/polling/batched-tick-dispatcher.d.ts +83 -0
  525. package/dist/viewer/polling/batched-tick-dispatcher.js +183 -0
  526. package/dist/viewer/polling/batched-tick-dispatcher.js.map +1 -0
  527. package/dist/viewer/polling/index.d.ts +13 -0
  528. package/dist/viewer/polling/index.js +16 -0
  529. package/dist/viewer/polling/index.js.map +1 -0
  530. package/dist/viewer/polling/polling-manager.d.ts +82 -0
  531. package/dist/viewer/polling/polling-manager.js +254 -0
  532. package/dist/viewer/polling/polling-manager.js.map +1 -0
  533. package/dist/viewer/polling/polling-restart.d.ts +45 -0
  534. package/dist/viewer/polling/polling-restart.js +98 -0
  535. package/dist/viewer/polling/polling-restart.js.map +1 -0
  536. package/dist/viewer/polling/polling-state.d.ts +182 -0
  537. package/dist/viewer/polling/polling-state.js +306 -0
  538. package/dist/viewer/polling/polling-state.js.map +1 -0
  539. package/dist/viewer/polling/tab-visibility.d.ts +112 -0
  540. package/dist/viewer/polling/tab-visibility.js +276 -0
  541. package/dist/viewer/polling/tab-visibility.js.map +1 -0
  542. package/dist/viewer/polling/tick-timer.d.ts +70 -0
  543. package/dist/viewer/polling/tick-timer.js +168 -0
  544. package/dist/viewer/polling/tick-timer.js.map +1 -0
  545. package/dist/viewer/polling/tick-visibility-gate.d.ts +92 -0
  546. package/dist/viewer/polling/tick-visibility-gate.js +146 -0
  547. package/dist/viewer/polling/tick-visibility-gate.js.map +1 -0
  548. package/dist/viewer/route-state.d.ts +8 -0
  549. package/dist/viewer/route-state.js +77 -0
  550. package/dist/viewer/route-state.js.map +1 -0
  551. package/dist/viewer/schema-compat.d.ts +17 -0
  552. package/dist/viewer/schema-compat.js +49 -0
  553. package/dist/viewer/schema-compat.js.map +1 -0
  554. package/dist/viewer/types.d.ts +66 -0
  555. package/dist/viewer/types.js +2 -0
  556. package/dist/viewer/types.js.map +1 -0
  557. package/dist/viewer/usage/constants.d.ts +14 -0
  558. package/dist/viewer/usage/constants.js +14 -0
  559. package/dist/viewer/usage/constants.js.map +1 -0
  560. package/dist/viewer/usage/index.d.ts +7 -0
  561. package/dist/viewer/usage/index.js +8 -0
  562. package/dist/viewer/usage/index.js.map +1 -0
  563. package/dist/viewer/utils.d.ts +25 -0
  564. package/dist/viewer/utils.js +48 -0
  565. package/dist/viewer/utils.js.map +1 -0
  566. package/dist/viewer/validate.d.ts +23 -0
  567. package/dist/viewer/validate.js +275 -0
  568. package/dist/viewer/validate.js.map +1 -0
  569. package/dist/viewer/views/analysis.d.ts +10 -0
  570. package/dist/viewer/views/analysis.js +109 -0
  571. package/dist/viewer/views/analysis.js.map +1 -0
  572. package/dist/viewer/views/architecture.d.ts +10 -0
  573. package/dist/viewer/views/architecture.js +44 -0
  574. package/dist/viewer/views/architecture.js.map +1 -0
  575. package/dist/viewer/views/domain-hench.d.ts +13 -0
  576. package/dist/viewer/views/domain-hench.js +14 -0
  577. package/dist/viewer/views/domain-hench.js.map +1 -0
  578. package/dist/viewer/views/domain-rex.d.ts +17 -0
  579. package/dist/viewer/views/domain-rex.js +18 -0
  580. package/dist/viewer/views/domain-rex.js.map +1 -0
  581. package/dist/viewer/views/domain-settings.d.ts +12 -0
  582. package/dist/viewer/views/domain-settings.js +13 -0
  583. package/dist/viewer/views/domain-settings.js.map +1 -0
  584. package/dist/viewer/views/domain-sourcevision.d.ts +20 -0
  585. package/dist/viewer/views/domain-sourcevision.js +21 -0
  586. package/dist/viewer/views/domain-sourcevision.js.map +1 -0
  587. package/dist/viewer/views/enrichment-thresholds.d.ts +11 -0
  588. package/dist/viewer/views/enrichment-thresholds.js +12 -0
  589. package/dist/viewer/views/enrichment-thresholds.js.map +1 -0
  590. package/dist/viewer/views/feature-toggles.d.ts +13 -0
  591. package/dist/viewer/views/feature-toggles.js +185 -0
  592. package/dist/viewer/views/feature-toggles.js.map +1 -0
  593. package/dist/viewer/views/files.d.ts +13 -0
  594. package/dist/viewer/views/files.js +174 -0
  595. package/dist/viewer/views/files.js.map +1 -0
  596. package/dist/viewer/views/graph.d.ts +12 -0
  597. package/dist/viewer/views/graph.js +316 -0
  598. package/dist/viewer/views/graph.js.map +1 -0
  599. package/dist/viewer/views/hench-config.d.ts +39 -0
  600. package/dist/viewer/views/hench-config.js +473 -0
  601. package/dist/viewer/views/hench-config.js.map +1 -0
  602. package/dist/viewer/views/hench-runs.d.ts +19 -0
  603. package/dist/viewer/views/hench-runs.js +460 -0
  604. package/dist/viewer/views/hench-runs.js.map +1 -0
  605. package/dist/viewer/views/hench-templates.d.ts +17 -0
  606. package/dist/viewer/views/hench-templates.js +262 -0
  607. package/dist/viewer/views/hench-templates.js.map +1 -0
  608. package/dist/viewer/views/integration-config.d.ts +73 -0
  609. package/dist/viewer/views/integration-config.js +524 -0
  610. package/dist/viewer/views/integration-config.js.map +1 -0
  611. package/dist/viewer/views/notion-config.d.ts +16 -0
  612. package/dist/viewer/views/notion-config.js +357 -0
  613. package/dist/viewer/views/notion-config.js.map +1 -0
  614. package/dist/viewer/views/overview.d.ts +10 -0
  615. package/dist/viewer/views/overview.js +187 -0
  616. package/dist/viewer/views/overview.js.map +1 -0
  617. package/dist/viewer/views/pr-markdown.d.ts +3 -0
  618. package/dist/viewer/views/pr-markdown.js +350 -0
  619. package/dist/viewer/views/pr-markdown.js.map +1 -0
  620. package/dist/viewer/views/prd.d.ts +34 -0
  621. package/dist/viewer/views/prd.js +257 -0
  622. package/dist/viewer/views/prd.js.map +1 -0
  623. package/dist/viewer/views/problems.d.ts +8 -0
  624. package/dist/viewer/views/problems.js +50 -0
  625. package/dist/viewer/views/problems.js.map +1 -0
  626. package/dist/viewer/views/rex-dashboard.d.ts +14 -0
  627. package/dist/viewer/views/rex-dashboard.js +334 -0
  628. package/dist/viewer/views/rex-dashboard.js.map +1 -0
  629. package/dist/viewer/views/routes.d.ts +8 -0
  630. package/dist/viewer/views/routes.js +216 -0
  631. package/dist/viewer/views/routes.js.map +1 -0
  632. package/dist/viewer/views/sourcevision-tabs.d.ts +18 -0
  633. package/dist/viewer/views/sourcevision-tabs.js +14 -0
  634. package/dist/viewer/views/sourcevision-tabs.js.map +1 -0
  635. package/dist/viewer/views/suggestions.d.ts +8 -0
  636. package/dist/viewer/views/suggestions.js +36 -0
  637. package/dist/viewer/views/suggestions.js.map +1 -0
  638. package/dist/viewer/views/task-audit.d.ts +18 -0
  639. package/dist/viewer/views/task-audit.js +413 -0
  640. package/dist/viewer/views/task-audit.js.map +1 -0
  641. package/dist/viewer/views/token-usage.d.ts +10 -0
  642. package/dist/viewer/views/token-usage.js +410 -0
  643. package/dist/viewer/views/token-usage.js.map +1 -0
  644. package/dist/viewer/views/validation.d.ts +11 -0
  645. package/dist/viewer/views/validation.js +475 -0
  646. package/dist/viewer/views/validation.js.map +1 -0
  647. package/dist/viewer/views/view-registry.d.ts +27 -0
  648. package/dist/viewer/views/view-registry.js +70 -0
  649. package/dist/viewer/views/view-registry.js.map +1 -0
  650. package/dist/viewer/views/workflow-optimization.d.ts +12 -0
  651. package/dist/viewer/views/workflow-optimization.js +311 -0
  652. package/dist/viewer/views/workflow-optimization.js.map +1 -0
  653. package/dist/viewer/views/zone-types.d.ts +69 -0
  654. package/dist/viewer/views/zone-types.js +5 -0
  655. package/dist/viewer/views/zone-types.js.map +1 -0
  656. package/dist/viewer/views/zones.d.ts +50 -0
  657. package/dist/viewer/views/zones.js +1438 -0
  658. package/dist/viewer/views/zones.js.map +1 -0
  659. package/dist/viewer/visualization/colors.d.ts +16 -0
  660. package/dist/viewer/visualization/colors.js +31 -0
  661. package/dist/viewer/visualization/colors.js.map +1 -0
  662. package/dist/viewer/visualization/flow.d.ts +54 -0
  663. package/dist/viewer/visualization/flow.js +123 -0
  664. package/dist/viewer/visualization/flow.js.map +1 -0
  665. package/dist/viewer/visualization/index.d.ts +34 -0
  666. package/dist/viewer/visualization/index.js +40 -0
  667. package/dist/viewer/visualization/index.js.map +1 -0
  668. package/dist/viewer/visualization/metrics.d.ts +8 -0
  669. package/dist/viewer/visualization/metrics.js +16 -0
  670. package/dist/viewer/visualization/metrics.js.map +1 -0
  671. package/package.json +54 -0
@@ -0,0 +1,1438 @@
1
+ /**
2
+ * GraphRenderer — imperative SVG rendering for the force-directed import graph.
3
+ *
4
+ * Extracted from viewer/views/graph.ts. Owns all DOM manipulation, event
5
+ * handlers, LOD, and physics integration. Uses AbortController for clean
6
+ * teardown of all event listeners.
7
+ *
8
+ * @remarks Class methods (highlightNode, centerOnNode, selectNode, etc.) are
9
+ * exported as part of the GraphRenderer class. Static analysis may flag these
10
+ * as unused because they are called via class instances, not via static imports.
11
+ */
12
+ import { initZoneClusteredPositions, tick, } from "./physics.js";
13
+ import { basename, truncateFilename } from "../utils.js";
14
+ const SVG_NS = "http://www.w3.org/2000/svg";
15
+ // ── GraphRenderer class ──────────────────────────────────────────────────────
16
+ export class GraphRenderer {
17
+ nodes;
18
+ nodeGroups;
19
+ svg;
20
+ g;
21
+ linkElements;
22
+ nodeRadii;
23
+ resolvedLinks;
24
+ nodeEdgeMap;
25
+ ac;
26
+ sim;
27
+ width;
28
+ height;
29
+ // ViewBox state for zoom/pan
30
+ viewX = 0;
31
+ viewY = 0;
32
+ viewW;
33
+ viewH;
34
+ scale = 1;
35
+ // Lifecycle flag — prevents scheduling new animation frames after destroy
36
+ destroyed = false;
37
+ // Selection state — persists until explicitly cleared
38
+ selectedNodeId = null;
39
+ // Pan gesture flag — set true when a pan actually moved the view;
40
+ // zone hull click handlers check this to suppress clicks after panning.
41
+ wasPanning = false;
42
+ // Label management state
43
+ labelsHidden = false; // user toggle
44
+ labelRects; // reused per frame to avoid GC
45
+ tooltip; // shared tooltip element
46
+ // Zone grouping state
47
+ zoneInfos;
48
+ zoneHullGroup; // container for zone hulls
49
+ zoneHullElements = new Map();
50
+ zoneLabelElements = new Map(); // zone label groups (bg + text)
51
+ collapsedZones = new Set();
52
+ zoneNodeIndices = new Map();
53
+ zonesVisible = true;
54
+ onZoneSelect;
55
+ zoneLabelLayer; // separate layer above nodes for zone labels
56
+ constructor(opts) {
57
+ const { svg, nodes, links, width, height, onNodeSelect, onNodeDblClick, onZoneSelect, zoneInfos } = opts;
58
+ this.svg = svg;
59
+ this.nodes = nodes;
60
+ this.width = width;
61
+ this.height = height;
62
+ this.viewW = width;
63
+ this.viewH = height;
64
+ this.nodeGroups = [];
65
+ this.linkElements = [];
66
+ this.nodeRadii = [];
67
+ this.ac = new AbortController();
68
+ this.zoneInfos = zoneInfos;
69
+ this.onZoneSelect = onZoneSelect;
70
+ // Clear existing SVG content and set up root structure
71
+ svg.innerHTML = "";
72
+ this.updateViewBox();
73
+ this.createSvgDefs(svg);
74
+ this.g = document.createElementNS(SVG_NS, "g");
75
+ svg.appendChild(this.g);
76
+ // Build zone → node index map and zone hull/label layers
77
+ this.buildZoneNodeMap();
78
+ this.zoneHullGroup = this.createSvgLayer("zone-hulls");
79
+ this.zoneLabelLayer = document.createElementNS(SVG_NS, "g");
80
+ this.zoneLabelLayer.setAttribute("class", "zone-label-layer");
81
+ this.createZoneHulls();
82
+ // Resolve link references and build adjacency map
83
+ const nodeMap = new Map();
84
+ for (const n of nodes)
85
+ nodeMap.set(n.id, n);
86
+ initZoneClusteredPositions(nodes, width, height);
87
+ this.resolvedLinks = this.resolveGraphLinks(links, nodeMap);
88
+ this.nodeEdgeMap = this.buildAdjacencyMap();
89
+ // Create SVG elements for links and nodes
90
+ this.createLinkElements();
91
+ const nodeLayer = this.createSvgLayer("graph-node-layer");
92
+ this.createNodeElements(nodeLayer);
93
+ this.labelRects = this.allocateLabelRects();
94
+ // Tooltip and top layers
95
+ this.tooltip = this.createTooltipElement();
96
+ this.g.appendChild(this.zoneLabelLayer);
97
+ this.g.appendChild(this.tooltip);
98
+ // Initialize physics simulation
99
+ this.sim = {
100
+ nodes,
101
+ resolvedLinks: this.resolvedLinks,
102
+ width,
103
+ height,
104
+ alpha: { value: 1 },
105
+ frameCount: 0,
106
+ hasFitted: false,
107
+ scale: this.scale,
108
+ nodeRadii: this.nodeRadii,
109
+ };
110
+ // Set up event listeners and start simulation
111
+ this.setupZoom();
112
+ this.setupPanAndDrag(onNodeSelect, onNodeDblClick);
113
+ this.setupHoverHighlighting();
114
+ this.setupLabelTooltips();
115
+ this.setupTouchInteraction(onNodeSelect, onNodeDblClick);
116
+ this.startSimulation();
117
+ }
118
+ // ── Public API ─────────────────────────────────────────────────────────────
119
+ highlightNode(id) {
120
+ // Clear previous highlights
121
+ const existing = this.g.querySelectorAll(".graph-search-ring");
122
+ existing.forEach((el) => el.remove());
123
+ if (!id)
124
+ return;
125
+ const idx = this.nodes.findIndex((n) => n.id === id);
126
+ if (idx < 0)
127
+ return;
128
+ const ns = SVG_NS;
129
+ const ring = document.createElementNS(ns, "circle");
130
+ ring.setAttribute("class", "graph-search-ring");
131
+ ring.setAttribute("r", String(this.nodeRadii[idx] + 4));
132
+ ring.setAttribute("fill", "none");
133
+ ring.setAttribute("stroke", "var(--accent)");
134
+ ring.setAttribute("stroke-width", "2");
135
+ this.nodeGroups[idx].appendChild(ring);
136
+ }
137
+ centerOnNode(id) {
138
+ const idx = this.nodes.findIndex((n) => n.id === id);
139
+ if (idx < 0)
140
+ return;
141
+ const n = this.nodes[idx];
142
+ if (n.x == null || n.y == null)
143
+ return;
144
+ this.viewX = n.x - this.viewW / 2;
145
+ this.viewY = n.y - this.viewH / 2;
146
+ this.updateViewBox();
147
+ }
148
+ /** Select a node and apply persistent highlighting to its connections. */
149
+ selectNode(id) {
150
+ this.clearSelection();
151
+ if (!id)
152
+ return;
153
+ const idx = this.nodes.findIndex((n) => n.id === id);
154
+ if (idx < 0)
155
+ return;
156
+ this.selectedNodeId = id;
157
+ this.applySelectionHighlight(id);
158
+ this.nodeGroups[idx].classList.add("selected");
159
+ }
160
+ /** Clear the current persistent selection. */
161
+ clearSelection() {
162
+ if (!this.selectedNodeId)
163
+ return;
164
+ // Remove selected class from previously selected node
165
+ const prevIdx = this.nodes.findIndex((n) => n.id === this.selectedNodeId);
166
+ if (prevIdx >= 0) {
167
+ this.nodeGroups[prevIdx].classList.remove("selected");
168
+ }
169
+ this.selectedNodeId = null;
170
+ // Reset all opacities
171
+ for (let j = 0; j < this.nodes.length; j++) {
172
+ this.nodeGroups[j].style.opacity = "";
173
+ }
174
+ for (let j = 0; j < this.linkElements.length; j++) {
175
+ this.linkElements[j].style.strokeOpacity = "";
176
+ this.linkElements[j].style.stroke = "";
177
+ }
178
+ }
179
+ /** Toggle label visibility on/off. Returns the new state. */
180
+ toggleLabels() {
181
+ this.labelsHidden = !this.labelsHidden;
182
+ this.updateLOD();
183
+ return !this.labelsHidden; // true = labels visible
184
+ }
185
+ /** Get current label visibility state. */
186
+ get labelsVisible() {
187
+ return !this.labelsHidden;
188
+ }
189
+ /** Toggle zone hull visibility on/off. Returns the new state. */
190
+ toggleZones() {
191
+ this.zonesVisible = !this.zonesVisible;
192
+ this.zoneHullGroup.style.display = this.zonesVisible ? "" : "none";
193
+ this.zoneLabelLayer.style.display = this.zonesVisible ? "" : "none";
194
+ return this.zonesVisible;
195
+ }
196
+ /** Get current zone visibility state. */
197
+ get zonesGroupsVisible() {
198
+ return this.zonesVisible;
199
+ }
200
+ /** Collapse a zone group — hides member nodes/edges and shows a summary node. */
201
+ collapseZone(zoneId) {
202
+ if (this.collapsedZones.has(zoneId))
203
+ return;
204
+ this.collapsedZones.add(zoneId);
205
+ this.applyZoneCollapse(zoneId);
206
+ }
207
+ /** Expand a previously collapsed zone group. */
208
+ expandZone(zoneId) {
209
+ if (!this.collapsedZones.has(zoneId))
210
+ return;
211
+ this.collapsedZones.delete(zoneId);
212
+ this.applyZoneExpand(zoneId);
213
+ }
214
+ /** Toggle a zone between collapsed and expanded. Returns true if now collapsed. */
215
+ toggleZoneCollapse(zoneId) {
216
+ if (this.collapsedZones.has(zoneId)) {
217
+ this.expandZone(zoneId);
218
+ return false;
219
+ }
220
+ else {
221
+ this.collapseZone(zoneId);
222
+ return true;
223
+ }
224
+ }
225
+ /** Check if a zone is currently collapsed. */
226
+ isZoneCollapsed(zoneId) {
227
+ return this.collapsedZones.has(zoneId);
228
+ }
229
+ /** Zoom in by the given factor (default 1.25 = 25% closer). Zooms toward center. */
230
+ zoomIn(factor = 1.25) {
231
+ this.applyZoomFromCenter(1 / factor);
232
+ }
233
+ /** Zoom out by the given factor (default 1.25 = 25% further). Zooms from center. */
234
+ zoomOut(factor = 1.25) {
235
+ this.applyZoomFromCenter(factor);
236
+ }
237
+ /** Reset the viewport to fit all content. */
238
+ resetView() {
239
+ this.fitToContent();
240
+ this.updateLOD();
241
+ }
242
+ destroy() {
243
+ this.destroyed = true;
244
+ this.ac.abort();
245
+ // Stop the physics simulation so no more ticks are scheduled
246
+ this.sim.alpha.value = 0;
247
+ // Release large data structures for GC
248
+ this.nodeEdgeMap.clear();
249
+ this.zoneHullElements.clear();
250
+ this.zoneLabelElements.clear();
251
+ this.collapsedZones.clear();
252
+ this.zoneNodeIndices.clear();
253
+ this.resolvedLinks.length = 0;
254
+ this.linkElements.length = 0;
255
+ this.nodeGroups.length = 0;
256
+ this.nodes.length = 0;
257
+ this.labelRects.length = 0;
258
+ }
259
+ // ── Private: Constructor helpers ──────────────────────────────────────────
260
+ /** Create SVG <defs> with the arrowhead marker. */
261
+ createSvgDefs(svg) {
262
+ const ns = SVG_NS;
263
+ const defs = document.createElementNS(ns, "defs");
264
+ const marker = document.createElementNS(ns, "marker");
265
+ marker.setAttribute("id", "arrowhead");
266
+ marker.setAttribute("viewBox", "0 0 10 7");
267
+ marker.setAttribute("refX", "10");
268
+ marker.setAttribute("refY", "3.5");
269
+ marker.setAttribute("markerWidth", "6");
270
+ marker.setAttribute("markerHeight", "5");
271
+ marker.setAttribute("orient", "auto");
272
+ const polygon = document.createElementNS(ns, "polygon");
273
+ polygon.setAttribute("points", "0 0, 10 3.5, 0 7");
274
+ polygon.setAttribute("fill", "var(--border)");
275
+ marker.appendChild(polygon);
276
+ defs.appendChild(marker);
277
+ svg.appendChild(defs);
278
+ }
279
+ /** Build the zone → node-index map used for hull rendering and collapse. */
280
+ buildZoneNodeMap() {
281
+ for (let i = 0; i < this.nodes.length; i++) {
282
+ const z = this.nodes[i].zone;
283
+ if (!z)
284
+ continue;
285
+ let indices = this.zoneNodeIndices.get(z);
286
+ if (!indices) {
287
+ indices = [];
288
+ this.zoneNodeIndices.set(z, indices);
289
+ }
290
+ indices.push(i);
291
+ }
292
+ }
293
+ /** Create an SVG <g> layer, append it to the root group, and return it. */
294
+ createSvgLayer(className) {
295
+ const layer = document.createElementNS(SVG_NS, "g");
296
+ layer.setAttribute("class", className);
297
+ this.g.appendChild(layer);
298
+ return layer;
299
+ }
300
+ /** Resolve string-based link endpoints to GraphNode references. */
301
+ resolveGraphLinks(links, nodeMap) {
302
+ return links.map((l) => ({
303
+ ...l,
304
+ source: nodeMap.get(typeof l.source === "string" ? l.source : l.source.id),
305
+ target: nodeMap.get(typeof l.target === "string" ? l.target : l.target.id),
306
+ })).filter((l) => l.source && l.target);
307
+ }
308
+ /** Build a node-id → edge-index adjacency map for hover/select highlighting. */
309
+ buildAdjacencyMap() {
310
+ const map = new Map();
311
+ for (let i = 0; i < this.resolvedLinks.length; i++) {
312
+ const l = this.resolvedLinks[i];
313
+ const sId = l.source.id;
314
+ const tId = l.target.id;
315
+ if (!map.has(sId))
316
+ map.set(sId, new Set());
317
+ if (!map.has(tId))
318
+ map.set(tId, new Set());
319
+ map.get(sId).add(i);
320
+ map.get(tId).add(i);
321
+ }
322
+ return map;
323
+ }
324
+ /** Create SVG line elements for all resolved links. */
325
+ createLinkElements() {
326
+ for (const l of this.resolvedLinks) {
327
+ const line = document.createElementNS(SVG_NS, "line");
328
+ line.setAttribute("class", `graph-link${l.crossZone ? " cross-zone" : ""}`);
329
+ line.setAttribute("marker-end", "url(#arrowhead)");
330
+ this.g.appendChild(line);
331
+ this.linkElements.push(line);
332
+ }
333
+ }
334
+ /** Create SVG groups for all nodes (hit target, circle, label). */
335
+ createNodeElements(nodeLayer) {
336
+ for (const n of this.nodes) {
337
+ const group = document.createElementNS(SVG_NS, "g");
338
+ group.setAttribute("class", "graph-node");
339
+ const radius = Math.min(3 + Math.sqrt(n.importCount) * 2, 16);
340
+ this.nodeRadii.push(radius);
341
+ // Invisible hit target — larger than the visible circle for easy clicking
342
+ const hitTarget = document.createElementNS(SVG_NS, "circle");
343
+ hitTarget.setAttribute("r", String(Math.max(radius + 4, 10)));
344
+ hitTarget.setAttribute("fill", "transparent");
345
+ hitTarget.setAttribute("class", "graph-node-hit");
346
+ group.appendChild(hitTarget);
347
+ const circle = document.createElementNS(SVG_NS, "circle");
348
+ circle.setAttribute("r", String(radius));
349
+ circle.setAttribute("fill", n.zoneColor || "#555");
350
+ group.appendChild(circle);
351
+ // Always create labels — LOD + overlap detection controls visibility
352
+ const fullName = basename(n.id);
353
+ const label = document.createElementNS(SVG_NS, "text");
354
+ label.setAttribute("class", "graph-label");
355
+ label.setAttribute("dy", String(-radius - 3));
356
+ label.setAttribute("text-anchor", "middle");
357
+ label.setAttribute("data-full", fullName);
358
+ label.textContent = truncateFilename(fullName);
359
+ group.appendChild(label);
360
+ nodeLayer.appendChild(group);
361
+ this.nodeGroups.push(group);
362
+ }
363
+ }
364
+ /** Pre-allocate label rect objects reused each frame to avoid GC pressure. */
365
+ allocateLabelRects() {
366
+ const rects = new Array(this.nodes.length);
367
+ for (let i = 0; i < this.nodes.length; i++) {
368
+ rects[i] = { x: 0, y: 0, w: 0, h: 0 };
369
+ }
370
+ return rects;
371
+ }
372
+ /** Create the shared tooltip SVG group (hidden by default). */
373
+ createTooltipElement() {
374
+ const tooltip = document.createElementNS(SVG_NS, "g");
375
+ tooltip.setAttribute("class", "graph-tooltip");
376
+ tooltip.style.display = "none";
377
+ tooltip.style.pointerEvents = "none";
378
+ const tooltipBg = document.createElementNS(SVG_NS, "rect");
379
+ tooltipBg.setAttribute("class", "graph-tooltip-bg");
380
+ tooltipBg.setAttribute("rx", "3");
381
+ tooltipBg.setAttribute("ry", "3");
382
+ tooltip.appendChild(tooltipBg);
383
+ const tooltipText = document.createElementNS(SVG_NS, "text");
384
+ tooltipText.setAttribute("class", "graph-tooltip-text");
385
+ tooltipText.setAttribute("dy", "0.35em");
386
+ tooltip.appendChild(tooltipText);
387
+ return tooltip;
388
+ }
389
+ // ── Private: Selection highlighting ───────────────────────────────────────
390
+ applySelectionHighlight(nodeId) {
391
+ const connectedEdges = this.nodeEdgeMap.get(nodeId) ?? new Set();
392
+ const connectedNodes = new Set([nodeId]);
393
+ for (const ei of connectedEdges) {
394
+ const l = this.resolvedLinks[ei];
395
+ connectedNodes.add(l.source.id);
396
+ connectedNodes.add(l.target.id);
397
+ }
398
+ // Dim non-connected nodes
399
+ for (let j = 0; j < this.nodes.length; j++) {
400
+ const ng = this.nodeGroups[j];
401
+ if (connectedNodes.has(this.nodes[j].id)) {
402
+ ng.style.opacity = "1";
403
+ }
404
+ else {
405
+ ng.style.opacity = "0.2";
406
+ }
407
+ }
408
+ // Highlight connected edges
409
+ for (let j = 0; j < this.linkElements.length; j++) {
410
+ if (connectedEdges.has(j)) {
411
+ this.linkElements[j].style.strokeOpacity = "0.9";
412
+ this.linkElements[j].style.stroke = "var(--accent)";
413
+ }
414
+ else {
415
+ this.linkElements[j].style.strokeOpacity = "0.05";
416
+ }
417
+ }
418
+ }
419
+ // ── Private: Zone hull management ─────────────────────────────────────────
420
+ /** Create SVG elements for each zone hull (background + label). */
421
+ createZoneHulls() {
422
+ const ns = SVG_NS;
423
+ const signal = this.ac.signal;
424
+ for (const zi of this.zoneInfos) {
425
+ const indices = this.zoneNodeIndices.get(zi.id);
426
+ if (!indices || indices.length < 2)
427
+ continue;
428
+ // Zone hull group
429
+ const zoneG = document.createElementNS(ns, "g");
430
+ zoneG.setAttribute("class", "zone-hull-group");
431
+ zoneG.setAttribute("data-zone", zi.id);
432
+ // Hull path (filled background)
433
+ const path = document.createElementNS(ns, "path");
434
+ path.setAttribute("class", "zone-hull");
435
+ path.setAttribute("fill", zi.color);
436
+ path.setAttribute("stroke", zi.color);
437
+ zoneG.appendChild(path);
438
+ this.zoneHullElements.set(zi.id, path);
439
+ // Zone label group — rendered in a separate layer above nodes for readability
440
+ const labelGroup = document.createElementNS(ns, "g");
441
+ labelGroup.setAttribute("class", "zone-hull-label-group");
442
+ labelGroup.setAttribute("data-zone", zi.id);
443
+ const labelBg = document.createElementNS(ns, "rect");
444
+ labelBg.setAttribute("class", "zone-hull-label-bg");
445
+ labelBg.setAttribute("rx", "4");
446
+ labelBg.setAttribute("ry", "4");
447
+ labelGroup.appendChild(labelBg);
448
+ const labelText = document.createElementNS(ns, "text");
449
+ labelText.setAttribute("class", "zone-hull-label");
450
+ labelText.setAttribute("text-anchor", "middle");
451
+ labelText.setAttribute("dy", "0.35em");
452
+ labelText.textContent = zi.name;
453
+ labelGroup.appendChild(labelText);
454
+ this.zoneLabelLayer.appendChild(labelGroup);
455
+ this.zoneLabelElements.set(zi.id, labelGroup);
456
+ this.zoneHullGroup.appendChild(zoneG);
457
+ // Click on zone hull to select/toggle collapse
458
+ zoneG.addEventListener("click", (e) => {
459
+ // Don't trigger if clicking a node inside the hull
460
+ const target = e.target;
461
+ if (target.tagName === "circle")
462
+ return;
463
+ // Suppress zone select if this click completed a pan gesture
464
+ if (this.wasPanning) {
465
+ this.wasPanning = false;
466
+ return;
467
+ }
468
+ e.stopPropagation();
469
+ if (this.onZoneSelect) {
470
+ this.onZoneSelect(zi.id);
471
+ }
472
+ }, { signal });
473
+ // Double-click to toggle collapse
474
+ zoneG.addEventListener("dblclick", (e) => {
475
+ const target = e.target;
476
+ if (target.tagName === "circle")
477
+ return;
478
+ e.preventDefault();
479
+ e.stopPropagation();
480
+ this.toggleZoneCollapse(zi.id);
481
+ }, { signal });
482
+ }
483
+ }
484
+ /** Update hull paths and labels to match current node positions. */
485
+ updateZoneHulls() {
486
+ if (!this.zonesVisible)
487
+ return;
488
+ // Zone label font size scales with zoom (stays readable at all levels)
489
+ const zoneFontSize = Math.max(10, Math.min(14, 12 / Math.sqrt(this.scale)));
490
+ for (const zi of this.zoneInfos) {
491
+ const indices = this.zoneNodeIndices.get(zi.id);
492
+ if (!indices || indices.length < 2)
493
+ continue;
494
+ const path = this.zoneHullElements.get(zi.id);
495
+ const labelGroup = this.zoneLabelElements.get(zi.id);
496
+ if (!path || !labelGroup)
497
+ continue;
498
+ // Skip collapsed zones — hull and label are hidden
499
+ if (this.collapsedZones.has(zi.id)) {
500
+ path.parentElement?.style.setProperty("display", "none");
501
+ labelGroup.style.display = "none";
502
+ continue;
503
+ }
504
+ path.parentElement?.style.removeProperty("display");
505
+ labelGroup.style.display = "";
506
+ // Collect visible node positions with padding
507
+ const points = [];
508
+ let cx = 0, cy = 0;
509
+ for (const idx of indices) {
510
+ const n = this.nodes[idx];
511
+ if (n.x != null && n.y != null) {
512
+ points.push([n.x, n.y]);
513
+ cx += n.x;
514
+ cy += n.y;
515
+ }
516
+ }
517
+ if (points.length < 2)
518
+ continue;
519
+ cx /= points.length;
520
+ cy /= points.length;
521
+ // Compute convex hull with padding
522
+ const padding = 25;
523
+ const hull = convexHull(points);
524
+ const paddedHull = padHull(hull, padding);
525
+ const d = hullToSmoothPath(paddedHull);
526
+ path.setAttribute("d", d);
527
+ // Position zone label above the zone cluster
528
+ const labelY = cy - this.getZoneRadius(indices) - 15;
529
+ const labelText = labelGroup.querySelector("text");
530
+ const labelBg = labelGroup.querySelector("rect");
531
+ if (labelText && labelBg) {
532
+ labelText.style.fontSize = `${zoneFontSize}px`;
533
+ // Position the group at the label center
534
+ labelGroup.setAttribute("transform", `translate(${cx},${labelY})`);
535
+ // Size the background pill around the text
536
+ const textLen = (labelText.textContent || "").length;
537
+ const charWidth = zoneFontSize * 0.55;
538
+ const pillW = textLen * charWidth + 12; // 6px padding each side
539
+ const pillH = zoneFontSize + 8; // 4px padding top/bottom
540
+ labelBg.setAttribute("x", String(-pillW / 2));
541
+ labelBg.setAttribute("y", String(-pillH / 2));
542
+ labelBg.setAttribute("width", String(pillW));
543
+ labelBg.setAttribute("height", String(pillH));
544
+ }
545
+ }
546
+ }
547
+ /** Get approximate radius of a zone cluster for label positioning. */
548
+ getZoneRadius(indices) {
549
+ if (indices.length === 0)
550
+ return 0;
551
+ let cx = 0, cy = 0;
552
+ for (const i of indices) {
553
+ cx += this.nodes[i].x ?? 0;
554
+ cy += this.nodes[i].y ?? 0;
555
+ }
556
+ cx /= indices.length;
557
+ cy /= indices.length;
558
+ let maxDist = 0;
559
+ for (const i of indices) {
560
+ const dx = (this.nodes[i].x ?? 0) - cx;
561
+ const dy = (this.nodes[i].y ?? 0) - cy;
562
+ maxDist = Math.max(maxDist, Math.sqrt(dx * dx + dy * dy));
563
+ }
564
+ return maxDist;
565
+ }
566
+ /** Hide member nodes/edges when a zone is collapsed, show summary. */
567
+ applyZoneCollapse(zoneId) {
568
+ const indices = this.zoneNodeIndices.get(zoneId);
569
+ if (!indices)
570
+ return;
571
+ // Compute centroid for summary position
572
+ let cx = 0, cy = 0;
573
+ for (const i of indices) {
574
+ cx += this.nodes[i].x ?? 0;
575
+ cy += this.nodes[i].y ?? 0;
576
+ }
577
+ cx /= indices.length;
578
+ cy /= indices.length;
579
+ // Hide individual nodes
580
+ for (const i of indices) {
581
+ this.nodeGroups[i].style.display = "none";
582
+ }
583
+ // Dim edges connected only to hidden nodes
584
+ const hiddenIds = new Set(indices.map(i => this.nodes[i].id));
585
+ for (let i = 0; i < this.resolvedLinks.length; i++) {
586
+ const l = this.resolvedLinks[i];
587
+ const srcHidden = hiddenIds.has(l.source.id);
588
+ const tgtHidden = hiddenIds.has(l.target.id);
589
+ if (srcHidden && tgtHidden) {
590
+ this.linkElements[i].style.display = "none";
591
+ }
592
+ else if (srcHidden || tgtHidden) {
593
+ this.linkElements[i].style.opacity = "0.3";
594
+ }
595
+ }
596
+ // Show collapsed summary hull
597
+ const hullGroup = this.zoneHullGroup.querySelector(`[data-zone="${zoneId}"]`);
598
+ if (hullGroup) {
599
+ hullGroup.classList.add("collapsed");
600
+ // Position a summary badge at the centroid
601
+ const ns = SVG_NS;
602
+ let badge = hullGroup.querySelector(".zone-collapse-badge");
603
+ if (!badge) {
604
+ badge = document.createElementNS(ns, "g");
605
+ badge.setAttribute("class", "zone-collapse-badge");
606
+ const circle = document.createElementNS(ns, "circle");
607
+ circle.setAttribute("r", "18");
608
+ circle.setAttribute("class", "zone-collapse-circle");
609
+ const info = this.zoneInfos.find(z => z.id === zoneId);
610
+ if (info)
611
+ circle.setAttribute("fill", info.color);
612
+ badge.appendChild(circle);
613
+ const text = document.createElementNS(ns, "text");
614
+ text.setAttribute("class", "zone-collapse-count");
615
+ text.setAttribute("text-anchor", "middle");
616
+ text.setAttribute("dy", "0.35em");
617
+ text.textContent = String(indices.length);
618
+ badge.appendChild(text);
619
+ hullGroup.appendChild(badge);
620
+ }
621
+ badge.setAttribute("transform", `translate(${cx},${cy})`);
622
+ badge.style.display = "";
623
+ }
624
+ }
625
+ /** Show member nodes/edges when a zone is expanded. */
626
+ applyZoneExpand(zoneId) {
627
+ const indices = this.zoneNodeIndices.get(zoneId);
628
+ if (!indices)
629
+ return;
630
+ // Show individual nodes
631
+ for (const i of indices) {
632
+ this.nodeGroups[i].style.display = "";
633
+ }
634
+ // Restore edges
635
+ const hiddenIds = new Set(indices.map(i => this.nodes[i].id));
636
+ for (let i = 0; i < this.resolvedLinks.length; i++) {
637
+ const l = this.resolvedLinks[i];
638
+ const srcWas = hiddenIds.has(l.source.id);
639
+ const tgtWas = hiddenIds.has(l.target.id);
640
+ if (srcWas || tgtWas) {
641
+ this.linkElements[i].style.display = "";
642
+ this.linkElements[i].style.opacity = "";
643
+ }
644
+ }
645
+ // Hide collapsed summary
646
+ const hullGroup = this.zoneHullGroup.querySelector(`[data-zone="${zoneId}"]`);
647
+ if (hullGroup) {
648
+ hullGroup.classList.remove("collapsed");
649
+ const badge = hullGroup.querySelector(".zone-collapse-badge");
650
+ if (badge)
651
+ badge.style.display = "none";
652
+ }
653
+ }
654
+ // ── Private: ViewBox ───────────────────────────────────────────────────────
655
+ updateViewBox() {
656
+ this.svg.setAttribute("viewBox", `${this.viewX} ${this.viewY} ${this.viewW} ${this.viewH}`);
657
+ this.scale = this.viewW / this.width;
658
+ }
659
+ fitToContent() {
660
+ if (this.nodes.length === 0)
661
+ return;
662
+ const xs = this.nodes.map((n) => n.x).sort((a, b) => a - b);
663
+ const ys = this.nodes.map((n) => n.y).sort((a, b) => a - b);
664
+ const lo = Math.floor(this.nodes.length * 0.02);
665
+ const hi = Math.min(Math.ceil(this.nodes.length * 0.98), this.nodes.length - 1);
666
+ const minX = xs[lo];
667
+ const maxX = xs[hi];
668
+ const minY = ys[lo];
669
+ const maxY = ys[hi];
670
+ const padding = 60;
671
+ let fitW = (maxX - minX) + padding * 2;
672
+ let fitH = (maxY - minY) + padding * 2;
673
+ let fitX = minX - padding;
674
+ let fitY = minY - padding;
675
+ // Maintain aspect ratio of container
676
+ const aspect = this.width / this.height;
677
+ const contentAspect = fitW / fitH;
678
+ if (contentAspect > aspect) {
679
+ const newH = fitW / aspect;
680
+ fitY -= (newH - fitH) / 2;
681
+ fitH = newH;
682
+ }
683
+ else {
684
+ const newW = fitH * aspect;
685
+ fitX -= (newW - fitW) / 2;
686
+ fitW = newW;
687
+ }
688
+ this.viewX = fitX;
689
+ this.viewY = fitY;
690
+ this.viewW = fitW;
691
+ this.viewH = fitH;
692
+ this.scale = this.viewW / this.width;
693
+ this.updateViewBox();
694
+ }
695
+ // ── Private: LOD + Label overlap detection ─────────────────────────────────
696
+ /**
697
+ * Update level-of-detail: circle sizing, label visibility, and overlap hiding.
698
+ *
699
+ * Strategy:
700
+ * 1. Zoom-based LOD: hide labels when nodes are too small to read.
701
+ * 2. Density-aware priority: in crowded areas, only show labels for high-import
702
+ * nodes (they are the most useful landmarks).
703
+ * 3. Greedy overlap removal: iterate nodes by importance (import count desc),
704
+ * place labels greedily, hide labels that would overlap already-placed ones.
705
+ * 4. User toggle: labelsHidden overrides everything.
706
+ */
707
+ updateLOD() {
708
+ const n = this.nodes.length;
709
+ // Phase 1: circle sizing + compute which labels *could* show (zoom-based)
710
+ const fontSize = Math.max(7, Math.min(11, 9 / Math.sqrt(this.scale)));
711
+ // Approximate label dimensions in SVG units
712
+ const charWidth = fontSize * 0.55;
713
+ const labelHeight = fontSize * 1.3;
714
+ // Track which labels pass zoom test (before overlap)
715
+ const zoomVisible = new Array(n);
716
+ for (let i = 0; i < n; i++) {
717
+ const visualRadius = this.nodeRadii[i] / this.scale;
718
+ // Select visible circle (skip the hit target which has class .graph-node-hit)
719
+ const circle = this.nodeGroups[i].querySelector("circle:not(.graph-node-hit)");
720
+ this.nodeGroups[i].style.display = "";
721
+ if (circle && visualRadius < 1) {
722
+ circle.setAttribute("r", String(this.scale));
723
+ }
724
+ else if (circle) {
725
+ circle.setAttribute("r", String(this.nodeRadii[i]));
726
+ }
727
+ // Also update hit target radius to stay proportional
728
+ const hitTarget = this.nodeGroups[i].querySelector(".graph-node-hit");
729
+ if (hitTarget) {
730
+ const hitRadius = Math.max(this.nodeRadii[i] + 4, 10);
731
+ hitTarget.setAttribute("r", String(visualRadius < 1 ? this.scale + 4 : hitRadius));
732
+ }
733
+ zoomVisible[i] = !this.labelsHidden && visualRadius >= 3;
734
+ }
735
+ // Phase 2: sort by importance for greedy placement (descending import count)
736
+ // Use a lightweight index array to avoid allocations on hot path
737
+ const order = this.getSortedLabelOrder();
738
+ // Phase 3: greedy overlap removal
739
+ // We track placed label bounding boxes and skip labels that overlap.
740
+ // Zone labels are pre-seeded as reserved regions so file labels never cover them.
741
+ const placed = [];
742
+ const showLabel = new Array(n).fill(false);
743
+ // Pre-seed zone label bounding boxes as reserved regions
744
+ if (this.zonesVisible) {
745
+ const zoneFontSize = Math.max(10, Math.min(14, 12 / Math.sqrt(this.scale)));
746
+ const zoneCharWidth = zoneFontSize * 0.55;
747
+ for (const zi of this.zoneInfos) {
748
+ const indices = this.zoneNodeIndices.get(zi.id);
749
+ if (!indices || indices.length < 2)
750
+ continue;
751
+ if (this.collapsedZones.has(zi.id))
752
+ continue;
753
+ const labelGroup = this.zoneLabelElements.get(zi.id);
754
+ if (!labelGroup || labelGroup.style.display === "none")
755
+ continue;
756
+ const labelText = labelGroup.querySelector("text");
757
+ if (!labelText)
758
+ continue;
759
+ const textLen = (labelText.textContent || "").length;
760
+ const pillW = textLen * zoneCharWidth + 12;
761
+ const pillH = zoneFontSize + 8;
762
+ // Extract position from transform attribute
763
+ const transform = labelGroup.getAttribute("transform");
764
+ const match = transform?.match(/translate\(([-\d.]+),([-\d.]+)\)/);
765
+ if (match) {
766
+ const zx = parseFloat(match[1]);
767
+ const zy = parseFloat(match[2]);
768
+ placed.push({ x: zx - pillW / 2, y: zy - pillH / 2, w: pillW, h: pillH });
769
+ }
770
+ }
771
+ }
772
+ for (let oi = 0; oi < n; oi++) {
773
+ const i = order[oi];
774
+ if (!zoomVisible[i])
775
+ continue;
776
+ const node = this.nodes[i];
777
+ const label = this.nodeGroups[i].querySelector("text");
778
+ if (!label)
779
+ continue;
780
+ const textLen = (label.textContent || "").length;
781
+ const lw = textLen * charWidth;
782
+ const lh = labelHeight;
783
+ const lx = (node.x ?? 0) - lw / 2;
784
+ const ly = (node.y ?? 0) - this.nodeRadii[i] - 3 - lh;
785
+ // Check overlap against already-placed labels and reserved zone labels
786
+ let overlaps = false;
787
+ for (let j = 0; j < placed.length; j++) {
788
+ const p = placed[j];
789
+ if (lx < p.x + p.w && lx + lw > p.x && ly < p.y + p.h && ly + lh > p.y) {
790
+ overlaps = true;
791
+ break;
792
+ }
793
+ }
794
+ if (!overlaps) {
795
+ showLabel[i] = true;
796
+ // Reuse pre-allocated rect
797
+ const rect = this.labelRects[i];
798
+ rect.x = lx;
799
+ rect.y = ly;
800
+ rect.w = lw;
801
+ rect.h = lh;
802
+ placed.push(rect);
803
+ }
804
+ }
805
+ // Phase 4: apply visibility + font size
806
+ for (let i = 0; i < n; i++) {
807
+ const label = this.nodeGroups[i].querySelector("text");
808
+ if (!label)
809
+ continue;
810
+ if (showLabel[i]) {
811
+ label.style.display = "";
812
+ label.style.fontSize = `${fontSize}px`;
813
+ label.classList.remove("graph-label-hidden");
814
+ }
815
+ else {
816
+ label.style.display = "none";
817
+ label.classList.add("graph-label-hidden");
818
+ }
819
+ }
820
+ }
821
+ /** Cached sort order — recomputed only when node count changes. */
822
+ _sortedOrder = null;
823
+ _sortedOrderLen = -1;
824
+ /** Get indices sorted by import count (descending). Cached per node-count. */
825
+ getSortedLabelOrder() {
826
+ const n = this.nodes.length;
827
+ if (this._sortedOrder && this._sortedOrderLen === n)
828
+ return this._sortedOrder;
829
+ const order = new Array(n);
830
+ for (let i = 0; i < n; i++)
831
+ order[i] = i;
832
+ const nodes = this.nodes;
833
+ order.sort((a, b) => nodes[b].importCount - nodes[a].importCount);
834
+ this._sortedOrder = order;
835
+ this._sortedOrderLen = n;
836
+ return order;
837
+ }
838
+ // ── Private: DOM update (called by physics tick) ───────────────────────────
839
+ updateDOM() {
840
+ // Update link positions
841
+ for (let i = 0; i < this.resolvedLinks.length; i++) {
842
+ const l = this.resolvedLinks[i];
843
+ this.linkElements[i].setAttribute("x1", String(l.source.x));
844
+ this.linkElements[i].setAttribute("y1", String(l.source.y));
845
+ this.linkElements[i].setAttribute("x2", String(l.target.x));
846
+ this.linkElements[i].setAttribute("y2", String(l.target.y));
847
+ }
848
+ // Update node positions and LOD
849
+ for (let i = 0; i < this.nodes.length; i++) {
850
+ this.nodeGroups[i].setAttribute("transform", `translate(${this.nodes[i].x},${this.nodes[i].y})`);
851
+ }
852
+ this.updateLOD();
853
+ // Update zone hull boundaries
854
+ this.updateZoneHulls();
855
+ }
856
+ // ── Private: Simulation ────────────────────────────────────────────────────
857
+ tickCallbacks() {
858
+ return {
859
+ updateDOM: () => this.updateDOM(),
860
+ fitToContent: () => this.fitToContent(),
861
+ scheduleNextTick: (fn) => { if (!this.destroyed)
862
+ requestAnimationFrame(fn); },
863
+ };
864
+ }
865
+ startSimulation() {
866
+ if (this.destroyed)
867
+ return;
868
+ const runTick = () => { if (!this.destroyed)
869
+ tick(this.sim, this.tickCallbacks()); };
870
+ requestAnimationFrame(runTick);
871
+ }
872
+ /** Re-heat the simulation to adapt to moved nodes. */
873
+ reheat() {
874
+ if (this.destroyed)
875
+ return;
876
+ if (this.sim.alpha.value < 0.01) {
877
+ this.sim.alpha.value = 0.3;
878
+ requestAnimationFrame(() => { if (!this.destroyed)
879
+ tick(this.sim, this.tickCallbacks()); });
880
+ }
881
+ else {
882
+ this.sim.alpha.value = Math.max(this.sim.alpha.value, 0.3);
883
+ }
884
+ }
885
+ // ── Private: Coordinate helpers ──────────────────────────────────────────
886
+ /** Convert client (screen) coordinates to SVG viewBox coordinates. */
887
+ clientToViewBox(clientX, clientY) {
888
+ const rect = this.svg.getBoundingClientRect();
889
+ return {
890
+ x: this.viewX + ((clientX - rect.left) / rect.width) * this.viewW,
891
+ y: this.viewY + ((clientY - rect.top) / rect.height) * this.viewH,
892
+ };
893
+ }
894
+ /** Find the node index under a given SVG element by walking up the DOM tree. */
895
+ findNodeIndex(target) {
896
+ let el = target;
897
+ // Walk up until we find a .graph-node group or leave the SVG
898
+ while (el && el !== this.svg) {
899
+ if (el.classList?.contains("graph-node")) {
900
+ return this.nodeGroups.indexOf(el);
901
+ }
902
+ // Direct circle/text child of a node group (fast path)
903
+ if ((el.tagName === "circle" || el.tagName === "text") && el.parentElement?.classList?.contains("graph-node")) {
904
+ return this.nodeGroups.indexOf(el.parentElement);
905
+ }
906
+ el = el.parentElement;
907
+ }
908
+ return -1;
909
+ }
910
+ // ── Private: Zoom ──────────────────────────────────────────────────────────
911
+ /** Apply a zoom factor centered on the current viewport center. */
912
+ applyZoomFromCenter(factor) {
913
+ const cx = this.viewX + this.viewW / 2;
914
+ const cy = this.viewY + this.viewH / 2;
915
+ const newW = this.viewW * factor;
916
+ const newH = this.viewH * factor;
917
+ this.viewX = cx - newW / 2;
918
+ this.viewY = cy - newH / 2;
919
+ this.viewW = newW;
920
+ this.viewH = newH;
921
+ this.scale = this.viewW / this.width;
922
+ this.updateViewBox();
923
+ this.updateLOD();
924
+ }
925
+ setupZoom() {
926
+ this.svg.addEventListener("wheel", (e) => {
927
+ e.preventDefault();
928
+ const mouseVB = this.clientToViewBox(e.clientX, e.clientY);
929
+ const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9;
930
+ const newW = this.viewW * zoomFactor;
931
+ const newH = this.viewH * zoomFactor;
932
+ this.viewX = mouseVB.x - (mouseVB.x - this.viewX) * (newW / this.viewW);
933
+ this.viewY = mouseVB.y - (mouseVB.y - this.viewY) * (newH / this.viewH);
934
+ this.viewW = newW;
935
+ this.viewH = newH;
936
+ this.scale = this.viewW / this.width;
937
+ this.updateViewBox();
938
+ this.updateLOD();
939
+ }, { passive: false, signal: this.ac.signal });
940
+ }
941
+ // ── Private: Pan + Node drag (mouse) ───────────────────────────────────────
942
+ setupPanAndDrag(onNodeSelect, onNodeDblClick) {
943
+ let isPanning = false;
944
+ let panStartX = 0, panStartY = 0;
945
+ let panStartVX = 0, panStartVY = 0;
946
+ let dragNode = null;
947
+ let dragNodeIdx = -1;
948
+ let mouseDownPos = null;
949
+ let isDragging = false;
950
+ const DRAG_THRESHOLD = 3;
951
+ const signal = this.ac.signal;
952
+ // Double-click handler
953
+ this.svg.addEventListener("dblclick", (e) => {
954
+ const target = e.target;
955
+ const idx = this.findNodeIndex(target);
956
+ if (idx >= 0 && onNodeDblClick) {
957
+ e.preventDefault();
958
+ onNodeDblClick(this.nodes[idx].id);
959
+ }
960
+ }, { signal });
961
+ this.svg.addEventListener("mousedown", (e) => {
962
+ // Only handle primary (left) button
963
+ if (e.button !== 0)
964
+ return;
965
+ // Reset pan gesture flag at the start of each interaction
966
+ this.wasPanning = false;
967
+ const target = e.target;
968
+ const idx = this.findNodeIndex(target);
969
+ if (idx >= 0) {
970
+ // Prevent browser text selection / native drag during node drag
971
+ e.preventDefault();
972
+ dragNode = this.nodes[idx];
973
+ dragNodeIdx = idx;
974
+ mouseDownPos = { x: e.clientX, y: e.clientY };
975
+ isDragging = false;
976
+ return;
977
+ }
978
+ // Background click clears selection
979
+ if (target === this.svg || target === this.g) {
980
+ this.clearSelection();
981
+ }
982
+ // Prevent browser text selection / native drag during pan
983
+ e.preventDefault();
984
+ // Background pan
985
+ isPanning = true;
986
+ panStartX = e.clientX;
987
+ panStartY = e.clientY;
988
+ panStartVX = this.viewX;
989
+ panStartVY = this.viewY;
990
+ this.svg.classList.add("grabbing");
991
+ }, { signal });
992
+ // mousemove and mouseup are attached to window so dragging/panning
993
+ // continues even when the pointer leaves the SVG boundary.
994
+ const onMouseMove = (e) => {
995
+ // Node drag
996
+ if (dragNode && mouseDownPos) {
997
+ const dx = e.clientX - mouseDownPos.x;
998
+ const dy = e.clientY - mouseDownPos.y;
999
+ if (!isDragging) {
1000
+ if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
1001
+ isDragging = true;
1002
+ dragNode.fx = dragNode.x;
1003
+ dragNode.fy = dragNode.y;
1004
+ // Visual feedback for dragging
1005
+ this.svg.classList.add("node-dragging");
1006
+ if (dragNodeIdx >= 0)
1007
+ this.nodeGroups[dragNodeIdx].classList.add("dragging");
1008
+ this.reheat();
1009
+ }
1010
+ return;
1011
+ }
1012
+ // Convert client coords to viewBox coords
1013
+ const vb = this.clientToViewBox(e.clientX, e.clientY);
1014
+ dragNode.fx = vb.x;
1015
+ dragNode.fy = vb.y;
1016
+ return;
1017
+ }
1018
+ // Background pan
1019
+ if (isPanning) {
1020
+ const dx = e.clientX - panStartX;
1021
+ const dy = e.clientY - panStartY;
1022
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
1023
+ this.wasPanning = true;
1024
+ }
1025
+ const rect = this.svg.getBoundingClientRect();
1026
+ this.viewX = panStartVX - (dx / rect.width) * this.viewW;
1027
+ this.viewY = panStartVY - (dy / rect.height) * this.viewH;
1028
+ this.updateViewBox();
1029
+ }
1030
+ };
1031
+ const onMouseUp = () => {
1032
+ if (dragNode) {
1033
+ if (!isDragging && dragNodeIdx >= 0) {
1034
+ // Click (no drag) — select node and show details
1035
+ const n = this.nodes[dragNodeIdx];
1036
+ const fileName = basename(n.id);
1037
+ this.selectNode(n.id);
1038
+ onNodeSelect({
1039
+ title: fileName,
1040
+ path: n.id,
1041
+ zone: n.zone || "unzoned",
1042
+ incomingImports: n.importCount,
1043
+ });
1044
+ }
1045
+ else if (isDragging) {
1046
+ // Drag ended — persist position for the session by updating x/y
1047
+ // and then release the fixed constraint
1048
+ dragNode.x = dragNode.fx;
1049
+ dragNode.y = dragNode.fy;
1050
+ dragNode.vx = 0;
1051
+ dragNode.vy = 0;
1052
+ }
1053
+ // Clear drag visual feedback
1054
+ this.svg.classList.remove("node-dragging");
1055
+ if (dragNodeIdx >= 0)
1056
+ this.nodeGroups[dragNodeIdx].classList.remove("dragging");
1057
+ dragNode.fx = null;
1058
+ dragNode.fy = null;
1059
+ dragNode = null;
1060
+ dragNodeIdx = -1;
1061
+ mouseDownPos = null;
1062
+ isDragging = false;
1063
+ }
1064
+ if (isPanning) {
1065
+ isPanning = false;
1066
+ this.svg.classList.remove("grabbing");
1067
+ }
1068
+ };
1069
+ window.addEventListener("mousemove", onMouseMove, { signal });
1070
+ window.addEventListener("mouseup", onMouseUp, { signal });
1071
+ }
1072
+ // ── Private: Touch interaction ─────────────────────────────────────────────
1073
+ setupTouchInteraction(onNodeSelect, onNodeDblClick) {
1074
+ let touchDragNode = null;
1075
+ let touchDragIdx = -1;
1076
+ let touchStartPos = null;
1077
+ let isTouchDragging = false;
1078
+ const DRAG_THRESHOLD = 8; // higher threshold for touch
1079
+ // Touch pan state
1080
+ let isTouchPanning = false;
1081
+ let touchPanStartX = 0, touchPanStartY = 0;
1082
+ let touchPanStartVX = 0, touchPanStartVY = 0;
1083
+ // Double-tap detection
1084
+ let lastTapTime = 0;
1085
+ let lastTapNodeIdx = -1;
1086
+ const DOUBLE_TAP_DELAY = 300;
1087
+ // Pinch-zoom state
1088
+ let pinchStartDist = 0;
1089
+ let pinchStartViewW = 0;
1090
+ let pinchStartViewH = 0;
1091
+ let pinchMidX = 0;
1092
+ let pinchMidY = 0;
1093
+ const signal = this.ac.signal;
1094
+ this.svg.addEventListener("touchstart", (e) => {
1095
+ // Pinch-zoom: two-finger gesture
1096
+ if (e.touches.length === 2) {
1097
+ e.preventDefault();
1098
+ isTouchPanning = false;
1099
+ touchDragNode = null;
1100
+ const t0 = e.touches[0];
1101
+ const t1 = e.touches[1];
1102
+ pinchStartDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
1103
+ pinchStartViewW = this.viewW;
1104
+ pinchStartViewH = this.viewH;
1105
+ const midX = (t0.clientX + t1.clientX) / 2;
1106
+ const midY = (t0.clientY + t1.clientY) / 2;
1107
+ const vb = this.clientToViewBox(midX, midY);
1108
+ pinchMidX = vb.x;
1109
+ pinchMidY = vb.y;
1110
+ return;
1111
+ }
1112
+ if (e.touches.length !== 1)
1113
+ return;
1114
+ const touch = e.touches[0];
1115
+ const target = touch.target;
1116
+ const idx = this.findNodeIndex(target);
1117
+ if (idx >= 0) {
1118
+ e.preventDefault();
1119
+ touchDragNode = this.nodes[idx];
1120
+ touchDragIdx = idx;
1121
+ touchStartPos = { x: touch.clientX, y: touch.clientY };
1122
+ isTouchDragging = false;
1123
+ return;
1124
+ }
1125
+ // Background touch pan
1126
+ isTouchPanning = true;
1127
+ touchPanStartX = touch.clientX;
1128
+ touchPanStartY = touch.clientY;
1129
+ touchPanStartVX = this.viewX;
1130
+ touchPanStartVY = this.viewY;
1131
+ }, { passive: false, signal });
1132
+ this.svg.addEventListener("touchmove", (e) => {
1133
+ // Pinch-zoom
1134
+ if (e.touches.length === 2) {
1135
+ e.preventDefault();
1136
+ const t0 = e.touches[0];
1137
+ const t1 = e.touches[1];
1138
+ const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
1139
+ const ratio = pinchStartDist / dist;
1140
+ const newW = pinchStartViewW * ratio;
1141
+ const newH = pinchStartViewH * ratio;
1142
+ this.viewX = pinchMidX - (pinchMidX - this.viewX) * (newW / this.viewW);
1143
+ this.viewY = pinchMidY - (pinchMidY - this.viewY) * (newH / this.viewH);
1144
+ this.viewW = newW;
1145
+ this.viewH = newH;
1146
+ this.scale = this.viewW / this.width;
1147
+ this.updateViewBox();
1148
+ this.updateLOD();
1149
+ return;
1150
+ }
1151
+ if (e.touches.length !== 1)
1152
+ return;
1153
+ const touch = e.touches[0];
1154
+ // Node drag
1155
+ if (touchDragNode && touchStartPos) {
1156
+ const dx = touch.clientX - touchStartPos.x;
1157
+ const dy = touch.clientY - touchStartPos.y;
1158
+ if (!isTouchDragging) {
1159
+ if (Math.hypot(dx, dy) > DRAG_THRESHOLD) {
1160
+ isTouchDragging = true;
1161
+ touchDragNode.fx = touchDragNode.x;
1162
+ touchDragNode.fy = touchDragNode.y;
1163
+ // Visual feedback for touch dragging
1164
+ this.svg.classList.add("node-dragging");
1165
+ if (touchDragIdx >= 0)
1166
+ this.nodeGroups[touchDragIdx].classList.add("dragging");
1167
+ this.reheat();
1168
+ }
1169
+ return;
1170
+ }
1171
+ e.preventDefault();
1172
+ const vb = this.clientToViewBox(touch.clientX, touch.clientY);
1173
+ touchDragNode.fx = vb.x;
1174
+ touchDragNode.fy = vb.y;
1175
+ return;
1176
+ }
1177
+ // Background pan
1178
+ if (isTouchPanning) {
1179
+ const dx = touch.clientX - touchPanStartX;
1180
+ const dy = touch.clientY - touchPanStartY;
1181
+ const rect = this.svg.getBoundingClientRect();
1182
+ this.viewX = touchPanStartVX - (dx / rect.width) * this.viewW;
1183
+ this.viewY = touchPanStartVY - (dy / rect.height) * this.viewH;
1184
+ this.updateViewBox();
1185
+ }
1186
+ }, { passive: false, signal });
1187
+ this.svg.addEventListener("touchend", (e) => {
1188
+ if (touchDragNode) {
1189
+ if (!isTouchDragging && touchDragIdx >= 0) {
1190
+ const n = this.nodes[touchDragIdx];
1191
+ const now = Date.now();
1192
+ // Double-tap detection
1193
+ if (now - lastTapTime < DOUBLE_TAP_DELAY && lastTapNodeIdx === touchDragIdx) {
1194
+ if (onNodeDblClick)
1195
+ onNodeDblClick(n.id);
1196
+ lastTapTime = 0;
1197
+ lastTapNodeIdx = -1;
1198
+ }
1199
+ else {
1200
+ // Single tap — select node
1201
+ lastTapTime = now;
1202
+ lastTapNodeIdx = touchDragIdx;
1203
+ const fileName = basename(n.id);
1204
+ this.selectNode(n.id);
1205
+ onNodeSelect({
1206
+ title: fileName,
1207
+ path: n.id,
1208
+ zone: n.zone || "unzoned",
1209
+ incomingImports: n.importCount,
1210
+ });
1211
+ }
1212
+ }
1213
+ else if (isTouchDragging && touchDragNode) {
1214
+ // Drag ended — persist position
1215
+ touchDragNode.x = touchDragNode.fx;
1216
+ touchDragNode.y = touchDragNode.fy;
1217
+ touchDragNode.vx = 0;
1218
+ touchDragNode.vy = 0;
1219
+ }
1220
+ // Clear drag visual feedback
1221
+ this.svg.classList.remove("node-dragging");
1222
+ if (touchDragIdx >= 0)
1223
+ this.nodeGroups[touchDragIdx].classList.remove("dragging");
1224
+ if (touchDragNode) {
1225
+ touchDragNode.fx = null;
1226
+ touchDragNode.fy = null;
1227
+ }
1228
+ touchDragNode = null;
1229
+ touchDragIdx = -1;
1230
+ touchStartPos = null;
1231
+ isTouchDragging = false;
1232
+ }
1233
+ // Clear touch pan on last finger lift
1234
+ if (e.touches.length === 0) {
1235
+ isTouchPanning = false;
1236
+ }
1237
+ }, { signal });
1238
+ this.svg.addEventListener("touchcancel", () => {
1239
+ this.svg.classList.remove("node-dragging");
1240
+ if (touchDragIdx >= 0)
1241
+ this.nodeGroups[touchDragIdx].classList.remove("dragging");
1242
+ if (touchDragNode) {
1243
+ touchDragNode.fx = null;
1244
+ touchDragNode.fy = null;
1245
+ touchDragNode = null;
1246
+ }
1247
+ touchDragIdx = -1;
1248
+ touchStartPos = null;
1249
+ isTouchDragging = false;
1250
+ isTouchPanning = false;
1251
+ }, { signal });
1252
+ }
1253
+ // ── Private: Label tooltips ──────────────────────────────────────────────
1254
+ /**
1255
+ * Show full filename tooltip on node hover. The tooltip is a single shared
1256
+ * SVG group that follows the hovered node. This avoids per-node <title>
1257
+ * elements (which have inconsistent browser rendering and delays).
1258
+ */
1259
+ setupLabelTooltips() {
1260
+ const signal = this.ac.signal;
1261
+ const tooltipText = this.tooltip.querySelector("text");
1262
+ const tooltipBg = this.tooltip.querySelector("rect");
1263
+ for (let i = 0; i < this.nodes.length; i++) {
1264
+ this.nodeGroups[i].addEventListener("mouseenter", () => {
1265
+ const node = this.nodes[i];
1266
+ const fullName = basename(node.id);
1267
+ const displayName = this.nodeGroups[i].querySelector("text")?.textContent || "";
1268
+ // Only show tooltip if the label is truncated or hidden
1269
+ const label = this.nodeGroups[i].querySelector("text");
1270
+ const labelHidden = label?.style.display === "none";
1271
+ if (fullName === displayName && !labelHidden) {
1272
+ return;
1273
+ }
1274
+ tooltipText.textContent = fullName;
1275
+ // Position tooltip above node
1276
+ const tx = node.x ?? 0;
1277
+ const ty = (node.y ?? 0) - this.nodeRadii[i] - 18;
1278
+ this.tooltip.setAttribute("transform", `translate(${tx},${ty})`);
1279
+ // Size the background rectangle around the text
1280
+ const textLen = fullName.length;
1281
+ const approxWidth = textLen * (9 * 0.55) + 8; // 9px font, 0.55 char width, 4px padding each side
1282
+ tooltipBg.setAttribute("x", String(-approxWidth / 2));
1283
+ tooltipBg.setAttribute("y", "-8");
1284
+ tooltipBg.setAttribute("width", String(approxWidth));
1285
+ tooltipBg.setAttribute("height", "16");
1286
+ tooltipText.setAttribute("text-anchor", "middle");
1287
+ this.tooltip.style.display = "";
1288
+ }, { signal });
1289
+ this.nodeGroups[i].addEventListener("mouseleave", () => {
1290
+ this.tooltip.style.display = "none";
1291
+ }, { signal });
1292
+ }
1293
+ }
1294
+ // ── Private: Hover highlighting ────────────────────────────────────────────
1295
+ setupHoverHighlighting() {
1296
+ let hoveredNode = null;
1297
+ const signal = this.ac.signal;
1298
+ for (let i = 0; i < this.nodes.length; i++) {
1299
+ const nodeId = this.nodes[i].id;
1300
+ this.nodeGroups[i].addEventListener("mouseenter", () => {
1301
+ // Don't override persistent selection with hover
1302
+ if (this.selectedNodeId)
1303
+ return;
1304
+ if (hoveredNode === nodeId)
1305
+ return;
1306
+ hoveredNode = nodeId;
1307
+ const connectedEdges = this.nodeEdgeMap.get(nodeId) ?? new Set();
1308
+ const connectedNodes = new Set([nodeId]);
1309
+ for (const ei of connectedEdges) {
1310
+ const l = this.resolvedLinks[ei];
1311
+ connectedNodes.add(l.source.id);
1312
+ connectedNodes.add(l.target.id);
1313
+ }
1314
+ // Dim non-connected
1315
+ for (let j = 0; j < this.nodes.length; j++) {
1316
+ const ng = this.nodeGroups[j];
1317
+ if (connectedNodes.has(this.nodes[j].id)) {
1318
+ ng.style.opacity = "1";
1319
+ }
1320
+ else {
1321
+ ng.style.opacity = "0.2";
1322
+ }
1323
+ }
1324
+ for (let j = 0; j < this.linkElements.length; j++) {
1325
+ if (connectedEdges.has(j)) {
1326
+ this.linkElements[j].style.strokeOpacity = "0.9";
1327
+ this.linkElements[j].style.stroke = "var(--accent)";
1328
+ }
1329
+ else {
1330
+ this.linkElements[j].style.strokeOpacity = "0.05";
1331
+ }
1332
+ }
1333
+ }, { signal });
1334
+ this.nodeGroups[i].addEventListener("mouseleave", () => {
1335
+ // Don't clear if a node is persistently selected
1336
+ if (this.selectedNodeId)
1337
+ return;
1338
+ if (hoveredNode !== nodeId)
1339
+ return;
1340
+ hoveredNode = null;
1341
+ for (let j = 0; j < this.nodes.length; j++) {
1342
+ this.nodeGroups[j].style.opacity = "";
1343
+ }
1344
+ for (let j = 0; j < this.linkElements.length; j++) {
1345
+ this.linkElements[j].style.strokeOpacity = "";
1346
+ this.linkElements[j].style.stroke = "";
1347
+ }
1348
+ }, { signal });
1349
+ }
1350
+ }
1351
+ }
1352
+ // ── Geometry helpers (module-level, no DOM) ──────────────────────────────────
1353
+ /**
1354
+ * Compute convex hull of a set of 2D points using Andrew's monotone chain.
1355
+ * Returns points in counter-clockwise order. O(n log n).
1356
+ */
1357
+ function convexHull(points) {
1358
+ if (points.length <= 2)
1359
+ return [...points];
1360
+ const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
1361
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
1362
+ const lower = [];
1363
+ for (const p of sorted) {
1364
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
1365
+ lower.pop();
1366
+ lower.push(p);
1367
+ }
1368
+ const upper = [];
1369
+ for (let i = sorted.length - 1; i >= 0; i--) {
1370
+ const p = sorted[i];
1371
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
1372
+ upper.pop();
1373
+ upper.push(p);
1374
+ }
1375
+ // Remove last point of each half (duplicate of first point of other half)
1376
+ lower.pop();
1377
+ upper.pop();
1378
+ return lower.concat(upper);
1379
+ }
1380
+ /** Expand a convex hull outward by a padding distance. */
1381
+ function padHull(hull, padding) {
1382
+ if (hull.length < 3) {
1383
+ // For degenerate cases, expand bounding box
1384
+ const xs = hull.map(p => p[0]);
1385
+ const ys = hull.map(p => p[1]);
1386
+ const minX = Math.min(...xs) - padding;
1387
+ const maxX = Math.max(...xs) + padding;
1388
+ const minY = Math.min(...ys) - padding;
1389
+ const maxY = Math.max(...ys) + padding;
1390
+ return [[minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY]];
1391
+ }
1392
+ const result = [];
1393
+ const n = hull.length;
1394
+ // Compute centroid
1395
+ let cx = 0, cy = 0;
1396
+ for (const [x, y] of hull) {
1397
+ cx += x;
1398
+ cy += y;
1399
+ }
1400
+ cx /= n;
1401
+ cy /= n;
1402
+ // Push each vertex outward from centroid
1403
+ for (const [x, y] of hull) {
1404
+ const dx = x - cx;
1405
+ const dy = y - cy;
1406
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
1407
+ result.push([x + (dx / dist) * padding, y + (dy / dist) * padding]);
1408
+ }
1409
+ return result;
1410
+ }
1411
+ /** Convert a hull (polygon) to a smooth SVG path with rounded corners. */
1412
+ function hullToSmoothPath(hull) {
1413
+ if (hull.length < 3)
1414
+ return "";
1415
+ const n = hull.length;
1416
+ // Use cardinal spline approach: cubic Bézier through hull vertices
1417
+ const tension = 0.3;
1418
+ const parts = [];
1419
+ // Start at midpoint between last and first vertex
1420
+ const mx = (hull[n - 1][0] + hull[0][0]) / 2;
1421
+ const my = (hull[n - 1][1] + hull[0][1]) / 2;
1422
+ parts.push(`M ${mx} ${my}`);
1423
+ for (let i = 0; i < n; i++) {
1424
+ const p0 = hull[(i - 1 + n) % n];
1425
+ const p1 = hull[i];
1426
+ const p2 = hull[(i + 1) % n];
1427
+ const p3 = hull[(i + 2) % n];
1428
+ // Control points using Catmull-Rom to Bézier conversion
1429
+ const cp1x = p1[0] + (p2[0] - p0[0]) * tension;
1430
+ const cp1y = p1[1] + (p2[1] - p0[1]) * tension;
1431
+ const cp2x = p2[0] - (p3[0] - p1[0]) * tension;
1432
+ const cp2y = p2[1] - (p3[1] - p1[1]) * tension;
1433
+ parts.push(`C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2[0]} ${p2[1]}`);
1434
+ }
1435
+ parts.push("Z");
1436
+ return parts.join(" ");
1437
+ }
1438
+ //# sourceMappingURL=renderer.js.map