Responsive Modal Default
Responsive Modal Default é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
UI Layouts
MIT
overlays
O que é
Responsive Modal Default é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion lucide-react
Licença
Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Responsive Modal Default
'use client';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { Drawer as VaulDrawer } from 'vaul';
interface DrawerContextProps {
open: boolean;
setOpen: (open: boolean) => void;
}
const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);
export const useResponsiveModal = () => {
const context = useContext(DrawerContext);
if (!context) {
throw new Error('useResponsiveModal must be used within a ResponsiveModalProvider');
}
return context;
};
interface ResponsiveModalProps {
children: ReactNode;
open?: boolean;
setOpen?: (open: boolean) => void;
classname?: string;
clsBtnClassname?: string;
}
export function ResponsiveModal({
children,
open: controlledOpen,
setOpen: controlledSetOpen,
classname,
clsBtnClassname,
}: ResponsiveModalProps) {
const [internalOpen, setInternalOpen] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
const [mounted, setMounted] = useState(false);
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
const setOpen = controlledSetOpen || setInternalOpen;
useEffect(() => {
setMounted(true);
const mediaQuery = window.matchMedia('(min-width: 768px)');
const handleMediaChange = (event: MediaQueryListEvent) => {
setIsDesktop(event.matches);
};
setIsDesktop(mediaQuery.matches);
mediaQuery.addEventListener('change', handleMediaChange);
return () => mediaQuery.removeEventListener('change', handleMediaChange);
}, []);
const trigger = React.Children.toArray(children).find(
(child: any) => child.type === ResponsiveModalTrigger
);
const content = React.Children.toArray(children).filter(
(child: any) => child.type !== ResponsiveModalTrigger
);
const desktopModal =
isDesktop && mounted
? createPortal(
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-xs cursor-zoom-out'
onClick={() => setOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: 'spring', duration: 0.5 }}
onClick={(e) => e.stopPropagation()}
className={cn(
'relative w-full max-w-md border bg-white dark:bg-neutral-900 rounded-lg cursor-default',
classname
)}
>
<button
className={cn(
'absolute top-2 right-2 bg-primary text-background p-2 border z-1 rounded-md',
clsBtnClassname
)}
onClick={() => setOpen(false)}
aria-label='Close'
>
<X />
</button>
{content}
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body
)
: null;
return (
<DrawerContext.Provider value={{ open, setOpen }}>
{trigger}
{desktopModal}
{!isDesktop && (
<VaulDrawer.Root shouldScaleBackground open={open} onOpenChange={setOpen}>
<VaulDrawer.Portal>
<VaulDrawer.Overlay className='fixed inset-0 z-50 bg-white/50 dark:bg-black/50 backdrop-blur-xs' />
<VaulDrawer.Content className='fixed bottom-0 left-0 z-50 w-full max-h-[96%] bg-white dark:bg-neutral-900'>
<div className='mx-auto w-16 h-[0.30rem] shrink-0 rounded-full bg-neutral-600 my-4' />
<div className='w-full mx-auto max-h-[96vh] overflow-auto px-4 pb-2'>{content}</div>
</VaulDrawer.Content>
</VaulDrawer.Portal>
</VaulDrawer.Root>
)}
</DrawerContext.Provider>
);
}
export function ResponsiveModalContent({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn('', className)}>{children}</div>;
}
interface ResponsiveModalTriggerProps {
children: ReactNode;
asChild?: boolean;
}
export function ResponsiveModalTrigger({ children, asChild = false }: ResponsiveModalTriggerProps) {
const { setOpen } = useResponsiveModal();
if (asChild) {
// Clone the child element and add onClick handler
const child = React.Children.only(children) as React.ReactElement<any>;
return React.cloneElement(child, {
...child.props,
onClick: (e: React.MouseEvent) => {
setOpen(true);
// Call original onClick if it exists
child.props.onClick?.(e);
},
} as any);
}
return <div onClick={() => setOpen(true)}>{children}</div>;
}
'use client';
import {
ResponsiveModal,
ResponsiveModalContent,
ResponsiveModalTrigger,
} from '@/components/ui/responsive-modal';
import { useMediaQuery } from '@/hooks/use-media-query';
import { motion } from 'motion/react';
import { useState } from 'react';
import { toast } from 'sonner';
export default function MyDrawer() {
// const [drawerOpen, setDrawerOpen] = useState(false);
const handleSubmit = () => {
toast.success('Profile image updated successfully');
// setDrawerOpen(false);
};
return (
<>
{/* <div className='flex justify-between'>
<motion.button
onClick={() => setDrawerOpen(true)}
whileTap={{ scale: 0.9 }}
className='inline-flex h-12 w-fit mx-auto animate-background-shine items-center justify-center rounded-md border-2 dark:border-[#656fe2] border-[#c0c6fc] dark:bg-[linear-gradient(110deg,#1e2a78,45%,#3749be,55%,#1e2a78)] bg-[linear-gradient(110deg,#3d5af1,45%,#5471ff,55%,#3d5af1)] bg-size-[200%_100%] dark:hover:border-white px-6 font-medium text-white dark:text-white transition-colors focus:outline-hidden focus:ring-2 dark:focus:ring-neutral-400 focus:ring-offset-2 focus:ring-offset-neutral-50'
>
Open Dialog
</motion.button>
</div> */}
{/* <ResponsiveModal open={drawerOpen} setOpen={setDrawerOpen}> */}
<div className='flex justify-center items-center'>
<ResponsiveModal>
<ResponsiveModalTrigger asChild>
<motion.button
whileTap={{ scale: 0.9 }}
className='inline-flex h-12 w-fit mx-auto animate-background-shine items-center justify-center rounded-md border-2 dark:border-[#656fe2] border-[#c0c6fc] dark:bg-[linear-gradient(110deg,#1e2a78,45%,#3749be,55%,#1e2a78)] bg-[linear-gradient(110deg,#3d5af1,45%,#5471ff,55%,#3d5af1)] bg-size-[200%_100%] dark:hover:border-white px-6 font-medium text-white dark:text-white transition-colors focus:outline-hidden focus:ring-2 dark:focus:ring-neutral-400 focus:ring-offset-2 focus:ring-offset-neutral-50'
>
Open Dialog
</motion.button>
</ResponsiveModalTrigger>
<ResponsiveModalContent>
<figure className='flex flex-col space-y-1.5 text-center h-fit dark:bg-neutral-800 md:p-4 p-6'>
<h1 className='font-medium text-2xl'>Update Profile Image</h1>
<p className='text-sm text-muted-foreground w-4/5 mx-auto'>
Upload a new profile image or remove the current one.
</p>
<div data-vaul-no-drag className='py-4 space-y-4'>
<span className='relative flex justify-center overflow-hidden rounded-xl w-full '>
<span className='grid place-content-center h-40 w-40 rounded-xl dark:bg-neutral-950 bg-muted'>
JP
</span>
</span>
<div className='mb-3'>
<input
className='w-full border file:p-2 file:bg-black file:border-none file:text-white rounded-xs overflow-hidden'
type='file'
id='formFile'
/>
</div>
<button
type='submit'
onClick={handleSubmit}
className='w-full rounded-xs dark:bg-white bg-black p-2 dark:text-black text-white'
>
Submit
</button>
</div>
</figure>
</ResponsiveModalContent>
</ResponsiveModal>
</div>
</>
);
}