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.
Files changed (90) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/LICENSE +21 -0
  3. package/README.md +203 -0
  4. package/bin/clone-architect.mjs +476 -0
  5. package/bin/prism.mjs +467 -0
  6. package/catalog/index.json +1155 -0
  7. package/extractions/airbnb.com/DESIGN.md +1068 -0
  8. package/extractions/airbnb.com/tokens.json +507 -0
  9. package/extractions/attio.com/DESIGN.md +1295 -0
  10. package/extractions/attio.com/tokens.json +438 -0
  11. package/extractions/auroxdashboard.com/DESIGN.md +724 -0
  12. package/extractions/auroxdashboard.com/tokens.json +195 -0
  13. package/extractions/careerexplorer.com/DESIGN.md +1178 -0
  14. package/extractions/careerexplorer.com/tokens.json +141 -0
  15. package/extractions/chance.co/DESIGN.md +1209 -0
  16. package/extractions/chance.co/tokens.json +160 -0
  17. package/extractions/choisis-ton-avenir.com/DESIGN.md +1265 -0
  18. package/extractions/choisis-ton-avenir.com/tokens.json +227 -0
  19. package/extractions/example.com/DESIGN.md +436 -0
  20. package/extractions/example.com/tokens.json +91 -0
  21. package/extractions/getdesign.md/DESIGN.md +1009 -0
  22. package/extractions/getdesign.md/tokens.json +219 -0
  23. package/extractions/github.com/DESIGN.md +1130 -0
  24. package/extractions/github.com/tokens.json +2092 -0
  25. package/extractions/hello-charly.com/DESIGN.md +1146 -0
  26. package/extractions/hello-charly.com/tokens.json +322 -0
  27. package/extractions/hyperliquid.xyz/DESIGN.md +779 -0
  28. package/extractions/hyperliquid.xyz/tokens.json +598 -0
  29. package/extractions/instagram.com/DESIGN.md +996 -0
  30. package/extractions/instagram.com/tokens.json +1240 -0
  31. package/extractions/jobirl.com/DESIGN.md +1160 -0
  32. package/extractions/jobirl.com/tokens.json +139 -0
  33. package/extractions/life360.com/DESIGN.md +1133 -0
  34. package/extractions/life360.com/tokens.json +491 -0
  35. package/extractions/lifesum.com/DESIGN.md +965 -0
  36. package/extractions/lifesum.com/tokens.json +170 -0
  37. package/extractions/linear.app/DESIGN.md +1301 -0
  38. package/extractions/linear.app/tokens.json +732 -0
  39. package/extractions/mavoie.org/DESIGN.md +1148 -0
  40. package/extractions/mavoie.org/tokens.json +128 -0
  41. package/extractions/miro.com/DESIGN.md +1237 -0
  42. package/extractions/miro.com/tokens.json +401 -0
  43. package/extractions/notion.so/DESIGN.md +1319 -0
  44. package/extractions/notion.so/tokens.json +906 -0
  45. package/extractions/onetonline.org/DESIGN.md +909 -0
  46. package/extractions/onetonline.org/tokens.json +280 -0
  47. package/extractions/posthog.com/DESIGN.md +1024 -0
  48. package/extractions/posthog.com/tokens.json +197 -0
  49. package/extractions/revolut.com/DESIGN.md +1080 -0
  50. package/extractions/revolut.com/tokens.json +401 -0
  51. package/extractions/stripe.com/DESIGN.md +1272 -0
  52. package/extractions/stripe.com/tokens.json +794 -0
  53. package/extractions/switchcollective.com/DESIGN.md +1040 -0
  54. package/extractions/switchcollective.com/tokens.json +98 -0
  55. package/extractions/truity.com/DESIGN.md +970 -0
  56. package/extractions/truity.com/tokens.json +166 -0
  57. package/extractions/uniquekicks.be/DESIGN.md +1171 -0
  58. package/extractions/uniquekicks.be/tokens.json +237 -0
  59. package/package.json +122 -0
  60. package/scripts/analyze.ts +281 -0
  61. package/scripts/bank-register.ts +379 -0
  62. package/scripts/bank.ts +374 -0
  63. package/scripts/browser-stealth.ts +189 -0
  64. package/scripts/clone.ts +198 -0
  65. package/scripts/compare-vs-gd-final.ts +273 -0
  66. package/scripts/compare-vs-gd.ts +269 -0
  67. package/scripts/compare.ts +405 -0
  68. package/scripts/deploy-site.ts +181 -0
  69. package/scripts/diff-snapshots.ts +340 -0
  70. package/scripts/enrich-catalog.ts +212 -0
  71. package/scripts/extract.ts +2038 -0
  72. package/scripts/extractors/advanced.ts +524 -0
  73. package/scripts/extractors/widgets.ts +711 -0
  74. package/scripts/generate-design-md.ts +5775 -0
  75. package/scripts/generate-final-pdf.ts +274 -0
  76. package/scripts/generate-og-image.ts +87 -0
  77. package/scripts/generate-showcase.ts +1588 -0
  78. package/scripts/generate-site.ts +847 -0
  79. package/scripts/mass-extract.sh +91 -0
  80. package/scripts/post-process-all.sh +55 -0
  81. package/scripts/regen-catalog.ts +203 -0
  82. package/scripts/shared/cache.ts +149 -0
  83. package/scripts/shared/css-helpers.ts +263 -0
  84. package/scripts/shared/logger.ts +57 -0
  85. package/scripts/shared/named-colors.ts +355 -0
  86. package/scripts/shared/types.ts +220 -0
  87. package/scripts/sync-catalog.ts +105 -0
  88. package/scripts/tokenize.ts +988 -0
  89. package/templates/layout-template.md +52 -0
  90. package/templates/tokens-template.json +34 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * bank-register.ts — Prism Component Bank
