@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,191 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Flex,
6
+ Input,
7
+ Modal,
8
+ ModalBody,
9
+ ModalContent,
10
+ ModalFooter,
11
+ ModalHeader,
12
+ ModalOverlay,
13
+ Text,
14
+ } from '@chakra-ui/react'
15
+ import type { ViewTreeNode } from '../types'
16
+
17
+ interface Props {
18
+ isOpen: boolean
19
+ onClose: () => void
20
+ targetId: number | null
21
+ treeData: ViewTreeNode[]
22
+ onConfirm: (childId: number) => Promise<void>
23
+ }
24
+
25
+ export default function SetChildModal({ isOpen, onClose, targetId, treeData, onConfirm }: Props) {
26
+ const [search, setSearch] = useState('')
27
+ const [selectedId, setSelectedId] = useState<number | null>(null)
28
+ const [saving, setSaving] = useState(false)
29
+
30
+ useEffect(() => {
31
+ if (isOpen) {
32
+ setSearch('')
33
+ setSelectedId(null)
34
+ }
35
+ }, [isOpen])
36
+
37
+ const targetNode = useMemo(
38
+ () => treeData.find((n) => n.id === targetId),
39
+ [treeData, targetId]
40
+ )
41
+
42
+ // Forbidden: self + all ancestors of the target (to prevent cycles)
43
+ const forbiddenIds = useMemo(() => {
44
+ if (targetId === null) return new Set<number>()
45
+ const ancestors = new Set<number>()
46
+ ancestors.add(targetId)
47
+ let current = treeData.find((n) => n.id === targetId)
48
+ while (current && current.parent_view_id !== null) {
49
+ ancestors.add(current.parent_view_id)
50
+ current = treeData.find((n) => n.id === current!.parent_view_id)
51
+ }
52
+ return ancestors
53
+ }, [treeData, targetId])
54
+
55
+ // IDs that are already direct children of this diagram
56
+ const existingChildIds = useMemo(() => {
57
+ if (targetId === null) return new Set<number>()
58
+ return new Set(treeData.filter((n) => n.parent_view_id === targetId).map((n) => n.id))
59
+ }, [treeData, targetId])
60
+
61
+ const filteredList = useMemo(() => {
62
+ const q = search.trim().toLowerCase()
63
+ return treeData
64
+ .filter((n) => !forbiddenIds.has(n.id))
65
+ .filter((n) => !q || n.name.toLowerCase().includes(q))
66
+ .sort((a, b) => a.name.localeCompare(b.name))
67
+ }, [treeData, forbiddenIds, search])
68
+
69
+ const handleConfirm = async () => {
70
+ if (selectedId === null) return
71
+ setSaving(true)
72
+ try {
73
+ await onConfirm(selectedId)
74
+ } finally {
75
+ setSaving(false)
76
+ }
77
+ }
78
+
79
+ return (
80
+ <Modal isOpen={isOpen} onClose={onClose} isCentered size="sm">
81
+ <ModalOverlay bg="blackAlpha.700" backdropFilter="blur(4px)" />
82
+ <ModalContent
83
+ bg="var(--bg-panel)"
84
+ border="1px solid"
85
+ borderColor="var(--border-main)"
86
+ borderRadius="12px"
87
+ boxShadow="0 20px 60px rgba(0,0,0,0.6)"
88
+ >
89
+ <ModalHeader color="gray.100" pb={2} fontSize="md" fontWeight="semibold">
90
+ Add Existing Child
91
+ </ModalHeader>
92
+ <ModalBody pb={0}>
93
+ {targetNode && (
94
+ <Text fontSize="xs" color="gray.500" mb={3}>
95
+ Parent: <Text as="span" color="gray.300" fontWeight="medium">{targetNode.name}</Text>
96
+ </Text>
97
+ )}
98
+ <Input
99
+ placeholder="Search diagrams…"
100
+ size="sm"
101
+ value={search}
102
+ onChange={(e) => setSearch(e.target.value)}
103
+ mb={2}
104
+ autoFocus
105
+ bg="var(--bg-canvas)"
106
+ borderColor="gray.600"
107
+ color="gray.200"
108
+ _placeholder={{ color: 'gray.600' }}
109
+ _focus={{ borderColor: 'var(--accent)', boxShadow: '0 0 0 1px var(--accent)' }}
110
+ onKeyDown={(e) => { if (e.key === 'Enter' && selectedId) handleConfirm() }}
111
+ />
112
+ <Box
113
+ maxH="260px"
114
+ overflowY="auto"
115
+ borderRadius="8px"
116
+ border="1px solid"
117
+ borderColor="var(--border-main)"
118
+ css={{ '&::-webkit-scrollbar': { width: '4px' }, '&::-webkit-scrollbar-thumb': { background: '#4a5568', borderRadius: '2px' } }}
119
+ >
120
+ {filteredList.length === 0 && (
121
+ <Box px={3} py={4} textAlign="center">
122
+ <Text fontSize="sm" color="gray.600">No diagrams available</Text>
123
+ </Box>
124
+ )}
125
+ {filteredList.map((n, idx) => {
126
+ const isAlreadyChild = existingChildIds.has(n.id)
127
+ const isSelected = selectedId === n.id
128
+ return (
129
+ <Flex
130
+ key={n.id}
131
+ px={3}
132
+ py="9px"
133
+ align="center"
134
+ justify="space-between"
135
+ cursor="pointer"
136
+ bg={isSelected ? 'rgba(var(--accent-rgb), 0.12)' : 'transparent'}
137
+ borderBottom={idx < filteredList.length - 1 ? '1px solid' : 'none'}
138
+ borderColor="var(--border-main)"
139
+ transition="background 0.1s"
140
+ _hover={{ bg: isSelected ? 'rgba(var(--accent-rgb), 0.16)' : 'whiteAlpha.50' }}
141
+ onClick={() => setSelectedId(isSelected ? null : n.id)}
142
+ >
143
+ <Text
144
+ fontSize="sm"
145
+ color={isSelected ? 'blue.200' : 'gray.300'}
146
+ fontWeight={isSelected ? 'medium' : 'normal'}
147
+ noOfLines={1}
148
+ flex={1}
149
+ minW={0}
150
+ pr={2}
151
+ >
152
+ {n.name}
153
+ </Text>
154
+ <Flex align="center" gap={2} flexShrink={0}>
155
+ {isAlreadyChild && (
156
+ <Text fontSize="8px" color="blue.500" letterSpacing="0.1em" fontWeight="bold" textTransform="uppercase">
157
+ child
158
+ </Text>
159
+ )}
160
+ {n.parent_view_id !== null && !isAlreadyChild && (
161
+ <Text fontSize="8px" color="gray.600" letterSpacing="0.08em" fontWeight="bold" textTransform="uppercase">
162
+ has parent
163
+ </Text>
164
+ )}
165
+ {isSelected && (
166
+ <Box color="blue.400" fontSize="13px" lineHeight={1}>✓</Box>
167
+ )}
168
+ </Flex>
169
+ </Flex>
170
+ )
171
+ })}
172
+ </Box>
173
+ </ModalBody>
174
+ <ModalFooter gap={2} pt={3}>
175
+ <Button size="sm" variant="ghost" color="gray.500" _hover={{ color: 'gray.300', bg: 'whiteAlpha.100' }} onClick={onClose}>
176
+ Cancel
177
+ </Button>
178
+ <Button
179
+ size="sm"
180
+ colorScheme="blue"
181
+ isDisabled={selectedId === null}
182
+ isLoading={saving}
183
+ onClick={handleConfirm}
184
+ >
185
+ Set as Child
186
+ </Button>
187
+ </ModalFooter>
188
+ </ModalContent>
189
+ </Modal>
190
+ )
191
+ }
@@ -0,0 +1,187 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Flex,
6
+ Input,
7
+ Modal,
8
+ ModalBody,
9
+ ModalContent,
10
+ ModalFooter,
11
+ ModalHeader,
12
+ ModalOverlay,
13
+ Text,
14
+ } from '@chakra-ui/react'
15
+ import type { ViewTreeNode } from '../types'
16
+
17
+ interface Props {
18
+ isOpen: boolean
19
+ onClose: () => void
20
+ targetId: number | null
21
+ treeData: ViewTreeNode[]
22
+ onConfirm: (newParentId: number) => Promise<void>
23
+ }
24
+
25
+ export default function SetParentModal({ isOpen, onClose, targetId, treeData, onConfirm }: Props) {
26
+ const [search, setSearch] = useState('')
27
+ const [selectedId, setSelectedId] = useState<number | null>(null)
28
+ const [saving, setSaving] = useState(false)
29
+
30
+ useEffect(() => {
31
+ if (isOpen) {
32
+ setSearch('')
33
+ setSelectedId(null)
34
+ }
35
+ }, [isOpen])
36
+
37
+ const targetNode = useMemo(
38
+ () => treeData.find((n) => n.id === targetId),
39
+ [treeData, targetId]
40
+ )
41
+
42
+ // Compute forbidden IDs: self + all descendants (BFS)
43
+ const forbiddenIds = useMemo(() => {
44
+ if (targetId === null) return new Set<number>()
45
+ const descendants = new Set<number>()
46
+ const stack: number[] = [targetId]
47
+ while (stack.length > 0) {
48
+ const id = stack.pop()!
49
+ treeData.forEach((n) => {
50
+ if (n.parent_view_id === id) {
51
+ descendants.add(n.id)
52
+ stack.push(n.id)
53
+ }
54
+ })
55
+ }
56
+ descendants.add(targetId)
57
+ return descendants
58
+ }, [treeData, targetId])
59
+
60
+ const currentParentId = targetNode?.parent_view_id ?? null
61
+
62
+ const filteredList = useMemo(() => {
63
+ const q = search.trim().toLowerCase()
64
+ return treeData
65
+ .filter((n) => !forbiddenIds.has(n.id))
66
+ .filter((n) => !q || n.name.toLowerCase().includes(q))
67
+ .sort((a, b) => a.name.localeCompare(b.name))
68
+ }, [treeData, forbiddenIds, search])
69
+
70
+ const handleConfirm = async () => {
71
+ if (selectedId === null) return
72
+ setSaving(true)
73
+ try {
74
+ await onConfirm(selectedId)
75
+ } finally {
76
+ setSaving(false)
77
+ }
78
+ }
79
+
80
+ return (
81
+ <Modal isOpen={isOpen} onClose={onClose} isCentered size="sm">
82
+ <ModalOverlay bg="blackAlpha.700" backdropFilter="blur(4px)" />
83
+ <ModalContent
84
+ bg="var(--bg-panel)"
85
+ border="1px solid"
86
+ borderColor="var(--border-main)"
87
+ borderRadius="12px"
88
+ boxShadow="0 20px 60px rgba(0,0,0,0.6)"
89
+ >
90
+ <ModalHeader color="gray.100" pb={2} fontSize="md" fontWeight="semibold">
91
+ Set Parent Diagram
92
+ </ModalHeader>
93
+ <ModalBody pb={0}>
94
+ {targetNode && (
95
+ <Text fontSize="xs" color="gray.500" mb={3}>
96
+ Moving: <Text as="span" color="gray.300" fontWeight="medium">{targetNode.name}</Text>
97
+ </Text>
98
+ )}
99
+ <Input
100
+ placeholder="Search diagrams…"
101
+ size="sm"
102
+ value={search}
103
+ onChange={(e) => setSearch(e.target.value)}
104
+ mb={2}
105
+ autoFocus
106
+ bg="var(--bg-canvas)"
107
+ borderColor="gray.600"
108
+ color="gray.200"
109
+ _placeholder={{ color: 'gray.600' }}
110
+ _focus={{ borderColor: 'var(--accent)', boxShadow: '0 0 0 1px var(--accent)' }}
111
+ onKeyDown={(e) => { if (e.key === 'Enter' && selectedId) handleConfirm() }}
112
+ />
113
+ <Box
114
+ maxH="260px"
115
+ overflowY="auto"
116
+ borderRadius="8px"
117
+ border="1px solid"
118
+ borderColor="var(--border-main)"
119
+ css={{ '&::-webkit-scrollbar': { width: '4px' }, '&::-webkit-scrollbar-thumb': { background: '#4a5568', borderRadius: '2px' } }}
120
+ >
121
+ {filteredList.length === 0 && (
122
+ <Box px={3} py={4} textAlign="center">
123
+ <Text fontSize="sm" color="gray.600">No diagrams available</Text>
124
+ </Box>
125
+ )}
126
+ {filteredList.map((n, idx) => {
127
+ const isCurrent = n.id === currentParentId
128
+ const isSelected = selectedId === n.id
129
+ return (
130
+ <Flex
131
+ key={n.id}
132
+ px={3}
133
+ py="9px"
134
+ align="center"
135
+ justify="space-between"
136
+ cursor="pointer"
137
+ bg={isSelected ? 'rgba(var(--accent-rgb), 0.12)' : 'transparent'}
138
+ borderBottom={idx < filteredList.length - 1 ? '1px solid' : 'none'}
139
+ borderColor="var(--border-main)"
140
+ transition="background 0.1s"
141
+ _hover={{ bg: isSelected ? 'rgba(var(--accent-rgb), 0.16)' : 'whiteAlpha.50' }}
142
+ onClick={() => setSelectedId(isSelected ? null : n.id)}
143
+ >
144
+ <Text
145
+ fontSize="sm"
146
+ color={isSelected ? 'blue.200' : 'gray.300'}
147
+ fontWeight={isSelected ? 'medium' : 'normal'}
148
+ noOfLines={1}
149
+ flex={1}
150
+ minW={0}
151
+ pr={2}
152
+ >
153
+ {n.name}
154
+ </Text>
155
+ <Flex align="center" gap={2} flexShrink={0}>
156
+ {isCurrent && (
157
+ <Text fontSize="8px" color="blue.500" letterSpacing="0.1em" fontWeight="bold" textTransform="uppercase">
158
+ current
159
+ </Text>
160
+ )}
161
+ {isSelected && (
162
+ <Box color="blue.400" fontSize="13px" lineHeight={1}>✓</Box>
163
+ )}
164
+ </Flex>
165
+ </Flex>
166
+ )
167
+ })}
168
+ </Box>
169
+ </ModalBody>
170
+ <ModalFooter gap={2} pt={3}>
171
+ <Button size="sm" variant="ghost" color="gray.500" _hover={{ color: 'gray.300', bg: 'whiteAlpha.100' }} onClick={onClose}>
172
+ Cancel
173
+ </Button>
174
+ <Button
175
+ size="sm"
176
+ colorScheme="blue"
177
+ isDisabled={selectedId === null}
178
+ isLoading={saving}
179
+ onClick={handleConfirm}
180
+ >
181
+ Set Parent
182
+ </Button>
183
+ </ModalFooter>
184
+ </ModalContent>
185
+ </Modal>
186
+ )
187
+ }
@@ -0,0 +1,114 @@
1
+ import { AnimatePresence, motion } from 'framer-motion'
2
+ import { Box } from '@chakra-ui/react'
3
+ import { type ReactNode, useRef, useEffect } from 'react'
4
+
5
+ const EASE = [0.25, 0.46, 0.45, 0.94]
6
+
7
+ interface Props {
8
+ isOpen: boolean
9
+ onClose: () => void
10
+ panelKey: string
11
+ side?: 'left' | 'right'
12
+ width?: string | Record<string, string>
13
+ minWidth?: string | Record<string, string>
14
+ maxHeight?: string
15
+ height?: string
16
+ hasBackdrop?: boolean
17
+ zIndex?: number
18
+ children: ReactNode
19
+ }
20
+
21
+ export default function SlidingPanel({
22
+ isOpen,
23
+ onClose,
24
+ panelKey,
25
+ side = 'right',
26
+ width = '300px',
27
+ minWidth,
28
+ maxHeight = 'calc(90vh - 7rem)',
29
+ height = 'calc(90vh - 7rem)',
30
+ hasBackdrop = true,
31
+ zIndex = 1000,
32
+ children,
33
+ }: Props) {
34
+ // Use width if it's a fixed value, otherwise default to a safe offscreen distance
35
+ const resolvedWidth = typeof width === 'string' ? width : '320px'
36
+ const isFixed = resolvedWidth.endsWith('px')
37
+ const widthVal = isFixed ? parseInt(resolvedWidth) : 320
38
+ const offset = side === 'right' ? widthVal + 24 : -(widthVal + 24)
39
+
40
+ // Prevent wheel events from propagating to the canvas (React Flow would pan/zoom)
41
+ const boxRef = useRef<HTMLDivElement>(null)
42
+ useEffect(() => {
43
+ const el = boxRef.current
44
+ if (!el) return
45
+ const handler = (e: WheelEvent) => e.stopPropagation()
46
+ el.addEventListener('wheel', handler, { passive: true })
47
+ return () => el.removeEventListener('wheel', handler)
48
+ }, [isOpen])
49
+
50
+ return (
51
+ <>
52
+ {hasBackdrop && (
53
+ <AnimatePresence>
54
+ {isOpen && (
55
+ <motion.div
56
+ key={`${panelKey}-backdrop`}
57
+ initial={{ opacity: 0 }}
58
+ animate={{ opacity: 1 }}
59
+ exit={{ opacity: 0 }}
60
+ transition={{ duration: 0.15 }}
61
+ style={{ position: 'fixed', inset: 0, zIndex: zIndex - 1 }}
62
+ onClick={onClose}
63
+ />
64
+ )}
65
+ </AnimatePresence>
66
+ )}
67
+ <AnimatePresence>
68
+ {isOpen && (
69
+ <motion.div
70
+ key={`${panelKey}-panel`}
71
+ initial={{ x: offset, opacity: 0 }}
72
+ animate={{ x: 0, opacity: 1 }}
73
+ exit={{ x: offset, opacity: 0 }}
74
+ transition={{ duration: 0.2, ease: EASE }}
75
+ style={{
76
+ position: 'fixed',
77
+ [side]: '1rem',
78
+ top: 0,
79
+ bottom: 0,
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ zIndex,
83
+ pointerEvents: 'none',
84
+ }}
85
+ >
86
+ <Box
87
+ ref={boxRef}
88
+ pointerEvents="auto"
89
+ w={width}
90
+ minW={minWidth}
91
+ maxW="calc(100vw - 24px)"
92
+ h={height}
93
+ maxH={maxHeight}
94
+ overflow="hidden"
95
+ display="flex"
96
+ flexDir="column"
97
+ bg="var(--bg-panel)"
98
+ bgImage="var(--grad-panel)"
99
+ backdropFilter="blur(24px)"
100
+ border="1px solid"
101
+ borderColor="whiteAlpha.100"
102
+ borderTop="2px solid"
103
+ borderTopColor="var(--accent)"
104
+ rounded="xl"
105
+ shadow="panel"
106
+ >
107
+ {children}
108
+ </Box>
109
+ </motion.div>
110
+ )}
111
+ </AnimatePresence>
112
+ </>
113
+ )
114
+ }
@@ -0,0 +1,142 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Box, HStack, Input, Text, VStack } from '@chakra-ui/react'
3
+
4
+ interface Props {
5
+ currentTags: string[]
6
+ availableTags: string[]
7
+ onAddTag: (tag: string) => void
8
+ isReadOnly?: boolean
9
+ }
10
+
11
+ export default function TagUpsert({
12
+ currentTags,
13
+ availableTags,
14
+ onAddTag,
15
+ isReadOnly = false,
16
+ }: Props) {
17
+ const [query, setQuery] = useState('')
18
+ const [activeIndex, setActiveIndex] = useState(0)
19
+ const inputRef = useRef<HTMLInputElement>(null)
20
+
21
+ const filtered = (() => {
22
+ if (!query.trim()) return []
23
+ const q = query.toLowerCase()
24
+ return availableTags
25
+ .filter((t) => t.toLowerCase().includes(q) && !currentTags.includes(t))
26
+ .slice(0, 8)
27
+ })()
28
+
29
+ type ResultItem =
30
+ | { kind: 'new'; label: string }
31
+ | { kind: 'existing'; tag: string }
32
+
33
+ const results: ResultItem[] = []
34
+
35
+ if (query.trim() && !currentTags.includes(query.trim())) {
36
+ results.push({ kind: 'new', label: query.trim() })
37
+ }
38
+
39
+ filtered.forEach(tag => {
40
+ if (tag.toLowerCase() !== query.trim().toLowerCase()) {
41
+ results.push({ kind: 'existing', tag })
42
+ }
43
+ })
44
+
45
+ useEffect(() => { setActiveIndex(0) }, [query])
46
+
47
+ const confirm = (idx: number) => {
48
+ const item = results[idx]
49
+ if (!item) return
50
+ if (item.kind === 'new') {
51
+ onAddTag(item.label)
52
+ } else {
53
+ onAddTag(item.tag)
54
+ }
55
+ setQuery('')
56
+ }
57
+
58
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
59
+ if (e.key === 'Enter') {
60
+ e.preventDefault()
61
+ if (results.length > 0) {
62
+ confirm(activeIndex)
63
+ } else if (query.trim() && !currentTags.includes(query.trim())) {
64
+ onAddTag(query.trim())
65
+ setQuery('')
66
+ }
67
+ return
68
+ }
69
+ if (e.key === 'ArrowDown') {
70
+ e.preventDefault()
71
+ setActiveIndex((i) => Math.min(i + 1, results.length - 1))
72
+ }
73
+ if (e.key === 'ArrowUp') {
74
+ e.preventDefault()
75
+ setActiveIndex((i) => Math.max(i - 1, 0))
76
+ }
77
+ }
78
+
79
+ return (
80
+ <VStack align="stretch" spacing={2}>
81
+ <Box position="relative">
82
+ <Input
83
+ ref={inputRef}
84
+ value={query}
85
+ onChange={(e) => setQuery(e.target.value)}
86
+ onKeyDown={onKeyDown}
87
+ placeholder="Search or create tag..."
88
+ size="sm"
89
+ bg="blackAlpha.300"
90
+ border="1px solid"
91
+ borderColor="whiteAlpha.200"
92
+ _focus={{ borderColor: 'var(--accent)', boxShadow: 'none' }}
93
+ rounded="md"
94
+ color="white"
95
+ isDisabled={isReadOnly}
96
+ autoComplete="off"
97
+ />
98
+
99
+ {query.trim() && results.length > 0 && (
100
+ <Box
101
+ position="absolute"
102
+ left="0"
103
+ top="calc(100% + 4px)"
104
+ zIndex={100}
105
+ bg="gray.800"
106
+ border="1px solid"
107
+ borderColor="whiteAlpha.300"
108
+ rounded="md"
109
+ shadow="xl"
110
+ w="full"
111
+ maxH="200px"
112
+ overflowY="auto"
113
+ >
114
+ <VStack spacing={0} align="stretch">
115
+ {results.map((item, i) => (
116
+ <Box
117
+ key={i}
118
+ px={3}
119
+ py={2}
120
+ bg={i === activeIndex ? 'whiteAlpha.200' : 'transparent'}
121
+ cursor="pointer"
122
+ _hover={{ bg: 'whiteAlpha.100' }}
123
+ onMouseEnter={() => setActiveIndex(i)}
124
+ onClick={() => confirm(i)}
125
+ >
126
+ {item.kind === 'new' ? (
127
+ <HStack spacing={1.5}>
128
+ <Text fontSize="10px" color="var(--accent)" fontWeight="bold">+ Create</Text>
129
+ <Text fontSize="xs" color="white" noOfLines={1}>{item.label}</Text>
130
+ </HStack>
131
+ ) : (
132
+ <Text fontSize="xs" color="gray.200" noOfLines={1}>{item.tag}</Text>
133
+ )}
134
+ </Box>
135
+ ))}
136
+ </VStack>
137
+ </Box>
138
+ )}
139
+ </Box>
140
+ </VStack>
141
+ )
142
+ }