253 lines
6.9 KiB
TypeScript
253 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Dialog as DialogPrimitive } from 'radix-ui';
|
|
import { X } from 'lucide-react';
|
|
import {
|
|
AnimatePresence,
|
|
motion,
|
|
type HTMLMotionProps,
|
|
type Transition,
|
|
} from 'motion/react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
|
|
type DialogContextType = {
|
|
isOpen: boolean;
|
|
};
|
|
|
|
const DialogContext = React.createContext<DialogContextType | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const useDialog = (): DialogContextType => {
|
|
const context = React.useContext(DialogContext);
|
|
if (!context) {
|
|
throw new Error('useDialog must be used within a Dialog');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;
|
|
|
|
function Dialog({ children, ...props }: DialogProps) {
|
|
const [isOpen, setIsOpen] = React.useState(
|
|
props?.open ?? props?.defaultOpen ?? false,
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (props?.open !== undefined) setIsOpen(props.open);
|
|
}, [props?.open]);
|
|
|
|
const handleOpenChange = React.useCallback(
|
|
(open: boolean) => {
|
|
setIsOpen(open);
|
|
props.onOpenChange?.(open);
|
|
},
|
|
[props],
|
|
);
|
|
|
|
return (
|
|
<DialogContext.Provider value={{ isOpen }}>
|
|
<DialogPrimitive.Root
|
|
data-slot="dialog"
|
|
{...props}
|
|
onOpenChange={handleOpenChange}
|
|
>
|
|
{children}
|
|
</DialogPrimitive.Root>
|
|
</DialogContext.Provider>
|
|
);
|
|
}
|
|
|
|
type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;
|
|
|
|
function DialogTrigger(props: DialogTriggerProps) {
|
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
}
|
|
|
|
type DialogPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal>;
|
|
|
|
function DialogPortal(props: DialogPortalProps) {
|
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
}
|
|
|
|
type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>;
|
|
|
|
function DialogClose(props: DialogCloseProps) {
|
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
}
|
|
|
|
type DialogOverlayProps = React.ComponentProps<typeof DialogPrimitive.Overlay>;
|
|
|
|
function DialogOverlay({ className, ...props }: DialogOverlayProps) {
|
|
return (
|
|
<DialogPrimitive.Overlay
|
|
data-slot="dialog-overlay"
|
|
className={cn(
|
|
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type FlipDirection = 'top' | 'bottom' | 'left' | 'right';
|
|
|
|
type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> &
|
|
HTMLMotionProps<'div'> & {
|
|
from?: FlipDirection;
|
|
transition?: Transition;
|
|
};
|
|
|
|
function DialogContent({
|
|
className,
|
|
children,
|
|
from = 'top',
|
|
transition = { type: 'spring', stiffness: 150, damping: 25 },
|
|
...props
|
|
}: DialogContentProps) {
|
|
const { isOpen } = useDialog();
|
|
|
|
const initialRotation =
|
|
from === 'top' || from === 'left' ? '20deg' : '-20deg';
|
|
const isVertical = from === 'top' || from === 'bottom';
|
|
const rotateAxis = isVertical ? 'rotateX' : 'rotateY';
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<DialogPortal forceMount data-slot="dialog-portal">
|
|
<DialogOverlay asChild forceMount>
|
|
<motion.div
|
|
key="dialog-overlay"
|
|
initial={{ opacity: 0, filter: 'blur(4px)' }}
|
|
animate={{ opacity: 1, filter: 'blur(0px)' }}
|
|
exit={{ opacity: 0, filter: 'blur(4px)' }}
|
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
/>
|
|
</DialogOverlay>
|
|
<DialogPrimitive.Content asChild forceMount {...props}>
|
|
<motion.div
|
|
key="dialog-content"
|
|
data-slot="dialog-content"
|
|
initial={{
|
|
opacity: 0,
|
|
filter: 'blur(4px)',
|
|
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
|
}}
|
|
animate={{
|
|
opacity: 1,
|
|
filter: 'blur(0px)',
|
|
transform: `perspective(500px) ${rotateAxis}(0deg) scale(1)`,
|
|
}}
|
|
exit={{
|
|
opacity: 0,
|
|
filter: 'blur(4px)',
|
|
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
|
}}
|
|
transition={transition}
|
|
className={cn(
|
|
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg rounded-xl',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Close</span>
|
|
</DialogPrimitive.Close>
|
|
</motion.div>
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|
|
|
|
type DialogHeaderProps = React.ComponentProps<'div'>;
|
|
|
|
function DialogHeader({ className, ...props }: DialogHeaderProps) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-header"
|
|
className={cn(
|
|
'flex flex-col space-y-1.5 text-center sm:text-left',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type DialogFooterProps = React.ComponentProps<'div'>;
|
|
|
|
function DialogFooter({ className, ...props }: DialogFooterProps) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-footer"
|
|
className={cn(
|
|
'flex flex-col-reverse sm:flex-row sm:justify-end gap-2',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type DialogTitleProps = React.ComponentProps<typeof DialogPrimitive.Title>;
|
|
|
|
function DialogTitle({ className, ...props }: DialogTitleProps) {
|
|
return (
|
|
<DialogPrimitive.Title
|
|
data-slot="dialog-title"
|
|
className={cn(
|
|
'text-lg font-semibold leading-none tracking-tight',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type DialogDescriptionProps = React.ComponentProps<
|
|
typeof DialogPrimitive.Description
|
|
>;
|
|
|
|
function DialogDescription({ className, ...props }: DialogDescriptionProps) {
|
|
return (
|
|
<DialogPrimitive.Description
|
|
data-slot="dialog-description"
|
|
className={cn('text-sm text-muted-foreground', className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Dialog,
|
|
DialogPortal,
|
|
DialogOverlay,
|
|
DialogClose,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogFooter,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
useDialog,
|
|
type DialogContextType,
|
|
type DialogProps,
|
|
type DialogTriggerProps,
|
|
type DialogPortalProps,
|
|
type DialogCloseProps,
|
|
type DialogOverlayProps,
|
|
type DialogContentProps,
|
|
type DialogHeaderProps,
|
|
type DialogFooterProps,
|
|
type DialogTitleProps,
|
|
type DialogDescriptionProps,
|
|
};
|