3
+ * Reads existing extractions → creates ComponentSnapshot JSON files
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+
9
+ const EXTRACTIONS_DIR = path.join(process.cwd(), 'extractions');
10
+ export const BANK_DIR = path.join(process.cwd(), 'bank');
11
+
12
+ // ─── Interfaces (unifiées via shared/types.ts) ──────────────────────────────
13
+
14
+ import type {
15
+ SiteTokens,
16
+ ComponentSnapshot,
17
+ BankIndexEntry,
18
+ BankIndex,
19
+ BankCatalog,
20
+ } from './shared/types.js';
21
+
22
+ // Re-export pour backward compat
23
+ export type { SiteTokens, ComponentSnapshot, BankIndexEntry, BankIndex, BankCatalog };
24
+
25
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
26
+
27
+ export function slugify(str: string): string {
28
+ return str
29
+ .replace(/\./g, '-')
30
+ .replace(/[^a-z0-9-]/gi, '-')
31
+ .replace(/-+/g, '-')
32
+ .toLowerCase()
33
+ .replace(/^-|-$/g, '');
34
+ }
35
+
36
+ import {
37
+ parseRgb as parseRgbShared,
38
+ luminanceMemo as luminance,
39
+ colorHueTag,
40
+ } from './shared/css-helpers.js';
41
+
42
+ /** Wrapper avec fallback [0,0,0] pour compat bank-register */
43
+ function parseRgb(color: string): [number, number, number] {
44
+ return parseRgbShared(color) ?? [0, 0, 0];
45
+ }
46
+
47
+ function detectTags(domain: string, siteTokens: SiteTokens): string[] {
48
+ const tags: string[] = [];
49
+
50
+ // Theme
51
+ tags.push(siteTokens.isDark ? 'dark' : 'light');
52
+
53
+ // Industry
54
+ if (/sneakers|resell|wehaveit|limited|wethenew|shopify|product|store|ecom/i.test(domain)) {
55
+ tags.push('ecommerce');
56
+ }
57
+ if (/linear|raindrop|notion|figma|vercel|app|studio|dashboard/i.test(domain)) {
58
+ tags.push('saas');
59
+ }
60
+
61
+ // Font style
62
+ const font = siteTokens.typography.primary.toLowerCase().replace(/['"]/g, '');
63
+ if (/grotesque|grotesk|gothic|helvetica|inter|dm\s?sans|figtree|roboto|manrope|outfit/i.test(font)) {
64
+ tags.push('sans-modern');
65
+ } else if (/serif|times|georgia|playfair|garamond/i.test(font)) {
66
+ tags.push('serif');
67
+ } else if (/mono|code|courier|jetbrains/i.test(font)) {
68
+ tags.push('mono');
69
+ } else if (/montserrat|futura|avenir|proxima|gilroy/i.test(font)) {
70
+ tags.push('geometric-sans');
71
+ } else {
72
+ tags.push('custom-font');
73
+ }
74
+
75
+ // Border radius
76
+ const br = parseFloat(siteTokens.borderRadius.md || '0');
77
+ if (br <= 2) tags.push('sharp');
78
+ else if (br <= 8) tags.push('subtle-radius');
79
+ else if (br <= 16) tags.push('rounded');
80
+ else tags.push('pill-radius');
81
+
82
+ // Accent color hue
83
+ const hueTag = colorHueTag(siteTokens.colors.accent);
84
+ if (hueTag) tags.push(hueTag);
85
+ else tags.push('accent-neutral');
86
+
87
+ return [...new Set(tags)];
88
+ }
89
+
90
+ function buildSiteTokens(tokens: any): SiteTokens {
91
+ const bgColor = tokens.colors?.background?.primary || 'rgb(255, 255, 255)';
92
+ const isTransparent = bgColor.includes('rgba(0, 0, 0, 0)');
93
+ const resolvedBg = isTransparent ? 'rgb(255, 255, 255)' : bgColor;
94
+ const [r, g, b] = parseRgb(resolvedBg);
95
+
96
+ return {
97
+ colors: {
98
+ background: resolvedBg,
99
+ text: tokens.colors?.text?.primary || 'rgb(0, 0, 0)',
100
+ accent: tokens.colors?.accent?.primary || 'rgb(0, 0, 0)',
101
+ border: tokens.colors?.border || 'rgb(229, 231, 235)',
102
+ },
103
+ typography: {
104
+ primary: tokens.typography?.fontFamily?.primary || 'sans-serif',
105
+ secondary: tokens.typography?.fontFamily?.secondary,
106
+ },
107
+ borderRadius: {
108
+ sm: tokens.borderRadius?.sm || '4px',
109
+ md: tokens.borderRadius?.md || '8px',
110
+ full: tokens.borderRadius?.full || '9999px',
111
+ },
112
+ spacing: {
113
+ xs: tokens.spacing?.xs || '4px',
114
+ sm: tokens.spacing?.sm || '8px',
115
+ md: tokens.spacing?.md || '16px',
116
+ lg: tokens.spacing?.lg || '24px',
117
+ },
118
+ isDark: luminance(r, g, b) < 0.3,
119
+ };
120
+ }
121
+
122
+ function normalizeType(rawType: string): string {
123
+ if (rawType.startsWith('heading')) return 'heading';
124
+ if (rawType === 'buttons') return 'button';
125
+ if (rawType === 'cards') return 'card';
126
+ if (rawType === 'inputs') return 'input';
127
+ if (rawType === 'badges') return 'badge';
128
+ if (rawType === 'links') return 'link';
129
+ return rawType;
130
+ }
131
+
132
+ function labelVariant(type: string, rawType: string, index: number, styles: any): string {
133
+ if (type === 'button') {
134
+ if (index === 0) return 'Primary Button';
135
+ const bg = styles?.backgroundColor || '';
136
+ const [r, g, b] = parseRgb(bg);
137
+ const lum = luminance(r, g, b);
138
+ if (lum > 0.8) return `Ghost Button`;
139
+ if (lum > 0.4) return `Light Button`;
140
+ return `Dark Button`;
141
+ }
142
+ if (type === 'card') {
143
+ if (index === 0) return 'Primary Card';
144
+ return `Card Variant ${index + 1}`;
145
+ }
146
+ if (type === 'heading') {
147
+ const level = rawType.replace('heading', '').replace('H', 'h');
148
+ return `Heading ${level.toUpperCase() || (index + 1)}`;
149
+ }
150
+ if (type === 'input') return index === 0 ? 'Text Input' : `Input Variant ${index + 1}`;
151
+ if (type === 'badge') return index === 0 ? 'Primary Badge' : `Badge Variant ${index + 1}`;
152
+ if (type === 'link') return index === 0 ? 'Text Link' : `Link Variant ${index + 1}`;
153
+ return `${type} ${index + 1}`;
154
+ }
155
+
156
+ // ─── Core: register one domain ────────────────────────────────────────────────
157
+
158
+ export async function registerDomain(domain: string, verbose = true): Promise<number> {
159
+ const extractionPath = path.join(EXTRACTIONS_DIR, domain);
160
+
161
+ if (!fs.existsSync(extractionPath)) {
162
+ if (verbose) console.error(` ❌ Extraction introuvable: ${domain}`);
163
+ return 0;
164
+ }
165
+
166
+ const tokensPath = path.join(extractionPath, 'tokens.json');
167
+ const rawCssPath = path.join(extractionPath, 'raw-css.json');
168
+ const designMdPath = path.join(extractionPath, 'DESIGN.md');
169
+
170
+ if (!fs.existsSync(tokensPath) || !fs.existsSync(rawCssPath)) {
171
+ if (verbose) console.error(` ❌ tokens.json ou raw-css.json manquant pour ${domain}`);
172
+ return 0;
173
+ }
174
+
175
+ const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
176
+ const rawCss = JSON.parse(fs.readFileSync(rawCssPath, 'utf-8'));
177
+ const desktop = rawCss.desktop || rawCss;
178
+ const componentVariants: Record<string, any> = desktop.componentVariants || {};
179
+
180
+ const siteTokens = buildSiteTokens(tokens);
181
+ const baseTags = detectTags(domain, siteTokens);
182
+ const registeredAt = new Date().toISOString();
183
+ const extractedAt = tokens.meta?.extractedAt || tokens.meta?.tokenizedAt || registeredAt;
184
+ const url = tokens.meta?.source || `https://${domain.replace(/--.*$/, '')}/`;
185
+
186
+ // Ensure bank directory
187
+ fs.mkdirSync(BANK_DIR, { recursive: true });
188
+
189
+ // Load or init bank index / catalog
190
+ const indexPath = path.join(BANK_DIR, '_index.json');
191
+ const catalogPath = path.join(BANK_DIR, '_catalog.json');
192
+
193
+ const index: BankIndex = fs.existsSync(indexPath)
194
+ ? JSON.parse(fs.readFileSync(indexPath, 'utf-8'))
195
+ : { version: '1.0.0', updatedAt: registeredAt, components: [] };
196
+
197
+ const catalog: BankCatalog = fs.existsSync(catalogPath)
198
+ ? JSON.parse(fs.readFileSync(catalogPath, 'utf-8'))
199
+ : { version: '1.0.0', updatedAt: registeredAt, byType: {}, byDomain: {}, byTags: {} };
200
+
201
+ // Remove existing entries for this domain (clean re-register)
202
+ const domainSlug = slugify(domain);
203
+ index.components = index.components.filter(c => c.domain !== domain);
204
+ for (const t of Object.keys(catalog.byType)) {
205
+ catalog.byType[t] = catalog.byType[t].filter(id => !id.startsWith(domainSlug + '--'));
206
+ }
207
+ delete catalog.byDomain[domain];
208
+
209
+ let count = 0;
210
+
211
+ for (const [rawType, variants] of Object.entries(componentVariants)) {
212
+ const variantArray = Array.isArray(variants) ? variants : [variants];
213
+ const normType = normalizeType(rawType);
214
+
215
+ for (let i = 0; i < variantArray.length; i++) {
216
+ const variant = variantArray[i];
217
+ if (!variant || typeof variant !== 'object' || Object.keys(variant).length === 0) continue;
218
+
219
+ const id = `${domainSlug}--${rawType}--${i}`;
220
+ const label = labelVariant(normType, rawType, i, variant);
221
+
222
+ // Component-specific tags
223
+ const componentTags = [...baseTags];
224
+ if (normType === 'button') componentTags.push('button');
225
+ if (normType === 'card') componentTags.push('card');
226
+ if (normType === 'heading') componentTags.push('heading', 'typography');
227
+ if (normType === 'input') componentTags.push('form', 'input');
228
+ if (normType === 'badge') componentTags.push('badge');
229
+ if (normType === 'link') componentTags.push('link');
230
+
231
+ const screenshotFull = path.join(extractionPath, 'screenshots', 'desktop-full.png');
232
+ const screenshotThumb = path.join(extractionPath, 'screenshots', 'desktop-thumb.png');
233
+ const screenshotPath = fs.existsSync(screenshotFull)
234
+ ? path.relative(BANK_DIR, screenshotFull)
235
+ : fs.existsSync(screenshotThumb)
236
+ ? path.relative(BANK_DIR, screenshotThumb)
237
+ : undefined;
238
+
239
+ const snapshot: ComponentSnapshot = {
240
+ id,
241
+ domain,
242
+ extractionPath: path.relative(BANK_DIR, extractionPath),
243
+ url,
244
+ extractedAt,
245
+ registeredAt,
246
+ componentType: normType,
247
+ componentLabel: label,
248
+ variantIndex: i,
249
+ siteTokens,
250
+ styles: variant,
251
+ tags: [...new Set(componentTags)],
252
+ screenshotPath,
253
+ designMdPath: fs.existsSync(designMdPath)
254
+ ? path.relative(BANK_DIR, designMdPath)
255
+ : undefined,
256
+ };
257
+
258
+ // Save snapshot to bank/{id}/snapshot.json
259
+ const snapshotDir = path.join(BANK_DIR, id);
260
+ fs.mkdirSync(snapshotDir, { recursive: true });
261
+ fs.writeFileSync(
262
+ path.join(snapshotDir, 'snapshot.json'),
263
+ JSON.stringify(snapshot, null, 2)
264
+ );
265
+
266
+ // Update index
267
+ index.components.push({
268
+ id,
269
+ domain,
270
+ type: normType,
271
+ label,
272
+ tags: snapshot.tags,
273
+ accent: siteTokens.colors.accent,
274
+ background: siteTokens.colors.background,
275
+ isDark: siteTokens.isDark,
276
+ extractedAt,
277
+ registeredAt,
278
+ });
279
+
280
+ // Update catalog
281
+ if (!catalog.byType[normType]) catalog.byType[normType] = [];
282
+ if (!catalog.byType[normType].includes(id)) catalog.byType[normType].push(id);
283
+
284
+ if (!catalog.byDomain[domain]) catalog.byDomain[domain] = [];
285
+ catalog.byDomain[domain].push(id);
286
+
287
+ for (const tag of snapshot.tags) {
288
+ if (!catalog.byTags[tag]) catalog.byTags[tag] = [];
289
+ if (!catalog.byTags[tag].includes(id)) catalog.byTags[tag].push(id);
290
+ }
291
+
292
+ count++;
293
+ }
294
+ }
295
+
296
+ // ── Register block extractions (from extract-block.ts) ─────────────
297
+ const blocksDir = path.join(extractionPath, 'blocks');
298
+ if (fs.existsSync(blocksDir)) {
299
+ const blockFiles = fs.readdirSync(blocksDir).filter(f => f.endsWith('.json'));
300
+ for (const file of blockFiles) {
301
+ try {
302
+ const block = JSON.parse(fs.readFileSync(path.join(blocksDir, file), 'utf-8'));
303
+ const blockSlug = file.replace('.json', '');
304
+ const id = `${domainSlug}--block--${blockSlug}`;
305
+
306
+ const blockTags = [...baseTags, 'block'];
307
+ // Detect block type from selector/tag
308
+ const rootTag = block.rootNode?.tag || 'div';
309
+ if (['header', 'nav'].includes(rootTag)) blockTags.push('navigation');
310
+ if (['main', 'section', 'article'].includes(rootTag)) blockTags.push('content');
311
+ if (rootTag === 'footer') blockTags.push('footer');
312
+
313
+ const snapshot: ComponentSnapshot = {
314
+ id,
315
+ domain,
316
+ extractionPath: path.relative(BANK_DIR, extractionPath),
317
+ url: block.url || url,
318
+ extractedAt: block.extractedAt || extractedAt,
319
+ registeredAt,
320
+ componentType: 'block',
321
+ componentLabel: `Block: ${block.selector || blockSlug}`,
322
+ variantIndex: 0,
323
+ siteTokens,
324
+ styles: block.rootNode?.styles || {},
325
+ tags: [...new Set(blockTags)],
326
+ screenshotPath: block.screenshotPath
327
+ ? path.relative(BANK_DIR, block.screenshotPath)
328
+ : undefined,
329
+ designMdPath: fs.existsSync(designMdPath)
330
+ ? path.relative(BANK_DIR, designMdPath)
331
+ : undefined,
332
+ };
333
+
334
+ // Save snapshot
335
+ const snapshotDir = path.join(BANK_DIR, id);
336
+ fs.mkdirSync(snapshotDir, { recursive: true });
337
+ fs.writeFileSync(path.join(snapshotDir, 'snapshot.json'), JSON.stringify(snapshot, null, 2));
338
+
339
+ // Also copy the full block extraction (with DOM tree) alongside snapshot
340
+ fs.writeFileSync(path.join(snapshotDir, 'block-tree.json'), JSON.stringify(block, null, 2));
341
+
342
+ // Update index
343
+ index.components.push({
344
+ id, domain, type: 'block',
345
+ label: snapshot.componentLabel,
346
+ tags: snapshot.tags,
347
+ accent: siteTokens.colors.accent,
348
+ background: siteTokens.colors.background,
349
+ isDark: siteTokens.isDark,
350
+ extractedAt: block.extractedAt || extractedAt,
351
+ registeredAt,
352
+ });
353
+
354
+ // Update catalog
355
+ if (!catalog.byType['block']) catalog.byType['block'] = [];
356
+ catalog.byType['block'].push(id);
357
+ if (!catalog.byDomain[domain]) catalog.byDomain[domain] = [];
358
+ catalog.byDomain[domain].push(id);
359
+ for (const tag of snapshot.tags) {
360
+ if (!catalog.byTags[tag]) catalog.byTags[tag] = [];
361
+ if (!catalog.byTags[tag].includes(id)) catalog.byTags[tag].push(id);
362
+ }
363
+
364
+ count++;
365
+ } catch {
366
+ if (verbose) console.error(` ⚠️ Block ${file} skipped (parse error)`);
367
+ }
368
+ }
369
+ }
370
+
371
+ // Save updated index + catalog
372
+ index.updatedAt = registeredAt;
373
+ catalog.updatedAt = registeredAt;
374
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
375
+ fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
376
+
377
+ if (verbose) console.log(` ✅ ${domain} → ${count} composants enregistrés`);
378
+ return count;
379
+ }