dembrandt 0.12.6 → 0.12.8

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
@@ -181,37 +181,6 @@ dembrandt example.com --brand-guide
181
181
  # Saves to: output/example.com/TIMESTAMP.brand-guide.pdf
182
182
  ```
183
183
 
184
- ## Local UI
185
-
186
- Browse your extractions in a visual interface.
187
-
188
- ### Setup
189
-
190
- ```bash
191
- cd local-ui
192
- npm install
193
- ```
194
-
195
- ### Running
196
-
197
- ```bash
198
- npm start
199
- ```
200
-
201
- Opens http://localhost:5173 with API on port 3002.
202
-
203
- ### Features
204
-
205
- - Visual grid of all extractions
206
- - Color palettes with click-to-copy
207
- - Typography specimens
208
- - Spacing, shadows, border radius visualization
209
- - Button and link component previews
210
- - Dark/light theme toggle
211
- - Section nav links on extraction pages, jump directly to Colors, Typography, Shadows, etc. via a sticky sidebar
212
-
213
- Extractions are performed via CLI (`dembrandt <url> --save-output`) and automatically appear in the UI.
214
-
215
184
  ## Recipes
216
185
 
217
186
  **Quick brand scan**
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
- console.log(
237
+ const jsonLabel = opts.dtcg
238
+ ? 'DTCG tokens saved (--dtcg)'
239
+ : 'JSON saved (--save-output)';
240
+ savedNotices.push(
234
241
  chalk.dim(
235
- `💾 JSON saved to: ${chalk.hex('#8BE9FD')(
242
+ `💾 ${jsonLabel}: ${color.info(
236
243
  `output/${domain}/${filename}`
237
244
  )}`
238
245
  )
239
246
  );
