Skip to content

Commit

Permalink
Feature/themes (#58)
Browse files Browse the repository at this point in the history
* First approach for adding themes

* Update README

* Changeset

* Fix typographies

* Fix lint

* Update config template

* Add theming explanation

* Remove custom format if not exit
  • Loading branch information
Imar Abreu authored Jan 10, 2024
1 parent a88c9f6 commit e4f8329
Show file tree
Hide file tree
Showing 24 changed files with 207 additions and 169 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-laws-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@runroom/design-tokens': major
---

Remove darkmode and add themes
21 changes: 5 additions & 16 deletions .template-designtokensrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"Design Tokens": ["Typography", "Shadows", "Borders", "Breakpoints"],
"Spacings": ["Spacings"]
},
"darkMode": true,
"figmaThemes": ["dark"],
"styleDictionary": {
"source": ["design-tokens/**/*.json"],
"platforms": {
Expand All @@ -19,6 +19,10 @@
{
"destination": "variables.css",
"format": "css/variables"
},
{
"destination": "variables-themes.css",
"format": "css/variables-themes"
}
]
},
Expand All @@ -33,20 +37,5 @@
]
}
}
},
"darkModeStyleDictionary": {
"source": ["design-tokens/**/*.dark.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "design-tokens/tokens/",
"files": [
{
"destination": "variables-dark.css",
"format": "css/variables"
}
]
}
}
}
}
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ example:

### Config

