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