@tldiagram/core-ui 1.90.1 → 1.92.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.
@@ -161,7 +161,7 @@ export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _d
161
161
  clientX: number;
162
162
  clientY: number;
163
163
  }) => void;
164
- showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect") => void;
164
+ showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect", forceConnect?: boolean) => void;
165
165
  onNodesChange: (changes: NodeChange[]) => void;
166
166
  onEdgesChange: (changes: EdgeChange[]) => void;
167
167
  onNodeDragStart: NodeDragHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tldiagram/core-ui",
3
- "version": "1.90.1",
3
+ "version": "1.92.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.tsx CHANGED
@@ -10,7 +10,6 @@ import Settings from './pages/Settings'
10
10
  import AppearanceSettings from './pages/AppearanceSettings'
11
11
  import { HeaderProvider, useHeader } from './components/HeaderContext'
12
12
  import TopMenuBar from './components/TopMenuBar'
13
- import DemoPage, { DemoNavigator } from './demo/DemoPage'
14
13
  import { ThemeProvider } from './context/ThemeContext'
15
14
  import { ACCENT_DEFAULT, BACKGROUND_DEFAULT, ELEMENT_DEFAULT, hexToRgba } from './constants/colors'
16
15
  import { platform } from './platform/local'
@@ -111,9 +110,6 @@ export default function App() {
111
110
  {platform.getRoutes({ user: null })}
112
111
 
113
112
  <Route path="/explore/shared/:token" element={<Box h="100vh" overflow="hidden"><HeaderProvider><SharedInfiniteZoom /></HeaderProvider></Box>} />
114
- <Route path="/demo" element={<DemoNavigator />} />
115
- <Route path="/demo/:id" element={<DemoPage />} />
116
-
117
113
  <Route
118
114
  element={
119
115
  <HeaderProvider>
package/src/index.ts CHANGED
@@ -148,7 +148,6 @@ export * from './crossBranch/store'
148
148
  export * from './crossBranch/types'
149
149
 
150
150
  // ─── Demo ────────────────────────────────────────────────────────────────────
151
- export { default as DemoPage, DemoNavigator } from './demo/DemoPage'
152
151
  export * from './demo/viewEditor'
153
152
 
154
153
  // ─── Utilities ───────────────────────────────────────────────────────────────
@@ -26,7 +26,7 @@ interface ConnectorContextMenuProps {
26
26
  onDelete: (edgeId: number) => void
27
27
  }
28
28
 
29
- export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = ({
29
+ export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = React.memo(({
30
30
  menu,
31
31
  onEdit,
32
32
  onMoveSource,
@@ -68,14 +68,15 @@ export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = ({
68
68
  </VStack>
69
69
  </Box>
70
70
  )
71
- }
71
+ })
72
+ ConnectorContextMenu.displayName = 'ConnectorContextMenu'
72
73
 
73
74
  interface CanvasContextMenuProps {
74
75
  menu: { x: number; y: number; flowX: number; flowY: number } | null
75
76
  onAddElement: (x: number, y: number) => void
76
77
  }
77
78
 
78
- export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = ({ menu, onAddElement }) => {
79
+ export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = React.memo(({ menu, onAddElement }) => {
79
80
  const { canEdit, snapToGrid, setSnapToGrid } = useViewEditorContext()
80
81
  if (!menu) return null
81
82
 
@@ -109,4 +110,5 @@ export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = ({ menu, onAd
109
110
  </VStack>
110
111
  </Box>
111
112
  )
112
- }
113
+ })
114
+ CanvasContextMenu.displayName = 'CanvasContextMenu'
@@ -22,7 +22,7 @@ interface EditorOverlaysProps {
22
22
  rfNodes: RFNode[]
23
23
  }
24
24
 
25
- export const EditorOverlays: React.FC<EditorOverlaysProps> = ({
25
+ export const EditorOverlays: React.FC<EditorOverlaysProps> = React.memo(({
26
26
  connectGhostPos,
27
27
  clickConnectMode,
28
28
  clickConnectCursorPos,
@@ -169,4 +169,5 @@ export const EditorOverlays: React.FC<EditorOverlaysProps> = ({
169
169
 
170
170
  </>
171
171
  )
172
- }
172
+ })
173
+ EditorOverlays.displayName = 'EditorOverlays'
@@ -6,7 +6,7 @@ interface EmptyCanvasStateProps {
6
6
  hasNodes: boolean
7
7
  }
8
8
 
9
- export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = ({ isMobile, hasNodes }) => {
9
+ export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = React.memo(({ isMobile, hasNodes }) => {
10
10
  if (hasNodes) return null
11
11
 
12
12
  return (
@@ -39,4 +39,5 @@ export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = ({ isMobile, ha
39
39
  </Text>
40
40
  </Box>
41
41
  )
42
- }
42
+ })
43
+ EmptyCanvasState.displayName = 'EmptyCanvasState'
@@ -36,7 +36,6 @@ import {
36
36
  } from '../../../utils/edgeDistribution'
37
37
 
38
38
  const SNAP_RADIUS = 75
39
- const CONTEXT_BOUNDARY_INSET = 36
40
39
 
41
40
  interface CanvasInteractionOptions {
42
41
  viewId: number | null
@@ -260,37 +259,29 @@ export function useCanvasInteractions({
260
259
  const openConnectorPanelRef = useRef(openConnectorPanel)
261
260
  openConnectorPanelRef.current = openConnectorPanel
262
261
 
263
- const resolvePickerMode = useCallback((flowX: number, flowY: number, preferredMode: 'add' | 'connect') => {
262
+ const resolvePickerMode = useCallback((flowX: number, flowY: number, preferredMode: 'add' | 'connect', forceConnect = false) => {
264
263
  if (preferredMode !== 'connect') return preferredMode
264
+ if (forceConnect) return 'connect'
265
265
 
266
- const mainNodes = rfNodesRef.current.filter((node) => node.type === 'elementNode')
267
- if (mainNodes.length === 0) return preferredMode
268
-
269
- let minX = Infinity
270
- let minY = Infinity
271
- let maxX = -Infinity
272
- let maxY = -Infinity
273
-
274
- for (const node of mainNodes) {
275
- const width = node.width ?? 200
276
- const height = node.height ?? 90
277
- minX = Math.min(minX, node.position.x)
278
- minY = Math.min(minY, node.position.y)
279
- maxX = Math.max(maxX, node.position.x + width)
280
- maxY = Math.max(maxY, node.position.y + height)
281
- }
266
+ const boundaryNode = rfNodesRef.current.find((node) => node.type === 'ContextBoundaryElement')
267
+ if (!boundaryNode) return 'add'
268
+
269
+ const boundaryData = boundaryNode.data as { width?: number; height?: number } | undefined
270
+ const width = boundaryData?.width ?? boundaryNode.width
271
+ const height = boundaryData?.height ?? boundaryNode.height
272
+ if (width == null || height == null) return 'add'
282
273
 
283
274
  const withinBoundary =
284
- flowX >= minX - CONTEXT_BOUNDARY_INSET &&
285
- flowX <= maxX + CONTEXT_BOUNDARY_INSET &&
286
- flowY >= minY - CONTEXT_BOUNDARY_INSET &&
287
- flowY <= maxY + CONTEXT_BOUNDARY_INSET
275
+ flowX >= boundaryNode.position.x &&
276
+ flowX <= boundaryNode.position.x + width &&
277
+ flowY >= boundaryNode.position.y &&
278
+ flowY <= boundaryNode.position.y + height
288
279
 
289
280
  return withinBoundary ? 'add' : 'connect'
290
281
  }, [rfNodesRef])
291
282
 
292
283
  // ── showAddingElementAt ─────────────────────────────────────────────────────
293
- const showAddingElementAt = useCallback((clientX: number, clientY: number, expandResults = false, mode: 'add' | 'connect' = 'add') => {
284
+ const showAddingElementAt = useCallback((clientX: number, clientY: number, expandResults = false, mode: 'add' | 'connect' = 'add', forceConnect = false) => {
294
285
  const rect = containerRef.current?.getBoundingClientRect()
295
286
  if (!rect) return
296
287
  const flowPos = screenToFlowPositionRef.current({ x: clientX, y: clientY })
@@ -305,7 +296,7 @@ export function useCanvasInteractions({
305
296
  ? Math.max(100, Math.min(px, rect.width - 450))
306
297
  : Math.max(120, Math.min(px, rect.width - 120))
307
298
  const y = Math.max(40, Math.min(py, rect.height - 250))
308
- setAddingElementAt({ x, y, flowX, flowY, expandResults, mode: resolvePickerMode(flowX, flowY, mode) })
299
+ setAddingElementAt({ x, y, flowX, flowY, expandResults, mode: resolvePickerMode(flowX, flowY, mode, forceConnect) })
309
300
  }, [containerRef, snapToGrid, resolvePickerMode])
310
301
 
311
302
  // ── Inline element adder handlers ───────────────────────────────────────────
@@ -633,7 +624,7 @@ export function useCanvasInteractions({
633
624
  } else {
634
625
  setPendingConnectionSource(sourceElementId)
635
626
  suppressNextPaneClickRef.current = true
636
- showAddingElementAt(clientX, clientY, true, 'connect')
627
+ showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
637
628
  }
638
629
  }, [canEdit, setConnectors, showAddingElementAt, rfNodesRef, viewIdRef])
639
630
 
@@ -913,7 +904,7 @@ export function useCanvasInteractions({
913
904
  } else {
914
905
  setInteractionSourceId(null)
915
906
  setPendingConnectionSource(sourceId)
916
- showAddingElementAt(e.clientX, e.clientY, true, 'connect')
907
+ showAddingElementAt(e.clientX, e.clientY, true, 'connect', e.shiftKey)
917
908
  }
918
909
  return
919
910
  }
@@ -112,7 +112,7 @@ export function useViewContextNeighbours({
112
112
  const centerY = (minY + maxY) / 2
113
113
  const boundaryW = maxX - minX
114
114
  const boundaryH = maxY - minY
115
- const totalInset = 100
115
+ const totalInset = 200
116
116
  const padding = 180
117
117
  const radiusX = boundaryW / 2 + padding
118
118
  const radiusY = boundaryH / 2 + padding
@@ -13,7 +13,7 @@ import ReactFlow, {
13
13
  useReactFlow,
14
14
  applyNodeChanges,
15
15
  } from 'reactflow'
16
- import type { EdgeMarker as RFEdgeMarker, Node as RFNode, NodeChange } from 'reactflow'
16
+ import type { Edge as RFEdge, EdgeMarker as RFEdgeMarker, Node as RFNode, NodeChange } from 'reactflow'
17
17
  import 'reactflow/dist/style.css'
18
18
  import { toPng, toSvg } from 'html-to-image'
19
19
  import {
@@ -708,64 +708,105 @@ function ViewEditorInner({
708
708
  const contextNodeIdsRef = useRef<Set<string>>(new Set())
709
709
  useEffect(() => {
710
710
  contextNodeIdsRef.current = new Set(contextNodes.map((n) => n.id))
711
- setLiveContextNodes((prev) =>
712
- contextNodes.map((n) => {
713
- const existing = prev.find((p) => p.id === n.id)
711
+ setLiveContextNodes((prev) => {
712
+ const prevById = new Map(prev.map((p) => [p.id, p]))
713
+ return contextNodes.map((n) => {
714
+ const existing = prevById.get(n.id)
714
715
  if (existing?.width != null && existing?.height != null) {
715
716
  return { ...n, width: existing.width, height: existing.height }
716
717
  }
717
718
  return n
718
719
  })
719
- )
720
+ })
720
721
  }, [contextNodes])
721
722
 
723
+ const fadedNodeCacheRef = useRef<WeakMap<RFNode, RFNode>>(new WeakMap())
724
+ const fadedEdgeCacheRef = useRef<WeakMap<RFEdge, RFEdge>>(new WeakMap())
725
+
722
726
  const flowNodes = useMemo(() => {
723
- const allNodes = [...liveContextNodes, ...rfNodes]
724
- const selectedNodeIds = new Set(allNodes.filter((n) => n.selected).map((n) => n.id))
727
+ const allNodes = liveContextNodes.length === 0
728
+ ? rfNodes
729
+ : rfNodes.length === 0
730
+ ? liveContextNodes
731
+ : [...liveContextNodes, ...rfNodes]
732
+
733
+ let hasNodeSel = false
734
+ const selectedNodeIds = new Set<string>()
735
+ for (const n of allNodes) {
736
+ if (n.selected) { selectedNodeIds.add(n.id); hasNodeSel = true }
737
+ }
738
+
739
+ const allEdges = contextConnectors.length === 0
740
+ ? rfEdges
741
+ : rfEdges.length === 0
742
+ ? contextConnectors
743
+ : [...contextConnectors, ...rfEdges]
725
744
 
726
- const allEdges = [...contextConnectors, ...rfEdges]
727
745
  const selectedEdgeEndPoints = new Set<string>()
728
- allEdges.forEach((e) => {
746
+ let hasEdgeSel = false
747
+ for (const e of allEdges) {
729
748
  if (e.selected) {
730
749
  selectedEdgeEndPoints.add(e.source)
731
750
  selectedEdgeEndPoints.add(e.target)
751
+ hasEdgeSel = true
732
752
  }
733
- })
753
+ }
734
754
 
735
755
  const neighborNodeIds = new Set<string>()
736
- if (selectedNodeIds.size > 0) {
737
- allEdges.forEach((e) => {
756
+ if (hasNodeSel) {
757
+ for (const e of allEdges) {
738
758
  if (selectedNodeIds.has(e.source)) neighborNodeIds.add(e.target)
739
759
  if (selectedNodeIds.has(e.target)) neighborNodeIds.add(e.source)
740
- })
760
+ }
741
761
  }
742
762
 
743
- if (selectedNodeIds.size === 0 && selectedEdgeEndPoints.size === 0) return allNodes
763
+ if (!hasNodeSel && !hasEdgeSel) return allNodes
744
764
 
765
+ const cache = fadedNodeCacheRef.current
745
766
  return allNodes.map((n) => {
746
767
  const isHighlighted = selectedNodeIds.has(n.id) || selectedEdgeEndPoints.has(n.id) || neighborNodeIds.has(n.id)
747
768
  if (isHighlighted) return n
748
- return {
769
+ const cached = cache.get(n)
770
+ if (cached) return cached
771
+ const faded: RFNode = {
749
772
  ...n,
750
773
  style: { ...n.style, opacity: (Number(n.style?.opacity ?? 1)) * 0.2 },
751
774
  }
775
+ cache.set(n, faded)
776
+ return faded
752
777
  })
753
778
  }, [liveContextNodes, rfNodes, contextConnectors, rfEdges])
754
779
 
755
780
  const flowEdges = useMemo(() => {
756
- const allEdges = [...contextConnectors, ...rfEdges]
757
- const allNodes = [...liveContextNodes, ...rfNodes]
758
- const selectedNodeIds = new Set(allNodes.filter((n) => n.selected).map((n) => n.id))
759
- const hasEdgeSelection = allEdges.some((e) => e.selected)
781
+ const allEdges = contextConnectors.length === 0
782
+ ? rfEdges
783
+ : rfEdges.length === 0
784
+ ? contextConnectors
785
+ : [...contextConnectors, ...rfEdges]
786
+ const allNodes = liveContextNodes.length === 0
787
+ ? rfNodes
788
+ : rfNodes.length === 0
789
+ ? liveContextNodes
790
+ : [...liveContextNodes, ...rfNodes]
791
+
792
+ const selectedNodeIds = new Set<string>()
793
+ let hasNodeSel = false
794
+ for (const n of allNodes) {
795
+ if (n.selected) { selectedNodeIds.add(n.id); hasNodeSel = true }
796
+ }
797
+ let hasEdgeSel = false
798
+ for (const e of allEdges) { if (e.selected) { hasEdgeSel = true; break } }
760
799
 
761
- if (selectedNodeIds.size === 0 && !hasEdgeSelection) return allEdges
800
+ if (!hasNodeSel && !hasEdgeSel) return allEdges
762
801
 
802
+ const cache = fadedEdgeCacheRef.current
763
803
  return allEdges.map((e) => {
764
804
  const isHighlighted = e.selected || selectedNodeIds.has(e.source) || selectedNodeIds.has(e.target)
765
805
  if (isHighlighted) return e
766
-
806
+ const cached = cache.get(e)
807
+ if (cached) return cached
767
808
  const multiplier = 0.2
768
- return {
809
+ const faded: RFEdge = {
769
810
  ...e,
770
811
  style: { ...e.style, opacity: (Number(e.style?.opacity ?? 0.8)) * multiplier },
771
812
  labelStyle: e.labelStyle ? { ...e.labelStyle, opacity: (Number(e.labelStyle.opacity ?? 1)) * multiplier } : undefined,
@@ -773,6 +814,8 @@ function ViewEditorInner({
773
814
  markerEnd: fadeMarker(e.markerEnd, multiplier),
774
815
  markerStart: fadeMarker(e.markerStart, multiplier),
775
816
  }
817
+ cache.set(e, faded)
818
+ return faded
776
819
  })
777
820
  }, [contextConnectors, rfEdges, liveContextNodes, rfNodes])
778
821
 
@@ -870,13 +913,18 @@ function ViewEditorInner({
870
913
  [-vw * VIEW_EDITOR_EMPTY_EXTENT_RATIO, -vh * VIEW_EDITOR_EMPTY_EXTENT_RATIO],
871
914
  [vw * VIEW_EDITOR_EMPTY_EXTENT_RATIO, vh * VIEW_EDITOR_EMPTY_EXTENT_RATIO],
872
915
  ]
873
- if (flowNodes.length === 0 && drawingPaths.length === 0) {
916
+ const boundsNodes = liveContextNodes.length === 0
917
+ ? rfNodes
918
+ : rfNodes.length === 0
919
+ ? liveContextNodes
920
+ : [...liveContextNodes, ...rfNodes]
921
+ if (boundsNodes.length === 0 && drawingPaths.length === 0) {
874
922
  setComputedMinZoom((prev) => prev === VIEW_EDITOR_MIN_ZOOM_FLOOR ? prev : VIEW_EDITOR_MIN_ZOOM_FLOOR)
875
923
  setComputedTranslateExtent((prev) => areTranslateExtentsEqual(prev, emptyExtent) ? prev : emptyExtent)
876
924
  return
877
925
  }
878
926
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
879
- for (const n of flowNodes) {
927
+ for (const n of boundsNodes) {
880
928
  minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y)
881
929
  maxX = Math.max(maxX, n.position.x + (n.width ?? 180)); maxY = Math.max(maxY, n.position.y + (n.height ?? 80))
882
930
  }
@@ -903,7 +951,7 @@ function ViewEditorInner({
903
951
  const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2
904
952
  const nextTranslateExtent: [[number, number], [number, number]] = [[cx - spanX / 2, cy - spanY / 2], [cx + spanX / 2, cy + spanY / 2]]
905
953
  setComputedTranslateExtent((prev) => areTranslateExtentsEqual(prev, nextTranslateExtent) ? prev : nextTranslateExtent)
906
- }, [flowNodes, drawingPaths])
954
+ }, [rfNodes, liveContextNodes, drawingPaths])
907
955
 
908
956
  // ── Keyboard shortcuts for drawing ────────────────────────────────────────
909
957
  useEffect(() => {
@@ -951,7 +999,27 @@ function ViewEditorInner({
951
999
  // ── Share ──────────────────────────────────────────────────────────────────
952
1000
  const onShare = useCallback(() => {}, [])
953
1001
 
1002
+ const handleExplorerHoverZoom = useCallback((elementId: number | null, type: 'in' | 'out' | null) => {
1003
+ setHoveredZoom(type && elementId ? { elementId, type } : null)
1004
+ }, [])
1005
+ const handleToggleExplorer = useCallback(() => setIsExplorerOpen((v) => !v), [])
1006
+ const handleCloseLibrary = useCallback(() => setLibraryOpen(false), [])
1007
+ const handleCreateNewLibraryRef = useRef<() => void>(() => {})
1008
+ const handleCreateNewLibrary = useCallback(() => handleCreateNewLibraryRef.current(), [])
1009
+ const handleFocusModeChange = useCallback((v: boolean) => setCrossBranchEnabled(!v), [setCrossBranchEnabled])
1010
+ const handleOpenExport = useCallback(() => exportModal.onOpen(), [exportModal])
1011
+ const handleConnectorSave = useCallback((updated: Connector) => {
1012
+ upsertConnectorGraphSnapshot(updated)
1013
+ setConnectors((prev) => prev.map((c) => (c.id === updated.id ? updated : c)))
1014
+ }, [setConnectors])
1015
+ const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
1016
+ if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
1017
+ setConnectors((prev) => prev.filter((c) => c.id !== edgeId))
1018
+ }, [setConnectors, viewId])
1019
+ const handleViewSave = useCallback((updated: ViewTreeNode) => setView(updated), [setView])
1020
+
954
1021
  // ── Library helpers ────────────────────────────────────────────────────────
1022
+ // Assigned below; referenced by memoized callbacks (e.g. ElementLibrary onCreateNew).
955
1023
  const handleAddElementAtCenter = useCallback((forceCenter = false) => {
956
1024
  if (!canEdit) return
957
1025
  const rect = containerRef.current?.getBoundingClientRect()
@@ -965,6 +1033,7 @@ function ViewEditorInner({
965
1033
  }
966
1034
  showAddingElementAt(cx, cy, true)
967
1035
  }, [canEdit, showAddingElementAt, lastMousePosRef])
1036
+ handleCreateNewLibraryRef.current = () => handleAddElementAtCenter(true)
968
1037
 
969
1038
  const handleTapAdd = useCallback(async (obj: WorkspaceElement) => {
970
1039
  if (!canEdit || !viewId || existingElementIds.has(obj.id)) return
@@ -1177,8 +1246,8 @@ function ViewEditorInner({
1177
1246
  treeNodes={treeData}
1178
1247
  linksMap={linksMap} viewElements={viewElements}
1179
1248
  onNavigate={canvas.stableOnNavigateToView}
1180
- onHoverZoom={(elementId, type) => setHoveredZoom(type && elementId ? { elementId, type } : null)}
1181
- isOpen={isExplorerOpen} onToggle={() => setIsExplorerOpen((v) => !v)}
1249
+ onHoverZoom={handleExplorerHoverZoom}
1250
+ isOpen={isExplorerOpen} onToggle={handleToggleExplorer}
1182
1251
  isMobile={isMobileLayout}
1183
1252
  activeTags={activeTags}
1184
1253
  setActiveTags={handleSetActiveTags}
@@ -1288,9 +1357,9 @@ function ViewEditorInner({
1288
1357
  hasDrawingPaths={drawingPaths.length > 0} drawingVisible={drawingVisible} setDrawingVisible={setDrawingVisible}
1289
1358
  extrasOpen={extrasOpen} setExtrasOpen={setExtrasOpen}
1290
1359
  focusMode={!crossBranchSettings.enabled}
1291
- onFocusModeChange={(v) => setCrossBranchEnabled(!v)}
1360
+ onFocusModeChange={handleFocusModeChange}
1292
1361
  disableImportExport={disableImportExport}
1293
- onImport={importModal.onOpen} onExport={() => exportModal.onOpen()} onShare={onShare}
1362
+ onImport={importModal.onOpen} onExport={handleOpenExport} onShare={onShare}
1294
1363
  allTags={availableTags}
1295
1364
  layers={layers}
1296
1365
  tagColors={tagColors}
@@ -1310,8 +1379,8 @@ function ViewEditorInner({
1310
1379
  <ElementLibrary
1311
1380
  existingElementIds={existingElementIds}
1312
1381
  existingElements={existingElements}
1313
- onCreateNew={() => handleAddElementAtCenter(true)} refresh={libraryRefresh}
1314
- isOpen={libraryOpen} onClose={() => setLibraryOpen(false)}
1382
+ onCreateNew={handleCreateNewLibrary} refresh={libraryRefresh}
1383
+ isOpen={libraryOpen} onClose={handleCloseLibrary}
1315
1384
  onTapAdd={canEdit ? handleTapAdd : undefined}
1316
1385
  onFindElement={handleFindElement}
1317
1386
  onTouchDrop={canEdit ? handleTouchDrop : undefined}
@@ -1334,14 +1403,8 @@ function ViewEditorInner({
1334
1403
  <ConnectorPanel
1335
1404
  isOpen={connectorPanel.isOpen} onClose={connectorPanel.onClose} connector={selectedEdge}
1336
1405
  orgId={''}
1337
- onSave={(updated: Connector) => {
1338
- upsertConnectorGraphSnapshot(updated)
1339
- setConnectors((prev) => prev.map((connector) => (connector.id === updated.id ? updated : connector)))
1340
- }} autoSave
1341
- onDelete={(edgeId: number) => {
1342
- if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
1343
- setConnectors((prev) => prev.filter((connector) => connector.id !== edgeId))
1344
- }}
1406
+ onSave={handleConnectorSave} autoSave
1407
+ onDelete={handleConnectorDeleteInPanel}
1345
1408
  hasBackdrop={isMobileLayout}
1346
1409
  connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
1347
1410
  />
@@ -1355,7 +1418,7 @@ function ViewEditorInner({
1355
1418
  <ViewPanel
1356
1419
  isOpen={viewDetails.isOpen} onClose={viewDetails.onClose}
1357
1420
  view={view as ViewTreeNode}
1358
- onSave={(updated) => setView(updated)} hasBackdrop={isMobileLayout}
1421
+ onSave={handleViewSave} hasBackdrop={isMobileLayout}
1359
1422
  />
1360
1423
 
1361
1424
  <ExportModal
@@ -1,9 +0,0 @@
1
- /**
2
- * Demo entry point.
3
- * Overrides the real `api` singleton with localStorage-backed implementations
4
- * and patches window.history to redirect /views/:id → /demo/:id, all for the
5
- * lifetime of this component. Restores everything on unmount.
6
- * No auth required; served at /demo and /demo/:id routes.
7
- */
8
- export declare function DemoNavigator(): null;
9
- export default function DemoPage(): import("react/jsx-runtime").JSX.Element;
@@ -1,9 +0,0 @@
1
- import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer } from '../types';
2
- export declare const DEMO_ELEMENTS: LibraryElement[];
3
- export declare const DEMO_VIEWS: ViewTreeNode[];
4
- type ViewPlacements = Record<number, PlacedElement[]>;
5
- type ViewConnectors = Record<number, Connector[]>;
6
- export declare const DEMO_PLACEMENTS: ViewPlacements;
7
- export declare const DEMO_CONNECTORS: ViewConnectors;
8
- export declare const DEMO_LAYERS: Record<number, ViewLayer[]>;
9
- export {};
@@ -1,137 +0,0 @@
1
- /**
2
- * localStorage-backed store for the demo mode.
3
- * Implements the subset of the `api` interface used by ViewEditor and its hooks.
4
- * Data is scoped under the `diag:demo:*` key namespace to avoid colliding
5
- * with a real logged-in session.
6
- */
7
- import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer, Tag, ElementPlacement, ExploreData } from '../types';
8
- export declare function initDemoStore(): void;
9
- export declare function resetDemoStore(): void;
10
- export declare const demoApi: {
11
- explore: {
12
- load: () => Promise<ExploreData>;
13
- };
14
- elements: {
15
- list: (_params?: unknown) => Promise<LibraryElement[]>;
16
- get: (id: number) => Promise<LibraryElement>;
17
- create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
18
- update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
19
- delete: (_orgId: string, id: number) => Promise<void>;
20
- placements: (id: number) => Promise<ElementPlacement[]>;
21
- };
22
- workspace: {
23
- orgs: {
24
- tagColors: {
25
- list: () => Promise<Record<string, Tag>>;
26
- set: (tag: string, color: string, description?: string) => Promise<void>;
27
- };
28
- };
29
- views: {
30
- list: () => Promise<{
31
- id: number;
32
- owner_element_id: number | null;
33
- name: string;
34
- label: string | null;
35
- is_root: boolean;
36
- created_at: string;
37
- updated_at: string;
38
- }[]>;
39
- get: (id: number) => Promise<ViewTreeNode>;
40
- content: (id: number) => Promise<{
41
- placements: PlacedElement[];
42
- connectors: Connector[];
43
- }>;
44
- tree: () => Promise<ViewTreeNode[]>;
45
- create: (data: {
46
- name: string;
47
- label?: string;
48
- parent_view_id?: number | null;
49
- }) => Promise<{
50
- id: number;
51
- owner_element_id: number | null;
52
- name: string;
53
- label: string | null;
54
- is_root: boolean;
55
- created_at: string;
56
- updated_at: string;
57
- }>;
58
- update: (id: number, data: {
59
- name: string;
60
- label?: string;
61
- }) => Promise<{
62
- id: number;
63
- owner_element_id: number | null;
64
- name: string;
65
- label: string | null;
66
- is_root: boolean;
67
- created_at: string;
68
- updated_at: string;
69
- }>;
70
- delete: (_orgId: string, id: number) => Promise<void>;
71
- placements: {
72
- list: (viewId: number) => Promise<ElementPlacement[]>;
73
- add: (viewId: number, elementId: number, x?: number, y?: number) => Promise<ElementPlacement>;
74
- updatePosition: (viewId: number, elementId: number, x: number, y: number) => Promise<void>;
75
- remove: (viewId: number, elementId: number) => Promise<void>;
76
- };
77
- layers: {
78
- list: (viewId: number) => Promise<ViewLayer[]>;
79
- create: (viewId: number, data: {
80
- name: string;
81
- tags: string[];
82
- color?: string;
83
- }) => Promise<ViewLayer>;
84
- update: (viewId: number, layerId: number, data: Partial<ViewLayer>) => Promise<ViewLayer>;
85
- delete: (viewId: number, layerId: number) => Promise<void>;
86
- };
87
- reactions: {
88
- list: (_viewId: number) => Promise<never[]>;
89
- };
90
- threads: {
91
- listForElement: () => Promise<never[]>;
92
- listForConnector: () => Promise<never[]>;
93
- createForElement: () => Promise<never>;
94
- createForConnector: () => Promise<never>;
95
- addComment: () => Promise<never>;
96
- resolve: () => Promise<void>;
97
- };
98
- thumbnail: (_id: number) => Promise<null>;
99
- rename: (id: number, name: string) => Promise<{
100
- id: number;
101
- owner_element_id: number | null;
102
- name: string;
103
- label: string | null;
104
- is_root: boolean;
105
- created_at: string;
106
- updated_at: string;
107
- }>;
108
- setLevel: () => Promise<void>;
109
- reparent: () => Promise<never>;
110
- };
111
- connectors: {
112
- list: (viewId: number) => Promise<Connector[]>;
113
- create: (viewId: number, data: {
114
- source_element_id: number;
115
- target_element_id: number;
116
- label?: string;
117
- description?: string;
118
- relationship?: string;
119
- direction?: string;
120
- style?: string;
121
- url?: string;
122
- source_handle?: string | null;
123
- target_handle?: string | null;
124
- }) => Promise<Connector>;
125
- update: (viewId: number, connectorId: number, data: Partial<Connector>) => Promise<Connector>;
126
- delete: (_orgId: string, connectorId: number) => Promise<void>;
127
- };
128
- elements: {
129
- list: (params?: unknown) => Promise<LibraryElement[]>;
130
- get: (id: number) => Promise<LibraryElement>;
131
- create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
132
- update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
133
- delete: (orgId: string, id: number) => Promise<void>;
134
- placements: (id: number) => Promise<ElementPlacement[]>;
135
- };
136
- };
137
- };