designlang 7.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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +9 -7
- package/bin/design-extract.js +18 -1
- 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/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 +1 -1
- package/src/config.js +3 -0
- package/src/crawler.js +20 -2
- package/src/utils-cookies.js +73 -0
- package/tests/cookies.test.js +98 -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,369 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// §05 A11y remediation — interactive contrast slider.
|
|
4
|
+
// WCAG relative-luminance formula copied here (~12 lines) so this stays
|
|
5
|
+
// client-side and doesn't import from the CLI package.
|
|
6
|
+
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import Rule from './Rule';
|
|
9
|
+
import Marginalia from './Marginalia';
|
|
10
|
+
|
|
11
|
+
// ── WCAG helpers ────────────────────────────────────────────────
|
|
12
|
+
function toRgb(hex) {
|
|
13
|
+
const h = String(hex || '').replace('#', '');
|
|
14
|
+
const n = h.length === 3 ? h.split('').map((x) => x + x).join('') : h;
|
|
15
|
+
const i = parseInt(n, 16);
|
|
16
|
+
return [(i >> 16) & 255, (i >> 8) & 255, i & 255];
|
|
17
|
+
}
|
|
18
|
+
function relLum([r, g, b]) {
|
|
19
|
+
const f = (c) => {
|
|
20
|
+
const s = c / 255;
|
|
21
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
22
|
+
};
|
|
23
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
24
|
+
}
|
|
25
|
+
function contrast(a, b) {
|
|
26
|
+
const la = relLum(toRgb(a));
|
|
27
|
+
const lb = relLum(toRgb(b));
|
|
28
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
29
|
+
}
|
|
30
|
+
function lerpHex(a, b, t) {
|
|
31
|
+
const [ar, ag, ab] = toRgb(a);
|
|
32
|
+
const [br, bg, bb] = toRgb(b);
|
|
33
|
+
const r = Math.round(ar + (br - ar) * t);
|
|
34
|
+
const g = Math.round(ag + (bg - ag) * t);
|
|
35
|
+
const bl = Math.round(ab + (bb - ab) * t);
|
|
36
|
+
return (
|
|
37
|
+
'#' +
|
|
38
|
+
[r, g, bl].map((x) => x.toString(16).padStart(2, '0')).join('').toUpperCase()
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Demo data (shaped like remediateFailingPairs output) ────────
|
|
43
|
+
const BG = '#F3F1EA';
|
|
44
|
+
const FAIL_FG = '#B8B199';
|
|
45
|
+
const PASS_FG = '#403C34';
|
|
46
|
+
|
|
47
|
+
const REMEDIATIONS = [
|
|
48
|
+
{
|
|
49
|
+
fg: '#B8B199',
|
|
50
|
+
bg: '#F3F1EA',
|
|
51
|
+
ratio: 2.11,
|
|
52
|
+
rule: 'AA-normal',
|
|
53
|
+
suggestion: { replace: 'fg', color: '#403C34', newRatio: 7.84 },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
fg: '#8B8778',
|
|
57
|
+
bg: '#F3F1EA',
|
|
58
|
+
ratio: 3.28,
|
|
59
|
+
rule: 'AA-normal',
|
|
60
|
+
suggestion: { replace: 'fg', color: '#0A0908', newRatio: 15.92 },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
fg: '#FF4800',
|
|
64
|
+
bg: '#ECE8DD',
|
|
65
|
+
ratio: 3.51,
|
|
66
|
+
rule: 'AAA-normal',
|
|
67
|
+
suggestion: { replace: 'fg', color: '#0A0908', newRatio: 15.31 },
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function tagFor(ratio) {
|
|
72
|
+
if (ratio >= 7) return { label: 'AAA', color: 'var(--ink)', underline: true };
|
|
73
|
+
if (ratio >= 4.5) return { label: 'AA', color: 'var(--accent)', underline: false };
|
|
74
|
+
if (ratio >= 3) return { label: 'AA large', color: 'var(--accent)', underline: false };
|
|
75
|
+
return { label: 'FAIL', color: 'var(--ink)', underline: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function A11ySlider() {
|
|
79
|
+
const [t, setT] = useState(0);
|
|
80
|
+
const fg = useMemo(() => lerpHex(FAIL_FG, PASS_FG, t / 100), [t]);
|
|
81
|
+
const ratio = useMemo(() => contrast(fg, BG), [fg]);
|
|
82
|
+
const tag = tagFor(ratio);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<Rule number="05" label="A11y remediation" />
|
|
87
|
+
<div className="with-margin" style={{ marginTop: 'var(--r5)' }}>
|
|
88
|
+
<div>
|
|
89
|
+
<div className="eyebrow" style={{ marginBottom: 'var(--r3)' }}>§05 A11y remediation</div>
|
|
90
|
+
<h2 className="display" style={{ marginBottom: 'var(--r4)' }}>
|
|
91
|
+
From score to fix.
|
|
92
|
+
</h2>
|
|
93
|
+
<p className="prose" style={{ fontSize: 18, maxWidth: '62ch', color: 'var(--ink-2)' }}>
|
|
94
|
+
Most tools hand you a failing contrast ratio. designlang hands you the next color in
|
|
95
|
+
your own palette that passes AA. Drag to see the difference.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
className="grid-12"
|
|
100
|
+
style={{ marginTop: 'var(--r7)', alignItems: 'start' }}
|
|
101
|
+
>
|
|
102
|
+
{/* LEFT — slider tiles (span 6) */}
|
|
103
|
+
<div style={{ gridColumn: 'span 6' }}>
|
|
104
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--r4)' }}>
|
|
105
|
+
{/* Failing tile (static reference) */}
|
|
106
|
+
<Tile
|
|
107
|
+
bg={BG}
|
|
108
|
+
fg={FAIL_FG}
|
|
109
|
+
ratio={contrast(FAIL_FG, BG)}
|
|
110
|
+
label="failing pair"
|
|
111
|
+
staticTag={{ label: 'FAIL', color: 'var(--ink)', underline: false }}
|
|
112
|
+
/>
|
|
113
|
+
{/* Live tile driven by slider */}
|
|
114
|
+
<Tile
|
|
115
|
+
bg={BG}
|
|
116
|
+
fg={fg}
|
|
117
|
+
ratio={ratio}
|
|
118
|
+
label="remediated"
|
|
119
|
+
staticTag={tag}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style={{ marginTop: 'var(--r5)' }}>
|
|
124
|
+
<label
|
|
125
|
+
htmlFor="a11y-range"
|
|
126
|
+
className="mono"
|
|
127
|
+
style={{
|
|
128
|
+
fontSize: 11,
|
|
129
|
+
letterSpacing: '0.14em',
|
|
130
|
+
textTransform: 'uppercase',
|
|
131
|
+
color: 'var(--ink-2)',
|
|
132
|
+
display: 'block',
|
|
133
|
+
marginBottom: 8,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
drag: failing → remediated
|
|
137
|
+
</label>
|
|
138
|
+
<input
|
|
139
|
+
id="a11y-range"
|
|
140
|
+
type="range"
|
|
141
|
+
min="0"
|
|
142
|
+
max="100"
|
|
143
|
+
value={t}
|
|
144
|
+
onChange={(e) => setT(Number(e.target.value))}
|
|
145
|
+
aria-valuetext={`contrast ratio ${ratio.toFixed(2)} to 1`}
|
|
146
|
+
className="a11y-range"
|
|
147
|
+
style={{ width: '100%' }}
|
|
148
|
+
/>
|
|
149
|
+
<div
|
|
150
|
+
className="mono"
|
|
151
|
+
style={{
|
|
152
|
+
display: 'flex',
|
|
153
|
+
justifyContent: 'space-between',
|
|
154
|
+
marginTop: 6,
|
|
155
|
+
fontSize: 11,
|
|
156
|
+
color: 'var(--ink-3)',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<span>{FAIL_FG}</span>
|
|
160
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{fg}</span>
|
|
161
|
+
<span>{PASS_FG}</span>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* RIGHT — remediation list (span 6) */}
|
|
167
|
+
<div style={{ gridColumn: 'span 6' }}>
|
|
168
|
+
<div
|
|
169
|
+
className="mono"
|
|
170
|
+
style={{
|
|
171
|
+
fontSize: 11,
|
|
172
|
+
letterSpacing: '0.14em',
|
|
173
|
+
textTransform: 'uppercase',
|
|
174
|
+
color: 'var(--ink-2)',
|
|
175
|
+
marginBottom: 'var(--r3)',
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
remediateFailingPairs() output
|
|
179
|
+
</div>
|
|
180
|
+
<div
|
|
181
|
+
role="table"
|
|
182
|
+
style={{
|
|
183
|
+
border: '1px solid var(--ink)',
|
|
184
|
+
fontFamily: 'var(--font-mono)',
|
|
185
|
+
fontSize: 12,
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<div
|
|
189
|
+
role="row"
|
|
190
|
+
style={{
|
|
191
|
+
display: 'grid',
|
|
192
|
+
gridTemplateColumns: '1.3fr 0.3fr 1.3fr 0.9fr 0.7fr',
|
|
193
|
+
padding: '8px 12px',
|
|
194
|
+
borderBottom: '1px solid var(--ink)',
|
|
195
|
+
color: 'var(--ink-3)',
|
|
196
|
+
letterSpacing: '0.08em',
|
|
197
|
+
textTransform: 'uppercase',
|
|
198
|
+
fontSize: 10,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<span>original</span>
|
|
202
|
+
<span />
|
|
203
|
+
<span>suggested</span>
|
|
204
|
+
<span>ratio</span>
|
|
205
|
+
<span>rule</span>
|
|
206
|
+
</div>
|
|
207
|
+
{REMEDIATIONS.map((r, i) => (
|
|
208
|
+
<div
|
|
209
|
+
key={i}
|
|
210
|
+
role="row"
|
|
211
|
+
style={{
|
|
212
|
+
display: 'grid',
|
|
213
|
+
gridTemplateColumns: '1.3fr 0.3fr 1.3fr 0.9fr 0.7fr',
|
|
214
|
+
padding: '10px 12px',
|
|
215
|
+
borderTop: i === 0 ? 0 : '1px solid var(--ink-3)',
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<span>
|
|
220
|
+
<Swatch color={r.fg} /> {r.fg}{' '}
|
|
221
|
+
<span style={{ color: 'var(--ink-3)' }}>on</span>{' '}
|
|
222
|
+
<Swatch color={r.bg} />
|
|
223
|
+
</span>
|
|
224
|
+
<span style={{ color: 'var(--ink-3)', textAlign: 'center' }}>→</span>
|
|
225
|
+
<span>
|
|
226
|
+
<Swatch color={r.suggestion.color} /> {r.suggestion.color}
|
|
227
|
+
</span>
|
|
228
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
229
|
+
<span style={{ color: 'var(--ink-3)' }}>{r.ratio.toFixed(2)}</span>
|
|
230
|
+
<span style={{ color: 'var(--ink-3)' }}> → </span>
|
|
231
|
+
<span style={{ color: 'var(--accent)' }}>
|
|
232
|
+
{r.suggestion.newRatio.toFixed(2)}
|
|
233
|
+
</span>
|
|
234
|
+
</span>
|
|
235
|
+
<span style={{ color: 'var(--ink-2)' }}>{r.rule}</span>
|
|
236
|
+
</div>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
<p
|
|
240
|
+
className="mono"
|
|
241
|
+
style={{
|
|
242
|
+
marginTop: 'var(--r3)',
|
|
243
|
+
fontSize: 11,
|
|
244
|
+
color: 'var(--ink-3)',
|
|
245
|
+
maxWidth: '52ch',
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
designlang only suggests colors that already exist in the extracted palette. No
|
|
249
|
+
invented tokens.
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<Marginalia>
|
|
256
|
+
<div>WCAG thresholds</div>
|
|
257
|
+
<div style={{ marginTop: 6 }}>
|
|
258
|
+
<div><code>AA normal 4.5:1</code></div>
|
|
259
|
+
<div><code>AA large 3:1</code></div>
|
|
260
|
+
<div><code>AAA normal 7:1</code></div>
|
|
261
|
+
<div><code>AAA large 4.5:1</code></div>
|
|
262
|
+
</div>
|
|
263
|
+
<hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
|
|
264
|
+
<p className="foot">
|
|
265
|
+
Run <code>designlang <url></code> and every failing pair ships with a drop-in fix.
|
|
266
|
+
</p>
|
|
267
|
+
</Marginalia>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<style jsx>{`
|
|
271
|
+
.a11y-range {
|
|
272
|
+
-webkit-appearance: none;
|
|
273
|
+
appearance: none;
|
|
274
|
+
height: 2px;
|
|
275
|
+
background: var(--ink);
|
|
276
|
+
outline: 0;
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
}
|
|
279
|
+
.a11y-range::-webkit-slider-thumb {
|
|
280
|
+
-webkit-appearance: none;
|
|
281
|
+
appearance: none;
|
|
282
|
+
width: 18px;
|
|
283
|
+
height: 18px;
|
|
284
|
+
background: var(--paper);
|
|
285
|
+
border: 2px solid var(--ink);
|
|
286
|
+
border-radius: 0;
|
|
287
|
+
cursor: grab;
|
|
288
|
+
}
|
|
289
|
+
.a11y-range::-moz-range-thumb {
|
|
290
|
+
width: 18px;
|
|
291
|
+
height: 18px;
|
|
292
|
+
background: var(--paper);
|
|
293
|
+
border: 2px solid var(--ink);
|
|
294
|
+
border-radius: 0;
|
|
295
|
+
cursor: grab;
|
|
296
|
+
}
|
|
297
|
+
.a11y-range:focus-visible {
|
|
298
|
+
outline: 2px solid var(--accent);
|
|
299
|
+
outline-offset: 3px;
|
|
300
|
+
}
|
|
301
|
+
`}</style>
|
|
302
|
+
</>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function Swatch({ color }) {
|
|
307
|
+
return (
|
|
308
|
+
<span
|
|
309
|
+
aria-hidden="true"
|
|
310
|
+
style={{
|
|
311
|
+
display: 'inline-block',
|
|
312
|
+
width: 10,
|
|
313
|
+
height: 10,
|
|
314
|
+
background: color,
|
|
315
|
+
border: '1px solid var(--ink)',
|
|
316
|
+
verticalAlign: 'middle',
|
|
317
|
+
marginRight: 4,
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Tile({ bg, fg, ratio, label, staticTag }) {
|
|
324
|
+
return (
|
|
325
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--r3)' }}>
|
|
326
|
+
<div
|
|
327
|
+
style={{
|
|
328
|
+
width: '100%',
|
|
329
|
+
aspectRatio: '1 / 1',
|
|
330
|
+
maxWidth: 220,
|
|
331
|
+
maxHeight: 220,
|
|
332
|
+
background: bg,
|
|
333
|
+
border: '1px solid var(--ink)',
|
|
334
|
+
display: 'flex',
|
|
335
|
+
alignItems: 'center',
|
|
336
|
+
justifyContent: 'center',
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
<span
|
|
340
|
+
className="display"
|
|
341
|
+
style={{ color: fg, fontSize: 'clamp(36px, 4vw, 64px)', letterSpacing: '-0.03em' }}
|
|
342
|
+
>
|
|
343
|
+
Aa
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="mono" style={{ fontSize: 11, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
347
|
+
<span style={{ color: 'var(--ink-3)', letterSpacing: '0.1em', textTransform: 'uppercase' }}>
|
|
348
|
+
{label}
|
|
349
|
+
</span>
|
|
350
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
351
|
+
{fg.toUpperCase()} <span style={{ color: 'var(--ink-3)' }}>on</span> {bg.toUpperCase()}
|
|
352
|
+
</span>
|
|
353
|
+
<span
|
|
354
|
+
style={{
|
|
355
|
+
color: staticTag.color,
|
|
356
|
+
fontVariantNumeric: 'tabular-nums',
|
|
357
|
+
letterSpacing: '0.06em',
|
|
358
|
+
textTransform: 'uppercase',
|
|
359
|
+
textDecoration: staticTag.underline ? 'underline' : 'none',
|
|
360
|
+
textDecorationColor: 'var(--accent)',
|
|
361
|
+
textUnderlineOffset: 3,
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
{staticTag.label} {ratio.toFixed(2)}:1
|
|
365
|
+
</span>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import Marginalia from './Marginalia';
|
|
2
|
+
|
|
3
|
+
const TOOLS = [
|
|
4
|
+
'designlang',
|
|
5
|
+
'v0',
|
|
6
|
+
'Builder.io Visual Copilot',
|
|
7
|
+
'Style Dictionary',
|
|
8
|
+
'Subframe',
|
|
9
|
+
'Project Wallace',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// Y = yes, N = no, P = partial
|
|
13
|
+
const ROWS = [
|
|
14
|
+
{
|
|
15
|
+
feature: 'Extracts from a live URL',
|
|
16
|
+
cells: ['Y', 'N', 'Y', 'N', 'N', 'Y'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
feature: 'Emits W3C DTCG tokens',
|
|
20
|
+
cells: ['Y', 'N', 'N', 'Y', 'N', 'N'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
feature: 'Semantic alias layer',
|
|
24
|
+
cells: ['Y', 'N', 'N', 'Y', 'P', 'N'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
feature: 'Multi-platform output (iOS, Android, Flutter)',
|
|
28
|
+
cells: ['Y', 'N', 'N', 'Y', 'N', 'N'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
feature: 'MCP server over stdio',
|
|
32
|
+
cells: ['Y', 'N', 'N', 'N', 'N', 'N'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
feature: 'CSS health audit',
|
|
36
|
+
cells: ['Y', 'N', 'N', 'N', 'N', 'Y'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
feature: 'A11y remediation suggestions',
|
|
40
|
+
cells: ['P', 'N', 'P', 'N', 'N', 'P'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
feature: 'Component cluster detection',
|
|
44
|
+
cells: ['P', 'Y', 'Y', 'N', 'Y', 'N'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
feature: 'Offline / local-only',
|
|
48
|
+
cells: ['Y', 'N', 'N', 'Y', 'N', 'N'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
feature: 'Open source / MIT',
|
|
52
|
+
cells: ['Y', 'N', 'N', 'Y', 'N', 'P'],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function Mark({ kind }) {
|
|
57
|
+
if (kind === 'Y') {
|
|
58
|
+
return (
|
|
59
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-label="yes" role="img">
|
|
60
|
+
<rect x="2" y="2" width="10" height="10" fill="var(--ink)" transform="rotate(45 7 7)" />
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (kind === 'P') {
|
|
65
|
+
return (
|
|
66
|
+
<svg width="16" height="16" viewBox="0 0 16 16" aria-label="partial" role="img">
|
|
67
|
+
<rect
|
|
68
|
+
x="3"
|
|
69
|
+
y="3"
|
|
70
|
+
width="10"
|
|
71
|
+
height="10"
|
|
72
|
+
fill="var(--accent)"
|
|
73
|
+
stroke="var(--ink)"
|
|
74
|
+
strokeWidth="1"
|
|
75
|
+
transform="rotate(45 8 8)"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
<span className="mono" style={{ color: 'var(--ink-3)', fontSize: 14 }} aria-label="no">
|
|
82
|
+
—
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default function Comparison() {
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<div className="with-margin" style={{ marginTop: 'var(--r5)', marginBottom: 'var(--r7)' }}>
|
|
91
|
+
<div>
|
|
92
|
+
<div className="section-label" style={{ marginBottom: 'var(--r5)' }}>
|
|
93
|
+
<span>§08 — Compared</span>
|
|
94
|
+
</div>
|
|
95
|
+
<h2 className="display" style={{ marginBottom: 'var(--r4)' }}>
|
|
96
|
+
Where designlang doesn't win,<br />
|
|
97
|
+
<em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>it says so.</em>
|
|
98
|
+
</h2>
|
|
99
|
+
<p className="prose" style={{ fontSize: 18, lineHeight: 1.5, maxWidth: '62ch' }}>
|
|
100
|
+
We picked five tools doing closely adjacent work. The matrix below is our honest
|
|
101
|
+
assessment on 2026-04-18, written by someone who actually uses all six.
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
<Marginalia>
|
|
105
|
+
<div>legend</div>
|
|
106
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
|
|
107
|
+
<Mark kind="Y" /> <span>supported</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
|
|
110
|
+
<Mark kind="P" /> <span>partial</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
|
|
113
|
+
<Mark kind="N" /> <span>not supported</span>
|
|
114
|
+
</div>
|
|
115
|
+
<hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
|
|
116
|
+
<p className="foot">
|
|
117
|
+
Open a PR if this matrix is wrong for your tool. We'll update it or explain
|
|
118
|
+
our read.
|
|
119
|
+
</p>
|
|
120
|
+
</Marginalia>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style={{ overflowX: 'auto', marginBottom: 'var(--r7)' }}>
|
|
124
|
+
<table
|
|
125
|
+
style={{
|
|
126
|
+
width: '100%',
|
|
127
|
+
minWidth: 900,
|
|
128
|
+
borderCollapse: 'collapse',
|
|
129
|
+
tableLayout: 'fixed',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<thead>
|
|
133
|
+
<tr>
|
|
134
|
+
<th
|
|
135
|
+
scope="col"
|
|
136
|
+
className="mono"
|
|
137
|
+
style={{
|
|
138
|
+
textAlign: 'left',
|
|
139
|
+
fontSize: 12,
|
|
140
|
+
textTransform: 'uppercase',
|
|
141
|
+
letterSpacing: '0.12em',
|
|
142
|
+
color: 'var(--ink-2)',
|
|
143
|
+
padding: '12px 20px 12px 0',
|
|
144
|
+
borderBottom: '1px solid var(--ink)',
|
|
145
|
+
fontWeight: 400,
|
|
146
|
+
width: '28%',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
Feature
|
|
150
|
+
</th>
|
|
151
|
+
{TOOLS.map((t, i) => (
|
|
152
|
+
<th
|
|
153
|
+
key={t}
|
|
154
|
+
scope="col"
|
|
155
|
+
className="mono"
|
|
156
|
+
style={{
|
|
157
|
+
textAlign: 'center',
|
|
158
|
+
fontSize: 12,
|
|
159
|
+
textTransform: 'uppercase',
|
|
160
|
+
letterSpacing: '0.08em',
|
|
161
|
+
color: i === 0 ? 'var(--ink)' : 'var(--ink-2)',
|
|
162
|
+
padding: '12px 8px',
|
|
163
|
+
borderBottom: '1px solid var(--ink)',
|
|
164
|
+
fontWeight: 400,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{t}
|
|
168
|
+
</th>
|
|
169
|
+
))}
|
|
170
|
+
</tr>
|
|
171
|
+
</thead>
|
|
172
|
+
<tbody>
|
|
173
|
+
{ROWS.map((row, rIdx) => (
|
|
174
|
+
<tr key={row.feature}>
|
|
175
|
+
<th
|
|
176
|
+
scope="row"
|
|
177
|
+
style={{
|
|
178
|
+
textAlign: 'left',
|
|
179
|
+
padding: '14px 20px 14px 0',
|
|
180
|
+
fontWeight: 400,
|
|
181
|
+
fontSize: 15,
|
|
182
|
+
color: 'var(--ink)',
|
|
183
|
+
borderBottom: rIdx === ROWS.length - 1 ? 0 : '1px solid var(--paper-3)',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{row.feature}
|
|
187
|
+
</th>
|
|
188
|
+
{row.cells.map((c, i) => (
|
|
189
|
+
<td
|
|
190
|
+
key={i}
|
|
191
|
+
style={{
|
|
192
|
+
textAlign: 'center',
|
|
193
|
+
padding: '14px 8px',
|
|
194
|
+
verticalAlign: 'middle',
|
|
195
|
+
borderBottom: rIdx === ROWS.length - 1 ? 0 : '1px solid var(--paper-3)',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
199
|
+
<Mark kind={c} />
|
|
200
|
+
</span>
|
|
201
|
+
</td>
|
|
202
|
+
))}
|
|
203
|
+
</tr>
|
|
204
|
+
))}
|
|
205
|
+
</tbody>
|
|
206
|
+
</table>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="with-margin">
|
|
210
|
+
<div>
|
|
211
|
+
<h3
|
|
212
|
+
className="mono"
|
|
213
|
+
style={{
|
|
214
|
+
fontSize: 12,
|
|
215
|
+
textTransform: 'uppercase',
|
|
216
|
+
letterSpacing: '0.14em',
|
|
217
|
+
color: 'var(--ink-2)',
|
|
218
|
+
marginBottom: 'var(--r4)',
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
What designlang is not
|
|
222
|
+
</h3>
|
|
223
|
+
<ul
|
|
224
|
+
style={{
|
|
225
|
+
listStyle: 'none',
|
|
226
|
+
padding: 0,
|
|
227
|
+
display: 'grid',
|
|
228
|
+
gap: 'var(--r3)',
|
|
229
|
+
maxWidth: '72ch',
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<li
|
|
233
|
+
style={{
|
|
234
|
+
borderLeft: '1px solid var(--ink)',
|
|
235
|
+
paddingLeft: 'var(--r4)',
|
|
236
|
+
fontSize: 16,
|
|
237
|
+
lineHeight: 1.5,
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<em style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic' }}>
|
|
241
|
+
It is not a design-to-code generator.
|
|
242
|
+
</em>{' '}
|
|
243
|
+
It extracts the <strong>system</strong>, not the components as JSX.
|
|
244
|
+
</li>
|
|
245
|
+
<li
|
|
246
|
+
style={{
|
|
247
|
+
borderLeft: '1px solid var(--ink)',
|
|
248
|
+
paddingLeft: 'var(--r4)',
|
|
249
|
+
fontSize: 16,
|
|
250
|
+
lineHeight: 1.5,
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<em style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic' }}>
|
|
254
|
+
It is not a hosted cloud service.
|
|
255
|
+
</em>{' '}
|
|
256
|
+
The free website extractor is rate-limited; production use should run the CLI or
|
|
257
|
+
MCP server locally.
|
|
258
|
+
</li>
|
|
259
|
+
<li
|
|
260
|
+
style={{
|
|
261
|
+
borderLeft: '1px solid var(--ink)',
|
|
262
|
+
paddingLeft: 'var(--r4)',
|
|
263
|
+
fontSize: 16,
|
|
264
|
+
lineHeight: 1.5,
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<em style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic' }}>
|
|
268
|
+
It is not a Figma plugin.
|
|
269
|
+
</em>{' '}
|
|
270
|
+
designlang reads the <strong>rendered DOM</strong>, not the Figma file — the
|
|
271
|
+
output is what your users actually see, not what a designer intended.
|
|
272
|
+
</li>
|
|
273
|
+
</ul>
|
|
274
|
+
</div>
|
|
275
|
+
<Marginalia>
|
|
276
|
+
<div>honesty clause</div>
|
|
277
|
+
<p className="foot" style={{ marginTop: 6 }}>
|
|
278
|
+
Two partial cells for designlang on purpose — the a11y remediation engine
|
|
279
|
+
suggests fixes but won't rewrite your markup, and component cluster
|
|
280
|
+
detection is heuristic, not vision-based.
|
|
281
|
+
</p>
|
|
282
|
+
</Marginalia>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|