roast-my-design-system 9.6.0 → 9.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 +2 -2
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +275 -37
- package/skills/roast-my-design-system/scripts/harvest/coloruse.mjs +856 -0
- package/skills/roast-my-design-system/scripts/harvest/index.mjs +57 -3
- package/skills/roast-my-design-system/scripts/harvest/tokens.mjs +16 -10
- package/skills/roast-my-design-system/scripts/harvest/walk.mjs +14 -4
- package/skills/roast-my-design-system/scripts/lib/fixprompt.mjs +13 -2
- package/skills/roast-my-design-system/scripts/lib/kitlist.mjs +60 -0
- package/skills/roast-my-design-system/scripts/lib/kitpaint.mjs +142 -8
- package/skills/roast-my-design-system/scripts/lib/palette.mjs +28 -11
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/engine.mjs +3 -1
- package/skills/roast-my-design-system/scripts/mcp/tools.mjs +57 -4
- package/skills/roast-my-design-system/scripts/profiles/antd.mjs +4 -0
- package/skills/roast-my-design-system/scripts/profiles/chakra.mjs +3 -0
- package/skills/roast-my-design-system/scripts/profiles/index.mjs +16 -3
- package/skills/roast-my-design-system/scripts/profiles/kit-common.mjs +118 -12
- package/skills/roast-my-design-system/scripts/profiles/mantine.mjs +3 -0
- package/skills/roast-my-design-system/scripts/profiles/mui.mjs +4 -0
- package/skills/roast-my-design-system/scripts/profiles/shadcn.mjs +25 -6
- package/skills/roast-my-design-system/scripts/profiles/tailwind.mjs +10 -5
- package/skills/roast-my-design-system/scripts/rules/build.mjs +50 -8
package/README.md
CHANGED
|
@@ -142,11 +142,11 @@ Eleven reports, hosted exactly as the tool writes them. Every number determinist
|
|
|
142
142
|
|
|
143
143
|
The full report for vercel/ai-chatbot. The verdict answers what the agent will learn here, the gap section comes first, then "What the repo teaches the agent", Claude's read of the scan:
|
|
144
144
|
|
|
145
|
-

|
|
146
146
|
|
|
147
147
|
The same report in light mode (one file, built-in toggle):
|
|
148
148
|
|
|
149
|
-

