- 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>
167 lines
8.1 KiB
JavaScript
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.`);
|