@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,853 @@
1
+ // src/components/ZUI/ZUICanvas.tsx
2
+
3
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, useCallback } from 'react'
4
+ import {
5
+ Box,
6
+ Text,
7
+ Icon,
8
+ Breadcrumb,
9
+ BreadcrumbItem,
10
+ BreadcrumbLink,
11
+ Popover,
12
+ PopoverTrigger,
13
+ PopoverContent,
14
+ PopoverBody,
15
+ PopoverHeader,
16
+ PopoverArrow,
17
+ Button,
18
+ VStack,
19
+ HStack,
20
+ Badge,
21
+ Divider,
22
+ Portal,
23
+ Image as ChakraImage,
24
+ useBreakpointValue,
25
+ } from '@chakra-ui/react'
26
+ import { Link as RouterLink } from 'react-router-dom'
27
+ import { ExternalLinkIcon } from '@chakra-ui/icons'
28
+ import type { ExploreData } from '../../types'
29
+ import { computeLayout } from './layout'
30
+ import { renderFrame, getExpandThresholds, setOnImageLoadCallback, setHighlightedTags as setRendererHighlightedTags, setHiddenTags as setRendererHiddenTags, setHighlightColor as setRendererHighlightColor } from './renderer'
31
+ import { useZUIInteraction } from './useZUIInteraction'
32
+ import type { DiagramGroupLayout, ZUIViewState } from './types'
33
+ import { buildWorkspaceGraphSnapshot } from '../../crossBranch/graph'
34
+ import type { CrossBranchContextSettings } from '../../crossBranch/types'
35
+ import type { ZUIResolvedConnector } from '../../crossBranch/resolve'
36
+ import { buildVisibleProxyConnectors, collectVisibleNodeAnchors, drawVisibleProxyConnectors, findHoveredProxyConnector } from './proxy'
37
+
38
+ export interface ZUICanvasHandle {
39
+ fitView(): void
40
+ }
41
+
42
+ interface Props {
43
+ data: ExploreData
44
+ onReady?: () => void
45
+ onZoom?: () => void
46
+ onPan?: () => void
47
+ highlightedTags?: string[]
48
+ highlightColor?: string
49
+ hiddenTags?: string[]
50
+ crossBranchSettings: CrossBranchContextSettings
51
+ hoverLocked?: boolean
52
+ }
53
+
54
+ interface PathItem {
55
+ id: string
56
+ label: string
57
+ type: 'group' | 'node'
58
+ isCircular?: boolean
59
+ // Absolute world coordinates for zooming
60
+ absX: number
61
+ absY: number
62
+ absW: number
63
+ absH: number
64
+ }
65
+
66
+ function getPathAt(
67
+ view: ZUIViewState,
68
+ groups: DiagramGroupLayout[],
69
+ canvasW: number,
70
+ canvasH: number,
71
+ ): PathItem[] {
72
+ if (canvasW === 0 || canvasH === 0) return []
73
+
74
+ // World center of the screen
75
+ const worldCenterX = (canvasW / 2 - view.x) / view.zoom
76
+ const worldCenterY = (canvasH / 2 - view.y) / view.zoom
77
+ const thresholds = getExpandThresholds(canvasW)
78
+
79
+ for (const group of groups) {
80
+ if (
81
+ worldCenterX >= group.worldX &&
82
+ worldCenterX <= group.worldX + group.worldW &&
83
+ worldCenterY >= group.worldY &&
84
+ worldCenterY <= group.worldY + group.worldH
85
+ ) {
86
+ const path: PathItem[] = [
87
+ {
88
+ id: `g-${group.diagramId}`,
89
+ label: group.label,
90
+ type: 'group',
91
+ absX: group.worldX,
92
+ absY: group.worldY,
93
+ absW: group.worldW,
94
+ absH: group.worldH,
95
+ },
96
+ ]
97
+
98
+ let currentNodes = group.nodes
99
+ let currentX = worldCenterX
100
+ let currentY = worldCenterY
101
+
102
+ // Track cumulative transform for children
103
+ // We start at 0 because root-level nodes already have absolute worldX/Y
104
+ let parentAbsX = 0
105
+ let parentAbsY = 0
106
+ let parentAbsScale = 1
107
+ let parentChildOffsetX = 0
108
+ let parentChildOffsetY = 0
109
+
110
+ while (true) {
111
+ let found = false
112
+ for (const node of currentNodes) {
113
+ if (
114
+ currentX >= node.worldX &&
115
+ currentX <= node.worldX + node.worldW &&
116
+ currentY >= node.worldY &&
117
+ currentY <= node.worldY + node.worldH
118
+ ) {
119
+ // Absolute position of this node
120
+ const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
121
+ const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
122
+ const absW = node.worldW * parentAbsScale
123
+ const absH = node.worldH * parentAbsScale
124
+
125
+ // Screen width of this node to check expansion
126
+ const screenW = absW * view.zoom
127
+
128
+ path.push({
129
+ id: node.id,
130
+ label: node.label,
131
+ type: 'node',
132
+ isCircular: node.isCircular,
133
+ absX,
134
+ absY,
135
+ absW,
136
+ absH,
137
+ })
138
+
139
+ // Only descend if the node is "expanded" enough on screen
140
+ // and has children. This makes the breadcrumb track the visual focus.
141
+ const isExpanded = screenW > thresholds.start * 1.1
142
+
143
+ if (isExpanded && node.children && node.children.length > 0) {
144
+ // Update parent context for next level
145
+ parentAbsX = absX
146
+ parentAbsY = absY
147
+ parentAbsScale = parentAbsScale * node.childScale
148
+ parentChildOffsetX = node.childOffsetX
149
+ parentChildOffsetY = node.childOffsetY
150
+
151
+ // Transform current center into child-local space
152
+ currentX = (currentX - node.worldX) / node.childScale + node.childOffsetX
153
+ currentY = (currentY - node.worldY) / node.childScale + node.childOffsetY
154
+ currentNodes = node.children
155
+ found = true
156
+ break
157
+ } else {
158
+ // Stop breadcrumb at the deepest visible/centered node
159
+ found = false
160
+ break
161
+ }
162
+ }
163
+ }
164
+ if (!found) break
165
+ }
166
+ return path
167
+ }
168
+ }
169
+ return []
170
+ }
171
+
172
+ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
173
+ const canvasRef = useRef<HTMLCanvasElement>(null)
174
+ const containerRef = useRef<HTMLDivElement>(null)
175
+ const [initialized, setInitialized] = useState(false)
176
+ const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
177
+ const isMobileLayout = useBreakpointValue({ base: true, md: false }) ?? false
178
+
179
+ // ── Layout ──────────────────────────────────────────────────────
180
+ const layout = useMemo(() => computeLayout(data), [data])
181
+ const workspaceSnapshot = useMemo(() => buildWorkspaceGraphSnapshot(data), [data])
182
+ // Holds the most-recently resolved connector topology so hover detection can
183
+ // use it without re-running the expensive O(connectors) resolution on every mousemove.
184
+ const proxyConnectorsRef = useRef<ZUIResolvedConnector[]>([])
185
+
186
+ const resolveHoveredProxyItem = useCallback((worldX: number, worldY: number, view: ZUIViewState, canvasW: number) => {
187
+ const freshAnchors = collectVisibleNodeAnchors(layout.groups, view, canvasW, hiddenTags)
188
+ return findHoveredProxyConnector(worldX, worldY, proxyConnectorsRef.current, freshAnchors.byNodeId, view)
189
+ }, [hiddenTags, layout.groups])
190
+
191
+ // ── Interaction ─────────────────────────────────────────────────
192
+ const { viewState, viewStateRef, setViewState, fitView, maxZoom, hoveredItem, setHoveredItem, setHoverLocked } = useZUIInteraction(
193
+ canvasRef,
194
+ undefined,
195
+ layout.groups,
196
+ layout.bbox,
197
+ onZoom,
198
+ onPan,
199
+ isMobileLayout,
200
+ resolveHoveredProxyItem,
201
+ )
202
+
203
+ // Anchor positions recompute every render (fast tree traversal, view-dependent).
204
+ const anchors = useMemo(() =>
205
+ collectVisibleNodeAnchors(layout.groups, viewState, containerSize.w || 1, hiddenTags),
206
+ [layout.groups, viewState, containerSize.w, hiddenTags],
207
+ )
208
+
209
+ // A stable string key encoding which element→nodeId pairs are currently visible.
210
+ // This only changes when nodes cross zoom-expansion thresholds not on every pan pixel.
211
+ const visibleElementSig = useMemo(() =>
212
+ Array.from(anchors.visibleAnchors.entries())
213
+ .sort(([a], [b]) => a - b)
214
+ .map(([id, anchor]) => `${id}:${anchor.nodeId}`)
215
+ .join(','),
216
+ [anchors.visibleAnchors],
217
+ )
218
+
219
+ // Connector topology: expensive O(connectors) resolution only when visibility set changes.
220
+ const proxyConnectors = useMemo(() => {
221
+ const resolved = buildVisibleProxyConnectors(workspaceSnapshot, anchors.visibleAnchors, crossBranchSettings)
222
+ proxyConnectorsRef.current = resolved
223
+ return resolved
224
+ // eslint-disable-next-line react-hooks/exhaustive-deps
225
+ }, [workspaceSnapshot, visibleElementSig, crossBranchSettings])
226
+
227
+ const visibleProxyState = useMemo(() => ({
228
+ ...anchors,
229
+ proxyConnectors,
230
+ }), [anchors, proxyConnectors])
231
+
232
+ const visibleProxyStateRef = useRef(visibleProxyState)
233
+ visibleProxyStateRef.current = visibleProxyState
234
+
235
+ const labelBgRef = useRef('#171923')
236
+ useEffect(() => {
237
+ const update = () => {
238
+ labelBgRef.current = getComputedStyle(document.documentElement).getPropertyValue('--chakra-colors-gray-900').trim() || '#171923'
239
+ needsRedrawRef.current = true
240
+ }
241
+ update()
242
+ const mo = new MutationObserver(update)
243
+ mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] })
244
+ return () => mo.disconnect()
245
+ }, [])
246
+
247
+ // ── Hierarchy Breadcrumb ────────────────────────────────────────
248
+ const hoveredScreenRect = useMemo(() => {
249
+ if (!hoveredItem) return null
250
+ let absX, absY, absW, absH
251
+ if (hoveredItem.type === 'node') {
252
+ ({ absX, absY, absW, absH } = hoveredItem)
253
+ } else if (hoveredItem.type === 'edge') {
254
+ ({ absX, absY } = hoveredItem)
255
+ absW = 0
256
+ absH = 0
257
+ } else {
258
+ const g = hoveredItem.data
259
+ // Target the label area (centered above diagram)
260
+ absW = 200 / viewState.zoom
261
+ absH = 50 / viewState.zoom
262
+ absX = g.worldX + g.diagramX + g.diagramW / 2 - absW / 2
263
+ absY = g.worldY + g.diagramY - absH
264
+ }
265
+
266
+ const sx = absX * viewState.zoom + viewState.x
267
+ const sy = absY * viewState.zoom + viewState.y
268
+ const sw = absW * viewState.zoom
269
+ const sh = absH * viewState.zoom
270
+
271
+ return { sx, sy, sw, sh }
272
+ }, [hoveredItem, viewState])
273
+
274
+ const isHoveredItemFullyVisible = useMemo(() => {
275
+ if (!hoveredScreenRect || containerSize.w === 0) return false
276
+ // A target is "fully visible" if its screen-space rect is entirely within viewport.
277
+ return (
278
+ hoveredScreenRect.sx >= 2 &&
279
+ hoveredScreenRect.sy >= 2 &&
280
+ hoveredScreenRect.sx + hoveredScreenRect.sw <= containerSize.w - 2 &&
281
+ hoveredScreenRect.sy + hoveredScreenRect.sh <= containerSize.h - 2
282
+ )
283
+ }, [hoveredScreenRect, containerSize])
284
+
285
+ // Debounce breadcrumb computation so getPathAt doesn't run on every scroll tick
286
+ const [breadcrumbView, setBreadcrumbView] = useState(viewState)
287
+ const breadcrumbTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
288
+ useEffect(() => {
289
+ if (breadcrumbTimerRef.current) clearTimeout(breadcrumbTimerRef.current)
290
+ breadcrumbTimerRef.current = setTimeout(() => setBreadcrumbView(viewState), 80)
291
+ return () => { if (breadcrumbTimerRef.current) clearTimeout(breadcrumbTimerRef.current) }
292
+ }, [viewState])
293
+
294
+ const currentPath = useMemo(() => {
295
+ return getPathAt(breadcrumbView, layout.groups, containerSize.w, containerSize.h)
296
+ }, [breadcrumbView, layout.groups, containerSize])
297
+
298
+ const zoomToPathItem = useCallback((item: PathItem) => {
299
+ if (containerSize.w === 0 || containerSize.h === 0) return
300
+ setHoveredItem(null, true) // Clear popover immediately on breadcrumb jump
301
+
302
+ // Use a comfortable padding for the focused item
303
+ const padding = 0.15
304
+ const bboxW = item.absW
305
+ const bboxH = item.absH
306
+
307
+ const zoom = Math.min(
308
+ (containerSize.w * (1 - padding * 2)) / bboxW,
309
+ (containerSize.h * (1 - padding * 2)) / bboxH,
310
+ maxZoom
311
+ )
312
+
313
+ // Center the item exactly
314
+ const x = (containerSize.w - bboxW * zoom) / 2 - item.absX * zoom
315
+ const y = (containerSize.h - bboxH * zoom) / 2 - item.absY * zoom
316
+
317
+ setViewState({ x, y, zoom })
318
+ }, [containerSize, maxZoom, setViewState, setHoveredItem])
319
+
320
+ // ── Fit view on mount and when layout changes ────────────────────
321
+ useEffect(() => {
322
+ const el = containerRef.current
323
+ if (!el) return
324
+ const { offsetWidth: w, offsetHeight: h } = el
325
+
326
+ // Only set as initialized if we have valid dimensions
327
+ if (w > 0 && h > 0) {
328
+ setContainerSize({ w, h })
329
+ fitView(w, h, layout.bbox)
330
+ if (!initialized) {
331
+ setInitialized(true)
332
+ onReady?.()
333
+ }
334
+ }
335
+ // eslint-disable-next-line react-hooks/exhaustive-deps
336
+ }, [layout, initialized, onReady])
337
+
338
+ // ── Expose fitView to parent ─────────────────────────────────────
339
+ useImperativeHandle(
340
+ ref,
341
+ () => ({
342
+ fitView() {
343
+ const el = containerRef.current
344
+ if (!el) return
345
+ setHoveredItem(null, true) // Clear popover immediately on fitView
346
+ fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
347
+ },
348
+ }),
349
+ [fitView, layout.bbox, setHoveredItem],
350
+ )
351
+
352
+ // ── RAF render loop ──────────────────────────────────────────────
353
+ // viewStateRef comes from useZUIInteraction updated synchronously on input events,
354
+ // so the RAF loop sees new state immediately without waiting for React to re-render.
355
+ const needsRedrawRef = useRef(true) // Force first frame
356
+
357
+ useEffect(() => {
358
+ setOnImageLoadCallback(() => {
359
+ needsRedrawRef.current = true
360
+ })
361
+ return () => setOnImageLoadCallback(null)
362
+ }, [])
363
+
364
+ useEffect(() => {
365
+ needsRedrawRef.current = true
366
+ }, [viewState])
367
+
368
+ useEffect(() => {
369
+ needsRedrawRef.current = true
370
+ }, [crossBranchSettings])
371
+
372
+ // ── HiDPI canvas resize ──────────────────────────────────────────
373
+ useEffect(() => {
374
+ const canvas = canvasRef.current
375
+ const container = containerRef.current
376
+ if (!canvas || !container) return
377
+
378
+ function resize() {
379
+ if (!canvas || !container) return
380
+ const dpr = window.devicePixelRatio || 1
381
+ const w = container.offsetWidth
382
+ const h = container.offsetHeight
383
+ if (w === 0 || h === 0) return
384
+
385
+ setContainerSize({ w, h })
386
+ canvas.width = w * dpr
387
+ canvas.height = h * dpr
388
+ canvas.style.width = `${w}px`
389
+ canvas.style.height = `${h}px`
390
+ const ctx = canvas.getContext('2d')
391
+ if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
392
+
393
+ needsRedrawRef.current = true // Canvas was cleared!
394
+
395
+ // Trigger initialization if it hasn't happened yet
396
+ if (!initialized && w > 0 && h > 0) {
397
+ fitView(w, h, layout.bbox)
398
+ setInitialized(true)
399
+ onReady?.()
400
+ }
401
+ }
402
+
403
+ const ro = new ResizeObserver(resize)
404
+ ro.observe(container)
405
+ resize()
406
+ return () => ro.disconnect()
407
+ }, [initialized, layout, fitView, onReady])
408
+
409
+ useEffect(() => {
410
+ if (!initialized) return // Don't start loop until initialized
411
+
412
+ const canvas = canvasRef.current
413
+ const container = containerRef.current
414
+ if (!canvas || !container) return
415
+
416
+ let rafId: number
417
+ let lastView = { x: NaN, y: NaN, zoom: NaN } // Force first draw
418
+
419
+ function frame() {
420
+ const ctx = canvas!.getContext('2d')
421
+ if (!ctx) { rafId = requestAnimationFrame(frame); return }
422
+
423
+ const dpr = window.devicePixelRatio || 1
424
+ const w = container!.offsetWidth
425
+ const h = container!.offsetHeight
426
+ if (w === 0 || h === 0) { rafId = requestAnimationFrame(frame); return }
427
+
428
+ const currentView = viewStateRef.current
429
+
430
+ // Only redraw when view changed (saves GPU on idle)
431
+ const changed =
432
+ lastView.x !== currentView.x ||
433
+ lastView.y !== currentView.y ||
434
+ lastView.zoom !== currentView.zoom ||
435
+ needsRedrawRef.current
436
+
437
+ if (changed) {
438
+ ctx.save()
439
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
440
+ const occupiedLabelRects = renderFrame(ctx, layout.groups, currentView, w, h)
441
+ ctx.save()
442
+ ctx.translate(currentView.x, currentView.y)
443
+ ctx.scale(currentView.zoom, currentView.zoom)
444
+ drawVisibleProxyConnectors(
445
+ ctx,
446
+ visibleProxyStateRef.current.proxyConnectors,
447
+ visibleProxyStateRef.current.byNodeId,
448
+ currentView.zoom,
449
+ labelBgRef.current,
450
+ occupiedLabelRects,
451
+ )
452
+ ctx.restore()
453
+ ctx.restore()
454
+ lastView = currentView
455
+ needsRedrawRef.current = false
456
+ }
457
+
458
+ rafId = requestAnimationFrame(frame)
459
+ }
460
+
461
+ rafId = requestAnimationFrame(frame)
462
+ return () => cancelAnimationFrame(rafId)
463
+ }, [initialized, layout, viewStateRef])
464
+
465
+ // Force draw when layout changes (though NaN in RAF loop should cover it)
466
+ useEffect(() => {
467
+ if (initialized) needsRedrawRef.current = true
468
+ }, [layout, initialized])
469
+
470
+ // Sync highlighted tags + color with renderer module
471
+ useEffect(() => {
472
+ setRendererHighlightedTags(new Set(highlightedTags ?? []))
473
+ setRendererHighlightColor(highlightColor ?? '')
474
+ needsRedrawRef.current = true
475
+ }, [highlightedTags, highlightColor])
476
+
477
+ // Sync hidden tags with renderer module
478
+ useEffect(() => {
479
+ setRendererHiddenTags(new Set(hiddenTags ?? []))
480
+ needsRedrawRef.current = true
481
+ }, [hiddenTags])
482
+
483
+ useEffect(() => {
484
+ setHoverLocked(hoverLocked)
485
+ }, [hoverLocked, setHoverLocked])
486
+
487
+ // Clear renderer state on unmount
488
+ useEffect(() => {
489
+ return () => {
490
+ setRendererHighlightedTags(new Set())
491
+ setRendererHighlightColor('')
492
+ setRendererHiddenTags(new Set())
493
+ }
494
+ }, [])
495
+
496
+ return (
497
+ <div
498
+ ref={containerRef}
499
+ style={{ width: '100%', height: '100%', overflow: 'hidden', position: 'relative' }}
500
+ >
501
+ <canvas
502
+ ref={canvasRef}
503
+ style={{
504
+ display: 'block',
505
+ width: '100%',
506
+ height: '100%',
507
+ opacity: initialized ? 1 : 0,
508
+ transition: 'opacity 0.2s ease-in',
509
+ touchAction: 'none',
510
+ }}
511
+ />
512
+
513
+ {/* Breadcrumb Overlay */}
514
+ {initialized && currentPath.length > 0 && (
515
+ <Box
516
+ position="absolute"
517
+ top={isMobileLayout ? "66px" : 4}
518
+ left={4}
519
+ zIndex={10}
520
+ className="glass"
521
+ borderRadius="lg"
522
+ px={3}
523
+ py={1.5}
524
+ pointerEvents="auto"
525
+ >
526
+ <Breadcrumb
527
+ spacing="8px"
528
+ separator={<Text color="whiteAlpha.400" fontSize="xs">/</Text>}
529
+ >
530
+ {currentPath.map((item, idx) => (
531
+ <BreadcrumbItem key={item.id} isCurrentPage={idx === currentPath.length - 1}>
532
+ <BreadcrumbLink
533
+ onClick={() => zoomToPathItem(item)}
534
+ color={idx === currentPath.length - 1 ? "var(--accent)" : "gray.400"}
535
+ fontSize="xs"
536
+ fontWeight={idx === currentPath.length - 1 ? "600" : "normal"}
537
+ _hover={{ color: "var(--accent)", textDecoration: "none" }}
538
+ display="flex"
539
+ alignItems="center"
540
+ gap={1.5}
541
+ >
542
+ {item.type === 'group' && (
543
+ <Icon viewBox="0 0 24 24" boxSize={3} fill="none" stroke="currentColor" strokeWidth="2">
544
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
545
+ <polyline points="9 22 9 12 15 12 15 22" />
546
+ </Icon>
547
+ )}
548
+ {item.isCircular && (
549
+ <Icon viewBox="0 0 24 24" boxSize={3.5} fill="none" stroke="currentColor" strokeWidth="3.5">
550
+ <path d="M20 4l-4 4 4 4" />
551
+ <path d="M16 8h-4a8 8 0 1 0 8 8" />
552
+ </Icon>
553
+ )}
554
+ {item.label}
555
+ </BreadcrumbLink>
556
+ </BreadcrumbItem>
557
+ ))}
558
+ </Breadcrumb>
559
+ {currentPath[currentPath.length - 1]?.isCircular && (
560
+ <Text mt={1.5} color="var(--accent)" fontSize="2xs" fontWeight="500" letterSpacing="wide">
561
+ CIRCULAR LINK - CLICK BREADCRUMB TO JUMP BACK
562
+ </Text>
563
+ )}
564
+ </Box>
565
+ )}
566
+
567
+ {/* Hover metadata card */}
568
+ <Popover
569
+ isOpen={isHoveredItemFullyVisible}
570
+ placement="right-start"
571
+ closeOnBlur={false}
572
+ gutter={12}
573
+ isLazy
574
+ >
575
+ <PopoverTrigger>
576
+ <Box
577
+ position="absolute"
578
+ left={hoveredScreenRect?.sx ?? 0}
579
+ top={hoveredScreenRect?.sy ?? 0}
580
+ width={hoveredScreenRect?.sw ?? 0}
581
+ height={hoveredScreenRect?.sh ?? 0}
582
+ pointerEvents="none"
583
+ />
584
+ </PopoverTrigger>
585
+ <Portal>
586
+ <PopoverContent
587
+ bg="gray.900"
588
+ borderColor="whiteAlpha.300"
589
+ boxShadow="2xl"
590
+ width="280px"
591
+ _focus={{ boxShadow: 'none' }}
592
+ pointerEvents="auto"
593
+ onMouseEnter={() => setHoverLocked(true)}
594
+ onMouseLeave={() => setHoverLocked(false)}
595
+ >
596
+ <PopoverArrow bg="gray.900" />
597
+ {hoveredItem?.type === 'node' && (
598
+ <>
599
+ <PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
600
+ <HStack spacing={3}>
601
+ {hoveredItem.data.logoUrl && (
602
+ <Box flexShrink={0}>
603
+ <ChakraImage src={hoveredItem.data.logoUrl} boxSize="24px" objectFit="contain" />
604
+ </Box>
605
+ )}
606
+ <VStack align="start" spacing={0} flex={1} overflow="hidden">
607
+ <Text fontWeight="600" fontSize="sm" isTruncated width="100%" color="white">
608
+ {hoveredItem.data.label}
609
+ </Text>
610
+ <Badge colorScheme={hoveredItem.data.isPortal ? "purple" : "blue"} variant="subtle" fontSize="2xs">
611
+ {hoveredItem.data.isPortal ? "Portal" : hoveredItem.data.type}
612
+ </Badge>
613
+ </VStack>
614
+ </HStack>
615
+ </PopoverHeader>
616
+ <PopoverBody px={4} py={3}>
617
+ <VStack align="start" spacing={3}>
618
+ {hoveredItem.data.technology && (
619
+ <Box>
620
+ <Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">TECHNOLOGY</Text>
621
+ <Text fontSize="xs" color="gray.200">{hoveredItem.data.technology}</Text>
622
+ </Box>
623
+ )}
624
+ {hoveredItem.data.description && (
625
+ <Box>
626
+ <Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">DESCRIPTION</Text>
627
+ <Text fontSize="xs" color="gray.200" noOfLines={4}>{hoveredItem.data.description}</Text>
628
+ </Box>
629
+ )}
630
+ {hoveredItem.data.linkedDiagramId && (
631
+ <Box>
632
+ <Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">LINKS TO</Text>
633
+ <Text fontSize="xs" color="teal.300" fontWeight="500">
634
+ ⊞ {hoveredItem.data.linkedDiagramLabel}
635
+ </Text>
636
+ </Box>
637
+ )}
638
+ <Divider borderColor="whiteAlpha.200" />
639
+ <Button
640
+ as={RouterLink}
641
+ to={hoveredItem.data.isPortal
642
+ ? `/views/${hoveredItem.data.linkedDiagramId}`
643
+ : `/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.elementId}`
644
+ }
645
+ size="xs"
646
+ colorScheme="teal"
647
+ variant="solid"
648
+ width="full"
649
+ rightIcon={<ExternalLinkIcon />}
650
+ onClick={(e) => e.stopPropagation()}
651
+ >
652
+ {hoveredItem.data.isPortal ? 'Open Diagram' : 'Open in Editor'}
653
+ </Button>
654
+ </VStack>
655
+ </PopoverBody>
656
+ </>
657
+ )}
658
+ {hoveredItem?.type === 'edge' && hoveredItem.data.isProxy && hoveredItem.data.details && (
659
+ <>
660
+ <PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
661
+ <VStack align="start" spacing={0}>
662
+ <Text fontWeight="600" fontSize="sm" color="white">
663
+ Cross-Branch Connector
664
+ </Text>
665
+ <Badge colorScheme="blue" variant="subtle" fontSize="2xs">
666
+ {hoveredItem.data.details.count} connector{hoveredItem.data.details.count === 1 ? '' : 's'}
667
+ </Badge>
668
+ </VStack>
669
+ </PopoverHeader>
670
+ <PopoverBody px={4} py={3}>
671
+ <VStack align="start" spacing={3}>
672
+ <VStack align="start" spacing={1}>
673
+ <Text color="gray.400" fontSize="2xs" fontWeight="600" letterSpacing="wider">BETWEEN</Text>
674
+ <Text fontSize="xs" color="gray.200">
675
+ {hoveredItem.data.details.sourceAnchorName} &rarr; {hoveredItem.data.details.targetAnchorName}
676
+ </Text>
677
+ <Text fontSize="xs" color="gray.400">{hoveredItem.data.details.label}</Text>
678
+ </VStack>
679
+ <Divider borderColor="whiteAlpha.200" />
680
+ <VStack align="stretch" spacing={2} width="full">
681
+ {hoveredItem.data.details.ownerViewIds.map((ownerViewId, index) => (
682
+ <Button
683
+ key={`${ownerViewId}-${index}`}
684
+ as={RouterLink}
685
+ to={`/views/${ownerViewId}`}
686
+ size="xs"
687
+ colorScheme="gray"
688
+ variant="solid"
689
+ width="full"
690
+ justifyContent="space-between"
691
+ rightIcon={<ExternalLinkIcon />}
692
+ onClick={(e) => e.stopPropagation()}
693
+ >
694
+ {hoveredItem.data.details!.ownerViewNames[index] ?? `Open View ${ownerViewId}`}
695
+ </Button>
696
+ ))}
697
+ </VStack>
698
+ <Divider borderColor="whiteAlpha.200" />
699
+ <HStack width="full" spacing={2}>
700
+ <Button
701
+ as={RouterLink}
702
+ to={`/views/${hoveredItem.data.details!.connectors[0]?.source.anchorViewId ?? hoveredItem.data.diagramId}?element=${hoveredItem.data.sourceObjId}`}
703
+ size="xs"
704
+ colorScheme="gray"
705
+ variant="solid"
706
+ flex={1}
707
+ rightIcon={<ExternalLinkIcon />}
708
+ onClick={(e) => e.stopPropagation()}
709
+ >
710
+ Open Source
711
+ </Button>
712
+ <Button
713
+ as={RouterLink}
714
+ to={`/views/${hoveredItem.data.details!.connectors[0]?.target.anchorViewId ?? hoveredItem.data.diagramId}?element=${hoveredItem.data.targetObjId}`}
715
+ size="xs"
716
+ colorScheme="teal"
717
+ variant="solid"
718
+ flex={1}
719
+ rightIcon={<ExternalLinkIcon />}
720
+ onClick={(e) => e.stopPropagation()}
721
+ >
722
+ Open Target
723
+ </Button>
724
+ </HStack>
725
+ </VStack>
726
+ </PopoverBody>
727
+ </>
728
+ )}
729
+ {hoveredItem?.type === 'edge' && !hoveredItem.data.isProxy && (
730
+ <>
731
+ <PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
732
+ <VStack align="start" spacing={0}>
733
+ <Text fontWeight="600" fontSize="sm" color="white">
734
+ {hoveredItem.data.label}
735
+ </Text>
736
+ <Badge colorScheme={hoveredItem.data.isPortalConn ? "purple" : "orange"} variant="subtle" fontSize="2xs">
737
+ {hoveredItem.data.isPortalConn ? "Portal Connection" : "Connection"}
738
+ </Badge>
739
+ </VStack>
740
+ </PopoverHeader>
741
+ <PopoverBody px={4} py={3}>
742
+ <VStack align="start" spacing={3}>
743
+ <VStack align="start" spacing={1}>
744
+ <Text color="gray.400" fontSize="2xs" fontWeight="600" letterSpacing="wider">BETWEEN</Text>
745
+ <Text fontSize="xs" color="gray.200">{hoveredItem.data.sourceId} & {hoveredItem.data.targetId}</Text>
746
+ </VStack>
747
+ <Divider borderColor="whiteAlpha.200" />
748
+
749
+ {hoveredItem.data.isPortalConn ? (
750
+ <>
751
+ <Button
752
+ as={RouterLink}
753
+ to={`/views/${hoveredItem.data.diagramId}`}
754
+ size="xs"
755
+ colorScheme="gray"
756
+ variant="solid"
757
+ width="full"
758
+ rightIcon={<ExternalLinkIcon />}
759
+ onClick={(e) => e.stopPropagation()}
760
+ >
761
+ Open {hoveredItem.data.sourceId}
762
+ </Button>
763
+ <Button
764
+ as={RouterLink}
765
+ to={`/views/${hoveredItem.data.targetDiagId}`}
766
+ size="xs"
767
+ colorScheme="teal"
768
+ variant="solid"
769
+ width="full"
770
+ rightIcon={<ExternalLinkIcon />}
771
+ onClick={(e) => e.stopPropagation()}
772
+ >
773
+ Open {hoveredItem.data.targetId}
774
+ </Button>
775
+ </>
776
+ ) : (
777
+ <>
778
+ <Button
779
+ as={RouterLink}
780
+ to={`/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.sourceObjId}`}
781
+ size="xs"
782
+ colorScheme="gray"
783
+ variant="solid"
784
+ width="full"
785
+ rightIcon={<ExternalLinkIcon />}
786
+ onClick={(e) => e.stopPropagation()}
787
+ >
788
+ Go to {hoveredItem.data.sourceId}
789
+ </Button>
790
+ <Button
791
+ as={RouterLink}
792
+ to={`/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.targetObjId}`}
793
+ size="xs"
794
+ colorScheme="teal"
795
+ variant="solid"
796
+ width="full"
797
+ rightIcon={<ExternalLinkIcon />}
798
+ onClick={(e) => e.stopPropagation()}
799
+ >
800
+ Go to {hoveredItem.data.targetId}
801
+ </Button>
802
+ </>
803
+ )}
804
+ </VStack>
805
+ </PopoverBody>
806
+ </>
807
+ )}
808
+ {hoveredItem?.type === 'group' && (
809
+ <>
810
+ <PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
811
+ <VStack align="start" spacing={0}>
812
+ <Text fontWeight="600" fontSize="sm" color="white">
813
+ {hoveredItem.data.label}
814
+ </Text>
815
+ <Badge colorScheme="purple" variant="subtle" fontSize="2xs">
816
+ Diagram Group
817
+ </Badge>
818
+ </VStack>
819
+ </PopoverHeader>
820
+ <PopoverBody px={4} py={3}>
821
+ <VStack align="start" spacing={3}>
822
+ {hoveredItem.data.description && (
823
+ <Box>
824
+ <Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">DESCRIPTION</Text>
825
+ <Text fontSize="xs" color="gray.200" noOfLines={4}>{hoveredItem.data.description}</Text>
826
+ </Box>
827
+ )}
828
+ <Text fontSize="xs" color="gray.300">
829
+ Root level diagram containing {hoveredItem.data.nodes.length} elements and {hoveredItem.data.edges.length} connections.
830
+ </Text>
831
+ <Divider borderColor="whiteAlpha.200" />
832
+ <Button
833
+ as={RouterLink}
834
+ to={`/views/${hoveredItem.data.diagramId}`}
835
+ size="xs"
836
+ colorScheme="teal"
837
+ variant="solid"
838
+ width="full"
839
+ rightIcon={<ExternalLinkIcon />}
840
+ onClick={(e) => e.stopPropagation()}
841
+ >
842
+ Open Diagram
843
+ </Button>
844
+ </VStack>
845
+ </PopoverBody>
846
+ </>
847
+ )}
848
+ </PopoverContent>
849
+ </Portal>
850
+ </Popover>
851
+ </div>
852
+ )
853
+ })