designlang 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -5
- package/bin/design-extract.js +215 -91
- package/package.json +9 -4
- package/src/config.js +36 -0
- package/src/crawler.js +181 -95
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/components.js +77 -1
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/variables.js +20 -1
- package/src/formatters/figma.js +66 -47
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +84 -0
- package/src/history.js +8 -1
- package/src/index.js +46 -20
- package/src/utils.js +68 -0
- package/tests/cli.test.js +34 -0
- package/tests/extractors.test.js +661 -0
- package/tests/formatters.test.js +477 -0
- package/tests/utils.test.js +413 -0
|
@@ -1,27 +1,70 @@
|
|
|
1
1
|
export function extractShadows(computedStyles) {
|
|
2
2
|
const shadowSet = new Set();
|
|
3
|
+
const textShadowSet = new Set();
|
|
3
4
|
|
|
4
5
|
for (const el of computedStyles) {
|
|
5
6
|
if (el.boxShadow && el.boxShadow !== 'none') {
|
|
6
7
|
shadowSet.add(el.boxShadow);
|
|
7
8
|
}
|
|
9
|
+
if (el.textShadow && el.textShadow !== 'none') {
|
|
10
|
+
textShadowSet.add(el.textShadow);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const values = [...shadowSet].map(raw => parseShadow(raw));
|
|
15
|
+
values.sort((a, b) => a.visualWeight - b.visualWeight);
|
|
16
|
+
|
|
17
|
+
const textShadows = [...textShadowSet].map(raw => parseShadow(raw));
|
|
18
|
+
textShadows.sort((a, b) => a.visualWeight - b.visualWeight);
|
|
19
|
+
|
|
20
|
+
return { values, textShadows };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseShadow(raw) {
|
|
24
|
+
const inset = raw.includes('inset');
|
|
25
|
+
const cleaned = raw.replace(/\binset\b/g, '').trim();
|
|
26
|
+
|
|
27
|
+
// Extract color (rgb/rgba/hsl/hsla or named/hex) — find the non-numeric portion
|
|
28
|
+
let color = '';
|
|
29
|
+
let numericPart = cleaned;
|
|
30
|
+
// Match color functions first (they may contain commas/numbers)
|
|
31
|
+
const colorFnMatch = cleaned.match(/(rgba?\([^)]+\)|hsla?\([^)]+\))/);
|
|
32
|
+
if (colorFnMatch) {
|
|
33
|
+
color = colorFnMatch[1];
|
|
34
|
+
numericPart = cleaned.replace(color, '').trim();
|
|
35
|
+
} else {
|
|
36
|
+
// Try hex or named color at start or end
|
|
37
|
+
const hexMatch = cleaned.match(/(#[0-9a-fA-F]{3,8})/);
|
|
38
|
+
if (hexMatch) {
|
|
39
|
+
color = hexMatch[1];
|
|
40
|
+
numericPart = cleaned.replace(color, '').trim();
|
|
41
|
+
} else {
|
|
42
|
+
// Named color — typically last or first token that isn't a length
|
|
43
|
+
const tokens = cleaned.split(/\s+/);
|
|
44
|
+
const colorToken = tokens.find(t => !/^-?[\d.]+px$/.test(t) && !/^[\d.]+$/.test(t));
|
|
45
|
+
if (colorToken) {
|
|
46
|
+
color = colorToken;
|
|
47
|
+
numericPart = cleaned.replace(colorToken, '').trim();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
8
50
|
}
|
|
9
51
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
// Parse numeric values (offset-x, offset-y, blur, spread)
|
|
53
|
+
const nums = numericPart.match(/-?[\d.]+px/g)?.map(n => parseFloat(n)) || [];
|
|
54
|
+
const offsetX = nums[0] || 0;
|
|
55
|
+
const offsetY = nums[1] || 0;
|
|
56
|
+
const blur = nums[2] || 0;
|
|
57
|
+
const spread = nums[3] || 0;
|
|
58
|
+
|
|
59
|
+
// Visual weight = distance + blur
|
|
60
|
+
const visualWeight = Math.sqrt(offsetX * offsetX + offsetY * offsetY) + blur;
|
|
61
|
+
|
|
62
|
+
let label = 'none';
|
|
63
|
+
if (visualWeight > 0 && visualWeight <= 3) label = 'xs';
|
|
64
|
+
else if (visualWeight <= 8) label = 'sm';
|
|
65
|
+
else if (visualWeight <= 16) label = 'md';
|
|
66
|
+
else if (visualWeight <= 32) label = 'lg';
|
|
67
|
+
else if (visualWeight > 32) label = 'xl';
|
|
68
|
+
|
|
69
|
+
return { raw, offsetX, offsetY, blur, spread, color, inset, visualWeight: Math.round(visualWeight * 100) / 100, label };
|
|
27
70
|
}
|
|
@@ -1,4 +1,33 @@
|
|
|
1
|
-
import { parseCSSValue,
|
|
1
|
+
import { parseCSSValue, detectScale } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
function naturalBreakCluster(values) {
|
|
4
|
+
if (values.length <= 1) return values;
|
|
5
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6
|
+
if (sorted.length <= 2) return sorted;
|
|
7
|
+
|
|
8
|
+
// Compute gaps between consecutive values
|
|
9
|
+
const gaps = [];
|
|
10
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
11
|
+
gaps.push(sorted[i] - sorted[i - 1]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Find median gap
|
|
15
|
+
const sortedGaps = [...gaps].sort((a, b) => a - b);
|
|
16
|
+
const medianGap = sortedGaps[Math.floor(sortedGaps.length / 2)];
|
|
17
|
+
|
|
18
|
+
// Split into clusters at gaps larger than the median
|
|
19
|
+
const clusters = [[sorted[0]]];
|
|
20
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
21
|
+
if (gaps[i - 1] > medianGap) {
|
|
22
|
+
clusters.push([sorted[i]]);
|
|
23
|
+
} else {
|
|
24
|
+
clusters[clusters.length - 1].push(sorted[i]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Use the first (smallest) value in each cluster as representative
|
|
29
|
+
return clusters.map(c => c[0]);
|
|
30
|
+
}
|
|
2
31
|
|
|
3
32
|
export function extractSpacing(computedStyles) {
|
|
4
33
|
const allValues = new Set();
|
|
@@ -13,7 +42,7 @@ export function extractSpacing(computedStyles) {
|
|
|
13
42
|
}
|
|
14
43
|
|
|
15
44
|
const sorted = [...allValues].sort((a, b) => a - b);
|
|
16
|
-
const clustered =
|
|
45
|
+
const clustered = naturalBreakCluster(sorted);
|
|
17
46
|
const { base, scale } = detectScale(clustered);
|
|
18
47
|
|
|
19
48
|
// Build named tokens
|
|
@@ -18,5 +18,24 @@ export function extractVariables(cssVariables) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Build dependency map: which variables reference other variables
|
|
22
|
+
const dependencies = {};
|
|
23
|
+
for (const [name, value] of Object.entries(cssVariables)) {
|
|
24
|
+
const refs = [...value.matchAll(/var\((--[\w-]+)/g)].map(m => m[1]);
|
|
25
|
+
if (refs.length > 0) {
|
|
26
|
+
dependencies[name] = refs;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Semantic grouping by name patterns
|
|
31
|
+
const semantic = { success: {}, warning: {}, error: {}, info: {} };
|
|
32
|
+
for (const [name, value] of Object.entries(cssVariables)) {
|
|
33
|
+
const lower = name.toLowerCase();
|
|
34
|
+
if (/success|green|valid|positive/.test(lower)) semantic.success[name] = value;
|
|
35
|
+
else if (/warning|warn|yellow|caution|amber/.test(lower)) semantic.warning[name] = value;
|
|
36
|
+
else if (/error|danger|destructive|red|invalid|negative/.test(lower)) semantic.error[name] = value;
|
|
37
|
+
else if (/info|informati|blue|notice/.test(lower)) semantic.info[name] = value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { ...categories, dependencies, semantic };
|
|
22
41
|
}
|
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) {
|
|
@@ -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];
|