dembrandt 0.11.0 → 0.12.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.
- package/README.md +10 -12
- package/index.js +14 -6
- package/lib/extractors.js +15 -9
- package/package.json +1 -1
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,7 +176,7 @@ 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
|
|
|
@@ -204,9 +204,9 @@ Uses Playwright to render the page, reads computed styles from the DOM, analyzes
|
|
|
204
204
|
|
|
205
205
|
### Color Confidence
|
|
206
206
|
|
|
207
|
-
- High
|
|
208
|
-
- Medium
|
|
209
|
-
- Low
|
|
207
|
+
- High: Logo, primary interactive elements
|
|
208
|
+
- Medium: Secondary interactive elements, icons, navigation
|
|
209
|
+
- Low: Generic UI components (filtered from display)
|
|
210
210
|
- Only shows high and medium confidence colors in terminal. Full palette in JSON.
|
|
211
211
|
|
|
212
212
|
## Limitations
|
|
@@ -228,12 +228,10 @@ Dembrandt does not host, redistribute, or claim rights to any third-party brand
|
|
|
228
228
|
|
|
229
229
|
## Contributing
|
|
230
230
|
|
|
231
|
-
Bugs, weird sites, pull requests
|
|
231
|
+
Bugs, weird sites, pull requests. All welcome.
|
|
232
232
|
|
|
233
233
|
Open an [Issue](https://github.com/dembrandt/dembrandt/issues) or PR.
|
|
234
234
|
|
|
235
235
|
@thevangelist
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
MIT — do whatever you want with it.
|
|
237
|
+
MIT. Do whatever you want with it.
|
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ program
|
|
|
27
27
|
.description("Extract design tokens from any website")
|
|
28
28
|
.version("0.11.0")
|
|
29
29
|
.argument("<url>")
|
|
30
|
-
.option("--browser <type>", "Browser to use (chromium|firefox)", "chromium")
|
|
30
|
+
.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
31
|
.option("--json-only", "Output raw JSON")
|
|
32
32
|
.option("--save-output", "Save JSON file to output folder")
|
|
33
33
|
.option("--dtcg", "Export in W3C Design Tokens (DTCG) format")
|
|
@@ -93,10 +93,18 @@ program
|
|
|
93
93
|
if (opts.noSandbox && opts.browser === 'chromium') {
|
|
94
94
|
launchArgs.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
if (process.env.BROWSER_CDP_ENDPOINT) {
|
|
97
|
+
if (opts.browser !== 'chromium') {
|
|
98
|
+
throw new Error("BROWSER_CDP_ENDPOINT is only supported with --browser chromium.");
|
|
99
|
+
}
|
|
100
|
+
spinner.text = "Connecting over CDP...";
|
|
101
|
+
browser = await browserType.connectOverCDP(process.env.BROWSER_CDP_ENDPOINT);
|
|
102
|
+
} else {
|
|
103
|
+
browser = await browserType.launch({
|
|
104
|
+
headless: !useHeaded,
|
|
105
|
+
args: launchArgs,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
try {
|
|
102
110
|
const isMultiPage = opts.pages || opts.sitemap;
|
|
@@ -170,7 +178,7 @@ program
|
|
|
170
178
|
await browser.close();
|
|
171
179
|
browser = null;
|
|
172
180
|
|
|
173
|
-
if (useHeaded) throw err;
|
|
181
|
+
if (useHeaded || process.env.BROWSER_CDP_ENDPOINT) throw err;
|
|
174
182
|
|
|
175
183
|
if (
|
|
176
184
|
err.message.includes("Timeout") ||
|
package/lib/extractors.js
CHANGED
|
@@ -464,7 +464,7 @@ export async function extractBranding(
|
|
|
464
464
|
// Merge hover/focus colors into palette
|
|
465
465
|
hoverFocusColors.forEach(({ color, property }) => {
|
|
466
466
|
const isDuplicate = colors.palette.some((c) => c.color === color);
|
|
467
|
-
if (!isDuplicate && color) {
|
|
467
|
+
if (!isDuplicate && color && /^(rgba?\(|hsla?\(|#)/i.test(color.trim())) {
|
|
468
468
|
// Normalize and add to palette
|
|
469
469
|
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
470
470
|
let normalized = color.toLowerCase();
|
|
@@ -1000,6 +1000,8 @@ async function extractColors(page) {
|
|
|
1000
1000
|
// Only accept if it contains rgb/hsl/# inside
|
|
1001
1001
|
return /#[0-9a-f]{3,6}|rgba?\(|hsla?\(/i.test(value);
|
|
1002
1002
|
}
|
|
1003
|
+
// Reject modern color functions not parseable to hex (oklab, oklch, lch, lab, color(), display-p3)
|
|
1004
|
+
if (/^(oklab|oklch|lch|lab|color)\s*\(/i.test(value)) return false;
|
|
1003
1005
|
// Accept hex, rgb, hsl, named colors
|
|
1004
1006
|
return /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|[a-z]+)/i.test(value);
|
|
1005
1007
|
}
|
|
@@ -1171,12 +1173,14 @@ async function extractColors(page) {
|
|
|
1171
1173
|
const colorRegex = /(#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-z]+)/gi;
|
|
1172
1174
|
const matches = colorValue.match(colorRegex) || [];
|
|
1173
1175
|
|
|
1174
|
-
// Filter out invalid matches
|
|
1176
|
+
// Filter out invalid matches and modern CSS function names that aren't parseable
|
|
1177
|
+
const cssColorFunctions = new Set(['oklab','oklch','lch','lab','color','display','hsl','rgb','rgba','hsla','inherit','initial','unset','none','auto','normal']);
|
|
1175
1178
|
return matches.filter(c =>
|
|
1176
1179
|
c !== 'transparent' &&
|
|
1177
1180
|
c !== 'rgba(0, 0, 0, 0)' &&
|
|
1178
1181
|
c !== 'rgba(0,0,0,0)' &&
|
|
1179
|
-
c.length > 2
|
|
1182
|
+
c.length > 2 &&
|
|
1183
|
+
!cssColorFunctions.has(c.toLowerCase())
|
|
1180
1184
|
);
|
|
1181
1185
|
}
|
|
1182
1186
|
|
|
@@ -1244,9 +1248,10 @@ async function extractColors(page) {
|
|
|
1244
1248
|
return true;
|
|
1245
1249
|
}
|
|
1246
1250
|
|
|
1247
|
-
// Chromatic colors
|
|
1248
|
-
// or framework internals
|
|
1249
|
-
|
|
1251
|
+
// Chromatic colors with no background usage and no semantic context are likely browser
|
|
1252
|
+
// defaults or framework internals. But link/text brand colors have semantic score > count
|
|
1253
|
+
// (they appear on semantically-labeled elements), so exempt those.
|
|
1254
|
+
if (data.bgCount === 0 && data.score < data.count * 1.5) {
|
|
1250
1255
|
const hex = normalized.replace('#', '');
|
|
1251
1256
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
1252
1257
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
@@ -1254,7 +1259,6 @@ async function extractColors(page) {
|
|
|
1254
1259
|
const max = Math.max(r, g, b);
|
|
1255
1260
|
const min = Math.min(r, g, b);
|
|
1256
1261
|
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
1257
|
-
// If saturation > 0.3, this is a chromatic color with no background usage
|
|
1258
1262
|
if (saturation > 0.3) {
|
|
1259
1263
|
return true;
|
|
1260
1264
|
}
|
|
@@ -1347,8 +1351,10 @@ async function extractColors(page) {
|
|
|
1347
1351
|
|
|
1348
1352
|
const palette = Array.from(colorMap.entries())
|
|
1349
1353
|
.filter(([normalizedColor, data]) => {
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1354
|
+
// High-score colors (semantically significant: buttons, headers with brand colors) bypass
|
|
1355
|
+
// the count threshold — a header that appears once can still be a primary brand color.
|
|
1356
|
+
const highScore = data.score >= 10 || (data.count > 0 && data.score / data.count >= 3);
|
|
1357
|
+
if (!highScore && data.count < threshold) return false;
|
|
1352
1358
|
|
|
1353
1359
|
// Filter out structural colors (very high usage without semantic context)
|
|
1354
1360
|
if (isStructuralColor(data, totalElements)) {
|