{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "modal", "title": "Integrated Modal", "description": "Composable Modal and AlertModal built on shadcn base-ui Dialog / AlertDialog, plus imperative alert / confirm helpers powered by @easy-shadcn/command-modal.", "dependencies": [ "@easy-shadcn/command-modal" ], "registryDependencies": [ "dialog", "alert-dialog", "@easy-shadcn/async-button" ], "files": [ { "path": "registry/ui/modal/index.tsx", "content": "\"use client\";\n\nexport type { CommandModalHandler } from \"@easy-shadcn/command-modal\";\nexport type { AlertModalProps } from \"./alert-modal\";\nexport type { ModalProps } from \"./modal\";\n\nimport CommandModal from \"@easy-shadcn/command-modal\";\nimport { AlertModal as OriginalAlertModal } from \"./alert-modal\";\nimport AlertCommandModal from \"./alert-modal-helper\";\nimport { Modal as OriginalModal } from \"./modal\";\n\n// Only the public command-modal surface rides on the Modal namespace —\n// internals like the reducer and contexts stay in the package.\nconst commandModalApi = {\n create: CommandModal.create,\n hide: CommandModal.hide,\n Provider: CommandModal.Provider,\n register: CommandModal.register,\n remove: CommandModal.remove,\n show: CommandModal.show,\n unregister: CommandModal.unregister,\n useModal: CommandModal.useModal,\n useModalHolder: CommandModal.useModalHolder,\n};\n\nexport const AlertModal: typeof OriginalAlertModal & typeof AlertCommandModal =\n Object.assign(OriginalAlertModal, AlertCommandModal);\n\nexport const Modal: typeof OriginalModal & typeof commandModalApi =\n Object.assign(OriginalModal, commandModalApi);\n", "type": "registry:component", "target": "components/easy/modal/index.tsx" }, { "path": "registry/ui/modal/modal.tsx", "content": "\"use client\";\n\nimport type { ClassValue } from \"class-variance-authority/types\";\nimport type React from \"react\";\nimport {\n type ComponentProps,\n type ReactElement,\n type ReactNode,\n useState,\n} from \"react\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { AsyncButton } from \"../async-button\";\n\nexport interface ModalProps {\n /**\n * Base UI Dialog's post-transition hook — fires after the open/close\n * animation completes (with the resulting `open` state). Forwarded straight\n * to the underlying Dialog. command-modal's adapter wires teardown here.\n */\n onOpenChangeComplete?: (open: boolean) => void;\n /**\n * Extra props for the default footer's cancel AsyncButton. Passing\n * `onClick` replaces the built-in close handler — prefer `onCancel`.\n */\n cancelProps?: ComponentProps;\n cancelText?: ReactNode;\n children?: ReactNode;\n className?: ClassValue;\n /**\n * Extra props for the default footer's confirm AsyncButton. Passing\n * `onClick` replaces the built-in confirm-and-close handler — prefer\n * `onConfirm`.\n */\n confirmProps?: ComponentProps;\n confirmText?: ReactNode;\n contentClassName?: ClassValue;\n defaultOpen?: boolean;\n description?: ReactNode;\n descriptionClassName?: ClassValue;\n /** Prevents closing via outside clicks. Forwarded to the Base UI Dialog. */\n disablePointerDismissal?: boolean;\n /** Overrides the default confirm/cancel footer entirely. */\n footer?: ReactNode;\n footerClassName?: ClassValue;\n headerClassName?: ClassValue;\n /**\n * Called when the cancel button is pressed. May return a Promise — the\n * button shows a pending state and the modal stays open until it resolves;\n * a rejection keeps the modal open.\n */\n onCancel?: () => void | Promise;\n /**\n * Called when the confirm button is pressed. May return a Promise — the\n * button shows a pending state and the modal stays open until it resolves;\n * a rejection keeps the modal open.\n */\n onConfirm?: () => void | Promise;\n onOpenChange?: (open: boolean) => void;\n open?: boolean;\n showCloseButton?: boolean;\n title?: ReactNode;\n titleClassName?: ClassValue;\n trigger?: ReactElement;\n}\n\nexport const Modal: React.FC = ({\n open,\n defaultOpen,\n onOpenChange,\n title,\n titleClassName,\n description,\n descriptionClassName,\n children,\n contentClassName,\n className,\n disablePointerDismissal,\n footer,\n footerClassName,\n headerClassName,\n cancelProps,\n cancelText,\n onCancel,\n confirmProps,\n confirmText,\n onConfirm,\n trigger,\n showCloseButton = true,\n onOpenChangeComplete,\n}) => {\n const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n const currentOpen = open === undefined ? internalOpen : open;\n\n const handleOpenChange = (next: boolean) => {\n if (open === undefined) {\n setInternalOpen(next);\n }\n onOpenChange?.(next);\n };\n\n const close = () => handleOpenChange(false);\n\n // The flat confirm/cancel footer only appears when asked for; a rejected\n // async handler skips close() (AsyncButton catches it), keeping the modal\n // open on errors.\n const hasDefaultFooter =\n onConfirm !== undefined ||\n onCancel !== undefined ||\n confirmText !== undefined ||\n cancelText !== undefined ||\n confirmProps !== undefined ||\n cancelProps !== undefined;\n const footerNode =\n footer ??\n (hasDefaultFooter ? (\n <>\n {\n await onCancel?.();\n close();\n }}\n variant=\"outline\"\n {...cancelProps}\n >\n {cancelText ?? \"Cancel\"}\n \n {\n await onConfirm?.();\n close();\n }}\n {...confirmProps}\n >\n {confirmText ?? \"OK\"}\n \n \n ) : null);\n\n return (\n \n {trigger && }\n \n {(title || description) && (\n \n {title && (\n {title}\n )}\n {description && (\n }\n >\n {description}\n \n )}\n \n )}\n {children && (\n
{children}
\n )}\n {footerNode && (\n \n {footerNode}\n \n )}\n \n \n );\n};\n", "type": "registry:component", "target": "components/easy/modal/modal.tsx" }, { "path": "registry/ui/modal/alert-modal.tsx", "content": "\"use client\";\n\nimport type { ClassValue } from \"class-variance-authority/types\";\nimport type React from \"react\";\nimport {\n type ComponentProps,\n type ReactElement,\n type ReactNode,\n useState,\n} from \"react\";\nimport {\n AlertDialog,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { AsyncButton } from \"../async-button\";\n\nexport interface AlertModalProps {\n /**\n * Base UI AlertDialog's post-transition hook — fires after the open/close\n * animation completes (with the resulting `open` state). Forwarded straight\n * to the underlying AlertDialog. command-modal's adapter wires teardown here.\n */\n onOpenChangeComplete?: (open: boolean) => void;\n /**\n * Extra props for the cancel AsyncButton. Passing `onClick` replaces the\n * built-in close handler — prefer `onCancel`.\n */\n cancelProps?: ComponentProps;\n cancelText?: ReactNode;\n className?: ClassValue;\n /**\n * Extra props for the confirm AsyncButton. Passing `onClick` replaces the\n * built-in confirm-and-close handler — prefer `onConfirm`.\n */\n confirmProps?: ComponentProps;\n confirmText?: ReactNode;\n defaultOpen?: boolean;\n description?: ReactNode;\n descriptionClassName?: ClassValue;\n /** Overrides the default confirm/cancel footer entirely. */\n footer?: ReactNode;\n footerClassName?: ClassValue;\n headerClassName?: ClassValue;\n /**\n * Called when the cancel button is pressed. May return a Promise — the\n * button shows a pending state and the modal stays open until it resolves;\n * a rejection keeps the modal open.\n */\n onCancel?: () => void | Promise;\n /**\n * Called when the confirm button is pressed. May return a Promise — the\n * button shows a pending state and the modal stays open until it resolves;\n * a rejection keeps the modal open.\n */\n onConfirm?: () => void | Promise;\n onOpenChange?: (open: boolean) => void;\n open?: boolean;\n /** Hide the cancel button for single-action alerts. @default true */\n showCancel?: boolean;\n /** Forwarded to the AlertDialogContent primitive. */\n size?: \"default\" | \"sm\";\n title?: ReactNode;\n titleClassName?: ClassValue;\n trigger?: ReactElement;\n}\n\nexport const AlertModal: React.FC = ({\n open,\n defaultOpen,\n onOpenChange,\n title,\n titleClassName,\n description,\n descriptionClassName,\n className,\n headerClassName,\n size,\n trigger,\n footer,\n footerClassName,\n cancelText,\n onCancel,\n cancelProps,\n confirmText,\n onConfirm,\n confirmProps,\n showCancel = true,\n onOpenChangeComplete,\n}) => {\n const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n const currentOpen = open === undefined ? internalOpen : open;\n\n const handleOpenChange = (next: boolean) => {\n if (open === undefined) {\n setInternalOpen(next);\n }\n onOpenChange?.(next);\n };\n\n const close = () => handleOpenChange(false);\n\n return (\n \n {trigger && }\n \n {(title || description) && (\n \n {title && (\n \n {title}\n \n )}\n {description && (\n }\n >\n {description}\n \n )}\n \n )}\n \n {footer ?? (\n <>\n {showCancel && (\n {\n await onCancel?.();\n close();\n }}\n variant=\"outline\"\n {...cancelProps}\n >\n {cancelText ?? \"Cancel\"}\n \n )}\n {\n await onConfirm?.();\n close();\n }}\n {...confirmProps}\n >\n {confirmText ?? \"OK\"}\n \n \n )}\n \n \n \n );\n};\n", "type": "registry:component", "target": "components/easy/modal/alert-modal.tsx" }, { "path": "registry/ui/modal/alert-modal-helper.tsx", "content": "\"use client\";\n\nimport CommandModal from \"@easy-shadcn/command-modal\";\nimport { AlertModal, type AlertModalProps } from \"./alert-modal\";\n\ntype AlertHelperProps = Omit;\n\ntype CloseCompleteHook = ((open: boolean) => void) | undefined;\n\n/**\n * Compose the caller's and command-modal's `onOpenChangeComplete` hooks, then\n * unregister the one-shot modal once it has fully closed. Each helper call\n * creates a fresh component — without this, long-lived apps leak one registry\n * entry per call.\n */\nconst chainCloseAndUnregister =\n (id: string, callerHook: CloseCompleteHook, modalHook: CloseCompleteHook) =>\n (open: boolean) => {\n callerHook?.(open);\n modalHook?.(open);\n if (!open) {\n CommandModal.unregister(id);\n }\n };\n\n/**\n * Promise-style alert: a single OK button.\n * Resolves once the user confirms or dismisses the dialog.\n */\nconst alert = (\n props: Omit<\n AlertHelperProps,\n // A custom footer would disconnect the promise from the OK button.\n \"cancelProps\" | \"cancelText\" | \"footer\" | \"onCancel\" | \"showCancel\"\n >\n): Promise => {\n const AlertCommandModal = CommandModal.create(() => {\n const { id, modalProps, resolve } = CommandModal.useModal();\n return (\n {\n await props.onConfirm?.();\n resolve(true);\n }}\n onOpenChangeComplete={chainCloseAndUnregister(\n id,\n props.onOpenChangeComplete,\n modalProps.onOpenChangeComplete\n )}\n showCancel={false}\n />\n );\n });\n\n return CommandModal.show(AlertCommandModal).then(() => {\n // Dismissal and confirmation both settle the alert; there is nothing to\n // distinguish for a single-button dialog.\n return;\n });\n};\n\n/**\n * Promise-style confirm. Resolves `true` when confirmed, `false` when\n * cancelled or dismissed (Escape) — it never rejects.\n */\nconst confirm = (\n props: Omit\n): Promise => {\n const AlertCommandModal = CommandModal.create(() => {\n const { id, modalProps, resolve } = CommandModal.useModal();\n\n return (\n {\n await props.onCancel?.();\n resolve(false);\n }}\n onConfirm={async () => {\n await props.onConfirm?.();\n resolve(true);\n }}\n onOpenChangeComplete={chainCloseAndUnregister(\n id,\n props.onOpenChangeComplete,\n modalProps.onOpenChangeComplete\n )}\n />\n );\n });\n\n return CommandModal.show(AlertCommandModal).then((value) => value === true);\n};\n\nexport default {\n alert,\n confirm,\n};\n", "type": "registry:component", "target": "components/easy/modal/alert-modal-helper.tsx" } ], "type": "registry:component" }