@flowuent-org/diagramming-core 1.3.5 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,6 +14,7 @@ import { NodeActionButtons } from './NodeActionButtons';
14
14
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
15
15
  import { useSearch } from '../../contexts/SearchContext';
16
16
  import { getStatusColor } from './statusColors';
17
+ import { EndNodeIcon } from '@flowuent-labs/molecules';
17
18
 
18
19
  interface AutomationEndNodeProps {
19
20
  data: {
@@ -57,6 +58,7 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
57
58
 
58
59
  // Get the icon component based on the iconName
59
60
  const IconComponent = getIconByName(data.iconName);
61
+ const isEndNode = data.label === 'End' || data.label === 'End Node';
60
62
 
61
63
  const handleJsonClick = () => {
62
64
  if (nodeId) setSelectedNode(nodeId);
@@ -275,7 +277,11 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
275
277
  justifyContent: 'center',
276
278
  }}
277
279
  >
278
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
280
+ {isEndNode ? (
281
+ <EndNodeIcon size={18} color="#FFFFFF" />
282
+ ) : (
283
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
284
+ )}
279
285
  </Box>
280
286
  <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
281
287
  {highlightText(data.label)}
@@ -23,6 +23,7 @@ import { NodeActionButtons } from './NodeActionButtons';
23
23
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
24
24
  import { useSearch } from '../../contexts/SearchContext';
25
25
  import { getStatusColor } from './statusColors';
26
+ import { ArticleAnalyzerIcon } from '@flowuent-labs/molecules';
26
27
 
27
28
  interface AutomationFormattingNodeProps {
28
29
  data: {
@@ -80,8 +81,13 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
80
81
  const nodes = useDiagram((state) => state.nodes);
81
82
  const setNodes = useDiagram((state) => state.setNodes);
82
83
 
83
- // Get the icon component based on the iconName
84
- const IconComponent = getIconByName(data.iconName);
84
+ // Check if this is Article Analyzer node
85
+ const isArticleAnalyzer = data.label === 'Article Analyzer';
86
+
87
+ // Get the icon component based on the iconName or use ArticleAnalyzerIcon for Article Analyzer
88
+ const IconComponent = isArticleAnalyzer
89
+ ? ArticleAnalyzerIcon
90
+ : getIconByName(data.iconName);
85
91
 
86
92
  const handleJsonClick = () => {
87
93
  if (nodeId) setSelectedNode(nodeId);
@@ -342,14 +348,18 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
342
348
  sx={{
343
349
  width: '32px',
344
350
  height: '32px',
345
- backgroundColor: '#8b5cf6', // Purple like in image
351
+ backgroundColor: '#1E3A8A',
346
352
  borderRadius: '50%',
347
353
  display: 'flex',
348
354
  alignItems: 'center',
349
355
  justifyContent: 'center',
350
356
  }}
351
357
  >
352
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
358
+ {isArticleAnalyzer ? (
359
+ <ArticleAnalyzerIcon size={18} color="#FFFFFF" />
360
+ ) : (
361
+ <IconComponent size={18} color="#FFFFFF" />
362
+ )}
353
363
  </Box>
354
364
  <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
355
365
  {highlightText(data.label)}
@@ -4,7 +4,7 @@ import { Handle, Position, useNodeId } from '@xyflow/react';
4
4
  import { Box, Typography, Chip, IconButton, Card, CardContent, Tooltip } from '@mui/material';
5
5
  import {
6
6
  AccessTime as AccessTimeIcon,
7
- Navigation as NavigationIcon,
7
+ Navigation as MuiNavigationIcon,
8
8
  OpenInBrowser as OpenInBrowserIcon,
9
9
  TouchApp as TouchAppIcon,
10
10
  Search as SearchIcon,
@@ -19,6 +19,7 @@ import { NodeActionButtons } from './NodeActionButtons';
19
19
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
20
20
  import { useSearch } from '../../contexts/SearchContext';
21
21
  import { getStatusColor } from './statusColors';
22
+ import { NavigationIcon } from '@flowuent-labs/molecules';
22
23
 
23
24
  interface AutomationNavigationNodeProps {
24
25
  data: {
@@ -212,13 +213,13 @@ export const AutomationNavigationNode: React.FC<AutomationNavigationNodeProps> =
212
213
  case 'click':
213
214
  return <TouchAppIcon sx={{ fontSize: '18px' }} />;
214
215
  case 'scroll':
215
- return <NavigationIcon sx={{ fontSize: '18px' }} />;
216
+ return <NavigationIcon size={18} color="#93C5FD" />;
216
217
  case 'wait':
217
218
  return <AccessTimeIcon sx={{ fontSize: '18px' }} />;
218
219
  case 'extract':
219
220
  return <SearchIcon sx={{ fontSize: '18px' }} />;
220
221
  default:
221
- return <NavigationIcon sx={{ fontSize: '18px' }} />;
222
+ return <NavigationIcon size={18} color="#93C5FD" />;
222
223
  }
223
224
  };
224
225
 
@@ -66,6 +66,7 @@ export interface DiagramState {
66
66
  nodes: ICardNode[];
67
67
  defaultNodes: ICardNode[];
68
68
  dragStartPosition: DragStartPosition | null;
69
+ dragStartPositions: Record<string, { x: number; y: number }> | null;
69
70
  edges: Edge[];
70
71
  defaultEdges: Edge[];
71
72
  history: HistoryEvent[];
@@ -20,9 +20,26 @@ export const onNodeDragStart =
20
20
  positionAbsolute: cardNode.position,
21
21
  };
22
22
 
23
+ // Store initial positions for ALL selected nodes (for multi-node dragging)
24
+ // Get selected nodes from state.nodes (they should have selected property set by ReactFlow)
25
+ const selectedNodes = state.nodes.filter(n => (n as any).selected);
26
+ let dragStartPositions: Record<string, { x: number; y: number }> | null = null;
27
+
28
+ if (selectedNodes.length > 1) {
29
+ // Multiple nodes are selected - store all their initial positions
30
+ dragStartPositions = {};
31
+ selectedNodes.forEach(selectedNode => {
32
+ dragStartPositions![selectedNode.id] = {
33
+ x: selectedNode.position.x,
34
+ y: selectedNode.position.y,
35
+ };
36
+ });
37
+ }
38
+
23
39
  return {
24
40
  ...state,
25
41
  dragStartPosition: changeForUndo,
42
+ dragStartPositions: dragStartPositions || null,
26
43
  };
27
44
  });
28
45
  };
@@ -18,10 +18,101 @@ export const onNodeDragEnd =
18
18
  const cardNode = node as ICardNode;
19
19
  set((state: DiagramStore) => {
20
20
  const dragStartPosition = state.dragStartPosition;
21
+ const dragStartPositions = state.dragStartPositions;
21
22
 
22
- // If there's no drag start position, just return the state unchanged
23
+ // Get all currently selected nodes from state (they should have selected property)
24
+ // Also get the current node's position from ReactFlow (cardNode parameter)
25
+ const selectedNodesInState = state.nodes.filter(n => (n as any).selected);
26
+ const isMultiDrag = dragStartPositions && Object.keys(dragStartPositions).length > 1;
27
+
28
+ // If multiple nodes were dragged, handle all of them
29
+ if (isMultiDrag && dragStartPositions) {
30
+ const historyEvents: HistoryEvent[] = [];
31
+ const updatedNodes = state.nodes.map((n) => {
32
+ // Check if this node was part of the multi-drag
33
+ if (dragStartPositions![n.id]) {
34
+ const startPos = dragStartPositions[n.id];
35
+ // For the dragged node, use position from ReactFlow's node parameter (most up-to-date)
36
+ // For other nodes, use position from state.nodes (updated by onNodesChange)
37
+ let endPos = n.position;
38
+ if (n.id === cardNode.id) {
39
+ // Use the position from ReactFlow for the node being dragged
40
+ endPos = cardNode.position;
41
+ }
42
+
43
+ // Check if this node moved
44
+ if (startPos.x !== endPos.x || startPos.y !== endPos.y) {
45
+ // Create history event for this node
46
+ historyEvents.push({
47
+ title: `Node '${n.id}' moved from (${startPos.x.toFixed(1)}, ${startPos.y.toFixed(1)}) to (${endPos.x.toFixed(1)}, ${endPos.y.toFixed(1)})`,
48
+ backward: {
49
+ t: 'node-change',
50
+ type: 'position',
51
+ id: n.id,
52
+ message: `Revert: Node '${n.id}' position back to (${startPos.x.toFixed(1)}, ${startPos.y.toFixed(1)})`,
53
+ position: startPos,
54
+ positionAbsolute: startPos,
55
+ },
56
+ forward: {
57
+ t: 'node-change',
58
+ type: 'position',
59
+ id: n.id,
60
+ message: `Redo: Node '${n.id}' moved to (${endPos.x}, ${endPos.y})`,
61
+ position: endPos,
62
+ positionAbsolute: endPos,
63
+ },
64
+ });
65
+
66
+ // Update node position - ensure it matches ReactFlow's current state
67
+ return {
68
+ ...n,
69
+ position: endPos,
70
+ data: { ...n.data, isPinned: true },
71
+ };
72
+ }
73
+ }
74
+ return n;
75
+ });
76
+
77
+ // Trigger onChange callbacks for all moved nodes
78
+ if (onChange && historyEvents.length > 0) {
79
+ historyEvents.forEach((historyEvent) => {
80
+ onChange({
81
+ t: 'node-change',
82
+ type: 'position',
83
+ id: historyEvent.forward.id,
84
+ position: historyEvent.forward.position,
85
+ positionAbsolute: historyEvent.forward.positionAbsolute,
86
+ message: historyEvent.title,
87
+ });
88
+ });
89
+ }
90
+
91
+ return {
92
+ ...addToHistory(state, historyEvents),
93
+ dragStartPosition: null,
94
+ dragStartPositions: null,
95
+ nodes: updatedNodes,
96
+ };
97
+ }
98
+
99
+ // Single node drag handling (original logic)
23
100
  if (!dragStartPosition) {
24
- return state;
101
+ // Ensure the node's current position is persisted in state
102
+ const nodeInState = state.nodes.find((n) => n.id === cardNode.id);
103
+ if (nodeInState && (
104
+ nodeInState.position.x !== cardNode.position.x ||
105
+ nodeInState.position.y !== cardNode.position.y
106
+ )) {
107
+ // Update the node position to match the dragged position
108
+ const updatedNodes = state.nodes.map((n) =>
109
+ n.id === cardNode.id
110
+ ? { ...n, position: cardNode.position, data: { ...n.data, isPinned: true } }
111
+ : n,
112
+ );
113
+ return { ...state, nodes: updatedNodes, dragStartPositions: null };
114
+ }
115
+ return { ...state, dragStartPositions: null };
25
116
  }
26
117
 
27
118
  // Check if the node has moved
@@ -29,17 +120,21 @@ export const onNodeDragEnd =
29
120
  dragStartPosition.position.x !== cardNode.position.x ||
30
121
  dragStartPosition.position.y !== cardNode.position.y
31
122
  ) {
32
- const updatedNodes = state.nodes.map((n) =>
33
- n.id === cardNode.id
34
- ? { ...n, data: { ...n.data, isPinned: true } }
35
- : n,
36
- );
123
+ // Update nodes array to ensure position is persisted
124
+ const updatedNodes = state.nodes.map((n) => {
125
+ if (n.id === cardNode.id) {
126
+ return {
127
+ ...n,
128
+ position: cardNode.position,
129
+ data: { ...n.data, isPinned: true }
130
+ };
131
+ }
132
+ return n;
133
+ });
37
134
 
38
- // Create the HistoryEvent entry with forward and backward changes
135
+ // Create the HistoryEvent entry
39
136
  const historyEvent: HistoryEvent = {
40
137
  title: `Node '${cardNode.id}' moved from (${dragStartPosition.position.x.toFixed(1)}, ${dragStartPosition.position.y.toFixed(1)}) to (${cardNode.position.x.toFixed(1)}, ${cardNode.position.y.toFixed(1)})`,
41
-
42
- // Backward change for undo
43
138
  backward: {
44
139
  t: 'node-change',
45
140
  type: 'position',
@@ -48,8 +143,6 @@ export const onNodeDragEnd =
48
143
  position: dragStartPosition.position,
49
144
  positionAbsolute: dragStartPosition.position,
50
145
  },
51
-
52
- // Forward change for redo
53
146
  forward: {
54
147
  t: 'node-change',
55
148
  type: 'position',
@@ -74,12 +167,13 @@ export const onNodeDragEnd =
74
167
 
75
168
  return {
76
169
  ...addToHistory(state, [historyEvent]),
77
- dragStartPosition: null, // Clear the drag start position
170
+ dragStartPosition: null,
171
+ dragStartPositions: null,
78
172
  nodes: updatedNodes,
79
173
  };
80
174
  }
81
175
 
82
- // If no movement occurred, just reset dragStartPosition without updating history
83
- return { ...state, dragStartPosition: null };
176
+ // If no movement occurred, just reset dragStartPosition
177
+ return { ...state, dragStartPosition: null, dragStartPositions: null };
84
178
  });
85
179
  };
@@ -277,24 +277,44 @@ function handleElementChanges(
277
277
 
278
278
  const currentElements = elementType === 'node' ? get().nodes : get().edges;
279
279
 
280
+ // Separate position changes from other changes
281
+ const positionChanges: NodePositionChange[] = [];
282
+ const dimensionChanges: NodeDimensionChange[] = [];
283
+ const otherChanges: (NodeChange | EdgeChange)[] = [];
284
+
280
285
  changes.forEach((change) => {
281
- if (change.type === 'position' || change.type === 'dimensions') {
282
- return;
286
+ if (change.type === 'position') {
287
+ positionChanges.push(change as NodePositionChange);
288
+ } else if (change.type === 'dimensions') {
289
+ dimensionChanges.push(change as NodeDimensionChange);
283
290
  } else if (change.type === 'replace') {
284
291
  accumulatedResetChanges.push(change as ResetChange & { t: string });
285
292
  } else {
286
- const historyEvent = createHistoryEvent(
287
- change,
288
- currentElements,
289
- elementType,
290
- );
291
- if (historyEvent) {
292
- newChanges.push(historyEvent.forward);
293
- newHistory.push(historyEvent);
294
- }
293
+ otherChanges.push(change);
294
+ }
295
+ });
296
+
297
+ // Handle other changes (add, remove, etc.)
298
+ otherChanges.forEach((change) => {
299
+ const historyEvent = createHistoryEvent(
300
+ change,
301
+ currentElements,
302
+ elementType,
303
+ );
304
+ if (historyEvent) {
305
+ newChanges.push(historyEvent.forward);
306
+ newHistory.push(historyEvent);
295
307
  }
296
308
  });
297
309
 
310
+ // Handle position changes - apply them but don't add to history
311
+ // History will be handled by onNodeDragEnd for individual nodes
312
+ // For multiple nodes, we need to ensure positions are applied
313
+ if (positionChanges.length > 0 && elementType === 'node') {
314
+ // Apply position changes immediately to ensure they persist
315
+ // Don't add to history here as onNodeDragEnd will handle it
316
+ }
317
+
298
318
  if (accumulatedResetChanges.length > 0) {
299
319
  const [resetChangesToEmit, historyChanges] = handleAccumulatedResets(
300
320
  accumulatedResetChanges,
@@ -310,6 +330,7 @@ function handleElementChanges(
310
330
  newChanges.forEach((change) => onChange(change));
311
331
  }
312
332
 
333
+ // Apply ALL changes including position changes to ensure they persist
313
334
  const updatedElements =
314
335
  elementType === 'node'
315
336
  ? applyNodeChanges(
@@ -1,25 +1,113 @@
1
1
  import React from 'react';
2
- import { Typography } from '@mui/material';
3
- import { useNodeTitle, useNodeType } from '../../contexts/CardDataProvider';
2
+ import { Typography, Box } from '@mui/material';
3
+ import { useNodeTitle, useNodeType, useNodeVariant } from '../../contexts/CardDataProvider';
4
+ import { NodeVariant } from '../../types/card-node';
5
+ import {
6
+ ParallelIcon,
7
+ BranchIcon,
8
+ GroupIcon,
9
+ ReturnIcon,
10
+ LoopIcon,
11
+ SwitchIcon,
12
+ TryIcon,
13
+ CatchIcon,
14
+ TryCatchIcon,
15
+ FunctionIcon,
16
+ CallIcon,
17
+ LetIcon,
18
+ SetIcon,
19
+ EmptyNodeIcon,
20
+ EntityIcon,
21
+ } from '@flowuent-labs/molecules';
22
+
23
+ // Icon mapping for different node types
24
+ const getNodeIcon = (type: NodeVariant | string | undefined) => {
25
+ if (!type) return null;
26
+
27
+ const iconStyle = {
28
+ marginRight: '6px',
29
+ display: 'inline-block',
30
+ verticalAlign: 'middle',
31
+ flexShrink: 0,
32
+ };
33
+
34
+ switch (type) {
35
+ case 'parallel':
36
+ return <ParallelIcon size={15} color="#86EFAC" style={iconStyle} />;
37
+ case 'branch':
38
+ return <BranchIcon size={15} color="#86EFAC" style={iconStyle} />;
39
+ case 'group':
40
+ return <GroupIcon size={15} color="#86EFAC" style={iconStyle} />;
41
+ case 'return':
42
+ return <ReturnIcon size={15} color="#86EFAC" style={iconStyle} />;
43
+ case 'loop':
44
+ case 'forLoop':
45
+ return <LoopIcon size={15} color="#86EFAC" style={iconStyle} />;
46
+ case 'switch':
47
+ return <SwitchIcon size={15} color="#86EFAC" style={iconStyle} />;
48
+ case 'try':
49
+ return <TryIcon size={15} color="#86EFAC" style={iconStyle} />;
50
+ case 'catch':
51
+ return <CatchIcon size={15} color="#86EFAC" style={iconStyle} />;
52
+ case 'tryCatch':
53
+ return <TryCatchIcon size={15} color="#86EFAC" style={iconStyle} />;
54
+ case 'function':
55
+ return <FunctionIcon size={15} color="#86EFAC" style={iconStyle} />;
56
+ case 'call':
57
+ return <CallIcon size={15} color="#86EFAC" style={iconStyle} />;
58
+ case 'let':
59
+ return <LetIcon size={15} color="#86EFAC" style={iconStyle} />;
60
+ case 'set':
61
+ return <SetIcon size={15} color="#86EFAC" style={iconStyle} />;
62
+ case 'emptyNode':
63
+ return <EmptyNodeIcon size={15} color="#86EFAC" style={iconStyle} />;
64
+ case 'entity':
65
+ return <EntityIcon size={15} color="#86EFAC" style={iconStyle} />;
66
+ default:
67
+ return null;
68
+ }
69
+ };
4
70
 
5
71
  const NodeTypeDisplay = React.memo(() => {
6
72
  const type = useNodeType();
73
+ const variant = useNodeVariant();
7
74
  const title = useNodeTitle();
75
+ // Get type from data.type or data.variant as fallback
76
+ const nodeType = (type || variant) as NodeVariant | string | undefined;
77
+ const icon = getNodeIcon(nodeType as NodeVariant);
78
+
8
79
  return (
9
- <Typography
10
- sx={{
11
- textTransform: 'capitalize',
12
- // fontWeight: 'bold',
13
- fontStyle: 'italic',
14
- fontSize: 12,
15
- color: '#FFFFFF',
16
- }}
17
- >
18
- {`[${type}]`}
19
- {(title === 'Then' || title === 'Else') && (
20
- <span >{': ' + title}</span>
80
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'nowrap' }}>
81
+ {icon && (
82
+ <Box
83
+ component="span"
84
+ sx={{
85
+ display: 'inline-flex',
86
+ alignItems: 'center',
87
+ justifyContent: 'center',
88
+ flexShrink: 0,
89
+ lineHeight: 1,
90
+ }}
91
+ >
92
+ {icon}
93
+ </Box>
21
94
  )}
22
- </Typography>
95
+ <Typography
96
+ sx={{
97
+ textTransform: 'capitalize',
98
+ fontStyle: 'italic',
99
+ fontSize: 12,
100
+ color: '#FFFFFF',
101
+ display: 'inline-flex',
102
+ alignItems: 'center',
103
+ }}
104
+ >
105
+ {`[${nodeType || 'unknown'}]`}
106
+ {(title === 'Then' || title === 'Else') && (
107
+ <span>{': ' + title}</span>
108
+ )}
109
+ </Typography>
110
+ </Box>
23
111
  );
24
112
  });
25
113
 
@@ -0,0 +1,23 @@
1
+ // Re-export all icons from molecules package
2
+ export {
3
+ ParallelIcon,
4
+ BranchIcon,
5
+ GroupIcon,
6
+ ReturnIcon,
7
+ LoopIcon,
8
+ SwitchIcon,
9
+ TryIcon,
10
+ CatchIcon,
11
+ TryCatchIcon,
12
+ FunctionIcon,
13
+ CallIcon,
14
+ LetIcon,
15
+ SetIcon,
16
+ EmptyNodeIcon,
17
+ EntityIcon,
18
+ StartNodeIcon,
19
+ NavigationIcon,
20
+ } from '@flowuent-labs/molecules';
21
+
22
+ // Re-export types for backward compatibility
23
+ export type { SvgIconProps as IconProps } from '@flowuent-labs/molecules';
@@ -87,13 +87,19 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
87
87
  const canRedo = useCanRedo();
88
88
 
89
89
  // Zoom hooks - get zoom functions from ReactFlow
90
- const { zoomIn, zoomOut, fitView, setViewport, getViewport, getNodes } = useReactFlow();
90
+ const { zoomIn, zoomOut, fitView, setViewport, getViewport, getNodes, screenToFlowPosition } = useReactFlow();
91
91
 
92
92
  // Search bar state
93
93
  const [showSearchBar, setShowSearchBar] = useState(false);
94
94
 
95
- // Clipboard state for copy/paste
96
- const [copiedNode, setCopiedNode] = useState<Node | null>(null);
95
+ // Clipboard state for copy/paste - support multiple nodes
96
+ const [copiedNodes, setCopiedNodes] = useState<Node[]>([]);
97
+
98
+ // Store last click position on canvas for paste positioning
99
+ const [lastClickPosition, setLastClickPosition] = useState<{ x: number; y: number } | null>(null);
100
+
101
+ // Flag to prevent layout recalculation immediately after paste
102
+ const skipLayoutRef = useRef(false);
97
103
 
98
104
  // Rename state
99
105
  const [renamingNodeId, setRenamingNodeId] = useState<string | null>(null);
@@ -109,40 +115,111 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
109
115
  setShowSearchBar(true);
110
116
  }, []);
111
117
 
112
- // Copy selected node
118
+ // Copy selected nodes (single or multiple)
113
119
  const handleCopyNode = useCallback(() => {
114
- const nodeToCopy = visibleNodes.find(node => node.id === selectedNode || node.selected);
115
- if (nodeToCopy) {
116
- setCopiedNode(nodeToCopy);
120
+ // Get all selected nodes from ReactFlow
121
+ const selectedNodesList = getNodes().filter(node => node.selected);
122
+
123
+ // If multiple nodes are selected, copy all of them
124
+ if (selectedNodesList.length > 0) {
125
+ setCopiedNodes(selectedNodesList);
126
+ } else {
127
+ // Fallback to single node selection (custom selectedNode state)
128
+ const nodeToCopy = visibleNodes.find(node => node.id === selectedNode);
129
+ if (nodeToCopy) {
130
+ setCopiedNodes([nodeToCopy]);
131
+ }
117
132
  }
118
- }, [visibleNodes, selectedNode]);
133
+ }, [visibleNodes, selectedNode, getNodes]);
119
134
 
120
- // Paste node
135
+ // Paste nodes (single or multiple) at the clicked position
121
136
  const handlePasteNode = useCallback(() => {
122
- if (!copiedNode) return;
137
+ if (copiedNodes.length === 0) return;
123
138
 
124
- const newNode = {
125
- ...copiedNode,
126
- id: `${copiedNode.id}-copy-${Date.now()}`,
127
- position: {
128
- x: copiedNode.position.x + 50,
129
- y: copiedNode.position.y + 50,
130
- },
131
- selected: false,
132
- };
139
+ // Determine paste position:
140
+ // 1. Use last click position on canvas if available
141
+ // 2. Fallback to offset from original position
142
+ let pasteX: number;
143
+ let pasteY: number;
144
+
145
+ if (lastClickPosition) {
146
+ // Use stored click position
147
+ pasteX = lastClickPosition.x;
148
+ pasteY = lastClickPosition.y;
149
+ } else {
150
+ // Fallback: use offset from original position
151
+ const minX = Math.min(...copiedNodes.map(node => node.position.x));
152
+ const minY = Math.min(...copiedNodes.map(node => node.position.y));
153
+ pasteX = minX + 50;
154
+ pasteY = minY + 50;
155
+ }
156
+
157
+ // Find the minimum X and Y positions of copied nodes (reference point)
158
+ const minX = Math.min(...copiedNodes.map(node => node.position.x));
159
+ const minY = Math.min(...copiedNodes.map(node => node.position.y));
160
+
161
+ // Calculate offset from reference point to paste position
162
+ const offsetX = pasteX - minX;
163
+ const offsetY = pasteY - minY;
164
+
165
+ // Create new nodes with updated IDs and positions
166
+ const newNodes = copiedNodes.map((node, index) => {
167
+ // Calculate relative position from the minimum position
168
+ const relativeX = node.position.x - minX;
169
+ const relativeY = node.position.y - minY;
170
+
171
+ return {
172
+ ...node,
173
+ id: `${node.id}-copy-${Date.now()}-${index}`,
174
+ position: {
175
+ x: minX + relativeX + offsetX,
176
+ y: minY + relativeY + offsetY,
177
+ },
178
+ selected: false,
179
+ data: {
180
+ ...node.data,
181
+ isPinned: true, // Mark as pinned to prevent auto-layout from moving it
182
+ },
183
+ };
184
+ });
185
+
186
+ // Prevent layout recalculation from moving pasted nodes
187
+ skipLayoutRef.current = true;
188
+
189
+ // Add all new nodes to the diagram
190
+ setNodes([...nodes, ...newNodes as any]);
133
191
 
134
- setNodes([...nodes, newNode as any]);
135
- setSelectedNode(newNode.id);
136
- }, [copiedNode, nodes, setNodes, setSelectedNode]);
192
+ // Select the first pasted node (or all if multiple)
193
+ if (newNodes.length === 1) {
194
+ setSelectedNode(newNodes[0].id);
195
+ } else {
196
+ // For multiple nodes, select the first one (ReactFlow will handle multi-selection)
197
+ setSelectedNode(newNodes[0].id);
198
+ }
199
+ }, [copiedNodes, nodes, setNodes, setSelectedNode, lastClickPosition]);
137
200
 
138
- // Delete selected node
201
+ // Delete selected nodes (single or multiple)
139
202
  const handleDeleteNode = useCallback(() => {
140
- const nodeToDelete = visibleNodes.find(node => node.id === selectedNode || node.selected);
141
- if (nodeToDelete && onNodesChange) {
142
- onNodesChange([{ id: nodeToDelete.id, type: 'remove' }]);
203
+ // Get all selected nodes from ReactFlow
204
+ const selectedNodesList = getNodes().filter(node => node.selected);
205
+
206
+ if (selectedNodesList.length > 0) {
207
+ // Delete all selected nodes
208
+ const changes = selectedNodesList.map(node => ({
209
+ id: node.id,
210
+ type: 'remove' as const,
211
+ }));
212
+ onNodesChange(changes);
143
213
  setSelectedNode(null);
214
+ } else {
215
+ // Fallback to single node deletion (custom selectedNode state)
216
+ const nodeToDelete = visibleNodes.find(node => node.id === selectedNode);
217
+ if (nodeToDelete && onNodesChange) {
218
+ onNodesChange([{ id: nodeToDelete.id, type: 'remove' }]);
219
+ setSelectedNode(null);
220
+ }
144
221
  }
145
- }, [visibleNodes, selectedNode, onNodesChange, setSelectedNode]);
222
+ }, [visibleNodes, selectedNode, onNodesChange, setSelectedNode, getNodes]);
146
223
 
147
224
  // Rename node
148
225
  const handleRenameNode = useCallback(() => {
@@ -388,6 +465,12 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
388
465
 
389
466
  // Debounce layout recalculation to prevent continuous propagation
390
467
  useEffect(() => {
468
+ // Skip layout recalculation if we just pasted nodes
469
+ if (skipLayoutRef.current) {
470
+ skipLayoutRef.current = false;
471
+ return;
472
+ }
473
+
391
474
  // Clear any pending layout recalculation
392
475
  if (layoutTimeoutRef.current) {
393
476
  clearTimeout(layoutTimeoutRef.current);
@@ -536,18 +619,35 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
536
619
  onEdgesChange={onEdgesChange}
537
620
  onReconnect={onReconnect}
538
621
  onNodeClick={(event, node) => {
539
- event.stopPropagation();
540
- setSelectedNode(node.id);
622
+ // Don't interfere with multi-selection - let ReactFlow handle it
623
+ // Only set our custom selectedNode if it's a single click without modifiers
624
+ if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
625
+ // Check if multiple nodes are selected - if so, don't override
626
+ const selectedNodes = getNodes().filter(n => n.selected);
627
+ if (selectedNodes.length <= 1) {
628
+ event.stopPropagation();
629
+ setSelectedNode(node.id);
630
+ }
631
+ // If multiple nodes are selected, let ReactFlow handle the drag
632
+ }
541
633
  }}
542
634
  onPaneClick={(event) => {
543
635
  setSelectedNode(null);
544
636
  closeModalsOnClickOutside(event);
637
+ // Store click position for paste operation
638
+ const flowPosition = screenToFlowPosition({
639
+ x: event.clientX,
640
+ y: event.clientY,
641
+ });
642
+ setLastClickPosition(flowPosition);
545
643
  }}
546
644
  nodesConnectable={true}
547
645
  nodesFocusable={true}
548
646
  edgesFocusable={true}
549
647
  elementsSelectable={true}
550
648
  selectNodesOnDrag={false}
649
+ selectionOnDrag={true}
650
+ selectionKeyCode={["Control", "Meta"]}
551
651
  defaultEdgeOptions={{
552
652
  reconnectable: true,
553
653
  }}
@@ -672,7 +772,30 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
672
772
  connectionMode={ConnectionMode.Strict}
673
773
  suppressContentEditableWarning
674
774
  suppressHydrationWarning
675
- onPaneClick={closeModalsOnClickOutside}
775
+ onPaneClick={(event) => {
776
+ closeModalsOnClickOutside(event);
777
+ // Store click position for paste operation
778
+ const flowPosition = screenToFlowPosition({
779
+ x: event.clientX,
780
+ y: event.clientY,
781
+ });
782
+ setLastClickPosition(flowPosition);
783
+ }}
784
+ onNodeClick={(event, node) => {
785
+ // Don't interfere with multi-selection - let ReactFlow handle it
786
+ // Only set our custom selectedNode if it's a single click without modifiers
787
+ if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
788
+ // Check if multiple nodes are selected - if so, don't override
789
+ const selectedNodes = getNodes().filter(n => n.selected);
790
+ if (selectedNodes.length <= 1) {
791
+ event.stopPropagation();
792
+ setSelectedNode(node.id);
793
+ }
794
+ // If multiple nodes are selected, let ReactFlow handle the drag
795
+ }
796
+ }}
797
+ selectionOnDrag={true}
798
+ selectionKeyCode={["Control", "Meta"]}
676
799
  {...props}
677
800
  />
678
801
  {props.children}
@@ -6,7 +6,29 @@ import ParamsActionField from './lib/ParamsActionField/ParamsActionField';
6
6
  import EditorTab from './lib/EditorTab/EditorTab';
7
7
  import CloseableButton from './lib/ClosableButton/CloseableButton';
8
8
 
9
-
9
+ // Export SVG Icons
10
+ export {
11
+ ArticleAnalyzerIcon,
12
+ StartNodeIcon,
13
+ EndNodeIcon,
14
+ NavigationIcon,
15
+ ParallelIcon,
16
+ BranchIcon,
17
+ GroupIcon,
18
+ ReturnIcon,
19
+ LoopIcon,
20
+ SwitchIcon,
21
+ TryIcon,
22
+ CatchIcon,
23
+ TryCatchIcon,
24
+ FunctionIcon,
25
+ CallIcon,
26
+ LetIcon,
27
+ SetIcon,
28
+ EmptyNodeIcon,
29
+ EntityIcon,
30
+ } from './lib/SvgIcons';
31
+ export type { SvgIconProps } from './lib/SvgIcons';
10
32
 
11
33
  export {
12
34
  UserCard,
@@ -15,8 +37,5 @@ export {
15
37
  HTTPVerbSelection,
16
38
  ParamsActionField,
17
39
  EditorTab,
18
-
19
-
20
-
21
40
  CloseableButton
22
41
  };
@@ -0,0 +1,291 @@
1
+ import React from 'react';
2
+ import { SvgIconProps } from './types';
3
+
4
+ // Base SVG icon component
5
+ const BaseIcon: React.FC<SvgIconProps & { children: React.ReactNode }> = ({
6
+ size = 15,
7
+ color = '#86EFAC',
8
+ style,
9
+ children
10
+ }) => (
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 15 15"
15
+ fill="none"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ style={style}
18
+ >
19
+ {children}
20
+ </svg>
21
+ );
22
+
23
+ // Article Analyzer Icon
24
+ export const ArticleAnalyzerIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
25
+ <svg
26
+ width={size || 12}
27
+ height={size ? (size * 15) / 12 : 15}
28
+ viewBox="0 0 12 15"
29
+ fill="none"
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ style={style}
32
+ >
33
+ <path
34
+ d="M7.83333 0.5H1.83333C1.47971 0.5 1.14057 0.640476 0.890524 0.890524C0.640476 1.14057 0.5 1.47971 0.5 1.83333V12.5C0.5 12.8536 0.640476 13.1928 0.890524 13.4428C1.14057 13.6929 1.47971 13.8333 1.83333 13.8333H9.83333C10.187 13.8333 10.5261 13.6929 10.7761 13.4428C11.0262 13.1928 11.1667 12.8536 11.1667 12.5V3.83333L7.83333 0.5Z"
35
+ stroke={color || "#D8B4FE"}
36
+ strokeLinecap="round"
37
+ strokeLinejoin="round"
38
+ />
39
+ <path
40
+ d="M7.16797 0.5V3.16667C7.16797 3.52029 7.30844 3.85943 7.55849 4.10948C7.80854 4.35952 8.14768 4.5 8.5013 4.5H11.168"
41
+ stroke={color || "#D8B4FE"}
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ />
45
+ <path
46
+ d="M4.5013 5.16699H3.16797"
47
+ stroke={color || "#D8B4FE"}
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ />
51
+ <path
52
+ d="M8.5013 7.83398H3.16797"
53
+ stroke={color || "#D8B4FE"}
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ />
57
+ <path
58
+ d="M8.5013 10.5H3.16797"
59
+ stroke={color || "#D8B4FE"}
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ />
63
+ </svg>
64
+ );
65
+
66
+ // Start Node Icon
67
+ export const StartNodeIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
68
+ <BaseIcon size={size} color={color} style={style}>
69
+ <path
70
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
71
+ stroke={color}
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ />
75
+ <path
76
+ d="M5.83203 4.5L9.83203 7.16667L5.83203 9.83333V4.5Z"
77
+ stroke={color}
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ />
81
+ </BaseIcon>
82
+ );
83
+
84
+ // Navigation Icon
85
+ export const NavigationIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#93C5FD', style }) => (
86
+ <BaseIcon size={size} color={color} style={style}>
87
+ <path
88
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
89
+ stroke={color}
90
+ strokeLinecap="round"
91
+ strokeLinejoin="round"
92
+ />
93
+ <path
94
+ d="M7.16667 0.5C5.45482 2.29744 4.5 4.68449 4.5 7.16667C4.5 9.64884 5.45482 12.0359 7.16667 13.8333C8.87851 12.0359 9.83333 9.64884 9.83333 7.16667C9.83333 4.68449 8.87851 2.29744 7.16667 0.5Z"
95
+ stroke={color}
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ />
99
+ <path
100
+ d="M0.5 7.16699H13.8333"
101
+ stroke={color}
102
+ strokeLinecap="round"
103
+ strokeLinejoin="round"
104
+ />
105
+ </BaseIcon>
106
+ );
107
+
108
+ // Workflow node type icons
109
+ export const ParallelIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
110
+ <BaseIcon size={size} color={color} style={style}>
111
+ <path
112
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
113
+ stroke={color}
114
+ strokeLinecap="round"
115
+ strokeLinejoin="round"
116
+ />
117
+ </BaseIcon>
118
+ );
119
+
120
+ export const BranchIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
121
+ <BaseIcon size={size} color={color} style={style}>
122
+ <path
123
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
124
+ stroke={color}
125
+ strokeLinecap="round"
126
+ strokeLinejoin="round"
127
+ />
128
+ </BaseIcon>
129
+ );
130
+
131
+ export const GroupIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
132
+ <BaseIcon size={size} color={color} style={style}>
133
+ <path
134
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
135
+ stroke={color}
136
+ strokeLinecap="round"
137
+ strokeLinejoin="round"
138
+ />
139
+ </BaseIcon>
140
+ );
141
+
142
+ export const ReturnIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
143
+ <BaseIcon size={size} color={color} style={style}>
144
+ <path
145
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
146
+ stroke={color}
147
+ strokeLinecap="round"
148
+ strokeLinejoin="round"
149
+ />
150
+ </BaseIcon>
151
+ );
152
+
153
+ export const LoopIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
154
+ <BaseIcon size={size} color={color} style={style}>
155
+ <path
156
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
157
+ stroke={color}
158
+ strokeLinecap="round"
159
+ strokeLinejoin="round"
160
+ />
161
+ </BaseIcon>
162
+ );
163
+
164
+ export const SwitchIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
165
+ <BaseIcon size={size} color={color} style={style}>
166
+ <path
167
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
168
+ stroke={color}
169
+ strokeLinecap="round"
170
+ strokeLinejoin="round"
171
+ />
172
+ </BaseIcon>
173
+ );
174
+
175
+ export const TryIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
176
+ <BaseIcon size={size} color={color} style={style}>
177
+ <path
178
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
179
+ stroke={color}
180
+ strokeLinecap="round"
181
+ strokeLinejoin="round"
182
+ />
183
+ </BaseIcon>
184
+ );
185
+
186
+ export const CatchIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
187
+ <BaseIcon size={size} color={color} style={style}>
188
+ <path
189
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
190
+ stroke={color}
191
+ strokeLinecap="round"
192
+ strokeLinejoin="round"
193
+ />
194
+ </BaseIcon>
195
+ );
196
+
197
+ export const TryCatchIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
198
+ <BaseIcon size={size} color={color} style={style}>
199
+ <path
200
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
201
+ stroke={color}
202
+ strokeLinecap="round"
203
+ strokeLinejoin="round"
204
+ />
205
+ </BaseIcon>
206
+ );
207
+
208
+ export const FunctionIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
209
+ <BaseIcon size={size} color={color} style={style}>
210
+ <path
211
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
212
+ stroke={color}
213
+ strokeLinecap="round"
214
+ strokeLinejoin="round"
215
+ />
216
+ </BaseIcon>
217
+ );
218
+
219
+ export const CallIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
220
+ <BaseIcon size={size} color={color} style={style}>
221
+ <path
222
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
223
+ stroke={color}
224
+ strokeLinecap="round"
225
+ strokeLinejoin="round"
226
+ />
227
+ </BaseIcon>
228
+ );
229
+
230
+ export const LetIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
231
+ <BaseIcon size={size} color={color} style={style}>
232
+ <path
233
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
234
+ stroke={color}
235
+ strokeLinecap="round"
236
+ strokeLinejoin="round"
237
+ />
238
+ </BaseIcon>
239
+ );
240
+
241
+ export const SetIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
242
+ <BaseIcon size={size} color={color} style={style}>
243
+ <path
244
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
245
+ stroke={color}
246
+ strokeLinecap="round"
247
+ strokeLinejoin="round"
248
+ />
249
+ </BaseIcon>
250
+ );
251
+
252
+ export const EmptyNodeIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
253
+ <BaseIcon size={size} color={color} style={style}>
254
+ <path
255
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
256
+ stroke={color}
257
+ strokeLinecap="round"
258
+ strokeLinejoin="round"
259
+ />
260
+ </BaseIcon>
261
+ );
262
+
263
+ export const EntityIcon: React.FC<SvgIconProps> = ({ size = 15, color = '#86EFAC', style }) => (
264
+ <BaseIcon size={size} color={color} style={style}>
265
+ <path
266
+ d="M7.16667 13.8333C10.8486 13.8333 13.8333 10.8486 13.8333 7.16667C13.8333 3.48477 10.8486 0.5 7.16667 0.5C3.48477 0.5 0.5 3.48477 0.5 7.16667C0.5 10.8486 3.48477 13.8333 7.16667 13.8333Z"
267
+ stroke={color}
268
+ strokeLinecap="round"
269
+ strokeLinejoin="round"
270
+ />
271
+ </BaseIcon>
272
+ );
273
+
274
+ // End Node Icon
275
+ export const EndNodeIcon: React.FC<SvgIconProps> = ({ size = 13, color = '#FCA5A5', style }) => (
276
+ <svg
277
+ width={size}
278
+ height={size}
279
+ viewBox="0 0 13 13"
280
+ fill="none"
281
+ xmlns="http://www.w3.org/2000/svg"
282
+ style={style}
283
+ >
284
+ <path
285
+ d="M11.1667 0.5H1.83333C1.09695 0.5 0.5 1.09695 0.5 1.83333V11.1667C0.5 11.903 1.09695 12.5 1.83333 12.5H11.1667C11.903 12.5 12.5 11.903 12.5 11.1667V1.83333C12.5 1.09695 11.903 0.5 11.1667 0.5Z"
286
+ stroke={color}
287
+ strokeLinecap="round"
288
+ strokeLinejoin="round"
289
+ />
290
+ </svg>
291
+ );
@@ -0,0 +1,25 @@
1
+ // Export types
2
+ export type { SvgIconProps } from './types';
3
+
4
+ // Export all icons from single icons file
5
+ export {
6
+ ArticleAnalyzerIcon,
7
+ StartNodeIcon,
8
+ EndNodeIcon,
9
+ NavigationIcon,
10
+ ParallelIcon,
11
+ BranchIcon,
12
+ GroupIcon,
13
+ ReturnIcon,
14
+ LoopIcon,
15
+ SwitchIcon,
16
+ TryIcon,
17
+ CatchIcon,
18
+ TryCatchIcon,
19
+ FunctionIcon,
20
+ CallIcon,
21
+ LetIcon,
22
+ SetIcon,
23
+ EmptyNodeIcon,
24
+ EntityIcon,
25
+ } from './icons';
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export interface SvgIconProps {
4
+ size?: number;
5
+ color?: string;
6
+ style?: React.CSSProperties;
7
+ }