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.
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/CHANGELOG.md +43 -0
- package/README.md +177 -6
- package/bin/design-extract.js +302 -92
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
- package/package.json +13 -7
- package/src/config.js +59 -0
- package/src/crawler.js +297 -95
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/components.js +77 -1
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/stack-fingerprint.js +88 -0
- package/src/extractors/variables.js +20 -1
- package/src/formatters/_token-ref.js +44 -0
- package/src/formatters/agent-rules.js +116 -0
- package/src/formatters/android-compose.js +164 -0
- package/src/formatters/dtcg-tokens.js +175 -0
- package/src/formatters/figma.js +66 -47
- package/src/formatters/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +267 -0
- package/src/history.js +8 -1
- package/src/index.js +76 -20
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/src/utils.js +68 -0
- package/tests/cli.test.js +84 -0
- package/tests/extractors.test.js +792 -0
- package/tests/formatters.test.js +709 -0
- package/tests/mcp.test.js +68 -0
- package/tests/utils.test.js +413 -0
- 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
|
-
|
|
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];
|