import { fileHeader } from 'style-dictionary/utils'; import { type FormatFn, type DesignToken } from 'style-dictionary/types'; import { tailwindTokenFilesMapper } from './tailwindTokenFilesMapper.js'; const PREFIX = 'mfui'; /** * Maps Tailwind v3 theme properties to Tailwind v4 CSS variable namespaces * @see https://tailwindcss.com/docs/theme - Tailwind v4 theme variable namespaces */ const NAMESPACE_MAP: Record = { colors: 'color', fontFamily: 'font', fontSize: 'text', fontWeight: 'font-weight', lineHeight: 'leading', spacing: 'spacing', borderRadius: 'radius', borderWidth: 'border', boxShadow: 'shadow', transitionDuration: 'duration', transitionTimingFunction: 'ease', }; /** * Creates a standard CSS variable string */ const createCSSVariable = (namespace: string, tokenName: string, value: string | number): string => { return ` --${namespace}-${tokenName}: ${value};`; }; /** * Creates an easing CSS variable from an array of cubic-bezier values */ const createEasingCSSVariable = (namespace: string, tokenName: string, easingValue: any): string => { // Handle array format: [0.5, 0, 0.25, 1] if (Array.isArray(easingValue)) { const cssValue = `cubic-bezier(${easingValue.join(', ')})`; return ` --${namespace}-${tokenName}: ${cssValue};`; } // Handle string format (already formatted) return ` --${namespace}-${tokenName}: ${easingValue};`; }; /** * Creates a font-family CSS variable from an array of font names */ const createFontFamilyCSSVariable = (namespace: string, tokenName: string, fontValue: any): string => { // Handle array format: ["Meiryo", "Hiragino Sans", "sans-serif"] if (Array.isArray(fontValue)) { const cssValue = fontValue.join(', '); return ` --${namespace}-${tokenName}: ${cssValue};`; } // Handle string format (already formatted) return ` --${namespace}-${tokenName}: ${fontValue};`; }; /** * Groups shadow tokens by their parent name (removing -shadow-N suffix) * This allows combining multi-layer shadows like elevation-plus-1-shadow-1 and elevation-plus-1-shadow-2 */ const groupShadowTokens = (tokens: DesignToken[]): Map => { const shadowGroups = new Map(); tokens.forEach((token) => { // Skip tokens without names if (!token.name) { return; } const tokenName = token.name; // Check if token name ends with -shadow-N pattern const shadowMatch = tokenName.match(/^(.+)-shadow-\d+$/); if (shadowMatch && shadowMatch[1]) { const parentName = shadowMatch[1]; if (!shadowGroups.has(parentName)) { shadowGroups.set(parentName, []); } const group = shadowGroups.get(parentName); if (group) { group.push(token); } } else { // Not a multi-part shadow, treat as single shadow shadowGroups.set(tokenName, [token]); } }); return shadowGroups; }; /** * Tailwind CSS v4 Theme Format * * Generates a CSS file with @theme directive containing CSS variables * that map to Tailwind v4 utility classes. * * @see https://tailwindcss.com/docs/theme - Tailwind v4 theme configuration */ export const tailwindV4ThemeFormat: FormatFn = async ({ dictionary, file }) => { const cssVariables: string[] = []; // Add documentation comment cssVariables.push('/**'); cssVariables.push(' * Tailwind CSS v4 Theme Configuration'); cssVariables.push(' * '); cssVariables.push(' * This file defines MFUI design tokens as CSS variables using the @theme directive.'); cssVariables.push(' * Import this file before importing tailwindcss to use MFUI tokens.'); cssVariables.push(' * '); cssVariables.push(' * Usage:'); cssVariables.push(' * @import "@moneyforward/mfui-design-tokens/css/tailwind-v4.css";'); cssVariables.push(' * @import "tailwindcss";'); cssVariables.push(' * '); cssVariables.push(' * Available utility classes:'); cssVariables.push(' * - Colors: bg-mfui-*, text-mfui-*, border-mfui-*'); cssVariables.push(' * - Spacing: p-mfui-*, m-mfui-*, gap-mfui-*, w-mfui-*, h-mfui-*'); cssVariables.push(' * - Typography: font-mfui-*, text-mfui-*, leading-mfui-*'); cssVariables.push(' * - Shadows: shadow-mfui-*'); cssVariables.push(' * - Radius: rounded-mfui-*'); cssVariables.push(' */'); cssVariables.push(''); // Process each theme property from the mapper tailwindTokenFilesMapper.forEach(({ themeProperty, fileMatchPattern }) => { const namespace = NAMESPACE_MAP[themeProperty]; if (!namespace) { console.warn(`No namespace mapping for theme property: ${themeProperty}`); return; } // Filter tokens by file pattern const tokens = dictionary.allTokens.filter((token) => token.filePath.match(fileMatchPattern)); if (tokens.length === 0) { return; } // Add section comment cssVariables.push(` /* ${themeProperty} */`); // Special handling for different token types if (themeProperty === 'boxShadow') { // Group shadow tokens to handle multi-layer shadows const shadowGroups = groupShadowTokens(tokens); shadowGroups.forEach((shadowTokens, parentName) => { const tokenName = `${PREFIX}-${parentName}`; // Shadow values are already transformed to CSS strings by Style Dictionary // e.g., "0px 1px 1px 1px #00000033" // We just need to combine multiple shadows with commas const shadowValues = shadowTokens .map((token) => String(token.$value)) .filter((val) => val && val !== 'undefined'); if (shadowValues.length > 0) { const combinedValue = shadowValues.join(', '); cssVariables.push(` --${namespace}-${tokenName}: ${combinedValue};`); } }); } else if (themeProperty === 'transitionTimingFunction') { // Easing functions tokens.forEach((token) => { if (!token.name) return; const tokenName = `${PREFIX}-${token.name}`; const cssVar = createEasingCSSVariable(namespace, tokenName, token.$value); cssVariables.push(cssVar); }); } else if (themeProperty === 'fontFamily') { // Font families tokens.forEach((token) => { if (!token.name) return; const tokenName = `${PREFIX}-${token.name}`; const cssVar = createFontFamilyCSSVariable(namespace, tokenName, token.$value); cssVariables.push(cssVar); }); } else { // Standard tokens (colors, spacing, fontSize, etc.) tokens.forEach((token) => { if (!token.name) return; const tokenName = `${PREFIX}-${token.name}`; const cssVar = createCSSVariable(namespace, tokenName, String(token.$value)); cssVariables.push(cssVar); }); } cssVariables.push(''); }); // Build final output const output = [await fileHeader({ file }), '', '@theme {', ...cssVariables, '}', ''].join('\n'); return output; };