@lorenzo.franzone/tws
Version:
Tailwind 4 Styles Generator
232 lines (231 loc) • 8.93 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processColors = processColors;
const chroma_js_1 = __importDefault(require("chroma-js"));
/**
* ==================================================
* processColors
* ==================================================
* Transforms a color config object into a CSS-ready structure,
* including theme variables, custom variants, and utility classes.
* Handles multiple color schemes, Reference compatibility, and dark mode.
*/
function processColors(input) {
const errors = new Set();
validateConfig(input, errors);
if (errors.size > 0) {
console.error('Hold on! Found some issues with your config:');
errors.forEach(e => console.error(e));
return { outDir: '', data: [] };
}
const { outDir, data } = input;
const { schemes, colors: colorsConfig, default: defaultKey, adapter } = data;
const { base, map } = colorsConfig;
const modes = schemes?.modes ?? ['light'];
const toggle = schemes?.toggle ?? 'auto';
const defaultMode = modes[0];
const secondMode = modes[1];
const hasMultiple = modes.length > 1;
const refPrefix = '--tws-color';
const regPrefix = '--color';
const prefix = adapter === 'reference' ? refPrefix : regPrefix;
const toCssVar = (v) => v.startsWith('--') ? `var(${prefix}-${v.slice(2)})` : v;
const toRootVar = (v) => v.startsWith('--') ? `var(${regPrefix}-${v.slice(2)})` : v;
const theme = {};
const firstAdapted = {};
const secondAdapted = {};
const entries = Object.entries(map);
// Step 0 - Reference root vars (if using REF adapter)
if (adapter === 'reference') {
entries.forEach(([key, props]) => {
Object.entries(props).forEach(([prop, vals]) => {
const v = vals[0];
if (v && isValidColor(v)) {
firstAdapted[`${refPrefix}-${key}-${prop}`] = toRootVar(v);
}
});
});
const defMap = map[defaultKey];
Object.keys(defMap).forEach(prop => {
const v = defMap[prop][0];
if (v && isValidColor(v)) {
theme[`${regPrefix}-${prop}`] = `var(${refPrefix}-${prop})`;
firstAdapted[`${refPrefix}-${prop}`] = toRootVar(v);
}
});
}
// Step 1 - Base semantic keys and fixed versions
entries.forEach(([key, props]) => {
const v = props[base]?.[0];
if (v && !isValidColor(v)) {
errors.add(`Invalid base for ${key}: ${v}`);
return;
}
if (v) {
theme[`${regPrefix}-${key}`] = adapter === 'reference'
? `var(${refPrefix}-${key}-${base})`
: toCssVar(v);
theme[`${regPrefix}-${key}-fixed`] = toRootVar(v);
}
});
// Step 2 - Semantic defaults
const defProps = map[defaultKey];
Object.entries(defProps).forEach(([prop, vals]) => {
const v = vals[0];
if (v && !isValidColor(v)) {
errors.add(`Invalid default ${prop}: ${v}`);
return;
}
if (v && !theme.hasOwnProperty(`${regPrefix}-${prop}`)) {
theme[`${regPrefix}-${prop}`] = adapter === 'reference'
? `var(${refPrefix}-${prop})`
: toCssVar(`--${defaultKey}-${prop}`);
}
});
// Step 3 - All primary values for each color prop
entries.forEach(([key, props]) => {
Object.entries(props).forEach(([prop, vals]) => {
const v = vals[0];
if (v && !isValidColor(v)) {
errors.add(`Invalid ${key}-${prop}: ${v}`);
return;
}
if (v) {
const name = `${regPrefix}-${key}-${prop}`;
theme[name] = adapter === 'reference'
? `var(${refPrefix}-${key}-${prop})`
: toCssVar(v);
}
});
});
// Step 4 - Dark mode support (if second mode is present)
if (hasMultiple && secondMode) {
entries.forEach(([key, props]) => {
const v = props[base]?.[1];
if (v && isValidColor(v)) {
const name = adapter === 'reference'
? `${refPrefix}-${key}`
: `${regPrefix}-${key}`;
secondAdapted[name] = toRootVar(v);
}
Object.entries(props).forEach(([prop, vals]) => {
const v2 = vals[1];
if (v2 && isValidColor(v2)) {
const name2 = adapter === 'reference'
? `${refPrefix}-${key}-${prop}`
: `${regPrefix}-${key}-${prop}`;
secondAdapted[name2] = toRootVar(v2);
}
});
});
Object.entries(defProps).forEach(([prop, vals]) => {
const v2 = vals[1];
if (v2 && isValidColor(v2)) {
const name = adapter === 'reference'
? `${refPrefix}-${prop}`
: `${regPrefix}-${prop}`;
secondAdapted[name] = toRootVar(v2);
}
});
secondAdapted['color-scheme'] = secondMode;
}
// Step 5 - Custom variant generation (class or attr toggle)
let customVar;
if (hasMultiple && secondMode) {
if (toggle === 'class') {
customVar = `@custom-variant ${secondMode} (&:where(.${secondMode}, .${secondMode} *));`;
}
else if (toggle === 'attr') {
customVar = `@custom-variant ${secondMode} (&:where([data-theme=${secondMode}], [data-theme=${secondMode}] *));`;
}
}
// Step 6 - Utility and dynamic classes
const utility = {};
const dynamic = { '@layer utilities': { '[class*="theme-"]': {} } };
entries.forEach(([key, props]) => {
const utilTheme = {};
const utilFixed = {};
Object.entries(props).forEach(([prop, vals]) => {
utilTheme[`--color-${prop}`] = adapter === 'reference'
? `var(${refPrefix}-${key}-${prop})`
: `var(${regPrefix}-${key}-${prop})`;
const bv = vals[0];
if (bv)
utilFixed[`--color-${prop}`] = toRootVar(bv);
});
utility[`@utility theme-${key}`] = utilTheme;
utility[`@utility theme-${key}-fixed`] = utilFixed;
const accentVal = props['accent']?.[0];
const onAccentVal = props['on-accent']?.[0];
if (accentVal && onAccentVal) {
dynamic['@layer utilities']['[class*="theme-"]'][`:is(.theme-${key}) .theme-accent`] = {
'--color-color': adapter === 'reference'
? `var(${refPrefix}-${key}-accent)`
: `var(${regPrefix}-${key}-accent)`,
'--color-on-color': adapter === 'reference'
? `var(${refPrefix}-${key}-on-accent)`
: `var(${regPrefix}-${key}-on-accent)`,
};
dynamic['@layer utilities']['[class*="theme-"]'][`:is(.theme-${key}-fixed) .theme-accent`] = {
'--color-color': toRootVar(accentVal),
'--color-on-color': toRootVar(onAccentVal),
};
}
});
// Step 7 - Base layer for :root and dark variant if needed
const baseLayer = {
'@layer base': {
':root': {
...(adapter === 'reference' ? firstAdapted : {}),
'color-scheme': defaultMode
}
}
};
if (hasMultiple && Object.keys(secondAdapted).length) {
const sel = toggle === 'class'
? `&.${secondMode}`
: toggle === 'attr'
? `&[data-theme="${secondMode}"]`
: '@media (prefers-color-scheme: dark)';
baseLayer['@layer base'][':root'][sel] = secondAdapted;
}
return {
outDir,
data: [
{ '@theme': theme },
baseLayer,
...(customVar ? [customVar] : []),
utility,
...(Object.keys(dynamic['@layer utilities']['[class*="theme-"]']).length > 0
? [dynamic]
: []),
],
};
}
// Check if a string is a valid color (or CSS variable)
function isValidColor(value) {
if (value.startsWith('--'))
return true;
try {
(0, chroma_js_1.default)(value);
return true;
}
catch {
return false;
}
}
// Basic config shape validation
function validateConfig(config, errors) {
const { data } = config;
if (!data) {
errors.add("colors: Missing data");
return;
}
if (!data.schemes)
errors.add("colors: Missing schemes");
if (!data.colors)
errors.add("colors: Missing colors");
}