Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(css): add CSS utility classes to the ionic theme bundle to match other themes #29974

Merged
merged 9 commits into from
Oct 30, 2024
5 changes: 5 additions & 0 deletions core/scripts/testing/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
linkTag.setAttribute('href', '/css/ionic/bundle.ionic.css');
document.head.appendChild(linkTag);
}

const defaultThemeLinkTag = document.querySelector('link[href*="css/ionic.bundle.css"]');
if (defaultThemeLinkTag) {
defaultThemeLinkTag.remove();
}
}

window.Ionic = window.Ionic || {};
Expand Down
257 changes: 130 additions & 127 deletions core/scripts/tokens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
generateTypographyOutput,
generateValue,
generateColorUtilityClasses,
generateDefaultSpaceUtilityClasses,
generateSpaceUtilityClasses,
removeConsecutiveRepeatedWords,
setPrefixValue,
Expand All @@ -23,136 +24,138 @@
} = require('./utils.js');

const StyleDictionary = (await import('style-dictionary')).default;

// Set the prefix for variables and classes
setPrefixValue('ion');

// Register a custom file header
StyleDictionary.registerFileHeader({
name: 'custom-header',
fileHeader: async (defaultMessages = []) => {
return [...defaultMessages, 'Do not edit directly, this file was auto-generated.'];
},
name: 'custom-header',
fileHeader: async (defaultMessages = []) => {
return [...defaultMessages, 'Do not edit directly, this file was auto-generated.'];
},
});

// SCSS variables format
StyleDictionary.registerFormat({
name: 'scssVariablesFormat',
format: async function({ dictionary, file}) {
format: async function ({ dictionary, file }) {

console.log('Generating SCSS variables...');

// Separate typography tokens from the rest
const typographyProperties = dictionary.allTokens.filter((prop) => prop.$type === 'typography');
const otherProperties = dictionary.allTokens.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) => {
// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);

switch (prop.$type) {
case 'boxShadow':
return generateShadowValue(prop, propName);
case 'fontFamilies':
return generateFontFamilyValue(prop, propName, 'scss');
case 'fontSizes':
return generateFontSizeValue(prop, propName, 'scss');
case 'typography':
return generateTypographyOutput(prop, propName, true);
default:
return generateValue(prop, propName);
}
});

const fileHeader = await file.options.fileHeader();
return [
`/*\n${fileHeader.join('\n')}\n*/`,
'@use "../themes/functions.sizes" as font;\n',
prefixedVariables.join('\n') + '\n',
].join('\n');
// Separate typography tokens from the rest
const typographyProperties = dictionary.allTokens.filter((prop) => prop.$type === 'typography');
const otherProperties = dictionary.allTokens.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) => {
// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);

switch (prop.$type) {
case 'boxShadow':
return generateShadowValue(prop, propName);
case 'fontFamilies':
return generateFontFamilyValue(prop, propName, 'scss');
case 'fontSizes':
return generateFontSizeValue(prop, propName, 'scss');
case 'typography':
return generateTypographyOutput(prop, propName, true);
default:
return generateValue(prop, propName);
}
});

const fileHeader = await file.options.fileHeader();

return [
`/*\n${fileHeader.join('\n')}\n*/`,
'@use "../themes/functions.sizes" as font;\n',
prefixedVariables.join('\n') + '\n',
].join('\n');
},
});

