Files
EpicNext-Cms/scripts/check-admin-colors.mjs
T
openhands 58c35a2920
CI / runtime-diagnostics (push) Skipped
CI / check (push) Successful in 32s
CI / release (push) Skipped
CI / deploy (push) Successful in 58s
feat: enforce no hardcoded colors across entire CMS
Added scripts/check-admin-colors.mjs — scans all src/ files for:
- text-white, text-black (use theme text vars)
- bg-white, bg-black (use theme background/overlay vars)
- bg/text/border/ring with gray/slate/zinc/stone palette
- bg/text/border/ring with red/green/blue/etc palette

Fixed 21 violations across 11 files:
- Overlays: bg-black/* → bg-foreground/*
- Text: text-white → text-primary-foreground
- Backgrounds: bg-white/10 → bg-background/10
- Green accents: bg-green-* → bg-primary
- Red accents: bg-red-* → bg-destructive

Integrated into:
- lint-staged: runs on every *.ts/*.tsx commit
- vitest: src/lib/no-hardcoded-colors.test.ts replaces old audit test
- Allowlist: shadcn/ui primitives (button, badge, dialog) + 4 graphical files
2026-09-01 19:33:50 +02:00

144 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Checks admin source files for hardcoded color utilities that should use
* theme CSS variables instead. Exits 1 on violations.
*
* Usage: node scripts/check-admin-colors.mjs [file ...]
* When called with file arguments (by lint-staged), only those files are
* checked. Without arguments, scans all ROOTS.
*/
import { readdirSync, readFileSync } from "node:fs";
import { join, relative, resolve } from "node:path";
const ROOTS = [
"src/app",
"src/components",
"src/lib",
"src/features",
"src/hooks",
];
const ALLOWLIST = new Set([
"src/app/admin/favicon/favicon-generator.tsx",
"src/app/admin/import/clone/import-clone-client.tsx",
"src/components/admin/catalog/items-shop-preview.tsx",
"src/components/admin/media-grid.tsx",
// shadcn/ui primitives — hardcoded colors are intentional design tokens
"src/components/ui/button.tsx",
"src/components/ui/badge.tsx",
"src/components/ui/dialog.tsx",
]);
// Patterns that indicate hardcoded theme colors
const RULES = [
{
name: "no-text-white",
pattern: /text-white(?:\/\d+)?/g,
message:
"Use theme text colors (e.g. text-foreground, text-card-foreground)",
},
{
name: "no-bg-white",
pattern: /bg-white(?:\/\d+)?/g,
message:
"Use theme background colors (e.g. bg-background, bg-card, bg-surface)",
},
{
name: "no-text-black",
pattern: /text-black(?:\/\d+)?/g,
message:
"Use theme text colors (e.g. text-foreground, text-card-foreground)",
},
{
name: "no-bg-black-hardcoded",
pattern: /bg-black(?:\/\d+)?/g,
message:
"Use theme overlay vars (e.g. bg-foreground/50) or documented overrides",
},
{
name: "no-gray-palette",
pattern:
/(?:text|bg|border|ring)-(?:gray|slate|zinc|neutral|stone)-(?:[1-9]00|50)(?:\/\d+)?/g,
message: "Use theme CSS variables instead of Tailwind gray palette",
},
{
name: "no-colored-palette",
pattern:
/(?:text|bg|border|ring)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:[1-9]00|50)(?:\/\d+)?/g,
message:
"Use theme CSS variables (e.g. text-destructive, bg-accent) instead",
},
];
function sourceFiles(directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(path)
: /\.(?:ts|tsx)$/.test(entry.name) &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".test.tsx")
? [path]
: [];
});
}
const cwd = process.cwd();
const files =
process.argv.length > 2
? process.argv.slice(2).map((f) => resolve(f))
: ROOTS.flatMap((r) => sourceFiles(r));
const violations = [];
for (const file of files) {
const normalized = relative(cwd, file).replaceAll("\\", "/");
if (ALLOWLIST.has(normalized)) continue;
if (normalized.endsWith(".test.ts") || normalized.endsWith(".test.tsx"))
continue;
if (!/\.(?:ts|tsx)$/.test(normalized)) continue;
const source = readFileSync(file, "utf8");
const lines = source.split("\n");
for (const rule of RULES) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// skip comments
const trimmed = line.trimStart();
if (
trimmed.startsWith("//") ||
trimmed.startsWith("*") ||
trimmed.startsWith("/*")
)
continue;
for (const match of line.matchAll(rule.pattern)) {
violations.push({
file: normalized,
line: i + 1,
match: match[0],
rule: rule.name,
message: rule.message,
});
}
}
}
}
if (violations.length > 0) {
console.error("\n🚫 Hardcoded color violations found in admin files:\n");
for (const v of violations) {
console.error(` ${v.file}:${v.line} ${v.match}`);
console.error(` → ${v.message}`);
}
console.error(
`\n${violations.length} violation(s) found. Use theme CSS variables instead.\n`,
);
process.exit(1);
} else {
console.log("✅ No hardcoded color violations found.");
}