designlang 12.1.0 → 12.3.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/CHANGELOG.md +71 -0
- package/README.md +23 -6
- package/bin/design-extract.js +164 -2
- package/package.json +1 -1
- package/src/chat.js +0 -14
- package/src/formatters/badge.js +90 -0
- package/src/formatters/battle.js +338 -0
- package/src/formatters/css-vars.js +0 -2
- package/src/formatters/remix.js +379 -0
- package/src/formatters/tailwind.js +1 -1
- package/src/formatters/vue-theme.js +0 -2
- package/src/history.js +1 -1
- package/src/index.js +1 -2
- package/src/studio.js +2 -2
- package/src/sync.js +17 -6
- package/src/visual-diff.js +0 -1
- package/src/vocabularies/art-deco.js +79 -0
- package/src/vocabularies/brutalist.js +72 -0
- package/src/vocabularies/cyberpunk.js +92 -0
- package/src/vocabularies/editorial.js +75 -0
- package/src/vocabularies/index.js +35 -0
- package/src/vocabularies/soft-ui.js +83 -0
- package/src/vocabularies/swiss.js +60 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// designlang remix — render an extracted page-shape in a different design vocabulary.
|
|
2
|
+
//
|
|
3
|
+
// Inputs: the design object from extractDesignLanguage() (we read meta, voice,
|
|
4
|
+
// pageIntent, sectionRoles) + a vocabulary from src/vocabularies/.
|
|
5
|
+
// Output: a self-contained single HTML file. Same content, different language.
|
|
6
|
+
|
|
7
|
+
function esc(s) {
|
|
8
|
+
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function host(url) {
|
|
12
|
+
try { return new URL(url).hostname; } catch { return String(url || ''); }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pickHeadings(design, count = 6) {
|
|
16
|
+
const fromVoice = (design.voice?.sampleHeadings || []).filter(Boolean);
|
|
17
|
+
const fromSections = (design.sectionRoles?.sections || [])
|
|
18
|
+
.map(s => s.heading || s.slots?.heading)
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
const merged = [...new Set([...fromVoice, ...fromSections])];
|
|
21
|
+
return merged.slice(0, count);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pickCtas(design, count = 4) {
|
|
25
|
+
const verbs = (design.voice?.ctaVerbs || []).filter(Boolean);
|
|
26
|
+
const phrases = verbs.map(v => {
|
|
27
|
+
if (typeof v === 'string') return v;
|
|
28
|
+
return v.verb || v.text || v.phrase || '';
|
|
29
|
+
}).filter(Boolean);
|
|
30
|
+
if (phrases.length >= count) return phrases.slice(0, count);
|
|
31
|
+
// Pad with generic verbs informed by tone.
|
|
32
|
+
const tone = design.voice?.tone || 'neutral';
|
|
33
|
+
const fallback = tone === 'playful' ? ['Try it', 'Get started', 'See it', 'Play']
|
|
34
|
+
: tone === 'technical' ? ['Read docs', 'Get started', 'View API', 'Install']
|
|
35
|
+
: tone === 'formal' ? ['Begin', 'Continue', 'Learn more', 'Contact']
|
|
36
|
+
: ['Get started', 'Learn more', 'Sign up', 'See more'];
|
|
37
|
+
return [...phrases, ...fallback].slice(0, count);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Map each section role to a vocabulary-styled markup block.
|
|
41
|
+
function renderSection(section, ctx) {
|
|
42
|
+
const { vocab, headings, ctas, design } = ctx;
|
|
43
|
+
const sectionHeading = section.heading || section.slots?.heading || headings.shift() || vocab.name;
|
|
44
|
+
const lede = section.slots?.lede || '';
|
|
45
|
+
const role = section.role;
|
|
46
|
+
const buttonCount = Math.max(1, section.buttonCount || 1);
|
|
47
|
+
|
|
48
|
+
switch (role) {
|
|
49
|
+
case 'nav':
|
|
50
|
+
case 'footer':
|
|
51
|
+
return ''; // rendered globally outside sections
|
|
52
|
+
|
|
53
|
+
case 'hero': {
|
|
54
|
+
const ctaSet = ctas.slice(0, Math.max(1, Math.min(2, buttonCount)));
|
|
55
|
+
return `
|
|
56
|
+
<section class="v-hero">
|
|
57
|
+
<p class="v-pill">${esc((design.pageIntent?.type || 'landing').toUpperCase())}</p>
|
|
58
|
+
<h1 class="v-display v-h1">${esc(sectionHeading)}</h1>
|
|
59
|
+
${lede ? `<p class="v-lede">${esc(lede)}</p>` : ''}
|
|
60
|
+
<div class="v-cta-row">
|
|
61
|
+
${ctaSet.map((c, i) => `<a href="#" class="v-cta${i > 0 ? ' v-cta-ghost' : ''}">${esc(c)}</a>`).join('')}
|
|
62
|
+
</div>
|
|
63
|
+
</section>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'feature-grid':
|
|
67
|
+
case 'bento': {
|
|
68
|
+
const items = headings.splice(0, 3);
|
|
69
|
+
while (items.length < 3) items.push('Feature');
|
|
70
|
+
return `
|
|
71
|
+
<section class="v-section">
|
|
72
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
73
|
+
${lede ? `<p class="v-lede">${esc(lede)}</p>` : ''}
|
|
74
|
+
<div class="v-grid v-grid-3">
|
|
75
|
+
${items.map(t => `
|
|
76
|
+
<div class="v-card">
|
|
77
|
+
<div class="v-card-num">·</div>
|
|
78
|
+
<h3 class="v-display v-h3">${esc(t)}</h3>
|
|
79
|
+
<p class="v-body">${esc(squeeze(lede || sectionHeading, 90))}</p>
|
|
80
|
+
</div>`).join('')}
|
|
81
|
+
</div>
|
|
82
|
+
</section>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'stats': {
|
|
86
|
+
const numbers = ['10×', '99.9%', '< 50ms', '500K+'];
|
|
87
|
+
return `
|
|
88
|
+
<section class="v-section v-section-rule">
|
|
89
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
90
|
+
<div class="v-grid v-grid-4">
|
|
91
|
+
${numbers.map((n, i) => `
|
|
92
|
+
<div class="v-stat">
|
|
93
|
+
<div class="v-display v-stat-num">${esc(n)}</div>
|
|
94
|
+
<div class="v-pill">${esc((headings[i] || ['speed','uptime','latency','users'][i]).toUpperCase())}</div>
|
|
95
|
+
</div>`).join('')}
|
|
96
|
+
</div>
|
|
97
|
+
</section>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'testimonial': {
|
|
101
|
+
return `
|
|
102
|
+
<section class="v-section v-section-quiet">
|
|
103
|
+
<blockquote class="v-quote">
|
|
104
|
+
<p class="v-display v-quote-text">"${esc(lede || sectionHeading)}"</p>
|
|
105
|
+
<footer class="v-quote-attrib">— ${esc(headings.shift() || 'A satisfied user')}</footer>
|
|
106
|
+
</blockquote>
|
|
107
|
+
</section>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'pricing-table': {
|
|
111
|
+
const tiers = headings.splice(0, 3);
|
|
112
|
+
while (tiers.length < 3) tiers.push('Plan');
|
|
113
|
+
const prices = ['$0', '$29', '$99'];
|
|
114
|
+
return `
|
|
115
|
+
<section class="v-section">
|
|
116
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
117
|
+
<div class="v-grid v-grid-3">
|
|
118
|
+
${tiers.map((t, i) => `
|
|
119
|
+
<div class="v-card${i === 1 ? ' v-card-emphasis' : ''}">
|
|
120
|
+
<p class="v-pill">${esc(t.toUpperCase())}</p>
|
|
121
|
+
<div class="v-display v-price">${esc(prices[i])}</div>
|
|
122
|
+
<a href="#" class="v-cta">${esc(ctas[i] || 'Choose')}</a>
|
|
123
|
+
</div>`).join('')}
|
|
124
|
+
</div>
|
|
125
|
+
</section>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'faq': {
|
|
129
|
+
const qs = headings.splice(0, 4);
|
|
130
|
+
while (qs.length < 3) qs.push('A common question');
|
|
131
|
+
return `
|
|
132
|
+
<section class="v-section">
|
|
133
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
134
|
+
<div class="v-faq">
|
|
135
|
+
${qs.map(q => `
|
|
136
|
+
<details class="v-faq-item">
|
|
137
|
+
<summary class="v-faq-q">${esc(q)}</summary>
|
|
138
|
+
<p class="v-body">${esc(squeeze(lede || 'A short, useful answer in the voice of the original site.', 240))}</p>
|
|
139
|
+
</details>`).join('')}
|
|
140
|
+
</div>
|
|
141
|
+
</section>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'logo-wall': {
|
|
145
|
+
return `
|
|
146
|
+
<section class="v-section v-section-quiet">
|
|
147
|
+
<p class="v-pill v-pill-center">${esc(sectionHeading || 'Trusted by')}</p>
|
|
148
|
+
<div class="v-logos">
|
|
149
|
+
${Array.from({ length: 6 }).map((_, i) => `<div class="v-logo">${esc((headings[i] || `BRAND ${i + 1}`).toUpperCase())}</div>`).join('')}
|
|
150
|
+
</div>
|
|
151
|
+
</section>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'steps': {
|
|
155
|
+
const steps = headings.splice(0, 3);
|
|
156
|
+
while (steps.length < 3) steps.push('Step');
|
|
157
|
+
return `
|
|
158
|
+
<section class="v-section">
|
|
159
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
160
|
+
<ol class="v-steps">
|
|
161
|
+
${steps.map((s, i) => `
|
|
162
|
+
<li class="v-step">
|
|
163
|
+
<span class="v-display v-step-num">${String(i + 1).padStart(2, '0')}</span>
|
|
164
|
+
<h3 class="v-display v-h3">${esc(s)}</h3>
|
|
165
|
+
<p class="v-body">${esc(squeeze(lede || s, 120))}</p>
|
|
166
|
+
</li>`).join('')}
|
|
167
|
+
</ol>
|
|
168
|
+
</section>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'cta': {
|
|
172
|
+
const ctaSet = ctas.slice(0, 2);
|
|
173
|
+
return `
|
|
174
|
+
<section class="v-section v-section-cta">
|
|
175
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
176
|
+
${lede ? `<p class="v-lede">${esc(lede)}</p>` : ''}
|
|
177
|
+
<div class="v-cta-row">
|
|
178
|
+
${ctaSet.map((c, i) => `<a href="#" class="v-cta${i > 0 ? ' v-cta-ghost' : ''}">${esc(c)}</a>`).join('')}
|
|
179
|
+
</div>
|
|
180
|
+
</section>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default: {
|
|
184
|
+
return `
|
|
185
|
+
<section class="v-section">
|
|
186
|
+
<h2 class="v-display v-h2">${esc(sectionHeading)}</h2>
|
|
187
|
+
${lede ? `<p class="v-body">${esc(lede)}</p>` : ''}
|
|
188
|
+
</section>`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function squeeze(s, max) {
|
|
194
|
+
if (!s) return '';
|
|
195
|
+
s = String(s).replace(/\s+/g, ' ').trim();
|
|
196
|
+
if (s.length <= max) return s;
|
|
197
|
+
const cut = s.slice(0, max);
|
|
198
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
199
|
+
return (lastSpace > max * 0.7 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function formatRemix(design, vocab, opts = {}) {
|
|
203
|
+
if (!design) throw new Error('remix: design is required');
|
|
204
|
+
if (!vocab || !vocab.tokens) throw new Error('remix: vocabulary is required');
|
|
205
|
+
|
|
206
|
+
const url = design.meta?.url || '';
|
|
207
|
+
const hostName = host(url);
|
|
208
|
+
const title = design.meta?.title || hostName;
|
|
209
|
+
const ctas = pickCtas(design, 6);
|
|
210
|
+
|
|
211
|
+
// Dedup adjacent sections that share the same heading. Real-world section
|
|
212
|
+
// walkers (especially on SPA-rendered marketing pages) often emit a hero
|
|
213
|
+
// wrapper + an inner hero with identical h1 — visually one block, but two
|
|
214
|
+
// entries in sectionRoles.sections.
|
|
215
|
+
const seenHeadings = new Set();
|
|
216
|
+
const sections = (design.sectionRoles?.sections || [])
|
|
217
|
+
.filter(s => s.role !== 'nav' && s.role !== 'footer')
|
|
218
|
+
.filter(s => {
|
|
219
|
+
const h = (s.heading || s.slots?.heading || '').trim().toLowerCase().slice(0, 80);
|
|
220
|
+
if (!h) return true; // keep heading-less sections (logo-walls, footers)
|
|
221
|
+
if (seenHeadings.has(h)) return false;
|
|
222
|
+
seenHeadings.add(h);
|
|
223
|
+
return true;
|
|
224
|
+
})
|
|
225
|
+
.slice(0, 8);
|
|
226
|
+
|
|
227
|
+
// Headings pool for sections that don't carry their own. Exclude any heading
|
|
228
|
+
// already claimed by a kept section so heading-less sections (cta bands,
|
|
229
|
+
// logo-walls) don't shift a heading that another section will also render.
|
|
230
|
+
const claimed = new Set(
|
|
231
|
+
sections
|
|
232
|
+
.map(s => (s.heading || s.slots?.heading || '').trim().toLowerCase())
|
|
233
|
+
.filter(Boolean),
|
|
234
|
+
);
|
|
235
|
+
const headings = pickHeadings(design, 16).filter(
|
|
236
|
+
h => !claimed.has(h.trim().toLowerCase()),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// If extraction surfaced no sections (rare, but happens for SPA-rendered
|
|
240
|
+
// pages), synthesize a hero + features + cta from voice + intent so the
|
|
241
|
+
// remix still produces a believable artifact.
|
|
242
|
+
if (sections.length === 0) {
|
|
243
|
+
sections.push(
|
|
244
|
+
{ role: 'hero', heading: headings[0] || hostName, slots: { lede: design.pageIntent?.signals?.[0] }, buttonCount: 2 },
|
|
245
|
+
{ role: 'feature-grid', heading: headings[1] || 'What it does', slots: {} },
|
|
246
|
+
{ role: 'cta', heading: headings[2] || 'Get started', slots: {} },
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const ctx = { vocab, headings: [...headings], ctas, design };
|
|
251
|
+
const sectionsHtml = sections.map(s => renderSection(s, ctx)).join('');
|
|
252
|
+
|
|
253
|
+
const t = vocab.tokens;
|
|
254
|
+
const fontImports = [vocab.fonts?.display?.import, vocab.fonts?.body?.import]
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.filter((v, i, a) => a.indexOf(v) === i);
|
|
257
|
+
|
|
258
|
+
const ogTitle = `${hostName} · remixed as ${vocab.name.toLowerCase()}`;
|
|
259
|
+
const ogDesc = `${title} reimagined in the ${vocab.name} vocabulary by designlang.`;
|
|
260
|
+
|
|
261
|
+
return `<!doctype html>
|
|
262
|
+
<html lang="en">
|
|
263
|
+
<head>
|
|
264
|
+
<meta charset="utf-8">
|
|
265
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
266
|
+
<title>${esc(ogTitle)}</title>
|
|
267
|
+
<meta name="description" content="${esc(ogDesc)}">
|
|
268
|
+
<meta property="og:title" content="${esc(ogTitle)}">
|
|
269
|
+
<meta property="og:description" content="${esc(ogDesc)}">
|
|
270
|
+
<meta property="og:type" content="article">
|
|
271
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
272
|
+
${fontImports.map(href => `<link href="${esc(href)}" rel="stylesheet">`).join('')}
|
|
273
|
+
<style>
|
|
274
|
+
:root {
|
|
275
|
+
--paper: ${t.paper};
|
|
276
|
+
--ink: ${t.ink};
|
|
277
|
+
--ink-soft: ${t.inkSoft};
|
|
278
|
+
--accent: ${t.accent};
|
|
279
|
+
--rule: ${t.rule};
|
|
280
|
+
--radius: ${t.radius};
|
|
281
|
+
--radius-lg: ${t.radiusLg};
|
|
282
|
+
--shadow: ${t.shadow};
|
|
283
|
+
--shadow-sm: ${t.shadowSm};
|
|
284
|
+
--container: ${t.container};
|
|
285
|
+
--rhythm: ${t.rhythm};
|
|
286
|
+
}
|
|
287
|
+
* { box-sizing: border-box; }
|
|
288
|
+
html, body { margin: 0; padding: 0; }
|
|
289
|
+
${vocab.css || ''}
|
|
290
|
+
.v-wrap { max-width: var(--container); margin: 0 auto; padding: 40px 32px 80px; }
|
|
291
|
+
@media (max-width: 640px) { .v-wrap { padding: 28px 20px 56px; } }
|
|
292
|
+
.v-topbar { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 18px; border-bottom: 1px solid var(--rule); margin-bottom: 64px; }
|
|
293
|
+
.v-topbar .v-brand { font-family: var(--vocab-display); font-size: 22px; }
|
|
294
|
+
.v-topbar .v-meta { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
|
|
295
|
+
|
|
296
|
+
/* — Hero — */
|
|
297
|
+
.v-hero { padding: 32px 0 80px; }
|
|
298
|
+
.v-h1 { font-size: clamp(40px, 7vw, 88px); margin: 18px 0 22px; }
|
|
299
|
+
.v-h2 { font-size: clamp(28px, 4vw, 48px); margin: 0 0 18px; }
|
|
300
|
+
.v-h3 { font-size: 20px; margin: 12px 0 8px; }
|
|
301
|
+
.v-lede { font-size: clamp(17px, 1.6vw, 22px); line-height: 1.5; max-width: 56ch; margin: 0 0 28px; color: var(--ink-soft); }
|
|
302
|
+
.v-body { font-size: 14px; line-height: var(--rhythm); margin: 0; color: var(--ink-soft); }
|
|
303
|
+
.v-cta-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
|
|
304
|
+
.v-cta-ghost { background: transparent !important; color: var(--ink) !important; border-color: var(--ink) !important; box-shadow: none !important; }
|
|
305
|
+
|
|
306
|
+
/* — Sections — */
|
|
307
|
+
section.v-section { padding: 64px 0; border-top: 1px solid var(--rule); }
|
|
308
|
+
section.v-section-quiet { background: rgba(0,0,0,0.015); border-radius: var(--radius-lg); padding: 56px 48px; margin: 32px 0; }
|
|
309
|
+
section.v-section-cta { text-align: center; padding: 96px 0; border-top: 1px solid var(--rule); }
|
|
310
|
+
section.v-section-cta .v-cta-row { justify-content: center; }
|
|
311
|
+
section.v-section-rule .v-grid { padding-top: 28px; border-top: 1px solid var(--rule); }
|
|
312
|
+
|
|
313
|
+
/* — Grids — */
|
|
314
|
+
.v-grid { display: grid; gap: 28px; margin-top: 28px; }
|
|
315
|
+
.v-grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
|
316
|
+
.v-grid-4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
|
317
|
+
.v-card { padding: 24px; }
|
|
318
|
+
.v-card-num { font-family: var(--vocab-display); font-size: 32px; opacity: .35; margin-bottom: 8px; }
|
|
319
|
+
.v-card-emphasis { transform: translateY(-8px); }
|
|
320
|
+
|
|
321
|
+
/* — Stats — */
|
|
322
|
+
.v-stat { padding: 12px 0; }
|
|
323
|
+
.v-stat-num { font-size: clamp(36px, 4vw, 56px); line-height: 1; margin-bottom: 8px; }
|
|
324
|
+
|
|
325
|
+
/* — Quote — */
|
|
326
|
+
.v-quote { margin: 0; padding: 0; }
|
|
327
|
+
.v-quote-text { font-size: clamp(22px, 3vw, 38px); line-height: 1.25; margin: 0 0 24px; }
|
|
328
|
+
.v-quote-attrib { font-size: 14px; color: var(--ink-soft); }
|
|
329
|
+
|
|
330
|
+
/* — Pricing — */
|
|
331
|
+
.v-price { font-size: clamp(40px, 5vw, 72px); line-height: 1; margin: 14px 0 22px; }
|
|
332
|
+
|
|
333
|
+
/* — FAQ — */
|
|
334
|
+
.v-faq { margin-top: 28px; }
|
|
335
|
+
.v-faq-item { padding: 20px 0; border-top: 1px solid var(--rule); }
|
|
336
|
+
.v-faq-item[open] { padding-bottom: 24px; }
|
|
337
|
+
.v-faq-q { font-family: var(--vocab-display); font-size: 20px; cursor: pointer; list-style: none; display: flex; justify-content: space-between; align-items: center; }
|
|
338
|
+
.v-faq-q::after { content: '+'; font-size: 24px; color: var(--ink-soft); transition: transform .2s; }
|
|
339
|
+
.v-faq-item[open] .v-faq-q::after { transform: rotate(45deg); }
|
|
340
|
+
.v-faq-q::-webkit-details-marker { display: none; }
|
|
341
|
+
|
|
342
|
+
/* — Logos — */
|
|
343
|
+
.v-pill-center { display: inline-block; }
|
|
344
|
+
.v-logos { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 24px; margin-top: 28px; }
|
|
345
|
+
.v-logo { font-family: var(--vocab-display); font-weight: 700; opacity: .55; padding: 12px 0; text-align: center; letter-spacing: .04em; }
|
|
346
|
+
|
|
347
|
+
/* — Steps — */
|
|
348
|
+
.v-steps { list-style: none; padding: 0; margin: 32px 0 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 32px; counter-reset: step; }
|
|
349
|
+
.v-step { padding-top: 12px; border-top: 2px solid var(--ink); }
|
|
350
|
+
.v-step-num { font-size: 32px; opacity: .25; margin-bottom: 10px; display: block; }
|
|
351
|
+
|
|
352
|
+
/* — Footer — */
|
|
353
|
+
footer.v-footer { margin-top: 96px; padding-top: 32px; border-top: 1px solid var(--rule); display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap; gap: 16px; font-size: 12px; }
|
|
354
|
+
footer.v-footer .v-sig { font-family: var(--vocab-display); font-size: 22px; }
|
|
355
|
+
footer.v-footer code { font-family: ${vocab.fonts?.body?.family ? `'${vocab.fonts.body.family}'` : 'ui-monospace'}, monospace; font-size: 11px; }
|
|
356
|
+
</style>
|
|
357
|
+
</head>
|
|
358
|
+
<body>
|
|
359
|
+
<main class="v-wrap">
|
|
360
|
+
<header class="v-topbar">
|
|
361
|
+
<div class="v-brand">${esc(hostName)}</div>
|
|
362
|
+
<div class="v-meta">remixed · ${esc(vocab.name)}</div>
|
|
363
|
+
</header>
|
|
364
|
+
|
|
365
|
+
${sectionsHtml}
|
|
366
|
+
|
|
367
|
+
<footer class="v-footer">
|
|
368
|
+
<div>
|
|
369
|
+
<div class="v-sig">${esc(hostName)} <span class="v-mark">×</span> ${esc(vocab.name)}</div>
|
|
370
|
+
<div class="v-meta" style="margin-top:6px">${esc(title)}</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div>
|
|
373
|
+
<code>npx designlang remix ${esc(hostName)} --as ${esc(opts.vocabId || '')}</code>
|
|
374
|
+
</div>
|
|
375
|
+
</footer>
|
|
376
|
+
</main>
|
|
377
|
+
</body>
|
|
378
|
+
</html>`;
|
|
379
|
+
}
|
package/src/history.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Historical tracking — save and compare design snapshots over time
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync,
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { extractWideGamut } from './extractors/wide-gamut.js';
|
|
|
26
26
|
import { extractTokenSources } from './extractors/token-sources.js';
|
|
27
27
|
import { extractInteractionStates } from './extractors/interaction-states.js';
|
|
28
28
|
import { extractMotion } from './extractors/motion.js';
|
|
29
|
-
import { extractComponentAnatomy
|
|
29
|
+
import { extractComponentAnatomy } from './extractors/component-anatomy.js';
|
|
30
30
|
import { extractVoice } from './extractors/voice.js';
|
|
31
31
|
import { extractPageIntent } from './extractors/page-intent.js';
|
|
32
32
|
import { extractSectionRoles } from './extractors/section-roles.js';
|
|
@@ -39,7 +39,6 @@ import { extractBackgroundPatterns } from './extractors/background-patterns.js';
|
|
|
39
39
|
import { extractStackIntel } from './extractors/stack-intel.js';
|
|
40
40
|
import { extractFormStates } from './extractors/form-states.js';
|
|
41
41
|
import { formatDtcgTokens } from './formatters/dtcg-tokens.js';
|
|
42
|
-
import { formatMotionTokens } from './formatters/motion-tokens.js';
|
|
43
42
|
|
|
44
43
|
function safeExtract(fn, ...args) {
|
|
45
44
|
try { return fn(...args); } catch { return null; }
|
package/src/studio.js
CHANGED
|
@@ -302,9 +302,9 @@ export async function runStudio(opts) {
|
|
|
302
302
|
res.end('not found');
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
|
-
// Race-
|
|
305
|
+
// Race-free read — let readFileSync surface ENOENT / EISDIR / EACCES
|
|
306
|
+
// in one syscall instead of a stat→read pair (which would TOCTOU).
|
|
306
307
|
try {
|
|
307
|
-
if (!statSync(filePath).isFile()) throw new Error('not a file');
|
|
308
308
|
const body = readFileSync(filePath);
|
|
309
309
|
const ext = extname(filePath).toLowerCase();
|
|
310
310
|
res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' });
|
package/src/sync.js
CHANGED
|
@@ -5,17 +5,28 @@ import { formatTokens } from './formatters/tokens.js';
|
|
|
5
5
|
import { formatTailwind } from './formatters/tailwind.js';
|
|
6
6
|
import { formatCssVars } from './formatters/css-vars.js';
|
|
7
7
|
import { saveSnapshot, getHistory } from './history.js';
|
|
8
|
-
import {
|
|
8
|
+
import { openSync, closeSync, ftruncateSync, writeSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
|
|
11
|
-
// Race-
|
|
12
|
-
//
|
|
11
|
+
// Race-free "update only if file exists" — open with 'r+' atomically
|
|
12
|
+
// requires an existing file (throws ENOENT otherwise) and gives us a
|
|
13
|
+
// write-capable descriptor in one syscall, eliminating the toctou window
|
|
14
|
+
// that statSync→writeFileSync would have. Truncate then write through the
|
|
15
|
+
// same fd so no other process can sneak between check and write.
|
|
13
16
|
function updateIfExists(path, content) {
|
|
17
|
+
let fd;
|
|
14
18
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
fd = openSync(path, 'r+');
|
|
20
|
+
ftruncateSync(fd, 0);
|
|
21
|
+
writeSync(fd, content, 0, 'utf-8');
|
|
17
22
|
return true;
|
|
18
|
-
} catch {
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
} finally {
|
|
26
|
+
if (fd !== undefined) {
|
|
27
|
+
try { closeSync(fd); } catch { /* best-effort close */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
export async function syncDesign(url, options = {}) {
|
package/src/visual-diff.js
CHANGED
|
@@ -7,7 +7,6 @@ import { extractDesignLanguage } from './index.js';
|
|
|
7
7
|
import { diffDesigns } from './diff.js';
|
|
8
8
|
import { nameFromUrl } from './utils.js';
|
|
9
9
|
import { statSync, existsSync, readFileSync } from 'fs';
|
|
10
|
-
import { basename } from 'path';
|
|
11
10
|
|
|
12
11
|
function fileKb(p) {
|
|
13
12
|
try { return Math.round(statSync(p).size / 1024); } catch { return 0; }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Art Deco — geometry, gold, ornament, vertical typography.
|
|
2
|
+
// References: Chrysler Building, 1920s Vogue covers, Gatsby-era posters.
|
|
3
|
+
|
|
4
|
+
export const artDeco = {
|
|
5
|
+
name: 'Art Deco',
|
|
6
|
+
blurb: 'Gold on ink, geometric ornament, vertical type.',
|
|
7
|
+
fonts: {
|
|
8
|
+
display: { family: 'Playfair Display', weights: [400, 700, 900], import: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&display=swap' },
|
|
9
|
+
body: { family: 'Cormorant Garamond', weights: [400, 500, 700], import: 'https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;700&display=swap' },
|
|
10
|
+
},
|
|
11
|
+
tokens: {
|
|
12
|
+
paper: '#0d1117',
|
|
13
|
+
ink: '#e8d4a0',
|
|
14
|
+
inkSoft: '#a89968',
|
|
15
|
+
accent: '#d4af37',
|
|
16
|
+
rule: '#a89968',
|
|
17
|
+
radius: '0px',
|
|
18
|
+
radiusLg: '2px',
|
|
19
|
+
shadow: 'none',
|
|
20
|
+
shadowSm: 'none',
|
|
21
|
+
spacingUnit: 8,
|
|
22
|
+
container: '1080px',
|
|
23
|
+
rhythm: 1.55,
|
|
24
|
+
},
|
|
25
|
+
css: `
|
|
26
|
+
:root {
|
|
27
|
+
--vocab-display: 'Playfair Display', 'Times New Roman', serif;
|
|
28
|
+
--vocab-body: 'Cormorant Garamond', 'Garamond', serif;
|
|
29
|
+
}
|
|
30
|
+
body {
|
|
31
|
+
background: var(--paper);
|
|
32
|
+
color: var(--ink);
|
|
33
|
+
font-family: var(--vocab-body);
|
|
34
|
+
font-size: 18px;
|
|
35
|
+
line-height: 1.6;
|
|
36
|
+
background-image:
|
|
37
|
+
linear-gradient(135deg, rgba(212,175,55,0.04) 0%, transparent 40%),
|
|
38
|
+
radial-gradient(ellipse at top, rgba(232,212,160,0.06) 0%, transparent 70%);
|
|
39
|
+
}
|
|
40
|
+
.v-display, h1, h2, h3 {
|
|
41
|
+
font-family: var(--vocab-display);
|
|
42
|
+
font-weight: 900;
|
|
43
|
+
letter-spacing: 0.005em;
|
|
44
|
+
line-height: 1.0;
|
|
45
|
+
color: var(--accent);
|
|
46
|
+
}
|
|
47
|
+
.v-card { border: 1px solid var(--rule); padding: 28px; position: relative; }
|
|
48
|
+
.v-card::before, .v-card::after {
|
|
49
|
+
content: '';
|
|
50
|
+
position: absolute; width: 12px; height: 12px;
|
|
51
|
+
border: 1px solid var(--accent);
|
|
52
|
+
}
|
|
53
|
+
.v-card::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
|
|
54
|
+
.v-card::after { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }
|
|
55
|
+
.v-rule {
|
|
56
|
+
border: 0;
|
|
57
|
+
height: 1px;
|
|
58
|
+
background: linear-gradient(90deg, transparent, var(--accent) 20%, var(--accent) 80%, transparent);
|
|
59
|
+
margin: 32px auto;
|
|
60
|
+
max-width: 200px;
|
|
61
|
+
}
|
|
62
|
+
.v-cta {
|
|
63
|
+
background: var(--paper);
|
|
64
|
+
color: var(--accent);
|
|
65
|
+
border: 1.5px solid var(--accent);
|
|
66
|
+
padding: 14px 32px;
|
|
67
|
+
font-family: var(--vocab-display);
|
|
68
|
+
font-weight: 700;
|
|
69
|
+
letter-spacing: 0.18em;
|
|
70
|
+
text-transform: uppercase;
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
transition: background .2s;
|
|
73
|
+
}
|
|
74
|
+
.v-cta:hover { background: var(--accent); color: var(--paper); }
|
|
75
|
+
.v-pill { font-family: var(--vocab-body); font-style: italic; color: var(--accent); letter-spacing: 0.04em; }
|
|
76
|
+
.v-mark { color: var(--accent); font-style: italic; }
|
|
77
|
+
a { color: var(--accent); text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px; }
|
|
78
|
+
`,
|
|
79
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Brutalist — exposed structure, hard edges, raw type, single accent.
|
|
2
|
+
// References: David Carson's Ray Gun, early Bloomberg.com, Balenciaga,
|
|
3
|
+
// brutalistwebsites.com archive.
|
|
4
|
+
|
|
5
|
+
export const brutalist = {
|
|
6
|
+
name: 'Brutalist',
|
|
7
|
+
blurb: 'Hard edges, mono type, single screaming accent.',
|
|
8
|
+
fonts: {
|
|
9
|
+
display: { family: 'Space Grotesk', weights: [500, 700], import: 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&display=swap' },
|
|
10
|
+
body: { family: 'IBM Plex Mono', weights: [400, 500, 700], import: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap' },
|
|
11
|
+
},
|
|
12
|
+
tokens: {
|
|
13
|
+
paper: '#f4f1ea',
|
|
14
|
+
ink: '#0a0a0a',
|
|
15
|
+
inkSoft: '#3a3a3a',
|
|
16
|
+
accent: '#ff4800',
|
|
17
|
+
rule: '#0a0a0a',
|
|
18
|
+
radius: '0px',
|
|
19
|
+
radiusLg: '0px',
|
|
20
|
+
shadow: '6px 6px 0 #0a0a0a',
|
|
21
|
+
shadowSm: '3px 3px 0 #0a0a0a',
|
|
22
|
+
spacingUnit: 8,
|
|
23
|
+
container: '1100px',
|
|
24
|
+
rhythm: 1.45,
|
|
25
|
+
},
|
|
26
|
+
// Signature CSS — applied alongside the per-instance vars below.
|
|
27
|
+
// Use --vocab-* prefix so tokens compose without colliding with the page shape.
|
|
28
|
+
css: `
|
|
29
|
+
:root {
|
|
30
|
+
--vocab-display: 'Space Grotesk', 'Helvetica Neue', sans-serif;
|
|
31
|
+
--vocab-body: 'IBM Plex Mono', ui-monospace, monospace;
|
|
32
|
+
}
|
|
33
|
+
body {
|
|
34
|
+
background: var(--paper);
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font-family: var(--vocab-body);
|
|
37
|
+
font-size: 15px;
|
|
38
|
+
line-height: 1.55;
|
|
39
|
+
text-transform: uppercase;
|
|
40
|
+
letter-spacing: 0.02em;
|
|
41
|
+
}
|
|
42
|
+
.v-display, h1, h2, h3 {
|
|
43
|
+
font-family: var(--vocab-display);
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
letter-spacing: -0.02em;
|
|
46
|
+
text-transform: none;
|
|
47
|
+
line-height: 0.95;
|
|
48
|
+
}
|
|
49
|
+
.v-card { border: 2px solid var(--ink); background: var(--paper); box-shadow: var(--shadow); }
|
|
50
|
+
.v-rule { border-top: 2px solid var(--ink); }
|
|
51
|
+
.v-cta {
|
|
52
|
+
background: var(--accent);
|
|
53
|
+
color: var(--ink);
|
|
54
|
+
border: 2px solid var(--ink);
|
|
55
|
+
box-shadow: var(--shadow-sm);
|
|
56
|
+
padding: 14px 22px;
|
|
57
|
+
font-family: var(--vocab-display);
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
letter-spacing: 0.04em;
|
|
61
|
+
transition: transform .08s, box-shadow .08s;
|
|
62
|
+
}
|
|
63
|
+
.v-cta:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--ink); }
|
|
64
|
+
.v-pill { display: inline-block; padding: 4px 10px; border: 1.5px solid var(--ink); background: var(--paper); }
|
|
65
|
+
.v-mark { background: var(--accent); padding: 0 4px; }
|
|
66
|
+
.v-noise {
|
|
67
|
+
background-image: repeating-linear-gradient(45deg, transparent 0 6px, rgba(0,0,0,0.04) 6px 7px);
|
|
68
|
+
}
|
|
69
|
+
a { color: var(--ink); text-decoration: none; border-bottom: 2px solid var(--accent); }
|
|
70
|
+
a:hover { background: var(--accent); }
|
|
71
|
+
`,
|
|
72
|
+
};
|