@primer/mcp 0.2.0 → 0.3.1
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/dist/index.js +4 -1
- package/dist/primitives.d.ts +123 -4
- package/dist/primitives.d.ts.map +1 -1
- package/dist/server-owUZMSeG.js +1525 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/stdio.js +4 -1
- package/package.json +3 -3
- package/src/primitives.ts +694 -1
- package/src/server.ts +259 -17
- package/dist/server-CjO5UCV7.js +0 -766
package/src/primitives.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs'
|
|
2
|
+
import {createRequire} from 'node:module'
|
|
3
|
+
import {spawn} from 'child_process'
|
|
1
4
|
import baseMotion from '@primer/primitives/dist/docs/base/motion/motion.json' with {type: 'json'}
|
|
2
5
|
import baseSize from '@primer/primitives/dist/docs/base/size/size.json' with {type: 'json'}
|
|
3
6
|
import baseTypography from '@primer/primitives/dist/docs/base/typography/typography.json' with {type: 'json'}
|
|
@@ -8,6 +11,16 @@ import functionalSize from '@primer/primitives/dist/docs/functional/size/size.js
|
|
|
8
11
|
import light from '@primer/primitives/dist/docs/functional/themes/light.json' with {type: 'json'}
|
|
9
12
|
import functionalTypography from '@primer/primitives/dist/docs/functional/typography/typography.json' with {type: 'json'}
|
|
10
13
|
|
|
14
|
+
// radius.json may not exist in all versions of @primer/primitives
|
|
15
|
+
let functionalSizeRadius: Record<string, {name: string; type: string; value: string}> = {}
|
|
16
|
+
try {
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
const radiusPath = require.resolve('@primer/primitives/dist/docs/functional/size/radius.json')
|
|
19
|
+
functionalSizeRadius = JSON.parse(readFileSync(radiusPath, 'utf-8'))
|
|
20
|
+
} catch {
|
|
21
|
+
// radius.json not available in this version of @primer/primitives
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
const categories = {
|
|
12
25
|
base: {
|
|
13
26
|
motion: Object.values(baseMotion).map(token => {
|
|
@@ -40,6 +53,13 @@ const categories = {
|
|
|
40
53
|
value: token.value,
|
|
41
54
|
}
|
|
42
55
|
}),
|
|
56
|
+
radius: Object.values(functionalSizeRadius).map(token => {
|
|
57
|
+
return {
|
|
58
|
+
name: token.name,
|
|
59
|
+
type: token.type,
|
|
60
|
+
value: token.value,
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
43
63
|
sizeCoarse: Object.values(functionalSizeCoarse).map(token => {
|
|
44
64
|
return {
|
|
45
65
|
name: token.name,
|
|
@@ -85,6 +105,7 @@ const tokens = [
|
|
|
85
105
|
...categories.base.size,
|
|
86
106
|
...categories.base.typography,
|
|
87
107
|
...categories.functional.border,
|
|
108
|
+
...categories.functional.radius,
|
|
88
109
|
...categories.functional.sizeCoarse,
|
|
89
110
|
...categories.functional.sizeFine,
|
|
90
111
|
...categories.functional.size,
|
|
@@ -100,4 +121,676 @@ function serialize(value: typeof tokens): string {
|
|
|
100
121
|
.join('\n')
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
|
|
124
|
+
// Semantic group prefixes that apply to any element
|
|
125
|
+
const SEMANTIC_PREFIXES = [
|
|
126
|
+
'bgColor',
|
|
127
|
+
'fgColor',
|
|
128
|
+
'border',
|
|
129
|
+
'borderColor',
|
|
130
|
+
'shadow',
|
|
131
|
+
'focus',
|
|
132
|
+
'color',
|
|
133
|
+
'animation',
|
|
134
|
+
'duration',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
type TokenGroup = {
|
|
138
|
+
name: string
|
|
139
|
+
count: number
|
|
140
|
+
subGroups?: string[]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type TokenGroups = {
|
|
144
|
+
semantic: TokenGroup[]
|
|
145
|
+
component: TokenGroup[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listTokenGroups(): TokenGroups {
|
|
149
|
+
// Use the full token set so non-theme groups (stack, text, borderRadius, etc.) are included
|
|
150
|
+
const allTokens = tokens
|
|
151
|
+
|
|
152
|
+
// Group tokens by their first segment
|
|
153
|
+
const groupMap = new Map<string, {count: number; subGroups: Set<string>}>()
|
|
154
|
+
|
|
155
|
+
for (const token of allTokens) {
|
|
156
|
+
const parts = token.name.split('-')
|
|
157
|
+
const prefix = parts[0]
|
|
158
|
+
|
|
159
|
+
if (!groupMap.has(prefix)) {
|
|
160
|
+
groupMap.set(prefix, {count: 0, subGroups: new Set()})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const group = groupMap.get(prefix)!
|
|
164
|
+
group.count++
|
|
165
|
+
|
|
166
|
+
// For component tokens, track sub-groups (e.g., button-bgColor -> bgColor)
|
|
167
|
+
if (!SEMANTIC_PREFIXES.includes(prefix) && parts.length > 1) {
|
|
168
|
+
const subGroup = parts[1]
|
|
169
|
+
if (SEMANTIC_PREFIXES.includes(subGroup)) {
|
|
170
|
+
group.subGroups.add(subGroup)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const semantic: TokenGroup[] = []
|
|
176
|
+
const component: TokenGroup[] = []
|
|
177
|
+
|
|
178
|
+
for (const [name, data] of groupMap.entries()) {
|
|
179
|
+
const group: TokenGroup = {
|
|
180
|
+
name,
|
|
181
|
+
count: data.count,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (data.subGroups.size > 0) {
|
|
185
|
+
group.subGroups = Array.from(data.subGroups).sort()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (SEMANTIC_PREFIXES.includes(name)) {
|
|
189
|
+
semantic.push(group)
|
|
190
|
+
} else {
|
|
191
|
+
component.push(group)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Sort by count descending
|
|
196
|
+
semantic.sort((a, b) => b.count - a.count)
|
|
197
|
+
component.sort((a, b) => b.count - a.count)
|
|
198
|
+
|
|
199
|
+
return {semantic, component}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {categories, tokens, serialize, listTokenGroups, type TokenGroups}
|
|
203
|
+
|
|
204
|
+
// Token with guidelines from markdown
|
|
205
|
+
type TokenWithGuidelines = {
|
|
206
|
+
name: string
|
|
207
|
+
value: string
|
|
208
|
+
useCase: string
|
|
209
|
+
rules: string
|
|
210
|
+
group: string
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Parse design tokens spec from new DESIGN_TOKENS_SPEC.md format
|
|
214
|
+
function parseDesignTokensSpec(markdown: string): TokenWithGuidelines[] {
|
|
215
|
+
const results: TokenWithGuidelines[] = []
|
|
216
|
+
const lines = markdown.split('\n')
|
|
217
|
+
|
|
218
|
+
let currentGroup = ''
|
|
219
|
+
let currentToken: Partial<TokenWithGuidelines> | null = null
|
|
220
|
+
let descriptionLines: string[] = []
|
|
221
|
+
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
// Match group headings (## heading)
|
|
224
|
+
const groupMatch = line.match(/^## (.+)$/)
|
|
225
|
+
if (groupMatch) {
|
|
226
|
+
// Save previous token if exists
|
|
227
|
+
if (currentToken?.name) {
|
|
228
|
+
results.push({
|
|
229
|
+
name: currentToken.name,
|
|
230
|
+
value: getTokenValue(currentToken.name),
|
|
231
|
+
useCase: currentToken.useCase || descriptionLines.join(' '),
|
|
232
|
+
rules: currentToken.rules || '',
|
|
233
|
+
group: currentToken.group || '',
|
|
234
|
+
})
|
|
235
|
+
descriptionLines = []
|
|
236
|
+
}
|
|
237
|
+
currentGroup = groupMatch[1].trim()
|
|
238
|
+
currentToken = null
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Match token name (### tokenName)
|
|
243
|
+
const tokenMatch = line.match(/^### (.+)$/)
|
|
244
|
+
if (tokenMatch) {
|
|
245
|
+
// Save previous token if exists
|
|
246
|
+
if (currentToken?.name) {
|
|
247
|
+
results.push({
|
|
248
|
+
name: currentToken.name,
|
|
249
|
+
value: getTokenValue(currentToken.name),
|
|
250
|
+
useCase: currentToken.useCase || descriptionLines.join(' '),
|
|
251
|
+
rules: currentToken.rules || '',
|
|
252
|
+
group: currentToken.group || '',
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
descriptionLines = []
|
|
256
|
+
currentToken = {
|
|
257
|
+
name: tokenMatch[1].trim(),
|
|
258
|
+
group: currentGroup,
|
|
259
|
+
}
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Match new format usage line (**U:**)
|
|
264
|
+
const newUsageMatch = line.match(/^\*\*U:\*\*\s*(.+)$/)
|
|
265
|
+
if (newUsageMatch && currentToken) {
|
|
266
|
+
currentToken.useCase = newUsageMatch[1].trim()
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Match new format rules line (**R:**)
|
|
271
|
+
const newRulesMatch = line.match(/^\*\*R:\*\*\s*(.+)$/)
|
|
272
|
+
if (newRulesMatch && currentToken) {
|
|
273
|
+
currentToken.rules = newRulesMatch[1].trim()
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Description line (line after token name, before U:/R:)
|
|
278
|
+
if (currentToken && !currentToken.useCase && !line.startsWith('**') && line.trim() && !line.startsWith('#')) {
|
|
279
|
+
descriptionLines.push(line.trim())
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Don't forget the last token
|
|
284
|
+
if (currentToken?.name) {
|
|
285
|
+
results.push({
|
|
286
|
+
name: currentToken.name,
|
|
287
|
+
value: getTokenValue(currentToken.name),
|
|
288
|
+
useCase: currentToken.useCase || descriptionLines.join(' '),
|
|
289
|
+
rules: currentToken.rules || '',
|
|
290
|
+
group: currentToken.group || '',
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return results
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Get token value from the loaded tokens
|
|
298
|
+
function getTokenValue(tokenName: string): string {
|
|
299
|
+
const found = tokens.find(token => token.name === tokenName)
|
|
300
|
+
return found ? String(found.value) : ''
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Human-readable display labels for canonical group prefixes
|
|
304
|
+
const GROUP_LABELS: Record<string, string> = {
|
|
305
|
+
bgColor: 'Background Color',
|
|
306
|
+
fgColor: 'Foreground Color',
|
|
307
|
+
borderColor: 'Border Color',
|
|
308
|
+
border: 'Border',
|
|
309
|
+
shadow: 'Shadow',
|
|
310
|
+
focus: 'Focus',
|
|
311
|
+
color: 'Color',
|
|
312
|
+
borderWidth: 'Border Width',
|
|
313
|
+
borderRadius: 'Border Radius',
|
|
314
|
+
boxShadow: 'Box Shadow',
|
|
315
|
+
controlStack: 'Control Stack',
|
|
316
|
+
fontStack: 'Font Stack',
|
|
317
|
+
outline: 'Outline',
|
|
318
|
+
text: 'Text',
|
|
319
|
+
control: 'Control',
|
|
320
|
+
overlay: 'Overlay',
|
|
321
|
+
stack: 'Stack',
|
|
322
|
+
spinner: 'Spinner',
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Get canonical group prefix from token name
|
|
326
|
+
function getGroupFromName(name: string): string {
|
|
327
|
+
return name.split('-')[0]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Build complete token list from JSON (includes all tokens, not just those with guidelines)
|
|
331
|
+
function buildAllTokens(guidelinesTokens: TokenWithGuidelines[]): TokenWithGuidelines[] {
|
|
332
|
+
const guidelinesMap = new Map(guidelinesTokens.map(t => [t.name, t]))
|
|
333
|
+
|
|
334
|
+
// Include theme tokens AND size/typography/border tokens
|
|
335
|
+
const allSourceTokens = [
|
|
336
|
+
...categories.base.motion,
|
|
337
|
+
...categories.base.size,
|
|
338
|
+
...categories.base.typography,
|
|
339
|
+
...categories.functional.themes.light,
|
|
340
|
+
...categories.functional.size,
|
|
341
|
+
...categories.functional.sizeCoarse,
|
|
342
|
+
...categories.functional.sizeFine,
|
|
343
|
+
...categories.functional.border,
|
|
344
|
+
...categories.functional.radius,
|
|
345
|
+
...categories.functional.typography,
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
const allTokens: TokenWithGuidelines[] = []
|
|
349
|
+
const seen = new Set<string>()
|
|
350
|
+
|
|
351
|
+
for (const token of allSourceTokens) {
|
|
352
|
+
if (seen.has(token.name)) continue
|
|
353
|
+
seen.add(token.name)
|
|
354
|
+
|
|
355
|
+
const existing = guidelinesMap.get(token.name)
|
|
356
|
+
if (existing) {
|
|
357
|
+
allTokens.push(existing)
|
|
358
|
+
} else {
|
|
359
|
+
allTokens.push({
|
|
360
|
+
name: token.name,
|
|
361
|
+
value: String(token.value),
|
|
362
|
+
useCase: '',
|
|
363
|
+
rules: '',
|
|
364
|
+
group: getGroupFromName(token.name),
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return allTokens
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Find tokens matching query and optional group filter
|
|
373
|
+
function findTokens(allTokens: TokenWithGuidelines[], query: string, group?: string): TokenWithGuidelines[] {
|
|
374
|
+
const lowerQuery = query.toLowerCase()
|
|
375
|
+
const lowerGroup = group?.toLowerCase()
|
|
376
|
+
|
|
377
|
+
return allTokens.filter(token => {
|
|
378
|
+
// Filter by group if provided - exact match only
|
|
379
|
+
if (lowerGroup && token.group.toLowerCase() !== lowerGroup) {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Fuzzy match against name, useCase, and rules
|
|
384
|
+
const searchText = `${token.name} ${token.useCase} ${token.rules}`.toLowerCase()
|
|
385
|
+
return searchText.includes(lowerQuery)
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Expand token patterns like [accent, danger] into multiple tokens
|
|
390
|
+
function expandTokenPattern(token: TokenWithGuidelines): TokenWithGuidelines[] {
|
|
391
|
+
const bracketRegex = /\[(.*?)\]/
|
|
392
|
+
const match = token.name.match(bracketRegex)
|
|
393
|
+
|
|
394
|
+
if (!match) return [token]
|
|
395
|
+
|
|
396
|
+
const variants = match[1].split(',').map((s: string) => s.trim())
|
|
397
|
+
return variants.map(variant => ({
|
|
398
|
+
...token,
|
|
399
|
+
name: token.name.replace(bracketRegex, variant),
|
|
400
|
+
}))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Load and parse token guidelines, then build complete token list
|
|
404
|
+
function loadAllTokensWithGuidelines(): TokenWithGuidelines[] {
|
|
405
|
+
try {
|
|
406
|
+
const specMarkdown = loadDesignTokensSpec()
|
|
407
|
+
const specTokens = parseDesignTokensSpec(specMarkdown)
|
|
408
|
+
return buildAllTokens(specTokens)
|
|
409
|
+
} catch {
|
|
410
|
+
// DESIGN_TOKENS_SPEC.md not available in this version of @primer/primitives
|
|
411
|
+
return buildAllTokens([])
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Load the design tokens guide (logic, rules, patterns, golden examples)
|
|
416
|
+
function loadDesignTokensGuide(): string {
|
|
417
|
+
const require = createRequire(import.meta.url)
|
|
418
|
+
const guidePath = require.resolve('@primer/primitives/DESIGN_TOKENS_GUIDE.md')
|
|
419
|
+
return readFileSync(guidePath, 'utf-8')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Load the design tokens spec (token dictionary with use cases and rules)
|
|
423
|
+
function loadDesignTokensSpec(): string {
|
|
424
|
+
const require = createRequire(import.meta.url)
|
|
425
|
+
const specPath = require.resolve('@primer/primitives/DESIGN_TOKENS_SPEC.md')
|
|
426
|
+
return readFileSync(specPath, 'utf-8')
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Get design token specifications text with dynamic group information
|
|
430
|
+
function getDesignTokenSpecsText(groups: TokenGroups): string {
|
|
431
|
+
return `
|
|
432
|
+
# Design Token Specifications
|
|
433
|
+
|
|
434
|
+
## 1. Core Rule & Enforcement
|
|
435
|
+
* **Expert Mode**: CSS expert. NEVER use raw values (hex, px, etc.). Tokens only.
|
|
436
|
+
* **Motion & Transitions:** Every interactive state change (Hover, Active) MUST include a transition. NEVER use raw values like 200ms or ease-in. Use var(--base-duration-...) and var(--base-easing-...).
|
|
437
|
+
* **Shorthand**: MUST use \`font: var(...)\`. NEVER split size/weight.
|
|
438
|
+
* **Shorthand Fallback**: If no shorthand exists (e.g. Monospace), use individual tokens for font-size, family, and line-height. NEVER raw 1.5.
|
|
439
|
+
* **States**: Define 5: Rest, Hover, Focus-visible, Active, Disabled.
|
|
440
|
+
* **Focus**: \`:focus-visible\` MUST use \`outline: var(--focus-outline)\` AND \`outline-offset: var(--outline-focus-offset)\`.
|
|
441
|
+
* **Validation**: CALL \`lint_css\` after any CSS change. Task is incomplete without a success message.
|
|
442
|
+
* **Self-Correction**: Adopt autofixes immediately. Report unfixable errors to the user.
|
|
443
|
+
|
|
444
|
+
## 2. Typography Constraints (STRICT)
|
|
445
|
+
- **Body Only**: Only \`body\` group supports size suffixes (e.g., \`body-small\`).
|
|
446
|
+
- **Static Shorthands**: NEVER add suffixes to \`caption\`, \`display\`, \`codeBlock\`, or \`codeInline\`.
|
|
447
|
+
|
|
448
|
+
## 3. Logic Matrix: Color & Semantic Mapping
|
|
449
|
+
| Input Color/Intent | Semantic Role | Background Suffix | Foreground Requirement |
|
|
450
|
+
| :--- | :--- | :--- | :--- |
|
|
451
|
+
| Blue / Interactive | \`accent\` | \`-emphasis\` (Solid) | \`fgColor-onEmphasis\` |
|
|
452
|
+
| Green / Positive | \`success\` | \`-muted\` (Light) | \`fgColor-{semantic}\` |
|
|
453
|
+
| Red / Danger | \`danger\` | \`-emphasis\` | \`fgColor-onEmphasis\` |
|
|
454
|
+
| Yellow / Warning | \`attention\` | \`-muted\` | \`fgColor-attention\` |
|
|
455
|
+
| Orange / Critical | \`severe\` | \`-emphasis\` | \`fgColor-onEmphasis\` |
|
|
456
|
+
| Purple / Done | \`done\` | Any | Match intent |
|
|
457
|
+
| Pink / Sponsors | \`sponsors\` | Any | Match intent |
|
|
458
|
+
| Grey / Neutral | \`default\` | \`bgColor-muted\` | \`fgColor-default\` (Not muted) |
|
|
459
|
+
|
|
460
|
+
## 4. Optimization & Recipes (MANDATORY)
|
|
461
|
+
**Strategy**: STOP property-by-property searching. Use \`get_token_group_bundle\` for these common patterns:
|
|
462
|
+
- **Forms**: \`["control", "focus", "outline", "text", "borderRadius", "stack", "animation"]\`
|
|
463
|
+
- **Modals/Cards**: \`["overlay", "shadow", "outline", "borderRadius", "bgColor", "stack", "animation"]\`
|
|
464
|
+
- **Tables/Lists**: \`["stack", "borderColor", "text", "bgColor", "control"]\`
|
|
465
|
+
- **Nav/Sidebars**: \`["control", "text", "accent", "stack", "focus", "animation"]\`
|
|
466
|
+
- **Status/Badges**: \`["text", "success", "danger", "attention", "severe", "stack"]\`
|
|
467
|
+
|
|
468
|
+
## 5. Available Groups
|
|
469
|
+
- **Semantic**: ${groups.semantic.map(g => `${g.name}\``).join(', ')}
|
|
470
|
+
- **Components**: ${groups.component.map(g => `\`${g.name}\``).join(', ')}
|
|
471
|
+
`.trim()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Get token usage patterns text (static golden examples)
|
|
475
|
+
function getTokenUsagePatternsText(): string {
|
|
476
|
+
return `
|
|
477
|
+
# Design Token Reference Examples
|
|
478
|
+
|
|
479
|
+
> **CRITICAL FOR AI**: To implement the examples below, DO NOT search for tokens one-by-one.
|
|
480
|
+
> Use \`get_token_group_bundle(groups: ["control", "stack", "focus", "borderRadius"])\` to fetch the required token values in a single call.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## 1. Interaction Pattern: The Primary Button
|
|
485
|
+
*Demonstrates: 5 states, color pairing, typography shorthand, and motion.*
|
|
486
|
+
|
|
487
|
+
\`\`\`css
|
|
488
|
+
.btn-primary {
|
|
489
|
+
/* Logic: Use control tokens for interactive elements */
|
|
490
|
+
background-color: var(--control-bgColor-rest);
|
|
491
|
+
color: var(--fgColor-default);
|
|
492
|
+
font: var(--text-body-shorthand-medium); /* MUST use shorthand */
|
|
493
|
+
|
|
494
|
+
/* Scale: DEFAULT is medium/normal */
|
|
495
|
+
padding-block: var(--control-medium-paddingBlock);
|
|
496
|
+
padding-inline: var(--control-medium-paddingInline-normal);
|
|
497
|
+
border: none;
|
|
498
|
+
border-radius: var(--borderRadius-medium);
|
|
499
|
+
cursor: pointer;
|
|
500
|
+
|
|
501
|
+
/* Motion: MUST be <300ms */
|
|
502
|
+
transition: background-color 150ms ease, transform 100ms ease;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.btn-primary:hover {
|
|
506
|
+
background-color: var(--control-bgColor-hover);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.btn-primary:focus-visible {
|
|
510
|
+
outline: var(--focus-outline);
|
|
511
|
+
outline-offset: var(--outline-focus-offset);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.btn-primary:active {
|
|
515
|
+
background-color: var(--control-bgColor-active);
|
|
516
|
+
transform: scale(0.98);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.btn-primary:disabled {
|
|
520
|
+
/* Logic: MUST pair bgColor-disabled with fgColor-disabled */
|
|
521
|
+
background-color: var(--bgColor-disabled);
|
|
522
|
+
color: var(--fgColor-disabled);
|
|
523
|
+
cursor: not-allowed;
|
|
524
|
+
}
|
|
525
|
+
\`\`\`
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 2. Layout Pattern: Vertical Stack
|
|
530
|
+
*Demonstrates: Layout spacing rules and matching padding density.*
|
|
531
|
+
|
|
532
|
+
\`\`\`css
|
|
533
|
+
.card-stack {
|
|
534
|
+
display: flex;
|
|
535
|
+
flex-direction: column;
|
|
536
|
+
|
|
537
|
+
/* Logic: Use stack tokens for layout spacing */
|
|
538
|
+
gap: var(--stack-gap-normal);
|
|
539
|
+
padding: var(--stack-padding-normal);
|
|
540
|
+
|
|
541
|
+
background-color: var(--bgColor-default);
|
|
542
|
+
border: 1px solid var(--borderColor-default);
|
|
543
|
+
border-radius: var(--borderRadius-large);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Logic: Matching padding density to purpose */
|
|
547
|
+
.card-header {
|
|
548
|
+
padding-block-end: var(--stack-gap-condensed);
|
|
549
|
+
border-bottom: 1px solid var(--borderColor-muted);
|
|
550
|
+
}
|
|
551
|
+
\`\`\`
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Implementation Rules for AI:
|
|
556
|
+
1. **Shorthand First**: Always use \`font: var(...)\` rather than splitting size/weight.
|
|
557
|
+
2. **States**: Never implement a button without all 5 states.
|
|
558
|
+
3. **Spacing**: Use \`control-\` tokens for the component itself and \`stack-\` tokens for the container/layout.
|
|
559
|
+
4. **Motion**: Always include the \`prefers-reduced-motion\` media query to set transitions to \`none\`.
|
|
560
|
+
\`\`\`css
|
|
561
|
+
@media (prefers-reduced-motion: reduce) {
|
|
562
|
+
.btn-primary {
|
|
563
|
+
transition: none;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
\`\`\`
|
|
567
|
+
`.trim()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Search tokens with keyword matching and optional group filter
|
|
571
|
+
// Returns expanded tokens (patterns like [accent, danger] are expanded) filtered by query
|
|
572
|
+
function searchTokens(allTokens: TokenWithGuidelines[], query: string, group?: string): TokenWithGuidelines[] {
|
|
573
|
+
// 1. Flatten and expand all patterns first (e.g., [accent, danger])
|
|
574
|
+
const expandedTokens = allTokens.flatMap(expandTokenPattern)
|
|
575
|
+
|
|
576
|
+
// 2. Prepare keywords and group filter
|
|
577
|
+
const keywords = query
|
|
578
|
+
.toLowerCase()
|
|
579
|
+
.split(/\s+/)
|
|
580
|
+
.filter(k => k.length > 0)
|
|
581
|
+
|
|
582
|
+
// 3. Perform filtered search with keyword splitting (Logical AND)
|
|
583
|
+
return expandedTokens.filter(token => {
|
|
584
|
+
// Combine all relevant metadata into one searchable string
|
|
585
|
+
const searchableText = `${token.name} ${token.useCase} ${token.rules} ${token.group}`.toLowerCase()
|
|
586
|
+
|
|
587
|
+
// Ensure EVERY keyword in the query exists somewhere in this token's metadata
|
|
588
|
+
const matchesKeywords = keywords.every(word => searchableText.includes(word))
|
|
589
|
+
|
|
590
|
+
const matchesGroup = !group || tokenMatchesGroup(token, group)
|
|
591
|
+
|
|
592
|
+
return matchesKeywords && matchesGroup
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Alias map: fuzzy/human-readable names → canonical token name prefix
|
|
597
|
+
const GROUP_ALIASES: Record<string, string> = {
|
|
598
|
+
// Identity mappings (canonical prefixes, lowercased key)
|
|
599
|
+
bgcolor: 'bgColor',
|
|
600
|
+
fgcolor: 'fgColor',
|
|
601
|
+
bordercolor: 'borderColor',
|
|
602
|
+
border: 'border',
|
|
603
|
+
shadow: 'shadow',
|
|
604
|
+
focus: 'focus',
|
|
605
|
+
color: 'color',
|
|
606
|
+
button: 'button',
|
|
607
|
+
control: 'control',
|
|
608
|
+
overlay: 'overlay',
|
|
609
|
+
borderradius: 'borderRadius',
|
|
610
|
+
boxshadow: 'boxShadow',
|
|
611
|
+
fontstack: 'fontStack',
|
|
612
|
+
spinner: 'spinner',
|
|
613
|
+
|
|
614
|
+
// Fuzzy aliases
|
|
615
|
+
background: 'bgColor',
|
|
616
|
+
backgroundcolor: 'bgColor',
|
|
617
|
+
bg: 'bgColor',
|
|
618
|
+
foreground: 'fgColor',
|
|
619
|
+
foregroundcolor: 'fgColor',
|
|
620
|
+
textcolor: 'fgColor',
|
|
621
|
+
fg: 'fgColor',
|
|
622
|
+
radius: 'borderRadius',
|
|
623
|
+
rounded: 'borderRadius',
|
|
624
|
+
elevation: 'overlay',
|
|
625
|
+
depth: 'overlay',
|
|
626
|
+
btn: 'button',
|
|
627
|
+
typography: 'text',
|
|
628
|
+
font: 'text',
|
|
629
|
+
text: 'text',
|
|
630
|
+
'line-height': 'text',
|
|
631
|
+
lineheight: 'text',
|
|
632
|
+
leading: 'text',
|
|
633
|
+
|
|
634
|
+
// Layout & Spacing
|
|
635
|
+
stack: 'stack',
|
|
636
|
+
controlstack: 'controlStack',
|
|
637
|
+
padding: 'stack',
|
|
638
|
+
margin: 'stack',
|
|
639
|
+
gap: 'stack',
|
|
640
|
+
spacing: 'stack',
|
|
641
|
+
layout: 'stack',
|
|
642
|
+
|
|
643
|
+
// State & Interaction
|
|
644
|
+
offset: 'focus',
|
|
645
|
+
outline: 'outline',
|
|
646
|
+
ring: 'focus',
|
|
647
|
+
|
|
648
|
+
// Decoration & Borders
|
|
649
|
+
borderwidth: 'borderWidth',
|
|
650
|
+
line: 'borderColor',
|
|
651
|
+
stroke: 'borderColor',
|
|
652
|
+
separator: 'borderColor',
|
|
653
|
+
|
|
654
|
+
// Color-to-Semantic Intent Mapping
|
|
655
|
+
red: 'danger',
|
|
656
|
+
green: 'success',
|
|
657
|
+
yellow: 'attention',
|
|
658
|
+
orange: 'severe',
|
|
659
|
+
blue: 'accent',
|
|
660
|
+
purple: 'done',
|
|
661
|
+
pink: 'sponsors',
|
|
662
|
+
grey: 'neutral',
|
|
663
|
+
gray: 'neutral',
|
|
664
|
+
|
|
665
|
+
// Descriptive Aliases
|
|
666
|
+
light: 'muted',
|
|
667
|
+
subtle: 'muted',
|
|
668
|
+
dark: 'emphasis',
|
|
669
|
+
strong: 'emphasis',
|
|
670
|
+
intense: 'emphasis',
|
|
671
|
+
bold: 'emphasis',
|
|
672
|
+
vivid: 'emphasis',
|
|
673
|
+
highlight: 'emphasis',
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Match a token against a resolved group by checking both the token name prefix and the group label
|
|
677
|
+
function tokenMatchesGroup(token: TokenWithGuidelines, resolvedGroup: string): boolean {
|
|
678
|
+
const rg = resolvedGroup.toLowerCase()
|
|
679
|
+
const tokenPrefix = token.name.split('-')[0].toLowerCase()
|
|
680
|
+
const tokenGroup = token.group.toLowerCase()
|
|
681
|
+
return tokenPrefix === rg || tokenGroup === rg
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Group tokens by their group property and format as Markdown
|
|
685
|
+
function formatBundle(bundleTokens: TokenWithGuidelines[]): string {
|
|
686
|
+
const grouped = bundleTokens.reduce<Record<string, TokenWithGuidelines[]>>((acc, token) => {
|
|
687
|
+
const group = GROUP_LABELS[token.group] || token.group || 'Ungrouped'
|
|
688
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
689
|
+
if (!acc[group]) acc[group] = []
|
|
690
|
+
acc[group].push(token)
|
|
691
|
+
return acc
|
|
692
|
+
}, {})
|
|
693
|
+
|
|
694
|
+
return Object.entries(grouped)
|
|
695
|
+
.map(([group, groupTokens]) => {
|
|
696
|
+
const tokenList = groupTokens
|
|
697
|
+
.map(t => {
|
|
698
|
+
const nameLabel = t.value ? `\`${t.name}\` → \`${t.value}\`` : `\`${t.name}\``
|
|
699
|
+
return `- ${nameLabel}\n - **U**: ${t.useCase || '(none)'}\n - **R**: ${t.rules || '(none)'}`
|
|
700
|
+
})
|
|
701
|
+
.join('\n')
|
|
702
|
+
return `## ${group}\n\n${tokenList}`
|
|
703
|
+
})
|
|
704
|
+
.join('\n\n---\n\n')
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Generates a sorted, unique list of group names from the current token cache.
|
|
709
|
+
* Used for "Healing" error messages and the Design System Search Map.
|
|
710
|
+
*/
|
|
711
|
+
function getValidGroupsList(validTokens: TokenWithGuidelines[]): string {
|
|
712
|
+
if (validTokens.length === 0) {
|
|
713
|
+
return 'No groups available.'
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 1. Extract unique group names
|
|
717
|
+
const uniqueGroups = Array.from(new Set(validTokens.map(t => t.group)))
|
|
718
|
+
|
|
719
|
+
// 2. Sort alphabetically for consistency
|
|
720
|
+
uniqueGroups.sort((a, b) => a.localeCompare(b))
|
|
721
|
+
|
|
722
|
+
// 3. Return as a formatted Markdown string with backticks
|
|
723
|
+
return uniqueGroups.map(g => `\`${g}\``).join(', ')
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Usage Guidance Hints
|
|
727
|
+
const groupHints: Record<string, string> = {
|
|
728
|
+
control: '`control` tokens are for form inputs/checkboxes. For buttons, use the `button` group.',
|
|
729
|
+
button: '`button` tokens are for standard triggers. For form-fields, see the `control` group.',
|
|
730
|
+
text: 'STRICT: The following typography groups do NOT support size suffixes (-small, -medium, -large): `caption`, `display`, `codeBlock`, and `codeInline`. STRICT: Use shorthand tokens where possible. If splitting, you MUST fetch line-height tokens (e.g., --text-body-lineHeight-small) instead of using raw numbers.',
|
|
731
|
+
fgColor: 'Use `fgColor` for text. For borders, use `borderColor`.',
|
|
732
|
+
borderWidth:
|
|
733
|
+
'`borderWidth` only has sizing values (thin, thick, thicker). For border *colors*, use the `borderColor` or `border` group.',
|
|
734
|
+
animation:
|
|
735
|
+
'TRANSITION RULE: Apply duration and easing to the base class, not the :hover state. Standard pairing: `transition: background-color var(--base-duration-200) var(--base-easing-easeInOut);`',
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// -----------------------------------------------------------------------------
|
|
739
|
+
// Stylelint runner
|
|
740
|
+
// -----------------------------------------------------------------------------
|
|
741
|
+
function runStylelint(css: string): Promise<{stdout: string; stderr: string}> {
|
|
742
|
+
return new Promise((resolve, reject) => {
|
|
743
|
+
const proc = spawn('npx', ['stylelint', '--stdin', '--fix'], {
|
|
744
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
745
|
+
shell: true,
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
let stdout = ''
|
|
749
|
+
let stderr = ''
|
|
750
|
+
|
|
751
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
752
|
+
stdout += data.toString()
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
756
|
+
stderr += data.toString()
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
proc.on('close', code => {
|
|
760
|
+
if (code === 0) {
|
|
761
|
+
resolve({stdout, stderr})
|
|
762
|
+
} else {
|
|
763
|
+
const error = new Error(`Stylelint exited with code ${code}`) as Error & {stdout: string; stderr: string}
|
|
764
|
+
error.stdout = stdout
|
|
765
|
+
error.stderr = stderr
|
|
766
|
+
reject(error)
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
proc.on('error', reject)
|
|
771
|
+
|
|
772
|
+
proc.stdin.write(css)
|
|
773
|
+
proc.stdin.end()
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export {
|
|
778
|
+
parseDesignTokensSpec,
|
|
779
|
+
findTokens,
|
|
780
|
+
buildAllTokens,
|
|
781
|
+
expandTokenPattern,
|
|
782
|
+
loadAllTokensWithGuidelines,
|
|
783
|
+
loadDesignTokensGuide,
|
|
784
|
+
loadDesignTokensSpec,
|
|
785
|
+
getDesignTokenSpecsText,
|
|
786
|
+
getTokenUsagePatternsText,
|
|
787
|
+
getValidGroupsList,
|
|
788
|
+
searchTokens,
|
|
789
|
+
formatBundle,
|
|
790
|
+
groupHints,
|
|
791
|
+
GROUP_ALIASES,
|
|
792
|
+
GROUP_LABELS,
|
|
793
|
+
tokenMatchesGroup,
|
|
794
|
+
runStylelint,
|
|
795
|
+
type TokenWithGuidelines,
|
|
796
|
+
}
|