@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
@@ -10,14 +10,14 @@ import type {
10
10
  ComponentActivityEvent,
11
11
  ComponentActionEvent,
12
12
  EdgeAnimationEvent,
13
- PathBasedEvent
13
+ PathBasedEvent,
14
14
  } from '@principal-ai/principal-view-core';
15
15
  import {
16
16
  logLevelToNodeAnimation,
17
17
  actionToNodeAnimation,
18
18
  actionToEdgeAnimation,
19
19
  type NodeAnimation,
20
- type EdgeAnimation
20
+ type EdgeAnimation,
21
21
  } from '../utils/animationMapping';
22
22
 
23
23
  /**
@@ -52,53 +52,62 @@ export function usePathBasedEvents({
52
52
  events,
53
53
  callbacks,
54
54
  onEventProcessed,
55
- minLogLevel = 'info'
55
+ minLogLevel = 'info',
56
56
  }: UsePathBasedEventsOptions): void {
57
57
  const { onNodeAnimation, onEdgeAnimation } = callbacks;
58
58
 
59
59
  // Process component activity events (Milestone 1)
60
- const processActivityEvent = useCallback((event: ComponentActivityEvent) => {
61
- // Map log level to animation
62
- const animation = logLevelToNodeAnimation(event.level);
63
-
64
- // Trigger node animation
65
- onNodeAnimation(event.componentId, {
66
- ...animation,
67
- timestamp: event.timestamp
68
- });
69
-
70
- onEventProcessed?.(event);
71
- }, [onNodeAnimation, onEventProcessed]);
60
+ const processActivityEvent = useCallback(
61
+ (event: ComponentActivityEvent) => {
62
+ // Map log level to animation
63
+ const animation = logLevelToNodeAnimation(event.level);
64
+
65
+ // Trigger node animation
66
+ onNodeAnimation(event.componentId, {
67
+ ...animation,
68
+ timestamp: event.timestamp,
69
+ });
70
+
71
+ onEventProcessed?.(event);
72
+ },
73
+ [onNodeAnimation, onEventProcessed]
74
+ );
72
75
 
73
76
  // Process component action events (Milestone 2)
74
- const processActionEvent = useCallback((event: ComponentActionEvent) => {
75
- // Map action/state to animation
76
- const animation = actionToNodeAnimation(event.action, event.state);
77
-
78
- // Trigger node animation
79
- onNodeAnimation(event.componentId, {
80
- ...animation,
81
- timestamp: event.timestamp
82
- });
83
-
84
- onEventProcessed?.(event);
85
- }, [onNodeAnimation, onEventProcessed]);
77
+ const processActionEvent = useCallback(
78
+ (event: ComponentActionEvent) => {
79
+ // Map action/state to animation
80
+ const animation = actionToNodeAnimation(event.action, event.state);
81
+
82
+ // Trigger node animation
83
+ onNodeAnimation(event.componentId, {
84
+ ...animation,
85
+ timestamp: event.timestamp,
86
+ });
87
+
88
+ onEventProcessed?.(event);
89
+ },
90
+ [onNodeAnimation, onEventProcessed]
91
+ );
86
92
 
87
93
  // Process edge animation events (Milestone 2)
88
- const processEdgeAnimationEvent = useCallback((event: EdgeAnimationEvent) => {
89
- const animation = actionToEdgeAnimation(event.triggeredBy?.action || 'unknown', {
90
- type: event.animation,
91
- duration: event.duration,
92
- direction: event.direction
93
- });
94
-
95
- onEdgeAnimation(event.edgeId, {
96
- ...animation,
97
- timestamp: event.timestamp
98
- });
99
-
100
- onEventProcessed?.(event);
101
- }, [onEdgeAnimation, onEventProcessed]);
94
+ const processEdgeAnimationEvent = useCallback(
95
+ (event: EdgeAnimationEvent) => {
96
+ const animation = actionToEdgeAnimation(event.triggeredBy?.action || 'unknown', {
97
+ type: event.animation,
98
+ duration: event.duration,
99
+ direction: event.direction,
100
+ });
101
+
102
+ onEdgeAnimation(event.edgeId, {
103
+ ...animation,
104
+ timestamp: event.timestamp,
105
+ });
106
+
107
+ onEventProcessed?.(event);
108
+ },
109
+ [onEdgeAnimation, onEventProcessed]
110
+ );
102
111
 
103
112
  // Process events when they change
104
113
  useEffect(() => {
@@ -149,7 +158,7 @@ export function processBatchEvents(
149
158
  const animation = logLevelToNodeAnimation(event.level);
150
159
  callbacks.onNodeAnimation(event.componentId, {
151
160
  ...animation,
152
- timestamp: event.timestamp
161
+ timestamp: event.timestamp,
153
162
  });
154
163
  }
155
164
  break;
@@ -159,7 +168,7 @@ export function processBatchEvents(
159
168
  const animation = actionToNodeAnimation(event.action, event.state);
160
169
  callbacks.onNodeAnimation(event.componentId, {
161
170
  ...animation,
162
- timestamp: event.timestamp
171
+ timestamp: event.timestamp,
163
172
  });
164
173
  }
165
174
  break;
@@ -169,11 +178,11 @@ export function processBatchEvents(
169
178
  const animation = actionToEdgeAnimation(event.triggeredBy?.action || 'unknown', {
170
179
  type: event.animation,
171
180
  duration: event.duration,
172
- direction: event.direction
181
+ direction: event.direction,
173
182
  });
174
183
  callbacks.onEdgeAnimation(event.edgeId, {
175
184
  ...animation,
176
- timestamp: event.timestamp
185
+ timestamp: event.timestamp,
177
186
  });
178
187
  }
179
188
  break;
package/src/index.ts CHANGED
@@ -30,7 +30,12 @@ export type {
30
30
 
31
31
  // Export components
32
32
  export { GraphRenderer } from './components/GraphRenderer';
33
- export type { GraphRendererProps, GraphRendererHandle, NodePositionChange, PendingChanges } from './components/GraphRenderer';
33
+ export type {
34
+ GraphRendererProps,
35
+ GraphRendererHandle,
36
+ NodePositionChange,
37
+ PendingChanges,
38
+ } from './components/GraphRenderer';
34
39
 
35
40
  export { EventLog } from './components/EventLog';
36
41
  export type { EventLogProps } from './components/EventLog';
@@ -61,7 +66,11 @@ export { CustomEdge } from './edges/CustomEdge';
61
66
  export type { CustomEdgeData } from './edges/CustomEdge';
62
67
 
63
68
  // Export utilities
64
- export { convertToXYFlowNodes, convertToXYFlowEdges, autoLayoutNodes } from './utils/graphConverter';
69
+ export {
70
+ convertToXYFlowNodes,
71
+ convertToXYFlowEdges,
72
+ autoLayoutNodes,
73
+ } from './utils/graphConverter';
65
74
  export type { EdgeStateWithHandles } from './utils/graphConverter';
66
75
  export { Icon, resolveIcon } from './utils/iconResolver';
67
76
  export type { IconProps } from './utils/iconResolver';
@@ -37,9 +37,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
37
37
  if (!typeDefinition) {
38
38
  return (
39
39
  <div style={{ padding: '10px', border: '2px solid red', borderRadius: '4px' }}>
40
- <div style={{ fontSize: '12px', color: 'red' }}>
41
- Error: Missing node type definition
42
- </div>
40
+ <div style={{ fontSize: '12px', color: 'red' }}>Error: Missing node type definition</div>
43
41
  </div>
44
42
  );
45
43
  }
@@ -49,8 +47,11 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
49
47
  const nodeDataColor = nodeData.color as string | undefined;
50
48
  const baseColor = nodeDataColor || typeDefinition.color || '#888';
51
49
  // Check node's own states first (from pv.states), then fall back to type definition states
52
- const nodeDataStates = nodeData.states as Record<string, { color?: string; label?: string; icon?: string }> | undefined;
53
- const stateColor = state && (nodeDataStates?.[state]?.color || typeDefinition.states?.[state]?.color);
50
+ const nodeDataStates = nodeData.states as
51
+ | Record<string, { color?: string; label?: string; icon?: string }>
52
+ | undefined;
53
+ const stateColor =
54
+ state && (nodeDataStates?.[state]?.color || typeDefinition.states?.[state]?.color);
54
55
  const fillColor = stateColor || baseColor;
55
56
 
56
57
  // Get stroke color - priority: node data stroke > type definition stroke > fill color
@@ -64,9 +65,10 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
64
65
  const displayName = nodeProps.name;
65
66
 
66
67
  // Icon priority: node data override > state icon (node data states first) > type definition icon
67
- const icon = (nodeData.icon as string)
68
- || (state && (nodeDataStates?.[state]?.icon || typeDefinition.states?.[state]?.icon))
69
- || typeDefinition.icon;
68
+ const icon =
69
+ (nodeData.icon as string) ||
70
+ (state && (nodeDataStates?.[state]?.icon || typeDefinition.states?.[state]?.icon)) ||
71
+ typeDefinition.icon;
70
72
 
71
73
  // Get animation class based on type
72
74
  const getAnimationClass = () => {
@@ -86,11 +88,14 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
86
88
 
87
89
  const animationClass = getAnimationClass();
88
90
 
91
+ // Check if this is a group node
92
+ const isGroup = nodeData.canvasType === 'group';
93
+
89
94
  // Shape-specific styles
90
95
  const getShapeStyles = () => {
91
96
  const baseStyles = {
92
97
  padding: '12px 16px',
93
- backgroundColor: 'white',
98
+ backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : 'white',
94
99
  color: '#000',
95
100
  border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
96
101
  fontSize: '12px',
@@ -100,7 +105,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
100
105
  display: 'flex',
101
106
  flexDirection: 'column' as const,
102
107
  alignItems: 'center',
103
- justifyContent: 'center',
108
+ justifyContent: isGroup ? 'flex-start' : 'center',
104
109
  gap: '4px',
105
110
  boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
106
111
  transition: 'all 0.2s ease',
@@ -156,47 +161,53 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
156
161
  // Hexagon with gentle diagonals
157
162
  const hexagonClipPath = 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)';
158
163
  const hexagonBorderWidth = 2;
159
- const hexagonBorderStyle: React.CSSProperties = isHexagon ? {
160
- position: 'relative',
161
- clipPath: hexagonClipPath,
162
- backgroundColor: hasViolations ? '#D0021B' : strokeColor,
163
- width: typeDefinition.size?.width || 120,
164
- height: typeDefinition.size?.height || 120,
165
- boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
166
- transition: 'all 0.2s ease',
167
- } : {};
164
+ const hexagonBorderStyle: React.CSSProperties = isHexagon
165
+ ? {
166
+ position: 'relative',
167
+ clipPath: hexagonClipPath,
168
+ backgroundColor: hasViolations ? '#D0021B' : strokeColor,
169
+ width: typeDefinition.size?.width || 120,
170
+ height: typeDefinition.size?.height || 120,
171
+ boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
172
+ transition: 'all 0.2s ease',
173
+ }
174
+ : {};
168
175
 
169
176
  // Hexagon inner fill styles (white background inset from border)
170
- const hexagonInnerStyle: React.CSSProperties = isHexagon ? {
171
- position: 'absolute',
172
- top: hexagonBorderWidth,
173
- left: hexagonBorderWidth,
174
- right: hexagonBorderWidth,
175
- bottom: hexagonBorderWidth,
176
- clipPath: hexagonClipPath,
177
- backgroundColor: 'white',
178
- color: '#000',
179
- display: 'flex',
180
- flexDirection: 'column',
181
- alignItems: 'center',
182
- justifyContent: 'center',
183
- fontSize: '12px',
184
- fontWeight: 500,
185
- gap: '4px',
186
- } : {};
177
+ const hexagonInnerStyle: React.CSSProperties = isHexagon
178
+ ? {
179
+ position: 'absolute',
180
+ top: hexagonBorderWidth,
181
+ left: hexagonBorderWidth,
182
+ right: hexagonBorderWidth,
183
+ bottom: hexagonBorderWidth,
184
+ clipPath: hexagonClipPath,
185
+ backgroundColor: 'white',
186
+ color: '#000',
187
+ display: 'flex',
188
+ flexDirection: 'column',
189
+ alignItems: 'center',
190
+ justifyContent: 'center',
191
+ fontSize: '12px',
192
+ fontWeight: 500,
193
+ gap: '4px',
194
+ }
195
+ : {};
187
196
 
188
197
  // Handle styles - larger and more visible in edit mode
189
- const baseHandleStyle = editable ? {
190
- background: color,
191
- width: 12,
192
- height: 12,
193
- border: '2px solid white',
194
- boxShadow: '0 0 0 1px ' + color,
195
- } : {
196
- background: color,
197
- width: 8,
198
- height: 8,
199
- };
198
+ const baseHandleStyle = editable
199
+ ? {
200
+ background: color,
201
+ width: 12,
202
+ height: 12,
203
+ border: '2px solid white',
204
+ boxShadow: '0 0 0 1px ' + color,
205
+ }
206
+ : {
207
+ background: color,
208
+ width: 8,
209
+ height: 8,
210
+ };
200
211
 
201
212
  // Diamond handles need to be offset to reach the tips of the rotated shape
202
213
  // A 45° rotated square has tips at ~41% beyond the original edges
@@ -234,52 +245,44 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
234
245
 
235
246
  return (
236
247
  <>
237
- {/* Input handles - multiple connection points */}
238
- <Handle
239
- type="target"
240
- position={Position.Top}
241
- id="top"
242
- style={getHandleStyle('top')}
243
- />
244
- <Handle
245
- type="target"
246
- position={Position.Left}
247
- id="left"
248
- style={getHandleStyle('left')}
249
- />
250
- <Handle
251
- type="target"
252
- position={Position.Right}
253
- id="right"
254
- style={getHandleStyle('right')}
255
- />
248
+ {/* Input handles - all 4 sides for incoming connections */}
249
+ <Handle type="target" position={Position.Top} id="top" style={getHandleStyle('top')} />
250
+ <Handle type="target" position={Position.Bottom} id="bottom" style={getHandleStyle('bottom')} />
251
+ <Handle type="target" position={Position.Left} id="left" style={getHandleStyle('left')} />
252
+ <Handle type="target" position={Position.Right} id="right" style={getHandleStyle('right')} />
256
253
 
