@principal-ai/principal-view-react 0.6.10 → 0.6.12

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 (66) hide show
  1. package/README.md +2 -5
  2. package/dist/components/ConfigurationSelector.js +4 -2
  3. package/dist/components/ConfigurationSelector.js.map +1 -1
  4. package/dist/components/EdgeInfoPanel.d.ts.map +1 -1
  5. package/dist/components/EdgeInfoPanel.js +43 -13
  6. package/dist/components/EdgeInfoPanel.js.map +1 -1
  7. package/dist/components/GraphRenderer.d.ts.map +1 -1
  8. package/dist/components/GraphRenderer.js +133 -83
  9. package/dist/components/GraphRenderer.js.map +1 -1
  10. package/dist/components/NodeInfoPanel.d.ts.map +1 -1
  11. package/dist/components/NodeInfoPanel.js +143 -45
  12. package/dist/components/NodeInfoPanel.js.map +1 -1
  13. package/dist/edges/CustomEdge.d.ts +1 -0
  14. package/dist/edges/CustomEdge.d.ts.map +1 -1
  15. package/dist/edges/CustomEdge.js +18 -4
  16. package/dist/edges/CustomEdge.js.map +1 -1
  17. package/dist/edges/GenericEdge.d.ts.map +1 -1
  18. package/dist/edges/GenericEdge.js +2 -2
  19. package/dist/edges/GenericEdge.js.map +1 -1
  20. package/dist/hooks/usePathBasedEvents.d.ts +1 -1
  21. package/dist/hooks/usePathBasedEvents.d.ts.map +1 -1
  22. package/dist/hooks/usePathBasedEvents.js +9 -9
  23. package/dist/hooks/usePathBasedEvents.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/nodes/CustomNode.d.ts.map +1 -1
  28. package/dist/nodes/CustomNode.js +62 -45
  29. package/dist/nodes/CustomNode.js.map +1 -1
  30. package/dist/nodes/GenericNode.d.ts.map +1 -1
  31. package/dist/nodes/GenericNode.js.map +1 -1
  32. package/dist/utils/animationMapping.d.ts.map +1 -1
  33. package/dist/utils/animationMapping.js +12 -12
  34. package/dist/utils/animationMapping.js.map +1 -1
  35. package/dist/utils/graphConverter.d.ts.map +1 -1
  36. package/dist/utils/graphConverter.js +47 -19
  37. package/dist/utils/graphConverter.js.map +1 -1
  38. package/dist/utils/iconResolver.d.ts.map +1 -1
  39. package/dist/utils/iconResolver.js +1 -1
  40. package/dist/utils/iconResolver.js.map +1 -1
  41. package/package.json +2 -1
  42. package/src/components/ConfigurationSelector.tsx +5 -5
  43. package/src/components/EdgeInfoPanel.tsx +79 -37
  44. package/src/components/GraphRenderer.tsx +526 -365
  45. package/src/components/NodeInfoPanel.tsx +209 -86
  46. package/src/edges/CustomEdge.tsx +40 -7
  47. package/src/edges/GenericEdge.tsx +2 -6
  48. package/src/hooks/usePathBasedEvents.ts +54 -45
  49. package/src/index.ts +11 -2
  50. package/src/nodes/CustomNode.tsx +137 -109
  51. package/src/nodes/GenericNode.tsx +4 -3
  52. package/src/stories/AnimationWorkshop.stories.tsx +131 -12
  53. package/src/stories/CanvasEdgeTypes.stories.tsx +980 -0
  54. package/src/stories/CanvasNodeTypes.stories.tsx +898 -0
  55. package/src/stories/ColorPriority.stories.tsx +20 -10
  56. package/src/stories/EventDrivenAnimations.stories.tsx +8 -0
  57. package/src/stories/EventLog.stories.tsx +1 -1
  58. package/src/stories/GraphRenderer.stories.tsx +23 -10
  59. package/src/stories/IndustryThemes.stories.tsx +481 -0
  60. package/src/stories/MultiConfig.stories.tsx +8 -0
  61. package/src/stories/MultiDirectionalConnections.stories.tsx +8 -0
  62. package/src/stories/NodeFieldsAudit.stories.tsx +124 -37
  63. package/src/stories/NodeShapes.stories.tsx +73 -59
  64. package/src/utils/animationMapping.ts +19 -23
  65. package/src/utils/graphConverter.ts +61 -21
  66. package/src/utils/iconResolver.tsx +5 -1
