Skip to content

Commit f25ef2b

Browse files
committed
feat(v11.2): DESIGN.md emitter + vs/design-extractor page
Neutralizes design-extractor.com's entire differentiator and ships an honest, dogfooded comparison page on the website. - src/formatters/design-md.js: emits a single *-DESIGN.md compatible with the 8-canonical-section convention (Overview, Colors, Typography, Layout, Elevation and Depth, Shapes, Components, Do's and Don'ts). YAML front matter holds machine-readable token snapshot. Body is driven by v10/v11 semantic layer (intent, material, voice, library, anatomy) so it has more to say than a token dump. - bin: writes *-DESIGN.md by default; --no-design-md to opt out. - website/app/vs/design-extractor/page.js: editorial comparison page with feature table, real DESIGN.md sample, and the actual extraction we ran on design-extractor.com itself (tailwindcss 0.842, friendly voice, 8 colors). Homepage nav gets a "vs" link in accent color. Credit acknowledged in the page: they invented the format; we extended the pipeline behind it.
1 parent 6301114 commit f25ef2b

4 files changed

Lines changed: 576 additions & 0 deletions

File tree

bin/design-extract.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ program
9494
.option('--smart', 'use optional LLM fallback when heuristic classifiers have low confidence (needs OPENAI_API_KEY or ANTHROPIC_API_KEY)')
9595
.option('--pages <n>', 'crawl N canonical pages (pricing/docs/blog/about/product) in addition to the homepage', parseInt)
9696
.option('--no-prompts', 'skip writing the prompt-pack directory')
97+
.option('--no-design-md', 'skip writing the agent-native DESIGN.md (single-file, 8-section, YAML front matter)')
9798
.option('--responsive-shots', 'capture full-page PNGs at 4 breakpoints × (light,dark)')
9899
.option('--perf', 'measure Core Web Vitals + bundle profile (LCP/CLS/INP, JS/CSS/font/img bytes, third-party count)')
99100
.option('--json', 'output raw JSON to stdout (for CI/CD)')
@@ -352,6 +353,13 @@ program
352353
}
353354
files.push({ name: `${prefix}-voice.json`, content: JSON.stringify(design.voice || {}, null, 2), label: 'Brand Voice' });
354355

356+
// v11.2: agent-native single-file DESIGN.md (compatible with the
357+
// 8-canonical-section convention; default-on, opt-out via --no-design-md).
358+
if (merged.designMd !== false) {
359+
const { formatDesignMd } = await import('../src/formatters/design-md.js');
360+
files.push({ name: `${prefix}-DESIGN.md`, content: formatDesignMd(design), label: 'DESIGN.md (agent-native)' });
361+
}
362+
355363
// v10: page intent + section roles + visual DNA + component library + multi-page + prompt pack.
356364
files.push({ name: `${prefix}-intent.json`, content: JSON.stringify({ pageIntent: design.pageIntent, sectionRoles: design.sectionRoles }, null, 2), label: 'Page Intent + Section Roles' });
357365
files.push({ name: `${prefix}-visual-dna.json`, content: JSON.stringify({ materialLanguage: design.materialLanguage, imageryStyle: design.imageryStyle, backgroundPatterns: design.backgroundPatterns }, null, 2), label: 'Visual DNA' });

