@violice/rmu 0.2.6 → 0.2.8
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 +341 -36
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +13 -28
- package/dist/index.mjs +2 -204
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -19
- package/src/constants.ts +8 -0
- package/src/emitter.test.ts +85 -0
- package/src/emitter.ts +24 -16
- package/src/events.test.ts +59 -0
- package/src/events.ts +14 -13
- package/src/integration.test.tsx +266 -0
- package/src/rmu-outlet.test.tsx +213 -0
- package/src/rmu-outlet.tsx +3 -2
- package/src/rmu-provider.test.tsx +15 -0
- package/src/types.ts +20 -16
- package/src/use-rmu-events.test.ts +117 -0
- package/src/use-rmu-events.ts +5 -5
- package/src/use-rmu-state.test.ts +129 -0
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -231
package/README.md
CHANGED
|
@@ -1,54 +1,359 @@
|
|
|
1
|
-
# RMU (React
|
|
1
|
+
# RMU (React Modal Utility)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@violice/rmu)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
A lightweight, zero-dependency utility for managing modals in React applications. Built with TypeScript, supports multiple outlets, and works seamlessly with React context.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Zero Dependencies** - No external libraries required
|
|
11
|
+
- **TypeScript** - Full type safety out of the box
|
|
12
|
+
- **Multiple Outlets** - Render modals in different parts of your app
|
|
13
|
+
- **Context-Aware** - Modals have access to React context at their outlet location
|
|
14
|
+
- **Imperative API** - Open/close modals from anywhere without prop drilling
|
|
15
|
+
- **Lightweight** - < 2KB gzipped
|
|
16
|
+
- **React Native Support** - Works with both React DOM and React Native
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
6
19
|
|
|
7
20
|
```bash
|
|
8
|
-
npm install
|
|
21
|
+
npm install @violice/rmu
|
|
22
|
+
# or
|
|
9
23
|
yarn add @violice/rmu
|
|
10
|
-
|
|
24
|
+
# or
|
|
25
|
+
pnpm add @violice/rmu
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- React >= 19
|
|
31
|
+
- Node.js >= 24
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
11
34
|
|
|
12
|
-
|
|
35
|
+
### 1. Wrap your app with `RMUProvider`
|
|
13
36
|
|
|
14
|
-
```
|
|
15
|
-
import {
|
|
37
|
+
```tsx
|
|
38
|
+
import { RMUProvider } from '@violice/rmu';
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
function App() {
|
|
18
41
|
return (
|
|
19
42
|
<RMUProvider>
|
|
20
|
-
|
|
21
|
-
onClick={() => {
|
|
22
|
-
const modal = openModal(
|
|
23
|
-
<TestModal onClose={() => closeModal(modal)} />
|
|
24
|
-
);
|
|
25
|
-
}}
|
|
26
|
-
>
|
|
27
|
-
Open modal
|
|
28
|
-
</button>
|
|
29
|
-
<SomeContextProvider>
|
|
30
|
-
<button onClick={() => {
|
|
31
|
-
const modal = openModal(
|
|
32
|
-
<TestContextModal onClose={() => closeModal(modal)} />,
|
|
33
|
-
{ outletId: 'MY_OUTLET' },
|
|
34
|
-
);
|
|
35
|
-
}}>
|
|
36
|
-
Open modal with access to some context
|
|
37
|
-
</button>
|
|
38
|
-
<RMUOutlet outletId="MY_OUTLET">
|
|
39
|
-
</SomeContextProvider>
|
|
40
|
-
<RMUOutlet />
|
|
43
|
+
<YourApp />
|
|
41
44
|
</RMUProvider>
|
|
42
45
|
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Add `RMUOutlet` where you want modals to render
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { RMUOutlet } from '@violice/rmu';
|
|
53
|
+
|
|
54
|
+
function Layout() {
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<main>{/* Your page content */}</main>
|
|
58
|
+
<RMUOutlet /> {/* Modals will render here */}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Open modals from anywhere
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { openModal, closeModal } from '@violice/rmu';
|
|
68
|
+
|
|
69
|
+
function MyComponent() {
|
|
70
|
+
const handleOpenModal = () => {
|
|
71
|
+
const modal = openModal(
|
|
72
|
+
<MyModal onClose={() => closeModal(modal)} />
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return <button onClick={handleOpenModal}>Open Modal</button>;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### Components
|
|
83
|
+
|
|
84
|
+
#### `RMUProvider`
|
|
85
|
+
|
|
86
|
+
Root provider component that must wrap your application or the part of the app that uses modals.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
interface RMUProviderProps {
|
|
90
|
+
children: React.ReactNode;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Usage:**
|
|
95
|
+
```tsx
|
|
96
|
+
<RMUProvider>
|
|
97
|
+
<App />
|
|
98
|
+
</RMUProvider>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `RMUOutlet`
|
|
102
|
+
|
|
103
|
+
Component that renders modals. Multiple outlets can be used with different IDs to render modals in different locations.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
interface RMUOutletProps {
|
|
107
|
+
outletId?: string; // Default: 'RMU:DEFAULT_OUTLET'
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Usage:**
|
|
112
|
+
```tsx
|
|
113
|
+
// Default outlet
|
|
114
|
+
<RMUOutlet />
|
|
115
|
+
|
|
116
|
+
// Custom outlet
|
|
117
|
+
<RMUOutlet outletId="sidebar-outlet" />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Functions
|
|
121
|
+
|
|
122
|
+
#### `openModal(component, config?)`
|
|
123
|
+
|
|
124
|
+
Opens a modal and returns its identifier.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
function openModal(
|
|
128
|
+
component: React.ReactNode,
|
|
129
|
+
config?: { outletId?: string }
|
|
130
|
+
): { modalId: string; outletId: string }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Parameters:**
|
|
134
|
+
- `component` - React element to render as modal
|
|
135
|
+
- `config.outletId` - Optional outlet ID (defaults to default outlet)
|
|
136
|
+
|
|
137
|
+
**Returns:**
|
|
138
|
+
- `modalId` - Unique identifier for the modal
|
|
139
|
+
- `outletId` - The outlet where the modal is rendered
|
|
140
|
+
|
|
141
|
+
**Usage:**
|
|
142
|
+
```tsx
|
|
143
|
+
const { modalId, outletId } = openModal(<MyModal />, {
|
|
144
|
+
outletId: 'custom-outlet'
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `closeModal({ modalId, outletId })`
|
|
149
|
+
|
|
150
|
+
Closes a specific modal by its identifier.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
function closeModal({
|
|
154
|
+
modalId: string,
|
|
155
|
+
outletId: string
|
|
156
|
+
}): void
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Usage:**
|
|
160
|
+
```tsx
|
|
161
|
+
const modal = openModal(<MyModal />);
|
|
162
|
+
// ... later
|
|
163
|
+
closeModal(modal);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Advanced Usage
|
|
167
|
+
|
|
168
|
+
### Multiple Outlets with Context Access
|
|
169
|
+
|
|
170
|
+
One of RMU's key features is the ability to render modals in different outlets, allowing them to access React context at that location.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { RMUProvider, RMUOutlet, openModal, closeModal } from '@violice/rmu';
|
|
174
|
+
import { ThemeContext, UserContext } from './contexts';
|
|
175
|
+
|
|
176
|
+
function App() {
|
|
177
|
+
return (
|
|
178
|
+
<RMUProvider>
|
|
179
|
+
<ThemeContext.Provider value="dark">
|
|
180
|
+
{/* Modals here have access to ThemeContext */}
|
|
181
|
+
<RMUOutlet outletId="theme-aware" />
|
|
182
|
+
|
|
183
|
+
<UserContext.Provider value={{ name: 'John' }}>
|
|
184
|
+
{/* Modals here have access to both contexts */}
|
|
185
|
+
<RMUOutlet outletId="user-aware" />
|
|
186
|
+
|
|
187
|
+
<button onClick={() => {
|
|
188
|
+
const modal = openModal(<UserProfileModal />, {
|
|
189
|
+
outletId: 'user-aware'
|
|
190
|
+
});
|
|
191
|
+
}}>
|
|
192
|
+
Open User Modal
|
|
193
|
+
</button>
|
|
194
|
+
</UserContext.Provider>
|
|
195
|
+
</ThemeContext.Provider>
|
|
196
|
+
</RMUProvider>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Managing Multiple Modals
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
function MultipleModalsExample() {
|
|
205
|
+
const modalsRef = useRef<Array<{ modalId: string; outletId: string }>>([]);
|
|
206
|
+
|
|
207
|
+
const openFirstModal = () => {
|
|
208
|
+
const modal = openModal(
|
|
209
|
+
<Modal1 onClose={() => closeModal(modal)} />
|
|
210
|
+
);
|
|
211
|
+
modalsRef.current.push(modal);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const openSecondModal = () => {
|
|
215
|
+
const modal = openModal(
|
|
216
|
+
<Modal2 onClose={() => closeModal(modal)} />
|
|
217
|
+
);
|
|
218
|
+
modalsRef.current.push(modal);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const closeAllModals = () => {
|
|
222
|
+
modalsRef.current.forEach(modal => closeModal(modal));
|
|
223
|
+
modalsRef.current = [];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div>
|
|
228
|
+
<button onClick={openFirstModal}>Open Modal 1</button>
|
|
229
|
+
<button onClick={openSecondModal}>Open Modal 2</button>
|
|
230
|
+
<button onClick={closeAllModals}>Close All</button>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Complete Modal Component Example
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
interface ConfirmModalProps {
|
|
240
|
+
title: string;
|
|
241
|
+
message: string;
|
|
242
|
+
onConfirm: () => void;
|
|
243
|
+
onClose: () => void;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ConfirmModal({ title, message, onConfirm, onClose }: ConfirmModalProps) {
|
|
247
|
+
return (
|
|
248
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
249
|
+
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
|
250
|
+
<h2>{title}</h2>
|
|
251
|
+
<p>{message}</p>
|
|
252
|
+
<div className="modal-actions">
|
|
253
|
+
<button onClick={onClose}>Cancel</button>
|
|
254
|
+
<button onClick={() => { onConfirm(); onClose(); }}>
|
|
255
|
+
Confirm
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Usage
|
|
264
|
+
function MyComponent() {
|
|
265
|
+
const handleDelete = () => {
|
|
266
|
+
const modal = openModal(
|
|
267
|
+
<ConfirmModal
|
|
268
|
+
title="Delete Item"
|
|
269
|
+
message="Are you sure you want to delete this item?"
|
|
270
|
+
onConfirm={() => console.log('Deleted!')}
|
|
271
|
+
onClose={() => closeModal(modal)}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return <button onClick={handleDelete}>Delete</button>;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## How It Works
|
|
281
|
+
|
|
282
|
+
RMU uses a combination of patterns to provide its functionality:
|
|
283
|
+
|
|
284
|
+
1. **Event Emitter** - A custom event system allows imperative `openModal()`/`closeModal()` calls from anywhere
|
|
285
|
+
2. **React Context** - `RMUProvider` maintains state and provides it to the component tree
|
|
286
|
+
3. **Reducer Pattern** - State updates are handled through a reducer for predictable state management
|
|
287
|
+
4. **Outlet System** - Multiple named outlets allow flexible modal placement
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
┌─────────────────────────────────────┐
|
|
291
|
+
│ RMUProvider │
|
|
292
|
+
│ ┌───────────────────────────────┐ │
|
|
293
|
+
│ │ State (Reducer) │ │
|
|
294
|
+
│ │ outlets: { │ │
|
|
295
|
+
│ │ 'default': { │ │
|
|
296
|
+
│ │ 'modal-1': <ModalA />, │ │
|
|
297
|
+
│ │ 'modal-2': <ModalB /> │ │
|
|
298
|
+
│ │ } │ │
|
|
299
|
+
│ │ } │ │
|
|
300
|
+
│ └───────────────────────────────┘ │
|
|
301
|
+
│ │ │
|
|
302
|
+
│ ▼ │
|
|
303
|
+
│ ┌───────────────────────────────┐ │
|
|
304
|
+
│ │ RMUOutlet │ │
|
|
305
|
+
│ │ Renders: <ModalA /> │ │
|
|
306
|
+
│ │ <ModalB /> │ │
|
|
307
|
+
│ └───────────────────────────────┘ │
|
|
308
|
+
└─────────────────────────────────────┘
|
|
309
|
+
▲
|
|
310
|
+
│ Events
|
|
311
|
+
openModal() / closeModal()
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## TypeScript
|
|
315
|
+
|
|
316
|
+
RMU is written in TypeScript and provides full type safety.
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import type { OpenModalPayload, CloseModalPayload } from '@violice/rmu';
|
|
320
|
+
|
|
321
|
+
// Types are available for advanced use cases
|
|
322
|
+
const handleOpen = (payload: OpenModalPayload) => {
|
|
323
|
+
console.log(payload.modalId, payload.outletId);
|
|
43
324
|
};
|
|
44
325
|
```
|
|
45
326
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
327
|
+
## Contributing
|
|
328
|
+
|
|
329
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
330
|
+
|
|
331
|
+
1. Fork the repository
|
|
332
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
333
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
334
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
335
|
+
5. Open a Pull Request
|
|
336
|
+
|
|
337
|
+
## Development
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# Install dependencies
|
|
341
|
+
npm install
|
|
342
|
+
|
|
343
|
+
# Run tests
|
|
344
|
+
npm test
|
|
345
|
+
|
|
346
|
+
# Build the library
|
|
347
|
+
npm run build
|
|
348
|
+
|
|
349
|
+
# Check bundle size
|
|
350
|
+
npm run size
|
|
351
|
+
```
|
|
51
352
|
|
|
52
353
|
## License
|
|
53
354
|
|
|
54
|
-
Licensed under MIT
|
|
355
|
+
Licensed under [MIT](LICENSE).
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
**Made with ❤️ by [Sergey Ivashko](https://github.com/violice)**
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`react`);c=s(c,1);let l=require(`react/jsx-runtime`);const u=(0,c.createContext)(null);let d=function(e){return e.OpenModal=`RMU:OPEN_MODAL`,e.CloseModal=`RMU:CLOSE_MODAL`,e}({});const f=`RMU:DEFAULT_OUTLET`,p={open:d.OpenModal,close:d.CloseModal},m={},h={subscribe(e,t){m[e]||(m[e]=new Set),m[e].add(t)},unsubscribe(e,t){let n=m[e];n&&(n.delete(t),n.size===0&&delete m[e])},emit(e,t){let n=m[e];n&&[...n].forEach(e=>{try{e(t)}catch{}})},_clear(){Object.keys(m).forEach(e=>{delete m[e]})}},g=e=>{let t={open:t=>e.openModal(t),close:t=>e.closeModal(t)};(0,c.useEffect)(()=>(Object.keys(t).forEach(e=>{h.subscribe(p[e],t[e])}),()=>{Object.keys(t).forEach(e=>{h.unsubscribe(p[e],t[e])})}),[])},_={openModal:`rmu:open-modal`,closeModal:`rmu:close-modal`,addOutlet:`rmu:add-modal`,removeOutlet:`rmu:remove-outlet`},v=(e,t)=>{switch(t.type){case _.openModal:{let{modalId:n,modalComponent:r,outletId:i}=t.payload,a=e.outlets[i];if(!a)throw Error(`Outlet with id ${i} not found`);return{...e,outlets:{...e.outlets,[i]:{...a,[n]:r}}}}case _.closeModal:{let{modalId:n,outletId:r}=t.payload,i=e.outlets[r];if(!i)throw Error(`Outlet with id ${r} not found`);let{[n]:a,...o}=i;return{...e,outlets:{...e.outlets,[r]:o}}}case _.addOutlet:{let{outletId:n}=t.payload;if(e.outlets[n])throw Error(`Outlet with id ${n} already exists`);return{...e,outlets:{...e.outlets,[n]:{}}}}case _.removeOutlet:{let{outletId:n}=t.payload,{[n]:r,...i}=e.outlets;return{...e,outlets:i}}default:return e}},y=()=>{let[e,t]=(0,c.useReducer)(v,{outlets:{}}),n=({modalId:e,modalComponent:n,outletId:r})=>{t({type:_.openModal,payload:{modalId:e,modalComponent:n,outletId:r}})},r=({modalId:e,outletId:n})=>{t({type:_.closeModal,payload:{modalId:e,outletId:n}})},i=e=>{t({type:_.addOutlet,payload:{outletId:e}})},a=e=>{t({type:_.removeOutlet,payload:{outletId:e}})};return{...e,openModal:n,closeModal:r,addOutlet:i,removeOutlet:a}},b=({children:e})=>{let t=y();return g(t),(0,l.jsx)(u.Provider,{value:t,children:e})},x=({outletId:e=f})=>{let t=(0,c.useContext)(u);if(!t)throw Error(`RMUProvider not found in component tree`);let{outlets:n,addOutlet:r,removeOutlet:i}=t;(0,c.useEffect)(()=>(r(e),()=>{i(e)}),[]);let a=n[e]??{};return(0,l.jsx)(l.Fragment,{children:Object.entries(a).map(([e,t])=>(0,l.jsx)(c.Fragment,{children:t},e))})},S=(e,t={})=>{let n=`rmu-modal-${new Date().getTime().toString()}`,{outletId:r=f}=t,i={modalId:n,modalComponent:e,outletId:r};return h.emit(d.OpenModal,i),{modalId:n,outletId:r}},C=({modalId:e,outletId:t})=>{let n={modalId:e,outletId:t};h.emit(d.CloseModal,n)};exports.RMUOutlet=x,exports.RMUProvider=b,exports.closeModal=C,exports.openModal=S;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Fragment"],"sources":["../src/rmu-context.ts","../src/types.ts","../src/constants.ts","../src/emitter.ts","../src/use-rmu-events.ts","../src/use-rmu-state.ts","../src/rmu-provider.tsx","../src/rmu-outlet.tsx","../src/events.ts"],"sourcesContent":["import { createContext } from 'react';\nimport { RMUContextState } from './types';\n\nexport const RMUContext = createContext<RMUContextState | null>(null);\n","import { ReactNode } from 'react';\n\nexport enum RMUEventType {\n OpenModal = 'RMU:OPEN_MODAL',\n CloseModal = 'RMU:CLOSE_MODAL',\n}\n\nexport type RMUContextState = {\n outlets: Record<string, Record<string, ReactNode>>;\n openModal: (payload: OpenModalPayload) => void;\n closeModal: (payload: CloseModalPayload) => void;\n addOutlet: (outletId: string) => void;\n removeOutlet: (outletId: string) => void;\n};\n\nexport type OpenModalPayload = {\n modalId: string;\n modalComponent: ReactNode;\n outletId: string;\n};\n\nexport type CloseModalPayload = {\n modalId: string;\n outletId: string;\n};\n\nexport type RMUEventPayload = OpenModalPayload | CloseModalPayload;\n","import { RMUEventType } from \"./types\";\n\nexport const RMU_DEFAULT_OUTLET_ID = 'RMU:DEFAULT_OUTLET' as const;\n\nexport const RMU_EVENTS = {\n open: RMUEventType.OpenModal,\n close: RMUEventType.CloseModal,\n} as const;\n","export type EventHandler<T = unknown> = (payload: T) => void;\n\ntype EventListenersMap = Record<string, Set<EventHandler<unknown>>>;\n\nconst listenersByType: EventListenersMap = {};\n\nexport const emitter = {\n subscribe<T>(type: string, handler: EventHandler<T>) {\n if (!listenersByType[type]) {\n listenersByType[type] = new Set<EventHandler<unknown>>();\n }\n listenersByType[type].add(handler as EventHandler<unknown>);\n },\n\n unsubscribe<T>(type: string, handler: EventHandler<T>) {\n const listeners = listenersByType[type];\n if (listeners) {\n listeners.delete(handler as EventHandler<unknown>);\n if (listeners.size === 0) {\n delete listenersByType[type];\n }\n }\n },\n\n emit<T>(type: string, payload: T) {\n const listeners = listenersByType[type];\n if (listeners) {\n // Clone to avoid issues if handlers mutate subscription during emit\n [...listeners].forEach((handler) => {\n try {\n handler(payload);\n } catch {\n // Swallow to ensure one handler error does not break others\n }\n });\n }\n },\n\n // For testing purposes only\n _clear() {\n Object.keys(listenersByType).forEach((key) => {\n delete listenersByType[key];\n });\n },\n};\n","import { useEffect } from 'react';\nimport { RMUContextState, OpenModalPayload, CloseModalPayload } from './types';\nimport { RMU_EVENTS } from './constants';\nimport { emitter } from './emitter';\n\nexport const useRMUEvents = (ctx: RMUContextState) => {\n const events = {\n open: (payload: OpenModalPayload) => ctx.openModal(payload),\n close: (payload: CloseModalPayload) => ctx.closeModal(payload),\n };\n\n useEffect(() => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.subscribe(RMU_EVENTS[event], events[event]);\n });\n\n return () => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.unsubscribe(RMU_EVENTS[event], events[event]);\n });\n };\n }, []);\n};\n","import { useReducer } from 'react';\nimport { RMUContextState } from './types';\n\nconst ACTIONS = {\n openModal: 'rmu:open-modal',\n closeModal: 'rmu:close-modal',\n addOutlet: 'rmu:add-modal',\n removeOutlet: 'rmu:remove-outlet',\n} as const;\n\nconst reducer = (\n state: {\n outlets: RMUContextState['outlets'];\n },\n action: {\n type: string;\n payload: Record<string, any>;\n }\n) => {\n switch (action.type) {\n case ACTIONS.openModal: {\n const { modalId, modalComponent, outletId } = action.payload;\n const modalOutlet = state.outlets[outletId];\n\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {\n ...modalOutlet,\n [modalId]: modalComponent,\n },\n },\n };\n }\n case ACTIONS.closeModal: {\n const { modalId, outletId } = action.payload;\n\n const modalOutlet = state.outlets[outletId];\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n const { [modalId]: _removed, ...restModals } = modalOutlet;\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: restModals,\n },\n };\n }\n case ACTIONS.addOutlet: {\n const { outletId } = action.payload;\n\n if (!!state.outlets[outletId]) {\n throw new Error(`Outlet with id ${outletId} already exists`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {},\n },\n };\n }\n case ACTIONS.removeOutlet: {\n const { outletId } = action.payload;\n\n const { [outletId]: _removed, ...restOutlets } = state.outlets;\n\n return {\n ...state,\n outlets: restOutlets,\n };\n }\n default:\n return state;\n }\n};\n\nexport const useRMUState = () => {\n const [state, dispatch] = useReducer(reducer, {\n outlets: {},\n });\n\n const openModal: RMUContextState['openModal'] = ({\n modalId,\n modalComponent,\n outletId,\n }) => {\n dispatch({\n type: ACTIONS.openModal,\n payload: { modalId, modalComponent, outletId },\n });\n };\n\n const closeModal: RMUContextState['closeModal'] = ({ modalId, outletId }) => {\n dispatch({ type: ACTIONS.closeModal, payload: { modalId, outletId } });\n };\n\n const addOutlet: RMUContextState['addOutlet'] = outletId => {\n dispatch({ type: ACTIONS.addOutlet, payload: { outletId } });\n };\n\n const removeOutlet: RMUContextState['removeOutlet'] = outletId => {\n dispatch({ type: ACTIONS.removeOutlet, payload: { outletId } });\n };\n\n return {\n ...state,\n openModal,\n closeModal,\n addOutlet,\n removeOutlet,\n };\n};\n","import React from 'react';\nimport { RMUContext } from './rmu-context';\nimport { useRMUEvents } from './use-rmu-events';\nimport { useRMUState } from './use-rmu-state';\n\nexport const RMUProvider = ({ children }: { children: React.ReactNode }) => {\n const state = useRMUState();\n\n useRMUEvents(state);\n\n return <RMUContext.Provider value={state}>{children}</RMUContext.Provider>;\n};\n","import React, { Fragment, useContext, useEffect } from 'react';\nimport { RMUContext } from './rmu-context';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const RMUOutlet = ({ outletId = RMU_DEFAULT_OUTLET_ID }) => {\n const ctx = useContext(RMUContext);\n\n if (!ctx) {\n throw new Error('RMUProvider not found in component tree');\n }\n\n const { outlets, addOutlet, removeOutlet } = ctx;\n\n useEffect(() => {\n addOutlet(outletId);\n return () => {\n removeOutlet(outletId);\n };\n }, []);\n\n const modals = outlets[outletId] ?? {};\n\n return (\n <>\n {Object.entries(modals).map(([modalId, modalComponent]) => (\n <Fragment key={modalId}>{modalComponent}</Fragment>\n ))}\n </>\n );\n};\n\nexport default RMUOutlet;\n","import { ReactNode } from 'react';\nimport { OpenModalPayload, CloseModalPayload, RMUEventType } from './types';\nimport { emitter } from './emitter';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const openModal = (\n modalComponent: ReactNode,\n config: { outletId?: string } = {}\n): { modalId: string; outletId: string } => {\n const modalId = `rmu-modal-${new Date().getTime().toString()}`;\n const { outletId = RMU_DEFAULT_OUTLET_ID } = config;\n const payload: OpenModalPayload = {\n modalId,\n modalComponent,\n outletId,\n };\n emitter.emit<OpenModalPayload>(RMUEventType.OpenModal, payload);\n return { modalId, outletId };\n};\n\nexport const closeModal = ({\n modalId,\n outletId,\n}: {\n modalId: string;\n outletId: string;\n}): void => {\n const payload: CloseModalPayload = { modalId, outletId };\n emitter.emit<CloseModalPayload>(RMUEventType.CloseModal, payload);\n};\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import React, { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/rmu-provider.d.ts
|
|
5
|
+
declare const RMUProvider: ({
|
|
6
|
+
children
|
|
7
|
+
}: {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}) => _$react_jsx_runtime0.JSX.Element;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/rmu-outlet.d.ts
|
|
12
|
+
declare const RMUOutlet: ({
|
|
13
|
+
outletId
|
|
14
|
+
}: {
|
|
15
|
+
outletId?: "RMU:DEFAULT_OUTLET" | undefined;
|
|
16
|
+
}) => _$react_jsx_runtime0.JSX.Element;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/events.d.ts
|
|
19
|
+
declare const openModal: (modalComponent: ReactNode, config?: {
|
|
20
|
+
outletId?: string;
|
|
21
|
+
}) => {
|
|
22
|
+
modalId: string;
|
|
23
|
+
outletId: string;
|
|
24
|
+
};
|
|
25
|
+
declare const closeModal: ({
|
|
26
|
+
modalId,
|
|
27
|
+
outletId
|
|
28
|
+
}: {
|
|
29
|
+
modalId: string;
|
|
30
|
+
outletId: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { RMUOutlet, RMUProvider, closeModal, openModal };
|
|
34
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,41 +1,19 @@
|
|
|
1
1
|
import React, { ReactNode } from "react";
|
|
2
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
3
|
|
|
3
4
|
//#region src/rmu-provider.d.ts
|
|
4
5
|
declare const RMUProvider: ({
|
|
5
6
|
children
|
|
6
7
|
}: {
|
|
7
8
|
children: React.ReactNode;
|
|
8
|
-
}) =>
|
|
9
|
+
}) => _$react_jsx_runtime0.JSX.Element;
|
|
9
10
|
//#endregion
|
|
10
11
|
//#region src/rmu-outlet.d.ts
|
|
11
12
|
declare const RMUOutlet: ({
|
|
12
13
|
outletId
|
|
13
14
|
}: {
|
|
14
|
-
outletId?:
|
|
15
|
-
}) =>
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/types.d.ts
|
|
18
|
-
type RMUContextState = {
|
|
19
|
-
outlets: Record<string, Record<string, ReactNode>>;
|
|
20
|
-
openModal: ({
|
|
21
|
-
modalId,
|
|
22
|
-
modalComponent,
|
|
23
|
-
outletId
|
|
24
|
-
}: {
|
|
25
|
-
modalId: string;
|
|
26
|
-
modalComponent: ReactNode;
|
|
27
|
-
outletId: string;
|
|
28
|
-
}) => void;
|
|
29
|
-
closeModal: ({
|
|
30
|
-
modalId,
|
|
31
|
-
outletId
|
|
32
|
-
}: {
|
|
33
|
-
modalId: string;
|
|
34
|
-
outletId: string;
|
|
35
|
-
}) => void;
|
|
36
|
-
addOutlet: (outletId: string) => void;
|
|
37
|
-
removeOutlet: (outletId: string) => void;
|
|
38
|
-
};
|
|
15
|
+
outletId?: "RMU:DEFAULT_OUTLET" | undefined;
|
|
16
|
+
}) => _$react_jsx_runtime0.JSX.Element;
|
|
39
17
|
//#endregion
|
|
40
18
|
//#region src/events.d.ts
|
|
41
19
|
declare const openModal: (modalComponent: ReactNode, config?: {
|
|
@@ -44,6 +22,13 @@ declare const openModal: (modalComponent: ReactNode, config?: {
|
|
|
44
22
|
modalId: string;
|
|
45
23
|
outletId: string;
|
|
46
24
|
};
|
|
47
|
-
declare const closeModal:
|
|
25
|
+
declare const closeModal: ({
|
|
26
|
+
modalId,
|
|
27
|
+
outletId
|
|
28
|
+
}: {
|
|
29
|
+
modalId: string;
|
|
30
|
+
outletId: string;
|
|
31
|
+
}) => void;
|
|
48
32
|
//#endregion
|
|
49
|
-
export { RMUOutlet, RMUProvider, closeModal, openModal };
|
|
33
|
+
export { RMUOutlet, RMUProvider, closeModal, openModal };
|
|
34
|
+
//# sourceMappingURL=index.d.mts.map
|