@principal-ai/principal-view-react 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/GraphRenderer.d.ts +10 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +120 -21
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/SelectionSidebar.d.ts +18 -0
- package/dist/components/SelectionSidebar.d.ts.map +1 -0
- package/dist/components/SelectionSidebar.js +183 -0
- package/dist/components/SelectionSidebar.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +87 -52
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +166 -22
- package/src/components/PendingChanges.test.tsx +433 -0
- package/src/components/SelectionSidebar.tsx +341 -0
- package/src/nodes/CustomNode.tsx +132 -56
- package/src/stories/NodeShapes.stories.tsx +430 -5
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { NodeState, NodeTypeDefinition } from '@principal-ai/principal-view-core';
|
|
3
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
4
|
+
import { resolveIcon } from '../utils/iconResolver';
|
|
5
|
+
|
|
6
|
+
export interface SelectionSidebarProps {
|
|
7
|
+
/** Set of selected node IDs */
|
|
8
|
+
selectedNodeIds: Set<string>;
|
|
9
|
+
/** All nodes in the graph */
|
|
10
|
+
nodes: NodeState[];
|
|
11
|
+
/** Node type definitions for icons and colors */
|
|
12
|
+
nodeTypeDefinitions: Record<string, NodeTypeDefinition>;
|
|
13
|
+
/** Callback when sidebar is closed */
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ExpandedState {
|
|
18
|
+
[nodeId: string]: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sidebar that displays information about multiple selected nodes
|
|
23
|
+
* Shows name and description for each node, with expandable details
|
|
24
|
+
*/
|
|
25
|
+
export const SelectionSidebar: React.FC<SelectionSidebarProps> = ({
|
|
26
|
+
selectedNodeIds,
|
|
27
|
+
nodes,
|
|
28
|
+
nodeTypeDefinitions,
|
|
29
|
+
onClose,
|
|
30
|
+
}) => {
|
|
31
|
+
const { theme } = useTheme();
|
|
32
|
+
const [expandedNodes, setExpandedNodes] = useState<ExpandedState>({});
|
|
33
|
+
|
|
34
|
+
// Get the selected nodes in order
|
|
35
|
+
const selectedNodes = nodes.filter((n) => selectedNodeIds.has(n.id));
|
|
36
|
+
|
|
37
|
+
const toggleExpanded = (nodeId: string) => {
|
|
38
|
+
setExpandedNodes((prev) => ({
|
|
39
|
+
...prev,
|
|
40
|
+
[nodeId]: !prev[nodeId],
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Internal fields that should not be displayed
|
|
45
|
+
const internalFields = [
|
|
46
|
+
'icon',
|
|
47
|
+
'name',
|
|
48
|
+
'description',
|
|
49
|
+
'sources',
|
|
50
|
+
'color',
|
|
51
|
+
'stroke',
|
|
52
|
+
'width',
|
|
53
|
+
'height',
|
|
54
|
+
'canvasType',
|
|
55
|
+
'text',
|
|
56
|
+
'file',
|
|
57
|
+
'url',
|
|
58
|
+
'shape',
|
|
59
|
+
'states',
|
|
60
|
+
'actions',
|
|
61
|
+
'nodeType',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: '60px',
|
|
69
|
+
right: '20px',
|
|
70
|
+
backgroundColor: theme.colors.background,
|
|
71
|
+
color: theme.colors.text,
|
|
72
|
+
borderRadius: '8px',
|
|
73
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
74
|
+
padding: '16px',
|
|
75
|
+
minWidth: '280px',
|
|
76
|
+
maxWidth: '350px',
|
|
77
|
+
maxHeight: 'calc(100vh - 120px)',
|
|
78
|
+
overflowY: 'auto',
|
|
79
|
+
zIndex: 1000,
|
|
80
|
+
border: `1px solid ${theme.colors.border}`,
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
display: 'flex',
|
|
87
|
+
justifyContent: 'space-between',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
marginBottom: '12px',
|
|
90
|
+
paddingBottom: '8px',
|
|
91
|
+
borderBottom: `2px solid ${theme.colors.primary}`,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div style={{ fontWeight: 'bold', fontSize: '14px', color: theme.colors.primary }}>
|
|
95
|
+
{selectedNodes.length} nodes selected
|
|
96
|
+
</div>
|
|
97
|
+
<button
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
style={{
|
|
100
|
+
border: 'none',
|
|
101
|
+
background: 'none',
|
|
102
|
+
cursor: 'pointer',
|
|
103
|
+
fontSize: '18px',
|
|
104
|
+
color: theme.colors.textSecondary,
|
|
105
|
+
padding: '0',
|
|
106
|
+
width: '24px',
|
|
107
|
+
height: '24px',
|
|
108
|
+
display: 'flex',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
x
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Node List */}
|
|
118
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
119
|
+
{selectedNodes.map((node) => {
|
|
120
|
+
const typeDefinition = nodeTypeDefinitions[node.type];
|
|
121
|
+
const nodeColor =
|
|
122
|
+
(node.data?.color as string) || typeDefinition?.color || theme.colors.secondary;
|
|
123
|
+
const icon = (node.data?.icon as string) || typeDefinition?.icon;
|
|
124
|
+
const isExpanded = expandedNodes[node.id] || false;
|
|
125
|
+
const description = node.data?.description as string | undefined;
|
|
126
|
+
|
|
127
|
+
// Get displayable data entries
|
|
128
|
+
const dataEntries = node.data
|
|
129
|
+
? Object.entries(node.data).filter(([key]) => !internalFields.includes(key))
|
|
130
|
+
: [];
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
key={node.id}
|
|
135
|
+
style={{
|
|
136
|
+
backgroundColor: theme.colors.surface,
|
|
137
|
+
borderRadius: '6px',
|
|
138
|
+
border: `1px solid ${theme.colors.border}`,
|
|
139
|
+
overflow: 'hidden',
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{/* Node Header - Always visible */}
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => toggleExpanded(node.id)}
|
|
145
|
+
style={{
|
|
146
|
+
width: '100%',
|
|
147
|
+
padding: '10px 12px',
|
|
148
|
+
backgroundColor: 'transparent',
|
|
149
|
+
border: 'none',
|
|
150
|
+
cursor: 'pointer',
|
|
151
|
+
display: 'flex',
|
|
152
|
+
alignItems: 'flex-start',
|
|
153
|
+
gap: '10px',
|
|
154
|
+
textAlign: 'left',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{/* Icon */}
|
|
158
|
+
<div
|
|
159
|
+
style={{
|
|
160
|
+
width: '28px',
|
|
161
|
+
height: '28px',
|
|
162
|
+
borderRadius: '6px',
|
|
163
|
+
backgroundColor: nodeColor,
|
|
164
|
+
display: 'flex',
|
|
165
|
+
alignItems: 'center',
|
|
166
|
+
justifyContent: 'center',
|
|
167
|
+
flexShrink: 0,
|
|
168
|
+
color: 'white',
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{icon ? resolveIcon(icon, 16) : null}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Name and Description */}
|
|
175
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
176
|
+
<div
|
|
177
|
+
style={{
|
|
178
|
+
fontWeight: 600,
|
|
179
|
+
fontSize: '13px',
|
|
180
|
+
color: theme.colors.text,
|
|
181
|
+
marginBottom: description ? '2px' : 0,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{node.name || node.id}
|
|
185
|
+
</div>
|
|
186
|
+
{description && (
|
|
187
|
+
<div
|
|
188
|
+
style={{
|
|
189
|
+
fontSize: '11px',
|
|
190
|
+
color: theme.colors.textSecondary,
|
|
191
|
+
overflow: 'hidden',
|
|
192
|
+
textOverflow: 'ellipsis',
|
|
193
|
+
display: '-webkit-box',
|
|
194
|
+
WebkitLineClamp: isExpanded ? 'unset' : 2,
|
|
195
|
+
WebkitBoxOrient: 'vertical',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{description}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Expand/Collapse Indicator */}
|
|
204
|
+
<div
|
|
205
|
+
style={{
|
|
206
|
+
color: theme.colors.textSecondary,
|
|
207
|
+
fontSize: '10px',
|
|
208
|
+
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
209
|
+
transition: 'transform 0.2s',
|
|
210
|
+
flexShrink: 0,
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
▼
|
|
214
|
+
</div>
|
|
215
|
+
</button>
|
|
216
|
+
|
|
217
|
+
{/* Expanded Details */}
|
|
218
|
+
{isExpanded && (
|
|
219
|
+
<div
|
|
220
|
+
style={{
|
|
221
|
+
padding: '0 12px 10px 12px',
|
|
222
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
223
|
+
marginTop: '0',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{/* Node Type */}
|
|
227
|
+
<div style={{ marginTop: '10px' }}>
|
|
228
|
+
<div
|
|
229
|
+
style={{
|
|
230
|
+
fontSize: '10px',
|
|
231
|
+
color: theme.colors.textSecondary,
|
|
232
|
+
marginBottom: '4px',
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
Type
|
|
236
|
+
</div>
|
|
237
|
+
<div
|
|
238
|
+
style={{
|
|
239
|
+
fontSize: '11px',
|
|
240
|
+
padding: '3px 8px',
|
|
241
|
+
backgroundColor: nodeColor,
|
|
242
|
+
color: 'white',
|
|
243
|
+
borderRadius: '4px',
|
|
244
|
+
display: 'inline-block',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{node.type}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Node State */}
|
|
252
|
+
{node.state && (
|
|
253
|
+
<div style={{ marginTop: '8px' }}>
|
|
254
|
+
<div
|
|
255
|
+
style={{
|
|
256
|
+
fontSize: '10px',
|
|
257
|
+
color: theme.colors.textSecondary,
|
|
258
|
+
marginBottom: '4px',
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
State
|
|
262
|
+
</div>
|
|
263
|
+
<div
|
|
264
|
+
style={{
|
|
265
|
+
fontSize: '11px',
|
|
266
|
+
padding: '3px 8px',
|
|
267
|
+
backgroundColor:
|
|
268
|
+
typeDefinition?.states?.[node.state]?.color || theme.colors.secondary,
|
|
269
|
+
color: 'white',
|
|
270
|
+
borderRadius: '4px',
|
|
271
|
+
display: 'inline-block',
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
{typeDefinition?.states?.[node.state]?.label || node.state}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* Data Properties */}
|
|
280
|
+
{dataEntries.length > 0 && (
|
|
281
|
+
<div style={{ marginTop: '8px' }}>
|
|
282
|
+
<div
|
|
283
|
+
style={{
|
|
284
|
+
fontSize: '10px',
|
|
285
|
+
color: theme.colors.textSecondary,
|
|
286
|
+
marginBottom: '6px',
|
|
287
|
+
fontWeight: 'bold',
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
Properties
|
|
291
|
+
</div>
|
|
292
|
+
{dataEntries.map(([key, value]) => (
|
|
293
|
+
<div key={key} style={{ marginBottom: '6px' }}>
|
|
294
|
+
<div
|
|
295
|
+
style={{
|
|
296
|
+
fontSize: '10px',
|
|
297
|
+
color: theme.colors.textSecondary,
|
|
298
|
+
marginBottom: '2px',
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{key}
|
|
302
|
+
</div>
|
|
303
|
+
<div
|
|
304
|
+
style={{
|
|
305
|
+
fontSize: '11px',
|
|
306
|
+
color: theme.colors.text,
|
|
307
|
+
wordBreak: 'break-word',
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
{value !== undefined && value !== null
|
|
311
|
+
? typeof value === 'object'
|
|
312
|
+
? JSON.stringify(value, null, 2)
|
|
313
|
+
: String(value)
|
|
314
|
+
: '-'}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Node ID */}
|
|
322
|
+
<div
|
|
323
|
+
style={{
|
|
324
|
+
fontSize: '10px',
|
|
325
|
+
color: theme.colors.textMuted,
|
|
326
|
+
marginTop: '8px',
|
|
327
|
+
paddingTop: '6px',
|
|
328
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
ID: {node.id}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
};
|
package/src/nodes/CustomNode.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Handle, Position } from '@xyflow/react';
|
|
2
|
+
import { Handle, Position, NodeResizer } from '@xyflow/react';
|
|
3
3
|
import type { NodeProps } from '@xyflow/react';
|
|
4
4
|
import type { NodeTypeDefinition } from '@principal-ai/principal-view-core';
|
|
5
5
|
import { resolveIcon } from '../utils/iconResolver';
|
|
@@ -100,6 +100,9 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
100
100
|
border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
|
|
101
101
|
fontSize: '12px',
|
|
102
102
|
fontWeight: 500,
|
|
103
|
+
// Use 100% width/height to fill the node container (for resizing support)
|
|
104
|
+
width: '100%',
|
|
105
|
+
height: '100%',
|
|
103
106
|
minWidth: typeDefinition.size?.width || 80,
|
|
104
107
|
minHeight: typeDefinition.size?.height || 40,
|
|
105
108
|
display: 'flex',
|
|
@@ -108,8 +111,9 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
108
111
|
justifyContent: isGroup ? 'flex-start' : 'center',
|
|
109
112
|
gap: '4px',
|
|
110
113
|
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
111
|
-
transition: '
|
|
114
|
+
transition: 'box-shadow 0.2s ease',
|
|
112
115
|
animationDuration: animationType ? `${animationDuration}ms` : undefined,
|
|
116
|
+
boxSizing: 'border-box' as const,
|
|
113
117
|
};
|
|
114
118
|
|
|
115
119
|
switch (typeDefinition.shape) {
|
|
@@ -117,8 +121,6 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
117
121
|
return {
|
|
118
122
|
...baseStyles,
|
|
119
123
|
borderRadius: '50%',
|
|
120
|
-
width: typeDefinition.size?.width || 80,
|
|
121
|
-
height: typeDefinition.size?.height || 80,
|
|
122
124
|
padding: '8px',
|
|
123
125
|
};
|
|
124
126
|
case 'hexagon':
|
|
@@ -136,14 +138,18 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
136
138
|
boxShadow: 'none', // Shadow handled by wrapper
|
|
137
139
|
};
|
|
138
140
|
case 'diamond':
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
+
// Diamond uses wrapper approach for proper border - styles returned here are for inner fill
|
|
142
|
+
// The outer border wrapper is rendered separately in the JSX
|
|
141
143
|
return {
|
|
142
144
|
...baseStyles,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
border: 'none', // Border handled by wrapper
|
|
146
|
+
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
|
147
|
+
width: '100%',
|
|
148
|
+
height: '100%',
|
|
149
|
+
minWidth: 'unset',
|
|
150
|
+
minHeight: 'unset',
|
|
151
|
+
padding: '8px 16px',
|
|
152
|
+
boxShadow: 'none', // Shadow handled by wrapper
|
|
147
153
|
};
|
|
148
154
|
case 'rectangle':
|
|
149
155
|
default:
|
|
@@ -156,6 +162,14 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
156
162
|
|
|
157
163
|
const isDiamond = typeDefinition.shape === 'diamond';
|
|
158
164
|
const isHexagon = typeDefinition.shape === 'hexagon';
|
|
165
|
+
const isCircle = typeDefinition.shape === 'circle';
|
|
166
|
+
|
|
167
|
+
// Determine if aspect ratio should be locked (circles should maintain square aspect)
|
|
168
|
+
const keepAspectRatio = isCircle;
|
|
169
|
+
|
|
170
|
+
// Minimum dimensions for resizing
|
|
171
|
+
const minWidth = typeDefinition.size?.width || 80;
|
|
172
|
+
const minHeight = typeDefinition.size?.height || (isCircle ? minWidth : 40);
|
|
159
173
|
|
|
160
174
|
// Hexagon border wrapper styles (outer shape that acts as border)
|
|
161
175
|
// Hexagon with gentle diagonals
|
|
@@ -166,10 +180,14 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
166
180
|
position: 'relative',
|
|
167
181
|
clipPath: hexagonClipPath,
|
|
168
182
|
backgroundColor: hasViolations ? '#D0021B' : strokeColor,
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
// Use 100% to fill container for resizing support
|
|
184
|
+
width: '100%',
|
|
185
|
+
height: '100%',
|
|
186
|
+
minWidth: typeDefinition.size?.width || 120,
|
|
187
|
+
minHeight: typeDefinition.size?.height || 120,
|
|
171
188
|
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
172
|
-
transition: '
|
|
189
|
+
transition: 'box-shadow 0.2s ease',
|
|
190
|
+
boxSizing: 'border-box',
|
|
173
191
|
}
|
|
174
192
|
: {};
|
|
175
193
|
|
|
@@ -194,6 +212,47 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
194
212
|
}
|
|
195
213
|
: {};
|
|
196
214
|
|
|
215
|
+
// Diamond clip-path
|
|
216
|
+
const diamondClipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
|
217
|
+
const diamondBorderWidth = 2;
|
|
218
|
+
|
|
219
|
+
// Diamond border wrapper styles (outer shape that acts as border)
|
|
220
|
+
const diamondBorderStyle: React.CSSProperties = isDiamond
|
|
221
|
+
? {
|
|
222
|
+
position: 'relative',
|
|
223
|
+
clipPath: diamondClipPath,
|
|
224
|
+
backgroundColor: hasViolations ? '#D0021B' : strokeColor,
|
|
225
|
+
width: '100%',
|
|
226
|
+
height: '100%',
|
|
227
|
+
minWidth: typeDefinition.size?.width || 80,
|
|
228
|
+
minHeight: typeDefinition.size?.height || 80,
|
|
229
|
+
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
230
|
+
transition: 'box-shadow 0.2s ease',
|
|
231
|
+
boxSizing: 'border-box',
|
|
232
|
+
}
|
|
233
|
+
: {};
|
|
234
|
+
|
|
235
|
+
// Diamond inner fill styles (white background inset from border)
|
|
236
|
+
const diamondInnerStyle: React.CSSProperties = isDiamond
|
|
237
|
+
? {
|
|
238
|
+
position: 'absolute',
|
|
239
|
+
top: diamondBorderWidth,
|
|
240
|
+
left: diamondBorderWidth,
|
|
241
|
+
right: diamondBorderWidth,
|
|
242
|
+
bottom: diamondBorderWidth,
|
|
243
|
+
clipPath: diamondClipPath,
|
|
244
|
+
backgroundColor: 'white',
|
|
245
|
+
color: '#000',
|
|
246
|
+
display: 'flex',
|
|
247
|
+
flexDirection: 'column',
|
|
248
|
+
alignItems: 'center',
|
|
249
|
+
justifyContent: 'center',
|
|
250
|
+
fontSize: '12px',
|
|
251
|
+
fontWeight: 500,
|
|
252
|
+
gap: '4px',
|
|
253
|
+
}
|
|
254
|
+
: {};
|
|
255
|
+
|
|
197
256
|
// Handle styles - larger and more visible in edit mode
|
|
198
257
|
const baseHandleStyle = editable
|
|
199
258
|
? {
|
|
@@ -209,34 +268,13 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
209
268
|
height: 8,
|
|
210
269
|
};
|
|
211
270
|
|
|
212
|
-
|
|
213
|
-
// A 45° rotated square has tips at ~41% beyond the original edges
|
|
214
|
-
const diamondOffset = isDiamond ? '21%' : '0';
|
|
215
|
-
|
|
216
|
-
const getHandleStyle = (position: 'top' | 'bottom' | 'left' | 'right') => {
|
|
271
|
+
const getHandleStyle = (_position: 'top' | 'bottom' | 'left' | 'right') => {
|
|
217
272
|
if (!isDiamond && !isHexagon) return baseHandleStyle;
|
|
218
273
|
|
|
219
274
|
const offsetStyle: React.CSSProperties = { ...baseHandleStyle };
|
|
220
275
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case 'top':
|
|
224
|
-
offsetStyle.top = `-${diamondOffset}`;
|
|
225
|
-
break;
|
|
226
|
-
case 'bottom':
|
|
227
|
-
offsetStyle.bottom = `-${diamondOffset}`;
|
|
228
|
-
break;
|
|
229
|
-
case 'left':
|
|
230
|
-
offsetStyle.left = `-${diamondOffset}`;
|
|
231
|
-
break;
|
|
232
|
-
case 'right':
|
|
233
|
-
offsetStyle.right = `-${diamondOffset}`;
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (isHexagon) {
|
|
239
|
-
// Bring handles above the hexagon layers
|
|
276
|
+
// Bring handles above the clip-path layers for hexagon and diamond
|
|
277
|
+
if (isHexagon || isDiamond) {
|
|
240
278
|
offsetStyle.zIndex = 10;
|
|
241
279
|
}
|
|
242
280
|
|
|
@@ -245,13 +283,32 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
245
283
|
|
|
246
284
|
return (
|
|
247
285
|
<>
|
|
286
|
+
{/* Node Resizer - only shown in edit mode */}
|
|
287
|
+
{editable && (
|
|
288
|
+
<NodeResizer
|
|
289
|
+
color={strokeColor}
|
|
290
|
+
isVisible={selected}
|
|
291
|
+
minWidth={minWidth}
|
|
292
|
+
minHeight={minHeight}
|
|
293
|
+
keepAspectRatio={keepAspectRatio}
|
|
294
|
+
handleStyle={{
|
|
295
|
+
width: 8,
|
|
296
|
+
height: 8,
|
|
297
|
+
borderRadius: 2,
|
|
298
|
+
}}
|
|
299
|
+
lineStyle={{
|
|
300
|
+
borderWidth: 1,
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
|
|
248
305
|
{/* Input handles - all 4 sides for incoming connections */}
|
|
249
306
|
<Handle type="target" position={Position.Top} id="top" style={getHandleStyle('top')} />
|
|
250
307
|
<Handle type="target" position={Position.Bottom} id="bottom" style={getHandleStyle('bottom')} />
|
|
251
308
|
<Handle type="target" position={Position.Left} id="left" style={getHandleStyle('left')} />
|
|
252
309
|
<Handle type="target" position={Position.Right} id="right" style={getHandleStyle('right')} />
|
|
253
310
|
|
|
254
|
-
{/* Hexagon
|
|
311
|
+
{/* Hexagon and Diamond need a wrapper for proper border rendering */}
|
|
255
312
|
{isHexagon ? (
|
|
256
313
|
<div style={hexagonBorderStyle} className={animationClass}>
|
|
257
314
|
<div style={hexagonInnerStyle}>
|
|
@@ -288,12 +345,47 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
288
345
|
)}
|
|
289
346
|
</div>
|
|
290
347
|
</div>
|
|
348
|
+
) : isDiamond ? (
|
|
349
|
+
<div style={diamondBorderStyle} className={animationClass}>
|
|
350
|
+
<div style={diamondInnerStyle}>
|
|
351
|
+
{icon && (
|
|
352
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
353
|
+
{resolveIcon(icon, 20)}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
<div style={{ textAlign: 'center', wordBreak: 'break-word' }}>{displayName}</div>
|
|
357
|
+
{state && (
|
|
358
|
+
<div
|
|
359
|
+
style={{
|
|
360
|
+
fontSize: '10px',
|
|
361
|
+
backgroundColor: color,
|
|
362
|
+
color: 'white',
|
|
363
|
+
padding: '2px 6px',
|
|
364
|
+
borderRadius: '4px',
|
|
365
|
+
textAlign: 'center',
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
{hasViolations && (
|
|
372
|
+
<div
|
|
373
|
+
style={{
|
|
374
|
+
fontSize: '10px',
|
|
375
|
+
color: '#D0021B',
|
|
376
|
+
fontWeight: 'bold',
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
⚠️
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
291
384
|
) : (
|
|
292
385
|
<div style={getShapeStyles()} className={animationClass}>
|
|
293
|
-
{/* Inner content
|
|
386
|
+
{/* Inner content */}
|
|
294
387
|
<div
|
|
295
388
|
style={{
|
|
296
|
-
...(isDiamond ? { transform: 'rotate(-45deg)' } : {}),
|
|
297
389
|
...(isGroup ? { width: '100%' } : {}),
|
|
298
390
|
}}
|
|
299
391
|
>
|
|
@@ -437,22 +529,6 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
437
529
|
}
|
|
438
530
|
}
|
|
439
531
|
|
|
440
|
-
/* Special handling for diamond shape with shake */
|
|
441
|
-
.node-shake[style*="rotate(45deg)"] {
|
|
442
|
-
animation: node-shake-diamond ease-in-out;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
@keyframes node-shake-diamond {
|
|
446
|
-
0%, 100% {
|
|
447
|
-
transform: rotate(45deg) translateX(0);
|
|
448
|
-
}
|
|
449
|
-
10%, 30%, 50%, 70%, 90% {
|
|
450
|
-
transform: rotate(45deg) translateX(-4px);
|
|
451
|
-
}
|
|
452
|
-
20%, 40%, 60%, 80% {
|
|
453
|
-
transform: rotate(45deg) translateX(4px);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
532
|
`}</style>
|
|
457
533
|
</>
|
|
458
534
|
);
|