designlang 7.2.0 → 9.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 (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +154 -13
  3. package/bin/design-extract.js +94 -1
  4. package/package.json +9 -3
  5. package/src/config.js +2 -0
  6. package/src/crawler.js +55 -6
  7. package/src/drift.js +137 -0
  8. package/src/extractors/accessibility.js +44 -1
  9. package/src/extractors/colors.js +50 -12
  10. package/src/extractors/component-anatomy.js +123 -0
  11. package/src/extractors/motion.js +184 -0
  12. package/src/extractors/scoring.js +49 -30
  13. package/src/extractors/voice.js +96 -0
  14. package/src/formatters/markdown.js +88 -0
  15. package/src/formatters/motion-tokens.js +22 -0
  16. package/src/index.js +14 -0
  17. package/src/lint.js +198 -0
  18. package/src/visual-diff.js +116 -0
  19. package/.github/FUNDING.yml +0 -1
  20. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  21. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  22. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
  23. package/.github/og-preview.png +0 -0
  24. package/.github/workflows/manavarya-bot.yml +0 -17
  25. package/chrome-extension/README.md +0 -41
  26. package/chrome-extension/icons/favicon.svg +0 -7
  27. package/chrome-extension/icons/icon-128.png +0 -0
  28. package/chrome-extension/icons/icon-16.png +0 -0
  29. package/chrome-extension/icons/icon-32.png +0 -0
  30. package/chrome-extension/icons/icon-48.png +0 -0
  31. package/chrome-extension/manifest.json +0 -26
  32. package/chrome-extension/popup.html +0 -167
  33. package/chrome-extension/popup.js +0 -59
  34. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +0 -1121
  35. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +0 -150
  36. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +0 -120
  37. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +0 -111
  38. package/tests/cli.test.js +0 -84
  39. package/tests/cookies.test.js +0 -98
  40. package/tests/extractors.test.js +0 -792
  41. package/tests/formatters.test.js +0 -709
  42. package/tests/interaction-states.test.js +0 -62
  43. package/tests/mcp.test.js +0 -68
  44. package/tests/modern-css.test.js +0 -104
  45. package/tests/routes-reconciliation.test.js +0 -120
  46. package/tests/utils.test.js +0 -413
  47. package/tests/wide-gamut.test.js +0 -90
  48. package/website/.claude/launch.json +0 -11
  49. package/website/AGENTS.md +0 -5
  50. package/website/CLAUDE.md +0 -1
  51. package/website/README.md +0 -36
  52. package/website/app/api/extract/route.js +0 -245
  53. package/website/app/components/A11ySlider.js +0 -369
  54. package/website/app/components/Comparison.js +0 -286
  55. package/website/app/components/CssHealth.js +0 -243
  56. package/website/app/components/Extractor.js +0 -184
  57. package/website/app/components/HeroExtractor.js +0 -455
  58. package/website/app/components/Marginalia.js +0 -3
  59. package/website/app/components/McpSection.js +0 -223
  60. package/website/app/components/PlatformTabs.js +0 -250
  61. package/website/app/components/RegionsComponents.js +0 -429
  62. package/website/app/components/Rule.js +0 -13
  63. package/website/app/components/Specimens.js +0 -237
  64. package/website/app/components/StructuredData.js +0 -144
  65. package/website/app/components/TokenBrowser.js +0 -344
  66. package/website/app/components/token-browser-sample.js +0 -65
  67. package/website/app/globals.css +0 -505
  68. package/website/app/icon.svg +0 -7
  69. package/website/app/layout.js +0 -126
  70. package/website/app/opengraph-image.js +0 -170
  71. package/website/app/page.js +0 -399
  72. package/website/app/robots.js +0 -15
  73. package/website/app/seo-config.js +0 -82
  74. package/website/app/sitemap.js +0 -18
  75. package/website/jsconfig.json +0 -7
  76. package/website/lib/cache.js +0 -73
  77. package/website/lib/rate-limit.js +0 -30
  78. package/website/lib/rate-limit.test.js +0 -55
  79. package/website/lib/specimens.json +0 -86
  80. package/website/lib/token-helpers.js +0 -70
  81. package/website/lib/url-safety.js +0 -103
  82. package/website/lib/url-safety.test.js +0 -116
  83. package/website/lib/zip-files.js +0 -15
  84. package/website/next.config.mjs +0 -15
  85. package/website/package-lock.json +0 -1353
  86. package/website/package.json +0 -19
  87. package/website/public/favicon.svg +0 -7
  88. package/website/public/logo-specimen.svg +0 -76
  89. package/website/public/mark.svg +0 -12
  90. package/website/public/site.webmanifest +0 -13
@@ -1,184 +0,0 @@
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
- }
@@ -1,455 +0,0 @@
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 · &lt; 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
- }
@@ -1,3 +0,0 @@
1
- export default function Marginalia({ children }) {
2
- return <aside className="marginalia">{children}</aside>;
3
- }