#!/usr/bin/env node /** * Replace old hand-written CSS class names with Tailwind v4 equivalents. * SAFE version: only replaces inside class="..." attribute values. * * Run: node scripts/cleanup-css.mjs */ import { readFileSync, writeFileSync } from 'fs'; import { globSync } from 'glob'; // Order matters: most-specific (multi-word) patterns first. // Each entry: [old-class-string, new-tailwind-classes] const REPLACEMENTS = [ // ── Page structure ────────────────────────────────────────────── ['page-hero-card page-actions', 'mb-6 flex items-start justify-between gap-4'], ['page-actions-right', 'flex items-center gap-2'], ['page-actions', 'mb-6 flex items-start justify-between gap-4'], ['page-title-block', 'flex flex-col gap-1'], ['page-title', 'text-2xl font-bold text-gray-900'], ['page-subtitle', 'mt-1 text-sm text-gray-500'], // ── Card ──────────────────────────────────────────────────────── ['card', 'rounded-xl border border-gray-200 bg-white shadow-sm'], // ── Buttons (specific before generic) ─────────────────────────── ['btn navy', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'], ['btn primary', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'], ['btn danger', 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors'], ['btn sm', 'inline-flex items-center rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 transition-colors'], ['btn', 'inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors'], // ── Table ──────────────────────────────────────────────────────── ['table-wrap', 'overflow-x-auto'], ['list-table', 'w-full text-sm'], ['perm-table', 'w-full text-sm'], ['table-actions', 'flex items-center justify-end gap-1'], ['align-right', 'text-right'], ['row-selected', 'bg-orange-50'], // ── Icon action buttons ────────────────────────────────────────── ['action-icon-btn danger', 'rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm'], ['action-icon-btn', 'rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm'], // ── Status chips (specific before generic) ─────────────────────── ['status-chip active', 'inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700'], ['status-chip pending', 'inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-700'], ['status-chip inactive', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500'], ['status-chip', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600'], // ── Error / notice ─────────────────────────────────────────────── ['error-box', 'mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'], // ── Form sections ──────────────────────────────────────────────── ['role-form-section', 'mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm'], ['field-grid-2', 'mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2'], ['module-picker', 'mt-3 flex flex-wrap gap-2'], ['module-chip selected', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-orange-300 bg-orange-50 px-3 py-1.5 text-xs font-medium text-orange-700'], ['module-chip', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300'], // ── Legacy tab nav (now handled by AdminShell) ──────────────────── ['admin-link-tabs', 'hidden'], ['admin-link-tab active', 'hidden'], ['admin-link-tab', 'hidden'], ]; /** * Replace old CSS class names only inside JSX class attribute values. * Handles: * class="foo bar baz" * class={'foo bar baz'} * class={`foo bar baz`} * class={condition ? 'foo' : 'bar'} * * We do this by extracting the string portion of class attributes and * replacing only within those extracted portions. */ function replaceInClassAttributes(src) { let changed = false; // Process class="..." and class='...' (static strings) src = src.replace(/class=["']([^"']*)["']/g, (match, classes) => { let replaced = classes; for (const [from, to] of REPLACEMENTS) { // Replace whole-word occurrences within the class string // Use word boundaries to avoid partial matches const re = new RegExp(`(? { let replaced = classes; for (const [from, to] of REPLACEMENTS) { const re = new RegExp(`(? { let replaced = classes; for (const [from, to] of REPLACEMENTS) { const re = new RegExp(`(? { // Replace inside single/double-quoted string literals within the expression const replaced = match.replace(/'([^']*)'/g, (m, inner) => { let r = inner; for (const [from, to] of REPLACEMENTS) { const re = new RegExp(`(? { // skip JSX attributes within the expression (avoid double replacement) let r = inner; for (const [from, to] of REPLACEMENTS) { const re = new RegExp(`(?