react-flow-modal 0.3.0 → 0.4.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
@@ -17,84 +17,247 @@ npm install react-flow-modal
17
17
  yarn add react-flow-modal
18
18
  ```
19
19
 
20
+ ---
21
+
20
22
  ## Basic Usage
21
23
 
22
- ```JSX
23
- import { ModalProvider, ModalHost, useModal } from "react-flow-modal";
24
+ ```tsx
25
+ import { ModalProvider, useModal, renderModals } from "react-flow-modal";
26
+
27
+ function ConfirmModal({
28
+ onConfirm,
29
+ onCancel,
30
+ }: {
31
+ onConfirm: () => void;
32
+ onCancel: () => void;
33
+ }) {
34
+ return (
35
+ <div
36
+ style={{
37
+ position: "fixed",
38
+ inset: 0,
39
+ background: "rgba(0,0,0,0.5)",
40
+ display: "grid",
41
+ placeItems: "center",
42
+ zIndex: 1000,
43
+ }}
44
+ >
45
+ <div
46
+ style={{
47
+ background: "white",
48
+ padding: 24,
49
+ borderRadius: 8,
50
+ minWidth: 300,
51
+ }}
52
+ >
53
+ <h3>Are you sure?</h3>
54
+ <p>This action cannot be undone.</p>
55
+
56
+ <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
57
+ <button onClick={onCancel}>Cancel</button>
58
+ <button onClick={onConfirm}>Confirm</button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ function App() {
66
+ const modal = useModal();
67
+
68
+ const onClick = async () => {
69
+ const result = await modal.open("confirm", (resolve) => (
70
+ <ConfirmModal
71
+ onConfirm={() => resolve(true)}
72
+ onCancel={() => resolve(false)}
73
+ />
74
+ ));
75
+
76
+ console.log("Result:", result);
77
+ };
78
+
79
+ return <button onClick={onClick}>Open Confirm Modal</button>;
80
+ }
81
+
82
+ function ModalRenderer() {
83
+ return renderModals();
84
+ }
85
+
86
+ export default function Root() {
87
+ return (
88
+ <ModalProvider>
89
+ <App />
90
+ <ModalRenderer />
91
+ </ModalProvider>
92
+ );
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## With AnimatePresence (Framer Motion)
99
+
100
+ To support exit animations, modals must be rendered inside the same
101
+ React tree as `AnimatePresence`.
102
+
103
+ ```tsx
104
+ import { ModalProvider, useModal, renderModals } from "react-flow-modal";
105
+ import { motion, AnimatePresence } from "motion/react";
106
+
107
+ function ConfirmModal({
108
+ onConfirm,
109
+ onCancel,
110
+ }: {
111
+ onConfirm: () => void;
112
+ onCancel: () => void;
113
+ }) {
114
+ return (
115
+ <motion.div
116
+ key="modal"
117
+ initial={{ opacity: 0 }}
118
+ animate={{ opacity: 1 }}
119
+ exit={{ opacity: 0 }}
120
+ style={{
121
+ position: "fixed",
122
+ inset: 0,
123
+ background: "rgba(0,0,0,0.5)",
124
+ display: "grid",
125
+ placeItems: "center",
126
+ zIndex: 1000,
127
+ }}
128
+ >
129
+ <motion.div
130
+ key="modal-content"
131
+ initial={{ scale: 0.9, opacity: 0 }}
132
+ animate={{ scale: 1, opacity: 1 }}
133
+ exit={{ scale: 0.95, opacity: 0 }}
134
+ transition={{ duration: 0.2 }}
135
+ style={{
136
+ background: "white",
137
+ padding: 24,
138
+ borderRadius: 8,
139
+ minWidth: 300,
140
+ }}
141
+ >
142
+ <h3>Are you sure?</h3>
143
+ <p>This action cannot be undone.</p>
144
+
145
+ <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
146
+ <button onClick={onCancel}>Cancel</button>
147
+ <button onClick={onConfirm}>Confirm</button>
148
+ </div>
149
+ </motion.div>
150
+ </motion.div>
151
+ );
152
+ }
24
153
 
