designlang 12.2.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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.3.0] — 2026-05-05
4
+
5
+ **Remix — restyle any site in a different design vocabulary.**
6
+
7
+ A genuinely new product surface: take an extracted page-shape (sections,
8
+ voice, page-intent, anatomy) and re-render it under one of six
9
+ opinionated design vocabularies. "What would stripe.com look like if it
10
+ had been designed brutalist? Or art-deco? Or cyberpunk?"
11
+
12
+ ### Added
13
+
14
+ - **`designlang remix <url> --as <vocab>`** — re-renders the audited page
15
+ using the host's *own copy* (headings, ledes, CTA verbs from voice) but
16
+ styled in another vocabulary. Six built-ins:
17
+ - `brutalist` — hard edges, mono type, single screaming accent
18
+ - `swiss` — Helvetica, grids, restraint (post-Bauhaus default)
19
+ - `art-deco` — gold on ink, geometric ornament, vertical type
20
+ - `cyberpunk` — neon on midnight, scanlines, mono with glitch energy
21
+ - `soft-ui` — cushioned shapes, low contrast, Vision-OS-adjacent
22
+ - `editorial` — broadsheet serifs, generous whitespace, ink on paper
23
+ - `--all` flag emits one HTML per vocabulary in a single extraction.
24
+ - `--list` prints the vocabulary registry with blurbs.
25
+ - New formatter: `src/formatters/remix.js` — maps every section role
26
+ (hero, feature-grid, pricing-table, stats, testimonial, faq,
27
+ logo-wall, steps, cta) to vocabulary-styled markup.
28
+ - New module: `src/vocabularies/` — six self-contained vocab definitions
29
+ (tokens + font stack + signature CSS) plus `index.js` registry.
30
+ - Hero-deduplication: real-world section walkers (especially on SPA
31
+ marketing pages) often emit a hero wrapper + an inner hero with the
32
+ same h1. Remix now dedupes by heading and excludes claimed headings
33
+ from the voice pool, so heading-less sections (cta bands, logo walls)
34
+ don't re-render an already-claimed heading.
35
+ - 14 new tests (350 total, all passing). Cover registry shape,
36
+ per-vocab token validity, dedup, XSS escaping, missing-input errors.
37
+
38
+ Why: Grade (v12.1) is the audit, Battle (v12.2) is the comparison,
39
+ Remix is the *transformation*. Pure visual moat — no competitor
40
+ (Dembrandt, Superposition, html.to.design, Builder Visual Copilot)
41
+ ships site-shape-preserving vocabulary swap.
42
+
3
43
  ## [12.2.0] — 2026-05-02
4
44
 
5
45
  **Battle cards + design score badges — distribution + virality on top of Grade.**
package/README.md CHANGED
@@ -26,8 +26,10 @@ It also goes where extractors don't: **layout patterns**, **responsive behavior
26
26
 
27
27
  ```bash
28
28
  npx designlang https://stripe.com # extract everything
29
- npx designlang grade https://stripe.com --badge # report card + SVG badge ← v12.2
30
- npx designlang battle stripe.com vercel.com # head-to-head graded fight ← v12.2
29
+ npx designlang remix stripe.com --as cyberpunk # restyle in another vocabulary ← v12.3
30
+ npx designlang remix stripe.com --all # emit all 6 vocabs at once ← v12.3
31
+ npx designlang grade https://stripe.com --badge # report card + SVG badge ← v12.2
32
+ npx designlang battle stripe.com vercel.com # head-to-head graded fight ← v12.2
31
33
  npx designlang clone https://stripe.com # working Next.js starter
32
34
  npx designlang --full https://stripe.com # screenshots + responsive + interactions
33
35
  ```
@@ -65,6 +67,7 @@ Each run writes 17+ files to `./design-extract-output/`. The headline outputs:
65
67
  | `*-grade.html` | **v12.1** Shareable Design Report Card (letter grade + evidence) |
66
68
  | `*-grade.svg` | **v12.2** Shields.io-style design-score badge (drop into any README) |
67
69
  | `*-battle.html` | **v12.2** Head-to-head graded battle card from `designlang battle` |
70
+ | `*-remix.<vocab>.html` | **v12.3** Site restyled in another vocabulary — brutalist / swiss / art-deco / cyberpunk / soft-ui / editorial |
68
71
 
