Implement comprehensive dark/light mode overhaul for admin panel
- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes. - Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`. - Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors. - Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system. - Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes. - Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* گاردِ تم: رنگها باید از توکنهای `styles.css` بیایند تا دارکمود همهجا کار کند.
|
||||
* hex ثابت و کلاسهای پالت Tailwind با تغییر تم عوض نمیشوند.
|
||||
*/
|
||||
|
||||
const ROOT = join(__dirname, '..');
|
||||
|
||||
/** فاز ۳ — هنوز جفت slate/gray + dark: دارند؛ کار میکنند ولی از design-system منحرفاند. */
|
||||
const DEFERRED = [
|
||||
'pages/DoctorDetailPage.tsx',
|
||||
'pages/UserDetailPage.tsx',
|
||||
'components/schedule/ScheduleSection.tsx',
|
||||
];
|
||||
|
||||
/** رنگهای عمداً ثابت: دادهٔ کاربر یا پوششی که در هر دو تم یکسان است. */
|
||||
const INTENTIONAL = [
|
||||
'pages/TagsSettingsPage.tsx', // TAG_COLORS — پالت انتخابی کاربر برای برچسب
|
||||
'components/ImageCropModal.tsx', // bg-black/50 — اسکریم مودال، مثل .overlay
|
||||
'stores/uiStore.ts', // BrandFixed — هیوی نارنجی عمداً hex ثابت است
|
||||
];
|
||||
|
||||
const HEX = /#[0-9a-fA-F]{3,8}\b/;
|
||||
const PALETTE_UTIL =
|
||||
/(?:^|[\s"'`{])(?:dark:|hover:|focus:|active:|group-hover:|md:|lg:|sm:)*(?:bg|text|border|divide|ring|fill|stroke)-(?:white|black|gray|slate|zinc|neutral|stone|red|green|emerald|blue|sky|amber|yellow|orange|purple|indigo|violet|pink|rose|teal|cyan)(?:-\d{2,3})?\b/;
|
||||
|
||||
function sourceFiles(): string[] {
|
||||
const out: string[] = [];
|
||||
(function walk(dir: string) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
if (statSync(p).isDirectory()) { walk(p); continue; }
|
||||
if (!/\.tsx?$/.test(entry) || /\.test\.tsx?$/.test(entry)) continue;
|
||||
const rel = relative(ROOT, p);
|
||||
if (rel.startsWith('test/')) continue;
|
||||
out.push(rel);
|
||||
}
|
||||
})(ROOT);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** کامنتها و fallbackهای `var(--x, #hex)` رنگِ رندرشده نیستند. */
|
||||
function strip(src: string): string {
|
||||
return src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
||||
.replace(/var\(\s*--[a-z0-9-]+\s*,[^)]*\)/gi, 'var(--token)');
|
||||
}
|
||||
|
||||
describe('theme tokens', () => {
|
||||
const files = sourceFiles();
|
||||
|
||||
it('سورس ادمین را واقعاً پیدا میکند', () => {
|
||||
expect(files.length).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('هیچ کلاس dark: مردهای کنار inline style رنگی نمانده است', () => {
|
||||
const offenders = files.filter((f) => {
|
||||
const src = readFileSync(join(ROOT, f), 'utf8');
|
||||
return src.split('\n').some((line) =>
|
||||
/style=\{\{[^}]*(?:color|background)/.test(line)
|
||||
&& /className=[^>]*\bdark:(?:text|bg|border)-/.test(line));
|
||||
});
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it('فایلهای پاکشده hex ثابت ندارند', () => {
|
||||
const offenders = files
|
||||
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
|
||||
.filter((f) => HEX.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it('فایلهای پاکشده کلاس پالت Tailwind ندارند', () => {
|
||||
const offenders = files
|
||||
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
|
||||
.filter((f) => PALETTE_UTIL.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user