- 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
85 lines
1.9 KiB
TypeScript
85 lines
1.9 KiB
TypeScript
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,
|
|
};
|
|
}
|