@tldiagram/core-ui 2.0.3 → 2.0.5
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/components/FloatingEdge.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8700 -8398
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +1 -0
- package/dist/pages/ViewEditor/index.d.ts +6 -1
- package/dist/utils/string.d.ts +4 -0
- package/dist/utils/string.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/CrossBranchControls.tsx +178 -61
- package/src/components/FloatingEdge.tsx +145 -39
- package/src/components/ProxyConnectorPanel.tsx +3 -2
- package/src/components/ViewFloatingMenu.tsx +158 -68
- package/src/index.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +4 -4
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +38 -0
- package/src/pages/ViewEditor/hooks/useViewData.ts +12 -2
- package/src/pages/ViewEditor/index.tsx +51 -22
- package/src/pages/ViewsGrid.tsx +2 -2
- package/src/utils/string.test.ts +19 -0
- package/src/utils/string.ts +7 -0
|
@@ -173,6 +173,7 @@ export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _d
|
|
|
173
173
|
clientX: number;
|
|
174
174
|
clientY: number;
|
|
175
175
|
}) => void;
|
|
176
|
+
stableOnReconnectPick: (targetElementId: number) => Promise<boolean>;
|
|
176
177
|
showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect", forceConnect?: boolean) => void;
|
|
177
178
|
onNodesChange: (changes: NodeChange[]) => void;
|
|
178
179
|
onEdgesChange: (changes: EdgeChange[]) => void;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { CoreUISlots } from '../../slots';
|
|
2
2
|
import 'reactflow/dist/style.css';
|
|
3
3
|
import { type ViewEditorDemoOptions } from '../../demo/viewEditor';
|
|
4
|
-
interface
|
|
4
|
+
export interface ViewEditorPermissions {
|
|
5
|
+
canEdit?: boolean;
|
|
6
|
+
isOwner?: boolean;
|
|
7
|
+
isFreePlan?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface Props extends CoreUISlots, ViewEditorPermissions {
|
|
5
10
|
demoOptions?: ViewEditorDemoOptions;
|
|
6
11
|
}
|
|
7
12
|
export default function ViewEditor(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
Box,
|
|
3
4
|
Button,
|
|
4
5
|
HStack,
|
|
5
6
|
Popover,
|
|
6
|
-
PopoverArrow,
|
|
7
7
|
PopoverBody,
|
|
8
8
|
PopoverContent,
|
|
9
9
|
PopoverTrigger,
|
|
@@ -14,14 +14,30 @@ import {
|
|
|
14
14
|
SliderTrack,
|
|
15
15
|
Switch,
|
|
16
16
|
Text,
|
|
17
|
+
useDisclosure,
|
|
17
18
|
VStack,
|
|
18
19
|
} from '@chakra-ui/react'
|
|
20
|
+
import { FocusIcon, ChevronDownIcon } from './Icons'
|
|
19
21
|
import {
|
|
20
22
|
CROSS_BRANCH_CONNECTOR_BUDGET_MAX,
|
|
21
23
|
CROSS_BRANCH_CONNECTOR_BUDGET_MIN,
|
|
22
24
|
} from '../crossBranch/types'
|
|
23
25
|
import type { CrossBranchConnectorPriority, CrossBranchContextSettings } from '../crossBranch/types'
|
|
24
26
|
|
|
27
|
+
const DENSITY_STOPS = [
|
|
28
|
+
{ value: -2, label: 'Quiet', budget: CROSS_BRANCH_CONNECTOR_BUDGET_MIN },
|
|
29
|
+
{ value: -1, label: 'Lean', budget: 25 },
|
|
30
|
+
{ value: 0, label: 'Normal', budget: 50 },
|
|
31
|
+
{ value: 1, label: 'Rich', budget: 100 },
|
|
32
|
+
{ value: 2, label: 'Full', budget: CROSS_BRANCH_CONNECTOR_BUDGET_MAX },
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
function densityFromBudget(budget: number) {
|
|
36
|
+
return DENSITY_STOPS.reduce((closest, stop) => (
|
|
37
|
+
Math.abs(stop.budget - budget) < Math.abs(closest.budget - budget) ? stop : closest
|
|
38
|
+
), DENSITY_STOPS[0]).value
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
interface Props {
|
|
26
42
|
settings: CrossBranchContextSettings
|
|
27
43
|
onEnabledChange: (enabled: boolean) => void
|
|
@@ -38,92 +54,193 @@ export default function CrossBranchControls({
|
|
|
38
54
|
label = 'Cross-Branch',
|
|
39
55
|
}: Props) {
|
|
40
56
|
const connectorBudget = settings.connectorBudget
|
|
57
|
+
const { isOpen, onClose, onToggle } = useDisclosure()
|
|
58
|
+
const [draftDensityLevel, setDraftDensityLevel] = useState<number>(() => densityFromBudget(connectorBudget))
|
|
59
|
+
const activeDensity = DENSITY_STOPS.find((stop) => stop.value === draftDensityLevel) ?? DENSITY_STOPS[2]
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setDraftDensityLevel(densityFromBudget(connectorBudget))
|
|
63
|
+
}, [connectorBudget])
|
|
41
64
|
|
|
42
65
|
return (
|
|
43
|
-
<Popover placement="top-start" isLazy>
|
|
66
|
+
<Popover isOpen={isOpen} onClose={onClose} placement="top-start" isLazy closeOnBlur>
|
|
44
67
|
<PopoverTrigger>
|
|
45
68
|
<Button
|
|
46
69
|
variant="ghost"
|
|
47
70
|
h="28px"
|
|
48
71
|
px={2.5}
|
|
49
|
-
color={settings.enabled ? 'var(--accent)' : 'gray.300'}
|
|
72
|
+
color={isOpen || settings.enabled ? 'var(--accent)' : 'gray.300'}
|
|
50
73
|
bg={settings.enabled ? 'rgba(var(--accent-rgb), 0.12)' : 'transparent'}
|
|
51
74
|
_hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
|
|
75
|
+
onClick={onToggle}
|
|
76
|
+
aria-label={`Open ${label} filters`}
|
|
52
77
|
>
|
|
53
78
|
<HStack spacing={1.5}>
|
|
54
|
-
<
|
|
55
|
-
<Text fontSize="11px" fontWeight=
|
|
56
|
-
<Text fontSize="10px" color=
|
|
79
|
+
<FocusIcon />
|
|
80
|
+
<Text fontSize="11px" fontWeight={settings.enabled ? 'semibold' : 'normal'}>{label}</Text>
|
|
81
|
+
<Text fontSize="10px" color={settings.enabled ? 'var(--accent)' : 'gray.400'}>
|
|
82
|
+
{settings.enabled ? activeDensity.label : 'Off'}
|
|
83
|
+
</Text>
|
|
84
|
+
<ChevronDownIcon size={10} strokeWidth={3.5} />
|
|
57
85
|
</HStack>
|
|
58
86
|
</Button>
|
|
59
87
|
</PopoverTrigger>
|
|
60
88
|
<Portal>
|
|
61
89
|
<PopoverContent
|
|
62
|
-
bg="
|
|
63
|
-
backdropFilter="blur(
|
|
64
|
-
borderColor="
|
|
65
|
-
boxShadow="
|
|
90
|
+
bg="linear-gradient(180deg, rgba(var(--bg-main-rgb), 0.98) 0%, rgba(var(--bg-main-rgb), 0.92) 100%)"
|
|
91
|
+
backdropFilter="blur(22px)"
|
|
92
|
+
borderColor="whiteAlpha.100"
|
|
93
|
+
boxShadow="0 18px 48px rgba(0,0,0,0.46), inset 0 1px 0 rgba(255,255,255,0.04)"
|
|
66
94
|
borderRadius="lg"
|
|
67
|
-
width="
|
|
95
|
+
width="292px"
|
|
68
96
|
_focus={{ boxShadow: 'none' }}
|
|
69
97
|
>
|
|
70
|
-
<PopoverArrow bg="glass.bg" />
|
|
71
98
|
<PopoverBody p={3}>
|
|
72
99
|
<VStack align="stretch" spacing={3}>
|
|
73
|
-
<HStack
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
<HStack
|
|
101
|
+
justify="space-between"
|
|
102
|
+
spacing={3}
|
|
103
|
+
px={2.5}
|
|
104
|
+
py={2}
|
|
105
|
+
rounded="md"
|
|
106
|
+
bg={settings.enabled ? 'rgba(var(--accent-rgb), 0.10)' : 'whiteAlpha.50'}
|
|
107
|
+
border="1px solid"
|
|
108
|
+
borderColor={settings.enabled ? 'rgba(var(--accent-rgb), 0.22)' : 'whiteAlpha.100'}
|
|
109
|
+
>
|
|
110
|
+
<HStack spacing={2.5} minW={0}>
|
|
111
|
+
<Box color={settings.enabled ? 'var(--accent)' : 'gray.400'} flexShrink={0}>
|
|
112
|
+
<FocusIcon />
|
|
113
|
+
</Box>
|
|
114
|
+
<Box minW={0}>
|
|
115
|
+
<Text fontSize="xs" fontWeight="semibold" color="whiteAlpha.900">Cross-branch context</Text>
|
|
116
|
+
<Text fontSize="10px" color="whiteAlpha.600" noOfLines={1}>
|
|
117
|
+
{settings.enabled ? 'Show relationships across branches' : 'Branch context is hidden'}
|
|
118
|
+
</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
</HStack>
|
|
121
|
+
<Switch
|
|
122
|
+
size="sm"
|
|
123
|
+
isChecked={settings.enabled}
|
|
124
|
+
onChange={(event) => onEnabledChange(event.target.checked)}
|
|
125
|
+
colorScheme="teal"
|
|
126
|
+
flexShrink={0}
|
|
127
|
+
aria-label="Toggle cross-branch context"
|
|
128
|
+
/>
|
|
76
129
|
</HStack>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
130
|
+
|
|
131
|
+
<Box
|
|
132
|
+
opacity={settings.enabled ? 1 : 0.45}
|
|
133
|
+
px={2.5}
|
|
134
|
+
py={2.5}
|
|
135
|
+
rounded="md"
|
|
136
|
+
bg="whiteAlpha.50"
|
|
137
|
+
border="1px solid"
|
|
138
|
+
borderColor="whiteAlpha.100"
|
|
139
|
+
>
|
|
140
|
+
<HStack justify="space-between" mb={2}>
|
|
141
|
+
<Box>
|
|
142
|
+
<Text fontSize="xs" fontWeight="semibold" color="whiteAlpha.900">Priority</Text>
|
|
143
|
+
<Text fontSize="10px" color="whiteAlpha.600">Choose connector type to prioritize</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
</HStack>
|
|
146
|
+
<HStack spacing={1} bg="rgba(255,255,255,0.06)" borderRadius="md" p={1}>
|
|
147
|
+
{(['external', 'internal'] as const).map((priority) => (
|
|
148
|
+
<Button
|
|
149
|
+
key={priority}
|
|
150
|
+
size="xs"
|
|
151
|
+
h="24px"
|
|
152
|
+
flex={1}
|
|
153
|
+
isDisabled={!settings.enabled}
|
|
154
|
+
variant="ghost"
|
|
155
|
+
bg={settings.connectorPriority === priority ? 'rgba(var(--accent-rgb), 0.18)' : 'transparent'}
|
|
156
|
+
color={settings.connectorPriority === priority ? 'var(--accent)' : 'gray.300'}
|
|
157
|
+
_hover={{ bg: settings.connectorPriority === priority ? 'rgba(var(--accent-rgb), 0.22)' : 'whiteAlpha.100' }}
|
|
158
|
+
onClick={() => onPriorityChange(priority)}
|
|
159
|
+
>
|
|
160
|
+
{priority === 'external' ? 'External' : 'Internal'}
|
|
161
|
+
</Button>
|
|
162
|
+
))}
|
|
101
163
|
</HStack>
|
|
102
164
|
</Box>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
165
|
+
|
|
166
|
+
<Box
|
|
167
|
+
opacity={settings.enabled ? 1 : 0.45}
|
|
168
|
+
px={2.5}
|
|
169
|
+
py={2.5}
|
|
170
|
+
rounded="md"
|
|
171
|
+
bg="whiteAlpha.50"
|
|
172
|
+
border="1px solid"
|
|
173
|
+
borderColor="whiteAlpha.100"
|
|
174
|
+
>
|
|
175
|
+
<HStack justify="space-between" mb={2.5}>
|
|
176
|
+
<Box>
|
|
177
|
+
<Text fontSize="xs" fontWeight="semibold" color="whiteAlpha.900">Density</Text>
|
|
178
|
+
</Box>
|
|
179
|
+
<Text
|
|
180
|
+
fontSize="10px"
|
|
181
|
+
fontWeight="bold"
|
|
182
|
+
color="var(--accent)"
|
|
183
|
+
bg="rgba(var(--accent-rgb), 0.10)"
|
|
184
|
+
border="1px solid"
|
|
185
|
+
borderColor="rgba(var(--accent-rgb), 0.18)"
|
|
186
|
+
rounded="full"
|
|
187
|
+
px={2}
|
|
188
|
+
py={0.5}
|
|
189
|
+
>
|
|
190
|
+
{activeDensity.label}
|
|
107
191
|
</Text>
|
|
108
|
-
<Text fontSize="xs" color="gray.300">{connectorBudget}</Text>
|
|
109
|
-
</HStack>
|
|
110
|
-
<Slider
|
|
111
|
-
isDisabled={!settings.enabled}
|
|
112
|
-
min={CROSS_BRANCH_CONNECTOR_BUDGET_MIN}
|
|
113
|
-
max={CROSS_BRANCH_CONNECTOR_BUDGET_MAX}
|
|
114
|
-
step={1}
|
|
115
|
-
value={connectorBudget}
|
|
116
|
-
onChange={onBudgetChange}
|
|
117
|
-
>
|
|
118
|
-
<SliderTrack bg="whiteAlpha.200">
|
|
119
|
-
<SliderFilledTrack bg="var(--accent)" />
|
|
120
|
-
</SliderTrack>
|
|
121
|
-
<SliderThumb boxSize={4} />
|
|
122
|
-
</Slider>
|
|
123
|
-
<HStack justify="space-between" mt={1}>
|
|
124
|
-
<Text fontSize="10px" color="gray.500">{CROSS_BRANCH_CONNECTOR_BUDGET_MIN}</Text>
|
|
125
|
-
<Text fontSize="10px" color="gray.500">{CROSS_BRANCH_CONNECTOR_BUDGET_MAX}</Text>
|
|
126
192
|
</HStack>
|
|
193
|
+
<Box px={1} pt={1} pb={0.5}>
|
|
194
|
+
<Slider
|
|
195
|
+
aria-label="Branch density"
|
|
196
|
+
isDisabled={!settings.enabled}
|
|
197
|
+
min={-2}
|
|
198
|
+
max={2}
|
|
199
|
+
step={1}
|
|
200
|
+
value={draftDensityLevel}
|
|
201
|
+
onChange={setDraftDensityLevel}
|
|
202
|
+
onChangeEnd={(value) => {
|
|
203
|
+
setDraftDensityLevel(value)
|
|
204
|
+
const next = DENSITY_STOPS.find((stop) => stop.value === value) ?? DENSITY_STOPS[2]
|
|
205
|
+
onBudgetChange(next.budget)
|
|
206
|
+
}}
|
|
207
|
+
focusThumbOnChange={false}
|
|
208
|
+
>
|
|
209
|
+
<SliderTrack h="4px" bg="whiteAlpha.200">
|
|
210
|
+
<SliderFilledTrack bg="var(--accent)" />
|
|
211
|
+
</SliderTrack>
|
|
212
|
+
{DENSITY_STOPS.map((stop) => (
|
|
213
|
+
<Box
|
|
214
|
+
key={stop.value}
|
|
215
|
+
position="absolute"
|
|
216
|
+
left={`${((stop.value + 2) / 4) * 100}%`}
|
|
217
|
+
top="50%"
|
|
218
|
+
transform="translate(-50%, -50%)"
|
|
219
|
+
w={stop.value === draftDensityLevel ? '6px' : '2px'}
|
|
220
|
+
h={stop.value === draftDensityLevel ? '6px' : '10px'}
|
|
221
|
+
rounded="full"
|
|
222
|
+
bg={draftDensityLevel >= stop.value ? 'var(--accent)' : 'whiteAlpha.500'}
|
|
223
|
+
pointerEvents="none"
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
<SliderThumb boxSize="14px" bg="white" border="2px solid" borderColor="var(--accent)" />
|
|
227
|
+
</Slider>
|
|
228
|
+
<HStack justify="space-between" mt={2} px={0.5}>
|
|
229
|
+
{DENSITY_STOPS.map((stop) => (
|
|
230
|
+
<Text
|
|
231
|
+
key={stop.value}
|
|
232
|
+
fontSize="9px"
|
|
233
|
+
fontWeight={stop.value === draftDensityLevel ? 'bold' : 'medium'}
|
|
234
|
+
color={stop.value === draftDensityLevel ? 'whiteAlpha.900' : 'whiteAlpha.500'}
|
|
235
|
+
>
|
|
236
|
+
{stop.label}
|
|
237
|
+
</Text>
|
|
238
|
+
))}
|
|
239
|
+
</HStack>
|
|
240
|
+
<Text fontSize="10px" color="whiteAlpha.500" mt={2}>
|
|
241
|
+
Connector budget {connectorBudget}
|
|
242
|
+
</Text>
|
|
243
|
+
</Box>
|
|
127
244
|
</Box>
|
|
128
245
|
</VStack>
|
|
129
246
|
</PopoverBody>
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import { memo, useState } from 'react'
|
|
2
|
-
import { useStore,
|
|
3
|
-
|
|
4
|
-
function getNodeBorderPoint(
|
|
5
|
-
node: { positionAbsolute?: { x: number; y: number }; width?: number | null; height?: number | null },
|
|
6
|
-
targetCenter: { x: number; y: number }
|
|
7
|
-
) {
|
|
8
|
-
const w = (node.width ?? 0) / 2
|
|
9
|
-
const h = (node.height ?? 0) / 2
|
|
10
|
-
const cx = (node.positionAbsolute?.x ?? 0) + w
|
|
11
|
-
const cy = (node.positionAbsolute?.y ?? 0) + h
|
|
12
|
-
const dx = targetCenter.x - cx
|
|
13
|
-
const dy = targetCenter.y - cy
|
|
14
|
-
if (dx === 0 && dy === 0) return { x: cx, y: cy }
|
|
15
|
-
const scaleX = dx !== 0 ? Math.abs(w / dx) : Infinity
|
|
16
|
-
const scaleY = dy !== 0 ? Math.abs(h / dy) : Infinity
|
|
17
|
-
const scale = Math.min(scaleX, scaleY)
|
|
18
|
-
return { x: cx + dx * scale, y: cy + dy * scale }
|
|
19
|
-
}
|
|
2
|
+
import { useStore, type Edge, type EdgeProps } from 'reactflow'
|
|
20
3
|
|
|
21
4
|
export interface FloatingConnectorData {
|
|
22
5
|
color: string
|
|
@@ -24,15 +7,87 @@ export interface FloatingConnectorData {
|
|
|
24
7
|
dashed?: boolean
|
|
25
8
|
}
|
|
26
9
|
|
|
10
|
+
type FlowNode = {
|
|
11
|
+
positionAbsolute?: { x: number; y: number }
|
|
12
|
+
width?: number | null
|
|
13
|
+
height?: number | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Point = { x: number; y: number }
|
|
17
|
+
type RouteDirection = 'down' | 'up'
|
|
18
|
+
|
|
19
|
+
interface OrthogonalRoute {
|
|
20
|
+
source: Point
|
|
21
|
+
target: Point
|
|
22
|
+
busY: number
|
|
23
|
+
direction: RouteDirection
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BundleMember {
|
|
27
|
+
id: string
|
|
28
|
+
target: Point
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const GRID_BUNDLE_OFFSET = 52
|
|
32
|
+
const MIN_BUNDLE_OFFSET = 26
|
|
33
|
+
|
|
34
|
+
function nodeCenter(node: FlowNode) {
|
|
35
|
+
return {
|
|
36
|
+
x: (node.positionAbsolute?.x ?? 0) + (node.width ?? 0) / 2,
|
|
37
|
+
y: (node.positionAbsolute?.y ?? 0) + (node.height ?? 0) / 2,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getOrthogonalRoute(sourceNode: FlowNode, targetNode: FlowNode): OrthogonalRoute {
|
|
42
|
+
const sourceCenter = nodeCenter(sourceNode)
|
|
43
|
+
const targetCenter = nodeCenter(targetNode)
|
|
44
|
+
const sourceHeight = sourceNode.height ?? 0
|
|
45
|
+
const targetHeight = targetNode.height ?? 0
|
|
46
|
+
const direction: RouteDirection = targetCenter.y >= sourceCenter.y ? 'down' : 'up'
|
|
47
|
+
const source = {
|
|
48
|
+
x: sourceCenter.x,
|
|
49
|
+
y: direction === 'down'
|
|
50
|
+
? (sourceNode.positionAbsolute?.y ?? 0) + sourceHeight
|
|
51
|
+
: (sourceNode.positionAbsolute?.y ?? 0),
|
|
52
|
+
}
|
|
53
|
+
const target = {
|
|
54
|
+
x: targetCenter.x,
|
|
55
|
+
y: direction === 'down'
|
|
56
|
+
? (targetNode.positionAbsolute?.y ?? 0)
|
|
57
|
+
: (targetNode.positionAbsolute?.y ?? 0) + targetHeight,
|
|
58
|
+
}
|
|
59
|
+
const availableGap = Math.abs(target.y - source.y)
|
|
60
|
+
const offset = Math.max(MIN_BUNDLE_OFFSET, Math.min(GRID_BUNDLE_OFFSET, availableGap / 2))
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
source,
|
|
64
|
+
target,
|
|
65
|
+
direction,
|
|
66
|
+
busY: direction === 'down' ? source.y + offset : source.y - offset,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function linePath(points: Point[]) {
|
|
71
|
+
if (points.length === 0) return ''
|
|
72
|
+
return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x},${point.y}`).join(' ')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function edgeStyleKey(edge: Edge<FloatingConnectorData>) {
|
|
76
|
+
return `${edge.data?.dashed ? 'dashed' : 'solid'}:${edge.data?.color ?? ''}`
|
|
77
|
+
}
|
|
78
|
+
|
|
27
79
|
function FloatingConnector({
|
|
80
|
+
id,
|
|
28
81
|
source,
|
|
29
82
|
target,
|
|
30
83
|
data,
|
|
31
84
|
selected,
|
|
32
85
|
}: EdgeProps<FloatingConnectorData>) {
|
|
33
86
|
const [hovered, setHovered] = useState(false)
|
|
34
|
-
const
|
|
35
|
-
const
|
|
87
|
+
const edges = useStore((s) => s.edges as Edge<FloatingConnectorData>[])
|
|
88
|
+
const nodeInternals = useStore((s) => s.nodeInternals)
|
|
89
|
+
const sourceNode = nodeInternals.get(source)
|
|
90
|
+
const targetNode = nodeInternals.get(target)
|
|
36
91
|
|
|
37
92
|
if (
|
|
38
93
|
!sourceNode?.positionAbsolute || !targetNode?.positionAbsolute ||
|
|
@@ -40,27 +95,75 @@ function FloatingConnector({
|
|
|
40
95
|
!isFinite(targetNode.positionAbsolute.x) || !isFinite(targetNode.positionAbsolute.y)
|
|
41
96
|
) return null
|
|
42
97
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
98
|
+
const route = getOrthogonalRoute(sourceNode, targetNode)
|
|
99
|
+
const styleKey = `${data?.dashed ? 'dashed' : 'solid'}:${data?.color ?? ''}`
|
|
100
|
+
const members = edges
|
|
101
|
+
.filter((edge): edge is Edge<FloatingConnectorData> => (
|
|
102
|
+
edge.type === 'floating' &&
|
|
103
|
+
edge.source === source &&
|
|
104
|
+
edgeStyleKey(edge) === styleKey
|
|
105
|
+
))
|
|
106
|
+
.map((edge) => {
|
|
107
|
+
const edgeTargetNode = nodeInternals.get(edge.target)
|
|
108
|
+
if (!edgeTargetNode?.positionAbsolute) return null
|
|
109
|
+
const edgeRoute = getOrthogonalRoute(sourceNode, edgeTargetNode)
|
|
110
|
+
if (edgeRoute.direction !== route.direction) return null
|
|
111
|
+
return { id: edge.id, target: edgeRoute.target }
|
|
112
|
+
})
|
|
113
|
+
.filter((member): member is BundleMember => member !== null)
|
|
114
|
+
.sort((a, b) => a.target.x - b.target.x || a.target.y - b.target.y || a.id.localeCompare(b.id))
|
|
47
115
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
116
|
+
const isBundleRepresentative = members[0]?.id === id
|
|
117
|
+
const isBundled = members.length > 1
|
|
118
|
+
const busXs = isBundled
|
|
119
|
+
? [route.source.x, ...members.map((member) => member.target.x)]
|
|
120
|
+
: [route.source.x, route.target.x]
|
|
121
|
+
const busStartX = Math.min(...busXs)
|
|
122
|
+
const busEndX = Math.max(...busXs)
|
|
123
|
+
const connectorPath = isBundled
|
|
124
|
+
? linePath([
|
|
125
|
+
{ x: route.target.x, y: route.busY },
|
|
126
|
+
route.target,
|
|
127
|
+
])
|
|
128
|
+
: linePath([
|
|
129
|
+
route.source,
|
|
130
|
+
{ x: route.source.x, y: route.busY },
|
|
131
|
+
{ x: route.target.x, y: route.busY },
|
|
132
|
+
route.target,
|
|
133
|
+
])
|
|
134
|
+
const sharedPath = isBundled && isBundleRepresentative
|
|
135
|
+
? linePath([
|
|
136
|
+
route.source,
|
|
137
|
+
{ x: route.source.x, y: route.busY },
|
|
138
|
+
{ x: busStartX, y: route.busY },
|
|
139
|
+
{ x: busEndX, y: route.busY },
|
|
140
|
+
])
|
|
141
|
+
: ''
|
|
142
|
+
const hitPath = sharedPath ? `${sharedPath} ${connectorPath}` : connectorPath
|
|
57
143
|
|
|
58
144
|
const color = data?.color ?? '#718096'
|
|
59
145
|
const isPortal = data?.dashed ?? false
|
|
60
146
|
const active = hovered || !!selected
|
|
147
|
+
const strokeWidth = isBundled && isBundleRepresentative
|
|
148
|
+
? active ? 1.8 : 1.35
|
|
149
|
+
: active ? 1.5 : 1
|
|
61
150
|
|
|
62
151
|
return (
|
|
63
152
|
<g>
|
|
153
|
+
{sharedPath && (
|
|
154
|
+
<path
|
|
155
|
+
d={sharedPath}
|
|
156
|
+
fill="none"
|
|
157
|
+
stroke={color}
|
|
158
|
+
strokeWidth={strokeWidth}
|
|
159
|
+
strokeDasharray={isPortal ? '1.5 7' : undefined}
|
|
160
|
+
strokeLinecap="round"
|
|
161
|
+
strokeLinejoin="round"
|
|
162
|
+
opacity={active ? 0.9 : 0.62}
|
|
163
|
+
style={{ transition: 'opacity 0.15s ease, stroke-width 0.15s ease' }}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
|
|
64
167
|
{/* Main stroke */}
|
|
65
168
|
{isPortal ? (
|
|
66
169
|
/* Portal: fine rounded dots - distinct from hierarchy */
|
|
@@ -68,9 +171,10 @@ function FloatingConnector({
|
|
|
68
171
|
d={connectorPath}
|
|
69
172
|
fill="none"
|
|
70
173
|
stroke={color}
|
|
71
|
-
strokeWidth={
|
|
174
|
+
strokeWidth={strokeWidth}
|
|
72
175
|
strokeDasharray="1.5 7"
|
|
73
176
|
strokeLinecap="round"
|
|
177
|
+
strokeLinejoin="round"
|
|
74
178
|
opacity={active ? 0.9 : 0.6}
|
|
75
179
|
style={{ transition: 'opacity 0.15s ease, stroke-width 0.15s ease' }}
|
|
76
180
|
/>
|
|
@@ -80,17 +184,19 @@ function FloatingConnector({
|
|
|
80
184
|
d={connectorPath}
|
|
81
185
|
fill="none"
|
|
82
186
|
stroke={color}
|
|
83
|
-
strokeWidth={
|
|
187
|
+
strokeWidth={strokeWidth}
|
|
188
|
+
strokeLinecap="round"
|
|
189
|
+
strokeLinejoin="round"
|
|
84
190
|
opacity={active ? 0.85 : 0.6}
|
|
85
191
|
style={{ transition: 'opacity 0.15s ease, stroke-width 0.15s ease' }}
|
|
86
192
|
/>
|
|
87
193
|
)}
|
|
88
194
|
|
|
89
195
|
{/* Source terminus dot - hierarchy only, signals the origin node */}
|
|
90
|
-
{!isPortal && (
|
|
196
|
+
{!isPortal && (!isBundled || isBundleRepresentative) && (
|
|
91
197
|
<circle
|
|
92
|
-
cx={
|
|
93
|
-
cy={
|
|
198
|
+
cx={route.source.x}
|
|
199
|
+
cy={route.source.y}
|
|
94
200
|
r={active ? 2.5 : 2}
|
|
95
201
|
fill={color}
|
|
96
202
|
opacity={active ? 0.85 : 0.55}
|
|
@@ -100,7 +206,7 @@ function FloatingConnector({
|
|
|
100
206
|
|
|
101
207
|
{/* Wide transparent hit area for hover detection */}
|
|
102
208
|
<path
|
|
103
|
-
d={
|
|
209
|
+
d={hitPath}
|
|
104
210
|
fill="none"
|
|
105
211
|
stroke="transparent"
|
|
106
212
|
strokeWidth={16}
|
|
@@ -6,6 +6,7 @@ import PanelHeader from './PanelHeader'
|
|
|
6
6
|
import { ChevronRightIcon, NavigationIcon, TrashIcon, EditIcon } from './Icons'
|
|
7
7
|
import { useViewEditorContext } from '../pages/ViewEditor/context'
|
|
8
8
|
import type { Connector } from '../types'
|
|
9
|
+
import { truncate } from '../utils/string'
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
isOpen: boolean
|
|
@@ -83,11 +84,11 @@ export default function ProxyConnectorPanel({
|
|
|
83
84
|
<VStack align="start" spacing={1} flex={1}>
|
|
84
85
|
<HStack spacing={2}>
|
|
85
86
|
<Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
|
|
86
|
-
{leaf.source.actualElementName}
|
|
87
|
+
{truncate(leaf.source.actualElementName)}
|
|
87
88
|
</Text>
|
|
88
89
|
<Icon as={ChevronRightIcon} color="whiteAlpha.400" />
|
|
89
90
|
<Text color="white" fontSize="sm" fontWeight="semibold" isTruncated>
|
|
90
|
-
{leaf.target.actualElementName}
|
|
91
|
+
{truncate(leaf.target.actualElementName)}
|
|
91
92
|
</Text>
|
|
92
93
|
</HStack>
|
|
93
94
|
|