import * as RadixDialog from '@radix-ui/react-dialog'; import { motion, type Variants } from 'framer-motion'; import React, { memo, type ReactNode, useState, useEffect } from 'react'; import { classNames } from '~/utils/classNames'; import { cubicEasingFn } from '~/utils/easings'; import { IconButton } from './IconButton'; import { Button } from './Button'; import { FixedSizeList } from 'react-window'; import { Checkbox } from './Checkbox'; import { Label } from './Label'; export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog'; interface DialogButtonProps { type: 'primary' | 'secondary' | 'danger'; children: ReactNode; onClick?: (event: React.MouseEvent) => void; disabled?: boolean; } export const DialogButton = memo(({ type, children, onClick, disabled }: DialogButtonProps) => { return ( {children} ); }); export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => { return ( {children} ); }); export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => { return ( {children} ); }); const transition = { duration: 0.15, ease: cubicEasingFn, }; export const dialogBackdropVariants = { closed: { opacity: 0, transition, }, open: { opacity: 1, transition, }, } satisfies Variants; export const dialogVariants = { closed: { x: '-50%', y: '-40%', scale: 0.96, opacity: 0, transition, }, open: { x: '-50%', y: '-50%', scale: 1, opacity: 1, transition, }, } satisfies Variants; interface DialogProps { children: ReactNode; className?: string; showCloseButton?: boolean; onClose?: () => void; onBackdrop?: () => void; } export const Dialog = memo(({ children, className, showCloseButton = true, onClose, onBackdrop }: DialogProps) => { return ( {children} {showCloseButton && ( )} ); }); /** * Props for the ConfirmationDialog component */ export interface ConfirmationDialogProps { /** * Whether the dialog is open */ isOpen: boolean; /** * Callback when the dialog is closed */ onClose: () => void; /** * Callback when the confirm button is clicked */ onConfirm: () => void; /** * The title of the dialog */ title: string; /** * The description of the dialog */ description: string; /** * The text for the confirm button */ confirmLabel?: string; /** * The text for the cancel button */ cancelLabel?: string; /** * The variant of the confirm button */ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; /** * Whether the confirm button is in a loading state */ isLoading?: boolean; } /** * A reusable confirmation dialog component that uses the Dialog component */ export function ConfirmationDialog({ isOpen, onClose, title, description, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'default', isLoading = false, onConfirm, }: ConfirmationDialogProps) { return ( {title} {description} {cancelLabel} {isLoading ? ( <> {confirmLabel} > ) : ( confirmLabel )} ); } /** * Type for selection item in SelectionDialog */ type SelectionItem = { id: string; label: string; description?: string; }; /** * Props for the SelectionDialog component */ export interface SelectionDialogProps { /** * The title of the dialog */ title: string; /** * The items to select from */ items: SelectionItem[]; /** * Whether the dialog is open */ isOpen: boolean; /** * Callback when the dialog is closed */ onClose: () => void; /** * Callback when the confirm button is clicked with selected item IDs */ onConfirm: (selectedIds: string[]) => void; /** * The text for the confirm button */ confirmLabel?: string; /** * The maximum height of the selection list */ maxHeight?: string; } /** * A reusable selection dialog component that uses the Dialog component */ export function SelectionDialog({ title, items, isOpen, onClose, onConfirm, confirmLabel = 'Confirm', maxHeight = '60vh', }: SelectionDialogProps) { const [selectedItems, setSelectedItems] = useState([]); const [selectAll, setSelectAll] = useState(false); // Reset selected items when dialog opens useEffect(() => { if (isOpen) { setSelectedItems([]); setSelectAll(false); } }, [isOpen]); const handleToggleItem = (id: string) => { setSelectedItems((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id])); }; const handleSelectAll = () => { if (selectedItems.length === items.length) { setSelectedItems([]); setSelectAll(false); } else { setSelectedItems(items.map((item) => item.id)); setSelectAll(true); } }; const handleConfirm = () => { onConfirm(selectedItems); onClose(); }; // Calculate the height for the virtualized list const listHeight = Math.min( items.length * 60, parseInt(maxHeight.replace('vh', '')) * window.innerHeight * 0.01 - 40, ); // Render each item in the virtualized list const ItemRenderer = ({ index, style }: { index: number; style: React.CSSProperties }) => { const item = items[index]; return ( handleToggleItem(item.id)} /> {item.label} {item.description && {item.description}} ); }; return ( {title} Select the items you want to include and click{' '} {confirmLabel}. {selectedItems.length} of {items.length} selected {selectAll ? 'Deselect All' : 'Select All'} {items.length > 0 ? ( {ItemRenderer} ) : ( No items to display )} Cancel {confirmLabel} ); }
{item.description}