@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,3 @@
1
+ // src/components/ZUI/index.ts
2
+ export { ZUICanvas } from './ZUICanvas'
3
+ export type { ZUICanvasHandle } from './ZUICanvas'
@@ -0,0 +1,323 @@
1
+ // src/components/ZUI/layout.ts
2
+
3
+ import type {
4
+ DiagramGroupLayout,
5
+ LayoutNode,
6
+ ZUILayout,
7
+ } from './types'
8
+ import type {
9
+ PlacedElement,
10
+ ExploreData,
11
+ ViewConnector,
12
+ } from '../../types'
13
+ import { resolveIconPath } from '../../utils/url'
14
+
15
+ // ── Constants ──────────────────────────────────────────────────────
16
+
17
+ export const NODE_W = 200
18
+ export const NODE_H = 100
19
+ const GROUP_PAD = 80 // padding inside a diagram group box
20
+ const GROUP_SPACING = 400 // horizontal gap between root diagrams
21
+ const CHILD_PAD = 1 // padding inside a node when rendering children
22
+
23
+ // ── Helpers ────────────────────────────────────────────────────────
24
+
25
+ function nodeId(diagramId: number, elementId: number): string {
26
+ return `d${diagramId}-o${elementId}`
27
+ }
28
+
29
+ function getPos(obj: PlacedElement, axis: 'x' | 'y'): number {
30
+ const val = axis === 'x' ? obj.position_x : obj.position_y
31
+ return typeof val === 'number' && isFinite(val) ? val : 0
32
+ }
33
+
34
+ function calcBBox(elements: PlacedElement[]): {
35
+ minX: number; minY: number; width: number; height: number
36
+ } {
37
+ if (elements.length === 0) {
38
+ return { minX: 0, minY: 0, width: NODE_W, height: NODE_H }
39
+ }
40
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
41
+ for (const o of elements) {
42
+ const px = getPos(o, 'x')
43
+ const py = getPos(o, 'y')
44
+ minX = Math.min(minX, px)
45
+ minY = Math.min(minY, py)
46
+ maxX = Math.max(maxX, px + NODE_W)
47
+ maxY = Math.max(maxY, py + NODE_H)
48
+ }
49
+ if (!isFinite(minX)) return { minX: 0, minY: 0, width: NODE_W, height: NODE_H }
50
+ return { minX, minY, width: maxX - minX, height: maxY - minY }
51
+ }
52
+
53
+ /** Build a lookup: elementId → ElementDiagramLink for child connectors only. */
54
+ function buildChildLinkMap(
55
+ links: ViewConnector[],
56
+ fromDiagramId: number,
57
+ ): Map<number, ViewConnector> {
58
+ const m = new Map<number, ViewConnector>()
59
+ for (const l of links) {
60
+ if (l.from_view_id === fromDiagramId && l.relation_type === 'child' && l.element_id != null) {
61
+ m.set(l.element_id, l)
62
+ }
63
+ }
64
+ return m
65
+ }
66
+
67
+ /**
68
+ * Recursively build LayoutNode[] for all elements in a diagram.
69
+ * `worldOffsetX/Y` are the world-space origin of the diagram that
70
+ * contains these elements.
71
+ * `pad` is added to each element's local position (GROUP_PAD for root
72
+ * diagrams, 0 for children stored in raw editor coords).
73
+ *
74
+ * `visited` is a Set of diagram IDs to prevent infinite recursion for cyclic links.
75
+ */
76
+ function buildNodes(
77
+ elements: PlacedElement[],
78
+ views: ExploreData['views'],
79
+ links: ViewConnector[],
80
+ diagramId: number,
81
+ worldOffsetX: number,
82
+ worldOffsetY: number,
83
+ bboxMinX: number,
84
+ bboxMinY: number,
85
+ visited: Set<number>,
86
+ pad: number = GROUP_PAD,
87
+ diagramX: number = 0,
88
+ _ignoredPortalsXOffset: number = 0, // kept to minimize recursive signature change
89
+ _diagramW: number = 0,
90
+ _diagramH: number = 0,
91
+ tree: ExploreData['tree'] = [],
92
+ ancestorElementIds: number[] = [],
93
+ ): LayoutNode[] {
94
+ const childLinkMap = buildChildLinkMap(links, diagramId)
95
+ const _treeMap = new Map(tree.map((d) => [d.id, d]))
96
+
97
+ const realNodes: LayoutNode[] = elements.map((obj) => {
98
+ const localX = getPos(obj, 'x') - bboxMinX + pad
99
+ const localY = getPos(obj, 'y') - bboxMinY + pad
100
+
101
+ const link = childLinkMap.get(obj.element_id)
102
+
103
+ // ── Build children if this element connects to a child diagram ──
104
+ let children: LayoutNode[] = []
105
+ let childScale = 1
106
+ let childOffsetX = 0
107
+ let childOffsetY = 0
108
+ let linkedDiagramId: number | undefined
109
+ let linkedDiagramLabel: string | undefined
110
+ let isCircular = false
111
+
112
+ if (link) {
113
+ linkedDiagramId = link.to_view_id
114
+ linkedDiagramLabel = link.to_view_name
115
+
116
+ // Check for cycle before recursing
117
+ if (visited.has(link.to_view_id)) {
118
+ isCircular = true
119
+ } else {
120
+ const childDiagData = views[String(link.to_view_id)]
121
+
122
+ if (childDiagData && childDiagData.placements.length > 0) {
123
+ const cBBox = calcBBox(childDiagData.placements)
124
+
125
+ const contentW = cBBox.width + CHILD_PAD * 2
126
+ const contentH = cBBox.height + CHILD_PAD * 2
127
+ const fittedH = Math.min(contentH, contentW)
128
+ childScale = Math.min(
129
+ (NODE_W - CHILD_PAD * 2) / contentW,
130
+ (Math.min(NODE_H, fittedH) - CHILD_PAD * 2) / Math.max(contentH, 1),
131
+ 0.45,
132
+ )
133
+ const scaledW = cBBox.width * childScale
134
+ const scaledH = cBBox.height * childScale
135
+ const marginX = (NODE_W - scaledW) / 2
136
+ const marginY = (NODE_H - scaledH) / 2
137
+ childOffsetX = cBBox.minX - marginX / childScale
138
+ childOffsetY = cBBox.minY - marginY / childScale
139
+
140
+ const nextVisited = new Set(visited)
141
+ nextVisited.add(link.to_view_id)
142
+
143
+ children = buildNodes(
144
+ childDiagData.placements,
145
+ views,
146
+ links,
147
+ link.to_view_id,
148
+ 0, 0, 0, 0,
149
+ nextVisited,
150
+ 0,
151
+ 0, 0, 0, 0,
152
+ tree,
153
+ [...ancestorElementIds, obj.element_id],
154
+ )
155
+ }
156
+ }
157
+ }
158
+
159
+ const edgesOut = (views[String(diagramId)]?.connectors ?? [])
160
+ .filter((e) => e.source_element_id === obj.element_id)
161
+ .map((e) => ({
162
+ targetId: nodeId(diagramId, e.target_element_id),
163
+ label: e.label ?? '',
164
+ direction: e.direction ?? 'forward',
165
+ sourceHandle: e.source_handle ?? null,
166
+ targetHandle: e.target_handle ?? null,
167
+ type: e.style || 'bezier',
168
+ }))
169
+
170
+ const derivedPrimaryIconPath = (() => {
171
+ const selected = obj.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
172
+ if (!selected?.slug) return null
173
+ return resolveIconPath(`/icons/${selected.slug}.png`)
174
+ })()
175
+
176
+ return {
177
+ id: nodeId(diagramId, obj.element_id),
178
+ elementId: obj.element_id,
179
+ diagramId,
180
+ worldX: worldOffsetX + diagramX + localX,
181
+ worldY: worldOffsetY + localY,
182
+ worldW: NODE_W,
183
+ worldH: NODE_H,
184
+ label: obj.name,
185
+ type: obj.kind ?? 'system',
186
+ logoUrl: obj.logo_url ? resolveIconPath(obj.logo_url) : derivedPrimaryIconPath,
187
+ description: obj.description ?? null,
188
+ technology: obj.technology ?? null,
189
+ tags: obj.tags ?? [],
190
+ ancestorElementIds,
191
+ pathElementIds: [...ancestorElementIds, obj.element_id],
192
+ linkedDiagramId,
193
+ linkedDiagramLabel,
194
+ isCircular,
195
+ isPortal: false,
196
+ children,
197
+ childScale,
198
+ childOffsetX,
199
+ childOffsetY,
200
+ edgesOut,
201
+ }
202
+ })
203
+
204
+ return realNodes
205
+ }
206
+
207
+ // ── Public API ──────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Compute the full world-space layout for all diagrams in `data`.
211
+ * Root diagrams are placed side-by-side horizontally.
212
+ */
213
+ export function computeLayout(data: ExploreData): ZUILayout {
214
+ const rootDiagrams = (data.tree ?? []).filter((d) => !d.parent_view_id)
215
+ const groups: DiagramGroupLayout[] = []
216
+ let xCursor = 0
217
+
218
+ for (const diag of rootDiagrams) {
219
+ const diagData = data.views[String(diag.id)]
220
+ if (!diagData) continue
221
+
222
+ const bbox = calcBBox(diagData.placements ?? [])
223
+ const diagramW = bbox.width + GROUP_PAD * 2
224
+ const diagramH = bbox.height + GROUP_PAD * 2
225
+
226
+ const TOP_PAD = 80
227
+ const worldW = diagramW
228
+ const worldH = diagramH + TOP_PAD
229
+ const diagramX = 0
230
+ const diagramY = TOP_PAD
231
+
232
+ const visited = new Set<number>()
233
+ visited.add(diag.id)
234
+
235
+ const nodes = buildNodes(
236
+ diagData.placements ?? [],
237
+ data.views,
238
+ data.navigations ?? [],
239
+ diag.id,
240
+ xCursor,
241
+ diagramY, // Note: this acts as worldOffsetY in buildNodes, effectively shifting everything down
242
+ bbox.minX,
243
+ bbox.minY,
244
+ visited,
245
+ GROUP_PAD,
246
+ diagramX, // Replacing portalYOffset placeholder with diagram parameters for proper circle tracing
247
+ 0, // Replacing portalsXOffset
248
+ diagramW, // passing these so buildNodes can compute diagram center in world
249
+ diagramH,
250
+ data.tree ?? [],
251
+ [],
252
+ )
253
+
254
+ // Edges within the same diagram (world-level, not children)
255
+ const edges = (diagData.connectors ?? []).map((e) => ({
256
+ sourceId: nodeId(diag.id, e.source_element_id),
257
+ targetId: nodeId(diag.id, e.target_element_id),
258
+ label: e.label ?? '',
259
+ direction: e.direction ?? 'forward',
260
+ sourceHandle: e.source_handle ?? null,
261
+ targetHandle: e.target_handle ?? null,
262
+ type: e.style || 'bezier',
263
+ }))
264
+
265
+ groups.push({
266
+ diagramId: diag.id,
267
+ label: diag.name,
268
+ description: diag.description ?? null,
269
+ level: diag.level,
270
+ levelLabel: diag.level_label,
271
+ worldX: xCursor,
272
+ worldY: 0,
273
+ worldW,
274
+ worldH,
275
+ diagramW,
276
+ diagramH,
277
+ diagramX,
278
+ diagramY,
279
+ nodes,
280
+ edges,
281
+ })
282
+
283
+ xCursor += worldW + GROUP_SPACING
284
+ }
285
+
286
+ // Compute overall bounding box
287
+ const allX = groups.flatMap((g) => [g.worldX, g.worldX + g.worldW])
288
+ const allY = groups.flatMap((g) => [g.worldY, g.worldY + g.worldH])
289
+ const bbox = {
290
+ minX: Math.min(...allX, 0),
291
+ minY: Math.min(...allY, 0),
292
+ maxX: Math.max(...allX, 100),
293
+ maxY: Math.max(...allY, 100),
294
+ }
295
+
296
+ return { groups, bbox }
297
+ }
298
+
299
+ /**
300
+ * Compute the initial viewport that fits the entire layout on screen.
301
+ * Returns a ZUIViewState (x=panX, y=panY, zoom).
302
+ */
303
+ export function fitViewport(
304
+ layout: ZUILayout,
305
+ canvasW: number,
306
+ canvasH: number,
307
+ padding = 0.1,
308
+ ): { x: number; y: number; zoom: number } {
309
+ const { bbox } = layout
310
+ const bboxW = bbox.maxX - bbox.minX
311
+ const bboxH = bbox.maxY - bbox.minY
312
+ if (bboxW <= 0 || bboxH <= 0) return { x: 0, y: 0, zoom: 1 }
313
+
314
+ const pad = padding
315
+ const zoom = Math.min(
316
+ (canvasW * (1 - pad * 2)) / bboxW,
317
+ (canvasH * (1 - pad * 2)) / bboxH,
318
+ 4,
319
+ )
320
+ const x = (canvasW - bboxW * zoom) / 2 - bbox.minX * zoom
321
+ const y = (canvasH - bboxH * zoom) / 2 - bbox.minY * zoom
322
+ return { x, y, zoom }
323
+ }
@@ -0,0 +1,278 @@
1
+ import { resolveZUIProxyConnectors, type ZUIResolvedConnector } from '../../crossBranch/resolve'
2
+ import type { WorkspaceGraphSnapshot } from '../../crossBranch/types'
3
+ import type { LayoutNode, ZUIViewState, HoveredItem } from './types'
4
+ import { getExpandThresholds, pickEdgeLabelPosition, type ScreenRect } from './renderer'
5
+ import type { CrossBranchContextSettings } from '../../crossBranch/types'
6
+
7
+ export interface VisibleNodeAnchor {
8
+ nodeId: string
9
+ elementId: number
10
+ label: string
11
+ worldX: number
12
+ worldY: number
13
+ worldW: number
14
+ worldH: number
15
+ pathDepth: number
16
+ renderAlpha: number
17
+ }
18
+
19
+ function clamp(value: number, min: number, max: number) {
20
+ return value < min ? min : value > max ? max : value
21
+ }
22
+
23
+ function transitionT(screenW: number, start: number, end: number): number {
24
+ return clamp((screenW - start) / (end - start), 0, 1)
25
+ }
26
+
27
+ function collectVisibleAnchorsInNodes(
28
+ nodes: LayoutNode[],
29
+ view: ZUIViewState,
30
+ thresholds: { start: number; end: number },
31
+ hiddenTags: Set<string>,
32
+ visibleAnchors: Map<number, VisibleNodeAnchor>,
33
+ byNodeId: Map<string, VisibleNodeAnchor>,
34
+ inheritedAlpha: number,
35
+ parentAbsX: number,
36
+ parentAbsY: number,
37
+ parentAbsScale: number,
38
+ parentChildOffsetX: number,
39
+ parentChildOffsetY: number,
40
+ ) {
41
+ for (const node of nodes) {
42
+ if (hiddenTags.size > 0 && node.tags.some((tag) => hiddenTags.has(tag))) continue
43
+
44
+ const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
45
+ const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
46
+ const absScale = parentAbsScale
47
+ const absW = node.worldW * absScale
48
+ const absH = node.worldH * absScale
49
+ const screenW = absW * view.zoom
50
+ if (screenW < 2) continue
51
+
52
+ const hasChildren = node.children && node.children.length > 0
53
+ const t = hasChildren ? transitionT(screenW, thresholds.start, thresholds.end) : 0
54
+ const parentAlpha = inheritedAlpha * (1 - t)
55
+ const childAlpha = inheritedAlpha * t
56
+
57
+ if (!hasChildren || t <= 0.95) {
58
+ const anchor: VisibleNodeAnchor = {
59
+ nodeId: node.id,
60
+ elementId: node.elementId,
61
+ label: node.label,
62
+ worldX: absX,
63
+ worldY: absY,
64
+ worldW: absW,
65
+ worldH: absH,
66
+ pathDepth: node.pathElementIds.length,
67
+ renderAlpha: hasChildren ? parentAlpha : inheritedAlpha,
68
+ }
69
+ const existing = visibleAnchors.get(node.elementId)
70
+ if (!existing || existing.pathDepth < anchor.pathDepth) visibleAnchors.set(node.elementId, anchor)
71
+ byNodeId.set(node.id, anchor)
72
+ }
73
+
74
+ if (hasChildren && t > 0.05) {
75
+ collectVisibleAnchorsInNodes(
76
+ node.children,
77
+ view,
78
+ thresholds,
79
+ hiddenTags,
80
+ visibleAnchors,
81
+ byNodeId,
82
+ childAlpha,
83
+ absX,
84
+ absY,
85
+ absScale * node.childScale,
86
+ node.childOffsetX,
87
+ node.childOffsetY,
88
+ )
89
+ }
90
+ }
91
+ }
92
+
93
+ export function collectVisibleNodeAnchors(
94
+ groups: Array<{ nodes: LayoutNode[] }>,
95
+ view: ZUIViewState,
96
+ canvasW: number,
97
+ hiddenTags: string[] = [],
98
+ ) {
99
+ const thresholds = getExpandThresholds(canvasW)
100
+ const visibleAnchors = new Map<number, VisibleNodeAnchor>()
101
+ const byNodeId = new Map<string, VisibleNodeAnchor>()
102
+ const hiddenTagSet = new Set(hiddenTags)
103
+
104
+ for (const group of groups) {
105
+ collectVisibleAnchorsInNodes(
106
+ group.nodes,
107
+ view,
108
+ thresholds,
109
+ hiddenTagSet,
110
+ visibleAnchors,
111
+ byNodeId,
112
+ 1,
113
+ 0,
114
+ 0,
115
+ 1,
116
+ 0,
117
+ 0,
118
+ )
119
+ }
120
+
121
+ return { visibleAnchors, byNodeId }
122
+ }
123
+
124
+ function getDirectAnchorPoint(anchor: VisibleNodeAnchor, towards: VisibleNodeAnchor) {
125
+ const cx = anchor.worldX + anchor.worldW / 2
126
+ const cy = anchor.worldY + anchor.worldH / 2
127
+ const tx = towards.worldX + towards.worldW / 2
128
+ const ty = towards.worldY + towards.worldH / 2
129
+ const dx = tx - cx
130
+ const dy = ty - cy
131
+ const hw = anchor.worldW / 2
132
+ const hh = anchor.worldH / 2
133
+
134
+ if (dx === 0 && dy === 0) return { x: cx, y: cy }
135
+
136
+ const tanTheta = Math.abs(dy / dx)
137
+ const boxRatio = hh / hw
138
+ if (tanTheta < boxRatio) {
139
+ return {
140
+ x: cx + Math.sign(dx) * hw,
141
+ y: cy + Math.sign(dx) * hw * (dy / dx),
142
+ }
143
+ }
144
+
145
+ return {
146
+ y: cy + Math.sign(dy) * hh,
147
+ x: cx + Math.sign(dy) * hh * (dx / dy),
148
+ }
149
+ }
150
+
151
+ export function buildVisibleProxyConnectors(
152
+ snapshot: WorkspaceGraphSnapshot | null,
153
+ visibleAnchors: Map<number, VisibleNodeAnchor>,
154
+ settings: CrossBranchContextSettings,
155
+ ): ZUIResolvedConnector[] {
156
+ return resolveZUIProxyConnectors(
157
+ snapshot,
158
+ new Map(Array.from(visibleAnchors.entries()).map(([elementId, anchor]) => [elementId, anchor.nodeId])),
159
+ settings,
160
+ )
161
+ }
162
+
163
+ export function drawVisibleProxyConnectors(
164
+ ctx: CanvasRenderingContext2D,
165
+ connectors: ZUIResolvedConnector[],
166
+ visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
167
+ zoom: number,
168
+ labelBg: string,
169
+ occupiedLabelRects: ScreenRect[],
170
+ ) {
171
+ for (const connector of connectors) {
172
+ const source = visibleAnchorsByNodeId.get(connector.sourceNodeId)
173
+ const target = visibleAnchorsByNodeId.get(connector.targetNodeId)
174
+ if (!source || !target) continue
175
+ const alpha = Math.min(source.renderAlpha, target.renderAlpha)
176
+ if (alpha < 0.01) continue
177
+
178
+ const sourcePoint = getDirectAnchorPoint(source, target)
179
+ const targetPoint = getDirectAnchorPoint(target, source)
180
+ const midX = (sourcePoint.x + targetPoint.x) / 2
181
+ const midY = (sourcePoint.y + targetPoint.y) / 2
182
+ const label = String(connector.details.count)
183
+
184
+ ctx.save()
185
+ ctx.globalAlpha = alpha
186
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'
187
+ ctx.lineWidth = 2 / zoom
188
+ ctx.beginPath()
189
+ ctx.moveTo(sourcePoint.x, sourcePoint.y)
190
+ ctx.lineTo(targetPoint.x, targetPoint.y)
191
+ ctx.stroke()
192
+ const fontSize = 11 / zoom
193
+ ctx.font = `${fontSize}px Inter, system-ui, sans-serif`
194
+ const textMetrics = ctx.measureText(label)
195
+ const textW = textMetrics.width
196
+ const textH = fontSize
197
+ const labelPos = pickEdgeLabelPosition(
198
+ ctx.getTransform(),
199
+ midX,
200
+ midY,
201
+ textW,
202
+ textH,
203
+ targetPoint.x - sourcePoint.x,
204
+ targetPoint.y - sourcePoint.y,
205
+ occupiedLabelRects,
206
+ )
207
+ const px = 6 / zoom
208
+ const py = 4 / zoom
209
+ const badgeW = textW + px * 2
210
+ const badgeH = textH + py * 2
211
+ const badgeRadius = badgeH / 2
212
+ ctx.fillStyle = labelBg
213
+ ctx.beginPath()
214
+ ctx.roundRect(
215
+ labelPos.x - badgeW / 2,
216
+ labelPos.y - badgeH / 2,
217
+ badgeW,
218
+ badgeH,
219
+ badgeRadius,
220
+ )
221
+ ctx.fill()
222
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'
223
+ ctx.lineWidth = 1 / zoom
224
+ ctx.stroke()
225
+ ctx.fillStyle = 'white'
226
+ ctx.textAlign = 'center'
227
+ ctx.textBaseline = 'middle'
228
+ ctx.fillText(label, labelPos.x, labelPos.y)
229
+
230
+ ctx.restore()
231
+ }
232
+ }
233
+
234
+ export function findHoveredProxyConnector(
235
+ worldX: number,
236
+ worldY: number,
237
+ connectors: ZUIResolvedConnector[],
238
+ visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
239
+ view: ZUIViewState,
240
+ ): HoveredItem | null {
241
+ const threshold = 18 / view.zoom
242
+ for (const connector of connectors) {
243
+ const source = visibleAnchorsByNodeId.get(connector.sourceNodeId)
244
+ const target = visibleAnchorsByNodeId.get(connector.targetNodeId)
245
+ if (!source || !target) continue
246
+ const x1 = source.worldX + source.worldW / 2
247
+ const y1 = source.worldY + source.worldH / 2
248
+ const x2 = target.worldX + target.worldW / 2
249
+ const y2 = target.worldY + target.worldH / 2
250
+ const dx = x2 - x1
251
+ const dy = y2 - y1
252
+ const l2 = dx * dx + dy * dy
253
+ if (l2 === 0) continue
254
+ let t = ((worldX - x1) * dx + (worldY - y1) * dy) / l2
255
+ t = Math.max(0, Math.min(1, t))
256
+ const nearestX = x1 + t * dx
257
+ const nearestY = y1 + t * dy
258
+ const dist = Math.sqrt((worldX - nearestX) ** 2 + (worldY - nearestY) ** 2)
259
+ if (dist > threshold) continue
260
+
261
+ return {
262
+ type: 'edge',
263
+ data: {
264
+ sourceId: connector.details.sourceAnchorName,
265
+ targetId: connector.details.targetAnchorName,
266
+ label: connector.details.label || 'Cross-branch connector',
267
+ diagramId: connector.details.ownerViewIds[0] ?? 0,
268
+ sourceObjId: connector.sourceAnchorElementId,
269
+ targetObjId: connector.targetAnchorElementId,
270
+ isProxy: true,
271
+ details: connector.details,
272
+ },
273
+ absX: (x1 + x2) / 2,
274
+ absY: (y1 + y2) / 2,
275
+ }
276
+ }
277
+ return null
278
+ }