Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): add tailwind preset example #1344

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/advanced/tailwind-preset/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
demo/output.css
build
node_modules
package-lock.json
85 changes: 85 additions & 0 deletions examples/advanced/tailwind-preset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Tailwind preset

Builds [Tailwind preset](https://tailwindcss.com/docs/presets#creating-a-preset) from tokens.

## Building the preset

Run `npm run build-tokens` to generate files in `build/tailwind/`.

### cssVarPlugin.js

A [Tailwind plugin](https://tailwindcss.com/docs/plugins) for registering new base styles.

Token values are transformed into space-separated RGB channels for compatability with [Tailwind's opacity modifier syntax](https://tailwindcss.com/docs/customizing-colors#using-css-variables).

```js
import plugin from 'tailwindcss/plugin';

export default plugin(function ({ addBase }) {
addBase({
':root': {
'--sd-text-small': '0.75',
'--sd-text-base': '#2E2E46',
'--sd-text-secondary': '100 100 115',
'--sd-text-tertiary': '129 129 142',
'--sd-theme': '31 197 191',
'--sd-theme-light': '153 235 226',
'--sd-theme-dark': '0 179 172',
'--sd-theme-secondary': '106 80 150',
'--sd-theme-secondary-dark': '63 28 119',
'--sd-theme-secondary-light': '196 178 225',
},
});
});
```

### themeColors.js

Tailwind theme color values that reference the plugin css vars.

```js
export default {
'sd-text-secondary': 'rgb(var(--sd-text-secondary) / <alpha-value>)',
'sd-text-tertiary': 'rgb(var(--sd-text-tertiary) / <alpha-value>)',
'sd-theme': 'rgb(var(--sd-theme) / <alpha-value>)',
'sd-theme-light': 'rgb(var(--sd-theme-light) / <alpha-value>)',
'sd-theme-dark': 'rgb(var(--sd-theme-dark) / <alpha-value>)',
'sd-theme-secondary': 'rgb(var(--sd-theme-secondary) / <alpha-value>)',
'sd-theme-secondary-dark': 'rgb(var(--sd-theme-secondary-dark) / <alpha-value>)',
'sd-theme-secondary-light': 'rgb(var(--sd-theme-secondary-light) / <alpha-value>)',
};
```

### preset.js

[Tailwind preset](https://tailwindcss.com/docs/presets) file that imports the colors and plugin.

```js
import themeColors from './themeColors.js';
import cssVarsPlugin from './cssVarsPlugin.js';

export default {
theme: {
extend: {
colors: {
...themeColors, // <-- theme colors
},
},
},
plugins: [cssVarsPlugin], // <-- plugin
};
```

## Building the CSS

The [Tailwind preset](https://tailwindcss.com/docs/presets#creating-a-preset) is imported from the build directory in `tailwind.config.js`.

```js
/** @type {import('tailwindcss').Config} */
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as other comment, let's try using ESM over CJS

module.exports = {
presets: [require('./build/tailwind/preset')], // <-- preset imported here
content: ['./demo/**/*.{html,js}'], // <-- files to watch
};
```

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`.
56 changes: 56 additions & 0 deletions examples/advanced/tailwind-preset/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import StyleDictionary from 'style-dictionary';
import { isColor } from './config/filter.js';
import { cssVarsPlugin, preset, themeColors } from './config/format.js';
import { rgbChannels } from './config/transform.js';

StyleDictionary.registerTransform({
name: 'color/rgb-channels',
type: 'value',
filter: isColor,
transform: rgbChannels,
});

StyleDictionary.registerTransformGroup({
name: 'color/tailwind',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
name: 'color/tailwind',
name: 'tailwind',

I think our convention for transformGroups is that they are generally called after certain platform output types, and if we call it tailwind we can easily add other transforms that are necessary for tailwind output if needed in the future (e.g. for dimensions or whatever other types of tokens), without that being confusing.

transforms: ['name/kebab', 'color/rgb', 'color/rgb-channels'],
});

StyleDictionary.registerFormat({
name: 'tailwind/css-vars-plugin',
format: cssVarsPlugin,
});

StyleDictionary.registerFormat({
name: 'tailwind/theme-colors',
format: themeColors,
});

StyleDictionary.registerFormat({
name: 'tailwind/preset',
format: preset,
});

export default {
source: ['./tokens/**/*.json'],
platforms: {
tailwindPreset: {
buildPath: 'build/tailwind/',
transformGroup: 'color/tailwind',
usesDtcg: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm I think this can be left out, as Style Dictionary's auto-detection for whether a tokenset is DTCG or not is pretty decent and this way the example will also be valid for folks who just want to copy this example and paste in their own tokens which may not be DTCG formatted.

I kind of regret making this a user configurable platform option, in hindsight, but this may all change in a future v5 where hopefully we can just use DTCG at all times and just pre-convert people's tokens to DTCG if needed (see #1352)

files: [
{
destination: 'cssVarsPlugin.js',
format: 'tailwind/css-vars-plugin',
},
{
destination: 'themeColors.js',
format: 'tailwind/theme-colors',
},
{
destination: 'preset.js',
format: 'tailwind/preset',
},
],
},
},
};
3 changes: 3 additions & 0 deletions examples/advanced/tailwind-preset/config/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isColor = (token, options) => {
return (options?.usesDtcg ? token.$type : token.type) === 'color';
};
15 changes: 15 additions & 0 deletions examples/advanced/tailwind-preset/config/filter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { isColor } from './filter.js';

describe('isColor', () => {
it('should handle legacy and dtcg formats', () => {
expect(isColor({ type: 'color' }, { usesDtcg: false })).to.equal(true);
expect(isColor({ type: 'color' }, { usesDtcg: true })).to.equal(false);
expect(isColor({ type: 'fontSize' }, { usesDtcg: false })).to.equal(false);

expect(isColor({ $type: 'color' }, { usesDtcg: true })).to.equal(true);
expect(isColor({ $type: 'color' }, { usesDtcg: false })).to.equal(false);
expect(isColor({ $type: 'fontSize' }, { usesDtcg: true })).to.equal(false);
});
});
56 changes: 56 additions & 0 deletions examples/advanced/tailwind-preset/config/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { isColor } from './filter.js';

/**
* Exports tailwind plugin for declaring root CSS vars
* @see https://tailwindcss.com/docs/plugins#overview
*/
export const cssVarsPlugin = ({ dictionary, options }) => {
const vars = dictionary.allTokens
.map((token) => {
const value = options.usesDtcg ? token.$value : token.value;
return `'--${token.name}': '${value}'`;
})
.join(',\n ');

return `import plugin from 'tailwindcss/plugin';

export default plugin(function ({ addBase }) {
addBase({
':root': {
${vars},
},
});
});\n`;
};
Comment on lines +13 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

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

So, this is a bit of a nitpick so feel free to ignore it, in fact, none of our examples or formats are doing this, but it occurred to me that it may be better to use tab characters over spaces:

Suggested change
.join(',\n ');
return `import plugin from 'tailwindcss/plugin';
export default plugin(function ({ addBase }) {
addBase({
':root': {
${vars},
},
});
});\n`;
};
.join(',\n\t\t\t');
return `import plugin from 'tailwindcss/plugin';
export default plugin(function ({ addBase }) {
\taddBase({
\t\t':root': {
\t\t\t${vars},
\t\t},
\t});
});\n`;
};

This would be more inclusive to user preferences (tab width, a11y reasons).

I'm just assuming this would be \t (tab character), but definitely test it for yourself to see if that works 😅 I haven't tested this.

If this ends up working well I'll raise an issue to improve this in all of our examples & templates, you'd have set the first proper example of it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll definitely try that out. I wasn't aware of the accessibility issues- thanks for sharing.


/**
* Exports colors as space-separated RGB channels
*/
export const themeColors = ({ dictionary, options }) => {
const tokens = dictionary.allTokens.filter((token) => isColor(token, options));

const theme = tokens
.map((token) => ` '${token.name}': 'rgb(var(--${token.name}) / <alpha-value>)'`)
.join(',\n');
return `export default {\n${theme},\n};\n`;
};

/**
* Exports tailwind preset
* @see https://tailwindcss.com/docs/presets
*/
export const preset = () => {
return `import themeColors from './themeColors.js';
import cssVarsPlugin from './cssVarsPlugin.js';

export default {
theme: {
extend: {
colors: {
...themeColors, // <-- theme colors defined here
},
},
},
plugins: [cssVarsPlugin], // <-- plugin imported here
};\n`;
};
10 changes: 10 additions & 0 deletions examples/advanced/tailwind-preset/config/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const rgbChannels = (token, options) => {
const regex = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should take into account that values may be of format rgba(r, g, b, a) (try it e.g. #FF00008C, the "color/rgb" transform outputs rgba(255, 0, 0, 0.55))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooo, good catch, I'll revise.

const value = options.usesDtcg ? token.$value : token.value;

const matches = value.match(regex);
if (!matches) {
throw new Error(`Value '${value}' is not a valid rgb format.`);
}
return `${matches[1]} ${matches[2]} ${matches[3]}`;
};
23 changes: 23 additions & 0 deletions examples/advanced/tailwind-preset/config/transform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { rgbChannels } from './transform.js';

describe('rgbChannels', () => {
it('should extract RGB channels from valid RGB string', () => {
const token = { value: 'rgb(255, 255, 255)' };
expect(rgbChannels(token, { usesDtcg: false })).to.equal('255 255 255');

const dtcgToken = { $value: 'rgb(1, 2, 3)' };
expect(rgbChannels(dtcgToken, { usesDtcg: true })).to.equal('1 2 3');
});

it('should throw error for invalid RGB string', () => {
const expectedErr = "Value 'mock' is not a valid rgb format.";

const token = { value: 'mock' };
expect(() => rgbChannels(token, { usesDtcg: false })).to.throw(expectedErr);

const dtcgToken = { $value: 'mock' };
expect(() => rgbChannels(dtcgToken, { usesDtcg: true })).to.throw(expectedErr);
});
});
24 changes: 24 additions & 0 deletions examples/advanced/tailwind-preset/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--
Run `npm run build-tokens` to generate build/preset.js.
Then run `npm run build-css` to generate `output.css`.
-->
<link href="./output.css" rel="stylesheet" />
</head>
<body>
<div class="h-screen w-full bg-sd-theme-secondary/10">
<div class="p-4 grid grid-cols-1 gap-4 text-5xl">
<span class="text-sd-theme-dark">Hello tokens</span>
<span class="text-sd-theme">Hello tokens</span>
<span class="text-sd-theme-light">Hello tokens</span>
<span class="text-sd-theme-secondary-dark">Hello tokens</span>
<span class="text-sd-theme-secondary">Hello tokens</span>
<span class="text-sd-theme-secondary-light">Hello tokens</span>
</div>
</div>
</body>
</html>
3 changes: 3 additions & 0 deletions examples/advanced/tailwind-preset/demo/input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
18 changes: 18 additions & 0 deletions examples/advanced/tailwind-preset/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "tailwind-preset",
"version": "1.0.0",
"description": "Builds tailwind preset from tokens",
"type": "module",
"scripts": {
"build-tokens": "style-dictionary build --config ./config.js",
"build-css": "npx tailwindcss -i ./demo/input.css -o ./demo/output.css --watch",
"test": "mocha 'config/**/*test.js'"
},
"license": "Apache-2.0",
"devDependencies": {
"style-dictionary": "^4.0.0",
"tailwindcss": "~3.4.12",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you expect breaking changes for this example in minor bumps for tailwind? Otherwise I'd go with ^3.4.12

"mocha": "^10.2.0",
"chai": "^5.0.0-alpha.2"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Stable v5 was released recently 🎉 so we can use ^5.1.1

I definitely appreciate you putting some tests in the tailwind example, that's neat!

}
}
5 changes: 5 additions & 0 deletions examples/advanced/tailwind-preset/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('tailwindcss').Config} */
Copy link
Collaborator

Choose a reason for hiding this comment

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

Tailwind supports ESM which is the more modern format, so let's use that everywhere instead of CJS

module.exports = {
presets: [require('./build/tailwind/preset')],
content: ['./demo/**/*.{html,js}'],
};
45 changes: 45 additions & 0 deletions examples/advanced/tailwind-preset/tokens/tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"sd": {
"text": {
"small": {
"$value": "0.75",
"$type": "fontSize"
},
"base": {
"type$": "color",
"$value": "#2E2E46"
},
"secondary": {
"$type": "color",
"$value": "#646473"
},
"tertiary": {
"$type": "color",
"$value": "#81818E"
}
},
"theme": {
"$type": "color",
"_": {
"$value": "#1FC5BF"
},
"light": {
"$value": "#99EBE2"
},
"dark": {
"$value": "#00B3AC"
},
"secondary": {
"_": {
"$value": "#6A5096"
},
"dark": {
"$value": "#3F1C77"
},
"light": {
"$value": "#C4B2E1"
}
}
}
}
}