@principal-ai/principal-view-react 0.13.2 → 0.13.4

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.
@@ -101,14 +101,14 @@ interface GraphRendererBaseProps {
101
101
  showBackground?: boolean;
102
102
  /**
103
103
  * Background variant style.
104
- * - 'dots': Small dots pattern (default)
105
- * - 'lines': Grid lines
104
+ * - 'dots': Small dots pattern
105
+ * - 'lines': Grid lines (default)
106
106
  * - 'cross': Cross pattern
107
107
  */
108
108
  backgroundVariant?: 'dots' | 'lines' | 'cross';
109
109
  /**
110
110
  * Gap between background pattern elements in pixels.
111
- * Defaults to 16 for dots, 50 for lines/cross.
111
+ * Defaults to 50 for lines/cross, 16 for dots.
112
112
  */
113
113
  backgroundGap?: number;
114
114
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"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"}
@@ -9,7 +9,6 @@ import { CustomEdge } from '../edges/CustomEdge';
9
9
  import { convertToXYFlowNodes, convertToXYFlowEdges, } from '../utils/graphConverter';
10
10
  import { EdgeInfoPanel } from './EdgeInfoPanel';
11
11
  import { NodeInfoPanel } from './NodeInfoPanel';
12
- import { SelectionSidebar } from './SelectionSidebar';
13
12
  // Define custom node types
