@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.
- package/dist/components/GraphRenderer.d.ts +3 -3
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +195 -39
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +10 -5
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +292 -56
- package/src/nodes/CustomNode.tsx +14 -5
- package/src/stories/GraphRenderer.stories.tsx +1 -1
|
@@ -101,14 +101,14 @@ interface GraphRendererBaseProps {
|
|
|
101
101
|
showBackground?: boolean;
|
|
102
102
|
/**
|
|
103
103
|
* Background variant style.
|
|
104
|
-
* - 'dots': Small dots pattern
|
|
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
|
|
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":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAmBf,OAAO,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAIV,SAAS,EACT,UAAU,EAIV,cAAc,EACd,gBAAgB,EAEhB,aAAa,EACd,MAAM,mCAAmC,CAAC;
|
|
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 = '
|
|
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
|
|
309
|
+
// Handle node click (toggle selection, supports Cmd/Ctrl for hide/dim)
|
|
218
310
|
const onNodeClick = useCallback((event, node) => {
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
//
|
|
230
|
-
if (event.shiftKey
|
|
231
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
setSelectedNodeIds(new Set());
|
|
251
|
-
setShowNodePanel(false);
|
|
380
|
+
setSelectedNodeIds(new Set([node.id]));
|
|
381
|
+
if (shouldShowPanel) {
|
|
382
|
+
setShowNodePanel(true);
|
|
252
383
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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 =
|
|
865
|
-
|
|
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
|
-
|
|
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,
|
|
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 })
|
|
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%',
|