Initial commit: baseline snapshot before UI/UX refactor

Captures the existing lovelope.app codebase (Next.js App Router,
custom Tailwind components, Prisma) as a rollback point prior to
adopting shadcn/ui and a mobile-first redesign.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 19:27:12 +02:00
commit 1064a0362b
150 changed files with 19224 additions and 0 deletions
@@ -0,0 +1,17 @@
# UI Styling Skill Dependencies
# Python 3.10+ required
# No Python package dependencies - uses only standard library
# Testing dependencies (dev)
pytest>=8.0.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
# Note: This skill works with shadcn/ui and Tailwind CSS
# Requires Node.js and package managers:
# - Node.js 18+: https://nodejs.org/
# - npm (comes with Node.js)
#
# shadcn/ui CLI is installed per-project:
# npx shadcn-ui@latest init
@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
shadcn/ui Component Installer
Add shadcn/ui components to project with automatic dependency handling.
Wraps shadcn CLI for programmatic component installation.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import List, Optional
class ShadcnInstaller:
"""Handle shadcn/ui component installation."""
def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False):
"""
Initialize installer.
Args:
project_root: Project root directory (default: current directory)
dry_run: If True, show actions without executing
"""
self.project_root = project_root or Path.cwd()
self.dry_run = dry_run
self.components_json = self.project_root / "components.json"
def check_shadcn_config(self) -> bool:
"""
Check if shadcn is initialized in project.
Returns:
True if components.json exists
"""
return self.components_json.exists()
def get_installed_components(self) -> List[str]:
"""
Get list of already installed components.
Returns:
List of installed component names
"""
if not self.check_shadcn_config():
return []
try:
with open(self.components_json) as f:
config = json.load(f)
components_dir = self.project_root / config.get("aliases", {}).get(
"components", "components"
).replace("@/", "")
ui_dir = components_dir / "ui"
if not ui_dir.exists():
return []
return [f.stem for f in ui_dir.glob("*.tsx") if f.is_file()]
except (json.JSONDecodeError, KeyError, OSError):
return []
def _get_shadcn_version(self) -> str:
"""Read shadcn version from project package.json; fall back to a pinned default."""
pkg_json = self.project_root / "package.json"
if pkg_json.exists():
try:
pkg = json.loads(pkg_json.read_text())
for section in ("dependencies", "devDependencies"):
version = pkg.get(section, {}).get("shadcn")
if version:
return version.lstrip("^~>=<").split()[0]
except (json.JSONDecodeError, KeyError):
pass
return "2.3.0" # pinned fallback; update when newer stable release is needed
def add_components(
self, components: List[str], overwrite: bool = False
) -> tuple[bool, str]:
"""
Add shadcn/ui components.
Args:
components: List of component names to add
overwrite: If True, overwrite existing components
Returns:
Tuple of (success, message)
"""
if not components:
return False, "No components specified"
if not self.check_shadcn_config():
return (
False,
"shadcn not initialized. Run 'npx shadcn@latest init' first",
)
# Check which components already exist
installed = self.get_installed_components()
already_installed = [c for c in components if c in installed]
if already_installed and not overwrite:
return (
False,
f"Components already installed: {', '.join(already_installed)}. "
"Use --overwrite to reinstall",
)
# Build command
shadcn_version = self._get_shadcn_version()
cmd = ["npx", f"shadcn@{shadcn_version}", "add"] + components
if overwrite:
cmd.append("--overwrite")
if self.dry_run:
return True, f"Would run: {' '.join(cmd)}"
# Execute command
try:
result = subprocess.run(
cmd,
cwd=self.project_root,
capture_output=True,
text=True,
check=True,
)
success_msg = f"Successfully added components: {', '.join(components)}"
if result.stdout:
success_msg += f"\n\nOutput:\n{result.stdout}"
return True, success_msg
except subprocess.CalledProcessError as e:
error_msg = f"Failed to add components: {e.stderr or e.stdout or str(e)}"
return False, error_msg
except FileNotFoundError:
return False, "npx not found. Ensure Node.js is installed"
def add_all_components(self, overwrite: bool = False) -> tuple[bool, str]:
"""
Add all available shadcn/ui components.
Args:
overwrite: If True, overwrite existing components
Returns:
Tuple of (success, message)
"""
if not self.check_shadcn_config():
return (
False,
"shadcn not initialized. Run 'npx shadcn@latest init' first",
)
shadcn_version = self._get_shadcn_version()
cmd = ["npx", f"shadcn@{shadcn_version}", "add", "--all"]
if overwrite:
cmd.append("--overwrite")
if self.dry_run:
return True, f"Would run: {' '.join(cmd)}"
try:
result = subprocess.run(
cmd,
cwd=self.project_root,
capture_output=True,
text=True,
check=True,
)
success_msg = "Successfully added all components"
if result.stdout:
success_msg += f"\n\nOutput:\n{result.stdout}"
return True, success_msg
except subprocess.CalledProcessError as e:
error_msg = f"Failed to add all components: {e.stderr or e.stdout or str(e)}"
return False, error_msg
except FileNotFoundError:
return False, "npx not found. Ensure Node.js is installed"
def list_installed(self) -> tuple[bool, str]:
"""
List installed components.
Returns:
Tuple of (success, message with component list)
"""
if not self.check_shadcn_config():
return False, "shadcn not initialized"
installed = self.get_installed_components()
if not installed:
return True, "No components installed"
return True, f"Installed components:\n" + "\n".join(f" - {c}" for c in sorted(installed))
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(
description="Add shadcn/ui components to your project",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Add single component
python shadcn_add.py button
# Add multiple components
python shadcn_add.py button card dialog
# Add all components
python shadcn_add.py --all
# Overwrite existing components
python shadcn_add.py button --overwrite
# Dry run (show what would be done)
python shadcn_add.py button card --dry-run
# List installed components
python shadcn_add.py --list
""",
)
parser.add_argument(
"components",
nargs="*",
help="Component names to add (e.g., button, card, dialog)",
)
parser.add_argument(
"--all",
action="store_true",
help="Add all available components",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Overwrite existing components",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without executing",
)
parser.add_argument(
"--list",
action="store_true",
help="List installed components",
)
parser.add_argument(
"--project-root",
type=Path,
help="Project root directory (default: current directory)",
)
args = parser.parse_args()
# Initialize installer
installer = ShadcnInstaller(
project_root=args.project_root,
dry_run=args.dry_run,
)
# Handle list command
if args.list:
success, message = installer.list_installed()
print(message)
sys.exit(0 if success else 1)
# Handle add all command
if args.all:
success, message = installer.add_all_components(overwrite=args.overwrite)
print(message)
sys.exit(0 if success else 1)
# Handle add specific components
if not args.components:
parser.print_help()
sys.exit(1)
success, message = installer.add_components(
args.components,
overwrite=args.overwrite,
)
print(message)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
@@ -0,0 +1,473 @@
#!/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 re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
# Valid npm package name pattern: optional @scope/, then package name with
# optional subpath. Only allows alphanumeric, hyphens, dots, underscores,
# and forward slashes — no quotes, parens, or semicolons.
_VALID_PLUGIN_NAME = re.compile(r'^(@[a-zA-Z0-9_-]+/)?[a-zA-Z0-9_-]+(/[a-zA-Z0-9_.-]+)*$')
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.
Validates each plugin name against a strict allowlist pattern
to prevent code injection via crafted require() statements
(see: CWE-94).
"""
if not self.config["plugins"]:
return ""
plugin_requires = []
for plugin in self.config["plugins"]:
if not _VALID_PLUGIN_NAME.match(plugin):
raise ValueError(
f"Invalid plugin name: {plugin!r}. "
"Plugin names must be valid npm package names "
"(e.g. '@tailwindcss/typography')."
)
plugin_requires.append(f"require('{plugin}')")
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()