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:
@@ -0,0 +1,84 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
|
||||
interface ConfirmDialogState {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant: "default" | "destructive";
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: ConfirmDialogState = {
|
||||
isOpen: false,
|
||||
title: "",
|
||||
message: "",
|
||||
variant: "default",
|
||||
onConfirm: () => {},
|
||||
};
|
||||
|
||||
interface ConfirmOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
|
||||
export function useConfirmDialog() {
|
||||
const [state, setState] = useState<ConfirmDialogState>(INITIAL_STATE);
|
||||
const resolveRef = useRef<((value: boolean) => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resolveRef.current) {
|
||||
resolveRef.current(false);
|
||||
resolveRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const confirm = useCallback(
|
||||
(options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
resolveRef.current = resolve;
|
||||
setState({
|
||||
isOpen: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
confirmText: options.confirmText,
|
||||
cancelText: options.cancelText,
|
||||
variant: options.variant || "default",
|
||||
onConfirm: () => {
|
||||
resolveRef.current = null;
|
||||
resolve(true);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (resolveRef.current) {
|
||||
resolveRef.current(false);
|
||||
resolveRef.current = null;
|
||||
}
|
||||
setState(INITIAL_STATE);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dialogProps: {
|
||||
isOpen: state.isOpen,
|
||||
onClose: close,
|
||||
onConfirm: state.onConfirm,
|
||||
title: state.title,
|
||||
message: state.message,
|
||||
confirmText: state.confirmText,
|
||||
cancelText: state.cancelText,
|
||||
variant: state.variant,
|
||||
},
|
||||
confirm,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user