Skip to content
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/generate tailwind config #494

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FormatterArguments } from 'style-dictionary/types/Format';
import { config } from './config';
import StyleDictionaryBase, { TransformedToken } from 'style-dictionary';
import { createTailwindSdFormatter } from './createTailwindConfig';
import * as fs from 'fs';

const StyleDictionary = StyleDictionaryBase.extend(config);
Expand All @@ -9,6 +10,8 @@ const fileHeader = StyleDictionary.formatHelpers.fileHeader;
console.log('Build started...');
console.log('\n==============================================');

StyleDictionary.registerFormat(createTailwindSdFormatter());

StyleDictionary.registerFormat({
formatter: ({ file, dictionary, options }: FormatterArguments) => {
const symbols = dictionary.allProperties.map(cssTemplate).join('') + '\n';
Expand Down
19 changes: 19 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ export const config: Config = {
transformGroup: 'js',
transforms: ['attribute/cti', 'name/cti/kebab', 'color/css'],
},
tailwind: {
buildPath: 'dist/tailwind/',
prefix: 'sbb',
files: [
{
destination: 'tailwind.config.json',
format: 'custom/tailwind',
},
],
transforms: [
'attribute/cti',
'name/cti/kebab',
'time/seconds',
'content/icon',
'color/css',
'size/pxToRem',
'size/rem',
],
},
scss: {
buildPath: 'dist/scss/',
prefix: 'sbb',
Expand Down
147 changes: 147 additions & 0 deletions createTailwindConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { Format, Named, TransformedToken } from 'style-dictionary';
import * as SBBTokens from './designTokens';
import { Config, CustomThemeConfig } from 'tailwindcss/types/config';

export function createTailwindSdFormatter(): Named<Format> {
return {
name: 'custom/tailwind',
formatter: (args) => {
return createTailwindConfig(args.dictionary.allTokens);
},
};
}

export function createTailwindConfig(tokens: TransformedToken[]) {
const sbbTokens = unnestObjects<typeof SBBTokens>(tokens);

// map colors and respective transparency variants to a common color
// e.g. "black" and "blackAlpha" will get merged into one color object, with the value of "black" as the default
const colors = sbbTokens.color;
Object.keys(colors).forEach((color) => {
if (color.endsWith('Alpha') && typeof colors[color] === 'object') {
const realColorName = color.replace('Alpha', '');
colors[realColorName] = withTwDefault(colors[realColorName], colors[color]);
delete colors[color];
}
});

const { fixed, responsive } = sbbTokens.spacing;

// TODO: implement appropriate media queries (like currently done in lyne-components)
const responsiveSizes = Object.fromEntries(
Object.keys(responsive).map((size) => {
const breakpoint = 'zero';
const variableName = responsive[size][breakpoint].replace(`-${breakpoint}`, '');
return [size, variableName];
}),
);

const fixedSizes = removeDashPrefix(fixed);

const spacing = { ...fixedSizes, ...responsiveSizes, '0': '0' };

// ignore the max values from the breakpoint and only use the min sizes,
// since min-width media queries are to be used
const minWidthScreens = Object.fromEntries(
Object.entries(sbbTokens.breakpoint).map(([bpName, range]) => [bpName, range.min]),
);

const maxWidthScreens = Object.fromEntries(
Object.entries(sbbTokens.breakpoint).map(([bpName, range]) => [
`max-${bpName}`,
{ max: range.max },
]),
);

const typeFaces = sbbTokens.typo.typeFace;

const fontFamily: CustomThemeConfig['fontFamily'] = {
roman: typeFaces.sbbRoman,
bold: typeFaces.sbbBold,
light: typeFaces.sbbLight,
};

// const fontSize =

// Object.fromEntries(
// Object.entries(sbbTokens.typo.typeFace).map(([name, value]) => {
// if (name.startsWith('sbb')) name = name.substring(0, 3);
// return [name, ];
// }),
// );

// const font: CustomThemeConfig["fontFamily"]

const tailwindTheme: Partial<CustomThemeConfig> = {
colors: { transparent: 'transparent', current: 'currentColor', ...colors },
screens: { ...minWidthScreens, ...maxWidthScreens },
transitionDuration: removeDashPrefix(sbbTokens.animation.duration),
transitionTimingFunction: withTwDefault(sbbTokens.animation.easing),
borderRadius: { ...sbbTokens.border.radius, '0': '0', full: '9999px' },
borderWidth: { ...sbbTokens.border.width, '0': '0' },
outlineOffset: withTwDefault(sbbTokens.focus.outline.offset),
spacing,
fontFamily,

// TODO:
// font
// shadow
// grid layout
};

return JSON.stringify({ theme: tailwindTheme } as Config, null, 2);
}
const withTwDefault = <T extends object, V>(defaultValue: V, obj = {} as T) => ({
...obj,
DEFAULT: defaultValue,
});

// remove the dash prefix from the keys
// e.g. "-1x" becomes "1x" in the key
const removeDashPrefix = (obj: Record<string, any>) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key.startsWith('-') ? key.substring(1) : key,
value,
]),
);

// this type recursively unnests objects that have a "value" property
// e.g. recursively transforms objects like { a: { value: "b" } } to { a: "b" }
type UnnestValue<T> = {
[K in keyof T]: T[K] extends { value: any } ? T[K]['value'] : UnnestValue<T[K]>;
};

function unnestObjects<T extends object>(objects: TransformedToken[]): UnnestValue<T> {
const nestedObject: any = {};

for (const obj of objects) {
let currentObject = nestedObject;
const path = obj.path;

for (let i = 0; i < path.length - 1; i++) {
const key = path[i];

if (!(key in currentObject)) {
currentObject[key] = {};
}

currentObject = currentObject[key];
}

const finalKey = path[path.length - 1];

if (path[0] === 'breakpoint') {
// breakpoints don't support css variables, we need to use the actual value of the variable instead
currentObject[finalKey] = `${obj.value} /* var(--${obj.name}) */`;
} else {
// add the actual value behind the variable as a comment for a better developer experience
currentObject[finalKey] =
`var(--${obj.name}) /* ${obj.attributes?.category === 'size' ? obj.original.value + 'px' : obj.value} */`;
}
}

return nestedObject;
}
4 changes: 2 additions & 2 deletions designTokens/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const attributes = () =>
},
};

