From bc72f09557da6c2dba34a1003cddc93e911f0a89 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:07:00 -0600 Subject: [PATCH] feat: add desktop sidebar collapse to icon-only mode (refs #176) Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/Layout.tsx | 168 ++++++++++++++++++----------- frontend/src/core/store/app.ts | 10 ++ 2 files changed, 113 insertions(+), 65 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index ada37fc..3fd08f1 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -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 = ({ 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 = ({ children, mobileMode = false }) { name: 'Settings', href: '/garage/settings', icon: }, ]; + const sidebarWidth = sidebarCollapsed ? 64 : 256; + // Mobile layout if (mobileMode) { return ( @@ -107,61 +110,65 @@ export const Layout: React.FC = ({ 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', }} > - ({ - backgroundColor: 'primary.main', - ...theme.applyStyles('dark', { - backgroundColor: 'transparent', - }), - borderRadius: 0.5, - px: 1, - py: 0.5, - display: 'inline-flex', - alignItems: 'center' - })} - > - MotoVaultPro - + {!sidebarCollapsed && ( + ({ + backgroundColor: 'primary.main', + ...theme.applyStyles('dark', { + backgroundColor: 'transparent', + }), + borderRadius: 0.5, + px: 1, + py: 0.5, + display: 'inline-flex', + alignItems: 'center', + overflow: 'hidden', + })} + > + MotoVaultPro + + )} - + {sidebarCollapsed ? : } - + {navigation.map((item) => { const isActive = location.pathname.startsWith(item.href); - return ( + const navItem = ( = ({ 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,52 +197,82 @@ export const Layout: React.FC = ({ children, mobileMode = false }) } }} > - + {item.icon} - - {item.name} - + {!sidebarCollapsed && ( + + {item.name} + + )} ); + return sidebarCollapsed ? ( + + {navItem} + + ) : ( + navItem + ); })} - - - - {user?.name?.charAt(0) || user?.email?.charAt(0)} - - - - {user?.name || user?.email} - - - - + + {sidebarCollapsed ? ( + + logout()} + > + {user?.name?.charAt(0) || user?.email?.charAt(0)} + + + ) : ( + <> + + + {user?.name?.charAt(0) || user?.email?.charAt(0)} + + + + {user?.name || user?.email} + + + + + + )} {/* Main content */} @@ -255,7 +293,7 @@ export const Layout: React.FC = ({ children, mobileMode = false }) px: 3 }}> diff --git a/frontend/src/core/store/app.ts b/frontend/src/core/store/app.ts index a1d7e53..165330b 100644 --- a/frontend/src/core/store/app.ts +++ b/frontend/src/core/store/app.ts @@ -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((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 }), })); \ No newline at end of file