fix(ui): complete P5-00 shell readiness gates
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Toolbar,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||
@@ -118,7 +120,7 @@ export default function AppShell({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { language, setLanguage, t } = useI18n();
|
||||
const isMobile = useMediaQuery("(max-width:767.95px)");
|
||||
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down("md"));
|
||||
const [desktopNavCollapsed, setDesktopNavCollapsed] = useState(() => {
|
||||
try {
|
||||
return window.localStorage.getItem(DESKTOP_SIDEBAR_KEY) === "1";
|
||||
@@ -127,6 +129,22 @@ export default function AppShell({
|
||||
}
|
||||
});
|
||||
const drawerWidth = isMobile ? 254 : desktopNavCollapsed ? 84 : 254;
|
||||
const navCollapsed = !isMobile && desktopNavCollapsed;
|
||||
const [header, setHeader] = useState<HTMLElement | null>(null);
|
||||
const [headerHeight, setHeaderHeight] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile && drawerOpen) onToggleDrawer(false);
|
||||
}, [isMobile, drawerOpen, onToggleDrawer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!header || typeof ResizeObserver === "undefined") return;
|
||||
const measure = () => setHeaderHeight(Math.ceil(header.getBoundingClientRect().height));
|
||||
measure();
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(header);
|
||||
return () => observer.disconnect();
|
||||
}, [header]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -159,15 +177,15 @@ export default function AppShell({
|
||||
);
|
||||
|
||||
const renderNavList = (groups: Array<[string, NavItem[]]>) => (
|
||||
<Box sx={{ px: desktopNavCollapsed ? 0.75 : 1.25, pt: 1 }}>
|
||||
<Box sx={{ px: navCollapsed ? 0.75 : 1.25, pt: 1 }}>
|
||||
{groups.map(([section, rows]) => (
|
||||
<Box key={section || "_"} sx={{ mb: desktopNavCollapsed ? 1 : 1.25 }}>
|
||||
{section && !desktopNavCollapsed ? (
|
||||
<Box key={section || "_"} sx={{ mb: navCollapsed ? 1 : 1.25 }}>
|
||||
{section && !navCollapsed ? (
|
||||
<Typography variant="caption" sx={{ px: 1.25, color: SIDEBAR_TEXT_MUTED, fontWeight: 600, textTransform: "uppercase" }}>
|
||||
{section}
|
||||
</Typography>
|
||||
) : null}
|
||||
<List sx={{ px: desktopNavCollapsed ? 0.25 : 0.75, pt: desktopNavCollapsed ? 0.25 : 0.75 }}>
|
||||
<List sx={{ px: navCollapsed ? 0.25 : 0.75, pt: navCollapsed ? 0.25 : 0.75 }}>
|
||||
{rows.map((item) => {
|
||||
const selected = item.to === activeTo;
|
||||
return (
|
||||
@@ -175,12 +193,14 @@ export default function AppShell({
|
||||
<ListItemButton
|
||||
selected={selected}
|
||||
onClick={() => onNavigate(item.to)}
|
||||
title={desktopNavCollapsed ? item.label : undefined}
|
||||
aria-label={item.label}
|
||||
aria-current={selected ? "page" : undefined}
|
||||
title={navCollapsed ? item.label : undefined}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
minHeight: 44,
|
||||
px: desktopNavCollapsed ? 1 : 1.5,
|
||||
justifyContent: desktopNavCollapsed ? "center" : "flex-start",
|
||||
px: navCollapsed ? 1 : 1.5,
|
||||
justifyContent: navCollapsed ? "center" : "flex-start",
|
||||
border: "1px solid transparent",
|
||||
color: SIDEBAR_TEXT_MUTED,
|
||||
"&:hover": { backgroundColor: "var(--palette-navigation-hover)", color: SIDEBAR_TEXT },
|
||||
@@ -193,14 +213,14 @@ export default function AppShell({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: desktopNavCollapsed ? 0 : 36, justifyContent: "center", color: selected ? SIDEBAR_SELECTED_ICON : SIDEBAR_TEXT_MUTED }}>
|
||||
<ListItemIcon sx={{ minWidth: navCollapsed ? 0 : 36, justifyContent: "center", color: selected ? SIDEBAR_SELECTED_ICON : SIDEBAR_TEXT_MUTED }}>
|
||||
{item.badgeCount && item.badgeCount > 0 ? (
|
||||
<Badge color="error" badgeContent={item.badgeCount > 99 ? "99+" : item.badgeCount}>
|
||||
{item.icon}
|
||||
</Badge>
|
||||
) : item.icon}
|
||||
</ListItemIcon>
|
||||
{!desktopNavCollapsed ? <ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} /> : null}
|
||||
{!navCollapsed ? <ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} /> : null}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
@@ -213,10 +233,10 @@ export default function AppShell({
|
||||
|
||||
const drawerContent = (
|
||||
<Box sx={{ "--app-focus-color": "var(--palette-focus-inverse)", height: "100%", display: "flex", flexDirection: "column", backgroundColor: SIDEBAR_BG }}>
|
||||
<Box sx={{ px: desktopNavCollapsed ? 1.5 : 2.25, py: desktopNavCollapsed ? 2 : 2.5, display: "flex", justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}>
|
||||
<Box sx={{ px: navCollapsed ? 1.5 : 2.25, py: navCollapsed ? 2 : 2.5, display: "flex", justifyContent: navCollapsed ? "center" : "flex-start" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: navCollapsed ? "center" : "flex-start" }}>
|
||||
<JobbjaktMark style={{ width: 30, height: 30, flexShrink: 0 }} />
|
||||
{!desktopNavCollapsed ? (
|
||||
{!navCollapsed ? (
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: SIDEBAR_SELECTED_TEXT }}>
|
||||
Jobbjakt
|
||||
@@ -270,6 +290,7 @@ export default function AppShell({
|
||||
return (
|
||||
<Box sx={{ display: "flex", minHeight: "100vh" }}>
|
||||
<AppBar
|
||||
ref={setHeader}
|
||||
position="fixed"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
@@ -277,7 +298,7 @@ export default function AppShell({
|
||||
borderBottom: `1px solid ${muiTheme.vars.palette.grey[300]}`,
|
||||
backgroundColor: muiTheme.vars.palette.background.default,
|
||||
backgroundImage: "none",
|
||||
zIndex: muiTheme.zIndex.drawer + 1,
|
||||
zIndex: isMobile ? muiTheme.zIndex.appBar : muiTheme.zIndex.drawer + 1,
|
||||
})}
|
||||
>
|
||||
<Toolbar
|
||||
@@ -299,6 +320,8 @@ export default function AppShell({
|
||||
size="small"
|
||||
color="secondary"
|
||||
aria-label={t("openNavigation")}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={drawerOpen}
|
||||
onClick={() => onToggleDrawer(true)}
|
||||
sx={{ border: "1px solid", borderColor: "divider", borderRadius: 2.5, width: 42, height: 42 }}
|
||||
>
|
||||
@@ -311,7 +334,6 @@ export default function AppShell({
|
||||
Jobbjakt
|
||||
</Typography>
|
||||
</Box>
|
||||
{buildBadge}
|
||||
</Box>
|
||||
|
||||
<StackLanguageToggle language={language} setLanguage={setLanguage} />
|
||||
@@ -452,6 +474,12 @@ export default function AppShell({
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
{isMobile && buildVersion ? (
|
||||
<ListSubheader disableSticky role="presentation" sx={{ maxWidth: 280, py: 1, lineHeight: 1.5, overflowWrap: "anywhere" }}>
|
||||
<Typography variant="body2">{t("applicationVersion", { version: buildVersion })}</Typography>
|
||||
{buildCommit ? <Typography variant="caption">commit {buildCommit}</Typography> : null}
|
||||
</ListSubheader>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setUserMenuAnchor(null);
|
||||
@@ -484,7 +512,7 @@ export default function AppShell({
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: "none", md: "block" },
|
||||
display: isMobile ? "none" : "block",
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: {
|
||||
@@ -498,17 +526,23 @@ export default function AppShell({
|
||||
}}
|
||||
open
|
||||
>
|
||||
<Toolbar sx={{ minHeight: { xs: 68, md: 76 } }} />
|
||||
<Box aria-hidden sx={{ height: headerHeight ?? { xs: 68, md: 76 }, flexShrink: 0 }} />
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={drawerOpen}
|
||||
open={isMobile && drawerOpen}
|
||||
onClose={() => onToggleDrawer(false)}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
slotProps={{
|
||||
paper: { "aria-label": t("navigation") },
|
||||
transition: { onEntered: (node: HTMLElement) => {
|
||||
if (node === document.activeElement) node.querySelector<HTMLElement>('[role="button"]')?.focus();
|
||||
} },
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: "block", md: "none" },
|
||||
display: isMobile ? "block" : "none",
|
||||
[`& .MuiDrawer-paper`]: {
|
||||
width: drawerWidth,
|
||||
borderRight: `1px solid ${SIDEBAR_BORDER}`,
|
||||
@@ -525,13 +559,15 @@ export default function AppShell({
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: "var(--app-page-gutter)",
|
||||
// Keep the usual feedback lane stable before a delayed toast arrives.
|
||||
pb: "calc(var(--app-page-gutter) + max(96px, var(--app-toast-clearance, 0px)) + env(safe-area-inset-bottom, 0px))",
|
||||
bgcolor: "background.default",
|
||||
minHeight: "100vh",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mx: "auto", maxWidth: pageLayout ? `var(--app-page-width-${pageLayout})` : 1320, width: "100%", minWidth: 0 }}>
|
||||
<Toolbar sx={{ minHeight: { xs: isMobile ? 124 : 68, md: 76 } }} />
|
||||
<Box aria-hidden sx={{ height: headerHeight ?? { xs: isMobile ? 124 : 68, md: 76 } }} />
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75, mb: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Breadcrumbs sx={{ color: "text.secondary", mb: 0.5, '& .MuiBreadcrumbs-ol': { flexWrap: 'wrap' } }}>
|
||||
|
||||
Reference in New Issue
Block a user