dembrandt 0.12.0 → 0.12.2
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 +69 -0
- package/index.js +14 -8
- 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 +368 -0
- package/lib/extractors/index.js +497 -0
- package/lib/extractors/logo.js +402 -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 -2766
- /package/lib/{w3c-exporter.js → formatters/w3c.js} +0 -0
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ dembrandt example.com --sitemap # Discover pages from sitemap.xml instead
|
|
|
71
71
|
dembrandt example.com --pages 10 --sitemap # Combine: up to 10 pages discovered via sitemap
|
|
72
72
|
dembrandt example.com --no-sandbox # Disable Chromium sandbox (required for Docker/CI)
|
|
73
73
|
dembrandt example.com --browser=firefox # Use Firefox instead of Chromium (better for Cloudflare bypass)
|
|
74
|
+
dembrandt example.com --wcag # WCAG 2.1 contrast analysis — real DOM pairs, AA/AAA grades
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
Default: formatted terminal display only. Use `--save-output` to persist results as JSON files. Browser automatically retries in visible mode if headless extraction fails.
|
|
@@ -140,6 +141,16 @@ dembrandt example.com --design-md
|
|
|
140
141
|
# Saves to: output/example.com/DESIGN.md
|
|
141
142
|
```
|
|
142
143
|
|
|
144
|
+
### WCAG Contrast Analysis
|
|
145
|
+
|
|
146
|
+
Use `--wcag` to check accessibility contrast ratios across the page. Unlike palette-based checkers, dembrandt walks the actual DOM and finds what color is rendered on top of what background — per element.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
dembrandt stripe.com --wcag
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Returns every text/background pair with contrast ratio and WCAG 2.1 grade (AA, AA-Large, AAA, or fail), sorted by how often each pair appears. Results are shown in terminal and included in JSON output as `wcag`.
|
|
153
|
+
|
|
143
154
|
### Brand Guide PDF
|
|
144
155
|
|
|
145
156
|
Use `--brand-guide` to generate a printable PDF summarizing the extracted design system: colors, typography, components, and logo on a single document.
|
|
@@ -180,6 +191,64 @@ Opens http://localhost:5173 with API on port 3002.
|
|
|
180
191
|
|
|
181
192
|
Extractions are performed via CLI (`dembrandt <url> --save-output`) and automatically appear in the UI.
|
|
182
193
|
|
|
194
|
+
## Recipes
|
|
195
|
+
|
|
196
|
+
**Quick brand scan**
|
|
197
|
+
```bash
|
|
198
|
+
dembrandt stripe.com
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Compare two sites**
|
|
202
|
+
```bash
|
|
203
|
+
dembrandt stripe.com --save-output
|
|
204
|
+
dembrandt braintree.com --save-output
|
|
205
|
+
# Compare output/stripe.com and output/braintree.com side by side
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Multi-page audit** — get a fuller picture across the whole site
|
|
209
|
+
```bash
|
|
210
|
+
dembrandt stripe.com --pages 10 --sitemap --save-output
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Spot-check a value** — verify a specific token fast
|
|
214
|
+
```bash
|
|
215
|
+
dembrandt stripe.com --json-only | grep -i "border-radius"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Export for Tailwind** — get spacing and color values into your config
|
|
219
|
+
```bash
|
|
220
|
+
dembrandt stripe.com --dtcg --save-output
|
|
221
|
+
# Use the .tokens.json with Style Dictionary to generate tailwind.config.js
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Export for Tokens Studio / Figma**
|
|
225
|
+
```bash
|
|
226
|
+
dembrandt stripe.com --dtcg --save-output
|
|
227
|
+
# Import the .tokens.json directly into Tokens Studio
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Generate DESIGN.md for your AI agent**
|
|
231
|
+
```bash
|
|
232
|
+
dembrandt stripe.com --design-md
|
|
233
|
+
# Point your agent at the output DESIGN.md
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Accessibility audit** — check contrast on any live URL
|
|
237
|
+
```bash
|
|
238
|
+
dembrandt stripe.com --wcag
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Regression baseline** — snapshot now, catch drift later
|
|
242
|
+
```bash
|
|
243
|
+
dembrandt myapp.com --save-output --dtcg
|
|
244
|
+
# Store output as baseline, re-run after deploys and diff
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**CI / headless environments**
|
|
248
|
+
```bash
|
|
249
|
+
dembrandt myapp.com --no-sandbox --save-output
|
|
250
|
+
```
|
|
251
|
+
|
|
183
252
|
## Use Cases
|
|
184
253
|
|
|
185
254
|
- Design system documentation
|
package/index.js
CHANGED
|
@@ -11,21 +11,25 @@ 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
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")
|
|
@@ -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}`);
|
|
@@ -116,6 +121,7 @@ program
|
|
|
116
121
|
slow: opts.slow,
|
|
117
122
|
screenshotPath: opts.screenshot,
|
|
118
123
|
discoverLinks: isMultiPage && !opts.sitemap ? maxPages : null,
|
|
124
|
+
wcag: opts.wcag,
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
// Multi-page crawl
|
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
|
+
}
|