nxtgauge-admin-solid/scripts/cleanup-css.mjs
Ashwin Kumar a272276055 feat(admin): Phase 0 — Tailwind v4 foundation, shell rewrite, modern dashboard
- Install Tailwind CSS v4 via @tailwindcss/vite; configure vite.config.ts
- Rewrite app.css: Tailwind base, Exo 2 font, brand tokens (orange #fd6216, navy #050026), scrollbar utility; fix @import order
- Rewrite AdminShell.tsx: fixed header, fixed inset body grid (sidebar + main), session check, sub-tab system, logout, admin avatar/name/role
- Rewrite AdminSidebar.tsx: collapsible w-64/w-20, orange active rail + badge/dot, CSS filter for SVG icon tinting, min-h-0 flex fix
- Replace 84 route stub CSS classes (page-title, card, btn, table-wrap, etc.) with Tailwind equivalents via safe class-attr-only regex script
- Rewrite admin dashboard: Lucide icons in colored chip backgrounds, 4-col KPI grid, Control Plane 6-module grid, hover lift animations
- Disable SSR (ssr: false) to fix Vinxi dev manifest error; clear stale .vinxi cache
- Add lucide-solid icon library
- Add scripts/cleanup-css.mjs for class migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:00:21 +01:00

167 lines
8.1 KiB
JavaScript

#!/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(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
const quote = match[6] === '"' ? '"' : "'"; // char after `class=`
return `class=${quote}${replaced}${quote}`;
});
// Process class={`...`} (template literals with static content)
src = src.replace(/class=\{`([^`]*)`\}/g, (match, classes) => {
let replaced = classes;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
return `class={\`${replaced}\`}`;
});
// Process class={'...'} and class={"..."} (single-expression strings)
src = src.replace(/class=\{(['"])([^'"]*)\1\}/g, (match, q, classes) => {
let replaced = classes;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
return `class={${q}${replaced}${q}}`;
});
// Process ternary class values: class={cond ? 'foo' : 'bar'}
// Match string literals inside class={...} expressions
src = src.replace(/class=\{[^}]+\}/g, (match) => {
// 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(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
r = r.replace(re, to);
}
if (r !== inner) changed = true;
return `'${r}'`;
}).replace(/"([^"]*)"/g, (m, inner) => {
// skip JSX attributes within the expression (avoid double replacement)
let r = inner;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
r = r.replace(re, to);
}
if (r !== inner) changed = true;
return `"${r}"`;
});
return replaced;
});
return { src, changed };
}
function escapeRe(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const files = globSync('src/routes/admin/**/*.tsx', {
cwd: '/Users/ashwin/workspace/nxtgauge-admin-solid',
absolute: true,
});
let totalChanged = 0;
for (const file of files) {
const original = readFileSync(file, 'utf8');
const { src, changed } = replaceInClassAttributes(original);
if (changed) {
writeFileSync(file, src, 'utf8');
totalChanged++;
console.log(` updated: ${file.replace('/Users/ashwin/workspace/nxtgauge-admin-solid/', '')}`);
}
}
console.log(`\nDone. Updated ${totalChanged} / ${files.length} files.`);