@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.
Files changed (96) hide show
  1. package/README.md +111 -0
  2. package/dist/components/ConfigurationSelector.d.ts +37 -0
  3. package/dist/components/ConfigurationSelector.d.ts.map +1 -0
  4. package/dist/components/ConfigurationSelector.js +67 -0
  5. package/dist/components/ConfigurationSelector.js.map +1 -0
  6. package/dist/components/EdgeInfoPanel.d.ts +16 -0
  7. package/dist/components/EdgeInfoPanel.d.ts.map +1 -0
  8. package/dist/components/EdgeInfoPanel.js +85 -0
  9. package/dist/components/EdgeInfoPanel.js.map +1 -0
  10. package/dist/components/EventLog.d.ts +20 -0
  11. package/dist/components/EventLog.d.ts.map +1 -0
  12. package/dist/components/EventLog.js +13 -0
  13. package/dist/components/EventLog.js.map +1 -0
  14. package/dist/components/EventLog.test.d.ts +2 -0
  15. package/dist/components/EventLog.test.d.ts.map +1 -0
  16. package/dist/components/EventLog.test.js +73 -0
  17. package/dist/components/EventLog.test.js.map +1 -0
  18. package/dist/components/GraphRenderer.d.ts +121 -0
  19. package/dist/components/GraphRenderer.d.ts.map +1 -0
  20. package/dist/components/GraphRenderer.js +809 -0
  21. package/dist/components/GraphRenderer.js.map +1 -0
  22. package/dist/components/GraphRenderer.test.d.ts +2 -0
  23. package/dist/components/GraphRenderer.test.d.ts.map +1 -0
  24. package/dist/components/GraphRenderer.test.js +88 -0
  25. package/dist/components/GraphRenderer.test.js.map +1 -0
  26. package/dist/components/MetricsDashboard.d.ts +14 -0
  27. package/dist/components/MetricsDashboard.d.ts.map +1 -0
  28. package/dist/components/MetricsDashboard.js +13 -0
  29. package/dist/components/MetricsDashboard.js.map +1 -0
  30. package/dist/components/NodeInfoPanel.d.ts +21 -0
  31. package/dist/components/NodeInfoPanel.d.ts.map +1 -0
  32. package/dist/components/NodeInfoPanel.js +217 -0
  33. package/dist/components/NodeInfoPanel.js.map +1 -0
  34. package/dist/edges/CustomEdge.d.ts +16 -0
  35. package/dist/edges/CustomEdge.d.ts.map +1 -0
  36. package/dist/edges/CustomEdge.js +200 -0
  37. package/dist/edges/CustomEdge.js.map +1 -0
  38. package/dist/edges/GenericEdge.d.ts +18 -0
  39. package/dist/edges/GenericEdge.d.ts.map +1 -0
  40. package/dist/edges/GenericEdge.js +14 -0
  41. package/dist/edges/GenericEdge.js.map +1 -0
  42. package/dist/hooks/usePathBasedEvents.d.ts +42 -0
  43. package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
  44. package/dist/hooks/usePathBasedEvents.js +122 -0
  45. package/dist/hooks/usePathBasedEvents.js.map +1 -0
  46. package/dist/index.d.ts +33 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +41 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/nodes/CustomNode.d.ts +18 -0
  51. package/dist/nodes/CustomNode.d.ts.map +1 -0
  52. package/dist/nodes/CustomNode.js +298 -0
  53. package/dist/nodes/CustomNode.js.map +1 -0
  54. package/dist/nodes/GenericNode.d.ts +20 -0
  55. package/dist/nodes/GenericNode.d.ts.map +1 -0
  56. package/dist/nodes/GenericNode.js +24 -0
  57. package/dist/nodes/GenericNode.js.map +1 -0
  58. package/dist/utils/animationMapping.d.ts +53 -0
  59. package/dist/utils/animationMapping.d.ts.map +1 -0
  60. package/dist/utils/animationMapping.js +133 -0
  61. package/dist/utils/animationMapping.js.map +1 -0
  62. package/dist/utils/graphConverter.d.ts +22 -0
  63. package/dist/utils/graphConverter.d.ts.map +1 -0
  64. package/dist/utils/graphConverter.js +176 -0
  65. package/dist/utils/graphConverter.js.map +1 -0
  66. package/dist/utils/iconResolver.d.ts +29 -0
  67. package/dist/utils/iconResolver.d.ts.map +1 -0
  68. package/dist/utils/iconResolver.js +68 -0
  69. package/dist/utils/iconResolver.js.map +1 -0
  70. package/package.json +61 -0
  71. package/src/components/ConfigurationSelector.tsx +147 -0
  72. package/src/components/EdgeInfoPanel.tsx +198 -0
  73. package/src/components/EventLog.test.tsx +85 -0
  74. package/src/components/EventLog.tsx +51 -0
  75. package/src/components/GraphRenderer.test.tsx +118 -0
  76. package/src/components/GraphRenderer.tsx +1222 -0
  77. package/src/components/MetricsDashboard.tsx +40 -0
  78. package/src/components/NodeInfoPanel.tsx +425 -0
  79. package/src/edges/CustomEdge.tsx +344 -0
  80. package/src/edges/GenericEdge.tsx +40 -0
  81. package/src/hooks/usePathBasedEvents.ts +182 -0
  82. package/src/index.ts +67 -0
  83. package/src/nodes/CustomNode.tsx +432 -0
  84. package/src/nodes/GenericNode.tsx +54 -0
  85. package/src/stories/AnimationWorkshop.stories.tsx +608 -0
  86. package/src/stories/EventDrivenAnimations.stories.tsx +499 -0
  87. package/src/stories/EventLog.stories.tsx +161 -0
  88. package/src/stories/GraphRenderer.stories.tsx +628 -0
  89. package/src/stories/Introduction.mdx +51 -0
  90. package/src/stories/MetricsDashboard.stories.tsx +227 -0
  91. package/src/stories/MultiConfig.stories.tsx +531 -0
  92. package/src/stories/MultiDirectionalConnections.stories.tsx +345 -0
  93. package/src/stories/NodeShapes.stories.tsx +769 -0
  94. package/src/utils/animationMapping.ts +170 -0
  95. package/src/utils/graphConverter.ts +218 -0
  96. 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
+ };