From 771229db1816b005a531c33a7787b87bdb245716 Mon Sep 17 00:00:00 2001 From: brian-inspiren Date: Thu, 6 Aug 2026 16:11:37 +0800 Subject: [PATCH] feat: collapsible sidebar --- resources/js/components/sidebar.tsx | 78 +++++++++++++++++++---------- resources/js/layouts/app-layout.tsx | 76 +++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 34 deletions(-) diff --git a/resources/js/components/sidebar.tsx b/resources/js/components/sidebar.tsx index 8c38248..b58999c 100644 --- a/resources/js/components/sidebar.tsx +++ b/resources/js/components/sidebar.tsx @@ -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: , 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 = ( + ) => { + e.preventDefault(); + if (hasChildren && !collapsed) toggle(item.label); + else if (item.href) Inertia.visit(item.href); + }} + > + {item.icon && ( + + {item.icon} + + )} + {!collapsed && {item.label}} + {hasChildren && !collapsed && ( + + )} + + ); + return (
- { - e.preventDefault(); - if (hasChildren) toggle(item.label); - else if (item.href) Inertia.visit(item.href); - }} - > - {item.icon && {item.icon}} - {item.label} - {hasChildren && ( - - )} - + {collapsed ? ( + + {button} + + ) : ( + button + )} - {hasChildren && ( + {hasChildren && !collapsed && ( {item.children!.map((child) => renderItem(child, depth + 1))} @@ -147,7 +170,7 @@ export default function Sidebar() { return ( {sections.map((section) => (
- {section} + {!collapsed && ( + {section} + )} {items .filter((item) => item.section === section) .map((item) => renderItem(item))} - + {!collapsed && }
))}
diff --git a/resources/js/layouts/app-layout.tsx b/resources/js/layouts/app-layout.tsx index 25dcab5..fcd64ac 100644 --- a/resources/js/layouts/app-layout.tsx +++ b/resources/js/layouts/app-layout.tsx @@ -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 ( - + setSidebarCollapsed(!open)} + >
} header={ @@ -1165,11 +1186,50 @@ export default function AppLayout({ children }: Props) { styles={{ display: 'none' }} > setOpened((o) => !o)} + opened={mobileOpened} + onClick={() => + setMobileOpened((o) => !o) + } size="sm" /> + + + + setSidebarCollapsed( + (collapsed) => !collapsed, + ) + } + aria-label={ + sidebarCollapsed + ? 'Expand sidebar' + : 'Collapse sidebar' + } + > + {sidebarCollapsed ? ( + + ) : ( + + )} + + + Inspiren SEM Tool