25
154
  function App() {
26
- const modal = useModal();
155
+ const modal = useModal();
27
156
 
28
- const onClick = async () => {
29
- const result = await modal.open("confirm",
30
- (resolve, reject) => (
31
- <ConfirmModal
32
- onConfirm={() => resolve(true)}
33
- onCancel={() => resolve(false)}
34
- />
35
- ));
157
+ const onClick = async () => {
158
+ const result = await modal.open("confirm", (resolve) => (
159
+ <ConfirmModal
160
+ onConfirm={() => resolve(true)}
161
+ onCancel={() => resolve(false)}
162
+ />
163
+ ));
36
164
 
37
- // Resolving or rejecting the promise will also remove the modal from the stack
165
+ console.log("Result:", result);
166
+ };
38
167
 
39
- console.log(result);
40
- };
168
+ return <button onClick={onClick}>Open Confirm Modal</button>;
169
+ }
41
170
 
42
- return <button onClick={onClick}>Open modal</button>;
171
+ function ModalRenderer() {
172
+ return (
173
+ <AnimatePresence>
174
+ {renderModals()}
175
+ </AnimatePresence>
176
+ );
43
177
  }
44
178
 
45
179
  export default function Root() {
46
180
  return (
47
181
  <ModalProvider>
48
182
  <App />
49
- <ModalHost />
183
+ <ModalRenderer />
50
184
  </ModalProvider>
51
185
  );
52
186
  }
53
187
  ```
54
188
 
189
+ ---
190
+
55
191
  ## API
56
192
 
57
- ### open
58
- ```TS
59
- open<T>(
60
- key: string,
61
- render: (
62
- resolve: (value: T) => void,
63
- reject: (reason?: unknown) => void
64
- ) => React.ReactNode
65
- ): Promise<T>
193
+ ### useModal
194
+
195
+ ```ts
196
+ const modal = useModal();
197
+ ```
198
+
199
+ Returns an object that controls the modal flow.
200
+
201
+ ```ts
202
+ {
203
+ open<T>(
204
+ key: string,
205
+ render: (
206
+ resolve: (value: T) => void,
207
+ reject: (reason?: unknown) => void
208
+ ) => React.ReactNode
209
+ ): Promise<T>;
210
+ }
211
+ ```
212
+
213
+ ### renderModals
214
+ ```ts
215
+ renderModals(): React.ReactNode
66
216
  ```
217
+ Renders the entire modal stack. This function should be rendered **once** in your React tree.
218
+
219
+ ---
67
220
 
68
221
  ## Important
69
222
 
70
- > ⚠️ Make sure to always resolve or reject the promise.
223
+ > ⚠️ Always resolve or reject the promise.
71
224
  > Leaving it pending will block the async flow.
72
225
 
226
+ > ⚠️ `renderModals()` should be rendered once.
227
+ > Rendering it multiple times may result in duplicated modals.
228
+
229
+ ---
230
+
73
231
  ## Why react-flow-modal?
74
232
 
75
233
  Most modal libraries are state-driven:
76
- ```JSX
234
+
235
+ ```tsx
77
236
  setOpen(true);
78
237
  ```
79
238
 
80
239
  This makes modal control implicit and tightly coupled to rendering.
81
240
 
82
- react-flow-modal treats modals as explicit async control points:
241
+ `react-flow-modal` treats modals as explicit async control points:
83
242
 
84
- ```JSX
243
+ ```tsx
85
244
  const result = await open(...);
86
245
  ```
87
246
 
88
247
  This keeps control flow readable, composable, and testable.
89
248
 
249
+ ---
250
+
90
251
  ## Features
91
252
 
92
- - Headless API (no styles, no UI constraints)
93
- - Promise-based modal control
94
- - Stack-based modal rendering
95
- - Fully controlled by user events
96
- - Works naturally with async / await
253
+ * Headless API (no styles, no UI constraints)
254
+ * Promise-based modal control
255
+ * Internal stack management
256
+ * Render location fully controlled by the user
257
+ * Works naturally with async / await
258
+
259
+ ---
97
260
 
