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.
@@ -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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
+ }
@@ -1,4 +1,4 @@
1
- import { rgbToHex, rgbToHsl } from '../utils.js';
1
+ import { rgbToHsl } from '../utils.js';
2
2
 
3
3
  function generateColorScale(hex, parsed) {
4
4
  const { h, s } = parsed.hsl ?? rgbToHsl(parsed.rgb);
@@ -1,5 +1,3 @@
1
- import { rgbToHsl } from '../utils.js';
2
-
3
1
  export function formatVueTheme(design) {
4
2
  const { colors, typography, spacing, borders, shadows } = design;
5
3
  const lines = [];
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, existsSync, mkdirSync } from 'fs';
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, formatAnatomyStubs } from './extractors/component-anatomy.js';
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; }