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 +40 -0
- package/README.md +9 -4
- package/bin/design-extract.js +71 -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/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
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
|
|
30
|
-
npx designlang
|
|
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 (
|
|
128
|
-
| Badge (
|
|
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
|
package/bin/design-extract.js
CHANGED
|
@@ -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.
|
|
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)) }));
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|