@principal-ai/principal-view-react 0.6.15 → 0.6.17
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 +28 -11
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +168 -24
- package/src/components/PendingChanges.test.tsx +433 -0
- package/src/components/SelectionSidebar.tsx +341 -0
- package/src/nodes/CustomNode.tsx +43 -11
|
@@ -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,13 +138,12 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
136
138
|
boxShadow: 'none', // Shadow handled by wrapper
|
|
137
139
|
};
|
|
138
140
|
case 'diamond':
|
|
139
|
-
// Rotated square -
|
|
140
|
-
const diamondSize = typeDefinition.size?.width || 70;
|
|
141
|
+
// Rotated square - use 100% to fill container
|
|
141
142
|
return {
|
|
142
143
|
...baseStyles,
|
|
143
144
|
transform: 'rotate(45deg)',
|
|
144
|
-
width:
|
|
145
|
-
height:
|
|
145
|
+
width: '100%',
|
|
146
|
+
height: '100%',
|
|
146
147
|
padding: '8px',
|
|
147
148
|
};
|
|
148
149
|
case 'rectangle':
|
|
@@ -156,6 +157,14 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
156
157
|
|
|
157
158
|
const isDiamond = typeDefinition.shape === 'diamond';
|
|
158
159
|
const isHexagon = typeDefinition.shape === 'hexagon';
|
|
160
|
+
const isCircle = typeDefinition.shape === 'circle';
|
|
161
|
+
|
|
162
|
+
// Determine if aspect ratio should be locked (circles and diamonds should maintain square aspect)
|
|
163
|
+
const keepAspectRatio = isCircle || isDiamond;
|
|
164
|
+
|
|
165
|
+
// Minimum dimensions for resizing
|
|
166
|
+
const minWidth = typeDefinition.size?.width || 80;
|
|
167
|
+
const minHeight = typeDefinition.size?.height || (isCircle ? minWidth : 40);
|
|
159
168
|
|
|
160
169
|
// Hexagon border wrapper styles (outer shape that acts as border)
|
|
161
170
|
// Hexagon with gentle diagonals
|
|
@@ -166,10 +175,14 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
166
175
|
position: 'relative',
|
|
167
176
|
clipPath: hexagonClipPath,
|
|
168
177
|
backgroundColor: hasViolations ? '#D0021B' : strokeColor,
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
// Use 100% to fill container for resizing support
|
|
179
|
+
width: '100%',
|
|
180
|
+
height: '100%',
|
|
181
|
+
minWidth: typeDefinition.size?.width || 120,
|
|
182
|
+
minHeight: typeDefinition.size?.height || 120,
|
|
171
183
|
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
172
|
-
transition: '
|
|
184
|
+
transition: 'box-shadow 0.2s ease',
|
|
185
|
+
boxSizing: 'border-box',
|
|
173
186
|
}
|
|
174
187
|
: {};
|
|
175
188
|
|
|
@@ -245,6 +258,25 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
|
245
258
|
|
|
246
259
|
return (
|
|
247
260
|
<>
|
|
261
|
+
{/* Node Resizer - only shown in edit mode */}
|
|
262
|
+
{editable && (
|
|
263
|
+
<NodeResizer
|
|
264
|
+
color={strokeColor}
|
|
265
|
+
isVisible={selected}
|
|
266
|
+
minWidth={minWidth}
|
|
267
|
+
minHeight={minHeight}
|
|
268
|
+
keepAspectRatio={keepAspectRatio}
|
|
269
|
+
handleStyle={{
|
|
270
|
+
width: 8,
|
|
271
|
+
height: 8,
|
|
272
|
+
borderRadius: 2,
|
|
273
|
+
}}
|
|
274
|
+
lineStyle={{
|
|
275
|
+
borderWidth: 1,
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
|
|
248
280
|
{/* Input handles - all 4 sides for incoming connections */}
|
|
249
281
|
<Handle type="target" position={Position.Top} id="top" style={getHandleStyle('top')} />
|
|
250
282
|
<Handle type="target" position={Position.Bottom} id="bottom" style={getHandleStyle('bottom')} />
|