feat: port tauri header top-menu to admin topbar
Rebuild admin Topbar to match clinic-pro-tauri header: - Icon order: theme -> settings -> bell (was theme -> bell -> settings) - Settings gear now navigates to the role-based settings page instead of opening the personalization panel - Search placeholder changed to «جستجو», dropped the ⌘K hint - Add ProfileMenu: avatar + user name + context/role subtitle + caret, with a dropdown (profile / payments / settings / personalization / logout). Links are role-aware; personalization keeps the existing settings panel. Frontend-only, no API changes. Adds ProfileMenu.test + Topbar.test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { renderWithProviders } from '../../test/utils';
|
||||||
|
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||||
|
return { ...actual, useNavigate: () => navigateMock };
|
||||||
|
});
|
||||||
|
|
||||||
|
import ProfileMenu, { settingsPath } from './ProfileMenu';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import { useUiStore } from '../../stores/uiStore';
|
||||||
|
|
||||||
|
const logoutMock = vi.fn();
|
||||||
|
const togglePanelMock = vi.fn();
|
||||||
|
|
||||||
|
function setAuth(partial: Record<string, unknown>) {
|
||||||
|
useAuthStore.setState({
|
||||||
|
userName: 'دکتر رضایی',
|
||||||
|
primaryRole: 'doctor',
|
||||||
|
context: { name: 'کلینیک نور' },
|
||||||
|
logout: logoutMock,
|
||||||
|
...partial,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
navigateMock.mockClear();
|
||||||
|
logoutMock.mockClear();
|
||||||
|
togglePanelMock.mockClear();
|
||||||
|
useUiStore.setState({ toggleSettingsPanel: togglePanelMock } as any);
|
||||||
|
setAuth({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProfileMenu — نمایش نام و تخصص', () => {
|
||||||
|
it('نام کاربر و نام محیط کاری (context) را نشان میدهد', () => {
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
expect(screen.getByText('دکتر رضایی')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('کلینیک نور')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('وقتی context نیست، برچسب نقش را جایگزین میکند', () => {
|
||||||
|
setAuth({ context: null });
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
expect(screen.getByText('پزشک')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('حالت خالی: نام پیشفرض «کاربر» و حرف اول «ک»', () => {
|
||||||
|
setAuth({ userName: null, context: null, primaryRole: null });
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
expect(screen.getByText('کاربر')).toBeInTheDocument();
|
||||||
|
// حرف اولِ آواتار
|
||||||
|
expect(screen.getByTitle('کاربر').textContent).toContain('ک');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProfileMenu — باز/بستن و آیتمها', () => {
|
||||||
|
it('منو ابتدا بسته است و با کلیک باز میشود', () => {
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
expect(screen.queryByText('خروج')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle('دکتر رضایی'));
|
||||||
|
|
||||||
|
['پروفایل من', 'پرداختها', 'تنظیمات', 'شخصیسازی', 'خروج'].forEach((label) => {
|
||||||
|
expect(screen.getByText(label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProfileMenu — لینکهای نقشمحور', () => {
|
||||||
|
it('پزشک: پروفایل → /admin/profile، پرداختها → /admin/my-payments', () => {
|
||||||
|
setAuth({ primaryRole: 'doctor' });
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
fireEvent.click(screen.getByTitle('دکتر رضایی'));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('پرداختها'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/my-payments');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle('دکتر رضایی'));
|
||||||
|
fireEvent.click(screen.getByText('پروفایل من'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('مدیر: پرداختها → /admin/payments، تنظیمات → /admin/settings', () => {
|
||||||
|
setAuth({ primaryRole: 'admin', userName: 'ادمین' });
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
fireEvent.click(screen.getByTitle('ادمین'));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('پرداختها'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/payments');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle('ادمین'));
|
||||||
|
fireEvent.click(screen.getByText('تنظیمات'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/settings');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProfileMenu — اکشنها', () => {
|
||||||
|
it('شخصیسازی پنل تنظیمات را باز میکند', () => {
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
fireEvent.click(screen.getByTitle('دکتر رضایی'));
|
||||||
|
fireEvent.click(screen.getByText('شخصیسازی'));
|
||||||
|
expect(togglePanelMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('خروج، logout را صدا میزند و به صفحه ورود میرود', () => {
|
||||||
|
renderWithProviders(<ProfileMenu />);
|
||||||
|
fireEvent.click(screen.getByTitle('دکتر رضایی'));
|
||||||
|
fireEvent.click(screen.getByText('خروج'));
|
||||||
|
expect(logoutMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/login');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('settingsPath — نگاشت نقش', () => {
|
||||||
|
it('نقشها را به مسیر درست نگاشت میکند', () => {
|
||||||
|
expect(settingsPath('admin')).toBe('/admin/settings');
|
||||||
|
expect(settingsPath('doctor')).toBe('/admin/settings-menu');
|
||||||
|
expect(settingsPath('clinic')).toBe('/admin/settings-menu');
|
||||||
|
expect(settingsPath('secretary')).toBe('/admin/account-settings');
|
||||||
|
expect(settingsPath(null)).toBe('/admin/account-settings');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
UserCircleIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
SwatchIcon,
|
||||||
|
ArrowLeftOnRectangleIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
import { useUiStore } from '../../stores/uiStore';
|
||||||
|
|
||||||
|
type Role = string | null;
|
||||||
|
|
||||||
|
// نگاشت نقش → برچسب فارسی (همتراز با Sidebar).
|
||||||
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
|
admin: 'مدیر کل',
|
||||||
|
clinic: 'مالک کلینیک',
|
||||||
|
doctor: 'پزشک',
|
||||||
|
secretary: 'منشی',
|
||||||
|
representation: 'نماینده',
|
||||||
|
user: 'کاربر',
|
||||||
|
};
|
||||||
|
|
||||||
|
const HUES = [256, 205, 162, 295, 272];
|
||||||
|
function avatarBg(name: string): string {
|
||||||
|
const hue = HUES[(name.charCodeAt(0) ?? 0) % HUES.length];
|
||||||
|
return `linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// مسیرهای نقشمحور — معادلِ لینکهای MenuProfile در clinic-pro-tauri.
|
||||||
|
function profilePath(role: Role): string {
|
||||||
|
if (role === 'doctor') return '/admin/profile';
|
||||||
|
if (role === 'representation') return '/admin/representation-profile';
|
||||||
|
return '/admin/account-settings';
|
||||||
|
}
|
||||||
|
function paymentsPath(role: Role): string {
|
||||||
|
return role === 'admin' ? '/admin/payments' : '/admin/my-payments';
|
||||||
|
}
|
||||||
|
export function settingsPath(role: Role): string {
|
||||||
|
if (role === 'admin') return '/admin/settings';
|
||||||
|
if (role === 'doctor' || role === 'clinic') return '/admin/settings-menu';
|
||||||
|
return '/admin/account-settings';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileMenu() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const userName = useAuthStore((s) => s.userName);
|
||||||
|
const primaryRole = useAuthStore((s) => s.primaryRole);
|
||||||
|
const context = useAuthStore((s) => s.context);
|
||||||
|
const logout = useAuthStore((s) => s.logout);
|
||||||
|
const toggleSettingsPanel = useUiStore((s) => s.toggleSettingsPanel);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null);
|
||||||
|
const btnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const name = userName ?? 'کاربر';
|
||||||
|
const initial = name.charAt(0).toUpperCase();
|
||||||
|
// زیرعنوان: نام محیط کاری (context) وگرنه برچسب نقش — معادلِ «expertise» در tauri.
|
||||||
|
const subtitle = context?.name ?? ROLE_LABELS[primaryRole ?? ''] ?? primaryRole ?? '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
function handler(e: MouseEvent) {
|
||||||
|
const target = e.target as Node;
|
||||||
|
if (!btnRef.current?.contains(target) && !menuRef.current?.contains(target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handler);
|
||||||
|
return () => document.removeEventListener('mousedown', handler);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
function reposition() {
|
||||||
|
if (btnRef.current) {
|
||||||
|
const rect = btnRef.current.getBoundingClientRect();
|
||||||
|
setMenuPos({ top: rect.bottom + 6, right: window.innerWidth - rect.right });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', reposition, true);
|
||||||
|
window.addEventListener('resize', reposition);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', reposition, true);
|
||||||
|
window.removeEventListener('resize', reposition);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
if (!open && btnRef.current) {
|
||||||
|
const rect = btnRef.current.getBoundingClientRect();
|
||||||
|
setMenuPos({ top: rect.bottom + 6, right: window.innerWidth - rect.right });
|
||||||
|
}
|
||||||
|
setOpen((o) => !o);
|
||||||
|
}
|
||||||
|
|
||||||
|
function go(path: string) {
|
||||||
|
setOpen(false);
|
||||||
|
navigate(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
setOpen(false);
|
||||||
|
logout();
|
||||||
|
navigate('/admin/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ label: 'پروفایل من', icon: UserCircleIcon, onClick: () => go(profilePath(primaryRole)) },
|
||||||
|
{ label: 'پرداختها', icon: CreditCardIcon, onClick: () => go(paymentsPath(primaryRole)) },
|
||||||
|
{ label: 'تنظیمات', icon: Cog6ToothIcon, onClick: () => go(settingsPath(primaryRole)) },
|
||||||
|
{ label: 'شخصیسازی', icon: SwatchIcon, onClick: () => { setOpen(false); toggleSettingsPanel(); } },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button ref={btnRef} className="profile-trigger" onClick={handleToggle} title={name}>
|
||||||
|
<div className="avatar sm" style={{ background: avatarBg(name), flexShrink: 0 }}>
|
||||||
|
{initial}
|
||||||
|
</div>
|
||||||
|
<div className="profile-meta">
|
||||||
|
<b>{name}</b>
|
||||||
|
{subtitle && <span>{subtitle}</span>}
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon className="profile-caret" style={{ width: 15, height: 15, flexShrink: 0 }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && menuPos && ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="profile-menu"
|
||||||
|
style={{ position: 'fixed', top: menuPos.top, right: menuPos.right }}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<button key={item.label} className="profile-item" onClick={item.onClick}>
|
||||||
|
<item.icon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button className="profile-item danger" onClick={handleLogout}>
|
||||||
|
<ArrowLeftOnRectangleIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||||
|
خروج
|
||||||
|
</button>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { renderWithProviders } from '../../test/utils';
|
||||||
|
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||||
|
return { ...actual, useNavigate: () => navigateMock };
|
||||||
|
});
|
||||||
|
|
||||||
|
import Topbar from './Topbar';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
navigateMock.mockClear();
|
||||||
|
useAuthStore.setState({
|
||||||
|
userName: 'ادمین', primaryRole: 'admin', context: null, logout: vi.fn(),
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Topbar — هدر مطابق clinic-pro-tauri', () => {
|
||||||
|
it('placeholder سرچ «جستجو» است و ⌘K ندارد', () => {
|
||||||
|
renderWithProviders(<Topbar />);
|
||||||
|
expect(screen.getByPlaceholderText('جستجو')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('⌘K')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('دکمه تنظیمات (چرخدنده) به صفحه تنظیمات نقش میرود', () => {
|
||||||
|
renderWithProviders(<Topbar />);
|
||||||
|
fireEvent.click(screen.getByTitle('تنظیمات'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/admin/settings'); // نقش admin
|
||||||
|
});
|
||||||
|
|
||||||
|
it('منوی پروفایل با نام کاربر رندر میشود', () => {
|
||||||
|
renderWithProviders(<Topbar />);
|
||||||
|
expect(screen.getByText('ادمین')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
BellIcon, SunIcon, MoonIcon, Bars3Icon, Cog6ToothIcon,
|
BellIcon, SunIcon, MoonIcon, Bars3Icon, Cog6ToothIcon,
|
||||||
MagnifyingGlassIcon, XMarkIcon, CheckIcon,
|
MagnifyingGlassIcon, XMarkIcon, CheckIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { useUiStore, HUES, type BrandHue } from '../../stores/uiStore';
|
import { useUiStore, HUES, type BrandHue } from '../../stores/uiStore';
|
||||||
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import Portal from '../ui/Portal';
|
import Portal from '../ui/Portal';
|
||||||
|
import ProfileMenu, { settingsPath } from './ProfileMenu';
|
||||||
|
|
||||||
export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () => void }) {
|
export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () => void }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const primaryRole = useAuthStore((s) => s.primaryRole);
|
||||||
const toggleSidebar = useUiStore((s) => s.toggleSidebar);
|
const toggleSidebar = useUiStore((s) => s.toggleSidebar);
|
||||||
const darkMode = useUiStore((s) => s.darkMode);
|
const darkMode = useUiStore((s) => s.darkMode);
|
||||||
const toggleDarkMode = useUiStore((s) => s.toggleDarkMode);
|
const toggleDarkMode = useUiStore((s) => s.toggleDarkMode);
|
||||||
@@ -30,15 +35,15 @@ export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () =>
|
|||||||
<Bars3Icon style={{ width: 20, height: 20 }} />
|
<Bars3Icon style={{ width: 20, height: 20 }} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Search bar */}
|
{/* Search bar — placeholder «جستجو» مطابق clinic-pro-tauri */}
|
||||||
<div className="topbar-search">
|
<div className="topbar-search">
|
||||||
<MagnifyingGlassIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
<MagnifyingGlassIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||||
<input placeholder="جستجوی بیمار، پزشک، نوبت..." readOnly />
|
<input placeholder="جستجو" readOnly />
|
||||||
<kbd>⌘K</kbd>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
|
{/* ترتیب آیکونها مطابق header در clinic-pro-tauri: تم → تنظیمات → اعلان */}
|
||||||
{/* Dark mode */}
|
{/* Dark mode */}
|
||||||
<button className="icon-btn" onClick={toggleDarkMode} title={darkMode ? 'حالت روشن' : 'حالت تاریک'}>
|
<button className="icon-btn" onClick={toggleDarkMode} title={darkMode ? 'حالت روشن' : 'حالت تاریک'}>
|
||||||
{darkMode
|
{darkMode
|
||||||
@@ -46,28 +51,19 @@ export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () =>
|
|||||||
: <MoonIcon style={{ width: 18, height: 18 }} />}
|
: <MoonIcon style={{ width: 18, height: 18 }} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Settings — به صفحه تنظیمات میرود (مثل tauri) */}
|
||||||
|
<button className="icon-btn" onClick={() => navigate(settingsPath(primaryRole))} title="تنظیمات">
|
||||||
|
<Cog6ToothIcon style={{ width: 18, height: 18 }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Bell */}
|
{/* Bell */}
|
||||||
<button className="icon-btn" title="اعلانها" style={{ position: 'relative' }}>
|
<button className="icon-btn" title="اعلانها" style={{ position: 'relative' }}>
|
||||||
<BellIcon style={{ width: 18, height: 18 }} />
|
<BellIcon style={{ width: 18, height: 18 }} />
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Settings */}
|
{/* Profile menu — آواتار + نام + تخصص + منوی کشویی مطابق tauri */}
|
||||||
<button className="icon-btn" onClick={toggleSettingsPanel} title="شخصیسازی">
|
<ProfileMenu />
|
||||||
<Cog6ToothIcon style={{ width: 18, height: 18 }} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Avatar */}
|
|
||||||
<div
|
|
||||||
className="avatar sm"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(145deg, oklch(0.62 0.15 256), oklch(0.48 0.16 256))',
|
|
||||||
cursor: 'pointer',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Settings Panel */}
|
{/* Settings Panel */}
|
||||||
|
|||||||
@@ -457,6 +457,43 @@ body {
|
|||||||
.topbar-crumbs { display: flex; align-items: center; gap: 8px; color: var(--text-3); font-size: 13px; }
|
.topbar-crumbs { display: flex; align-items: center; gap: 8px; color: var(--text-3); font-size: 13px; }
|
||||||
.topbar-crumbs b { color: var(--text); }
|
.topbar-crumbs b { color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Profile menu (topbar) — معادلِ MenuProfile در clinic-pro-tauri ── */
|
||||||
|
.profile-trigger {
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
||||||
|
padding: 4px 6px; border-radius: var(--r-sm);
|
||||||
|
background: none; border: 1px solid transparent; cursor: pointer;
|
||||||
|
transition: .15s; color: var(--text);
|
||||||
|
}
|
||||||
|
.profile-trigger:hover { background: var(--surface-2); border-color: var(--border); }
|
||||||
|
.profile-meta { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.35; overflow: hidden; }
|
||||||
|
.profile-meta b { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; }
|
||||||
|
.profile-meta span { font-size: 11.5px; color: var(--text-3); white-space: nowrap; }
|
||||||
|
.profile-caret { color: var(--text-3); }
|
||||||
|
/* موبایل: فقط آواتار (مثل md:hidden در tauri) */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.profile-meta, .profile-caret { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-menu {
|
||||||
|
z-index: 9000; min-width: 190px; overflow: hidden;
|
||||||
|
padding: 8px; display: flex; flex-direction: column; gap: 2px;
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r); box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
.profile-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
width: 100%; padding: 8px 10px; border-radius: var(--r-sm);
|
||||||
|
font-size: 13px; font-weight: 500; color: var(--text-2);
|
||||||
|
background: transparent; border: none; cursor: pointer; text-align: right;
|
||||||
|
transition: .12s;
|
||||||
|
}
|
||||||
|
.profile-item:hover { background: var(--surface-2); color: var(--text); }
|
||||||
|
.profile-item.danger {
|
||||||
|
margin-top: 6px; justify-content: center;
|
||||||
|
color: var(--on-primary); background: var(--danger); font-weight: 600;
|
||||||
|
}
|
||||||
|
.profile-item.danger:hover { background: var(--danger); filter: brightness(0.94); color: var(--on-primary); }
|
||||||
|
|
||||||
/* در dark مرز سایدبار/هدر محو است — مثل clinic-pro-tauri */
|
/* در dark مرز سایدبار/هدر محو است — مثل clinic-pro-tauri */
|
||||||
[data-theme="dark"] .sidebar,
|
[data-theme="dark"] .sidebar,
|
||||||
[data-theme="dark"] .sidebar-brand,
|
[data-theme="dark"] .sidebar-brand,
|
||||||
|
|||||||
Reference in New Issue
Block a user