Files
SRCmail/components/layout/mobile-header.tsx
T
Matthieu MALVACHEandMatthieu MALVACHE a43096485b feat: add UI/UX polish with navigation rail, confirm dialogs, welcome banner, and form validation
- Add NavigationRail component (desktop vertical icon sidebar + mobile bottom tab bar)
- Add ConfirmDialog with promise-based useConfirmDialog hook for async confirmation flow
- Add WelcomeBanner onboarding component (one-time display, localStorage persistence)
- Polish login form UX (shake on error, TOTP slide animation, password visibility toggle, session expired banner)
- Add inline form validation with shake animation in email composer and contacts
- Add empty state patterns for contacts (no data vs no search results with contextual actions)
- Improve toast notification system with undo action support and typed durations
- Add WCAG AA prefers-reduced-motion media query, safe area insets, sr-only live regions
- Add template settings tab and keyboard shortcut integration
- Update all 8 locale translations
2026-02-17 02:31:50 +01:00

151 lines
3.6 KiB
TypeScript

"use client";
import { Menu, ArrowLeft, Plus, Search, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUIStore } from "@/stores/ui-store";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
interface MobileHeaderProps {
title: string;
showBack?: boolean;
onBack?: () => void;
onCompose?: () => void;
onSearch?: () => void;
className?: string;
}
export function MobileHeader({
title,
showBack = false,
onBack,
onCompose,
onSearch,
className,
}: MobileHeaderProps) {
const t = useTranslations('sidebar');
const { toggleSidebar, goBack, sidebarOpen } = useUIStore();
const handleLeftAction = () => {
if (showBack && onBack) {
onBack();
} else if (showBack) {
goBack();
} else {
toggleSidebar();
}
};
return (
<header
className={cn(
"flex items-center justify-between px-4 h-14 border-b border-border bg-background shrink-0",
"lg:hidden", // Only visible on mobile/tablet
className
)}
>
{/* Left action: Menu or Back button */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={handleLeftAction}
className={cn(
"h-11 w-11",
!showBack && sidebarOpen && "bg-accent"
)}
aria-label={showBack ? "Go back" : "Toggle menu"}
aria-expanded={!showBack ? sidebarOpen : undefined}
>
{showBack ? (
<ArrowLeft className="h-5 w-5" />
) : sidebarOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button>
{/* Title */}
<h1 className="font-semibold text-lg truncate">{title}</h1>
</div>
{/* Right actions */}
<div className="flex items-center gap-1">
{onSearch && (
<Button
variant="ghost"
size="icon"
onClick={onSearch}
className="h-11 w-11"
aria-label={t('mobile.search')}
>
<Search className="h-5 w-5" />
</Button>
)}
{onCompose && (
<Button
variant="ghost"
size="icon"
onClick={onCompose}
className="h-11 w-11 text-primary"
aria-label={t('mobile.compose')}
>
<Plus className="h-5 w-5" />
</Button>
)}
</div>
</header>
);
}
/**
* Viewer header for mobile - shows when viewing an email
*/
interface MobileViewerHeaderProps {
subject?: string;
onBack: () => void;
onDelete?: () => void;
onArchive?: () => void;
className?: string;
}
export function MobileViewerHeader({
subject,
onBack,
onDelete: _onDelete,
onArchive: _onArchive,
className,
}: MobileViewerHeaderProps) {
const t = useTranslations('sidebar');
return (
<header
className={cn(
"flex items-center justify-between px-2 h-14 border-b border-border bg-background shrink-0",
"lg:hidden", // Only visible on mobile/tablet
className
)}
>
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-11 w-11"
aria-label={t('mobile.go_back')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="flex-1 font-medium text-sm truncate px-2 text-center">
{subject || "(No Subject)"}
</h1>
<div className="flex items-center">
{/* Placeholder for additional actions - kept minimal */}
<div className="w-10" />
</div>
</header>
);
}