Skip to content

Commit 6324bf6

Browse files
committed
[Tailwind-email]: Add new config for tailwind-email
This is a greatly pared down version of the Tailwind config with the constraints of HTML email development in mind.
1 parent 1106dbc commit 6324bf6

File tree

8 files changed

+332
-0
lines changed

8 files changed

+332
-0
lines changed

src/tokens/config.ts

+24
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@ export default function getConfig(layers: string[]) {
5858
'tailwind/extract_component_styles'
5959
]
6060
},
61+
'tailwind-email': {
62+
transformGroup: 'tailwind/css',
63+
buildPath: 'tokens/tailwind-email/',
64+
preset: formatLayerPathPart(layers),
65+
files: [
66+
{
67+
destination: 'tokens.js',
68+
format: 'tailwind/tokens',
69+
filter: 'tw/filterTokens',
70+
options: {
71+
showFileHeader: false
72+
}
73+
},
74+
{
75+
destination: 'plugins/typography.js',
76+
format: 'tailwind/fonts',
77+
filter: 'tw/filterFonts',
78+
options: {
79+
showFileHeader: false
80+
}
81+
}
82+
],
83+
actions: ['tailwind/copy_static_files']
84+
},
6185
css: {
6286
transformGroup: 'custom/css',
6387
buildPath: 'tokens/css/',

src/tokens/transformTokens.ts

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import getConfig from './config'
44
// Register transforms
55
import './transformation/web'
66
import './transformation/tailwind'
7+
import './transformation/tailwind-email'
78
import './transformation/skia'
89
import './transformation/ios'
910
import './transformation/android'
@@ -54,3 +55,6 @@ for (const layer of layers) {
5455
StyleDictionaryExtended.buildPlatform('json-flat')
5556
StyleDictionaryExtended.buildPlatform('tailwind')
5657
}
58+
59+
const StyleDictionaryExtended = StyleDictionary.extend(getConfig(['universal']))
60+
StyleDictionaryExtended.buildPlatform('tailwind-email')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { join } = require('path')
2+
const { readdirSync, unlink, cpSync, rmdir, statSync } = require('fs')
3+
4+
const staticFilesPath = join(__dirname, './static')
5+
const staticFiles = readdirSync(staticFilesPath)
6+
7+
module.exports = {
8+
do: function (dictionary, config) {
9+
const targetDir = join(config.buildPath, config.preset)
10+
cpSync(staticFilesPath, targetDir, { recursive: true })
11+
},
12+
undo: function (dictionary, config) {
13+
staticFiles.forEach((file) => {
14+
const target = join(config.buildPath, config.preset, file)
15+
16+
if (statSync(target).isDirectory()) {
17+
rmdir(target)
18+
} else {
19+
unlink(target)
20+
}
21+
})
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import merge from 'lodash.merge'
2+
import { Formatter } from 'style-dictionary'
3+
4+
const themes = ['light', 'dark']
5+
6+
const kebabCase = (str: string) => str && str.toLowerCase().replaceAll(' ', '-')
7+
8+
/**
9+
* This function transforms tokens into a nested object
10+
* structure ready for Tailwind. The conditional statements
11+
* are largely to handle creating both static and dynamic
12+
* tokens. E.g. A given token needs three variants:
13+
* dynamic, static light, and static dark. This function gets
14+
* run twice: once to create all static tokens, and another
15+
* time to create the dynamic tokens which places values on
16+
* the parent (e.g. the root object, or 'legacy') and uses the
17+
* appropriate color variable.
18+
*/
19+
function createColorTokensFromGroup(tokens, staticTheme = true) {
20+
const colorTokens = {}
21+
tokens.forEach(({ type, name, ...t }) => {
22+
if (type === 'color') {
23+
/**
24+
* The following conditions are in order to properly group
25+
* color tokens and format into a nested object structure
26+
* for use in Tailwind.
27+
*/
28+
let colorGroup = colorTokens[t.attributes.type] ?? {}
29+
30+
const tItem = kebabCase(t.attributes.item)
31+
const tSubItem = kebabCase(t.attributes.subitem)
32+
33+
/**
34+
* `state` is for the deepest level on a token.
35+
* E.g. `icon` in colors.systemfeedback.success.icon
36+
*/
37+
if (t.attributes.state) {
38+
if (!staticTheme) {
39+
// If not on a static theme, do not place within `dark` or `light` groups
40+
colorTokens[tItem] = colorTokens[tItem] || {}
41+
const tokenGroup = colorTokens[tItem][tSubItem] ?? {}
42+
colorTokens[tItem][tSubItem] = merge(tokenGroup, {
43+
[t.attributes.state]: t.value
44+
})
45+
} else {
46+
// If on a static theme, place within `dark` or `light` groups
47+
const tokenGroup = colorGroup[tItem]
48+
colorGroup[tItem] = merge(tokenGroup, {
49+
[tSubItem]: t.value
50+
})
51+
}
52+
} else if (tSubItem) {
53+
/**
54+
* If not on a static theme AND theme is determined by `type`
55+
* property do not place within `dark` or `light` groups
56+
*/
57+
if (themes.includes(t.attributes.type) && !staticTheme) {
58+
const tokenGroup = colorTokens[tItem] ?? {}
59+
colorTokens[tItem] = merge(tokenGroup, {
60+
[tSubItem]: t.value
61+
})
62+
63+
/**
64+
* If not on a static theme AND theme is determined by `item`
65+
* property (e.g. legacy tokens) do not place within `dark`
66+
* or `light` groups
67+
*/
68+
} else if (themes.includes(t.attributes.item) && !staticTheme) {
69+
const tokenGroup = colorTokens[t.attributes.type] ?? {}
70+
colorTokens[t.attributes.type] = merge(tokenGroup, {
71+
[tSubItem]: t.value
72+
})
73+
} else {
74+
// If on a static theme, place within `dark` or `light` groups
75+
const tokenGroup = colorGroup[tItem]
76+
colorGroup[tItem] = merge(tokenGroup, {
77+
[tSubItem]: t.value
78+
})
79+
}
80+
81+
/**
82+
* If `item` property is the token name, don't nest inside object
83+
*/
84+
} else if (t.attributes.item) {
85+
colorGroup[tItem] = t.value
86+
87+
/**
88+
* If `item` property is the token name, set directly on colorGroup
89+
*/
90+
} else if (t.attributes.type) {
91+
colorGroup = t.value
92+
}
93+
94+
if (Object.keys(colorGroup).length > 0) {
95+
colorTokens[t.attributes.type] = colorGroup
96+
}
97+
}
98+
})
99+
return colorTokens
100+
}
101+
102+
export default (({ dictionary }) => {
103+
const colorTokens = createColorTokensFromGroup(dictionary.allTokens)
104+
105+
const borderRadii = new Map([['none', '0']])
106+
const spacing = new Map<string | number, string | number>([[0, 0]]) // Initialize with option for 0 spacing
107+
const gradients = new Map()
108+
const boxShadows = new Map([['none', 'none']])
109+
const dropShadows = new Map<string, string | string[]>([
110+
['none', '0 0 #0000']
111+
])
112+
113+
// Format all other tokens
114+
dictionary.allTokens.forEach(({ type, name, ...t }) => {
115+
const attributes = t.attributes!
116+
if (attributes.category === 'radius') {
117+
if (attributes.type === 'full') {
118+
borderRadii.set(attributes.type, '9999px')
119+
} else {
120+
borderRadii.set(attributes.type!, t.value)
121+
}
122+
} else if (attributes.category === 'spacing') {
123+
spacing.set(attributes.type!, t.value)
124+
} else if (type === 'custom-gradient') {
125+
const [, ...pathParts] = t.path
126+
gradients.set(pathParts.join('-'), t.value)
127+
} else if (type === 'custom-shadow') {
128+
const [, ...pathParts] = t.path
129+
boxShadows.set(
130+
pathParts
131+
.filter((v) => !['elevation', 'light', 'dark'].includes(v))
132+
.join('-')
133+
.replaceAll(' ', '-'),
134+
t.value.boxShadow
135+
)
136+
dropShadows.set(
137+
pathParts
138+
.filter((v) => !['elevation', 'light', 'dark'].includes(v))
139+
.join('-')
140+
.replaceAll(' ', '-'),
141+
t.value.dropShadow
142+
)
143+
}
144+
})
145+
146+
// Note: replace strips out 'light-mode' and 'dark-mode' inside media queries
147+
return `module.exports = ${JSON.stringify(
148+
{
149+
colors: colorTokens,
150+
spacing: Object.fromEntries(spacing),
151+
borderRadius: Object.fromEntries(borderRadii),
152+
boxShadow: Object.fromEntries(boxShadows),
153+
dropShadow: Object.fromEntries(dropShadows),
154+
gradients: Object.fromEntries(gradients)
155+
},
156+
null,
157+
' '.repeat(2)
158+
)}`
159+
}) as Formatter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import StyleDictionary from 'style-dictionary'
2+
3+
import twFilterTokens from '../tailwind/twFilterTokens'
4+
import twFilterFonts from '../tailwind/twFilterFonts'
5+
6+
import sizePx from '../web/sizePx'
7+
import twShadows from '../tailwind/twShadows'
8+
import webRadius from '../web/webRadius'
9+
import webSize from '../web/webSize'
10+
import webPadding from '../web/webPadding'
11+
import twFont from '../tailwind/twFont'
12+
import webGradient from '../web/webGradient'
13+
import formatFonts from '../tailwind/formatFonts'
14+
15+
import formatTokens from './formatTokens'
16+
import copyStaticFiles from './copyStaticFiles'
17+
18+
// Filters
19+
StyleDictionary.registerFilter({
20+
name: 'tw/filterTokens',
21+
matcher: twFilterTokens
22+
})
23+
24+
StyleDictionary.registerFilter({
25+
name: 'tw/filterFonts',
26+
matcher: twFilterFonts
27+
})
28+
29+
// Transforms
30+
StyleDictionary.registerTransform({
31+
name: 'size/px',
32+
...sizePx
33+
})
34+
StyleDictionary.registerTransform({
35+
name: 'tw/shadow',
36+
...twShadows
37+
})
38+
StyleDictionary.registerTransform({
39+
name: 'web/radius',
40+
...webRadius
41+
})
42+
StyleDictionary.registerTransform({
43+
name: 'web/size',
44+
...webSize
45+
})
46+
StyleDictionary.registerTransform({
47+
name: 'web/padding',
48+
...webPadding
49+
})
50+
StyleDictionary.registerTransform({
51+
name: 'tw/font',
52+
...twFont
53+
})
54+
StyleDictionary.registerTransform({
55+
name: 'web/gradient',
56+
...webGradient
57+
})
58+
59+
StyleDictionary.registerTransformGroup({
60+
name: 'tailwind/css',
61+
transforms: StyleDictionary.transformGroup.css.concat([
62+
'size/px',
63+
'tw/shadow',
64+
'web/radius',
65+
'web/size',
66+
'web/padding',
67+
'tw/font',
68+
'web/gradient'
69+
])
70+
})
71+
72+
StyleDictionary.registerFormat({
73+
name: 'tailwind/tokens',
74+
formatter: formatTokens
75+
})
76+
77+
StyleDictionary.registerFormat({
78+
name: 'tailwind/fonts',
79+
formatter: formatFonts
80+
})
81+
82+
// Actions
83+
StyleDictionary.registerAction({
84+
name: 'tailwind/copy_static_files',
85+
do: copyStaticFiles.do,
86+
undo: copyStaticFiles.undo
87+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const {
2+
colors,
3+
boxShadow,
4+
dropShadow,
5+
gradients,
6+
borderRadius,
7+
spacing
8+
} = require('./tokens')
9+
10+
/** @type {import('tailwindcss').Config} */
11+
module.exports = {
12+
theme: {
13+
boxShadow: {},
14+
borderRadius: {},
15+
spacing: {},
16+
dropShadow: {},
17+
colors: {},
18+
extend: {
19+
boxShadow,
20+
borderRadius,
21+
spacing,
22+
dropShadow,
23+
colors: colors,
24+
backgroundImage: {
25+
...gradients
26+
}
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "commonjs"
3+
}

src/tokens/transformation/tailwind/formatFonts.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import defaultTwTheme from 'tailwindcss/defaultTheme'
12
import { Formatter } from 'style-dictionary'
23

34
export default (({ dictionary }) => {
@@ -17,6 +18,8 @@ export default (({ dictionary }) => {
1718
fontClass += `-${attributes.state}`
1819
}
1920

21+
t.value.fontFamily = `${t.value.fontFamily},${defaultTwTheme.fontFamily.sans.join(',')}`
22+
2023
fontClasses.set(fontClass, t.value)
2124
})
2225

0 commit comments

Comments
 (0)