@@ -1,4 +1,12 @@
1
- import React, { useMemo, useState, useEffect, useCallback, useRef, useImperativeHandle, forwardRef } from 'react';
1
+ import React, {
2
+ useMemo,
3
+ useState,
4
+ useEffect,
5
+ useCallback,
6
+ useRef,
7
+ useImperativeHandle,
8
+ forwardRef,
9
+ } from 'react';
2
10
  import {
3
11
  ReactFlow,
4
12
  Background,
@@ -13,13 +21,26 @@ import {
13
21
  type Connection,
14
22
  } from '@xyflow/react';
15
23
  import '@xyflow/react/dist/style.css';
16
- import type { GraphConfiguration, NodeState, EdgeState, Violation, GraphEvent, ExtendedCanvas, ComponentLibrary } from '@principal-ai/principal-view-core';
24
+ import type {
25
+ GraphConfiguration,
26
+ NodeState,
27
+ EdgeState,
28
+ Violation,
29
+ GraphEvent,
30
+ ExtendedCanvas,
31
+ ComponentLibrary,
32
+ } from '@principal-ai/principal-view-core';
17
33
  import { CanvasConverter } from '@principal-ai/principal-view-core';
34
+ import { useTheme } from '@principal-ade/industry-theme';
18
35
  import { CustomNode } from '../nodes/CustomNode';
19
36
  import type { CustomNodeData } from '../nodes/CustomNode';
20
37
  import { CustomEdge } from '../edges/CustomEdge';
21
38
  import type { CustomEdgeData } from '../edges/CustomEdge';
22
- import { convertToXYFlowNodes, convertToXYFlowEdges, autoLayoutNodes } from '../utils/graphConverter';
39
+ import {
40
+ convertToXYFlowNodes,
41
+ convertToXYFlowEdges,
42
+ autoLayoutNodes,
43
+ } from '../utils/graphConverter';
23
44
  import { EdgeInfoPanel } from './EdgeInfoPanel';
24
45
  import { NodeInfoPanel } from './NodeInfoPanel';
25
46
 
@@ -34,11 +55,20 @@ export interface PendingChanges {
34
55
  /** Node position changes */
35
56
  positionChanges: NodePositionChange[];
36
57
  /** Node updates (type, data changes) */
37
- nodeUpdates: Array<{ nodeId: string; updates: { type?: string; data?: Record<string, unknown> } }>;
58
+ nodeUpdates: Array<{
59
+ nodeId: string;
60
+ updates: { type?: string; data?: Record<string, unknown> };
61
+ }>;
38
62
  /** Deleted node IDs */
39
63
  deletedNodeIds: string[];
40
64
  /** New edges created (with optional handle info for connection points) */
41
- createdEdges: Array<{ from: string; to: string; type: string; sourceHandle?: string; targetHandle?: string }>;
65
+ createdEdges: Array<{
66
+ from: string;
67
+ to: string;
68
+ type: string;
69
+ sourceHandle?: string;
70
+ targetHandle?: string;
71
+ }>;
42
72
  /** Deleted edges (with full connection info for config removal) */
43
73
  deletedEdges: Array<{ from: string; to: string; type: string }>;
44
74
  /** Whether there are any changes */
@@ -127,8 +157,19 @@ const edgeTypes = {
127
157
 
128
158
  // Animation state for nodes and edges
129
159
  interface AnimationState {
130
- nodeAnimations: Record<string, { type: 'pulse' | 'flash' | 'shake' | 'entry'; duration: number; timestamp: number }>;
131
- edgeAnimations: Record<string, { type: 'flow' | 'particle' | 'pulse' | 'glow'; duration: number; direction?: 'forward' | 'backward' | 'bidirectional'; timestamp: number }>;
160
+ nodeAnimations: Record<
161
+ string,
162
+ { type: 'pulse' | 'flash' | 'shake' | 'entry'; duration: number; timestamp: number }
163
+ >;
164
+ edgeAnimations: Record<
165
+ string,
166
+ {
167
+ type: 'flow' | 'particle' | 'pulse' | 'glow';
168
+ duration: number;
169
+ direction?: 'forward' | 'backward' | 'bidirectional';
170
+ timestamp: number;
171
+ }
172
+ >;
132
173
  }
133
174
 
134
175
  // Internal edit state tracking
@@ -136,7 +177,14 @@ interface EditState {
136
177
  positionChanges: Map<string, { x: number; y: number }>;
137
178
  nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
138
179
  deletedNodeIds: Set<string>;
139
- createdEdges: Array<{ id: string; from: string; to: string; type: string; sourceHandle?: string; targetHandle?: string }>;
180
+ createdEdges: Array<{
181
+ id: string;
182
+ from: string;
183
+ to: string;
184
+ type: string;
185
+ sourceHandle?: string;
186
+ targetHandle?: string;
187
+ }>;
140
188
  deletedEdges: Array<{ id: string; from: string; to: string; type: string }>;
141
189
  }
142
190
 
@@ -186,6 +234,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
186
234
  editStateRef,
187
235
  }) => {
188
236
  const { fitView } = useReactFlow();
237
+ const { theme } = useTheme();
189
238
 
190
239
  // Track active animations
191
240
  const [animationState, setAnimationState] = useState<AnimationState>({
@@ -217,14 +266,30 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
217
266
  const [localEdges, setLocalEdges] = useState<EdgeState[]>(propEdges);
218
267
 
219
268
  // Track the prop values to detect external changes
220
- const propNodesKeyRef = useRef(propNodes.map(n => n.id).sort().join(','));
221
- const propEdgesKeyRef = useRef(propEdges.map(e => e.id).sort().join(','));
269
+ const propNodesKeyRef = useRef(
270
+ propNodes
271
+ .map((n) => n.id)
272
+ .sort()
273
+ .join(',')
274
+ );
275
+ const propEdgesKeyRef = useRef(
276
+ propEdges
277
+ .map((e) => e.id)
278
+ .sort()
279
+ .join(',')
280
+ );
222
281
 
223
282
  // Sync local state with props when props change (e.g., config reload)
224
283
  // This only happens when the structure changes, not during editing
225
284
  useEffect(() => {
226
- const newNodesKey = propNodes.map(n => n.id).sort().join(',');
227
- const newEdgesKey = propEdges.map(e => e.id).sort().join(',');
285
+ const newNodesKey = propNodes
286
+ .map((n) => n.id)
287
+ .sort()
288
+ .join(',');
289
+ const newEdgesKey = propEdges
290
+ .map((e) => e.id)
291
+ .sort()
292
+ .join(',');
228
293
 
229
294
  if (newNodesKey !== propNodesKeyRef.current || newEdgesKey !== propEdgesKeyRef.current) {
230
295
  propNodesKeyRef.current = newNodesKey;
@@ -245,20 +310,25 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
245
310
 
246
311
  // Helper to check if there are pending changes
247
312
  const checkHasChanges = useCallback((state: EditState): boolean => {
248
- return state.positionChanges.size > 0 ||
313
+ return (
314
+ state.positionChanges.size > 0 ||
249
315
  state.nodeUpdates.size > 0 ||
250
316
  state.deletedNodeIds.size > 0 ||
251
317
  state.createdEdges.length > 0 ||
252
- state.deletedEdges.length > 0;
318
+ state.deletedEdges.length > 0
319
+ );
253
320
  }, []);
254
321
 
255
322
  // Helper to update edit state and notify parent
256
- const updateEditState = useCallback((updater: (prev: EditState) => EditState) => {
257
- const newState = updater(editStateRef.current);
258
- editStateRef.current = newState;
259
- onEditStateChange?.(newState);
260
- onPendingChangesChange?.(checkHasChanges(newState));
261
- }, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
323
+ const updateEditState = useCallback(
324
+ (updater: (prev: EditState) => EditState) => {
325
+ const newState = updater(editStateRef.current);
326
+ editStateRef.current = newState;
327
+ onEditStateChange?.(newState);
328
+ onPendingChangesChange?.(checkHasChanges(newState));
329
+ },
330
+ [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]
331
+ );
262
332
 
263
333
  // ============================================
264
334
  // EVENT HANDLERS
@@ -287,180 +357,210 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
287
357
  }, []);
288
358
 
289
359
  // Handle node update (internal - updates local state only)
290
- const handleNodeUpdate = useCallback((nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => {
291
- if (!editable) return;
360
+ const handleNodeUpdate = useCallback(
361
+ (nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => {
362
+ if (!editable) return;
363
+
364
+ // Update local nodes
365
+ setLocalNodes((prev) =>
366
+ prev.map((node) => {
367
+ if (node.id === nodeId) {
368
+ return {
369
+ ...node,
370
+ type: updates.type ?? node.type,
371
+ data: updates.data ? { ...node.data, ...updates.data } : node.data,
372
+ };
373
+ }
374
+ return node;
375
+ })
376
+ );
292
377
 
293
- // Update local nodes
294
- setLocalNodes(prev => prev.map(node => {
295
- if (node.id === nodeId) {
296
- return {
297
- ...node,
298
- type: updates.type ?? node.type,
299
- data: updates.data ? { ...node.data, ...updates.data } : node.data,
300
- };
301
- }
302
- return node;
303
- }));
304
-
305
- // Track the change
306
- updateEditState(prev => {
307
- const newUpdates = new Map(prev.nodeUpdates);
308
- const existing = newUpdates.get(nodeId) || {};
309
- newUpdates.set(nodeId, {
310
- type: updates.type ?? existing.type,
311
- data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
378
+ // Track the change
379
+ updateEditState((prev) => {
380
+ const newUpdates = new Map(prev.nodeUpdates);
381
+ const existing = newUpdates.get(nodeId) || {};
382
+ newUpdates.set(nodeId, {
383
+ type: updates.type ?? existing.type,
384
+ data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
385
+ });
386
+ return { ...prev, nodeUpdates: newUpdates };
312
387
  });
313
- return { ...prev, nodeUpdates: newUpdates };
314
- });
315
- }, [editable, updateEditState]);
388
+ },
389
+ [editable, updateEditState]
390
+ );
316
391
 
317
392
  // Handle node delete (internal)
318
- const handleNodeDelete = useCallback((nodeId: string) => {
319
- if (!editable) return;
320
-
321
- // Remove from local state
322
- setLocalNodes(prev => prev.filter(n => n.id !== nodeId));
323
- setLocalEdges(prev => prev.filter(e => e.from !== nodeId && e.to !== nodeId));
324
-
325
- // Track the change
326
- updateEditState(prev => {
327
- const newDeletedNodes = new Set(prev.deletedNodeIds);
328
- newDeletedNodes.add(nodeId);
329
- // Remove any pending updates for this node
330
- const newUpdates = new Map(prev.nodeUpdates);
331
- newUpdates.delete(nodeId);
332
- // Remove any position changes for this node
333
- const newPositions = new Map(prev.positionChanges);
334
- newPositions.delete(nodeId);
335
- // Remove created edges that involve this node
336
- const newCreatedEdges = prev.createdEdges.filter(e => e.from !== nodeId && e.to !== nodeId);
337
- return {
338
- ...prev,
339
- deletedNodeIds: newDeletedNodes,
340
- nodeUpdates: newUpdates,
341
- positionChanges: newPositions,
342
- createdEdges: newCreatedEdges,
343
- };
344
- });
393
+ const handleNodeDelete = useCallback(
394
+ (nodeId: string) => {
395
+ if (!editable) return;
396
+
397
+ // Remove from local state
398
+ setLocalNodes((prev) => prev.filter((n) => n.id !== nodeId));
399
+ setLocalEdges((prev) => prev.filter((e) => e.from !== nodeId && e.to !== nodeId));
400
+
401
+ // Track the change
402
+ updateEditState((prev) => {
403
+ const newDeletedNodes = new Set(prev.deletedNodeIds);
404
+ newDeletedNodes.add(nodeId);
405
+ // Remove any pending updates for this node
406
+ const newUpdates = new Map(prev.nodeUpdates);
407
+ newUpdates.delete(nodeId);
408
+ // Remove any position changes for this node
409
+ const newPositions = new Map(prev.positionChanges);
410
+ newPositions.delete(nodeId);
411
+ // Remove created edges that involve this node
412
+ const newCreatedEdges = prev.createdEdges.filter(
413
+ (e) => e.from !== nodeId && e.to !== nodeId
414
+ );
415
+ return {
416
+ ...prev,
417
+ deletedNodeIds: newDeletedNodes,
418
+ nodeUpdates: newUpdates,
419
+ positionChanges: newPositions,
420
+ createdEdges: newCreatedEdges,
421
+ };
422
+ });
345
423
 
346
- setSelectedNodeId(null);
347
- }, [editable, updateEditState]);
424
+ setSelectedNodeId(null);
425
+ },
426
+ [editable, updateEditState]
427
+ );
348
428
 
349
429
  // Handle edge delete (internal)
350
- const handleEdgeDelete = useCallback((edgeId: string) => {
351
- if (!editable) return;
352
-
353
- // Find the edge before removing it so we can track its full info
354
- const edgeToDelete = localEdges.find(e => e.id === edgeId);
355
-
356
- // Remove from local state
357
- setLocalEdges(prev => prev.filter(e => e.id !== edgeId));
358
-
359
- // Track the change
360
- updateEditState(prev => {
361
- // Check if this was a newly created edge
362
- const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === edgeId);
363
- if (createdEdgeIndex >= 0) {
364
- // Just remove it from created edges
365
- const newCreatedEdges = [...prev.createdEdges];
366
- newCreatedEdges.splice(createdEdgeIndex, 1);
367
- return { ...prev, createdEdges: newCreatedEdges };
368
- }
369
- // Otherwise mark as deleted with full edge info
370
- if (edgeToDelete) {
371
- const newDeletedEdges = [...prev.deletedEdges, {
372
- id: edgeId,
373
- from: edgeToDelete.from,
374
- to: edgeToDelete.to,
375
- type: edgeToDelete.type
376
- }];
377
- return { ...prev, deletedEdges: newDeletedEdges };
378
- }
379
- return prev;
380
- });
430
+ const handleEdgeDelete = useCallback(
431
+ (edgeId: string) => {
432
+ if (!editable) return;
433
+
434
+ // Find the edge before removing it so we can track its full info
435
+ const edgeToDelete = localEdges.find((e) => e.id === edgeId);
436
+
437
+ // Remove from local state
438
+ setLocalEdges((prev) => prev.filter((e) => e.id !== edgeId));
439
+
440
+ // Track the change
441
+ updateEditState((prev) => {
442
+ // Check if this was a newly created edge
443
+ const createdEdgeIndex = prev.createdEdges.findIndex((e) => e.id === edgeId);
444
+ if (createdEdgeIndex >= 0) {
445
+ // Just remove it from created edges
446
+ const newCreatedEdges = [...prev.createdEdges];
447
+ newCreatedEdges.splice(createdEdgeIndex, 1);
448
+ return { ...prev, createdEdges: newCreatedEdges };
449
+ }
450
+ // Otherwise mark as deleted with full edge info
451
+ if (edgeToDelete) {
452
+ const newDeletedEdges = [
453
+ ...prev.deletedEdges,
454
+ {
455
+ id: edgeId,
456
+ from: edgeToDelete.from,
457
+ to: edgeToDelete.to,
458
+ type: edgeToDelete.type,
459
+ },
460
+ ];
461
+ return { ...prev, deletedEdges: newDeletedEdges };
462
+ }
463
+ return prev;
464
+ });
381
465
 
382
- setSelectedEdgeId(null);
383
- }, [editable, updateEditState, localEdges]);
466
+ setSelectedEdgeId(null);
467
+ },
468
+ [editable, updateEditState, localEdges]
469
+ );
384
470
 
385
471
  // Handle new connection from drag
386
- const handleConnect = useCallback((connection: Connection) => {
387
- if (!editable || !connection.source || !connection.target) return;
388
-
389
- // Find source and target node types
390
- const sourceNode = nodes.find(n => n.id === connection.source);
391
- const targetNode = nodes.find(n => n.id === connection.target);
392
- if (!sourceNode || !targetNode) return;
393
-
394
- // Find valid edge types for this connection
395
- const validTypes = configuration.allowedConnections
396
- .filter(ac => ac.from === sourceNode.type && ac.to === targetNode.type)
397
- .map(ac => ac.via);
398
-
399
- const uniqueTypes = [...new Set(validTypes)];
400
-
401
- if (uniqueTypes.length === 0) {
402
- console.warn(`No valid edge types for connection from ${sourceNode.type} to ${targetNode.type}`);
403
- return;
404
- }
472
+ const handleConnect = useCallback(
473
+ (connection: Connection) => {
474
+ if (!editable || !connection.source || !connection.target) return;
475
+
476
+ // Find source and target node types
477
+ const sourceNode = nodes.find((n) => n.id === connection.source);
478
+ const targetNode = nodes.find((n) => n.id === connection.target);
479
+ if (!sourceNode || !targetNode) return;
480
+
481
+ // Find valid edge types for this connection
482
+ const validTypes = configuration.allowedConnections
483
+ .filter((ac) => ac.from === sourceNode.type && ac.to === targetNode.type)
484
+ .map((ac) => ac.via);
485
+
486
+ const uniqueTypes = [...new Set(validTypes)];
487
+
488
+ if (uniqueTypes.length === 0) {
489
+ console.warn(
490
+ `No valid edge types for connection from ${sourceNode.type} to ${targetNode.type}`
491
+ );
492
+ return;
493
+ }
405
494
 
406
- if (uniqueTypes.length === 1) {
407
- // Create edge immediately with handle information
408
- createEdge(
409
- connection.source,
410
- connection.target,
411
- uniqueTypes[0],
412
- connection.sourceHandle ?? undefined,
413
- connection.targetHandle ?? undefined
414
- );
415
- } else {
416
- // Show picker
417
- setPendingConnection({
418
- from: connection.source,
419
- to: connection.target,
420
- sourceHandle: connection.sourceHandle ?? undefined,
421
- targetHandle: connection.targetHandle ?? undefined,
422
- validTypes: uniqueTypes,
423
- });
424
- }
425
- }, [editable, nodes, configuration.allowedConnections]);
495
+ if (uniqueTypes.length === 1) {
496
+ // Create edge immediately with handle information
497
+ createEdge(
498
+ connection.source,
499
+ connection.target,
500
+ uniqueTypes[0],
501
+ connection.sourceHandle ?? undefined,
502
+ connection.targetHandle ?? undefined
503
+ );
504
+ } else {
505
+ // Show picker
506
+ setPendingConnection({
507
+ from: connection.source,
508
+ to: connection.target,
509
+ sourceHandle: connection.sourceHandle ?? undefined,
510
+ targetHandle: connection.targetHandle ?? undefined,
511
+ validTypes: uniqueTypes,
512
+ });
513
+ }
514
+ },
515
+ [editable, nodes, configuration.allowedConnections]
516
+ );
426
517
 
427
518
  // Create edge helper
428
- const createEdge = useCallback((from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
429
- const edgeId = `${from}-${to}-${type}-${Date.now()}`;
430
-
431
- // Add to local state with handle information
432
- const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
433
- id: edgeId,
434
- type,
435
- from,
436
- to,
437
- data: {},
438
- createdAt: Date.now(),
439
- updatedAt: Date.now(),
440
- sourceHandle,
441
- targetHandle,
442
- };
443
- setLocalEdges(prev => [...prev, newEdge]);
519
+ const createEdge = useCallback(
520
+ (from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
521
+ const edgeId = `${from}-${to}-${type}-${Date.now()}`;
522
+
523
+ // Add to local state with handle information
524
+ const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
525
+ id: edgeId,
526
+ type,
527
+ from,
528
+ to,
529
+ data: {},
530
+ createdAt: Date.now(),
531
+ updatedAt: Date.now(),
532
+ sourceHandle,
533
+ targetHandle,
534
+ };
535
+ setLocalEdges((prev) => [...prev, newEdge]);
444
536
 
445
- // Track the change
446
- updateEditState(prev => ({
447
- ...prev,
448
- createdEdges: [...prev.createdEdges, { id: edgeId, from, to, type, sourceHandle, targetHandle }],
449
- }));
450
- }, [updateEditState]);
537
+ // Track the change
538
+ updateEditState((prev) => ({
539
+ ...prev,
540
+ createdEdges: [
541
+ ...prev.createdEdges,
542
+ { id: edgeId, from, to, type, sourceHandle, targetHandle },
543
+ ],
544
+ }));
545
+ },
546
+ [updateEditState]
547
+ );
451
548
 
452
549
  // Handle edge type selection from picker
453
- const handleEdgeTypeSelect = useCallback((type: string) => {
454
- if (!pendingConnection) return;
455
- createEdge(
456
- pendingConnection.from,
457
- pendingConnection.to,
458
- type,
459
- pendingConnection.sourceHandle,
460
- pendingConnection.targetHandle
461
- );
462
- setPendingConnection(null);
463
- }, [pendingConnection, createEdge]);
550
+ const handleEdgeTypeSelect = useCallback(
551
+ (type: string) => {
552
+ if (!pendingConnection) return;
553
+ createEdge(
554
+ pendingConnection.from,
555
+ pendingConnection.to,
556
+ type,
557
+ pendingConnection.sourceHandle,
558
+ pendingConnection.targetHandle
559
+ );
560
+ setPendingConnection(null);
561
+ },
562
+ [pendingConnection, createEdge]
563
+ );
464
564
 
465
565
  // Cancel edge type picker
466
566
  const handleCancelEdgeTypePicker = useCallback(() => {
@@ -476,84 +576,98 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
476
576
  }, []);
477
577
 
478
578
  // Handle edge reconnection (dragging edge endpoint to new node)
479
- const handleReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
480
- if (!editable || !newConnection.source || !newConnection.target) return;
481
-
482
- // Find the original edge in our local state
483
- const originalEdge = localEdges.find(e => e.id === oldEdge.id);
484
- if (!originalEdge) return;
485
-
486
- // Find source and target node types for validation
487
- const sourceNode = nodes.find(n => n.id === newConnection.source);
488
- const targetNode = nodes.find(n => n.id === newConnection.target);
489
- if (!sourceNode || !targetNode) return;
579
+ const handleReconnect = useCallback(
580
+ (oldEdge: Edge, newConnection: Connection) => {
581
+ if (!editable || !newConnection.source || !newConnection.target) return;
582
+
583
+ // Find the original edge in our local state
584
+ const originalEdge = localEdges.find((e) => e.id === oldEdge.id);
585
+ if (!originalEdge) return;
586
+
587
+ // Find source and target node types for validation
588
+ const sourceNode = nodes.find((n) => n.id === newConnection.source);
589
+ const targetNode = nodes.find((n) => n.id === newConnection.target);
590
+ if (!sourceNode || !targetNode) return;
591
+
592
+ // Check if the new connection is valid for this edge type
593
+ const isValidConnection = configuration.allowedConnections.some(
594
+ (ac) =>
595
+ ac.from === sourceNode.type && ac.to === targetNode.type && ac.via === originalEdge.type
596
+ );
490
597
 
491
- // Check if the new connection is valid for this edge type
492
- const isValidConnection = configuration.allowedConnections.some(
493
- ac => ac.from === sourceNode.type && ac.to === targetNode.type && ac.via === originalEdge.type
494
- );
598
+ if (!isValidConnection) {
599
+ console.warn(
600
+ `Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.type} to ${targetNode.type}`
601
+ );
602
+ return;
603
+ }
495
604
 
496
- if (!isValidConnection) {
497
- console.warn(`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.type} to ${targetNode.type}`);
498
- return;
499
- }
605
+ // Mark as successful before updating
606
+ edgeReconnectSuccessful.current = true;
607
+
608
+ // Update local edges - manually update the edge to preserve its type and id
609
+ setLocalEdges((prev) =>
610
+ prev.map((edge) => {
611
+ if (edge.id === oldEdge.id) {
612
+ return {
613
+ ...edge,
614
+ from: newConnection.source!,
615
+ to: newConnection.target!,
616
+ sourceHandle: newConnection.sourceHandle ?? undefined,
617
+ targetHandle: newConnection.targetHandle ?? undefined,
618
+ updatedAt: Date.now(),
619
+ };
620
+ }
621
+ return edge;
622
+ })
623
+ );
500
624
 
501
- // Mark as successful before updating
502
- edgeReconnectSuccessful.current = true;
625
+ // Track the change - remove old edge and add new one
626
+ updateEditState((prev) => {
627
+ // Check if this was a newly created edge
628
+ const createdEdgeIndex = prev.createdEdges.findIndex((e) => e.id === oldEdge.id);
629
+
630
+ if (createdEdgeIndex >= 0) {
631
+ // Update the created edge entry
632
+ const newCreatedEdges = [...prev.createdEdges];
633
+ newCreatedEdges[createdEdgeIndex] = {
634
+ ...newCreatedEdges[createdEdgeIndex],
635
+ from: newConnection.source!,
636
+ to: newConnection.target!,
637
+ sourceHandle: newConnection.sourceHandle ?? undefined,
638
+ targetHandle: newConnection.targetHandle ?? undefined,
639
+ };
640
+ return { ...prev, createdEdges: newCreatedEdges };
641
+ }
503
642
 
504
- // Update local edges - manually update the edge to preserve its type and id
505
- setLocalEdges(prev => prev.map(edge => {
506
- if (edge.id === oldEdge.id) {
507
- return {
508
- ...edge,
509
- from: newConnection.source!,
510
- to: newConnection.target!,
511
- sourceHandle: newConnection.sourceHandle ?? undefined,
512
- targetHandle: newConnection.targetHandle ?? undefined,
513
- updatedAt: Date.now(),
514
- };
515
- }
516
- return edge;
517
- }));
518
-
519
- // Track the change - remove old edge and add new one
520
- updateEditState(prev => {
521
- // Check if this was a newly created edge
522
- const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === oldEdge.id);
523
-
524
- if (createdEdgeIndex >= 0) {
525
- // Update the created edge entry
526
- const newCreatedEdges = [...prev.createdEdges];
527
- newCreatedEdges[createdEdgeIndex] = {
528
- ...newCreatedEdges[createdEdgeIndex],
529
- from: newConnection.source!,
530
- to: newConnection.target!,
531
- sourceHandle: newConnection.sourceHandle ?? undefined,
532
- targetHandle: newConnection.targetHandle ?? undefined,
533
- };
534
- return { ...prev, createdEdges: newCreatedEdges };
535
- }
643
+ // For existing edges, track as delete + create
644
+ const newDeletedEdges = [
645
+ ...prev.deletedEdges,
646
+ {
647
+ id: oldEdge.id,
648
+ from: originalEdge.from,
649
+ to: originalEdge.to,
650
+ type: originalEdge.type,
651
+ },
652
+ ];
653
+
654
+ const newCreatedEdges = [
655
+ ...prev.createdEdges,
656
+ {
657
+ id: oldEdge.id,
658
+ from: newConnection.source!,
659
+ to: newConnection.target!,
660
+ type: originalEdge.type,
661
+ sourceHandle: newConnection.sourceHandle ?? undefined,
662
+ targetHandle: newConnection.targetHandle ?? undefined,
663
+ },
664
+ ];
536
665
 
537
- // For existing edges, track as delete + create
538
- const newDeletedEdges = [...prev.deletedEdges, {
539
- id: oldEdge.id,
540
- from: originalEdge.from,
541
- to: originalEdge.to,
542
- type: originalEdge.type,
543
- }];
544
-
545
- const newCreatedEdges = [...prev.createdEdges, {
546
- id: oldEdge.id,
547
- from: newConnection.source!,
548
- to: newConnection.target!,
549
- type: originalEdge.type,
550
- sourceHandle: newConnection.sourceHandle ?? undefined,
551
- targetHandle: newConnection.targetHandle ?? undefined,
552
- }];
553
-
554
- return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
555
- });
556
- }, [editable, localEdges, nodes, configuration.allowedConnections, updateEditState]);
666
+ return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
667
+ });
668
+ },
669
+ [editable, localEdges, nodes, configuration.allowedConnections, updateEditState]
670
+ );
557
671
 
558
672
  // Called when reconnection ends (whether successful or not)
559
673
  const handleReconnectEnd = useCallback(() => {
@@ -569,7 +683,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
569
683
 
570
684
  const selectedEdge = useMemo(() => {
571
685
  if (!selectedEdgeId) return null;
572
- return edges.find(e => e.id === selectedEdgeId);
686
+ return edges.find((e) => e.id === selectedEdgeId);
573
687
  }, [selectedEdgeId, edges]);
574
688
 
575
689
  const selectedEdgeTypeDefinition = useMemo(() => {
@@ -579,7 +693,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
579
693
 
580
694
  const selectedNode = useMemo(() => {
581
695
  if (!selectedNodeId) return null;
582
- return nodes.find(n => n.id === selectedNodeId);
696
+ return nodes.find((n) => n.id === selectedNodeId);
583
697
  }, [selectedNodeId, nodes]);
584
698
 
585
699
  const selectedNodeTypeDefinition = useMemo(() => {
@@ -602,7 +716,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
602
716
  const animation = edgeEvent.animation;
603
717
 
604
718
  if (animation && edgeId) {
605
- setAnimationState(prev => ({
719
+ setAnimationState((prev) => ({
606
720
  ...prev,
607
721
  edgeAnimations: {
608
722
  ...prev.edgeAnimations,
@@ -617,7 +731,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
617
731
 
618
732
  const duration = animation.duration || 1000;
619
733
  setTimeout(() => {
620
- setAnimationState(prev => {
734
+ setAnimationState((prev) => {
621
735
  const newEdgeAnimations = { ...prev.edgeAnimations };
622
736
  delete newEdgeAnimations[edgeId];
623
737
  return { ...prev, edgeAnimations: newEdgeAnimations };
@@ -635,9 +749,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
635
749
 
636
750
  if (nodeId && newState) {
637
751
  // Update the node's state
638
- setLocalNodes(prev => prev.map(node =>
639
- node.id === nodeId ? { ...node, state: newState } : node
640
- ));
752
+ setLocalNodes((prev) =>
753
+ prev.map((node) => (node.id === nodeId ? { ...node, state: newState } : node))
754
+ );
641
755
 
642
756
  // Trigger animation based on state
643
757
  const stateToAnimation: Record<string, 'pulse' | 'flash' | 'shake'> = {
@@ -648,9 +762,10 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
648
762
 
649
763
  const animationType = stateToAnimation[newState];
650
764
  if (animationType) {
651
- const duration = animationType === 'pulse' ? 1500 : animationType === 'flash' ? 1000 : 500;
765
+ const duration =
766
+ animationType === 'pulse' ? 1500 : animationType === 'flash' ? 1000 : 500;
652
767
 
653
- setAnimationState(prev => ({
768
+ setAnimationState((prev) => ({
654
769
  ...prev,
655
770
  nodeAnimations: {
656
771
  ...prev.nodeAnimations,
@@ -660,7 +775,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
660
775
 
661
776
  if (animationType !== 'pulse') {
662
777
  setTimeout(() => {
663
- setAnimationState(prev => {
778
+ setAnimationState((prev) => {
664
779
  const newNodeAnimations = { ...prev.nodeAnimations };
665
780
  delete newNodeAnimations[nodeId];
666
781
  return { ...prev, nodeAnimations: newNodeAnimations };
@@ -678,7 +793,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
678
793
  const nodeId = nodeEvent.nodeId;
679
794
 
680
795
  if (nodeId) {
681
- setAnimationState(prev => ({
796
+ setAnimationState((prev) => ({
682
797
  ...prev,
683
798
  nodeAnimations: {
684
799
  ...prev.nodeAnimations,
@@ -687,7 +802,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
687
802
  }));
688
803
 
689
804
  setTimeout(() => {
690
- setAnimationState(prev => {
805
+ setAnimationState((prev) => {
691
806
  const newNodeAnimations = { ...prev.nodeAnimations };
692
807
  delete newNodeAnimations[nodeId];
693
808
  return { ...prev, nodeAnimations: newNodeAnimations };
@@ -708,7 +823,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
708
823
  const layoutType = configuration.display?.layout || 'hierarchical';
709
824
  const positioned = autoLayoutNodes(converted, [], layoutType);
710
825
 
711
- return positioned.map(node => {
826
+ return positioned.map((node) => {
712
827
  const animation = animationState.nodeAnimations[node.id];
713
828
  // Apply any pending position changes
714
829
  const pendingPosition = editStateRef.current.positionChanges.get(node.id);
@@ -718,17 +833,22 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
718
833
  data: {
719
834
  ...node.data,
720
835
  editable,
721
- ...(animation ? {
722
- animationType: animation.type,
723
- animationDuration: animation.duration,
724
- } : {}),
836
+ ...(animation
837
+ ? {
838
+ animationType: animation.type,
839
+ animationDuration: animation.duration,
840
+ }
841
+ : {}),
725
842
  } as CustomNodeData,
726
843
  };
727
844
  });
728
845
  }, [nodes, configuration, violations, animationState.nodeAnimations, editable, editStateRef]);
729
846
 
730
847
  const baseNodesKey = useMemo(() => {
731
- return nodes.map(n => n.id).sort().join(',');
848
+ return nodes
849
+ .map((n) => n.id)
850
+ .sort()
851
+ .join(',');
732
852
  }, [nodes]);
733
853
 
734
854
  // Local xyflow nodes state for dragging
@@ -756,43 +876,48 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
756
876
  const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
757
877
 
758
878
  // Handle node changes (drag events)
759
- const handleNodesChange = useCallback((changes: NodeChange[]) => {
760
- if (!editable) return;
761
-
762
- setXyflowLocalNodes(nds => applyNodeChanges(changes, nds) as Node<CustomNodeData>[]);
763
-
764
- // Track position changes on drag end
765
- const positionChanges = changes
766
- .filter((change): change is NodeChange & {
767
- type: 'position';
768
- position: { x: number; y: number };
769
- dragging: boolean
770
- } =>
771
- change.type === 'position' &&
772
- 'position' in change &&
773
- change.position !== undefined &&
774
- 'dragging' in change &&
775
- change.dragging === false
879
+ const handleNodesChange = useCallback(
880
+ (changes: NodeChange[]) => {
881
+ if (!editable) return;
882
+
883
+ setXyflowLocalNodes((nds) => applyNodeChanges(changes, nds) as Node<CustomNodeData>[]);
884
+
885
+ // Track position changes on drag end
886
+ const positionChanges = changes.filter(
887
+ (
888
+ change
889
+ ): change is NodeChange & {
890
+ type: 'position';
891
+ position: { x: number; y: number };
892
+ dragging: boolean;
893
+ } =>
894
+ change.type === 'position' &&
895
+ 'position' in change &&
896
+ change.position !== undefined &&
897
+ 'dragging' in change &&
898
+ change.dragging === false
776
899
  );
777
900
 
778
- if (positionChanges.length > 0) {
779
- updateEditState(prev => {
780
- const newPositions = new Map(prev.positionChanges);
781
- for (const change of positionChanges) {
782
- newPositions.set(change.id, {
783
- x: Math.round(change.position.x),
784
- y: Math.round(change.position.y),
785
- });
786
- }
787
- return { ...prev, positionChanges: newPositions };
788
- });
789
- }
790
- }, [editable, updateEditState]);
901
+ if (positionChanges.length > 0) {
902
+ updateEditState((prev) => {
903
+ const newPositions = new Map(prev.positionChanges);
904
+ for (const change of positionChanges) {
905
+ newPositions.set(change.id, {
906
+ x: Math.round(change.position.x),
907
+ y: Math.round(change.position.y),
908
+ });
909
+ }
910
+ return { ...prev, positionChanges: newPositions };
911
+ });
912
+ }
913
+ },
914
+ [editable, updateEditState]
915
+ );
791
916
 
792
917
  const xyflowEdges = useMemo(() => {
793
918
  const converted = convertToXYFlowEdges(edges, configuration, violations);
794
919
 
795
- return converted.map(edge => {
920
+ return converted.map((edge) => {
796
921
  const animation = animationState.edgeAnimations[edge.id];
797
922
  if (animation) {
798
923
  return {
@@ -854,20 +979,19 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
854
979
  panOnDrag
855
980
  selectionOnDrag={false}
856
981
  >
857
- {showBackground && <Background color="#e5e5e5" gap={16} size={1} />}
982
+ {showBackground && <Background color={theme.colors.border} gap={16} size={1} />}
858
983
  {showControls && <Controls showZoom showFitView showInteractive />}
859
984
  {showMinimap && (
860
985
  <MiniMap
861
986
  nodeColor={(node) => {
862
987
  const nodeData = node.data as CustomNodeData;
863
- return nodeData?.typeDefinition?.color || '#888';
988
+ return nodeData?.typeDefinition?.color || theme.colors.secondary;
864
989
  }}
865
990
  nodeBorderRadius={2}
866
991
  pannable
867
992
  zoomable
868
993
  />
869
994
  )}
870
-
871
995
  </ReactFlow>
872
996
 
873
997
  {selectedEdge && selectedEdgeTypeDefinition && (
@@ -899,22 +1023,24 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
899
1023
  top: '50%',
900
1024
  left: '50%',
901
1025
  transform: 'translate(-50%, -50%)',
902
- backgroundColor: 'white',
1026
+ backgroundColor: theme.colors.background,
1027
+ color: theme.colors.text,
903
1028
  borderRadius: '8px',
904
1029
  boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
905
1030
  padding: '16px',
906
1031
  minWidth: '200px',
907
1032
  zIndex: 1000,
1033
+ border: `1px solid ${theme.colors.border}`,
908
1034
  }}
909
1035
  >
910
1036
  <div style={{ fontWeight: 'bold', marginBottom: '12px', fontSize: '14px' }}>
911
1037
  Select Edge Type
912
1038
  </div>
913
- <div style={{ fontSize: '12px', color: '#666', marginBottom: '12px' }}>
1039
+ <div style={{ fontSize: '12px', color: theme.colors.textSecondary, marginBottom: '12px' }}>
914
1040
  {pendingConnection.from} → {pendingConnection.to}
915
1041
  </div>
916
1042
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
917
- {pendingConnection.validTypes.map(type => {
1043
+ {pendingConnection.validTypes.map((type) => {
918
1044
  const typeDefinition = configuration.edgeTypes[type];
919
1045
  return (
920
1046
  <button
@@ -922,8 +1048,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
922
1048
  onClick={() => handleEdgeTypeSelect(type)}
923
1049
  style={{
924
1050
  padding: '8px 12px',
925
- backgroundColor: typeDefinition?.color || '#888',
926
- color: 'white',
1051
+ backgroundColor: typeDefinition?.color || theme.colors.secondary,
1052
+ color: theme.colors.background,
927
1053
  border: 'none',
928
1054
  borderRadius: '4px',
929
1055
  cursor: 'pointer',
@@ -943,9 +1069,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
943
1069
  marginTop: '12px',
944
1070
  width: '100%',
945
1071
  padding: '8px 12px',
946
- backgroundColor: '#f0f0f0',
947
- color: '#666',
948
- border: 'none',
1072
+ backgroundColor: theme.colors.surface,
1073
+ color: theme.colors.textSecondary,
1074
+ border: `1px solid ${theme.colors.border}`,
949
1075
  borderRadius: '4px',
950
1076
  cursor: 'pointer',
951
1077
  fontSize: '12px',
@@ -962,7 +1088,10 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
962
1088
  /**
963
1089
  * Convert canvas to legacy configuration format for internal use
964
1090
  */
965
- function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: ComponentLibrary): {
1091
+ function useCanvasToLegacy(
1092
+ canvas: ExtendedCanvas | undefined,
1093
+ library?: ComponentLibrary
1094
+ ): {
966
1095
  configuration: GraphConfiguration;
967
1096
  nodes: NodeState[];
968
1097
  edges: EdgeState[];
@@ -1025,12 +1154,23 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
1025
1154
 
1026
1155
  if (!nodeTypes[nodeType]) {
1027
1156
  // Color priority: vv.fill > node.color > vv.states.idle.color
1028
- const fillColor = vv?.fill
1029
- || (typeof node.color === 'string' ? node.color : undefined)
1030
- || vv?.states?.idle?.color;
1157
+ const fillColor =
1158
+ vv?.fill ||
1159
+ (typeof node.color === 'string' ? node.color : undefined) ||
1160
+ vv?.states?.idle?.color;
1161
+
1162
+ // Derive description from text content (everything after line 1) for text nodes
1163
+ let nodeDescription = `${nodeType} node`;
1164
+ if (node.type === 'text' && 'text' in node) {
1165
+ const lines = node.text.split('\n');
1166
+ const descFromText = lines.slice(1).join('\n').trim();
1167
+ if (descFromText) {
1168
+ nodeDescription = descFromText;
1169
+ }
1170
+ }
1031
1171
 
1032
1172
  nodeTypes[nodeType] = {
1033
- description: vv?.description || `${nodeType} node`,
1173
+ description: nodeDescription,
1034
1174
  shape: vv?.shape || 'rectangle',
1035
1175
  icon: vv?.icon,
1036
1176
  color: fillColor,
@@ -1073,8 +1213,8 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
1073
1213
  }
1074
1214
 
1075
1215
  // Find node types for from/to
1076
- const fromNode = canvas.nodes?.find(n => n.id === edge.fromNode);
1077
- const toNode = canvas.nodes?.find(n => n.id === edge.toNode);
1216
+ const fromNode = canvas.nodes?.find((n) => n.id === edge.fromNode);
1217
+ const toNode = canvas.nodes?.find((n) => n.id === edge.toNode);
1078
1218
  const fromType = fromNode?.pv?.nodeType || edge.fromNode;
1079
1219
  const toType = toNode?.pv?.nodeType || edge.toNode;
1080
1220
 
@@ -1133,66 +1273,87 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
1133
1273
  */
1134
1274
  export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>((props, ref) => {
1135
1275
  const { canvas, library, className, width = '100%', height = '100%' } = props;
1276
+ const { theme } = useTheme();
1136
1277
 
1137
1278
  // Convert canvas to internal format (merging library types if provided)
1138
1279
  const canvasData = useCanvasToLegacy(canvas, library);
1139
1280
 
1281
+ // Internal edit state ref - must be before any conditional returns
1282
+ const editStateRef = useRef<EditState>(createEmptyEditState());
1283
+
1284
+ // Expose imperative handle - must be before any conditional returns
1285
+ useImperativeHandle(
1286
+ ref,
1287
+ () => ({
1288
+ getPendingChanges: (): PendingChanges => {
1289
+ const state = editStateRef.current;
1290
+ return {
1291
+ positionChanges: Array.from(state.positionChanges.entries()).map(
1292
+ ([nodeId, position]) => ({
1293
+ nodeId,
1294
+ position,
1295
+ })
1296
+ ),
1297
+ nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
1298
+ nodeId,
1299
+ updates,
1300
+ })),
1301
+ deletedNodeIds: Array.from(state.deletedNodeIds),
1302
+ createdEdges: state.createdEdges.map((e) => ({
1303
+ from: e.from,
1304
+ to: e.to,
1305
+ type: e.type,
1306
+ sourceHandle: e.sourceHandle,
1307
+ targetHandle: e.targetHandle,
1308
+ })),
1309
+ deletedEdges: state.deletedEdges.map((e) => ({ from: e.from, to: e.to, type: e.type })),
1310
+ hasChanges:
1311
+ state.positionChanges.size > 0 ||
1312
+ state.nodeUpdates.size > 0 ||
1313
+ state.deletedNodeIds.size > 0 ||
1314
+ state.createdEdges.length > 0 ||
1315
+ state.deletedEdges.length > 0,
1316
+ };
1317
+ },
1318
+ resetEditState: () => {
1319
+ editStateRef.current = createEmptyEditState();
1320
+ },
1321
+ hasUnsavedChanges: (): boolean => {
1322
+ const state = editStateRef.current;
1323
+ return (
1324
+ state.positionChanges.size > 0 ||
1325
+ state.nodeUpdates.size > 0 ||
1326
+ state.deletedNodeIds.size > 0 ||
1327
+ state.createdEdges.length > 0 ||
1328
+ state.deletedEdges.length > 0
1329
+ );
1330
+ },
1331
+ }),
1332
+ []
1333
+ );
1334
+
1140
1335
  // Validate we have required data
1141
1336
  if (!canvasData) {
1142
1337
  return (
1143
- <div className={className} style={{ width, height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
1144
- <p style={{ color: '#666' }}>No canvas data provided.</p>
1338
+ <div
1339
+ className={className}
1340
+ style={{
1341
+ width,
1342
+ height,
1343
+ display: 'flex',
1344
+ alignItems: 'center',
1345
+ justifyContent: 'center',
1346
+ backgroundColor: theme.colors.background,
1347
+ color: theme.colors.textSecondary,
1348
+ }}
1349
+ >
1350
+ <p>No canvas data provided.</p>
1145
1351
  </div>
1146
1352
  );
1147
1353
  }
1148
1354
 
1149
1355
  const { configuration, nodes, edges } = canvasData;
1150
1356
 
1151
- // Internal edit state ref
1152
- const editStateRef = useRef<EditState>(createEmptyEditState());
1153
-
1154
- // Expose imperative handle
1155
- useImperativeHandle(ref, () => ({
1156
- getPendingChanges: (): PendingChanges => {
1157
- const state = editStateRef.current;
1158
- return {
1159
- positionChanges: Array.from(state.positionChanges.entries()).map(([nodeId, position]) => ({
1160
- nodeId,
1161
- position,
1162
- })),
1163
- nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
1164
- nodeId,
1165
- updates,
1166
- })),
1167
- deletedNodeIds: Array.from(state.deletedNodeIds),
1168
- createdEdges: state.createdEdges.map(e => ({
1169
- from: e.from,
1170
- to: e.to,
1171
- type: e.type,
1172
- sourceHandle: e.sourceHandle,
1173
- targetHandle: e.targetHandle,
1174
- })),
1175
- deletedEdges: state.deletedEdges.map(e => ({ from: e.from, to: e.to, type: e.type })),
1176
- hasChanges: state.positionChanges.size > 0 ||
1177
- state.nodeUpdates.size > 0 ||
1178
- state.deletedNodeIds.size > 0 ||
1179
- state.createdEdges.length > 0 ||
1180
- state.deletedEdges.length > 0,
1181
- };
1182
- },
1183
- resetEditState: () => {
1184
- editStateRef.current = createEmptyEditState();
1185
- },
1186
- hasUnsavedChanges: (): boolean => {
1187
- const state = editStateRef.current;
1188
- return state.positionChanges.size > 0 ||
1189
- state.nodeUpdates.size > 0 ||
1190
- state.deletedNodeIds.size > 0 ||
1191
- state.createdEdges.length > 0 ||
1192
- state.deletedEdges.length > 0;
1193
- },
1194
- }), []);
1195
-
1196
1357
  // Extract only the props that inner component needs
1197
1358
  const {
1198
1359
  violations,