fix: adaptive #3

This commit is contained in:
nikita
2026-04-05 08:23:50 +03:00
parent 9f6defd25c
commit 5b90447984
2 changed files with 348 additions and 267 deletions
+14 -2
View File
@@ -1,6 +1,9 @@
import { useState, useEffect, type ReactNode } from "react";
import { Sidebar } from "@/app/providers/layout/sidebar/sidebar";
import { Navigation } from "@/app/providers/layout/navigation/navigation";
import {
Navigation,
BottomNav,
} from "@/app/providers/layout/navigation/navigation";
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
export const Layout = ({ children }: { children: ReactNode }) => {
@@ -8,6 +11,9 @@ export const Layout = ({ children }: { children: ReactNode }) => {
const [isMobile, setIsMobile] = useState(() =>
typeof window !== "undefined" ? window.innerWidth < 856 : false,
);
const [isVerySmall, setIsVerySmall] = useState(() =>
typeof window !== "undefined" ? window.innerWidth < 600 : false,
);
const { fetchAgents } = useAgentStore();
const sidebarOpen = isMobile ? mobileOpen : true;
@@ -19,6 +25,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
if (!mobile) {
setMobileOpen(false);
}
setIsVerySmall(window.innerWidth < 600);
};
window.addEventListener("resize", handleResize);
@@ -55,8 +62,13 @@ export const Layout = ({ children }: { children: ReactNode }) => {
isMobile={isMobile}
/>
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Navigation onToggleSidebar={toggleSidebar} isMobile={isMobile} />
<Navigation
onToggleSidebar={toggleSidebar}
isMobile={isMobile}
isVerySmall={isVerySmall}
/>
<div className="flex-1 overflow-auto p-4">{children}</div>
{isVerySmall && <BottomNav />}
</div>
</div>
);
@@ -24,11 +24,13 @@ import {
interface NavigationProps {
onToggleSidebar?: () => void;
isMobile?: boolean;
isVerySmall?: boolean;
}
export const Navigation: React.FC<NavigationProps> = ({
onToggleSidebar,
isMobile,
isVerySmall = false,
}) => {
const navigate = useNavigate();
const location = useLocation();
@@ -48,7 +50,6 @@ export const Navigation: React.FC<NavigationProps> = ({
const isActive = (path: string) => location.pathname === path;
// Закрытие дропдауна при клике вне
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
@@ -74,7 +75,50 @@ export const Navigation: React.FC<NavigationProps> = ({
setThemePickerOpen(false);
};
const renderNavItems = (showLabels: boolean, iconSize: number) => (
<div className="flex items-center gap-1 whitespace-nowrap">
{navItems
.filter((item) => {
if ((item as any).adminOnly && !user?.permission_admin) return false;
return true;
})
.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => navigate(item.path)}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg font-medium transition-all flex-shrink-0"
style={{
backgroundColor: active ? "var(--accent)" : "transparent",
color: active ? "var(--accent-text)" : "var(--text-secondary)",
}}
onMouseEnter={(e) => {
if (!active) {
e.currentTarget.style.backgroundColor = "var(--bg-secondary)";
e.currentTarget.style.color = "var(--text-primary)";
}
}}
onMouseLeave={(e) => {
if (!active) {
e.currentTarget.style.backgroundColor = "transparent";
e.currentTarget.style.color = "var(--text-secondary)";
}
}}
title={item.label}
>
<Icon size={iconSize} />
{showLabels && <span className="text-xs">{item.label}</span>}
</button>
);
})}
</div>
);
return (
<>
{/* Верхний бар */}
<div
className="flex-shrink-0 border-b"
style={{
@@ -99,50 +143,24 @@ export const Navigation: React.FC<NavigationProps> = ({
</button>
)}
{/* Навигация */}
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
<div className="flex items-center gap-1 whitespace-nowrap">
{navItems
.filter((item) => {
if ((item as any).adminOnly && !user?.permission_admin)
return false;
return true;
})
.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => navigate(item.path)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all flex-shrink-0"
style={{
backgroundColor: active ? "var(--accent)" : "transparent",
color: active
? "var(--accent-text)"
: "var(--text-secondary)",
}}
onMouseEnter={(e) => {
if (!active) {
e.currentTarget.style.backgroundColor =
"var(--bg-secondary)";
e.currentTarget.style.color = "var(--text-primary)";
}
}}
onMouseLeave={(e) => {
if (!active) {
e.currentTarget.style.backgroundColor = "transparent";
e.currentTarget.style.color = "var(--text-secondary)";
}
}}
{/* Название по центру — только на очень маленьких экранах */}
{isVerySmall && (
<div className="flex-1 text-center mx-4">
<span
className="text-sm font-bold"
style={{ color: "var(--text-primary)" }}
>
<Icon size={12} />
<span>{item.label}</span>
</button>
);
})}
HellreigN
</span>
</div>
)}
{/* Навигация — только если НЕ очень маленький экран */}
{!isVerySmall && (
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
{renderNavItems(true, 12)}
</div>
)}
{/* Профиль пользователя — дропдаун */}
<div className="relative" ref={dropdownRef}>
@@ -178,7 +196,6 @@ export const Navigation: React.FC<NavigationProps> = ({
/>
</button>
{/* Dropdown menu */}
{dropdownOpen && (
<div
className="absolute right-0 top-full mt-2 rounded-lg shadow-xl border z-50"
@@ -188,7 +205,6 @@ export const Navigation: React.FC<NavigationProps> = ({
minWidth: "220px",
}}
>
{/* User info */}
<div
className="px-4 py-3 border-b"
style={{ borderColor: "var(--border)" }}
@@ -198,7 +214,10 @@ export const Navigation: React.FC<NavigationProps> = ({
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: "var(--accent)" }}
>
<FaUser size={12} style={{ color: "var(--accent-text)" }} />
<FaUser
size={12}
style={{ color: "var(--accent-text)" }}
/>
</div>
<div>
<p
@@ -217,7 +236,6 @@ export const Navigation: React.FC<NavigationProps> = ({
</div>
</div>
{/* Theme selector */}
<div className="relative">
<button
onClick={() => setThemePickerOpen(!themePickerOpen)}
@@ -250,7 +268,6 @@ export const Navigation: React.FC<NavigationProps> = ({
/>
</button>
{/* Theme sub-dropdown */}
{themePickerOpen && (
<div
className="absolute right-full top-0 mr-1 rounded-lg shadow-xl border z-50"
@@ -302,7 +319,6 @@ export const Navigation: React.FC<NavigationProps> = ({
)}
</div>
{/* Admin (if admin) */}
{user?.permission_admin && (
<button
onClick={() => {
@@ -324,13 +340,11 @@ export const Navigation: React.FC<NavigationProps> = ({
</button>
)}
{/* Divider */}
<div
className="my-1 border-b"
style={{ borderColor: "var(--border)" }}
/>
{/* Logout */}
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors rounded-b-lg"
@@ -351,5 +365,60 @@ export const Navigation: React.FC<NavigationProps> = ({
</div>
</div>
</div>
</>
);
};
export const BottomNav: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useAuthStore();
const navItems = [
{ path: "/templates", label: "Шаблоны", icon: FaCode },
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
{ path: "/registration", label: "Регистрация", icon: FaKey },
{ path: "/logs", label: "Логи", icon: FaFileAlt },
];
const isActive = (path: string) => location.pathname === path;
return (
<div
className="flex-shrink-0 border-t"
style={{
backgroundColor: "var(--card-bg)",
borderColor: "var(--border)",
}}
>
<div className="flex items-center justify-around px-2 py-2">
{navItems
.filter((item) => {
if ((item as any).adminOnly && !user?.permission_admin)
return false;
return true;
})
.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => navigate(item.path)}
className="flex items-center justify-center p-3 rounded-lg transition-all"
style={{
backgroundColor: active ? "var(--accent)" : "transparent",
color: active
? "var(--accent-text)"
: "var(--text-secondary)",
}}
title={item.label}
>
<Icon size={20} />
</button>
);
})}
</div>
</div>
);
};