#!/usr/bin/env python3 """Rewrite hardcoded dark-only / light-only colors to semantic theme tokens.""" from __future__ import annotations import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] TARGETS = [ ROOT / "app", ROOT / "components" / "ui", ] SKIP_NAMES = {"Sidebar.tsx"} TOKEN_MAP = { "text-gray-300": "text-muted-foreground", "text-gray-400": "text-muted-foreground", "text-gray-500": "text-muted-foreground", "text-gray-600": "text-muted-foreground", "text-gray-700": "text-foreground", "text-gray-800": "text-foreground", "text-gray-900": "text-foreground", "placeholder-gray-500": "placeholder:text-muted-foreground", "placeholder:text-gray-400": "placeholder:text-muted-foreground", "placeholder:text-gray-500": "placeholder:text-muted-foreground", "bg-white/5": "bg-muted/50", "bg-white/[0.02]": "bg-muted/30", "bg-white/[0.01]": "bg-muted/20", "hover:bg-white/5": "hover:bg-muted", "hover:bg-white/10": "hover:bg-muted", "border-white/10": "border-border", "border-white/5": "border-border", "border-white/20": "border-border", "divide-white/5": "divide-border", "divide-white/10": "divide-border", "bg-zinc-900": "bg-card", "bg-zinc-800": "bg-muted", "border-white/30": "border-border", } COLORED_BG = re.compile( r"bg-(red-|primary|green-|emerald-|blue-500|blue-600|blue-700|amber-500|navy|\[#|black)" ) CLASS_ATTR = re.compile( r"""(?:className|class)\s*=\s*(?:\{`([^`]*)`\}|"([^"]*)"|\{"([^"]*)"\})""", re.MULTILINE, ) def rewrite_class_string(raw: str) -> str: tokens = raw.split() has_colored = any(COLORED_BG.search(t) for t in tokens) out = [] for token in tokens: if token in TOKEN_MAP: out.append(TOKEN_MAP[token]) continue if token == "text-white" and not has_colored: out.append("text-foreground") continue if token == "hover:text-white" and not has_colored: out.append("hover:text-foreground") continue out.append(token) return " ".join(out) def rewrite_file(path: Path) -> bool: original = path.read_text(encoding="utf-8") updated = original def repl(match: re.Match) -> str: full = match.group(0) body = match.group(1) or match.group(2) or match.group(3) or "" new_body = rewrite_class_string(body) if new_body == body: return full return full.replace(body, new_body, 1) updated = CLASS_ATTR.sub(repl, updated) # Bare replacements outside className (template fragments) for old, new in TOKEN_MAP.items(): updated = updated.replace(old, new) if updated != original: path.write_text(updated, encoding="utf-8") return True return False def main() -> None: changed = [] for folder in TARGETS: for path in folder.rglob("*.tsx"): if path.name in SKIP_NAMES: continue if rewrite_file(path): changed.append(str(path.relative_to(ROOT))) print(f"updated {len(changed)} files") for name in changed: print(f" {name}") if __name__ == "__main__": main()