Directional Drawer_default
Directional Drawer_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 é
Directional Drawer_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.
Directional Drawer_default
'use client';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
import { Drawer as VaulSidebar } from 'vaul';
interface DrawerContextProps {
open: boolean;
setOpen: (open: boolean) => void;
}
const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);
export const useDirectionalDrawer = () => {
const context = useContext(DrawerContext);
if (!context) {
throw new Error('useDirectionalDrawer must be used within a DirectionalDrawer');
}
return context;
};
interface DirectionalDrawerProps {
children: ReactNode;
open?: boolean;
setOpen?: (open: boolean) => void;
direction?: 'left' | 'right' | 'top' | 'bottom';
outsideClose?: boolean;
className?: string;
}
export function DirectionalDrawer({
children,
open: controlledOpen,
setOpen: controlledSetOpen,
direction = 'left',
outsideClose = true,
className,
}: DirectionalDrawerProps) {
const [internalOpen, setInternalOpen] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
const setOpen = controlledSetOpen || setInternalOpen;
useEffect(() => {
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 === DrawerTrigger
);
const content = React.Children.toArray(children).filter(
(child: any) => child.type !== DrawerTrigger
);
// Helper function to get positioning and sizing classes
const getDirectionClasses = () => {
switch (direction) {
case 'right':
return {
position: 'right-0 bottom-0',
size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
border: 'border-l',
handlePosition: 'top-[40%] left-2',
handleSize: 'h-16 w-[0.30rem]',
};
case 'top':
return {
position: 'top-0 left-0',
size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
border: 'border-b',
handlePosition: 'bottom-2 left-[40%]',
handleSize: 'w-16 h-[0.30rem]',
};
case 'bottom':
return {
position: 'bottom-0 left-0',
size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
border: 'border-t',
handlePosition: 'top-2 left-[40%]',
handleSize: 'w-16 h-[0.30rem]',
};
case 'left':
default:
return {
position: 'left-0 bottom-0',
size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
border: 'border-r',
handlePosition: 'top-[40%] right-2',
handleSize: 'h-16 w-[0.30rem]',
};
}
};
const directionClasses = getDirectionClasses();
const vaulDirection =
direction === 'right'
? 'right'
: direction === 'top'
? 'top'
: direction === 'bottom'
? 'bottom'
: 'left';
return (
<DrawerContext.Provider value={{ open, setOpen }}>
{trigger}
<VaulSidebar.Root
open={open}
direction={vaulDirection}
onOpenChange={setOpen}
dismissible={isDesktop ? false : true}
>
<VaulSidebar.Portal>
<VaulSidebar.Overlay
className='fixed inset-0 dark:bg-black/40 bg-white/50 backdrop-blur-xs z-50'
onClick={() => setOpen(false)}
/>
<VaulSidebar.Content
className={cn(
`${directionClasses.border} z-50 ${directionClasses.size} fixed ${directionClasses.position} ${
outsideClose ? 'dark:bg-zinc-950 bg-zinc-100' : ''
}`,
className
)}
>
<div
className={`${
outsideClose
? 'w-full h-full'
: `dark:bg-neutral-900 relative bg-white ${directionClasses.border} ${directionClasses.size}`
}`}
>
{isDesktop ? (
<button
className='flex justify-end w-full absolute right-2 top-2'
onClick={() => setOpen(false)}
>
<X />
</button>
) : (
<div
className={`absolute ${directionClasses.handlePosition} mx-auto ${directionClasses.handleSize} shrink-0 rounded-full bg-neutral-600 my-4`}
/>
)}
{content}
</div>
</VaulSidebar.Content>
</VaulSidebar.Portal>
</VaulSidebar.Root>
</DrawerContext.Provider>
);
}
export function DrawerContent({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn('', className)}>{children}</div>;
}
export function DrawerTrigger({ children }: { children: ReactNode }) {
const { setOpen } = useDirectionalDrawer();
return <div onClick={() => setOpen(true)}>{children}</div>;
}
'use client';
import {
DirectionalDrawer,
DrawerContent,
DrawerTrigger,
} from '@/components/ui/directional-drawer';
import { Edit } from 'lucide-react';
import { motion } from 'motion/react';
import Image from 'next/image';
export default function DirectionalDrawerDefault() {
return (
<>
<div className='flex justify-center'>
<figure className='h-96 w-96 relative'>
<Image
src={'/myself2.webp'}
width={600}
height={600}
className='h-full w-full object-cover rounded-lg'
alt='profile_image'
/>
<DirectionalDrawer direction={'right'} outsideClose={true}>
<DrawerTrigger>
<motion.button
whileTap={{ scale: 0.8 }}
className='absolute right-2 bottom-2 p-4 dark:bg-black bg-white rounded-lg shadow-black'
>
<Edit />
</motion.button>
</DrawerTrigger>
<DrawerContent>
<figure className='flex flex-col w-full h-full rounded-xl overflow-hidden'>
{/* Header / Body */}
<div className='p-6 flex flex-col gap-4 grow'>
<div>
<h1 className='font-semibold text-xl'>Update Profile Image</h1>
<p className='text-sm text-muted-foreground mt-1'>
Upload a new profile image or remove the current one.
</p>
</div>
{/* Avatar Preview */}
<div className='flex justify-center'>
<div className='h-32 w-32 rounded-xl dark:bg-neutral-800 bg-muted grid place-content-center text-xl font-medium'>
JP
</div>
</div>
{/* File Input */}
<div>
<label className='text-sm font-medium mb-1 block'>Profile Picture</label>
<input
type='file'
id='formFile'
className='w-full bg-background border border-border rounded-md p-2 file:bg-black file:text-white file:border-none file:px-3 file:py-1 cursor-pointer'
/>
</div>
{/* Submit */}
<button
type='submit'
className='w-full rounded-md bg-black text-white dark:bg-white dark:text-black p-2 font-medium'
>
Save Changes
</button>
</div>
</figure>
</DrawerContent>
</DirectionalDrawer>
</figure>
</div>
</>
);
}