@tldiagram/core-ui 1.87.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 (272) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/api/client.d.ts +143 -0
  3. package/dist/api/transport-vscode.d.ts +8 -0
  4. package/dist/api/transport.d.ts +1 -0
  5. package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
  6. package/dist/components/CodePreviewPanel.d.ts +9 -0
  7. package/dist/components/ConfirmDialog.d.ts +12 -0
  8. package/dist/components/ConnectorPanel.d.ts +21 -0
  9. package/dist/components/ContextBoundaryElement.d.ts +11 -0
  10. package/dist/components/ContextNeighborElement.d.ts +29 -0
  11. package/dist/components/ContextStraightConnector.d.ts +4 -0
  12. package/dist/components/CrossBranchControls.d.ts +9 -0
  13. package/dist/components/DependenciesOnboarding.d.ts +5 -0
  14. package/dist/components/DrawingCanvas.d.ts +39 -0
  15. package/dist/components/ElementLibrary-vscode.d.ts +7 -0
  16. package/dist/components/ElementLibrary.d.ts +22 -0
  17. package/dist/components/ElementNode.d.ts +36 -0
  18. package/dist/components/ElementPanel.d.ts +25 -0
  19. package/dist/components/ExploreOnboarding.d.ts +5 -0
  20. package/dist/components/ExplorePageOnboarding.d.ts +5 -0
  21. package/dist/components/ExportModal.d.ts +16 -0
  22. package/dist/components/FloatingEdge.d.ts +9 -0
  23. package/dist/components/GitSourceLinker.d.ts +8 -0
  24. package/dist/components/HeaderContext.d.ts +16 -0
  25. package/dist/components/Icons.d.ts +95 -0
  26. package/dist/components/ImportModal.d.ts +10 -0
  27. package/dist/components/InlineElementAdder.d.ts +17 -0
  28. package/dist/components/LayoutSection.d.ts +7 -0
  29. package/dist/components/LocalSourceLinker.d.ts +8 -0
  30. package/dist/components/MiniZoomOnboarding.d.ts +5 -0
  31. package/dist/components/NavBreadcrumb.d.ts +6 -0
  32. package/dist/components/NodeBody.d.ts +12 -0
  33. package/dist/components/NodeContainer.d.ts +8 -0
  34. package/dist/components/NodeHoverCard.d.ts +10 -0
  35. package/dist/components/PanelHeader.d.ts +8 -0
  36. package/dist/components/PanelUI.d.ts +3 -0
  37. package/dist/components/ProxyConnectorEdge.d.ts +4 -0
  38. package/dist/components/ProxyConnectorPanel.d.ts +9 -0
  39. package/dist/components/SafeBackground.d.ts +13 -0
  40. package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
  41. package/dist/components/SetChildModal.d.ts +10 -0
  42. package/dist/components/SetParentModal.d.ts +10 -0
  43. package/dist/components/SlidingPanel.d.ts +16 -0
  44. package/dist/components/TagUpsert.d.ts +8 -0
  45. package/dist/components/TopMenuBar.d.ts +8 -0
  46. package/dist/components/ViewBezierConnector.d.ts +4 -0
  47. package/dist/components/ViewDrawMenu.d.ts +22 -0
  48. package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
  49. package/dist/components/ViewEditorOnboarding.d.ts +5 -0
  50. package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
  51. package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
  52. package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
  53. package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
  54. package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
  55. package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
  56. package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
  57. package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
  58. package/dist/components/ViewExplorer/index.d.ts +31 -0
  59. package/dist/components/ViewExplorer/types.d.ts +11 -0
  60. package/dist/components/ViewExplorer/utils.d.ts +6 -0
  61. package/dist/components/ViewExplorer-vscode.d.ts +6 -0
  62. package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
  63. package/dist/components/ViewFloatingMenu.d.ts +39 -0
  64. package/dist/components/ViewGridNode.d.ts +29 -0
  65. package/dist/components/ViewHeaderButton.d.ts +11 -0
  66. package/dist/components/ViewPanel.d.ts +18 -0
  67. package/dist/components/ViewsGridOnboarding.d.ts +5 -0
  68. package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
  69. package/dist/components/ZUI/index.d.ts +2 -0
  70. package/dist/components/ZUI/layout.d.ts +18 -0
  71. package/dist/components/ZUI/proxy.d.ts +25 -0
  72. package/dist/components/ZUI/renderer.d.ts +30 -0
  73. package/dist/components/ZUI/types.d.ts +140 -0
  74. package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
  75. package/dist/config/runtime-vscode.d.ts +22 -0
  76. package/dist/config/runtime.d.ts +5 -0
  77. package/dist/constants/colors.d.ts +27 -0
  78. package/dist/constants/diagramColors.d.ts +1 -0
  79. package/dist/context/ThemeContext.d.ts +27 -0
  80. package/dist/crossBranch/graph.d.ts +13 -0
  81. package/dist/crossBranch/resolve.d.ts +22 -0
  82. package/dist/crossBranch/settings.d.ts +6 -0
  83. package/dist/crossBranch/store.d.ts +11 -0
  84. package/dist/crossBranch/types.d.ts +96 -0
  85. package/dist/demo/DemoPage.d.ts +9 -0
  86. package/dist/demo/seed.d.ts +9 -0
  87. package/dist/demo/store.d.ts +137 -0
  88. package/dist/demo/viewEditor.d.ts +26 -0
  89. package/dist/favicon.svg +35 -0
  90. package/dist/hooks/useSafeFitView.d.ts +16 -0
  91. package/dist/index.css +1 -0
  92. package/dist/index.d.ts +115 -0
  93. package/dist/index.js +19966 -0
  94. package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
  95. package/dist/lib/vscodeBridge.d.ts +5 -0
  96. package/dist/logo-120.png +0 -0
  97. package/dist/logo-bw.png +0 -0
  98. package/dist/logo-bw.svg +15 -0
  99. package/dist/logo-text.svg +51 -0
  100. package/dist/logo.svg +35 -0
  101. package/dist/pages/AppearanceSettings.d.ts +3 -0
  102. package/dist/pages/Dependencies.d.ts +1 -0
  103. package/dist/pages/InfiniteZoom.d.ts +7 -0
  104. package/dist/pages/Settings.d.ts +7 -0
  105. package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
  106. package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
  107. package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
  108. package/dist/pages/ViewEditor/context.d.ts +13 -0
  109. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
  110. package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
  111. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
  112. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
  113. package/dist/pages/ViewEditor/index.d.ts +8 -0
  114. package/dist/pages/ViewEditor/utils.d.ts +14 -0
  115. package/dist/pages/Views.d.ts +6 -0
  116. package/dist/pages/ViewsGrid.d.ts +6 -0
  117. package/dist/pkg/importer/mermaid.d.ts +7 -0
  118. package/dist/pkg/importer/mermaid.test.d.ts +1 -0
  119. package/dist/platform/PlatformContext.d.ts +6 -0
  120. package/dist/platform/context.d.ts +3 -0
  121. package/dist/platform/local.d.ts +2 -0
  122. package/dist/platform/types.d.ts +17 -0
  123. package/dist/slots.d.ts +67 -0
  124. package/dist/theme.d.ts +2 -0
  125. package/dist/types/index.d.ts +193 -0
  126. package/dist/types/vscode-messages.d.ts +60 -0
  127. package/dist/utils/edgeDistribution.d.ts +34 -0
  128. package/dist/utils/githubApi.d.ts +4 -0
  129. package/dist/utils/githubCache.d.ts +17 -0
  130. package/dist/utils/ids.d.ts +2 -0
  131. package/dist/utils/technologyCatalog.d.ts +15 -0
  132. package/dist/utils/toast.d.ts +15 -0
  133. package/dist/utils/treesitter.d.ts +13 -0
  134. package/dist/utils/url.d.ts +12 -0
  135. package/package.json +159 -0
  136. package/src/App.tsx +141 -0
  137. package/src/api/client.ts +618 -0
  138. package/src/api/transport-vscode.ts +28 -0
  139. package/src/api/transport.ts +7 -0
  140. package/src/assets/logo-mark.svg +31 -0
  141. package/src/assets/logo-wordmark.svg +22 -0
  142. package/src/assets/logo.svg +35 -0
  143. package/src/components/CodePreviewPanel-vscode.tsx +85 -0
  144. package/src/components/CodePreviewPanel.tsx +384 -0
  145. package/src/components/ConfirmDialog.tsx +66 -0
  146. package/src/components/ConnectorPanel.tsx +403 -0
  147. package/src/components/ContextBoundaryElement.tsx +35 -0
  148. package/src/components/ContextNeighborElement.tsx +282 -0
  149. package/src/components/ContextStraightConnector.tsx +144 -0
  150. package/src/components/CrossBranchControls.tsx +105 -0
  151. package/src/components/DependenciesOnboarding.tsx +427 -0
  152. package/src/components/DrawingCanvas.tsx +391 -0
  153. package/src/components/ElementLibrary-vscode.tsx +9 -0
  154. package/src/components/ElementLibrary.tsx +512 -0
  155. package/src/components/ElementNode.tsx +1033 -0
  156. package/src/components/ElementPanel.tsx +928 -0
  157. package/src/components/ExploreOnboarding.tsx +347 -0
  158. package/src/components/ExplorePageOnboarding.tsx +383 -0
  159. package/src/components/ExportModal.tsx +132 -0
  160. package/src/components/FloatingEdge.tsx +115 -0
  161. package/src/components/GitSourceLinker.tsx +1053 -0
  162. package/src/components/HeaderContext.tsx +30 -0
  163. package/src/components/Icons.tsx +245 -0
  164. package/src/components/ImportModal.tsx +219 -0
  165. package/src/components/InlineElementAdder.tsx +216 -0
  166. package/src/components/LayoutSection.tsx +624 -0
  167. package/src/components/LocalSourceLinker.tsx +330 -0
  168. package/src/components/MiniZoomOnboarding.tsx +78 -0
  169. package/src/components/NavBreadcrumb.tsx +24 -0
  170. package/src/components/NodeBody.tsx +89 -0
  171. package/src/components/NodeContainer.tsx +58 -0
  172. package/src/components/NodeHoverCard.tsx +135 -0
  173. package/src/components/PanelHeader.tsx +36 -0
  174. package/src/components/PanelUI.tsx +24 -0
  175. package/src/components/ProxyConnectorEdge.tsx +169 -0
  176. package/src/components/ProxyConnectorPanel.tsx +130 -0
  177. package/src/components/SafeBackground.tsx +19 -0
  178. package/src/components/ScrollIndicatorWrapper.tsx +117 -0
  179. package/src/components/SetChildModal.tsx +191 -0
  180. package/src/components/SetParentModal.tsx +187 -0
  181. package/src/components/SlidingPanel.tsx +114 -0
  182. package/src/components/TagUpsert.tsx +142 -0
  183. package/src/components/TopMenuBar.tsx +380 -0
  184. package/src/components/ViewBezierConnector.tsx +143 -0
  185. package/src/components/ViewDrawMenu.tsx +270 -0
  186. package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
  187. package/src/components/ViewEditorOnboarding.tsx +445 -0
  188. package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
  189. package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
  190. package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
  191. package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
  192. package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
  193. package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
  194. package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
  195. package/src/components/ViewExplorer/ViewTree.tsx +98 -0
  196. package/src/components/ViewExplorer/index.tsx +384 -0
  197. package/src/components/ViewExplorer/types.ts +13 -0
  198. package/src/components/ViewExplorer/utils.ts +56 -0
  199. package/src/components/ViewExplorer-vscode.tsx +8 -0
  200. package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
  201. package/src/components/ViewFloatingMenu.tsx +379 -0
  202. package/src/components/ViewGridNode.tsx +451 -0
  203. package/src/components/ViewHeaderButton.tsx +60 -0
  204. package/src/components/ViewPanel.tsx +162 -0
  205. package/src/components/ViewsGridOnboarding.tsx +400 -0
  206. package/src/components/ZUI/ZUICanvas.tsx +853 -0
  207. package/src/components/ZUI/index.ts +3 -0
  208. package/src/components/ZUI/layout.ts +323 -0
  209. package/src/components/ZUI/proxy.ts +278 -0
  210. package/src/components/ZUI/renderer.ts +1189 -0
  211. package/src/components/ZUI/types.ts +150 -0
  212. package/src/components/ZUI/useZUIInteraction.ts +720 -0
  213. package/src/config/runtime-vscode.ts +46 -0
  214. package/src/config/runtime.ts +30 -0
  215. package/src/constants/colors.ts +80 -0
  216. package/src/constants/diagramColors.ts +9 -0
  217. package/src/context/ThemeContext.tsx +158 -0
  218. package/src/crossBranch/graph.ts +207 -0
  219. package/src/crossBranch/resolve.ts +643 -0
  220. package/src/crossBranch/settings.ts +59 -0
  221. package/src/crossBranch/store.ts +71 -0
  222. package/src/crossBranch/types.ts +102 -0
  223. package/src/demo/DemoPage.tsx +184 -0
  224. package/src/demo/seed.ts +67 -0
  225. package/src/demo/store.ts +536 -0
  226. package/src/demo/viewEditor.ts +110 -0
  227. package/src/hooks/useSafeFitView.ts +60 -0
  228. package/src/index.css +309 -0
  229. package/src/index.ts +184 -0
  230. package/src/kafka-ss.png +0 -0
  231. package/src/lib/vscodeBridge-vscode.ts +27 -0
  232. package/src/lib/vscodeBridge.ts +7 -0
  233. package/src/main.tsx +46 -0
  234. package/src/pages/AppearanceSettings.tsx +135 -0
  235. package/src/pages/Dependencies.tsx +926 -0
  236. package/src/pages/InfiniteZoom.tsx +404 -0
  237. package/src/pages/Settings.tsx +91 -0
  238. package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
  239. package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
  240. package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
  241. package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
  242. package/src/pages/ViewEditor/context.tsx +21 -0
  243. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
  244. package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
  245. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
  246. package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
  247. package/src/pages/ViewEditor/index.tsx +1366 -0
  248. package/src/pages/ViewEditor/utils.ts +88 -0
  249. package/src/pages/Views.tsx +171 -0
  250. package/src/pages/ViewsGrid.tsx +1310 -0
  251. package/src/pkg/importer/mermaid.test.ts +141 -0
  252. package/src/pkg/importer/mermaid.ts +76 -0
  253. package/src/platform/PlatformContext.tsx +17 -0
  254. package/src/platform/context.ts +9 -0
  255. package/src/platform/local.tsx +15 -0
  256. package/src/platform/types.ts +19 -0
  257. package/src/slots.ts +92 -0
  258. package/src/styles/editor-panels.css +66 -0
  259. package/src/styles/theme.css +56 -0
  260. package/src/theme.ts +336 -0
  261. package/src/types/index.ts +234 -0
  262. package/src/types/offline-ambient.d.ts +14 -0
  263. package/src/types/vscode-messages.ts +32 -0
  264. package/src/utils/edgeDistribution.ts +103 -0
  265. package/src/utils/githubApi.ts +121 -0
  266. package/src/utils/githubCache.ts +108 -0
  267. package/src/utils/ids.ts +9 -0
  268. package/src/utils/technologyCatalog.ts +143 -0
  269. package/src/utils/toast.ts +100 -0
  270. package/src/utils/treesitter.ts +147 -0
  271. package/src/utils/url.ts +72 -0
  272. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,926 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { motion } from 'framer-motion'
