@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,624 @@
1
+ import { useState } from 'react'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Collapse,
6
+ FormControl,
7
+ FormLabel,
8
+ Grid,
9
+ HStack,
10
+ NumberDecrementStepper,
11
+ NumberIncrementStepper,
12
+ NumberInput,
13
+ NumberInputField,
14
+ NumberInputStepper,
15
+ Select,
16
+ Text,
17
+ VStack,
18
+ Icon,
19
+ } from '@chakra-ui/react'
20
+ import { ChevronDownIcon, ChevronRightIcon } from './Icons'
21
+ import { api } from '../api/client'
22
+ import type { ViewTreeNode } from '../types'
23
+
24
+ type Algorithm = 'elk' | 'force'
25
+
26
+ interface ElkConfig {
27
+ algorithm: 'layered' | 'force' | 'mrtree' | 'box'
28
+ direction: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP'
29
+ nodeSpacing: number
30
+ layerSpacing: number
31
+ }
32
+
33
+ interface ForceConfig {
34
+ linkDistance: number
35
+ chargeStrength: number
36
+ collideRadius: number
37
+ iterations: number
38
+ }
39
+
40
+ const NODE_W = 200
41
+ const NODE_H = 120
42
+
43
+ const ALGO_META: Record<Algorithm, { label: string }> = {
44
+ elk: { label: 'Layered' },
45
+ force: { label: 'Organic' },
46
+ }
47
+
48
+ interface Props {
49
+ view: ViewTreeNode | null
50
+ canEdit: boolean
51
+ }
52
+
53
+ export default function LayoutSection({ view, canEdit }: Props) {
54
+ const [open, setOpen] = useState(false)
55
+ const [algo, setAlgo] = useState<Algorithm>('elk')
56
+ const [running, setRunning] = useState(false)
57
+ const [collisionRunning, setCollisionRunning] = useState(false)
58
+
59
+ const [elkConfig, setElkConfig] = useState<ElkConfig>({
60
+ algorithm: 'layered',
61
+ direction: 'DOWN',
62
+ nodeSpacing: 75,
63
+ layerSpacing: 75,
64
+ })
65
+
66
+ const [forceConfig, setForceConfig] = useState<ForceConfig>({
67
+ linkDistance: 180,
68
+ chargeStrength: -150,
69
+ collideRadius: 130,
70
+ iterations: 300,
71
+ })
72
+
73
+ const handleCollisionRemoval = async () => {
74
+ if (!canEdit || !view) return
75
+ setCollisionRunning(true)
76
+ try {
77
+ const [objs, edgeList] = await Promise.all([
78
+ api.workspace.views.placements.list(view.id),
79
+ api.workspace.connectors.list(view.id),
80
+ ])
81
+
82
+ const WIDTH = NODE_W
83
+ const HEIGHT = NODE_H
84
+ const PADDING = 40
85
+
86
+ const newPositions = new Map<number, { x: number; y: number }>()
87
+ objs.forEach((o) => newPositions.set(o.element_id, { x: o.position_x, y: o.position_y }))
88
+
89
+ for (let pass = 0; pass < 3; pass++) {
90
+ for (let j = 0; j < objs.length; j++) {
91
+ for (let k = j + 1; k < objs.length; k++) {
92
+ const a = objs[j]
93
+ const b = objs[k]
94
+ const posA = newPositions.get(a.element_id)!
95
+ const posB = newPositions.get(b.element_id)!
96
+
97
+ const dx = posB.x + WIDTH / 2 - (posA.x + WIDTH / 2)
98
+ const dy = posB.y + HEIGHT / 2 - (posA.y + HEIGHT / 2)
99
+ const adx = Math.abs(dx)
100
+ const ady = Math.abs(dy)
101
+
102
+ const minX = WIDTH + PADDING
103
+ const minY = HEIGHT + PADDING
104
+
105
+ if (adx < minX && ady < minY) {
106
+ const pushX = (minX - adx) / 2
107
+ const pushY = (minY - ady) / 2
108
+ const factorX = dx >= 0 ? 1 : -1
109
+ const factorY = dy >= 0 ? 1 : -1
110
+
111
+ posA.x -= pushX * factorX
112
+ posA.y -= pushY * factorY
113
+ posB.x += pushX * factorX
114
+ posB.y += pushY * factorY
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ await Promise.all(
121
+ objs.map((obj) => {
122
+ const pos = newPositions.get(obj.element_id)!
123
+ return api.workspace.views.placements.updatePosition(
124
+ view.id,
125
+ obj.element_id,
126
+ Math.round(pos.x),
127
+ Math.round(pos.y)
128
+ )
129
+ })
130
+ )
131
+
132
+ const handleUpdates = []
133
+ for (const edge of edgeList) {
134
+ const sPos = newPositions.get(edge.source_element_id)
135
+ const tPos = newPositions.get(edge.target_element_id)
136
+ if (!sPos || !tPos) continue
137
+
138
+ const sourceHandles: Record<string, { x: number; y: number }> = {
139
+ top: { x: sPos.x + WIDTH / 2, y: sPos.y },
140
+ bottom: { x: sPos.x + WIDTH / 2, y: sPos.y + HEIGHT },
141
+ left: { x: sPos.x, y: sPos.y + HEIGHT / 2 },
142
+ right: { x: sPos.x + WIDTH, y: sPos.y + HEIGHT / 2 },
143
+ }
144
+
145
+ const targetHandles: Record<string, { x: number; y: number }> = {
146
+ top: { x: tPos.x + WIDTH / 2, y: tPos.y },
147
+ bottom: { x: tPos.x + WIDTH / 2, y: tPos.y + HEIGHT },
148
+ left: { x: tPos.x, y: tPos.y + HEIGHT / 2 },
149
+ right: { x: tPos.x + WIDTH, y: tPos.y + HEIGHT / 2 },
150
+ }
151
+
152
+ let minDist = Infinity
153
+ let bestSource = edge.source_handle || 'top'
154
+ let bestTarget = edge.target_handle || 'top'
155
+
156
+ for (const [sId, sCoord] of Object.entries(sourceHandles)) {
157
+ for (const [tId, tCoord] of Object.entries(targetHandles)) {
158
+ const dist = Math.sqrt((sCoord.x - tCoord.x) ** 2 + (sCoord.y - tCoord.y) ** 2)
159
+ if (dist < minDist) {
160
+ minDist = dist
161
+ bestSource = sId
162
+ bestTarget = tId
163
+ }
164
+ }
165
+ }
166
+
167
+ if (bestSource !== edge.source_handle || bestTarget !== edge.target_handle) {
168
+ handleUpdates.push(api.workspace.connectors.update(view.id, edge.id, {
169
+ source_element_id: edge.source_element_id,
170
+ target_element_id: edge.target_element_id,
171
+ source_handle: bestSource,
172
+ target_handle: bestTarget,
173
+ label: edge.label || undefined,
174
+ description: edge.description || undefined,
175
+ relationship: edge.relationship || undefined,
176
+ direction: edge.direction || undefined,
177
+ style: edge.style === 'default' ? 'bezier' : (edge.style || 'bezier'),
178
+ url: edge.url || undefined,
179
+ }))
180
+ }
181
+ }
182
+
183
+ await Promise.all(handleUpdates)
184
+ window.location.reload()
185
+ } catch (err) {
186
+ console.error('Collision removal failed:', err)
187
+ } finally {
188
+ setCollisionRunning(false)
189
+ }
190
+ }
191
+
192
+ const applyLayout = async () => {
193
+ if (!view || !canEdit) return
194
+ setRunning(true)
195
+ try {
196
+ const [objs, edgeList] = await Promise.all([
197
+ api.workspace.views.placements.list(view.id),
198
+ api.workspace.connectors.list(view.id),
199
+ ])
200
+
201
+ let positions: Map<number, { x: number; y: number }>
202
+ if (algo === 'elk') {
203
+ positions = await runElk(objs, edgeList)
204
+ } else {
205
+ positions = await runForce(objs, edgeList)
206
+ }
207
+
208
+ await Promise.all(
209
+ objs.map(obj => {
210
+ const pos = positions.get(obj.element_id) ?? { x: obj.position_x, y: obj.position_y }
211
+ return api.workspace.views.placements.updatePosition(
212
+ view.id, obj.element_id, Math.round(pos.x), Math.round(pos.y)
213
+ )
214
+ })
215
+ )
216
+
217
+ // Re-optimise connector handle directions to match new positions
218
+ const handleUpdates = []
219
+ for (const edge of edgeList) {
220
+ const sPos = positions.get(edge.source_element_id)
221
+ const tPos = positions.get(edge.target_element_id)
222
+ if (!sPos || !tPos) continue
223
+
224
+ const srcHandles: Record<string, { x: number; y: number }> = {
225
+ top: { x: sPos.x + NODE_W / 2, y: sPos.y },
226
+ bottom: { x: sPos.x + NODE_W / 2, y: sPos.y + NODE_H },
227
+ left: { x: sPos.x, y: sPos.y + NODE_H / 2 },
228
+ right: { x: sPos.x + NODE_W, y: sPos.y + NODE_H / 2 },
229
+ }
230
+ const tgtHandles: Record<string, { x: number; y: number }> = {
231
+ top: { x: tPos.x + NODE_W / 2, y: tPos.y },
232
+ bottom: { x: tPos.x + NODE_W / 2, y: tPos.y + NODE_H },
233
+ left: { x: tPos.x, y: tPos.y + NODE_H / 2 },
234
+ right: { x: tPos.x + NODE_W, y: tPos.y + NODE_H / 2 },
235
+ }
236
+
237
+ let minDist = Infinity
238
+ let bestSrc = edge.source_handle || 'top'
239
+ let bestTgt = edge.target_handle || 'top'
240
+
241
+ for (const [sId, sC] of Object.entries(srcHandles)) {
242
+ for (const [tId, tC] of Object.entries(tgtHandles)) {
243
+ const d = Math.sqrt((sC.x - tC.x) ** 2 + (sC.y - tC.y) ** 2)
244
+ if (d < minDist) { minDist = d; bestSrc = sId; bestTgt = tId }
245
+ }
246
+ }
247
+
248
+ if (bestSrc !== edge.source_handle || bestTgt !== edge.target_handle) {
249
+ handleUpdates.push(api.workspace.connectors.update(view.id, edge.id, {
250
+ source_element_id: edge.source_element_id,
251
+ target_element_id: edge.target_element_id,
252
+ source_handle: bestSrc,
253
+ target_handle: bestTgt,
254
+ label: edge.label || undefined,
255
+ description: edge.description || undefined,
256
+ relationship: edge.relationship || undefined,
257
+ direction: edge.direction || undefined,
258
+ style: edge.style === 'default' ? 'bezier' : (edge.style || 'bezier'),
259
+ url: edge.url || undefined,
260
+ }))
261
+ }
262
+ }
263
+
264
+ await Promise.all(handleUpdates)
265
+ window.location.reload()
266
+ } catch (err) {
267
+ console.error('Layout failed:', err)
268
+ } finally {
269
+ setRunning(false)
270
+ }
271
+ }
272
+
273
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
274
+ const runElk = async (objs: any[], edgeList: any[]) => {
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ const ELKModule = await import('elkjs/lib/elk.bundled.js') as any
277
+ const elk = new ELKModule.default()
278
+
279
+ const layoutOptions: Record<string, string> = {
280
+ 'elk.algorithm': elkConfig.algorithm,
281
+ 'elk.spacing.nodeNode': String(elkConfig.nodeSpacing),
282
+ }
283
+ if (elkConfig.algorithm === 'layered') {
284
+ layoutOptions['elk.direction'] = elkConfig.direction
285
+ layoutOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(elkConfig.layerSpacing)
286
+ }
287
+
288
+ const objSet = new Set<number>(objs.map((o: { element_id?: number }) => Number(o.element_id)))
289
+ const graph = {
290
+ id: 'root',
291
+ layoutOptions,
292
+ children: objs.map((obj: { element_id: number }) => ({
293
+ id: String(obj.element_id),
294
+ width: NODE_W,
295
+ height: NODE_H,
296
+ })),
297
+ edges: edgeList
298
+ .filter((e: { source_element_id: number; target_element_id: number }) =>
299
+ objSet.has(e.source_element_id) && objSet.has(e.target_element_id)
300
+ )
301
+ .map((e: { id: number; source_element_id: number; target_element_id: number }) => ({
302
+ id: String(e.id),
303
+ sources: [String(e.source_element_id)],
304
+ targets: [String(e.target_element_id)],
305
+ })),
306
+ }
307
+
308
+ const result = await elk.layout(graph)
309
+ const positions = new Map<number, { x: number; y: number }>()
310
+ result.children?.forEach((child: { id: string; x?: number; y?: number }) => {
311
+ const id = Number(child.id)
312
+ if (!Number.isFinite(id)) return
313
+ positions.set(id, { x: child.x ?? 0, y: child.y ?? 0 })
314
+ })
315
+ return positions
316
+ }
317
+
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ const runForce = async (objs: any[], edgeList: any[]) => {
320
+ const d3 = await import('d3-force')
321
+
322
+ const nodes = objs.map((obj: { element_id: number; position_x: number; position_y: number }) => ({
323
+ id: obj.element_id,
324
+ x: obj.position_x + NODE_W / 2,
325
+ y: obj.position_y + NODE_H / 2,
326
+ }))
327
+ const nodeIds = new Set(nodes.map(n => n.id))
328
+ const links = edgeList
329
+ .filter((e: { source_element_id: number; target_element_id: number }) =>
330
+ nodeIds.has(e.source_element_id) && nodeIds.has(e.target_element_id)
331
+ )
332
+ .map((e: { source_element_id: number; target_element_id: number }) => ({
333
+ source: e.source_element_id,
334
+ target: e.target_element_id,
335
+ }))
336
+
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ const sim = d3.forceSimulation(nodes as any)
339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
340
+ .force('link', d3.forceLink(links as any).id((d: any) => d.id).distance(forceConfig.linkDistance))
341
+ .force('charge', d3.forceManyBody().strength(forceConfig.chargeStrength))
342
+ .force('center', d3.forceCenter(0, 0))
343
+ .force('collide', d3.forceCollide(forceConfig.collideRadius))
344
+ .stop()
345
+
346
+ for (let i = 0; i < forceConfig.iterations; i++) sim.tick()
347
+
348
+ const positions = new Map<number, { x: number; y: number }>()
349
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
350
+ nodes.forEach((n: any) => {
351
+ positions.set(n.id, { x: (n.x ?? 0) - NODE_W / 2, y: (n.y ?? 0) - NODE_H / 2 })
352
+ })
353
+ return positions
354
+ }
355
+
356
+ const LabelStyle = {
357
+ fontSize: '9px',
358
+ color: 'whiteAlpha.600',
359
+ mb: 1.5,
360
+ textTransform: 'uppercase' as const,
361
+ letterSpacing: '0.1em',
362
+ fontWeight: '700',
363
+ }
364
+
365
+ return (
366
+ <Box borderTop="1px solid" borderColor="whiteAlpha.100" pt={4}>
367
+ {/* Section header */}
368
+ <HStack
369
+ mb={open ? 4 : 0}
370
+ cursor="pointer"
371
+ onClick={() => setOpen(v => !v)}
372
+ color={open ? 'blue.400' : 'whiteAlpha.700'}
373
+ _hover={{ color: 'blue.300' }}
374
+ transition="all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
375
+ userSelect="none"
376
+ >
377
+ <Icon
378
+ as={open ? ChevronDownIcon : ChevronRightIcon}
379
+ boxSize={4}
380
+ strokeWidth={3.5}
381
+ transition="transform 0.25s cubic-bezier(0.25, 1, 0.5, 1)"
382
+ />
383
+ <Text
384
+ fontSize="11px"
385
+ fontWeight="800"
386
+ letterSpacing="0.15em"
387
+ textTransform="uppercase"
388
+ >
389
+ Adjust Layout
390
+ </Text>
391
+ </HStack>
392
+
393
+ <Collapse in={open} animateOpacity>
394
+ <VStack pb={5} spacing={5} align="stretch">
395
+
396
+ {/* Algorithm segmented control */}
397
+ <Box p={1} bg="whiteAlpha.50" borderRadius="xl">
398
+ <HStack spacing={1}>
399
+ {(Object.keys(ALGO_META) as Algorithm[]).map(a => (
400
+ <Box
401
+ key={a}
402
+ flex={1}
403
+ as="button"
404
+ py={2}
405
+ fontSize="10px"
406
+ fontWeight="800"
407
+ letterSpacing="0.08em"
408
+ textTransform="uppercase"
409
+ cursor="pointer"
410
+ onClick={() => setAlgo(a)}
411
+ bg={algo === a ? 'whiteAlpha.200' : 'transparent'}
412
+ color={algo === a ? 'white' : 'whiteAlpha.500'}
413
+ borderRadius="lg"
414
+ _hover={{ bg: algo === a ? 'whiteAlpha.200' : 'whiteAlpha.100', color: 'white' }}
415
+ transition="all 0.2s"
416
+ >
417
+ {ALGO_META[a].label}
418
+ </Box>
419
+ ))}
420
+ </HStack>
421
+ </Box>
422
+
423
+ {/* Parameters grid */}
424
+ <Box
425
+ p={4}
426
+ bg="whiteAlpha.50"
427
+ borderRadius="xl"
428
+ border="1px solid"
429
+ borderColor="whiteAlpha.100"
430
+ >
431
+ {algo === 'elk' ? (
432
+ <Grid templateColumns="1fr 1fr" gap={4}>
433
+ <FormControl gridColumn="span 2">
434
+ <FormLabel {...LabelStyle}>Algorithm</FormLabel>
435
+ <Select
436
+ size="xs"
437
+ variant="filled"
438
+ bg="whiteAlpha.100"
439
+ border="none"
440
+ _hover={{ bg: 'whiteAlpha.200' }}
441
+ value={elkConfig.algorithm}
442
+ onChange={e => setElkConfig(c => ({ ...c, algorithm: e.target.value as ElkConfig['algorithm'] }))}
443
+ >
444
+ <option value="layered">Layered</option>
445
+ <option value="force">Force</option>
446
+ <option value="mrtree">Mr. Tree</option>
447
+ <option value="box">Box</option>
448
+ </Select>
449
+ </FormControl>
450
+
451
+ {elkConfig.algorithm === 'layered' && (
452
+ <FormControl gridColumn="span 2">
453
+ <FormLabel {...LabelStyle}>Direction</FormLabel>
454
+ <Select
455
+ size="xs"
456
+ variant="filled"
457
+ bg="whiteAlpha.100"
458
+ border="none"
459
+ _hover={{ bg: 'whiteAlpha.200' }}
460
+ value={elkConfig.direction}
461
+ onChange={e => setElkConfig(c => ({ ...c, direction: e.target.value as ElkConfig['direction'] }))}
462
+ >
463
+ <option value="DOWN">Top → Bottom</option>
464
+ <option value="UP">Bottom → Top</option>
465
+ <option value="RIGHT">Left → Right</option>
466
+ <option value="LEFT">Right → Left</option>
467
+ </Select>
468
+ </FormControl>
469
+ )}
470
+
471
+ <FormControl>
472
+ <FormLabel {...LabelStyle}>Element Gap</FormLabel>
473
+ <NumberInput
474
+ size="xs"
475
+ variant="filled"
476
+ value={elkConfig.nodeSpacing}
477
+ min={10} max={400} step={10}
478
+ onChange={(_, v) => !isNaN(v) && setElkConfig(c => ({ ...c, nodeSpacing: v }))}
479
+ >
480
+ <NumberInputField bg="whiteAlpha.100" border="none" />
481
+ <NumberInputStepper>
482
+ <NumberIncrementStepper border="none" />
483
+ <NumberDecrementStepper border="none" />
484
+ </NumberInputStepper>
485
+ </NumberInput>
486
+ </FormControl>
487
+
488
+ {elkConfig.algorithm === 'layered' && (
489
+ <FormControl>
490
+ <FormLabel {...LabelStyle}>Layer Gap</FormLabel>
491
+ <NumberInput
492
+ size="xs"
493
+ variant="filled"
494
+ value={elkConfig.layerSpacing}
495
+ min={10} max={400} step={10}
496
+ onChange={(_, v) => !isNaN(v) && setElkConfig(c => ({ ...c, layerSpacing: v }))}
497
+ >
498
+ <NumberInputField bg="whiteAlpha.100" border="none" />
499
+ <NumberInputStepper>
500
+ <NumberIncrementStepper border="none" />
501
+ <NumberDecrementStepper border="none" />
502
+ </NumberInputStepper>
503
+ </NumberInput>
504
+ </FormControl>
505
+ )}
506
+ </Grid>
507
+ ) : (
508
+ <Grid templateColumns="1fr 1fr" gap={4}>
509
+ <FormControl>
510
+ <FormLabel {...LabelStyle}>Distance</FormLabel>
511
+ <NumberInput
512
+ size="xs"
513
+ variant="filled"
514
+ value={forceConfig.linkDistance}
515
+ min={50} max={600} step={10}
516
+ onChange={(_, v) => !isNaN(v) && setForceConfig(c => ({ ...c, linkDistance: v }))}
517
+ >
518
+ <NumberInputField bg="whiteAlpha.100" border="none" />
519
+ <NumberInputStepper>
520
+ <NumberIncrementStepper border="none" />
521
+ <NumberDecrementStepper border="none" />
522
+ </NumberInputStepper>
523
+ </NumberInput>
524
+ </FormControl>
525
+
526
+ <FormControl>
527
+ <FormLabel {...LabelStyle}>Strength</FormLabel>
528
+ <NumberInput
529
+ size="xs"
530
+ variant="filled"
531
+ value={forceConfig.chargeStrength}
532
+ min={-2000} max={-10} step={10}
533
+ onChange={(_, v) => !isNaN(v) && setForceConfig(c => ({ ...c, chargeStrength: v }))}
534
+ >
535
+ <NumberInputField bg="whiteAlpha.100" border="none" />
536
+ <NumberInputStepper>
537
+ <NumberIncrementStepper border="none" />
538
+ <NumberDecrementStepper border="none" />
539
+ </NumberInputStepper>
540
+ </NumberInput>
541
+ </FormControl>
542
+
543
+ <FormControl>
544
+ <FormLabel {...LabelStyle}>Radius</FormLabel>
545
+ <NumberInput
546
+ size="xs"
547
+ variant="filled"
548
+ value={forceConfig.collideRadius}
549
+ min={50} max={500} step={10}
550
+ onChange={(_, v) => !isNaN(v) && setForceConfig(c => ({ ...c, collideRadius: v }))}
551
+ >
552
+ <NumberInputField bg="whiteAlpha.100" border="none" />
553
+ <NumberInputStepper>
554
+ <NumberIncrementStepper border="none" />
555
+ <NumberDecrementStepper border="none" />
556
+ </NumberInputStepper>
557
+ </NumberInput>
558
+ </FormControl>
559
+
560
+ <FormControl>
561
+ <FormLabel {...LabelStyle}>Quality</FormLabel>
562
+ <NumberInput
563
+ size="xs"
564
+ variant="filled"
565
+ value={forceConfig.iterations}
566
+ min={50} max={1000} step={50}
567
+ onChange={(_, v) => !isNaN(v) && setForceConfig(c => ({ ...c, iterations: v }))}
568
+ >
569
+ <NumberInputField bg="whiteAlpha.100" border="none" />
570
+ <NumberInputStepper>
571
+ <NumberIncrementStepper border="none" />
572
+ <NumberDecrementStepper border="none" />
573
+ </NumberInputStepper>
574
+ </NumberInput>
575
+ </FormControl>
576
+ </Grid>
577
+ )}
578
+ </Box>
579
+ <Button
580
+ size="sm"
581
+ w="full"
582
+ colorScheme="blue"
583
+ onClick={applyLayout}
584
+ isLoading={running}
585
+ isDisabled={!canEdit || !view}
586
+ loadingText="Applying Layout..."
587
+ fontWeight="bold"
588
+ fontSize="xs"
589
+ letterSpacing="0.05em"
590
+ textTransform="uppercase"
591
+ h="32px"
592
+ transition="all 0.2s"
593
+ >
594
+ Apply Layout
595
+ </Button>
596
+ {/* Apply button */}
597
+ <VStack spacing={2} w="full">
598
+ <Button
599
+ size="sm"
600
+ w="full"
601
+ variant="outline"
602
+ colorScheme="blue"
603
+ onClick={handleCollisionRemoval}
604
+ isLoading={collisionRunning}
605
+ isDisabled={!canEdit || !view}
606
+ loadingText="Removing Connector Collisions..."
607
+ fontWeight="bold"
608
+ fontSize="xs"
609
+ letterSpacing="0.05em"
610
+ textTransform="uppercase"
611
+ h="32px"
612
+ transition="all 0.2s"
613
+
614
+ >
615
+ Adjust Connectors
616
+ </Button>
617
+
618
+ </VStack>
619
+
620
+ </VStack>
621
+ </Collapse>
622
+ </Box>
623
+ )
624
+ }