designlang 5.0.0 → 7.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 (51) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +177 -6
  7. package/bin/design-extract.js +302 -92
  8. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  9. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  10. package/package.json +13 -7
  11. package/src/config.js +59 -0
  12. package/src/crawler.js +297 -95
  13. package/src/extractors/a11y-remediation.js +47 -0
  14. package/src/extractors/animations.js +37 -5
  15. package/src/extractors/borders.js +40 -5
  16. package/src/extractors/component-clusters.js +39 -0
  17. package/src/extractors/components.js +77 -1
  18. package/src/extractors/css-health.js +151 -0
  19. package/src/extractors/gradients.js +25 -5
  20. package/src/extractors/scoring.js +20 -1
  21. package/src/extractors/semantic-regions.js +44 -0
  22. package/src/extractors/shadows.js +60 -17
  23. package/src/extractors/spacing.js +31 -2
  24. package/src/extractors/stack-fingerprint.js +88 -0
  25. package/src/extractors/variables.js +20 -1
  26. package/src/formatters/_token-ref.js +44 -0
  27. package/src/formatters/agent-rules.js +116 -0
  28. package/src/formatters/android-compose.js +164 -0
  29. package/src/formatters/dtcg-tokens.js +175 -0
  30. package/src/formatters/figma.js +66 -47
  31. package/src/formatters/flutter-dart.js +130 -0
  32. package/src/formatters/ios-swiftui.js +161 -0
  33. package/src/formatters/markdown.js +25 -0
  34. package/src/formatters/preview.js +65 -22
  35. package/src/formatters/svelte-theme.js +40 -0
  36. package/src/formatters/tailwind.js +57 -4
  37. package/src/formatters/theme.js +134 -0
  38. package/src/formatters/vue-theme.js +44 -0
  39. package/src/formatters/wordpress.js +267 -0
  40. package/src/history.js +8 -1
  41. package/src/index.js +76 -20
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils.js +68 -0
  46. package/tests/cli.test.js +84 -0
  47. package/tests/extractors.test.js +792 -0
  48. package/tests/formatters.test.js +709 -0
  49. package/tests/mcp.test.js +68 -0
  50. package/tests/utils.test.js +413 -0
  51. package/website/app/globals.css +11 -11
