Skip to content

Commit

Permalink
feat(examples): add tailwind preset example (amzn#1344)
Browse files Browse the repository at this point in the history
Co-authored-by: Jessica Lynch <[email protected]>
  • Loading branch information
2 people authored and tkgroot committed Dec 9, 2024
1 parent 555b324 commit 263254a
Show file tree
Hide file tree
Showing 17 changed files with 410 additions and 13 deletions.
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} */
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} */
export default {
theme: {
extend: {},
},
presets: [tailwindPreset],
content: ['./demo/**/*.{html,js}'],
plugins: [],
};
Loading

0 comments on commit 263254a

Please sign in to comment.