257
254
  {/* Hexagon needs a wrapper for proper border rendering */}
258
255
  {isHexagon ? (
259
256
  <div style={hexagonBorderStyle} className={animationClass}>
260
257
  <div style={hexagonInnerStyle}>
261
- {icon && <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{resolveIcon(icon, 20)}</div>}
262
- <div style={{ textAlign: 'center', wordBreak: 'break-word' }}>
263
- {displayName}
264
- </div>
258
+ {icon && (
259
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
260
+ {resolveIcon(icon, 20)}
261
+ </div>
262
+ )}
263
+ <div style={{ textAlign: 'center', wordBreak: 'break-word' }}>{displayName}</div>
265
264
  {state && (
266
- <div style={{
267
- fontSize: '10px',
268
- backgroundColor: color,
269
- color: 'white',
270
- padding: '2px 6px',
271
- borderRadius: '4px',
272
- textAlign: 'center',
273
- }}>
265
+ <div
266
+ style={{
267
+ fontSize: '10px',
268
+ backgroundColor: color,
269
+ color: 'white',
270
+ padding: '2px 6px',
271
+ borderRadius: '4px',
272
+ textAlign: 'center',
273
+ }}
274
+ >
274
275
  {nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
275
276
  </div>
276
277
  )}
277
278
  {hasViolations && (
278
- <div style={{
279
- fontSize: '10px',
280
- color: '#D0021B',
281
- fontWeight: 'bold',
282
- }}>
279
+ <div
280
+ style={{
281
+ fontSize: '10px',
282
+ color: '#D0021B',
283
+ fontWeight: 'bold',
284
+ }}
285
+ >
283
286
  ⚠️
284
287
  </div>
285
288
  )}
@@ -288,29 +291,58 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
288
291
  ) : (
289
292
  <div style={getShapeStyles()} className={animationClass}>
290
293
  {/* Inner content (rotated back if diamond) */}
291
- <div style={isDiamond ? { transform: 'rotate(-45deg)' } : {}}>
292
- {icon && <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{resolveIcon(icon, 20)}</div>}
293
- <div style={{ textAlign: 'center', wordBreak: 'break-word' }}>
294
- {displayName}
295
- </div>
294
+ <div
295
+ style={{
296
+ ...(isDiamond ? { transform: 'rotate(-45deg)' } : {}),
297
+ ...(isGroup ? { width: '100%' } : {}),
298
+ }}
299
+ >
300
+ {/* Groups: icon and text inline, centered */}
301
+ {isGroup ? (
302
+ <div
303
+ style={{
304
+ display: 'flex',
305
+ alignItems: 'center',
306
+ justifyContent: 'center',
307
+ gap: '6px',
308
+ width: '100%',
309
+ }}
310
+ >
311
+ {icon && resolveIcon(icon, 18)}
312
+ <div style={{ wordBreak: 'break-word' }}>{displayName}</div>
313
+ </div>
314
+ ) : (
315
+ <>
316
+ {icon && (
317
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
318
+ {resolveIcon(icon, 20)}
319
+ </div>
320
+ )}
321
+ <div style={{ textAlign: 'center', wordBreak: 'break-word' }}>{displayName}</div>
322
+ </>
323
+ )}
296
324
  {state && (
297
- <div style={{
298
- fontSize: '10px',
299
- backgroundColor: color,
300
- color: 'white',
301
- padding: '2px 6px',
302
- borderRadius: '4px',
303
- textAlign: 'center',
304
- }}>
325
+ <div
326
+ style={{
327
+ fontSize: '10px',
328
+ backgroundColor: color,
329
+ color: 'white',
330
+ padding: '2px 6px',
331
+ borderRadius: '4px',
332
+ textAlign: 'center',
333
+ }}
334
+ >
305
335
  {nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
306
336
  </div>
307
337
  )}
308
338
  {hasViolations && (
309
- <div style={{
310
- fontSize: '10px',
311
- color: '#D0021B',
312
- fontWeight: 'bold',
313
- }}>
339
+ <div
340
+ style={{
341
+ fontSize: '10px',
342
+ color: '#D0021B',
343
+ fontWeight: 'bold',
344
+ }}
345
+ >
314
346
  ⚠️
315
347
  </div>
316
348
  )}
@@ -318,19 +350,15 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
318
350
  </div>
319
351
  )}
320
352
 
321
- {/* Output handles - multiple connection points */}
353
+ {/* Output handles - all 4 sides for outgoing connections */}
354
+ <Handle type="source" position={Position.Top} id="top-out" style={getHandleStyle('top')} />
322
355
  <Handle
323
356
  type="source"
324
357
  position={Position.Bottom}
325
- id="bottom"
358
+ id="bottom-out"
326
359
  style={getHandleStyle('bottom')}
327
360
  />
328
- <Handle
329
- type="source"
330
- position={Position.Left}
331
- id="left-out"
332
- style={getHandleStyle('left')}
333
- />
361
+ <Handle type="source" position={Position.Left} id="left-out" style={getHandleStyle('left')} />
334
362
  <Handle
335
363
  type="source"
336
364
  position={Position.Right}
@@ -29,9 +29,10 @@ export const GenericNode: React.FC<GenericNodeProps> = ({
29
29
  hasViolations,
30
30
  }) => {
31
31
  // Get color based on state or default
32
- const color = state && typeDefinition.states?.[state]?.color
33
- ? typeDefinition.states[state].color
34
- : typeDefinition.color || '#888';
32
+ const color =
33
+ state && typeDefinition.states?.[state]?.color
34
+ ? typeDefinition.states[state].color
35
+ : typeDefinition.color || '#888';
35
36
 
36
37
  return (
37
38
  <div