drwpow / cobalt-ui

Use W3C Design Token Community Group tokens in CI and code

Home Page:https://cobalt-ui.pages.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FR: Tailwind CSS

frodeste opened this issue · comments

Is a tailwind exporter planned?

Not currently; I’m not a Tailwind user myself. But I’d love to support a Tailwind export plugin be created in any capacity (whether that’s helping the author, or improving docs, etc.). Cobalt is designed to be pluggable, so anyone can make a custom plugin for their needs without too much hassle.

For future readers, here's the tailwind plugin we wrote. It's highly specific to our tokens and what we have defined (and what we don't yet), so it will require some tweaking.

In general, I'm not sure how generic a tailwind plugin could be. It would either need a specific token structure or a lot of options to be passed in.

import setWith from 'lodash.setwith';
import util from 'node:util';

/**
 * @param {import('@cobalt-ui/core').ParsedToken[]} tokens
 * @param {Record<string, any>} initial
 * @returns {Record<string, any>}
 */
function expandThemeGroup(tokens, initial) {
	return tokens.reduce((acc, token) => {
		let path = token.id;
		let value = token._original.$value;

		// Default value, capitalize for tailwind
		if (path.endsWith('default')) {
			path = path.replace('default', 'DEFAULT');
		}

		setWith(
			acc,
			path.split('.').map((v) => v.replace('_', '.')),
			value,
			Object,
		);

		return acc;
	}, initial);
}

/**
 * @param {Record<string, any>} tokens
 * @param {number} depth
 * @returns {string}
 */
function printThemeGroup(tokens, depth) {
	let keys = Object.keys(tokens);
	let padding = new Array(depth).fill('\t').join('');
	let lines = keys.map((key) => {
		let val = tokens[key];
		let unsafeKey = /[ .,-]/g.test(key);
		let paddedKey = `${padding}${unsafeKey === true ? "'" : ''}${key}${unsafeKey === true ? "'" : ''}`;

		if (Array.isArray(val)) {
			return `${paddedKey}: ${util.inspect(val, { compact: true, showHidden: false })},`;
		} else if (typeof val === 'object') {
			return `${paddedKey}: {
${printThemeGroup(val, depth + 1)}
${padding}},`;
		} else if (typeof val === 'string') {
			// Aliased value, transform it into a `theme()` call
			if (val.startsWith('{')) {
				return `${paddedKey}: theme('${val.slice(1, -1)}'),`;
			}

			return `${paddedKey}: '${val}',`;
		} else {
			return `${paddedKey}: ${val},`;
		}
	});

	return lines.join('\n');
}

/** @returns {import('@cobalt-ui/core').Plugin} */
export default function pluginTailwind() {
	return {
		name: 'tailwind',
		async build({ tokens }) {
			let colors = tokens.filter((token) => token.id.startsWith('colors.'));
			let aliasedColors = tokens.filter((token) => token.$type === 'color' && !token.id.startsWith('colors.'));
			let spacing = tokens.filter((token) => token.id.startsWith('spacing.'));
			let fontWeights = tokens.filter((token) => token.$type === 'fontWeight');
			let borderRadii = tokens.filter((token) => token.id.startsWith('radius.'));
			let fontSizes = tokens.filter((token) => token.id.startsWith('fontSize.'));
			let nestedColors = expandThemeGroup(colors, { colors: { transparent: 'transparent', current: 'currentColor' } });
			let nestedAliasColors = expandThemeGroup(aliasedColors, {});
			let nestedSpacing = expandThemeGroup(spacing, { spacing: { px: '1px' } });
			let nestedFontWeights = expandThemeGroup(fontWeights, {});
			let nestedBorderRadii = expandThemeGroup(borderRadii, { radius: { none: '0', full: '100%' } });
			let nestedFontSizes = expandThemeGroup(fontSizes, {});

			return [
				{
					filename: 'tailwind.config.js',
					contents: `/**
 * Design Tokens
 * Autogenerated from tokens.json.
 * DO NOT EDIT!
 */

module.exports = {
	theme: {
		colors: (theme) => ({
${printThemeGroup(nestedColors.colors, 3)}
${printThemeGroup(nestedAliasColors, 3)}
		}),
		spacing: {
${printThemeGroup(nestedSpacing.spacing, 3)}
		},
		fontWeight: (theme) => ({
${printThemeGroup(nestedFontWeights.fontWeight, 3)}
		}),
		borderRadius: {
${printThemeGroup(nestedBorderRadii.radius, 3)}
		},
		fontSize: (theme) => ({
${printThemeGroup(nestedFontSizes.fontSize, 3)}
		}),
	},
}`,
				},
			];
		},
	};
}

@mike-engel ah that’s interesting—so you’re generating the configuration that Tailwind is then picking up.

I had been mulling around a similar idea in #129, but I do like the idea of just generating a Tailwind config automatically. Then you’re using Tailwind and not “we have Tailwind at home.”

I like this approach more than trying to create some competitor to Tailwind that frankly I have no interest in trying to get people to use.

Absolutely! We're so far on the tailwind train (for better or worse) that the approach we took was way simpler 😂

Just published @cobalt-ui/plugin-tailwind to hopefully provide a sane-ish DX (docs). It taps into Tailwind’s presets so people can still keep their current setup, basically just provide tokens as a base.

I’m not the biggest fan of having to manually run a command for Tailwind (I wish it would just “pick up on changes”), but I guess it‘s consistent with how the other Cobalt plugins work currently.

Anyway, it’s version 0.0.1 so would love feedback if anyone has any 🙏