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.
Files changed (92) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/.vercel/README.txt +11 -0
  6. package/.vercel/project.json +1 -0
  7. package/CHANGELOG.md +58 -0
  8. package/CONTRIBUTING.md +25 -0
  9. package/README.md +120 -8
  10. package/bin/design-extract.js +106 -3
  11. package/chrome-extension/README.md +41 -0
  12. package/chrome-extension/icons/favicon.svg +7 -0
  13. package/chrome-extension/icons/icon-128.png +0 -0
  14. package/chrome-extension/icons/icon-16.png +0 -0
  15. package/chrome-extension/icons/icon-32.png +0 -0
  16. package/chrome-extension/icons/icon-48.png +0 -0
  17. package/chrome-extension/manifest.json +26 -0
  18. package/chrome-extension/popup.html +167 -0
  19. package/chrome-extension/popup.js +59 -0
  20. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  21. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  22. package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +120 -0
  23. package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +111 -0
  24. package/package.json +5 -4
  25. package/src/config.js +26 -0
  26. package/src/crawler.js +136 -2
  27. package/src/extractors/a11y-remediation.js +47 -0
  28. package/src/extractors/component-clusters.js +39 -0
  29. package/src/extractors/css-health.js +151 -0
  30. package/src/extractors/scoring.js +20 -1
  31. package/src/extractors/semantic-regions.js +44 -0
  32. package/src/extractors/stack-fingerprint.js +88 -0
  33. package/src/formatters/_token-ref.js +44 -0
  34. package/src/formatters/agent-rules.js +116 -0
  35. package/src/formatters/android-compose.js +164 -0
  36. package/src/formatters/dtcg-tokens.js +175 -0
  37. package/src/formatters/flutter-dart.js +130 -0
  38. package/src/formatters/ios-swiftui.js +161 -0
  39. package/src/formatters/markdown.js +25 -0
  40. package/src/formatters/wordpress.js +183 -0
  41. package/src/index.js +30 -0
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils-cookies.js +73 -0
  46. package/tests/cli.test.js +50 -0
  47. package/tests/cookies.test.js +98 -0
  48. package/tests/extractors.test.js +131 -0
  49. package/tests/formatters.test.js +232 -0
  50. package/tests/mcp.test.js +68 -0
  51. package/website/app/api/extract/route.js +216 -56
  52. package/website/app/components/A11ySlider.js +369 -0
  53. package/website/app/components/Comparison.js +286 -0
  54. package/website/app/components/CssHealth.js +243 -0
  55. package/website/app/components/HeroExtractor.js +455 -0
  56. package/website/app/components/Marginalia.js +3 -0
  57. package/website/app/components/McpSection.js +223 -0
  58. package/website/app/components/PlatformTabs.js +250 -0
  59. package/website/app/components/RegionsComponents.js +429 -0
  60. package/website/app/components/Rule.js +13 -0
  61. package/website/app/components/Specimens.js +237 -0
  62. package/website/app/components/StructuredData.js +144 -0
  63. package/website/app/components/TokenBrowser.js +344 -0
  64. package/website/app/components/token-browser-sample.js +65 -0
  65. package/website/app/globals.css +415 -633
  66. package/website/app/icon.svg +7 -0
  67. package/website/app/layout.js +113 -6
  68. package/website/app/opengraph-image.js +170 -0
  69. package/website/app/page.js +325 -148
  70. package/website/app/robots.js +15 -0
  71. package/website/app/seo-config.js +82 -0
  72. package/website/app/sitemap.js +18 -0
  73. package/website/lib/cache.js +73 -0
  74. package/website/lib/rate-limit.js +30 -0
  75. package/website/lib/rate-limit.test.js +55 -0
  76. package/website/lib/specimens.json +86 -0
  77. package/website/lib/token-helpers.js +70 -0
  78. package/website/lib/url-safety.js +103 -0
  79. package/website/lib/url-safety.test.js +116 -0
  80. package/website/lib/zip-files.js +15 -0
  81. package/website/package-lock.json +85 -0
  82. package/website/package.json +1 -0
  83. package/website/public/favicon.svg +7 -0
  84. package/website/public/logo-specimen.svg +76 -0
  85. package/website/public/mark.svg +12 -0
  86. package/website/public/site.webmanifest +13 -0
  87. package/website/app/favicon.ico +0 -0
  88. package/website/public/file.svg +0 -1
  89. package/website/public/globe.svg +0 -1
  90. package/website/public/next.svg +0 -1
  91. package/website/public/vercel.svg +0 -1
  92. package/website/public/window.svg +0 -1
