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.
Files changed (40) hide show
  1. package/README.md +66 -5
  2. package/bin/design-extract.js +269 -70
  3. package/package.json +9 -4
  4. package/src/apply.js +65 -0
  5. package/src/config.js +36 -0
  6. package/src/crawler.js +247 -82
  7. package/src/darkdiff.js +65 -0
  8. package/src/extractors/animations.js +76 -8
  9. package/src/extractors/borders.js +40 -5
  10. package/src/extractors/components.js +100 -1
  11. package/src/extractors/fonts.js +82 -0
  12. package/src/extractors/gradients.js +100 -0
  13. package/src/extractors/icons.js +80 -0
  14. package/src/extractors/images.js +76 -0
  15. package/src/extractors/shadows.js +60 -17
  16. package/src/extractors/spacing.js +31 -2
  17. package/src/extractors/variables.js +20 -1
  18. package/src/extractors/zindex.js +65 -0
  19. package/src/formatters/figma.js +66 -47
  20. package/src/formatters/markdown.js +98 -0
  21. package/src/formatters/preview.js +65 -22
  22. package/src/formatters/svelte-theme.js +40 -0
  23. package/src/formatters/tailwind.js +57 -4
  24. package/src/formatters/theme.js +134 -0
  25. package/src/formatters/vue-theme.js +44 -0
  26. package/src/formatters/wordpress.js +84 -0
  27. package/src/history.js +8 -1
  28. package/src/index.js +54 -16
  29. package/src/utils.js +68 -0
  30. package/tests/cli.test.js +34 -0
  31. package/tests/extractors.test.js +661 -0
  32. package/tests/formatters.test.js +477 -0
  33. package/tests/utils.test.js +413 -0
  34. package/website/app/api/extract/route.js +85 -0
  35. package/website/app/components/Extractor.js +184 -0
  36. package/website/app/globals.css +291 -0
  37. package/website/app/page.js +13 -0
  38. package/website/next.config.mjs +10 -1
  39. package/website/package-lock.json +356 -0
  40. 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
+ }
@@ -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) {
@@ -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
- 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];