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