roast-my-design-system 3.5.0 → 3.6.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 +3 -3
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +46 -9
- 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/rules/build.mjs +18 -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
|
|
|
@@ -127,4 +127,4 @@ Yes, the median repo is already a mess. That's the point.
|
|
|
127
127
|
|
|
128
128
|
MIT
|
|
129
129
|
|
|
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.
|
|
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.6.0"><img src="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark.png?v=3.6.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.6.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",
|
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
|
|
16
17
|
// The benchmark (Ideal-2026 norms + scanned-repo stats) ships next to the
|
|
17
18
|
// code so the page works offline; degrade gracefully when absent.
|
|
@@ -34,7 +35,7 @@ const GK_MASK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqa
|
|
|
34
35
|
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
36
|
|
|
36
37
|
// Shown in the report footer; keep in step with plugin.json when releasing.
|
|
37
|
-
const VERSION = '3.
|
|
38
|
+
const VERSION = '3.6.0';
|
|
38
39
|
|
|
39
40
|
// Two shipped skins, same layout: 'dark' (navy glass, mint accent) and
|
|
40
41
|
// 'light' (lilac wash, white glass, violet accent). --theme picks one.
|
|
@@ -151,6 +152,22 @@ const greyStrays = greys.filter((c) => !c.isToken).length;
|
|
|
151
152
|
const arbitrary = h.tokens.tailwind?.arbitrary ?? [];
|
|
152
153
|
const arbitraryCount = arbitrary.reduce((sum, a) => sum + a.count, 0);
|
|
153
154
|
|
|
155
|
+
// !important declarations: the cascade admitting defeat.
|
|
156
|
+
const important = h.tokens.important ?? { count: 0, files: [] };
|
|
157
|
+
|
|
158
|
+
const nearPairs = nearColorPairs(colors);
|
|
159
|
+
|
|
160
|
+
// Components defined but never imported — scoped to the design-system
|
|
161
|
+
// directory, where "nobody imports this" is meaningful. File-local
|
|
162
|
+
// subcomponents elsewhere legitimately never get imported by name.
|
|
163
|
+
const dsDirRe = h.profile?.uiDir
|
|
164
|
+
? new RegExp(`^${h.profile.uiDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
|
|
165
|
+
: /(^|\/)(packages\/ui|design-system|ui-kit)\//;
|
|
166
|
+
const usedFiles = new Set(reusable.filter((c) => c.usageCount > 0).map((c) => c.file));
|
|
167
|
+
const neverImported = reusable.filter((c) => !c.usageCount && dsDirRe.test(c.file)
|
|
168
|
+
&& !/(^|\/)icons?\//.test(c.file) // glyph sets are deliberately complete
|
|
169
|
+
&& !usedFiles.has(c.file)); // compound API: an adopted file's sub-exports are surface, not dead
|
|
170
|
+
|
|
154
171
|
// Benchmark helpers: for a metric value, where does this repo sit among the
|
|
155
172
|
// scanned fleet? ("more colours than 90% of scanned repos")
|
|
156
173
|
function percentile(metric, value) {
|
|
@@ -204,6 +221,8 @@ if (effGreys > 13 * 1.25) candidates.push({ ratio: effGreys / 13, text: tokenLed
|
|
|
204
221
|
? `${greyStrays} hardcoded greys outside the token set`
|
|
205
222
|
: `${greys.length} shades of grey doing the job of 13` });
|
|
206
223
|
if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} off-scale spacing values where a dozen would do` });
|
|
224
|
+
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` });
|
|
225
|
+
if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations, the cascade admitting defeat` });
|
|
207
226
|
if (iconCollisions.length >= 5) candidates.push({ ratio: 1 + iconCollisions.length / 8, text: `two icon sets collide on ${iconCollisions.length} names` });
|
|
208
227
|
if (hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} implemented more than once` });
|
|
209
228
|
if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks bypassing every system` });
|
|
@@ -361,6 +380,9 @@ function paletteSection() {
|
|
|
361
380
|
</div>
|
|
362
381
|
<div class="usage-bar">${cells}${restCell}</div>
|
|
363
382
|
${ramp}
|
|
383
|
+
${nearPairs.length ? `
|
|
384
|
+
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · copy-paste, not decisions`)}
|
|
385
|
+
<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
386
|
</div>
|
|
365
387
|
</section>`;
|
|
366
388
|
}
|
|
@@ -457,20 +479,24 @@ function offendersSection() {
|
|
|
457
479
|
}
|
|
458
480
|
|
|
459
481
|
function inlineSection() {
|
|
460
|
-
if (inline.count < 5) return '';
|
|
482
|
+
if (inline.count < 5 && important.count < 5) return '';
|
|
461
483
|
const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
<div class="
|
|
484
|
+
const importantRow = important.count ? `
|
|
485
|
+
<div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · the cascade admitting defeat`)}
|
|
486
|
+
<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>` : '';
|
|
487
|
+
const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
|
|
465
488
|
<div class="mini-card col">
|
|
466
489
|
<div class="mc-row">${fileLink(f.file)}<span class="mc-count">×${f.count}</span></div>
|
|
467
490
|
<div class="mc-track"><div class="mc-bar" style="width:${Math.round((f.count / max) * 100)}%"></div></div>
|
|
468
|
-
</div>`).join('')}</div
|
|
491
|
+
</div>`).join('')}</div>` : '';
|
|
492
|
+
return `<div class="glass pad half">
|
|
493
|
+
${sectionHead(inline.count >= 5 ? `${n(inline.count)} inline style blocks` : `${n(important.count)} !important declarations`, 'styling no system can see')}
|
|
494
|
+
${blocks}${importantRow}</div>`;
|
|
469
495
|
}
|
|
470
496
|
|
|
471
497
|
function componentsSection() {
|
|
472
498
|
const top = reusable.filter((c) => c.usageCount > 0).slice(0, 10);
|
|
473
|
-
if (!top.length) return '';
|
|
499
|
+
if (!top.length && neverImported.length < 2) return '';
|
|
474
500
|
const rows = top.map((c) => `
|
|
475
501
|
<tr><td class="mono strong"><${esc(c.name)}></td>
|
|
476
502
|
<td><span class="pill pill-mint">${c.usageCount}×</span></td>
|
|
@@ -478,7 +504,11 @@ function componentsSection() {
|
|
|
478
504
|
<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
505
|
return `<section class="glass pad">
|
|
480
506
|
${sectionHead('What you actually use', `${n(reusable.length)} components defined · top by adoption · the real system, buried in here`)}
|
|
481
|
-
|
|
507
|
+
${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>` : ''}
|
|
508
|
+
${neverImported.length >= 2 ? `
|
|
509
|
+
<div class="receipts">${eyebrow(`${n(neverImported.length)} components defined but never imported · candidates for deletion, or the system nobody found`)}
|
|
510
|
+
<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>
|
|
511
|
+
<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
512
|
}
|
|
483
513
|
|
|
484
514
|
// "Where to start" — at most three moves, every one derived from this repo's
|
|
@@ -830,17 +860,24 @@ const html = `<!doctype html>
|
|
|
830
860
|
footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
|
|
831
861
|
footer .brand:hover { text-decoration:underline; }
|
|
832
862
|
footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
|
|
863
|
+
.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); }
|
|
864
|
+
.nsim { color:var(--dim2); font-weight:400; padding:0 2px; }
|
|
833
865
|
.gift-sec { margin-top:16px; }
|
|
834
866
|
.gift-stage { position:relative; display:grid; place-items:center; min-height:120px; }
|
|
835
867
|
.gift-stage.open { min-height:0; }
|
|
836
868
|
.gift[hidden] { display:none; }
|
|
837
|
-
.gift { background:none; border:0; cursor:pointer; display:grid; place-items:center; gap:9px; padding:14px; color:var(--dim);
|
|
869
|
+
.gift { position:relative; background:none; border:0; cursor:pointer; display:grid; place-items:center; gap:9px; padding:14px; color:var(--dim);
|
|
838
870
|
transition:transform .25s ease, opacity .25s ease, filter .25s ease; }
|
|
839
871
|
.gift svg { width:72px; height:72px; }
|
|
840
872
|
.gift .ribbon { stroke:var(--accent); }
|
|
841
873
|
.gift:hover { transform:translateY(-3px) scale(1.04); color:var(--text); }
|
|
842
874
|
.gift.gone { transform:scale(.5); opacity:0; filter:blur(5px); }
|
|
843
875
|
.gift-hint { font:700 10px/1 var(--sans); letter-spacing:.18em; text-transform:uppercase; }
|
|
876
|
+
.gift::after { content:'your agent rules file, generated from this scan'; position:absolute; top:100%; left:50%;
|
|
877
|
+
transform:translateX(-50%); white-space:nowrap; font:400 11.5px/1 var(--sans); letter-spacing:0; text-transform:none;
|
|
878
|
+
color:var(--dim2); opacity:0; transition:opacity .3s ease .6s; pointer-events:none; }
|
|
879
|
+
.gift:hover::after { opacity:1; }
|
|
880
|
+
.gift.gone::after { opacity:0; transition:none; }
|
|
844
881
|
.gift-pop { position:absolute; inset:0; pointer-events:none; }
|
|
845
882
|
.gift-pop i { position:absolute; left:50%; top:42%; width:7px; height:10px; border-radius:2px; opacity:0;
|
|
846
883
|
animation:gpop .85s cubic-bezier(.16,.8,.32,1) forwards; }
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
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';
|
|
8
9
|
|
|
9
10
|
export function rulesMarkdown(h) {
|
|
10
11
|
const repoName = h.profile?.name ?? 'this repo';
|
|
@@ -36,6 +37,8 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
36
37
|
} else if ((t.colors ?? []).length) {
|
|
37
38
|
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
39
|
}
|
|
40
|
+
const near = nearColorPairs(t.colors ?? []);
|
|
41
|
+
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
42
|
const ds = h.profile?.designSystem;
|
|
40
43
|
if (ds?.kind && !['none', 'custom'].includes(ds.kind) && ds.confidence !== 'low') {
|
|
41
44
|
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 +87,20 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
84
87
|
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
88
|
}
|
|
86
89
|
|
|
87
|
-
// ----------
|
|
90
|
+
// ---------- never-imported components ----------
|
|
91
|
+
const dsDirRe = h.profile?.uiDir
|
|
92
|
+
? new RegExp(`^${h.profile.uiDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
|
|
93
|
+
: /(^|\/)(packages\/ui|design-system|ui-kit)\//;
|
|
94
|
+
const usedFiles = new Set(reusable.filter((c) => c.usageCount > 0).map((c) => c.file));
|
|
95
|
+
const neverImported = reusable.filter((c) => !c.usageCount && dsDirRe.test(c.file)
|
|
96
|
+
&& !/(^|\/)icons?\//.test(c.file) // glyph sets are deliberately complete
|
|
97
|
+
&& !usedFiles.has(c.file)); // compound API: an adopted file's sub-exports are surface, not dead
|
|
98
|
+
if (neverImported.length >= 3) {
|
|
99
|
+
section('Components nobody imports');
|
|
100
|
+
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.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------- spacing ----------
|
|
88
104
|
const arbitrary = t.tailwind?.arbitrary ?? [];
|
|
89
105
|
const arbCount = arbitrary.reduce((s, a) => s + a.count, 0);
|
|
90
106
|
const offScale = (t.spacing ?? []).length;
|
|
@@ -106,6 +122,7 @@ const repoName = h.profile?.name ?? 'this repo';
|
|
|
106
122
|
section('Styling discipline');
|
|
107
123
|
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
124
|
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.)`);
|
|
125
|
+
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
126
|
rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
|
|
110
127
|
|
|
111
128
|
lines.push('');
|