Add a config file on the root directory of your project. This repository uses the standard config file naming by [cosmiconfig](https://www.npmjs.com/package/cosmiconfig).
By default, the config file name should be use `designtokens` as module name, like `.designtokensrc.json` or `.designtokensrc.js`, also as a `package.json` entry with the key `designtokens`.
Add a config file on the root directory of your project. This repository uses the standard config file naming
by [cosmiconfig](https://www.npmjs.com/package/cosmiconfig).
By default, the config file name should be use `designtokens` as module name, like `.designtokensrc.json`
or `.designtokensrc.js`, also as a `package.json` entry with the key `designtokens`.

If you want to use a different name for your config file, you can use the parameter `--config-file=FILENAME` when you execute the command.
If you want to use a different name for your config file, you can use the parameter `--config-file=FILENAME` when you
execute the command.

You can find a template for your config file [here](.template-designtokensrc.json).

Expand All @@ -44,13 +47,27 @@ array of the figma frames that contains the tokens

`outputDir`: The source where will be stored the package's output

`darkMode`: Boolean to enable/disable the dark mode support
### Optional config

### Optional config fields for [Style Dictionary](https://amzn.github.io/style-dictionary/#/config)
`figmaThemes`: An array of the name of the themes you want to sync. This option is only available for color tokens, and
we take as theme the first block of the name. For example: `dark-primary` or `dark-background-secondary` will be `dark`.

#### Config 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**
## Theming: How to use the themes

The themes are generated in the same file as the tokens, but if you want to extract the themes to a different file with
the selector `[data-theme="THEME_NAME"]`, you can add the following config to the css files, in the style dictionary
config, how you can see in the config template:

```json
{
"destination": "variables-themes.css",
"format": "css/variables-themes"
}
```

## For Devs:

Expand All @@ -73,7 +90,8 @@ Expanding the design tokens in your project is a straightforward process. Here's

4. **Create a Token:**
- To manage and process the new tokens, you'll need to create a corresponding class.
- Inside the `src/tokens` directory, create a new TypeScript file with a method that return a `DesignTokensGenerator`.
- Inside the `src/tokens` directory, create a new TypeScript file with a method that return
a `DesignTokensGenerator`.
- Implement the necessary methods for handling the new tokens. These methods may include writing to
files, or other token-specific functionality.

Expand All @@ -83,7 +101,7 @@ Expanding the design tokens in your project is a straightforward process. Here's
- 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.
- If the token needs a specific parser, add it to the `src/styleDictionary/styleDictionary.ts` file.

## License

Expand Down
4 changes: 2 additions & 2 deletions src/api/figmaApiConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const figmaApiConnection = async ({
figmaApiKey,
figmaProjectId,
figmaPages,
darkMode
figmaThemes
}: Config): Promise<DesignTokensGenerator[]> => {
log('Connecting with Figma...', EMOJIS.workingInProgress);

Expand All @@ -46,7 +46,7 @@ const figmaApiConnection = async ({
throw new Error(`No styles found`);
}

const parsedTokens = parseFigma(responseJson, figmaPages, darkMode);
const parsedTokens = parseFigma(responseJson, figmaPages, figmaThemes);

if (!parsedTokens || !parsedTokens.length) {
throw new Error(`No styles found`);
Expand Down
4 changes: 2 additions & 2 deletions src/api/parseFigma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, darkMode?: boolean) => {
const parseFigma = (response: FigmaResponse, FIGMA_PAGES: FigmaPages, figmaThemes?: string[]) => {
if (!response) {
throw new Error(`\x1b[31m\n\n${EMOJIS.error} No styles found\n`);
}
Expand All @@ -30,7 +30,7 @@ const parseFigma = (response: FigmaResponse, FIGMA_PAGES: FigmaPages, darkMode?:
}

const figmaDesignTokensFrames = figmaPage.children;
const generateTokens = generateDesignTokens(frames, figmaDesignTokensFrames, darkMode);
const generateTokens = generateDesignTokens(frames, figmaDesignTokensFrames, figmaThemes);
tokens.push(...generateTokens);
}

Expand Down
10 changes: 5 additions & 5 deletions src/functions/getTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ const designTokensBuilder = <T>(
name: DesignPages,
pages: string[],
frame: FigmaFrame,
designToken: ({ frame, darkMode }: TokenPayload) => T,
darkMode?: boolean
designToken: ({ frame, themes }: TokenPayload) => T,
themes?: string[]
): T | undefined => {
if (!pages.includes(name)) {
return;
}

return designToken({ frame, darkMode });
return designToken({ frame, themes });
};

const generateDesignTokens = (
pages: string[],
figmaDesignTokensFrames: FigmaFrame[],
darkMode?: boolean
figmaThemes?: string[]
) => {
const writeFilePromises: DesignTokensGenerator[] = figmaDesignTokensFrames
.map(frame => {
Expand All @@ -120,7 +120,7 @@ const generateDesignTokens = (
pages,
frame,
tokenGenerator,
darkMode
figmaThemes
);
})
.filter(truthy);
Expand Down
135 changes: 67 additions & 68 deletions src/styleDictionary/styleDictionary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import StyleDictionary, { Config as StyleDictionaryConfig } from 'style-dictionary';
import StyleDictionary, {
Config as StyleDictionaryConfig,
TransformedToken
} from 'style-dictionary';
import { EMOJIS, log } from '@/functions';
import { getGradientsParser, getShadowsParser, getTypographiesParser } from '@/tokens';
import { Config } from '@/types/designTokens';
Expand All @@ -9,82 +12,81 @@ const registerParsers = () => {
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;
}
const buildStyles = (styleDictionary: StyleDictionaryConfig) => {
const extendedDictionary = StyleDictionary.extend(styleDictionary);

return `${path.replace(`.json`, `.dark.json`)}`;
});
log('Compiling styles...', EMOJIS.workingInProgress);
extendedDictionary.buildAllPlatforms();
log('Styles compiled', EMOJIS.success);
};

const getDarkModeStyleDictionaryDefaultConfig = (settings: Config) => {
const source = getDarkModeSource(settings.styleDictionary?.source, settings.outputDir);
const buildPath =
settings.styleDictionary!.platforms.css.buildPath ?? `${settings.outputDir}/tokens/`;
const formatTokenToCssVariable = (token: TransformedToken) => `--${token.name}: ${token.value};`;

const formatTokenValueWithTheme = (token: TransformedToken) => {
const name = token.name.split('-').slice(1).join('-');
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`
}
]
}
}
...token,
name
};
};

const buildDarkModeStyles = (settings: Config) => {
const darkModeConfig =
settings.darkModeStyleDictionary ?? getDarkModeStyleDictionaryDefaultConfig(settings);
const getColorTheme = (token: TransformedToken) => token.name.split('-')[0];

const extendedDictionary = StyleDictionary.extend(darkModeConfig);

log('Compiling dark mode styles...', EMOJIS.workingInProgress);
extendedDictionary.buildAllPlatforms();
log('Dark mode styles compiled', EMOJIS.success);
const splitColorsByTheme = (colors: TransformedToken[]) => {
const colorsByTheme = {} as any;
colors.forEach(token => {
const theme = getColorTheme(token);
if (!colorsByTheme[theme]) colorsByTheme[theme] = [];
colorsByTheme[theme].push(formatTokenValueWithTheme(token));
});
return colorsByTheme;
};

const buildStyles = (styleDictionary: StyleDictionaryConfig) => {
const extendedDictionary = StyleDictionary.extend(styleDictionary);
const isColorToken = (token: TransformedToken) => token.type === 'color';

const addColorsThemeFormat = (themes: string[]) => {
StyleDictionary.registerFormat({
name: 'css/variables',
formatter({ dictionary }) {
const variables = dictionary.allProperties
.map(token => {
if (token.type === 'color') {
const theme = getColorTheme(token);
if (theme === 'default') {
return formatTokenToCssVariable(formatTokenValueWithTheme(token));
}
return false;
}

log('Compiling styles...', EMOJIS.workingInProgress);
extendedDictionary.buildAllPlatforms();
log('Styles compiled', EMOJIS.success);
};
return formatTokenToCssVariable(token);
})
.filter(Boolean)
.join(' ');

const getStyleDictionaryWithoutDarkFiles = (
styleDictionary: StyleDictionaryConfig
): StyleDictionaryConfig => {
const source = styleDictionary.source
? styleDictionary.source.map(sourcePath => sourcePath.replace('*.json', '!(*.dark).json'))
: [];
return `:root { ${variables} }\n`;
}
});

return {
...styleDictionary,
source,
filter: {
...styleDictionary.filter,
dark: ({ filePath }: { filePath: string }) => filePath.indexOf(`.dark`) === -1
StyleDictionary.registerFormat({
name: 'css/variables-themes',
formatter({ dictionary }) {
const colors = dictionary.allProperties.filter(isColorToken);
const colorsByTheme = splitColorsByTheme(colors);
const variables = themes
.map(theme => {
const themeColors = colorsByTheme[theme];
const cssVariables = themeColors.map(formatTokenToCssVariable).join(' ');
return `[data-theme="${theme}"] { ${cssVariables} }`;
})
.join(' ');

return `${variables}\n`;
}
};
});
};

const isThereThemeFormat = (styleDictionary: StyleDictionaryConfig) => {
return styleDictionary.platforms.css.files?.some(file => file.format === 'css/variables-themes');
};

const buildStyleDictionary = (settings: Config) => {
Expand All @@ -95,13 +97,10 @@ const buildStyleDictionary = (settings: Config) => {

registerParsers();

const { darkMode, styleDictionary } = settings;
const { styleDictionary, figmaThemes } = settings;

if (darkMode) {
const styleDictionaryWithoutDarkFiles = getStyleDictionaryWithoutDarkFiles(styleDictionary);
buildStyles(styleDictionaryWithoutDarkFiles);
buildDarkModeStyles(settings);
return;
if (figmaThemes && isThereThemeFormat(styleDictionary)) {
addColorsThemeFormat(figmaThemes);
}

buildStyles(styleDictionary);
Expand Down
6 changes: 5 additions & 1 deletion src/tokens/Borders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from '@/types/designTokens';
import { FigmaBorderComponent } from '@/types/figma';

const BORDER_TYPE = 'border';

const getBoundingWidth = (component: FigmaBorderComponent): BorderToken | false => {
if (!(component && component.name)) {
return false;
Expand All @@ -24,6 +26,8 @@ const getBoundingWidth = (component: FigmaBorderComponent): BorderToken | false

return {
[name]: {
name,
type: BORDER_TYPE,
value
}
};
Expand All @@ -49,4 +53,4 @@ const Borders = ({ frame }: TokenPayload): DesignTokensGenerator => {
};
};

export { Borders };
export { Borders, BORDER_TYPE };
Loading

3 comments on commit e4f8329

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.❔
Category Percentage Covered / Total
🟑 Statements 72.79% 511/702
🟑 Branches 60.89% 137/225
🟑 Functions 69.95% 142/203
🟑 Lines 72.15% 469/650

Test suite run success

85 tests passing in 4 suites.

Report generated by πŸ§ͺjest coverage report action from e4f8329

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.❔
Category Percentage Covered / Total
🟑 Statements 72.79% 511/702
🟑 Branches 60.89% 137/225
🟑 Functions 69.95% 142/203
🟑 Lines 72.15% 469/650

Test suite run success

85 tests passing in 4 suites.

Report generated by πŸ§ͺjest coverage report action from e4f8329

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.❔
Category Percentage Covered / Total
🟑 Statements 72.79% 511/702
🟑 Branches 60.89% 137/225
🟑 Functions 69.95% 142/203
🟑 Lines 72.15% 469/650

Test suite run success

85 tests passing in 4 suites.

Report generated by πŸ§ͺjest coverage report action from e4f8329

Please sign in to comment.