@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,404 @@
1
+ // src/pages/InfiniteZoom.tsx Explore page holds the ZUI feature
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
+ import { useNavigate, useParams } from 'react-router-dom'
4
+ import {
5
+ Box,
6
+ Button,
7
+ Center,
8
+ HStack,
9
+ IconButton,
10
+ Popover,
11
+ PopoverBody,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ Portal,
15
+ Spinner,
16
+ Text,
17
+ Tooltip,
18
+ useDisclosure,
19
+ VStack,
20
+ } from '@chakra-ui/react'
21
+ import { useSetHeader } from '../components/HeaderContext'
22
+ import { api } from '../api/client'
23
+ import type { ExploreData, ViewLayer } from '../types'
24
+ import { FitViewIcon as FitViewSvg, TagsIcon, EyeIcon, EyeOffIcon, FocusIcon as FocusSvg } from '../components/Icons'
25
+ import ExploreOnboarding from '../components/ExploreOnboarding'
26
+ import ExplorePageOnboarding from '../components/ExplorePageOnboarding'
27
+ import MiniZoomOnboarding from '../components/MiniZoomOnboarding'
28
+ import { ZUICanvas, type ZUICanvasHandle } from '../components/ZUI'
29
+ import { useCrossBranchContextSettings } from '../crossBranch/settings'
30
+ import { primeWorkspaceGraphSnapshot } from '../crossBranch/store'
31
+
32
+ // ── Types ──────────────────────────────────────────────────────────
33
+ interface Props {
34
+ sharedToken?: string
35
+ shareSlot?: React.ReactNode
36
+ }
37
+
38
+ const MINI_ONBOARDING_KEY = 'shared_zoom_onboarding_dismissed'
39
+
40
+ // ── Inner component ────────────────────────────────────────────────
41
+ function InfiniteZoomInner({ sharedToken, shareSlot }: Props) {
42
+ const navigate = useNavigate()
43
+ const setHeader = useSetHeader()
44
+
45
+ const [data, setData] = useState<ExploreData | null>(null)
46
+ const [loading, setLoading] = useState(true)
47
+ const [canvasReady, setCanvasReady] = useState(false)
48
+ const [showMiniOnboarding, setShowMiniOnboarding] = useState(false)
49
+ const [tagColors] = useState<Record<string, import('../types').Tag>>({})
50
+ const [layers, setLayers] = useState<ViewLayer[]>([])
51
+ const [highlightedTags, setHighlightedTags] = useState<string[]>([])
52
+ const [highlightColor, setHighlightColor] = useState('')
53
+ const [hiddenTags, setHiddenTags] = useState<string[]>([])
54
+ const { isOpen: isTagsOpen, onClose: onTagsClose, onToggle: onTagsToggle } = useDisclosure()
55
+ const zuiRef = useRef<ZUICanvasHandle>(null)
56
+ const crossBranchSurface = sharedToken ? 'zui-shared' : 'zui'
57
+ const { settings: crossBranchSettings, setEnabled: setCrossBranchEnabled } = useCrossBranchContextSettings(crossBranchSurface)
58
+
59
+ // ── No data or No content ────────────────────────────────────────
60
+ const hasPlacements = useMemo(() => {
61
+ if (!data || !data.views) return false
62
+ return Object.values(data.views).some(d => (d && d.placements && d.placements.length > 0))
63
+ }, [data])
64
+
65
+ const allTags = useMemo(() => {
66
+ if (!data || !data.views) return []
67
+ const tagSet = new Set<string>()
68
+ Object.values(data.views).forEach(d => {
69
+ (d?.placements ?? []).forEach(p => { (p.tags ?? []).forEach(t => tagSet.add(t)) })
70
+ })
71
+ return Array.from(tagSet).sort()
72
+ }, [data])
73
+
74
+ const tagCounts = useMemo(() => {
75
+ if (!data || !data.views) return {} as Record<string, number>
76
+ const counts: Record<string, number> = {}
77
+ Object.values(data.views).forEach(d => {
78
+ (d?.placements ?? []).forEach(p => {
79
+ (p.tags ?? []).forEach(t => { counts[t] = (counts[t] ?? 0) + 1 })
80
+ })
81
+ })
82
+ return counts
83
+ }, [data])
84
+
85
+ const layerElementCounts = useMemo(() => {
86
+ if (!data || !data.views) return {} as Record<number, number>
87
+ const counts: Record<number, number> = {}
88
+ for (const layer of layers) {
89
+ let count = 0
90
+ Object.values(data.views).forEach(d => {
91
+ (d?.placements ?? []).forEach(p => {
92
+ if ((p.tags ?? []).some(t => layer.tags.includes(t))) count++
93
+ })
94
+ })
95
+ counts[layer.id] = count
96
+ }
97
+ return counts
98
+ }, [data, layers])
99
+
100
+ const toggleLayerVisibility = useCallback((layer: ViewLayer) => {
101
+ if (layer.tags.length === 0) return
102
+ setHiddenTags(prev => {
103
+ const allHidden = layer.tags.every(t => prev.includes(t))
104
+ return allHidden
105
+ ? prev.filter(t => !layer.tags.includes(t))
106
+ : Array.from(new Set([...prev, ...layer.tags]))
107
+ })
108
+ }, [])
109
+
110
+ const toggleTagVisibility = useCallback((tag: string) => {
111
+ setHiddenTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])
112
+ }, [])
113
+
114
+ // Set page header
115
+ useEffect(() => {
116
+ setHeader({ node: <Text fontWeight="medium" fontSize="sm" color="gray.300">Explore</Text> })
117
+ return () => setHeader(null)
118
+ }, [setHeader])
119
+ useEffect(() => {
120
+ if (sharedToken && canvasReady && !localStorage.getItem(MINI_ONBOARDING_KEY)) {
121
+ setShowMiniOnboarding(true)
122
+ }
123
+ }, [sharedToken, canvasReady])
124
+
125
+ const handleInteraction = useCallback(() => {
126
+ if (showMiniOnboarding) {
127
+ setShowMiniOnboarding(false)
128
+ localStorage.setItem(MINI_ONBOARDING_KEY, 'true')
129
+ }
130
+ }, [showMiniOnboarding])
131
+ useEffect(() => {
132
+ const loader = api.explore.load()
133
+ loader.then((d) => {
134
+ if (d.password_required) {
135
+ setLoading(false)
136
+ } else {
137
+ primeWorkspaceGraphSnapshot(d)
138
+ setData(d)
139
+ setLoading(false)
140
+ }
141
+ }).catch(() => setLoading(false))
142
+ }, [sharedToken])
143
+
144
+ // Fetch tag colors and layers once data is loaded (authenticated users only).
145
+ // Only fetch from root tree nodes child/nested diagrams would duplicate the same layers.
146
+ useEffect(() => {
147
+ if (!data) return
148
+ let cancelled = false
149
+ const rootIds = (data.tree ?? []).map(n => n.id)
150
+ const fetchTagData = async () => {
151
+ const diagramLayers = await Promise.all(
152
+ rootIds.map(id => api.workspace.views.layers.list(id)),
153
+ )
154
+ if (!cancelled) {
155
+ // Deduplicate by layer ID in case of any API overlap
156
+ const seen = new Set<number>()
157
+ const unique = diagramLayers.flat().filter(l => seen.has(l.id) ? false : (seen.add(l.id), true))
158
+ setLayers(unique)
159
+ }
160
+ }
161
+ void fetchTagData()
162
+ return () => { cancelled = true }
163
+ }, [data])
164
+
165
+ const handleCanvasReady = useCallback(() => {
166
+ setCanvasReady(true)
167
+ }, [])
168
+
169
+ if (!loading && (!data || (data.tree ?? []).length === 0 || !hasPlacements)) {
170
+ const noDiagrams = !data || (data.tree ?? []).length === 0
171
+ return (
172
+ <Center h="100%" flexDir="column" gap={4} px={6} textAlign="center">
173
+ <VStack spacing={2}>
174
+ <Text color="gray.300" fontWeight="bold" fontSize="lg">
175
+ {noDiagrams ? 'No diagrams to explore yet' : 'Your diagrams are empty'}
176
+ </Text>
177
+ <Text color="gray.500" fontSize="sm" maxW="400px">
178
+ {noDiagrams
179
+ ? 'Start by creating your first diagram in the workspace.'
180
+ : 'Add elements to your diagrams in the editor to see them rendered on this infinite canvas.'}
181
+ </Text>
182
+ </VStack>
183
+
184
+ {!sharedToken && (
185
+ <Button size="sm" colorScheme="blue" onClick={() => navigate('/views')} borderRadius="full" px={6}>
186
+ {noDiagrams ? 'Create First Diagram' : 'Go to Editor'}
187
+ </Button>
188
+ )}
189
+ {!noDiagrams && <ExplorePageOnboarding hasDiagrams={!noDiagrams} />}
190
+ </Center>
191
+ )
192
+ }
193
+
194
+ // ── Main view with loading overlay ────────────────────────────────
195
+ const showContent = !loading && !!data && canvasReady
196
+
197
+ return (
198
+ <Box position="relative" w="full" h="full" overflow="hidden">
199
+ {/* Loading overlay - stays until data and canvas are ready */}
200
+ {(!loading && data && !canvasReady) || loading ? (
201
+ <Center
202
+ position="absolute"
203
+ top={0} left={0} right={0} bottom={0}
204
+ zIndex={100}
205
+ bg="var(--bg-primary)"
206
+ >
207
+ <Spinner size="xl" color="var(--accent)" />
208
+ </Center>
209
+ ) : null}
210
+
211
+ {data && (
212
+ <>
213
+ <ZUICanvas
214
+ ref={zuiRef}
215
+ data={data}
216
+ onReady={handleCanvasReady}
217
+ onZoom={handleInteraction}
218
+ onPan={handleInteraction}
219
+ highlightedTags={highlightedTags}
220
+ highlightColor={highlightColor}
221
+ hiddenTags={hiddenTags}
222
+ crossBranchSettings={crossBranchSettings}
223
+ hoverLocked={isTagsOpen}
224
+ />
225
+
226
+ {/* Onboarding overlay */}
227
+ {data && <ExploreOnboarding hasLinkedNodes={!!(data.navigations?.length > 0)} />}
228
+ <MiniZoomOnboarding isVisible={showMiniOnboarding} />
229
+
230
+ {/* Bottom toolbar */}
231
+ <Box
232
+ position="absolute"
233
+ bottom={4}
234
+ left="50%"
235
+ transform="translateX(-50%)"
236
+ zIndex={10}
237
+ className="glass"
238
+ borderRadius="lg"
239
+ px={2}
240
+ py={1}
241
+ opacity={showContent ? 1 : 0}
242
+ transition="opacity 0.3s"
243
+ >
244
+ <HStack spacing={0}>
245
+ <Tooltip label="Fit View" placement="top" openDelay={200}>
246
+ <Button
247
+ variant="ghost" h="28px" px={2.5}
248
+ color="gray.300"
249
+ _hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
250
+ onClick={() => zuiRef.current?.fitView()}
251
+ >
252
+ <HStack spacing={1.5}>
253
+ <FitViewSvg />
254
+ <Text fontSize="11px" fontWeight="normal">Fit View</Text>
255
+ </HStack>
256
+ </Button>
257
+ </Tooltip>
258
+
259
+ {shareSlot}
260
+
261
+ <Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
262
+ <Tooltip label={!crossBranchSettings.enabled ? 'Show branches' : 'Focus on this view'} placement="top" openDelay={200}>
263
+ <Button
264
+ variant="ghost" h="28px" px={2.5}
265
+ color={!crossBranchSettings.enabled ? 'var(--accent)' : 'gray.300'}
266
+ bg={!crossBranchSettings.enabled ? 'rgba(var(--accent-rgb), 0.12)' : 'transparent'}
267
+ _hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
268
+ onClick={() => setCrossBranchEnabled(!crossBranchSettings.enabled)}
269
+ >
270
+ <HStack spacing={1.5}>
271
+ <FocusSvg />
272
+ <Text fontSize="11px" fontWeight="normal">Focus View</Text>
273
+ <Box w="6px" h="6px" rounded="full" bg={!crossBranchSettings.enabled ? 'var(--accent)' : 'gray.500'} />
274
+ </HStack>
275
+ </Button>
276
+ </Tooltip>
277
+
278
+ {(allTags.length > 0 || layers.length > 0) && (
279
+ <>
280
+ <Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
281
+ <Popover
282
+ isOpen={isTagsOpen}
283
+ onClose={() => { onTagsClose(); setHighlightedTags([]); setHighlightColor('') }}
284
+ placement="top"
285
+ isLazy
286
+ closeOnBlur
287
+ >
288
+ <PopoverTrigger>
289
+ <Button
290
+ variant="ghost" h="28px" px={2.5}
291
+ color={isTagsOpen ? 'var(--accent)' : 'gray.300'}
292
+ _hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
293
+ onClick={onTagsToggle}
294
+ >
295
+ <HStack spacing={1.5}>
296
+ <TagsIcon />
297
+ <Text fontSize="11px" fontWeight="normal">Tags</Text>
298
+ </HStack>
299
+ </Button>
300
+ </PopoverTrigger>
301
+ <Portal>
302
+ <PopoverContent
303
+ bg="glass.bg"
304
+ backdropFilter="blur(16px)"
305
+ borderColor="glass.border"
306
+ boxShadow="panel"
307
+ borderRadius="lg"
308
+ width="220px"
309
+ _focus={{ boxShadow: 'none' }}
310
+ onMouseLeave={() => { setHighlightedTags([]); setHighlightColor('') }}
311
+ >
312
+ <PopoverBody p={2} maxH="360px" overflowY="auto">
313
+ {layers.map(layer => {
314
+ const isHidden = layer.tags.length > 0 && layer.tags.every(t => hiddenTags.includes(t))
315
+ return (
316
+ <HStack
317
+ key={`layer-${layer.id}`}
318
+ px={2}
319
+ py={1}
320
+ spacing={2}
321
+ borderRadius="md"
322
+ _hover={{ bg: 'whiteAlpha.100' }}
323
+ onMouseEnter={() => { setHighlightedTags(layer.tags); setHighlightColor(layer.color || '') }}
324
+ opacity={isHidden ? 0.4 : 1}
325
+ transition="opacity 0.15s"
326
+ >
327
+ <Box w="10px" h="10px" rounded="full" bg={layer.color || 'gray.500'} flexShrink={0} />
328
+ <Text fontSize="xs" fontWeight="600" color="white" flex={1} isTruncated>
329
+ {layer.name}
330
+ </Text>
331
+ <Text fontSize="10px" color="gray.600" flexShrink={0}>
332
+ {layerElementCounts[layer.id] ?? 0}
333
+ </Text>
334
+ <IconButton
335
+ aria-label={isHidden ? 'Show layer' : 'Hide layer'}
336
+ icon={isHidden ? <EyeOffIcon size={12} /> : <EyeIcon size={12} />}
337
+ size="xs"
338
+ variant="ghost"
339
+ color={isHidden ? 'whiteAlpha.300' : 'whiteAlpha.600'}
340
+ _hover={{ color: 'white', bg: 'whiteAlpha.200' }}
341
+ onClick={(e) => { e.stopPropagation(); toggleLayerVisibility(layer) }}
342
+ flexShrink={0}
343
+ />
344
+ </HStack>
345
+ )
346
+ })}
347
+
348
+ {allTags.map(tag => {
349
+ const isHidden = hiddenTags.includes(tag)
350
+ return (
351
+ <HStack
352
+ key={`tag-${tag}`}
353
+ px={2}
354
+ py={1}
355
+ spacing={2}
356
+ borderRadius="md"
357
+ onMouseEnter={() => { setHighlightedTags([tag]); setHighlightColor(tagColors[tag]?.color || '') }}
358
+ opacity={isHidden ? 0.4 : 1}
359
+ transition="opacity 0.15s"
360
+ >
361
+ <Box w="8px" h="8px" rounded="full" bg={tagColors[tag]?.color || '#A0AEC0'} flexShrink={0} />
362
+ <Text fontSize="xs" fontWeight="600" color="gray.300" flex={1} isTruncated>
363
+ {tag}
364
+ </Text>
365
+ <Text fontSize="10px" color="gray.600" flexShrink={0}>
366
+ {tagCounts[tag] ?? 0}
367
+ </Text>
368
+ <IconButton
369
+ aria-label={isHidden ? 'Show tag' : 'Hide tag'}
370
+ icon={isHidden ? <EyeOffIcon size={12} /> : <EyeIcon size={12} />}
371
+ size="xs"
372
+ variant="ghost"
373
+ color={isHidden ? 'whiteAlpha.300' : 'whiteAlpha.600'}
374
+ _hover={{ color: 'white', bg: 'whiteAlpha.200' }}
375
+ onClick={(e) => { e.stopPropagation(); toggleTagVisibility(tag) }}
376
+ flexShrink={0}
377
+ />
378
+ </HStack>
379
+ )
380
+ })}
381
+ </PopoverBody>
382
+ </PopoverContent>
383
+ </Portal>
384
+ </Popover>
385
+ </>
386
+ )}
387
+ </HStack>
388
+ </Box>
389
+ </>
390
+ )}
391
+ </Box>
392
+ )
393
+ }
394
+
395
+ // ── Exports ───────────────────────────────────────────────────────
396
+
397
+ export default function InfiniteZoom(props: Props) {
398
+ return <InfiniteZoomInner {...props} />
399
+ }
400
+
401
+ export function SharedInfiniteZoom(props: Props) {
402
+ const { token } = useParams()
403
+ return <InfiniteZoomInner {...props} sharedToken={token} />
404
+ }
@@ -0,0 +1,91 @@
1
+ import { Box, Flex, Text, VStack } from '@chakra-ui/react'
2
+ import { useEffect } from 'react'
3
+ import { Outlet, useLocation, useNavigate } from 'react-router-dom'
4
+ import { useSetHeader } from '../components/HeaderContext'
5
+
6
+
7
+
8
+ const DEFAULT_NAV_ITEMS = [
9
+ { label: 'Appearance', path: '/settings/appearance' },
10
+ ]
11
+
12
+ export interface SettingsProps {
13
+ extraNavItems?: Array<{ label: string; path: string }>
14
+ }
15
+
16
+ export default function Settings({ extraNavItems = [] }: SettingsProps) {
17
+ const navItems = [...extraNavItems, ...DEFAULT_NAV_ITEMS]
18
+ const navigate = useNavigate()
19
+ const location = useLocation()
20
+ const setHeader = useSetHeader()
21
+
22
+ // Clear any page-specific header when on settings
23
+ useEffect(() => {
24
+ setHeader(null)
25
+ return () => setHeader(null)
26
+ }, [setHeader])
27
+
28
+
29
+
30
+ return (
31
+ <Flex direction="column" h="100vh">
32
+ <Flex flex={1} overflow="hidden" direction={{ base: 'column', md: 'row' }}>
33
+ {/* Sidebar (hidden on small screens) */}
34
+ <VStack
35
+ w={{ base: '0', md: '200px' }}
36
+ display={{ base: 'none', md: 'flex' }}
37
+ flexShrink={0}
38
+ bg="var(--bg-panel)"
39
+ borderRight="1px solid"
40
+ borderColor="var(--border-main)"
41
+ py={4}
42
+ px={2}
43
+ spacing={1}
44
+ align="stretch"
45
+ >
46
+ <Text
47
+ fontSize="xs"
48
+ fontWeight="bold"
49
+ color="gray.500"
50
+ textTransform="uppercase"
51
+ px={2}
52
+ mb={2}
53
+ >
54
+ Settings
55
+ </Text>
56
+ {navItems.map((item) => {
57
+ const active = location.pathname === item.path
58
+ return (
59
+ <Box
60
+ key={item.path}
61
+ as="button"
62
+ px={3}
63
+ py={1.5}
64
+ fontSize="sm"
65
+ textAlign="left"
66
+ borderRadius="md"
67
+ color={active ? 'gray.100' : 'gray.400'}
68
+ bg={active ? 'whiteAlpha.100' : 'transparent'}
69
+ _hover={{ bg: 'whiteAlpha.50', color: 'gray.200' }}
70
+ onClick={() => navigate(item.path)}
71
+ >
72
+ {item.label}
73
+ </Box>
74
+ )
75
+ })}
76
+ </VStack>
77
+
78
+ {/* Main content - allow scrolling on small screens */}
79
+ <Box
80
+ flex={1}
81
+ minH={0}
82
+ overflowY="auto"
83
+ p={6}
84
+ pb={{ base: 'calc(var(--bottomnav-container-h) + env(safe-area-inset-bottom, 0px) + 24px)', sm: 6 }}
85
+ >
86
+ <Outlet />
87
+ </Box>
88
+ </Flex>
89
+ </Flex>
90
+ )
91
+ }
@@ -0,0 +1,64 @@
1
+ # Edge Distribution System (ViewEditor)
2
+
3
+ This system prevents connectors from overlapping when sharing nodes or handles. It distributes overlapping edges spatially, aligns them to real React Flow handles, and sorts them to minimize crossings.
4
+
5
+ ## Source Files
6
+
7
+ - **Data Orchestration**: `src/pages/ViewEditor/hooks/useViewData.ts`
8
+ - **Curved Edges**: `src/components/ViewBezierConnector.tsx`
9
+ - **Straight Edges**: `src/components/ContextStraightConnector.tsx`
10
+ - **Ghost Edges**: `src/components/ProxyConnectorEdge.tsx`
11
+
12
+ ---
13
+
14
+ ## 1. Data Layer Logic (`useViewData.ts`)
15
+
16
+ The grouping logic is located in the `useEffect` that derives `rfEdges`.
17
+
18
+ ### Grouping Mechanism
19
+ Instead of grouping by edge corridors (source-target pairs), we group by **individual handle usage**.
20
+ 1. We collect all connectors attached to each specific `elementId-handle` (e.g., `123-right`).
21
+ 2. Both `source` and `target` usages are tracked in the same pool for that handle.
22
+ 3. Each usage record stores the `otherNodeCoord` (the Y or X position of the node at the other end).
23
+
24
+ ### Spatial Sorting
25
+ To minimize crossings near the handle, members sharing a handle are sorted based on their `otherNodeCoord`:
26
+ - **Left / Right handles**: Sorted by the `Y` coordinate of the connected node.
27
+ - **Top / Bottom handles**: Sorted by the `X` coordinate of the connected node.
28
+
29
+ ### Edge Data Payload
30
+ Each edge is enriched with distribution metadata in its `data` object:
31
+ - `sourceGroupIndex` / `sourceGroupCount`
32
+ - `targetGroupIndex` / `targetGroupCount`
33
+ - `sourceHandleSide` / `targetHandleSide`
34
+ - `sourceHandleSlot` / `targetHandleSlot`
35
+
36
+ ### Visual Handle Mapping
37
+ - The backend still stores logical handles as `top`, `right`, `bottom`, `left`.
38
+ - React Flow renders each logical side as a capped bead of **5 physical handles**: `side-0` through `side-4`.
39
+ - Group members are mapped onto those 5 slots using their sorted rank. Once a side has more than 5 edges, extra edges reuse the nearest slot instead of creating more unique handle positions.
40
+
41
+ ---
42
+
43
+ ## 2. Rendering Logic
44
+
45
+ ### ViewBezierConnector (Curved Edges)
46
+ - Curved connectors now use the actual React Flow handle positions directly.
47
+ - `useViewData.ts` assigns each edge to a physical `side-slot` handle id so the curve endpoint already lands on the correct circle.
48
+ - Slot spacing is currently `12px`.
49
+
50
+ ### Straight Connectors (Diagonal/Corridors)
51
+ Uses a perpendicular vector shift to ensure distribution works at any angle.
52
+ 1. Calculate direction vector `(dx, dy)`.
53
+ 2. Compute normal vector `nx = -dy / len`, `ny = dx / len`.
54
+ 3. Shift the endpoint by `(offset * nx, offset * ny)`.
55
+
56
+ ---
57
+
58
+ ## 3. Maintenance Notes
59
+
60
+ - **Ghost Connectors**: `useViewContextNeighbours.ts` currently deduplicates ghost edges by node pairs. However, the `ProxyConnectorEdge` component supports the distribution props if deduplication is ever removed or if they overlap with standard edges.
61
+ - **Slot Count**: Adjust `HANDLE_SLOT_COUNT` in `src/utils/edgeDistribution.ts` to change the number of unique handle positions per side.
62
+ - **Slot Gap**: Adjust `HANDLE_SLOT_GAP` in `src/utils/edgeDistribution.ts` to change the spacing between circles.
63
+ - **Labeling**: Labels are calculated based on the shifted coordinates/path to ensure they stay centered on the distributed line.
64
+ - **Selection**: When an edge is selected, the physical handles used by that edge grow in size so reconnection targets stay obvious.
@@ -0,0 +1,112 @@
1
+ import React from 'react'
2
+ import { Box, Button, Divider, HStack, Text, VStack } from '@chakra-ui/react'
3
+ import {
4
+ AddElementIcon as AddElementSvg,
5
+ TrashIcon as TrashSvg,
6
+ EditIcon as PencilSvg,
7
+ MoveSourceIcon as MoveSourceSvg,
8
+ MoveTargetIcon as MoveTargetSvg,
9
+ GridIcon as GridSvg,
10
+ } from '../../../components/Icons'
11
+ import { useViewEditorContext } from '../context'
12
+
13
+ const KbdHint = ({ children }: { children: string }) => (
14
+ <Box as="span" display="inline-flex" alignItems="center" justifyContent="center"
15
+ px={1.5} py={0.5} bg="whiteAlpha.300" rounded="sm" fontSize="8px"
16
+ fontWeight="bold" color="whiteAlpha.900" flexShrink={0}>
17
+ {children}
18
+ </Box>
19
+ )
20
+
21
+ interface ConnectorContextMenuProps {
22
+ menu: { edgeId: number; x: number; y: number } | null
23
+ onEdit: (edgeId: number) => void
24
+ onMoveSource: (edgeId: number) => void
25
+ onMoveTarget: (edgeId: number) => void
26
+ onDelete: (edgeId: number) => void
27
+ }
28
+
29
+ export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = ({
30
+ menu,
31
+ onEdit,
32
+ onMoveSource,
33
+ onMoveTarget,
34
+ onDelete,
35
+ }) => {
36
+ const { canEdit } = useViewEditorContext()
37
+ if (!menu) return null
38
+
39
+ return (
40
+ <Box position="absolute" left={`${menu.x}px`} top={`${menu.y}px`}
41
+ transform="translate(-50%, calc(-100% - 8px))" zIndex={1000} bg="var(--bg-panel)"
42
+ border="1px solid" borderColor="whiteAlpha.100" rounded="xl" boxShadow="0 8px 32px rgba(0,0,0,0.5)"
43
+ backdropFilter="blur(20px)" p={1.5} minW="192px"
44
+ onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
45
+ <VStack spacing={0} align="stretch">
46
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start" color="gray.200" _hover={{ bg: 'whiteAlpha.100' }}
47
+ onClick={() => onEdit(menu.edgeId)}>
48
+ <HStack spacing={2} w="full"><PencilSvg /><Text fontSize="xs" fontWeight="normal" flex={1}>Edit Connector</Text></HStack>
49
+ </Button>
50
+ {canEdit && (
51
+ <>
52
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start" color="gray.200" _hover={{ bg: 'whiteAlpha.100' }}
53
+ onClick={() => onMoveSource(menu.edgeId)}>
54
+ <HStack spacing={2} w="full"><MoveSourceSvg /><Text fontSize="xs" fontWeight="normal" flex={1}>Move Source</Text></HStack>
55
+ </Button>
56
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start" color="gray.200" _hover={{ bg: 'whiteAlpha.100' }}
57
+ onClick={() => onMoveTarget(menu.edgeId)}>
58
+ <HStack spacing={2} w="full"><MoveTargetSvg /><Text fontSize="xs" fontWeight="normal" flex={1}>Move Target</Text></HStack>
59
+ </Button>
60
+ <Divider borderColor="whiteAlpha.100" my={1} />
61
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start" color="red.400" _hover={{ bg: 'rgba(254,178,178,0.08)', color: 'red.300' }}
62
+ onClick={() => onDelete(menu.edgeId)}>
63
+ <HStack spacing={2} w="full"><TrashSvg /><Text fontSize="xs" fontWeight="normal" flex={1}>Delete</Text></HStack>
64
+ </Button>
65
+ </>
66
+ )}
67
+ {!canEdit && <Divider borderColor="whiteAlpha.100" my={0} />}
68
+ </VStack>
69
+ </Box>
70
+ )
71
+ }
72
+
73
+ interface CanvasContextMenuProps {
74
+ menu: { x: number; y: number; flowX: number; flowY: number } | null
75
+ onAddElement: (x: number, y: number) => void
76
+ }
77
+
78
+ export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = ({ menu, onAddElement }) => {
79
+ const { canEdit, snapToGrid, setSnapToGrid } = useViewEditorContext()
80
+ if (!menu) return null
81
+
82
+ return (
83
+ <Box position="absolute" left={`${menu.x}px`} top={`${menu.y}px`}
84
+ transform="translate(-50%, calc(-100% - 6px))" zIndex={1000} bg="var(--bg-panel)"
85
+ border="1px solid" borderColor="whiteAlpha.100" rounded="xl" boxShadow="0 8px 32px rgba(0,0,0,0.5)"
86
+ backdropFilter="blur(20px)" p={1.5} minW="180px"
87
+ onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
88
+ <VStack spacing={0} align="stretch">
89
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start"
90
+ color={canEdit ? 'var(--accent)' : 'gray.500'} _hover={{ bg: 'whiteAlpha.100', color: 'var(--accent)' }}
91
+ _disabled={{ opacity: 0.4, cursor: 'not-allowed' }} isDisabled={!canEdit}
92
+ onClick={() => onAddElement(menu.x, menu.y)}>
93
+ <HStack spacing={2} w="full">
94
+ <AddElementSvg />
95
+ <Text fontSize="xs" fontWeight="normal" flex={1}>Add Element</Text>
96
+ <KbdHint>C</KbdHint>
97
+ </HStack>
98
+ </Button>
99
+ <Divider borderColor="whiteAlpha.100" my={1} />
100
+ <Button size="sm" variant="ghost" h="30px" px={2.5} justifyContent="flex-start"
101
+ color="clay.text" _hover={{ bg: 'whiteAlpha.100' }}
102
+ onClick={() => setSnapToGrid(!snapToGrid)}>
103
+ <HStack spacing={2} w="full">
104
+ <GridSvg />
105
+ <Text fontSize="xs" fontWeight="normal" flex={1}>Snap to Grid</Text>
106
+ {snapToGrid && <Box w="6px" h="6px" rounded="full" bg="var(--accent)" />}
107
+ </HStack>
108
+ </Button>
109
+ </VStack>
110
+ </Box>
111
+ )
112
+ }