import { Dialog as D } from "radix-ui";
import type { ReactNode } from "react";
import { Button } from "./Button";
import { cn } from "./cn";
import { Icon } from "./Icon";
// Open on the first field, or on the dialog itself (its title is read), never on Close.
const focusFirstField = (e: Event) => {
e.preventDefault();
const content = e.currentTarget as HTMLElement;
(content.querySelector<HTMLElement>("input:not([type=hidden]):not(:disabled), textarea:not(:disabled), select:not(:disabled)") ?? content).focus();
};
const overlay = "fixed inset-0 z-40 bg-[rgba(24,24,27,0.32)] data-[state=open]:animate-[context-fade_160ms_ease-out]";
function Head({ title }: { title: string }) {
return (
<div className="flex items-center justify-between gap-3">
<D.Title className="m-0 text-[16px] leading-6 font-semibold tracking-[-0.01em] text-ink">{title}</D.Title>
<D.Close asChild>
<Button variant="ghost" size="icon-sm" aria-label="Close" className="-mr-1.5 text-muted">
<Icon name="close-line" size={18} />
</Button>
</D.Close>
</div>
);
}
/**
* A centred dialog. Focus stays inside and Escape closes it; a click outside does not, so a half-typed
* form is never lost. The body unmounts when it closes.
*/
export function Modal({ open, title, onClose, children, wide }: { open: boolean; title: string; onClose: () => void; children: ReactNode; wide?: boolean }) {
return (
<D.Root open={open} onOpenChange={(o) => !o && onClose()}>
<D.Portal>
<D.Overlay className={overlay} />
<D.Content
aria-describedby={undefined}
tabIndex={-1}
onOpenAutoFocus={focusFirstField}
onInteractOutside={(e) => e.preventDefault()}
className={cn(
"fixed top-1/2 left-1/2 z-50 flex max-h-[calc(100dvh-32px)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 overflow-y-auto rounded-2xl bg-page p-5 text-[13.5px] leading-5 text-body shadow-pop outline-none data-[state=open]:animate-[context-pop_200ms_var(--ease-out-expo)]",
wide ? "w-[min(720px,calc(100vw-32px))]" : "w-[min(480px,calc(100vw-32px))]",
)}
>
<Head title={title} />
{children}
</D.Content>
</D.Portal>
</D.Root>
);
}
/** A side drawer for sources and explanations ("sources in drawers"). A click outside closes it. */
export function Drawer({ open, title, onClose, children }: { open: boolean; title: string; onClose: () => void; children: ReactNode }) {
return (
<D.Root open={open} onOpenChange={(o) => !o && onClose()}>
<D.Portal>
<D.Overlay className={overlay} />
<D.Content
aria-describedby={undefined}
tabIndex={-1}
onOpenAutoFocus={focusFirstField}
className="fixed inset-y-0 right-0 z-50 flex w-[min(520px,100vw)] flex-col gap-3 overflow-y-auto bg-page px-5 pt-4 pb-7 text-[13.5px] leading-5 text-body shadow-pop outline-none data-[state=open]:animate-[context-slide_260ms_var(--ease-out-expo)] sm:rounded-l-2xl"
>
<Head title={title} />
{children}
</D.Content>
</D.Portal>
</D.Root>
);
}
/** The buttons along the bottom of a dialog: secondary first, the one action last. */
export const DialogActions = ({ children, className }: { children: ReactNode; className?: string }) => (
<div className={cn("flex flex-wrap justify-end gap-2 pt-1", className)}>{children}</div>
);