/* eslint-disable @typescript-eslint/no-var-requires */ // For generating Design Tokens, we use Style Dictionary for several reasons: // - It's prepared to easily generate tokens for multiple types of outputs (CSS, SCSS, iOS, Android, documentation, etc.). // - It also works very well out of the box with any kind of Design Tokens formats, like Figma Tokens, as well as APIs to adjust to more custom ones. // - It is probably the most well-known and widely used Design Tokens tool. It has also been regularly maintained for a long time. // - It can easily scale to different necessities we might have in the future. const fs = require('fs'); const path = require('path'); const StyleDictionary = require('style-dictionary'); const targetPath = './src/foundations/'; const { variablesPrefix, getRgbaValue, hexToRgb, generateShadowValue, generateFontSizeValue, generateFontFamilyValue, generateTypographyValue, generateRgbValue, generateColorUtilityClasses, generateFontUtilityClass, generateSpaceUtilityClasses, generateTypographyUtilityClass, } = require('./utilities'); const { fileHeader } = StyleDictionary.formatHelpers; // CSS vanilla :root format StyleDictionary.registerFormat({ name: 'rootFormat', formatter({ dictionary, file }) { /* * This will loop through all tokens and based on the type it will * call a utility function that will return the expected format for the CSS Variable */ const prefixedVariables = dictionary.allProperties .filter((prop) => prop['$type'] !== 'typography') .map((prop) => { if (prop.attributes.category.startsWith('Elevation')) { const cssShadow = prop.value.map(generateShadowValue).join(', '); return `--${variablesPrefix}-${prop.name}: ${cssShadow};`; } else if (prop.attributes.category.match('font-family')) { return generateFontFamilyValue(prop); } else if (prop.attributes.category.match('font-size')) { return generateFontSizeValue(prop); } else { const rgb = hexToRgb(prop.value); prop.value = getRgbaValue(prop.value); return ` --${variablesPrefix}-${prop.name}: ${prop.value};${ rgb ? `\n --${variablesPrefix}-${prop.name}-rgb: ${rgb.r}, ${rgb.g}, ${rgb.b};` : `` }`; } }); return [ fileHeader({ file }), '@use "../themes/functions.sizes" as font;\n', ':root {\n' + prefixedVariables.join('\n') + '\n}\n', ].join('\n'); }, }); // SCSS variables format StyleDictionary.registerFormat({ name: 'scssVariablesFormat', formatter({ dictionary, file }) { const typographyProperties = dictionary.allProperties.filter((prop) => prop['$type'] === 'typography'); const otherProperties = dictionary.allProperties.filter((prop) => prop['$type'] !== 'typography'); // Make sure the reused scss variables are defined first, to avoid compilation errors const sortedProperties = [...otherProperties, ...typographyProperties]; const prefixedVariables = sortedProperties.map((prop) => { if (prop.attributes.category.startsWith('Elevation')) { const cssShadow = prop.value.map(generateShadowValue).join(', '); return `$${variablesPrefix}-${prop.name}: var(--${variablesPrefix}-${prop.name}, ${cssShadow});`; } else if (prop.attributes.category.match('font-family')) { return generateFontFamilyValue(prop, 'scss'); } else if (prop.attributes.category.match('font-size')) { return generateFontSizeValue(prop, 'scss'); } else if (prop['$type'] === 'typography') { return generateTypographyValue(prop, dictionary); } else { return generateRgbValue(prop); } }); return [ fileHeader({ file }), '@use "../themes/functions.sizes" as font;\n', prefixedVariables.join('\n') + '\n', ].join('\n'); }, }); // Create utility-classes StyleDictionary.registerFormat({ name: 'cssUtilityClassesFormat', formatter({ dictionary, file }) { const utilityClasses = dictionary.allProperties.map((prop) => { let tokenType = prop.attributes.category; const className = `${prop.name}`; let utilityClass = ''; if (tokenType.startsWith('Elevation')) { return `.${variablesPrefix}-${className} {\n box-shadow: $ionic-${prop.name};\n}`; } else if (prop['$type'] === 'typography') { return generateTypographyUtilityClass(prop, dictionary); /* * Not creating for the tokens below, as they make no sense to exist as utility-classes. * Font-family should be defined on global scope, not component. * Scale its an abstract token group, to be used by other tokens, like the space ones. */ } else if (prop.attributes.category.match('font-family') || tokenType === 'scale') { return; } const tokenTypeLower = tokenType.toLowerCase(); switch (tokenTypeLower) { case 'color': case 'state': case 'guidelines': case 'disabled': case 'hover': case 'pressed': utilityClass = generateColorUtilityClasses(prop, className); break; case 'border-size': utilityClass = `.${variablesPrefix}-${className} {\n border-width: $ionic-${prop.name};\n}`; break; case 'font': utilityClass = generateFontUtilityClass(prop, className); break; case 'space': utilityClass = generateSpaceUtilityClasses(prop, className); break; default: utilityClass = `.${variablesPrefix}-${className} {\n ${tokenType}: $ionic-${prop.name};\n}`; } return utilityClass; }); return [ fileHeader({ file }), '@import "./ionic.vars";\n@import "../themes/mixins";\n', utilityClasses.join('\n'), ].join('\n'); }, }); // Register the custom format to generate HTML // Load the HTML template const template = fs.readFileSync(path.join(__dirname, 'preview.template.html'), 'utf8'); StyleDictionary.registerFormat({ name: 'html/tokens', formatter: function ({ dictionary }) { // Function to extract numerical value from token name const extractValue = (tokenName) => { const match = tokenName.match(/-([0-9]+)/); return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER; }; let colorTokens = ` `; let fontSizeTokens = ''; let boxShadowTokens = ''; let borderSizeTokens = ''; let borderRadiusTokens = ''; let borderStyleTokens = ''; let fontWeightTokens = ''; let letterSpacingTokens = ''; let spaceTokens = ''; // Collect border-radius and space tokens for separate sorting let borderRadiusTokenList = []; let spaceTokenList = []; dictionary.allProperties.forEach((token) => { if (token.attributes.category === 'color') { colorTokens += ` `; } else if (token.attributes.category === 'font-size') { fontSizeTokens += `
${token.name} (${token.value})
`; } else if (token.attributes.category.startsWith('Elevation')) { const cssShadow = token.value.map(generateShadowValue).join(', '); boxShadowTokens += `
${token.name}
`; } else if (token.attributes.category === 'border-size' || token.attributes.category === 'border-width') { borderSizeTokens += `
${token.name} (${token.value})
`; } else if (token.attributes.category === 'border-radius') { borderRadiusTokenList.push(token); // Collect border-radius tokens } else if (token.attributes.category === 'border-style') { borderStyleTokens += `
${token.name} (${token.value})
`; } else if (token.attributes.category === 'font-weight') { fontWeightTokens += `
${token.name} (${token.value})
`; } else if (token.attributes.category === 'letter-spacing') { // Convert % to px const letterSpacingValue = token.value.replace('%', '') + 'px'; letterSpacingTokens += `
${token.name} (${letterSpacingValue})
`; } else if (token.attributes.category === 'space') { spaceTokenList.push(token); // Collect space tokens } }); // Sort border-radius and space tokens borderRadiusTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name)); spaceTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name)); // Generate HTML for sorted border-radius tokens borderRadiusTokenList.forEach((token) => { borderRadiusTokens += `
${token.name} (${token.value})
`; }); // Generate HTML for sorted space tokens spaceTokenList.forEach((token) => { spaceTokens += `
${token.name} (${token.value})
`; }); colorTokens += '
Color Hex Token Name
${token.value} ${token.name}
'; return template .replace('{{colorTokens}}', colorTokens) .replace('{{fontSizeTokens}}', fontSizeTokens) .replace('{{boxShadowTokens}}', boxShadowTokens) .replace('{{borderSizeTokens}}', borderSizeTokens) .replace('{{borderRadiusTokens}}', borderRadiusTokens) .replace('{{borderStyleTokens}}', borderStyleTokens) .replace('{{fontWeightTokens}}', fontWeightTokens) .replace('{{letterSpacingTokens}}', letterSpacingTokens) .replace('{{spaceTokens}}', spaceTokens); }, }); // Custom transform to ensure unique token names StyleDictionary.registerTransform({ name: 'name/cti/kebab-unique', type: 'name', transformer: function (prop, options) { return [options.prefix].concat(prop.path).join('-').toLowerCase(); }, }); // Register the custom transform group for html file generation StyleDictionary.registerTransformGroup({ name: 'custom', transforms: ['attribute/cti', 'name/cti/kebab-unique', 'size/rem', 'color/css'], }); // Make Style Dictionary comply with the $ format on properties from W3C Guidelines const w3cTokenJsonParser = { pattern: /\.json|\.tokens\.json|\.tokens$/, parse({ contents }) { // replace $value with value so that style dictionary recognizes it var preparedContent = (contents || '{}') .replace(/"\$?value":/g, '"value":') // convert $description to comment .replace(/"\$?description":/g, '"comment":'); // return JSON.parse(preparedContent); }, }; StyleDictionary.registerParser(w3cTokenJsonParser); // Generate Tokens StyleDictionary.extend({ source: ['./src/foundations/tokens/*.json', './src/foundations/tokens/theme/*.json'], platforms: { css: { buildPath: targetPath, transformGroup: 'css', files: [ { destination: 'ionic.root.scss', format: 'rootFormat', options: { outputReferences: true, fileHeader: `myFileHeader`, }, }, ], }, scss: { buildPath: targetPath, transformGroup: 'scss', files: [ { destination: 'ionic.vars.scss', format: 'scssVariablesFormat', options: { outputReferences: true, fileHeader: `myFileHeader`, }, }, { destination: 'ionic.utility.scss', format: 'cssUtilityClassesFormat', options: { outputReferences: true, fileHeader: `myFileHeader`, }, }, ], }, html: { transformGroup: 'custom', buildPath: targetPath, files: [ { destination: 'tokens.preview.html', format: 'html/tokens', }, ], }, }, fileHeader: { myFileHeader: () => { return [`This is an auto-generated file, please do not change it directly.`, `Ionic Design System`]; }, }, }).buildAllPlatforms();