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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const LAYER_DEFS = [
|
|
2
|
+
{ name: 'modal', min: 1000, max: Infinity },
|
|
3
|
+
{ name: 'dropdown', min: 100, max: 999 },
|
|
4
|
+
{ name: 'sticky', min: 10, max: 99 },
|
|
5
|
+
{ name: 'base', min: -Infinity, max: 9 },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function elLabel(el) {
|
|
9
|
+
const cls = el.classList?.length ? '.' + [...el.classList].join('.') : '';
|
|
10
|
+
return el.tag + cls;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractZIndex(styles) {
|
|
14
|
+
// Filter and parse explicit z-index values
|
|
15
|
+
const entries = styles
|
|
16
|
+
.filter(el => el.zIndex !== 'auto')
|
|
17
|
+
.map(el => ({ value: parseInt(el.zIndex, 10), el }))
|
|
18
|
+
.filter(e => !isNaN(e.value));
|
|
19
|
+
|
|
20
|
+
// Group by z-index value
|
|
21
|
+
const byValue = new Map();
|
|
22
|
+
for (const { value, el } of entries) {
|
|
23
|
+
if (!byValue.has(value)) byValue.set(value, []);
|
|
24
|
+
byValue.get(value).push(el);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const allValues = [...byValue.keys()].sort((a, b) => a - b);
|
|
28
|
+
|
|
29
|
+
// Build scale: each unique value with count and representative elements
|
|
30
|
+
const scale = allValues.map(value => ({
|
|
31
|
+
value,
|
|
32
|
+
count: byValue.get(value).length,
|
|
33
|
+
elements: byValue.get(value).map(elLabel),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Build layers from predefined ranges
|
|
37
|
+
const layers = LAYER_DEFS
|
|
38
|
+
.map(def => {
|
|
39
|
+
const matching = allValues.filter(v => v >= def.min && v <= def.max);
|
|
40
|
+
if (!matching.length) return null;
|
|
41
|
+
const elements = matching.flatMap(v => byValue.get(v).map(elLabel));
|
|
42
|
+
return {
|
|
43
|
+
name: def.name,
|
|
44
|
+
range: [Math.min(...matching), Math.max(...matching)],
|
|
45
|
+
elements,
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
|
|
50
|
+
// Detect issues
|
|
51
|
+
const issues = [];
|
|
52
|
+
const highValues = allValues.filter(v => v > 9999);
|
|
53
|
+
if (highValues.length) {
|
|
54
|
+
issues.push({ type: 'excessive', message: `Very high z-index values: ${highValues.join(', ')}` });
|
|
55
|
+
}
|
|
56
|
+
if (allValues.length >= 5) {
|
|
57
|
+
const spread = allValues[allValues.length - 1] - allValues[0];
|
|
58
|
+
const density = allValues.length / (spread || 1);
|
|
59
|
+
if (density > 0.3) {
|
|
60
|
+
issues.push({ type: 'z-index-war', message: `${allValues.length} unique values in a narrow range (${allValues[0]}-${allValues[allValues.length - 1]})` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { layers, allValues, issues, scale };
|
|
65
|
+
}
|
package/src/formatters/figma.js
CHANGED
|
@@ -1,76 +1,95 @@
|
|
|
1
1
|
// Figma Variables JSON format (compatible with Figma Variables import)
|
|
2
2
|
export function formatFigma(design) {
|
|
3
|
-
const
|
|
3
|
+
const collections = [];
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// --- Brand collection (colors with light/dark modes) ---
|
|
6
|
+
const brandVars = [];
|
|
7
|
+
const hasDarkMode = !!design.darkMode;
|
|
8
|
+
const brandModes = hasDarkMode ? ['light', 'dark'] : ['light'];
|
|
9
|
+
|
|
10
|
+
// Brand colors
|
|
6
11
|
if (design.colors.primary) {
|
|
7
|
-
|
|
12
|
+
const v = { name: 'color/primary', type: 'COLOR', values: { light: colorVal(design.colors.primary.hex) } };
|
|
13
|
+
if (hasDarkMode && design.darkMode.colors.primary) v.values.dark = colorVal(design.darkMode.colors.primary.hex);
|
|
14
|
+
else if (hasDarkMode) v.values.dark = v.values.light;
|
|
15
|
+
brandVars.push(v);
|
|
8
16
|
}
|
|
9
17
|
if (design.colors.secondary) {
|
|
10
|
-
|
|
18
|
+
const v = { name: 'color/secondary', type: 'COLOR', values: { light: colorVal(design.colors.secondary.hex) } };
|
|
19
|
+
if (hasDarkMode && design.darkMode.colors.secondary) v.values.dark = colorVal(design.darkMode.colors.secondary.hex);
|
|
20
|
+
else if (hasDarkMode) v.values.dark = v.values.light;
|
|
21
|
+
brandVars.push(v);
|
|
11
22
|
}
|
|
12
23
|
if (design.colors.accent) {
|
|
13
|
-
|
|
24
|
+
const v = { name: 'color/accent', type: 'COLOR', values: { light: colorVal(design.colors.accent.hex) } };
|
|
25
|
+
if (hasDarkMode && design.darkMode.colors.accent) v.values.dark = v.values.light;
|
|
26
|
+
brandVars.push(v);
|
|
14
27
|
}
|
|
28
|
+
|
|
29
|
+
// Neutrals
|
|
15
30
|
for (let i = 0; i < design.colors.neutrals.length && i < 10; i++) {
|
|
16
|
-
|
|
31
|
+
const label = i * 100 || 50;
|
|
32
|
+
const v = { name: `color/neutral/${label}`, type: 'COLOR', values: { light: colorVal(design.colors.neutrals[i].hex) } };
|
|
33
|
+
if (hasDarkMode && design.darkMode.colors.neutrals[i]) v.values.dark = colorVal(design.darkMode.colors.neutrals[i].hex);
|
|
34
|
+
else if (hasDarkMode) v.values.dark = v.values.light;
|
|
35
|
+
brandVars.push(v);
|
|
17
36
|
}
|
|
37
|
+
|
|
38
|
+
// Semantic colors (backgrounds, text)
|
|
18
39
|
for (let i = 0; i < design.colors.backgrounds.length; i++) {
|
|
19
|
-
|
|
40
|
+
const label = i === 0 ? 'default' : `${i}`;
|
|
41
|
+
const v = { name: `color/background/${label}`, type: 'COLOR', values: { light: colorVal(design.colors.backgrounds[i]) } };
|
|
42
|
+
if (hasDarkMode && design.darkMode.colors.backgrounds[i]) v.values.dark = colorVal(design.darkMode.colors.backgrounds[i]);
|
|
43
|
+
else if (hasDarkMode) v.values.dark = v.values.light;
|
|
44
|
+
brandVars.push(v);
|
|
20
45
|
}
|
|
21
46
|
for (let i = 0; i < design.colors.text.length && i < 5; i++) {
|
|
22
|
-
|
|
47
|
+
const label = i === 0 ? 'default' : `${i}`;
|
|
48
|
+
const v = { name: `color/text/${label}`, type: 'COLOR', values: { light: colorVal(design.colors.text[i]) } };
|
|
49
|
+
if (hasDarkMode && design.darkMode.colors.text[i]) v.values.dark = colorVal(design.darkMode.colors.text[i]);
|
|
50
|
+
else if (hasDarkMode) v.values.dark = v.values.light;
|
|
51
|
+
brandVars.push(v);
|
|
23
52
|
}
|
|
24
53
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
collections.push({ name: 'Brand', modes: brandModes, variables: brandVars });
|
|
55
|
+
|
|
56
|
+
// --- Typography collection ---
|
|
57
|
+
const typoVars = [];
|
|
58
|
+
for (const s of design.typography.scale.slice(0, 12)) {
|
|
59
|
+
typoVars.push({ name: `font/size/${s.size}`, type: 'FLOAT', values: { default: s.size } });
|
|
60
|
+
if (s.weight) {
|
|
61
|
+
typoVars.push({ name: `font/weight/${s.size}`, type: 'FLOAT', values: { default: parseInt(s.weight) || 400 } });
|
|
62
|
+
}
|
|
63
|
+
if (s.lineHeight && s.lineHeight !== 'normal') {
|
|
64
|
+
const lh = parseFloat(s.lineHeight);
|
|
65
|
+
if (!isNaN(lh)) {
|
|
66
|
+
typoVars.push({ name: `font/lineHeight/${s.size}`, type: 'FLOAT', values: { default: lh } });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (typoVars.length > 0) {
|
|
71
|
+
collections.push({ name: 'Typography', modes: ['default'], variables: typoVars });
|
|
28
72
|
}
|
|
29
73
|
|
|
74
|
+
// --- Spacing collection ---
|
|
75
|
+
const spacingVars = [];
|
|
76
|
+
for (const v of design.spacing.scale.slice(0, 20)) {
|
|
77
|
+
spacingVars.push({ name: `spacing/${v}`, type: 'FLOAT', values: { default: v } });
|
|
78
|
+
}
|
|
30
79
|
// Border radius
|
|
31
80
|
for (const r of design.borders.radii) {
|
|
32
|
-
|
|
81
|
+
spacingVars.push({ name: `radius/${r.label}`, type: 'FLOAT', values: { default: r.value } });
|
|
33
82
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for (const s of design.typography.scale.slice(0, 12)) {
|
|
37
|
-
variables.push({ name: `fontSize/${s.size}`, type: 'FLOAT', value: s.size, scopes: ['FONT_SIZE'] });
|
|
83
|
+
if (spacingVars.length > 0) {
|
|
84
|
+
collections.push({ name: 'Spacing', modes: ['default'], variables: spacingVars });
|
|
38
85
|
}
|
|
39
86
|
|
|
40
|
-
|
|
41
|
-
name: `Design Language — ${design.meta.title || 'Extracted'}`,
|
|
42
|
-
modes: [{ name: 'Default', variables }],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Add dark mode if available
|
|
46
|
-
if (design.darkMode) {
|
|
47
|
-
const darkVars = [];
|
|
48
|
-
const dc = design.darkMode.colors;
|
|
49
|
-
if (dc.primary) darkVars.push(colorVar('color/primary', dc.primary.hex));
|
|
50
|
-
if (dc.secondary) darkVars.push(colorVar('color/secondary', dc.secondary.hex));
|
|
51
|
-
for (let i = 0; i < dc.neutrals.length && i < 10; i++) {
|
|
52
|
-
darkVars.push(colorVar(`color/neutral/${i * 100 || 50}`, dc.neutrals[i].hex));
|
|
53
|
-
}
|
|
54
|
-
for (let i = 0; i < dc.backgrounds.length; i++) {
|
|
55
|
-
darkVars.push(colorVar(`color/background/${i === 0 ? 'default' : i}`, dc.backgrounds[i]));
|
|
56
|
-
}
|
|
57
|
-
for (let i = 0; i < dc.text.length && i < 5; i++) {
|
|
58
|
-
darkVars.push(colorVar(`color/text/${i === 0 ? 'default' : i}`, dc.text[i]));
|
|
59
|
-
}
|
|
60
|
-
collection.modes.push({ name: 'Dark', variables: darkVars });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return JSON.stringify(collection, null, 2);
|
|
87
|
+
return JSON.stringify({ collections }, null, 2);
|
|
64
88
|
}
|
|
65
89
|
|
|
66
|
-
function
|
|
90
|
+
function colorVal(hex) {
|
|
67
91
|
const rgb = hexToRgb(hex);
|
|
68
|
-
return {
|
|
69
|
-
name,
|
|
70
|
-
type: 'COLOR',
|
|
71
|
-
value: { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255, a: 1 },
|
|
72
|
-
scopes: ['ALL_SCOPES'],
|
|
73
|
-
};
|
|
92
|
+
return { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255, a: 1 };
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
function hexToRgb(hex) {
|
|
@@ -500,6 +500,104 @@ export function formatMarkdown(design) {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// ── Gradients ──
|
|
504
|
+
if (design.gradients && design.gradients.count > 0) {
|
|
505
|
+
lines.push('## Gradients');
|
|
506
|
+
lines.push('');
|
|
507
|
+
lines.push(`**${design.gradients.count} unique gradients** detected.`);
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push('| Type | Direction | Stops | Classification |');
|
|
510
|
+
lines.push('|------|-----------|-------|----------------|');
|
|
511
|
+
for (const g of design.gradients.gradients.slice(0, 15)) {
|
|
512
|
+
lines.push(`| ${g.type} | ${g.direction || '—'} | ${g.stops.length} | ${g.classification} |`);
|
|
513
|
+
}
|
|
514
|
+
lines.push('');
|
|
515
|
+
lines.push('```css');
|
|
516
|
+
for (const g of design.gradients.gradients.slice(0, 5)) {
|
|
517
|
+
lines.push(`background: ${g.raw};`);
|
|
518
|
+
}
|
|
519
|
+
lines.push('```');
|
|
520
|
+
lines.push('');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Z-Index Map ──
|
|
524
|
+
if (design.zIndex && design.zIndex.allValues.length > 0) {
|
|
525
|
+
lines.push('## Z-Index Map');
|
|
526
|
+
lines.push('');
|
|
527
|
+
lines.push(`**${design.zIndex.allValues.length} unique z-index values** across ${design.zIndex.layers.length} layers.`);
|
|
528
|
+
lines.push('');
|
|
529
|
+
if (design.zIndex.layers.length > 0) {
|
|
530
|
+
lines.push('| Layer | Range | Elements |');
|
|
531
|
+
lines.push('|-------|-------|----------|');
|
|
532
|
+
for (const l of design.zIndex.layers) {
|
|
533
|
+
const elNames = l.elements.slice(0, 3).join(', ');
|
|
534
|
+
lines.push(`| ${l.name} | ${l.range} | ${elNames} |`);
|
|
535
|
+
}
|
|
536
|
+
lines.push('');
|
|
537
|
+
}
|
|
538
|
+
if (design.zIndex.issues.length > 0) {
|
|
539
|
+
lines.push('**Issues:**');
|
|
540
|
+
for (const issue of design.zIndex.issues) {
|
|
541
|
+
lines.push(`- ${issue}`);
|
|
542
|
+
}
|
|
543
|
+
lines.push('');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Icons ──
|
|
548
|
+
if (design.icons && design.icons.count > 0) {
|
|
549
|
+
lines.push('## SVG Icons');
|
|
550
|
+
lines.push('');
|
|
551
|
+
lines.push(`**${design.icons.count} unique SVG icons** detected. Dominant style: **${design.icons.dominantStyle || 'mixed'}**.`);
|
|
552
|
+
lines.push('');
|
|
553
|
+
const dist = design.icons.sizeDistribution;
|
|
554
|
+
if (dist) {
|
|
555
|
+
lines.push('| Size Class | Count |');
|
|
556
|
+
lines.push('|------------|-------|');
|
|
557
|
+
for (const [cls, count] of Object.entries(dist)) {
|
|
558
|
+
if (count > 0) lines.push(`| ${cls} | ${count} |`);
|
|
559
|
+
}
|
|
560
|
+
lines.push('');
|
|
561
|
+
}
|
|
562
|
+
if (design.icons.colorPalette.length > 0) {
|
|
563
|
+
lines.push(`**Icon colors:** ${design.icons.colorPalette.slice(0, 10).map(c => `\`${c}\``).join(', ')}`);
|
|
564
|
+
lines.push('');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Font Files ──
|
|
569
|
+
if (design.fonts && design.fonts.fonts.length > 0) {
|
|
570
|
+
lines.push('## Font Files');
|
|
571
|
+
lines.push('');
|
|
572
|
+
lines.push('| Family | Source | Weights | Styles |');
|
|
573
|
+
lines.push('|--------|--------|---------|--------|');
|
|
574
|
+
for (const f of design.fonts.fonts) {
|
|
575
|
+
lines.push(`| ${f.family} | ${f.source} | ${f.weights.join(', ')} | ${f.styles.join(', ')} |`);
|
|
576
|
+
}
|
|
577
|
+
lines.push('');
|
|
578
|
+
if (design.fonts.googleFontsUrl) {
|
|
579
|
+
lines.push(`**Google Fonts URL:** \`${design.fonts.googleFontsUrl}\``);
|
|
580
|
+
lines.push('');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Image Styles ──
|
|
585
|
+
if (design.images && design.images.patterns.length > 0) {
|
|
586
|
+
lines.push('## Image Style Patterns');
|
|
587
|
+
lines.push('');
|
|
588
|
+
lines.push('| Pattern | Count | Key Styles |');
|
|
589
|
+
lines.push('|---------|-------|------------|');
|
|
590
|
+
for (const p of design.images.patterns) {
|
|
591
|
+
const styles = Object.entries(p.styles || {}).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
592
|
+
lines.push(`| ${p.name} | ${p.count} | ${styles || '—'} |`);
|
|
593
|
+
}
|
|
594
|
+
lines.push('');
|
|
595
|
+
if (design.images.aspectRatios.length > 0) {
|
|
596
|
+
lines.push(`**Aspect ratios:** ${design.images.aspectRatios.slice(0, 8).map(a => `${a.ratio} (${a.count}x)`).join(', ')}`);
|
|
597
|
+
lines.push('');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
503
601
|
// ── Quick Start ──
|
|
504
602
|
lines.push('## Quick Start');
|
|
505
603
|
lines.push('');
|
|
@@ -9,39 +9,59 @@ export function formatPreview(design) {
|
|
|
9
9
|
<title>Design Language: ${esc(meta.title)}</title>
|
|
10
10
|
<style>
|
|
11
11
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
-
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0a0a0a; --bg-card: #141414; --border: #222; --text: #e5e5e5; --text-heading: #fff;
|
|
14
|
+
--text-muted: #666; --text-sub: #a0a0a0; --text-dim: #444; --text-faint: #555; --row-border: #1a1a1a;
|
|
15
|
+
}
|
|
16
|
+
[data-theme="light"] {
|
|
17
|
+
--bg: #f5f5f5; --bg-card: #fff; --border: #ddd; --text: #333; --text-heading: #111;
|
|
18
|
+
--text-muted: #888; --text-sub: #666; --text-dim: #999; --text-faint: #aaa; --row-border: #e5e5e5;
|
|
19
|
+
}
|
|
20
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
|
|
13
21
|
.container { max-width: 1200px; margin: 0 auto; padding: 40px 24px; }
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
.header-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
|
23
|
+
.theme-toggle { background: var(--bg-card); border: 1px solid var(--border); color: var(--text); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; white-space: nowrap; }
|
|
24
|
+
h1 { font-size: 36px; font-weight: 700; margin-bottom: 8px; color: var(--text-heading); }
|
|
25
|
+
h2 { font-size: 24px; font-weight: 600; margin: 48px 0 20px; color: var(--text-heading); border-bottom: 1px solid var(--border); padding-bottom: 12px; }
|
|
26
|
+
h3 { font-size: 16px; font-weight: 600; margin: 24px 0 12px; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
27
|
+
.meta { color: var(--text-muted); font-size: 14px; margin-bottom: 32px; }
|
|
18
28
|
.meta span { margin-right: 16px; }
|
|
19
29
|
.grid { display: grid; gap: 12px; }
|
|
20
30
|
.grid-2 { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
|
|
21
31
|
.grid-3 { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
|
22
32
|
.grid-4 { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
|
|
33
|
+
@media (max-width: 600px) {
|
|
34
|
+
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr 1fr; }
|
|
35
|
+
h1 { font-size: 24px; }
|
|
36
|
+
.container { padding: 20px 12px; }
|
|
37
|
+
}
|
|
38
|
+
@media (max-width: 380px) {
|
|
39
|
+
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
|
40
|
+
}
|
|
23
41
|
|
|
24
42
|
/* Color swatches */
|
|
25
|
-
.swatch { border-radius: 12px; overflow: hidden; background:
|
|
43
|
+
.swatch { border-radius: 12px; overflow: hidden; background: var(--bg-card); border: 1px solid var(--border); cursor: pointer; position: relative; }
|
|
26
44
|
.swatch-color { height: 80px; position: relative; }
|
|
27
45
|
.swatch-info { padding: 10px 12px; font-size: 13px; }
|
|
28
|
-
.swatch-hex { font-weight: 600; font-family: monospace; color:
|
|
29
|
-
.swatch-label { font-size: 11px; color:
|
|
30
|
-
.swatch-role { display: inline-block; font-size: 10px; background:
|
|
46
|
+
.swatch-hex { font-weight: 600; font-family: monospace; color: var(--text-heading); }
|
|
47
|
+
.swatch-label { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
48
|
+
.swatch-role { display: inline-block; font-size: 10px; background: var(--border); color: var(--text-sub); padding: 2px 8px; border-radius: 4px; margin-top: 4px; }
|
|
49
|
+
.copied-tip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: #fff; padding: 4px 12px; border-radius: 6px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.2s; }
|
|
50
|
+
.copied-tip.show { opacity: 1; }
|
|
31
51
|
|
|
32
52
|
/* Type scale */
|
|
33
|
-
.type-row { display: flex; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px solid
|
|
34
|
-
.type-size { font-family: monospace; color:
|
|
35
|
-
.type-meta { font-size: 12px; color:
|
|
53
|
+
.type-row { display: flex; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--row-border); }
|
|
54
|
+
.type-size { font-family: monospace; color: var(--text-muted); min-width: 60px; font-size: 13px; }
|
|
55
|
+
.type-meta { font-size: 12px; color: var(--text-dim); margin-left: auto; font-family: monospace; }
|
|
36
56
|
|
|
37
57
|
/* Spacing */
|
|
38
58
|
.spacing-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
|
39
59
|
.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:
|
|
60
|
+
.spacing-label { font-family: monospace; font-size: 13px; color: var(--text-sub); min-width: 60px; }
|
|
41
61
|
|
|
42
62
|
/* Shadows */
|
|
43
|
-
.shadow-card { background:
|
|
44
|
-
.shadow-label { font-size: 12px; color:
|
|
63
|
+
.shadow-card { background: var(--bg-card); border-radius: 12px; padding: 24px; text-align: center; min-height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid var(--border); }
|
|
64
|
+
.shadow-label { font-size: 12px; color: var(--text); font-family: monospace; }
|
|
45
65
|
|
|
46
66
|
/* Radii */
|
|
47
67
|
.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; }
|
|
@@ -51,7 +71,7 @@ export function formatPreview(design) {
|
|
|
51
71
|
.a11y-score.good { color: #22c55e; }
|
|
52
72
|
.a11y-score.warn { color: #eab308; }
|
|
53
73
|
.a11y-score.bad { color: #ef4444; }
|
|
54
|
-
.a11y-pair { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background:
|
|
74
|
+
.a11y-pair { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--bg-card); border-radius: 8px; margin-bottom: 6px; border: 1px solid var(--border); flex-wrap: wrap; }
|
|
55
75
|
.a11y-sample { width: 120px; padding: 6px 12px; border-radius: 6px; text-align: center; font-size: 14px; font-weight: 500; }
|
|
56
76
|
.a11y-ratio { font-family: monospace; font-size: 14px; min-width: 50px; }
|
|
57
77
|
.a11y-badge { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
|
|
@@ -59,21 +79,25 @@ export function formatPreview(design) {
|
|
|
59
79
|
.a11y-badge.fail { background: #ef444420; color: #ef4444; }
|
|
60
80
|
|
|
61
81
|
/* Components */
|
|
62
|
-
.comp-screenshot { border-radius: 8px; border: 1px solid
|
|
82
|
+
.comp-screenshot { border-radius: 8px; border: 1px solid var(--border); max-width: 100%; }
|
|
63
83
|
|
|
64
84
|
/* Stat cards */
|
|
65
85
|
.stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin: 24px 0; }
|
|
66
|
-
.stat { background:
|
|
67
|
-
.stat-value { font-size: 28px; font-weight: 700; color:
|
|
68
|
-
.stat-label { font-size: 12px; color:
|
|
86
|
+
.stat { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
|
|
87
|
+
.stat-value { font-size: 28px; font-weight: 700; color: var(--text-heading); }
|
|
88
|
+
.stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
|
69
89
|
|
|
70
90
|
.font-tag { display: inline-block; background: #1e1e2e; color: #a78bfa; padding: 4px 10px; border-radius: 6px; font-size: 13px; margin: 4px 4px 4px 0; }
|
|
91
|
+
[data-theme="light"] .font-tag { background: #ede9fe; color: #6d28d9; }
|
|
71
92
|
</style>
|
|
72
93
|
</head>
|
|
73
94
|
<body>
|
|
74
95
|
<div class="container">
|
|
75
96
|
|
|
76
|
-
<
|
|
97
|
+
<div class="header-row">
|
|
98
|
+
<h1>${esc(meta.title)}</h1>
|
|
99
|
+
<button class="theme-toggle" onclick="toggleTheme()">Toggle Light/Dark</button>
|
|
100
|
+
</div>
|
|
77
101
|
<div class="meta">
|
|
78
102
|
<span>${esc(meta.url)}</span>
|
|
79
103
|
<span>${meta.elementCount} elements</span>
|
|
@@ -135,7 +159,7 @@ ${typography.scale.length > 0 ? `
|
|
|
135
159
|
${typography.scale.slice(0, 12).map(s => `
|
|
136
160
|
<div class="type-row">
|
|
137
161
|
<span class="type-size">${s.size}px</span>
|
|
138
|
-
<span style="font-size:${Math.min(s.size, 48)}px;font-weight:${s.weight};color
|
|
162
|
+
<span style="font-size:${Math.min(s.size, 48)}px;font-weight:${s.weight};color:var(--text-heading)">The quick brown fox</span>
|
|
139
163
|
<span class="type-meta">${s.weight} / ${s.lineHeight}</span>
|
|
140
164
|
</div>`).join('')}
|
|
141
165
|
</div>` : ''}
|
|
@@ -206,6 +230,25 @@ ${componentScreenshots && Object.keys(componentScreenshots).length > 0 ? `
|
|
|
206
230
|
</div>` : ''}
|
|
207
231
|
|
|
208
232
|
</div>
|
|
233
|
+
<script>
|
|
234
|
+
function toggleTheme() {
|
|
235
|
+
const html = document.documentElement;
|
|
236
|
+
const current = html.getAttribute('data-theme');
|
|
237
|
+
html.setAttribute('data-theme', current === 'light' ? 'dark' : 'light');
|
|
238
|
+
}
|
|
239
|
+
document.querySelectorAll('.swatch').forEach(el => {
|
|
240
|
+
el.addEventListener('click', () => {
|
|
241
|
+
const hex = el.querySelector('.swatch-hex');
|
|
242
|
+
if (!hex) return;
|
|
243
|
+
navigator.clipboard.writeText(hex.textContent).then(() => {
|
|
244
|
+
let tip = el.querySelector('.copied-tip');
|
|
245
|
+
if (!tip) { tip = document.createElement('div'); tip.className = 'copied-tip'; tip.textContent = 'Copied!'; el.appendChild(tip); }
|
|
246
|
+
tip.classList.add('show');
|
|
247
|
+
setTimeout(() => tip.classList.remove('show'), 1200);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
</script>
|
|
209
252
|
</body>
|
|
210
253
|
</html>`;
|
|
211
254
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function formatSvelteTheme(design) {
|
|
2
|
+
const { colors, typography, spacing, borders } = design;
|
|
3
|
+
const lines = [];
|
|
4
|
+
|
|
5
|
+
lines.push('/* Svelte theme — generated by designlang */');
|
|
6
|
+
lines.push('/* Import in +layout.svelte or app.css */');
|
|
7
|
+
lines.push('');
|
|
8
|
+
lines.push(':root {');
|
|
9
|
+
lines.push(' /* Colors */');
|
|
10
|
+
if (colors.primary) lines.push(` --color-primary: ${colors.primary.hex};`);
|
|
11
|
+
if (colors.secondary) lines.push(` --color-secondary: ${colors.secondary.hex};`);
|
|
12
|
+
if (colors.accent) lines.push(` --color-accent: ${colors.accent.hex};`);
|
|
13
|
+
for (const [i, n] of colors.neutrals.slice(0, 10).entries()) {
|
|
14
|
+
lines.push(` --color-neutral-${(i + 1) * 100}: ${n.hex};`);
|
|
15
|
+
}
|
|
16
|
+
if (colors.backgrounds.length > 0) lines.push(` --color-background: ${colors.backgrounds[0]};`);
|
|
17
|
+
if (colors.text.length > 0) lines.push(` --color-text: ${colors.text[0]};`);
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push(' /* Typography */');
|
|
20
|
+
if (typography.families.length > 0) lines.push(` --font-primary: '${typography.families[0].name}', sans-serif;`);
|
|
21
|
+
if (typography.families.length > 1) lines.push(` --font-secondary: '${typography.families[1].name}', sans-serif;`);
|
|
22
|
+
if (typography.body) lines.push(` --font-size-base: ${typography.body.size}px;`);
|
|
23
|
+
for (const s of typography.scale.slice(0, 8)) {
|
|
24
|
+
lines.push(` --font-size-${s.size}: ${s.size}px;`);
|
|
25
|
+
}
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(' /* Spacing */');
|
|
28
|
+
if (spacing.base) lines.push(` --spacing-base: ${spacing.base}px;`);
|
|
29
|
+
for (const [i, val] of spacing.scale.slice(0, 12).entries()) {
|
|
30
|
+
lines.push(` --spacing-${i + 1}: ${val}px;`);
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push(' /* Border Radii */');
|
|
34
|
+
for (const r of borders.radii) {
|
|
35
|
+
lines.push(` --radius-${r.label}: ${r.value}px;`);
|
|
36
|
+
}
|
|
37
|
+
lines.push('}');
|
|
38
|
+
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
import { rgbToHex, rgbToHsl } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
function generateColorScale(hex, parsed) {
|
|
4
|
+
const { h, s } = rgbToHsl(parsed);
|
|
5
|
+
const scale = {};
|
|
6
|
+
const levels = [
|
|
7
|
+
{ name: '50', l: 97 }, { name: '100', l: 94 }, { name: '200', l: 86 },
|
|
8
|
+
{ name: '300', l: 76 }, { name: '400', l: 64 }, { name: '500', l: 50 },
|
|
9
|
+
{ name: '600', l: 40 }, { name: '700', l: 32 }, { name: '800', l: 24 },
|
|
10
|
+
{ name: '900', l: 16 }, { name: '950', l: 10 },
|
|
11
|
+
];
|
|
12
|
+
for (const { name, l } of levels) {
|
|
13
|
+
scale[name] = `hsl(${h}, ${s}%, ${l}%)`;
|
|
14
|
+
}
|
|
15
|
+
return scale;
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
export function formatTailwind(design) {
|
|
2
19
|
const config = {
|
|
3
20
|
colors: {},
|
|
@@ -12,10 +29,19 @@ export function formatTailwind(design) {
|
|
|
12
29
|
transitionTimingFunction: {},
|
|
13
30
|
};
|
|
14
31
|
|
|
15
|
-
// Colors
|
|
16
|
-
if (design.colors.primary)
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
// Colors — generate full scales from brand colors
|
|
33
|
+
if (design.colors.primary) {
|
|
34
|
+
config.colors.primary = generateColorScale(design.colors.primary.hex, design.colors.primary);
|
|
35
|
+
config.colors.primary.DEFAULT = design.colors.primary.hex;
|
|
36
|
+
}
|
|
37
|
+
if (design.colors.secondary) {
|
|
38
|
+
config.colors.secondary = generateColorScale(design.colors.secondary.hex, design.colors.secondary);
|
|
39
|
+
config.colors.secondary.DEFAULT = design.colors.secondary.hex;
|
|
40
|
+
}
|
|
41
|
+
if (design.colors.accent) {
|
|
42
|
+
config.colors.accent = generateColorScale(design.colors.accent.hex, design.colors.accent);
|
|
43
|
+
config.colors.accent.DEFAULT = design.colors.accent.hex;
|
|
44
|
+
}
|
|
19
45
|
for (let i = 0; i < design.colors.neutrals.length && i < 10; i++) {
|
|
20
46
|
config.colors[`neutral-${i * 100 || 50}`] = design.colors.neutrals[i].hex;
|
|
21
47
|
}
|
|
@@ -60,6 +86,33 @@ export function formatTailwind(design) {
|
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
89
|
+
// Animations
|
|
90
|
+
if (design.animations) {
|
|
91
|
+
if (design.animations.durations.length > 0) {
|
|
92
|
+
config.transitionDuration = {};
|
|
93
|
+
for (const d of design.animations.durations) {
|
|
94
|
+
const ms = d.endsWith('ms') ? parseInt(d) : parseFloat(d) * 1000;
|
|
95
|
+
config.transitionDuration[`${ms}`] = d;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (design.animations.easings.length > 0) {
|
|
99
|
+
config.transitionTimingFunction = {};
|
|
100
|
+
for (const e of design.animations.easings) {
|
|
101
|
+
const val = typeof e === 'object' ? e.value : e;
|
|
102
|
+
const name = val.startsWith('cubic-bezier') ? 'custom' : val.replace(/ease-?/g, '').replace(/-/g, '') || 'default';
|
|
103
|
+
config.transitionTimingFunction[name] = val;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Container
|
|
109
|
+
if (design.layout && design.layout.containerWidths.length > 0) {
|
|
110
|
+
const maxW = design.layout.containerWidths[0].maxWidth;
|
|
111
|
+
const padding = design.layout.containerWidths[0].padding;
|
|
112
|
+
config.container = { center: true, padding: padding || '1rem' };
|
|
113
|
+
if (maxW) config.maxWidth = { container: maxW };
|
|
114
|
+
}
|
|
115
|
+
|
|
63
116
|
// Clean empty objects
|
|
64
117
|
for (const [key, val] of Object.entries(config)) {
|
|
65
118
|
if (typeof val === 'object' && Object.keys(val).length === 0) delete config[key];
|