// Create utility-classes
StyleDictionary.registerFormat({
name: 'cssUtilityClassesFormat',
format: async function({ dictionary, file}) {

console.log('Generating Utility-Classes...');

// Arrays to store specific utility classes
const typographyUtilityClasses = [];
const otherUtilityClasses = [];

// Generate utility classes for each token
dictionary.allTokens.map((prop) => {

const tokenCategory = prop.attributes.category;

if (prop.$type === 'fontFamilies' || tokenCategory === 'scale' || tokenCategory === 'backdrop') {
// Not creating for the tokens below, as they make no sense to exist as utility-classes.
return;
}

// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);

if (prop.$type === 'typography') {
// Typography tokens are handled differently, as each might have a different tokenType
return typographyUtilityClasses.push(generateTypographyOutput(prop, propName, false));

} else if(tokenCategory.startsWith('round') || tokenCategory.startsWith('rectangular') || tokenCategory.startsWith('soft')) {
// Generate utility classes for border-radius shape tokens, as they have their own token json file, based on primitive tokens
return otherUtilityClasses.push(generateRadiusUtilityClasses(propName));

}

let utilityClass = '';

switch (tokenCategory) {
case 'color':
case 'primitives':
case 'semantics':
case 'text':
case 'bg':
case 'icon':
case 'state':
utilityClass = generateColorUtilityClasses(prop, propName);
break;
case 'border-size':
utilityClass = generateBorderSizeUtilityClasses(propName);
break;
case 'font':
utilityClass = generateFontUtilityClasses(prop, propName);
break;
case 'space':
utilityClass = generateSpaceUtilityClasses(prop, propName);
break;
case 'shadow':
case 'elevation':
utilityClass = generateShadowUtilityClasses(propName);
break;
default:
utilityClass = generateUtilityClasses(tokenCategory, propName);
}

return otherUtilityClasses.push(utilityClass);
});

// Concatenate typography utility classes at the beginning
const finalOutput = typographyUtilityClasses.concat(otherUtilityClasses).join('\n');

const fileHeader = await file.options.fileHeader();

return [
`/*\n${fileHeader.join('\n')}\n*/`,
'@import "./ionic.vars";\n@import "../themes/mixins";\n',
finalOutput,
].join('\n');
format: async function ({ dictionary, file }) {

console.log('Generating Utility-Classes...');

// Arrays to store specific utility classes
const typographyUtilityClasses = [];
const otherUtilityClasses = [];

// Generate utility classes for each token
dictionary.allTokens.map((prop) => {

const tokenCategory = prop.attributes.category;

if (prop.$type === 'fontFamilies' || tokenCategory === 'scale' || tokenCategory === 'backdrop') {
// Not creating for the tokens below, as they make no sense to exist as utility-classes.
return;
}

// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);

if (prop.$type === 'typography') {
// Typography tokens are handled differently, as each might have a different tokenType
return typographyUtilityClasses.push(generateTypographyOutput(prop, propName, false));

} else if (tokenCategory.startsWith('round') || tokenCategory.startsWith('rectangular') || tokenCategory.startsWith('soft')) {
// Generate utility classes for border-radius shape tokens, as they have their own token json file, based on primitive tokens
return otherUtilityClasses.push(generateRadiusUtilityClasses(propName));
}

let utilityClass = '';

switch (tokenCategory) {
case 'color':
case 'primitives':
case 'semantics':
case 'text':
case 'bg':
case 'icon':
case 'state':
utilityClass = generateColorUtilityClasses(prop, propName);
break;
case 'border-size':
utilityClass = generateBorderSizeUtilityClasses(propName);
break;
case 'font':
utilityClass = generateFontUtilityClasses(prop, propName);
break;
case 'space':
utilityClass = generateSpaceUtilityClasses(prop, propName);
break;
case 'shadow':
case 'elevation':
utilityClass = generateShadowUtilityClasses(propName);
break;
default:
utilityClass = generateUtilityClasses(tokenCategory, propName);
}

return otherUtilityClasses.push(utilityClass);
});

const defaultSpaceUtilityClasses = generateDefaultSpaceUtilityClasses();
otherUtilityClasses.push(defaultSpaceUtilityClasses);
Comment on lines +146 to +147
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were the only lines added - the rest is formatting


// Concatenate typography utility classes at the beginning
const finalOutput = typographyUtilityClasses.concat(otherUtilityClasses).join('\n');

const fileHeader = await file.options.fileHeader();

return [
`/*\n${fileHeader.join('\n')}\n*/`,
'@import "./ionic.vars";\n@import "../themes/mixins";\n',
finalOutput,
].join('\n');
},
});

Expand All @@ -163,24 +166,24 @@ module.exports = {
source: ["node_modules/outsystems-design-tokens/tokens/**/*.json"],
platforms: {
scss: {
transformGroup: "scss",
buildPath: './src/foundations/',
files: [
{
destination: "ionic.vars.scss",
format: "scssVariablesFormat",
options: {
fileHeader: `custom-header`,
},
transformGroup: "scss",
buildPath: './src/foundations/',
files: [
{
destination: "ionic.vars.scss",
format: "scssVariablesFormat",
options: {
fileHeader: `custom-header`,
},
{
destination: "ionic.utility.scss",
format: "cssUtilityClassesFormat",
options: {
fileHeader: `custom-header`
}
},
{
destination: "ionic.utility.scss",
format: "cssUtilityClassesFormat",
options: {
fileHeader: `custom-header`
}
]
}
]
}
}
};
68 changes: 68 additions & 0 deletions core/scripts/tokens/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,73 @@ function generateColorUtilityClasses(prop, className) {
.${variablesPrefix}-background-${className} {\n background-color: $${variablesPrefix}-${prop.name};\n}`;
}

// Generates margin and padding utility classes to match the token-agnostic
// utilities provided by the Ionic Framework
function generateDefaultSpaceUtilityClasses() {
const zeroMarginPaddingToken = 'space-0';
const defaultMarginPaddingToken = 'space-400';

const marginPaddingTemplate = (type) => `
.${variablesPrefix}-no-${type} {
--${type}-top: #{$${variablesPrefix}-${zeroMarginPaddingToken}};
--${type}-end: #{$${variablesPrefix}-${zeroMarginPaddingToken}};
--${type}-bottom: #{$${variablesPrefix}-${zeroMarginPaddingToken}};
--${type}-start: #{$${variablesPrefix}-${zeroMarginPaddingToken}};

@include ${type}($${variablesPrefix}-${zeroMarginPaddingToken});
};

.${variablesPrefix}-${type} {
--${type}-top: #{$${variablesPrefix}-${defaultMarginPaddingToken}};
--${type}-end: #{$${variablesPrefix}-${defaultMarginPaddingToken}};
--${type}-bottom: #{$${variablesPrefix}-${defaultMarginPaddingToken}};
--${type}-start: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}($${variablesPrefix}-${defaultMarginPaddingToken});
};

.${variablesPrefix}-${type}-top {
--${type}-top: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}($${variablesPrefix}-${defaultMarginPaddingToken}, null, null, null);
};

.${variablesPrefix}-${type}-end {
--${type}-end: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}(null, $${variablesPrefix}-${defaultMarginPaddingToken}, null, null);
};

.${variablesPrefix}-${type}-bottom {
--${type}-bottom: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}(null, null, $${variablesPrefix}-${defaultMarginPaddingToken}, null);
};

