/
t3
/
web-ui
Обзор
Документация
Войти
/
t3
/
web-ui
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
src/components/layout/AppShell.tsx
188 строк
8 KB
Ivan
ui fixes
24 июл 2026, 17:30
24 июл 2026, 17:30
92d754e
Код
Авторство
О чём код?
import { useState } from "react"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { useAuth } from "@/features/auth/AuthContext"; import { useAccess } from "@/features/auth/useAccess"; import { useTheme } from "@/theme/ThemeProvider"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { LayoutDashboard, Users, Shield, FileSearch, Database, LogOut, Settings, Sun, Moon, Monitor, Menu, X, ChevronDown, Gauge, } from "lucide-react"; import { cn } from "@/lib/utils"; const navItems = [ { to: "/", label: "Дашборд", icon: LayoutDashboard, end: true, service: undefined as string | undefined }, { to: "/buckets", label: "Объектное хранилище", icon: Database, service: "s3" }, { to: "/users", label: "Пользователи", icon: Users, service: "iam" }, { to: "/roles", label: "Роли", icon: Shield, service: "iam" }, { to: "/audit", label: "Аудит", icon: FileSearch, service: undefined as string | undefined }, ]; const themeOptions = [ { value: "light" as const, label: "Светлая", icon: Sun }, { value: "dark" as const, label: "Тёмная", icon: Moon }, { value: "system" as const, label: "Системная", icon: Monitor }, ]; export function AppShell() { const navigate = useNavigate(); const { user, logout } = useAuth(); const { hasAllowForService, loading: accessLoading } = useAccess(); const { theme, setTheme } = useTheme(); const [sidebarOpen, setSidebarOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); const visibleItems = accessLoading ? navItems : navItems.filter((item) => !item.service || hasAllowForService(item.service)); return ( <div className="flex h-screen overflow-hidden bg-background"> {/* Mobile overlay */} {sidebarOpen && ( <div className="fixed inset-0 z-40 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} /> )} {/* Sidebar */} <aside className={cn( "fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r bg-card transition-transform lg:static lg:translate-x-0", sidebarOpen ? "translate-x-0" : "-translate-x-full", )} > <div className="flex h-14 items-center border-b px-4"> <Gauge className="mr-2 h-5 w-5 text-primary" /> <span className="text-sm font-semibold">T3 Management</span> <Button variant="ghost" size="icon" className="ml-auto lg:hidden" onClick={() => setSidebarOpen(false)} > <X className="h-4 w-4" /> </Button> </div> <nav className="flex-1 space-y-1 p-3"> {visibleItems.map((item) => ( <NavLink key={item.to} to={item.to} end={item.end} onClick={() => setSidebarOpen(false)} className={({ isActive }) => cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", isActive ? "bg-primary/10 text-primary" : "text-sidebar-foreground hover:bg-accent hover:text-accent-foreground", ) } > <item.icon className="h-4 w-4" /> {item.label} </NavLink> ))} </nav> {/* User menu at bottom of sidebar */} <div className="p-3"> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenuTrigger asChild> <button className={cn( "flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground", )} > <div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary"> {user?.display_name?.charAt(0)?.toUpperCase() ?? "U"} </div> <span className="flex-1 truncate text-left">{user?.display_name}</span> <ChevronDown className="h-3 w-3 text-muted-foreground" /> </button> </DropdownMenuTrigger> <DropdownMenuContent side="top" align="start" className="w-56"> <DropdownMenuLabel className="text-xs font-medium text-muted-foreground"> {user?.display_name} </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => navigate(`/users/${user?.id}`, { state: { fromUserMenu: true } })}> <Settings className="h-4 w-4" /> Настройки </DropdownMenuItem> <DropdownMenuItem onClick={() => logout()}> <LogOut className="h-4 w-4" /> Выход </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> {/* Theme panel at very bottom */} <div className="border-t p-3"> <div className="flex gap-1 rounded-md bg-muted p-1"> {themeOptions.map((opt) => ( <Button key={opt.value} variant="ghost" size="sm" className={cn( "flex-1 h-7", theme === opt.value && "bg-background shadow-sm", )} onClick={() => setTheme(opt.value)} > <opt.icon className="h-3.5 w-3.5" /> <span className="sr-only">{opt.label}</span> </Button> ))} </div> </div> </aside> {/* Main */} <div className="flex flex-1 flex-col overflow-hidden"> {/* Topbar — mobile only */} <header className="flex h-14 items-center gap-4 border-b bg-card px-4 lg:hidden"> <Button variant="ghost" size="icon" onClick={() => setSidebarOpen(true)} > <Menu className="h-5 w-5" /> </Button> <div className="flex-1" /> </header> {/* Content */} <main className="flex-1 overflow-auto"> <div className="container mx-auto max-w-7xl p-4 lg:p-6"> <Outlet /> </div> </main> </div> </div> ); }