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
This commit is contained in:
Matthieu MALVACHE
2026-02-17 02:31:50 +01:00
committed by Matthieu MALVACHE
parent 2636a88820
commit a43096485b
44 changed files with 2148 additions and 669 deletions
+28 -2
View File
@@ -30,6 +30,8 @@ import {
import { DragDropProvider } from "@/contexts/drag-drop-context";
import { AdvancedSearchPanel } from "@/components/search/advanced-search-panel";
import { isFilterEmpty } from "@/lib/jmap/search-utils";
import { WelcomeBanner } from "@/components/ui/welcome-banner";
import { NavigationRail } from "@/components/layout/navigation-rail";
export default function Home() {
const router = useRouter();
@@ -37,6 +39,7 @@ export default function Home() {
const tCommon = useTranslations('common');
const [showComposer, setShowComposer] = useState(false);
const [composerMode, setComposerMode] = useState<'compose' | 'reply' | 'replyAll' | 'forward'>('compose');
const [composerDraftText, setComposerDraftText] = useState("");
const [initialCheckDone, setInitialCheckDone] = useState(false);
const [showShortcutsModal, setShowShortcutsModal] = useState(false);
// Mobile conversation view state
@@ -395,7 +398,8 @@ export default function Home() {
}
};
const handleReply = () => {
const handleReply = (draftText?: string) => {
setComposerDraftText(draftText || "");
setComposerMode('reply');
setShowComposer(true);
};
@@ -718,6 +722,13 @@ export default function Home() {
return (
<DragDropProvider>
<div className="flex h-screen bg-background overflow-hidden">
{/* Desktop Navigation Rail */}
{!isMobile && !isTablet && (
<div className="w-14 border-r border-border bg-secondary flex flex-col items-center flex-shrink-0">
<NavigationRail collapsed />
</div>
)}
{/* Mobile/Tablet Sidebar Overlay Backdrop */}
{(isMobile || isTablet) && sidebarOpen && (
<div
@@ -760,7 +771,8 @@ export default function Home() {
</div>
{/* Main Content Area */}
<div className="flex flex-1 min-w-0 h-full">
<div className="flex flex-col flex-1 min-w-0 h-full">
<div className="flex flex-1 min-h-0">
{/* Email List - full width on mobile, fixed width on tablet/desktop */}
<div
className={cn(
@@ -796,6 +808,8 @@ export default function Home() {
onClose={toggleAdvancedSearch}
/>
<WelcomeBanner />
<ErrorBoundary fallback={EmailListErrorFallback}>
<EmailList
emails={emails}
@@ -917,6 +931,7 @@ export default function Home() {
setTabletListVisible(true);
selectEmail(null);
}}
onShowShortcuts={() => setShowShortcutsModal(true)}
currentUserEmail={client?.["username"]}
currentUserName={client?.["username"]?.split("@")[0]}
currentMailboxRole={mailboxes.find(m => m.id === selectedMailbox)?.role}
@@ -926,6 +941,12 @@ export default function Home() {
</>
)}
</div>
</div>
{/* Mobile Bottom Navigation */}
{isMobile && activeView !== "viewer" && (
<NavigationRail orientation="horizontal" />
)}
</div>
{/* Email Composer Modal */}
@@ -952,10 +973,12 @@ export default function Home() {
body: selectedEmail.bodyValues?.[selectedEmail.textBody?.[0]?.partId || '']?.value || selectedEmail.preview || '',
receivedAt: selectedEmail.receivedAt
} : undefined}
initialDraftText={composerDraftText}
onSend={handleEmailSend}
onClose={() => {
setShowComposer(false);
setComposerMode('compose');
setComposerDraftText("");
}}
onDiscardDraft={handleDiscardDraft}
/>
@@ -969,6 +992,9 @@ export default function Home() {
isOpen={showShortcutsModal}
onClose={() => setShowShortcutsModal(false)}
/>
{/* Screen reader live region for dynamic status announcements */}
<div className="sr-only" aria-live="polite" aria-atomic="true" id="sr-status" />
</div>
</DragDropProvider>
);