@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,491 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { MarkerType, type Edge as RFEdge, type Node as RFNode } from 'reactflow'
3
+ import { api } from '../../../api/client'
4
+ import type {
5
+ ViewTreeNode,
6
+ PlacedElement,
7
+ LibraryElement,
8
+ Connector,
9
+ IncomingViewConnector,
10
+ ViewConnector,
11
+ Tag,
12
+ } from '../../../types'
13
+ import {
14
+ DEFAULT_SOURCE_HANDLE_SIDE,
15
+ DEFAULT_TARGET_HANDLE_SIDE,
16
+ getLogicalHandleId,
17
+ getVisualHandleIdForGroup,
18
+ getVisualHandleSlot,
19
+ } from '../../../utils/edgeDistribution'
20
+
21
+ interface ViewDataOptions {
22
+ viewId: number | null
23
+ interactionSourceId: number | null
24
+ clickConnectMode: { sourceNodeId: string; sourceHandle: string; targetHandle?: string } | null
25
+ selectedEdgeId: number | null
26
+ activeTags: string[]
27
+ hiddenLayerTags: string[]
28
+ hoveredLayerTags: string[] | null
29
+ hoveredLayerColor: string | null
30
+ tagColors: Record<string, Tag>
31
+ // Node-level callbacks (stable refs from parent)
32
+ stableOnZoomIn: (elementId: number) => Promise<void>
33
+ stableOnZoomOut: (elementId: number) => Promise<void>
34
+ stableOnNavigateToView: (id: number) => void
35
+ stableOnSelect: (obj: PlacedElement) => void
36
+ stableOnOpenCodePreview: (elementId: number) => void
37
+ stableOnInteractionStart: (elementId: number) => void
38
+ stableOnConnectTo: (targetElementId: number) => Promise<void>
39
+ stableOnStartHandleReconnect: (args: { edgeId: string; endpoint: 'source' | 'target'; handleId: string; clientX: number; clientY: number }) => void
40
+ stableOnRemoveElement: (elementId: number) => Promise<void>
41
+ stableOnHoverZoom: (elementId: number, type: 'in' | 'out' | null) => void
42
+ hoveredZoomRef: React.MutableRefObject<{ elementId: number | null; type: 'in' | 'out' | null } | null>
43
+ }
44
+
45
+ function alphaColor(color: string, opacity: number): string {
46
+ if (opacity >= 1) return color
47
+ return `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, transparent)`
48
+ }
49
+
50
+ export function useViewData({
51
+ viewId,
52
+ interactionSourceId,
53
+ clickConnectMode,
54
+ selectedEdgeId,
55
+ activeTags,
56
+ hiddenLayerTags,
57
+ hoveredLayerTags,
58
+ hoveredLayerColor,
59
+ tagColors,
60
+ stableOnZoomIn,
61
+ stableOnZoomOut,
62
+ stableOnNavigateToView,
63
+ stableOnSelect,
64
+ stableOnOpenCodePreview,
65
+ stableOnInteractionStart,
66
+ stableOnConnectTo,
67
+ stableOnStartHandleReconnect,
68
+ stableOnRemoveElement,
69
+ stableOnHoverZoom,
70
+ hoveredZoomRef,
71
+ }: ViewDataOptions) {
72
+ const [view, setView] = useState<ViewTreeNode | null | undefined>(undefined)
73
+ const [viewElements, setViewElements] = useState<PlacedElement[]>([])
74
+ const [connectors, setConnectors] = useState<Connector[]>([])
75
+ const [rfNodes, setRfNodes] = useState<RFNode[]>([])
76
+ const [rfEdges, setRfEdges] = useState<RFEdge[]>([])
77
+ const [linksMap, setLinksMap] = useState<Record<number, ViewConnector[]>>({})
78
+ const [parentLinksMap, setParentLinksMap] = useState<Record<number, ViewConnector[]>>({})
79
+ const [incomingLinks, setIncomingLinks] = useState<IncomingViewConnector[]>([])
80
+ const [treeData, setTreeData] = useState<ViewTreeNode[]>([])
81
+ const [allElements, setAllElements] = useState<LibraryElement[]>([])
82
+ const [libraryRefresh, setLibraryRefresh] = useState(0)
83
+
84
+ // Mutable refs for stable callbacks
85
+ const viewElementsRef = useRef(viewElements)
86
+ viewElementsRef.current = viewElements
87
+ const linksMapRef = useRef(linksMap)
88
+ linksMapRef.current = linksMap
89
+ const parentLinksMapRef = useRef(parentLinksMap)
90
+ parentLinksMapRef.current = parentLinksMap
91
+ const incomingLinksRef = useRef(incomingLinks)
92
+ incomingLinksRef.current = incomingLinks
93
+ const treeDataRef = useRef(treeData)
94
+ treeDataRef.current = treeData
95
+ const rfNodesRef = useRef(rfNodes)
96
+ rfNodesRef.current = rfNodes
97
+ const rfEdgesRef = useRef(rfEdges)
98
+ rfEdgesRef.current = rfEdges
99
+ const viewIdRef = useRef(viewId)
100
+ viewIdRef.current = viewId
101
+
102
+ // ── Fetch tree ─────────────────────────────────────────────────────────────
103
+ const refreshGrid = useCallback(async () => {
104
+ const tree = await api.workspace.views.tree().catch(() => null)
105
+ if (tree) setTreeData(tree)
106
+ }, [])
107
+
108
+ // ── Fetch view content ──────────────────────────────────────────────────
109
+ useEffect(() => {
110
+ if (viewId === null) return
111
+ let active = true
112
+
113
+ const load = async () => {
114
+ try {
115
+ const [diag, content, tree] = await Promise.all([
116
+ api.workspace.views.get(viewId),
117
+ api.workspace.views.content(viewId),
118
+ api.workspace.views.tree(),
119
+ ])
120
+ if (!active) return
121
+
122
+ const safeObjs = content.placements || []
123
+ const safeConnectors = content.connectors || []
124
+
125
+ const linksObj: Record<number, ViewConnector[]> = {}
126
+ const parentLinksObj: Record<number, ViewConnector[]> = {}
127
+
128
+ // Helper: recursively find nodes in tree that are owned by elements on canvas (zoom-in)
129
+ // OR the parent view of the current view (zoom-out)
130
+ const findViewByOwner = (nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null => {
131
+ for (const node of nodes) {
132
+ if (node.owner_element_id !== null && Number(node.owner_element_id) === Number(elementId)) return node
133
+ const found = findViewByOwner(node.children, elementId)
134
+ if (found) return found
135
+ }
136
+ return null
137
+ }
138
+
139
+ const findViewPath = (nodes: ViewTreeNode[], targetId: number, path: ViewTreeNode[] = []): ViewTreeNode[] | null => {
140
+ for (const node of nodes) {
141
+ if (node.id === targetId) return [...path, node]
142
+ const found = findViewPath(node.children, targetId, [...path, node])
143
+ if (found) return found
144
+ }
145
+ return null
146
+ }
147
+
148
+ const viewPath = findViewPath(tree, viewId)
149
+ const parentView = viewPath && viewPath.length > 1 ? viewPath[viewPath.length - 2] : null
150
+ const currentViewInTree = viewPath ? viewPath[viewPath.length - 1] : null
151
+
152
+ const incoming: IncomingViewConnector[] = []
153
+ if (parentView && currentViewInTree?.owner_element_id) {
154
+ incoming.push({
155
+ id: 0,
156
+ element_id: currentViewInTree.owner_element_id,
157
+ element_name: 'Parent', // Optional: could find name in parentView.placements
158
+ from_view_id: parentView.id,
159
+ from_view_name: parentView.name,
160
+ to_view_id: viewId,
161
+ })
162
+ }
163
+
164
+ for (const obj of safeObjs) {
165
+ // Child Link: if there exists a view owned by this element
166
+ const childView = findViewByOwner(tree, obj.element_id)
167
+ if (childView) {
168
+ linksObj[obj.element_id] = [{
169
+ id: 0,
170
+ element_id: obj.element_id,
171
+ from_view_id: viewId,
172
+ to_view_id: childView.id,
173
+ to_view_name: childView.name,
174
+ relation_type: 'child',
175
+ }]
176
+ }
177
+
178
+ // Parent Link: all elements in a view can 'zoom out' to its structural parent
179
+ if (parentView) {
180
+ parentLinksObj[obj.element_id] = [{
181
+ id: 0,
182
+ element_id: obj.element_id,
183
+ from_view_id: parentView.id, // we go TO the parentView, coming FROM the parentView context?
184
+ to_view_id: parentView.id,
185
+ to_view_name: parentView.name,
186
+ relation_type: 'parent',
187
+ }]
188
+ }
189
+ }
190
+
191
+ setLinksMap(linksObj)
192
+ setParentLinksMap(parentLinksObj)
193
+ setConnectors(safeConnectors)
194
+ setViewElements(safeObjs)
195
+ setIncomingLinks(incoming)
196
+ setView(diag)
197
+ setTreeData(tree)
198
+ } catch (err) {
199
+ console.error('DIAGRAM EDITOR LOAD ERROR:', err)
200
+ if (active) setView(null)
201
+ }
202
+ }
203
+
204
+ load()
205
+ return () => { active = false }
206
+ }, [viewId])
207
+
208
+ // ── Clear canvas on navigation ─────────────────────────────────────────────
209
+ useEffect(() => {
210
+ setRfNodes([])
211
+ setRfEdges([])
212
+ }, [viewId])
213
+
214
+ // ── Keep all-org elements for inline adder ──────────────────────────────────
215
+ useEffect(() => {
216
+ api.elements.list().then(setAllElements).catch(() => { /* intentionally empty */ })
217
+ }, [libraryRefresh])
218
+
219
+ // ── Refresh elements ────────────────────────────────────────────────────────
220
+ const refreshElements = useCallback(async () => {
221
+ if (viewId === null) return
222
+ const fresh = await api.workspace.views.content(viewId).catch(() => null)
223
+ if (fresh) setViewElements(fresh.placements)
224
+ }, [viewId])
225
+
226
+ // ── CRDT-aware element mutation helpers ────────────────────────────────────
227
+ const handleElementDeleted = useCallback((deletedId: number) => {
228
+ setViewElements((prev) => prev.filter((o) => o.element_id !== deletedId))
229
+ }, [])
230
+
231
+ const handleElementPermanentlyDeleted = useCallback((deletedId: number) => {
232
+ setViewElements((prev) => prev.filter((o) => o.element_id !== deletedId))
233
+ setLibraryRefresh((n) => n + 1)
234
+ }, [])
235
+
236
+ const handleElementSaved = useCallback((saved: LibraryElement) => {
237
+ setLibraryRefresh((n) => n + 1)
238
+ setViewElements((prev) =>
239
+ prev.map((o) =>
240
+ o.element_id === saved.id
241
+ ? {
242
+ ...o,
243
+ name: saved.name,
244
+ description: saved.description,
245
+ kind: saved.kind,
246
+ technology: saved.technology,
247
+ url: saved.url,
248
+ logo_url: saved.logo_url,
249
+ technology_connectors: saved.technology_connectors,
250
+ tags: saved.tags,
251
+ }
252
+ : o,
253
+ ),
254
+ )
255
+ }, [])
256
+
257
+ // ── Stable element ID set ───────────────────────────────────────────────────
258
+ const existingElementIdsRef = useRef<Set<number>>(new Set())
259
+ const existingElementIds = useMemo(() => {
260
+ const nextIds = new Set(viewElements.map((o) => o.element_id))
261
+ const prevIds = existingElementIdsRef.current
262
+ if (nextIds.size === prevIds.size) {
263
+ let changed = false
264
+ for (const id of nextIds) { if (!prevIds.has(id)) { changed = true; break } }
265
+ if (!changed) return prevIds
266
+ }
267
+ existingElementIdsRef.current = nextIds
268
+ return nextIds
269
+ }, [viewElements])
270
+
271
+ // ── Derive RF nodes ────────────────────────────────────────────────────────
272
+ useEffect(() => {
273
+ setRfNodes((prevNodes) => {
274
+ // The structural parent of the current view is stored under the view's
275
+ // owner element ID which differs from the IDs of elements placed inside
276
+ // the view. Pre-compute it so every node in the view shows the correct
277
+ // zoom-out state even when their own element_id has no direct parent link.
278
+ const viewParentLinks = Object.values(parentLinksMap).flat()
279
+ const findInTreeById = (nodes: ViewTreeNode[], id: number): ViewTreeNode | null => {
280
+ for (const node of nodes) {
281
+ if (node.id === id) return node
282
+ const found = findInTreeById(node.children, id)
283
+ if (found) return found
284
+ }
285
+ return null
286
+ }
287
+ const currentView = findInTreeById(treeData, viewId || -1)
288
+ const parentViewId = currentView?.parent_view_id
289
+
290
+ return viewElements.map((obj) => {
291
+ const existing = prevNodes.find((n) => n.id === String(obj.element_id))
292
+ const objTags = obj.tags || []
293
+ const isHiddenByLayer = hiddenLayerTags.length > 0 && objTags.some((t) => hiddenLayerTags.includes(t))
294
+ const isInactive = isHiddenByLayer || (activeTags.length > 0 && !objTags.some((t) => activeTags.includes(t)))
295
+ const isLayerHighlighted = hoveredLayerTags !== null && objTags.some((t) => hoveredLayerTags.includes(t))
296
+ const isSoftFocused = hoveredLayerTags !== null && !isLayerHighlighted
297
+
298
+ return {
299
+ id: String(obj.element_id),
300
+ type: 'elementNode',
301
+ position: existing?.dragging ? existing.position : { x: obj.position_x ?? 0, y: obj.position_y ?? 0 },
302
+ width: existing?.width,
303
+ height: existing?.height,
304
+ selected: existing?.selected,
305
+ dragging: existing?.dragging,
306
+ zIndex: isLayerHighlighted ? 10 : interactionSourceId === obj.element_id ? 1000 : 0,
307
+ style: isInactive
308
+ ? { opacity: 0.1, pointerEvents: 'none' }
309
+ : isSoftFocused
310
+ ? { opacity: 0.2 }
311
+ : undefined,
312
+ data: {
313
+ ...obj,
314
+ links: linksMap[obj.element_id] || [],
315
+ parentLinks: parentLinksMap[obj.element_id] || viewParentLinks,
316
+ parentViewId,
317
+ onZoomIn: stableOnZoomIn,
318
+ onZoomOut: stableOnZoomOut,
319
+ onNavigateToView: stableOnNavigateToView,
320
+ onSelect: stableOnSelect,
321
+ onOpenCodePreview: stableOnOpenCodePreview,
322
+ onInteractionStart: stableOnInteractionStart,
323
+ onConnectTo: stableOnConnectTo,
324
+ onStartHandleReconnect: stableOnStartHandleReconnect,
325
+ onRemove: stableOnRemoveElement,
326
+ onHoverZoom: stableOnHoverZoom,
327
+ isZoomHovered: hoveredZoomRef.current?.elementId === obj.element_id ? hoveredZoomRef.current.type : null,
328
+ interactionSourceId,
329
+ isClickConnectMode: clickConnectMode !== null,
330
+ tagColors,
331
+ layerHighlightColor: isLayerHighlighted ? (hoveredLayerColor ?? undefined) : undefined,
332
+ forceShowTagPopup: isLayerHighlighted,
333
+ },
334
+ }
335
+ })
336
+ })
337
+ }, [
338
+ viewElements, viewId, linksMap, parentLinksMap, treeData,
339
+ interactionSourceId, clickConnectMode,
340
+ stableOnZoomIn, stableOnZoomOut, stableOnNavigateToView, stableOnSelect,
341
+ stableOnInteractionStart, stableOnConnectTo, stableOnStartHandleReconnect, stableOnRemoveElement, stableOnHoverZoom,
342
+ stableOnOpenCodePreview, hoveredZoomRef, activeTags, hiddenLayerTags, hoveredLayerTags, hoveredLayerColor, tagColors,
343
+ ])
344
+
345
+ // ── Derive RF connectors ────────────────────────────────────────────────────────
346
+ useEffect(() => {
347
+ const visibleElementIds = new Set(viewElements.map((element) => element.element_id))
348
+ const filtered = connectors.filter((connector) => visibleElementIds.has(connector.source_element_id) && visibleElementIds.has(connector.target_element_id))
349
+
350
+ const handleUsage: Record<string, { id: string; type: 'source' | 'target'; otherNodeCoord: number }[]> = {}
351
+ filtered.forEach((c) => {
352
+ const srcNode = viewElements.find((o) => o.element_id === c.source_element_id)
353
+ const tgtNode = viewElements.find((o) => o.element_id === c.target_element_id)
354
+ if (!srcNode || !tgtNode) return
355
+
356
+ const sourceSide = getLogicalHandleId(c.source_handle, DEFAULT_SOURCE_HANDLE_SIDE)
357
+ const targetSide = getLogicalHandleId(c.target_handle, DEFAULT_TARGET_HANDLE_SIDE)
358
+
359
+ const srcKey = `${c.source_element_id}-${sourceSide}`
360
+ handleUsage[srcKey] ??= []
361
+ const srcCoord = (sourceSide === 'left' || sourceSide === 'right') ? (tgtNode.position_y ?? 0) : (tgtNode.position_x ?? 0)
362
+ handleUsage[srcKey].push({ id: String(c.id), type: 'source', otherNodeCoord: srcCoord })
363
+
364
+ const tgtKey = `${c.target_element_id}-${targetSide}`
365
+ handleUsage[tgtKey] ??= []
366
+ const tgtCoord = (targetSide === 'left' || targetSide === 'right') ? (srcNode.position_y ?? 0) : (srcNode.position_x ?? 0)
367
+ handleUsage[tgtKey].push({ id: String(c.id), type: 'target', otherNodeCoord: tgtCoord })
368
+ })
369
+
370
+ Object.values(handleUsage).forEach((usages) => {
371
+ usages.sort((a, b) => a.otherNodeCoord - b.otherNodeCoord)
372
+ })
373
+
374
+
375
+ setRfEdges((prevConnectors) =>
376
+ filtered.map((e) => {
377
+ const existing = prevConnectors.find((re) => re.id === String(e.id))
378
+ const dir = e.direction ?? 'forward'
379
+ const arrowMarker = { type: MarkerType.ArrowClosed, width: 14, height: 14 }
380
+
381
+ const sourceObj = viewElements.find((o) => o.element_id === e.source_element_id)
382
+ const targetObj = viewElements.find((o) => o.element_id === e.target_element_id)
383
+ const srcTags = sourceObj?.tags || []
384
+ const tgtTags = targetObj?.tags || []
385
+ const isInactiveByLayer = hiddenLayerTags.length > 0 && (
386
+ srcTags.some((t) => hiddenLayerTags.includes(t)) ||
387
+ tgtTags.some((t) => hiddenLayerTags.includes(t))
388
+ )
389
+ const isInactiveByFilter = activeTags.length > 0 && (
390
+ !srcTags.some((t) => activeTags.includes(t)) ||
391
+ !tgtTags.some((t) => activeTags.includes(t))
392
+ )
393
+ const isInactive = isInactiveByLayer || isInactiveByFilter
394
+ const isSoftFocused = hoveredLayerTags !== null && (
395
+ !sourceObj?.tags?.some((t) => hoveredLayerTags.includes(t)) ||
396
+ !targetObj?.tags?.some((t) => hoveredLayerTags.includes(t))
397
+ )
398
+ const edgeOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.8
399
+ const markerOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 1
400
+ const sourceSide = getLogicalHandleId(e.source_handle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
401
+ const targetSide = getLogicalHandleId(e.target_handle, DEFAULT_TARGET_HANDLE_SIDE) ?? DEFAULT_TARGET_HANDLE_SIDE
402
+
403
+ const srcKey = `${e.source_element_id}-${sourceSide}`
404
+ const tgtKey = `${e.target_element_id}-${targetSide}`
405
+ const srcGroup = handleUsage[srcKey] ?? []
406
+ const tgtGroup = handleUsage[tgtKey] ?? []
407
+ const sourceGroupIndex = srcGroup.findIndex((u) => u.id === String(e.id) && u.type === 'source')
408
+ const targetGroupIndex = tgtGroup.findIndex((u) => u.id === String(e.id) && u.type === 'target')
409
+ const sourceHandleSlot = getVisualHandleSlot(sourceGroupIndex, Math.max(srcGroup.length, 1))
410
+ const targetHandleSlot = getVisualHandleSlot(targetGroupIndex, Math.max(tgtGroup.length, 1))
411
+
412
+ return {
413
+ id: String(e.id),
414
+ source: String(e.source_element_id),
415
+ target: String(e.target_element_id),
416
+ sourceHandle: getVisualHandleIdForGroup(sourceSide, sourceGroupIndex, Math.max(srcGroup.length, 1)),
417
+ targetHandle: getVisualHandleIdForGroup(targetSide, targetGroupIndex, Math.max(tgtGroup.length, 1)),
418
+ type: e.style === 'bezier' ? 'default' : (e.style || 'default'),
419
+ label: e.label ?? '',
420
+ data: {
421
+ ...e,
422
+ sourceGroupIndex,
423
+ sourceGroupCount: srcGroup.length,
424
+ targetGroupIndex,
425
+ targetGroupCount: tgtGroup.length,
426
+ sourceHandleSide: sourceSide,
427
+ targetHandleSide: targetSide,
428
+ sourceHandleSlot,
429
+ targetHandleSlot,
430
+ },
431
+
432
+ style: { stroke: 'var(--accent)', strokeWidth: 2, opacity: edgeOpacity, pointerEvents: (isInactive || isSoftFocused) ? 'none' : 'auto' },
433
+ labelStyle: { fontSize: 11, fill: 'var(--accent)', opacity: markerOpacity },
434
+ labelBgStyle: { fill: 'var(--chakra-colors-gray-900)', fillOpacity: isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.95 },
435
+ markerEnd: (dir === 'forward' || dir === 'both') ? { ...arrowMarker, color: alphaColor('var(--accent)', markerOpacity) } : undefined,
436
+ markerStart: (dir === 'backward' || dir === 'both') ? { ...arrowMarker, color: alphaColor('var(--accent)', markerOpacity) } : undefined,
437
+ selected: existing?.selected,
438
+ zIndex: selectedEdgeId !== null && existing?.id === String(selectedEdgeId) ? 1000 : 100,
439
+ }
440
+ }),
441
+ )
442
+ }, [connectors, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, viewElements])
443
+
444
+
445
+ // ── Boost z-index of selected connector ────────────────────────────────────────
446
+ useEffect(() => {
447
+ setRfEdges((prev) =>
448
+ prev.map((e) => ({ ...e, zIndex: selectedEdgeId !== null && e.id === String(selectedEdgeId) ? 1000 : 100 })),
449
+ )
450
+ }, [selectedEdgeId])
451
+
452
+ return {
453
+ // State
454
+ view,
455
+ setView,
456
+ viewElements,
457
+ setViewElements,
458
+ connectors,
459
+ setConnectors,
460
+ rfNodes,
461
+ setRfNodes,
462
+ rfEdges,
463
+ setRfEdges,
464
+ linksMap,
465
+ setLinksMap,
466
+ parentLinksMap,
467
+ setParentLinksMap,
468
+ incomingLinks,
469
+ treeData,
470
+ allElements,
471
+ libraryRefresh,
472
+ setLibraryRefresh,
473
+ existingElementIds,
474
+ // Stable refs
475
+ viewElementsRef,
476
+ linksMapRef,
477
+ parentLinksMapRef,
478
+ incomingLinksRef,
479
+ treeDataRef,
480
+ rfNodesRef,
481
+ rfEdgesRef,
482
+ viewIdRef,
483
+ // Actions
484
+ refreshGrid,
485
+ refreshElements,
486
+ handleElementDeleted,
487
+ handleElementPermanentlyDeleted,
488
+ handleElementSaved,
489
+ setAllElements,
490
+ }
491
+ }