mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { AlertTriangle } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export function ConfirmDialog({
|
|
cancelLabel = "Cancel",
|
|
confirmLabel = "Confirm",
|
|
description,
|
|
destructive = false,
|
|
loading = false,
|
|
onCancel,
|
|
onConfirm,
|
|
open,
|
|
title,
|
|
}: ConfirmDialogProps) {
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Focus the confirm button when opened; trap ESC to cancel.
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const prevActive = document.activeElement as HTMLElement | null;
|
|
dialogRef.current
|
|
?.querySelector<HTMLButtonElement>("[data-confirm]")
|
|
?.focus();
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
onCancel();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", onKey);
|
|
const prevOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = "hidden";
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", onKey);
|
|
document.body.style.overflow = prevOverflow;
|
|
prevActive?.focus?.();
|
|
};
|
|
}, [open, onCancel]);
|
|
|
|
if (!open) return null;
|
|
|
|
return createPortal(
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="confirm-dialog-title"
|
|
aria-describedby={description ? "confirm-dialog-desc" : undefined}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onCancel();
|
|
}}
|
|
className={cn(
|
|
"fixed inset-0 z-50 flex items-center justify-center",
|
|
"bg-black/60 backdrop-blur-sm",
|
|
"animate-[fade-in_150ms_ease-out]",
|
|
)}
|
|
>
|
|
<div
|
|
ref={dialogRef}
|
|
className={cn(
|
|
"relative w-full max-w-md mx-4",
|
|
"border border-border bg-card shadow-lg",
|
|
"animate-[dialog-in_180ms_ease-out]",
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3 p-4 border-b border-border">
|
|
{destructive && (
|
|
<div
|
|
aria-hidden
|
|
className="mt-0.5 shrink-0 text-destructive"
|
|
>
|
|
<AlertTriangle className="h-4 w-4" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
<h2
|
|
id="confirm-dialog-title"
|
|
className="font-expanded text-sm font-bold tracking-[0.08em] uppercase blend-lighter"
|
|
>
|
|
{title}
|
|
</h2>
|
|
|
|
{description && (
|
|
<p
|
|
id="confirm-dialog-desc"
|
|
className="font-mondwest text-xs text-muted-foreground leading-relaxed"
|
|
>
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 p-3">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onCancel}
|
|
disabled={loading}
|
|
>
|
|
{cancelLabel}
|
|
</Button>
|
|
<Button
|
|
data-confirm
|
|
type="button"
|
|
variant={destructive ? "destructive" : "default"}
|
|
size="sm"
|
|
onClick={onConfirm}
|
|
disabled={loading}
|
|
>
|
|
{loading ? "…" : confirmLabel}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|
|
|
|
interface ConfirmDialogProps {
|
|
cancelLabel?: string;
|
|
confirmLabel?: string;
|
|
description?: string;
|
|
destructive?: boolean;
|
|
loading?: boolean;
|
|
onCancel: () => void;
|
|
onConfirm: () => void;
|
|
open: boolean;
|
|
title: string;
|
|
}
|