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 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.**
@@ -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
- if (opts.format === 'all' || opts.format === 'html') {
1481
- const html = formatBrandBook(design, { version: PKG_VERSION });
1482
- const p = join(outDir, `${prefix}.html`);
1483
- writeFileSync(p, html);
1484
- written.push(p);
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.10.1",
4
- "description": "Extract the complete design language from any website and ship it \u2014 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.",
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
56
+ }