240
247
  } catch (err) {
241
248
  console.log(
242
- chalk.hex('#FFB86C')(`⚠ Could not save JSON file: ${err.message}`)
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
- console.log(
268
+ savedNotices.push(
262
269
  chalk.dim(
263
- `PDF saved to: ${chalk.hex('#8BE9FD')(
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
- chalk.hex('#FFB86C')(`Could not generate PDF: ${err.message}`)
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
- console.log(
291
+ savedNotices.push(
285
292
  chalk.dim(
286
- `DESIGN.md saved to: ${chalk.hex('#8BE9FD')(
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
- chalk.hex('#FFB86C')(`Could not generate DESIGN.md: ${err.message}`)
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");
@@ -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(chalk.hex('#FFB86C')(` Page redirected to different domain:`));
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(chalk.hex('#8BE9FD')(` Page redirected within same domain:`));
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(chalk.hex('#50FA7B')(` ✓ Page loaded`));
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(chalk.hex('#50FA7B')(` ✓ Body content rendered`));
94
+ console.log(color.success(` ✓ Body content rendered`));
94
95
  } catch {
95
96
  spinner.stop();
96
- console.log(chalk.hex('#FFB86C')(` Body content timeout (continuing)`));
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(chalk.hex('#50FA7B')(` ✓ Hydration complete (${hydrationTime / 1000}s)`));
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(chalk.hex('#50FA7B')(` ✓ Main content detected`));
113
+ console.log(color.success(` ✓ Main content detected`));
113
114
  } catch {
114
115
  spinner.stop();
115
- console.log(chalk.hex('#FFB86C')(` Main content selector timeout (continuing)`));
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(chalk.hex('#50FA7B')(` ✓ Full page scrolled (lazy content triggered)`));
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(chalk.hex('#50FA7B')(` ✓ Page fully loaded and stable`));
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(chalk.hex('#50FA7B')(` ✓ Content validated: ${contentLength} chars`));
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(chalk.hex('#FFB86C')(` Content length: ${contentLength} chars (expected >100)`));
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(chalk.hex('#8BE9FD')("\n Extracting design tokens...\n"));
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 ? chalk.hex('#50FA7B')(` ✓ Colors: ${colors.palette.length} found`) : chalk.hex('#FFB86C')(` Colors: 0 found`));
211
- console.log(typography.styles.length > 0 ? chalk.hex('#50FA7B')(` ✓ Typography: ${typography.styles.length} styles`) : chalk.hex('#FFB86C')(` Typography: 0 styles`));
212
- console.log(spacing.commonValues.length > 0 ? chalk.hex('#50FA7B')(` ✓ Spacing: ${spacing.commonValues.length} values`) : chalk.hex('#FFB86C')(` Spacing: 0 values`));
213
- console.log(borderRadius.values.length > 0 ? chalk.hex('#50FA7B')(` ✓ Border radius: ${borderRadius.values.length} values`) : chalk.hex('#FFB86C')(` Border radius: 0 values`));
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 ? chalk.hex('#50FA7B')(` ✓ Borders: ${bordersTotal} combinations`) : chalk.hex('#FFB86C')(` Borders: 0 found`));
217
- console.log(shadows.length > 0 ? chalk.hex('#50FA7B')(` ✓ Shadows: ${shadows.length} found`) : chalk.hex('#FFB86C')(` Shadows: 0 found`));
218
- console.log(buttons.length > 0 ? chalk.hex('#50FA7B')(` ✓ Buttons: ${buttons.length} variants`) : chalk.hex('#FFB86C')(` Buttons: 0 variants`));
219
- console.log(inputs.text?.length > 0 ? chalk.hex('#50FA7B')(` ✓ Inputs: found`) : chalk.hex('#FFB86C')(` Inputs: 0 styles`));
220
- console.log(links.length > 0 ? chalk.hex('#50FA7B')(` ✓ Links: ${links.length} styles`) : chalk.hex('#FFB86C')(` Links: 0 styles`));
221
- console.log(breakpoints.length > 0 ? chalk.hex('#50FA7B')(` ✓ Breakpoints: ${breakpoints.length} detected`) : chalk.hex('#FFB86C')(` Breakpoints: 0 detected`));
222
- console.log(iconSystem.length > 0 ? chalk.hex('#50FA7B')(` ✓ Icon systems: ${iconSystem.length} detected`) : chalk.hex('#FFB86C')(` Icon systems: 0 detected`));
223
- console.log(frameworks.length > 0 ? chalk.hex('#50FA7B')(` ✓ Frameworks: ${frameworks.length} detected`) : chalk.hex('#FFB86C')(` Frameworks: 0 detected`));
224
- console.log(gradients.length > 0 ? chalk.hex('#50FA7B')(` ✓ Gradients: ${gradients.length} found`) : chalk.hex('#8BE9FD')(` · Gradients: 0 found`));
225
- console.log(motion.durations.length > 0 ? chalk.hex('#50FA7B')(` ✓ Motion: ${motion.durations.length} durations, ${motion.easings.length} easings`) : chalk.hex('#8BE9FD')(` · Motion: none detected`));
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
- chalk.hex('#50FA7B')(` ✓ Hover/focus: ${hoverFocusColors.length} state colors found`) :
431
- chalk.hex('#FFB86C')(` Hover/focus: 0 state colors found`));
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(chalk.hex('#50FA7B')(` ✓ Dark mode: +${darkModeColors.palette.length} colors`));
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(chalk.hex('#50FA7B')(` ✓ Mobile: +${mobileColors.palette.length} colors`));
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(chalk.hex('#50FA7B').bold(" Brand extraction complete!"));
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(chalk.hex('#FFB86C')(`⚠ ${timeouts.length} timeout(s) occurred during extraction:`));
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(chalk.hex('#8BE9FD')(`💡 Tip: Try running with ${chalk.bold('--slow')} flag for more reliable results on slow-loading sites`));
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(chalk.hex('#50FA7B')(` ✓ WCAG: ${staticPassing}/${staticTotal} pairs pass AA`) +
535
- (statesFailing ? chalk.hex('#FFB86C')(` · ${statesFailing} state pair(s) fail`) : ''));
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
  }
@@ -157,6 +157,7 @@ function buildTypographyTokens(result) {
157
157
  fontWeight: normalizeFontWeight(style.fontWeight ?? style.weight),
158
158
  lineHeight: normalizeLineHeight(style.lineHeight),
159
159
  letterSpacing: normalizeDimension(style.letterSpacing),
160
+ fontFeature: normalizeFontFeature(style.fontFeatures ?? style.fontFeature),
160
161
  });
161
162
 
162
163
  if (!token || !token.fontFamily && !token.fontSize) continue;
@@ -408,6 +409,12 @@ function normalizeFontFamily(value) {
408
409
  return String(value).trim().replace(/\s+/g, ' ');
409
410
  }
410
411
 
412
+ function normalizeFontFeature(value) {
413
+ if (!value) return null;
414
+ const raw = String(value).trim();
415
+ return raw && raw !== 'normal' ? raw : null;
416
+ }
417
+
411
418
  function normalizeFontWeight(value) {
412
419
  if (value == null) return null;
413
420
  const parsed = parseInt(value, 10);
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import chalk from 'chalk';
9
+ import { color } from './theme.js';
9
10
  import { convertColor } from '../colors.js';
10
11
 
11
12
  /**
@@ -59,7 +60,7 @@ export function displayResults(data) {
59
60
  displayWcag(data.wcag);
60
61
 
61
62
  console.log(chalk.dim('│'));
62
- console.log(chalk.dim('└─') + ' ' + chalk.hex('#50FA7B')('✓ Complete'));
63
+ console.log(chalk.dim('└─') + ' ' + color.success('✓ Complete'));
63
64
  console.log('');
64
65
  }
65
66
 
@@ -95,7 +96,7 @@ function displayFavicons(favicons) {
95
96
  const isLast = index === favicons.length - 1;
96
97
  const branch = isLast ? '└─' : '├─';
97
98
  const sizeInfo = favicon.sizes ? chalk.dim(` · ${favicon.sizes}`) : '';
98
- console.log(chalk.dim(`│ ${branch}`) + ' ' + `${chalk.hex('#8BE9FD')(favicon.type.padEnd(18))} ${terminalLink(favicon.url)}${sizeInfo}`);
99
+ console.log(chalk.dim(`│ ${branch}`) + ' ' + `${color.info(favicon.type.padEnd(18))} ${terminalLink(favicon.url)}${sizeInfo}`);
99
100
  });
100
101
 
101
102
  console.log(chalk.dim('│'));
@@ -217,33 +218,36 @@ function displayColors(colors) {
217
218
 
218
219
  const uniqueColors = Array.from(colorMap.values());
219
220
 
220
- // Display all colors with hex, RGB, LCH, and OKLCH formats
221
- uniqueColors.forEach(({ hex, rgb, lch, oklch, label, confidence }, index) => {
221
+ // Display each color on a single line: swatch, hex, role, rgb, oklch.
222
+ // lch is omitted here for compactness but remains in JSON output.
223
+ uniqueColors.forEach(({ hex, rgb, oklch, label, confidence }, index) => {
222
224
  const isLast = index === uniqueColors.length - 1;
223
225
  const branch = isLast ? '└─' : '├─';
224
- const indent = isLast ? ' ' : '│ ';
225
226
 
227
+ let conf;
228
+ if (confidence === 'high') conf = color.success('●');
229
+ else if (confidence === 'medium') conf = color.warning('●');
230
+ else conf = chalk.gray('●'); // low confidence
231
+
232
+ let swatch;
226
233
  try {
227
- const colorBlock = chalk.bgHex(hex)(' ');
228
- let conf;
229
- if (confidence === 'high') conf = chalk.hex('#50FA7B')('●');
230
- else if (confidence === 'medium') conf = chalk.hex('#FFB86C')('●');
231
- else conf = chalk.gray('●'); // low confidence
232
-
233
- const labelText = label ? chalk.dim(` ${label}`) : '';
234
-
235
- // First line: color swatch, hex, and label
236
- console.log(chalk.dim(`│ ${branch}`) + ' ' + `${conf} ${colorBlock} ${hex}${labelText}`);
237
- // Second line: RGB and LCH
238
- console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('rgb: ') + rgb);
239
- console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('lch: ') + lch);
240
- console.log(chalk.dim(`│ ${indent}└─`) + ' ' + chalk.dim('oklch: ') + oklch);
234
+ swatch = chalk.bgHex(hex)(' ');
241
235
  } catch {
242
- console.log(chalk.dim(`│ ${branch}`) + ' ' + `${hex} ${label ? chalk.dim(label) : ''}`);
243
- console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('rgb: ') + rgb);
244
- console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('lch: ') + lch);
245
- console.log(chalk.dim(`│ ${indent}└─`) + ' ' + chalk.dim('oklch: ') + oklch);
236
+ swatch = ' ';
246
237
  }
238
+
239
+ const rawLabel = label || '';
240
+ const truncated = rawLabel.length > 22 ? rawLabel.slice(0, 21) + '…' : rawLabel;
241
+ const labelText = chalk.dim(truncated.padEnd(23));
242
+ const rgbText = chalk.dim((rgb || '').padEnd(20));
243
+
244
+ console.log(
245
+ chalk.dim(`│ ${branch}`) + ' ' +
246
+ `${conf} ${swatch} ${hex} ` +
247
+ labelText + ' ' +
248
+ rgbText + ' ' +
249
+ chalk.dim(oklch || '')
250
+ );
247
251
  });
248
252
 
249
253
  const cssVarLimit = 15;
@@ -272,7 +276,7 @@ function displayTypography(typography) {
272
276
  }
273
277
  }
274
278
 
275
- // Group styles by font family, then by context
279
+ // Group styles by font family: collect unique sizes (largest first) and weights
276
280
  if (typography.styles?.length > 0) {
277
281
  const fontFamilies = new Map();
278
282
 
@@ -281,18 +285,17 @@ function displayTypography(typography) {
281
285
  if (!style.family) return;
282
286
 
283
287
  if (!fontFamilies.has(style.family)) {
284
- fontFamilies.set(style.family, {
285
- fallbacks: style.fallbacks,
286
- contexts: new Map()
287
- });
288
+ fontFamilies.set(style.family, { sizes: new Set(), weights: new Set() });
288
289
  }
289
290
 
290
291
  const familyData = fontFamilies.get(style.family);
291
- const contextKey = style.context || 'unknown';
292
- if (!familyData.contexts.has(contextKey)) {
293
- familyData.contexts.set(contextKey, []);
292
+ if (style.size) {
293
+ const px = Math.round(parseFloat(style.size) || 0);
294
+ if (px) familyData.sizes.add(px);
295
+ }
296
+ if (style.weight && style.weight !== 400) {
297
+ familyData.weights.add(style.weight);
294
298
  }
295
- familyData.contexts.get(contextKey).push(style);
296
299
  });
297
300
 
298
301
  let fontIndex = 0;
@@ -303,69 +306,19 @@ function displayTypography(typography) {
303
306
  const isFontLast = fontIndex === totalFonts;
304
307
  const fontBranch = isFontLast ? '└─' : '├─';
305
308
 
306
- console.log(chalk.dim(`│ ${fontBranch}`) + ' ' + chalk.bold(family));
307
-
308
- if (data.fallbacks) {
309
- const indent = isFontLast ? ' ' : '│ ';
310
- console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim(`fallbacks: ${data.fallbacks}`));
311
- }
309
+ const sizes = [...data.sizes]
310
+ .sort((a, b) => b - a)
311
+ .map(px => `${px}px`);
312
+ const sizeList = sizes.length
313
+ ? ' ' + chalk.dim('[ ') + sizes.join(', ') + chalk.dim(' ]')
314
+ : '';
312
315
 
313
- let contextIndex = 0;
314
- const totalContexts = data.contexts.size;
316
+ console.log(chalk.dim(`│ ${fontBranch}`) + ' ' + chalk.bold(family) + sizeList);
315
317
 
316
- for (const [context, styles] of data.contexts) {
317
- contextIndex++;
318
- const isContextLast = contextIndex === totalContexts;
318
+ const weights = [...data.weights].sort((a, b) => a - b);
319
+ if (weights.length) {
319
320
  const indent = isFontLast ? ' ' : '│ ';
320
- const contextBranch = isContextLast ? '└─' : '├─';
321
-
322
- console.log(chalk.dim(`│ ${indent}${contextBranch}`) + ' ' + chalk.hex('#8BE9FD')(context));
323
-
324
- styles.forEach((style, styleIndex) => {
325
- const isStyleLast = styleIndex === styles.length - 1;
326
- const styleIndent = isFontLast ? ' ' : '│ ';
327
- const contextIndent = isContextLast ? ' ' : '│ ';
328
- const styleBranch = isStyleLast ? '└─' : '├─';
329
- const propIndent = isStyleLast ? ' ' : '│ ';
330
-
331
- // Main size line
332
- console.log(chalk.dim(`│ ${styleIndent}${contextIndent}${styleBranch}`) + ' ' + `${style.size}`);
333
-
334
- // Collect properties
335
- const props = [];
336
- if (style.weight && style.weight !== 400) {
337
- props.push({ key: 'weight', value: style.weight });
338
- }
339
- if (style.lineHeight) {
340
- const lh = parseFloat(style.lineHeight);
341
- let lhLabel = '';
342
- if (lh <= 1.3) lhLabel = ' (tight)';
343
- else if (lh >= 1.6) lhLabel = ' (relaxed)';
344
- props.push({ key: 'line-height', value: `${style.lineHeight}${lhLabel}` });
345
- }
346
- if (style.transform) {
347
- props.push({ key: 'transform', value: style.transform });
348
- }
349
- if (style.spacing) {
350
- props.push({ key: 'letter-spacing', value: style.spacing });
351
- }
352
- if (style.isFluid) {
353
- props.push({ key: 'fluid', value: 'yes' });
354
- }
355
- if (style.fontFeatures) {
356
- props.push({ key: 'features', value: style.fontFeatures });
357
- }
358
-
359
- // Display properties
360
- props.forEach((prop, propIndex) => {
361
- const isLastProp = propIndex === props.length - 1;
362
- const propBranch = isLastProp ? '└─' : '├─';
363
- console.log(
364
- chalk.dim(`│ ${styleIndent}${contextIndent}${propIndent}${propBranch}`) + ' ' +
365
- chalk.dim(`${prop.key}: `) + `${prop.value}`
366
- );
367
- });
368
- });
321
+ console.log(chalk.dim(`│ ${indent}└─`) + ' ' + chalk.dim('Weights: ') + weights.join(', '));
369
322
  }
370
323
  }
371
324
  }
@@ -417,7 +370,7 @@ function displayBorders(borders) {
417
370
  highConfCombos.slice(0, 10).forEach((combo, index) => {
418
371
  const isLast = index === Math.min(highConfCombos.length, 10) - 1;
419
372
  const branch = isLast ? '└─' : '├─';
420
- const conf = combo.confidence === 'high' ? chalk.hex('#50FA7B')('●') : chalk.hex('#FFB86C')('●');
373
+ const conf = combo.confidence === 'high' ? color.success('●') : color.warning('●');
421
374
 
422
375
  try {
423
376
  const formats = normalizeColorFormat(combo.color);
@@ -470,7 +423,7 @@ function displayShadows(shadows) {
470
423
  sorted.slice(0, 8).forEach((s, index) => {
471
424
  const isLast = index === Math.min(sorted.length, 8) - 1 && sorted.length <= 8;
472
425
  const branch = isLast ? '└─' : '├─';
473
- const conf = s.confidence === 'high' ? chalk.hex('#50FA7B')('●') : chalk.hex('#FFB86C')('●');
426
+ const conf = s.confidence === 'high' ? color.success('●') : color.warning('●');
474
427
  console.log(chalk.dim(`│ ${branch}`) + ' ' + `${conf} ${s.shadow}`);
475
428
  });
476
429
  if (highConfShadows.length > 8) {
@@ -543,7 +496,7 @@ function displayButtons(buttons) {
543
496
  const stateBranch = isLastState ? '└─' : '├─';
544
497
  const stateIndent = isLastState ? ' ' : '│ ';
545
498
 
546
- console.log(chalk.dim(`│ ${btnIndent}${stateBranch}`) + ' ' + chalk.hex('#8BE9FD')(stateInfo.label));
499
+ console.log(chalk.dim(`│ ${btnIndent}${stateBranch}`) + ' ' + color.info(stateInfo.label));
547
500
 
548
501
  const props = [];
549
502
 
@@ -662,7 +615,7 @@ function displayBadges(badges) {
662
615
 
663
616
  // Show badge type
664
617
  const typeLabel = badge.isRounded ? 'Pill' : badge.styleType === 'outline' ? 'Outline' : badge.styleType === 'subtle' ? 'Subtle' : 'Filled';
665
- console.log(chalk.dim(`│ ${variantIndent}${badgeBranch}`) + ' ' + chalk.hex('#8BE9FD')(typeLabel));
618
+ console.log(chalk.dim(`│ ${variantIndent}${badgeBranch}`) + ' ' + color.info(typeLabel));
666
619
 
667
620
  const props = [];
668
621
 
@@ -750,11 +703,11 @@ function displayInputs(inputs) {
750
703
  const branch = isLast ? '└─' : '├─';
751
704
  const indent = isLast ? ' ' : '│ ';
752
705
 
753
- console.log(chalk.dim(`│ ${groupIndent}${branch}`) + ' ' + chalk.hex('#8BE9FD')(input.specificType));
706
+ console.log(chalk.dim(`│ ${groupIndent}${branch}`) + ' ' + color.info(input.specificType));
754
707
 
755
708
  // Display default state
756
709
  const defaultState = input.states.default;
757
- console.log(chalk.dim(`│ ${groupIndent}${indent}├─`) + ' ' + chalk.hex('#8BE9FD')('Default'));
710
+ console.log(chalk.dim(`│ ${groupIndent}${indent}├─`) + ' ' + color.info('Default'));
758
711
 
759
712
  const defaultProps = [];
760
713
 
@@ -802,7 +755,7 @@ function displayInputs(inputs) {
802
755
  // Display focus state if available
803
756
  if (input.states.focus) {
804
757
  const focusState = input.states.focus;
805
- console.log(chalk.dim(`│ ${groupIndent}${indent}└─`) + ' ' + chalk.hex('#8BE9FD')('Focus'));
758
+ console.log(chalk.dim(`│ ${groupIndent}${indent}└─`) + ' ' + color.info('Focus'));
806
759
 
807
760
  const focusProps = [];
808
761
 
@@ -925,7 +878,7 @@ function displayLinks(links) {
925
878
 
926
879
  // Only show default state if there's decoration or hover state
927
880
  if (hasDecoration || hasHover) {
928
- console.log(chalk.dim(`│ ${linkIndent}├─`) + ' ' + chalk.hex('#8BE9FD')('Default'));
881
+ console.log(chalk.dim(`│ ${linkIndent}├─`) + ' ' + color.info('Default'));
929
882
 
930
883
  if (hasDecoration) {
931
884
  const decorBranch = hasHover ? '├─' : '└─';
@@ -936,7 +889,7 @@ function displayLinks(links) {
936
889
  // Display hover state if available
937
890
  if (hasHover) {
938
891
  const hoverState = link.states.hover;
939
- console.log(chalk.dim(`│ ${linkIndent}└─`) + ' ' + chalk.hex('#8BE9FD')('Hover'));
892
+ console.log(chalk.dim(`│ ${linkIndent}└─`) + ' ' + color.info('Hover'));
940
893
 
941
894
  const hoverProps = [];
942
895
 
@@ -998,7 +951,7 @@ function displayFrameworks(frameworks) {
998
951
  frameworks.forEach((fw, index) => {
999
952
  const isLast = index === frameworks.length - 1;
1000
953
  const branch = isLast ? '└─' : '├─';
1001
- const conf = fw.confidence === 'high' ? chalk.hex('#50FA7B')('●') : chalk.hex('#FFB86C')('●');
954
+ const conf = fw.confidence === 'high' ? color.success('●') : color.warning('●');
1002
955
  console.log(chalk.dim(`│ ${branch}`) + ' ' + `${conf} ${fw.name} ${chalk.dim(fw.evidence)}`);
1003
956
  });
1004
957
  console.log(chalk.dim('│'));
@@ -1085,12 +1038,12 @@ function displayWcag(wcag) {
1085
1038
  const fgSwatch = chalk.bgHex(pair.fg)(' ');
1086
1039
  const bgSwatch = chalk.bgHex(pair.bg)(' ');
1087
1040
  const grade = pair.aaa
1088
- ? chalk.hex('#50FA7B')('AAA')
1041
+ ? color.success('AAA')
1089
1042
  : pair.aa
1090
- ? chalk.hex('#50FA7B')('AA ')
1043
+ ? color.success('AA ')
1091
1044
  : pair.aaLarge
1092
- ? chalk.hex('#FFB86C')('AA-Large')
1093
- : chalk.hex('#FF5555')('fail');
1045
+ ? color.warning('AA-Large')
1046
+ : color.error('fail');
1094
1047
  const ratio = chalk.bold(`${pair.ratio}:1`);
1095
1048
  const stateTag = pair.state ? chalk.dim(` [${pair.state}]`) : '';
1096
1049
  console.log(chalk.dim(`│ ${branch}`) + ' ' + `${fgSwatch} ${bgSwatch} ${ratio} ${grade}${stateTag} ${chalk.dim(pair.fg + ' / ' + pair.bg)}`);
@@ -0,0 +1,38 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Terminal theme: semantic colors and canonical status icons.
5
+ *
6
+ * Colors use the ANSI 16-color palette (chalk named colors) instead of fixed
7
+ * truecolor hexes. Named colors are remapped by the user's terminal theme, so
8
+ * they stay legible on both light and dark backgrounds. Fixed pastels (the old
9
+ * Dracula hexes) wash out on light terminals.
10
+ *
11
+ * Each entry is a chalk function: color.success('done').
12
+ */
13
+ export const color = {
14
+ success: chalk.green, // completed steps, high confidence
15
+ warning: chalk.yellow, // recoverable issues, medium confidence
16
+ error: chalk.red, // failures
17
+ info: chalk.cyan, // links, file paths, context labels
18
+ accent: chalk.magenta, // summary line, special highlights
19
+ heading: chalk.bold, // section titles
20
+ muted: chalk.gray, // low confidence, secondary text
21
+ faint: chalk.dim, // tree lines, metadata
22
+ };
23
+
24
+ /**
25
+ * Canonical status icons. All single display-column on every terminal: the
26
+ * light check/x (U+2713/U+2717) and ASCII '!'/'i' avoid the emoji-presentation
27
+ * width-2 rendering that the heavy variants (✔ ✘) and circled glyphs (ⓘ) get
28
+ * in many terminals, which would skew left-edge alignment. Status markers in
29
+ * the output use these exact glyphs.
30
+ */
31
+ export const icon = {
32
+ success: '✓',
33
+ warning: '!',
34
+ error: '✗',
35
+ info: 'i',
36
+ arrow: '→',
37
+ bullet: '●',
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dembrandt",
3
- "version": "0.12.6",
3
+ "version": "0.12.8",
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",
@@ -17,7 +17,6 @@
17
17
  "scripts": {
18
18
  "start": "node index.js",
19
19
  "install-browser": "npx playwright install chromium firefox || echo 'Playwright browser installation failed. You may need to install system dependencies manually.'",
20
- "local-ui": "cd local-ui && npm start",
21
20
  "qa:baseline": "node test/qa.mjs --baseline",
22
21
  "qa:diff": "node test/qa.mjs --diff",
23
22
  "qa:site": "node test/qa.mjs --site"