modal-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ConfirmModal.tsx (DaisyUI)
3
+ *
4
+ * Uses the native <dialog> API via the Modal primitive in Modal.tsx.
5
+ */
6
+
7
+ import { useRef } from "react";
8
+ import type { BaseModalProps } from "modal-system";
9
+ import type { ConfirmModalData } from "./modals";
10
+ import { Modal, ModalContent } from "./Modal";
11
+
12
+ export function ConfirmModal({
13
+ isOpen,
14
+ onClose,
15
+ data,
16
+ }: BaseModalProps<ConfirmModalData>) {
17
+ const modalRef = useRef<HTMLDialogElement>(null);
18
+
19
+ const handleCancel = () => {
20
+ data?.onCancel?.();
21
+ onClose();
22
+ };
23
+
24
+ const handleConfirm = () => {
25
+ data?.onConfirm?.();
26
+ onClose();
27
+ };
28
+
29
+ return (
30
+ <Modal ref={modalRef} isOpen={isOpen} onClose={onClose}>
31
+ <ModalContent className="text-left">
32
+ {data?.title && (
33
+ <h3 className="font-bold text-lg">{data.title}</h3>
34
+ )}
35
+ {data?.message && (
36
+ <p className="mt-4 text-sm text-base-content/80">{data.message}</p>
37
+ )}
38
+
39
+ <div className="modal-action">
40
+ <form method="dialog">
41
+ <div className="flex gap-2">
42
+ <button
43
+ type="button"
44
+ className="btn btn-sm btn-soft"
45
+ onClick={handleCancel}
46
+ >
47
+ {data?.cancelText ?? "Cancel"}
48
+ </button>
49
+
50
+ {(data?.showConfirm ?? true) && (
51
+ <button
52
+ type="button"
53
+ className="btn btn-soft btn-sm bg-red-100 dark:bg-red-800 dark:text-white border-none text-red-500"
54
+ onClick={handleConfirm}
55
+ >
56
+ {data?.confirmText ?? "Confirm"}
57
+ </button>
58
+ )}
59
+ </div>
60
+ </form>
61
+ </div>
62
+ </ModalContent>
63
+ </Modal>
64
+ );
65
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Modal.tsx (DaisyUI)
3
+ *
4
+ * Base modal primitives for DaisyUI.
5
+ * Uses the native <dialog> API — no CSS `modal-open` class trick needed.
6
+ *
7
+ * Usage:
8
+ * import { Modal, ModalContent } from "./Modal"
9
+ *
10
+ * <Modal isOpen={isOpen} onClose={onClose}>
11
+ * <ModalContent>...</ModalContent>
12
+ * </Modal>
13
+ */
14
+
15
+ import {
16
+ forwardRef,
17
+ useEffect,
18
+ useRef,
19
+ type HTMLAttributes,
20
+ type ReactNode,
21
+ } from "react";
22
+
23
+ type ModalProps = {
24
+ children: ReactNode;
25
+ isOpen?: boolean;
26
+ onClose?: () => void;
27
+ };
28
+
29
+ /**
30
+ * Wraps the native <dialog> element.
31
+ * Calls showModal() / close() in response to the `isOpen` prop so that
32
+ * DaisyUI's backdrop and animations work correctly.
33
+ */
34
+ const Modal = forwardRef<HTMLDialogElement, ModalProps>(
35
+ ({ children, isOpen, onClose }, forwardedRef) => {
36
+ const internalRef = useRef<HTMLDialogElement>(null);
37
+ const ref = (forwardedRef as React.RefObject<HTMLDialogElement>) ?? internalRef;
38
+
39
+ useEffect(() => {
40
+ const dialog = ref.current;
41
+ if (!dialog) return;
42
+
43
+ if (isOpen) {
44
+ // showModal() enables the backdrop and traps focus
45
+ if (!dialog.open) dialog.showModal();
46
+ } else {
47
+ if (dialog.open) dialog.close();
48
+ }
49
+ }, [isOpen, ref]);
50
+
51
+ return (
52
+ <dialog ref={ref} className="modal" onClose={onClose}>
53
+ {children}
54
+ {/* Clicking the backdrop closes the dialog via the native form[method=dialog] */}
55
+ <form method="dialog" className="modal-backdrop">
56
+ <button type="submit" aria-label="Close modal" />
57
+ </form>
58
+ </dialog>
59
+ );
60
+ }
61
+ );
62
+
63
+ Modal.displayName = "Modal";
64
+
65
+ /** Applies DaisyUI modal-box styling with a white background */
66
+ const ModalContent = ({
67
+ className = "",
68
+ children,
69
+ ...props
70
+ }: HTMLAttributes<HTMLDivElement>) => (
71
+ <div className={`modal-box bg-base-100 ${className}`} {...props}>
72
+ {children}
73
+ </div>
74
+ );
75
+
76
+ export { Modal, ModalContent };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * modals/index.ts
3
+ *
4
+ * Register your modal components here.
5
+ * The key you use (e.g. "confirm") becomes the name you pass to openModal().
6
+ *
7
+ * Steps for adding a new modal:
8
+ * 1. Add its data type in modals.ts
9
+ * 2. Create the component (copy ConfirmModal.tsx as a starting point)
10
+ * 3. Add an entry to MODALS below
11
+ */
12
+
13
+ import { defineModals, createModalHook } from "modal-system";
14
+
15
+ import { ConfirmModal } from "./ConfirmModal";
16
+ // import { DeleteUserModal } from "./DeleteUserModal";
17
+
18
+ // ── Register all your modals here ────────────────────────────────────────────
19
+
20
+ export const MODALS = defineModals({
21
+ confirm: ConfirmModal,
22
+ // deleteUser: DeleteUserModal,
23
+ });
24
+
25
+ // ── Export the typed hook ─────────────────────────────────────────────────────
26
+ //
27
+ // Import `useModal` from THIS file in your components, not from "modal-system".
28
+ // That way openModal() knows about your specific modals and provides autocomplete.
29
+
30
+ export const useModal = createModalHook(MODALS);
31
+
32
+ // ── Re-export Modal primitives ────────────────────────────────────────────────
33
+ //
34
+ // Use Modal + ModalContent when building your own modals so they share
35
+ // the same <dialog> / DaisyUI base behaviour.
36
+
37
+ export { Modal, ModalContent } from "./Modal";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * modals.ts
3
+ *
4
+ * Define the data interface for each modal here.
5
+ * The key must match the name you register in index.ts.
6
+ *
7
+ * Your component receives these fields via the `data` prop:
8
+ * function ConfirmModal({ data }: BaseModalProps<ConfirmModalData>) {
9
+ * data?.title
10
+ * data?.message
11
+ * }
12
+ */
13
+
14
+ // ── Add your own modals below ────────────────────────────────────────────────
15
+
16
+ export interface ConfirmModalData {
17
+ title?: string;
18
+ /** Body text shown below the title */
19
+ message?: string;
20
+ /** Confirm button label. Defaults to "Confirm" */
21
+ confirmText?: string;
22
+ /** Cancel button label. Defaults to "Cancel" */
23
+ cancelText?: string;
24
+ /** Called when the user clicks the confirm button */
25
+ onConfirm?: () => void;
26
+ /** Called when the user clicks the cancel button */
27
+ onCancel?: () => void;
28
+ /** Set false to hide the confirm button entirely. Defaults to true */
29
+ showConfirm?: boolean;
30
+ }
31
+
32
+ // Example of another modal:
33
+ // export interface DeleteUserModalData {
34
+ // userId: string;
35
+ // userName: string;
36
+ // onDeleted?: () => void;
37
+ // }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ConfirmModal.tsx (shadcn/ui — backed by Base UI)
3
+ *
4
+ * Requires: @base-ui-components/react
5
+ * bun add @base-ui-components/react
6
+ */
7
+
8
+ import { Dialog } from "@base-ui-components/react/dialog";
9
+ import type { BaseModalProps } from "modal-system";
10
+ import type { ConfirmModalData } from "./modals";
11
+
12
+ export function ConfirmModal({
13
+ isOpen,
14
+ onClose,
15
+ data,
16
+ }: BaseModalProps<ConfirmModalData>) {
17
+ const handleClose = () => {
18
+ data?.onCancel?.();
19
+ onClose();
20
+ };
21
+
22
+ const handleConfirm = () => {
23
+ data?.onConfirm?.();
24
+ onClose();
25
+ };
26
+
27
+ return (
28
+ <Dialog.Root open={isOpen} onOpenChange={(open) => !open && handleClose()}>
29
+ <Dialog.Portal>
30
+ <Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 transition-opacity data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" />
31
+
32
+ <Dialog.Popup className="fixed left-1/2 top-1/2 z-50 w-full max-w-md min-h-40 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-background p-6 shadow-lg transition-all data-[ending-style]:opacity-0 data-[ending-style]:scale-95 data-[starting-style]:opacity-0 data-[starting-style]:scale-95">
33
+ <div className="flex flex-col gap-1">
34
+ <Dialog.Title className="text-lg">
35
+ {data?.title ?? "Are you sure?"}
36
+ </Dialog.Title>
37
+ {data?.description && (
38
+ <Dialog.Description className="text-xs text-muted-foreground">
39
+ {data.description}
40
+ </Dialog.Description>
41
+ )}
42
+ </div>
43
+
44
+ <div className="absolute bottom-3 right-3 flex gap-1">
45
+ <Dialog.Close
46
+ className="inline-flex h-9 items-center justify-center rounded-md bg-secondary px-4 text-sm font-medium text-secondary-foreground shadow-sm transition-colors hover:bg-secondary/80"
47
+ onClick={handleClose}
48
+ >
49
+ {data?.closeText ?? "Cancel"}
50
+ </Dialog.Close>
51
+
52
+ <button
53
+ type="button"
54
+ onClick={handleConfirm}
55
+ className={`inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium shadow transition-colors ${
56
+ data?.isActionButtonDestructive
57
+ ? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
58
+ : "bg-primary text-primary-foreground hover:bg-primary/90"
59
+ }`}
60
+ >
61
+ {data?.actionText ?? "Confirm"}
62
+ </button>
63
+ </div>
64
+ </Dialog.Popup>
65
+ </Dialog.Portal>
66
+ </Dialog.Root>
67
+ );
68
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * modals/index.ts
3
+ *
4
+ * Register your modal components here.
5
+ */
6
+
7
+ import { defineModals, createModalHook } from "modal-system";
8
+
9
+ import { ConfirmModal } from "./ConfirmModal";
10
+
11
+ export const MODALS = defineModals({
12
+ confirm: ConfirmModal,
13
+ });
14
+
15
+ export const useModal = createModalHook(MODALS);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * modals.ts
3
+ *
4
+ * Define the data interface for each modal here.
5
+ * The key must match the name you register in index.ts.
6
+ */
7
+
8
+ import type { ReactNode } from "react";
9
+
10
+ export interface ConfirmModalData {
11
+ title?: string;
12
+ description?: string | ReactNode;
13
+ /** Confirm button label. Defaults to "Confirm" */
14
+ actionText?: string;
15
+ /** Cancel button label. Defaults to "Cancel" */
16
+ closeText?: string;
17
+ onConfirm?: () => void;
18
+ onCancel?: () => void;
19
+ isActionButtonDestructive?: boolean;
20
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ConfirmModal.tsx (shadcn/ui — backed by Radix UI)
3
+ *
4
+ * Requires shadcn/ui Dialog and Button components in your project:
5
+ * npx shadcn@latest add dialog button
6
+ */
7
+
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ } from "@/components/ui/dialog";
14
+ import { Button } from "@/components/ui/button";
15
+ import type { BaseModalProps } from "modal-system";
16
+ import type { ConfirmModalData } from "./modals";
17
+
18
+ export function ConfirmModal({
19
+ isOpen,
20
+ onClose,
21
+ data,
22
+ }: BaseModalProps<ConfirmModalData>) {
23
+ const handleClose = () => {
24
+ data?.onCancel?.();
25
+ onClose();
26
+ };
27
+
28
+ const handleConfirm = () => {
29
+ data?.onConfirm?.();
30
+ onClose();
31
+ };
32
+
33
+ return (
34
+ <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
35
+ <DialogContent className="min-h-40">
36
+ <DialogHeader className="flex flex-col">
37
+ <span className="text-lg">
38
+ {data?.title ?? "Are you sure?"}
39
+ </span>
40
+ {data?.description && (
41
+ <span className="text-xs text-muted-foreground">
42
+ {data.description}
43
+ </span>
44
+ )}
45
+ </DialogHeader>
46
+
47
+ <DialogDescription asChild>
48
+ <div className="absolute bottom-3 right-3 flex gap-1">
49
+ <Button onClick={handleClose} variant="secondary">
50
+ {data?.closeText ?? "Cancel"}
51
+ </Button>
52
+ <Button
53
+ onClick={handleConfirm}
54
+ variant={data?.isActionButtonDestructive ? "destructive" : "default"}
55
+ >
56
+ {data?.actionText ?? "Confirm"}
57
+ </Button>
58
+ </div>
59
+ </DialogDescription>
60
+ </DialogContent>
61
+ </Dialog>
62
+ );
63
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * modals/index.ts
3
+ *
4
+ * Register your modal components here.
5
+ * The key you use (e.g. "confirm") becomes the name you pass to openModal().
6
+ */
7
+
8
+ import { defineModals, createModalHook } from "modal-system";
9
+
10
+ import { ConfirmModal } from "./ConfirmModal";
11
+ // import { DeleteUserModal } from "./DeleteUserModal";
12
+
13
+ export const MODALS = defineModals({
14
+ confirm: ConfirmModal,
15
+ // deleteUser: DeleteUserModal,
16
+ });
17
+
18
+ // Import `useModal` from THIS file in your components — it is typed to your registry.
19
+ export const useModal = createModalHook(MODALS);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * modals.ts
3
+ *
4
+ * Define the data interface for each modal here.
5
+ * The key must match the name you register in index.ts.
6
+ *
7
+ * Your component receives these fields via the `data` prop:
8
+ * function ConfirmModal({ data }: BaseModalProps<ConfirmModalData>) {
9
+ * data?.title
10
+ * }
11
+ */
12
+
13
+ import type { ReactNode } from "react";
14
+
15
+ // ── Add your own modals below ────────────────────────────────────────────────
16
+
17
+ export interface ConfirmModalData {
18
+ title?: string;
19
+ description?: string | ReactNode;
20
+ /** Confirm button label. Defaults to "Confirm" */
21
+ actionText?: string;
22
+ /** Cancel button label. Defaults to "Cancel" */
23
+ closeText?: string;
24
+ /** Called when the user clicks the confirm button */
25
+ onConfirm?: () => void;
26
+ /** Called when the user clicks the cancel / X button */
27
+ onCancel?: () => void;
28
+ /** Renders the confirm button with destructive (red) variant */
29
+ isActionButtonDestructive?: boolean;
30
+ }
31
+
32
+ // Example of another modal:
33
+ // export interface DeleteUserModalData {
34
+ // userId: string;
35
+ // userName: string;
36
+ // onDeleted?: () => void;
37
+ // }
@@ -0,0 +1,86 @@
1
+ import React, { ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Every modal component must accept these props.
6
+ * TData is the shape of the `data` object passed through openModal().
7
+ *
8
+ * @example
9
+ * function ConfirmModal({ isOpen, onClose, data }: BaseModalProps<ConfirmModalData>) {
10
+ * return <dialog>...{data?.title}...</dialog>
11
+ * }
12
+ */
13
+ interface BaseModalProps<TData = unknown> {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ data?: TData;
17
+ }
18
+ type ModalRegistry = Record<string, ComponentType<BaseModalProps<any>>>;
19
+ /**
20
+ * Given a modal component, extracts its TData generic.
21
+ *
22
+ * ExtractModalData<typeof ConfirmModal>
23
+ * → ConfirmModalData → { title?: string; description?: string; ... }
24
+ */
25
+ type ExtractModalData<C> = C extends ComponentType<BaseModalProps<infer D>> ? D : never;
26
+ /**
27
+ * Builds a full map of modalName → its data interface from a ModalRegistry.
28
+ *
29
+ * ModalDataMap<typeof MODALS>
30
+ * → { confirm: ConfirmModalData, upload: UploadModalData, ... }
31
+ */
32
+ type ModalDataMap<T extends ModalRegistry> = {
33
+ [K in keyof T]: ExtractModalData<T[K]>;
34
+ };
35
+
36
+ interface ModalProviderProps<T extends ModalRegistry> {
37
+ modals: T;
38
+ children: React.ReactNode;
39
+ }
40
+ /**
41
+ * Wrap your application root with this provider.
42
+ *
43
+ * @example
44
+ * import { ModalProvider } from "modal-system"
45
+ * import { MODALS } from "./modals"
46
+ *
47
+ * <ModalProvider modals={MODALS}>
48
+ * <App />
49
+ * </ModalProvider>
50
+ */
51
+ declare function ModalProvider<T extends ModalRegistry>({ modals, children, }: ModalProviderProps<T>): react_jsx_runtime.JSX.Element;
52
+
53
+ /**
54
+ * Factory that creates a fully type-safe `useModal` hook bound to your modals registry.
55
+ *
56
+ * @example
57
+ * // modals/index.ts
58
+ * export const MODALS = defineModals({ confirm: ConfirmModal })
59
+ * export const useModal = createModalHook(MODALS)
60
+ *
61
+ * // SomeComponent.tsx
62
+ * const { openModal, closeModal } = useModal()
63
+ * openModal("confirm", { title: "Delete?" }) // ← autocomplete on name + data shape
64
+ */
65
+ declare function createModalHook<T extends ModalRegistry>(_modals: T): () => {
66
+ /**
67
+ * Open a modal by name. The second argument must match the data
68
+ * interface of the registered component.
69
+ */
70
+ openModal: <K extends keyof T & string>(name: K, data?: ModalDataMap<T>[K]) => void;
71
+ closeModal: () => void;
72
+ };
73
+
74
+ /**
75
+ * Helper that returns its argument unchanged but enforces the ModalRegistry constraint.
76
+ * Use this when defining your MODALS object to get inference + type checking.
77
+ *
78
+ * @example
79
+ * export const MODALS = defineModals({
80
+ * confirm: ConfirmModal,
81
+ * upload: UploadModal,
82
+ * })
83
+ */
84
+ declare function defineModals<T extends ModalRegistry>(modals: T): T;
85
+
86
+ export { type BaseModalProps, type ModalDataMap, ModalProvider, type ModalRegistry, createModalHook, defineModals };
@@ -0,0 +1,86 @@
1
+ import React, { ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Every modal component must accept these props.
6
+ * TData is the shape of the `data` object passed through openModal().
7
+ *
8
+ * @example
9
+ * function ConfirmModal({ isOpen, onClose, data }: BaseModalProps<ConfirmModalData>) {
10
+ * return <dialog>...{data?.title}...</dialog>
11
+ * }
12
+ */
13
+ interface BaseModalProps<TData = unknown> {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ data?: TData;
17
+ }
18
+ type ModalRegistry = Record<string, ComponentType<BaseModalProps<any>>>;
19
+ /**
20
+ * Given a modal component, extracts its TData generic.
21
+ *
22
+ * ExtractModalData<typeof ConfirmModal>
23
+ * → ConfirmModalData → { title?: string; description?: string; ... }
24
+ */
25
+ type ExtractModalData<C> = C extends ComponentType<BaseModalProps<infer D>> ? D : never;
26
+ /**
27
+ * Builds a full map of modalName → its data interface from a ModalRegistry.
28
+ *
29
+ * ModalDataMap<typeof MODALS>
30
+ * → { confirm: ConfirmModalData, upload: UploadModalData, ... }
31
+ */
32
+ type ModalDataMap<T extends ModalRegistry> = {
33
+ [K in keyof T]: ExtractModalData<T[K]>;
34
+ };
35
+
36
+ interface ModalProviderProps<T extends ModalRegistry> {
37
+ modals: T;
38
+ children: React.ReactNode;
39
+ }
40
+ /**
41
+ * Wrap your application root with this provider.
42
+ *
43
+ * @example
44
+ * import { ModalProvider } from "modal-system"
45
+ * import { MODALS } from "./modals"
46
+ *
47
+ * <ModalProvider modals={MODALS}>
48
+ * <App />
49
+ * </ModalProvider>
50
+ */
51
+ declare function ModalProvider<T extends ModalRegistry>({ modals, children, }: ModalProviderProps<T>): react_jsx_runtime.JSX.Element;
52
+
53
+ /**
54
+ * Factory that creates a fully type-safe `useModal` hook bound to your modals registry.
55
+ *
56
+ * @example
57
+ * // modals/index.ts
58
+ * export const MODALS = defineModals({ confirm: ConfirmModal })
59
+ * export const useModal = createModalHook(MODALS)
60
+ *
61
+ * // SomeComponent.tsx
62
+ * const { openModal, closeModal } = useModal()
63
+ * openModal("confirm", { title: "Delete?" }) // ← autocomplete on name + data shape
64
+ */
65
+ declare function createModalHook<T extends ModalRegistry>(_modals: T): () => {
66
+ /**
67
+ * Open a modal by name. The second argument must match the data
68
+ * interface of the registered component.
69
+ */
70
+ openModal: <K extends keyof T & string>(name: K, data?: ModalDataMap<T>[K]) => void;
71
+ closeModal: () => void;
72
+ };
73
+
74
+ /**
75
+ * Helper that returns its argument unchanged but enforces the ModalRegistry constraint.
76
+ * Use this when defining your MODALS object to get inference + type checking.
77
+ *
78
+ * @example
79
+ * export const MODALS = defineModals({
80
+ * confirm: ConfirmModal,
81
+ * upload: UploadModal,
82
+ * })
83
+ */
84
+ declare function defineModals<T extends ModalRegistry>(modals: T): T;
85
+
86
+ export { type BaseModalProps, type ModalDataMap, ModalProvider, type ModalRegistry, createModalHook, defineModals };