@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,853 @@
|
|
|
1
|
+
// src/components/ZUI/ZUICanvas.tsx
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, useCallback } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Text,
|
|
7
|
+
Icon,
|
|
8
|
+
Breadcrumb,
|
|
9
|
+
BreadcrumbItem,
|
|
10
|
+
BreadcrumbLink,
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverTrigger,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverBody,
|
|
15
|
+
PopoverHeader,
|
|
16
|
+
PopoverArrow,
|
|
17
|
+
Button,
|
|
18
|
+
VStack,
|
|
19
|
+
HStack,
|
|
20
|
+
Badge,
|
|
21
|
+
Divider,
|
|
22
|
+
Portal,
|
|
23
|
+
Image as ChakraImage,
|
|
24
|
+
useBreakpointValue,
|
|
25
|
+
} from '@chakra-ui/react'
|
|
26
|
+
import { Link as RouterLink } from 'react-router-dom'
|
|
27
|
+
import { ExternalLinkIcon } from '@chakra-ui/icons'
|
|
28
|
+
import type { ExploreData } from '../../types'
|
|
29
|
+
import { computeLayout } from './layout'
|
|
30
|
+
import { renderFrame, getExpandThresholds, setOnImageLoadCallback, setHighlightedTags as setRendererHighlightedTags, setHiddenTags as setRendererHiddenTags, setHighlightColor as setRendererHighlightColor } from './renderer'
|
|
31
|
+
import { useZUIInteraction } from './useZUIInteraction'
|
|
32
|
+
import type { DiagramGroupLayout, ZUIViewState } from './types'
|
|
33
|
+
import { buildWorkspaceGraphSnapshot } from '../../crossBranch/graph'
|
|
34
|
+
import type { CrossBranchContextSettings } from '../../crossBranch/types'
|
|
35
|
+
import type { ZUIResolvedConnector } from '../../crossBranch/resolve'
|
|
36
|
+
import { buildVisibleProxyConnectors, collectVisibleNodeAnchors, drawVisibleProxyConnectors, findHoveredProxyConnector } from './proxy'
|
|
37
|
+
|
|
38
|
+
export interface ZUICanvasHandle {
|
|
39
|
+
fitView(): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
data: ExploreData
|
|
44
|
+
onReady?: () => void
|
|
45
|
+
onZoom?: () => void
|
|
46
|
+
onPan?: () => void
|
|
47
|
+
highlightedTags?: string[]
|
|
48
|
+
highlightColor?: string
|
|
49
|
+
hiddenTags?: string[]
|
|
50
|
+
crossBranchSettings: CrossBranchContextSettings
|
|
51
|
+
hoverLocked?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PathItem {
|
|
55
|
+
id: string
|
|
56
|
+
label: string
|
|
57
|
+
type: 'group' | 'node'
|
|
58
|
+
isCircular?: boolean
|
|
59
|
+
// Absolute world coordinates for zooming
|
|
60
|
+
absX: number
|
|
61
|
+
absY: number
|
|
62
|
+
absW: number
|
|
63
|
+
absH: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPathAt(
|
|
67
|
+
view: ZUIViewState,
|
|
68
|
+
groups: DiagramGroupLayout[],
|
|
69
|
+
canvasW: number,
|
|
70
|
+
canvasH: number,
|
|
71
|
+
): PathItem[] {
|
|
72
|
+
if (canvasW === 0 || canvasH === 0) return []
|
|
73
|
+
|
|
74
|
+
// World center of the screen
|
|
75
|
+
const worldCenterX = (canvasW / 2 - view.x) / view.zoom
|
|
76
|
+
const worldCenterY = (canvasH / 2 - view.y) / view.zoom
|
|
77
|
+
const thresholds = getExpandThresholds(canvasW)
|
|
78
|
+
|
|
79
|
+
for (const group of groups) {
|
|
80
|
+
if (
|
|
81
|
+
worldCenterX >= group.worldX &&
|
|
82
|
+
worldCenterX <= group.worldX + group.worldW &&
|
|
83
|
+
worldCenterY >= group.worldY &&
|
|
84
|
+
worldCenterY <= group.worldY + group.worldH
|
|
85
|
+
) {
|
|
86
|
+
const path: PathItem[] = [
|
|
87
|
+
{
|
|
88
|
+
id: `g-${group.diagramId}`,
|
|
89
|
+
label: group.label,
|
|
90
|
+
type: 'group',
|
|
91
|
+
absX: group.worldX,
|
|
92
|
+
absY: group.worldY,
|
|
93
|
+
absW: group.worldW,
|
|
94
|
+
absH: group.worldH,
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
let currentNodes = group.nodes
|
|
99
|
+
let currentX = worldCenterX
|
|
100
|
+
let currentY = worldCenterY
|
|
101
|
+
|
|
102
|
+
// Track cumulative transform for children
|
|
103
|
+
// We start at 0 because root-level nodes already have absolute worldX/Y
|
|
104
|
+
let parentAbsX = 0
|
|
105
|
+
let parentAbsY = 0
|
|
106
|
+
let parentAbsScale = 1
|
|
107
|
+
let parentChildOffsetX = 0
|
|
108
|
+
let parentChildOffsetY = 0
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
let found = false
|
|
112
|
+
for (const node of currentNodes) {
|
|
113
|
+
if (
|
|
114
|
+
currentX >= node.worldX &&
|
|
115
|
+
currentX <= node.worldX + node.worldW &&
|
|
116
|
+
currentY >= node.worldY &&
|
|
117
|
+
currentY <= node.worldY + node.worldH
|
|
118
|
+
) {
|
|
119
|
+
// Absolute position of this node
|
|
120
|
+
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
121
|
+
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
122
|
+
const absW = node.worldW * parentAbsScale
|
|
123
|
+
const absH = node.worldH * parentAbsScale
|
|
124
|
+
|
|
125
|
+
// Screen width of this node to check expansion
|
|
126
|
+
const screenW = absW * view.zoom
|
|
127
|
+
|
|
128
|
+
path.push({
|
|
129
|
+
id: node.id,
|
|
130
|
+
label: node.label,
|
|
131
|
+
type: 'node',
|
|
132
|
+
isCircular: node.isCircular,
|
|
133
|
+
absX,
|
|
134
|
+
absY,
|
|
135
|
+
absW,
|
|
136
|
+
absH,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Only descend if the node is "expanded" enough on screen
|
|
140
|
+
// and has children. This makes the breadcrumb track the visual focus.
|
|
141
|
+
const isExpanded = screenW > thresholds.start * 1.1
|
|
142
|
+
|
|
143
|
+
if (isExpanded && node.children && node.children.length > 0) {
|
|
144
|
+
// Update parent context for next level
|
|
145
|
+
parentAbsX = absX
|
|
146
|
+
parentAbsY = absY
|
|
147
|
+
parentAbsScale = parentAbsScale * node.childScale
|
|
148
|
+
parentChildOffsetX = node.childOffsetX
|
|
149
|
+
parentChildOffsetY = node.childOffsetY
|
|
150
|
+
|
|
151
|
+
// Transform current center into child-local space
|
|
152
|
+
currentX = (currentX - node.worldX) / node.childScale + node.childOffsetX
|
|
153
|
+
currentY = (currentY - node.worldY) / node.childScale + node.childOffsetY
|
|
154
|
+
currentNodes = node.children
|
|
155
|
+
found = true
|
|
156
|
+
break
|
|
157
|
+
} else {
|
|
158
|
+
// Stop breadcrumb at the deepest visible/centered node
|
|
159
|
+
found = false
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!found) break
|
|
165
|
+
}
|
|
166
|
+
return path
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
|
|
173
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
174
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
175
|
+
const [initialized, setInitialized] = useState(false)
|
|
176
|
+
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
|
|
177
|
+
const isMobileLayout = useBreakpointValue({ base: true, md: false }) ?? false
|
|
178
|
+
|
|
179
|
+
// ── Layout ──────────────────────────────────────────────────────
|
|
180
|
+
const layout = useMemo(() => computeLayout(data), [data])
|
|
181
|
+
const workspaceSnapshot = useMemo(() => buildWorkspaceGraphSnapshot(data), [data])
|
|
182
|
+
// Holds the most-recently resolved connector topology so hover detection can
|
|
183
|
+
// use it without re-running the expensive O(connectors) resolution on every mousemove.
|
|
184
|
+
const proxyConnectorsRef = useRef<ZUIResolvedConnector[]>([])
|
|
185
|
+
|
|
186
|
+
const resolveHoveredProxyItem = useCallback((worldX: number, worldY: number, view: ZUIViewState, canvasW: number) => {
|
|
187
|
+
const freshAnchors = collectVisibleNodeAnchors(layout.groups, view, canvasW, hiddenTags)
|
|
188
|
+
return findHoveredProxyConnector(worldX, worldY, proxyConnectorsRef.current, freshAnchors.byNodeId, view)
|
|
189
|
+
}, [hiddenTags, layout.groups])
|
|
190
|
+
|
|
191
|
+
// ── Interaction ─────────────────────────────────────────────────
|
|
192
|
+
const { viewState, viewStateRef, setViewState, fitView, maxZoom, hoveredItem, setHoveredItem, setHoverLocked } = useZUIInteraction(
|
|
193
|
+
canvasRef,
|
|
194
|
+
undefined,
|
|
195
|
+
layout.groups,
|
|
196
|
+
layout.bbox,
|
|
197
|
+
onZoom,
|
|
198
|
+
onPan,
|
|
199
|
+
isMobileLayout,
|
|
200
|
+
resolveHoveredProxyItem,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Anchor positions recompute every render (fast tree traversal, view-dependent).
|
|
204
|
+
const anchors = useMemo(() =>
|
|
205
|
+
collectVisibleNodeAnchors(layout.groups, viewState, containerSize.w || 1, hiddenTags),
|
|
206
|
+
[layout.groups, viewState, containerSize.w, hiddenTags],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// A stable string key encoding which element→nodeId pairs are currently visible.
|
|
210
|
+
// This only changes when nodes cross zoom-expansion thresholds not on every pan pixel.
|
|
211
|
+
const visibleElementSig = useMemo(() =>
|
|
212
|
+
Array.from(anchors.visibleAnchors.entries())
|
|
213
|
+
.sort(([a], [b]) => a - b)
|
|
214
|
+
.map(([id, anchor]) => `${id}:${anchor.nodeId}`)
|
|
215
|
+
.join(','),
|
|
216
|
+
[anchors.visibleAnchors],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
// Connector topology: expensive O(connectors) resolution only when visibility set changes.
|
|
220
|
+
const proxyConnectors = useMemo(() => {
|
|
221
|
+
const resolved = buildVisibleProxyConnectors(workspaceSnapshot, anchors.visibleAnchors, crossBranchSettings)
|
|
222
|
+
proxyConnectorsRef.current = resolved
|
|
223
|
+
return resolved
|
|
224
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
225
|
+
}, [workspaceSnapshot, visibleElementSig, crossBranchSettings])
|
|
226
|
+
|
|
227
|
+
const visibleProxyState = useMemo(() => ({
|
|
228
|
+
...anchors,
|
|
229
|
+
proxyConnectors,
|
|
230
|
+
}), [anchors, proxyConnectors])
|
|
231
|
+
|
|
232
|
+
const visibleProxyStateRef = useRef(visibleProxyState)
|
|
233
|
+
visibleProxyStateRef.current = visibleProxyState
|
|
234
|
+
|
|
235
|
+
const labelBgRef = useRef('#171923')
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const update = () => {
|
|
238
|
+
labelBgRef.current = getComputedStyle(document.documentElement).getPropertyValue('--chakra-colors-gray-900').trim() || '#171923'
|
|
239
|
+
needsRedrawRef.current = true
|
|
240
|
+
}
|
|
241
|
+
update()
|
|
242
|
+
const mo = new MutationObserver(update)
|
|
243
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] })
|
|
244
|
+
return () => mo.disconnect()
|
|
245
|
+
}, [])
|
|
246
|
+
|
|
247
|
+
// ── Hierarchy Breadcrumb ────────────────────────────────────────
|
|
248
|
+
const hoveredScreenRect = useMemo(() => {
|
|
249
|
+
if (!hoveredItem) return null
|
|
250
|
+
let absX, absY, absW, absH
|
|
251
|
+
if (hoveredItem.type === 'node') {
|
|
252
|
+
({ absX, absY, absW, absH } = hoveredItem)
|
|
253
|
+
} else if (hoveredItem.type === 'edge') {
|
|
254
|
+
({ absX, absY } = hoveredItem)
|
|
255
|
+
absW = 0
|
|
256
|
+
absH = 0
|
|
257
|
+
} else {
|
|
258
|
+
const g = hoveredItem.data
|
|
259
|
+
// Target the label area (centered above diagram)
|
|
260
|
+
absW = 200 / viewState.zoom
|
|
261
|
+
absH = 50 / viewState.zoom
|
|
262
|
+
absX = g.worldX + g.diagramX + g.diagramW / 2 - absW / 2
|
|
263
|
+
absY = g.worldY + g.diagramY - absH
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const sx = absX * viewState.zoom + viewState.x
|
|
267
|
+
const sy = absY * viewState.zoom + viewState.y
|
|
268
|
+
const sw = absW * viewState.zoom
|
|
269
|
+
const sh = absH * viewState.zoom
|
|
270
|
+
|
|
271
|
+
return { sx, sy, sw, sh }
|
|
272
|
+
}, [hoveredItem, viewState])
|
|
273
|
+
|
|
274
|
+
const isHoveredItemFullyVisible = useMemo(() => {
|
|
275
|
+
if (!hoveredScreenRect || containerSize.w === 0) return false
|
|
276
|
+
// A target is "fully visible" if its screen-space rect is entirely within viewport.
|
|
277
|
+
return (
|
|
278
|
+
hoveredScreenRect.sx >= 2 &&
|
|
279
|
+
hoveredScreenRect.sy >= 2 &&
|
|
280
|
+
hoveredScreenRect.sx + hoveredScreenRect.sw <= containerSize.w - 2 &&
|
|
281
|
+
hoveredScreenRect.sy + hoveredScreenRect.sh <= containerSize.h - 2
|
|
282
|
+
)
|
|
283
|
+
}, [hoveredScreenRect, containerSize])
|
|
284
|
+
|
|
285
|
+
// Debounce breadcrumb computation so getPathAt doesn't run on every scroll tick
|
|
286
|
+
const [breadcrumbView, setBreadcrumbView] = useState(viewState)
|
|
287
|
+
const breadcrumbTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (breadcrumbTimerRef.current) clearTimeout(breadcrumbTimerRef.current)
|
|
290
|
+
breadcrumbTimerRef.current = setTimeout(() => setBreadcrumbView(viewState), 80)
|
|
291
|
+
return () => { if (breadcrumbTimerRef.current) clearTimeout(breadcrumbTimerRef.current) }
|
|
292
|
+
}, [viewState])
|
|
293
|
+
|
|
294
|
+
const currentPath = useMemo(() => {
|
|
295
|
+
return getPathAt(breadcrumbView, layout.groups, containerSize.w, containerSize.h)
|
|
296
|
+
}, [breadcrumbView, layout.groups, containerSize])
|
|
297
|
+
|
|
298
|
+
const zoomToPathItem = useCallback((item: PathItem) => {
|
|
299
|
+
if (containerSize.w === 0 || containerSize.h === 0) return
|
|
300
|
+
setHoveredItem(null, true) // Clear popover immediately on breadcrumb jump
|
|
301
|
+
|
|
302
|
+
// Use a comfortable padding for the focused item
|
|
303
|
+
const padding = 0.15
|
|
304
|
+
const bboxW = item.absW
|
|
305
|
+
const bboxH = item.absH
|
|
306
|
+
|
|
307
|
+
const zoom = Math.min(
|
|
308
|
+
(containerSize.w * (1 - padding * 2)) / bboxW,
|
|
309
|
+
(containerSize.h * (1 - padding * 2)) / bboxH,
|
|
310
|
+
maxZoom
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
// Center the item exactly
|
|
314
|
+
const x = (containerSize.w - bboxW * zoom) / 2 - item.absX * zoom
|
|
315
|
+
const y = (containerSize.h - bboxH * zoom) / 2 - item.absY * zoom
|
|
316
|
+
|
|
317
|
+
setViewState({ x, y, zoom })
|
|
318
|
+
}, [containerSize, maxZoom, setViewState, setHoveredItem])
|
|
319
|
+
|
|
320
|
+
// ── Fit view on mount and when layout changes ────────────────────
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const el = containerRef.current
|
|
323
|
+
if (!el) return
|
|
324
|
+
const { offsetWidth: w, offsetHeight: h } = el
|
|
325
|
+
|
|
326
|
+
// Only set as initialized if we have valid dimensions
|
|
327
|
+
if (w > 0 && h > 0) {
|
|
328
|
+
setContainerSize({ w, h })
|
|
329
|
+
fitView(w, h, layout.bbox)
|
|
330
|
+
if (!initialized) {
|
|
331
|
+
setInitialized(true)
|
|
332
|
+
onReady?.()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
336
|
+
}, [layout, initialized, onReady])
|
|
337
|
+
|
|
338
|
+
// ── Expose fitView to parent ─────────────────────────────────────
|
|
339
|
+
useImperativeHandle(
|
|
340
|
+
ref,
|
|
341
|
+
() => ({
|
|
342
|
+
fitView() {
|
|
343
|
+
const el = containerRef.current
|
|
344
|
+
if (!el) return
|
|
345
|
+
setHoveredItem(null, true) // Clear popover immediately on fitView
|
|
346
|
+
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
[fitView, layout.bbox, setHoveredItem],
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
// ── RAF render loop ──────────────────────────────────────────────
|
|
353
|
+
// viewStateRef comes from useZUIInteraction updated synchronously on input events,
|
|
354
|
+
// so the RAF loop sees new state immediately without waiting for React to re-render.
|
|
355
|
+
const needsRedrawRef = useRef(true) // Force first frame
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
setOnImageLoadCallback(() => {
|
|
359
|
+
needsRedrawRef.current = true
|
|
360
|
+
})
|
|
361
|
+
return () => setOnImageLoadCallback(null)
|
|
362
|
+
}, [])
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
needsRedrawRef.current = true
|
|
366
|
+
}, [viewState])
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
needsRedrawRef.current = true
|
|
370
|
+
}, [crossBranchSettings])
|
|
371
|
+
|
|
372
|
+
// ── HiDPI canvas resize ──────────────────────────────────────────
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
const canvas = canvasRef.current
|
|
375
|
+
const container = containerRef.current
|
|
376
|
+
if (!canvas || !container) return
|
|
377
|
+
|
|
378
|
+
function resize() {
|
|
379
|
+
if (!canvas || !container) return
|
|
380
|
+
const dpr = window.devicePixelRatio || 1
|
|
381
|
+
const w = container.offsetWidth
|
|
382
|
+
const h = container.offsetHeight
|
|
383
|
+
if (w === 0 || h === 0) return
|
|
384
|
+
|
|
385
|
+
setContainerSize({ w, h })
|
|
386
|
+
canvas.width = w * dpr
|
|
387
|
+
canvas.height = h * dpr
|
|
388
|
+
canvas.style.width = `${w}px`
|
|
389
|
+
canvas.style.height = `${h}px`
|
|
390
|
+
const ctx = canvas.getContext('2d')
|
|
391
|
+
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
392
|
+
|
|
393
|
+
needsRedrawRef.current = true // Canvas was cleared!
|
|
394
|
+
|
|
395
|
+
// Trigger initialization if it hasn't happened yet
|
|
396
|
+
if (!initialized && w > 0 && h > 0) {
|
|
397
|
+
fitView(w, h, layout.bbox)
|
|
398
|
+
setInitialized(true)
|
|
399
|
+
onReady?.()
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const ro = new ResizeObserver(resize)
|
|
404
|
+
ro.observe(container)
|
|
405
|
+
resize()
|
|
406
|
+
return () => ro.disconnect()
|
|
407
|
+
}, [initialized, layout, fitView, onReady])
|
|
408
|
+
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (!initialized) return // Don't start loop until initialized
|
|
411
|
+
|
|
412
|
+
const canvas = canvasRef.current
|
|
413
|
+
const container = containerRef.current
|
|
414
|
+
if (!canvas || !container) return
|
|
415
|
+
|
|
416
|
+
let rafId: number
|
|
417
|
+
let lastView = { x: NaN, y: NaN, zoom: NaN } // Force first draw
|
|
418
|
+
|
|
419
|
+
function frame() {
|
|
420
|
+
const ctx = canvas!.getContext('2d')
|
|
421
|
+
if (!ctx) { rafId = requestAnimationFrame(frame); return }
|
|
422
|
+
|
|
423
|
+
const dpr = window.devicePixelRatio || 1
|
|
424
|
+
const w = container!.offsetWidth
|
|
425
|
+
const h = container!.offsetHeight
|
|
426
|
+
if (w === 0 || h === 0) { rafId = requestAnimationFrame(frame); return }
|
|
427
|
+
|
|
428
|
+
const currentView = viewStateRef.current
|
|
429
|
+
|
|
430
|
+
// Only redraw when view changed (saves GPU on idle)
|
|
431
|
+
const changed =
|
|
432
|
+
lastView.x !== currentView.x ||
|
|
433
|
+
lastView.y !== currentView.y ||
|
|
434
|
+
lastView.zoom !== currentView.zoom ||
|
|
435
|
+
needsRedrawRef.current
|
|
436
|
+
|
|
437
|
+
if (changed) {
|
|
438
|
+
ctx.save()
|
|
439
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
440
|
+
const occupiedLabelRects = renderFrame(ctx, layout.groups, currentView, w, h)
|
|
441
|
+
ctx.save()
|
|
442
|
+
ctx.translate(currentView.x, currentView.y)
|
|
443
|
+
ctx.scale(currentView.zoom, currentView.zoom)
|
|
444
|
+
drawVisibleProxyConnectors(
|
|
445
|
+
ctx,
|
|
446
|
+
visibleProxyStateRef.current.proxyConnectors,
|
|
447
|
+
visibleProxyStateRef.current.byNodeId,
|
|
448
|
+
currentView.zoom,
|
|
449
|
+
labelBgRef.current,
|
|
450
|
+
occupiedLabelRects,
|
|
451
|
+
)
|
|
452
|
+
ctx.restore()
|
|
453
|
+
ctx.restore()
|
|
454
|
+
lastView = currentView
|
|
455
|
+
needsRedrawRef.current = false
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
rafId = requestAnimationFrame(frame)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
rafId = requestAnimationFrame(frame)
|
|
462
|
+
return () => cancelAnimationFrame(rafId)
|
|
463
|
+
}, [initialized, layout, viewStateRef])
|
|
464
|
+
|
|
465
|
+
// Force draw when layout changes (though NaN in RAF loop should cover it)
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (initialized) needsRedrawRef.current = true
|
|
468
|
+
}, [layout, initialized])
|
|
469
|
+
|
|
470
|
+
// Sync highlighted tags + color with renderer module
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
setRendererHighlightedTags(new Set(highlightedTags ?? []))
|
|
473
|
+
setRendererHighlightColor(highlightColor ?? '')
|
|
474
|
+
needsRedrawRef.current = true
|
|
475
|
+
}, [highlightedTags, highlightColor])
|
|
476
|
+
|
|
477
|
+
// Sync hidden tags with renderer module
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
setRendererHiddenTags(new Set(hiddenTags ?? []))
|
|
480
|
+
needsRedrawRef.current = true
|
|
481
|
+
}, [hiddenTags])
|
|
482
|
+
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
setHoverLocked(hoverLocked)
|
|
485
|
+
}, [hoverLocked, setHoverLocked])
|
|
486
|
+
|
|
487
|
+
// Clear renderer state on unmount
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
return () => {
|
|
490
|
+
setRendererHighlightedTags(new Set())
|
|
491
|
+
setRendererHighlightColor('')
|
|
492
|
+
setRendererHiddenTags(new Set())
|
|
493
|
+
}
|
|
494
|
+
}, [])
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<div
|
|
498
|
+
ref={containerRef}
|
|
499
|
+
style={{ width: '100%', height: '100%', overflow: 'hidden', position: 'relative' }}
|
|
500
|
+
>
|
|
501
|
+
<canvas
|
|
502
|
+
ref={canvasRef}
|
|
503
|
+
style={{
|
|
504
|
+
display: 'block',
|
|
505
|
+
width: '100%',
|
|
506
|
+
height: '100%',
|
|
507
|
+
opacity: initialized ? 1 : 0,
|
|
508
|
+
transition: 'opacity 0.2s ease-in',
|
|
509
|
+
touchAction: 'none',
|
|
510
|
+
}}
|
|
511
|
+
/>
|
|
512
|
+
|
|
513
|
+
{/* Breadcrumb Overlay */}
|
|
514
|
+
{initialized && currentPath.length > 0 && (
|
|
515
|
+
<Box
|
|
516
|
+
position="absolute"
|
|
517
|
+
top={isMobileLayout ? "66px" : 4}
|
|
518
|
+
left={4}
|
|
519
|
+
zIndex={10}
|
|
520
|
+
className="glass"
|
|
521
|
+
borderRadius="lg"
|
|
522
|
+
px={3}
|
|
523
|
+
py={1.5}
|
|
524
|
+
pointerEvents="auto"
|
|
525
|
+
>
|
|
526
|
+
<Breadcrumb
|
|
527
|
+
spacing="8px"
|
|
528
|
+
separator={<Text color="whiteAlpha.400" fontSize="xs">/</Text>}
|
|
529
|
+
>
|
|
530
|
+
{currentPath.map((item, idx) => (
|
|
531
|
+
<BreadcrumbItem key={item.id} isCurrentPage={idx === currentPath.length - 1}>
|
|
532
|
+
<BreadcrumbLink
|
|
533
|
+
onClick={() => zoomToPathItem(item)}
|
|
534
|
+
color={idx === currentPath.length - 1 ? "var(--accent)" : "gray.400"}
|
|
535
|
+
fontSize="xs"
|
|
536
|
+
fontWeight={idx === currentPath.length - 1 ? "600" : "normal"}
|
|
537
|
+
_hover={{ color: "var(--accent)", textDecoration: "none" }}
|
|
538
|
+
display="flex"
|
|
539
|
+
alignItems="center"
|
|
540
|
+
gap={1.5}
|
|
541
|
+
>
|
|
542
|
+
{item.type === 'group' && (
|
|
543
|
+
<Icon viewBox="0 0 24 24" boxSize={3} fill="none" stroke="currentColor" strokeWidth="2">
|
|
544
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
545
|
+
<polyline points="9 22 9 12 15 12 15 22" />
|
|
546
|
+
</Icon>
|
|
547
|
+
)}
|
|
548
|
+
{item.isCircular && (
|
|
549
|
+
<Icon viewBox="0 0 24 24" boxSize={3.5} fill="none" stroke="currentColor" strokeWidth="3.5">
|
|
550
|
+
<path d="M20 4l-4 4 4 4" />
|
|
551
|
+
<path d="M16 8h-4a8 8 0 1 0 8 8" />
|
|
552
|
+
</Icon>
|
|
553
|
+
)}
|
|
554
|
+
{item.label}
|
|
555
|
+
</BreadcrumbLink>
|
|
556
|
+
</BreadcrumbItem>
|
|
557
|
+
))}
|
|
558
|
+
</Breadcrumb>
|
|
559
|
+
{currentPath[currentPath.length - 1]?.isCircular && (
|
|
560
|
+
<Text mt={1.5} color="var(--accent)" fontSize="2xs" fontWeight="500" letterSpacing="wide">
|
|
561
|
+
CIRCULAR LINK - CLICK BREADCRUMB TO JUMP BACK
|
|
562
|
+
</Text>
|
|
563
|
+
)}
|
|
564
|
+
</Box>
|
|
565
|
+
)}
|
|
566
|
+
|
|
567
|
+
{/* Hover metadata card */}
|
|
568
|
+
<Popover
|
|
569
|
+
isOpen={isHoveredItemFullyVisible}
|
|
570
|
+
placement="right-start"
|
|
571
|
+
closeOnBlur={false}
|
|
572
|
+
gutter={12}
|
|
573
|
+
isLazy
|
|
574
|
+
>
|
|
575
|
+
<PopoverTrigger>
|
|
576
|
+
<Box
|
|
577
|
+
position="absolute"
|
|
578
|
+
left={hoveredScreenRect?.sx ?? 0}
|
|
579
|
+
top={hoveredScreenRect?.sy ?? 0}
|
|
580
|
+
width={hoveredScreenRect?.sw ?? 0}
|
|
581
|
+
height={hoveredScreenRect?.sh ?? 0}
|
|
582
|
+
pointerEvents="none"
|
|
583
|
+
/>
|
|
584
|
+
</PopoverTrigger>
|
|
585
|
+
<Portal>
|
|
586
|
+
<PopoverContent
|
|
587
|
+
bg="gray.900"
|
|
588
|
+
borderColor="whiteAlpha.300"
|
|
589
|
+
boxShadow="2xl"
|
|
590
|
+
width="280px"
|
|
591
|
+
_focus={{ boxShadow: 'none' }}
|
|
592
|
+
pointerEvents="auto"
|
|
593
|
+
onMouseEnter={() => setHoverLocked(true)}
|
|
594
|
+
onMouseLeave={() => setHoverLocked(false)}
|
|
595
|
+
>
|
|
596
|
+
<PopoverArrow bg="gray.900" />
|
|
597
|
+
{hoveredItem?.type === 'node' && (
|
|
598
|
+
<>
|
|
599
|
+
<PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
|
|
600
|
+
<HStack spacing={3}>
|
|
601
|
+
{hoveredItem.data.logoUrl && (
|
|
602
|
+
<Box flexShrink={0}>
|
|
603
|
+
<ChakraImage src={hoveredItem.data.logoUrl} boxSize="24px" objectFit="contain" />
|
|
604
|
+
</Box>
|
|
605
|
+
)}
|
|
606
|
+
<VStack align="start" spacing={0} flex={1} overflow="hidden">
|
|
607
|
+
<Text fontWeight="600" fontSize="sm" isTruncated width="100%" color="white">
|
|
608
|
+
{hoveredItem.data.label}
|
|
609
|
+
</Text>
|
|
610
|
+
<Badge colorScheme={hoveredItem.data.isPortal ? "purple" : "blue"} variant="subtle" fontSize="2xs">
|
|
611
|
+
{hoveredItem.data.isPortal ? "Portal" : hoveredItem.data.type}
|
|
612
|
+
</Badge>
|
|
613
|
+
</VStack>
|
|
614
|
+
</HStack>
|
|
615
|
+
</PopoverHeader>
|
|
616
|
+
<PopoverBody px={4} py={3}>
|
|
617
|
+
<VStack align="start" spacing={3}>
|
|
618
|
+
{hoveredItem.data.technology && (
|
|
619
|
+
<Box>
|
|
620
|
+
<Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">TECHNOLOGY</Text>
|
|
621
|
+
<Text fontSize="xs" color="gray.200">{hoveredItem.data.technology}</Text>
|
|
622
|
+
</Box>
|
|
623
|
+
)}
|
|
624
|
+
{hoveredItem.data.description && (
|
|
625
|
+
<Box>
|
|
626
|
+
<Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">DESCRIPTION</Text>
|
|
627
|
+
<Text fontSize="xs" color="gray.200" noOfLines={4}>{hoveredItem.data.description}</Text>
|
|
628
|
+
</Box>
|
|
629
|
+
)}
|
|
630
|
+
{hoveredItem.data.linkedDiagramId && (
|
|
631
|
+
<Box>
|
|
632
|
+
<Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">LINKS TO</Text>
|
|
633
|
+
<Text fontSize="xs" color="teal.300" fontWeight="500">
|
|
634
|
+
⊞ {hoveredItem.data.linkedDiagramLabel}
|
|
635
|
+
</Text>
|
|
636
|
+
</Box>
|
|
637
|
+
)}
|
|
638
|
+
<Divider borderColor="whiteAlpha.200" />
|
|
639
|
+
<Button
|
|
640
|
+
as={RouterLink}
|
|
641
|
+
to={hoveredItem.data.isPortal
|
|
642
|
+
? `/views/${hoveredItem.data.linkedDiagramId}`
|
|
643
|
+
: `/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.elementId}`
|
|
644
|
+
}
|
|
645
|
+
size="xs"
|
|
646
|
+
colorScheme="teal"
|
|
647
|
+
variant="solid"
|
|
648
|
+
width="full"
|
|
649
|
+
rightIcon={<ExternalLinkIcon />}
|
|
650
|
+
onClick={(e) => e.stopPropagation()}
|
|
651
|
+
>
|
|
652
|
+
{hoveredItem.data.isPortal ? 'Open Diagram' : 'Open in Editor'}
|
|
653
|
+
</Button>
|
|
654
|
+
</VStack>
|
|
655
|
+
</PopoverBody>
|
|
656
|
+
</>
|
|
657
|
+
)}
|
|
658
|
+
{hoveredItem?.type === 'edge' && hoveredItem.data.isProxy && hoveredItem.data.details && (
|
|
659
|
+
<>
|
|
660
|
+
<PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
|
|
661
|
+
<VStack align="start" spacing={0}>
|
|
662
|
+
<Text fontWeight="600" fontSize="sm" color="white">
|
|
663
|
+
Cross-Branch Connector
|
|
664
|
+
</Text>
|
|
665
|
+
<Badge colorScheme="blue" variant="subtle" fontSize="2xs">
|
|
666
|
+
{hoveredItem.data.details.count} connector{hoveredItem.data.details.count === 1 ? '' : 's'}
|
|
667
|
+
</Badge>
|
|
668
|
+
</VStack>
|
|
669
|
+
</PopoverHeader>
|
|
670
|
+
<PopoverBody px={4} py={3}>
|
|
671
|
+
<VStack align="start" spacing={3}>
|
|
672
|
+
<VStack align="start" spacing={1}>
|
|
673
|
+
<Text color="gray.400" fontSize="2xs" fontWeight="600" letterSpacing="wider">BETWEEN</Text>
|
|
674
|
+
<Text fontSize="xs" color="gray.200">
|
|
675
|
+
{hoveredItem.data.details.sourceAnchorName} → {hoveredItem.data.details.targetAnchorName}
|
|
676
|
+
</Text>
|
|
677
|
+
<Text fontSize="xs" color="gray.400">{hoveredItem.data.details.label}</Text>
|
|
678
|
+
</VStack>
|
|
679
|
+
<Divider borderColor="whiteAlpha.200" />
|
|
680
|
+
<VStack align="stretch" spacing={2} width="full">
|
|
681
|
+
{hoveredItem.data.details.ownerViewIds.map((ownerViewId, index) => (
|
|
682
|
+
<Button
|
|
683
|
+
key={`${ownerViewId}-${index}`}
|
|
684
|
+
as={RouterLink}
|
|
685
|
+
to={`/views/${ownerViewId}`}
|
|
686
|
+
size="xs"
|
|
687
|
+
colorScheme="gray"
|
|
688
|
+
variant="solid"
|
|
689
|
+
width="full"
|
|
690
|
+
justifyContent="space-between"
|
|
691
|
+
rightIcon={<ExternalLinkIcon />}
|
|
692
|
+
onClick={(e) => e.stopPropagation()}
|
|
693
|
+
>
|
|
694
|
+
{hoveredItem.data.details!.ownerViewNames[index] ?? `Open View ${ownerViewId}`}
|
|
695
|
+
</Button>
|
|
696
|
+
))}
|
|
697
|
+
</VStack>
|
|
698
|
+
<Divider borderColor="whiteAlpha.200" />
|
|
699
|
+
<HStack width="full" spacing={2}>
|
|
700
|
+
<Button
|
|
701
|
+
as={RouterLink}
|
|
702
|
+
to={`/views/${hoveredItem.data.details!.connectors[0]?.source.anchorViewId ?? hoveredItem.data.diagramId}?element=${hoveredItem.data.sourceObjId}`}
|
|
703
|
+
size="xs"
|
|
704
|
+
colorScheme="gray"
|
|
705
|
+
variant="solid"
|
|
706
|
+
flex={1}
|
|
707
|
+
rightIcon={<ExternalLinkIcon />}
|
|
708
|
+
onClick={(e) => e.stopPropagation()}
|
|
709
|
+
>
|
|
710
|
+
Open Source
|
|
711
|
+
</Button>
|
|
712
|
+
<Button
|
|
713
|
+
as={RouterLink}
|
|
714
|
+
to={`/views/${hoveredItem.data.details!.connectors[0]?.target.anchorViewId ?? hoveredItem.data.diagramId}?element=${hoveredItem.data.targetObjId}`}
|
|
715
|
+
size="xs"
|
|
716
|
+
colorScheme="teal"
|
|
717
|
+
variant="solid"
|
|
718
|
+
flex={1}
|
|
719
|
+
rightIcon={<ExternalLinkIcon />}
|
|
720
|
+
onClick={(e) => e.stopPropagation()}
|
|
721
|
+
>
|
|
722
|
+
Open Target
|
|
723
|
+
</Button>
|
|
724
|
+
</HStack>
|
|
725
|
+
</VStack>
|
|
726
|
+
</PopoverBody>
|
|
727
|
+
</>
|
|
728
|
+
)}
|
|
729
|
+
{hoveredItem?.type === 'edge' && !hoveredItem.data.isProxy && (
|
|
730
|
+
<>
|
|
731
|
+
<PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
|
|
732
|
+
<VStack align="start" spacing={0}>
|
|
733
|
+
<Text fontWeight="600" fontSize="sm" color="white">
|
|
734
|
+
{hoveredItem.data.label}
|
|
735
|
+
</Text>
|
|
736
|
+
<Badge colorScheme={hoveredItem.data.isPortalConn ? "purple" : "orange"} variant="subtle" fontSize="2xs">
|
|
737
|
+
{hoveredItem.data.isPortalConn ? "Portal Connection" : "Connection"}
|
|
738
|
+
</Badge>
|
|
739
|
+
</VStack>
|
|
740
|
+
</PopoverHeader>
|
|
741
|
+
<PopoverBody px={4} py={3}>
|
|
742
|
+
<VStack align="start" spacing={3}>
|
|
743
|
+
<VStack align="start" spacing={1}>
|
|
744
|
+
<Text color="gray.400" fontSize="2xs" fontWeight="600" letterSpacing="wider">BETWEEN</Text>
|
|
745
|
+
<Text fontSize="xs" color="gray.200">{hoveredItem.data.sourceId} & {hoveredItem.data.targetId}</Text>
|
|
746
|
+
</VStack>
|
|
747
|
+
<Divider borderColor="whiteAlpha.200" />
|
|
748
|
+
|
|
749
|
+
{hoveredItem.data.isPortalConn ? (
|
|
750
|
+
<>
|
|
751
|
+
<Button
|
|
752
|
+
as={RouterLink}
|
|
753
|
+
to={`/views/${hoveredItem.data.diagramId}`}
|
|
754
|
+
size="xs"
|
|
755
|
+
colorScheme="gray"
|
|
756
|
+
variant="solid"
|
|
757
|
+
width="full"
|
|
758
|
+
rightIcon={<ExternalLinkIcon />}
|
|
759
|
+
onClick={(e) => e.stopPropagation()}
|
|
760
|
+
>
|
|
761
|
+
Open {hoveredItem.data.sourceId}
|
|
762
|
+
</Button>
|
|
763
|
+
<Button
|
|
764
|
+
as={RouterLink}
|
|
765
|
+
to={`/views/${hoveredItem.data.targetDiagId}`}
|
|
766
|
+
size="xs"
|
|
767
|
+
colorScheme="teal"
|
|
768
|
+
variant="solid"
|
|
769
|
+
width="full"
|
|
770
|
+
rightIcon={<ExternalLinkIcon />}
|
|
771
|
+
onClick={(e) => e.stopPropagation()}
|
|
772
|
+
>
|
|
773
|
+
Open {hoveredItem.data.targetId}
|
|
774
|
+
</Button>
|
|
775
|
+
</>
|
|
776
|
+
) : (
|
|
777
|
+
<>
|
|
778
|
+
<Button
|
|
779
|
+
as={RouterLink}
|
|
780
|
+
to={`/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.sourceObjId}`}
|
|
781
|
+
size="xs"
|
|
782
|
+
colorScheme="gray"
|
|
783
|
+
variant="solid"
|
|
784
|
+
width="full"
|
|
785
|
+
rightIcon={<ExternalLinkIcon />}
|
|
786
|
+
onClick={(e) => e.stopPropagation()}
|
|
787
|
+
>
|
|
788
|
+
Go to {hoveredItem.data.sourceId}
|
|
789
|
+
</Button>
|
|
790
|
+
<Button
|
|
791
|
+
as={RouterLink}
|
|
792
|
+
to={`/views/${hoveredItem.data.diagramId}?element=${hoveredItem.data.targetObjId}`}
|
|
793
|
+
size="xs"
|
|
794
|
+
colorScheme="teal"
|
|
795
|
+
variant="solid"
|
|
796
|
+
width="full"
|
|
797
|
+
rightIcon={<ExternalLinkIcon />}
|
|
798
|
+
onClick={(e) => e.stopPropagation()}
|
|
799
|
+
>
|
|
800
|
+
Go to {hoveredItem.data.targetId}
|
|
801
|
+
</Button>
|
|
802
|
+
</>
|
|
803
|
+
)}
|
|
804
|
+
</VStack>
|
|
805
|
+
</PopoverBody>
|
|
806
|
+
</>
|
|
807
|
+
)}
|
|
808
|
+
{hoveredItem?.type === 'group' && (
|
|
809
|
+
<>
|
|
810
|
+
<PopoverHeader borderBottom="1px solid" borderColor="whiteAlpha.200" px={4} py={3}>
|
|
811
|
+
<VStack align="start" spacing={0}>
|
|
812
|
+
<Text fontWeight="600" fontSize="sm" color="white">
|
|
813
|
+
{hoveredItem.data.label}
|
|
814
|
+
</Text>
|
|
815
|
+
<Badge colorScheme="purple" variant="subtle" fontSize="2xs">
|
|
816
|
+
Diagram Group
|
|
817
|
+
</Badge>
|
|
818
|
+
</VStack>
|
|
819
|
+
</PopoverHeader>
|
|
820
|
+
<PopoverBody px={4} py={3}>
|
|
821
|
+
<VStack align="start" spacing={3}>
|
|
822
|
+
{hoveredItem.data.description && (
|
|
823
|
+
<Box>
|
|
824
|
+
<Text color="gray.400" fontSize="xs" fontWeight="600" mb={0.5} letterSpacing="wider">DESCRIPTION</Text>
|
|
825
|
+
<Text fontSize="xs" color="gray.200" noOfLines={4}>{hoveredItem.data.description}</Text>
|
|
826
|
+
</Box>
|
|
827
|
+
)}
|
|
828
|
+
<Text fontSize="xs" color="gray.300">
|
|
829
|
+
Root level diagram containing {hoveredItem.data.nodes.length} elements and {hoveredItem.data.edges.length} connections.
|
|
830
|
+
</Text>
|
|
831
|
+
<Divider borderColor="whiteAlpha.200" />
|
|
832
|
+
<Button
|
|
833
|
+
as={RouterLink}
|
|
834
|
+
to={`/views/${hoveredItem.data.diagramId}`}
|
|
835
|
+
size="xs"
|
|
836
|
+
colorScheme="teal"
|
|
837
|
+
variant="solid"
|
|
838
|
+
width="full"
|
|
839
|
+
rightIcon={<ExternalLinkIcon />}
|
|
840
|
+
onClick={(e) => e.stopPropagation()}
|
|
841
|
+
>
|
|
842
|
+
Open Diagram
|
|
843
|
+
</Button>
|
|
844
|
+
</VStack>
|
|
845
|
+
</PopoverBody>
|
|
846
|
+
</>
|
|
847
|
+
)}
|
|
848
|
+
</PopoverContent>
|
|
849
|
+
</Portal>
|
|
850
|
+
</Popover>
|
|
851
|
+
</div>
|
|
852
|
+
)
|
|
853
|
+
})
|