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 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)
@@ -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
- { logo, instances: logoInstances, favicons },
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
- siteName,
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,
@@ -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, { sizes: new Set(), weights: new Set() });
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) familyData.sizes.add(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 sizes = [...data.sizes]
321
- .sort((a, b) => b - a)
322
- .map(px => `${px}px`);
323
- const sizeList = sizes.length
324
- ? ' ' + chalk.dim('[ ') + sizes.join(', ') + 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dembrandt",
3
- "version": "0.12.10",
3
+ "version": "0.13.0",
4
4
  "description": "Extract design tokens and publicly visible CSS information from any website",
5
5
  "mcpName": "io.github.dembrandt/dembrandt",
6
6
  "main": "index.js",