.${variablesPrefix}-${type}-start {
--${type}-start: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}(null, null, null, $${variablesPrefix}-${defaultMarginPaddingToken});
};

.${variablesPrefix}-${type}-vertical {
--${type}-top: #{$${variablesPrefix}-${defaultMarginPaddingToken}};
--${type}-bottom: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}($${variablesPrefix}-${defaultMarginPaddingToken}, null, $${variablesPrefix}-${defaultMarginPaddingToken}, null);
};

.${variablesPrefix}-${type}-horizontal {
--${type}-start: #{$${variablesPrefix}-${defaultMarginPaddingToken}};
--${type}-end: #{$${variablesPrefix}-${defaultMarginPaddingToken}};

@include ${type}(null, $${variablesPrefix}-${defaultMarginPaddingToken}, null, $${variablesPrefix}-${defaultMarginPaddingToken});
};
`;

return `${marginPaddingTemplate('margin')}\n${marginPaddingTemplate('padding')}`;
}

// Generates a margin or padding based css utility-class from a space Design Token structure
function generateSpaceUtilityClasses(prop, className) {
// This exact format is needed so that it compiles the tokens with the expected lint rules
Expand Down Expand Up @@ -203,6 +270,7 @@ module.exports = {
setPrefixValue,
generateRadiusUtilityClasses,
generateColorUtilityClasses,
generateDefaultSpaceUtilityClasses,
generateSpaceUtilityClasses,
removeConsecutiveRepeatedWords,
generateBorderSizeUtilityClasses,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading