dembrandt 0.12.5 → 0.12.7
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 +23 -2
- package/index.js +29 -9
- package/lib/extractors/index.js +37 -36
- package/lib/formatters/markdown.js +527 -202
- package/lib/formatters/terminal.js +59 -106
- package/lib/formatters/theme.js +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,11 +45,12 @@ Or add to your project's `.mcp.json`:
|
|
|
45
45
|
|
|
46
46
|
## What to expect from extraction?
|
|
47
47
|
|
|
48
|
-
- Colors (semantic, palette, CSS variables)
|
|
48
|
+
- Colors (semantic, palette, CSS variables, gradients)
|
|
49
49
|
- Typography (fonts, sizes, weights, sources)
|
|
50
50
|
- Spacing (margin/padding scales)
|
|
51
51
|
- Borders (radius, widths, styles, colors)
|
|
52
52
|
- Shadows
|
|
53
|
+
- Motion (duration scale, easing curves, hover patterns per component type)
|
|
53
54
|
- Components (buttons, badges, inputs, links)
|
|
54
55
|
- Breakpoints
|
|
55
56
|
- Icons & frameworks
|
|
@@ -134,13 +135,15 @@ The DTCG format is an industry-standard JSON schema that can be consumed by desi
|
|
|
134
135
|
|
|
135
136
|
### DESIGN.md
|
|
136
137
|
|
|
137
|
-
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.
|
|
138
|
+
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. The export follows Google's DESIGN.md draft format: YAML design tokens in front matter plus ordered Markdown guidance sections.
|
|
138
139
|
|
|
139
140
|
```bash
|
|
140
141
|
dembrandt example.com --design-md
|
|
141
142
|
# Saves to: output/example.com/DESIGN.md
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
DESIGN.md reports only what Dembrandt observed on the source site. Exact values (colors, typography, spacing, radii, shadows) live in the YAML front matter when available, and the Markdown body adds human-readable context. Sections with no extracted evidence are omitted rather than filled with invented defaults. For example, the elevation section is dropped when the site uses no box-shadow tokens.
|
|
146
|
+
|
|
144
147
|
### WCAG Contrast Analysis
|
|
145
148
|
|
|
146
149
|
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.
|
|
@@ -151,6 +154,24 @@ dembrandt stripe.com --wcag
|
|
|
151
154
|
|
|
152
155
|
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
156
|
|
|
157
|
+
Also captures **interactive state contrast**: dembrandt simulates hover, focus, and disabled states on buttons, links, and inputs and checks contrast on each state. State pairs are tagged `[hover]`, `[focus]`, or `[disabled]` in output so you can catch contrast failures that only appear on interaction.
|
|
158
|
+
|
|
159
|
+
### Motion Tokens
|
|
160
|
+
|
|
161
|
+
Motion tokens are extracted automatically on every run — no flag needed. Dembrandt analyzes CSS transitions and animations across the page and returns a structured motion profile.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
dembrandt stripe.com
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
- **Duration scale**: all unique animation durations found on the page
|
|
169
|
+
- **Easing curves**: named easing types (ease-out, spring, custom cubic-bezier) with usage counts
|
|
170
|
+
- **Per-context profiles**: motion behavior by component type (button, nav, card, modal, hero)
|
|
171
|
+
- **Hover interaction deltas**: which properties animate on hover (transform, opacity, background, color) and the pattern (scale-up, fade-in, color-shift, slide-y)
|
|
172
|
+
|
|
173
|
+
Motion data is included in JSON output as `motion` and printed in terminal under a dedicated Motion section.
|
|
174
|
+
|
|
154
175
|
### Brand Guide PDF
|
|
155
176
|
|
|
156
177
|
Use `--brand-guide` to generate a printable PDF summarizing the extracted design system: colors, typography, components, and logo on a single document.
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import ora from "ora";
|
|
|
13
13
|
import { chromium, firefox } from "playwright-core";
|
|
14
14
|
import { extractBranding } from "./lib/extractors/index.js";
|
|
15
15
|
import { displayResults } from "./lib/formatters/terminal.js";
|
|
16
|
+
import { color } from "./lib/formatters/theme.js";
|
|
16
17
|
import { toW3CFormat } from "./lib/formatters/w3c.js";
|
|
17
18
|
import { generatePDF } from "./lib/formatters/pdf.js";
|
|
18
19
|
import { generateDesignMd } from "./lib/formatters/markdown.js";
|
|
@@ -213,6 +214,9 @@ program
|
|
|
213
214
|
// Convert to W3C format if requested
|
|
214
215
|
const outputData = opts.dtcg ? toW3CFormat(result) : result;
|
|
215
216
|
|
|
217
|
+
// Collect "saved to" notices and print them after the results below
|
|
218
|
+
const savedNotices = [];
|
|
219
|
+
|
|
216
220
|
// Save JSON output if --save-output or --dtcg is specified
|
|
217
221
|
if (opts.saveOutput || opts.dtcg) {
|
|
218
222
|
try {
|
|
@@ -230,16 +234,19 @@ program
|
|
|
230
234
|
const filepath = join(outputDir, filename);
|
|
231
235
|
writeFileSync(filepath, JSON.stringify(outputData, null, 2));
|
|
232
236
|
|
|
233
|
-
|
|
237
|
+
const jsonLabel = opts.dtcg
|
|
238
|
+
? 'DTCG tokens saved (--dtcg)'
|
|
239
|
+
: 'JSON saved (--save-output)';
|
|
240
|
+
savedNotices.push(
|
|
234
241
|
chalk.dim(
|
|
235
|
-
`💾
|
|
242
|
+
`💾 ${jsonLabel}: ${color.info(
|
|
236
243
|
`output/${domain}/${filename}`
|
|
237
244
|
)}`
|
|
238
245
|
)
|
|
239
246
|
);
|
|
240
247
|
} catch (err) {
|
|
241
248
|
console.log(
|
|
242
|
-
|
|
249
|
+
color.warning(`! Could not save JSON file: ${err.message}`)
|
|
243
250
|
);
|
|
244
251
|
}
|
|
245
252
|
}
|
|
@@ -258,9 +265,9 @@ program
|
|
|
258
265
|
spinner.start("Generating PDF brand guide...");
|
|
259
266
|
await generatePDF(result, pdfPath, browser);
|
|
260
267
|
spinner.stop();
|
|
261
|
-
|
|
268
|
+
savedNotices.push(
|
|
262
269
|
chalk.dim(
|
|
263
|
-
|
|
270
|
+
`💾 Brand guide PDF saved (--brand-guide): ${color.info(
|
|
264
271
|
`output/${pdfDomain}/${pdfFilename}`
|
|
265
272
|
)}`
|
|
266
273
|
)
|
|
@@ -268,7 +275,7 @@ program
|
|
|
268
275
|
} catch (err) {
|
|
269
276
|
spinner.stop();
|
|
270
277
|
console.log(
|
|
271
|
-
|
|
278
|
+
color.warning(`Could not generate PDF: ${err.message}`)
|
|
272
279
|
);
|
|
273
280
|
}
|
|
274
281
|
}
|
|
@@ -281,27 +288,40 @@ program
|
|
|
281
288
|
mkdirSync(mdDir, { recursive: true });
|
|
282
289
|
const mdPath = join(mdDir, "DESIGN.md");
|
|
283
290
|
writeFileSync(mdPath, generateDesignMd(result));
|
|
284
|
-
|
|
291
|
+
savedNotices.push(
|
|
285
292
|
chalk.dim(
|
|
286
|
-
|
|
293
|
+
`💾 DESIGN.md saved (--design-md): ${color.info(
|
|
287
294
|
`output/${mdDomain}/DESIGN.md`
|
|
288
295
|
)}`
|
|
289
296
|
)
|
|
290
297
|
);
|
|
291
298
|
} catch (err) {
|
|
292
299
|
console.log(
|
|
293
|
-
|
|
300
|
+
color.warning(`Could not generate DESIGN.md: ${err.message}`)
|
|
294
301
|
);
|
|
295
302
|
}
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
// Output to terminal
|
|
306
|
+
const summaryLine =
|
|
307
|
+
color.accent('✨ Analysis summary: ') +
|
|
308
|
+
chalk.dim(
|
|
309
|
+
`${result.colors?.palette?.length ?? 0} colors, ` +
|
|
310
|
+
`${result.typography?.styles?.length ?? 0} text styles, ` +
|
|
311
|
+
`${result.breakpoints?.length ?? 0} breakpoints.`
|
|
312
|
+
);
|
|
299
313
|
if (opts.jsonOnly) {
|
|
300
314
|
console.log = originalConsoleLog;
|
|
301
315
|
console.log(JSON.stringify(outputData, null, 2));
|
|
316
|
+
// Keep stdout pure JSON: summary and notices go to stderr
|
|
317
|
+
console.error(summaryLine);
|
|
318
|
+
for (const notice of savedNotices) console.error(notice);
|
|
302
319
|
} else {
|
|
303
320
|
console.log();
|
|
304
321
|
displayResults(result);
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(summaryLine);
|
|
324
|
+
for (const notice of savedNotices) console.log(notice);
|
|
305
325
|
}
|
|
306
326
|
} catch (err) {
|
|
307
327
|
spinner.fail("Failed");
|
package/lib/extractors/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { color } from '../formatters/theme.js';
|
|
2
3
|
import { discoverLinks } from '../discovery.js';
|
|
3
4
|
import { extractLogo, extractSiteName } from './logo.js';
|
|
4
5
|
import { extractColors } from './colors.js';
|
|
@@ -69,11 +70,11 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
69
70
|
const initialDomain = new URL(initialUrl).hostname;
|
|
70
71
|
const finalDomain = new URL(finalUrl).hostname;
|
|
71
72
|
if (initialDomain !== finalDomain) {
|
|
72
|
-
console.log(
|
|
73
|
+
console.log(color.warning(` ! Page redirected to different domain:`));
|
|
73
74
|
console.log(chalk.dim(` From: ${initialUrl}`));
|
|
74
75
|
console.log(chalk.dim(` To: ${finalUrl}`));
|
|
75
76
|
} else {
|
|
76
|
-
console.log(
|
|
77
|
+
console.log(color.info(` i Page redirected within same domain:`));
|
|
77
78
|
console.log(chalk.dim(` From: ${initialUrl}`));
|
|
78
79
|
console.log(chalk.dim(` To: ${finalUrl}`));
|
|
79
80
|
}
|
|
@@ -81,7 +82,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
spinner.stop();
|
|
84
|
-
console.log(
|
|
85
|
+
console.log(color.success(` ✓ Page loaded`));
|
|
85
86
|
|
|
86
87
|
spinner.start("Waiting for body content to render...");
|
|
87
88
|
try {
|
|
@@ -90,10 +91,10 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
90
91
|
{ timeout: (options.navigationTimeout || 20000) * timeoutMultiplier }
|
|
91
92
|
);
|
|
92
93
|
spinner.stop();
|
|
93
|
-
console.log(
|
|
94
|
+
console.log(color.success(` ✓ Body content rendered`));
|
|
94
95
|
} catch {
|
|
95
96
|
spinner.stop();
|
|
96
|
-
console.log(
|
|
97
|
+
console.log(color.warning(` ! Body content timeout (continuing)`));
|
|
97
98
|
timeouts.push('Body content rendering');
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -101,7 +102,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
101
102
|
const hydrationTime = 8000 * timeoutMultiplier;
|
|
102
103
|
await page.waitForTimeout(hydrationTime);
|
|
103
104
|
spinner.stop();
|
|
104
|
-
console.log(
|
|
105
|
+
console.log(color.success(` ✓ Hydration complete (${hydrationTime / 1000}s)`));
|
|
105
106
|
|
|
106
107
|
spinner.start("Waiting for main content...");
|
|
107
108
|
try {
|
|
@@ -109,10 +110,10 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
109
110
|
timeout: 10000 * timeoutMultiplier,
|
|
110
111
|
});
|
|
111
112
|
spinner.stop();
|
|
112
|
-
console.log(
|
|
113
|
+
console.log(color.success(` ✓ Main content detected`));
|
|
113
114
|
} catch {
|
|
114
115
|
spinner.stop();
|
|
115
|
-
console.log(
|
|
116
|
+
console.log(color.warning(` ! Main content selector timeout (continuing)`));
|
|
116
117
|
timeouts.push('Main content selector');
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -131,25 +132,25 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
131
132
|
window.scrollTo(0, 0);
|
|
132
133
|
});
|
|
133
134
|
spinner.stop();
|
|
134
|
-
console.log(
|
|
135
|
+
console.log(color.success(` ✓ Full page scrolled (lazy content triggered)`));
|
|
135
136
|
|
|
136
137
|
spinner.start("Final content stabilization...");
|
|
137
138
|
const stabilizationTime = 4000 * timeoutMultiplier;
|
|
138
139
|
await page.waitForTimeout(stabilizationTime);
|
|
139
140
|
spinner.stop();
|
|
140
|
-
console.log(
|
|
141
|
+
console.log(color.success(` ✓ Page fully loaded and stable`));
|
|
141
142
|
|
|
142
143
|
spinner.start("Validating page content...");
|
|
143
144
|
const contentLength = await page.evaluate(() => document.body.textContent.length);
|
|
144
145
|
spinner.stop();
|
|
145
146
|
|
|
146
147
|
if (contentLength > 100) {
|
|
147
|
-
console.log(
|
|
148
|
+
console.log(color.success(` ✓ Content validated: ${contentLength} chars`));
|
|
148
149
|
break;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
spinner.warn(`Page seems empty (attempt ${attempts}/${maxAttempts}), retrying...`);
|
|
152
|
-
console.log(
|
|
153
|
+
console.log(color.warning(` ! Content length: ${contentLength} chars (expected >100)`));
|
|
153
154
|
await page.waitForTimeout(3000 * timeoutMultiplier);
|
|
154
155
|
} catch (err) {
|
|
155
156
|
if (attempts >= maxAttempts) {
|
|
@@ -165,7 +166,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
spinner.stop();
|
|
168
|
-
console.log(
|
|
169
|
+
console.log(color.info("\n Extracting design tokens...\n"));
|
|
169
170
|
|
|
170
171
|
spinner.start("Analyzing design system (16 parallel tasks)...");
|
|
171
172
|
const [
|
|
@@ -207,22 +208,22 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
207
208
|
]);
|
|
208
209
|
|
|
209
210
|
spinner.stop();
|
|
210
|
-
console.log(colors.palette.length > 0 ?
|
|
211
|
-
console.log(typography.styles.length > 0 ?
|
|
212
|
-
console.log(spacing.commonValues.length > 0 ?
|
|
213
|
-
console.log(borderRadius.values.length > 0 ?
|
|
211
|
+
console.log(colors.palette.length > 0 ? color.success(` ✓ Colors: ${colors.palette.length} found`) : color.warning(` ! Colors: 0 found`));
|
|
212
|
+
console.log(typography.styles.length > 0 ? color.success(` ✓ Typography: ${typography.styles.length} styles`) : color.warning(` ! Typography: 0 styles`));
|
|
213
|
+
console.log(spacing.commonValues.length > 0 ? color.success(` ✓ Spacing: ${spacing.commonValues.length} values`) : color.warning(` ! Spacing: 0 values`));
|
|
214
|
+
console.log(borderRadius.values.length > 0 ? color.success(` ✓ Border radius: ${borderRadius.values.length} values`) : color.warning(` ! Border radius: 0 values`));
|
|
214
215
|
|
|
215
216
|
const bordersTotal = (borders?.combinations?.length || 0);
|
|
216
|
-
console.log(bordersTotal > 0 ?
|
|
217
|
-
console.log(shadows.length > 0 ?
|
|
218
|
-
console.log(buttons.length > 0 ?
|
|
219
|
-
console.log(inputs.text?.length > 0 ?
|
|
220
|
-
console.log(links.length > 0 ?
|
|
221
|
-
console.log(breakpoints.length > 0 ?
|
|
222
|
-
console.log(iconSystem.length > 0 ?
|
|
223
|
-
console.log(frameworks.length > 0 ?
|
|
224
|
-
console.log(gradients.length > 0 ?
|
|
225
|
-
console.log(motion.durations.length > 0 ?
|
|
217
|
+
console.log(bordersTotal > 0 ? color.success(` ✓ Borders: ${bordersTotal} combinations`) : color.warning(` ! Borders: 0 found`));
|
|
218
|
+
console.log(shadows.length > 0 ? color.success(` ✓ Shadows: ${shadows.length} found`) : color.warning(` ! Shadows: 0 found`));
|
|
219
|
+
console.log(buttons.length > 0 ? color.success(` ✓ Buttons: ${buttons.length} variants`) : color.warning(` ! Buttons: 0 variants`));
|
|
220
|
+
console.log(inputs.text?.length > 0 ? color.success(` ✓ Inputs: found`) : color.warning(` ! Inputs: 0 styles`));
|
|
221
|
+
console.log(links.length > 0 ? color.success(` ✓ Links: ${links.length} styles`) : color.warning(` ! Links: 0 styles`));
|
|
222
|
+
console.log(breakpoints.length > 0 ? color.success(` ✓ Breakpoints: ${breakpoints.length} detected`) : color.warning(` ! Breakpoints: 0 detected`));
|
|
223
|
+
console.log(iconSystem.length > 0 ? color.success(` ✓ Icon systems: ${iconSystem.length} detected`) : color.warning(` ! Icon systems: 0 detected`));
|
|
224
|
+
console.log(frameworks.length > 0 ? color.success(` ✓ Frameworks: ${frameworks.length} detected`) : color.warning(` ! Frameworks: 0 detected`));
|
|
225
|
+
console.log(gradients.length > 0 ? color.success(` ✓ Gradients: ${gradients.length} found`) : color.info(` · Gradients: 0 found`));
|
|
226
|
+
console.log(motion.durations.length > 0 ? color.success(` ✓ Motion: ${motion.durations.length} durations, ${motion.easings.length} easings`) : color.info(` · Motion: none detected`));
|
|
226
227
|
console.log();
|
|
227
228
|
|
|
228
229
|
// Hover/focus state extraction
|
|
@@ -427,8 +428,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
427
428
|
|
|
428
429
|
spinner.stop();
|
|
429
430
|
console.log(hoverFocusColors.length > 0 ?
|
|
430
|
-
|
|
431
|
-
|
|
431
|
+
color.success(` ✓ Hover/focus: ${hoverFocusColors.length} state colors found`) :
|
|
432
|
+
color.warning(` ! Hover/focus: 0 state colors found`));
|
|
432
433
|
|
|
433
434
|
// Dark mode
|
|
434
435
|
if (options.darkMode) {
|
|
@@ -458,7 +459,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
458
459
|
links.push(...darkModeLinks.map((link) => ({ ...link, source: "dark-mode" })));
|
|
459
460
|
|
|
460
461
|
spinner.stop();
|
|
461
|
-
console.log(
|
|
462
|
+
console.log(color.success(` ✓ Dark mode: +${darkModeColors.palette.length} colors`));
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
// Mobile viewport
|
|
@@ -476,19 +477,19 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
476
477
|
colors.palette = mergedPalette;
|
|
477
478
|
|
|
478
479
|
spinner.stop();
|
|
479
|
-
console.log(
|
|
480
|
+
console.log(color.success(` ✓ Mobile: +${mobileColors.palette.length} colors`));
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
spinner.stop();
|
|
483
484
|
console.log();
|
|
484
|
-
console.log(
|
|
485
|
+
console.log(color.success.bold("✓ Brand extraction complete!"));
|
|
485
486
|
|
|
486
487
|
if (timeouts.length > 0 && !options.slow) {
|
|
487
488
|
console.log();
|
|
488
|
-
console.log(
|
|
489
|
+
console.log(color.warning(`! ${timeouts.length} timeout(s) occurred during extraction:`));
|
|
489
490
|
timeouts.forEach(t => console.log(chalk.dim(` • ${t}`)));
|
|
490
491
|
console.log();
|
|
491
|
-
console.log(
|
|
492
|
+
console.log(color.info(`💡 Tip: Try running with ${chalk.bold('--slow')} flag for more reliable results on slow-loading sites`));
|
|
492
493
|
}
|
|
493
494
|
|
|
494
495
|
let wcag = [];
|
|
@@ -531,8 +532,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
531
532
|
const staticPassing = wcag.filter(p => !p.source && p.aa).length;
|
|
532
533
|
const staticTotal = wcag.filter(p => !p.source).length;
|
|
533
534
|
const statesFailing = wcag.filter(p => p.source === 'state' && !p.aa).length;
|
|
534
|
-
console.log(
|
|
535
|
-
(statesFailing ?
|
|
535
|
+
console.log(color.success(` ✓ WCAG: ${staticPassing}/${staticTotal} pairs pass AA`) +
|
|
536
|
+
(statesFailing ? color.warning(` · ${statesFailing} state pair(s) fail`) : ''));
|
|
536
537
|
} catch {
|
|
537
538
|
spinner.stop();
|
|
538
539
|
}
|