update ui admin

This commit is contained in:
2026-06-11 20:17:18 +05:30
parent 4ad40b2c6d
commit 0736712464
51 changed files with 5466 additions and 1445 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
@@ -17,15 +17,17 @@ import {
Tooltip,
Button,
Stack,
Select,
alpha
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import LogoutIcon from '@mui/icons-material/Logout';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
@@ -33,33 +35,41 @@ import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import Logo from '@/components/Logo';
import { locations } from '@/data/mock';
import { useFilters } from '@/store/Filters';
const RED = '#C01227'; // brand accent (avatars, dots)
const BAR = '#8E1F2A'; // muted deep-brick top bar (toned down from vivid #C01227)
const RED = '#C01227'; // brand accent (reserved for attention: avatar, unread dots)
const INITIAL_NOTIFICATIONS = [
{ id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false },
{ id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false },
{ id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/fleet', read: false },
{ id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false },
{ id: 4, icon: AssignmentOutlinedIcon, title: 'MileTruth AI re-optimized 41 routes', time: '3 hrs ago', to: '/dispatch', read: true }
];
const MESSAGES = [
{ id: 1, name: 'Priya Nair', text: 'Can we reroute the MG Road batch?', time: '5 min ago', initials: 'PN' },
{ id: 2, name: 'Imran Khan', text: 'Reached the warehouse, loading now.', time: '22 min ago', initials: 'IK' },
{ id: 3, name: 'Acme Logistics', text: 'Please confirm the revised pricing.', time: '2 hrs ago', initials: 'AL' }
];
export default function Header({ onToggle }) {
const navigate = useNavigate();
const [account, setAccount] = useState(null);
const [notifAnchor, setNotifAnchor] = useState(null);
const [msgAnchor, setMsgAnchor] = useState(null);
const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS);
const [search, setSearch] = useState('');
const { location, setLocation } = useFilters(); // global location — single source of truth
const searchRef = useRef(null);
const unread = notifications.filter((n) => !n.read).length;
// ⌘K / Ctrl+K focuses global search
useEffect(() => {
const onKey = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
searchRef.current?.focus();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const openNotif = (e) => setNotifAnchor(e.currentTarget);
const closeNotif = () => setNotifAnchor(null);
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
@@ -79,76 +89,124 @@ export default function Header({ onToggle }) {
<AppBar
position="fixed"
elevation={0}
sx={{ bgcolor: BAR, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
sx={{
bgcolor: '#FFFFFF',
color: 'grey.800',
zIndex: (t) => t.zIndex.drawer + 1,
borderBottom: '1px solid',
borderColor: 'grey.200'
}}
>
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}>
<MenuIcon />
</IconButton>
{/* Brand wordmark — left side */}
<Box
onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
>
<Logo onDark height={22} />
{/* LEFT — hamburger + brand (equal-flex zone) */}
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton edge="start" onClick={onToggle} sx={{ color: 'grey.700' }}>
<MenuIcon />
</IconButton>
<Box
onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer', flexShrink: 0 }}
>
<Logo height={24} />
</Box>
</Box>
<Box sx={{ flexGrow: 1 }} />
{/* Search — moved to the right */}
{/* CENTER — global search, the primary nav element (fixed width = stays truly centered) */}
<Box
component="form"
onSubmit={submitSearch}
sx={{
display: { xs: 'none', sm: 'flex' },
flexShrink: 0,
alignItems: 'center',
bgcolor: alpha('#fff', 0.16),
borderRadius: 2,
px: 1.5,
py: 0.5,
width: { sm: 240, md: 320 },
'&:hover': { bgcolor: alpha('#fff', 0.22) },
'&:focus-within': { bgcolor: alpha('#fff', 0.26) }
height: 46,
px: 1.75,
bgcolor: 'grey.50',
border: '1px solid',
borderColor: 'grey.200',
borderRadius: 2.5,
width: { sm: 300, md: 460, lg: 560 },
transition: 'all 0.15s ease',
'&:hover': { borderColor: 'grey.300' },
'&:focus-within': { borderColor: 'primary.main', bgcolor: '#fff', boxShadow: '0 0 0 3px rgba(192,18,39,0.08)' }
}}
>
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} />
<SearchIcon sx={{ fontSize: 20, mr: 1, color: 'grey.500' }} />
<InputBase
inputRef={searchRef}
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search orders, riders, customers…"
sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }}
inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }}
placeholder="Search orders, shipments, riders, customers…"
sx={{ color: 'grey.800', fontSize: '0.875rem', flex: 1 }}
inputProps={{ 'aria-label': 'search' }}
/>
<Box
sx={{
display: { xs: 'none', md: 'flex' },
alignItems: 'center',
gap: 0.25,
px: 0.75,
py: 0.4,
ml: 1,
borderRadius: 1,
border: '1px solid',
borderColor: 'grey.300',
bgcolor: '#fff',
color: 'grey.500',
fontSize: '0.6875rem',
fontWeight: 700,
lineHeight: 1,
flexShrink: 0
}}
>
K
</Box>
</Box>
<Tooltip title="Messages">
<IconButton color="inherit" onClick={(e) => setMsgAnchor(e.currentTarget)}>
<Badge badgeContent={MESSAGES.length} color="warning">
<ChatBubbleOutlineIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Notifications">
<IconButton color="inherit" onClick={openNotif}>
<Badge badgeContent={unread} color="warning">
<NotificationsNoneIcon />
</Badge>
</IconButton>
</Tooltip>
{/* RIGHT — location + notifications + profile (equal-flex zone, right-aligned) */}
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5 }}>
<Select
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
IconComponent={KeyboardArrowDownIcon}
startAdornment={<PlaceOutlinedIcon sx={{ fontSize: 18, color: 'grey.500', mr: 0.75 }} />}
sx={{
display: { xs: 'none', md: 'flex' },
minWidth: 168,
bgcolor: 'grey.50',
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'grey.200' },
'& .MuiSelect-select': { display: 'flex', alignItems: 'center', fontSize: '0.8125rem', fontWeight: 600, color: 'grey.700' }
}}
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => (
<MenuItem key={l} value={l}>{l}</MenuItem>
))}
</Select>
<Box
onClick={(e) => setAccount(e.currentTarget)}
sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }}
>
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>AD</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}>
Aman Deshmukh
</Typography>
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}>
Operations Admin
</Typography>
<Tooltip title="Notifications">
<IconButton onClick={openNotif} sx={{ color: 'grey.700' }}>
<Badge badgeContent={unread} color="error">
<NotificationsNoneIcon />
</Badge>
</IconButton>
</Tooltip>
<Box
onClick={(e) => setAccount(e.currentTarget)}
sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: 'grey.100' } }}
>
<Avatar sx={{ width: 34, height: 34, bgcolor: RED, color: '#fff', fontWeight: 700, fontSize: '0.8rem' }}>AD</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: 'grey.800', fontWeight: 600 }}>
Aman Deshmukh
</Typography>
<Typography variant="caption" sx={{ color: 'grey.500' }}>
Operations Admin
</Typography>
</Box>
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: 'grey.500', display: { xs: 'none', md: 'block' } }} />
</Box>
</Box>
@@ -180,7 +238,7 @@ export default function Header({ onToggle }) {
return (
<MenuItem key={n.id} onClick={() => onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.200' : alpha(RED, 0.12), color: RED }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.100' : alpha(RED, 0.12), color: n.read ? 'grey.500' : RED }}>
<Icon fontSize="small" />
</Avatar>
</ListItemIcon>
@@ -200,39 +258,6 @@ export default function Header({ onToggle }) {
</MenuItem>
</Menu>
{/* Messages dropdown */}
<Menu
anchorEl={msgAnchor}
open={Boolean(msgAnchor)}
onClose={() => setMsgAnchor(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }}
>
<Typography variant="subtitle1" sx={{ fontWeight: 700, px: 2, py: 1.25 }}>
Messages
</Typography>
<Divider />
{MESSAGES.map((m) => (
<MenuItem key={m.id} onClick={() => setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, fontSize: '0.8rem' }}>
{m.initials}
</Avatar>
</ListItemIcon>
<ListItemText
primary={m.name}
secondary={m.text}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 700 }}
secondaryTypographyProps={{ fontSize: '0.8rem' }}
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, mt: 0.5, flexShrink: 0 }}>
{m.time}
</Typography>
</MenuItem>
))}
</Menu>
{/* Account dropdown */}
<Menu
anchorEl={account}

