"use client" import React, { useCallback, useRef } from 'react' import { usePreventScroll } from '@react-aria/overlays' import { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation' import { cn } from '../../utils' import { useFocusTrap } from '../../hooks/ui/use-focus-trap' import { Logout02Icon, PenEditIcon, UserSearchIcon } from '../icons-v2-generated' import { Button, SquareAvatar } from '../ui' import { OVERLAY_BACKDROP_CLASS } from '../ui/drawer' // Header height constant — the unified top-navigation `small` bar (56px on all screens) const HEADER_HEIGHT = 56 export interface MobileBurgerMenuProps { /** Whether the menu is open */ isOpen: boolean /** Callback to close the menu */ onClose: () => void /** Sidebar configuration */ config: NavigationSidebarConfig /** User info for the header card */ user?: { userName?: string userEmail?: string userAvatarUrl?: string | null userRole?: string } /** Callback when search user button is clicked */ onSearchUser?: () => void /** Callback when edit profile button is clicked */ onEditProfile?: () => void /** Callback when logout button is clicked */ onLogout?: () => void /** * When true, all interactive items inside the menu (nav items, action buttons, * logout) are disabled. The burger toggle and backdrop click to close still work. */ disabled?: boolean } export const MobileBurgerMenu = React.memo(function MobileBurgerMenu({ isOpen, onClose, config, user, onSearchUser, onEditProfile, onLogout, disabled = false, }: MobileBurgerMenuProps) { const panelRef = useRef(null) // Shared ref-counted, iOS-aware scroll lock (react-aria) while open. usePreventScroll({ isDisabled: !isOpen }) // Initial focus, Tab containment, Escape-to-close, guarded focus restore. useFocusTrap(panelRef, isOpen, { onEscape: onClose }) const handleItemClick = useCallback((item: NavigationSidebarItem) => { if (item.onClick) { item.onClick() } else if (item.path) { config.onNavigate?.(item.path) } onClose() }, [config, onClose]) // Separate primary and secondary items const primaryItems = config.items.filter(item => item.section !== 'secondary') const secondaryItems = config.items.filter(item => item.section === 'secondary') const renderNavigationItem = (item: NavigationSidebarItem, isGridItem = false) => { const isActive = item.isActive ?? false return ( ) } // Render grid of navigation items (2 columns). CSS grid keeps a lone last-row // item exactly one column wide (aligned with the column above) instead of // stretching to fill the row. const renderNavigationGrid = (items: NavigationSidebarItem[]) => (
{items.map((item) => ( {renderNavigationItem(item, true)} ))}
) return ( <> {/* Dim backdrop (no blur, unified with Drawer) - positioned below header. `absolute` (not `fixed`) so it anchors to the AppLayout row it renders in — the row that sits BELOW the optional `topBar` banner — rather than the viewport. This keeps `top: HEADER_HEIGHT` measured from just under the header even when a top banner pushes the header down. With no banner the row fills the viewport, so this is identical to the previous behavior. */}