designlang 12.1.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 +71 -0
- package/README.md +23 -6
- package/bin/design-extract.js +164 -2
- package/package.json +1 -1
- package/src/chat.js +0 -14
- package/src/formatters/badge.js +90 -0
- package/src/formatters/battle.js +338 -0
- 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,76 @@
|
|
|
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
|
+
|
|
43
|
+
## [12.2.0] — 2026-05-02
|
|
44
|
+
|
|
45
|
+
**Battle cards + design score badges — distribution + virality on top of Grade.**
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`designlang battle <urlA> <urlB>`** — head-to-head graded battle card.
|
|
50
|
+
Single shareable HTML pitting two sites against each other, dimension by
|
|
51
|
+
dimension, with a verdict line and a per-dimension bar table. Both sites
|
|
52
|
+
are extracted in parallel. Emits `*.battle.html`, `*.battle.md`, and
|
|
53
|
+
`*.battle.json`.
|
|
54
|
+
- **`designlang grade --badge`** — also emit `*.grade.svg`, a shields.io-style
|
|
55
|
+
SVG badge (`design · B · 87`) coloured by letter grade. Drop into any
|
|
56
|
+
README.
|
|
57
|
+
- **Live badge endpoint** at `https://designlang.app/badge/<host>.svg` (with
|
|
58
|
+
rewrites from `/badge/<host>` and `/api/badge/<host>`). Reuses the same
|
|
59
|
+
blob cache the `/api/extract` route writes to, so the first hit warms the
|
|
60
|
+
cache and every subsequent hit is served from edge cache in ~50ms. 6h
|
|
61
|
+
fresh / 24h stale-while-revalidate / 7d max — friendly to the GitHub image
|
|
62
|
+
proxy.
|
|
63
|
+
- New formatters: `src/formatters/battle.js`, `src/formatters/badge.js`,
|
|
64
|
+
with exports `formatBattle`, `formatBattleMarkdown`, `compareScores`,
|
|
65
|
+
`formatBadge`, `formatScoreBadge`.
|
|
66
|
+
- 13 new tests (battle markup, score comparison thresholds, SVG escaping,
|
|
67
|
+
grade-color mapping, missing-score handling).
|
|
68
|
+
|
|
69
|
+
Why this exists: the v12.1 Grade card was the differentiator. Battle is the
|
|
70
|
+
viral content layer ("Stripe vs Vercel — guess who lost"). The badge is the
|
|
71
|
+
distribution layer — every README that adopts it is a permanent backlink to
|
|
72
|
+
a public grade page.
|
|
73
|
+
|
|
3
74
|
## [12.1.0] — 2026-04-29
|
|
4
75
|
|
|
5
76
|
**Design Report Card — a shareable audit page, generated from any URL.**
|
package/README.md
CHANGED
|
@@ -25,10 +25,19 @@ It also goes where extractors don't: **layout patterns**, **responsive behavior
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx designlang https://stripe.com
|
|
29
|
-
npx designlang
|
|
30
|
-
npx designlang
|
|
31
|
-
npx designlang
|
|
28
|
+
npx designlang https://stripe.com # extract everything
|
|
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
|
|
33
|
+
npx designlang clone https://stripe.com # working Next.js starter
|
|
34
|
+
npx designlang --full https://stripe.com # screenshots + responsive + interactions
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Drop a live design-score badge in any README:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+

