fix: collapsed sidebar

This commit is contained in:
Ida Dittrich 2025-12-15 13:55:27 +01:00
parent aaf64b869f
commit 78889bf963
7 changed files with 590 additions and 76 deletions

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useRef } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IoIosArrowDown } from "react-icons/io"; import { IoIosArrowDown } from "react-icons/io";
@ -17,6 +17,112 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>; const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
const hasSubItems = item.submenu && item.submenu.length > 0; const hasSubItems = item.submenu && item.submenu.length > 0;
const isDisabled = item.moduleEnabled === false; const isDisabled = item.moduleEnabled === false;
const iconContainerRef = useRef<HTMLDivElement>(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) => { const toggleSubmenu = (e: React.MouseEvent) => {
if (isDisabled) { if (isDisabled) {
@ -45,9 +151,16 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
return ( return (
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}> <div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
<li className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}> <li
{/* Icon - always visible */} className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}
{Icon && <Icon className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`} />} data-item-name={item.name}
>
{/* Icon - always render, CSS handles positioning */}
{Icon && !isMinimized && (
<Icon
className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`}
/>
)}
{/* Text and arrow - hidden when minimized */} {/* Text and arrow - hidden when minimized */}
{!isMinimized && ( {!isMinimized && (
@ -85,6 +198,38 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
)} )}
{/* Icon for minimized state - render directly as child of li */}
{Icon && isMinimized && (
<div
ref={iconContainerRef}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '25px',
height: '25px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 10
}}
>
<Icon
className={`${styles.icon} ${styles.iconMinimized} ${isDisabled ? styles.disabledIcon : ''}`}
size={25}
style={{
width: '25px',
height: '25px',
color: '#000000',
fill: '#000000'
}}
/>
</div>
)}
{/* Clickable overlay for items without submenu */}
{isMinimized && !isDisabled && !hasSubItems && ( {isMinimized && !isDisabled && !hasSubItems && (
<Link <Link
to={item.link || "#"} to={item.link || "#"}
@ -93,8 +238,18 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
onClick={handleLinkClick} onClick={handleLinkClick}
/> />
)} )}
{/* Clickable overlay for items with submenu */}
{isMinimized && hasSubItems && !isDisabled && (
<button
onClick={toggleSubmenu}
className={styles.minimizedSubmenuToggle}
title={`Toggle ${item.name} submenu`}
aria-expanded={isOpen}
/>
)}
</li> </li>
{hasSubItems && !isMinimized && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} />} {hasSubItems && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} isMinimized={isMinimized} />}
</div> </div>
); );
}); });

View file

@ -37,6 +37,10 @@
margin: 0; margin: 0;
} }
.sidebarContainer.minimized .sidebar {
overflow: visible !important;
}
.logoContainer { .logoContainer {
display: flex; display: flex;
height: 80px; height: 80px;
@ -124,11 +128,15 @@
/* Minimized Sidebar Styles */ /* Minimized Sidebar Styles */
.sidebarContainer.minimized { .sidebarContainer.minimized {
width: 80px; width: 80px;
overflow: visible !important;
} }
.sidebarContainer.minimized .sidebar { .sidebarContainer.minimized .sidebar {
width: 80px; width: 80px;
align-items: center; align-items: center;
overflow: visible !important;
overflow-y: visible !important;
overflow-x: visible !important;
} }
.sidebarContainer.minimized .logoContainer { .sidebarContainer.minimized .logoContainer {

View file

@ -7,6 +7,10 @@
padding: 0; padding: 0;
} }
.menu.minimized {
position: relative !important;
}
.menu li { .menu li {
display: flex; display: flex;
width: 220px; width: 220px;
@ -125,6 +129,15 @@
flex-grow: 0; 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 { .hassubmenu {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -147,7 +160,24 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 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 */ /* Minimized Menu Styles */
@ -155,12 +185,106 @@
width: 46px; width: 46px;
padding: 0; padding: 0;
justify-content: center; 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{ .menu.minimized li a{
opacity: 0; 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); 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 */ /* Disabled item styles */
.menu.disabled, .menu.disabled,
.menu li.disabledItem { .menu li.disabledItem {

View file

@ -68,3 +68,152 @@
color: #181818; color: #181818;
flex-shrink: 0; 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;
}

View file

@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { useRef, useEffect, useState } from 'react'; import { useRef, useEffect, useState } from 'react';
import { SidebarSubmenuProps } from './sidebarTypes'; import { SidebarSubmenuProps } from './sidebarTypes';
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => { const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimized = false }) => {
if (!item.submenu) return null; if (!item.submenu) return null;
return ( return (
@ -12,12 +12,71 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
{isOpen && ( {isOpen && (
<motion.div <motion.div
initial={{ height: 0, opacity: 0 }} initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }} animate={{
exit={{ height: 0, opacity: 0 }} height: "auto",
transition={{ duration: 0.3, ease: "easeInOut" }} opacity: 1,
className={styles.submenu} transition: {
height: { duration: 0.3, ease: "easeInOut", delay: 0 },
opacity: { duration: 0.25, ease: "easeInOut", delay: 0.3 }
}
}}
exit={{
height: 0,
opacity: 0,
transition: {
opacity: { duration: 0.25, ease: "easeInOut", delay: 0 },
height: { duration: 0.3, ease: "easeInOut", delay: 0.25 }
}
}}
style={{ overflow: "hidden" }}
className={`${styles.submenu} ${isMinimized ? styles.minimized : ''}`}
>
{isMinimized ? (
// Horizontal layout for minimized sidebar
<motion.div
className={styles.submenuHorizontalContainer}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.3, duration: 0.25 } }}
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
>
<ul className={styles.submenuHorizontalList}>
{item.submenu.map(subitem => {
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
return (
<li key={subitem.id} className={styles.submenuHorizontalItem}>
<Link
to={subitem.link || '#'}
title={subitem.name}
className={styles.submenuHorizontalLink}
>
{SubIcon && (
<SubIcon
className={styles.submenuHorizontalIcon}
size={16}
style={{
width: '16px',
height: '16px',
color: '#181818',
display: 'block'
}}
/>
)}
</Link>
</li>
);
})}
</ul>
</motion.div>
) : (
// Vertical layout for expanded sidebar
<motion.div
className={styles.submenuLineContainer}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.3, duration: 0.25 } }}
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
> >
<div className={styles.submenuLineContainer}>
<ul className={styles.submenuList}> <ul className={styles.submenuList}>
{item.submenu.map(subitem => { {item.submenu.map(subitem => {
const textRef = useRef<HTMLSpanElement>(null); const textRef = useRef<HTMLSpanElement>(null);
@ -82,7 +141,8 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
); );
})} })}
</ul> </ul>
</div> </motion.div>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View file

@ -44,7 +44,7 @@ export const useSidebarLogic = (): SidebarContextType => {
setState(prevState => ({ setState(prevState => ({
...prevState, ...prevState,
isMinimized: true, isMinimized: true,
openItemId: null, // Close any open submenu when minimizing // Keep submenus open when minimizing - submenu state is independent
})); }));
}, []); }, []);

View file

@ -51,6 +51,7 @@ export interface SidebarItemProps {
export interface SidebarSubmenuProps { export interface SidebarSubmenuProps {
item: SidebarItemData; item: SidebarItemData;
isOpen: boolean; isOpen: boolean;
isMinimized?: boolean;
} }
export interface SidebarUserProps { export interface SidebarUserProps {