designlang 5.0.0 → 6.0.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 +66 -5
- package/bin/design-extract.js +215 -91
- package/package.json +9 -4
- package/src/config.js +36 -0
- package/src/crawler.js +181 -95
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/components.js +77 -1
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/variables.js +20 -1
- package/src/formatters/figma.js +66 -47
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +84 -0
- package/src/history.js +8 -1
- package/src/index.js +46 -20
- package/src/utils.js +68 -0
- package/tests/cli.test.js +34 -0
- package/tests/extractors.test.js +661 -0
- package/tests/formatters.test.js +477 -0
- package/tests/utils.test.js +413 -0
package/src/formatters/theme.js
CHANGED
|
@@ -46,15 +46,149 @@ export function formatReactTheme(design) {
|
|
|
46
46
|
theme.shadows[s.label] = s.raw;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Component variant tokens for interactive states
|
|
50
|
+
theme.states = {
|
|
51
|
+
hover: { opacity: 0.08 },
|
|
52
|
+
focus: { opacity: 0.12 },
|
|
53
|
+
active: { opacity: 0.16 },
|
|
54
|
+
disabled: { opacity: 0.38 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Build MUI v5 theme
|
|
58
|
+
const muiTheme = buildMuiTheme(design);
|
|
59
|
+
|
|
60
|
+
// Build TypeScript type definition comment
|
|
61
|
+
const tsType = buildTypeComment(theme);
|
|
62
|
+
|
|
49
63
|
return `// React Theme — extracted from ${design.meta.url}
|
|
50
64
|
// Compatible with: Chakra UI, Stitches, Vanilla Extract, or any CSS-in-JS
|
|
51
65
|
|
|
66
|
+
${tsType}
|
|
67
|
+
|
|
52
68
|
export const theme = ${JSON.stringify(theme, null, 2)};
|
|
53
69
|
|
|
70
|
+
// MUI v5 theme
|
|
71
|
+
export const muiTheme = ${JSON.stringify(muiTheme, null, 2)};
|
|
72
|
+
|
|
54
73
|
export default theme;
|
|
55
74
|
`;
|
|
56
75
|
}
|
|
57
76
|
|
|
77
|
+
function buildTypeComment(theme) {
|
|
78
|
+
const colorKeys = Object.keys(theme.colors || {}).map(k => ` ${k}: string;`).join('\n');
|
|
79
|
+
const fontKeys = Object.keys(theme.fonts || {}).map(k => ` ${k}: string;`).join('\n');
|
|
80
|
+
const sizeKeys = Object.keys(theme.fontSizes || {}).map(k => ` '${k}': string;`).join('\n');
|
|
81
|
+
const spaceKeys = Object.keys(theme.space || {}).map(k => ` '${k}': string;`).join('\n');
|
|
82
|
+
const radiiKeys = Object.keys(theme.radii || {}).map(k => ` ${k}: string;`).join('\n');
|
|
83
|
+
const shadowKeys = Object.keys(theme.shadows || {}).map(k => ` ${k}: string;`).join('\n');
|
|
84
|
+
|
|
85
|
+
return `/**
|
|
86
|
+
* TypeScript type definition for this theme:
|
|
87
|
+
*
|
|
88
|
+
* interface Theme {
|
|
89
|
+
* colors: {
|
|
90
|
+
${colorKeys}
|
|
91
|
+
* };
|
|
92
|
+
* fonts: {
|
|
93
|
+
${fontKeys}
|
|
94
|
+
* };
|
|
95
|
+
* fontSizes: {
|
|
96
|
+
${sizeKeys}
|
|
97
|
+
* };
|
|
98
|
+
* space: {
|
|
99
|
+
${spaceKeys}
|
|
100
|
+
* };
|
|
101
|
+
* radii: {
|
|
102
|
+
${radiiKeys}
|
|
103
|
+
* };
|
|
104
|
+
* shadows: {
|
|
105
|
+
${shadowKeys}
|
|
106
|
+
* };
|
|
107
|
+
* states: {
|
|
108
|
+
* hover: { opacity: number };
|
|
109
|
+
* focus: { opacity: number };
|
|
110
|
+
* active: { opacity: number };
|
|
111
|
+
* disabled: { opacity: number };
|
|
112
|
+
* };
|
|
113
|
+
* }
|
|
114
|
+
*/`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildMuiTheme(design) {
|
|
118
|
+
const { colors, typography, borders, shadows } = design;
|
|
119
|
+
const mui = { palette: {}, typography: {}, shape: {}, shadows: [] };
|
|
120
|
+
|
|
121
|
+
// Palette
|
|
122
|
+
if (colors.primary) {
|
|
123
|
+
mui.palette.primary = { main: colors.primary.hex };
|
|
124
|
+
const pHsl = toHslParts(colors.primary.hex);
|
|
125
|
+
if (pHsl) {
|
|
126
|
+
mui.palette.primary.light = `hsl(${pHsl.h}, ${pHsl.s}%, ${Math.min(pHsl.l + 15, 95)}%)`;
|
|
127
|
+
mui.palette.primary.dark = `hsl(${pHsl.h}, ${pHsl.s}%, ${Math.max(pHsl.l - 15, 10)}%)`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (colors.secondary) {
|
|
131
|
+
mui.palette.secondary = { main: colors.secondary.hex };
|
|
132
|
+
const sHsl = toHslParts(colors.secondary.hex);
|
|
133
|
+
if (sHsl) {
|
|
134
|
+
mui.palette.secondary.light = `hsl(${sHsl.h}, ${sHsl.s}%, ${Math.min(sHsl.l + 15, 95)}%)`;
|
|
135
|
+
mui.palette.secondary.dark = `hsl(${sHsl.h}, ${sHsl.s}%, ${Math.max(sHsl.l - 15, 10)}%)`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
mui.palette.background = {};
|
|
139
|
+
if (colors.backgrounds.length > 0) mui.palette.background.default = colors.backgrounds[0];
|
|
140
|
+
if (colors.backgrounds.length > 1) mui.palette.background.paper = colors.backgrounds[1];
|
|
141
|
+
else if (colors.backgrounds.length > 0) mui.palette.background.paper = colors.backgrounds[0];
|
|
142
|
+
mui.palette.text = {};
|
|
143
|
+
if (colors.text.length > 0) mui.palette.text.primary = colors.text[0];
|
|
144
|
+
if (colors.text.length > 1) mui.palette.text.secondary = colors.text[1];
|
|
145
|
+
|
|
146
|
+
// Typography
|
|
147
|
+
const bodyFont = typography.families.find(f => f.usage === 'body');
|
|
148
|
+
const headingFont = typography.families.find(f => f.usage === 'headings');
|
|
149
|
+
mui.typography.fontFamily = bodyFont ? `'${bodyFont.name}', sans-serif` : undefined;
|
|
150
|
+
for (const s of typography.scale.slice(0, 6)) {
|
|
151
|
+
const level = s.size >= 32 ? 'h1' : s.size >= 24 ? 'h2' : s.size >= 20 ? 'h3' : s.size >= 16 ? 'body1' : 'body2';
|
|
152
|
+
mui.typography[level] = {
|
|
153
|
+
fontSize: `${s.size}px`,
|
|
154
|
+
fontWeight: s.weight || 400,
|
|
155
|
+
lineHeight: s.lineHeight || 1.5,
|
|
156
|
+
};
|
|
157
|
+
if (headingFont && level.startsWith('h')) {
|
|
158
|
+
mui.typography[level].fontFamily = `'${headingFont.name}', sans-serif`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Shape
|
|
163
|
+
if (borders.radii.length > 0) {
|
|
164
|
+
const md = borders.radii.find(r => r.label === 'md') || borders.radii[0];
|
|
165
|
+
mui.shape.borderRadius = md.value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Shadows (first few)
|
|
169
|
+
mui.shadows = shadows.values.slice(0, 5).map(s => s.raw);
|
|
170
|
+
|
|
171
|
+
return mui;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toHslParts(hex) {
|
|
175
|
+
if (!hex) return null;
|
|
176
|
+
const h = hex.replace('#', '');
|
|
177
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
178
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
179
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
180
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
181
|
+
const l = (max + min) / 2;
|
|
182
|
+
if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
183
|
+
const d = max - min;
|
|
184
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
185
|
+
let hue;
|
|
186
|
+
if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
187
|
+
else if (max === g) hue = ((b - r) / d + 2) / 6;
|
|
188
|
+
else hue = ((r - g) / d + 4) / 6;
|
|
189
|
+
return { h: Math.round(hue * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
190
|
+
}
|
|
191
|
+
|
|
58
192
|
export function formatShadcnTheme(design) {
|
|
59
193
|
const { colors, borders } = design;
|
|
60
194
|
const lines = ['@layer base {', ' :root {'];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { rgbToHsl } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
export function formatVueTheme(design) {
|
|
4
|
+
const { colors, typography, spacing, borders, shadows } = design;
|
|
5
|
+
const lines = [];
|
|
6
|
+
|
|
7
|
+
lines.push('// Vuetify 3 theme configuration');
|
|
8
|
+
lines.push('// Generated by designlang');
|
|
9
|
+
lines.push('');
|
|
10
|
+
lines.push('export const designlangTheme = {');
|
|
11
|
+
lines.push(' dark: false,');
|
|
12
|
+
lines.push(' colors: {');
|
|
13
|
+
if (colors.primary) lines.push(` primary: '${colors.primary.hex}',`);
|
|
14
|
+
if (colors.secondary) lines.push(` secondary: '${colors.secondary.hex}',`);
|
|
15
|
+
if (colors.accent) lines.push(` accent: '${colors.accent.hex}',`);
|
|
16
|
+
if (colors.backgrounds.length > 0) lines.push(` background: '${colors.backgrounds[0]}',`);
|
|
17
|
+
if (colors.text.length > 0) lines.push(` 'on-background': '${colors.text[0]}',`);
|
|
18
|
+
lines.push(' },');
|
|
19
|
+
lines.push('};');
|
|
20
|
+
lines.push('');
|
|
21
|
+
|
|
22
|
+
// CSS variables for non-Vuetify usage
|
|
23
|
+
lines.push('// CSS custom properties');
|
|
24
|
+
lines.push('export const cssVariables = `');
|
|
25
|
+
lines.push(':root {');
|
|
26
|
+
if (colors.primary) lines.push(` --dl-color-primary: ${colors.primary.hex};`);
|
|
27
|
+
if (colors.secondary) lines.push(` --dl-color-secondary: ${colors.secondary.hex};`);
|
|
28
|
+
for (const [i, n] of colors.neutrals.slice(0, 8).entries()) {
|
|
29
|
+
lines.push(` --dl-color-neutral-${i + 1}: ${n.hex};`);
|
|
30
|
+
}
|
|
31
|
+
for (const [i, val] of spacing.scale.slice(0, 12).entries()) {
|
|
32
|
+
lines.push(` --dl-spacing-${i + 1}: ${val}px;`);
|
|
33
|
+
}
|
|
34
|
+
for (const r of borders.radii) {
|
|
35
|
+
lines.push(` --dl-radius-${r.label}: ${r.value}px;`);
|
|
36
|
+
}
|
|
37
|
+
if (typography.families.length > 0) {
|
|
38
|
+
lines.push(` --dl-font-primary: '${typography.families[0].name}', sans-serif;`);
|
|
39
|
+
}
|
|
40
|
+
lines.push('}');
|
|
41
|
+
lines.push('`;');
|
|
42
|
+
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function formatWordPress(design) {
|
|
2
|
+
const { colors, typography, spacing } = design;
|
|
3
|
+
|
|
4
|
+
const theme = {
|
|
5
|
+
$schema: "https://schemas.wp.org/trunk/theme.json",
|
|
6
|
+
version: 3,
|
|
7
|
+
settings: {
|
|
8
|
+
color: {
|
|
9
|
+
palette: [],
|
|
10
|
+
gradients: [],
|
|
11
|
+
},
|
|
12
|
+
typography: {
|
|
13
|
+
fontFamilies: [],
|
|
14
|
+
fontSizes: [],
|
|
15
|
+
},
|
|
16
|
+
spacing: {
|
|
17
|
+
spacingSizes: [],
|
|
18
|
+
},
|
|
19
|
+
layout: {
|
|
20
|
+
contentSize: "1200px",
|
|
21
|
+
wideSize: "1400px",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
styles: {
|
|
25
|
+
color: {},
|
|
26
|
+
typography: {},
|
|
27
|
+
spacing: {},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Colors
|
|
32
|
+
if (colors.primary) theme.settings.color.palette.push({ slug: 'primary', color: colors.primary.hex, name: 'Primary' });
|
|
33
|
+
if (colors.secondary) theme.settings.color.palette.push({ slug: 'secondary', color: colors.secondary.hex, name: 'Secondary' });
|
|
34
|
+
if (colors.accent) theme.settings.color.palette.push({ slug: 'accent', color: colors.accent.hex, name: 'Accent' });
|
|
35
|
+
for (let i = 0; i < Math.min(colors.neutrals.length, 5); i++) {
|
|
36
|
+
theme.settings.color.palette.push({ slug: `neutral-${i + 1}`, color: colors.neutrals[i].hex, name: `Neutral ${i + 1}` });
|
|
37
|
+
}
|
|
38
|
+
for (const bg of colors.backgrounds.slice(0, 3)) {
|
|
39
|
+
theme.settings.color.palette.push({ slug: `bg-${bg.replace('#', '')}`, color: bg, name: `Background ${bg}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Typography
|
|
43
|
+
for (const fam of typography.families) {
|
|
44
|
+
theme.settings.typography.fontFamilies.push({ fontFamily: fam.name, slug: fam.name.toLowerCase().replace(/\s+/g, '-'), name: fam.name });
|
|
45
|
+
}
|
|
46
|
+
for (const s of typography.scale.slice(0, 8)) {
|
|
47
|
+
theme.settings.typography.fontSizes.push({ size: `${s.size}px`, slug: `size-${s.size}`, name: `${s.size}px` });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Spacing
|
|
51
|
+
for (let i = 0; i < Math.min(spacing.scale.length, 8); i++) {
|
|
52
|
+
const val = spacing.scale[i];
|
|
53
|
+
theme.settings.spacing.spacingSizes.push({ size: `${val}px`, slug: `spacing-${val}`, name: `${val}px` });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Layout from extracted containers
|
|
57
|
+
if (design.layout && design.layout.containerWidths.length > 0) {
|
|
58
|
+
theme.settings.layout.contentSize = design.layout.containerWidths[0].maxWidth;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Body styles
|
|
62
|
+
if (typography.body) {
|
|
63
|
+
theme.styles.typography.fontSize = `${typography.body.size}px`;
|
|
64
|
+
theme.styles.typography.lineHeight = typography.body.lineHeight;
|
|
65
|
+
}
|
|
66
|
+
if (typography.families.length > 0) {
|
|
67
|
+
theme.styles.typography.fontFamily = typography.families[0].name;
|
|
68
|
+
}
|
|
69
|
+
if (colors.backgrounds.length > 0) {
|
|
70
|
+
theme.styles.color.background = colors.backgrounds[0];
|
|
71
|
+
}
|
|
72
|
+
if (colors.text.length > 0) {
|
|
73
|
+
theme.styles.color.text = colors.text[0];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Gradients from design
|
|
77
|
+
if (design.gradients && design.gradients.gradients) {
|
|
78
|
+
for (const g of design.gradients.gradients.slice(0, 5)) {
|
|
79
|
+
theme.settings.color.gradients.push({ slug: `gradient-${theme.settings.color.gradients.length + 1}`, gradient: g.raw, name: `Gradient ${theme.settings.color.gradients.length + 1}` });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return JSON.stringify(theme, null, 2);
|
|
84
|
+
}
|
package/src/history.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
5
6
|
|
|
6
|
-
const HISTORY_DIR = join(
|
|
7
|
+
const HISTORY_DIR = join(homedir(), '.designlang');
|
|
7
8
|
|
|
8
9
|
function ensureDir() {
|
|
9
10
|
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
@@ -50,6 +51,12 @@ export function saveSnapshot(design) {
|
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
history.push(snapshot);
|
|
54
|
+
|
|
55
|
+
// Prune oldest entries if history exceeds 50 snapshots
|
|
56
|
+
if (history.length > 50) {
|
|
57
|
+
history = history.slice(history.length - 50);
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
writeFileSync(file, JSON.stringify(history, null, 2), 'utf-8');
|
|
54
61
|
return { hostname, snapshotCount: history.length, file };
|
|
55
62
|
}
|
package/src/index.js
CHANGED
|
@@ -17,9 +17,17 @@ import { extractIcons } from './extractors/icons.js';
|
|
|
17
17
|
import { extractFonts } from './extractors/fonts.js';
|
|
18
18
|
import { extractImageStyles } from './extractors/images.js';
|
|
19
19
|
|
|
20
|
+
function safeExtract(fn, ...args) {
|
|
21
|
+
try { return fn(...args); } catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
export async function extractDesignLanguage(url, options = {}) {
|
|
21
|
-
const rawData = await crawlPage(url,
|
|
25
|
+
const rawData = await crawlPage(url, {
|
|
26
|
+
...options,
|
|
27
|
+
ignore: options.ignore,
|
|
28
|
+
});
|
|
22
29
|
const styles = rawData.light.computedStyles;
|
|
30
|
+
const warnings = [];
|
|
23
31
|
|
|
24
32
|
const design = {
|
|
25
33
|
meta: {
|
|
@@ -29,34 +37,48 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
29
37
|
elementCount: styles.length,
|
|
30
38
|
pagesAnalyzed: rawData.pagesAnalyzed || 1,
|
|
31
39
|
},
|
|
32
|
-
colors: extractColors
|
|
33
|
-
typography: extractTypography
|
|
34
|
-
spacing: extractSpacing
|
|
35
|
-
shadows: extractShadows
|
|
36
|
-
borders: extractBorders
|
|
37
|
-
variables: extractVariables
|
|
38
|
-
breakpoints: extractBreakpoints
|
|
39
|
-
animations: extractAnimations
|
|
40
|
-
components: extractComponents
|
|
41
|
-
accessibility: extractAccessibility
|
|
42
|
-
layout: extractLayout
|
|
43
|
-
gradients: extractGradients
|
|
44
|
-
zIndex: extractZIndex
|
|
45
|
-
icons: rawData.light.icons ? extractIcons
|
|
46
|
-
fonts: rawData.light.fontData ? extractFonts
|
|
47
|
-
images: rawData.light.images ? extractImageStyles
|
|
40
|
+
colors: safeExtract(extractColors, styles) || { primary: null, secondary: null, accent: null, neutrals: [], backgrounds: [], text: [], gradients: [], all: [] },
|
|
41
|
+
typography: safeExtract(extractTypography, styles) || { families: [], scale: [] },
|
|
42
|
+
spacing: safeExtract(extractSpacing, styles) || { scale: [], base: null },
|
|
43
|
+
shadows: safeExtract(extractShadows, styles) || { values: [] },
|
|
44
|
+
borders: safeExtract(extractBorders, styles) || { radii: [] },
|
|
45
|
+
variables: safeExtract(extractVariables, rawData.light.cssVariables) || {},
|
|
46
|
+
breakpoints: safeExtract(extractBreakpoints, rawData.light.mediaQueries) || [],
|
|
47
|
+
animations: safeExtract(extractAnimations, styles, rawData.light.keyframes) || { transitions: [], keyframes: [] },
|
|
48
|
+
components: safeExtract(extractComponents, styles) || {},
|
|
49
|
+
accessibility: safeExtract(extractAccessibility, styles) || { score: 0, failCount: 0 },
|
|
50
|
+
layout: safeExtract(extractLayout, styles) || { gridCount: 0, flexCount: 0 },
|
|
51
|
+
gradients: safeExtract(extractGradients, styles) || { count: 0 },
|
|
52
|
+
zIndex: safeExtract(extractZIndex, styles) || { allValues: [], issues: [] },
|
|
53
|
+
icons: rawData.light.icons ? (safeExtract(extractIcons, rawData.light.icons) || { icons: [], count: 0 }) : { icons: [], count: 0 },
|
|
54
|
+
fonts: rawData.light.fontData ? (safeExtract(extractFonts, rawData.light.fontData) || { fonts: [], systemFonts: [] }) : { fonts: [], systemFonts: [] },
|
|
55
|
+
images: rawData.light.images ? (safeExtract(extractImageStyles, rawData.light.images) || { patterns: [], aspectRatios: [] }) : { patterns: [], aspectRatios: [] },
|
|
48
56
|
componentScreenshots: rawData.componentScreenshots || {},
|
|
49
57
|
score: null,
|
|
50
58
|
};
|
|
51
59
|
|
|
60
|
+
// Track which extractors failed
|
|
61
|
+
const extractorChecks = [
|
|
62
|
+
['colors', design.colors], ['typography', design.typography], ['spacing', design.spacing],
|
|
63
|
+
['shadows', design.shadows], ['borders', design.borders], ['variables', design.variables],
|
|
64
|
+
['breakpoints', design.breakpoints], ['animations', design.animations], ['components', design.components],
|
|
65
|
+
['accessibility', design.accessibility], ['layout', design.layout], ['gradients', design.gradients],
|
|
66
|
+
['zIndex', design.zIndex],
|
|
67
|
+
];
|
|
68
|
+
for (const [name, result] of extractorChecks) {
|
|
69
|
+
if (result === null) warnings.push(`${name} extractor failed`);
|
|
70
|
+
}
|
|
71
|
+
design.warnings = warnings;
|
|
72
|
+
|
|
52
73
|
if (rawData.dark) {
|
|
53
74
|
design.darkMode = {
|
|
54
|
-
colors: extractColors
|
|
55
|
-
variables: extractVariables
|
|
75
|
+
colors: safeExtract(extractColors, rawData.dark.computedStyles) || { primary: null, secondary: null, accent: null, neutrals: [], backgrounds: [], text: [], gradients: [], all: [] },
|
|
76
|
+
variables: safeExtract(extractVariables, rawData.dark.cssVariables) || {},
|
|
56
77
|
};
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
design.score = scoreDesignSystem
|
|
80
|
+
design.score = safeExtract(scoreDesignSystem, design);
|
|
81
|
+
if (design.score === null) warnings.push('scoring failed');
|
|
60
82
|
|
|
61
83
|
return design;
|
|
62
84
|
}
|
|
@@ -69,6 +91,9 @@ export { formatCssVars } from './formatters/css-vars.js';
|
|
|
69
91
|
export { formatPreview } from './formatters/preview.js';
|
|
70
92
|
export { formatFigma } from './formatters/figma.js';
|
|
71
93
|
export { formatReactTheme, formatShadcnTheme } from './formatters/theme.js';
|
|
94
|
+
export { formatWordPress } from './formatters/wordpress.js';
|
|
95
|
+
export { formatVueTheme } from './formatters/vue-theme.js';
|
|
96
|
+
export { formatSvelteTheme } from './formatters/svelte-theme.js';
|
|
72
97
|
export { diffDesigns, formatDiffMarkdown, formatDiffHtml } from './diff.js';
|
|
73
98
|
export { saveSnapshot, getHistory, formatHistoryMarkdown } from './history.js';
|
|
74
99
|
export { captureResponsive } from './extractors/responsive.js';
|
|
@@ -80,3 +105,4 @@ export { scoreDesignSystem } from './extractors/scoring.js';
|
|
|
80
105
|
export { watchSite } from './watch.js';
|
|
81
106
|
export { diffDarkMode } from './darkdiff.js';
|
|
82
107
|
export { applyDesign } from './apply.js';
|
|
108
|
+
export { loadConfig, mergeConfig } from './config.js';
|
package/src/utils.js
CHANGED
|
@@ -20,6 +20,29 @@ const NAMED_COLORS = {
|
|
|
20
20
|
maroon: { r: 128, g: 0, b: 0, a: 1 },
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
function oklabToRgb(L, a, b) {
|
|
24
|
+
// OKLab -> linear sRGB
|
|
25
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
26
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
27
|
+
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
28
|
+
const l = l_ * l_ * l_;
|
|
29
|
+
const m = m_ * m_ * m_;
|
|
30
|
+
const s = s_ * s_ * s_;
|
|
31
|
+
let r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
32
|
+
let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
33
|
+
let bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
|
|
34
|
+
// Clamp to [0,255]
|
|
35
|
+
const clamp = v => Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
36
|
+
return { r: clamp(r), g: clamp(g), b: clamp(bl) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function oklchToRgb(L, C, H) {
|
|
40
|
+
const hRad = H * Math.PI / 180;
|
|
41
|
+
const a = C * Math.cos(hRad);
|
|
42
|
+
const b = C * Math.sin(hRad);
|
|
43
|
+
return oklabToRgb(L, a, b);
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function parseColor(str) {
|
|
24
47
|
if (!str || str === 'none' || str === 'currentcolor' || str === 'inherit' || str === 'initial') return null;
|
|
25
48
|
str = str.trim().toLowerCase();
|
|
@@ -58,6 +81,51 @@ export function parseColor(str) {
|
|
|
58
81
|
return { ...rgb, a: hslMatch[4] !== undefined ? +hslMatch[4] : 1 };
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
// hsl modern: hsl(210 50% 40%) or hsl(210 50% 40% / 0.5)
|
|
85
|
+
const hslModern = str.match(/hsla?\(\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%\s*(?:\/\s*([\d.]+%?))?\s*\)/);
|
|
86
|
+
if (hslModern) {
|
|
87
|
+
const rgb = hslToRgb(+hslModern[1], +hslModern[2], +hslModern[3]);
|
|
88
|
+
let a = 1;
|
|
89
|
+
if (hslModern[4] !== undefined) {
|
|
90
|
+
a = hslModern[4].endsWith('%') ? parseFloat(hslModern[4]) / 100 : +hslModern[4];
|
|
91
|
+
}
|
|
92
|
+
return { ...rgb, a };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// oklch(L C H) or oklch(L C H / a)
|
|
96
|
+
const oklchMatch = str.match(/oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+%?))?\s*\)/);
|
|
97
|
+
if (oklchMatch) {
|
|
98
|
+
const rgb = oklchToRgb(+oklchMatch[1], +oklchMatch[2], +oklchMatch[3]);
|
|
99
|
+
let a = 1;
|
|
100
|
+
if (oklchMatch[4] !== undefined) {
|
|
101
|
+
a = oklchMatch[4].endsWith('%') ? parseFloat(oklchMatch[4]) / 100 : +oklchMatch[4];
|
|
102
|
+
}
|
|
103
|
+
return { ...rgb, a };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// oklab(L a b) or oklab(L a b / alpha)
|
|
107
|
+
const oklabMatch = str.match(/oklab\(\s*([\d.e+-]+)\s+([\d.e+-]+)\s+([\d.e+-]+)\s*(?:\/\s*([\d.]+%?))?\s*\)/);
|
|
108
|
+
if (oklabMatch) {
|
|
109
|
+
const rgb = oklabToRgb(+oklabMatch[1], +oklabMatch[2], +oklabMatch[3]);
|
|
110
|
+
let a = 1;
|
|
111
|
+
if (oklabMatch[4] !== undefined) {
|
|
112
|
+
a = oklabMatch[4].endsWith('%') ? parseFloat(oklabMatch[4]) / 100 : +oklabMatch[4];
|
|
113
|
+
}
|
|
114
|
+
return { ...rgb, a };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// color-mix(in srgb, color1 pct, color2)
|
|
118
|
+
const mixMatch = str.match(/color-mix\(\s*in\s+\w+\s*,\s*(.+?)\s*,\s*(.+?)\s*\)/);
|
|
119
|
+
if (mixMatch) {
|
|
120
|
+
const part1 = mixMatch[1].trim().replace(/\s+\d+%$/, '');
|
|
121
|
+
const part2 = mixMatch[2].trim().replace(/\s+\d+%$/, '');
|
|
122
|
+
const c1 = parseColor(part1);
|
|
123
|
+
const c2 = parseColor(part2);
|
|
124
|
+
if (c1 && c2) {
|
|
125
|
+
return { r: Math.round((c1.r + c2.r) / 2), g: Math.round((c1.g + c2.g) / 2), b: Math.round((c1.b + c2.b) / 2), a: (c1.a + c2.a) / 2 };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
return null;
|
|
62
130
|
}
|
|
63
131
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const CLI_PATH = resolve(import.meta.dirname, '..', 'bin', 'design-extract.js');
|
|
7
|
+
|
|
8
|
+
describe('CLI', () => {
|
|
9
|
+
it('shows help with --help', () => {
|
|
10
|
+
const output = execFileSync('node', [CLI_PATH, '--help'], { encoding: 'utf-8' });
|
|
11
|
+
assert.ok(output.includes('designlang'));
|
|
12
|
+
assert.ok(output.includes('Extract'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows version with --version', () => {
|
|
16
|
+
const output = execFileSync('node', [CLI_PATH, '--version'], { encoding: 'utf-8' });
|
|
17
|
+
assert.ok(output.trim().match(/^\d+\.\d+\.\d+$/));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('shows version number 6.0.0', () => {
|
|
21
|
+
const output = execFileSync('node', [CLI_PATH, '--version'], { encoding: 'utf-8' });
|
|
22
|
+
assert.equal(output.trim(), '6.0.0');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('exits with error when no arguments provided', () => {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync('node', [CLI_PATH], { encoding: 'utf-8', stdio: 'pipe' });
|
|
28
|
+
assert.fail('Should have thrown');
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Commander exits with code 1 when required argument is missing
|
|
31
|
+
assert.ok(err.status !== 0);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|