|
|
32
41
|
```
|
|
33
42
|
|
|
34
43
|
## Install
|
|
@@ -56,6 +65,9 @@ Each run writes 17+ files to `./design-extract-output/`. The headline outputs:
|
|
|
56
65
|
| `*-prompts/` | Paste-ready prompts for v0, Lovable, Cursor, Claude Artifacts |
|
|
57
66
|
| `*-mcp.json` | Disk-backed MCP server payload |
|
|
58
67
|
| `*-grade.html` | **v12.1** Shareable Design Report Card (letter grade + evidence) |
|
|
68
|
+
| `*-grade.svg` | **v12.2** Shields.io-style design-score badge (drop into any README) |
|
|
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 |
|
|
59
71
|
|
|
60
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.
|
|
61
73
|
|
|
@@ -114,7 +126,10 @@ designlang mcp # stdio MCP server for Cursor / Clau
|
|
|
114
126
|
| Apply | `designlang apply <url>` | Auto-detect framework and write tokens to your project |
|
|
115
127
|
| Clone | `designlang clone <url>` | Generate a working Next.js starter with extracted design |
|
|
116
128
|
| Score | `designlang score <url>` | Rate design quality with visual bar chart breakdown |
|
|
117
|
-
| Grade (
|
|
129
|
+
| Grade (v12.1) | `designlang grade <url>` | Shareable HTML "Design Report Card" — letter grade, 8 dimensions, evidence, strengths + fixes |
|
|
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 |
|
|
118
133
|
| Watch | `designlang watch <url>` | Monitor for design changes on interval |
|
|
119
134
|
| Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
|
|
120
135
|
| Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
|
|
@@ -167,7 +182,9 @@ Commands:
|
|
|
167
182
|
apply <url> Extract and apply design directly to your project
|
|
168
183
|
clone <url> Generate a working Next.js starter from extracted design
|
|
169
184
|
score <url> Rate design quality (7 categories, A-F, bar chart)
|
|
170
|
-
grade <url> Generate a shareable HTML Design Report Card (--format html|md|json|all, --open)
|
|
185
|
+
grade <url> Generate a shareable HTML Design Report Card (--format html|md|json|svg|all, --badge, --open)
|
|
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)
|
|
171
188
|
watch <url> Monitor for design changes on interval
|
|
172
189
|
diff <urlA> <urlB> Compare two sites' design languages
|
|
173
190
|
brands <urls...> Multi-brand comparison matrix
|
package/bin/design-extract.js
CHANGED
|
@@ -43,9 +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';
|
|
48
|
+
import { formatBattle, formatBattleMarkdown } from '../src/formatters/battle.js';
|
|
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';
|
|
49
52
|
import { nameFromUrl } from '../src/utils.js';
|
|
50
53
|
|
|
51
54
|
function validateUrl(url) {
|
|
@@ -941,7 +944,8 @@ program
|
|
|
941
944
|
.description('Generate a shareable Design Report Card (HTML + JSON + Markdown)')
|
|
942
945
|
.option('-o, --out <dir>', 'output directory', './design-extract-output')
|
|
943
946
|
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
|
|
944
|
-
.option('--format <fmt>', 'output format: html, md, json, all', 'all')
|
|
947
|
+
.option('--format <fmt>', 'output format: html, md, json, svg, all', 'all')
|
|
948
|
+
.option('--badge', 'also emit *-badge.svg (shields.io-style) — implies adding svg to format')
|
|
945
949
|
.option('--open', 'open the HTML report in the default browser')
|
|
946
950
|
.action(async (url, opts) => {
|
|
947
951
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
@@ -957,6 +961,7 @@ program
|
|
|
957
961
|
mkdirSync(outDir, { recursive: true });
|
|
958
962
|
const prefix = opts.name || nameFromUrl(url);
|
|
959
963
|
const written = [];
|
|
964
|
+
const wantSvg = opts.badge || opts.format === 'svg' || opts.format === 'all';
|
|
960
965
|
|
|
961
966
|
if (opts.format === 'all' || opts.format === 'html') {
|
|
962
967
|
const html = formatGrade(design, { version: PKG_VERSION });
|
|
@@ -984,6 +989,12 @@ program
|
|
|
984
989
|
}, null, 2));
|
|
985
990
|
written.push(p);
|
|
986
991
|
}
|
|
992
|
+
if (wantSvg) {
|
|
993
|
+
const svg = formatScoreBadge(s);
|
|
994
|
+
const p = join(outDir, `${prefix}.grade.svg`);
|
|
995
|
+
writeFileSync(p, svg);
|
|
996
|
+
written.push(p);
|
|
997
|
+
}
|
|
987
998
|
|
|
988
999
|
spinner.stop();
|
|
989
1000
|
const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
|
|
@@ -1010,6 +1021,157 @@ program
|
|
|
1010
1021
|
}
|
|
1011
1022
|
});
|
|
1012
1023
|
|
|
1024
|
+
// ── Battle command — head-to-head graded comparison ────────
|
|
1025
|
+
program
|
|
1026
|
+
.command('battle <urlA> <urlB>')
|
|
1027
|
+
.description('Generate a head-to-head graded battle card (HTML + JSON + Markdown)')
|
|
1028
|
+
.option('-o, --out <dir>', 'output directory', './design-extract-output')
|
|
1029
|
+
.option('-n, --name <name>', 'output file prefix (default: a-vs-b)')
|
|
1030
|
+
.option('--format <fmt>', 'output format: html, md, json, all', 'all')
|
|
1031
|
+
.option('--open', 'open the battle card in the default browser')
|
|
1032
|
+
.action(async (urlA, urlB, opts) => {
|
|
1033
|
+
if (!urlA.startsWith('http')) urlA = `https://${urlA}`;
|
|
1034
|
+
if (!urlB.startsWith('http')) urlB = `https://${urlB}`;
|
|
1035
|
+
validateUrl(urlA);
|
|
1036
|
+
validateUrl(urlB);
|
|
1037
|
+
|
|
1038
|
+
const spinner = ora(`Auditing ${urlA} and ${urlB} in parallel...`).start();
|
|
1039
|
+
try {
|
|
1040
|
+
const [designA, designB] = await Promise.all([
|
|
1041
|
+
extractDesignLanguage(urlA),
|
|
1042
|
+
extractDesignLanguage(urlB),
|
|
1043
|
+
]);
|
|
1044
|
+
if (!designA.score || !designB.score) throw new Error('scoring failed for one or both sites');
|
|
1045
|
+
|
|
1046
|
+
const outDir = resolve(opts.out);
|
|
1047
|
+
mkdirSync(outDir, { recursive: true });
|
|
1048
|
+
const prefix = opts.name || `${nameFromUrl(urlA)}-vs-${nameFromUrl(urlB)}`;
|
|
1049
|
+
const written = [];
|
|
1050
|
+
|
|
1051
|
+
if (opts.format === 'all' || opts.format === 'html') {
|
|
1052
|
+
const html = formatBattle(designA, designB, { version: PKG_VERSION });
|
|
1053
|
+
const p = join(outDir, `${prefix}.battle.html`);
|
|
1054
|
+
writeFileSync(p, html);
|
|
1055
|
+
written.push(p);
|
|
1056
|
+
}
|
|
1057
|
+
if (opts.format === 'all' || opts.format === 'md') {
|
|
1058
|
+
const md = formatBattleMarkdown(designA, designB);
|
|
1059
|
+
const p = join(outDir, `${prefix}.battle.md`);
|
|
1060
|
+
writeFileSync(p, md);
|
|
1061
|
+
written.push(p);
|
|
1062
|
+
}
|
|
1063
|
+
if (opts.format === 'all' || opts.format === 'json') {
|
|
1064
|
+
const p = join(outDir, `${prefix}.battle.json`);
|
|
1065
|
+
writeFileSync(p, JSON.stringify({
|
|
1066
|
+
a: { url: designA.meta?.url, grade: designA.score.grade, overall: designA.score.overall, scores: designA.score.scores },
|
|
1067
|
+
b: { url: designB.meta?.url, grade: designB.score.grade, overall: designB.score.overall, scores: designB.score.scores },
|
|
1068
|
+
timestamp: new Date().toISOString(),
|
|
1069
|
+
}, null, 2));
|
|
1070
|
+
written.push(p);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
spinner.stop();
|
|
1074
|
+
const aGrade = designA.score.grade, bGrade = designB.score.grade;
|
|
1075
|
+
const aColor = aGrade === 'A' ? chalk.green : aGrade === 'B' ? chalk.cyan : aGrade === 'C' ? chalk.yellow : chalk.red;
|
|
1076
|
+
const bColor = bGrade === 'A' ? chalk.green : bGrade === 'B' ? chalk.cyan : bGrade === 'C' ? chalk.yellow : chalk.red;
|
|
1077
|
+
console.log('');
|
|
1078
|
+
console.log(` ${aColor.bold(`${aGrade} · ${designA.score.overall}`)} ${chalk.gray(designA.meta?.url || urlA)}`);
|
|
1079
|
+
console.log(` ${chalk.gray('vs')}`);
|
|
1080
|
+
console.log(` ${bColor.bold(`${bGrade} · ${designB.score.overall}`)} ${chalk.gray(designB.meta?.url || urlB)}`);
|
|
1081
|
+
console.log('');
|
|
1082
|
+
const winner =
|
|
1083
|
+
designA.score.overall - designB.score.overall >= 3 ? `${chalk.bold(designA.meta?.url || urlA)} wins`
|
|
1084
|
+
: designB.score.overall - designA.score.overall >= 3 ? `${chalk.bold(designB.meta?.url || urlB)} wins`
|
|
1085
|
+
: 'Too close to call';
|
|
1086
|
+
console.log(` Verdict: ${winner}`);
|
|
1087
|
+
console.log('');
|
|
1088
|
+
for (const f of written) console.log(` ${chalk.green('✓')} ${chalk.gray(f)}`);
|
|
1089
|
+
console.log('');
|
|
1090
|
+
|
|
1091
|
+
if (opts.open) {
|
|
1092
|
+
const htmlPath = written.find(p => p.endsWith('.html'));
|
|
1093
|
+
if (htmlPath) {
|
|
1094
|
+
const { spawn } = await import('child_process');
|
|
1095
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1096
|
+
spawn(cmd, [htmlPath], { detached: true, stdio: 'ignore' }).unref();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
spinner.fail('Battle failed');
|
|
1101
|
+
console.error(chalk.red(`\n ${err.message}\n`));
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
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
|
+
|
|
1013
1175
|
// ── Apply command ──────────────────────────────────────────
|
|
1014
1176
|
program
|
|
1015
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,90 @@
|
|
|
1
|
+
// designlang badge — shields.io-style SVG badge for design score.
|
|
2
|
+
// Uses Verdana web-safe font + computed text width so it renders identically
|
|
3
|
+
// on GitHub, npm, and arbitrary markdown. No external font dependency.
|
|
4
|
+
|
|
5
|
+
const COLORS = {
|
|
6
|
+
A: '#0a8a52',
|
|
7
|
+
B: '#1f6feb',
|
|
8
|
+
C: '#b08400',
|
|
9
|
+
D: '#d2691e',
|
|
10
|
+
F: '#c43d3d',
|
|
11
|
+
unknown: '#555',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Verdana 11px character width approximation (em units × 11). Verdana is
|
|
15
|
+
// chosen because it ships natively on every OS and matches shields.io's
|
|
16
|
+
// rendering, so widths are predictable across platforms.
|
|
17
|
+
const VERDANA_WIDTHS = {
|
|
18
|
+
' ': 5, '!': 5, '#': 9, '$': 7, '%': 12, '&': 9, "'": 3, '(': 5, ')': 5,
|
|
19
|
+
'*': 7, '+': 7, ',': 5, '-': 5, '.': 5, '/': 5, '0': 7, '1': 7, '2': 7,
|
|
20
|
+
'3': 7, '4': 7, '5': 7, '6': 7, '7': 7, '8': 7, '9': 7, ':': 5, ';': 5,
|
|
21
|
+
'<': 7, '=': 7, '>': 7, '?': 7, '@': 12, 'A': 8, 'B': 8, 'C': 8, 'D': 9,
|
|
22
|
+
'E': 7, 'F': 7, 'G': 9, 'H': 9, 'I': 5, 'J': 5, 'K': 8, 'L': 7, 'M': 10,
|
|
23
|
+
'N': 9, 'O': 9, 'P': 7, 'Q': 9, 'R': 8, 'S': 7, 'T': 7, 'U': 9, 'V': 8,
|
|
24
|
+
'W': 12, 'X': 8, 'Y': 8, 'Z': 7, '[': 5, ']': 5, '_': 7, '`': 7,
|
|
25
|
+
'a': 7, 'b': 7, 'c': 6, 'd': 7, 'e': 7, 'f': 4, 'g': 7, 'h': 7, 'i': 3,
|
|
26
|
+
'j': 4, 'k': 7, 'l': 3, 'm': 11, 'n': 7, 'o': 7, 'p': 7, 'q': 7, 'r': 5,
|
|
27
|
+
's': 6, 't': 4, 'u': 7, 'v': 7, 'w': 10, 'x': 7, 'y': 7, 'z': 6, '|': 5,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function textWidth(s) {
|
|
31
|
+
let w = 0;
|
|
32
|
+
for (const ch of String(s)) w += VERDANA_WIDTHS[ch] ?? 7;
|
|
33
|
+
return w;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function esc(s) {
|
|
37
|
+
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render a shields.io-style two-section SVG badge.
|
|
42
|
+
* formatBadge({ label: 'design', value: 'B · 87', grade: 'B' })
|
|
43
|
+
*
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {string} opts.label — left-side label (default 'design')
|
|
46
|
+
* @param {string} opts.value — right-side value (e.g. 'B · 87' or 'F · 12')
|
|
47
|
+
* @param {string} [opts.grade] — A–F, controls right-side fill color
|
|
48
|
+
* @param {string} [opts.color] — explicit hex override
|
|
49
|
+
* @returns {string} SVG markup
|
|
50
|
+
*/
|
|
51
|
+
export function formatBadge({ label = 'design', value = '—', grade, color } = {}) {
|
|
52
|
+
const fill = color || COLORS[grade] || COLORS.unknown;
|
|
53
|
+
const labelW = textWidth(label) + 12; // 6px padding each side
|
|
54
|
+
const valueW = textWidth(value) + 12;
|
|
55
|
+
const totalW = labelW + valueW;
|
|
56
|
+
const labelX = labelW / 2;
|
|
57
|
+
const valueX = labelW + valueW / 2;
|
|
58
|
+
|
|
59
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="20" role="img" aria-label="${esc(label)}: ${esc(value)}">
|
|
60
|
+
<title>${esc(label)}: ${esc(value)}</title>
|
|
61
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
62
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
63
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
64
|
+
</linearGradient>
|
|
65
|
+
<clipPath id="r"><rect width="${totalW}" height="20" rx="3" fill="#fff"/></clipPath>
|
|
66
|
+
<g clip-path="url(#r)">
|
|
67
|
+
<rect width="${labelW}" height="20" fill="#555"/>
|
|
68
|
+
<rect x="${labelW}" width="${valueW}" height="20" fill="${fill}"/>
|
|
69
|
+
<rect width="${totalW}" height="20" fill="url(#s)"/>
|
|
70
|
+
</g>
|
|
71
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
|
|
72
|
+
<text aria-hidden="true" x="${labelX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${textWidth(label) * 10}">${esc(label)}</text>
|
|
73
|
+
<text x="${labelX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${textWidth(label) * 10}">${esc(label)}</text>
|
|
74
|
+
<text aria-hidden="true" x="${valueX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${textWidth(value) * 10}">${esc(value)}</text>
|
|
75
|
+
<text x="${valueX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${textWidth(value) * 10}">${esc(value)}</text>
|
|
76
|
+
</g>
|
|
77
|
+
</svg>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Convenience: render a badge directly from a design.score object. */
|
|
81
|
+
export function formatScoreBadge(score, opts = {}) {
|
|
82
|
+
if (!score || !score.grade) {
|
|
83
|
+
return formatBadge({ label: opts.label || 'design', value: '—', grade: 'unknown' });
|
|
84
|
+
}
|
|
85
|
+
return formatBadge({
|
|
86
|
+
label: opts.label || 'design',
|
|
87
|
+
value: `${score.grade} · ${score.overall}`,
|
|
88
|
+
grade: score.grade,
|
|
89
|
+
});
|
|
90
|
+
}
|