- 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
141 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|