@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.
@@ -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
- * Creates a unique key for grouping tokens with identical guidelines
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
- const categoryMap = {
39
- bgColor: 'background color',
40
- fgColor: 'text and foreground color',
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
- * Creates a key for grouping by usage and rules only (not description)
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
- * @description Outputs a markdown file with LLM token guidelines, extracting
79
- * description from $description and usage/rules from $extensions['org.primer.llm'].
80
- * Tokens with identical guidelines (from group-level inheritance) are consolidated
81
- * into a single entry listing all token names.
82
- * @param FormatFnArguments
83
- * @returns formatted markdown `string`
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
- // Build markdown output
117
- const lines = ['# Token Guidelines', ''];
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
- // Check if all tokens in category share the same usage/rules AND there are multiple tokens
122
- const usageRulesKeys = new Set(categoryGuidelines.map(createUsageRulesKey));
123
- const sharedUsageRules = usageRulesKeys.size === 1 && categoryGuidelines.length > 1;
124
- // If shared, output usage/rules once at category level
125
- if (sharedUsageRules) {
126
- const first = categoryGuidelines[0];
127
- if (first.usage && first.usage.length > 0) {
128
- lines.push(`**Usage:** ${first.usage.join(', ')}`);
129
- }
130
- if (first.rules) {
131
- lines.push(`**Rules:** ${first.rules}`);
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
- // Group tokens with identical guidelines (description + usage + rules)
136
- const consolidatedGroups = new Map();
137
- for (const guideline of categoryGuidelines) {
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 (!consolidatedGroups.has(key)) {
140
- consolidatedGroups.set(key, []);
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 (subcategory) {
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
- else {
154
- // No common subcategory - use "general" or skip heading if description serves as intro
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(`**Usage:** ${first.usage.join(', ')}`);
787
+ lines.push(`**U:** ${first.usage.join(', ')}`);
166
788
  }
167
789
  if (first.rules) {
168
- lines.push(`**Rules:** ${first.rules}`);
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(`**Usage:** ${first.usage.join(', ')}`);
803
+ lines.push(`**U:** ${first.usage.join(', ')}`);
184
804
  }
185
805
  if (first.rules) {
186
- lines.push(`**Rules:** ${first.rules}`);
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
  });