17a5c7470d
One implementation each, for every page to use instead of its own copy:
- lib/toast.ts + components/Toaster.tsx: toast.success|error|info from
anywhere, rendered in polite/assertive live regions; an error stays until
dismissed, a success fades, a repeat collapses.
- lib/confirm.ts + components/ConfirmHost.tsx: await confirmDialog({...})
and confirmDiscard() through the themed ConfirmModal, replacing
window.confirm and the actions that asked nothing.
- hooks/useDialog.ts: the modal focus contract (focus in, Tab trapped,
Escape, focus restored), extracted from ConfirmModal; dialogs stack so
only the top one answers keys. ConfirmModal now uses it.
- hooks/useArmed.ts: two-click confirmation that disarms after 3 s.
- hooks/useUnsavedGuard.ts: beforeunload while a draft is dirty.
- lib/clipboard.ts copyText: Clipboard API with an execCommand fallback for
non-secure origins, outcome toasted.
- lib/keys.ts isSubmitEnter: ignores the Enter that confirms an IME
candidate (Vietnamese Telex, CJK).
34 lines
1.1 KiB
TypeScript
34 lines
1.1 KiB
TypeScript
/**
|
|
* @file ConfirmHost.tsx
|
|
* @description Renders the one pending `confirmDialog()` from `lib/confirm.ts`
|
|
* through `ConfirmModal`. Mounted once in `App.tsx`.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ConfirmModal } from "./ConfirmModal";
|
|
import { settleConfirm, subscribeConfirm, type PendingConfirm } from "../lib/confirm";
|
|
|
|
const cancel = () => settleConfirm(false);
|
|
const confirm = () => settleConfirm(true);
|
|
|
|
/** The single host for `confirmDialog()`. */
|
|
export function ConfirmHost() {
|
|
const { t } = useTranslation("common");
|
|
const [p, setP] = useState<PendingConfirm | null>(null);
|
|
useEffect(() => subscribeConfirm(setP), []);
|
|
return (
|
|
<ConfirmModal
|
|
open={p !== null}
|
|
title={p?.title ?? ""}
|
|
message={p?.message}
|
|
confirmLabel={p?.confirmLabel ?? t("confirm")}
|
|
cancelLabel={p?.cancelLabel ?? t("cancel")}
|
|
destructive={p?.destructive ?? true}
|
|
onConfirm={confirm}
|
|
onCancel={cancel}
|
|
/>
|
|
);
|
|
}
|