designlang 1.0.0 → 3.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 +108 -83
- package/bin/design-extract.js +247 -10
- package/package.json +1 -1
- package/skills/extract-design/SKILL.md +52 -25
- package/src/crawler.js +132 -20
- package/src/diff.js +146 -0
- package/src/extractors/accessibility.js +95 -0
- package/src/extractors/interactions.js +128 -0
- package/src/extractors/layout.js +114 -0
- package/src/extractors/responsive.js +132 -0
- package/src/formatters/figma.js +83 -0
- package/src/formatters/markdown.js +183 -1
- package/src/formatters/preview.js +237 -0
- package/src/formatters/theme.js +128 -0
- package/src/history.js +103 -0
- package/src/index.js +15 -0
- package/src/multibrand.js +151 -0
- package/src/sync.js +69 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export function formatPreview(design) {
|
|
2
|
+
const { meta, colors, typography, spacing, shadows, borders, accessibility, components, componentScreenshots } = design;
|
|
3
|
+
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
+
<title>Design Language: ${esc(meta.title)}</title>
|
|
10
|
+
<style>
|
|
11
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e5e5e5; line-height: 1.6; }
|
|
13
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 40px 24px; }
|
|
14
|
+
h1 { font-size: 36px; font-weight: 700; margin-bottom: 8px; color: #fff; }
|
|
15
|
+
h2 { font-size: 24px; font-weight: 600; margin: 48px 0 20px; color: #fff; border-bottom: 1px solid #222; padding-bottom: 12px; }
|
|
16
|
+
h3 { font-size: 16px; font-weight: 600; margin: 24px 0 12px; color: #a0a0a0; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
17
|
+
.meta { color: #666; font-size: 14px; margin-bottom: 32px; }
|
|
18
|
+
.meta span { margin-right: 16px; }
|
|
19
|
+
.grid { display: grid; gap: 12px; }
|
|
20
|
+
.grid-2 { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
|
|
21
|
+
.grid-3 { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
|
22
|
+
.grid-4 { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
|
|
23
|
+
|
|
24
|
+
/* Color swatches */
|
|
25
|
+
.swatch { border-radius: 12px; overflow: hidden; background: #141414; border: 1px solid #222; }
|
|
26
|
+
.swatch-color { height: 80px; position: relative; }
|
|
27
|
+
.swatch-info { padding: 10px 12px; font-size: 13px; }
|
|
28
|
+
.swatch-hex { font-weight: 600; font-family: monospace; color: #fff; }
|
|
29
|
+
.swatch-label { font-size: 11px; color: #666; margin-top: 2px; }
|
|
30
|
+
.swatch-role { display: inline-block; font-size: 10px; background: #222; color: #aaa; padding: 2px 8px; border-radius: 4px; margin-top: 4px; }
|
|
31
|
+
|
|
32
|
+
/* Type scale */
|
|
33
|
+
.type-row { display: flex; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px solid #1a1a1a; }
|
|
34
|
+
.type-size { font-family: monospace; color: #666; min-width: 60px; font-size: 13px; }
|
|
35
|
+
.type-meta { font-size: 12px; color: #444; margin-left: auto; font-family: monospace; }
|
|
36
|
+
|
|
37
|
+
/* Spacing */
|
|
38
|
+
.spacing-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
|
39
|
+
.spacing-bar { background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 4px; height: 24px; min-width: 4px; transition: width 0.3s; }
|
|
40
|
+
.spacing-label { font-family: monospace; font-size: 13px; color: #888; min-width: 60px; }
|
|
41
|
+
|
|
42
|
+
/* Shadows */
|
|
43
|
+
.shadow-card { background: #fff; border-radius: 12px; padding: 24px; text-align: center; min-height: 80px; display: flex; align-items: center; justify-content: center; }
|
|
44
|
+
.shadow-label { font-size: 12px; color: #333; font-family: monospace; }
|
|
45
|
+
|
|
46
|
+
/* Radii */
|
|
47
|
+
.radius-item { width: 60px; height: 60px; background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; font-size: 11px; color: #fff; font-weight: 600; }
|
|
48
|
+
|
|
49
|
+
/* Accessibility */
|
|
50
|
+
.a11y-score { font-size: 64px; font-weight: 800; }
|
|
51
|
+
.a11y-score.good { color: #22c55e; }
|
|
52
|
+
.a11y-score.warn { color: #eab308; }
|
|
53
|
+
.a11y-score.bad { color: #ef4444; }
|
|
54
|
+
.a11y-pair { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: #141414; border-radius: 8px; margin-bottom: 6px; border: 1px solid #222; }
|
|
55
|
+
.a11y-sample { width: 120px; padding: 6px 12px; border-radius: 6px; text-align: center; font-size: 14px; font-weight: 500; }
|
|
56
|
+
.a11y-ratio { font-family: monospace; font-size: 14px; min-width: 50px; }
|
|
57
|
+
.a11y-badge { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
|
|
58
|
+
.a11y-badge.pass { background: #22c55e20; color: #22c55e; }
|
|
59
|
+
.a11y-badge.fail { background: #ef444420; color: #ef4444; }
|
|
60
|
+
|
|
61
|
+
/* Components */
|
|
62
|
+
.comp-screenshot { border-radius: 8px; border: 1px solid #222; max-width: 100%; }
|
|
63
|
+
|
|
64
|
+
/* Stat cards */
|
|
65
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin: 24px 0; }
|
|
66
|
+
.stat { background: #141414; border: 1px solid #222; border-radius: 12px; padding: 16px; }
|
|
67
|
+
.stat-value { font-size: 28px; font-weight: 700; color: #fff; }
|
|
68
|
+
.stat-label { font-size: 12px; color: #666; margin-top: 4px; }
|
|
69
|
+
|
|
70
|
+
.font-tag { display: inline-block; background: #1e1e2e; color: #a78bfa; padding: 4px 10px; border-radius: 6px; font-size: 13px; margin: 4px 4px 4px 0; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div class="container">
|
|
75
|
+
|
|
76
|
+
<h1>${esc(meta.title)}</h1>
|
|
77
|
+
<div class="meta">
|
|
78
|
+
<span>${esc(meta.url)}</span>
|
|
79
|
+
<span>${meta.elementCount} elements</span>
|
|
80
|
+
<span>${new Date(meta.timestamp).toLocaleDateString()}</span>
|
|
81
|
+
${meta.pagesAnalyzed > 1 ? `<span>${meta.pagesAnalyzed} pages crawled</span>` : ''}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="stats">
|
|
85
|
+
<div class="stat"><div class="stat-value">${colors.all.length}</div><div class="stat-label">Colors</div></div>
|
|
86
|
+
<div class="stat"><div class="stat-value">${typography.families.length}</div><div class="stat-label">Font Families</div></div>
|
|
87
|
+
<div class="stat"><div class="stat-value">${spacing.scale.length}</div><div class="stat-label">Spacing Values</div></div>
|
|
88
|
+
<div class="stat"><div class="stat-value">${shadows.values.length}</div><div class="stat-label">Shadows</div></div>
|
|
89
|
+
<div class="stat"><div class="stat-value">${borders.radii.length}</div><div class="stat-label">Border Radii</div></div>
|
|
90
|
+
<div class="stat"><div class="stat-value">${Object.keys(components).length}</div><div class="stat-label">Components</div></div>
|
|
91
|
+
${accessibility ? `<div class="stat"><div class="stat-value ${accessibility.score >= 80 ? 'good' : accessibility.score >= 50 ? 'warn' : 'bad'}" style="color: ${accessibility.score >= 80 ? '#22c55e' : accessibility.score >= 50 ? '#eab308' : '#ef4444'}">${accessibility.score}%</div><div class="stat-label">A11y Score</div></div>` : ''}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Colors -->
|
|
95
|
+
<h2>Color Palette</h2>
|
|
96
|
+
|
|
97
|
+
${colors.primary ? `
|
|
98
|
+
<h3>Brand Colors</h3>
|
|
99
|
+
<div class="grid grid-3">
|
|
100
|
+
${colors.primary ? swatch(colors.primary.hex, 'Primary', colors.primary.count + ' uses') : ''}
|
|
101
|
+
${colors.secondary ? swatch(colors.secondary.hex, 'Secondary', colors.secondary.count + ' uses') : ''}
|
|
102
|
+
${colors.accent ? swatch(colors.accent.hex, 'Accent', colors.accent.count + ' uses') : ''}
|
|
103
|
+
</div>` : ''}
|
|
104
|
+
|
|
105
|
+
${colors.neutrals.length > 0 ? `
|
|
106
|
+
<h3>Neutrals</h3>
|
|
107
|
+
<div class="grid grid-4">
|
|
108
|
+
${colors.neutrals.slice(0, 10).map(c => swatch(c.hex, '', c.count + ' uses')).join('\n ')}
|
|
109
|
+
</div>` : ''}
|
|
110
|
+
|
|
111
|
+
${colors.all.length > 3 ? `
|
|
112
|
+
<h3>Full Palette</h3>
|
|
113
|
+
<div class="grid grid-4">
|
|
114
|
+
${colors.all.slice(0, 20).map(c => swatch(c.hex, c.contexts.join(', '), c.count + ' uses')).join('\n ')}
|
|
115
|
+
</div>` : ''}
|
|
116
|
+
|
|
117
|
+
${colors.gradients.length > 0 ? `
|
|
118
|
+
<h3>Gradients</h3>
|
|
119
|
+
<div class="grid grid-2">
|
|
120
|
+
${colors.gradients.slice(0, 6).map(g => `<div class="swatch"><div class="swatch-color" style="background-image:${g};height:100px"></div></div>`).join('\n ')}
|
|
121
|
+
</div>` : ''}
|
|
122
|
+
|
|
123
|
+
<!-- Typography -->
|
|
124
|
+
<h2>Typography</h2>
|
|
125
|
+
|
|
126
|
+
${typography.families.length > 0 ? `
|
|
127
|
+
<h3>Font Families</h3>
|
|
128
|
+
<div>
|
|
129
|
+
${typography.families.map(f => `<span class="font-tag">${esc(f.name)} <span style="color:#666">(${f.usage}, ${f.count}x)</span></span>`).join('')}
|
|
130
|
+
</div>` : ''}
|
|
131
|
+
|
|
132
|
+
${typography.scale.length > 0 ? `
|
|
133
|
+
<h3>Type Scale</h3>
|
|
134
|
+
<div>
|
|
135
|
+
${typography.scale.slice(0, 12).map(s => `
|
|
136
|
+
<div class="type-row">
|
|
137
|
+
<span class="type-size">${s.size}px</span>
|
|
138
|
+
<span style="font-size:${Math.min(s.size, 48)}px;font-weight:${s.weight};color:#fff">The quick brown fox</span>
|
|
139
|
+
<span class="type-meta">${s.weight} / ${s.lineHeight}</span>
|
|
140
|
+
</div>`).join('')}
|
|
141
|
+
</div>` : ''}
|
|
142
|
+
|
|
143
|
+
<!-- Spacing -->
|
|
144
|
+
${spacing.scale.length > 0 ? `
|
|
145
|
+
<h2>Spacing Scale${spacing.base ? ` (base: ${spacing.base}px)` : ''}</h2>
|
|
146
|
+
<div>
|
|
147
|
+
${spacing.scale.slice(0, 16).map(v => `
|
|
148
|
+
<div class="spacing-row">
|
|
149
|
+
<span class="spacing-label">${v}px</span>
|
|
150
|
+
<div class="spacing-bar" style="width:${Math.min(v * 2, 500)}px"></div>
|
|
151
|
+
</div>`).join('')}
|
|
152
|
+
</div>` : ''}
|
|
153
|
+
|
|
154
|
+
<!-- Shadows -->
|
|
155
|
+
${shadows.values.length > 0 ? `
|
|
156
|
+
<h2>Box Shadows</h2>
|
|
157
|
+
<div class="grid grid-3">
|
|
158
|
+
${shadows.values.map(s => `
|
|
159
|
+
<div class="shadow-card" style="box-shadow:${s.raw}">
|
|
160
|
+
<span class="shadow-label">${s.label}${s.inset ? ' (inset)' : ''}</span>
|
|
161
|
+
</div>`).join('')}
|
|
162
|
+
</div>` : ''}
|
|
163
|
+
|
|
164
|
+
<!-- Border Radii -->
|
|
165
|
+
${borders.radii.length > 0 ? `
|
|
166
|
+
<h2>Border Radii</h2>
|
|
167
|
+
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:end">
|
|
168
|
+
${borders.radii.map(r => `
|
|
169
|
+
<div style="text-align:center">
|
|
170
|
+
<div class="radius-item" style="border-radius:${r.value}px">${r.value}px</div>
|
|
171
|
+
<div style="font-size:11px;color:#666;margin-top:6px">${r.label}</div>
|
|
172
|
+
</div>`).join('')}
|
|
173
|
+
</div>` : ''}
|
|
174
|
+
|
|
175
|
+
<!-- Accessibility -->
|
|
176
|
+
${accessibility ? `
|
|
177
|
+
<h2>Accessibility</h2>
|
|
178
|
+
<div style="display:flex;align-items:center;gap:24px;margin-bottom:24px">
|
|
179
|
+
<div class="a11y-score ${accessibility.score >= 80 ? 'good' : accessibility.score >= 50 ? 'warn' : 'bad'}">${accessibility.score}%</div>
|
|
180
|
+
<div>
|
|
181
|
+
<div style="color:#fff;font-weight:600">WCAG Contrast Score</div>
|
|
182
|
+
<div style="color:#666;font-size:14px">${accessibility.passCount} passing / ${accessibility.failCount} failing color pairs</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<h3>Color Pair Analysis</h3>
|
|
187
|
+
${accessibility.pairs.slice(0, 20).map(p => `
|
|
188
|
+
<div class="a11y-pair">
|
|
189
|
+
<div class="a11y-sample" style="background:${p.background};color:${p.foreground}">Sample</div>
|
|
190
|
+
<div style="font-family:monospace;font-size:12px;color:#888">${p.foreground} on ${p.background}</div>
|
|
191
|
+
<div class="a11y-ratio">${p.ratio}:1</div>
|
|
192
|
+
<span class="a11y-badge ${p.level === 'FAIL' ? 'fail' : 'pass'}">${p.level}</span>
|
|
193
|
+
<div style="font-size:11px;color:#555;margin-left:auto">${p.count}x</div>
|
|
194
|
+
</div>`).join('')}
|
|
195
|
+
` : ''}
|
|
196
|
+
|
|
197
|
+
<!-- Component Screenshots -->
|
|
198
|
+
${componentScreenshots && Object.keys(componentScreenshots).length > 0 ? `
|
|
199
|
+
<h2>Component Screenshots</h2>
|
|
200
|
+
<div class="grid grid-2">
|
|
201
|
+
${Object.entries(componentScreenshots).map(([name, info]) => `
|
|
202
|
+
<div>
|
|
203
|
+
<h3>${info.label}</h3>
|
|
204
|
+
<img class="comp-screenshot" src="${info.path}" alt="${info.label}" />
|
|
205
|
+
</div>`).join('')}
|
|
206
|
+
</div>` : ''}
|
|
207
|
+
|
|
208
|
+
</div>
|
|
209
|
+
</body>
|
|
210
|
+
</html>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function swatch(hex, label, meta) {
|
|
214
|
+
const rgb = hexToRgb(hex);
|
|
215
|
+
const textColor = isLight(rgb) ? '#000' : '#fff';
|
|
216
|
+
return `<div class="swatch">
|
|
217
|
+
<div class="swatch-color" style="background:${hex}"></div>
|
|
218
|
+
<div class="swatch-info">
|
|
219
|
+
<div class="swatch-hex">${hex}</div>
|
|
220
|
+
${label ? `<div class="swatch-label">${esc(label)}</div>` : ''}
|
|
221
|
+
${meta ? `<div class="swatch-label">${esc(meta)}</div>` : ''}
|
|
222
|
+
</div>
|
|
223
|
+
</div>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function hexToRgb(hex) {
|
|
227
|
+
const m = hex.replace('#', '').match(/.{2}/g);
|
|
228
|
+
return m ? { r: parseInt(m[0], 16), g: parseInt(m[1], 16), b: parseInt(m[2], 16) } : { r: 0, g: 0, b: 0 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isLight({ r, g, b }) {
|
|
232
|
+
return (r * 0.299 + g * 0.587 + b * 0.114) > 150;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function esc(s) {
|
|
236
|
+
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
237
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Framework-specific theme generators
|
|
2
|
+
|
|
3
|
+
export function formatReactTheme(design) {
|
|
4
|
+
const { colors, typography, spacing, shadows, borders } = design;
|
|
5
|
+
|
|
6
|
+
const theme = {};
|
|
7
|
+
|
|
8
|
+
// Colors
|
|
9
|
+
theme.colors = {};
|
|
10
|
+
if (colors.primary) theme.colors.primary = colors.primary.hex;
|
|
11
|
+
if (colors.secondary) theme.colors.secondary = colors.secondary.hex;
|
|
12
|
+
if (colors.accent) theme.colors.accent = colors.accent.hex;
|
|
13
|
+
if (colors.backgrounds.length) theme.colors.background = colors.backgrounds[0];
|
|
14
|
+
if (colors.text.length) theme.colors.foreground = colors.text[0];
|
|
15
|
+
for (let i = 0; i < colors.neutrals.length && i < 10; i++) {
|
|
16
|
+
theme.colors[`neutral${i * 100 || 50}`] = colors.neutrals[i].hex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Typography
|
|
20
|
+
theme.fonts = {};
|
|
21
|
+
for (const f of typography.families) {
|
|
22
|
+
const key = f.name.toLowerCase().includes('mono') ? 'mono' : f.usage === 'headings' ? 'heading' : 'body';
|
|
23
|
+
theme.fonts[key] = `'${f.name}', ${f.name.toLowerCase().includes('mono') ? 'monospace' : 'sans-serif'}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
theme.fontSizes = {};
|
|
27
|
+
for (const s of typography.scale.slice(0, 12)) {
|
|
28
|
+
theme.fontSizes[s.size] = `${s.size}px`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Spacing
|
|
32
|
+
theme.space = {};
|
|
33
|
+
for (const v of spacing.scale.slice(0, 16)) {
|
|
34
|
+
theme.space[v] = `${v}px`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Radii
|
|
38
|
+
theme.radii = {};
|
|
39
|
+
for (const r of borders.radii) {
|
|
40
|
+
theme.radii[r.label] = `${r.value}px`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Shadows
|
|
44
|
+
theme.shadows = {};
|
|
45
|
+
for (const s of shadows.values) {
|
|
46
|
+
theme.shadows[s.label] = s.raw;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `// React Theme — extracted from ${design.meta.url}
|
|
50
|
+
// Compatible with: Chakra UI, Stitches, Vanilla Extract, or any CSS-in-JS
|
|
51
|
+
|
|
52
|
+
export const theme = ${JSON.stringify(theme, null, 2)};
|
|
53
|
+
|
|
54
|
+
export default theme;
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatShadcnTheme(design) {
|
|
59
|
+
const { colors, borders } = design;
|
|
60
|
+
const lines = ['@layer base {', ' :root {'];
|
|
61
|
+
|
|
62
|
+
// Map to shadcn/ui CSS variable naming convention
|
|
63
|
+
if (colors.backgrounds.length) lines.push(` --background: ${toHslString(colors.backgrounds[0])};`);
|
|
64
|
+
if (colors.text.length) lines.push(` --foreground: ${toHslString(colors.text[0])};`);
|
|
65
|
+
if (colors.primary) {
|
|
66
|
+
lines.push(` --primary: ${toHslString(colors.primary.hex)};`);
|
|
67
|
+
lines.push(` --primary-foreground: ${isLightHex(colors.primary.hex) ? '0 0% 0%' : '0 0% 100%'};`);
|
|
68
|
+
}
|
|
69
|
+
if (colors.secondary) {
|
|
70
|
+
lines.push(` --secondary: ${toHslString(colors.secondary.hex)};`);
|
|
71
|
+
lines.push(` --secondary-foreground: ${isLightHex(colors.secondary.hex) ? '0 0% 0%' : '0 0% 100%'};`);
|
|
72
|
+
}
|
|
73
|
+
if (colors.accent) {
|
|
74
|
+
lines.push(` --accent: ${toHslString(colors.accent.hex)};`);
|
|
75
|
+
lines.push(` --accent-foreground: ${isLightHex(colors.accent.hex) ? '0 0% 0%' : '0 0% 100%'};`);
|
|
76
|
+
}
|
|
77
|
+
if (colors.neutrals.length > 0) {
|
|
78
|
+
lines.push(` --muted: ${toHslString(colors.neutrals[colors.neutrals.length - 1]?.hex || '#888')};`);
|
|
79
|
+
lines.push(` --muted-foreground: ${toHslString(colors.neutrals[0]?.hex || '#333')};`);
|
|
80
|
+
lines.push(` --border: ${toHslString(colors.neutrals[Math.min(4, colors.neutrals.length - 1)]?.hex || '#e5e5e5')};`);
|
|
81
|
+
}
|
|
82
|
+
if (borders.radii.length > 0) {
|
|
83
|
+
const md = borders.radii.find(r => r.label === 'md') || borders.radii[0];
|
|
84
|
+
lines.push(` --radius: ${md.value}px;`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
lines.push(' }');
|
|
88
|
+
|
|
89
|
+
// Dark mode
|
|
90
|
+
if (design.darkMode) {
|
|
91
|
+
lines.push(' .dark {');
|
|
92
|
+
const dc = design.darkMode.colors;
|
|
93
|
+
if (dc.backgrounds.length) lines.push(` --background: ${toHslString(dc.backgrounds[0])};`);
|
|
94
|
+
if (dc.text.length) lines.push(` --foreground: ${toHslString(dc.text[0])};`);
|
|
95
|
+
if (dc.primary) lines.push(` --primary: ${toHslString(dc.primary.hex)};`);
|
|
96
|
+
lines.push(' }');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push('}');
|
|
100
|
+
|
|
101
|
+
return `/* shadcn/ui Theme — extracted from ${design.meta.url} */\n/* Paste into your globals.css */\n\n${lines.join('\n')}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toHslString(hex) {
|
|
105
|
+
if (!hex) return '0 0% 0%';
|
|
106
|
+
const h = hex.replace('#', '');
|
|
107
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
108
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
109
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
110
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
111
|
+
const l = (max + min) / 2;
|
|
112
|
+
if (max === min) return `0 0% ${Math.round(l * 100)}%`;
|
|
113
|
+
const d = max - min;
|
|
114
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
115
|
+
let hue;
|
|
116
|
+
if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
117
|
+
else if (max === g) hue = ((b - r) / d + 2) / 6;
|
|
118
|
+
else hue = ((r - g) / d + 4) / 6;
|
|
119
|
+
return `${Math.round(hue * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isLightHex(hex) {
|
|
123
|
+
const h = hex.replace('#', '');
|
|
124
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
125
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
126
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
127
|
+
return (r * 0.299 + g * 0.587 + b * 0.114) > 150;
|
|
128
|
+
}
|
package/src/history.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Historical tracking — save and compare design snapshots over time
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const HISTORY_DIR = join(process.env.HOME || process.env.USERPROFILE || '.', '.designlang');
|
|
7
|
+
|
|
8
|
+
function ensureDir() {
|
|
9
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function historyFile(hostname) {
|
|
13
|
+
return join(HISTORY_DIR, `${hostname}.json`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveSnapshot(design) {
|
|
17
|
+
ensureDir();
|
|
18
|
+
const hostname = new URL(design.meta.url).hostname.replace(/^www\./, '');
|
|
19
|
+
const file = historyFile(hostname);
|
|
20
|
+
|
|
21
|
+
let history = [];
|
|
22
|
+
if (existsSync(file)) {
|
|
23
|
+
try { history = JSON.parse(readFileSync(file, 'utf-8')); } catch { history = []; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Compact snapshot — only store key metrics, not full data
|
|
27
|
+
const snapshot = {
|
|
28
|
+
timestamp: design.meta.timestamp,
|
|
29
|
+
url: design.meta.url,
|
|
30
|
+
colors: {
|
|
31
|
+
count: design.colors.all.length,
|
|
32
|
+
primary: design.colors.primary?.hex || null,
|
|
33
|
+
secondary: design.colors.secondary?.hex || null,
|
|
34
|
+
accent: design.colors.accent?.hex || null,
|
|
35
|
+
},
|
|
36
|
+
typography: {
|
|
37
|
+
families: design.typography.families.map(f => f.name),
|
|
38
|
+
scaleCount: design.typography.scale.length,
|
|
39
|
+
},
|
|
40
|
+
spacing: {
|
|
41
|
+
base: design.spacing.base,
|
|
42
|
+
count: design.spacing.scale.length,
|
|
43
|
+
},
|
|
44
|
+
shadows: design.shadows.values.length,
|
|
45
|
+
radii: design.borders.radii.length,
|
|
46
|
+
breakpoints: design.breakpoints.length,
|
|
47
|
+
components: Object.keys(design.components),
|
|
48
|
+
a11yScore: design.accessibility?.score || null,
|
|
49
|
+
cssVarCount: Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
history.push(snapshot);
|
|
53
|
+
writeFileSync(file, JSON.stringify(history, null, 2), 'utf-8');
|
|
54
|
+
return { hostname, snapshotCount: history.length, file };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getHistory(url) {
|
|
58
|
+
ensureDir();
|
|
59
|
+
const hostname = new URL(url).hostname.replace(/^www\./, '');
|
|
60
|
+
const file = historyFile(hostname);
|
|
61
|
+
if (!existsSync(file)) return [];
|
|
62
|
+
try { return JSON.parse(readFileSync(file, 'utf-8')); } catch { return []; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatHistoryMarkdown(url, history) {
|
|
66
|
+
if (history.length === 0) return `No history found for ${url}.\n`;
|
|
67
|
+
|
|
68
|
+
const hostname = new URL(url).hostname;
|
|
69
|
+
const lines = [`# Design History: ${hostname}`, '', `${history.length} snapshots recorded.`, ''];
|
|
70
|
+
|
|
71
|
+
lines.push('| Date | Colors | Fonts | Spacing | A11y | CSS Vars |');
|
|
72
|
+
lines.push('|------|--------|-------|---------|------|----------|');
|
|
73
|
+
|
|
74
|
+
for (const snap of history.reverse()) {
|
|
75
|
+
const date = new Date(snap.timestamp).toLocaleDateString();
|
|
76
|
+
lines.push(`| ${date} | ${snap.colors.count} (primary: \`${snap.colors.primary}\`) | ${snap.typography.families.join(', ')} | ${snap.spacing.count} vals | ${snap.a11yScore ?? 'n/a'}% | ${snap.cssVarCount} |`);
|
|
77
|
+
}
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
// Detect changes between first and last snapshot
|
|
81
|
+
if (history.length >= 2) {
|
|
82
|
+
const first = history[history.length - 1]; // oldest (reversed)
|
|
83
|
+
const last = history[0]; // newest
|
|
84
|
+
|
|
85
|
+
lines.push('## Changes Over Time');
|
|
86
|
+
lines.push('');
|
|
87
|
+
if (first.colors.primary !== last.colors.primary) {
|
|
88
|
+
lines.push(`- **Primary color changed:** \`${first.colors.primary}\` → \`${last.colors.primary}\``);
|
|
89
|
+
}
|
|
90
|
+
if (first.typography.families.join(',') !== last.typography.families.join(',')) {
|
|
91
|
+
lines.push(`- **Fonts changed:** ${first.typography.families.join(', ')} → ${last.typography.families.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
if (first.colors.count !== last.colors.count) {
|
|
94
|
+
lines.push(`- **Color count:** ${first.colors.count} → ${last.colors.count}`);
|
|
95
|
+
}
|
|
96
|
+
if (first.a11yScore !== last.a11yScore) {
|
|
97
|
+
lines.push(`- **A11y score:** ${first.a11yScore}% → ${last.a11yScore}%`);
|
|
98
|
+
}
|
|
99
|
+
lines.push('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import { extractVariables } from './extractors/variables.js';
|
|
|
8
8
|
import { extractBreakpoints } from './extractors/breakpoints.js';
|
|
9
9
|
import { extractAnimations } from './extractors/animations.js';
|
|
10
10
|
import { extractComponents } from './extractors/components.js';
|
|
11
|
+
import { extractAccessibility } from './extractors/accessibility.js';
|
|
12
|
+
import { extractLayout } from './extractors/layout.js';
|
|
11
13
|
|
|
12
14
|
export async function extractDesignLanguage(url, options = {}) {
|
|
13
15
|
const rawData = await crawlPage(url, options);
|
|
@@ -19,6 +21,7 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
19
21
|
title: rawData.title,
|
|
20
22
|
timestamp: new Date().toISOString(),
|
|
21
23
|
elementCount: styles.length,
|
|
24
|
+
pagesAnalyzed: rawData.pagesAnalyzed || 1,
|
|
22
25
|
},
|
|
23
26
|
colors: extractColors(styles),
|
|
24
27
|
typography: extractTypography(styles),
|
|
@@ -29,6 +32,9 @@ export async function extractDesignLanguage(url, options = {}) {
|
|
|
29
32
|
breakpoints: extractBreakpoints(rawData.light.mediaQueries),
|
|
30
33
|
animations: extractAnimations(styles, rawData.light.keyframes),
|
|
31
34
|
components: extractComponents(styles),
|
|
35
|
+
accessibility: extractAccessibility(styles),
|
|
36
|
+
layout: extractLayout(styles),
|
|
37
|
+
componentScreenshots: rawData.componentScreenshots || {},
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
if (rawData.dark) {
|
|
@@ -46,3 +52,12 @@ export { formatTokens } from './formatters/tokens.js';
|
|
|
46
52
|
export { formatMarkdown } from './formatters/markdown.js';
|
|
47
53
|
export { formatTailwind } from './formatters/tailwind.js';
|
|
48
54
|
export { formatCssVars } from './formatters/css-vars.js';
|
|
55
|
+
export { formatPreview } from './formatters/preview.js';
|
|
56
|
+
export { formatFigma } from './formatters/figma.js';
|
|
57
|
+
export { formatReactTheme, formatShadcnTheme } from './formatters/theme.js';
|
|
58
|
+
export { diffDesigns, formatDiffMarkdown, formatDiffHtml } from './diff.js';
|
|
59
|
+
export { saveSnapshot, getHistory, formatHistoryMarkdown } from './history.js';
|
|
60
|
+
export { captureResponsive } from './extractors/responsive.js';
|
|
61
|
+
export { captureInteractions } from './extractors/interactions.js';
|
|
62
|
+
export { syncDesign } from './sync.js';
|
|
63
|
+
export { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from './multibrand.js';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Multi-brand N-site comparison matrix
|
|
2
|
+
|
|
3
|
+
import { extractDesignLanguage } from './index.js';
|
|
4
|
+
|
|
5
|
+
export async function compareBrands(urls, options = {}) {
|
|
6
|
+
const brands = [];
|
|
7
|
+
|
|
8
|
+
for (const url of urls) {
|
|
9
|
+
const normalized = url.startsWith('http') ? url : `https://${url}`;
|
|
10
|
+
try {
|
|
11
|
+
const design = await extractDesignLanguage(normalized, options);
|
|
12
|
+
const hostname = new URL(normalized).hostname.replace(/^www\./, '');
|
|
13
|
+
brands.push({ url: normalized, hostname, design });
|
|
14
|
+
} catch (err) {
|
|
15
|
+
brands.push({ url: normalized, hostname: url, error: err.message });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return brands;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatBrandMatrix(brands) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
const valid = brands.filter(b => !b.error);
|
|
25
|
+
|
|
26
|
+
lines.push('# Multi-Brand Design Comparison');
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push(`Comparing ${valid.length} brands.`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
|
|
31
|
+
// Overview table
|
|
32
|
+
const headers = ['Property', ...valid.map(b => b.hostname)];
|
|
33
|
+
lines.push(`| ${headers.join(' | ')} |`);
|
|
34
|
+
lines.push(`| ${headers.map(() => '---').join(' | ')} |`);
|
|
35
|
+
|
|
36
|
+
// Primary color
|
|
37
|
+
lines.push(`| Primary Color | ${valid.map(b => `\`${b.design.colors.primary?.hex || 'none'}\``).join(' | ')} |`);
|
|
38
|
+
// Secondary color
|
|
39
|
+
lines.push(`| Secondary Color | ${valid.map(b => `\`${b.design.colors.secondary?.hex || 'none'}\``).join(' | ')} |`);
|
|
40
|
+
// Fonts
|
|
41
|
+
lines.push(`| Fonts | ${valid.map(b => b.design.typography.families.map(f => f.name).join(', ') || 'none').join(' | ')} |`);
|
|
42
|
+
// Color count
|
|
43
|
+
lines.push(`| Color Count | ${valid.map(b => b.design.colors.all.length).join(' | ')} |`);
|
|
44
|
+
// Spacing base
|
|
45
|
+
lines.push(`| Spacing Base | ${valid.map(b => b.design.spacing.base ? `${b.design.spacing.base}px` : 'none').join(' | ')} |`);
|
|
46
|
+
// A11y score
|
|
47
|
+
lines.push(`| A11y Score | ${valid.map(b => b.design.accessibility ? `${b.design.accessibility.score}%` : 'n/a').join(' | ')} |`);
|
|
48
|
+
// Shadows
|
|
49
|
+
lines.push(`| Shadows | ${valid.map(b => b.design.shadows.values.length).join(' | ')} |`);
|
|
50
|
+
// Radii
|
|
51
|
+
lines.push(`| Border Radii | ${valid.map(b => b.design.borders.radii.length).join(' | ')} |`);
|
|
52
|
+
// CSS vars
|
|
53
|
+
lines.push(`| CSS Variables | ${valid.map(b => Object.values(b.design.variables).reduce((s, v) => s + Object.keys(v).length, 0)).join(' | ')} |`);
|
|
54
|
+
// Components
|
|
55
|
+
lines.push(`| Components | ${valid.map(b => Object.keys(b.design.components).join(', ') || 'none').join(' | ')} |`);
|
|
56
|
+
|
|
57
|
+
lines.push('');
|
|
58
|
+
|
|
59
|
+
// Color overlap matrix
|
|
60
|
+
lines.push('## Color Overlap');
|
|
61
|
+
lines.push('');
|
|
62
|
+
if (valid.length >= 2) {
|
|
63
|
+
for (let i = 0; i < valid.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < valid.length; j++) {
|
|
65
|
+
const colorsA = new Set(valid[i].design.colors.all.map(c => c.hex));
|
|
66
|
+
const colorsB = new Set(valid[j].design.colors.all.map(c => c.hex));
|
|
67
|
+
const shared = [...colorsA].filter(c => colorsB.has(c));
|
|
68
|
+
lines.push(`**${valid[i].hostname} vs ${valid[j].hostname}:** ${shared.length} shared colors${shared.length > 0 ? ` (${shared.slice(0, 5).map(c => `\`${c}\``).join(', ')})` : ''}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Font comparison
|
|
75
|
+
lines.push('## Typography Comparison');
|
|
76
|
+
lines.push('');
|
|
77
|
+
const allFonts = new Set();
|
|
78
|
+
for (const b of valid) {
|
|
79
|
+
for (const f of b.design.typography.families) allFonts.add(f.name);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`| Font | ${valid.map(b => b.hostname).join(' | ')} |`);
|
|
82
|
+
lines.push(`| --- | ${valid.map(() => '---').join(' | ')} |`);
|
|
83
|
+
for (const font of allFonts) {
|
|
84
|
+
lines.push(`| ${font} | ${valid.map(b => b.design.typography.families.some(f => f.name === font) ? 'Yes' : '-').join(' | ')} |`);
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
// Errors
|
|
89
|
+
const errored = brands.filter(b => b.error);
|
|
90
|
+
if (errored.length > 0) {
|
|
91
|
+
lines.push('## Errors');
|
|
92
|
+
lines.push('');
|
|
93
|
+
for (const b of errored) {
|
|
94
|
+
lines.push(`- **${b.url}**: ${b.error}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatBrandMatrixHtml(brands) {
|
|
103
|
+
const valid = brands.filter(b => !b.error);
|
|
104
|
+
|
|
105
|
+
const swatchCell = (hex) => hex
|
|
106
|
+
? `<td><span style="display:inline-block;width:16px;height:16px;border-radius:4px;background:${hex};border:1px solid #333;vertical-align:middle;margin-right:6px"></span><code>${hex}</code></td>`
|
|
107
|
+
: `<td>-</td>`;
|
|
108
|
+
|
|
109
|
+
return `<!DOCTYPE html>
|
|
110
|
+
<html><head><meta charset="UTF-8"><title>Multi-Brand Comparison</title>
|
|
111
|
+
<style>
|
|
112
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
113
|
+
body { font-family:-apple-system,sans-serif; background:#0a0a0a; color:#e5e5e5; padding:40px; }
|
|
114
|
+
h1 { font-size:28px; color:#fff; margin-bottom:24px; }
|
|
115
|
+
h2 { font-size:18px; color:#fff; margin:32px 0 12px; }
|
|
116
|
+
table { width:100%; border-collapse:collapse; margin:12px 0; }
|
|
117
|
+
th { text-align:left; padding:10px 12px; background:#141414; color:#888; font-size:12px; text-transform:uppercase; letter-spacing:0.05em; border-bottom:1px solid #222; }
|
|
118
|
+
td { padding:10px 12px; border-bottom:1px solid #1a1a1a; font-size:13px; }
|
|
119
|
+
tr:hover td { background:#111; }
|
|
120
|
+
code { background:#1e1e2e; padding:2px 6px; border-radius:4px; font-size:12px; color:#a78bfa; }
|
|
121
|
+
.score-good { color:#22c55e; } .score-warn { color:#eab308; } .score-bad { color:#ef4444; }
|
|
122
|
+
</style></head><body>
|
|
123
|
+
<h1>Multi-Brand Design Comparison</h1>
|
|
124
|
+
<p style="color:#666;margin-bottom:24px">${valid.length} brands analyzed</p>
|
|
125
|
+
|
|
126
|
+
<table>
|
|
127
|
+
<tr><th>Property</th>${valid.map(b => `<th>${b.hostname}</th>`).join('')}</tr>
|
|
128
|
+
<tr><td>Primary Color</td>${valid.map(b => swatchCell(b.design.colors.primary?.hex)).join('')}</tr>
|
|
129
|
+
<tr><td>Secondary Color</td>${valid.map(b => swatchCell(b.design.colors.secondary?.hex)).join('')}</tr>
|
|
130
|
+
<tr><td>Fonts</td>${valid.map(b => `<td>${b.design.typography.families.map(f => `<code>${f.name}</code>`).join(' ')}</td>`).join('')}</tr>
|
|
131
|
+
<tr><td>Colors</td>${valid.map(b => `<td>${b.design.colors.all.length}</td>`).join('')}</tr>
|
|
132
|
+
<tr><td>Spacing Base</td>${valid.map(b => `<td>${b.design.spacing.base ? b.design.spacing.base + 'px' : '-'}</td>`).join('')}</tr>
|
|
133
|
+
<tr><td>A11y Score</td>${valid.map(b => {
|
|
134
|
+
const s = b.design.accessibility?.score;
|
|
135
|
+
const cls = s >= 80 ? 'score-good' : s >= 50 ? 'score-warn' : 'score-bad';
|
|
136
|
+
return `<td class="${cls}">${s ?? 'n/a'}%</td>`;
|
|
137
|
+
}).join('')}</tr>
|
|
138
|
+
<tr><td>Shadows</td>${valid.map(b => `<td>${b.design.shadows.values.length}</td>`).join('')}</tr>
|
|
139
|
+
<tr><td>Border Radii</td>${valid.map(b => `<td>${b.design.borders.radii.length}</td>`).join('')}</tr>
|
|
140
|
+
<tr><td>CSS Variables</td>${valid.map(b => `<td>${Object.values(b.design.variables).reduce((s, v) => s + Object.keys(v).length, 0)}</td>`).join('')}</tr>
|
|
141
|
+
</table>
|
|
142
|
+
|
|
143
|
+
<h2>Full Color Palettes</h2>
|
|
144
|
+
${valid.map(b => `
|
|
145
|
+
<h3 style="color:#888;font-size:14px;margin:16px 0 8px">${b.hostname}</h3>
|
|
146
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
147
|
+
${b.design.colors.all.slice(0, 15).map(c => `<div style="width:32px;height:32px;border-radius:6px;background:${c.hex};border:1px solid #333" title="${c.hex}"></div>`).join('')}
|
|
148
|
+
</div>`).join('')}
|
|
149
|
+
|
|
150
|
+
</body></html>`;
|
|
151
|
+
}
|