@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,170 @@
1
+ /**
2
+ * Animation mapping utilities
3
+ *
4
+ * Maps log levels and component events to appropriate animations
5
+ * for Milestone 1 default visualization behavior
6
+ */
7
+
8
+ import type { LogLevel } from '@principal-ai/principal-view-core';
9
+
10
+ /**
11
+ * Node animation configuration
12
+ */
13
+ export interface NodeAnimation {
14
+ type: 'pulse' | 'flash' | 'shake' | 'entry';
15
+ duration: number;
16
+ intensity?: number; // 0-1 scale
17
+ color?: string;
18
+ }
19
+
20
+ /**
21
+ * Edge animation configuration
22
+ */
23
+ export interface EdgeAnimation {
24
+ type: 'flow' | 'particle' | 'pulse' | 'glow';
25
+ duration: number;
26
+ direction?: 'forward' | 'backward' | 'bidirectional';
27
+ color?: string;
28
+ }
29
+
30
+ /**
31
+ * Map log level to node animation
32
+ *
33
+ * Default behavior for Milestone 1:
34
+ * - debug: subtle pulse (low intensity)
35
+ * - info: standard pulse (medium intensity)
36
+ * - warn: amber pulse (high intensity)
37
+ * - error: red flash + shake
38
+ */
39
+ export function logLevelToNodeAnimation(level: LogLevel): NodeAnimation {
40
+ switch (level) {
41
+ case 'debug':
42
+ return {
43
+ type: 'pulse',
44
+ duration: 800,
45
+ intensity: 0.3,
46
+ color: '#94a3b8' // slate-400
47
+ };
48
+
49
+ case 'info':
50
+ return {
51
+ type: 'pulse',
52
+ duration: 1000,
53
+ intensity: 0.5,
54
+ color: '#3b82f6' // blue-500
55
+ };
56
+
57
+ case 'warn':
58
+ return {
59
+ type: 'pulse',
60
+ duration: 1200,
61
+ intensity: 1.0,
62
+ color: '#f59e0b' // amber-500
63
+ };
64
+
65
+ case 'error':
66
+ return {
67
+ type: 'flash', // More dramatic animation for errors
68
+ duration: 1500,
69
+ intensity: 1.0,
70
+ color: '#ef4444' // red-500
71
+ };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Map component action to node animation (Milestone 2)
77
+ */
78
+ export function actionToNodeAnimation(
79
+ _action: string,
80
+ state?: string
81
+ ): NodeAnimation {
82
+ // Default mapping - can be overridden by configuration
83
+ switch (state) {
84
+ case 'acquired':
85
+ case 'active':
86
+ case 'processing':
87
+ return {
88
+ type: 'pulse',
89
+ duration: 1000,
90
+ intensity: 0.8,
91
+ color: '#22c55e' // green-500
92
+ };
93
+
94
+ case 'waiting':
95
+ case 'pending':
96
+ return {
97
+ type: 'pulse',
98
+ duration: 1500,
99
+ intensity: 0.5,
100
+ color: '#eab308' // yellow-500
101
+ };
102
+
103
+ case 'error':
104
+ case 'failed':
105
+ return {
106
+ type: 'shake',
107
+ duration: 600,
108
+ intensity: 1.0,
109
+ color: '#ef4444' // red-500
110
+ };
111
+
112
+ case 'completed':
113
+ case 'success':
114
+ return {
115
+ type: 'flash',
116
+ duration: 800,
117
+ intensity: 0.7,
118
+ color: '#22c55e' // green-500
119
+ };
120
+
121
+ default:
122
+ // Generic action animation
123
+ return {
124
+ type: 'pulse',
125
+ duration: 1000,
126
+ intensity: 0.6,
127
+ color: '#3b82f6' // blue-500
128
+ };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get animation config for edge based on action (Milestone 2)
134
+ */
135
+ export function actionToEdgeAnimation(
136
+ _action: string,
137
+ edgeConfig?: EdgeAnimation
138
+ ): EdgeAnimation {
139
+ // Use edge config if provided, otherwise default
140
+ return edgeConfig || {
141
+ type: 'flow',
142
+ duration: 2000,
143
+ direction: 'forward'
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Check if log level should trigger animation
149
+ */
150
+ export function shouldAnimate(level: LogLevel, minLevel: LogLevel = 'info'): boolean {
151
+ const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
152
+ const minIndex = levels.indexOf(minLevel);
153
+ const currentIndex = levels.indexOf(level);
154
+
155
+ return currentIndex >= minIndex;
156
+ }
157
+
158
+ /**
159
+ * Calculate animation intensity based on frequency
160
+ * Used to adjust animation intensity when multiple logs occur rapidly
161
+ */
162
+ export function calculateIntensity(
163
+ baseIntensity: number,
164
+ eventCount: number,
165
+ _timeWindow: number = 1000
166
+ ): number {
167
+ // Reduce intensity if too many events in time window
168
+ const scaleFactor = Math.min(1.0, 5.0 / eventCount);
169
+ return Math.max(0.2, baseIntensity * scaleFactor);
170
+ }
@@ -0,0 +1,218 @@
1
+ import { MarkerType, type Node, type Edge } from '@xyflow/react';
2
+ import type { NodeState, EdgeState, GraphConfiguration, Violation } from '@principal-ai/principal-view-core';
3
+ import type { CustomNodeData } from '../nodes/CustomNode';
4
+ import type { CustomEdgeData } from '../edges/CustomEdge';
5
+
6
+ /**
7
+ * Convert our NodeState to xyflow Node format
8
+ */
9
+ export function convertToXYFlowNodes(
10
+ nodes: NodeState[],
11
+ configuration: GraphConfiguration,
12
+ violations: Violation[] = []
13
+ ): Node<CustomNodeData>[] {
14
+ return nodes.map((node) => {
15
+ const typeDefinition = configuration.nodeTypes[node.type];
16
+
17
+ // Warn if node type is not defined in configuration
18
+ if (!typeDefinition) {
19
+ console.warn(`Node type "${node.type}" not found in configuration for node "${node.id}"`);
20
+ }
21
+
22
+ const hasViolations = violations.some(v => v.context?.nodeId === node.id);
23
+
24
+ return {
25
+ id: node.id,
26
+ type: 'custom',
27
+ position: node.position || { x: 0, y: 0 },
28
+ data: {
29
+ label: node.id,
30
+ typeDefinition,
31
+ state: node.state,
32
+ hasViolations,
33
+ data: node.data,
34
+ },
35
+ };
36
+ });
37
+ }
38
+
39
+ /** Extended edge state with optional handle information for ReactFlow */
40
+ export interface EdgeStateWithHandles extends EdgeState {
41
+ sourceHandle?: string;
42
+ targetHandle?: string;
43
+ }
44
+
45
+ /**
46
+ * Convert our EdgeState to xyflow Edge format
47
+ */
48
+ export function convertToXYFlowEdges(
49
+ edges: (EdgeState | EdgeStateWithHandles)[],
50
+ configuration: GraphConfiguration,
51
+ violations: Violation[] = []
52
+ ): Edge<CustomEdgeData>[] {
53
+ return edges.map((edge) => {
54
+ const typeDefinition = configuration.edgeTypes[edge.type];
55
+
56
+ // Warn if edge type is not defined in configuration
57
+ if (!typeDefinition) {
58
+ console.warn(`Edge type "${edge.type}" not found in configuration for edge "${edge.id}"`);
59
+ }
60
+
61
+ const hasViolations = violations.some(v => v.context?.edgeId === edge.id);
62
+ const edgeWithHandles = edge as EdgeStateWithHandles;
63
+
64
+ // Add arrow marker if edge type is directed
65
+ const markerEnd = typeDefinition?.directed !== false ? {
66
+ type: MarkerType.ArrowClosed,
67
+ color: typeDefinition?.color || '#888',
68
+ width: 20,
69
+ height: 20,
70
+ } : undefined;
71
+
72
+ return {
73
+ id: edge.id,
74
+ source: edge.from,
75
+ target: edge.to,
76
+ sourceHandle: edgeWithHandles.sourceHandle,
77
+ targetHandle: edgeWithHandles.targetHandle,
78
+ type: 'custom',
79
+ animated: typeDefinition?.style === 'animated',
80
+ markerEnd,
81
+ data: {
82
+ typeDefinition,
83
+ hasViolations,
84
+ data: edge.data,
85
+ },
86
+ };
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Auto-layout nodes if they don't have positions
92
+ */
93
+ export function autoLayoutNodes<T extends Record<string, unknown>>(
94
+ nodes: Node<T>[],
95
+ edges: Edge[],
96
+ layoutType: 'hierarchical' | 'force-directed' | 'circular' | 'manual' = 'hierarchical'
97
+ ): Node<T>[] {
98
+ // Skip if all nodes have positions
99
+ const hasPositions = nodes.every(n => n.position.x !== 0 || n.position.y !== 0);
100
+ if (hasPositions || layoutType === 'manual') {
101
+ return nodes;
102
+ }
103
+
104
+ switch (layoutType) {
105
+ case 'hierarchical':
106
+ return applyHierarchicalLayout(nodes, edges);
107
+ case 'circular':
108
+ return applyCircularLayout(nodes);
109
+ case 'force-directed':
110
+ // For now, use hierarchical as fallback
111
+ // TODO: Implement force-directed with elkjs
112
+ return applyHierarchicalLayout(nodes, edges);
113
+ default:
114
+ return nodes;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Simple hierarchical layout algorithm
120
+ */
121
+ function applyHierarchicalLayout<T extends Record<string, unknown>>(nodes: Node<T>[], edges: Edge[]): Node<T>[] {
122
+ // Build adjacency list
123
+ const adjacency = new Map<string, string[]>();
124
+ const inDegree = new Map<string, number>();
125
+
126
+ nodes.forEach(node => {
127
+ adjacency.set(node.id, []);
128
+ inDegree.set(node.id, 0);
129
+ });
130
+
131
+ edges.forEach(edge => {
132
+ const targets = adjacency.get(edge.source) || [];
133
+ targets.push(edge.target);
134
+ adjacency.set(edge.source, targets);
135
+ inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
136
+ });
137
+
138
+ // Topological sort to determine layers
139
+ const layers: string[][] = [];
140
+ const queue: string[] = [];
141
+
142
+ // Start with nodes that have no incoming edges
143
+ inDegree.forEach((degree, nodeId) => {
144
+ if (degree === 0) {
145
+ queue.push(nodeId);
146
+ }
147
+ });
148
+
149
+ const visited = new Set<string>();
150
+
151
+ while (queue.length > 0) {
152
+ const currentLayer: string[] = [];
153
+ const layerSize = queue.length;
154
+
155
+ for (let i = 0; i < layerSize; i++) {
156
+ const nodeId = queue.shift()!;
157
+ currentLayer.push(nodeId);
158
+ visited.add(nodeId);
159
+
160
+ const neighbors = adjacency.get(nodeId) || [];
161
+ neighbors.forEach(neighbor => {
162
+ const degree = inDegree.get(neighbor)! - 1;
163
+ inDegree.set(neighbor, degree);
164
+ if (degree === 0 && !visited.has(neighbor)) {
165
+ queue.push(neighbor);
166
+ }
167
+ });
168
+ }
169
+
170
+ if (currentLayer.length > 0) {
171
+ layers.push(currentLayer);
172
+ }
173
+ }
174
+
175
+ // Handle any remaining nodes (cycles or disconnected)
176
+ const remainingNodes = nodes.filter(n => !visited.has(n.id)).map(n => n.id);
177
+ if (remainingNodes.length > 0) {
178
+ layers.push(remainingNodes);
179
+ }
180
+
181
+ // Position nodes based on layers
182
+ const LAYER_HEIGHT = 150;
183
+ const NODE_WIDTH = 200;
184
+
185
+ return nodes.map(node => {
186
+ const layerIndex = layers.findIndex(layer => layer.includes(node.id));
187
+ const layer = layers[layerIndex] || [];
188
+ const positionInLayer = layer.indexOf(node.id);
189
+ const layerWidth = layer.length * NODE_WIDTH;
190
+
191
+ return {
192
+ ...node,
193
+ position: {
194
+ x: (positionInLayer * NODE_WIDTH) + (NODE_WIDTH / 2) - (layerWidth / 2),
195
+ y: layerIndex * LAYER_HEIGHT,
196
+ },
197
+ };
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Simple circular layout algorithm
203
+ */
204
+ function applyCircularLayout<T extends Record<string, unknown>>(nodes: Node<T>[]): Node<T>[] {
205
+ const radius = Math.max(200, nodes.length * 30);
206
+ const angleStep = (2 * Math.PI) / nodes.length;
207
+
208
+ return nodes.map((node, index) => {
209
+ const angle = index * angleStep;
210
+ return {
211
+ ...node,
212
+ position: {
213
+ x: radius * Math.cos(angle) + radius,
214
+ y: radius * Math.sin(angle) + radius,
215
+ },
216
+ };
217
+ });
218
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import * as LucideIcons from 'lucide-react';
3
+
4
+ /**
5
+ * Icon Resolver
6
+ *
7
+ * Resolves icon strings to React components. Supports:
8
+ * 1. Lucide icon names (e.g., "Database", "Settings", "Package")
9
+ * 2. Emoji/Unicode strings (e.g., "⚙️", "💾", "📦")
10
+ *
11
+ * This keeps configurations JSON-serializable while supporting icon libraries.
12
+ */
13
+
14
+ export interface IconProps {
15
+ icon?: string;
16
+ size?: number;
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * Resolves an icon string to a React element
22
+ *
23
+ * @param icon - Icon name (Lucide) or emoji/text string
24
+ * @param size - Icon size in pixels (default: 20)
25
+ * @param className - Optional CSS class
26
+ * @returns React element or null
27
+ */
28
+ export function resolveIcon(icon?: string, size: number = 20, className?: string): React.ReactNode {
29
+ if (!icon) return null;
30
+
31
+ // Try to resolve as Lucide icon first
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ const LucideIcon = (LucideIcons as any)[icon];
34
+
35
+ // Check if it's a valid React component (function or object with $$typeof)
36
+ if (LucideIcon && (typeof LucideIcon === 'function' || typeof LucideIcon === 'object')) {
37
+ return <LucideIcon size={size} className={className} />;
38
+ }
39
+
40
+ // Fall back to rendering as text (emoji or unicode)
41
+ return <span className={className} style={{ fontSize: `${size}px` }}>{icon}</span>;
42
+ }
43
+
44
+ /**
45
+ * Icon component that resolves icon strings
46
+ */
47
+ export const Icon: React.FC<IconProps> = ({ icon, size = 20, className }) => {
48
+ return <>{resolveIcon(icon, size, className)}</>;
49
+ };