Add collapse button and logo

This commit is contained in:
Alex Holliday
2025-09-27 15:47:56 -07:00
parent c30d6019df
commit fc9018e314
11 changed files with 243 additions and 34 deletions
@@ -0,0 +1,36 @@
import LeftArrow from "@/assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
export const ArrowLeft = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -0,0 +1,28 @@
import RightArrow from "@/assets/icons/right-arrow.svg?react";
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
export const ArrowRight = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -1,39 +1,6 @@
import { useState, useEffect } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { Outlet } from "react-router";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
const COLLAPSED_WIDTH = 50;
const EXPANDED_WIDTH = 250;
const SideBar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
setCollapsed(isSmall);
}, [isSmall]);
return (
<Stack
border="1px solid red"
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
sx={{
transition: "width 0.3s ease",
}}
>
<Box
border="1px solid blue"
onClick={() => setCollapsed(!collapsed)}
>
Sidebar Content
</Box>
</Stack>
);
};
import { SideBar } from "@/Components/v2/Layouts/Sidebar";
const RootLayout = () => {
return (
@@ -0,0 +1,50 @@
import IconButton from "@mui/material/IconButton";
import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight";
import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft";
import { useTheme } from "@mui/material/styles";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => {
const theme = useTheme();
const dispatch = useDispatch();
const arrowIcon = collapsed ? (
<ArrowRight
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
) : (
<ArrowLeft
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
);
return (
<IconButton
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
p: theme.spacing(2.5),
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
}}
onClick={() => {
dispatch(toggleSidebar());
}}
>
{arrowIcon}
</IconButton>
);
};
@@ -0,0 +1,59 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
export const Logo = ({ collapsed }: { collapsed: boolean }) => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
return (
<Stack
pt={theme.spacing(6)}
pb={theme.spacing(12)}
pl={theme.spacing(8)}
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Typography
pl={theme.spacing("1px")}
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.accent.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.accent.contrastText}
fontSize={18}
>
C
</Typography>
<Box
overflow={"hidden"}
sx={{
transition: "opacity 900ms ease, width 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
width: collapsed ? 0 : "100%",
}}
>
{" "}
<Typography
lineHeight={1}
mt={theme.spacing(2)}
color={theme.palette.primary.contrastText}
variant="h2"
>
{t("common.appName")}
</Typography>
</Box>
</Stack>
);
};
@@ -0,0 +1,40 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setCollapsed } from "@/Features/UI/uiSlice";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton";
import Stack from "@mui/material/Stack";
import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo";
export const COLLAPSED_WIDTH = 64;
export const EXPANDED_WIDTH = 250;
export const SideBar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const dispatch = useDispatch();
const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed);
useEffect(() => {
dispatch(setCollapsed({ collapsed: isSmall }));
}, [isSmall]);
return (
<Stack
position="sticky"
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
sx={{
transition: "width 0.3s ease",
}}
>
<CollapseButton collapsed={collapsed} />
<Logo collapsed={collapsed} />
</Stack>
);
};
+5
View File
@@ -47,6 +47,10 @@ const uiSlice = createSlice({
toggleSidebar: (state) => {
state.sidebar.collapsed = !state.sidebar.collapsed;
},
setCollapsed: (state, action) => {
const { collapsed } = action.payload;
state.sidebar.collapsed = collapsed;
},
setMode: (state, action) => {
state.mode = action.payload;
},
@@ -73,6 +77,7 @@ export default uiSlice.reducer;
export const {
setRowsPerPage,
toggleSidebar,
setCollapsed,
setMode,
setShowURL,
setGreeting,
+10
View File
@@ -54,12 +54,17 @@ export const lightPalette = {
main: colors.offWhite,
contrastText: colors.blueGray800,
contrastTextSecondary: colors.blueGray600,
lowContrast: colors.gray250,
},
secondary: {
main: colors.gray200,
light: colors.lightBlueWave,
contrastText: colors.blueGray600,
},
tertiary: {
main: colors.gray100,
contrastText: colors.blueGray800,
},
};
export const darkPalette = {
@@ -73,10 +78,15 @@ export const darkPalette = {
main: colors.offBlack,
contrastText: colors.blueGray50,
contrastTextSecondary: colors.gray200,
lowContrast: colors.blueGray600,
},
secondary: {
main: "#313131",
light: colors.lightBlueWave,
contrastText: colors.gray200,
},
tertiary: {
main: colors.blueGray800,
contrastText: colors.gray100,
},
};
+3
View File
@@ -67,6 +67,9 @@ export const theme = (mode: string, palette: any) =>
},
},
},
shape: {
borderRadius: 2,
},
});
export const lightTheme = createTheme(theme("light", lightPalette));
+8
View File
@@ -7,3 +7,11 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare module "*.svg?react" {
import * as React from "react";
const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
export default ReactComponent;
}
+3
View File
@@ -3,13 +3,16 @@ import "@mui/material/Button";
declare module "@mui/material/styles" {
interface Palette {
accent: Palette["primary"];
tertiary: Palette["primary"];
}
interface PaletteOptions {
accent?: PaletteOptions["primary"];
tertiary?: PaletteOptions["primary"];
}
interface PaletteColor {
contrastTextSecondary?: string;
lowContrast?: string;
}
}