designlang 12.3.0 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +8 -5
- package/bin/design-extract.js +53 -0
- package/package.json +1 -1
- package/src/pack.js +376 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.4.0] — 2026-05-05
|
|
4
|
+
|
|
5
|
+
**Pack — one command, one polished design-system bundle.**
|
|
6
|
+
|
|
7
|
+
\`designlang pack <url>\` runs the full extraction once and writes every
|
|
8
|
+
emitter output into a single, signed, layered directory. One artifact a
|
|
9
|
+
designer or dev can clone, drop into a project, or zip up and send to a
|
|
10
|
+
client. Closes [#59](https://github.com/Manavarya09/design-extract/issues/59).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- New CLI command: \`designlang pack <url> [-o <dir>] [--with-clone] [--open]\`
|
|
15
|
+
- New module \`src/pack.js\` exporting \`buildPack(design, opts)\`
|
|
16
|
+
- Layout:
|
|
17
|
+
\`\`\`
|
|
18
|
+
<host>-design-system/
|
|
19
|
+
├── README.md — bespoke, "Built from <host>" + grade + at-a-glance stats
|
|
20
|
+
├── LICENSE.txt — provenance + usage guidance
|
|
21
|
+
├── tokens/ — DTCG + Tailwind + CSS vars + Figma vars + motion + theme.js
|
|
22
|
+
├── components/ — typed React stubs (anatomy.tsx)
|
|
23
|
+
├── storybook/ — runnable Storybook project
|
|
24
|
+
├── starter/ — minimal HTML starter wired to tokens/variables.css
|
|
25
|
+
├── prompts/ — v0.txt · lovable.txt · cursor.md · claude-artifacts.md
|
|
26
|
+
│ └── recipes/ — recipe-<component>.md cards (named, no longer indexed)
|
|
27
|
+
└── extras/ — voice.json + prompt-pack.md rollup
|
|
28
|
+
\`\`\`
|
|
29
|
+
- 7 new tests covering directory shape, valid JSON outputs, README content,
|
|
30
|
+
starter wiring, recipe filenames, and a regression test for the
|
|
31
|
+
double-stringify bug that broke first integration.
|
|
32
|
+
|
|
33
|
+
### Why
|
|
34
|
+
|
|
35
|
+
The 17+ loose files designlang already emits are the right pieces, but
|
|
36
|
+
asking a user to zip them themselves is friction. \`pack\` is the same
|
|
37
|
+
artifacts as one polished, downloadable, cite-able bundle.
|
|
38
|
+
|
|
3
39
|
## [12.3.0] — 2026-05-05
|
|
4
40
|
|
|
5
41
|
**Remix — restyle any site in a different design vocabulary.**
|
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@ 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 remix stripe.com --
|
|
31
|
-
npx designlang
|
|
32
|
-
npx designlang
|
|
29
|
+
npx designlang pack stripe.com # one polished design-system directory ← v12.4
|
|
30
|
+
npx designlang remix stripe.com --as cyberpunk # restyle in another vocabulary ← v12.3
|
|
31
|
+
npx designlang remix stripe.com --all # emit all 6 vocabs at once ← v12.3
|
|
32
|
+
npx designlang grade https://stripe.com --badge # report card + SVG badge ← v12.2
|
|
33
|
+
npx designlang battle stripe.com vercel.com # head-to-head graded fight ← v12.2
|
|
33
34
|
npx designlang clone https://stripe.com # working Next.js starter
|
|
34
35
|
npx designlang --full https://stripe.com # screenshots + responsive + interactions
|
|
35
36
|
```
|
|
@@ -129,7 +130,8 @@ designlang mcp # stdio MCP server for Cursor / Clau
|
|
|
129
130
|
| Grade (v12.1) | `designlang grade <url>` | Shareable HTML "Design Report Card" — letter grade, 8 dimensions, evidence, strengths + fixes |
|
|
130
131
|
| Battle (v12.2) | `designlang battle <A> <B>` | Head-to-head graded battle card with verdict, dimension table, palette comparison |
|
|
131
132
|
| 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 (
|
|
133
|
+
| Remix (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 |
|
|
134
|
+
| Pack (NEW v12.4) | `designlang pack <url>` | Bundle every output (tokens / components / Storybook / starter / prompts) into one polished design-system directory |
|
|
133
135
|
| Watch | `designlang watch <url>` | Monitor for design changes on interval |
|
|
134
136
|
| Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
|
|
135
137
|
| Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
|
|
@@ -185,6 +187,7 @@ Commands:
|
|
|
185
187
|
grade <url> Generate a shareable HTML Design Report Card (--format html|md|json|svg|all, --badge, --open)
|
|
186
188
|
battle <urlA> <urlB> Head-to-head graded battle card (--format html|md|json|all, --open)
|
|
187
189
|
remix <url> Restyle in another vocabulary (--as brutalist|swiss|art-deco|cyberpunk|soft-ui|editorial, --all, --list, --open)
|
|
190
|
+
pack <url> Bundle every output into one design-system directory (--with-clone, --open)
|
|
188
191
|
watch <url> Monitor for design changes on interval
|
|
189
192
|
diff <urlA> <urlB> Compare two sites' design languages
|
|
190
193
|
brands <urls...> Multi-brand comparison matrix
|
package/bin/design-extract.js
CHANGED
|
@@ -49,6 +49,7 @@ import { formatBattle, formatBattleMarkdown } from '../src/formatters/battle.js'
|
|
|
49
49
|
import { formatScoreBadge } from '../src/formatters/badge.js';
|
|
50
50
|
import { formatRemix } from '../src/formatters/remix.js';
|
|
51
51
|
import { VOCABULARIES, getVocabulary, listVocabularies } from '../src/vocabularies/index.js';
|
|
52
|
+
import { buildPack } from '../src/pack.js';
|
|
52
53
|
import { nameFromUrl } from '../src/utils.js';
|
|
53
54
|
|
|
54
55
|
function validateUrl(url) {
|
|
@@ -1172,6 +1173,58 @@ program
|
|
|
1172
1173
|
}
|
|
1173
1174
|
});
|
|
1174
1175
|
|
|
1176
|
+
// ── Pack command — bundle every emitter into one design-system directory
|
|
1177
|
+
program
|
|
1178
|
+
.command('pack <url>')
|
|
1179
|
+
.description('Bundle every output (tokens, components, storybook, prompts, starter) into a single design-system directory')
|
|
1180
|
+
.option('-o, --out <dir>', 'output directory (default: ./<host>-design-system)')
|
|
1181
|
+
.option('--with-clone', 'include the full Next.js clone as the starter (slower; otherwise emits a minimal HTML starter)')
|
|
1182
|
+
.option('--open', 'open the starter index.html in the default browser')
|
|
1183
|
+
.action(async (url, opts) => {
|
|
1184
|
+
if (!url.startsWith('http')) url = `https://${url}`;
|
|
1185
|
+
validateUrl(url);
|
|
1186
|
+
|
|
1187
|
+
const spinner = ora(`Extracting ${url}...`).start();
|
|
1188
|
+
try {
|
|
1189
|
+
const design = await extractDesignLanguage(url);
|
|
1190
|
+
|
|
1191
|
+
const defaultDirName = `${nameFromUrl(url)}-design-system`;
|
|
1192
|
+
const outDir = resolve(opts.out || defaultDirName);
|
|
1193
|
+
|
|
1194
|
+
spinner.text = 'Packing artifacts...';
|
|
1195
|
+
const { files } = buildPack(design, {
|
|
1196
|
+
outDir,
|
|
1197
|
+
version: PKG_VERSION,
|
|
1198
|
+
withClone: !!opts.withClone,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
spinner.stop();
|
|
1202
|
+
console.log('');
|
|
1203
|
+
console.log(` ${chalk.bold('Packed')} ${chalk.gray('·')} ${chalk.cyan(files.length)} files ${chalk.gray('·')} ${chalk.gray(url)}`);
|
|
1204
|
+
console.log('');
|
|
1205
|
+
console.log(` ${chalk.green('✓')} ${chalk.bold(outDir)}`);
|
|
1206
|
+
console.log('');
|
|
1207
|
+
console.log(chalk.gray(' Top-level layout:'));
|
|
1208
|
+
const top = ['README.md', 'LICENSE.txt', 'tokens/', 'components/', 'storybook/', 'starter/', 'prompts/', 'extras/'];
|
|
1209
|
+
for (const t of top) console.log(` ${chalk.gray('·')} ${t}`);
|
|
1210
|
+
console.log('');
|
|
1211
|
+
console.log(chalk.gray(` Zip it: cd ${outDir} && zip -r ../${defaultDirName}.zip .`));
|
|
1212
|
+
console.log(chalk.gray(` Storybook: cd ${outDir}/storybook && npm install && npm run storybook`));
|
|
1213
|
+
console.log('');
|
|
1214
|
+
|
|
1215
|
+
if (opts.open) {
|
|
1216
|
+
const starter = join(outDir, 'starter', 'index.html');
|
|
1217
|
+
const { spawn } = await import('child_process');
|
|
1218
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1219
|
+
spawn(cmd, [starter], { detached: true, stdio: 'ignore' }).unref();
|
|
1220
|
+
}
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
spinner.fail('Pack failed');
|
|
1223
|
+
console.error(chalk.red(`\n ${err.message}\n`));
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1175
1228
|
// ── Apply command ──────────────────────────────────────────
|
|
1176
1229
|
program
|
|
1177
1230
|
.command('apply <url>')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.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/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
|
+
}
|