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 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
- ![Dembrandt — Any website to design tokens](docs/screenshots/banner.png)
9
+ ![Dembrandt — Any website to design tokens](https://raw.githubusercontent.com/dembrandt/dembrandt/main/docs/images/banner.png)
10
10
 
11
11
  **CLI output**
12
12
 
13
- ![CLI extraction of netflix.com](docs/screenshots/cli-output.png)
13
+ ![CLI extraction of netflix.com](https://raw.githubusercontent.com/dembrandt/dembrandt/main/docs/images/cli-output.png)
14
+
15
+ **Brand Guide PDF**
16
+
17
+ ![Brand guide PDF extracted from any URL](https://raw.githubusercontent.com/dembrandt/dembrandt/main/docs/images/brand-guide.png)
14
18
 
15
19
  **Local UI**
16
20
 
17
- ![Local UI showing extracted brand](docs/screenshots/local-ui.png)
21
+ ![Local UI showing extracted brand](https://raw.githubusercontent.com/dembrandt/dembrandt/main/docs/images/local-ui.png)
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.0")
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 all colors from this element
1120
- const allColors = [
1121
- ...extractColorsFromValue(bgColor),
1122
- ...extractColorsFromValue(textColor),
1123
- ...extractColorsFromValue(borderColor)
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.0",
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",