3
+ import {
4
+ Box,
5
+ Button,
6
+ Flex,
7
+ HStack,
8
+ Input,
9
+ InputGroup,
10
+ InputLeftElement,
11
+ Menu,
12
+ MenuButton,
13
+ MenuItem,
14
+ MenuList,
15
+ Spinner,
16
+ Tag,
17
+ Text,
18
+ VStack,
19
+ Grid,
20
+ } from '@chakra-ui/react'
21
+ import { api } from '../api/client'
22
+ import type { DependencyConnector, DependencyElement } from '../types'
23
+ import { TYPE_COLORS, ELEMENT_TYPES } from '../types'
24
+ import { useSetHeader } from '../components/HeaderContext'
25
+ import { ElementContainer } from '../components/NodeContainer'
26
+ import { ElementBody } from '../components/NodeBody'
27
+ import DependenciesOnboarding from '../components/DependenciesOnboarding'
28
+ import { useTheme } from '../context/ThemeContext'
29
+ import { hexToRgba } from '../constants/colors'
30
+
31
+ // ── Data types ─────────────────────────────────────────────────────────────
32
+ interface ElementWithNeighbours extends DependencyElement {
33
+ neighbourCount: number
34
+ }
35
+
36
+ interface NeighbourNode {
37
+ element: DependencyElement
38
+ connectors: DependencyConnector[]
39
+ position: 'left' | 'right' | 'top' | 'bottom'
40
+ }
41
+
42
+ // ── Helpers ────────────────────────────────────────────────────────────────
43
+ function computeNeighbourCounts(elements: DependencyElement[], connectors: DependencyConnector[]): ElementWithNeighbours[] {
44
+ const counts = new Map<string, Set<string>>()
45
+ elements.forEach((element) => counts.set(element.id, new Set()))
46
+ connectors.forEach((connector) => {
47
+ counts.get(connector.source_element_id)?.add(connector.target_element_id)
48
+ counts.get(connector.target_element_id)?.add(connector.source_element_id)
49
+ })
50
+ return elements.map((element) => ({ ...element, neighbourCount: counts.get(element.id)?.size ?? 0 }))
51
+ }
52
+
53
+ function getNeighbourGraph(selectedId: string, elements: DependencyElement[], allConnectors: DependencyConnector[]): NeighbourNode[] {
54
+ const elementMap = new Map<string, DependencyElement>(elements.map((element) => [element.id, element]))
55
+ const related = allConnectors.filter(
56
+ (connector) => connector.source_element_id === selectedId || connector.target_element_id === selectedId,
57
+ )
58
+ const grouped = new Map<string, DependencyConnector[]>()
59
+ related.forEach((connector) => {
60
+ const otherId = connector.source_element_id === selectedId ? connector.target_element_id : connector.source_element_id
61
+ if (!grouped.has(otherId)) grouped.set(otherId, [])
62
+ grouped.get(otherId)!.push(connector)
63
+ })
64
+ const result: NeighbourNode[] = []
65
+ grouped.forEach((connectors, otherId) => {
66
+ const element = elementMap.get(otherId)
67
+ if (!element) return
68
+ let hasIncoming = false
69
+ let hasOutgoing = false
70
+ let hasBoth = false
71
+ let hasUndirected = false
72
+ connectors.forEach((connector) => {
73
+ const dir = connector.direction || 'forward'
74
+ if (dir === 'both' || dir === 'bidirectional') hasBoth = true
75
+ else if (dir === 'none') hasUndirected = true
76
+ else if (dir === 'forward') {
77
+ if (connector.source_element_id === selectedId) hasOutgoing = true
78
+ else hasIncoming = true
79
+ } else if (dir === 'backward') {
80
+ if (connector.source_element_id === selectedId) hasIncoming = true
81
+ else hasOutgoing = true
82
+ }
83
+ })
84
+ let position: NeighbourNode['position']
85
+ if (hasBoth) position = 'top'
86
+ else if (hasUndirected) position = 'bottom'
87
+ else if (hasIncoming && hasOutgoing) position = 'top'
88
+ else if (hasIncoming) position = 'left'
89
+ else position = 'right'
90
+ result.push({ element, connectors, position })
91
+ })
92
+ return result
93
+ }
94
+
95
+ function chunkNodes(nodes: NeighbourNode[], size = 20): NeighbourNode[][] {
96
+ if (nodes.length <= size) return [nodes]
97
+
98
+ const chunks: NeighbourNode[][] = []
99
+ for (let index = 0; index < nodes.length; index += size) {
100
+ chunks.push(nodes.slice(index, index + size))
101
+ }
102
+ return chunks
103
+ }
104
+
105
+ // ── Direction indicator ─────────────────────────────────────────────────────
106
+ function ConnectionIndicator({
107
+ position,
108
+ compactLevel,
109
+ }: {
110
+ position: NeighbourNode['position']
111
+ compactLevel: number
112
+ }) {
113
+ const orientation = position === 'left' || position === 'right' ? 'horizontal' : 'vertical'
114
+ const config =
115
+ position === 'bottom'
116
+ ? { icon: '·', label: 'undirected', color: '#94a3b8', tint: 'rgba(148,163,184,0.16)' }
117
+ : position === 'top'
118
+ ? { icon: '↕', label: 'bidirectional', color: '#5eead4', tint: 'rgba(45,212,191,0.16)' }
119
+ : position === 'left'
120
+ ? { icon: '→', label: 'directional', color: '#c4b5fd', tint: 'rgba(167,139,250,0.18)' }
121
+ : { icon: '→', label: 'directional', color: '#7dd3fc', tint: 'rgba(56,189,248,0.18)' }
122
+ const isCompact = compactLevel >= 2
123
+ const lineColor = `${config.color}66`
124
+ // outer = away from center node; inner = toward center node (longer to visually reach it)
125
+ const outerLine = isCompact ? '10px' : '18px'
126
+ const innerLine = isCompact ? '24px' : '44px'
127
+ const firstLineSize = (position === 'right' || position === 'bottom') ? innerLine : outerLine
128
+ const secondLineSize = (position === 'left' || position === 'top') ? innerLine : outerLine
129
+
130
+ return (
131
+ <Flex
132
+ align="center"
133
+ justify="center"
134
+ direction={orientation === 'horizontal' ? 'row' : 'column'}
135
+ gap={isCompact ? 1 : 1.5}
136
+ flexShrink={0}
137
+ aria-label={config.label}
138
+ >
139
+ <Box
140
+ w={orientation === 'horizontal' ? firstLineSize : '1px'}
141
+ h={orientation === 'vertical' ? firstLineSize : '1px'}
142
+ bg={lineColor}
143
+ borderRadius="full"
144
+ />
145
+ <Flex
146
+ align="center"
147
+ justify="center"
148
+ w={isCompact ? '20px' : '24px'}
149
+ h={isCompact ? '20px' : '24px'}
150
+ borderRadius="full"
151
+ border="1px solid"
152
+ borderColor={lineColor}
153
+ color={config.color}
154
+ bg={config.tint}
155
+ boxShadow={`0 0 0 1px ${config.tint}`}
156
+ fontSize={isCompact ? '11px' : '12px'}
157
+ fontWeight="bold"
158
+ >
159
+ {config.icon}
160
+ </Flex>
161
+ <Box
162
+ w={orientation === 'horizontal' ? secondLineSize : '1px'}
163
+ h={orientation === 'vertical' ? secondLineSize : '1px'}
164
+ bg={lineColor}
165
+ borderRadius="full"
166
+ />
167
+ </Flex>
168
+ )
169
+ }
170
+
171
+ // ── Neighbour card ───────────────────────────────────────────────────────────
172
+ // compactLevel: 0 = full, 1 = no connector labels, 2 = no connectors/tech, 3 = name only + minimal padding
173
+ function NeighbourCard({
174
+ node,
175
+ onClick,
176
+ setRef,
177
+ compactLevel = 0,
178
+ }: {
179
+ node: NeighbourNode
180
+ onClick: () => void
181
+ setRef?: (el: HTMLDivElement | null) => void
182
+ compactLevel?: number
183
+ }) {
184
+ const cardPadding = compactLevel >= 3 ? 1 : compactLevel >= 2 ? 1.5 : compactLevel >= 1 ? 2 : 3
185
+ const showTech = compactLevel < 2
186
+ const showType = compactLevel < 3
187
+ const minW = compactLevel >= 2 ? '100px' : '130px'
188
+ const maxW = compactLevel >= 2 ? '160px' : '200px'
189
+
190
+ const rawName = node.element.name ?? ''
191
+ const truncatedName = rawName.length > 30 ? rawName.slice(0, 29) + '…' : rawName
192
+ const nameLen = truncatedName.length
193
+ const nameSize =
194
+ compactLevel >= 3 ? (nameLen > 15 ? '2xs' : 'xs') :
195
+ compactLevel >= 2 ? (nameLen > 20 ? '2xs' : 'xs') :
196
+ compactLevel >= 1 ? (nameLen > 22 ? 'xs' : 'sm') :
197
+ (nameLen > 24 ? 'xs' : 'sm')
198
+
199
+ return (
200
+ <motion.div
201
+ ref={setRef}
202
+ data-pan-block="true"
203
+ initial={{ opacity: 0, scale: 0.92 }}
204
+ animate={{ opacity: 1, scale: 1 }}
205
+ whileHover={{ scale: 1.02 }}
206
+ transition={{ duration: 0.18 }}
207
+ >
208
+ <ElementContainer
209
+ onClick={onClick}
210
+ minW={minW}
211
+ maxW={maxW}
212
+ p={0}
213
+ cursor="pointer"
214
+ borderColor="whiteAlpha.200"
215
+ _hover={{ borderColor: 'var(--accent)', boxShadow: '0 0 0 1px rgba(var(--accent-rgb), 0.25)' }}
216
+ >
217
+ <ElementBody
218
+ name={truncatedName}
219
+ type={showType ? (node.element.type ?? '') : ''}
220
+ technology={showTech ? (node.element.technology || undefined) : undefined}
221
+ nameSize={nameSize}
222
+ align="flex-start"
223
+ p={cardPadding}
224
+ />
225
+ </ElementContainer>
226
+ </motion.div>
227
+ )
228
+ }
229
+
230
+ // ── Search icon ──────────────────────────────────────────────────────────────
231
+ function SearchIcon() {
232
+ return (
233
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
234
+ <circle cx="11" cy="11" r="8" />
235
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
236
+ </svg>
237
+ )
238
+ }
239
+
240
+ // ── Type accent color map (CSS hex values for non-Chakra contexts) ───────────
241
+ const TYPE_HEX: Record<string, string> = {
242
+ person: '#4fd1c5',
243
+ system: '#63b3ed',
244
+ container: '#b794f4',
245
+ component: '#f6ad55',
246
+ database: '#76e4f7',
247
+ queue: '#faf089',
248
+ api: '#68d391',
249
+ service: '#fbb6ce',
250
+ external: '#718096',
251
+ }
252
+
253
+ // ── Main element/component ───────────────────────────────────────────────────────────
254
+ export default function Dependencies() {
255
+ const setHeader = useSetHeader()
256
+ const { accent, elementColor } = useTheme()
257
+
258
+ const [elements, setElements] = useState<DependencyElement[]>([])
259
+ const [allEdges, setAllEdges] = useState<DependencyConnector[]>([])
260
+ const [loading, setLoading] = useState(true)
261
+ const [search, setSearch] = useState('')
262
+ const [typeFilter, setTypeFilter] = useState('')
263
+ const [selectedId, setSelectedId] = useState<string | null>(null)
264
+ const [topRatio, setTopRatio] = useState(0.45)
265
+
266
+ // Graph layout measurement
267
+ const graphRef = useRef<HTMLDivElement>(null)
268
+ const [graphHeight, setGraphHeight] = useState(0)
269
+
270
+ // Divider drag
271
+ const containerRef = useRef<HTMLDivElement>(null)
272
+ const draggingRef = useRef(false)
273
+ const [isDragging, setIsDragging] = useState(false)
274
+
275
+ // Canvas pan kept entirely off React state to avoid re-renders on every mousemove
276
+ const canvasPanRef = useRef({ x: 0, y: 0 })
277
+ const panContainerRef = useRef<HTMLDivElement>(null)
278
+ const canvasPanningRef = useRef(false)
279
+ const canvasPanStartRef = useRef({ touchX: 0, touchY: 0, panX: 0, panY: 0 })
280
+
281
+ const applyPan = useCallback((x: number, y: number) => {
282
+ canvasPanRef.current = { x, y }
283
+ if (panContainerRef.current) {
284
+ panContainerRef.current.style.transform = `translate(${x}px, ${y}px)`
285
+ }
286
+ }, [])
287
+
288
+ useEffect(() => { applyPan(0, 0) }, [selectedId, applyPan])
289
+
290
+ // Header
291
+ useEffect(() => {
292
+ setHeader({
293
+ hideMobileBar: true,
294
+ node: (
295
+ <HStack
296
+ bg="whiteAlpha.50"
297
+ border="1px solid"
298
+ borderColor="whiteAlpha.100"
299
+ px={3}
300
+ py={1}
301
+ borderRadius="md"
302
+ spacing={3}
303
+ >
304
+ <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', compact: 'inline' }}>
305
+ {elements.length} <Text as="span" color="whiteAlpha.400" fontWeight="normal">elements</Text>
306
+ </Text>
307
+ <Box w="1px" h="10px" bg="whiteAlpha.200" display={{ base: 'none', compact: 'block' }} />
308
+ <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', compact: 'inline' }}>
309
+ {allEdges.length} <Text as="span" color="whiteAlpha.400" fontWeight="normal">connectors</Text>
310
+ </Text>
311
+ <Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" display={{ base: 'none', sm: 'inline', compact: 'none' }}>
312
+ {elements.length}<Text as="span" color="whiteAlpha.400">E</Text>
313
+ <Text as="span" color="whiteAlpha.200" mx={1}>/</Text>
314
+ {allEdges.length}<Text as="span" color="whiteAlpha.400">C</Text>
315
+ </Text>
316
+ </HStack>
317
+ ),
318
+ })
319
+ return () => setHeader(null)
320
+ }, [elements.length, allEdges.length, setHeader])
321
+
322
+ // Data fetch
323
+ useEffect(() => {
324
+ api.dependencies
325
+ .list()
326
+ .then((resp) => {
327
+ const objs = resp.elements || []
328
+ const edgs = resp.connectors || []
329
+ setElements(objs)
330
+ setAllEdges(edgs)
331
+
332
+ if (objs.length > 0) {
333
+ const withCounts = computeNeighbourCounts(objs, edgs)
334
+ const sorted = [...withCounts].sort((a, b) => b.neighbourCount - a.neighbourCount)
335
+ setSelectedId(sorted[0].id)
336
+ }
337
+ })
338
+ .catch(() => { /* intentionally empty */ })
339
+ .finally(() => setLoading(false))
340
+ }, [])
341
+
342
+ // Derived data
343
+ const elementsWithCounts = useMemo(
344
+ () => computeNeighbourCounts(elements, allEdges),
345
+ [elements, allEdges],
346
+ )
347
+
348
+ const filteredElements = useMemo(() => {
349
+ let list = elementsWithCounts
350
+ if (search) {
351
+ const q = search.toLowerCase()
352
+ list = list.filter((o) => {
353
+ const nameMatch = (o.name || '').toLowerCase().includes(q)
354
+ const typeMatch = (o.type || '').toLowerCase().includes(q)
355
+ const techMatch = (o.technology || '').toLowerCase().includes(q)
356
+ const tags = Array.isArray(o.tags) ? o.tags : []
357
+ const tagMatch = tags.some((t) => (t || '').toLowerCase().includes(q))
358
+ return nameMatch || typeMatch || techMatch || tagMatch
359
+ })
360
+ }
361
+ if (typeFilter) list = list.filter((o) => o.type === typeFilter)
362
+ return [...list].sort((a, b) => b.neighbourCount - a.neighbourCount)
363
+ }, [elementsWithCounts, search, typeFilter])
364
+
365
+ const selectedElement = useMemo(() => {
366
+ if (selectedId === null) return null
367
+ return elements.find((o) => o.id === selectedId) || null
368
+ }, [elements, selectedId])
369
+ const neighbourGraph = useMemo(() => {
370
+ if (selectedId === null) return []
371
+ return getNeighbourGraph(selectedId, elements, allEdges)
372
+ }, [selectedId, elements, allEdges])
373
+
374
+ // Divider drag
375
+ const startDrag = useCallback(() => {
376
+ draggingRef.current = true
377
+ setIsDragging(true)
378
+ document.body.style.cursor = 'row-resize'
379
+ document.body.style.userSelect = 'none'
380
+ }, [])
381
+
382
+ const onDividerMouseDown = useCallback(() => { startDrag() }, [startDrag])
383
+
384
+ const onDividerTouchStart = useCallback((e: React.TouchEvent) => {
385
+ e.preventDefault()
386
+ startDrag()
387
+ }, [startDrag])
388
+
389
+ useEffect(() => {
390
+ const applyClientY = (clientY: number) => {
391
+ if (!draggingRef.current || !containerRef.current) return
392
+ const rect = containerRef.current.getBoundingClientRect()
393
+ const ratio = Math.max(0.15, Math.min(0.85, (clientY - rect.top) / rect.height))
394
+ setTopRatio(ratio)
395
+ }
396
+ const stopDrag = () => {
397
+ draggingRef.current = false
398
+ setIsDragging(false)
399
+ document.body.style.cursor = ''
400
+ document.body.style.userSelect = ''
401
+ }
402
+ const onMouseMove = (e: MouseEvent) => applyClientY(e.clientY)
403
+ const onTouchMove = (e: TouchEvent) => {
404
+ if (!draggingRef.current) return
405
+ e.preventDefault()
406
+ applyClientY(e.touches[0].clientY)
407
+ }
408
+ window.addEventListener('mousemove', onMouseMove)
409
+ window.addEventListener('mouseup', stopDrag)
410
+ window.addEventListener('touchmove', onTouchMove, { passive: false })
411
+ window.addEventListener('touchend', stopDrag)
412
+ return () => {
413
+ window.removeEventListener('mousemove', onMouseMove)
414
+ window.removeEventListener('mouseup', stopDrag)
415
+ window.removeEventListener('touchmove', onTouchMove)
416
+ window.removeEventListener('touchend', stopDrag)
417
+ }
418
+ }, [])
419
+
420
+ const shouldBlockCanvasPan = useCallback((target: EventTarget | null) => {
421
+ return target instanceof HTMLElement && Boolean(target.closest('[data-pan-block="true"]'))
422
+ }, [])
423
+
424
+ const startCanvasPan = useCallback((clientX: number, clientY: number, target: EventTarget | null) => {
425
+ if (shouldBlockCanvasPan(target)) return
426
+ canvasPanningRef.current = true
427
+ if (graphRef.current) graphRef.current.style.cursor = 'grabbing'
428
+ canvasPanStartRef.current = {
429
+ touchX: clientX,
430
+ touchY: clientY,
431
+ panX: canvasPanRef.current.x,
432
+ panY: canvasPanRef.current.y,
433
+ }
434
+ }, [shouldBlockCanvasPan])
435
+
436
+ const onCanvasMouseDown = useCallback((e: React.MouseEvent) => {
437
+ if (e.button !== 0) return
438
+ e.preventDefault()
439
+ startCanvasPan(e.clientX, e.clientY, e.target)
440
+ }, [startCanvasPan])
441
+
442
+ const onCanvasTouchStart = useCallback((e: React.TouchEvent) => {
443
+ if (e.touches.length !== 1) return
444
+ startCanvasPan(e.touches[0].clientX, e.touches[0].clientY, e.target)
445
+ }, [startCanvasPan])
446
+
447
+ useEffect(() => {
448
+ const onMouseMove = (e: MouseEvent) => {
449
+ if (!canvasPanningRef.current) return
450
+ applyPan(
451
+ canvasPanStartRef.current.panX + e.clientX - canvasPanStartRef.current.touchX,
452
+ canvasPanStartRef.current.panY + e.clientY - canvasPanStartRef.current.touchY,
453
+ )
454
+ }
455
+ const stopCanvasPan = () => {
456
+ if (!canvasPanningRef.current) return
457
+ canvasPanningRef.current = false
458
+ if (graphRef.current) graphRef.current.style.cursor = 'grab'
459
+ }
460
+ window.addEventListener('mousemove', onMouseMove)
461
+ window.addEventListener('mouseup', stopCanvasPan)
462
+ return () => {
463
+ window.removeEventListener('mousemove', onMouseMove)
464
+ window.removeEventListener('mouseup', stopCanvasPan)
465
+ }
466
+ }, [applyPan])
467
+
468
+ useEffect(() => {
469
+ const onTouchMove = (e: TouchEvent) => {
470
+ if (!canvasPanningRef.current || e.touches.length !== 1) return
471
+ e.preventDefault()
472
+ applyPan(
473
+ canvasPanStartRef.current.panX + e.touches[0].clientX - canvasPanStartRef.current.touchX,
474
+ canvasPanStartRef.current.panY + e.touches[0].clientY - canvasPanStartRef.current.touchY,
475
+ )
476
+ }
477
+ const onTouchEnd = () => { canvasPanningRef.current = false }
478
+ window.addEventListener('touchmove', onTouchMove, { passive: false })
479
+ window.addEventListener('touchend', onTouchEnd)
480
+ return () => {
481
+ window.removeEventListener('touchmove', onTouchMove)
482
+ window.removeEventListener('touchend', onTouchEnd)
483
+ }
484
+ }, [applyPan])
485
+
486
+ // Track graph container height for responsive compactness
487
+ useEffect(() => {
488
+ if (selectedId === null) { setGraphHeight(0); return }
489
+ const el = graphRef.current
490
+ if (!el) return
491
+ const ro = new ResizeObserver((entries) => {
492
+ setGraphHeight(entries[0]?.contentRect.height ?? 0)
493
+ })
494
+ ro.observe(el)
495
+ return () => ro.disconnect()
496
+ }, [selectedId, topRatio])
497
+
498
+ if (loading) {
499
+ return (
500
+ <Flex h="100vh" align="center" justify="center">
501
+ <Spinner size="xl" color="blue.500" thickness="3px" />
502
+ </Flex>
503
+ )
504
+ }
505
+
506
+ const leftNodes = neighbourGraph.filter((n) => n.position === 'left')
507
+ const rightNodes = neighbourGraph.filter((n) => n.position === 'right')
508
+ const topNodes = neighbourGraph.filter((n) => n.position === 'top')
509
+ const bottomNodes = neighbourGraph.filter((n) => n.position === 'bottom')
510
+ const leftColumns = chunkNodes(leftNodes)
511
+ const rightColumns = chunkNodes(rightNodes)
512
+ const topRows = chunkNodes(topNodes)
513
+ const bottomRows = chunkNodes(bottomNodes)
514
+ const leftColumnSize = Math.max(...leftColumns.map((column) => column.length), 0)
515
+ const rightColumnSize = Math.max(...rightColumns.map((column) => column.length), 0)
516
+
517
+ // Responsive compactness: computed independently per column
518
+ const toCompactLevel = (pxPerSlot: number) =>
519
+ pxPerSlot > 160 ? 0 : pxPerSlot > 110 ? 1 : pxPerSlot > 70 ? 2 : 3
520
+ const leftCompactLevel = toCompactLevel(
521
+ graphHeight > 0 && leftColumnSize > 0 ? graphHeight / leftColumnSize : 999,
522
+ )
523
+ const rightCompactLevel = toCompactLevel(
524
+ graphHeight > 0 && rightColumnSize > 0 ? graphHeight / rightColumnSize : 999,
525
+ )
526
+ // Top/bottom rows and overall layout spacing use the worst-case level
527
+ const maxCompactLevel = Math.max(leftCompactLevel, rightCompactLevel, 0)
528
+ const colSpacing = maxCompactLevel >= 3 ? 2 : maxCompactLevel >= 2 ? 3 : maxCompactLevel >= 1 ? 5 : 8
529
+ const nodeSpacing = maxCompactLevel >= 2 ? 1 : maxCompactLevel >= 1 ? 2 : 3
530
+ const selectedCardShadow = `0 0 0 3px ${hexToRgba(accent, 0.38)}, 0 18px 48px ${hexToRgba(accent, 0.12)}, 0 10px 36px rgba(0,0,0,0.55), 0 3px 10px rgba(0,0,0,0.4)`
531
+
532
+ return (
533
+ <Box h="100vh" display="flex" flexDir="column" bg="var(--bg-canvas)">
534
+ <Box ref={containerRef} flex={1} display="flex" flexDir="column" overflow="hidden">
535
+
536
+ {/* ── Top: Listing ──────────────────────────────────────────────────── */}
537
+ <Box
538
+ h={`${topRatio * 100}%`}
539
+ minH="120px"
540
+ display="flex"
541
+ flexDir="column"
542
+ overflow="hidden"
543
+ bg="var(--bg-canvas)"
544
+ >
545
+ {/* Filter bar */}
546
+ <Flex
547
+ px={5}
548
+ py={2.5}
549
+ gap={3}
550
+ flexShrink={0}
551
+ align="center"
552
+ borderBottom="1px solid"
553
+ borderColor="whiteAlpha.100"
554
+ >
555
+ <InputGroup size="sm" maxW="340px">
556
+ <InputLeftElement pointerEvents="none" color="gray.600" h="full" pl={1}>
557
+ <SearchIcon />
558
+ </InputLeftElement>
559
+ <Input
560
+ variant="elevated"
561
+ placeholder="Search by name, type, technology…"
562
+ value={search}
563
+ onChange={(e) => setSearch(e.target.value)}
564
+ pl={8}
565
+ fontSize="sm"
566
+ />
567
+ </InputGroup>
568
+ <Menu placement="bottom-start">
569
+ <MenuButton
570
+ as={Button}
571
+ variant="elevated"
572
+ size="sm"
573
+ minW="120px"
574
+ textAlign="left"
575
+ fontWeight="medium"
576
+ fontSize="sm"
577
+ rightIcon={
578
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
579
+ <polyline points="6 9 12 15 18 9" />
580
+ </svg>
581
+ }
582
+ >
583
+ {typeFilter || 'All types'}
584
+ </MenuButton>
585
+ <MenuList>
586
+ <MenuItem onClick={() => setTypeFilter('')} fontWeight={!typeFilter ? 'bold' : 'normal'}>
587
+ All types
588
+ </MenuItem>
589
+ {ELEMENT_TYPES.map((t) => (
590
+ <MenuItem key={t} onClick={() => setTypeFilter(t)} fontWeight={typeFilter === t ? 'bold' : 'normal'}>
591
+ {t}
592
+ </MenuItem>
593
+ ))}
594
+ </MenuList>
595
+ </Menu>
596
+ <Box flex={1} />
597
+ <Text fontSize="xs" color="gray.600">
598
+ {filteredElements.length} element{filteredElements.length !== 1 ? 's' : ''}
599
+ </Text>
600
+ </Flex>
601
+
602
+ {/* Column headers */}
603
+ <Flex
604
+ px={5}
605
+ py={1.5}
606
+ gap={0}
607
+ borderBottom="1px solid"
608
+ borderColor="whiteAlpha.50"
609
+ flexShrink={0}
610
+ align="center"
611
+ >
612
+ <Box flex={1} minW={0} pl={5}>
613
+ <Text fontSize="10px" color="gray.600" textTransform="uppercase" letterSpacing="0.08em" fontWeight="bold">Name</Text>
614
+ </Box>
615
+ <Box w="110px" flexShrink={0}>
616
+ <Text fontSize="10px" color="gray.600" textTransform="uppercase" letterSpacing="0.08em" fontWeight="bold">Type</Text>
617
+ </Box>
618
+ <Box w="130px" flexShrink={0} display={{ base: 'none', lg: 'block' }}>
619
+ <Text fontSize="10px" color="gray.600" textTransform="uppercase" letterSpacing="0.08em" fontWeight="bold">Technology</Text>
620
+ </Box>
621
+ </Flex>
622
+
623
+ {/* Scrollable list */}
624
+ <Box flex={1} overflowY="auto">
625
+ {filteredElements.length === 0 ? (
626
+ <Flex flexDir="column" align="center" justify="center" py={12} gap={2}>
627
+ <Text color="gray.600" fontSize="sm">No elements match your filters.</Text>
628
+ <Button
629
+ variant="link"
630
+ size="sm"
631
+ color="blue.400"
632
+ onClick={() => { setSearch(''); setTypeFilter('') }}
633
+ >
634
+ Clear filters
635
+ </Button>
636
+ </Flex>
637
+ ) : (
638
+ filteredElements.map((obj) => {
639
+ const typeKey = obj.type ?? ''
640
+ const color = TYPE_COLORS[typeKey] ?? 'gray'
641
+ const accentHex = TYPE_HEX[typeKey] ?? '#718096'
642
+ const isSelected = selectedId === obj.id
643
+
644
+ return (
645
+ <Flex
646
+ key={obj.id}
647
+ px={5}
648
+ h="42px"
649
+ align="center"
650
+ cursor="pointer"
651
+ borderBottom="1px solid"
652
+ borderColor="whiteAlpha.50"
653
+ bg={isSelected ? 'rgba(66,153,225,0.07)' : 'transparent'}
654
+ _hover={{ bg: isSelected ? 'rgba(66,153,225,0.1)' : 'whiteAlpha.50' }}
655
+ transition="background 0.1s"
656
+ onClick={() => setSelectedId(isSelected ? null : obj.id)}
657
+ position="relative"
658
+ role="row"
659
+ >
660
+ {/* Left type-color accent */}
661
+ <Box
662
+ w="3px"
663
+ alignSelf="stretch"
664
+ borderRadius="full"
665
+ flexShrink={0}
666
+ mr={3.5}
667
+ style={{ background: accentHex, opacity: isSelected ? 1 : 0.4 }}
668
+ />
669
+
670
+ {/* Name */}
671
+ <Box flex={1} minW={0} mr={4}>
672
+ <Text
673
+ fontSize="sm"
674
+ fontWeight={isSelected ? 'semibold' : 'medium'}
675
+ color={isSelected ? 'white' : 'gray.100'}
676
+ noOfLines={1}
677
+ >
678
+ {obj.name}
679
+ </Text>
680
+ </Box>
681
+
682
+ {/* Type badge */}
683
+ <Box w="110px" flexShrink={0}>
684
+ {obj.type && (
685
+ <Tag
686
+ size="sm"
687
+ colorScheme={color}
688
+ variant="subtle"
689
+ fontSize="9px"
690
+ fontWeight="bold"
691
+ textTransform="uppercase"
692
+ letterSpacing="0.06em"
693
+ >
694
+ {obj.type}
695
+ </Tag>
696
+ )}
697
+ </Box>
698
+
699
+ {/* Technology */}
700
+ <Box w="130px" flexShrink={0} display={{ base: 'none', lg: 'block' }}>
701
+ <Text fontSize="xs" color="gray.500" fontFamily="mono" noOfLines={1}>
702
+ {obj.technology || <Text as="span" color="gray.700">-</Text>}
703
+ </Text>
704
+ </Box>
705
+
706
+ </Flex>
707
+ )
708
+ })
709
+ )}
710
+ </Box>
711
+ </Box>
712
+
713
+ {/* ── Divider ───────────────────────────────────────────────────────── */}
714
+ <Flex
715
+ h={{ base: '20px', md: '8px' }}
716
+ flexShrink={0}
717
+ cursor="row-resize"
718
+ align="center"
719
+ justify="center"
720
+ bg="var(--bg-panel)"
721
+ borderY="1px solid"
722
+ borderColor="whiteAlpha.100"
723
+ _hover={{ bg: 'blue.900', borderColor: 'blue.500' }}
724
+ transition="all 0.2s"
725
+ onMouseDown={onDividerMouseDown}
726
+ onTouchStart={onDividerTouchStart}
727
+ role="separator"
728
+ sx={{ touchAction: 'none' }}
729
+ >
730
+ <Box w="48px" h="3px" bg={isDragging ? 'blue.400' : 'whiteAlpha.200'} borderRadius="full" />
731
+ </Flex>
732
+
733
+ {/* ── Bottom: Dependency graph ──────────────────────────────────────── */}
734
+ <Box
735
+ flex={1}
736
+ minH="120px"
737
+ display="flex"
738
+ alignItems="center"
739
+ justifyContent="center"
740
+ bg="var(--bg-canvas)"
741
+ backgroundImage="radial-gradient(circle, #2D3748 0.5px, transparent 0.5px)"
742
+ backgroundSize="24px 24px"
743
+ position="relative"
744
+ overflow="hidden"
745
+ >
746
+ {!selectedId ? (
747
+ <VStack spacing={3} opacity={0.55}>
748
+ <Box
749
+ p={4}
750
+ borderRadius="full"
751
+ bg="whiteAlpha.50"
752
+ border="1px dashed"
753
+ borderColor="whiteAlpha.150"
754
+ >
755
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" style={{ color: '#4A5568' }}>
756
+ <circle cx="12" cy="5" r="2" />
757
+ <circle cx="5" cy="19" r="2" />
758
+ <circle cx="19" cy="19" r="2" />
759
+ <line x1="12" y1="7" x2="5" y2="17" />
760
+ <line x1="12" y1="7" x2="19" y2="17" />
761
+ </svg>
762
+ </Box>
763
+ <VStack spacing={0.5}>
764
+ <Text color="gray.400" fontSize="sm" fontWeight="medium">Select an element to explore</Text>
765
+ <Text color="gray.600" fontSize="xs">Dependency graph appears here</Text>
766
+ </VStack>
767
+ </VStack>
768
+ ) : (
769
+ <Box
770
+ ref={graphRef}
771
+ key={selectedId}
772
+ w="full"
773
+ h="full"
774
+ position="relative"
775
+ style={{ cursor: 'grab' }}
776
+ onMouseDown={onCanvasMouseDown}
777
+ onTouchStart={onCanvasTouchStart}
778
+ sx={{ touchAction: 'none' }}
779
+ >
780
+ {/* Pannable inner container transform applied imperatively to avoid re-renders */}
781
+ <div ref={panContainerRef} style={{ position: 'absolute', inset: 0, overflow: 'visible' }}>
782
+ {/* Node layout */}
783
+ <motion.div
784
+ initial={{ opacity: 0, y: 6 }}
785
+ animate={{ opacity: 1, y: 0 }}
786
+ transition={{ duration: 0.22 }}
787
+ style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', zIndex: 1, padding: '40px' }}
788
+ >
789
+ {/* Three-column row: left group | center column | right group */}
790
+ <Flex direction="column" align="center">
791
+
792
+ {/* Top group */}
793
+ {topNodes.length > 0 && (
794
+ <Flex direction="column" align="center">
795
+ <VStack spacing={nodeSpacing} align="center">
796
+ {topRows.map((row, rowIndex) => (
797
+ <HStack key={`top-row-${rowIndex}`} spacing={nodeSpacing} align="flex-end">
798
+ {row.map((n) => (
799
+ <NeighbourCard
800
+ key={n.element.id}
801
+ node={n}
802
+ compactLevel={maxCompactLevel}
803
+ onClick={() => setSelectedId(n.element.id)}
804
+ />
805
+ ))}
806
+ </HStack>
807
+ ))}
808
+ </VStack>
809
+ <ConnectionIndicator position="top" compactLevel={maxCompactLevel} />
810
+ </Flex>
811
+ )}
812
+
813
+ {/* Middle Row: Left Group → Selected Node → Right Group */}
814
+ <Grid templateColumns="1fr auto 1fr" gap={colSpacing} alignItems="center" w="full">
815
+ {/* Left group */}
816
+ <Flex justify="flex-end">
817
+ {leftNodes.length > 0 && (
818
+ <Flex gap={nodeSpacing} align="center">
819
+ {leftColumns.map((column, columnIndex) => (
820
+ <VStack key={`left-column-${columnIndex}`} spacing={nodeSpacing} align="flex-end">
821
+ {column.map((n) => (
822
+ <NeighbourCard
823
+ key={n.element.id}
824
+ node={n}
825
+ compactLevel={leftCompactLevel}
826
+ onClick={() => setSelectedId(n.element.id)}
827
+ />
828
+ ))}
829
+ </VStack>
830
+ ))}
831
+ <ConnectionIndicator position="left" compactLevel={leftCompactLevel} />
832
+ </Flex>
833
+ )}
834
+ </Flex>
835
+
836
+ {/* Center: selected node */}
837
+ <Box position="relative" zIndex={10} isolation="isolate" data-pan-block="true">
838
+ <ElementContainer
839
+ isSelected
840
+ px={8}
841
+ py={6}
842
+ minW="220px"
843
+ maxW="300px"
844
+ bg={elementColor}
845
+ borderColor={accent}
846
+ borderWidth="2px"
847
+ boxShadow={selectedCardShadow}
848
+ >
849
+ <ElementBody
850
+ name={selectedElement?.name || ''}
851
+ type={selectedElement?.type || ''}
852
+ technology={selectedElement?.technology || undefined}
853
+ nameSize="md"
854
+ p={0}
855
+ >
856
+ <HStack mt={4} spacing={2}>
857
+ <Box w="6px" h="6px" borderRadius="full" bg={accent} />
858
+ <Text fontSize="xs" color={accent} fontWeight="bold">
859
+ {neighbourGraph.length} connection{neighbourGraph.length !== 1 ? 's' : ''}
860
+ </Text>
861
+ </HStack>
862
+ </ElementBody>
863
+ </ElementContainer>
864
+ </Box>
865
+
866
+ {/* Right group */}
867
+ <Flex justify="flex-start">
868
+ {rightNodes.length > 0 && (
869
+ <Flex gap={nodeSpacing} align="center">
870
+ <ConnectionIndicator position="right" compactLevel={rightCompactLevel} />
871
+ {rightColumns.map((column, columnIndex) => (
872
+ <VStack key={`right-column-${columnIndex}`} spacing={nodeSpacing} align="flex-start">
873
+ {column.map((n) => (
874
+ <NeighbourCard
875
+ key={n.element.id}
876
+ node={n}
877
+ compactLevel={rightCompactLevel}
878
+ onClick={() => setSelectedId(n.element.id)}
879
+ />
880
+ ))}
881
+ </VStack>
882
+ ))}
883
+ </Flex>
884
+ )}
885
+ </Flex>
886
+ </Grid>
887
+
888
+ {/* Bottom group */}
889
+ {bottomNodes.length > 0 && (
890
+ <Flex direction="column" align="center">
891
+ <ConnectionIndicator position="bottom" compactLevel={maxCompactLevel} />
892
+ <VStack spacing={nodeSpacing} align="center">
893
+ {bottomRows.map((row, rowIndex) => (
894
+ <HStack key={`bottom-row-${rowIndex}`} spacing={nodeSpacing} align="flex-start">
895
+ {row.map((n) => (
896
+ <NeighbourCard
897
+ key={n.element.id}
898
+ node={n}
899
+ compactLevel={maxCompactLevel}
900
+ onClick={() => setSelectedId(n.element.id)}
901
+ />
902
+ ))}
903
+ </HStack>
904
+ ))}
905
+ </VStack>
906
+ </Flex>
907
+ )}
908
+
909
+ {neighbourGraph.length === 0 && (
910
+ <Text color="gray.600" fontSize="sm" fontStyle="italic">
911
+ No direct connections found.
912
+ </Text>
913
+ )}
914
+
915
+ </Flex>
916
+ </motion.div>
917
+ </div>{/* end pannable inner container */}
918
+ </Box>
919
+ )}
920
+ </Box>
921
+ </Box>
922
+
923
+ <DependenciesOnboarding hasElements={elements.length > 0} />
924
+ </Box>
925
+ )
926
+ }