@tldiagram/core-ui 1.92.0 → 1.94.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.
Files changed (37) hide show
  1. package/dist/api/client.d.ts +13 -1
  2. package/dist/components/ElementNode.d.ts +14 -1
  3. package/dist/components/ZUI/ZUICanvas.d.ts +1 -0
  4. package/dist/config/runtime-vscode.d.ts +1 -0
  5. package/dist/config/runtime.d.ts +1 -0
  6. package/dist/index.js +10875 -9550
  7. package/dist/pages/InfiniteZoom.d.ts +5 -2
  8. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +10 -3
  9. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.test.d.ts +1 -0
  10. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +27 -24
  11. package/dist/pages/ViewsGrid.d.ts +9 -1
  12. package/dist/shims/empty-node-module.d.ts +2 -0
  13. package/dist/store/useStore.d.ts +80 -0
  14. package/dist/store/useStore.test.d.ts +1 -0
  15. package/package.json +10 -7
  16. package/src/api/client.ts +39 -1
  17. package/src/components/ElementNode.tsx +21 -59
  18. package/src/components/ElementPanel.tsx +2 -3
  19. package/src/components/LayoutSection.tsx +95 -104
  20. package/src/components/ViewGridNode.tsx +1 -4
  21. package/src/components/ZUI/ZUICanvas.tsx +138 -1
  22. package/src/components/ZUI/renderer.ts +166 -66
  23. package/src/components/ZUI/useZUIInteraction.ts +235 -81
  24. package/src/config/runtime-vscode.ts +6 -0
  25. package/src/config/runtime.ts +4 -0
  26. package/src/main.tsx +26 -14
  27. package/src/pages/InfiniteZoom.tsx +14 -5
  28. package/src/pages/ViewEditor/context.tsx +14 -3
  29. package/src/pages/ViewEditor/hooks/useCanvasInteractions.test.ts +30 -0
  30. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +294 -146
  31. package/src/pages/ViewEditor/hooks/useViewData.ts +459 -256
  32. package/src/pages/ViewEditor/index.tsx +67 -70
  33. package/src/pages/Views.tsx +552 -83
  34. package/src/pages/ViewsGrid.tsx +26 -337
  35. package/src/shims/empty-node-module.ts +1 -0
  36. package/src/store/useStore.test.ts +285 -0
  37. package/src/store/useStore.ts +327 -0
@@ -11,7 +11,6 @@ import ReactFlow, {
11
11
  PanOnScrollMode,
12
12
  ReactFlowProvider,
13
13
  useReactFlow,
14
- applyNodeChanges,
15
14
  } from 'reactflow'
16
15
  import type { Edge as RFEdge, EdgeMarker as RFEdgeMarker, Node as RFNode, NodeChange } from 'reactflow'
17
16
  import 'reactflow/dist/style.css'
@@ -74,8 +73,8 @@ import type { ExtensionToWebviewMessage } from '../../types/vscode-messages'
74
73
  import { ViewEditorContext } from './context'
75
74
  import { useViewData } from './hooks/useViewData'
76
75
  import { useDrawingEngine } from './hooks/useDrawingEngine'
77
- import { useCanvasInteractions } from './hooks/useCanvasInteractions'
78
- import { sanitizeExportFilename, triggerDownload } from './utils'
76
+ import { applyNodeChangesWithStructuralSharing, useCanvasInteractions } from './hooks/useCanvasInteractions'
77
+ import { connectorToConnector, findClosestHandles, sanitizeExportFilename, triggerDownload } from './utils'
79
78
  import { pickUnusedColor } from '../../components/ViewExplorer/utils'
80
79
 
81
80
  import { EmptyCanvasState } from './components/EmptyCanvasState'
@@ -86,6 +85,7 @@ import { useCrossBranchContextSettings } from '../../crossBranch/settings'
86
85
  import { removeConnectorGraphSnapshot, upsertConnectorGraphSnapshot, useWorkspaceGraphSnapshot } from '../../crossBranch/store'
87
86
  import type { ProxyConnectorDetails } from '../../crossBranch/types'
88
87
  import { useDemoRevealViewport, type ViewEditorDemoOptions } from '../../demo/viewEditor'
88
+ import { buildElementLibraryItems, useStore } from '../../store/useStore'
89
89
 
