dembrandt 0.6.0 → 0.7.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 +40 -1
- package/index.js +33 -2
- package/lib/colors.js +242 -0
- package/lib/display.js +74 -84
- package/lib/extractors.js +199 -11
- package/lib/pdf.js +970 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
Extract any website’s design system into design tokens in a few seconds: logo, colors, typography, borders, and more. One command.
|
|
8
8
|
|
|
9
|
-

|
|
10
|
+
|
|
11
|
+
**CLI output**
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**Local UI**
|
|
16
|
+
|
|
17
|
+

|
|
10
18
|
|
|
11
19
|
## Install
|
|
12
20
|
|
|
@@ -82,6 +90,37 @@ dembrandt stripe.com --dtcg
|
|
|
82
90
|
|
|
83
91
|
The DTCG format is an industry-standard JSON schema that can be consumed by design tools and token transformation libraries like [Style Dictionary](https://styledictionary.com).
|
|
84
92
|
|
|
93
|
+
## Local UI
|
|
94
|
+
|
|
95
|
+
Browse your extracted brands in a visual interface.
|
|
96
|
+
|
|
97
|
+
### Setup
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd local-ui
|
|
101
|
+
npm install
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Running
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm start
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Opens http://localhost:5173 with API on port 3002.
|
|
111
|
+
|
|
112
|
+
### Features
|
|
113
|
+
|
|
114
|
+
- Visual grid of all extracted brands
|
|
115
|
+
- Color palettes with click-to-copy
|
|
116
|
+
- Typography specimens
|
|
117
|
+
- Spacing, shadows, border radius visualization
|
|
118
|
+
- Button and link component previews
|
|
119
|
+
- Dark/light theme toggle
|
|
120
|
+
- Section nav links on extraction pages — jump directly to Colors, Typography, Shadows, etc. via a sticky sidebar
|
|
121
|
+
|
|
122
|
+
Extractions are performed via CLI (`dembrandt <url> --save-output`) and automatically appear in the UI.
|
|
123
|
+
|
|
85
124
|
## Use Cases
|
|
86
125
|
|
|
87
126
|
- Brand audits & competitive analysis
|
package/index.js
CHANGED
|
@@ -14,13 +14,14 @@ import { chromium, firefox } from "playwright-core";
|
|
|
14
14
|
import { extractBranding } from "./lib/extractors.js";
|
|
15
15
|
import { displayResults } from "./lib/display.js";
|
|
16
16
|
import { toW3CFormat } from "./lib/w3c-exporter.js";
|
|
17
|
+
import { generatePDF } from "./lib/pdf.js";
|
|
17
18
|
import { writeFileSync, mkdirSync } from "fs";
|
|
18
19
|
import { join } from "path";
|
|
19
20
|
|
|
20
21
|
program
|
|
21
22
|
.name("dembrandt")
|
|
22
23
|
.description("Extract design tokens from any website")
|
|
23
|
-
.version("0.
|
|
24
|
+
.version("0.7.0")
|
|
24
25
|
.argument("<url>")
|
|
25
26
|
.option("--browser <type>", "Browser to use (chromium|firefox)", "chromium")
|
|
26
27
|
.option("--json-only", "Output raw JSON")
|
|
@@ -29,6 +30,7 @@ program
|
|
|
29
30
|
.option("--dark-mode", "Extract colors from dark mode")
|
|
30
31
|
.option("--mobile", "Extract from mobile viewport")
|
|
31
32
|
.option("--slow", "3x longer timeouts for slow-loading sites")
|
|
33
|
+
.option("--brand-guide", "Export a brand guide PDF")
|
|
32
34
|
.option("--no-sandbox", "Disable browser sandbox (needed for Docker/CI)")
|
|
33
35
|
.action(async (input, opts) => {
|
|
34
36
|
let url = input;
|
|
@@ -99,7 +101,7 @@ program
|
|
|
99
101
|
const outputData = opts.dtcg ? toW3CFormat(result) : result;
|
|
100
102
|
|
|
101
103
|
// Save JSON output if --save-output or --dtcg is specified
|
|
102
|
-
if (
|
|
104
|
+
if (opts.saveOutput || opts.dtcg) {
|
|
103
105
|
try {
|
|
104
106
|
const domain = new URL(url).hostname.replace("www.", "");
|
|
105
107
|
const timestamp = new Date()
|
|
@@ -129,6 +131,35 @@ program
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
// Generate PDF brand guide
|
|
135
|
+
if (opts.brandGuide) {
|
|
136
|
+
try {
|
|
137
|
+
const pdfDomain = new URL(url).hostname.replace("www.", "");
|
|
138
|
+
const now = new Date();
|
|
139
|
+
const pdfDate = now.toISOString().slice(0, 10);
|
|
140
|
+
const pdfTime = `${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}`;
|
|
141
|
+
const pdfDir = join(process.cwd(), "output", pdfDomain);
|
|
142
|
+
mkdirSync(pdfDir, { recursive: true });
|
|
143
|
+
const pdfFilename = `${pdfDomain}-brand-guide-${pdfDate}-${pdfTime}.pdf`;
|
|
144
|
+
const pdfPath = join(pdfDir, pdfFilename);
|
|
145
|
+
spinner.start("Generating PDF brand guide...");
|
|
146
|
+
await generatePDF(result, pdfPath, browser);
|
|
147
|
+
spinner.stop();
|
|
148
|
+
console.log(
|
|
149
|
+
chalk.dim(
|
|
150
|
+
`PDF saved to: ${chalk.hex('#8BE9FD')(
|
|
151
|
+
`output/${pdfDomain}/${pdfFilename}`
|
|
152
|
+
)}`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
spinner.stop();
|
|
157
|
+
console.log(
|
|
158
|
+
chalk.hex('#FFB86C')(`Could not generate PDF: ${err.message}`)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
// Output to terminal
|
|
133
164
|
if (opts.jsonOnly) {
|
|
134
165
|
console.log(JSON.stringify(outputData, null, 2));
|
package/lib/colors.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Conversion Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts colors between RGB, LCH, and OKLCH color spaces.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert sRGB to linear RGB
|
|
9
|
+
*/
|
|
10
|
+
function srgbToLinear(c) {
|
|
11
|
+
c = c / 255;
|
|
12
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert linear RGB to XYZ (D65 illuminant)
|
|
17
|
+
*/
|
|
18
|
+
function linearRgbToXyz(r, g, b) {
|
|
19
|
+
return {
|
|
20
|
+
x: 0.4124564 * r + 0.3575761 * g + 0.1804375 * b,
|
|
21
|
+
y: 0.2126729 * r + 0.7151522 * g + 0.0721750 * b,
|
|
22
|
+
z: 0.0193339 * r + 0.1191920 * g + 0.9503041 * b
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert XYZ to Lab (D65 reference white)
|
|
28
|
+
*/
|
|
29
|
+
function xyzToLab(x, y, z) {
|
|
30
|
+
// D65 reference white
|
|
31
|
+
const xn = 0.95047;
|
|
32
|
+
const yn = 1.00000;
|
|
33
|
+
const zn = 1.08883;
|
|
34
|
+
|
|
35
|
+
const f = (t) => t > 0.008856 ? Math.cbrt(t) : (903.3 * t + 16) / 116;
|
|
36
|
+
|
|
37
|
+
const fx = f(x / xn);
|
|
38
|
+
const fy = f(y / yn);
|
|
39
|
+
const fz = f(z / zn);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
l: 116 * fy - 16,
|
|
43
|
+
a: 500 * (fx - fy),
|
|
44
|
+
b: 200 * (fy - fz)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert Lab to LCH
|
|
50
|
+
*/
|
|
51
|
+
function labToLch(l, a, b) {
|
|
52
|
+
const c = Math.sqrt(a * a + b * b);
|
|
53
|
+
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
54
|
+
if (h < 0) h += 360;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
l: l,
|
|
58
|
+
c: c,
|
|
59
|
+
h: h
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert RGB to LCH
|
|
65
|
+
* @param {number} r - Red (0-255)
|
|
66
|
+
* @param {number} g - Green (0-255)
|
|
67
|
+
* @param {number} b - Blue (0-255)
|
|
68
|
+
* @returns {{ l: number, c: number, h: number }}
|
|
69
|
+
*/
|
|
70
|
+
export function rgbToLch(r, g, b) {
|
|
71
|
+
const lr = srgbToLinear(r);
|
|
72
|
+
const lg = srgbToLinear(g);
|
|
73
|
+
const lb = srgbToLinear(b);
|
|
74
|
+
|
|
75
|
+
const xyz = linearRgbToXyz(lr, lg, lb);
|
|
76
|
+
const lab = xyzToLab(xyz.x, xyz.y, xyz.z);
|
|
77
|
+
return labToLch(lab.l, lab.a, lab.b);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert linear RGB to OKLab
|
|
82
|
+
* Uses the OKLab color space for perceptual uniformity
|
|
83
|
+
*/
|
|
84
|
+
function linearRgbToOklab(r, g, b) {
|
|
85
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
86
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
87
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
L: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
91
|
+
a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
92
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert OKLab to OKLCH
|
|
98
|
+
*/
|
|
99
|
+
function oklabToOklch(L, a, b) {
|
|
100
|
+
const c = Math.sqrt(a * a + b * b);
|
|
101
|
+
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
102
|
+
if (h < 0) h += 360;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
l: L,
|
|
106
|
+
c: c,
|
|
107
|
+
h: h
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert RGB to OKLCH
|
|
113
|
+
* @param {number} r - Red (0-255)
|
|
114
|
+
* @param {number} g - Green (0-255)
|
|
115
|
+
* @param {number} b - Blue (0-255)
|
|
116
|
+
* @returns {{ l: number, c: number, h: number }}
|
|
117
|
+
*/
|
|
118
|
+
export function rgbToOklch(r, g, b) {
|
|
119
|
+
const lr = srgbToLinear(r);
|
|
120
|
+
const lg = srgbToLinear(g);
|
|
121
|
+
const lb = srgbToLinear(b);
|
|
122
|
+
|
|
123
|
+
const oklab = linearRgbToOklab(lr, lg, lb);
|
|
124
|
+
return oklabToOklch(oklab.L, oklab.a, oklab.b);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format LCH values as CSS lch() string
|
|
129
|
+
* @param {{ l: number, c: number, h: number }} lch
|
|
130
|
+
* @param {number} [alpha] - Optional alpha value (0-1)
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function formatLch(lch, alpha) {
|
|
134
|
+
const l = Math.round(lch.l * 100) / 100;
|
|
135
|
+
const c = Math.round(lch.c * 100) / 100;
|
|
136
|
+
const h = Math.round(lch.h * 100) / 100;
|
|
137
|
+
|
|
138
|
+
if (alpha !== undefined && alpha < 1) {
|
|
139
|
+
return `lch(${l}% ${c} ${h} / ${alpha})`;
|
|
140
|
+
}
|
|
141
|
+
return `lch(${l}% ${c} ${h})`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format OKLCH values as CSS oklch() string
|
|
146
|
+
* @param {{ l: number, c: number, h: number }} oklch
|
|
147
|
+
* @param {number} [alpha] - Optional alpha value (0-1)
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function formatOklch(oklch, alpha) {
|
|
151
|
+
// OKLCH lightness is 0-1, displayed as percentage
|
|
152
|
+
const l = Math.round(oklch.l * 10000) / 100;
|
|
153
|
+
const c = Math.round(oklch.c * 1000) / 1000;
|
|
154
|
+
const h = Math.round(oklch.h * 100) / 100;
|
|
155
|
+
|
|
156
|
+
if (alpha !== undefined && alpha < 1) {
|
|
157
|
+
return `oklch(${l}% ${c} ${h} / ${alpha})`;
|
|
158
|
+
}
|
|
159
|
+
return `oklch(${l}% ${c} ${h})`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse a hex color string and return RGB values
|
|
164
|
+
* @param {string} hex - Hex color (#fff, #ffffff, #ffffffaa)
|
|
165
|
+
* @returns {{ r: number, g: number, b: number, a?: number } | null}
|
|
166
|
+
*/
|
|
167
|
+
export function hexToRgb(hex) {
|
|
168
|
+
if (!hex || !hex.startsWith('#')) return null;
|
|
169
|
+
|
|
170
|
+
// Remove #
|
|
171
|
+
hex = hex.slice(1);
|
|
172
|
+
|
|
173
|
+
// Handle 3-digit hex
|
|
174
|
+
if (hex.length === 3) {
|
|
175
|
+
return {
|
|
176
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
177
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
178
|
+
b: parseInt(hex[2] + hex[2], 16)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle 6-digit hex
|
|
183
|
+
if (hex.length === 6) {
|
|
184
|
+
return {
|
|
185
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
186
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
187
|
+
b: parseInt(hex.slice(4, 6), 16)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle 8-digit hex (with alpha)
|
|
192
|
+
if (hex.length === 8) {
|
|
193
|
+
return {
|
|
194
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
195
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
196
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
197
|
+
a: parseInt(hex.slice(6, 8), 16) / 255
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Convert any supported color format to all formats
|
|
206
|
+
* @param {string} colorString - Color in hex, rgb(), or rgba() format
|
|
207
|
+
* @returns {{ hex: string, rgb: string, lch: string, oklch: string, hasAlpha: boolean } | null}
|
|
208
|
+
*/
|
|
209
|
+
export function convertColor(colorString) {
|
|
210
|
+
let r, g, b, a;
|
|
211
|
+
|
|
212
|
+
// Parse rgba/rgb
|
|
213
|
+
const rgbaMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
214
|
+
if (rgbaMatch) {
|
|
215
|
+
r = parseInt(rgbaMatch[1]);
|
|
216
|
+
g = parseInt(rgbaMatch[2]);
|
|
217
|
+
b = parseInt(rgbaMatch[3]);
|
|
218
|
+
a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : undefined;
|
|
219
|
+
} else {
|
|
220
|
+
// Try hex
|
|
221
|
+
const rgb = hexToRgb(colorString);
|
|
222
|
+
if (!rgb) return null;
|
|
223
|
+
r = rgb.r;
|
|
224
|
+
g = rgb.g;
|
|
225
|
+
b = rgb.b;
|
|
226
|
+
a = rgb.a;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
230
|
+
const rgbStr = a !== undefined ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
|
|
231
|
+
|
|
232
|
+
const lchValues = rgbToLch(r, g, b);
|
|
233
|
+
const oklchValues = rgbToOklch(r, g, b);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
hex: hex.toLowerCase(),
|
|
237
|
+
rgb: rgbStr,
|
|
238
|
+
lch: formatLch(lchValues, a),
|
|
239
|
+
oklch: formatOklch(oklchValues, a),
|
|
240
|
+
hasAlpha: a !== undefined && a < 1
|
|
241
|
+
};
|
|
242
|
+
}
|
package/lib/display.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
+
import { convertColor } from './colors.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Creates a clickable terminal link using ANSI escape codes
|
|
@@ -94,65 +95,20 @@ function displayFavicons(favicons) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
function normalizeColorFormat(colorString) {
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
const g = parseInt(rgbaMatch[2]);
|
|
102
|
-
const b = parseInt(rgbaMatch[3]);
|
|
103
|
-
const a = rgbaMatch[4];
|
|
104
|
-
|
|
105
|
-
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
106
|
-
const rgb = a ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
|
|
107
|
-
|
|
108
|
-
return { hex, rgb, hasAlpha: !!a };
|
|
98
|
+
// Use the centralized color conversion utility
|
|
99
|
+
const converted = convertColor(colorString);
|
|
100
|
+
if (converted) {
|
|
101
|
+
return converted;
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
hex: `#${hexMatch3[1]}${hexMatch3[1]}${hexMatch3[2]}${hexMatch3[2]}${hexMatch3[3]}${hexMatch3[3]}`.toLowerCase(),
|
|
121
|
-
rgb: `rgb(${r}, ${g}, ${b})`,
|
|
122
|
-
hasAlpha: false
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Match 8-digit hex with alpha (#ffffff80, #00ff00ff, etc.)
|
|
127
|
-
const hexMatch8 = colorString.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
128
|
-
if (hexMatch8) {
|
|
129
|
-
const r = parseInt(hexMatch8[1], 16);
|
|
130
|
-
const g = parseInt(hexMatch8[2], 16);
|
|
131
|
-
const b = parseInt(hexMatch8[3], 16);
|
|
132
|
-
const a = (parseInt(hexMatch8[4], 16) / 255).toFixed(2);
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
hex: `#${hexMatch8[1]}${hexMatch8[2]}${hexMatch8[3]}`.toLowerCase(),
|
|
136
|
-
rgb: `rgba(${r}, ${g}, ${b}, ${a})`,
|
|
137
|
-
hasAlpha: true
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Match 6-digit hex (#ffffff, #f0a0b0, etc.)
|
|
142
|
-
const hexMatch6 = colorString.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
143
|
-
if (hexMatch6) {
|
|
144
|
-
const r = parseInt(hexMatch6[1], 16);
|
|
145
|
-
const g = parseInt(hexMatch6[2], 16);
|
|
146
|
-
const b = parseInt(hexMatch6[3], 16);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
hex: colorString.toLowerCase(),
|
|
150
|
-
rgb: `rgb(${r}, ${g}, ${b})`,
|
|
151
|
-
hasAlpha: false
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { hex: colorString, rgb: colorString, hasAlpha: false };
|
|
104
|
+
// Fallback for unparseable colors
|
|
105
|
+
return {
|
|
106
|
+
hex: colorString,
|
|
107
|
+
rgb: colorString,
|
|
108
|
+
lch: colorString,
|
|
109
|
+
oklch: colorString,
|
|
110
|
+
hasAlpha: false
|
|
111
|
+
};
|
|
156
112
|
}
|
|
157
113
|
|
|
158
114
|
function displayColors(colors) {
|
|
@@ -170,6 +126,8 @@ function displayColors(colors) {
|
|
|
170
126
|
allColors.push({
|
|
171
127
|
hex: formats.hex,
|
|
172
128
|
rgb: formats.rgb,
|
|
129
|
+
lch: formats.lch,
|
|
130
|
+
oklch: formats.oklch,
|
|
173
131
|
hasAlpha: formats.hasAlpha,
|
|
174
132
|
label: role,
|
|
175
133
|
type: 'semantic',
|
|
@@ -181,12 +139,18 @@ function displayColors(colors) {
|
|
|
181
139
|
// Add CSS variables
|
|
182
140
|
if (colors.cssVariables) {
|
|
183
141
|
const limit = 15;
|
|
184
|
-
Object.entries(colors.cssVariables).slice(0, limit).forEach(([name,
|
|
142
|
+
Object.entries(colors.cssVariables).slice(0, limit).forEach(([name, varData]) => {
|
|
185
143
|
try {
|
|
186
|
-
|
|
144
|
+
// Handle both old format (string) and new format (object with value, lch, oklch)
|
|
145
|
+
const colorValue = typeof varData === 'string' ? varData : varData.value;
|
|
146
|
+
const formats = normalizeColorFormat(colorValue);
|
|
147
|
+
|
|
148
|
+
// Use pre-computed LCH/OKLCH from extractor if available
|
|
187
149
|
allColors.push({
|
|
188
150
|
hex: formats.hex,
|
|
189
151
|
rgb: formats.rgb,
|
|
152
|
+
lch: (typeof varData === 'object' && varData.lch) || formats.lch,
|
|
153
|
+
oklch: (typeof varData === 'object' && varData.oklch) || formats.oklch,
|
|
190
154
|
hasAlpha: formats.hasAlpha,
|
|
191
155
|
label: name,
|
|
192
156
|
type: 'variable',
|
|
@@ -208,6 +172,8 @@ function displayColors(colors) {
|
|
|
208
172
|
allColors.push({
|
|
209
173
|
hex: formats.hex,
|
|
210
174
|
rgb: formats.rgb,
|
|
175
|
+
lch: c.lch || formats.lch,
|
|
176
|
+
oklch: c.oklch || formats.oklch,
|
|
211
177
|
hasAlpha: formats.hasAlpha,
|
|
212
178
|
label: '',
|
|
213
179
|
type: 'palette',
|
|
@@ -244,10 +210,11 @@ function displayColors(colors) {
|
|
|
244
210
|
|
|
245
211
|
const uniqueColors = Array.from(colorMap.values());
|
|
246
212
|
|
|
247
|
-
// Display all colors with
|
|
248
|
-
uniqueColors.forEach(({ hex, rgb, label, confidence }, index) => {
|
|
213
|
+
// Display all colors with hex, RGB, LCH, and OKLCH formats
|
|
214
|
+
uniqueColors.forEach(({ hex, rgb, lch, oklch, label, confidence }, index) => {
|
|
249
215
|
const isLast = index === uniqueColors.length - 1;
|
|
250
216
|
const branch = isLast ? '└─' : '├─';
|
|
217
|
+
const indent = isLast ? ' ' : '│ ';
|
|
251
218
|
|
|
252
219
|
try {
|
|
253
220
|
const colorBlock = chalk.bgHex(hex)(' ');
|
|
@@ -256,12 +223,19 @@ function displayColors(colors) {
|
|
|
256
223
|
else if (confidence === 'medium') conf = chalk.hex('#FFB86C')('●');
|
|
257
224
|
else conf = chalk.gray('●'); // low confidence
|
|
258
225
|
|
|
259
|
-
const labelText = label ? chalk.dim(label) : '';
|
|
226
|
+
const labelText = label ? chalk.dim(` ${label}`) : '';
|
|
260
227
|
|
|
261
|
-
//
|
|
262
|
-
console.log(chalk.dim(`│ ${branch}`) + ' ' + `${conf} ${colorBlock} ${hex
|
|
228
|
+
// First line: color swatch, hex, and label
|
|
229
|
+
console.log(chalk.dim(`│ ${branch}`) + ' ' + `${conf} ${colorBlock} ${hex}${labelText}`);
|
|
230
|
+
// Second line: RGB and LCH
|
|
231
|
+
console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('rgb: ') + rgb);
|
|
232
|
+
console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('lch: ') + lch);
|
|
233
|
+
console.log(chalk.dim(`│ ${indent}└─`) + ' ' + chalk.dim('oklch: ') + oklch);
|
|
263
234
|
} catch {
|
|
264
|
-
console.log(chalk.dim(`│ ${branch}`) + ' ' + `${hex
|
|
235
|
+
console.log(chalk.dim(`│ ${branch}`) + ' ' + `${hex} ${label ? chalk.dim(label) : ''}`);
|
|
236
|
+
console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('rgb: ') + rgb);
|
|
237
|
+
console.log(chalk.dim(`│ ${indent}├─`) + ' ' + chalk.dim('lch: ') + lch);
|
|
238
|
+
console.log(chalk.dim(`│ ${indent}└─`) + ' ' + chalk.dim('oklch: ') + oklch);
|
|
265
239
|
}
|
|
266
240
|
});
|
|
267
241
|
|
|
@@ -341,33 +315,49 @@ function displayTypography(typography) {
|
|
|
341
315
|
console.log(chalk.dim(`│ ${indent}${contextBranch}`) + ' ' + chalk.hex('#8BE9FD')(context));
|
|
342
316
|
|
|
343
317
|
styles.forEach((style, styleIndex) => {
|
|
344
|
-
const
|
|
318
|
+
const isStyleLast = styleIndex === styles.length - 1;
|
|
319
|
+
const styleIndent = isFontLast ? ' ' : '│ ';
|
|
320
|
+
const contextIndent = isContextLast ? ' ' : '│ ';
|
|
321
|
+
const styleBranch = isStyleLast ? '└─' : '├─';
|
|
322
|
+
const propIndent = isStyleLast ? ' ' : '│ ';
|
|
323
|
+
|
|
324
|
+
// Main size line
|
|
325
|
+
console.log(chalk.dim(`│ ${styleIndent}${contextIndent}${styleBranch}`) + ' ' + `${style.size}`);
|
|
345
326
|
|
|
327
|
+
// Collect properties
|
|
328
|
+
const props = [];
|
|
346
329
|
if (style.weight && style.weight !== 400) {
|
|
347
|
-
|
|
330
|
+
props.push({ key: 'weight', value: style.weight });
|
|
348
331
|
}
|
|
349
|
-
|
|
350
332
|
if (style.lineHeight) {
|
|
351
333
|
const lh = parseFloat(style.lineHeight);
|
|
352
334
|
let lhLabel = '';
|
|
353
|
-
if (lh <= 1.3) lhLabel = 'tight';
|
|
354
|
-
else if (lh >= 1.6) lhLabel = 'relaxed';
|
|
355
|
-
|
|
335
|
+
if (lh <= 1.3) lhLabel = ' (tight)';
|
|
336
|
+
else if (lh >= 1.6) lhLabel = ' (relaxed)';
|
|
337
|
+
props.push({ key: 'line-height', value: `${style.lineHeight}${lhLabel}` });
|
|
338
|
+
}
|
|
339
|
+
if (style.transform) {
|
|
340
|
+
props.push({ key: 'transform', value: style.transform });
|
|
341
|
+
}
|
|
342
|
+
if (style.spacing) {
|
|
343
|
+
props.push({ key: 'letter-spacing', value: style.spacing });
|
|
344
|
+
}
|
|
345
|
+
if (style.isFluid) {
|
|
346
|
+
props.push({ key: 'fluid', value: 'yes' });
|
|
347
|
+
}
|
|
348
|
+
if (style.fontFeatures) {
|
|
349
|
+
props.push({ key: 'features', value: style.fontFeatures });
|
|
356
350
|
}
|
|
357
351
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const contextIndent = isContextLast ? ' ' : '│ ';
|
|
368
|
-
const styleBranch = isStyleLast ? '└─' : '├─';
|
|
369
|
-
|
|
370
|
-
console.log(chalk.dim(`│ ${styleIndent}${contextIndent}${styleBranch}`) + ' ' + `${style.size}${modifierStr}`);
|
|
352
|
+
// Display properties
|
|
353
|
+
props.forEach((prop, propIndex) => {
|
|
354
|
+
const isLastProp = propIndex === props.length - 1;
|
|
355
|
+
const propBranch = isLastProp ? '└─' : '├─';
|
|
356
|
+
console.log(
|
|
357
|
+
chalk.dim(`│ ${styleIndent}${contextIndent}${propIndent}${propBranch}`) + ' ' +
|
|
358
|
+
chalk.dim(`${prop.key}: `) + `${prop.value}`
|
|
359
|
+
);
|
|
360
|
+
});
|
|
371
361
|
});
|
|
372
362
|
}
|
|
373
363
|
}
|