roast-my-design-system 3.5.1 → 3.7.0
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/README.md +6 -3
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/benchmark/benchmark.json +332 -41
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +37 -10
- package/skills/roast-my-design-system/scripts/harvest/tokens.mjs +9 -0
- package/skills/roast-my-design-system/scripts/lib/nearpairs.mjs +31 -0
- package/skills/roast-my-design-system/scripts/lib/neverimported.mjs +17 -0
- package/skills/roast-my-design-system/scripts/rules/build.mjs +13 -1
package/README.md
CHANGED
|
@@ -24,11 +24,11 @@ Your AI agent (Claude, Cursor, Copilot) builds UI by imitating what's already in
|
|
|
24
24
|
|
|
25
25
|
The full report for vercel/ai-chatbot, top to bottom:
|
|
26
26
|
|
|
27
|
-

|
|
28
28
|
|
|
29
29
|
The same report in light mode (one file, built-in toggle):
|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
## What makes the numbers trustworthy
|
|
34
34
|
|
|
@@ -120,6 +120,9 @@ Three real roasts of public repos, hosted as-is (the same self-contained HTML th
|
|
|
120
120
|
| Duplicated components | 0 | 14 | 10 |
|
|
121
121
|
| Inline style blocks | 0 | 42 | 12 |
|
|
122
122
|
| Arbitrary Tailwind values | ~20 | 91 | 0 |
|
|
123
|
+
| Near-identical colour pairs | 0 | 2 | 1 |
|
|
124
|
+
| !important declarations | 0 | 5 | 3 |
|
|
125
|
+
| Components never imported | 0 | 0 | 0 |
|
|
123
126
|
|
|
124
127
|
Yes, the median repo is already a mess. That's the point.
|
|
125
128
|
|
|
@@ -127,4 +130,4 @@ Yes, the median repo is already a mess. That's the point.
|
|
|
127
130
|
|
|
128
131
|
MIT
|
|
129
132
|
|
|
130
|
-
Built and designed by <a href="https://gregkozakiewicz.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark-dark.png?v=3.
|
|
133
|
+
Built and designed by <a href="https://gregkozakiewicz.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark-dark.png?v=3.7.0"><img src="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark.png?v=3.7.0" height="15" alt="GK mark"></picture> Greg Kozakiewicz</a>.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Roast your design system with real data. A deterministic, zero-dependency scanner that counts every colour, grey, spacing value, typeface, duplicated component, inline style and arbitrary Tailwind value in your repo, scores it 0-100 against Ideal Design System norms and a 30-repo benchmark, generates a shareable HTML diagnosis, and with --rules writes the agent rules file (CLAUDE.md, .cursor/rules) that stops the mess coming back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"builtAt": "2026-08-
|
|
2
|
+
"builtAt": "2026-08-03T16:33:32.652Z",
|
|
3
3
|
"repoCount": 30,
|
|
4
4
|
"ideal2026": {
|
|
5
5
|
"colors": {
|
|
@@ -38,6 +38,18 @@
|
|
|
38
38
|
"value": 0,
|
|
39
39
|
"note": "styling belongs to the system"
|
|
40
40
|
},
|
|
41
|
+
"nearPairs": {
|
|
42
|
+
"value": 0,
|
|
43
|
+
"note": "near-identical colours are copy-paste, not decisions"
|
|
44
|
+
},
|
|
45
|
+
"important": {
|
|
46
|
+
"value": 0,
|
|
47
|
+
"note": "!important is the cascade admitting defeat"
|
|
48
|
+
},
|
|
49
|
+
"neverImported": {
|
|
50
|
+
"value": 0,
|
|
51
|
+
"note": "a system component nobody imports is dead weight"
|
|
52
|
+
},
|
|
41
53
|
"arbitrary": {
|
|
42
54
|
"value": 20,
|
|
43
55
|
"note": "bracket escape hatches; a handful of deliberate exceptions"
|
|
@@ -104,7 +116,7 @@
|
|
|
104
116
|
174,
|
|
105
117
|
206,
|
|
106
118
|
246,
|
|
107
|
-
|
|
119
|
+
303,
|
|
108
120
|
385,
|
|
109
121
|
403,
|
|
110
122
|
1645
|
|
@@ -112,7 +124,7 @@
|
|
|
112
124
|
"p25": 38,
|
|
113
125
|
"median": 96,
|
|
114
126
|
"p75": 170,
|
|
115
|
-
"p90":
|
|
127
|
+
"p90": 311
|
|
116
128
|
},
|
|
117
129
|
"greys": {
|
|
118
130
|
"values": [
|
|
@@ -373,7 +385,7 @@
|
|
|
373
385
|
95,
|
|
374
386
|
103,
|
|
375
387
|
115,
|
|
376
|
-
|
|
388
|
+
189
|
|
377
389
|
],
|
|
378
390
|
"p25": 1,
|
|
379
391
|
"median": 14,
|
|
@@ -397,7 +409,7 @@
|
|
|
397
409
|
23,
|
|
398
410
|
37,
|
|
399
411
|
41,
|
|
400
|
-
|
|
412
|
+
43,
|
|
401
413
|
46,
|
|
402
414
|
47,
|
|
403
415
|
51,
|
|
@@ -411,7 +423,7 @@
|
|
|
411
423
|
122,
|
|
412
424
|
173,
|
|
413
425
|
1178,
|
|
414
|
-
|
|
426
|
+
2923
|
|
415
427
|
],
|
|
416
428
|
"p25": 8,
|
|
417
429
|
"median": 42,
|
|
@@ -443,7 +455,7 @@
|
|
|
443
455
|
234,
|
|
444
456
|
271,
|
|
445
457
|
321,
|
|
446
|
-
|
|
458
|
+
380,
|
|
447
459
|
407,
|
|
448
460
|
468,
|
|
449
461
|
470,
|
|
@@ -456,6 +468,120 @@
|
|
|
456
468
|
"p75": 309,
|
|
457
469
|
"p90": 472
|
|
458
470
|
},
|
|
471
|
+
"nearPairs": {
|
|
472
|
+
"values": [
|
|
473
|
+
0,
|
|
474
|
+
0,
|
|
475
|
+
0,
|
|
476
|
+
0,
|
|
477
|
+
0,
|
|
478
|
+
0,
|
|
479
|
+
0,
|
|
480
|
+
0,
|
|
481
|
+
0,
|
|
482
|
+
0,
|
|
483
|
+
0,
|
|
484
|
+
1,
|
|
485
|
+
1,
|
|
486
|
+
2,
|
|
487
|
+
2,
|
|
488
|
+
2,
|
|
489
|
+
9,
|
|
490
|
+
12,
|
|
491
|
+
13,
|
|
492
|
+
13,
|
|
493
|
+
15,
|
|
494
|
+
16,
|
|
495
|
+
23,
|
|
496
|
+
26,
|
|
497
|
+
30,
|
|
498
|
+
37,
|
|
499
|
+
47,
|
|
500
|
+
49,
|
|
501
|
+
70,
|
|
502
|
+
146
|
|
503
|
+
],
|
|
504
|
+
"p25": 0,
|
|
505
|
+
"median": 2,
|
|
506
|
+
"p75": 21,
|
|
507
|
+
"p90": 47
|
|
508
|
+
},
|
|
509
|
+
"important": {
|
|
510
|
+
"values": [
|
|
511
|
+
0,
|
|
512
|
+
0,
|
|
513
|
+
0,
|
|
514
|
+
0,
|
|
515
|
+
0,
|
|
516
|
+
0,
|
|
517
|
+
0,
|
|
518
|
+
0,
|
|
519
|
+
0,
|
|
520
|
+
0,
|
|
521
|
+
0,
|
|
522
|
+
1,
|
|
523
|
+
1,
|
|
524
|
+
2,
|
|
525
|
+
4,
|
|
526
|
+
5,
|
|
527
|
+
5,
|
|
528
|
+
6,
|
|
529
|
+
9,
|
|
530
|
+
9,
|
|
531
|
+
11,
|
|
532
|
+
12,
|
|
533
|
+
20,
|
|
534
|
+
35,
|
|
535
|
+
42,
|
|
536
|
+
42,
|
|
537
|
+
74,
|
|
538
|
+
83,
|
|
539
|
+
88,
|
|
540
|
+
161
|
|
541
|
+
],
|
|
542
|
+
"p25": 0,
|
|
543
|
+
"median": 5,
|
|
544
|
+
"p75": 18,
|
|
545
|
+
"p90": 75
|
|
546
|
+
},
|
|
547
|
+
"neverImported": {
|
|
548
|
+
"values": [
|
|
549
|
+
0,
|
|
550
|
+
0,
|
|
551
|
+
0,
|
|
552
|
+
0,
|
|
553
|
+
0,
|
|
554
|
+
0,
|
|
555
|
+
0,
|
|
556
|
+
0,
|
|
557
|
+
0,
|
|
558
|
+
0,
|
|
559
|
+
0,
|
|
560
|
+
0,
|
|
561
|
+
0,
|
|
562
|
+
0,
|
|
563
|
+
0,
|
|
564
|
+
0,
|
|
565
|
+
0,
|
|
566
|
+
0,
|
|
567
|
+
5,
|
|
568
|
+
7,
|
|
569
|
+
10,
|
|
570
|
+
13,
|
|
571
|
+
22,
|
|
572
|
+
22,
|
|
573
|
+
26,
|
|
574
|
+
36,
|
|
575
|
+
39,
|
|
576
|
+
45,
|
|
577
|
+
94,
|
|
578
|
+
100
|
|
579
|
+
],
|
|
580
|
+
"p25": 0,
|
|
581
|
+
"median": 0,
|
|
582
|
+
"p75": 20,
|
|
583
|
+
"p90": 40
|
|
584
|
+
},
|
|
459
585
|
"components": {
|
|
460
586
|
"values": [
|
|
461
587
|
9,
|
|
@@ -471,27 +597,27 @@
|
|
|
471
597
|
342,
|
|
472
598
|
350,
|
|
473
599
|
417,
|
|
474
|
-
|
|
475
|
-
|
|
600
|
+
555,
|
|
601
|
+
637,
|
|
476
602
|
777,
|
|
477
|
-
|
|
478
|
-
|
|
603
|
+
796,
|
|
604
|
+
836,
|
|
479
605
|
936,
|
|
480
606
|
940,
|
|
481
|
-
|
|
607
|
+
962,
|
|
482
608
|
970,
|
|
483
|
-
|
|
609
|
+
1052,
|
|
484
610
|
1081,
|
|
485
611
|
1216,
|
|
486
612
|
1379,
|
|
487
613
|
1414,
|
|
488
614
|
1619,
|
|
489
|
-
|
|
490
|
-
|
|
615
|
+
2134,
|
|
616
|
+
2566
|
|
491
617
|
],
|
|
492
618
|
"p25": 233,
|
|
493
|
-
"median":
|
|
494
|
-
"p75":
|
|
619
|
+
"median": 707,
|
|
620
|
+
"p75": 1032,
|
|
495
621
|
"p90": 1435
|
|
496
622
|
}
|
|
497
623
|
},
|
|
@@ -511,6 +637,9 @@
|
|
|
511
637
|
"exactDuplicates": 115,
|
|
512
638
|
"inlineStyles": 79,
|
|
513
639
|
"arbitrary": 321,
|
|
640
|
+
"nearPairs": 47,
|
|
641
|
+
"important": 161,
|
|
642
|
+
"neverImported": 39,
|
|
514
643
|
"components": 1414
|
|
515
644
|
}
|
|
516
645
|
},
|
|
@@ -529,6 +658,9 @@
|
|
|
529
658
|
"exactDuplicates": 3,
|
|
530
659
|
"inlineStyles": 82,
|
|
531
660
|
"arbitrary": 0,
|
|
661
|
+
"nearPairs": 49,
|
|
662
|
+
"important": 88,
|
|
663
|
+
"neverImported": 0,
|
|
532
664
|
"components": 342
|
|
533
665
|
}
|
|
534
666
|
},
|
|
@@ -547,13 +679,16 @@
|
|
|
547
679
|
"exactDuplicates": 4,
|
|
548
680
|
"inlineStyles": 23,
|
|
549
681
|
"arbitrary": 0,
|
|
682
|
+
"nearPairs": 37,
|
|
683
|
+
"important": 4,
|
|
684
|
+
"neverImported": 0,
|
|
550
685
|
"components": 50
|
|
551
686
|
}
|
|
552
687
|
},
|
|
553
688
|
{
|
|
554
689
|
"repo": "outline/outline",
|
|
555
690
|
"designSystem": "custom",
|
|
556
|
-
"codeFiles":
|
|
691
|
+
"codeFiles": 2122,
|
|
557
692
|
"metrics": {
|
|
558
693
|
"colors": 89,
|
|
559
694
|
"greys": 15,
|
|
@@ -565,7 +700,10 @@
|
|
|
565
700
|
"exactDuplicates": 23,
|
|
566
701
|
"inlineStyles": 76,
|
|
567
702
|
"arbitrary": 0,
|
|
568
|
-
"
|
|
703
|
+
"nearPairs": 1,
|
|
704
|
+
"important": 0,
|
|
705
|
+
"neverImported": 0,
|
|
706
|
+
"components": 637
|
|
569
707
|
}
|
|
570
708
|
},
|
|
571
709
|
{
|
|
@@ -583,6 +721,9 @@
|
|
|
583
721
|
"exactDuplicates": 2,
|
|
584
722
|
"inlineStyles": 0,
|
|
585
723
|
"arbitrary": 4,
|
|
724
|
+
"nearPairs": 0,
|
|
725
|
+
"important": 5,
|
|
726
|
+
"neverImported": 0,
|
|
586
727
|
"components": 220
|
|
587
728
|
}
|
|
588
729
|
},
|
|
@@ -601,6 +742,9 @@
|
|
|
601
742
|
"exactDuplicates": 0,
|
|
602
743
|
"inlineStyles": 0,
|
|
603
744
|
"arbitrary": 0,
|
|
745
|
+
"nearPairs": 0,
|
|
746
|
+
"important": 0,
|
|
747
|
+
"neverImported": 0,
|
|
604
748
|
"components": 25
|
|
605
749
|
}
|
|
606
750
|
},
|
|
@@ -619,6 +763,9 @@
|
|
|
619
763
|
"exactDuplicates": 0,
|
|
620
764
|
"inlineStyles": 0,
|
|
621
765
|
"arbitrary": 60,
|
|
766
|
+
"nearPairs": 0,
|
|
767
|
+
"important": 1,
|
|
768
|
+
"neverImported": 94,
|
|
622
769
|
"components": 182
|
|
623
770
|
}
|
|
624
771
|
},
|
|
@@ -637,6 +784,9 @@
|
|
|
637
784
|
"exactDuplicates": 0,
|
|
638
785
|
"inlineStyles": 0,
|
|
639
786
|
"arbitrary": 0,
|
|
787
|
+
"nearPairs": 0,
|
|
788
|
+
"important": 0,
|
|
789
|
+
"neverImported": 0,
|
|
640
790
|
"components": 9
|
|
641
791
|
}
|
|
642
792
|
},
|
|
@@ -655,6 +805,9 @@
|
|
|
655
805
|
"exactDuplicates": 1,
|
|
656
806
|
"inlineStyles": 2,
|
|
657
807
|
"arbitrary": 16,
|
|
808
|
+
"nearPairs": 0,
|
|
809
|
+
"important": 2,
|
|
810
|
+
"neverImported": 0,
|
|
658
811
|
"components": 60
|
|
659
812
|
}
|
|
660
813
|
},
|
|
@@ -673,6 +826,9 @@
|
|
|
673
826
|
"exactDuplicates": 1,
|
|
674
827
|
"inlineStyles": 7,
|
|
675
828
|
"arbitrary": 103,
|
|
829
|
+
"nearPairs": 0,
|
|
830
|
+
"important": 9,
|
|
831
|
+
"neverImported": 0,
|
|
676
832
|
"components": 315
|
|
677
833
|
}
|
|
678
834
|
},
|
|
@@ -691,13 +847,16 @@
|
|
|
691
847
|
"exactDuplicates": 1,
|
|
692
848
|
"inlineStyles": 11,
|
|
693
849
|
"arbitrary": 148,
|
|
850
|
+
"nearPairs": 0,
|
|
851
|
+
"important": 0,
|
|
852
|
+
"neverImported": 100,
|
|
694
853
|
"components": 272
|
|
695
854
|
}
|
|
696
855
|
},
|
|
697
856
|
{
|
|
698
857
|
"repo": "documenso/documenso",
|
|
699
858
|
"designSystem": "custom",
|
|
700
|
-
"codeFiles":
|
|
859
|
+
"codeFiles": 1912,
|
|
701
860
|
"metrics": {
|
|
702
861
|
"colors": 36,
|
|
703
862
|
"greys": 13,
|
|
@@ -709,13 +868,16 @@
|
|
|
709
868
|
"exactDuplicates": 17,
|
|
710
869
|
"inlineStyles": 69,
|
|
711
870
|
"arbitrary": 234,
|
|
712
|
-
"
|
|
871
|
+
"nearPairs": 15,
|
|
872
|
+
"important": 0,
|
|
873
|
+
"neverImported": 45,
|
|
874
|
+
"components": 796
|
|
713
875
|
}
|
|
714
876
|
},
|
|
715
877
|
{
|
|
716
878
|
"repo": "dubinc/dub",
|
|
717
879
|
"designSystem": "custom",
|
|
718
|
-
"codeFiles":
|
|
880
|
+
"codeFiles": 3526,
|
|
719
881
|
"metrics": {
|
|
720
882
|
"colors": 102,
|
|
721
883
|
"greys": 14,
|
|
@@ -727,6 +889,9 @@
|
|
|
727
889
|
"exactDuplicates": 17,
|
|
728
890
|
"inlineStyles": 47,
|
|
729
891
|
"arbitrary": 470,
|
|
892
|
+
"nearPairs": 13,
|
|
893
|
+
"important": 1,
|
|
894
|
+
"neverImported": 22,
|
|
730
895
|
"components": 1379
|
|
731
896
|
}
|
|
732
897
|
},
|
|
@@ -745,6 +910,9 @@
|
|
|
745
910
|
"exactDuplicates": 59,
|
|
746
911
|
"inlineStyles": 18,
|
|
747
912
|
"arbitrary": 164,
|
|
913
|
+
"nearPairs": 13,
|
|
914
|
+
"important": 83,
|
|
915
|
+
"neverImported": 0,
|
|
748
916
|
"components": 940
|
|
749
917
|
}
|
|
750
918
|
},
|
|
@@ -763,13 +931,16 @@
|
|
|
763
931
|
"exactDuplicates": 92,
|
|
764
932
|
"inlineStyles": 119,
|
|
765
933
|
"arbitrary": 164,
|
|
934
|
+
"nearPairs": 0,
|
|
935
|
+
"important": 0,
|
|
936
|
+
"neverImported": 5,
|
|
766
937
|
"components": 1081
|
|
767
938
|
}
|
|
768
939
|
},
|
|
769
940
|
{
|
|
770
941
|
"repo": "lukevella/rallly",
|
|
771
942
|
"designSystem": "custom",
|
|
772
|
-
"codeFiles":
|
|
943
|
+
"codeFiles": 692,
|
|
773
944
|
"metrics": {
|
|
774
945
|
"colors": 26,
|
|
775
946
|
"greys": 4,
|
|
@@ -781,7 +952,10 @@
|
|
|
781
952
|
"exactDuplicates": 10,
|
|
782
953
|
"inlineStyles": 23,
|
|
783
954
|
"arbitrary": 27,
|
|
784
|
-
"
|
|
955
|
+
"nearPairs": 2,
|
|
956
|
+
"important": 0,
|
|
957
|
+
"neverImported": 22,
|
|
958
|
+
"components": 555
|
|
785
959
|
}
|
|
786
960
|
},
|
|
787
961
|
{
|
|
@@ -799,13 +973,16 @@
|
|
|
799
973
|
"exactDuplicates": 20,
|
|
800
974
|
"inlineStyles": 46,
|
|
801
975
|
"arbitrary": 407,
|
|
976
|
+
"nearPairs": 0,
|
|
977
|
+
"important": 42,
|
|
978
|
+
"neverImported": 13,
|
|
802
979
|
"components": 777
|
|
803
980
|
}
|
|
804
981
|
},
|
|
805
982
|
{
|
|
806
983
|
"repo": "elie222/inbox-zero",
|
|
807
984
|
"designSystem": "custom",
|
|
808
|
-
"codeFiles":
|
|
985
|
+
"codeFiles": 1821,
|
|
809
986
|
"metrics": {
|
|
810
987
|
"colors": 154,
|
|
811
988
|
"greys": 24,
|
|
@@ -817,7 +994,10 @@
|
|
|
817
994
|
"exactDuplicates": 21,
|
|
818
995
|
"inlineStyles": 5,
|
|
819
996
|
"arbitrary": 125,
|
|
820
|
-
"
|
|
997
|
+
"nearPairs": 26,
|
|
998
|
+
"important": 6,
|
|
999
|
+
"neverImported": 0,
|
|
1000
|
+
"components": 836
|
|
821
1001
|
}
|
|
822
1002
|
},
|
|
823
1003
|
{
|
|
@@ -835,6 +1015,9 @@
|
|
|
835
1015
|
"exactDuplicates": 7,
|
|
836
1016
|
"inlineStyles": 41,
|
|
837
1017
|
"arbitrary": 78,
|
|
1018
|
+
"nearPairs": 0,
|
|
1019
|
+
"important": 0,
|
|
1020
|
+
"neverImported": 0,
|
|
838
1021
|
"components": 190
|
|
839
1022
|
}
|
|
840
1023
|
},
|
|
@@ -853,13 +1036,16 @@
|
|
|
853
1036
|
"exactDuplicates": 0,
|
|
854
1037
|
"inlineStyles": 173,
|
|
855
1038
|
"arbitrary": 1,
|
|
1039
|
+
"nearPairs": 2,
|
|
1040
|
+
"important": 5,
|
|
1041
|
+
"neverImported": 0,
|
|
856
1042
|
"components": 417
|
|
857
1043
|
}
|
|
858
1044
|
},
|
|
859
1045
|
{
|
|
860
1046
|
"repo": "twentyhq/twenty",
|
|
861
1047
|
"designSystem": "library",
|
|
862
|
-
"codeFiles":
|
|
1048
|
+
"codeFiles": 15132,
|
|
863
1049
|
"metrics": {
|
|
864
1050
|
"colors": 1645,
|
|
865
1051
|
"greys": 100,
|
|
@@ -871,13 +1057,16 @@
|
|
|
871
1057
|
"exactDuplicates": 22,
|
|
872
1058
|
"inlineStyles": 122,
|
|
873
1059
|
"arbitrary": 0,
|
|
874
|
-
"
|
|
1060
|
+
"nearPairs": 146,
|
|
1061
|
+
"important": 9,
|
|
1062
|
+
"neverImported": 0,
|
|
1063
|
+
"components": 2134
|
|
875
1064
|
}
|
|
876
1065
|
},
|
|
877
1066
|
{
|
|
878
1067
|
"repo": "langfuse/langfuse",
|
|
879
1068
|
"designSystem": "custom",
|
|
880
|
-
"codeFiles":
|
|
1069
|
+
"codeFiles": 2774,
|
|
881
1070
|
"metrics": {
|
|
882
1071
|
"colors": 170,
|
|
883
1072
|
"greys": 4,
|
|
@@ -887,9 +1076,12 @@
|
|
|
887
1076
|
"radii": 10,
|
|
888
1077
|
"shadows": 0,
|
|
889
1078
|
"exactDuplicates": 7,
|
|
890
|
-
"inlineStyles":
|
|
891
|
-
"arbitrary":
|
|
892
|
-
"
|
|
1079
|
+
"inlineStyles": 43,
|
|
1080
|
+
"arbitrary": 380,
|
|
1081
|
+
"nearPairs": 2,
|
|
1082
|
+
"important": 12,
|
|
1083
|
+
"neverImported": 10,
|
|
1084
|
+
"components": 1052
|
|
893
1085
|
}
|
|
894
1086
|
},
|
|
895
1087
|
{
|
|
@@ -907,6 +1099,9 @@
|
|
|
907
1099
|
"exactDuplicates": 45,
|
|
908
1100
|
"inlineStyles": 37,
|
|
909
1101
|
"arbitrary": 468,
|
|
1102
|
+
"nearPairs": 70,
|
|
1103
|
+
"important": 42,
|
|
1104
|
+
"neverImported": 0,
|
|
910
1105
|
"components": 1216
|
|
911
1106
|
}
|
|
912
1107
|
},
|
|
@@ -925,6 +1120,9 @@
|
|
|
925
1120
|
"exactDuplicates": 95,
|
|
926
1121
|
"inlineStyles": 22,
|
|
927
1122
|
"arbitrary": 561,
|
|
1123
|
+
"nearPairs": 16,
|
|
1124
|
+
"important": 74,
|
|
1125
|
+
"neverImported": 7,
|
|
928
1126
|
"components": 1619
|
|
929
1127
|
}
|
|
930
1128
|
},
|
|
@@ -943,31 +1141,37 @@
|
|
|
943
1141
|
"exactDuplicates": 19,
|
|
944
1142
|
"inlineStyles": 1178,
|
|
945
1143
|
"arbitrary": 0,
|
|
1144
|
+
"nearPairs": 23,
|
|
1145
|
+
"important": 35,
|
|
1146
|
+
"neverImported": 36,
|
|
946
1147
|
"components": 350
|
|
947
1148
|
}
|
|
948
1149
|
},
|
|
949
1150
|
{
|
|
950
1151
|
"repo": "lobehub/lobe-chat",
|
|
951
1152
|
"designSystem": "library",
|
|
952
|
-
"codeFiles":
|
|
1153
|
+
"codeFiles": 8344,
|
|
953
1154
|
"metrics": {
|
|
954
|
-
"colors":
|
|
1155
|
+
"colors": 303,
|
|
955
1156
|
"greys": 37,
|
|
956
1157
|
"spacing": 61,
|
|
957
1158
|
"typefaces": 1,
|
|
958
1159
|
"fontSizes": 2,
|
|
959
1160
|
"radii": 1,
|
|
960
1161
|
"shadows": 0,
|
|
961
|
-
"exactDuplicates":
|
|
962
|
-
"inlineStyles":
|
|
1162
|
+
"exactDuplicates": 189,
|
|
1163
|
+
"inlineStyles": 2923,
|
|
963
1164
|
"arbitrary": 0,
|
|
964
|
-
"
|
|
1165
|
+
"nearPairs": 9,
|
|
1166
|
+
"important": 0,
|
|
1167
|
+
"neverImported": 0,
|
|
1168
|
+
"components": 2566
|
|
965
1169
|
}
|
|
966
1170
|
},
|
|
967
1171
|
{
|
|
968
1172
|
"repo": "danny-avila/LibreChat",
|
|
969
1173
|
"designSystem": "custom",
|
|
970
|
-
"codeFiles":
|
|
1174
|
+
"codeFiles": 2430,
|
|
971
1175
|
"metrics": {
|
|
972
1176
|
"colors": 174,
|
|
973
1177
|
"greys": 30,
|
|
@@ -979,7 +1183,10 @@
|
|
|
979
1183
|
"exactDuplicates": 38,
|
|
980
1184
|
"inlineStyles": 71,
|
|
981
1185
|
"arbitrary": 492,
|
|
982
|
-
"
|
|
1186
|
+
"nearPairs": 30,
|
|
1187
|
+
"important": 20,
|
|
1188
|
+
"neverImported": 0,
|
|
1189
|
+
"components": 962
|
|
983
1190
|
}
|
|
984
1191
|
},
|
|
985
1192
|
{
|
|
@@ -997,13 +1204,16 @@
|
|
|
997
1204
|
"exactDuplicates": 103,
|
|
998
1205
|
"inlineStyles": 117,
|
|
999
1206
|
"arbitrary": 1751,
|
|
1207
|
+
"nearPairs": 12,
|
|
1208
|
+
"important": 0,
|
|
1209
|
+
"neverImported": 26,
|
|
1000
1210
|
"components": 970
|
|
1001
1211
|
}
|
|
1002
1212
|
},
|
|
1003
1213
|
{
|
|
1004
1214
|
"repo": "triggerdotdev/trigger.dev",
|
|
1005
1215
|
"designSystem": "custom",
|
|
1006
|
-
"codeFiles":
|
|
1216
|
+
"codeFiles": 2366,
|
|
1007
1217
|
"metrics": {
|
|
1008
1218
|
"colors": 137,
|
|
1009
1219
|
"greys": 18,
|
|
@@ -1015,6 +1225,9 @@
|
|
|
1015
1225
|
"exactDuplicates": 8,
|
|
1016
1226
|
"inlineStyles": 51,
|
|
1017
1227
|
"arbitrary": 271,
|
|
1228
|
+
"nearPairs": 1,
|
|
1229
|
+
"important": 11,
|
|
1230
|
+
"neverImported": 0,
|
|
1018
1231
|
"components": 936
|
|
1019
1232
|
}
|
|
1020
1233
|
},
|
|
@@ -1033,12 +1246,15 @@
|
|
|
1033
1246
|
"exactDuplicates": 0,
|
|
1034
1247
|
"inlineStyles": 0,
|
|
1035
1248
|
"arbitrary": 26,
|
|
1249
|
+
"nearPairs": 0,
|
|
1250
|
+
"important": 0,
|
|
1251
|
+
"neverImported": 0,
|
|
1036
1252
|
"components": 36
|
|
1037
1253
|
}
|
|
1038
1254
|
}
|
|
1039
1255
|
],
|
|
1040
1256
|
"referenceSystems": {
|
|
1041
|
-
"builtAt": "2026-08-
|
|
1257
|
+
"builtAt": "2026-08-03T16:33:34.617Z",
|
|
1042
1258
|
"count": 10,
|
|
1043
1259
|
"note": "Curated scoped scans of reputable public design systems. Not comparable head-to-head with product repos: see per-system notes.",
|
|
1044
1260
|
"stats": {
|
|
@@ -1192,6 +1408,51 @@
|
|
|
1192
1408
|
],
|
|
1193
1409
|
"median": 0
|
|
1194
1410
|
},
|
|
1411
|
+
"nearPairs": {
|
|
1412
|
+
"values": [
|
|
1413
|
+
0,
|
|
1414
|
+
0,
|
|
1415
|
+
0,
|
|
1416
|
+
0,
|
|
1417
|
+
1,
|
|
1418
|
+
1,
|
|
1419
|
+
1,
|
|
1420
|
+
5,
|
|
1421
|
+
33,
|
|
1422
|
+
50
|
|
1423
|
+
],
|
|
1424
|
+
"median": 1
|
|
1425
|
+
},
|
|
1426
|
+
"important": {
|
|
1427
|
+
"values": [
|
|
1428
|
+
0,
|
|
1429
|
+
0,
|
|
1430
|
+
0,
|
|
1431
|
+
1,
|
|
1432
|
+
1,
|
|
1433
|
+
4,
|
|
1434
|
+
4,
|
|
1435
|
+
30,
|
|
1436
|
+
33,
|
|
1437
|
+
61
|
|
1438
|
+
],
|
|
1439
|
+
"median": 3
|
|
1440
|
+
},
|
|
1441
|
+
"neverImported": {
|
|
1442
|
+
"values": [
|
|
1443
|
+
0,
|
|
1444
|
+
0,
|
|
1445
|
+
0,
|
|
1446
|
+
0,
|
|
1447
|
+
0,
|
|
1448
|
+
0,
|
|
1449
|
+
0,
|
|
1450
|
+
0,
|
|
1451
|
+
0,
|
|
1452
|
+
0
|
|
1453
|
+
],
|
|
1454
|
+
"median": 0
|
|
1455
|
+
},
|
|
1195
1456
|
"components": {
|
|
1196
1457
|
"values": [
|
|
1197
1458
|
0,
|
|
@@ -1226,6 +1487,9 @@
|
|
|
1226
1487
|
"exactDuplicates": 0,
|
|
1227
1488
|
"inlineStyles": 1,
|
|
1228
1489
|
"arbitrary": 35,
|
|
1490
|
+
"nearPairs": 0,
|
|
1491
|
+
"important": 0,
|
|
1492
|
+
"neverImported": 0,
|
|
1229
1493
|
"components": 331
|
|
1230
1494
|
}
|
|
1231
1495
|
},
|
|
@@ -1246,6 +1510,9 @@
|
|
|
1246
1510
|
"exactDuplicates": 13,
|
|
1247
1511
|
"inlineStyles": 14,
|
|
1248
1512
|
"arbitrary": 0,
|
|
1513
|
+
"nearPairs": 5,
|
|
1514
|
+
"important": 33,
|
|
1515
|
+
"neverImported": 0,
|
|
1249
1516
|
"components": 228
|
|
1250
1517
|
}
|
|
1251
1518
|
},
|
|
@@ -1266,6 +1533,9 @@
|
|
|
1266
1533
|
"exactDuplicates": 15,
|
|
1267
1534
|
"inlineStyles": 10,
|
|
1268
1535
|
"arbitrary": 0,
|
|
1536
|
+
"nearPairs": 0,
|
|
1537
|
+
"important": 30,
|
|
1538
|
+
"neverImported": 0,
|
|
1269
1539
|
"components": 243
|
|
1270
1540
|
}
|
|
1271
1541
|
},
|
|
@@ -1286,6 +1556,9 @@
|
|
|
1286
1556
|
"exactDuplicates": 6,
|
|
1287
1557
|
"inlineStyles": 17,
|
|
1288
1558
|
"arbitrary": 0,
|
|
1559
|
+
"nearPairs": 1,
|
|
1560
|
+
"important": 4,
|
|
1561
|
+
"neverImported": 0,
|
|
1289
1562
|
"components": 314
|
|
1290
1563
|
}
|
|
1291
1564
|
},
|
|
@@ -1306,6 +1579,9 @@
|
|
|
1306
1579
|
"exactDuplicates": 2,
|
|
1307
1580
|
"inlineStyles": 10,
|
|
1308
1581
|
"arbitrary": 0,
|
|
1582
|
+
"nearPairs": 50,
|
|
1583
|
+
"important": 0,
|
|
1584
|
+
"neverImported": 0,
|
|
1309
1585
|
"components": 170
|
|
1310
1586
|
}
|
|
1311
1587
|
},
|
|
@@ -1326,6 +1602,9 @@
|
|
|
1326
1602
|
"exactDuplicates": 0,
|
|
1327
1603
|
"inlineStyles": 1,
|
|
1328
1604
|
"arbitrary": 0,
|
|
1605
|
+
"nearPairs": 1,
|
|
1606
|
+
"important": 0,
|
|
1607
|
+
"neverImported": 0,
|
|
1329
1608
|
"components": 711
|
|
1330
1609
|
}
|
|
1331
1610
|
},
|
|
@@ -1346,6 +1625,9 @@
|
|
|
1346
1625
|
"exactDuplicates": 17,
|
|
1347
1626
|
"inlineStyles": 24,
|
|
1348
1627
|
"arbitrary": 0,
|
|
1628
|
+
"nearPairs": 1,
|
|
1629
|
+
"important": 1,
|
|
1630
|
+
"neverImported": 0,
|
|
1349
1631
|
"components": 246
|
|
1350
1632
|
}
|
|
1351
1633
|
},
|
|
@@ -1366,6 +1648,9 @@
|
|
|
1366
1648
|
"exactDuplicates": 0,
|
|
1367
1649
|
"inlineStyles": 0,
|
|
1368
1650
|
"arbitrary": 0,
|
|
1651
|
+
"nearPairs": 33,
|
|
1652
|
+
"important": 61,
|
|
1653
|
+
"neverImported": 0,
|
|
1369
1654
|
"components": 0
|
|
1370
1655
|
}
|
|
1371
1656
|
},
|
|
@@ -1386,6 +1671,9 @@
|
|
|
1386
1671
|
"exactDuplicates": 170,
|
|
1387
1672
|
"inlineStyles": 14,
|
|
1388
1673
|
"arbitrary": 0,
|
|
1674
|
+
"nearPairs": 0,
|
|
1675
|
+
"important": 1,
|
|
1676
|
+
"neverImported": 0,
|
|
1389
1677
|
"components": 819
|
|
1390
1678
|
}
|
|
1391
1679
|
},
|
|
@@ -1406,6 +1694,9 @@
|
|
|
1406
1694
|
"exactDuplicates": 28,
|
|
1407
1695
|
"inlineStyles": 22,
|
|
1408
1696
|
"arbitrary": 0,
|
|
1697
|
+
"nearPairs": 0,
|
|
1698
|
+
"important": 4,
|
|
1699
|
+
"neverImported": 0,
|
|
1409
1700
|
"components": 508
|
|
1410
1701
|
}
|
|
1411
1702
|
}
|
|
@@ -12,6 +12,8 @@ import { resolve, basename, join, dirname } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { distinctTypefaces } from '../lib/typefaces.mjs';
|
|
14
14
|
import { rulesMarkdown } from '../rules/build.mjs';
|
|
15
|
+
import { nearColorPairs } from '../lib/nearpairs.mjs';
|
|
16
|
+
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
15
17
|
|
|
16
18
|
// The benchmark (Ideal-2026 norms + scanned-repo stats) ships next to the
|
|
17
19
|
// code so the page works offline; degrade gracefully when absent.
|
|
@@ -34,7 +36,7 @@ const GK_MASK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqa
|
|
|
34
36
|
const GK_MARK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAAAAAAAAAQCEeRdzAAACPklEQVR4nOVWTYh5URR/PlJYEKV8LKwkZalsmI2dZmMnK8pKmaZ87JSanexEkayUhSxsZyfJNBQ7UYqVhSShfM+v/6mXmtU81+p/Frfze+/d3++ce88993Hcf2cikehZ1GazWafTPUVDLBZjLBQKnU6HMTVvcrl8Op3ebrdsNgsokUiYUVP4Pp/v9s96vd79cwZGK95sNkkgGAwC2u12lhrY3u12C/b1ei2TyV5eXjabjV6vZ6BBa51MJin8arUKWKlU4H99fSkUCo5JUQ2HQxJwuVzIYLVawcdIVSs8CQoNpMQ+Go0Aw+EwwXw+D+h2u4Vr0LRSqUSMqVQKsN1uE3Q4HEql8ng8BgIBTnDhqlSqxWIBuvP5bDAYLBbL5XIBHAwGeBuJRODvdjun0/nnPOjraDRK8TYaDcBMJkMwFosBfn9/E0wkEn9OgjYgnU6fTidQwAGcTCbw9/u9VqvFUYB/vV5RskajkRNcTiaTKR6PI9j5fE7xlstlPM/lcgTr9ToneJ/5oOB4PB70O8Tb7/f9fv9sNqPkXl9fhQsQ9f1ktVqNvUVCxA6ZRxsf5pMAxnsuVBSOwvv7+6Phk0PsBNn0OOLC9haLReqdvBL/wUNKNPnt7Q0LjeVutVqhUEij0TwU9W8BVA4JUEUul8tarcZGhpbIarWiSeA0HQ4Hkvn4+GB56VMvov6DA+z1eplR89btdsE+Ho9tNhv3jOv+8/MTFz06M3ypVMqMnTfcwPhn4djG/tue+Mf4RGom9gMt6lAx16huIwAAAABJRU5ErkJggg==';
|
|
35
37
|
|
|
36
38
|
// Shown in the report footer; keep in step with plugin.json when releasing.
|
|
37
|
-
const VERSION = '3.
|
|
39
|
+
const VERSION = '3.7.0';
|
|
38
40
|
|
|
39
41
|
// Two shipped skins, same layout: 'dark' (navy glass, mint accent) and
|
|
40
42
|
// 'light' (lilac wash, white glass, violet accent). --theme picks one.
|
|
@@ -151,6 +153,13 @@ const greyStrays = greys.filter((c) => !c.isToken).length;
|
|
|
151
153
|
const arbitrary = h.tokens.tailwind?.arbitrary ?? [];
|
|
152
154
|
const arbitraryCount = arbitrary.reduce((sum, a) => sum + a.count, 0);
|
|
153
155
|
|
|
156
|
+
// !important declarations: the cascade admitting defeat.
|
|
157
|
+
const important = h.tokens.important ?? { count: 0, files: [] };
|
|
158
|
+
|
|
159
|
+
const nearPairs = nearColorPairs(colors);
|
|
160
|
+
|
|
161
|
+
const neverImported = neverImportedComponents(h.components, h.profile?.uiDir);
|
|
162
|
+
|
|
154
163
|
// Benchmark helpers: for a metric value, where does this repo sit among the
|
|
155
164
|
// scanned fleet? ("more colours than 90% of scanned repos")
|
|
156
165
|
function percentile(metric, value) {
|
|
@@ -204,6 +213,8 @@ if (effGreys > 13 * 1.25) candidates.push({ ratio: effGreys / 13, text: tokenLed
|
|
|
204
213
|
? `${greyStrays} hardcoded greys outside the token set`
|
|
205
214
|
: `${greys.length} shades of grey doing the job of 13` });
|
|
206
215
|
if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} off-scale spacing values where a dozen would do` });
|
|
216
|
+
if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length} colour pairs are nearly identical (${nearPairs[0].a.value} next to ${nearPairs[0].b.value}), copy-paste, not decisions` });
|
|
217
|
+
if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations, the cascade admitting defeat` });
|
|
207
218
|
if (iconCollisions.length >= 5) candidates.push({ ratio: 1 + iconCollisions.length / 8, text: `two icon sets collide on ${iconCollisions.length} names` });
|
|
208
219
|
if (hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} implemented more than once` });
|
|
209
220
|
if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks bypassing every system` });
|
|
@@ -235,8 +246,8 @@ if (bench && findings.length) {
|
|
|
235
246
|
// Where the median is missing or sits at/below ideal, warn caps at 1.5x ideal.
|
|
236
247
|
// Zero-ideal metrics (duplicates, inline styles): 0 is good; a small tolerance
|
|
237
248
|
// is warn; beyond that bad.
|
|
238
|
-
const ZERO_IDEAL = new Set(['exactDuplicates', 'inlineStyles']);
|
|
239
|
-
const WARN_TOLERANCE = { exactDuplicates: 2, inlineStyles: 10 };
|
|
249
|
+
const ZERO_IDEAL = new Set(['exactDuplicates', 'inlineStyles', 'nearPairs', 'important', 'neverImported']);
|
|
250
|
+
const WARN_TOLERANCE = { exactDuplicates: 2, inlineStyles: 10, nearPairs: 2, important: 5, neverImported: 2 };
|
|
240
251
|
// Absence rule also skipped for arbitrary values: zero brackets is discipline
|
|
241
252
|
// (nine of ten reputable systems sit at 0, including Tailwind-native shadcn/ui).
|
|
242
253
|
const NO_ABSENCE_RULE = new Set(['colors', 'greys', 'arbitrary']);
|
|
@@ -289,6 +300,9 @@ const bigStats = [
|
|
|
289
300
|
tile(spacingTotal, 'off-scale spacing values', 'spacing', 'a dozen deliberate exceptions'),
|
|
290
301
|
tile(hardDupes.length, 'duplicated components', 'exactDuplicates', 'should be 0'),
|
|
291
302
|
tile(inline.count, 'inline style blocks', 'inlineStyles', 'invisible to any system'),
|
|
303
|
+
tile(nearPairs.length, 'near-identical colour pairs', 'nearPairs', 'copy-paste, not decisions'),
|
|
304
|
+
tile(important.count, '!important declarations', 'important', 'the cascade admitting defeat'),
|
|
305
|
+
tile(neverImported.length, 'components never imported', 'neverImported', 'the system nobody found'),
|
|
292
306
|
tile(arbitraryCount, 'arbitrary bracket values', 'arbitrary', 'a handful of deliberate exceptions'),
|
|
293
307
|
];
|
|
294
308
|
|
|
@@ -361,6 +375,9 @@ function paletteSection() {
|
|
|
361
375
|
</div>
|
|
362
376
|
<div class="usage-bar">${cells}${restCell}</div>
|
|
363
377
|
${ramp}
|
|
378
|
+
${nearPairs.length ? `
|
|
379
|
+
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · copy-paste, not decisions`)}
|
|
380
|
+
<div class="chips-row">${nearPairs.slice(0, 8).map((pr) => `<span class="vchip" title="every channel within ${pr.d} of its twin"><i class="ndot" style="background:${esc(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${esc(pr.b.value)}"></i>${esc(pr.b.value)} ×${pr.b.count}</span>`).join('')}${nearPairs.length > 8 ? `<span class="vchip dim">+${nearPairs.length - 8} more</span>` : ''}</div></div>` : ''}
|
|
364
381
|
</div>
|
|
365
382
|
</section>`;
|
|
366
383
|
}
|
|
@@ -457,20 +474,24 @@ function offendersSection() {
|
|
|
457
474
|
}
|
|
458
475
|
|
|
459
476
|
function inlineSection() {
|
|
460
|
-
if (inline.count < 5) return '';
|
|
477
|
+
if (inline.count < 5 && important.count < 5) return '';
|
|
461
478
|
const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
<div class="
|
|
479
|
+
const importantRow = important.count ? `
|
|
480
|
+
<div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · the cascade admitting defeat`)}
|
|
481
|
+
<div class="chips-row">${important.files.map((f) => `<span class="vchip bad" title="${esc(f.file)}">${esc(basename(f.file))} ×${f.count}</span>`).join('')}</div></div>` : '';
|
|
482
|
+
const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
|
|
465
483
|
<div class="mini-card col">
|
|
466
484
|
<div class="mc-row">${fileLink(f.file)}<span class="mc-count">×${f.count}</span></div>
|
|
467
485
|
<div class="mc-track"><div class="mc-bar" style="width:${Math.round((f.count / max) * 100)}%"></div></div>
|
|
468
|
-
</div>`).join('')}</div
|
|
486
|
+
</div>`).join('')}</div>` : '';
|
|
487
|
+
return `<div class="glass pad half">
|
|
488
|
+
${sectionHead(inline.count >= 5 ? `${n(inline.count)} inline style blocks` : `${n(important.count)} !important declarations`, 'styling no system can see')}
|
|
489
|
+
${blocks}${importantRow}</div>`;
|
|
469
490
|
}
|
|
470
491
|
|
|
471
492
|
function componentsSection() {
|
|
472
493
|
const top = reusable.filter((c) => c.usageCount > 0).slice(0, 10);
|
|
473
|
-
if (!top.length) return '';
|
|
494
|
+
if (!top.length && neverImported.length < 2) return '';
|
|
474
495
|
const rows = top.map((c) => `
|
|
475
496
|
<tr><td class="mono strong"><${esc(c.name)}></td>
|
|
476
497
|
<td><span class="pill pill-mint">${c.usageCount}×</span></td>
|
|
@@ -478,7 +499,11 @@ function componentsSection() {
|
|
|
478
499
|
<td>${c.propsHint?.named?.length ? c.propsHint.named.slice(0, 5).map((p) => `<span class="chip chip-xs">${esc(p)}</span>`).join(' ') : '<span class="dim">none</span>'}</td></tr>`).join('');
|
|
479
500
|
return `<section class="glass pad">
|
|
480
501
|
${sectionHead('What you actually use', `${n(reusable.length)} components defined · top by adoption · the real system, buried in here`)}
|
|
481
|
-
|
|
502
|
+
${top.length ? `<div class="tbl-wrap"><table><thead><tr><th>component</th><th>used</th><th>defined in</th><th>props</th></tr></thead><tbody>${rows}</tbody></table></div>` : ''}
|
|
503
|
+
${neverImported.length >= 2 ? `
|
|
504
|
+
<div class="receipts">${eyebrow(`${n(neverImported.length)} components defined but never imported · candidates for deletion, or the system nobody found`)}
|
|
505
|
+
<div class="chips-row">${neverImported.slice(0, 8).map((c) => `<span class="vchip" title="${esc(c.file)}"><${esc(c.name)}></span>`).join('')}${neverImported.length > 8 ? `<span class="vchip dim">+${neverImported.length - 8} more</span>` : ''}</div>
|
|
506
|
+
<p class="sub" style="margin-top:8px">Routers, dynamic imports and barrel files can hide real usage, so treat this as a shortlist to check, not a demolition order.</p>` : ''}</section>`;
|
|
482
507
|
}
|
|
483
508
|
|
|
484
509
|
// "Where to start" — at most three moves, every one derived from this repo's
|
|
@@ -830,6 +855,8 @@ const html = `<!doctype html>
|
|
|
830
855
|
footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
|
|
831
856
|
footer .brand:hover { text-decoration:underline; }
|
|
832
857
|
footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
|
|
858
|
+
.ndot { display:inline-block; width:9px; height:9px; border-radius:3px; margin-right:4px; vertical-align:-1px; box-shadow:inset 0 0 0 1px var(--cell-ring); }
|
|
859
|
+
.nsim { color:var(--dim2); font-weight:400; padding:0 2px; }
|
|
833
860
|
.gift-sec { margin-top:16px; }
|
|
834
861
|
.gift-stage { position:relative; display:grid; place-items:center; min-height:120px; }
|
|
835
862
|
.gift-stage.open { min-height:0; }
|
|
@@ -129,6 +129,8 @@ export function harvestTokens(root, styleFiles, codeFiles) {
|
|
|
129
129
|
const twArbitrary = new Tally();
|
|
130
130
|
let inlineStyleCount = 0;
|
|
131
131
|
const inlineStyleFiles = new Map();
|
|
132
|
+
// !important is the cascade admitting defeat; counted per style file.
|
|
133
|
+
const importantFiles = new Map();
|
|
132
134
|
|
|
133
135
|
// Colours that appear in a CSS custom-property DEFINITION (--grey-100: #f5f5f5)
|
|
134
136
|
// are deliberate tokens; everything else is a hardcoded stray. The diagnosis
|
|
@@ -137,6 +139,8 @@ export function harvestTokens(root, styleFiles, codeFiles) {
|
|
|
137
139
|
const tokenDefsPerFile = new Map();
|
|
138
140
|
|
|
139
141
|
const scanCssText = (text, file) => {
|
|
142
|
+
const imp = (text.match(/!\s*important/gi) ?? []).length;
|
|
143
|
+
if (imp) importantFiles.set(file, (importantFiles.get(file) ?? 0) + imp);
|
|
140
144
|
for (const m of text.matchAll(/--[\w-]+\s*:\s*([^;{}]+)[;}]/g)) {
|
|
141
145
|
tokenDefsPerFile.set(file, (tokenDefsPerFile.get(file) ?? 0) + 1);
|
|
142
146
|
for (const c of m[1].matchAll(HEX_RE)) tokenDefined.add(normalizeHex(c[0]));
|
|
@@ -303,6 +307,11 @@ export function harvestTokens(root, styleFiles, codeFiles) {
|
|
|
303
307
|
textSizes: twTextSizes.toJSON(),
|
|
304
308
|
arbitrary: twArbitrary.toJSON(),
|
|
305
309
|
},
|
|
310
|
+
important: {
|
|
311
|
+
count: [...importantFiles.values()].reduce((a, b) => a + b, 0),
|
|
312
|
+
files: [...importantFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6)
|
|
313
|
+
.map(([file, count]) => ({ file, count })),
|
|
314
|
+
},
|
|
306
315
|
inlineStyles: {
|
|
307
316
|
count: inlineStyleCount,
|
|
308
317
|
files: [...inlineStyleFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Near-identical colour pairs (#f5f5f5 next to #f6f6f6): copy-paste, not
|
|
3
|
+
* decisions. Hex-parseable colours only; a pair is "near" when every channel
|
|
4
|
+
* sits within 8 of its twin and alpha matches. Two TOKENS sitting close are a
|
|
5
|
+
* designed ramp (every good grey scale has near neighbours) and are skipped;
|
|
6
|
+
* drift needs at least one hardcoded stray. Shared by diagnose and rules.
|
|
7
|
+
*/
|
|
8
|
+
export function hexRgb(v) {
|
|
9
|
+
const m = /^#([0-9a-f]{3,8})$/i.exec(v);
|
|
10
|
+
if (!m) return null;
|
|
11
|
+
let x = m[1];
|
|
12
|
+
if (x.length === 3 || x.length === 4) x = [...x].map((c) => c + c).join('');
|
|
13
|
+
if (x.length !== 6 && x.length !== 8) return null;
|
|
14
|
+
return { r: parseInt(x.slice(0, 2), 16), g: parseInt(x.slice(2, 4), 16), b: parseInt(x.slice(4, 6), 16),
|
|
15
|
+
a: x.length === 8 ? x.slice(6, 8) : 'ff' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function nearColorPairs(colorList) {
|
|
19
|
+
const hexes = colorList.map((c) => ({ ...c, rgb: hexRgb(c.value) })).filter((c) => c.rgb);
|
|
20
|
+
const pairs = [];
|
|
21
|
+
for (let i = 0; i < hexes.length; i++) {
|
|
22
|
+
for (let j = i + 1; j < hexes.length; j++) {
|
|
23
|
+
const a = hexes[i].rgb, b = hexes[j].rgb;
|
|
24
|
+
if (a.a !== b.a) continue;
|
|
25
|
+
if (hexes[i].isToken && hexes[j].isToken) continue;
|
|
26
|
+
const d = Math.max(Math.abs(a.r - b.r), Math.abs(a.g - b.g), Math.abs(a.b - b.b));
|
|
27
|
+
if (d > 0 && d <= 8) pairs.push({ a: hexes[i], b: hexes[j], d });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return pairs.sort((x, y) => (y.a.count + y.b.count) - (x.a.count + x.b.count));
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design-system components defined but never imported — scoped so the claim
|
|
3
|
+
* is honest. File-local subcomponents elsewhere legitimately never get
|
|
4
|
+
* imported by name; icon sets are deliberately complete; sub-exports of an
|
|
5
|
+
* adopted file are API surface, not dead code. Shared by diagnose, rules and
|
|
6
|
+
* the benchmark builder.
|
|
7
|
+
*/
|
|
8
|
+
export function neverImportedComponents(components, uiDir) {
|
|
9
|
+
const reusable = (components ?? []).filter((c) => !c.isPage);
|
|
10
|
+
const dsDirRe = uiDir
|
|
11
|
+
? new RegExp(`^${uiDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
|
|
12
|
+
: /(^|\/)(packages\/ui|design-system|ui-kit)\//;
|
|
13
|
+
const usedFiles = new Set(reusable.filter((c) => c.usageCount > 0).map((c) => c.file));
|
|
14
|
+
return reusable.filter((c) => !c.usageCount && dsDirRe.test(c.file)
|
|
15
|
+
&& !/(^|\/)icons?\//.test(c.file)
|
|
16
|
+
&& !usedFiles.has(c.file));
|
|
17
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* with a receipt from the scanned repo.
|
|
6
6
|
*/
|
|
7
7
|
import { distinctTypefaces } from '../lib/typefaces.mjs';
|
|
8
|
+
import { nearColorPairs } from './../lib/nearpairs.mjs';
|
|
9
|
+
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
8
10
|
|
|
9
11
|
export function rulesMarkdown(h) {
|
|
10
12
|
const repoName = h.profile?.name ?? 'this repo';
|
|
@@ -36,6 +38,8 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
36
38
|
} else if ((t.colors ?? []).length) {
|
|
37
39
|
rule(`There is no token file yet. Until one exists, reuse the colours already in the codebase instead of introducing new ones (${t.colors.length} distinct colours are already in play).`);
|
|
38
40
|
}
|
|
41
|
+
const near = nearColorPairs(t.colors ?? []);
|
|
42
|
+
if (near.length) rule(`Never eyeball a colour from memory: the scan found ${near.length} nearly identical pair${near.length === 1 ? '' : 's'} (like ${near[0].a.value} next to ${near[0].b.value}). Look the exact value up, or better, use its token.`);
|
|
39
43
|
const ds = h.profile?.designSystem;
|
|
40
44
|
if (ds?.kind && !['none', 'custom'].includes(ds.kind) && ds.confidence !== 'low') {
|
|
41
45
|
rule(`This repo uses ${ds.name ?? ds.kind}${h.profile.uiDir ? `; its components live in \`${h.profile.uiDir}\`` : ''}. Prefer extending it over building parallel pieces.`);
|
|
@@ -84,7 +88,14 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
84
88
|
if (icons.length) rule(`Two icon sets collide on ${icons.length} name${icons.length === 1 ? '' : 's'}. Before adding any icon, check which set the surrounding file already imports and stay with it.`);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
// ----------
|
|
91
|
+
// ---------- never-imported components ----------
|
|
92
|
+
const neverImported = neverImportedComponents(h.components, h.profile?.uiDir);
|
|
93
|
+
if (neverImported.length >= 3) {
|
|
94
|
+
section('Components nobody imports');
|
|
95
|
+
rule(`${neverImported.length} components are defined but never imported (${neverImported.slice(0, 3).map((c) => `\`<${c.name}>\``).join(', ')}…). Before writing any new component, check this list first; adopt one or flag it for deletion instead of adding another.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------- spacing ----------
|
|
88
99
|
const arbitrary = t.tailwind?.arbitrary ?? [];
|
|
89
100
|
const arbCount = arbitrary.reduce((s, a) => s + a.count, 0);
|
|
90
101
|
const offScale = (t.spacing ?? []).length;
|
|
@@ -106,6 +117,7 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
106
117
|
section('Styling discipline');
|
|
107
118
|
rule(`Never write ${reusable.length ? '`style={{ ... }}`' : 'inline `style="..."` attributes'} for static values; styling belongs to classes and tokens where the system can see it.`);
|
|
108
119
|
if ((t.inlineStyles?.count ?? 0) > 0) lines.push(` (${t.inlineStyles.count} static inline block${t.inlineStyles.count === 1 ? '' : 's'} already exist; do not add to them.)`);
|
|
120
|
+
if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of shouting over it.`);
|
|
109
121
|
rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
|
|
110
122
|
|
|
111
123
|
lines.push('');
|