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