@@ -4,9 +4,34 @@ import { Navigation } from "@/app/providers/layout/navigation/navigation";
|
||||
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
|
||||
|
||||
export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
const [isOpen, setOpen] = useState(true);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(() =>
|
||||
typeof window !== "undefined" ? window.innerWidth < 856 : false,
|
||||
);
|
||||
const { fetchAgents } = useAgentStore();
|
||||
|
||||
const sidebarOpen = isMobile ? mobileOpen : true;
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const mobile = window.innerWidth < 856;
|
||||
setIsMobile(mobile);
|
||||
if (!mobile) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (isMobile) {
|
||||
setMobileOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
@@ -20,10 +45,17 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
}, [fetchAgents]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden" style={{ backgroundColor: "var(--bg-primary)" }}>
|
||||
<Sidebar isOpen={isOpen} onToggle={() => setOpen(!isOpen)} />
|
||||
<div
|
||||
className="flex h-screen overflow-hidden"
|
||||
style={{ backgroundColor: "var(--bg-primary)" }}
|
||||
>
|
||||
<Sidebar
|
||||
isOpen={sidebarOpen}
|
||||
onToggle={toggleSidebar}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
<Navigation />
|
||||
<Navigation onToggleSidebar={toggleSidebar} isMobile={isMobile} />
|
||||
<div className="flex-1 overflow-auto p-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { FaCode, FaChevronDown } from "react-icons/fa";
|
||||
import { FaBars, FaCode, FaChevronDown } from "react-icons/fa";
|
||||
import {
|
||||
FaHome,
|
||||
FaServer,
|
||||
@@ -21,7 +21,15 @@ import {
|
||||
getCurrentTheme,
|
||||
} from "@/modules/theme-changer/utils/apply.theme";
|
||||
|
||||
export const Navigation = () => {
|
||||
interface NavigationProps {
|
||||
onToggleSidebar?: () => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export const Navigation: React.FC<NavigationProps> = ({
|
||||
onToggleSidebar,
|
||||
isMobile,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, logout } = useAuthStore();
|
||||
@@ -75,6 +83,22 @@ export const Navigation = () => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2.5">
|
||||
{/* Бургер — только на мобильных */}
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={onToggleSidebar}
|
||||
className="p-1.5 mr-2 rounded-lg transition-colors flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--text-secondary)",
|
||||
border: "1px solid var(--border)",
|
||||
}}
|
||||
aria-label="Открыть sidebar"
|
||||
>
|
||||
<FaBars size={14} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Навигация */}
|
||||
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useMemo, useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
FaBars,
|
||||
FaMicrochip,
|
||||
@@ -21,11 +21,13 @@ import { adminApi } from "@/modules/admin/api/admin.api";
|
||||
interface SidebarProps {
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
isOpen = true,
|
||||
onToggle,
|
||||
isMobile = false,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { agents, isLoading, error, fetchAgents, removeAgent } =
|
||||
@@ -35,10 +37,26 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showTokenModal, setShowTokenModal] = useState(false);
|
||||
const [showGraphs, setShowGraphs] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(288);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedAgents, setExpandedAgents] = useState<Set<string>>(
|
||||
new Set(agents.map((a) => a.label)),
|
||||
);
|
||||
|
||||
// Рассчитываем максимальную ширину при переключении на графы
|
||||
useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
const targetWidth = showGraphs ? 500 : 288;
|
||||
const maxWidth = window.innerWidth - 200;
|
||||
const finalWidth = Math.min(targetWidth, maxWidth);
|
||||
setSidebarWidth(Math.max(finalWidth, 250));
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => window.removeEventListener("resize", updateWidth);
|
||||
}, [showGraphs]);
|
||||
|
||||
// Token generation state
|
||||
const [tokenLabel, setTokenLabel] = useState("");
|
||||
const [generatedToken, setGeneratedToken] = useState<string | null>(null);
|
||||
@@ -132,35 +150,21 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="fixed top-4 left-4 z-50 p-2.5 rounded-lg shadow-lg transition-colors md:hidden"
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--accent-text)",
|
||||
}}
|
||||
aria-label="Открыть sidebar"
|
||||
>
|
||||
<FaBars size={18} />
|
||||
</button>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay для мобильных */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
||||
onClick={onToggle}
|
||||
/>
|
||||
{/* Overlay — только на мобильных (< 856px) */}
|
||||
{isMobile && (
|
||||
<div className="fixed inset-0 bg-black/50 z-40" onClick={onToggle} />
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={`fixed md:relative z-50 transition-all duration-300 ease-in-out flex flex-col ${
|
||||
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||
}`}
|
||||
ref={sidebarRef}
|
||||
className={`${isMobile ? "fixed" : "relative"} z-50 transition-all duration-300 ease-in-out flex flex-col`}
|
||||
style={{
|
||||
width: showGraphs ? "500px" : "288px",
|
||||
width: `${sidebarWidth}px`,
|
||||
height: "100vh",
|
||||
backgroundColor: "var(--card-bg)",
|
||||
borderRight: "1px solid var(--border)",
|
||||
@@ -191,8 +195,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-1 rounded transition-colors md:hidden"
|
||||
className={`p-1 rounded transition-colors ${isMobile ? "" : "hidden"}`}
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
aria-label="Закрыть sidebar"
|
||||
>
|
||||
<FaTimes size={14} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user