@hydration-audit/dashboard 0.2.1 → 0.2.3
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/dist/index.cjs +3 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.html +1607 -0
- package/dist/index.mjs +3 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.html
ADDED
|
@@ -0,0 +1,1607 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Hydration Tax Dashboard</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #07090e;
|
|
13
|
+
--surface: #0e111a;
|
|
14
|
+
--surface-hover: #151926;
|
|
15
|
+
--surface-active: #1d2234;
|
|
16
|
+
--border: #1f2538;
|
|
17
|
+
--border-focus: #3b82f6;
|
|
18
|
+
--text: #f1f5f9;
|
|
19
|
+
--text-secondary: #94a3b8;
|
|
20
|
+
--text-muted: #64748b;
|
|
21
|
+
--green: #10b981;
|
|
22
|
+
--green-alpha: rgba(16, 185, 129, 0.15);
|
|
23
|
+
--yellow: #f59e0b;
|
|
24
|
+
--yellow-alpha: rgba(245, 158, 11, 0.15);
|
|
25
|
+
--red: #ef4444;
|
|
26
|
+
--red-alpha: rgba(239, 68, 68, 0.15);
|
|
27
|
+
--blue: #3b82f6;
|
|
28
|
+
--blue-alpha: rgba(59, 130, 246, 0.15);
|
|
29
|
+
--purple: #8b5cf6;
|
|
30
|
+
--purple-alpha: rgba(139, 92, 246, 0.15);
|
|
31
|
+
--sidebar-width: 260px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
35
|
+
body {
|
|
36
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
overflow-x: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Layout */
|
|
44
|
+
.container {
|
|
45
|
+
display: flex;
|
|
46
|
+
min-height: 100vh;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sidebar {
|
|
50
|
+
width: var(--sidebar-width);
|
|
51
|
+
background: var(--surface);
|
|
52
|
+
border-right: 1px solid var(--border);
|
|
53
|
+
padding: 2rem 1.5rem;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 2rem;
|
|
57
|
+
position: fixed;
|
|
58
|
+
height: 100vh;
|
|
59
|
+
z-index: 10;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.main-content {
|
|
63
|
+
flex: 1;
|
|
64
|
+
margin-left: var(--sidebar-width);
|
|
65
|
+
padding: 2rem 3rem;
|
|
66
|
+
max-width: 1600px;
|
|
67
|
+
width: calc(100% - var(--sidebar-width));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Brand & Info */
|
|
71
|
+
.brand h1 {
|
|
72
|
+
font-size: 1.2rem;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
letter-spacing: -0.02em;
|
|
75
|
+
background: linear-gradient(135deg, #fff 0%, var(--text-secondary) 100%);
|
|
76
|
+
-webkit-background-clip: text;
|
|
77
|
+
-webkit-text-fill-color: transparent;
|
|
78
|
+
margin-bottom: 0.25rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.brand .framework-tag {
|
|
82
|
+
font-size: 0.75rem;
|
|
83
|
+
padding: 2px 8px;
|
|
84
|
+
background: var(--surface-active);
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
color: var(--blue);
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
letter-spacing: 0.05em;
|
|
90
|
+
display: inline-block;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.project-meta {
|
|
94
|
+
font-size: 0.8rem;
|
|
95
|
+
color: var(--text-muted);
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
gap: 0.5rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.project-meta div span {
|
|
102
|
+
display: block;
|
|
103
|
+
color: var(--text-secondary);
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Sidebar Navigation */
|
|
108
|
+
.nav {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
gap: 0.5rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.nav-item {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
padding: 0.75rem 1rem;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
color: var(--text-secondary);
|
|
121
|
+
text-decoration: none;
|
|
122
|
+
font-size: 0.9rem;
|
|
123
|
+
font-weight: 500;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: all 0.2s ease;
|
|
126
|
+
background: transparent;
|
|
127
|
+
border: none;
|
|
128
|
+
text-align: left;
|
|
129
|
+
width: 100%;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.nav-item:hover {
|
|
133
|
+
background: var(--surface-hover);
|
|
134
|
+
color: var(--text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.nav-item.active {
|
|
138
|
+
background: var(--surface-active);
|
|
139
|
+
color: var(--text);
|
|
140
|
+
border-left: 3px solid var(--blue);
|
|
141
|
+
border-top-left-radius: 4px;
|
|
142
|
+
border-bottom-left-radius: 4px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.nav-badge {
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
padding: 2px 6px;
|
|
148
|
+
border-radius: 20px;
|
|
149
|
+
background: var(--surface-hover);
|
|
150
|
+
color: var(--text-secondary);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.nav-item.active .nav-badge {
|
|
154
|
+
background: var(--blue-alpha);
|
|
155
|
+
color: var(--blue);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Status dot */
|
|
159
|
+
.status {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
font-size: 0.8rem;
|
|
164
|
+
color: var(--text-secondary);
|
|
165
|
+
margin-top: auto;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.status-dot {
|
|
169
|
+
width: 8px;
|
|
170
|
+
height: 8px;
|
|
171
|
+
border-radius: 50%;
|
|
172
|
+
background: var(--green);
|
|
173
|
+
animation: pulse 2s infinite;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@keyframes pulse {
|
|
177
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
178
|
+
50% { opacity: 0.4; transform: scale(1.1); }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Header */
|
|
182
|
+
.header {
|
|
183
|
+
display: flex;
|
|
184
|
+
justify-content: space-between;
|
|
185
|
+
align-items: center;
|
|
186
|
+
margin-bottom: 2rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.header-title h2 {
|
|
190
|
+
font-size: 1.75rem;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
letter-spacing: -0.02em;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.header-title p {
|
|
196
|
+
color: var(--text-secondary);
|
|
197
|
+
font-size: 0.9rem;
|
|
198
|
+
margin-top: 0.25rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Grid layout for Overview */
|
|
202
|
+
.overview-grid {
|
|
203
|
+
display: grid;
|
|
204
|
+
grid-template-columns: 2fr 1fr;
|
|
205
|
+
gap: 1.5rem;
|
|
206
|
+
margin-top: 1.5rem;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Stats Grid */
|
|
210
|
+
.stats-grid {
|
|
211
|
+
display: grid;
|
|
212
|
+
grid-template-columns: repeat(4, 1fr);
|
|
213
|
+
gap: 1.25rem;
|
|
214
|
+
margin-bottom: 1.5rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.stat-card {
|
|
218
|
+
background: var(--surface);
|
|
219
|
+
border: 1px solid var(--border);
|
|
220
|
+
border-radius: 12px;
|
|
221
|
+
padding: 1.25rem;
|
|
222
|
+
transition: border-color 0.2s ease;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.stat-card:hover {
|
|
226
|
+
border-color: var(--surface-active);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.stat-label {
|
|
230
|
+
font-size: 0.8rem;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
color: var(--text-secondary);
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
letter-spacing: 0.05em;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.stat-value {
|
|
238
|
+
font-size: 1.8rem;
|
|
239
|
+
font-weight: 700;
|
|
240
|
+
margin-top: 0.5rem;
|
|
241
|
+
letter-spacing: -0.02em;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.stat-value.green { color: var(--green); }
|
|
245
|
+
.stat-value.yellow { color: var(--yellow); }
|
|
246
|
+
.stat-value.red { color: var(--red); }
|
|
247
|
+
|
|
248
|
+
/* Card standard */
|
|
249
|
+
.card {
|
|
250
|
+
background: var(--surface);
|
|
251
|
+
border: 1px solid var(--border);
|
|
252
|
+
border-radius: 12px;
|
|
253
|
+
padding: 1.5rem;
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.card h3 {
|
|
259
|
+
font-size: 1rem;
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
margin-bottom: 1.25rem;
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
justify-content: space-between;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Treemap */
|
|
268
|
+
.treemap-wrapper {
|
|
269
|
+
position: relative;
|
|
270
|
+
width: 100%;
|
|
271
|
+
height: 420px;
|
|
272
|
+
background: var(--bg);
|
|
273
|
+
border-radius: 8px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
border: 1px solid var(--border);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.treemap-node {
|
|
279
|
+
position: absolute;
|
|
280
|
+
border: 1px solid var(--bg);
|
|
281
|
+
border-radius: 6px;
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
align-items: center;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
padding: 8px;
|
|
287
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.treemap-node:hover {
|
|
293
|
+
transform: scale(0.99);
|
|
294
|
+
filter: brightness(1.15);
|
|
295
|
+
z-index: 2;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.treemap-node-name {
|
|
299
|
+
font-size: 0.8rem;
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
white-space: nowrap;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
text-overflow: ellipsis;
|
|
304
|
+
max-width: 100%;
|
|
305
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.treemap-node-size {
|
|
309
|
+
font-size: 0.7rem;
|
|
310
|
+
opacity: 0.85;
|
|
311
|
+
margin-top: 2px;
|
|
312
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Budget gauges */
|
|
316
|
+
.budget-list {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
gap: 1.25rem;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.budget-item {
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
gap: 0.5rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.budget-meta {
|
|
329
|
+
display: flex;
|
|
330
|
+
justify-content: space-between;
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.budget-name {
|
|
335
|
+
font-weight: 500;
|
|
336
|
+
color: var(--text-secondary);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.budget-sizes {
|
|
340
|
+
color: var(--text-muted);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.budget-sizes strong {
|
|
344
|
+
color: var(--text);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.gauge-bar {
|
|
348
|
+
height: 10px;
|
|
349
|
+
background: var(--surface-active);
|
|
350
|
+
border-radius: 5px;
|
|
351
|
+
overflow: hidden;
|
|
352
|
+
position: relative;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.gauge-fill {
|
|
356
|
+
height: 100%;
|
|
357
|
+
border-radius: 5px;
|
|
358
|
+
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Filters Bar */
|
|
362
|
+
.filter-bar {
|
|
363
|
+
display: flex;
|
|
364
|
+
gap: 1rem;
|
|
365
|
+
align-items: center;
|
|
366
|
+
margin-bottom: 1.5rem;
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
background: var(--surface);
|
|
369
|
+
border: 1px solid var(--border);
|
|
370
|
+
padding: 1rem;
|
|
371
|
+
border-radius: 8px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.search-input {
|
|
375
|
+
background: var(--bg);
|
|
376
|
+
border: 1px solid var(--border);
|
|
377
|
+
padding: 0.5rem 1rem;
|
|
378
|
+
border-radius: 6px;
|
|
379
|
+
color: var(--text);
|
|
380
|
+
font-family: inherit;
|
|
381
|
+
font-size: 0.9rem;
|
|
382
|
+
width: 260px;
|
|
383
|
+
transition: border-color 0.2s ease;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.search-input:focus {
|
|
387
|
+
outline: none;
|
|
388
|
+
border-color: var(--border-focus);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.filter-group {
|
|
392
|
+
display: flex;
|
|
393
|
+
gap: 0.35rem;
|
|
394
|
+
align-items: center;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.filter-label {
|
|
398
|
+
font-size: 0.8rem;
|
|
399
|
+
color: var(--text-secondary);
|
|
400
|
+
margin-right: 0.5rem;
|
|
401
|
+
font-weight: 500;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.filter-pill {
|
|
405
|
+
background: var(--surface-hover);
|
|
406
|
+
border: 1px solid var(--border);
|
|
407
|
+
padding: 0.35rem 0.75rem;
|
|
408
|
+
border-radius: 6px;
|
|
409
|
+
font-size: 0.8rem;
|
|
410
|
+
font-weight: 500;
|
|
411
|
+
cursor: pointer;
|
|
412
|
+
color: var(--text-secondary);
|
|
413
|
+
transition: all 0.15s ease;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.filter-pill:hover {
|
|
417
|
+
background: var(--surface-active);
|
|
418
|
+
color: var(--text);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.filter-pill.active {
|
|
422
|
+
background: var(--blue-alpha);
|
|
423
|
+
border-color: var(--blue);
|
|
424
|
+
color: var(--blue);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* Table styles */
|
|
428
|
+
.table-wrapper {
|
|
429
|
+
overflow-x: auto;
|
|
430
|
+
border: 1px solid var(--border);
|
|
431
|
+
border-radius: 8px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
table {
|
|
435
|
+
width: 100%;
|
|
436
|
+
border-collapse: collapse;
|
|
437
|
+
text-align: left;
|
|
438
|
+
font-size: 0.9rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
th {
|
|
442
|
+
padding: 1rem;
|
|
443
|
+
background: var(--surface-hover);
|
|
444
|
+
color: var(--text-secondary);
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
border-bottom: 1px solid var(--border);
|
|
447
|
+
cursor: pointer;
|
|
448
|
+
user-select: none;
|
|
449
|
+
transition: color 0.15s ease;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
th:hover {
|
|
453
|
+
color: var(--text);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
td {
|
|
457
|
+
padding: 1rem;
|
|
458
|
+
border-bottom: 1px solid var(--border);
|
|
459
|
+
transition: background 0.15s ease;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
tr:last-child td {
|
|
463
|
+
border-bottom: none;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
tr.clickable-row {
|
|
467
|
+
cursor: pointer;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
tr.clickable-row:hover td {
|
|
471
|
+
background: var(--surface-hover);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.directive-tag {
|
|
475
|
+
display: inline-block;
|
|
476
|
+
padding: 2px 8px;
|
|
477
|
+
border-radius: 4px;
|
|
478
|
+
font-size: 0.75rem;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
font-family: 'JetBrains Mono', monospace;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.directive-tag.load { background: var(--red-alpha); color: var(--red); }
|
|
484
|
+
.directive-tag.idle { background: var(--yellow-alpha); color: var(--yellow); }
|
|
485
|
+
.directive-tag.visible { background: var(--green-alpha); color: var(--green); }
|
|
486
|
+
.directive-tag.media { background: var(--blue-alpha); color: var(--blue); }
|
|
487
|
+
.directive-tag.only { background: var(--purple-alpha); color: var(--purple); }
|
|
488
|
+
.directive-tag.script { background: var(--blue-alpha); color: var(--blue); }
|
|
489
|
+
|
|
490
|
+
.framework-badge {
|
|
491
|
+
display: inline-flex;
|
|
492
|
+
align-items: center;
|
|
493
|
+
font-size: 0.85rem;
|
|
494
|
+
font-weight: 500;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.framework-badge::before {
|
|
498
|
+
content: '';
|
|
499
|
+
display: inline-block;
|
|
500
|
+
width: 8px;
|
|
501
|
+
height: 8px;
|
|
502
|
+
border-radius: 50%;
|
|
503
|
+
margin-right: 8px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.framework-badge.react::before { background: #61dafb; }
|
|
507
|
+
.framework-badge.svelte::before { background: #ff3e00; }
|
|
508
|
+
.framework-badge.vue::before { background: #41b883; }
|
|
509
|
+
.framework-badge.solid::before { background: #446b9f; }
|
|
510
|
+
.framework-badge.qwik::before { background: #00f3cf; }
|
|
511
|
+
.framework-badge.astro::before { background: #ff5a03; }
|
|
512
|
+
.framework-badge.unknown::before { background: var(--text-muted); }
|
|
513
|
+
|
|
514
|
+
.issues-count {
|
|
515
|
+
display: inline-flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
min-width: 20px;
|
|
519
|
+
height: 20px;
|
|
520
|
+
border-radius: 50%;
|
|
521
|
+
font-size: 0.75rem;
|
|
522
|
+
font-weight: 600;
|
|
523
|
+
padding: 0 4px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.issues-count.zero { background: var(--surface-hover); color: var(--text-muted); }
|
|
527
|
+
.issues-count.warn { background: var(--yellow-alpha); color: var(--yellow); }
|
|
528
|
+
.issues-count.error { background: var(--red-alpha); color: var(--red); }
|
|
529
|
+
|
|
530
|
+
/* Issues list */
|
|
531
|
+
.issues-list {
|
|
532
|
+
display: flex;
|
|
533
|
+
flex-direction: column;
|
|
534
|
+
gap: 1rem;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.issue-card {
|
|
538
|
+
background: var(--surface-hover);
|
|
539
|
+
border: 1px solid var(--border);
|
|
540
|
+
border-radius: 8px;
|
|
541
|
+
padding: 1.25rem;
|
|
542
|
+
display: flex;
|
|
543
|
+
flex-direction: column;
|
|
544
|
+
gap: 0.5rem;
|
|
545
|
+
position: relative;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.issue-card.error { border-left: 4px solid var(--red); }
|
|
549
|
+
.issue-card.warning { border-left: 4px solid var(--yellow); }
|
|
550
|
+
.issue-card.info { border-left: 4px solid var(--blue); }
|
|
551
|
+
|
|
552
|
+
.issue-meta {
|
|
553
|
+
display: flex;
|
|
554
|
+
justify-content: space-between;
|
|
555
|
+
align-items: center;
|
|
556
|
+
font-size: 0.8rem;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.issue-component {
|
|
560
|
+
font-weight: 600;
|
|
561
|
+
color: var(--text);
|
|
562
|
+
cursor: pointer;
|
|
563
|
+
text-decoration: underline;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.issue-component:hover {
|
|
567
|
+
color: var(--blue);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.issue-severity {
|
|
571
|
+
text-transform: uppercase;
|
|
572
|
+
font-weight: 700;
|
|
573
|
+
font-size: 0.75rem;
|
|
574
|
+
letter-spacing: 0.05em;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.issue-severity.error { color: var(--red); }
|
|
578
|
+
.issue-severity.warning { color: var(--yellow); }
|
|
579
|
+
.issue-severity.info { color: var(--blue); }
|
|
580
|
+
|
|
581
|
+
.issue-message {
|
|
582
|
+
font-size: 0.95rem;
|
|
583
|
+
font-weight: 500;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.issue-recommendation {
|
|
587
|
+
background: var(--bg);
|
|
588
|
+
padding: 0.75rem 1rem;
|
|
589
|
+
border-radius: 6px;
|
|
590
|
+
font-size: 0.85rem;
|
|
591
|
+
color: var(--text-secondary);
|
|
592
|
+
border: 1px solid var(--border);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* Detail Inspector Drawer */
|
|
596
|
+
.drawer-overlay {
|
|
597
|
+
position: fixed;
|
|
598
|
+
top: 0;
|
|
599
|
+
left: 0;
|
|
600
|
+
right: 0;
|
|
601
|
+
bottom: 0;
|
|
602
|
+
background: rgba(0, 0, 0, 0.6);
|
|
603
|
+
backdrop-filter: blur(4px);
|
|
604
|
+
z-index: 99;
|
|
605
|
+
opacity: 0;
|
|
606
|
+
pointer-events: none;
|
|
607
|
+
transition: opacity 0.3s ease;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.drawer-overlay.open {
|
|
611
|
+
opacity: 1;
|
|
612
|
+
pointer-events: auto;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.drawer {
|
|
616
|
+
position: fixed;
|
|
617
|
+
top: 0;
|
|
618
|
+
right: 0;
|
|
619
|
+
width: 580px;
|
|
620
|
+
height: 100vh;
|
|
621
|
+
background: var(--surface);
|
|
622
|
+
border-left: 1px solid var(--border);
|
|
623
|
+
z-index: 100;
|
|
624
|
+
padding: 2rem;
|
|
625
|
+
display: flex;
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
gap: 1.5rem;
|
|
628
|
+
transform: translateX(100%);
|
|
629
|
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
630
|
+
box-shadow: -10px 0 30px rgba(0,0,0,0.5);
|
|
631
|
+
overflow-y: auto;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.drawer.open {
|
|
635
|
+
transform: translateX(0);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.drawer-header {
|
|
639
|
+
display: flex;
|
|
640
|
+
justify-content: space-between;
|
|
641
|
+
align-items: center;
|
|
642
|
+
border-bottom: 1px solid var(--border);
|
|
643
|
+
padding-bottom: 1.25rem;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.drawer-title h3 {
|
|
647
|
+
font-size: 1.35rem;
|
|
648
|
+
font-weight: 600;
|
|
649
|
+
letter-spacing: -0.02em;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.drawer-close {
|
|
653
|
+
background: var(--surface-hover);
|
|
654
|
+
border: 1px solid var(--border);
|
|
655
|
+
color: var(--text-secondary);
|
|
656
|
+
width: 32px;
|
|
657
|
+
height: 32px;
|
|
658
|
+
border-radius: 50%;
|
|
659
|
+
cursor: pointer;
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
justify-content: center;
|
|
663
|
+
font-size: 1.25rem;
|
|
664
|
+
transition: all 0.15s ease;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.drawer-close:hover {
|
|
668
|
+
background: var(--surface-active);
|
|
669
|
+
color: var(--text);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.drawer-section-title {
|
|
673
|
+
font-size: 0.85rem;
|
|
674
|
+
font-weight: 600;
|
|
675
|
+
text-transform: uppercase;
|
|
676
|
+
letter-spacing: 0.05em;
|
|
677
|
+
color: var(--text-muted);
|
|
678
|
+
margin-bottom: 0.75rem;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.drawer-meta-grid {
|
|
682
|
+
display: grid;
|
|
683
|
+
grid-template-columns: 1fr 1fr;
|
|
684
|
+
gap: 1rem;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.drawer-meta-item {
|
|
688
|
+
background: var(--surface-hover);
|
|
689
|
+
border: 1px solid var(--border);
|
|
690
|
+
padding: 0.75rem 1rem;
|
|
691
|
+
border-radius: 8px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.drawer-meta-label {
|
|
695
|
+
font-size: 0.75rem;
|
|
696
|
+
color: var(--text-muted);
|
|
697
|
+
margin-bottom: 0.25rem;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.drawer-meta-value {
|
|
701
|
+
font-size: 0.9rem;
|
|
702
|
+
font-weight: 500;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.drawer-meta-value a {
|
|
706
|
+
color: var(--blue);
|
|
707
|
+
text-decoration: none;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.drawer-meta-value a:hover {
|
|
711
|
+
text-decoration: underline;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* Size breakdown cards */
|
|
715
|
+
.size-breakdown-grid {
|
|
716
|
+
display: grid;
|
|
717
|
+
grid-template-columns: repeat(3, 1fr);
|
|
718
|
+
gap: 0.75rem;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.size-breakdown-card {
|
|
722
|
+
background: var(--surface-hover);
|
|
723
|
+
border: 1px solid var(--border);
|
|
724
|
+
border-radius: 8px;
|
|
725
|
+
padding: 0.75rem 1rem;
|
|
726
|
+
text-align: center;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.size-value {
|
|
730
|
+
font-size: 1.2rem;
|
|
731
|
+
font-weight: 700;
|
|
732
|
+
color: var(--text);
|
|
733
|
+
margin-top: 0.25rem;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.size-value.exclusive { color: var(--blue); }
|
|
737
|
+
.size-value.shared { color: var(--purple); }
|
|
738
|
+
|
|
739
|
+
/* Chunks list */
|
|
740
|
+
.chunks-list {
|
|
741
|
+
display: flex;
|
|
742
|
+
flex-direction: column;
|
|
743
|
+
gap: 0.5rem;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.chunk-item {
|
|
747
|
+
background: var(--bg);
|
|
748
|
+
border: 1px solid var(--border);
|
|
749
|
+
border-radius: 6px;
|
|
750
|
+
padding: 0.75rem 1rem;
|
|
751
|
+
display: flex;
|
|
752
|
+
justify-content: space-between;
|
|
753
|
+
align-items: center;
|
|
754
|
+
font-size: 0.85rem;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.chunk-name {
|
|
758
|
+
font-family: 'JetBrains Mono', monospace;
|
|
759
|
+
color: var(--text-secondary);
|
|
760
|
+
overflow: hidden;
|
|
761
|
+
text-overflow: ellipsis;
|
|
762
|
+
white-space: nowrap;
|
|
763
|
+
max-width: 70%;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.chunk-size {
|
|
767
|
+
font-weight: 600;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.chunk-badge {
|
|
771
|
+
font-size: 0.7rem;
|
|
772
|
+
padding: 1px 6px;
|
|
773
|
+
border-radius: 4px;
|
|
774
|
+
background: var(--surface-active);
|
|
775
|
+
color: var(--text-secondary);
|
|
776
|
+
margin-left: 8px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.chunk-badge.shared {
|
|
780
|
+
background: var(--purple-alpha);
|
|
781
|
+
color: var(--purple);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.loading-container {
|
|
785
|
+
display: flex;
|
|
786
|
+
align-items: center;
|
|
787
|
+
justify-content: center;
|
|
788
|
+
height: 100vh;
|
|
789
|
+
width: 100%;
|
|
790
|
+
flex-direction: column;
|
|
791
|
+
gap: 1rem;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.spinner {
|
|
795
|
+
width: 40px;
|
|
796
|
+
height: 40px;
|
|
797
|
+
border: 4px solid var(--border);
|
|
798
|
+
border-top-color: var(--blue);
|
|
799
|
+
border-radius: 50%;
|
|
800
|
+
animation: spin 1s infinite linear;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
@keyframes spin {
|
|
804
|
+
to { transform: rotate(360deg); }
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* Views */
|
|
808
|
+
.view {
|
|
809
|
+
display: none;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.view.active {
|
|
813
|
+
display: block;
|
|
814
|
+
}
|
|
815
|
+
</style>
|
|
816
|
+
</head>
|
|
817
|
+
<body>
|
|
818
|
+
|
|
819
|
+
<div id="app-root">
|
|
820
|
+
<div class="loading-container">
|
|
821
|
+
<div class="spinner"></div>
|
|
822
|
+
<p style="color:var(--text-secondary); font-weight:500;">Loading Hydration Audit Report...</p>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
|
|
826
|
+
<script type="module">
|
|
827
|
+
let report = null;
|
|
828
|
+
let activeTab = 'overview';
|
|
829
|
+
|
|
830
|
+
// Filtering and search state
|
|
831
|
+
let searchFilter = '';
|
|
832
|
+
let frameworkFilter = 'all';
|
|
833
|
+
let directiveFilter = 'all';
|
|
834
|
+
let severityFilter = 'all';
|
|
835
|
+
|
|
836
|
+
// Sorting state
|
|
837
|
+
let sortColumn = 'gzip';
|
|
838
|
+
let sortAsc = false;
|
|
839
|
+
|
|
840
|
+
// Detailed inspector state
|
|
841
|
+
let selectedIsland = null;
|
|
842
|
+
|
|
843
|
+
// Load initial report
|
|
844
|
+
async function init() {
|
|
845
|
+
try {
|
|
846
|
+
const res = await fetch('/api/report');
|
|
847
|
+
report = await res.json();
|
|
848
|
+
renderLayout();
|
|
849
|
+
connectWS();
|
|
850
|
+
} catch (e) {
|
|
851
|
+
document.getElementById('app-root').innerHTML = `
|
|
852
|
+
<div class="loading-container">
|
|
853
|
+
<h3 style="color:var(--red)">Failed to load report</h3>
|
|
854
|
+
<p style="color:var(--text-secondary); margin-top:0.5rem">Make sure the report JSON file exists and the dashboard server is active.</p>
|
|
855
|
+
</div>
|
|
856
|
+
`;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Live reload WebSocket client
|
|
861
|
+
function connectWS() {
|
|
862
|
+
try {
|
|
863
|
+
const ws = new WebSocket(`ws://${location.host}`);
|
|
864
|
+
ws.onmessage = (event) => {
|
|
865
|
+
const data = JSON.parse(event.data);
|
|
866
|
+
if (data.type === 'update') {
|
|
867
|
+
report = data.report;
|
|
868
|
+
updateMetricsAndViews();
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
ws.onclose = () => setTimeout(connectWS, 3000);
|
|
872
|
+
} catch {}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Calculate issues counts
|
|
876
|
+
function getAllIssues() {
|
|
877
|
+
if (!report) return [];
|
|
878
|
+
return [
|
|
879
|
+
...(report.islands || []).flatMap(i => i.issues.map(iss => ({ ...iss, source: 'island', component: i.component.name }))),
|
|
880
|
+
...(report.pages || []).flatMap(p => p.issues.map(iss => ({ ...iss, source: 'page', route: p.route }))),
|
|
881
|
+
...(report.issues || []).map(iss => ({ ...iss, source: 'global' }))
|
|
882
|
+
];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function getIssuesCount(severity) {
|
|
886
|
+
return getAllIssues().filter(i => i.severity === severity).length;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Render outer workspace structural shell
|
|
890
|
+
function renderLayout() {
|
|
891
|
+
const container = document.createElement('div');
|
|
892
|
+
container.className = 'container';
|
|
893
|
+
|
|
894
|
+
const totalIssues = getAllIssues().length;
|
|
895
|
+
|
|
896
|
+
container.innerHTML = `
|
|
897
|
+
<div class="sidebar">
|
|
898
|
+
<div class="brand">
|
|
899
|
+
<h1>Hydration Tax</h1>
|
|
900
|
+
<span class="framework-tag">${report.framework}</span>
|
|
901
|
+
</div>
|
|
902
|
+
|
|
903
|
+
<div class="project-meta">
|
|
904
|
+
<div>
|
|
905
|
+
<span>Project</span>
|
|
906
|
+
<strong>${report.projectName}</strong>
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<span>Generated</span>
|
|
910
|
+
<strong>${new Date(report.timestamp).toLocaleTimeString()}</strong>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<div class="nav">
|
|
915
|
+
<button class="nav-item ${activeTab === 'overview' ? 'active' : ''}" data-tab="overview">
|
|
916
|
+
<span>Overview</span>
|
|
917
|
+
</button>
|
|
918
|
+
<button class="nav-item ${activeTab === 'islands' ? 'active' : ''}" data-tab="islands">
|
|
919
|
+
<span>Islands</span>
|
|
920
|
+
<span class="nav-badge" id="badge-islands-count">${report.islands.length}</span>
|
|
921
|
+
</button>
|
|
922
|
+
<button class="nav-item ${activeTab === 'issues' ? 'active' : ''}" data-tab="issues">
|
|
923
|
+
<span>Issues</span>
|
|
924
|
+
<span class="nav-badge" id="badge-issues-count" style="${totalIssues > 0 ? 'background:var(--yellow-alpha);color:var(--yellow);' : ''}">${totalIssues}</span>
|
|
925
|
+
</button>
|
|
926
|
+
</div>
|
|
927
|
+
|
|
928
|
+
<div class="status">
|
|
929
|
+
<span class="status-dot"></span>
|
|
930
|
+
<span>Live reload active</span>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
|
|
934
|
+
<div class="main-content">
|
|
935
|
+
<!-- Overview View -->
|
|
936
|
+
<div class="view ${activeTab === 'overview' ? 'active' : ''}" id="view-overview">
|
|
937
|
+
<div class="header">
|
|
938
|
+
<div class="header-title">
|
|
939
|
+
<h2>Performance Overview</h2>
|
|
940
|
+
<p>Overall hydration sizes and performance health check metrics.</p>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
<div class="stats-grid" id="overview-stats"></div>
|
|
944
|
+
<div class="overview-grid">
|
|
945
|
+
<div class="card">
|
|
946
|
+
<h3>Bundle Size Treemap</h3>
|
|
947
|
+
<div class="treemap-wrapper" id="treemap-container"></div>
|
|
948
|
+
</div>
|
|
949
|
+
<div class="card">
|
|
950
|
+
<h3>Budget Usage Status</h3>
|
|
951
|
+
<div class="budget-list" id="budget-gauges"></div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<!-- Islands Table View -->
|
|
957
|
+
<div class="view ${activeTab === 'islands' ? 'active' : ''}" id="view-islands">
|
|
958
|
+
<div class="header">
|
|
959
|
+
<div class="header-title">
|
|
960
|
+
<h2>Hydrated Islands</h2>
|
|
961
|
+
<p>Explore, search, and sort components loaded for client-side hydration.</p>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
<div class="filter-bar">
|
|
966
|
+
<input type="text" class="search-input" id="search-input" placeholder="Search components..." value="${searchFilter}">
|
|
967
|
+
|
|
968
|
+
<div class="filter-group">
|
|
969
|
+
<span class="filter-label">Framework:</span>
|
|
970
|
+
<button class="filter-pill ${frameworkFilter === 'all' ? 'active' : ''}" data-framework="all">All</button>
|
|
971
|
+
<button class="filter-pill ${frameworkFilter === 'react' ? 'active' : ''}" data-framework="react">React</button>
|
|
972
|
+
<button class="filter-pill ${frameworkFilter === 'svelte' ? 'active' : ''}" data-framework="svelte">Svelte</button>
|
|
973
|
+
<button class="filter-pill ${frameworkFilter === 'vue' ? 'active' : ''}" data-framework="vue">Vue</button>
|
|
974
|
+
</div>
|
|
975
|
+
|
|
976
|
+
<div class="filter-group">
|
|
977
|
+
<span class="filter-label">Directive:</span>
|
|
978
|
+
<button class="filter-pill ${directiveFilter === 'all' ? 'active' : ''}" data-directive="all">All</button>
|
|
979
|
+
<button class="filter-pill ${directiveFilter === 'client:load' ? 'active' : ''}" data-directive="client:load">load</button>
|
|
980
|
+
<button class="filter-pill ${directiveFilter === 'client:visible' ? 'active' : ''}" data-directive="client:visible">visible</button>
|
|
981
|
+
<button class="filter-pill ${directiveFilter === 'client:idle' ? 'active' : ''}" data-directive="client:idle">idle</button>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
|
|
985
|
+
<div class="table-wrapper">
|
|
986
|
+
<table id="islands-table">
|
|
987
|
+
<thead>
|
|
988
|
+
<tr>
|
|
989
|
+
<th data-col="name">Component Name</th>
|
|
990
|
+
<th data-col="directive">Directive</th>
|
|
991
|
+
<th data-col="framework">Framework</th>
|
|
992
|
+
<th data-col="gzip">Gzip Cost</th>
|
|
993
|
+
<th data-col="brotli">Brotli Cost</th>
|
|
994
|
+
<th data-col="issues">Issues</th>
|
|
995
|
+
</tr>
|
|
996
|
+
</thead>
|
|
997
|
+
<tbody id="islands-table-body"></tbody>
|
|
998
|
+
</table>
|
|
999
|
+
</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
|
|
1002
|
+
<!-- Issues Warnings View -->
|
|
1003
|
+
<div class="view ${activeTab === 'issues' ? 'active' : ''}" id="view-issues">
|
|
1004
|
+
<div class="header">
|
|
1005
|
+
<div class="header-title">
|
|
1006
|
+
<h2>Hydration Diagnostics</h2>
|
|
1007
|
+
<p>Issues, warnings, and optimization recommendations flagged for your code.</p>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
|
|
1011
|
+
<div class="filter-bar">
|
|
1012
|
+
<div class="filter-group">
|
|
1013
|
+
<span class="filter-label">Severity:</span>
|
|
1014
|
+
<button class="filter-pill ${severityFilter === 'all' ? 'active' : ''}" data-severity="all">All</button>
|
|
1015
|
+
<button class="filter-pill ${severityFilter === 'error' ? 'active' : ''}" data-severity="error">Errors (${getIssuesCount('error')})</button>
|
|
1016
|
+
<button class="filter-pill ${severityFilter === 'warning' ? 'active' : ''}" data-severity="warning">Warnings (${getIssuesCount('warning')})</button>
|
|
1017
|
+
<button class="filter-pill ${severityFilter === 'info' ? 'active' : ''}" data-severity="info">Info (${getIssuesCount('info')})</button>
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
|
|
1021
|
+
<div class="issues-list" id="issues-list-container"></div>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
|
|
1025
|
+
<!-- Detail Inspector Slide-out Drawer -->
|
|
1026
|
+
<div class="drawer-overlay" id="drawer-overlay"></div>
|
|
1027
|
+
<div class="drawer" id="detail-drawer">
|
|
1028
|
+
<div class="drawer-header">
|
|
1029
|
+
<div class="drawer-title" id="drawer-title-container">
|
|
1030
|
+
<h3>Island Details</h3>
|
|
1031
|
+
</div>
|
|
1032
|
+
<button class="drawer-close" id="drawer-close-btn">×</button>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div id="drawer-content-container"></div>
|
|
1035
|
+
</div>
|
|
1036
|
+
`;
|
|
1037
|
+
|
|
1038
|
+
document.getElementById('app-root').replaceChildren(container);
|
|
1039
|
+
|
|
1040
|
+
// Wire sidebar tabs switching
|
|
1041
|
+
document.querySelectorAll('.sidebar .nav-item').forEach(btn => {
|
|
1042
|
+
btn.addEventListener('click', () => {
|
|
1043
|
+
activeTab = btn.dataset.tab;
|
|
1044
|
+
document.querySelectorAll('.sidebar .nav-item').forEach(b => b.classList.remove('active'));
|
|
1045
|
+
btn.classList.add('active');
|
|
1046
|
+
|
|
1047
|
+
document.querySelectorAll('.main-content .view').forEach(view => view.classList.remove('active'));
|
|
1048
|
+
document.getElementById(`view-${activeTab}`).classList.add('active');
|
|
1049
|
+
|
|
1050
|
+
// Re-trigger layout sizing on tab changes (specifically for Treemap resizing)
|
|
1051
|
+
if (activeTab === 'overview') {
|
|
1052
|
+
setTimeout(renderTreemap, 50);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Wire search and table filter events
|
|
1058
|
+
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
1059
|
+
searchFilter = e.target.value;
|
|
1060
|
+
renderIslandsTable();
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
document.querySelectorAll('[data-framework]').forEach(btn => {
|
|
1064
|
+
btn.addEventListener('click', () => {
|
|
1065
|
+
frameworkFilter = btn.dataset.framework;
|
|
1066
|
+
document.querySelectorAll('[data-framework]').forEach(b => b.classList.remove('active'));
|
|
1067
|
+
btn.classList.add('active');
|
|
1068
|
+
renderIslandsTable();
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
document.querySelectorAll('[data-directive]').forEach(btn => {
|
|
1073
|
+
btn.addEventListener('click', () => {
|
|
1074
|
+
directiveFilter = btn.dataset.directive;
|
|
1075
|
+
document.querySelectorAll('[data-directive]').forEach(b => b.classList.remove('active'));
|
|
1076
|
+
btn.classList.add('active');
|
|
1077
|
+
renderIslandsTable();
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
document.querySelectorAll('[data-severity]').forEach(btn => {
|
|
1082
|
+
btn.addEventListener('click', () => {
|
|
1083
|
+
severityFilter = btn.dataset.severity;
|
|
1084
|
+
document.querySelectorAll('[data-severity]').forEach(b => b.classList.remove('active'));
|
|
1085
|
+
btn.classList.add('active');
|
|
1086
|
+
renderIssuesList();
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// Sort handlers
|
|
1091
|
+
document.querySelectorAll('#islands-table th').forEach(th => {
|
|
1092
|
+
th.addEventListener('click', () => {
|
|
1093
|
+
const col = th.dataset.col;
|
|
1094
|
+
if (sortColumn === col) {
|
|
1095
|
+
sortAsc = !sortAsc;
|
|
1096
|
+
} else {
|
|
1097
|
+
sortColumn = col;
|
|
1098
|
+
sortAsc = false;
|
|
1099
|
+
}
|
|
1100
|
+
renderIslandsTable();
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// Close drawer handler
|
|
1105
|
+
document.getElementById('drawer-close-btn').addEventListener('click', closeInspector);
|
|
1106
|
+
document.getElementById('drawer-overlay').addEventListener('click', closeInspector);
|
|
1107
|
+
|
|
1108
|
+
// Populate views initially
|
|
1109
|
+
updateMetricsAndViews();
|
|
1110
|
+
|
|
1111
|
+
// Listen to resize events for treemap squarify calculations
|
|
1112
|
+
window.addEventListener('resize', () => {
|
|
1113
|
+
if (activeTab === 'overview') renderTreemap();
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Sync metrics and populate views on initial load or WebSocket update
|
|
1118
|
+
function updateMetricsAndViews() {
|
|
1119
|
+
if (!report) return;
|
|
1120
|
+
|
|
1121
|
+
// Update sidebar tab counts
|
|
1122
|
+
document.getElementById('badge-islands-count').textContent = report.islands.length;
|
|
1123
|
+
|
|
1124
|
+
const totalIssues = getAllIssues().length;
|
|
1125
|
+
const issuesBadge = document.getElementById('badge-issues-count');
|
|
1126
|
+
issuesBadge.textContent = totalIssues;
|
|
1127
|
+
if (totalIssues > 0) {
|
|
1128
|
+
issuesBadge.style.background = 'var(--yellow-alpha)';
|
|
1129
|
+
issuesBadge.style.color = 'var(--yellow)';
|
|
1130
|
+
} else {
|
|
1131
|
+
issuesBadge.style.background = 'var(--surface-hover)';
|
|
1132
|
+
issuesBadge.style.color = 'var(--text-secondary)';
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Re-populate sections
|
|
1136
|
+
renderOverviewStats();
|
|
1137
|
+
renderTreemap();
|
|
1138
|
+
renderBudgetGauges();
|
|
1139
|
+
renderIslandsTable();
|
|
1140
|
+
renderIssuesList();
|
|
1141
|
+
|
|
1142
|
+
// Update drawer if opened
|
|
1143
|
+
if (selectedIsland) {
|
|
1144
|
+
const freshData = report.islands.find(i => i.component.name === selectedIsland.component.name);
|
|
1145
|
+
if (freshData) {
|
|
1146
|
+
showInspector(freshData);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Populate the Overview Metrics cards
|
|
1152
|
+
function renderOverviewStats() {
|
|
1153
|
+
const statsGrid = document.getElementById('overview-stats');
|
|
1154
|
+
const t = report.totals;
|
|
1155
|
+
|
|
1156
|
+
const sizeClass = t.totalGzipSize > 300000 ? 'red' : t.totalGzipSize > 150000 ? 'yellow' : 'green';
|
|
1157
|
+
const issueClass = t.issuesBySeverity.error > 0 ? 'red' : t.issuesBySeverity.warning > 0 ? 'yellow' : 'green';
|
|
1158
|
+
|
|
1159
|
+
statsGrid.innerHTML = `
|
|
1160
|
+
<div class="stat-card">
|
|
1161
|
+
<div class="stat-label">Total Islands</div>
|
|
1162
|
+
<div class="stat-value">${t.totalIslands}</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div class="stat-card">
|
|
1165
|
+
<div class="stat-label">Total JS (gzip)</div>
|
|
1166
|
+
<div class="stat-value ${sizeClass}">${formatBytes(t.totalGzipSize)}</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="stat-card">
|
|
1169
|
+
<div class="stat-label">Total JS (brotli)</div>
|
|
1170
|
+
<div class="stat-value">${formatBytes(t.totalBrotliSize)}</div>
|
|
1171
|
+
</div>
|
|
1172
|
+
<div class="stat-card">
|
|
1173
|
+
<div class="stat-label">Total Issues</div>
|
|
1174
|
+
<div class="stat-value ${issueClass}">${t.totalIssues}</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
`;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Populate Overview Budget Gauges
|
|
1180
|
+
function renderBudgetGauges() {
|
|
1181
|
+
const budgetGauges = document.getElementById('budget-gauges');
|
|
1182
|
+
const config = report.config;
|
|
1183
|
+
if (!config) {
|
|
1184
|
+
budgetGauges.innerHTML = `<p style="color:var(--text-muted)">No configuration thresholds defined.</p>`;
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const gauges = [];
|
|
1189
|
+
|
|
1190
|
+
// Per-page budgets
|
|
1191
|
+
for (const page of report.pages) {
|
|
1192
|
+
const limit = config.thresholds.pageBudget;
|
|
1193
|
+
const pct = Math.min((page.totalGzipSize / limit) * 100, 100);
|
|
1194
|
+
const color = pct > 100 ? 'var(--red)' : pct > 66 ? 'var(--yellow)' : 'var(--green)';
|
|
1195
|
+
|
|
1196
|
+
gauges.push(`
|
|
1197
|
+
<div class="budget-item">
|
|
1198
|
+
<div class="budget-meta">
|
|
1199
|
+
<span class="budget-name">${page.route}</span>
|
|
1200
|
+
<span class="budget-sizes"><strong>${formatBytes(page.totalGzipSize)}</strong> / ${formatBytes(limit)}</span>
|
|
1201
|
+
</div>
|
|
1202
|
+
<div class="gauge-bar">
|
|
1203
|
+
<div class="gauge-fill" style="width: ${pct}%; background: ${color};"></div>
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
`);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Total budget
|
|
1210
|
+
const totalLimit = config.thresholds.totalBudget;
|
|
1211
|
+
const totalPct = Math.min((report.totals.totalGzipSize / totalLimit) * 100, 100);
|
|
1212
|
+
const totalColor = totalPct > 100 ? 'var(--red)' : totalPct > 66 ? 'var(--yellow)' : 'var(--green)';
|
|
1213
|
+
|
|
1214
|
+
gauges.push(`
|
|
1215
|
+
<div class="budget-item" style="border-top:1px solid var(--border); padding-top:1.25rem; margin-top:0.75rem;">
|
|
1216
|
+
<div class="budget-meta">
|
|
1217
|
+
<span class="budget-name" style="font-weight:600; color:var(--text)">Total Site Budget</span>
|
|
1218
|
+
<span class="budget-sizes"><strong>${formatBytes(report.totals.totalGzipSize)}</strong> / ${formatBytes(totalLimit)}</span>
|
|
1219
|
+
</div>
|
|
1220
|
+
<div class="gauge-bar" style="height:12px;">
|
|
1221
|
+
<div class="gauge-fill" style="width: ${totalPct}%; background: ${totalColor};"></div>
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
`);
|
|
1225
|
+
|
|
1226
|
+
budgetGauges.innerHTML = gauges.join('');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Populate Overview Treemap
|
|
1230
|
+
function renderTreemap() {
|
|
1231
|
+
const container = document.getElementById('treemap-container');
|
|
1232
|
+
if (!container || !report || !report.islands.length) return;
|
|
1233
|
+
|
|
1234
|
+
const width = container.offsetWidth;
|
|
1235
|
+
const height = container.offsetHeight;
|
|
1236
|
+
|
|
1237
|
+
// Map components to value metrics
|
|
1238
|
+
const items = report.islands.map(i => ({
|
|
1239
|
+
name: i.component.name,
|
|
1240
|
+
value: Math.max(i.bundle.totalGzipSize, 100), // Minimum weight for rendering small elements
|
|
1241
|
+
gzip: i.bundle.totalGzipSize,
|
|
1242
|
+
issues: i.issues.length,
|
|
1243
|
+
rawRecord: i
|
|
1244
|
+
}));
|
|
1245
|
+
|
|
1246
|
+
// Squarified treemap calculation
|
|
1247
|
+
const nodes = squarify(items, 0, 0, width, height);
|
|
1248
|
+
|
|
1249
|
+
container.innerHTML = nodes.map(n => {
|
|
1250
|
+
const color = getNodeColor(n);
|
|
1251
|
+
return `
|
|
1252
|
+
<div class="treemap-node"
|
|
1253
|
+
style="left:${n.x}px; top:${n.y}px; width:${n.w}px; height:${n.h}px; background:${color}"
|
|
1254
|
+
title="${n.name}: ${formatBytes(n.gzip)} (${n.issues} issue${n.issues !== 1 ? 's' : ''})"
|
|
1255
|
+
data-treemap-item="${n.name}">
|
|
1256
|
+
${n.w > 65 && n.h > 35 ? `<div class="treemap-node-name">${n.name}</div>` : ''}
|
|
1257
|
+
${n.w > 55 && n.h > 50 ? `<div class="treemap-node-size">${formatBytes(n.gzip)}</div>` : ''}
|
|
1258
|
+
</div>
|
|
1259
|
+
`;
|
|
1260
|
+
}).join('');
|
|
1261
|
+
|
|
1262
|
+
// Click handler on nodes
|
|
1263
|
+
container.querySelectorAll('.treemap-node').forEach(node => {
|
|
1264
|
+
node.addEventListener('click', () => {
|
|
1265
|
+
const componentName = node.dataset.treemapItem;
|
|
1266
|
+
const island = report.islands.find(i => i.component.name === componentName);
|
|
1267
|
+
if (island) showInspector(island);
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Treemap node color categorization based on issues and size weight
|
|
1273
|
+
function getNodeColor(node) {
|
|
1274
|
+
if (node.issues > 0) {
|
|
1275
|
+
return node.gzip > 80000 ? 'rgba(239, 68, 68, 0.7)' : 'rgba(245, 158, 11, 0.6)';
|
|
1276
|
+
}
|
|
1277
|
+
if (node.gzip > 50000) return 'rgba(245, 158, 11, 0.45)';
|
|
1278
|
+
if (node.gzip > 15000) return 'rgba(59, 130, 246, 0.45)';
|
|
1279
|
+
return 'rgba(16, 185, 129, 0.4)';
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Squarified treemap layout algorithm
|
|
1283
|
+
function squarify(items, x, y, w, h) {
|
|
1284
|
+
const total = items.reduce((s, i) => s + i.value, 0);
|
|
1285
|
+
if (total === 0 || items.length === 0) return [];
|
|
1286
|
+
|
|
1287
|
+
const sorted = [...items].sort((a, b) => b.value - a.value);
|
|
1288
|
+
const result = [];
|
|
1289
|
+
let cx = x, cy = y, cw = w, ch = h;
|
|
1290
|
+
|
|
1291
|
+
for (const item of sorted) {
|
|
1292
|
+
const ratio = item.value / total;
|
|
1293
|
+
const isWide = cw >= ch;
|
|
1294
|
+
|
|
1295
|
+
let nw, nh;
|
|
1296
|
+
if (isWide) {
|
|
1297
|
+
nw = cw * ratio;
|
|
1298
|
+
nh = ch;
|
|
1299
|
+
result.push({ ...item, x: cx, y: cy, w: Math.max(nw - 2, 0), h: Math.max(nh - 2, 0) });
|
|
1300
|
+
cx += nw;
|
|
1301
|
+
cw -= nw;
|
|
1302
|
+
} else {
|
|
1303
|
+
nw = cw;
|
|
1304
|
+
nh = ch * ratio;
|
|
1305
|
+
result.push({ ...item, x: cx, y: cy, w: Math.max(nw - 2, 0), h: Math.max(nh - 2, 0) });
|
|
1306
|
+
cy += nh;
|
|
1307
|
+
ch -= nh;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Sort and filter Islands grid/table
|
|
1315
|
+
function getSortedAndFilteredIslands() {
|
|
1316
|
+
let list = [...(report.islands || [])];
|
|
1317
|
+
|
|
1318
|
+
// Search query filter
|
|
1319
|
+
if (searchFilter.trim() !== '') {
|
|
1320
|
+
const search = searchFilter.toLowerCase();
|
|
1321
|
+
list = list.filter(i => i.component.name.toLowerCase().includes(search));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Framework filter
|
|
1325
|
+
if (frameworkFilter !== 'all') {
|
|
1326
|
+
list = list.filter(i => i.component.uiFramework.toLowerCase() === frameworkFilter);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Directive filter
|
|
1330
|
+
if (directiveFilter !== 'all') {
|
|
1331
|
+
list = list.filter(i => i.component.directive === directiveFilter);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Sorting
|
|
1335
|
+
list.sort((a, b) => {
|
|
1336
|
+
let va, vb;
|
|
1337
|
+
switch (sortColumn) {
|
|
1338
|
+
case 'name': va = a.component.name; vb = b.component.name; break;
|
|
1339
|
+
case 'directive': va = a.component.directive; vb = b.component.directive; break;
|
|
1340
|
+
case 'framework': va = a.component.uiFramework; vb = b.component.uiFramework; break;
|
|
1341
|
+
case 'gzip': va = a.bundle.totalGzipSize; vb = b.bundle.totalGzipSize; break;
|
|
1342
|
+
case 'brotli': va = a.bundle.totalBrotliSize; vb = b.bundle.totalBrotliSize; break;
|
|
1343
|
+
case 'issues': va = a.issues.length; vb = b.issues.length; break;
|
|
1344
|
+
default: va = a.bundle.totalGzipSize; vb = b.bundle.totalGzipSize;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (typeof va === 'string') {
|
|
1348
|
+
return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
1349
|
+
}
|
|
1350
|
+
return sortAsc ? va - vb : vb - va;
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
return list;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Render sorted/filtered table body rows
|
|
1357
|
+
function renderIslandsTable() {
|
|
1358
|
+
const tbody = document.getElementById('islands-table-body');
|
|
1359
|
+
const list = getSortedAndFilteredIslands();
|
|
1360
|
+
|
|
1361
|
+
if (list.length === 0) {
|
|
1362
|
+
tbody.innerHTML = `
|
|
1363
|
+
<tr>
|
|
1364
|
+
<td colspan="6" style="text-align:center; color:var(--text-muted); padding:3rem 0;">
|
|
1365
|
+
No matching hydrated islands found.
|
|
1366
|
+
</td>
|
|
1367
|
+
</tr>
|
|
1368
|
+
`;
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
tbody.innerHTML = list.map(i => {
|
|
1373
|
+
const directiveName = i.component.directive.split(':')[1] || 'load';
|
|
1374
|
+
const issuesClass = i.issues.length > 0 ? (i.issues.some(iss => iss.severity === 'error') ? 'error' : 'warn') : 'zero';
|
|
1375
|
+
|
|
1376
|
+
return `
|
|
1377
|
+
<tr class="clickable-row" data-component-row="${i.component.name}">
|
|
1378
|
+
<td><strong style="color:var(--text);">${i.component.name}</strong></td>
|
|
1379
|
+
<td><span class="directive-tag ${directiveName}">${i.component.directive}</span></td>
|
|
1380
|
+
<td><span class="framework-badge ${i.component.uiFramework}">${i.component.uiFramework}</span></td>
|
|
1381
|
+
<td style="font-weight:500;">${formatBytes(i.bundle.totalGzipSize)}</td>
|
|
1382
|
+
<td style="color:var(--text-secondary);">${formatBytes(i.bundle.totalBrotliSize)}</td>
|
|
1383
|
+
<td><span class="issues-count ${issuesClass}">${i.issues.length}</span></td>
|
|
1384
|
+
</tr>
|
|
1385
|
+
`;
|
|
1386
|
+
}).join('');
|
|
1387
|
+
|
|
1388
|
+
// Wire row clicks to Inspector Drawer
|
|
1389
|
+
tbody.querySelectorAll('.clickable-row').forEach(row => {
|
|
1390
|
+
row.addEventListener('click', () => {
|
|
1391
|
+
const componentName = row.dataset.componentRow;
|
|
1392
|
+
const island = report.islands.find(i => i.component.name === componentName);
|
|
1393
|
+
if (island) showInspector(island);
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Render Diagnostics/Issues list
|
|
1399
|
+
function renderIssuesList() {
|
|
1400
|
+
const container = document.getElementById('issues-list-container');
|
|
1401
|
+
let issues = getAllIssues();
|
|
1402
|
+
|
|
1403
|
+
// Severity filters
|
|
1404
|
+
if (severityFilter !== 'all') {
|
|
1405
|
+
issues = issues.filter(i => i.severity === severityFilter);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (issues.length === 0) {
|
|
1409
|
+
container.innerHTML = `
|
|
1410
|
+
<div style="text-align:center; color:var(--text-muted); padding:4rem 0;">
|
|
1411
|
+
No hydration diagnostics or warnings flagged.
|
|
1412
|
+
</div>
|
|
1413
|
+
`;
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
container.innerHTML = issues.map(iss => {
|
|
1418
|
+
const contextMeta = iss.source === 'island'
|
|
1419
|
+
? `Island · <span class="issue-component" data-inspect-comp="${iss.component}">${iss.component}</span>`
|
|
1420
|
+
: iss.source === 'page'
|
|
1421
|
+
? `Page · <strong>${iss.route}</strong>`
|
|
1422
|
+
: `Global Metric`;
|
|
1423
|
+
|
|
1424
|
+
return `
|
|
1425
|
+
<div class="issue-card ${iss.severity}">
|
|
1426
|
+
<div class="issue-meta">
|
|
1427
|
+
<span>${contextMeta}</span>
|
|
1428
|
+
<span class="issue-severity ${iss.severity}">${iss.severity}</span>
|
|
1429
|
+
</div>
|
|
1430
|
+
<div class="issue-message">${iss.message}</div>
|
|
1431
|
+
${iss.recommendation ? `<div class="issue-recommendation">${iss.recommendation}</div>` : ''}
|
|
1432
|
+
</div>
|
|
1433
|
+
`;
|
|
1434
|
+
}).join('');
|
|
1435
|
+
|
|
1436
|
+
// Click handlers on issue component links to trigger Inspector Drawer
|
|
1437
|
+
container.querySelectorAll('.issue-component').forEach(link => {
|
|
1438
|
+
link.addEventListener('click', () => {
|
|
1439
|
+
const comp = link.dataset.inspectComp;
|
|
1440
|
+
const island = report.islands.find(i => i.component.name === comp);
|
|
1441
|
+
if (island) showInspector(island);
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Inspector Slide-out Drawer Actions
|
|
1447
|
+
function showInspector(island) {
|
|
1448
|
+
selectedIsland = island;
|
|
1449
|
+
const overlay = document.getElementById('drawer-overlay');
|
|
1450
|
+
const drawer = document.getElementById('detail-drawer');
|
|
1451
|
+
const titleContainer = document.getElementById('drawer-title-container');
|
|
1452
|
+
const contentContainer = document.getElementById('drawer-content-container');
|
|
1453
|
+
|
|
1454
|
+
// Update header
|
|
1455
|
+
titleContainer.innerHTML = `
|
|
1456
|
+
<h3 style="font-size:1.3rem;">${island.component.name}</h3>
|
|
1457
|
+
<span class="framework-tag" style="margin-top:0.35rem;">${island.component.uiFramework}</span>
|
|
1458
|
+
`;
|
|
1459
|
+
|
|
1460
|
+
// Build issues breakdown HTML
|
|
1461
|
+
let issuesHtml = '';
|
|
1462
|
+
if (island.issues.length > 0) {
|
|
1463
|
+
issuesHtml = `
|
|
1464
|
+
<div style="margin-bottom:1.5rem;">
|
|
1465
|
+
<div class="drawer-section-title">Diagnostics (${island.issues.length})</div>
|
|
1466
|
+
<div style="display:flex; flex-direction:column; gap:0.75rem;">
|
|
1467
|
+
${island.issues.map(iss => `
|
|
1468
|
+
<div class="issue-card ${iss.severity}" style="padding:1rem; font-size:0.85rem;">
|
|
1469
|
+
<div class="issue-meta">
|
|
1470
|
+
<span class="issue-severity ${iss.severity}">${iss.severity}</span>
|
|
1471
|
+
</div>
|
|
1472
|
+
<div class="issue-message" style="margin:2px 0 6px 0;">${iss.message}</div>
|
|
1473
|
+
${iss.recommendation ? `<div class="issue-recommendation">${iss.recommendation}</div>` : ''}
|
|
1474
|
+
</div>
|
|
1475
|
+
`).join('')}
|
|
1476
|
+
</div>
|
|
1477
|
+
</div>
|
|
1478
|
+
`;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Build chunks lists
|
|
1482
|
+
const exclusiveChunks = island.bundle.chunks.filter(c => !c.isShared);
|
|
1483
|
+
const sharedChunks = island.bundle.sharedChunks;
|
|
1484
|
+
|
|
1485
|
+
let chunksHtml = '';
|
|
1486
|
+
if (exclusiveChunks.length > 0 || sharedChunks.length > 0) {
|
|
1487
|
+
chunksHtml = `
|
|
1488
|
+
<div style="margin-bottom:1.5rem;">
|
|
1489
|
+
<div class="drawer-section-title">JS Bundle Chunks</div>
|
|
1490
|
+
<div class="chunks-list">
|
|
1491
|
+
${exclusiveChunks.map(c => `
|
|
1492
|
+
<div class="chunk-item">
|
|
1493
|
+
<span class="chunk-name" title="${c.filePath}">${pathBasename(c.filePath)}</span>
|
|
1494
|
+
<span class="chunk-size">${formatBytes(c.gzipSize)} <span class="chunk-badge">exclusive</span></span>
|
|
1495
|
+
</div>
|
|
1496
|
+
`).join('')}
|
|
1497
|
+
${sharedChunks.map(sc => `
|
|
1498
|
+
<div class="chunk-item">
|
|
1499
|
+
<span class="chunk-name" title="${sc.chunk.filePath}">${pathBasename(sc.chunk.filePath)}</span>
|
|
1500
|
+
<span class="chunk-size">
|
|
1501
|
+
${formatBytes(sc.chunk.gzipSize)}
|
|
1502
|
+
<span class="chunk-badge shared">shared (1/${sc.sharedBy})</span>
|
|
1503
|
+
</span>
|
|
1504
|
+
</div>
|
|
1505
|
+
`).join('')}
|
|
1506
|
+
</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
`;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Parse route details
|
|
1512
|
+
const pagesHtml = island.component.pages.map(p => `
|
|
1513
|
+
<span style="font-size:0.8rem; background:var(--surface-hover); border:1px solid var(--border); padding:2px 8px; border-radius:4px; margin-right:4px; display:inline-block; margin-top:4px;">
|
|
1514
|
+
${p}
|
|
1515
|
+
</span>
|
|
1516
|
+
`).join('');
|
|
1517
|
+
|
|
1518
|
+
// Build content body
|
|
1519
|
+
contentContainer.innerHTML = `
|
|
1520
|
+
<!-- Metadata -->
|
|
1521
|
+
<div style="margin-bottom:1.5rem;">
|
|
1522
|
+
<div class="drawer-section-title">Component Definition</div>
|
|
1523
|
+
<div class="drawer-meta-grid">
|
|
1524
|
+
<div class="drawer-meta-item" style="grid-column: 1 / -1;">
|
|
1525
|
+
<div class="drawer-meta-label">Source File Location</div>
|
|
1526
|
+
<div class="drawer-meta-value" style="font-size:0.8rem; font-family:'JetBrains Mono', monospace; word-break:break-all;">
|
|
1527
|
+
<a href="file://${island.component.sourceFile}" title="Click to view file">${island.component.sourceFile}</a>
|
|
1528
|
+
${island.component.sourceLine ? `<span style="color:var(--text-muted)">:L${island.component.sourceLine}</span>` : ''}
|
|
1529
|
+
</div>
|
|
1530
|
+
</div>
|
|
1531
|
+
<div class="drawer-meta-item">
|
|
1532
|
+
<div class="drawer-meta-label">Hydration Directive</div>
|
|
1533
|
+
<div class="drawer-meta-value">
|
|
1534
|
+
<span class="directive-tag ${island.component.directive.split(':')[1]}">${island.component.directive}</span>
|
|
1535
|
+
${island.component.directiveArg ? `<span style="font-size:0.75rem; color:var(--text-secondary)">=${island.component.directiveArg}</span>` : ''}
|
|
1536
|
+
</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
<div class="drawer-meta-item">
|
|
1539
|
+
<div class="drawer-meta-label">Meta Framework</div>
|
|
1540
|
+
<div class="drawer-meta-value" style="text-transform:uppercase; font-size:0.8rem; font-weight:600;">
|
|
1541
|
+
${island.component.metaFramework}
|
|
1542
|
+
</div>
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
|
|
1547
|
+
<!-- Size breakdown -->
|
|
1548
|
+
<div style="margin-bottom:1.5rem;">
|
|
1549
|
+
<div class="drawer-section-title">Size Breakdown (Gzip)</div>
|
|
1550
|
+
<div class="size-breakdown-grid">
|
|
1551
|
+
<div class="size-breakdown-card">
|
|
1552
|
+
<div class="drawer-meta-label">Exclusive</div>
|
|
1553
|
+
<div class="size-value exclusive">${formatBytes(island.bundle.exclusiveGzipSize)}</div>
|
|
1554
|
+
</div>
|
|
1555
|
+
<div class="size-breakdown-card">
|
|
1556
|
+
<div class="drawer-meta-label">Shared (Attribution)</div>
|
|
1557
|
+
<div class="size-value shared">${formatBytes(island.bundle.sharedGzipSize)}</div>
|
|
1558
|
+
</div>
|
|
1559
|
+
<div class="size-breakdown-card">
|
|
1560
|
+
<div class="drawer-meta-label">Total Cost</div>
|
|
1561
|
+
<div class="size-value" style="font-weight:800;">${formatBytes(island.bundle.totalGzipSize)}</div>
|
|
1562
|
+
</div>
|
|
1563
|
+
</div>
|
|
1564
|
+
</div>
|
|
1565
|
+
|
|
1566
|
+
<!-- Pages list -->
|
|
1567
|
+
<div style="margin-bottom:1.5rem;">
|
|
1568
|
+
<div class="drawer-section-title">Pages Residing On</div>
|
|
1569
|
+
<div>${pagesHtml}</div>
|
|
1570
|
+
</div>
|
|
1571
|
+
|
|
1572
|
+
<!-- Chunks list section -->
|
|
1573
|
+
${chunksHtml}
|
|
1574
|
+
|
|
1575
|
+
<!-- Issues Warnings list -->
|
|
1576
|
+
${issuesHtml}
|
|
1577
|
+
`;
|
|
1578
|
+
|
|
1579
|
+
// Show overlay and slide in drawer
|
|
1580
|
+
overlay.classList.add('open');
|
|
1581
|
+
drawer.classList.add('open');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function closeInspector() {
|
|
1585
|
+
selectedIsland = null;
|
|
1586
|
+
document.getElementById('drawer-overlay').classList.remove('open');
|
|
1587
|
+
document.getElementById('detail-drawer').classList.remove('open');
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Utility helpers
|
|
1591
|
+
function formatBytes(bytes) {
|
|
1592
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
1593
|
+
if (bytes < 1024) return bytes + ' B';
|
|
1594
|
+
const kb = bytes / 1024;
|
|
1595
|
+
if (kb < 1024) return kb.toFixed(1) + ' KB';
|
|
1596
|
+
return (kb / 1024).toFixed(2) + ' MB';
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function pathBasename(filePath) {
|
|
1600
|
+
return filePath.split(/[\\/]/).pop() || filePath;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Launch UI init
|
|
1604
|
+
init();
|
|
1605
|
+
</script>
|
|
1606
|
+
</body>
|
|
1607
|
+
</html>
|