designlang 6.0.0 → 7.1.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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +120 -8
- package/bin/design-extract.js +106 -3
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/icons/favicon.svg +7 -0
- package/chrome-extension/icons/icon-128.png +0 -0
- package/chrome-extension/icons/icon-16.png +0 -0
- package/chrome-extension/icons/icon-32.png +0 -0
- package/chrome-extension/icons/icon-48.png +0 -0
- package/chrome-extension/manifest.json +26 -0
- package/chrome-extension/popup.html +167 -0
- package/chrome-extension/popup.js +59 -0
- 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/docs/superpowers/specs/2026-04-18-website-redesign-design.md +120 -0
- package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +111 -0
- package/package.json +5 -4
- package/src/config.js +26 -0
- package/src/crawler.js +136 -2
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/stack-fingerprint.js +88 -0
- 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/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/wordpress.js +183 -0
- package/src/index.js +30 -0
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/src/utils-cookies.js +73 -0
- package/tests/cli.test.js +50 -0
- package/tests/cookies.test.js +98 -0
- package/tests/extractors.test.js +131 -0
- package/tests/formatters.test.js +232 -0
- package/tests/mcp.test.js +68 -0
- package/website/app/api/extract/route.js +216 -56
- package/website/app/components/A11ySlider.js +369 -0
- package/website/app/components/Comparison.js +286 -0
- package/website/app/components/CssHealth.js +243 -0
- package/website/app/components/HeroExtractor.js +455 -0
- package/website/app/components/Marginalia.js +3 -0
- package/website/app/components/McpSection.js +223 -0
- package/website/app/components/PlatformTabs.js +250 -0
- package/website/app/components/RegionsComponents.js +429 -0
- package/website/app/components/Rule.js +13 -0
- package/website/app/components/Specimens.js +237 -0
- package/website/app/components/StructuredData.js +144 -0
- package/website/app/components/TokenBrowser.js +344 -0
- package/website/app/components/token-browser-sample.js +65 -0
- package/website/app/globals.css +415 -633
- package/website/app/icon.svg +7 -0
- package/website/app/layout.js +113 -6
- package/website/app/opengraph-image.js +170 -0
- package/website/app/page.js +325 -148
- package/website/app/robots.js +15 -0
- package/website/app/seo-config.js +82 -0
- package/website/app/sitemap.js +18 -0
- package/website/lib/cache.js +73 -0
- package/website/lib/rate-limit.js +30 -0
- package/website/lib/rate-limit.test.js +55 -0
- package/website/lib/specimens.json +86 -0
- package/website/lib/token-helpers.js +70 -0
- package/website/lib/url-safety.js +103 -0
- package/website/lib/url-safety.test.js +116 -0
- package/website/lib/zip-files.js +15 -0
- package/website/package-lock.json +85 -0
- package/website/package.json +1 -0
- package/website/public/favicon.svg +7 -0
- package/website/public/logo-specimen.svg +76 -0
- package/website/public/mark.svg +12 -0
- package/website/public/site.webmanifest +13 -0
- package/website/app/favicon.ico +0 -0
- package/website/public/file.svg +0 -1
- package/website/public/globe.svg +0 -1
- package/website/public/next.svg +0 -1
- package/website/public/vercel.svg +0 -1
- package/website/public/window.svg +0 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import Marginalia from './Marginalia';
|
|
5
|
+
|
|
6
|
+
const STAGE_LABEL = {
|
|
7
|
+
crawl: 'walking DOM',
|
|
8
|
+
colors: 'resolving palette',
|
|
9
|
+
typography: 'reading type',
|
|
10
|
+
spacing: 'measuring rhythm',
|
|
11
|
+
shadows: 'catching shadows',
|
|
12
|
+
borders: 'tracing radii',
|
|
13
|
+
components: 'clustering components',
|
|
14
|
+
regions: 'naming regions',
|
|
15
|
+
a11y: 'auditing contrast',
|
|
16
|
+
score: 'scoring system',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MAX_SWATCHES = 24;
|
|
20
|
+
const MAX_DIMENSIONS = 12;
|
|
21
|
+
|
|
22
|
+
export default function HeroExtractor() {
|
|
23
|
+
const [status, setStatus] = useState('idle'); // idle | streaming | done | error
|
|
24
|
+
const [stage, setStage] = useState(null);
|
|
25
|
+
const [cached, setCached] = useState(false);
|
|
26
|
+
const [swatches, setSwatches] = useState([]);
|
|
27
|
+
const [fontSample, setFontSample] = useState(null);
|
|
28
|
+
const [dimensions, setDimensions] = useState([]);
|
|
29
|
+
const [summary, setSummary] = useState(null);
|
|
30
|
+
const [files, setFiles] = useState(null);
|
|
31
|
+
const [errorMsg, setErrorMsg] = useState(null);
|
|
32
|
+
const [rateLimitMsg, setRateLimitMsg] = useState(null);
|
|
33
|
+
const [downloadBusy, setDownloadBusy] = useState(false);
|
|
34
|
+
const [copied, setCopied] = useState(false);
|
|
35
|
+
const inputRef = useRef(null);
|
|
36
|
+
|
|
37
|
+
// Accept ?url= query param (Chrome extension handoff, deep links). Only
|
|
38
|
+
// applied once on mount; the extension prefills the input and the user still
|
|
39
|
+
// controls submission.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (typeof window === 'undefined') return;
|
|
42
|
+
const params = new URLSearchParams(window.location.search);
|
|
43
|
+
const incoming = params.get('url');
|
|
44
|
+
if (!incoming || !inputRef.current) return;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(incoming);
|
|
47
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
48
|
+
inputRef.current.value = parsed.toString();
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore malformed URLs silently — user still sees the default placeholder.
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const reset = useCallback(() => {
|
|
56
|
+
setStage(null);
|
|
57
|
+
setCached(false);
|
|
58
|
+
setSwatches([]);
|
|
59
|
+
setFontSample(null);
|
|
60
|
+
setDimensions([]);
|
|
61
|
+
setSummary(null);
|
|
62
|
+
setFiles(null);
|
|
63
|
+
setErrorMsg(null);
|
|
64
|
+
setRateLimitMsg(null);
|
|
65
|
+
setCopied(false);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const handleEvent = useCallback((event) => {
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case 'cache':
|
|
71
|
+
setCached(true);
|
|
72
|
+
break;
|
|
73
|
+
case 'stage':
|
|
74
|
+
setStage(event.name);
|
|
75
|
+
break;
|
|
76
|
+
case 'token': {
|
|
77
|
+
if (event.$type === 'color' && typeof event.value === 'string' && event.value.startsWith('#')) {
|
|
78
|
+
setSwatches((prev) => {
|
|
79
|
+
if (prev.length >= MAX_SWATCHES) return prev;
|
|
80
|
+
if (prev.some((s) => s.path === event.path)) return prev;
|
|
81
|
+
return [...prev, { path: event.path, hex: event.value }];
|
|
82
|
+
});
|
|
83
|
+
} else if (event.$type === 'fontFamily' && typeof event.value === 'string') {
|
|
84
|
+
setFontSample((prev) => prev || event.value);
|
|
85
|
+
} else if (event.$type === 'dimension' && typeof event.value === 'string') {
|
|
86
|
+
const px = parseFloat(event.value);
|
|
87
|
+
if (!Number.isNaN(px)) {
|
|
88
|
+
setDimensions((prev) => {
|
|
89
|
+
if (prev.length >= MAX_DIMENSIONS) return prev;
|
|
90
|
+
return [...prev, { path: event.path, px, raw: event.value }];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'summary':
|
|
97
|
+
setSummary(event.summary);
|
|
98
|
+
break;
|
|
99
|
+
case 'files':
|
|
100
|
+
setFiles(event.files);
|
|
101
|
+
setStatus('done');
|
|
102
|
+
break;
|
|
103
|
+
case 'error':
|
|
104
|
+
setErrorMsg(event.error || 'Extraction failed');
|
|
105
|
+
setStatus('error');
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const handleSubmit = useCallback(async (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (status === 'streaming') return;
|
|
113
|
+
reset();
|
|
114
|
+
setStatus('streaming');
|
|
115
|
+
|
|
116
|
+
const url = inputRef.current?.value?.trim() || '';
|
|
117
|
+
let res;
|
|
118
|
+
try {
|
|
119
|
+
res = await fetch('/api/extract', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'content-type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ url }),
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
setErrorMsg('Network error — check your connection.');
|
|
126
|
+
setStatus('error');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
let body = null;
|
|
132
|
+
try { body = await res.json(); } catch {}
|
|
133
|
+
if (res.status === 429) {
|
|
134
|
+
setRateLimitMsg(body?.error || 'Rate limit reached. Try again in 24h.');
|
|
135
|
+
} else if (res.status === 400) {
|
|
136
|
+
setErrorMsg(body?.error || 'Invalid URL.');
|
|
137
|
+
} else {
|
|
138
|
+
setErrorMsg(body?.error || 'Extraction failed. Try another URL.');
|
|
139
|
+
}
|
|
140
|
+
setStatus('error');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!res.body) {
|
|
145
|
+
setErrorMsg('Browser does not support streaming responses.');
|
|
146
|
+
setStatus('error');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
151
|
+
let buffer = '';
|
|
152
|
+
try {
|
|
153
|
+
while (true) {
|
|
154
|
+
const { value, done } = await reader.read();
|
|
155
|
+
if (done) break;
|
|
156
|
+
buffer += value;
|
|
157
|
+
let nl;
|
|
158
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
159
|
+
const line = buffer.slice(0, nl).trim();
|
|
160
|
+
buffer = buffer.slice(nl + 1);
|
|
161
|
+
if (!line) continue;
|
|
162
|
+
try {
|
|
163
|
+
handleEvent(JSON.parse(line));
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore partial / malformed lines — defensive only.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (buffer.trim()) {
|
|
170
|
+
try { handleEvent(JSON.parse(buffer.trim())); } catch {}
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
setErrorMsg('Stream interrupted. Try another URL.');
|
|
174
|
+
setStatus('error');
|
|
175
|
+
}
|
|
176
|
+
}, [status, handleEvent, reset]);
|
|
177
|
+
|
|
178
|
+
const handleDownload = useCallback(async () => {
|
|
179
|
+
if (!files || downloadBusy) return;
|
|
180
|
+
setDownloadBusy(true);
|
|
181
|
+
try {
|
|
182
|
+
const { zipFilesToUrl } = await import('../../lib/zip-files.js');
|
|
183
|
+
const { url, filename } = await zipFilesToUrl(files, {
|
|
184
|
+
name: `designlang-${new Date().toISOString().slice(0, 10)}`,
|
|
185
|
+
});
|
|
186
|
+
const a = document.createElement('a');
|
|
187
|
+
a.href = url;
|
|
188
|
+
a.download = filename;
|
|
189
|
+
a.click();
|
|
190
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
191
|
+
} finally {
|
|
192
|
+
setDownloadBusy(false);
|
|
193
|
+
}
|
|
194
|
+
}, [files, downloadBusy]);
|
|
195
|
+
|
|
196
|
+
const handleCopyMarkdown = useCallback(async () => {
|
|
197
|
+
if (!files) return;
|
|
198
|
+
const mdKey = Object.keys(files).find((k) => k.endsWith('-design-language.md'));
|
|
199
|
+
if (!mdKey) return;
|
|
200
|
+
try {
|
|
201
|
+
await navigator.clipboard.writeText(files[mdKey]);
|
|
202
|
+
setCopied(true);
|
|
203
|
+
setTimeout(() => setCopied(false), 1800);
|
|
204
|
+
} catch {
|
|
205
|
+
// no-op
|
|
206
|
+
}
|
|
207
|
+
}, [files]);
|
|
208
|
+
|
|
209
|
+
const stageLabel = stage ? STAGE_LABEL[stage] || stage : null;
|
|
210
|
+
const streaming = status === 'streaming';
|
|
211
|
+
const disabled = streaming;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
<div className="with-margin" style={{ marginTop: 'var(--r8)' }}>
|
|
216
|
+
<form
|
|
217
|
+
onSubmit={handleSubmit}
|
|
218
|
+
style={{
|
|
219
|
+
display: 'flex',
|
|
220
|
+
alignItems: 'stretch',
|
|
221
|
+
gap: 0,
|
|
222
|
+
maxWidth: 720,
|
|
223
|
+
border: 'var(--hair)',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<label htmlFor="url" style={{ display: 'none' }}>URL</label>
|
|
227
|
+
<input
|
|
228
|
+
id="url"
|
|
229
|
+
ref={inputRef}
|
|
230
|
+
name="url"
|
|
231
|
+
type="url"
|
|
232
|
+
placeholder="https://stripe.com"
|
|
233
|
+
defaultValue="https://stripe.com"
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
className="mono"
|
|
236
|
+
style={{
|
|
237
|
+
flex: 1,
|
|
238
|
+
padding: '18px 20px',
|
|
239
|
+
fontSize: 16,
|
|
240
|
+
color: 'var(--ink)',
|
|
241
|
+
background: 'transparent',
|
|
242
|
+
borderRight: 'var(--hair)',
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
<button
|
|
246
|
+
type="submit"
|
|
247
|
+
className="cta"
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
style={{ boxShadow: 'none' }}
|
|
250
|
+
>
|
|
251
|
+
{streaming ? 'Extracting…' : 'Extract'}
|
|
252
|
+
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--ink-3)' }}>↵</span>
|
|
253
|
+
</button>
|
|
254
|
+
</form>
|
|
255
|
+
|
|
256
|
+
<Marginalia>
|
|
257
|
+
<div>status</div>
|
|
258
|
+
<div style={{ minHeight: '1.5em' }}>
|
|
259
|
+
{status === 'idle' && <code style={{ color: 'var(--ink-3)' }}>awaiting URL</code>}
|
|
260
|
+
{streaming && stageLabel && (
|
|
261
|
+
<code>§ {stageLabel}{cached ? ' (cached)' : ''}</code>
|
|
262
|
+
)}
|
|
263
|
+
{streaming && !stageLabel && <code>§ opening stream</code>}
|
|
264
|
+
{status === 'done' && <code style={{ color: 'var(--accent)' }}>complete{cached ? ' (cached)' : ''}</code>}
|
|
265
|
+
{status === 'error' && <code style={{ color: 'var(--accent)' }}>halted</code>}
|
|
266
|
+
</div>
|
|
267
|
+
{cached && (
|
|
268
|
+
<>
|
|
269
|
+
<hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
|
|
270
|
+
<span className="chip" style={{ fontSize: 10 }}>cached · < 24h</span>
|
|
271
|
+
</>
|
|
272
|
+
)}
|
|
273
|
+
</Marginalia>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{errorMsg && (
|
|
277
|
+
<p className="mono" role="alert" style={{ marginTop: 'var(--r4)', fontSize: 12, color: 'var(--accent)' }}>
|
|
278
|
+
{errorMsg}{' '}
|
|
279
|
+
{status === 'error' && (
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={reset}
|
|
283
|
+
style={{ textDecoration: 'underline', color: 'inherit', marginLeft: 8 }}
|
|
284
|
+
>
|
|
285
|
+
Try another URL
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
</p>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{rateLimitMsg && (
|
|
292
|
+
<p className="mono" role="alert" style={{ marginTop: 'var(--r4)', fontSize: 12, color: 'var(--accent)' }}>
|
|
293
|
+
{rateLimitMsg}
|
|
294
|
+
</p>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
<p className="mono" style={{ marginTop: 14, fontSize: 11, color: 'var(--ink-3)' }}>
|
|
298
|
+
Free, rate-limited to 3 extractions per day. Private IPs rejected. No accounts.
|
|
299
|
+
</p>
|
|
300
|
+
|
|
301
|
+
{/* ── Live paint ─────────────────────────────────────── */}
|
|
302
|
+
{(swatches.length > 0 || fontSample || dimensions.length > 0 || summary) && (
|
|
303
|
+
<div style={{ marginTop: 'var(--r7)', display: 'grid', gap: 'var(--r5)' }}>
|
|
304
|
+
{swatches.length > 0 && (
|
|
305
|
+
<div>
|
|
306
|
+
<div className="section-label" style={{ marginBottom: 'var(--r3)' }}>
|
|
307
|
+
<span>palette</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--r3)' }}>
|
|
310
|
+
{swatches.map((s, i) => (
|
|
311
|
+
<div
|
|
312
|
+
key={s.path}
|
|
313
|
+
style={{
|
|
314
|
+
opacity: 0,
|
|
315
|
+
animation: `designlang-fade-in 200ms ease-out forwards`,
|
|
316
|
+
animationDelay: `${i * 40}ms`,
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<div
|
|
320
|
+
style={{
|
|
321
|
+
width: 40,
|
|
322
|
+
height: 40,
|
|
323
|
+
background: s.hex,
|
|
324
|
+
border: 'var(--hair)',
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
<div
|
|
328
|
+
className="mono"
|
|
329
|
+
style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 4 }}
|
|
330
|
+
>
|
|
331
|
+
{s.hex}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{fontSample && (
|
|
340
|
+
<div>
|
|
341
|
+
<div className="section-label" style={{ marginBottom: 'var(--r3)' }}>
|
|
342
|
+
<span>type</span>
|
|
343
|
+
</div>
|
|
344
|
+
<div
|
|
345
|
+
style={{
|
|
346
|
+
fontFamily: `${fontSample}, system-ui, sans-serif`,
|
|
347
|
+
fontSize: 32,
|
|
348
|
+
lineHeight: 1.2,
|
|
349
|
+
letterSpacing: '-0.01em',
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
Aa Bb Cc 123
|
|
353
|
+
</div>
|
|
354
|
+
<div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 4 }}>
|
|
355
|
+
{fontSample}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{dimensions.length > 0 && (
|
|
361
|
+
<div>
|
|
362
|
+
<div className="section-label" style={{ marginBottom: 'var(--r3)' }}>
|
|
363
|
+
<span>scale</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 8 }}>
|
|
366
|
+
{dimensions.map((d, i) => (
|
|
367
|
+
<div
|
|
368
|
+
key={d.path}
|
|
369
|
+
title={`${d.path} — ${d.raw}`}
|
|
370
|
+
style={{
|
|
371
|
+
width: 14,
|
|
372
|
+
height: Math.min(Math.max(d.px, 4), 120),
|
|
373
|
+
background: 'var(--ink)',
|
|
374
|
+
opacity: 0,
|
|
375
|
+
animation: `designlang-fade-in 200ms ease-out forwards`,
|
|
376
|
+
animationDelay: `${i * 30}ms`,
|
|
377
|
+
}}
|
|
378
|
+
/>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
{summary && (
|
|
385
|
+
<div
|
|
386
|
+
style={{
|
|
387
|
+
display: 'grid',
|
|
388
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
|
389
|
+
gap: 'var(--r5)',
|
|
390
|
+
borderTop: 'var(--hair)',
|
|
391
|
+
paddingTop: 'var(--r5)',
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
<Numeral value={summary.colors} label="colors" />
|
|
395
|
+
<Numeral value={summary.spacingCount} label="spacing" />
|
|
396
|
+
<Numeral value={summary.shadowCount} label="shadows" />
|
|
397
|
+
<Numeral value={summary.componentCount} label="components" />
|
|
398
|
+
<Numeral
|
|
399
|
+
value={summary.score?.overall ?? '—'}
|
|
400
|
+
label={`score ${summary.score?.grade ? `(${summary.score.grade})` : ''}`}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{files && (
|
|
408
|
+
<div style={{ marginTop: 'var(--r6)', display: 'flex', gap: 'var(--r3)', flexWrap: 'wrap' }}>
|
|
409
|
+
<button
|
|
410
|
+
type="button"
|
|
411
|
+
onClick={handleDownload}
|
|
412
|
+
className="cta"
|
|
413
|
+
disabled={downloadBusy}
|
|
414
|
+
>
|
|
415
|
+
{downloadBusy ? 'Zipping…' : `Download all (.zip) — ${Object.keys(files).length} files`}
|
|
416
|
+
</button>
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
onClick={handleCopyMarkdown}
|
|
420
|
+
className="cta"
|
|
421
|
+
style={{ background: 'var(--paper)', color: 'var(--ink)', boxShadow: 'none' }}
|
|
422
|
+
>
|
|
423
|
+
{copied ? 'Copied' : 'Copy markdown'}
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
<style jsx>{`
|
|
429
|
+
@keyframes designlang-fade-in {
|
|
430
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
431
|
+
to { opacity: 1; transform: translateY(0); }
|
|
432
|
+
}
|
|
433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
434
|
+
* { animation: none !important; }
|
|
435
|
+
}
|
|
436
|
+
`}</style>
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function Numeral({ value, label }) {
|
|
442
|
+
return (
|
|
443
|
+
<div>
|
|
444
|
+
<div
|
|
445
|
+
className="display"
|
|
446
|
+
style={{ fontSize: 'clamp(32px, 5vw, 56px)', lineHeight: 1 }}
|
|
447
|
+
>
|
|
448
|
+
{value}
|
|
449
|
+
</div>
|
|
450
|
+
<div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6, textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
451
|
+
{label}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import Marginalia from './Marginalia';
|
|
5
|
+
|
|
6
|
+
const CURSOR_RULE = `---
|
|
7
|
+
description: Design system extracted from https://stripe.com — use these tokens, do not invent new ones.
|
|
8
|
+
globs: **/*.{ts,tsx,jsx,js,css,scss,html,vue,svelte,swift,kt,dart,php}
|
|
9
|
+
alwaysApply: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Design system reference
|
|
13
|
+
|
|
14
|
+
Source: https://stripe.com
|
|
15
|
+
Extracted by designlang v7.0.0 on 2026-04-18T12:00:00Z
|
|
16
|
+
|
|
17
|
+
## Semantic tokens (use these)
|
|
18
|
+
- color.action.primary: #533afd
|
|
19
|
+
- color.surface.default: #ffffff
|
|
20
|
+
- color.text.body: #0a2540
|
|
21
|
+
- radius.control: 8px
|
|
22
|
+
- typography.body.fontFamily: sohne-var, Helvetica Neue, Arial, sans-serif
|
|
23
|
+
|
|
24
|
+
## How to use
|
|
25
|
+
- Prefer \`semantic.*\` tokens over \`primitive.*\`.
|
|
26
|
+
- Never invent new tokens or hex values; reuse the ones above.
|
|
27
|
+
- When a value is missing, pick the closest existing semantic token and flag the gap.
|
|
28
|
+
- Reference tokens by their dotted path (e.g. \`semantic.color.action.primary\`).
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
// The transcript. Each step is either typed (user input) or printed (server output).
|
|
32
|
+
const TRANSCRIPT = [
|
|
33
|
+
{ kind: 'typed', text: '$ designlang mcp --output-dir ./design-extract-output' },
|
|
34
|
+
{ kind: 'print', text: '{"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"designlang","version":"7.0.0"}}}' },
|
|
35
|
+
{ kind: 'typed', text: '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_tokens","arguments":{"query":"action.primary"}}}' },
|
|
36
|
+
{ kind: 'print', text: '{"jsonrpc":"2.0","id":2,"result":{"matches":[{"path":"semantic.color.action.primary","$type":"color","$value":"#533afd"}]}}' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function Terminal({ reduced }) {
|
|
40
|
+
const [lines, setLines] = useState(() =>
|
|
41
|
+
reduced ? TRANSCRIPT.map((t) => ({ ...t, rendered: t.text })) : []
|
|
42
|
+
);
|
|
43
|
+
const containerRef = useRef(null);
|
|
44
|
+
const [inView, setInView] = useState(false);
|
|
45
|
+
|
|
46
|
+
// IntersectionObserver to pause when offscreen.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (reduced) return;
|
|
49
|
+
if (!containerRef.current) return;
|
|
50
|
+
const el = containerRef.current;
|
|
51
|
+
const io = new IntersectionObserver(
|
|
52
|
+
([entry]) => setInView(entry.isIntersecting),
|
|
53
|
+
{ threshold: 0.25 }
|
|
54
|
+
);
|
|
55
|
+
io.observe(el);
|
|
56
|
+
return () => io.disconnect();
|
|
57
|
+
}, [reduced]);
|
|
58
|
+
|
|
59
|
+
// Typewriter runner.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (reduced) return;
|
|
62
|
+
if (!inView) return;
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const signal = controller.signal;
|
|
65
|
+
|
|
66
|
+
async function run() {
|
|
67
|
+
while (!signal.aborted) {
|
|
68
|
+
setLines([]);
|
|
69
|
+
for (let i = 0; i < TRANSCRIPT.length; i++) {
|
|
70
|
+
if (signal.aborted) return;
|
|
71
|
+
const step = TRANSCRIPT[i];
|
|
72
|
+
if (step.kind === 'typed') {
|
|
73
|
+
// ~55 cps ≈ 18ms per char
|
|
74
|
+
for (let j = 0; j <= step.text.length; j++) {
|
|
75
|
+
if (signal.aborted) return;
|
|
76
|
+
await wait(18, signal);
|
|
77
|
+
setLines((prev) => {
|
|
78
|
+
const next = [...prev];
|
|
79
|
+
next[i] = { ...step, rendered: step.text.slice(0, j) };
|
|
80
|
+
return next;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await wait(280, signal);
|
|
84
|
+
} else {
|
|
85
|
+
await wait(220, signal);
|
|
86
|
+
if (signal.aborted) return;
|
|
87
|
+
setLines((prev) => {
|
|
88
|
+
const next = [...prev];
|
|
89
|
+
next[i] = { ...step, rendered: step.text };
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await wait(2000, signal);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
run().catch(() => {});
|
|
99
|
+
return () => controller.abort();
|
|
100
|
+
}, [inView, reduced]);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={containerRef}
|
|
105
|
+
aria-label="designlang MCP terminal transcript"
|
|
106
|
+
style={{
|
|
107
|
+
background: 'var(--ink)',
|
|
108
|
+
color: 'var(--paper)',
|
|
109
|
+
border: 'var(--hair)',
|
|
110
|
+
fontFamily: 'var(--font-mono)',
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
lineHeight: 1.55,
|
|
113
|
+
padding: 'var(--r4) var(--r5)',
|
|
114
|
+
minHeight: 320,
|
|
115
|
+
overflow: 'auto',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<div style={{ color: 'var(--ink-3)', marginBottom: 8 }}>
|
|
119
|
+
designlang-mcp · stdio
|
|
120
|
+
</div>
|
|
121
|
+
{TRANSCRIPT.map((step, i) => {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
const rendered = line?.rendered ?? '';
|
|
124
|
+
const done = rendered.length === step.text.length;
|
|
125
|
+
return (
|
|
126
|
+
<pre
|
|
127
|
+
key={i}
|
|
128
|
+
style={{
|
|
129
|
+
margin: 0,
|
|
130
|
+
whiteSpace: 'pre-wrap',
|
|
131
|
+
wordBreak: 'break-all',
|
|
132
|
+
color: step.kind === 'typed' ? 'var(--paper)' : 'var(--ink-3)',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{step.kind === 'print' && rendered ? <span style={{ color: 'var(--accent)' }}>» </span> : null}
|
|
136
|
+
{rendered}
|
|
137
|
+
{!done && !reduced ? <span style={{ opacity: 0.7 }}>▍</span> : null}
|
|
138
|
+
</pre>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wait(ms, signal) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const t = setTimeout(resolve, ms);
|
|
148
|
+
signal.addEventListener('abort', () => {
|
|
149
|
+
clearTimeout(t);
|
|
150
|
+
reject(new Error('aborted'));
|
|
151
|
+
}, { once: true });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default function McpSection() {
|
|
156
|
+
const [reduced, setReduced] = useState(false);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
159
|
+
const update = () => setReduced(mq.matches);
|
|
160
|
+
update();
|
|
161
|
+
mq.addEventListener?.('change', update);
|
|
162
|
+
return () => mq.removeEventListener?.('change', update);
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<div style={{ padding: 'var(--r5) 0 var(--r6)' }}>
|
|
168
|
+
<h2 className="display">Your editor reads this.</h2>
|
|
169
|
+
<p className="prose" style={{ marginTop: 'var(--r4)', fontSize: 18, maxWidth: '52ch' }}>
|
|
170
|
+
The MCP server exposes five resources and five tools over stdio, speaking
|
|
171
|
+
JSON-RPC to Claude Code, Cursor, and Windsurf. See <a href="#install">§09</a> for install.
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
style={{
|
|
177
|
+
display: 'grid',
|
|
178
|
+
gridTemplateColumns: 'minmax(0, 7fr) minmax(0, 5fr)',
|
|
179
|
+
columnGap: 0,
|
|
180
|
+
alignItems: 'stretch',
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{/* Left: rule file */}
|
|
184
|
+
<div
|
|
185
|
+
style={{
|
|
186
|
+
background: 'var(--paper-2)',
|
|
187
|
+
border: 'var(--hair)',
|
|
188
|
+
borderRight: 0,
|
|
189
|
+
padding: 'var(--r4) var(--r5)',
|
|
190
|
+
fontFamily: 'var(--font-mono)',
|
|
191
|
+
fontSize: 12,
|
|
192
|
+
lineHeight: 1.55,
|
|
193
|
+
overflow: 'auto',
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 'var(--r3)' }}>
|
|
197
|
+
<span style={{ color: 'var(--ink-3)' }}>.cursor/rules/designlang.mdc</span>
|
|
198
|
+
<span className="chip" style={{ color: 'var(--accent)', borderColor: 'var(--accent)' }}>
|
|
199
|
+
alwaysApply: true
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>
|
|
203
|
+
{CURSOR_RULE}
|
|
204
|
+
</pre>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Right: terminal */}
|
|
208
|
+
<Terminal reduced={reduced} />
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div style={{ marginTop: 'var(--r6)' }}>
|
|
212
|
+
<Marginalia>
|
|
213
|
+
<div>install</div>
|
|
214
|
+
<div>
|
|
215
|
+
<code>{'{ "mcpServers": { "designlang": { "command": "npx", "args": ["designlang","mcp"] } } }'}</code>
|
|
216
|
+
</div>
|
|
217
|
+
<hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
|
|
218
|
+
<div>Zero-config for any MCP-aware agent.</div>
|
|
219
|
+
</Marginalia>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|