14
13
  const nodeTypes = {
15
14
  custom: CustomNode,
@@ -26,6 +25,36 @@ const createEmptyEditState = () => ({
26
25
  createdEdges: [],
27
26
  deletedEdges: [],
28
27
  });
28
+ /**
29
+ * Alignment guides component that renders visual lines when nodes align during drag.
30
+ * Shows vertical lines for horizontal alignment (left/center/right) and
31
+ * horizontal lines for vertical alignment (top/middle/bottom).
32
+ */
33
+ const AlignmentGuidesOverlay = ({ guides, color }) => {
34
+ const { x: viewportX, y: viewportY, zoom } = useViewport();
35
+ if (guides.length === 0)
36
+ return null;
37
+ return (_jsx("svg", { style: {
38
+ position: 'absolute',
39
+ top: 0,
40
+ left: 0,
41
+ width: '100%',
42
+ height: '100%',
43
+ pointerEvents: 'none',
44
+ zIndex: 10,
45
+ }, children: guides.map((guide, index) => {
46
+ if (guide.type === 'vertical') {
47
+ // Vertical line (for horizontal alignment)
48
+ const screenX = guide.position * zoom + viewportX;
49
+ return (_jsx("line", { x1: screenX, y1: 0, x2: screenX, y2: "100%", stroke: color, strokeWidth: 1.5, strokeDasharray: "4,4", opacity: 0.8 }, `${guide.type}-${guide.position}-${index}`));
50
+ }
51
+ else {
52
+ // Horizontal line (for vertical alignment)
53
+ const screenY = guide.position * zoom + viewportY;
54
+ return (_jsx("line", { x1: 0, y1: screenY, x2: "100%", y2: screenY, stroke: color, strokeWidth: 1.5, strokeDasharray: "4,4", opacity: 0.8 }, `${guide.type}-${guide.position}-${index}`));
55
+ }
56
+ }) }));
57
+ };
29
58
  /**
30
59
  * Center indicator component that shows a crosshair at the canvas origin (0,0).
31
60
  * Uses viewport transform to position correctly regardless of pan/zoom.
@@ -51,12 +80,14 @@ const CenterIndicator = ({ color }) => {
51
80
  /**
52
81
  * Inner component that uses ReactFlow hooks
53
82
  */
54
- const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'dots', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, onNodeClick: onNodeClickProp, showNodeDetailPanel, resolveEventRef, }) => {
83
+ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'lines', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, onNodeClick: onNodeClickProp, showNodeDetailPanel, resolveEventRef, }) => {
55
84
  const { fitView } = useReactFlow();
56
85
  const updateNodeInternals = useUpdateNodeInternals();
57
86
  const { theme } = useTheme();
58
87
  // Track shift key state for tooltip control
59
88
  const [shiftKeyPressed, setShiftKeyPressed] = useState(false);
89
+ // Track if we're currently processing a node hide operation
90
+ const hidingNodeRef = useRef(false);
60
91
  // Setup keyboard event listeners for shift key
61
92
  useEffect(() => {
62
93
  const handleKeyDown = (e) => {
@@ -81,6 +112,8 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
81
112
  nodeAnimations: {},
82
113
  edgeAnimations: {},
83
114
  });
115
+ // Track alignment guides during drag operations
116
+ const [alignmentGuides, setAlignmentGuides] = useState([]);
84
117
  // Track selected edges for info panel (supports multi-select)
85
118
  const [selectedEdgeIds, setSelectedEdgeIds] = useState(new Set());
86
119
  // Track selected nodes for info panel (supports multi-select)
@@ -88,6 +121,8 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
88
121
  // Track whether panel should be shown (only on explicit clicks, not after dragging)
89
122
  const [showNodePanel, setShowNodePanel] = useState(false);
90
123
  const [showEdgePanel, setShowEdgePanel] = useState(false);
124
+ // Track hidden nodes (shift-click to toggle)
125
+ const [hiddenNodeIds, setHiddenNodeIds] = useState(new Set());
91
126
  // Track pending connection for edge type picker
92
127
  const [pendingConnection, setPendingConnection] = useState(null);
93
128
  // Sync highlightedNodeId to selection state so info panel shows
@@ -181,6 +216,63 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
181
216
  onPendingChangesChange?.(hasChanges);
182
217
  }, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
183
218
  // ============================================
219
+ // ALIGNMENT GUIDES
220
+ // ============================================
221
+ /**
222
+ * Detect alignment guides between a dragging node and other nodes
223
+ * @param draggingNodeId - ID of the node being dragged
224
+ * @param nodes - All nodes in the graph
225
+ * @returns Array of alignment guides to display
226
+ */
227
+ const detectAlignmentGuides = useCallback((draggingNodeId, nodes) => {
228
+ const threshold = 5; // pixels - how close to snap/show guide
229
+ const guides = [];
230
+ const draggingNode = nodes.find((n) => n.id === draggingNodeId);
231
+ if (!draggingNode)
232
+ return guides;
233
+ const dragLeft = draggingNode.position.x;
234
+ const dragRight = draggingNode.position.x + (draggingNode.width ?? 0);
235
+ const dragCenterX = draggingNode.position.x + (draggingNode.width ?? 0) / 2;
236
+ const dragTop = draggingNode.position.y;
237
+ const dragBottom = draggingNode.position.y + (draggingNode.height ?? 0);
238
+ const dragCenterY = draggingNode.position.y + (draggingNode.height ?? 0) / 2;
239
+ // Check alignment with other nodes
240
+ for (const node of nodes) {
241
+ if (node.id === draggingNodeId)
242
+ continue;
243
+ const nodeLeft = node.position.x;
244
+ const nodeRight = node.position.x + (node.width ?? 0);
245
+ const nodeCenterX = node.position.x + (node.width ?? 0) / 2;
246
+ const nodeTop = node.position.y;
247
+ const nodeBottom = node.position.y + (node.height ?? 0);
248
+ const nodeCenterY = node.position.y + (node.height ?? 0) / 2;
249
+ // Vertical guides (align horizontally - left/center/right)
250
+ if (Math.abs(dragLeft - nodeLeft) < threshold) {
251
+ guides.push({ type: 'vertical', position: nodeLeft, label: 'Left' });
252
+ }
253
+ else if (Math.abs(dragCenterX - nodeCenterX) < threshold) {
254
+ guides.push({ type: 'vertical', position: nodeCenterX, label: 'Center' });
255
+ }
256
+ else if (Math.abs(dragRight - nodeRight) < threshold) {
257
+ guides.push({ type: 'vertical', position: nodeRight, label: 'Right' });
258
+ }
259
+ // Horizontal guides (align vertically - top/middle/bottom)
260
+ if (Math.abs(dragTop - nodeTop) < threshold) {
261
+ guides.push({ type: 'horizontal', position: nodeTop, label: 'Top' });
262
+ }
263
+ else if (Math.abs(dragCenterY - nodeCenterY) < threshold) {
264
+ guides.push({ type: 'horizontal', position: nodeCenterY, label: 'Middle' });
265
+ }
266
+ else if (Math.abs(dragBottom - nodeBottom) < threshold) {
267
+ guides.push({ type: 'horizontal', position: nodeBottom, label: 'Bottom' });
268
+ }
269
+ }
270
+ // Remove duplicates (same position guides)
271
+ const uniqueGuides = guides.filter((guide, index, self) => index ===
272
+ self.findIndex((g) => g.type === guide.type && g.position === guide.position));
273
+ return uniqueGuides;
274
+ }, []);
275
+ // ============================================
184
276
  // EVENT HANDLERS
185
277
  // ============================================
186
278
  // Handle edge click (toggle selection, supports Shift for multi-select)
@@ -214,21 +306,34 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
214
306
  setShowNodePanel(false);
215
307
  }