@@ -0,0 +1,130 @@
1
+ // Flutter Dart emitter — consumes a DTCG token object and emits a Dart file
2
+ // with a DesignTokens class and a buildDesignlangTheme() ThemeData helper.
3
+
4
+ import { resolveRef } from './_token-ref.js';
5
+
6
+ const HEADER_VERSION = '7.0.0';
7
+
8
+ function* walkLeaves(node, prefix) {
9
+ if (node == null || typeof node !== 'object') return;
10
+ if ('$value' in node && '$type' in node) {
11
+ yield { path: prefix, token: node };
12
+ return;
13
+ }
14
+ for (const key of Object.keys(node)) {
15
+ yield* walkLeaves(node[key], prefix ? `${prefix}.${key}` : key);
16
+ }
17
+ }
18
+
19
+ function camelFromPath(path) {
20
+ const parts = path.split('.');
21
+ const trimmed = parts.slice(1);
22
+ let segs;
23
+ if (trimmed[0] === 'color' && trimmed.length >= 3) {
24
+ segs = trimmed.slice(1);
25
+ } else {
26
+ segs = trimmed;
27
+ }
28
+ return segs
29
+ .map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))
30
+ .join('')
31
+ .replace(/[^a-zA-Z0-9]/g, '');
32
+ }
33
+
34
+ function hexToDartArgb(hex) {
35
+ if (typeof hex !== 'string') return null;
36
+ let h = hex.trim().replace(/^#/, '');
37
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
38
+ if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) return `0xFF${h.toUpperCase()}`;
39
+ if (h.length === 8 && /^[0-9a-fA-F]{8}$/.test(h)) return `0x${h.toUpperCase()}`;
40
+ return null;
41
+ }
42
+
43
+ function dimensionToNumber(value) {
44
+ if (typeof value === 'number') return value;
45
+ if (typeof value !== 'string') return null;
46
+ const m = value.match(/^(-?\d+(?:\.\d+)?)(px|pt|dp|rem|em)?$/);
47
+ if (!m) return null;
48
+ const n = parseFloat(m[1]);
49
+ if (m[2] === 'rem' || m[2] === 'em') return n * 16;
50
+ return n;
51
+ }
52
+
53
+ export function formatFlutterDart(tokens) {
54
+ const source = tokens?.$metadata?.source || '';
55
+ const lines = [];
56
+ lines.push(`// Generated by designlang v${HEADER_VERSION}`);
57
+ if (source) lines.push(`// Source: ${source}`);
58
+ lines.push("import 'package:flutter/material.dart';");
59
+ lines.push('');
60
+ lines.push('class DesignTokens {');
61
+ lines.push(' DesignTokens._();');
62
+ lines.push('');
63
+
64
+ // Semantic colors
65
+ const semColor = tokens?.semantic?.color;
66
+ const semEntries = [];
67
+ if (semColor) {
68
+ lines.push(' // Semantic');
69
+ for (const leaf of walkLeaves(semColor, 'semantic.color')) {
70
+ if (leaf.token.$type !== 'color') continue;
71
+ const resolved = resolveRef(tokens, leaf.path);
72
+ const argb = hexToDartArgb(resolved);
73
+ if (!argb) continue;
74
+ const name = camelFromPath(leaf.path);
75
+ lines.push(` static const Color ${name} = Color(${argb});`);
76
+ semEntries.push({ name, path: leaf.path });
77
+ }
78
+ }
79
+
80
+ // Primitive spacing
81
+ const spacing = tokens?.primitive?.spacing || {};
82
+ for (const key of Object.keys(spacing)) {
83
+ const tok = spacing[key];
84
+ if (!tok || tok.$type !== 'dimension') continue;
85
+ const n = dimensionToNumber(resolveRef(tokens, `primitive.spacing.${key}`));
86
+ if (n == null) continue;
87
+ lines.push(` static const double ${camelFromPath(`primitive.spacing.${key}`)} = ${n};`);
88
+ }
89
+
90
+ // Primitive radius
91
+ const radius = tokens?.primitive?.radius || {};
92
+ for (const key of Object.keys(radius)) {
93
+ const tok = radius[key];
94
+ if (!tok || tok.$type !== 'dimension') continue;
95
+ const n = dimensionToNumber(resolveRef(tokens, `primitive.radius.${key}`));
96
+ if (n == null) continue;
97
+ lines.push(` static const double ${camelFromPath(`primitive.radius.${key}`)} = ${n};`);
98
+ }
99
+
100
+ // Primitive colors for direct use
101
+ const primColor = tokens?.primitive?.color;
102
+ if (primColor) {
103
+ lines.push('');
104
+ lines.push(' // Primitive palette');
105
+ for (const leaf of walkLeaves(primColor, 'primitive.color')) {
106
+ if (leaf.token.$type !== 'color') continue;
107
+ const resolved = resolveRef(tokens, leaf.path);
108
+ const argb = hexToDartArgb(resolved);
109
+ if (!argb) continue;
110
+ lines.push(` static const Color ${camelFromPath(leaf.path)} = Color(${argb});`);
111
+ }
112
+ }
113
+
114
+ lines.push('}');
115
+ lines.push('');
116
+
117
+ // ThemeData helper
118
+ const hasActionPrimary = semEntries.some((e) => e.name === 'actionPrimary');
119
+ const hasSurfaceDefault = semEntries.some((e) => e.name === 'surfaceDefault');
120
+ lines.push('ThemeData buildDesignlangTheme() {');
121
+ lines.push(' return ThemeData(');
122
+ lines.push(' colorScheme: const ColorScheme.light(');
123
+ if (hasActionPrimary) lines.push(' primary: DesignTokens.actionPrimary,');
124
+ if (hasSurfaceDefault) lines.push(' surface: DesignTokens.surfaceDefault,');
125
+ lines.push(' ),');
126
+ lines.push(' );');
127
+ lines.push('}');
128
+
129
+ return lines.join('\n') + '\n';
130
+ }
@@ -0,0 +1,161 @@
1
+ // iOS SwiftUI emitter — consumes a DTCG token object and produces a single
2
+ // Swift file exposing semantic Colors, primitive spacing/radius as CGFloat, and
3
+ // primitive palette colors.
4
+
5
+ import { resolveRef } from './_token-ref.js';
6
+
7
+ const HEADER_VERSION = '7.0.0';
8
+
9
+ // Convert a dotted DTCG path to camelCase identifier.
10
+ // e.g. "semantic.color.action.primary" → "actionPrimary"
11
+ // "primitive.color.brand.primary" → "brandPrimary"
12
+ // "primitive.spacing.s0" → "spacingS0"
13
+ // "primitive.radius.r0" → "radiusR0"
14
+ function camelFromPath(path) {
15
+ const parts = path.split('.');
16
+ // Drop leading "primitive"/"semantic" and the type bucket.
17
+ // We keep meaningful parts.
18
+ const trimmed = parts.slice(1); // drop primitive/semantic
19
+ // Drop the type segment for semantic colors ("color.action.primary" → "action.primary")
20
+ // For primitive colors ("color.brand.primary" → keep "brand.primary")
21
+ // For primitive spacing ("spacing.s0") we want "spacingS0" — keep all.
22
+ let segments;
23
+ if (trimmed[0] === 'color' && trimmed.length >= 3) {
24
+ segments = trimmed.slice(1);
25
+ } else {
26
+ segments = trimmed;
27
+ }
28
+ return segments
29
+ .map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))
30
+ .join('')
31
+ .replace(/[^a-zA-Z0-9]/g, '');
32
+ }
33
+
34
+ // Convert "#rrggbb" or "#rgb" to "0xRRGGBB" for SwiftUI Color(hex:).
35
+ function hexToSwiftHex(hex) {
36
+ if (typeof hex !== 'string') return null;
37
+ let h = hex.trim().replace(/^#/, '');
38
+ if (h.length === 3) {
39
+ h = h.split('').map((c) => c + c).join('');
40
+ }
41
+ if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) {
42
+ return `0x${h.toUpperCase()}`;
43
+ }
44
+ if (h.length === 8 && /^[0-9a-fA-F]{8}$/.test(h)) {
45
+ // drop alpha
46
+ return `0x${h.slice(2).toUpperCase()}`;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function dimensionToNumber(value) {
52
+ if (typeof value === 'number') return value;
53
+ if (typeof value !== 'string') return null;
54
+ const m = value.match(/^(-?\d+(?:\.\d+)?)(px|pt|rem|em)?$/);
55
+ if (!m) return null;
56
+ const n = parseFloat(m[1]);
57
+ const unit = m[2];
58
+ if (unit === 'rem' || unit === 'em') return n * 16;
59
+ return n;
60
+ }
61
+
62
+ // Walk leaves of a subtree producing { path, token } pairs.
63
+ function* walkLeaves(node, prefix) {
64
+ if (node == null || typeof node !== 'object') return;
65
+ if ('$value' in node && '$type' in node) {
66
+ yield { path: prefix, token: node };
67
+ return;
68
+ }
69
+ for (const key of Object.keys(node)) {
70
+ yield* walkLeaves(node[key], prefix ? `${prefix}.${key}` : key);
71
+ }
72
+ }
73
+
74
+ export function formatIosSwiftUI(tokens) {
75
+ const lines = [];
76
+ const source = tokens?.$metadata?.source || '';
77
+ lines.push(`// Generated by designlang v${HEADER_VERSION} — https://github.com/Manavarya09/design-extract`);
78
+ if (source) lines.push(`// Source: ${source}`);
79
+ lines.push('import SwiftUI');
80
+ lines.push('');
81
+ lines.push('extension Color {');
82
+ lines.push(' init(hex: UInt32) {');
83
+ lines.push(' let r = Double((hex >> 16) & 0xFF) / 255');
84
+ lines.push(' let g = Double((hex >> 8) & 0xFF) / 255');
85
+ lines.push(' let b = Double(hex & 0xFF) / 255');
86
+ lines.push(' self.init(red: r, green: g, blue: b)');
87
+ lines.push(' }');
88
+ lines.push('');
89
+ lines.push(' // MARK: Semantic');
90
+
91
+ // Semantic colors
92
+ const semanticColor = tokens?.semantic?.color;
93
+ if (semanticColor) {
94
+ for (const leaf of walkLeaves(semanticColor, 'semantic.color')) {
95
+ if (leaf.token.$type !== 'color') continue;
96
+ const resolved = resolveRef(tokens, leaf.path);
97
+ const swiftHex = hexToSwiftHex(resolved);
98
+ if (!swiftHex) continue;
99
+ lines.push(` static let ${camelFromPath(leaf.path)} = Color(hex: ${swiftHex})`);
100
+ }
101
+ }
102
+ lines.push('}');
103
+ lines.push('');
104
+
105
+ // CGFloat spacing + radius
106
+ lines.push('extension CGFloat {');
107
+ lines.push(' // MARK: Primitive spacing');
108
+ const spacing = tokens?.primitive?.spacing || {};
109
+ for (const key of Object.keys(spacing)) {
110
+ const tok = spacing[key];
111
+ if (!tok || tok.$type !== 'dimension') continue;
112
+ const n = dimensionToNumber(resolveRef(tokens, `primitive.spacing.${key}`));
113
+ if (n == null) continue;
114
+ const name = camelFromPath(`primitive.spacing.${key}`);
115
+ lines.push(` static let ${name}: CGFloat = ${n}`);
116
+ }
117
+ lines.push('');
118
+ lines.push(' // MARK: Primitive radius');
119
+ const radius = tokens?.primitive?.radius || {};
120
+ for (const key of Object.keys(radius)) {
121
+ const tok = radius[key];
122
+ if (!tok || tok.$type !== 'dimension') continue;
123
+ const n = dimensionToNumber(resolveRef(tokens, `primitive.radius.${key}`));
124
+ if (n == null) continue;
125
+ const name = camelFromPath(`primitive.radius.${key}`);
126
+ lines.push(` static let ${name}: CGFloat = ${n}`);
127
+ }
128
+ lines.push('}');
129
+ lines.push('');
130
+
131
+ // Primitive palette colors for direct use
132
+ lines.push('// MARK: Primitive palette (for direct use)');
133
+ lines.push('extension Color {');
134
+ const primColor = tokens?.primitive?.color;
135
+ if (primColor) {
136
+ for (const leaf of walkLeaves(primColor, 'primitive.color')) {
137
+ if (leaf.token.$type !== 'color') continue;
138
+ const resolved = resolveRef(tokens, leaf.path);
139
+ const swiftHex = hexToSwiftHex(resolved);
140
+ if (!swiftHex) continue;
141
+ lines.push(` static let ${camelFromPath(leaf.path)} = Color(hex: ${swiftHex})`);
142
+ }
143
+ }
144
+ lines.push('}');
145
+ lines.push('');
146
+
147
+ // Typography hint comments (composite typography tokens)
148
+ const typography = tokens?.semantic?.typography;
149
+ if (typography) {
150
+ for (const key of Object.keys(typography)) {
151
+ const tok = typography[key];
152
+ if (!tok || tok.$type !== 'typography') continue;
153
+ const v = tok.$value || {};
154
+ lines.push(
155
+ `// typography.${key}: family=${v.fontFamily ?? ''} size=${v.fontSize ?? ''} weight=${v.fontWeight ?? ''} lineHeight=${v.lineHeight ?? ''}`,
156
+ );
157
+ }
158
+ }
159
+
160
+ return lines.join('\n') + '\n';
161
+ }
@@ -3,6 +3,7 @@ import { pxToRem } from '../utils.js';
3
3
  export function formatMarkdown(design) {
4
4
  const lines = [];
5
5
  const { meta, colors, typography, spacing, shadows, borders, variables, breakpoints, animations, components } = design;
6
+ const componentClusters = Array.isArray(design.componentClusters) ? design.componentClusters : [];
6
7
 
7
8
  lines.push(`# Design Language: ${meta.title || 'Unknown Site'}`);
8
9
  lines.push('');
@@ -259,6 +260,30 @@ export function formatMarkdown(design) {
259
260
  }
260
261
  }
261
262
 
263
+ // ── Component Clusters (v7) ──
264
+ if (componentClusters.length > 0) {
265
+ lines.push('## Component Clusters');
266
+ lines.push('');
267
+ lines.push('Reusable component instances grouped by DOM structure and style similarity:');
268
+ lines.push('');
269
+ for (const cluster of componentClusters) {
270
+ const kindLabel = cluster.kind.charAt(0).toUpperCase() + cluster.kind.slice(1);
271
+ lines.push(`### ${kindLabel} — ${cluster.instanceCount} instance${cluster.instanceCount === 1 ? '' : 's'}, ${cluster.variants.length} variant${cluster.variants.length === 1 ? '' : 's'}`);
272
+ lines.push('');
273
+ cluster.variants.forEach((v, i) => {
274
+ lines.push(`**Variant ${i + 1}** (${v.instanceCount} instance${v.instanceCount === 1 ? '' : 's'})`);
275
+ lines.push('');
276
+ lines.push('```css');
277
+ for (const [prop, val] of Object.entries(v.css || {})) {
278
+ const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
279
+ lines.push(` ${cssProp}: ${val};`);
280
+ }
281
+ lines.push('```');
282
+ lines.push('');
283
+ });
284
+ }
285
+ }
286
+
262
287
  // ── Layout ──
263
288
  if (design.layout) {
264
289
  const l = design.layout;
@@ -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];