mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
refactor(theming): re-apply theme validator suite after parallel rebase
The plan-doc commits129971ffc+9db044178dropped the audit-theme-tokens → validate-theme-variables rename, the validate-theme-tokens → validate-theme-utilities rename, the new validate-theme-parity script, brand-literals.md, and the corresponding package.json + lint-staged.config.js + themes.css wiring. The files still existed on disk (git mv changes survived) but were untracked. Restore the validator suite so `pnpm run validate:all` works again: - validate:theme-variables (CSS var names: --muted → --color-muted) - validate:theme-utilities (Tailwind: no white/N, no neutral palette) - validate:theme-parity (every --color-* in :root ⇔ .dark + each [data-theme="..."]) All three wired into validate:all and lint-staged. `pnpm run validate:all` is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
766ad2ea8f
commit
430aa30cbf
7 changed files with 411 additions and 17 deletions
219
scripts/validate-theme-variables.mjs
Normal file
219
scripts/validate-theme-variables.mjs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Validate theme CSS variable names.
|
||||
*
|
||||
* The Mana theme system standardizes on `--color-*` CSS custom properties
|
||||
* (defined in `packages/shared-tailwind/src/themes.css`). Earlier, components
|
||||
* ported from shadcn drifted into bare names (`--muted`, `--primary`, …) and
|
||||
* `--theme-*` prefixes, neither of which exist in the theme. Those
|
||||
* references silently fell back to nothing (or to literal fallbacks) and
|
||||
* stopped tracking the active theme variant.
|
||||
*
|
||||
* This check greps Svelte/CSS/TS source files for those legacy patterns
|
||||
* and fails if any remain. Run in CI and lint-staged so the drift can't
|
||||
* sneak back in.
|
||||
*
|
||||
* Companion check: `validate-theme-utilities.mjs` enforces theme-token
|
||||
* Tailwind classes (e.g. `text-foreground` instead of `text-white/80`).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/validate-theme-variables.mjs
|
||||
*/
|
||||
|
||||
import { readdirSync, statSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..');
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const DIM = '\x1b[2m';
|
||||
const RESET = '\x1b[0m';
|
||||
const BOLD = '\x1b[1m';
|
||||
|
||||
const SCAN_ROOTS = ['apps', 'packages'];
|
||||
const SCAN_EXTS = new Set(['.svelte', '.css', '.ts', '.tsx']);
|
||||
const SKIP_DIRS = new Set([
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.svelte-kit',
|
||||
'.next',
|
||||
'.turbo',
|
||||
'.vercel',
|
||||
'.vite',
|
||||
]);
|
||||
|
||||
// Shadcn-convention names that should be `--color-*` in this repo.
|
||||
const SHADCN_TOKENS = [
|
||||
'muted',
|
||||
'muted-foreground',
|
||||
'primary',
|
||||
'primary-foreground',
|
||||
'secondary',
|
||||
'secondary-foreground',
|
||||
'accent',
|
||||
'accent-foreground',
|
||||
'foreground',
|
||||
'background',
|
||||
'border',
|
||||
'border-strong',
|
||||
'card',
|
||||
'card-foreground',
|
||||
'popover',
|
||||
'popover-foreground',
|
||||
'destructive',
|
||||
'destructive-foreground',
|
||||
'input',
|
||||
'ring',
|
||||
'surface',
|
||||
'surface-hover',
|
||||
'surface-elevated',
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
];
|
||||
|
||||
// Per-element inline custom properties and unrelated local vars that happen
|
||||
// to collide on a word. Keeping this list narrow — add entries with care.
|
||||
const ALLOWED_LOCAL_NAMES = new Set([
|
||||
'primary-color', // shared-auth-ui inline prop carrier
|
||||
'app-color',
|
||||
'btn-color',
|
||||
'tag-color',
|
||||
'content-bg',
|
||||
'ring-color',
|
||||
]);
|
||||
|
||||
// Files we know carry module-literal brand accents (see themes.css §4 —
|
||||
// brand/domain semantics stay as literals, not theme tokens).
|
||||
const ALLOWED_FILES = new Set([
|
||||
'apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte',
|
||||
'apps/mana/apps/web/src/routes/(app)/news-research/+page.svelte',
|
||||
// Agent template colors are set per-element via style="--accent: ..."
|
||||
'apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte',
|
||||
]);
|
||||
|
||||
const bareTokenRe = new RegExp(`var\\(--(${SHADCN_TOKENS.join('|')})\\s*[,)]`, 'g');
|
||||
const themePrefixRe = /var\(--theme-[a-z-]+\s*[,)]/g;
|
||||
|
||||
function* walk(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (SKIP_DIRS.has(name)) continue;
|
||||
const full = join(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync(full);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (st.isDirectory()) {
|
||||
yield* walk(full);
|
||||
} else if (SCAN_EXTS.has(name.slice(name.lastIndexOf('.')))) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* findViolations(filePath) {
|
||||
const rel = filePath.slice(ROOT.length + 1);
|
||||
if (ALLOWED_FILES.has(rel)) return;
|
||||
|
||||
let content;
|
||||
try {
|
||||
content = readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Skip var(--color-*) — those are correct. We also skip the
|
||||
// redefinition block inside @theme in themes.css itself.
|
||||
for (const m of line.matchAll(bareTokenRe)) {
|
||||
const match = m[0];
|
||||
// Ignore if this is actually var(--color-X) with a shared suffix
|
||||
// (regex doesn't match because of the leading `--color-`, but be
|
||||
// doubly safe in case of future edits).
|
||||
if (match.startsWith('var(--color-')) continue;
|
||||
yield { line: i + 1, col: m.index + 1, text: line.trim(), kind: 'shadcn' };
|
||||
}
|
||||
|
||||
for (const m of line.matchAll(themePrefixRe)) {
|
||||
yield { line: i + 1, col: m.index + 1, text: line.trim(), kind: 'theme-prefix' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalFiles = 0;
|
||||
let violationCount = 0;
|
||||
const byFile = new Map();
|
||||
|
||||
for (const sub of SCAN_ROOTS) {
|
||||
for (const file of walk(join(ROOT, sub))) {
|
||||
totalFiles++;
|
||||
const hits = [...findViolations(file)];
|
||||
if (hits.length === 0) continue;
|
||||
|
||||
// Second-level filter: ALLOWED_LOCAL_NAMES (collision guard).
|
||||
const filtered = hits.filter((h) => {
|
||||
if (h.kind !== 'shadcn') return true;
|
||||
const nameMatch = h.text.match(/var\(--([a-z-]+)/);
|
||||
if (!nameMatch) return true;
|
||||
return !ALLOWED_LOCAL_NAMES.has(nameMatch[1]);
|
||||
});
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
byFile.set(file.slice(ROOT.length + 1), filtered);
|
||||
violationCount += filtered.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (violationCount === 0) {
|
||||
console.log(
|
||||
`${GREEN}✓${RESET} Theme tokens clean — scanned ${totalFiles} files, no bare ${BOLD}--muted${RESET}/${BOLD}--primary${RESET}/${BOLD}--theme-*${RESET} references.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${RED}✗${RESET} Found ${BOLD}${violationCount}${RESET} theme-token violation${violationCount === 1 ? '' : 's'} across ${byFile.size} file${byFile.size === 1 ? '' : 's'}:\n`
|
||||
);
|
||||
|
||||
for (const [file, hits] of byFile) {
|
||||
console.log(`${BOLD}${file}${RESET}`);
|
||||
for (const h of hits) {
|
||||
const tag =
|
||||
h.kind === 'theme-prefix' ? `${YELLOW}theme-prefix${RESET}` : `${YELLOW}shadcn${RESET}`;
|
||||
console.log(` ${DIM}${h.line}:${h.col}${RESET} [${tag}] ${h.text}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${DIM}Fix: replace bare tokens with --color-* equivalents. Example:${RESET}
|
||||
${RED}-${RESET} background: hsl(var(--muted));
|
||||
${GREEN}+${RESET} background: hsl(var(--color-muted));
|
||||
|
||||
${DIM}Or for shadcn-style inline fallbacks:${RESET}
|
||||
${RED}-${RESET} color: var(--foreground, #111);
|
||||
${GREEN}+${RESET} color: hsl(var(--color-foreground));
|
||||
|
||||
${DIM}See packages/shared-tailwind/src/themes.css for the full token list.${RESET}
|
||||
`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
Loading…
Add table
Add a link
Reference in a new issue