prism-design 2.13.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 +292 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/clone-architect.mjs +476 -0
- package/bin/prism.mjs +467 -0
- package/catalog/index.json +1155 -0
- package/extractions/airbnb.com/DESIGN.md +1068 -0
- package/extractions/airbnb.com/tokens.json +507 -0
- package/extractions/attio.com/DESIGN.md +1295 -0
- package/extractions/attio.com/tokens.json +438 -0
- package/extractions/auroxdashboard.com/DESIGN.md +724 -0
- package/extractions/auroxdashboard.com/tokens.json +195 -0
- package/extractions/careerexplorer.com/DESIGN.md +1178 -0
- package/extractions/careerexplorer.com/tokens.json +141 -0
- package/extractions/chance.co/DESIGN.md +1209 -0
- package/extractions/chance.co/tokens.json +160 -0
- package/extractions/choisis-ton-avenir.com/DESIGN.md +1265 -0
- package/extractions/choisis-ton-avenir.com/tokens.json +227 -0
- package/extractions/example.com/DESIGN.md +436 -0
- package/extractions/example.com/tokens.json +91 -0
- package/extractions/getdesign.md/DESIGN.md +1009 -0
- package/extractions/getdesign.md/tokens.json +219 -0
- package/extractions/github.com/DESIGN.md +1130 -0
- package/extractions/github.com/tokens.json +2092 -0
- package/extractions/hello-charly.com/DESIGN.md +1146 -0
- package/extractions/hello-charly.com/tokens.json +322 -0
- package/extractions/hyperliquid.xyz/DESIGN.md +779 -0
- package/extractions/hyperliquid.xyz/tokens.json +598 -0
- package/extractions/instagram.com/DESIGN.md +996 -0
- package/extractions/instagram.com/tokens.json +1240 -0
- package/extractions/jobirl.com/DESIGN.md +1160 -0
- package/extractions/jobirl.com/tokens.json +139 -0
- package/extractions/life360.com/DESIGN.md +1133 -0
- package/extractions/life360.com/tokens.json +491 -0
- package/extractions/lifesum.com/DESIGN.md +965 -0
- package/extractions/lifesum.com/tokens.json +170 -0
- package/extractions/linear.app/DESIGN.md +1301 -0
- package/extractions/linear.app/tokens.json +732 -0
- package/extractions/mavoie.org/DESIGN.md +1148 -0
- package/extractions/mavoie.org/tokens.json +128 -0
- package/extractions/miro.com/DESIGN.md +1237 -0
- package/extractions/miro.com/tokens.json +401 -0
- package/extractions/notion.so/DESIGN.md +1319 -0
- package/extractions/notion.so/tokens.json +906 -0
- package/extractions/onetonline.org/DESIGN.md +909 -0
- package/extractions/onetonline.org/tokens.json +280 -0
- package/extractions/posthog.com/DESIGN.md +1024 -0
- package/extractions/posthog.com/tokens.json +197 -0
- package/extractions/revolut.com/DESIGN.md +1080 -0
- package/extractions/revolut.com/tokens.json +401 -0
- package/extractions/stripe.com/DESIGN.md +1272 -0
- package/extractions/stripe.com/tokens.json +794 -0
- package/extractions/switchcollective.com/DESIGN.md +1040 -0
- package/extractions/switchcollective.com/tokens.json +98 -0
- package/extractions/truity.com/DESIGN.md +970 -0
- package/extractions/truity.com/tokens.json +166 -0
- package/extractions/uniquekicks.be/DESIGN.md +1171 -0
- package/extractions/uniquekicks.be/tokens.json +237 -0
- package/package.json +122 -0
- package/scripts/analyze.ts +281 -0
- package/scripts/bank-register.ts +379 -0
- package/scripts/bank.ts +374 -0
- package/scripts/browser-stealth.ts +189 -0
- package/scripts/clone.ts +198 -0
- package/scripts/compare-vs-gd-final.ts +273 -0
- package/scripts/compare-vs-gd.ts +269 -0
- package/scripts/compare.ts +405 -0
- package/scripts/deploy-site.ts +181 -0
- package/scripts/diff-snapshots.ts +340 -0
- package/scripts/enrich-catalog.ts +212 -0
- package/scripts/extract.ts +2038 -0
- package/scripts/extractors/advanced.ts +524 -0
- package/scripts/extractors/widgets.ts +711 -0
- package/scripts/generate-design-md.ts +5775 -0
- package/scripts/generate-final-pdf.ts +274 -0
- package/scripts/generate-og-image.ts +87 -0
- package/scripts/generate-showcase.ts +1588 -0
- package/scripts/generate-site.ts +847 -0
- package/scripts/mass-extract.sh +91 -0
- package/scripts/post-process-all.sh +55 -0
- package/scripts/regen-catalog.ts +203 -0
- package/scripts/shared/cache.ts +149 -0
- package/scripts/shared/css-helpers.ts +263 -0
- package/scripts/shared/logger.ts +57 -0
- package/scripts/shared/named-colors.ts +355 -0
- package/scripts/shared/types.ts +220 -0
- package/scripts/sync-catalog.ts +105 -0
- package/scripts/tokenize.ts +988 -0
- package/templates/layout-template.md +52 -0
- package/templates/tokens-template.json +34 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism โ Deploy Site to /opt/clone-architect-site
|
|
3
|
+
*
|
|
4
|
+
* 1. Gรฉnรจre public/index.html via generate-site.ts
|
|
5
|
+
* 2. Copie public/index.html โ /opt/clone-architect-site/index.html
|
|
6
|
+
* 3. Copie chaque extractions/{domain}/showcase/index.html โ /opt/clone-architect-site/brands/{domain}/index.html
|
|
7
|
+
* 4. Crรฉe /opt/clone-architect-site/404.html
|
|
8
|
+
* 5. Affiche le rรฉsumรฉ du dรฉploiement
|
|
9
|
+
*
|
|
10
|
+
* Usage: npx tsx scripts/deploy-site.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
const ROOT = process.cwd();
|
|
18
|
+
const DEPLOY_DIR = '/opt/clone-architect-site';
|
|
19
|
+
const CATALOG_PATH = join(ROOT, 'catalog', 'index.json');
|
|
20
|
+
const PUBLIC_INDEX = join(ROOT, 'public', 'index.html');
|
|
21
|
+
|
|
22
|
+
function writeWithSudo(targetPath: string, content: string): void {
|
|
23
|
+
// Try direct write first (paul owns /opt/clone-architect-site)
|
|
24
|
+
try {
|
|
25
|
+
const dir = dirname(targetPath);
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
writeFileSync(targetPath, content);
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback: sudo node write
|
|
30
|
+
const escaped = content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
31
|
+
execSync(`sudo /usr/bin/node -e "
|
|
32
|
+
require('fs').mkdirSync(require('path').dirname('${targetPath}'), {recursive:true});
|
|
33
|
+
require('fs').writeFileSync('${targetPath}', \\\`${escaped}\\\`);
|
|
34
|
+
"`, { stdio: 'ignore' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyWithSudo(src: string, dest: string): void {
|
|
39
|
+
try {
|
|
40
|
+
const dir = dirname(dest);
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
copyFileSync(src, dest);
|
|
43
|
+
} catch {
|
|
44
|
+
const dir = dirname(dest);
|
|
45
|
+
execSync(`sudo /usr/bin/node -e "
|
|
46
|
+
require('fs').mkdirSync('${dir}', {recursive:true});
|
|
47
|
+
require('fs').copyFileSync('${src}', '${dest}');
|
|
48
|
+
"`, { stdio: 'ignore' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
console.log('๐ Prism โ Deploy Site\n');
|
|
54
|
+
|
|
55
|
+
// Step 1 โ Regenerate public/index.html
|
|
56
|
+
console.log('๐ Gรฉnรฉration public/index.html...');
|
|
57
|
+
execSync('npx tsx scripts/generate-site.ts', { cwd: ROOT, stdio: 'inherit' });
|
|
58
|
+
|
|
59
|
+
// Step 2 โ Ensure deploy dir exists
|
|
60
|
+
try {
|
|
61
|
+
mkdirSync(DEPLOY_DIR, { recursive: true });
|
|
62
|
+
} catch {
|
|
63
|
+
execSync(`sudo /usr/bin/node -e "require('fs').mkdirSync('${DEPLOY_DIR}', {recursive:true})"`, { stdio: 'ignore' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 2b โ Generate og-image.png (non-fatal: SEO/social-preview only, must NOT abort deploy).
|
|
67
|
+
// A transient Playwright screenshot failure here previously killed the whole deploy before
|
|
68
|
+
// showcases were copied. og-image regeneration is cosmetic โ the existing one is reused on failure.
|
|
69
|
+
console.log('\n๐จ Gรฉnรฉration og-image.png...');
|
|
70
|
+
try {
|
|
71
|
+
execSync('npx tsx scripts/generate-og-image.ts', { cwd: ROOT, stdio: 'inherit' });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(` โ ๏ธ og-image generation failed (non-fatal, keeping existing): ${(e as Error).message.split('\n')[0]}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3 โ Copy index.html + og-image.png
|
|
77
|
+
console.log('\n๐ Copie index.html + og-image.png โ /opt/clone-architect-site/...');
|
|
78
|
+
const indexContent = readFileSync(PUBLIC_INDEX, 'utf-8');
|
|
79
|
+
writeWithSudo(join(DEPLOY_DIR, 'index.html'), indexContent);
|
|
80
|
+
const ogPng = join(ROOT, 'public', 'og-image.png');
|
|
81
|
+
if (existsSync(ogPng)) copyWithSudo(ogPng, join(DEPLOY_DIR, 'og-image.png'));
|
|
82
|
+
console.log(' โ
index.html + og-image.png dรฉployรฉs');
|
|
83
|
+
|
|
84
|
+
// Step 4 โ Copy showcase for each brand
|
|
85
|
+
console.log('\n๐จ Dรฉploiement des showcases...');
|
|
86
|
+
const catalogRaw = readFileSync(CATALOG_PATH, 'utf-8');
|
|
87
|
+
const catalog = JSON.parse(catalogRaw);
|
|
88
|
+
|
|
89
|
+
let deployed = 0;
|
|
90
|
+
let skipped = 0;
|
|
91
|
+
|
|
92
|
+
for (const brand of catalog.brands) {
|
|
93
|
+
const domain = brand.domain;
|
|
94
|
+
const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html');
|
|
95
|
+
const showcaseDest = join(DEPLOY_DIR, 'brands', domain, 'index.html');
|
|
96
|
+
|
|
97
|
+
if (existsSync(showcaseSrc)) {
|
|
98
|
+
copyWithSudo(showcaseSrc, showcaseDest);
|
|
99
|
+
deployed++;
|
|
100
|
+
} else {
|
|
101
|
+
skipped++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(` โ
${deployed} showcases dรฉployรฉs โ /opt/clone-architect-site/brands/`);
|
|
106
|
+
if (skipped > 0) console.log(` โญ ${skipped} brands sans showcase (extraction incomplรจte)`);
|
|
107
|
+
|
|
108
|
+
// Step 4b โ Generate sitemap.xml (SEO indexation)
|
|
109
|
+
console.log('\n๐บ๏ธ Gรฉnรฉration sitemap.xml...');
|
|
110
|
+
const baseUrl = 'https://prism.ps-tools.dev';
|
|
111
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
112
|
+
const sitemapUrls = [
|
|
113
|
+
`<url><loc>${baseUrl}/</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>`,
|
|
114
|
+
];
|
|
115
|
+
for (const brand of catalog.brands) {
|
|
116
|
+
const domain = brand.domain;
|
|
117
|
+
const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html');
|
|
118
|
+
if (existsSync(showcaseSrc)) {
|
|
119
|
+
sitemapUrls.push(`<url><loc>${baseUrl}/brands/${domain}/</loc><lastmod>${brand.extractedAt?.slice(0, 10) || today}</lastmod><changefreq>monthly</changefreq><priority>0.7</priority></url>`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
123
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
124
|
+
${sitemapUrls.join('\n')}
|
|
125
|
+
</urlset>`;
|
|
126
|
+
writeWithSudo(join(DEPLOY_DIR, 'sitemap.xml'), sitemapXml);
|
|
127
|
+
writeWithSudo(join(ROOT, 'public', 'sitemap.xml'), sitemapXml);
|
|
128
|
+
console.log(` โ
sitemap.xml gรฉnรฉrรฉ (${sitemapUrls.length} URLs)`);
|
|
129
|
+
|
|
130
|
+
// Step 4c โ robots.txt
|
|
131
|
+
const robotsTxt = `User-agent: *
|
|
132
|
+
Allow: /
|
|
133
|
+
Disallow: /api/
|
|
134
|
+
|
|
135
|
+
Sitemap: ${baseUrl}/sitemap.xml
|
|
136
|
+
`;
|
|
137
|
+
writeWithSudo(join(DEPLOY_DIR, 'robots.txt'), robotsTxt);
|
|
138
|
+
writeWithSudo(join(ROOT, 'public', 'robots.txt'), robotsTxt);
|
|
139
|
+
console.log(` โ
robots.txt dรฉployรฉ`);
|
|
140
|
+
|
|
141
|
+
// Step 5 โ Create 404.html
|
|
142
|
+
const notFoundHtml = `<!DOCTYPE html>
|
|
143
|
+
<html lang="en">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="UTF-8">
|
|
146
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
147
|
+
<title>Not found โ Prism</title>
|
|
148
|
+
<style>
|
|
149
|
+
body { margin: 0; background: #0a0a0b; color: #e5e5e5; font-family: -apple-system, sans-serif;
|
|
150
|
+
display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
|
151
|
+
.wrap { text-align: center; }
|
|
152
|
+
h1 { font-size: 4rem; margin: 0; color: #5e6ad2; }
|
|
153
|
+
p { color: #888; margin: 12px 0 32px; }
|
|
154
|
+
a { display: inline-block; padding: 10px 24px; background: #5e6ad2; color: #fff;
|
|
155
|
+
border-radius: 6px; text-decoration: none; font-size: 14px; }
|
|
156
|
+
a:hover { background: #4f5bbf; }
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
<body>
|
|
160
|
+
<div class="wrap">
|
|
161
|
+
<h1>404</h1>
|
|
162
|
+
<p>This brand hasn't been extracted yet.</p>
|
|
163
|
+
<a href="/">Browse 82 extractions โ</a>
|
|
164
|
+
</div>
|
|
165
|
+
</body>
|
|
166
|
+
</html>`;
|
|
167
|
+
|
|
168
|
+
writeWithSudo(join(DEPLOY_DIR, '404.html'), notFoundHtml);
|
|
169
|
+
writeWithSudo(join(ROOT, 'public', '404.html'), notFoundHtml);
|
|
170
|
+
console.log('\n๐ 404.html crรฉรฉ');
|
|
171
|
+
|
|
172
|
+
// Step 6 โ Summary
|
|
173
|
+
console.log('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
174
|
+
console.log('โ
DEPLOY COMPLET');
|
|
175
|
+
console.log(` Site : https://prism.ps-tools.dev`);
|
|
176
|
+
console.log(` Brands : https://prism.ps-tools.dev/brands/linear.app/`);
|
|
177
|
+
console.log(` Total : ${deployed} showcases + index.html + 404.html`);
|
|
178
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch(err => { console.error('โ Deploy error:', err); process.exit(1); });
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism โ Snapshot Diff Feature (unique moat vs getdesign.md)
|
|
3
|
+
*
|
|
4
|
+
* Compare two extractions of the same domain (at different dates) and report
|
|
5
|
+
* what changed in the design tokens: colors, typography, spacing, components.
|
|
6
|
+
*
|
|
7
|
+
* Use case: "What did Linear change in the last 3 months?"
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx scripts/diff-snapshots.ts <domain> # latest vs previous in archive/
|
|
11
|
+
* npx tsx scripts/diff-snapshots.ts <domain> <date1> <date2> # specific snapshots
|
|
12
|
+
* npx tsx scripts/diff-snapshots.ts --create <domain> # snapshot current state
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, copyFileSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
const ROOT = process.cwd();
|
|
19
|
+
const ARCHIVE_DIR = join(ROOT, 'extractions', '_snapshots');
|
|
20
|
+
|
|
21
|
+
interface DesignTokens {
|
|
22
|
+
meta?: { extractedAt?: string; domain?: string };
|
|
23
|
+
colors?: {
|
|
24
|
+
background?: { primary?: string; secondary?: string; tertiary?: string };
|
|
25
|
+
text?: { primary?: string; secondary?: string; muted?: string };
|
|
26
|
+
accent?: any;
|
|
27
|
+
border?: any;
|
|
28
|
+
};
|
|
29
|
+
typography?: {
|
|
30
|
+
fontFamily?: { primary?: string; secondary?: string; mono?: string };
|
|
31
|
+
fontSize?: Record<string, string>;
|
|
32
|
+
fontWeight?: Record<string, string | number>;
|
|
33
|
+
};
|
|
34
|
+
spacing?: Record<string, string>;
|
|
35
|
+
borderRadius?: Record<string, string>;
|
|
36
|
+
shadows?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadTokens(domain: string, snapshot?: string): DesignTokens | null {
|
|
40
|
+
let path: string;
|
|
41
|
+
if (snapshot) {
|
|
42
|
+
path = join(ARCHIVE_DIR, domain, snapshot, 'tokens.json');
|
|
43
|
+
} else {
|
|
44
|
+
path = join(ROOT, 'extractions', domain, 'tokens.json');
|
|
45
|
+
}
|
|
46
|
+
if (!existsSync(path)) return null;
|
|
47
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listSnapshots(domain: string): string[] {
|
|
51
|
+
const dir = join(ARCHIVE_DIR, domain);
|
|
52
|
+
if (!existsSync(dir)) return [];
|
|
53
|
+
return readdirSync(dir).filter(d => /^\d{4}-\d{2}-\d{2}/.test(d)).sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createSnapshot(domain: string): void {
|
|
57
|
+
const extractDir = join(ROOT, 'extractions', domain);
|
|
58
|
+
if (!existsSync(extractDir)) {
|
|
59
|
+
console.error(`โ Extraction not found: ${extractDir}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
63
|
+
const snapDir = join(ARCHIVE_DIR, domain, date);
|
|
64
|
+
mkdirSync(snapDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
// Copy tokens.json + DESIGN.md
|
|
67
|
+
for (const file of ['tokens.json', 'DESIGN.md', 'extraction-summary.json']) {
|
|
68
|
+
const src = join(extractDir, file);
|
|
69
|
+
if (existsSync(src)) copyFileSync(src, join(snapDir, file));
|
|
70
|
+
}
|
|
71
|
+
console.log(`๐ธ Snapshot created: ${snapDir}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function diffValue(label: string, a: any, b: any): string[] {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
if (a === b) return lines;
|
|
77
|
+
if (a === undefined || a === null) {
|
|
78
|
+
lines.push(` + ${label}: ${JSON.stringify(b)} (added)`);
|
|
79
|
+
} else if (b === undefined || b === null) {
|
|
80
|
+
lines.push(` - ${label}: ${JSON.stringify(a)} (removed)`);
|
|
81
|
+
} else {
|
|
82
|
+
lines.push(` ~ ${label}: ${JSON.stringify(a)} โ ${JSON.stringify(b)}`);
|
|
83
|
+
}
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function diffObject(label: string, a: Record<string, any> = {}, b: Record<string, any> = {}): string[] {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
if (a[k] !== b[k]) {
|
|
92
|
+
lines.push(...diffValue(`${label}.${k}`, a[k], b[k]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function diffTokens(before: DesignTokens, after: DesignTokens): {
|
|
99
|
+
summary: { changed: number; added: number; removed: number };
|
|
100
|
+
lines: string[];
|
|
101
|
+
} {
|
|
102
|
+
const lines: string[] = [];
|
|
103
|
+
|
|
104
|
+
// Meta
|
|
105
|
+
const dateBefore = before.meta?.extractedAt?.slice(0, 10) || '?';
|
|
106
|
+
const dateAfter = after.meta?.extractedAt?.slice(0, 10) || '?';
|
|
107
|
+
lines.push(`\n๐
Comparison: ${dateBefore} โ ${dateAfter}\n`);
|
|
108
|
+
|
|
109
|
+
// Colors
|
|
110
|
+
const colorChanges: string[] = [];
|
|
111
|
+
colorChanges.push(...diffObject('background', before.colors?.background, after.colors?.background));
|
|
112
|
+
colorChanges.push(...diffObject('text', before.colors?.text, after.colors?.text));
|
|
113
|
+
if (typeof before.colors?.accent === 'object' && typeof after.colors?.accent === 'object') {
|
|
114
|
+
colorChanges.push(...diffObject('accent', before.colors.accent, after.colors.accent));
|
|
115
|
+
}
|
|
116
|
+
if (typeof before.colors?.border === 'object' && typeof after.colors?.border === 'object') {
|
|
117
|
+
colorChanges.push(...diffObject('border', before.colors.border, after.colors.border));
|
|
118
|
+
}
|
|
119
|
+
if (colorChanges.length > 0) {
|
|
120
|
+
lines.push('๐จ Colors:');
|
|
121
|
+
lines.push(...colorChanges);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Typography
|
|
125
|
+
const typoChanges: string[] = [];
|
|
126
|
+
typoChanges.push(...diffObject('fontFamily', before.typography?.fontFamily, after.typography?.fontFamily));
|
|
127
|
+
typoChanges.push(...diffObject('fontSize', before.typography?.fontSize, after.typography?.fontSize));
|
|
128
|
+
typoChanges.push(...diffObject('fontWeight', before.typography?.fontWeight, after.typography?.fontWeight));
|
|
129
|
+
if (typoChanges.length > 0) {
|
|
130
|
+
lines.push('\n๐ค Typography:');
|
|
131
|
+
lines.push(...typoChanges);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Spacing
|
|
135
|
+
const spacingChanges = diffObject('spacing', before.spacing, after.spacing);
|
|
136
|
+
if (spacingChanges.length > 0) {
|
|
137
|
+
lines.push('\n๐ Spacing:');
|
|
138
|
+
lines.push(...spacingChanges);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Border radius
|
|
142
|
+
const radiusChanges = diffObject('borderRadius', before.borderRadius, after.borderRadius);
|
|
143
|
+
if (radiusChanges.length > 0) {
|
|
144
|
+
lines.push('\nโญ Border radius:');
|
|
145
|
+
lines.push(...radiusChanges);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Shadows
|
|
149
|
+
const shadowChanges = diffObject('shadow', before.shadows, after.shadows);
|
|
150
|
+
if (shadowChanges.length > 0) {
|
|
151
|
+
lines.push('\n๐ง Shadows:');
|
|
152
|
+
lines.push(...shadowChanges);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Summary
|
|
156
|
+
let changed = 0, added = 0, removed = 0;
|
|
157
|
+
for (const l of lines) {
|
|
158
|
+
if (l.startsWith(' ~ ')) changed++;
|
|
159
|
+
else if (l.startsWith(' + ')) added++;
|
|
160
|
+
else if (l.startsWith(' - ')) removed++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
summary: { changed, added, removed },
|
|
165
|
+
lines,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// โโ Cross-domain comparison (Design Diff killer feature) โโโโโโโโโโโโโโโโโโ
|
|
170
|
+
|
|
171
|
+
function compareDomains(domainA: string, domainB: string): void {
|
|
172
|
+
const tokensA = loadTokens(domainA);
|
|
173
|
+
const tokensB = loadTokens(domainB);
|
|
174
|
+
|
|
175
|
+
if (!tokensA) {
|
|
176
|
+
console.error(`โ No extraction for "${domainA}". Run: prism extract https://${domainA}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
if (!tokensB) {
|
|
180
|
+
console.error(`โ No extraction for "${domainB}". Run: prism extract https://${domainB}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n๐ฌ Prism Design Diff โ Cross-brand comparison`);
|
|
185
|
+
console.log(` ${domainA} โ ${domainB}\n`);
|
|
186
|
+
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
|
|
189
|
+
// Helper: color contrast label
|
|
190
|
+
function colorLabel(val: string | undefined): string {
|
|
191
|
+
return val ? val : 'โ';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Colors table
|
|
195
|
+
lines.push('๐จ COLORS\n');
|
|
196
|
+
const colorPairs = [
|
|
197
|
+
['Background', tokensA.colors?.background?.primary, tokensB.colors?.background?.primary],
|
|
198
|
+
['Background 2', tokensA.colors?.background?.secondary, tokensB.colors?.background?.secondary],
|
|
199
|
+
['Text primary', tokensA.colors?.text?.primary, tokensB.colors?.text?.primary],
|
|
200
|
+
['Text muted', tokensA.colors?.text?.muted, tokensB.colors?.text?.muted],
|
|
201
|
+
['Accent', (tokensA.colors?.accent as any)?.primary, (tokensB.colors?.accent as any)?.primary],
|
|
202
|
+
['Border', tokensA.colors?.border as string, tokensB.colors?.border as string],
|
|
203
|
+
];
|
|
204
|
+
for (const [label, a, b] of colorPairs) {
|
|
205
|
+
const same = a === b ? ' โ same' : '';
|
|
206
|
+
lines.push(` ${String(label).padEnd(14)} ${domainA.padEnd(20)} ${colorLabel(a as string)}`);
|
|
207
|
+
lines.push(` ${''.padEnd(14)} ${domainB.padEnd(20)} ${colorLabel(b as string)}${same}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Typography table
|
|
211
|
+
lines.push('\n๐ค TYPOGRAPHY\n');
|
|
212
|
+
const typoPairs = [
|
|
213
|
+
['Font body', tokensA.typography?.fontFamily?.primary, tokensB.typography?.fontFamily?.primary],
|
|
214
|
+
['Font mono', tokensA.typography?.fontFamily?.mono, tokensB.typography?.fontFamily?.mono],
|
|
215
|
+
['Size body', tokensA.typography?.fontSize?.base, tokensB.typography?.fontSize?.base],
|
|
216
|
+
['Size heading', tokensA.typography?.fontSize?.xl, tokensB.typography?.fontSize?.xl],
|
|
217
|
+
['Size display', tokensA.typography?.fontSize?.['3xl'], tokensB.typography?.fontSize?.['3xl']],
|
|
218
|
+
['Weight bold', String(tokensA.typography?.fontWeight?.bold || ''), String(tokensB.typography?.fontWeight?.bold || '')],
|
|
219
|
+
['Letter-spacing', (tokensA.typography as any)?.letterSpacing?.tight, (tokensB.typography as any)?.letterSpacing?.tight],
|
|
220
|
+
];
|
|
221
|
+
for (const [label, a, b] of typoPairs) {
|
|
222
|
+
if (!a && !b) continue;
|
|
223
|
+
const delta = a !== b ? ' โ differs' : '';
|
|
224
|
+
lines.push(` ${String(label).padEnd(16)} ${String(a || 'โ').padEnd(28)} ${String(b || 'โ')}${delta}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Spacing table
|
|
228
|
+
lines.push('\n\n๐ SPACING\n');
|
|
229
|
+
const spacingKeys = ['sm', 'md', 'base', 'lg', 'xl'];
|
|
230
|
+
for (const k of spacingKeys) {
|
|
231
|
+
const a = (tokensA.spacing as any)?.[k];
|
|
232
|
+
const b = (tokensB.spacing as any)?.[k];
|
|
233
|
+
if (!a && !b) continue;
|
|
234
|
+
const delta = a !== b ? ' โ differs' : '';
|
|
235
|
+
lines.push(` ${k.padEnd(6)} ${String(a || 'โ').padEnd(12)} ${String(b || 'โ')}${delta}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Border radius
|
|
239
|
+
lines.push('\n\nโญ BORDER RADIUS\n');
|
|
240
|
+
const radiusKeys = ['sm', 'md', 'lg', 'full'];
|
|
241
|
+
for (const k of radiusKeys) {
|
|
242
|
+
const a = (tokensA.borderRadius as any)?.[k];
|
|
243
|
+
const b = (tokensB.borderRadius as any)?.[k];
|
|
244
|
+
if (!a && !b) continue;
|
|
245
|
+
const delta = a !== b ? ' โ differs' : '';
|
|
246
|
+
lines.push(` ${k.padEnd(6)} ${String(a || 'โ').padEnd(12)} ${String(b || 'โ')}${delta}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Delta summary
|
|
250
|
+
let diffCount = 0;
|
|
251
|
+
for (const l of lines) { if (l.includes('โ differs')) diffCount++; }
|
|
252
|
+
|
|
253
|
+
lines.push('\n');
|
|
254
|
+
lines.push(`\n${'โ'.repeat(60)}`);
|
|
255
|
+
lines.push(`๐ ${domainA} vs ${domainB}: ${diffCount} design differences found`);
|
|
256
|
+
lines.push(`${'โ'.repeat(60)}\n`);
|
|
257
|
+
|
|
258
|
+
// Verdict
|
|
259
|
+
const aIsDark = (tokensA.meta as any)?.isDark ?? false;
|
|
260
|
+
const bIsDark = (tokensB.meta as any)?.isDark ?? false;
|
|
261
|
+
if (aIsDark !== bIsDark) {
|
|
262
|
+
lines.push(`๐ก Mode: ${domainA} is ${aIsDark ? 'dark' : 'light'}-native, ${domainB} is ${bIsDark ? 'dark' : 'light'}-native`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(lines.join('\n'));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function main() {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const createMode = args.includes('--create');
|
|
271
|
+
const compareMode = args.includes('--compare') || (args.length === 2 && !args[0].startsWith('--') && !args[1].startsWith('--') && args[0].includes('.') && args[1].includes('.'));
|
|
272
|
+
|
|
273
|
+
if (createMode) {
|
|
274
|
+
const domain = args[args.indexOf('--create') + 1];
|
|
275
|
+
if (!domain) {
|
|
276
|
+
console.error('Usage: --create <domain>');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
createSnapshot(domain);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Cross-domain comparison: prism diff linear.app notion.so
|
|
284
|
+
if (compareMode) {
|
|
285
|
+
const domainArgs = args.filter(a => !a.startsWith('--'));
|
|
286
|
+
if (domainArgs.length < 2) {
|
|
287
|
+
console.error('Usage: prism diff <domain-a> <domain-b>');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
compareDomains(domainArgs[0], domainArgs[1]);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const domain = args[0];
|
|
295
|
+
if (!domain) {
|
|
296
|
+
console.error(`Usage:
|
|
297
|
+
prism diff <domain-a> <domain-b> # compare two brands (Design Diff)
|
|
298
|
+
prism diff <domain> # current vs latest snapshot
|
|
299
|
+
prism diff <domain> <snap1> <snap2> # specific snapshots
|
|
300
|
+
prism diff --create <domain> # snapshot current state`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const snapshots = listSnapshots(domain);
|
|
305
|
+
let before: DesignTokens | null = null;
|
|
306
|
+
let after: DesignTokens | null = null;
|
|
307
|
+
let beforeLabel = '?', afterLabel = '?';
|
|
308
|
+
|
|
309
|
+
if (args.length >= 3 && !args[1].includes('.') && !args[2].includes('.')) {
|
|
310
|
+
beforeLabel = args[1];
|
|
311
|
+
afterLabel = args[2];
|
|
312
|
+
before = loadTokens(domain, beforeLabel);
|
|
313
|
+
after = loadTokens(domain, afterLabel);
|
|
314
|
+
} else if (snapshots.length >= 1) {
|
|
315
|
+
beforeLabel = snapshots[0];
|
|
316
|
+
afterLabel = 'current';
|
|
317
|
+
before = loadTokens(domain, beforeLabel);
|
|
318
|
+
after = loadTokens(domain);
|
|
319
|
+
} else {
|
|
320
|
+
console.error(`โ No snapshots found for ${domain}. Create one with: prism diff --create ${domain}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!before || !after) {
|
|
325
|
+
console.error(`โ Missing tokens.json (before: ${!!before}, after: ${!!after})`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(`\n๐ Prism โ Diff ${domain}`);
|
|
330
|
+
console.log(` ${beforeLabel} โ ${afterLabel}\n`);
|
|
331
|
+
|
|
332
|
+
const diff = diffTokens(before, after);
|
|
333
|
+
console.log(diff.lines.join('\n'));
|
|
334
|
+
|
|
335
|
+
console.log(`\n${'โ'.repeat(42)}`);
|
|
336
|
+
console.log(`๐ Summary: ~${diff.summary.changed} changed ยท +${diff.summary.added} added ยท -${diff.summary.removed} removed`);
|
|
337
|
+
console.log(`${'โ'.repeat(42)}\n`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch(err => { console.error(err); process.exit(1); });
|