feat: collapsible sidebar
This commit is contained in:
parent
83c0f449f5
commit
771229db18
@ -9,6 +9,7 @@ import {
|
||||
Collapse,
|
||||
rem,
|
||||
Divider,
|
||||
Tooltip,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
@ -61,6 +62,11 @@ const useStyles = createStyles((theme) => ({
|
||||
textTransform: 'uppercase',
|
||||
padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
|
||||
},
|
||||
collapsedLink: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: theme.spacing.xs,
|
||||
paddingRight: theme.spacing.xs,
|
||||
},
|
||||
}));
|
||||
|
||||
type SidebarItem = {
|
||||
@ -94,7 +100,7 @@ const items: SidebarItem[] = [
|
||||
// { label: 'Settings', href: '/settings', icon: <IconSettings size={16} /> , section: 'Management' },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
export default function Sidebar({ collapsed = false }: { collapsed?: boolean }) {
|
||||
const theme = useMantineTheme();
|
||||
const { classes, cx } = useStyles();
|
||||
const location = window.location.pathname;
|
||||
@ -110,31 +116,48 @@ export default function Sidebar() {
|
||||
item.href === location ||
|
||||
(hasChildren && item.children?.some((c) => c.href === location));
|
||||
|
||||
const button = (
|
||||
<UnstyledButton
|
||||
className={cx(classes.link, {
|
||||
[classes.active]: isActive,
|
||||
[classes.collapsedLink]: collapsed,
|
||||
})}
|
||||
style={{ paddingLeft: collapsed ? undefined : rem(16 + depth * 16) }}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (hasChildren && !collapsed) toggle(item.label);
|
||||
else if (item.href) Inertia.visit(item.href);
|
||||
}}
|
||||
>
|
||||
{item.icon && (
|
||||
<ThemeIcon size="sm" variant="light" mr={collapsed ? 0 : 'sm'}>
|
||||
{item.icon}
|
||||
</ThemeIcon>
|
||||
)}
|
||||
{!collapsed && <Text>{item.label}</Text>}
|
||||
{hasChildren && !collapsed && (
|
||||
<IconChevronDown
|
||||
size={16}
|
||||
className={classes.toggleIcon}
|
||||
style={{
|
||||
transform: opened[item.label] ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UnstyledButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={item.label}>
|
||||
<UnstyledButton
|
||||
className={cx(classes.link, { [classes.active]: isActive })}
|
||||
style={{ paddingLeft: rem(16 + depth * 16) }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (hasChildren) toggle(item.label);
|
||||
else if (item.href) Inertia.visit(item.href);
|
||||
}}
|
||||
>
|
||||
{item.icon && <ThemeIcon size="sm" variant="light" mr="sm">{item.icon}</ThemeIcon>}
|
||||
<Text>{item.label}</Text>
|
||||
{hasChildren && (
|
||||
<IconChevronDown
|
||||
size={16}
|
||||
className={classes.toggleIcon}
|
||||
style={{
|
||||
transform: opened[item.label] ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UnstyledButton>
|
||||
{collapsed ? (
|
||||
<Tooltip label={item.label} position="right" withinPortal>
|
||||
{button}
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
)}
|
||||
|
||||
{hasChildren && (
|
||||
{hasChildren && !collapsed && (
|
||||
<Collapse in={opened[item.label]}>
|
||||
{item.children!.map((child) => renderItem(child, depth + 1))}
|
||||
</Collapse>
|
||||
@ -147,7 +170,7 @@ export default function Sidebar() {
|
||||
|
||||
return (
|
||||
<Navbar
|
||||
width={{ base: 280 }}
|
||||
width={{ base: collapsed ? 60 : 280 }}
|
||||
p="xs"
|
||||
style={{
|
||||
backgroundColor:
|
||||
@ -160,16 +183,19 @@ export default function Sidebar() {
|
||||
: theme.colors.gray[2]
|
||||
}`,
|
||||
minHeight: '100%',
|
||||
transition: 'width 160ms ease',
|
||||
}}
|
||||
>
|
||||
<Stack spacing="xs">
|
||||
{sections.map((section) => (
|
||||
<div key={section}>
|
||||
<Text className={classes.sectionTitle}>{section}</Text>
|
||||
{!collapsed && (
|
||||
<Text className={classes.sectionTitle}>{section}</Text>
|
||||
)}
|
||||
{items
|
||||
.filter((item) => item.section === section)
|
||||
.map((item) => renderItem(item))}
|
||||
<Divider my="xs" />
|
||||
{!collapsed && <Divider my="xs" />}
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@ -39,6 +39,8 @@ import {
|
||||
IconBell,
|
||||
IconCircleX,
|
||||
IconInfoCircle,
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
IconLogout,
|
||||
IconSettings,
|
||||
IconUserPlus,
|
||||
@ -1083,7 +1085,14 @@ type Props = {
|
||||
|
||||
export default function AppLayout({ children }: Props) {
|
||||
const theme = useMantineTheme();
|
||||
const [opened, setOpened] = React.useState(false);
|
||||
const [mobileOpened, setMobileOpened] = React.useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.localStorage.getItem('sidebar_collapsed') === 'true';
|
||||
});
|
||||
|
||||
const isDark = theme.colorScheme === 'dark';
|
||||
|
||||
@ -1117,26 +1126,38 @@ export default function AppLayout({ children }: Props) {
|
||||
});
|
||||
}
|
||||
}, [flash]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'sidebar_collapsed',
|
||||
String(sidebarCollapsed),
|
||||
);
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
return (
|
||||
<SidebarProvider open={opened} onOpenChange={setOpened}>
|
||||
<SidebarProvider
|
||||
open={!sidebarCollapsed}
|
||||
onOpenChange={(open) => setSidebarCollapsed(!open)}
|
||||
>
|
||||
<AppShell
|
||||
padding="lg"
|
||||
navbarOffsetBreakpoint="sm"
|
||||
navbar={
|
||||
<Navbar
|
||||
width={{ base: 260 }}
|
||||
width={{ base: sidebarCollapsed ? 76 : 260 }}
|
||||
hiddenBreakpoint="sm"
|
||||
hidden={!opened}
|
||||
p="md"
|
||||
hidden={!mobileOpened}
|
||||
p={sidebarCollapsed ? 'xs' : 'md'}
|
||||
style={{
|
||||
borderRight: `1px solid ${
|
||||
isDark
|
||||
? theme.colors.dark[4]
|
||||
: theme.colors.gray[3]
|
||||
}`,
|
||||
transition: 'width 160ms ease',
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
</Navbar>
|
||||
}
|
||||
header={
|
||||
@ -1165,11 +1186,50 @@ export default function AppLayout({ children }: Props) {
|
||||
styles={{ display: 'none' }}
|
||||
>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={() => setOpened((o) => !o)}
|
||||
opened={mobileOpened}
|
||||
onClick={() =>
|
||||
setMobileOpened((o) => !o)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
</MediaQuery>
|
||||
<MediaQuery
|
||||
smallerThan="sm"
|
||||
styles={{ display: 'none' }}
|
||||
>
|
||||
<Tooltip
|
||||
label={
|
||||
sidebarCollapsed
|
||||
? 'Expand sidebar'
|
||||
: 'Collapse sidebar'
|
||||
}
|
||||
withinPortal
|
||||
>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
setSidebarCollapsed(
|
||||
(collapsed) => !collapsed,
|
||||
)
|
||||
}
|
||||
aria-label={
|
||||
sidebarCollapsed
|
||||
? 'Expand sidebar'
|
||||
: 'Collapse sidebar'
|
||||
}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<IconLayoutSidebarLeftExpand
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<IconLayoutSidebarLeftCollapse
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</MediaQuery>
|
||||
|
||||
<Text weight={700} size="lg">
|
||||
Inspiren SEM Tool
|
||||
|
||||
Loading…
Reference in New Issue
Block a user