-
Notifications
You must be signed in to change notification settings - Fork 544
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
base: main
Are you sure you want to change the base?
Changes from all commits
dd56b25
6d35833
4dd2a6f
704894c
22f32bc
5bfce92
fba6d51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
demo/output.css | ||
build | ||
node_modules | ||
package-lock.json |
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} */ | ||
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`. |
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', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||||||
}, | ||||||
], | ||||||
}, | ||||||
}, | ||||||
}; |
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'; | ||
}; |
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); | ||
}); | ||
}); |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
This would be more inclusive to user preferences (tab width, a11y reasons). I'm just assuming this would be 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
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+)\)/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should take into account that values may be of format There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]}`; | ||
}; |
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); | ||
}); | ||
}); |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; |
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
"mocha": "^10.2.0", | ||
"chai": "^5.0.0-alpha.2" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stable v5 was released recently 🎉 so we can use I definitely appreciate you putting some tests in the tailwind example, that's neat! |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import('tailwindcss').Config} */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}'], | ||
}; |
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" | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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