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:
hamed
2026-07-27 16:41:52 +03:30
parent c29035c21e
commit 55ab2f5dfc
73 changed files with 1485 additions and 1110 deletions
@@ -130,7 +130,7 @@ export default function ServiceSlotPicker({
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
background: active ? 'var(--primary)' : 'transparent',
}}>
{active && <span style={{ width: 8, height: 8, background: '#fff', borderRadius: 2 }} />}
{active && <span style={{ width: 8, height: 8, background: 'var(--surface)', borderRadius: 2 }} />}
</span>
{s.name}
</span>
@@ -182,7 +182,7 @@ export default function ServiceSlotPicker({
<button type="button" aria-label={`حذف ${s.name}`} onClick={() => remove(s.uuid)}
style={{
display: 'grid', placeItems: 'center', width: 18, height: 18, borderRadius: 999, flexShrink: 0,
border: 'none', cursor: 'pointer', background: 'var(--primary)', color: '#fff',
border: 'none', cursor: 'pointer', background: 'var(--primary)', color: 'var(--on-primary)',
fontSize: 13, lineHeight: 1, fontFamily: 'inherit',
}}>×</button>
</div>
@@ -211,7 +211,7 @@ export default function ServiceSlotPicker({
style={{
fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontFamily: 'inherit',
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
background: active ? 'var(--primary)' : 'var(--surface)', color: active ? '#fff' : 'var(--text)',
background: active ? 'var(--primary)' : 'var(--surface)', color: active ? 'var(--on-primary)' : 'var(--text)',
}}>
{s.start_time}
</button>
@@ -32,10 +32,10 @@ function BadgeIcon({ Icon, color, bg }: { Icon: React.ElementType; color: string
export default function TurnsStatInfo({ stats }: { stats: TurnsStats }) {
const items: StatDef[] = [
{ title: 'کل نوبت های امروز', value: stats.total, Icon: UsersIcon, iconColor: '#494cb3', iconBg: '#e9ebfb' },
{ title: 'نوبت های انجام شده', value: stats.completed, Icon: UserPlusIcon, iconColor: '#0d9f43', iconBg: '#e7f6ed' },
{ title: 'بیماران در انتظار', value: stats.waiting, Icon: ClockIcon, iconColor: '#e0a838', iconBg: '#fff2dc' },
{ title: 'نوبت های لغو شده', value: stats.cancelled, Icon: XCircleIcon, iconColor: '#ee5c6d', iconBg: '#fad6da' },
{ title: 'کل نوبت های امروز', value: stats.total, Icon: UsersIcon, iconColor: 'var(--primary-600)', iconBg: 'var(--primary-soft)' },
{ title: 'نوبت های انجام شده', value: stats.completed, Icon: UserPlusIcon, iconColor: 'var(--success)', iconBg: 'var(--success-bg)' },
{ title: 'بیماران در انتظار', value: stats.waiting, Icon: ClockIcon, iconColor: 'var(--warning)', iconBg: 'var(--warning-bg)' },
{ title: 'نوبت های لغو شده', value: stats.cancelled, Icon: XCircleIcon, iconColor: 'var(--danger)', iconBg: 'var(--danger-bg)' },
];
return (
@@ -77,7 +77,7 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
cursor: isPast ? 'not-allowed' : 'pointer', opacity: isPast ? 0.7 : 1, minHeight: 76,
}}
>
<InnerTimes start={slot.start_time} end={slot.end_time} color={isPast ? '#9E9E9E' : cfg.dotColor} />
<InnerTimes start={slot.start_time} end={slot.end_time} color={isPast ? 'var(--text-3)' : cfg.dotColor} />
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
<span style={{ fontSize: 13.5, color: isPast ? 'var(--text-3)' : cfg.textColor, fontWeight: 500 }}>
{isPast ? 'گذشته' : 'افزودن نوبت سریع'}
@@ -88,8 +88,8 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
{!isPast && (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 99,
fontSize: 12, fontWeight: 700, color: cfg.textColor, background: `${cfg.dotColor}15`,
border: `1.5px solid ${cfg.dotColor}30`, whiteSpace: 'nowrap', alignSelf: 'flex-start',
fontSize: 12, fontWeight: 700, color: cfg.textColor, background: `color-mix(in srgb, ${cfg.dotColor} 8%, transparent)`,
border: `1.5px solid color-mix(in srgb, ${cfg.dotColor} 19%, transparent)`, whiteSpace: 'nowrap', alignSelf: 'flex-start',
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: cfg.dotColor }} />
نوبت جدید
@@ -6,85 +6,33 @@
export interface TurnStatusConfig {
label: string;
bgColor: string;
darkBgColor: string;
borderColor: string;
darkBorderColor: string;
dotColor: string;
textColor: string;
darkTextColor: string;
}
// وضعیت‌های بک‌اند → رنگ طرح tauri.
/** یک وضعیت = یک توکن؛ خود توکن با تم عوض می‌شود، پس جفت light/dark لازم نیست. */
const of = (fg: string, bg: string): Omit<TurnStatusConfig, 'label'> => ({
bgColor: `var(--${bg})`, borderColor: `var(--${fg})`,
dotColor: `var(--${fg})`, textColor: `var(--${fg})`,
});
// وضعیت‌های بک‌اند → توکن‌های تم (هیوی طرح tauri حفظ شده است).
const STATUS: Record<string, TurnStatusConfig> = {
// pending = «ثبت شده» (tauri: registered)
pending: {
label: 'ثبت شده',
bgColor: '#E3F2FD', darkBgColor: 'rgba(85, 89, 206, 0.18)',
borderColor: '#2196F3', darkBorderColor: 'rgba(199, 206, 244, 0.42)',
dotColor: '#2196F3', textColor: '#2196F3', darkTextColor: '#C7CEF4',
},
// confirmed = «قطعی شده» (tauri: finalized)
confirmed: {
label: 'قطعی شده',
bgColor: '#E0F2F1', darkBgColor: 'rgba(25, 191, 211, 0.14)',
borderColor: '#009688', darkBorderColor: 'rgba(25, 191, 211, 0.44)',
dotColor: '#009688', textColor: '#009688', darkTextColor: '#19BFD3',
},
// following_up = «در حال پیگیری» (tauri: in_progress)
following_up: {
label: 'در حال پیگیری',
bgColor: '#FFF3E0', darkBgColor: 'rgba(241, 119, 50, 0.16)',
borderColor: '#FF9800', darkBorderColor: 'rgba(241, 119, 50, 0.46)',
dotColor: '#FF9800', textColor: '#d87f00', darkTextColor: '#F17732',
},
// salon = «سالن»
salon: {
label: 'سالن',
bgColor: '#F3E5F5', darkBgColor: 'rgba(199, 206, 244, 0.16)',
borderColor: '#9C27B0', darkBorderColor: 'rgba(199, 206, 244, 0.42)',
dotColor: '#9C27B0', textColor: '#9C27B0', darkTextColor: '#C7CEF4',
},
// completed = «ویزیت شده» (tauri: success)
completed: {
label: 'ویزیت شده',
bgColor: '#E8F5E9', darkBgColor: 'rgba(34, 197, 94, 0.14)',
borderColor: '#4CAF50', darkBorderColor: 'rgba(34, 197, 94, 0.42)',
dotColor: '#4CAF50', textColor: '#0d9f43', darkTextColor: '#4ADE80',
},
// cancel variants = «لغو شده» (tauri: failed)
cancelled_by_doctor: {
label: 'لغو شده',
bgColor: '#fde7e9', darkBgColor: 'rgba(255, 84, 80, 0.16)',
borderColor: '#ee5c6d', darkBorderColor: 'rgba(255, 84, 80, 0.44)',
dotColor: '#ee5c6d', textColor: '#ee5c6d', darkTextColor: '#FF7A76',
},
cancelled_by_user: {
label: 'لغو توسط بیمار',
bgColor: '#fde7e9', darkBgColor: 'rgba(255, 84, 80, 0.16)',
borderColor: '#ee5c6d', darkBorderColor: 'rgba(255, 84, 80, 0.44)',
dotColor: '#ee5c6d', textColor: '#ee5c6d', darkTextColor: '#FF7A76',
},
// no_show / expired = خنثی (tauri: pending amber-ish → اینجا خاکستری)
no_show: {
label: 'غیبت',
bgColor: '#f4f4f5', darkBgColor: 'rgba(160, 160, 160, 0.14)',
borderColor: '#9E9E9E', darkBorderColor: 'rgba(160, 160, 160, 0.4)',
dotColor: '#9E9E9E', textColor: '#757575', darkTextColor: '#A1A1A1',
},
expired: {
label: 'منقضی',
bgColor: '#f4f4f5', darkBgColor: 'rgba(160, 160, 160, 0.14)',
borderColor: '#9E9E9E', darkBorderColor: 'rgba(160, 160, 160, 0.4)',
dotColor: '#9E9E9E', textColor: '#757575', darkTextColor: '#A1A1A1',
},
pending: { label: 'ثبت شده', ...of('info', 'info-bg') },
confirmed: { label: 'قطعی شده', ...of('primary', 'primary-soft') },
following_up: { label: 'در حال پیگیری', ...of('accent', 'accent-bg') },
salon: { label: 'سالن', ...of('violet', 'violet-bg') },
completed: { label: 'ویزیت شده', ...of('success', 'success-bg') },
cancelled_by_doctor: { label: 'لغو شده', ...of('danger', 'danger-bg') },
cancelled_by_user: { label: 'لغو توسط بیمار', ...of('danger', 'danger-bg') },
no_show: { label: 'غیبت', ...of('text-3', 'surface-2') },
expired: { label: 'منقضی', ...of('text-3', 'surface-2') },
};
// اسلات خالی (نوبت جدید) — tauri: empty.
export const EMPTY_SLOT_CONFIG: TurnStatusConfig = {
label: 'نوبت جدید',
bgColor: '#E3F2FD', darkBgColor: 'rgba(85, 89, 206, 0.16)',
borderColor: '#2196F3', darkBorderColor: 'rgba(199, 206, 244, 0.38)',
dotColor: '#2196F3', textColor: '#5559ce', darkTextColor: '#C7CEF4',
label: 'نوبت جدید', ...of('primary', 'primary-soft'),
};
export function turnStatusConfig(status: string): TurnStatusConfig {