diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx index 87b61e2..70058ed 100644 --- a/src/Components/Layout/SidebarMenu.jsx +++ b/src/Components/Layout/SidebarMenu.jsx @@ -6,6 +6,7 @@ import { IconButton, Tooltip, Box, + alpha } from "@mui/material"; import SidebarFooter from "./SidebarMenuComponents/SidebarFooter"; import useSidebarResize from "../hooks/useSidebarResize"; @@ -20,7 +21,8 @@ import { PointerSensor, useSensor, useSensors, - DragOverlay + DragOverlay, + MeasuringStrategy } from "@dnd-kit/core"; import { SortableContext, @@ -38,13 +40,15 @@ const SidebarMenu = ({ user, }) => { const [collapsed, setCollapsed] = useState(false); - const { sidebarWidth, startResizing } = useSidebarResize(290); + const { sidebarWidth, startResizing } = useSidebarResize(320); // Увеличил минимальную ширину const [menuItems, setMenuItems] = useState(data.items || []); const [activeItem, setActiveItem] = useState(null); + const [hoveredItem, setHoveredItem] = useState(null); + const [dropIndicator, setDropIndicator] = useState({ show: false, position: null, targetId: null }); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { - distance: 8, + distance: 4, }, })); @@ -61,9 +65,12 @@ const SidebarMenu = ({ } }, [data]); - const handleToggleCollapse = () => setCollapsed(!collapsed); + const handleToggleCollapse = () => { + setCollapsed(!collapsed); + setHoveredItem(null); + }; - // Функция для поиска элемента по ID во всем дереве + // Функции для работы с деревом (остаются без изменений) const findItemInTree = (items, id) => { for (const item of items) { if (item.id === id) return item; @@ -75,7 +82,6 @@ const SidebarMenu = ({ return null; }; - // Функция для удаления элемента из дерева const removeItemFromTree = (items, id) => { return items.filter(item => { if (item.id === id) return false; @@ -86,7 +92,6 @@ const SidebarMenu = ({ }); }; - // Функция для добавления элемента в конкретную папку const addItemToFolder = (items, folderId, newItem) => { return items.map(item => { if (item.id === folderId) { @@ -105,7 +110,6 @@ const SidebarMenu = ({ }); }; - // Функция для поиска родителя элемента const findParent = (items, childId, parent = null) => { for (const item of items) { if (item.id === childId) return parent; @@ -117,7 +121,6 @@ const SidebarMenu = ({ return null; }; - // Функция для добавления элемента на тот же уровень const addItemAtSameLevel = (items, parentId, newItem, afterId = null) => { return items.map(item => { if (item.id === parentId) { @@ -143,11 +146,13 @@ const SidebarMenu = ({ const { active } = event; const item = findItemInTree(menuItems, active.id); setActiveItem(item); + setDropIndicator({ show: false, position: null, targetId: null }); }; - const handleDragEnd = (event) => { const { active, over } = event; setActiveItem(null); + setHoveredItem(null); + setDropIndicator({ show: false, position: null, targetId: null }); if (!over) return; if (active.id === over.id) return; @@ -155,69 +160,190 @@ const SidebarMenu = ({ const draggedItem = findItemInTree(menuItems, active.id); if (!draggedItem) return; - // Определяем тип целевого элемента (папка или элемент) const overItem = findItemInTree(menuItems, over.id); - const isOverFolder = overItem && Array.isArray(overItem.items); + + // Проверяем, не пытаемся ли переместить элемент в его же потомка + if (isDescendant(draggedItem, overItem)) { + return; + } let newTree; - if (isOverFolder) { - // Перемещаем в папку + if (dropIndicator.position === 'inside' && overItem && Array.isArray(overItem.items)) { + // Вставка внутрь папки newTree = removeItemFromTree([...menuItems], active.id); newTree = addItemToFolder(newTree, over.id, draggedItem); } else { - // Перемещаем на тот же уровень + // Вставка на том же уровне const overParent = findParent(menuItems, over.id); if (!overParent) return; - // Удаляем из старого места newTree = removeItemFromTree([...menuItems], active.id); - // Добавляем на новый уровень - newTree = addItemAtSameLevel(newTree, overParent.id, draggedItem, over.id); + // Определяем позицию для вставки + let insertAfterId = null; + if (dropIndicator.position === 'below') { + insertAfterId = over.id; + } else if (dropIndicator.position === 'above') { + const siblings = overParent.items || []; + const overIndex = siblings.findIndex(item => item.id === over.id); + if (overIndex > 0) { + insertAfterId = siblings[overIndex - 1].id; + } + } + + newTree = addItemAtSameLevel(newTree, overParent.id, draggedItem, insertAfterId); } setMenuItems(newTree); localStorage.setItem("menuTree", JSON.stringify(newTree)); }; + const handleDragOver = (event) => { + const { active, over } = event; + + if (!over) { + setDropIndicator({ show: false, position: null, targetId: null }); + return; + } + + const overItem = findItemInTree(menuItems, over.id); + const activeItem = findItemInTree(menuItems, active.id); + + if (!overItem || !activeItem || active.id === over.id) { + setDropIndicator({ show: false, position: null, targetId: null }); + return; + } + + // Проверяем, можно ли перемещать элемент + if (isDescendant(activeItem, overItem)) { + setDropIndicator({ show: false, position: null, targetId: null }); + return; + } + + const overRect = over.rect.current; + if (!overRect) return; + + const relativeY = event.delta.y; + const isOverFolder = overItem && Array.isArray(overItem.items); + const isTopHalf = relativeY < overRect.height * 0.4; + const isBottomHalf = relativeY > overRect.height * 0.6; + + if (isOverFolder && !isTopHalf && !isBottomHalf) { + // Показываем индикатор для вставки в папку + setDropIndicator({ + show: true, + position: 'inside', + targetId: over.id + }); + setHoveredItem(over.id); + } else if (isTopHalf) { + // Показываем индикатор для вставки выше + setDropIndicator({ + show: true, + position: 'above', + targetId: over.id + }); + setHoveredItem(null); + } else if (isBottomHalf) { + // Показываем индикатор для вставки ниже + setDropIndicator({ + show: true, + position: 'below', + targetId: over.id + }); + setHoveredItem(null); + } else { + setDropIndicator({ show: false, position: null, targetId: null }); + setHoveredItem(null); + } + }; + + const isDescendant = (parent, child) => { + if (!parent || !child || !parent.items) return false; + + const checkChildren = (items, targetId) => { + for (const item of items) { + if (item.id === targetId) return true; + if (item.items && checkChildren(item.items, targetId)) return true; + } + return false; + }; + + return checkChildren(parent.items, child.id); + }; + const SidebarResizer = styled("div")(({ theme }) => ({ - width: "4px", - cursor: "ew-resize", - backgroundColor: "transparent", + width: "3px", + cursor: "col-resize", + backgroundColor: alpha(theme.palette.primary.main, 0.3), "&:hover": { - backgroundColor: theme.palette.action.hover, + backgroundColor: theme.palette.primary.main, }, height: "100%", position: "absolute", top: 0, right: 0, zIndex: 1000, + transition: "background-color 0.2s ease", })); + const DropIndicator = ({ position, targetId }) => { + if (!targetId) return null; + + return ( + + ); + }; + + return ( @@ -227,12 +353,14 @@ const SidebarMenu = ({ display: "flex", alignItems: "center", justifyContent: "center", - p: 1, + p: 2, borderBottom: "1px solid", borderColor: "divider", - backgroundColor: "custom.sidebar", + backgroundColor: "background.paper", height: 80, position: "relative", + transition: "all 0.2s ease", + minHeight: 80, }} > {collapsed ? ( - + ) : ( - + )} - + {collapsed ? : } @@ -277,23 +425,67 @@ const SidebarMenu = ({ {/* Основное содержимое меню */} i.id)} strategy={verticalListSortingStrategy}> - + {menuItems.map((item) => ( - + + {dropIndicator.show && dropIndicator.targetId === item.id && + dropIndicator.position !== 'inside' && ( + + )} + + ))} @@ -304,13 +496,18 @@ const SidebarMenu = ({ sx={{ backgroundColor: 'primary.main', color: 'white', - padding: 1, - borderRadius: 1, - boxShadow: 3, - maxWidth: 200, + padding: '8px 12px', + borderRadius: '8px', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)', + maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + fontSize: '0.875rem', + fontWeight: 500, + backdropFilter: 'blur(10px)', + border: '1px solid rgba(255, 255, 255, 0.1)', + transform: 'rotate(5deg)', }} > {activeItem.title} @@ -328,7 +525,11 @@ const SidebarMenu = ({ /> - {!collapsed && } + {!collapsed && ( + + + + )} ); diff --git a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx index afb8801..3d7235b 100644 --- a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx +++ b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx @@ -1,121 +1,121 @@ -// MenuItem.jsx -import React, { useState } from "react"; -import { - ListItem, - ListItemIcon, - ListItemText, - Collapse, - List, - styled, - Menu, - MenuItem as MuiMenuItem -} from "@mui/material"; -import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material"; -import StatusIndicator from "./StatusIndicator"; +// // MenuItem.jsx +// import React, { useState } from "react"; +// import { +// ListItem, +// ListItemIcon, +// ListItemText, +// Collapse, +// List, +// styled, +// Menu, +// MenuItem as MuiMenuItem +// } from "@mui/material"; +// import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material"; +// import StatusIndicator from "./StatusIndicator"; -const StyledListItem = styled(ListItem)(({ theme, level }) => ({ - cursor: "pointer", - paddingLeft: theme.spacing(2 + level * 2), - position: 'relative', - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&.Mui-selected': { - backgroundColor: theme.palette.custom.sidebarHover, - }, -})); +// const StyledListItem = styled(ListItem)(({ theme, level }) => ({ +// cursor: "pointer", +// paddingLeft: theme.spacing(2 + level * 2), +// position: 'relative', +// '&:hover': { +// backgroundColor: theme.palette.action.hover, +// }, +// '&.Mui-selected': { +// backgroundColor: theme.palette.custom.sidebarHover, +// }, +// })); -const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => { - const [isOpen, setIsOpen] = useState(false); - const [contextMenu, setContextMenu] = useState(null); - const hasChildren = Array.isArray(item.items) && item.items.length > 0; +// const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => { +// const [isOpen, setIsOpen] = useState(false); +// const [contextMenu, setContextMenu] = useState(null); +// const hasChildren = Array.isArray(item.items) && item.items.length > 0; - const handleContextMenu = (e) => { - e.preventDefault(); - setContextMenu( - contextMenu === null - ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 } - : null - ); - }; +// const handleContextMenu = (e) => { +// e.preventDefault(); +// setContextMenu( +// contextMenu === null +// ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 } +// : null +// ); +// }; - const handleCloseContextMenu = () => { - setContextMenu(null); - }; +// const handleCloseContextMenu = () => { +// setContextMenu(null); +// }; - const handleToggle = (e) => { - e.stopPropagation(); - setIsOpen(!isOpen); - }; +// const handleToggle = (e) => { +// e.stopPropagation(); +// setIsOpen(!isOpen); +// }; - const handleClick = () => { - if (onSelectItem) { - onSelectItem(item); - } - }; +// const handleClick = () => { +// if (onSelectItem) { +// onSelectItem(item); +// } +// }; - return ( - <> - - {!collapsed && } +// return ( +// <> +// +// {!collapsed && } - - {hasChildren ? (isOpen ? : ) : } - +// +// {hasChildren ? (isOpen ? : ) : } +// - {!collapsed && ( - <> - - {hasChildren && (isOpen ? : )} - - )} - +// {!collapsed && ( +// <> +// +// {hasChildren && (isOpen ? : )} +// +// )} +// - +// - +// - {hasChildren && !collapsed && ( - - - {item.items.map((child, index) => ( - - ))} - - - )} - - ); -}; +// {hasChildren && !collapsed && ( +// +// +// {item.items.map((child, index) => ( +// +// ))} +// +// +// )} +// +// ); +// }; -export default MenuItem; +// export default MenuItem; diff --git a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx index 0ed21f7..3f08a11 100644 --- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx +++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx @@ -1,20 +1,24 @@ import React, { useState } from "react"; -import { Brightness4, Brightness7 } from "@mui/icons-material"; -import { IconButton, Tooltip } from "@mui/material"; +import { Brightness4, Brightness7, Settings, Help } from "@mui/icons-material"; +import { + IconButton, + Tooltip, + Box, + Button, + alpha +} from "@mui/material"; import { List, ListItem, ListItemText, styled, Switch, - Box, - Button } from "@mui/material"; import SettingsModal from "../SettingsModal"; import { RoleBasedRender } from "../../UI/RoleBasedRender"; const FooterList = styled(List)(({ theme }) => ({ - backgroundColor: theme.palette.custom.sidebar, + backgroundColor: 'background.paper', padding: theme.spacing(1, 0), borderTop: `1px solid ${theme.palette.divider}`, marginTop: 'auto' @@ -22,12 +26,15 @@ const FooterList = styled(List)(({ theme }) => ({ const FooterListItem = styled(ListItem)(({ theme }) => ({ '&:hover': { - backgroundColor: theme.palette.custom.sidebarHover, + backgroundColor: alpha(theme.palette.action.hover, 0.4), }, padding: theme.spacing(1, 2), display: 'flex', justifyContent: 'space-between', - alignItems: 'center' + alignItems: 'center', + borderRadius: '8px', + margin: '0 8px 4px', + transition: 'all 0.2s ease', })); const SidebarFooter = ({ @@ -46,72 +53,93 @@ const SidebarFooter = ({ const handleSettingsClose = () => { setSettingsOpen(false); }; - /*console.log('SidebarFooter user with role:', { - ...user, - hasRole: 'role' in user, - roleValue: user?.role - }); */ + return ( <> - {!collapsed && ( - - - - )} - - {/* кнопка настроек */} - - {!collapsed && ( + {!collapsed ? ( + <> + - )} - - - - setIsDarkMode(!isDarkMode)} - sx={{ color: 'custom.sidebarText' }} + + + setIsDarkMode(!isDarkMode)} + sx={{ + color: 'text.secondary', + '&:hover': { + color: 'text.primary', + backgroundColor: alpha('#000000', 0.1) + } + }} + > + {isDarkMode ? : } + + + setIsDarkMode(!isDarkMode)} + size="small" + color="primary" + /> + + + + + + + + ) : ( + + + + - {!collapsed && ( - setIsDarkMode(!isDarkMode)} - size="small" - /> - )} - - + + )} - {/* Используем RoleBasedRender для модального окна */} { +const SortableMenuItem = ({ + item, + collapsed, + onSelectItem, + level = 0, + isHovered = false, + showDropIndicator = false, + sidebarWidth = 300 +}) => { const [isOpen, setIsOpen] = useState(false); + const [isLocalHovered, setIsLocalHovered] = useState(false); const { attributes, @@ -21,22 +33,34 @@ const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => { setNodeRef, transform, transition, - isDragging + isDragging, + isOver } = useSortable({ id: item.id, data: { type: 'menu-item', - item + item, + level } }); const style = { transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, + transition: transition || 'all 0.2s ease', + opacity: isDragging ? 0.6 : 1, + zIndex: isDragging ? 1000 : 1, }; const hasChildren = Array.isArray(item.items) && item.items.length > 0; + const isFolder = hasChildren; + const isHighlighted = isHovered || isOver; + + // Рассчитываем максимальную ширину текста в зависимости от уровня вложенности + const calculateMaxTextWidth = () => { + const baseWidth = sidebarWidth - 40; // Отступы и иконки + const levelOffset = level * 24; // Отступ для каждого уровня + return baseWidth - levelOffset - 60; // Оставляем место для иконок и отступов + }; const handleClick = (e) => { e.stopPropagation(); @@ -47,63 +71,146 @@ const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => { } }; + const handleMouseEnter = () => { + setIsLocalHovered(true); + }; + + const handleMouseLeave = () => { + setIsLocalHovered(false); + }; + + const getBackgroundColor = (theme) => { + if (isDragging) return alpha(theme.palette.primary.main, 0.1); + if (isHighlighted) return alpha(theme.palette.primary.main, 0.08); + if (isLocalHovered) return alpha(theme.palette.action.hover, 0.4); + return 'transparent'; + }; + return ( - + alpha(theme.palette.primary.main, 0.1), + border: (theme) => `2px dashed ${theme.palette.primary.main}`, + borderRadius: '8px', + }) + }} + > getBackgroundColor(theme), + borderRadius: '6px', + margin: '1px 4px', + transition: 'all 0.2s ease', }} onClick={handleClick} > + {!collapsed && ( )} - - {hasChildren ? (isOpen ? : ) : } - - {!collapsed && ( <> - - {hasChildren && (isOpen ? : )} + + + {item.title} + + } + sx={{ mr: 0.5, flex: '1 1 auto', minWidth: 0 }} + /> + + {hasChildren && ( + + )} )} {hasChildren && !collapsed && ( - + `1px solid ${alpha(theme.palette.divider, 0.1)}`, + marginLeft: 2, + position: 'relative', + }} + > {item.items.map((child) => ( - + + + ))}