@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,512 @@
1
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import {
3
+ Badge,
4
+ Box,
5
+ Button,
6
+ Checkbox,
7
+ Divider,
8
+ Flex,
9
+ HStack,
10
+ IconButton,
11
+ Input,
12
+ InputGroup,
13
+ InputLeftElement,
14
+ Spinner,
15
+ Text,
16
+ Tooltip,
17
+ VStack,
18
+ useBreakpointValue,
19
+ } from '@chakra-ui/react'
20
+ import SlidingPanel from './SlidingPanel'
21
+ import PanelHeader from './PanelHeader'
22
+ import { AddIcon, CheckIcon, SearchIcon, ViewIcon } from '@chakra-ui/icons'
23
+ import '../styles/editor-panels.css'
24
+ import { KbdHint } from './PanelUI'
25
+ import { api } from '../api/client'
26
+ import type { LibraryElement } from '../types'
27
+ import { TYPE_COLORS } from '../types'
28
+ import { resolveIconPath } from '../utils/url'
29
+ import ScrollIndicatorWrapper from './ScrollIndicatorWrapper'
30
+
31
+ import { useViewEditorContext } from '../pages/ViewEditor/context'
32
+
33
+ interface Props {
34
+ existingElementIds: Set<number>
35
+ existingElements?: LibraryElement[]
36
+ onCreateNew: () => void
37
+ refresh: number
38
+ isOpen: boolean
39
+ onClose: () => void
40
+ onTapAdd?: (obj: LibraryElement) => void
41
+ onFindElement?: (id: number) => void
42
+ onTouchDrop?: (obj: LibraryElement, clientX: number, clientY: number) => void
43
+ }
44
+
45
+ function mergeUniqueElements(existing: LibraryElement[], incoming: LibraryElement[]) {
46
+ if (incoming.length === 0) return existing
47
+
48
+ const merged = [...existing]
49
+ const seenIds = new Set(existing.map((element) => element.id))
50
+
51
+ for (const element of incoming) {
52
+ if (seenIds.has(element.id)) continue
53
+ seenIds.add(element.id)
54
+ merged.push(element)
55
+ }
56
+
57
+ return merged
58
+ }
59
+
60
+ const DragHandle = () => (
61
+ <Box
62
+ as="svg"
63
+ width="10px"
64
+ height="16px"
65
+ viewBox="0 0 10 16"
66
+ fill="currentColor"
67
+ color="whiteAlpha.300"
68
+ flexShrink={0}
69
+ mr={2}
70
+ >
71
+ <circle cx="3" cy="2" r="1" />
72
+ <circle cx="7" cy="2" r="1" />
73
+ <circle cx="3" cy="6" r="1" />
74
+ <circle cx="7" cy="6" r="1" />
75
+ <circle cx="3" cy="10" r="1" />
76
+ <circle cx="7" cy="10" r="1" />
77
+ <circle cx="3" cy="14" r="1" />
78
+ <circle cx="7" cy="14" r="1" />
79
+ </Box>
80
+ )
81
+
82
+ /**
83
+ * Name: Element Library
84
+ * Role: Panel/drawer that displays the organization's element library and shows which are in the current view and which are not. Includes an add new element button.
85
+ * Location: Left side panel/drawer on desktop. Collapsed under a button on mobile.
86
+ * Aliases: Element sidebar, Library panel.
87
+ */
88
+ function ElementLibrary({
89
+ existingElementIds,
90
+ existingElements = [],
91
+ onCreateNew,
92
+ refresh,
93
+ isOpen,
94
+ onClose,
95
+ onTapAdd,
96
+ onFindElement,
97
+ onTouchDrop,
98
+ }: Props) {
99
+ const { canEdit } = useViewEditorContext()
100
+ const [elements, setElements] = useState<LibraryElement[]>([])
101
+ const [search, setSearch] = useState('')
102
+ const [loading, setLoading] = useState(false)
103
+ const [hasMore, setHasMore] = useState(true)
104
+ const [hideExisting, setHideExisting] = useState(false)
105
+ const isMobile = useBreakpointValue({ base: true, md: false }) ?? false
106
+
107
+ const isFetching = useRef(false)
108
+ const searchRef = useRef(search)
109
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
110
+ useEffect(() => { searchRef.current = search }, [search])
111
+
112
+ const fetchElements = useCallback(async (offset: number, currentSearch: string, isInitial = false) => {
113
+ if (isFetching.current) return
114
+ isFetching.current = true
115
+ setLoading(true)
116
+ try {
117
+ const limit = 20
118
+ const newElements = await api.elements.list({ limit, offset, search: currentSearch })
119
+ if (isInitial) {
120
+ setElements(mergeUniqueElements([], newElements))
121
+ } else {
122
+ setElements((prev) => mergeUniqueElements(prev, newElements))
123
+ }
124
+ setHasMore(newElements.length === limit)
125
+ } catch (err) {
126
+ console.error('Failed to fetch elements:', err)
127
+ } finally {
128
+ isFetching.current = false
129
+ setLoading(false)
130
+ }
131
+ }, [])
132
+
133
+ useEffect(() => {
134
+ if (isOpen) {
135
+ fetchElements(0, searchRef.current, true)
136
+ }
137
+ }, [isOpen, refresh, fetchElements])
138
+
139
+ // Debounced search
140
+ useEffect(() => {
141
+ if (!isOpen) return
142
+ const timer = setTimeout(() => {
143
+ fetchElements(0, search, true)
144
+ }, 300)
145
+ return () => clearTimeout(timer)
146
+ }, [search, isOpen, fetchElements])
147
+
148
+ const filtered = useMemo(() => {
149
+ let searchResults = existingElements
150
+ if (search.trim()) {
151
+ const query = search.toLowerCase()
152
+ searchResults = searchResults.filter(o =>
153
+ (o.name || '').toLowerCase().includes(query) ||
154
+ (o.description || '').toLowerCase().includes(query) ||
155
+ (o.technology || '').toLowerCase().includes(query) ||
156
+ (o.kind || '').toLowerCase().includes(query)
157
+ )
158
+ }
159
+
160
+ // Combine fetched elements with existing elements from the view to ensure they're always visible and sorted first.
161
+ let result = mergeUniqueElements(searchResults, elements)
162
+ if (hideExisting) {
163
+ result = result.filter((o) => !existingElementIds.has(o.id))
164
+ }
165
+
166
+ return result.sort((a, b) => {
167
+ const aExists = existingElementIds.has(a.id)
168
+ const bExists = existingElementIds.has(b.id)
169
+ if (aExists && !bExists) return -1
170
+ if (!aExists && bExists) return 1
171
+ return (a.name || '').localeCompare(b.name || '')
172
+ })
173
+ }, [existingElementIds, existingElements, hideExisting, elements, search])
174
+
175
+ // If a fetch completes but the container still isn't scrollable, keep loading
176
+ useEffect(() => {
177
+ if (!hasMore || loading || !isOpen) return
178
+ const el = scrollContainerRef.current
179
+ if (!el) return
180
+ if (el.scrollHeight <= el.clientHeight) {
181
+ fetchElements(elements.length, search)
182
+ }
183
+ }, [elements, hasMore, loading, isOpen, search, fetchElements])
184
+
185
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
186
+ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget
187
+ if (scrollHeight - scrollTop <= clientHeight + 50 && hasMore && !loading) {
188
+ fetchElements(elements.length, search)
189
+ }
190
+ }
191
+
192
+ const onDragStart = (e: React.DragEvent, obj: LibraryElement) => {
193
+ if (!canEdit) return
194
+ e.dataTransfer.setData('application/diag-element', JSON.stringify(obj))
195
+ e.dataTransfer.effectAllowed = 'move'
196
+ }
197
+
198
+ const onItemPointerDown = (e: React.PointerEvent, obj: LibraryElement) => {
199
+ if (!canEdit) return
200
+ if (e.pointerType === 'mouse') return
201
+ if (existingElementIds.has(obj.id)) return
202
+ // Don't intercept taps on buttons
203
+ if ((e.target as HTMLElement).closest('button')) return
204
+
205
+ const startX = e.clientX
206
+ const startY = e.clientY
207
+ const pointerId = e.pointerId
208
+ let dragging = false
209
+ let decided = false
210
+ let ghostEl: HTMLDivElement | null = null
211
+ let captureDiv: HTMLDivElement | null = null
212
+
213
+ const showGhost = (x: number, y: number): HTMLDivElement => {
214
+ const el = document.createElement('div')
215
+ const color = TYPE_COLORS[obj.kind ?? ''] ?? 'gray'
216
+ el.style.cssText = [
217
+ 'position:fixed',
218
+ `left:${x}px`,
219
+ `top:${y}px`,
220
+ 'transform:translate(-50%,-120%)',
221
+ 'z-index:9999',
222
+ 'pointer-events:none',
223
+ 'background:#161e2b',
224
+ 'border:2px solid var(--accent)',
225
+ 'border-radius:8px',
226
+ 'padding:8px 10px',
227
+ 'box-shadow:0 8px 30px rgba(0,0,0,0.6)',
228
+ 'opacity:0.75',
229
+ 'max-width:180px',
230
+ 'overflow:hidden',
231
+ ].join(';')
232
+ el.innerHTML = `
233
+ <div style="color:#e2e8f0;font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${obj.name}</div>
234
+ <span style="font-size:10px;font-weight:600;text-transform:uppercase;color:${color === 'gray' ? '#a0aec0' : 'var(--accent)'};">${obj.kind ?? ''}</span>
235
+ `
236
+ document.body.appendChild(el)
237
+ return el as HTMLDivElement
238
+ }
239
+
240
+ const cleanup = () => {
241
+ document.removeEventListener('pointermove', onMove)
242
+ document.removeEventListener('pointerup', onUp)
243
+ document.removeEventListener('pointercancel', onCancel)
244
+ captureDiv?.remove()
245
+ captureDiv = null
246
+ ghostEl?.remove()
247
+ ghostEl = null
248
+ }
249
+
250
+ const onMove = (me: PointerEvent) => {
251
+ if (me.pointerId !== pointerId) return
252
+ const dx = me.clientX - startX
253
+ const dy = me.clientY - startY
254
+ if (!decided && Math.hypot(dx, dy) > 8) {
255
+ decided = true
256
+ if (Math.abs(dy) > Math.abs(dx)) {
257
+ // Primarily vertical it's a scroll, bail out
258
+ cleanup()
259
+ return
260
+ }
261
+ // Primarily horizontal it's a drag
262
+ dragging = true
263
+ captureDiv = document.createElement('div')
264
+ captureDiv.style.cssText = 'position:fixed;inset:0;z-index:9998;touch-action:none;'
265
+ document.body.appendChild(captureDiv)
266
+ try { captureDiv.setPointerCapture(pointerId) } catch { /* intentionally empty */ }
267
+ onClose()
268
+ ghostEl = showGhost(me.clientX, me.clientY)
269
+ }
270
+ if (dragging) {
271
+ me.preventDefault()
272
+ if (ghostEl) {
273
+ ghostEl.style.left = `${me.clientX}px`
274
+ ghostEl.style.top = `${me.clientY}px`
275
+ }
276
+ }
277
+ }
278
+
279
+ const onUp = (ue: PointerEvent) => {
280
+ if (ue.pointerId !== pointerId) return
281
+ const wasDropped = dragging
282
+ cleanup()
283
+ if (wasDropped) onTouchDrop?.(obj, ue.clientX, ue.clientY)
284
+ }
285
+
286
+ const onCancel = (ce: PointerEvent) => {
287
+ if (ce.pointerId !== pointerId) return
288
+ cleanup()
289
+ }
290
+
291
+ document.addEventListener('pointermove', onMove)
292
+ document.addEventListener('pointerup', onUp)
293
+ document.addEventListener('pointercancel', onCancel)
294
+ }
295
+
296
+ const listContent = (
297
+ <>
298
+ {/* New Element */}
299
+ {canEdit && (
300
+ <>
301
+ <Box flexShrink={0}>
302
+ <Box
303
+ as="button"
304
+ className="panel-action-button"
305
+ onClick={onCreateNew}
306
+ role="group"
307
+ >
308
+ <Box className="panel-action-icon-container" color="var(--accent)">
309
+ <AddIcon boxSize={4} />
310
+ </Box>
311
+ <VStack align="start" spacing={0} flex={1}>
312
+ <Text fontWeight="semibold" fontSize="sm" color="white">New Element</Text>
313
+ <Text fontSize="10px" color="gray.500">Add to the catalog</Text>
314
+ </VStack>
315
+ <KbdHint>C</KbdHint>
316
+ </Box>
317
+ </Box>
318
+ <Divider borderColor="whiteAlpha.100" />
319
+ </>
320
+ )}
321
+
322
+ {/* Search */}
323
+ <Box className="panel-search-container">
324
+ <InputGroup size="sm">
325
+ <InputLeftElement pointerEvents="none">
326
+ <SearchIcon color="gray.500" />
327
+ </InputLeftElement>
328
+ <Input
329
+ className="panel-search-input"
330
+ placeholder="Search catalog…"
331
+ value={search}
332
+ onChange={(e) => setSearch(e.target.value)}
333
+ />
334
+ </InputGroup>
335
+ <Checkbox
336
+ size="sm"
337
+ mt={2}
338
+ ml={0.5}
339
+ colorScheme="blue"
340
+ isChecked={hideExisting}
341
+ onChange={(e) => setHideExisting(e.target.checked)}
342
+ >
343
+ <Text fontSize="11px" color="gray.400">Hide existing</Text>
344
+ </Checkbox>
345
+ </Box>
346
+
347
+
348
+ {/* List */}
349
+ <ScrollIndicatorWrapper ref={scrollContainerRef} flex={1} px={2} pt={2} pb={3} onScroll={handleScroll}>
350
+ <VStack align="stretch" spacing={1.5}>
351
+ {loading && elements.length === 0 && (
352
+ <Flex justify="center" py={10}>
353
+ <Spinner size="sm" color="blue.500" thickness="2px" />
354
+ </Flex>
355
+ )}
356
+
357
+ {!loading && filtered.length === 0 && elements.length === 0 && (
358
+ <VStack spacing={2} py={6} px={2} textAlign="center">
359
+ <Text color="gray.600" fontSize="sm" fontWeight="medium">No elements yet</Text>
360
+ <Text color="gray.700" fontSize="xs" lineHeight="tall">
361
+ Elements are reusable building blocks - services, databases, people, and more.
362
+ </Text>
363
+ {canEdit && (
364
+ <Button size="xs" colorScheme="blue" variant="outline" mt={1} onClick={onCreateNew}>
365
+ Create your first element
366
+ </Button>
367
+ )}
368
+ </VStack>
369
+ )}
370
+ {filtered.length === 0 && elements.length > 0 && (
371
+ <Text color="gray.700" textAlign="center" py={6} fontSize="sm">
372
+ No results for &ldquo;{search}&rdquo;
373
+ </Text>
374
+ )}
375
+ {filtered.map((obj) => {
376
+ const already = existingElementIds.has(obj.id)
377
+ const color = TYPE_COLORS[obj.kind ?? ''] ?? 'gray'
378
+ const hasLogo = !!obj.logo_url
379
+
380
+ return (
381
+ <Tooltip
382
+ key={obj.id}
383
+ label={already ? 'Already on canvas' : isMobile ? 'Drag to canvas or tap +' : 'Drag to canvas'}
384
+ placement="right"
385
+ openDelay={500}
386
+ >
387
+ <Box
388
+ draggable={canEdit && !already && !isMobile}
389
+ onDragStart={(e) => onDragStart(e, obj)}
390
+ onPointerDown={(e) => onItemPointerDown(e, obj)}
391
+ onClick={() => {
392
+ if (already && onFindElement) onFindElement(obj.id)
393
+ }}
394
+ p={2}
395
+ minH="54px"
396
+ display="flex"
397
+ alignItems="center"
398
+ bg={already ? 'rgba(var(--accent-rgb), 0.06)' : 'whiteAlpha.50'}
399
+ border="1px solid"
400
+ borderColor={already ? 'rgba(var(--accent-rgb), 0.25)' : 'whiteAlpha.100'}
401
+ rounded="lg"
402
+ cursor={!canEdit ? 'default' : already ? 'pointer' : (isMobile ? 'pointer' : 'grab')}
403
+ position="relative"
404
+ role="group"
405
+ transition="all 0.15s ease"
406
+ _hover={{
407
+ bg: already ? 'rgba(var(--accent-rgb), 0.1)' : 'whiteAlpha.100',
408
+ borderColor: already ? 'rgba(var(--accent-rgb), 0.45)' : 'whiteAlpha.300',
409
+ transform: already ? 'none' : 'translateY(-1px)',
410
+ boxShadow: already ? 'none' : '0 4px 12px rgba(0,0,0,0.4)',
411
+ }}
412
+ >
413
+ <HStack spacing={2} align="center" w="full">
414
+ <HStack spacing={0} flexShrink={0}>
415
+ {already && onFindElement && (
416
+ <Tooltip label="Find on canvas" placement="top" openDelay={200}>
417
+ <IconButton
418
+ aria-label="Find on canvas"
419
+ icon={<ViewIcon />}
420
+ size="xs"
421
+ variant="ghost"
422
+ colorScheme="blue"
423
+ color="var(--accent)"
424
+ _hover={{ bg: 'rgba(var(--accent-rgb), 0.12)' }}
425
+ onClick={(e) => {
426
+ e.stopPropagation()
427
+ onFindElement(obj.id)
428
+ }}
429
+ />
430
+ </Tooltip>
431
+ )}
432
+ {canEdit && !already && onTapAdd && (
433
+ <Tooltip label="Add to canvas" placement="top" openDelay={200}>
434
+ <IconButton
435
+ aria-label="Add to canvas"
436
+ icon={<AddIcon boxSize={2.5} />}
437
+ size="xs"
438
+ colorScheme="blue"
439
+ variant="ghost"
440
+ flexShrink={0}
441
+ onClick={(e) => {
442
+ e.stopPropagation()
443
+ onTapAdd(obj)
444
+ if (isMobile) onClose()
445
+ }}
446
+ />
447
+ </Tooltip>
448
+ )}
449
+ </HStack>
450
+
451
+ {hasLogo ? (
452
+ <Flex w="24px" h="24px" align="center" justify="center" flexShrink={0} bg="whiteAlpha.100" rounded="md" p={1}>
453
+ <Box as="img" src={resolveIconPath(obj.logo_url!)} maxW="100%" maxH="100%" objectFit="contain" />
454
+ </Flex>
455
+ ) : (
456
+ <Flex w="24px" h="24px" align="center" justify="center" flexShrink={0} bg={`${color}.900`} color={`${color}.300`} rounded="md" fontSize="10px" fontWeight="bold">
457
+ {(obj.kind || '?').charAt(0).toUpperCase()}
458
+ </Flex>
459
+ )}
460
+
461
+ <Box flex={1} minW={0}>
462
+ <Text fontSize="sm" fontWeight="medium" noOfLines={1} color={already ? 'gray.400' : 'gray.100'}>
463
+ {obj.name}
464
+ </Text>
465
+ <HStack spacing={1} mt={0.5}>
466
+ <Badge variant="subtle" colorScheme={color} fontSize="8px" px={1} rounded="sm">
467
+ {obj.kind}
468
+ </Badge>
469
+ {obj.technology && (
470
+ <Text fontSize="10px" color="gray.500" noOfLines={1}>
471
+ {obj.technology}
472
+ </Text>
473
+ )}
474
+ </HStack>
475
+ </Box>
476
+
477
+ <Flex w="24px" justify="center" align="center" ml={0} flexShrink={0}>
478
+ {already ? (
479
+ <CheckIcon color="var(--accent)" boxSize={3} transform="translateX(-4px)" />
480
+ ) : (
481
+ canEdit && !isMobile && <DragHandle />
482
+ )}
483
+ </Flex>
484
+
485
+ </HStack>
486
+ </Box>
487
+ </Tooltip>
488
+ )
489
+ })}
490
+
491
+ {loading && elements.length > 0 && (
492
+ <Flex justify="center" py={2}>
493
+ <Spinner size="xs" color="blue.500" />
494
+ </Flex>
495
+ )}
496
+ </VStack>
497
+ </ScrollIndicatorWrapper>
498
+ </>
499
+ )
500
+
501
+ return (
502
+ <SlidingPanel isOpen={isOpen} onClose={onClose} panelKey="elementlibrary" side="left" width="300px" hasBackdrop={false} zIndex={1000}>
503
+ <PanelHeader title="Element Library" onClose={onClose} hasCloseButton={isMobile} />
504
+
505
+ <Box p={0} display="flex" flexDir="column" overflow="hidden" flex={1}>
506
+ {listContent}
507
+ </Box>
508
+ </SlidingPanel>
509
+ )
510
+ }
511
+
512
+ export default memo(ElementLibrary)