|
|
150
150
|
|
|
151
151
|
## Why the numbers hold
|
|
152
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.0",
|
|
4
4
|
"mcpName": "io.github.gregkozakiewicz/roast-my-design-system",
|
|
5
5
|
"description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 112 public repos, reads React and web components (Stencil, Lit), hands you a copy-paste fix prompt for each top finding, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,6 +36,8 @@ import { ownSpacing, profileOf, installedOf, splitArbitrary, installedByOwner }
|
|
|
36
36
|
import { KITS } from '../profiles/kit-common.mjs';
|
|
37
37
|
import { rulesOn as lintRulesOn, describeRule as lintDescribe, entryMatcher as lintMatcher, RULE_TILE as LINT_TILE } from '../profiles/shadcn-lint.mjs';
|
|
38
38
|
import { publishesLine } from '../profiles/registry.mjs';
|
|
39
|
+
import { CONTRACT_MIN } from '../lib/palette.mjs';
|
|
40
|
+
import { GREY_HUE_RE } from '../harvest/paint.mjs';
|
|
39
41
|
|
|
40
42
|
// The benchmark and every judgement made against it live in score.mjs; this
|
|
41
43
|
// file only draws. The same numbers reach summary.json through scoreHarvest.
|
|
@@ -55,6 +57,13 @@ const h = JSON.parse(readFileSync(inPath, 'utf8'));
|
|
|
55
57
|
// the shadcn repos in the fleet).
|
|
56
58
|
const B = benchHelpers(bench, profileOf(h).kind);
|
|
57
59
|
const { percentile, cleanerPct, ideal, median, displayAvg, refMedian } = B;
|
|
60
|
+
// The group the medians come from, by name: a registry is compared with the
|
|
61
|
+
// shadcn repos, an MUI product with the MUI ones. The verdict said "the
|
|
62
|
+
// median of 34 scanned repos" when the medians came from a kit's own group,
|
|
63
|
+
// on 51 of 204 fleet scans, and the tiles said "Avg registry repo" for a
|
|
64
|
+
// group that does not exist (2026-10-01).
|
|
65
|
+
const GROUP_NAMES = { shadcn: 'shadcn', registry: 'shadcn', tailwind: 'Tailwind', mui: 'MUI', mantine: 'Mantine', chakra: 'Chakra', antd: 'Ant Design' };
|
|
66
|
+
const benchGroup = B.sliceInfo ? { name: GROUP_NAMES[B.sliceInfo.kind] ?? B.sliceInfo.kind, repoCount: B.sliceInfo.repoCount } : null;
|
|
58
67
|
const healthOf = makeHealthOf(B);
|
|
59
68
|
const M = coreMetrics(h);
|
|
60
69
|
|
|
@@ -260,6 +269,14 @@ const fresh = P.isShadcn && P.shadcn?.fresh?.fresh === true;
|
|
|
260
269
|
if (P.isShadcn) arbitrary = splitArbitrary(arbitrary, installedOf(P)).own;
|
|
261
270
|
const breakdown = scoreBreakdown(h, bench);
|
|
262
271
|
const neverImported = neverImportedComponents(h.components, P.uiDir);
|
|
272
|
+
// In a shadcn catalogue folder the unused list holds two things: shadcn's
|
|
273
|
+
// stock, installed and waiting to be used, and components the team wrote
|
|
274
|
+
// itself that nothing imports. Both were called stock until 9.7.0: 178 of
|
|
275
|
+
// 2,182 such components in 16 shadcn repos are the team's own (cal.com 38,
|
|
276
|
+
// 2026-10-01). Neither is scored; the words now tell them apart.
|
|
277
|
+
const isInstalled = installedOf(P);
|
|
278
|
+
const neverStock = P.vendoredUi ? neverImported.filter((c) => isInstalled(c.file)) : [];
|
|
279
|
+
const neverOwn = P.vendoredUi ? neverImported.filter((c) => !isInstalled(c.file)) : neverImported;
|
|
263
280
|
|
|
264
281
|
// Measurability and kind, decided once in profiles/ (telekom/scale, 2026-09-01):
|
|
265
282
|
// when the component detector could not read this repo's stack, the component
|
|
@@ -372,7 +389,8 @@ if (bench && findings.length && !fresh) {
|
|
|
372
389
|
const core = [['colors', tokenLed ? colorStrays : colors.length], ['greys', tokenLed ? greyStrays : greys.length],
|
|
373
390
|
['spacing', spacingTotal], ['typefaces', typefaces.length], ['exactDuplicates', hardDupes.length], ['inlineStyles', inline.count]];
|
|
374
391
|
const worse = core.filter(([m, v]) => median(m) !== null && v > median(m)).length;
|
|
375
|
-
|
|
392
|
+
const pool = benchGroup ? `${benchGroup.repoCount} scanned ${benchGroup.name} repos` : `${bench.repoCount} scanned repos`;
|
|
393
|
+
if (worse >= 3) verdict += ` Messier than the median of ${pool} on ${worse} of ${core.length} core metrics. And the median repo is already a mess.`;
|
|
376
394
|
}
|
|
377
395
|
|
|
378
396
|
// ---------- health scoring ----------
|
|
@@ -429,13 +447,15 @@ function tile(t, pLabel, pMetric, pFallback) {
|
|
|
429
447
|
const avgNote = pct !== null && pct >= 60 && health !== 'good' ? ` · messier than ${pct}%`
|
|
430
448
|
: clean !== null && clean >= 60 ? ` · cleaner than ${clean}%` : '';
|
|
431
449
|
const av = displayAvg(metric);
|
|
432
|
-
if (av !== null) rows.push(row(
|
|
450
|
+
if (av !== null) rows.push(row(benchGroup ? `Avg ${benchGroup.name} repo` : 'Avg Design System', `${n(av)}${avgNote}`, av, value));
|
|
433
451
|
if (rm !== null && (rm > 0 || ZERO_IDEAL.has(metric) || metric === 'arbitrary')) rows.push(row('Reputable systems', n(rm), rm, value));
|
|
434
452
|
// A vendored catalogue is never judged for what it left on the shelf, and a
|
|
435
453
|
// library's internal use says nothing about adoption: shown, not scored.
|
|
436
454
|
if (health === 'info' && metric === 'neverImported') {
|
|
437
455
|
rows.push({ label: isLibrary
|
|
438
456
|
? 'library: internal use only, downstream consumers invisible'
|
|
457
|
+
: neverOwn.length
|
|
458
|
+
? `${neverStock.length ? `${n(neverStock.length)} installed by the shadcn CLI and ` : ''}${n(neverOwn.length)} of the team's own, not used yet; not scored`
|
|
439
459
|
: 'catalogue stock: installed by the shadcn CLI, not used yet', val: '', dir: '' });
|
|
440
460
|
}
|
|
441
461
|
if (health === 'info' && metric === 'paintTin') {
|
|
@@ -520,16 +540,7 @@ function paletteSection() {
|
|
|
520
540
|
const strays = colors.length - tokens;
|
|
521
541
|
const tokenPct = Math.round((tokens / colors.length) * 100);
|
|
522
542
|
|
|
523
|
-
|
|
524
|
-
// Beyond 60 segments the bar turns to noise; fold the tail into one cell.
|
|
525
|
-
const sorted = [...colors].sort((a, b) => b.count - a.count);
|
|
526
|
-
const shown = sorted.slice(0, 60);
|
|
527
|
-
const rest = sorted.slice(60);
|
|
528
|
-
const cells = shown.map((c) => {
|
|
529
|
-
const src = c.files?.[0] ? ` in ${c.files[0].file}` : '';
|
|
530
|
-
return `<div class="uc${c.isToken ? '' : ' stray'}" style="background:${swatch(c.value)};flex-grow:${Math.max(c.count, 1)}" title="${esc(c.value)} ×${c.count}${esc(src)}">${c.isToken ? '' : '<i></i>'}</div>`;
|
|
531
|
-
}).join('');
|
|
532
|
-
const restCell = rest.length ? `<div class="uc uc-rest" style="flex-grow:${Math.max(rest.reduce((s, c) => s + c.count, 0), 1)}" title="${rest.length} more colours">+${rest.length}</div>` : '';
|
|
543
|
+
const usage = colourUsage();
|
|
533
544
|
|
|
534
545
|
const rampFor = (list) => `
|
|
535
546
|
<div class="grey-strip">${list.map((c) => `<div class="grey-cell" style="background:${swatch(c.value)}" title="${esc(c.value)} ×${c.count}"></div>`).join('')}</div>
|
|
@@ -555,10 +566,11 @@ function paletteSection() {
|
|
|
555
566
|
</div>
|
|
556
567
|
<div class="glass pal-usage">
|
|
557
568
|
<div class="pal-usage-head">
|
|
558
|
-
<div>${eyebrow('Colour usage')}<div class="h3d"
|
|
559
|
-
<div class="legend"
|
|
569
|
+
<div>${eyebrow('Colour usage')}<div class="h3d">${usage.subtitle}</div></div>
|
|
570
|
+
<div class="legend">${usage.legend}</div>
|
|
560
571
|
</div>
|
|
561
|
-
<div class="usage-bar">${cells}
|
|
572
|
+
<div class="usage-bar">${usage.cells}</div>
|
|
573
|
+
${usage.line}
|
|
562
574
|
${ramp}
|
|
563
575
|
${nearPairs.length ? `
|
|
564
576
|
<div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · each pair is 1 colour recorded twice`)}
|
|
@@ -568,6 +580,138 @@ function paletteSection() {
|
|
|
568
580
|
</section>`;
|
|
569
581
|
}
|
|
570
582
|
|
|
583
|
+
// The Colour usage bar. Since 9.7.0 it is sized by USE (harvest/coloruse.mjs):
|
|
584
|
+
// a theme class, a var() read or a read of the kit's theme joins its colour's
|
|
585
|
+
// segment, a Tailwind palette class is a palette cell or a stray as the one
|
|
586
|
+
// palette rule says, and a definition is not a use. Until then it was sized by
|
|
587
|
+
// literal writes, the theme's own definitions included, so bg-primary on every
|
|
588
|
+
// button drew primary as one of the thinnest segments. Where the repo reads
|
|
589
|
+
// its theme mostly through code the pass cannot follow, and for a harvest
|
|
590
|
+
// written before 9.7.0, the bar stays sized by written values and says so.
|
|
591
|
+
// Beyond 60 segments the bar turns to noise; the tail folds into one cell
|
|
592
|
+
// per state, so the visible proportions stay true.
|
|
593
|
+
function colourUsage() {
|
|
594
|
+
const use = h.colourUse ?? null;
|
|
595
|
+
const written = () => {
|
|
596
|
+
const sorted = [...colors].sort((a, b) => b.count - a.count);
|
|
597
|
+
const shown = sorted.slice(0, 60);
|
|
598
|
+
const rest = sorted.slice(60);
|
|
599
|
+
const cells = shown.map((c) => {
|
|
600
|
+
const src = c.files?.[0] ? ` in ${c.files[0].file}` : '';
|
|
601
|
+
return `<div class="uc${c.isToken ? '' : ' stray'}" style="background:${swatch(c.value)};flex-grow:${Math.max(c.count, 1)}" title="${esc(c.value)} ×${c.count}${esc(src)}">${c.isToken ? '' : '<i></i>'}</div>`;
|
|
602
|
+
}).join('');
|
|
603
|
+
const restCell = rest.length ? `<div class="uc uc-rest" style="flex-grow:${Math.max(rest.reduce((s, c) => s + c.count, 0), 1)}" title="${rest.length} more colours">+${rest.length}</div>` : '';
|
|
604
|
+
return { cells: cells + restCell, subtitle: 'Sized by how often each value is written',
|
|
605
|
+
legend: '<span><i class="lg lg-tok"></i> token</span><span><i class="lg lg-stray"></i> stray</span>' };
|
|
606
|
+
};
|
|
607
|
+
if (!use || !use.totals?.total) return { ...written(), line: '' };
|
|
608
|
+
if (use.fallback) {
|
|
609
|
+
const pct = Math.round(use.fallback.share * 100);
|
|
610
|
+
const words = { sass: 'Sass variables', less: 'Less variables', 'js-theme': 'JavaScript theme objects' };
|
|
611
|
+
const what = listWords(use.fallback.why.map((w) => words[w]).filter(Boolean));
|
|
612
|
+
return { ...written(), line: `<p class="sub use-line">${pct} in every 100 colour uses here go through ${what}. This bar cannot follow those yet, so it counts written values instead.</p>` };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const kitName = P.kit?.name ?? 'kit';
|
|
616
|
+
// a dark: override on a theme whose configured sheet sets dark values
|
|
617
|
+
const darkRows = (P.shadcn?.sheet?.darkRows ?? 0) > 0;
|
|
618
|
+
const how = (c) => listWords([c.classes ? `${n(c.classes)} as classes` : '', c.vars ? `${n(c.vars)} through var()` : '', c.kit ? `${n(c.kit)} through the ${kitName} theme` : ''].filter(Boolean));
|
|
619
|
+
const label = (c) => {
|
|
620
|
+
if (c.named === 'kit') return `${c.names[0]} ×${n(c.weight)}, read by name from the ${kitName} theme`;
|
|
621
|
+
if (c.named === 'outside') return `${c.names[0]} ×${n(c.weight)}, read by name; its value is not a colour this scan can read`;
|
|
622
|
+
if (c.state === 'token') return `${c.value}${c.names.length ? ` (${c.names.join(', ')})` : ''} ×${n(c.weight)}: ${how(c)}`;
|
|
623
|
+
if (c.state === 'palette') return `${c.names[0]} ×${n(c.weight)} as Tailwind palette classes`;
|
|
624
|
+
if (c.written) return `${c.value} ×${n(c.weight)} written by hand${c.names.length ? `; the theme names it ${c.names[0]}` : c.token ? '; a token already holds this value' : ''}`;
|
|
625
|
+
if (c.dark) return `${c.names[0]} ×${n(c.weight)} as dark: overrides written by hand${darkRows ? ', where the theme already sets its own dark-mode colours' : ''}`;
|
|
626
|
+
return `${c.names[0]} ×${n(c.weight)} as palette classes where the theme names its own colours`;
|
|
627
|
+
};
|
|
628
|
+
const MARK = { token: '', palette: ' pal', stray: ' stray' };
|
|
629
|
+
const cells = use.segments.map((c) => {
|
|
630
|
+
const src = c.file ? ` · most in ${c.file}` : '';
|
|
631
|
+
const bg = c.named ? '' : `background:${swatch(c.value)};`;
|
|
632
|
+
return `<div class="uc${MARK[c.state]}${c.named ? ' named' : ''}" style="${bg}flex-grow:${Math.max(c.weight, 1)}" title="${esc(label(c))}${esc(src)}">${c.state === 'token' ? '' : '<i></i>'}</div>`;
|
|
633
|
+
}).join('');
|
|
634
|
+
const REST = { token: 'reached by name', palette: 'as Tailwind palette classes', stray: 'as strays' };
|
|
635
|
+
const rest = ['token', 'palette', 'stray'].filter((k) => use.rest[k].count)
|
|
636
|
+
.map((k) => `<div class="uc uc-rest${MARK[k]}" style="flex-grow:${Math.max(use.rest[k].weight, 1)}" title="${n(use.rest[k].count)} more colours ${REST[k]}, ${n(use.rest[k].weight)} uses">+${n(use.rest[k].count)}${k === 'token' ? '' : '<i></i>'}</div>`).join('');
|
|
637
|
+
|
|
638
|
+
// A legend entry for each kind of cell the bar holds. "token" is a colour
|
|
639
|
+
// drawn with its swatch, here or folded into the "+N" tail; a bar whose
|
|
640
|
+
// token cells and tail are all names with no colour shown has none
|
|
641
|
+
// (Unleash: every cell an MUI read). The dotted "named" entry stands for
|
|
642
|
+
// the dotted cells drawn.
|
|
643
|
+
const has = (state) => use.segments.some((c) => c.state === state) || use.rest[state].count > 0;
|
|
644
|
+
const restSwatched = use.rest.token.count - (use.rest.token.named?.count ?? 0);
|
|
645
|
+
const swatched = use.segments.some((c) => c.state === 'token' && !c.named) || restSwatched > 0;
|
|
646
|
+
const named = use.segments.some((c) => c.state === 'token' && c.named);
|
|
647
|
+
const legend = [
|
|
648
|
+
swatched ? '<span><i class="lg lg-tok"></i> token</span>' : '',
|
|
649
|
+
named ? '<span><i class="lg lg-named"></i> named, colour not shown</span>' : '',
|
|
650
|
+
has('palette') ? '<span><i class="lg lg-pal"></i> Tailwind palette</span>' : '',
|
|
651
|
+
has('stray') ? '<span><i class="lg lg-stray"></i> stray</span>' : '',
|
|
652
|
+
].join('');
|
|
653
|
+
|
|
654
|
+
// Of every 100 uses: three shares that add up to 100 (largest remainder)
|
|
655
|
+
const raw = [use.share.token, use.share.palette, use.share.stray].map((x) => x * 100);
|
|
656
|
+
const [t, p, st] = (() => {
|
|
657
|
+
const out = raw.map(Math.floor);
|
|
658
|
+
const order = raw.map((x, i) => [x - Math.floor(x), i]).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
|
|
659
|
+
const short = 100 - out.reduce((a, b) => a + b, 0);
|
|
660
|
+
for (let k = 0; k < short; k++) out[order[k][1]] += 1;
|
|
661
|
+
return out;
|
|
662
|
+
})();
|
|
663
|
+
const T = use.totals;
|
|
664
|
+
// what a stray is here, said once after the shares
|
|
665
|
+
const strayWhat = T.written && T.paletteStray ? 'a value written by hand, or a palette class where the theme has its own colour'
|
|
666
|
+
: T.written ? 'a value written by hand' : 'a palette class where the theme has its own colour';
|
|
667
|
+
const parts = [
|
|
668
|
+
t ? `${t} use${t === 1 ? 's' : ''} a theme colour by name` : '',
|
|
669
|
+
p ? `${p} use${p === 1 ? 's' : ''} Tailwind's palette` : '',
|
|
670
|
+
st ? `${st} ${st === 1 ? 'is a stray' : 'are strays'}` : '',
|
|
671
|
+
].filter(Boolean);
|
|
672
|
+
const sentences = [`Of every 100 colour uses, ${listWords(parts)}.${st ? ` A stray is ${strayWhat}.` : ''}`];
|
|
673
|
+
// why the palette cells are not strays, in the one rule's terms
|
|
674
|
+
if (p) {
|
|
675
|
+
const w = use.paletteWhy ?? {};
|
|
676
|
+
if (!P.palette && (w.off ?? 0) > 0) {
|
|
677
|
+
sentences.push(ds.kind === 'shadcn' && ds.cssVariables === false
|
|
678
|
+
? "shadcn is set up without CSS variables here, so Tailwind's palette is the theme."
|
|
679
|
+
// a shadcn repo with no theme the rule can read still has its
|
|
680
|
+
// palette classes counted by the off-theme colours tile (workout-cool)
|
|
681
|
+
: P.isShadcn && (P.shadcn?.paint?.tin?.uses ?? 0) > 0
|
|
682
|
+
? "No theme was found to check these palette classes against, so the bar shows them as Tailwind's palette. The off-theme colours tile still counts palette classes."
|
|
683
|
+
: 'The report does not check palette classes against a theme here, so none of them count as strays.');
|
|
684
|
+
}
|
|
685
|
+
const fam = P.palette?.families ?? null;
|
|
686
|
+
// white and black apart, and which dark: overrides are strays on the bar
|
|
687
|
+
const wb = [(w.white ?? 0) > 0 ? 'white' : '', (w.black ?? 0) > 0 ? 'black' : ''].filter(Boolean);
|
|
688
|
+
const left = [
|
|
689
|
+
// the rule flags dark: backgrounds, text and borders; every other
|
|
690
|
+
// dark: white or black (dark:via-white, dark:ring-white) stays plain
|
|
691
|
+
wb.length ? `plain ${listWords(wb)}${T.paletteStrayDark ? ', except dark: backgrounds, text and borders' : ''}` : '',
|
|
692
|
+
w.family && fam ? (!fam.grey ? 'greys (the theme names no grey)' : 'colours other than grey (the theme names only greys)') : '',
|
|
693
|
+
w.apply ? '@apply lines in stylesheets' : '',
|
|
694
|
+
w.sides ? `side borders and ring offsets${w.sidesTop ? `, such as ${w.sidesTop}` : ''}` : '',
|
|
695
|
+
w.installed ? "shadcn's own components" : '',
|
|
696
|
+
].filter(Boolean);
|
|
697
|
+
// semicolons: "plain white and black" carries its own "and"
|
|
698
|
+
if (P.palette && left.length) sentences.push(`Palette classes not counted as strays: ${left.join('; ')}.`);
|
|
699
|
+
}
|
|
700
|
+
if (use.scope === 'installed') sentences.push("Your own code uses colour fewer than 20 times so far, so shadcn's installed components are counted too.");
|
|
701
|
+
else if (P.isShadcn && use.doorFiles) sentences.push("shadcn's own components are left out, as they are in the score.");
|
|
702
|
+
return {
|
|
703
|
+
cells: cells + rest, legend,
|
|
704
|
+
subtitle: 'Sized by how often the code uses each colour',
|
|
705
|
+
// the shares in one line; how they were counted folded under it, the
|
|
706
|
+
// same fold as "why this matters" (Greg, 1 October)
|
|
707
|
+
line: `<p class="sub use-line">${esc(sentences[0])}</p>${sentences.length > 1 ? `<div class="whywrap"><button type="button" class="whybtn" data-why="colouruse">${WHY_ICON}<span class="why-lab">how this is counted</span></button><div class="whytext" hidden id="why-colouruse">${sentences.slice(1).map((x) => `<p>${esc(x)}</p>`).join('')}</div></div>` : ''}`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
// "a", "a and b", "a, b and c"
|
|
711
|
+
function listWords(xs) {
|
|
712
|
+
return xs.length <= 1 ? (xs[0] ?? '') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`;
|
|
713
|
+
}
|
|
714
|
+
|
|
571
715
|
// Agent traps: findings that do not just sit there but multiply, because an
|
|
572
716
|
// agent reads the repo as instruction. Copy only, no new measurement, and a
|
|
573
717
|
// trap renders only when its mechanism is real for this repo, so the marker
|
|
@@ -914,12 +1058,12 @@ function adoptionMap(tiled) {
|
|
|
914
1058
|
// the obituary column. "Untouched since 2023" needs no decoding, and a heavy
|
|
915
1059
|
// year reads as what it is, the fossil of one abandoned effort. Orphans
|
|
916
1060
|
// without a date (no git) fall back to one plain row, exactly as before.
|
|
917
|
-
function orphanRows() {
|
|
918
|
-
const shown =
|
|
1061
|
+
function orphanRows(list = neverImported) {
|
|
1062
|
+
const shown = list.slice(0, 16);
|
|
919
1063
|
const chip = (c) => `<span class="vchip" title="${esc(c.file)}${c.lastTouched ? ` · untouched since ${esc(c.lastTouched)}` : ''}"><${esc(c.name)}></span>`;
|
|
920
|
-
const more =
|
|
1064
|
+
const more = list.length > 16 ? `<span class="vchip dim">+${list.length - 16} more</span>` : '';
|
|
921
1065
|
const dated = shown.filter((c) => c.lastTouched);
|
|
922
|
-
if (dated.length < 2) return `<div class="chips-row">${shown.slice(0, 8).map(chip).join('')}${
|
|
1066
|
+
if (dated.length < 2) return `<div class="chips-row">${shown.slice(0, 8).map(chip).join('')}${list.length > 8 ? `<span class="vchip dim">+${list.length - 8} more</span>` : ''}</div>`;
|
|
923
1067
|
const byYear = new Map();
|
|
924
1068
|
for (const c of shown) {
|
|
925
1069
|
const k = c.lastTouched ? c.lastTouched.slice(0, 4) : 'no git date';
|
|
@@ -959,11 +1103,18 @@ function componentsSection() {
|
|
|
959
1103
|
${fresh ? '<p class="sub">No code of your own uses the kit yet. The counts below are the components using each other.</p>' : ''}
|
|
960
1104
|
${onceUsed.length && !fresh ? `<p class="sub">${n(onceUsed.length)} component${onceUsed.length === 1 ? ' is' : 's are'} imported exactly once: ${onceUsed.slice(0, 6).map((c) => `<${esc(c.name)}>`).join(', ')}${onceUsed.length > 6 ? ` and ${onceUsed.length - 6} more` : ''}. Quiet corners, not yet a system.</p>` : ''}
|
|
961
1105
|
${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>` : ''}
|
|
962
|
-
${neverImported.length >= 2 ? `
|
|
963
|
-
<div class="receipts">${eyebrow(
|
|
1106
|
+
${neverImported.length >= 2 && vendoredUi ? `${neverStock.length ? `
|
|
1107
|
+
<div class="receipts">${eyebrow(`${n(neverStock.length)} shadcn component${neverStock.length === 1 ? '' : 's'} installed but not used yet · added by the shadcn CLI, not written by this team`)}
|
|
1108
|
+
${orphanRows(neverStock)}
|
|
1109
|
+
<p class="sub" style="margin-top:8px">Use one before you build anything new. This list is counted and shown, and takes nothing off the score.</p></div>` : ''}${neverOwn.length ? `
|
|
1110
|
+
<div class="receipts">${eyebrow(`${n(neverOwn.length)} component${neverOwn.length === 1 ? '' : 's'} the team wrote in ${esc(P.uiDir)} ${neverOwn.length === 1 ? 'is' : 'are'} never imported · an agent may use ${neverOwn.length === 1 ? 'it' : 'one of these'} by mistake`)}
|
|
1111
|
+
${orphanRows(neverOwn)}
|
|
1112
|
+
<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. Counted and shown, not scored.</p>
|
|
1113
|
+
${whyToggle('neverImported')}</div>` : ''}` : neverImported.length >= 2 ? `
|
|
1114
|
+
<div class="receipts">${eyebrow(isLibrary ? `${n(neverImported.length)} components unused internally · showroom stock to review, not dead weight: consumers in other repos are invisible from here` : `${n(neverImported.length)} components defined but never imported · an agent may use one of these by mistake`)}
|
|
964
1115
|
${orphanRows()}
|
|
965
|
-
<p class="sub" style="margin-top:8px"
|
|
966
|
-
${isLibrary
|
|
1116
|
+
<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>
|
|
1117
|
+
${isLibrary ? '' : whyToggle('neverImported')}` : ''}</section>`;
|
|
967
1118
|
}
|
|
968
1119
|
|
|
969
1120
|
// "Where to start" — at most three moves, every one derived from this repo's
|
|
@@ -1164,12 +1315,41 @@ function whereToStartSection() {
|
|
|
1164
1315
|
const pt = P.shadcn.paint;
|
|
1165
1316
|
if (pt.tin.uses >= 10) {
|
|
1166
1317
|
const s0 = pt.tin.samples[0], f0 = productFirst(pt.tin.top)[0];
|
|
1167
|
-
// The swap only works when the theme
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1172
|
-
|
|
1318
|
+
// The swap only works when the theme holds the variables. Which first
|
|
1319
|
+
// move fits depends on where the theme lives (9.7.0). Until then the
|
|
1320
|
+
// gate read the configured sheet alone, and sent documenso and
|
|
1321
|
+
// taxonomy (shadcn's rows in a stylesheet no config names) and
|
|
1322
|
+
// formbricks (a v4 theme of its own, written straight into @theme) to
|
|
1323
|
+
// "Decide what the product paints from", telling the first two their
|
|
1324
|
+
// theme defined none of shadcn's colour variables.
|
|
1325
|
+
// - a configured sheet holding shadcn's rows, or utility-class mode: repaint
|
|
1326
|
+
// - a configured sheet that is a theme of the repo's own (3 names or
|
|
1327
|
+
// more) lacking shadcn's greys: map the palette onto it first
|
|
1328
|
+
// - no configured sheet, but shadcn's rows in a stylesheet: repaint
|
|
1329
|
+
// - otherwise the palette is the system today: decide first
|
|
1330
|
+
const sc = P.shadcn ?? {};
|
|
1331
|
+
const tr = sc.themeRead ?? null;
|
|
1332
|
+
const missing = tr ? ['muted-foreground', 'border', 'background', 'foreground'].filter((r) => !tr.names.includes(r)) : [];
|
|
1333
|
+
const gate = sc.kit?.cssVariables === false ? 'repaint'
|
|
1334
|
+
: sc.sheet?.found
|
|
1335
|
+
? ((sc.sheet.shadcnPresent ?? 0) >= CONTRACT_MIN ? 'repaint'
|
|
1336
|
+
: tr && tr.file === sc.sheet.file && tr.count >= 3 ? (missing.length ? 'map' : 'repaint') : 'decide')
|
|
1337
|
+
: (sc.contract?.rows ?? 0) >= CONTRACT_MIN ? 'repaint' : 'decide';
|
|
1338
|
+
// a palette grey among the samples (white and black are not greys here)
|
|
1339
|
+
const isGreyClass = (v) => GREY_HUE_RE.test(v) && !/-(?:white|black)(?:\/|$)/.test(v);
|
|
1340
|
+
const grey = (pt.tin.samples ?? []).map((x) => x.value).find(isGreyClass) ?? null;
|
|
1341
|
+
const textGrey = (pt.tin.samples ?? []).map((x) => x.value.replace(/^(?:[\w-]+:)*/, '').replace(/\/\d+$/, '')).find((v) => /^text-/.test(v) && isGreyClass(v)) ?? 'text-slate-500';
|
|
1342
|
+
// a theme's names, one per family (brand, not brand-light and brand-dark),
|
|
1343
|
+
// in the theme's own order
|
|
1344
|
+
const families = tr ? [...tr.names.reduce((m, nm) => { const k = nm.split('-')[0]; if (!m.has(k) || nm.length < m.get(k).length) m.set(k, nm); return m; }, new Map()).values()] : [];
|
|
1345
|
+
const themeNames = !tr ? ''
|
|
1346
|
+
: tr.count <= 4 ? `${n(tr.count)} colours of its own (${listWords(tr.names)})`
|
|
1347
|
+
: families.length >= 4 ? `${n(tr.count)} colours of its own, from ${listWords(families.slice(0, 3))} to ${families.at(-1)}`
|
|
1348
|
+
: `${n(tr.count)} colours of its own, among them ${listWords(families)}`;
|
|
1349
|
+
if (gate === 'map') c.push({ score: 20 + pt.tin.per100 / 4, metric: 'paintTin', after: 0,
|
|
1350
|
+
title: `Map the palette onto the theme you already have`,
|
|
1351
|
+
sub: `${esc(s0.value)} appears ${s0.count} times.${f0 ? ` ${esc(basename(f0.file))} alone has ${f0.count} palette classes.` : ''} ${esc(tr.file)} already names ${esc(themeNames)}. ${grey ? 'It does not name the greys these classes replace' : "It does not name shadcn's colours for text, borders and surfaces"}: ${listWords(missing.slice(0, 3))}. Add ${grey ? 'those greys' : 'them'} to the theme first, then change the classes one file at a time.${missing.includes('muted-foreground') ? ` If you change <code>${esc(textGrey)}</code> to <code>text-muted-foreground</code> before that, the text will have no colour.` : ''}` });
|
|
1352
|
+
else c.push(gate === 'repaint'
|
|
1173
1353
|
? { score: 20 + pt.tin.per100 / 4, metric: 'paintTin', after: 0,
|
|
1174
1354
|
title: `Repaint the ${n(pt.tin.uses)} colours from outside the theme`,
|
|
1175
1355
|
sub: `${esc(s0.value)} appears ${s0.count} times${f0 ? `, ${esc(basename(f0.file))} alone carries ${f0.count}` : ''}. The theme file already has a variable for each (a grey is <code>text-muted-foreground</code>, a status colour is a Badge variant or a variable you add). Swap the class, not the value.` }
|
|
@@ -1191,13 +1371,13 @@ function whereToStartSection() {
|
|
|
1191
1371
|
const s0 = k.colour.samples[0], f0 = productFirst(k.colour.top)[0];
|
|
1192
1372
|
c.push({ score: 20 + k.colour.per100 / 4, metric: 'kitColour', after: 0,
|
|
1193
1373
|
title: `Move the ${n(k.colour.uses)} colours written on components into the ${esc(k.name)} theme`,
|
|
1194
|
-
sub: `The most written is ${esc(s0.value)} (${s0.count} times)${f0 ? `; ${esc(basename(f0.file))} carries the most, ${f0.count}, mostly ${esc(f0.sample)}` : ''}.${s0.inTheme ? ` ${esc(s0.value)} is already in your theme: read it from there, from the entry that holds it in every mode.` : ''} ${k.themeFiles.length ? `The theme in ${esc(k.themeFiles[0])} is where a colour is decided` : `There is no theme yet: start one with ${KITS[k.name]?.advice.themeCall ?? "the kit's theme call"} and put the palette there`}. ${KITS[k.name]?.advice.colourHow ?? ''}` });
|
|
1374
|
+
sub: `The most written is ${esc(s0.value)} (${s0.count} times)${f0 ? `; ${esc(basename(f0.file))} carries the most, ${f0.count}, mostly ${esc(f0.sample)}` : ''}.${s0.inTheme ? ` ${esc(s0.value)} is already in your theme: read it from there, from the entry that holds it in every mode.` : ''} ${k.themeFiles.length ? `The theme in ${esc(k.themeFiles[0])} is where a colour is decided` : `There is no theme yet: start one with ${KITS[k.name]?.advice.themeCall ?? "the kit's theme call"} and put the palette there`}. ${KITS[k.name]?.advice.colourHow ?? ''}${secondKitMoveNote(k, 'colours')}` });
|
|
1195
1375
|
}
|
|
1196
1376
|
if (k.px.uses >= 10) {
|
|
1197
1377
|
const s0 = k.px.samples[0], f0 = productFirst(k.px.top)[0];
|
|
1198
1378
|
c.push({ score: 15 + k.px.per100 / 4, metric: 'kitPx', after: 0,
|
|
1199
1379
|
title: `Put the ${n(k.px.uses)} pixel spacings on the theme's spacing steps`,
|
|
1200
|
-
sub: `The most written is ${esc(s0.value)} (${s0.count} times)${f0 ? `; ${esc(basename(f0.file))} carries the most, ${f0.count}, mostly ${esc(f0.sample)}` : ''}. ${KITS[k.name]?.advice.spacingHow(k) ?? ''}` });
|
|
1380
|
+
sub: `The most written is ${esc(s0.value)} (${s0.count} times)${f0 ? `; ${esc(basename(f0.file))} carries the most, ${f0.count}, mostly ${esc(f0.sample)}` : ''}. ${KITS[k.name]?.advice.spacingHow(k) ?? ''}${secondKitMoveNote(k, 'px')}` });
|
|
1201
1381
|
}
|
|
1202
1382
|
}
|
|
1203
1383
|
|
|
@@ -1271,6 +1451,7 @@ function whereToStartSection() {
|
|
|
1271
1451
|
const promptFor = (item) => fixPrompt({
|
|
1272
1452
|
metric: item.metric,
|
|
1273
1453
|
kit: P.isKit ? P.kit?.name : null,
|
|
1454
|
+
second: P.isKit && P.kit?.second ? { name: P.kit.second.name, pkg: P.kit.second.pkg } : null,
|
|
1274
1455
|
title: unesc(item.title),
|
|
1275
1456
|
sub: unesc(item.sub),
|
|
1276
1457
|
// the points arrive when the whole finding is done; one file moves the count
|
|
@@ -1469,9 +1650,24 @@ function shadcnReceipt() {
|
|
|
1469
1650
|
const counted = (r.counted?.code ?? 0) === 0 && r.themes?.total
|
|
1470
1651
|
? `<b>It publishes no code, so the themes check is the score</b>: the ${n(kept)} files of the app that makes them are not counted.`
|
|
1471
1652
|
: `<b>Only what it publishes is counted</b>: ${n(r.counted?.code ?? 0)} code file${(r.counted?.code ?? 0) === 1 ? '' : 's'}${kept ? `, with ${n(kept)} more kept out as its site, demos and examples` : ''}.`;
|
|
1472
|
-
return `<div class="excl fresh">Read as a shadcn registry: ${what}${r.variants.length ? `, the same components kept in ${r.variants.length} variants` : ''}. ${counted}${th}</div><div class="excl">Evidence: ${receipt}</div
|
|
1653
|
+
return `<div class="excl fresh">Read as a shadcn registry: ${what}${r.variants.length ? `, the same components kept in ${r.variants.length} variants` : ''}. ${counted}${th}</div><div class="excl">Evidence: ${receipt}</div>${themeUsesLine()}`;
|
|
1473
1654
|
}
|
|
1474
|
-
return `<div class="excl">Read as a shadcn install (${esc(P.confidence ?? 'medium')} confidence): ${receipt}</div
|
|
1655
|
+
return `<div class="excl">Read as a shadcn install (${esc(P.confidence ?? 'medium')} confidence): ${receipt}</div>${themeUsesLine()}`;
|
|
1656
|
+
}
|
|
1657
|
+
// The theme's names, and how often class names matching them appear across
|
|
1658
|
+
// the repo, by the Tailwind receipt's counter with comments blanked (shadcn's
|
|
1659
|
+
// own components included, as there) (9.7.0). A count under half the bar's
|
|
1660
|
+
// own class uses means the receipt read the wrong names: onlook's @theme
|
|
1661
|
+
// holds only its 8 sidebar names (49 uses, the bar counts 1,926), and
|
|
1662
|
+
// supabase's reader took a comment saying "@theme inline block" for the
|
|
1663
|
+
// theme (551 against 5,869). Said nothing rather than something the bar
|
|
1664
|
+
// beside it contradicts.
|
|
1665
|
+
function themeUsesLine() {
|
|
1666
|
+
const u = P.shadcn?.themeUses ?? null;
|
|
1667
|
+
if (!u?.uses) return '';
|
|
1668
|
+
if (h.colourUse?.totals && u.uses * 2 < h.colourUse.totals.classes) return '';
|
|
1669
|
+
const N = u.names.length;
|
|
1670
|
+
return `<div class="excl">Its theme names ${n(N)} colour${N === 1 ? '' : 's'} in ${esc(u.file)}; class names matching them appear ${n(u.uses)} time${u.uses === 1 ? '' : 's'} across ${n(u.usedIn)} file${u.usedIn === 1 ? '' : 's'}.</div>`;
|
|
1475
1671
|
}
|
|
1476
1672
|
|
|
1477
1673
|
// A Tailwind repo with its own theme: what the vocabulary is, how much of
|
|
@@ -1488,7 +1684,39 @@ function publishesRegistryLine() {
|
|
|
1488
1684
|
function kitReceipt() {
|
|
1489
1685
|
if (!P.isKit || !P.kit) return '';
|
|
1490
1686
|
const k = P.kit;
|
|
1491
|
-
|
|
1687
|
+
const sk = k.second;
|
|
1688
|
+
// a second kit: named, and why its own files are not in the counts
|
|
1689
|
+
// the files without the first kit stay out of the kit counts; they still
|
|
1690
|
+
// count in the repo-wide tiles like any other file
|
|
1691
|
+
const without = !sk?.withoutFirst ? '' : sk.withoutFirst === sk.files ? `, none of them with ${esc(k.name)}` : `, ${n(sk.withoutFirst)} of them without ${esc(k.name)}`;
|
|
1692
|
+
const second = !sk ? '' : ` ${esc(sk.name)} is imported in ${n(sk.files)} files too${sk.withoutFirst
|
|
1693
|
+
? `${without}. ${sk.withoutFirst === 1 ? 'That file is' : `Those ${n(sk.withoutFirst)} files are`} not in the kit counts: ${esc(sk.name)} components do not read the ${esc(k.name)} theme.`
|
|
1694
|
+
: `, each of them beside ${esc(k.name)}.`}`;
|
|
1695
|
+
return `<div class="excl fresh">Read as a product built on ${esc(k.name)}: imported in ${n(k.kitFiles)} files, ${k.themeFiles.length ? `theme defined in ${esc(k.themeFiles[0])}` : `no theme of its own, so the ${esc(k.name)} defaults are the theme`}. The kit's own code is installed from npm and not counted; what is counted is what the repo writes onto its components.${second}</div><div class="excl">Evidence: ${esc(P.evidence.join(' · '))}</div>`;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// A second kit, on the two kit moves: change only the first kit's
|
|
1699
|
+
// components, said only where some of that card's values sit in files that
|
|
1700
|
+
// use both kits
|
|
1701
|
+
function secondKitMoveNote(k, part) {
|
|
1702
|
+
const sk = k.second;
|
|
1703
|
+
return sk?.mixed && sk.mixedPaint?.[part] ? ` In the ${n(sk.mixed)} file${sk.mixed === 1 ? '' : 's'} that also use${sk.mixed === 1 ? 's' : ''} ${esc(sk.name)}, change only what sits on ${esc(k.name)} components.` : '';
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const aKitName = (name) => `${/^(?:[AEIOU]|MUI)/.test(name) ? 'an' : 'a'} ${esc(name)}`;
|
|
1707
|
+
// A second kit, in the kit section: which files are counted and which are not
|
|
1708
|
+
function secondKitBlock(k) {
|
|
1709
|
+
const sk = k.second;
|
|
1710
|
+
if (!sk) return '';
|
|
1711
|
+
const mp = sk.mixedPaint ?? { colours: 0, px: 0 };
|
|
1712
|
+
const inThem = [mp.colours ? `${n(mp.colours)} of the colours` : '', mp.px ? `${n(mp.px)} of the pixel sizes` : ''].filter(Boolean).join(' and ');
|
|
1713
|
+
const one = mp.colours + mp.px === 1;
|
|
1714
|
+
const parts = [
|
|
1715
|
+
sk.withoutFirst ? `${n(sk.withoutFirst)} file${sk.withoutFirst === 1 ? ' uses' : 's use'} ${esc(sk.name)} without ${esc(k.name)}. ${sk.withoutFirst === 1 ? 'It is' : 'They are'} not in the counts above.` : '',
|
|
1716
|
+
sk.mixed ? `${n(sk.mixed)} file${sk.mixed === 1 ? ' uses' : 's use'} both kits and ${sk.mixed === 1 ? 'is' : 'are'} counted.${inThem ? ` ${inThem[0].toUpperCase()}${inThem.slice(1)} above ${one ? 'is' : 'are'} in ${sk.mixed === 1 ? 'that file' : 'those files'}, so ${one ? `it may be on ${aKitName(sk.name)} component` : `some may be on ${esc(sk.name)} components`}.` : ''}` : '',
|
|
1717
|
+
"Follow the kit the file already uses. Never put one kit's styling on the other's components.",
|
|
1718
|
+
].filter(Boolean);
|
|
1719
|
+
return `<div class="receipts">${eyebrow(`Two kits · ${esc(sk.name)} in ${n(sk.files)} files`)}<p class="sub">${parts.join(' ')}</p></div>`;
|
|
1492
1720
|
}
|
|
1493
1721
|
|
|
1494
1722
|
// A kit product: the theme, how often components read it, and where a colour
|
|
@@ -1501,6 +1729,7 @@ function kitSection() {
|
|
|
1501
1729
|
const parts = [`<div class="receipts">${eyebrow(k.themeFiles.length ? `Theme in ${esc(k.themeFiles[0])}${k.themeFiles.length > 1 ? ` and ${n(k.themeFiles.length - 1)} more` : ''} · read ${n(k.refs)} times from components` : `No theme of its own · the ${esc(k.name)} defaults`)}<p class="sub">${k.themeFiles.length ? `Components reach the theme ${n(k.refsPer100)} times per 100 kit files, through ${KITS[k.name]?.advice.refExamples ?? 'theme references'}. That is the system working.` : `Without a theme of its own, every colour a component needs is either ${esc(k.name)}'s default or written by hand.`}</p></div>`];
|
|
1502
1730
|
parts.push(`<div class="receipts">${eyebrow(`${n(k.colour.uses)} colours written on components · ${n(k.colour.per100)} per 100 kit files`)}${k.colour.uses ? `<div class="chips-row">${chips(k.colour.samples, 'bad')}</div><div class="chips-row">${files(k.colour.top)}</div>` : '<p class="sub">Every colour on a component comes from the theme.</p>'}${whyToggle('kitColour')}</div>`);
|
|
1503
1731
|
parts.push(`<div class="receipts">${eyebrow(`${n(k.px.uses)} pixel sizes written on components · ${n(k.px.per100)} per 100 kit files`)}${k.px.uses ? `<div class="chips-row">${chips(k.px.samples, 'warn')}</div><div class="chips-row">${files(k.px.top)}</div>` : '<p class="sub">Spacing, type size and radius on components come from the theme.</p>'}${whyToggle('kitPx')}</div>`);
|
|
1732
|
+
if (k.second) parts.push(secondKitBlock(k));
|
|
1504
1733
|
if (k.exempt?.length) parts.push(`<p class="sub">Not counted: ${n(k.exempt.length)} file${k.exempt.length === 1 ? '' : 's'} of colour data or artwork (${esc(k.exempt.slice(0, 3).map((e) => basename(e.file)).join(', '))}).</p>`);
|
|
1505
1734
|
return `<section class="glass pad">${sectionHead(`Your ${esc(k.name)} theme, and what is written around it`, 'the theme, and the places a colour or a pixel size was written onto a component instead')}${parts.join('')}</section>`;
|
|
1506
1735
|
}
|
|
@@ -1659,7 +1888,8 @@ function exceptionsBlock() {
|
|
|
1659
1888
|
const vd = r.variantsDropped ?? [];
|
|
1660
1889
|
if (vd.length) lines.push(`${vd.length} variant${vd.length === 1 ? '' : 's'} of the same components (${vd.map((e) => esc(basename(dirname(e.dir)) === 'bases' ? basename(dirname(e.dir)) + '/' + basename(e.dir) : e.dir)).join(', ')}, ${n(vd.reduce((a, e) => a + e.files, 0))} files) counted once, through the variant the registry file names.`);
|
|
1661
1890
|
}
|
|
1662
|
-
if (vendoredUi &&
|
|
1891
|
+
if (vendoredUi && neverStock.length) lines.push(`${n(neverStock.length)} shadcn component${neverStock.length === 1 ? ' is' : 's are'} installed but not used yet. ${neverStock.length === 1 ? 'It is' : 'They are'} not scored.`);
|
|
1892
|
+
if (vendoredUi && neverOwn.length) lines.push(`${n(neverOwn.length)} component${neverOwn.length === 1 ? '' : 's'} the team wrote in the same folder ${neverOwn.length === 1 ? 'is' : 'are'} not imported either. ${neverOwn.length === 1 ? 'It is' : 'They are'} listed under the adoption map and ${neverOwn.length === 1 ? 'is' : 'are'} not scored.`);
|
|
1663
1893
|
const rp = P.shadcn?.registryPaint;
|
|
1664
1894
|
if (rp) lines.push(`Installed registr${rp.dirs.length === 1 ? 'y' : 'ies'} ${esc(rp.dirs.map((d) => basename(d)).join(', '))}: ${n(rp.files)} file${rp.files === 1 ? '' : 's'}${rp.tinUses ? `, ${n(rp.tinUses)} palette colour${rp.tinUses === 1 ? '' : 's'}` : ''}. Kept in the score, left out of the fixes. Your agent reads them like everything else.`);
|
|
1665
1895
|
} else if (isLibrary) {
|
|
@@ -1905,6 +2135,7 @@ ${ogTags()}
|
|
|
1905
2135
|
|
|
1906
2136
|
/* palette */
|
|
1907
2137
|
.palette-grid { display:grid; grid-template-columns:1fr 1.6fr; gap:16px; }
|
|
2138
|
+
.palette-grid > * { min-width:0; }
|
|
1908
2139
|
@media (max-width:860px) { .palette-grid { grid-template-columns:1fr; } }
|
|
1909
2140
|
.pal-hero { position:relative; overflow:hidden; padding:26px 28px; }
|
|
1910
2141
|
.pal-hero .b1 { right:-40px; top:-40px; width:160px; height:160px; background:var(--blob); filter:blur(32px); }
|
|
@@ -1924,8 +2155,13 @@ ${ogTags()}
|
|
|
1924
2155
|
.lg-tok { background:var(--text); } .lg-stray { background:var(--coral); }
|
|
1925
2156
|
.usage-bar { display:flex; height:96px; gap:2px; margin-top:18px; border-radius:16px; overflow:hidden; }
|
|
1926
2157
|
.uc { position:relative; min-width:3px; box-shadow:0 0 0 1px var(--cell-ring) inset; }
|
|
2158
|
+
.usage-bar .uc { min-width:2px; }
|
|
1927
2159
|
.uc:first-child { border-radius:16px 0 0 16px; } .uc:last-child { border-radius:0 16px 16px 0; }
|
|
1928
|
-
.uc.stray i { position:absolute; top:4px; right:4px; width:6px; height:6px; border-radius:99px; background:var(--coral); box-shadow:0 0 0 2px var(--card-solid); }
|
|
2160
|
+
.uc.stray i, .uc.pal i { position:absolute; top:4px; right:4px; width:6px; height:6px; border-radius:99px; background:var(--coral); box-shadow:0 0 0 2px var(--card-solid); }
|
|
2161
|
+
.uc.pal i { background:var(--dim); }
|
|
2162
|
+
.uc.named { background:radial-gradient(var(--dim2) 1.2px, transparent 1.8px) 0 0/7px 7px, var(--deep); }
|
|
2163
|
+
.lg-pal { background:var(--dim); } .lg-named { background:radial-gradient(var(--dim) 1px, transparent 1.4px) 0 0/4px 4px, var(--deep); }
|
|
2164
|
+
.use-line { margin-top:12px; }
|
|
1929
2165
|
.uc-rest { background:repeating-linear-gradient(45deg,var(--deep),var(--deep) 4px,var(--card-solid) 4px,var(--card-solid) 8px);
|
|
1930
2166
|
display:flex; align-items:center; justify-content:center; font:600 10px var(--mono); color:var(--dim); }
|
|
1931
2167
|
.ramp-head { margin-top:26px; }
|
|
@@ -2320,7 +2556,9 @@ if (summaryPath) {
|
|
|
2320
2556
|
role: P.role,
|
|
2321
2557
|
kind: P.kind,
|
|
2322
2558
|
...(P.publishesRegistry ? { publishesRegistry: { source: P.publishesRegistry.source, publishes: P.publishesRegistry.publishes } } : {}),
|
|
2323
|
-
...(P.isKit && P.kit ? { kit: { name: P.kit.name, kitFiles: P.kit.kitFiles, themeFiles: P.kit.themeFiles, spacingUnit: P.kit.spacingUnit ?? null, refs: P.kit.refs, colours: P.kit.colour.uses, pixelSizes: P.kit.px.uses, evidence: P.evidence
|
|
2559
|
+
...(P.isKit && P.kit ? { kit: { name: P.kit.name, kitFiles: P.kit.kitFiles, themeFiles: P.kit.themeFiles, spacingUnit: P.kit.spacingUnit ?? null, refs: P.kit.refs, colours: P.kit.colour.uses, pixelSizes: P.kit.px.uses, evidence: P.evidence,
|
|
2560
|
+
// a second kit, named and not scored (9.7.0); absent when there is none
|
|
2561
|
+
...(P.kit.second ? { second: { name: P.kit.second.name, pkg: P.kit.second.pkg, files: P.kit.second.files, withoutFirst: P.kit.second.withoutFirst, mixed: P.kit.second.mixed } } : {}) } } : {}),
|
|
2324
2562
|
...(P.isTailwind && P.tailwind ? { tailwind: { file: P.tailwind.file, names: P.tailwind.names.length, restated: P.tailwind.restated, retuned: P.tailwind.retuned?.length ?? 0, uses: P.tailwind.uses, usedIn: P.tailwind.usedIn, adopted: P.tailwind.adopted, evidence: P.evidence } } : {}),
|
|
2325
2563
|
...(P.isRegistry && P.registry ? { registry: { source: P.registry.source, builtFrom: P.registry.builtFrom, items: P.registry.items, publishes: P.registry.publishes, variants: P.registry.variants, counted: P.registry.counted ?? null, showcase: P.registry.showcase ?? [], variantsDropped: P.registry.variantsDropped ?? [], themes: P.registry.themes ? { total: P.registry.themes.total, incomplete: P.registry.themes.incomplete } : null } } : {}),
|
|
2326
2564
|
...(P.isShadcn && P.shadcn ? { shadcn: { confidence: P.confidence, evidence: P.evidence, style: P.shadcn.kit?.style ?? null, baseColor: P.shadcn.kit?.baseColor ?? null, tailwind: P.shadcn.kit?.tailwind ?? null, catalogues: P.uiDirs, registries: P.shadcn.registryDirs ?? [], ...(P.shadcn.registryPaint ? { registryFiles: P.shadcn.registryPaint.files, registryPaletteColours: P.shadcn.registryPaint.tinUses } : {}), ownFiles: P.shadcn.paint?.ownFiles ?? null, fresh: P.shadcn.fresh?.fresh === true, ...(P.shadcn.lint ? { lint: { file: P.shadcn.lint.file, kind: P.shadcn.lint.kind, rulesOn: lintRulesOn(P.shadcn.lint), readFully: P.shadcn.lint.readFully } } : {}) } } : {}),
|