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 { 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 hasSubItems = item.submenu && item.submenu.length > 0;
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) => {
if (isDisabled) {
@ -45,9 +151,16 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
return (
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
<li className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}>
{/* Icon - always visible */}
{Icon && <Icon className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`} />}
<li
className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}
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 */}
{!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 && (
<Link
to={item.link || "#"}
@ -93,8 +238,18 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
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>
{hasSubItems && !isMinimized && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} />}
{hasSubItems && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} isMinimized={isMinimized} />}
</div>
);
});

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}

View file

@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { useRef, useEffect, useState } from 'react';
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;
return (
@ -12,77 +12,137 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className={styles.submenu}
animate={{
height: "auto",
opacity: 1,
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 : ''}`}
>
<div className={styles.submenuLineContainer}>
<ul className={styles.submenuList}>
{item.submenu.map(subitem => {
const textRef = useRef<HTMLSpanElement>(null);
const containerRef = useRef<HTMLDivElement>(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
<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>>;
checkOverflow();
// Also check on window resize
window.addEventListener('resize', checkOverflow);
return () => window.removeEventListener('resize', checkOverflow);
}, [subitem.name]);
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
return (
<li key={subitem.id}>
<Link
to={subitem.link || '#'}
title={subitem.name}
>
<div
ref={containerRef}
className={styles.textContainer}
return (
<li key={subitem.id} className={styles.submenuHorizontalItem}>
<Link
to={subitem.link || '#'}
title={subitem.name}
className={styles.submenuHorizontalLink}
>
<motion.span
ref={textRef}
style={{
display: 'block',
whiteSpace: 'nowrap',
}}
initial={{ x: 0 }}
animate={{ x: 0 }}
{...(isOverflowing && {
whileHover: {
x: -(textRef.current?.scrollWidth || 0) + (containerRef.current?.clientWidth || 153),
transition: {
duration: 2,
ease: "linear"
}
}
})}
{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 } }}
>
<ul className={styles.submenuList}>
{item.submenu.map(subitem => {
const textRef = useRef<HTMLSpanElement>(null);
const containerRef = useRef<HTMLDivElement>(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<React.SVGProps<SVGSVGElement>>;
return (
<li key={subitem.id}>
<Link
to={subitem.link || '#'}
title={subitem.name}
>
<div
ref={containerRef}
className={styles.textContainer}
>
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
{SubIcon && <SubIcon className={styles.submenuIcon} />}
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
{subitem.name}
</span>
</div>
</motion.span>
</div>
</Link>
</li>
);
})}
</ul>
</div>
<motion.span
ref={textRef}
style={{
display: 'block',
whiteSpace: 'nowrap',
}}
initial={{ x: 0 }}
animate={{ x: 0 }}
{...(isOverflowing && {
whileHover: {
x: -(textRef.current?.scrollWidth || 0) + (containerRef.current?.clientWidth || 153),
transition: {
duration: 2,
ease: "linear"
}
}
})}
>
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
{SubIcon && <SubIcon className={styles.submenuIcon} />}
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
{subitem.name}
</span>
</div>
</motion.span>
</div>
</Link>
</li>
);
})}
</ul>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>

View file

@ -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
}));
}, []);

View file

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