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
113 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|