designlang 12.2.0 → 12.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pack.js ADDED
@@ -0,0 +1,376 @@
1
+ // designlang pack — bundle every emitter output into a single, polished
2
+ // design-system directory. One artifact a designer or dev can clone, drop
3
+ // into a project, or zip up and send to a client.
4
+ //
5
+ // Layout:
6
+ // <host>-design-system/
7
+ // README.md
8
+ // LICENSE.txt
9
+ // tokens/
10
+ // design-tokens.json DTCG (primitive + semantic)
11
+ // tokens.flat.json legacy flat
12
+ // tailwind.config.js
13
+ // variables.css
14
+ // figma-variables.json
15
+ // motion-tokens.json
16
+ // theme.js React theme object
17
+ // components/
18
+ // anatomy.tsx typed React stubs
19
+ // storybook/ runnable Storybook project
20
+ // starter/ minimal Next.js or HTML starter
21
+ // prompts/
22
+ // v0.txt
23
+ // lovable.txt
24
+ // cursor.md
25
+ // claude-artifacts.md
26
+ // recipes/<component>.md …
27
+ // extras/
28
+ // voice.json
29
+ // prompt-pack.md single-file rollup
30
+
31
+ import { mkdirSync, writeFileSync, statSync } from 'fs';
32
+ import { join } from 'path';
33
+
34
+ import { formatDtcgTokens } from './formatters/dtcg-tokens.js';
35
+ import { formatTokens } from './formatters/tokens.js';
36
+ import { formatTailwind } from './formatters/tailwind.js';
37
+ import { formatCssVars } from './formatters/css-vars.js';
38
+ import { formatFigma } from './formatters/figma.js';
39
+ import { formatMotionTokens } from './formatters/motion-tokens.js';
40
+ import { formatReactTheme } from './formatters/theme.js';
41
+ import { formatStorybook } from './formatters/storybook.js';
42
+ import { formatAnatomyStubs } from './extractors/component-anatomy.js';
43
+ import { buildPromptPack } from './formatters/prompt-pack.js';
44
+ import { generateClone } from './clone.js';
45
+
46
+ function nameFromUrl(url) {
47
+ try {
48
+ const u = new URL(url);
49
+ return u.hostname.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
50
+ } catch {
51
+ return 'design-system';
52
+ }
53
+ }
54
+
55
+ function host(url) {
56
+ try { return new URL(url).hostname; } catch { return String(url || ''); }
57
+ }
58
+
59
+ function exists(p) {
60
+ try { statSync(p); return true; } catch { return false; }
61
+ }
62
+
63
+ // Normalise emitter output to a writable string. Different formatters return
64
+ // different shapes (string, object, undefined when feature absent) — coerce
65
+ // here so callers don't have to know.
66
+ function toText(content, fallback = '') {
67
+ if (content == null) return fallback;
68
+ if (typeof content === 'string') return content;
69
+ if (Buffer.isBuffer(content)) return content;
70
+ return JSON.stringify(content, null, 2);
71
+ }
72
+
73
+ function writeFile(path, content) {
74
+ mkdirSync(join(path, '..'), { recursive: true });
75
+ writeFileSync(path, toText(content), 'utf-8');
76
+ }
77
+
78
+ function buildReadme(design, opts) {
79
+ const meta = design.meta || {};
80
+ const hostName = host(meta.url);
81
+ const grade = design.score?.grade || '—';
82
+ const overall = design.score?.overall ?? '—';
83
+ const families = (design.typography?.families || []).map(f => (typeof f === 'string' ? f : f.name)).filter(Boolean).slice(0, 3);
84
+ const colorCount = (design.colors?.all || []).length;
85
+ const spacingBase = design.spacing?.base ?? '—';
86
+ const componentLib = design.componentLibrary?.library || 'unknown';
87
+ const stack = design.stack?.framework || 'unknown';
88
+
89
+ return `# ${hostName} — design system pack
90
+
91
+ > Built from \`${meta.url || ''}\` on ${new Date(meta.timestamp || Date.now()).toISOString().slice(0, 10)} by [designlang](https://designlang.app) v${opts.version || ''}.
92
+
93
+ A single, polished bundle of every artifact designlang emits for ${hostName}: tokens, components, a runnable Storybook, a minimal starter, and paste-ready prompts for v0 / Lovable / Cursor / Claude Artifacts.
94
+
95
+ ## At a glance
96
+
97
+ - **Grade:** ${grade} (${overall}/100)
98
+ - **Stack:** ${stack} · component library: ${componentLib}
99
+ - **Type families:** ${families.length ? families.join(', ') : '—'}
100
+ - **Palette:** ${colorCount} colors
101
+ - **Spacing base:** ${spacingBase}
102
+
103
+ ## What's in this pack
104
+
105
+ \`\`\`
106
+ ${nameFromUrl(meta.url || '')}-design-system/
107
+ ├── README.md ← you are here
108
+ ├── LICENSE.txt
109
+ ├── tokens/ ← DTCG + Tailwind + CSS vars + Figma vars + motion + theme.js
110
+ ├── components/ ← typed React stubs (anatomy.tsx)
111
+ ├── storybook/ ← runnable Storybook project
112
+ ├── starter/ ← minimal starter app
113
+ ├── prompts/ ← v0 · Lovable · Cursor · Claude Artifacts
114
+ └── extras/ ← voice fingerprint + recipe cards
115
+ \`\`\`
116
+
117
+ ## Install the tokens
118
+
119
+ ### Tailwind
120
+
121
+ \`\`\`js
122
+ // tailwind.config.js
123
+ import config from './tokens/tailwind.config.js';
124
+ export default config;
125
+ \`\`\`
126
+
127
+ ### CSS variables
128
+
129
+ \`\`\`html
130
+ <link rel="stylesheet" href="tokens/variables.css">
131
+ \`\`\`
132
+
133
+ ### Figma
134
+
135
+ In Figma → Variables panel → import \`tokens/figma-variables.json\`.
136
+
137
+ ### Storybook
138
+
139
+ \`\`\`bash
140
+ cd storybook && npm install && npm run storybook
141
+ \`\`\`
142
+
143
+ ## Provenance
144
+
145
+ This pack was extracted from a publicly-accessible URL and represents the *observable design language* of that site at the time of capture. Token values are inferred from computed styles — no source files were accessed. See \`LICENSE.txt\` for usage guidance.
146
+
147
+ Re-pack at any time:
148
+
149
+ \`\`\`bash
150
+ npx designlang pack ${hostName}
151
+ \`\`\`
152
+ `;
153
+ }
154
+
155
+ function buildLicense(design) {
156
+ const hostName = host(design.meta?.url);
157
+ const date = new Date(design.meta?.timestamp || Date.now()).toISOString().slice(0, 10);
158
+ return `Design System Pack — Provenance
159
+ ================================
160
+
161
+ Source: ${design.meta?.url || hostName}
162
+ Captured: ${date}
163
+ Tool: designlang (https://designlang.app, MIT)
164
+
165
+ The token values, type scale, spacing system, and component anatomy in
166
+ this pack were inferred from the publicly-accessible computed styles of
167
+ the source URL via a headless browser. No source files, proprietary
168
+ assets, or copyrighted media were accessed or included.
169
+
170
+ You are free to use these values as a starting point, reference, or
171
+ inspiration. The packaging itself (this README, the bundle layout, the
172
+ emitter output formats) is released under MIT by the designlang project.
173
+
174
+ Trademarks, logos, brand names, and other identifiable assets of the
175
+ source remain the property of their respective owners and are not
176
+ licensed by this pack. Do not pass off this pack as the original site's
177
+ official design system without permission.
178
+ `;
179
+ }
180
+
181
+ function buildStarter(design) {
182
+ // Simple, dependency-free HTML starter that consumes tokens/variables.css
183
+ // and emits a hero + button preview. The full clone (Next.js) is left as
184
+ // an opt-in via --with-clone.
185
+ const hostName = host(design.meta?.url);
186
+ const families = (design.typography?.families || []).map(f => (typeof f === 'string' ? f : f.name)).filter(Boolean);
187
+ const display = families[0] || 'system-ui';
188
+ const heading = (design.voice?.sampleHeadings || [])[0] || `Built from ${hostName}`;
189
+ return `<!doctype html>
190
+ <html lang="en">
191
+ <head>
192
+ <meta charset="utf-8">
193
+ <title>${hostName} starter</title>
194
+ <link rel="stylesheet" href="../tokens/variables.css">
195
+ <style>
196
+ body {
197
+ margin: 0;
198
+ font-family: ${JSON.stringify(display)}, -apple-system, BlinkMacSystemFont, sans-serif;
199
+ background: var(--color-background, #fff);
200
+ color: var(--color-text, #111);
201
+ line-height: 1.5;
202
+ }
203
+ main { max-width: 840px; margin: 0 auto; padding: 64px 32px; }
204
+ h1 { font-size: clamp(40px, 6vw, 72px); line-height: 1.05; margin: 0 0 18px; }
205
+ p { font-size: 18px; max-width: 56ch; color: var(--color-text-secondary, #555); }
206
+ .cta {
207
+ display: inline-block;
208
+ padding: 14px 28px;
209
+ margin-top: 28px;
210
+ background: var(--color-primary, #0a0a0a);
211
+ color: var(--color-on-primary, #fff);
212
+ border-radius: var(--radius-md, 8px);
213
+ text-decoration: none;
214
+ font-weight: 600;
215
+ }
216
+ .cta:hover { opacity: 0.9; }
217
+ .meta { margin-top: 64px; font-size: 12px; color: #888; }
218
+ .meta a { color: inherit; }
219
+ </style>
220
+ </head>
221
+ <body>
222
+ <main>
223
+ <h1>${heading}</h1>
224
+ <p>This starter is wired to the tokens in <code>tokens/variables.css</code>. Edit the variables and watch this page change. Drop in your own components and use the same tokens to keep the visual language consistent.</p>
225
+ <a class="cta" href="#">Get started</a>
226
+ <p class="meta">Generated by <a href="https://designlang.app">designlang</a>. Re-pack with <code>npx designlang pack ${hostName}</code>.</p>
227
+ </main>
228
+ </body>
229
+ </html>
230
+ `;
231
+ }
232
+
233
+ function buildPromptPackDocument(design) {
234
+ const pack = buildPromptPack(design);
235
+ const lines = [
236
+ '# Prompt pack',
237
+ '',
238
+ `Paste-ready prompts for ${host(design.meta?.url)}. Use the variant that matches your tool.`,
239
+ '',
240
+ ];
241
+ for (const [name, body] of Object.entries(pack)) {
242
+ if (name === 'recipes') continue;
243
+ const text = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
244
+ lines.push(`## ${name}\n`, '```', text.trim(), '```\n');
245
+ }
246
+ if (Array.isArray(pack.recipes) && pack.recipes.length) {
247
+ lines.push('## Recipes\n');
248
+ for (const recipe of pack.recipes) {
249
+ const text = typeof recipe.content === 'string' ? recipe.content : JSON.stringify(recipe.content, null, 2);
250
+ lines.push(`### ${recipe.name || 'recipe'}\n`, text.trim(), '\n');
251
+ }
252
+ }
253
+ return lines.join('\n');
254
+ }
255
+
256
+ /**
257
+ * Build a design-system pack on disk.
258
+ *
259
+ * @param {object} design — full design from extractDesignLanguage()
260
+ * @param {object} opts
261
+ * @param {string} opts.outDir — where to write the pack
262
+ * @param {string} opts.version — designlang version, embedded in README
263
+ * @param {boolean} [opts.withClone=true] — include the Next.js starter
264
+ * @returns {object} { dir, files: string[] }
265
+ */
266
+ export function buildPack(design, opts = {}) {
267
+ const outDir = opts.outDir;
268
+ if (!outDir) throw new Error('pack: outDir is required');
269
+ const written = [];
270
+
271
+ mkdirSync(outDir, { recursive: true });
272
+
273
+ // Top-level
274
+ const readmePath = join(outDir, 'README.md');
275
+ writeFile(readmePath, buildReadme(design, opts));
276
+ written.push(readmePath);
277
+
278
+ const licensePath = join(outDir, 'LICENSE.txt');
279
+ writeFile(licensePath, buildLicense(design));
280
+ written.push(licensePath);
281
+
282
+ // tokens/
283
+ const tokensDir = join(outDir, 'tokens');
284
+ mkdirSync(tokensDir, { recursive: true });
285
+
286
+ // Each formatter has a different return shape — toText() (called by
287
+ // writeFile) handles both objects and pre-stringified JSON correctly.
288
+ // formatDtcgTokens returns an object; formatTokens / formatFigma /
289
+ // formatMotionTokens return JSON strings directly; formatTailwind /
290
+ // formatCssVars / formatReactTheme return code strings.
291
+ const tokenWrites = [
292
+ ['design-tokens.json', formatDtcgTokens(design)],
293
+ ['tokens.flat.json', formatTokens(design)],
294
+ ['tailwind.config.js', formatTailwind(design)],
295
+ ['variables.css', formatCssVars(design)],
296
+ ['figma-variables.json', formatFigma(design)],
297
+ ['motion-tokens.json', formatMotionTokens(design.motion || {})],
298
+ ['theme.js', formatReactTheme(design)],
299
+ ];
300
+ for (const [name, content] of tokenWrites) {
301
+ const p = join(tokensDir, name);
302
+ writeFile(p, content);
303
+ written.push(p);
304
+ }
305
+
306
+ // components/
307
+ const componentsDir = join(outDir, 'components');
308
+ mkdirSync(componentsDir, { recursive: true });
309
+ const anatomyPath = join(componentsDir, 'anatomy.tsx');
310
+ writeFile(anatomyPath, formatAnatomyStubs(design.componentAnatomy || []));
311
+ written.push(anatomyPath);
312
+
313
+ // storybook/
314
+ const sbFiles = formatStorybook(design);
315
+ for (const [relPath, content] of Object.entries(sbFiles)) {
316
+ const p = join(outDir, 'storybook', relPath);
317
+ mkdirSync(join(p, '..'), { recursive: true });
318
+ writeFile(p, content);
319
+ written.push(p);
320
+ }
321
+
322
+ // starter/ — minimal HTML by default; full Next.js when --with-clone
323
+ const starterDir = join(outDir, 'starter');
324
+ if (opts.withClone) {
325
+ try {
326
+ generateClone(design, starterDir);
327
+ written.push(starterDir);
328
+ } catch (err) {
329
+ // Clone is best-effort — fall back to HTML starter if it errors.
330
+ mkdirSync(starterDir, { recursive: true });
331
+ writeFile(join(starterDir, 'index.html'), buildStarter(design));
332
+ written.push(join(starterDir, 'index.html'));
333
+ }
334
+ } else {
335
+ mkdirSync(starterDir, { recursive: true });
336
+ const starterPath = join(starterDir, 'index.html');
337
+ writeFile(starterPath, buildStarter(design));
338
+ written.push(starterPath);
339
+ }
340
+
341
+ // prompts/
342
+ const pack = buildPromptPack(design);
343
+ const promptsDir = join(outDir, 'prompts');
344
+ mkdirSync(promptsDir, { recursive: true });
345
+ for (const [name, content] of Object.entries(pack)) {
346
+ if (name === 'recipes') continue;
347
+ const p = join(promptsDir, name);
348
+ writeFile(p, content);
349
+ written.push(p);
350
+ }
351
+ if (Array.isArray(pack.recipes) && pack.recipes.length) {
352
+ const recipesDir = join(promptsDir, 'recipes');
353
+ mkdirSync(recipesDir, { recursive: true });
354
+ for (const recipe of pack.recipes) {
355
+ // Each recipe = { name, content }; sanitise name to a safe filename.
356
+ const safeName = String(recipe.name || 'recipe').replace(/[^a-z0-9-_]+/gi, '-').toLowerCase();
357
+ const p = join(recipesDir, `${safeName}.md`);
358
+ writeFile(p, recipe.content);
359
+ written.push(p);
360
+ }
361
+ }
362
+
363
+ // extras/
364
+ const extrasDir = join(outDir, 'extras');
365
+ mkdirSync(extrasDir, { recursive: true });
366
+ if (design.voice) {
367
+ const p = join(extrasDir, 'voice.json');
368
+ writeFile(p, JSON.stringify(design.voice, null, 2));
369
+ written.push(p);
370
+ }
371
+ const promptDocPath = join(extrasDir, 'prompt-pack.md');
372
+ writeFile(promptDocPath, buildPromptPackDocument(design));
373
+ written.push(promptDocPath);
374
+
375
+ return { dir: outDir, files: written };
376
+ }
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
+ };