@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,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
|
+
};
|