| import React, { | ||
| useContext, | ||
| createContext, | ||
| type PropsWithChildren, | ||
| useState, | ||
| useRef, | ||
| useEffect, | ||
| useCallback, | ||
| } from "react"; | ||
| import { twMerge } from "tailwind-merge"; | ||
| import { clsx } from "clsx"; | ||
| // Interfaz para el contexto del modal | ||
| interface Context { | ||
| isOpenModal: boolean; // Indica si el modal está abierto | ||
| openModal: () => void; // Función para abrir el modal | ||
| closeModal: () => void; // Función para cerrar el modal | ||
| classNameContent: string; // Clases CSS para el contenido del modal | ||
| classNameBackdrop: string; // Clases CSS para el fondo/exterior del modal | ||
| closeOnOutsideClick: boolean; // Cierra el modal al hacer clic fuera | ||
| closeOnEscape: boolean; // Cierra el modal al presionar Escape | ||
| } | ||
| // Contexto global para el estado del modal | ||
| const ModalContext = createContext<Context | null>(null); | ||
| // Hook para acceder al contexto del modal | ||
| export const useModal = () => { | ||
| const context = useContext(ModalContext); | ||
| if (!context) { | ||
| throw new Error("useModal debe usarse dentro de un ModalProvider"); | ||
| } | ||
| return context; | ||
| }; | ||
| /* | ||
| ╔══════════════════════════════════════════════════════════════════════════╗ | ||
| ║ Componente proveedor del modal, maneja el estado abierto/cerrado ║ | ||
| ╚══════════════════════════════════════════════════════════════════════════╝ | ||
| */ | ||
| interface ModalProps { | ||
| classNameContent?: string; | ||
| classNameBackdrop?: string; | ||
| closeOnOutsideClick?: boolean; | ||
| closeOnEscape?: boolean; | ||
| children: React.ReactNode; | ||
| } | ||
| export const Modal = ({ | ||
| children, | ||
| classNameContent = "", | ||
| classNameBackdrop = "", | ||
| closeOnEscape = true, | ||
| closeOnOutsideClick = true, | ||
| }: Readonly<ModalProps>) => { | ||
| const [isOpenModal, setIsOpenModal] = useState<boolean>(false); | ||
| // Función para abrir el modal | ||
| const openModal = useCallback(() => setIsOpenModal(true), []); | ||
| // Función para cerrar el modal | ||
| const closeModal = useCallback(() => setIsOpenModal(false), []); | ||
| // Provee el contexto a los hijos | ||
| return ( | ||
| <ModalContext.Provider | ||
| value={{ | ||
| closeModal, | ||
| openModal, | ||
| isOpenModal, | ||
| classNameContent, | ||
| classNameBackdrop, | ||
| closeOnEscape, | ||
| closeOnOutsideClick, | ||
| }} | ||
| > | ||
| {children} | ||
| </ModalContext.Provider> | ||
| ); | ||
| }; | ||
| /* | ||
| ╔══════════════════════════════════════════════════════════════════════════╗ | ||
| ║ Componente que activa la apertura del modal al hacer click ║ | ||
| ╚══════════════════════════════════════════════════════════════════════════╝ | ||
| */ | ||
| interface ModalTriggerProps { | ||
| children: React.ReactElement<{ onClick: () => void }>; | ||
| } | ||
| export const ModalTrigger = ({ children }: Readonly<ModalTriggerProps>) => { | ||
| const { openModal } = useModal(); | ||
| // Clona el hijo y le agrega el onClick para abrir el modal | ||
| return React.cloneElement(children, { | ||
| onClick: openModal, | ||
| }); | ||
| }; | ||
| /* | ||
| ╔══════════════════════════════════════════════════════════════════════════╗ | ||
| ║ Componente que representa el contenido del modal ║ | ||
| ╚══════════════════════════════════════════════════════════════════════════╝ | ||
| */ | ||
| export const ModalContent = ({ children }: Readonly<PropsWithChildren>) => { | ||
| const { isOpenModal, closeModal, closeOnOutsideClick, closeOnEscape, classNameContent, classNameBackdrop } = | ||
| useModal(); | ||
| const modalRef = useRef<HTMLDivElement | null>(null); | ||
| // Maneja el cierre al hacer click fuera del modal | ||
| const handleClickOutside = useCallback( | ||
| (event: MouseEvent) => { | ||
| if (closeOnOutsideClick && modalRef.current && modalRef.current === event.target) { | ||
| closeModal(); | ||
| } | ||
| }, | ||
| [closeModal, closeOnOutsideClick] | ||
| ); | ||
| // Maneja el cierre al presionar Escape | ||
| const handleEscapeKey = useCallback( | ||
| (event: KeyboardEvent) => { | ||
| if (closeOnEscape && event.key === "Escape") { | ||
| closeModal(); | ||
| } | ||
| }, | ||
| [closeModal, closeOnEscape] | ||
| ); | ||
| // Efecto para listeners y bloqueo de scroll cuando el modal está abierto | ||
| useEffect(() => { | ||
| if (isOpenModal) { | ||
| // Listeners para cerrar el modal | ||
| closeOnOutsideClick && document.addEventListener("mousedown", handleClickOutside); | ||
| closeOnEscape && document.addEventListener("keydown", handleEscapeKey); | ||
| // Bloquea el scroll del body | ||
| const originalOverflow = document.body.style.overflow; | ||
| document.body.style.overflow = "hidden"; | ||
| return () => { | ||
| // Limpieza de listeners y restaurar scroll | ||
| closeOnOutsideClick && document.removeEventListener("mousedown", handleClickOutside); | ||
| closeOnEscape && document.removeEventListener("keydown", handleEscapeKey); | ||
| document.body.style.overflow = originalOverflow; | ||
| }; | ||
| } | ||
| }, [isOpenModal, handleClickOutside, handleEscapeKey, closeOnOutsideClick, closeOnEscape]); | ||
| // Si el modal no está abierto, no renderiza nada | ||
| if (!isOpenModal) return null; | ||
| return ( | ||
| <div | ||
| ref={modalRef} | ||
| className={twMerge( | ||
| clsx("fixed bg-black/50 inset-0 flex items-center justify-center z-50", classNameBackdrop) | ||
| )} | ||
| > | ||
| <div | ||
| className={twMerge( | ||
| clsx("bg-white overflow-hidden shadow-xl max-h-[90vh] max-w-[90vw]", classNameContent) | ||
| )} | ||
| > | ||
| {children} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
| export const dependenciesMap = { | ||
| button: ["clsx", "tailwind-merge", "tailwind-variants", "lucide-react"], | ||
| imagecropper: ["clsx", "lucide-react"], | ||
| modal: ["clsx", "tailwind-merge"], | ||
| }; |
+1
-1
| { | ||
| "name": "daemonui", | ||
| "version": "1.0.39", | ||
| "version": "1.0.40", | ||
| "main": "index.js", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+51
-6
@@ -60,2 +60,5 @@ <div align="center"> | ||
| npx daemonui add imagecropper | ||
| # Agregar modal | ||
| npx daemonui add modal | ||
| ``` | ||
@@ -67,6 +70,7 @@ | ||
| | Componente | Descripción | Casos de uso | | ||
| | -------------- | --------------------------------------- | ----------------------------------------------- | | ||
| | `button` | Botón versátil con múltiples variantes | Acciones primarias, secundarias, enlaces | | ||
| | `imagecropper` | Editor avanzado de imágenes con recorte | Avatares, thumbnails, procesamiento de imágenes | | ||
| | Componente | Descripción | Casos de uso | | ||
| | -------------- | ------------------------------------------- | ----------------------------------------------- | | ||
| | `button` | Botón versátil con múltiples variantes | Acciones primarias, secundarias, enlaces | | ||
| | `imagecropper` | Editor avanzado de imágenes con recorte | Avatares, thumbnails, procesamiento de imágenes | | ||
| | `modal` | Modal accesible con backdrop personalizable | Confirmaciones, formularios, contenido overlay | | ||
@@ -83,3 +87,5 @@ --- | ||
| │ └── component.tsx | ||
| └── imagecropper/ | ||
| ├── imagecropper/ | ||
| │ └── component.tsx | ||
| └── modal/ | ||
| └── component.tsx | ||
@@ -180,2 +186,40 @@ ``` | ||
| ### Modal Component | ||
| ```tsx | ||
| import { Modal, ModalContent, ModalTrigger, useModal } from "./components/ui/modal/component"; | ||
| export default function ModalDemo() { | ||
| return ( | ||
| <Modal | ||
| classNameContent="p-5 rounded-xl max-w-md" | ||
| classNameBackdrop="backdrop-blur-xs bg-black/25" | ||
| closeOnOutsideClick={true} | ||
| closeOnEscape={false} | ||
| > | ||
| <ModalTrigger> | ||
| <button className="bg-black text-white px-4 py-2 rounded-md">Abrir modal</button> | ||
| </ModalTrigger> | ||
| <ModalContent> | ||
| <Component /> | ||
| </ModalContent> | ||
| </Modal> | ||
| ); | ||
| } | ||
| // Componente de contenido personalizado | ||
| const Component = () => { | ||
| const { closeModal } = useModal(); | ||
| return ( | ||
| <div> | ||
| <h2 className="text-xl font-bold mb-4">¡Hola! Este es el contenido de tu modal</h2> | ||
| <p className="mb-4">Puedes colocar aquí cualquier contenido que desees.</p> | ||
| <button className="bg-black text-white px-4 py-2 rounded-md" onClick={closeModal}> | ||
| Cerrar Modal | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; | ||
| ``` | ||
| --- | ||
@@ -227,3 +271,2 @@ | ||
| - [ ] `input` - Campos de entrada con validación | ||
| - [ ] `modal` - Modales accesibles y responsivos | ||
| - [ ] `table` - Tablas con sorting y paginación | ||
@@ -233,2 +276,4 @@ - [ ] `form` - Formularios con validación integrada | ||
| - [ ] `dropdown` - Menús desplegables | ||
| - [ ] `tooltip` - Tooltips accesibles | ||
| - [ ] `tabs` - Sistema de pestañas | ||
@@ -235,0 +280,0 @@ ### Mejoras planeadas |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
43255
23.59%9
12.5%729
26.12%322
16.25%