#!/usr/bin/env node /** * Validate that module ListView.svelte files use theme tokens instead of * raw white-alpha Tailwind utilities. * * Background: the unified Mana app supports multiple theme variants (Lume, * Nature, Stone, Ocean, plus dark). Utilities like `text-white/80`, * `bg-white/5`, `border-white/10` ignore the active theme and can render * white-on-white in light variants. The theme tokens (`text-foreground`, * `bg-muted`, `border-border`, etc.) are the canonical replacements — * they're generated by Tailwind v4 from `packages/shared-tailwind/src/themes.css` * and resolve per-theme automatically. * * Rule: `src/lib/modules/**\/ListView.svelte` must not contain * - `bg-white/` (e.g. bg-white/5, bg-white/10, hover:bg-white/5) * - `text-white/` (e.g. text-white/80, hover:text-white/90) * - `border-white/` (e.g. border-white/10, focus:border-white/20) * * Suggested replacements: * 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 * * `text-white` without an opacity modifier is allowed when it sits on a * guaranteed-dark surface (mood gradient overlays, photo viewer) — those * are brand literals per the themes.css policy. * * 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 LIST_VIEW_GLOB = 'apps/mana/apps/web/src/lib/modules/*/ListView.svelte'; // `\b` before ensures we catch `hover:bg-white/`, `focus:border-white/`, // etc., without matching unrelated class names like `off-white`. const FORBIDDEN = /(?:^|[\s:"'`])(bg|text|border)-white\//g; function listListViews() { const out = execSync(`git ls-files "${LIST_VIEW_GLOB}"`, { cwd: REPO_ROOT, encoding: 'utf8', }); return out .split('\n') .map((p) => p.trim()) .filter(Boolean); } function validate() { const paths = listListViews(); const violations = []; for (const rel of paths) { const abs = join(REPO_ROOT, rel); const src = readFileSync(abs, 'utf8'); const lines = src.split('\n'); lines.forEach((line, i) => { FORBIDDEN.lastIndex = 0; let match; while ((match = FORBIDDEN.exec(line)) !== null) { violations.push({ file: rel, line: i + 1, token: `${match[1]}-white/`, snippet: line.trim().slice(0, 120), }); } }); } if (violations.length > 0) { console.error(`\n✗ Theme-token check FAILED (${violations.length} violation(s)):\n`); for (const v of violations) { console.error(` • ${v.file}:${v.line} [${v.token}]`); console.error(` ${v.snippet}`); } console.error( `\nReplace raw white-alpha utilities 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\n` + `Rationale: raw white utilities ignore theme variants (Lume, Nature, ...)\n` + `and can render white-on-white under light themes. See\n` + `packages/shared-tailwind/src/themes.css for the full token set.\n` ); process.exit(1); } console.log(`✓ Theme-token check: ${paths.length} ListView(s) use theme tokens correctly.`); } validate();