designlang 8.0.0 → 9.0.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.
@@ -0,0 +1,116 @@
1
+ // designlang visual-diff <before> <after>
2
+ // Captures screenshots for two URLs (or re-snapshots one URL across a time delta)
3
+ // and emits a side-by-side HTML diff with token deltas and file-size/dimension signals.
4
+
5
+ import { crawlPage } from './crawler.js';
6
+ import { extractDesignLanguage } from './index.js';
7
+ import { diffDesigns } from './diff.js';
8
+ import { nameFromUrl } from './utils.js';
9
+ import { statSync, existsSync, readFileSync } from 'fs';
10
+ import { basename } from 'path';
11
+
12
+ function fileKb(p) {
13
+ try { return Math.round(statSync(p).size / 1024); } catch { return 0; }
14
+ }
15
+
16
+ function toDataUri(p) {
17
+ try {
18
+ const buf = readFileSync(p);
19
+ return 'data:image/png;base64,' + buf.toString('base64');
20
+ } catch { return ''; }
21
+ }
22
+
23
+ async function captureFor(url, options = {}) {
24
+ const raw = await crawlPage(url, { ...options, screenshots: true });
25
+ const design = await extractDesignLanguage(url, { ...options, screenshots: true });
26
+ return { raw, design };
27
+ }
28
+
29
+ export async function visualDiff({ beforeUrl, afterUrl, options = {} } = {}) {
30
+ const before = await captureFor(beforeUrl, options);
31
+ const after = await captureFor(afterUrl, options);
32
+ const tokenDelta = diffDesigns(before.design, after.design);
33
+
34
+ const shotsBefore = before.raw.componentScreenshots || {};
35
+ const shotsAfter = after.raw.componentScreenshots || {};
36
+ const keys = [...new Set([...Object.keys(shotsBefore), ...Object.keys(shotsAfter)])];
37
+
38
+ const pairs = keys.map(k => {
39
+ const a = shotsBefore[k];
40
+ const b = shotsAfter[k];
41
+ return {
42
+ key: k,
43
+ before: a && existsSync(a) ? { path: a, sizeKb: fileKb(a) } : null,
44
+ after: b && existsSync(b) ? { path: b, sizeKb: fileKb(b) } : null,
45
+ };
46
+ });
47
+
48
+ return { pairs, tokenDelta, before: before.design, after: after.design };
49
+ }
50
+
51
+ export function formatVisualDiffHtml(result, { beforeLabel, afterLabel } = {}) {
52
+ const { pairs, tokenDelta, before, after } = result;
53
+ const bL = beforeLabel || nameFromUrl(before.meta.url);
54
+ const aL = afterLabel || nameFromUrl(after.meta.url);
55
+
56
+ const pairHtml = pairs.map(p => {
57
+ const deltaKb = (p.after?.sizeKb || 0) - (p.before?.sizeKb || 0);
58
+ const tag = deltaKb === 0 ? 'identical-size' : Math.abs(deltaKb) > 50 ? 'major-delta' : 'minor-delta';
59
+ return `
60
+ <section class="pair" data-status="${tag}">
61
+ <header>
62
+ <h3>${p.key}</h3>
63
+ <span class="delta">${deltaKb >= 0 ? '+' : ''}${deltaKb} KB</span>
64
+ </header>
65
+ <div class="frames">
66
+ <figure><figcaption>${bL}</figcaption>${p.before ? `<img src="${toDataUri(p.before.path)}" alt="${p.key} before">` : '<div class="missing">missing</div>'}</figure>
67
+ <figure><figcaption>${aL}</figcaption>${p.after ? `<img src="${toDataUri(p.after.path)}" alt="${p.key} after">` : '<div class="missing">missing</div>'}</figure>
68
+ </div>
69
+ </section>`;
70
+ }).join('');
71
+
72
+ const changedColors = (tokenDelta.colors?.changed || []).slice(0, 20);
73
+ const tokenRows = changedColors.map(c => `<tr><td>${c.token || c.key || ''}</td><td style="background:${c.before};">${c.before}</td><td style="background:${c.after};">${c.after}</td></tr>`).join('');
74
+
75
+ return `<!doctype html>
76
+ <html lang="en">
77
+ <head>
78
+ <meta charset="utf-8">
79
+ <title>designlang · visual-diff · ${bL} → ${aL}</title>
80
+ <style>
81
+ :root { --ink: #0A0908; --paper: #F3F1EA; --accent: #FF4800; }
82
+ * { box-sizing: border-box; }
83
+ body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui; background: var(--paper); color: var(--ink); }
84
+ header.top { padding: 32px 40px; border-bottom: 1px solid var(--ink); display: flex; justify-content: space-between; align-items: baseline; }
85
+ header.top h1 { font-size: 28px; margin: 0; letter-spacing: -0.02em; }
86
+ header.top .meta { font-variant-numeric: tabular-nums; opacity: 0.7; }
87
+ main { padding: 32px 40px; max-width: 1400px; margin: 0 auto; }
88
+ .pair { margin-bottom: 48px; border: 1px solid var(--ink); background: white; }
89
+ .pair header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--ink); }
90
+ .pair[data-status="major-delta"] header { background: var(--accent); color: white; }
91
+ .pair[data-status="identical-size"] .delta::after { content: " — identical"; opacity: 0.6; }
92
+ .pair h3 { margin: 0; text-transform: uppercase; letter-spacing: 0.04em; }
93
+ .frames { display: grid; grid-template-columns: 1fr 1fr; }
94
+ figure { margin: 0; padding: 16px; border-right: 1px solid var(--ink); }
95
+ figure:last-child { border-right: 0; }
96
+ figcaption { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.6; margin-bottom: 8px; }
97
+ figure img { max-width: 100%; display: block; border: 1px solid #ddd; }
98
+ .missing { padding: 40px; text-align: center; opacity: 0.4; font-style: italic; border: 1px dashed #999; }
99
+ table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
100
+ th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--ink); }
101
+ h2 { margin-top: 48px; font-size: 20px; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <header class="top">
106
+ <h1>visual-diff</h1>
107
+ <div class="meta">${bL} → ${aL} · ${pairs.length} components</div>
108
+ </header>
109
+ <main>
110
+ <h2>Component snapshots</h2>
111
+ ${pairHtml || '<p>No component screenshots captured.</p>'}
112
+ ${changedColors.length ? `<h2>Color token changes</h2><table><thead><tr><th>token</th><th>before</th><th>after</th></tr></thead><tbody>${tokenRows}</tbody></table>` : ''}
113
+ </main>
114
+ </body>
115
+ </html>`;
116
+ }