@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.
@@ -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">&times;</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 &middot; <span class="issue-component" data-inspect-comp="${iss.component}">${iss.component}</span>`
1420
+ : iss.source === 'page'
1421
+ ? `Page &middot; <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>