Skip to content

Commit 5aad797

Browse files
authoredDec 8, 2024··
feat(examples): add tailwind preset example (#1344)
Co-authored-by: Jessica Lynch <jessicalynch@users.noreply.github.com>
1 parent 209085d commit 5aad797

File tree

17 files changed

+410
-13
lines changed

17 files changed

+410
-13
lines changed
 

‎.changeset/gentle-chairs-teach.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'style-dictionary': minor
3+
---
4+
5+
Add tailwind preset example, remove unused .editorconfig file

‎.editorconfig

-12
This file was deleted.

‎docs/src/content/docs/getting-started/examples.md

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ If you want to look at more advanced examples of possible applications and custo
4242
- [**npm-module**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/npm-module) shows how to set up a style dictionary as an npm module, either to publish to a local npm service or to publish externally.
4343
- [**referencing_aliasing**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/referencing_aliasing) shows how to use referencing (or "aliasing") to reference a value -or an attribute– of a token and assign it to the value –or attribute– of another token.
4444
- [**s3**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/s3) shows how to set up a style dictionary to build files for different platforms (web, iOS, Android) and upload those build artifacts, together with a group of assets, to an S3 bucket.
45+
- [**tailwind-preset**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/tailwind-preset) shows how to build a [tailwind preset](https://tailwindcss.com/docs/presets#creating-a-preset) with Style Dictionary.
4546
- [**tokens-deprecation**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/tokens-deprecation) shows one way to deprecate tokens by adding metadata to tokens and using custom formats to output comments in the generated files.
4647
- [**transitive-transforms**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/transitive-transforms) shows how to use transitive transforms to transform references
4748
- [**variables-in-outputs**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/variables-in-outputs) shows you how to use the `outputReferences` option to generate files variable references in them.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
demo/output.css
2+
build
3+
node_modules
4+
package-lock.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Tailwind preset
2+
3+
Builds [Tailwind preset](https://tailwindcss.com/docs/presets#creating-a-preset) from tokens.
4+
5+
## Building the preset
6+
7+
Run `npm run build-tokens` to generate these files in `build/tailwind`:
8+
9+
### cssVarPlugin.js
10+
11+
A [Tailwind plugin](https://tailwindcss.com/docs/plugins) for registering new [base styles](https://tailwindcss.com/docs/plugins#adding-base-styles).
12+
13+
The [rgbChannels](./config/transform.js) transform removes the color space function for compatability with [Tailwind's opacity modifier syntax](https://tailwindcss.com/docs/text-color#changing-the-opacity).
14+
15+
```js
16+
import plugin from 'tailwindcss/plugin.js';
17+
18+
export default plugin(function ({ addBase }) {
19+
addBase({
20+
':root': {
21+
'--sd-text-small': '0.75',
22+
'--sd-text-base': '46 46 70',
23+
'--sd-text-secondary': '100 100 115',
24+
'--sd-text-tertiary': '129 129 142',
25+
'--sd-text-neutral': '0 0 0 / 0.55',
26+
'--sd-theme': '31 197 191',
27+
'--sd-theme-light': '153 235 226',
28+
'--sd-theme-dark': '0 179 172',
29+
'--sd-theme-secondary': '106 80 150',
30+
'--sd-theme-secondary-dark': '63 28 119',
31+
'--sd-theme-secondary-light': '196 178 225',
32+
},
33+
});
34+
});
35+
```
36+
37+
### themeColors.js
38+
39+
Tailwind theme color values that reference the plugin [css vars](https://tailwindcss.com/docs/customizing-colors#using-css-variables).
40+
41+
```js
42+
export default {
43+
'sd-text-base': 'rgb(var(--sd-text-base))',
44+
'sd-text-secondary': 'rgb(var(--sd-text-secondary))',
45+
'sd-text-tertiary': 'rgb(var(--sd-text-tertiary))',
46+
'sd-text-neutral': 'rgb(var(--sd-text-neutral))',
47+
'sd-theme': 'rgb(var(--sd-theme))',
48+
'sd-theme-light': 'rgb(var(--sd-theme-light))',
49+
'sd-theme-dark': 'rgb(var(--sd-theme-dark))',
50+
'sd-theme-secondary': 'rgb(var(--sd-theme-secondary))',
51+
'sd-theme-secondary-dark': 'rgb(var(--sd-theme-secondary-dark))',
52+
'sd-theme-secondary-light': 'rgb(var(--sd-theme-secondary-light))',
53+
};
54+
```
55+
56+
### preset.js
57+
58+
[Tailwind preset](https://tailwindcss.com/docs/presets) file that imports the colors and plugin.
59+
60+
```js
61+
import themeColors from './themeColors.js';
62+
import cssVarsPlugin from './cssVarsPlugin.js';
63+
64+
export default {
65+
theme: {
66+
extend: {
67+
colors: {
68+
...themeColors, // <-- theme colors defined here
69+
},
70+
},
71+
},
72+
plugins: [cssVarsPlugin], // <-- plugin imported here
73+
};
74+
```
75+
76+
## Building the CSS
77+
78+
The [Tailwind preset](https://tailwindcss.com/docs/presets#creating-a-preset) is imported from the build directory in `tailwind.config.js`.
79+
80+
```js
81+
import tailwindPreset from './build/tailwind/preset.js';
82+
83+
/** @type {import('tailwindcss').Config} */
84+
export default {
85+
theme: {
86+
extend: {},
87+
},
88+
presets: [tailwindPreset],
89+
content: ['./demo/**/*.{html,js}'],
90+
plugins: [],
91+
};
92+
```
93+
94+
Run `npm run build-css` to watch the `demo/index.html` file for changes -- any Tailwind classes used will be compiled into `demo/output.css`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import StyleDictionary from 'style-dictionary';
2+
import { isColor } from './config/filter.js';
3+
import { cssVarsPlugin, preset, themeColors } from './config/format.js';
4+
import { rgbChannels } from './config/transform.js';
5+
6+
StyleDictionary.registerTransform({
7+
name: 'color/rgb-channels',
8+
type: 'value',
9+
filter: isColor,
10+
transform: rgbChannels,
11+
});
12+
13+
StyleDictionary.registerTransformGroup({
14+
name: 'tailwind',
15+
transforms: ['name/kebab', 'color/rgb', 'color/rgb-channels'],
16+
});
17+
18+
StyleDictionary.registerFormat({
19+
name: 'tailwind/css-vars-plugin',
20+
format: cssVarsPlugin,
21+
});
22+
23+
StyleDictionary.registerFormat({
24+
name: 'tailwind/theme-colors',
25+
format: themeColors,
26+
});
27+
28+
StyleDictionary.registerFormat({
29+
name: 'tailwind/preset',
30+
format: preset,
31+
});
32+
33+
export default {
34+
source: ['./tokens/**/*.json'],
35+
platforms: {
36+
tailwindPreset: {
37+
buildPath: 'build/tailwind/',
38+
transformGroup: 'tailwind',
39+
files: [
40+
{
41+
destination: 'cssVarsPlugin.js',
42+
format: 'tailwind/css-vars-plugin',
43+
},
44+
{
45+
destination: 'themeColors.js',
46+
format: 'tailwind/theme-colors',
47+
},
48+
{
49+
destination: 'preset.js',
50+
format: 'tailwind/preset',
51+
},
52+
],
53+
},
54+
},
55+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isColor(token) {
2+
return (token?.$type || token?.type) === 'color';
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
import { isColor } from './filter.js';
4+
5+
describe('isColor', () => {
6+
it('should handle legacy and dtcg formats', () => {
7+
expect(isColor({ type: 'color' })).to.equal(true);
8+
expect(isColor({ $type: 'color' })).to.equal(true);
9+
expect(isColor({ type: 'fontSize' })).to.equal(false);
10+
expect(isColor({ $type: 'fontSize' })).to.equal(false);
11+
});
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { isColor } from './filter.js';
2+
3+
/**
4+
* Exports tailwind plugin for declaring root CSS vars
5+
* @see https://tailwindcss.com/docs/plugins#overview
6+
*/
7+
export function cssVarsPlugin({ dictionary }) {
8+
const vars = dictionary.allTokens
9+
.map((token) => {
10+
const value = token?.$value || token?.value;
11+
return `'--${token.name}': '${value}'`;
12+
})
13+
.join(',\n\t\t\t');
14+
15+
return `import plugin from 'tailwindcss/plugin.js';
16+
17+
export default plugin(function ({ addBase }) {
18+
\taddBase({
19+
\t\t':root': {
20+
\t\t\t${vars},
21+
\t\t},
22+
\t});
23+
});\n`;
24+
}
25+
26+
/**
27+
* Exports theme color definitions
28+
* @see https://tailwindcss.com/docs/customizing-colors#using-css-variables
29+
*/
30+
export function themeColors({ dictionary, options }) {
31+
const tokens = dictionary.allTokens.filter((token) => isColor(token, options));
32+
33+
const theme = tokens
34+
.map((token) => {
35+
return `\t'${token.name}': 'rgb(var(--${token.name}))'`;
36+
})
37+
.join(',\n');
38+
39+
return `export default {\n${theme},\n};\n`;
40+
}
41+
42+
/**
43+
* Exports tailwind preset
44+
* @see https://tailwindcss.com/docs/presets
45+
*/
46+
export function preset() {
47+
return `import themeColors from './themeColors.js';
48+
import cssVarsPlugin from './cssVarsPlugin.js';
49+
50+
export default {
51+
\ttheme: {
52+
\t\textend: {
53+
\t\t\tcolors: {
54+
\t\t\t\t...themeColors, // <-- theme colors defined here
55+
\t\t\t},
56+
\t\t},
57+
\t},
58+
\tplugins: [cssVarsPlugin], // <-- plugin imported here
59+
};\n`;
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function rgbChannels(token) {
2+
const value = token?.$value || token?.value;
3+
const { r, g, b, a } = parseRGBA(value);
4+
const hasAlpha = a !== undefined;
5+
return `${r} ${g} ${b}${hasAlpha ? ' / ' + a : ''}`;
6+
}
7+
8+
function parseRGBA(value) {
9+
const regex = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+%?))?\s*\)/;
10+
const matches = value.match(regex);
11+
if (!matches) {
12+
throw new Error(`Value '${value}' is not a valid rgb or rgba format.`);
13+
}
14+
const [, r, g, b, a] = matches;
15+
return {
16+
r,
17+
g,
18+
b,
19+
a: a !== undefined ? (a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a)) : undefined,
20+
};
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
import { rgbChannels } from './transform.js';
4+
5+
describe('rgbChannels', () => {
6+
it('should extract RGB channels from valid RGB string', () => {
7+
const tokenValue = 'rgb(255, 255, 255)';
8+
expect(rgbChannels({ value: tokenValue })).to.equal('255 255 255');
9+
expect(rgbChannels({ $value: tokenValue })).to.equal('255 255 255');
10+
});
11+
12+
it('should extract RGB and alpha channels from valid RGBA string', () => {
13+
const tokenValue = 'rgba(255, 255, 255, 0.5)';
14+
expect(rgbChannels({ value: tokenValue })).to.equal('255 255 255 / 0.5');
15+
expect(rgbChannels({ $value: tokenValue })).to.equal('255 255 255 / 0.5');
16+
});
17+
18+
it('should handle different whitespace variations', () => {
19+
let tokenValue = 'rgb( 123 , 45,67 )';
20+
expect(rgbChannels({ value: tokenValue })).to.equal('123 45 67');
21+
tokenValue = 'rgba( 12, 34 , 56 , 0.75 )';
22+
expect(rgbChannels({ value: tokenValue })).to.equal('12 34 56 / 0.75');
23+
});
24+
25+
it('should handle different alpha formats', () => {
26+
const tokenValues = ['rgb(1, 2, 3, 50%)', 'rgb(1, 2, 3, .5)', 'rgb(1, 2, 3, .50)'];
27+
for (const tokenValue of tokenValues) {
28+
expect(rgbChannels({ value: tokenValue })).to.equal('1 2 3 / 0.5');
29+
}
30+
});
31+
32+
it('should throw error for invalid RGB string', () => {
33+
const expectedErr = "Value 'mock' is not a valid rgb or rgba format.";
34+
expect(() => rgbChannels({ value: 'mock' })).to.throw(expectedErr);
35+
expect(() => rgbChannels({ $value: 'mock' })).to.throw(expectedErr);
36+
});
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<!--
7+
Run `npm run build-tokens` to generate build/preset.js.
8+
Then run `npm run build-css` to generate `output.css`.
9+
-->
10+
<link href="./output.css" rel="stylesheet" />
11+
</head>
12+
<body>
13+
<div class="h-screen w-full bg-sd-theme-secondary/10">
14+
<div class="p-4 grid grid-cols-1 gap-4 text-5xl">
15+
<span class="text-sd-theme-dark">Hello tokens</span>
16+
<span class="text-sd-theme-dark/50">Hello tokens</span>
17+
<span class="text-sd-theme-dark/30">Hello tokens</span>
18+
<span class="text-sd-theme-dark/20">Hello tokens</span>
19+
<span class="text-sd-theme-secondary-dark">Hello tokens</span>
20+
<span class="text-sd-theme-secondary-dark/50">Hello tokens</span>
21+
<span class="text-sd-theme-secondary-dark/30">Hello tokens</span>
22+
<span class="text-sd-theme-secondary-dark/20">Hello tokens</span>
23+
<span class="text-sd-text-neutral">Hello tokens</span>
24+
</div>
25+
</div>
26+
</body>
27+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "tailwind-preset",
3+
"version": "1.0.0",
4+
"description": "Builds tailwind preset from tokens",
5+
"type": "module",
6+
"scripts": {
7+
"build-tokens": "style-dictionary build --config ./config.js",
8+
"build-css": "npx tailwindcss -i ./demo/input.css -o ./demo/output.css --watch",
9+
"test": "mocha 'config/**/*test.js'"
10+
},
11+
"license": "Apache-2.0",
12+
"devDependencies": {
13+
"style-dictionary": "^4.0.0",
14+
"tailwindcss": "^3.4.15",
15+
"mocha": "^10.2.0",
16+
"chai": "^5.1.1"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import tailwindPreset from './build/tailwind/preset.js';
2+
3+
/** @type {import('tailwindcss').Config} */
4+
export default {
5+
theme: {
6+
extend: {},
7+
},
8+
presets: [tailwindPreset],
9+
content: ['./demo/**/*.{html,js}'],
10+
plugins: [],
11+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"sd": {
3+
"text": {
4+
"small": {
5+
"$value": "0.75",
6+
"$type": "fontSize"
7+
},
8+
"base": {
9+
"$type": "color",
10+
"$value": "#2E2E46"
11+
},
12+
"secondary": {
13+
"$type": "color",
14+
"$value": "#646473"
15+
},
16+
"tertiary": {
17+
"$type": "color",
18+
"$value": "#81818E"
19+
},
20+
"neutral": {
21+
"$type": "color",
22+
"$value": "#0000008C"
23+
}
24+
},
25+
"theme": {
26+
"$type": "color",
27+
"_": {
28+
"$value": "#1FC5BF"
29+
},
30+
"light": {
31+
"$value": "#99EBE2"
32+
},
33+
"dark": {
34+
"$value": "#00B3AC"
35+
},
36+
"secondary": {
37+
"_": {
38+
"$value": "#6A5096"
39+
},
40+
"dark": {
41+
"$value": "#3F1C77"
42+
},
43+
"light": {
44+
"$value": "#C4B2E1"
45+
}
46+
}
47+
}
48+
}
49+
}

‎package.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,16 @@
8282
"singleQuote": true,
8383
"arrowParens": "always",
8484
"trailingComma": "all",
85-
"printWidth": 100
85+
"printWidth": 100,
86+
"useTabs": false,
87+
"overrides": [
88+
{
89+
"files": "examples/advanced/tailwind-preset/**/*",
90+
"options": {
91+
"useTabs": true
92+
}
93+
}
94+
]
8695
},
8796
"repository": {
8897
"type": "git",

0 commit comments

Comments
 (0)
Please sign in to comment.