react-modal-registry 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -1,12 +1,22 @@
1
1
  # react-modal-registry
2
2
 
3
- > Type-safe, promise-based modal management and registration for React.
3
+ > Type-safe, promise-based modal and dialog management for React.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/react-modal-registry?style=flat-square&color=black)](https://www.npmjs.com/package/react-modal-registry)
6
6
  [![license](https://img.shields.io/badge/license-MIT-black?style=flat-square)](./LICENSE)
7
7
  [![bundle size](https://img.shields.io/bundlephobia/minzip/react-modal-registry?style=flat-square&color=black)](https://bundlephobia.com/package/react-modal-registry)
8
8
  [![typescript](https://img.shields.io/badge/TypeScript-strict-black?style=flat-square)](https://www.typescriptlang.org/)
9
9
 
10
+ Managing dialogs in React often leads to **modal sprawl**—scattering `useState(false)` flags, prop drilling, and duplicate dialog components across pages.
11
+
12
+ `react-modal-registry` provides a single, type-safe registry to open and manage modals cleanly:
13
+
14
+ - **Promise-Based Dialogs**: Open modals imperatively (`await modal.open()`) and await user responses inline without local boolean states.
15
+ - **End-to-End Type Safety**: Autocomplete modal IDs, input payloads, and return values via TypeScript declaration merging.
16
+ - **URL Deep-Linking**: Synchronize modal visibility with the URL hash (`#modal=<id>`) or query params with native browser Back button dismissal.
17
+ - **Zero Re-Render Penalty**: Split-context architecture ensures caller components never re-render when modals open or close.
18
+ - **Headless & UI Agnostic**: Works out of the box with shadcn/ui, Radix UI, Tailwind CSS, or native HTML `<dialog>`.
19
+
10
20
  ---
11
21
 
12
22
  ## Installation
@@ -27,36 +37,34 @@ bun add react-modal-registry
27
37
 
28
38
  ### 1. Mount `ModalProvider`
29
39
 
30
- Wrap your application root or layout shell:
40
+ Wrap your application root:
31
41
 
32
42
  ```tsx
33
43
  import { ModalProvider } from 'react-modal-registry'
34
44
 
35
- export function RootLayout({ children }: { children: React.ReactNode }) {
45
+ export function App({ children }: { children: React.ReactNode }) {
36
46
  return <ModalProvider>{children}</ModalProvider>
37
47
  }
38
48
  ```
39
49
 
40
50
  ### 2. Declare Modal Types
41
51
 
42
- In an ambient declaration file (e.g., `env.d.ts` or `modals.d.ts`), augment `ModalDataMap` and `ModalResultMap`:
52
+ Declare modal IDs, payloads, and return types via TypeScript module augmentation (e.g., `env.d.ts` or `modals.d.ts`):
43
53
 
44
54
  ```ts
45
55
  declare module 'react-modal-registry' {
46
- interface ModalDataMap {
47
- confirmDelete: { userId: string; name: string }
48
- barcodeScanner: { onScan: (code: string) => void }
49
- }
50
-
51
- interface ModalResultMap {
52
- confirmDelete: boolean
56
+ interface ModalRegistry {
57
+ confirmDelete: {
58
+ data: { name: string }
59
+ result: boolean
60
+ }
53
61
  }
54
62
  }
55
63
  ```
56
64
 
57
65
  ### 3. Create a Modal Component
58
66
 
59
- Write your modal as a standard React component using `ModalProps<TData, TResult>`:
67
+ Use `ModalProps<TData, TResult>` for your component props:
60
68
 
61
69
  ```tsx
62
70
  import type { ModalProps } from 'react-modal-registry'
@@ -65,160 +73,176 @@ export function ConfirmDeleteModal({
65
73
  isOpen,
66
74
  data,
67
75
  close
68
- }: ModalProps<{ userId: string; name: string }, boolean>) {
76
+ }: ModalProps<{ name: string }, boolean>) {
69
77
  if (!isOpen) return null
70
78
 
71
79
  return (
72
80
  <dialog open={isOpen}>
73
- <h3>Delete {data.name}?</h3>
74
- <button onClick={() => close(true)}>Confirm</button>
81
+ <p>Are you sure you want to delete {data.name}?</p>
82
+ <button onClick={() => close(true)}>Delete</button>
75
83
  <button onClick={() => close(false)}>Cancel</button>
76
84
  </dialog>
77
85
  )
78
86
  }
79
87
  ```
80
88
 
81
- ### 4. Register Modals
89
+ ### 4. Register and Open
82
90
 
83
- Mount registrations at layout or router boundaries:
91
+ Register modals at app root or feature level, and trigger them anywhere with `useModal`:
84
92
 
85
93
  ```tsx
86
- import { ModalRegistry } from 'react-modal-registry'
94
+ import { ModalRegistry, useModal } from 'react-modal-registry'
87
95
  import { ConfirmDeleteModal } from './ConfirmDeleteModal'
88
96
 
89
- const appModals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
97
+ const modals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
90
98
 
91
- export function AppShell({ children }: { children: React.ReactNode }) {
99
+ export function Layout({ children }: { children: React.ReactNode }) {
92
100
  return (
93
101
  <>
94
- <ModalRegistry modals={appModals} />
102
+ <ModalRegistry modals={modals} />
95
103
  {children}
96
104
  </>
97
105
  )
98
106
  }
99
- ```
100
-
101
- ### 5. Trigger Anywhere with `useModal`
102
-
103
- ```tsx
104
- import { useModal } from 'react-modal-registry'
105
107
 
106
- export function DeleteButton({ user }: { user: { id: string; name: string } }) {
108
+ export function DeleteButton({ itemName }: { itemName: string }) {
107
109
  const confirmModal = useModal('confirmDelete')
108
110
 
109
111
  const handleDelete = async () => {
110
- // Typed payload and strictly typed return value (Promise<boolean>)
111
- const confirmed = await confirmModal.open({
112
- userId: user.id,
113
- name: user.name
114
- })
115
-
112
+ const confirmed = await confirmModal.open({ name: itemName })
116
113
  if (confirmed) {
117
- deleteUser(user.id)
114
+ // item deleted
118
115
  }
119
116
  }
120
117
 
121
- return <button onClick={handleDelete}>Delete User</button>
118
+ return <button onClick={handleDelete}>Delete</button>
122
119
  }
123
120
  ```
124
121
 
125
122
  ---
126
123
 
127
- ## Features
124
+ ## Usage & Features
128
125
 
129
- - **End-to-End Type Safety**: IDE autocompletion for modal IDs, payloads, and return types via TypeScript declaration merging.
130
- - **Promise-Based & Event-Driven**: `await modal.open(data)` for async results or pass callbacks (`onScan`, `onSuccess`) directly in payloads.
131
- - **Zero Re-Render Overhead**: Split-context architecture guarantees callers of `useModal()` never re-render when modals open, close, or animate.
132
- - **Code-Splitting Ready**: Register modals with `React.lazy()` at app shells without importing modal components into caller files.
133
- - **Lightweight & Headless**: Zero external runtime dependencies (~1.3 kB min+gzip). Works with any styling solution (Tailwind, Radix, Shadcn, MUI).
126
+ ### Promise-Based Responses
134
127
 
135
- ---
128
+ `modal.open(data)` returns a `Promise<TResult | undefined>`. Calling `close(result)` resolves the promise:
136
129
 
137
- ## Why react-modal-registry?
130
+ ```tsx
131
+ const confirmed = await modal.open({ name: 'Project Alpha' })
132
+ if (confirmed) {
133
+ await api.deleteProject()
134
+ }
135
+ ```
138
136
 
139
- | Feature | `react-modal-registry` | `@ebay/nice-modal-react` | `react-modal-promise` | `react-modal-hook` |
140
- | :--------------------------- | :-----------------------------------------------------------: | :---------------------------: | :---------------------------: | :------------------------: |
141
- | **Type Safety** | **Declaration merging** (autocompletes ID, payload, & return) | String ID / loose types | Direct component import | Direct component import |
142
- | **Caller Re-renders** | **None** (isolated split contexts) | Re-renders on context updates | Re-renders with wrapper state | Re-renders on state toggle |
143
- | **Promise Returns** | **Strictly typed** (`Promise<TResult>`) | Untyped (`Promise<unknown>`) | Manual generic | Not supported |
144
- | **Code Splitting** | **Native** (`React.lazy()` at registration shell) | Requires HOC per modal | Bundled at caller | Bundled at caller |
145
- | **Component Wrapper** | **None** (standard `ModalProps`) | Requires `NiceModal.create()` | Requires `create()` wrapper | Requires hook binding |
146
- | **UI Agnostic** | Yes (Tailwind, Radix, Shadcn, MUI) | Yes | Yes | Tied to `react-modal` |
147
- | **Bundle Size (min + gzip)** | **~1.3 kB** | ~2.4 kB | ~1.6 kB | ~1.3 kB |
137
+ If the modal is dismissed without a value (e.g. backdrop click or ESC), the promise resolves to `undefined`.
148
138
 
149
- ---
139
+ ### Rich Payloads & Callbacks
150
140
 
151
- ## Patterns
141
+ Pay-load data is passed in-memory, so you can pass callbacks, functions, or complex objects directly:
152
142
 
153
- ### Local Component Registrations
143
+ ```ts
144
+ declare module 'react-modal-registry' {
145
+ interface ModalRegistry {
146
+ scanner: {
147
+ data: { onScan: (code: string) => void }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ ```tsx
154
+ const scannerModal = useModal('scanner')
154
155
 
155
- Register modals scoped to the lifetime of a specific feature or page with `useRegisterModal`. Registrations unmount automatically when the caller unmounts:
156
+ scannerModal.open({
157
+ onScan: code => {
158
+ console.log('Scanned:', code)
159
+ }
160
+ })
161
+ ```
162
+
163
+ ### Component-Scoped Registrations
164
+
165
+ Register modals that exist only while a specific component is mounted:
156
166
 
157
167
  ```tsx
158
- import { useRegisterModal } from 'react-modal-registry'
159
- import { ReportModal } from './ReportModal'
168
+ import { useRegisterModal, useModal } from 'react-modal-registry'
169
+ import { FeatureModal } from './FeatureModal'
160
170
 
161
- export function AnalyticsPage() {
162
- useRegisterModal('customReport', ReportModal)
171
+ export function FeaturePage() {
172
+ useRegisterModal('featureDetails', FeatureModal)
173
+ const modal = useModal('featureDetails')
163
174
 
164
- return <main>Analytics Dashboard</main>
175
+ return <button onClick={() => modal.open()}>Open Details</button>
165
176
  }
166
177
  ```
167
178
 
168
- ### Callback-Driven Workflows
179
+ ### Code Splitting with `React.lazy`
169
180
 
170
- Pass event listeners or streams directly in modal payload data:
181
+ Split modal components into separate chunks:
171
182
 
172
183
  ```tsx
173
- function ScannerTrigger() {
174
- const scanner = useModal('barcodeScanner')
184
+ import { lazy } from 'react'
185
+ import { ModalRegistry } from 'react-modal-registry'
175
186
 
176
- const handleScan = () => {
177
- scanner.open({
178
- onScan: code => console.log('Scanned:', code)
179
- })
180
- }
187
+ const AnalyticsModal = lazy(() => import('./AnalyticsModal'))
188
+
189
+ const modals = [{ id: 'analytics', component: AnalyticsModal }] as const
181
190
 
182
- return <button onClick={handleScan}>Scan Code</button>
191
+ export function App() {
192
+ return <ModalRegistry modals={modals} />
183
193
  }
184
194
  ```
185
195
 
186
- ### Reactive State Subscriptions
196
+ ### URL Deep Linking
187
197
 
188
- `useModal(id).isOpen()` reads state imperatively without re-rendering the caller. Use `useIsModalOpen(id)` when you need reactive UI updates:
198
+ Optionally sync active modals with the browser URL (hash or query) and browser Back button:
189
199
 
190
200
  ```tsx
191
- import { useIsModalOpen } from 'react-modal-registry'
192
-
193
- function StatusIndicator() {
194
- const isOpen = useIsModalOpen('confirmDelete')
195
- return <span>{isOpen ? 'Active' : 'Idle'}</span>
196
- }
201
+ <ModalProvider
202
+ routing={{
203
+ strategy: 'hash', // 'hash' (#modal=changelog) or 'query' (?modal=changelog)
204
+ param: 'modal', // default: 'modal'
205
+ historyMode: 'push', // 'push' or 'replace'
206
+ defaultData: {
207
+ changelog: { version: '1.0.0' }
208
+ }
209
+ }}
210
+ >
211
+ {children}
212
+ </ModalProvider>
197
213
  ```
198
214
 
215
+ When routing is enabled:
216
+ - Opening a modal syncs `#modal=<id>` to the URL.
217
+ - Closing the modal cleans up the URL parameter.
218
+ - Pressing the browser **Back** button dismisses the modal.
219
+
199
220
  ---
200
221
 
201
222
  ## API Reference
202
223
 
203
- | Export | Type | Description |
204
- | :-------------------------------- | :-------- | :---------------------------------------------------------------------- |
205
- | `<ModalProvider>` | Component | Root provider managing modal lifecycle and rendering active slots. |
206
- | `<ModalRegistry>` | Component | Declarative component registering a batch of modals at root boundaries. |
207
- | `useModal(id)` | Hook | Returns typed imperative controls (`open`, `close`, `isOpen`). |
208
- | `useIsModalOpen(id)` | Hook | Reactively subscribes to the open status of a specific modal ID. |
209
- | `useRegisterModal(id, component)` | Hook | Dynamically registers modals for the lifetime of the calling component. |
210
- | `useCloseAll()` | Hook | Dismisses all currently open modals. |
211
- | `ModalProps<TData, TResult>` | Type | Standard props received by registered modal components. |
212
- | `ModalDataMap` | Interface | Augmentation interface for payload types. |
213
- | `ModalResultMap` | Interface | Augmentation interface for return types. |
214
- | `ModalRegistry` | Interface | Unified augmentation interface for paired data and result types. |
224
+ | Export | Type | Description |
225
+ | :--- | :--- | :--- |
226
+ | `<ModalProvider>` | Component | Root provider managing modal state and slot rendering. Accepts `unmountDelay`, `fallback`, and `routing`. |
227
+ | `<ModalRegistry>` | Component | Mounts a batch of static modal registrations: `<ModalRegistry modals={[...]} />`. |
228
+ | `useModal(id)` | Hook | Returns imperative controls: `{ open(data?), close(result?), isOpen() }`. |
229
+ | `useIsModalOpen(id)` | Hook | Reactively subscribes to whether a modal is currently open (`boolean`). |
230
+ | `useRegisterModal(id, component)` | Hook | Dynamically registers a modal for the calling component's lifecycle. |
231
+ | `useCloseAll()` | Hook | Dismisses all currently open modals. |
232
+ | `ModalProps<TData, TResult>` | Interface | Props received by modal components: `{ isOpen, data, close }`. |
233
+ | `ModalRegistry` | Interface | Target interface for module augmentation. |
215
234
 
216
235
  ---
217
236
 
218
- ## Architecture
237
+ ## Interactive Demo
238
+
239
+ A runnable demo showcasing forms, wizards, drawers, and deep linking is available in the [`demo`](./demo) directory:
219
240
 
220
- - **Isolated Contexts**: Action dispatchers (`open`, `close`, `register`) are kept separate from slot rendering states. Invoking modal methods does not trigger re-renders in calling views.
221
- - **Concurrent Requests**: Multiple concurrent invocations of `open()` with identical payloads reuse the active pending promise. Invoking `open()` with new parameters supersedes prior pending promises.
241
+ ```bash
242
+ cd demo
243
+ pnpm install
244
+ pnpm dev
245
+ ```
222
246
 
223
247
  ---
224
248
 
package/dist/index.cjs CHANGED
@@ -1,321 +1,2 @@
1
- "use strict";
2
- "use client";
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/index.tsx
22
- var index_exports = {};
23
- __export(index_exports, {
24
- ModalProvider: () => ModalProvider,
25
- ModalRegistry: () => ModalRegistry,
26
- useCloseAll: () => useCloseAll,
27
- useIsModalOpen: () => useIsModalOpen,
28
- useModal: () => useModal,
29
- useRegisterModal: () => useRegisterModal
30
- });
31
- module.exports = __toCommonJS(index_exports);
32
- var import_react = require("react");
33
- var import_jsx_runtime = require("react/jsx-runtime");
34
- var ModalActionsContext = (0, import_react.createContext)(null);
35
- var ModalSlotsContext = (0, import_react.createContext)(null);
36
- var UNMOUNT_DELAY_MS = 200;
37
- function ModalProvider({ children }) {
38
- const slotsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
39
- const pendingRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
40
- const timersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
41
- const [slots, setSlots] = (0, import_react.useState)(/* @__PURE__ */ new Map());
42
- (0, import_react.useEffect)(() => {
43
- const timers = timersRef.current;
44
- const pendings = pendingRef.current;
45
- return () => {
46
- for (const timer of timers.values()) {
47
- clearTimeout(timer);
48
- }
49
- timers.clear();
50
- for (const pending of pendings.values()) {
51
- pending.resolve(void 0);
52
- }
53
- pendings.clear();
54
- };
55
- }, []);
56
- const sync = (0, import_react.useCallback)(() => {
57
- setSlots(new Map(slotsRef.current));
58
- }, []);
59
- const register = (0, import_react.useCallback)(
60
- (idOrModals, component) => {
61
- const items = Array.isArray(idOrModals) ? idOrModals : component ? [
62
- {
63
- id: idOrModals,
64
- component
65
- }
66
- ] : [];
67
- let changed = false;
68
- for (const { id, component: comp } of items) {
69
- const existing = slotsRef.current.get(id);
70
- const typedComp = comp;
71
- if (existing) {
72
- if (existing.component !== typedComp) {
73
- slotsRef.current.set(id, {
74
- ...existing,
75
- component: typedComp
76
- });
77
- changed = true;
78
- }
79
- } else {
80
- slotsRef.current.set(id, {
81
- id,
82
- component: typedComp,
83
- isOpen: false,
84
- isMounted: false,
85
- data: void 0
86
- });
87
- changed = true;
88
- }
89
- }
90
- if (changed) {
91
- sync();
92
- }
93
- },
94
- [sync]
95
- );
96
- const unregister = (0, import_react.useCallback)(
97
- (idOrIds) => {
98
- const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
99
- let changed = false;
100
- for (const id of ids) {
101
- const timer = timersRef.current.get(id);
102
- if (timer) {
103
- clearTimeout(timer);
104
- timersRef.current.delete(id);
105
- }
106
- const pending = pendingRef.current.get(id);
107
- if (pending) {
108
- pendingRef.current.delete(id);
109
- pending.resolve(void 0);
110
- }
111
- if (slotsRef.current.has(id)) {
112
- slotsRef.current.delete(id);
113
- changed = true;
114
- }
115
- }
116
- if (changed) {
117
- sync();
118
- }
119
- },
120
- [sync]
121
- );
122
- const open = (0, import_react.useCallback)(
123
- (id, ...args) => {
124
- const data = args[0];
125
- const slot = slotsRef.current.get(id);
126
- if (!slot) {
127
- return Promise.reject(
128
- new Error(`[react-modal-registry] Modal "${String(id)}" is not registered.`)
129
- );
130
- }
131
- const existing = pendingRef.current.get(id);
132
- if (slot.isOpen && slot.data === data && existing) {
133
- return existing.promise;
134
- }
135
- if (existing) {
136
- pendingRef.current.delete(id);
137
- existing.resolve(void 0);
138
- }
139
- const timer = timersRef.current.get(id);
140
- if (timer) {
141
- clearTimeout(timer);
142
- timersRef.current.delete(id);
143
- }
144
- let resolver;
145
- const promise = new Promise((resolve) => {
146
- resolver = resolve;
147
- });
148
- pendingRef.current.set(id, {
149
- promise,
150
- resolve: resolver
151
- });
152
- slotsRef.current.set(id, { ...slot, isOpen: true, isMounted: true, data });
153
- sync();
154
- return promise;
155
- },
156
- [sync]
157
- );
158
- const scheduleUnmount = (0, import_react.useCallback)(
159
- (id) => {
160
- const timer = timersRef.current.get(id);
161
- if (timer) clearTimeout(timer);
162
- timersRef.current.set(
163
- id,
164
- setTimeout(() => {
165
- timersRef.current.delete(id);
166
- const current = slotsRef.current.get(id);
167
- if (current && !current.isOpen) {
168
- slotsRef.current.set(id, { ...current, isMounted: false });
169
- sync();
170
- }
171
- }, UNMOUNT_DELAY_MS)
172
- );
173
- },
174
- [sync]
175
- );
176
- const close = (0, import_react.useCallback)(
177
- (id, result) => {
178
- const slot = slotsRef.current.get(id);
179
- if (!slot || !slot.isOpen) return;
180
- const pending = pendingRef.current.get(id);
181
- if (pending) {
182
- pendingRef.current.delete(id);
183
- pending.resolve(result);
184
- }
185
- slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
186
- sync();
187
- scheduleUnmount(id);
188
- },
189
- [sync, scheduleUnmount]
190
- );
191
- const closeAll = (0, import_react.useCallback)(() => {
192
- for (const [, pending] of pendingRef.current) {
193
- pending.resolve(void 0);
194
- }
195
- pendingRef.current.clear();
196
- let changed = false;
197
- for (const [id, slot] of slotsRef.current) {
198
- if (slot.isOpen) {
199
- slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
200
- scheduleUnmount(id);
201
- changed = true;
202
- }
203
- }
204
- if (changed) {
205
- sync();
206
- }
207
- }, [sync, scheduleUnmount]);
208
- const isOpen = (0, import_react.useCallback)((id) => slotsRef.current.get(id)?.isOpen ?? false, []);
209
- const actions = (0, import_react.useMemo)(
210
- () => ({
211
- register,
212
- unregister,
213
- open,
214
- close,
215
- closeAll,
216
- isOpen
217
- }),
218
- [register, unregister, open, close, closeAll, isOpen]
219
- );
220
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalActionsContext.Provider, { value: actions, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalSlotsContext.Provider, { value: slots, children: [
221
- children,
222
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalRenderer, {})
223
- ] }) });
224
- }
225
- function useRegisterModal(idOrModals, component) {
226
- const { register, unregister } = useModalContext();
227
- (0, import_react.useEffect)(() => {
228
- if (Array.isArray(idOrModals)) {
229
- register(idOrModals);
230
- return () => {
231
- unregister(idOrModals.map((m) => m.id));
232
- };
233
- }
234
- if (component) {
235
- register(idOrModals, component);
236
- return () => {
237
- unregister(idOrModals);
238
- };
239
- }
240
- return void 0;
241
- }, [idOrModals, component, register, unregister]);
242
- }
243
- function ModalRegistry({
244
- modals
245
- }) {
246
- useRegisterModal(modals);
247
- return null;
248
- }
249
- function useModal(id) {
250
- const { open, close, isOpen } = useModalContext();
251
- return (0, import_react.useMemo)(
252
- () => ({
253
- open: (...args) => open(id, ...args),
254
- close: (result) => close(id, result),
255
- isOpen: () => isOpen(id)
256
- }),
257
- [open, close, isOpen, id]
258
- );
259
- }
260
- function useIsModalOpen(id) {
261
- const slots = (0, import_react.useContext)(ModalSlotsContext);
262
- if (!slots) {
263
- throw new Error("useIsModalOpen must be used within <ModalProvider>");
264
- }
265
- return slots.get(id)?.isOpen ?? false;
266
- }
267
- function useCloseAll() {
268
- return useModalContext().closeAll;
269
- }
270
- var ModalSlotItem = (0, import_react.memo)(function ModalSlotItem2({
271
- id,
272
- component: Component,
273
- isOpen,
274
- data,
275
- close
276
- }) {
277
- const handleClose = (0, import_react.useCallback)(
278
- (result) => {
279
- close(id, result);
280
- },
281
- [close, id]
282
- );
283
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { isOpen, data, close: handleClose }) });
284
- });
285
- function ModalRenderer() {
286
- const slots = (0, import_react.useContext)(ModalSlotsContext);
287
- const { close } = useModalContext();
288
- if (!slots) {
289
- return null;
290
- }
291
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: Array.from(slots.values()).map(
292
- (slot) => slot.isMounted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
293
- ModalSlotItem,
294
- {
295
- id: slot.id,
296
- component: slot.component,
297
- isOpen: slot.isOpen,
298
- data: slot.data,
299
- close
300
- },
301
- slot.id
302
- ) : null
303
- ) });
304
- }
305
- function useModalContext() {
306
- const context = (0, import_react.useContext)(ModalActionsContext);
307
- if (!context) {
308
- throw new Error("useModalContext must be used within <ModalProvider>");
309
- }
310
- return context;
311
- }
312
- // Annotate the CommonJS export names for ESM import in node:
313
- 0 && (module.exports = {
314
- ModalProvider,
315
- ModalRegistry,
316
- useCloseAll,
317
- useIsModalOpen,
318
- useModal,
319
- useRegisterModal
320
- });
1
+ "use strict";"use client";var E=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var Z=(t,e)=>{for(var s in e)E(t,s,{get:e[s],enumerable:!0})},ee=(t,e,s,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of W(e))!X.call(t,l)&&l!==s&&E(t,l,{get:()=>e[l],enumerable:!(a=Q(e,l))||a.enumerable});return t};var te=t=>ee(E({},"__esModule",{value:!0}),t);var Me={};Z(Me,{ModalProvider:()=>de,ModalRegistry:()=>ie,isBrowser:()=>O,useCloseAll:()=>pe,useIsModalOpen:()=>ce,useModal:()=>ue,useRegisterModal:()=>G});module.exports=te(Me);var r=require("react"),m=require("react/jsx-runtime"),k={strategy:"hash",param:"modal",historyMode:"push"},O=()=>typeof window<"u",ne=200,_=(0,r.createContext)(null),B=(0,r.createContext)(new Map);function oe(t,e){if(Object.is(t,e))return!0;if(typeof t!="object"||t===null||typeof e!="object"||e===null)return!1;if(t instanceof Date&&e instanceof Date)return t.getTime()===e.getTime();if(t instanceof RegExp&&e instanceof RegExp)return t.source===e.source&&t.flags===e.flags;if(t instanceof Map||t instanceof Set||e instanceof Map||e instanceof Set)return!1;let s=Object.keys(t),a=Object.keys(e);return s.length!==a.length?!1:s.every(l=>Object.prototype.hasOwnProperty.call(e,l)&&t[l]===e[l])}function re(t){let e=(0,r.useRef)(t);return Array.isArray(t)&&Array.isArray(e.current)&&e.current.length===t.length&&e.current.every((s,a)=>s.id===t[a].id&&s.component===t[a].component)?e.current:(e.current=t,t)}function z(t){let e=t.replace(/^#/,""),[s,a]=e.includes("?")?e.split("?",2):e.includes("=")?["",e]:[e,""];return{anchor:s,params:new URLSearchParams(a)}}function ae(t,e){let s=e.toString();return s?`#${t?`${t}?`:""}${s}`:t?`#${t}`:""}function D(t){if(!O()||!t||t.enabled===!1)return null;let e=t.param??k.param;return(t.strategy??k.strategy)==="query"?new URLSearchParams(window.location.search).get(e):z(window.location.hash).params.get(e)}function U(t,e,s){if(!O()||!e||e.enabled===!1||D(e)===t)return;let a=s??e.historyMode??k.historyMode,l=e.param??k.param,M=new URL(window.location.href),x=g=>t?g.set(l,t):g.delete(l);if((e.strategy??k.strategy)==="query")x(M.searchParams);else{let{anchor:g,params:f}=z(window.location.hash);x(f),M.hash=ae(g,f)}window.history[a==="push"?"pushState":"replaceState"](window.history.state,"",M.pathname+M.search+M.hash)}function F(t,e){if(t)return typeof t=="function"?t(e):Object.prototype.hasOwnProperty.call(t,e)?t[e]:void 0}function de({children:t,unmountDelay:e=ne,fallback:s=null,routing:a}){let l=(0,r.useRef)(new Map),[M,x]=(0,r.useState)(new Map),g=(0,r.useRef)(M),f=(0,r.useCallback)(n=>{let o=n(g.current);return g.current=o,x(o),o},[]),R=(0,r.useRef)(new Map),y=(0,r.useRef)(new Map),Y=(0,r.useRef)(0),C=(0,r.useMemo)(()=>a?{...k,...a,enabled:a.enabled!==!1}:null,[a?.enabled,a?.strategy,a?.param,a?.historyMode,a?.defaultData]),p=(0,r.useRef)(C);p.current=C,(0,r.useEffect)(()=>()=>{y.current.forEach(clearTimeout),y.current.clear(),R.current.forEach(n=>n.resolve(void 0)),R.current.clear()},[]);let b=(0,r.useCallback)(n=>{let o=l.current.get(n);return o?.[o.length-1]},[]),P=(0,r.useCallback)((n,o)=>{let c=y.current.get(n);c&&clearTimeout(c);let d;d=setTimeout(()=>{y.current.get(n)===d&&y.current.delete(n),f(i=>{let u=i.get(n);if(!u||u.isOpen||u.invocationId!==o)return i;let T=new Map(i);return T.delete(n),T})},e),y.current.set(n,d)},[e,f]),A=(0,r.useCallback)((n,o,c,d=!0)=>{let i=g.current.get(n);if(!i||!i.isOpen||o!==void 0&&i.invocationId!==o)return;let u=R.current.get(n);u&&(o===void 0||u.invocationId===o)&&(R.current.delete(n),u.resolve(c)),d&&p.current?.enabled&&D(p.current)===String(n)&&U(null,p.current,"replace");let T=!1;f(I=>{let v=I.get(n);if(!v||!v.isOpen||o!==void 0&&v.invocationId!==o)return I;T=!0;let h=new Map(I);return e<=0?h.delete(n):h.set(n,{...v,isOpen:!1}),h}),T&&e>0&&P(n,i.invocationId)},[P,e,f]),w=(0,r.useCallback)((n,o,c=!0)=>{let d=b(n);if(!d)return Promise.reject(new Error(`[react-modal-registry] Modal "${String(n)}" is not registered.`));let i=g.current.get(n),u=R.current.get(n);if(i?.isOpen&&i.component===d&&oe(i.data,o)&&u)return u.promise;c&&p.current?.enabled&&U(String(n),p.current),u&&(R.current.delete(n),u.resolve(void 0));let T=y.current.get(n);T&&(clearTimeout(T),y.current.delete(n));let I=++Y.current,v,h=new Promise(S=>{v=S});return R.current.set(n,{promise:h,resolve:v,invocationId:I}),f(S=>{let K=new Map(S);return K.set(n,{id:n,component:d,isOpen:!0,data:o,invocationId:I}),K}),h},[b,f]),N=(0,r.useCallback)((n,...o)=>w(n,o[0],!0),[w]),j=(0,r.useCallback)((n,o)=>{A(n,void 0,o,!0)},[A]),$=(0,r.useCallback)(()=>{let n=Array.from(R.current.values());if(R.current.clear(),p.current?.enabled&&U(null,p.current,"replace"),e<=0){for(let o of y.current.values())clearTimeout(o);y.current.clear(),f(()=>new Map)}else{for(let[o,c]of g.current)c.isOpen&&P(o,c.invocationId);f(o=>{let c=!1,d=new Map(o);for(let[i,u]of o)u.isOpen&&(d.set(i,{...u,isOpen:!1}),c=!0);return c?d:o})}n.forEach(o=>o.resolve(void 0))},[P,e,f]),q=(0,r.useCallback)(n=>g.current.get(n)?.isOpen??!1,[]),V=(0,r.useCallback)(((n,o)=>{let c=Array.isArray(n)?n:o?[{id:n,component:o}]:[],d=p.current?.enabled?D(p.current):null;for(let i of c){let u=l.current.get(i.id);if(u||(u=[],l.current.set(i.id,u)),u.push(i.component),d&&d===String(i.id)&&!g.current.get(i.id)?.isOpen){let T=F(p.current?.defaultData,i.id);w(i.id,T,!1)}}}),[w]),H=(0,r.useCallback)((n,o)=>{let c=Array.isArray(n)?n:[n];for(let d of c){let i=l.current.get(d);if(!i)continue;if(o){let I=i.lastIndexOf(o);if(I!==-1&&i.splice(I,1),i.length>0)continue}l.current.delete(d);let u=y.current.get(d);u&&(clearTimeout(u),y.current.delete(d));let T=R.current.get(d);T&&(R.current.delete(d),T.resolve(void 0)),p.current?.enabled&&D(p.current)===String(d)&&U(null,p.current,"replace"),f(I=>{if(!I.has(d))return I;let v=new Map(I);return v.delete(d),v})}},[f]);(0,r.useEffect)(()=>{if(!C||!C.enabled||!O())return;let n=()=>{let o=D(p.current),c=g.current;for(let[d,i]of c)String(d)!==o&&i.isOpen&&A(d,void 0,void 0,!1);if(o){let d=o;!c.get(d)?.isOpen&&b(d)&&w(d,F(p.current?.defaultData,d),!1)}};return n(),window.addEventListener("popstate",n),window.addEventListener("hashchange",n),()=>{window.removeEventListener("popstate",n),window.removeEventListener("hashchange",n)}},[C,b,w,A]);let J=(0,r.useMemo)(()=>({register:V,unregister:H,open:N,close:j,closeAll:$,isOpen:q}),[V,H,N,j,$,q]);return(0,m.jsx)(_.Provider,{value:J,children:(0,m.jsxs)(B.Provider,{value:M,children:[t,(0,m.jsx)(se,{activeModals:M,close:A,fallback:s})]})})}function se({activeModals:t,close:e,fallback:s}){return t.size===0?null:(0,m.jsx)(m.Fragment,{children:Array.from(t.values(),a=>(0,m.jsx)(le,{active:a,onClose:e,fallback:s},`${String(a.id)}:${a.invocationId}`))})}var le=(0,r.memo)(function({active:e,onClose:s,fallback:a}){let l=(0,r.useCallback)(x=>s(e.id,e.invocationId,x,!0),[s,e.id,e.invocationId]),M=e.component;return(0,m.jsx)(r.Suspense,{fallback:a??null,children:(0,m.jsx)(M,{isOpen:e.isOpen,data:e.data,close:l})})});function G(t,e){let{register:s,unregister:a}=L(),l=re(t);(0,r.useEffect)(()=>{if(Array.isArray(l))return s(l),()=>{for(let M of l)a(M.id,M.component)};if(e)return s(l,e),()=>{a(l,e)}},[l,e,s,a])}function ie({modals:t}){return G(t),null}function ue(t){let{open:e,close:s,isOpen:a}=L();return(0,r.useMemo)(()=>({open:(...l)=>e(t,...l),close:l=>s(t,l),isOpen:()=>a(t)}),[e,s,a,t])}function ce(t){return(0,r.useContext)(B).get(t)?.isOpen??!1}function pe(){return L().closeAll}function L(){let t=(0,r.useContext)(_);if(!t)throw new Error("useModalContext must be used within <ModalProvider>");return t}0&&(module.exports={ModalProvider,ModalRegistry,isBrowser,useCloseAll,useIsModalOpen,useModal,useRegisterModal});
321
2
  //# sourceMappingURL=index.cjs.map