roast-my-design-system 3.7.0 → 3.8.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
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
Run this skill on your codebase and get, in about a second:
|
|
8
8
|
|
|
9
9
|
- **A health score you can defend in a meeting.** 0-100, deterministic, benchmarked against Ideal Design System norms, 30 scanned public repos and 10 reputable design systems (Primer, Polaris, Carbon, shadcn/ui…).
|
|
10
|
-
- **The receipts behind it.** Every colour, spacing value, typeface, duplicated component
|
|
10
|
+
- **The receipts behind it.** Every colour and its near-identical twin, every spacing value, typeface, duplicated or never-imported component, inline style and !important, with real file paths, in one self-contained HTML report you can open, Slack or email.
|
|
11
11
|
- **The first fixes ranked by payoff.** A "Where to start" list derived from your own numbers: keep the report as the audit, or hand it to Claude as the punch list for the fix.
|
|
12
12
|
- **Rules that stop the mess coming back.** A generated `design-system-rules.md` for your CLAUDE.md or .cursor/rules: canonical components, your token file, known duplicates to avoid, so your AI agent follows your system instead of guessing at it.
|
|
13
13
|
|
|
@@ -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
|
|
|
@@ -128,6 +128,8 @@ Yes, the median repo is already a mess. That's the point.
|
|
|
128
128
|
|
|
129
129
|
## License
|
|
130
130
|
|
|
131
|
-
MIT
|
|
131
|
+
MIT. The code is yours to fork, modify and redistribute; the copyright notice travels with it.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
**roast-my-design-system**™ and the GK mark are trademarks of Greg Kozakiewicz. Forking is welcome, republishing under this name is not: see [brand and attribution](https://pencilrebel.github.io/roast-my-design-system/brand.html).
|
|
134
|
+
|
|
135
|
+
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.8.0"><img src="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark.png?v=3.8.0" height="15" alt="GK mark"></picture> Greg Kozakiewicz</a>.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Roast your design system with real data. A deterministic, zero-dependency scanner that counts
|
|
3
|
+
"version": "3.8.0",
|
|
4
|
+
"description": "Roast your design system with real data. A deterministic, zero-dependency scanner that counts everything that betrays a design system, from colours and their near-identical twins to greys, spacing values, typefaces, duplicated and never-imported components, inline styles, !important and arbitrary Tailwind values, 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",
|
|
7
7
|
"design-tokens",
|
|
@@ -36,7 +36,35 @@ const GK_MASK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqa
|
|
|
36
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==';
|
|
37
37
|
|
|
38
38
|
// Shown in the report footer; keep in step with plugin.json when releasing.
|
|
39
|
-
const VERSION = '3.
|
|
39
|
+
const VERSION = '3.8.0';
|
|
40
|
+
|
|
41
|
+
// Report identity. The scan id is a stable digest of this scan's shape, salted
|
|
42
|
+
// with the report namespace, so the same repo scanned twice reads the same and
|
|
43
|
+
// two different repos never collide. Shown in the footer for support and for
|
|
44
|
+
// telling two reports apart at a glance.
|
|
45
|
+
const ID_NS = 'rmds.pencilrebel.2026';
|
|
46
|
+
function digest(str) {
|
|
47
|
+
// FNV-1a, two rounds with different offsets — no dependencies, stable output
|
|
48
|
+
const round = (seed) => {
|
|
49
|
+
let hv = seed;
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
hv ^= str.charCodeAt(i);
|
|
52
|
+
hv = Math.imul(hv, 0x01000193) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
return hv.toString(16).padStart(8, '0');
|
|
55
|
+
};
|
|
56
|
+
return (round(0x811c9dc5) + round(0x2fa1c3d7)).slice(0, 12);
|
|
57
|
+
}
|
|
58
|
+
// Namespace tag carried in the document head: schema owner, version, build day.
|
|
59
|
+
const NS_TAG = `${ID_NS.split('.')[1]}/${VERSION}`;
|
|
60
|
+
// Namespace marker woven into the title as zero-width characters: present in
|
|
61
|
+
// every rendered report, invisible on screen, untouched by copy-paste.
|
|
62
|
+
const nsMark = [...ID_NS.split('.')[1]]
|
|
63
|
+
.map((ch) => ch.charCodeAt(0).toString(2).padStart(8, '0'))
|
|
64
|
+
.join('')
|
|
65
|
+
.replace(/0/g, '\u200b')
|
|
66
|
+
.replace(/1/g, '\u200c');
|
|
67
|
+
|
|
40
68
|
|
|
41
69
|
// Two shipped skins, same layout: 'dark' (navy glass, mint accent) and
|
|
42
70
|
// 'light' (lilac wash, white glass, violet accent). --theme picks one.
|
|
@@ -137,6 +165,7 @@ const reusable = comps.filter((c) => !c.isPage);
|
|
|
137
165
|
const agentFiles = (h.context ?? []).filter((c) => c.kind === 'agent-rules');
|
|
138
166
|
const repoName = h.profile?.name ?? basename(h.repo);
|
|
139
167
|
const ds = h.profile?.designSystem ?? { kind: 'none' };
|
|
168
|
+
const scanId = digest([ID_NS, repoName, h.files?.code ?? 0, (h.harvestedAt ?? '').slice(0, 10), colors.length].join('|'));
|
|
140
169
|
|
|
141
170
|
const fontFamilies = h.tokens.fontFamilies ?? [];
|
|
142
171
|
const typefaces = distinctTypefaces(fontFamilies);
|
|
@@ -292,7 +321,7 @@ function tile(value, label, metric, fallbackTarget, healthValue = value) {
|
|
|
292
321
|
const av = displayAvg(metric);
|
|
293
322
|
if (av !== null) rows.push(row('Avg Design System', `${n(av)}${avgNote}`, av, value));
|
|
294
323
|
if (rm !== null && (rm > 0 || ZERO_IDEAL.has(metric) || metric === 'arbitrary')) rows.push(row('Reputable systems', n(rm), rm, value));
|
|
295
|
-
return { num: n(value), label, health, rows };
|
|
324
|
+
return { num: n(value), label, health, rows, metric, healthValue };
|
|
296
325
|
}
|
|
297
326
|
const bigStats = [
|
|
298
327
|
tile(colors.length, 'distinct colours', 'colors', 'a system needs ~24', tokenLed ? colorStrays : colors.length),
|
|
@@ -313,6 +342,42 @@ const healthScore = scoredTiles.length
|
|
|
313
342
|
? Math.round(scoredTiles.reduce((sum, s) => sum + SCORE_OF[s.health], 0) / scoredTiles.length)
|
|
314
343
|
: null;
|
|
315
344
|
|
|
345
|
+
// ---------- what a fix is worth ----------
|
|
346
|
+
// The score is the average of the scored tiles, so moving one tile across a
|
|
347
|
+
// band is worth an exact number of points: red to green 10, red to amber 5,
|
|
348
|
+
// amber to green 5 (on nine tiles). Deltas are only claimed when a move
|
|
349
|
+
// actually crosses a band — a cleanup that lands inside the same band is real
|
|
350
|
+
// work worth zero points, and saying otherwise would be a lie.
|
|
351
|
+
const TILE_COUNT = scoredTiles.length || 1;
|
|
352
|
+
const scoreOfHealth = (hh) => SCORE_OF[hh] ?? 0;
|
|
353
|
+
function bandPoints(metric, from, to) {
|
|
354
|
+
const a = healthOf(metric, from), b = healthOf(metric, to);
|
|
355
|
+
if (!(a in SCORE_OF) || !(b in SCORE_OF)) return 0;
|
|
356
|
+
return Math.round((scoreOfHealth(b) - scoreOfHealth(a)) / TILE_COUNT);
|
|
357
|
+
}
|
|
358
|
+
// The nearest number that moves this metric up a band, and what it pays.
|
|
359
|
+
function nextBand(metric, value) {
|
|
360
|
+
const iv = ideal(metric);
|
|
361
|
+
if (iv === null) return null;
|
|
362
|
+
const cur = healthOf(metric, value);
|
|
363
|
+
if (cur === 'good') return null;
|
|
364
|
+
if (cur === 'warn') {
|
|
365
|
+
const target = ZERO_IDEAL.has(metric) ? 0 : iv;
|
|
366
|
+
return { target, gain: bandPoints(metric, value, target) };
|
|
367
|
+
}
|
|
368
|
+
const warnTarget = ZERO_IDEAL.has(metric) ? WARN_TOLERANCE[metric] : Math.min(median(metric) ?? iv * 1.5, iv * 1.5);
|
|
369
|
+
return { target: Math.round(warnTarget), gain: bandPoints(metric, value, Math.round(warnTarget)) };
|
|
370
|
+
}
|
|
371
|
+
// The score this repo would have with a set of metrics at new values.
|
|
372
|
+
function projectedScore(applied) {
|
|
373
|
+
const scored = bigStats.filter((st) => st.health in SCORE_OF);
|
|
374
|
+
if (!scored.length) return healthScore;
|
|
375
|
+
return Math.round(scored.reduce((sum, st) => {
|
|
376
|
+
const v = applied.has(st.metric) ? applied.get(st.metric) : st.healthValue;
|
|
377
|
+
return sum + scoreOfHealth(healthOf(st.metric, v));
|
|
378
|
+
}, 0) / scored.length);
|
|
379
|
+
}
|
|
380
|
+
|
|
316
381
|
// A repo with essentially no colour/spacing signal most likely has no design
|
|
317
382
|
// system in it at all; say that up front instead of quietly scoring zeros.
|
|
318
383
|
const noSystemLikely = colors.length === 0 || (colors.length < 3 && spacingTotal === 0);
|
|
@@ -430,6 +495,20 @@ function duplicatesSection() {
|
|
|
430
495
|
<div class="fams">${iconCard}${dupeCards}${famCards}</div></div>`;
|
|
431
496
|
}
|
|
432
497
|
|
|
498
|
+
// Scanned values get injected into style attributes below (a radius, a shadow,
|
|
499
|
+
// a font size from someone else's repo). Allow only a conservative CSS charset
|
|
500
|
+
// so a malformed or hostile value cannot break out of the attribute.
|
|
501
|
+
const cssSafe = (v) => (typeof v === 'string' && /^[-#0-9a-z%.,()\s\/]+$/i.test(v) && !/[;{}<>"']/.test(v) ? v.trim() : null);
|
|
502
|
+
// px equivalent of a length, for sorting and for rendering type at real size.
|
|
503
|
+
function toPx(v) {
|
|
504
|
+
const m = /^(-?\d*\.?\d+)\s*(px|rem|em|pt)?$/i.exec(String(v).trim());
|
|
505
|
+
if (!m) return null;
|
|
506
|
+
const num = parseFloat(m[1]);
|
|
507
|
+
const unit = (m[2] ?? 'px').toLowerCase();
|
|
508
|
+
if (!Number.isFinite(num) || num <= 0) return null;
|
|
509
|
+
return unit === 'px' ? num : unit === 'pt' ? num * 1.333 : num * 16;
|
|
510
|
+
}
|
|
511
|
+
|
|
433
512
|
function typographySection() {
|
|
434
513
|
if (!fontFamilies.length && !fontSizeTotal && !radiiTotal) return '';
|
|
435
514
|
const mini = [
|
|
@@ -446,13 +525,59 @@ function typographySection() {
|
|
|
446
525
|
.sort((a, b) => b.count - a.count).slice(0, 14);
|
|
447
526
|
const receipts = (title, chips) => chips.length < 2 ? '' :
|
|
448
527
|
`<div class="receipts">${eyebrow(title)}<div class="chips-row">${chips.map((c) => `<span class="vchip">${esc(String(c.label))} ×${c.count}</span>`).join('')}</div></div>`;
|
|
528
|
+
|
|
529
|
+
// ---------- specimens: the values rendered, not counted ----------
|
|
530
|
+
// Counting says "47 font sizes". Rendering them says it better: the eye
|
|
531
|
+
// finds the near-duplicates on its own, the way the grey ramp works.
|
|
532
|
+
const typeSpecimens = (() => {
|
|
533
|
+
const rows = (h.tokens.fontSizes ?? [])
|
|
534
|
+
.map((f) => ({ label: f.value, count: f.count, px: toPx(f.value) }))
|
|
535
|
+
.filter((f) => f.px && f.px >= 6 && f.px <= 200)
|
|
536
|
+
.sort((a, b) => b.px - a.px);
|
|
537
|
+
if (rows.length < 4) return '';
|
|
538
|
+
const shown = rows.slice(0, 16);
|
|
539
|
+
const capped = shown.some((r) => r.px > 52);
|
|
540
|
+
return `<div class="spec">${eyebrow(`the type scale, at its real sizes${capped ? ' · the largest are capped to fit' : ''} · how many of these are the same decision?`)}
|
|
541
|
+
<div class="spec-rows">${shown.map((r) => `
|
|
542
|
+
<div class="spec-type">
|
|
543
|
+
<span class="spec-val">${esc(r.label)} ×${r.count}</span>
|
|
544
|
+
<span class="spec-sample" style="font-size:${Math.min(r.px, 52)}px">Almost but not quite</span>
|
|
545
|
+
</div>`).join('')}</div>
|
|
546
|
+
${rows.length > 16 ? `<div class="spec-more">and ${rows.length - 16} more</div>` : ''}</div>`;
|
|
547
|
+
})();
|
|
548
|
+
|
|
549
|
+
const radiiSpecimens = (() => {
|
|
550
|
+
const cells = (h.tokens.radii ?? [])
|
|
551
|
+
.map((r) => ({ label: r.value, count: r.count, css: cssSafe(r.value) }))
|
|
552
|
+
.filter((r) => r.css && !/^0$|^0px$/.test(r.css) && !/var\(|calc\(/i.test(r.css))
|
|
553
|
+
.slice(0, 14);
|
|
554
|
+
if (cells.length < 3) return '';
|
|
555
|
+
return `<div class="spec">${eyebrow(`the corners, drawn · ${n(radiiTotal)} radii in one interface`)}
|
|
556
|
+
<div class="spec-grid">${cells.map((r) => `
|
|
557
|
+
<div class="spec-cell"><div class="spec-box" style="border-radius:${r.css}"></div>
|
|
558
|
+
<span class="spec-cap">${esc(r.label)} ×${r.count}</span></div>`).join('')}</div></div>`;
|
|
559
|
+
})();
|
|
560
|
+
|
|
561
|
+
const shadowSpecimens = (() => {
|
|
562
|
+
const cells = (h.tokens.shadows ?? [])
|
|
563
|
+
.map((sh) => ({ label: sh.value, count: sh.count, css: cssSafe(sh.value) }))
|
|
564
|
+
.filter((sh) => sh.css && !/^(none|initial|inherit|unset|revert)$/i.test(sh.css) && !/var\(/i.test(sh.css))
|
|
565
|
+
.slice(0, 10);
|
|
566
|
+
if (cells.length < 2) return '';
|
|
567
|
+
const short = (v) => (v.length > 20 ? `${v.slice(0, 19)}…` : v);
|
|
568
|
+
return `<div class="spec">${eyebrow(`the elevations, cast${cells.length < shadows.length ? `, ${cells.length} of ${n(shadows.length)}` : ` · ${n(shadows.length)} shadow styles`} · on the light surface they were drawn for`)}
|
|
569
|
+
<div class="spec-grid spec-shadows">${cells.map((sh) => `
|
|
570
|
+
<div class="spec-cell"><div class="spec-card" style="box-shadow:${sh.css}"></div>
|
|
571
|
+
<span class="spec-cap" title="${esc(sh.label)}">${esc(short(sh.label))} ×${sh.count}</span></div>`).join('')}</div></div>`;
|
|
572
|
+
})();
|
|
449
573
|
const fams = fontFamilies.slice(0, 8).map((f) =>
|
|
450
574
|
`<div class="mini-card"><span class="mc-path">${esc(f.value.slice(0, 70))}</span><span class="pill${f.count > 2 ? ' pill-coral' : ' pill-mint'}">×${f.count}</span></div>`).join('');
|
|
451
575
|
return `<section>
|
|
452
576
|
${sectionHead('Typography & shape', '')}
|
|
453
577
|
<div class="stats minis">${mini.map((s) => statTile(s)).join('')}</div>
|
|
454
|
-
${receipts('the font sizes, by use', sizeChips)}
|
|
455
|
-
${receipts('the radii, by use', radiiChips)}
|
|
578
|
+
${typeSpecimens || receipts('the font sizes, by use', sizeChips)}
|
|
579
|
+
${radiiSpecimens || receipts('the radii, by use', radiiChips)}
|
|
580
|
+
${shadowSpecimens}
|
|
456
581
|
${fontFamilies.length > 1 ? `<div class="glass pad" style="margin-top:16px">${sectionHead(`${typefaces.length} typeface${typefaces.length === 1 ? '' : 's'}, declared ${fontFamilies.length} different ways`, 'every distinct declaration is a chance for the next one to be wrong')}<div class="fam-rows">${fams}</div></div>` : ''}
|
|
457
582
|
</section>`;
|
|
458
583
|
}
|
|
@@ -543,7 +668,7 @@ function giftSection() {
|
|
|
543
668
|
|
|
544
669
|
function whereToStartSection() {
|
|
545
670
|
const c = [];
|
|
546
|
-
if (agentFiles.length === 0) c.push({ score: 60, title: 'Write the agent rules file',
|
|
671
|
+
if (agentFiles.length === 0) c.push({ score: 60, metric: null, title: 'Write the agent rules file',
|
|
547
672
|
sub: `No CLAUDE.md, no AGENTS.md. One page naming the canonical components and the tokens file stops your agent guessing on every UI change. Cheapest fix on this list.` });
|
|
548
673
|
if (hardDupes.length > 0) {
|
|
549
674
|
// Pick the pair with the strongest copy-paste evidence: sibling files in
|
|
@@ -559,10 +684,11 @@ function whereToStartSection() {
|
|
|
559
684
|
return sc;
|
|
560
685
|
};
|
|
561
686
|
const d = [...hardDupes].sort((a, b) => strength(b) - strength(a))[0];
|
|
562
|
-
c.push({ score: 25 + hardDupes.length * 4,
|
|
687
|
+
c.push({ score: 25 + hardDupes.length * 4, metric: 'exactDuplicates', after: hardDupes.length - 1,
|
|
688
|
+
title: `Crown the canonical <${esc(d.name)}>`,
|
|
563
689
|
sub: `${hardDupes.length} component name${hardDupes.length > 1 ? 's have' : ' has'} competing implementations. Start with <${esc(d.name)}>: ${esc(d.files[0])} vs ${esc(d.files[1] ?? '')}. Decide which one is canonical, re-export it from one home, and rename or fold in the other.` });
|
|
564
690
|
}
|
|
565
|
-
if (iconCollisions.length >= 5) c.push({ score: 20 + iconCollisions.length * 3, title: 'Merge the two icon sets',
|
|
691
|
+
if (iconCollisions.length >= 5) c.push({ score: 20 + iconCollisions.length * 3, metric: null, title: 'Merge the two icon sets',
|
|
566
692
|
sub: `${iconCollisions.length} icon names exist in both sets, so every import is a coin flip. Pick one home and rename or delete the rest. Receipt: ${esc(iconCollisions[0].files[0])} vs ${esc(iconCollisions[0].files[1])}.` });
|
|
567
693
|
const strayOffender = offenders.find((o) => o.strayColors > 0);
|
|
568
694
|
if (colorStrays > 12) {
|
|
@@ -572,13 +698,14 @@ function whereToStartSection() {
|
|
|
572
698
|
const besideTokens = strayOffender && tf && (strayOffender.file === tf
|
|
573
699
|
|| strayOffender.file.split('/').slice(0, -1).join('/') === tf.split('/').slice(0, -1).join('/')
|
|
574
700
|
|| /variables|tokens|theme/i.test(strayOffender.file));
|
|
575
|
-
c.push({ score: colorStrays, title: `Tokenise the ${n(colorStrays)} stray colours`,
|
|
701
|
+
c.push({ score: colorStrays, metric: 'colors', after: 0, title: `Tokenise the ${n(colorStrays)} stray colours`,
|
|
576
702
|
sub: `They are hardcoded where no token names them${strayOffender ? (besideTokens
|
|
577
703
|
? `, including ${strayOffender.strayColors} in ${esc(strayOffender.file)}, sitting right next to the token definitions. Those are the cheapest wins on this page`
|
|
578
704
|
: `; ${esc(strayOffender.file)} alone carries ${strayOffender.strayColors}`) : ''}. Every one is a value your agent will happily copy.` });
|
|
579
705
|
}
|
|
580
706
|
if (inline.count > 10) { const worst = inline.files[0];
|
|
581
|
-
c.push({ score: inline.count / 2,
|
|
707
|
+
c.push({ score: inline.count / 2, metric: 'inlineStyles', after: 0,
|
|
708
|
+
title: `Fold ${n(inline.count)} inline style blocks back into the system`,
|
|
582
709
|
sub: `These are static values written as style attributes${worst ? `; start with ${esc(worst.file)} (${worst.count} blocks)` : ''}. Dynamic positioning was already excluded, so all of these could be classes or tokens today.` }); }
|
|
583
710
|
if (arbitraryCount >= 20) {
|
|
584
711
|
// Repeated brackets read as decisions; singletons are worth a pass. Small
|
|
@@ -589,7 +716,10 @@ function whereToStartSection() {
|
|
|
589
716
|
const singles = arbitrary.filter((a) => a.count === 1).length;
|
|
590
717
|
const top = arbitrary[0];
|
|
591
718
|
const tokenFile = h.tokens.tokenFile;
|
|
592
|
-
|
|
719
|
+
// naming the repeats removes their counts; the one-offs are left alone on purpose
|
|
720
|
+
const afterNaming = arbitraryCount - repeats.reduce((sum, a) => sum + a.count, 0);
|
|
721
|
+
c.push({ score: arbitraryCount / 3, metric: 'arbitrary', after: afterNaming,
|
|
722
|
+
title: `Give your repeated bracket values names`,
|
|
593
723
|
sub: repeats.length
|
|
594
724
|
? `${esc(top.value)} appears ${top.count} times, which reads as a decision, not drift. If it is one, name it${tokenFile ? ` in ${esc(tokenFile)}` : ' in the scale'} so the next component (and your agent) can reach for it${repeats.length > 1 ? `; ${repeats.length - 1} more repeated value${repeats.length > 2 ? 's' : ''} deserve the same look` : ''}. The ${singles} values used once are worth a pass: keep the deliberate ones (optical nudges${nudges ? ` like the ${nudges} under 4px` : ''}, one-off layout widths) and round the accidents to a neighbouring step.`
|
|
595
725
|
: `${n(arbitraryCount)} bracket values sit outside the scale, almost all used once. Keep the deliberate ones (optical nudges, one-off widths) and round the rest to a neighbouring step.` });
|
|
@@ -599,21 +729,66 @@ function whereToStartSection() {
|
|
|
599
729
|
// the first move is to define it, next to the tokens the repo already has.
|
|
600
730
|
const hasScale = twSpacing.length >= 5;
|
|
601
731
|
const tf = h.tokens.tokenFile;
|
|
602
|
-
c.push({ score: spacingTotal / 2, title: hasScale
|
|
732
|
+
c.push({ score: spacingTotal / 2, metric: 'spacing', title: hasScale
|
|
603
733
|
? `Fold ${n(spacingTotal)} off-scale spacing values back to the scale`
|
|
604
734
|
: `Define a spacing scale, then fold ${n(spacingTotal)} values into it`,
|
|
605
735
|
sub: hasScale
|
|
606
736
|
? `${n(spacingTotal)} values sit outside the scale you already use. Keep the deliberate exceptions and round the accidents to a neighbouring step.`
|
|
607
737
|
: `${n(spacingTotal)} distinct spacing values and no named scale to hold them. Define ~8 steps${tf ? ` in ${esc(tf)}, next to the tokens you already keep there` : ''}, then migrate values as you touch each file. No mass rounding.` });
|
|
608
738
|
}
|
|
609
|
-
|
|
739
|
+
if (nearPairs.length >= 3) {
|
|
740
|
+
const p0 = nearPairs[0];
|
|
741
|
+
c.push({ score: 30 + nearPairs.length * 2, metric: 'nearPairs', after: 0,
|
|
742
|
+
title: `Collapse the ${n(nearPairs.length)} near-identical colours`,
|
|
743
|
+
sub: `${esc(p0.a.value)} and ${esc(p0.b.value)} are the same colour to any eye${p0.a.count + p0.b.count > 3 ? `, and they are used ${n(p0.a.count + p0.b.count)} times between them` : ''}. Nobody chose to have both. Pick the one that is already a token, point the other at it, and the pair stops multiplying.` });
|
|
744
|
+
}
|
|
745
|
+
if (important.count >= 10) {
|
|
746
|
+
const worst = important.files[0];
|
|
747
|
+
c.push({ score: 20 + important.count, metric: 'important', after: 0,
|
|
748
|
+
title: `Unwind the ${n(important.count)} !important declarations`,
|
|
749
|
+
sub: `Each one is a selector losing an argument with another selector${worst ? `; ${esc(basename(worst.file))} alone carries ${worst.count}` : ''}. Fix the specificity at the source and they stop being necessary.` });
|
|
750
|
+
}
|
|
751
|
+
if (neverImported.length >= 3) {
|
|
752
|
+
c.push({ score: 15 + neverImported.length, metric: 'neverImported', after: 0,
|
|
753
|
+
title: `Decide about the ${n(neverImported.length)} components nobody imports`,
|
|
754
|
+
sub: `${neverImported.slice(0, 3).map((x) => `<${esc(x.name)}>`).join(', ')}${neverImported.length > 3 ? ' and others' : ''} sit in the system with no callers. Adopt them or delete them: either answer is better than a system with rooms nobody enters.` });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// What each move is actually worth, then rank by payoff for real.
|
|
758
|
+
for (const item of c) {
|
|
759
|
+
item.delta = 0; item.target = null;
|
|
760
|
+
if (!item.metric) continue;
|
|
761
|
+
const now = bigStats.find((st) => st.metric === item.metric)?.healthValue;
|
|
762
|
+
if (now === undefined) continue;
|
|
763
|
+
if (item.after !== undefined) {
|
|
764
|
+
const gain = bandPoints(item.metric, now, item.after);
|
|
765
|
+
if (gain > 0) { item.delta = gain; continue; }
|
|
766
|
+
}
|
|
767
|
+
const nb = nextBand(item.metric, now);
|
|
768
|
+
if (nb && nb.gain > 0) item.target = nb;
|
|
769
|
+
}
|
|
770
|
+
const top3 = c.sort((a, b) => (b.delta - a.delta) || (b.score - a.score)).slice(0, 3);
|
|
610
771
|
if (!top3.length) return '';
|
|
772
|
+
// The headline is the score with every move applied together, not the sum of
|
|
773
|
+
// the parts: two moves on the same tile must not be counted twice.
|
|
774
|
+
const applied = new Map();
|
|
775
|
+
for (const item of top3) if (item.metric && item.after !== undefined && item.delta > 0) applied.set(item.metric, item.after);
|
|
776
|
+
const after = projectedScore(applied);
|
|
777
|
+
const words = ['One tweak', 'Two tweaks', 'Three tweaks'][top3.length - 1];
|
|
778
|
+
const head = healthScore !== null && after > healthScore
|
|
779
|
+
? `${words} · <b class="proj">${healthScore} → ${after}</b>`
|
|
780
|
+
: `${words} to increase your score.`;
|
|
781
|
+
const chip = (item) => item.delta > 0
|
|
782
|
+
? `<span class="delta">+${item.delta}</span>`
|
|
783
|
+
: item.target
|
|
784
|
+
? `<span class="delta delta-target">${item.target.target === 0 ? 'clear them all' : `under ${n(item.target.target)}`} · +${item.target.gain}</span>`
|
|
785
|
+
: '';
|
|
611
786
|
return `<section class="glass pad">
|
|
612
|
-
${sectionHead('Where to start',
|
|
787
|
+
${sectionHead('Where to start', head)}
|
|
613
788
|
<div class="ledger">${top3.map((item, i) => `
|
|
614
789
|
<div class="ledger-row start-row">
|
|
615
790
|
<span class="ledger-idx">${String(i + 1).padStart(2, '0')}</span>
|
|
616
|
-
<div class="start-body"><div class="start-title">${item.title}</div><div class="sub">${item.sub}</div></div>
|
|
791
|
+
<div class="start-body"><div class="start-head"><div class="start-title">${item.title}</div>${chip(item)}</div><div class="sub">${item.sub}</div></div>
|
|
617
792
|
</div>`).join('')}</div></section>`;
|
|
618
793
|
}
|
|
619
794
|
|
|
@@ -639,7 +814,7 @@ const stack = [
|
|
|
639
814
|
const html = `<!doctype html>
|
|
640
815
|
<html lang="en" data-theme="${themeName}"><head><meta charset="utf-8">
|
|
641
816
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
642
|
-
<title>Design-system diagnosis
|
|
817
|
+
<title>Design-system diagnosis:${nsMark} ${esc(repoName)}</title>
|
|
643
818
|
<meta name="author" content="Greg Kozakiewicz · gregkozakiewicz.com">
|
|
644
819
|
<link rel="icon" type="image/png" sizes="32x32" href="${GK_MARK}">
|
|
645
820
|
<link rel="icon" type="image/png" sizes="192x192" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAACXBIWXMAAAAAAAAAAQCEeRdzAAAQAElEQVR4nO1de0xVV/Y+tJ2mVSeAEBQlokMUNFZsUDSOjqmisaNNRqcRrbE+Ym0NLWJ8jKGCkRlMO9ZowbSOjVaMUyW+CLYayoypWkypNlRrfGAc0OioKT5SO5oWrvf3zfnmrN/23HuP1lYvj/X9cXO4HDaHuz/W+tbaa69tWQqFQqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoUi7Hjsscd48atf/eqpp57Cl08++eTTTz8d3qdStBg8/vjjXbp06dWrl3wJGuE1vE+laBkgUUaPHr1t27YBAwaY3xLLpFCEBAjUvn37xYsX+/3+HTt2dO3a1fxWGB9M0QJAG5OWllZRUXH79u3Gxsa8vLyOHTviTXixiIiIcD+gorniMRu8fuONN3w+339s/Pjjj7m5ubQ9TzzxRFifUdGMAfaQH3Fxce+99x78161bt77//ntcnDp1asqUKZYdlKkRUoQEYnXwY8KECV988QUsEPzXDz/8cO3aNXDo2LFjv//973EPblAprQiOdu3a4bWwsBCMaWpqunPnDjgEAsGL4Z3y8vLk5GTeqXZIEQSwQNHR0bt27QJdSBrQCDLoxo0bMEV4LSkpiY2NtTQcU7ggFuXVV1+tr68ngWCBfDZwQT7hdcGCBSBZ4A8q2i5AAgmvSktLQRR4LtgeXwDwLcjq8ePHQ03zR5RAiv/GX+RBSkrK119/DZbAYQWyh6oI3z1z5kxGRgbu79ChQ7ifXdEMIFFVbm7u1atX6aoCCQT2gEPXr1/HDfv37x84cKClmSGF5ZCgffv21dXVFM5B/RcJBClNPbRly5bOnTtzBKhvOLWw/hGKcIBJHbqwIUOGMOUD/9VkIKgMun37Nm4uKiriKgfi/6eeeircf43ikUPkc1RUVH5+PvPOVNBBCUQNBCN0+fJlXDQ0NOTk5MD8mAOG769RPEJE2GClGL7s27fv0aNH/TY8CMS0EF7pxWCrPvzwQ/g+y15q1Qx1GwJNxeOPPw4LhImfMWMG2XPr1i2Qw8OFgV64jb7s/PnzWVlZpKDJHrIzbH+b4hGAqWTOfVJS0vbt2yXv7GGBhEBwdrj+8ssv8bMmV3Atalo51JpBI0ECjRgxoq6ujo4JXikUgSQKg4LGBV4/+OADF0t0qbUNgc4LF9nZ2WDDjzZc7HERCK+wPVeuXKH/Gj9+vDmgUEdktRqhVgtMLb0Y5PPu3btBCHKoKQCBURhziWVlZTExMa4xeaHZxVYOOi/O96xZsy5evAhmBDqvoBqIb0IqLV682ApmY6iBXDpaTVGrAsvmLTv9s379ejE/jTa8CUSVvW/fPigny1lHM6UPuYIvWV1k2ZRSYdTawGkeOXIkV09Z++xNIBHRuP+tt97iPsNAAuEiISEhPj6eRfhqe1obZEbhxZYuXfrdd98x/dPowNsCgWdweRMmTLCcEmkBpQ+IlZOTU1hY+Jvf/MayVznMVLWixUMI1Llz58rKSiaUxfx4EAjXTDb+/e9/T0pKspxyDjEzXA6Ljo7GsLgf7ISvxLfgKLWCsfVActDwX+fOnaP/ajQQSCB5kwSaN28e9Y25Ai80Sk1NvXDhAm7D6xtvvGGFSEyra2uRkOgdDFi9ejVXTwPXv0zgnps3b5I98HT19fWDBg3iCKZdoQbq2LHjwoULYc+gyqGZampqxo0bZ+pr2iTLIZBZDKloARACdevWraqqirnBUNQhQBqwAW6OVNu8eTMEsuVs4RCQBz169Pj8889xP36KnDt48CBLz/B7zaYflpF41BitBYB+hOzB/EEF0/xwx4UHjWicQAg6u8zMTMvJJJnjkwQvvPACkwL8KXKuoqICxLJsDokFshzNZGnisUXAXPzq1KlTSUkJCYEojPF5KFBlgxO4QMzfvXt3y47gTLPB65iYmFWrVpE0PzrglytWrOCvZv4p8NkezYegeHBwjjmLaWlply5dAnsaGhromzw0ENfeucmwuLiYkZfZbAoj07ANGzbs8OHDIqpEYOFXXL16ddq0abyfdojPoxqoxUBsBhzHnDlzMK/c7SVrqKGiMLokvP7rX//KyMjgfJuzLlJ67ty5IpW4LZpqGkzF69mzZydNmmQ5JHbpHjVCzR2YNhqPZ599ds+ePRQ990Mg7tCglIGTMpt4WDYdSSC4to0bN5JtFNEYii6SNgx8ggeESOIPyl4iRYsBJ2zq1KlwKCSNizpBCQQSUGvn5uZaRhwnIJ8mT5584sQJWDWpeQVpOD449IMNjLN79+6UlBQYMK0cannAhHXs2PGvf/0rdUkgdYISiItf1dXVQ4cODTos9RDlM9hDcwUm0RThF5FJjOOA0tLSuLg4SytfWwrMxB1ETFVVFaOk+yQQLdDatWvbtWsXWH9IFQyjwlURYQk5xPCNYgsXN2/eZOEsSMz9QE/YUBo1a5grnQsXLoQ9oJe5H7AC+uLFi7NmzZLRZGR2b8VFTk7O+fPnzWhOqs/IJNFblERAdnZ2ZGQkRoMBU1/WfCFL5bAfCQkJmzdvDtw66AEmcvbu3QvpbRnL75xyfBkVFQUGbNiwgZ4LBDLLPwizEo0MwwN88803bAGr66zNFEIdmaEXX3wRoTgDpaB1hqEI9Oabb3IEcEUcopgNcKu2tjawJYMHgfBaU1PDeo+gqUVF+CGmQnI2q1evpluh1A1FIJcYOn369PDhwy27fJFBO4flehZeQS/qJCa1PQjE5BClEn5KqdPcIQtPmObU1NQjR45IVtAXAk3OAhbVD/D222+ziYJZvAFS8svY2Fi2ZKBMNrnoczZymASSnKSrhbmiOULiZMx3VlYWd1NwhaEpWP8N03KQSZjy0aNHWwGtgGQldcSIEWzJQPXj8oDmsLK8CkO1adMmRmGEiujmCxII079lyxZOIdyNaRgC2cNwCRegGveeWk7GmVvJoIRkUfb999+nUQlMAbhGZlqIMd20adNkLUwQ3g9KERKYm/T09H//+98UH6yfD0UggtoZN+fm5lKpmHIK79B/DRkyhNE7TZo3gTAa80DwpCkpKXw2rqoqgZo1uHrKWeSqAuYyFHUwwZhmZp9xc1paWuCA0k4qMzPT7zQEpunyIBBvwK9et24dx3nMgcke11qbIpzgNMMH7dq1iwKF29rNdF8gpILsk08+oXx2zSiThz179vzoo4+YJ5RUoQeBKIBOnDgxYcIE2TsmEi0wIjMX/CNC4CF/fm0bsBOc6VGjRsGi0AA0Ou1XPQjESBtKZdGiRZxFieYw5ZBTkZGRuB43bhzuIS0C/VcoAm3bti0uLs4lgPjAv/71r+Pj4zMyMiRpyYUO8XFKoEcKLl3JwU1kBqVuKP9FR0OlcvTo0d/97nccykUgCpclS5b4nYaKQevRXCNzHSMvL0+e0EUF0B2kPHToUHFxMfOfUOvMgiqBwgDRuQcPHnQ13/AgEHmGOxG1gXyiUQQ0Hv3795c9ZaGqGV1u0W/3dn3uueesAOrgUbmkv3z5ctx28uTJF154wVziUAKFAZyA1157jdWl98kezjRiq7lz53IcF4Ho1LKystjkJWhDDxeBWA6Lm8EPUzi7EBMTs2/fPtoz2L/BgwfzTi7XK4HCgB49eqxdu9Zn52BM9nhMNgXQP//5z2eeeQYzBLciHelFSst5Gty7c0/zwzj/22+/ZT+hqKgoyzAqUqwNcc1cQ0NDA15ra2tl/xBsIcWQ1FMrhx4u+MliSo4cOcJCHNPReAheZoDeeecdy146ZWMNc6aBsWPHsnj+nmUhPievjd+4fft2JgVYne0yafhF4DrtGTPm+Fm40W7dullO4jvC7p9nPpIS6GGBn+zKlSu5dgH1KksNQavGxDJx7w4oYhmLX5wwEgivRUVFMBIY08MC8RfR/DTZSYFXXnkFcVaEE7cLKTlscnLy6dOnxaqBSXCR4P2KFSuYSohw9iTRqWn68aEjKSkJnogEMqP3oJWHpvlZt24d5AgXLjhUhFHT2KlTJ6hyzjSzSqFsD9dWeee5c+egaayAdjCyUQT0ovekt+UZU0xpzp8/H9Sh85LYXjgUts+3tUKCF+hcObhJUjVBS1fF/PDO7Oxsy55UOeAywum9igvomLq6Oi5+eStoRnMk0NatW1n9QxLIozJTlZCQsG3bNgptmiumEkip48ePT5kyhX9XZGQka5JECYXjM269iDC67JaVlfmdY1PEp4TyX3gT0wax8tlnn1GpYBzhomwJwjtQKix8DtqR0wR3aLAyevLkyXRA5tZmUIEb7DMyMiCx+Rg+p5MVwd914MABbrCnBlIB9FDAdXJ+rH379j158qRUCTaFkD5kD1N8nKrCwkIWWpidD2SqEhMT9+/fz+ULb/Y0Ov3IceeJEyfg+CyjLywGhB6i+YFRWbp0qaQ6fU4VkYzD0oBPP/20d+/eLApQ2/OwIIdz5+bmskyHXiYUe4RDjLShW1n9Y86QlP6AnTNmzJBhvQkku+Iht9esWcNBZAOGmSCANuJGEdYrNhkrLTRjkEH8E2D8QDvzkRS/MEggSGDTToTyXC5rAVRWVgYWH4JMXOmEFYFS4Z0e6sfFodra2jFjxtADCi/5Sv81bdo0bv0JmqBqsivwWTKLaCA/P5/PpocD/fIQB/Hb3/6WPcK89yybM0QP8qc//YkT41pGIIEw7NWrV71beZhg6eOePXvw4+aqluVsb4UhASnfffddv3P4RlAaSaEIl3ghp+TvffQfcmsG/6HBoWXLlkkUcz/mh5OH2Kpfv35WQPEG/QX+77l6+pPMD6QxforjuE5j4dM+//zz33zzDYMvqQwhhyS3Kc/JNZmvvvqKJ9irEfqFwZnu2bMn08TSO9ybQJxpzNOOHTtoJFwbtThPUOXMKt1TPhOsXsWTDB8+XIRz4NOC6ySlz9jFEbTitslJoOOB4WrhFh/ZB9u2MH36dL/TEqopdOhu/lvj/tOnT7/66qscwUUgfvnKK694Zw6DEmj9+vUwXS7NK0xKSkrikj41kDeBhEO8LigosO5uU6T4WeCsYErYeb7JVjbeAoiTwZmurq7myqU5mlxERUVRqcCDeJCGv8jn1DQipnv99dctw9e4jNDEiRO50dGjxNYXsDcIgBKaOXNm4ICKBwfdBMQB5DN1rrej8Tmnn9LTvfXWW+ZoInU57HPPPcflC4/VUwnluBbB4sPevXtLupl4ygavN2zYIKLKgz3SKg/XTFaVlpYmJydbSqCfCXPXKYEwShIq3sKZW1QZKB0/fnzkyJHmOJLqpf/Ky8trsqMh79VTn1P6Y26Ihlg22yfIyZhgwLFjx/xON0UPC8RTzPjYJBA7FcE5ujrFKn4aWIsukmXgwIGffPKJ3+mG4YFGo4UqsHXr1i5dupgjy0IBJrtr165yHpR3AZD04sSUwze9+OKLlj3NPDRDRubF/PnzeXJv0DU102fRIyOu5OC1tbWI3Sw9ROHngytfmB7+I86ZM4dpYuKeHOJ/840bN9hP3jyqfiQsIQAAD1ZJREFUUvpmWrYqP3v2LEjZ0NBgzrQpSpocqybFh5BiiYmJVkDykK8xMTEVFRU+p9jDg0Bi1bidDfdDjfGcMhamKR4csu/Csl3DBx984Lc3c92TQNS5IAQ4BPmckZFhGZuXWaMjBNq4caP0qwuMj1wEwiuL10A7VzQX4VTOg+79+/evr69nirnJ1vuhCESJBki3IZY1mmu9igcHOETzM2rUqAMHDrCfnGRWvNM/sD1g25///Ofo6GhzTOm8YdlFsTU1NX57t2HjvbZz0IVh5MOHD/fp08cKSBZT/eDXLVq06OrVq01OqHhPCwSe0V4eOXKE8lnPkvoFwCI9NsooKipiMIyZBi1YROYL3fiHJKurqxs3bpxlmx+MI//TEivNmDFD9kQLHUMRiAUhuKGgoCCwG7AgJSXlyy+/5NN6VJiYwF/ENyHMuStNClsf3cfdioHJnj179j/+8Y+LFy9KWRYbXAZdNpd39u7dm5qaKuOw0k++7Ny5c3l5udSYehCoyRHR+NWQxkOGDOEIQSd40qRJ0jgxlI00eclO0ywWGDp0qEg0JdDPgiuuwX/kgAEDcnJySktLEafAQYialn90mRsqaCju/fv3L1iwAD9oejGxQ6zzktROUAKZpKQzqqysRGwoq10CRkyI6YqLi/lU97MlyOdUmeFN6G6zrkgJ9EvCtByDBg3Kz89HIM3gxecsM7k8mpTsnDhxAh4QUTeCeTnrFDEO5JFrUdaDQDQ/8HdgJKfWtdhJkQ5ScvXU5ynRzPH5kBcuXEDkL/8tSqCHCGqj+Pj4559//r333jt16hRnl1ES0zk+p4iMEpXq+/PPP8/Ly+Nput27d6+qquLiV6CvMSdY1skxGn5XQkIC66lduwcZM0Ie+Z06f4+VFpOjlM8wq/369WNJGv2XNvH4hUFxYP5T4oPu2LFjWloatPCmTZuYuJPVA+nz4jdw6dIlBPZ/+9vfSkpKGDx7z3GTU3VKe7Z161bJ95jgZCM027Fjh//uI1q9CcQv8RgYWepVBOH6qFs5+D/Kne18B/+4SUlJf/zjHwsLCz/++GOwhE7klg36OFoFYRKdlyT6glogYQ9zxKdPnwZT5TFMC0RdNW3atPr6epHP90Mg8hKGjQe1cFuPuaFM8VAgu664DVQ+a7Bq4MCBM2fOhI05dOiQHK5Dt8IjUfilubQuGSDOrsy3TD/ZJiLXups9liPRuHqKm++TQCKtEAzCM1pOnKgW6NFBasS4T9l8f8yYMWvWrIHDgj6VIwrE3lAVSaKPGYGmu+vzxfwwzF69erVlt/kx9+4I0tPTjx49ituYQvQgkM8p4ZCOIsuXLwdvJFeuGihsoGEw17EhkhB/bd68GQ4Iro3N7WSNk6QBva5fv06SueZeTsA4duwYj3Iy64pMIDBkSTXzjd4WiG/Sf4HirhpW3RsfNgR+6PBxT9tISUmZPXs2FZKsPf3gQGJ+KdDhApZ4wJUrV8bFxWE02QAvRgIGIzExEW6I4v27777jwkhQAvnsGjfwWI7thfxnsQAeMrDVS1g+xrYL7w8dPqJbt24jR47Mzc2trKwU0rA5Jju8kkzSVlxYtWrVKi6Syy+SbsC4gHHiRkdQh9S8pwtrchZZs7KyLGcftKuzhxIozKDK5sqXWVsTGRnZu3dvBD7FxcWHDx8WS4MZpf2gNiIVaI3OnDlTVla2ZMmSIUOGSF6bO2XxW0AvUkcSm94uTBwZ/BcPWjCDSrMyRBFOUFxLJ01WF5laG/4I+qOgoAAOiPXLorVZoMMaj0Yn/sf7Bw4cKCoqmjx5ct++fSl7Y2NjWXwousqjXpsuTGoK8vLy2CpEN/G0DEjaRppBE5DG06dPp9bmIbr0Xz5nQyAMjJwBDVy7dg1R/Zw5cwYPHjxv3jwKbXPxK5TVaTJOacVPsdGnBlwtD7LoHWGczI13evXq9dprr+3evRtam5NN34QLRGqsJJGNpADePH/+vPd2DpfbEmO2c+fOnj17WnqmWMsF2YOY3zWFCQkJ0NrwL9Ao0n8IUfrly5dhe7jZg0cj+p2mY77QxUku80MCXblyZerUqbSCaoFaMLj3QwjETmF0c9DazzzzDKZ5w4YNbFBnam14NDlaSnJLPs9eM03GXsezZ892797d0gPCWxkYtZnyyLIrX4cOHTp37tyPP/4YE+83ADLBi0nwb+a1G53TCwMJBLe4fft2uk7detE6QSPkcm2IvDIzMxH8Hzx4kN16aYFu3LjB5KSscIXqHEKjdeHChZdfftnso6hotSCTYC1k02D79u0HDBgAg7Rr164zZ87IRhFRx40BcBGIW61ddk7RykFVFB0dLWYpJiYGcXhBQQEIAX0tCxS+u/vFCoGYVYL0ZqNqRRuC6Fxm/GRDKhRMXFxcp06dJk2a9P7773/99deNTpcWrrKZ5odppJqamj/84Q8aurdpBJZb4MuoqCi4tuzs7C1btrBc32/XIXGpn2tteKesrAx3muf0hOmPUDQbmGSCcerTp8+ECRP+8pe/fPrpp6KQuIIBLF682DKOLQ/fUyuaGVztRDp06IDgf+HChTt37jx16hSZdOjQoUGDBllOR2k1P4r/gUlt5iTBJDOZhHcgelatWnX8+PElS5aAWIzslECK/4dpe+QcBbNvEF67dOnCBXzpKK1QuGEyQ2yMuRnIUu2suE+IUpbt7rrpXfETYO48lFVbNT+PDuYHLamXlvXpmwSSHe8t609ohTAby0UEbHluhjMUEYBwP1FbglCEmxYsJ2bmPzRXFczjB10bG3Ta2iioNy07t9utW7esrKx58+YlJSXxu+3bt5dCdGZcWEIvDFMCtXXIlNO6rFix4tatWzU1NRs3bszMzIyNjZU7QaZ27dpJeY2exK74L+ieZOLl4JLr16+fPHmyrKyssLBw3Lhxru5jrlMjTQ2rBGpbkHaqnPjXX3/90qVLV69eNbtFV1dXFxcXz549Oz09XZoTyI+zpNBcsFQatUWQARMnTmRl++3bt3lQvFnQXlVVtWjRoqFDh0IksdGp68cVbRdkwOjRo7/44gsSiG1QuZePp6+TT/BuFRUVkNspKSk8XJeIcHpAKdoi6HT69eu3c+dOv3N8SZPTkJWFf+Y2ie+///7y5cslJSXjx4/v2rWrGeFLXbOiDYEESkxMZAuwwHr1RrsJJvdFyJkBkEpnzpzZt2/f22+/PWrUKB6MGmF31dCIrG2B8/3kk08uX77ctQXdbJUiZBITJUWlp06d+uijj2bOnAmR5GovL/vkufVCidU6QQUzf/58OimhjnSIDuwTTe/GbTQEIjgopPz8fAT/vXr1kgMlZcuOHAytaG0ggaZPn474SwSQ6whSj63Et27dMndvQWtv2bJl1qxZffv2RcjmsjpBWyAqWjbod8aMGVNbW8tNn/dPIN7DBuRmo1ZQCmHdihUrEN9BYJnncymBWhuYUUxPT6+urm50DlDyGyexezTHkIaHuICyZudo5gKa7KMCv/322wMHDuTm5o4YMYJa27IpK8VfKrpbPGiBevbsWVpa6ndOmwt1frYL5s3SO4yH/UgeEjeARjBv5eXlOTk5qamporXN2viw/f2KnwlOXnx8/Jo1a/zOUSb3wx7CRTXxbjRONEWikM6dO1dZWbl27VpEbT169JBn0DYaLRgkEATv0qVL/c65u/dPIBd1mgL6H0ijVrNNwsmTJ0tKShYtWjRs2DDXQpuihUE6Ms+ZM0eczgMQSGgU2ECDkDzkDRu8Lioq4sKIKqGWCpm5zMxMupsHZo/POIkyEGxBx46IVOifffbZ8OHDdRm/ZSPC2aTXv3//CxcuSGenOz8RQiB4Kx5U6LJGZM+VK1e4ZrJ69WoIL7OaVtFSQRmUnJzMM00ejEB37LMsmJ6+YzfKZIQvBMI7Fy9eZLJxwYIFrsoQRQsG4+rExMS9e/f6nCbOD0AgJoQYxrucF8+o89unZE6dOhXCmcdG8wHUhbVscCLj4uI2bdrU6Jzd/FPZI8z7jw26M+lnSMP21VdfjR071rKVe5cuXVi6j1dZO1O0SDChFxUV9c4774hYCRTI9+QQzQ+VuHlMmM8+lL68vDwtLc2y7Q0PRmUX6cCGm4oWBhIIPiUrK4sEkmX5n0QjsOfatWty5CVtEsY5e/bs2rVreSahnLChaD3gjMIkTJw4kRkaCaPM8iBvAvntxvJwXo32keF3bE2NNxsaGt58801ZCOPujrD+uYpfGuJBBg8eXFdX53fOew9KoFDWSOwWrmGHWOOBsO7ll1/mIYfgDYycbOdQ4dwK0b17d0RJ1ED3QyDhEC/otuDCGMxXVlaOGTNGWl5atqnj6clKoNaJ6OjoqqqqO/YxXmZ5a1ACNRmlZ36jCzgF0J49e9LT0zmsmSrUM29bG0xLAF+zY8cOltCLFxO6mEySL+84K1zcU0YltGnTJu60ZzW0pTvIWjFMAsFUFBUVkQeuQCwogXxO0Q+DdryeO3du2bJlpIsUaeiZga0fsiafnZ196dIlWpSgBDIrfoQ9sDp45/jx4y+99BLHYcNUbsmw7m6UqWiFEKmLSL6+vp4E8hn1Yi4C+Zyzc3gcMyXz2LFjaXXkxMIIo4NYuP9ExcOETPCzzz7LY7apgcAP023xAu9fv34dDKPQxuvmzZtFMovDinA6nWmiufVDittjY2O5T57rEjdu3JCqZylOZU0Pz0fGxbvvvisnBJqWRmvm2xY40/BB5eXlLNnhfh1TBgmBmPKBrVq6dClLCiMjI2WPswyoBGpDEOOxbt068oMLGkEzh3gf7JkyZYplc84kjQyoBGpbkJleuXIldbGcNSmA7aEqqqioGDZsGO/XKF3xXwiBcnJyGMmTRuANTwukC4NZWr9+ff/+/XmzFmMo/gch0EsvvVRbW8uc8h2n2RRX6SGZCwoKUlNTeaerqXTYHl3RHCAMyMjIqKmp8dvdW2Q7DswPWLVs2bK4uDjLdlu8X/vDK9zo06fPwYMHuYGLeULg8OHDU6dObdeuHUMt6bOhSlnhRufOnffs2UMCsaMUJPPgwYMto4+93KwEUtwFdjssLi7mYipo9OGHHyYnJ1tORVjQH1EOKf4HWhd2vLt582ZRUVF8fLz5LRMRAXjUj6tobmBMvnjx4rq6uvz8fGlWH2o5XUmjuAskxLBhwyZNmsRDM0T0ROhZ2or7AQ/EsGyf1aFDB3N1Xe2NQqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVC0Wrxf0UR0D7z0nlYAAAAAElFTkSuQmCC">
|
|
@@ -852,9 +1027,30 @@ const html = `<!doctype html>
|
|
|
852
1027
|
border-top:1px solid var(--line); margin-top:40px; padding-top:22px; color:var(--dim); font-size:11.5px; }
|
|
853
1028
|
.foot-left { display:grid; gap:5px; }
|
|
854
1029
|
.foot-sub { color:var(--dim2); }
|
|
1030
|
+
.scan-id { font-family:var(--mono); font-size:11px; letter-spacing:.04em; }
|
|
855
1031
|
footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
|
|
856
1032
|
footer .brand:hover { text-decoration:underline; }
|
|
857
1033
|
footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
|
|
1034
|
+
.spec { margin-top:18px; }
|
|
1035
|
+
.spec-rows { display:grid; gap:2px; margin-top:8px; }
|
|
1036
|
+
.spec-type { display:flex; align-items:baseline; gap:16px; padding:5px 0; border-bottom:1px solid var(--line-soft); }
|
|
1037
|
+
.spec-type:last-child { border-bottom:0; }
|
|
1038
|
+
.spec-val { font:600 11px/1.4 var(--mono); color:var(--dim2); min-width:104px; flex-shrink:0; }
|
|
1039
|
+
.spec-sample { color:var(--text); line-height:1.15; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
|
1040
|
+
font-family:var(--disp); letter-spacing:-.01em; }
|
|
1041
|
+
.spec-more { font:600 10.5px/1 var(--mono); color:var(--dim2); margin-top:10px; }
|
|
1042
|
+
.spec-grid { display:flex; flex-wrap:wrap; gap:16px; margin-top:12px; }
|
|
1043
|
+
.spec-cell { display:grid; gap:7px; justify-items:center; }
|
|
1044
|
+
.spec-box { width:46px; height:46px; background:var(--deep); box-shadow:inset 0 0 0 1px var(--cell-ring); }
|
|
1045
|
+
.spec-shadows { background:#f2f2f5; border-radius:14px; padding:20px 18px 14px; gap:22px; }
|
|
1046
|
+
.spec-card { width:84px; height:50px; border-radius:10px; background:#ffffff; }
|
|
1047
|
+
.spec-shadows .spec-cap { color:#71718a; max-width:104px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1048
|
+
.spec-cap { font:500 10.5px/1 var(--mono); color:var(--dim2); }
|
|
1049
|
+
.start-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
|
1050
|
+
.delta { font:700 11px/1 var(--sans); letter-spacing:.04em; padding:5px 9px; border-radius:99px;
|
|
1051
|
+
background:var(--ok-soft); color:var(--ok); white-space:nowrap; }
|
|
1052
|
+
.delta-target { background:var(--amber-soft); color:var(--amber); font-weight:600; }
|
|
1053
|
+
.proj { color:var(--accent); font-weight:700; }
|
|
858
1054
|
.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
1055
|
.nsim { color:var(--dim2); font-weight:400; padding:0 2px; }
|
|
860
1056
|
.gift-sec { margin-top:16px; }
|
|
@@ -896,7 +1092,9 @@ const html = `<!doctype html>
|
|
|
896
1092
|
.gk-mark { display:inline-block; width:15px; height:15px; margin-right:5px; vertical-align:-3px;
|
|
897
1093
|
background:currentColor; -webkit-mask:url(${GK_MASK}) center/contain no-repeat; mask:url(${GK_MASK}) center/contain no-repeat; }
|
|
898
1094
|
.author:hover { text-decoration:underline; }
|
|
899
|
-
</style
|
|
1095
|
+
</style>
|
|
1096
|
+
<!-- rmds-schema: ${NS_TAG} -->
|
|
1097
|
+
</head><body>
|
|
900
1098
|
<button class="theme-toggle glass" aria-label="Switch between light and dark mode" onclick="(function(){var r=document.documentElement,t=r.getAttribute('data-theme')==='light'?'dark':'light';r.setAttribute('data-theme',t);try{localStorage.setItem('roast-theme',t)}catch(e){}})()">
|
|
901
1099
|
<svg class="only-dark" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="7" y2="7"/><line x1="17" y1="17" x2="19.1" y2="19.1"/><line x1="4.9" y1="19.1" x2="7" y2="17"/><line x1="17" y1="7" x2="19.1" y2="4.9"/></svg>
|
|
902
1100
|
<svg class="only-light" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
|
@@ -943,8 +1141,8 @@ ${componentsSection()}
|
|
|
943
1141
|
|
|
944
1142
|
<footer>
|
|
945
1143
|
<div class="foot-left">
|
|
946
|
-
<div>Generated by <a class="brand" href="https://github.com/pencilrebel/roast-my-design-system">roast-my-design-system</a>,
|
|
947
|
-
<div class="foot-sub"
|
|
1144
|
+
<div>Generated by <a class="brand" href="https://github.com/pencilrebel/roast-my-design-system">roast-my-design-system</a> <span class="ver">ver. ${VERSION}</span>, free on npm and as a Claude Code + Codex skill</div>
|
|
1145
|
+
<div class="foot-sub">Designed and built by <a class="author" href="https://gregkozakiewicz.com"><span class="gk-mark"></span>Greg Kozakiewicz</a> · <span class="scan-id" title="scan id">${scanId}</span></div>
|
|
948
1146
|
</div>
|
|
949
1147
|
<span class="creds"><span>Non-destructive scan</span><span>Read-only</span><span>Paths are real</span></span>
|
|
950
1148
|
</footer>
|