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
+24 -16
View File
@@ -1,5 +1,5 @@
import { create } from "zustand";
import { Toast } from "@/components/ui/toast";
import { Toast, ToastAction } from "@/components/ui/toast";
interface ToastStore {
toasts: Toast[];
@@ -16,7 +16,7 @@ export const useToastStore = create<ToastStore>((set) => ({
const newToast: Toast = {
...toast,
id,
duration: toast.duration ?? 5000, // Default 5 seconds
duration: toast.duration ?? 5000,
};
set((state) => ({
@@ -35,18 +35,26 @@ export const useToastStore = create<ToastStore>((set) => ({
},
}));
// Helper functions for common toast types
interface ToastOptions {
message?: string;
action?: ToastAction;
duration?: number;
}
function showToast(type: Toast["type"], title: string, options?: string | ToastOptions, defaultDuration?: number): void {
const opts = typeof options === "string" ? { message: options } : options;
useToastStore.getState().addToast({
type,
title,
message: opts?.message,
action: opts?.action,
duration: opts?.duration ?? defaultDuration,
});
}
export const toast = {
success: (title: string, message?: string) => {
useToastStore.getState().addToast({ type: "success", title, message });
},
error: (title: string, message?: string) => {
useToastStore.getState().addToast({ type: "error", title, message, duration: 10000 });
},
info: (title: string, message?: string) => {
useToastStore.getState().addToast({ type: "info", title, message });
},
warning: (title: string, message?: string) => {
useToastStore.getState().addToast({ type: "warning", title, message });
},
};
success: (title: string, options?: string | ToastOptions) => showToast("success", title, options),
error: (title: string, options?: string | ToastOptions) => showToast("error", title, options, 10000),
info: (title: string, options?: string | ToastOptions) => showToast("info", title, options),
warning: (title: string, options?: string | ToastOptions) => showToast("warning", title, options),
};