outsystems-design-tokens
Version:
Store the Design Tokens used on the Ionic Framework and Widgets Library
518 lines (432 loc) • 18.3 kB
JavaScript
let variablesPrefix = ''; // Variable that holds the prefix used on all css variables generated
let classAndScssPrefix = ''; // Variable that holds the prefix used on all css utility-classes and scss variables generated
let scssVariablePrefix = ''; // Variable that holds the prefix specifically for SCSS variable names
// Set the variable prefix value
export function setVariablePrefixValue(prefix) {
variablesPrefix = prefix;
return variablesPrefix;
}
export function setClassesAndScssPrefixValue(prefix) {
classAndScssPrefix = prefix;
return classAndScssPrefix;
}
export function setScssVariablePrefix(prefix) {
scssVariablePrefix = prefix;
return scssVariablePrefix;
}
// Generates a valid rgba() color
export function getRgbaValue(propValue) {
// Check if its rgba color
const isRgba = hexToRgba(propValue);
// If it is, then compose rgba() color, otherwise use the normal color
if (isRgba !== null) {
return (propValue = `rgba(${isRgba.r}, ${isRgba.g}, ${isRgba.b},${isRgba.a})`);
} else {
return propValue;
}
}
// Translates an hex color value to rgb
export function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
// Translates an hex color value to rgba
function hexToRgba(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: Math.round((parseInt(result[4], 16) * 100) / 255) / 100,
}
: null;
}
// Utility function to remove consecutive repeated words
export function removeConsecutiveRepeatedWords(str) {
return str.replace(/(\b\w+\b)(-\1)+/g, '$1');
}
// Helper to replace the CSS prefix with SCSS prefix in a name
function replaceWithScssPrefix(name) {
// If scssVariablePrefix is the same as variablesPrefix, no replacement needed
if (scssVariablePrefix === variablesPrefix || !scssVariablePrefix) {
return name;
}
// If name starts with the CSS prefix, replace it with the SCSS prefix
if (variablesPrefix && name.startsWith(`${variablesPrefix}-`)) {
return name.replace(`${variablesPrefix}-`, `${scssVariablePrefix}-`);
}
// If no CSS prefix was set, add the SCSS prefix
if (!variablesPrefix && scssVariablePrefix) {
return `${scssVariablePrefix}-${name}`;
}
return name;
}
// Helper function to create SCSS variable name (propName already has prefix from Style Dictionary)
function scssVar(name) {
return `$${replaceWithScssPrefix(name)}`;
}
// Helper function to create CSS custom property name (propName already has prefix from Style Dictionary)
function cssVar(name) {
return `--${name}`;
}
// Helper function to create class name (propName already has prefix from Style Dictionary)
function className(name) {
return `.${name}`;
}
// Helper function to create CSS custom property name (for CSS vars in utility classes)
function cssPropVar(name) {
return `--${name}`;
}
// Generates a reference variable for an alias token type
// (e.g., $ion-border-default: var(--ion-border-default, #d5d5d5) → $ion-border-default: var(--ion-border-default, $ion-primitives-neutral-400))
export function getAliasReferenceVariable(prop) {
if (typeof prop.$value === 'string' && prop.$value.startsWith('{') && prop.$value.endsWith('}')) {
// Remove curly braces and replace dots with dashes
let ref = prop.$value.slice(1, -1).replace(/\./g, '-');
// Remove consecutive repeated words (e.g., border-border-radius-0 → border-radius-0)
ref = removeConsecutiveRepeatedWords(ref);
// Add prefix if it exists
const fullRef = variablesPrefix ? `${variablesPrefix}-${ref}` : ref;
return scssVar(fullRef);
}
return null;
}
// Generates a valid box-shadow value from a shadow Design Token structure for CSS custom properties
export function generateCssShadowValue(prop, propName) {
const cssShadow = prop.$value.map(shadow => {
const color = getRgbaValue(shadow.color);
return `${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px ${color}`;
}).join(', ');
return `--${propName}: ${cssShadow};`;
}
// Generates a valid box-shadow value from a shadow Design Token structure for SCSS variables
export function generateShadowValue(prop, propName) {
const cssShadow = prop.$value.map(shadow => {
const color = getRgbaValue(shadow.color);
return `${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px ${color}`;
}).join(', ');
return `${scssVar(propName)}: var(${cssVar(propName)}, ${cssShadow});`;
}
// Generates a valid font-size value from a font-size Design Token structure
export function generateFontSizeValue(prop, propName, variableType = 'css') {
if (variableType === 'scss') {
return `${scssVar(propName)}: var(${cssVar(propName)}, ${prop.$value});`;
} else {
// For CSS custom properties, just return the pixel value
return `--${propName}: ${prop.$value};`;
}
}
// Generates a valid font-family value from a font-family Design Token structure
export function generateFontFamilyValue(prop, propName, variableType = 'css') {
// Remove the last word from the token, as it contains the name of the font, which we don't want to be included on the generated variables
const _propName = propName.split('-').slice(0, -1).join('-');
return variableType === 'scss'
? `${scssVar(_propName)}: var(${cssVar(_propName)}, ${prop.$value});`
: `--${_propName}: ${prop.$value};`;
}
// Generates a reference variable name for CSS custom properties (for aliases)
function getCssAliasReferenceVariable(prop) {
if (typeof prop.$value === 'string' && prop.$value.startsWith('{') && prop.$value.endsWith('}')) {
// Remove curly braces and replace dots with dashes
let ref = prop.$value.slice(1, -1).replace(/\./g, '-');
// Remove consecutive repeated words (e.g., border-border-radius-0 → border-radius-0)
ref = removeConsecutiveRepeatedWords(ref);
return `--${ref}`;
}
return null;
}
// Generates CSS custom properties for the :root format with alias support
export function generateCssValue(prop, propName) {
// Check if this token is an alias (references another token)
const isAlias = typeof prop.original.$value === 'string' &&
prop.original.$value.startsWith('{') &&
prop.original.$value.endsWith('}');
const value = prop.$value;
let mainValue;
if (isAlias) {
// Extract the referenced token name
let ref = prop.original.$value.slice(1, -1).replace(/\./g, '-');
ref = removeConsecutiveRepeatedWords(ref);
const refVarName = variablesPrefix ? `--${variablesPrefix}-${ref}` : `--${ref}`;
// Use var() with the resolved value as fallback
mainValue = `--${propName}: var(${refVarName}, ${value});`;
} else {
// Not an alias, use the value directly
mainValue = `--${propName}: ${value};`;
}
// Always generate the -rgb variable if it's a color
const rgb = hexToRgb(prop.$value);
let rgbDeclaration = '';
if (rgb) {
rgbDeclaration = `\n--${propName}-rgb: ${rgb.r}, ${rgb.g}, ${rgb.b};`;
}
return `${mainValue}${rgbDeclaration}`;
}
// Generates SCSS variables with CSS variable fallbacks for aliases
export function generateValue(prop, propName) {
// Use the original value to detect aliases
const aliasVar = getAliasReferenceVariable({ $value: prop.original.$value });
const value = prop.$value;
// Always generate the main variable
let mainValue;
if (aliasVar) {
mainValue = `${scssVar(propName)}: var(${cssVar(propName)}, ${aliasVar});`;
} else {
mainValue = `${scssVar(propName)}: var(${cssVar(propName)}, ${value});`;
}
// Always generate the -rgb variable if it's a color
const rgb = hexToRgb(prop.$value);
let rgbDeclaration = '';
if (rgb) {
rgbDeclaration = `\n${scssVar(propName + '-rgb')}: var(${cssVar(propName + '-rgb')}, ${rgb.r}, ${rgb.g}, ${rgb.b});`;
}
return `${mainValue}${rgbDeclaration}`;
}
// Generates a typography based css utility-class or scss variable from a typography token structure
export function generateTypographyOutput(prop, propName, isVariable) {
const typography = prop.original.$value;
// Extract the part after the last dot and trim any extraneous characters
const extractLastPart = (str) => str.split('.').pop().replace(/[^\w-]/g, '');
const _initialWrapper = isVariable ? ': (' : ` {`;
const _endWrapper = isVariable ? ')' : `}`;
const _prefix = isVariable ? '$' : `.`;
const _endChar = isVariable ? ',' : ';';
// propName already has the prefix from Style Dictionary, so use it directly
// Build the font property names with the same prefix that's in propName
const prefix = variablesPrefix ? `${variablesPrefix}-` : '';
const fontSizeName = `${prefix}font-size-${extractLastPart(typography.fontSize)}`;
const fontWeightName = `${prefix}font-weight-${extractLastPart(typography.fontWeight)}`;
const letterSpacingName = `${prefix}font-letter-spacing-${extractLastPart(typography.letterSpacing) || 0}`;
const lineHeightName = `${prefix}font-line-height-${extractLastPart(typography.lineHeight)}`;
// This exact format is needed so that it compiles the tokens with the expected lint rules
if (isVariable) {
// SCSS variable format (no leading newline, no extra indentation)
return `${_prefix}${replaceWithScssPrefix(propName)}${_initialWrapper}
font-size: ${scssVar(fontSizeName)}${_endChar}
font-style: ${prop.attributes.item?.toLowerCase() === 'italic' ? 'italic' : 'normal'}${_endChar}
font-weight: ${scssVar(fontWeightName)}${_endChar}
letter-spacing: ${scssVar(letterSpacingName)}${_endChar}
line-height: ${scssVar(lineHeightName)}${_endChar}
text-transform: ${typography.textCase}${_endChar}
text-decoration: ${typography.textDecoration}${_endChar}
${_endWrapper};`;
} else {
// Utility class format (with leading newline and indentation)
return `
${_prefix}${propName}${_initialWrapper}
font-size: $${fontSizeName}${_endChar}
font-style: ${prop.attributes.item?.toLowerCase() === 'italic' ? 'italic' : 'normal'}${_endChar}
font-weight: $${fontWeightName}${_endChar}
letter-spacing: $${letterSpacingName}${_endChar}
line-height: $${lineHeightName}${_endChar}
text-transform: ${typography.textCase}${_endChar}
text-decoration: ${typography.textDecoration}${_endChar}
${_endWrapper};
`;
}
}
// Generates a color based css utility-class from a color Design Token structure
export function generateColorUtilityClasses(prop, className) {
const isBg = className.includes('bg');
const cssProp = isBg ? 'background-color' : 'color';
return `.${className} {
--${cssProp}: #{$${className}};
${cssProp}: $${className};
}`;
}
// Generates margin and padding utility classes to match the token-agnostic
// utilities provided by the Ionic Framework
export function generateDefaultSpaceUtilityClasses() {
const zeroMarginPaddingToken = variablesPrefix ? `${variablesPrefix}-space-0` : 'space-0';
const defaultMarginPaddingToken = variablesPrefix ? `${variablesPrefix}-space-400` : 'space-400';
const classPrefix = variablesPrefix ? `${variablesPrefix}-` : '';
const marginPaddingTemplate = (type) => `
.${classPrefix}no-${type} {
--${type}-top: #{$${zeroMarginPaddingToken}};
--${type}-end: #{$${zeroMarginPaddingToken}};
--${type}-bottom: #{$${zeroMarginPaddingToken}};
--${type}-start: #{$${zeroMarginPaddingToken}};
${type}-block-start: $${zeroMarginPaddingToken};
${type}-inline-end: $${zeroMarginPaddingToken};
${type}-block-end: $${zeroMarginPaddingToken};
${type}-inline-start: $${zeroMarginPaddingToken};
}
.${classPrefix}${type} {
--${type}-top: #{$${defaultMarginPaddingToken}};
--${type}-end: #{$${defaultMarginPaddingToken}};
--${type}-bottom: #{$${defaultMarginPaddingToken}};
--${type}-start: #{$${defaultMarginPaddingToken}};
${type}-block-start: $${defaultMarginPaddingToken};
${type}-inline-end: $${defaultMarginPaddingToken};
${type}-block-end: $${defaultMarginPaddingToken};
${type}-inline-start: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-top {
--${type}-top: #{$${defaultMarginPaddingToken}};
${type}-block-start: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-end {
--${type}-end: #{$${defaultMarginPaddingToken}};
${type}-inline-end: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-bottom {
--${type}-bottom: #{$${defaultMarginPaddingToken}};
${type}-block-end: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-start {
--${type}-start: #{$${defaultMarginPaddingToken}};
${type}-inline-start: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-vertical {
--${type}-top: #{$${defaultMarginPaddingToken}};
--${type}-bottom: #{$${defaultMarginPaddingToken}};
${type}-block-start: $${defaultMarginPaddingToken};
${type}-block-end: $${defaultMarginPaddingToken};
}
.${classPrefix}${type}-horizontal {
--${type}-start: #{$${defaultMarginPaddingToken}};
--${type}-end: #{$${defaultMarginPaddingToken}};
${type}-inline-start: $${defaultMarginPaddingToken};
${type}-inline-end: $${defaultMarginPaddingToken};
}
`;
return `${marginPaddingTemplate('margin')}\n${marginPaddingTemplate('padding')}`;
}
// Generates a margin or padding based css utility-class from a space Design Token structure
export function generateSpaceUtilityClasses(prop, className) {
// className already has the prefix from Style Dictionary (e.g., "token-space-0")
// We need to extract the numeric value and remove "space"
// From "token-space-0" to "token-gap-0", "token-margin-0", "token-margin-top-0", etc.
const extractPrefixAndValue = (name) => {
// If there's a prefix, extract it and remove "space"
if (variablesPrefix) {
const regex = new RegExp(`^${variablesPrefix}-space-(.*)`);
const match = name.match(regex);
if (match) {
return { prefix: variablesPrefix, value: match[1] };
}
}
// No prefix case - remove "space-" prefix
const spaceRemoved = name.replace(/^space-/, '');
return { prefix: '', value: spaceRemoved };
};
const { prefix, value } = extractPrefixAndValue(className);
const buildClassName = (type, direction = null) => {
// Build class name as: {prefix}-{type}-{direction}-{value}
// or {prefix}-{type}-{value} if no direction
const parts = [];
if (prefix) parts.push(prefix);
parts.push(type);
if (direction) parts.push(direction);
parts.push(value);
return parts.join('-');
};
const marginPaddingTemplate = (type) => {
const baseClassName = buildClassName(type);
const topClassName = buildClassName(type, 'top');
const endClassName = buildClassName(type, 'end');
const bottomClassName = buildClassName(type, 'bottom');
const startClassName = buildClassName(type, 'start');
return `
.${baseClassName} {
--${type}-top: #{$${className}};
--${type}-end: #{$${className}};
--${type}-bottom: #{$${className}};
--${type}-start: #{$${className}};
${type}-block-start: $${className};
${type}-inline-end: $${className};
${type}-block-end: $${className};
${type}-inline-start: $${className};
}
.${topClassName} {
--${type}-top: #{$${className}};
${type}-block-start: $${className};
}
.${endClassName} {
--${type}-end: #{$${className}};
${type}-inline-end: $${className};
}
.${bottomClassName} {
--${type}-bottom: #{$${className}};
${type}-block-end: $${className};
}
.${startClassName} {
--${type}-start: #{$${className}};
${type}-inline-start: $${className};
}
`;
};
// Add gap utility classes for gap tokens
const gapClassName = buildClassName('gap');
const generateGapUtilityClasses = () =>`
.${gapClassName} {
gap: #{$${className}};
}
`;
return `${generateGapUtilityClasses()}\n${marginPaddingTemplate('margin')}\n${marginPaddingTemplate('padding')}`;
}
// Generates a valid box-shadow value from a shadow Design Token structure
export function generateRadiusUtilityClasses(propName) {
// propName already has the prefix from Style Dictionary
return `.${propName} {
--border-radius: #{$${propName}};
border-radius: $${propName};
}`;
}
// Generates a border based css utility-class from a font Design Token structure
export function generateBorderUtilityClasses(prop, propName) {
let attribute;
switch (prop.attributes.type) {
case 'border-radius':
case 'border-style':
attribute = prop.attributes.type;
break;
case 'border-size':
attribute = 'border-width';
break;
default:
attribute = 'border-color';
}
return `.${propName} {
--${attribute}: #{$${propName}};
${attribute}: $${propName};
}`;
}
// Generates a font based css utility-class from a font Design Token structure
export function generateFontUtilityClasses(prop, propName) {
return `.${propName} {\n ${prop.attributes.type}: $${propName};\n}`;
}
// Generates a valid box-shadow value from a shadow Design Token structure
export function generateShadowUtilityClasses(propName) {
// propName already has the prefix from Style Dictionary
return `.${propName} {
--box-shadow: #{$${propName}};
box-shadow: $${propName};
}`;
}
// Generates a utility class for a given token category and name
export function generateUtilityClasses(tokenCategory, propName){
// propName already has the prefix from Style Dictionary
return `.${propName} {\n ${tokenCategory}: $${propName};\n}`;
}
// Sets the source and target paths based on provided arguments or defaults
export function setSourceAndTargetPaths(sourcePath, targetPath) {
const defaultSourcePath = ['tokens/**/*.json'];
const defaultTargetPath = 'dist/';
return {
sourcePath: sourcePath && sourcePath !== 'undefined' ? [sourcePath] : defaultSourcePath,
targetPath: targetPath && targetPath !== 'undefined' ? targetPath : defaultTargetPath
};
}
// Sets the variable type prefix (e.g., '--' for CSS custom properties, '$' for SCSS variables)
export function setVariableTypePrefix(prefix) {
variablesPrefix = prefix;
return variablesPrefix;
}