Polish Novya UI with brand colors, first-load skeletons, and session transition overlay.

Keep chat pinned to the latest message while AI thinks or streams, and align buttons/inputs with the logo teal/blue focus treatment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ar.badiei
2026-09-09 01:31:24 +03:30
co-authored by Cursor
parent 4ac0c16665
commit 8016b8f45e
56 changed files with 1874 additions and 529 deletions
+50 -19
View File
@@ -3,16 +3,18 @@ import { Link, useLocation, useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/hooks/useAuth';
import { APP_NAME } from '@/utils/constants';
import { BrandLogo } from '@/components/BrandLogo';
import { ClipboardList, LogOut, Moon, Sun, User } from 'lucide-react';
import { MOTION_DURATION, MOTION_EASE, fastTransition } from '@/lib/motion';
import { useTheme } from '@/hooks/useTheme';
import { cn } from '@/lib/utils';
/**
* Minimal top shell: product mark + profile menu (Course Status, Logout).
* Replaces the former bottom navigation.
*/
export function AppHeader() {
const { isAuthenticated, signOut, isGuest } = useAuth();
const { isAuthenticated, signOut, isGuest, user } = useAuth();
const { toggleTheme, isDark } = useTheme();
const navigate = useNavigate();
const location = useLocation();
@@ -21,6 +23,9 @@ export function AppHeader() {
const menuRef = useRef<HTMLDivElement>(null);
const isAuthPage = location.pathname === '/login' || location.pathname === '/signup';
const usernameLabel = isGuest
? 'مهمان'
: user?.email?.trim() || 'کاربر';
useEffect(() => {
setMenuOpen(false);
@@ -35,8 +40,16 @@ export function AppHeader() {
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setMenuOpen(false);
};
document.addEventListener('mousedown', onPointerDown);
return () => document.removeEventListener('mousedown', onPointerDown);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('keydown', onKeyDown);
};
}, [menuOpen]);
const handleSignOut = async () => {
@@ -52,24 +65,31 @@ export function AppHeader() {
return (
<>
<header className="app-header border-b border-border/40 backdrop-blur-2xl">
<header className="app-header">
<div className="mx-auto flex h-14 w-full items-center justify-between gap-3 px-4">
<Link
to="/"
className="truncate text-base font-semibold tracking-tight text-foreground"
className="inline-flex shrink-0 items-center transition-opacity duration-300 hover:opacity-90"
aria-label={APP_NAME}
>
{APP_NAME}
<BrandLogo imgClassName="h-7 sm:h-8" />
</Link>
<div className="relative" ref={menuRef}>
<motion.button
type="button"
onClick={() => setMenuOpen((open) => !open)}
whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
transition={fastTransition}
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-cyan-200 bg-white text-cyan-700 shadow-sm transition-colors hover:border-cyan-500 hover:text-cyan-800 dark:border-neon-cyan/40 dark:bg-background/70 dark:text-neon-cyan dark:shadow-none dark:hover:border-neon-cyan dark:hover:text-neon-cyan dark:hover:shadow-glow-sm"
className={cn(
'inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors duration-200',
'border-slate-200 bg-white text-sky-700 shadow-sm',
'hover:border-sky-300 hover:bg-sky-50 hover:text-sky-800',
'dark:border-slate-600/60 dark:bg-slate-800 dark:text-sky-300 dark:shadow-none',
'dark:hover:border-slate-500 dark:hover:bg-slate-700 dark:hover:text-sky-200',
menuOpen && 'border-sky-400 ring-2 ring-sky-400/25 dark:border-sky-500 dark:ring-sky-500/20'
)}
aria-label="منوی پروفایل"
aria-expanded={menuOpen}
aria-haspopup="menu"
@@ -80,39 +100,50 @@ export function AppHeader() {
<AnimatePresence>
{menuOpen && (
<motion.div
key="menu-panel"
role="menu"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
initial={{ opacity: 0, scale: 0.96, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: -4 }}
transition={{ duration: MOTION_DURATION.fast, ease: MOTION_EASE }}
className="glass-panel-strong absolute left-0 z-[70] mt-2 w-52 origin-top-left p-1"
className="header-menu-panel"
>
<div className="header-menu-identity" aria-label="نام کاربری">
<p className="text-[11px] font-medium text-muted-foreground">نام کاربری</p>
<p
className="truncate text-sm font-semibold text-foreground"
title={usernameLabel}
dir="ltr"
>
{usernameLabel}
</p>
</div>
<Link
to="/course-status"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-body transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/5 dark:hover:text-white"
className="header-menu-item"
onClick={() => setMenuOpen(false)}
>
<ClipboardList className="h-4 w-4 shrink-0 text-neon-cyan" />
<ClipboardList className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
وضعیت دوره
</Link>
<button
type="button"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-body transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/5 dark:hover:text-white"
className="header-menu-item"
onClick={toggleTheme}
>
{isDark ? (
<Sun className="h-4 w-4 shrink-0 text-neon-cyan" />
<Sun className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
) : (
<Moon className="h-4 w-4 shrink-0 text-neon-cyan" />
<Moon className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
)}
{isDark ? 'حالت روشن' : 'حالت تاریک'}
</button>
<button
type="button"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-500/10 dark:text-red-300"
className="header-menu-item header-menu-item-danger"
onClick={() => {
setMenuOpen(false);
setShowLogoutConfirm(true);
@@ -129,10 +160,10 @@ export function AppHeader() {
</header>
{showLogoutConfirm && (
<div className="fixed inset-0 z-[60] flex items-end justify-center bg-navy-deep/70 p-4 backdrop-blur-sm sm:items-center">
<div className="fixed inset-0 z-[60] flex items-end justify-center bg-slate-900/50 p-4 backdrop-blur-md sm:items-center">
<div className="glass-panel-strong w-full max-w-sm space-y-4 p-6">
<h3 className="text-lg font-semibold text-foreground">خروج از حساب کاربری</h3>
<p className="text-sm text-body">آیا مطمئن هستید که می‌خواهید خارج شوید؟</p>
<p className="text-sm leading-relaxed text-body">آیا مطمئن هستید که می‌خواهید خارج شوید؟</p>
<div className="flex gap-3">
<button
type="button"