fix(ui): Fix build and final polish
- Fixed unknown utility class - Verified all issue solutions Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,30 @@ export const api = {
|
||||
const response = await client.get<ApiResponse<MessageDetails>>(`/v1/messages/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
getRecipients: async () => {
|
||||
if (useStore.getState().isMockMode) {
|
||||
return mockApi.getRecipients();
|
||||
}
|
||||
const client = getClient();
|
||||
const response = await client.get<ApiResponse<{ id: string; name: string }[]>>('/v1/messages/recipients');
|
||||
return response.data.data;
|
||||
},
|
||||
sendMessage: async (recipientId: string, subject: string, text: string) => {
|
||||
if (useStore.getState().isMockMode) {
|
||||
return mockApi.sendMessage(recipientId, subject, text);
|
||||
}
|
||||
const client = getClient();
|
||||
const response = await client.post('/v1/messages', { recipientId, subject, text });
|
||||
return response.data;
|
||||
},
|
||||
replyToMessage: async (id: string, text: string, item?: string, idinfo?: string) => {
|
||||
if (useStore.getState().isMockMode) {
|
||||
return mockApi.replyToMessage(id, text, item, idinfo);
|
||||
}
|
||||
const client = getClient();
|
||||
const response = await client.post(`/v1/messages/${id}/reply`, { text, item, idinfo });
|
||||
return response.data;
|
||||
},
|
||||
getSchedule: async () => {
|
||||
if (useStore.getState().isMockMode) {
|
||||
return mockApi.getSchedule();
|
||||
|
||||
@@ -85,6 +85,8 @@ export const mockApi = {
|
||||
setTimeout(() => resolve({
|
||||
id,
|
||||
text: 'Добрый день!\n\nИнформируем вас о необходимости произвести оплату за 4 семестр обучения до 15 апреля 2026 года. В случае возникновения вопросов, пожалуйста, свяжитесь с бухгалтерией по внутреннему номеру 102.\n\nС уважением, Администрация.',
|
||||
idinfo: '456',
|
||||
item: '789',
|
||||
history: [
|
||||
{ date: '10.04.2026', author: 'Бухгалтерия', text: 'Уведомление об оплате' }
|
||||
],
|
||||
@@ -92,6 +94,27 @@ export const mockApi = {
|
||||
}), 500);
|
||||
});
|
||||
},
|
||||
getRecipients: async (): Promise<{ id: string; name: string }[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve([
|
||||
{ id: '1', name: 'Петров Петр Петрович' },
|
||||
{ id: '2', name: 'Сидоров Сидор Сидорович' },
|
||||
{ id: '3', name: 'Деканат ИСС' }
|
||||
]), 300);
|
||||
});
|
||||
},
|
||||
sendMessage: async (recipientId: string, subject: string, text: string): Promise<any> => {
|
||||
return new Promise((resolve) => {
|
||||
console.log('Mock Send:', { recipientId, subject, text });
|
||||
setTimeout(() => resolve({ success: true }), 1000);
|
||||
});
|
||||
},
|
||||
replyToMessage: async (id: string, text: string, item?: string, idinfo?: string): Promise<any> => {
|
||||
return new Promise((resolve) => {
|
||||
console.log('Mock Reply:', { id, text, item, idinfo });
|
||||
setTimeout(() => resolve({ success: true }), 1000);
|
||||
});
|
||||
},
|
||||
getSchedule: async (): Promise<Schedule> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(mockSchedule), 500);
|
||||
|
||||
@@ -22,7 +22,7 @@ interface NavItemConfig {
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
badgeKey?: 'messages';
|
||||
children?: { path: string; label: string }[];
|
||||
children?: { path: string; label: string; exact?: boolean }[];
|
||||
hideInRail?: boolean;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const navItems: NavItemConfig[] = [
|
||||
label: 'Расписание',
|
||||
icon: CalendarDays,
|
||||
children: [
|
||||
{ path: '/schedule', label: 'Текущая неделя' },
|
||||
{ path: '/schedule', label: 'Текущая неделя', exact: true },
|
||||
{ path: '/schedule/session', label: 'Сессия' },
|
||||
]
|
||||
},
|
||||
@@ -42,7 +42,7 @@ const navItems: NavItemConfig[] = [
|
||||
label: 'Успеваемость',
|
||||
icon: GraduationCap,
|
||||
children: [
|
||||
{ path: '/grades', label: 'Оценки' },
|
||||
{ path: '/grades', label: 'Оценки', exact: true },
|
||||
{ path: '/debts', label: 'Задолженности' },
|
||||
]
|
||||
},
|
||||
@@ -114,44 +114,53 @@ export const Navigation: React.FC = () => {
|
||||
<button className="icon-button" onClick={toggleRail}>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
{isExpanded && <span className="rail-title font-semibold text-lg ml-2">Bonch</span>}
|
||||
{isExpanded && <span className="rail-title font-bold text-xl ml-3 tracking-tight">Bonch</span>}
|
||||
</div>
|
||||
|
||||
{isExpanded && profile && (
|
||||
{profile && (
|
||||
<div className={cn("px-4 mb-6 transition-all duration-300", isExpanded ? "items-stretch" : "items-center")}>
|
||||
<NavLink
|
||||
to="/profile"
|
||||
className={cn(
|
||||
"mx-4 mb-6 p-3 rounded-2xl flex items-center gap-3 border transition-all hover:bg-sidebar-accent/50",
|
||||
"flex items-center gap-3 border transition-all hover:bg-sidebar-accent/50",
|
||||
isExpanded ? "p-3 rounded-2xl" : "w-12 h-12 rounded-full justify-center",
|
||||
location.pathname === '/profile' ? "bg-sidebar-primary border-white/10" : "bg-sidebar-accent/20 border-white/5"
|
||||
)}
|
||||
>
|
||||
<Avatar className="h-10 w-10 border border-white/10 shadow-sm">
|
||||
<Avatar className={cn("border border-white/10 shadow-sm transition-all", isExpanded ? "h-10 w-10" : "h-8 w-8")}>
|
||||
<AvatarFallback className="bg-primary text-primary-foreground text-xs font-bold">
|
||||
{getInitials(profile.fullName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{isExpanded && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<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>
|
||||
)}
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rail-items flex-1 overflow-y-auto">
|
||||
<div className={cn("rail-items flex-1 overflow-y-auto w-full", !isExpanded && "items-center")}>
|
||||
{navItems.filter(i => !i.hideInRail).map((item) => {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
|
||||
// Fix #14: Sub-item logic
|
||||
const isChildActive = hasChildren && item.children!.some(child => location.pathname === child.path);
|
||||
// Improved active state logic for sub-items
|
||||
const isChildActive = hasChildren && item.children!.some(child =>
|
||||
child.exact ? location.pathname === child.path : location.pathname.startsWith(child.path)
|
||||
);
|
||||
|
||||
const isItemExpanded = expandedItems.includes(item.label) || isChildActive;
|
||||
|
||||
return (
|
||||
<div key={item.label} className="nav-group mb-1">
|
||||
<div key={item.label} className="nav-group mb-1 flex flex-col items-center w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"nav-item cursor-pointer",
|
||||
isChildActive && !isExpanded && "active",
|
||||
isExpanded && isChildActive && "bg-sidebar-accent/30"
|
||||
isExpanded && isChildActive && "bg-sidebar-accent/30 text-foreground",
|
||||
isExpanded && "w-auto self-stretch"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (hasChildren && isExpanded) {
|
||||
@@ -174,7 +183,7 @@ export const Navigation: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="submenu"
|
||||
className="submenu w-full"
|
||||
style={{
|
||||
maxHeight: isExpanded && isItemExpanded ? '200px' : '0',
|
||||
opacity: isExpanded && isItemExpanded ? 1 : 0,
|
||||
@@ -185,7 +194,7 @@ export const Navigation: React.FC = () => {
|
||||
<NavLink
|
||||
key={child.path}
|
||||
to={child.path}
|
||||
end // Exact match
|
||||
end={child.exact}
|
||||
className={({ isActive }) => cn("submenu-item", isActive && "active")}
|
||||
>
|
||||
{child.label}
|
||||
@@ -197,10 +206,14 @@ export const Navigation: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto p-4 space-y-2">
|
||||
<div className={cn("mt-auto p-4 space-y-2 w-full", !isExpanded && "items-center")}>
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={({ isActive }) => cn("nav-item mx-0 h-10 px-4", isActive && "active")}
|
||||
className={({ isActive }) => cn(
|
||||
"nav-item mx-0 h-10 px-4",
|
||||
isActive && "active",
|
||||
!isExpanded && "w-10 px-0 justify-center mx-auto"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings2 size={18} />
|
||||
@@ -209,7 +222,10 @@ export const Navigation: React.FC = () => {
|
||||
</NavLink>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="nav-item mx-0 h-10 w-full px-4 hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
className={cn(
|
||||
"nav-item mx-0 h-10 w-full px-4 hover:bg-destructive/10 hover:text-destructive transition-colors",
|
||||
!isExpanded && "w-10 px-0 justify-center mx-auto"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<LogOut size={18} />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { MessageListItem, MessageDetails } from '../types/api';
|
||||
import { Mail, Send, Paperclip, ChevronDown, Inbox, MessageSquareText, Download, Reply, Loader2, Plus } from 'lucide-react';
|
||||
import { Mail, Send, Paperclip, ChevronDown, Inbox, MessageSquareText, Download, Loader2, Plus, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { TextField } from '../components/TextField';
|
||||
|
||||
export const Messages: React.FC = () => {
|
||||
const [messages, setMessages] = useState<MessageListItem[]>([]);
|
||||
@@ -14,7 +15,22 @@ export const Messages: React.FC = () => {
|
||||
const [type, setType] = useState<'in' | 'out'>('in');
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
|
||||
// Reply State
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [isReplying, setIsReplying] = useState(false);
|
||||
|
||||
// New Message State
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [recipients, setRecipients] = useState<{ id: string; name: string }[]>([]);
|
||||
const [newRecipient, setNewRecipient] = useState('');
|
||||
const [newSubject, setNewSubject] = useState('');
|
||||
const [newText, setNewText] = useState('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
}, [type]);
|
||||
|
||||
const fetchMessages = async () => {
|
||||
setListLoading(true);
|
||||
setExpandedId(null);
|
||||
@@ -23,20 +39,21 @@ export const Messages: React.FC = () => {
|
||||
setMessages(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Ошибка загрузки сообщений');
|
||||
} finally {
|
||||
setListLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMessages();
|
||||
}, [type]);
|
||||
|
||||
const toggleExpand = async (id: string) => {
|
||||
if (expandedId === id) {
|
||||
setExpandedId(null);
|
||||
setReplyText('');
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedId(id);
|
||||
setReplyText('');
|
||||
|
||||
if (!details[id]) {
|
||||
setLoadingIds(prev => new Set(prev).add(id));
|
||||
@@ -45,6 +62,7 @@ export const Messages: React.FC = () => {
|
||||
setDetails(prev => ({ ...prev, [id]: data }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Ошибка загрузки содержания');
|
||||
} finally {
|
||||
setLoadingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -55,17 +73,54 @@ export const Messages: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReply = (e: React.MouseEvent, subject: string) => {
|
||||
e.stopPropagation();
|
||||
toast.info(`Ответ на: ${subject}`, {
|
||||
description: 'Функция отправки ответа будет доступна в следующем обновлении.'
|
||||
});
|
||||
const handleReply = async (id: string) => {
|
||||
if (!replyText.trim()) return;
|
||||
setIsReplying(true);
|
||||
const msgDetails = details[id];
|
||||
try {
|
||||
await api.replyToMessage(id, replyText, msgDetails?.item, msgDetails?.idinfo);
|
||||
toast.success('Ответ отправлен');
|
||||
setReplyText('');
|
||||
setExpandedId(null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Ошибка при отправке ответа');
|
||||
} finally {
|
||||
setIsReplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewMessage = () => {
|
||||
toast.success('Новое сообщение', {
|
||||
description: 'Открытие формы создания сообщения...'
|
||||
});
|
||||
const handleCreateNew = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const data = await api.getRecipients();
|
||||
setRecipients(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendNew = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRecipient || !newSubject || !newText) {
|
||||
toast.error('Заполните все поля');
|
||||
return;
|
||||
}
|
||||
setIsSending(true);
|
||||
try {
|
||||
await api.sendMessage(newRecipient, newSubject, newText);
|
||||
toast.success('Сообщение отправлено');
|
||||
setIsCreating(false);
|
||||
setNewRecipient('');
|
||||
setNewSubject('');
|
||||
setNewText('');
|
||||
if (type === 'out') fetchMessages();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Ошибка при отправке сообщения');
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -102,12 +157,65 @@ export const Messages: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleNewMessage} size="icon" className="rounded-2xl h-11 w-11 shadow-md">
|
||||
<Button onClick={handleCreateNew} size="icon" className="rounded-2xl h-11 w-11 shadow-md">
|
||||
<Plus size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreating && (
|
||||
<div className="surface-card p-6 space-y-6 animate-in fade-in zoom-in-95 duration-300 relative border-2 border-primary/20">
|
||||
<button
|
||||
onClick={() => setIsCreating(false)}
|
||||
className="absolute top-4 right-4 icon-button h-8 w-8"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
<h2 className="text-xl font-bold">Новое сообщение</h2>
|
||||
|
||||
<form onSubmit={handleSendNew} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground px-4">Получатель</label>
|
||||
<select
|
||||
value={newRecipient}
|
||||
onChange={(e) => setNewRecipient(e.target.value)}
|
||||
className="w-full h-14 px-4 rounded-2xl bg-muted/30 border border-white/5 focus:border-primary/50 outline-none transition-all text-sm appearance-none"
|
||||
>
|
||||
<option value="">Выберите преподавателя...</option>
|
||||
{recipients.map(r => (
|
||||
<option key={r.id} value={r.id}>{r.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label="Тема"
|
||||
value={newSubject}
|
||||
onChange={(e) => setNewSubject(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground px-4">Текст сообщения</label>
|
||||
<textarea
|
||||
value={newText}
|
||||
onChange={(e) => setNewText(e.target.value)}
|
||||
className="w-full min-h-[150px] p-4 rounded-2xl bg-muted/30 border border-white/5 focus:border-primary/50 outline-none transition-all resize-none text-sm"
|
||||
placeholder="Введите текст сообщения..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="ghost" type="button" onClick={() => setIsCreating(false)}>Отмена</Button>
|
||||
<Button type="submit" disabled={isSending} className="min-w-[140px]">
|
||||
{isSending ? <Loader2 size={18} className="animate-spin mr-2" /> : <Send size={18} className="mr-2" />}
|
||||
Отправить
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{listLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4">
|
||||
<Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
|
||||
@@ -141,7 +249,7 @@ export const Messages: React.FC = () => {
|
||||
>
|
||||
<div className={cn(
|
||||
"hidden sm:flex w-12 h-12 rounded-2xl items-center justify-center shrink-0 transition-all duration-500",
|
||||
isExpanded ? "bg-primary text-primary-foreground scale-90 rotate-[15deg]" : "bg-primary/10 text-primary"
|
||||
isExpanded ? "bg-primary text-primary-foreground rotate-[15deg] scale-90" : "bg-primary/10 text-primary"
|
||||
)}>
|
||||
{type === 'in' ? <Mail size={22} /> : <Send size={22} />}
|
||||
</div>
|
||||
@@ -182,9 +290,9 @@ export const Messages: React.FC = () => {
|
||||
|
||||
<div className={cn(
|
||||
"overflow-hidden transition-all duration-500 ease-[var(--md-sys-motion-easing-emphasized)]",
|
||||
isExpanded ? "max-h-[1000px] border-t border-white/10 opacity-100" : "max-h-0 opacity-0"
|
||||
isExpanded ? "max-h-[1200px] border-t border-white/10 opacity-100" : "max-h-0 opacity-0"
|
||||
)}>
|
||||
<div className="p-5 sm:p-8 space-y-6">
|
||||
<div className="p-5 sm:p-8 space-y-8">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 gap-3 text-muted-foreground">
|
||||
<Loader2 size={18} className="animate-spin text-primary" />
|
||||
@@ -192,36 +300,46 @@ export const Messages: React.FC = () => {
|
||||
</div>
|
||||
) : msgDetails ? (
|
||||
<>
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-xl h-9 px-4 gap-2 border-white/10 hover:bg-primary/10 hover:text-primary transition-all"
|
||||
onClick={(e) => handleReply(e, msg.subject)}
|
||||
>
|
||||
<Reply size={16} />
|
||||
Ответить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap text-sm sm:text-base">
|
||||
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap text-sm sm:text-base bg-muted/20 p-6 rounded-2xl border border-white/5">
|
||||
{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="space-y-3">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground px-2">Вложения</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">
|
||||
<div key={i} className="flex items-center gap-2.5 p-3 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" />
|
||||
<Download size={12} className="ml-1 opacity-40 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-6 border-t border-white/10 space-y-4">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground px-2">Быстрый ответ</h4>
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
className="w-full min-h-[100px] p-4 rounded-2xl bg-muted/30 border border-white/5 focus:border-primary/50 outline-none transition-all resize-none text-sm"
|
||||
placeholder="Ваш ответ..."
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={!replyText.trim() || isReplying}
|
||||
onClick={() => handleReply(msg.id)}
|
||||
className="gap-2 px-6 rounded-xl"
|
||||
>
|
||||
{isReplying ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
|
||||
Отправить ответ
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center py-4 text-destructive text-sm font-medium">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
:root {
|
||||
--radius: 1rem;
|
||||
--nav-rail-width: 80px;
|
||||
--nav-rail-width: 88px; /* Slightly wider for better icon spacing */
|
||||
--nav-rail-expanded-width: 280px;
|
||||
--nav-bar-height: 80px;
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
@apply border-border outline-ring/45;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -129,15 +129,10 @@
|
||||
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-muted/90 backdrop-blur-2xl flex justify-around items-center px-4 z-50 transition-transform duration-300 border-t border-white/10;
|
||||
@apply fixed left-0 right-0 bottom-0 h-[var(--nav-bar-height)] bg-muted/95 backdrop-blur-2xl flex justify-around items-center px-4 z-50 transition-transform duration-300 border-t border-white/10;
|
||||
}
|
||||
|
||||
.navigation-bar.hidden {
|
||||
@@ -145,15 +140,22 @@
|
||||
}
|
||||
|
||||
.navigation-rail {
|
||||
@apply fixed left-0 top-0 bottom-0 w-[var(--nav-rail-width)] bg-sidebar/40 backdrop-blur-md flex flex-col py-4 border-r border-white/5 z-50 transition-all duration-300 ease-[var(--md-sys-motion-easing-emphasized)] overflow-hidden;
|
||||
/* Fix #15: Better alignment and spacing */
|
||||
@apply fixed left-0 top-0 bottom-0 w-[var(--nav-rail-width)] bg-sidebar/40 backdrop-blur-md flex flex-col py-6 border-r border-white/5 z-50 transition-all duration-300 ease-[var(--md-sys-motion-easing-emphasized)] overflow-hidden items-center;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded {
|
||||
@apply w-[var(--nav-rail-expanded-width)];
|
||||
@apply w-[var(--nav-rail-expanded-width)] items-stretch;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply flex items-center no-underline text-muted-foreground h-14 px-3 relative transition-all duration-300 rounded-2xl mx-2;
|
||||
@apply flex items-center no-underline text-muted-foreground h-14 transition-all duration-300 rounded-2xl relative;
|
||||
width: 56px; /* Icon-only width */
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .nav-item {
|
||||
@apply w-auto mx-4 px-4;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
@@ -165,7 +167,7 @@
|
||||
}
|
||||
|
||||
.navigation-bar .nav-item {
|
||||
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16 bg-transparent;
|
||||
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16 bg-transparent w-auto;
|
||||
}
|
||||
|
||||
.navigation-bar .nav-item.active {
|
||||
@@ -184,18 +186,18 @@
|
||||
@apply text-[11px] font-medium whitespace-nowrap transition-all duration-300;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .nav-item {
|
||||
@apply h-12 rounded-2xl mx-4 px-4;
|
||||
.navigation-rail:not(.expanded) .nav-item-label {
|
||||
@apply absolute opacity-0 scale-95 pointer-events-none;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .nav-item-label {
|
||||
@apply text-sm ml-3 flex-1 opacity-100;
|
||||
@apply text-sm ml-3 flex-1 opacity-100 scale-100;
|
||||
}
|
||||
|
||||
/* Submenu */
|
||||
|
||||
.submenu {
|
||||
@apply flex flex-col pl-14 pr-4 gap-1 mb-2 overflow-hidden transition-all duration-300;
|
||||
@apply flex flex-col pl-14 pr-4 gap-1 overflow-hidden transition-all duration-300;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
@@ -213,21 +215,21 @@
|
||||
/* 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;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.navigation-bar { display: none; }
|
||||
.main-content {
|
||||
margin-left: var(--nav-rail-width);
|
||||
padding-top: 3rem;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1240px) {
|
||||
.main-content.with-expanded-rail {
|
||||
margin-left: var(--nav-rail-expanded-width);
|
||||
padding-top: 4rem;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,3 +239,12 @@
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rail-header {
|
||||
@apply flex items-center justify-center h-14 mb-8 transition-all duration-300;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .rail-header {
|
||||
@apply justify-start px-6;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user