69
72
  Multi-platform (`--platforms web,ios,android,flutter,wordpress,all`) adds `ios/`, `android/`, `flutter/`, and a WordPress block theme. `--emit-agent-rules` adds Cursor / Claude Code / generic agent rule files.
70
73
 
@@ -124,8 +127,9 @@ designlang mcp # stdio MCP server for Cursor / Clau
124
127
  | Clone | `designlang clone <url>` | Generate a working Next.js starter with extracted design |
125
128
  | Score | `designlang score <url>` | Rate design quality with visual bar chart breakdown |
126
129
  | Grade (v12.1) | `designlang grade <url>` | Shareable HTML "Design Report Card" — letter grade, 8 dimensions, evidence, strengths + fixes |
127
- | Battle (NEW v12.2) | `designlang battle <A> <B>` | Head-to-head graded battle card with verdict, dimension table, palette comparison |
128
- | Badge (NEW v12.2) | `designlang grade --badge` | Shields.io-style SVG badge — `design · B · 87` — drop into any README. Live endpoint: `designlang.app/badge/<host>.svg` |
130
+ | Battle (v12.2) | `designlang battle <A> <B>` | Head-to-head graded battle card with verdict, dimension table, palette comparison |
131
+ | Badge (v12.2) | `designlang grade --badge` | Shields.io-style SVG badge — `design · B · 87` — drop into any README. Live endpoint: `designlang.app/badge/<host>.svg` |
132
+ | Remix (NEW v12.3) | `designlang remix <url> --as <vocab>` | Restyle the audited page in another vocabulary (brutalist / swiss / art-deco / cyberpunk / soft-ui / editorial). `--all` emits all 6 |
129
133
  | Watch | `designlang watch <url>` | Monitor for design changes on interval |
130
134
  | Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
131
135
  | Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
@@ -180,6 +184,7 @@ Commands:
180
184
  score <url> Rate design quality (7 categories, A-F, bar chart)
181
185
  grade <url> Generate a shareable HTML Design Report Card (--format html|md|json|svg|all, --badge, --open)
182
186
  battle <urlA> <urlB> Head-to-head graded battle card (--format html|md|json|all, --open)
187
+ remix <url> Restyle in another vocabulary (--as brutalist|swiss|art-deco|cyberpunk|soft-ui|editorial, --all, --list, --open)
183
188
  watch <url> Monitor for design changes on interval
184
189
  diff <urlA> <urlB> Compare two sites' design languages
185
190
  brands <urls...> Multi-brand comparison matrix
@@ -43,11 +43,12 @@ import { syncDesign } from '../src/sync.js';
43
43
  import { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from '../src/multibrand.js';
44
44
  import { generateClone } from '../src/clone.js';
45
45
  import { watchSite } from '../src/watch.js';
46
- import { diffDarkMode } from '../src/darkdiff.js';
47
46
  import { applyDesign } from '../src/apply.js';
48
47
  import { formatGrade, formatGradeMarkdown } from '../src/formatters/grade.js';
49
48
  import { formatBattle, formatBattleMarkdown } from '../src/formatters/battle.js';
50
49
  import { formatScoreBadge } from '../src/formatters/badge.js';
50
+ import { formatRemix } from '../src/formatters/remix.js';
51
+ import { VOCABULARIES, getVocabulary, listVocabularies } from '../src/vocabularies/index.js';
51
52
  import { nameFromUrl } from '../src/utils.js';
52
53
 
53
54
  function validateUrl(url) {
@@ -1102,6 +1103,75 @@ program
1102
1103
  }
1103
1104
  });
1104
1105
 
