From 78889bf9639e7d68e386a952e27ab323f2022517 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 15 Dec 2025 13:55:27 +0100 Subject: [PATCH] fix: collapsed sidebar --- src/components/Sidebar/SidebarItem.tsx | 165 ++++++++++++++- .../Sidebar/SidebarStyles/Sidebar.module.css | 8 + .../SidebarStyles/SidebarItem.module.css | 145 ++++++++++++- .../SidebarStyles/SidebarSubmenu.module.css | 149 +++++++++++++ src/components/Sidebar/SidebarSubmenu.tsx | 196 ++++++++++++------ src/components/Sidebar/sidebarLogic.ts | 2 +- src/components/Sidebar/sidebarTypes.ts | 1 + 7 files changed, 590 insertions(+), 76 deletions(-) diff --git a/src/components/Sidebar/SidebarItem.tsx b/src/components/Sidebar/SidebarItem.tsx index 1ffe515..dc0ff14 100644 --- a/src/components/Sidebar/SidebarItem.tsx +++ b/src/components/Sidebar/SidebarItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useRef } from "react"; import { Link } from "react-router-dom"; import { IoIosArrowDown } from "react-icons/io"; @@ -17,6 +17,112 @@ const SidebarItem: React.FC = React.memo(({ const Icon = item.icon as React.ComponentType>; const hasSubItems = item.submenu && item.submenu.length > 0; const isDisabled = item.moduleEnabled === false; + const iconContainerRef = useRef(null); + + // Fix SVG dimensions when minimized - react-icons uses 1em which can be invisible + useEffect(() => { + if (isMinimized && iconContainerRef.current) { + const wrapper = iconContainerRef.current; + const svg = wrapper.querySelector('svg'); + if (svg) { + // Force explicit pixel dimensions + svg.setAttribute('width', '25'); + svg.setAttribute('height', '25'); + svg.style.cssText = 'width: 25px !important; height: 25px !important; display: block !important;'; + + // Get the actual color from parent li element + const parentLi = wrapper.closest('li'); + const parentColor = parentLi ? window.getComputedStyle(parentLi).color : '#000000'; + + // Force color directly - use black for now to ensure visibility + const iconColor = '#000000'; // Force black for visibility + svg.style.setProperty('color', iconColor, 'important'); + svg.style.setProperty('fill', iconColor, 'important'); + svg.style.setProperty('stroke', iconColor, 'important'); + svg.setAttribute('fill', iconColor); + svg.setAttribute('stroke', iconColor); + + // Set fill/stroke on all paths - use black + const paths = svg.querySelectorAll('path'); + paths.forEach(path => { + const originalFill = path.getAttribute('fill'); + const originalStroke = path.getAttribute('stroke'); + const strokeWidth = path.getAttribute('stroke-width'); + + if (originalFill === 'none' || (!originalFill && originalStroke)) { + path.removeAttribute('fill'); + path.setAttribute('stroke', iconColor); + if (!strokeWidth || strokeWidth === '0') { + path.setAttribute('stroke-width', '2'); + } + path.style.setProperty('stroke', iconColor, 'important'); + path.style.setProperty('stroke-width', '2px', 'important'); + path.style.setProperty('fill', 'none', 'important'); + } else { + path.removeAttribute('fill'); + path.setAttribute('fill', iconColor); + path.style.setProperty('fill', iconColor, 'important'); + path.style.setProperty('stroke', iconColor, 'important'); + } + }); + + // Debug: Check if wrapper is visible + const wrapperRect = wrapper.getBoundingClientRect(); + const wrapperComputed = window.getComputedStyle(wrapper); + const button = wrapper.parentElement?.querySelector('button'); + const buttonComputed = button ? window.getComputedStyle(button) : null; + const svgRect = svg.getBoundingClientRect(); + const svgComputed = window.getComputedStyle(svg); + const firstPath = paths[0]; + const pathComputed = firstPath ? window.getComputedStyle(firstPath) : null; + const parentLiComputed = parentLi ? window.getComputedStyle(parentLi) : null; + const parentMenu = wrapper.closest(`.${styles.menu}`); + const parentMenuComputed = parentMenu ? window.getComputedStyle(parentMenu) : null; + + // Check what's actually at the icon position + const centerX = wrapperRect.left + wrapperRect.width / 2; + const centerY = wrapperRect.top + wrapperRect.height / 2; + const elementsAtPoint = document.elementsFromPoint(centerX, centerY); + const wrapperInElements = elementsAtPoint.includes(wrapper); + + console.log(`[${item.name}] Icon wrapper check:`, { + hasSubItems, + wrapperVisible: wrapperComputed.visibility === 'visible', + wrapperOpacity: wrapperComputed.opacity, + wrapperZIndex: wrapperComputed.zIndex, + wrapperDisplay: wrapperComputed.display, + wrapperBackgroundColor: wrapperComputed.backgroundColor, + wrapperRect: wrapperRect ? `width: ${wrapperRect.width}, height: ${wrapperRect.height}, top: ${wrapperRect.top}, left: ${wrapperRect.left}` : 'no rect', + svgExists: !!svg, + svgRect: svgRect ? `width: ${svgRect.width}, height: ${svgRect.height}, top: ${svgRect.top}, left: ${svgRect.left}` : 'no rect', + svgDisplay: svgComputed.display, + svgVisibility: svgComputed.visibility, + svgOpacity: svgComputed.opacity, + svgColor: svgComputed.color, + pathsCount: paths.length, + firstPathFill: pathComputed?.fill, + firstPathOpacity: pathComputed?.opacity, + buttonExists: !!button, + buttonZIndex: buttonComputed?.zIndex, + buttonPosition: buttonComputed?.position, + buttonRect: button ? button.getBoundingClientRect() : null, + elementsAtIconCenter: elementsAtPoint.slice(0, 5).map(el => ({ + tag: el.tagName, + class: el.className?.split(' ')[0] || 'no-class', + zIndex: window.getComputedStyle(el).zIndex + })), + wrapperInElements: wrapperInElements, + parentLiOverflow: parentLiComputed?.overflow, + parentLiClipPath: parentLiComputed?.clipPath, + parentMenuOverflow: parentMenuComputed?.overflow, + parentMenuClipPath: parentMenuComputed?.clipPath + }); + } else { + console.error(`[${item.name}] SVG NOT FOUND in wrapper!`); + } + } + }, [isMinimized, isActive, item.name, hasSubItems]); + const toggleSubmenu = (e: React.MouseEvent) => { if (isDisabled) { @@ -45,9 +151,16 @@ const SidebarItem: React.FC = React.memo(({ return (
-
  • - {/* Icon - always visible */} - {Icon && } +
  • + {/* Icon - always render, CSS handles positioning */} + {Icon && !isMinimized && ( + + )} {/* Text and arrow - hidden when minimized */} {!isMinimized && ( @@ -85,6 +198,38 @@ const SidebarItem: React.FC = React.memo(({ )} + {/* Icon for minimized state - render directly as child of li */} + {Icon && isMinimized && ( +
    + +
    + )} + + {/* Clickable overlay for items without submenu */} {isMinimized && !isDisabled && !hasSubItems && ( = React.memo(({ onClick={handleLinkClick} /> )} + + {/* Clickable overlay for items with submenu */} + {isMinimized && hasSubItems && !isDisabled && ( +
  • - {hasSubItems && !isMinimized && !isDisabled && } + {hasSubItems && !isDisabled && }
    ); }); diff --git a/src/components/Sidebar/SidebarStyles/Sidebar.module.css b/src/components/Sidebar/SidebarStyles/Sidebar.module.css index 1f548aa..e594c0c 100644 --- a/src/components/Sidebar/SidebarStyles/Sidebar.module.css +++ b/src/components/Sidebar/SidebarStyles/Sidebar.module.css @@ -37,6 +37,10 @@ margin: 0; } +.sidebarContainer.minimized .sidebar { + overflow: visible !important; +} + .logoContainer { display: flex; height: 80px; @@ -124,11 +128,15 @@ /* Minimized Sidebar Styles */ .sidebarContainer.minimized { width: 80px; + overflow: visible !important; } .sidebarContainer.minimized .sidebar { width: 80px; align-items: center; + overflow: visible !important; + overflow-y: visible !important; + overflow-x: visible !important; } .sidebarContainer.minimized .logoContainer { diff --git a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css b/src/components/Sidebar/SidebarStyles/SidebarItem.module.css index 50f77d8..7e1cbeb 100644 --- a/src/components/Sidebar/SidebarStyles/SidebarItem.module.css +++ b/src/components/Sidebar/SidebarStyles/SidebarItem.module.css @@ -7,6 +7,10 @@ padding: 0; } +.menu.minimized { + position: relative !important; +} + .menu li { display: flex; width: 220px; @@ -125,6 +129,15 @@ flex-grow: 0; } +/* Ensure icon is visible when minimized */ +.menu.minimized li .icon.iconMinimized { + display: flex !important; + opacity: 1 !important; + visibility: visible !important; + position: absolute !important; + z-index: 99999 !important; +} + .hassubmenu { width: 20px; height: 20px; @@ -147,7 +160,24 @@ left: 0; width: 100%; height: 100%; - z-index: 10; + z-index: 1; + pointer-events: auto; + background: transparent; +} + +.minimizedSubmenuToggle { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: none; + border: none; + cursor: pointer; + z-index: 1; + padding: 0; + margin: 0; + pointer-events: auto; } /* Minimized Menu Styles */ @@ -155,12 +185,106 @@ width: 46px; padding: 0; justify-content: center; - position: relative; + align-items: center; + position: relative !important; + overflow: visible !important; + clip-path: none !important; + clip: none !important; + contain: none !important; + isolation: auto !important; + /* Don't create stacking context that interferes */ + transform: none !important; } .menu.minimized li a{ opacity: 0; } +/* Ensure icons are never hidden when minimized */ +.menu.minimized li .iconMinimized, +.menu.minimized li [data-debug="icon-minimized"] { + opacity: 1 !important; + visibility: visible !important; + display: flex !important; + position: absolute !important; + z-index: 99999 !important; +} + +.iconMinimizedWrapper { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + font-size: 25px !important; + line-height: 25px !important; + width: 25px !important; + height: 25px !important; + min-width: 25px !important; + min-height: 25px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + pointer-events: none !important; + overflow: visible !important; + visibility: visible !important; + opacity: 1 !important; + flex-shrink: 0 !important; + margin: 0 !important; + z-index: 2 !important; +} + +.iconMinimized { + margin: 0 !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + width: 25px !important; + height: 25px !important; + padding: 2.292px 2.3px 2.508px 2.292px !important; + color: var(--color-text) !important; + opacity: 1 !important; + visibility: visible !important; + font-size: 25px !important; +} + +.iconMinimized svg { + width: 25px !important; + height: 25px !important; + min-width: 25px !important; + min-height: 25px !important; + max-width: 25px !important; + max-height: 25px !important; + display: block !important; + opacity: 1 !important; + visibility: visible !important; +} + +.iconMinimized svg path { + fill: inherit !important; + stroke: inherit !important; +} + +.menu.minimized li.active .iconMinimized svg, +.menu.minimized li:hover .iconMinimized svg { + color: white !important; + fill: white !important; + stroke: white !important; +} + +.menu.minimized li.active .iconMinimized svg path, +.menu.minimized li:hover .iconMinimized svg path { + fill: white !important; + stroke: white !important; +} + +/* Ensure submenu can expand below minimized items */ +.menu.minimized { + width: 100%; + align-items: center; + overflow: visible !important; + min-height: auto !important; + height: auto !important; +} + @@ -170,6 +294,23 @@ color: var(--color-bg); } +.menu.minimized li.active .iconMinimized, +.menu.minimized li:hover .iconMinimized { + color: white !important; +} + +.menu.minimized li.active .iconMinimized svg, +.menu.minimized li:hover .iconMinimized svg { + color: white !important; + fill: white !important; +} + +.menu.minimized li.active .iconMinimized svg path, +.menu.minimized li:hover .iconMinimized svg path { + fill: white !important; + stroke: white !important; +} + /* Disabled item styles */ .menu.disabled, .menu li.disabledItem { diff --git a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css index b69574f..5763af8 100644 --- a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css +++ b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css @@ -67,4 +67,153 @@ height: 16px; color: #181818; flex-shrink: 0; +} + +/* Horizontal layout for minimized sidebar submenus */ +.submenu.minimized { + width: 100% !important; + margin: 0; + padding: 4px 0; + position: relative; + border-radius: 0; + overflow: hidden !important; + display: flex !important; + align-items: center; + justify-content: center; + opacity: 1 !important; + visibility: visible !important; + z-index: 10 !important; +} + +.submenuHorizontalContainer { + display: flex !important; + align-items: center; + justify-content: center; + width: 100%; + padding: 0; + margin: 0; + box-sizing: border-box; + opacity: 1 !important; + visibility: visible !important; +} + +.submenuHorizontalList { + display: flex !important; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 6px; + list-style: none; + padding: 0; + margin: 0; + flex-wrap: wrap; + opacity: 1 !important; + visibility: visible !important; +} + +.submenuHorizontalItem { + display: flex !important; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + width: auto; + height: auto; + list-style: none; + opacity: 1 !important; + visibility: visible !important; +} + +.submenuHorizontalLink { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 40px; + height: 40px; + border-radius: 12px; + background: none; + border: none; + cursor: pointer; + transition: background-color 0.2s ease; + text-decoration: none; + color: #181818 !important; + padding: 0; + margin: 0; + box-sizing: border-box; + position: relative; + overflow: visible; + opacity: 1 !important; + visibility: visible !important; +} + +.submenuHorizontalLink:hover { + background-color: var(--color-secondary); + color: white; +} + +.submenuHorizontalLink:hover .submenuHorizontalIcon { + color: white !important; +} + +.submenuHorizontalLink:hover .submenuHorizontalIcon svg { + color: white !important; +} + +.submenuHorizontalLink:hover .submenuHorizontalIcon svg path { + fill: currentColor !important; + stroke: currentColor !important; +} + +.submenuHorizontalIcon { + display: block !important; + width: 16px !important; + height: 16px !important; + padding: 0; + flex-shrink: 0; + flex-grow: 0; + color: #181818 !important; + margin: 0; + box-sizing: border-box; + position: relative !important; + top: auto !important; + left: auto !important; + transform: none !important; + opacity: 1 !important; + visibility: visible !important; +} + +.submenuHorizontalIcon svg { + width: 16px !important; + height: 16px !important; + min-width: 16px !important; + min-height: 16px !important; + max-width: 16px !important; + max-height: 16px !important; + display: block !important; + margin: 0; + padding: 0; + opacity: 1 !important; + visibility: visible !important; + color: #181818 !important; +} + +.submenuHorizontalIcon svg path { + color: inherit !important; + fill: currentColor !important; + stroke: currentColor !important; +} + +/* Ensure submenu content is visible when sidebar is minimized */ +.menu.minimized .submenu.minimized, +.menu.minimized .submenu.minimized * { + opacity: 1 !important; + visibility: visible !important; + color: #181818 !important; +} + +.menu.minimized .submenuHorizontalLink, +.menu.minimized .submenuHorizontalLink * { + opacity: 1 !important; + visibility: visible !important; + color: #181818 !important; } \ No newline at end of file diff --git a/src/components/Sidebar/SidebarSubmenu.tsx b/src/components/Sidebar/SidebarSubmenu.tsx index 8a7e830..3d48094 100644 --- a/src/components/Sidebar/SidebarSubmenu.tsx +++ b/src/components/Sidebar/SidebarSubmenu.tsx @@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useRef, useEffect, useState } from 'react'; import { SidebarSubmenuProps } from './sidebarTypes'; -const SidebarSubmenu: React.FC = ({ item, isOpen }) => { +const SidebarSubmenu: React.FC = ({ item, isOpen, isMinimized = false }) => { if (!item.submenu) return null; return ( @@ -12,77 +12,137 @@ const SidebarSubmenu: React.FC = ({ item, isOpen }) => { {isOpen && ( -
    -
      - {item.submenu.map(subitem => { - const textRef = useRef(null); - const containerRef = useRef(null); - const [isOverflowing, setIsOverflowing] = useState(false); - - useEffect(() => { - const checkOverflow = () => { - if (textRef.current && containerRef.current) { - const textWidth = textRef.current.scrollWidth; - const containerWidth = containerRef.current.clientWidth; - setIsOverflowing(textWidth > containerWidth); - } - }; + {isMinimized ? ( + // Horizontal layout for minimized sidebar + +
        + {item.submenu.map(subitem => { + const SubIcon = subitem.icon as React.ComponentType>; - checkOverflow(); - // Also check on window resize - window.addEventListener('resize', checkOverflow); - return () => window.removeEventListener('resize', checkOverflow); - }, [subitem.name]); - - const SubIcon = subitem.icon as React.ComponentType>; - - return ( -
      • - -
        + - + )} + + +
      • + ); + })} +
      +
      + ) : ( + // Vertical layout for expanded sidebar + +
        + {item.submenu.map(subitem => { + const textRef = useRef(null); + const containerRef = useRef(null); + const [isOverflowing, setIsOverflowing] = useState(false); + + useEffect(() => { + const checkOverflow = () => { + if (textRef.current && containerRef.current) { + const textWidth = textRef.current.scrollWidth; + const containerWidth = containerRef.current.clientWidth; + setIsOverflowing(textWidth > containerWidth); + } + }; + + checkOverflow(); + // Also check on window resize + window.addEventListener('resize', checkOverflow); + return () => window.removeEventListener('resize', checkOverflow); + }, [subitem.name]); + + const SubIcon = subitem.icon as React.ComponentType>; + + return ( +
      • + +
        -
        - {SubIcon && } - - {subitem.name} - -
        - -
        - -
      • - ); - })} -
      -
    + +
    + {SubIcon && } + + {subitem.name} + +
    +
    + + + + ); + })} + +
    + )} )} diff --git a/src/components/Sidebar/sidebarLogic.ts b/src/components/Sidebar/sidebarLogic.ts index fc95d94..0c957ad 100644 --- a/src/components/Sidebar/sidebarLogic.ts +++ b/src/components/Sidebar/sidebarLogic.ts @@ -44,7 +44,7 @@ export const useSidebarLogic = (): SidebarContextType => { setState(prevState => ({ ...prevState, isMinimized: true, - openItemId: null, // Close any open submenu when minimizing + // Keep submenus open when minimizing - submenu state is independent })); }, []); diff --git a/src/components/Sidebar/sidebarTypes.ts b/src/components/Sidebar/sidebarTypes.ts index b5466ac..dd2a5b9 100644 --- a/src/components/Sidebar/sidebarTypes.ts +++ b/src/components/Sidebar/sidebarTypes.ts @@ -51,6 +51,7 @@ export interface SidebarItemProps { export interface SidebarSubmenuProps { item: SidebarItemData; isOpen: boolean; + isMinimized?: boolean; } export interface SidebarUserProps {