// components/ui/dialog.tsx import * as React from 'react'; import { View, Pressable, Animated, StyleSheet } from 'react-native'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; import { Text } from './text'; import { AdaptiveModal } from '@lunar-primitive/adaptive-modal'; // ─── CVA variants ───────────────────────────────────────────────────────────── const dialogContentVariants = cva( 'bg-background rounded-2xl border border-border shadow-2xl overflow-hidden', { variants: { size: { sm: 'web:max-w-sm', md: 'web:max-w-md', lg: 'web:max-w-lg', full: 'web:max-w-full', }, }, defaultVariants: { size: 'md', }, } ); const dialogHeaderVariants = cva('mb-4', { variants: { align: { start: 'items-start', center: 'items-center', }, }, defaultVariants: { align: 'start' }, }); const dialogFooterVariants = cva('flex-row gap-2 mt-6', { variants: { justify: { end: 'justify-end', start: 'justify-start', center: 'justify-center', between: 'justify-between', }, }, defaultVariants: { justify: 'end' }, }); // ─── Interfaces ─────────────────────────────────────────────────────────────── interface DialogProps { open?: boolean; onOpenChange?: (open: boolean) => void; children: React.ReactNode; } interface DialogContentProps extends VariantProps { children: React.ReactNode; className?: string; /** Whether tapping the backdrop closes the dialog. @default true */ closeOnBackdropPress?: boolean; } interface DialogHeaderProps extends VariantProps { children: React.ReactNode; className?: string; } interface DialogFooterProps extends VariantProps { children: React.ReactNode; className?: string; } interface DialogTitleProps { children: React.ReactNode; className?: string; } interface DialogDescriptionProps { children: React.ReactNode; className?: string; } // ─── Context ────────────────────────────────────────────────────────────────── const DialogContext = React.createContext<{ open: boolean; onOpenChange: (open: boolean) => void; } | null>(null); function useDialog() { const context = React.useContext(DialogContext); if (!context) { throw new Error('Dialog components must be used within Dialog'); } return context; } // ─── Dialog Root ────────────────────────────────────────────────────────────── export function Dialog({ open: controlledOpen, onOpenChange: controlledOnOpenChange, children }: DialogProps) { const [internalOpen, setInternalOpen] = React.useState(false); const open = controlledOpen !== undefined ? controlledOpen : internalOpen; const onOpenChange = controlledOnOpenChange || setInternalOpen; return ( {children} ); } // ─── Dialog Trigger ─────────────────────────────────────────────────────────── export function DialogTrigger({ children }: { children: React.ReactNode }) { const { onOpenChange } = useDialog(); if (React.isValidElement(children)) { return React.cloneElement(children as React.ReactElement, { onPress: () => onOpenChange(true), }); } return ( onOpenChange(true)}> {children} ); } // ─── Dialog Content ─────────────────────────────────────────────────────────── export function DialogContent({ children, className, size, closeOnBackdropPress = true }: DialogContentProps) { const { open, onOpenChange } = useDialog(); const [visible, setVisible] = React.useState(false); const scaleAnim = React.useRef(new Animated.Value(0.95)).current; const opacityAnim = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { if (open) { setVisible(true); Animated.parallel([ Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true, tension: 80, friction: 10, }), Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } else { Animated.parallel([ Animated.timing(scaleAnim, { toValue: 0.95, duration: 150, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 0, duration: 150, useNativeDriver: true, }), ]).start(() => { setVisible(false); }); } }, [open]); return ( onOpenChange(false)} closeOnBackdropPress={closeOnBackdropPress} backdropColor="transparent" animationType="none" > {/* Full-screen root with backdrop */} {/* Backdrop dismissal layer */} onOpenChange(false)} style={StyleSheet.absoluteFillObject} /> {/* Centering wrapper with screen margin */} e.stopPropagation()} className={cn('w-full', dialogContentVariants({ size }), className)} > {children} ); } // ─── Dialog Header ──────────────────────────────────────────────────────────── export function DialogHeader({ children, className, align }: DialogHeaderProps) { return ( {children} ); } // ─── Dialog Title ───────────────────────────────────────────────────────────── export function DialogTitle({ children, className }: DialogTitleProps) { return ( {children} ); } // ─── Dialog Description ─────────────────────────────────────────────────────── export function DialogDescription({ children, className }: DialogDescriptionProps) { return ( {children} ); } // ─── Dialog Footer ──────────────────────────────────────────────────────────── export function DialogFooter({ children, className, justify }: DialogFooterProps) { return ( {children} ); } // ─── Dialog Close ───────────────────────────────────────────────────────────── export function DialogClose({ children }: { children: React.ReactNode }) { const { onOpenChange } = useDialog(); if (React.isValidElement(children)) { return React.cloneElement(children as React.ReactElement, { onPress: () => onOpenChange(false), }); } return ( onOpenChange(false)}> {children} ); }