diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index ad0f3c60d..909449c6b 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -5,12 +5,22 @@ import postcssImport from 'postcss-import'; postcssTape(plugin)({ basic: { message: "supports basic usage", - options: {}, plugins: [ postcssImport(), plugin() ] }, + 'basic:rootFontSize-20': { + message: "supports basic usage with { unitsAndValues { rootFontSize: 20 } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: 20 + } + }) + ] + }, 'errors': { message: "handles issues correctly", options: {}, diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts index ac789984f..5b1a12f1e 100644 --- a/plugins/postcss-design-tokens/src/data-formats/base/token.ts +++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts @@ -1,3 +1,10 @@ +export interface TokenTransformOptions { + pluginOptions?: { + rootFontSize?: number; + }; + toUnit?: string; +} + export interface Token { - cssValue(): string + cssValue(opts?: TokenTransformOptions): string } diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index e32b3aa1e..897bed1bf 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -1,5 +1,6 @@ +import { TokenTransformOptions } from '../../base/token'; import { toposort } from '../../toposort/toposort'; -import { StyleDictionaryV3TokenValue } from './value'; +import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value'; export function dereferenceTokenValues(tokens: Map): Map { const tainted = new Set(); @@ -53,8 +54,8 @@ export function dereferenceTokenValues(tokens: Map { - return value ?? ''; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }; tokens.set(id, currentToken); @@ -125,8 +126,8 @@ export function dereferenceTokenValues(tokens: Map { - return value ?? ''; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }; tokens.set(id, currentToken); diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts index 8b96fbefe..9f3a5a1fe 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -1,5 +1,8 @@ +import { TokenTransformOptions } from '../../base/token'; +import valueParser from 'postcss-value-parser'; + export type StyleDictionaryV3TokenValue = { - cssValue(): string + cssValue(transformOptions?: TokenTransformOptions): string // The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array. value: unknown // Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object. @@ -33,8 +36,8 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path: return { value: value, - cssValue: () => { - return value ?? ''; + cssValue: (transformOptions?: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }, name: node['name'] ?? key, comment: node['comment'] ?? undefined, @@ -46,3 +49,58 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path: }, }; } + +export function applyTransformsToValue(value: string|undefined|null, transformOptions?: TokenTransformOptions): string { + if (!value) { + return ''; + } + + if (!transformOptions) { + return value; + } + + if (transformOptions.toUnit) { + const dimension = valueParser.unit(value ?? ''); + if (!dimension || dimension.unit === transformOptions.toUnit) { + return `${value}`; + } + + if (dimension.unit === 'rem' && transformOptions.toUnit === 'px') { + return remToPx(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + + if (dimension.unit === 'px' && transformOptions.toUnit === 'rem') { + return pxToRem(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + } + + return value; +} + +function remToPx(value: number, rootFontSize: number): string { + return `${formatFloat(value * rootFontSize)}px`; +} + +function pxToRem(value: number, rootFontSize: number): string { + return `${formatFloat(value / rootFontSize)}rem`; +} + +function formatFloat(value: number): string { + if (Number.isInteger(value)) { + return value.toString(); + } + + let fixedPrecision = value.toFixed(5); + for (let i = fixedPrecision.length; i > 0; i--) { + if (fixedPrecision[i] === '.') { + break; + } + + if (fixedPrecision[i] !== '0') { + fixedPrecision = fixedPrecision.slice(0, i + 1); + continue; + } + } + + return fixedPrecision; +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index a6e724c19..580defa0a 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -2,11 +2,9 @@ import type { Node, PluginCreator } from 'postcss'; import { Token } from './data-formats/base/token'; import { tokensFromImport } from './data-formats/parse-import'; import { mergeTokens } from './data-formats/token'; +import { pluginOptions } from './options'; import { onCSSValue } from './values'; -type pluginOptions = { - is?: Array -} const creator: PluginCreator = (opts?: pluginOptions) => { const buildIs = opts?.is ?? []; @@ -70,7 +68,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - const modifiedValue = onCSSValue(tokens, result, decl); + const modifiedValue = onCSSValue(tokens, result, decl, opts); if (modifiedValue === decl.value) { return; } diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts new file mode 100644 index 000000000..1d4dcb734 --- /dev/null +++ b/plugins/postcss-design-tokens/src/options.ts @@ -0,0 +1,6 @@ +export type pluginOptions = { + is?: Array + unitsAndValues?: { + rootFontSize?: number + } +} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts index e1db3194d..22cc949dc 100644 --- a/plugins/postcss-design-tokens/src/values.ts +++ b/plugins/postcss-design-tokens/src/values.ts @@ -1,8 +1,9 @@ import type { Declaration, Result } from 'postcss'; import valueParser from 'postcss-value-parser'; -import { Token } from './data-formats/base/token'; +import { Token, TokenTransformOptions } from './data-formats/base/token'; +import { pluginOptions } from './options'; -export function onCSSValue(tokens: Map, result: Result, decl: Declaration) { +export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts?: pluginOptions) { const valueAST = valueParser(decl.value); valueAST.walk(node => { @@ -10,23 +11,47 @@ export function onCSSValue(tokens: Map, result: Result, decl: Dec return; } - if (!node.nodes || node.nodes.length !== 1) { - decl.warn(result, 'Expected a single string literal for the design-token function.'); + if (!node.nodes || node.nodes.length === 0) { + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); return; } if (node.nodes[0].type !== 'string') { - decl.warn(result, 'Expected a single string literal for the design-token function.'); + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); return; } - const replacement = tokens.get(node.nodes[0].value); + const tokenName = node.nodes[0].value; + const replacement = tokens.get(tokenName); if (!replacement) { - decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`); + decl.warn(result, `design-token: "${tokenName}" is not configured.`); return; } - node.value = replacement.cssValue(); + const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word'); + if (!remainingNodes.length) { + node.value = replacement.cssValue(); + node.nodes = undefined; + return; + } + + const transformOptions: TokenTransformOptions = { + pluginOptions: opts?.unitsAndValues, + }; + for (let i = 0; i < remainingNodes.length; i++) { + if ( + remainingNodes[i].type === 'word' && + remainingNodes[i].value === 'to' && + remainingNodes[i + 1] && + remainingNodes[i + 1].type === 'word' && + ['px', 'rem'].includes(remainingNodes[i + 1].value) + ) { + transformOptions.toUnit = remainingNodes[i + 1].value; + i++; + } + } + + node.value = replacement.cssValue(transformOptions); node.nodes = undefined; }); diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 3f9ddf7f4..9fc8643f7 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -10,4 +10,45 @@ .card { background-color: design-token('card.background'); color: design-token('card.foreground'); + color: design-token( 'card.foreground'); + color: design-token('card.foreground' ); + color: design-token( + /* a foreground color */ + 'card.foreground' + ); + color: design-token( + 'card.foreground' + /* a foreground color */ + ); +} + +.px-to-px { + padding-bottom: design-token('space.small' to px); + padding-bottom: design-token('space.default' to px); + padding-bottom: design-token('space.large' to px); +} + +.px-to-rem { + padding-bottom: design-token('space.small' to rem); + padding-bottom: design-token('space.default' to rem); + padding-bottom: design-token('space.large' to rem); +} + +.rem-to-rem { + padding-bottom: design-token('space.small-b' to rem); + padding-bottom: design-token('space.default-b' to rem); + padding-bottom: design-token('space.large-b' to rem); +} + +.rem-to-px { + padding-bottom: design-token('space.small-b' to px); + padding-bottom: design-token('space.default-b' to px); + padding-bottom: design-token('space.large-b' to px); +} + +.invalid-conversion { + color: design-token('card.foreground' to rem); + color: design-token('card.foreground' to px); + color: design-token('space.lh' to rem); + color: design-token('space.lh' to px); } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index 459bcb6ad..d4b93d305 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -6,4 +6,34 @@ .card { background-color: blue; color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; } diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css new file mode 100644 index 000000000..d1915ac36 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.4rem; + padding-bottom: 0.9rem; + padding-bottom: 1.6rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 10px; + padding-bottom: 22.5px; + padding-bottom: 40px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/tokens/basic.json b/plugins/postcss-design-tokens/test/tokens/basic.json index 925b731ff..1ec411322 100644 --- a/plugins/postcss-design-tokens/test/tokens/basic.json +++ b/plugins/postcss-design-tokens/test/tokens/basic.json @@ -18,5 +18,28 @@ "logical-color": { "foreground": { "value": "{base-color.red}" }, "background": { "value": "{base-color.blue}" } + }, + "space": { + "small": { + "value": "8px" + }, + "default": { + "value": "18px" + }, + "large": { + "value": "32px" + }, + "small-b": { + "value": "0.5rem" + }, + "default-b": { + "value": "1.125rem" + }, + "large-b": { + "value": "2rem" + }, + "lh": { + "value": "1lh" + } } }