90
90
  const nodeTypes = {
91
91
  elementNode: ElementNode,
@@ -99,6 +99,7 @@ const VIEW_EDITOR_EMPTY_EXTENT_RATIO = 0.75
99
99
  const VIEW_EDITOR_PAN_MARGIN_RATIO = 0.25
100
100
  const VIEW_EDITOR_PAN_MARGIN_MIN = 180
101
101
  const VIEW_EDITOR_PAN_MARGIN_MAX = 720
102
+ const SNAP_GRID: [number, number] = [30, 30]
102
103
 
103
104
  function alphaColor(color: string, opacity: number): string {
104
105
  if (opacity >= 1) return color
@@ -200,10 +201,8 @@ function ViewEditorInner({
200
201
  const closeImportModalRef = useRef(importModal.onClose)
201
202
  closeImportModalRef.current = importModal.onClose
202
203
 
203
-
204
204
  const [selectedElement, setSelectedElement] = useState<WorkspaceElement | null>(null)
205
205
  const [selectedEdge, setSelectedEdge] = useState<Connector | null>(null)
206
- const [selectedEdgeId, setSelectedEdgeId] = useState<number | null>(null)
207
206
  const [selectedProxyConnectorDetails, setSelectedProxyConnectorDetails] = useState<ProxyConnectorDetails | null>(null)
208
207
  const [previewElement, setPreviewElement] = useState<PlacedElement | null>(null)
209
208
  const [libraryOpen, setLibraryOpen] = useState(() => {
@@ -222,11 +221,34 @@ function ViewEditorInner({
222
221
  const [extrasOpen, setExtrasOpen] = useState(false)
223
222
  const [isImporting, setIsImporting] = useState(false)
224
223
  const [isExporting, setIsExporting] = useState(false)
225
- const [snapToGrid, setSnapToGrid] = useState(() => {
226
- if (typeof window === 'undefined') return false
227
- const stored = localStorage.getItem('diag:snapToGrid')
228
- return stored === 'true'
229
- })
224
+ const setViewEditorUi = useStore((state) => state.setViewEditorUi)
225
+ const snapToGrid = useStore((state) => state.snapToGrid)
226
+ const setStoreSnapToGrid = useStore((state) => state.setSnapToGrid)
227
+ const upsertStoreConnector = useStore((state) => state.upsertConnector)
228
+ const removeStoreConnector = useStore((state) => state.removeConnector)
229
+ const refreshElementsRef = useRef<() => Promise<void>>(async () => {})
230
+ const setSnapToGrid = useCallback((snap: boolean) => {
231
+ setStoreSnapToGrid(snap)
232
+ if (typeof window !== 'undefined') localStorage.setItem('diag:snapToGrid', String(snap))
233
+ }, [setStoreSnapToGrid])
234
+
235
+ useEffect(() => {
236
+ if (typeof window === 'undefined') return
237
+ setStoreSnapToGrid(localStorage.getItem('diag:snapToGrid') === 'true')
238
+ }, [setStoreSnapToGrid])
239
+
240
+ useEffect(() => {
241
+ setViewEditorUi({
242
+ viewId,
243
+ canEdit,
244
+ isOwner,
245
+ isFreePlan,
246
+ snapToGrid,
247
+ selectedElement,
248
+ selectedConnector: selectedEdge,
249
+ })
250
+ }, [canEdit, isFreePlan, isOwner, selectedEdge, selectedElement, setViewEditorUi, snapToGrid, viewId])
251
+
230
252
  useEffect(() => { localStorage.setItem('diag:snapToGrid', String(snapToGrid)) }, [snapToGrid])
231
253
  const [, setHoveredZoom] = useState<{ elementId: number | null; type: 'in' | 'out' | null } | null>(null)
232
254
  const hoveredZoomRef = useRef<{ elementId: number | null; type: 'in' | 'out' | null } | null>(null)
@@ -325,6 +347,7 @@ function ViewEditorInner({
325
347
 
326
348
  // stableOnConnectTo is wired after canvasInteractions is declared
327
349
  const stableOnConnectToRef = useRef<(targetElementId: number) => Promise<void>>(async () => { })
350
+ const stableOnInteractionStartRef = useRef<(elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => void>(() => { })
328
351
  const stableOnStartHandleReconnectRef = useRef<(args: { edgeId: string; endpoint: 'source' | 'target'; handleId: string; clientX: number; clientY: number }) => void>(() => { })
329
352
 
330
353
  // ── Drawing engine ────────────────────────────────────────────────────────
@@ -347,7 +370,7 @@ function ViewEditorInner({
347
370
  viewId,
348
371
  interactionSourceId: interactionSourceIdRef.current,
349
372
  clickConnectMode: null, // wired after canvasInteractions
350
- selectedEdgeId,
373
+ selectedConnector: selectedEdge,
351
374
  activeTags,
352
375
  hiddenLayerTags,
353
376
  hoveredLayerTags,
@@ -358,7 +381,6 @@ function ViewEditorInner({
358
381
  stableOnNavigateToView: useCallback((id: number) => { stableOnNavigateToViewRef.current(id) }, []),
359
382
  stableOnSelect: useCallback((obj: PlacedElement) => {
360
383
  setSelectedEdge(null)
361
- setSelectedEdgeId(null)
362
384
  setSelectedProxyConnectorDetails(null)
363
385
  closeProxyConnectorPanelRef.current()
364
386
  closeConnectorPanelRef.current()
@@ -378,10 +400,9 @@ function ViewEditorInner({
378
400
  openCodePreviewRef.current()
379
401
  }
380
402
  }, []),
381
- stableOnInteractionStart: useCallback((elementId: number) => {
382
- if (!canEdit) return
383
- interactionSourceIdRef.current = interactionSourceIdRef.current === elementId ? null : elementId
384
- }, [canEdit]),
403
+ stableOnInteractionStart: useCallback((elementId: number, options?: { sourceHandle?: string; clientX?: number; clientY?: number }) => {
404
+ stableOnInteractionStartRef.current(elementId, options)
405
+ }, []),
385
406
  stableOnConnectTo: useCallback(async (targetElementId: number) => {
386
407
  await stableOnConnectToRef.current(targetElementId)
387
408
  }, []),
@@ -409,6 +430,7 @@ function ViewEditorInner({
409
430
  handleElementDeleted, handleElementPermanentlyDeleted, handleElementSaved,
410
431
  setAllElements: _setAllElements,
411
432
  } = data
433
+ refreshElementsRef.current = refreshElements
412
434
 
413
435
  const tagCounts = useMemo(() => {
414
436
  const counts: Record<string, number> = {}
@@ -476,27 +498,7 @@ function ViewEditorInner({
476
498
  return unsub
477
499
  }, [fitView, viewId, refreshElements])
478
500
 
479
- const existingElements = useMemo(() => {
480
- return viewElements.map(obj => ({
481
- id: obj.element_id,
482
- name: obj.name,
483
- kind: obj.kind,
484
- description: obj.description,
485
- technology: obj.technology,
486
- url: obj.url,
487
- logo_url: obj.logo_url,
488
- technology_connectors: obj.technology_connectors,
489
- tags: obj.tags,
490
- repo: obj.repo,
491
- branch: obj.branch,
492
- file_path: obj.file_path,
493
- language: obj.language,
494
- created_at: '',
495
- updated_at: '',
496
- has_view: false,
497
- view_label: null,
498
- } as WorkspaceElement))
499
- }, [viewElements])
501
+ const existingElements = useMemo(() => buildElementLibraryItems(allElements, viewElements), [allElements, viewElements])
500
502
 
501
503
  const availableTags = useMemo(() => {
502
504
  const tags = new Set<string>()
@@ -528,7 +530,6 @@ function ViewEditorInner({
528
530
  const match = viewElements.find((element) => element.element_id === requestedElementId)
529
531
  if (!match) return
530
532
  setSelectedEdge(null)
531
- setSelectedEdgeId(null)
532
533
  setSelectedProxyConnectorDetails(null)
533
534
  closeConnectorPanelRef.current()
534
535
  closeProxyConnectorPanelRef.current()
@@ -613,7 +614,7 @@ function ViewEditorInner({
613
614
  const targetNode = rfNodesRef.current.find((n) => n.id === String(targetElementId))
614
615
  let finalSourceHandle = 'right'; let finalTargetHandle = 'left'
615
616
  if (sourceNode && targetNode) {
616
- const h = (await import('./utils')).findClosestHandles(sourceNode, targetNode)
617
+ const h = findClosestHandles(sourceNode, targetNode)
617
618
  finalSourceHandle = h.sourceHandle; finalTargetHandle = h.targetHandle
618
619
  }
619
620
  try {
@@ -621,9 +622,9 @@ function ViewEditorInner({
621
622
  source_element_id: sourceId, target_element_id: targetElementId,
622
623
  source_handle: finalSourceHandle, target_handle: finalTargetHandle, direction: 'forward',
623
624
  })
624
- const connector = (await import('./utils')).connectorToConnector(newConnector)
625
+ const connector = connectorToConnector(newConnector)
625
626
  upsertConnectorGraphSnapshot(connector)
626
- setConnectors((prev) => [...prev, connector])
627
+ upsertStoreConnector(connector)
627
628
  } catch { /* intentionally empty */ }
628
629
  },
629
630
  existingElementIds, linksMapRef, parentLinksMapRef,
@@ -631,33 +632,22 @@ function ViewEditorInner({
631
632
  closeElementPanel: useCallback(() => closeElementPanelRef.current(), []),
632
633
  openConnectorPanel: useCallback(() => openConnectorPanelRef.current(), []),
633
634
  closeConnectorPanel: useCallback(() => closeConnectorPanelRef.current(), []),
634
- selectedElement, selectedEdgeId, connectors,
635
+ selectedElement, selectedConnector: selectedEdge, connectors,
635
636
  layers,
636
637
  setSelectedElement,
637
- setSelectedEdge, setSelectedEdgeId,
638
+ setSelectedEdge,
638
639
  setSelectedProxyConnectorDetails,
639
640
  openProxyConnectorPanel: useCallback(() => openProxyConnectorPanelRef.current(), []),
640
641
  closeProxyConnectorPanel: useCallback(() => closeProxyConnectorPanelRef.current(), []),
641
642
  handleElementDeleted, handleElementPermanentlyDeleted,
642
643
  handleConnectorDeleted: useCallback((edgeId: number) => {
643
644
  if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
644
- setConnectors((prev) => prev.filter((connector) => connector.id !== edgeId))
645
- }, [setConnectors, viewId]),
645
+ removeStoreConnector(edgeId)
646
+ void refreshElementsRef.current()
647
+ }, [removeStoreConnector, viewId]),
646
648
  handleUpdateTags,
647
649
  drawingCanvasRef,
648
650
  snapToGrid,
649
- onMoveStateChange: useCallback((moving: boolean) => {
650
- setLiveContextNodes((nds) => {
651
- let changed = false
652
- const nextNodes = nds.map((node) => {
653
- const currentMoving = Boolean((node.data as { isCanvasMoving?: boolean }).isCanvasMoving)
654
- if (currentMoving === moving) return node
655
- changed = true
656
- return { ...node, data: { ...node.data, isCanvasMoving: moving } }
657
- })
658
- return changed ? nextNodes : nds
659
- })
660
- }, []),
661
651
  })
662
652
 
663
653
  // Wire stable placeholders to the real implementations from canvas hook
@@ -667,8 +657,9 @@ function ViewEditorInner({
667
657
  stableOnNavigateToViewRef.current = canvas.stableOnNavigateToView
668
658
  stableOnRemoveElementRef.current = canvas.stableOnRemoveElement
669
659
  stableOnConnectToRef.current = canvas.stableOnConnectTo
660
+ stableOnInteractionStartRef.current = canvas.stableOnInteractionStart
670
661
  stableOnStartHandleReconnectRef.current = canvas.stableOnStartHandleReconnect
671
- }, [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])
672
663
  const viewName = view?.name ?? null
673
664
 
674
665
  const [expandedAncestorGroups, setExpandedAncestorGroups] = useState<Set<string>>(new Set())
@@ -691,7 +682,6 @@ function ViewEditorInner({
691
682
  onSelectProxyDetails: useCallback((details: ProxyConnectorDetails) => {
692
683
  setSelectedElement(null)
693
684
  setSelectedEdge(null)
694
- setSelectedEdgeId(null)
695
685
  closeConnectorPanelRef.current()
696
686
  closeElementPanelRef.current()
697
687
  setSelectedProxyConnectorDetails(details)
@@ -826,7 +816,7 @@ function ViewEditorInner({
826
816
  const ctxChanges = changes.filter((c) => 'id' in c && contextNodeIdsRef.current.has((c as { id: string }).id))
827
817
  const mainChanges = changes.filter((c) => !('id' in c) || !contextNodeIdsRef.current.has((c as { id: string }).id))
828
818
  if (ctxChanges.length > 0) {
829
- setLiveContextNodes((nds) => applyNodeChanges(ctxChanges, nds))
819
+ setLiveContextNodes((nds) => applyNodeChangesWithStructuralSharing(ctxChanges, nds))
830
820
  }
831
821
  if (mainChanges.length > 0) {
832
822
  canvasOnNodesChange(mainChanges)
@@ -904,7 +894,7 @@ function ViewEditorInner({
904
894
  return () => observer.disconnect()
905
895
  }, [maybeFitView])
906
896
 
907
- useEffect(() => { setRfNodes([]); setRfEdges([]); needsFitView.current = true }, [viewId, setRfEdges, setRfNodes])
897
+ useEffect(() => { needsFitView.current = true }, [viewId])
908
898
 
909
899
  // ── Dynamic viewport bounds ────────────────────────────────────────────────
910
900
  useEffect(() => {
@@ -1010,12 +1000,17 @@ function ViewEditorInner({
1010
1000
  const handleOpenExport = useCallback(() => exportModal.onOpen(), [exportModal])
1011
1001
  const handleConnectorSave = useCallback((updated: Connector) => {
1012
1002
  upsertConnectorGraphSnapshot(updated)
1013
- setConnectors((prev) => prev.map((c) => (c.id === updated.id ? updated : c)))
1014
- }, [setConnectors])
1015
- const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
1003
+ upsertStoreConnector(updated)
1004
+ }, [upsertStoreConnector])
1005
+ const handleConnectorDeleted = useCallback((edgeId: number) => {
1016
1006
  if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
1017
- setConnectors((prev) => prev.filter((c) => c.id !== edgeId))
1018
- }, [setConnectors, viewId])
1007
+ removeStoreConnector(edgeId)
1008
+ void refreshElements()
1009
+ }, [refreshElements, removeStoreConnector, viewId])
1010
+ const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
1011
+ handleConnectorDeleted(edgeId)
1012
+ setSelectedEdge(null)
1013
+ }, [handleConnectorDeleted, setSelectedEdge])
1019
1014
  const handleViewSave = useCallback((updated: ViewTreeNode) => setView(updated), [setView])
1020
1015
 
1021
1016
  // ── Library helpers ────────────────────────────────────────────────────────
@@ -1224,8 +1219,10 @@ function ViewEditorInner({
1224
1219
  nodesDraggable={canEdit} connectionMode={ConnectionMode.Loose} connectionRadius={25}
1225
1220
  edgesUpdatable={canEdit} reconnectRadius={0}
1226
1221
  snapToGrid={snapToGrid}
1227
- snapGrid={[30, 30]}
1222
+ snapGrid={SNAP_GRID}
1228
1223
  deleteKeyCode={null}
1224
+ onlyRenderVisibleElements
1225
+ autoPanOnNodeDrag={false}
1229
1226
  panOnDrag={!drawingMode}
1230
1227
  panOnScroll={!isMobileLayout} panOnScrollSpeed={1.2} panOnScrollMode={PanOnScrollMode.Free}
1231
1228
  zoomOnScroll={false} zoomOnPinch
@@ -1308,7 +1305,7 @@ function ViewEditorInner({
1308
1305
  try {
1309
1306
  await api.workspace.connectors.delete('', edgeId)
1310
1307
  removeConnectorGraphSnapshot(viewId, edgeId)
1311
- setConnectors((prev) => prev.filter((connector) => connector.id !== edgeId))
1308
+ removeStoreConnector(edgeId)
1312
1309
  } catch { /* intentionally empty */ }
1313
1310
  }}
1314
1311
  />
@@ -1406,8 +1403,8 @@ function ViewEditorInner({
1406
1403
  onSave={handleConnectorSave} autoSave
1407
1404
  onDelete={handleConnectorDeleteInPanel}
1408
1405
  hasBackdrop={isMobileLayout}
1409
- connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
1410
- />
1406
+ connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
1407
+ />
1411
1408
  <ProxyConnectorPanel
1412
1409
  isOpen={proxyConnectorPanel.isOpen}
1413
1410
  onClose={proxyConnectorPanel.onClose}