1106
+ // ── Remix command — restyle an extracted page in another vocabulary ─
1107
+ program
1108
+ .command('remix <url>')
1109
+ .description('Restyle a site in a different design vocabulary (brutalist, swiss, art-deco, cyberpunk, soft-ui, editorial)')
1110
+ .option('-o, --out <dir>', 'output directory', './design-extract-output')
1111
+ .option('-n, --name <name>', 'output file prefix (default: derived from URL)')
1112
+ .option('--as <vocab>', 'vocabulary id (run `designlang remix --list` to see all)', 'brutalist')
1113
+ .option('--list', 'list all vocabularies and exit')
1114
+ .option('--all', 'emit one HTML per vocabulary (six files at once)')
1115
+ .option('--open', 'open the result in the default browser')
1116
+ .action(async (url, opts) => {
1117
+ if (opts.list) {
1118
+ console.log('');
1119
+ console.log(chalk.bold(' Vocabularies'));
1120
+ console.log('');
1121
+ for (const v of listVocabularies()) {
1122
+ console.log(` ${chalk.cyan(v.id.padEnd(14))} ${chalk.gray(v.blurb)}`);
1123
+ }
1124
+ console.log('');
1125
+ console.log(chalk.gray(` Use: designlang remix <url> --as <id>`));
1126
+ console.log('');
1127
+ return;
1128
+ }
1129
+ if (!url.startsWith('http')) url = `https://${url}`;
1130
+ validateUrl(url);
1131
+
1132
+ const vocabIds = opts.all ? Object.keys(VOCABULARIES) : [opts.as];
1133
+ // Validate vocab early so we fail before extraction.
1134
+ for (const id of vocabIds) getVocabulary(id);
1135
+
1136
+ const spinner = ora(`Extracting ${url}...`).start();
1137
+ try {
1138
+ const design = await extractDesignLanguage(url);
1139
+
1140
+ const outDir = resolve(opts.out);
1141
+ mkdirSync(outDir, { recursive: true });
1142
+ const prefix = opts.name || nameFromUrl(url);
1143
+ const written = [];
1144
+
1145
+ for (const id of vocabIds) {
1146
+ spinner.text = `Rendering ${id}...`;
1147
+ const vocab = getVocabulary(id);
1148
+ const html = formatRemix(design, vocab, { vocabId: id, version: PKG_VERSION });
1149
+ const p = join(outDir, `${prefix}.remix.${id}.html`);
1150
+ writeFileSync(p, html);
1151
+ written.push(p);
1152
+ }
1153
+
1154
+ spinner.stop();
1155
+ console.log('');
1156
+ console.log(` ${chalk.bold('Remixed')} ${chalk.gray('·')} ${chalk.cyan(vocabIds.join(', '))} ${chalk.gray('·')} ${chalk.gray(url)}`);
1157
+ console.log('');
1158
+ for (const f of written) console.log(` ${chalk.green('✓')} ${chalk.gray(f)}`);
1159
+ console.log('');
1160
+ console.log(chalk.gray(` Open the .html in a browser. One file per vocabulary, fully self-contained.`));
1161
+ console.log('');
1162
+
1163
+ if (opts.open && written.length > 0) {
1164
+ const { spawn } = await import('child_process');
1165
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1166
+ spawn(cmd, [written[0]], { detached: true, stdio: 'ignore' }).unref();
1167
+ }
1168
+ } catch (err) {
1169
+ spinner.fail('Remix failed');
1170
+ console.error(chalk.red(`\n ${err.message}\n`));
1171
+ process.exit(1);
1172
+ }
1173
+ });
1174
+
1105
1175
  // ── Apply command ──────────────────────────────────────────
1106
1176
  program
1107
1177
  .command('apply <url>')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designlang",
3
- "version": "12.2.0",
3
+ "version": "12.3.0",
4
4
  "description": "Extract the complete design language from any website and ship it — clone to a working Next.js starter, guard tokens with a CI drift bot, or browse everything in a local studio. Outputs W3C DTCG tokens, motion tokens, typed anatomy stubs, Tailwind config, and ready-to-paste v0 / Lovable / Cursor / Claude-Artifacts prompts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/chat.js CHANGED
@@ -22,20 +22,6 @@ function isHex(s) {
22
22
  return typeof s === 'string' && /^#[0-9a-f]{3,8}$/i.test(s.trim());
23
23
  }
24
24
 
