designlang 9.0.0 → 10.1.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 +11 -0
- package/CHANGELOG.md +62 -0
- package/README.md +16 -1
- package/bin/design-extract.js +107 -1
- package/package.json +2 -2
- package/src/classifiers/smart.js +130 -0
- package/src/extractors/component-library.js +193 -0
- package/src/extractors/component-screenshots.js +161 -0
- package/src/extractors/imagery-style.js +131 -0
- package/src/extractors/logo.js +142 -0
- package/src/extractors/material-language.js +152 -0
- package/src/extractors/page-intent.js +172 -0
- package/src/extractors/section-roles.js +135 -0
- package/src/formatters/markdown.js +109 -0
- package/src/formatters/prompt-pack.js +214 -0
- package/src/index.js +27 -0
- package/src/multipage.js +233 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Classify a crawled URL into a canonical page type. Heuristic-only by default
|
|
2
|
+
// (zero deps, deterministic). Returns { type, confidence, signals, alternates }.
|
|
3
|
+
// Consumers can fall back to the optional --smart LLM pass when confidence is low.
|
|
4
|
+
|
|
5
|
+
const TYPES = [
|
|
6
|
+
'landing', 'pricing', 'docs', 'blog', 'blog-post',
|
|
7
|
+
'product', 'about', 'dashboard', 'auth', 'legal', 'unknown',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const URL_RULES = [
|
|
11
|
+
{ re: /\/pricing(\/|$)/i, type: 'pricing', weight: 0.9 },
|
|
12
|
+
{ re: /\/plans?(\/|$)/i, type: 'pricing', weight: 0.75 },
|
|
13
|
+
{ re: /\/docs?(\/|$)|\/documentation|\/guide/i, type: 'docs', weight: 0.9 },
|
|
14
|
+
{ re: /\/api-reference|\/reference(\/|$)/i, type: 'docs', weight: 0.85 },
|
|
15
|
+
{ re: /\/blog(\/[\w-]+)+/i, type: 'blog-post', weight: 0.9 },
|
|
16
|
+
{ re: /\/blog(\/|$)/i, type: 'blog', weight: 0.85 },
|
|
17
|
+
{ re: /\/changelog(\/|$)/i, type: 'blog', weight: 0.6 },
|
|
18
|
+
{ re: /\/about(\/|$)|\/company|\/team/i, type: 'about', weight: 0.85 },
|
|
19
|
+
{ re: /\/product(\/|$)|\/products\//i, type: 'product', weight: 0.75 },
|
|
20
|
+
{ re: /\/features?(\/|$)|\/solutions?(\/|$)/i, type: 'product', weight: 0.7 },
|
|
21
|
+
{ re: /\/login|\/signin|\/sign-in|\/signup|\/sign-up|\/register/i, type: 'auth', weight: 0.95 },
|
|
22
|
+
{ re: /\/terms|\/privacy|\/legal|\/cookie-policy/i, type: 'legal', weight: 0.95 },
|
|
23
|
+
{ re: /\/app(\/|$)|\/dashboard|\/console|\/admin/i, type: 'dashboard', weight: 0.8 },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const PRICING_TEXT = /(\$\s?\d|€\s?\d|£\s?\d|₹\s?\d)|\b(per\s?(month|user|seat)|\/mo\b|\/month|\/year|\/yr|billed (annually|monthly)|free (forever|plan|tier)|start (free|trial))\b/i;
|
|
27
|
+
const DOCS_TEXT = /\b(installation|getting started|api reference|parameters|return value|npm install|yarn add|pnpm add|`npx |import \{|quickstart)\b/i;
|
|
28
|
+
const BLOG_POST_TEXT = /\b(by\s+[A-Z][a-z]+\s+[A-Z][a-z]+|posted on|published (on|in)|min read|\d+\s+min read)\b/i;
|
|
29
|
+
const LEGAL_TEXT = /\b(privacy policy|terms of service|terms of use|cookie policy|data protection|gdpr|ccpa|effective date|last updated)\b/i;
|
|
30
|
+
const AUTH_TEXT = /\b(sign in|log in|create (an )?account|forgot password|email address|password)\b/i;
|
|
31
|
+
|
|
32
|
+
function rootPath(url) {
|
|
33
|
+
try {
|
|
34
|
+
const u = new URL(url);
|
|
35
|
+
return (u.pathname || '/').replace(/\/+$/, '') || '/';
|
|
36
|
+
} catch { return '/'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function countFormFields(sections) {
|
|
40
|
+
// sections text doesn't include input tags, so we approximate via headings + text.
|
|
41
|
+
return sections.reduce((n, s) => n + (s.buttonCount || 0), 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectDocsLayout(sections) {
|
|
45
|
+
// Docs pages often have a sidebar nav + long-form article.
|
|
46
|
+
const hasSidebar = sections.some(s =>
|
|
47
|
+
(s.tag === 'aside') || /sidebar|toc|nav-?docs|left-?nav/i.test(s.className || '')
|
|
48
|
+
);
|
|
49
|
+
const longArticle = sections.find(s => s.tag === 'main' || s.tag === 'section');
|
|
50
|
+
const articleLen = longArticle ? (longArticle.text || '').length : 0;
|
|
51
|
+
return { hasSidebar, articleLen };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectPricingLayout(sections) {
|
|
55
|
+
// 2-4 similarly-sized cards with currency signals = pricing table.
|
|
56
|
+
for (const s of sections) {
|
|
57
|
+
if (!PRICING_TEXT.test(s.text || '')) continue;
|
|
58
|
+
if ((s.cardCount || 0) >= 2 && (s.cardCount || 0) <= 6) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function extractPageIntent(rawData = {}, opts = {}) {
|
|
64
|
+
const url = opts.url || rawData.url || '';
|
|
65
|
+
const path = rootPath(url);
|
|
66
|
+
const title = (opts.title || rawData.title || '').toLowerCase();
|
|
67
|
+
const sections = (rawData.light && rawData.light.sections) || rawData.sections || [];
|
|
68
|
+
const metas = ((rawData.light && rawData.light.stack && rawData.light.stack.metas) || []).map(m => ({
|
|
69
|
+
name: (m.name || '').toLowerCase(),
|
|
70
|
+
content: (m.content || ''),
|
|
71
|
+
}));
|
|
72
|
+
const description = (metas.find(m => m.name === 'description') || {}).content || '';
|
|
73
|
+
const ogType = (metas.find(m => m.name === 'og:type') || {}).content || '';
|
|
74
|
+
|
|
75
|
+
const scores = Object.fromEntries(TYPES.map(t => [t, 0]));
|
|
76
|
+
const signals = [];
|
|
77
|
+
|
|
78
|
+
// 1) URL rules (strongest signal).
|
|
79
|
+
for (const rule of URL_RULES) {
|
|
80
|
+
if (rule.re.test(path)) {
|
|
81
|
+
scores[rule.type] += rule.weight;
|
|
82
|
+
signals.push({ kind: 'url', type: rule.type, weight: rule.weight });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2) og:type.
|
|
87
|
+
if (ogType === 'article') {
|
|
88
|
+
scores['blog-post'] += 0.6;
|
|
89
|
+
signals.push({ kind: 'meta', type: 'blog-post', weight: 0.6, detail: 'og:type=article' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3) Title keywords.
|
|
93
|
+
if (/\bpricing\b|\bplans?\b/.test(title)) { scores.pricing += 0.4; signals.push({ kind: 'title', type: 'pricing', weight: 0.4 }); }
|
|
94
|
+
if (/\bdocs?\b|documentation|guide/.test(title)) { scores.docs += 0.4; signals.push({ kind: 'title', type: 'docs', weight: 0.4 }); }
|
|
95
|
+
if (/\bblog\b/.test(title)) { scores.blog += 0.3; signals.push({ kind: 'title', type: 'blog', weight: 0.3 }); }
|
|
96
|
+
if (/\bprivacy|\bterms\b/.test(title)) { scores.legal += 0.5; signals.push({ kind: 'title', type: 'legal', weight: 0.5 }); }
|
|
97
|
+
if (/\bsign.?in|\blog.?in|\bsign.?up\b/.test(title)) { scores.auth += 0.5; signals.push({ kind: 'title', type: 'auth', weight: 0.5 }); }
|
|
98
|
+
|
|
99
|
+
// 4) DOM text signals.
|
|
100
|
+
const bigText = sections.map(s => s.text || '').join('\n').slice(0, 8000);
|
|
101
|
+
if (PRICING_TEXT.test(bigText) && detectPricingLayout(sections)) {
|
|
102
|
+
scores.pricing += 0.6;
|
|
103
|
+
signals.push({ kind: 'dom', type: 'pricing', weight: 0.6, detail: 'currency+card-grid' });
|
|
104
|
+
}
|
|
105
|
+
if (DOCS_TEXT.test(bigText)) {
|
|
106
|
+
const { hasSidebar, articleLen } = detectDocsLayout(sections);
|
|
107
|
+
const w = 0.3 + (hasSidebar ? 0.25 : 0) + (articleLen > 1500 ? 0.15 : 0);
|
|
108
|
+
scores.docs += w;
|
|
109
|
+
signals.push({ kind: 'dom', type: 'docs', weight: w, detail: `sidebar=${hasSidebar} article=${articleLen}` });
|
|
110
|
+
}
|
|
111
|
+
if (BLOG_POST_TEXT.test(bigText)) {
|
|
112
|
+
scores['blog-post'] += 0.35;
|
|
113
|
+
signals.push({ kind: 'dom', type: 'blog-post', weight: 0.35, detail: 'byline|min-read' });
|
|
114
|
+
}
|
|
115
|
+
if (LEGAL_TEXT.test(bigText)) {
|
|
116
|
+
scores.legal += 0.4;
|
|
117
|
+
signals.push({ kind: 'dom', type: 'legal', weight: 0.4 });
|
|
118
|
+
}
|
|
119
|
+
if (AUTH_TEXT.test(bigText) && countFormFields(sections) < 8 && bigText.length < 3000) {
|
|
120
|
+
scores.auth += 0.35;
|
|
121
|
+
signals.push({ kind: 'dom', type: 'auth', weight: 0.35, detail: 'auth-form-shape' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5) Path="/" fallback → landing (weak prior).
|
|
125
|
+
if (path === '/' || path === '') {
|
|
126
|
+
scores.landing += 0.45;
|
|
127
|
+
signals.push({ kind: 'url', type: 'landing', weight: 0.45, detail: 'root-path' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 6) Generic "has hero + features + cta" shape → landing.
|
|
131
|
+
const roles = new Set();
|
|
132
|
+
for (const s of sections) {
|
|
133
|
+
const blob = `${s.className || ''} ${s.id || ''}`.toLowerCase();
|
|
134
|
+
if (/hero/.test(blob)) roles.add('hero');
|
|
135
|
+
if ((s.cardCount || 0) >= 3) roles.add('features');
|
|
136
|
+
if (/cta|get-?started/.test(blob)) roles.add('cta');
|
|
137
|
+
if (s.tag === 'footer') roles.add('footer');
|
|
138
|
+
}
|
|
139
|
+
if (roles.has('hero') && (roles.has('features') || roles.has('cta'))) {
|
|
140
|
+
scores.landing += 0.3;
|
|
141
|
+
signals.push({ kind: 'shape', type: 'landing', weight: 0.3, detail: [...roles].join('+') });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pick winner.
|
|
145
|
+
const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
146
|
+
const [winType, winScore] = ranked[0];
|
|
147
|
+
const [, secondScore] = ranked[1] || ['unknown', 0];
|
|
148
|
+
const margin = winScore - secondScore;
|
|
149
|
+
let confidence = 0;
|
|
150
|
+
if (winScore > 0) {
|
|
151
|
+
confidence = Math.min(1, winScore * 0.6 + margin * 0.4);
|
|
152
|
+
}
|
|
153
|
+
const type = winScore === 0 ? 'unknown' : winType;
|
|
154
|
+
|
|
155
|
+
const alternates = ranked
|
|
156
|
+
.filter(([, s]) => s > 0 && s !== winScore)
|
|
157
|
+
.slice(0, 3)
|
|
158
|
+
.map(([t, s]) => ({ type: t, score: Number(s.toFixed(3)) }));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
type,
|
|
162
|
+
confidence: Number(confidence.toFixed(3)),
|
|
163
|
+
path,
|
|
164
|
+
title: opts.title || rawData.title || '',
|
|
165
|
+
description: description.slice(0, 200),
|
|
166
|
+
signals: signals.slice(0, 20),
|
|
167
|
+
alternates,
|
|
168
|
+
needsSmart: confidence < 0.6,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const PAGE_TYPES = TYPES;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Attach richer semantic roles to the sections that semantic-regions.js already
|
|
2
|
+
// found. v9 labels were coarse (hero/features/pricing/testimonials/cta/footer/nav);
|
|
3
|
+
// v10 adds logo-wall, stats, faq, comparison, steps, gallery, bento, blog-grid.
|
|
4
|
+
// Input: enriched sections (from crawler) + existing `regions` array. Output: a
|
|
5
|
+
// parallel array with { role, subrole, confidence, slots }.
|
|
6
|
+
|
|
7
|
+
const CTA_RE = /\b(get started|sign ?up|try (free|it|now)|start (free|trial|now)|book a demo|request demo|contact sales|talk to sales|learn more|watch demo)\b/i;
|
|
8
|
+
const STATS_RE = /\b(\d[\d,.]*[+%]|\d+x\b|\$\d+[MBK]\b)/i;
|
|
9
|
+
const FAQ_RE = /\b(frequently asked|faq|common questions|questions & answers)\b/i;
|
|
10
|
+
const STEPS_RE = /\b(step\s?\d|how it works|\d\s*\.\s+[A-Z])/i;
|
|
11
|
+
const COMPARE_RE = /\b(vs\.?|compared to|free vs|basic vs|enterprise vs)\b/i;
|
|
12
|
+
const TESTIMONIAL_RE = /(".{20,}"|".{20,}"|—\s?[A-Z][a-z]+\s+[A-Z][a-z]+|ceo|founder|head of)/i;
|
|
13
|
+
const PRICING_RE = /(\$\s?\d|€\s?\d|£\s?\d|per\s?(month|user|seat)|\/mo\b|\/month|billed)/i;
|
|
14
|
+
|
|
15
|
+
function blob(s) {
|
|
16
|
+
return `${s.className || ''} ${s.id || ''}`.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function detectLogoWall(s) {
|
|
20
|
+
// Logo walls: many small images in one row, minimal text.
|
|
21
|
+
// The enriched sections don't carry image counts directly — we proxy via
|
|
22
|
+
// cardCount with very short text.
|
|
23
|
+
if ((s.cardCount || 0) >= 5 && (s.text || '').length < 300 &&
|
|
24
|
+
/(trusted by|used by|customers|as seen in|logos?|partners|clients)/i.test(s.text || '' + ' ' + blob(s))) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detectBento(s) {
|
|
31
|
+
const b = blob(s);
|
|
32
|
+
if (/bento/.test(b)) return true;
|
|
33
|
+
// Bento hint: 4-8 cards with irregular sizes — we can't measure layout grid
|
|
34
|
+
// here, so fall back to class name + cardCount heuristic.
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function classifyRole(s, existingRole, pageType) {
|
|
39
|
+
const text = (s.text || '').slice(0, 2000);
|
|
40
|
+
const b = blob(s);
|
|
41
|
+
const headings = s.headings || [];
|
|
42
|
+
const h1 = (headings[0] || '').toLowerCase();
|
|
43
|
+
|
|
44
|
+
// Landmarks come first.
|
|
45
|
+
if (s.tag === 'footer') return { role: 'footer', confidence: 0.95 };
|
|
46
|
+
if (s.tag === 'nav' || /^nav|header-nav|top-?bar/.test(b)) return { role: 'nav', confidence: 0.9 };
|
|
47
|
+
|
|
48
|
+
// Strong class-based hints.
|
|
49
|
+
if (/logo-?(wall|cloud|grid|strip)|trusted-?by/.test(b) || detectLogoWall(s)) {
|
|
50
|
+
return { role: 'logo-wall', confidence: 0.85 };
|
|
51
|
+
}
|
|
52
|
+
if (/bento/.test(b) || detectBento(s)) return { role: 'bento', subrole: 'features', confidence: 0.75 };
|
|
53
|
+
if (/gallery|carousel|slider/.test(b)) return { role: 'gallery', confidence: 0.7 };
|
|
54
|
+
if (/stat(s|istic)|metric|number/.test(b) && STATS_RE.test(text)) return { role: 'stats', confidence: 0.85 };
|
|
55
|
+
if (FAQ_RE.test(text) || /\bfaq\b/.test(b)) return { role: 'faq', confidence: 0.85 };
|
|
56
|
+
if (STEPS_RE.test(text) && (s.cardCount || 0) >= 3) return { role: 'steps', confidence: 0.75 };
|
|
57
|
+
if (COMPARE_RE.test(text) && (s.cardCount || 0) >= 2) return { role: 'comparison', confidence: 0.7 };
|
|
58
|
+
if (PRICING_RE.test(text) && (s.cardCount || 0) >= 2) return { role: 'pricing-table', confidence: 0.9 };
|
|
59
|
+
|
|
60
|
+
// Testimonials vs hero vs features — existing classifier handled most of these;
|
|
61
|
+
// re-run with tighter signals.
|
|
62
|
+
if (TESTIMONIAL_RE.test(text) || /testimonial|review|quote/.test(b)) {
|
|
63
|
+
return { role: 'testimonial', confidence: 0.8 };
|
|
64
|
+
}
|
|
65
|
+
if (/hero/.test(b) || (headings.length === 1 && (s.buttonCount || 0) >= 1 && s.bounds && s.bounds.h > 300 && s.bounds.y < 400)) {
|
|
66
|
+
return { role: 'hero', confidence: 0.85 };
|
|
67
|
+
}
|
|
68
|
+
if ((s.cardCount || 0) >= 3 && (/(feature|benefit|what you get|why )/i.test(text) || /feature|grid/.test(b))) {
|
|
69
|
+
return { role: 'feature-grid', confidence: 0.8 };
|
|
70
|
+
}
|
|
71
|
+
if (CTA_RE.test(text) && (s.buttonCount || 0) >= 1 && text.length < 600) {
|
|
72
|
+
return { role: 'cta', confidence: 0.75 };
|
|
73
|
+
}
|
|
74
|
+
if (pageType === 'blog' && (s.cardCount || 0) >= 3) {
|
|
75
|
+
return { role: 'blog-grid', confidence: 0.75 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fall back to the coarse existing label if we have one.
|
|
79
|
+
if (existingRole && existingRole !== 'content') {
|
|
80
|
+
return { role: existingRole, confidence: 0.4 };
|
|
81
|
+
}
|
|
82
|
+
return { role: 'content', confidence: 0.3 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractSlots(s) {
|
|
86
|
+
// Lightweight slot detection from headings + button count. Real DOM-walking
|
|
87
|
+
// slot extraction happens in component-anatomy.js — here we just record
|
|
88
|
+
// rendered copy so prompts can quote the actual words.
|
|
89
|
+
const slots = {};
|
|
90
|
+
if ((s.headings || []).length) slots.heading = s.headings[0];
|
|
91
|
+
if ((s.headings || []).length > 1) slots.subheadings = s.headings.slice(1);
|
|
92
|
+
if (s.buttonCount > 0) slots.ctaCount = s.buttonCount;
|
|
93
|
+
const text = s.text || '';
|
|
94
|
+
const firstPara = text.split(/\n{2,}/)[0];
|
|
95
|
+
if (firstPara && firstPara.length < 400 && firstPara !== slots.heading) {
|
|
96
|
+
slots.lede = firstPara.trim();
|
|
97
|
+
}
|
|
98
|
+
return slots;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function extractSectionRoles(sections = [], regions = [], pageIntent = null) {
|
|
102
|
+
const pageType = pageIntent && pageIntent.type;
|
|
103
|
+
const labeled = sections.map((s, i) => {
|
|
104
|
+
const existing = regions[i] ? regions[i].role : null;
|
|
105
|
+
const classified = classifyRole(s, existing, pageType);
|
|
106
|
+
return {
|
|
107
|
+
index: i,
|
|
108
|
+
tag: s.tag,
|
|
109
|
+
role: classified.role,
|
|
110
|
+
subrole: classified.subrole || null,
|
|
111
|
+
confidence: Number((classified.confidence || 0).toFixed(3)),
|
|
112
|
+
heading: (s.headings && s.headings[0]) || null,
|
|
113
|
+
bounds: s.bounds,
|
|
114
|
+
buttonCount: s.buttonCount || 0,
|
|
115
|
+
cardCount: s.cardCount || 0,
|
|
116
|
+
slots: extractSlots(s),
|
|
117
|
+
needsSmart: (classified.confidence || 0) < 0.5,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Reading-order + emphasis summary.
|
|
122
|
+
const byRole = {};
|
|
123
|
+
for (const r of labeled) {
|
|
124
|
+
byRole[r.role] = (byRole[r.role] || 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
sections: labeled,
|
|
129
|
+
counts: byRole,
|
|
130
|
+
readingOrder: labeled
|
|
131
|
+
.filter(r => r.bounds)
|
|
132
|
+
.sort((a, b) => (a.bounds?.y || 0) - (b.bounds?.y || 0))
|
|
133
|
+
.map(r => r.role),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -711,6 +711,115 @@ export function formatMarkdown(design) {
|
|
|
711
711
|
}
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
+
// ── v10: Page Intent ──
|
|
715
|
+
if (design.pageIntent && design.pageIntent.type) {
|
|
716
|
+
lines.push('## Page Intent');
|
|
717
|
+
lines.push('');
|
|
718
|
+
lines.push(`**Type:** \`${design.pageIntent.type}\` (confidence ${design.pageIntent.confidence})`);
|
|
719
|
+
if (design.pageIntent.description) lines.push(`**Description:** ${design.pageIntent.description}`);
|
|
720
|
+
if (design.pageIntent.alternates?.length) {
|
|
721
|
+
lines.push('');
|
|
722
|
+
lines.push('Alternates: ' + design.pageIntent.alternates.map(a => `${a.type} (${a.score})`).join(', '));
|
|
723
|
+
}
|
|
724
|
+
lines.push('');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ── v10: Section Roles ──
|
|
728
|
+
if (design.sectionRoles && design.sectionRoles.sections?.length) {
|
|
729
|
+
lines.push('## Section Roles');
|
|
730
|
+
lines.push('');
|
|
731
|
+
lines.push('Reading order (top→bottom): ' + (design.sectionRoles.readingOrder || []).join(' → '));
|
|
732
|
+
lines.push('');
|
|
733
|
+
lines.push('| # | Role | Heading | Confidence |');
|
|
734
|
+
lines.push('|---|------|---------|------------|');
|
|
735
|
+
for (const s of design.sectionRoles.sections.slice(0, 20)) {
|
|
736
|
+
const h = (s.heading || '').replace(/\|/g, '\\|').slice(0, 80);
|
|
737
|
+
lines.push(`| ${s.index} | ${s.role}${s.subrole ? ` · ${s.subrole}` : ''} | ${h || '—'} | ${s.confidence} |`);
|
|
738
|
+
}
|
|
739
|
+
lines.push('');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ── v10: Material Language ──
|
|
743
|
+
if (design.materialLanguage && design.materialLanguage.label) {
|
|
744
|
+
lines.push('## Material Language');
|
|
745
|
+
lines.push('');
|
|
746
|
+
lines.push(`**Label:** \`${design.materialLanguage.label}\` (confidence ${design.materialLanguage.confidence})`);
|
|
747
|
+
const m = design.materialLanguage.metrics || {};
|
|
748
|
+
lines.push('');
|
|
749
|
+
lines.push('| Metric | Value |');
|
|
750
|
+
lines.push('|--------|-------|');
|
|
751
|
+
if (m.saturation != null) lines.push(`| Avg saturation | ${m.saturation} |`);
|
|
752
|
+
if (m.shadowProfile) lines.push(`| Shadow profile | ${m.shadowProfile} |`);
|
|
753
|
+
if (m.avgShadowBlur != null) lines.push(`| Avg shadow blur | ${m.avgShadowBlur}px |`);
|
|
754
|
+
if (m.maxRadius != null) lines.push(`| Max radius | ${m.maxRadius}px |`);
|
|
755
|
+
if (m.hasBackdropBlur != null) lines.push(`| backdrop-filter in use | ${m.hasBackdropBlur ? 'yes' : 'no'} |`);
|
|
756
|
+
if (m.gradientCount != null) lines.push(`| Gradients | ${m.gradientCount} |`);
|
|
757
|
+
lines.push('');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── v10: Imagery Style ──
|
|
761
|
+
if (design.imageryStyle && design.imageryStyle.label && design.imageryStyle.label !== 'none') {
|
|
762
|
+
lines.push('## Imagery Style');
|
|
763
|
+
lines.push('');
|
|
764
|
+
lines.push(`**Label:** \`${design.imageryStyle.label}\` (confidence ${design.imageryStyle.confidence})`);
|
|
765
|
+
const c = design.imageryStyle.counts || {};
|
|
766
|
+
lines.push(`**Counts:** total ${c.total || 0}, svg ${c.svg || 0}, icon ${c.icon || 0}, screenshot-like ${c.screenshot || 0}, photo-like ${c.photoLike || 0}`);
|
|
767
|
+
if (design.imageryStyle.dominantAspect) lines.push(`**Dominant aspect:** ${design.imageryStyle.dominantAspect}`);
|
|
768
|
+
if (design.imageryStyle.radiusProfile) lines.push(`**Radius profile on images:** ${design.imageryStyle.radiusProfile}`);
|
|
769
|
+
lines.push('');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ── v10: Component Library ──
|
|
773
|
+
if (design.componentLibrary && design.componentLibrary.library && design.componentLibrary.library !== 'unknown') {
|
|
774
|
+
lines.push('## Component Library');
|
|
775
|
+
lines.push('');
|
|
776
|
+
lines.push(`**Detected:** \`${design.componentLibrary.library}\` (confidence ${design.componentLibrary.confidence})`);
|
|
777
|
+
if ((design.componentLibrary.evidence || []).length) {
|
|
778
|
+
lines.push('');
|
|
779
|
+
lines.push('Evidence:');
|
|
780
|
+
for (const e of design.componentLibrary.evidence) lines.push(`- ${e}`);
|
|
781
|
+
}
|
|
782
|
+
if ((design.componentLibrary.alternates || []).length) {
|
|
783
|
+
lines.push('');
|
|
784
|
+
lines.push('Also considered: ' + design.componentLibrary.alternates.map(a => `${a.id} (${a.score})`).join(', '));
|
|
785
|
+
}
|
|
786
|
+
lines.push('');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ── v10: Multi-Page Map ──
|
|
790
|
+
if (design.multiPage && Array.isArray(design.multiPage.pages) && design.multiPage.pages.length) {
|
|
791
|
+
lines.push('## Multi-Page Map');
|
|
792
|
+
lines.push('');
|
|
793
|
+
lines.push('| Page Type | URL | Status |');
|
|
794
|
+
lines.push('|-----------|-----|--------|');
|
|
795
|
+
for (const p of design.multiPage.pages) {
|
|
796
|
+
lines.push(`| ${p.type || '—'} | ${p.url} | ${p.error ? 'error' : 'ok'} |`);
|
|
797
|
+
}
|
|
798
|
+
lines.push('');
|
|
799
|
+
if (design.multiPage.consistency?.shared?.colors?.length) {
|
|
800
|
+
lines.push(`**Shared colors across pages:** ${design.multiPage.consistency.shared.colors.slice(0, 10).map(c => `\`${c}\``).join(', ')}`);
|
|
801
|
+
lines.push('');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ── v10.1: Component Screenshots ──
|
|
806
|
+
if (design.componentScreenshots && Array.isArray(design.componentScreenshots.components) && design.componentScreenshots.components.length) {
|
|
807
|
+
lines.push('## Component Screenshots');
|
|
808
|
+
lines.push('');
|
|
809
|
+
lines.push(`${design.componentScreenshots.components.length} retina crops written to \`screenshots/\`. Index: \`*-screenshots.json\`.`);
|
|
810
|
+
lines.push('');
|
|
811
|
+
lines.push('| Cluster | Variant | Size (px) | File |');
|
|
812
|
+
lines.push('|---------|---------|-----------|------|');
|
|
813
|
+
for (const c of design.componentScreenshots.components.slice(0, 20)) {
|
|
814
|
+
lines.push(`| ${c.cluster} | ${c.variant} | ${c.bounds?.w || '?'} × ${c.bounds?.h || '?'} | \`${c.path}\` |`);
|
|
815
|
+
}
|
|
816
|
+
if (design.componentScreenshots.fullPage) {
|
|
817
|
+
lines.push('');
|
|
818
|
+
lines.push(`Full-page: \`${design.componentScreenshots.fullPage.path}\``);
|
|
819
|
+
}
|
|
820
|
+
lines.push('');
|
|
821
|
+
}
|
|
822
|
+
|
|
714
823
|
// ── Quick Start ──
|
|
715
824
|
lines.push('## Quick Start');
|
|
716
825
|
lines.push('');
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Generates ready-to-paste prompts for v0, Lovable, Cursor, and Claude
|
|
2
|
+
// Artifacts — plus atomic per-component "recipe cards". The point is to get an
|
|
3
|
+
// LLM to reproduce a site's look without the user having to hand-translate the
|
|
4
|
+
// extracted JSON. We inline the tokens, voice, section order, and library
|
|
5
|
+
// guidance so the model has everything it needs in one message.
|
|
6
|
+
|
|
7
|
+
function colorList(design) {
|
|
8
|
+
const all = (design.colors?.all || []).map(c => c.hex).filter(Boolean);
|
|
9
|
+
return [...new Set(all)].slice(0, 14);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function typeFamilies(design) {
|
|
13
|
+
return [...new Set((design.typography?.families || []).filter(Boolean))].slice(0, 4);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scaleSnippet(scale = []) {
|
|
17
|
+
if (!scale.length) return '(not detected)';
|
|
18
|
+
return scale.slice(0, 8).map(s => (s.value || s) + (s.label ? ` (${s.label})` : '')).join(', ');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function radiiSnippet(borders) {
|
|
22
|
+
const r = (borders?.radii || []).slice(0, 6).map(x => (x.value || x).toString());
|
|
23
|
+
return r.length ? r.join(', ') : '(none)';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shadowSnippet(shadows) {
|
|
27
|
+
const s = (shadows?.values || []).slice(0, 3).map(x => (x.value || x).toString());
|
|
28
|
+
return s.length ? s.join(' | ') : '(none)';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function librarySuggest(library) {
|
|
32
|
+
if (!library || library.library === 'unknown') return null;
|
|
33
|
+
const map = {
|
|
34
|
+
'shadcn/ui': 'Use shadcn/ui components (Button, Card, Dialog, Input, Sheet, Tabs). Pair with Tailwind.',
|
|
35
|
+
'radix-ui': 'Use Radix UI primitives for accessibility. Style with your preferred CSS solution.',
|
|
36
|
+
'headlessui': 'Use Headless UI primitives styled with Tailwind.',
|
|
37
|
+
'mui': 'Use MUI v5 components with a custom ThemeProvider matching the tokens below.',
|
|
38
|
+
'chakra-ui': 'Use Chakra UI components with a custom extendTheme({}) block.',
|
|
39
|
+
'mantine': 'Use Mantine UI components with MantineProvider theme overrides.',
|
|
40
|
+
'ant-design': 'Use Ant Design v5 with ConfigProvider theme tokens.',
|
|
41
|
+
'bootstrap': 'Use Bootstrap 5 utility classes. Customize via CSS variables.',
|
|
42
|
+
'heroui': 'Use HeroUI/NextUI components.',
|
|
43
|
+
'tailwind-ui': 'Use Tailwind UI patterns — no component library runtime, just Tailwind classes.',
|
|
44
|
+
'tailwindcss': 'Use plain Tailwind CSS without a component library.',
|
|
45
|
+
'vuetify': 'Use Vuetify 3 components with a custom theme object.',
|
|
46
|
+
};
|
|
47
|
+
return map[library.library] || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sectionList(sectionRoles) {
|
|
51
|
+
if (!sectionRoles || !sectionRoles.sections) return [];
|
|
52
|
+
return sectionRoles.sections
|
|
53
|
+
.filter(s => s.role && s.role !== 'content' && s.role !== 'nav')
|
|
54
|
+
.map(s => {
|
|
55
|
+
const slot = s.slots?.heading ? ` — heading: "${s.slots.heading.slice(0, 80)}"` : '';
|
|
56
|
+
return `- ${s.role}${slot}`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function voiceBlock(voice) {
|
|
61
|
+
if (!voice) return '';
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (voice.tone) parts.push(`Tone: ${voice.tone}`);
|
|
64
|
+
if (voice.headingStyle) parts.push(`Headings: ${voice.headingStyle}`);
|
|
65
|
+
if (voice.pronounPosture) parts.push(`Pronoun posture: ${voice.pronounPosture}`);
|
|
66
|
+
if ((voice.ctaVerbs || []).length) parts.push(`CTA verbs: ${(voice.ctaVerbs || []).slice(0, 6).join(', ')}`);
|
|
67
|
+
return parts.join(' · ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function coreBrief(design, opts = {}) {
|
|
71
|
+
const colors = colorList(design);
|
|
72
|
+
const fonts = typeFamilies(design);
|
|
73
|
+
const spacing = scaleSnippet(design.spacing?.scale);
|
|
74
|
+
const radii = radiiSnippet(design.borders);
|
|
75
|
+
const shadows = shadowSnippet(design.shadows);
|
|
76
|
+
const material = design.materialLanguage?.label || 'flat';
|
|
77
|
+
const intent = design.pageIntent?.type || 'landing';
|
|
78
|
+
const sections = sectionList(design.sectionRoles);
|
|
79
|
+
const voice = voiceBlock(design.voice);
|
|
80
|
+
const lib = librarySuggest(design.componentLibrary);
|
|
81
|
+
return {
|
|
82
|
+
colors, fonts, spacing, radii, shadows, material, intent, sections, voice, lib,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatV0Prompt(design) {
|
|
87
|
+
const b = coreBrief(design);
|
|
88
|
+
return [
|
|
89
|
+
`Build a ${b.intent} page with this exact visual language.`,
|
|
90
|
+
'',
|
|
91
|
+
'COLORS:',
|
|
92
|
+
b.colors.map(c => ` ${c}`).join('\n'),
|
|
93
|
+
'',
|
|
94
|
+
`FONTS: ${b.fonts.join(', ') || 'system-ui'}`,
|
|
95
|
+
`SPACING: ${b.spacing}`,
|
|
96
|
+
`RADIUS: ${b.radii}`,
|
|
97
|
+
`SHADOWS: ${b.shadows}`,
|
|
98
|
+
`MATERIAL LANGUAGE: ${b.material}`,
|
|
99
|
+
b.voice ? `VOICE: ${b.voice}` : '',
|
|
100
|
+
b.lib ? `LIBRARY: ${b.lib}` : '',
|
|
101
|
+
'',
|
|
102
|
+
'SECTIONS (in order):',
|
|
103
|
+
(b.sections.length ? b.sections : ['- hero', '- features', '- cta', '- footer']).join('\n'),
|
|
104
|
+
'',
|
|
105
|
+
'Use Tailwind. Match these tokens exactly. Keep the material language consistent.',
|
|
106
|
+
].filter(Boolean).join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatLovablePrompt(design) {
|
|
110
|
+
const b = coreBrief(design);
|
|
111
|
+
return [
|
|
112
|
+
`Clone the design language of this ${b.intent} page and build a fresh equivalent.`,
|
|
113
|
+
'',
|
|
114
|
+
`Visual feel: ${b.material}. ${b.voice || ''}`,
|
|
115
|
+
`Primary palette: ${b.colors.slice(0, 5).join(', ')}.`,
|
|
116
|
+
`Typography: ${b.fonts.join(', ') || 'system-ui'}.`,
|
|
117
|
+
`Corner radius vocabulary: ${b.radii}.`,
|
|
118
|
+
`Shadow vocabulary: ${b.shadows}.`,
|
|
119
|
+
b.lib ? `Use: ${b.lib}` : '',
|
|
120
|
+
'',
|
|
121
|
+
'Page structure:',
|
|
122
|
+
(b.sections.length ? b.sections : ['- hero', '- features', '- social proof', '- cta', '- footer']).join('\n'),
|
|
123
|
+
].filter(Boolean).join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatCursorPrompt(design) {
|
|
127
|
+
const b = coreBrief(design);
|
|
128
|
+
return [
|
|
129
|
+
'# Design brief',
|
|
130
|
+
'',
|
|
131
|
+
`Page type: **${b.intent}**.`,
|
|
132
|
+
`Material language: **${b.material}**.`,
|
|
133
|
+
b.voice ? `Voice: ${b.voice}.` : '',
|
|
134
|
+
'',
|
|
135
|
+
'## Tokens',
|
|
136
|
+
'',
|
|
137
|
+
'```ts',
|
|
138
|
+
'export const tokens = {',
|
|
139
|
+
` colors: [${b.colors.map(c => `'${c}'`).join(', ')}],`,
|
|
140
|
+
` fonts: [${b.fonts.map(f => `'${f}'`).join(', ')}],`,
|
|
141
|
+
` radii: [${(design.borders?.radii || []).slice(0, 6).map(r => `'${(r.value || r)}'`).join(', ')}],`,
|
|
142
|
+
` shadows: [${(design.shadows?.values || []).slice(0, 3).map(s => `'${(s.value || s).replace(/'/g, "\\'")}'`).join(', ')}],`,
|
|
143
|
+
'};',
|
|
144
|
+
'```',
|
|
145
|
+
'',
|
|
146
|
+
'## Sections',
|
|
147
|
+
(b.sections.length ? b.sections : ['- hero', '- features', '- cta', '- footer']).join('\n'),
|
|
148
|
+
'',
|
|
149
|
+
b.lib ? `## Library\n\n${b.lib}` : '',
|
|
150
|
+
].filter(Boolean).join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function formatClaudeArtifactPrompt(design) {
|
|
154
|
+
const b = coreBrief(design);
|
|
155
|
+
return [
|
|
156
|
+
'Create a React artifact that reproduces this brand\'s design language.',
|
|
157
|
+
'',
|
|
158
|
+
`Page intent: ${b.intent}.`,
|
|
159
|
+
`Material language: ${b.material}.`,
|
|
160
|
+
b.voice ? `Voice: ${b.voice}.` : '',
|
|
161
|
+
b.lib ? `Library preference: ${b.lib}` : '',
|
|
162
|
+
'',
|
|
163
|
+
`Colors to use: ${b.colors.join(', ')}.`,
|
|
164
|
+
`Fonts: ${b.fonts.join(', ') || 'system-ui'}.`,
|
|
165
|
+
`Radius vocabulary: ${b.radii}.`,
|
|
166
|
+
'',
|
|
167
|
+
'Sections:',
|
|
168
|
+
(b.sections.length ? b.sections : ['- hero', '- features', '- cta', '- footer']).join('\n'),
|
|
169
|
+
'',
|
|
170
|
+
'Use Tailwind via CDN, lucide-react for icons, and keep the material language consistent across sections. Do not add extra decorative elements outside this vocabulary.',
|
|
171
|
+
].filter(Boolean).join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function formatRecipeCards(design) {
|
|
175
|
+
const clusters = design.componentClusters || [];
|
|
176
|
+
if (!clusters.length) return [];
|
|
177
|
+
const b = coreBrief(design);
|
|
178
|
+
return clusters.slice(0, 12).map((c, i) => {
|
|
179
|
+
const name = c.name || c.kind || `component-${i + 1}`;
|
|
180
|
+
const signals = [
|
|
181
|
+
b.lib,
|
|
182
|
+
`Radius: ${b.radii}`,
|
|
183
|
+
`Shadows: ${b.shadows}`,
|
|
184
|
+
].filter(Boolean).join(' · ');
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
content: [
|
|
188
|
+
`# Recipe: ${name}`,
|
|
189
|
+
'',
|
|
190
|
+
`Build one ${name} component that matches this brand.`,
|
|
191
|
+
'',
|
|
192
|
+
`Palette: ${b.colors.slice(0, 6).join(', ')}`,
|
|
193
|
+
`Typography: ${b.fonts.join(', ') || 'system-ui'}`,
|
|
194
|
+
`Material: ${b.material}`,
|
|
195
|
+
signals ? `Signals: ${signals}` : '',
|
|
196
|
+
'',
|
|
197
|
+
'## Anatomy (detected)',
|
|
198
|
+
'```json',
|
|
199
|
+
JSON.stringify(c, null, 2).slice(0, 1500),
|
|
200
|
+
'```',
|
|
201
|
+
].filter(Boolean).join('\n'),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildPromptPack(design) {
|
|
207
|
+
return {
|
|
208
|
+
'v0.txt': formatV0Prompt(design),
|
|
209
|
+
'lovable.txt': formatLovablePrompt(design),
|
|
210
|
+
'cursor.md': formatCursorPrompt(design),
|
|
211
|
+
'claude-artifacts.md': formatClaudeArtifactPrompt(design),
|
|
212
|
+
recipes: formatRecipeCards(design),
|
|
213
|
+
};
|
|
214
|
+
}
|