import * as React from 'react'; import { cn } from '@/lib/utils'; import { buttonVariants } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, } from '@/components/ui/alert-dialog'; const KICKER_CLASS = 'font-mono text-[10px] uppercase tracking-[0.22em]'; type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'wide'; const SIZE_CLASS: Record = { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-xl w-[95vw]', wide: 'max-w-5xl w-[95vw]', }; interface ModalProps { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode; size?: ModalSize; className?: string; /** Pass through to DialogContent. Set to false when the modal renders its own close affordance. */ showClose?: boolean; /** Fill the viewport below md (for large overlays like logs / shell). Opt-in so * small confirm dialogs keep their compact centered size on a phone. */ mobileFullScreen?: boolean; } export function Modal({ open, onOpenChange, children, size = 'md', className, showClose, mobileFullScreen }: ModalProps) { return ( {children} ); } interface ModalHeaderBaseProps { kicker: string; title: React.ReactNode; description?: string; } type HeaderVariant = 'default' | 'destructive'; const HEADER_VARIANT: Record = { default: { rail: 'bg-brand', kicker: 'text-stat-subtitle' }, destructive: { rail: 'bg-destructive', kicker: 'text-destructive' }, }; interface HeaderShellProps extends ModalHeaderBaseProps { variant: HeaderVariant; TitleComponent: React.ElementType; DescriptionComponent: React.ElementType; } function HeaderShell({ kicker, title, description, variant, TitleComponent, DescriptionComponent, }: HeaderShellProps) { const v = HEADER_VARIANT[variant]; return (
{kicker}
{title} {description ?? (typeof title === 'string' ? title : kicker)}
); } export function ModalHeader(props: ModalHeaderBaseProps) { return ( ); } export function ModalDestructiveHeader(props: ModalHeaderBaseProps) { return ( ); } function ConfirmHeader(props: ModalHeaderBaseProps) { return ( ); } function ConfirmDestructiveHeader(props: ModalHeaderBaseProps) { return ( ); } export function ModalBody({ className, ...props }: React.HTMLAttributes) { return
; } interface ModalFooterProps { primary: React.ReactNode; secondary?: React.ReactNode; hint?: React.ReactNode; hintAccent?: React.ReactNode; } export function ModalFooter({ primary, secondary, hint, hintAccent }: ModalFooterProps) { return (
{hint} {hintAccent !== undefined && ( {hintAccent} )}
{secondary} {primary}
); } type ConfirmSize = 'sm' | 'md'; interface ConfirmModalProps { open: boolean; onOpenChange: (open: boolean) => void; variant?: 'default' | 'destructive'; size?: ConfirmSize; kicker: string; title: React.ReactNode; description?: string; hint?: React.ReactNode; confirmLabel: React.ReactNode; cancelLabel?: React.ReactNode; confirming?: boolean; onConfirm: () => void | Promise; onCancel?: () => void; children?: React.ReactNode; } export function ConfirmModal({ open, onOpenChange, variant = 'default', size = 'sm', kicker, title, description, hint, confirmLabel, cancelLabel = 'Cancel', confirming = false, onConfirm, onCancel, children, }: ConfirmModalProps) { const Header = variant === 'destructive' ? ConfirmDestructiveHeader : ConfirmHeader; const cancelClass = buttonVariants({ variant: 'outline', size: 'sm' }); const actionClass = buttonVariants({ variant: variant === 'destructive' ? 'destructive' : 'default', size: 'sm', }); return (
{children !== undefined && {children}} {cancelLabel} } primary={ { const result = onConfirm(); // Async confirms keep the dialog open so the caller can render // `confirming` state and close via onOpenChange when work completes. // Sync confirms let Radix auto-close. if (result instanceof Promise) { e.preventDefault(); void result; } }} > {confirmLabel} } /> ); }