@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.
@@ -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 Props extends CoreUISlots {
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,4 @@
1
+ /**
2
+ * Truncates a string to a specified length and appends an ellipsis.
3
+ */
4
+ export declare const truncate: (str: string, limit?: number) => string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tldiagram/core-ui",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- <Box w="7px" h="7px" rounded="full" bg={settings.enabled ? 'var(--accent)' : 'gray.500'} />
55
- <Text fontSize="11px" fontWeight="normal">{label}</Text>
56
- <Text fontSize="10px" color="gray.400">{settings.enabled ? connectorBudget : 'Off'}</Text>
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="glass.bg"
63
- backdropFilter="blur(16px)"
64
- borderColor="glass.border"
65
- boxShadow="panel"
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="240px"
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 justify="space-between">
74
- <Text fontSize="xs" fontWeight="600" color="white">Show cross-branch context</Text>
75
- <Switch isChecked={settings.enabled} onChange={(event) => onEnabledChange(event.target.checked)} colorScheme="blue" />
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
- <Box opacity={settings.enabled ? 1 : 0.4}>
78
- <Text fontSize="10px" fontWeight="700" color="gray.400" letterSpacing="0.08em" textTransform="uppercase" mb={2}>
79
- Priority
80
- </Text>
81
- <HStack spacing={1} bg="whiteAlpha.100" borderRadius="md" p={1}>
82
- {(['external', 'internal'] as const).map((priority) => {
83
- const active = settings.connectorPriority === priority
84
- return (
85
- <Button
86
- key={priority}
87
- size="xs"
88
- h="24px"
89
- flex={1}
90
- isDisabled={!settings.enabled}
91
- variant="ghost"
92
- bg={active ? 'rgba(var(--accent-rgb), 0.18)' : 'transparent'}
93
- color={active ? 'var(--accent)' : 'gray.300'}
94
- _hover={{ bg: active ? 'rgba(var(--accent-rgb), 0.22)' : 'whiteAlpha.100' }}
95
- onClick={() => onPriorityChange(priority)}
96
- >
97
- {priority === 'external' ? 'External' : 'Internal'}
98
- </Button>
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
- <Box opacity={settings.enabled ? 1 : 0.4}>
104
- <HStack justify="space-between" mb={2}>
105
- <Text fontSize="10px" fontWeight="700" color="gray.400" letterSpacing="0.08em" textTransform="uppercase">
106
- Connector Budget
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, getStraightPath, type EdgeProps } from 'reactflow'
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 sourceNode = useStore((s) => s.nodeInternals.get(source))
35
- const targetNode = useStore((s) => s.nodeInternals.get(target))
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 sourceCx = (sourceNode.positionAbsolute.x ?? 0) + (sourceNode.width ?? 0) / 2
44
- const sourceCy = (sourceNode.positionAbsolute.y ?? 0) + (sourceNode.height ?? 0) / 2
45
- const targetCx = (targetNode.positionAbsolute.x ?? 0) + (targetNode.width ?? 0) / 2
46
- const targetCy = (targetNode.positionAbsolute.y ?? 0) + (targetNode.height ?? 0) / 2
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 sourcePoint = getNodeBorderPoint(sourceNode, { x: targetCx, y: targetCy })
49
- const targetPoint = getNodeBorderPoint(targetNode, { x: sourceCx, y: sourceCy })
50
-
51
- const [connectorPath] = getStraightPath({
52
- sourceX: sourcePoint.x,
53
- sourceY: sourcePoint.y,
54
- targetX: targetPoint.x,
55
- targetY: targetPoint.y,
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={active ? 1.5 : 1}
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={active ? 1.5 : 1}
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={sourcePoint.x}
93
- cy={sourcePoint.y}
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={connectorPath}
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