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:
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { X, Keyboard } from "lucide-react";
|
||||
import { KEYBOARD_SHORTCUTS } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFocusTrap } from "@/hooks/use-focus-trap";
|
||||
|
||||
interface KeyboardShortcutsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,43 +13,28 @@ interface KeyboardShortcutsModalProps {
|
||||
|
||||
export function KeyboardShortcutsModal({ isOpen, onClose }: KeyboardShortcutsModalProps) {
|
||||
const t = useTranslations();
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close on any key press
|
||||
useEffect(() => {
|
||||
const handleKeyDown = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
const modalRef = useFocusTrap({
|
||||
isActive: isOpen,
|
||||
onEscape: onClose,
|
||||
restoreFocus: true,
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-in fade-in duration-150">
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-in fade-in duration-150"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="shortcuts-dialog-title"
|
||||
className={cn(
|
||||
"bg-background border border-border rounded-lg shadow-xl",
|
||||
"w-full max-w-2xl max-h-[80vh] overflow-hidden",
|
||||
"w-full max-w-2xl max-h-[90vh] overflow-hidden",
|
||||
"animate-in zoom-in-95 duration-200"
|
||||
)}
|
||||
>
|
||||
@@ -57,20 +42,21 @@ export function KeyboardShortcutsModal({ isOpen, onClose }: KeyboardShortcutsMod
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<Keyboard className="w-5 h-5 text-muted-foreground" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
<h2 id="shortcuts-dialog-title" className="text-lg font-semibold text-foreground">
|
||||
{t("shortcuts.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(80vh-80px)]">
|
||||
<div className="p-4 md:p-6 overflow-y-auto max-h-[calc(90vh-80px)]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Navigation Section */}
|
||||
<section>
|
||||
|
||||
Reference in New Issue
Block a user