diff --git a/.changeset/gentle-chairs-teach.md b/.changeset/gentle-chairs-teach.md new file mode 100644 index 000000000..583b61285 --- /dev/null +++ b/.changeset/gentle-chairs-teach.md @@ -0,0 +1,5 @@ +--- +'style-dictionary': minor +--- + +Add tailwind preset example, remove unused .editorconfig file diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 1cd9f64bd..000000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/docs/src/content/docs/getting-started/examples.md b/docs/src/content/docs/getting-started/examples.md index f4e4f83ca..c326351e8 100644 --- a/docs/src/content/docs/getting-started/examples.md +++ b/docs/src/content/docs/getting-started/examples.md @@ -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. diff --git a/examples/advanced/tailwind-preset/.gitignore b/examples/advanced/tailwind-preset/.gitignore new file mode 100644 index 000000000..162d145fa --- /dev/null +++ b/examples/advanced/tailwind-preset/.gitignore @@ -0,0 +1,4 @@ +demo/output.css +build +node_modules +package-lock.json \ No newline at end of file diff --git a/examples/advanced/tailwind-preset/README.md b/examples/advanced/tailwind-preset/README.md new file mode 100644 index 000000000..88f6d9cab --- /dev/null +++ b/examples/advanced/tailwind-preset/README.md @@ -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`. diff --git a/examples/advanced/tailwind-preset/config.js b/examples/advanced/tailwind-preset/config.js new file mode 100644 index 000000000..a95101d95 --- /dev/null +++ b/examples/advanced/tailwind-preset/config.js @@ -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', + }, + ], + }, + }, +}; diff --git a/examples/advanced/tailwind-preset/config/filter.js b/examples/advanced/tailwind-preset/config/filter.js new file mode 100644 index 000000000..eb5d3331f --- /dev/null +++ b/examples/advanced/tailwind-preset/config/filter.js @@ -0,0 +1,3 @@ +export function isColor(token) { + return (token?.$type || token?.type) === 'color'; +} diff --git a/examples/advanced/tailwind-preset/config/filter.test.js b/examples/advanced/tailwind-preset/config/filter.test.js new file mode 100644 index 000000000..28170f3e7 --- /dev/null +++ b/examples/advanced/tailwind-preset/config/filter.test.js @@ -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); + }); +}); diff --git a/examples/advanced/tailwind-preset/config/format.js b/examples/advanced/tailwind-preset/config/format.js new file mode 100644 index 000000000..175aa2ac9 --- /dev/null +++ b/examples/advanced/tailwind-preset/config/format.js @@ -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`; +} diff --git a/examples/advanced/tailwind-preset/config/transform.js b/examples/advanced/tailwind-preset/config/transform.js new file mode 100644 index 000000000..98ece8115 --- /dev/null +++ b/examples/advanced/tailwind-preset/config/transform.js @@ -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, + }; +} diff --git a/examples/advanced/tailwind-preset/config/transform.test.js b/examples/advanced/tailwind-preset/config/transform.test.js new file mode 100644 index 000000000..658fb1e13 --- /dev/null +++ b/examples/advanced/tailwind-preset/config/transform.test.js @@ -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); + }); +}); diff --git a/examples/advanced/tailwind-preset/demo/index.html b/examples/advanced/tailwind-preset/demo/index.html new file mode 100644 index 000000000..c15de998a --- /dev/null +++ b/examples/advanced/tailwind-preset/demo/index.html @@ -0,0 +1,27 @@ + + + + + + + + + +
+
+ Hello tokens + Hello tokens + Hello tokens + Hello tokens + Hello tokens + Hello tokens + Hello tokens + Hello tokens + Hello tokens +
+
+ + diff --git a/examples/advanced/tailwind-preset/demo/input.css b/examples/advanced/tailwind-preset/demo/input.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/examples/advanced/tailwind-preset/demo/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/advanced/tailwind-preset/package.json b/examples/advanced/tailwind-preset/package.json new file mode 100644 index 000000000..28e3886c3 --- /dev/null +++ b/examples/advanced/tailwind-preset/package.json @@ -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" + } +} diff --git a/examples/advanced/tailwind-preset/tailwind.config.js b/examples/advanced/tailwind-preset/tailwind.config.js new file mode 100644 index 000000000..ce066420f --- /dev/null +++ b/examples/advanced/tailwind-preset/tailwind.config.js @@ -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: [], +}; diff --git a/examples/advanced/tailwind-preset/tokens/tokens.json b/examples/advanced/tailwind-preset/tokens/tokens.json new file mode 100644 index 000000000..80eccdb37 --- /dev/null +++ b/examples/advanced/tailwind-preset/tokens/tokens.json @@ -0,0 +1,49 @@ +{ + "sd": { + "text": { + "small": { + "$value": "0.75", + "$type": "fontSize" + }, + "base": { + "$type": "color", + "$value": "#2E2E46" + }, + "secondary": { + "$type": "color", + "$value": "#646473" + }, + "tertiary": { + "$type": "color", + "$value": "#81818E" + }, + "neutral": { + "$type": "color", + "$value": "#0000008C" + } + }, + "theme": { + "$type": "color", + "_": { + "$value": "#1FC5BF" + }, + "light": { + "$value": "#99EBE2" + }, + "dark": { + "$value": "#00B3AC" + }, + "secondary": { + "_": { + "$value": "#6A5096" + }, + "dark": { + "$value": "#3F1C77" + }, + "light": { + "$value": "#C4B2E1" + } + } + } + } +} diff --git a/package.json b/package.json index 660bbfdfc..5aef0f706 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,16 @@ "singleQuote": true, "arrowParens": "always", "trailingComma": "all", - "printWidth": 100 + "printWidth": 100, + "useTabs": false, + "overrides": [ + { + "files": "examples/advanced/tailwind-preset/**/*", + "options": { + "useTabs": true + } + } + ] }, "repository": { "type": "git",