From 58ada36df04b7de382e2b2d22e73cda8d1f3d91c Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Sun, 13 Oct 2024 17:47:27 +0200 Subject: [PATCH] feat(cli): build tokens for color-{primary,support} modes --- packages/cli/src/tokens/build.ts | 55 +++++++++-- packages/cli/src/tokens/build/configs.ts | 92 +++++++++++++++---- packages/cli/src/tokens/build/formats/css.ts | 24 +++++ .../cli/src/tokens/build/utils/entryfile.ts | 6 ++ .../src/tokens/build/utils/permutateThemes.ts | 54 +++++++---- 5 files changed, 189 insertions(+), 42 deletions(-) diff --git a/packages/cli/src/tokens/build.ts b/packages/cli/src/tokens/build.ts index 2ea341093c..134da38e75 100644 --- a/packages/cli/src/tokens/build.ts +++ b/packages/cli/src/tokens/build.ts @@ -8,13 +8,14 @@ import StyleDictionary from 'style-dictionary'; import * as configs from './build/configs.js'; import { makeEntryFile } from './build/utils/entryfile.js'; +import { groupThemes } from './build/utils/permutateThemes.js'; const { permutateThemes, getConfigs } = configs; type Options = { - /** Design tokens path */ + /** Design tokens path */ tokens: string; - /** Output directoru for built tokens */ + /** Output directory for built tokens */ out: string; /** Generate preview tokens */ preview: boolean; @@ -22,8 +23,6 @@ type Options = { verbose: boolean; }; -// type FormattedCSSPlatform = { css: { output: string; destination: string }[] }; - const sd = new StyleDictionary(); export async function buildTokens(options: Options): Promise { @@ -40,14 +39,36 @@ export async function buildTokens(options: Options): Promise { return true; }); - - const themes = permutateThemes(relevant$themes); + const grouped$themes = groupThemes(relevant$themes); + const themes = permutateThemes(grouped$themes); const typographyThemes = R.filter((val) => val.mode === 'light', themes); const colormodeThemes = R.filter((val) => val.typography === 'primary', themes); + const primaryColors = R.filter( + (val) => grouped$themes.colorPrimary.some((theme) => val.colorPrimary === theme.name), + themes, + ); + const supportColors = R.filter( + (val) => grouped$themes.colorSupport.some((theme) => val.colorSupport === theme.name), + themes, + ); const semanticThemes = R.filter((val) => val.mode === 'light' && val.typography === 'primary', themes); const colorModeConfigs = getConfigs(configs.colorModeVariables, outPath, tokensDir, colormodeThemes, verbosity); + const primaryColorConfigs = getConfigs( + configs.colorCategories('primary'), + outPath, + tokensDir, + primaryColors, + verbosity, + ); + const supportColorConfigs = getConfigs( + configs.colorCategories('support'), + outPath, + tokensDir, + supportColors, + verbosity, + ); const semanticConfigs = getConfigs(configs.semanticVariables, outPath, tokensDir, semanticThemes, verbosity); const typographyConfigs = getConfigs(configs.typographyVariables, outPath, tokensDir, typographyThemes, verbosity); const storefrontConfigs = getConfigs( @@ -87,6 +108,28 @@ export async function buildTokens(options: Options): Promise { ); } + if (primaryColorConfigs.length > 0) { + console.log(`\nšŸ± Building ${chalk.green('color-primary')}`); + + await Promise.all( + primaryColorConfigs.map(async ({ theme, colorPrimary, config }) => { + console.log(`šŸ‘· ${theme} - ${colorPrimary}`); + return (await sd.extend(config)).buildAllPlatforms(); + }), + ); + } + + if (supportColorConfigs.length > 0) { + console.log(`\nšŸ± Building ${chalk.green('color-support')}`); + + await Promise.all( + supportColorConfigs.map(async ({ theme, colorSupport, config }) => { + console.log(`šŸ‘· ${theme} - ${colorSupport}`); + return (await sd.extend(config)).buildAllPlatforms(); + }), + ); + } + if (semanticConfigs.length > 0) { console.log(`\nšŸ± Building ${chalk.green('semantic')}`); diff --git a/packages/cli/src/tokens/build/configs.ts b/packages/cli/src/tokens/build/configs.ts index 2b10177672..f6fc137ab0 100644 --- a/packages/cli/src/tokens/build/configs.ts +++ b/packages/cli/src/tokens/build/configs.ts @@ -1,5 +1,4 @@ import { expandTypesMap, register } from '@tokens-studio/sd-transforms'; -import type { ThemeObject } from '@tokens-studio/types'; import * as R from 'ramda'; import StyleDictionary from 'style-dictionary'; import type { Config, LogConfig, TransformedToken } from 'style-dictionary/types'; @@ -9,7 +8,7 @@ import * as formats from './formats/css.js'; import { jsTokens } from './formats/js-tokens.js'; import { nameKebab, sizeRem, typographyName } from './transformers.js'; import { permutateThemes as permutateThemes_ } from './utils/permutateThemes.js'; -import type { PermutatedThemes } from './utils/permutateThemes.js'; +import type { GroupedThemes, PermutatedTheme, PermutatedThemes, PermutationProps } from './utils/permutateThemes.js'; import { pathStartsWithOneOf, typeEquals } from './utils/utils.js'; void register(StyleDictionary, { withSDBuiltins: false }); @@ -29,6 +28,7 @@ StyleDictionary.registerTransform(typographyName); StyleDictionary.registerFormat(jsTokens); StyleDictionary.registerFormat(formats.colormode); +StyleDictionary.registerFormat(formats.colorcategory); StyleDictionary.registerFormat(formats.semantic); StyleDictionary.registerFormat(formats.typography); @@ -51,7 +51,7 @@ const hasUnknownProps = R.pipe(R.values, R.none(R.equals('unknown')), R.not); const outputColorReferences = (token: TransformedToken) => { if ( - R.test(/accent|neutral|brand1|brand2|brand3|success|danger|warning/, token.name) && + R.test(/accent|primary|support|neutral|brand1|brand2|brand3|success|danger|warning/, token.name) && R.includes('semantic/color', token.filePath) ) { return true; @@ -62,19 +62,16 @@ const outputColorReferences = (token: TransformedToken) => { export type IsCalculatedToken = (token: TransformedToken, options?: Config) => boolean; -export const permutateThemes = ($themes: ThemeObject[]) => - permutateThemes_($themes, { +export const permutateThemes = (groupedThemes: GroupedThemes) => + permutateThemes_(groupedThemes, { separator, }); -type GetConfig = (options: { - mode?: string; - theme?: string; - semantic?: string; - size?: string; - typography?: string; - outPath?: string; -}) => Config; +type GetConfig = ( + options: PermutationProps & { + outPath?: string; + }, +) => Config; export const colorModeVariables: GetConfig = ({ mode = 'light', outPath, theme }) => { const selector = `${mode === 'light' ? ':root, ' : ''}[data-ds-color-mode="${mode}"]`; @@ -99,7 +96,10 @@ export const colorModeVariables: GetConfig = ({ mode = 'light', outPath, theme } { destination: `color-mode/${mode}.css`, format: formats.colormode.name, - filter: (token) => !token.isSource && typeEquals('color', token), + filter: (token) => + !token.isSource && + typeEquals('color', token) && + !(['primary', 'support'] as const).some((category) => isColorCategoryToken(token, category)), }, ], options: { @@ -111,6 +111,62 @@ export const colorModeVariables: GetConfig = ({ mode = 'light', outPath, theme } }; }; +function isColorCategoryToken(token: TransformedToken, category: 'primary' | 'support') { + return R.startsWith(['color', category], token.path); +} + +export function capitalize(str: T): Capitalize { + if (!str) { + return str as Capitalize; + } + return (str[0].toUpperCase() + str.slice(1)) as Capitalize; +} + +export const colorCategories = + (category: 'primary' | 'support' = 'primary'): GetConfig => + ({ mode, outPath, theme, [`color${capitalize(category)}` as const]: color }) => { + const layer = `ds.theme.color-${category}`; + const optionalRootSelector = + (category === 'primary' && color === 'dominant') || (category === 'support' && color === 'support1') + ? ':root' + : undefined; + const dataSelector = `[data-ds-color-${category}="${color}"]`; + const selector = [optionalRootSelector, dataSelector].filter((x) => x !== undefined).join(', '); + + return { + usesDtcg, + preprocessors: ['tokens-studio'], + platforms: { + css: { + // custom + outPath, + mode, + theme, + selector, + layer, + // + prefix, + buildPath: `${outPath}/${theme}/`, + transforms: dsTransformers, + files: [ + { + destination: `color-${category}/${color}.css`, + format: formats.colorcategory.name, + filter: (token) => { + return !token.isSource && isColorCategoryToken(token, category); + }, + }, + ], + options: { + fileHeader, + outputReferences: (token, options) => + outputColorReferences(token) && outputReferencesFilter(token, options), + }, + }, + }, + }; + }; + export const semanticVariables: GetConfig = ({ outPath, theme }) => { const selector = `:root`; const layer = `ds.theme.semantic`; @@ -258,13 +314,15 @@ type getConfigs = ( tokensDir: string, themes: PermutatedThemes, logVerbosity: LogConfig['verbosity'], -) => { mode: string; theme: string; semantic: string; size: string; typography: string; config: Config }[]; +) => (PermutationProps & { config: Config })[]; export const getConfigs: getConfigs = (getConfig, outPath, tokensDir, permutatedThemes, logVerbosity) => permutatedThemes .map((permutatedTheme) => { const { selectedTokenSets = [], + colorPrimary = 'unknown', + colorSupport = 'unknown', mode = 'unknown', theme = 'unknown', semantic = 'unknown', @@ -284,6 +342,8 @@ export const getConfigs: getConfigs = (getConfig, outPath, tokensDir, permutated outPath, theme, mode, + colorPrimary, + colorSupport, semantic, size, typography, @@ -299,6 +359,6 @@ export const getConfigs: getConfigs = (getConfig, outPath, tokensDir, permutated include, }; - return { mode, theme, semantic, size, typography, config }; + return { mode, colorPrimary, colorSupport, theme, semantic, size, typography, config }; }) .sort(); diff --git a/packages/cli/src/tokens/build/formats/css.ts b/packages/cli/src/tokens/build/formats/css.ts index 9a269a7184..5cc1d219a8 100644 --- a/packages/cli/src/tokens/build/formats/css.ts +++ b/packages/cli/src/tokens/build/formats/css.ts @@ -44,6 +44,30 @@ export const colormode: Format = { }, }; +export const colorcategory: Format = { + name: 'ds/css-colorcategory', + format: async ({ dictionary, file, options, platform }) => { + const { allTokens } = dictionary; + const { outputReferences, usesDtcg } = options; + const { selector, mode, layer } = platform; + + const header = await fileHeader({ file }); + + const format = createPropertyFormatter({ + outputReferences, + dictionary, + format: 'css', + usesDtcg, + }); + + const formattedTokens = dictionary.allTokens.map(format).join('\n'); + const content = `{\n${formattedTokens}\n}\n`; + const body = R.isNotNil(layer) ? `@layer ${layer} {\n${selector} ${content}\n}\n` : `${selector} ${content}\n`; + + return header + body; + }, +}; + const calculatedVariable = R.pipe(R.split(/:(.*?);/g), (split) => `${split[0]}: calc(${R.trim(split[1])});`); export const semantic: Format = { diff --git a/packages/cli/src/tokens/build/utils/entryfile.ts b/packages/cli/src/tokens/build/utils/entryfile.ts index 3f316ad1b8..a6b6635021 100644 --- a/packages/cli/src/tokens/build/utils/entryfile.ts +++ b/packages/cli/src/tokens/build/utils/entryfile.ts @@ -16,6 +16,12 @@ const sortOrder = [ 'color-mode/dark', 'color-mode/contrast', 'typography/primary', + 'color-primary/dominant', + 'color-primary/complimentary', + 'color-primary/accent', + 'color-support/support1', + 'color-support/support2', + 'color-support/support3' ]; const sortByDefinedOrder = R.sortBy((fileName) => { diff --git a/packages/cli/src/tokens/build/utils/permutateThemes.ts b/packages/cli/src/tokens/build/utils/permutateThemes.ts index 290f0f03c4..128d218cfd 100644 --- a/packages/cli/src/tokens/build/utils/permutateThemes.ts +++ b/packages/cli/src/tokens/build/utils/permutateThemes.ts @@ -1,36 +1,36 @@ import type { ThemeObject } from '@tokens-studio/types'; import { TokenSetStatus } from '@tokens-studio/types'; +import { camelCase } from 'change-case'; import * as R from 'ramda'; declare interface Options { separator?: string; } +// Color group names +const PRIMARY_COLOR_GROUP = 'colorPrimary'; +const SUPPORT_COLOR_GROUP = 'colorSupport'; + +// TODO: Should we validate that these groups exist in $theme.json? +export type PermutationProps = { + mode: string; + [PRIMARY_COLOR_GROUP]: string; + [SUPPORT_COLOR_GROUP]: string; + semantic: string; + size: string; + theme: string; + typography: string; +}; + export type PermutatedTheme = { - mode?: string; - semantic?: string; - size?: string; - theme?: string; - typography?: string; name: string; selectedTokenSets: string[]; -}; +} & Partial; +export type GroupedThemes = Record; export type PermutatedThemes = PermutatedTheme[]; -export function permutateThemes(themes: ThemeObject[], { separator = '-' } = {} as Options): PermutatedThemes { - // Sort themes by groups - const groups: Record = {}; - for (const theme of themes) { - if (theme.group) { - groups[theme.group] = [...(groups[theme.group] ?? []), theme]; - } else { - throw new Error( - `Theme ${theme.name} does not have a group property, which is required for multi-dimensional theming.`, - ); - } - } - +export function permutateThemes(groups: GroupedThemes, { separator = '-' } = {} as Options): PermutatedThemes { // Create theme permutations const permutations = cartesian(Object.values(groups)) as Array; @@ -41,7 +41,7 @@ export function permutateThemes(themes: ThemeObject[], { separator = '-' } = {} let updatedPermutatedTheme = acc; if (group) { - const groupProp = R.lensProp(group.toLowerCase() as keyof PermutatedTheme); + const groupProp = R.lensProp(camelCase(group) as keyof PermutationProps); updatedPermutatedTheme = R.set(groupProp, name.toLowerCase(), updatedPermutatedTheme); } @@ -65,6 +65,20 @@ export function permutateThemes(themes: ThemeObject[], { separator = '-' } = {} return permutatedThemes; } +export function groupThemes(themes: ThemeObject[]): GroupedThemes { + const groups: GroupedThemes = {}; + for (const theme of themes) { + if (theme.group) { + groups[camelCase(theme.group)] = [...(groups[camelCase(theme.group)] ?? []), theme]; + } else { + throw new Error( + `Theme ${theme.name} does not have a group property, which is required for multi-dimensional theming.`, + ); + } + } + return groups; +} + function filterTokenSets(tokensets: Record) { return ( Object.entries(tokensets)