dembrandt 0.12.6 → 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/index.js +29 -9
- package/lib/extractors/index.js +37 -36
- package/lib/formatters/terminal.js +59 -106
- package/lib/formatters/theme.js +38 -0
- package/package.json +1 -1
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
|
}
|
|
@@ -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('└─') + ' ' +
|
|
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}`) + ' ' + `${
|
|
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
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
familyData.
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
const totalContexts = data.contexts.size;
|
|
316
|
+
console.log(chalk.dim(`│ ${fontBranch}`) + ' ' + chalk.bold(family) + sizeList);
|
|
315
317
|
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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' ?
|
|
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' ?
|
|
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}`) + ' ' +
|
|
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}`) + ' ' +
|
|
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}`) + ' ' +
|
|
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}├─`) + ' ' +
|
|
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}└─`) + ' ' +
|
|
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}├─`) + ' ' +
|
|
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}└─`) + ' ' +
|
|
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' ?
|
|
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
|
-
?
|
|
1041
|
+
? color.success('AAA')
|
|
1089
1042
|
: pair.aa
|
|
1090
|
-
?
|
|
1043
|
+
? color.success('AA ')
|
|
1091
1044
|
: pair.aaLarge
|
|
1092
|
-
?
|
|
1093
|
-
:
|
|
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
|
+
};
|