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