@tldiagram/core-ui 1.93.0 → 1.94.1

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.
@@ -129,6 +129,12 @@ export function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nod
129
129
  return didChange ? nextNodes : nodes
130
130
  }
131
131
 
132
+ export function getConnectorDeletionTarget(
133
+ selectedConnector: Connector | null,
134
+ ) {
135
+ return selectedConnector?.id ?? null
136
+ }
137
+
132
138
  interface CanvasInteractionOptions {
133
139
  viewId: number | null
134
140
  canEdit: boolean
@@ -164,12 +170,11 @@ interface CanvasInteractionOptions {
164
170
  openConnectorPanel: () => void
165
171
  closeConnectorPanel: () => void
166
172
  selectedElement: LibraryElement | null
167
- selectedEdgeId: number | null
173
+ selectedConnector: Connector | null
168
174
  connectors: Connector[]
169
175
  layers: ViewLayer[]
170
176
  setSelectedElement: React.Dispatch<React.SetStateAction<LibraryElement | null>>
171
177
  setSelectedEdge: (e: Connector | null) => void
172
- setSelectedEdgeId: (id: number | null) => void
173
178
  setSelectedProxyConnectorDetails: React.Dispatch<React.SetStateAction<import('../../../crossBranch/types').ProxyConnectorDetails | null>>
174
179
  openProxyConnectorPanel: () => void
175
180
  closeProxyConnectorPanel: () => void
@@ -202,6 +207,12 @@ type HandleReconnectDragState = {
202
207
  hoveredHandleId?: string
203
208
  }
204
209
 
210
+ type InteractionStartOptions = {
211
+ sourceHandle?: string
212
+ clientX?: number
213
+ clientY?: number
214
+ }
215
+
205
216
  export function useCanvasInteractions({
206
217
  viewId,
207
218
  canEdit,
@@ -237,12 +248,11 @@ export function useCanvasInteractions({
237
248
  openConnectorPanel: openConnectorPanel,
238
249
  closeConnectorPanel: closeConnectorPanel,
239
250
  selectedElement,
240
- selectedEdgeId,
251
+ selectedConnector,
241
252
  connectors,
242
253
  layers,
243
254
  setSelectedElement,
244
255
  setSelectedEdge,
245
- setSelectedEdgeId,
246
256
  setSelectedProxyConnectorDetails,
247
257
  openProxyConnectorPanel,
248
258
  closeProxyConnectorPanel,
@@ -288,6 +298,7 @@ export function useCanvasInteractions({
288
298
  const suppressNextPaneClickRef = useRef(false)
289
299
  const longPressCanvasRef = useRef<{ timer: ReturnType<typeof setTimeout>; clientX: number; clientY: number } | null>(null)
290
300
  const pendingConnectionSourceRef = useRef(pendingConnectionSource)
301
+ const pendingConnectionSourceHandleRef = useRef<string | null>(null)
291
302
  pendingConnectionSourceRef.current = pendingConnectionSource
292
303
  const clickConnectModeRef = useRef(clickConnectMode)
293
304
  clickConnectModeRef.current = clickConnectMode
@@ -323,6 +334,12 @@ export function useCanvasInteractions({
323
334
  isReconnectingRef.current = false
324
335
  }, [clearHandleReconnectListeners])
325
336
 
337
+ const finalizeConnectorCreate = useCallback(async (connector: Connector) => {
338
+ upsertConnectorGraphSnapshot(connector)
339
+ upsertConnector(connector)
340
+ await refreshElements()
341
+ }, [refreshElements, upsertConnector])
342
+
326
343
  // ── Ref-forwarded callbacks ────────────────────────────────────────────────
327
344
  const openConnectorPanelRef = useRef(openConnectorPanel)
328
345
  openConnectorPanelRef.current = openConnectorPanel
@@ -372,8 +389,10 @@ export function useCanvasInteractions({
372
389
  if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'add') return
373
390
  const { flowX, flowY } = addingElementAt
374
391
  const sourceId = pendingConnectionSourceRef.current
392
+ const pendingSourceHandle = pendingConnectionSourceHandleRef.current
375
393
  setAddingElementAt(null)
376
394
  setPendingConnectionSource(null)
395
+ pendingConnectionSourceHandleRef.current = null
377
396
  try {
378
397
  const obj = await api.elements.create({ name, kind: '' })
379
398
  await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
@@ -387,21 +406,22 @@ export function useCanvasInteractions({
387
406
  : { sourceHandle: 'right', targetHandle: 'left' }
388
407
  const newConnector = await api.workspace.connectors.create(viewId, {
389
408
  source_element_id: sourceId, target_element_id: obj.id,
390
- source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
409
+ source_handle: pendingSourceHandle ?? sourceHandle, target_handle: targetHandle, direction: 'forward',
391
410
  })
392
411
  const connector = connectorToConnector(newConnector)
393
- upsertConnectorGraphSnapshot(connector)
394
- upsertConnector(connector)
412
+ await finalizeConnectorCreate(connector)
395
413
  }
396
414
  } catch { /* intentionally empty */ }
397
- }, [canEdit, viewId, addingElementAt, refreshElements, rfNodesRef, upsertConnector, viewElementsRef])
415
+ }, [addingElementAt, canEdit, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
398
416
 
399
417
  const handleConfirmExistingElement = useCallback(async (obj: LibraryElement) => {
400
418
  if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'add') return
401
419
  const { flowX, flowY } = addingElementAt
402
420
  const sourceId = pendingConnectionSourceRef.current
421
+ const pendingSourceHandle = pendingConnectionSourceHandleRef.current
403
422
  setAddingElementAt(null)
404
423
  setPendingConnectionSource(null)
424
+ pendingConnectionSourceHandleRef.current = null
405
425
  try {
406
426
  if (!existingElementIds.has(obj.id)) {
407
427
  await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
@@ -419,21 +439,22 @@ export function useCanvasInteractions({
419
439
  : { sourceHandle: 'right', targetHandle: 'left' }
420
440
  const newConnector = await api.workspace.connectors.create(viewId, {
421
441
  source_element_id: sourceId, target_element_id: obj.id,
422
- source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
442
+ source_handle: pendingSourceHandle ?? sourceHandle, target_handle: targetHandle, direction: 'forward',
423
443
  })
424
444
  const connector = connectorToConnector(newConnector)
425
- upsertConnectorGraphSnapshot(connector)
426
- upsertConnector(connector)
445
+ await finalizeConnectorCreate(connector)
427
446
  }
428
447
  } catch { /* intentionally empty */ }
429
- }, [canEdit, viewId, addingElementAt, existingElementIds, refreshElements, rfNodesRef, upsertConnector, viewElementsRef])
448
+ }, [addingElementAt, canEdit, existingElementIds, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
430
449
 
431
450
  const handleConfirmConnectExistingElement = useCallback(async (obj: LibraryElement) => {
432
451
  if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'connect') return
433
452
  const { flowX, flowY } = addingElementAt
434
453
  const sourceId = pendingConnectionSourceRef.current
454
+ const pendingSourceHandle = pendingConnectionSourceHandleRef.current
435
455
  setAddingElementAt(null)
436
456
  setPendingConnectionSource(null)
457
+ pendingConnectionSourceHandleRef.current = null
437
458
  if (sourceId == null || sourceId === obj.id) return
438
459
  try {
439
460
  const sourceNode = rfNodesRef.current.find((n) => n.id === String(sourceId))
@@ -443,15 +464,14 @@ export function useCanvasInteractions({
443
464
  const newConnector = await api.workspace.connectors.create(viewId, {
444
465
  source_element_id: sourceId,
445
466
  target_element_id: obj.id,
446
- source_handle: sourceHandle,
467
+ source_handle: pendingSourceHandle ?? sourceHandle,
447
468
  target_handle: targetHandle,
448
469
  direction: 'forward',
449
470
  })
450
471
  const connector = connectorToConnector(newConnector)
451
- upsertConnectorGraphSnapshot(connector)
452
- upsertConnector(connector)
472
+ await finalizeConnectorCreate(connector)
453
473
  } catch { /* intentionally empty */ }
454
- }, [addingElementAt, canEdit, rfNodesRef, upsertConnector, viewId])
474
+ }, [addingElementAt, canEdit, finalizeConnectorCreate, rfNodesRef, viewId])
455
475
 
456
476
  // ── Zoom-in / zoom-out stable callbacks ───────────────────────────────────
457
477
  const stableOnZoomIn = useCallback(async (elementId: number) => {
@@ -541,9 +561,86 @@ export function useCanvasInteractions({
541
561
  removeElementPlacement(elementId)
542
562
  handleElementDeleted(elementId)
543
563
  setInteractionSourceId(null)
564
+ pendingConnectionSourceHandleRef.current = null
544
565
  } catch { /* intentionally empty */ }
545
566
  }, [canEdit, viewId, removeElementPlacement, handleElementDeleted])
546
567
 
568
+ const connectClickModeToHandle = useCallback(async (targetElementId: number, targetHandle: string) => {
569
+ if (!canEdit) return
570
+ const cid = viewIdRef.current
571
+ const sourceElementId = interactionSourceIdRef.current
572
+ if (cid === null || sourceElementId === null || sourceElementId === targetElementId) return
573
+
574
+ const sourceHandle = pendingConnectionSourceHandleRef.current ??
575
+ (clickConnectModeRef.current?.sourceHandle
576
+ ? getLogicalHandleId(clickConnectModeRef.current.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
577
+ : DEFAULT_SOURCE_HANDLE_SIDE)
578
+ const logicalTargetHandle = getLogicalHandleId(targetHandle, DEFAULT_TARGET_HANDLE_SIDE) ?? DEFAULT_TARGET_HANDLE_SIDE
579
+
580
+ setInteractionSourceId(null)
581
+ setPendingConnectionSource(null)
582
+ pendingConnectionSourceHandleRef.current = null
583
+ setClickConnectMode(null)
584
+ setClickConnectCursorPos(null)
585
+ setConnectGhostPos(null)
586
+
587
+ try {
588
+ const newConnector = await api.workspace.connectors.create(cid, {
589
+ source_element_id: sourceElementId,
590
+ target_element_id: targetElementId,
591
+ source_handle: sourceHandle,
592
+ target_handle: logicalTargetHandle,
593
+ direction: 'forward',
594
+ })
595
+ const connector = connectorToConnector(newConnector)
596
+ await finalizeConnectorCreate(connector)
597
+ } catch { /* intentionally empty */ }
598
+ }, [canEdit, finalizeConnectorCreate, interactionSourceIdRef, viewIdRef])
599
+
600
+ const stableOnInteractionStart = useCallback((elementId: number, options?: InteractionStartOptions) => {
601
+ if (!canEdit) return
602
+ const sourceHandle = options?.sourceHandle
603
+
604
+ if (sourceHandle) {
605
+ const cursorPos = options?.clientX !== undefined && options.clientY !== undefined
606
+ ? { x: options.clientX, y: options.clientY }
607
+ : lastMousePosRef.current
608
+ ? { x: lastMousePosRef.current.clientX, y: lastMousePosRef.current.clientY }
609
+ : null
610
+ if (!cursorPos) return
611
+
612
+ const activeSourceId = interactionSourceIdRef.current
613
+ if (activeSourceId !== null && activeSourceId !== elementId) {
614
+ void connectClickModeToHandle(elementId, sourceHandle)
615
+ return
616
+ }
617
+
618
+ pendingConnectionSourceHandleRef.current = getLogicalHandleId(sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
619
+ setInteractionSourceId(elementId)
620
+ setPendingConnectionSource(null)
621
+ setAddingElementAt(null)
622
+ setClickConnectMode({
623
+ sourceNodeId: String(elementId),
624
+ sourceHandle: ensureVisualHandleId(sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? sourceHandle,
625
+ })
626
+ setClickConnectCursorPos(cursorPos)
627
+ setConnectGhostPos(cursorPos)
628
+ return
629
+ }
630
+
631
+ const isCancelling = interactionSourceIdRef.current === elementId
632
+ const nextSourceId = isCancelling ? null : elementId
633
+
634
+ if (isCancelling) pendingConnectionSourceHandleRef.current = null
635
+ setInteractionSourceId(nextSourceId)
636
+
637
+ if (isCancelling) {
638
+ setClickConnectMode(null)
639
+ setClickConnectCursorPos(null)
640
+ setConnectGhostPos(null)
641
+ }
642
+ }, [canEdit, connectClickModeToHandle, interactionSourceIdRef])
643
+
547
644
  // ── Node/connector changes ─────────────────────────────────────────────────────
548
645
  const onNodesChange = useCallback((changes: NodeChange[]) => {
549
646
  if (!canEdit) {
@@ -614,10 +711,9 @@ export function useCanvasInteractions({
614
711
  direction: 'forward', style: 'bezier',
615
712
  })
616
713
  const connector = connectorToConnector(newConnector)
617
- upsertConnectorGraphSnapshot(connector)
618
- upsertConnector(connector)
714
+ await finalizeConnectorCreate(connector)
619
715
  } catch { /* intentionally empty */ }
620
- }, [canEdit, upsertConnector, viewId])
716
+ }, [canEdit, finalizeConnectorCreate, viewId])
621
717
 
622
718
  const onConnectStart = useCallback((_: React.MouseEvent | React.TouchEvent, { nodeId }: OnConnectStartParams) => {
623
719
  if (!canEdit || isReconnectingRef.current) return
@@ -677,15 +773,14 @@ export function useCanvasInteractions({
677
773
  source_handle: sourceHandle, target_handle: targetHandle, direction: 'forward',
678
774
  }).then((connector) => {
679
775
  const next = connectorToConnector(connector)
680
- upsertConnectorGraphSnapshot(next)
681
- upsertConnector(next)
776
+ void finalizeConnectorCreate(next)
682
777
  }).catch(() => { /* intentionally empty */ })
683
778
  } else {
684
779
  setPendingConnectionSource(sourceElementId)
685
780
  suppressNextPaneClickRef.current = true
686
781
  showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
687
782
  }
688
- }, [canEdit, upsertConnector, showAddingElementAt, rfNodesRef, viewIdRef])
783
+ }, [canEdit, finalizeConnectorCreate, showAddingElementAt, rfNodesRef, viewIdRef])
689
784
 
690
785
  // ── Reconnect ──────────────────────────────────────────────────────────────
691
786
  const performReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
@@ -695,7 +790,6 @@ export function useCanvasInteractions({
695
790
  const targetId = parseNumericId(newConnection.target)
696
791
  if (edgeId === null || sourceId === null || targetId === null) return
697
792
  setRfEdges((eds) => reconnectEdge(oldConnector, newConnection, eds))
698
- setSelectedEdgeId(null)
699
793
  try {
700
794
  const existingData = oldConnector.data as Connector
701
795
  const sourceHandle = getLogicalHandleId(newConnection.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE)
@@ -713,7 +807,7 @@ export function useCanvasInteractions({
713
807
  upsertConnectorGraphSnapshot(connector)
714
808
  replaceConnector(connector)
715
809
  } catch { /* intentionally empty */ }
716
- }, [canEdit, replaceConnector, viewId, setRfEdges, setSelectedEdgeId])
810
+ }, [canEdit, replaceConnector, viewId, setRfEdges])
717
811
  const onReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
718
812
  await performReconnect(oldConnector, newConnection)
719
813
  }, [performReconnect])
@@ -904,7 +998,6 @@ export function useCanvasInteractions({
904
998
  setSelectedElement(null)
905
999
  closeElementPanel()
906
1000
  setSelectedEdge(null)
907
- setSelectedEdgeId(null)
908
1001
  closeConnectorPanel()
909
1002
  setSelectedProxyConnectorDetails((rfConnector.data as { details?: import('../../../crossBranch/types').ProxyConnectorDetails }).details ?? null)
910
1003
  openProxyConnectorPanel()
@@ -912,16 +1005,14 @@ export function useCanvasInteractions({
912
1005
  }
913
1006
  const clickedId = parseNumericId(rfConnector.id)
914
1007
  if (clickedId === null) return
915
- if (selectedEdgeId === clickedId) {
916
- const connector = connectors.find((e) => e.id === clickedId)
917
- if (connector) { setSelectedEdge(connector); openConnectorPanelRef.current() }
918
- setSelectedEdgeId(null)
919
- } else {
920
- setSelectedElement(null)
921
- closeElementPanel()
922
- setSelectedEdgeId(clickedId)
923
- }
924
- }, [closeConnectorPanel, closeElementPanel, connectors, openProxyConnectorPanel, selectedEdgeId, setSelectedEdge, setSelectedEdgeId, setSelectedElement, setSelectedProxyConnectorDetails])
1008
+ const connector = connectors.find((e) => e.id === clickedId)
1009
+ if (!connector) return
1010
+ setSelectedElement(null)
1011
+ closeElementPanel()
1012
+ setSelectedProxyConnectorDetails(null)
1013
+ setSelectedEdge(connector)
1014
+ openConnectorPanelRef.current()
1015
+ }, [closeElementPanel, connectors, openProxyConnectorPanel, setSelectedEdge, setSelectedElement, setSelectedProxyConnectorDetails])
925
1016
 
926
1017
  // ── Pane interactions ─────────────────────────────────────────────────────
927
1018
  const onPaneClick = useCallback((e: React.MouseEvent) => {
@@ -930,7 +1021,6 @@ export function useCanvasInteractions({
930
1021
  setReconnectPicking(null)
931
1022
  setSelectedElement(null)
932
1023
  setSelectedEdge(null)
933
- setSelectedEdgeId(null)
934
1024
  setSelectedProxyConnectorDetails(null)
935
1025
  setConnectorLongPressMenu(null)
936
1026
  setCanvasMenu(null)
@@ -953,14 +1043,18 @@ export function useCanvasInteractions({
953
1043
  } else {
954
1044
  setInteractionSourceId(null)
955
1045
  setPendingConnectionSource(sourceId)
1046
+ pendingConnectionSourceHandleRef.current = clickConnectModeRef.current?.sourceHandle
1047
+ ? getLogicalHandleId(clickConnectModeRef.current.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
1048
+ : pendingConnectionSourceHandleRef.current
956
1049
  showAddingElementAt(e.clientX, e.clientY, true, 'connect', e.shiftKey)
957
1050
  }
958
1051
  return
959
1052
  }
960
1053
  setInteractionSourceId(null)
961
1054
  setPendingConnectionSource(null)
1055
+ pendingConnectionSourceHandleRef.current = null
962
1056
  setAddingElementAt(null)
963
- }, [stableOnConnectTo, showAddingElementAt, closeElementPanel, closeConnectorPanel, closeProxyConnectorPanel, rfNodesRef, interactionSourceIdRef, setSelectedElement, setSelectedEdge, setSelectedEdgeId, setSelectedProxyConnectorDetails])
1057
+ }, [stableOnConnectTo, showAddingElementAt, closeElementPanel, closeConnectorPanel, closeProxyConnectorPanel, rfNodesRef, interactionSourceIdRef, setSelectedElement, setSelectedEdge, setSelectedProxyConnectorDetails])
964
1058
 
965
1059
  const onPaneContextMenu = useCallback((e: React.MouseEvent) => {
966
1060
  e.preventDefault()
@@ -1124,10 +1218,12 @@ export function useCanvasInteractions({
1124
1218
  setSelectedElement(null)
1125
1219
  closeElementPanel()
1126
1220
  }
1127
- } else if (selectedEdgeId) {
1128
- api.workspace.connectors.delete('', selectedEdgeId).then(() => {
1129
- handleConnectorDeleted(selectedEdgeId)
1130
- setSelectedEdgeId(null)
1221
+ } else {
1222
+ const connectorId = getConnectorDeletionTarget(selectedConnector)
1223
+ if (connectorId === null) return
1224
+ api.workspace.connectors.delete('', connectorId).then(() => {
1225
+ handleConnectorDeleted(connectorId)
1226
+ setSelectedEdge(null)
1131
1227
  closeConnectorPanel()
1132
1228
  }).catch(() => { /* intentionally empty */ })
1133
1229
  }
@@ -1236,7 +1332,7 @@ export function useCanvasInteractions({
1236
1332
  }
1237
1333
  window.addEventListener('keydown', handler)
1238
1334
  return () => window.removeEventListener('keydown', handler)
1239
- }, [canEdit, refreshGrid, selectedElement, selectedEdgeId, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted, closeElementPanel, closeConnectorPanel, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, rfNodesRef, viewElementsRef, setLinksMap, showAddingElementAt, setSelectedElement, setSelectedEdge, setSelectedEdgeId, containerRef, linksMapRef])
1335
+ }, [canEdit, refreshGrid, selectedElement, selectedConnector, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted, closeElementPanel, closeConnectorPanel, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, rfNodesRef, viewElementsRef, setLinksMap, showAddingElementAt, setSelectedElement, setSelectedEdge, containerRef, linksMapRef])
1240
1336
 
1241
1337
  // ── DnD handlers ──────────────────────────────────────────────────────────
1242
1338
  const onDragOver = useCallback((e: React.DragEvent) => {
@@ -1351,6 +1447,7 @@ export function useCanvasInteractions({
1351
1447
  stableOnHoverZoom,
1352
1448
  stableOnRemoveElement,
1353
1449
  stableOnConnectTo,
1450
+ stableOnInteractionStart,
1354
1451
  stableOnStartHandleReconnect,
1355
1452
  showAddingElementAt,
1356
1453
  // RF event handlers
@@ -23,7 +23,7 @@ interface ViewDataOptions {
23
23
  viewId: number | null
24
24
  interactionSourceId: number | null
25
25
  clickConnectMode: { sourceNodeId: string; sourceHandle: string; targetHandle?: string } | null
26
- selectedEdgeId: number | null
26
+ selectedConnector: Connector | null
27
27
  activeTags: string[]
28
28
  hiddenLayerTags: string[]
29
29
  hoveredLayerTags: string[] | null
@@ -35,7 +35,7 @@ interface ViewDataOptions {
35
35
  stableOnNavigateToView: (id: number) => void
36
36
  stableOnSelect: (obj: PlacedElement) => void
37
37
  stableOnOpenCodePreview: (elementId: number) => void
38
- stableOnInteractionStart: (elementId: number) => void
38
+ stableOnInteractionStart: (elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => void
39
39
  stableOnConnectTo: (targetElementId: number) => Promise<void>
40
40
  stableOnStartHandleReconnect: (args: { edgeId: string; endpoint: 'source' | 'target'; handleId: string; clientX: number; clientY: number }) => void
41
41
  stableOnRemoveElement: (elementId: number) => Promise<void>
@@ -152,7 +152,7 @@ export function useViewData({
152
152
  viewId,
153
153
  interactionSourceId,
154
154
  clickConnectMode,
155
- selectedEdgeId,
155
+ selectedConnector,
156
156
  activeTags,
157
157
  hiddenLayerTags,
158
158
  hoveredLayerTags,
@@ -170,6 +170,7 @@ export function useViewData({
170
170
  stableOnHoverZoom,
171
171
  hoveredZoomRef,
172
172
  }: ViewDataOptions) {
173
+ const selectedEdgeId = selectedConnector?.id ?? null
173
174
  const queryClient = useQueryClient()
174
175
  const view = useStore((state) => state.view)
175
176
  const setView = useStore((state) => state.setView)
@@ -85,7 +85,7 @@ import { useCrossBranchContextSettings } from '../../crossBranch/settings'
85
85
  import { removeConnectorGraphSnapshot, upsertConnectorGraphSnapshot, useWorkspaceGraphSnapshot } from '../../crossBranch/store'
86
86
  import type { ProxyConnectorDetails } from '../../crossBranch/types'
87
87
  import { useDemoRevealViewport, type ViewEditorDemoOptions } from '../../demo/viewEditor'
88
- import { useStore } from '../../store/useStore'
88
+ import { buildElementLibraryItems, useStore } from '../../store/useStore'
89
89
 
90
90
  const nodeTypes = {
91
91
  elementNode: ElementNode,
@@ -201,10 +201,8 @@ function ViewEditorInner({
201
201
  const closeImportModalRef = useRef(importModal.onClose)
202
202
  closeImportModalRef.current = importModal.onClose
203
203
 
204
-
205
204
  const [selectedElement, setSelectedElement] = useState<WorkspaceElement | null>(null)
206
205
  const [selectedEdge, setSelectedEdge] = useState<Connector | null>(null)
207
- const [selectedEdgeId, setSelectedEdgeId] = useState<number | null>(null)
208
206
  const [selectedProxyConnectorDetails, setSelectedProxyConnectorDetails] = useState<ProxyConnectorDetails | null>(null)
209
207
  const [previewElement, setPreviewElement] = useState<PlacedElement | null>(null)
210
208
  const [libraryOpen, setLibraryOpen] = useState(() => {
@@ -228,6 +226,7 @@ function ViewEditorInner({
228
226
  const setStoreSnapToGrid = useStore((state) => state.setSnapToGrid)
229
227
  const upsertStoreConnector = useStore((state) => state.upsertConnector)
230
228
  const removeStoreConnector = useStore((state) => state.removeConnector)
229
+ const refreshElementsRef = useRef<() => Promise<void>>(async () => {})
231
230
  const setSnapToGrid = useCallback((snap: boolean) => {
232
231
  setStoreSnapToGrid(snap)
233
232
  if (typeof window !== 'undefined') localStorage.setItem('diag:snapToGrid', String(snap))
@@ -348,6 +347,7 @@ function ViewEditorInner({
348
347
 
349
348
  // stableOnConnectTo is wired after canvasInteractions is declared
350
349
  const stableOnConnectToRef = useRef<(targetElementId: number) => Promise<void>>(async () => { })
350
+ const stableOnInteractionStartRef = useRef<(elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => void>(() => { })
351
351
  const stableOnStartHandleReconnectRef = useRef<(args: { edgeId: string; endpoint: 'source' | 'target'; handleId: string; clientX: number; clientY: number }) => void>(() => { })
352
352
 
353
353
  // ── Drawing engine ────────────────────────────────────────────────────────
@@ -370,7 +370,7 @@ function ViewEditorInner({
370
370
  viewId,
371
371
  interactionSourceId: interactionSourceIdRef.current,
372
372
  clickConnectMode: null, // wired after canvasInteractions
373
- selectedEdgeId,
373
+ selectedConnector: selectedEdge,
374
374
  activeTags,
375
375
  hiddenLayerTags,
376
376
  hoveredLayerTags,
@@ -381,7 +381,6 @@ function ViewEditorInner({
381
381
  stableOnNavigateToView: useCallback((id: number) => { stableOnNavigateToViewRef.current(id) }, []),
382
382
  stableOnSelect: useCallback((obj: PlacedElement) => {
383
383
  setSelectedEdge(null)
384
- setSelectedEdgeId(null)
385
384
  setSelectedProxyConnectorDetails(null)
386
385
  closeProxyConnectorPanelRef.current()
387
386
  closeConnectorPanelRef.current()
@@ -401,10 +400,9 @@ function ViewEditorInner({
401
400
  openCodePreviewRef.current()
402
401
  }
403
402
  }, []),
404
- stableOnInteractionStart: useCallback((elementId: number) => {
405
- if (!canEdit) return
406
- interactionSourceIdRef.current = interactionSourceIdRef.current === elementId ? null : elementId
407
- }, [canEdit]),
403
+ stableOnInteractionStart: useCallback((elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => {
404
+ stableOnInteractionStartRef.current(elementId, options)
405
+ }, []),
408
406
  stableOnConnectTo: useCallback(async (targetElementId: number) => {
409
407
  await stableOnConnectToRef.current(targetElementId)
410
408
  }, []),
@@ -432,6 +430,7 @@ function ViewEditorInner({
432
430
  handleElementDeleted, handleElementPermanentlyDeleted, handleElementSaved,
433
431
  setAllElements: _setAllElements,
434
432
  } = data
433
+ refreshElementsRef.current = refreshElements
435
434
 
436
435
  const tagCounts = useMemo(() => {
437
436
  const counts: Record<string, number> = {}
@@ -499,27 +498,7 @@ function ViewEditorInner({
499
498
  return unsub
500
499
  }, [fitView, viewId, refreshElements])
501
500
 
502
- const existingElements = useMemo(() => {
503
- return viewElements.map(obj => ({
504
- id: obj.element_id,
505
- name: obj.name,
506
- kind: obj.kind,
507
- description: obj.description,
508
- technology: obj.technology,
509
- url: obj.url,
510
- logo_url: obj.logo_url,
511
- technology_connectors: obj.technology_connectors,
512
- tags: obj.tags,
513
- repo: obj.repo,
514
- branch: obj.branch,
515
- file_path: obj.file_path,
516
- language: obj.language,
517
- created_at: '',
518
- updated_at: '',
519
- has_view: false,
520
- view_label: null,
521
- } as WorkspaceElement))
522
- }, [viewElements])
501
+ const existingElements = useMemo(() => buildElementLibraryItems(allElements, viewElements), [allElements, viewElements])
523
502
 
524
503
  const availableTags = useMemo(() => {
525
504
  const tags = new Set<string>()
@@ -551,7 +530,6 @@ function ViewEditorInner({
551
530
  const match = viewElements.find((element) => element.element_id === requestedElementId)
552
531
  if (!match) return
553
532
  setSelectedEdge(null)
554
- setSelectedEdgeId(null)
555
533
  setSelectedProxyConnectorDetails(null)
556
534
  closeConnectorPanelRef.current()
557
535
  closeProxyConnectorPanelRef.current()
@@ -654,10 +632,10 @@ function ViewEditorInner({
654
632
  closeElementPanel: useCallback(() => closeElementPanelRef.current(), []),
655
633
  openConnectorPanel: useCallback(() => openConnectorPanelRef.current(), []),
656
634
  closeConnectorPanel: useCallback(() => closeConnectorPanelRef.current(), []),
657
- selectedElement, selectedEdgeId, connectors,
635
+ selectedElement, selectedConnector: selectedEdge, connectors,
658
636
  layers,
659
637
  setSelectedElement,
660
- setSelectedEdge, setSelectedEdgeId,
638
+ setSelectedEdge,
661
639
  setSelectedProxyConnectorDetails,
662
640
  openProxyConnectorPanel: useCallback(() => openProxyConnectorPanelRef.current(), []),
663
641
  closeProxyConnectorPanel: useCallback(() => closeProxyConnectorPanelRef.current(), []),
@@ -665,6 +643,7 @@ function ViewEditorInner({
665
643
  handleConnectorDeleted: useCallback((edgeId: number) => {
666
644
  if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
667
645
  removeStoreConnector(edgeId)
646
+ void refreshElementsRef.current()
668
647
  }, [removeStoreConnector, viewId]),
669
648
  handleUpdateTags,
670
649
  drawingCanvasRef,
@@ -678,8 +657,9 @@ function ViewEditorInner({
678
657
  stableOnNavigateToViewRef.current = canvas.stableOnNavigateToView
679
658
  stableOnRemoveElementRef.current = canvas.stableOnRemoveElement
680
659
  stableOnConnectToRef.current = canvas.stableOnConnectTo
660
+ stableOnInteractionStartRef.current = canvas.stableOnInteractionStart
681
661
  stableOnStartHandleReconnectRef.current = canvas.stableOnStartHandleReconnect
682
- }, [canvas.stableOnZoomIn, canvas.stableOnZoomOut, canvas.stableOnNavigateToView, canvas.stableOnRemoveElement, canvas.stableOnConnectTo, canvas.stableOnStartHandleReconnect])
662
+ }, [canvas.stableOnZoomIn, canvas.stableOnZoomOut, canvas.stableOnNavigateToView, canvas.stableOnRemoveElement, canvas.stableOnConnectTo, canvas.stableOnInteractionStart, canvas.stableOnStartHandleReconnect])
683
663
  const viewName = view?.name ?? null
684
664
 
685
665
  const [expandedAncestorGroups, setExpandedAncestorGroups] = useState<Set<string>>(new Set())
@@ -702,7 +682,6 @@ function ViewEditorInner({
702
682
  onSelectProxyDetails: useCallback((details: ProxyConnectorDetails) => {
703
683
  setSelectedElement(null)
704
684
  setSelectedEdge(null)
705
- setSelectedEdgeId(null)
706
685
  closeConnectorPanelRef.current()
707
686
  closeElementPanelRef.current()
708
687
  setSelectedProxyConnectorDetails(details)
@@ -915,7 +894,7 @@ function ViewEditorInner({
915
894
  return () => observer.disconnect()
916
895
  }, [maybeFitView])
917
896
 
918
- useEffect(() => { setRfNodes([]); setRfEdges([]); needsFitView.current = true }, [viewId, setRfEdges, setRfNodes])
897
+ useEffect(() => { needsFitView.current = true }, [viewId])
919
898
 
920
899
  // ── Dynamic viewport bounds ────────────────────────────────────────────────
921
900
  useEffect(() => {
@@ -1023,10 +1002,15 @@ function ViewEditorInner({
1023
1002
  upsertConnectorGraphSnapshot(updated)
1024
1003
  upsertStoreConnector(updated)
1025
1004
  }, [upsertStoreConnector])
1026
- const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
1005
+ const handleConnectorDeleted = useCallback((edgeId: number) => {
1027
1006
  if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
1028
1007
  removeStoreConnector(edgeId)
1029
- }, [removeStoreConnector, viewId])
1008
+ void refreshElements()
1009
+ }, [refreshElements, removeStoreConnector, viewId])
1010
+ const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
1011
+ handleConnectorDeleted(edgeId)
1012
+ setSelectedEdge(null)
1013
+ }, [handleConnectorDeleted, setSelectedEdge])
1030
1014
  const handleViewSave = useCallback((updated: ViewTreeNode) => setView(updated), [setView])
1031
1015
 
1032
1016
  // ── Library helpers ────────────────────────────────────────────────────────
@@ -1419,8 +1403,8 @@ function ViewEditorInner({
1419
1403
  onSave={handleConnectorSave} autoSave
1420
1404
  onDelete={handleConnectorDeleteInPanel}
1421
1405
  hasBackdrop={isMobileLayout}
1422
- connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
1423
- />
1406
+ connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
1407
+ />
1424
1408
  <ProxyConnectorPanel
1425
1409
  isOpen={proxyConnectorPanel.isOpen}
1426
1410
  onClose={proxyConnectorPanel.onClose}