@primer/primitives
Version:
Typography, spacing, and color primitives for Primer design system
1,166 lines • 50.1 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { sortByName } from 'style-dictionary/utils';
// Semantic sets that should be grouped into tables
const SEMANTIC_SETS = [
'accent',
'danger',
'success',
'attention',
'severe',
'open',
'closed',
'done',
'neutral',
'sponsors',
'upsell',
];
// Categories that can be merged with wildcard patterns
const MERGEABLE_CATEGORIES = ['bgColor', 'borderColor', 'fgColor'];
// Global semantic key definitions - these explain what each semantic color means
// so we don't repeat this info in every table row
const SEMANTIC_KEY = {
danger: {
meaning: 'Errors, destructive actions, critical warnings',
usage: 'delete buttons, error messages, validation errors',
textPairing: 'fg.danger (muted bg) or fg.onEmphasis (emphasis bg)',
},
success: {
meaning: 'Positive states, confirmations, completed actions',
usage: 'merge buttons, success messages, confirmations',
textPairing: 'fg.success (muted bg) or fg.onEmphasis (emphasis bg)',
},
attention: {
meaning: 'Warnings, caution states requiring user awareness',
usage: 'warning banners, caution labels, pending states',
textPairing: 'fg.attention (muted bg) or fg.default (emphasis bg, due to yellow contrast)',
},
severe: {
meaning: 'High-priority warnings, more urgent than attention',
usage: 'urgent messages, escalations, high-priority indicators',
textPairing: 'fg.severe (muted bg) or fg.onEmphasis (emphasis bg)',
},
accent: {
meaning: 'Selected, focused, or highlighted interactive elements',
usage: 'active states, selected rows, focus indicators',
textPairing: 'fg.accent (muted bg) or fg.onEmphasis (emphasis bg)',
},
neutral: {
meaning: 'Non-semantic, secondary UI elements',
usage: 'secondary buttons, tags, labels without status meaning',
textPairing: 'fg.default (muted bg) or fg.onEmphasis (emphasis bg)',
},
open: {
meaning: 'Open/active state indicators (GitHub issues, PRs)',
usage: 'open issues, open PRs, active discussions',
textPairing: 'fg.open (muted bg) or fg.onEmphasis (emphasis bg)',
},
closed: {
meaning: 'Closed/declined state indicators (GitHub issues, PRs)',
usage: 'closed issues, closed PRs, declined items',
textPairing: 'fg.closed (muted bg) or fg.onEmphasis (emphasis bg)',
},
done: {
meaning: 'Completed/merged state indicators',
usage: 'merged PRs, completed tasks, finished items',
textPairing: 'fg.done (muted bg) or fg.onEmphasis (emphasis bg)',
},
sponsors: {
meaning: 'GitHub Sponsors content only',
usage: 'sponsor buttons, funding prompts, sponsor cards',
textPairing: 'fg.sponsors (muted bg) or fg.onEmphasis (emphasis bg)',
},
upsell: {
meaning: 'Upgrade prompts, premium features, promotional content',
usage: 'upgrade buttons, premium badges, promotional banners',
textPairing: 'fg.upsell (muted bg) or fg.onEmphasis (emphasis bg)',
},
};
// Typography role groupings for better LLM comprehension
const TYPOGRAPHY_ROLES = [
{
role: 'Headings',
description: 'Title and display text styles for headings and hero sections.',
patterns: ['text-title-', 'text-display-', 'text-subtitle-'],
},
{
role: 'Body',
description: 'Body text and caption styles for content and UI labels.',
patterns: ['text-body-', 'text-caption-'],
},
{
role: 'Code',
description: 'Monospace text styles for code blocks and inline code.',
patterns: ['text-code'],
},
];
// Size scales for pattern-based compression
const SIZE_SCALES = ['xsmall', 'small', 'medium', 'large', 'xlarge'];
const DENSITY_SCALES = ['condensed', 'normal', 'spacious'];
// Categories that use pattern-based compression for size tokens
const PATTERN_COMPRESSED_CATEGORIES = {
control: {
scaleNote: 'Use xsmall/small for dense layouts, medium for default UI, large/xlarge for prominent CTAs.',
sizeScale: SIZE_SCALES,
densityScale: DENSITY_SCALES,
stateGroups: ['checked', 'transparent'],
},
controlStack: {
scaleNote: 'Match gap size to control size. Use condensed for tight groupings, spacious for separated actions.',
sizeScale: ['small', 'medium', 'large'],
densityScale: ['condensed', 'spacious'],
},
overlay: {
scaleNote: 'Use xsmall/small for menus and tooltips, medium for dialogs, large/xlarge for complex modals or sheets.',
sizeScale: ['xsmall', 'small', 'medium', 'large', 'xlarge'],
densityScale: ['condensed', 'normal'],
},
spinner: {
scaleNote: 'Use small for inline loading, medium for buttons/cards, large for full-page states.',
sizeScale: ['small', 'medium', 'large'],
},
stack: {
scaleNote: 'Use condensed for dense lists, normal for standard layouts, spacious for prominent sections.',
densityScale: DENSITY_SCALES,
},
};
/**
* Outputs pattern-compressed tokens for a category
*/
function outputPatternCompressedCategory(category, tokens, config, lines) {
var _a;
const tokenNames = tokens.map(t => t.name);
// Output scale note
lines.push(`**Scale:** ${config.scaleNote}`);
lines.push('');
// Categorize tokens
const sizeTokens = [];
const stateTokens = new Map();
const otherTokens = [];
for (const name of tokenNames) {
const rest = name.replace(`${category}-`, '');
// Check if it's a state token (checked, transparent)
let isStateToken = false;
if (config.stateGroups) {
for (const state of config.stateGroups) {
if (rest.startsWith(`${state}-`)) {
if (!stateTokens.has(state)) {
stateTokens.set(state, []);
}
stateTokens.get(state).push(rest.replace(`${state}-`, ''));
isStateToken = true;
break;
}
}
}
if (isStateToken)
continue;
// Check if it's a size token (exact match on part, not substring)
const parts = rest.split('-');
const hasSizeScale = (_a = config.sizeScale) === null || _a === void 0 ? void 0 : _a.some(s => parts.includes(s));
if (hasSizeScale) {
sizeTokens.push(name);
}
else {
otherTokens.push(name);
}
}
// Output size patterns
if (sizeTokens.length > 0 && config.sizeScale) {
// Group by property - handle both prefix-size and size-suffix patterns
// e.g., control-medium-gap (size first) vs overlay-width-medium (size last)
const byPattern = new Map();
for (const name of sizeTokens) {
const rest = name.replace(`${category}-`, '');
const parts = rest.split('-');
// Find exact size match in parts
for (const size of config.sizeScale) {
const sizeIdx = parts.indexOf(size);
if (sizeIdx >= 0) {
const prefix = parts.slice(0, sizeIdx).join('-');
const suffix = parts.slice(sizeIdx + 1).join('-');
// Create pattern key with placeholder for size
let patternKey;
if (prefix && suffix) {
patternKey = `${prefix}-[size]-${suffix}`;
}
else if (prefix) {
patternKey = `${prefix}-[size]`;
}
else if (suffix) {
patternKey = `[size]-${suffix}`;
}
else {
patternKey = '[size]';
}
if (!byPattern.has(patternKey)) {
byPattern.set(patternKey, new Set());
}
byPattern.get(patternKey).add(size);
break;
}
}
}
// Group patterns by their size sets for compact output
const bySizeSet = new Map();
for (const [pattern, sizes] of byPattern) {
const sizeKey = [...sizes].sort((a, b) => config.sizeScale.indexOf(a) - config.sizeScale.indexOf(b)).join(',');
if (!bySizeSet.has(sizeKey)) {
bySizeSet.set(sizeKey, []);
}
bySizeSet.get(sizeKey).push(pattern);
}
lines.push('**Size patterns:**');
for (const [sizeKey, patterns] of bySizeSet) {
const sizes = sizeKey.split(',');
const sizeNotation = sizes.length > 1 ? `[${sizes.join(', ')}]` : sizes[0];
// Replace [size] placeholder with actual size notation
const formattedPatterns = patterns.map(p => p.replace('[size]', sizeNotation)).sort();
// Try to merge patterns with same size but different properties
if (formattedPatterns.length > 1) {
// Check if they share a common structure
const suffixes = formattedPatterns.map(p => {
const escapedSizeNotation = sizeNotation.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = p.match(new RegExp(`${escapedSizeNotation}-(.+)$`));
return match ? match[1] : null;
});
if (suffixes.every(s => s !== null)) {
const uniqueSuffixes = [...new Set(suffixes)].sort();
if (uniqueSuffixes.length > 1) {
lines.push(`- \`${category}-${sizeNotation}-[${uniqueSuffixes.join(', ')}]\``);
}
else {
lines.push(`- \`${category}-${sizeNotation}-${uniqueSuffixes[0]}\``);
}
}
else {
// Output each pattern separately
for (const pattern of formattedPatterns) {
lines.push(`- \`${category}-${pattern}\``);
}
}
}
else {
lines.push(`- \`${category}-${formattedPatterns[0]}\``);
}
}
lines.push('');
}
// Output state tokens
if (stateTokens.size > 0) {
lines.push('**State variants:**');
for (const [state, suffixes] of stateTokens) {
const sortedSuffixes = [...new Set(suffixes)].sort();
const suffixNotation = sortedSuffixes.length > 1 ? `[${sortedSuffixes.join(', ')}]` : sortedSuffixes[0];
lines.push(`- \`${category}-${state}-${suffixNotation}\``);
}
lines.push('');
}
// Output other tokens (colors, misc)
if (otherTokens.length > 0) {
// Group by type (bgColor, fgColor, etc.)
const byType = new Map();
for (const name of otherTokens) {
const rest = name.replace(`${category}-`, '');
const parts = rest.split('-');
// Find the type (bgColor, fgColor, borderColor, etc.)
let type = parts[0];
let variant = parts.slice(1).join('-') || 'default';
// Handle compound types like minTarget
if (parts.length >= 2 && !['bgColor', 'fgColor', 'borderColor', 'iconColor'].includes(type)) {
type = parts.slice(0, -1).join('-');
variant = parts[parts.length - 1];
}
if (!byType.has(type)) {
byType.set(type, []);
}
byType.get(type).push(variant);
}
lines.push('**Other tokens:**');
for (const [type, variants] of [...byType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
const sortedVariants = [...new Set(variants)].sort();
if (sortedVariants.length > 1) {
lines.push(`- \`${category}-${type}-[${sortedVariants.join(', ')}]\``);
}
else {
lines.push(`- \`${category}-${type}-${sortedVariants[0]}\``);
}
}
lines.push('');
}
}
/**
* Outputs pattern-compressed display color tokens
* Extracts unique colors and properties to show as display-[color]-[property] pattern
*/
function outputDisplayColorPattern(tokens, lines) {
const tokenNames = tokens.map(t => t.name);
// Extract unique colors and properties
const colors = new Set();
const properties = new Set();
const hasScale = new Set();
for (const name of tokenNames) {
// Parse: display-{color}-{property} or display-{color}-scale-{n}
const parts = name.replace('display-', '').split('-');
if (parts.length < 2)
continue;
const color = parts[0];
colors.add(color);
// Check for scale tokens (display-blue-scale-0)
if (parts[1] === 'scale' && parts.length >= 3) {
hasScale.add(parts[2]);
}
else {
// Property tokens (display-blue-bgColor-emphasis, display-blue-fgColor)
const property = parts.slice(1).join('-');
properties.add(property);
}
}
// Sort colors alphabetically
const sortedColors = [...colors].sort();
const sortedProperties = [...properties].sort((a, b) => {
// Sort by type first (bgColor, borderColor, fgColor)
const typeOrder = ['bgColor-emphasis', 'bgColor-muted', 'borderColor-emphasis', 'borderColor-muted', 'fgColor'];
const aIdx = typeOrder.indexOf(a);
const bIdx = typeOrder.indexOf(b);
if (aIdx !== -1 && bIdx !== -1)
return aIdx - bIdx;
if (aIdx !== -1)
return -1;
if (bIdx !== -1)
return 1;
return a.localeCompare(b);
});
// Output pattern with variables
lines.push('**Pattern:** `display-[color]-[property]`');
lines.push('');
lines.push('**Variables:**');
lines.push(`- **color:** ${sortedColors.join(' | ')}`);
lines.push(`- **property:** ${sortedProperties.join(' | ')}`);
lines.push('');
// Output scale pattern if present
if (hasScale.size > 0) {
const sortedScales = [...hasScale].sort((a, b) => parseInt(a) - parseInt(b));
lines.push('**Scale pattern:** `display-[color]-scale-[n]`');
lines.push(`- **n:** ${sortedScales.join(' | ')}`);
lines.push('');
lines.push('*Scale 0-2: lighter (backgrounds), 3-5: mid-tones, 6-9: darker (foregrounds/borders)*');
lines.push('');
}
}
/**
* Outputs pattern-compressed data visualization tokens
* Extracts unique colors and variants to show as data-[color]-color-[variant] pattern
*/
function outputDataVisColorPattern(tokens, lines) {
const tokenNames = tokens.map(t => t.name);
// Extract unique colors and variants
const colors = new Set();
const variants = new Set();
for (const name of tokenNames) {
// Parse: data-{color}-color-{variant}
const parts = name.replace('data-', '').split('-');
if (parts.length < 3)
continue;
const color = parts[0];
colors.add(color);
// Variant is the last part (emphasis, muted)
// Format: color-color-variant -> parts[1] is 'color', parts[2] is variant
if (parts[1] === 'color' && parts.length >= 3) {
variants.add(parts[2]);
}
}
// Sort colors alphabetically
const sortedColors = [...colors].sort();
const sortedVariants = [...variants].sort((a, b) => {
// emphasis first, then muted
const order = ['emphasis', 'muted'];
return order.indexOf(a) - order.indexOf(b);
});
// Output pattern with variables
lines.push('**Pattern:** `data-[color]-color-[variant]`');
lines.push('');
lines.push('**Variables:**');
lines.push(`- **color:** ${sortedColors.join(' | ')}`);
lines.push(`- **variant:** ${sortedVariants.join(' | ')}`);
lines.push('');
// Add usage guidance
lines.push('**Variant usage:**');
lines.push('- **emphasis:** Lines, bars, borders, data points');
lines.push('- **muted:** Area fills, backgrounds, subtle regions');
lines.push('');
lines.push('*Pair emphasis with muted variants of the same color for cohesive chart styling.*');
lines.push('');
}
/**
* Outputs pattern-compressed ANSI color tokens
* Extracts unique colors and variants (default/bright) to show as color-ansi-[color]-[variant] pattern
*/
function outputAnsiColorPattern(tokens, lines) {
const tokenNames = tokens.map(t => t.name);
// Extract unique colors and variants
const colors = new Set();
const hasVariants = new Set();
for (const name of tokenNames) {
// Parse: color-ansi-{color} or color-ansi-{color}-bright
const parts = name.replace('color-ansi-', '').split('-');
if (parts.length < 1)
continue;
const color = parts[0];
colors.add(color);
// Check for bright variant
if (parts.length > 1 && parts[1] === 'bright') {
hasVariants.add('bright');
}
else {
hasVariants.add('default');
}
}
// Sort colors in ANSI order
const ansiOrder = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray'];
const sortedColors = [...colors].sort((a, b) => {
const aIdx = ansiOrder.indexOf(a);
const bIdx = ansiOrder.indexOf(b);
if (aIdx !== -1 && bIdx !== -1)
return aIdx - bIdx;
if (aIdx !== -1)
return -1;
if (bIdx !== -1)
return 1;
return a.localeCompare(b);
});
// Output pattern with variables
lines.push('**Pattern:** `color-ansi-[color]` or `color-ansi-[color]-bright`');
lines.push('');
lines.push('**Variables:**');
lines.push(`- **color:** ${sortedColors.join(' | ')}`);
if (hasVariants.has('bright')) {
lines.push('- **variant:** default (no suffix) | bright');
}
lines.push('');
}
/**
* Outputs pattern-compressed prettylights syntax tokens using prefix grouping
* Groups tokens by their primary element category
*/
function outputPrettylightsPattern(tokens, lines) {
const tokenNames = tokens.map(t => t.name);
// Common single-word elements (no sub-variants)
const simpleElements = [];
// Grouped elements with sub-variants (markup-*, brackethighlighter-*, etc.)
const groupedElements = new Map();
// Elements with bg/text variants - track the base name and which suffixes exist
const bgTextVariants = new Map();
for (const name of tokenNames) {
// Parse: color-prettylights-syntax-{element} or color-prettylights-syntax-{group}-{sub}
const element = name.replace('color-prettylights-syntax-', '');
const parts = element.split('-');
if (parts.length === 1) {
// Simple element: comment, constant, entity, keyword, string, variable
simpleElements.push(parts[0]);
}
else {
const group = parts[0];
const rest = parts.slice(1).join('-');
// Check for bg/text suffix patterns (e.g., markup-changed-bg, markup-changed-text)
if (rest.endsWith('-bg') || rest.endsWith('-text')) {
const baseName = rest.replace(/-bg$/, '').replace(/-text$/, '');
const suffix = rest.endsWith('-bg') ? 'bg' : 'text';
const key = `${group}:${baseName}`; // Use colon to separate group from base
if (!bgTextVariants.has(key)) {
bgTextVariants.set(key, new Set());
}
bgTextVariants.get(key).add(suffix);
}
else {
// Regular grouped element
if (!groupedElements.has(group)) {
groupedElements.set(group, new Set());
}
groupedElements.get(group).add(rest);
}
}
}
// Output pattern
lines.push('**Pattern:** `color-prettylights-syntax-[element]`');
lines.push('');
// Output simple elements
if (simpleElements.length > 0) {
const sorted = [...new Set(simpleElements)].sort();
lines.push(`**Core elements:** ${sorted.join(', ')}`);
}
// Merge bg/text variants into grouped elements for cleaner output
// Group bg/text variants by their prefix group
const bgTextByGroup = new Map();
for (const [key] of bgTextVariants) {
const [group, baseName] = key.split(':');
if (!bgTextByGroup.has(group)) {
bgTextByGroup.set(group, []);
}
bgTextByGroup.get(group).push(baseName);
}
// Output grouped elements
if (groupedElements.size > 0 || bgTextByGroup.size > 0) {
lines.push('');
lines.push('**Compound elements:**');
// Get all groups (both regular and bg/text)
const allGroups = new Set([...groupedElements.keys(), ...bgTextByGroup.keys()]);
for (const group of [...allGroups].sort()) {
const regularSubs = groupedElements.get(group) || new Set();
const bgTextBases = bgTextByGroup.get(group) || [];
// Combine regular subs
if (regularSubs.size > 0) {
const uniqueSubs = [...regularSubs].sort();
lines.push(`- \`${group}-[${uniqueSubs.join(', ')}]\``);
}
// Output bg/text variants separately for clarity
if (bgTextBases.length > 0) {
const uniqueBases = [...new Set(bgTextBases)].sort();
lines.push(`- \`${group}-[${uniqueBases.join(', ')}]-[bg, text]\``);
}
}
}
lines.push('');
}
// Human-readable category names and descriptions
const CATEGORY_INFO = {
bgColor: {
name: 'Background Colors',
description: 'Background color tokens for surfaces, containers, and UI elements.',
},
borderColor: {
name: 'Border Colors',
description: 'Border color tokens for boundaries, dividers, and outlines.',
},
fgColor: {
name: 'Foreground Colors',
description: 'Text and icon color tokens.',
},
borderRadius: { name: 'Border Radius', description: 'Corner radius tokens for rounded elements.' },
borderWidth: { name: 'Border Width', description: 'Border thickness tokens.' },
border: { name: 'Border', description: 'Composite border tokens combining color, width, and style.' },
control: { name: 'Controls', description: 'Tokens for interactive controls like buttons, inputs, and selects.' },
controlKnob: {
name: 'Control Knob',
description: 'Tokens for toggle switch knobs (the circular handle that moves along the track).',
},
controlStack: {
name: 'Control Stack',
description: 'Gap tokens for groups of controls arranged in a row or column.',
},
controlTrack: {
name: 'Control Track',
description: 'Tokens for toggle switch tracks (the background rail that the knob slides along).',
},
data: {
name: 'Data Visualization',
description: 'Color tokens for charts, graphs, and diagrams. Use emphasis variants for lines/bars, muted variants for fills.',
},
display: {
name: 'Display Colors',
description: 'Decorative colors for categorization without semantic meaning. Use for labels, tags, avatars, and user-assigned colors. Do NOT use for success/error/warning—use semantic colors instead.',
},
easing: { name: 'Easing', description: 'Animation easing function tokens.' },
focus: { name: 'Focus', description: 'Focus ring and outline tokens for keyboard navigation accessibility.' },
fontStack: { name: 'Font Stacks', description: 'Font family tokens.' },
outline: { name: 'Outline', description: 'Outline tokens for focus indicators.' },
overlay: { name: 'Overlay', description: 'Tokens for modals, dialogs, popovers, and dropdown menus.' },
selection: { name: 'Selection', description: 'Tokens for text selection highlights.' },
shadow: { name: 'Shadow', description: 'Box shadow tokens for elevation and depth.' },
spinner: { name: 'Spinner', description: 'Loading spinner size and stroke tokens.' },
stack: { name: 'Stack', description: 'Spacing tokens for Stack layout components.' },
text: { name: 'Typography', description: 'Text style shorthand tokens for consistent typography across the UI.' },
};
/**
* Densifies description by removing filler words
*/
function densifyDescription(description) {
return description
.replace(/^Use this for\s+/i, 'For ')
.replace(/^This is used for\s+/i, 'For ')
.replace(/^Used for\s+/i, 'For ')
.replace(/^Use for\s+/i, 'For ')
.replace(/^This is\s+/i, '')
.replace(/^This\s+/i, '');
}
/**
* Shortens rules by applying key shorthands
*/
function shortenRules(rules) {
return rules
.replace(/Pair with\s+/gi, 'Pair -> ')
.replace(/\bfgColor\./g, 'fg.')
.replace(/\bbgColor\./g, 'bg.')
.replace(/\bborderColor\./g, 'border.');
}
/**
* Limits usage to max 3 most relevant items
*/
function limitUsage(usage) {
if (usage.length <= 3)
return usage;
return usage.slice(0, 3);
}
/**
* Creates a unique key for grouping tokens with identical guidelines within a category
*/
function createGuidelineKey(guideline) {
var _a;
return JSON.stringify({
category: guideline.category,
description: guideline.description || '',
usage: ((_a = guideline.usage) === null || _a === void 0 ? void 0 : _a.sort()) || [],
rules: guideline.rules || '',
});
}
/**
* Extracts category from token name
*/
function extractCategory(tokenName) {
const parts = tokenName.split('-');
if (parts[0] === 'base' && parts[1]) {
return parts[1];
}
return parts[0] || 'other';
}
/**
* Extracts semantic subcategory from token name (e.g., "bgColor-danger-emphasis" -> "danger")
*/
function extractSemanticSubcategory(tokenName) {
const parts = tokenName.split('-');
if (parts.length >= 2) {
const subcat = parts[1];
if (SEMANTIC_SETS.includes(subcat)) {
return subcat;
}
}
return null;
}
/**
* Extracts variant from token name (e.g., "bgColor-danger-emphasis" -> "emphasis")
*/
function extractVariant(tokenName) {
const parts = tokenName.split('-');
if (parts.length >= 3) {
return parts.slice(2).join('-');
}
return null;
}
/**
* Formats category name for display
*/
function formatCategoryName(category) {
if (category in CATEGORY_INFO) {
return CATEGORY_INFO[category].name;
}
return category.charAt(0).toUpperCase() + category.slice(1);
}
/**
* Gets category description if available
*/
function getCategoryDescription(category) {
if (category in CATEGORY_INFO) {
return CATEGORY_INFO[category].description;
}
return null;
}
/**
* Creates a key for grouping by usage and rules only
*/
function createUsageRulesKey(guideline) {
var _a;
return JSON.stringify({
usage: ((_a = guideline.usage) === null || _a === void 0 ? void 0 : _a.sort()) || [],
rules: guideline.rules || '',
});
}
/**
* Creates a key for cross-category pattern matching
*/
function createPatternKey(description, rules) {
const normalizedDesc = description
.replace(/background/gi, 'COLOR_TYPE')
.replace(/border/gi, 'COLOR_TYPE')
.replace(/text/gi, 'COLOR_TYPE')
.replace(/foreground/gi, 'COLOR_TYPE');
const normalizedRules = rules
.replace(/bgColor/g, 'COLOR_TOKEN')
.replace(/borderColor/g, 'COLOR_TOKEN')
.replace(/fgColor/g, 'COLOR_TOKEN');
return JSON.stringify({ description: normalizedDesc, rules: normalizedRules });
}
/**
* Extracts a subcategory name from token names for headings
*/
function extractSubcategory(tokenNames) {
if (tokenNames.length < 2)
return null;
const subcategories = tokenNames.map(name => {
const parts = name.split('-');
return parts[1] || null;
});
const uniqueSubcats = [...new Set(subcategories.filter(Boolean))];
if (uniqueSubcats.length === 1) {
return uniqueSubcats[0];
}
return null;
}
/**
* Outputs typography tokens grouped by role (Headings, Body, Code)
*/
function outputTypographyByRole(tokens, lines) {
var _a;
for (const { role, description, patterns } of TYPOGRAPHY_ROLES) {
const roleTokens = tokens.filter(t => patterns.some(p => t.name.startsWith(p)));
if (roleTokens.length === 0)
continue;
lines.push(`### ${role}`);
lines.push('');
lines.push(description);
lines.push('');
// Create table for role tokens
const headers = ['Token', 'Description', 'U:', 'R:'];
lines.push(`| ${headers.join(' | ')} |`);
lines.push(`|${headers.map(() => '---').join('|')}|`);
for (const token of roleTokens.sort((a, b) => a.name.localeCompare(b.name))) {
const cells = [
`**${token.name}**`,
escapeTableCell(token.description || '-'),
((_a = token.usage) === null || _a === void 0 ? void 0 : _a.join(', ')) || '-',
escapeTableCell(token.rules || '-'),
];
lines.push(`| ${cells.join(' | ')} |`);
}
lines.push('');
}
}
/**
* Escapes pipe characters for markdown tables
*/
function escapeTableCell(text) {
// First escape backslashes, then escape pipe characters, to avoid
// existing backslashes altering how pipes are interpreted.
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
}
/**
* @description Outputs a hyper-optimized markdown file with LLM token guidelines.
* Optimizations:
* - Bracket notation for tokens with identical guidelines
* - Global category rules extracted to paragraph
* - Shortened keys (U:, R:, Pair ->)
* - Max 3 usage items
* - No boilerplate
*/
export const markdownLlmGuidelines = (_a) => __awaiter(void 0, [_a], void 0, function* ({ dictionary }) {
var _b;
const tokens = dictionary.allTokens.sort(sortByName);
const guidelines = [];
for (const token of tokens) {
const llmExt = (_b = token.$extensions) === null || _b === void 0 ? void 0 : _b['org.primer.llm'];
if (!llmExt)
continue;
const guideline = {
name: token.name,
category: extractCategory(token.name),
};
if (token.$description && typeof token.$description === 'string') {
guideline.description = densifyDescription(token.$description);
}
if (llmExt.usage && Array.isArray(llmExt.usage)) {
guideline.usage = limitUsage(llmExt.usage);
}
if (llmExt.rules && typeof llmExt.rules === 'string') {
guideline.rules = shortenRules(llmExt.rules);
}
guidelines.push(guideline);
}
// Group by category
const grouped = {};
for (const guideline of guidelines) {
if (!Object.hasOwn(grouped, guideline.category)) {
grouped[guideline.category] = [];
}
grouped[guideline.category].push(guideline);
}
const lines = [
'# Primer Design Token Guidelines',
'',
'> Metadata: This file is a Dictionary of tokens. For usage rules, contrast requirements, and motion logic, refer to DESIGN_TOKENS_GUIDE.md.',
'',
'Reference for using GitHub Primer design tokens.',
'',
'## Legend',
'',
'- **U:** Use cases',
'- **R:** Token-specific rules (see Semantic Key for general meaning)',
'- **emphasis** variant: Strong/prominent version, use `fg.onEmphasis` for text',
'- **muted** variant: Subtle version, use matching `fg.*` color for text',
'- **[a, b]** Bracket notation groups related tokens',
'',
'## Semantic Key',
'',
'These semantic meanings apply across all token types (bgColor, borderColor, fgColor, border).',
'',
'| Semantic | Meaning | Example Usage | Text Pairing |',
'|---|---|---|---|',
];
// Output semantic key table
for (const [key, info] of Object.entries(SEMANTIC_KEY)) {
lines.push(`| **${key}** | ${info.meaning} | ${info.usage} | ${info.textPairing} |`);
}
lines.push('');
// Collect semantic tokens across mergeable categories for cross-category patterns
const semanticTokensByPattern = new Map();
for (const category of MERGEABLE_CATEGORIES) {
if (!(category in grouped))
continue;
for (const guideline of grouped[category]) {
const subcategory = extractSemanticSubcategory(guideline.name);
const variant = extractVariant(guideline.name);
if (subcategory && variant) {
const patternKey = createPatternKey(guideline.description || '', guideline.rules || '');
const key = `${subcategory}-${variant}-${patternKey}`;
if (!semanticTokensByPattern.has(key)) {
semanticTokensByPattern.set(key, []);
}
semanticTokensByPattern.get(key).push({ subcategory, variant, categories: [category], guideline });
}
}
}
// Find patterns spanning multiple categories (must have entries from at least 2 different categories)
const mergedEntries = [];
const mergedTokens = new Set();
for (const [, entries] of semanticTokensByPattern) {
// Get unique categories in this pattern
const uniqueCategories = new Set(entries.map(e => e.categories[0]));
if (uniqueCategories.size > 1) {
// Only merge if pattern spans multiple categories
mergedEntries.push({
subcategory: entries[0].subcategory,
variant: entries[0].variant,
guideline: entries[0].guideline,
});
for (const e of entries) {
mergedTokens.add(`${e.categories[0]}-${e.subcategory}-${e.variant}`);
}
}
}
// Output compact semantic color reference
// Group by variant (emphasis/muted) and list all applicable semantics
if (mergedEntries.length > 0) {
lines.push('## Semantic Colors');
lines.push('');
lines.push('Apply to `bgColor-*`, `borderColor-*`, `fgColor-*`, and `border-*` tokens.');
lines.push('Refer to Semantic Key above for meaning and text pairings.');
lines.push('');
// Group entries by variant
const byVariant = new Map();
for (const entry of mergedEntries) {
if (!byVariant.has(entry.variant)) {
byVariant.set(entry.variant, []);
}
byVariant.get(entry.variant).push(entry.subcategory);
}
// Output as compact list
lines.push('| Pattern | Semantics |');
lines.push('|---|---|');
for (const [variant, semantics] of byVariant) {
const sortedSemantics = [...new Set(semantics)].sort();
lines.push(`| **\\*-[${sortedSemantics.join(', ')}]-${variant}** | See Semantic Key |`);
}
lines.push('');
// Add special notes for tokens with unique constraints
lines.push('**Special cases:**');
lines.push('- `*-attention-emphasis`: Use `fg.default` for text (yellow has poor contrast)');
lines.push('- `*-sponsors-*`: GitHub Sponsors only, not for general pink UI');
lines.push('- `*-upsell-*`: Promotional content only, not for regular features');
lines.push('- `*-open/*-closed/*-done`: GitHub issue/PR states specifically');
lines.push('');
}
for (const category of Object.keys(grouped).sort()) {
const categoryGuidelines = grouped[category];
// Separate semantic and non-semantic tokens
const semanticTokens = [];
const nonSemanticTokens = [];
for (const guideline of categoryGuidelines) {
if (mergedTokens.has(guideline.name))
continue;
const subcategory = extractSemanticSubcategory(guideline.name);
if (subcategory) {
semanticTokens.push(guideline);
}
else {
nonSemanticTokens.push(guideline);
}
}
if (semanticTokens.length === 0 && nonSemanticTokens.length === 0)
continue;
lines.push(`## ${formatCategoryName(category)}`);
// Special handling for typography - group by role
if (category === 'text') {
const categoryDesc = getCategoryDescription(category);
if (categoryDesc) {
lines.push('');
lines.push(categoryDesc);
}
lines.push('');
outputTypographyByRole(nonSemanticTokens, lines);
continue;
}
// Special handling for data visualization colors - use pattern compression
if (category === 'data') {
const categoryDesc = getCategoryDescription(category);
if (categoryDesc) {
lines.push('');
lines.push(categoryDesc);
}
lines.push('');
// Get usage and rules from the first token with LLM metadata
const firstWithMeta = nonSemanticTokens.find(t => t.usage || t.rules);
if (firstWithMeta) {
if (firstWithMeta.usage && firstWithMeta.usage.length > 0) {
lines.push(`**U:** ${firstWithMeta.usage.join(', ')}`);
}
if (firstWithMeta.rules) {
lines.push(`**R:** ${firstWithMeta.rules}`);
}
lines.push('');
}
outputDataVisColorPattern(nonSemanticTokens, lines);
continue;
}
// Special handling for display colors - use pattern compression
if (category === 'display') {
const categoryDesc = getCategoryDescription(category);
if (categoryDesc) {
lines.push('');
lines.push(categoryDesc);
}
lines.push('');
// Get usage and rules from the first token with LLM metadata
const firstWithMeta = nonSemanticTokens.find(t => t.usage || t.rules);
if (firstWithMeta) {
if (firstWithMeta.usage && firstWithMeta.usage.length > 0) {
lines.push(`**U:** ${firstWithMeta.usage.join(', ')}`);
}
if (firstWithMeta.rules) {
lines.push(`**R:** ${firstWithMeta.rules}`);
}
lines.push('');
}
outputDisplayColorPattern(nonSemanticTokens, lines);
continue;
}
// Special handling for color category (ansi, prettylights subcategories)
if (category === 'color') {
// Separate tokens by subcategory
const ansiTokens = nonSemanticTokens.filter(t => t.name.startsWith('color-ansi-'));
const prettylightsTokens = nonSemanticTokens.filter(t => t.name.startsWith('color-prettylights-'));
const otherTokens = nonSemanticTokens.filter(t => !t.name.startsWith('color-ansi-') && !t.name.startsWith('color-prettylights-'));
// Output ANSI tokens with pattern compression
if (ansiTokens.length > 0) {
lines.push('### ANSI Terminal Colors');
lines.push('');
const ansiMeta = ansiTokens.find(t => t.description);
if (ansiMeta === null || ansiMeta === void 0 ? void 0 : ansiMeta.description) {
lines.push(ansiMeta.description);
lines.push('');
}
const ansiWithMeta = ansiTokens.find(t => t.usage || t.rules);
if (ansiWithMeta) {
if (ansiWithMeta.usage && ansiWithMeta.usage.length > 0) {
lines.push(`**U:** ${ansiWithMeta.usage.join(', ')}`);
}
if (ansiWithMeta.rules) {
lines.push(`**R:** ${ansiWithMeta.rules}`);
}
lines.push('');
}
outputAnsiColorPattern(ansiTokens, lines);
}
// Output prettylights tokens with prefix grouping
if (prettylightsTokens.length > 0) {
lines.push('### Syntax Highlighting (prettylights)');
lines.push('');
const plMeta = prettylightsTokens.find(t => t.description);
if (plMeta === null || plMeta === void 0 ? void 0 : plMeta.description) {
lines.push(plMeta.description);
lines.push('');
}
const plWithMeta = prettylightsTokens.find(t => t.usage || t.rules);
if (plWithMeta) {
if (plWithMeta.usage && plWithMeta.usage.length > 0) {
lines.push(`**U:** ${plWithMeta.usage.join(', ')}`);
}
if (plWithMeta.rules) {
lines.push(`**R:** ${plWithMeta.rules}`);
}
lines.push('');
}
outputPrettylightsPattern(prettylightsTokens, lines);
}
// Output any other color tokens normally
if (otherTokens.length > 0) {
for (const token of otherTokens) {
lines.push(`### ${token.name}`);
if (token.description) {
lines.push(token.description);
}
if (token.usage && token.usage.length > 0) {
lines.push(`**U:** ${token.usage.join(', ')}`);
}
if (token.rules) {
lines.push(`**R:** ${token.rules}`);
}
lines.push('');
}
}
continue;
}
// Special handling for pattern-compressed categories (control, overlay, stack, spinner)
if (category in PATTERN_COMPRESSED_CATEGORIES) {
const categoryDesc = getCategoryDescription(category);
if (categoryDesc) {
lines.push('');
lines.push(categoryDesc);
}
lines.push('');
// Include semantic tokens in the output for pattern compression
const allTokens = [...semanticTokens, ...nonSemanticTokens];
outputPatternCompressedCategory(category, allTokens, PATTERN_COMPRESSED_CATEGORIES[category], lines);
continue;
}
// Determine best category description: prefer token description for single-group categories
const consolidatedGroupsPreview = new Map();
for (const guideline of nonSemanticTokens) {
const key = createGuidelineKey(guideline);
if (!consolidatedGroupsPreview.has(key)) {
consolidatedGroupsPreview.set(key, []);
}
consolidatedGroupsPreview.get(key).push(guideline);
}
// Use token description if there's only one group with multiple tokens that has a description
const singleGroupWithDesc = consolidatedGroupsPreview.size === 1 &&
nonSemanticTokens.length > 1 &&
nonSemanticTokens[0].description &&
semanticTokens.length === 0;
const categoryDesc = singleGroupWithDesc ? nonSemanticTokens[0].description : getCategoryDescription(category);
if (categoryDesc) {
lines.push('');
lines.push(categoryDesc);
}
lines.push('');
// Output semantic tokens as compact reference (details in Semantic Key)
// Track if we've output shared usage/rules to avoid duplication
let outputSharedUsage = false;
let outputSharedRules = false;
if (semanticTokens.length > 0) {
// Group semantic tokens by variant for compact display
const byVariant = new Map();
const noVariantTokens = []; // For single-level semantic tokens like fgColor-danger
for (const token of semanticTokens) {
const subcategory = extractSemanticSubcategory(token.name);
const variant = extractVariant(token.name);
if (subcategory) {
if (variant) {
if (!byVariant.has(variant)) {
byVariant.set(variant, []);
}
byVariant.get(variant).push(subcategory);
}
else {
noVariantTokens.push(subcategory);
}
}
}
// Output compact semantic reference
if (byVariant.size > 0 || noVariantTokens.length > 0) {
lines.push('**Semantic tokens** (see Semantic Key for meaning):');
// Tokens with variants (bgColor-danger-emphasis, etc.)
for (const [variant, semantics] of byVariant) {
const uniqueSemantics = [...new Set(semantics)].sort();
lines.push(`- \`${category}-[${uniqueSemantics.join(', ')}]-${variant}\``);
}
// Single-level semantic tokens (fgColor-danger, etc.)
if (noVariantTokens.length > 0) {
const uniqueSemantics = [...new Set(noVariantTokens)].sort();
lines.push(`- \`${category}-[${uniqueSemantics.join(', ')}]\``);
}
lines.push('');
outputSharedUsage = true;
outputSharedRules = true;
}
}
// Check shared usage/rules for non-semantic tokens
const usageRulesKeys = new Set(nonSemanticTokens.map(createUsageRulesKey));
const sharedUsageRules = usageRulesKeys.size === 1 && nonSemanticTokens.length > 1;
// Only output if not already output for semantic tokens and content is different
if (sharedUsageRules && nonSemanticTokens.length > 0) {
const first = nonSemanticTokens[0];
const shouldOutputUsage = first.usage && first.usage.length > 0 && !outputSharedUsage;
const shouldOutputRules = first.rules && !outputSharedRules;
if (shouldOutputUsage || shouldOutputRules) {
if (shouldOutputUsage) {
lines.push(`**U:** ${first.usage.join(', ')}`);
}
if (shouldOutputRules) {
lines.push(`**R:** ${first.rules}`);
}
lines.push('');
}
}
// Group non-semantic tokens with identical guidelines (reuse preview)
const consolidatedGroups = consolidatedGroupsPreview;
for (const [, guidelinesGroup] of consolidatedGroups) {
const first = guidelinesGroup[0];
const tokenNames = guidelinesGroup.map(g => g.name);
if (guidelinesGroup.length > 1) {
const subcategory = extractSubcategory(tokenNames);
// Only add heading if there's a meaningful subcategory and multiple groups
if (subcategory && consolidatedGroups.size > 1) {
lines.push(`### ${subcategory}`);
}
// Only output description if no category description was already shown
if (first.description && !categoryDesc) {
lines.push(first.description);
}
if (!sharedUsageRules) {
if (first.usage && first.usage.length > 0) {
lines.push(`**U:** ${first.usage.join(', ')}`);
}
if (first.rules) {
lines.push(`**R:** ${first.rules}`);
}
}
lines.push(`**Tokens:** ${tokenNames.join(', ')}`);
lines.push('');
}
else {
lines.push(`### ${first.name}`);
if (first.description) {
lines.push(first.description);
}
if (!sharedUsageRules) {
if (first.usage && first.usage.length > 0) {
lines.push(`**U:** ${first.usage.join(', ')}`);
}
if (first.rules) {
lines.push(`**R:** ${first.rules}`);
}
}
lines.push('');
}
}
}
// Add final directive for AI
lines.push('---');
lines.push('');
lines.push('**Final Directive for AI**:');
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]-[