@principal-ai/principal-view-react 0.6.16 → 0.6.18

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.
@@ -45,6 +45,7 @@ import {
45
45
  } from '../utils/graphConverter';
46
46
  import { EdgeInfoPanel } from './EdgeInfoPanel';
47
47
  import { NodeInfoPanel } from './NodeInfoPanel';
48
+ import { SelectionSidebar } from './SelectionSidebar';
48
49
 
49
50
  /** Position change event for tracking node movements */
50
51
  export interface NodePositionChange {
@@ -52,10 +53,18 @@ export interface NodePositionChange {
52
53
  position: { x: number; y: number };
53
54
  }
54
55
 
56
+ /** Dimension change event for tracking node resizing */
57
+ export interface NodeDimensionChange {
58
+ nodeId: string;
59
+ dimensions: { width: number; height: number };
60
+ }
61
+
55
62
  /** All pending changes that can be saved */
56
63
  export interface PendingChanges {
57
64
  /** Node position changes */
58
65
  positionChanges: NodePositionChange[];
66
+ /** Node dimension changes (from resizing) */
67
+ dimensionChanges: NodeDimensionChange[];
59
68
  /** Node updates (type, data changes) */
60
69
  nodeUpdates: Array<{
61
70
  nodeId: string;
@@ -184,6 +193,7 @@ interface AnimationState {
184
193
  // Internal edit state tracking
185
194
  interface EditState {
186
195
  positionChanges: Map<string, { x: number; y: number }>;
196
+ dimensionChanges: Map<string, { width: number; height: number }>;
187
197
  nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
188
198
  deletedNodeIds: Set<string>;
189
199
  createdEdges: Array<{
@@ -199,6 +209,7 @@ interface EditState {
199
209
 
200
210
  const createEmptyEditState = (): EditState => ({
201
211
  positionChanges: new Map(),
212
+ dimensionChanges: new Map(),
202
213
  nodeUpdates: new Map(),
203
214
  deletedNodeIds: new Set(),
204
215
  createdEdges: [],
@@ -253,11 +264,11 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
253
264
  edgeAnimations: {},
254
265
  });
255
266
 
256
- // Track selected edge for info panel
257
- const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
267
+ // Track selected edges for info panel (supports multi-select)
268
+ const [selectedEdgeIds, setSelectedEdgeIds] = useState<Set<string>>(new Set());
258
269
 
259
- // Track selected node for info panel
260
- const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
270
+ // Track selected nodes for info panel (supports multi-select)
271
+ const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
261
272
 
262
273
  // Track pending connection for edge type picker
263
274
  const [pendingConnection, setPendingConnection] = useState<{
@@ -323,6 +334,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
323
334
  const checkHasChanges = useCallback((state: EditState): boolean => {
324
335
  return (
325
336
  state.positionChanges.size > 0 ||
337
+ state.dimensionChanges.size > 0 ||
326
338
  state.nodeUpdates.size > 0 ||
327
339
  state.deletedNodeIds.size > 0 ||
328
340
  state.createdEdges.length > 0 ||
@@ -345,21 +357,65 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
345
357
  // EVENT HANDLERS
346
358
  // ============================================
347
359
 
348
- // Handle edge click (toggle selection)
349
- const onEdgeClick = useCallback((_event: React.MouseEvent, edge: Edge) => {
350
- setSelectedEdgeId((prev) => (prev === edge.id ? null : edge.id));
351
- setSelectedNodeId(null);
352
- }, []);
360
+ // Handle edge click (toggle selection, supports Shift for multi-select)
361
+ const onEdgeClick = useCallback(
362
+ (event: React.MouseEvent, edge: Edge) => {
363
+ if (event.shiftKey && editable) {
364
+ // Shift+click: toggle edge in selection
365
+ setSelectedEdgeIds((prev) => {
366
+ const next = new Set(prev);
367
+ if (next.has(edge.id)) {
368
+ next.delete(edge.id);
369
+ } else {
370
+ next.add(edge.id);
371
+ }
372
+ return next;
373
+ });
374
+ } else {
375
+ // Regular click: single select (replace selection)
376
+ setSelectedEdgeIds((prev) => {
377
+ if (prev.size === 1 && prev.has(edge.id)) {
378
+ return new Set(); // Deselect if clicking same edge
379
+ }
380
+ return new Set([edge.id]);
381
+ });
382
+ setSelectedNodeIds(new Set());
383
+ }
384
+ },
385
+ [editable]
386
+ );
353
387
 
354
- // Handle node click (toggle selection)
355
- const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
356
- setSelectedNodeId((prev) => (prev === node.id ? null : node.id));
357
- setSelectedEdgeId(null);
358
- }, []);
388
+ // Handle node click (toggle selection, supports Shift for multi-select)
389
+ const onNodeClick = useCallback(
390
+ (event: React.MouseEvent, node: Node) => {
391
+ if (event.shiftKey && editable) {
392
+ // Shift+click: toggle node in selection
393
+ setSelectedNodeIds((prev) => {
394
+ const next = new Set(prev);
395
+ if (next.has(node.id)) {
396
+ next.delete(node.id);
397
+ } else {
398
+ next.add(node.id);
399
+ }
400
+ return next;
401
+ });
402
+ } else {
403
+ // Regular click: single select (replace selection)
404
+ setSelectedNodeIds((prev) => {
405
+ if (prev.size === 1 && prev.has(node.id)) {
406
+ return new Set(); // Deselect if clicking same node
407
+ }
408
+ return new Set([node.id]);
409
+ });
410
+ setSelectedEdgeIds(new Set());
411
+ }
412
+ },
413
+ [editable]
414
+ );
359
415
 
360
416
  // Handle close edge info panel
361
417
  const onCloseEdgeInfoPanel = useCallback(() => {
362
- setSelectedEdgeId(null);
418
+ setSelectedEdgeIds(new Set());
363
419
  }, []);
364
420
 
365
421
  // Handle edge side updates from EdgeInfoPanel
@@ -385,9 +441,26 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
385
441
 
386
442
  // Handle close node info panel
387
443
  const onCloseNodeInfoPanel = useCallback(() => {
388
- setSelectedNodeId(null);
444
+ setSelectedNodeIds(new Set());
445
+ }, []);
446
+
447
+ // Handle pane click (clear selection when clicking empty space)
448
+ const onPaneClick = useCallback(() => {
449
+ setSelectedNodeIds(new Set());
450
+ setSelectedEdgeIds(new Set());
389
451
  }, []);
390
452
 
453
+ // Handle selection change from ReactFlow (box selection)
454
+ const handleSelectionChange = useCallback(
455
+ ({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => {
456
+ if (editable) {
457
+ setSelectedNodeIds(new Set(selectedNodes.map((n) => n.id)));
458
+ setSelectedEdgeIds(new Set(selectedEdges.map((e) => e.id)));
459
+ }
460
+ },
461
+ [editable]
462
+ );
463
+
391
464
  // Handle node update (internal - updates local state only)
392
465
  const handleNodeUpdate = useCallback(
393
466
  (nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => {
@@ -440,6 +513,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
440
513
  // Remove any position changes for this node
441
514
  const newPositions = new Map(prev.positionChanges);
442
515
  newPositions.delete(nodeId);
516
+ // Remove any dimension changes for this node
517
+ const newDimensions = new Map(prev.dimensionChanges);
518
+ newDimensions.delete(nodeId);
443
519
  // Remove created edges that involve this node
444
520
  const newCreatedEdges = prev.createdEdges.filter(
445
521
  (e) => e.from !== nodeId && e.to !== nodeId
@@ -449,11 +525,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
449
525
  deletedNodeIds: newDeletedNodes,
450
526
  nodeUpdates: newUpdates,
451
527
  positionChanges: newPositions,
528
+ dimensionChanges: newDimensions,
452
529
  createdEdges: newCreatedEdges,
453
530
  };
454
531
  });
455
532
 
456
- setSelectedNodeId(null);
533
+ setSelectedNodeIds(new Set());
457
534
  },
458
535
  [editable, updateEditState]
459
536
  );
@@ -495,7 +572,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
495
572
  return prev;
496
573
  });
497
574
 
498
- setSelectedEdgeId(null);
575
+ setSelectedEdgeIds(new Set());
499
576
  },
500
577
  [editable, updateEditState, localEdges]
501
578
  );
@@ -713,6 +790,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
713
790
  // SELECTED ITEMS
714
791
  // ============================================
715
792
 
793
+ // Get first selected edge (for single-selection info panel)
794
+ const selectedEdgeId = useMemo(() => {
795
+ if (selectedEdgeIds.size === 0) return null;
796
+ return selectedEdgeIds.values().next().value;
797
+ }, [selectedEdgeIds]);
798
+
716
799
  const selectedEdge = useMemo(() => {
717
800
  if (!selectedEdgeId) return null;
718
801
  return edges.find((e) => e.id === selectedEdgeId);
@@ -723,6 +806,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
723
806
  return configuration.edgeTypes[selectedEdge.type];
724
807
  }, [selectedEdge, configuration.edgeTypes]);
725
808
 
809
+ // Get first selected node (for single-selection info panel)
810
+ const selectedNodeId = useMemo(() => {
811
+ if (selectedNodeIds.size === 0) return null;
812
+ return selectedNodeIds.values().next().value;
813
+ }, [selectedNodeIds]);
814
+
726
815
  const selectedNode = useMemo(() => {
727
816
  if (!selectedNodeId) return null;
728
817
  return nodes.find((n) => n.id === selectedNodeId);
@@ -907,7 +996,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
907
996
 
908
997
  const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
909
998
 
910
- // Handle node changes (drag events)
999
+ // Handle node changes (drag and resize events)
911
1000
  const handleNodesChange = useCallback(
912
1001
  (changes: NodeChange[]) => {
913
1002
  if (!editable) return;
@@ -930,6 +1019,37 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
930
1019
  change.dragging === false
931
1020
  );
932
1021
 
1022
+ // Track dimension changes (from NodeResizer)
1023
+ const dimensionChanges = changes.filter(
1024
+ (
1025
+ change
1026
+ ): change is NodeChange & {
1027
+ type: 'dimensions';
1028
+ dimensions: { width: number; height: number };
1029
+ resizing: boolean;
1030
+ } =>
1031
+ change.type === 'dimensions' &&
1032
+ 'dimensions' in change &&
1033
+ change.dimensions !== undefined &&
1034
+ 'resizing' in change &&
1035
+ change.resizing === false
1036
+ );
1037
+
1038
+ if (dimensionChanges.length > 0) {
1039
+ updateEditState((prev) => {
1040
+ const newDimensions = new Map(prev.dimensionChanges);
1041
+ for (const change of dimensionChanges) {
1042
+ if (change.dimensions) {
1043
+ newDimensions.set(change.id, {
1044
+ width: Math.round(change.dimensions.width),
1045
+ height: Math.round(change.dimensions.height),
1046
+ });
1047
+ }
1048
+ }
1049
+ return { ...prev, dimensionChanges: newDimensions };
1050
+ });
1051
+ }
1052
+
933
1053
  if (positionChanges.length > 0) {
934
1054
  updateEditState((prev) => {
935
1055
  const newPositions = new Map(prev.positionChanges);
@@ -1063,8 +1183,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1063
1183
  onReconnectStart={handleReconnectStart}
1064
1184
  onReconnect={handleReconnect}
1065
1185
  onReconnectEnd={handleReconnectEnd}
1066
- panOnDrag
1186
+ onPaneClick={onPaneClick}
1187
+ onSelectionChange={handleSelectionChange}
1188
+ panOnDrag={true}
1067
1189
  selectionOnDrag={false}
1190
+ selectionKeyCode={editable ? 'Shift' : null}
1191
+ multiSelectionKeyCode="Shift"
1068
1192
  >
1069
1193
  {showBackground && <Background color={theme.colors.border} gap={16} size={1} />}
1070
1194
  {showControls && <Controls showZoom showFitView showInteractive />}
@@ -1081,7 +1205,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1081
1205
  )}
1082
1206
  </ReactFlow>
1083
1207
 
1084
- {selectedEdge && selectedEdgeTypeDefinition && (
1208
+ {/* Multi-selection sidebar - shown when 2+ nodes are selected */}
1209
+ {selectedNodeIds.size >= 2 && (
1210
+ <SelectionSidebar
1211
+ selectedNodeIds={selectedNodeIds}
1212
+ nodes={nodes}
1213
+ nodeTypeDefinitions={configuration.nodeTypes}
1214
+ onClose={onCloseNodeInfoPanel}
1215
+ />
1216
+ )}
1217
+
1218
+ {/* Single edge info panel - hidden when multiple edges selected */}
1219
+ {selectedEdgeIds.size === 1 && selectedEdge && selectedEdgeTypeDefinition && (
1085
1220
  <EdgeInfoPanel
1086
1221
  edge={selectedEdge}
1087
1222
  typeDefinition={selectedEdgeTypeDefinition}
@@ -1093,7 +1228,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1093
1228
  />
1094
1229
  )}
1095
1230
 
1096
- {selectedNode && selectedNodeTypeDefinition && (
1231
+ {/* Single node info panel - hidden when multiple nodes selected */}
1232
+ {selectedNodeIds.size === 1 && selectedNode && selectedNodeTypeDefinition && (
1097
1233
  <NodeInfoPanel
1098
1234
  node={selectedNode}
1099
1235
  typeDefinition={selectedNodeTypeDefinition}
@@ -1382,6 +1518,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
1382
1518
  position,
1383
1519
  })
1384
1520
  ),
1521
+ dimensionChanges: Array.from(state.dimensionChanges.entries()).map(
1522
+ ([nodeId, dimensions]) => ({
1523
+ nodeId,
1524
+ dimensions,
1525
+ })
1526
+ ),
1385
1527
  nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
1386
1528
  nodeId,
1387
1529
  updates,
@@ -1397,6 +1539,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
1397
1539
  deletedEdges: state.deletedEdges.map((e) => ({ from: e.from, to: e.to, type: e.type })),
1398
1540
  hasChanges:
1399
1541
  state.positionChanges.size > 0 ||
1542
+ state.dimensionChanges.size > 0 ||
1400
1543
  state.nodeUpdates.size > 0 ||
1401
1544
  state.deletedNodeIds.size > 0 ||
1402
1545
  state.createdEdges.length > 0 ||
@@ -1410,6 +1553,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
1410
1553
  const state = editStateRef.current;
1411
1554
  return (
1412
1555
  state.positionChanges.size > 0 ||
1556
+ state.dimensionChanges.size > 0 ||
1413
1557
  state.nodeUpdates.size > 0 ||
1414
1558
  state.deletedNodeIds.size > 0 ||
1415
1559
  state.createdEdges.length > 0 ||