refactor: update UserDetailPage styles and introduce avatar gradient utility

- Refactored role metadata styles in UserDetailPage for consistency with design tokens.
- Replaced hardcoded avatar colors with a utility function to generate gradients based on user ID.
- Improved InfoCard component styles for better hover effects and accessibility.
- Removed deprecated color classes and adjusted background gradients for various components.
- Updated theme token tests to reflect the removal of deferred files and ensure compliance with design standards.
- Added new avatarColors utility file to manage avatar gradient definitions.
This commit is contained in:
hamed
2026-07-27 16:58:03 +03:30
parent 55ab2f5dfc
commit bdc00fe267
6 changed files with 153 additions and 147 deletions
@@ -169,7 +169,7 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
<div>
<button ref={btnRef} type="button" onClick={openPicker}
className="cp-input h-11 flex items-center justify-between w-full cursor-pointer">
<span className={value ? 'text-[var(--text)] dark:text-[var(--text)]' : 'text-[var(--text-3)] dark:text-[var(--text-2)]'}>
<span className={value ? 'text-[var(--text)]' : 'text-[var(--text-3)]'}>
{value ? formatPersianDate(value) : (placeholder ?? 'انتخاب تاریخ')}
</span>
<CalendarIcon className="w-4 h-4 text-[var(--text-3)] shrink-0 mr-2" />
@@ -205,10 +205,10 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
return (
<button key={jd} type="button" disabled={dis} onClick={() => selectDay(jd)}
className={`w-full aspect-square flex items-center justify-center rounded-lg text-sm transition-colors
${dis ? 'text-[var(--text-3)] dark:text-[var(--text-2)] cursor-not-allowed'
: sel ? 'bg-primary-500 text-[var(--on-primary)] font-semibold shadow-sm'
: tod ? 'ring-1 ring-primary-400 text-primary-700 dark:text-primary-300 font-medium'
: 'text-[var(--text)] dark:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]'}`}>
${dis ? 'text-[var(--text-3)] cursor-not-allowed'
: sel ? 'bg-[var(--primary)] text-[var(--on-primary)] font-semibold shadow-sm'
: tod ? 'ring-1 ring-[var(--primary)] text-[var(--primary-700)] dark:text-[var(--primary)] font-medium'
: 'text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]'}`}>
{toPersianNums(jd)}
</button>
);
@@ -352,7 +352,7 @@ function SlotEditor({ slots, onChange }: {
</div>
))}
<button type="button" onClick={add}
className="flex items-center gap-1.5 text-xs text-primary-600 dark:text-primary-400 hover:underline mt-1">
className="flex items-center gap-1.5 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline mt-1">
<PlusIcon className="w-3.5 h-3.5" />افزودن بازه زمانی
</button>
</div>
@@ -511,7 +511,7 @@ function NoLocationsNotice({ clinicUuid }: { clinicUuid?: string | null }) {
: ['ابتدا آدرس مطب را ثبت کنید', 'برنامه کاری نیاز به حداقل یک مکان نوبت دارد'];
return (
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
<div className="w-12 h-12 rounded-full bg-[var(--warning-bg)] dark:bg-amber-500/10 border border-[var(--warning)] dark:border-amber-500/30 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-[var(--warning-bg)] border border-[var(--warning)] flex items-center justify-center">
<MapPinIcon className="w-6 h-6 text-[var(--warning)]" />
</div>
<div>
@@ -627,9 +627,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
return (
<div className="space-y-2">
{totalSlots > 0 && (
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
<span className="text-sm text-emerald-700 dark:text-emerald-300">
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] border border-[var(--success)]">
<CalendarIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
<span className="text-sm text-[var(--success)]">
مجموع <strong>{totalSlots}</strong> نوبت در هفته
</span>
</div>
@@ -638,7 +638,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
const activeSessions = (scheduleMap[day.key]?.sessions ?? []).filter(s => s.active);
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
return (
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/40">
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)]">
<b style={{ fontSize: 14, minWidth: 56, flexShrink: 0 }}>{day.label}</b>
<div className="flex-1 flex gap-2 items-center flex-wrap min-w-0">
{activeSessions.length > 0 ? activeSessions.map((s, i) => (
@@ -665,9 +665,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
<div className="space-y-2">
{/* ─ خلاصه کل هفته */}
{totalSlots > 0 && (
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
<span className="text-sm text-emerald-700 dark:text-emerald-300">
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] border border-[var(--success)]">
<CalendarIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
<span className="text-sm text-[var(--success)]">
مجموع <strong>{totalSlots}</strong> نوبت در هفته
</span>
</div>
@@ -675,7 +675,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
{/* ─ روش نوبت‌دهی */}
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
<div className="px-4 py-3 bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
<div className="px-4 py-3 bg-[var(--surface-2)]">
<span className="text-sm font-medium text-[var(--text)]">روش نوبتدهی</span>
</div>
<div className="px-4 py-3 space-y-3">
@@ -694,11 +694,11 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
className={`text-right p-3 rounded-lg border transition-colors ${
selected
? 'border-[var(--primary)] bg-[var(--primary)]/5'
: 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--bg)]'
: 'border-[var(--border)] bg-[var(--surface)]'
} ${modeLocked ? 'opacity-70 cursor-not-allowed' : 'hover:border-[var(--primary)]'}`}
>
<div className="flex items-center gap-2 mb-1">
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-[var(--border-2)] dark:border-gray-600'}`} />
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-[var(--border-2)]'}`} />
<span className="text-sm font-medium text-[var(--text)]">{lbl}</span>
</div>
<p className="text-xs text-[var(--text-2)] leading-relaxed">{desc}</p>
@@ -712,7 +712,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
نوع نوبتدهی ثبت شده و دیگر قابل تغییر نیست.
</p>
) : (
<p className="text-xs text-[var(--warning)] dark:text-amber-400 leading-relaxed">
<p className="text-xs text-[var(--warning)] leading-relaxed">
توجه: نوع نوبتدهی پس از اولین ثبت <span className="font-medium">بههیچعنوان قابل تغییر نیست</span>. پیش از ذخیره با دقت انتخاب کنید.
</p>
)}
@@ -745,7 +745,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
{/* ─ نوبت‌دهی آنلاین */}
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
{/* header + toggle */}
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-[var(--surface-2)]">
<div className="flex items-center gap-2">
<GlobeAltIcon className="w-4 h-4 text-[var(--text-2)] shrink-0" />
<span className="text-sm font-medium text-[var(--text)]">نوبتدهی آنلاین</span>
@@ -757,7 +757,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
checked={meta.online_booking_enabled}
onChange={(e) => setMeta(m => ({ ...m, online_booking_enabled: e.target.checked }))}
/>
<span className="w-10 h-6 rounded-full bg-slate-300 dark:bg-gray-600 transition-colors peer-checked:bg-emerald-500" />
<span className="w-10 h-6 rounded-full bg-[var(--surface-3)] transition-colors peer-checked:bg-[var(--success)]" />
<span className="absolute top-0.5 right-0.5 w-5 h-5 rounded-full bg-[var(--surface)] shadow transition-transform peer-checked:-translate-x-4" />
</span>
</label>
@@ -864,7 +864,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
<div className="flex items-center justify-between pt-3">
{(hasAnyOverlap || missingLocation) && (
<p className="text-xs text-[var(--danger)] dark:text-red-400 flex items-center gap-1">
<p className="text-xs text-[var(--danger)] flex items-center gap-1">
<ExclamationTriangleIcon className="w-3.5 h-3.5" />
{hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازه‌ها الزامی است'}
</p>
@@ -966,7 +966,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
<div className="grid grid-cols-2 gap-2">
<button type="button"
onClick={() => setOverrideType('closed')}
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-red-400 bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 text-[var(--danger)] dark:text-red-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-[var(--danger)] bg-[var(--danger-bg)] text-[var(--danger)]' : 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)]'}`}>
<XCircleIcon className="w-5 h-5 shrink-0" />
<div>
<p className="font-medium leading-tight">تعطیل است</p>
@@ -975,7 +975,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
</button>
<button type="button"
onClick={() => setOverrideType('custom')}
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-amber-400 bg-[var(--warning-bg)] dark:bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]' : 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)]'}`}>
<CalendarIcon className="w-5 h-5 shrink-0" />
<div>
<p className="font-medium leading-tight">ساعات خاص</p>
@@ -1020,9 +1020,9 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
)}
{overrideType === 'closed' && (
<div className="flex items-start gap-2 p-3 rounded-xl bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 border border-[var(--danger)] dark:border-red-500/30">
<div className="flex items-start gap-2 p-3 rounded-xl bg-[var(--danger-bg)] border border-[var(--danger)]">
<ExclamationTriangleIcon className="w-4 h-4 text-[var(--danger)] shrink-0 mt-0.5" />
<p className="text-xs text-[var(--danger)] dark:text-red-400">
<p className="text-xs text-[var(--danger)]">
در این روز هیچ نوبتی داده نخواهد شد، حتی اگر در برنامه هفتگی ساعات کاری داشته باشید.
</p>
</div>
@@ -1079,16 +1079,16 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
) : (
<div className="space-y-2">
{overrides.map(ov => (
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/60">
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-[var(--border)] bg-[var(--surface)]">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-[var(--text)]">{formatPersianDate(tsToDate(ov.date))}</span>
{ov.active ? (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--warning-bg)] dark:bg-amber-500/20 text-amber-700 dark:text-amber-300">
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--warning-bg)] text-[var(--warning)]">
{ov.custom_slots.length > 0 ? `${ov.custom_slots.length} بازه سفارشی` : 'ساعات خاص'}
</span>
) : (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400">
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] text-[var(--danger)]">
تعطیل
</span>
)}
@@ -1098,7 +1098,7 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
{!readOnly && (
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => { setEditing(ov); setModalOpen(true); }}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
<PencilIcon className="w-3.5 h-3.5" />
</button>
<button onClick={() => setDeletingUuid(ov.uuid)}
@@ -1250,17 +1250,17 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
const isCurrent = h.start_date <= now && h.end_date >= now;
const sameDay = tsToDate(h.start_date) === tsToDate(h.end_date);
return (
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-[var(--danger)] dark:border-red-500/30 bg-[var(--danger-bg)]/40 dark:bg-[var(--danger)]/5' : isPast ? 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface-2)]/40 dark:bg-[var(--surface)]/30 opacity-70' : 'border-orange-200 dark:border-orange-500/30 bg-orange-50/30 dark:bg-orange-500/5'}`}>
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-[var(--danger)] bg-[var(--danger-bg)]/40' : isPast ? 'border-[var(--border)] bg-[var(--surface-2)]/40 opacity-70' : 'border-[var(--accent)] bg-[var(--accent-bg)]/30'}`}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-sm font-medium ${isPast ? 'text-[var(--text-2)] dark:text-[var(--text-3)]' : 'text-[var(--text)] dark:text-[var(--text)]'}`}>
<span className={`text-sm font-medium ${isPast ? 'text-[var(--text-2)]' : 'text-[var(--text)]'}`}>
{sameDay ? formatPersianDate(tsToDate(h.start_date)) : `${formatPersianDate(tsToDate(h.start_date))} تا ${formatPersianDate(tsToDate(h.end_date))}`}
</span>
{isCurrent && h.active && (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400 animate-pulse">در جریان</span>
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] text-[var(--danger)] animate-pulse">در جریان</span>
)}
{!isCurrent && !isPast && h.active && (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400">آینده</span>
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--accent-bg)] text-[var(--accent)]">آینده</span>
)}
{isPast && (
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-3)]">گذشته</span>
@@ -1274,11 +1274,11 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
{!readOnly && (
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => toggleMut.mutate(h)} disabled={toggleMut.isPending}
className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-[var(--border)] dark:border-gray-600 text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)] hover:text-[var(--text)] dark:hover:text-[var(--text)]' : 'border-emerald-200 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 hover:bg-[var(--success-bg)] dark:hover:bg-emerald-500/10'}`}>
className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)] hover:text-[var(--text)]' : 'border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] dark:hover:bg-[var(--success)]/10'}`}>
{h.active ? 'غیرفعال' : 'فعال'}
</button>
<button onClick={() => { setEditing(h); setModalOpen(true); }}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
<PencilIcon className="w-3.5 h-3.5" />
</button>
<button onClick={() => setDeletingUuid(h.uuid)}
@@ -1329,8 +1329,8 @@ export function ScheduleSection({ doctorUuid, clinicUuid, readOnly = false }: {
<button key={t.id} type="button" onClick={() => setTab(t.id)}
className={`flex-1 text-xs sm:text-sm font-medium px-2 py-1.5 rounded-lg transition-colors ${
tab === t.id
? 'bg-[var(--surface)] dark:bg-[var(--surface-2)] text-[var(--text)] dark:text-[var(--text)] shadow-sm'
: 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:text-[var(--text)] dark:hover:text-[var(--text)]'
? 'bg-[var(--surface)] text-[var(--text)] shadow-sm'
: 'text-[var(--text-2)] hover:text-[var(--text)]'
}`}>
{t.label}
</button>