457 lines
13 KiB
Python
457 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tailwind CSS Configuration Generator
|
|
|
|
Generate tailwind.config.js/ts with custom theme configuration.
|
|
Supports colors, fonts, spacing, breakpoints, and plugin recommendations.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
class TailwindConfigGenerator:
|
|
"""Generate Tailwind CSS configuration files."""
|
|
|
|
def __init__(
|
|
self,
|
|
typescript: bool = True,
|
|
framework: str = "react",
|
|
output_path: Optional[Path] = None,
|
|
):
|
|
"""
|
|
Initialize generator.
|
|
|
|
Args:
|
|
typescript: If True, generate .ts config, else .js
|
|
framework: Framework name (react, vue, svelte, nextjs)
|
|
output_path: Output file path (default: auto-detect)
|
|
"""
|
|
self.typescript = typescript
|
|
self.framework = framework
|
|
self.output_path = output_path or self._default_output_path()
|
|
self.config: Dict[str, Any] = self._base_config()
|
|
|
|
def _default_output_path(self) -> Path:
|
|
"""Determine default output path."""
|
|
ext = "ts" if self.typescript else "js"
|
|
return Path.cwd() / f"tailwind.config.{ext}"
|
|
|
|
def _base_config(self) -> Dict[str, Any]:
|
|
"""Create base configuration structure."""
|
|
return {
|
|
"darkMode": ["class"],
|
|
"content": self._default_content_paths(),
|
|
"theme": {
|
|
"extend": {}
|
|
},
|
|
"plugins": []
|
|
}
|
|
|
|
def _default_content_paths(self) -> List[str]:
|
|
"""Get default content paths for framework."""
|
|
paths = {
|
|
"react": [
|
|
"./src/**/*.{js,jsx,ts,tsx}",
|
|
"./index.html",
|
|
],
|
|
"vue": [
|
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
|
"./index.html",
|
|
],
|
|
"svelte": [
|
|
"./src/**/*.{svelte,js,ts}",
|
|
"./src/app.html",
|
|
],
|
|
"nextjs": [
|
|
"./app/**/*.{js,ts,jsx,tsx}",
|
|
"./pages/**/*.{js,ts,jsx,tsx}",
|
|
"./components/**/*.{js,ts,jsx,tsx}",
|
|
],
|
|
}
|
|
return paths.get(self.framework, paths["react"])
|
|
|
|
def add_colors(self, colors: Dict[str, str]) -> None:
|
|
"""
|
|
Add custom colors to theme.
|
|
|
|
Args:
|
|
colors: Dict of color_name: color_value
|
|
Value can be hex (#3b82f6) or variable (hsl(var(--primary)))
|
|
"""
|
|
if "colors" not in self.config["theme"]["extend"]:
|
|
self.config["theme"]["extend"]["colors"] = {}
|
|
|
|
self.config["theme"]["extend"]["colors"].update(colors)
|
|
|
|
def add_color_palette(self, name: str, base_color: str) -> None:
|
|
"""
|
|
Add full color palette (50-950 shades) for a base color.
|
|
|
|
Args:
|
|
name: Color name (e.g., 'brand', 'primary')
|
|
base_color: Base color in oklch format or hex
|
|
"""
|
|
# For simplicity, use CSS variable approach
|
|
if "colors" not in self.config["theme"]["extend"]:
|
|
self.config["theme"]["extend"]["colors"] = {}
|
|
|
|
self.config["theme"]["extend"]["colors"][name] = {
|
|
"50": f"var(--color-{name}-50)",
|
|
"100": f"var(--color-{name}-100)",
|
|
"200": f"var(--color-{name}-200)",
|
|
"300": f"var(--color-{name}-300)",
|
|
"400": f"var(--color-{name}-400)",
|
|
"500": f"var(--color-{name}-500)",
|
|
"600": f"var(--color-{name}-600)",
|
|
"700": f"var(--color-{name}-700)",
|
|
"800": f"var(--color-{name}-800)",
|
|
"900": f"var(--color-{name}-900)",
|
|
"950": f"var(--color-{name}-950)",
|
|
}
|
|
|
|
def add_fonts(self, fonts: Dict[str, List[str]]) -> None:
|
|
"""
|
|
Add custom font families.
|
|
|
|
Args:
|
|
fonts: Dict of font_type: [font_names]
|
|
e.g., {'sans': ['Inter', 'system-ui', 'sans-serif']}
|
|
"""
|
|
if "fontFamily" not in self.config["theme"]["extend"]:
|
|
self.config["theme"]["extend"]["fontFamily"] = {}
|
|
|
|
self.config["theme"]["extend"]["fontFamily"].update(fonts)
|
|
|
|
def add_spacing(self, spacing: Dict[str, str]) -> None:
|
|
"""
|
|
Add custom spacing values.
|
|
|
|
Args:
|
|
spacing: Dict of name: value
|
|
e.g., {'18': '4.5rem', 'navbar': '4rem'}
|
|
"""
|
|
if "spacing" not in self.config["theme"]["extend"]:
|
|
self.config["theme"]["extend"]["spacing"] = {}
|
|
|
|
self.config["theme"]["extend"]["spacing"].update(spacing)
|
|
|
|
def add_breakpoints(self, breakpoints: Dict[str, str]) -> None:
|
|
"""
|
|
Add custom breakpoints.
|
|
|
|
Args:
|
|
breakpoints: Dict of name: width
|
|
e.g., {'3xl': '1920px', 'tablet': '768px'}
|
|
"""
|
|
if "screens" not in self.config["theme"]["extend"]:
|
|
self.config["theme"]["extend"]["screens"] = {}
|
|
|
|
self.config["theme"]["extend"]["screens"].update(breakpoints)
|
|
|
|
def add_plugins(self, plugins: List[str]) -> None:
|
|
"""
|
|
Add plugin requirements.
|
|
|
|
Args:
|
|
plugins: List of plugin names
|
|
e.g., ['@tailwindcss/typography', '@tailwindcss/forms']
|
|
"""
|
|
for plugin in plugins:
|
|
if plugin not in self.config["plugins"]:
|
|
self.config["plugins"].append(plugin)
|
|
|
|
def recommend_plugins(self) -> List[str]:
|
|
"""
|
|
Get plugin recommendations based on configuration.
|
|
|
|
Returns:
|
|
List of recommended plugin package names
|
|
"""
|
|
recommendations = []
|
|
|
|
# Always recommend animation plugin
|
|
recommendations.append("tailwindcss-animate")
|
|
|
|
# Framework-specific recommendations
|
|
if self.framework == "nextjs":
|
|
recommendations.append("@tailwindcss/typography")
|
|
|
|
return recommendations
|
|
|
|
def generate_config_string(self) -> str:
|
|
"""
|
|
Generate configuration file content.
|
|
|
|
Returns:
|
|
Configuration file as string
|
|
"""
|
|
if self.typescript:
|
|
return self._generate_typescript()
|
|
return self._generate_javascript()
|
|
|
|
def _generate_typescript(self) -> str:
|
|
"""Generate TypeScript configuration."""
|
|
plugins_str = self._format_plugins()
|
|
|
|
config_json = json.dumps(self.config, indent=2)
|
|
|
|
# Remove plugin array from JSON (we'll add it with require())
|
|
config_obj = self.config.copy()
|
|
config_obj.pop("plugins", None)
|
|
config_json = json.dumps(config_obj, indent=2)
|
|
|
|
return f"""import type {{ Config }} from 'tailwindcss'
|
|
|
|
const config: Config = {{
|
|
{self._indent_json(config_json, 1)}
|
|
plugins: [{plugins_str}],
|
|
}}
|
|
|
|
export default config
|
|
"""
|
|
|
|
def _generate_javascript(self) -> str:
|
|
"""Generate JavaScript configuration."""
|
|
plugins_str = self._format_plugins()
|
|
|
|
config_obj = self.config.copy()
|
|
config_obj.pop("plugins", None)
|
|
config_json = json.dumps(config_obj, indent=2)
|
|
|
|
return f"""/** @type {{import('tailwindcss').Config}} */
|
|
module.exports = {{
|
|
{self._indent_json(config_json, 1)}
|
|
plugins: [{plugins_str}],
|
|
}}
|
|
"""
|
|
|
|
def _format_plugins(self) -> str:
|
|
"""Format plugins array for config."""
|
|
if not self.config["plugins"]:
|
|
return ""
|
|
|
|
plugin_requires = [
|
|
f"require('{plugin}')" for plugin in self.config["plugins"]
|
|
]
|
|
return ", ".join(plugin_requires)
|
|
|
|
def _indent_json(self, json_str: str, level: int) -> str:
|
|
"""Add indentation to JSON string."""
|
|
indent = " " * level
|
|
lines = json_str.split("\n")
|
|
# Skip first and last lines (braces)
|
|
indented = [indent + line for line in lines[1:-1]]
|
|
return "\n".join(indented)
|
|
|
|
def write_config(self) -> tuple[bool, str]:
|
|
"""
|
|
Write configuration to file.
|
|
|
|
Returns:
|
|
Tuple of (success, message)
|
|
"""
|
|
try:
|
|
config_content = self.generate_config_string()
|
|
|
|
self.output_path.write_text(config_content)
|
|
|
|
return True, f"Configuration written to {self.output_path}"
|
|
|
|
except OSError as e:
|
|
return False, f"Failed to write config: {e}"
|
|
|
|
def validate_config(self) -> tuple[bool, str]:
|
|
"""
|
|
Validate configuration.
|
|
|
|
Returns:
|
|
Tuple of (valid, message)
|
|
"""
|
|
# Check content paths exist
|
|
if not self.config["content"]:
|
|
return False, "No content paths specified"
|
|
|
|
# Check if extending empty theme
|
|
if not self.config["theme"]["extend"]:
|
|
return True, "Warning: No theme extensions defined"
|
|
|
|
return True, "Configuration valid"
|
|
|
|
|
|
def main():
|
|
"""CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate Tailwind CSS configuration",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Generate TypeScript config for Next.js
|
|
python tailwind_config_gen.py --framework nextjs
|
|
|
|
# Generate JavaScript config with custom colors
|
|
python tailwind_config_gen.py --js --colors brand:#3b82f6 accent:#8b5cf6
|
|
|
|
# Add custom fonts
|
|
python tailwind_config_gen.py --fonts display:"Playfair Display,serif"
|
|
|
|
# Add custom spacing and breakpoints
|
|
python tailwind_config_gen.py --spacing navbar:4rem --breakpoints 3xl:1920px
|
|
|
|
# Add recommended plugins
|
|
python tailwind_config_gen.py --plugins
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--framework",
|
|
choices=["react", "vue", "svelte", "nextjs"],
|
|
default="react",
|
|
help="Target framework (default: react)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--js",
|
|
action="store_true",
|
|
help="Generate JavaScript config instead of TypeScript",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
help="Output file path",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--colors",
|
|
nargs="*",
|
|
metavar="NAME:VALUE",
|
|
help="Custom colors (e.g., brand:#3b82f6)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--fonts",
|
|
nargs="*",
|
|
metavar="TYPE:FAMILY",
|
|
help="Custom fonts (e.g., sans:'Inter,system-ui')",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--spacing",
|
|
nargs="*",
|
|
metavar="NAME:VALUE",
|
|
help="Custom spacing (e.g., navbar:4rem)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--breakpoints",
|
|
nargs="*",
|
|
metavar="NAME:WIDTH",
|
|
help="Custom breakpoints (e.g., 3xl:1920px)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--plugins",
|
|
action="store_true",
|
|
help="Add recommended plugins",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--validate-only",
|
|
action="store_true",
|
|
help="Validate config without writing file",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Initialize generator
|
|
generator = TailwindConfigGenerator(
|
|
typescript=not args.js,
|
|
framework=args.framework,
|
|
output_path=args.output,
|
|
)
|
|
|
|
# Add custom colors
|
|
if args.colors:
|
|
colors = {}
|
|
for color_spec in args.colors:
|
|
try:
|
|
name, value = color_spec.split(":", 1)
|
|
colors[name] = value
|
|
except ValueError:
|
|
print(f"Invalid color spec: {color_spec}", file=sys.stderr)
|
|
sys.exit(1)
|
|
generator.add_colors(colors)
|
|
|
|
# Add custom fonts
|
|
if args.fonts:
|
|
fonts = {}
|
|
for font_spec in args.fonts:
|
|
try:
|
|
font_type, family = font_spec.split(":", 1)
|
|
fonts[font_type] = [f.strip().strip("'\"") for f in family.split(",")]
|
|
except ValueError:
|
|
print(f"Invalid font spec: {font_spec}", file=sys.stderr)
|
|
sys.exit(1)
|
|
generator.add_fonts(fonts)
|
|
|
|
# Add custom spacing
|
|
if args.spacing:
|
|
spacing = {}
|
|
for spacing_spec in args.spacing:
|
|
try:
|
|
name, value = spacing_spec.split(":", 1)
|
|
spacing[name] = value
|
|
except ValueError:
|
|
print(f"Invalid spacing spec: {spacing_spec}", file=sys.stderr)
|
|
sys.exit(1)
|
|
generator.add_spacing(spacing)
|
|
|
|
# Add custom breakpoints
|
|
if args.breakpoints:
|
|
breakpoints = {}
|
|
for bp_spec in args.breakpoints:
|
|
try:
|
|
name, width = bp_spec.split(":", 1)
|
|
breakpoints[name] = width
|
|
except ValueError:
|
|
print(f"Invalid breakpoint spec: {bp_spec}", file=sys.stderr)
|
|
sys.exit(1)
|
|
generator.add_breakpoints(breakpoints)
|
|
|
|
# Add recommended plugins
|
|
if args.plugins:
|
|
recommended = generator.recommend_plugins()
|
|
generator.add_plugins(recommended)
|
|
print(f"Added recommended plugins: {', '.join(recommended)}")
|
|
print("\nInstall with:")
|
|
print(f" npm install -D {' '.join(recommended)}")
|
|
|
|
# Validate
|
|
valid, message = generator.validate_config()
|
|
if not valid:
|
|
print(f"Validation failed: {message}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if message.startswith("Warning"):
|
|
print(message)
|
|
|
|
# Validate only mode
|
|
if args.validate_only:
|
|
print("Configuration valid")
|
|
print("\nGenerated config:")
|
|
print(generator.generate_config_string())
|
|
sys.exit(0)
|
|
|
|
# Write config
|
|
success, message = generator.write_config()
|
|
print(message)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|