export const animation: DesignTokens = {
export const animation = {
duration: {
'-1x': {
value: duration(1),
Expand Down Expand Up @@ -43,4 +43,4 @@ export const animation: DesignTokens = {
easing: {
value: 'cubic-bezier(.47, .1, 1, .63)',
},
};
} satisfies DesignTokens;
4 changes: 2 additions & 2 deletions designTokens/border.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const attributes = () =>
},
};

export const border: DesignTokens = {
export const border = {
width: {
'1x': {
value: borderWidth(1),
Expand Down Expand Up @@ -45,5 +45,5 @@ export const border: DesignTokens = {
value: borderRadius(16),
...attributes(),
},
},
} satisfies DesignTokens,
};
4 changes: 2 additions & 2 deletions designTokens/breakpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const attributes = () =>
},
};

export const breakpoint: DesignTokens = {
export const breakpoint = {
zero: {
min: {
value: 0,
Expand Down Expand Up @@ -78,4 +78,4 @@ export const breakpoint: DesignTokens = {
...attributes(),
},
},
};
} satisfies DesignTokens;
5 changes: 3 additions & 2 deletions designTokens/typo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const attributes = () =>
},
};

export const typo: DesignTokens = {
export const typo = {
fontFamilyFallback: {
value: '"Helvetica Neue", Helvetica, Arial, sans-serif',
},
Expand All @@ -24,6 +24,7 @@ export const typo: DesignTokens = {
sbbBold: {
value: '"SBBWeb Bold", {typo.fontFamilyFallback.value}',
},
// is this necessary?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just rebase/merge the main, we have removed it

i18n: {
traditionalChinese: {
value: '"Example for possible i18n structure"',
Expand Down Expand Up @@ -96,4 +97,4 @@ export const typo: DesignTokens = {
...attributes(),
},
},
};
} satisfies DesignTokens;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"lint-staged": "15.2.2",
"prettier": "3.2.5",
"style-dictionary": "3.9.2",
"tailwindcss": "3.4.1",
"tsx": "4.7.1",
"typescript": "5.4.3"
},
Expand Down
Loading