25
- function hexToRgb(hex) {
26
- const m = String(hex).trim().toLowerCase().replace(/^#/, '');
27
- const full = m.length === 3 ? m.split('').map((c) => c + c).join('') : m.slice(0, 6);
28
- return {
29
- r: parseInt(full.slice(0, 2), 16) || 0,
30
- g: parseInt(full.slice(2, 4), 16) || 0,
31
- b: parseInt(full.slice(4, 6), 16) || 0,
32
- };
33
- }
34
-
35
- function rgbToHex({ r, g, b }) {
36
- return '#' + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
37
- }
38
-
39
25
  function opSharpenRadii(design, factor = 0.5) {
40
26
  const radii = design.borders?.radii || [];
41
27
  const next = radii.map((r) => ({ ...r, value: Math.max(0, Math.round((r.value || 0) * factor)) }));
@@ -1,5 +1,3 @@
1
- import { pxToRem } from '../utils.js';
2
-
3
1
  export function formatCssVars(design) {
4
2
  const lines = [':root {'];
5
3
 
@@ -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; }
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-safe read — single try/catch instead of exists→stat→read chain.
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 { writeFileSync, readFileSync, statSync } from 'fs';
8
+ import { openSync, closeSync, ftruncateSync, writeSync } from 'fs';
9
9
  import { join } from 'path';
10
10
 
11
- // Race-safe "update only if file exists" — statSync inside try/catch
12
- // closes the toctou window vs. existsSync→writeFileSync.
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
- if (!statSync(path).isFile()) return false;
16
- writeFileSync(path, content, 'utf-8');
19
+ fd = openSync(path, 'r+');
20
+ ftruncateSync(fd, 0);
21
+ writeSync(fd, content, 0, 'utf-8');
17
22
  return true;
18
- } catch { return false; }
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 = {}) {
@@ -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
+ };
@@ -0,0 +1,92 @@
1
+ // Cyberpunk — neon on midnight, scanlines, glitch type, electric accents.
2
+ // References: Blade Runner 2049 UI, Cyberpunk 2077, vaporwave.
3
+
4
+ export const cyberpunk = {
5
+ name: 'Cyberpunk',
6
+ blurb: 'Neon on midnight, scanlines, mono type with glitch energy.',
7
+ fonts: {
8
+ display: { family: 'Space Grotesk', weights: [500, 700], import: 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&display=swap' },
9
+ body: { family: 'JetBrains Mono', weights: [400, 500, 700], import: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap' },
10
+ },
11
+ tokens: {
12
+ paper: '#0a0815',
13
+ ink: '#e0e0ff',
14
+ inkSoft: '#7d80b0',
15
+ accent: '#ff2bd6',
16
+ accentAlt: '#00f0ff',
17
+ rule: '#2a2050',
18
+ radius: '2px',
19
+ radiusLg: '4px',
20
+ shadow: '0 0 28px rgba(255,43,214,0.4), 0 0 0 1px rgba(255,43,214,0.6)',
21
+ shadowSm: '0 0 14px rgba(0,240,255,0.3)',
22
+ spacingUnit: 8,
23
+ container: '1140px',
24
+ rhythm: 1.5,
25
+ },
26
+ css: `
27
+ :root {
28
+ --vocab-display: 'Space Grotesk', 'Eurostile', sans-serif;
29
+ --vocab-body: 'JetBrains Mono', ui-monospace, monospace;
30
+ --accent-alt: #00f0ff;
31
+ }
32
+ body {
33
+ background: var(--paper);
34
+ color: var(--ink);
35
+ font-family: var(--vocab-body);
36
+ font-size: 14px;
37
+ line-height: 1.55;
38
+ letter-spacing: 0.01em;
39
+ background-image:
40
+ radial-gradient(ellipse at top right, rgba(255,43,214,0.08) 0%, transparent 50%),
41
+ radial-gradient(ellipse at bottom left, rgba(0,240,255,0.08) 0%, transparent 50%),
42
+ repeating-linear-gradient(0deg, transparent 0 2px, rgba(255,255,255,0.012) 2px 3px);
43
+ }
44
+ .v-display, h1, h2, h3 {
45
+ font-family: var(--vocab-display);
46
+ font-weight: 700;
47
+ letter-spacing: -0.02em;
48
+ line-height: 1.0;
49
+ text-shadow: 2px 0 0 rgba(255,43,214,0.4), -2px 0 0 rgba(0,240,255,0.4);
50
+ }
51
+ h1::before { content: '> '; color: var(--accent-alt); }
52
+ .v-card {
53
+ background: linear-gradient(160deg, rgba(40,30,80,0.5), rgba(20,15,40,0.5));
54
+ border: 1px solid var(--rule);
55
+ box-shadow: var(--shadow-sm);
56
+ padding: 24px;
57
+ position: relative;
58
+ }
59
+ .v-card::before {
60
+ content: ''; position: absolute; inset: 0;
61
+ background: linear-gradient(45deg, transparent 49%, var(--accent) 49.5%, var(--accent) 50%, transparent 50.5%) top right / 12px 12px no-repeat;
62
+ }
63
+ .v-rule { border: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--accent), transparent); }
64
+ .v-cta {
65
+ background: transparent;
66
+ color: var(--accent);
67
+ border: 1px solid var(--accent);
68
+ padding: 14px 26px;
69
+ font-family: var(--vocab-body);
70
+ font-weight: 700;
71
+ letter-spacing: 0.18em;
72
+ text-transform: uppercase;
73
+ font-size: 12px;
74
+ box-shadow: 0 0 0 0 var(--accent), inset 0 0 0 0 var(--accent);
75
+ transition: box-shadow .15s, color .15s;
76
+ }
77
+ .v-cta:hover { color: var(--paper); box-shadow: 0 0 24px var(--accent), inset 0 0 0 2em var(--accent); }
78
+ .v-pill {
79
+ font-family: var(--vocab-body);
80
+ font-size: 10px;
81
+ letter-spacing: 0.2em;
82
+ text-transform: uppercase;
83
+ color: var(--accent-alt);
84
+ border: 1px solid var(--accent-alt);
85
+ padding: 3px 8px;
86
+ box-shadow: 0 0 10px rgba(0,240,255,0.3);
87
+ }
88
+ .v-mark { color: var(--accent); }
89
+ a { color: var(--accent-alt); text-decoration: none; border-bottom: 1px dashed currentColor; }
90
+ a:hover { color: var(--accent); }
91
+ `,
92
+ };
@@ -0,0 +1,75 @@
1
+ // Editorial — broadsheet typography, generous whitespace, ink-on-paper.
2
+ // References: NYT Magazine, The Atlantic redesigns, Bloomberg Businessweek.
3
+
4
+ export const editorial = {
5
+ name: 'Editorial',
6
+ blurb: 'Broadsheet serifs, generous whitespace, ink on paper.',
7
+ fonts: {
8
+ display: { family: 'Instrument Serif', weights: [400], import: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap' },
9
+ body: { family: 'EB Garamond', weights: [400, 500, 700], import: 'https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500;700&display=swap' },
10
+ },
11
+ tokens: {
12
+ paper: '#f7f5ef',
13
+ ink: '#141414',
14
+ inkSoft: '#555049',
15
+ accent: '#a52a2a',
16
+ rule: '#d8d3c4',
17
+ radius: '0px',
18
+ radiusLg: '0px',
19
+ shadow: 'none',
20
+ shadowSm: 'none',
21
+ spacingUnit: 8,
22
+ container: '760px',
23
+ rhythm: 1.7,
24
+ },
25
+ css: `
26
+ :root {
27
+ --vocab-display: 'Instrument Serif', 'Times New Roman', serif;
28
+ --vocab-body: 'EB Garamond', 'Garamond', serif;
29
+ }
30
+ body {
31
+ background: var(--paper);
32
+ color: var(--ink);
33
+ font-family: var(--vocab-body);
34
+ font-size: 19px;
35
+ line-height: 1.65;
36
+ letter-spacing: 0.005em;
37
+ }
38
+ .v-display, h1, h2, h3 {
39
+ font-family: var(--vocab-display);
40
+ font-weight: 400;
41
+ letter-spacing: -0.005em;
42
+ line-height: 1.05;
43
+ }
44
+ .v-display em, h1 em, h2 em, h3 em {
45
+ font-style: italic;
46
+ color: var(--accent);
47
+ }
48
+ .v-card { border-top: 1px solid var(--rule); padding-top: 28px; }
49
+ .v-rule { border: 0; height: 1px; background: var(--rule); margin: 32px 0; }
50
+ .v-cta {
51
+ background: transparent;
52
+ color: var(--ink);
53
+ border-bottom: 2px solid var(--accent);
54
+ padding: 4px 0;
55
+ font-family: var(--vocab-display);
56
+ font-style: italic;
57
+ font-size: 22px;
58
+ transition: color .15s;
59
+ }
60
+ .v-cta:hover { color: var(--accent); }
61
+ .v-pill {
62
+ font-family: var(--vocab-body);
63
+ font-size: 11px;
64
+ letter-spacing: 0.18em;
65
+ text-transform: uppercase;
66
+ color: var(--ink-soft);
67
+ }
68
+ .v-mark {
69
+ font-style: italic;
70
+ color: var(--accent);
71
+ }
72
+ a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 1px; }
73
+ a:hover { border-bottom-color: var(--accent); color: var(--accent); }
74
+ `,
75
+ };
@@ -0,0 +1,35 @@
1
+ // Design vocabularies — opinionated token overlays applied during `designlang remix`.
2
+ //
3
+ // A vocabulary is a self-contained set of tokens + signature CSS that imposes
4
+ // a visual language on top of the page-shape (sections, voice, anatomy)
5
+ // extracted from a real URL. The output is a single HTML file: "what would
6
+ // stripe.com look like if it had been designed brutalist?"
7
+
8
+ import { brutalist } from './brutalist.js';
9
+ import { swiss } from './swiss.js';
10
+ import { artDeco } from './art-deco.js';
11
+ import { cyberpunk } from './cyberpunk.js';
12
+ import { softUi } from './soft-ui.js';
13
+ import { editorial } from './editorial.js';
14
+
15
+ export const VOCABULARIES = {
16
+ brutalist,
17
+ swiss,
18
+ 'art-deco': artDeco,
19
+ cyberpunk,
20
+ 'soft-ui': softUi,
21
+ editorial,
22
+ };
23
+
24
+ export function listVocabularies() {
25
+ return Object.entries(VOCABULARIES).map(([id, v]) => ({ id, name: v.name, blurb: v.blurb }));
26
+ }
27
+
28
+ export function getVocabulary(id) {
29
+ const v = VOCABULARIES[id];
30
+ if (!v) {
31
+ const available = Object.keys(VOCABULARIES).join(', ');
32
+ throw new Error(`unknown vocabulary "${id}" — available: ${available}`);
33
+ }
34
+ return v;
35
+ }
@@ -0,0 +1,83 @@
1
+ // Soft UI — neumorphism reborn. Cushioned shapes, low contrast, single hue.
2
+ // References: Apple Vision OS chrome, modern dashboard work, Spline UI demos.
3
+
4
+ export const softUi = {
5
+ name: 'Soft UI',
6
+ blurb: 'Cushioned shapes, low contrast, single hue. Vision-OS-adjacent.',
7
+ fonts: {
8
+ display: { family: 'Manrope', weights: [400, 600, 800], import: 'https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;800&display=swap' },
9
+ body: { family: 'Manrope', weights: [400, 500, 600], import: '' },
10
+ },
11
+ tokens: {
12
+ paper: '#eef0f7',
13
+ ink: '#1a1f2e',
14
+ inkSoft: '#6b7280',
15
+ accent: '#6366f1',
16
+ rule: 'rgba(26,31,46,0.08)',
17
+ radius: '14px',
18
+ radiusLg: '24px',
19
+ shadow: '12px 12px 32px rgba(160,170,200,0.45), -12px -12px 32px rgba(255,255,255,0.9)',
20
+ shadowSm: '6px 6px 14px rgba(160,170,200,0.35), -6px -6px 14px rgba(255,255,255,0.85)',
21
+ spacingUnit: 8,
22
+ container: '1100px',
23
+ rhythm: 1.55,
24
+ },
25
+ css: `
26
+ :root {
27
+ --vocab-display: 'Manrope', -apple-system, sans-serif;
28
+ --vocab-body: 'Manrope', -apple-system, sans-serif;
29
+ }
30
+ body {
31
+ background: var(--paper);
32
+ color: var(--ink);
33
+ font-family: var(--vocab-body);
34
+ font-size: 15px;
35
+ line-height: 1.6;
36
+ letter-spacing: -0.005em;
37
+ }
38
+ .v-display, h1, h2, h3 {
39
+ font-family: var(--vocab-display);
40
+ font-weight: 800;
41
+ letter-spacing: -0.025em;
42
+ line-height: 1.05;
43
+ }
44
+ .v-card {
45
+ background: var(--paper);
46
+ border-radius: var(--radius-lg);
47
+ box-shadow: var(--shadow);
48
+ padding: 28px;
49
+ }
50
+ .v-rule {
51
+ border: 0;
52
+ height: 4px;
53
+ border-radius: 2px;
54
+ background: var(--paper);
55
+ box-shadow: inset 2px 2px 4px rgba(160,170,200,0.4), inset -2px -2px 4px rgba(255,255,255,0.9);
56
+ }
57
+ .v-cta {
58
+ background: var(--accent);
59
+ color: white;
60
+ border-radius: var(--radius);
61
+ padding: 14px 26px;
62
+ font-family: var(--vocab-display);
63
+ font-weight: 600;
64
+ letter-spacing: -0.005em;
65
+ box-shadow: 6px 6px 14px rgba(99,102,241,0.35), -2px -2px 8px rgba(255,255,255,0.4);
66
+ transition: transform .12s, box-shadow .12s;
67
+ }
68
+ .v-cta:hover { transform: translateY(-1px); box-shadow: 8px 10px 20px rgba(99,102,241,0.45), -3px -3px 8px rgba(255,255,255,0.5); }
69
+ .v-cta:active { transform: translateY(1px); box-shadow: inset 4px 4px 8px rgba(0,0,0,0.15); }
70
+ .v-pill {
71
+ background: var(--paper);
72
+ border-radius: 999px;
73
+ padding: 4px 12px;
74
+ font-size: 11px;
75
+ font-weight: 600;
76
+ color: var(--ink-soft);
77
+ box-shadow: inset 2px 2px 4px rgba(160,170,200,0.3), inset -2px -2px 4px rgba(255,255,255,0.9);
78
+ }
79
+ .v-mark { color: var(--accent); font-weight: 700; }
80
+ a { color: var(--accent); text-decoration: none; }
81
+ a:hover { text-decoration: underline; text-underline-offset: 4px; }
82
+ `,
83
+ };
@@ -0,0 +1,60 @@
1
+ // Swiss — international typographic style, restraint, grids, helvetica.
2
+ // References: Müller-Brockmann, Vignelli, post-Bauhaus corporate identity.
3
+
4
+ export const swiss = {
5
+ name: 'Swiss',
6
+ blurb: 'Helvetica, grids, restraint. The post-Bauhaus default.',
7
+ fonts: {
8
+ display: { family: 'Inter', weights: [400, 700, 900], import: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap' },
9
+ body: { family: 'Inter', weights: [400, 500], import: '' },
10
+ },
11
+ tokens: {
12
+ paper: '#ffffff',
13
+ ink: '#111111',
14
+ inkSoft: '#5b5b5b',
15
+ accent: '#d62828',
16
+ rule: '#111111',
17
+ radius: '0px',
18
+ radiusLg: '0px',
19
+ shadow: 'none',
20
+ shadowSm: 'none',
21
+ spacingUnit: 8,
22
+ container: '1180px',
23
+ rhythm: 1.5,
24
+ },
25
+ css: `
26
+ :root {
27
+ --vocab-display: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
28
+ --vocab-body: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
29
+ }
30
+ body {
31
+ background: var(--paper);
32
+ color: var(--ink);
33
+ font-family: var(--vocab-body);
34
+ font-size: 15px;
35
+ line-height: 1.5;
36
+ letter-spacing: -0.005em;
37
+ }
38
+ .v-display, h1, h2, h3 {
39
+ font-family: var(--vocab-display);
40
+ font-weight: 900;
41
+ letter-spacing: -0.025em;
42
+ line-height: 1.0;
43
+ }
44
+ .v-card { border-top: 1px solid var(--ink); padding-top: 24px; }
45
+ .v-rule { border-top: 1px solid var(--ink); }
46
+ .v-cta {
47
+ background: var(--ink);
48
+ color: var(--paper);
49
+ padding: 14px 22px;
50
+ font-family: var(--vocab-display);
51
+ font-weight: 700;
52
+ letter-spacing: -0.005em;
53
+ }
54
+ .v-cta:hover { background: var(--accent); }
55
+ .v-pill { font-family: var(--vocab-body); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-soft); }
56
+ .v-mark { color: var(--accent); }
57
+ a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
58
+ a:hover { color: var(--accent); }
59
+ `,
60
+ };