cm_bot_v2/web/components/confirm-dialog.tsx
yiekheng e507714dc5 feat(web): delete with confirm dialog + fix iOS auto-zoom on edit
Adds × delete button per row in both tables (desktop column +
mobile card header). Click → native <dialog> confirm modal with
Esc/backdrop-cancel, destructive red button, error inline.
Wires deleteAccount/deleteUser Server Actions calling the new
api-server routes; revalidatePath refreshes the list on success.

EditableCell input switches to text-base (16px) on phone (sm:text-[13px]
above 640px), preventing iOS Safari auto-zoom-on-focus that was
shifting the layout when the soft keyboard appeared.
2026-05-02 21:17:19 +08:00

89 lines
2.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
type Props = {
open: boolean;
onCancel: () => void;
onConfirm: () => void;
title: string;
message: React.ReactNode;
confirmLabel?: string;
destructive?: boolean;
pending?: boolean;
};
/**
* Centered modal confirmation dialog. Uses the native <dialog> element
* so we get Esc-to-close, focus trapping, and the ::backdrop pseudo for
* the scrim — no a11y tax we'd pay rolling our own. Backdrop click
* cancels.
*/
export default function ConfirmDialog({
open,
onCancel,
onConfirm,
title,
message,
confirmLabel = "Confirm",
destructive = false,
pending = false,
}: Props) {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = ref.current;
if (!dialog) return;
if (open && !dialog.open) {
dialog.showModal();
} else if (!open && dialog.open) {
dialog.close();
}
}, [open]);
return (
<dialog
ref={ref}
onClose={onCancel}
onClick={(e) => {
// Click on the dialog background (not the inner form) cancels.
if (e.target === ref.current) onCancel();
}}
className="m-auto w-[min(92vw,440px)] rounded-2xl bg-white p-0 ring-1 ring-zinc-200/60 backdrop:bg-zinc-900/50 backdrop:backdrop-blur-sm"
>
<form
method="dialog"
onSubmit={(e) => {
e.preventDefault();
onConfirm();
}}
className="flex flex-col gap-4 p-6"
>
<h2 className="text-lg font-semibold tracking-tight text-zinc-900">
{title}
</h2>
<div className="text-sm leading-relaxed text-zinc-600">{message}</div>
<div className="mt-2 flex items-center justify-end gap-2">
<button
type="button"
onClick={onCancel}
disabled={pending}
className="rounded-full px-4 py-2 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-100 hover:text-zinc-900 disabled:opacity-60"
>
Cancel
</button>
<button
type="submit"
disabled={pending}
className={`rounded-full px-4 py-2 text-xs font-medium text-white transition-colors disabled:opacity-60 ${
destructive ? "bg-red-600 hover:bg-red-700" : "bg-zinc-900 hover:bg-zinc-700"
}`}
>
{pending ? "…" : confirmLabel}
</button>
</div>
</form>
</dialog>
);
}