feat: clean navbar icons, floating theme bubble, auto theme on first visit

- Navbar: remove theme switcher, clean nav icons (no text labels),
  active state is accent dot underline + icon color — no pill bg
- useTheme: drop 'system' from stored type; first visit = auto
  by system preference, stays unset until user explicitly clicks
- ThemeBubble: fixed bottom-right pill with Sun/Moon icon + label,
  hover lift, click toggles light ↔ dark and persists to localStorage
- App.tsx: render ThemeBubble inside ThemeProvider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 00:51:42 +03:00
parent 492bd1b4e1
commit ae01ebdeea
4 changed files with 80 additions and 60 deletions

View File

@@ -1,26 +1,17 @@
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useTheme } from '@/hooks/useTheme';
import UserAvatar from '@/components/UserAvatar';
import { Home, PlusSquare, Shield, Sun, Moon, Monitor } from 'lucide-react';
import { Home, PlusSquare, Shield } from 'lucide-react';
export default function Navbar() {
const { user } = useAuth();
const { theme, setTheme } = useTheme();
const location = useLocation();
const cycleTheme = () => {
const next: Record<string, 'dark' | 'system' | 'light'> = { light: 'dark', dark: 'system', system: 'light' };
setTheme(next[theme]);
};
const ThemeIcon = theme === 'dark' ? Moon : theme === 'light' ? Sun : Monitor;
const themeLabel = theme === 'dark' ? 'Тёмная' : theme === 'light' ? 'Светлая' : 'Авто';
if (!user) return null;
const isProfile = location.pathname.startsWith('/profile');
const isFeed = location.pathname === '/feed' || location.pathname === '/';
const isFeed = location.pathname === '/feed' || location.pathname === '/';
const isUpload = location.pathname === '/upload';
const isProfile = location.pathname.startsWith('/profile');
return (
<nav className="sticky top-0 z-50 nav-glass border-b border-[var(--border-light)]">
@@ -36,34 +27,27 @@ export default function Navbar() {
</Link>
{/* Навигация */}
<div className="flex items-center gap-0.5">
<NavLink to="/feed" active={isFeed} icon={Home} label="Лента" />
<NavLink to="/upload" active={isUpload} icon={PlusSquare} label="Добавить" />
{/* Переключатель темы */}
<button
onClick={cycleTheme}
title={`Тема: ${themeLabel}`}
className="flex flex-col items-center justify-center gap-0.5 h-11 w-11 rounded-xl transition-all text-[var(--fg-tertiary)] hover:text-[var(--fg-secondary)] hover:bg-[var(--border-light)]"
>
<ThemeIcon className="h-[18px] w-[18px]" strokeWidth={1.5} />
<span className="text-[9px] font-semibold uppercase tracking-wide hidden sm:block opacity-60">{themeLabel}</span>
</button>
<div className="flex items-center gap-1">
<NavIcon to="/feed" active={isFeed} icon={Home} label="Лента" />
<NavIcon to="/upload" active={isUpload} icon={PlusSquare} label="Добавить" />
{/* Аватар */}
<Link to="/profile" className="ml-1 relative flex items-center">
<div className={`flex items-center justify-center h-9 w-9 rounded-full transition-all duration-200 ${
<Link
to="/profile"
title="Профиль"
className={[
'relative ml-1 flex items-center rounded-full transition-all duration-200',
isProfile
? 'ring-[2.5px] ring-[var(--accent)] ring-offset-2 ring-offset-[var(--bg)]'
: 'opacity-80 hover:opacity-100'
}`}>
<UserAvatar
avatarUrl={user.avatar_url}
username={user.username}
className="h-9 w-9"
fallbackClassName="text-[11px] avatar-colored bg-[var(--accent)]"
/>
</div>
: 'opacity-75 hover:opacity-100',
].join(' ')}
>
<UserAvatar
avatarUrl={user.avatar_url}
username={user.username}
className="h-8 w-8"
fallbackClassName="text-[11px] avatar-colored bg-[var(--accent)]"
/>
{user.is_admin && (
<span className="absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-[var(--accent)] flex items-center justify-center ring-2 ring-[var(--bg)]">
<Shield className="h-2 w-2 text-white" strokeWidth={3} />
@@ -76,22 +60,27 @@ export default function Navbar() {
);
}
function NavLink({
function NavIcon({
to, active, icon: Icon, label,
}: {
to: string; active: boolean; icon: any; label: string;
to: string; active: boolean; icon: React.ComponentType<any>; label: string;
}) {
return (
<Link
to={to}
className={`flex flex-col items-center justify-center gap-0.5 h-11 w-11 rounded-xl transition-all duration-200 ${
title={label}
className={[
'relative flex items-center justify-center h-9 w-9 rounded-xl',
'transition-all duration-200',
active
? 'bg-[var(--accent-soft)] text-[var(--accent)]'
: 'text-[var(--fg-tertiary)] hover:text-[var(--fg-secondary)] hover:bg-[var(--border-light)]'
}`}
? 'text-[var(--accent)] bg-[var(--accent-soft)]'
: 'text-[var(--fg-tertiary)] hover:text-[var(--fg)] hover:bg-[var(--border-light)]',
].join(' ')}
>
<Icon className="h-[18px] w-[18px]" strokeWidth={active ? 2.2 : 1.5} />
<span className="text-[9px] font-semibold uppercase tracking-wide hidden sm:block opacity-70">{label}</span>
<Icon className="h-5 w-5" strokeWidth={active ? 2.2 : 1.7} />
{active && (
<span className="absolute -bottom-[11px] left-1/2 -translate-x-1/2 h-[2.5px] w-4 rounded-full bg-[var(--accent)]" />
)}
</Link>
);
}