feat: Implement collapsible and responsive sidebar with icon-only view and toggle functionality.

This commit is contained in:
2025-12-06 01:04:42 +01:00
parent 4c72030687
commit 4db05100cf
4 changed files with 121 additions and 41 deletions

View File

@@ -36,3 +36,5 @@ File riassuntivo dello stato di sviluppo di Zentral.
- Rimozione tab interne e header dal modulo Magazzino per uniformità con la UI principale.
- [2025-12-05 Live Data Alignment](./devlog/2025-12-05-230000_live_data_alignment.md) - **Completato**
- Implementazione `SchemaDiscoveryService` per allineamento automatico dataset report con strutture dati live.
- [2025-12-06 Sidebar Collapsible](./devlog/2025-12-06-010500_sidebar_collapsible.md) - **Completato**
- Reso il menu laterale collassabile (manuale e responsive) con visualizzazione a sole icone.

View File

@@ -0,0 +1,30 @@
# Sidebar Collapsible and Responsive
## Obiettivo
Rendere il menu laterale (Sidebar) collassabile manualmente e automaticamente responsive (si chiude se la finestra si riduce).
## Stato
Completato.
## Modifiche Apportate
### Frontend
- **`src/frontend/src/components/Layout.tsx`**:
- Aggiunto stato `isCollapsed`.
- Aggiunto hook `useMediaQuery` per rilevare la larghezza dello schermo (`md` breakpoint).
- Implementata logica `useEffect` per collassare automaticamente la sidebar su schermi medi (tra `sm` e `md`).
- Aggiornato il calcolo della larghezza dinamica (`currentDrawerWidth`) per `AppBar`, `Drawer` e `Main Content`.
- Aggiunta transizione CSS per un'animazione fluida.
- **`src/frontend/src/components/Sidebar.tsx`**:
- Aggiunto pulsante di toggle (freccia sinistra/destra) nell'header della sidebar.
- Implementata la modalità "collassata":
- Nasconde i testi (`ListItemText`).
- Nasconde le icone di espansione (`ExpandLess`/`ExpandMore`).
- Centra le icone (`ListItemIcon`).
- Aggiunge `Tooltip` al passaggio del mouse per mostrare l'etichetta del menu.
- Gestione click in modalità collassata: se si clicca una voce con sottomenu, la sidebar si espande automaticamente e apre il sottomenu.
## Verifica
- Testato il toggle manuale.
- Testato il comportamento responsive (simulato tramite logica breakpoint).
- Verificato che i tooltip appaiano correttamente in modalità collassata.

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Outlet, useLocation } from "react-router-dom";
import {
Box,
@@ -19,6 +19,7 @@ import SearchBar from "./SearchBar";
import TabsBar from "./TabsBar";
const DRAWER_WIDTH = 280; // Increased width for better readability
const COLLAPSED_DRAWER_WIDTH = 72;
export default function Layout() {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -27,6 +28,19 @@ export default function Layout() {
// Breakpoints
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px
const isMdUp = useMediaQuery(theme.breakpoints.up("md")); // >= 900px
const [isCollapsed, setIsCollapsed] = useState(false);
useEffect(() => {
setIsCollapsed(!isMdUp);
}, [isMdUp]);
const handleCollapseToggle = () => {
setIsCollapsed(!isCollapsed);
};
const currentDrawerWidth = isCollapsed ? COLLAPSED_DRAWER_WIDTH : DRAWER_WIDTH;
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
@@ -37,8 +51,8 @@ export default function Layout() {
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${DRAWER_WIDTH}px)` },
ml: { sm: `${DRAWER_WIDTH}px` },
width: { sm: `calc(100% - ${currentDrawerWidth}px)` },
ml: { sm: `${currentDrawerWidth}px` },
boxShadow: 1,
zIndex: (theme) => theme.zIndex.drawer + 1,
}}
@@ -69,7 +83,7 @@ export default function Layout() {
<Box
component="nav"
sx={{
width: { sm: DRAWER_WIDTH },
width: { sm: currentDrawerWidth },
flexShrink: { sm: 0 },
}}
>
@@ -96,12 +110,17 @@ export default function Layout() {
display: { xs: "none", sm: "block" },
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: DRAWER_WIDTH,
width: currentDrawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
},
}}
open
>
<Sidebar />
<Sidebar isCollapsed={isCollapsed} onToggleCollapse={handleCollapseToggle} />
</Drawer>
</Box>
@@ -112,7 +131,7 @@ export default function Layout() {
flexGrow: 1,
width: {
xs: "100vw",
sm: `calc(100vw - ${DRAWER_WIDTH}px)`,
sm: `calc(100vw - ${currentDrawerWidth}px)`,
},
height: "100vh",
display: "flex",

View File

@@ -9,6 +9,8 @@ import {
Box,
Typography,
Divider,
Tooltip,
IconButton,
} from '@mui/material';
import {
ExpandLess,
@@ -36,6 +38,8 @@ import {
Category as CategoryIcon,
AttachMoney as AttachMoneyIcon,
Receipt as ReceiptIcon,
ChevronLeft,
ChevronRight,
} from '@mui/icons-material';
import { useLocation } from 'react-router-dom';
import { useLanguage } from '../contexts/LanguageContext';
@@ -52,7 +56,13 @@ interface MenuItem {
appCode?: string;
}
export default function Sidebar({ onClose }: { onClose?: () => void }) {
interface SidebarProps {
onClose?: () => void;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
}
export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse }: SidebarProps) {
const { t } = useLanguage();
const { activeApps } = useApps();
const { openTab } = useTabs();
@@ -73,6 +83,12 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
};
const handleItemClick = (item: MenuItem) => {
if (isCollapsed && item.children && onToggleCollapse) {
onToggleCollapse();
setOpenItems((prev) => ({ ...prev, [item.id]: true }));
return;
}
if (item.path) {
openTab(item.path, item.tabLabel || item.label);
if (onClose) onClose();
@@ -187,37 +203,43 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
return (
<React.Fragment key={item.id}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
onClick={() => handleItemClick(item)}
selected={isSelected}
sx={{
minHeight: 48,
justifyContent: 'initial',
px: 2.5,
pl: level * 2 + 2.5,
}}
>
<ListItemIcon
<Tooltip title={isCollapsed ? item.label : ''} placement="right">
<ListItemButton
onClick={() => handleItemClick(item)}
selected={isSelected}
sx={{
minWidth: 0,
mr: 3,
justifyContent: 'center',
color: isSelected ? 'primary.main' : 'inherit',
minHeight: 48,
justifyContent: isCollapsed ? 'center' : 'initial',
px: 2.5,
pl: isCollapsed ? 2.5 : level * 2 + 2.5,
}}
>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontWeight: isSelected ? 'bold' : 'medium',
fontSize: level === 0 ? '0.95rem' : '0.9rem',
}}
/>
{hasChildren ? (isOpen ? <ExpandLess /> : <ExpandMore />) : null}
</ListItemButton>
<ListItemIcon
sx={{
minWidth: 0,
mr: isCollapsed ? 0 : 3,
justifyContent: 'center',
color: isSelected ? 'primary.main' : 'inherit',
}}
>
{item.icon}
</ListItemIcon>
{!isCollapsed && (
<>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontWeight: isSelected ? 'bold' : 'medium',
fontSize: level === 0 ? '0.95rem' : '0.9rem',
}}
/>
{hasChildren ? (isOpen ? <ExpandLess /> : <ExpandMore />) : null}
</>
)}
</ListItemButton>
</Tooltip>
</ListItem>
{hasChildren && (
{!isCollapsed && hasChildren && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.children?.map((child) => renderMenuItem(child, level + 1))}
@@ -229,14 +251,21 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
};
return (
<Box sx={{ overflow: 'auto' }}>
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
Zentral
</Typography>
<Box sx={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: isCollapsed ? 'center' : 'space-between', minHeight: 64 }}>
{!isCollapsed && (
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
Zentral
</Typography>
)}
{onToggleCollapse && (
<IconButton onClick={onToggleCollapse}>
{isCollapsed ? <ChevronRight /> : <ChevronLeft />}
</IconButton>
)}
</Box>
<Divider />
<List>
<List sx={{ overflowY: 'auto', flex: 1 }}>
{menuStructure.map((item) => renderMenuItem(item))}
</List>
</Box>