feat: add desktop sidebar collapse to icon-only mode (refs #176)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import React from 'react';
|
|||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useLogout } from '../core/auth/useLogout';
|
import { useLogout } from '../core/auth/useLogout';
|
||||||
import { Container, Paper, Typography, Box, IconButton, Avatar } from '@mui/material';
|
import { Container, Paper, Typography, Box, IconButton, Avatar, Tooltip } from '@mui/material';
|
||||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
|
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
|
||||||
import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded';
|
import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded';
|
||||||
import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded';
|
import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded';
|
||||||
@@ -15,7 +15,8 @@ import PlaceRoundedIcon from '@mui/icons-material/PlaceRounded';
|
|||||||
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
|
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
|
||||||
import DescriptionRoundedIcon from '@mui/icons-material/DescriptionRounded';
|
import DescriptionRoundedIcon from '@mui/icons-material/DescriptionRounded';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';
|
||||||
|
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
|
||||||
import { useAppStore } from '../core/store';
|
import { useAppStore } from '../core/store';
|
||||||
import { Button } from '../shared-minimal/components/Button';
|
import { Button } from '../shared-minimal/components/Button';
|
||||||
import { NotificationBell } from '../features/notifications';
|
import { NotificationBell } from '../features/notifications';
|
||||||
@@ -29,7 +30,7 @@ interface LayoutProps {
|
|||||||
export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false }) => {
|
export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false }) => {
|
||||||
const { user } = useAuth0();
|
const { user } = useAuth0();
|
||||||
const { logout } = useLogout();
|
const { logout } = useLogout();
|
||||||
const { sidebarOpen, toggleSidebar, setSidebarOpen } = useAppStore();
|
const { sidebarOpen, sidebarCollapsed, toggleSidebar, setSidebarOpen, toggleSidebarCollapse } = useAppStore();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Sync theme preference with backend
|
// Sync theme preference with backend
|
||||||
@@ -52,6 +53,8 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
{ name: 'Settings', href: '/garage/settings', icon: <SettingsRoundedIcon sx={{ fontSize: 20 }} /> },
|
{ name: 'Settings', href: '/garage/settings', icon: <SettingsRoundedIcon sx={{ fontSize: 20 }} /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const sidebarWidth = sidebarCollapsed ? 64 : 256;
|
||||||
|
|
||||||
// Mobile layout
|
// Mobile layout
|
||||||
if (mobileMode) {
|
if (mobileMode) {
|
||||||
return (
|
return (
|
||||||
@@ -107,29 +110,31 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
width: 256,
|
width: sidebarWidth,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
borderRight: 1,
|
borderRight: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
transform: sidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
|
transform: sidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
|
||||||
transition: 'transform 0.2s ease-in-out',
|
transition: 'transform 0.2s ease-in-out, width 0.2s ease-in-out',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: sidebarCollapsed ? 'center' : 'space-between',
|
||||||
height: 64,
|
height: 64,
|
||||||
px: 2,
|
px: sidebarCollapsed ? 1 : 2,
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
gap: 1
|
gap: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
<Box
|
<Box
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
backgroundColor: 'primary.main',
|
backgroundColor: 'primary.main',
|
||||||
@@ -140,7 +145,8 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
px: 1,
|
px: 1,
|
||||||
py: 0.5,
|
py: 0.5,
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center'
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -149,19 +155,20 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
style={{ height: 24, width: 'auto', maxWidth: 180 }}
|
style={{ height: 24, width: 'auto', maxWidth: 180 }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebarCollapse}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ color: 'text.secondary', flexShrink: 0 }}
|
sx={{ color: 'text.secondary', flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<CloseIcon />
|
{sidebarCollapsed ? <ChevronRightRoundedIcon /> : <ChevronLeftRoundedIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mt: 3, px: 2, flex: 1 }}>
|
<Box sx={{ mt: 3, px: sidebarCollapsed ? 1 : 2, flex: 1 }}>
|
||||||
{navigation.map((item) => {
|
{navigation.map((item) => {
|
||||||
const isActive = location.pathname.startsWith(item.href);
|
const isActive = location.pathname.startsWith(item.href);
|
||||||
return (
|
const navItem = (
|
||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
to={item.href}
|
to={item.href}
|
||||||
@@ -171,7 +178,8 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
px: 2,
|
justifyContent: sidebarCollapsed ? 'center' : 'flex-start',
|
||||||
|
px: sidebarCollapsed ? 1 : 2,
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
mb: 0.5,
|
mb: 0.5,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@@ -189,19 +197,47 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ mr: sidebarCollapsed ? 0 : 2, display: 'flex', alignItems: 'center' }}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
{!sidebarCollapsed && (
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
return sidebarCollapsed ? (
|
||||||
|
<Tooltip key={item.name} title={item.name} placement="right" arrow>
|
||||||
|
{navItem}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
navItem
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider', mt: 'auto' }}>
|
<Box sx={{ p: sidebarCollapsed ? 1 : 2, borderTop: 1, borderColor: 'divider', mt: 'auto' }}>
|
||||||
|
{sidebarCollapsed ? (
|
||||||
|
<Tooltip title={user?.name || user?.email || 'User'} placement="right" arrow>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
mx: 'auto',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => logout()}
|
||||||
|
>
|
||||||
|
{user?.name?.charAt(0) || user?.email?.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
sx={{
|
sx={{
|
||||||
@@ -228,13 +264,15 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
ml: sidebarOpen ? '256px' : '0',
|
ml: sidebarOpen ? `${sidebarWidth}px` : '0',
|
||||||
transition: 'margin-left 0.2s ease-in-out',
|
transition: 'margin-left 0.2s ease-in-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -255,7 +293,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
|
|||||||
px: 3
|
px: 3
|
||||||
}}>
|
}}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebarCollapse}
|
||||||
sx={{ color: 'text.secondary' }}
|
sx={{ color: 'text.secondary' }}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
|
|||||||
@@ -4,21 +4,31 @@ import { Vehicle } from '../../features/vehicles/types/vehicles.types';
|
|||||||
interface AppState {
|
interface AppState {
|
||||||
// UI state
|
// UI state
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
|
sidebarCollapsed: boolean;
|
||||||
selectedVehicle: Vehicle | null;
|
selectedVehicle: Vehicle | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
setSidebarOpen: (open: boolean) => void;
|
setSidebarOpen: (open: boolean) => void;
|
||||||
|
toggleSidebarCollapse: () => void;
|
||||||
setSelectedVehicle: (vehicle: Vehicle | null) => void;
|
setSelectedVehicle: (vehicle: Vehicle | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||||
|
|
||||||
export const useAppStore = create<AppState>((set) => ({
|
export const useAppStore = create<AppState>((set) => ({
|
||||||
// Initial state
|
// Initial state
|
||||||
sidebarOpen: false,
|
sidebarOpen: false,
|
||||||
|
sidebarCollapsed: savedCollapsed,
|
||||||
selectedVehicle: null,
|
selectedVehicle: null,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||||
setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }),
|
setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }),
|
||||||
|
toggleSidebarCollapse: () => set((state) => {
|
||||||
|
const next = !state.sidebarCollapsed;
|
||||||
|
localStorage.setItem('sidebarCollapsed', String(next));
|
||||||
|
return { sidebarCollapsed: next };
|
||||||
|
}),
|
||||||
setSelectedVehicle: (vehicle: Vehicle | null) => set({ selectedVehicle: vehicle }),
|
setSelectedVehicle: (vehicle: Vehicle | null) => set({ selectedVehicle: vehicle }),
|
||||||
}));
|
}));
|
||||||
Reference in New Issue
Block a user