designlang 12.0.0 → 12.2.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/.claude/launch.json +6 -0
- package/CHANGELOG.md +53 -0
- package/README.md +82 -476
- package/bin/design-extract.js +168 -0
- package/package.json +1 -1
- package/src/formatters/badge.js +90 -0
- package/src/formatters/battle.js +338 -0
- package/src/formatters/grade.js +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// designlang grade — standalone, shareable HTML "Design Report Card".
|
|
2
|
+
// Renders the existing scoring output as an editorial-style audit page that
|
|
3
|
+
// embeds the audited site's own design language (palette, type, spacing) as
|
|
4
|
+
// visual evidence. Self-contained: no external assets except Google Fonts.
|
|
5
|
+
|
|
6
|
+
const FONT_DISPLAY = 'Instrument Serif';
|
|
7
|
+
const FONT_BODY = 'Inter';
|
|
8
|
+
const FONT_MONO = 'JetBrains Mono';
|
|
9
|
+
|
|
10
|
+
const DIMENSIONS = [
|
|
11
|
+
['colorDiscipline', 'Color Discipline', 'Palette breadth, brand clarity, restraint.'],
|
|
12
|
+
['typographyConsistency', 'Typography', 'Family count, weight discipline, scale length.'],
|
|
13
|
+
['spacingSystem', 'Spacing System', 'Base unit fit, value count, rhythm.'],
|
|
14
|
+
['shadowConsistency', 'Elevation', 'Shadow count and tier discipline.'],
|
|
15
|
+
['radiusConsistency', 'Border Radii', 'Radius scale tightness.'],
|
|
16
|
+
['accessibility', 'Accessibility', 'WCAG contrast pass rate.'],
|
|
17
|
+
['tokenization', 'Tokenization', 'CSS variable depth.'],
|
|
18
|
+
['cssHealth', 'CSS Health', 'Unused rules, !important, duplicates.'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function esc(s) {
|
|
22
|
+
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function gradeAccent(grade) {
|
|
26
|
+
return ({ A: '#0a8a52', B: '#1f6feb', C: '#b08400', D: '#d2691e', F: '#c43d3d' })[grade] || '#1f1f1f';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describeScore(n) {
|
|
30
|
+
if (n >= 90) return 'Exemplary';
|
|
31
|
+
if (n >= 80) return 'Strong';
|
|
32
|
+
if (n >= 70) return 'Adequate';
|
|
33
|
+
if (n >= 60) return 'Below standard';
|
|
34
|
+
return 'Needs work';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function arcGauge(value, accent) {
|
|
38
|
+
const v = Math.max(0, Math.min(100, value));
|
|
39
|
+
const r = 36;
|
|
40
|
+
const c = 2 * Math.PI * r;
|
|
41
|
+
const offset = c * (1 - v / 100);
|
|
42
|
+
return `
|
|
43
|
+
<svg viewBox="0 0 88 88" class="gauge" aria-hidden="true">
|
|
44
|
+
<circle cx="44" cy="44" r="${r}" class="gauge-track" fill="none" stroke-width="4"/>
|
|
45
|
+
<circle cx="44" cy="44" r="${r}" class="gauge-fill" fill="none" stroke-width="4"
|
|
46
|
+
stroke="${accent}" stroke-linecap="round"
|
|
47
|
+
stroke-dasharray="${c.toFixed(2)}"
|
|
48
|
+
stroke-dashoffset="${offset.toFixed(2)}"
|
|
49
|
+
transform="rotate(-90 44 44)"/>
|
|
50
|
+
</svg>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function colorSwatches(design) {
|
|
54
|
+
const all = (design.colors?.all || []).slice(0, 18);
|
|
55
|
+
if (!all.length) return '';
|
|
56
|
+
return all.map(c => {
|
|
57
|
+
const hex = esc(c.hex || c);
|
|
58
|
+
return `<li class="swatch" style="--c:${hex}"><span class="swatch-chip"></span><code>${hex}</code></li>`;
|
|
59
|
+
}).join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function typeSpecimen(design) {
|
|
63
|
+
const families = (design.typography?.families || []).slice(0, 2).map(familyName).filter(Boolean);
|
|
64
|
+
const sizeOf = s => typeof s === 'number' ? s : (s?.size ?? 0);
|
|
65
|
+
const allScale = (design.typography?.scale || []).map(sizeOf).filter(n => n > 0).sort((a, b) => b - a);
|
|
66
|
+
const scale = allScale.slice(0, 5);
|
|
67
|
+
if (!families.length) return '';
|
|
68
|
+
const head = families[0];
|
|
69
|
+
const weights = (design.typography?.weights || []).map(w => typeof w === 'object' ? (w.value || w.weight) : w).filter(Boolean);
|
|
70
|
+
return `
|
|
71
|
+
<div class="specimen" style="font-family: ${esc(head)}, ${FONT_DISPLAY}, serif">
|
|
72
|
+
${scale.map((s, i) => {
|
|
73
|
+
const lines = [
|
|
74
|
+
'The quiet authority of restraint.',
|
|
75
|
+
'How the page reads at rest.',
|
|
76
|
+
'Form follows feeling.',
|
|
77
|
+
'Calm hierarchy is a craft.',
|
|
78
|
+
'Notes from the audit.',
|
|
79
|
+
];
|
|
80
|
+
return `<div class="spec-line" style="font-size:${Math.min(s, 72)}px">${lines[i] || lines[0]}</div>`;
|
|
81
|
+
}).join('')}
|
|
82
|
+
</div>
|
|
83
|
+
<div class="specimen-meta">
|
|
84
|
+
<span>Families · ${families.map(esc).join(' / ')}</span>
|
|
85
|
+
<span>Scale · ${(design.typography?.scale || []).length} sizes</span>
|
|
86
|
+
<span>Weights · ${weights.join(', ') || '—'}</span>
|
|
87
|
+
</div>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function spacingScale(design) {
|
|
91
|
+
const raw = (design.spacing?.scale || []).map(v => typeof v === 'number' ? v : (v?.value ?? v?.size ?? 0)).filter(n => n > 0);
|
|
92
|
+
const scale = raw.slice().sort((a, b) => a - b).slice(0, 10);
|
|
93
|
+
if (!scale.length) return '';
|
|
94
|
+
return scale.map(v => `<div class="rhythm-bar" style="width:${Math.min(v * 1.6, 220)}px"><span>${v}</span></div>`).join('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function familyName(f) {
|
|
98
|
+
if (!f) return '';
|
|
99
|
+
if (typeof f === 'string') return f;
|
|
100
|
+
return f.name || f.family || '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fontHref(family) {
|
|
104
|
+
const name = familyName(family);
|
|
105
|
+
if (!name) return '';
|
|
106
|
+
const f = name.replace(/['"]/g, '').split(',')[0].trim();
|
|
107
|
+
if (!f) return '';
|
|
108
|
+
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@400;500;700&display=swap`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatGrade(design, opts = {}) {
|
|
112
|
+
const s = design.score;
|
|
113
|
+
if (!s) throw new Error('grade: design.score missing — extract failed to score');
|
|
114
|
+
|
|
115
|
+
const accent = gradeAccent(s.grade);
|
|
116
|
+
const headFamily = (design.typography?.families || [])[0];
|
|
117
|
+
const headHref = fontHref(headFamily);
|
|
118
|
+
const url = design.meta?.url || '';
|
|
119
|
+
const title = design.meta?.title || url;
|
|
120
|
+
const host = (() => { try { return new URL(url).hostname; } catch { return url; } })();
|
|
121
|
+
const date = new Date(design.meta?.timestamp || Date.now()).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
|
122
|
+
const issues = s.issues || [];
|
|
123
|
+
const strengths = s.strengths || [];
|
|
124
|
+
const ogTitle = `${host} — Grade ${s.grade}`;
|
|
125
|
+
const ogDesc = `Design system audit by designlang. ${s.overall}/100 across 8 dimensions.`;
|
|
126
|
+
|
|
127
|
+
const dims = DIMENSIONS
|
|
128
|
+
.filter(([k]) => s.scores[k] !== undefined)
|
|
129
|
+
.map(([k, label, blurb]) => {
|
|
130
|
+
const score = Math.round(s.scores[k]);
|
|
131
|
+
const dimAccent = score >= 80 ? '#0a8a52' : score >= 60 ? '#b08400' : '#c43d3d';
|
|
132
|
+
return `
|
|
133
|
+
<article class="dim">
|
|
134
|
+
<div class="dim-gauge">${arcGauge(score, dimAccent)}<span class="dim-score">${score}</span></div>
|
|
135
|
+
<div class="dim-body">
|
|
136
|
+
<h3>${esc(label)}</h3>
|
|
137
|
+
<p class="dim-blurb">${esc(blurb)}</p>
|
|
138
|
+
<p class="dim-verdict" style="color:${dimAccent}">${describeScore(score)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
</article>`;
|
|
141
|
+
}).join('');
|
|
142
|
+
|
|
143
|
+
return `<!doctype html>
|
|
144
|
+
<html lang="en">
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="utf-8">
|
|
147
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
148
|
+
<title>${esc(ogTitle)} · designlang</title>
|
|
149
|
+
<meta name="description" content="${esc(ogDesc)}">
|
|
150
|
+
<meta property="og:title" content="${esc(ogTitle)}">
|
|
151
|
+
<meta property="og:description" content="${esc(ogDesc)}">
|
|
152
|
+
<meta property="og:type" content="article">
|
|
153
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
154
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
155
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
156
|
+
<link href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(FONT_DISPLAY)}&family=${encodeURIComponent(FONT_BODY)}:wght@400;500;600&family=${encodeURIComponent(FONT_MONO)}:wght@400;500&display=swap" rel="stylesheet">
|
|
157
|
+
${headHref ? `<link href="${esc(headHref)}" rel="stylesheet">` : ''}
|
|
158
|
+
<style>
|
|
159
|
+
:root {
|
|
160
|
+
--paper: #f7f5ef;
|
|
161
|
+
--ink: #141414;
|
|
162
|
+
--ink-soft: #555049;
|
|
163
|
+
--rule: #e5e1d6;
|
|
164
|
+
--accent: ${accent};
|
|
165
|
+
--display: '${FONT_DISPLAY}', 'Times New Roman', serif;
|
|
166
|
+
--body: '${FONT_BODY}', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
167
|
+
--mono: '${FONT_MONO}', ui-monospace, 'SF Mono', monospace;
|
|
168
|
+
}
|
|
169
|
+
[data-theme="dark"] {
|
|
170
|
+
--paper: #0e0d0b;
|
|
171
|
+
--ink: #f0ece2;
|
|
172
|
+
--ink-soft: #9b9589;
|
|
173
|
+
--rule: #2a2823;
|
|
174
|
+
}
|
|
175
|
+
* { box-sizing: border-box; }
|
|
176
|
+
html, body { margin: 0; padding: 0; }
|
|
177
|
+
body {
|
|
178
|
+
background: var(--paper);
|
|
179
|
+
color: var(--ink);
|
|
180
|
+
font-family: var(--body);
|
|
181
|
+
font-size: 16px;
|
|
182
|
+
line-height: 1.55;
|
|
183
|
+
-webkit-font-smoothing: antialiased;
|
|
184
|
+
-moz-osx-font-smoothing: grayscale;
|
|
185
|
+
transition: background .25s ease, color .25s ease;
|
|
186
|
+
}
|
|
187
|
+
.wrap { max-width: 920px; margin: 0 auto; padding: 56px 40px 96px; }
|
|
188
|
+
@media (max-width: 640px) { .wrap { padding: 32px 22px 64px; } }
|
|
189
|
+
|
|
190
|
+
/* — Top bar — */
|
|
191
|
+
.topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom: 72px; font-size: 13px; }
|
|
192
|
+
.brand { font-family: var(--display); font-size: 22px; letter-spacing: .01em; }
|
|
193
|
+
.brand a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 1px; }
|
|
194
|
+
.topbar nav { display:flex; gap: 18px; align-items: center; color: var(--ink-soft); }
|
|
195
|
+
.theme-btn {
|
|
196
|
+
background: transparent; border: 1px solid var(--rule); color: var(--ink-soft);
|
|
197
|
+
font-family: var(--body); font-size: 12px; padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
|
198
|
+
letter-spacing: .04em; text-transform: uppercase;
|
|
199
|
+
}
|
|
200
|
+
.theme-btn:hover { color: var(--ink); border-color: var(--ink); }
|
|
201
|
+
|
|
202
|
+
/* — Hero — */
|
|
203
|
+
.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 32px; align-items: end; padding-bottom: 56px; border-bottom: 1px solid var(--rule); }
|
|
204
|
+
.kicker { text-transform: uppercase; letter-spacing: .18em; font-size: 11px; color: var(--ink-soft); margin-bottom: 18px; }
|
|
205
|
+
.hero h1 { font-family: var(--display); font-weight: 400; font-size: clamp(40px, 6vw, 72px); line-height: 1.02; margin: 0 0 18px; letter-spacing: -.01em; }
|
|
206
|
+
.hero h1 em { font-style: italic; color: var(--accent); }
|
|
207
|
+
.hero h1 a { color: var(--ink); text-decoration: none; border-bottom: 2px solid var(--accent); padding-bottom: 2px; transition: color .15s; }
|
|
208
|
+
.hero h1 a:hover { color: var(--accent); }
|
|
209
|
+
.hero .subject { font-size: 15px; color: var(--ink-soft); margin: 0; }
|
|
210
|
+
.hero .subject a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
|
|
211
|
+
.hero .meta { display:flex; gap: 18px; margin-top: 22px; font-size: 12px; color: var(--ink-soft); font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
|
212
|
+
.grade-block { text-align: center; }
|
|
213
|
+
.grade-letter {
|
|
214
|
+
font-family: var(--display); font-size: clamp(180px, 26vw, 280px); line-height: .82; color: var(--accent);
|
|
215
|
+
font-weight: 400; letter-spacing: -.04em; margin: 0;
|
|
216
|
+
animation: gradeIn .9s cubic-bezier(.2,.8,.2,1);
|
|
217
|
+
}
|
|
218
|
+
.grade-score { font-family: var(--mono); font-size: 14px; color: var(--ink-soft); letter-spacing: .04em; margin-top: 8px; }
|
|
219
|
+
@keyframes gradeIn { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: none; } }
|
|
220
|
+
@media (max-width: 640px) { .hero { grid-template-columns: 1fr; gap: 0; } .grade-block { text-align: left; margin-top: 24px; } }
|
|
221
|
+
|
|
222
|
+
/* — Section frame — */
|
|
223
|
+
section { padding: 64px 0; border-bottom: 1px solid var(--rule); }
|
|
224
|
+
section:last-of-type { border-bottom: 0; }
|
|
225
|
+
section > h2 { font-family: var(--display); font-weight: 400; font-size: 32px; margin: 0 0 8px; letter-spacing: -.005em; }
|
|
226
|
+
section > h2 + .lead { color: var(--ink-soft); margin: 0 0 36px; max-width: 60ch; }
|
|
227
|
+
|
|
228
|
+
/* — Dimensions grid — */
|
|
229
|
+
.dims { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 48px; }
|
|
230
|
+
@media (max-width: 640px) { .dims { grid-template-columns: 1fr; gap: 28px; } }
|
|
231
|
+
.dim { display: grid; grid-template-columns: 88px 1fr; gap: 18px; align-items: start; }
|
|
232
|
+
.dim-gauge { position: relative; width: 88px; height: 88px; }
|
|
233
|
+
.gauge { width: 100%; height: 100%; }
|
|
234
|
+
.gauge-track { stroke: var(--rule); }
|
|
235
|
+
.gauge-fill { stroke-dasharray: var(--c, 226); animation: arc 1.1s cubic-bezier(.2,.8,.2,1) both; }
|
|
236
|
+
@keyframes arc { from { stroke-dashoffset: 226; } }
|
|
237
|
+
.dim-score { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--mono); font-weight: 500; font-size: 18px; }
|
|
238
|
+
.dim h3 { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 4px 0 6px; }
|
|
239
|
+
.dim-blurb { color: var(--ink-soft); font-size: 14px; margin: 0 0 8px; }
|
|
240
|
+
.dim-verdict { font-size: 12px; font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; margin: 0; }
|
|
241
|
+
|
|
242
|
+
/* — Lists — */
|
|
243
|
+
.ledger { display: grid; grid-template-columns: repeat(2, 1fr); gap: 48px; }
|
|
244
|
+
@media (max-width: 640px) { .ledger { grid-template-columns: 1fr; gap: 36px; } }
|
|
245
|
+
.ledger h3 { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 0 0 16px; }
|
|
246
|
+
.ledger ul { list-style: none; padding: 0; margin: 0; }
|
|
247
|
+
.ledger li { padding: 14px 0; border-top: 1px solid var(--rule); display: flex; gap: 14px; align-items: baseline; }
|
|
248
|
+
.ledger li:last-child { border-bottom: 1px solid var(--rule); }
|
|
249
|
+
.ledger .marker { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); flex: 0 0 24px; padding-top: 2px; letter-spacing: .04em; }
|
|
250
|
+
.ledger li p { margin: 0; font-size: 15px; line-height: 1.5; }
|
|
251
|
+
.ledger.empty p { color: var(--ink-soft); font-style: italic; font-family: var(--display); font-size: 16px; }
|
|
252
|
+
|
|
253
|
+
/* — Evidence — */
|
|
254
|
+
.swatches { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; }
|
|
255
|
+
.swatch { display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 6px; background: rgba(0,0,0,.02); border: 1px solid var(--rule); border-radius: 6px; }
|
|
256
|
+
[data-theme="dark"] .swatch { background: rgba(255,255,255,.03); }
|
|
257
|
+
.swatch-chip { width: 22px; height: 22px; border-radius: 4px; background: var(--c); box-shadow: inset 0 0 0 1px rgba(0,0,0,.06); flex: 0 0 auto; }
|
|
258
|
+
.swatch code { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); }
|
|
259
|
+
|
|
260
|
+
.specimen { padding: 24px 0; }
|
|
261
|
+
.spec-line { line-height: 1.05; margin: 0 0 14px; letter-spacing: -.01em; }
|
|
262
|
+
.spec-line:nth-child(2) { color: var(--ink-soft); font-style: italic; }
|
|
263
|
+
.specimen-meta { display: flex; flex-wrap: wrap; gap: 24px; font-family: var(--mono); font-size: 11px; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; padding-top: 18px; border-top: 1px solid var(--rule); }
|
|
264
|
+
|
|
265
|
+
.rhythm { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
|
|
266
|
+
.rhythm-bar { height: 14px; background: var(--accent); opacity: .82; border-radius: 2px; display: flex; align-items: center; padding-left: 10px; transition: opacity .15s; }
|
|
267
|
+
.rhythm-bar span { font-family: var(--mono); font-size: 10px; color: var(--paper); mix-blend-mode: difference; filter: invert(1); }
|
|
268
|
+
.rhythm-bar:hover { opacity: 1; }
|
|
269
|
+
|
|
270
|
+
/* — Footer — */
|
|
271
|
+
footer { padding: 48px 0 0; font-size: 13px; color: var(--ink-soft); display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap; gap: 16px; }
|
|
272
|
+
footer .sig { font-family: var(--display); font-size: 22px; color: var(--ink); }
|
|
273
|
+
footer a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
|
|
274
|
+
footer .stamp { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
|
|
275
|
+
|
|
276
|
+
/* — Print — */
|
|
277
|
+
@media print {
|
|
278
|
+
body { background: white; color: black; }
|
|
279
|
+
.topbar nav, .theme-btn { display: none; }
|
|
280
|
+
section, .hero { page-break-inside: avoid; border-color: #ddd; }
|
|
281
|
+
.grade-letter { color: black; }
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
284
|
+
</head>
|
|
285
|
+
<body>
|
|
286
|
+
<div class="wrap">
|
|
287
|
+
<header class="topbar">
|
|
288
|
+
<div class="brand"><a href="https://designlang.dev">designlang</a></div>
|
|
289
|
+
<nav>
|
|
290
|
+
<span>Report Card</span>
|
|
291
|
+
<button class="theme-btn" id="themeBtn" type="button">Theme</button>
|
|
292
|
+
</nav>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<div class="hero">
|
|
296
|
+
<div>
|
|
297
|
+
<p class="kicker">Design Audit · ${esc(date)}</p>
|
|
298
|
+
<h1>An <em>independent reading</em> of the design system at <a href="${esc(url)}" target="_blank" rel="noopener">${esc(host)}</a>.</h1>
|
|
299
|
+
<p class="subject">${esc(title)}</p>
|
|
300
|
+
<div class="meta">
|
|
301
|
+
<span>${(design.colors?.all || []).length} colors</span>
|
|
302
|
+
<span>${(design.typography?.scale || []).length} sizes</span>
|
|
303
|
+
<span>${(design.spacing?.scale || []).length} spacings</span>
|
|
304
|
+
<span>${(design.shadows?.values || []).length} shadows</span>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="grade-block">
|
|
308
|
+
<div class="grade-letter">${esc(s.grade)}</div>
|
|
309
|
+
<div class="grade-score">${s.overall} / 100</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<section>
|
|
314
|
+
<h2>Eight dimensions, scored.</h2>
|
|
315
|
+
<p class="lead">Each dimension is graded against calibrated thresholds drawn from production design systems (Stripe, Linear, Vercel, GitHub, Apple). The number is the headline; the prose underneath is what to do next.</p>
|
|
316
|
+
<div class="dims">${dims}</div>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
319
|
+
<section>
|
|
320
|
+
<h2>What's working. What to fix.</h2>
|
|
321
|
+
<div class="ledger">
|
|
322
|
+
<div class="${strengths.length ? '' : 'empty'}">
|
|
323
|
+
<h3>Strengths</h3>
|
|
324
|
+
${strengths.length
|
|
325
|
+
? `<ul>${strengths.map((str, i) => `<li><span class="marker">${String(i + 1).padStart(2, '0')}</span><p>${esc(str)}</p></li>`).join('')}</ul>`
|
|
326
|
+
: `<p>No standout strengths surfaced — the system is uniformly mid-tier.</p>`}
|
|
327
|
+
</div>
|
|
328
|
+
<div class="${issues.length ? '' : 'empty'}">
|
|
329
|
+
<h3>What to fix</h3>
|
|
330
|
+
${issues.length
|
|
331
|
+
? `<ul>${issues.map((iss, i) => `<li><span class="marker">${String(i + 1).padStart(2, '0')}</span><p>${esc(iss)}</p></li>`).join('')}</ul>`
|
|
332
|
+
: `<p>Nothing material flagged. Polish, then ship.</p>`}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</section>
|
|
336
|
+
|
|
337
|
+
<section>
|
|
338
|
+
<h2>Evidence.</h2>
|
|
339
|
+
<p class="lead">The audit reads from the page itself. Here is what the auditor saw — palette, type, rhythm — drawn straight from the live styles.</p>
|
|
340
|
+
|
|
341
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:24px 0 12px;color:var(--ink-soft)">Palette</h3>
|
|
342
|
+
<ul class="swatches">${colorSwatches(design)}</ul>
|
|
343
|
+
|
|
344
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:32px 0 0;color:var(--ink-soft)">Type</h3>
|
|
345
|
+
${typeSpecimen(design)}
|
|
346
|
+
|
|
347
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:32px 0 0;color:var(--ink-soft)">Spacing rhythm</h3>
|
|
348
|
+
<div class="rhythm">${spacingScale(design)}</div>
|
|
349
|
+
</section>
|
|
350
|
+
|
|
351
|
+
<footer>
|
|
352
|
+
<div>
|
|
353
|
+
<div class="sig">designlang</div>
|
|
354
|
+
<div>Audit any site: <code style="font-family:var(--mono)">npx designlang grade ${esc(host)}</code></div>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="stamp">${esc(date)} · v${esc(opts.version || '')}</div>
|
|
357
|
+
</footer>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<script>
|
|
361
|
+
(function () {
|
|
362
|
+
var btn = document.getElementById('themeBtn');
|
|
363
|
+
var saved = null;
|
|
364
|
+
try { saved = localStorage.getItem('dl-theme'); } catch (e) {}
|
|
365
|
+
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
|
366
|
+
btn && btn.addEventListener('click', function () {
|
|
367
|
+
var cur = document.documentElement.getAttribute('data-theme') === 'dark' ? '' : 'dark';
|
|
368
|
+
if (cur) document.documentElement.setAttribute('data-theme', cur);
|
|
369
|
+
else document.documentElement.removeAttribute('data-theme');
|
|
370
|
+
try { localStorage.setItem('dl-theme', cur); } catch (e) {}
|
|
371
|
+
});
|
|
372
|
+
})();
|
|
373
|
+
</script>
|
|
374
|
+
</body>
|
|
375
|
+
</html>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function formatGradeMarkdown(design) {
|
|
379
|
+
const s = design.score;
|
|
380
|
+
if (!s) throw new Error('grade: design.score missing');
|
|
381
|
+
const url = design.meta?.url || '';
|
|
382
|
+
const date = new Date(design.meta?.timestamp || Date.now()).toISOString().split('T')[0];
|
|
383
|
+
const lines = [
|
|
384
|
+
`# Design Report Card — ${url}`,
|
|
385
|
+
``,
|
|
386
|
+
`**Grade ${s.grade}** · ${s.overall}/100 · _${date}_`,
|
|
387
|
+
``,
|
|
388
|
+
`## Dimensions`,
|
|
389
|
+
``,
|
|
390
|
+
`| Dimension | Score | Verdict |`,
|
|
391
|
+
`|---|---|---|`,
|
|
392
|
+
...DIMENSIONS.filter(([k]) => s.scores[k] !== undefined).map(([k, label]) =>
|
|
393
|
+
`| ${label} | ${Math.round(s.scores[k])}/100 | ${describeScore(s.scores[k])} |`),
|
|
394
|
+
``,
|
|
395
|
+
];
|
|
396
|
+
if (s.strengths?.length) {
|
|
397
|
+
lines.push(`## Strengths`, ``, ...s.strengths.map(x => `- ${x}`), ``);
|
|
398
|
+
}
|
|
399
|
+
if (s.issues?.length) {
|
|
400
|
+
lines.push(`## What to fix`, ``, ...s.issues.map(x => `- ${x}`), ``);
|
|
401
|
+
}
|
|
402
|
+
lines.push(`---`, `_Audited by [designlang](https://designlang.dev) · \`npx designlang grade ${url}\`_`);
|
|
403
|
+
return lines.join('\n');
|
|
404
|
+
}
|