/** * DTCG Output Generators — generate CSS, SCSS, Tailwind, and Figma output * from a DTCG token source file. * * Makes DTCG the single source of truth with everything else as derived output. */ import type { DTCGTokenFile } from './dtcg.js'; import { parseColor, parseDtcgTokens } from './tokens/index.js'; // --------------------------------------------------------------------------- // Internal: parse a DTCG file to flat tokens // --------------------------------------------------------------------------- function parseToFlatTokens( tokens: DTCGTokenFile, prefix?: string, ): Array<{ cssName: string; cssValue: string; category: string; description?: string }> { // Use parseDTCGFile to get consistent output const parsed = parseDtcgTokens(JSON.stringify(tokens), 'tokens.tokens.json'); const effectivePrefix = prefix ? `--${prefix.replace(/^--/, '').replace(/-$/, '')}-` : parsed.prefix; const result: Array<{ cssName: string; cssValue: string; category: string; description?: string }> = []; for (const [category, categoryTokens] of Object.entries(parsed.categories)) { for (const token of categoryTokens) { // Re-prefix if a custom prefix was requested let cssName = token.name; if (prefix && token.name.startsWith(parsed.prefix)) { cssName = effectivePrefix + token.name.slice(parsed.prefix.length); } result.push({ cssName, cssValue: token.value ?? '', category, description: token.description, }); } } return result; } // --------------------------------------------------------------------------- // CSS Custom Properties Generator // --------------------------------------------------------------------------- export interface CSSGeneratorOptions { /** Token name prefix (e.g., 'ds' → '--ds-*') */ prefix?: string; /** CSS selector to wrap variables in (default: ':root') */ selector?: string; } /** * Generate CSS custom properties from a DTCG token file. */ export function generateCSSCustomProperties( tokens: DTCGTokenFile, options?: CSSGeneratorOptions, ): string { const selector = options?.selector ?? ':root'; const flatTokens = parseToFlatTokens(tokens, options?.prefix); const lines: string[] = []; lines.push(`${selector} {`); // Group by category for readability const grouped = new Map(); for (const token of flatTokens) { const group = grouped.get(token.category) ?? []; group.push(token); grouped.set(token.category, group); } for (const [category, categoryTokens] of grouped) { lines.push(` /* ${category} */`); for (const token of categoryTokens) { if (token.description) { lines.push(` /* ${token.description} */`); } lines.push(` ${token.cssName}: ${token.cssValue};`); } lines.push(''); } lines.push('}'); return lines.join('\n'); } // --------------------------------------------------------------------------- // SCSS Variables Generator // --------------------------------------------------------------------------- export interface SCSSGeneratorOptions { /** Token name prefix (e.g., 'ds' → '$ds-*') */ prefix?: string; } /** * Generate SCSS variables from a DTCG token file. */ export function generateSCSSVariables( tokens: DTCGTokenFile, options?: SCSSGeneratorOptions, ): string { const flatTokens = parseToFlatTokens(tokens, options?.prefix); const lines: string[] = []; lines.push('// Auto-generated from DTCG token file'); lines.push('// Do not edit directly — modify the .tokens.json source'); lines.push(''); // Group by category for readability const grouped = new Map(); for (const token of flatTokens) { const group = grouped.get(token.category) ?? []; group.push(token); grouped.set(token.category, group); } for (const [category, categoryTokens] of grouped) { lines.push(`// ${category}`); for (const token of categoryTokens) { // Convert CSS variable name to SCSS variable name const scssName = token.cssName.replace(/^--/, '$'); lines.push(`${scssName}: ${token.cssValue} !default;`); } lines.push(''); } return lines.join('\n'); } // --------------------------------------------------------------------------- // Tailwind Config Generator // --------------------------------------------------------------------------- /** * Generate a Tailwind CSS configuration object from a DTCG token file. * Compatible with Tailwind v3 and v4. */ export function generateTailwindConfig( tokens: DTCGTokenFile, ): Record { const flatTokens = parseToFlatTokens(tokens); const theme: Record> = {}; for (const token of flatTokens) { const tailwindKey = categoryToTailwindKey(token.category); if (!tailwindKey) continue; if (!theme[tailwindKey]) { theme[tailwindKey] = {}; } // Convert CSS name to Tailwind token key // e.g., "--ds-color-brand-primary" → "brand-primary" const parts = token.cssName.replace(/^--[\w]+-/, '').split('-'); // Remove the category prefix if it matches const tokenKey = parts.join('-') || token.cssName; theme[tailwindKey][tokenKey] = `var(${token.cssName})`; } return { theme: { extend: theme, }, }; } function categoryToTailwindKey(category: string): string | undefined { switch (category) { case 'colors': case 'surfaces': case 'text': return 'colors'; case 'spacing': return 'spacing'; case 'radius': return 'borderRadius'; case 'typography': return 'fontFamily'; case 'shadows': return 'boxShadow'; case 'borders': return 'borderWidth'; case 'transitions': return 'transitionDuration'; case 'z-index': return 'zIndex'; default: return undefined; } } // --------------------------------------------------------------------------- // Figma Variables Generator // --------------------------------------------------------------------------- export interface FigmaVariable { name: string; type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN'; value: unknown; description?: string; scopes?: string[]; } export interface FigmaVariableCollection { name: string; modes: Array<{ name: string; modeId: string }>; variables: FigmaVariable[]; } /** * Generate Figma Variables REST API-compatible payload from a DTCG token file. */ export function generateFigmaVariables( tokens: DTCGTokenFile, ): FigmaVariableCollection[] { const flatTokens = parseToFlatTokens(tokens); // Group by category into collections const collections = new Map(); for (const token of flatTokens) { const collectionName = categoryToFigmaCollection(token.category); const vars = collections.get(collectionName) ?? []; vars.push({ name: token.cssName.replace(/^--/, '').replace(/-/g, '/'), type: categoryToFigmaType(token.category), value: parseFigmaValue(token.cssValue, token.category), description: token.description, scopes: categoryToFigmaScopes(token.category), }); collections.set(collectionName, vars); } return Array.from(collections.entries()).map(([name, variables]) => ({ name, modes: [{ name: 'Default', modeId: 'default' }], variables, })); } function categoryToFigmaCollection(category: string): string { switch (category) { case 'colors': case 'surfaces': case 'text': return 'Colors'; case 'spacing': case 'radius': return 'Dimensions'; case 'typography': return 'Typography'; case 'shadows': return 'Effects'; default: return 'Other'; } } function categoryToFigmaType(category: string): 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN' { switch (category) { case 'colors': case 'surfaces': case 'text': return 'COLOR'; case 'spacing': case 'radius': case 'z-index': return 'FLOAT'; default: return 'STRING'; } } function categoryToFigmaScopes(category: string): string[] { switch (category) { case 'colors': return ['ALL_FILLS', 'STROKE_COLOR']; case 'surfaces': return ['FRAME_FILL', 'SHAPE_FILL']; case 'text': return ['TEXT_FILL']; case 'spacing': return ['GAP', 'WIDTH_HEIGHT']; case 'radius': return ['CORNER_RADIUS']; default: return ['ALL_SCOPES']; } } function parseFigmaValue(cssValue: string, category: string): unknown { // For colors, try to parse hex to Figma RGBA object if (category === 'colors' || category === 'surfaces' || category === 'text') { const color = parseColor(cssValue); if (color) { return { r: color.r / 255, g: color.g / 255, b: color.b / 255, a: color.a ?? 1, }; } } // For dimensions, extract numeric value if (category === 'spacing' || category === 'radius') { const numMatch = cssValue.match(/^(\d+(?:\.\d+)?)/); if (numMatch) { return parseFloat(numMatch[1]); } } return cssValue; }