@principal-ai/principal-view-react 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/components/ConfigurationSelector.d.ts +37 -0
- package/dist/components/ConfigurationSelector.d.ts.map +1 -0
- package/dist/components/ConfigurationSelector.js +67 -0
- package/dist/components/ConfigurationSelector.js.map +1 -0
- package/dist/components/EdgeInfoPanel.d.ts +16 -0
- package/dist/components/EdgeInfoPanel.d.ts.map +1 -0
- package/dist/components/EdgeInfoPanel.js +85 -0
- package/dist/components/EdgeInfoPanel.js.map +1 -0
- package/dist/components/EventLog.d.ts +20 -0
- package/dist/components/EventLog.d.ts.map +1 -0
- package/dist/components/EventLog.js +13 -0
- package/dist/components/EventLog.js.map +1 -0
- package/dist/components/EventLog.test.d.ts +2 -0
- package/dist/components/EventLog.test.d.ts.map +1 -0
- package/dist/components/EventLog.test.js +73 -0
- package/dist/components/EventLog.test.js.map +1 -0
- package/dist/components/GraphRenderer.d.ts +121 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -0
- package/dist/components/GraphRenderer.js +809 -0
- package/dist/components/GraphRenderer.js.map +1 -0
- package/dist/components/GraphRenderer.test.d.ts +2 -0
- package/dist/components/GraphRenderer.test.d.ts.map +1 -0
- package/dist/components/GraphRenderer.test.js +88 -0
- package/dist/components/GraphRenderer.test.js.map +1 -0
- package/dist/components/MetricsDashboard.d.ts +14 -0
- package/dist/components/MetricsDashboard.d.ts.map +1 -0
- package/dist/components/MetricsDashboard.js +13 -0
- package/dist/components/MetricsDashboard.js.map +1 -0
- package/dist/components/NodeInfoPanel.d.ts +21 -0
- package/dist/components/NodeInfoPanel.d.ts.map +1 -0
- package/dist/components/NodeInfoPanel.js +217 -0
- package/dist/components/NodeInfoPanel.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +16 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -0
- package/dist/edges/CustomEdge.js +200 -0
- package/dist/edges/CustomEdge.js.map +1 -0
- package/dist/edges/GenericEdge.d.ts +18 -0
- package/dist/edges/GenericEdge.d.ts.map +1 -0
- package/dist/edges/GenericEdge.js +14 -0
- package/dist/edges/GenericEdge.js.map +1 -0
- package/dist/hooks/usePathBasedEvents.d.ts +42 -0
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
- package/dist/hooks/usePathBasedEvents.js +122 -0
- package/dist/hooks/usePathBasedEvents.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts +18 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -0
- package/dist/nodes/CustomNode.js +298 -0
- package/dist/nodes/CustomNode.js.map +1 -0
- package/dist/nodes/GenericNode.d.ts +20 -0
- package/dist/nodes/GenericNode.d.ts.map +1 -0
- package/dist/nodes/GenericNode.js +24 -0
- package/dist/nodes/GenericNode.js.map +1 -0
- package/dist/utils/animationMapping.d.ts +53 -0
- package/dist/utils/animationMapping.d.ts.map +1 -0
- package/dist/utils/animationMapping.js +133 -0
- package/dist/utils/animationMapping.js.map +1 -0
- package/dist/utils/graphConverter.d.ts +22 -0
- package/dist/utils/graphConverter.d.ts.map +1 -0
- package/dist/utils/graphConverter.js +176 -0
- package/dist/utils/graphConverter.js.map +1 -0
- package/dist/utils/iconResolver.d.ts +29 -0
- package/dist/utils/iconResolver.d.ts.map +1 -0
- package/dist/utils/iconResolver.js +68 -0
- package/dist/utils/iconResolver.js.map +1 -0
- package/package.json +61 -0
- package/src/components/ConfigurationSelector.tsx +147 -0
- package/src/components/EdgeInfoPanel.tsx +198 -0
- package/src/components/EventLog.test.tsx +85 -0
- package/src/components/EventLog.tsx +51 -0
- package/src/components/GraphRenderer.test.tsx +118 -0
- package/src/components/GraphRenderer.tsx +1222 -0
- package/src/components/MetricsDashboard.tsx +40 -0
- package/src/components/NodeInfoPanel.tsx +425 -0
- package/src/edges/CustomEdge.tsx +344 -0
- package/src/edges/GenericEdge.tsx +40 -0
- package/src/hooks/usePathBasedEvents.ts +182 -0
- package/src/index.ts +67 -0
- package/src/nodes/CustomNode.tsx +432 -0
- package/src/nodes/GenericNode.tsx +54 -0
- package/src/stories/AnimationWorkshop.stories.tsx +608 -0
- package/src/stories/EventDrivenAnimations.stories.tsx +499 -0
- package/src/stories/EventLog.stories.tsx +161 -0
- package/src/stories/GraphRenderer.stories.tsx +628 -0
- package/src/stories/Introduction.mdx +51 -0
- package/src/stories/MetricsDashboard.stories.tsx +227 -0
- package/src/stories/MultiConfig.stories.tsx +531 -0
- package/src/stories/MultiDirectionalConnections.stories.tsx +345 -0
- package/src/stories/NodeShapes.stories.tsx +769 -0
- package/src/utils/animationMapping.ts +170 -0
- package/src/utils/graphConverter.ts +218 -0
- package/src/utils/iconResolver.tsx +49 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Handle, Position } from '@xyflow/react';
|
|
3
|
+
import type { NodeProps } from '@xyflow/react';
|
|
4
|
+
import type { NodeTypeDefinition } from '@principal-ai/principal-view-core';
|
|
5
|
+
import { resolveIcon } from '../utils/iconResolver';
|
|
6
|
+
|
|
7
|
+
export interface CustomNodeData extends Record<string, unknown> {
|
|
8
|
+
label: string;
|
|
9
|
+
typeDefinition: NodeTypeDefinition;
|
|
10
|
+
state?: string;
|
|
11
|
+
hasViolations?: boolean;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
// Animation control
|
|
14
|
+
animationType?: 'pulse' | 'flash' | 'shake' | 'entry' | null;
|
|
15
|
+
animationDuration?: number;
|
|
16
|
+
// Edit mode - shows larger connection handles
|
|
17
|
+
editable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom node component for xyflow that renders based on NodeTypeDefinition
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected }) => {
|
|
25
|
+
const nodeProps = data as CustomNodeData;
|
|
26
|
+
const {
|
|
27
|
+
typeDefinition,
|
|
28
|
+
state,
|
|
29
|
+
hasViolations,
|
|
30
|
+
data: nodeData,
|
|
31
|
+
animationType,
|
|
32
|
+
animationDuration = 1000,
|
|
33
|
+
editable = false,
|
|
34
|
+
} = nodeProps;
|
|
35
|
+
|
|
36
|
+
// Guard against missing typeDefinition
|
|
37
|
+
if (!typeDefinition) {
|
|
38
|
+
return (
|
|
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>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get fill color based on state or default
|
|
48
|
+
// Priority: state color > node data color > type definition color > default
|
|
49
|
+
const nodeDataColor = nodeData.color as string | undefined;
|
|
50
|
+
const baseColor = nodeDataColor || typeDefinition.color || '#888';
|
|
51
|
+
const stateColor = state && typeDefinition.states?.[state]?.color;
|
|
52
|
+
const fillColor = stateColor || baseColor;
|
|
53
|
+
|
|
54
|
+
// Get stroke color - priority: node data stroke > type definition stroke > fill color
|
|
55
|
+
const nodeDataStroke = nodeData.stroke as string | undefined;
|
|
56
|
+
const strokeColor = nodeDataStroke || typeDefinition.stroke || fillColor;
|
|
57
|
+
|
|
58
|
+
// Use fillColor as the primary "color" for backwards compatibility
|
|
59
|
+
const color = fillColor;
|
|
60
|
+
|
|
61
|
+
// Get label from data schema
|
|
62
|
+
const labelField = Object.entries(typeDefinition.dataSchema).find(
|
|
63
|
+
([, schema]) => schema.displayInLabel
|
|
64
|
+
)?.[0];
|
|
65
|
+
const displayLabel = labelField && nodeData[labelField] ? String(nodeData[labelField]) : nodeProps.label;
|
|
66
|
+
|
|
67
|
+
// Icon priority: node data override > state icon > type definition icon
|
|
68
|
+
const icon = (nodeData.icon as string)
|
|
69
|
+
|| (state && typeDefinition.states?.[state]?.icon)
|
|
70
|
+
|| typeDefinition.icon;
|
|
71
|
+
|
|
72
|
+
// Get animation class based on type
|
|
73
|
+
const getAnimationClass = () => {
|
|
74
|
+
switch (animationType) {
|
|
75
|
+
case 'pulse':
|
|
76
|
+
return 'node-pulse';
|
|
77
|
+
case 'flash':
|
|
78
|
+
return 'node-flash';
|
|
79
|
+
case 'shake':
|
|
80
|
+
return 'node-shake';
|
|
81
|
+
case 'entry':
|
|
82
|
+
return 'node-entry';
|
|
83
|
+
default:
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const animationClass = getAnimationClass();
|
|
89
|
+
|
|
90
|
+
// Shape-specific styles
|
|
91
|
+
const getShapeStyles = () => {
|
|
92
|
+
const baseStyles = {
|
|
93
|
+
padding: '12px 16px',
|
|
94
|
+
backgroundColor: 'white',
|
|
95
|
+
color: '#000',
|
|
96
|
+
border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
|
|
97
|
+
fontSize: '12px',
|
|
98
|
+
fontWeight: 500,
|
|
99
|
+
minWidth: typeDefinition.size?.width || 80,
|
|
100
|
+
minHeight: typeDefinition.size?.height || 40,
|
|
101
|
+
display: 'flex',
|
|
102
|
+
flexDirection: 'column' as const,
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
justifyContent: 'center',
|
|
105
|
+
gap: '4px',
|
|
106
|
+
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
107
|
+
transition: 'all 0.2s ease',
|
|
108
|
+
animationDuration: animationType ? `${animationDuration}ms` : undefined,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
switch (typeDefinition.shape) {
|
|
112
|
+
case 'circle':
|
|
113
|
+
return {
|
|
114
|
+
...baseStyles,
|
|
115
|
+
borderRadius: '50%',
|
|
116
|
+
width: typeDefinition.size?.width || 80,
|
|
117
|
+
height: typeDefinition.size?.height || 80,
|
|
118
|
+
padding: '8px',
|
|
119
|
+
};
|
|
120
|
+
case 'hexagon':
|
|
121
|
+
// Hexagon uses wrapper approach for proper border - styles returned here are for inner fill
|
|
122
|
+
// The outer border wrapper is rendered separately in the JSX
|
|
123
|
+
return {
|
|
124
|
+
...baseStyles,
|
|
125
|
+
border: 'none', // Border handled by wrapper
|
|
126
|
+
clipPath: 'polygon(20% 0%, 80% 0%, 100% 50%, 80% 100%, 20% 100%, 0% 50%)',
|
|
127
|
+
width: '100%',
|
|
128
|
+
height: '100%',
|
|
129
|
+
minWidth: 'unset',
|
|
130
|
+
minHeight: 'unset',
|
|
131
|
+
padding: '8px 20px',
|
|
132
|
+
boxShadow: 'none', // Shadow handled by wrapper
|
|
133
|
+
};
|
|
134
|
+
case 'diamond':
|
|
135
|
+
// Rotated square - fixed dimensions for proper diamond shape
|
|
136
|
+
const diamondSize = typeDefinition.size?.width || 70;
|
|
137
|
+
return {
|
|
138
|
+
...baseStyles,
|
|
139
|
+
transform: 'rotate(45deg)',
|
|
140
|
+
width: diamondSize,
|
|
141
|
+
height: diamondSize,
|
|
142
|
+
padding: '8px',
|
|
143
|
+
};
|
|
144
|
+
case 'rectangle':
|
|
145
|
+
default:
|
|
146
|
+
return {
|
|
147
|
+
...baseStyles,
|
|
148
|
+
borderRadius: '8px',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const isDiamond = typeDefinition.shape === 'diamond';
|
|
154
|
+
const isHexagon = typeDefinition.shape === 'hexagon';
|
|
155
|
+
|
|
156
|
+
// Hexagon border wrapper styles (outer shape that acts as border)
|
|
157
|
+
// Hexagon with gentle diagonals
|
|
158
|
+
const hexagonClipPath = 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)';
|
|
159
|
+
const hexagonBorderWidth = 2;
|
|
160
|
+
const hexagonBorderStyle: React.CSSProperties = isHexagon ? {
|
|
161
|
+
position: 'relative',
|
|
162
|
+
clipPath: hexagonClipPath,
|
|
163
|
+
backgroundColor: hasViolations ? '#D0021B' : strokeColor,
|
|
164
|
+
width: typeDefinition.size?.width || 120,
|
|
165
|
+
height: typeDefinition.size?.height || 120,
|
|
166
|
+
boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
|
|
167
|
+
transition: 'all 0.2s ease',
|
|
168
|
+
} : {};
|
|
169
|
+
|
|
170
|
+
// Hexagon inner fill styles (white background inset from border)
|
|
171
|
+
const hexagonInnerStyle: React.CSSProperties = isHexagon ? {
|
|
172
|
+
position: 'absolute',
|
|
173
|
+
top: hexagonBorderWidth,
|
|
174
|
+
left: hexagonBorderWidth,
|
|
175
|
+
right: hexagonBorderWidth,
|
|
176
|
+
bottom: hexagonBorderWidth,
|
|
177
|
+
clipPath: hexagonClipPath,
|
|
178
|
+
backgroundColor: 'white',
|
|
179
|
+
color: '#000',
|
|
180
|
+
display: 'flex',
|
|
181
|
+
flexDirection: 'column',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
justifyContent: 'center',
|
|
184
|
+
fontSize: '12px',
|
|
185
|
+
fontWeight: 500,
|
|
186
|
+
gap: '4px',
|
|
187
|
+
} : {};
|
|
188
|
+
|
|
189
|
+
// Handle styles - larger and more visible in edit mode
|
|
190
|
+
const baseHandleStyle = editable ? {
|
|
191
|
+
background: color,
|
|
192
|
+
width: 12,
|
|
193
|
+
height: 12,
|
|
194
|
+
border: '2px solid white',
|
|
195
|
+
boxShadow: '0 0 0 1px ' + color,
|
|
196
|
+
} : {
|
|
197
|
+
background: color,
|
|
198
|
+
width: 8,
|
|
199
|
+
height: 8,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Diamond handles need to be offset to reach the tips of the rotated shape
|
|
203
|
+
// A 45° rotated square has tips at ~41% beyond the original edges
|
|
204
|
+
const diamondOffset = isDiamond ? '21%' : '0';
|
|
205
|
+
|
|
206
|
+
const getHandleStyle = (position: 'top' | 'bottom' | 'left' | 'right') => {
|
|
207
|
+
if (!isDiamond && !isHexagon) return baseHandleStyle;
|
|
208
|
+
|
|
209
|
+
const offsetStyle: React.CSSProperties = { ...baseHandleStyle };
|
|
210
|
+
|
|
211
|
+
if (isDiamond) {
|
|
212
|
+
switch (position) {
|
|
213
|
+
case 'top':
|
|
214
|
+
offsetStyle.top = `-${diamondOffset}`;
|
|
215
|
+
break;
|
|
216
|
+
case 'bottom':
|
|
217
|
+
offsetStyle.bottom = `-${diamondOffset}`;
|
|
218
|
+
break;
|
|
219
|
+
case 'left':
|
|
220
|
+
offsetStyle.left = `-${diamondOffset}`;
|
|
221
|
+
break;
|
|
222
|
+
case 'right':
|
|
223
|
+
offsetStyle.right = `-${diamondOffset}`;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (isHexagon) {
|
|
229
|
+
// Bring handles above the hexagon layers
|
|
230
|
+
offsetStyle.zIndex = 10;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return offsetStyle;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
{/* Input handles - multiple connection points */}
|
|
239
|
+
<Handle
|
|
240
|
+
type="target"
|
|
241
|
+
position={Position.Top}
|
|
242
|
+
id="top"
|
|
243
|
+
style={getHandleStyle('top')}
|
|
244
|
+
/>
|
|
245
|
+
<Handle
|
|
246
|
+
type="target"
|
|
247
|
+
position={Position.Left}
|
|
248
|
+
id="left"
|
|
249
|
+
style={getHandleStyle('left')}
|
|
250
|
+
/>
|
|
251
|
+
<Handle
|
|
252
|
+
type="target"
|
|
253
|
+
position={Position.Right}
|
|
254
|
+
id="right"
|
|
255
|
+
style={getHandleStyle('right')}
|
|
256
|
+
/>
|
|
257
|
+
|
|
258
|
+
{/* Hexagon needs a wrapper for proper border rendering */}
|
|
259
|
+
{isHexagon ? (
|
|
260
|
+
<div style={hexagonBorderStyle} className={animationClass}>
|
|
261
|
+
<div style={hexagonInnerStyle}>
|
|
262
|
+
{icon && <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{resolveIcon(icon, 20)}</div>}
|
|
263
|
+
<div style={{ textAlign: 'center', wordBreak: 'break-word' }}>
|
|
264
|
+
{displayLabel}
|
|
265
|
+
</div>
|
|
266
|
+
{state && (
|
|
267
|
+
<div style={{
|
|
268
|
+
fontSize: '10px',
|
|
269
|
+
backgroundColor: color,
|
|
270
|
+
color: 'white',
|
|
271
|
+
padding: '2px 6px',
|
|
272
|
+
borderRadius: '4px',
|
|
273
|
+
textAlign: 'center',
|
|
274
|
+
}}>
|
|
275
|
+
{typeDefinition.states?.[state]?.label || state}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{hasViolations && (
|
|
279
|
+
<div style={{
|
|
280
|
+
fontSize: '10px',
|
|
281
|
+
color: '#D0021B',
|
|
282
|
+
fontWeight: 'bold',
|
|
283
|
+
}}>
|
|
284
|
+
⚠️
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<div style={getShapeStyles()} className={animationClass}>
|
|
291
|
+
{/* Inner content (rotated back if diamond) */}
|
|
292
|
+
<div style={isDiamond ? { transform: 'rotate(-45deg)' } : {}}>
|
|
293
|
+
{icon && <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{resolveIcon(icon, 20)}</div>}
|
|
294
|
+
<div style={{ textAlign: 'center', wordBreak: 'break-word' }}>
|
|
295
|
+
{displayLabel}
|
|
296
|
+
</div>
|
|
297
|
+
{state && (
|
|
298
|
+
<div style={{
|
|
299
|
+
fontSize: '10px',
|
|
300
|
+
backgroundColor: color,
|
|
301
|
+
color: 'white',
|
|
302
|
+
padding: '2px 6px',
|
|
303
|
+
borderRadius: '4px',
|
|
304
|
+
textAlign: 'center',
|
|
305
|
+
}}>
|
|
306
|
+
{typeDefinition.states?.[state]?.label || state}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
{hasViolations && (
|
|
310
|
+
<div style={{
|
|
311
|
+
fontSize: '10px',
|
|
312
|
+
color: '#D0021B',
|
|
313
|
+
fontWeight: 'bold',
|
|
314
|
+
}}>
|
|
315
|
+
⚠️
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Output handles - multiple connection points */}
|
|
323
|
+
<Handle
|
|
324
|
+
type="source"
|
|
325
|
+
position={Position.Bottom}
|
|
326
|
+
id="bottom"
|
|
327
|
+
style={getHandleStyle('bottom')}
|
|
328
|
+
/>
|
|
329
|
+
<Handle
|
|
330
|
+
type="source"
|
|
331
|
+
position={Position.Left}
|
|
332
|
+
id="left-out"
|
|
333
|
+
style={getHandleStyle('left')}
|
|
334
|
+
/>
|
|
335
|
+
<Handle
|
|
336
|
+
type="source"
|
|
337
|
+
position={Position.Right}
|
|
338
|
+
id="right-out"
|
|
339
|
+
style={getHandleStyle('right')}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{/* CSS animations for node animation types */}
|
|
343
|
+
<style>{`
|
|
344
|
+
/* Processing pulse - continuous breathing effect */
|
|
345
|
+
.node-pulse {
|
|
346
|
+
animation: node-pulse ease-in-out infinite;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@keyframes node-pulse {
|
|
350
|
+
0%, 100% {
|
|
351
|
+
transform: scale(1);
|
|
352
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
353
|
+
}
|
|
354
|
+
50% {
|
|
355
|
+
transform: scale(1.05);
|
|
356
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2), 0 0 0 4px rgba(59, 130, 246, 0.3);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Success flash - brief green glow */
|
|
361
|
+
.node-flash {
|
|
362
|
+
animation: node-flash ease-out forwards;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@keyframes node-flash {
|
|
366
|
+
0% {
|
|
367
|
+
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.8);
|
|
368
|
+
background-color: rgba(34, 197, 94, 0.1);
|
|
369
|
+
}
|
|
370
|
+
50% {
|
|
371
|
+
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
|
372
|
+
background-color: rgba(34, 197, 94, 0.2);
|
|
373
|
+
}
|
|
374
|
+
100% {
|
|
375
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
376
|
+
background-color: white;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Error shake - vibrate effect */
|
|
381
|
+
.node-shake {
|
|
382
|
+
animation: node-shake ease-in-out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@keyframes node-shake {
|
|
386
|
+
0%, 100% {
|
|
387
|
+
transform: translateX(0);
|
|
388
|
+
}
|
|
389
|
+
10%, 30%, 50%, 70%, 90% {
|
|
390
|
+
transform: translateX(-4px);
|
|
391
|
+
}
|
|
392
|
+
20%, 40%, 60%, 80% {
|
|
393
|
+
transform: translateX(4px);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Entry animation - scale up and fade in */
|
|
398
|
+
.node-entry {
|
|
399
|
+
animation: node-entry ease-out forwards;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@keyframes node-entry {
|
|
403
|
+
0% {
|
|
404
|
+
opacity: 0;
|
|
405
|
+
transform: scale(0.8);
|
|
406
|
+
}
|
|
407
|
+
100% {
|
|
408
|
+
opacity: 1;
|
|
409
|
+
transform: scale(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Special handling for diamond shape with shake */
|
|
414
|
+
.node-shake[style*="rotate(45deg)"] {
|
|
415
|
+
animation: node-shake-diamond ease-in-out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@keyframes node-shake-diamond {
|
|
419
|
+
0%, 100% {
|
|
420
|
+
transform: rotate(45deg) translateX(0);
|
|
421
|
+
}
|
|
422
|
+
10%, 30%, 50%, 70%, 90% {
|
|
423
|
+
transform: rotate(45deg) translateX(-4px);
|
|
424
|
+
}
|
|
425
|
+
20%, 40%, 60%, 80% {
|
|
426
|
+
transform: rotate(45deg) translateX(4px);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
`}</style>
|
|
430
|
+
</>
|
|
431
|
+
);
|
|
432
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NodeTypeDefinition } from '@principal-ai/principal-view-core';
|
|
3
|
+
|
|
4
|
+
export interface GenericNodeProps {
|
|
5
|
+
/** Node ID */
|
|
6
|
+
id: string;
|
|
7
|
+
|
|
8
|
+
/** Node type definition from configuration */
|
|
9
|
+
typeDefinition: NodeTypeDefinition;
|
|
10
|
+
|
|
11
|
+
/** Node data */
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
/** Current state (if applicable) */
|
|
15
|
+
state?: string;
|
|
16
|
+
|
|
17
|
+
/** Whether this node has violations */
|
|
18
|
+
hasViolations?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generic node renderer that adapts based on NodeTypeDefinition
|
|
23
|
+
* TODO: Implement different shapes, icons, and states
|
|
24
|
+
*/
|
|
25
|
+
export const GenericNode: React.FC<GenericNodeProps> = ({
|
|
26
|
+
id,
|
|
27
|
+
typeDefinition,
|
|
28
|
+
state,
|
|
29
|
+
hasViolations,
|
|
30
|
+
}) => {
|
|
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';
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
padding: '10px',
|
|
40
|
+
border: `2px solid ${hasViolations ? 'red' : color}`,
|
|
41
|
+
borderRadius: typeDefinition.shape === 'circle' ? '50%' : '4px',
|
|
42
|
+
backgroundColor: 'white',
|
|
43
|
+
minWidth: '60px',
|
|
44
|
+
textAlign: 'center',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<div style={{ fontSize: '12px', fontWeight: 'bold' }}>{id}</div>
|
|
48
|
+
{state && <div style={{ fontSize: '10px', color: '#666' }}>{state}</div>}
|
|
49
|
+
<div style={{ fontSize: '10px' }}>
|
|
50
|
+
<strong>TODO:</strong> Render shapes, icons
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|