ca-grow-ops-manager/frontend/src/components/layout/MobileNavSheet.tsx
fullsizemalt efb298e119
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix: thorough Linear design audit
- ThemeToggle: Single button cycle instead of 3-button bar
- UserMenu: Cleaner styling with accent avatar
- MobileNavSheet: Consistent Linear tokens
- Walkthrough checklists: Desktop two-column layout
- RoleModal: Toggle buttons instead of tiny checkboxes
- IPMScheduleModal: Toggle buttons instead of checkbox
- ScoutingModal: Toggle buttons instead of checkbox
2025-12-12 15:49:21 -08:00

141 lines
5.9 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { X, LogOut } from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { usePermissions } from '../../hooks/usePermissions';
import { getFilteredNavSections } from '../../lib/navigation';
import { RoleBadge } from '../ui/RoleBadge';
import ThemeToggle from '../ThemeToggle';
interface MobileNavSheetProps {
isOpen: boolean;
onClose: () => void;
}
/**
* Mobile slide-up navigation sheet
* Linear-inspired: clean, smooth animations
*/
export function MobileNavSheet({ isOpen, onClose }: MobileNavSheetProps) {
const { user, logout } = useAuth();
const { role } = usePermissions();
const location = useLocation();
const sections = getFilteredNavSections(role);
const sheetRef = useRef<HTMLDivElement>(null);
// Close on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
// Close when navigating
useEffect(() => {
onClose();
}, [location.pathname]);
if (!isOpen) return null;
return (
<div className="md:hidden fixed inset-0 z-50">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm animate-fade-in"
onClick={onClose}
/>
{/* Sheet */}
<div
ref={sheetRef}
className="absolute bottom-0 left-0 right-0 bg-elevated rounded-t-xl shadow-2xl animate-slide-up max-h-[85vh] flex flex-col"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
>
{/* Handle */}
<div className="flex justify-center py-3">
<div className="w-10 h-1 bg-subtle rounded-full" />
</div>
{/* Header */}
<div className="flex items-center justify-between px-4 pb-4 border-b border-default">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-md bg-accent flex items-center justify-center text-white font-medium">
{user?.email?.[0]?.toUpperCase() || '?'}
</div>
<div>
<p className="font-medium text-primary text-sm">
{user?.name || user?.email}
</p>
<RoleBadge size="sm" />
</div>
</div>
<button
onClick={onClose}
className="p-2 rounded-md hover:bg-tertiary transition-colors duration-fast"
aria-label="Close menu"
>
<X size={18} className="text-secondary" />
</button>
</div>
{/* Navigation Sections */}
<div className="flex-1 overflow-y-auto p-4 space-y-5 custom-scrollbar">
{sections.map(section => (
<div key={section.id}>
<h3 className="text-[10px] font-semibold text-tertiary uppercase tracking-wider mb-2 px-1">
{section.label}
</h3>
<div className="grid grid-cols-2 gap-2">
{section.items.map(item => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<Link
key={item.id}
to={item.path}
onClick={onClose}
className={`flex items-center gap-2.5 p-3 rounded-md transition-colors duration-fast ${isActive
? 'bg-accent-muted text-accent'
: 'bg-tertiary text-secondary active:bg-secondary'
}`}
>
<Icon size={16} strokeWidth={isActive ? 2 : 1.5} />
<span className={`text-sm truncate ${isActive ? 'font-medium' : ''}`}>
{item.shortLabel || item.label}
</span>
</Link>
);
})}
</div>
</div>
))}
</div>
{/* Footer Actions */}
<div className="p-4 border-t border-default space-y-3">
<ThemeToggle />
<button
onClick={() => {
logout();
onClose();
}}
className="w-full flex items-center justify-center gap-2 py-3 px-4 bg-destructive-muted text-destructive rounded-md font-medium active:opacity-80 transition-opacity"
>
<LogOut size={16} />
Sign Out
</button>
</div>
</div>
</div>
);
}