designlang 7.0.0 → 7.2.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/og-preview.png +0 -0
- package/.github/workflows/manavarya-bot.yml +17 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/CHANGELOG.md +29 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +38 -11
- package/bin/design-extract.js +41 -2
- 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 +5 -1
- package/src/crawler.js +361 -2
- package/src/extractors/interaction-states.js +57 -0
- package/src/extractors/modern-css.js +100 -0
- package/src/extractors/token-sources.js +65 -0
- package/src/extractors/wide-gamut.js +47 -0
- package/src/formatters/routes-reconciliation.js +160 -0
- package/src/index.js +29 -0
- package/src/utils/color-gamut.js +82 -0
- package/src/utils-cookies.js +73 -0
- package/tests/cookies.test.js +98 -0
- package/tests/interaction-states.test.js +62 -0
- package/tests/modern-css.test.js +104 -0
- package/tests/routes-reconciliation.test.js +120 -0
- package/tests/wide-gamut.test.js +90 -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 +372 -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,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SITE_URL,
|
|
3
|
+
SITE_NAME,
|
|
4
|
+
SITE_DESCRIPTION,
|
|
5
|
+
SITE_KEYWORDS,
|
|
6
|
+
} from '../seo-config';
|
|
7
|
+
|
|
8
|
+
// JSON-LD structured data. All content is server-literal — no user input is
|
|
9
|
+
// interpolated into the JSON string. This matches the Next.js App Router
|
|
10
|
+
// metadata docs' recommended inline-script pattern.
|
|
11
|
+
// https://nextjs.org/docs/app/building-your-application/optimizing/metadata#json-ld
|
|
12
|
+
export default function StructuredData() {
|
|
13
|
+
const graph = {
|
|
14
|
+
'@context': 'https://schema.org',
|
|
15
|
+
'@graph': [
|
|
16
|
+
{
|
|
17
|
+
'@type': 'SoftwareApplication',
|
|
18
|
+
'@id': `${SITE_URL}/#software`,
|
|
19
|
+
name: 'designlang',
|
|
20
|
+
alternateName: ['design-extract', 'designlang CLI'],
|
|
21
|
+
description: SITE_DESCRIPTION,
|
|
22
|
+
applicationCategory: 'DeveloperApplication',
|
|
23
|
+
applicationSubCategory: 'Design System Extractor',
|
|
24
|
+
operatingSystem: 'macOS, Windows, Linux',
|
|
25
|
+
softwareVersion: '7.0.0',
|
|
26
|
+
downloadUrl: 'https://www.npmjs.com/package/designlang',
|
|
27
|
+
installUrl: 'https://www.npmjs.com/package/designlang',
|
|
28
|
+
url: SITE_URL,
|
|
29
|
+
offers: {
|
|
30
|
+
'@type': 'Offer',
|
|
31
|
+
price: '0',
|
|
32
|
+
priceCurrency: 'USD',
|
|
33
|
+
availability: 'https://schema.org/InStock',
|
|
34
|
+
},
|
|
35
|
+
license: 'https://opensource.org/licenses/MIT',
|
|
36
|
+
codeRepository: 'https://github.com/Manavarya09/design-extract',
|
|
37
|
+
programmingLanguage: ['JavaScript', 'TypeScript'],
|
|
38
|
+
runtimePlatform: 'Node.js 20+',
|
|
39
|
+
featureList: [
|
|
40
|
+
'Reverse-engineer any website into a W3C DTCG design system',
|
|
41
|
+
'Emits primitive, semantic, and composite tokens',
|
|
42
|
+
'Multi-platform output: Tailwind, CSS variables, Figma variables, shadcn/ui, React, Vue, Svelte, iOS SwiftUI, Android Compose, Flutter, WordPress block theme',
|
|
43
|
+
'Stdio MCP server exposing tokens, regions, components, CSS health, and remediation',
|
|
44
|
+
'Agent rule emitter for Cursor, Claude Code, Windsurf, AGENTS.md',
|
|
45
|
+
'CSS health audit: specificity graph, !important count, unused CSS via Coverage API, @keyframes catalog',
|
|
46
|
+
'WCAG accessibility remediation with nearest passing palette color',
|
|
47
|
+
'Semantic region classifier (nav, hero, pricing, testimonials, footer, and more)',
|
|
48
|
+
'Reusable component clustering with variant detection',
|
|
49
|
+
'Dark mode diff, authenticated page crawling, responsive breakpoint capture',
|
|
50
|
+
],
|
|
51
|
+
keywords: SITE_KEYWORDS.slice(0, 25).join(', '),
|
|
52
|
+
author: {
|
|
53
|
+
'@type': 'Person',
|
|
54
|
+
name: 'Manav Arya Singh',
|
|
55
|
+
url: 'https://manavaryasingh.com',
|
|
56
|
+
},
|
|
57
|
+
publisher: {
|
|
58
|
+
'@type': 'Person',
|
|
59
|
+
name: 'Manav Arya Singh',
|
|
60
|
+
url: 'https://manavaryasingh.com',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
'@type': 'WebSite',
|
|
65
|
+
'@id': `${SITE_URL}/#website`,
|
|
66
|
+
url: SITE_URL,
|
|
67
|
+
name: SITE_NAME,
|
|
68
|
+
description: SITE_DESCRIPTION,
|
|
69
|
+
publisher: { '@id': `${SITE_URL}/#person` },
|
|
70
|
+
inLanguage: 'en-US',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
'@type': 'Person',
|
|
74
|
+
'@id': `${SITE_URL}/#person`,
|
|
75
|
+
name: 'Manav Arya Singh',
|
|
76
|
+
url: 'https://manavaryasingh.com',
|
|
77
|
+
sameAs: [
|
|
78
|
+
'https://github.com/Manavarya09',
|
|
79
|
+
'https://www.npmjs.com/~manavarya0909',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
'@type': 'BreadcrumbList',
|
|
84
|
+
'@id': `${SITE_URL}/#breadcrumbs`,
|
|
85
|
+
itemListElement: [
|
|
86
|
+
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
|
|
87
|
+
{ '@type': 'ListItem', position: 2, name: 'Extract', item: `${SITE_URL}/#extract` },
|
|
88
|
+
{ '@type': 'ListItem', position: 3, name: 'Features', item: `${SITE_URL}/#features` },
|
|
89
|
+
{ '@type': 'ListItem', position: 4, name: 'Specimens', item: `${SITE_URL}/#specimens` },
|
|
90
|
+
{ '@type': 'ListItem', position: 5, name: 'Install', item: `${SITE_URL}/#install` },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
'@type': 'FAQPage',
|
|
95
|
+
'@id': `${SITE_URL}/#faq`,
|
|
96
|
+
mainEntity: [
|
|
97
|
+
{
|
|
98
|
+
'@type': 'Question',
|
|
99
|
+
name: 'What is designlang?',
|
|
100
|
+
acceptedAnswer: {
|
|
101
|
+
'@type': 'Answer',
|
|
102
|
+
text: 'designlang is an open-source CLI tool that crawls any live website with Playwright and emits its complete design system in W3C DTCG token format, plus Tailwind, CSS variables, Figma variables, shadcn/ui theme, and native emitters for iOS SwiftUI, Android Compose, Flutter, and WordPress block themes.',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
'@type': 'Question',
|
|
107
|
+
name: 'Does designlang work with AI coding agents like Claude Code and Cursor?',
|
|
108
|
+
acceptedAnswer: {
|
|
109
|
+
'@type': 'Answer',
|
|
110
|
+
text: 'Yes. designlang ships a stdio MCP server (designlang mcp) that exposes extracted tokens, regions, components, CSS health, and accessibility remediation as MCP resources and tools, plus an --emit-agent-rules flag that writes .cursor/rules/designlang.mdc, .claude/skills/designlang/SKILL.md, CLAUDE.md.fragment, and agents.md.',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
'@type': 'Question',
|
|
115
|
+
name: 'How is designlang different from Style Dictionary, Subframe, v0, Builder.io, and Project Wallace?',
|
|
116
|
+
acceptedAnswer: {
|
|
117
|
+
'@type': 'Answer',
|
|
118
|
+
text: 'designlang extracts from a live rendered URL, not a Figma file or a manually-authored token source. It outputs strict DTCG with a semantic/primitive layering and composite tokens, ships multi-platform emitters (iOS, Android, Flutter, WordPress) from one extraction, runs a CSS health audit plus a11y remediation, classifies page regions, and detects reusable components automatically. It is open-source under MIT and runs locally as a CLI or MCP server.',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
'@type': 'Question',
|
|
123
|
+
name: 'Is designlang free and open source?',
|
|
124
|
+
acceptedAnswer: {
|
|
125
|
+
'@type': 'Answer',
|
|
126
|
+
text: 'Yes. designlang is MIT-licensed, free, and fully open-source. The CLI installs with npx designlang <url> and requires only Node.js 20 or later. The website at designlang.manavaryasingh.com offers a free rate-limited extraction for quick demos.',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
'@type': 'Question',
|
|
131
|
+
name: 'What output formats does designlang support?',
|
|
132
|
+
acceptedAnswer: {
|
|
133
|
+
'@type': 'Answer',
|
|
134
|
+
text: 'W3C DTCG JSON (primitive, semantic, composite layers), Tailwind CSS config, CSS custom properties, Figma Variables JSON, shadcn/ui theme, React / Vue / Svelte theme objects, iOS SwiftUI Color + CGFloat extensions, Android Jetpack Compose Theme.kt and colors.xml, Flutter ThemeData, and a WordPress block theme skeleton (theme.json, style.css, templates).',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const json = JSON.stringify(graph).replace(/</g, '\\u003c');
|
|
143
|
+
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: json }} />;
|
|
144
|
+
}
|
|
@@ -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';
|