TorbatYar/frontend/components/ds/Dialog.tsx
Mortezakoohjani 7953e47f8b Ship accounting UX: Rial integers, auto doc numbers, reverse-and-edit, real reports, customers nav.
Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 15:52:06 +03:30

119 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "./Button";
export function Dialog({
open,
onClose,
title,
description,
children,
className,
size = "md",
}: {
open: boolean;
onClose: () => void;
title: string;
description?: string;
children: React.ReactNode;
className?: string;
size?: "sm" | "md" | "lg" | "xl";
}) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (!open || typeof document === "undefined") return null;
const widths = {
sm: "max-w-md",
md: "max-w-lg",
lg: "max-w-2xl",
xl: "max-w-4xl",
};
return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<button
type="button"
aria-label="بستن"
className="absolute inset-0 bg-black/40 backdrop-blur-[1px]"
onClick={onClose}
/>
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
className={cn(
"relative z-10 max-h-[92vh] w-full overflow-y-auto rounded-2xl border border-[var(--border)] bg-[var(--surface)] shadow-[var(--shadow-lg)]",
widths[size],
className
)}
>
<div className="sticky top-0 z-10 flex items-start justify-between gap-3 border-b border-[var(--border)] bg-[var(--surface)] px-5 py-4">
<div>
<h2 id="dialog-title" className="text-base font-semibold text-secondary">
{title}
</h2>
{description ? (
<p className="mt-1 text-sm text-[var(--muted)]">{description}</p>
) : null}
</div>
<Button type="button" variant="ghost" size="icon" onClick={onClose} aria-label="بستن">
<X className="h-4 w-4" />
</Button>
</div>
<div className="px-5 py-4">{children}</div>
</div>
</div>,
document.body
);
}
export function ConfirmDialog({
open,
onClose,
onConfirm,
title,
description,
confirmLabel = "تأیید",
danger,
loading,
}: {
open: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description?: string;
confirmLabel?: string;
danger?: boolean;
loading?: boolean;
}) {
return (
<Dialog open={open} onClose={onClose} title={title} description={description} size="sm">
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
انصراف
</Button>
<Button
type="button"
variant={danger ? "danger" : "default"}
onClick={onConfirm}
disabled={loading}
>
{loading ? "در حال انجام…" : confirmLabel}
</Button>
</div>
</Dialog>
);
}