Skip to content

Commit

Permalink
feat(tokens): add tooling for EDS theming (#1738)
Browse files Browse the repository at this point in the history
Tickets: [EDS-996, EDS-746, EDS-747, EDS-749]
- handle initializing token override file from base
- handle configuration for local paths to use
- handle comparison to base in package for checksum
- handle updates to themes when base has changed
- apply updates to prepare for introduction to EDS
- update naming for consistency
- add in better ordering of commands and exception handling
- sync changes to style-dictionary
- add documentation for using EDS commands
- update README to link to new documentation

Tested with tag `13.2.0-alpha.0`
  • Loading branch information
booc0mtaco authored Sep 7, 2023
1 parent 2b0855e commit 91497bf
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 10 deletions.
101 changes: 99 additions & 2 deletions .storybook/components/Docs/Guidelines/Theming.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,110 @@ import { Canvas, Meta } from '@storybook/blocks';

# Theming overview

"Theming", in the context of EDS, is the process of overriding the default styles of EDS components to match a different brand (or "theme"). The "Theming" directory in storybook demonstrates examples of theming:
"Theming", in the context of EDS, is the process of overriding the default styles of EDS components to match a different brand (or "theme"). We include useful examples under "Pages":

- A [wireframe theme](./?path=/story/pages-theming-wireframedemo--default) (an unbranded theme that can be used for prototyping a product before it has an official visual style).

Below are instructions on how to use the tooling and tokens to define custom theme values for a project.

## How to apply a theme in another product

In EDS, theming is implemented by overriding the values of the CSS variables representing tokens, which the EDS components use in their styles. This should update the style of the components to match the branding of a different product with minimum manual CSS styling overrides. (Some manual styling overrides will be necessary though because we don't have tokens for every little detail. In those cases, we could create a new token to make those overrides easier if it looks like something that could very well be useful for other products as well.)
EDS comes with some tooling to allow easy transfer of theme data from Figma (or some style-dictionary compatible format) into code.

* `eds-init-theme` - This command sets up the initial file(s) for theming your application
* `eds-apply-theme` - This command parses the style dictionary files to generate the tokens (CSS Variables) used by EDS

Each of these tools reads config to figure out where to read/write files. This can be defined in several ways, e.g., a top-level file `.edsrc.json`, or as a key-value set in package.json. Example:

`package.json`

```json
"eds": {
"json": "src/components/",
"css": "src/components/"
},
```

`.edsrc.json`

```json
{
"json": "src/components/",
"css": "src/components/"
}
```

`json` determines where the core theme file will be copied to OR read from, and `css` determines where the resulting css token file will be stored.

### eds-init-theme

This will create an initial JSON file `app-theme.json` that defines ALL the available tokens for EDS that you can edit.

EDS comes pre-packaged with many tokens that define the base style and character of the system. Users of EDS can theme certain aspects of all components, or details on specific components.

```json
{
"eds": {
"anim": {
"fade": {
"quick": {
"value": "0.15s"
},
"long": {
"value": "0.4s"
}
},
"move": {
"quick": {
"value": "0.15s"
},
"medium": {
"value": "0.3s"
},
"long": {
"value": "0.4s"
}
},
"ease": {
"value": "ease"
}
},
// ...other token values
},
}
```

### eds-apply-theme

After making changes to the `app-theme.json` to reflect what has been defined by design, update the CSS token file by running `npx eds-apply-theme`.

Once run, you will have a CSS file `app-theme.css` that includes a set of token values that can be used in the app as appropriate.

```css
/**
* Do not edit directly
* Generated on Sunday, 01 Jan 2023 12:34:56 GMT
* To update, edit app-theme.json, then run `npx eds-apply-theme`
*/

:root {
--eds-anim-fade-quick: 0.15s;
--eds-anim-fade-long: 0.4s;
--eds-anim-move-quick: 0.15s;
--eds-anim-move-medium: 0.3s;
--eds-anim-move-long: 0.4s;
--eds-anim-ease: ease;
/* ...other token values... */
}
```

Add this file to your core app root file. That's it! Now, the theme will be applied to the tokens used by EDS components. To make other changes, edit `app-theme.json`, then re-run `npx eds-apply-theme`.

**NOTE**: do not edit this file directly. Instead, follow the instructions at the top of the file!

## How to manually apply a theme in another product

You can also manage the creation of theme token definitions manually. In EDS, theming is implemented by overriding the values of the CSS variables representing tokens, which the EDS components use in their styles. This should update the style of the components to match the branding of a different product with minimum manual CSS styling overrides. (Some manual styling overrides will be necessary though because we don't have tokens for every little detail. In those cases, we could create a new token to make those overrides easier if it looks like something that could very well be useful for other products as well.)

These CSS variables overrides lives in the products using EDS components. This allows product teams to quickly iterate on their theme without making changes to EDS itself.

Expand Down
2 changes: 1 addition & 1 deletion .storybook/components/Docs/Guidelines/Tokens.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,4 @@ If the EDS tailwind config theme is being used, Tier 2 and tier 3 color tokens a
<!-- will reflect respective color utility tokens
(background-brand-primary-strong and border-brand-primary-strong) -->
<div className="bg-brand-primary-strong border-brand-primary-strong"></div>
```
```
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ html {

### Tailwind Setup

The EDS Tailwind theme provides EDS color [tokens](https://chanzuckerberg.github.io/edu-design-system/?path=/story/documentation-guidelines-tokens--page) and screens. Import the tailwind config into the app's tailwind config and supply the [content](https://tailwindcss.com/docs/content-configuration) property for use:
The EDS Tailwind theme provides EDS color [tokens][tokens] and screens. Import the tailwind config into the app's tailwind config and supply the [content](https://tailwindcss.com/docs/content-configuration) property for use:

```js
const edsConfig.theme = require('@chanzuckerberg/eds/tailwind.config');
Expand All @@ -47,7 +47,15 @@ module.exports = {
};
```

Refer to the [tokens tailwind section](https://chanzuckerberg.github.io/edu-design-system/?path=/story/documentation-guidelines-tokens--page#tailwind-class-tokens) for usage guidelines.
Refer to the [tokens tailwind section][tokens] for usage guidelines.

[tokens]: https://chanzuckerberg.github.io/edu-design-system/?path=/docs/documentation-guidelines-tokens--docs


### Theming Setup

Refer to the "EDS Token and Theme Tools" in [the tokens documentation](https://chanzuckerberg.github.io/edu-design-system/?path=/docs/documentation-theming--docs) to learn about the optional tooling setup.


## Usage

Expand Down Expand Up @@ -101,6 +109,6 @@ This project is governed under the [Contributor Covenant](https://www.contributo

See our [Security Readme](https://github.com/chanzuckerberg/edu-design-system/blob/main/SECURITY.md).

## More Information and Support
## FAQ, More Information, and Support

Please review our Education Design System Site (SSO Required): [/Paper](https://eds.czi.design/0843bc428/p/581284-education-design-system)
24 changes: 24 additions & 0 deletions bin/_util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
/**
* Fetch the EDS config from the project using the lilconfig hierarchy.
* This can be from package.json, or from various separate non-YAML files.
*
* @see https://github.com/antonk52/lilconfig#usage
* @returns nullable config object returned from lilconfig
*/
getConfig: async function () {
const { lilconfig } = require('lilconfig');

// read in the config from config file, package json "eds", etc.
const settings = await lilconfig('eds').search();

// If no config exists, fail
if (!settings) {
throw new Error(
'Please add EDS config to your project before continuing (specify "json" and "css" target paths)',
);
}

return settings.config;
},
};
95 changes: 95 additions & 0 deletions bin/eds-apply-theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node
(async function () {
const StyleDictionary = require('style-dictionary');
const path = require('path');
const fs = require('fs');
const { getConfig } = require('./_util');

let packageRootPath;
try {
packageRootPath =
path.dirname(require.resolve('@chanzuckerberg/eds')) + '/tokens/json/';
} catch (e) {
console.error('EDS package not installed. Using local path...');
packageRootPath =
path.dirname(require.main.path) + '/src/tokens-dist/json/';
}

// Read the config to sort out where to read JSON from and where to write the CSS file
const config = await getConfig();

// read and parse JSON files on disk
const localTheme = JSON.parse(
fs.readFileSync(`${config.json}app-theme.json`, 'utf8'),
);
const baseTheme = JSON.parse(
fs.readFileSync(`${packageRootPath}theme-base.json`, 'utf8'),
);

// define the header to use in the resulting CSS file so people know not to edit it directly
StyleDictionary.registerFileHeader({
name: 'cssOverrideHeader',
fileHeader: (defaultMessage) => [
...defaultMessage,
'To update, edit app-theme.json, then run `npx eds-apply-theme`',
],
});

const EDSStyleDictionary = StyleDictionary.extend({
source: [config.json + 'app-theme.json'],
platforms: {
css: {
transforms: [...StyleDictionary.transformGroup.css, 'name/cti/kebab'],
buildPath: config.css,
files: [
{
format: 'css/variables',
destination: 'app-theme.css',
options: {
fileHeader: 'cssOverrideHeader',
},
filter: function (token) {
// don't allow theming on legacy tokens
return token.attributes.category !== 'legacy';
},
},
],
},
},
});

/**
* Determine if the given theme file is a subset of what's in the base theme file.
* If it isnt, throw an error:
* - If keys are in base that are missing in the theme file, that's OK (no need to override everything)
* - If keys are in theme that aren't in base, throw (you can't theme tokens that don't exist in EDS)
* @param {object} base The tokens theme file stored in the EDS project
* @param {object} theme The project theme file stored in the app code (same format as bas)
* @param {Array} path The base path, stored as an array of object key names (default [])
* @throws Error when there are tokens in theme that aren't in base
*/
function isStrictSubset(base, theme, path = []) {
for (const name in theme) {
if (typeof theme[name] === 'object') {
if (base[name] === undefined) {
throw new Error(
`Local themeable value does not exist in base theme: --${path.join(
'-',
)}.${name}"`,
);
}
isStrictSubset(base[name], theme[name], path.concat(name));
}
}
}

try {
// Keys in the theme file must be a strict subset of those in the base file
isStrictSubset(baseTheme, localTheme);
EDSStyleDictionary.buildAllPlatforms();
} catch (error) {
// TODO: if theme has things not in base, error showing where the conflict
console.error('EDS theming error:', error.message);
return;
}
})();
37 changes: 37 additions & 0 deletions bin/eds-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
(async function () {
const fs = require('fs');
const path = require('path');
const { getConfig } = require('./_util');

let packageRootPath;
try {
packageRootPath =
path.dirname(require.resolve('@chanzuckerberg/eds')) + '/tokens/json/';
} catch (e) {
console.error('EDS package not installed. Using local path...');
packageRootPath =
path.dirname(require.main.path) + '/src/tokens-dist/json/';
}

// read in the config from config file, package json "eds", etc.
const config = await getConfig();

// take the packaged token file and place a copy in the project's 'json' specified path
if (config) {
try {
fs.copyFileSync(
packageRootPath + 'theme-base.json',
`${config.json}app-theme.json`,
fs.constants.COPYFILE_EXCL,
);
} catch (error) {
console.error('The local theme file already exists. Exiting.');
return 1;
}

console.log(
'File copy completed! Please use `npx eds-apply-theme` to generate theme tokens (CSS Variables).',
);
}
})();
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
"url": "https://github.com/chanzuckerberg/edu-design-system/issues"
},
"files": [
"/bin",
"/lib",
"tailwind.config.*"
],
"bin": {
"eds-apply-theme": "bin/eds-apply-theme.js",
"eds-init-theme": "bin/eds-init.js"
},
"scripts": {
"build": "yarn build:clean && yarn build:tokens && yarn build:declarations && yarn build:js && yarn copy-fonts-to-lib",
"build:clean": "rm -rf lib/",
Expand Down Expand Up @@ -89,12 +94,16 @@
"@types/lodash": "^4.14.197",
"clsx": "^1.2.1",
"graphemer": "^1.4.0",
"lilconfig": "^2.0.6",
"lodash": "^4.17.21",
"react-beautiful-dnd": "^13.1.1",
"react-children-by-type": "^1.1.0",
"react-focus-lock": "^2.9.5",
"react-popper": "^2.3.0",
"react-portal": "^4.2.2"
"react-portal": "^4.2.2",
"react-uid": "^2.3.2",
"style-dictionary": "^3.7.0",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"@babel/preset-env": "^7.22.14",
Expand Down
Loading

0 comments on commit 91497bf

Please sign in to comment.