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

View File

@@ -18,16 +18,17 @@ import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarRail,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarGroup,
SidebarGroupLabel,
SidebarRail,
SidebarTrigger,
useSidebar,
} from '@/components/ui/sidebar';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
@@ -41,6 +42,7 @@ function isRouteActive(pathname: string, path?: string) {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
const { hasPermission } = usePermissions();
const { isMobile } = useSidebar();
const authUser = useAppStore((state) => state.user);
const navItems = filterMenuItems(menuItems, hasPermission);
const user = {
@@ -53,17 +55,21 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 py-2">
<LogoWrapper size="sm" variant="sidebar" />
<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 h-8 w-full items-center gap-3 group-data-[collapsible=icon]:w-8 group-data-[collapsible=icon]:justify-center">
<SidebarBrandControl isMobile={isMobile} />
<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 text-xs">Road Intelligence</span>
</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>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarContent className="px-2 py-3">
<SidebarGroup className="p-0">
<SidebarMenu>
{navItems.map((item) => (
<SidebarNavItem
@@ -75,7 +81,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarFooter className="border-t border-sidebar-border px-3 py-3">
<NavUser user={user} />
</SidebarFooter>
<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({
item,
pathname,
@@ -159,4 +182,3 @@ function SidebarNavItem({
</SidebarMenuItem>
);
}

View File

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