designlang 6.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +120 -8
- package/bin/design-extract.js +106 -3
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/icons/favicon.svg +7 -0
- package/chrome-extension/icons/icon-128.png +0 -0
- package/chrome-extension/icons/icon-16.png +0 -0
- package/chrome-extension/icons/icon-32.png +0 -0
- package/chrome-extension/icons/icon-48.png +0 -0
- package/chrome-extension/manifest.json +26 -0
- package/chrome-extension/popup.html +167 -0
- package/chrome-extension/popup.js +59 -0
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
- package/docs/superpowers/specs/2026-04-18-website-redesign-design.md +120 -0
- package/docs/superpowers/specs/2026-04-19-designlang-v7-1-design.md +111 -0
- package/package.json +5 -4
- package/src/config.js +26 -0
- package/src/crawler.js +136 -2
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/stack-fingerprint.js +88 -0
- package/src/formatters/_token-ref.js +44 -0
- package/src/formatters/agent-rules.js +116 -0
- package/src/formatters/android-compose.js +164 -0
- package/src/formatters/dtcg-tokens.js +175 -0
- package/src/formatters/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/wordpress.js +183 -0
- package/src/index.js +30 -0
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/src/utils-cookies.js +73 -0
- package/tests/cli.test.js +50 -0
- package/tests/cookies.test.js +98 -0
- package/tests/extractors.test.js +131 -0
- package/tests/formatters.test.js +232 -0
- package/tests/mcp.test.js +68 -0
- package/website/app/api/extract/route.js +216 -56
- package/website/app/components/A11ySlider.js +369 -0
- package/website/app/components/Comparison.js +286 -0
- package/website/app/components/CssHealth.js +243 -0
- package/website/app/components/HeroExtractor.js +455 -0
- package/website/app/components/Marginalia.js +3 -0
- package/website/app/components/McpSection.js +223 -0
- package/website/app/components/PlatformTabs.js +250 -0
- package/website/app/components/RegionsComponents.js +429 -0
- package/website/app/components/Rule.js +13 -0
- package/website/app/components/Specimens.js +237 -0
- package/website/app/components/StructuredData.js +144 -0
- package/website/app/components/TokenBrowser.js +344 -0
- package/website/app/components/token-browser-sample.js +65 -0
- package/website/app/globals.css +415 -633
- package/website/app/icon.svg +7 -0
- package/website/app/layout.js +113 -6
- package/website/app/opengraph-image.js +170 -0
- package/website/app/page.js +325 -148
- package/website/app/robots.js +15 -0
- package/website/app/seo-config.js +82 -0
- package/website/app/sitemap.js +18 -0
- package/website/lib/cache.js +73 -0
- package/website/lib/rate-limit.js +30 -0
- package/website/lib/rate-limit.test.js +55 -0
- package/website/lib/specimens.json +86 -0
- package/website/lib/token-helpers.js +70 -0
- package/website/lib/url-safety.js +103 -0
- package/website/lib/url-safety.test.js +116 -0
- package/website/lib/zip-files.js +15 -0
- package/website/package-lock.json +85 -0
- package/website/package.json +1 -0
- package/website/public/favicon.svg +7 -0
- package/website/public/logo-specimen.svg +76 -0
- package/website/public/mark.svg +12 -0
- package/website/public/site.webmanifest +13 -0
- package/website/app/favicon.ico +0 -0
- package/website/public/file.svg +0 -1
- package/website/public/globe.svg +0 -1
- package/website/public/next.svg +0 -1
- package/website/public/vercel.svg +0 -1
- package/website/public/window.svg +0 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const TABS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'web',
|
|
8
|
+
label: 'Web',
|
|
9
|
+
filename: 'tokens.css',
|
|
10
|
+
code: `:root {
|
|
11
|
+
--color-action-primary: #533afd;
|
|
12
|
+
--radius-control: 8px;
|
|
13
|
+
}`,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'ios',
|
|
17
|
+
label: 'iOS',
|
|
18
|
+
filename: 'DesignTokens.swift',
|
|
19
|
+
code: `import SwiftUI
|
|
20
|
+
|
|
21
|
+
extension Color {
|
|
22
|
+
static let actionPrimary = Color(hex: 0x533AFD)
|
|
23
|
+
}`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'android',
|
|
27
|
+
label: 'Android',
|
|
28
|
+
filename: 'DesignTokens.kt',
|
|
29
|
+
code: `import androidx.compose.ui.graphics.Color
|
|
30
|
+
|
|
31
|
+
val ActionPrimary = Color(0xFF533AFD)
|
|
32
|
+
|
|
33
|
+
// res/values/colors.xml
|
|
34
|
+
// <color name="action_primary">#FF533AFD</color>`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'flutter',
|
|
38
|
+
label: 'Flutter',
|
|
39
|
+
filename: 'design_tokens.dart',
|
|
40
|
+
code: `import 'package:flutter/material.dart';
|
|
41
|
+
|
|
42
|
+
class DesignTokens {
|
|
43
|
+
static const Color actionPrimary = Color(0xFF533AFD);
|
|
44
|
+
}`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'wordpress',
|
|
48
|
+
label: 'WordPress',
|
|
49
|
+
filename: 'theme.json',
|
|
50
|
+
code: `{
|
|
51
|
+
"settings": {
|
|
52
|
+
"color": {
|
|
53
|
+
"palette": [
|
|
54
|
+
{ "slug": "action-primary", "color": "#533AFD", "name": "Action Primary" }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}`,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export default function PlatformTabs() {
|
|
63
|
+
const [activeId, setActiveId] = useState(TABS[0].id);
|
|
64
|
+
const [copied, setCopied] = useState(false);
|
|
65
|
+
const tabRefs = useRef({});
|
|
66
|
+
|
|
67
|
+
const active = TABS.find((t) => t.id === activeId) || TABS[0];
|
|
68
|
+
const lines = active.code.split('\n');
|
|
69
|
+
|
|
70
|
+
const onKeyDown = (e) => {
|
|
71
|
+
const idx = TABS.findIndex((t) => t.id === activeId);
|
|
72
|
+
if (e.key === 'ArrowRight') {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const next = TABS[(idx + 1) % TABS.length];
|
|
75
|
+
setActiveId(next.id);
|
|
76
|
+
tabRefs.current[next.id]?.focus();
|
|
77
|
+
} else if (e.key === 'ArrowLeft') {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
const next = TABS[(idx - 1 + TABS.length) % TABS.length];
|
|
80
|
+
setActiveId(next.id);
|
|
81
|
+
tabRefs.current[next.id]?.focus();
|
|
82
|
+
} else if (e.key === 'Home') {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
setActiveId(TABS[0].id);
|
|
85
|
+
tabRefs.current[TABS[0].id]?.focus();
|
|
86
|
+
} else if (e.key === 'End') {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const last = TABS[TABS.length - 1];
|
|
89
|
+
setActiveId(last.id);
|
|
90
|
+
tabRefs.current[last.id]?.focus();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onCopy = async () => {
|
|
95
|
+
try {
|
|
96
|
+
await navigator.clipboard.writeText(active.code);
|
|
97
|
+
setCopied(true);
|
|
98
|
+
setTimeout(() => setCopied(false), 1400);
|
|
99
|
+
} catch {
|
|
100
|
+
// noop
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
<div style={{ padding: 'var(--r5) 0 var(--r4)' }}>
|
|
107
|
+
<div className="eyebrow" style={{ marginBottom: 'var(--r3)' }}>§03 Multi-platform emitters</div>
|
|
108
|
+
<h2 className="display">One token. Five languages.</h2>
|
|
109
|
+
<p className="prose" style={{ marginTop: 'var(--r4)', fontSize: 18, maxWidth: '52ch' }}>
|
|
110
|
+
The same semantic token, emitted in five native dialects. No style leaks,
|
|
111
|
+
no translation layer.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Tab strip */}
|
|
116
|
+
<div
|
|
117
|
+
role="tablist"
|
|
118
|
+
aria-label="Platform emitters"
|
|
119
|
+
onKeyDown={onKeyDown}
|
|
120
|
+
style={{
|
|
121
|
+
display: 'flex',
|
|
122
|
+
gap: 0,
|
|
123
|
+
borderBottom: 'var(--hair)',
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{TABS.map((t) => {
|
|
127
|
+
const selected = t.id === activeId;
|
|
128
|
+
return (
|
|
129
|
+
<button
|
|
130
|
+
key={t.id}
|
|
131
|
+
ref={(el) => (tabRefs.current[t.id] = el)}
|
|
132
|
+
role="tab"
|
|
133
|
+
id={`platform-tab-${t.id}`}
|
|
134
|
+
aria-selected={selected}
|
|
135
|
+
aria-controls={`platform-panel-${t.id}`}
|
|
136
|
+
tabIndex={selected ? 0 : -1}
|
|
137
|
+
onClick={() => setActiveId(t.id)}
|
|
138
|
+
style={{
|
|
139
|
+
padding: '14px 18px 13px',
|
|
140
|
+
fontFamily: 'var(--font-mono)',
|
|
141
|
+
fontSize: 12,
|
|
142
|
+
letterSpacing: '0.08em',
|
|
143
|
+
textTransform: 'uppercase',
|
|
144
|
+
color: selected ? 'var(--ink)' : 'var(--ink-2)',
|
|
145
|
+
borderBottom: selected ? '2px solid var(--accent)' : '2px solid transparent',
|
|
146
|
+
marginBottom: '-1px',
|
|
147
|
+
background: 'transparent',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{t.label}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Code panel */}
|
|
157
|
+
<div
|
|
158
|
+
role="tabpanel"
|
|
159
|
+
id={`platform-panel-${active.id}`}
|
|
160
|
+
aria-labelledby={`platform-tab-${active.id}`}
|
|
161
|
+
style={{
|
|
162
|
+
position: 'relative',
|
|
163
|
+
background: 'var(--ink)',
|
|
164
|
+
color: 'var(--paper)',
|
|
165
|
+
border: 'var(--hair)',
|
|
166
|
+
borderTop: 0,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<div
|
|
170
|
+
style={{
|
|
171
|
+
display: 'flex',
|
|
172
|
+
justifyContent: 'space-between',
|
|
173
|
+
alignItems: 'baseline',
|
|
174
|
+
padding: '12px 20px',
|
|
175
|
+
borderBottom: '1px solid var(--ink-2)',
|
|
176
|
+
fontFamily: 'var(--font-mono)',
|
|
177
|
+
fontSize: 11,
|
|
178
|
+
color: 'var(--ink-3)',
|
|
179
|
+
letterSpacing: '0.04em',
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<span>{active.filename}</span>
|
|
183
|
+
<button
|
|
184
|
+
onClick={onCopy}
|
|
185
|
+
style={{
|
|
186
|
+
fontFamily: 'var(--font-mono)',
|
|
187
|
+
fontSize: 11,
|
|
188
|
+
letterSpacing: '0.08em',
|
|
189
|
+
textTransform: 'uppercase',
|
|
190
|
+
color: copied ? 'var(--accent)' : 'var(--paper)',
|
|
191
|
+
padding: '4px 10px',
|
|
192
|
+
border: '1px solid var(--ink-3)',
|
|
193
|
+
}}
|
|
194
|
+
aria-live="polite"
|
|
195
|
+
>
|
|
196
|
+
{copied ? 'Copied' : 'Copy'}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div
|
|
201
|
+
style={{
|
|
202
|
+
padding: '20px 0',
|
|
203
|
+
fontFamily: 'var(--font-mono)',
|
|
204
|
+
fontSize: 13,
|
|
205
|
+
lineHeight: 1.6,
|
|
206
|
+
fontFeatureSettings: "'zero' 1, 'ss01' 1",
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{lines.map((line, i) => (
|
|
210
|
+
<div
|
|
211
|
+
key={i}
|
|
212
|
+
style={{
|
|
213
|
+
display: 'grid',
|
|
214
|
+
gridTemplateColumns: '3.5rem 1fr',
|
|
215
|
+
alignItems: 'baseline',
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<span
|
|
219
|
+
style={{
|
|
220
|
+
color: 'var(--ink-3)',
|
|
221
|
+
textAlign: 'right',
|
|
222
|
+
paddingRight: 14,
|
|
223
|
+
borderRight: '1px solid var(--ink-2)',
|
|
224
|
+
userSelect: 'none',
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{i + 1}
|
|
228
|
+
</span>
|
|
229
|
+
<pre style={{ margin: 0, paddingLeft: 20, whiteSpace: 'pre', color: 'var(--paper)' }}>
|
|
230
|
+
{line || ' '}
|
|
231
|
+
</pre>
|
|
232
|
+
</div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<p
|
|
238
|
+
className="mono"
|
|
239
|
+
style={{
|
|
240
|
+
marginTop: 'var(--r4)',
|
|
241
|
+
fontSize: 12,
|
|
242
|
+
color: 'var(--ink-2)',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<code>designlang <url> --platforms all</code> writes these files under{' '}
|
|
246
|
+
<code>./design-extract-output/<platform>/</code>.
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// §06 Regions + components.
|
|
4
|
+
// Two sub-blocks:
|
|
5
|
+
// a. Region classifier — schematic page + legend of 9 roles
|
|
6
|
+
// b. Component clusters — an "unfold" list of 4 button variants
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import Rule from './Rule';
|
|
10
|
+
import Marginalia from './Marginalia';
|
|
11
|
+
|
|
12
|
+
// Schematic regions — shape matches semantic-regions.js output
|
|
13
|
+
// { role, selector, bbox: { x, y, w, h }, signals: [...] }
|
|
14
|
+
const REGIONS = [
|
|
15
|
+
{ role: 'nav', x: 0, y: 0, w: 640, h: 48 },
|
|
16
|
+
{ role: 'hero', x: 0, y: 48, w: 640, h: 140 },
|
|
17
|
+
{ role: 'features', x: 0, y: 188, w: 640, h: 84 },
|
|
18
|
+
{ role: 'pricing', x: 0, y: 272, w: 640, h: 70 },
|
|
19
|
+
{ role: 'testimonials', x: 0, y: 342, w: 640, h: 42 },
|
|
20
|
+
{ role: 'footer', x: 0, y: 384, w: 640, h: 36 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ROLES = [
|
|
24
|
+
{ role: 'nav', desc: 'top-anchored navigation', signal: 'landmark + top-anchored + link density > 0.5' },
|
|
25
|
+
{ role: 'hero', desc: 'primary above-the-fold statement', signal: 'large type + early viewport + CTA' },
|
|
26
|
+
{ role: 'features', desc: 'grid of benefit/feature cells', signal: '3+ siblings, similar structural hash' },
|
|
27
|
+
{ role: 'pricing', desc: 'price tiers and plans', signal: 'currency symbol + repeated card shape' },
|
|
28
|
+
{ role: 'testimonials', desc: 'social proof / quotes', signal: 'blockquote or quote glyph + attribution' },
|
|
29
|
+
{ role: 'cta', desc: 'standalone conversion band', signal: 'single button + heading, full-width container' },
|
|
30
|
+
{ role: 'footer', desc: 'bottom landmark', signal: 'landmark + bottom-anchored + link density high' },
|
|
31
|
+
{ role: 'sidebar', desc: 'lateral aside content', signal: 'landmark=complementary or aside element' },
|
|
32
|
+
{ role: 'content', desc: 'prose / article body', signal: 'main landmark, paragraph density, reading width' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Cluster — shape matches component-clusters.js output
|
|
36
|
+
const BUTTON_CLUSTER = {
|
|
37
|
+
kind: 'button',
|
|
38
|
+
instanceCount: 24,
|
|
39
|
+
variants: [
|
|
40
|
+
{
|
|
41
|
+
name: 'primary',
|
|
42
|
+
css: {
|
|
43
|
+
background: 'var(--accent)',
|
|
44
|
+
color: 'var(--paper)',
|
|
45
|
+
border: '1px solid var(--accent)',
|
|
46
|
+
padding: '10px 18px',
|
|
47
|
+
},
|
|
48
|
+
instanceCount: 14,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'secondary',
|
|
52
|
+
css: {
|
|
53
|
+
background: 'transparent',
|
|
54
|
+
color: 'var(--ink)',
|
|
55
|
+
border: '1px solid var(--ink)',
|
|
56
|
+
padding: '10px 18px',
|
|
57
|
+
},
|
|
58
|
+
instanceCount: 6,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ghost',
|
|
62
|
+
css: {
|
|
63
|
+
background: 'transparent',
|
|
64
|
+
color: 'var(--ink)',
|
|
65
|
+
border: '1px solid transparent',
|
|
66
|
+
padding: '10px 18px',
|
|
67
|
+
textDecoration: 'underline',
|
|
68
|
+
textUnderlineOffset: 3,
|
|
69
|
+
},
|
|
70
|
+
instanceCount: 3,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'disabled',
|
|
74
|
+
css: {
|
|
75
|
+
background: 'var(--paper-2)',
|
|
76
|
+
color: 'var(--ink-3)',
|
|
77
|
+
border: '1px solid var(--ink-3)',
|
|
78
|
+
padding: '10px 18px',
|
|
79
|
+
},
|
|
80
|
+
instanceCount: 1,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default function RegionsComponents() {
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<Rule number="06" label="Regions and components" />
|
|
89
|
+
<div className="with-margin" style={{ marginTop: 'var(--r5)' }}>
|
|
90
|
+
<div>
|
|
91
|
+
<div className="eyebrow" style={{ marginBottom: 'var(--r3)' }}>§06 Regions + components</div>
|
|
92
|
+
<h2 className="display" style={{ marginBottom: 'var(--r4)' }}>
|
|
93
|
+
Structure, not just style.
|
|
94
|
+
</h2>
|
|
95
|
+
<p className="prose" style={{ fontSize: 18, maxWidth: '62ch', color: 'var(--ink-2)' }}>
|
|
96
|
+
designlang labels the page before measuring it. Nav, hero, pricing, footer — nine
|
|
97
|
+
roles; landmarks first, heuristics second. Components are clustered by DOM signature
|
|
98
|
+
and style vector, not by guessing at class names.
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<RegionOverlay />
|
|
102
|
+
|
|
103
|
+
<div
|
|
104
|
+
style={{
|
|
105
|
+
borderTop: '1px solid var(--ink)',
|
|
106
|
+
marginTop: 'var(--r8)',
|
|
107
|
+
paddingTop: 'var(--r6)',
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<ComponentClusters />
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<Marginalia>
|
|
115
|
+
<div>MCP companion</div>
|
|
116
|
+
<p style={{ marginTop: 6 }}>
|
|
117
|
+
Regions and components live alongside the tokens, in the MCP companion JSON and the
|
|
118
|
+
DTCG <code>$extensions</code> field.
|
|
119
|
+
</p>
|
|
120
|
+
<hr style={{ margin: '12px 0', border: 0, borderTop: '1px solid var(--ink-3)' }} />
|
|
121
|
+
<p className="foot">
|
|
122
|
+
Agents can query <code>get_region</code> and <code>get_component</code> over MCP to
|
|
123
|
+
regenerate a section in matching style.
|
|
124
|
+
</p>
|
|
125
|
+
</Marginalia>
|
|
126
|
+
</div>
|
|
127
|
+
</>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Sub-block A — Region classifier ────────────────────────────
|
|
132
|
+
function RegionOverlay() {
|
|
133
|
+
const [hovered, setHovered] = useState(null);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div style={{ marginTop: 'var(--r7)' }}>
|
|
137
|
+
<div
|
|
138
|
+
className="mono"
|
|
139
|
+
style={{
|
|
140
|
+
fontSize: 11,
|
|
141
|
+
letterSpacing: '0.14em',
|
|
142
|
+
textTransform: 'uppercase',
|
|
143
|
+
color: 'var(--ink-2)',
|
|
144
|
+
marginBottom: 'var(--r3)',
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
§06.a Region classifier
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="grid-12" style={{ alignItems: 'start' }}>
|
|
151
|
+
{/* Schematic page — span 7 */}
|
|
152
|
+
<div style={{ gridColumn: 'span 7' }}>
|
|
153
|
+
<div
|
|
154
|
+
style={{
|
|
155
|
+
position: 'relative',
|
|
156
|
+
width: '100%',
|
|
157
|
+
aspectRatio: '640 / 420',
|
|
158
|
+
background: 'var(--paper-2)',
|
|
159
|
+
border: '1px solid var(--ink)',
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<svg
|
|
163
|
+
viewBox="0 0 640 420"
|
|
164
|
+
preserveAspectRatio="none"
|
|
165
|
+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
|
|
166
|
+
role="img"
|
|
167
|
+
aria-label="Schematic page with six classified regions"
|
|
168
|
+
>
|
|
169
|
+
{REGIONS.map((r) => {
|
|
170
|
+
const active = hovered === r.role;
|
|
171
|
+
return (
|
|
172
|
+
<g
|
|
173
|
+
key={r.role}
|
|
174
|
+
onMouseEnter={() => setHovered(r.role)}
|
|
175
|
+
onMouseLeave={() => setHovered(null)}
|
|
176
|
+
style={{ cursor: 'pointer' }}
|
|
177
|
+
>
|
|
178
|
+
<rect
|
|
179
|
+
x={r.x + 4}
|
|
180
|
+
y={r.y + 4}
|
|
181
|
+
width={r.w - 8}
|
|
182
|
+
height={r.h - 8}
|
|
183
|
+
fill={active ? 'var(--paper)' : 'transparent'}
|
|
184
|
+
stroke="var(--ink)"
|
|
185
|
+
strokeWidth="1"
|
|
186
|
+
/>
|
|
187
|
+
<rect
|
|
188
|
+
x={r.x + 10}
|
|
189
|
+
y={r.y + 10}
|
|
190
|
+
width={r.role.length * 7 + 10}
|
|
191
|
+
height={16}
|
|
192
|
+
fill="var(--paper)"
|
|
193
|
+
stroke="var(--ink)"
|
|
194
|
+
strokeWidth="0.5"
|
|
195
|
+
/>
|
|
196
|
+
<text
|
|
197
|
+
x={r.x + 15}
|
|
198
|
+
y={r.y + 21}
|
|
199
|
+
fontFamily="var(--font-mono)"
|
|
200
|
+
fontSize="10"
|
|
201
|
+
fill={active ? 'var(--accent)' : 'var(--ink)'}
|
|
202
|
+
letterSpacing="0.06em"
|
|
203
|
+
>
|
|
204
|
+
{r.role}
|
|
205
|
+
</text>
|
|
206
|
+
</g>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</svg>
|
|
210
|
+
</div>
|
|
211
|
+
<div
|
|
212
|
+
className="mono"
|
|
213
|
+
style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 'var(--r3)' }}
|
|
214
|
+
>
|
|
215
|
+
hover a region to match its role in the legend →
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Legend — span 5 */}
|
|
220
|
+
<div style={{ gridColumn: 'span 5' }}>
|
|
221
|
+
<div
|
|
222
|
+
className="mono"
|
|
223
|
+
style={{
|
|
224
|
+
fontSize: 10,
|
|
225
|
+
letterSpacing: '0.14em',
|
|
226
|
+
textTransform: 'uppercase',
|
|
227
|
+
color: 'var(--ink-3)',
|
|
228
|
+
marginBottom: 'var(--r3)',
|
|
229
|
+
display: 'grid',
|
|
230
|
+
gridTemplateColumns: '100px 1fr',
|
|
231
|
+
gap: 12,
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<span>role</span>
|
|
235
|
+
<span>signal</span>
|
|
236
|
+
</div>
|
|
237
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: 0, borderTop: '1px solid var(--ink)' }}>
|
|
238
|
+
{ROLES.map((r) => {
|
|
239
|
+
const active = hovered === r.role;
|
|
240
|
+
return (
|
|
241
|
+
<li
|
|
242
|
+
key={r.role}
|
|
243
|
+
onMouseEnter={() => setHovered(r.role)}
|
|
244
|
+
onMouseLeave={() => setHovered(null)}
|
|
245
|
+
style={{
|
|
246
|
+
display: 'grid',
|
|
247
|
+
gridTemplateColumns: '100px 1fr',
|
|
248
|
+
gap: 12,
|
|
249
|
+
padding: '8px 0',
|
|
250
|
+
borderBottom: '1px solid var(--ink-3)',
|
|
251
|
+
alignItems: 'baseline',
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<span
|
|
255
|
+
className="mono"
|
|
256
|
+
style={{
|
|
257
|
+
fontSize: 12,
|
|
258
|
+
color: active ? 'var(--ink)' : 'var(--ink-2)',
|
|
259
|
+
textDecoration: active ? 'underline' : 'none',
|
|
260
|
+
textDecorationColor: 'var(--accent)',
|
|
261
|
+
textUnderlineOffset: 3,
|
|
262
|
+
textDecorationThickness: 2,
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
{r.role}
|
|
266
|
+
</span>
|
|
267
|
+
<span style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.45 }}>
|
|
268
|
+
<span style={{ color: 'var(--ink)' }}>{r.desc}.</span>{' '}
|
|
269
|
+
<span className="mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
|
|
270
|
+
{r.signal}
|
|
271
|
+
</span>
|
|
272
|
+
</span>
|
|
273
|
+
</li>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
</ul>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Sub-block B — Component clusters ───────────────────────────
|
|
284
|
+
function ComponentClusters() {
|
|
285
|
+
const ref = useRef(null);
|
|
286
|
+
const [visible, setVisible] = useState(false);
|
|
287
|
+
const [reduced, setReduced] = useState(false);
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (typeof window === 'undefined') return;
|
|
291
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
292
|
+
setReduced(mq.matches);
|
|
293
|
+
}, []);
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (!ref.current) return;
|
|
297
|
+
const el = ref.current;
|
|
298
|
+
const io = new IntersectionObserver(
|
|
299
|
+
(entries) => {
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.isIntersecting) {
|
|
302
|
+
setVisible(true);
|
|
303
|
+
io.disconnect();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{ threshold: 0.25 }
|
|
308
|
+
);
|
|
309
|
+
io.observe(el);
|
|
310
|
+
return () => io.disconnect();
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div>
|
|
315
|
+
<div
|
|
316
|
+
className="mono"
|
|
317
|
+
style={{
|
|
318
|
+
fontSize: 11,
|
|
319
|
+
letterSpacing: '0.14em',
|
|
320
|
+
textTransform: 'uppercase',
|
|
321
|
+
color: 'var(--ink-2)',
|
|
322
|
+
marginBottom: 'var(--r3)',
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
§06.b Component clusters
|
|
326
|
+
</div>
|
|
327
|
+
<div
|
|
328
|
+
className="mono"
|
|
329
|
+
style={{
|
|
330
|
+
fontSize: 12,
|
|
331
|
+
color: 'var(--ink)',
|
|
332
|
+
marginBottom: 'var(--r4)',
|
|
333
|
+
paddingBottom: 'var(--r3)',
|
|
334
|
+
borderBottom: '1px solid var(--ink)',
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
button — {BUTTON_CLUSTER.instanceCount} instances, {BUTTON_CLUSTER.variants.length}{' '}
|
|
338
|
+
variants ({BUTTON_CLUSTER.variants.map((v) => v.name).join(', ')})
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div
|
|
342
|
+
ref={ref}
|
|
343
|
+
style={{
|
|
344
|
+
display: 'grid',
|
|
345
|
+
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
346
|
+
gap: 'var(--r4)',
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
{BUTTON_CLUSTER.variants.map((v, i) => {
|
|
350
|
+
const shown = reduced || visible;
|
|
351
|
+
return (
|
|
352
|
+
<div
|
|
353
|
+
key={v.name}
|
|
354
|
+
style={{
|
|
355
|
+
border: '1px solid var(--ink)',
|
|
356
|
+
padding: 'var(--r4)',
|
|
357
|
+
display: 'flex',
|
|
358
|
+
flexDirection: 'column',
|
|
359
|
+
gap: 'var(--r3)',
|
|
360
|
+
background: 'var(--paper)',
|
|
361
|
+
opacity: shown ? 1 : 0,
|
|
362
|
+
transform: shown ? 'translateX(0)' : 'translateX(-12px)',
|
|
363
|
+
transition: reduced
|
|
364
|
+
? 'none'
|
|
365
|
+
: `opacity 200ms ease ${i * 80}ms, transform 200ms ease ${i * 80}ms`,
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
<div
|
|
369
|
+
className="mono"
|
|
370
|
+
style={{
|
|
371
|
+
fontSize: 10,
|
|
372
|
+
letterSpacing: '0.14em',
|
|
373
|
+
textTransform: 'uppercase',
|
|
374
|
+
color: 'var(--ink-3)',
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
{v.name} · {v.instanceCount}
|
|
378
|
+
</div>
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
display: 'flex',
|
|
382
|
+
alignItems: 'center',
|
|
383
|
+
justifyContent: 'center',
|
|
384
|
+
minHeight: 92,
|
|
385
|
+
background: 'var(--paper-2)',
|
|
386
|
+
border: '1px solid var(--ink-3)',
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<span
|
|
390
|
+
className="mono"
|
|
391
|
+
style={{
|
|
392
|
+
...v.css,
|
|
393
|
+
fontSize: 13,
|
|
394
|
+
letterSpacing: '0.04em',
|
|
395
|
+
textTransform: 'uppercase',
|
|
396
|
+
display: 'inline-block',
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
Continue
|
|
400
|
+
</span>
|
|
401
|
+
</div>
|
|
402
|
+
<pre
|
|
403
|
+
className="mono"
|
|
404
|
+
style={{
|
|
405
|
+
fontSize: 10,
|
|
406
|
+
color: 'var(--ink-2)',
|
|
407
|
+
lineHeight: 1.5,
|
|
408
|
+
whiteSpace: 'pre-wrap',
|
|
409
|
+
margin: 0,
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
{formatCss(v.css)}
|
|
413
|
+
</pre>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function formatCss(css) {
|
|
423
|
+
return Object.entries(css)
|
|
424
|
+
.map(([k, v]) => `${kebab(k)}: ${v};`)
|
|
425
|
+
.join('\n');
|
|
426
|
+
}
|
|
427
|
+
function kebab(s) {
|
|
428
|
+
return s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
429
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function Rule({ number, label }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="rule" role="separator" aria-label={label}>
|
|
4
|
+
<div className="rule-line" />
|
|
5
|
+
{(number || label) && (
|
|
6
|
+
<div className="rule-label">
|
|
7
|
+
{number && <span className="rule-number">§{number}</span>}
|
|
8
|
+
{label}
|
|
9
|
+
</div>
|
|
10
|
+
)}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|