From e5d55223348515f185674d74dfcf00a235923304 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Mon, 18 Nov 2024 11:43:06 +0100 Subject: [PATCH] working on preprocessor --- .changeset/great-insects-act.md | 5 + .../adrs/adr-004-token-overrides.md | 8 +- scripts/buildFigma.ts | 34 +++-- scripts/buildTokens.ts | 17 ++- scripts/themes.config.ts | 23 ++- src/filters/isColor.ts | 3 - src/platforms/css.ts | 4 + src/platforms/docJson.ts | 6 +- src/platforms/figma.ts | 7 +- src/platforms/javascript.ts | 6 +- src/platforms/json.ts | 6 +- src/platforms/styleLint.ts | 6 +- src/platforms/typeDefinitions.ts | 6 +- src/platforms/typescript.ts | 6 +- src/preprocessor/themeOverrides.test.ts | 142 ++++++++++++++++++ src/preprocessor/themeOverrides.ts | 27 ++++ src/preprocessor/utilities/transformTokens.ts | 25 +++ src/primerStyleDictionary.ts | 3 + src/schemas/colorToken.ts | 26 ++++ src/test-utilities/getMockToken.ts | 2 +- src/transformers/figmaAttributes.ts | 2 +- src/types/styleDictionaryConfigGenerator.d.ts | 1 + src/types/tokenBuildInput.d.ts | 2 + 23 files changed, 331 insertions(+), 36 deletions(-) create mode 100644 .changeset/great-insects-act.md create mode 100644 src/preprocessor/themeOverrides.test.ts create mode 100644 src/preprocessor/themeOverrides.ts create mode 100644 src/preprocessor/utilities/transformTokens.ts diff --git a/.changeset/great-insects-act.md b/.changeset/great-insects-act.md new file mode 100644 index 000000000..2c4832110 --- /dev/null +++ b/.changeset/great-insects-act.md @@ -0,0 +1,5 @@ +--- +'@primer/primitives': minor +--- + +Adding new themeOverride preprocessor diff --git a/contributor-docs/adrs/adr-004-token-overrides.md b/contributor-docs/adrs/adr-004-token-overrides.md index ce23c1ae7..bba793239 100644 --- a/contributor-docs/adrs/adr-004-token-overrides.md +++ b/contributor-docs/adrs/adr-004-token-overrides.md @@ -2,10 +2,10 @@ ## Status -| Stage | Status | -| -------- | ----------- | -| Approved | ✅ | -| Adopted | 🚧 | +| Stage | Status | +| -------- | ------ | +| Approved | ✅ | +| Adopted | 🚧 | ## Context diff --git a/scripts/buildFigma.ts b/scripts/buildFigma.ts index f3a051b7a..534be0cc5 100644 --- a/scripts/buildFigma.ts +++ b/scripts/buildFigma.ts @@ -12,10 +12,12 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = const baseScales = [ { name: 'light', + theme: 'light', source: [`src/tokens/base/color/light/light.json5`, `src/tokens/base/color/light/display-light.json5`], }, { name: 'light-high-constrast', + theme: 'light-high-constrast', source: [ `src/tokens/base/color/light/light.json5`, `src/tokens/base/color/light/display-light.json5`, @@ -24,9 +26,11 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = }, { name: 'dark', + theme: 'dark', source: [`src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/display-dark.json5`], }, { + theme: 'dark-high-constrast', name: 'dark-high-constrast', source: [ `src/tokens/base/color/dark/dark.json5`, @@ -35,6 +39,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = ], }, { + theme: 'dark-dimmed', name: 'dark-dimmed', source: [ `src/tokens/base/color/dark/dark.json5`, @@ -62,7 +67,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = include, platforms: { figma: figma(`figma/themes/${filename}.json`, buildOptions.prefix, buildOptions.buildPath, { - mode: filename.replaceAll('-', ' '), + theme: filename.replaceAll('-', ' '), }), }, }) @@ -118,7 +123,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/light/primitives-light.json5`, `src/tokens/functional/color/light/patterns-light.json5`, ], - mode: 'light', + theme: 'light', }, { name: 'light-high-contrast', @@ -130,7 +135,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/light/primitives-light.json5`, `src/tokens/functional/color/light/patterns-light.json5`, ], - mode: 'light high contrast', + theme: 'light high contrast', }, { name: 'light-colorblind', @@ -142,7 +147,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/light/primitives-light.json5`, `src/tokens/functional/color/light/patterns-light.json5`, ], - mode: 'light colorblind', + theme: 'light colorblind', }, { name: 'light-tritanopia', @@ -154,7 +159,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/light/primitives-light.json5`, `src/tokens/functional/color/light/patterns-light.json5`, ], - mode: 'light tritanopia', + theme: 'light tritanopia', }, { name: 'dark', @@ -165,7 +170,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/dark/primitives-dark.json5`, `src/tokens/functional/color/dark/patterns-dark.json5`, ], - mode: 'dark', + theme: 'dark', }, { name: 'dark-high-contrast', @@ -177,7 +182,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/dark/primitives-dark.json5`, `src/tokens/functional/color/dark/patterns-dark.json5`, ], - mode: 'dark high contrast', + theme: 'dark high contrast', }, { name: 'dark-dimmed', @@ -189,7 +194,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/dark/primitives-dark.json5`, `src/tokens/functional/color/dark/patterns-dark.json5`, ], - mode: 'dark dimmed', + theme: 'dark dimmed', }, { name: 'dark-colorblind', @@ -201,7 +206,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/dark/primitives-dark.json5`, `src/tokens/functional/color/dark/patterns-dark.json5`, ], - mode: 'dark colorblind', + theme: 'dark colorblind', }, { name: 'dark-tritanopia', @@ -213,16 +218,16 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = `src/tokens/functional/color/dark/primitives-dark.json5`, `src/tokens/functional/color/dark/patterns-dark.json5`, ], - mode: 'dark tritanopia', + theme: 'dark tritanopia', }, ] // - for (const {name, source, include, mode} of shadowFiles) { + for (const {name, source, include, theme} of shadowFiles) { const extended = await PrimerStyleDictionary.extend({ source, include, platforms: { - figma: figma(`figma/shadows/${name}.json`, buildOptions.prefix, buildOptions.buildPath, {mode}), + figma: figma(`figma/shadows/${name}.json`, buildOptions.prefix, buildOptions.buildPath, {theme}), }, }) @@ -247,6 +252,7 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = group: string name: string }> = files.flatMap(filePath => JSON.parse(fs.readFileSync(filePath, 'utf8'))) + // create a list of groups with collections and modes const collections: Record< string, @@ -257,6 +263,9 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = > = {} for (const {collection, mode, group} of tokens) { + if (!collection) { + continue + } if (!(collection in collections)) { collections[collection] = { modes: [], @@ -271,7 +280,6 @@ const buildFigma = async (buildOptions: ConfigGeneratorOptions): Promise = collections[collection].groups.push(group) } } - // define the order of the modes // we inverse it to deal with the -1 of the indexOf if item is not found in the array: basically anything that is not in the list should come last const modeOrder = [ diff --git a/scripts/buildTokens.ts b/scripts/buildTokens.ts index 54cf247aa..7dc017f3d 100644 --- a/scripts/buildTokens.ts +++ b/scripts/buildTokens.ts @@ -37,9 +37,16 @@ const getStyleDictionaryConfig: StyleDictionaryConfigGenerator = ( }, platforms: Object.fromEntries( Object.entries({ - css: css(`css/${filename}.css`, options.prefix, options.buildPath, {themed: options.themed}), - docJson: docJson(`docs/${filename}.json`, options.prefix, options.buildPath), - styleLint: styleLint(`styleLint/${filename}.json`, options.prefix, options.buildPath), + css: css(`css/${filename}.css`, options.prefix, options.buildPath, { + themed: options.themed, + theme: options.theme, + }), + docJson: docJson(`docs/${filename}.json`, options.prefix, options.buildPath, { + theme: options.theme, + }), + styleLint: styleLint(`styleLint/${filename}.json`, options.prefix, options.buildPath, { + theme: options.theme, + }), fallbacks: fallbacks(`fallbacks/${filename}.json`, options.prefix, options.buildPath), ...platforms, }).filter((entry: [string, unknown]) => entry[1] !== undefined), @@ -71,14 +78,14 @@ export const buildDesignTokens = async (buildOptions: ConfigGeneratorOptions): P * Colors, shadows & borders * ----------------------------------- */ try { - for (const {filename, source, include} of themes) { + for (const {filename, source, include, theme} of themes) { // build functional scales const extendedSD = await PrimerStyleDictionary.extend( getStyleDictionaryConfig( `functional/themes/${filename}`, source, include, - {...buildOptions, themed: true}, + {...buildOptions, themed: true, theme}, // disable fallbacks for themes {fallbacks: undefined}, ), diff --git a/scripts/themes.config.ts b/scripts/themes.config.ts index 0b54d9d3a..7f7af978c 100644 --- a/scripts/themes.config.ts +++ b/scripts/themes.config.ts @@ -3,40 +3,48 @@ import type {TokenBuildInput} from '../src/types/tokenBuildInput.js' export const themes: TokenBuildInput[] = [ { filename: 'light', + theme: 'light', source: [ `src/tokens/functional/color/light/*.json5`, `src/tokens/functional/shadow/light.json5`, `src/tokens/functional/border/light.json5`, + `src/tokens/functional/color/components/*.json5`, ], include: [`src/tokens/base/color/light/light.json5`, `src/tokens/base/color/light/display-light.json5`], }, { filename: 'light-tritanopia', + theme: 'light-tritanopia', source: [ `src/tokens/functional/color/light/*.json5`, `src/tokens/functional/shadow/light.json5`, `src/tokens/functional/border/light.json5`, + `src/tokens/functional/color/components/*.json5`, `src/tokens/functional/color/light/overrides/light.tritanopia.json5`, ], include: [`src/tokens/base/color/light/light.json5`, `src/tokens/base/color/light/display-light.json5`], }, { filename: 'light-colorblind', + theme: 'light-protanopia-deuteranopia', source: [ `src/tokens/functional/color/light/*.json5`, `src/tokens/functional/shadow/light.json5`, `src/tokens/functional/border/light.json5`, + `src/tokens/functional/color/components/*.json5`, `src/tokens/functional/color/light/overrides/light.protanopia-deuteranopia.json5`, ], include: [`src/tokens/base/color/light/light.json5`, `src/tokens/base/color/light/display-light.json5`], }, { filename: 'light-high-contrast', + theme: 'light-high-contrast', source: [ `src/tokens/functional/color/light/*.json5`, - `src/tokens/functional/color/light/overrides/light.high-contrast.json5`, `src/tokens/functional/shadow/light.json5`, `src/tokens/functional/border/light.json5`, + `src/tokens/functional/color/components/*.json5`, + `src/tokens/functional/color/light/overrides/light.high-contrast.json5`, ], include: [ `src/tokens/base/color/light/light.json5`, @@ -46,54 +54,65 @@ export const themes: TokenBuildInput[] = [ }, { filename: 'dark', + theme: 'dark', source: [ `src/tokens/functional/color/dark/*.json5`, `src/tokens/functional/shadow/dark.json5`, `src/tokens/functional/border/dark.json5`, + `src/tokens/functional/color/components/*.json5`, ], include: [`src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/display-dark.json5`], }, { filename: 'dark-dimmed', + theme: 'dark-dimmed', source: [ `src/tokens/functional/color/dark/*.json5`, `src/tokens/functional/color/dark/overrides/dark.dimmed.json5`, `src/tokens/functional/shadow/dark.json5`, `src/tokens/functional/border/dark.json5`, + `src/tokens/functional/color/components/*.json5`, ], include: [ `src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/dark.dimmed.json5`, `src/tokens/base/color/dark/display-dark.json5`, + `src/tokens/functional/color/components/*.json5`, ], }, { filename: 'dark-tritanopia', + theme: 'dark-tritanopia', source: [ `src/tokens/functional/color/dark/*.json5`, `src/tokens/functional/shadow/dark.json5`, `src/tokens/functional/border/dark.json5`, + `src/tokens/functional/color/components/*.json5`, `src/tokens/functional/color/dark/overrides/dark.tritanopia.json5`, ], include: [`src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/display-dark.json5`], }, { filename: 'dark-colorblind', + theme: 'dark-protanopia-deuteranopia', source: [ `src/tokens/functional/color/dark/*.json5`, `src/tokens/functional/shadow/dark.json5`, `src/tokens/functional/border/dark.json5`, + `src/tokens/functional/color/components/*.json5`, `src/tokens/functional/color/dark/overrides/dark.protanopia-deuteranopia.json5`, ], include: [`src/tokens/base/color/dark/dark.json5`, `src/tokens/base/color/dark/display-dark.json5`], }, { filename: 'dark-high-contrast', + theme: 'dark-high-contrast', source: [ `src/tokens/functional/color/dark/*.json5`, - `src/tokens/functional/color/dark/overrides/dark.high-contrast.json5`, `src/tokens/functional/shadow/dark.json5`, `src/tokens/functional/border/dark.json5`, + `src/tokens/functional/color/components/*.json5`, + `src/tokens/functional/color/dark/overrides/dark.high-contrast.json5`, ], include: [ `src/tokens/base/color/dark/dark.json5`, diff --git a/src/filters/isColor.ts b/src/filters/isColor.ts index f32e7ee90..cbbed8333 100644 --- a/src/filters/isColor.ts +++ b/src/filters/isColor.ts @@ -7,8 +7,5 @@ import type {TransformedToken} from 'style-dictionary/types' */ export const isColor = (token: TransformedToken): boolean => { const typeValue = token.$type ?? token.type - // if (token.path.join('-') === 'control-transparent-bgColor-selected') { - // console.log(typeValue === 'color', token) - // } return typeValue === 'color' } diff --git a/src/platforms/css.ts b/src/platforms/css.ts index f2a339e84..0af9084ac 100644 --- a/src/platforms/css.ts +++ b/src/platforms/css.ts @@ -24,6 +24,7 @@ export const css: PlatformInitializer = (outputFile, prefix, buildPath, options) return { prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'name/pathToKebabCase', 'color/hex', @@ -39,6 +40,9 @@ export const css: PlatformInitializer = (outputFile, prefix, buildPath, options) ], options: { basePxFontSize: 16, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/platforms/docJson.ts b/src/platforms/docJson.ts index ca55ba44b..43dc06167 100644 --- a/src/platforms/docJson.ts +++ b/src/platforms/docJson.ts @@ -2,9 +2,10 @@ import type {PlatformInitializer} from '../types/platformInitializer.js' import {isSource} from '../filters/index.js' import type {PlatformConfig} from 'style-dictionary/types' -export const docJson: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const docJson: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'name/pathToKebabCase', 'color/hex', @@ -23,6 +24,9 @@ export const docJson: PlatformInitializer = (outputFile, prefix, buildPath): Pla $type: 'type', $description: 'description', }, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/platforms/figma.ts b/src/platforms/figma.ts index c8016e842..5164eb7f9 100644 --- a/src/platforms/figma.ts +++ b/src/platforms/figma.ts @@ -23,6 +23,7 @@ const validFigmaToken = async (token: TransformedToken, options: Config) => { export const figma: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'color/rgbaFloat', 'fontFamily/figma', @@ -42,7 +43,11 @@ export const figma: PlatformInitializer = (outputFile, prefix, buildPath, option 'fontStack/sansSerifDisplay': 'SF Pro Display', 'fontStack/monospace': 'SF Mono', }, + // should this object be spread here? ...options, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { @@ -53,7 +58,7 @@ export const figma: PlatformInitializer = (outputFile, prefix, buildPath, option format: `json/figma`, options: { outputReferences: true, - mode: options?.mode, + theme: options?.theme, }, }, ], diff --git a/src/platforms/javascript.ts b/src/platforms/javascript.ts index 3013a69d1..2e1a8c407 100644 --- a/src/platforms/javascript.ts +++ b/src/platforms/javascript.ts @@ -2,9 +2,10 @@ import type {PlatformInitializer} from '../types/platformInitializer.js' import type {PlatformConfig} from 'style-dictionary/types' import {isSource} from '../filters/index.js' -export const javascript: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const javascript: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'color/hex', 'color/hexMix', @@ -18,6 +19,9 @@ export const javascript: PlatformInitializer = (outputFile, prefix, buildPath): options: { showFileHeader: false, basePxFontSize: 16, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/platforms/json.ts b/src/platforms/json.ts index 74437dc62..62cb256e5 100644 --- a/src/platforms/json.ts +++ b/src/platforms/json.ts @@ -2,9 +2,10 @@ import type {PlatformInitializer} from '../types/platformInitializer.js' import {isSource} from '../filters/index.js' import type {PlatformConfig} from 'style-dictionary/types' -export const json: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const json: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'color/hex', 'color/hexMix', @@ -17,6 +18,9 @@ export const json: PlatformInitializer = (outputFile, prefix, buildPath): Platfo ], options: { basePxFontSize: 16, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/platforms/styleLint.ts b/src/platforms/styleLint.ts index a90a273e3..1d273bac6 100644 --- a/src/platforms/styleLint.ts +++ b/src/platforms/styleLint.ts @@ -2,9 +2,10 @@ import type {PlatformInitializer} from '../types/platformInitializer.js' import {isSource} from '../filters/index.js' import type {PlatformConfig} from 'style-dictionary/types' -export const styleLint: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const styleLint: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'name/pathToKebabCase', 'color/hex', @@ -18,6 +19,9 @@ export const styleLint: PlatformInitializer = (outputFile, prefix, buildPath): P ], options: { basePxFontSize: 16, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/platforms/typeDefinitions.ts b/src/platforms/typeDefinitions.ts index d68358a79..3a8d7b0f1 100644 --- a/src/platforms/typeDefinitions.ts +++ b/src/platforms/typeDefinitions.ts @@ -3,9 +3,10 @@ import {isSource} from '../filters/index.js' import {upperCaseFirstCharacter} from '../utilities/index.js' import type {PlatformConfig} from 'style-dictionary/types' -export const typeDefinitions: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const typeDefinitions: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'color/hex', 'shadow/css', @@ -23,6 +24,9 @@ export const typeDefinitions: PlatformInitializer = (outputFile, prefix, buildPa options: { tokenTypesPath: './src/types/', moduleName: `${upperCaseFirstCharacter(outputFile)}DesignTokens`, + themeOverrides: { + theme: options?.theme, + }, }, }, ], diff --git a/src/platforms/typescript.ts b/src/platforms/typescript.ts index 75e7f0d50..97afff985 100644 --- a/src/platforms/typescript.ts +++ b/src/platforms/typescript.ts @@ -2,9 +2,10 @@ import type {PlatformInitializer} from '../types/platformInitializer.js' import type {PlatformConfig} from 'style-dictionary/types' import {isSource} from '../filters/index.js' -export const typescript: PlatformInitializer = (outputFile, prefix, buildPath): PlatformConfig => ({ +export const typescript: PlatformInitializer = (outputFile, prefix, buildPath, options): PlatformConfig => ({ prefix, buildPath, + preprocessors: ['themeOverrides'], transforms: [ 'color/hex', 'color/hexMix', @@ -18,6 +19,9 @@ export const typescript: PlatformInitializer = (outputFile, prefix, buildPath): options: { showFileHeader: false, basePxFontSize: 16, + themeOverrides: { + theme: options?.theme, + }, }, files: [ { diff --git a/src/preprocessor/themeOverrides.test.ts b/src/preprocessor/themeOverrides.test.ts new file mode 100644 index 000000000..02d11c0c6 --- /dev/null +++ b/src/preprocessor/themeOverrides.test.ts @@ -0,0 +1,142 @@ +import {getMockDictionary} from '../test-utilities/getMockDictionary.js' +import {themeOverrides} from './themeOverrides.js' +import {getMockToken} from '../test-utilities/getMockToken.js' + +describe('Preprocessor: themeOverrides', () => { + it('works with default settings', () => { + const dictionary = getMockDictionary({ + valueOverride: getMockToken({ + name: 'red', + description: 'This is a description', + $value: 'transformedValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + 'org.primer.overrides': { + dark: 'darkValue', + }, + }, + }), + objectOverride: getMockToken({ + name: 'red', + description: 'This is a description', + $value: 'transformedValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + 'org.primer.overrides': { + dark: { + $value: 'darkValue', + description: 'DarkMode description', + }, + }, + }, + }), + }) + + const resultDictionary = getMockDictionary({ + valueOverride: getMockToken({ + name: 'red', + description: 'This is a description', + $value: 'darkValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + 'org.primer.overrides': { + dark: 'darkValue', + }, + }, + }), + objectOverride: getMockToken({ + name: 'red', + description: 'DarkMode description', + $value: 'darkValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + 'org.primer.overrides': { + dark: { + $value: 'darkValue', + description: 'DarkMode description', + }, + }, + }, + }), + }) + + expect(themeOverrides.preprocessor(dictionary.tokens, {})).toStrictEqual(dictionary.tokens) + expect( + themeOverrides.preprocessor(dictionary.tokens, { + options: {themeOverrides: {theme: 'dark'}}, + }), + ).toStrictEqual(resultDictionary.tokens) + }) + + it('works with custom configuration', () => { + const dictionary = getMockDictionary({ + valueOverride: getMockToken({ + name: 'red', + description: 'This is a description', + value: 'transformedValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + theme: { + dark: 'darkValue', + }, + }, + }), + objectOverride: getMockToken({ + name: 'red', + description: 'This is a description', + value: 'transformedValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + theme: { + dark: { + value: 'darkValue', + description: 'DarkMode description', + }, + }, + }, + }), + }) + + const resultDictionary = getMockDictionary({ + valueOverride: getMockToken({ + name: 'red', + description: 'This is a description', + value: 'darkValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + theme: { + dark: 'darkValue', + }, + }, + }), + objectOverride: getMockToken({ + name: 'red', + description: 'DarkMode description', + value: 'darkValue', + path: ['tokens', 'subgroup', 'red'], + $extensions: { + theme: { + dark: { + value: 'darkValue', + description: 'DarkMode description', + }, + }, + }, + }), + }) + + expect( + themeOverrides.preprocessor(dictionary.tokens, { + themeOverrides: { + valueProp: 'value', + extensionProp: 'theme', + }, + }), + ).toStrictEqual(dictionary.tokens) + expect( + themeOverrides.preprocessor(dictionary.tokens, { + options: {themeOverrides: {theme: 'dark', valueProp: 'value', extensionProp: 'theme'}}, + }), + ).toStrictEqual(resultDictionary.tokens) + }) +}) diff --git a/src/preprocessor/themeOverrides.ts b/src/preprocessor/themeOverrides.ts new file mode 100644 index 000000000..5b45adf89 --- /dev/null +++ b/src/preprocessor/themeOverrides.ts @@ -0,0 +1,27 @@ +import type {PlatformConfig, PreprocessedTokens, Preprocessor} from 'style-dictionary/types' +import {transformTokens} from './utilities/transformTokens.js' + +export const themeOverrides: Preprocessor = { + name: 'themeOverrides', + preprocessor: (dictionary: PreprocessedTokens, config: PlatformConfig): PreprocessedTokens => { + const extensionProp = config.options?.themeOverrides?.extensionProp || 'org.primer.overrides' + const valueProp = config.options?.themeOverrides?.valueProp || '$value' + const currentTheme = config.options?.themeOverrides?.theme + + const tokens = transformTokens(dictionary, token => { + // return early if no theme value is set + if (!currentTheme || !token.$extensions?.[extensionProp] || !token.$extensions?.[extensionProp][currentTheme]) { + return token + } + + // get override + const override = token.$extensions?.[extensionProp][currentTheme] + // token an theme value exist + return { + ...token, + ...(typeof override === 'object' ? override : {[valueProp]: override}), + } + }) + return tokens + }, +} diff --git a/src/preprocessor/utilities/transformTokens.ts b/src/preprocessor/utilities/transformTokens.ts new file mode 100644 index 000000000..63413957c --- /dev/null +++ b/src/preprocessor/utilities/transformTokens.ts @@ -0,0 +1,25 @@ +import type {DesignToken} from 'style-dictionary/types' +/** + * jsonToNestedValue + * @description creates a nested json tree where every final value is the `.value` prop + * @param token StyleDictionary.DesignToken + * @returns nested json three + */ +export const transformTokens = ( + token: DesignToken | Record, + transform: (token: DesignToken) => DesignToken, +) => { + // is non-object value + if (typeof token !== 'object') return token + // is design token + if ('$value' in token || 'value' in token) { + return transform(token as DesignToken) + } + // is obj + const nextObj = {} + for (const [prop, value] of Object.entries(token)) { + // @ts-expect-error: can't predict type + nextObj[prop] = transformTokens(value, transform) + } + return nextObj +} diff --git a/src/primerStyleDictionary.ts b/src/primerStyleDictionary.ts index 1a57e7b4a..57c76b7a6 100644 --- a/src/primerStyleDictionary.ts +++ b/src/primerStyleDictionary.ts @@ -37,6 +37,7 @@ import { cssAdvanced, jsonFigma, } from './formats/index.js' +import {themeOverrides} from './preprocessor/themeOverrides.js' /** * @name {@link PrimerStyleDictionary} @@ -150,3 +151,5 @@ PrimerStyleDictionary.registerTransform(fontWeightToNumber) PrimerStyleDictionary.registerTransform(fontFamilyToCss) PrimerStyleDictionary.registerTransform(fontFamilyToFigma) + +PrimerStyleDictionary.registerPreprocessor(themeOverrides) diff --git a/src/schemas/colorToken.ts b/src/schemas/colorToken.ts index c3d9df389..9de338619 100644 --- a/src/schemas/colorToken.ts +++ b/src/schemas/colorToken.ts @@ -48,6 +48,32 @@ export const colorToken = baseToken group: z.string().optional(), }) .optional(), + 'org.primer.overrides': z + .object( + { + light: z.union([colorHexValue, referenceValue]).optional(), + 'light-tritanopia': z.union([colorHexValue, referenceValue]).optional(), + 'light-deutranopia-protanopia': z.union([colorHexValue, referenceValue]).optional(), + 'light-high-contrast': z.union([colorHexValue, referenceValue]).optional(), + dark: z.union([colorHexValue, referenceValue]).optional(), + 'dark-tritanopia': z.union([colorHexValue, referenceValue]).optional(), + 'dark-deutranopia-protanopia': z.union([colorHexValue, referenceValue]).optional(), + 'dark-high-contrast': z.union([colorHexValue, referenceValue]).optional(), + 'dark-dimmed': z.union([colorHexValue, referenceValue]).optional(), + }, + { + errorMap: e => { + if (e.code === 'unrecognized_keys') { + return { + message: `Unrecognized key: "${e.keys.join(', ')}", must be one of: light, light-tritanopia, light-deutranopia-protanopia, light-high-contrast, dark, dark-tritanopia, dark-deutranopia-protanopia, dark-high-contrast, dark-dimmed`, + } + } + return {message: `Error: ${e.code}`} + }, + }, + ) + .strict() + .optional(), }) .optional(), }) diff --git a/src/test-utilities/getMockToken.ts b/src/test-utilities/getMockToken.ts index 4ebd349d2..885e29fd2 100644 --- a/src/test-utilities/getMockToken.ts +++ b/src/test-utilities/getMockToken.ts @@ -1,4 +1,4 @@ -import {TransformedToken} from 'style-dictionary/types' +import type {TransformedToken} from 'style-dictionary/types' const mockTokenDefaults = { name: 'tokenName', diff --git a/src/transformers/figmaAttributes.ts b/src/transformers/figmaAttributes.ts index 505e66e26..3cc8bb61c 100644 --- a/src/transformers/figmaAttributes.ts +++ b/src/transformers/figmaAttributes.ts @@ -70,7 +70,7 @@ export const figmaAttributes: Transform = { transform: (token: TransformedToken, platform: PlatformConfig = {}) => { const {modeOverride, collection, scopes, group, codeSyntax} = token.$extensions?.['org.primer.figma'] || {} return { - mode: modeOverride || platform.options?.mode || 'default', + mode: platform.options?.theme || modeOverride || 'default', collection, group: group || collection, scopes: getScopes(scopes), diff --git a/src/types/styleDictionaryConfigGenerator.d.ts b/src/types/styleDictionaryConfigGenerator.d.ts index ffa53f330..9990e4ec2 100644 --- a/src/types/styleDictionaryConfigGenerator.d.ts +++ b/src/types/styleDictionaryConfigGenerator.d.ts @@ -4,6 +4,7 @@ export type ConfigGeneratorOptions = { buildPath: string prefix?: string themed?: boolean + theme?: string } export type StyleDictionaryConfigGenerator = ( diff --git a/src/types/tokenBuildInput.d.ts b/src/types/tokenBuildInput.d.ts index fc00fe31d..6cc65bf42 100644 --- a/src/types/tokenBuildInput.d.ts +++ b/src/types/tokenBuildInput.d.ts @@ -3,6 +3,8 @@ export type TokenBuildInput = { filename: string // Array of `filepaths` to token files that should be converted and included in the output. Accepts relative or glob paths. source: string[] + // The mode of the theme + theme?: string // Array of `filepaths` to token fils that should NOT be included in the output, but should be available to reference during compilation e.g. base color scales include: string[] }