refactor(layout): improve module header and sidebar toggle UX

This commit is contained in:
2026-07-09 11:54:16 +05:30
parent 4fe93460d9
commit a376558b2f
4 changed files with 100 additions and 31 deletions

View File

@@ -0,0 +1,27 @@
'use client';
import { BreadcrumbBasic } from '@/components/app-breadcrumb';
import { ModeToggle } from '@/components/mode-toogle';
import { Separator } from '@/components/ui/separator';
import { SidebarTrigger, useSidebar } from '@/components/ui/sidebar';
export function ModuleShellHeader() {
const { isMobile } = useSidebar();
return (
<div className="sticky top-0 z-50 flex items-center justify-between gap-4 border-b border-border bg-background/95 px-6 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="flex min-w-0 items-center gap-3">
{isMobile ? (
<>
<SidebarTrigger className="shrink-0 bg-background/80 text-muted-foreground shadow-xs backdrop-blur-sm" />
<Separator orientation="vertical" className="h-4 shrink-0" />
</>
) : null}
<div className="min-w-0">
<BreadcrumbBasic />
</div>
</div>
<ModeToggle />
</div>
);
}

View File

@@ -1,10 +1,8 @@
import { BreadcrumbBasic } from '@/components/app-breadcrumb';
import { AppSidebar } from '@/components/app-sidebar'; import { AppSidebar } from '@/components/app-sidebar';
import { ModeToggle } from '@/components/mode-toogle'; import { SidebarProvider } from '@/components/ui/sidebar';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { Separator } from '@/components/ui/separator';
import { AuthGuard } from '@/guards'; import { AuthGuard } from '@/guards';
import React from 'react'; import React from 'react';
import { ModuleShellHeader } from './_components/module-shell-header';
const ModulesLayout = ({ const ModulesLayout = ({
children, children,
@@ -15,16 +13,11 @@ const ModulesLayout = ({
<AuthGuard> <AuthGuard>
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<main className="flex flex-1 flex-col w-full h-screen overflow-hidden"> <main className="flex flex-1 h-screen w-full flex-col overflow-hidden">
<div className="scroll-stable flex-1 overflow-auto"> <div className="scroll-stable flex-1 overflow-auto">
<div className="max-w-380 mx-auto w-full flex flex-col min-h-full"> <div className="mx-auto flex min-h-full w-full max-w-380 flex-col">
<div className="flex gap-3 items-center sticky top-0 bg-background z-50 py-3 px-6 border-b border-border"> <ModuleShellHeader />
<SidebarTrigger /> <div className="flex-1 p-6">{children}</div>
<ModeToggle />
<Separator orientation="vertical" className="mx-2 h-4" />
<BreadcrumbBasic />
</div>
<div className="p-6 flex-1">{children}</div>
</div> </div>
</div> </div>
</main> </main>

View File

@@ -18,16 +18,17 @@ import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter, SidebarFooter,
SidebarGroup,
SidebarHeader, SidebarHeader,
SidebarRail,
SidebarMenu, SidebarMenu,
SidebarMenuItem,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub, SidebarMenuSub,
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
SidebarGroup, SidebarRail,
SidebarGroupLabel, SidebarTrigger,
useSidebar,
} from '@/components/ui/sidebar'; } from '@/components/ui/sidebar';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
@@ -41,6 +42,7 @@ function isRouteActive(pathname: string, path?: string) {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname(); const pathname = usePathname();
const { hasPermission } = usePermissions(); const { hasPermission } = usePermissions();
const { isMobile } = useSidebar();
const authUser = useAppStore((state) => state.user); const authUser = useAppStore((state) => state.user);
const navItems = filterMenuItems(menuItems, hasPermission); const navItems = filterMenuItems(menuItems, hasPermission);
const user = { const user = {
@@ -53,17 +55,21 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
<SidebarHeader> <SidebarHeader className="border-b border-sidebar-border px-3 py-3 group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:border-b-0 group-data-[collapsible=icon]:px-2">
<div className="flex items-center gap-2 py-2"> <div className="flex h-8 w-full items-center gap-3 group-data-[collapsible=icon]:w-8 group-data-[collapsible=icon]:justify-center">
<LogoWrapper size="sm" variant="sidebar" /> <SidebarBrandControl isMobile={isMobile} />
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden"> <div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-semibold">RoadMonitor</span> <span className="truncate font-semibold">RoadMonitor</span>
<span className="truncate text-xs">Road Intelligence</span> <span className="truncate text-xs">Road Intelligence</span>
</div> </div>
<SidebarTrigger
className="ml-auto shrink-0 bg-sidebar/80 text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden"
aria-label={isMobile ? 'Close menu' : 'Hide menu'}
/>
</div> </div>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent className="px-2 py-3">
<SidebarGroup> <SidebarGroup className="p-0">
<SidebarMenu> <SidebarMenu>
{navItems.map((item) => ( {navItems.map((item) => (
<SidebarNavItem <SidebarNavItem
@@ -75,7 +81,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter className="border-t border-sidebar-border px-3 py-3">
<NavUser user={user} /> <NavUser user={user} />
</SidebarFooter> </SidebarFooter>
<SidebarRail /> <SidebarRail />
@@ -83,6 +89,23 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
); );
} }
function SidebarBrandControl({ isMobile }: { isMobile: boolean }) {
return (
<div className="group/sidebar-brand relative flex size-8 shrink-0 items-center justify-center">
<LogoWrapper
size="sm"
variant="sidebar"
className="transition-opacity duration-150 group-data-[collapsible=icon]:group-hover/sidebar-brand:opacity-0 group-data-[collapsible=icon]:group-focus-within/sidebar-brand:opacity-0"
/>
<SidebarTrigger
tooltip="Open sidebar"
className="pointer-events-none absolute inset-0 bg-sidebar text-sidebar-foreground/80 opacity-0 shadow-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:pointer-events-auto group-data-[collapsible=icon]:group-hover/sidebar-brand:opacity-100 group-data-[collapsible=icon]:group-focus-within/sidebar-brand:opacity-100"
aria-label={isMobile ? 'Close menu' : 'Open menu'}
/>
</div>
);
}
function SidebarNavItem({ function SidebarNavItem({
item, item,
pathname, pathname,
@@ -159,4 +182,3 @@ function SidebarNavItem({
</SidebarMenuItem> </SidebarMenuItem>
); );
} }

View File

@@ -256,29 +256,56 @@ function Sidebar({
function SidebarTrigger({ function SidebarTrigger({
className, className,
onClick, onClick,
tooltip,
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button> & {
const { toggleSidebar } = useSidebar(); tooltip?: string | React.ComponentProps<typeof TooltipContent>;
}) {
const { isMobile, toggleSidebar } = useSidebar();
return ( const button = (
<Button <Button
data-sidebar="trigger" data-sidebar="trigger"
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon-sm"
className={cn('size-7', className)} className={cn(
'rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
className,
)}
onClick={(event) => { onClick={(event) => {
onClick?.(event); onClick?.(event);
toggleSidebar(); toggleSidebar();
}} }}
{...props} {...props}
> >
<PanelLeftIcon /> <PanelLeftIcon className="size-4" />
<span className="sr-only">Toggle Sidebar</span> <span className="sr-only">Toggle Sidebar</span>
</Button> </Button>
); );
}
if (!tooltip) {
return button;
}
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const { toggleSidebar } = useSidebar(); const { toggleSidebar } = useSidebar();