@hydration-audit/dashboard 0.2.2 → 0.2.4
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.html +1400 -232
- package/package.json +2 -2
package/dist/index.html
CHANGED
|
@@ -4,19 +4,31 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
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">
|
|
7
10
|
<style>
|
|
8
11
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--surface: #
|
|
11
|
-
--surface-
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
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;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -25,119 +37,827 @@
|
|
|
25
37
|
background: var(--bg);
|
|
26
38
|
color: var(--text);
|
|
27
39
|
line-height: 1.5;
|
|
40
|
+
overflow-x: hidden;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
33
115
|
display: flex;
|
|
34
116
|
align-items: center;
|
|
35
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%;
|
|
36
130
|
}
|
|
37
|
-
.header h1 { font-size: 1.25rem; font-weight: 600; }
|
|
38
|
-
.header .meta { color: var(--text-dim); font-size: 0.85rem; }
|
|
39
|
-
.live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-right: 6px; animation: pulse 2s infinite; }
|
|
40
|
-
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
41
131
|
|
|
42
|
-
.
|
|
43
|
-
|
|
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 {
|
|
44
218
|
background: var(--surface);
|
|
45
219
|
border: 1px solid var(--border);
|
|
46
220
|
border-radius: 12px;
|
|
47
221
|
padding: 1.25rem;
|
|
222
|
+
transition: border-color 0.2s ease;
|
|
48
223
|
}
|
|
49
|
-
.card h2 { font-size: 0.9rem; font-weight: 500; color: var(--text-dim); margin-bottom: 1rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
50
|
-
.card.full { grid-column: 1 / -1; }
|
|
51
224
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 {
|
|
55
250
|
background: var(--surface);
|
|
56
251
|
border: 1px solid var(--border);
|
|
57
252
|
border-radius: 12px;
|
|
58
|
-
padding:
|
|
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;
|
|
59
265
|
}
|
|
60
|
-
.stat-card .label { font-size: 0.8rem; color: var(--text-dim); }
|
|
61
|
-
.stat-card .value { font-size: 1.75rem; font-weight: 700; margin-top: 0.25rem; }
|
|
62
|
-
.stat-card .value.green { color: var(--green); }
|
|
63
|
-
.stat-card .value.yellow { color: var(--yellow); }
|
|
64
|
-
.stat-card .value.red { color: var(--red); }
|
|
65
266
|
|
|
66
267
|
/* Treemap */
|
|
67
|
-
.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
|
+
|
|
68
278
|
.treemap-node {
|
|
69
279
|
position: absolute;
|
|
70
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;
|
|
71
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 {
|
|
72
532
|
display: flex;
|
|
73
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;
|
|
74
661
|
align-items: center;
|
|
75
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 {
|
|
76
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;
|
|
77
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);
|
|
78
760
|
overflow: hidden;
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
792
|
}
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
.issue-info { background: rgba(96, 165, 250, 0.1); border-left: 3px solid var(--blue); }
|
|
104
|
-
.issue .rec { color: var(--text-dim); font-size: 0.8rem; margin-top: 0.25rem; }
|
|
105
|
-
|
|
106
|
-
/* Budget Gauge */
|
|
107
|
-
.gauge { position: relative; height: 24px; background: var(--surface-2); border-radius: 12px; overflow: hidden; margin-top: 0.5rem; }
|
|
108
|
-
.gauge-fill { height: 100%; border-radius: 12px; transition: width 0.5s ease; }
|
|
109
|
-
.gauge-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.75rem; font-weight: 600; }
|
|
110
|
-
|
|
111
|
-
.loading { text-align: center; padding: 4rem; color: var(--text-dim); }
|
|
112
|
-
|
|
113
|
-
@media (max-width: 768px) {
|
|
114
|
-
.dashboard { grid-template-columns: 1fr; }
|
|
115
|
-
.stats { grid-template-columns: repeat(2, 1fr); }
|
|
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;
|
|
116
814
|
}
|
|
117
815
|
</style>
|
|
118
816
|
</head>
|
|
119
817
|
<body>
|
|
120
|
-
|
|
121
|
-
|
|
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>
|
|
122
824
|
</div>
|
|
123
825
|
|
|
124
826
|
<script type="module">
|
|
125
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
|
|
126
837
|
let sortColumn = 'gzip';
|
|
127
838
|
let sortAsc = false;
|
|
128
839
|
|
|
129
|
-
//
|
|
130
|
-
|
|
840
|
+
// Detailed inspector state
|
|
841
|
+
let selectedIsland = null;
|
|
842
|
+
|
|
843
|
+
// Load initial report
|
|
844
|
+
async function init() {
|
|
131
845
|
try {
|
|
132
846
|
const res = await fetch('/api/report');
|
|
133
847
|
report = await res.json();
|
|
134
|
-
|
|
848
|
+
renderLayout();
|
|
849
|
+
connectWS();
|
|
135
850
|
} catch (e) {
|
|
136
|
-
document.getElementById('app').innerHTML =
|
|
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
|
+
`;
|
|
137
857
|
}
|
|
138
858
|
}
|
|
139
859
|
|
|
140
|
-
// WebSocket
|
|
860
|
+
// Live reload WebSocket client
|
|
141
861
|
function connectWS() {
|
|
142
862
|
try {
|
|
143
863
|
const ws = new WebSocket(`ws://${location.host}`);
|
|
@@ -145,211 +865,421 @@
|
|
|
145
865
|
const data = JSON.parse(event.data);
|
|
146
866
|
if (data.type === 'update') {
|
|
147
867
|
report = data.report;
|
|
148
|
-
|
|
868
|
+
updateMetricsAndViews();
|
|
149
869
|
}
|
|
150
870
|
};
|
|
151
871
|
ws.onclose = () => setTimeout(connectWS, 3000);
|
|
152
872
|
} catch {}
|
|
153
873
|
}
|
|
154
874
|
|
|
155
|
-
//
|
|
156
|
-
function
|
|
157
|
-
if (!report
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
888
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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>
|
|
170
901
|
</div>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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>
|
|
174
912
|
</div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
${
|
|
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>
|
|
178
926
|
</div>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
927
|
+
|
|
928
|
+
<div class="status">
|
|
929
|
+
<span class="status-dot"></span>
|
|
930
|
+
<span>Live reload active</span>
|
|
182
931
|
</div>
|
|
183
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>
|
|
184
1036
|
`;
|
|
185
1037
|
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
});
|
|
188
1115
|
}
|
|
189
1116
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
1149
|
}
|
|
199
1150
|
|
|
200
|
-
|
|
1151
|
+
// Populate the Overview Metrics cards
|
|
1152
|
+
function renderOverviewStats() {
|
|
1153
|
+
const statsGrid = document.getElementById('overview-stats');
|
|
201
1154
|
const t = report.totals;
|
|
1155
|
+
|
|
202
1156
|
const sizeClass = t.totalGzipSize > 300000 ? 'red' : t.totalGzipSize > 150000 ? 'yellow' : 'green';
|
|
203
1157
|
const issueClass = t.issuesBySeverity.error > 0 ? 'red' : t.issuesBySeverity.warning > 0 ? 'yellow' : 'green';
|
|
204
1158
|
|
|
205
|
-
|
|
1159
|
+
statsGrid.innerHTML = `
|
|
206
1160
|
<div class="stat-card">
|
|
207
|
-
<div class="label">Total Islands</div>
|
|
208
|
-
<div class="value">${t.totalIslands}</div>
|
|
1161
|
+
<div class="stat-label">Total Islands</div>
|
|
1162
|
+
<div class="stat-value">${t.totalIslands}</div>
|
|
209
1163
|
</div>
|
|
210
1164
|
<div class="stat-card">
|
|
211
|
-
<div class="label">Total JS (gzip)</div>
|
|
212
|
-
<div class="value ${sizeClass}">${formatBytes(t.totalGzipSize)}</div>
|
|
1165
|
+
<div class="stat-label">Total JS (gzip)</div>
|
|
1166
|
+
<div class="stat-value ${sizeClass}">${formatBytes(t.totalGzipSize)}</div>
|
|
213
1167
|
</div>
|
|
214
1168
|
<div class="stat-card">
|
|
215
|
-
<div class="label">Total JS (brotli)</div>
|
|
216
|
-
<div class="value">${formatBytes(t.totalBrotliSize)}</div>
|
|
1169
|
+
<div class="stat-label">Total JS (brotli)</div>
|
|
1170
|
+
<div class="stat-value">${formatBytes(t.totalBrotliSize)}</div>
|
|
217
1171
|
</div>
|
|
218
1172
|
<div class="stat-card">
|
|
219
|
-
<div class="label">Issues</div>
|
|
220
|
-
<div class="value ${issueClass}">${t.totalIssues}</div>
|
|
1173
|
+
<div class="stat-label">Total Issues</div>
|
|
1174
|
+
<div class="stat-value ${issueClass}">${t.totalIssues}</div>
|
|
221
1175
|
</div>
|
|
222
|
-
|
|
1176
|
+
`;
|
|
223
1177
|
}
|
|
224
1178
|
|
|
1179
|
+
// Populate Overview Budget Gauges
|
|
225
1180
|
function renderBudgetGauges() {
|
|
1181
|
+
const budgetGauges = document.getElementById('budget-gauges');
|
|
226
1182
|
const config = report.config;
|
|
227
|
-
if (!config)
|
|
1183
|
+
if (!config) {
|
|
1184
|
+
budgetGauges.innerHTML = `<p style="color:var(--text-muted)">No configuration thresholds defined.</p>`;
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
228
1187
|
|
|
229
1188
|
const gauges = [];
|
|
230
1189
|
|
|
231
1190
|
// Per-page budgets
|
|
232
1191
|
for (const page of report.pages) {
|
|
233
|
-
const
|
|
1192
|
+
const limit = config.thresholds.pageBudget;
|
|
1193
|
+
const pct = Math.min((page.totalGzipSize / limit) * 100, 100);
|
|
234
1194
|
const color = pct > 100 ? 'var(--red)' : pct > 66 ? 'var(--yellow)' : 'var(--green)';
|
|
1195
|
+
|
|
235
1196
|
gauges.push(`
|
|
236
|
-
<div
|
|
237
|
-
<div
|
|
238
|
-
<span>${page.route}</span>
|
|
239
|
-
<span>${formatBytes(page.totalGzipSize)} / ${formatBytes(
|
|
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>
|
|
240
1201
|
</div>
|
|
241
|
-
<div class="gauge">
|
|
242
|
-
<div class="gauge-fill" style="width
|
|
1202
|
+
<div class="gauge-bar">
|
|
1203
|
+
<div class="gauge-fill" style="width: ${pct}%; background: ${color};"></div>
|
|
243
1204
|
</div>
|
|
244
1205
|
</div>
|
|
245
1206
|
`);
|
|
246
1207
|
}
|
|
247
1208
|
|
|
248
1209
|
// Total budget
|
|
249
|
-
const
|
|
1210
|
+
const totalLimit = config.thresholds.totalBudget;
|
|
1211
|
+
const totalPct = Math.min((report.totals.totalGzipSize / totalLimit) * 100, 100);
|
|
250
1212
|
const totalColor = totalPct > 100 ? 'var(--red)' : totalPct > 66 ? 'var(--yellow)' : 'var(--green)';
|
|
1213
|
+
|
|
251
1214
|
gauges.push(`
|
|
252
|
-
<div style="
|
|
253
|
-
<div
|
|
254
|
-
<
|
|
255
|
-
<span>${formatBytes(report.totals.totalGzipSize)} / ${formatBytes(
|
|
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>
|
|
256
1219
|
</div>
|
|
257
|
-
<div class="gauge">
|
|
258
|
-
<div class="gauge-fill" style="width
|
|
1220
|
+
<div class="gauge-bar" style="height:12px;">
|
|
1221
|
+
<div class="gauge-fill" style="width: ${totalPct}%; background: ${totalColor};"></div>
|
|
259
1222
|
</div>
|
|
260
1223
|
</div>
|
|
261
1224
|
`);
|
|
262
1225
|
|
|
263
|
-
|
|
1226
|
+
budgetGauges.innerHTML = gauges.join('');
|
|
264
1227
|
}
|
|
265
1228
|
|
|
266
|
-
|
|
267
|
-
const islands = getSortedIslands();
|
|
268
|
-
if (islands.length === 0) return '<p style="color:var(--text-dim)">No islands found</p>';
|
|
269
|
-
|
|
270
|
-
const arrow = (col) => sortColumn === col ? (sortAsc ? ' ↑' : ' ↓') : '';
|
|
271
|
-
|
|
272
|
-
return `<table>
|
|
273
|
-
<thead>
|
|
274
|
-
<tr>
|
|
275
|
-
<th data-sort="name">Name${arrow('name')}</th>
|
|
276
|
-
<th data-sort="directive">Directive${arrow('directive')}</th>
|
|
277
|
-
<th data-sort="framework">Framework${arrow('framework')}</th>
|
|
278
|
-
<th data-sort="gzip">Gzip${arrow('gzip')}</th>
|
|
279
|
-
<th data-sort="brotli">Brotli${arrow('brotli')}</th>
|
|
280
|
-
<th data-sort="issues">Issues${arrow('issues')}</th>
|
|
281
|
-
<th>Pages</th>
|
|
282
|
-
</tr>
|
|
283
|
-
</thead>
|
|
284
|
-
<tbody>
|
|
285
|
-
${islands.map(i => `<tr>
|
|
286
|
-
<td><strong>${i.component.name}</strong></td>
|
|
287
|
-
<td><span class="directive directive-${i.component.directive.split(':')[1]}">${i.component.directive}</span></td>
|
|
288
|
-
<td>${i.component.uiFramework}</td>
|
|
289
|
-
<td>${formatBytes(i.bundle.totalGzipSize)}</td>
|
|
290
|
-
<td style="color:var(--text-dim)">${formatBytes(i.bundle.totalBrotliSize)}</td>
|
|
291
|
-
<td>${i.issues.length > 0 ? `<span style="color:var(--yellow)">${i.issues.length}</span>` : '<span style="color:var(--green)">0</span>'}</td>
|
|
292
|
-
<td style="color:var(--text-dim);font-size:0.8rem">${i.component.pages.join(', ')}</td>
|
|
293
|
-
</tr>`).join('')}
|
|
294
|
-
</tbody>
|
|
295
|
-
</table>`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function renderIssues() {
|
|
299
|
-
const issues = getAllIssues();
|
|
300
|
-
if (issues.length === 0) return '<p style="color:var(--green)">No issues found!</p>';
|
|
301
|
-
|
|
302
|
-
return issues.map(issue => `
|
|
303
|
-
<div class="issue issue-${issue.severity}">
|
|
304
|
-
<div>${issue.message}</div>
|
|
305
|
-
<div class="rec">${issue.recommendation}</div>
|
|
306
|
-
</div>
|
|
307
|
-
`).join('');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ─── Treemap ───────────────────────────────────────────────
|
|
1229
|
+
// Populate Overview Treemap
|
|
311
1230
|
function renderTreemap() {
|
|
312
|
-
const container = document.getElementById('treemap');
|
|
313
|
-
if (!container || !report.islands.length) return;
|
|
1231
|
+
const container = document.getElementById('treemap-container');
|
|
1232
|
+
if (!container || !report || !report.islands.length) return;
|
|
314
1233
|
|
|
315
1234
|
const width = container.offsetWidth;
|
|
316
1235
|
const height = container.offsetHeight;
|
|
317
1236
|
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
name:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// Simple treemap layout (no d3 dependency — inline squarify)
|
|
331
|
-
const nodes = squarify(data.children, 0, 0, width, height);
|
|
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);
|
|
332
1248
|
|
|
333
1249
|
container.innerHTML = nodes.map(n => {
|
|
334
1250
|
const color = getNodeColor(n);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
`;
|
|
340
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
|
+
});
|
|
341
1270
|
}
|
|
342
1271
|
|
|
1272
|
+
// Treemap node color categorization based on issues and size weight
|
|
343
1273
|
function getNodeColor(node) {
|
|
344
1274
|
if (node.issues > 0) {
|
|
345
|
-
return node.gzip >
|
|
1275
|
+
return node.gzip > 80000 ? 'rgba(239, 68, 68, 0.7)' : 'rgba(245, 158, 11, 0.6)';
|
|
346
1276
|
}
|
|
347
|
-
if (node.gzip > 50000) return 'rgba(
|
|
348
|
-
if (node.gzip >
|
|
349
|
-
return 'rgba(
|
|
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)';
|
|
350
1280
|
}
|
|
351
1281
|
|
|
352
|
-
//
|
|
1282
|
+
// Squarified treemap layout algorithm
|
|
353
1283
|
function squarify(items, x, y, w, h) {
|
|
354
1284
|
const total = items.reduce((s, i) => s + i.value, 0);
|
|
355
1285
|
if (total === 0 || items.length === 0) return [];
|
|
@@ -381,10 +1311,28 @@
|
|
|
381
1311
|
return result;
|
|
382
1312
|
}
|
|
383
1313
|
|
|
384
|
-
//
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
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) => {
|
|
388
1336
|
let va, vb;
|
|
389
1337
|
switch (sortColumn) {
|
|
390
1338
|
case 'name': va = a.component.name; vb = b.component.name; break;
|
|
@@ -395,33 +1343,251 @@
|
|
|
395
1343
|
case 'issues': va = a.issues.length; vb = b.issues.length; break;
|
|
396
1344
|
default: va = a.bundle.totalGzipSize; vb = b.bundle.totalGzipSize;
|
|
397
1345
|
}
|
|
398
|
-
|
|
1346
|
+
|
|
1347
|
+
if (typeof va === 'string') {
|
|
1348
|
+
return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
1349
|
+
}
|
|
399
1350
|
return sortAsc ? va - vb : vb - va;
|
|
400
1351
|
});
|
|
401
|
-
|
|
1352
|
+
|
|
1353
|
+
return list;
|
|
402
1354
|
}
|
|
403
1355
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
411
1394
|
});
|
|
412
1395
|
});
|
|
413
1396
|
}
|
|
414
1397
|
|
|
415
|
-
//
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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');
|
|
423
1582
|
}
|
|
424
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
|
|
425
1591
|
function formatBytes(bytes) {
|
|
426
1592
|
if (!bytes || bytes === 0) return '0 B';
|
|
427
1593
|
if (bytes < 1024) return bytes + ' B';
|
|
@@ -430,10 +1596,12 @@
|
|
|
430
1596
|
return (kb / 1024).toFixed(2) + ' MB';
|
|
431
1597
|
}
|
|
432
1598
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1599
|
+
function pathBasename(filePath) {
|
|
1600
|
+
return filePath.split(/[\\/]/).pop() || filePath;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Launch UI init
|
|
1604
|
+
init();
|
|
437
1605
|
</script>
|
|
438
1606
|
</body>
|
|
439
1607
|
</html>
|