mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 22:19:40 +02:00
Expand validate-theme-tokens.mjs scope from ListViews only to all
lib/modules/**/*.svelte and routes/(app)/**/*.svelte. Add a second rule
banning the neutral Tailwind palette (gray/slate/zinc/neutral/stone-N)
— these should be theme tokens (bg-card, bg-muted, text-foreground,
text-muted-foreground, border-border) instead.
Apply one-shot codemod (scripts/migrate-theme-tokens.mjs) that
replaces:
bg-gray-800/900 → bg-card
bg-gray-600/700 → bg-muted (with opacity preserved)
border-gray-600..900 → border-border
text-gray-800/900 → text-foreground
text-gray-300 → text-foreground/90
text-gray-400/500/700 → text-muted-foreground
placeholder-gray-* → placeholder:text-muted-foreground/60
bg/border-white/N → bg-muted/N, border-border/N
text-white/70-90 → text-foreground
text-white/40-60 → text-muted-foreground
text-white/10-30 → text-muted-foreground/70
42 files touched; biggest: presi/deck/[id] (91 subs), uload/analytics
(58), uload/+page (53), presi/+page (47), who/PlayView (35),
skilltree/Edit+AddXpModal (28 each), context/* (115 across 4 pages),
uload/links+tags (50 across 2).
Brand-literal overlays in moodlit/components/mood/{MoodFullscreen,
MoodCard,CreateMoodDialog}.svelte stay unmigrated — they render on
vivid colour gradients. Validator exempts these 3 files from the
white-alpha rule; they still obey the neutral-palette rule.
Result: 527 files pass validate:theme-tokens; svelte-check clean with
0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
5.6 KiB
JavaScript
162 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Validate that the unified Mana web app uses theme tokens instead of raw
|
|
* Tailwind utilities that bypass the theme system.
|
|
*
|
|
* Background: the unified Mana app supports multiple theme variants (Lume,
|
|
* Nature, Stone, Ocean, plus dark). Utilities like `text-white/80`,
|
|
* `bg-gray-800`, `border-neutral-700` ignore the active theme and can
|
|
* render white-on-white (or dark-on-dark) under the wrong variant. The
|
|
* theme tokens (`text-foreground`, `bg-muted`, `border-border`, etc.) are
|
|
* generated by Tailwind v4 from `packages/shared-tailwind/src/themes.css`
|
|
* and resolve per-theme automatically.
|
|
*
|
|
* Rules (enforced across `apps/mana/apps/web/src/lib/modules/**` and
|
|
* `apps/mana/apps/web/src/routes/(app)/**`):
|
|
*
|
|
* 1. No white-alpha utilities:
|
|
* `bg-white/N`, `text-white/N`, `border-white/N` (and hover:/focus:/etc. variants)
|
|
*
|
|
* 2. No neutral-palette utilities:
|
|
* `(bg|text|border)-(gray|slate|zinc|neutral|stone)-N` (with or without /N)
|
|
* — these should be theme tokens instead.
|
|
*
|
|
* Replacement cheat-sheet:
|
|
* bg-white/N → bg-muted/N or bg-card
|
|
* text-white → text-foreground
|
|
* text-white/40..60 → text-muted-foreground
|
|
* border-white/N → border-border
|
|
* bg-gray-800 → bg-card or bg-muted
|
|
* bg-neutral-900 → bg-card
|
|
* text-gray-400 → text-muted-foreground
|
|
* text-slate-300 → text-foreground/90
|
|
* border-gray-700 → border-border
|
|
*
|
|
* Brand-literal escape hatch: if a color must stay literal (overlay on a
|
|
* vivid gradient, photo viewer backdrop, domain-semantic palette), move
|
|
* it into a scoped <style> block — the validator only scans class
|
|
* attributes. See `lib/modules/moodlit/ListView.svelte:.mood-card:hover`
|
|
* for the pattern.
|
|
*
|
|
* Zero deps — runs as plain Node ESM. Uses `git ls-files` to respect
|
|
* .gitignore.
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = join(__dirname, '..');
|
|
|
|
const SCAN_GLOBS = [
|
|
'apps/mana/apps/web/src/lib/modules/**/*.svelte',
|
|
'apps/mana/apps/web/src/routes/(app)/**/*.svelte',
|
|
];
|
|
|
|
/**
|
|
* Files where white-alpha utilities are intentional brand literals —
|
|
* they render overlays on vivid mood-gradient backgrounds, not theme
|
|
* surfaces. Exempt from the `white/` rule but still must follow the
|
|
* neutral-palette rule.
|
|
*/
|
|
const BRAND_OVERLAY_FILES = new Set([
|
|
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodFullscreen.svelte',
|
|
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodCard.svelte',
|
|
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/CreateMoodDialog.svelte',
|
|
]);
|
|
|
|
// `(?:^|[\s:"'`])` anchors so `hover:bg-white/5` and class=`bg-white/10`
|
|
// both match, without tripping on unrelated substrings like `off-white`.
|
|
const RULES = [
|
|
{
|
|
name: 'white-alpha',
|
|
pattern: /(?:^|[\s:"'`])(bg|text|border)-white\/\d/g,
|
|
describe: (m) => `${m[1]}-white/`,
|
|
},
|
|
{
|
|
name: 'neutral-palette',
|
|
pattern: /(?:^|[\s:"'`])(bg|text|border)-(gray|slate|zinc|neutral|stone)-\d/g,
|
|
describe: (m) => `${m[1]}-${m[2]}-`,
|
|
},
|
|
];
|
|
|
|
function listFiles() {
|
|
const args = SCAN_GLOBS.map((g) => `"${g}"`).join(' ');
|
|
const out = execSync(`git ls-files ${args}`, {
|
|
cwd: REPO_ROOT,
|
|
encoding: 'utf8',
|
|
});
|
|
return out
|
|
.split('\n')
|
|
.map((p) => p.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function validate() {
|
|
const paths = listFiles();
|
|
const violations = [];
|
|
|
|
for (const rel of paths) {
|
|
const abs = join(REPO_ROOT, rel);
|
|
const src = readFileSync(abs, 'utf8');
|
|
const lines = src.split('\n');
|
|
|
|
const brandOverlay = BRAND_OVERLAY_FILES.has(rel);
|
|
lines.forEach((line, i) => {
|
|
for (const rule of RULES) {
|
|
if (brandOverlay && rule.name === 'white-alpha') continue;
|
|
rule.pattern.lastIndex = 0;
|
|
let match;
|
|
while ((match = rule.pattern.exec(line)) !== null) {
|
|
violations.push({
|
|
file: rel,
|
|
line: i + 1,
|
|
rule: rule.name,
|
|
token: rule.describe(match),
|
|
snippet: line.trim().slice(0, 120),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
// Group by file for readability.
|
|
const byFile = new Map();
|
|
for (const v of violations) {
|
|
if (!byFile.has(v.file)) byFile.set(v.file, []);
|
|
byFile.get(v.file).push(v);
|
|
}
|
|
console.error(
|
|
`\n✗ Theme-token check FAILED (${violations.length} violation(s) in ${byFile.size} file(s)):\n`
|
|
);
|
|
for (const [file, vs] of byFile) {
|
|
console.error(` ${file} (${vs.length})`);
|
|
for (const v of vs.slice(0, 5)) {
|
|
console.error(` :${v.line} [${v.rule}: ${v.token}] ${v.snippet.slice(0, 80)}`);
|
|
}
|
|
if (vs.length > 5) console.error(` … +${vs.length - 5} more`);
|
|
}
|
|
console.error(
|
|
`\nReplace with theme tokens:\n` +
|
|
` bg-white/N → bg-muted/N or bg-card\n` +
|
|
` text-white → text-foreground\n` +
|
|
` text-white/40..60 → text-muted-foreground\n` +
|
|
` border-white/N → border-border\n` +
|
|
` bg-gray/slate/neutral-N → bg-muted / bg-card\n` +
|
|
` text-gray/slate-N → text-foreground / text-muted-foreground\n` +
|
|
` border-gray/slate-N → border-border\n\n` +
|
|
`Rationale: raw color utilities ignore theme variants (Lume, Nature, ...)\n` +
|
|
`and can render invisible under other themes. See\n` +
|
|
`packages/shared-tailwind/src/themes.css for the full token set.\n` +
|
|
`Brand-literal overlays may move into scoped <style> blocks.\n`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`✓ Theme-token check: ${paths.length} files use theme tokens correctly.`);
|
|
}
|
|
|
|
validate();
|