feat: add gradient fade indicators to scrollable desktop nav

This commit is contained in:
Fringg
2026-03-09 21:09:37 +03:00
parent ab7d1b7f25
commit 622172f038

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, Link } from 'react-router'; import { useLocation, Link } from 'react-router';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -269,6 +269,31 @@ export function AppShell({ children }: AppShellProps) {
haptic.impact('light'); haptic.impact('light');
}; };
// Desktop nav scroll fade indicators
const navRef = useRef<HTMLElement>(null);
const [navCanScrollLeft, setNavCanScrollLeft] = useState(false);
const [navCanScrollRight, setNavCanScrollRight] = useState(false);
const updateNavScroll = useCallback(() => {
const el = navRef.current;
if (!el) return;
setNavCanScrollLeft(el.scrollLeft > 2);
setNavCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 2);
}, []);
useEffect(() => {
const el = navRef.current;
if (!el) return;
updateNavScroll();
el.addEventListener('scroll', updateNavScroll, { passive: true });
const ro = new ResizeObserver(updateNavScroll);
ro.observe(el);
return () => {
el.removeEventListener('scroll', updateNavScroll);
ro.disconnect();
};
}, [updateNavScroll]);
// Calculate header height based on fullscreen mode (only on mobile Telegram) // Calculate header height based on fullscreen mode (only on mobile Telegram)
// On iOS: contentSafeAreaInset.top includes status bar + dynamic island + Telegram header // On iOS: contentSafeAreaInset.top includes status bar + dynamic island + Telegram header
// On Android: safeAreaInset.top only includes status bar, need to add Telegram header height (~48px) // On Android: safeAreaInset.top only includes status bar, need to add Telegram header height (~48px)
@@ -317,73 +342,89 @@ export function AppShell({ children }: AppShellProps) {
</Link> </Link>
{/* Center Navigation */} {/* Center Navigation */}
<nav className="scrollbar-hide flex items-center gap-1 overflow-x-auto"> <div className="relative min-w-0">
{desktopNavItems.map((item) => ( {/* Left fade */}
<Link <div
key={item.path} className={cn(
to={item.path} 'pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-6 bg-gradient-to-r from-dark-950/95 to-transparent transition-opacity duration-200',
onClick={handleNavClick} navCanScrollLeft ? 'opacity-100' : 'opacity-0',
className={cn( )}
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors', />
isActive(item.path) {/* Right fade */}
? 'bg-dark-800 text-dark-50' <div
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200', className={cn(
)} 'pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-6 bg-gradient-to-l from-dark-950/95 to-transparent transition-opacity duration-200',
> navCanScrollRight ? 'opacity-100' : 'opacity-0',
<item.icon className="h-4 w-4" /> )}
<span>{item.label}</span> />
</Link> <nav ref={navRef} className="scrollbar-hide flex items-center gap-1 overflow-x-auto">
))} {desktopNavItems.map((item) => (
{referralEnabled && (
<Link
to="/referral"
onClick={handleNavClick}
className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/referral')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<UsersIcon className="h-4 w-4" />
<span>{t('nav.referral')}</span>
</Link>
)}
{giftEnabled && (
<Link
to="/gift"
onClick={handleNavClick}
className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/gift')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<GiftIcon className="h-4 w-4" />
<span>{t('nav.gift')}</span>
</Link>
)}
{isAdmin && (
<>
{/* Separator before admin */}
<div className="mx-2 h-5 w-px shrink-0 bg-dark-700" />
<Link <Link
to="/admin" key={item.path}
to={item.path}
onClick={handleNavClick} onClick={handleNavClick}
className={cn( className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors', 'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
location.pathname.startsWith('/admin') isActive(item.path)
? 'bg-warning-500/10 text-warning-400' ? 'bg-dark-800 text-dark-50'
: 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400', : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)} )}
> >
<ShieldIcon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
<span>{t('admin.nav.title')}</span> <span>{item.label}</span>
</Link> </Link>
</> ))}
)} {referralEnabled && (
</nav> <Link
to="/referral"
onClick={handleNavClick}
className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/referral')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<UsersIcon className="h-4 w-4" />
<span>{t('nav.referral')}</span>
</Link>
)}
{giftEnabled && (
<Link
to="/gift"
onClick={handleNavClick}
className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/gift')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<GiftIcon className="h-4 w-4" />
<span>{t('nav.gift')}</span>
</Link>
)}
{isAdmin && (
<>
{/* Separator before admin */}
<div className="mx-2 h-5 w-px shrink-0 bg-dark-700" />
<Link
to="/admin"
onClick={handleNavClick}
className={cn(
'flex shrink-0 items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition-colors',
location.pathname.startsWith('/admin')
? 'bg-warning-500/10 text-warning-400'
: 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400',
)}
>
<ShieldIcon className="h-4 w-4" />
<span>{t('admin.nav.title')}</span>
</Link>
</>
)}
</nav>
</div>
{/* Right side actions */} {/* Right side actions */}
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">