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:
+28
-2
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user