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

Merged
merged 2 commits into from
Dec 8, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/gentle-chairs-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'style-dictionary': minor
---

Add tailwind preset example, remove unused .editorconfig file
12 changes: 0 additions & 12 deletions .editorconfig

This file was deleted.

1 change: 1 addition & 0 deletions docs/src/content/docs/getting-started/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ If you want to look at more advanced examples of possible applications and custo
- [**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.
- [**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.
- [**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.
- [**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.
- [**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.
- [**transitive-transforms**](https://github.com/amzn/style-dictionary/tree/main/examples/advanced/transitive-transforms) shows how to use transitive transforms to transform references
- [**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.
Expand Down
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
94 changes: 94 additions & 0 deletions examples/advanced/tailwind-preset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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 these files in `build/tailwind`:

### cssVarPlugin.js

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

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

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

export default plugin(function ({ addBase }) {
addBase({
':root': {
'--sd-text-small': '0.75',
'--sd-text-base': '46 46 70',
'--sd-text-secondary': '100 100 115',
'--sd-text-tertiary': '129 129 142',
'--sd-text-neutral': '0 0 0 / 0.55',
'--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](https://tailwindcss.com/docs/customizing-colors#using-css-variables).

```js
export default {
'sd-text-base': 'rgb(var(--sd-text-base))',
'sd-text-secondary': 'rgb(var(--sd-text-secondary))',
'sd-text-tertiary': 'rgb(var(--sd-text-tertiary))',
'sd-text-neutral': 'rgb(var(--sd-text-neutral))',
'sd-theme': 'rgb(var(--sd-theme))',
'sd-theme-light': 'rgb(var(--sd-theme-light))',
'sd-theme-dark': 'rgb(var(--sd-theme-dark))',
'sd-theme-secondary': 'rgb(var(--sd-theme-secondary))',
'sd-theme-secondary-dark': 'rgb(var(--sd-theme-secondary-dark))',
'sd-theme-secondary-light': 'rgb(var(--sd-theme-secondary-light))',
};
```

### 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 defined here
},
},
},
plugins: [cssVarsPlugin], // <-- plugin imported here
};
```

## 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
import tailwindPreset from './build/tailwind/preset.js';

/** @type {import('tailwindcss').Config} */
jessicalynch marked this conversation as resolved.
Show resolved Hide resolved
export default {
theme: {
extend: {},
},
presets: [tailwindPreset],
content: ['./demo/**/*.{html,js}'],
plugins: [],
};
```

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`.
55 changes: 55 additions & 0 deletions examples/advanced/tailwind-preset/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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: 'tailwind',
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: 'tailwind',
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 function isColor(token) {
return (token?.$type || token?.type) === 'color';
}
12 changes: 12 additions & 0 deletions examples/advanced/tailwind-preset/config/filter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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' })).to.equal(true);
expect(isColor({ $type: 'color' })).to.equal(true);
expect(isColor({ type: 'fontSize' })).to.equal(false);
expect(isColor({ $type: 'fontSize' })).to.equal(false);
});
});
60 changes: 60 additions & 0 deletions examples/advanced/tailwind-preset/config/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isColor } from './filter.js';

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

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

export default plugin(function ({ addBase }) {
\taddBase({
\t\t':root': {
\t\t\t${vars},
\t\t},
\t});
});\n`;
}

/**
* Exports theme color definitions
* @see https://tailwindcss.com/docs/customizing-colors#using-css-variables
*/
export function themeColors({ dictionary, options }) {
const tokens = dictionary.allTokens.filter((token) => isColor(token, options));

const theme = tokens
.map((token) => {
return `\t'${token.name}': 'rgb(var(--${token.name}))'`;
})
.join(',\n');

return `export default {\n${theme},\n};\n`;
}

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

export default {
\ttheme: {
\t\textend: {
\t\t\tcolors: {
\t\t\t\t...themeColors, // <-- theme colors defined here
\t\t\t},
\t\t},
\t},
\tplugins: [cssVarsPlugin], // <-- plugin imported here
};\n`;
}
21 changes: 21 additions & 0 deletions examples/advanced/tailwind-preset/config/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function rgbChannels(token) {
const value = token?.$value || token?.value;
const { r, g, b, a } = parseRGBA(value);
const hasAlpha = a !== undefined;
return `${r} ${g} ${b}${hasAlpha ? ' / ' + a : ''}`;
}

function parseRGBA(value) {
const regex = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+%?))?\s*\)/;
const matches = value.match(regex);
if (!matches) {
throw new Error(`Value '${value}' is not a valid rgb or rgba format.`);
}
const [, r, g, b, a] = matches;
return {
r,
g,
b,
a: a !== undefined ? (a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a)) : undefined,
};
}
37 changes: 37 additions & 0 deletions examples/advanced/tailwind-preset/config/transform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 tokenValue = 'rgb(255, 255, 255)';
expect(rgbChannels({ value: tokenValue })).to.equal('255 255 255');
expect(rgbChannels({ $value: tokenValue })).to.equal('255 255 255');
});

it('should extract RGB and alpha channels from valid RGBA string', () => {
const tokenValue = 'rgba(255, 255, 255, 0.5)';
expect(rgbChannels({ value: tokenValue })).to.equal('255 255 255 / 0.5');
expect(rgbChannels({ $value: tokenValue })).to.equal('255 255 255 / 0.5');
});

it('should handle different whitespace variations', () => {
let tokenValue = 'rgb( 123 , 45,67 )';
expect(rgbChannels({ value: tokenValue })).to.equal('123 45 67');
tokenValue = 'rgba( 12, 34 , 56 , 0.75 )';
expect(rgbChannels({ value: tokenValue })).to.equal('12 34 56 / 0.75');
});

it('should handle different alpha formats', () => {
const tokenValues = ['rgb(1, 2, 3, 50%)', 'rgb(1, 2, 3, .5)', 'rgb(1, 2, 3, .50)'];
for (const tokenValue of tokenValues) {
expect(rgbChannels({ value: tokenValue })).to.equal('1 2 3 / 0.5');
}
});

it('should throw error for invalid RGB string', () => {
const expectedErr = "Value 'mock' is not a valid rgb or rgba format.";
expect(() => rgbChannels({ value: 'mock' })).to.throw(expectedErr);
expect(() => rgbChannels({ $value: 'mock' })).to.throw(expectedErr);
});
});
27 changes: 27 additions & 0 deletions examples/advanced/tailwind-preset/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!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-dark/50">Hello tokens</span>
<span class="text-sd-theme-dark/30">Hello tokens</span>
<span class="text-sd-theme-dark/20">Hello tokens</span>
<span class="text-sd-theme-secondary-dark">Hello tokens</span>
<span class="text-sd-theme-secondary-dark/50">Hello tokens</span>
<span class="text-sd-theme-secondary-dark/30">Hello tokens</span>
<span class="text-sd-theme-secondary-dark/20">Hello tokens</span>
<span class="text-sd-text-neutral">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.15",
"mocha": "^10.2.0",
"chai": "^5.1.1"
}
}
11 changes: 11 additions & 0 deletions examples/advanced/tailwind-preset/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import tailwindPreset from './build/tailwind/preset.js';

/** @type {import('tailwindcss').Config} */
jessicalynch marked this conversation as resolved.
Show resolved Hide resolved
export default {
theme: {
extend: {},
},
presets: [tailwindPreset],
content: ['./demo/**/*.{html,js}'],
plugins: [],
};
Loading