98
261
  ## License
99
262
 
100
- MIT
263
+ MIT
package/dist/index.cjs CHANGED
@@ -22,7 +22,8 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ModalHost: () => ModalHost,
24
24
  ModalProvider: () => ModalProvider,
25
- useModal: () => useModal
25
+ useModal: () => useModal,
26
+ useModalHost: () => useModalHost
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
 
@@ -43,9 +44,6 @@ var ModalProvider = ({ children }) => {
43
44
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalContext.Provider, { value: { stack, setStack }, children });
44
45
  };
45
46
 
46
- // src/ModalHost.tsx
47
- var import_react4 = require("react");
48
-
49
47
  // src/useModalContext.ts
50
48
  var import_react3 = require("react");
51
49
  var useModalContext = () => {
@@ -55,17 +53,14 @@ var useModalContext = () => {
55
53
  }
56
54
  return context;
57
55
  };
58
-
59
- // src/ModalHost.tsx
60
- var import_jsx_runtime2 = require("react/jsx-runtime");
61
- var ModalHost = () => {
56
+ var useModalHost = () => {
62
57
  const { stack } = useModalContext();
63
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Fragment, { children: stack.map((item) => item) });
58
+ return {
59
+ render: () => stack.map((item) => item)
60
+ };
64
61
  };
65
62
 
66
63
  // src/useModal.tsx
67
- var import_react5 = require("react");
68
- var import_jsx_runtime3 = require("react/jsx-runtime");
69
64
  var useModal = () => {
70
65
  const { setStack } = useModalContext();
71
66
  const pop = () => {
@@ -83,14 +78,23 @@ var useModal = () => {
83
78
  reject(reason);
84
79
  pop();
85
80
  });
86
- push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: element }, key));
81
+ push(element);
87
82
  });
88
83
  };
89
84
  return { open };
90
85
  };
86
+
87
+ // src/ModalHost.tsx
88
+ var import_react4 = require("react");
89
+ var import_jsx_runtime2 = require("react/jsx-runtime");
90
+ var ModalHost = () => {
91
+ const { render } = useModalHost();
92
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Fragment, { children: render() });
93
+ };
91
94
  // Annotate the CommonJS export names for ESM import in node:
92
95
  0 && (module.exports = {
93
96
  ModalHost,
94
97
  ModalProvider,
95
- useModal
98
+ useModal,
99
+ useModalHost
96
100
  });
package/dist/index.d.cts CHANGED
@@ -1,11 +1,17 @@
1
- import React, { FC, PropsWithChildren } from 'react';
1
+ import * as react from 'react';
2
+ import react__default, { FC, PropsWithChildren } from 'react';
2
3
 
3
4
  declare const ModalProvider: FC<PropsWithChildren>;
4
5
 
6
+ declare const useModal: () => {
7
+ open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
8
+ };
9
+
10
+ /** @deprecated use renderModals instead */
5
11
  declare const ModalHost: FC;
6
12
 
7
- declare const useModal: () => {
8
- open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => React.ReactNode) => Promise<T>;
13
+ declare const useModalHost: () => {
14
+ render: () => react.ReactNode[];
9
15
  };
10
16
 
11
- export { ModalHost, ModalProvider, useModal };
17
+ export { ModalHost, ModalProvider, useModal, useModalHost };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,17 @@
1
- import React, { FC, PropsWithChildren } from 'react';
1
+ import * as react from 'react';
2
+ import react__default, { FC, PropsWithChildren } from 'react';
2
3
 
3
4
  declare const ModalProvider: FC<PropsWithChildren>;
4
5
 
6
+ declare const useModal: () => {
7
+ open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
8
+ };
9
+
10
+ /** @deprecated use renderModals instead */
5
11
  declare const ModalHost: FC;
6
12
 
7
- declare const useModal: () => {
8
- open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => React.ReactNode) => Promise<T>;
13
+ declare const useModalHost: () => {
14
+ render: () => react.ReactNode[];
9
15
  };
