designlang 12.4.0 → 12.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/.claude-plugin/marketplace.json +15 -7
- package/.claude-plugin/plugin.json +19 -8
- package/CHANGELOG.md +254 -0
- package/README.md +34 -4
- package/SUPPORT.md +22 -0
- package/bin/design-extract.js +295 -0
- package/commands/battle.md +27 -0
- package/commands/brand.md +59 -0
- package/commands/extract.md +41 -0
- package/commands/grade.md +29 -0
- package/commands/pack.md +37 -0
- package/commands/pair.md +68 -0
- package/commands/remix.md +29 -0
- package/commands/theme-swap.md +42 -0
- package/package.json +3 -3
- package/src/ci.js +36 -2
- package/src/formatters/brand-book.js +1052 -0
- package/src/formatters/pair.js +331 -0
- package/src/formatters/theme-swap.js +272 -0
- package/src/fuse.js +154 -0
- package/src/recolor.js +199 -0
- package/src/utils/color-gamut.js +64 -0
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
// designlang brand-book — full editorial brand-guidelines document.
|
|
2
|
+
//
|
|
3
|
+
// A single self-contained HTML book that documents an extracted design
|
|
4
|
+
// system the way a hand-off doc would: cover, table of contents, 13
|
|
5
|
+
// chapters. The layout is editorial (Stripe Press / Pentagram-leaning),
|
|
6
|
+
// not data-dump — each chapter shows the actual values, not just lists.
|
|
7
|
+
//
|
|
8
|
+
// Two design rules:
|
|
9
|
+
// 1. The brand's primary colour leads the cover. It's the first thing
|
|
10
|
+
// anyone sees, full bleed. Everything else dials down from there.
|
|
11
|
+
// 2. Real text from the site whenever possible. The voice extractor
|
|
12
|
+
// surfaces real headings — use them. No filler aphorisms.
|
|
13
|
+
|
|
14
|
+
const FONT_DISPLAY = 'Instrument Serif';
|
|
15
|
+
const FONT_BODY = 'Inter';
|
|
16
|
+
const FONT_MONO = 'JetBrains Mono';
|
|
17
|
+
|
|
18
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function esc(s) {
|
|
21
|
+
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function host(url) {
|
|
25
|
+
try { return new URL(url).hostname; } catch { return String(url || ''); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function familyName(f) {
|
|
29
|
+
if (!f) return '';
|
|
30
|
+
if (typeof f === 'string') return f;
|
|
31
|
+
return f.name || f.family || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Coerce *anything* into a list of strings. The component-anatomy
|
|
35
|
+
// extractor returns slots/props as objects, arrays, or strings.
|
|
36
|
+
function asList(v) {
|
|
37
|
+
if (v == null) return [];
|
|
38
|
+
if (Array.isArray(v)) return v.filter(x => x != null);
|
|
39
|
+
if (typeof v === 'string') return v.split(',').map(s => s.trim()).filter(Boolean);
|
|
40
|
+
if (typeof v === 'object') return Object.keys(v);
|
|
41
|
+
return [String(v)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hexToRgb(hex) {
|
|
45
|
+
if (!hex) return null;
|
|
46
|
+
const s = String(hex).trim().replace(/^#/, '');
|
|
47
|
+
const full = s.length === 3 ? s.split('').map(c => c + c).join('') : s.slice(0, 6);
|
|
48
|
+
if (!/^[0-9a-f]{6}$/i.test(full)) return null;
|
|
49
|
+
return {
|
|
50
|
+
r: parseInt(full.slice(0, 2), 16),
|
|
51
|
+
g: parseInt(full.slice(2, 4), 16),
|
|
52
|
+
b: parseInt(full.slice(4, 6), 16),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rgbToHsl({ r, g, b }) {
|
|
57
|
+
const rN = r / 255, gN = g / 255, bN = b / 255;
|
|
58
|
+
const max = Math.max(rN, gN, bN), min = Math.min(rN, gN, bN);
|
|
59
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
60
|
+
if (max !== min) {
|
|
61
|
+
const d = max - min;
|
|
62
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
63
|
+
switch (max) {
|
|
64
|
+
case rN: h = (gN - bN) / d + (gN < bN ? 6 : 0); break;
|
|
65
|
+
case gN: h = (bN - rN) / d + 2; break;
|
|
66
|
+
case bN: h = (rN - gN) / d + 4; break;
|
|
67
|
+
}
|
|
68
|
+
h /= 6;
|
|
69
|
+
}
|
|
70
|
+
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function relLum({ r, g, b }) {
|
|
74
|
+
const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
75
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bestTextOn(rgb) {
|
|
79
|
+
return relLum(rgb) > 0.5 ? '#0a0a0a' : '#ffffff';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pickAccent(design) {
|
|
83
|
+
// The cover band uses the brand's actual primary if we can detect one
|
|
84
|
+
// — that's the whole point. Fall back through secondary, accent, the
|
|
85
|
+
// most-used coloured token, then ink.
|
|
86
|
+
const candidates = [
|
|
87
|
+
design.colors?.primary?.hex,
|
|
88
|
+
design.colors?.secondary?.hex,
|
|
89
|
+
design.colors?.accent?.hex,
|
|
90
|
+
...(design.colors?.all || []).map(c => c?.hex).filter(Boolean),
|
|
91
|
+
];
|
|
92
|
+
for (const hex of candidates) {
|
|
93
|
+
const rgb = hexToRgb(hex);
|
|
94
|
+
if (!rgb) continue;
|
|
95
|
+
// Skip near-greys.
|
|
96
|
+
const max = Math.max(rgb.r, rgb.g, rgb.b);
|
|
97
|
+
const min = Math.min(rgb.r, rgb.g, rgb.b);
|
|
98
|
+
if (max - min > 24) return hex;
|
|
99
|
+
}
|
|
100
|
+
return '#141414';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pull two real lines from the site for the type specimen. Falls back
|
|
104
|
+
// to a neutral pangram only when the voice extractor came back empty.
|
|
105
|
+
function specimenLines(design) {
|
|
106
|
+
const headings = (design.voice?.sampleHeadings || []).filter(h => typeof h === 'string' && h.trim().length > 4 && h.length < 120);
|
|
107
|
+
if (headings.length >= 2) return [headings[0], headings[1]];
|
|
108
|
+
if (headings.length === 1) return [headings[0], 'Quick brown fox jumps over the lazy dog.'];
|
|
109
|
+
return ['Quick brown fox jumps over the lazy dog.', 'AaBbCc 0123456789 ?!&'];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Section builders ──────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function buildCover(design, accent) {
|
|
115
|
+
const meta = design.meta || {};
|
|
116
|
+
const hostName = host(meta.url);
|
|
117
|
+
const date = new Date(meta.timestamp || Date.now()).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
118
|
+
const grade = design.score?.grade || '—';
|
|
119
|
+
const overall = design.score?.overall ?? '—';
|
|
120
|
+
const intent = design.pageIntent?.type || 'site';
|
|
121
|
+
const fg = bestTextOn(hexToRgb(accent) || { r: 0, g: 0, b: 0 });
|
|
122
|
+
return `
|
|
123
|
+
<section class="cover" id="cover" aria-labelledby="cover-title">
|
|
124
|
+
<div class="cover-band" style="background:${esc(accent)};color:${fg}">
|
|
125
|
+
<span class="cover-band-label">Brand guidelines</span>
|
|
126
|
+
<span class="cover-band-hex"><code>${esc(accent)}</code></span>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="cover-body">
|
|
129
|
+
<p class="kicker">${esc(date)}</p>
|
|
130
|
+
<h1 id="cover-title" class="cover-title">${esc(hostName)}</h1>
|
|
131
|
+
<p class="cover-sub">A reading of the visual language at <a href="${esc(meta.url || '')}" target="_blank" rel="noopener">${esc(meta.url || hostName)}</a>. Every token, every rule, every component — captured from the live site.</p>
|
|
132
|
+
<dl class="cover-meta">
|
|
133
|
+
<div><dt>Page intent</dt><dd>${esc(intent)}</dd></div>
|
|
134
|
+
<div><dt>System grade</dt><dd>${esc(grade)} <span class="muted">${esc(String(overall))}/100</span></dd></div>
|
|
135
|
+
<div><dt>Tokens</dt><dd>${(design.colors?.all || []).length} colours · ${(design.typography?.scale || []).length} sizes</dd></div>
|
|
136
|
+
<div><dt>Generated</dt><dd>designlang</dd></div>
|
|
137
|
+
</dl>
|
|
138
|
+
</div>
|
|
139
|
+
</section>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildToc() {
|
|
143
|
+
const items = [
|
|
144
|
+
['about', '01', 'About'],
|
|
145
|
+
['logo', '02', 'Logo'],
|
|
146
|
+
['color', '03', 'Colour'],
|
|
147
|
+
['type', '04', 'Typography'],
|
|
148
|
+
['spacing', '05', 'Spacing'],
|
|
149
|
+
['shape', '06', 'Shape'],
|
|
150
|
+
['iconography', '07', 'Iconography'],
|
|
151
|
+
['motion', '08', 'Motion'],
|
|
152
|
+
['components', '09', 'Components'],
|
|
153
|
+
['voice', '10', 'Voice'],
|
|
154
|
+
['accessibility', '11', 'Accessibility'],
|
|
155
|
+
['tokens', '12', 'Tokens'],
|
|
156
|
+
['usage', '13', 'Usage'],
|
|
157
|
+
];
|
|
158
|
+
return `
|
|
159
|
+
<nav class="toc" aria-label="Table of contents">
|
|
160
|
+
<h2 class="toc-title">Contents</h2>
|
|
161
|
+
<ol>
|
|
162
|
+
${items.map(([id, n, label]) => `<li><a href="#${id}"><span class="toc-num">${n}</span><span class="toc-label">${label}</span></a></li>`).join('')}
|
|
163
|
+
</ol>
|
|
164
|
+
</nav>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function chapterHeader(num, title) {
|
|
168
|
+
return `
|
|
169
|
+
<header class="chapter-header">
|
|
170
|
+
<span class="sec-num">Chapter ${num}</span>
|
|
171
|
+
<h2>${esc(title)}</h2>
|
|
172
|
+
</header>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildAbout(design) {
|
|
176
|
+
const intent = design.pageIntent || {};
|
|
177
|
+
const material = design.materialLanguage || {};
|
|
178
|
+
const imagery = design.imageryStyle || {};
|
|
179
|
+
const lib = design.componentLibrary || {};
|
|
180
|
+
const stack = design.stack || {};
|
|
181
|
+
const voice = design.voice || {};
|
|
182
|
+
return `
|
|
183
|
+
<section id="about">
|
|
184
|
+
${chapterHeader('01', 'About')}
|
|
185
|
+
<dl class="meta-grid">
|
|
186
|
+
<div><dt>Page intent</dt><dd>${esc(intent.type || 'unknown')}${typeof intent.confidence === 'number' ? ` <span class="muted">${(intent.confidence * 100).toFixed(0)}% confidence</span>` : ''}</dd></div>
|
|
187
|
+
<div><dt>Material language</dt><dd>${esc(material.label || 'unknown')}</dd></div>
|
|
188
|
+
<div><dt>Imagery style</dt><dd>${esc(imagery.label || 'unknown')}</dd></div>
|
|
189
|
+
<div><dt>Component library</dt><dd>${esc(lib.library || 'unknown')}</dd></div>
|
|
190
|
+
<div><dt>Stack</dt><dd>${esc(stack.framework || 'unknown')}${stack.css?.tailwind ? ' · Tailwind' : ''}</dd></div>
|
|
191
|
+
<div><dt>Voice tone</dt><dd>${esc(voice.tone || 'neutral')}</dd></div>
|
|
192
|
+
</dl>
|
|
193
|
+
</section>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildLogo(design) {
|
|
197
|
+
const logo = design.logo || {};
|
|
198
|
+
const hasSvg = typeof logo.svg === 'string' && logo.svg.includes('<svg');
|
|
199
|
+
const hostName = host(design.meta?.url) || 'logo';
|
|
200
|
+
return `
|
|
201
|
+
<section id="logo">
|
|
202
|
+
${chapterHeader('02', 'Logo')}
|
|
203
|
+
<div class="logo-card">
|
|
204
|
+
<div class="logo-canvas" aria-label="Extracted logo on canvas">
|
|
205
|
+
${hasSvg ? logo.svg : `<span class="logo-placeholder">${esc(hostName)}</span>`}
|
|
206
|
+
</div>
|
|
207
|
+
<dl class="meta-grid logo-meta">
|
|
208
|
+
<div><dt>Source</dt><dd>${esc(logo.source || 'inferred')}</dd></div>
|
|
209
|
+
<div><dt>Dimensions</dt><dd>${logo.width ? `${logo.width} × ${logo.height} px` : '—'}</dd></div>
|
|
210
|
+
<div><dt>Aspect</dt><dd>${logo.aspect ? logo.aspect.toFixed(2) : '—'}</dd></div>
|
|
211
|
+
<div><dt>Format</dt><dd>${esc(logo.format || (hasSvg ? 'svg' : 'unknown'))}</dd></div>
|
|
212
|
+
</dl>
|
|
213
|
+
</div>
|
|
214
|
+
</section>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function bigSwatch(name, c, role) {
|
|
218
|
+
if (!c?.hex) return '';
|
|
219
|
+
const rgb = hexToRgb(c.hex);
|
|
220
|
+
if (!rgb) return '';
|
|
221
|
+
const fg = bestTextOn(rgb);
|
|
222
|
+
const hsl = rgbToHsl(rgb);
|
|
223
|
+
return `
|
|
224
|
+
<article class="brand-color brand-color-${role}">
|
|
225
|
+
<div class="big-swatch" style="background:${esc(c.hex)};color:${fg}">
|
|
226
|
+
<span class="big-swatch-name">${esc(name)}</span>
|
|
227
|
+
<span class="big-swatch-hex">${esc(c.hex.toUpperCase())}</span>
|
|
228
|
+
</div>
|
|
229
|
+
<dl class="color-meta">
|
|
230
|
+
<div><dt>RGB</dt><dd><code>${rgb.r}, ${rgb.g}, ${rgb.b}</code></dd></div>
|
|
231
|
+
<div><dt>HSL</dt><dd><code>${hsl.h}°, ${hsl.s}%, ${hsl.l}%</code></dd></div>
|
|
232
|
+
</dl>
|
|
233
|
+
</article>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildColor(design) {
|
|
237
|
+
const colors = design.colors || {};
|
|
238
|
+
const primary = colors.primary;
|
|
239
|
+
const secondary = colors.secondary;
|
|
240
|
+
const accent = colors.accent;
|
|
241
|
+
const neutrals = colors.neutrals || [];
|
|
242
|
+
const all = colors.all || [];
|
|
243
|
+
const a11y = design.accessibility || {};
|
|
244
|
+
const summary = [
|
|
245
|
+
primary?.hex && '1 primary',
|
|
246
|
+
secondary?.hex && '1 secondary',
|
|
247
|
+
accent?.hex && '1 accent',
|
|
248
|
+
`${neutrals.length} neutrals`,
|
|
249
|
+
`${all.length} total`,
|
|
250
|
+
].filter(Boolean).join(' · ');
|
|
251
|
+
|
|
252
|
+
return `
|
|
253
|
+
<section id="color">
|
|
254
|
+
${chapterHeader('03', 'Colour')}
|
|
255
|
+
<p class="summary">${esc(summary)}</p>
|
|
256
|
+
|
|
257
|
+
${primary?.hex ? `<div class="brand-grid brand-grid-primary">${bigSwatch('Primary', primary, 'primary')}</div>` : ''}
|
|
258
|
+
|
|
259
|
+
${(secondary?.hex || accent?.hex) ? `
|
|
260
|
+
<div class="brand-grid brand-grid-pair">
|
|
261
|
+
${secondary?.hex ? bigSwatch('Secondary', secondary, 'secondary') : ''}
|
|
262
|
+
${accent?.hex ? bigSwatch('Accent', accent, 'accent') : ''}
|
|
263
|
+
</div>
|
|
264
|
+
` : ''}
|
|
265
|
+
|
|
266
|
+
${neutrals.length ? `
|
|
267
|
+
<h3 class="sub">Neutrals</h3>
|
|
268
|
+
<div class="neutral-strip">
|
|
269
|
+
${neutrals.slice(0, 12).map(c => {
|
|
270
|
+
const rgb = hexToRgb(c.hex);
|
|
271
|
+
if (!rgb) return '';
|
|
272
|
+
const fg = bestTextOn(rgb);
|
|
273
|
+
return `<div class="neutral-cell" style="background:${esc(c.hex)};color:${fg}"><code>${esc(c.hex.toUpperCase())}</code></div>`;
|
|
274
|
+
}).join('')}
|
|
275
|
+
</div>
|
|
276
|
+
` : ''}
|
|
277
|
+
|
|
278
|
+
<h3 class="sub">Full palette</h3>
|
|
279
|
+
<div class="palette-grid">
|
|
280
|
+
${all.slice(0, 28).map(c => {
|
|
281
|
+
const rgb = hexToRgb(c.hex);
|
|
282
|
+
if (!rgb) return '';
|
|
283
|
+
const fg = bestTextOn(rgb);
|
|
284
|
+
return `<div class="mini-swatch" style="background:${esc(c.hex)};color:${fg}"><code>${esc(c.hex.toUpperCase())}</code></div>`;
|
|
285
|
+
}).join('')}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
${a11y.score != null ? `
|
|
289
|
+
<p class="callout"><strong>WCAG ${a11y.score}%</strong> · ${a11y.passCount || 0} passing pairs · ${a11y.failCount || 0} failing. Full breakdown in §11.</p>
|
|
290
|
+
` : ''}
|
|
291
|
+
</section>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildType(design) {
|
|
295
|
+
const t = design.typography || {};
|
|
296
|
+
const families = (t.families || []).map(familyName).filter(Boolean);
|
|
297
|
+
const scaleRaw = (t.scale || []);
|
|
298
|
+
const sizes = scaleRaw.map(s => typeof s === 'number' ? s : (s?.size ?? 0)).filter(n => n > 0).sort((a, b) => b - a);
|
|
299
|
+
const weights = (t.weights || []).map(w => typeof w === 'object' ? (w.weight || w.value) : w).filter(Boolean);
|
|
300
|
+
const head = families[0];
|
|
301
|
+
const body = families[1] || head;
|
|
302
|
+
const lines = specimenLines(design);
|
|
303
|
+
const headStack = head ? `${esc(head)}, '${FONT_DISPLAY}', serif` : `'${FONT_DISPLAY}', serif`;
|
|
304
|
+
return `
|
|
305
|
+
<section id="type">
|
|
306
|
+
${chapterHeader('04', 'Typography')}
|
|
307
|
+
<p class="summary">${families.length || 0} families · ${sizes.length} sizes · ${weights.length} weights</p>
|
|
308
|
+
|
|
309
|
+
<dl class="meta-grid">
|
|
310
|
+
<div><dt>Display</dt><dd>${esc(head || '—')}</dd></div>
|
|
311
|
+
<div><dt>Body</dt><dd>${esc(body || '—')}</dd></div>
|
|
312
|
+
<div><dt>Weights</dt><dd>${weights.join(', ') || '—'}</dd></div>
|
|
313
|
+
</dl>
|
|
314
|
+
|
|
315
|
+
<h3 class="sub">Specimen</h3>
|
|
316
|
+
<div class="specimen" style="font-family: ${headStack};">
|
|
317
|
+
<div class="spec-line spec-display" style="font-size: ${Math.min(sizes[0] || 64, 80)}px">${esc(lines[0])}</div>
|
|
318
|
+
<div class="spec-line spec-body" style="font-size: 24px">${esc(lines[1])}</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<h3 class="sub">Scale</h3>
|
|
322
|
+
<table class="scale-table">
|
|
323
|
+
<thead><tr><th class="t-step">Step</th><th class="t-size">Size</th><th class="t-sample">Sample</th></tr></thead>
|
|
324
|
+
<tbody>
|
|
325
|
+
${sizes.slice(0, 10).map((s, i) => `
|
|
326
|
+
<tr>
|
|
327
|
+
<td class="t-step"><code>t${String(i).padStart(2, '0')}</code></td>
|
|
328
|
+
<td class="t-size"><code>${s}px</code></td>
|
|
329
|
+
<td class="t-sample" style="font-size:${Math.min(s, 56)}px; font-family: ${headStack}; line-height: 1;">${esc(lines[0].slice(0, 28))}</td>
|
|
330
|
+
</tr>
|
|
331
|
+
`).join('')}
|
|
332
|
+
</tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</section>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildSpacing(design) {
|
|
338
|
+
const sp = design.spacing || {};
|
|
339
|
+
const raw = (sp.scale || []).map(v => typeof v === 'number' ? v : (v?.value ?? v?.size ?? 0)).filter(n => n > 0);
|
|
340
|
+
const scale = raw.slice().sort((a, b) => a - b).slice(0, 10);
|
|
341
|
+
const base = sp.base || (scale[0] || 4);
|
|
342
|
+
return `
|
|
343
|
+
<section id="spacing">
|
|
344
|
+
${chapterHeader('05', 'Spacing')}
|
|
345
|
+
<p class="summary">Base ${base}px · ${(sp.scale || []).length} steps captured</p>
|
|
346
|
+
|
|
347
|
+
<h3 class="sub">Rhythm</h3>
|
|
348
|
+
<div class="space-rhythm">
|
|
349
|
+
${scale.map(v => `
|
|
350
|
+
<div class="space-step">
|
|
351
|
+
<div class="space-block" style="width:${Math.min(v * 1.6, 320)}px"></div>
|
|
352
|
+
<code>${v}px</code>
|
|
353
|
+
</div>
|
|
354
|
+
`).join('')}
|
|
355
|
+
</div>
|
|
356
|
+
</section>`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function buildShape(design) {
|
|
360
|
+
const radii = (design.borders?.radii || [])
|
|
361
|
+
.map(r => typeof r === 'object' ? r.value : r)
|
|
362
|
+
.filter(n => typeof n === 'number')
|
|
363
|
+
.sort((a, b) => a - b);
|
|
364
|
+
const shadowsRaw = (design.shadows?.values || []).slice(0, 6);
|
|
365
|
+
const shadowLabels = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
366
|
+
return `
|
|
367
|
+
<section id="shape">
|
|
368
|
+
${chapterHeader('06', 'Shape')}
|
|
369
|
+
<p class="summary">${radii.length} radii · ${shadowsRaw.length} elevation tiers</p>
|
|
370
|
+
|
|
371
|
+
<h3 class="sub">Border radii</h3>
|
|
372
|
+
<div class="radii-row">
|
|
373
|
+
${radii.slice(0, 6).map(v => `
|
|
374
|
+
<div class="radius-card">
|
|
375
|
+
<div class="radius-block" style="border-radius:${v}px"></div>
|
|
376
|
+
<code>${v}px</code>
|
|
377
|
+
</div>
|
|
378
|
+
`).join('')}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
${shadowsRaw.length ? `
|
|
382
|
+
<h3 class="sub">Elevation</h3>
|
|
383
|
+
<div class="shadows-row">
|
|
384
|
+
${shadowsRaw.map((s, i) => {
|
|
385
|
+
const raw = typeof s === 'string' ? s : (s?.raw || '');
|
|
386
|
+
const label = shadowLabels[i] || `e${i}`;
|
|
387
|
+
return `
|
|
388
|
+
<div class="shadow-card">
|
|
389
|
+
<div class="shadow-block" style="box-shadow:${esc(raw)}"></div>
|
|
390
|
+
<code>${esc(label)}</code>
|
|
391
|
+
</div>
|
|
392
|
+
`;
|
|
393
|
+
}).join('')}
|
|
394
|
+
</div>
|
|
395
|
+
` : ''}
|
|
396
|
+
</section>`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildIconography(design) {
|
|
400
|
+
const sys = design.iconSystem || {};
|
|
401
|
+
const icons = (sys.icons || design.icons?.icons || []).slice(0, 24);
|
|
402
|
+
return `
|
|
403
|
+
<section id="iconography">
|
|
404
|
+
${chapterHeader('07', 'Iconography')}
|
|
405
|
+
<p class="summary">${esc(sys.library || 'unknown library')} · ${icons.length} captured</p>
|
|
406
|
+
|
|
407
|
+
<dl class="meta-grid">
|
|
408
|
+
<div><dt>Library</dt><dd>${esc(sys.library || 'unknown')}</dd></div>
|
|
409
|
+
<div><dt>Confidence</dt><dd>${sys.confidence != null ? Math.round(sys.confidence * 100) + '%' : '—'}</dd></div>
|
|
410
|
+
<div><dt>Stroke style</dt><dd>${esc((sys.signals && sys.signals[0]) || '—')}</dd></div>
|
|
411
|
+
</dl>
|
|
412
|
+
|
|
413
|
+
${icons.length ? `
|
|
414
|
+
<div class="icon-grid">
|
|
415
|
+
${icons.map(i => `<div class="icon-cell">${i.svg ? i.svg : `<code>${esc(i.name || 'icon')}</code>`}</div>`).join('')}
|
|
416
|
+
</div>
|
|
417
|
+
` : '<p class="muted">No icons captured.</p>'}
|
|
418
|
+
</section>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function buildMotion(design, accent) {
|
|
422
|
+
const m = design.motion || {};
|
|
423
|
+
const durations = (m.durations || []).slice(0, 6);
|
|
424
|
+
const easings = (m.easings || []).slice(0, 4);
|
|
425
|
+
return `
|
|
426
|
+
<section id="motion">
|
|
427
|
+
${chapterHeader('08', 'Motion')}
|
|
428
|
+
<p class="summary">Feel: ${esc(m.feel || 'unknown')} · ${(m.durations || []).length} durations · ${(m.easings || []).length} easings</p>
|
|
429
|
+
|
|
430
|
+
${durations.length ? `
|
|
431
|
+
<h3 class="sub">Duration scale</h3>
|
|
432
|
+
<div class="motion-tempo">
|
|
433
|
+
${durations.map(d => {
|
|
434
|
+
const ms = d.ms || (typeof d === 'number' ? d : 0);
|
|
435
|
+
const label = d.label ? esc(d.label) : '';
|
|
436
|
+
return `
|
|
437
|
+
<div class="tempo-item">
|
|
438
|
+
<div class="tempo-bar" style="--dur:${ms}ms; background:${esc(accent)}"></div>
|
|
439
|
+
<code>${ms}ms${label ? ' · ' + label : ''}</code>
|
|
440
|
+
</div>
|
|
441
|
+
`;
|
|
442
|
+
}).join('')}
|
|
443
|
+
</div>
|
|
444
|
+
` : ''}
|
|
445
|
+
|
|
446
|
+
${easings.length ? `
|
|
447
|
+
<h3 class="sub">Easings</h3>
|
|
448
|
+
<ul class="easings">
|
|
449
|
+
${easings.map(e => `<li><code>${esc(typeof e === 'string' ? e : (e.value || e.label || e.family || ''))}</code></li>`).join('')}
|
|
450
|
+
</ul>
|
|
451
|
+
` : ''}
|
|
452
|
+
</section>`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function buildComponents(design, accent) {
|
|
456
|
+
const lib = design.componentLibrary || {};
|
|
457
|
+
const anatomy = design.componentAnatomy || [];
|
|
458
|
+
const components = anatomy.slice(0, 6);
|
|
459
|
+
const radii = (design.borders?.radii || [])
|
|
460
|
+
.map(r => typeof r === 'object' ? r.value : r)
|
|
461
|
+
.filter(n => typeof n === 'number')
|
|
462
|
+
.sort((a, b) => a - b);
|
|
463
|
+
const radius = radii[1] || radii[0] || 6;
|
|
464
|
+
const accentRgb = hexToRgb(accent) || { r: 20, g: 20, b: 20 };
|
|
465
|
+
const accentFg = bestTextOn(accentRgb);
|
|
466
|
+
const neutral = (design.colors?.neutrals || [])[0]?.hex || '#1a1a1a';
|
|
467
|
+
const surface = (design.colors?.backgrounds || [])[1] || '#f7f5ef';
|
|
468
|
+
const text = (design.colors?.text || [])[0] || '#0a0a0a';
|
|
469
|
+
return `
|
|
470
|
+
<section id="components">
|
|
471
|
+
${chapterHeader('09', 'Components')}
|
|
472
|
+
<p class="summary">${esc(lib.library || 'unknown library')} · ${components.length} component patterns captured</p>
|
|
473
|
+
|
|
474
|
+
<h3 class="sub">Mocks</h3>
|
|
475
|
+
<div class="mock-grid">
|
|
476
|
+
<div class="mock-button">
|
|
477
|
+
<button type="button" class="m-btn m-btn-primary" style="background:${esc(accent)};color:${accentFg};border-radius:${radius}px">Primary action</button>
|
|
478
|
+
<button type="button" class="m-btn m-btn-secondary" style="border-radius:${radius}px;border-color:${esc(accent)};color:${esc(accent)}">Secondary</button>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="mock-card" style="background:${esc(surface)};color:${esc(text)};border-radius:${radius * 1.5}px">
|
|
481
|
+
<span class="m-card-eyebrow">Card</span>
|
|
482
|
+
<h4 class="m-card-title">Built from these tokens</h4>
|
|
483
|
+
<p class="m-card-body">Radius, primary, surface, text — all sampled from the live site.</p>
|
|
484
|
+
<a class="m-card-link" href="#" style="color:${esc(accent)}">Read more →</a>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
${components.length ? `
|
|
489
|
+
<h3 class="sub">Detected patterns</h3>
|
|
490
|
+
<div class="component-list">
|
|
491
|
+
${components.map(c => `
|
|
492
|
+
<article class="component-card">
|
|
493
|
+
<h4>${esc(c.kind || c.name || 'component')}</h4>
|
|
494
|
+
<dl class="meta-grid">
|
|
495
|
+
<div><dt>Slots</dt><dd>${asList(c.slots).map(esc).join(', ') || '—'}</dd></div>
|
|
496
|
+
<div><dt>Variants</dt><dd>${asList(c.props && c.props.variant).map(esc).join(', ') || '—'}</dd></div>
|
|
497
|
+
<div><dt>Sizes</dt><dd>${asList(c.props && c.props.size).map(esc).join(', ') || '—'}</dd></div>
|
|
498
|
+
</dl>
|
|
499
|
+
</article>
|
|
500
|
+
`).join('')}
|
|
501
|
+
</div>
|
|
502
|
+
` : ''}
|
|
503
|
+
</section>`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function buildVoice(design) {
|
|
507
|
+
const v = design.voice || {};
|
|
508
|
+
const headings = (v.sampleHeadings || []).slice(0, 4);
|
|
509
|
+
const verbs = (v.ctaVerbs || []).slice(0, 8);
|
|
510
|
+
return `
|
|
511
|
+
<section id="voice">
|
|
512
|
+
${chapterHeader('10', 'Voice')}
|
|
513
|
+
<p class="summary">${esc(v.tone || 'neutral')} · ${esc(v.pronounPosture || '—')} · ${esc(v.headingCase || '—')}</p>
|
|
514
|
+
|
|
515
|
+
${headings.length ? `
|
|
516
|
+
<h3 class="sub">Headlines from the site</h3>
|
|
517
|
+
<ul class="quotes">
|
|
518
|
+
${headings.map(h => `<li>${esc(h)}</li>`).join('')}
|
|
519
|
+
</ul>
|
|
520
|
+
` : ''}
|
|
521
|
+
|
|
522
|
+
${verbs.length ? `
|
|
523
|
+
<h3 class="sub">CTA verbs</h3>
|
|
524
|
+
<ul class="cta-verbs">
|
|
525
|
+
${verbs.map(v2 => {
|
|
526
|
+
const word = typeof v2 === 'string' ? v2 : (v2.verb || '');
|
|
527
|
+
const count = typeof v2 === 'object' ? v2.count : null;
|
|
528
|
+
return `<li><code>${esc(word)}</code>${count ? `<span class="muted">×${count}</span>` : ''}</li>`;
|
|
529
|
+
}).join('')}
|
|
530
|
+
</ul>
|
|
531
|
+
` : ''}
|
|
532
|
+
</section>`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function buildAccessibility(design) {
|
|
536
|
+
const a = design.accessibility || {};
|
|
537
|
+
const failing = (a.failingPairs || []).slice(0, 5);
|
|
538
|
+
const remediation = (a.remediation || []).slice(0, 5);
|
|
539
|
+
const score = a.score != null ? a.score : null;
|
|
540
|
+
return `
|
|
541
|
+
<section id="accessibility">
|
|
542
|
+
${chapterHeader('11', 'Accessibility')}
|
|
543
|
+
|
|
544
|
+
<div class="a11y-score">
|
|
545
|
+
<span class="a11y-num">${score != null ? score : '—'}<span class="a11y-suffix">${score != null ? '%' : ''}</span></span>
|
|
546
|
+
<span class="a11y-label">WCAG 2.1 contrast<br>${a.passCount ?? '—'} passing · ${a.failCount ?? '—'} failing</span>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
${failing.length ? `
|
|
550
|
+
<h3 class="sub">Failing pairs</h3>
|
|
551
|
+
<div class="a11y-pairs">
|
|
552
|
+
${failing.map(p => {
|
|
553
|
+
const fg = p.fg || p.foreground;
|
|
554
|
+
const bg = p.bg || p.background;
|
|
555
|
+
const ratio = (p.ratio || 0).toFixed(2);
|
|
556
|
+
const rule = p.rule || 'AA';
|
|
557
|
+
return `
|
|
558
|
+
<div class="a11y-pair">
|
|
559
|
+
<div class="a11y-stack" style="background:${esc(bg)};color:${esc(fg)}">
|
|
560
|
+
<span class="a11y-Aa">Aa</span>
|
|
561
|
+
<span class="a11y-ratio">${ratio}:1</span>
|
|
562
|
+
</div>
|
|
563
|
+
<dl>
|
|
564
|
+
<div><dt>Foreground</dt><dd><code>${esc(fg)}</code></dd></div>
|
|
565
|
+
<div><dt>Background</dt><dd><code>${esc(bg)}</code></dd></div>
|
|
566
|
+
<div><dt>Rule</dt><dd>${esc(rule)}</dd></div>
|
|
567
|
+
</dl>
|
|
568
|
+
</div>
|
|
569
|
+
`;
|
|
570
|
+
}).join('')}
|
|
571
|
+
</div>
|
|
572
|
+
` : '<p class="muted">No failing contrast pairs detected.</p>'}
|
|
573
|
+
|
|
574
|
+
${remediation.length ? `
|
|
575
|
+
<h3 class="sub">Suggested replacements</h3>
|
|
576
|
+
<ul class="remediation">
|
|
577
|
+
${remediation.map(r => `<li><code>${esc(r.from || r.fg)}</code> → <code>${esc(r.to || r.suggested)}</code> ${r.ratio ? `<span class="muted">(${r.ratio.toFixed(2)}:1)</span>` : ''}</li>`).join('')}
|
|
578
|
+
</ul>
|
|
579
|
+
` : ''}
|
|
580
|
+
</section>`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildTokens(design) {
|
|
584
|
+
const primary = design.colors?.primary?.hex || '#0066cc';
|
|
585
|
+
const fontHead = familyName((design.typography?.families || [])[0]) || 'Inter';
|
|
586
|
+
const space = design.spacing?.base || 4;
|
|
587
|
+
const radii = (design.borders?.radii || [])
|
|
588
|
+
.map(r => typeof r === 'object' ? r.value : r)
|
|
589
|
+
.filter(n => typeof n === 'number')
|
|
590
|
+
.sort((a, b) => a - b);
|
|
591
|
+
const radiusMd = radii[1] || radii[0] || 8;
|
|
592
|
+
const hostName = host(design.meta?.url) || '<url>';
|
|
593
|
+
const cssVars = `:root {
|
|
594
|
+
--color-primary: ${primary};
|
|
595
|
+
--font-display: "${fontHead}";
|
|
596
|
+
--space-base: ${space}px;
|
|
597
|
+
--radius-md: ${radiusMd}px;
|
|
598
|
+
}`;
|
|
599
|
+
const tailwind = `// tailwind.config.js
|
|
600
|
+
module.exports = {
|
|
601
|
+
theme: {
|
|
602
|
+
extend: {
|
|
603
|
+
colors: { brand: '${primary}' },
|
|
604
|
+
fontFamily: { display: ['${fontHead}', 'serif'] },
|
|
605
|
+
spacing: { base: '${space}px' },
|
|
606
|
+
borderRadius: { md: '${radiusMd}px' },
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
};`;
|
|
610
|
+
return `
|
|
611
|
+
<section id="tokens">
|
|
612
|
+
${chapterHeader('12', 'Tokens')}
|
|
613
|
+
<p class="summary">Drop-in code for the most common stacks. All values from the live extraction.</p>
|
|
614
|
+
|
|
615
|
+
<div class="codeblock-wrap">
|
|
616
|
+
<div class="codeblock-head"><span>CSS variables</span><span class="muted">variables.css</span></div>
|
|
617
|
+
<pre class="codeblock"><code>${esc(cssVars)}</code></pre>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div class="codeblock-wrap">
|
|
621
|
+
<div class="codeblock-head"><span>Tailwind config</span><span class="muted">tailwind.config.js</span></div>
|
|
622
|
+
<pre class="codeblock"><code>${esc(tailwind)}</code></pre>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<p class="muted">Run <code>npx designlang pack ${esc(hostName)}</code> for the full bundle (DTCG, shadcn, Figma vars, motion, anatomy, Storybook).</p>
|
|
626
|
+
</section>`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function buildUsage() {
|
|
630
|
+
const rules = [
|
|
631
|
+
['Lead with the primary.', 'It belongs on calls-to-action and one accent moment per screen. Not on body copy.'],
|
|
632
|
+
['Two type families, three weights.', 'Display for headlines, body for paragraphs. Resist a third unless there is a real reason.'],
|
|
633
|
+
['Snap to the spacing scale.', 'Padding, margin, and gap should land on the values in §05. One-off pixels accumulate into noise.'],
|
|
634
|
+
['Treat accessibility as a hard constraint.', 'When a colour pair fails WCAG, fix the colour — not the contrast check.'],
|
|
635
|
+
];
|
|
636
|
+
return `
|
|
637
|
+
<section id="usage">
|
|
638
|
+
${chapterHeader('13', 'Usage')}
|
|
639
|
+
<ol class="usage-list">
|
|
640
|
+
${rules.map(([title, body]) => `
|
|
641
|
+
<li>
|
|
642
|
+
<h3 class="usage-title">${esc(title)}</h3>
|
|
643
|
+
<p class="usage-body">${esc(body)}</p>
|
|
644
|
+
</li>
|
|
645
|
+
`).join('')}
|
|
646
|
+
</ol>
|
|
647
|
+
</section>`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── The book ────────────────────────────────────────────────────
|
|
651
|
+
|
|
652
|
+
export function formatBrandBook(design, opts = {}) {
|
|
653
|
+
if (!design) throw new Error('formatBrandBook: design is required');
|
|
654
|
+
const meta = design.meta || {};
|
|
655
|
+
const hostName = host(meta.url);
|
|
656
|
+
const accent = pickAccent(design);
|
|
657
|
+
const ogTitle = `${hostName} · brand guidelines`;
|
|
658
|
+
const ogDesc = `Design-system documentation for ${hostName}: colour, type, spacing, motion, voice, accessibility, components, and drop-in tokens.`;
|
|
659
|
+
|
|
660
|
+
return `<!doctype html>
|
|
661
|
+
<html lang="en">
|
|
662
|
+
<head>
|
|
663
|
+
<meta charset="utf-8">
|
|
664
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
665
|
+
<title>${esc(ogTitle)}</title>
|
|
666
|
+
<meta name="description" content="${esc(ogDesc)}">
|
|
667
|
+
<meta property="og:title" content="${esc(ogTitle)}">
|
|
668
|
+
<meta property="og:description" content="${esc(ogDesc)}">
|
|
669
|
+
<meta property="og:type" content="article">
|
|
670
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
671
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
672
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
673
|
+
<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">
|
|
674
|
+
<style>
|
|
675
|
+
:root {
|
|
676
|
+
--paper: #f6f3ec;
|
|
677
|
+
--paper-2: #efebe1;
|
|
678
|
+
--ink: #131313;
|
|
679
|
+
--ink-soft: #555048;
|
|
680
|
+
--ink-faint: #8a8579;
|
|
681
|
+
--rule: #e0dccf;
|
|
682
|
+
--accent: ${accent};
|
|
683
|
+
--display: '${FONT_DISPLAY}', 'Times New Roman', serif;
|
|
684
|
+
--body: '${FONT_BODY}', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
685
|
+
--mono: '${FONT_MONO}', ui-monospace, 'SF Mono', monospace;
|
|
686
|
+
}
|
|
687
|
+
[data-theme="dark"] {
|
|
688
|
+
--paper: #0d0c0a;
|
|
689
|
+
--paper-2: #15140f;
|
|
690
|
+
--ink: #ece8de;
|
|
691
|
+
--ink-soft: #9d978a;
|
|
692
|
+
--ink-faint: #5b574e;
|
|
693
|
+
--rule: #292621;
|
|
694
|
+
}
|
|
695
|
+
* { box-sizing: border-box; }
|
|
696
|
+
html { scroll-behavior: smooth; }
|
|
697
|
+
html, body { margin: 0; padding: 0; }
|
|
698
|
+
body {
|
|
699
|
+
background: var(--paper);
|
|
700
|
+
color: var(--ink);
|
|
701
|
+
font-family: var(--body);
|
|
702
|
+
font-size: 16px;
|
|
703
|
+
line-height: 1.6;
|
|
704
|
+
-webkit-font-smoothing: antialiased;
|
|
705
|
+
-moz-osx-font-smoothing: grayscale;
|
|
706
|
+
transition: background .25s, color .25s;
|
|
707
|
+
}
|
|
708
|
+
a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 1px; }
|
|
709
|
+
a:hover { border-color: var(--ink); }
|
|
710
|
+
code { font-family: var(--mono); font-size: 0.92em; }
|
|
711
|
+
.muted { color: var(--ink-soft); }
|
|
712
|
+
.summary { font-family: var(--mono); font-size: 12px; letter-spacing: .04em; color: var(--ink-faint); margin: 0 0 32px; padding-bottom: 14px; border-bottom: 1px solid var(--rule); }
|
|
713
|
+
|
|
714
|
+
.wrap { max-width: 880px; margin: 0 auto; padding: 0 0 96px; }
|
|
715
|
+
|
|
716
|
+
/* — Top bar — */
|
|
717
|
+
.topbar { display: flex; justify-content: space-between; align-items: center; padding: 24px 40px; font-size: 13px; border-bottom: 1px solid var(--rule); }
|
|
718
|
+
.brand { font-family: var(--display); font-size: 22px; }
|
|
719
|
+
.brand a { color: var(--ink); text-decoration: none; border-bottom: 0; }
|
|
720
|
+
.topbar nav { display: flex; gap: 18px; align-items: center; color: var(--ink-soft); }
|
|
721
|
+
.theme-btn { background: transparent; border: 1px solid var(--rule); color: var(--ink-soft); font-size: 11px; padding: 6px 12px; border-radius: 999px; cursor: pointer; letter-spacing: .12em; text-transform: uppercase; font-family: var(--body); }
|
|
722
|
+
.theme-btn:hover { color: var(--ink); border-color: var(--ink); }
|
|
723
|
+
|
|
724
|
+
/* — Cover — */
|
|
725
|
+
.cover { padding: 0; border-bottom: 1px solid var(--rule); }
|
|
726
|
+
.cover-band {
|
|
727
|
+
height: clamp(180px, 28vw, 320px);
|
|
728
|
+
display: flex;
|
|
729
|
+
align-items: flex-end;
|
|
730
|
+
justify-content: space-between;
|
|
731
|
+
padding: 32px 40px;
|
|
732
|
+
}
|
|
733
|
+
.cover-band-label {
|
|
734
|
+
font-family: var(--mono);
|
|
735
|
+
font-size: 11px;
|
|
736
|
+
text-transform: uppercase;
|
|
737
|
+
letter-spacing: .18em;
|
|
738
|
+
opacity: 0.78;
|
|
739
|
+
}
|
|
740
|
+
.cover-band-hex code { font-family: var(--mono); font-size: 13px; opacity: 0.9; }
|
|
741
|
+
.cover-body { padding: 56px 40px 64px; }
|
|
742
|
+
.kicker { font-family: var(--mono); text-transform: uppercase; letter-spacing: .18em; font-size: 11px; color: var(--ink-soft); margin: 0 0 18px; }
|
|
743
|
+
.cover-title {
|
|
744
|
+
font-family: var(--display);
|
|
745
|
+
font-weight: 400;
|
|
746
|
+
font-size: clamp(56px, 11vw, 132px);
|
|
747
|
+
line-height: .94;
|
|
748
|
+
letter-spacing: -.02em;
|
|
749
|
+
margin: 0 0 24px;
|
|
750
|
+
}
|
|
751
|
+
.cover-sub { font-size: 19px; line-height: 1.5; color: var(--ink-soft); max-width: 60ch; margin: 0 0 36px; }
|
|
752
|
+
.cover-meta { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 22px 32px; padding: 28px 0 0; border-top: 1px solid var(--rule); margin: 0; }
|
|
753
|
+
.cover-meta dt { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-faint); margin-bottom: 4px; }
|
|
754
|
+
.cover-meta dd { margin: 0; font-size: 15px; }
|
|
755
|
+
|
|
756
|
+
/* — TOC — */
|
|
757
|
+
.toc { padding: 56px 40px; border-bottom: 1px solid var(--rule); background: var(--paper-2); }
|
|
758
|
+
.toc-title { font-family: var(--display); font-weight: 400; font-size: 28px; margin: 0 0 24px; }
|
|
759
|
+
.toc ol { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 48px; }
|
|
760
|
+
@media (max-width: 640px) { .toc ol { columns: 1; } .toc, .topbar, .cover-band, .cover-body { padding-left: 22px; padding-right: 22px; } }
|
|
761
|
+
.toc li { padding: 10px 0; border-top: 1px solid var(--rule); break-inside: avoid; }
|
|
762
|
+
.toc li:last-child { border-bottom: 1px solid var(--rule); }
|
|
763
|
+
.toc a { display: flex; gap: 14px; align-items: baseline; border: 0; }
|
|
764
|
+
.toc-num { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); width: 26px; }
|
|
765
|
+
.toc-label { font-family: var(--display); font-size: 18px; }
|
|
766
|
+
|
|
767
|
+
/* — Sections — */
|
|
768
|
+
section { padding: 72px 40px; border-bottom: 1px solid var(--rule); }
|
|
769
|
+
section:last-of-type { border-bottom: 0; }
|
|
770
|
+
@media (max-width: 640px) { section { padding: 48px 22px; } }
|
|
771
|
+
.chapter-header { padding-bottom: 18px; margin-bottom: 28px; border-bottom: 1px solid var(--rule); }
|
|
772
|
+
.sec-num { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; color: var(--ink-faint); text-transform: uppercase; display: block; margin-bottom: 10px; }
|
|
773
|
+
section h2 { font-family: var(--display); font-weight: 400; font-size: clamp(36px, 4.5vw, 56px); line-height: 1.04; letter-spacing: -.005em; margin: 0; }
|
|
774
|
+
.sub { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 36px 0 14px; }
|
|
775
|
+
|
|
776
|
+
/* — Meta grid — */
|
|
777
|
+
.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px 32px; padding: 0; margin: 0 0 24px; }
|
|
778
|
+
.meta-grid > div { padding: 0; }
|
|
779
|
+
.meta-grid dt { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink-faint); margin-bottom: 4px; }
|
|
780
|
+
.meta-grid dd { margin: 0; font-size: 15px; }
|
|
781
|
+
|
|
782
|
+
/* — Color section — */
|
|
783
|
+
.brand-grid { display: grid; gap: 20px; margin: 0 0 28px; }
|
|
784
|
+
.brand-grid-pair { grid-template-columns: 1fr 1fr; }
|
|
785
|
+
@media (max-width: 640px) { .brand-grid-pair { grid-template-columns: 1fr; } }
|
|
786
|
+
.brand-color { display: flex; flex-direction: column; gap: 14px; }
|
|
787
|
+
.big-swatch {
|
|
788
|
+
aspect-ratio: 1.9 / 1;
|
|
789
|
+
border-radius: 8px;
|
|
790
|
+
padding: 22px 24px;
|
|
791
|
+
display: flex;
|
|
792
|
+
flex-direction: column;
|
|
793
|
+
justify-content: space-between;
|
|
794
|
+
box-shadow: inset 0 0 0 1px rgba(0,0,0,.06);
|
|
795
|
+
}
|
|
796
|
+
.brand-color-primary .big-swatch { aspect-ratio: 2.6 / 1; }
|
|
797
|
+
.big-swatch-name { font-family: var(--display); font-size: 32px; line-height: 1; }
|
|
798
|
+
.big-swatch-hex { font-family: var(--mono); font-size: 14px; letter-spacing: .04em; opacity: 0.92; }
|
|
799
|
+
.color-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 0 24px; padding: 0; margin: 0; }
|
|
800
|
+
.color-meta dt { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink-faint); margin-bottom: 2px; }
|
|
801
|
+
.color-meta dd { margin: 0 0 10px; font-size: 14px; }
|
|
802
|
+
|
|
803
|
+
.neutral-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
|
|
804
|
+
.neutral-cell { aspect-ratio: 1.8 / 1; display: flex; align-items: flex-end; padding: 10px 12px; }
|
|
805
|
+
.neutral-cell code { font-size: 11px; }
|
|
806
|
+
|
|
807
|
+
.palette-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 6px; }
|
|
808
|
+
.mini-swatch { aspect-ratio: 1.6 / 1; border-radius: 5px; padding: 9px 11px; display: flex; align-items: flex-end; box-shadow: inset 0 0 0 1px rgba(0,0,0,.06); }
|
|
809
|
+
.mini-swatch code { font-size: 10px; opacity: 0.92; }
|
|
810
|
+
|
|
811
|
+
.callout {
|
|
812
|
+
margin-top: 32px;
|
|
813
|
+
padding: 14px 18px;
|
|
814
|
+
background: rgba(0,0,0,.03);
|
|
815
|
+
border-left: 2px solid var(--accent);
|
|
816
|
+
border-radius: 0 4px 4px 0;
|
|
817
|
+
font-size: 14px;
|
|
818
|
+
}
|
|
819
|
+
[data-theme="dark"] .callout { background: rgba(255,255,255,.04); }
|
|
820
|
+
|
|
821
|
+
/* — Logo — */
|
|
822
|
+
.logo-card { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; align-items: center; padding: 24px 0; }
|
|
823
|
+
@media (max-width: 640px) { .logo-card { grid-template-columns: 1fr; } }
|
|
824
|
+
.logo-canvas { aspect-ratio: 16 / 9; background: var(--paper-2); border-radius: 8px; display: flex; align-items: center; justify-content: center; padding: 32px; box-shadow: inset 0 0 0 1px var(--rule); }
|
|
825
|
+
.logo-canvas svg { max-width: 60%; max-height: 80%; }
|
|
826
|
+
.logo-placeholder { font-family: var(--display); font-size: 36px; color: var(--ink-soft); }
|
|
827
|
+
.logo-meta { grid-template-columns: 1fr; }
|
|
828
|
+
|
|
829
|
+
/* — Type — */
|
|
830
|
+
.specimen { padding: 18px 0 26px; }
|
|
831
|
+
.spec-line { line-height: 1.05; margin: 0 0 18px; letter-spacing: -.01em; }
|
|
832
|
+
.spec-display { font-weight: 400; }
|
|
833
|
+
.spec-body { color: var(--ink-soft); font-style: italic; }
|
|
834
|
+
.scale-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
835
|
+
.scale-table th, .scale-table td { padding: 14px 8px; text-align: left; border-bottom: 1px solid var(--rule); vertical-align: middle; }
|
|
836
|
+
.scale-table th { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink-faint); border-bottom: 1px solid var(--ink); }
|
|
837
|
+
.scale-table .t-step { width: 64px; }
|
|
838
|
+
.scale-table .t-size { width: 80px; }
|
|
839
|
+
.scale-table .t-sample { color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
840
|
+
|
|
841
|
+
/* — Spacing — */
|
|
842
|
+
.space-rhythm { display: flex; flex-direction: column; gap: 10px; padding: 8px 0; }
|
|
843
|
+
.space-step { display: flex; align-items: center; gap: 14px; }
|
|
844
|
+
.space-block { height: 14px; background: var(--ink); border-radius: 2px; flex: 0 0 auto; }
|
|
845
|
+
[data-theme="dark"] .space-block { background: var(--ink); }
|
|
846
|
+
.space-step code { font-size: 11px; color: var(--ink-soft); min-width: 56px; }
|
|
847
|
+
|
|
848
|
+
/* — Shape — */
|
|
849
|
+
.radii-row, .shadows-row { display: grid; gap: 16px; }
|
|
850
|
+
.radii-row { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
|
|
851
|
+
.shadows-row { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
|
|
852
|
+
.radius-card, .shadow-card { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
|
|
853
|
+
.radius-block { width: 96px; height: 96px; background: var(--paper-2); box-shadow: inset 0 0 0 1px var(--rule); }
|
|
854
|
+
.shadow-block { width: 100%; height: 96px; background: var(--paper); border-radius: 10px; }
|
|
855
|
+
[data-theme="dark"] .shadow-block { background: var(--paper-2); }
|
|
856
|
+
.radius-card code, .shadow-card code { font-size: 11px; color: var(--ink-soft); }
|
|
857
|
+
|
|
858
|
+
/* — Iconography — */
|
|
859
|
+
.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); gap: 10px; }
|
|
860
|
+
.icon-cell { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border: 1px solid var(--rule); border-radius: 6px; padding: 10px; background: var(--paper-2); }
|
|
861
|
+
.icon-cell svg { width: 100%; height: 100%; }
|
|
862
|
+
|
|
863
|
+
/* — Motion — */
|
|
864
|
+
.motion-tempo { display: flex; flex-direction: column; gap: 10px; }
|
|
865
|
+
.tempo-item { display: flex; align-items: center; gap: 14px; }
|
|
866
|
+
.tempo-bar { height: 6px; border-radius: 999px; min-width: 24px; opacity: 0.8; flex: 0 0 auto; animation: tempoSlide var(--dur, 600ms) cubic-bezier(.2,.7,.2,1) infinite alternate; }
|
|
867
|
+
@keyframes tempoSlide { from { width: 24px; opacity: 0.5; } to { width: 280px; opacity: 0.95; } }
|
|
868
|
+
.tempo-item code { font-family: var(--mono); font-size: 12px; color: var(--ink-soft); min-width: 120px; }
|
|
869
|
+
.easings { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
870
|
+
.easings li { padding: 6px 12px; background: var(--paper-2); border: 1px solid var(--rule); border-radius: 4px; font-size: 12px; }
|
|
871
|
+
|
|
872
|
+
/* — Components — */
|
|
873
|
+
.mock-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 24px; align-items: start; padding: 8px 0 28px; }
|
|
874
|
+
@media (max-width: 640px) { .mock-grid { grid-template-columns: 1fr; } }
|
|
875
|
+
.mock-button { display: flex; flex-direction: column; gap: 14px; padding: 32px; background: var(--paper-2); border-radius: 12px; box-shadow: inset 0 0 0 1px var(--rule); align-items: flex-start; }
|
|
876
|
+
.m-btn { font-family: var(--body); font-size: 14px; font-weight: 500; padding: 12px 22px; border: 1px solid transparent; cursor: pointer; }
|
|
877
|
+
.m-btn-secondary { background: transparent; border-style: solid; border-width: 1px; }
|
|
878
|
+
.mock-card { padding: 26px 28px; box-shadow: inset 0 0 0 1px var(--rule); }
|
|
879
|
+
.m-card-eyebrow { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-faint); }
|
|
880
|
+
.m-card-title { font-family: var(--display); font-weight: 400; font-size: 26px; margin: 8px 0 8px; line-height: 1.1; }
|
|
881
|
+
.m-card-body { margin: 0 0 14px; font-size: 14px; color: var(--ink-soft); line-height: 1.5; }
|
|
882
|
+
.m-card-link { font-family: var(--body); font-size: 13px; font-weight: 500; border: 0; padding: 0; }
|
|
883
|
+
|
|
884
|
+
.component-list { display: flex; flex-direction: column; }
|
|
885
|
+
.component-card { padding: 18px 0; border-top: 1px solid var(--rule); }
|
|
886
|
+
.component-card:last-of-type { border-bottom: 1px solid var(--rule); }
|
|
887
|
+
.component-card h4 { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 0 0 14px; }
|
|
888
|
+
|
|
889
|
+
/* — Voice — */
|
|
890
|
+
.quotes { list-style: none; padding: 0; margin: 0; }
|
|
891
|
+
.quotes li {
|
|
892
|
+
font-family: var(--display);
|
|
893
|
+
font-size: 26px;
|
|
894
|
+
line-height: 1.18;
|
|
895
|
+
padding: 22px 0;
|
|
896
|
+
border-top: 1px solid var(--rule);
|
|
897
|
+
color: var(--ink);
|
|
898
|
+
}
|
|
899
|
+
.quotes li:last-child { border-bottom: 1px solid var(--rule); }
|
|
900
|
+
.cta-verbs { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px 20px; }
|
|
901
|
+
.cta-verbs li { display: flex; align-items: baseline; gap: 6px; font-size: 14px; }
|
|
902
|
+
.cta-verbs code { font-size: 13px; }
|
|
903
|
+
.cta-verbs .muted { font-family: var(--mono); font-size: 11px; }
|
|
904
|
+
|
|
905
|
+
/* — Accessibility — */
|
|
906
|
+
.a11y-score { display: flex; align-items: baseline; gap: 24px; padding: 12px 0 32px; border-bottom: 1px solid var(--rule); margin-bottom: 24px; }
|
|
907
|
+
.a11y-num { font-family: var(--display); font-size: clamp(72px, 10vw, 120px); line-height: 1; letter-spacing: -.02em; }
|
|
908
|
+
.a11y-suffix { font-size: 0.5em; color: var(--ink-soft); margin-left: 4px; }
|
|
909
|
+
.a11y-label { color: var(--ink-soft); font-size: 14px; line-height: 1.4; }
|
|
910
|
+
.a11y-pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
|
|
911
|
+
.a11y-pair { display: flex; flex-direction: column; gap: 12px; }
|
|
912
|
+
.a11y-stack { padding: 22px 18px; border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.06); display: flex; justify-content: space-between; align-items: center; }
|
|
913
|
+
.a11y-Aa { font-family: var(--display); font-size: 32px; line-height: 1; }
|
|
914
|
+
.a11y-ratio { font-family: var(--mono); font-size: 12px; opacity: 0.9; }
|
|
915
|
+
.a11y-pair dl { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; padding: 0; margin: 0; }
|
|
916
|
+
.a11y-pair dt { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-faint); }
|
|
917
|
+
.a11y-pair dd { margin: 0 0 4px; font-size: 12px; }
|
|
918
|
+
.remediation { list-style: none; padding: 0; margin: 0; }
|
|
919
|
+
.remediation li { padding: 10px 0; border-top: 1px solid var(--rule); }
|
|
920
|
+
.remediation li:last-child { border-bottom: 1px solid var(--rule); }
|
|
921
|
+
|
|
922
|
+
/* — Tokens — */
|
|
923
|
+
.codeblock-wrap { margin: 0 0 18px; border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
|
|
924
|
+
.codeblock-head { display: flex; justify-content: space-between; padding: 10px 14px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-faint); background: var(--paper-2); border-bottom: 1px solid var(--rule); }
|
|
925
|
+
.codeblock { background: var(--paper-2); padding: 16px 18px; overflow-x: auto; font-family: var(--mono); font-size: 12px; line-height: 1.6; margin: 0; color: var(--ink); }
|
|
926
|
+
|
|
927
|
+
/* — Usage — */
|
|
928
|
+
.usage-list { padding: 0; margin: 0; counter-reset: rule; list-style: none; }
|
|
929
|
+
.usage-list li { padding: 22px 0; border-top: 1px solid var(--rule); position: relative; padding-left: 56px; counter-increment: rule; }
|
|
930
|
+
.usage-list li:last-child { border-bottom: 1px solid var(--rule); }
|
|
931
|
+
.usage-list li::before { content: counter(rule, decimal-leading-zero); position: absolute; left: 0; top: 26px; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); letter-spacing: .14em; }
|
|
932
|
+
.usage-title { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 0 0 6px; }
|
|
933
|
+
.usage-body { margin: 0; color: var(--ink-soft); font-size: 15px; line-height: 1.55; }
|
|
934
|
+
|
|
935
|
+
/* — Footer — */
|
|
936
|
+
footer { padding: 40px 40px 0; font-size: 13px; color: var(--ink-soft); display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap; gap: 16px; }
|
|
937
|
+
footer .sig { font-family: var(--display); font-size: 22px; color: var(--ink); }
|
|
938
|
+
footer .stamp { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; }
|
|
939
|
+
@media (max-width: 640px) { footer { padding: 40px 22px 0; } }
|
|
940
|
+
|
|
941
|
+
/* — Print — */
|
|
942
|
+
@media print {
|
|
943
|
+
body { background: white; color: black; }
|
|
944
|
+
.topbar nav, .theme-btn { display: none; }
|
|
945
|
+
.cover, .toc, section { page-break-inside: avoid; border-color: #ddd; padding: 36px 32px; }
|
|
946
|
+
.cover-band { background-color: var(--accent) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
947
|
+
.toc, .cover { page-break-after: always; }
|
|
948
|
+
section { page-break-after: always; }
|
|
949
|
+
.scale-table, .a11y-pair, .component-card { page-break-inside: avoid; }
|
|
950
|
+
}
|
|
951
|
+
</style>
|
|
952
|
+
</head>
|
|
953
|
+
<body>
|
|
954
|
+
<header class="topbar">
|
|
955
|
+
<div class="brand"><a href="https://designlang.app">designlang</a></div>
|
|
956
|
+
<nav>
|
|
957
|
+
<span>Brand guidelines</span>
|
|
958
|
+
<button class="theme-btn" id="themeBtn" type="button">Theme</button>
|
|
959
|
+
</nav>
|
|
960
|
+
</header>
|
|
961
|
+
|
|
962
|
+
<main class="wrap">
|
|
963
|
+
${buildCover(design, accent)}
|
|
964
|
+
${buildToc()}
|
|
965
|
+
${buildAbout(design)}
|
|
966
|
+
${buildLogo(design)}
|
|
967
|
+
${buildColor(design)}
|
|
968
|
+
${buildType(design)}
|
|
969
|
+
${buildSpacing(design)}
|
|
970
|
+
${buildShape(design)}
|
|
971
|
+
${buildIconography(design)}
|
|
972
|
+
${buildMotion(design, accent)}
|
|
973
|
+
${buildComponents(design, accent)}
|
|
974
|
+
${buildVoice(design)}
|
|
975
|
+
${buildAccessibility(design)}
|
|
976
|
+
${buildTokens(design)}
|
|
977
|
+
${buildUsage()}
|
|
978
|
+
|
|
979
|
+
<footer>
|
|
980
|
+
<div>
|
|
981
|
+
<div class="sig">designlang</div>
|
|
982
|
+
<div>Re-run: <code>npx designlang brand ${esc(host(design.meta?.url) || '<url>')}</code></div>
|
|
983
|
+
</div>
|
|
984
|
+
<div class="stamp">${esc(new Date(design.meta?.timestamp || Date.now()).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }))} · v${esc(opts.version || '')}</div>
|
|
985
|
+
</footer>
|
|
986
|
+
</main>
|
|
987
|
+
|
|
988
|
+
<script>
|
|
989
|
+
(function () {
|
|
990
|
+
var btn = document.getElementById('themeBtn');
|
|
991
|
+
var saved = null;
|
|
992
|
+
try { saved = localStorage.getItem('dl-theme'); } catch (e) {}
|
|
993
|
+
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
|
994
|
+
btn && btn.addEventListener('click', function () {
|
|
995
|
+
var cur = document.documentElement.getAttribute('data-theme') === 'dark' ? '' : 'dark';
|
|
996
|
+
if (cur) document.documentElement.setAttribute('data-theme', cur);
|
|
997
|
+
else document.documentElement.removeAttribute('data-theme');
|
|
998
|
+
try { localStorage.setItem('dl-theme', cur); } catch (e) {}
|
|
999
|
+
});
|
|
1000
|
+
})();
|
|
1001
|
+
</script>
|
|
1002
|
+
</body>
|
|
1003
|
+
</html>`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export function formatBrandBookMarkdown(design) {
|
|
1007
|
+
const meta = design.meta || {};
|
|
1008
|
+
const hostName = host(meta.url);
|
|
1009
|
+
const date = new Date(meta.timestamp || Date.now()).toISOString().slice(0, 10);
|
|
1010
|
+
const colors = design.colors || {};
|
|
1011
|
+
const t = design.typography || {};
|
|
1012
|
+
const families = (t.families || []).map(familyName).filter(Boolean);
|
|
1013
|
+
const lines = [
|
|
1014
|
+
`# ${hostName} — Brand guidelines`,
|
|
1015
|
+
``,
|
|
1016
|
+
`_Generated by designlang on ${date}._`,
|
|
1017
|
+
``,
|
|
1018
|
+
`## 01 · About`,
|
|
1019
|
+
``,
|
|
1020
|
+
`- Page intent: ${design.pageIntent?.type || 'unknown'}`,
|
|
1021
|
+
`- Material language: ${design.materialLanguage?.label || 'unknown'}`,
|
|
1022
|
+
`- Component library: ${design.componentLibrary?.library || 'unknown'}`,
|
|
1023
|
+
`- Voice tone: ${design.voice?.tone || 'neutral'}`,
|
|
1024
|
+
``,
|
|
1025
|
+
`## 03 · Colour`,
|
|
1026
|
+
``,
|
|
1027
|
+
colors.primary?.hex ? `- **Primary:** \`${colors.primary.hex}\`` : '- Primary: —',
|
|
1028
|
+
colors.secondary?.hex ? `- **Secondary:** \`${colors.secondary.hex}\`` : null,
|
|
1029
|
+
colors.accent?.hex ? `- **Accent:** \`${colors.accent.hex}\`` : null,
|
|
1030
|
+
`- ${(colors.all || []).length} total colours`,
|
|
1031
|
+
``,
|
|
1032
|
+
`## 04 · Typography`,
|
|
1033
|
+
``,
|
|
1034
|
+
`- Display: ${families[0] || '—'}`,
|
|
1035
|
+
`- Body: ${families[1] || families[0] || '—'}`,
|
|
1036
|
+
`- Scale: ${(t.scale || []).length} sizes`,
|
|
1037
|
+
``,
|
|
1038
|
+
`## 05 · Spacing`,
|
|
1039
|
+
``,
|
|
1040
|
+
`- Base unit: ${design.spacing?.base ? design.spacing.base + 'px' : '—'}`,
|
|
1041
|
+
`- Scale length: ${(design.spacing?.scale || []).length}`,
|
|
1042
|
+
``,
|
|
1043
|
+
`## 11 · Accessibility`,
|
|
1044
|
+
``,
|
|
1045
|
+
`- WCAG score: ${design.accessibility?.score ?? '—'}%`,
|
|
1046
|
+
`- Failing pairs: ${design.accessibility?.failCount ?? '—'}`,
|
|
1047
|
+
``,
|
|
1048
|
+
`---`,
|
|
1049
|
+
`_Re-run: \`npx designlang brand ${hostName}\`_`,
|
|
1050
|
+
].filter(Boolean);
|
|
1051
|
+
return lines.join('\n');
|
|
1052
|
+
}
|