@lcashe/react-modal-controller 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zakharov Vladyslav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # react-modal-controller
2
+
3
+ Tiny and type-safe modal controller for React and Next.js.
4
+
5
+ - No global modal registry
6
+ - No reducers
7
+ - No boilerplate
8
+ - Full TypeScript inference
9
+
10
+ ---
11
+
12
+ # Navigation
13
+
14
+ - [Installation](#installation)
15
+ - [React Setup](#react-setup)
16
+ - [Next.js Setup](#nextjs-setup)
17
+ - [Important](#important)
18
+ - [Simple Example](#simple-example)
19
+ - [Modal With Props](#modal-with-props)
20
+ - [Dynamic Initial Props](#dynamic-initial-props)
21
+ - [Auto Close Example](#auto-close-example)
22
+ - [Multiple Modals](#multiple-modals)
23
+ - [API](#api)
24
+
25
+ ---
26
+
27
+ # Installation
28
+
29
+ ```bash
30
+ npm install @lcashe/react-modal-controller
31
+ ```
32
+
33
+ ---
34
+
35
+ # React Setup
36
+
37
+ Wrap your application with `ModalScope`.
38
+
39
+ ```tsx
40
+ import ReactDOM from 'react-dom/client';
41
+
42
+ import { ModalScope } from '@lcashe/react-modal-controller';
43
+
44
+ import { App } from './app';
45
+
46
+ ReactDOM.createRoot(document.getElementById('root')!).render(
47
+ <ModalScope>
48
+ <App />
49
+ </ModalScope>,
50
+ );
51
+ ```
52
+
53
+ ---
54
+
55
+ # Next.js Setup
56
+
57
+ `app/layout.tsx`
58
+
59
+ ```tsx
60
+ import { ModalScope } from '@lcashe/react-modal-controller';
61
+
62
+ export default function Page() {
63
+ return <ModalScope>// PAGE CONTENT</ModalScope>;
64
+ }
65
+ ```
66
+
67
+ ---
68
+
69
+ # Important
70
+
71
+ Your modal component must accept:
72
+
73
+ ```ts
74
+ opened: boolean;
75
+ onClose: VoidFunction;
76
+ ```
77
+
78
+ Optional:
79
+
80
+ ```ts
81
+ onExited?: VoidFunction;
82
+ ```
83
+
84
+ Example:
85
+
86
+ ```tsx
87
+ type ModalProps = {
88
+ opened: boolean;
89
+ title: string;
90
+ onClose: VoidFunction;
91
+ };
92
+ ```
93
+
94
+ ---
95
+
96
+ # Simple Example
97
+
98
+ ## Modal
99
+
100
+ ```tsx
101
+ 'use client';
102
+
103
+
104
+ type ModalProps = {
105
+ opened: boolean;
106
+ onClose: VoidFunction;
107
+ };
108
+
109
+ export const Modal = ({ opened, onClose }: ModalProps) => {
110
+ if (!opened) {
111
+ return null;
112
+ }
113
+
114
+ return (
115
+ <div>
116
+ <h1>Modal</h1>
117
+
118
+ <button onClick={onClose}>Close</button>
119
+ </div>
120
+ );
121
+ };
122
+ ```
123
+
124
+ ## Usage
125
+
126
+ ```tsx
127
+ 'use client';
128
+
129
+ import { useModalController } from '@lcashe/react-modal-controller';
130
+
131
+ import { Modal } from './modal';
132
+
133
+ export const Component = () => {
134
+ const modal = useModalController(Modal);
135
+
136
+ return <button onClick={() => modal.open()}>Open modal</button>;
137
+ };
138
+ ```
139
+
140
+ ---
141
+
142
+ # Modal With Props
143
+
144
+ ## Modal
145
+
146
+ ```tsx
147
+ 'use client';
148
+
149
+ type ModalProps = {
150
+ opened: boolean;
151
+ title: string;
152
+ onClose: VoidFunction;
153
+ };
154
+
155
+ export const Modal = ({ title, opened, onClose }: ModalProps) => {
156
+ if (!opened) {
157
+ return null;
158
+ }
159
+
160
+ return (
161
+ <div>
162
+ <h1>{title}</h1>
163
+
164
+ <button onClick={onClose}>Close</button>
165
+ </div>
166
+ );
167
+ };
168
+ ```
169
+
170
+ ## Usage
171
+
172
+ ```tsx
173
+ 'use client';
174
+
175
+ import { useModalController } from '@lcashe/react-modal-controller';
176
+
177
+ import { Modal } from './modal';
178
+
179
+ export const Component = () => {
180
+ const modal = useModalController(Modal, {
181
+ title: 'Initial title',
182
+ });
183
+
184
+ return (
185
+ <button
186
+ onClick={() =>
187
+ modal.open({
188
+ title: 'Another title', // optional
189
+ })
190
+ }
191
+ >
192
+ Open modal
193
+ </button>
194
+ );
195
+ };
196
+ ```
197
+
198
+ ---
199
+
200
+ # TypeScript Inference
201
+
202
+ Props are inferred automatically.
203
+
204
+ ```tsx
205
+ modal.open({
206
+ title: 'Example title',
207
+ });
208
+ ```
209
+
210
+ Autocomplete works out of the box.
211
+
212
+ ---
213
+
214
+ # Dynamic Initial Props
215
+
216
+ `initialProps` stay synchronized automatically.
217
+
218
+ ```tsx
219
+ const modal = useModalController(Modal, {
220
+ title,
221
+ });
222
+ ```
223
+
224
+ When `title` changes, the next `open()` call will use the latest value.
225
+
226
+ ---
227
+
228
+ # Auto Close Example
229
+
230
+ ```tsx
231
+ 'use client';
232
+
233
+ import { useEffect } from 'react';
234
+
235
+ import { useModalController } from '@lcashe/react-modal-controller';
236
+
237
+ import { Modal } from './modal';
238
+
239
+ export const Component = () => {
240
+ const modal = useModalController(Modal);
241
+
242
+ useEffect(() => {
243
+ modal.open();
244
+
245
+ const timeoutId = setTimeout(() => {
246
+ modal.close();
247
+ }, 2000);
248
+
249
+ return () => {
250
+ clearTimeout(timeoutId);
251
+ };
252
+ }, [modal]);
253
+
254
+ return null;
255
+ };
256
+ ```
257
+
258
+ ---
259
+
260
+ # Multiple Modals
261
+
262
+ ```tsx
263
+ const firstModal = useModalController(FirstModal);
264
+
265
+ const secondModal = useModalController(SecondModal);
266
+
267
+ const thirdModal = useModalController(ThirdModal);
268
+ ```
269
+
270
+ ---
271
+
272
+ # Multiple Same Modals
273
+
274
+ Each `useModalController` call creates its own local modal instance.
275
+
276
+ Even if you pass the same modal component, every controller receives a unique internal id, so the modals are controlled separately.
277
+
278
+ ```tsx
279
+ const firstModal = useModalController(Modal);
280
+
281
+ const secondModal = useModalController(Modal);
282
+
283
+ ...
284
+
285
+ secondModal.open() // <-- will open only the second modal
286
+ ```
287
+
288
+ ---
289
+
290
+ # API
291
+
292
+ ## `useModalController(Component, initialProps?)`
293
+
294
+ Returns:
295
+
296
+ ```ts
297
+ {
298
+ open,
299
+ close,
300
+ remove,
301
+ }
302
+ ```
303
+
304
+ ## `open(props?)`
305
+
306
+ Open modal with optional props.
307
+
308
+ ```tsx
309
+ modal.open();
310
+
311
+ modal.open({
312
+ title: 'New title',
313
+ });
314
+ ```
315
+
316
+ ---
317
+
318
+ ## `close()`
319
+
320
+ Close modal.
321
+
322
+ ```tsx
323
+ modal.close();
324
+ ```
325
+
326
+ ---
327
+
328
+ ## `remove()`
329
+
330
+ Remove modal from the store completely.
331
+
332
+ ```tsx
333
+ modal.remove();
334
+ ```
335
+
336
+ ---
337
+
338
+ # License
339
+
340
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var M=Object.defineProperty,b=Object.defineProperties,j=Object.getOwnPropertyDescriptor,S=Object.getOwnPropertyDescriptors,h=Object.getOwnPropertyNames,I=Object.getOwnPropertySymbols;var v=Object.prototype.hasOwnProperty,k=Object.prototype.propertyIsEnumerable;var T=(t,e,o)=>e in t?M(t,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[e]=o,m=(t,e)=>{for(var o in e||(e={}))v.call(e,o)&&T(t,o,e[o]);if(I)for(var o of I(e))k.call(e,o)&&T(t,o,e[o]);return t},C=(t,e)=>b(t,S(e));var g=(t,e)=>{for(var o in e)M(t,o,{get:e[o],enumerable:!0})},V=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let p of h(e))!v.call(t,p)&&p!==o&&M(t,p,{get:()=>e[p],enumerable:!(r=j(e,p))||r.enumerable});return t};var w=t=>V(M({},"__esModule",{value:!0}),t);var W={};g(W,{ModalContext:()=>i,ModalScope:()=>N,useModalController:()=>R});module.exports=w(W);var a=require("react");var E=require("react"),i=(0,E.createContext)(null);var u=require("react/jsx-runtime"),N=({children:t})=>{let[e,o]=(0,a.useState)([]),r=(0,a.useCallback)(l=>{let d={id:l.id,opened:!0,Component:l.Component,props:l.props};o(n=>{let x=n.findIndex(y=>y.id===l.id);if(x===-1)return[...n,d];let f=[...n];return f[x]=d,f})},[]),p=(0,a.useCallback)(l=>{o(d=>d.map(n=>n.id===l?C(m({},n),{opened:!1}):n))},[]),c=(0,a.useCallback)(l=>{o(d=>d.filter(n=>n.id!==l))},[]),P=(0,a.useMemo)(()=>({open:r,close:p,remove:c}),[r,p,c]);return(0,u.jsxs)(i.Provider,{value:P,children:[t,e.map(({Component:l,id:d,opened:n,props:x})=>(0,u.jsx)(l,C(m({},x),{opened:n,onClose:()=>p(d),onExited:()=>c(d)}),d))]})};var s=require("react");var R=(t,e)=>{let o=(0,s.useContext)(i);if(!o)throw new Error("useModalController must be used inside <ModalScope>");let r=(0,s.useId)(),p=(0,s.useRef)(e);(0,s.useEffect)(()=>{p.current=e},[e]),(0,s.useEffect)(()=>()=>{o.remove(r)},[o,r]);let c=(0,s.useCallback)(d=>{let n=m(m({},p.current||{}),d||{});o.open({id:r,Component:t,props:n})},[o,r,t]),P=(0,s.useCallback)(()=>{o.close(r)},[o,r]),l=(0,s.useCallback)(()=>{o.remove(r)},[o,r]);return{open:c,close:P,remove:l}};0&&(module.exports={ModalContext,ModalScope,useModalController});
@@ -0,0 +1,42 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { PropsWithChildren, ComponentType } from 'react';
4
+
5
+ declare const ModalScope: ({ children }: PropsWithChildren) => react_jsx_runtime.JSX.Element;
6
+
7
+ type Nullable<T> = T | null;
8
+ type Maybe<T> = T | undefined;
9
+ type ModalInjectedProps = {
10
+ opened: boolean;
11
+ onClose: VoidFunction;
12
+ onExited?: VoidFunction;
13
+ };
14
+ type ModalComponent<TProps extends object = object> = ComponentType<TProps & ModalInjectedProps>;
15
+ type ExternalModalProps$1<TComponent> = TComponent extends ComponentType<infer TProps> ? Omit<TProps, keyof ModalInjectedProps> : never;
16
+ type ModalItem<TProps extends object = object> = {
17
+ id: string;
18
+ Component: ModalComponent<TProps>;
19
+ props: TProps;
20
+ };
21
+ type StoredModalItem = {
22
+ id: string;
23
+ opened: boolean;
24
+ props: object;
25
+ Component: ComponentType<object & ModalInjectedProps>;
26
+ };
27
+
28
+ type ExternalModalProps<TProps extends ModalInjectedProps> = Omit<TProps, keyof ModalInjectedProps>;
29
+ declare const useModalController: <TProps extends ModalInjectedProps>(Component: ComponentType<TProps>, initialProps?: ExternalModalProps<TProps>) => {
30
+ open: (props?: Partial<ExternalModalProps<TProps>>) => void;
31
+ close: () => void;
32
+ remove: () => void;
33
+ };
34
+
35
+ type ModalContextValue = {
36
+ open: <TProps extends object>(item: ModalItem<TProps>) => void;
37
+ close: (id: string) => void;
38
+ remove: (id: string) => void;
39
+ };
40
+ declare const ModalContext: react.Context<Nullable<ModalContextValue>>;
41
+
42
+ export { type ExternalModalProps$1 as ExternalModalProps, type Maybe, type ModalComponent, ModalContext, type ModalContextValue, type ModalInjectedProps, type ModalItem, ModalScope, type Nullable, type StoredModalItem, useModalController };
@@ -0,0 +1,42 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { PropsWithChildren, ComponentType } from 'react';
4
+
5
+ declare const ModalScope: ({ children }: PropsWithChildren) => react_jsx_runtime.JSX.Element;
6
+
7
+ type Nullable<T> = T | null;
8
+ type Maybe<T> = T | undefined;
9
+ type ModalInjectedProps = {
10
+ opened: boolean;
11
+ onClose: VoidFunction;
12
+ onExited?: VoidFunction;
13
+ };
14
+ type ModalComponent<TProps extends object = object> = ComponentType<TProps & ModalInjectedProps>;
15
+ type ExternalModalProps$1<TComponent> = TComponent extends ComponentType<infer TProps> ? Omit<TProps, keyof ModalInjectedProps> : never;
16
+ type ModalItem<TProps extends object = object> = {
17
+ id: string;
18
+ Component: ModalComponent<TProps>;
19
+ props: TProps;
20
+ };
21
+ type StoredModalItem = {
22
+ id: string;
23
+ opened: boolean;
24
+ props: object;
25
+ Component: ComponentType<object & ModalInjectedProps>;
26
+ };
27
+
28
+ type ExternalModalProps<TProps extends ModalInjectedProps> = Omit<TProps, keyof ModalInjectedProps>;
29
+ declare const useModalController: <TProps extends ModalInjectedProps>(Component: ComponentType<TProps>, initialProps?: ExternalModalProps<TProps>) => {
30
+ open: (props?: Partial<ExternalModalProps<TProps>>) => void;
31
+ close: () => void;
32
+ remove: () => void;
33
+ };
34
+
35
+ type ModalContextValue = {
36
+ open: <TProps extends object>(item: ModalItem<TProps>) => void;
37
+ close: (id: string) => void;
38
+ remove: (id: string) => void;
39
+ };
40
+ declare const ModalContext: react.Context<Nullable<ModalContextValue>>;
41
+
42
+ export { type ExternalModalProps$1 as ExternalModalProps, type Maybe, type ModalComponent, ModalContext, type ModalContextValue, type ModalInjectedProps, type ModalItem, ModalScope, type Nullable, type StoredModalItem, useModalController };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var v=Object.defineProperty,E=Object.defineProperties;var y=Object.getOwnPropertyDescriptors;var C=Object.getOwnPropertySymbols;var b=Object.prototype.hasOwnProperty,j=Object.prototype.propertyIsEnumerable;var f=(t,e,o)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[e]=o,d=(t,e)=>{for(var o in e||(e={}))b.call(e,o)&&f(t,o,e[o]);if(C)for(var o of C(e))j.call(e,o)&&f(t,o,e[o]);return t},x=(t,e)=>E(t,y(e));import{useCallback as M,useMemo as h,useState as k}from"react";import{createContext as S}from"react";var c=S(null);import{jsx as g,jsxs as V}from"react/jsx-runtime";var G=({children:t})=>{let[e,o]=k([]),p=M(n=>{let s={id:n.id,opened:!0,Component:n.Component,props:n.props};o(r=>{let m=r.findIndex(T=>T.id===n.id);if(m===-1)return[...r,s];let P=[...r];return P[m]=s,P})},[]),l=M(n=>{o(s=>s.map(r=>r.id===n?x(d({},r),{opened:!1}):r))},[]),a=M(n=>{o(s=>s.filter(r=>r.id!==n))},[]),i=h(()=>({open:p,close:l,remove:a}),[p,l,a]);return V(c.Provider,{value:i,children:[t,e.map(({Component:n,id:s,opened:r,props:m})=>g(n,x(d({},m),{opened:r,onClose:()=>l(s),onExited:()=>a(s)}),s))]})};import{useCallback as u,useContext as w,useEffect as I,useId as N,useRef as R}from"react";var X=(t,e)=>{let o=w(c);if(!o)throw new Error("useModalController must be used inside <ModalScope>");let p=N(),l=R(e);I(()=>{l.current=e},[e]),I(()=>()=>{o.remove(p)},[o,p]);let a=u(s=>{let r=d(d({},l.current||{}),s||{});o.open({id:p,Component:t,props:r})},[o,p,t]),i=u(()=>{o.close(p)},[o,p]),n=u(()=>{o.remove(p)},[o,p]);return{open:a,close:i,remove:n}};export{c as ModalContext,G as ModalScope,X as useModalController};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@lcashe/react-modal-controller",
3
+ "version": "1.0.0",
4
+ "description": "Tiny type-safe modal controller for React with inferred props and no registry.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup components/modal-controller/index.ts --format esm,cjs --dts --minify --external react --tsconfig tsconfig.build.json",
16
+ "dev": "tsup components/modal-controller/index.ts --format esm,cjs --dts --watch --external react --tsconfig tsconfig.build.json",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "react",
21
+ "modal",
22
+ "modal-controller",
23
+ "typescript",
24
+ "nextjs"
25
+ ],
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19",
31
+ "tsup": "latest",
32
+ "typescript": "^5"
33
+ }
34
+ }