@principal-ai/principal-view-react 0.6.6

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 (96) hide show
  1. package/README.md +111 -0
  2. package/dist/components/ConfigurationSelector.d.ts +37 -0
  3. package/dist/components/ConfigurationSelector.d.ts.map +1 -0
  4. package/dist/components/ConfigurationSelector.js +67 -0
  5. package/dist/components/ConfigurationSelector.js.map +1 -0
  6. package/dist/components/EdgeInfoPanel.d.ts +16 -0
  7. package/dist/components/EdgeInfoPanel.d.ts.map +1 -0
  8. package/dist/components/EdgeInfoPanel.js +85 -0
  9. package/dist/components/EdgeInfoPanel.js.map +1 -0
  10. package/dist/components/EventLog.d.ts +20 -0
  11. package/dist/components/EventLog.d.ts.map +1 -0
  12. package/dist/components/EventLog.js +13 -0
  13. package/dist/components/EventLog.js.map +1 -0
  14. package/dist/components/EventLog.test.d.ts +2 -0
  15. package/dist/components/EventLog.test.d.ts.map +1 -0
  16. package/dist/components/EventLog.test.js +73 -0
  17. package/dist/components/EventLog.test.js.map +1 -0
  18. package/dist/components/GraphRenderer.d.ts +121 -0
  19. package/dist/components/GraphRenderer.d.ts.map +1 -0
  20. package/dist/components/GraphRenderer.js +809 -0
  21. package/dist/components/GraphRenderer.js.map +1 -0
  22. package/dist/components/GraphRenderer.test.d.ts +2 -0
  23. package/dist/components/GraphRenderer.test.d.ts.map +1 -0
  24. package/dist/components/GraphRenderer.test.js +88 -0
  25. package/dist/components/GraphRenderer.test.js.map +1 -0
  26. package/dist/components/MetricsDashboard.d.ts +14 -0
  27. package/dist/components/MetricsDashboard.d.ts.map +1 -0
  28. package/dist/components/MetricsDashboard.js +13 -0
  29. package/dist/components/MetricsDashboard.js.map +1 -0
  30. package/dist/components/NodeInfoPanel.d.ts +21 -0
  31. package/dist/components/NodeInfoPanel.d.ts.map +1 -0
  32. package/dist/components/NodeInfoPanel.js +217 -0
  33. package/dist/components/NodeInfoPanel.js.map +1 -0
  34. package/dist/edges/CustomEdge.d.ts +16 -0
  35. package/dist/edges/CustomEdge.d.ts.map +1 -0
  36. package/dist/edges/CustomEdge.js +200 -0
  37. package/dist/edges/CustomEdge.js.map +1 -0
  38. package/dist/edges/GenericEdge.d.ts +18 -0
  39. package/dist/edges/GenericEdge.d.ts.map +1 -0
  40. package/dist/edges/GenericEdge.js +14 -0
  41. package/dist/edges/GenericEdge.js.map +1 -0
  42. package/dist/hooks/usePathBasedEvents.d.ts +42 -0
  43. package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
  44. package/dist/hooks/usePathBasedEvents.js +122 -0
  45. package/dist/hooks/usePathBasedEvents.js.map +1 -0
  46. package/dist/index.d.ts +33 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +41 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/nodes/CustomNode.d.ts +18 -0
  51. package/dist/nodes/CustomNode.d.ts.map +1 -0
  52. package/dist/nodes/CustomNode.js +298 -0
  53. package/dist/nodes/CustomNode.js.map +1 -0
  54. package/dist/nodes/GenericNode.d.ts +20 -0
  55. package/dist/nodes/GenericNode.d.ts.map +1 -0
  56. package/dist/nodes/GenericNode.js +24 -0
  57. package/dist/nodes/GenericNode.js.map +1 -0
  58. package/dist/utils/animationMapping.d.ts +53 -0
  59. package/dist/utils/animationMapping.d.ts.map +1 -0
  60. package/dist/utils/animationMapping.js +133 -0
  61. package/dist/utils/animationMapping.js.map +1 -0
  62. package/dist/utils/graphConverter.d.ts +22 -0
  63. package/dist/utils/graphConverter.d.ts.map +1 -0
  64. package/dist/utils/graphConverter.js +176 -0
  65. package/dist/utils/graphConverter.js.map +1 -0
  66. package/dist/utils/iconResolver.d.ts +29 -0
  67. package/dist/utils/iconResolver.d.ts.map +1 -0
  68. package/dist/utils/iconResolver.js +68 -0
  69. package/dist/utils/iconResolver.js.map +1 -0
  70. package/package.json +61 -0
  71. package/src/components/ConfigurationSelector.tsx +147 -0
  72. package/src/components/EdgeInfoPanel.tsx +198 -0
  73. package/src/components/EventLog.test.tsx +85 -0
  74. package/src/components/EventLog.tsx +51 -0
  75. package/src/components/GraphRenderer.test.tsx +118 -0
  76. package/src/components/GraphRenderer.tsx +1222 -0
  77. package/src/components/MetricsDashboard.tsx +40 -0
  78. package/src/components/NodeInfoPanel.tsx +425 -0
  79. package/src/edges/CustomEdge.tsx +344 -0
  80. package/src/edges/GenericEdge.tsx +40 -0
  81. package/src/hooks/usePathBasedEvents.ts +182 -0
  82. package/src/index.ts +67 -0
  83. package/src/nodes/CustomNode.tsx +432 -0
  84. package/src/nodes/GenericNode.tsx +54 -0
  85. package/src/stories/AnimationWorkshop.stories.tsx +608 -0
  86. package/src/stories/EventDrivenAnimations.stories.tsx +499 -0
  87. package/src/stories/EventLog.stories.tsx +161 -0
  88. package/src/stories/GraphRenderer.stories.tsx +628 -0
  89. package/src/stories/Introduction.mdx +51 -0
  90. package/src/stories/MetricsDashboard.stories.tsx +227 -0
  91. package/src/stories/MultiConfig.stories.tsx +531 -0
  92. package/src/stories/MultiDirectionalConnections.stories.tsx +345 -0
  93. package/src/stories/NodeShapes.stories.tsx +769 -0
  94. package/src/utils/animationMapping.ts +170 -0
  95. package/src/utils/graphConverter.ts +218 -0
  96. package/src/utils/iconResolver.tsx +49 -0
