dembrandt 0.11.0 → 0.12.1
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/README.md +63 -12
- package/index.js +28 -14
- package/lib/colors.js +46 -0
- package/lib/extractors/breakpoints.js +251 -0
- package/lib/extractors/colors.js +406 -0
- package/lib/extractors/components.js +406 -0
- package/lib/extractors/index.js +496 -0
- package/lib/extractors/logo.js +247 -0
- package/lib/extractors/spacing.js +178 -0
- package/lib/extractors/typography.js +136 -0
- package/lib/{design-md.js → formatters/markdown.js} +1 -1
- package/lib/{pdf.js → formatters/pdf.js} +1 -1
- package/lib/{display.js → formatters/terminal.js} +29 -1
- package/lib/types.js +204 -0
- package/package.json +2 -2
- package/lib/extractors.js +0 -2760
- /package/lib/{w3c-exporter.js → formatters/w3c.js} +0 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Extract a website's design system into design tokens in a few seconds: logo, colors, typography, borders, and more. One command.
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -77,7 +77,7 @@ Default: formatted terminal display only. Use `--save-output` to persist results
|
|
|
77
77
|
|
|
78
78
|
### Multi-Page Extraction
|
|
79
79
|
|
|
80
|
-
Analyze multiple pages to get a more complete picture of a site's design system. Results are merged into a single unified output with cross-page confidence boosting
|
|
80
|
+
Analyze multiple pages to get a more complete picture of a site's design system. Results are merged into a single unified output with cross-page confidence boosting: tokens appearing on multiple pages get higher confidence scores.
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
83
|
# Analyze homepage + 4 auto-discovered pages (default: 5 total)
|
|
@@ -133,7 +133,7 @@ The DTCG format is an industry-standard JSON schema that can be consumed by desi
|
|
|
133
133
|
|
|
134
134
|
### DESIGN.md
|
|
135
135
|
|
|
136
|
-
Use `--design-md` to generate a [DESIGN.md](https://stitch.withgoogle.com/docs/design-md) file
|
|
136
|
+
Use `--design-md` to generate a [DESIGN.md](https://stitch.withgoogle.com/docs/design-md) file, a plain-text design system document readable by AI agents.
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
dembrandt example.com --design-md
|
|
@@ -142,7 +142,7 @@ dembrandt example.com --design-md
|
|
|
142
142
|
|
|
143
143
|
### Brand Guide PDF
|
|
144
144
|
|
|
145
|
-
Use `--brand-guide` to generate a printable PDF summarizing the extracted design system
|
|
145
|
+
Use `--brand-guide` to generate a printable PDF summarizing the extracted design system: colors, typography, components, and logo on a single document.
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
148
|
dembrandt example.com --brand-guide
|
|
@@ -176,10 +176,63 @@ Opens http://localhost:5173 with API on port 3002.
|
|
|
176
176
|
- Spacing, shadows, border radius visualization
|
|
177
177
|
- Button and link component previews
|
|
178
178
|
- Dark/light theme toggle
|
|
179
|
-
- Section nav links on extraction pages
|
|
179
|
+
- Section nav links on extraction pages, jump directly to Colors, Typography, Shadows, etc. via a sticky sidebar
|
|
180
180
|
|
|
181
181
|
Extractions are performed via CLI (`dembrandt <url> --save-output`) and automatically appear in the UI.
|
|
182
182
|
|
|
183
|
+
## Recipes
|
|
184
|
+
|
|
185
|
+
**Quick brand scan**
|
|
186
|
+
```bash
|
|
187
|
+
dembrandt stripe.com
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Compare two sites**
|
|
191
|
+
```bash
|
|
192
|
+
dembrandt stripe.com --save-output
|
|
193
|
+
dembrandt braintree.com --save-output
|
|
194
|
+
# Compare output/stripe.com and output/braintree.com side by side
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Multi-page audit** — get a fuller picture across the whole site
|
|
198
|
+
```bash
|
|
199
|
+
dembrandt stripe.com --pages 10 --sitemap --save-output
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Spot-check a value** — verify a specific token fast
|
|
203
|
+
```bash
|
|
204
|
+
dembrandt stripe.com --json-only | grep -i "border-radius"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Export for Tailwind** — get spacing and color values into your config
|
|
208
|
+
```bash
|
|
209
|
+
dembrandt stripe.com --dtcg --save-output
|
|
210
|
+
# Use the .tokens.json with Style Dictionary to generate tailwind.config.js
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Export for Tokens Studio / Figma**
|
|
214
|
+
```bash
|
|
215
|
+
dembrandt stripe.com --dtcg --save-output
|
|
216
|
+
# Import the .tokens.json directly into Tokens Studio
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Generate DESIGN.md for your AI agent**
|
|
220
|
+
```bash
|
|
221
|
+
dembrandt stripe.com --design-md
|
|
222
|
+
# Point your agent at the output DESIGN.md
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Regression baseline** — snapshot now, catch drift later
|
|
226
|
+
```bash
|
|
227
|
+
dembrandt myapp.com --save-output --dtcg
|
|
228
|
+
# Store output as baseline, re-run after deploys and diff
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**CI / headless environments**
|
|
232
|
+
```bash
|
|
233
|
+
dembrandt myapp.com --no-sandbox --save-output
|
|
234
|
+
```
|
|
235
|
+
|
|
183
236
|
## Use Cases
|
|
184
237
|
|
|
185
238
|
- Design system documentation
|
|
@@ -204,9 +257,9 @@ Uses Playwright to render the page, reads computed styles from the DOM, analyzes
|
|
|
204
257
|
|
|
205
258
|
### Color Confidence
|
|
206
259
|
|
|
207
|
-
- High
|
|
208
|
-
- Medium
|
|
209
|
-
- Low
|
|
260
|
+
- High: Logo, primary interactive elements
|
|
261
|
+
- Medium: Secondary interactive elements, icons, navigation
|
|
262
|
+
- Low: Generic UI components (filtered from display)
|
|
210
263
|
- Only shows high and medium confidence colors in terminal. Full palette in JSON.
|
|
211
264
|
|
|
212
265
|
## Limitations
|
|
@@ -228,12 +281,10 @@ Dembrandt does not host, redistribute, or claim rights to any third-party brand
|
|
|
228
281
|
|
|
229
282
|
## Contributing
|
|
230
283
|
|
|
231
|
-
Bugs, weird sites, pull requests
|
|
284
|
+
Bugs, weird sites, pull requests. All welcome.
|
|
232
285
|
|
|
233
286
|
Open an [Issue](https://github.com/dembrandt/dembrandt/issues) or PR.
|
|
234
287
|
|
|
235
288
|
@thevangelist
|
|
236
289
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
MIT — do whatever you want with it.
|
|
290
|
+
MIT. Do whatever you want with it.
|
package/index.js
CHANGED
|
@@ -11,23 +11,27 @@ import { program } from "commander";
|
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import ora from "ora";
|
|
13
13
|
import { chromium, firefox } from "playwright-core";
|
|
14
|
-
import { extractBranding } from "./lib/extractors.js";
|
|
15
|
-
import { displayResults } from "./lib/
|
|
16
|
-
import { toW3CFormat } from "./lib/w3c
|
|
17
|
-
import { generatePDF } from "./lib/pdf.js";
|
|
18
|
-
import { generateDesignMd } from "./lib/
|
|
14
|
+
import { extractBranding } from "./lib/extractors/index.js";
|
|
15
|
+
import { displayResults } from "./lib/formatters/terminal.js";
|
|
16
|
+
import { toW3CFormat } from "./lib/formatters/w3c.js";
|
|
17
|
+
import { generatePDF } from "./lib/formatters/pdf.js";
|
|
18
|
+
import { generateDesignMd } from "./lib/formatters/markdown.js";
|
|
19
19
|
import { parseSitemap } from "./lib/discovery.js";
|
|
20
20
|
import { mergeResults } from "./lib/merger.js";
|
|
21
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
22
|
-
import { join } from "path";
|
|
21
|
+
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
22
|
+
import { join, dirname } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
23
24
|
import { checkRobotsTxt } from "./lib/robots.js";
|
|
24
25
|
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
28
|
+
|
|
25
29
|
program
|
|
26
30
|
.name("dembrandt")
|
|
27
31
|
.description("Extract design tokens from any website")
|
|
28
|
-
.version(
|
|
32
|
+
.version(version)
|
|
29
33
|
.argument("<url>")
|
|
30
|
-
.option("--browser <type>", "Browser to use (chromium|firefox)", "chromium")
|
|
34
|
+
.option("--browser <type>", "Browser to use (chromium|firefox); set BROWSER_CDP_ENDPOINT env var to connect to an existing Chromium instance via CDP", "chromium")
|
|
31
35
|
.option("--json-only", "Output raw JSON")
|
|
32
36
|
.option("--save-output", "Save JSON file to output folder")
|
|
33
37
|
.option("--dtcg", "Export in W3C Design Tokens (DTCG) format")
|
|
@@ -39,6 +43,7 @@ program
|
|
|
39
43
|
.option("--no-sandbox", "Disable browser sandbox (needed for Docker/CI)")
|
|
40
44
|
.option("--raw-colors", "Include pre-filter raw colors in JSON output")
|
|
41
45
|
.option("--screenshot <path>", "Save a screenshot of the page")
|
|
46
|
+
.option("--wcag", "Analyze WCAG contrast ratios between palette colors")
|
|
42
47
|
.option("--pages <n>", "Analyze up to N total pages including start URL (default: 5)", (v) => {
|
|
43
48
|
const n = parseInt(v, 10);
|
|
44
49
|
if (isNaN(n) || n < 1) throw new Error(`--pages must be a positive integer, got: ${v}`);
|
|
@@ -93,10 +98,18 @@ program
|
|
|
93
98
|
if (opts.noSandbox && opts.browser === 'chromium') {
|
|
94
99
|
launchArgs.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
95
100
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
if (process.env.BROWSER_CDP_ENDPOINT) {
|
|
102
|
+
if (opts.browser !== 'chromium') {
|
|
103
|
+
throw new Error("BROWSER_CDP_ENDPOINT is only supported with --browser chromium.");
|
|
104
|
+
}
|
|
105
|
+
spinner.text = "Connecting over CDP...";
|
|
106
|
+
browser = await browserType.connectOverCDP(process.env.BROWSER_CDP_ENDPOINT);
|
|
107
|
+
} else {
|
|
108
|
+
browser = await browserType.launch({
|
|
109
|
+
headless: !useHeaded,
|
|
110
|
+
args: launchArgs,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
100
113
|
|
|
101
114
|
try {
|
|
102
115
|
const isMultiPage = opts.pages || opts.sitemap;
|
|
@@ -108,6 +121,7 @@ program
|
|
|
108
121
|
slow: opts.slow,
|
|
109
122
|
screenshotPath: opts.screenshot,
|
|
110
123
|
discoverLinks: isMultiPage && !opts.sitemap ? maxPages : null,
|
|
124
|
+
wcag: opts.wcag,
|
|
111
125
|
});
|
|
112
126
|
|
|
113
127
|
// Multi-page crawl
|
|
@@ -170,7 +184,7 @@ program
|
|
|
170
184
|
await browser.close();
|
|
171
185
|
browser = null;
|
|
172
186
|
|
|
173
|
-
if (useHeaded) throw err;
|
|
187
|
+
if (useHeaded || process.env.BROWSER_CDP_ENDPOINT) throw err;
|
|
174
188
|
|
|
175
189
|
if (
|
|
176
190
|
err.message.includes("Timeout") ||
|
package/lib/colors.js
CHANGED
|
@@ -228,6 +228,52 @@ export function hexToRgb(hex) {
|
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Compute WCAG 2.1 relative luminance for a hex color.
|
|
233
|
+
* @param {string} hex
|
|
234
|
+
* @returns {number|null}
|
|
235
|
+
*/
|
|
236
|
+
export function relativeLuminance(hex) {
|
|
237
|
+
const rgb = hexToRgb(hex);
|
|
238
|
+
if (!rgb) return null;
|
|
239
|
+
return 0.2126 * srgbToLinear(rgb.r) + 0.7152 * srgbToLinear(rgb.g) + 0.0722 * srgbToLinear(rgb.b);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Compute WCAG contrast ratios for all pairs in a color palette.
|
|
244
|
+
* @param {Array<{color: string, normalized: string, confidence: string}>} palette
|
|
245
|
+
* @returns {Array<{fg: string, bg: string, ratio: number, aa: boolean, aaLarge: boolean, aaa: boolean}>}
|
|
246
|
+
*/
|
|
247
|
+
export function computeWcag(palette) {
|
|
248
|
+
const colors = palette
|
|
249
|
+
.filter(c => c.normalized && c.normalized.startsWith('#'))
|
|
250
|
+
.slice(0, 10);
|
|
251
|
+
|
|
252
|
+
const pairs = [];
|
|
253
|
+
for (let i = 0; i < colors.length; i++) {
|
|
254
|
+
for (let j = i + 1; j < colors.length; j++) {
|
|
255
|
+
const l1 = relativeLuminance(colors[i].normalized);
|
|
256
|
+
const l2 = relativeLuminance(colors[j].normalized);
|
|
257
|
+
if (l1 === null || l2 === null) continue;
|
|
258
|
+
const lighter = Math.max(l1, l2);
|
|
259
|
+
const darker = Math.min(l1, l2);
|
|
260
|
+
const ratio = (lighter + 0.05) / (darker + 0.05);
|
|
261
|
+
const fg = l1 >= l2 ? colors[i].normalized : colors[j].normalized;
|
|
262
|
+
const bg = l1 >= l2 ? colors[j].normalized : colors[i].normalized;
|
|
263
|
+
pairs.push({
|
|
264
|
+
fg,
|
|
265
|
+
bg,
|
|
266
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
267
|
+
aa: ratio >= 4.5,
|
|
268
|
+
aaLarge: ratio >= 3,
|
|
269
|
+
aaa: ratio >= 7,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return pairs.sort((a, b) => b.ratio - a.ratio);
|
|
275
|
+
}
|
|
276
|
+
|
|
231
277
|
/**
|
|
232
278
|
* Convert any supported color format to all formats
|
|
233
279
|
* @param {string} colorString - Color in hex, rgb(), or rgba() format
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
export async function extractBreakpoints(page) {
|
|
2
|
+
return await page.evaluate(() => {
|
|
3
|
+
const breakpoints = new Set();
|
|
4
|
+
|
|
5
|
+
for (const sheet of document.styleSheets) {
|
|
6
|
+
try {
|
|
7
|
+
for (const rule of sheet.cssRules || []) {
|
|
8
|
+
if (rule.media) {
|
|
9
|
+
const match = rule.media.mediaText.match(/(\d+)px/g);
|
|
10
|
+
if (match) match.forEach((m) => breakpoints.add(parseInt(m)));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
} catch (e) {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Array.from(breakpoints)
|
|
17
|
+
.sort((a, b) => a - b)
|
|
18
|
+
.map((px) => ({ px: px + "px" }));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function detectIconSystem(page) {
|
|
23
|
+
return await page.evaluate(() => {
|
|
24
|
+
const systems = [];
|
|
25
|
+
|
|
26
|
+
if (document.querySelector('[class*="fa-"]')) {
|
|
27
|
+
systems.push({ name: "Font Awesome", type: "icon-font" });
|
|
28
|
+
}
|
|
29
|
+
if (document.querySelector('[class*="material-icons"]')) {
|
|
30
|
+
systems.push({ name: "Material Icons", type: "icon-font" });
|
|
31
|
+
}
|
|
32
|
+
if (document.querySelector('svg[class*="heroicon"]') || document.querySelector('svg[data-slot="icon"]')) {
|
|
33
|
+
systems.push({ name: "Heroicons", type: "svg" });
|
|
34
|
+
}
|
|
35
|
+
if (document.querySelector('svg[class*="hugeicons"]') || document.querySelector('[class*="hugeicons-"]')) {
|
|
36
|
+
systems.push({ name: "Hugeicons", type: "svg" });
|
|
37
|
+
}
|
|
38
|
+
if (document.querySelector('ion-icon') || document.querySelector('[class*="ionicons"]')) {
|
|
39
|
+
systems.push({ name: "Ionicons", type: "svg" });
|
|
40
|
+
}
|
|
41
|
+
if (document.querySelector('svg[data-feather]') || document.querySelector('i[data-feather]') || document.querySelector('[class*="feather-"]')) {
|
|
42
|
+
systems.push({ name: "Feather Icons", type: "svg" });
|
|
43
|
+
}
|
|
44
|
+
if (document.querySelector('svg[class*="icon"]')) {
|
|
45
|
+
systems.push({ name: "SVG Icons", type: "svg" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return systems;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function detectFrameworks(page) {
|
|
53
|
+
return await page.evaluate(() => {
|
|
54
|
+
const frameworks = [];
|
|
55
|
+
const html = document.documentElement.outerHTML;
|
|
56
|
+
const body = document.body;
|
|
57
|
+
|
|
58
|
+
function countMatches(selector) {
|
|
59
|
+
try {
|
|
60
|
+
return document.querySelectorAll(selector).length;
|
|
61
|
+
} catch {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasResource(pattern) {
|
|
67
|
+
const links = Array.from(document.querySelectorAll('link[href], script[src]'));
|
|
68
|
+
return links.some(el => pattern.test(el.href || el.src));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tailwind CSS
|
|
72
|
+
const tailwindEvidence = [];
|
|
73
|
+
if (/\b\w+-\[[^\]]+\]/.test(html)) tailwindEvidence.push('arbitrary values (e.g., top-[117px])');
|
|
74
|
+
if (/(sm|md|lg|xl|2xl|dark|hover|focus|group-hover|peer-):[a-z]/.test(html)) tailwindEvidence.push('responsive/state modifiers');
|
|
75
|
+
if (hasResource(/tailwindcss|tailwind\.css|cdn\.tailwindcss/)) tailwindEvidence.push('stylesheet');
|
|
76
|
+
if (tailwindEvidence.length >= 2) {
|
|
77
|
+
frameworks.push({ name: 'Tailwind CSS', confidence: 'high', evidence: tailwindEvidence.join(', ') });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Bootstrap
|
|
81
|
+
const bootstrapEvidence = [];
|
|
82
|
+
const hasContainer = countMatches('.container, .container-fluid') > 0;
|
|
83
|
+
const hasRow = countMatches('.row') > 0;
|
|
84
|
+
const hasCol = countMatches('[class*="col-"]') > 0;
|
|
85
|
+
if (hasContainer && hasRow && hasCol) bootstrapEvidence.push('grid system (container + row + col)');
|
|
86
|
+
if (/\bbtn-primary\b|\bbtn-secondary\b|\bbtn-success\b/.test(html)) bootstrapEvidence.push('button variants');
|
|
87
|
+
if (hasResource(/bootstrap\.min\.css|bootstrap\.css|getbootstrap\.com/)) bootstrapEvidence.push('stylesheet');
|
|
88
|
+
if (bootstrapEvidence.length >= 2) {
|
|
89
|
+
frameworks.push({ name: 'Bootstrap', confidence: 'high', evidence: bootstrapEvidence.join(', ') });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Material UI (MUI)
|
|
93
|
+
const muiCount = countMatches('[class*="MuiBox-"], [class*="MuiButton-"], [class*="Mui"]');
|
|
94
|
+
if (muiCount > 3) frameworks.push({ name: 'Material UI (MUI)', confidence: 'high', evidence: `${muiCount} MUI components` });
|
|
95
|
+
|
|
96
|
+
// Chakra UI
|
|
97
|
+
const chakraCount = countMatches('[class*="chakra-"]');
|
|
98
|
+
if (chakraCount > 3) frameworks.push({ name: 'Chakra UI', confidence: 'high', evidence: `${chakraCount} Chakra components` });
|
|
99
|
+
|
|
100
|
+
// Ant Design
|
|
101
|
+
const antCount = countMatches('[class^="ant-"], [class*=" ant-"]');
|
|
102
|
+
if (antCount > 3) frameworks.push({ name: 'Ant Design', confidence: 'high', evidence: `${antCount} Ant components` });
|
|
103
|
+
|
|
104
|
+
// Vuetify
|
|
105
|
+
const vuetifySpecific = countMatches('[class*="v-btn"], [class*="v-card"], [class*="v-app"], [class*="v-toolbar"], [class*="v-navigation"], [class*="v-list"], [class*="v-sheet"]');
|
|
106
|
+
const hasVuetifyTheme = body.classList.contains('theme--light') || body.classList.contains('theme--dark');
|
|
107
|
+
const hasVuetifyApp = countMatches('[class*="v-application"]') > 0;
|
|
108
|
+
if ((vuetifySpecific > 8 && hasVuetifyApp) || (hasVuetifyTheme && vuetifySpecific > 5)) {
|
|
109
|
+
frameworks.push({ name: 'Vuetify', confidence: 'high', evidence: `${vuetifySpecific} Vuetify components` });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Shopify Polaris
|
|
113
|
+
const polarisCount = countMatches('[class*="Polaris-"]');
|
|
114
|
+
if (polarisCount > 2) frameworks.push({ name: 'Shopify Polaris', confidence: 'high', evidence: `${polarisCount} Polaris components` });
|
|
115
|
+
|
|
116
|
+
// Radix UI
|
|
117
|
+
const radixCount = document.querySelectorAll('[data-radix-], [data-state]').length;
|
|
118
|
+
if (radixCount > 5) frameworks.push({ name: 'Radix UI', confidence: 'high', evidence: `${radixCount} Radix primitives` });
|
|
119
|
+
|
|
120
|
+
// DaisyUI
|
|
121
|
+
if (tailwindEvidence.length >= 2) {
|
|
122
|
+
const daisySpecific = countMatches('.btn-primary.btn, .badge, .drawer, .swap, .mockup-code');
|
|
123
|
+
const hasDaisyTheme = body.hasAttribute('data-theme');
|
|
124
|
+
if (daisySpecific > 3 || hasDaisyTheme) {
|
|
125
|
+
frameworks.push({ name: 'DaisyUI', confidence: 'high', evidence: `Tailwind + ${daisySpecific} DaisyUI components` });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Foundation
|
|
130
|
+
const foundationEvidence = [];
|
|
131
|
+
if (countMatches('.grid-x, .grid-y, .cell') > 0 || countMatches('.button.primary, .button.secondary') > 0) foundationEvidence.push('grid/button system');
|
|
132
|
+
if (hasResource(/foundation\.min\.css|foundation\.css|zurb\.com\/foundation/)) foundationEvidence.push('stylesheet');
|
|
133
|
+
if (foundationEvidence.length >= 1 || countMatches('[data-foundation]') > 0) {
|
|
134
|
+
frameworks.push({ name: 'Foundation', confidence: 'high', evidence: foundationEvidence.join(', ') || 'data attributes' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Bulma
|
|
138
|
+
const bulmaEvidence = [];
|
|
139
|
+
if (countMatches('.columns, .column') > 0 && countMatches('.column') > 2) bulmaEvidence.push('columns system');
|
|
140
|
+
if (/\bbutton is-primary\b|\bbutton is-link\b/.test(html)) bulmaEvidence.push('button modifiers');
|
|
141
|
+
if (hasResource(/bulma\.min\.css|bulma\.css/)) bulmaEvidence.push('stylesheet');
|
|
142
|
+
if (bulmaEvidence.length >= 2) frameworks.push({ name: 'Bulma', confidence: 'high', evidence: bulmaEvidence.join(', ') });
|
|
143
|
+
|
|
144
|
+
// Semantic UI
|
|
145
|
+
const semanticCount = countMatches('.ui.button, .ui.menu, .ui.card, .ui.grid');
|
|
146
|
+
if (semanticCount > 3 || hasResource(/semantic\.min\.css|semantic-ui/)) {
|
|
147
|
+
frameworks.push({ name: 'Semantic UI', confidence: 'high', evidence: `${semanticCount} .ui components` });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// UIkit
|
|
151
|
+
const uikitCount = countMatches('[class*="uk-"], [uk-grid], [uk-navbar]');
|
|
152
|
+
if (uikitCount > 3 || hasResource(/uikit\.min\.css|getuikit\.com/)) {
|
|
153
|
+
frameworks.push({ name: 'UIkit', confidence: 'high', evidence: `${uikitCount} uk- components` });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// shadcn/ui
|
|
157
|
+
const shadcnClasses = /\bcn\(|\bslot-\w+|\bdata-\[state=/.test(html);
|
|
158
|
+
const hasShadcnComponents = countMatches('[data-slot], [data-state]') > 5;
|
|
159
|
+
if (tailwindEvidence.length >= 2 && radixCount > 3 && (shadcnClasses || hasShadcnComponents)) {
|
|
160
|
+
frameworks.push({ name: 'shadcn/ui', confidence: 'medium', evidence: 'Tailwind + Radix + component patterns' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Headless UI
|
|
164
|
+
const headlessCount = document.querySelectorAll('[aria-controls][aria-expanded], [role="dialog"][data-headlessui]').length;
|
|
165
|
+
if (tailwindEvidence.length >= 2 && headlessCount > 2) {
|
|
166
|
+
frameworks.push({ name: 'Headless UI', confidence: 'high', evidence: `${headlessCount} headless components with Tailwind` });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// PrimeReact/Vue/NG
|
|
170
|
+
const primeCount = countMatches('[class*="p-"], .p-component, .p-button, .p-datatable');
|
|
171
|
+
if (primeCount > 5) frameworks.push({ name: 'PrimeReact/Vue/NG', confidence: 'high', evidence: `${primeCount} Prime components` });
|
|
172
|
+
|
|
173
|
+
// Mantine
|
|
174
|
+
const mantineCount = countMatches('[class*="mantine-"], [data-mantine]');
|
|
175
|
+
if (mantineCount > 3) frameworks.push({ name: 'Mantine', confidence: 'high', evidence: `${mantineCount} Mantine components` });
|
|
176
|
+
|
|
177
|
+
// Carbon Design System
|
|
178
|
+
const carbonCount = countMatches('[class*="cds--"], [class*="bx--"]');
|
|
179
|
+
if (carbonCount > 3) frameworks.push({ name: 'Carbon Design System', confidence: 'high', evidence: `${carbonCount} Carbon components` });
|
|
180
|
+
|
|
181
|
+
// Fluent UI
|
|
182
|
+
const fluentCount = countMatches('[class*="ms-"], .ms-Button, .ms-TextField');
|
|
183
|
+
if (fluentCount > 5) frameworks.push({ name: 'Fluent UI', confidence: 'high', evidence: `${fluentCount} Fluent components` });
|
|
184
|
+
|
|
185
|
+
// Quasar
|
|
186
|
+
const quasarCount = countMatches('[class*="q-"]');
|
|
187
|
+
if (quasarCount > 5 || body.classList.contains('q-app')) {
|
|
188
|
+
frameworks.push({ name: 'Quasar', confidence: 'high', evidence: `${quasarCount} q- components` });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Element Plus/UI
|
|
192
|
+
const elementCount = countMatches('[class*="el-"]');
|
|
193
|
+
if (elementCount > 5) frameworks.push({ name: 'Element Plus/UI', confidence: 'high', evidence: `${elementCount} el- components` });
|
|
194
|
+
|
|
195
|
+
return frameworks;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function extractGradients(page) {
|
|
200
|
+
return await page.evaluate(() => {
|
|
201
|
+
const seen = new Map();
|
|
202
|
+
|
|
203
|
+
const els = document.querySelectorAll('*');
|
|
204
|
+
let checked = 0;
|
|
205
|
+
for (const el of els) {
|
|
206
|
+
if (checked++ > 2000) break;
|
|
207
|
+
if (el.style.backgroundImage === 'none') continue;
|
|
208
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0) continue;
|
|
209
|
+
const s = getComputedStyle(el);
|
|
210
|
+
const bg = s.backgroundImage;
|
|
211
|
+
if (!bg || bg === 'none') continue;
|
|
212
|
+
|
|
213
|
+
const gradients = [];
|
|
214
|
+
let depth = 0, start = 0;
|
|
215
|
+
for (let i = 0; i < bg.length; i++) {
|
|
216
|
+
if (bg[i] === '(') depth++;
|
|
217
|
+
else if (bg[i] === ')') depth--;
|
|
218
|
+
else if (bg[i] === ',' && depth === 0) {
|
|
219
|
+
gradients.push(bg.slice(start, i).trim());
|
|
220
|
+
start = i + 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
gradients.push(bg.slice(start).trim());
|
|
224
|
+
|
|
225
|
+
for (const grad of gradients) {
|
|
226
|
+
if (!/^(repeating-)?(linear|radial|conic)-gradient/.test(grad)) continue;
|
|
227
|
+
|
|
228
|
+
const base = grad.replace(/^repeating-/, '');
|
|
229
|
+
const repeating = grad.startsWith('repeating-');
|
|
230
|
+
const type = (base.startsWith('linear') ? 'linear' : base.startsWith('radial') ? 'radial' : 'conic') + (repeating ? '-repeating' : '');
|
|
231
|
+
|
|
232
|
+
const key = grad.replace(/\s+/g, ' ');
|
|
233
|
+
if (seen.has(key)) {
|
|
234
|
+
seen.get(key).count++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const stopColors = [];
|
|
239
|
+
const stopRe = /#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\)/gi;
|
|
240
|
+
let m;
|
|
241
|
+
while ((m = stopRe.exec(grad)) !== null) stopColors.push(m[0]);
|
|
242
|
+
|
|
243
|
+
seen.set(key, { gradient: key, type, stopColors, count: 1 });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return Array.from(seen.values())
|
|
248
|
+
.sort((a, b) => b.count - a.count)
|
|
249
|
+
.slice(0, 20);
|
|
250
|
+
});
|
|
251
|
+
}
|