@primer/primitives 11.4.0 → 11.4.1-rc.eb8ee149
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/DESIGN_TOKENS_SPEC.md +446 -0
- package/dist/build/formats/markdownLlmGuidelines.d.ts +7 -6
- package/dist/build/formats/markdownLlmGuidelines.js +685 -60
- package/dist/css/functional/themes/dark-colorblind-high-contrast.css +4 -0
- package/dist/css/functional/themes/dark-colorblind.css +4 -0
- package/dist/css/functional/themes/dark-dimmed-high-contrast.css +4 -0
- package/dist/css/functional/themes/dark-dimmed.css +4 -0
- package/dist/css/functional/themes/dark-high-contrast.css +4 -0
- package/dist/css/functional/themes/dark-tritanopia-high-contrast.css +4 -0
- package/dist/css/functional/themes/dark-tritanopia.css +4 -0
- package/dist/css/functional/themes/dark.css +4 -0
- package/dist/css/functional/themes/light-colorblind-high-contrast.css +4 -0
- package/dist/css/functional/themes/light-colorblind.css +4 -0
- package/dist/css/functional/themes/light-high-contrast.css +4 -0
- package/dist/css/functional/themes/light-tritanopia-high-contrast.css +4 -0
- package/dist/css/functional/themes/light-tritanopia.css +4 -0
- package/dist/css/functional/themes/light.css +4 -0
- package/dist/css/primitives.css +4 -0
- package/package.json +5 -4
- package/token-guidelines.llm.md +0 -695
|
@@ -8,12 +8,376 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { sortByName } from 'style-dictionary/utils';
|
|
11
|
+
// Semantic sets that should be grouped into tables
|
|
12
|
+
const SEMANTIC_SETS = [
|
|
13
|
+
'accent',
|
|
14
|
+
'danger',
|
|
15
|
+
'success',
|
|
16
|
+
'attention',
|
|
17
|
+
'severe',
|
|
18
|
+
'open',
|
|
19
|
+
'closed',
|
|
20
|
+
'done',
|
|
21
|
+
'neutral',
|
|
22
|
+
'sponsors',
|
|
23
|
+
'upsell',
|
|
24
|
+
];
|
|
25
|
+
// Categories that can be merged with wildcard patterns
|
|
26
|
+
const MERGEABLE_CATEGORIES = ['bgColor', 'borderColor', 'fgColor'];
|
|
27
|
+
// Global semantic key definitions - these explain what each semantic color means
|
|
28
|
+
// so we don't repeat this info in every table row
|
|
29
|
+
const SEMANTIC_KEY = {
|
|
30
|
+
danger: {
|
|
31
|
+
meaning: 'Errors, destructive actions, critical warnings',
|
|
32
|
+
usage: 'delete buttons, error messages, validation errors',
|
|
33
|
+
textPairing: 'fg.danger (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
34
|
+
},
|
|
35
|
+
success: {
|
|
36
|
+
meaning: 'Positive states, confirmations, completed actions',
|
|
37
|
+
usage: 'merge buttons, success messages, confirmations',
|
|
38
|
+
textPairing: 'fg.success (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
39
|
+
},
|
|
40
|
+
attention: {
|
|
41
|
+
meaning: 'Warnings, caution states requiring user awareness',
|
|
42
|
+
usage: 'warning banners, caution labels, pending states',
|
|
43
|
+
textPairing: 'fg.attention (muted bg) or fg.default (emphasis bg, due to yellow contrast)',
|
|
44
|
+
},
|
|
45
|
+
severe: {
|
|
46
|
+
meaning: 'High-priority warnings, more urgent than attention',
|
|
47
|
+
usage: 'urgent messages, escalations, high-priority indicators',
|
|
48
|
+
textPairing: 'fg.severe (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
49
|
+
},
|
|
50
|
+
accent: {
|
|
51
|
+
meaning: 'Selected, focused, or highlighted interactive elements',
|
|
52
|
+
usage: 'active states, selected rows, focus indicators',
|
|
53
|
+
textPairing: 'fg.accent (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
54
|
+
},
|
|
55
|
+
neutral: {
|
|
56
|
+
meaning: 'Non-semantic, secondary UI elements',
|
|
57
|
+
usage: 'secondary buttons, tags, labels without status meaning',
|
|
58
|
+
textPairing: 'fg.default (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
59
|
+
},
|
|
60
|
+
open: {
|
|
61
|
+
meaning: 'Open/active state indicators (GitHub issues, PRs)',
|
|
62
|
+
usage: 'open issues, open PRs, active discussions',
|
|
63
|
+
textPairing: 'fg.open (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
64
|
+
},
|
|
65
|
+
closed: {
|
|
66
|
+
meaning: 'Closed/declined state indicators (GitHub issues, PRs)',
|
|
67
|
+
usage: 'closed issues, closed PRs, declined items',
|
|
68
|
+
textPairing: 'fg.closed (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
69
|
+
},
|
|
70
|
+
done: {
|
|
71
|
+
meaning: 'Completed/merged state indicators',
|
|
72
|
+
usage: 'merged PRs, completed tasks, finished items',
|
|
73
|
+
textPairing: 'fg.done (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
74
|
+
},
|
|
75
|
+
sponsors: {
|
|
76
|
+
meaning: 'GitHub Sponsors content only',
|
|
77
|
+
usage: 'sponsor buttons, funding prompts, sponsor cards',
|
|
78
|
+
textPairing: 'fg.sponsors (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
79
|
+
},
|
|
80
|
+
upsell: {
|
|
81
|
+
meaning: 'Upgrade prompts, premium features, promotional content',
|
|
82
|
+
usage: 'upgrade buttons, premium badges, promotional banners',
|
|
83
|
+
textPairing: 'fg.upsell (muted bg) or fg.onEmphasis (emphasis bg)',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
// Typography role groupings for better LLM comprehension
|
|
87
|
+
const TYPOGRAPHY_ROLES = [
|
|
88
|
+
{
|
|
89
|
+
role: 'Headings',
|
|
90
|
+
description: 'Title and display text styles for headings and hero sections.',
|
|
91
|
+
patterns: ['text-title-', 'text-display-', 'text-subtitle-'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
role: 'Body',
|
|
95
|
+
description: 'Body text and caption styles for content and UI labels.',
|
|
96
|
+
patterns: ['text-body-', 'text-caption-'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
role: 'Code',
|
|
100
|
+
description: 'Monospace text styles for code blocks and inline code.',
|
|
101
|
+
patterns: ['text-code'],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
// Size scales for pattern-based compression
|
|
105
|
+
const SIZE_SCALES = ['xsmall', 'small', 'medium', 'large', 'xlarge'];
|
|
106
|
+
const DENSITY_SCALES = ['condensed', 'normal', 'spacious'];
|
|
107
|
+
// Categories that use pattern-based compression for size tokens
|
|
108
|
+
const PATTERN_COMPRESSED_CATEGORIES = {
|
|
109
|
+
control: {
|
|
110
|
+
scaleNote: 'Use xsmall/small for dense layouts, medium for default UI, large/xlarge for prominent CTAs.',
|
|
111
|
+
sizeScale: SIZE_SCALES,
|
|
112
|
+
densityScale: DENSITY_SCALES,
|
|
113
|
+
stateGroups: ['checked', 'transparent'],
|
|
114
|
+
},
|
|
115
|
+
controlStack: {
|
|
116
|
+
scaleNote: 'Match gap size to control size. Use condensed for tight groupings, spacious for separated actions.',
|
|
117
|
+
sizeScale: ['small', 'medium', 'large'],
|
|
118
|
+
densityScale: ['condensed', 'spacious'],
|
|
119
|
+
},
|
|
120
|
+
overlay: {
|
|
121
|
+
scaleNote: 'Use xsmall/small for menus and tooltips, medium for dialogs, large/xlarge for complex modals or sheets.',
|
|
122
|
+
sizeScale: ['xsmall', 'small', 'medium', 'large', 'xlarge'],
|
|
123
|
+
densityScale: ['condensed', 'normal'],
|
|
124
|
+
},
|
|
125
|
+
spinner: {
|
|
126
|
+
scaleNote: 'Use small for inline loading, medium for buttons/cards, large for full-page states.',
|
|
127
|
+
sizeScale: ['small', 'medium', 'large'],
|
|
128
|
+
},
|
|
129
|
+
stack: {
|
|
130
|
+
scaleNote: 'Use condensed for dense lists, normal for standard layouts, spacious for prominent sections.',
|
|
131
|
+
densityScale: DENSITY_SCALES,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Outputs pattern-compressed tokens for a category
|
|
136
|
+
*/
|
|
137
|
+
function outputPatternCompressedCategory(category, tokens, config, lines) {
|
|
138
|
+
var _a;
|
|
139
|
+
const tokenNames = tokens.map(t => t.name);
|
|
140
|
+
// Output scale note
|
|
141
|
+
lines.push(`**Scale:** ${config.scaleNote}`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
// Categorize tokens
|
|
144
|
+
const sizeTokens = [];
|
|
145
|
+
const stateTokens = new Map();
|
|
146
|
+
const otherTokens = [];
|
|
147
|
+
for (const name of tokenNames) {
|
|
148
|
+
const rest = name.replace(`${category}-`, '');
|
|
149
|
+
// Check if it's a state token (checked, transparent)
|
|
150
|
+
let isStateToken = false;
|
|
151
|
+
if (config.stateGroups) {
|
|
152
|
+
for (const state of config.stateGroups) {
|
|
153
|
+
if (rest.startsWith(`${state}-`)) {
|
|
154
|
+
if (!stateTokens.has(state)) {
|
|
155
|
+
stateTokens.set(state, []);
|
|
156
|
+
}
|
|
157
|
+
stateTokens.get(state).push(rest.replace(`${state}-`, ''));
|
|
158
|
+
isStateToken = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isStateToken)
|
|
164
|
+
continue;
|
|
165
|
+
// Check if it's a size token (exact match on part, not substring)
|
|
166
|
+
const parts = rest.split('-');
|
|
167
|
+
const hasSizeScale = (_a = config.sizeScale) === null || _a === void 0 ? void 0 : _a.some(s => parts.includes(s));
|
|
168
|
+
if (hasSizeScale) {
|
|
169
|
+
sizeTokens.push(name);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
otherTokens.push(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Output size patterns
|
|
176
|
+
if (sizeTokens.length > 0 && config.sizeScale) {
|
|
177
|
+
// Group by property - handle both prefix-size and size-suffix patterns
|
|
178
|
+
// e.g., control-medium-gap (size first) vs overlay-width-medium (size last)
|
|
179
|
+
const byPattern = new Map();
|
|
180
|
+
for (const name of sizeTokens) {
|
|
181
|
+
const rest = name.replace(`${category}-`, '');
|
|
182
|
+
const parts = rest.split('-');
|
|
183
|
+
// Find exact size match in parts
|
|
184
|
+
for (const size of config.sizeScale) {
|
|
185
|
+
const sizeIdx = parts.indexOf(size);
|
|
186
|
+
if (sizeIdx >= 0) {
|
|
187
|
+
const prefix = parts.slice(0, sizeIdx).join('-');
|
|
188
|
+
const suffix = parts.slice(sizeIdx + 1).join('-');
|
|
189
|
+
// Create pattern key with placeholder for size
|
|
190
|
+
let patternKey;
|
|
191
|
+
if (prefix && suffix) {
|
|
192
|
+
patternKey = `${prefix}-[size]-${suffix}`;
|
|
193
|
+
}
|
|
194
|
+
else if (prefix) {
|
|
195
|
+
patternKey = `${prefix}-[size]`;
|
|
196
|
+
}
|
|
197
|
+
else if (suffix) {
|
|
198
|
+
patternKey = `[size]-${suffix}`;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
patternKey = '[size]';
|
|
202
|
+
}
|
|
203
|
+
if (!byPattern.has(patternKey)) {
|
|
204
|
+
byPattern.set(patternKey, new Set());
|
|
205
|
+
}
|
|
206
|
+
byPattern.get(patternKey).add(size);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Group patterns by their size sets for compact output
|
|
212
|
+
const bySizeSet = new Map();
|
|
213
|
+
for (const [pattern, sizes] of byPattern) {
|
|
214
|
+
const sizeKey = [...sizes].sort((a, b) => config.sizeScale.indexOf(a) - config.sizeScale.indexOf(b)).join(',');
|
|
215
|
+
if (!bySizeSet.has(sizeKey)) {
|
|
216
|
+
bySizeSet.set(sizeKey, []);
|
|
217
|
+
}
|
|
218
|
+
bySizeSet.get(sizeKey).push(pattern);
|
|
219
|
+
}
|
|
220
|
+
lines.push('**Size patterns:**');
|
|
221
|
+
for (const [sizeKey, patterns] of bySizeSet) {
|
|
222
|
+
const sizes = sizeKey.split(',');
|
|
223
|
+
const sizeNotation = sizes.length > 1 ? `[${sizes.join(', ')}]` : sizes[0];
|
|
224
|
+
// Replace [size] placeholder with actual size notation
|
|
225
|
+
const formattedPatterns = patterns.map(p => p.replace('[size]', sizeNotation)).sort();
|
|
226
|
+
// Try to merge patterns with same size but different properties
|
|
227
|
+
if (formattedPatterns.length > 1) {
|
|
228
|
+
// Check if they share a common structure
|
|
229
|
+
const suffixes = formattedPatterns.map(p => {
|
|
230
|
+
const escapedSizeNotation = sizeNotation.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
const match = p.match(new RegExp(`${escapedSizeNotation}-(.+)$`));
|
|
232
|
+
return match ? match[1] : null;
|
|
233
|
+
});
|
|
234
|
+
if (suffixes.every(s => s !== null)) {
|
|
235
|
+
const uniqueSuffixes = [...new Set(suffixes)].sort();
|
|
236
|
+
if (uniqueSuffixes.length > 1) {
|
|
237
|
+
lines.push(`- \`${category}-${sizeNotation}-[${uniqueSuffixes.join(', ')}]\``);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
lines.push(`- \`${category}-${sizeNotation}-${uniqueSuffixes[0]}\``);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Output each pattern separately
|
|
245
|
+
for (const pattern of formattedPatterns) {
|
|
246
|
+
lines.push(`- \`${category}-${pattern}\``);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
lines.push(`- \`${category}-${formattedPatterns[0]}\``);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lines.push('');
|
|
255
|
+
}
|
|
256
|
+
// Output state tokens
|
|
257
|
+
if (stateTokens.size > 0) {
|
|
258
|
+
lines.push('**State variants:**');
|
|
259
|
+
for (const [state, suffixes] of stateTokens) {
|
|
260
|
+
const sortedSuffixes = [...new Set(suffixes)].sort();
|
|
261
|
+
const suffixNotation = sortedSuffixes.length > 1 ? `[${sortedSuffixes.join(', ')}]` : sortedSuffixes[0];
|
|
262
|
+
lines.push(`- \`${category}-${state}-${suffixNotation}\``);
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
}
|
|
266
|
+
// Output other tokens (colors, misc)
|
|
267
|
+
if (otherTokens.length > 0) {
|
|
268
|
+
// Group by type (bgColor, fgColor, etc.)
|
|
269
|
+
const byType = new Map();
|
|
270
|
+
for (const name of otherTokens) {
|
|
271
|
+
const rest = name.replace(`${category}-`, '');
|
|
272
|
+
const parts = rest.split('-');
|
|
273
|
+
// Find the type (bgColor, fgColor, borderColor, etc.)
|
|
274
|
+
let type = parts[0];
|
|
275
|
+
let variant = parts.slice(1).join('-') || 'default';
|
|
276
|
+
// Handle compound types like minTarget
|
|
277
|
+
if (parts.length >= 2 && !['bgColor', 'fgColor', 'borderColor', 'iconColor'].includes(type)) {
|
|
278
|
+
type = parts.slice(0, -1).join('-');
|
|
279
|
+
variant = parts[parts.length - 1];
|
|
280
|
+
}
|
|
281
|
+
if (!byType.has(type)) {
|
|
282
|
+
byType.set(type, []);
|
|
283
|
+
}
|
|
284
|
+
byType.get(type).push(variant);
|
|
285
|
+
}
|
|
286
|
+
lines.push('**Other tokens:**');
|
|
287
|
+
for (const [type, variants] of [...byType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
288
|
+
const sortedVariants = [...new Set(variants)].sort();
|
|
289
|
+
if (sortedVariants.length > 1) {
|
|
290
|
+
lines.push(`- \`${category}-${type}-[${sortedVariants.join(', ')}]\``);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
lines.push(`- \`${category}-${type}-${sortedVariants[0]}\``);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Human-readable category names and descriptions
|
|
300
|
+
const CATEGORY_INFO = {
|
|
301
|
+
bgColor: {
|
|
302
|
+
name: 'Background Colors',
|
|
303
|
+
description: 'Background color tokens for surfaces, containers, and UI elements.',
|
|
304
|
+
},
|
|
305
|
+
borderColor: {
|
|
306
|
+
name: 'Border Colors',
|
|
307
|
+
description: 'Border color tokens for boundaries, dividers, and outlines.',
|
|
308
|
+
},
|
|
309
|
+
fgColor: {
|
|
310
|
+
name: 'Foreground Colors',
|
|
311
|
+
description: 'Text and icon color tokens.',
|
|
312
|
+
},
|
|
313
|
+
borderRadius: { name: 'Border Radius', description: 'Corner radius tokens for rounded elements.' },
|
|
314
|
+
borderWidth: { name: 'Border Width', description: 'Border thickness tokens.' },
|
|
315
|
+
border: { name: 'Border', description: 'Composite border tokens combining color, width, and style.' },
|
|
316
|
+
control: { name: 'Controls', description: 'Tokens for interactive controls like buttons, inputs, and selects.' },
|
|
317
|
+
controlKnob: {
|
|
318
|
+
name: 'Control Knob',
|
|
319
|
+
description: 'Tokens for toggle switch knobs (the circular handle that moves along the track).',
|
|
320
|
+
},
|
|
321
|
+
controlStack: {
|
|
322
|
+
name: 'Control Stack',
|
|
323
|
+
description: 'Gap tokens for groups of controls arranged in a row or column.',
|
|
324
|
+
},
|
|
325
|
+
controlTrack: {
|
|
326
|
+
name: 'Control Track',
|
|
327
|
+
description: 'Tokens for toggle switch tracks (the background rail that the knob slides along).',
|
|
328
|
+
},
|
|
329
|
+
data: {
|
|
330
|
+
name: 'Data Visualization',
|
|
331
|
+
description: 'Color tokens for charts, graphs, and diagrams. Use emphasis variants for lines/bars, muted variants for fills.',
|
|
332
|
+
},
|
|
333
|
+
easing: { name: 'Easing', description: 'Animation easing function tokens.' },
|
|
334
|
+
focus: { name: 'Focus', description: 'Focus ring and outline tokens for keyboard navigation accessibility.' },
|
|
335
|
+
fontStack: { name: 'Font Stacks', description: 'Font family tokens.' },
|
|
336
|
+
outline: { name: 'Outline', description: 'Outline tokens for focus indicators.' },
|
|
337
|
+
overlay: { name: 'Overlay', description: 'Tokens for modals, dialogs, popovers, and dropdown menus.' },
|
|
338
|
+
selection: { name: 'Selection', description: 'Tokens for text selection highlights.' },
|
|
339
|
+
shadow: { name: 'Shadow', description: 'Box shadow tokens for elevation and depth.' },
|
|
340
|
+
spinner: { name: 'Spinner', description: 'Loading spinner size and stroke tokens.' },
|
|
341
|
+
stack: { name: 'Stack', description: 'Spacing tokens for Stack layout components.' },
|
|
342
|
+
text: { name: 'Typography', description: 'Text style shorthand tokens for consistent typography across the UI.' },
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Densifies description by removing filler words
|
|
346
|
+
*/
|
|
347
|
+
function densifyDescription(description) {
|
|
348
|
+
return description
|
|
349
|
+
.replace(/^Use this for\s+/i, 'For ')
|
|
350
|
+
.replace(/^This is used for\s+/i, 'For ')
|
|
351
|
+
.replace(/^Used for\s+/i, 'For ')
|
|
352
|
+
.replace(/^Use for\s+/i, 'For ')
|
|
353
|
+
.replace(/^This is\s+/i, '')
|
|
354
|
+
.replace(/^This\s+/i, '');
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Shortens rules by applying key shorthands
|
|
358
|
+
*/
|
|
359
|
+
function shortenRules(rules) {
|
|
360
|
+
return rules
|
|
361
|
+
.replace(/Pair with\s+/gi, 'Pair -> ')
|
|
362
|
+
.replace(/\bfgColor\./g, 'fg.')
|
|
363
|
+
.replace(/\bbgColor\./g, 'bg.')
|
|
364
|
+
.replace(/\bborderColor\./g, 'border.');
|
|
365
|
+
}
|
|
11
366
|
/**
|
|
12
|
-
*
|
|
367
|
+
* Limits usage to max 3 most relevant items
|
|
368
|
+
*/
|
|
369
|
+
function limitUsage(usage) {
|
|
370
|
+
if (usage.length <= 3)
|
|
371
|
+
return usage;
|
|
372
|
+
return usage.slice(0, 3);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Creates a unique key for grouping tokens with identical guidelines within a category
|
|
13
376
|
*/
|
|
14
377
|
function createGuidelineKey(guideline) {
|
|
15
378
|
var _a;
|
|
16
379
|
return JSON.stringify({
|
|
380
|
+
category: guideline.category,
|
|
17
381
|
description: guideline.description || '',
|
|
18
382
|
usage: ((_a = guideline.usage) === null || _a === void 0 ? void 0 : _a.sort()) || [],
|
|
19
383
|
rules: guideline.rules || '',
|
|
@@ -21,8 +385,6 @@ function createGuidelineKey(guideline) {
|
|
|
21
385
|
}
|
|
22
386
|
/**
|
|
23
387
|
* Extracts category from token name
|
|
24
|
-
* - For "base-*" tokens, uses second word (e.g., "base-easing-ease" -> "easing")
|
|
25
|
-
* - Otherwise uses first word (e.g., "bgColor-danger-emphasis" -> "bgColor")
|
|
26
388
|
*/
|
|
27
389
|
function extractCategory(tokenName) {
|
|
28
390
|
const parts = tokenName.split('-');
|
|
@@ -31,22 +393,49 @@ function extractCategory(tokenName) {
|
|
|
31
393
|
}
|
|
32
394
|
return parts[0] || 'other';
|
|
33
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Extracts semantic subcategory from token name (e.g., "bgColor-danger-emphasis" -> "danger")
|
|
398
|
+
*/
|
|
399
|
+
function extractSemanticSubcategory(tokenName) {
|
|
400
|
+
const parts = tokenName.split('-');
|
|
401
|
+
if (parts.length >= 2) {
|
|
402
|
+
const subcat = parts[1];
|
|
403
|
+
if (SEMANTIC_SETS.includes(subcat)) {
|
|
404
|
+
return subcat;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Extracts variant from token name (e.g., "bgColor-danger-emphasis" -> "emphasis")
|
|
411
|
+
*/
|
|
412
|
+
function extractVariant(tokenName) {
|
|
413
|
+
const parts = tokenName.split('-');
|
|
414
|
+
if (parts.length >= 3) {
|
|
415
|
+
return parts.slice(2).join('-');
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
34
419
|
/**
|
|
35
420
|
* Formats category name for display
|
|
36
421
|
*/
|
|
37
422
|
function formatCategoryName(category) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
if (categoryMap[category]) {
|
|
43
|
-
return categoryMap[category];
|
|
44
|
-
}
|
|
45
|
-
// Capitalize first letter
|
|
423
|
+
if (category in CATEGORY_INFO) {
|
|
424
|
+
return CATEGORY_INFO[category].name;
|
|
425
|
+
}
|
|
46
426
|
return category.charAt(0).toUpperCase() + category.slice(1);
|
|
47
427
|
}
|
|
48
428
|
/**
|
|
49
|
-
*
|
|
429
|
+
* Gets category description if available
|
|
430
|
+
*/
|
|
431
|
+
function getCategoryDescription(category) {
|
|
432
|
+
if (category in CATEGORY_INFO) {
|
|
433
|
+
return CATEGORY_INFO[category].description;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Creates a key for grouping by usage and rules only
|
|
50
439
|
*/
|
|
51
440
|
function createUsageRulesKey(guideline) {
|
|
52
441
|
var _a;
|
|
@@ -55,19 +444,31 @@ function createUsageRulesKey(guideline) {
|
|
|
55
444
|
rules: guideline.rules || '',
|
|
56
445
|
});
|
|
57
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Creates a key for cross-category pattern matching
|
|
449
|
+
*/
|
|
450
|
+
function createPatternKey(description, rules) {
|
|
451
|
+
const normalizedDesc = description
|
|
452
|
+
.replace(/background/gi, 'COLOR_TYPE')
|
|
453
|
+
.replace(/border/gi, 'COLOR_TYPE')
|
|
454
|
+
.replace(/text/gi, 'COLOR_TYPE')
|
|
455
|
+
.replace(/foreground/gi, 'COLOR_TYPE');
|
|
456
|
+
const normalizedRules = rules
|
|
457
|
+
.replace(/bgColor/g, 'COLOR_TOKEN')
|
|
458
|
+
.replace(/borderColor/g, 'COLOR_TOKEN')
|
|
459
|
+
.replace(/fgColor/g, 'COLOR_TOKEN');
|
|
460
|
+
return JSON.stringify({ description: normalizedDesc, rules: normalizedRules });
|
|
461
|
+
}
|
|
58
462
|
/**
|
|
59
463
|
* Extracts a subcategory name from token names for headings
|
|
60
|
-
* e.g., "border-accent-emphasis" -> "accent"
|
|
61
464
|
*/
|
|
62
465
|
function extractSubcategory(tokenNames) {
|
|
63
466
|
if (tokenNames.length < 2)
|
|
64
467
|
return null;
|
|
65
|
-
// Get the second part of each token name
|
|
66
468
|
const subcategories = tokenNames.map(name => {
|
|
67
469
|
const parts = name.split('-');
|
|
68
470
|
return parts[1] || null;
|
|
69
471
|
});
|
|
70
|
-
// Check if all tokens share the same subcategory
|
|
71
472
|
const uniqueSubcats = [...new Set(subcategories.filter(Boolean))];
|
|
72
473
|
if (uniqueSubcats.length === 1) {
|
|
73
474
|
return uniqueSubcats[0];
|
|
@@ -75,12 +476,50 @@ function extractSubcategory(tokenNames) {
|
|
|
75
476
|
return null;
|
|
76
477
|
}
|
|
77
478
|
/**
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
479
|
+
* Outputs typography tokens grouped by role (Headings, Body, Code)
|
|
480
|
+
*/
|
|
481
|
+
function outputTypographyByRole(tokens, lines) {
|
|
482
|
+
var _a;
|
|
483
|
+
for (const { role, description, patterns } of TYPOGRAPHY_ROLES) {
|
|
484
|
+
const roleTokens = tokens.filter(t => patterns.some(p => t.name.startsWith(p)));
|
|
485
|
+
if (roleTokens.length === 0)
|
|
486
|
+
continue;
|
|
487
|
+
lines.push(`### ${role}`);
|
|
488
|
+
lines.push('');
|
|
489
|
+
lines.push(description);
|
|
490
|
+
lines.push('');
|
|
491
|
+
// Create table for role tokens
|
|
492
|
+
const headers = ['Token', 'Description', 'U:', 'R:'];
|
|
493
|
+
lines.push(`| ${headers.join(' | ')} |`);
|
|
494
|
+
lines.push(`|${headers.map(() => '---').join('|')}|`);
|
|
495
|
+
for (const token of roleTokens.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
496
|
+
const cells = [
|
|
497
|
+
`**${token.name}**`,
|
|
498
|
+
escapeTableCell(token.description || '-'),
|
|
499
|
+
((_a = token.usage) === null || _a === void 0 ? void 0 : _a.join(', ')) || '-',
|
|
500
|
+
escapeTableCell(token.rules || '-'),
|
|
501
|
+
];
|
|
502
|
+
lines.push(`| ${cells.join(' | ')} |`);
|
|
503
|
+
}
|
|
504
|
+
lines.push('');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Escapes pipe characters for markdown tables
|
|
509
|
+
*/
|
|
510
|
+
function escapeTableCell(text) {
|
|
511
|
+
// First escape backslashes, then escape pipe characters, to avoid
|
|
512
|
+
// existing backslashes altering how pipes are interpreted.
|
|
513
|
+
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* @description Outputs a hyper-optimized markdown file with LLM token guidelines.
|
|
517
|
+
* Optimizations:
|
|
518
|
+
* - Bracket notation for tokens with identical guidelines
|
|
519
|
+
* - Global category rules extracted to paragraph
|
|
520
|
+
* - Shortened keys (U:, R:, Pair ->)
|
|
521
|
+
* - Max 3 usage items
|
|
522
|
+
* - No boilerplate
|
|
84
523
|
*/
|
|
85
524
|
export const markdownLlmGuidelines = (_a) => __awaiter(void 0, [_a], void 0, function* ({ dictionary }) {
|
|
86
525
|
var _b;
|
|
@@ -95,13 +534,13 @@ export const markdownLlmGuidelines = (_a) => __awaiter(void 0, [_a], void 0, fun
|
|
|
95
534
|
category: extractCategory(token.name),
|
|
96
535
|
};
|
|
97
536
|
if (token.$description && typeof token.$description === 'string') {
|
|
98
|
-
guideline.description = token.$description;
|
|
537
|
+
guideline.description = densifyDescription(token.$description);
|
|
99
538
|
}
|
|
100
539
|
if (llmExt.usage && Array.isArray(llmExt.usage)) {
|
|
101
|
-
guideline.usage = llmExt.usage;
|
|
540
|
+
guideline.usage = limitUsage(llmExt.usage);
|
|
102
541
|
}
|
|
103
542
|
if (llmExt.rules && typeof llmExt.rules === 'string') {
|
|
104
|
-
guideline.rules = llmExt.rules;
|
|
543
|
+
guideline.rules = shortenRules(llmExt.rules);
|
|
105
544
|
}
|
|
106
545
|
guidelines.push(guideline);
|
|
107
546
|
}
|
|
@@ -113,82 +552,268 @@ export const markdownLlmGuidelines = (_a) => __awaiter(void 0, [_a], void 0, fun
|
|
|
113
552
|
}
|
|
114
553
|
grouped[guideline.category].push(guideline);
|
|
115
554
|
}
|
|
116
|
-
|
|
117
|
-
|
|
555
|
+
const lines = [
|
|
556
|
+
'# Primer Design Token Guidelines',
|
|
557
|
+
'',
|
|
558
|
+
'Reference for using GitHub Primer design tokens.',
|
|
559
|
+
'',
|
|
560
|
+
'## Legend',
|
|
561
|
+
'',
|
|
562
|
+
'- **U:** Use cases',
|
|
563
|
+
'- **R:** Token-specific rules (see Semantic Key for general meaning)',
|
|
564
|
+
'- **emphasis** variant: Strong/prominent version, use `fg.onEmphasis` for text',
|
|
565
|
+
'- **muted** variant: Subtle version, use matching `fg.*` color for text',
|
|
566
|
+
'- **[a, b]** Bracket notation groups related tokens',
|
|
567
|
+
'',
|
|
568
|
+
'## Semantic Key',
|
|
569
|
+
'',
|
|
570
|
+
'These semantic meanings apply across all token types (bgColor, borderColor, fgColor, border).',
|
|
571
|
+
'',
|
|
572
|
+
'| Semantic | Meaning | Example Usage | Text Pairing |',
|
|
573
|
+
'|---|---|---|---|',
|
|
574
|
+
];
|
|
575
|
+
// Output semantic key table
|
|
576
|
+
for (const [key, info] of Object.entries(SEMANTIC_KEY)) {
|
|
577
|
+
lines.push(`| **${key}** | ${info.meaning} | ${info.usage} | ${info.textPairing} |`);
|
|
578
|
+
}
|
|
579
|
+
lines.push('');
|
|
580
|
+
// Collect semantic tokens across mergeable categories for cross-category patterns
|
|
581
|
+
const semanticTokensByPattern = new Map();
|
|
582
|
+
for (const category of MERGEABLE_CATEGORIES) {
|
|
583
|
+
if (!(category in grouped))
|
|
584
|
+
continue;
|
|
585
|
+
for (const guideline of grouped[category]) {
|
|
586
|
+
const subcategory = extractSemanticSubcategory(guideline.name);
|
|
587
|
+
const variant = extractVariant(guideline.name);
|
|
588
|
+
if (subcategory && variant) {
|
|
589
|
+
const patternKey = createPatternKey(guideline.description || '', guideline.rules || '');
|
|
590
|
+
const key = `${subcategory}-${variant}-${patternKey}`;
|
|
591
|
+
if (!semanticTokensByPattern.has(key)) {
|
|
592
|
+
semanticTokensByPattern.set(key, []);
|
|
593
|
+
}
|
|
594
|
+
semanticTokensByPattern.get(key).push({ subcategory, variant, categories: [category], guideline });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Find patterns spanning multiple categories (must have entries from at least 2 different categories)
|
|
599
|
+
const mergedEntries = [];
|
|
600
|
+
const mergedTokens = new Set();
|
|
601
|
+
for (const [, entries] of semanticTokensByPattern) {
|
|
602
|
+
// Get unique categories in this pattern
|
|
603
|
+
const uniqueCategories = new Set(entries.map(e => e.categories[0]));
|
|
604
|
+
if (uniqueCategories.size > 1) {
|
|
605
|
+
// Only merge if pattern spans multiple categories
|
|
606
|
+
mergedEntries.push({
|
|
607
|
+
subcategory: entries[0].subcategory,
|
|
608
|
+
variant: entries[0].variant,
|
|
609
|
+
guideline: entries[0].guideline,
|
|
610
|
+
});
|
|
611
|
+
for (const e of entries) {
|
|
612
|
+
mergedTokens.add(`${e.categories[0]}-${e.subcategory}-${e.variant}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Output compact semantic color reference
|
|
617
|
+
// Group by variant (emphasis/muted) and list all applicable semantics
|
|
618
|
+
if (mergedEntries.length > 0) {
|
|
619
|
+
lines.push('## Semantic Colors');
|
|
620
|
+
lines.push('');
|
|
621
|
+
lines.push('Apply to `bgColor-*`, `borderColor-*`, `fgColor-*`, and `border-*` tokens.');
|
|
622
|
+
lines.push('Refer to Semantic Key above for meaning and text pairings.');
|
|
623
|
+
lines.push('');
|
|
624
|
+
// Group entries by variant
|
|
625
|
+
const byVariant = new Map();
|
|
626
|
+
for (const entry of mergedEntries) {
|
|
627
|
+
if (!byVariant.has(entry.variant)) {
|
|
628
|
+
byVariant.set(entry.variant, []);
|
|
629
|
+
}
|
|
630
|
+
byVariant.get(entry.variant).push(entry.subcategory);
|
|
631
|
+
}
|
|
632
|
+
// Output as compact list
|
|
633
|
+
lines.push('| Pattern | Semantics |');
|
|
634
|
+
lines.push('|---|---|');
|
|
635
|
+
for (const [variant, semantics] of byVariant) {
|
|
636
|
+
const sortedSemantics = [...new Set(semantics)].sort();
|
|
637
|
+
lines.push(`| **\\*-[${sortedSemantics.join(', ')}]-${variant}** | See Semantic Key |`);
|
|
638
|
+
}
|
|
639
|
+
lines.push('');
|
|
640
|
+
// Add special notes for tokens with unique constraints
|
|
641
|
+
lines.push('**Special cases:**');
|
|
642
|
+
lines.push('- `*-attention-emphasis`: Use `fg.default` for text (yellow has poor contrast)');
|
|
643
|
+
lines.push('- `*-sponsors-*`: GitHub Sponsors only, not for general pink UI');
|
|
644
|
+
lines.push('- `*-upsell-*`: Promotional content only, not for regular features');
|
|
645
|
+
lines.push('- `*-open/*-closed/*-done`: GitHub issue/PR states specifically');
|
|
646
|
+
lines.push('');
|
|
647
|
+
}
|
|
118
648
|
for (const category of Object.keys(grouped).sort()) {
|
|
119
|
-
lines.push(`## ${formatCategoryName(category)}`, '');
|
|
120
649
|
const categoryGuidelines = grouped[category];
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
650
|
+
// Separate semantic and non-semantic tokens
|
|
651
|
+
const semanticTokens = [];
|
|
652
|
+
const nonSemanticTokens = [];
|
|
653
|
+
for (const guideline of categoryGuidelines) {
|
|
654
|
+
if (mergedTokens.has(guideline.name))
|
|
655
|
+
continue;
|
|
656
|
+
const subcategory = extractSemanticSubcategory(guideline.name);
|
|
657
|
+
if (subcategory) {
|
|
658
|
+
semanticTokens.push(guideline);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
nonSemanticTokens.push(guideline);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (semanticTokens.length === 0 && nonSemanticTokens.length === 0)
|
|
665
|
+
continue;
|
|
666
|
+
lines.push(`## ${formatCategoryName(category)}`);
|
|
667
|
+
// Special handling for typography - group by role
|
|
668
|
+
if (category === 'text') {
|
|
669
|
+
const categoryDesc = getCategoryDescription(category);
|
|
670
|
+
if (categoryDesc) {
|
|
671
|
+
lines.push('');
|
|
672
|
+
lines.push(categoryDesc);
|
|
132
673
|
}
|
|
133
674
|
lines.push('');
|
|
675
|
+
outputTypographyByRole(nonSemanticTokens, lines);
|
|
676
|
+
continue;
|
|
134
677
|
}
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
678
|
+
// Special handling for pattern-compressed categories (control, overlay, stack, spinner)
|
|
679
|
+
if (category in PATTERN_COMPRESSED_CATEGORIES) {
|
|
680
|
+
const categoryDesc = getCategoryDescription(category);
|
|
681
|
+
if (categoryDesc) {
|
|
682
|
+
lines.push('');
|
|
683
|
+
lines.push(categoryDesc);
|
|
684
|
+
}
|
|
685
|
+
lines.push('');
|
|
686
|
+
// Include semantic tokens in the output for pattern compression
|
|
687
|
+
const allTokens = [...semanticTokens, ...nonSemanticTokens];
|
|
688
|
+
outputPatternCompressedCategory(category, allTokens, PATTERN_COMPRESSED_CATEGORIES[category], lines);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
// Determine best category description: prefer token description for single-group categories
|
|
692
|
+
const consolidatedGroupsPreview = new Map();
|
|
693
|
+
for (const guideline of nonSemanticTokens) {
|
|
138
694
|
const key = createGuidelineKey(guideline);
|
|
139
|
-
if (!
|
|
140
|
-
|
|
695
|
+
if (!consolidatedGroupsPreview.has(key)) {
|
|
696
|
+
consolidatedGroupsPreview.set(key, []);
|
|
697
|
+
}
|
|
698
|
+
consolidatedGroupsPreview.get(key).push(guideline);
|
|
699
|
+
}
|
|
700
|
+
// Use token description if there's only one group with multiple tokens that has a description
|
|
701
|
+
const singleGroupWithDesc = consolidatedGroupsPreview.size === 1 &&
|
|
702
|
+
nonSemanticTokens.length > 1 &&
|
|
703
|
+
nonSemanticTokens[0].description &&
|
|
704
|
+
semanticTokens.length === 0;
|
|
705
|
+
const categoryDesc = singleGroupWithDesc ? nonSemanticTokens[0].description : getCategoryDescription(category);
|
|
706
|
+
if (categoryDesc) {
|
|
707
|
+
lines.push('');
|
|
708
|
+
lines.push(categoryDesc);
|
|
709
|
+
}
|
|
710
|
+
lines.push('');
|
|
711
|
+
// Output semantic tokens as compact reference (details in Semantic Key)
|
|
712
|
+
// Track if we've output shared usage/rules to avoid duplication
|
|
713
|
+
let outputSharedUsage = false;
|
|
714
|
+
let outputSharedRules = false;
|
|
715
|
+
if (semanticTokens.length > 0) {
|
|
716
|
+
// Group semantic tokens by variant for compact display
|
|
717
|
+
const byVariant = new Map();
|
|
718
|
+
const noVariantTokens = []; // For single-level semantic tokens like fgColor-danger
|
|
719
|
+
for (const token of semanticTokens) {
|
|
720
|
+
const subcategory = extractSemanticSubcategory(token.name);
|
|
721
|
+
const variant = extractVariant(token.name);
|
|
722
|
+
if (subcategory) {
|
|
723
|
+
if (variant) {
|
|
724
|
+
if (!byVariant.has(variant)) {
|
|
725
|
+
byVariant.set(variant, []);
|
|
726
|
+
}
|
|
727
|
+
byVariant.get(variant).push(subcategory);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
noVariantTokens.push(subcategory);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Output compact semantic reference
|
|
735
|
+
if (byVariant.size > 0 || noVariantTokens.length > 0) {
|
|
736
|
+
lines.push('**Semantic tokens** (see Semantic Key for meaning):');
|
|
737
|
+
// Tokens with variants (bgColor-danger-emphasis, etc.)
|
|
738
|
+
for (const [variant, semantics] of byVariant) {
|
|
739
|
+
const uniqueSemantics = [...new Set(semantics)].sort();
|
|
740
|
+
lines.push(`- \`${category}-[${uniqueSemantics.join(', ')}]-${variant}\``);
|
|
741
|
+
}
|
|
742
|
+
// Single-level semantic tokens (fgColor-danger, etc.)
|
|
743
|
+
if (noVariantTokens.length > 0) {
|
|
744
|
+
const uniqueSemantics = [...new Set(noVariantTokens)].sort();
|
|
745
|
+
lines.push(`- \`${category}-[${uniqueSemantics.join(', ')}]\``);
|
|
746
|
+
}
|
|
747
|
+
lines.push('');
|
|
748
|
+
outputSharedUsage = true;
|
|
749
|
+
outputSharedRules = true;
|
|
141
750
|
}
|
|
142
|
-
consolidatedGroups.get(key).push(guideline);
|
|
143
751
|
}
|
|
752
|
+
// Check shared usage/rules for non-semantic tokens
|
|
753
|
+
const usageRulesKeys = new Set(nonSemanticTokens.map(createUsageRulesKey));
|
|
754
|
+
const sharedUsageRules = usageRulesKeys.size === 1 && nonSemanticTokens.length > 1;
|
|
755
|
+
// Only output if not already output for semantic tokens and content is different
|
|
756
|
+
if (sharedUsageRules && nonSemanticTokens.length > 0) {
|
|
757
|
+
const first = nonSemanticTokens[0];
|
|
758
|
+
const shouldOutputUsage = first.usage && first.usage.length > 0 && !outputSharedUsage;
|
|
759
|
+
const shouldOutputRules = first.rules && !outputSharedRules;
|
|
760
|
+
if (shouldOutputUsage || shouldOutputRules) {
|
|
761
|
+
if (shouldOutputUsage) {
|
|
762
|
+
lines.push(`**U:** ${first.usage.join(', ')}`);
|
|
763
|
+
}
|
|
764
|
+
if (shouldOutputRules) {
|
|
765
|
+
lines.push(`**R:** ${first.rules}`);
|
|
766
|
+
}
|
|
767
|
+
lines.push('');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Group non-semantic tokens with identical guidelines (reuse preview)
|
|
771
|
+
const consolidatedGroups = consolidatedGroupsPreview;
|
|
144
772
|
for (const [, guidelinesGroup] of consolidatedGroups) {
|
|
145
773
|
const first = guidelinesGroup[0];
|
|
146
774
|
const tokenNames = guidelinesGroup.map(g => g.name);
|
|
147
775
|
if (guidelinesGroup.length > 1) {
|
|
148
|
-
// Multiple tokens share the same guidelines - consolidate
|
|
149
776
|
const subcategory = extractSubcategory(tokenNames);
|
|
150
|
-
if
|
|
777
|
+
// Only add heading if there's a meaningful subcategory and multiple groups
|
|
778
|
+
if (subcategory && consolidatedGroups.size > 1) {
|
|
151
779
|
lines.push(`### ${subcategory}`);
|
|
152
780
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (first.description && consolidatedGroups.size > 1) {
|
|
156
|
-
lines.push(`### general`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (first.description) {
|
|
781
|
+
// Only output description if no category description was already shown
|
|
782
|
+
if (first.description && !categoryDesc) {
|
|
160
783
|
lines.push(first.description);
|
|
161
784
|
}
|
|
162
|
-
// Only show usage/rules if not already shown at category level
|
|
163
785
|
if (!sharedUsageRules) {
|
|
164
786
|
if (first.usage && first.usage.length > 0) {
|
|
165
|
-
lines.push(`**
|
|
787
|
+
lines.push(`**U:** ${first.usage.join(', ')}`);
|
|
166
788
|
}
|
|
167
789
|
if (first.rules) {
|
|
168
|
-
lines.push(`**
|
|
790
|
+
lines.push(`**R:** ${first.rules}`);
|
|
169
791
|
}
|
|
170
792
|
}
|
|
171
793
|
lines.push(`**Tokens:** ${tokenNames.join(', ')}`);
|
|
172
794
|
lines.push('');
|
|
173
795
|
}
|
|
174
796
|
else {
|
|
175
|
-
// Single token - output individually
|
|
176
797
|
lines.push(`### ${first.name}`);
|
|
177
798
|
if (first.description) {
|
|
178
799
|
lines.push(first.description);
|
|
179
800
|
}
|
|
180
|
-
// Only show usage/rules if not already shown at category level
|
|
181
801
|
if (!sharedUsageRules) {
|
|
182
802
|
if (first.usage && first.usage.length > 0) {
|
|
183
|
-
lines.push(`**
|
|
803
|
+
lines.push(`**U:** ${first.usage.join(', ')}`);
|
|
184
804
|
}
|
|
185
805
|
if (first.rules) {
|
|
186
|
-
lines.push(`**
|
|
806
|
+
lines.push(`**R:** ${first.rules}`);
|
|
187
807
|
}
|
|
188
808
|
}
|
|
189
809
|
lines.push('');
|
|
190
810
|
}
|
|
191
811
|
}
|
|
192
812
|
}
|
|
813
|
+
// Add final directive for AI
|
|
814
|
+
lines.push('---');
|
|
815
|
+
lines.push('');
|
|
816
|
+
lines.push('**Final Directive for AI**:');
|
|
817
|
+
lines.push('Always cross-reference the `Semantic Key` at the top of this SPEC before confirming a token choice. If a specific component token is missing, derive it using the `[category]-[semantic]-[variant]` pattern.');
|
|
193
818
|
return lines.join('\n');
|
|
194
819
|
});
|