10
16
 
11
- export { ModalHost, ModalProvider, useModal };
17
+ export { ModalHost, ModalProvider, useModal, useModalHost };
package/dist/index.js CHANGED
@@ -15,9 +15,6 @@ var ModalProvider = ({ children }) => {
15
15
  return /* @__PURE__ */ jsx(ModalContext.Provider, { value: { stack, setStack }, children });
16
16
  };
17
17
 
18
- // src/ModalHost.tsx
19
- import { Fragment } from "react";
20
-
21
18
  // src/useModalContext.ts
22
19
  import { useContext } from "react";
23
20
  var useModalContext = () => {
@@ -27,17 +24,14 @@ var useModalContext = () => {
27
24
  }
28
25
  return context;
29
26
  };
30
-
31
- // src/ModalHost.tsx
32
- import { jsx as jsx2 } from "react/jsx-runtime";
33
- var ModalHost = () => {
27
+ var useModalHost = () => {
34
28
  const { stack } = useModalContext();
35
- return /* @__PURE__ */ jsx2(Fragment, { children: stack.map((item) => item) });
29
+ return {
30
+ render: () => stack.map((item) => item)
31
+ };
36
32
  };
37
33
 
38
34
  // src/useModal.tsx
39
- import { Fragment as Fragment2 } from "react";
40
- import { jsx as jsx3 } from "react/jsx-runtime";
41
35
  var useModal = () => {
42
36
  const { setStack } = useModalContext();
43
37
  const pop = () => {
@@ -55,13 +49,22 @@ var useModal = () => {
55
49
  reject(reason);
56
50
  pop();
57
51
  });
58
- push(/* @__PURE__ */ jsx3(Fragment2, { children: element }, key));
52
+ push(element);
59
53
  });
60
54
  };
61
55
  return { open };
62
56
  };
57
+
58
+ // src/ModalHost.tsx
59
+ import { Fragment } from "react";
60
+ import { jsx as jsx2 } from "react/jsx-runtime";
61
+ var ModalHost = () => {
62
+ const { render } = useModalHost();
63
+ return /* @__PURE__ */ jsx2(Fragment, { children: render() });
64
+ };
63
65
  export {
64
66
  ModalHost,
65
67
  ModalProvider,
66
- useModal
68
+ useModal,
69
+ useModalHost
67
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-flow-modal",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Promise-based modal flows for React",
5
5
  "repository": {
6
6
  "type": "git",
package/src/ModalHost.tsx CHANGED
@@ -1,12 +1,13 @@
1
- import React, { FC, Fragment } from 'react';
2
- import { useModalContext } from './useModalContext';
1
+ import { FC, Fragment } from 'react';
2
+ import { useModalHost } from './useModalContext';
3
3
 
4
+ /** @deprecated use renderModals instead */
4
5
  export const ModalHost: FC = () => {
5
- const { stack } = useModalContext();
6
+ const { render } = useModalHost();
6
7
 
7
8
  return (
8
9
  <Fragment>
9
- {stack.map((item) => (item))}
10
+ {render()}
10
11
  </Fragment>
11
12
  );
12
13
  };
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { ModalProvider } from './ModalProvider';
2
+ export { useModal } from './useModal';
2
3
  export { ModalHost } from './ModalHost';
3
- export { useModal } from './useModal';
4
+ export { useModalHost } from './useModalContext';
package/src/useModal.tsx CHANGED
@@ -29,7 +29,7 @@ export const useModal = () => {
29
29
  reject(reason);
30
30
  pop();
31
31
  });
32
- push(<Fragment key={key}>{element}</Fragment>);
32
+ push(element);
33
33
  });
34
34
  };
35
35
 
@@ -9,4 +9,12 @@ export const useModalContext = () => {
9
9
  }
10
10
 
11
11
  return context;
12
+ };
13
+
14
+ export const useModalHost = () => {
15
+ const { stack } = useModalContext();
16
+
17
+ return {
18
+ render: () => stack.map((item) => (item)),
19
+ };
12
20
  };