src/formatters/design-md.js

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// DESIGN.md — agent-native single-file output. Compatible with the
2+
// 8-canonical-section convention (Overview · Colors · Typography · Layout
3+
// · Elevation and Depth · Shapes · Components · Do's and Don'ts), plus
4+
// YAML front matter holding the machine-readable token snapshot.
5+
//
6+
// Designed to be a drop-in replacement for design-extractor.com's
7+
// DESIGN.md output, but driven by the v10/v11 semantic layer (intent,
8+
// material, voice, anatomy, library detection) rather than just colors.
9+
10+
import { readFileSync } from 'fs';
11+
12+
function yamlString(v) {
13+
if (v == null) return '~';
14+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
15+
const s = String(v);
16+
if (s === '' || /[:#&*!?[\]{},|>%@`'"\n]/.test(s) || /^\s|\s$/.test(s)) {
17+
return JSON.stringify(s);
18+
}
19+
return s;
20+
}
21+
22+
function yamlList(arr, indent = ' ') {
23+
if (!arr || arr.length === 0) return '[]';
24+
return '\n' + arr.map(v => `${indent}- ${yamlString(v)}`).join('\n');
25+
}
26+
27+
function yamlMap(obj, indent = ' ') {
28+
const entries = Object.entries(obj || {}).filter(([, v]) => v != null);
29+
if (entries.length === 0) return '{}';
30+
return '\n' + entries.map(([k, v]) => `${indent}${k}: ${yamlString(v)}`).join('\n');
31+
}
32+
33+
function topColor(colors, role) {
34+
return colors?.[role]?.hex || null;
35+
}
36+
37+
function fmtPx(v) {
38+
if (v == null) return null;
39+
const n = typeof v === 'number' ? v : parseFloat(String(v));
40+
return Number.isFinite(n) ? `${Math.round(n)}px` : String(v);
41+
}
42+
43+
function uniq(arr) { return [...new Set(arr.filter(Boolean))]; }
44+
45+
function pickHeading(voice, fallback) {
46+
const s = (voice?.sampleHeadings || []).find(h => h && h.length > 4 && h.length < 80);
47+
return s || fallback;
48+
}
49+
50+
function ratioLine(v, n) { return `${v} (${n})`; }
51+
52+
// ─── Section renderers ─────────────────────────────────────────
53+
54+
function sectionOverview(design) {
55+
const intent = design.pageIntent?.type || 'landing';
56+
const intentConf = design.pageIntent?.confidence;
57+
const material = design.materialLanguage?.label || 'flat';
58+
const matConf = design.materialLanguage?.confidence;
59+
const library = design.componentLibrary?.library;
60+
const libConf = design.componentLibrary?.confidence;
61+
const order = (design.sectionRoles?.readingOrder || []).join(' → ') || '—';
62+
const voice = design.voice || {};
63+
const tone = voice.tone || 'neutral';
64+
const lede = pickHeading(voice, design.meta?.title || '');
65+
const url = design.meta?.url || '';
66+
67+
const lines = [];
68+
lines.push(`A **${intent}** page${intentConf ? ` (heuristic confidence ${intentConf})` : ''}, dressed in **${material}** material${matConf ? ` (${matConf})` : ''}.`);
69+
if (library && library !== 'unknown') {
70+
lines.push('');
71+
lines.push(`Component library appears to be **${library}**${libConf ? ` (${libConf})` : ''}.`);
72+
}
73+
if (lede) {
74+
lines.push('');
75+
lines.push(`> "${lede}"`);
76+
}
77+
lines.push('');
78+
lines.push(`The author writes in a **${tone}** voice; headings tend to be **${voice.headingStyle || 'sentence'}** case and **${voice.headingLengthClass || 'balanced'}**.`);
79+
lines.push('');
80+
lines.push(`Reading order detected on the source: \`${order}\`.`);
81+
if (url) {
82+
lines.push('');
83+
lines.push(`Source: <${url}>.`);
84+
}
85+
return lines.join('\n');
86+
}
87+
88+
function sectionColors(design) {
89+
const c = design.colors || {};
90+
const lines = ['| role | hex | usage |', '|---|---|---|'];
91+
const rows = [
92+
['primary', c.primary?.hex, c.primary?.count],
93+
['secondary', c.secondary?.hex, c.secondary?.count],
94+
['accent', c.accent?.hex, c.accent?.count],
95+
['background', c.backgrounds?.[0], '—'],
96+
['foreground', c.text?.[0], '—'],
97+
];
98+
for (const [role, hex, count] of rows) {
99+
if (!hex) continue;
100+
lines.push(`| ${role} | \`${hex}\` | ${count ?? '—'} |`);
101+
}
102+
const neutrals = (c.neutrals || []).slice(0, 5);
103+
if (neutrals.length) {
104+
lines.push('');
105+
lines.push('**Neutrals:** ' + neutrals.map(n => `\`${n.hex}\``).join(' · '));
106+
}
107+
if (c.all?.length) {
108+
lines.push('');
109+
lines.push(`**Total unique colors detected:** ${c.all.length}.`);
110+
}
111+
return lines.join('\n');
112+
}
113+
114+
function sectionTypography(design) {
115+
const t = design.typography || {};
116+
const lines = [];
117+
if (t.families?.length) {
118+
lines.push('**Families**');
119+
for (const f of t.families.slice(0, 4)) {
120+
lines.push(`- \`${f.name}\`${f.weights ? ` — weights ${[...new Set(f.weights)].join(', ')}` : ''}${f.count ? ` · ${f.count} uses` : ''}`);
121+
}
122+
}
123+
if (t.body?.size) {
124+
lines.push('');
125+
lines.push(`**Body size:** \`${t.body.size}px\` / line-height \`${t.body.lineHeight ?? '1.5'}\`.`);
126+
}
127+
if (t.headings?.length) {
128+
lines.push('');
129+
lines.push('**Heading scale**');
130+
lines.push('| level | size | weight | line-height |');
131+
lines.push('|---|---|---|---|');
132+
for (const [i, h] of t.headings.slice(0, 4).entries()) {
133+
lines.push(`| h${i + 1} | \`${h.size}px\` | \`${h.weight}\` | \`${h.lineHeight}\` |`);
134+
}
135+
}
136+
return lines.join('\n');
137+
}
138+
139+
function sectionLayout(design) {
140+
const sp = design.spacing || {};
141+
const bp = design.breakpoints || [];
142+
const layout = design.layout || {};
143+
const lines = [];
144+
if (sp.base) lines.push(`**Spacing base:** \`${sp.base}px\` increments.`);
145+
if (sp.scale?.length) lines.push(`**Scale:** ${sp.scale.slice(0, 10).map(s => `\`${(s.value ?? s)}px\``).join(' · ')}`);
146+
if (layout.gridCount != null || layout.flexCount != null) {
147+
lines.push('');
148+
lines.push(`**Layout primitives:** ${layout.gridCount ?? 0} grid containers · ${layout.flexCount ?? 0} flex containers.`);
149+
}
150+
if (bp.length) {
151+
lines.push('');
152+
lines.push(`**Breakpoints:** ${bp.map(b => `\`${b}px\``).join(' · ')}`);
153+
}
154+
return lines.join('\n') || '_No layout signals captured._';
155+
}
156+
157+
function sectionElevation(design) {
158+
const sh = design.shadows?.values || [];
159+
const z = design.zIndex || {};
160+
const lines = [];
161+
if (sh.length) {
162+
lines.push('**Shadow scale**');
163+
for (const s of sh.slice(0, 6)) {
164+
lines.push(`- \`${s.label || '?'}\` — \`${s.raw || s.value}\``);
165+
}
166+
} else {
167+
lines.push('_No discrete shadow tokens detected — flat material._');
168+
}
169+
if (z.allValues?.length) {
170+
lines.push('');
171+
lines.push(`**Z-index layers:** ${z.allValues.length}${z.issues?.length ? ` · ⚠ ${z.issues.length} issue(s)` : ''}`);
172+
}
173+
return lines.join('\n');
174+
}
175+
176+
function sectionShapes(design) {
177+
const r = design.borders?.radii || [];
178+
const lines = [];
179+
if (r.length) {
180+
lines.push('**Radius scale**');
181+
for (const x of r.slice(0, 6)) lines.push(`- \`${x.label || '?'}\` — \`${x.value}px\``);
182+
} else {
183+
lines.push('_No discrete radius tokens detected — sharp/brutalist shapes._');
184+
}
185+
return lines.join('\n');
186+
}
187+
188+
function sectionComponents(design) {
189+
const lines = [];
190+
const detected = Object.keys(design.components || {});
191+
if (detected.length) {
192+
lines.push(`**Detected patterns:** ${detected.map(c => `\`${c}\``).join(' · ')}`);
193+
lines.push('');
194+
}
195+
const anatomies = design.componentAnatomy || [];
196+
if (anatomies.length) {
197+
lines.push('**Anatomy**');
198+
lines.push('| kind | variants | sizes | instances |');
199+
lines.push('|---|---|---|---|');
200+
for (const a of anatomies.slice(0, 8)) {
201+
const variants = (a.props?.variant || []).join(', ') || '—';
202+
const sizes = (a.props?.size || []).join(', ') || '—';
203+
lines.push(`| ${a.kind} | ${variants} | ${sizes} | ${a.totalInstances ?? '—'} |`);
204+
}
205+
}
206+
if (!lines.length) lines.push('_No component anatomy extracted._');
207+
return lines.join('\n');
208+
}
209+
210+
function sectionDosDonts(design) {
211+
const voice = design.voice || {};
212+
const score = design.score || {};
213+
const a11y = design.accessibility || {};
214+
const lines = [];
215+
216+
lines.push("**Do's**");
217+
const dos = [];
218+
const ctas = (voice.ctaVerbs || []).slice(0, 3).map(v => v.value).filter(Boolean);
219+
if (ctas.length) dos.push(`Use \`${ctas.join('\`, \`')}\` as the primary verbs in CTAs — these dominate the source.`);
220+
if (voice.headingStyle) dos.push(`Write headings in **${voice.headingStyle}** case, **${voice.headingLengthClass || 'balanced'}** length.`);
221+
if (voice.pronoun && voice.pronoun !== 'neutral') dos.push(`Address the reader with the pronoun posture **${voice.pronoun}**.`);
222+
if (design.materialLanguage?.label) dos.push(`Stay inside the **${design.materialLanguage.label}** material — match shadow and radius habits.`);
223+
if (!dos.length) dos.push('_No strong directional signals captured._');
224+
for (const d of dos) lines.push(`- ${d}`);
225+
226+
lines.push('');
227+
lines.push("**Don'ts**");
228+
const donts = [];
229+
if (a11y.failCount > 0) donts.push(`Don't ship copy on the colors flagged in accessibility — ${a11y.failCount} contrast pair(s) fail WCAG AA on the source itself.`);
230+
if (score.issues?.length) for (const i of score.issues.slice(0, 4)) donts.push(`Don't ${i.toLowerCase().replace(/^./, c => c.toLowerCase())}.`);
231+
if (!donts.length) donts.push("_No anti-patterns surfaced. Don't invent new tokens — reuse the scale above._");
232+
for (const d of donts) lines.push(`- ${d}`);
233+
234+
return lines.join('\n');
235+
}
236+
237+
// ─── YAML front matter ─────────────────────────────────────────
238+
239+
function frontMatter(design, version) {
240+
const url = design.meta?.url || '';
241+
const title = design.meta?.title || '';
242+
const c = design.colors || {};
243+
const t = design.typography || {};
244+
const sp = design.spacing || {};
245+
const r = design.borders?.radii || [];
246+
const sh = design.shadows?.values || [];
247+
248+
const lines = ['---'];
249+
lines.push(`site: ${yamlString(title || url)}`);
250+
if (url) lines.push(`url: ${yamlString(url)}`);
251+
lines.push(`generated_at: ${yamlString(new Date().toISOString())}`);
252+
lines.push(`generator: ${yamlString(`designlang@${version}`)}`);
253+
if (design.pageIntent?.type) lines.push(`intent: ${yamlString(design.pageIntent.type)}`);
254+
if (design.materialLanguage?.label) lines.push(`material: ${yamlString(design.materialLanguage.label)}`);
255+
if (design.componentLibrary?.library && design.componentLibrary.library !== 'unknown') {
256+
lines.push(`library: ${yamlString(design.componentLibrary.library)}`);
257+
}
258+
259+
lines.push('tokens:');
260+
lines.push(' colors:' + yamlMap({
261+
primary: topColor(c, 'primary'),
262+
secondary: topColor(c, 'secondary'),
263+
accent: topColor(c, 'accent'),
264+
background: c.backgrounds?.[0],
265+
foreground: c.text?.[0],
266+
}, ' '));
267+
lines.push(' typography:' + yamlMap({
268+
sans: t.families?.[0]?.name,
269+
mono: t.families?.find(f => /mono/i.test(f.name))?.name,
270+
base: t.body?.size,
271+
}, ' '));
272+
lines.push(' spacing:' + yamlMap({
273+
base: sp.base,
274+
scale: sp.scale?.length ? '[' + sp.scale.slice(0, 10).map(s => (s.value ?? s)).join(', ') + ']' : null,
275+
}, ' '));
276+
if (r.length) {
277+
lines.push(' radii:' + yamlMap(Object.fromEntries(r.slice(0, 6).map(x => [x.label || `r${x.value}`, x.value])), ' '));
278+
}
279+
if (sh.length) {
280+
lines.push(' shadows:' + yamlMap(Object.fromEntries(sh.slice(0, 4).map(x => [x.label || 'shadow', x.raw || x.value])), ' '));
281+
}
282+
lines.push('---');
283+
return lines.join('\n');
284+
}
285+
286+
// ─── Main ──────────────────────────────────────────────────────
287+
288+
let CACHED_VERSION = null;
289+
function pkgVersion() {
290+
if (CACHED_VERSION) return CACHED_VERSION;
291+
try {
292+
const url = new URL('../../package.json', import.meta.url);
293+
CACHED_VERSION = JSON.parse(readFileSync(url, 'utf-8')).version;
294+
} catch { CACHED_VERSION = '0.0.0'; }
295+
return CACHED_VERSION;
296+
}
297+
298+
export function formatDesignMd(design) {
299+
const version = pkgVersion();
300+
const fm = frontMatter(design, version);
301+
302+
const sections = [
303+
['Overview', sectionOverview(design)],
304+
['Colors', sectionColors(design)],
305+
['Typography', sectionTypography(design)],
306+
['Layout', sectionLayout(design)],
307+
['Elevation and Depth', sectionElevation(design)],
308+
['Shapes', sectionShapes(design)],
309+
['Components', sectionComponents(design)],
310+
["Do's and Don'ts", sectionDosDonts(design)],
311+
];
312+
313+
const body = sections.map(([h, b]) => `# ${h}\n\n${b || '_—_'}\n`).join('\n');
314+
315+
const sourceUrl = design.meta?.url || '';
316+
const footer = `\n---\n_Generated by [designlang](https://github.com/Manavarya09/design-extract) v${version} from <${sourceUrl}>._\n_Compatible with the DESIGN.md convention pioneered by [design-extractor.com](https://www.design-extractor.com) — extended with intent, material, voice, anatomy, and library detection._\n`;
317+
318+
return `${fm}\n\n${body}${footer}`;
319+
}

website/app/page.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default function Home() {
4141
<a href="#features" style={{ borderBottom: 0 }}>Features</a>
4242
<a href="#specimens" style={{ borderBottom: 0 }}>Specimens</a>
4343
<a href="#install" style={{ borderBottom: 0 }}>Install</a>
44+
<a href="/vs/design-extractor" style={{ borderBottom: 0, color: 'var(--accent)' }}>vs</a>
4445
<a href="https://github.com/Manavarya09/design-extract" style={{ borderBottom: 0 }}>GitHub</a>
4546
<a href="https://www.npmjs.com/package/designlang" style={{ borderBottom: 0 }}>npm</a>
4647
</nav>

0 commit comments

Comments
 (0)