/** * Style Dictionary configuration for FA Design System (v4) * * Transforms W3C DTCG token JSON into: * - CSS custom properties (for runtime theming) * - JavaScript ES6 module (for MUI theme consumption) * - Tailwind token module (for tailwind.config.js consumption) * - JSON (for Penpot import) */ import StyleDictionary from 'style-dictionary'; import { readFileSync, writeFileSync } from 'fs'; const sd = new StyleDictionary({ source: ['tokens/**/*.json'], usesDtcg: true, platforms: { css: { transformGroup: 'css', prefix: 'fa', buildPath: 'src/theme/generated/', files: [{ destination: 'tokens.css', format: 'css/variables', options: { outputReferences: true, }, }], }, js: { transformGroup: 'js', buildPath: 'src/theme/generated/', files: [{ destination: 'tokens.js', format: 'javascript/es6', }], }, json: { transformGroup: 'js', buildPath: 'tokens/export/', files: [{ destination: 'tokens-flat.json', format: 'json/flat', }], }, }, }); await sd.buildAllPlatforms(); // Generate TypeScript declarations for the JS token output const jsPath = 'src/theme/generated/tokens.js'; const dtsPath = 'src/theme/generated/tokens.d.ts'; const jsContent = readFileSync(jsPath, 'utf-8') .replace(/=\n\s+/g, '= '); const declarations = ['/**', ' * Do not edit directly, this file was auto-generated.', ' */']; for (const line of jsContent.split('\n')) { const m = line.match(/^export const (\w+)\s*=\s*(.+?)\s*;/); if (!m) continue; const [, name, val] = m; const isNum = !val.startsWith('"') && !val.startsWith("'") && !isNaN(Number(val)); declarations.push(`export declare const ${name}: ${isNum ? 'number' : 'string'};`); } writeFileSync(dtsPath, declarations.join('\n') + '\n'); console.log(`✓ Generated ${declarations.length - 3} TypeScript declarations`); // Generate Tailwind token module from the flat JSON export // Keys are PascalCase: ColorBrand500, Spacing4, FontFamilyBody, etc. const flatTokens = JSON.parse(readFileSync('tokens/export/tokens-flat.json', 'utf-8')); const twColors = {}; const twSpacing = {}; const twBorderRadius = {}; const twFontFamily = {}; const twFontSize = {}; const twFontWeight = {}; const twLineHeight = {}; const twLetterSpacing = {}; // Helper: split PascalCase into parts, e.g. "ColorBrand500" → ["Color", "Brand", "500"] function splitPascal(str) { return str.match(/[A-Z][a-z]*|\d+/g) || []; } for (const [key, value] of Object.entries(flatTokens)) { // Primitive colour tokens: ColorBrand500 → brand.500 const colorMatch = key.match(/^Color(Brand|Sage|Neutral|Red|Amber|Green|Blue)(\d+)$/); if (colorMatch) { const group = colorMatch[1].toLowerCase(); const shade = colorMatch[2]; if (!twColors[group]) twColors[group] = {}; twColors[group][shade] = value; continue; } // Single-value colors: ColorWhite, ColorBlack if (key === 'ColorWhite') { twColors['white'] = value; continue; } if (key === 'ColorBlack') { twColors['black'] = value; continue; } // Primitive spacing: Spacing1, Spacing0-5, Spacing16, etc. const spacingMatch = key.match(/^Spacing(\d+)$/); if (spacingMatch) { twSpacing[spacingMatch[1]] = value; continue; } if (key === 'Spacing05') { twSpacing['0.5'] = value; continue; } // Primitive border radius: BorderRadiusSm, BorderRadiusMd, etc. const radiusMatch = key.match(/^BorderRadius(\w+)$/); if (radiusMatch) { twBorderRadius[radiusMatch[1].toLowerCase()] = value; continue; } // Font family: FontFamilyBody, FontFamilyDisplay, FontFamilyMono const fontFamilyMatch = key.match(/^FontFamily(\w+)$/); if (fontFamilyMatch) { twFontFamily[fontFamilyMatch[1].toLowerCase()] = value; continue; } // Primitive font size: FontSize2xs, FontSizeBase, etc. (skip mobile/display) const fontSizeMatch = key.match(/^FontSize(2xs|Xs|Sm|Base|Md|Lg|Xl|2xl|3xl|4xl)$/i); if (fontSizeMatch) { twFontSize[fontSizeMatch[1].toLowerCase()] = value; continue; } // Font weight: FontWeightRegular, FontWeightMedium, etc. const fontWeightMatch = key.match(/^FontWeight(\w+)$/); if (fontWeightMatch) { twFontWeight[fontWeightMatch[1].toLowerCase()] = value; continue; } // Line height: LineHeightTight, LineHeightNormal, etc. const lineHeightMatch = key.match(/^LineHeight(\w+)$/); if (lineHeightMatch) { twLineHeight[lineHeightMatch[1].toLowerCase()] = value; continue; } // Letter spacing: LetterSpacingTight, LetterSpacingNormal, etc. const letterSpacingMatch = key.match(/^LetterSpacing(\w+)$/); if (letterSpacingMatch) { twLetterSpacing[letterSpacingMatch[1].toLowerCase()] = value; continue; } } const tailwindTokens = { colors: twColors, spacing: twSpacing, borderRadius: twBorderRadius, fontFamily: twFontFamily, fontSize: twFontSize, fontWeight: twFontWeight, lineHeight: twLineHeight, letterSpacing: twLetterSpacing, }; const twOutput = `/**\n * Auto-generated Tailwind tokens from Style Dictionary.\n * Do not edit directly — edit tokens/*.json and run npm run build:tokens.\n */\nmodule.exports = ${JSON.stringify(tailwindTokens, null, 2)};\n`; writeFileSync('src/theme/generated/tailwind-tokens.js', twOutput); console.log(`✓ Generated Tailwind token module`);