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,847 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism — Static Site Generator
|
|
3
|
+
*
|
|
4
|
+
* Reads catalog/index.json → generates public/index.html
|
|
5
|
+
* Full site: hero + stats + brands directory with search/filter + compare + how it works
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx tsx scripts/generate-site.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
const ROOT = process.cwd();
|
|
15
|
+
const CATALOG_PATH = join(ROOT, 'catalog', 'index.json');
|
|
16
|
+
const PUBLIC_DIR = join(ROOT, 'public');
|
|
17
|
+
const OUT_PATH = join(PUBLIC_DIR, 'index.html');
|
|
18
|
+
|
|
19
|
+
interface CatalogBrand {
|
|
20
|
+
domain: string;
|
|
21
|
+
hasDesignMd: boolean;
|
|
22
|
+
hasTokens: boolean;
|
|
23
|
+
hasScreenshots: boolean;
|
|
24
|
+
hasFrontmatter: boolean;
|
|
25
|
+
accent: string;
|
|
26
|
+
description: string;
|
|
27
|
+
sizeKb: number;
|
|
28
|
+
category: string;
|
|
29
|
+
dark: boolean;
|
|
30
|
+
font: string;
|
|
31
|
+
completeness: number;
|
|
32
|
+
extractedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Catalog {
|
|
36
|
+
version: string;
|
|
37
|
+
count: number;
|
|
38
|
+
brands: CatalogBrand[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function gradeColor(score: number): string {
|
|
42
|
+
if (score >= 90) return '#4ade80'; // green
|
|
43
|
+
if (score >= 75) return '#a3e635'; // lime
|
|
44
|
+
if (score >= 60) return '#facc15'; // yellow
|
|
45
|
+
return '#f87171'; // red
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function gradeLabel(score: number): string {
|
|
49
|
+
if (score >= 90) return 'A';
|
|
50
|
+
if (score >= 75) return 'B';
|
|
51
|
+
if (score >= 60) return 'C';
|
|
52
|
+
return 'D';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function domainName(domain: string): string {
|
|
56
|
+
return domain
|
|
57
|
+
.replace(/^www\./, '')
|
|
58
|
+
.replace(/\.(com|app|io|dev|net|org|ai|co|xyz|so|design|new)$/, '')
|
|
59
|
+
.replace(/^(.)/, c => c.toUpperCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function accentHex(accent: string): string {
|
|
63
|
+
if (!accent) return '#5e6ad2';
|
|
64
|
+
if (accent.startsWith('#')) return accent;
|
|
65
|
+
// parse rgb()
|
|
66
|
+
const m = accent.match(/\d+/g);
|
|
67
|
+
if (m && m.length >= 3) {
|
|
68
|
+
return '#' + m.slice(0, 3).map(v => parseInt(v).toString(16).padStart(2, '0')).join('');
|
|
69
|
+
}
|
|
70
|
+
return '#5e6ad2';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function showcaseUrl(domain: string): string {
|
|
74
|
+
// On the public site, showcases are served from /brands/{domain}/
|
|
75
|
+
return `brands/${domain}/`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
if (!existsSync(CATALOG_PATH)) {
|
|
80
|
+
console.error('catalog/index.json not found — run: npx tsx scripts/enrich-catalog.ts');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const catalogRaw = await readFile(CATALOG_PATH, 'utf-8');
|
|
85
|
+
const catalog: Catalog = JSON.parse(catalogRaw);
|
|
86
|
+
|
|
87
|
+
// Read npm package version (not catalog version) for eyebrow
|
|
88
|
+
const pkgRaw = await readFile(join(ROOT, 'package.json'), 'utf-8');
|
|
89
|
+
const pkgVersion: string = JSON.parse(pkgRaw).version;
|
|
90
|
+
|
|
91
|
+
// Sort by completeness desc
|
|
92
|
+
const brands = [...catalog.brands].sort((a, b) => (b.completeness || 0) - (a.completeness || 0));
|
|
93
|
+
|
|
94
|
+
const avgScore = Math.round(brands.reduce((s, b) => s + (b.completeness || 0), 0) / brands.length);
|
|
95
|
+
const darkCount = brands.filter(b => b.dark).length;
|
|
96
|
+
const categories = [...new Set(brands.map(b => b.category).filter(Boolean))].sort();
|
|
97
|
+
|
|
98
|
+
// Inline catalog as JSON for client-side filtering
|
|
99
|
+
const catalogJson = JSON.stringify(brands);
|
|
100
|
+
|
|
101
|
+
await mkdir(PUBLIC_DIR, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const html = `<!DOCTYPE html>
|
|
104
|
+
<html lang="en">
|
|
105
|
+
<head>
|
|
106
|
+
<meta charset="UTF-8">
|
|
107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
108
|
+
<title>Prism — Extract real design from any URL</title>
|
|
109
|
+
<meta name="description" content="Playwright + getComputedStyle() → DESIGN.md. ${brands.length} brands extracted. Verifiable, dated, diffable. MIT local-first.">
|
|
110
|
+
<link rel="canonical" href="https://prism.ps-tools.dev/">
|
|
111
|
+
<meta name="theme-color" content="#5e6ad2">
|
|
112
|
+
|
|
113
|
+
<!-- Open Graph -->
|
|
114
|
+
<meta property="og:title" content="Prism — Extract real design from any URL">
|
|
115
|
+
<meta property="og:description" content="${brands.length} brands · avg ${avgScore}/100 completeness · MIT · no catalog lock-in">
|
|
116
|
+
<meta property="og:type" content="website">
|
|
117
|
+
<meta property="og:url" content="https://prism.ps-tools.dev/">
|
|
118
|
+
<meta property="og:image" content="https://prism.ps-tools.dev/og-image.png">
|
|
119
|
+
<meta property="og:image:width" content="1200">
|
|
120
|
+
<meta property="og:image:height" content="630">
|
|
121
|
+
|
|
122
|
+
<!-- Twitter Card -->
|
|
123
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
124
|
+
<meta name="twitter:title" content="Prism — Extract real design from any URL">
|
|
125
|
+
<meta name="twitter:description" content="${brands.length} brands · ${avgScore}/100 avg · Playwright + getComputedStyle() = verifiable DESIGN.md">
|
|
126
|
+
<meta name="twitter:image" content="https://prism.ps-tools.dev/og-image.png">
|
|
127
|
+
|
|
128
|
+
<!-- Favicon (inline SVG) -->
|
|
129
|
+
<link rel="icon" type="image/svg+xml" href='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="22" fill="%2308090a"/><path d="M46 22 L72 70 H30 Z" fill="none" stroke="%23f7f8f8" stroke-width="6" stroke-linejoin="round"/><line x1="58" y1="46" x2="86" y2="40" stroke="%23ff5470" stroke-width="4"/><line x1="58" y1="46" x2="88" y2="48" stroke="%23ffd166" stroke-width="4"/><line x1="58" y1="46" x2="86" y2="56" stroke="%235e6ad2" stroke-width="4"/></svg>'>
|
|
130
|
+
|
|
131
|
+
<!-- Schema.org -->
|
|
132
|
+
<script type="application/ld+json">
|
|
133
|
+
{
|
|
134
|
+
"@context": "https://schema.org",
|
|
135
|
+
"@type": "SoftwareApplication",
|
|
136
|
+
"name": "Prism",
|
|
137
|
+
"applicationCategory": "DeveloperApplication",
|
|
138
|
+
"operatingSystem": "Linux, macOS, Windows",
|
|
139
|
+
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
|
|
140
|
+
"softwareVersion": "${pkgVersion}",
|
|
141
|
+
"url": "https://prism.ps-tools.dev",
|
|
142
|
+
"description": "Extract real design from any URL using Playwright + getComputedStyle"
|
|
143
|
+
}
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<link rel="preconnect" href="https://rsms.me/">
|
|
147
|
+
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
|
148
|
+
|
|
149
|
+
<style>
|
|
150
|
+
:root {
|
|
151
|
+
--bg-marketing: #010102;
|
|
152
|
+
--bg-primary: #08090a;
|
|
153
|
+
--bg-panel: #0f1011;
|
|
154
|
+
--bg-level-2: #141516;
|
|
155
|
+
--bg-level-3: #191a1b;
|
|
156
|
+
--bg-tertiary: #232326;
|
|
157
|
+
--text-primary: #f7f8f8;
|
|
158
|
+
--text-secondary: #d0d6e0;
|
|
159
|
+
--text-tertiary: #8a8f98;
|
|
160
|
+
--text-quaternary: #62666d;
|
|
161
|
+
--accent: #5e6ad2;
|
|
162
|
+
--accent-hover: #828fff;
|
|
163
|
+
--accent-tint: #18182f;
|
|
164
|
+
--border-t: rgba(255,255,255,0.05);
|
|
165
|
+
--border-ts: rgba(255,255,255,0.08);
|
|
166
|
+
--border-primary: #23252a;
|
|
167
|
+
--radius-sm: 4px;
|
|
168
|
+
--radius-md: 6px;
|
|
169
|
+
--radius-lg: 8px;
|
|
170
|
+
--radius-xl: 12px;
|
|
171
|
+
--radius-pill: 9999px;
|
|
172
|
+
--max-w: 1200px;
|
|
173
|
+
--tx: 0.16s cubic-bezier(0.25,0.46,0.45,0.94);
|
|
174
|
+
font-feature-settings: "cv11","ss03","cv02";
|
|
175
|
+
}
|
|
176
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
177
|
+
html { scroll-behavior: smooth; }
|
|
178
|
+
body {
|
|
179
|
+
font-family: 'Inter var','Inter',-apple-system,sans-serif;
|
|
180
|
+
background: var(--bg-primary);
|
|
181
|
+
color: var(--text-primary);
|
|
182
|
+
font-size: 16px; line-height: 1.5;
|
|
183
|
+
-webkit-font-smoothing: antialiased;
|
|
184
|
+
}
|
|
185
|
+
.container { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; }
|
|
186
|
+
|
|
187
|
+
/* Nav */
|
|
188
|
+
.nav {
|
|
189
|
+
position: sticky; top: 0; z-index: 50;
|
|
190
|
+
backdrop-filter: blur(12px);
|
|
191
|
+
background: rgba(8,9,10,0.8);
|
|
192
|
+
border-bottom: 1px solid var(--border-t);
|
|
193
|
+
}
|
|
194
|
+
.nav-inner {
|
|
195
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
196
|
+
height: 60px;
|
|
197
|
+
}
|
|
198
|
+
.brand-logo {
|
|
199
|
+
font-size: 15px; font-weight: 510; letter-spacing: -0.24px;
|
|
200
|
+
color: var(--text-primary); text-decoration: none;
|
|
201
|
+
display: flex; align-items: center; gap: 8px;
|
|
202
|
+
}
|
|
203
|
+
.brand-dot { width: 8px; height: 8px; background: var(--accent); border-radius: 2px; }
|
|
204
|
+
.nav-links { display: flex; align-items: center; gap: 24px; list-style: none; }
|
|
205
|
+
.nav-links a {
|
|
206
|
+
font-size: 13px; font-weight: 510; color: var(--text-tertiary);
|
|
207
|
+
text-decoration: none; transition: color var(--tx);
|
|
208
|
+
}
|
|
209
|
+
.nav-links a:hover { color: var(--text-primary); }
|
|
210
|
+
.nav-github {
|
|
211
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
212
|
+
height: 32px; padding: 0 12px;
|
|
213
|
+
background: var(--bg-panel); border: 1px solid var(--border-ts);
|
|
214
|
+
border-radius: var(--radius-md); font-size: 13px; font-weight: 510;
|
|
215
|
+
color: var(--text-secondary); text-decoration: none;
|
|
216
|
+
transition: border-color var(--tx);
|
|
217
|
+
}
|
|
218
|
+
.nav-github:hover { border-color: rgba(255,255,255,0.15); }
|
|
219
|
+
|
|
220
|
+
/* Hamburger (mobile only, hidden by default) */
|
|
221
|
+
.nav-hamburger-cb { display: none; }
|
|
222
|
+
.nav-hamburger-btn {
|
|
223
|
+
display: none; cursor: pointer; padding: 8px; margin: -8px;
|
|
224
|
+
border: 0; background: transparent;
|
|
225
|
+
}
|
|
226
|
+
.nav-hamburger-btn svg { display: block; width: 24px; height: 24px; }
|
|
227
|
+
|
|
228
|
+
/* Hero */
|
|
229
|
+
.hero {
|
|
230
|
+
padding: 112px 0 80px; text-align: center; position: relative; overflow: hidden;
|
|
231
|
+
}
|
|
232
|
+
.hero::before {
|
|
233
|
+
content: ""; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
|
|
234
|
+
width: 800px; height: 600px;
|
|
235
|
+
background: radial-gradient(ellipse, rgba(94,106,210,0.09) 0%, transparent 60%);
|
|
236
|
+
pointer-events: none; z-index: 0;
|
|
237
|
+
}
|
|
238
|
+
.hero-inner { position: relative; z-index: 1; }
|
|
239
|
+
.eyebrow {
|
|
240
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
241
|
+
padding: 4px 10px; background: var(--bg-panel);
|
|
242
|
+
border: 1px solid var(--border-ts); border-radius: var(--radius-pill);
|
|
243
|
+
font-size: 12px; font-weight: 510; color: var(--text-secondary);
|
|
244
|
+
margin-bottom: 24px; letter-spacing: -0.13px;
|
|
245
|
+
}
|
|
246
|
+
.eyebrow-dot {
|
|
247
|
+
width: 6px; height: 6px; background: var(--accent); border-radius: 50%;
|
|
248
|
+
box-shadow: 0 0 8px var(--accent);
|
|
249
|
+
}
|
|
250
|
+
h1 {
|
|
251
|
+
font-size: 60px; font-weight: 510; line-height: 1.0;
|
|
252
|
+
letter-spacing: -1.32px; color: var(--text-primary);
|
|
253
|
+
margin-bottom: 20px; max-width: 860px; margin-left: auto; margin-right: auto;
|
|
254
|
+
}
|
|
255
|
+
.h1-accent { color: var(--accent); }
|
|
256
|
+
.lede {
|
|
257
|
+
font-size: 18px; line-height: 1.55; color: var(--text-tertiary);
|
|
258
|
+
max-width: 620px; margin: 0 auto 36px; letter-spacing: -0.27px;
|
|
259
|
+
}
|
|
260
|
+
.cta-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 48px; }
|
|
261
|
+
.btn-primary {
|
|
262
|
+
display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px;
|
|
263
|
+
background: var(--accent); color: #fff; border: none; border-radius: var(--radius-md);
|
|
264
|
+
font-family: inherit; font-size: 14px; font-weight: 510; letter-spacing: -0.13px;
|
|
265
|
+
text-decoration: none; cursor: pointer; transition: background var(--tx);
|
|
266
|
+
}
|
|
267
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
268
|
+
.btn-ghost {
|
|
269
|
+
display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px;
|
|
270
|
+
background: transparent; color: var(--text-primary);
|
|
271
|
+
border: 1px solid var(--border-ts); border-radius: var(--radius-md);
|
|
272
|
+
font-family: inherit; font-size: 14px; font-weight: 510; letter-spacing: -0.13px;
|
|
273
|
+
text-decoration: none; cursor: pointer; transition: border-color var(--tx), background var(--tx);
|
|
274
|
+
}
|
|
275
|
+
.btn-ghost:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.12); }
|
|
276
|
+
.cmd-block {
|
|
277
|
+
display: inline-flex; align-items: center; gap: 12px;
|
|
278
|
+
background: var(--bg-panel); border: 1px solid var(--border-ts);
|
|
279
|
+
border-radius: var(--radius-md); padding: 12px 16px;
|
|
280
|
+
font-family: 'SF Mono',Menlo,Monaco,Consolas,monospace; font-size: 13px;
|
|
281
|
+
color: var(--text-secondary);
|
|
282
|
+
}
|
|
283
|
+
.cmd-prompt { color: var(--text-quaternary); user-select: none; }
|
|
284
|
+
.cmd-text { color: var(--text-primary); }
|
|
285
|
+
.cmd-copy {
|
|
286
|
+
background: transparent; border: 1px solid var(--border-ts);
|
|
287
|
+
color: var(--text-tertiary); border-radius: var(--radius-sm);
|
|
288
|
+
padding: 2px 6px; font-size: 11px; font-family: inherit;
|
|
289
|
+
cursor: pointer; transition: color var(--tx);
|
|
290
|
+
}
|
|
291
|
+
.cmd-copy:hover { color: var(--text-primary); }
|
|
292
|
+
|
|
293
|
+
/* Stats bar */
|
|
294
|
+
.stats-bar {
|
|
295
|
+
display: flex; justify-content: center; gap: 48px; flex-wrap: wrap;
|
|
296
|
+
padding: 20px 0 16px;
|
|
297
|
+
border-top: 1px solid var(--border-t); border-bottom: 1px solid var(--border-t);
|
|
298
|
+
}
|
|
299
|
+
.stat { text-align: center; }
|
|
300
|
+
.stat-num { font-size: 32px; font-weight: 510; letter-spacing: -0.64px; }
|
|
301
|
+
.stat-label { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
|
|
302
|
+
|
|
303
|
+
/* Section */
|
|
304
|
+
.section { padding: 80px 0; border-top: 1px solid var(--border-t); }
|
|
305
|
+
.section-eyebrow {
|
|
306
|
+
font-size: 12px; font-weight: 510; color: var(--accent);
|
|
307
|
+
text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;
|
|
308
|
+
}
|
|
309
|
+
.section h2 {
|
|
310
|
+
font-size: 36px; font-weight: 510; line-height: 1.1;
|
|
311
|
+
letter-spacing: -0.88px; color: var(--text-primary);
|
|
312
|
+
margin-bottom: 12px; max-width: 700px;
|
|
313
|
+
}
|
|
314
|
+
.section-lede {
|
|
315
|
+
font-size: 16px; color: var(--text-tertiary); max-width: 600px;
|
|
316
|
+
margin-bottom: 40px; line-height: 1.55;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Directory toolbar */
|
|
320
|
+
.dir-toolbar {
|
|
321
|
+
display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 24px;
|
|
322
|
+
}
|
|
323
|
+
.dir-search {
|
|
324
|
+
flex: 1; min-width: 200px; max-width: 280px;
|
|
325
|
+
background: var(--bg-panel); border: 1px solid var(--border-ts);
|
|
326
|
+
border-radius: var(--radius-md); padding: 8px 12px;
|
|
327
|
+
font-family: inherit; font-size: 13px; font-weight: 510; color: var(--text-primary);
|
|
328
|
+
outline: none; transition: border-color var(--tx);
|
|
329
|
+
}
|
|
330
|
+
.dir-search::placeholder { color: var(--text-quaternary); }
|
|
331
|
+
.dir-search:focus { border-color: rgba(255,255,255,0.18); }
|
|
332
|
+
.filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
333
|
+
.chip {
|
|
334
|
+
padding: 4px 10px; border-radius: var(--radius-pill);
|
|
335
|
+
font-size: 12px; font-weight: 510; cursor: pointer;
|
|
336
|
+
background: var(--bg-panel); border: 1px solid var(--border-ts);
|
|
337
|
+
color: var(--text-tertiary); transition: all var(--tx); white-space: nowrap;
|
|
338
|
+
}
|
|
339
|
+
.chip:hover { color: var(--text-primary); border-color: rgba(255,255,255,0.15); }
|
|
340
|
+
.chip.active {
|
|
341
|
+
background: var(--accent-tint); border-color: var(--accent);
|
|
342
|
+
color: var(--accent-hover);
|
|
343
|
+
}
|
|
344
|
+
.dir-count { font-size: 12px; color: var(--text-quaternary); margin-left: auto; white-space: nowrap; }
|
|
345
|
+
|
|
346
|
+
/* Brand grid */
|
|
347
|
+
.brand-grid {
|
|
348
|
+
display: grid;
|
|
349
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
350
|
+
gap: 12px;
|
|
351
|
+
}
|
|
352
|
+
.brand-card {
|
|
353
|
+
background: var(--bg-panel); border: 1px solid var(--border-t);
|
|
354
|
+
border-radius: var(--radius-lg); padding: 16px;
|
|
355
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
356
|
+
text-decoration: none; transition: background var(--tx), border-color var(--tx);
|
|
357
|
+
cursor: pointer;
|
|
358
|
+
}
|
|
359
|
+
.brand-card:hover { background: var(--bg-level-2); border-color: var(--border-ts); }
|
|
360
|
+
.brand-card-header { display: flex; align-items: center; gap: 8px; }
|
|
361
|
+
.brand-accent {
|
|
362
|
+
width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0;
|
|
363
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
364
|
+
}
|
|
365
|
+
.brand-domain {
|
|
366
|
+
font-size: 14px; font-weight: 510; color: var(--text-primary); letter-spacing: -0.13px;
|
|
367
|
+
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
368
|
+
}
|
|
369
|
+
.brand-badges { display: flex; gap: 4px; align-items: center; }
|
|
370
|
+
.badge-dark, .badge-light {
|
|
371
|
+
font-size: 10px; font-weight: 510; padding: 1px 6px; border-radius: 3px;
|
|
372
|
+
}
|
|
373
|
+
.badge-dark { background: rgba(255,255,255,0.06); color: var(--text-tertiary); }
|
|
374
|
+
.badge-light { background: rgba(255,255,255,0.04); color: var(--text-quaternary); }
|
|
375
|
+
.brand-font {
|
|
376
|
+
font-size: 11px; color: var(--text-quaternary);
|
|
377
|
+
font-family: 'SF Mono',Menlo,monospace;
|
|
378
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
379
|
+
}
|
|
380
|
+
.brand-score-row { display: flex; align-items: center; gap: 8px; }
|
|
381
|
+
.score-bar-wrap {
|
|
382
|
+
flex: 1; height: 3px; background: var(--bg-tertiary);
|
|
383
|
+
border-radius: 2px; overflow: hidden;
|
|
384
|
+
}
|
|
385
|
+
.score-bar { height: 100%; border-radius: 2px; transition: width 0.3s ease; }
|
|
386
|
+
.score-num { font-size: 11px; font-weight: 510; min-width: 28px; text-align: right; }
|
|
387
|
+
.brand-cat {
|
|
388
|
+
font-size: 10px; color: var(--text-quaternary); overflow: hidden;
|
|
389
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
390
|
+
}
|
|
391
|
+
.empty-state {
|
|
392
|
+
grid-column: 1/-1; text-align: center;
|
|
393
|
+
padding: 48px; color: var(--text-quaternary); font-size: 14px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Compare table */
|
|
397
|
+
.compare-wrap {
|
|
398
|
+
background: var(--bg-panel); border: 1px solid var(--border-ts);
|
|
399
|
+
border-radius: var(--radius-lg); overflow: hidden;
|
|
400
|
+
}
|
|
401
|
+
table.compare { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
402
|
+
.compare th, .compare td {
|
|
403
|
+
text-align: left; padding: 12px 20px;
|
|
404
|
+
border-bottom: 1px solid var(--border-t);
|
|
405
|
+
}
|
|
406
|
+
.compare th {
|
|
407
|
+
font-size: 11px; font-weight: 510; text-transform: uppercase;
|
|
408
|
+
letter-spacing: 0.5px; color: var(--text-tertiary);
|
|
409
|
+
background: var(--bg-level-2);
|
|
410
|
+
}
|
|
411
|
+
.compare tr:last-child td { border-bottom: none; }
|
|
412
|
+
.compare td:first-child { color: var(--text-secondary); }
|
|
413
|
+
.yes { color: #4ade80; font-weight: 510; }
|
|
414
|
+
.no { color: var(--text-quaternary); }
|
|
415
|
+
.ca-col { background: rgba(94,106,210,0.04); }
|
|
416
|
+
|
|
417
|
+
/* Steps */
|
|
418
|
+
.steps { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
|
|
419
|
+
.step {
|
|
420
|
+
background: var(--bg-panel); border: 1px solid var(--border-t);
|
|
421
|
+
border-radius: var(--radius-lg); padding: 24px;
|
|
422
|
+
}
|
|
423
|
+
.step-num {
|
|
424
|
+
display: inline-flex; width: 26px; height: 26px;
|
|
425
|
+
background: var(--accent-tint); color: var(--accent-hover);
|
|
426
|
+
border-radius: var(--radius-md); align-items: center; justify-content: center;
|
|
427
|
+
font-size: 12px; font-weight: 590; margin-bottom: 14px;
|
|
428
|
+
}
|
|
429
|
+
.step h3 { font-size: 16px; font-weight: 510; margin-bottom: 8px; letter-spacing: -0.27px; }
|
|
430
|
+
.step p { font-size: 13px; color: var(--text-tertiary); line-height: 1.55; }
|
|
431
|
+
.step p code {
|
|
432
|
+
font-size: 0.9em; color: var(--text-secondary);
|
|
433
|
+
background: var(--bg-level-3); padding: 1px 4px; border-radius: 3px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* Footer */
|
|
437
|
+
footer { padding: 40px 0 56px; border-top: 1px solid var(--border-t); }
|
|
438
|
+
.footer-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
439
|
+
.footer-meta { font-size: 12px; color: var(--text-quaternary); }
|
|
440
|
+
.footer-links { display: flex; gap: 20px; list-style: none; }
|
|
441
|
+
.footer-links a { font-size: 13px; color: var(--text-tertiary); text-decoration: none; }
|
|
442
|
+
.footer-links a:hover { color: var(--text-primary); }
|
|
443
|
+
|
|
444
|
+
/* Responsive */
|
|
445
|
+
@media (max-width: 768px) {
|
|
446
|
+
h1 { font-size: 38px; letter-spacing: -0.84px; }
|
|
447
|
+
.section h2 { font-size: 26px; }
|
|
448
|
+
.hero { padding: 72px 0 48px; }
|
|
449
|
+
.section { padding: 56px 0; }
|
|
450
|
+
.steps { grid-template-columns: 1fr; }
|
|
451
|
+
.cmd-block { font-size: 12px; }
|
|
452
|
+
.stats-bar { gap: 24px; }
|
|
453
|
+
|
|
454
|
+
/* Mobile nav — hamburger drawer */
|
|
455
|
+
.nav-hamburger-btn { display: inline-flex; color: var(--text-primary); }
|
|
456
|
+
.nav-github { display: none; }
|
|
457
|
+
.nav-links {
|
|
458
|
+
position: absolute; top: 100%; left: 0; right: 0;
|
|
459
|
+
flex-direction: column; align-items: stretch; gap: 0;
|
|
460
|
+
background: var(--bg-primary); border-bottom: 1px solid var(--border-ts);
|
|
461
|
+
padding: 16px 20px; transform: translateY(-12px);
|
|
462
|
+
opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s;
|
|
463
|
+
}
|
|
464
|
+
.nav-links li { width: 100%; }
|
|
465
|
+
.nav-links a {
|
|
466
|
+
display: block; padding: 12px 0; font-size: 15px;
|
|
467
|
+
color: var(--text-primary); border-bottom: 1px solid var(--border-tertiary, rgba(255,255,255,0.04));
|
|
468
|
+
}
|
|
469
|
+
.nav-hamburger-cb:checked ~ .container .nav-links {
|
|
470
|
+
transform: translateY(0); opacity: 1; pointer-events: auto;
|
|
471
|
+
}
|
|
472
|
+
.nav { position: relative; }
|
|
473
|
+
}
|
|
474
|
+
</style>
|
|
475
|
+
</head>
|
|
476
|
+
<body>
|
|
477
|
+
|
|
478
|
+
<nav class="nav">
|
|
479
|
+
<input type="checkbox" id="nav-toggle" class="nav-hamburger-cb">
|
|
480
|
+
<div class="container nav-inner">
|
|
481
|
+
<a href="/" class="brand-logo">
|
|
482
|
+
<span class="brand-dot"></span>
|
|
483
|
+
Prism
|
|
484
|
+
</a>
|
|
485
|
+
<ul class="nav-links">
|
|
486
|
+
<li><a href="#directory">Brands</a></li>
|
|
487
|
+
<li><a href="#compare">vs getdesign</a></li>
|
|
488
|
+
<li><a href="#fidelity">Fidelity</a></li>
|
|
489
|
+
<li><a href="#how">How it works</a></li>
|
|
490
|
+
<li><a href="https://github.com/paulsainton/clone-architect" target="_blank" rel="noopener">GitHub ↗</a></li>
|
|
491
|
+
</ul>
|
|
492
|
+
<a href="https://github.com/paulsainton/clone-architect" target="_blank" rel="noopener" class="nav-github">
|
|
493
|
+
GitHub ↗
|
|
494
|
+
</a>
|
|
495
|
+
<label for="nav-toggle" class="nav-hamburger-btn" aria-label="Menu">
|
|
496
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
497
|
+
</label>
|
|
498
|
+
</div>
|
|
499
|
+
</nav>
|
|
500
|
+
|
|
501
|
+
<section class="hero">
|
|
502
|
+
<div class="container hero-inner">
|
|
503
|
+
<div class="eyebrow">
|
|
504
|
+
<span class="eyebrow-dot"></span>
|
|
505
|
+
v${pkgVersion} · MIT · local-first · Playwright
|
|
506
|
+
</div>
|
|
507
|
+
<h1>Extract real design from<br><span class="h1-accent">any URL. Automatically.</span></h1>
|
|
508
|
+
<p class="lede">
|
|
509
|
+
Playwright + <code style="font-size:0.85em;color:var(--text-secondary)">getComputedStyle()</code>
|
|
510
|
+
→ a <a href="https://github.com/google-labs-code/design.md" target="_blank" rel="noopener" style="color:var(--text-secondary)">Google DESIGN.md</a> + tokens.json + screenshots.
|
|
511
|
+
The only extractor that <strong style="color:var(--text-secondary);font-weight:510">publishes its fidelity score — including where it falls short</strong>. No catalog. No $39/brand. MIT.
|
|
512
|
+
</p>
|
|
513
|
+
<div class="cta-row" style="flex-direction:column;gap:8px;align-items:flex-start">
|
|
514
|
+
<div style="font-size:11px;color:var(--text-tertiary);letter-spacing:0.08em;text-transform:uppercase;margin-bottom:2px">Install from source · npm package coming soon</div>
|
|
515
|
+
<div class="cmd-block">
|
|
516
|
+
<span class="cmd-prompt">$</span>
|
|
517
|
+
<span class="cmd-text">git clone github.com/paulsainton/clone-architect && npm i</span>
|
|
518
|
+
<button class="cmd-copy" onclick="navigator.clipboard.writeText('git clone https://github.com/paulsainton/clone-architect && cd clone-architect && npm i && npx playwright install chromium').then(()=>{this.textContent='✓';setTimeout(()=>{this.textContent='copy'},1500)})">copy</button>
|
|
519
|
+
</div>
|
|
520
|
+
<div style="font-size:11px;color:var(--text-tertiary);letter-spacing:0.08em;text-transform:uppercase;margin-top:8px;margin-bottom:2px">Extract any URL → DESIGN.md + tokens + screenshots + fidelity</div>
|
|
521
|
+
<div class="cmd-block">
|
|
522
|
+
<span class="cmd-prompt">$</span>
|
|
523
|
+
<span class="cmd-text">npx tsx scripts/clone.ts https://yoursite.com</span>
|
|
524
|
+
<button class="cmd-copy" onclick="navigator.clipboard.writeText('npx tsx scripts/clone.ts https://yoursite.com').then(()=>{this.textContent='✓';setTimeout(()=>{this.textContent='copy'},1500)})">copy</button>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="cta-row">
|
|
528
|
+
<a href="https://github.com/paulsainton/clone-architect" class="btn-primary" target="_blank" rel="noopener">
|
|
529
|
+
View on GitHub ↗
|
|
530
|
+
</a>
|
|
531
|
+
<a href="#directory" class="btn-ghost">Browse ${brands.length} extractions ↓</a>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</section>
|
|
535
|
+
|
|
536
|
+
<div class="container">
|
|
537
|
+
<div class="stats-bar">
|
|
538
|
+
<div class="stat">
|
|
539
|
+
<div class="stat-num" style="color:var(--accent)">${brands.length}</div>
|
|
540
|
+
<div class="stat-label">brands extracted</div>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="stat">
|
|
543
|
+
<div class="stat-num">${avgScore}<span style="font-size:0.55em;color:var(--text-tertiary)">/100</span></div>
|
|
544
|
+
<div class="stat-label">avg completeness</div>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="stat">
|
|
547
|
+
<div class="stat-num">${darkCount}</div>
|
|
548
|
+
<div class="stat-label">dark-mode sites</div>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="stat">
|
|
551
|
+
<div class="stat-num" style="color:#4ade80">MIT</div>
|
|
552
|
+
<div class="stat-label">license · local-first</div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<!-- DIRECTORY -->
|
|
558
|
+
<section class="section" id="directory">
|
|
559
|
+
<div class="container">
|
|
560
|
+
<div class="section-eyebrow">Directory</div>
|
|
561
|
+
<h2>${brands.length} design systems extracted</h2>
|
|
562
|
+
<p class="section-lede">
|
|
563
|
+
Each extraction ships with: raw-css.json (ground truth), desktop + mobile screenshots,
|
|
564
|
+
tokens.json, layout analysis, and a 24-section narrative DESIGN.md (Google DESIGN.md format).
|
|
565
|
+
</p>
|
|
566
|
+
|
|
567
|
+
<div class="dir-toolbar">
|
|
568
|
+
<input class="dir-search" type="search" placeholder="Search brands..." id="search" oninput="filterBrands()">
|
|
569
|
+
<div class="filter-chips" id="chips">
|
|
570
|
+
<button class="chip active" data-cat="all" onclick="setFilter(this,'all')">All</button>
|
|
571
|
+
${categories.map(c => `<button class="chip" data-cat="${c}" onclick="setFilter(this,'${c.replace(/'/g, "\\'")}')">${c}</button>`).join('\n ')}
|
|
572
|
+
</div>
|
|
573
|
+
<span class="dir-count" id="count">${brands.length} brands</span>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<div class="brand-grid" id="grid">
|
|
577
|
+
<!-- Rendered by JS -->
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
</section>
|
|
581
|
+
|
|
582
|
+
<!-- COMPARE -->
|
|
583
|
+
<section class="section" id="compare">
|
|
584
|
+
<div class="container">
|
|
585
|
+
<div class="section-eyebrow">Comparison</div>
|
|
586
|
+
<h2>Why not just use getdesign.md?</h2>
|
|
587
|
+
<p class="section-lede">
|
|
588
|
+
Respect to the VoltAgent team — getdesign.md proved the format (now a <a href="https://github.com/google-labs-code/design.md" target="_blank" rel="noopener" style="color:var(--text-secondary)">Google open standard</a>). It's a hand-curated catalog of 73 brands; Prism extracts any URL automatically and ships the proof. <strong style="color:var(--text-secondary);font-weight:510">Where getdesign still wins: hand-tuned editorial nuance — and we say so.</strong>
|
|
589
|
+
</p>
|
|
590
|
+
<div class="compare-wrap">
|
|
591
|
+
<table class="compare">
|
|
592
|
+
<thead>
|
|
593
|
+
<tr>
|
|
594
|
+
<th>Feature</th>
|
|
595
|
+
<th>getdesign.md</th>
|
|
596
|
+
<th class="ca-col">Prism</th>
|
|
597
|
+
</tr>
|
|
598
|
+
</thead>
|
|
599
|
+
<tbody>
|
|
600
|
+
<tr>
|
|
601
|
+
<td>Extract any URL</td>
|
|
602
|
+
<td class="no">73 pre-written brands</td>
|
|
603
|
+
<td class="yes ca-col">Unlimited · any URL</td>
|
|
604
|
+
</tr>
|
|
605
|
+
<tr>
|
|
606
|
+
<td>Source of truth</td>
|
|
607
|
+
<td class="no">Manually written</td>
|
|
608
|
+
<td class="yes ca-col">getComputedStyle() — the rendered truth</td>
|
|
609
|
+
</tr>
|
|
610
|
+
<tr>
|
|
611
|
+
<td>Raw CSS audit</td>
|
|
612
|
+
<td class="no">Not shipped</td>
|
|
613
|
+
<td class="yes ca-col">raw-css.json (360 KB for mistral.ai)</td>
|
|
614
|
+
</tr>
|
|
615
|
+
<tr>
|
|
616
|
+
<td>Visual proof</td>
|
|
617
|
+
<td class="no">None</td>
|
|
618
|
+
<td class="yes ca-col">Desktop 1440px + Mobile 390px screenshots</td>
|
|
619
|
+
</tr>
|
|
620
|
+
<tr>
|
|
621
|
+
<td>Structured tokens</td>
|
|
622
|
+
<td class="no">Markdown prose only</td>
|
|
623
|
+
<td class="yes ca-col">tokens.json — import directly in code</td>
|
|
624
|
+
</tr>
|
|
625
|
+
<tr>
|
|
626
|
+
<td>Component states</td>
|
|
627
|
+
<td class="no">Not captured</td>
|
|
628
|
+
<td class="yes ca-col">Hover / focus / pressed via Playwright interaction</td>
|
|
629
|
+
</tr>
|
|
630
|
+
<tr>
|
|
631
|
+
<td>Page structure skeleton</td>
|
|
632
|
+
<td class="no">Not included</td>
|
|
633
|
+
<td class="yes ca-col">§13 — section order + height + layout type</td>
|
|
634
|
+
</tr>
|
|
635
|
+
<tr>
|
|
636
|
+
<td>Real copy / CTAs</td>
|
|
637
|
+
<td class="no">Not included</td>
|
|
638
|
+
<td class="yes ca-col">§14 — h1/h2/h3/nav/CTAs extracted verbatim</td>
|
|
639
|
+
</tr>
|
|
640
|
+
<tr>
|
|
641
|
+
<td>Re-extract on demand</td>
|
|
642
|
+
<td class="no">Static — gets stale</td>
|
|
643
|
+
<td class="yes ca-col"><code>clone-architect update <domain></code></td>
|
|
644
|
+
</tr>
|
|
645
|
+
<tr>
|
|
646
|
+
<td>Completeness score</td>
|
|
647
|
+
<td class="no">No confidence signal</td>
|
|
648
|
+
<td class="yes ca-col">0-100 grade (A/B/C/D) per extraction</td>
|
|
649
|
+
</tr>
|
|
650
|
+
<tr>
|
|
651
|
+
<td>Fidelity score published</td>
|
|
652
|
+
<td class="no">Trust the author</td>
|
|
653
|
+
<td class="yes ca-col">Structure (SSIM) + flat-area vs original — incl. low scores</td>
|
|
654
|
+
</tr>
|
|
655
|
+
<tr>
|
|
656
|
+
<td>Editorial narrative</td>
|
|
657
|
+
<td style="color:#a3e635;font-weight:510">Hand-tuned by humans ✓</td>
|
|
658
|
+
<td class="no ca-col">Automated — honest, not hand-polished</td>
|
|
659
|
+
</tr>
|
|
660
|
+
<tr>
|
|
661
|
+
<td>DESIGN.md (Google standard)</td>
|
|
662
|
+
<td style="color:#4ade80;font-weight:510">Compatible</td>
|
|
663
|
+
<td class="yes ca-col">Compatible</td>
|
|
664
|
+
</tr>
|
|
665
|
+
<tr>
|
|
666
|
+
<td>Price per brand</td>
|
|
667
|
+
<td class="no">$39 custom + 2-day wait</td>
|
|
668
|
+
<td class="yes ca-col">Free · MIT · 90 seconds</td>
|
|
669
|
+
</tr>
|
|
670
|
+
</tbody>
|
|
671
|
+
</table>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</section>
|
|
675
|
+
|
|
676
|
+
<!-- MEASURED FIDELITY -->
|
|
677
|
+
<section class="section" id="fidelity">
|
|
678
|
+
<div class="container">
|
|
679
|
+
<div class="section-eyebrow">Measured fidelity</div>
|
|
680
|
+
<h2>We publish our scores — including the bad ones.</h2>
|
|
681
|
+
<p class="section-lede">
|
|
682
|
+
Prism rebuilds each page's real above-the-fold structure and scores it against the original screenshot with <strong style="color:var(--text-secondary);font-weight:510">two reproducible numbers</strong>: <em>structure</em> (SSIM — layout match) and <em>flat-area</em> (pixelmatch — fill match). Most tools never tell you whether the output is accurate. We do — even when it's low. That honesty <em>is</em> the moat.
|
|
683
|
+
</p>
|
|
684
|
+
<div class="compare-wrap">
|
|
685
|
+
<table class="compare">
|
|
686
|
+
<thead><tr><th>Site</th><th>Type</th><th class="ca-col">Structure (SSIM)</th><th>Flat-area</th></tr></thead>
|
|
687
|
+
<tbody>
|
|
688
|
+
<tr><td>linear.app</td><td class="no">CSS-in-JS (styled-components)</td><td class="yes ca-col">60</td><td>93</td></tr>
|
|
689
|
+
<tr><td>stripe.com</td><td class="no">marketing, mostly static</td><td class="yes ca-col">67</td><td>77</td></tr>
|
|
690
|
+
<tr><td>airbnb.com</td><td class="no">app shell / search UI</td><td class="ca-col" style="color:#facc15;font-weight:510">52</td><td>78</td></tr>
|
|
691
|
+
<tr><td>getdesign.md</td><td class="no">dark, image-heavy hero</td><td class="ca-col" style="color:#facc15;font-weight:510">31</td><td>82</td></tr>
|
|
692
|
+
</tbody>
|
|
693
|
+
</table>
|
|
694
|
+
</div>
|
|
695
|
+
<p style="font-size:13px;color:var(--text-tertiary);margin-top:16px;line-height:1.6;max-width:760px">
|
|
696
|
+
The structural rebuild scores <strong style="color:var(--text-secondary);font-weight:510">+4 to +9 on structure</strong> vs a palette-only re-skin of the same tokens — that delta is the real layout we reconstruct, and it's the number SSIM is built to see. Flat-area stays high but is background-dominated, so it can't tell a clone from a re-skin on its own. SSIM is a strict floor: we render neutral placeholders for product imagery we don't fetch, which caps it honestly. Every DESIGN.md also ends with a
|
|
697
|
+
<strong style="color:var(--text-secondary);font-weight:510">Known Gaps</strong> section listing exactly what we couldn't capture — no tool in this space does that.
|
|
698
|
+
</p>
|
|
699
|
+
</div>
|
|
700
|
+
</section>
|
|
701
|
+
|
|
702
|
+
<!-- HOW IT WORKS -->
|
|
703
|
+
<section class="section" id="how">
|
|
704
|
+
<div class="container">
|
|
705
|
+
<div class="section-eyebrow">How it works</div>
|
|
706
|
+
<h2>One command. ${brands.length > 1 ? 'Four' : 'Three'} artifacts.</h2>
|
|
707
|
+
<p class="section-lede">
|
|
708
|
+
No accounts, no API keys, no waiting.
|
|
709
|
+
Playwright loads the URL, scrapes computed styles, generates everything locally.
|
|
710
|
+
</p>
|
|
711
|
+
<div class="steps">
|
|
712
|
+
<div class="step">
|
|
713
|
+
<div class="step-num">1</div>
|
|
714
|
+
<h3>Playwright extraction</h3>
|
|
715
|
+
<p>Real browser headlessly loads the URL. <code>getComputedStyle()</code> captures every rendered value — not class names, actual pixels. Screenshots at 1440px and 390px.</p>
|
|
716
|
+
</div>
|
|
717
|
+
<div class="step">
|
|
718
|
+
<div class="step-num">2</div>
|
|
719
|
+
<h3>Semantic tokenization</h3>
|
|
720
|
+
<p>CSS custom properties (<code>--color-text-*</code>, <code>--color-bg-*</code>) map to semantic roles. OpenType features, variable font axes, translucent borders all surfaced automatically.</p>
|
|
721
|
+
</div>
|
|
722
|
+
<div class="step">
|
|
723
|
+
<div class="step-num">3</div>
|
|
724
|
+
<h3>DESIGN.md generation</h3>
|
|
725
|
+
<p>24 sections in the Google DESIGN.md format: Visual Theme, Colors, Typography, Components + state matrix, Layout, Motion, Depth, Do/Don'ts, Responsive, Agent Guide, Page Skeleton, Copy Library, Asset Inventory, CSS Raw Export, and a <strong style="color:var(--text-secondary);font-weight:510">Known Gaps</strong> section listing what couldn't be captured.</p>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
</section>
|
|
730
|
+
|
|
731
|
+
<footer>
|
|
732
|
+
<div class="container footer-row">
|
|
733
|
+
<div class="footer-meta">
|
|
734
|
+
Prism · MIT · Built by Paul Sainton ·
|
|
735
|
+
This site's design tokens were extracted by Prism from linear.app.
|
|
736
|
+
</div>
|
|
737
|
+
<ul class="footer-links">
|
|
738
|
+
<li><a href="https://github.com/paulsainton/clone-architect" target="_blank" rel="noopener">GitHub</a></li>
|
|
739
|
+
<li><a href="https://www.npmjs.com/package/clone-architect" target="_blank" rel="noopener">npm</a></li>
|
|
740
|
+
</ul>
|
|
741
|
+
</div>
|
|
742
|
+
</footer>
|
|
743
|
+
|
|
744
|
+
<script>
|
|
745
|
+
// Inline catalog data (baked at build time)
|
|
746
|
+
const BRANDS = ${catalogJson};
|
|
747
|
+
|
|
748
|
+
let currentFilter = 'all';
|
|
749
|
+
let currentSearch = '';
|
|
750
|
+
|
|
751
|
+
function gradeColor(s) {
|
|
752
|
+
if (s >= 90) return '#4ade80';
|
|
753
|
+
if (s >= 75) return '#a3e635';
|
|
754
|
+
if (s >= 60) return '#facc15';
|
|
755
|
+
return '#f87171';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function gradeLabel(s) {
|
|
759
|
+
if (s >= 90) return 'A';
|
|
760
|
+
if (s >= 75) return 'B';
|
|
761
|
+
if (s >= 60) return 'C';
|
|
762
|
+
return 'D';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function accentHex(accent) {
|
|
766
|
+
if (!accent) return '#5e6ad2';
|
|
767
|
+
if (accent.startsWith('#')) return accent;
|
|
768
|
+
const m = accent.match(/\\d+/g);
|
|
769
|
+
if (m && m.length >= 3) {
|
|
770
|
+
return '#' + m.slice(0,3).map(v => parseInt(v).toString(16).padStart(2,'0')).join('');
|
|
771
|
+
}
|
|
772
|
+
return '#5e6ad2';
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function renderGrid() {
|
|
776
|
+
const q = currentSearch.toLowerCase();
|
|
777
|
+
const filtered = BRANDS.filter(b => {
|
|
778
|
+
const matchCat = currentFilter === 'all' || b.category === currentFilter;
|
|
779
|
+
const matchQ = !q || b.domain.toLowerCase().includes(q) ||
|
|
780
|
+
(b.font||'').toLowerCase().includes(q) ||
|
|
781
|
+
(b.category||'').toLowerCase().includes(q);
|
|
782
|
+
return matchCat && matchQ;
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const grid = document.getElementById('grid');
|
|
786
|
+
const count = document.getElementById('count');
|
|
787
|
+
count.textContent = filtered.length + ' brand' + (filtered.length !== 1 ? 's' : '');
|
|
788
|
+
|
|
789
|
+
if (filtered.length === 0) {
|
|
790
|
+
grid.innerHTML = '<div class="empty-state">No brands match your search.</div>';
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
grid.innerHTML = filtered.map(b => {
|
|
795
|
+
const hex = accentHex(b.accent);
|
|
796
|
+
const score = b.completeness || 0;
|
|
797
|
+
const grade = gradeLabel(score);
|
|
798
|
+
const scoreColor = gradeColor(score);
|
|
799
|
+
const showcaseHref = 'brands/' + b.domain + '/';
|
|
800
|
+
const hasShowcase = b.hasDesignMd;
|
|
801
|
+
const fontStr = b.font ? b.font.split(',')[0].replace(/['"]/g,'').trim() : '';
|
|
802
|
+
return \`<a class="brand-card" href="\${hasShowcase ? showcaseHref : '#'}" \${hasShowcase ? 'target="_blank" rel="noopener"' : 'onclick="return false"'} title="\${b.domain}">
|
|
803
|
+
<div class="brand-card-header">
|
|
804
|
+
<span class="brand-accent" style="background:\${hex}"></span>
|
|
805
|
+
<span class="brand-domain">\${b.domain}</span>
|
|
806
|
+
<div class="brand-badges">
|
|
807
|
+
<span class="\${b.dark ? 'badge-dark' : 'badge-light'}">\${b.dark ? 'dark' : 'light'}</span>
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
\${fontStr ? \`<div class="brand-font">\${fontStr}</div>\` : ''}
|
|
811
|
+
<div class="brand-score-row">
|
|
812
|
+
<div class="score-bar-wrap">
|
|
813
|
+
<div class="score-bar" style="width:\${score}%;background:\${scoreColor}"></div>
|
|
814
|
+
</div>
|
|
815
|
+
<span class="score-num" style="color:\${scoreColor}">\${grade}</span>
|
|
816
|
+
</div>
|
|
817
|
+
<div class="brand-cat">\${b.category || ''}</div>
|
|
818
|
+
</a>\`;
|
|
819
|
+
}).join('');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function setFilter(el, cat) {
|
|
823
|
+
currentFilter = cat;
|
|
824
|
+
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
|
825
|
+
el.classList.add('active');
|
|
826
|
+
renderGrid();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function filterBrands() {
|
|
830
|
+
currentSearch = document.getElementById('search').value;
|
|
831
|
+
renderGrid();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Initial render
|
|
835
|
+
renderGrid();
|
|
836
|
+
</script>
|
|
837
|
+
|
|
838
|
+
</body>
|
|
839
|
+
</html>`;
|
|
840
|
+
|
|
841
|
+
await writeFile(OUT_PATH, html);
|
|
842
|
+
console.log(`✅ Site generated → ${OUT_PATH}`);
|
|
843
|
+
console.log(` ${brands.length} brands · avg ${avgScore}/100 · ${categories.length} categories`);
|
|
844
|
+
console.log(`\nDeploy: copy public/ to your web server root`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
main().catch(err => { console.error(err); process.exit(1); });
|