From 1b7ed3b5a2edfc60ecd9af50103b7192ec457456 Mon Sep 17 00:00:00 2001 From: Imar Abreu Date: Wed, 20 Dec 2023 14:48:30 +0000 Subject: [PATCH] Feature/dark mode (#54) * Move the generate style variables to StyleDictionary * Update README * Handle no style dictionary config * Changeset * Improve colors json struct * Fix tests * Up changeset to major * Add support for CSS dark mode * Update Readme and template * Refactor * Changeset --- .changeset/honest-bats-mate.md | 8 ++ README.md | 11 ++- src/api/figmaApiConnector.ts | 4 +- src/api/parseFigma.ts | 4 +- src/bin/designTokens.ts | 4 +- src/functions/getTokens.ts | 10 +-- src/functions/index.ts | 2 +- src/index.ts | 13 +-- src/styleDictionary/StyleDictionary.ts | 21 ----- src/styleDictionary/styleDictionary.ts | 110 +++++++++++++++++++++++++ src/tokens/Colors.ts | 36 +++++++- src/types/designTokens/config.ts | 6 +- src/types/designTokens/tokens.ts | 2 +- template.config.json | 55 +++++++++---- 14 files changed, 222 insertions(+), 64 deletions(-) create mode 100644 .changeset/honest-bats-mate.md delete mode 100644 src/styleDictionary/StyleDictionary.ts create mode 100644 src/styleDictionary/styleDictionary.ts diff --git a/.changeset/honest-bats-mate.md b/.changeset/honest-bats-mate.md new file mode 100644 index 0000000..43c4314 --- /dev/null +++ b/.changeset/honest-bats-mate.md @@ -0,0 +1,8 @@ +--- +'@runroom/design-tokens': major +--- + +- Move the `StyleDictionary` config to a `styleDictionary` entry in the design tokens config +- Add support for dark mode in the CSS variables with the `darkMode` and the `darkModeStyleDictionary` entries in the + design tokens config +- Remove `figmaThemes` diff --git a/README.md b/README.md index 0cf9560..f55cc41 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,15 @@ array of the figma frames that contains the tokens `outputDir`: The source where will be stored the package's output -`figmaThemes`: An array of your's figma project themes. If you aren't usign themes, just remove the key (first element of the -array, will be setted as default) +`darkMode`: Boolean to enable/disable the dark mode support + ### Optional config fields for [Style Dictionary](https://amzn.github.io/style-dictionary/#/config) +`styleDictionary`: The style dictionary config + +`darkModeStyleDictionary`: The style dictionary config for dark mode files **only works with CSS variables** + ### Execution Then execute: @@ -77,6 +81,9 @@ Expanding the design tokens in your project is a straightforward process. Here's - In the `src/types/figma` directory, define types that reflect the structure of the new tokens in Figma. - In the `src/types/designTokens` directory, define types specific to how the tokens will be used. +6. **Add Token to Style Dictionary:** + - If the token needs a specific parser, add it to the `src/styleDictionary/styleDictionary.ts` file. + ## License [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) diff --git a/src/api/figmaApiConnector.ts b/src/api/figmaApiConnector.ts index af16630..fe6a532 100644 --- a/src/api/figmaApiConnector.ts +++ b/src/api/figmaApiConnector.ts @@ -22,7 +22,7 @@ const figmaApiConnection = async ({ figmaApiKey, figmaProjectId, figmaPages, - figmaThemes + darkMode }: Config): Promise => { log('Connecting with Figma...', EMOJIS.workingInProgress); @@ -46,7 +46,7 @@ const figmaApiConnection = async ({ throw new Error(`No styles found`); } - const parsedTokens = parseFigma(responseJson, figmaPages, figmaThemes); + const parsedTokens = parseFigma(responseJson, figmaPages, darkMode); if (!parsedTokens || !parsedTokens.length) { throw new Error(`No styles found`); diff --git a/src/api/parseFigma.ts b/src/api/parseFigma.ts index ef9b2f9..219d54a 100644 --- a/src/api/parseFigma.ts +++ b/src/api/parseFigma.ts @@ -3,7 +3,7 @@ import { FigmaResponse } from '@/types/figma'; import { generateDesignTokens } from '@/functions/getTokens.ts'; import { DesignTokensGenerator, FigmaPages } from '@/types/designTokens'; -const parseFigma = (response: FigmaResponse, FIGMA_PAGES: FigmaPages, themes?: string[]) => { +const parseFigma = (response: FigmaResponse, FIGMA_PAGES: FigmaPages, darkMode?: boolean) => { if (!response) { throw new Error(`\x1b[31m\n\n${EMOJIS.error} No styles found\n`); } @@ -30,7 +30,7 @@ const parseFigma = (response: FigmaResponse, FIGMA_PAGES: FigmaPages, themes?: s } const figmaDesignTokensFrames = figmaPage.children; - const generateTokens = generateDesignTokens(frames, figmaDesignTokensFrames, themes); + const generateTokens = generateDesignTokens(frames, figmaDesignTokensFrames, darkMode); tokens.push(...generateTokens); } diff --git a/src/bin/designTokens.ts b/src/bin/designTokens.ts index 63bf55a..3cbe3b4 100755 --- a/src/bin/designTokens.ts +++ b/src/bin/designTokens.ts @@ -2,11 +2,11 @@ import parserRuntime from 'yargs-parser'; import designTokens from '@/index.ts'; -import { logError, configFileParser } from '@/functions'; +import { configFileParser, logError } from '@/functions'; import fs from 'fs'; const args = parserRuntime(process.argv.slice(2)); configFileParser(args, fs) - .then(config => designTokens(args, config)) + .then(config => designTokens(config)) .catch(err => logError(err)); diff --git a/src/functions/getTokens.ts b/src/functions/getTokens.ts index c79f679..ae01fe7 100644 --- a/src/functions/getTokens.ts +++ b/src/functions/getTokens.ts @@ -92,20 +92,20 @@ const designTokensBuilder = ( name: DesignPages, pages: string[], frame: FigmaFrame, - designToken: ({ frame, themes }: TokenPayload) => T, - themes?: string[] + designToken: ({ frame, darkMode }: TokenPayload) => T, + darkMode?: boolean ): T | undefined => { if (!pages.includes(name)) { return; } - return designToken({ frame, themes }); + return designToken({ frame, darkMode }); }; const generateDesignTokens = ( pages: string[], figmaDesignTokensFrames: FigmaFrame[], - themes?: string[] + darkMode?: boolean ) => { const writeFilePromises: DesignTokensGenerator[] = figmaDesignTokensFrames .map(frame => { @@ -120,7 +120,7 @@ const generateDesignTokens = ( pages, frame, tokenGenerator, - themes + darkMode ); }) .filter(truthy); diff --git a/src/functions/index.ts b/src/functions/index.ts index bede083..2de92a9 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -5,6 +5,6 @@ export * from './fileIO.ts'; export * from './getTokens.ts'; export * from './logger.ts'; export * from './stringManipulation.ts'; -export * from '../styleDictionary/StyleDictionary.ts'; +export * from '../styleDictionary/styleDictionary.ts'; export * from './unitsConvert.ts'; export * from './ensureType.ts'; diff --git a/src/index.ts b/src/index.ts index 1ab6160..837f96f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,17 @@ -import { buildStyleDictionary } from '@/styleDictionary/StyleDictionary.ts'; -import { Arguments } from 'yargs-parser'; +import { buildStyleDictionary } from '@/styleDictionary/styleDictionary.ts'; import { ParseConfig } from '@/types/designTokens'; import { figmaApiConnection } from '@/api'; import { createJsonTokenFiles, EMOJIS, log } from '@/functions'; -const designTokens = (args: Arguments, config: ParseConfig) => { - const { settings, configFile } = config; +const designTokens = (config: ParseConfig) => { + const { settings } = config; figmaApiConnection(settings).then(async generatedTokens => { log('Generating design tokens...', EMOJIS.workingInProgress); await createJsonTokenFiles(generatedTokens, settings); - if (settings.platforms) { - buildStyleDictionary(configFile); - } else { - log('No Style Dictionary config found', EMOJIS.warning); - } + buildStyleDictionary(settings); }); }; diff --git a/src/styleDictionary/StyleDictionary.ts b/src/styleDictionary/StyleDictionary.ts deleted file mode 100644 index ec80c19..0000000 --- a/src/styleDictionary/StyleDictionary.ts +++ /dev/null @@ -1,21 +0,0 @@ -import StyleDictionary from 'style-dictionary'; -import { EMOJIS, log } from '@/functions'; -import { getGradientsParser, getShadowsParser, getTypographiesParser } from '@/tokens'; - -const registerParsers = () => { - StyleDictionary.registerParser(getTypographiesParser()); - StyleDictionary.registerParser(getShadowsParser()); - StyleDictionary.registerParser(getGradientsParser()); -}; - -const buildStyleDictionary = (configFilePath: string) => { - registerParsers(); - - const extendedDictionary = StyleDictionary.extend(configFilePath); - - log('Compiling styles...', EMOJIS.workingInProgress); - extendedDictionary.buildAllPlatforms(); - log('Styles compiled', EMOJIS.success); -}; - -export { buildStyleDictionary }; diff --git a/src/styleDictionary/styleDictionary.ts b/src/styleDictionary/styleDictionary.ts new file mode 100644 index 0000000..d1dc5ec --- /dev/null +++ b/src/styleDictionary/styleDictionary.ts @@ -0,0 +1,110 @@ +import StyleDictionary, { Config as StyleDictionaryConfig } from 'style-dictionary'; +import { EMOJIS, log } from '@/functions'; +import { getGradientsParser, getShadowsParser, getTypographiesParser } from '@/tokens'; +import { Config } from '@/types/designTokens'; + +const registerParsers = () => { + StyleDictionary.registerParser(getTypographiesParser()); + StyleDictionary.registerParser(getShadowsParser()); + StyleDictionary.registerParser(getGradientsParser()); +}; + +const getDarkModeSource = (source?: string[], outputDir?: string) => { + const defaultJsonPath = `${outputDir}/**/*.dark.json`; + + if (!source) { + return [defaultJsonPath]; + } + + return source.map(path => { + if (path.indexOf(`.dark.json`) > -1) { + return path; + } + + return `${path.replace(`.json`, `.dark.json`)}`; + }); +}; + +const getDarkModeStyleDictionaryDefaultConfig = (settings: Config) => { + const source = getDarkModeSource(settings.styleDictionary?.source, settings.outputDir); + const buildPath = + settings.styleDictionary!.platforms.css.buildPath ?? `${settings.outputDir}/tokens/`; + + return { + include: source, + source, + filter: { + dark: ({ filePath }: { filePath: string }) => filePath.indexOf(`.dark`) > -1 + }, + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `variables-dark.css`, + format: `css/variables` + } + ] + } + } + }; +}; + +const buildDarkModeStyles = (settings: Config) => { + const darkModeConfig = + settings.darkModeStyleDictionary ?? getDarkModeStyleDictionaryDefaultConfig(settings); + + const extendedDictionary = StyleDictionary.extend(darkModeConfig); + + log('Compiling dark mode styles...', EMOJIS.workingInProgress); + extendedDictionary.buildAllPlatforms(); + log('Dark mode styles compiled', EMOJIS.success); +}; + +const buildStyles = (styleDictionary: StyleDictionaryConfig) => { + const extendedDictionary = StyleDictionary.extend(styleDictionary); + + log('Compiling styles...', EMOJIS.workingInProgress); + extendedDictionary.buildAllPlatforms(); + log('Styles compiled', EMOJIS.success); +}; + +const getStyleDictionaryWithoutDarkFiles = ( + styleDictionary: StyleDictionaryConfig +): StyleDictionaryConfig => { + const source = styleDictionary.source + ? styleDictionary.source.map(sourcePath => sourcePath.replace('*.json', '!(*.dark).json')) + : []; + + return { + ...styleDictionary, + source, + filter: { + ...styleDictionary.filter, + dark: ({ filePath }: { filePath: string }) => filePath.indexOf(`.dark`) === -1 + } + }; +}; + +const buildStyleDictionary = (settings: Config) => { + if (!settings.styleDictionary) { + log('No Style Dictionary config found', EMOJIS.warning); + return; + } + + registerParsers(); + + const { darkMode, styleDictionary } = settings; + + if (darkMode) { + const styleDictionaryWithoutDarkFiles = getStyleDictionaryWithoutDarkFiles(styleDictionary); + buildStyles(styleDictionaryWithoutDarkFiles); + buildDarkModeStyles(settings); + return; + } + + buildStyles(styleDictionary); +}; + +export { buildStyleDictionary }; diff --git a/src/tokens/Colors.ts b/src/tokens/Colors.ts index 0cd9f6f..62902e5 100644 --- a/src/tokens/Colors.ts +++ b/src/tokens/Colors.ts @@ -44,13 +44,43 @@ const getColors = (component: FigmaColorComponent): ColorToken | false => { return buildColorToken(component, tokenValue); }; +const buildColorTokensWithThemes = (tokens: ColorCollection) => { + const tokensWithThemes: { [key: string]: ColorCollection } = {}; + + tokensWithThemes['dark'] = { + colors: tokens.colors['dark'] as any + } as ColorCollection; + + delete tokens.colors['dark']; + + tokensWithThemes.default = tokens; + + return tokensWithThemes; +}; + +const getExt = (theme: string) => { + if (theme === 'default') { + return 'json'; + } + + return `${theme}.json`; +}; + const writeColorTokens = - (tokens: ColorCollection) => + (tokens: ColorCollection, darkMode?: boolean) => (createFile: CreateFile, outputDir: string, name = 'colors') => { + if (darkMode) { + const tokensWithThemes = buildColorTokensWithThemes(tokens); + + return Object.keys(tokensWithThemes).map(theme => + createFile(name, tokensWithThemes[theme], outputDir, getExt(theme)) + ); + } + return [createFile(name, tokens, outputDir, 'json')]; }; -const Colors = ({ frame }: TokenPayload): DesignTokensGenerator => { +const Colors = ({ frame, darkMode }: TokenPayload): DesignTokensGenerator => { const tokens = getTokens( 'Colors', frame, @@ -60,7 +90,7 @@ const Colors = ({ frame }: TokenPayload): DesignTokensGenerator => { return { name: 'Colors', tokens, - writeTokens: writeColorTokens(tokens) + writeTokens: writeColorTokens(tokens, darkMode) }; }; diff --git a/src/types/designTokens/config.ts b/src/types/designTokens/config.ts index 328d60e..d84f2d8 100644 --- a/src/types/designTokens/config.ts +++ b/src/types/designTokens/config.ts @@ -6,8 +6,10 @@ export type Config = { figmaProjectId: string; figmaPages: FigmaPages; outputDir: string; - figmaThemes?: string[]; -} & StyleDictionaryConfig; + darkMode?: boolean; + darkModeStyleDictionary?: StyleDictionaryConfig; + styleDictionary?: StyleDictionaryConfig; +}; export type ParseConfig = { settings: Config; diff --git a/src/types/designTokens/tokens.ts b/src/types/designTokens/tokens.ts index 6c2c896..3295d72 100644 --- a/src/types/designTokens/tokens.ts +++ b/src/types/designTokens/tokens.ts @@ -12,5 +12,5 @@ export interface TokenCollection { export type TokenPayload = { frame: FigmaFrame; - themes?: string[]; + darkMode?: boolean; }; diff --git a/template.config.json b/template.config.json index 7c57316..5df4f87 100644 --- a/template.config.json +++ b/template.config.json @@ -4,22 +4,49 @@ "outputDir": "design-tokens", "figmaPages": { "Colors": ["Colors"], - "Design Tokens v2": ["Gradients"], - "Design Tokens": ["Typography", "Shadows"], + "Other Page": ["Gradients"], + "Design Tokens": ["Typography", "Shadows", "Borders", "Breakpoints"], "Spacings": ["Spacings"] }, - "figmaThemes": ["light", "dark"], - "source": ["design-tokens/**/*.json"], - "platforms": { - "scss": { - "transformGroup": "scss", - "buildPath": "tokens/variables/scss/", - "files": [ - { - "destination": "variables.scss", - "format": "scss/variables" - } - ] + "darkMode": true, + "styleDictionary": { + "source": ["design-tokens/**/*.json"], + "platforms": { + "css": { + "transformGroup": "css", + "buildPath": "design-tokens/tokens/", + "files": [ + { + "destination": "variables.css", + "format": "css/variables" + } + ] + }, + "javascript": { + "transformGroup": "js", + "buildPath": "design-tokens/tokens/js/", + "files": [ + { + "destination": "variables.ts", + "format": "javascript/es6" + } + ] + } + } + }, + "darkModeStyleDictionary": { + "source": ["design-tokens/**/*.dark.json"], + "platforms": { + "css": { + "transformGroup": "css", + "buildPath": "design-tokens/tokens/", + "files": [ + { + "destination": "variables-dark.css", + "format": "css/variables" + } + ] + } } } }