Skip to content

Commit cbe3128

Browse files
author
Jeremiah Clothier
committed
Merge branch 'next' into jclothier/neutral-inline-notification
2 parents ba13d13 + 91497bf commit cbe3128

18 files changed

+577
-30
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"files": [
6565
"config.js",
6666
"*.config.js",
67-
"main.js",
67+
"main.ts",
6868
"plopfile.js",
6969
"src/**/*.js"
7070
],

.storybook/components/Docs/Guidelines/Theming.stories.mdx

+99-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,110 @@ import { Canvas, Meta } from '@storybook/blocks';
44

55
# Theming overview
66

7-
"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:
7+
"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":
88

99
- 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).
1010

11+
Below are instructions on how to use the tooling and tokens to define custom theme values for a project.
12+
1113
## How to apply a theme in another product
1214

13-
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.)
15+
EDS comes with some tooling to allow easy transfer of theme data from Figma (or some style-dictionary compatible format) into code.
16+
17+
* `eds-init-theme` - This command sets up the initial file(s) for theming your application
18+
* `eds-apply-theme` - This command parses the style dictionary files to generate the tokens (CSS Variables) used by EDS
19+
20+
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:
21+
22+
`package.json`
23+
24+
```json
25+
"eds": {
26+
"json": "src/components/",
27+
"css": "src/components/"
28+
},
29+
```
30+
31+
`.edsrc.json`
32+
33+
```json
34+
{
35+
"json": "src/components/",
36+
"css": "src/components/"
37+
}
38+
```
39+
40+
`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.
41+
42+
### eds-init-theme
43+
44+
This will create an initial JSON file `app-theme.json` that defines ALL the available tokens for EDS that you can edit.
45+
46+
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.
47+
48+
```json
49+
{
50+
"eds": {
51+
"anim": {
52+
"fade": {
53+
"quick": {
54+
"value": "0.15s"
55+
},
56+
"long": {
57+
"value": "0.4s"
58+
}
59+
},
60+
"move": {
61+
"quick": {
62+
"value": "0.15s"
63+
},
64+
"medium": {
65+
"value": "0.3s"
66+
},
67+
"long": {
68+
"value": "0.4s"
69+
}
70+
},
71+
"ease": {
72+
"value": "ease"
73+
}
74+
},
75+
// ...other token values
76+
},
77+
}
78+
```
79+
80+
### eds-apply-theme
81+
82+
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`.
83+
84+
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.
85+
86+
```css
87+
/**
88+
* Do not edit directly
89+
* Generated on Sunday, 01 Jan 2023 12:34:56 GMT
90+
* To update, edit app-theme.json, then run `npx eds-apply-theme`
91+
*/
92+
93+
:root {
94+
--eds-anim-fade-quick: 0.15s;
95+
--eds-anim-fade-long: 0.4s;
96+
--eds-anim-move-quick: 0.15s;
97+
--eds-anim-move-medium: 0.3s;
98+
--eds-anim-move-long: 0.4s;
99+
--eds-anim-ease: ease;
100+
/* ...other token values... */
101+
}
102+
```
103+
104+
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`.
105+
106+
**NOTE**: do not edit this file directly. Instead, follow the instructions at the top of the file!
107+
108+
## How to manually apply a theme in another product
109+
110+
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.)
14111

15112
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.
16113

.storybook/components/Docs/Guidelines/Tokens.stories.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,4 @@ If the EDS tailwind config theme is being used, Tier 2 and tier 3 color tokens a
174174
<!-- will reflect respective color utility tokens
175175
(background-brand-primary-strong and border-brand-primary-strong) -->
176176
<div className="bg-brand-primary-strong border-brand-primary-strong"></div>
177-
```
177+
```

.storybook/main.js renamed to .storybook/main.ts

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { StorybookConfig } from '@storybook/react-webpack5';
2+
import type { Configuration } from 'webpack';
3+
14
/**
25
* Although `--static-dir` is marked as deprecated. The The Chromatic CLI
36
* currently pulls the staticDir from the build script, but does not support
@@ -6,7 +9,7 @@
69
* We should refrain from using the staticDirs option in this configuration until
710
* https://github.com/chromaui/chromatic-cli/issues/462 is resolved.
811
*/
9-
module.exports = {
12+
const config: StorybookConfig = {
1013
stories: [
1114
'./components/**/*.stories.mdx',
1215
'./components/**/*.stories.@(js|jsx|ts|tsx)',
@@ -70,4 +73,47 @@ module.exports = {
7073
plugins: [],
7174
};
7275
},
76+
webpackFinal(config, { configType }) {
77+
if (configType === 'DEVELOPMENT') {
78+
updateCSSLoaderPlugin(config);
79+
}
80+
return config;
81+
},
7382
};
83+
84+
/**
85+
* Updates the `css-loader` webpack plugin to make class names human readable.
86+
*
87+
* NOTE: This should only be used for local development.
88+
*/
89+
function updateCSSLoaderPlugin(config: Configuration): Configuration {
90+
config.module?.rules?.forEach((rule) => {
91+
if (rule && typeof rule === 'object' && Array.isArray(rule.use)) {
92+
const isRuleForCSS = rule.test?.toString() === '/\\.css$/';
93+
if (isRuleForCSS) {
94+
rule.use.forEach((ruleSetRule) => {
95+
if (
96+
typeof ruleSetRule === 'object' &&
97+
ruleSetRule?.loader?.includes('node_modules/css-loader')
98+
) {
99+
ruleSetRule.options = {
100+
// @ts-expect-error css-loader doesn't accept "string" options
101+
// and will either be an object or undefined
102+
...ruleSetRule.options,
103+
modules: {
104+
// @ts-expect-error css-loader doesn't accept "string" options
105+
// and will either be an object or undefined
106+
...ruleSetRule.options?.modules,
107+
localIdentName: '[name]__[local]--[hash:base64:5]',
108+
},
109+
};
110+
}
111+
});
112+
}
113+
}
114+
});
115+
116+
return config;
117+
}
118+
119+
export default config;

README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ html {
3636

3737
### Tailwind Setup
3838

39-
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:
39+
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:
4040

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

50-
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.
50+
Refer to the [tokens tailwind section][tokens] for usage guidelines.
51+
52+
[tokens]: https://chanzuckerberg.github.io/edu-design-system/?path=/docs/documentation-guidelines-tokens--docs
53+
54+
55+
### Theming Setup
56+
57+
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.
58+
5159

5260
## Usage
5361

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

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

104-
## More Information and Support
112+
## FAQ, More Information, and Support
105113

106114
Please review our Education Design System Site (SSO Required): [/Paper](https://eds.czi.design/0843bc428/p/581284-education-design-system)

bin/_util.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
/**
3+
* Fetch the EDS config from the project using the lilconfig hierarchy.
4+
* This can be from package.json, or from various separate non-YAML files.
5+
*
6+
* @see https://github.com/antonk52/lilconfig#usage
7+
* @returns nullable config object returned from lilconfig
8+
*/
9+
getConfig: async function () {
10+
const { lilconfig } = require('lilconfig');
11+
12+
// read in the config from config file, package json "eds", etc.
13+
const settings = await lilconfig('eds').search();
14+
15+
// If no config exists, fail
16+
if (!settings) {
17+
throw new Error(
18+
'Please add EDS config to your project before continuing (specify "json" and "css" target paths)',
19+
);
20+
}
21+
22+
return settings.config;
23+
},
24+
};

bin/eds-apply-theme.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env node
2+
(async function () {
3+
const StyleDictionary = require('style-dictionary');
4+
const path = require('path');
5+
const fs = require('fs');
6+
const { getConfig } = require('./_util');
7+
8+
let packageRootPath;
9+
try {
10+
packageRootPath =
11+
path.dirname(require.resolve('@chanzuckerberg/eds')) + '/tokens/json/';
12+
} catch (e) {
13+
console.error('EDS package not installed. Using local path...');
14+
packageRootPath =
15+
path.dirname(require.main.path) + '/src/tokens-dist/json/';
16+
}
17+
18+
// Read the config to sort out where to read JSON from and where to write the CSS file
19+
const config = await getConfig();
20+
21+
// read and parse JSON files on disk
22+
const localTheme = JSON.parse(
23+
fs.readFileSync(`${config.json}app-theme.json`, 'utf8'),
24+
);
25+
const baseTheme = JSON.parse(
26+
fs.readFileSync(`${packageRootPath}theme-base.json`, 'utf8'),
27+
);
28+
29+
// define the header to use in the resulting CSS file so people know not to edit it directly
30+
StyleDictionary.registerFileHeader({
31+
name: 'cssOverrideHeader',
32+
fileHeader: (defaultMessage) => [
33+
...defaultMessage,
34+
'To update, edit app-theme.json, then run `npx eds-apply-theme`',
35+
],
36+
});
37+
38+
const EDSStyleDictionary = StyleDictionary.extend({
39+
source: [config.json + 'app-theme.json'],
40+
platforms: {
41+
css: {
42+
transforms: [...StyleDictionary.transformGroup.css, 'name/cti/kebab'],
43+
buildPath: config.css,
44+
files: [
45+
{
46+
format: 'css/variables',
47+
destination: 'app-theme.css',
48+
options: {
49+
fileHeader: 'cssOverrideHeader',
50+
},
51+
filter: function (token) {
52+
// don't allow theming on legacy tokens
53+
return token.attributes.category !== 'legacy';
54+
},
55+
},
56+
],
57+
},
58+
},
59+
});
60+
61+
/**
62+
* Determine if the given theme file is a subset of what's in the base theme file.
63+
* If it isnt, throw an error:
64+
* - If keys are in base that are missing in the theme file, that's OK (no need to override everything)
65+
* - If keys are in theme that aren't in base, throw (you can't theme tokens that don't exist in EDS)
66+
* @param {object} base The tokens theme file stored in the EDS project
67+
* @param {object} theme The project theme file stored in the app code (same format as bas)
68+
* @param {Array} path The base path, stored as an array of object key names (default [])
69+
* @throws Error when there are tokens in theme that aren't in base
70+
*/
71+
function isStrictSubset(base, theme, path = []) {
72+
for (const name in theme) {
73+
if (typeof theme[name] === 'object') {
74+
if (base[name] === undefined) {
75+
throw new Error(
76+
`Local themeable value does not exist in base theme: --${path.join(
77+
'-',
78+
)}.${name}"`,
79+
);
80+
}
81+
isStrictSubset(base[name], theme[name], path.concat(name));
82+
}
83+
}
84+
}
85+
86+
try {
87+
// Keys in the theme file must be a strict subset of those in the base file
88+
isStrictSubset(baseTheme, localTheme);
89+
EDSStyleDictionary.buildAllPlatforms();
90+
} catch (error) {
91+
// TODO: if theme has things not in base, error showing where the conflict
92+
console.error('EDS theming error:', error.message);
93+
return;
94+
}
95+
})();

bin/eds-init.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env node
2+
(async function () {
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { getConfig } = require('./_util');
6+
7+
let packageRootPath;
8+
try {
9+
packageRootPath =
10+
path.dirname(require.resolve('@chanzuckerberg/eds')) + '/tokens/json/';
11+
} catch (e) {
12+
console.error('EDS package not installed. Using local path...');
13+
packageRootPath =
14+
path.dirname(require.main.path) + '/src/tokens-dist/json/';
15+
}
16+
17+
// read in the config from config file, package json "eds", etc.
18+
const config = await getConfig();
19+
20+
// take the packaged token file and place a copy in the project's 'json' specified path
21+
if (config) {
22+
try {
23+
fs.copyFileSync(
24+
packageRootPath + 'theme-base.json',
25+
`${config.json}app-theme.json`,
26+
fs.constants.COPYFILE_EXCL,
27+
);
28+
} catch (error) {
29+
console.error('The local theme file already exists. Exiting.');
30+
return 1;
31+
}
32+
33+
console.log(
34+
'File copy completed! Please use `npx eds-apply-theme` to generate theme tokens (CSS Variables).',
35+
);
36+
}
37+
})();

0 commit comments

Comments
 (0)