#!/usr/bin/env node /** * @file One-off codemod: rewrites raw Tailwind gray-scale utility classes * (neutral, gray, slate, zinc, stone shades) across client/src to the * semantic surface/border/fg tokens introduced for dark/light mode. * Status colors (emerald/red/amber) and the categorical hue palette * (violet/indigo/cyan/teal/sky/rose/pink/orange/yellow) are deliberately * out of scope — see docs/superpowers/plans/2026-07-31-color-redesign-dark-light-mode.md. * @author Nguyễn Ngọc Trí Vĩ */ import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const MAP = { text: { "gray-50": "fg-primary", "gray-100": "fg-primary", "gray-200": "fg-primary", "neutral-50": "fg-primary", "neutral-100": "fg-primary", "gray-300": "fg-secondary", "gray-400": "fg-secondary", "neutral-200": "fg-secondary", "neutral-300": "fg-secondary", "neutral-400": "fg-secondary", "slate-300": "fg-secondary", "gray-500": "fg-muted", "gray-600": "fg-muted", "gray-700": "fg-muted", "neutral-500": "fg-muted", "neutral-600": "fg-muted", }, placeholder: { "gray-500": "fg-muted", "gray-600": "fg-muted", }, fill: { "gray-100": "fg-primary", "gray-300": "fg-secondary", "gray-600": "fg-muted", }, bg: { "neutral-900": "surface-0", "gray-900": "surface-0", "neutral-800": "surface-2", "gray-800": "surface-2", "neutral-700": "surface-3", "gray-700": "surface-3", "neutral-500": "surface-4", "gray-500": "surface-4", "gray-400": "surface-4", "gray-600": "surface-4", "slate-500": "surface-4", }, border: { "neutral-800": "border", "gray-800": "border", "neutral-700": "border-light", "gray-700": "border-light", "neutral-500": "border-light", "gray-500": "border-light", "gray-600": "border-light", }, ring: { "slate-500": "border-light", "slate-400": "border-light", }, }; function rewrite(text) { let out = text; for (const [prop, shadeMap] of Object.entries(MAP)) { for (const [rawShade, token] of Object.entries(shadeMap)) { // Matches `bg-gray-500`, `bg-gray-500/10`, `bg-gray-500/50` etc. — the // opacity suffix is preserved verbatim since the token's CSS var // already supports ``. const re = new RegExp(`\\b${prop}-${rawShade}(\\/[0-9]+)?\\b`, "g"); out = out.replace(re, (_m, opacity) => `${prop}-${token}${opacity || ""}`); } } return out; } function walk(dir, files = []) { for (const entry of readdirSync(dir)) { const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { if (entry === "node_modules") continue; walk(full, files); } else if (entry.endsWith(".tsx")) { files.push(full); } } return files; } const scriptDir = fileURLToPath(new URL(".", import.meta.url)); const root = join(scriptDir, "..", "client", "src"); const files = walk(root); let changed = 0; for (const file of files) { const before = readFileSync(file, "utf8"); const after = rewrite(before); if (after !== before) { writeFileSync(file, after); changed++; } } console.log(`rewrote ${changed} file(s)`);