Files
clinicpro/.claude/prompt/admin-theme-dark-light-audit.md
T
hamed 55ab2f5dfc 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.
2026-07-27 16:41:52 +03:30

325 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# اصلاح کامل لایت‌مود / دارک‌مود در پنل ادمین (SPA)
## پروژه
`clinicpro` — فقط فرانت‌اند پنل ادمین (`assets/admin/`). بک‌اند و API دست نمی‌خورد.
## زمینه
پنل ادمین یک design-system کامل مبتنی بر CSS variable دارد که در `assets/admin/styles.css` تعریف شده:
- `:root` مقادیر لایت (`--bg`, `--surface`, `--text`, `--text-2`, `--border`, `--primary`, `--danger`, …)
- `[data-theme="dark"]` همان توکن‌ها را برای دارک بازتعریف می‌کند
- `assets/admin/stores/uiStore.ts``applyTheme()` روی `document.documentElement` هم کلاس `dark` و هم `data-theme` را ست می‌کند، پس **هم `dark:` واریانت Tailwind و هم `var(--token)` هر دو فعال‌اند**
مشکل این است که بخش بزرگی از صفحات این سیستم را دور می‌زنند و رنگ ثابت می‌نویسند. آمار واقعی امروز (اندازه‌گیری‌شده روی `assets/admin`، بدون فایل‌های تست):
| الگو | تعداد |
|---|---|
| `style={{ color/background: ... }}` | ۱۰۳۷ نقطه در ~۶۰ فایل |
| hex ثابت داخل `.tsx` | ۷۵۲ |
| utility رنگ ثابت Tailwind (`bg-white`, `text-slate-400`, `border-gray-100`, …) | ۸۸۵ در ۲۰ فایل |
| از این‌ها با جفت `dark:` | ۳۲۳ |
| کلاس‌های Tailwind با توکن (`text-[var(--text-3)]`) | فقط ۱۵ مورد |
## مشکل / هدف
در دارک‌مود بخش‌هایی از پنل روشن/ناخوانا می‌مانند. سه دستهٔ خرابی، به ترتیب اهمیت:
### دستهٔ A — `style` inline با رنگ ثابت (خرابی قطعی)
inline style قابل override با `dark:` نیست. در ~۹۰ نقطه دقیقاً همین اشتباه رخ داده: کنار inline style یک کلاس `dark:` نوشته شده که **هیچ‌وقت اعمال نمی‌شود** چون specificity کلاس همیشه کمتر از style attribute است.
فایل‌های آلوده به این الگوی مرده:
| فایل | تعداد نقطهٔ `dark:` مرده |
|---|---|
| `components/session/PaymentStep.tsx` | ۲۷ |
| `components/session/DetailsStep.tsx` | ۲۶ |
| `components/session/CreateStep.tsx` | ۱۴ |
| `components/SessionPaymentAccordion.tsx` | ۷ |
| `components/PatientCaseBanner.tsx` | ۷ |
| `components/SessionServiceCard.tsx` | ۵ |
| `pages/SessionPaymentPage.tsx`, `pages/NewSessionPage.tsx`, `pages/EditSessionPage.tsx`, `components/AppointmentTurnCard.tsx` | ۲ هرکدام |
پرمصرف‌ترین فایل‌ها از نظر کل inline style رنگی: `pages/MyPatientsPage.tsx` (۶۷)، `pages/PatientDetailPage.tsx` (۶۲)، `pages/SmsWalletPage.tsx` (۴۶)، `components/session/PaymentStep.tsx` (۴۳)، `pages/SubscriptionPage.tsx` (۳۱)، `pages/DoctorDetailPage.tsx` (۳۰)، `pages/ClinicServicesPage.tsx` (۲۹)، `components/session/DetailsStep.tsx` (۲۸)، `components/session/CreateStep.tsx` (۲۷)، `pages/PatientsListPage.tsx` (۲۶).
### دستهٔ B — utility روشن بدون جفت `dark:`
| فایل | utility رنگی | تعداد `dark:` |
|---|---|---|
| `pages/RepresentationDetailPage.tsx` | ۳۳ | ۱ |
| `pages/AppointmentDetailPage.tsx` | ۲۷ | ۲ |
| `pages/PaymentDetailPage.tsx` | ۱۲ | ۲ |
| `components/ImageCropModal.tsx` | ۲۳ | ۹ (ناقص) |
### دستهٔ C — CSS ثابت در `styles.css` و آیکون‌های SVG
- `.inv-table thead tr { background: #e1e1e1 }` و `.inv-table th { color: #616161 }` و `.inv-table td { border-bottom: 1px solid #DBDBDB }` — هیچ override `.dark` ندارند (فقط `td` border و hover دارند)
- `.inv-tabs`, `.inv-tab`, `.inv-badge.*` با hex ثابت + override دستی `.dark` — به‌جای توکن
- آیکون‌های SVG با `stroke="#616161"` / `stroke="#3B3B3B"` ثابت (مثلاً `pages/MySecretariesPage.tsx` خط ۲۶–۳۵، `components/icons/`) در دارک‌مود روی پس‌زمینهٔ تیره محو می‌شوند
**هدف:** هر رنگ سطحی/متنی/حاشیه‌ای در پنل ادمین از توکن‌های `styles.css` بیاید، طوری که toggle دارک‌مود بدون هیچ نقطهٔ روشن/ناخوانا کار کند — و رگرسیون بعدی با تست گارد گرفته شود.
## معیار پذیرش
-**موفق:** با `darkMode: true` (کلید `clinicpro-ui` در localStorage یا دکمهٔ toggle در topbar)، در هر صفحهٔ فهرست «فایل‌های مرتبط» هیچ کارت/متن/حاشیه/badge با رنگ لایت باقی نمی‌ماند؛ کنتراست متن اصلی روی `--surface` حداقل ۴.۵:۱ و متن ثانویه حداقل ۳:۱.
-**موفق (لایت):** بعد از تغییرات، همان صفحات در لایت‌مود **دقیقاً** مثل قبل رندر می‌شوند — این ریفکتور نباید ظاهر لایت را عوض کند (توکن‌ها همان hexهای فعلی‌اند).
-**خطا / رگرسیون:** یک تست گارد (`assets/admin/test/theme-tokens.test.ts`) سورس را اسکن می‌کند و اگر در فایل‌های پاک‌شده دوباره hex ثابت رنگی یا utility رنگی بدون جفت `dark:` اضافه شود، fail می‌شود.
- ⚠️ **مرزی ۱:** تغییر brand hue (`setBrandHue`، مثلاً هیوی «نارنجی» که `fixed` دارد) در **هر دو** مود باید رنگ اصلی را عوض کند؛ هیچ جایی نباید `#5559CE` ثابت بماند.
- ⚠️ **مرزی ۲:** `density: compact` و مرورگر قدیمی بدون پشتیبانی `color-mix/oklch` (بلوک `@supports` در `styles.css`) — رنگ‌ها باید روی fallback sRGB هم درست باشند، نه اینکه به transparent بیفتند.
- ⚠️ **مرزی ۳:** جدول انبار (`.inv-table`) در دارک‌مود — thead، badgeها (`in-stock`/`low-stock`/`out-of-stock`) و tabها خوانا باشند.
## فایل‌های مرتبط
| فایل | نقش |
|---|---|
| `assets/admin/styles.css` | تعریف توکن‌ها (`:root`, `[data-theme="dark"]`, `@supports`) + کلاس‌های `.inv-*` که هنوز hex ثابت دارند |
| `assets/admin/stores/uiStore.ts` | `applyTheme()` / `applyBrand()` — منبع کلاس `dark` و `data-theme` و `--brand-h/--brand-c` |
| `assets/admin/components/session/PaymentStep.tsx` | بیشترین inline style رنگی + `dark:`های مرده |
| `assets/admin/components/session/DetailsStep.tsx` | همان الگو |
| `assets/admin/components/session/CreateStep.tsx` | همان الگو |
| `assets/admin/components/SessionPaymentAccordion.tsx`, `SessionServiceCard.tsx`, `PatientCaseBanner.tsx`, `AppointmentTurnCard.tsx` | `dark:`های مرده کنار inline style |
| `assets/admin/pages/MyPatientsPage.tsx`, `PatientDetailPage.tsx`, `SmsWalletPage.tsx`, `SubscriptionPage.tsx`, `ClinicServicesPage.tsx`, `PatientsListPage.tsx`, `MySecretariesPage.tsx` | حجم بالای inline style رنگی |
| `assets/admin/pages/RepresentationDetailPage.tsx`, `AppointmentDetailPage.tsx`, `PaymentDetailPage.tsx` | utility روشن بدون `dark:` |
| `assets/admin/components/ImageCropModal.tsx` | جفت `dark:` ناقص |
| `assets/admin/components/icons/` | SVGهای با stroke/fill ثابت |
| `docs/admin-ui/ui-design-spec.md` | سند UI — بخش ۱۶ «Dark Mode (اختیاری — فاز دوم)» کهنه است و با پیاده‌سازی فعلی نمی‌خواند |
## وضعیت فعلی
### الگوی مردهٔ `dark:` کنار inline style — `components/session/PaymentStep.tsx`
```tsx
// خط ۳۳–۳۵ — استایل‌های مشترک، همه hex ثابت
const fieldLabel: React.CSSProperties = { fontSize: 14, color: '#6B7280', marginBottom: 8, display: 'block' };
const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
// خط ۱۵۰ — کلاس dark: هیچ اثری ندارد چون inline style غالب است
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>{formatRial(finalPrice)}</span>
// خط ۱۵۵ — همان اشتباه روی border/background
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', borderRadius: 8, padding: 12, marginBottom: 16 }}>
```
### utility روشن بدون جفت dark — `pages/AppointmentDetailPage.tsx`
```tsx
// خط ۱۴۱–۱۴۲
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
```
### utility روشن بدون جفت dark — `pages/RepresentationDetailPage.tsx`
```tsx
// خط ۲۵۱
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
// خط ۳۰۹
<p className="text-sm text-gray-400 text-center py-6">اطلاعات بانکی ثبت نشده</p>
```
### CSS ثابت — `assets/admin/styles.css` خط ۹۵۸–۹۷۰
```css
.inv-table { width: 100%; border-collapse: collapse; }
.inv-table thead tr { background: #e1e1e1; } /* بدون override دارک */
.inv-table th {
color: #616161; font-size: 14px; font-weight: 400; /* بدون override دارک */
padding: 10px 18px; text-align: start; white-space: nowrap;
}
.inv-table td {
padding: 12px 18px; text-align: start; white-space: nowrap;
font-size: 16px; font-weight: 500; color: var(--text-2);
border-bottom: 1px solid #DBDBDB;
}
.dark .inv-table td { border-color: var(--border); }
.inv-table tbody tr:hover { background: #f4f5fd; }
.dark .inv-table tbody tr:hover { background: var(--surface-2); }
```
### الگوی درستِ موجود در پروژه (مرجع سبک) — `components/FreeVisitPrice.tsx`
```tsx
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 12px', lineHeight: 1.7 }}>
<span style={{ color: 'var(--danger)' }}> *</span>
```
این همان الگویی است که باید همه‌جا اعمال شود: inline style می‌ماند، فقط مقدار رنگ به توکن تبدیل می‌شود.
## وظایف
### ۱. تثبیت جدول نگاشت رنگ → توکن
قبل از هر تغییر، این جدول را مبنا بگیر. hexها از شمارش واقعی فایل‌های `.tsx` استخراج شده‌اند (ستون «تعداد» = تکرار در سورس):
| hex فعلی (لایت) | تعداد | hex فعلی (دارک، در `dark:`ها) | توکن مقصد |
|---|---|---|---|
| `#525252`, `#3b3b3b`, `#2f2f2f`, `#111827` | ۴۵+۱۳+۱۶+۱۱ | `#d7d8ed` (۷۶) | `var(--text)` |
| `#616161`, `#7e7e7e`, `#6b7280` | ۵۱+۱۹+۳۱ | `#a1a1a1` (۵۰) | `var(--text-2)` |
| `#9ca3af` | ۱۰ | — | `var(--text-3)` |
| `#5559ce` | ۷۵ | — | `var(--primary)` |
| `#f17732`, `#f0753b` | ۳۴+۴ | — | `var(--accent)` |
| `#ffffff` (سطح کارت) | ۴ | `#222433` (۲۵) | `var(--surface)` |
| `#fafafa`, `#f1f1f1` (پس‌زمینهٔ صفحه/فیلد) | ۶+۴ | — | `var(--bg)` / `var(--surface-2)` |
| `#efefef`, `#ededed`, `#e7e7e7` | ۱۸+۴+۴ | `#35343d` (۲۲), `#343645` (۱۱) | `var(--border)` |
| `#e0e0e0`, `#e1e1e1`, `#dbdbdb`, `#d7d7d7` | ۱۴+۸+… | `#404040` (۱۴) | `var(--border-2)` |
| `#ef4444`, `#d32f2f` | ۱۰+۹ | — | `var(--danger)` |
| `#2e7d32`, `#3c9a4f` | ۸+۷ | — | `var(--success)` |
| `#f59e0b` | ۵ | — | `var(--warning)` |
| پس‌زمینهٔ نرم بنفش `#f7f8ff`, `#f4f5fd`, `#e8ebff` | — | — | `var(--primary-soft)` / `var(--primary-soft2)` |
**نکته:** بعضی نگاشت‌ها دقیقاً یکسان نیستند (`#525252` در برابر `--text: #3b3b3b`). این اختلاف عمدی پذیرفته می‌شود چون هدف یکدست‌سازی است؛ اما اگر جایی اختلاف بصری محسوس شد (مثلاً متن اصلی کارت که واضحاً تیره‌تر/روشن‌تر می‌شود)، **همان‌جا را در گزارش پایانی لیست کن** به‌جای اینکه توکن جدید بسازی.
**نحوه تست:** جدول تصمیم است، نه کد. بعد از تعریف، در `docs/admin-ui/ui-design-spec.md` ثبتش کن (وظیفهٔ ۷).
### ۲. پاکسازی `dark:`های مرده (دستهٔ A، اولویت اول)
در ۱۰ فایلی که در بخش «مشکل» لیست شد: هر جا `className` شامل `dark:` کنار `style={{ color|background|border|borderColor: '#…' }}` است:
1. کلاس `dark:` مرده را **حذف** کن
2. مقدار hex داخل `style` را طبق جدول وظیفهٔ ۱ به `var(--token)` تبدیل کن
```tsx
// قبل
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>
// بعد
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>
```
```tsx
// قبل
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', ... }}>
// بعد
<div style={{ border: '1px solid var(--border)', background: 'var(--primary-soft)', ... }}>
```
ثابت‌های ماژول‌سطح (`fieldLabel`, `primaryBtn`, `ghostBtn` در `PaymentStep.tsx`) هم همین‌طور:
```tsx
const fieldLabel: React.CSSProperties = { fontSize: 14, color: 'var(--text-2)', marginBottom: 8, display: 'block' };
const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', ... };
const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', ... };
```
**نحوه تست:**
- `ddev exec npm run test` سبز بماند (تست‌های موجود `PaymentStep`/`SessionPaymentAccordion`/… نباید بشکنند)
- تأیید صفر شدن الگو:
`grep -rn "dark:" --include='*.tsx' assets/admin | grep -E "style=\{\{[^}]*(color|background)" | grep -v '\.test\.' | wc -l` → باید `0` شود
- بصری: `/admin` → جلسه (session) → مراحل ایجاد/جزئیات/پرداخت، یک‌بار لایت و یک‌بار دارک
### ۳. تبدیل بقیهٔ inline styleهای رنگی به توکن (دستهٔ A، ادامه)
فایل‌به‌فایل، به ترتیب حجم (MyPatientsPage → PatientDetailPage → SmsWalletPage → SubscriptionPage → ClinicServicesPage → PatientsListPage → MySecretariesPage → DoctorFormPage → DashboardPage → AdminSubscriptionPage → TurnsTimeline → …).
قواعد:
- رنگ ثابت داخل `style``var(--token)`
- کلاس‌های arbitrary دوتایی مثل `className="text-[#525252] dark:text-[#D7D8ED]"``className="text-[var(--text)]"` (یک کلاس، هر دو مود)
- `bg-[#FAFAFA] dark:bg-[#222433]``bg-[var(--surface)]` یا `bg-[var(--bg)]` بسته به اینکه سطح کارت است یا پس‌زمینهٔ فیلد
- استثنا: hex داخل SVG که با prop رنگ می‌گیرد (مثل `<PatientsGridView color={...} />`) — در وظیفهٔ ۶
- **هر فایل یک commit/گام مستقل**؛ فایل بعدی را قبل از تأیید بصری فایل قبلی شروع نکن
**نحوه تست:** بعد از هر فایل: `ddev exec npm run test`، سپس بازکردن صفحهٔ متناظر در `/admin` در هر دو مود و مقایسه با اسکرین‌شات قبلِ تغییر (لایت باید بدون تغییر باشد).
### ۴. اضافه کردن پوشش دارک به دستهٔ B
در `pages/RepresentationDetailPage.tsx`، `pages/AppointmentDetailPage.tsx`، `pages/PaymentDetailPage.tsx`، `components/ImageCropModal.tsx`:
utilityهای پالت ثابت را با کلاس توکنی جایگزین کن — نه اینکه جفت `dark:` اضافه کنی (جفت‌سازی بدهی را دو برابر می‌کند و با brand hue هم هماهنگ نمی‌شود):
```tsx
// قبل
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
// بعد
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">اطلاعات بیمار</h3>
```
```tsx
// قبل (RepresentationDetailPage خط ۲۵۱)
className="… border border-gray-300 text-gray-700 hover:bg-gray-50 …"
// بعد
className="… border border-[var(--border-2)] text-[var(--text)] hover:bg-[var(--surface-2)] …"
```
برای رنگ‌های وضعیتی (`text-red-600`, `border-red-300`, `bg-red-50`) → `text-[var(--danger)]`, `border-[var(--danger)]/30`, `bg-[var(--danger-bg)]`.
**نحوه تست:** `/admin/representations/{id}`، `/admin/appointments/{id}`، `/admin/payments/{id}` و مودال کراپ تصویر (آپلود آواتار پزشک) — هر کدام در دارک و لایت.
### ۵. پاکسازی `styles.css`
- `.inv-table thead tr``background: var(--surface-2)`
- `.inv-table th``color: var(--text-2)`
- `.inv-table td``border-bottom: 1px solid var(--border)` و حذف `.dark .inv-table td { border-color: … }` که دیگر لازم نیست
- `.inv-table tbody tr:hover``background: var(--surface-2)` و حذف override `.dark`
- `.inv-tabs``border-bottom: 1px solid var(--border)` و حذف `.dark .inv-tabs`
- `.inv-tab``color: var(--text-2)`؛ `.inv-tab.active``color: var(--primary); border-bottom: 3px solid var(--primary)`؛ حذف هر دو override `.dark`
- `.inv-badge.in-stock|low-stock|out-of-stock``background: var(--success-bg)|var(--warning-bg)|var(--danger-bg)` و `color: var(--success)|var(--warning)|var(--danger)`؛ حذف سه override `.dark`
`color: #fff` روی `.btn`/`.avatar` (خطوط ۲۶۳، ۴۱۱، ۵۹۸، ۶۷۱، ۷۷۵) → `var(--on-primary)` هر جا روی پس‌زمینهٔ برند است؛ `rgba(8,13,22,.5)` اسکریم مودال (خطوط ۶۹۶، ۷۹۸) عمداً در هر دو مود یکسان است و **دست نمی‌خورد** — در گزارش ذکرش کن.
**نحوه تست:** `/admin` → انبار (inventory): جدول، تب‌ها و badgeهای موجودی در هر دو مود؛ `ddev exec npm run build` بدون خطا.
### ۶. آیکون‌های SVG
آیکون‌های inline با `stroke="#616161"` / `#3B3B3B` / `fill="#7E7E7E"` (مثلاً `pages/MySecretariesPage.tsx` خطوط ۲۶–۳۵ و فایل‌های `components/icons/`) را به `stroke="currentColor"` تبدیل کن و رنگ را از کلاس والد بگیر (`text-[var(--text-2)]`).
استثنا: آیکون‌های چندرنگ برند (`stroke="#F17732"` در `components/icons/FilesServiceIcons.tsx`، ۲۲ مورد) — این‌ها هویت رنگی دارند؛ به `var(--accent)` تبدیل کن، نه `currentColor`.
```tsx
// قبل
<path d="…" stroke="#616161" strokeWidth="1.5" />
// بعد — رنگ از والد
<span className="text-[var(--text-2)]">
<path d="…" stroke="currentColor" strokeWidth="1.5" />
</span>
```
**نحوه تست:** صفحهٔ «منشی‌های من» و «سرویس‌ها» در دارک‌مود — آیکون‌ها باید دیده شوند نه اینکه در پس‌زمینه گم شوند.
### ۷. تست گارد + به‌روزرسانی سند
**۷.۱ تست گارد**`assets/admin/test/theme-tokens.test.ts` (Vitest، همان setup موجود در `assets/admin/test/`):
```ts
// اسکن سورس؛ فایل‌های پاک‌شده در فاز جاری در allowlist نیستند و باید تمیز بمانند.
const CLEANED = [ /* مسیر فایل‌هایی که در وظایف ۲–۴ پاک شدند */ ];
it('فایل‌های پاک‌شده hex ثابت رنگی ندارند', () => { /* regex #rrggbb داخل style/className، به‌جز SVG برند */ });
it('الگوی مردهٔ dark: کنار inline style رنگی وجود ندارد', () => { /* کل assets/admin */ });
```
تست دوم باید روی **کل** `assets/admin` اجرا شود (نه فقط allowlist) چون بعد از وظیفهٔ ۲ باید صفر باشد و هرگز برنگردد.
**۷.۲ سند**`docs/admin-ui/ui-design-spec.md`:
- بخش ۱۶ فعلی («Dark Mode (اختیاری — فاز دوم)» با `.dark { }` و پالت Tailwind) با پیاده‌سازی واقعی نمی‌خواند → بازنویسی شود: دارک‌مود پیاده‌شده است، از طریق `data-theme`/کلاس `dark` روی `<html>` توسط `uiStore.applyTheme()`، و منبع رنگ **فقط** توکن‌های `styles.css` است.
- جدول نگاشت وظیفهٔ ۱ + قاعدهٔ الزامی: «رنگ ثابت (hex یا utility پالت Tailwind) در `assets/admin` ممنوع؛ `var(--token)` یا `*-[var(--token)]`».
- بخش ۱ (Color Palette) که هنوز `--color-bg-sidebar: #0f172a` دارد با توکن‌های واقعی هم‌خوان شود.
**نحوه تست:** `ddev exec npm run test` — گارد جدید سبز؛ برای اطمینان از اینکه واقعاً گارد است، یک‌بار موقتاً `color: '#525252'` به یکی از فایل‌های allowlist اضافه کن، ببین تست قرمز می‌شود، بعد برگردان.
### ۸. (فاز ۳ — فقط با تأیید صریح کاربر) مهاجرت سه فایل بزرگ
`pages/DoctorDetailPage.tsx` (۳۱۳ utility رنگی / ۱۴۲ `dark:``components/schedule/ScheduleSection.tsx` (۲۶۰ / ۱۲۰)، `pages/UserDetailPage.tsx` (۱۹۸ / ۹۱).
این‌ها **در دارک‌مود کار می‌کنند** (جفت `slate/gray` + `dark:` دارند) ولی از design-system منحرف‌اند: با تغییر brand hue هماهنگ نمی‌شوند و رنگ خاکستری‌شان با `--surface`/`--text` مو نمی‌زند. مهاجرت ۷۷۱ نقطه ریسک رگرسیون بصری بالایی دارد و سود عملکردی فوری ندارد.
**توصیه:** این وظیفه را در همین پاس انجام نده. اگر کاربر تأیید کرد، فایل‌به‌فایل و با اسکرین‌شات قبل/بعد در هر دو مود.
## نکات مهم
- **علت ریشه‌ای، نه علامت:** الگوی `className="dark:…"` کنار `style={{color:…}}` نشان می‌دهد قبلاً تلاش شده دارک‌مود «وصله» شود بدون اینکه specificity در نظر گرفته شود. راه‌حل درست حذف رنگ ثابت است، نه اضافه کردن `!important` یا جفت `dark:` بیشتر. اگر جایی واقعاً به override نیاز شد، اول ببین آیا می‌شود کلاً از `style` به `className` رفت.
- **ترتیب اجرا اجباری است:** وظیفهٔ ۲ (خرابی قطعی، ۹۰ نقطه) قبل از وظیفهٔ ۳ (حجیم). اگر بودجه/زمان تمام شد، وظایف ۲، ۴، ۵ حداقل قابل تحویل مستقل‌اند.
- **لایت‌مود نباید تغییر کند.** توکن‌های لایت همان hexهای رایج‌اند؛ هر تغییر بصری محسوس در لایت یعنی نگاشت اشتباه انتخاب شده — گزارش کن، خودسرانه توکن جدید نساز.
- **توکن جدید فقط با دلیل.** اگر جایی هیچ توکن مناسبی نبود، اول بررسی کن آیا واقعاً رنگ جدید لازم است یا نزدیک‌ترین توکن کافی است (guidelines §۵ — abstraction بی‌مصرف ممنوع). توکن جدید اگر اضافه شد باید در **هر سه** بلوک `:root`، `[data-theme="dark"]` و در صورت لزوم `@supports` تعریف شود.
- **سازگاری مرورگر قدیمی:** بلوک `@supports (color: color-mix(in oklch, red, blue))` عمداً fallback sRGB دارد (iPhone 8 / WebKit قدیمی). هیچ توکنی نباید **فقط** داخل `@supports` تعریف شود.
- **`brandHue.fixed`:** هیوی «نارنجی» در `uiStore.ts` مقادیر `--primary*` را inline روی root می‌گذارد. یعنی هر جا `#5559CE` ثابت مانده باشد با انتخاب این هیو ناهماهنگ می‌شود — این یک تست بصری مستقل است، نه فقط دارک/لایت.
- **بدون تغییر backend/API.** این تسک صفر تغییر در `src/` و `docs/api/` دارد؛ تنها سند متأثر `docs/admin-ui/ui-design-spec.md` است.
- **دستورها داخل ddev اجرا می‌شوند:** `ddev exec npm run test`، `ddev exec npm run build`. برای مشاهدهٔ زنده `ddev exec npm run watch`.
- **حساب تست:** `09390039833 / 09390039833` روی `https://clinic-pro.ddev.site/admin`.