dembrandt 0.7.0 → 0.7.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 +8 -3
- package/index.js +9 -1
- package/lib/extractors.js +70 -7
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
Extract any 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
|
**CLI output**
|
|
12
12
|
|
|
13
|
-

|
|
14
|
+
|
|
15
|
+
**Brand Guide PDF**
|
|
16
|
+
|
|
17
|
+

|
|
14
18
|
|
|
15
19
|
**Local UI**
|
|
16
20
|
|
|
17
|
-

|
|
18
22
|
|
|
19
23
|
## Install
|
|
20
24
|
|
|
@@ -49,6 +53,7 @@ dembrandt bmw.de --dtcg # Export in W3C Design Tokens (DTCG) format (
|
|
|
49
53
|
dembrandt bmw.de --dark-mode # Extract colors from dark mode variant
|
|
50
54
|
dembrandt bmw.de --mobile # Use mobile viewport (390x844, iPhone 12/13/14/15) for responsive analysis
|
|
51
55
|
dembrandt bmw.de --slow # 3x longer timeouts (24s hydration) for JavaScript-heavy sites
|
|
56
|
+
dembrandt bmw.de --brand-guide # Generate a brand guide PDF
|
|
52
57
|
dembrandt bmw.de --no-sandbox # Disable Chromium sandbox (required for Docker/CI)
|
|
53
58
|
dembrandt bmw.de --browser=firefox # Use Firefox instead of Chromium (better for Cloudflare bypass)
|
|
54
59
|
```
|
package/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { join } from "path";
|
|
|
21
21
|
program
|
|
22
22
|
.name("dembrandt")
|
|
23
23
|
.description("Extract design tokens from any website")
|
|
24
|
-
.version("0.7.
|
|
24
|
+
.version("0.7.2")
|
|
25
25
|
.argument("<url>")
|
|
26
26
|
.option("--browser <type>", "Browser to use (chromium|firefox)", "chromium")
|
|
27
27
|
.option("--json-only", "Output raw JSON")
|
|
@@ -32,6 +32,8 @@ program
|
|
|
32
32
|
.option("--slow", "3x longer timeouts for slow-loading sites")
|
|
33
33
|
.option("--brand-guide", "Export a brand guide PDF")
|
|
34
34
|
.option("--no-sandbox", "Disable browser sandbox (needed for Docker/CI)")
|
|
35
|
+
.option("--raw-colors", "Include pre-filter raw colors in JSON output")
|
|
36
|
+
.option("--screenshot <path>", "Save a screenshot of the page")
|
|
35
37
|
.action(async (input, opts) => {
|
|
36
38
|
let url = input;
|
|
37
39
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
@@ -70,6 +72,7 @@ program
|
|
|
70
72
|
darkMode: opts.darkMode,
|
|
71
73
|
mobile: opts.mobile,
|
|
72
74
|
slow: opts.slow,
|
|
75
|
+
screenshotPath: opts.screenshot,
|
|
73
76
|
});
|
|
74
77
|
break;
|
|
75
78
|
} catch (err) {
|
|
@@ -97,6 +100,11 @@ program
|
|
|
97
100
|
|
|
98
101
|
console.log();
|
|
99
102
|
|
|
103
|
+
// Strip raw colors unless --raw-colors flag is set
|
|
104
|
+
if (!opts.rawColors && result.colors && result.colors.rawColors) {
|
|
105
|
+
delete result.colors.rawColors;
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
// Convert to W3C format if requested
|
|
101
109
|
const outputData = opts.dtcg ? toW3CFormat(result) : result;
|
|
102
110
|
|
package/lib/extractors.js
CHANGED
|
@@ -448,7 +448,7 @@ export async function extractBranding(
|
|
|
448
448
|
await page.mouse.move(0, 0).catch(() => { });
|
|
449
449
|
|
|
450
450
|
// Merge hover/focus colors into palette
|
|
451
|
-
hoverFocusColors.forEach(({ color }) => {
|
|
451
|
+
hoverFocusColors.forEach(({ color, property }) => {
|
|
452
452
|
const isDuplicate = colors.palette.some((c) => c.color === color);
|
|
453
453
|
if (!isDuplicate && color) {
|
|
454
454
|
// Normalize and add to palette
|
|
@@ -461,6 +461,18 @@ export async function extractBranding(
|
|
|
461
461
|
normalized = `#${r}${g}${b}`;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
// Skip chromatic colors that only appear in border/outline on hover — likely focus rings, not brand colors
|
|
465
|
+
if (property !== 'background-color') {
|
|
466
|
+
const hex = normalized.replace('#', '');
|
|
467
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
468
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
469
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
470
|
+
const max = Math.max(r, g, b);
|
|
471
|
+
const min = Math.min(r, g, b);
|
|
472
|
+
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
473
|
+
if (saturation > 0.3) return;
|
|
474
|
+
}
|
|
475
|
+
|
|
464
476
|
colors.palette.push({
|
|
465
477
|
color,
|
|
466
478
|
normalized,
|
|
@@ -619,6 +631,16 @@ export async function extractBranding(
|
|
|
619
631
|
result.isCanvasOnly = true;
|
|
620
632
|
}
|
|
621
633
|
|
|
634
|
+
// Take screenshot if requested
|
|
635
|
+
if (options.screenshotPath) {
|
|
636
|
+
await page.screenshot({ path: options.screenshotPath, fullPage: false });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Include raw colors before filtering if requested
|
|
640
|
+
if (options.includeRawColors) {
|
|
641
|
+
result.colors.rawColors = colors._raw || [];
|
|
642
|
+
}
|
|
643
|
+
|
|
622
644
|
return result;
|
|
623
645
|
} catch (error) {
|
|
624
646
|
spinner.fail("Extraction failed");
|
|
@@ -975,6 +997,21 @@ async function extractColors(page) {
|
|
|
975
997
|
continue;
|
|
976
998
|
}
|
|
977
999
|
|
|
1000
|
+
// Skip UI framework default theme variables
|
|
1001
|
+
if (
|
|
1002
|
+
prop.startsWith("--el-") || // Element Plus/UI
|
|
1003
|
+
prop.startsWith("--p-") || // PrimeVue/PrimeReact
|
|
1004
|
+
prop.startsWith("--chakra-") || // Chakra UI
|
|
1005
|
+
prop.startsWith("--mantine-") || // Mantine
|
|
1006
|
+
prop.startsWith("--ant-") || // Ant Design
|
|
1007
|
+
prop.startsWith("--bs-") || // Bootstrap
|
|
1008
|
+
prop.startsWith("--swiper-") || // Swiper
|
|
1009
|
+
prop.startsWith("--rsbs-") || // React Spring Bottom Sheet
|
|
1010
|
+
prop.startsWith("--toastify-") // React Toastify
|
|
1011
|
+
) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
978
1015
|
// Skip obvious system/default variables
|
|
979
1016
|
if (prop.includes("--system-") || prop.includes("--default-")) {
|
|
980
1017
|
continue;
|
|
@@ -1066,6 +1103,10 @@ async function extractColors(page) {
|
|
|
1066
1103
|
) {
|
|
1067
1104
|
return;
|
|
1068
1105
|
}
|
|
1106
|
+
const rect = el.getBoundingClientRect();
|
|
1107
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1069
1110
|
|
|
1070
1111
|
const bgColor = computed.backgroundColor;
|
|
1071
1112
|
const textColor = computed.color;
|
|
@@ -1116,12 +1157,11 @@ async function extractColors(page) {
|
|
|
1116
1157
|
);
|
|
1117
1158
|
}
|
|
1118
1159
|
|
|
1119
|
-
// Collect
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
];
|
|
1160
|
+
// Collect colors by property type
|
|
1161
|
+
const bgColors = extractColorsFromValue(bgColor);
|
|
1162
|
+
const textColors = extractColorsFromValue(textColor);
|
|
1163
|
+
const borderColors = extractColorsFromValue(borderColor);
|
|
1164
|
+
const allColors = [...bgColors, ...textColors, ...borderColors];
|
|
1125
1165
|
|
|
1126
1166
|
allColors.forEach((color) => {
|
|
1127
1167
|
if (color && color !== "rgba(0, 0, 0, 0)" && color !== "transparent") {
|
|
@@ -1129,10 +1169,12 @@ async function extractColors(page) {
|
|
|
1129
1169
|
const existing = colorMap.get(normalized) || {
|
|
1130
1170
|
original: color, // Keep first seen format
|
|
1131
1171
|
count: 0,
|
|
1172
|
+
bgCount: 0,
|
|
1132
1173
|
score: 0,
|
|
1133
1174
|
sources: new Set(),
|
|
1134
1175
|
};
|
|
1135
1176
|
existing.count++;
|
|
1177
|
+
if (bgColors.includes(color)) existing.bgCount++;
|
|
1136
1178
|
existing.score += score;
|
|
1137
1179
|
if (score > 1) {
|
|
1138
1180
|
const source = context.split(" ")[0].substring(0, 30); // Limit source length
|
|
@@ -1179,6 +1221,22 @@ async function extractColors(page) {
|
|
|
1179
1221
|
return true;
|
|
1180
1222
|
}
|
|
1181
1223
|
|
|
1224
|
+
// Chromatic colors (saturated) that never appear as backgrounds are likely browser defaults
|
|
1225
|
+
// or framework internals, not brand colors. Real brand colors appear on backgrounds somewhere.
|
|
1226
|
+
if (data.bgCount === 0) {
|
|
1227
|
+
const hex = normalized.replace('#', '');
|
|
1228
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1229
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1230
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1231
|
+
const max = Math.max(r, g, b);
|
|
1232
|
+
const min = Math.min(r, g, b);
|
|
1233
|
+
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
1234
|
+
// If saturation > 0.3, this is a chromatic color with no background usage
|
|
1235
|
+
if (saturation > 0.3) {
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1182
1240
|
return false;
|
|
1183
1241
|
}
|
|
1184
1242
|
|
|
@@ -1260,6 +1318,10 @@ async function extractColors(page) {
|
|
|
1260
1318
|
return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB);
|
|
1261
1319
|
}
|
|
1262
1320
|
|
|
1321
|
+
const rawColors = Array.from(colorMap.entries())
|
|
1322
|
+
.filter(([, data]) => data.count >= threshold)
|
|
1323
|
+
.map(([normalized, data]) => ({ color: data.original, normalized, count: data.count }));
|
|
1324
|
+
|
|
1263
1325
|
const palette = Array.from(colorMap.entries())
|
|
1264
1326
|
.filter(([normalizedColor, data]) => {
|
|
1265
1327
|
// Filter out colors below threshold
|
|
@@ -1349,6 +1411,7 @@ async function extractColors(page) {
|
|
|
1349
1411
|
semantic: semanticColors,
|
|
1350
1412
|
palette: perceptuallyDeduped,
|
|
1351
1413
|
cssVariables: filteredCssVariables,
|
|
1414
|
+
_raw: rawColors,
|
|
1352
1415
|
};
|
|
1353
1416
|
});
|
|
1354
1417
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dembrandt",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Extract design tokens and brand assets from any website",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"brand-challenge": "node run-no-login-challenge.mjs",
|
|
17
17
|
"brand-challenge:report": "node run-no-login-challenge.mjs || true",
|
|
18
18
|
"install-browser": "npx playwright install chromium firefox || echo 'Playwright browser installation failed. You may need to install system dependencies manually.'",
|
|
19
|
-
"local-ui": "cd local-ui && npm start"
|
|
19
|
+
"local-ui": "cd local-ui && npm start",
|
|
20
|
+
"qa:baseline": "node test/qa.mjs --baseline",
|
|
21
|
+
"qa:diff": "node test/qa.mjs --diff",
|
|
22
|
+
"qa:site": "node test/qa.mjs --site"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [
|
|
22
25
|
"design-tokens",
|