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:
Eric Gullickson
2026-02-13 20:07:00 -06:00
parent f987e94fed
commit bc72f09557
2 changed files with 113 additions and 65 deletions

View File

@@ -6,7 +6,7 @@ import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { Link, useLocation } from 'react-router-dom';
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 DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded';
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 DescriptionRoundedIcon from '@mui/icons-material/DescriptionRounded';
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 { Button } from '../shared-minimal/components/Button';
import { NotificationBell } from '../features/notifications';
@@ -29,7 +30,7 @@ interface LayoutProps {
export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false }) => {
const { user } = useAuth0();
const { logout } = useLogout();
const { sidebarOpen, toggleSidebar, setSidebarOpen } = useAppStore();
const { sidebarOpen, sidebarCollapsed, toggleSidebar, setSidebarOpen, toggleSidebarCollapse } = useAppStore();
const location = useLocation();
// 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 }} /> },
];
const sidebarWidth = sidebarCollapsed ? 64 : 256;
// Mobile layout
if (mobileMode) {
return (
@@ -107,29 +110,31 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
top: 0,
left: 0,
height: '100vh',
width: 256,
width: sidebarWidth,
zIndex: 1000,
borderRadius: 0,
borderRight: 1,
borderColor: 'divider',
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',
flexDirection: 'column'
flexDirection: 'column',
overflow: 'hidden',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: sidebarCollapsed ? 'center' : 'space-between',
height: 64,
px: 2,
px: sidebarCollapsed ? 1 : 2,
borderBottom: 1,
borderColor: 'divider',
gap: 1
}}
>
{!sidebarCollapsed && (
<Box
sx={(theme) => ({
backgroundColor: 'primary.main',
@@ -140,7 +145,8 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
px: 1,
py: 0.5,
display: 'inline-flex',
alignItems: 'center'
alignItems: 'center',
overflow: 'hidden',
})}
>
<img
@@ -149,19 +155,20 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
style={{ height: 24, width: 'auto', maxWidth: 180 }}
/>
</Box>
)}
<IconButton
onClick={toggleSidebar}
onClick={toggleSidebarCollapse}
size="small"
sx={{ color: 'text.secondary', flexShrink: 0 }}
>
<CloseIcon />
{sidebarCollapsed ? <ChevronRightRoundedIcon /> : <ChevronLeftRoundedIcon />}
</IconButton>
</Box>
<Box sx={{ mt: 3, px: 2, flex: 1 }}>
<Box sx={{ mt: 3, px: sidebarCollapsed ? 1 : 2, flex: 1 }}>
{navigation.map((item) => {
const isActive = location.pathname.startsWith(item.href);
return (
const navItem = (
<Link
key={item.name}
to={item.href}
@@ -171,7 +178,8 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
sx={{
display: 'flex',
alignItems: 'center',
px: 2,
justifyContent: sidebarCollapsed ? 'center' : 'flex-start',
px: sidebarCollapsed ? 1 : 2,
py: 1.5,
mb: 0.5,
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}
</Box>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{!sidebarCollapsed && (
<Typography variant="body2" sx={{ fontWeight: 500, whiteSpace: 'nowrap' }}>
{item.name}
</Typography>
)}
</Box>
</Link>
);
return sidebarCollapsed ? (
<Tooltip key={item.name} title={item.name} placement="right" arrow>
{navItem}
</Tooltip>
) : (
navItem
);
})}
</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 }}>
<Avatar
sx={{
@@ -228,13 +264,15 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
>
Sign Out
</Button>
</>
)}
</Box>
</Paper>
{/* Main content */}
<Box
sx={{
ml: sidebarOpen ? '256px' : '0',
ml: sidebarOpen ? `${sidebarWidth}px` : '0',
transition: 'margin-left 0.2s ease-in-out',
}}
>
@@ -255,7 +293,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
px: 3
}}>
<IconButton
onClick={toggleSidebar}
onClick={toggleSidebarCollapse}
sx={{ color: 'text.secondary' }}
>
<MenuIcon />

View File

@@ -4,21 +4,31 @@ import { Vehicle } from '../../features/vehicles/types/vehicles.types';
interface AppState {
// UI state
sidebarOpen: boolean;
sidebarCollapsed: boolean;
selectedVehicle: Vehicle | null;
// Actions
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
toggleSidebarCollapse: () => void;
setSelectedVehicle: (vehicle: Vehicle | null) => void;
}
const savedCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
export const useAppStore = create<AppState>((set) => ({
// Initial state
sidebarOpen: false,
sidebarCollapsed: savedCollapsed,
selectedVehicle: null,
// Actions
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
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 }),
}));