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.
|
||||
- [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.
|
||||
|
||||
@@ -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 {
|
||||
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",
|
||||
|
||||
@@ -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,26 +203,29 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<ListItem disablePadding sx={{ display: 'block' }}>
|
||||
<Tooltip title={isCollapsed ? item.label : ''} placement="right">
|
||||
<ListItemButton
|
||||
onClick={() => handleItemClick(item)}
|
||||
selected={isSelected}
|
||||
sx={{
|
||||
minHeight: 48,
|
||||
justifyContent: 'initial',
|
||||
justifyContent: isCollapsed ? 'center' : 'initial',
|
||||
px: 2.5,
|
||||
pl: level * 2 + 2.5,
|
||||
pl: isCollapsed ? 2.5 : level * 2 + 2.5,
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
mr: 3,
|
||||
mr: isCollapsed ? 0 : 3,
|
||||
justifyContent: 'center',
|
||||
color: isSelected ? 'primary.main' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={item.label}
|
||||
primaryTypographyProps={{
|
||||
@@ -215,9 +234,12 @@ export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
}}
|
||||
/>
|
||||
{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' }}>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user