@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.
- package/dist/App.d.ts +1 -0
- package/dist/api/client.d.ts +143 -0
- package/dist/api/transport-vscode.d.ts +8 -0
- package/dist/api/transport.d.ts +1 -0
- package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
- package/dist/components/CodePreviewPanel.d.ts +9 -0
- package/dist/components/ConfirmDialog.d.ts +12 -0
- package/dist/components/ConnectorPanel.d.ts +21 -0
- package/dist/components/ContextBoundaryElement.d.ts +11 -0
- package/dist/components/ContextNeighborElement.d.ts +29 -0
- package/dist/components/ContextStraightConnector.d.ts +4 -0
- package/dist/components/CrossBranchControls.d.ts +9 -0
- package/dist/components/DependenciesOnboarding.d.ts +5 -0
- package/dist/components/DrawingCanvas.d.ts +39 -0
- package/dist/components/ElementLibrary-vscode.d.ts +7 -0
- package/dist/components/ElementLibrary.d.ts +22 -0
- package/dist/components/ElementNode.d.ts +36 -0
- package/dist/components/ElementPanel.d.ts +25 -0
- package/dist/components/ExploreOnboarding.d.ts +5 -0
- package/dist/components/ExplorePageOnboarding.d.ts +5 -0
- package/dist/components/ExportModal.d.ts +16 -0
- package/dist/components/FloatingEdge.d.ts +9 -0
- package/dist/components/GitSourceLinker.d.ts +8 -0
- package/dist/components/HeaderContext.d.ts +16 -0
- package/dist/components/Icons.d.ts +95 -0
- package/dist/components/ImportModal.d.ts +10 -0
- package/dist/components/InlineElementAdder.d.ts +17 -0
- package/dist/components/LayoutSection.d.ts +7 -0
- package/dist/components/LocalSourceLinker.d.ts +8 -0
- package/dist/components/MiniZoomOnboarding.d.ts +5 -0
- package/dist/components/NavBreadcrumb.d.ts +6 -0
- package/dist/components/NodeBody.d.ts +12 -0
- package/dist/components/NodeContainer.d.ts +8 -0
- package/dist/components/NodeHoverCard.d.ts +10 -0
- package/dist/components/PanelHeader.d.ts +8 -0
- package/dist/components/PanelUI.d.ts +3 -0
- package/dist/components/ProxyConnectorEdge.d.ts +4 -0
- package/dist/components/ProxyConnectorPanel.d.ts +9 -0
- package/dist/components/SafeBackground.d.ts +13 -0
- package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
- package/dist/components/SetChildModal.d.ts +10 -0
- package/dist/components/SetParentModal.d.ts +10 -0
- package/dist/components/SlidingPanel.d.ts +16 -0
- package/dist/components/TagUpsert.d.ts +8 -0
- package/dist/components/TopMenuBar.d.ts +8 -0
- package/dist/components/ViewBezierConnector.d.ts +4 -0
- package/dist/components/ViewDrawMenu.d.ts +22 -0
- package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
- package/dist/components/ViewEditorOnboarding.d.ts +5 -0
- package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
- package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
- package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
- package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
- package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
- package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
- package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
- package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
- package/dist/components/ViewExplorer/index.d.ts +31 -0
- package/dist/components/ViewExplorer/types.d.ts +11 -0
- package/dist/components/ViewExplorer/utils.d.ts +6 -0
- package/dist/components/ViewExplorer-vscode.d.ts +6 -0
- package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
- package/dist/components/ViewFloatingMenu.d.ts +39 -0
- package/dist/components/ViewGridNode.d.ts +29 -0
- package/dist/components/ViewHeaderButton.d.ts +11 -0
- package/dist/components/ViewPanel.d.ts +18 -0
- package/dist/components/ViewsGridOnboarding.d.ts +5 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
- package/dist/components/ZUI/index.d.ts +2 -0
- package/dist/components/ZUI/layout.d.ts +18 -0
- package/dist/components/ZUI/proxy.d.ts +25 -0
- package/dist/components/ZUI/renderer.d.ts +30 -0
- package/dist/components/ZUI/types.d.ts +140 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
- package/dist/config/runtime-vscode.d.ts +22 -0
- package/dist/config/runtime.d.ts +5 -0
- package/dist/constants/colors.d.ts +27 -0
- package/dist/constants/diagramColors.d.ts +1 -0
- package/dist/context/ThemeContext.d.ts +27 -0
- package/dist/crossBranch/graph.d.ts +13 -0
- package/dist/crossBranch/resolve.d.ts +22 -0
- package/dist/crossBranch/settings.d.ts +6 -0
- package/dist/crossBranch/store.d.ts +11 -0
- package/dist/crossBranch/types.d.ts +96 -0
- package/dist/demo/DemoPage.d.ts +9 -0
- package/dist/demo/seed.d.ts +9 -0
- package/dist/demo/store.d.ts +137 -0
- package/dist/demo/viewEditor.d.ts +26 -0
- package/dist/favicon.svg +35 -0
- package/dist/hooks/useSafeFitView.d.ts +16 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +19966 -0
- package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
- package/dist/lib/vscodeBridge.d.ts +5 -0
- package/dist/logo-120.png +0 -0
- package/dist/logo-bw.png +0 -0
- package/dist/logo-bw.svg +15 -0
- package/dist/logo-text.svg +51 -0
- package/dist/logo.svg +35 -0
- package/dist/pages/AppearanceSettings.d.ts +3 -0
- package/dist/pages/Dependencies.d.ts +1 -0
- package/dist/pages/InfiniteZoom.d.ts +7 -0
- package/dist/pages/Settings.d.ts +7 -0
- package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
- package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
- package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
- package/dist/pages/ViewEditor/context.d.ts +13 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
- package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
- package/dist/pages/ViewEditor/index.d.ts +8 -0
- package/dist/pages/ViewEditor/utils.d.ts +14 -0
- package/dist/pages/Views.d.ts +6 -0
- package/dist/pages/ViewsGrid.d.ts +6 -0
- package/dist/pkg/importer/mermaid.d.ts +7 -0
- package/dist/pkg/importer/mermaid.test.d.ts +1 -0
- package/dist/platform/PlatformContext.d.ts +6 -0
- package/dist/platform/context.d.ts +3 -0
- package/dist/platform/local.d.ts +2 -0
- package/dist/platform/types.d.ts +17 -0
- package/dist/slots.d.ts +67 -0
- package/dist/theme.d.ts +2 -0
- package/dist/types/index.d.ts +193 -0
- package/dist/types/vscode-messages.d.ts +60 -0
- package/dist/utils/edgeDistribution.d.ts +34 -0
- package/dist/utils/githubApi.d.ts +4 -0
- package/dist/utils/githubCache.d.ts +17 -0
- package/dist/utils/ids.d.ts +2 -0
- package/dist/utils/technologyCatalog.d.ts +15 -0
- package/dist/utils/toast.d.ts +15 -0
- package/dist/utils/treesitter.d.ts +13 -0
- package/dist/utils/url.d.ts +12 -0
- package/package.json +159 -0
- package/src/App.tsx +141 -0
- package/src/api/client.ts +618 -0
- package/src/api/transport-vscode.ts +28 -0
- package/src/api/transport.ts +7 -0
- package/src/assets/logo-mark.svg +31 -0
- package/src/assets/logo-wordmark.svg +22 -0
- package/src/assets/logo.svg +35 -0
- package/src/components/CodePreviewPanel-vscode.tsx +85 -0
- package/src/components/CodePreviewPanel.tsx +384 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/ConnectorPanel.tsx +403 -0
- package/src/components/ContextBoundaryElement.tsx +35 -0
- package/src/components/ContextNeighborElement.tsx +282 -0
- package/src/components/ContextStraightConnector.tsx +144 -0
- package/src/components/CrossBranchControls.tsx +105 -0
- package/src/components/DependenciesOnboarding.tsx +427 -0
- package/src/components/DrawingCanvas.tsx +391 -0
- package/src/components/ElementLibrary-vscode.tsx +9 -0
- package/src/components/ElementLibrary.tsx +512 -0
- package/src/components/ElementNode.tsx +1033 -0
- package/src/components/ElementPanel.tsx +928 -0
- package/src/components/ExploreOnboarding.tsx +347 -0
- package/src/components/ExplorePageOnboarding.tsx +383 -0
- package/src/components/ExportModal.tsx +132 -0
- package/src/components/FloatingEdge.tsx +115 -0
- package/src/components/GitSourceLinker.tsx +1053 -0
- package/src/components/HeaderContext.tsx +30 -0
- package/src/components/Icons.tsx +245 -0
- package/src/components/ImportModal.tsx +219 -0
- package/src/components/InlineElementAdder.tsx +216 -0
- package/src/components/LayoutSection.tsx +624 -0
- package/src/components/LocalSourceLinker.tsx +330 -0
- package/src/components/MiniZoomOnboarding.tsx +78 -0
- package/src/components/NavBreadcrumb.tsx +24 -0
- package/src/components/NodeBody.tsx +89 -0
- package/src/components/NodeContainer.tsx +58 -0
- package/src/components/NodeHoverCard.tsx +135 -0
- package/src/components/PanelHeader.tsx +36 -0
- package/src/components/PanelUI.tsx +24 -0
- package/src/components/ProxyConnectorEdge.tsx +169 -0
- package/src/components/ProxyConnectorPanel.tsx +130 -0
- package/src/components/SafeBackground.tsx +19 -0
- package/src/components/ScrollIndicatorWrapper.tsx +117 -0
- package/src/components/SetChildModal.tsx +191 -0
- package/src/components/SetParentModal.tsx +187 -0
- package/src/components/SlidingPanel.tsx +114 -0
- package/src/components/TagUpsert.tsx +142 -0
- package/src/components/TopMenuBar.tsx +380 -0
- package/src/components/ViewBezierConnector.tsx +143 -0
- package/src/components/ViewDrawMenu.tsx +270 -0
- package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
- package/src/components/ViewEditorOnboarding.tsx +445 -0
- package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
- package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
- package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
- package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
- package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
- package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
- package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
- package/src/components/ViewExplorer/ViewTree.tsx +98 -0
- package/src/components/ViewExplorer/index.tsx +384 -0
- package/src/components/ViewExplorer/types.ts +13 -0
- package/src/components/ViewExplorer/utils.ts +56 -0
- package/src/components/ViewExplorer-vscode.tsx +8 -0
- package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
- package/src/components/ViewFloatingMenu.tsx +379 -0
- package/src/components/ViewGridNode.tsx +451 -0
- package/src/components/ViewHeaderButton.tsx +60 -0
- package/src/components/ViewPanel.tsx +162 -0
- package/src/components/ViewsGridOnboarding.tsx +400 -0
- package/src/components/ZUI/ZUICanvas.tsx +853 -0
- package/src/components/ZUI/index.ts +3 -0
- package/src/components/ZUI/layout.ts +323 -0
- package/src/components/ZUI/proxy.ts +278 -0
- package/src/components/ZUI/renderer.ts +1189 -0
- package/src/components/ZUI/types.ts +150 -0
- package/src/components/ZUI/useZUIInteraction.ts +720 -0
- package/src/config/runtime-vscode.ts +46 -0
- package/src/config/runtime.ts +30 -0
- package/src/constants/colors.ts +80 -0
- package/src/constants/diagramColors.ts +9 -0
- package/src/context/ThemeContext.tsx +158 -0
- package/src/crossBranch/graph.ts +207 -0
- package/src/crossBranch/resolve.ts +643 -0
- package/src/crossBranch/settings.ts +59 -0
- package/src/crossBranch/store.ts +71 -0
- package/src/crossBranch/types.ts +102 -0
- package/src/demo/DemoPage.tsx +184 -0
- package/src/demo/seed.ts +67 -0
- package/src/demo/store.ts +536 -0
- package/src/demo/viewEditor.ts +110 -0
- package/src/hooks/useSafeFitView.ts +60 -0
- package/src/index.css +309 -0
- package/src/index.ts +184 -0
- package/src/kafka-ss.png +0 -0
- package/src/lib/vscodeBridge-vscode.ts +27 -0
- package/src/lib/vscodeBridge.ts +7 -0
- package/src/main.tsx +46 -0
- package/src/pages/AppearanceSettings.tsx +135 -0
- package/src/pages/Dependencies.tsx +926 -0
- package/src/pages/InfiniteZoom.tsx +404 -0
- package/src/pages/Settings.tsx +91 -0
- package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
- package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
- package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
- package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
- package/src/pages/ViewEditor/context.tsx +21 -0
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
- package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
- package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
- package/src/pages/ViewEditor/index.tsx +1366 -0
- package/src/pages/ViewEditor/utils.ts +88 -0
- package/src/pages/Views.tsx +171 -0
- package/src/pages/ViewsGrid.tsx +1310 -0
- package/src/pkg/importer/mermaid.test.ts +141 -0
- package/src/pkg/importer/mermaid.ts +76 -0
- package/src/platform/PlatformContext.tsx +17 -0
- package/src/platform/context.ts +9 -0
- package/src/platform/local.tsx +15 -0
- package/src/platform/types.ts +19 -0
- package/src/slots.ts +92 -0
- package/src/styles/editor-panels.css +66 -0
- package/src/styles/theme.css +56 -0
- package/src/theme.ts +336 -0
- package/src/types/index.ts +234 -0
- package/src/types/offline-ambient.d.ts +14 -0
- package/src/types/vscode-messages.ts +32 -0
- package/src/utils/edgeDistribution.ts +103 -0
- package/src/utils/githubApi.ts +121 -0
- package/src/utils/githubCache.ts +108 -0
- package/src/utils/ids.ts +9 -0
- package/src/utils/technologyCatalog.ts +143 -0
- package/src/utils/toast.ts +100 -0
- package/src/utils/treesitter.ts +147 -0
- package/src/utils/url.ts +72 -0
- 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
|
+
}
|