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

View File

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