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

FR: Tailwind CSS #65

Closed
frodeste opened this issue Jul 28, 2023 · 5 comments · Fixed by #132
Closed

FR: Tailwind CSS #65

frodeste opened this issue Jul 28, 2023 · 5 comments · Fixed by #132
Labels
enhancement New feature or request help wanted PRs are welcome!

Comments

@frodeste
Copy link

Is a tailwind exporter planned?

@drwpow drwpow added enhancement New feature or request help wanted PRs are welcome! labels Jul 28, 2023
@drwpow
Copy link
Collaborator

drwpow commented Jul 28, 2023

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.

@mike-engel
Copy link
Contributor

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)}
		}),
	},
}`,
				},
			];
		},
	};
}

@drwpow
Copy link
Collaborator

drwpow commented Oct 16, 2023

@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.

@mike-engel
Copy link
Contributor

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

@drwpow
Copy link
Collaborator

drwpow commented Oct 23, 2023

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 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted PRs are welcome!
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants