feat(ui): Solve issues 4-10 and polish UI

- Added top padding to all pages (Fix #4)
- Implemented smooth accordion animations for navigation and messages (Fix #5, #9)
- Fixed double selection highlight in navigation bar (Fix #6)
- Enabled nested schedule routing for 'Session' view (Fix #7)
- Softened switching animations for message types (Fix #8)
- Reworked messages to use accordion expansion and removed history (Fix #9)
- Moved Support section into Settings page (Fix #10)
- Cleaned up unused imports and refined layouts

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-11 02:17:12 +03:00
parent 741e4f7db7
commit 24fb28f70a
6 changed files with 285 additions and 200 deletions

View File

@@ -37,8 +37,9 @@ function App() {
</ProtectedRoute>
}
/>
{/* Fix #7: Use wildcard for nested routes */}
<Route
path="/schedule"
path="/schedule/*"
element={
<ProtectedRoute>
<Schedule />

View File

@@ -3,7 +3,6 @@ import { NavLink, useNavigate } from 'react-router-dom';
import {
CalendarDays,
GraduationCap,
LifeBuoy,
LogOut,
Menu,
MessageSquareText,
@@ -47,7 +46,6 @@ const navItems: NavItemConfig[] = [
]
},
{ path: '/messages', label: 'Сообщения', icon: MessageSquareText, badgeKey: 'messages' },
{ path: '/support', label: 'Поддержка', icon: LifeBuoy },
];
export const Navigation: React.FC = () => {
@@ -80,7 +78,7 @@ export const Navigation: React.FC = () => {
if (navMode === 'bar') {
return (
<nav className={cn("navigation-bar", scrollDir === 'down' && "hidden")}>
{navItems.slice(0, 5).map((item) => (
{navItems.slice(0, 4).map((item) => (
<NavLink
key={item.path}
to={item.path}
@@ -109,15 +107,15 @@ export const Navigation: React.FC = () => {
</div>
{isExpanded && profile && (
<div className="mx-4 mb-6 p-3 rounded-2xl bg-sidebar-accent/30 flex items-center gap-3">
<Avatar className="h-10 w-10 border border-white/10">
<AvatarFallback className="bg-primary text-primary-foreground text-xs">
<div className="mx-4 mb-6 p-3 rounded-2xl bg-sidebar-accent/30 flex items-center gap-3 border border-white/5 animate-in fade-in slide-in-from-left-2 duration-300">
<Avatar className="h-10 w-10 border border-white/10 shadow-sm">
<AvatarFallback className="bg-primary text-primary-foreground text-xs font-bold">
{getInitials(profile.fullName)}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold truncate">{profile.fullName}</div>
<div className="text-[11px] opacity-60 truncate uppercase tracking-wider">{profile.group || 'БЕЗ ГРУППЫ'}</div>
<div className="text-sm font-semibold truncate leading-tight">{profile.fullName}</div>
<div className="text-[10px] opacity-60 truncate uppercase tracking-widest mt-0.5">{profile.group || 'БЕЗ ГРУППЫ'}</div>
</div>
</div>
)}
@@ -131,7 +129,7 @@ export const Navigation: React.FC = () => {
<div key={item.label} className="nav-group mb-1">
<NavLink
to={item.path}
className={({ isActive }) => cn("nav-item", isActive && "active")}
className={({ isActive }) => cn("nav-item", (isActive || (hasChildren && isItemExpanded)) && "active")}
onClick={(e) => {
if (hasChildren && isExpanded) {
e.preventDefault();
@@ -145,25 +143,30 @@ export const Navigation: React.FC = () => {
</div>
{isExpanded && <span className="nav-item-label">{item.label}</span>}
{isExpanded && hasChildren && (
<div className="ml-auto">
<div className="ml-auto opacity-60">
{isItemExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</div>
)}
</NavLink>
{isExpanded && isItemExpanded && hasChildren && (
<div className="submenu">
{item.children!.map(child => (
<NavLink
key={child.path}
to={child.path}
className={({ isActive }) => cn("submenu-item", isActive && "active")}
>
{child.label}
</NavLink>
))}
</div>
)}
<div
className="submenu"
style={{
maxHeight: isExpanded && isItemExpanded ? '200px' : '0',
opacity: isExpanded && isItemExpanded ? 1 : 0,
margin: isExpanded && isItemExpanded ? '4px 0 8px' : '0'
}}
>
{item.children?.map(child => (
<NavLink
key={child.path}
to={child.path}
className={({ isActive }) => cn("submenu-item", isActive && "active")}
>
{child.label}
</NavLink>
))}
</div>
</div>
);
})}
@@ -172,7 +175,7 @@ export const Navigation: React.FC = () => {
<div className="mt-auto p-4 space-y-2">
<NavLink
to="/settings"
className={({ isActive }) => cn("nav-item mx-0 h-10", isActive && "active")}
className={({ isActive }) => cn("nav-item mx-0 h-10 px-4", isActive && "active")}
>
<div className="flex items-center gap-3">
<MoreVertical size={18} />
@@ -181,7 +184,7 @@ export const Navigation: React.FC = () => {
</NavLink>
<button
onClick={handleLogout}
className="nav-item mx-0 h-10 w-full hover:bg-destructive/10 hover:text-destructive transition-colors"
className="nav-item mx-0 h-10 w-full px-4 hover:bg-destructive/10 hover:text-destructive transition-colors"
>
<div className="flex items-center gap-3">
<LogOut size={18} />

View File

@@ -1,133 +1,62 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { MessageListItem, MessageDetails } from '../types/api';
import { Mail, Send, Paperclip, ChevronRight, Inbox, MessageSquareText, ArrowLeft, Download, Reply } from 'lucide-react';
import { Mail, Send, Paperclip, ChevronDown, Inbox, MessageSquareText, Download, Reply, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '../components/ui/badge';
import { Button } from '../components/ui/button';
export const Messages: React.FC = () => {
const [messages, setMessages] = useState<MessageListItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [details, setDetails] = useState<MessageDetails | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [details, setDetails] = useState<Record<string, MessageDetails>>({});
const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
const [type, setType] = useState<'in' | 'out'>('in');
const [loading, setLoading] = useState(true);
const [detailsLoading, setDetailsLoading] = useState(false);
const [listLoading, setListLoading] = useState(true);
useEffect(() => {
const fetchMessages = async () => {
setLoading(true);
setListLoading(true);
setExpandedId(null);
try {
const data = await api.getMessages(type);
setMessages(data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
setListLoading(false);
}
};
fetchMessages();
}, [type]);
const handleSelectMessage = async (id: string) => {
setSelectedId(id);
setDetailsLoading(true);
try {
const data = await api.getMessageDetails(id);
setDetails(data);
} catch (err) {
console.error(err);
} finally {
setDetailsLoading(false);
const toggleExpand = async (id: string) => {
if (expandedId === id) {
setExpandedId(null);
return;
}
setExpandedId(id);
if (!details[id]) {
setLoadingIds(prev => new Set(prev).add(id));
try {
const data = await api.getMessageDetails(id);
setDetails(prev => ({ ...prev, [id]: data }));
} catch (err) {
console.error(err);
} finally {
setLoadingIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
}
};
const handleBack = () => {
setSelectedId(null);
setDetails(null);
};
if (selectedId) {
return (
<div className="page-enter max-w-4xl mx-auto space-y-6">
<button
onClick={handleBack}
className="flex items-center gap-2 text-primary font-medium hover:opacity-70 transition-opacity"
>
<ArrowLeft size={20} />
Назад к списку
</button>
{detailsLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4">
<div className="w-8 h-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin" />
<p className="text-sm font-medium">Загрузка содержания...</p>
</div>
) : details ? (
<div className="space-y-6">
<section className="surface-card p-8">
<div className="flex justify-between items-start gap-4 mb-8">
<div className="space-y-1">
<h1 className="text-2xl font-bold tracking-tight">{messages.find(m => m.id === selectedId)?.subject}</h1>
<p className="text-sm text-muted-foreground">
От: <span className="font-semibold text-primary">{messages.find(m => m.id === selectedId)?.senderOrRecipient}</span> {messages.find(m => m.id === selectedId)?.date}
</p>
</div>
<Button variant="outline" size="sm" className="gap-2">
<Reply size={16} />
Ответить
</Button>
</div>
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap">
{details.text}
</div>
{details.files && details.files.length > 0 && (
<div className="mt-10 pt-6 border-t border-white/5">
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-4">Вложения ({details.files.length})</h3>
<div className="flex flex-wrap gap-3">
{details.files.map((file, i) => (
<div key={i} className="flex items-center gap-3 p-3 rounded-xl bg-muted/30 border border-white/5 hover:bg-muted/50 transition-colors cursor-pointer group">
<div className="p-2 rounded-lg bg-primary/10 text-primary group-hover:scale-110 transition-transform">
<Paperclip size={16} />
</div>
<span className="text-sm font-medium">{file}</span>
<Download size={14} className="ml-2 opacity-40 group-hover:opacity-100" />
</div>
))}
</div>
</div>
)}
</section>
{details.history && details.history.length > 0 && (
<section className="space-y-4">
<h2 className="text-lg font-bold px-2">История переписки</h2>
{details.history.map((h, i) => (
<div key={i} className="surface-card p-5 bg-card/20 space-y-2 border-l-4 border-primary/30">
<div className="flex justify-between text-xs font-bold uppercase tracking-wider text-muted-foreground">
<span>{h.author}</span>
<span>{h.date}</span>
</div>
<p className="text-sm">{h.text}</p>
</div>
))}
</section>
)}
</div>
) : (
<div className="surface-card text-center py-20 text-muted-foreground">
Не удалось загрузить сообщение
</div>
)}
</div>
);
}
return (
<div className="page-enter max-w-4xl mx-auto space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-2">
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-2 animate-in fade-in slide-in-from-top-2 duration-500">
<div className="flex items-center gap-3">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<MessageSquareText size={24} />
@@ -138,7 +67,7 @@ export const Messages: React.FC = () => {
<div className="flex p-1 rounded-2xl bg-muted/30 border border-white/5 w-fit">
<button
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all",
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200",
type === 'in' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
)}
onClick={() => setType('in')}
@@ -148,7 +77,7 @@ export const Messages: React.FC = () => {
</button>
<button
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all",
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200",
type === 'out' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
)}
onClick={() => setType('out')}
@@ -159,57 +88,126 @@ export const Messages: React.FC = () => {
</div>
</div>
{loading ? (
{listLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4">
<div className="w-8 h-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin" />
<p className="text-sm font-medium">Загрузка сообщений...</p>
<Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
<p className="text-sm font-medium italic opacity-60">Загрузка сообщений...</p>
</div>
) : (
<div className="space-y-3">
{messages.length === 0 ? (
<div className="surface-card flex flex-col items-center justify-center py-20 text-muted-foreground gap-3">
<Mail size={40} className="opacity-20" />
<p className="text-sm">Нет сообщений в данной категории</p>
<div className="surface-card animate-in fade-in zoom-in-95 duration-300 flex flex-col items-center justify-center py-20 text-muted-foreground gap-3">
<Mail size={40} className="opacity-10" />
<p className="text-sm italic">Нет сообщений в данной категории</p>
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
onClick={() => handleSelectMessage(msg.id)}
className="surface-card group hover:bg-card/60 cursor-pointer flex items-center gap-4 p-4"
>
<div className="hidden sm:flex w-12 h-12 rounded-2xl bg-primary/10 text-primary items-center justify-center shrink-0 group-hover:scale-110 transition-transform">
{type === 'in' ? <Mail size={22} /> : <Send size={22} />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-xs font-bold uppercase tracking-wider text-primary truncate">
{msg.senderOrRecipient}
</span>
<span className="text-[11px] text-muted-foreground whitespace-nowrap">
{msg.date}
</span>
messages.map((msg, index) => {
const isExpanded = expandedId === msg.id;
const isLoading = loadingIds.has(msg.id);
const msgDetails = details[msg.id];
return (
<div
key={msg.id}
className={cn(
"surface-card flex flex-col p-0 overflow-hidden transition-all duration-300",
isExpanded ? "ring-2 ring-primary/20 bg-card/80" : "animate-in fade-in slide-in-from-bottom-2"
)}
style={{ animationDelay: `${index * 50}ms`, animationFillMode: 'both' }}
>
<div
onClick={() => toggleExpand(msg.id)}
className="flex items-center gap-4 p-4 sm:p-5 cursor-pointer hover:bg-white/5 transition-colors"
>
<div className={cn(
"hidden sm:flex w-12 h-12 rounded-2xl items-center justify-center shrink-0 transition-all duration-300",
isExpanded ? "bg-primary text-primary-foreground scale-90" : "bg-primary/10 text-primary"
)}>
{type === 'in' ? <Mail size={22} /> : <Send size={22} />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1">
<span className={cn(
"text-[10px] font-bold uppercase tracking-widest truncate transition-colors",
isExpanded ? "text-primary" : "text-muted-foreground opacity-80"
)}>
{msg.senderOrRecipient}
</span>
<span className="text-[11px] text-muted-foreground whitespace-nowrap opacity-60">
{msg.date}
</span>
</div>
<h3 className={cn(
"text-base font-semibold truncate transition-colors",
isExpanded && "text-primary whitespace-normal"
)}>
{msg.subject}
</h3>
{!isExpanded && msg.hasFiles && (
<div className="flex items-center gap-1.5 mt-1.5 text-primary opacity-80">
<Paperclip size={12} />
<span className="text-[10px] font-bold uppercase">Вложения</span>
</div>
)}
</div>
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-muted-foreground transition-transform duration-300",
isExpanded && "rotate-180 text-primary"
)}>
<ChevronDown size={20} />
</div>
</div>
<h3 className="text-base font-semibold truncate group-hover:text-primary transition-colors">
{msg.subject}
</h3>
<div className="flex items-center gap-3 mt-2">
{msg.hasFiles && (
<Badge variant="secondary" className="h-5 text-[10px] px-2 bg-primary/5 text-primary border-primary/10">
<Paperclip size={10} className="mr-1" />
Вложения
</Badge>
)}
<span className="text-xs text-muted-foreground truncate opacity-60">
Нажмите, чтобы прочитать полностью...
</span>
<div className={cn(
"overflow-hidden transition-all duration-300 ease-in-out",
isExpanded ? "max-h-[1000px] border-t border-white/5" : "max-h-0"
)}>
<div className="p-5 sm:p-8 space-y-6">
{isLoading ? (
<div className="flex items-center justify-center py-8 gap-3 text-muted-foreground">
<Loader2 size={18} className="animate-spin text-primary" />
<span className="text-sm italic">Загрузка содержания...</span>
</div>
) : msgDetails ? (
<>
<div className="flex justify-end mb-2">
<Button variant="outline" size="xs" className="rounded-xl h-8 px-3 gap-2 border-white/10">
<Reply size={14} />
Ответить
</Button>
</div>
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap text-sm sm:text-base">
{msgDetails.text}
</div>
{msgDetails.files && msgDetails.files.length > 0 && (
<div className="pt-6 mt-6 border-t border-white/5 space-y-3">
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">Вложения</h4>
<div className="flex flex-wrap gap-2">
{msgDetails.files.map((file, i) => (
<div key={i} className="flex items-center gap-2.5 p-2.5 rounded-xl bg-muted/30 border border-white/5 hover:bg-primary/5 hover:border-primary/20 transition-all cursor-pointer group">
<Paperclip size={14} className="text-primary/60 group-hover:text-primary transition-colors" />
<span className="text-xs font-medium truncate max-w-[150px]">{file}</span>
<Download size={12} className="ml-1 opacity-0 group-hover:opacity-60 transition-opacity" />
</div>
))}
</div>
</div>
)}
</>
) : (
<p className="text-center py-4 text-destructive text-sm font-medium">
Не удалось загрузить текст сообщения
</p>
)}
</div>
</div>
</div>
<div className="w-8 h-8 rounded-full flex items-center justify-center text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary transition-colors">
<ChevronRight size={18} />
</div>
</div>
))
);
})
)}
</div>
)}

View File

@@ -1,14 +1,18 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { Schedule as ScheduleType } from '../types/api';
import { Clock, User, MapPin, CalendarDays } from 'lucide-react';
import { Clock, User, MapPin, CalendarDays, Loader2 } from 'lucide-react';
import { useLocation } from 'react-router-dom';
export const Schedule: React.FC = () => {
const [schedule, setSchedule] = useState<ScheduleType | null>(null);
const [loading, setLoading] = useState(true);
const location = useLocation();
const isSession = location.pathname.includes('/session');
useEffect(() => {
const fetchSchedule = async () => {
setLoading(true);
try {
const data = await api.getSchedule();
setSchedule(data);
@@ -19,35 +23,37 @@ export const Schedule: React.FC = () => {
}
};
fetchSchedule();
}, []);
}, [isSession]);
if (loading) return (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4">
<div className="w-8 h-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin" />
<p className="text-sm font-medium">Загрузка расписания...</p>
<Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
<p className="text-sm font-medium italic opacity-60">Загрузка расписания...</p>
</div>
);
return (
<div className="page-enter max-w-4xl mx-auto space-y-8">
<div className="flex items-center gap-3 mb-2">
<div className="flex items-center gap-3 mb-2 animate-in fade-in slide-in-from-top-2 duration-500">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<CalendarDays size={24} />
</div>
<h1 className="text-2xl font-bold tracking-tight">Расписание</h1>
<h1 className="text-2xl font-bold tracking-tight">
{isSession ? 'Расписание сессии' : 'Расписание занятий'}
</h1>
</div>
{!schedule || schedule.days.length === 0 ? (
<div className="surface-card text-center py-12 text-muted-foreground">
<div className="surface-card text-center py-12 text-muted-foreground italic">
Нет данных о расписании
</div>
) : (
<div className="space-y-10">
{schedule.days.map((day, idx) => (
<section key={idx} className="space-y-4">
<section key={idx} className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-500" style={{ animationDelay: `${idx * 100}ms`, animationFillMode: 'both' }}>
<div className="flex items-center gap-4">
<div className="h-px flex-1 bg-white/5" />
<h2 className="text-[11px] font-bold text-primary uppercase tracking-[0.2em]">
<h2 className="text-[11px] font-bold text-primary uppercase tracking-[0.2em] bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
{day.date}
</h2>
<div className="h-px flex-1 bg-white/5" />
@@ -57,7 +63,7 @@ export const Schedule: React.FC = () => {
{day.lessons.map((lesson, lIdx) => (
<div
key={lIdx}
className="surface-card flex flex-col sm:flex-row gap-4 sm:items-center p-5 hover:bg-card/60 transition-colors"
className="surface-card flex flex-col sm:flex-row gap-4 sm:items-center p-5 hover:bg-card/60 transition-all active:scale-[0.99]"
>
<div className="flex sm:flex-col items-center sm:items-start gap-3 sm:gap-1 sm:min-w-[110px] border-b sm:border-b-0 sm:border-r border-white/5 pb-3 sm:pb-0 sm:pr-4">
<div className="flex items-center gap-2 text-primary font-bold">
@@ -71,10 +77,10 @@ export const Schedule: React.FC = () => {
<div className="flex-1 space-y-3">
<div>
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-1">
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-1 opacity-80">
{lesson.type || 'Занятие'}
</div>
<h3 className="text-lg font-bold leading-tight">{lesson.subject}</h3>
<h3 className="text-lg font-bold leading-tight group-hover:text-primary transition-colors">{lesson.subject}</h3>
</div>
<div className="flex flex-wrap gap-4">

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { useStore } from '../store/useStore';
import { Palette, Moon, Sun, Monitor, LogOut, Settings2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Palette, Moon, Sun, Monitor, LogOut, Settings2, LifeBuoy, ChevronRight, HelpCircle, MessageCircle } from 'lucide-react';
import { useNavigate, Link } from 'react-router-dom';
import { TextField } from '../components/TextField';
import { Button } from '../components/ui/button';
import { cn } from '@/lib/utils';
@@ -32,7 +32,7 @@ export const Settings: React.FC = () => {
};
return (
<div className="page-enter max-w-3xl mx-auto space-y-8">
<div className="page-enter max-w-3xl mx-auto space-y-8 pb-12">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<Settings2 size={24} />
@@ -137,6 +137,45 @@ export const Settings: React.FC = () => {
</div>
</section>
{/* Support Section Moved Here - Issue #10 */}
<section className="surface-card space-y-6">
<div className="space-y-1">
<h2 className="text-lg font-bold flex items-center gap-2">
<LifeBuoy size={20} className="text-primary" />
Поддержка
</h2>
<p className="text-muted-foreground text-sm">Помощь и обратная связь</p>
</div>
<div className="grid gap-3">
<Link to="/support" className="flex items-center justify-between p-4 rounded-2xl bg-muted/30 border border-transparent hover:border-primary/20 hover:bg-primary/5 transition-all group">
<div className="flex items-center gap-4">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<MessageCircle size={20} />
</div>
<div className="text-left">
<div className="text-sm font-bold">Написать в поддержку</div>
<div className="text-xs text-muted-foreground">Создать новое обращение</div>
</div>
</div>
<ChevronRight size={18} className="text-muted-foreground group-hover:text-primary transition-colors" />
</Link>
<button className="flex items-center justify-between p-4 rounded-2xl bg-muted/30 border border-transparent hover:border-primary/20 hover:bg-primary/5 transition-all group">
<div className="flex items-center gap-4">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<HelpCircle size={20} />
</div>
<div className="text-left">
<div className="text-sm font-bold">Частые вопросы (FAQ)</div>
<div className="text-xs text-muted-foreground">База знаний для студентов</div>
</div>
</div>
<ChevronRight size={18} className="text-muted-foreground group-hover:text-primary transition-colors" />
</button>
</div>
</section>
<section className="pt-4">
<Button
variant="outline"

View File

@@ -54,16 +54,17 @@
--md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1.0);
--md-sys-motion-duration-medium: 200ms;
--md-sys-motion-duration-long: 400ms;
}
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/45;
}
html,
body {
min-height: 100%;
@apply h-full overflow-x-hidden;
background-color: var(--md-sys-color-background);
}
@@ -119,19 +120,24 @@
}
.page-enter {
animation: fadeIn 0.3s ease-out;
animation: fadeIn var(--md-sys-motion-duration-long) var(--md-sys-motion-easing-emphasized);
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
/* Navigation System */
.navigation-bar {
@apply fixed left-0 right-0 bottom-0 h-[var(--nav-bar-height)] bg-background/80 backdrop-blur-xl flex justify-around items-center px-4 z-50 transition-transform duration-200 border-t border-white/5;
@apply fixed left-0 right-0 bottom-0 h-[var(--nav-bar-height)] bg-background/80 backdrop-blur-xl flex justify-around items-center px-4 z-50 transition-transform duration-300 border-t border-white/5;
}
.navigation-bar.hidden {
@@ -147,7 +153,7 @@
}
.nav-item {
@apply flex items-center no-underline text-muted-foreground h-14 px-3 relative transition-all duration-200 rounded-2xl mx-2;
@apply flex items-center no-underline text-muted-foreground h-14 px-3 relative transition-all duration-300 rounded-2xl mx-2;
}
.nav-item:hover {
@@ -155,23 +161,27 @@
}
.nav-item.active {
@apply bg-sidebar-primary text-sidebar-primary-foreground;
@apply bg-sidebar-primary text-sidebar-primary-foreground font-semibold;
}
.navigation-bar .nav-item {
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16;
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16 bg-transparent;
}
.navigation-bar .nav-item.active {
@apply bg-transparent text-primary;
}
.nav-item-icon-wrapper {
@apply w-12 h-8 flex items-center justify-center rounded-2xl transition-colors duration-200 relative;
@apply w-12 h-8 flex items-center justify-center rounded-2xl transition-all duration-300 relative;
}
.nav-item.active .nav-item-icon-wrapper {
.navigation-bar .nav-item.active .nav-item-icon-wrapper {
@apply bg-primary/10;
}
.nav-item-label {
@apply text-[11px] font-medium whitespace-nowrap;
@apply text-[11px] font-medium whitespace-nowrap transition-all duration-300;
}
.navigation-rail.expanded .nav-item {
@@ -179,13 +189,13 @@
}
.navigation-rail.expanded .nav-item-label {
@apply text-sm ml-3 flex-1;
@apply text-sm ml-3 flex-1 opacity-100;
}
/* Submenu */
.submenu {
@apply flex flex-col pl-14 pr-4 gap-1 mb-2;
@apply flex flex-col pl-14 pr-4 gap-1 mb-2 overflow-hidden transition-all duration-300;
}
.submenu-item {
@@ -199,3 +209,31 @@
.submenu-item.active {
@apply bg-primary/10 text-primary font-medium;
}
/* Main Content with Padding Fix #4 */
.main-content {
@apply flex-1 p-6 transition-all duration-300 ease-[var(--md-sys-motion-easing-emphasized)];
padding-top: 2rem;
}
@media (min-width: 600px) {
.navigation-bar { display: none; }
.main-content {
margin-left: var(--nav-rail-width);
padding-top: 3rem;
}
}
@media (min-width: 1240px) {
.main-content.with-expanded-rail {
margin-left: var(--nav-rail-expanded-width);
padding-top: 4rem;
}
}
@media (max-width: 599px) {
.main-content {
padding-bottom: calc(var(--nav-bar-height) + 2rem);
padding-top: 2rem;
}
}