designlang 4.0.0 → 5.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.
@@ -0,0 +1,85 @@
1
+ import { extractDesignLanguage } from '../../../../src/index.js';
2
+ import { formatMarkdown } from '../../../../src/formatters/markdown.js';
3
+ import { formatTokens } from '../../../../src/formatters/tokens.js';
4
+ import { formatTailwind } from '../../../../src/formatters/tailwind.js';
5
+ import { formatCssVars } from '../../../../src/formatters/css-vars.js';
6
+ import { formatPreview } from '../../../../src/formatters/preview.js';
7
+ import { formatFigma } from '../../../../src/formatters/figma.js';
8
+ import { formatReactTheme, formatShadcnTheme } from '../../../../src/formatters/theme.js';
9
+ import { nameFromUrl } from '../../../../src/utils.js';
10
+
11
+ export const maxDuration = 60;
12
+ export const dynamic = 'force-dynamic';
13
+
14
+ async function getBrowserOptions() {
15
+ // On Vercel/Lambda, use @sparticuz/chromium; locally, use playwright's bundled browser
16
+ if (process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
17
+ const chromium = (await import('@sparticuz/chromium')).default;
18
+ return {
19
+ executablePath: await chromium.executablePath(),
20
+ browserArgs: chromium.args,
21
+ };
22
+ }
23
+ return {};
24
+ }
25
+
26
+ export async function POST(request) {
27
+ try {
28
+ const { url } = await request.json();
29
+
30
+ if (!url) {
31
+ return Response.json({ error: 'URL is required' }, { status: 400 });
32
+ }
33
+
34
+ let targetUrl = url;
35
+ if (!targetUrl.startsWith('http')) targetUrl = `https://${targetUrl}`;
36
+
37
+ // Validate URL
38
+ try {
39
+ new URL(targetUrl);
40
+ } catch {
41
+ return Response.json({ error: 'Invalid URL' }, { status: 400 });
42
+ }
43
+
44
+ const browserOpts = await getBrowserOptions();
45
+ const design = await extractDesignLanguage(targetUrl, browserOpts);
46
+
47
+ const prefix = nameFromUrl(targetUrl);
48
+
49
+ const files = {
50
+ [`${prefix}-design-language.md`]: formatMarkdown(design),
51
+ [`${prefix}-design-tokens.json`]: formatTokens(design),
52
+ [`${prefix}-tailwind.config.js`]: formatTailwind(design),
53
+ [`${prefix}-variables.css`]: formatCssVars(design),
54
+ [`${prefix}-preview.html`]: formatPreview(design),
55
+ [`${prefix}-figma-variables.json`]: formatFigma(design),
56
+ [`${prefix}-theme.js`]: formatReactTheme(design),
57
+ [`${prefix}-shadcn-theme.css`]: formatShadcnTheme(design),
58
+ };
59
+
60
+ const summary = {
61
+ url: design.meta.url,
62
+ title: design.meta.title,
63
+ colors: design.colors.all.length,
64
+ colorList: design.colors.all.slice(0, 20).map(c => c.hex),
65
+ fonts: design.typography.families.map(f => f.name).join(', ') || 'none detected',
66
+ spacingCount: design.spacing.scale.length,
67
+ spacingBase: design.spacing.base,
68
+ shadowCount: design.shadows.values.length,
69
+ radiiCount: design.borders.radii.length,
70
+ componentCount: Object.keys(design.components).length,
71
+ cssVarCount: Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0),
72
+ a11yScore: design.accessibility?.score ?? null,
73
+ a11yFailCount: design.accessibility?.failCount ?? 0,
74
+ score: design.score,
75
+ };
76
+
77
+ return Response.json({ summary, files });
78
+ } catch (err) {
79
+ console.error('Extraction failed:', err);
80
+ return Response.json(
81
+ { error: err.message || 'Extraction failed' },
82
+ { status: 500 }
83
+ );
84
+ }
85
+ }
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export default function Extractor() {
6
+ const [url, setUrl] = useState('');
7
+ const [loading, setLoading] = useState(false);
8
+ const [error, setError] = useState(null);
9
+ const [result, setResult] = useState(null);
10
+
11
+ const handleExtract = async (e) => {
12
+ e.preventDefault();
13
+ if (!url.trim()) return;
14
+
15
+ setLoading(true);
16
+ setError(null);
17
+ setResult(null);
18
+
19
+ try {
20
+ const res = await fetch('/api/extract', {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({ url: url.trim() }),
24
+ });
25
+
26
+ const data = await res.json();
27
+ if (!res.ok) throw new Error(data.error || 'Extraction failed');
28
+ setResult(data);
29
+ } catch (err) {
30
+ setError(err.message);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ const handleDownload = async () => {
37
+ if (!result) return;
38
+
39
+ const JSZip = (await import('jszip')).default;
40
+ const zip = new JSZip();
41
+
42
+ for (const [filename, content] of Object.entries(result.files)) {
43
+ zip.file(filename, content);
44
+ }
45
+
46
+ const blob = await zip.generateAsync({ type: 'blob' });
47
+ const a = document.createElement('a');
48
+ a.href = URL.createObjectURL(blob);
49
+ a.download = `designlang-${new Date().toISOString().slice(0, 10)}.zip`;
50
+ a.click();
51
+ URL.revokeObjectURL(a.href);
52
+ };
53
+
54
+ return (
55
+ <div className="extractor">
56
+ <form onSubmit={handleExtract} className="extractor-form">
57
+ <input
58
+ type="text"
59
+ value={url}
60
+ onChange={(e) => setUrl(e.target.value)}
61
+ placeholder="https://vercel.com"
62
+ className="extractor-input"
63
+ disabled={loading}
64
+ />
65
+ <button type="submit" className="extractor-btn" disabled={loading || !url.trim()}>
66
+ {loading ? 'Extracting...' : 'Extract'}
67
+ </button>
68
+ </form>
69
+
70
+ {loading && (
71
+ <div className="extractor-loading">
72
+ <div className="extractor-spinner" />
73
+ <p>Launching headless browser, crawling DOM, extracting styles...</p>
74
+ <p className="extractor-loading-sub">This takes 15–30 seconds</p>
75
+ </div>
76
+ )}
77
+
78
+ {error && (
79
+ <div className="extractor-error">
80
+ <p>{error}</p>
81
+ <p className="extractor-error-hint">
82
+ Server too slow? Run it locally — it hits different:<br />
83
+ <code>npx designlang {url || 'https://example.com'}</code>
84
+ </p>
85
+ </div>
86
+ )}
87
+
88
+ {result && (
89
+ <div className="extractor-results">
90
+ <div className="extractor-results-header">
91
+ <h3>{result.summary.title || result.summary.url}</h3>
92
+ <button onClick={handleDownload} className="extractor-download">
93
+ Download ZIP ({Object.keys(result.files).length} files)
94
+ </button>
95
+ </div>
96
+
97
+ <div className="extractor-stats-grid">
98
+ <div className="extractor-stat">
99
+ <div className="extractor-stat-value">{result.summary.colors}</div>
100
+ <div className="extractor-stat-label">Colors</div>
101
+ </div>
102
+ <div className="extractor-stat">
103
+ <div className="extractor-stat-value">{result.summary.spacingCount}</div>
104
+ <div className="extractor-stat-label">Spacing Values</div>
105
+ </div>
106
+ <div className="extractor-stat">
107
+ <div className="extractor-stat-value">{result.summary.shadowCount}</div>
108
+ <div className="extractor-stat-label">Shadows</div>
109
+ </div>
110
+ <div className="extractor-stat">
111
+ <div className="extractor-stat-value">{result.summary.componentCount}</div>
112
+ <div className="extractor-stat-label">Components</div>
113
+ </div>
114
+ <div className="extractor-stat">
115
+ <div className="extractor-stat-value">{result.summary.cssVarCount}</div>
116
+ <div className="extractor-stat-label">CSS Variables</div>
117
+ </div>
118
+ <div className="extractor-stat">
119
+ <div className="extractor-stat-value">
120
+ {result.summary.score ? `${result.summary.score.overall}` : '—'}
121
+ </div>
122
+ <div className="extractor-stat-label">
123
+ Design Score {result.summary.score ? `(${result.summary.score.grade})` : ''}
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ {/* Color swatches */}
129
+ {result.summary.colorList && result.summary.colorList.length > 0 && (
130
+ <div className="extractor-section">
131
+ <div className="extractor-section-title">Colors</div>
132
+ <div className="extractor-colors">
133
+ {result.summary.colorList.map((hex, i) => (
134
+ <div key={i} className="extractor-swatch" title={hex}>
135
+ <div className="extractor-swatch-color" style={{ backgroundColor: hex }} />
136
+ <div className="extractor-swatch-hex">{hex}</div>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ </div>
141
+ )}
142
+
143
+ {/* Fonts */}
144
+ {result.summary.fonts && result.summary.fonts !== 'none detected' && (
145
+ <div className="extractor-section">
146
+ <div className="extractor-section-title">Typography</div>
147
+ <div className="extractor-fonts">{result.summary.fonts}</div>
148
+ </div>
149
+ )}
150
+
151
+ {/* Accessibility */}
152
+ {result.summary.a11yScore !== null && (
153
+ <div className="extractor-section">
154
+ <div className="extractor-section-title">Accessibility</div>
155
+ <div className="extractor-a11y">
156
+ <span className={`extractor-a11y-score ${result.summary.a11yScore >= 80 ? 'good' : result.summary.a11yScore >= 50 ? 'ok' : 'bad'}`}>
157
+ {result.summary.a11yScore}% WCAG
158
+ </span>
159
+ {result.summary.a11yFailCount > 0 && (
160
+ <span className="extractor-a11y-fails">{result.summary.a11yFailCount} failing contrast pairs</span>
161
+ )}
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ {/* Files list */}
167
+ <div className="extractor-section">
168
+ <div className="extractor-section-title">Output Files</div>
169
+ <div className="extractor-files">
170
+ {Object.entries(result.files).map(([name, content]) => (
171
+ <div key={name} className="extractor-file">
172
+ <span className="extractor-file-name">{name}</span>
173
+ <span className="extractor-file-size">
174
+ {content.length > 1024 ? `${(content.length / 1024).toFixed(1)}KB` : `${content.length}B`}
175
+ </span>
176
+ </div>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )}
182
+ </div>
183
+ );
184
+ }
Binary file