designlang 12.10.1 → 12.11.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 +24 -0
- package/bin/design-extract.js +50 -5
- package/package.json +4 -3
- package/src/formatters/brand-book.js +2 -1
- package/src/pdf.js +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.11.0] — 2026-05-15
|
|
4
|
+
|
|
5
|
+
**`brand --pdf` ships native PDF brand guides.**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx designlang brand stripe.com --pdf
|
|
9
|
+
# → stripe-com.brand.pdf (print-ready, ~3–5s)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
What you get on top of any "save as PDF":
|
|
13
|
+
|
|
14
|
+
- **Per-chapter page breaks** — every section starts on a fresh page.
|
|
15
|
+
- **Running footer** — `designlang · <subject> · <page> of <total>` on every page.
|
|
16
|
+
- **Selectable text + embedded fonts** — never rasterized.
|
|
17
|
+
- **`--paper a4|letter|tabloid` + `--landscape`** for any output target.
|
|
18
|
+
- **`--attach-tokens`** embeds the DTCG tokens JSON *inside* the PDF as a
|
|
19
|
+
proper file attachment — open the PDF in Preview/Acrobat, hit the
|
|
20
|
+
paperclip, drop the JSON straight into Tailwind.
|
|
21
|
+
- **`--no-print-background`** strips the brand-colour cover band for a
|
|
22
|
+
smaller file when you need it.
|
|
23
|
+
|
|
24
|
+
Adds one tiny dep (`pdf-lib`, MIT, ~600KB) used only when `--attach-tokens`
|
|
25
|
+
is passed; lazy-imported behind a dynamic `import()`.
|
|
26
|
+
|
|
3
27
|
## [12.10.1] — 2026-05-13
|
|
4
28
|
|
|
5
29
|
**Tiny ship: \`stats\` now scans multiple URLs at once.**
|
package/bin/design-extract.js
CHANGED
|
@@ -53,6 +53,7 @@ import { buildPack } from '../src/pack.js';
|
|
|
53
53
|
import { recolorDesign } from '../src/recolor.js';
|
|
54
54
|
import { formatThemeSwap, formatThemeSwapMarkdown } from '../src/formatters/theme-swap.js';
|
|
55
55
|
import { formatBrandBook, formatBrandBookMarkdown } from '../src/formatters/brand-book.js';
|
|
56
|
+
import { htmlToPdf } from '../src/pdf.js';
|
|
56
57
|
import { fuseDesigns, AXES } from '../src/fuse.js';
|
|
57
58
|
import { formatPair, formatPairMarkdown } from '../src/formatters/pair.js';
|
|
58
59
|
import { nameFromUrl } from '../src/utils.js';
|
|
@@ -1456,6 +1457,11 @@ program
|
|
|
1456
1457
|
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
|
|
1457
1458
|
.option('--format <fmt>', 'output format: html, md, json, all', 'all')
|
|
1458
1459
|
.option('--open', 'open the HTML book in the default browser')
|
|
1460
|
+
.option('--pdf', 'also emit a print-ready PDF brand guide (chapter bookmarks, running page numbers)')
|
|
1461
|
+
.option('--paper <size>', 'PDF paper size: a4 | letter | tabloid', 'a4')
|
|
1462
|
+
.option('--landscape', 'PDF landscape orientation')
|
|
1463
|
+
.option('--no-print-background', 'strip the brand-colour cover band from the PDF (smaller file)')
|
|
1464
|
+
.option('--attach-tokens', 'embed the DTCG tokens JSON as a PDF file attachment')
|
|
1459
1465
|
.action(async (url, opts) => {
|
|
1460
1466
|
if (!url.startsWith('http')) url = `https://${url}`;
|
|
1461
1467
|
validateUrl(url);
|
|
@@ -1477,11 +1483,14 @@ program
|
|
|
1477
1483
|
const prefix = opts.name || `${nameFromUrl(url)}.brand`;
|
|
1478
1484
|
const written = [];
|
|
1479
1485
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1486
|
+
let bookHtml = null;
|
|
1487
|
+
if (opts.format === 'all' || opts.format === 'html' || opts.pdf) {
|
|
1488
|
+
bookHtml = formatBrandBook(design, { version: PKG_VERSION });
|
|
1489
|
+
if (opts.format === 'all' || opts.format === 'html') {
|
|
1490
|
+
const p = join(outDir, `${prefix}.html`);
|
|
1491
|
+
writeFileSync(p, bookHtml);
|
|
1492
|
+
written.push(p);
|
|
1493
|
+
}
|
|
1485
1494
|
}
|
|
1486
1495
|
if (opts.format === 'all' || opts.format === 'md') {
|
|
1487
1496
|
const md = formatBrandBookMarkdown(design);
|
|
@@ -1515,6 +1524,42 @@ program
|
|
|
1515
1524
|
written.push(p);
|
|
1516
1525
|
}
|
|
1517
1526
|
|
|
1527
|
+
if (opts.pdf) {
|
|
1528
|
+
spinner.text = 'Rendering PDF...';
|
|
1529
|
+
const pdfPath = join(outDir, `${prefix}.pdf`);
|
|
1530
|
+
const attachments = [];
|
|
1531
|
+
if (opts.attachTokens) {
|
|
1532
|
+
// Reuse the JSON we just wrote if available; otherwise build a DTCG payload on the fly.
|
|
1533
|
+
let tokensJson;
|
|
1534
|
+
try {
|
|
1535
|
+
tokensJson = readFileSync(join(outDir, `${prefix}.json`));
|
|
1536
|
+
} catch {
|
|
1537
|
+
tokensJson = Buffer.from(JSON.stringify({
|
|
1538
|
+
colors: design.colors, typography: design.typography,
|
|
1539
|
+
spacing: design.spacing, motion: design.motion,
|
|
1540
|
+
}, null, 2));
|
|
1541
|
+
}
|
|
1542
|
+
attachments.push({
|
|
1543
|
+
filename: `${nameFromUrl(url)}-tokens.json`,
|
|
1544
|
+
contents: tokensJson,
|
|
1545
|
+
mimeType: 'application/json',
|
|
1546
|
+
description: 'Design tokens (DTCG-aligned)',
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
await htmlToPdf(bookHtml, {
|
|
1550
|
+
paper: opts.paper,
|
|
1551
|
+
landscape: !!opts.landscape,
|
|
1552
|
+
printBackground: opts.printBackground !== false,
|
|
1553
|
+
attachments,
|
|
1554
|
+
metadata: {
|
|
1555
|
+
title: `${new URL(url).hostname} brand guidelines`,
|
|
1556
|
+
subject: `${new URL(url).hostname} brand guidelines`,
|
|
1557
|
+
},
|
|
1558
|
+
outPath: pdfPath,
|
|
1559
|
+
});
|
|
1560
|
+
written.push(pdfPath);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1518
1563
|
spinner.stop();
|
|
1519
1564
|
const colorCount = (design.colors?.all || []).length;
|
|
1520
1565
|
const fontCount = (design.typography?.families || []).length;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "12.
|
|
4
|
-
"description": "Extract the complete design language from any website and ship it
|
|
3
|
+
"version": "12.11.0",
|
|
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": {
|
|
7
7
|
"designlang": "./bin/design-extract.js"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"chalk": "^5.3.0",
|
|
18
18
|
"commander": "^12.0.0",
|
|
19
19
|
"ora": "^8.0.0",
|
|
20
|
+
"pdf-lib": "^1.17.1",
|
|
20
21
|
"playwright": "^1.42.0"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
@@ -48,4 +49,4 @@
|
|
|
48
49
|
],
|
|
49
50
|
"author": "masyv",
|
|
50
51
|
"license": "MIT"
|
|
51
|
-
}
|
|
52
|
+
}
|
|
@@ -945,7 +945,8 @@ export function formatBrandBook(design, opts = {}) {
|
|
|
945
945
|
.cover, .toc, section { page-break-inside: avoid; border-color: #ddd; padding: 36px 32px; }
|
|
946
946
|
.cover-band { background-color: var(--accent) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
947
947
|
.toc, .cover { page-break-after: always; }
|
|
948
|
-
section { page-break-after: always; }
|
|
948
|
+
section { page-break-before: always; page-break-after: always; break-before: page; }
|
|
949
|
+
.topbar { display: none !important; }
|
|
949
950
|
.scale-table, .a11y-pair, .component-card { page-break-inside: avoid; }
|
|
950
951
|
}
|
|
951
952
|
</style>
|
package/src/pdf.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function htmlToPdf(html, opts = {}) {
|
|
5
|
+
const {
|
|
6
|
+
paper = 'a4',
|
|
7
|
+
landscape = false,
|
|
8
|
+
printBackground = true,
|
|
9
|
+
attachments = [],
|
|
10
|
+
metadata = {},
|
|
11
|
+
outPath,
|
|
12
|
+
} = opts;
|
|
13
|
+
|
|
14
|
+
const format = String(paper).toLowerCase();
|
|
15
|
+
const browser = await chromium.launch();
|
|
16
|
+
try {
|
|
17
|
+
const page = await browser.newPage();
|
|
18
|
+
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
19
|
+
|
|
20
|
+
const subject = metadata.subject || '';
|
|
21
|
+
const buffer = await page.pdf({
|
|
22
|
+
format,
|
|
23
|
+
landscape: !!landscape,
|
|
24
|
+
printBackground,
|
|
25
|
+
margin: { top: '24mm', right: '18mm', bottom: '20mm', left: '18mm' },
|
|
26
|
+
displayHeaderFooter: true,
|
|
27
|
+
headerTemplate: `<div></div>`,
|
|
28
|
+
footerTemplate: `<div style="font-family: -apple-system, sans-serif; font-size: 9px; color: #888; width: 100%; padding: 0 18mm; display: flex; justify-content: space-between;"><span>designlang${subject ? ' · ' + escapeHtml(subject) : ''}</span><span><span class="pageNumber"></span> of <span class="totalPages"></span></span></div>`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (attachments.length) {
|
|
32
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
33
|
+
const pdfDoc = await PDFDocument.load(buffer);
|
|
34
|
+
if (metadata.title) pdfDoc.setTitle(metadata.title);
|
|
35
|
+
if (metadata.subject) pdfDoc.setSubject(metadata.subject);
|
|
36
|
+
pdfDoc.setAuthor('designlang');
|
|
37
|
+
pdfDoc.setCreator('designlang');
|
|
38
|
+
for (const a of attachments) {
|
|
39
|
+
await pdfDoc.attach(a.contents, a.filename, {
|
|
40
|
+
mimeType: a.mimeType || 'application/json',
|
|
41
|
+
description: a.description || a.filename,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(outPath, await pdfDoc.save());
|
|
45
|
+
} else {
|
|
46
|
+
writeFileSync(outPath, buffer);
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
await browser.close();
|
|
50
|
+
}
|
|
51
|
+
return outPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeHtml(s) {
|
|
55
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
56
|
+
}
|