UNPKG

tailwind-theme-preset

Version:

A Tailwind CSS plugin for unified theme management with automatic CSS variable generation, supporting multi-theme configurations and Shadcn/ui compatibility.

211 lines (209 loc) 8.99 kB
"use strict"; //#region src/index.ts let flattenedTheme = {}; function presetTheme(theme, options) { const mergedTheme = deepMerge({}, theme); flattenedTheme = flatten(mergedTheme); return { safelist: ["dark", ...options.safelist || []], theme: { extend: { colors: generateColors(mergedTheme, options) } }, plugins: [processPlugin(mergedTheme)] }; } function generateColors(theme, options = { colorRule: "hsl" }) { const colors = {}; for (const key in theme) { const value = theme[key]; if (typeof value === "object") for (const cssVarKey in value) { const colorKey = `${key}-${cssVarKey}`; const colorValue = value[cssVarKey]; if (typeof colorValue === "object" && !Array.isArray(colorValue)) { const processedValue = deepMerge({}, colorValue); colors[colorKey] = processedColor(processedValue, `--${colorKey}`); } else if (cssVarKey === "DEFAULT") colors[key] = { DEFAULT: `var(--${key}, ${colorValue})` }; else { colors[key] = colors[key] || {}; colors[key][cssVarKey] = colors[key][cssVarKey] || {}; if (typeof colorValue === "string") colors[key][cssVarKey] = { DEFAULT: colorValue }; else if (Array.isArray(colorValue)) { if (colorValue[1] === void 0) colors[key][cssVarKey] = `var(--${colorKey}, ${colorValue[0]})`; else if (typeof colorValue[1] === "string") colors[key][cssVarKey] = `${colorValue[1]}(var(--${colorKey}, ${colorValue[0]}))`; else if (typeof colorValue[1] === "function") colors[key][cssVarKey] = colorValue[1](`--${colorKey}`, colorValue[0]); } } } } console.log("[THEME] Extend colors:", colors); return colors; function processedColor(colorValue, prefixKey) { for (const key in colorValue) { const value = colorValue[key]; if (typeof value === "string" || Array.isArray(value)) { let colorString; let colorRule = options.colorRule || "hsl"; let customHandler; if (Array.isArray(value)) { colorString = value[0]; const secondParam = value.length > 1 ? value[1] : void 0; if (typeof secondParam === "string") colorRule = secondParam; else if (typeof secondParam === "function") customHandler = secondParam; else if (secondParam === void 0) { colorValue[key] = `var(${prefixKey}${key === "DEFAULT" ? "" : `-${key}`}, ${colorString})`; continue; } } else if (typeof value === "string") colorString = value; else continue; const currentPrefixKey = key === "DEFAULT" ? prefixKey : `${prefixKey}-${key}`; if (customHandler) { colorValue[key] = customHandler(currentPrefixKey, colorString); continue; } const isCompleteColor = /^(?:rgb|rgba|hsl|hsla)\s*\(/.test(colorString) || /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(colorString) || /^var\s*\(/.test(colorString); if (isCompleteColor) colorValue[key] = `var(${currentPrefixKey}, ${colorString})`; else colorValue[key] = `${colorRule}(var(${currentPrefixKey}, ${colorString}))`; } else if (typeof value === "object" && value !== null) processedColor(value, `${prefixKey}-${key}`); } return colorValue; } } function processTheme(theme) { const processed = { ":root": {} }; for (const key in theme) { const value = theme[key]; if (typeof value === "object") for (const cssVarKey in value) { const cssVarValue = value[cssVarKey]; const processCssVarKey = `--${key}-${cssVarKey}`; if (typeof cssVarValue === "object" && !Array.isArray(cssVarValue)) processedTheme(cssVarValue, processCssVarKey, processed); else if (cssVarKey === "DEFAULT") { if (typeof cssVarValue === "string") processed[":root"][`--${key}`] = cssVarValue; else if (Array.isArray(cssVarValue)) processed[":root"][`--${key}`] = cssVarValue[0]; } else if (typeof cssVarValue === "string") { processed[`.${cssVarKey}`] = processed[`.${cssVarKey}`] || {}; processed[`.${cssVarKey}`][`--${key}`] = cssVarValue; } else if (Array.isArray(cssVarValue)) processed[":root"][processCssVarKey] = cssVarValue[0]; } } return processed; } function processedTheme(colorValue, prefixKey, processed) { for (const key in colorValue) { const value = colorValue[key]; if (typeof value === "object" && value !== null && !Array.isArray(value)) processedTheme(value, `${prefixKey}-${key}`, processed); else if (key === "DEFAULT") { if (typeof value === "string") if (/(?:hsl|rgb)\(var\(/.test(value)) processed[":root"][prefixKey] = resolvedValue(value); else processed[":root"][prefixKey] = value; else if (Array.isArray(value)) if (/(?:hsl|rgb)\(var\(/.test(value[0])) processed[":root"][prefixKey] = resolvedValue(value[0]); else processed[":root"][prefixKey] = value[0]; } else if (typeof value === "string") { processed[`.${key}`] = processed[`.${key}`] || {}; if (/(?:hsl|rgb)\(var\(/.test(value)) processed[`.${key}`][prefixKey] = resolvedValue(value); else processed[`.${key}`][prefixKey] = value; } } } function processPlugin(themeConfig) { const processedTheme$1 = processTheme(themeConfig); console.log("[PLUGINS] addUtilities:", processedTheme$1); return ({ addUtilities }) => { addUtilities(processedTheme$1); }; } function deepMerge(...args) { if (args.length < 2) return args[0]; const target = args[0] || {}; for (let i = 1; i < args.length; i++) { const source = args[i]; if (!source) continue; for (const key in source) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) target[key] = deepMerge(target[key] || {}, source[key]); else target[key] = source[key]; } return target; } function resolvedValue(cssVarValue) { return resolveNestedColorFunctions(cssVarValue, [], new Set()); } /** * 递归解析嵌套的颜色函数和变量引用 * @param value 要处理的CSS值 * @param visited 已访问的变量路径数组,用于生成注释 * @param visitedSet 已访问的变量集合,防止循环引用 */ function resolveNestedColorFunctions(value, visited, visitedSet) { const colorFuncRegex = /(hsl|rgb|rgba|hsla)\s*\((.*)\)/g; return value.replace(colorFuncRegex, (match, funcName, content) => { const { resolvedContent, finalVisited } = resolveVariablesInContent(content, visited, visitedSet); const nestedFuncRegex = new RegExp(`(${funcName})\\s*\\(([^)]+)\\)`); const nestedMatch = resolvedContent.match(nestedFuncRegex); if (nestedMatch) { const innerContent = nestedMatch[2]; const remainingContent = resolvedContent.replace(nestedFuncRegex, innerContent); if (finalVisited.length > 0) { const originalVar = `var(${finalVisited[0]})`; const finalVar = `var(${finalVisited.slice(-1)[0]})`; const result$1 = match.replace(originalVar, finalVar); return `${result$1}/* ${finalVisited.join(" -> ")} */`; } return `${funcName}(${remainingContent})`; } const result = `${funcName}(${resolvedContent})`; if (finalVisited.length > 0) { const originalVar = `var(${finalVisited[0]})`; const finalVar = `var(${finalVisited.slice(-1)[0]})`; const resultWithReplacedVar = match.replace(originalVar, finalVar); return `${resultWithReplacedVar}/* ${finalVisited.join(" -> ")} */`; } return result; }); } /** * 解析内容中的变量引用 * @param content 要解析的内容 * @param visited 已访问的变量路径数组 * @param visitedSet 已访问的变量集合 * @returns 解析后的内容和更新的访问路径 */ function resolveVariablesInContent(content, visited, visitedSet) { const varRegex = /var\s*\(\s*(--[^,)]+)(?:\s*,\s*([^)]+))?\s*\)/g; let finalVisited = [...visited]; const resolvedContent = content.replace(varRegex, (match, varName) => { if (visitedSet.has(varName)) return match; const varValue = flattenedTheme[varName]; if (!varValue) return match; const newVisitedSet = new Set(visitedSet); newVisitedSet.add(varName); const newVisited = [...visited, varName]; const colorFuncMatch = varValue.match(/^(hsl|rgb|rgba|hsla)\s*\((.+)\)$/); if (colorFuncMatch) { const [, _funcName, innerContent] = colorFuncMatch; const { resolvedContent: resolvedInnerContent, finalVisited: innerFinalVisited } = resolveVariablesInContent(innerContent, newVisited, newVisitedSet); finalVisited = innerFinalVisited; return resolvedInnerContent; } else { const resolvedVarValue = resolveNestedColorFunctions(varValue, newVisited, newVisitedSet); finalVisited = newVisited; return resolvedVarValue; } }); return { resolvedContent, finalVisited }; } function flatten(theme) { const result = {}; function recurse(curr, prefix) { for (const key in curr) { const value = curr[key]; const newKey = prefix ? key === "DEFAULT" ? prefix : `${prefix}-${key}` : `--${key}`; if (typeof value === "object" && value !== null && !Array.isArray(value)) recurse(value, newKey); else result[newKey] = value; } } recurse(theme, ""); return result; } //#endregion exports.flatten = flatten exports.generateColors = generateColors exports.presetTheme = presetTheme exports.processTheme = processTheme