@@ -0,0 +1,344 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { resolveRef, flattenTokens } from '../../lib/token-helpers';
5
+ import { sampleTokens, sampleCommand } from './token-browser-sample';
6
+ import Marginalia from './Marginalia';
7
+
8
+ const REF_PATTERN = /^\{([^}]+)\}$/;
9
+ function refTarget(value) {
10
+ if (typeof value !== 'string') return null;
11
+ const m = value.match(REF_PATTERN);
12
+ return m ? m[1] : null;
13
+ }
14
+
15
+ function Swatch({ value }) {
16
+ return (
17
+ <span
18
+ aria-hidden="true"
19
+ style={{
20
+ display: 'inline-block',
21
+ width: 14,
22
+ height: 14,
23
+ border: '1px solid var(--ink)',
24
+ background: value,
25
+ verticalAlign: 'middle',
26
+ marginRight: 8,
27
+ }}
28
+ />
29
+ );
30
+ }
31
+
32
+ function ValueSample({ row, resolvedHex, highlighted }) {
33
+ const { $type, $value } = row;
34
+ if ($type === 'color') {
35
+ const display = typeof $value === 'string' && $value.startsWith('{')
36
+ ? (resolvedHex || $value)
37
+ : $value;
38
+ return (
39
+ <span className="mono" style={{ fontSize: 12, display: 'inline-flex', alignItems: 'center' }}>
40
+ {typeof display === 'string' && display.startsWith('#') ? <Swatch value={display} /> : null}
41
+ <span style={{ color: highlighted ? 'var(--accent)' : 'var(--ink)' }}>{display}</span>
42
+ </span>
43
+ );
44
+ }
45
+ if ($type === 'dimension') {
46
+ return <span className="mono" style={{ fontSize: 12 }}>{String(resolvedHex || $value)}</span>;
47
+ }
48
+ if ($type === 'typography' && typeof $value === 'object' && $value) {
49
+ const { fontFamily, fontSize, lineHeight, fontWeight } = $value;
50
+ const label = `${String(fontFamily).split(',')[0]} ${fontSize}/${lineHeight} ${fontWeight}`;
51
+ return (
52
+ <span
53
+ style={{
54
+ fontFamily: String(fontFamily),
55
+ fontSize: 13,
56
+ lineHeight: 1.3,
57
+ fontWeight: Number(fontWeight) || 400,
58
+ }}
59
+ >
60
+ {label}
61
+ </span>
62
+ );
63
+ }
64
+ return <span className="mono" style={{ fontSize: 12 }}>{JSON.stringify($value)}</span>;
65
+ }
66
+
67
+ export default function TokenBrowser() {
68
+ const tokens = sampleTokens;
69
+ const semanticRows = useMemo(() => flattenTokens(tokens, { layer: 'semantic' }), [tokens]);
70
+ const primitiveRows = useMemo(() => flattenTokens(tokens, { layer: 'primitive' }), [tokens]);
71
+
72
+ const [activeIdx, setActiveIdx] = useState(-1);
73
+ const [reduced, setReduced] = useState(false);
74
+ const semanticRefs = useRef([]);
75
+ const primitiveRefs = useRef([]);
76
+ const containerRef = useRef(null);
77
+ const [lineGeom, setLineGeom] = useState(null); // { x1, y1, x2, y2, targetPrimitivePath }
78
+
79
+ useEffect(() => {
80
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
81
+ const update = () => setReduced(mq.matches);
82
+ update();
83
+ mq.addEventListener?.('change', update);
84
+ return () => mq.removeEventListener?.('change', update);
85
+ }, []);
86
+
87
+ // Compute line geometry when activeIdx changes.
88
+ useEffect(() => {
89
+ if (activeIdx < 0 || !containerRef.current) {
90
+ setLineGeom(null);
91
+ return;
92
+ }
93
+ const row = semanticRows[activeIdx];
94
+ if (!row) return setLineGeom(null);
95
+ const target = refTarget(row.$value);
96
+ if (!target) return setLineGeom(null);
97
+
98
+ const primitiveIdx = primitiveRows.findIndex((p) => p.path === target);
99
+ if (primitiveIdx < 0) return setLineGeom(null);
100
+
101
+ const semEl = semanticRefs.current[activeIdx];
102
+ const primEl = primitiveRefs.current[primitiveIdx];
103
+ const box = containerRef.current.getBoundingClientRect();
104
+ if (!semEl || !primEl) return setLineGeom(null);
105
+ const a = semEl.getBoundingClientRect();
106
+ const b = primEl.getBoundingClientRect();
107
+
108
+ setLineGeom({
109
+ x1: a.right - box.left,
110
+ y1: a.top + a.height / 2 - box.top,
111
+ x2: b.left - box.left,
112
+ y2: b.top + b.height / 2 - box.top,
113
+ targetPrimitivePath: target,
114
+ width: box.width,
115
+ height: box.height,
116
+ });
117
+ }, [activeIdx, semanticRows, primitiveRows]);
118
+
119
+ // Recompute on resize.
120
+ useEffect(() => {
121
+ const onResize = () => setActiveIdx((i) => i); // re-trigger
122
+ window.addEventListener('resize', onResize);
123
+ return () => window.removeEventListener('resize', onResize);
124
+ }, []);
125
+
126
+ const activeRow = activeIdx >= 0 ? semanticRows[activeIdx] : null;
127
+ const activeResolved = activeRow ? resolveRef(tokens, activeRow.path) : null;
128
+ const activeTargetPath = activeRow ? refTarget(activeRow.$value) : null;
129
+
130
+ const handleKey = (e) => {
131
+ if (e.key === 'ArrowDown') {
132
+ e.preventDefault();
133
+ setActiveIdx((i) => Math.min(semanticRows.length - 1, i < 0 ? 0 : i + 1));
134
+ } else if (e.key === 'ArrowUp') {
135
+ e.preventDefault();
136
+ setActiveIdx((i) => Math.max(0, i < 0 ? 0 : i - 1));
137
+ } else if (e.key === 'Escape') {
138
+ setActiveIdx(-1);
139
+ }
140
+ };
141
+
142
+ return (
143
+ <div className="with-margin">
144
+ <div>
145
+ <h2 className="display" style={{ marginBottom: 'var(--r3)' }}>Aliases, not values.</h2>
146
+ <p className="prose" style={{ fontSize: 18, marginBottom: 'var(--r6)' }}>
147
+ v7.0 writes tokens in W3C DTCG. Hover a semantic row on the left and watch the
148
+ alias resolve through to its primitive on the right.
149
+ </p>
150
+
151
+ <div
152
+ ref={containerRef}
153
+ role="group"
154
+ aria-label="DTCG token browser"
155
+ tabIndex={0}
156
+ onKeyDown={handleKey}
157
+ style={{
158
+ position: 'relative',
159
+ display: 'grid',
160
+ gridTemplateColumns: '5fr 2fr 5fr',
161
+ columnGap: 0,
162
+ border: 'var(--hair)',
163
+ background: 'var(--paper-2)',
164
+ }}
165
+ >
166
+ {/* Left: semantic */}
167
+ <div>
168
+ <div
169
+ className="mono"
170
+ style={{
171
+ fontSize: 11,
172
+ letterSpacing: '0.12em',
173
+ textTransform: 'uppercase',
174
+ color: 'var(--ink-2)',
175
+ padding: '10px 14px',
176
+ borderBottom: 'var(--hair)',
177
+ }}
178
+ >
179
+ semantic
180
+ </div>
181
+ {semanticRows.map((row, i) => {
182
+ const isActive = i === activeIdx;
183
+ return (
184
+ <div
185
+ key={row.path}
186
+ ref={(el) => (semanticRefs.current[i] = el)}
187
+ onMouseEnter={() => setActiveIdx(i)}
188
+ onFocus={() => setActiveIdx(i)}
189
+ tabIndex={-1}
190
+ role="row"
191
+ aria-selected={isActive}
192
+ style={{
193
+ display: 'grid',
194
+ gridTemplateColumns: '1fr auto',
195
+ alignItems: 'center',
196
+ gap: 12,
197
+ padding: '10px 14px',
198
+ borderBottom: i === semanticRows.length - 1 ? 0 : '1px solid var(--ink-3)',
199
+ background: isActive ? 'var(--paper)' : 'transparent',
200
+ cursor: 'pointer',
201
+ }}
202
+ >
203
+ <span className="mono" style={{ fontSize: 12, color: 'var(--ink)' }}>
204
+ {row.path.replace(/^semantic\./, '')}
205
+ </span>
206
+ <ValueSample row={row} resolvedHex={isActive ? activeResolved : null} highlighted={isActive} />
207
+ </div>
208
+ );
209
+ })}
210
+ </div>
211
+
212
+ {/* Middle: flight-path */}
213
+ <div style={{ position: 'relative', borderLeft: '1px solid var(--ink-3)', borderRight: '1px solid var(--ink-3)' }}>
214
+ {lineGeom && (
215
+ <svg
216
+ aria-hidden="true"
217
+ style={{
218
+ position: 'absolute',
219
+ inset: 0,
220
+ width: '100%',
221
+ height: '100%',
222
+ pointerEvents: 'none',
223
+ overflow: 'visible',
224
+ }}
225
+ viewBox={`0 0 ${lineGeom.width} ${lineGeom.height}`}
226
+ preserveAspectRatio="none"
227
+ >
228
+ <FlightLine geom={lineGeom} reduced={reduced} />
229
+ </svg>
230
+ )}
231
+ </div>
232
+
233
+ {/* Right: primitive */}
234
+ <div>
235
+ <div
236
+ className="mono"
237
+ style={{
238
+ fontSize: 11,
239
+ letterSpacing: '0.12em',
240
+ textTransform: 'uppercase',
241
+ color: 'var(--ink-2)',
242
+ padding: '10px 14px',
243
+ borderBottom: 'var(--hair)',
244
+ }}
245
+ >
246
+ primitive
247
+ </div>
248
+ {primitiveRows.map((row, i) => {
249
+ const isTarget = activeTargetPath === row.path;
250
+ return (
251
+ <div
252
+ key={row.path}
253
+ ref={(el) => (primitiveRefs.current[i] = el)}
254
+ role="row"
255
+ style={{
256
+ display: 'grid',
257
+ gridTemplateColumns: '1fr auto',
258
+ alignItems: 'center',
259
+ gap: 12,
260
+ padding: '10px 14px',
261
+ borderBottom: i === primitiveRows.length - 1 ? 0 : '1px solid var(--ink-3)',
262
+ background: isTarget ? 'var(--paper)' : 'transparent',
263
+ boxShadow: isTarget ? 'inset 3px 0 0 var(--accent)' : 'none',
264
+ }}
265
+ >
266
+ <span className="mono" style={{ fontSize: 12 }}>
267
+ {row.path.replace(/^primitive\./, '')}
268
+ </span>
269
+ <ValueSample row={row} highlighted={isTarget} />
270
+ </div>
271
+ );
272
+ })}
273
+ </div>
274
+ </div>
275
+
276
+ {/* Live region for screen readers */}
277
+ <div
278
+ aria-live="polite"
279
+ style={{
280
+ position: 'absolute',
281
+ width: 1,
282
+ height: 1,
283
+ overflow: 'hidden',
284
+ clip: 'rect(0 0 0 0)',
285
+ }}
286
+ >
287
+ {activeRow
288
+ ? `${activeRow.path} resolves to ${activeTargetPath || activeRow.$value} — ${activeResolved || ''}`
289
+ : ''}
290
+ </div>
291
+ </div>
292
+
293
+ <Marginalia>
294
+ <div>taxonomy</div>
295
+ <div>
296
+ Semantic tokens describe intent. Primitive tokens describe values.
297
+ designlang writes both in W3C DTCG so your consumer can choose which layer to bind to.
298
+ </div>
299
+ <hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
300
+ <div>produced by</div>
301
+ <div><code>{sampleCommand}</code></div>
302
+ </Marginalia>
303
+ </div>
304
+ );
305
+ }
306
+
307
+ function FlightLine({ geom, reduced }) {
308
+ const { x1, y1, x2, y2 } = geom;
309
+ // Curve control points for a smooth horizontal flight path.
310
+ const midX = (x1 + x2) / 2;
311
+ const d = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
312
+
313
+ const pathRef = useRef(null);
314
+ const [len, setLen] = useState(0);
315
+ useEffect(() => {
316
+ if (pathRef.current) setLen(pathRef.current.getTotalLength());
317
+ }, [x1, y1, x2, y2]);
318
+
319
+ return (
320
+ <>
321
+ <path
322
+ ref={pathRef}
323
+ d={d}
324
+ fill="none"
325
+ stroke="var(--accent)"
326
+ strokeWidth="1.5"
327
+ strokeDasharray={reduced ? '0' : len}
328
+ strokeDashoffset={reduced ? 0 : len}
329
+ style={{
330
+ animation: reduced ? 'none' : 'designlang-flight 320ms ease-out forwards',
331
+ }}
332
+ />
333
+ <circle cx={x2} cy={y2} r="3" fill="var(--accent)" />
334
+ <style>{`
335
+ @keyframes designlang-flight {
336
+ to { stroke-dashoffset: 0; }
337
+ }
338
+ @media (prefers-reduced-motion: reduce) {
339
+ @keyframes designlang-flight { to { stroke-dashoffset: 0; } }
340
+ }
341
+ `}</style>
342
+ </>
343
+ );
344
+ }
@@ -0,0 +1,65 @@
1
+ // Hand-curated DTCG sample shaped exactly like formatDtcgTokens() output.
2
+ // Derived from the Stripe PR B smoke run; used as seed data for <TokenBrowser />.
3
+
4
+ export const sampleTokens = {
5
+ $metadata: {
6
+ generator: 'designlang',
7
+ version: '7.0.0',
8
+ spec: 'https://design-tokens.github.io/community-group/format/',
9
+ source: 'https://stripe.com',
10
+ },
11
+ primitive: {
12
+ color: {
13
+ brand: {
14
+ primary: { $value: '#533afd', $type: 'color' },
15
+ secondary: { $value: '#0a2540', $type: 'color' },
16
+ },
17
+ neutral: {
18
+ n100: { $value: '#f6f9fc', $type: 'color' },
19
+ n500: { $value: '#8792a2', $type: 'color' },
20
+ n900: { $value: '#0a2540', $type: 'color' },
21
+ },
22
+ background: {
23
+ bg0: { $value: '#ffffff', $type: 'color' },
24
+ },
25
+ },
26
+ radius: {
27
+ r0: { $value: '4px', $type: 'dimension' },
28
+ r1: { $value: '8px', $type: 'dimension' },
29
+ },
30
+ spacing: {
31
+ s2: { $value: '8px', $type: 'dimension' },
32
+ s4: { $value: '16px', $type: 'dimension' },
33
+ },
34
+ },
35
+ semantic: {
36
+ color: {
37
+ action: {
38
+ primary: { $value: '{primitive.color.brand.primary}', $type: 'color' },
39
+ },
40
+ surface: {
41
+ default: { $value: '{primitive.color.background.bg0}', $type: 'color' },
42
+ },
43
+ text: {
44
+ body: { $value: '{primitive.color.neutral.n900}', $type: 'color' },
45
+ },
46
+ },
47
+ radius: {
48
+ control: { $value: '{primitive.radius.r1}', $type: 'dimension' },
49
+ },
50
+ typography: {
51
+ body: {
52
+ $value: {
53
+ fontFamily: 'sohne-var, Helvetica Neue, Arial, sans-serif',
54
+ fontSize: '16px',
55
+ fontWeight: '400',
56
+ lineHeight: '1.5',
57
+ },
58
+ $type: 'typography',
59
+ },
60
+ },
61
+ },
62
+ };
63
+
64
+ // The CLI invocation that produced the sample.
65
+ export const sampleCommand = '$ npx designlang https://stripe.com --format dtcg';