designlang 4.0.1 → 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 +269 -70
- package/package.json +9 -4
- package/src/apply.js +65 -0
- package/src/config.js +36 -0
- package/src/crawler.js +247 -82
- package/src/darkdiff.js +65 -0
- package/src/extractors/animations.js +76 -8
- package/src/extractors/borders.js +40 -5
- package/src/extractors/components.js +100 -1
- package/src/extractors/fonts.js +82 -0
- package/src/extractors/gradients.js +100 -0
- package/src/extractors/icons.js +80 -0
- package/src/extractors/images.js +76 -0
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/variables.js +20 -1
- package/src/extractors/zindex.js +65 -0
- package/src/formatters/figma.js +66 -47
- package/src/formatters/markdown.js +98 -0
- 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 +54 -16
- 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/website/app/api/extract/route.js +85 -0
- package/website/app/components/Extractor.js +184 -0
- package/website/app/globals.css +291 -0
- package/website/app/page.js +13 -0
- package/website/next.config.mjs +10 -1
- package/website/package-lock.json +356 -0
- package/website/package.json +4 -1
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
|
@@ -11,10 +11,23 @@ import { extractComponents } from './extractors/components.js';
|
|
|
11
11
|
import { extractAccessibility } from './extractors/accessibility.js';
|
|
12
12
|
import { extractLayout } from './extractors/layout.js';
|
|
13
13
|
import { scoreDesignSystem } from './extractors/scoring.js';
|
|
14
|
+
import { extractGradients } from './extractors/gradients.js';
|
|
15
|
+
import { extractZIndex } from './extractors/zindex.js';
|
|
16
|
+
import { extractIcons } from './extractors/icons.js';
|
|
17
|
+
import { extractFonts } from './extractors/fonts.js';
|
|
18
|
+
import { extractImageStyles } from './extractors/images.js';
|
|
19
|
+
|
|
20
|
+
function safeExtract(fn, ...args) {
|
|
21
|
+
try { return fn(...args); } catch { return null; }
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
export async function extractDesignLanguage(url, options = {}) {
|
|
16
|
-
const rawData = await crawlPage(url,
|
|
25
|
+
const rawData = await crawlPage(url, {
|
|
26
|
+
...options,
|
|
27
|
+
ignore: options.ignore,
|
|
28
|
+
});
|
|
17
29
|
const styles = rawData.light.computedStyles;
|
|
30
|
+
const warnings = [];
|
|
18
31
|
|
|
19
32
|
const design = {
|
|
20
33
|
meta: {
|
|
@@ -24,29 +37,48 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
24
37
|
elementCount: styles.length,
|
|
25
38
|
pagesAnalyzed: rawData.pagesAnalyzed || 1,
|
|
26
39
|
},
|
|
27
|
-
colors: extractColors
|
|
28
|
-
typography: extractTypography
|
|
29
|
-
spacing: extractSpacing
|
|
30
|
-
shadows: extractShadows
|
|
31
|
-
borders: extractBorders
|
|
32
|
-
variables: extractVariables
|
|
33
|
-
breakpoints: extractBreakpoints
|
|
34
|
-
animations: extractAnimations
|
|
35
|
-
components: extractComponents
|
|
36
|
-
accessibility: extractAccessibility
|
|
37
|
-
layout: extractLayout
|
|
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: [] },
|
|
38
56
|
componentScreenshots: rawData.componentScreenshots || {},
|
|
39
|
-
score: null,
|
|
57
|
+
score: null,
|
|
40
58
|
};
|
|
41
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
|
+
|
|
42
73
|
if (rawData.dark) {
|
|
43
74
|
design.darkMode = {
|
|
44
|
-
colors: extractColors
|
|
45
|
-
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) || {},
|
|
46
77
|
};
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
design.score = scoreDesignSystem
|
|
80
|
+
design.score = safeExtract(scoreDesignSystem, design);
|
|
81
|
+
if (design.score === null) warnings.push('scoring failed');
|
|
50
82
|
|
|
51
83
|
return design;
|
|
52
84
|
}
|
|
@@ -59,6 +91,9 @@ export { formatCssVars } from './formatters/css-vars.js';
|
|
|
59
91
|
export { formatPreview } from './formatters/preview.js';
|
|
60
92
|
export { formatFigma } from './formatters/figma.js';
|
|
61
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';
|
|
62
97
|
export { diffDesigns, formatDiffMarkdown, formatDiffHtml } from './diff.js';
|
|
63
98
|
export { saveSnapshot, getHistory, formatHistoryMarkdown } from './history.js';
|
|
64
99
|
export { captureResponsive } from './extractors/responsive.js';
|
|
@@ -68,3 +103,6 @@ export { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from './multi
|
|
|
68
103
|
export { generateClone } from './clone.js';
|
|
69
104
|
export { scoreDesignSystem } from './extractors/scoring.js';
|
|
70
105
|
export { watchSite } from './watch.js';
|
|
106
|
+
export { diffDarkMode } from './darkdiff.js';
|
|
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
|
+
});
|