@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.
- package/package.json +9 -3
- package/web/dist/assets/{index-Bu-j1wcp.js → index-R1Ga3mzd.js} +10 -10
- package/web/dist/assets/{index-B7g4wBah.css → index-cSvqzd3J.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/index.html +12 -0
- package/web/package.json +30 -0
- package/web/public/logo-transparent-bg.png +0 -0
- package/web/public/report-data.json +5827 -0
- package/web/src/App.tsx +192 -0
- package/web/src/components/ErrorDisplay.tsx +39 -0
- package/web/src/components/GraphCanvas.tsx +205 -0
- package/web/src/components/LegendPanel.tsx +284 -0
- package/web/src/components/LoadingSpinner.tsx +19 -0
- package/web/src/components/Navbar.tsx +169 -0
- package/web/src/components/NodeDetails.tsx +210 -0
- package/web/src/components/index.ts +6 -0
- package/web/src/constants.ts +99 -0
- package/web/src/css.d.ts +4 -0
- package/web/src/hooks/useDimensions.ts +28 -0
- package/web/src/hooks/useTheme.ts +27 -0
- package/web/src/main.tsx +11 -0
- package/web/src/style.css +2 -0
- package/web/src/styles/index.css +42 -0
- package/web/src/types.ts +78 -0
- package/web/src/utils.ts +184 -0
- package/web/tsconfig.json +22 -0
- package/web/vite.config.ts +46 -0
|
@@ -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';
|