@@ -0,0 +1,809 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphRenderer = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_2 = require("@xyflow/react");
7
+ require("@xyflow/react/dist/style.css");
8
+ const principal_view_core_1 = require("@principal-ai/principal-view-core");
9
+ const CustomNode_1 = require("../nodes/CustomNode");
10
+ const CustomEdge_1 = require("../edges/CustomEdge");
11
+ const graphConverter_1 = require("../utils/graphConverter");
12
+ const EdgeInfoPanel_1 = require("./EdgeInfoPanel");
13
+ const NodeInfoPanel_1 = require("./NodeInfoPanel");
14
+ // Define custom node types
15
+ const nodeTypes = {
16
+ custom: CustomNode_1.CustomNode,
17
+ };
18
+ // Define custom edge types
19
+ const edgeTypes = {
20
+ custom: CustomEdge_1.CustomEdge,
21
+ };
22
+ const createEmptyEditState = () => ({
23
+ positionChanges: new Map(),
24
+ nodeUpdates: new Map(),
25
+ deletedNodeIds: new Set(),
26
+ createdEdges: [],
27
+ deletedEdges: [],
28
+ });
29
+ /**
30
+ * Inner component that uses ReactFlow hooks
31
+ */
32
+ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = true, showControls = true, showBackground = true, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, }) => {
33
+ const { fitView } = (0, react_2.useReactFlow)();
34
+ // Track active animations
35
+ const [animationState, setAnimationState] = (0, react_1.useState)({
36
+ nodeAnimations: {},
37
+ edgeAnimations: {},
38
+ });
39
+ // Track selected edge for info panel
40
+ const [selectedEdgeId, setSelectedEdgeId] = (0, react_1.useState)(null);
41
+ // Track selected node for info panel
42
+ const [selectedNodeId, setSelectedNodeId] = (0, react_1.useState)(null);
43
+ // Track pending connection for edge type picker
44
+ const [pendingConnection, setPendingConnection] = (0, react_1.useState)(null);
45
+ // ============================================
46
+ // INTERNAL EDIT STATE
47
+ // ============================================
48
+ // Local copies of nodes and edges for editing
49
+ const [localNodes, setLocalNodes] = (0, react_1.useState)(propNodes);
50
+ const [localEdges, setLocalEdges] = (0, react_1.useState)(propEdges);
51
+ // Track the prop values to detect external changes
52
+ const propNodesKeyRef = (0, react_1.useRef)(propNodes.map(n => n.id).sort().join(','));
53
+ const propEdgesKeyRef = (0, react_1.useRef)(propEdges.map(e => e.id).sort().join(','));
54
+ // Sync local state with props when props change (e.g., config reload)
55
+ // This only happens when the structure changes, not during editing
56
+ (0, react_1.useEffect)(() => {
57
+ const newNodesKey = propNodes.map(n => n.id).sort().join(',');
58
+ const newEdgesKey = propEdges.map(e => e.id).sort().join(',');
59
+ if (newNodesKey !== propNodesKeyRef.current || newEdgesKey !== propEdgesKeyRef.current) {
60
+ propNodesKeyRef.current = newNodesKey;
61
+ propEdgesKeyRef.current = newEdgesKey;
62
+ setLocalNodes(propNodes);
63
+ setLocalEdges(propEdges);
64
+ // Reset edit state when props change
65
+ editStateRef.current = createEmptyEditState();
66
+ onEditStateChange?.(editStateRef.current);
67
+ onPendingChangesChange?.(false);
68
+ }
69
+ }, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
70
+ // Use local state when editable, props when not
71
+ const nodes = editable ? localNodes : propNodes;
72
+ const edges = editable ? localEdges : propEdges;
73
+ // Helper to check if there are pending changes
74
+ const checkHasChanges = (0, react_1.useCallback)((state) => {
75
+ return state.positionChanges.size > 0 ||
76
+ state.nodeUpdates.size > 0 ||
77
+ state.deletedNodeIds.size > 0 ||
78
+ state.createdEdges.length > 0 ||
79
+ state.deletedEdges.length > 0;
80
+ }, []);
81
+ // Helper to update edit state and notify parent
82
+ const updateEditState = (0, react_1.useCallback)((updater) => {
83
+ const newState = updater(editStateRef.current);
84
+ editStateRef.current = newState;
85
+ onEditStateChange?.(newState);
86
+ onPendingChangesChange?.(checkHasChanges(newState));
87
+ }, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
88
+ // ============================================
89
+ // EVENT HANDLERS
90
+ // ============================================
91
+ // Handle edge click
92
+ const onEdgeClick = (0, react_1.useCallback)((_event, edge) => {
93
+ setSelectedEdgeId(edge.id);
94
+ setSelectedNodeId(null);
95
+ }, []);
96
+ // Handle node click
97
+ const onNodeClick = (0, react_1.useCallback)((_event, node) => {
98
+ setSelectedNodeId(node.id);
99
+ setSelectedEdgeId(null);
100
+ }, []);
101
+ // Handle close edge info panel
102
+ const onCloseEdgeInfoPanel = (0, react_1.useCallback)(() => {
103
+ setSelectedEdgeId(null);
104
+ }, []);
105
+ // Handle close node info panel
106
+ const onCloseNodeInfoPanel = (0, react_1.useCallback)(() => {
107
+ setSelectedNodeId(null);
108
+ }, []);
109
+ // Handle node update (internal - updates local state only)
110
+ const handleNodeUpdate = (0, react_1.useCallback)((nodeId, updates) => {
111
+ if (!editable)
112
+ return;
113
+ // Update local nodes
114
+ setLocalNodes(prev => prev.map(node => {
115
+ if (node.id === nodeId) {
116
+ return {
117
+ ...node,
118
+ type: updates.type ?? node.type,
119
+ data: updates.data ? { ...node.data, ...updates.data } : node.data,
120
+ };
121
+ }
122
+ return node;
123
+ }));
124
+ // Track the change
125
+ updateEditState(prev => {
126
+ const newUpdates = new Map(prev.nodeUpdates);
127
+ const existing = newUpdates.get(nodeId) || {};
128
+ newUpdates.set(nodeId, {
129
+ type: updates.type ?? existing.type,
130
+ data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
131
+ });
132
+ return { ...prev, nodeUpdates: newUpdates };
133
+ });
134
+ }, [editable, updateEditState]);
135
+ // Handle node delete (internal)
136
+ const handleNodeDelete = (0, react_1.useCallback)((nodeId) => {
137
+ if (!editable)
138
+ return;
139
+ // Remove from local state
140
+ setLocalNodes(prev => prev.filter(n => n.id !== nodeId));
141
+ setLocalEdges(prev => prev.filter(e => e.from !== nodeId && e.to !== nodeId));
142
+ // Track the change
143
+ updateEditState(prev => {
144
+ const newDeletedNodes = new Set(prev.deletedNodeIds);
145
+ newDeletedNodes.add(nodeId);
146
+ // Remove any pending updates for this node
147
+ const newUpdates = new Map(prev.nodeUpdates);
148
+ newUpdates.delete(nodeId);
149
+ // Remove any position changes for this node
150
+ const newPositions = new Map(prev.positionChanges);
151
+ newPositions.delete(nodeId);
152
+ // Remove created edges that involve this node
153
+ const newCreatedEdges = prev.createdEdges.filter(e => e.from !== nodeId && e.to !== nodeId);
154
+ return {
155
+ ...prev,
156
+ deletedNodeIds: newDeletedNodes,
157
+ nodeUpdates: newUpdates,
158
+ positionChanges: newPositions,
159
+ createdEdges: newCreatedEdges,
160
+ };
161
+ });
162
+ setSelectedNodeId(null);
163
+ }, [editable, updateEditState]);
164
+ // Handle edge delete (internal)
165
+ const handleEdgeDelete = (0, react_1.useCallback)((edgeId) => {
166
+ if (!editable)
167
+ return;
168
+ // Find the edge before removing it so we can track its full info
169
+ const edgeToDelete = localEdges.find(e => e.id === edgeId);
170
+ // Remove from local state
171
+ setLocalEdges(prev => prev.filter(e => e.id !== edgeId));
172
+ // Track the change
173
+ updateEditState(prev => {
174
+ // Check if this was a newly created edge
175
+ const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === edgeId);
176
+ if (createdEdgeIndex >= 0) {
177
+ // Just remove it from created edges
178
+ const newCreatedEdges = [...prev.createdEdges];
179
+ newCreatedEdges.splice(createdEdgeIndex, 1);
180
+ return { ...prev, createdEdges: newCreatedEdges };
181
+ }
182
+ // Otherwise mark as deleted with full edge info
183
+ if (edgeToDelete) {
184
+ const newDeletedEdges = [...prev.deletedEdges, {
185
+ id: edgeId,
186
+ from: edgeToDelete.from,
187
+ to: edgeToDelete.to,
188
+ type: edgeToDelete.type
189
+ }];
190
+ return { ...prev, deletedEdges: newDeletedEdges };
191
+ }
192
+ return prev;
193
+ });
194
+ setSelectedEdgeId(null);
195
+ }, [editable, updateEditState, localEdges]);
196
+ // Handle new connection from drag
197
+ const handleConnect = (0, react_1.useCallback)((connection) => {
198
+ if (!editable || !connection.source || !connection.target)
199
+ return;
200
+ // Find source and target node types
201
+ const sourceNode = nodes.find(n => n.id === connection.source);
202
+ const targetNode = nodes.find(n => n.id === connection.target);
203
+ if (!sourceNode || !targetNode)
204
+ return;
205
+ // Find valid edge types for this connection
206
+ const validTypes = configuration.allowedConnections
207
+ .filter(ac => ac.from === sourceNode.type && ac.to === targetNode.type)
208
+ .map(ac => ac.via);
209
+ const uniqueTypes = [...new Set(validTypes)];
210
+ if (uniqueTypes.length === 0) {
211
+ console.warn(`No valid edge types for connection from ${sourceNode.type} to ${targetNode.type}`);
212
+ return;
213
+ }
214
+ if (uniqueTypes.length === 1) {
215
+ // Create edge immediately with handle information
216
+ createEdge(connection.source, connection.target, uniqueTypes[0], connection.sourceHandle ?? undefined, connection.targetHandle ?? undefined);
217
+ }
218
+ else {
219
+ // Show picker
220
+ setPendingConnection({
221
+ from: connection.source,
222
+ to: connection.target,
223
+ sourceHandle: connection.sourceHandle ?? undefined,
224
+ targetHandle: connection.targetHandle ?? undefined,
225
+ validTypes: uniqueTypes,
226
+ });
227
+ }
228
+ }, [editable, nodes, configuration.allowedConnections]);
229
+ // Create edge helper
230
+ const createEdge = (0, react_1.useCallback)((from, to, type, sourceHandle, targetHandle) => {
231
+ const edgeId = `${from}-${to}-${type}-${Date.now()}`;
232
+ // Add to local state with handle information
233
+ const newEdge = {
234
+ id: edgeId,
235
+ type,
236
+ from,
237
+ to,
238
+ data: {},
239
+ createdAt: Date.now(),
240
+ updatedAt: Date.now(),
241
+ sourceHandle,
242
+ targetHandle,
243
+ };
244
+ setLocalEdges(prev => [...prev, newEdge]);
245
+ // Track the change
246
+ updateEditState(prev => ({
247
+ ...prev,
248
+ createdEdges: [...prev.createdEdges, { id: edgeId, from, to, type, sourceHandle, targetHandle }],
249
+ }));
250
+ }, [updateEditState]);
251
+ // Handle edge type selection from picker
252
+ const handleEdgeTypeSelect = (0, react_1.useCallback)((type) => {
253
+ if (!pendingConnection)
254
+ return;
255
+ createEdge(pendingConnection.from, pendingConnection.to, type, pendingConnection.sourceHandle, pendingConnection.targetHandle);
256
+ setPendingConnection(null);
257
+ }, [pendingConnection, createEdge]);
258
+ // Cancel edge type picker
259
+ const handleCancelEdgeTypePicker = (0, react_1.useCallback)(() => {
260
+ setPendingConnection(null);
261
+ }, []);
262
+ // Track whether reconnection succeeded
263
+ const edgeReconnectSuccessful = (0, react_1.useRef)(true);
264
+ // Called when user starts dragging an edge endpoint
265
+ const handleReconnectStart = (0, react_1.useCallback)(() => {
266
+ edgeReconnectSuccessful.current = false;
267
+ }, []);
268
+ // Handle edge reconnection (dragging edge endpoint to new node)
269
+ const handleReconnect = (0, react_1.useCallback)((oldEdge, newConnection) => {
270
+ if (!editable || !newConnection.source || !newConnection.target)
271
+ return;
272
+ // Find the original edge in our local state
273
+ const originalEdge = localEdges.find(e => e.id === oldEdge.id);
274
+ if (!originalEdge)
275
+ return;
276
+ // Find source and target node types for validation
277
+ const sourceNode = nodes.find(n => n.id === newConnection.source);
278
+ const targetNode = nodes.find(n => n.id === newConnection.target);
279
+ if (!sourceNode || !targetNode)
280
+ return;
281
+ // Check if the new connection is valid for this edge type
282
+ const isValidConnection = configuration.allowedConnections.some(ac => ac.from === sourceNode.type && ac.to === targetNode.type && ac.via === originalEdge.type);
283
+ if (!isValidConnection) {
284
+ console.warn(`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.type} to ${targetNode.type}`);
285
+ return;
286
+ }
287
+ // Mark as successful before updating
288
+ edgeReconnectSuccessful.current = true;
289
+ // Update local edges - manually update the edge to preserve its type and id
290
+ setLocalEdges(prev => prev.map(edge => {
291
+ if (edge.id === oldEdge.id) {
292
+ return {
293
+ ...edge,
294
+ from: newConnection.source,
295
+ to: newConnection.target,
296
+ sourceHandle: newConnection.sourceHandle ?? undefined,
297
+ targetHandle: newConnection.targetHandle ?? undefined,
298
+ updatedAt: Date.now(),
299
+ };
300
+ }
301
+ return edge;
302
+ }));
303
+ // Track the change - remove old edge and add new one
304
+ updateEditState(prev => {
305
+ // Check if this was a newly created edge
306
+ const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === oldEdge.id);
307
+ if (createdEdgeIndex >= 0) {
308
+ // Update the created edge entry
309
+ const newCreatedEdges = [...prev.createdEdges];
310
+ newCreatedEdges[createdEdgeIndex] = {
311
+ ...newCreatedEdges[createdEdgeIndex],
312
+ from: newConnection.source,
313
+ to: newConnection.target,
314
+ sourceHandle: newConnection.sourceHandle ?? undefined,
315
+ targetHandle: newConnection.targetHandle ?? undefined,
316
+ };
317
+ return { ...prev, createdEdges: newCreatedEdges };
318
+ }
319
+ // For existing edges, track as delete + create
320
+ const newDeletedEdges = [...prev.deletedEdges, {
321
+ id: oldEdge.id,
322
+ from: originalEdge.from,
323
+ to: originalEdge.to,
324
+ type: originalEdge.type,
325
+ }];
326
+ const newCreatedEdges = [...prev.createdEdges, {
327
+ id: oldEdge.id,
328
+ from: newConnection.source,
329
+ to: newConnection.target,
330
+ type: originalEdge.type,
331
+ sourceHandle: newConnection.sourceHandle ?? undefined,
332
+ targetHandle: newConnection.targetHandle ?? undefined,
333
+ }];
334
+ return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
335
+ });
336
+ }, [editable, localEdges, nodes, configuration.allowedConnections, updateEditState]);
337
+ // Called when reconnection ends (whether successful or not)
338
+ const handleReconnectEnd = (0, react_1.useCallback)(() => {
339
+ // If reconnection wasn't successful, the edge was dropped in empty space
340
+ // We need to keep the original edge (do nothing, it's still in localEdges)
341
+ // Edge is still in localEdges, no action needed - ReactFlow will re-render with it
342
+ edgeReconnectSuccessful.current = true;
343
+ }, []);
344
+ // ============================================
345
+ // SELECTED ITEMS
346
+ // ============================================
347
+ const selectedEdge = (0, react_1.useMemo)(() => {
348
+ if (!selectedEdgeId)
349
+ return null;
350
+ return edges.find(e => e.id === selectedEdgeId);
351
+ }, [selectedEdgeId, edges]);
352
+ const selectedEdgeTypeDefinition = (0, react_1.useMemo)(() => {
353
+ if (!selectedEdge)
354
+ return null;
355
+ return configuration.edgeTypes[selectedEdge.type];
356
+ }, [selectedEdge, configuration.edgeTypes]);
357
+ const selectedNode = (0, react_1.useMemo)(() => {
358
+ if (!selectedNodeId)
359
+ return null;
360
+ return nodes.find(n => n.id === selectedNodeId);
361
+ }, [selectedNodeId, nodes]);
362
+ const selectedNodeTypeDefinition = (0, react_1.useMemo)(() => {
363
+ if (!selectedNode)
364
+ return null;
365
+ return configuration.nodeTypes[selectedNode.type];
366
+ }, [selectedNode, configuration.nodeTypes]);
367
+ // ============================================
368
+ // ANIMATIONS
369
+ // ============================================
370
+ (0, react_1.useEffect)(() => {
371
+ if (events.length === 0)
372
+ return;
373
+ const latestEvent = events[events.length - 1];
374
+ if (latestEvent.operation === 'animate' && latestEvent.category === 'edge') {
375
+ const edgeEvent = latestEvent.payload;
376
+ const edgeId = edgeEvent.edgeId;
377
+ const animation = edgeEvent.animation;
378
+ if (animation && edgeId) {
379
+ setAnimationState(prev => ({
380
+ ...prev,
381
+ edgeAnimations: {
382
+ ...prev.edgeAnimations,
383
+ [edgeId]: {
384
+ type: 'flow',
385
+ duration: animation.duration || 1000,
386
+ direction: animation.direction || 'forward',
387
+ timestamp: Date.now(),
388
+ },
389
+ },
390
+ }));
391
+ const duration = animation.duration || 1000;
392
+ setTimeout(() => {
393
+ setAnimationState(prev => {
394
+ const newEdgeAnimations = { ...prev.edgeAnimations };
395
+ delete newEdgeAnimations[edgeId];
396
+ return { ...prev, edgeAnimations: newEdgeAnimations };
397
+ });
398
+ }, duration);
399
+ onEventProcessed?.(latestEvent);
400
+ }
401
+ }
402
+ if (latestEvent.category === 'state') {
403
+ const stateEvent = latestEvent.payload;
404
+ const nodeId = stateEvent.nodeId;
405
+ const newState = stateEvent.newState;
406
+ if (nodeId && newState) {
407
+ const stateToAnimation = {
408
+ processing: 'pulse',
409
+ completed: 'flash',
410
+ error: 'shake',
411
+ };
412
+ const animationType = stateToAnimation[newState];
413
+ if (animationType) {
414
+ const duration = animationType === 'pulse' ? 1500 : animationType === 'flash' ? 1000 : 500;
415
+ setAnimationState(prev => ({
416
+ ...prev,
417
+ nodeAnimations: {
418
+ ...prev.nodeAnimations,
419
+ [nodeId]: { type: animationType, duration, timestamp: Date.now() },
420
+ },
421
+ }));
422
+ if (animationType !== 'pulse') {
423
+ setTimeout(() => {
424
+ setAnimationState(prev => {
425
+ const newNodeAnimations = { ...prev.nodeAnimations };
426
+ delete newNodeAnimations[nodeId];
427
+ return { ...prev, nodeAnimations: newNodeAnimations };
428
+ });
429
+ }, duration);
430
+ }
431
+ onEventProcessed?.(latestEvent);
432
+ }
433
+ }
434
+ }
435
+ if (latestEvent.category === 'node' && latestEvent.operation === 'create') {
436
+ const nodeEvent = latestEvent.payload;
437
+ const nodeId = nodeEvent.nodeId;
438
+ if (nodeId) {
439
+ setAnimationState(prev => ({
440
+ ...prev,
441
+ nodeAnimations: {
442
+ ...prev.nodeAnimations,
443
+ [nodeId]: { type: 'entry', duration: 600, timestamp: Date.now() },
444
+ },
445
+ }));
446
+ setTimeout(() => {
447
+ setAnimationState(prev => {
448
+ const newNodeAnimations = { ...prev.nodeAnimations };
449
+ delete newNodeAnimations[nodeId];
450
+ return { ...prev, nodeAnimations: newNodeAnimations };
451
+ });
452
+ }, 600);
453
+ onEventProcessed?.(latestEvent);
454
+ }
455
+ }
456
+ }, [events, onEventProcessed]);
457
+ // ============================================
458
+ // XYFLOW CONVERSION
459
+ // ============================================
460
+ const xyflowNodesBase = (0, react_1.useMemo)(() => {
461
+ const converted = (0, graphConverter_1.convertToXYFlowNodes)(nodes, configuration, violations);
462
+ const layoutType = configuration.display?.layout || 'hierarchical';
463
+ const positioned = (0, graphConverter_1.autoLayoutNodes)(converted, [], layoutType);
464
+ return positioned.map(node => {
465
+ const animation = animationState.nodeAnimations[node.id];
466
+ // Apply any pending position changes
467
+ const pendingPosition = editStateRef.current.positionChanges.get(node.id);
468
+ return {
469
+ ...node,
470
+ ...(pendingPosition ? { position: pendingPosition } : {}),
471
+ data: {
472
+ ...node.data,
473
+ editable,
474
+ ...(animation ? {
475
+ animationType: animation.type,
476
+ animationDuration: animation.duration,
477
+ } : {}),
478
+ },
479
+ };
480
+ });
481
+ }, [nodes, configuration, violations, animationState.nodeAnimations, editable, editStateRef]);
482
+ const baseNodesKey = (0, react_1.useMemo)(() => {
483
+ return nodes.map(n => n.id).sort().join(',');
484
+ }, [nodes]);
485
+ // Local xyflow nodes state for dragging
486
+ const [xyflowLocalNodes, setXyflowLocalNodes] = (0, react_1.useState)(xyflowNodesBase);
487
+ // Sync when base changes
488
+ const prevBaseNodesKeyRef = (0, react_1.useRef)(baseNodesKey);
489
+ (0, react_1.useEffect)(() => {
490
+ if (prevBaseNodesKeyRef.current !== baseNodesKey) {
491
+ prevBaseNodesKeyRef.current = baseNodesKey;
492
+ setXyflowLocalNodes(xyflowNodesBase);
493
+ }
494
+ }, [baseNodesKey, xyflowNodesBase]);
495
+ // Also sync when entering edit mode or when base nodes change content
496
+ const prevEditableRef = (0, react_1.useRef)(editable);
497
+ (0, react_1.useEffect)(() => {
498
+ if (editable && !prevEditableRef.current) {
499
+ // Entering edit mode - sync positions
500
+ setXyflowLocalNodes(xyflowNodesBase);
501
+ }
502
+ prevEditableRef.current = editable;
503
+ }, [editable, xyflowNodesBase]);
504
+ const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
505
+ // Handle node changes (drag events)
506
+ const handleNodesChange = (0, react_1.useCallback)((changes) => {
507
+ if (!editable)
508
+ return;
509
+ setXyflowLocalNodes(nds => (0, react_2.applyNodeChanges)(changes, nds));
510
+ // Track position changes on drag end
511
+ const positionChanges = changes
512
+ .filter((change) => change.type === 'position' &&
513
+ 'position' in change &&
514
+ change.position !== undefined &&
515
+ 'dragging' in change &&
516
+ change.dragging === false);
517
+ if (positionChanges.length > 0) {
518
+ updateEditState(prev => {
519
+ const newPositions = new Map(prev.positionChanges);
520
+ for (const change of positionChanges) {
521
+ newPositions.set(change.id, {
522
+ x: Math.round(change.position.x),
523
+ y: Math.round(change.position.y),
524
+ });
525
+ }
526
+ return { ...prev, positionChanges: newPositions };
527
+ });
528
+ }
529
+ }, [editable, updateEditState]);
530
+ const xyflowEdges = (0, react_1.useMemo)(() => {
531
+ const converted = (0, graphConverter_1.convertToXYFlowEdges)(edges, configuration, violations);
532
+ return converted.map(edge => {
533
+ const animation = animationState.edgeAnimations[edge.id];
534
+ if (animation) {
535
+ return {
536
+ ...edge,
537
+ data: {
538
+ ...edge.data,
539
+ animationType: animation.type,
540
+ animationDuration: animation.duration,
541
+ animationDirection: animation.direction,
542
+ },
543
+ };
544
+ }
545
+ return edge;
546
+ });
547
+ }, [edges, configuration, violations, animationState.edgeAnimations]);
548
+ // Fit view on mount and structure changes
549
+ (0, react_1.useEffect)(() => {
550
+ const timeoutId = setTimeout(() => {
551
+ fitView({
552
+ padding: 0.2,
553
+ includeHiddenNodes: false,
554
+ minZoom: 0.1,
555
+ maxZoom: 1.5,
556
+ duration: 200,
557
+ });
558
+ }, 100);
559
+ return () => clearTimeout(timeoutId);
560
+ }, [baseNodesKey, fitView]);
561
+ // ============================================
562
+ // RENDER
563
+ // ============================================
564
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_2.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: editable, nodesConnectable: editable, edgesReconnectable: editable, onNodesChange: handleNodesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, panOnDrag: true, selectionOnDrag: false, children: [showBackground && (0, jsx_runtime_1.jsx)(react_2.Background, { color: "#e5e5e5", gap: 16, size: 1 }), showControls && (0, jsx_runtime_1.jsx)(react_2.Controls, { showZoom: true, showFitView: true, showInteractive: true }), showMinimap && ((0, jsx_runtime_1.jsx)(react_2.MiniMap, { nodeColor: (node) => {
565
+ const nodeData = node.data;
566
+ return nodeData?.typeDefinition?.color || '#888';
567
+ }, nodeBorderRadius: 2, pannable: true, zoomable: true }))] }, baseNodesKey), selectedEdge && selectedEdgeTypeDefinition && ((0, jsx_runtime_1.jsx)(EdgeInfoPanel_1.EdgeInfoPanel, { edge: selectedEdge, typeDefinition: selectedEdgeTypeDefinition, sourceNodeId: selectedEdge.from, targetNodeId: selectedEdge.to, onClose: onCloseEdgeInfoPanel, onDelete: editable ? handleEdgeDelete : undefined })), selectedNode && selectedNodeTypeDefinition && ((0, jsx_runtime_1.jsx)(NodeInfoPanel_1.NodeInfoPanel, { node: selectedNode, typeDefinition: selectedNodeTypeDefinition, availableNodeTypes: configuration.nodeTypes, onClose: onCloseNodeInfoPanel, onDelete: editable ? handleNodeDelete : undefined, onUpdate: editable ? handleNodeUpdate : undefined })), pendingConnection && ((0, jsx_runtime_1.jsxs)("div", { style: {
568
+ position: 'absolute',
569
+ top: '50%',
570
+ left: '50%',
571
+ transform: 'translate(-50%, -50%)',
572
+ backgroundColor: 'white',
573
+ borderRadius: '8px',
574
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
575
+ padding: '16px',
576
+ minWidth: '200px',
577
+ zIndex: 1000,
578
+ }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 'bold', marginBottom: '12px', fontSize: '14px' }, children: "Select Edge Type" }), (0, jsx_runtime_1.jsxs)("div", { style: { fontSize: '12px', color: '#666', marginBottom: '12px' }, children: [pendingConnection.from, " \u2192 ", pendingConnection.to] }), (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' }, children: pendingConnection.validTypes.map(type => {
579
+ const typeDefinition = configuration.edgeTypes[type];
580
+ return ((0, jsx_runtime_1.jsx)("button", { onClick: () => handleEdgeTypeSelect(type), style: {
581
+ padding: '8px 12px',
582
+ backgroundColor: typeDefinition?.color || '#888',
583
+ color: 'white',
584
+ border: 'none',
585
+ borderRadius: '4px',
586
+ cursor: 'pointer',
587
+ fontSize: '12px',
588
+ fontWeight: 'bold',
589
+ textAlign: 'left',
590
+ }, children: type }, type));
591
+ }) }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCancelEdgeTypePicker, style: {
592
+ marginTop: '12px',
593
+ width: '100%',
594
+ padding: '8px 12px',
595
+ backgroundColor: '#f0f0f0',
596
+ color: '#666',
597
+ border: 'none',
598
+ borderRadius: '4px',
599
+ cursor: 'pointer',
600
+ fontSize: '12px',
601
+ }, children: "Cancel" })] }))] }));
602
+ };
603
+ /**
604
+ * Convert canvas to legacy configuration format for internal use
605
+ */
606
+ function useCanvasToLegacy(canvas, library) {
607
+ return (0, react_1.useMemo)(() => {
608
+ if (!canvas)
609
+ return null;
610
+ const { nodes, edges } = principal_view_core_1.CanvasConverter.canvasToGraph(canvas);
611
+ // Build GraphConfiguration from canvas
612
+ const nodeTypes = {};
613
+ const edgeTypes = {};
614
+ // First, add node types from library (lowest priority - can be overridden by canvas)
615
+ if (library?.nodeComponents) {
616
+ for (const [id, component] of Object.entries(library.nodeComponents)) {
617
+ nodeTypes[id] = {
618
+ shape: component.shape || 'rectangle',
619
+ icon: component.icon,
620
+ color: component.color,
621
+ size: component.size,
622
+ dataSchema: component.dataSchema || {},
623
+ states: component.states,
624
+ layout: component.layout,
625
+ };
626
+ }
627
+ }
628
+ // Then, add edge types from library
629
+ if (library?.edgeComponents) {
630
+ for (const [id, component] of Object.entries(library.edgeComponents)) {
631
+ edgeTypes[id] = {
632
+ style: component.style || 'solid',
633
+ color: component.color,
634
+ width: component.width,
635
+ directed: component.directed,
636
+ animation: component.animation,
637
+ };
638
+ }
639
+ }
640
+ // Next, add node types from canvas vv.nodeTypes (overrides library)
641
+ if (canvas.pv?.nodeTypes) {
642
+ for (const [id, def] of Object.entries(canvas.pv.nodeTypes)) {
643
+ nodeTypes[id] = {
644
+ shape: def.shape || 'rectangle',
645
+ icon: def.icon,
646
+ color: def.color,
647
+ dataSchema: {},
648
+ };
649
+ }
650
+ }
651
+ // Then extract node types from canvas nodes (for nodes that define their own types)
652
+ for (const node of canvas.nodes || []) {
653
+ const vv = node.pv;
654
+ const nodeType = vv?.nodeType || node.type;
655
+ if (!nodeTypes[nodeType]) {
656
+ // Color priority: vv.fill > node.color > vv.states.idle.color
657
+ const fillColor = vv?.fill
658
+ || (typeof node.color === 'string' ? node.color : undefined)
659
+ || vv?.states?.idle?.color;
660
+ nodeTypes[nodeType] = {
661
+ shape: vv?.shape || 'rectangle',
662
+ icon: vv?.icon,
663
+ color: fillColor,
664
+ stroke: vv?.stroke,
665
+ size: { width: node.width, height: node.height },
666
+ dataSchema: vv?.dataSchema || {},
667
+ states: vv?.states,
668
+ layout: vv?.layout,
669
+ };
670
+ }
671
+ }
672
+ // Extract edge types from canvas vv.edgeTypes
673
+ if (canvas.pv?.edgeTypes) {
674
+ for (const [id, def] of Object.entries(canvas.pv.edgeTypes)) {
675
+ edgeTypes[id] = {
676
+ style: def.style || 'solid',
677
+ color: def.color,
678
+ width: def.width,
679
+ directed: def.directed,
680
+ animation: def.animation,
681
+ label: def.labelConfig,
682
+ };
683
+ }
684
+ }
685
+ // Build allowed connections from edges
686
+ const allowedConnections = [];
687
+ for (const edge of canvas.edges || []) {
688
+ const edgeType = edge.pv?.edgeType || 'default';
689
+ // Ensure edge type exists
690
+ if (!edgeTypes[edgeType]) {
691
+ edgeTypes[edgeType] = {
692
+ style: edge.pv?.style || 'solid',
693
+ color: typeof edge.color === 'string' ? edge.color : undefined,
694
+ width: edge.pv?.width,
695
+ directed: true,
696
+ };
697
+ }
698
+ // Find node types for from/to
699
+ const fromNode = canvas.nodes?.find(n => n.id === edge.fromNode);
700
+ const toNode = canvas.nodes?.find(n => n.id === edge.toNode);
701
+ const fromType = fromNode?.pv?.nodeType || edge.fromNode;
702
+ const toType = toNode?.pv?.nodeType || edge.toNode;
703
+ allowedConnections.push({
704
+ from: fromType,
705
+ to: toType,
706
+ via: edgeType,
707
+ });
708
+ }
709
+ // Build display config with required layout field
710
+ const display = canvas.pv?.display
711
+ ? {
712
+ layout: canvas.pv.display.layout || 'manual',
713
+ theme: canvas.pv.display.theme,
714
+ animations: canvas.pv.display.animations,
715
+ }
716
+ : { layout: 'manual' };
717
+ const configuration = {
718
+ metadata: {
719
+ name: canvas.pv?.name || 'Untitled',
720
+ version: canvas.pv?.version || '1.0.0',
721
+ description: canvas.pv?.description,
722
+ },
723
+ nodeTypes,
724
+ edgeTypes,
725
+ allowedConnections,
726
+ display,
727
+ };
728
+ return { configuration, nodes, edges };
729
+ }, [canvas, library]);
730
+ }
731
+ /**
732
+ * Core graph visualization component using xyflow.
733
+ *
734
+ * Accepts an ExtendedCanvas document for rendering.
735
+ *
736
+ * When `editable` is true, the component manages its own edit state internally.
737
+ * Use the ref to get pending changes when the user wants to save:
738
+ *
739
+ * ```tsx
740
+ * <GraphRenderer canvas={myCanvas} />
741
+ *
742
+ * // With edit mode
743
+ * const graphRef = useRef<GraphRendererHandle>(null);
744
+ * <GraphRenderer
745
+ * ref={graphRef}
746
+ * canvas={myCanvas}
747
+ * editable={isEditMode}
748
+ * onPendingChangesChange={setHasUnsavedChanges}
749
+ * />
750
+ * ```
751
+ */
752
+ exports.GraphRenderer = (0, react_1.forwardRef)((props, ref) => {
753
+ const { canvas, library, className, width = '100%', height = '100%' } = props;
754
+ // Convert canvas to internal format (merging library types if provided)
755
+ const canvasData = useCanvasToLegacy(canvas, library);
756
+ // Validate we have required data
757
+ if (!canvasData) {
758
+ return ((0, jsx_runtime_1.jsx)("div", { className: className, style: { width, height, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: (0, jsx_runtime_1.jsx)("p", { style: { color: '#666' }, children: "No canvas data provided." }) }));
759
+ }
760
+ const { configuration, nodes, edges } = canvasData;
761
+ // Internal edit state ref
762
+ const editStateRef = (0, react_1.useRef)(createEmptyEditState());
763
+ // Expose imperative handle
764
+ (0, react_1.useImperativeHandle)(ref, () => ({
765
+ getPendingChanges: () => {
766
+ const state = editStateRef.current;
767
+ return {
768
+ positionChanges: Array.from(state.positionChanges.entries()).map(([nodeId, position]) => ({
769
+ nodeId,
770
+ position,
771
+ })),
772
+ nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
773
+ nodeId,
774
+ updates,
775
+ })),
776
+ deletedNodeIds: Array.from(state.deletedNodeIds),
777
+ createdEdges: state.createdEdges.map(e => ({
778
+ from: e.from,
779
+ to: e.to,
780
+ type: e.type,
781
+ sourceHandle: e.sourceHandle,
782
+ targetHandle: e.targetHandle,
783
+ })),
784
+ deletedEdges: state.deletedEdges.map(e => ({ from: e.from, to: e.to, type: e.type })),
785
+ hasChanges: state.positionChanges.size > 0 ||
786
+ state.nodeUpdates.size > 0 ||
787
+ state.deletedNodeIds.size > 0 ||
788
+ state.createdEdges.length > 0 ||
789
+ state.deletedEdges.length > 0,
790
+ };
791
+ },
792
+ resetEditState: () => {
793
+ editStateRef.current = createEmptyEditState();
794
+ },
795
+ hasUnsavedChanges: () => {
796
+ const state = editStateRef.current;
797
+ return state.positionChanges.size > 0 ||
798
+ state.nodeUpdates.size > 0 ||
799
+ state.deletedNodeIds.size > 0 ||
800
+ state.createdEdges.length > 0 ||
801
+ state.deletedEdges.length > 0;
802
+ },
803
+ }), []);
804
+ // Extract only the props that inner component needs
805
+ const { violations, configName, showMinimap, showControls, showBackground, events, onEventProcessed, editable, onPendingChangesChange, } = props;
806
+ return ((0, jsx_runtime_1.jsx)("div", { className: className, style: { width, height, position: 'relative' }, children: (0, jsx_runtime_1.jsx)(react_2.ReactFlowProvider, { children: (0, jsx_runtime_1.jsx)(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef }) }) }));
807
+ });
808
+ exports.GraphRenderer.displayName = 'GraphRenderer';
809
+ //# sourceMappingURL=GraphRenderer.js.map