View File

@@ -1,4 +1,3 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Drawer,
@@ -8,52 +7,62 @@ import {
ListItemIcon,
ListItemText,
Typography,
Collapse,
Tooltip,
Toolbar
} from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import navItems from '@/menu/navItems';
import Logo from '@/components/Logo';
export const DRAWER_WIDTH = 232;
export const MINI_WIDTH = 76;
export const DRAWER_WIDTH = 200;
export const MINI_WIDTH = 68;
const NAV_BG = '#8E1F2A'; // muted deep-brick brand red (toned down from vivid #C01227)
const BRAND = '#C01227';
function NavLeaf({ item, open, active, depth = 0, onClick }) {
// Light, operational sidebar — white surface, tight grouping, a single strong red active state.
function NavLeaf({ item, open, active, onClick }) {
const Icon = item.icon;
const button = (
<ListItemButton
selected={active}
onClick={onClick}
sx={{
minHeight: 44,
my: 0.25,
mx: open ? 1 : 0.75,
px: open ? 1.5 : 0,
position: 'relative',
minHeight: 40,
my: 0.2,
mx: open ? 0.75 : 0.5,
px: open ? 1.25 : 0,
justifyContent: open ? 'flex-start' : 'center',
borderRadius: 2,
color: 'rgba(255,255,255,0.78)',
'& .MuiListItemIcon-root': { color: 'inherit' },
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' },
borderRadius: 1.5,
color: 'grey.800',
transition: 'background-color .12s ease, color .12s ease',
'& .MuiListItemIcon-root': { color: '#B0566A', transition: 'color .12s ease' },
'&:hover': { bgcolor: 'rgba(192,18,39,0.07)', color: BRAND, '& .MuiListItemIcon-root': { color: BRAND } },
'&.Mui-selected': {
bgcolor: 'rgba(255,255,255,0.18)',
color: '#fff',
'&:hover': { bgcolor: 'rgba(255,255,255,0.22)' }
bgcolor: 'rgba(192,18,39,0.14)',
color: BRAND,
'& .MuiListItemIcon-root': { color: BRAND },
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 5,
bottom: 5,
width: 3,
borderRadius: '0 3px 3px 0',
bgcolor: BRAND
},
'&:hover': { bgcolor: 'rgba(192,18,39,0.18)' }
}
}}
>
<ListItemIcon sx={{ minWidth: open ? 34 : 'auto', justifyContent: 'center' }}>
{depth > 0 && !Icon ? <FiberManualRecordIcon sx={{ fontSize: 8 }} /> : Icon ? <Icon fontSize="small" /> : null}
<ListItemIcon sx={{ minWidth: open ? 30 : 'auto', justifyContent: 'center' }}>
<Icon sx={{ fontSize: 20 }} />
</ListItemIcon>
{open && (
<ListItemText
primary={item.title}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: active ? 700 : 500 }}
primaryTypographyProps={{ fontSize: '0.82rem', fontWeight: active ? 700 : 500, letterSpacing: '0.01em' }}
/>
)}
</ListItemButton>
@@ -68,122 +77,66 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
const isActive = (url) => url && location.pathname.startsWith(url);
const expanded = open || isMobile;
const initialOpen = navItems
.flatMap((g) => g.items)
.filter((i) => i.children && i.children.some((c) => isActive(c.url)))
.map((i) => i.id);
const [collapse, setCollapse] = useState(initialOpen);
const go = (url) => {
navigate(url);
if (isMobile) onMobileClose();
};
const content = (
<Box sx={{ bgcolor: NAV_BG, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}>
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}>
<Logo onDark compact={!expanded} />
<Box sx={{ bgcolor: '#FFF5F5', height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* brand — restrained height, generous top alignment */}
<Toolbar
sx={{
px: expanded ? 2.25 : 0,
pt: 0.5,
justifyContent: expanded ? 'flex-start' : 'center',
minHeight: 60,
borderBottom: '1px solid',
borderColor: 'rgba(192,18,39,0.10)'
}}
>
<Logo compact={!expanded} height={16} />
</Toolbar>
{/* navigation */}
<Box
sx={{
overflowY: 'auto',
overflowX: 'hidden',
flexGrow: 1,
pb: 2,
// slim, subtle scrollbar tuned for the dark-red sidebar — only shows on hover
py: 1,
scrollbarWidth: 'thin',
scrollbarColor: 'transparent transparent',
'&:hover': { scrollbarColor: 'rgba(255,255,255,0.3) transparent' },
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { background: 'transparent' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'transparent',
borderRadius: 8,
transition: 'background-color 0.2s ease'
},
'&:hover::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(255,255,255,0.28)' },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'rgba(255,255,255,0.45)' }
'&::-webkit-scrollbar-thumb': { backgroundColor: 'transparent', borderRadius: 8 },
'&:hover::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(0,0,0,0.18)' }
}}
>
{navItems.map((grp) => (
<Box key={grp.group} sx={{ mt: 1 }}>
{expanded && (
<Box key={grp.group || grp.items[0].id} sx={{ mt: grp.group ? 1.25 : 0.25 }}>
{expanded && grp.group && (
<Typography
variant="overline"
sx={{ px: 2.5, color: 'rgba(255,255,255,0.55)', fontSize: '0.6875rem', letterSpacing: '0.08em' }}
sx={{
display: 'block',
px: 2,
mb: 0.25,
color: 'rgba(158,14,32,0.62)',
fontSize: '0.6875rem',
fontWeight: 800,
textTransform: 'uppercase',
letterSpacing: '0.13em'
}}
>
{grp.group}
</Typography>
)}
<List disablePadding sx={{ mt: 0.5 }}>
{grp.items.map((item) => {
if (item.children) {
const opened = collapse.includes(item.id);
const childActive = item.children.some((c) => isActive(c.url));
const Icon = item.icon;
const head = (
<ListItemButton
onClick={() =>
expanded
? setCollapse((p) => (p.includes(item.id) ? p.filter((x) => x !== item.id) : [...p, item.id]))
: go(item.children[0].url)
}
sx={{
minHeight: 44,
my: 0.25,
mx: expanded ? 1 : 0.75,
px: expanded ? 1.5 : 0,
justifyContent: expanded ? 'flex-start' : 'center',
borderRadius: 2,
color: childActive ? '#fff' : 'rgba(255,255,255,0.78)',
bgcolor: childActive && !opened ? 'rgba(255,255,255,0.12)' : 'transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' }
}}
>
<ListItemIcon sx={{ minWidth: expanded ? 34 : 'auto', justifyContent: 'center', color: 'inherit' }}>
<Icon fontSize="small" />
</ListItemIcon>
{expanded && (
<>
<ListItemText primary={item.title} primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 500 }} />
{opened ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
</>
)}
</ListItemButton>
);
return (
<Box key={item.id}>
{expanded ? head : <Tooltip title={item.title} placement="right">{head}</Tooltip>}
{expanded && (
<Collapse in={opened} timeout="auto" unmountOnExit>
<Box sx={{ pl: 1.5 }}>
{item.children.map((c) => (
<NavLeaf key={c.id} item={c} open depth={1} active={isActive(c.url)} onClick={() => go(c.url)} />
))}
</Box>
</Collapse>
)}
</Box>
);
}
return (
<NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
);
})}
<List disablePadding>
{grp.items.map((item) => (
<NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
))}
</List>
</Box>
))}
</Box>
{expanded && (
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.12)' }}>
<Typography variant="caption" sx={{ color: '#fff', fontWeight: 600, display: 'block', lineHeight: 1.3 }}>
Delivering Trust.
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
Beyond Boundaries · v1.0
</Typography>
</Box>
)}
</Box>
);
@@ -201,23 +154,24 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
);
}
// Desktop: an in-flow grid-column element (NOT a fixed Drawer paper) so it can never be clipped
// or overlap the content. Width comes from the layout grid; this just fills its column and pins
// to the viewport while content scrolls.
return (
<Drawer
variant="permanent"
<Box
component="aside"
sx={{
width: open ? DRAWER_WIDTH : MINI_WIDTH,
flexShrink: 0,
whiteSpace: 'nowrap',
'& .MuiDrawer-paper': {
width: open ? DRAWER_WIDTH : MINI_WIDTH,
border: 'none',
overflowX: 'hidden',
transition: (t) => t.transitions.create('width', { duration: t.transitions.duration.standard })
}
gridColumn: 1,
position: 'sticky',
top: 0,
height: '100vh',
overflow: 'hidden',
borderRight: '1px solid',
borderColor: 'rgba(192,18,39,0.14)',
boxShadow: '1px 0 4px rgba(192,18,39,0.05)'
}}
open={open}
>
{content}
</Drawer>
</Box>
);
}

