dembrandt 0.12.10 → 0.13.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 +17 -0
- package/lib/extractors/index.js +67 -2
- package/lib/extractors/logo.js +9 -0
- package/lib/formatters/terminal.js +9 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,23 @@ Or add to your project's `.mcp.json`:
|
|
|
43
43
|
|
|
44
44
|
7 tools available: `get_design_tokens`, `get_color_palette`, `get_typography`, `get_component_styles`, `get_surfaces`, `get_spacing`, `get_brand_identity`.
|
|
45
45
|
|
|
46
|
+
Pair with **[dembrandt-skills](https://github.com/dembrandt/dembrandt-skills)** to give your agent UX intelligence on top of extracted tokens — hierarchy, accessibility, interaction states, and a full 6-stage design pipeline orchestrator.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx skills add dembrandt/dembrandt-skills
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Dembrandt App (Beta)
|
|
53
|
+
|
|
54
|
+
The **[Dembrandt App](https://www.dembrandt.com/app)** is the new home for your design system audits. It replaces the legacy Local UI with a more powerful, web-based management experience.
|
|
55
|
+
|
|
56
|
+
* **Visual Management:** View typography, color palettes, and spacing scales in a clean, human-readable dashboard.
|
|
57
|
+
* **Privacy-First:** No login required. No analytics. Your data is stored locally in your browser and is never sent to our servers.
|
|
58
|
+
* **CLI Integration:** Simply run your extraction with `--save-output` and open the resulting JSON in the App for deep analysis.
|
|
59
|
+
* **AI-Ready:** Copy tokens directly into your agentic workflows (Cursor, Claude, etc.) with one click.
|
|
60
|
+
* **Public Beta:** More best-in-class features will be added to dembrandt App in upcoming weeks.
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
## What to expect from extraction?
|
|
47
64
|
|
|
48
65
|
- Colors (semantic, palette, CSS variables, gradients)
|
package/lib/extractors/index.js
CHANGED
|
@@ -170,7 +170,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
170
170
|
|
|
171
171
|
spinner.start("Analyzing design system (17 parallel tasks)...");
|
|
172
172
|
const [
|
|
173
|
-
|
|
173
|
+
logoResult,
|
|
174
174
|
colors,
|
|
175
175
|
typography,
|
|
176
176
|
spacing,
|
|
@@ -184,7 +184,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
184
184
|
breakpoints,
|
|
185
185
|
iconSystem,
|
|
186
186
|
frameworks,
|
|
187
|
-
|
|
187
|
+
siteNameRaw,
|
|
188
188
|
gradients,
|
|
189
189
|
motion,
|
|
190
190
|
] = await Promise.all([
|
|
@@ -207,7 +207,71 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
207
207
|
extractMotion(page),
|
|
208
208
|
]);
|
|
209
209
|
|
|
210
|
+
const { logo, instances: logoInstances, favicons, manifest } = logoResult;
|
|
211
|
+
let siteName = siteNameRaw;
|
|
212
|
+
|
|
210
213
|
spinner.stop();
|
|
214
|
+
|
|
215
|
+
// Inject manifest theme_color / background_color as high-confidence palette entries
|
|
216
|
+
if (manifest) {
|
|
217
|
+
const manifestColorEntries = [
|
|
218
|
+
manifest.themeColor && { color: manifest.themeColor, label: 'manifest:theme_color' },
|
|
219
|
+
manifest.backgroundColor && { color: manifest.backgroundColor, label: 'manifest:background_color' },
|
|
220
|
+
].filter(Boolean);
|
|
221
|
+
|
|
222
|
+
const rawManifestColors = manifestColorEntries.map(e => e.color);
|
|
223
|
+
const manifestNormMap = rawManifestColors.length ? await page.evaluate((cols) => {
|
|
224
|
+
const canvas = document.createElement('canvas');
|
|
225
|
+
canvas.width = canvas.height = 1;
|
|
226
|
+
const ctx = canvas.getContext('2d');
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const c of cols) {
|
|
229
|
+
if (/^#[0-9a-f]{6}$/i.test(c)) { out[c] = c.toLowerCase(); continue; }
|
|
230
|
+
if (/^#[0-9a-f]{3}$/i.test(c)) { out[c] = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`.toLowerCase(); continue; }
|
|
231
|
+
if (/^#[0-9a-f]{8}$/i.test(c)) { out[c] = c.toLowerCase().slice(0, 7); continue; }
|
|
232
|
+
const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
233
|
+
if (m) { out[c] = `#${parseInt(m[1]).toString(16).padStart(2,'0')}${parseInt(m[2]).toString(16).padStart(2,'0')}${parseInt(m[3]).toString(16).padStart(2,'0')}`; continue; }
|
|
234
|
+
if (ctx) {
|
|
235
|
+
try {
|
|
236
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
237
|
+
ctx.fillStyle = 'rgba(0,0,0,0)';
|
|
238
|
+
ctx.fillStyle = c;
|
|
239
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
240
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
241
|
+
if (a > 0) { out[c] = `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`; continue; }
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
out[c] = c.toLowerCase();
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}, rawManifestColors) : {};
|
|
248
|
+
|
|
249
|
+
for (const { color: raw, label } of manifestColorEntries) {
|
|
250
|
+
const normalized = manifestNormMap[raw] ?? raw.toLowerCase();
|
|
251
|
+
if (!colors.palette.some(c => c.normalized === normalized)) {
|
|
252
|
+
colors.palette.push({ color: raw, normalized, count: 10, confidence: 'high', sources: [label] });
|
|
253
|
+
} else {
|
|
254
|
+
const existing = colors.palette.find(c => c.normalized === normalized);
|
|
255
|
+
if (existing) {
|
|
256
|
+
existing.confidence = 'high';
|
|
257
|
+
if (!existing.sources.includes(label)) existing.sources.push(label);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!siteName && (manifest.name || manifest.shortName)) {
|
|
263
|
+
siteName = manifest.name || manifest.shortName;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (manifest) {
|
|
268
|
+
const parts = [
|
|
269
|
+
manifest.themeColor && `theme: ${manifest.themeColor}`,
|
|
270
|
+
manifest.backgroundColor && `bg: ${manifest.backgroundColor}`,
|
|
271
|
+
manifest.name && `name: "${manifest.name}"`,
|
|
272
|
+
].filter(Boolean);
|
|
273
|
+
console.log(color.success(` ✓ Manifest: ${parts.join(', ')}`));
|
|
274
|
+
}
|
|
211
275
|
console.log(colors.palette.length > 0 ? color.success(` ✓ Colors: ${colors.palette.length} found`) : color.warning(` ! Colors: 0 found`));
|
|
212
276
|
console.log(typography.styles.length > 0 ? color.success(` ✓ Typography: ${typography.styles.length} styles`) : color.warning(` ! Typography: 0 styles`));
|
|
213
277
|
console.log(spacing.commonValues.length > 0 ? color.success(` ✓ Spacing: ${spacing.commonValues.length} values`) : color.warning(` ! Spacing: 0 values`));
|
|
@@ -546,6 +610,7 @@ export async function extractBranding(url, spinner, browser, options = {}) {
|
|
|
546
610
|
logo,
|
|
547
611
|
logoInstances,
|
|
548
612
|
favicons,
|
|
613
|
+
...(manifest ? { manifest } : {}),
|
|
549
614
|
colors,
|
|
550
615
|
typography,
|
|
551
616
|
spacing,
|
package/lib/extractors/logo.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function extractLogo(page, url) {
|
|
|
44
44
|
}, url);
|
|
45
45
|
|
|
46
46
|
let pwaIcons = [];
|
|
47
|
+
let manifestMeta = {};
|
|
47
48
|
if (manifestIcons.length > 0) {
|
|
48
49
|
try {
|
|
49
50
|
const manifestUrl = manifestIcons[0].manifestUrl;
|
|
@@ -63,6 +64,13 @@ export async function extractLogo(page, url) {
|
|
|
63
64
|
purpose: icon.purpose || 'any',
|
|
64
65
|
}));
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
if (response) {
|
|
69
|
+
if (response.theme_color) manifestMeta.themeColor = response.theme_color;
|
|
70
|
+
if (response.background_color) manifestMeta.backgroundColor = response.background_color;
|
|
71
|
+
if (response.name) manifestMeta.name = response.name;
|
|
72
|
+
if (response.short_name) manifestMeta.shortName = response.short_name;
|
|
73
|
+
}
|
|
66
74
|
} catch {}
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -558,6 +566,7 @@ export async function extractLogo(page, url) {
|
|
|
558
566
|
|
|
559
567
|
// Merge PWA icons into favicons
|
|
560
568
|
result.favicons = [...result.favicons, ...pwaIcons];
|
|
569
|
+
result.manifest = Object.keys(manifestMeta).length > 0 ? manifestMeta : null;
|
|
561
570
|
|
|
562
571
|
return result;
|
|
563
572
|
}
|
|
@@ -292,17 +292,18 @@ function displayTypography(typography) {
|
|
|
292
292
|
const fontFamilies = new Map();
|
|
293
293
|
|
|
294
294
|
typography.styles.forEach(style => {
|
|
295
|
-
// Skip styles without a family name
|
|
296
295
|
if (!style.family) return;
|
|
297
296
|
|
|
298
297
|
if (!fontFamilies.has(style.family)) {
|
|
299
|
-
fontFamilies.set(style.family, {
|
|
298
|
+
fontFamilies.set(style.family, { sizeContexts: new Map(), weights: new Set() });
|
|
300
299
|
}
|
|
301
300
|
|
|
302
301
|
const familyData = fontFamilies.get(style.family);
|
|
303
302
|
if (style.size) {
|
|
304
303
|
const px = Math.round(parseFloat(style.size) || 0);
|
|
305
|
-
if (px
|
|
304
|
+
if (px && !familyData.sizeContexts.has(px)) {
|
|
305
|
+
familyData.sizeContexts.set(px, style.context || null);
|
|
306
|
+
}
|
|
306
307
|
}
|
|
307
308
|
if (style.weight && style.weight !== 400) {
|
|
308
309
|
familyData.weights.add(style.weight);
|
|
@@ -317,11 +318,11 @@ function displayTypography(typography) {
|
|
|
317
318
|
const isFontLast = fontIndex === totalFonts;
|
|
318
319
|
const fontBranch = isFontLast ? '└─' : '├─';
|
|
319
320
|
|
|
320
|
-
const
|
|
321
|
-
.sort((a, b) => b - a)
|
|
322
|
-
.map(px => `${px}px`);
|
|
323
|
-
const sizeList =
|
|
324
|
-
? ' ' + chalk.dim('[ ') +
|
|
321
|
+
const sizeTokens = [...data.sizeContexts.entries()]
|
|
322
|
+
.sort((a, b) => b[0] - a[0])
|
|
323
|
+
.map(([px, ctx]) => ctx ? `${px}px ${chalk.dim(`(${ctx})`)}` : `${px}px`);
|
|
324
|
+
const sizeList = sizeTokens.length
|
|
325
|
+
? ' ' + chalk.dim('[ ') + sizeTokens.join(', ') + chalk.dim(' ]')
|
|
325
326
|
: '';
|
|
326
327
|
|
|
327
328
|
console.log(chalk.dim(`│ ${fontBranch}`) + ' ' + chalk.bold(family) + sizeList);
|