feat: Implement collapsible and responsive sidebar with icon-only view and toggle functionality.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user