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
+15 -1
View File
@@ -7,11 +7,13 @@ import { cn } from "@/lib/utils";
import { Inbox, CheckSquare, Square, Trash2, Mail, MailOpen, Loader2 } from "lucide-react";
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useEmailStore } from "@/stores/email-store";
import { useAuthStore } from "@/stores/auth-store";
import { useSettingsStore } from "@/stores/settings-store";
import { groupEmailsByThread, sortThreadGroups } from "@/lib/thread-utils";
import { useContextMenu } from "@/hooks/use-context-menu";
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
import { useTranslations } from "next-intl";
import { useVirtualizer } from "@tanstack/react-virtual";
import { SearchChips } from "@/components/search/search-chips";
@@ -90,6 +92,7 @@ export function EmailList({
}, [emails]);
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<Email>();
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
const [isProcessing, setIsProcessing] = useState(false);
const parentRef = useRef<HTMLDivElement>(null);
@@ -143,7 +146,16 @@ export function EmailList({
};
const handleBatchDelete = async () => {
if (!client || isProcessing || !confirm(`Delete ${selectedEmailIds.size} emails?`)) return;
if (!client || isProcessing) return;
const confirmed = await confirmDialog({
title: t('batch_actions.delete_confirm_title'),
message: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
confirmText: t('batch_actions.delete'),
variant: "destructive",
});
if (!confirmed) return;
setIsProcessing(true);
try {
await batchDelete(client);
@@ -456,6 +468,8 @@ export function EmailList({
}}
/>
)}
<ConfirmDialog {...confirmDialogProps} />
</div>
);
}