216
308
  }, [editable, selectedEdgeIds]);
217
- // Handle node click (toggle selection, supports Shift for multi-select)
309
+ // Handle node click (toggle selection, supports Cmd/Ctrl for hide/dim)
218
310
  const onNodeClick = useCallback((event, node) => {
219
- // Determine if we should show the panel based on showNodeDetailPanel prop
220
- const shouldShowPanel = showNodeDetailPanel !== false && (showNodeDetailPanel === true || !onNodeClickProp);
221
- // If custom node click handler is provided, call it
222
- if (onNodeClickProp) {
223
- onNodeClickProp(node.id, event);
224
- // If showNodeDetailPanel is not explicitly true, return early (old behavior)
225
- if (showNodeDetailPanel !== true) {
226
- return;
227
- }
311
+ // Cmd+click (Mac) or Ctrl+click (Windows/Linux): toggle node visibility (hide edges and dim node)
312
+ if (event.metaKey || event.ctrlKey) {
313
+ event.preventDefault();
314
+ event.stopPropagation();
315
+ // Mark that we're hiding a node to prevent ReactFlow's selection change
316
+ hidingNodeRef.current = true;
317
+ setHiddenNodeIds((prev) => {
318
+ const next = new Set(prev);
319
+ if (next.has(node.id)) {
320
+ next.delete(node.id);
321
+ }
322
+ else {
323
+ next.add(node.id);
324
+ }
325
+ return next;
326
+ });
327
+ // Reset the flag after state update completes
328
+ setTimeout(() => {
329
+ hidingNodeRef.current = false;
330
+ }, 0);
331
+ return;
228
332
  }
229
- // Always update selection state for visual feedback (even in read-only mode)
230
- if (event.shiftKey && editable) {
231
- // Shift+click: toggle node in selection
333
+ // Shift+click: toggle node in selection (add/remove from multi-select)
334
+ if (event.shiftKey) {
335
+ event.preventDefault();
336
+ event.stopPropagation();
232
337
  setSelectedNodeIds((prev) => {
233
338
  const next = new Set(prev);
234
339
  if (next.has(node.id)) {
@@ -237,29 +342,56 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
237
342
  else {
238
343
  next.add(node.id);
239
344
  }
345
+ // Also update local nodes selection state immediately
346
+ if (editable) {
347
+ setXyflowLocalNodes((nodes) => nodes.map((n) => ({
348
+ ...n,
349
+ selected: n.id === node.id ? !prev.has(node.id) : next.has(n.id),
350
+ })));
351
+ }
240
352
  return next;
241
353
  });
242
- if (shouldShowPanel) {
243
- setShowNodePanel(true);
354
+ return;
355
+ }
356
+ // Determine if we should show the panel based on showNodeDetailPanel prop
357
+ const shouldShowPanel = showNodeDetailPanel !== false && (showNodeDetailPanel === true || !onNodeClickProp);
358
+ // If custom node click handler is provided, call it
359
+ if (onNodeClickProp) {
360
+ onNodeClickProp(node.id, event);
361
+ // If showNodeDetailPanel is not explicitly true, return early (old behavior)
362
+ if (showNodeDetailPanel !== true) {
363
+ return;
364
+ }
365
+ }
366
+ // Regular click: single select (replace selection)
367
+ const shouldDeselect = selectedNodeIds.size === 1 && selectedNodeIds.has(node.id);
368
+ if (shouldDeselect) {
369
+ setSelectedNodeIds(new Set());
370
+ setShowNodePanel(false);
371
+ // Also update local nodes selection state immediately
372
+ if (editable) {
373
+ setXyflowLocalNodes((nodes) => nodes.map((n) => ({
374
+ ...n,
375
+ selected: false,
376
+ })));
244
377
  }
245
378
  }
246
379
  else {
247
- // Regular click: single select (replace selection)
248
- const shouldDeselect = selectedNodeIds.size === 1 && selectedNodeIds.has(node.id);
249
- if (shouldDeselect) {
250
- setSelectedNodeIds(new Set());
251
- setShowNodePanel(false);
380
+ setSelectedNodeIds(new Set([node.id]));
381
+ if (shouldShowPanel) {
382
+ setShowNodePanel(true);
252
383
  }
253
- else {
254
- setSelectedNodeIds(new Set([node.id]));
255
- if (shouldShowPanel) {
256
- setShowNodePanel(true);
257
- }
384
+ // Also update local nodes selection state immediately
385
+ if (editable) {
386
+ setXyflowLocalNodes((nodes) => nodes.map((n) => ({
387
+ ...n,
388
+ selected: n.id === node.id,
389
+ })));
258
390
  }
259
- setSelectedEdgeIds(new Set());
260
- setShowEdgePanel(false);
261
391
  }
262
- }, [editable, selectedNodeIds, onNodeClickProp, showNodeDetailPanel]);
392
+ setSelectedEdgeIds(new Set());
393
+ setShowEdgePanel(false);
394
+ }, [selectedNodeIds, onNodeClickProp, showNodeDetailPanel, editable]);
263
395
  // Handle close edge info panel
264
396
  const onCloseEdgeInfoPanel = useCallback(() => {
265
397
  setSelectedEdgeIds(new Set());
@@ -292,6 +424,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
292
424
  }, []);
293
425
  // Handle selection change from ReactFlow (box selection and clicks)
294
426
  const handleSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }) => {
427
+ // Ignore selection changes when we're hiding a node
428
+ if (hidingNodeRef.current) {
429
+ return;
430
+ }
295
431
  // Always update selection state, even in read-only mode (for visual feedback)
296
432
  setSelectedNodeIds(new Set(selectedNodes.map((n) => n.id)));
297
433
  setSelectedEdgeIds(new Set(selectedEdges.map((e) => e.id)));
@@ -741,6 +877,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
741
877
  shiftKeyPressed,
742
878
  isHighlighted: highlightedNodeId === node.id,
743
879
  isActive: !activeNodeIds || activeNodeIds.length === 0 || activeNodeIds.includes(node.id),
880
+ isHidden: hiddenNodeIds.has(node.id),
744
881
  ...(animation
745
882
  ? {
746
883
  animationType: animation.type,
@@ -750,7 +887,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
750
887
  },
751
888
  };
752
889
  });
753
- }, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds]);
890
+ }, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds, hiddenNodeIds]);
754
891
  const baseNodesKey = useMemo(() => {
755
892
  return nodes
756
893
  .map((n) => n.id)
@@ -790,6 +927,17 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
790
927
  prevEditableRef.current = editable;
791
928
  }, [editable, xyflowNodesBase, updateNodeInternals]);
