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.
@@ -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
- const values = [...shadowSet].map(raw => {
11
- // Parse: offset-x offset-y blur spread color [inset]
12
- const inset = raw.includes('inset');
13
- // Approximate blur from the string for classification
14
- const nums = raw.match(/([\d.]+)px/g)?.map(n => parseFloat(n)) || [];
15
- const blur = nums[2] || 0;
16
- let label = 'md';
17
- if (blur <= 2) label = 'xs';
18
- else if (blur <= 6) label = 'sm';
19
- else if (blur <= 15) label = 'md';
20
- else if (blur <= 30) label = 'lg';
21
- else label = 'xl';
22
- return { raw, blur, inset, label };
23
- });
24
-
25
- values.sort((a, b) => a.blur - b.blur);
26
- return { values };
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, clusterValues, detectScale } from '../utils.js';
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 = clusterValues(sorted, 2);
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
- return categories;
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
  }
@@ -1,76 +1,95 @@
1
1
  // Figma Variables JSON format (compatible with Figma Variables import)
2
2
  export function formatFigma(design) {
3
- const variables = [];
3
+ const collections = [];
4
4
 
5
- // Colors
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
- variables.push(colorVar('color/primary', design.colors.primary.hex));
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
- variables.push(colorVar('color/secondary', design.colors.secondary.hex));
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
- variables.push(colorVar('color/accent', design.colors.accent.hex));
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
- variables.push(colorVar(`color/neutral/${i * 100 || 50}`, design.colors.neutrals[i].hex));
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
- variables.push(colorVar(`color/background/${i === 0 ? 'default' : i}`, design.colors.backgrounds[i]));
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
- variables.push(colorVar(`color/text/${i === 0 ? 'default' : i}`, design.colors.text[i]));
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
- // Spacing
26
- for (const v of design.spacing.scale.slice(0, 20)) {
27
- variables.push({ name: `spacing/${v}`, type: 'FLOAT', value: v, scopes: ['GAP', 'ALL_SCOPES'] });
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
- variables.push({ name: `radius/${r.label}`, type: 'FLOAT', value: r.value, scopes: ['CORNER_RADIUS'] });
81
+ spacingVars.push({ name: `radius/${r.label}`, type: 'FLOAT', values: { default: r.value } });
33
82
  }
34
-
35
- // Font sizes
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
- const collection = {
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 colorVar(name, hex) {
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
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e5e5e5; line-height: 1.6; }
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
- 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; }
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: #141414; border: 1px solid #222; }
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: #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; }
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 #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; }
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: #888; min-width: 60px; }
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: #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; }
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: #141414; border-radius: 8px; margin-bottom: 6px; border: 1px solid #222; }
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 #222; max-width: 100%; }
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: #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; }
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
- <h1>${esc(meta.title)}</h1>
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:#fff">The quick brown fox</span>
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) config.colors.primary = design.colors.primary.hex;
17
- if (design.colors.secondary) config.colors.secondary = design.colors.secondary.hex;
18
- if (design.colors.accent) config.colors.accent = design.colors.accent.hex;
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];