Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

113 lines
3.3 KiB
TypeScript

"use client";
import { useEffect, type ReactNode } from "react";
import { X } from "lucide-react";
interface ConfirmDialogProps {
open: boolean;
title: string;
/** Body content — plain text or richer markup (e.g. an avatar row). */
children?: ReactNode;
confirmLabel: string;
cancelLabel?: string;
/** "danger" renders a red confirm button for destructive actions. */
tone?: "default" | "danger";
/** Disables the buttons and swaps the confirm label while pending. */
busy?: boolean;
busyLabel?: string;
onConfirm: () => void;
onCancel: () => void;
}
/**
* Small confirmation modal in the app's dialog style (overlay + card),
* replacing bare window.confirm() prompts. Closes on Escape and on
* overlay click; the cancel button takes initial focus so a stray Enter
* never triggers a destructive action.
*/
export function ConfirmDialog({
open,
title,
children,
confirmLabel,
cancelLabel = "Cancel",
tone = "default",
busy = false,
busyLabel,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !busy) onCancel();
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [open, busy, onCancel]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-[var(--overlay)] px-4"
role="alertdialog"
aria-modal="true"
aria-label={title}
onClick={() => {
if (!busy) onCancel();
}}
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-sm rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5 shadow-xl"
>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-base font-semibold text-[var(--foreground)]">
{title}
</h2>
<button
type="button"
onClick={onCancel}
disabled={busy}
className="rounded-md p-1 text-[var(--muted-foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)] disabled:opacity-40"
aria-label="Close"
>
<X size={16} />
</button>
</div>
{children && (
<div className="mb-4 text-sm text-[var(--muted-foreground)]">
{children}
</div>
)}
<div className="flex items-center justify-end gap-2">
<button
type="button"
onClick={onCancel}
disabled={busy}
autoFocus
className="rounded-lg px-3 py-1.5 text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] disabled:opacity-40"
>
{cancelLabel}
</button>
<button
type="button"
onClick={onConfirm}
disabled={busy}
className={`rounded-lg px-3 py-1.5 text-sm font-medium disabled:opacity-40 transition-colors ${
tone === "danger"
? "bg-red-600 text-white hover:bg-red-700"
: "bg-[var(--foreground)] text-[var(--background)] hover:opacity-90"
}`}
>
{busy ? (busyLabel ?? confirmLabel) : confirmLabel}
</button>
</div>
</div>
</div>
);
}