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,237 @@
1
+ {
2
+ "meta": {
3
+ "source": "https://uniquekicks.be",
4
+ "domain": "uniquekicks.be",
5
+ "extractedAt": "2026-05-31T04:14:13.647Z",
6
+ "tokenizedAt": "2026-05-31T04:14:34.479Z"
7
+ },
8
+ "colors": {
9
+ "background": {
10
+ "primary": "rgb(240, 240, 240)",
11
+ "secondary": "rgb(255, 255, 255)",
12
+ "tertiary": "rgba(255, 255, 255, 0.12)"
13
+ },
14
+ "text": {
15
+ "primary": "rgb(26, 26, 26)",
16
+ "secondary": "rgba(26, 26, 26, 0.65)",
17
+ "muted": "rgb(26, 26, 26)"
18
+ },
19
+ "accent": {
20
+ "primary": "rgb(0, 111, 207)",
21
+ "secondary": "rgb(30, 55, 100)"
22
+ },
23
+ "border": "rgb(228, 222, 222)",
24
+ "shadow": "rgba(0,0,0,0.1)"
25
+ },
26
+ "typography": {
27
+ "fontFamily": {
28
+ "primary": "Barlow",
29
+ "secondary": "GTStandard-M",
30
+ "mono": "monospace"
31
+ },
32
+ "fontSize": {
33
+ "xs": "9px",
34
+ "sm": "14px",
35
+ "base": "16px",
36
+ "lg": "20px",
37
+ "xl": "24px",
38
+ "2xl": "30px",
39
+ "3xl": "36px",
40
+ "4xl": "48px"
41
+ },
42
+ "fontWeight": {
43
+ "normal": "400",
44
+ "medium": "500",
45
+ "semibold": "700",
46
+ "bold": "700"
47
+ },
48
+ "lineHeight": {
49
+ "tight": "1.6",
50
+ "normal": "1.6",
51
+ "relaxed": "1.6"
52
+ },
53
+ "letterSpacing": {
54
+ "tight": "-0.020em",
55
+ "normal": "0em",
56
+ "wide": "-0.060em"
57
+ }
58
+ },
59
+ "spacing": {
60
+ "xxs": "2px",
61
+ "xs": "4px",
62
+ "sm": "8px",
63
+ "md": "12px",
64
+ "base": "16px",
65
+ "lg": "24px",
66
+ "xl": "32px",
67
+ "2xl": "48px",
68
+ "3xl": "64px"
69
+ },
70
+ "borderRadius": {
71
+ "none": "0px",
72
+ "xs": "4px",
73
+ "sm": "8px",
74
+ "md": "12px",
75
+ "lg": "12px",
76
+ "xl": "12px",
77
+ "full": "9999px"
78
+ },
79
+ "shadows": {
80
+ "shadow-1": "rgba(63, 63, 68, 0.4) 0px 4px 10px 0px",
81
+ "shadow-2": "rgba(0, 0, 0, 0.4) 0px 9999px 0px 9999px",
82
+ "shadow-3": "rgba(26, 26, 26, 0.1) 0px 5px 15px 0px, rgba(26, 26, 26, 0.12) 0px 0px 0px 1px",
83
+ "shadow-4": "rgba(26, 26, 26, 0.12) 0px 0px 0px 1px",
84
+ "shadow-5": "rgba(0, 0, 0, 0.15) 0px 0px 30px 0px"
85
+ },
86
+ "transitions": {
87
+ "transition-1": "all",
88
+ "transition-2": "background 0.25s",
89
+ "transition-3": "opacity 0.2s ease-in-out",
90
+ "transition-4": "transform 0.2s ease-in-out",
91
+ "transition-5": "background-size 0.3s ease-in-out"
92
+ },
93
+ "layout": {
94
+ "maxWidth": "1600px",
95
+ "headerHeight": "94px",
96
+ "sidebarWidth": "1440px",
97
+ "gap": "normal",
98
+ "containerPadding": "0px"
99
+ },
100
+ "cssCustomProperties": {
101
+ "--on-sale-text": "248 58 58",
102
+ "--sticky-header-enabled": "1",
103
+ "--heading-font-weight": "700",
104
+ "--header-height": "94px",
105
+ "--spacing-4": "1rem",
106
+ "--text-font-family": "Barlow, sans-serif",
107
+ "--spacing-1": "0.25rem",
108
+ "--spacing-9": "2.25rem",
109
+ "--sold-out-badge-background": "0 0 0",
110
+ "--spacing-36": "9rem",
111
+ "--shadow-block": "0px 18px 50px rgb(26 26 26 / 0.1)",
112
+ "--product-card-background": "255 255 255",
113
+ "--spacing-64": "16rem",
114
+ "--accent": "26 26 26",
115
+ "--cursor-close-svg-url": "url(//www.uniquekicks.be/cdn/shop/t/19/assets/cursor-close.svg?v=147174565022153725511740963773)",
116
+ "--shadow-md": "0 5px 30px rgb(26 26 26 / 0.1)",
117
+ "--text-h2": "3rem",
118
+ "--spacing-48": "12rem",
119
+ "--text-lg": "1.25rem",
120
+ "--background-primary": "240 240 240",
121
+ "--product-list-row-gap": "3rem",
122
+ "--spacing-52": "13rem",
123
+ "--rounded-sm": "0.375rem",
124
+ "--footer-text": "26 26 26",
125
+ "--spacing-4-5": "1.125rem",
126
+ "--text-h6": "1.25rem",
127
+ "--spacing-56": "14rem",
128
+ "--dialog-background": "255 255 255",
129
+ "--spacing-6": "1.5rem",
130
+ "--heading-font-family": "Barlow, sans-serif",
131
+ "--section-inner-max-spacing-block": "4.5rem",
132
+ "--button-text-secondary": "26 26 26",
133
+ "--success-text": "0 163 65",
134
+ "--cursor-zoom-out-svg-url": "url(//www.uniquekicks.be/cdn/shop/t/19/assets/cursor-zoom-out.svg?v=16155520337305705181740963761)",
135
+ "--spacing-1-5": "0.375rem",
136
+ "--spacing-14": "3.5rem",
137
+ "--rounded-xs": "0.25rem",
138
+ "--sticky-announcement-bar-enabled": "0",
139
+ "--spacing-12": "3rem",
140
+ "--product-list-column-gap": "1.5rem",
141
+ "--star-color": "255 183 74",
142
+ "--warning-text": "255 183 74",
143
+ "--text-font-weight": "500",
144
+ "--on-sale-badge-background": "248 58 58",
145
+ "--success-background": "224 244 232",
146
+ "--transform-logical-flip": "1",
147
+ "--input-height": "3.125rem",
148
+ "--spacing-80": "20rem",
149
+ "--spacing-72": "18rem",
150
+ "--text-h4": "2rem",
151
+ "--sold-out-badge-text": "255 255 255",
152
+ "--transform-origin-end": "right",
153
+ "--spacing-32": "8rem",
154
+ "--checkmark-svg-url": "url(//www.uniquekicks.be/cdn/shop/t/19/assets/checkmark.svg?v=77552481021870063511740963761)",
155
+ "--button-background-primary": "26 26 26",
156
+ "--text-letter-spacing": "0.0em",
157
+ "--border-color": "26 26 26 / 0.12",
158
+ "--shadow": "0 5px 15px rgb(26 26 26 / 0.1)",
159
+ "--kl-reviews-color-light-gray": "#e9e9eb",
160
+ "--transform-origin-start": "left",
161
+ "--button-background-secondary": "240 196 23",
162
+ "--error-text": "248 58 58",
163
+ "--spacing-8": "2rem",
164
+ "--heading-letter-spacing": "-0.02em",
165
+ "--text-sm": "0.875rem",
166
+ "--announcement-bar-height": "48px",
167
+ "--section-inner-spacing-inline": "4.5rem",
168
+ "--spacing-20": "5rem",
169
+ "--rounded": "0.75rem",
170
+ "--spacing-2": "0.5rem",
171
+ "--rounded-input": "0.5rem",
172
+ "--section-outer-spacing-block": "6rem",
173
+ "--primary-badge-text": "255 255 255",
174
+ "--spacing-7-5": "1.875rem",
175
+ "--spacing-44": "11rem",
176
+ "--rounded-lg": "1.5rem",
177
+ "--text-h5": "1.5rem",
178
+ "--input-gap": "1rem",
179
+ "--primary-badge-background": "128 60 238",
180
+ "--heading-text-transform": "normal",
181
+ "--container-narrow-max-width": "1350px",
182
+ "--section-stack-spacing-block": "3rem",
183
+ "--error-background": "254 231 231",
184
+ "--shadow-sm": "0 2px 8px rgb(26 26 26 / 0.1)",
185
+ "--spacing-3": "0.75rem",
186
+ "--spacing-96": "24rem",
187
+ "--warning-background": "255 246 233",
188
+ "--text-primary": "26 26 26",
189
+ "--rounded-button": "0.25rem",
190
+ "--spacing-11": "2.75rem",
191
+ "--spacing-6-5": "1.625rem",
192
+ "--spacing-28": "7rem",
193
+ "--cursor-zoom-in-svg-url": "url(//www.uniquekicks.be/cdn/shop/t/19/assets/cursor-zoom-in.svg?v=154953035094101115921740963763)",
194
+ "--text-font-style": "normal",
195
+ "--rounded-full": "9999px",
196
+ "--container-max-width": "1600px",
197
+ "--text-xs": "0.75rem",
198
+ "--on-sale-badge-text": "255 255 255",
199
+ "--spacing-5": "1.25rem",
200
+ "--spacing-8-5": "2.125rem",
201
+ "--spacing-5-5": "1.375rem",
202
+ "--spacing-24": "6rem",
203
+ "--spacing-3-5": "0.875rem",
204
+ "--text-h1": "3.75rem",
205
+ "--spacing-7": "1.75rem",
206
+ "--header-text": "26 26 26",
207
+ "--spacing-40": "10rem",
208
+ "--spacing-10": "2.5rem",
209
+ "--button-text-primary": "255 255 255",
210
+ "--sticky-area-height": "calc(0 * 48px + 1 * 94px)",
211
+ "--spacing-16": "4rem",
212
+ "--spacing-0-5": "0.125rem",
213
+ "--heading-font-style": "normal",
214
+ "--header-background": "240 240 240",
215
+ "--container-gutter": "3rem",
216
+ "--spacing-18": "4.5rem",
217
+ "--text-h0": "5rem",
218
+ "--text-h3": "2.25rem",
219
+ "--input-padding-inline": "1.25rem",
220
+ "--footer-background": "255 255 255",
221
+ "--product-card-text": "26 26 26",
222
+ "--spacing-2-5": "0.625rem",
223
+ "--text-base": "1.0rem",
224
+ "--spacing-60": "15rem",
225
+ "--grid-gutter": "1.5rem",
226
+ "--spacing-9-5": "2.375rem",
227
+ "--background": "240 240 240",
228
+ "--text-color": "26 26 26",
229
+ "--header-logo-width": "125px",
230
+ "--header-padding-block": "2.125rem",
231
+ "--header-transparent-text-color": "255 255 255",
232
+ "--header-background-blur-radius": "0px",
233
+ "--header-grid-template": "\"logo main-nav secondary-nav\" / minmax(0, 1fr) fit-content(55%) minmax(0, 1fr)",
234
+ "--header-background-opacity": "1.0",
235
+ "--header-logo-height": "21px"
236
+ }
237
+ }
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "prism-design",
3
+ "version": "2.13.0",
4
+ "description": "Refract any website into its real design system — computed CSS, screenshots, narrative DESIGN.md — in 90 seconds. The verifiable alternative to manually-written design docs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "prism": "bin/prism.mjs",
8
+ "clone-architect": "bin/prism.mjs"
9
+ },
10
+ "main": "./bin/clone-architect.mjs",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/paulsainton/prism.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/paulsainton/prism/issues"
17
+ },
18
+ "author": "Paul Sainton",
19
+ "files": [
20
+ "bin/",
21
+ "catalog/",
22
+ "extractions/*/DESIGN.md",
23
+ "extractions/*/tokens.json",
24
+ "scripts/clone.ts",
25
+ "scripts/extract.ts",
26
+ "scripts/analyze.ts",
27
+ "scripts/tokenize.ts",
28
+ "scripts/generate-design-md.ts",
29
+ "scripts/generate-showcase.ts",
30
+ "scripts/compare.ts",
31
+ "scripts/bank.ts",
32
+ "scripts/bank-register.ts",
33
+ "scripts/enrich-catalog.ts",
34
+ "scripts/regen-catalog.ts",
35
+ "scripts/generate-site.ts",
36
+ "scripts/deploy-site.ts",
37
+ "scripts/generate-og-image.ts",
38
+ "scripts/diff-snapshots.ts",
39
+ "scripts/compare-vs-gd.ts",
40
+ "scripts/compare-vs-gd-final.ts",
41
+ "scripts/generate-final-pdf.ts",
42
+ "scripts/sync-catalog.ts",
43
+ "scripts/mass-extract.sh",
44
+ "scripts/post-process-all.sh",
45
+ "scripts/verify-checklist.ts",
46
+ "scripts/browser-stealth.ts",
47
+ "scripts/extractors/",
48
+ "scripts/shared/",
49
+ "templates/",
50
+ "README.md",
51
+ "LICENSE",
52
+ "CHANGELOG.md"
53
+ ],
54
+ "keywords": [
55
+ "design-system",
56
+ "design-tokens",
57
+ "design-md",
58
+ "css-extraction",
59
+ "computed-styles",
60
+ "playwright",
61
+ "design-extraction",
62
+ "clone-website",
63
+ "ai-coding",
64
+ "claude-code",
65
+ "cursor",
66
+ "design-dna",
67
+ "visual-design",
68
+ "frontend-tools",
69
+ "design-audit"
70
+ ],
71
+ "homepage": "https://prism.ps-tools.dev", "license": "MIT",
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ },
75
+ "scripts": {
76
+ "extract": "tsx scripts/extract.ts",
77
+ "analyze": "tsx scripts/analyze.ts",
78
+ "tokenize": "tsx scripts/tokenize.ts",
79
+ "compare": "tsx scripts/compare.ts",
80
+ "generate-design": "tsx scripts/generate-design-md.ts",
81
+ "showcase": "tsx scripts/generate-showcase.ts",
82
+ "clone": "tsx scripts/clone.ts",
83
+ "worktree-build": "tsx scripts/worktree-build.ts",
84
+ "section-build": "tsx scripts/section-builder.ts",
85
+ "extract-block": "tsx scripts/extract-block.ts",
86
+ "bank:inject": "tsx scripts/bank-inject.ts",
87
+ "test": "tsx tests/run-all.ts",
88
+ "bank": "tsx scripts/bank.ts",
89
+ "bank:register": "tsx scripts/bank.ts register --all",
90
+ "bank:stats": "tsx scripts/bank.ts stats",
91
+ "bank:query": "tsx scripts/bank.ts query",
92
+ "enrich-catalog": "tsx scripts/enrich-catalog.ts",
93
+ "regen-catalog": "tsx scripts/regen-catalog.ts",
94
+ "generate-site": "tsx scripts/generate-site.ts",
95
+ "deploy": "tsx scripts/deploy-site.ts",
96
+ "compare-vs-gd": "tsx scripts/compare-vs-gd.ts",
97
+ "compare-vs-gd-final": "tsx scripts/compare-vs-gd-final.ts",
98
+ "final-pdf": "tsx scripts/generate-final-pdf.ts",
99
+ "sync-catalog": "tsx scripts/sync-catalog.ts",
100
+ "mass-extract": "bash scripts/mass-extract.sh",
101
+ "post-process": "bash scripts/post-process-all.sh",
102
+ "verify-checklist": "tsx scripts/verify-checklist.ts",
103
+ "diff": "tsx scripts/diff-snapshots.ts",
104
+ "snapshot": "tsx scripts/diff-snapshots.ts --create",
105
+ "prepublishOnly": "tsc --noEmit && CI=true tsx tests/run-all.ts"
106
+ },
107
+ "dependencies": {
108
+ "@anthropic-ai/sdk": "^0.100.1",
109
+ "pixelmatch": "^6.0.0",
110
+ "playwright": "^1.52.0",
111
+ "playwright-extra": "^4.3.6",
112
+ "pngjs": "^7.0.0",
113
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
114
+ "tsx": "^4.19.0"
115
+ },
116
+ "devDependencies": {
117
+ "@types/node": "^22.0.0",
118
+ "@types/pngjs": "^6.0.5",
119
+ "typescript": "^5.7.0"
120
+ },
121
+ "postinstall": "echo '\\n🧬 Prism installed.\\n\\n Path A — Browse catalog (no setup required):\\n prism list\\n prism add linear.app\\n\\n Path B — Extract any URL (requires Playwright):\\n npx playwright install chromium\\n prism extract https://yoursite.com\\n'"
122
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Prism — Layout Analyzer
3
+ *
4
+ * Prend le raw-css.json d'une extraction et produit layout-analysis.md :
5
+ * - Type de layout (sidebar+main, top-nav, full-width, split)
6
+ * - Dimensions clés (sidebar width, header height, card sizes, gaps)
7
+ * - Navigation (type, items, active state)
8
+ * - Grille (columns, pattern, aspect-ratio)
9
+ * - Composants identifiés
10
+ * - Animations/transitions
11
+ */
12
+
13
+ import { readFile, writeFile } from 'fs/promises';
14
+ import { join } from 'path';
15
+ import { parseRgb, luminanceSimple } from './shared/css-helpers.js';
16
+
17
+ interface ExtractionResult {
18
+ url: string;
19
+ domain: string;
20
+ viewport: { width: number; height: number };
21
+ pageTitle: string;
22
+ cssCustomProperties: Record<string, string>;
23
+ elements: Record<string, { tag: string; classes: string[]; text: string; styles: Record<string, string>; children: number; rect: { x: number; y: number; width: number; height: number } } | null>;
24
+ sections: Array<{ index: number; tag: string; classes: string[]; role: string; estimatedPurpose: string; rect: { x: number; y: number; width: number; height: number }; styles: Record<string, string>; childCount: number }>;
25
+ allColors: string[];
26
+ allFontFamilies: string[];
27
+ allFontSizes: string[];
28
+ allBorderRadii: string[];
29
+ allShadows: string[];
30
+ allTransitions: string[];
31
+ images: { src: string; alt: string; width: number; height: number }[];
32
+ links: { href: string; text: string; isNav: boolean }[];
33
+ }
34
+
35
+ function detectLayoutType(data: ExtractionResult): string {
36
+ const sidebar = data.elements.sidebar;
37
+ const header = data.elements.header;
38
+ const nav = data.elements.nav;
39
+ const main = data.elements.main;
40
+
41
+ if (sidebar && sidebar.rect.height > 200) {
42
+ return 'sidebar + main content';
43
+ }
44
+ if (header && nav) {
45
+ if (main) return 'top-nav + main content';
46
+ return 'top-nav + full-width sections';
47
+ }
48
+ if (!header && !nav && !sidebar) {
49
+ return 'full-width (single column)';
50
+ }
51
+ return 'top-nav + content';
52
+ }
53
+
54
+ function parsePixels(val: string): number {
55
+ const match = val?.match(/^([\d.]+)px$/);
56
+ return match ? parseFloat(match[1]) : 0;
57
+ }
58
+
59
+ function formatColor(color: string): string {
60
+ return color.replace(/\s+/g, '');
61
+ }
62
+
63
+ function groupColors(colors: string[]): { backgrounds: string[]; text: string[]; accents: string[] } {
64
+ const backgrounds: string[] = [];
65
+ const text: string[] = [];
66
+ const accents: string[] = [];
67
+
68
+ for (const c of colors) {
69
+ const rgb = parseRgb(c);
70
+ if (!rgb) continue;
71
+ const lum = luminanceSimple(rgb);
72
+
73
+ if (lum > 0.85) backgrounds.push(c);
74
+ else if (lum < 0.2) text.push(c);
75
+ else accents.push(c);
76
+ }
77
+
78
+ return { backgrounds, text, accents };
79
+ }
80
+
81
+ function generateAnalysis(data: ExtractionResult): string {
82
+ const layoutType = detectLayoutType(data);
83
+ const colorGroups = groupColors(data.allColors);
84
+ const navLinks = data.links.filter(l => l.isNav);
85
+
86
+ const lines: string[] = [];
87
+
88
+ lines.push(`# Layout Analysis — ${data.domain}`);
89
+ lines.push(`> Extracted from ${data.url} on ${new Date().toISOString().split('T')[0]}`);
90
+ lines.push(`> Viewport: ${data.viewport.width}x${data.viewport.height}`);
91
+ lines.push('');
92
+
93
+ // ── Layout Type ──
94
+ lines.push('## Layout Type');
95
+ lines.push(`**${layoutType}**`);
96
+ lines.push('');
97
+
98
+ // ── Dimensions ──
99
+ lines.push('## Key Dimensions');
100
+
101
+ const dims: string[] = [];
102
+ const { sidebar, header, main, hero } = data.elements;
103
+ if (header) dims.push(`- Header height: ${header.rect.height}px`);
104
+ if (sidebar) dims.push(`- Sidebar width: ${sidebar.rect.width}px`);
105
+ if (main) dims.push(`- Main content width: ${main.rect.width}px (max-width: ${main.styles.maxWidth})`);
106
+ if (hero) dims.push(`- Hero height: ${hero.rect.height}px`);
107
+
108
+ const body = data.elements.body;
109
+ if (body) {
110
+ dims.push(`- Page background: ${formatColor(body.styles.backgroundColor)}`);
111
+ dims.push(`- Base font: ${body.styles.fontFamily.split(',')[0].trim()}`);
112
+ dims.push(`- Base font size: ${body.styles.fontSize}`);
113
+ dims.push(`- Base text color: ${formatColor(body.styles.color)}`);
114
+ }
115
+
116
+ lines.push(dims.length ? dims.join('\n') : '- No key dimensions detected');
117
+ lines.push('');
118
+
119
+ // ── Navigation ──
120
+ lines.push('## Navigation');
121
+ const navEl = data.elements.nav || data.elements.header;
122
+ if (navEl) {
123
+ lines.push(`- Type: ${navEl.tag} (${navEl.styles.display})`);
124
+ lines.push(`- Position: ${navEl.styles.position}`);
125
+ lines.push(`- Background: ${formatColor(navEl.styles.backgroundColor)}`);
126
+ lines.push(`- Height: ${navEl.rect.height}px`);
127
+ if (navLinks.length > 0) {
128
+ lines.push(`- Items (${navLinks.length}):`);
129
+ for (const link of navLinks.slice(0, 15)) {
130
+ lines.push(` - "${link.text}" → ${link.href}`);
131
+ }
132
+ }
133
+ } else {
134
+ lines.push('- No navigation element detected');
135
+ }
136
+ lines.push('');
137
+
138
+ // ── Sections ──
139
+ lines.push('## Page Sections');
140
+ for (const section of data.sections.slice(0, 20)) {
141
+ lines.push(`### ${section.estimatedPurpose} (${section.tag}.${section.classes.slice(0, 2).join('.')})`);
142
+ lines.push(`- Position: y=${section.rect.y}px, height=${section.rect.height}px`);
143
+ lines.push(`- Layout: ${section.styles.display} ${section.styles.flexDirection || section.styles.gridTemplateColumns || ''}`);
144
+ lines.push(`- Background: ${formatColor(section.styles.backgroundColor)}`);
145
+ lines.push(`- Padding: ${section.styles.padding}`);
146
+ lines.push(`- Children: ${section.childCount}`);
147
+ lines.push('');
148
+ }
149
+
150
+ // ── Components ──
151
+ lines.push('## Components Detected');
152
+ const componentKeys = ['card', 'button', 'input', 'badge', 'avatar', 'logo', 'dropdown', 'modal'];
153
+ for (const key of componentKeys) {
154
+ const el = data.elements[key];
155
+ if (!el) continue;
156
+ lines.push(`### ${key}`);
157
+ lines.push(`- Tag: ${el.tag}`);
158
+ lines.push(`- Classes: ${el.classes.join(', ') || 'none'}`);
159
+ lines.push(`- Size: ${el.rect.width}x${el.rect.height}px`);
160
+ lines.push(`- Background: ${formatColor(el.styles.backgroundColor)}`);
161
+ lines.push(`- Border: ${el.styles.border}`);
162
+ lines.push(`- Border radius: ${el.styles.borderRadius}`);
163
+ lines.push(`- Shadow: ${el.styles.boxShadow}`);
164
+ lines.push(`- Font: ${el.styles.fontSize} ${el.styles.fontWeight}`);
165
+ lines.push(`- Text: "${el.text.slice(0, 80)}"`);
166
+ lines.push('');
167
+ }
168
+
169
+ // ── Colors ──
170
+ lines.push('## Color Palette');
171
+ lines.push(`Total unique colors: ${data.allColors.length}`);
172
+ lines.push('');
173
+ if (colorGroups.backgrounds.length) {
174
+ lines.push(`### Backgrounds (${colorGroups.backgrounds.length})`);
175
+ for (const c of colorGroups.backgrounds.slice(0, 10)) lines.push(`- ${formatColor(c)}`);
176
+ lines.push('');
177
+ }
178
+ if (colorGroups.text.length) {
179
+ lines.push(`### Text (${colorGroups.text.length})`);
180
+ for (const c of colorGroups.text.slice(0, 10)) lines.push(`- ${formatColor(c)}`);
181
+ lines.push('');
182
+ }
183
+ if (colorGroups.accents.length) {
184
+ lines.push(`### Accents (${colorGroups.accents.length})`);
185
+ for (const c of colorGroups.accents.slice(0, 10)) lines.push(`- ${formatColor(c)}`);
186
+ lines.push('');
187
+ }
188
+
189
+ // ── Typography ──
190
+ lines.push('## Typography');
191
+ lines.push(`### Font Families (${data.allFontFamilies.length})`);
192
+ for (const f of data.allFontFamilies.slice(0, 10)) {
193
+ lines.push(`- ${f.split(',')[0].trim()}`);
194
+ }
195
+ lines.push('');
196
+ lines.push(`### Font Sizes (${data.allFontSizes.length})`);
197
+ const sortedSizes = [...data.allFontSizes].sort((a, b) => parsePixels(a) - parsePixels(b));
198
+ for (const s of sortedSizes) lines.push(`- ${s}`);
199
+ lines.push('');
200
+
201
+ // ── Border Radii ──
202
+ lines.push('## Border Radii');
203
+ for (const r of data.allBorderRadii.slice(0, 10)) lines.push(`- ${r}`);
204
+ lines.push('');
205
+
206
+ // ── Shadows ──
207
+ lines.push('## Shadows');
208
+ for (const s of data.allShadows.slice(0, 10)) lines.push(`- ${s}`);
209
+ lines.push('');
210
+
211
+ // ── Transitions ──
212
+ lines.push('## Transitions & Animations');
213
+ for (const t of data.allTransitions.slice(0, 10)) lines.push(`- ${t}`);
214
+ lines.push('');
215
+
216
+ // ── CSS Custom Properties ──
217
+ const varsCount = Object.keys(data.cssCustomProperties).length;
218
+ if (varsCount > 0) {
219
+ lines.push(`## CSS Custom Properties (${varsCount})`);
220
+ const entries = Object.entries(data.cssCustomProperties).slice(0, 50);
221
+ for (const [prop, val] of entries) {
222
+ lines.push(`- \`${prop}\`: ${val}`);
223
+ }
224
+ lines.push('');
225
+ }
226
+
227
+ // ── Images ──
228
+ if (data.images.length > 0) {
229
+ lines.push(`## Images (${data.images.length})`);
230
+ for (const img of data.images.slice(0, 15)) {
231
+ lines.push(`- ${img.width}x${img.height} — ${img.alt || 'no alt'} — ${img.src.slice(0, 80)}`);
232
+ }
233
+ lines.push('');
234
+ }
235
+
236
+ return lines.join('\n');
237
+ }
238
+
239
+ // ── CLI ──────────────────────────────────────────────────────────────
240
+
241
+ const domain = process.argv[2];
242
+
243
+ if (!domain) {
244
+ console.error('Usage: npm run analyze -- <domain>');
245
+ console.error('Example: npm run analyze -- linear.app');
246
+ process.exit(1);
247
+ }
248
+
249
+ const baseDir = join(process.cwd(), 'extractions', domain);
250
+
251
+ async function main() {
252
+ console.log(`\n📊 Analyzing layout for ${domain}...`);
253
+
254
+ const rawPath = join(baseDir, 'raw-css.json');
255
+ const raw = JSON.parse(await readFile(rawPath, 'utf-8'));
256
+
257
+ // Analyze desktop viewport
258
+ const desktopData = raw.desktop as ExtractionResult;
259
+ if (!desktopData) {
260
+ console.error('No desktop data found in raw-css.json');
261
+ process.exit(1);
262
+ }
263
+
264
+ const analysis = generateAnalysis(desktopData);
265
+
266
+ // Also generate mobile analysis if present
267
+ let mobileAnalysis = '';
268
+ if (raw.mobile) {
269
+ mobileAnalysis = '\n\n---\n\n' + generateAnalysis(raw.mobile as ExtractionResult);
270
+ }
271
+
272
+ const outputPath = join(baseDir, 'layout-analysis.md');
273
+ await writeFile(outputPath, analysis + mobileAnalysis);
274
+
275
+ console.log(`✅ Layout analysis saved to ${outputPath}`);
276
+ }
277
+
278
+ main().catch(err => {
279
+ console.error('Error:', err);
280
+ process.exit(1);
281
+ });