792
929
  const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
930
+ // Handle node drag to show alignment guides
931
+ const handleNodeDrag = useCallback((_event, node) => {
932
+ if (!editable)
933
+ return;
934
+ const guides = detectAlignmentGuides(node.id, xyflowNodes);
935
+ setAlignmentGuides(guides);
936
+ }, [editable, xyflowNodes, detectAlignmentGuides]);
937
+ // Clear guides when drag ends
938
+ const handleNodeDragStop = useCallback(() => {
939
+ setAlignmentGuides([]);
940
+ }, []);
793
941
  // Handle node changes (drag and resize events)
794
942
  const handleNodesChange = useCallback((changes) => {
795
943
  if (!editable)
@@ -860,14 +1008,22 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
860
1008
  }, [editable, updateEditState]);
861
1009
  const xyflowEdgesBase = useMemo(() => {
862
1010
  const converted = convertToXYFlowEdges(edges, configuration, violations);
863
- // Filter out edges connected to inactive nodes
864
- const filtered = activeNodeIds && activeNodeIds.length > 0
865
- ? converted.filter(edge => {
1011
+ // Filter out edges connected to inactive or hidden nodes
1012
+ const filtered = converted.filter(edge => {
1013
+ // Filter by hidden nodes (shift-clicked)
1014
+ const sourceHidden = hiddenNodeIds.has(edge.source);
1015
+ const targetHidden = hiddenNodeIds.has(edge.target);
1016
+ if (sourceHidden || targetHidden) {
1017
+ return false;
1018
+ }
1019
+ // Filter by active nodes (scenario playback)
1020
+ if (activeNodeIds && activeNodeIds.length > 0) {
866
1021
  const sourceActive = activeNodeIds.includes(edge.source);
867
1022
  const targetActive = activeNodeIds.includes(edge.target);
868
1023
  return sourceActive && targetActive;
869
- })
870
- : converted;
1024
+ }
1025
+ return true;
1026
+ });
871
1027
  // Debug: Log edge counts to help diagnose disappearing edges
872
1028
  if (process.env.NODE_ENV === 'development') {
873
1029
  console.log('[GraphRenderer] xyflowEdges computed:', {
@@ -910,7 +1066,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
910
1066
  return -1; // b comes after a (rendered on top)
911
1067
  return 0; // maintain original order
912
1068
  });
913
- }, [edges, configuration, violations, animationState.edgeAnimations, showTooltips, selectedEdgeIds, shiftKeyPressed, activeNodeIds]);
1069
+ }, [edges, configuration, violations, animationState.edgeAnimations, showTooltips, selectedEdgeIds, shiftKeyPressed, activeNodeIds, hiddenNodeIds]);
914
1070
  // Local xyflow edges state for reconnection
