react-flow-modal 0.2.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 ADDED
@@ -0,0 +1,261 @@
1
+ # react-flow-modal
2
+
3
+ Promise-based modal flows for React.
4
+
5
+ `react-flow-modal` lets you treat modals as async flows using
6
+ `Promise` and `async/await`, without coupling your UI to state-driven logic.
7
+
8
+ ---
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pnpm add react-flow-modal
14
+ # or
15
+ npm install react-flow-modal
16
+ # or
17
+ yarn add react-flow-modal
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Basic Usage
23
+
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
+ }
151
+
152
+ function App() {
153
+ const modal = useModal();
154
+
155
+ const onClick = async () => {
156
+ const result = await modal.open("confirm", (resolve) => (
157
+ <ConfirmModal
158
+ onConfirm={() => resolve(true)}
159
+ onCancel={() => resolve(false)}
160
+ />
161
+ ));
162
+
163
+ console.log("Result:", result);
164
+ };
165
+
166
+ return <button onClick={onClick}>Open Confirm Modal</button>;
167
+ }
168
+
169
+ function ModalRenderer() {
170
+ return (
171
+ <AnimatePresence>
172
+ {renderModals()}
173
+ </AnimatePresence>
174
+ );
175
+ }
176
+
177
+ export default function Root() {
178
+ return (
179
+ <ModalProvider>
180
+ <App />
181
+ <ModalRenderer />
182
+ </ModalProvider>
183
+ );
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## API
190
+
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
214
+ ```
215
+ Renders the entire modal stack. This function should be rendered **once** in your React tree.
216
+
217
+ ---
218
+
219
+ ## Important
220
+
221
+ > ⚠️ Always resolve or reject the promise.
222
+ > Leaving it pending will block the async flow.
223
+
224
+ > ⚠️ `renderModals()` should be rendered once.
225
+ > Rendering it multiple times may result in duplicated modals.
226
+
227
+ ---
228
+
229
+ ## Why react-flow-modal?
230
+
231
+ Most modal libraries are state-driven:
232
+
233
+ ```tsx
234
+ setOpen(true);
235
+ ```
236
+
237
+ This makes modal control implicit and tightly coupled to rendering.
238
+
239
+ `react-flow-modal` treats modals as explicit async control points:
240
+
241
+ ```tsx
242
+ const result = await open(...);
243
+ ```
244
+
245
+ This keeps control flow readable, composable, and testable.
246
+
247
+ ---
248
+
249
+ ## Features
250
+
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
+ ---
258
+
259
+ ## License
260
+
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.2.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
  };