From fdcd565431e3e2c5815f741966311200de251040 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 21:31:54 +0100 Subject: [PATCH] Add base authenticated route --- matrixgw_frontend/src/App.tsx | 2 +- .../src/widgets/BaseAuthenticatedPage.tsx | 3 - .../dashboard/BaseAuthenticatedPage.tsx | 96 ++++++ .../src/widgets/dashboard/DashboardHeader.tsx | 130 ++++++++ .../widgets/dashboard/DashboardSidebar.tsx | 281 ++++++++++++++++++ .../dashboard/DashboardSidebarContext.tsx | 5 + .../dashboard/DashboardSidebarDividerItem.tsx | 28 ++ .../dashboard/DashboardSidebarHeaderItem.tsx | 46 +++ .../dashboard/DashboardSidebarPageItem.tsx | 253 ++++++++++++++++ .../widgets/dashboard/SidebarDividerItem.tsx | 28 ++ .../src/widgets/dashboard/ThemeSwitcher.tsx | 59 ++++ .../src/widgets/dashboard/constants.ts | 2 + .../src/widgets/dashboard/mixins.ts | 23 ++ 13 files changed, 952 insertions(+), 4 deletions(-) delete mode 100644 matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/constants.ts create mode 100644 matrixgw_frontend/src/widgets/dashboard/mixins.ts diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 4b0d983..22acab2 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -11,8 +11,8 @@ import { LoginRoute } from "./routes/auth/LoginRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { HomeRoute } from "./routes/HomeRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; -import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; +import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; interface AuthContext { signedIn: boolean; diff --git a/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx deleted file mode 100644 index 79607a4..0000000 --- a/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function BaseAuthenticatedPage(): React.ReactElement { - return

todo authenticated

; -} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx new file mode 100644 index 0000000..7973152 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -0,0 +1,96 @@ +import { mdiMessageTextFast } from "@mdi/js"; +import Icon from "@mdi/react"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import Toolbar from "@mui/material/Toolbar"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import * as React from "react"; +import { Outlet } from "react-router"; +import DashboardHeader from "./DashboardHeader"; +import DashboardSidebar from "./DashboardSidebar"; + +export default function BaseAuthenticatedPage(): React.ReactElement { + const theme = useTheme(); + + const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = + React.useState(true); + const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = + React.useState(false); + + const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); + + const isNavigationExpanded = isOverMdViewport + ? isDesktopNavigationExpanded + : isMobileNavigationExpanded; + + const setIsNavigationExpanded = React.useCallback( + (newExpanded: boolean) => { + if (isOverMdViewport) { + setIsDesktopNavigationExpanded(newExpanded); + } else { + setIsMobileNavigationExpanded(newExpanded); + } + }, + [ + isOverMdViewport, + setIsDesktopNavigationExpanded, + setIsMobileNavigationExpanded, + ] + ); + + const handleToggleHeaderMenu = React.useCallback( + (isExpanded: boolean) => { + setIsNavigationExpanded(isExpanded); + }, + [setIsNavigationExpanded] + ); + + const layoutRef = React.useRef(null); + + return ( + + } + title="" + menuOpen={isNavigationExpanded} + onToggleMenu={handleToggleHeaderMenu} + /> + + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx new file mode 100644 index 0000000..5c98db7 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -0,0 +1,130 @@ +import * as React from "react"; +import { styled, useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import MuiAppBar from "@mui/material/AppBar"; +import IconButton from "@mui/material/IconButton"; +import Toolbar from "@mui/material/Toolbar"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import MenuIcon from "@mui/icons-material/Menu"; +import MenuOpenIcon from "@mui/icons-material/MenuOpen"; +import Stack from "@mui/material/Stack"; +import { Link } from "react-router"; +import ThemeSwitcher from "./ThemeSwitcher"; + +const AppBar = styled(MuiAppBar)(({ theme }) => ({ + borderWidth: 0, + borderBottomWidth: 1, + borderStyle: "solid", + borderColor: (theme.vars ?? theme).palette.divider, + boxShadow: "none", + zIndex: theme.zIndex.drawer + 1, +})); + +const LogoContainer = styled("div")({ + position: "relative", + height: 40, + display: "flex", + alignItems: "center", + "& img": { + maxHeight: 40, + }, +}); + +export interface DashboardHeaderProps { + logo?: React.ReactNode; + title?: string; + menuOpen: boolean; + onToggleMenu: (open: boolean) => void; +} + +export default function DashboardHeader({ + logo, + title, + menuOpen, + onToggleMenu, +}: DashboardHeaderProps) { + const theme = useTheme(); + + const handleMenuOpen = React.useCallback(() => { + onToggleMenu(!menuOpen); + }, [menuOpen, onToggleMenu]); + + const getMenuIcon = React.useCallback( + (isExpanded: boolean) => { + const expandMenuActionText = "Expand"; + const collapseMenuActionText = "Collapse"; + + return ( + +
+ + {isExpanded ? : } + +
+
+ ); + }, + [handleMenuOpen] + ); + + return ( + + + + + {getMenuIcon(menuOpen)} + + + {logo ? {logo} : null} + {title ? ( + + {title} + + ) : null} + + + + + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx new file mode 100644 index 0000000..26111f6 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -0,0 +1,281 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import Toolbar from "@mui/material/Toolbar"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import PersonIcon from "@mui/icons-material/Person"; +import BarChartIcon from "@mui/icons-material/BarChart"; +import DescriptionIcon from "@mui/icons-material/Description"; +import LayersIcon from "@mui/icons-material/Layers"; +import { matchPath, useLocation } from "react-router"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants"; +import DashboardSidebarPageItem from "./DashboardSidebarPageItem"; +import DashboardSidebarHeaderItem from "./DashboardSidebarHeaderItem"; +import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem"; +import { + getDrawerSxTransitionMixin, + getDrawerWidthTransitionMixin, +} from "./mixins"; + +export interface DashboardSidebarProps { + expanded?: boolean; + setExpanded: (expanded: boolean) => void; + disableCollapsibleSidebar?: boolean; + container?: Element; +} + +export default function DashboardSidebar({ + expanded = true, + setExpanded, + disableCollapsibleSidebar = false, + container, +}: DashboardSidebarProps) { + const theme = useTheme(); + + const { pathname } = useLocation(); + + const [expandedItemIds, setExpandedItemIds] = React.useState([]); + + const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); + const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); + + const [isFullyExpanded, setIsFullyExpanded] = React.useState(expanded); + const [isFullyCollapsed, setIsFullyCollapsed] = React.useState(!expanded); + + React.useEffect(() => { + if (expanded) { + const drawerWidthTransitionTimeout = setTimeout(() => { + setIsFullyExpanded(true); + }, theme.transitions.duration.enteringScreen); + + return () => clearTimeout(drawerWidthTransitionTimeout); + } + + setIsFullyExpanded(false); + + return () => {}; + }, [expanded, theme.transitions.duration.enteringScreen]); + + React.useEffect(() => { + if (!expanded) { + const drawerWidthTransitionTimeout = setTimeout(() => { + setIsFullyCollapsed(true); + }, theme.transitions.duration.leavingScreen); + + return () => clearTimeout(drawerWidthTransitionTimeout); + } + + setIsFullyCollapsed(false); + + return () => {}; + }, [expanded, theme.transitions.duration.leavingScreen]); + + const mini = !disableCollapsibleSidebar && !expanded; + + const handleSetSidebarExpanded = React.useCallback( + (newExpanded: boolean) => () => { + setExpanded(newExpanded); + }, + [setExpanded] + ); + + const handlePageItemClick = React.useCallback( + (itemId: string, hasNestedNavigation: boolean) => { + if (hasNestedNavigation && !mini) { + setExpandedItemIds((previousValue) => + previousValue.includes(itemId) + ? previousValue.filter( + (previousValueItemId) => previousValueItemId !== itemId + ) + : [...previousValue, itemId] + ); + } else if (!isOverSmViewport && !hasNestedNavigation) { + setExpanded(false); + } + }, + [mini, setExpanded, isOverSmViewport] + ); + + const hasDrawerTransitions = + isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); + + const getDrawerContent = React.useCallback( + (viewport: "phone" | "tablet" | "desktop") => ( + + + + + Main items + } + href="/employees" + selected={ + !!matchPath("/employees/*", pathname) || pathname === "/" + } + /> + + + Example items + + } + href="/reports" + selected={!!matchPath("/reports", pathname)} + defaultExpanded={!!matchPath("/reports", pathname)} + expanded={expandedItemIds.includes("reports")} + nestedNavigation={ + + } + href="/reports/sales" + selected={!!matchPath("/reports/sales", pathname)} + /> + } + href="/reports/traffic" + selected={!!matchPath("/reports/traffic", pathname)} + /> + + } + /> + } + href="/integrations" + selected={!!matchPath("/integrations", pathname)} + /> + + + + ), + [mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname] + ); + + const getDrawerSharedSx = React.useCallback( + (isTemporary: boolean) => { + const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH; + + return { + displayPrint: "none", + width: drawerWidth, + flexShrink: 0, + ...getDrawerWidthTransitionMixin(expanded), + ...(isTemporary ? { position: "absolute" } : {}), + [`& .MuiDrawer-paper`]: { + position: "absolute", + width: drawerWidth, + boxSizing: "border-box", + backgroundImage: "none", + ...getDrawerWidthTransitionMixin(expanded), + }, + }; + }, + [expanded, mini] + ); + + const sidebarContextValue = React.useMemo(() => { + return { + onPageItemClick: handlePageItemClick, + mini, + fullyExpanded: isFullyExpanded, + fullyCollapsed: isFullyCollapsed, + hasDrawerTransitions, + }; + }, [ + handlePageItemClick, + mini, + isFullyExpanded, + isFullyCollapsed, + hasDrawerTransitions, + ]); + + return ( + + + {getDrawerContent("phone")} + + + {getDrawerContent("tablet")} + + + {getDrawerContent("desktop")} + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx new file mode 100644 index 0000000..56efe06 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -0,0 +1,5 @@ +import * as React from "react"; + +const DashboardSidebarContext = React.createContext(null); + +export default DashboardSidebarContext; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx new file mode 100644 index 0000000..d5e3178 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Divider from "@mui/material/Divider"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export default function DashboardSidebarDividerItem() { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext; + + return ( +
  • + +
  • + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx new file mode 100644 index 0000000..d4dc3ce --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import ListSubheader from "@mui/material/ListSubheader"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { DRAWER_WIDTH } from "./constants"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export interface DashboardSidebarHeaderItemProps { + children?: React.ReactNode; +} + +export default function DashboardSidebarHeaderItem({ + children, +}: DashboardSidebarHeaderItemProps) { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { + mini = false, + fullyExpanded = true, + hasDrawerTransitions, + } = sidebarContext; + + return ( + + {children} + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx new file mode 100644 index 0000000..637db5b --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -0,0 +1,253 @@ +import * as React from "react"; +import { type Theme, type SxProps } from "@mui/material/styles"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Collapse from "@mui/material/Collapse"; +import Grow from "@mui/material/Grow"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Link } from "react-router"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { MINI_DRAWER_WIDTH } from "./constants"; + +export interface DashboardSidebarPageItemProps { + id: string; + title: string; + icon?: React.ReactNode; + href: string; + action?: React.ReactNode; + defaultExpanded?: boolean; + expanded?: boolean; + selected?: boolean; + disabled?: boolean; + nestedNavigation?: React.ReactNode; +} + +export default function DashboardSidebarPageItem({ + id, + title, + icon, + href, + action, + defaultExpanded = false, + expanded = defaultExpanded, + selected = false, + disabled = false, + nestedNavigation, +}: DashboardSidebarPageItemProps) { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { + onPageItemClick, + mini = false, + fullyExpanded = true, + fullyCollapsed = false, + } = sidebarContext; + + const [isHovered, setIsHovered] = React.useState(false); + + const handleClick = React.useCallback(() => { + if (onPageItemClick) { + onPageItemClick(id, !!nestedNavigation); + } + }, [onPageItemClick, id, nestedNavigation]); + + let nestedNavigationCollapseSx: SxProps = { display: "none" }; + if (mini && fullyCollapsed) { + nestedNavigationCollapseSx = { + fontSize: 18, + position: "absolute", + top: "41.5%", + right: "2px", + transform: "translateY(-50%) rotate(-90deg)", + }; + } else if (!mini && fullyExpanded) { + nestedNavigationCollapseSx = { + ml: 0.5, + fontSize: 20, + transform: `rotate(${expanded ? 0 : -90}deg)`, + transition: (theme: Theme) => + theme.transitions.create("transform", { + easing: theme.transitions.easing.sharp, + duration: 100, + }), + }; + } + + const hasExternalHref = href + ? href.startsWith("http://") || href.startsWith("https://") + : false; + + const LinkComponent = hasExternalHref ? "a" : Link; + + const miniNestedNavigationSidebarContextValue = React.useMemo(() => { + return { + onPageItemClick: onPageItemClick ?? (() => {}), + mini: false, + fullyExpanded: true, + fullyCollapsed: false, + hasDrawerTransitions: false, + }; + }, [onPageItemClick]); + + return ( + + { + setIsHovered(true); + }, + onMouseLeave: () => { + setIsHovered(false); + }, + } + : {})} + sx={{ + display: "block", + py: 0, + px: 1, + overflowX: "hidden", + }} + > + + {icon || mini ? ( + + + {icon ?? null} + {!icon && mini ? ( + + {title + .split(" ") + .slice(0, 2) + .map((titleWord) => titleWord.charAt(0).toUpperCase())} + + ) : null} + + {mini ? ( + + {title} + + ) : null} + + ) : null} + {!mini ? ( + + ) : null} + {action && !mini && fullyExpanded ? action : null} + {nestedNavigation ? ( + + ) : null} + + {nestedNavigation && mini ? ( + + + + + {nestedNavigation} + + + + + ) : null} + + {nestedNavigation && !mini ? ( + + {nestedNavigation} + + ) : null} + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx b/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx new file mode 100644 index 0000000..d5e3178 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Divider from "@mui/material/Divider"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export default function DashboardSidebarDividerItem() { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext; + + return ( +
  • + +
  • + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx new file mode 100644 index 0000000..0fd38c6 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { useTheme, useColorScheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; +import type {} from "@mui/material/themeCssVarsAugmentation"; + +export default function ThemeSwitcher() { + const theme = useTheme(); + + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const preferredMode = prefersDarkMode ? "dark" : "light"; + + const { mode, setMode } = useColorScheme(); + + const paletteMode = !mode || mode === "system" ? preferredMode : mode; + + const toggleMode = React.useCallback(() => { + setMode(paletteMode === "dark" ? "light" : "dark"); + }, [setMode, paletteMode]); + + return ( + +
    + + + + + + +
    +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/constants.ts b/matrixgw_frontend/src/widgets/dashboard/constants.ts new file mode 100644 index 0000000..780f97a --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/constants.ts @@ -0,0 +1,2 @@ +export const DRAWER_WIDTH = 240; // px +export const MINI_DRAWER_WIDTH = 90; // px diff --git a/matrixgw_frontend/src/widgets/dashboard/mixins.ts b/matrixgw_frontend/src/widgets/dashboard/mixins.ts new file mode 100644 index 0000000..bee5cd9 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/mixins.ts @@ -0,0 +1,23 @@ +import { type Theme } from "@mui/material/styles"; + +export function getDrawerSxTransitionMixin( + isExpanded: boolean, + property: string +) { + return { + transition: (theme: Theme) => + theme.transitions.create(property, { + easing: theme.transitions.easing.sharp, + duration: isExpanded + ? theme.transitions.duration.enteringScreen + : theme.transitions.duration.leavingScreen, + }), + }; +} + +export function getDrawerWidthTransitionMixin(isExpanded: boolean) { + return { + ...getDrawerSxTransitionMixin(isExpanded, "width"), + overflowX: "hidden", + }; +}