Skip to content

Commit

Permalink
feat(cli): build tokens for color-{primary,support} modes
Browse files Browse the repository at this point in the history
  • Loading branch information
unekinn committed Oct 13, 2024
1 parent 8335c06 commit 58ada36
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 42 deletions.
55 changes: 49 additions & 6 deletions packages/cli/src/tokens/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,21 @@ 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;
/** Enable verbose output */
verbose: boolean;
};

// type FormattedCSSPlatform = { css: { output: string; destination: string }[] };

const sd = new StyleDictionary();

export async function buildTokens(options: Options): Promise<void> {
Expand All @@ -40,14 +39,36 @@ export async function buildTokens(options: Options): Promise<void> {

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(
Expand Down Expand Up @@ -87,6 +108,28 @@ export async function buildTokens(options: Options): Promise<void> {
);
}

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')}`);

Expand Down
92 changes: 76 additions & 16 deletions packages/cli/src/tokens/build/configs.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 });
Expand All @@ -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);

Expand All @@ -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;
Expand All @@ -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}"]`;
Expand All @@ -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: {
Expand All @@ -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<T extends string>(str: T): Capitalize<T> {
if (!str) {
return str as Capitalize<T>;
}
return (str[0].toUpperCase() + str.slice(1)) as Capitalize<T>;
}

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`;
Expand Down Expand Up @@ -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',
Expand All @@ -284,6 +342,8 @@ export const getConfigs: getConfigs = (getConfig, outPath, tokensDir, permutated
outPath,
theme,
mode,
colorPrimary,
colorSupport,
semantic,
size,
typography,
Expand All @@ -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();
24 changes: 24 additions & 0 deletions packages/cli/src/tokens/build/formats/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/tokens/build/utils/entryfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>((fileName) => {
Expand Down
54 changes: 34 additions & 20 deletions packages/cli/src/tokens/build/utils/permutateThemes.ts
Original file line number Diff line number Diff line change
@@ -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<PermutationProps>;

export type GroupedThemes = Record<string, ThemeObject[]>;
export type PermutatedThemes = PermutatedTheme[];

export function permutateThemes(themes: ThemeObject[], { separator = '-' } = {} as Options): PermutatedThemes {
// Sort themes by groups
const groups: Record<string, ThemeObject[]> = {};
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<ThemeObject[]>;

Expand All @@ -41,7 +41,7 @@ export function permutateThemes(themes: ThemeObject[], { separator = '-' } = {}
let updatedPermutatedTheme = acc;

if (group) {
const groupProp = R.lensProp<PermutatedTheme>(group.toLowerCase() as keyof PermutatedTheme);
const groupProp = R.lensProp<PermutatedTheme>(camelCase(group) as keyof PermutationProps);
updatedPermutatedTheme = R.set(groupProp, name.toLowerCase(), updatedPermutatedTheme);
}

Expand All @@ -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<string, TokenSetStatus>) {
return (
Object.entries(tokensets)
Expand Down

0 comments on commit 58ada36

Please sign in to comment.