designlang 12.2.0 → 12.4.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 +76 -0
- package/README.md +12 -4
- package/bin/design-extract.js +124 -1
- package/package.json +1 -1
- package/src/chat.js +0 -14
- 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/pack.js +376 -0
- 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; }
|