import { type SemanticTokens, type Tokens } from '@pandacss/dev'; import { fileHeader } from 'style-dictionary/utils'; import { type FormatFn, type Dictionary } from 'style-dictionary/types'; import { tokenFilesMapper } from './tokenFilesMapper.js'; type PandaCSSThemeObject = { [key: string]: { value: string; }; }; // Remove 'px' and convert to number const convertPxStringToNumber = (value: string) => Number(value.replace('px', '')); /** * Gets the token path for Panda CSS, removing 'base' suffix while preserving 'narrow'. * This ensures that base tokens represent the default responsive behavior without the suffix. */ const getPandaTokenPath = (token: { path: string[]; name?: string }): string => { const lastSegment = token.path[token.path.length - 1]; if (lastSegment === 'base') { // Remove 'base' from the path const pathWithoutBase = token.path.slice(0, -1); return 'mfui.' + pathWithoutBase.join('.'); } // For 'narrow' and other cases, use the full path return 'mfui.' + token.path.join('.'); }; /** * Creates a shadow token object from the dictionary for the Panda CSS theme. * * The shadow token is a dictionary with the following structure: * * ``` * { * "color": "#00000040", * "offsetX": "0px", * "offsetY": "4px", * "blur": "8px", * "spread": "0px" * } * ``` * * Panda CSS expects the shadow token to be a dictionary with the following structure: * * ``` * { * color: "#00000040" * offsetX: 0, * offsetY: 4, * blur: 8, * spread: 0, * } * ``` * * @see https://panda-css.com/docs/theming/tokens#shadows - Panda CSS documentation for shadows token. * * @param dictionary - The dictionary containing all the tokens. * @param fileMatchPattern - The file match pattern to filter the tokens. * @returns The PandaCSS theme object with the shadow tokens. */ const createShadowTokenObject = (dictionary: Dictionary, fileMatchPattern: string | RegExp) => Object.assign( {}, ...dictionary.allTokens .filter((token) => token.filePath.match(fileMatchPattern)) .map((token) => ({ [getPandaTokenPath(token)]: { value: { offsetX: convertPxStringToNumber(token.$value.offsetX), offsetY: convertPxStringToNumber(token.$value.offsetY), blur: convertPxStringToNumber(token.$value.blur), spread: convertPxStringToNumber(token.$value.spread), color: token.$value.color, }, }, })), ) as PandaCSSThemeObject; /** * Creates a token object from the dictionary for the Panda CSS theme excluding shadow tokens. * * @param dictionary - The dictionary containing all the tokens. * @param fileMatchPattern - The file match pattern to filter the tokens. * @returns The PandaCSS theme object with the token. */ const createTokenObject = (dictionary: Dictionary, fileMatchPattern: string | RegExp) => Object.assign( {}, ...dictionary.allTokens .filter((token) => token.filePath.match(fileMatchPattern)) .map((token) => ({ [getPandaTokenPath(token)]: { value: String(token.$value), }, })), ) as PandaCSSThemeObject; export const pandaCssConfigThemeFormat: FormatFn = async ({ dictionary, file }) => { const tokens: Tokens = {}; const semanticTokens: SemanticTokens = {}; tokenFilesMapper.forEach(({ themeType, tokensType, fileMatchPattern }) => { // Some tokens are not compatible with the Panda CSS theme structure. // So we need to create a different object for each token type. const tokenCreator = (() => { if (tokensType === 'shadows') { return createShadowTokenObject; } return createTokenObject; })(); const target = themeType === 'token' ? tokens : semanticTokens; const newObject = { ...target[tokensType], ...tokenCreator(dictionary, fileMatchPattern), }; target[tokensType] = newObject; }); const theme = { name: 'mfui-design-tokens', theme: { extend: { tokens, semanticTokens, }, }, }; return ( (await fileHeader({ file })) + "import { definePreset } from '@pandacss/dev';\n" + `export const mfuiPandaCSSPreset = definePreset(${JSON.stringify(theme, null, 2)})` ); };