Skip to content

Commit

Permalink
[Tailwind-email]: Add new config for tailwind-email
Browse files Browse the repository at this point in the history
This is a greatly pared down version of the Tailwind config with
the constraints of HTML email development in mind.
  • Loading branch information
AlanBreck committed Aug 27, 2024
1 parent 862f4d1 commit e158f7b
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/tokens/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ export default function getConfig(layers: string[]) {
'tailwind/extract_component_styles'
]
},
'tailwind-email': {
transformGroup: 'tailwind/css',
buildPath: 'tokens/tailwind-email/',
preset: formatLayerPathPart(layers),
files: [
{
destination: 'tokens.js',
format: 'tailwind/tokens',
filter: 'tw/filterTokens',
options: {
showFileHeader: false
}
},
{
destination: 'plugins/typography.js',
format: 'tailwind/fonts',
filter: 'tw/filterFonts',
options: {
showFileHeader: false
}
}
],
actions: ['tailwind/copy_static_files']
},
css: {
transformGroup: 'custom/css',
buildPath: 'tokens/css/',
Expand Down
4 changes: 4 additions & 0 deletions src/tokens/transformTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import getConfig from './config'
// Register transforms
import './transformation/web'
import './transformation/tailwind'
import './transformation/tailwind-email'
import './transformation/skia'
import './transformation/ios'
import './transformation/android'
Expand Down Expand Up @@ -54,3 +55,6 @@ for (const layer of layers) {
StyleDictionaryExtended.buildPlatform('json-flat')
StyleDictionaryExtended.buildPlatform('tailwind')
}

const StyleDictionaryExtended = StyleDictionary.extend(getConfig(['universal']))
StyleDictionaryExtended.buildPlatform('tailwind-email')
23 changes: 23 additions & 0 deletions src/tokens/transformation/tailwind-email/copyStaticFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { join } = require('path')
const { readdirSync, unlink, cpSync, rmdir, statSync } = require('fs')

const staticFilesPath = join(__dirname, './static')
const staticFiles = readdirSync(staticFilesPath)

module.exports = {
do: function (dictionary, config) {
const targetDir = join(config.buildPath, config.preset)
cpSync(staticFilesPath, targetDir, { recursive: true })
},
undo: function (dictionary, config) {
staticFiles.forEach((file) => {
const target = join(config.buildPath, config.preset, file)

if (statSync(target).isDirectory()) {
rmdir(target)
} else {
unlink(target)
}
})
}
}
159 changes: 159 additions & 0 deletions src/tokens/transformation/tailwind-email/formatTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import merge from 'lodash.merge'
import { Formatter } from 'style-dictionary'

const themes = ['light', 'dark']

const kebabCase = (str: string) => str && str.toLowerCase().replaceAll(' ', '-')

/**
* This function transforms tokens into a nested object
* structure ready for Tailwind. The conditional statements
* are largely to handle creating both static and dynamic
* tokens. E.g. A given token needs three variants:
* dynamic, static light, and static dark. This function gets
* run twice: once to create all static tokens, and another
* time to create the dynamic tokens which places values on
* the parent (e.g. the root object, or 'legacy') and uses the
* appropriate color variable.
*/
function createColorTokensFromGroup(tokens, staticTheme = true) {
const colorTokens = {}
tokens.forEach(({ type, name, ...t }) => {
if (type === 'color') {
/**
* The following conditions are in order to properly group
* color tokens and format into a nested object structure
* for use in Tailwind.
*/
let colorGroup = colorTokens[t.attributes.type] ?? {}

const tItem = kebabCase(t.attributes.item)
const tSubItem = kebabCase(t.attributes.subitem)

/**
* `state` is for the deepest level on a token.
* E.g. `icon` in colors.systemfeedback.success.icon
*/
if (t.attributes.state) {
if (!staticTheme) {
// If not on a static theme, do not place within `dark` or `light` groups
colorTokens[tItem] = colorTokens[tItem] || {}
const tokenGroup = colorTokens[tItem][tSubItem] ?? {}
colorTokens[tItem][tSubItem] = merge(tokenGroup, {
[t.attributes.state]: t.value
})
} else {
// If on a static theme, place within `dark` or `light` groups
const tokenGroup = colorGroup[tItem]
colorGroup[tItem] = merge(tokenGroup, {
[tSubItem]: t.value
})
}
} else if (tSubItem) {
/**
* If not on a static theme AND theme is determined by `type`
* property do not place within `dark` or `light` groups
*/
if (themes.includes(t.attributes.type) && !staticTheme) {
const tokenGroup = colorTokens[tItem] ?? {}
colorTokens[tItem] = merge(tokenGroup, {
[tSubItem]: t.value
})

/**
* If not on a static theme AND theme is determined by `item`
* property (e.g. legacy tokens) do not place within `dark`
* or `light` groups
*/
} else if (themes.includes(t.attributes.item) && !staticTheme) {
const tokenGroup = colorTokens[t.attributes.type] ?? {}
colorTokens[t.attributes.type] = merge(tokenGroup, {
[tSubItem]: t.value
})
} else {
// If on a static theme, place within `dark` or `light` groups
const tokenGroup = colorGroup[tItem]
colorGroup[tItem] = merge(tokenGroup, {
[tSubItem]: t.value
})
}

/**
* If `item` property is the token name, don't nest inside object
*/
} else if (t.attributes.item) {
colorGroup[tItem] = t.value

/**
* If `item` property is the token name, set directly on colorGroup
*/
} else if (t.attributes.type) {
colorGroup = t.value
}

if (Object.keys(colorGroup).length > 0) {
colorTokens[t.attributes.type] = colorGroup
}
}
})
return colorTokens
}

export default (({ dictionary }) => {
const colorTokens = createColorTokensFromGroup(dictionary.allTokens)

const borderRadii = new Map([['none', '0']])
const spacing = new Map<string | number, string | number>([[0, 0]]) // Initialize with option for 0 spacing
const gradients = new Map()
const boxShadows = new Map([['none', 'none']])
const dropShadows = new Map<string, string | string[]>([
['none', '0 0 #0000']
])

// Format all other tokens
dictionary.allTokens.forEach(({ type, name, ...t }) => {
const attributes = t.attributes!
if (attributes.category === 'radius') {
if (attributes.type === 'full') {
borderRadii.set(attributes.type, '9999px')
} else {
borderRadii.set(attributes.type!, t.value)
}
} else if (attributes.category === 'spacing') {
spacing.set(attributes.type!, t.value)
} else if (type === 'custom-gradient') {
const [, ...pathParts] = t.path
gradients.set(pathParts.join('-'), t.value)
} else if (type === 'custom-shadow') {
const [, ...pathParts] = t.path
boxShadows.set(
pathParts
.filter((v) => !['elevation', 'light', 'dark'].includes(v))
.join('-')
.replaceAll(' ', '-'),
t.value.boxShadow
)
dropShadows.set(
pathParts
.filter((v) => !['elevation', 'light', 'dark'].includes(v))
.join('-')
.replaceAll(' ', '-'),
t.value.dropShadow
)
}
})

// Note: replace strips out 'light-mode' and 'dark-mode' inside media queries
return `module.exports = ${JSON.stringify(
{
colors: colorTokens,
spacing: Object.fromEntries(spacing),
borderRadius: Object.fromEntries(borderRadii),
boxShadow: Object.fromEntries(boxShadows),
dropShadow: Object.fromEntries(dropShadows),
gradients: Object.fromEntries(gradients)
},
null,
' '.repeat(2)
)}`
}) as Formatter
87 changes: 87 additions & 0 deletions src/tokens/transformation/tailwind-email/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import StyleDictionary from 'style-dictionary'

import twFilterTokens from '../tailwind/twFilterTokens'
import twFilterFonts from '../tailwind/twFilterFonts'

import sizePx from '../web/sizePx'
import twShadows from '../tailwind/twShadows'
import webRadius from '../web/webRadius'
import webSize from '../web/webSize'
import webPadding from '../web/webPadding'
import twFont from '../tailwind/twFont'
import webGradient from '../web/webGradient'
import formatFonts from '../tailwind/formatFonts'

import formatTokens from './formatTokens'
import copyStaticFiles from './copyStaticFiles'

// Filters
StyleDictionary.registerFilter({
name: 'tw/filterTokens',
matcher: twFilterTokens
})

StyleDictionary.registerFilter({
name: 'tw/filterFonts',
matcher: twFilterFonts
})

// Transforms
StyleDictionary.registerTransform({
name: 'size/px',
...sizePx
})
StyleDictionary.registerTransform({
name: 'tw/shadow',
...twShadows
})
StyleDictionary.registerTransform({
name: 'web/radius',
...webRadius
})
StyleDictionary.registerTransform({
name: 'web/size',
...webSize
})
StyleDictionary.registerTransform({
name: 'web/padding',
...webPadding
})
StyleDictionary.registerTransform({
name: 'tw/font',
...twFont
})
StyleDictionary.registerTransform({
name: 'web/gradient',
...webGradient
})

StyleDictionary.registerTransformGroup({
name: 'tailwind/css',
transforms: StyleDictionary.transformGroup.css.concat([
'size/px',
'tw/shadow',
'web/radius',
'web/size',
'web/padding',
'tw/font',
'web/gradient'
])
})

StyleDictionary.registerFormat({
name: 'tailwind/tokens',
formatter: formatTokens
})

StyleDictionary.registerFormat({
name: 'tailwind/fonts',
formatter: formatFonts
})

// Actions
StyleDictionary.registerAction({
name: 'tailwind/copy_static_files',
do: copyStaticFiles.do,
undo: copyStaticFiles.undo
})
29 changes: 29 additions & 0 deletions src/tokens/transformation/tailwind-email/static/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const {
colors,
boxShadow,
dropShadow,
gradients,
borderRadius,
spacing
} = require('./tokens')

/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
boxShadow: {},
borderRadius: {},
spacing: {},
dropShadow: {},
colors: {},
extend: {
boxShadow,
borderRadius,
spacing,
dropShadow,
colors: colors,
backgroundImage: {
...gradients
}
}
}
}
3 changes: 3 additions & 0 deletions src/tokens/transformation/tailwind-email/static/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
3 changes: 3 additions & 0 deletions src/tokens/transformation/tailwind/formatFonts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import defaultTwTheme from 'tailwindcss/defaultTheme'
import { Formatter } from 'style-dictionary'

export default (({ dictionary }) => {
Expand All @@ -17,6 +18,8 @@ export default (({ dictionary }) => {
fontClass += `-${attributes.state}`
}

t.value.fontFamily = `${t.value.fontFamily},${defaultTwTheme.fontFamily.sans.join(',')}`

fontClasses.set(fontClass, t.value)
})

Expand Down

0 comments on commit e158f7b

Please sign in to comment.