View File

@@ -6,9 +6,16 @@ import { useTheme } from '@mui/material/styles';
import Header from './Header';
import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar';
// ==============================|| MAIN LAYOUT — CSS GRID SHELL ||============================== //
// One deterministic grid: [ sidebar (fixed px) | content (flexible, minWidth 0) ].
// The sidebar width is the single source of truth (exported from Sidebar.jsx). No width-calc
// hacks, no negative margins, no fixed-paper overlap — the content column begins exactly where
// the sidebar ends, the sidebar can never be clipped, and content can never overflow horizontally.
export default function MainLayout() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('lg'));
// Sidebar stays a permanent column on md+ (and through browser zoom); overlay drawer below md.
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
@@ -17,26 +24,27 @@ export default function MainLayout() {
else setOpen((p) => !p);
};
const sidebarW = open ? DRAWER_WIDTH : MINI_WIDTH;
return (
<Box sx={{ display: 'flex', bgcolor: 'background.default', minHeight: '100vh' }}>
<Box
sx={{
minHeight: '100vh',
bgcolor: 'background.default',
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: `${sidebarW}px minmax(0, 1fr)` },
transition: theme.transitions.create('grid-template-columns', { duration: theme.transitions.duration.standard })
}}
>
<Header onToggle={toggle} />
<Sidebar
open={open}
isMobile={isMobile}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
/>
<Box
component="main"
sx={{
flexGrow: 1,
width: { lg: `calc(100% - ${open ? DRAWER_WIDTH : MINI_WIDTH}px)` },
minHeight: '100vh',
transition: theme.transitions.create('width', { duration: theme.transitions.duration.standard })
}}
>
<Toolbar sx={{ minHeight: 64 }} />
<Box sx={{ p: { xs: 2, sm: 3 } }}>
{/* column 1 on md+ (in-flow); overlay drawer on mobile (taken out of flow) */}
<Sidebar open={open} isMobile={isMobile} mobileOpen={mobileOpen} onMobileClose={() => setMobileOpen(false)} />
{/* column 2 — content; minWidth:0 + overflowX:clip guarantee no horizontal spill */}
<Box component="main" sx={{ minWidth: 0, minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Toolbar sx={{ minHeight: 64, flexShrink: 0 }} />
<Box sx={{ p: { xs: 2, sm: 3 }, flexGrow: 1, minWidth: 0, overflowX: 'clip' }}>
<Outlet />
</Box>
</Box>