915
1071
  const [xyflowLocalEdges, setXyflowLocalEdges] = useState(xyflowEdgesBase);
916
1072
  // Sync when base edges change (structure changes like add/remove)
@@ -945,14 +1101,14 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
945
1101
  // ============================================
946
1102
  // RENDER
947
1103
  // ============================================
948
- return (_jsxs(_Fragment, { children: [_jsxs(ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, proOptions: { hideAttribution: true }, nodesDraggable: editable, elementsSelectable: true, nodesConnectable: editable, edgesReconnectable: editable, reconnectRadius: 25, elevateEdgesOnSelect: true, onNodesChange: handleNodesChange, onEdgesChange: handleEdgesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, onPaneClick: onPaneClick, onSelectionChange: handleSelectionChange, panOnDrag: true, selectionOnDrag: false, selectionKeyCode: editable ? 'Shift' : null, multiSelectionKeyCode: "Shift", children: [showBackground && (_jsx(Background, { color: backgroundVariant === 'dots' ? theme.colors.border : theme.colors.textMuted, gap: backgroundGap ?? (backgroundVariant === 'dots' ? 16 : 50), size: backgroundVariant === 'dots' ? 1 : 0.5, variant: backgroundVariant === 'dots'
1104
+ return (_jsxs(_Fragment, { children: [_jsxs(ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, onNodeDrag: handleNodeDrag, onNodeDragStop: handleNodeDragStop, proOptions: { hideAttribution: true }, nodesDraggable: editable, elementsSelectable: true, nodesConnectable: editable, edgesReconnectable: editable, reconnectRadius: 25, elevateEdgesOnSelect: true, onNodesChange: handleNodesChange, onEdgesChange: handleEdgesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, onPaneClick: onPaneClick, onSelectionChange: handleSelectionChange, panOnDrag: false, panOnScroll: true, zoomOnScroll: false, zoomOnPinch: true, selectionOnDrag: true, selectionKeyCode: null, multiSelectionKeyCode: null, children: [showBackground && (_jsx(Background, { color: backgroundVariant === 'dots' ? theme.colors.border : theme.colors.textMuted, gap: backgroundGap ?? (backgroundVariant === 'dots' ? 16 : 50), size: backgroundVariant === 'dots' ? 1 : 0.5, variant: backgroundVariant === 'dots'
949
1105
  ? BackgroundVariant.Dots
950
1106
  : backgroundVariant === 'lines'
951
1107
  ? BackgroundVariant.Lines
952
1108
  : BackgroundVariant.Cross })), showControls && _jsx(Controls, { showZoom: true, showFitView: true, showInteractive: true }), showMinimap && (_jsx(MiniMap, { nodeColor: (node) => {
953
1109
  const nodeData = node.data;
954
1110
  return nodeData?.typeDefinition?.color || theme.colors.secondary;
955
- }, nodeBorderRadius: 2, pannable: true, zoomable: true })), showCenterIndicator && _jsx(CenterIndicator, { color: theme.colors.textMuted })] }, `${baseNodesKey}-${baseEdgesKey}`), selectedNodeIds.size >= 2 && showNodePanel && showNodeDetailPanel !== false && (_jsx(SelectionSidebar, { selectedNodeIds: selectedNodeIds, nodes: nodes, nodeTypeDefinitions: configuration.nodeTypes, onClose: onCloseNodeInfoPanel })), selectedEdgeIds.size === 1 && selectedEdge && selectedEdgeTypeDefinition && showEdgePanel && (_jsx(EdgeInfoPanel, { edge: selectedEdge, typeDefinition: selectedEdgeTypeDefinition, sourceNodeId: selectedEdge.from, targetNodeId: selectedEdge.to, onClose: onCloseEdgeInfoPanel, onDelete: editable ? handleEdgeDelete : undefined, onUpdateSides: editable ? handleUpdateEdgeSides : undefined })), selectedNodeIds.size === 1 && selectedNode && selectedNodeTypeDefinition && showNodePanel && showNodeDetailPanel !== false && (_jsx(NodeInfoPanel, { node: selectedNode, typeDefinition: selectedNodeTypeDefinition, availableNodeTypes: configuration.nodeTypes, onClose: onCloseNodeInfoPanel, onDelete: editable ? handleNodeDelete : undefined, onUpdate: editable ? handleNodeUpdate : undefined, resolveEventRef: resolveEventRef })), pendingConnection && (_jsxs("div", { style: {
1111
+ }, nodeBorderRadius: 2, pannable: true, zoomable: true })), showCenterIndicator && _jsx(CenterIndicator, { color: theme.colors.textMuted }), editable && alignmentGuides.length > 0 && (_jsx(AlignmentGuidesOverlay, { guides: alignmentGuides, color: theme.colors.primary }))] }, `${baseNodesKey}-${baseEdgesKey}`), selectedEdgeIds.size === 1 && selectedEdge && selectedEdgeTypeDefinition && showEdgePanel && (_jsx(EdgeInfoPanel, { edge: selectedEdge, typeDefinition: selectedEdgeTypeDefinition, sourceNodeId: selectedEdge.from, targetNodeId: selectedEdge.to, onClose: onCloseEdgeInfoPanel, onDelete: editable ? handleEdgeDelete : undefined, onUpdateSides: editable ? handleUpdateEdgeSides : undefined })), selectedNodeIds.size === 1 && selectedNode && selectedNodeTypeDefinition && showNodePanel && showNodeDetailPanel !== false && (_jsx(NodeInfoPanel, { node: selectedNode, typeDefinition: selectedNodeTypeDefinition, availableNodeTypes: configuration.nodeTypes, onClose: onCloseNodeInfoPanel, onDelete: editable ? handleNodeDelete : undefined, onUpdate: editable ? handleNodeUpdate : undefined, resolveEventRef: resolveEventRef })), pendingConnection && (_jsxs("div", { style: {
956
1112
  position: 'absolute',
957
1113
  top: '50%',
958
1114
  left: '50%',