fix(ui): complete P5-00 shell readiness gates

This commit is contained in:
cesnimda
2026-09-21 21:38:11 +02:00
parent c325eb68c6
commit 926f2e5164
52 changed files with 6630 additions and 64 deletions
+56 -20
View File
@@ -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' } }}>