@aiready/visualizer 0.1.13 → 0.1.18

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.
@@ -0,0 +1,284 @@
1
+ import { ThemeColors, SeverityLevel, EdgeType } from '../types';
2
+ import { severityColors, edgeColors } from '../constants';
3
+
4
+ // Checkbox/Toggle Icon
5
+ const CheckIcon = ({ checked }: { checked: boolean }) => (
6
+ <svg
7
+ width="14"
8
+ height="14"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke={checked ? '#10b981' : '#6b7280'}
12
+ strokeWidth="3"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ >
16
+ {checked && <path d="M20 6L9 17l-5-5" />}
17
+ </svg>
18
+ );
19
+
20
+ // Legend Item with Toggle
21
+ function LegendItemWithToggle({
22
+ color,
23
+ label,
24
+ isGlow = false,
25
+ isLine = false,
26
+ colors,
27
+ isVisible,
28
+ onToggle
29
+ }: {
30
+ color: string;
31
+ label: string;
32
+ isGlow?: boolean;
33
+ isLine?: boolean;
34
+ colors: ThemeColors;
35
+ isVisible: boolean;
36
+ onToggle: () => void;
37
+ }) {
38
+ return (
39
+ <button
40
+ onClick={onToggle}
41
+ className="group cursor-pointer transition-all hover:bg-white/5 w-full"
42
+ style={{
43
+ display: 'flex',
44
+ alignItems: 'center',
45
+ gap: '8px',
46
+ padding: '6px 8px',
47
+ borderRadius: '8px',
48
+ opacity: isVisible ? 1 : 0.4,
49
+ }}
50
+ title={isVisible ? `Click to hide ${label}` : `Click to show ${label}`}
51
+ >
52
+ {/* Toggle checkbox */}
53
+ <div
54
+ className="flex-shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors"
55
+ style={{
56
+ borderColor: isVisible ? '#10b981' : colors.panelBorder,
57
+ backgroundColor: isVisible ? `${color}20` : 'transparent'
58
+ }}
59
+ >
60
+ {isVisible && (
61
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="3">
62
+ <path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
63
+ </svg>
64
+ )}
65
+ </div>
66
+
67
+ {isLine ? (
68
+ <span
69
+ className="w-10 h-1 rounded-full flex-shrink-0"
70
+ style={{ backgroundColor: color }}
71
+ />
72
+ ) : (
73
+ <span
74
+ className={`w-4 h-4 rounded-full flex-shrink-0 ${isGlow ? 'shadow-lg' : ''}`}
75
+ style={{
76
+ backgroundColor: color,
77
+ boxShadow: isGlow && isVisible ? `0 0 10px ${color}90` : 'none'
78
+ }}
79
+ />
80
+ )}
81
+ <span
82
+ className="text-sm font-medium transition-colors leading-tight"
83
+ style={{ color: colors.textMuted }}
84
+ >
85
+ {label}
86
+ </span>
87
+ </button>
88
+ );
89
+ }
90
+
91
+ // Regular Legend Item (non-toggleable)
92
+ function LegendItem({
93
+ color,
94
+ label,
95
+ isGlow = false,
96
+ isLine = false,
97
+ colors
98
+ }: {
99
+ color: string;
100
+ label: string;
101
+ isGlow?: boolean;
102
+ isLine?: boolean;
103
+ colors: ThemeColors;
104
+ }) {
105
+ return (
106
+ <div
107
+ className="group cursor-default transition-all hover:bg-white/5"
108
+ style={{
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ gap: '8px',
112
+ padding: '6px 0',
113
+ borderRadius: '8px'
114
+ }}
115
+ >
116
+ {isLine ? (
117
+ <span
118
+ className="w-10 h-1 rounded-full transition-transform group-hover:scale-y-150 flex-shrink-0"
119
+ style={{ backgroundColor: color }}
120
+ />
121
+ ) : (
122
+ <span
123
+ className={`w-4 h-4 rounded-full transition-transform group-hover:scale-125 flex-shrink-0 ${isGlow ? 'shadow-lg' : ''}`}
124
+ style={{
125
+ backgroundColor: color,
126
+ boxShadow: isGlow ? `0 0 10px ${color}90` : 'none'
127
+ }}
128
+ />
129
+ )}
130
+ <span
131
+ className="text-sm font-medium transition-colors leading-tight"
132
+ style={{ color: colors.textMuted }}
133
+ >
134
+ {label}
135
+ </span>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ interface LegendPanelProps {
141
+ colors: ThemeColors;
142
+ visibleSeverities: Set<SeverityLevel>;
143
+ visibleEdgeTypes: Set<EdgeType>;
144
+ onToggleSeverity: (severity: SeverityLevel) => void;
145
+ onToggleEdgeType: (edgeType: EdgeType) => void;
146
+ }
147
+
148
+ export function LegendPanel({
149
+ colors,
150
+ visibleSeverities,
151
+ visibleEdgeTypes,
152
+ onToggleSeverity,
153
+ onToggleEdgeType
154
+ }: LegendPanelProps) {
155
+ // Get visible counts - exclude 'default' (No Issues) from count
156
+ const visibleSeverityCount = visibleSeverities.size;
157
+ const totalSeverities = Object.keys(severityColors).length - 1; // -1 for 'default'
158
+ const visibleEdgeCount = visibleEdgeTypes.size;
159
+ const totalEdgeTypes = Object.keys(edgeColors).filter(k => k !== 'default' && k !== 'reference').length;
160
+
161
+ return (
162
+ <div style={{ padding: '16px 16px', animation: 'fadeIn 0.2s ease-in' }}>
163
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
164
+ {/* Header */}
165
+ <div style={{ paddingBottom: '16px', borderBottom: `1px solid ${colors.cardBorder}` }}>
166
+ <h2
167
+ className="text-base font-bold tracking-wide"
168
+ style={{ color: colors.text }}
169
+ >
170
+ Legend
171
+ </h2>
172
+ </div>
173
+
174
+ {/* Severity Legend with Toggles */}
175
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
176
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
177
+ <h3
178
+ className="text-xs font-bold uppercase tracking-widest"
179
+ style={{ color: colors.textMuted }}
180
+ >
181
+ Severity
182
+ </h3>
183
+ <span
184
+ className="text-xs font-medium"
185
+ style={{ color: visibleSeverityCount === totalSeverities ? '#10b981' : '#f59e0b' }}
186
+ >
187
+ {visibleSeverityCount}/{totalSeverities}
188
+ </span>
189
+ </div>
190
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
191
+ {Object.entries(severityColors)
192
+ .filter(([key]) => key !== 'default') // Filter out "No Issues" - not useful for visualization
193
+ .map(([key, color]) => (
194
+ <LegendItemWithToggle
195
+ key={key}
196
+ color={color}
197
+ label={key.charAt(0).toUpperCase() + key.slice(1)}
198
+ isGlow={key !== 'default'}
199
+ colors={colors}
200
+ isVisible={visibleSeverities.has(key as SeverityLevel)}
201
+ onToggle={() => onToggleSeverity(key as SeverityLevel)}
202
+ />
203
+ ))}
204
+ </div>
205
+ </div>
206
+
207
+ {/* Connections Legend with Toggles */}
208
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
209
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
210
+ <h3
211
+ className="text-xs font-bold uppercase tracking-widest"
212
+ style={{ color: colors.textMuted }}
213
+ >
214
+ Connections
215
+ </h3>
216
+ <span
217
+ className="text-xs font-medium"
218
+ style={{ color: visibleEdgeCount === totalEdgeTypes ? '#10b981' : '#f59e0b' }}
219
+ >
220
+ {visibleEdgeCount}/{totalEdgeTypes}
221
+ </span>
222
+ </div>
223
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
224
+ {Object.entries(edgeColors)
225
+ .filter(([k]) => k !== 'default' && k !== 'reference')
226
+ .map(([key, color]) => (
227
+ <LegendItemWithToggle
228
+ key={key}
229
+ color={color}
230
+ label={key.charAt(0).toUpperCase() + key.slice(1)}
231
+ isLine
232
+ colors={colors}
233
+ isVisible={visibleEdgeTypes.has(key as EdgeType)}
234
+ onToggle={() => onToggleEdgeType(key as EdgeType)}
235
+ />
236
+ ))}
237
+ </div>
238
+ </div>
239
+
240
+ {/* Node Size Info */}
241
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
242
+ <h3
243
+ className="text-xs font-bold uppercase tracking-widest"
244
+ style={{ color: colors.textMuted, marginBottom: '4px' }}
245
+ >
246
+ Node Size
247
+ </h3>
248
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
249
+ <p
250
+ className="text-xs leading-relaxed"
251
+ style={{ color: colors.textMuted }}
252
+ >
253
+ Larger nodes indicate higher <span className="text-cyan-400 font-medium">token cost</span> and more <span className="text-purple-400 font-medium">issues</span>.
254
+ </p>
255
+ <div className="flex items-center gap-3">
256
+ <div className="flex -space-x-2">
257
+ <span className="w-3 h-3 rounded-full bg-cyan-400 border-2" style={{ borderColor: colors.panel }} />
258
+ <span className="w-5 h-5 rounded-full bg-cyan-400 border-2" style={{ borderColor: colors.panel }} />
259
+ <span className="w-7 h-7 rounded-full bg-cyan-400 border-2" style={{ borderColor: colors.panel }} />
260
+ </div>
261
+ <span className="text-xs" style={{ color: colors.textMuted }}>→</span>
262
+ <span className="text-xs font-medium" style={{ color: colors.text }}>More Impact</span>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ {/* Quick Tips */}
268
+ <div
269
+ className="p-4 rounded-xl"
270
+ style={{
271
+ backgroundColor: `${colors.cardBg}80`
272
+ }}
273
+ >
274
+ <p
275
+ className="text-xs leading-relaxed"
276
+ style={{ color: colors.textMuted }}
277
+ >
278
+ <span className="font-semibold text-amber-400">💡 Tip:</span> Click and drag nodes to rearrange. Scroll to zoom. Toggle items above to filter.
279
+ </p>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ );
284
+ }
@@ -0,0 +1,19 @@
1
+ import { ThemeColors } from '../types';
2
+
3
+ interface LoadingSpinnerProps {
4
+ colors: ThemeColors;
5
+ }
6
+
7
+ export function LoadingSpinner({ colors }: LoadingSpinnerProps) {
8
+ return (
9
+ <div className="flex h-screen items-center justify-center" style={{ backgroundColor: colors.bg, color: colors.text }}>
10
+ <div className="text-center">
11
+ <div
12
+ className="animate-spin rounded-full h-12 w-12 border-b-2 mx-auto mb-4"
13
+ style={{ borderColor: colors.text }}
14
+ />
15
+ <p>Loading visualization...</p>
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,169 @@
1
+ import { ThemeColors, GraphData, Theme } from '../types';
2
+
3
+ // Icons as inline SVG components for the theme toggle
4
+ const SunIcon = ({ className }: { className?: string }) => (
5
+ <svg
6
+ className={className}
7
+ width="16"
8
+ height="16"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ >
16
+ <circle cx="12" cy="12" r="4" />
17
+ <path d="M12 2v2" />
18
+ <path d="M12 20v2" />
19
+ <path d="m4.93 4.93 1.41 1.41" />
20
+ <path d="m17.66 17.66 1.41 1.41" />
21
+ <path d="M2 12h2" />
22
+ <path d="M20 12h2" />
23
+ <path d="m6.34 17.66-1.41 1.41" />
24
+ <path d="m19.07 4.93-1.41 1.41" />
25
+ </svg>
26
+ );
27
+
28
+ const MoonIcon = ({ className }: { className?: string }) => (
29
+ <svg
30
+ className={className}
31
+ width="16"
32
+ height="16"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ strokeWidth="2"
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ >
40
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
41
+ </svg>
42
+ );
43
+
44
+ const SystemIcon = ({ className }: { className?: string }) => (
45
+ <svg
46
+ className={className}
47
+ width="16"
48
+ height="16"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ strokeWidth="2"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ >
56
+ <rect width="20" height="14" x="2" y="3" rx="2" />
57
+ <path d="M8 21h8" />
58
+ <path d="M12 17v4" />
59
+ </svg>
60
+ );
61
+
62
+ interface NavbarProps {
63
+ colors: ThemeColors;
64
+ theme: Theme;
65
+ setTheme: (theme: Theme) => void;
66
+ data: GraphData | null;
67
+ }
68
+
69
+ export function Navbar({ colors, theme, setTheme, data }: NavbarProps) {
70
+ return (
71
+ <nav
72
+ className="h-16 backdrop-blur-md border-b flex items-center justify-between px-6 z-50 relative"
73
+ style={{
74
+ backgroundColor: `${colors.panel}f5`,
75
+ borderColor: colors.panelBorder
76
+ }}
77
+ >
78
+ {/* Subtle gradient overlay */}
79
+ <div
80
+ className="absolute inset-0 pointer-events-none"
81
+ style={{
82
+ background: `linear-gradient(90deg, ${colors.panel}80 0%, transparent 50%, ${colors.panel}80 100%)`,
83
+ }}
84
+ />
85
+
86
+ <div className="flex items-center gap-5 relative z-10">
87
+ <div className="flex items-center justify-center">
88
+ <img
89
+ src="/logo-transparent-bg.png"
90
+ alt="AIReady"
91
+ className="h-9 w-auto"
92
+ />
93
+ </div>
94
+ <div
95
+ className="h-7 w-px"
96
+ style={{ backgroundColor: colors.panelBorder }}
97
+ />
98
+ <h1
99
+ className="text-sm font-semibold tracking-wide uppercase"
100
+ style={{ color: colors.textMuted }}
101
+ >
102
+ Codebase
103
+ </h1>
104
+ </div>
105
+
106
+ <div className="flex items-center gap-6 relative z-10">
107
+ {/* Modern Theme Toggle */}
108
+ <div className="flex items-center rounded-lg border px-4 py-3" style={{ borderColor: colors.panelBorder, backgroundColor: `${colors.panel}80`, gap: '12px' }}>
109
+ {[
110
+ { key: 'dark', icon: MoonIcon, label: 'Dark' },
111
+ { key: 'light', icon: SunIcon, label: 'Light' },
112
+ { key: 'system', icon: SystemIcon, label: 'System' },
113
+ ].map(({ key, icon: Icon, label }) => (
114
+ <button
115
+ key={key}
116
+ onClick={() => setTheme(key as Theme)}
117
+ className="group flex items-center rounded-md text-xs font-medium transition-all duration-200"
118
+ style={{
119
+ backgroundColor: theme === key ? colors.cardBg : 'transparent',
120
+ color: theme === key ? colors.text : colors.textMuted,
121
+ border: `1px solid ${theme === key ? colors.cardBorder : 'transparent'}`,
122
+ padding: '8px 16px',
123
+ gap: '8px',
124
+ }}
125
+ title={`Switch to ${label} theme`}
126
+ >
127
+ <Icon
128
+ className="transition-transform duration-200"
129
+ style={{ transform: theme === key ? 'scale(1.1)' : 'scale(1)' }}
130
+ />
131
+ <span>{label}</span>
132
+ </button>
133
+ ))}
134
+ </div>
135
+
136
+ {data && (
137
+ <div className="flex items-center" style={{ gap: '12px' }}>
138
+ <div
139
+ className="rounded-lg border text-xs font-medium flex items-center"
140
+ style={{
141
+ backgroundColor: `${colors.cardBg}cc`,
142
+ borderColor: colors.cardBorder,
143
+ padding: '8px 14px',
144
+ gap: '8px',
145
+ }}
146
+ >
147
+ <span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
148
+ <span style={{ color: colors.textMuted }}>Files</span>
149
+ <span style={{ color: colors.text }}>{data.nodes.length}</span>
150
+ </div>
151
+ <div
152
+ className="rounded-lg border text-xs font-medium flex items-center"
153
+ style={{
154
+ backgroundColor: `${colors.cardBg}cc`,
155
+ borderColor: colors.cardBorder,
156
+ padding: '8px 14px',
157
+ gap: '8px',
158
+ }}
159
+ >
160
+ <span className="w-2 h-2 rounded-full bg-purple-400" />
161
+ <span style={{ color: colors.textMuted }}>Links</span>
162
+ <span style={{ color: colors.text }}>{data.edges.length}</span>
163
+ </div>
164
+ </div>
165
+ )}
166
+ </div>
167
+ </nav>
168
+ );
169
+ }
@@ -0,0 +1,210 @@
1
+ import { ThemeColors, FileNode } from '../types';
2
+
3
+ // Info Row Component for consistent styling
4
+ function InfoRow({
5
+ label,
6
+ value,
7
+ valueColor = 'inherit',
8
+ highlight = false,
9
+ colors
10
+ }: {
11
+ label: string;
12
+ value: string | number;
13
+ valueColor?: string;
14
+ highlight?: boolean;
15
+ colors: ThemeColors;
16
+ }) {
17
+ return (
18
+ <div
19
+ className="flex justify-between items-center py-2.5 px-3 rounded-lg transition-colors hover:bg-white/5"
20
+ >
21
+ <span
22
+ className="text-xs font-medium"
23
+ style={{ color: colors.textMuted }}
24
+ >
25
+ {label}
26
+ </span>
27
+ <span
28
+ className={`text-xs font-semibold ${highlight ? 'px-3 py-1 rounded-full' : ''}`}
29
+ style={{
30
+ color: valueColor,
31
+ backgroundColor: highlight ? `${valueColor}25` : 'transparent'
32
+ }}
33
+ >
34
+ {value}
35
+ </span>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ interface NodeDetailsProps {
41
+ colors: ThemeColors;
42
+ selectedNode: FileNode | null;
43
+ onClose?: () => void;
44
+ }
45
+
46
+ export function NodeDetails({ colors, selectedNode, onClose }: NodeDetailsProps) {
47
+ return (
48
+ <div
49
+ style={{
50
+ padding: '16px 16px',
51
+ height: '100%',
52
+ overflowY: 'auto',
53
+ overflowX: 'hidden',
54
+ animation: 'fadeIn 0.2s ease-in'
55
+ }}
56
+ >
57
+ {/* Header */}
58
+ <div className="flex justify-between items-center" style={{ marginBottom: '16px' }}>
59
+ <div className="flex items-center gap-2">
60
+ <div
61
+ className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"
62
+ />
63
+ <h3
64
+ className="text-xs font-bold uppercase tracking-widest"
65
+ style={{ color: colors.textMuted }}
66
+ >
67
+ Selected Node
68
+ </h3>
69
+ </div>
70
+ {onClose && (
71
+ <button
72
+ onClick={onClose}
73
+ className="p-2 rounded-lg hover:bg-white/10 transition-colors"
74
+ style={{ color: colors.textMuted }}
75
+ title="Close details"
76
+ >
77
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
78
+ <path d="M18 6L6 18M6 6l12 12" />
79
+ </svg>
80
+ </button>
81
+ )}
82
+ </div>
83
+
84
+ {selectedNode ? (
85
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
86
+ {/* File Name - No border, just padding */}
87
+ <div className="p-4 rounded-xl"
88
+ style={{
89
+ backgroundColor: `${colors.cardBg}80`
90
+ }}
91
+ >
92
+ <div className="flex items-center gap-2 mb-2">
93
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-amber-400">
94
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
95
+ <polyline points="14,2 14,8 20,8" />
96
+ </svg>
97
+ <h4 className="font-semibold text-sm truncate" style={{ color: colors.text }}>
98
+ {selectedNode.label}
99
+ </h4>
100
+ </div>
101
+ <p
102
+ className="text-xs break-all truncate"
103
+ style={{ color: colors.textMuted }}
104
+ title={selectedNode.id}
105
+ >
106
+ {selectedNode.id}
107
+ </p>
108
+ </div>
109
+
110
+ {/* Metrics - No border */}
111
+ <div className="rounded-xl p-1">
112
+ <div className="px-3 py-2">
113
+ <span className="text-xs font-semibold uppercase tracking-wider" style={{ color: colors.textMuted }}>
114
+ Metrics
115
+ </span>
116
+ </div>
117
+ <div className="p-1">
118
+ <InfoRow
119
+ label="Severity"
120
+ value={selectedNode.severity || 'none'}
121
+ valueColor={selectedNode.color}
122
+ highlight
123
+ colors={colors}
124
+ />
125
+ <InfoRow
126
+ label="Token Cost"
127
+ value={selectedNode.tokenCost?.toLocaleString() || '0'}
128
+ valueColor="#22d3ee"
129
+ colors={colors}
130
+ />
131
+ {selectedNode.duplicates !== undefined && (
132
+ <InfoRow
133
+ label="Issues Found"
134
+ value={selectedNode.duplicates}
135
+ valueColor="#c084fc"
136
+ colors={colors}
137
+ />
138
+ )}
139
+ </div>
140
+ </div>
141
+
142
+ {/* Description/Details - No border */}
143
+ {selectedNode.title && (
144
+ <div
145
+ className="rounded-xl p-4"
146
+ style={{
147
+ backgroundColor: `${colors.cardBg}80`
148
+ }}
149
+ >
150
+ <div className="flex items-center gap-2 mb-3">
151
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ color: colors.textMuted }}>
152
+ <circle cx="12" cy="12" r="10" />
153
+ <path d="M12 16v-4M12 8h.01" />
154
+ </svg>
155
+ <h5
156
+ className="text-xs font-semibold uppercase tracking-wider"
157
+ style={{ color: colors.textMuted }}
158
+ >
159
+ Details
160
+ </h5>
161
+ </div>
162
+ <pre
163
+ className="text-xs whitespace-pre-wrap font-mono leading-relaxed p-3 rounded-lg"
164
+ style={{
165
+ color: colors.textMuted,
166
+ backgroundColor: colors.panel,
167
+ overflow: 'auto'
168
+ }}
169
+ >
170
+ {selectedNode.title}
171
+ </pre>
172
+ </div>
173
+ )}
174
+ </div>
175
+ ) : (
176
+ <div
177
+ className="flex flex-col items-center justify-center py-10 text-center rounded-xl"
178
+ style={{
179
+ backgroundColor: `${colors.cardBg}50`
180
+ }}
181
+ >
182
+ <svg
183
+ width="48"
184
+ height="48"
185
+ viewBox="0 0 24 24"
186
+ fill="none"
187
+ stroke="currentColor"
188
+ strokeWidth="1.5"
189
+ className="mb-3 opacity-40"
190
+ style={{ color: colors.textMuted }}
191
+ >
192
+ <circle cx="11" cy="11" r="8" />
193
+ <path d="m21 21-4.3-4.3" />
194
+ </svg>
195
+ <p
196
+ className="text-sm font-medium"
197
+ style={{ color: colors.textMuted }}
198
+ >
199
+ Click a node to view details
200
+ </p>
201
+ </div>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ // Keep the old component for backwards compatibility
208
+ export function NodeDetailsOld({ colors, selectedNode }: NodeDetailsProps) {
209
+ return <NodeDetails colors={colors} selectedNode={selectedNode} />;
210
+ }
@@ -0,0 +1,6 @@
1
+ export { LoadingSpinner } from './LoadingSpinner';
2
+ export { ErrorDisplay } from './ErrorDisplay';
3
+ export { Navbar } from './Navbar';
4
+ export { LegendPanel } from './LegendPanel';
5
+ export { NodeDetails } from './NodeDetails';
6
+ export { GraphCanvas } from './GraphCanvas';