@ivex0002/stack-modal 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/README.md +595 -0
- package/dist/index.cjs +120 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +83 -0
- package/dist/index.mjs +83 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# @ivex0002/stack-modal
|
|
2
|
+
|
|
3
|
+
A flexible and customizable modal stack management library for React applications. Modal Stack allows you to create layered modals with smooth animations and various preset styles.
|
|
4
|
+
|
|
5
|
+
## Preview
|
|
6
|
+
|
|
7
|
+
### default preset
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
### custom layout style (tailwind, framer-motion)
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
### test project link
|
|
16
|
+
|
|
17
|
+
https://github.com/Ivex0002/stack-modal-test-project
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- đ¨ **Multiple Presets**: Default, Minimal, and Drawer layouts
|
|
22
|
+
- đ **Modal Stacking**: Support for multiple modals with depth management
|
|
23
|
+
- â¨ī¸ **Keyboard Support**: ESC key to close modals
|
|
24
|
+
- đ **Custom Layouts**: Create your own modal layouts
|
|
25
|
+
- đĒļ **Lightweight**: Minimal dependencies
|
|
26
|
+
- đĒ **TypeScript**: Full type safety
|
|
27
|
+
- đ **React 18 Compatible**: Built with useSyncExternalStore
|
|
28
|
+
- đ¯ **Zero runtime dependencies** (React as peer dependency)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### Core Package
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @ivex0002/stack-modal
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Presets (Optional)
|
|
39
|
+
|
|
40
|
+
If you want to use built-in presets:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @ivex0002/stack-modal-presets
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> **Note**: Presets are optional. If you're creating custom layouts, you don't need to install this presets package.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createModalStack } from "@ivex0002/stack-modal";
|
|
52
|
+
import { defaultPreset } from "@ivex0002/stack-modal-presets";
|
|
53
|
+
|
|
54
|
+
// Define your modals
|
|
55
|
+
const modals = {
|
|
56
|
+
alert: ({ message }: { message: string }) => (
|
|
57
|
+
<div>
|
|
58
|
+
<h2>Alert</h2>
|
|
59
|
+
<p>{message}</p>
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
confirm: ({ title, onConfirm }: { title: string; onConfirm: () => void }) => (
|
|
63
|
+
<div>
|
|
64
|
+
<h2>{title}</h2>
|
|
65
|
+
<button onClick={onConfirm}>Confirm</button>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create modal instance with preset
|
|
71
|
+
const modal = createModalStack(modals, defaultPreset);
|
|
72
|
+
|
|
73
|
+
// Use in your component
|
|
74
|
+
function App() {
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<button onClick={() => modal.alert.push({ message: "Hello!" })}>
|
|
78
|
+
Show Alert
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
onClick={() =>
|
|
82
|
+
modal.confirm.push({
|
|
83
|
+
title: "Are you sure?",
|
|
84
|
+
onConfirm: () => console.log("Confirmed!"),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
Show Confirm
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Presets
|
|
96
|
+
|
|
97
|
+
### Default Preset
|
|
98
|
+
|
|
99
|
+
A centered modal with stacking animation. Multiple modals are displayed with offset and scale effects.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { defaultPreset } from "@ivex0002/stack-modal-presets";
|
|
103
|
+
|
|
104
|
+
const modal = createModalStack(modals, defaultPreset);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Minimal Preset
|
|
108
|
+
|
|
109
|
+
A simple centered modal with fade transitions. Only the top modal is visible.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { minimalPreset } from "@ivex0002/stack-modal-presets";
|
|
113
|
+
|
|
114
|
+
const modal = createModalStack(modals, minimalPreset);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Drawer Preset
|
|
118
|
+
|
|
119
|
+
A bottom sheet style modal that slides up from the bottom of the screen.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { drawerPreset } from "@ivex0002/stack-modal-presets";
|
|
123
|
+
|
|
124
|
+
const modal = createModalStack(modals, drawerPreset);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### `createModalStack(modals, modalLayout)`
|
|
130
|
+
|
|
131
|
+
Creates a modal instance with the specified modals and layout.
|
|
132
|
+
|
|
133
|
+
**Parameters:**
|
|
134
|
+
|
|
135
|
+
- `modals`: An object where keys are modal names and values are React components
|
|
136
|
+
- `modalLayout`: A layout configuration object (use presets or create custom)
|
|
137
|
+
|
|
138
|
+
**Returns:**
|
|
139
|
+
|
|
140
|
+
- Modal control object with `push` methods for each modal and a `pop` method
|
|
141
|
+
|
|
142
|
+
### Modal Control Object
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const modal = createModalStack(modals, preset);
|
|
146
|
+
|
|
147
|
+
// Push a modal
|
|
148
|
+
modal.modalName.push(props);
|
|
149
|
+
|
|
150
|
+
// Close the top modal
|
|
151
|
+
modal.pop();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Custom Layout
|
|
155
|
+
|
|
156
|
+
You can create your own modal layout by implementing the `ModalLayout` interface.
|
|
157
|
+
|
|
158
|
+
this example made with tailwindcss & framer-motion.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { motion } from "framer-motion";
|
|
162
|
+
import React from "react";
|
|
163
|
+
import type { ModalLayout } from "@ivex0002/stack-modal";
|
|
164
|
+
|
|
165
|
+
const STACK_OFFSET = 80;
|
|
166
|
+
const SCALE_STEP = 0.06;
|
|
167
|
+
const OPACITY_STEP = 0.08;
|
|
168
|
+
|
|
169
|
+
export const twfmModalLayoutExample: ModalLayout = {
|
|
170
|
+
Background: ({
|
|
171
|
+
children,
|
|
172
|
+
onClose,
|
|
173
|
+
}: {
|
|
174
|
+
children: React.ReactNode;
|
|
175
|
+
onClose: () => void;
|
|
176
|
+
}) => {
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
const onKey = (e: KeyboardEvent) => {
|
|
179
|
+
if (e.key === "Escape") onClose();
|
|
180
|
+
};
|
|
181
|
+
window.addEventListener("keyup", onKey);
|
|
182
|
+
return () => window.removeEventListener("keyup", onKey);
|
|
183
|
+
}, [onClose]);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<motion.div
|
|
187
|
+
className="fixed inset-0 z-1000 flex items-center justify-center backdrop-blur-md"
|
|
188
|
+
style={{
|
|
189
|
+
background:
|
|
190
|
+
"linear-gradient(to bottom right, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2), rgba(236, 72, 153, 0.2))",
|
|
191
|
+
}}
|
|
192
|
+
initial={{ opacity: 0 }}
|
|
193
|
+
animate={{ opacity: 1 }}
|
|
194
|
+
exit={{ opacity: 0 }}
|
|
195
|
+
transition={{ duration: 0.3 }}
|
|
196
|
+
onClick={onClose}
|
|
197
|
+
>
|
|
198
|
+
<motion.div
|
|
199
|
+
className="absolute inset-0 bg-black/40"
|
|
200
|
+
initial={{ opacity: 0 }}
|
|
201
|
+
animate={{ opacity: 1 }}
|
|
202
|
+
exit={{ opacity: 0 }}
|
|
203
|
+
/>
|
|
204
|
+
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
|
205
|
+
</motion.div>
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
ModalWrap: ({
|
|
210
|
+
children,
|
|
211
|
+
depth,
|
|
212
|
+
isTop,
|
|
213
|
+
}: {
|
|
214
|
+
children: React.ReactNode;
|
|
215
|
+
depth: number;
|
|
216
|
+
isTop: boolean;
|
|
217
|
+
}) => {
|
|
218
|
+
const x = -depth * STACK_OFFSET;
|
|
219
|
+
const scale = 1 - depth * SCALE_STEP;
|
|
220
|
+
const opacity = 1 - depth * OPACITY_STEP;
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<motion.div
|
|
224
|
+
className="absolute w-auto max-w-[90vw] rounded-3xl bg-white p-8 ring-1 ring-black/5"
|
|
225
|
+
style={{
|
|
226
|
+
left: "50%",
|
|
227
|
+
top: "50%",
|
|
228
|
+
pointerEvents: isTop ? "auto" : "none",
|
|
229
|
+
background:
|
|
230
|
+
"linear-gradient(to bottom right, white, white, rgb(249, 250, 251))",
|
|
231
|
+
boxShadow: isTop
|
|
232
|
+
? "0 20px 70px -10px rgba(0,0,0,0.3), 0 0 0 1px rgba(99,102,241,0.1)"
|
|
233
|
+
: "0 10px 40px -10px rgba(0,0,0,0.2)",
|
|
234
|
+
}}
|
|
235
|
+
initial={{
|
|
236
|
+
opacity: 0,
|
|
237
|
+
scale: 0.9,
|
|
238
|
+
x: "-50%",
|
|
239
|
+
y: "-45%",
|
|
240
|
+
}}
|
|
241
|
+
animate={{
|
|
242
|
+
opacity,
|
|
243
|
+
scale,
|
|
244
|
+
x: `calc(-50% + ${x}px)`,
|
|
245
|
+
y: "-50%",
|
|
246
|
+
}}
|
|
247
|
+
exit={{
|
|
248
|
+
opacity: 0,
|
|
249
|
+
scale: 0.9,
|
|
250
|
+
x: "-50%",
|
|
251
|
+
y: "-45%",
|
|
252
|
+
}}
|
|
253
|
+
transition={{
|
|
254
|
+
type: "spring",
|
|
255
|
+
stiffness: 280,
|
|
256
|
+
damping: 28,
|
|
257
|
+
mass: 0.8,
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<div
|
|
261
|
+
className="absolute inset-0 rounded-3xl pointer-events-none"
|
|
262
|
+
style={{
|
|
263
|
+
background:
|
|
264
|
+
"linear-gradient(to bottom right, rgba(99, 102, 241, 0.05), transparent, rgba(168, 85, 247, 0.05))",
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
<div
|
|
268
|
+
className="absolute -top-px left-1/2 -translate-x-1/2 w-1/2 h-px"
|
|
269
|
+
style={{
|
|
270
|
+
background:
|
|
271
|
+
"linear-gradient(to right, transparent, rgba(99, 102, 241, 0.5), transparent)",
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
<div className="relative z-10">{children}</div>
|
|
276
|
+
</motion.div>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const modal = createModalStack(modals, twfmModalLayoutExample);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### ModalLayout Interface
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
type ModalLayout = {
|
|
288
|
+
Background: React.ComponentType<{
|
|
289
|
+
children: React.ReactNode;
|
|
290
|
+
onClose: () => void;
|
|
291
|
+
}>;
|
|
292
|
+
ModalWrap: React.ComponentType<{
|
|
293
|
+
children: React.ReactNode;
|
|
294
|
+
depth: number;
|
|
295
|
+
isTop: boolean;
|
|
296
|
+
}>;
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- **Background**: The backdrop and container for all modals
|
|
301
|
+
|
|
302
|
+
- `onClose`: Function to close the top modal
|
|
303
|
+
- `children`: The modal content
|
|
304
|
+
|
|
305
|
+
- **ModalWrap**: Wrapper for each individual modal
|
|
306
|
+
- `depth`: The position in the stack (0 = top, 1 = second from top, etc.)
|
|
307
|
+
- `isTop`: Boolean indicating if this is the topmost modal
|
|
308
|
+
- `children`: The modal content
|
|
309
|
+
|
|
310
|
+
## Examples
|
|
311
|
+
|
|
312
|
+
### Alert Modal
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const modals = {
|
|
316
|
+
alert: ({ message }: { message: string }) => (
|
|
317
|
+
<div style={{ padding: "20px" }}>
|
|
318
|
+
<h2>Alert</h2>
|
|
319
|
+
<p>{message}</p>
|
|
320
|
+
<button onClick={() => modal.pop()}>OK</button>
|
|
321
|
+
</div>
|
|
322
|
+
),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Usage
|
|
326
|
+
modal.alert.push({ message: "Something happened!" });
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Confirmation Modal
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const modals = {
|
|
333
|
+
confirm: ({
|
|
334
|
+
title,
|
|
335
|
+
message,
|
|
336
|
+
onConfirm,
|
|
337
|
+
}: {
|
|
338
|
+
title: string;
|
|
339
|
+
message: string;
|
|
340
|
+
onConfirm: () => void;
|
|
341
|
+
}) => (
|
|
342
|
+
<div style={{ padding: "20px" }}>
|
|
343
|
+
<h2>{title}</h2>
|
|
344
|
+
<p>{message}</p>
|
|
345
|
+
<div style={{ display: "flex", gap: "10px" }}>
|
|
346
|
+
<button
|
|
347
|
+
onClick={() => {
|
|
348
|
+
onConfirm();
|
|
349
|
+
modal.pop();
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
Confirm
|
|
353
|
+
</button>
|
|
354
|
+
<button onClick={() => modal.pop()}>Cancel</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
),
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Usage
|
|
361
|
+
modal.confirm.push({
|
|
362
|
+
title: "Delete Item",
|
|
363
|
+
message: "Are you sure you want to delete this item?",
|
|
364
|
+
onConfirm: () => console.log("Item deleted"),
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Stacked Modals
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Open multiple modals
|
|
372
|
+
modal.first.push({ data: "First modal" });
|
|
373
|
+
modal.second.push({ data: "Second modal" });
|
|
374
|
+
modal.third.push({ data: "Third modal" });
|
|
375
|
+
|
|
376
|
+
// Close them one by one
|
|
377
|
+
modal.pop(); // Closes third
|
|
378
|
+
modal.pop(); // Closes second
|
|
379
|
+
modal.pop(); // Closes first
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## TypeScript Support
|
|
383
|
+
|
|
384
|
+
Modal Stack is written in TypeScript and provides full type safety:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Props are fully typed
|
|
388
|
+
const modals = {
|
|
389
|
+
user: ({ name, age }: { name: string; age: number }) => (
|
|
390
|
+
<div>
|
|
391
|
+
<p>Name: {name}</p>
|
|
392
|
+
<p>Age: {age}</p>
|
|
393
|
+
</div>
|
|
394
|
+
),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const modal = createModalStack(modals, defaultPreset);
|
|
398
|
+
|
|
399
|
+
// â
Correct
|
|
400
|
+
modal.user.push({ name: "John", age: 30 });
|
|
401
|
+
|
|
402
|
+
// â TypeScript error
|
|
403
|
+
modal.user.push({ name: "John" }); // Missing 'age'
|
|
404
|
+
modal.user.push({ name: "John", age: "30" }); // Wrong type
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## How It Works
|
|
408
|
+
|
|
409
|
+
Modal Stack uses a simple but powerful architecture to manage modal states and rendering.
|
|
410
|
+
|
|
411
|
+
### Core Components
|
|
412
|
+
|
|
413
|
+
#### 1. Stack Store
|
|
414
|
+
|
|
415
|
+
The store manages the modal stack state using a simple array:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
type StackItem = {
|
|
419
|
+
key: string;
|
|
420
|
+
props: unknown;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export const createStackStore = () => {
|
|
424
|
+
let stack: StackItem[] = [];
|
|
425
|
+
const listeners = new Set<() => void>();
|
|
426
|
+
return {
|
|
427
|
+
push(item: StackItem) {
|
|
428
|
+
stack = [...stack, item];
|
|
429
|
+
listeners.forEach((l) => l());
|
|
430
|
+
},
|
|
431
|
+
pop() {
|
|
432
|
+
stack = stack.slice(0, -1);
|
|
433
|
+
listeners.forEach((l) => l());
|
|
434
|
+
},
|
|
435
|
+
get() {
|
|
436
|
+
return stack;
|
|
437
|
+
},
|
|
438
|
+
subscribe(fn: () => void) {
|
|
439
|
+
listeners.add(fn);
|
|
440
|
+
return () => listeners.delete(fn);
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### 2. Modal Renderer
|
|
447
|
+
|
|
448
|
+
The `ModalRender` component subscribes to the store and renders the modal stack:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
type ModalRegistry = {
|
|
452
|
+
[key: string]: (props?: unknown) => React.ReactNode;
|
|
453
|
+
};
|
|
454
|
+
type ModalLayout = {
|
|
455
|
+
Background: React.ComponentType<{
|
|
456
|
+
children: React.ReactNode;
|
|
457
|
+
onClose: () => void;
|
|
458
|
+
}>;
|
|
459
|
+
ModalWrap: React.ComponentType<{
|
|
460
|
+
children: React.ReactNode;
|
|
461
|
+
depth: number;
|
|
462
|
+
isTop: boolean;
|
|
463
|
+
}>;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
function ModalRender<M extends ModalRegistry>({
|
|
467
|
+
modals,
|
|
468
|
+
modalLayout,
|
|
469
|
+
store,
|
|
470
|
+
}: {
|
|
471
|
+
modals: M;
|
|
472
|
+
modalLayout: ModalLayout;
|
|
473
|
+
store: StackStore;
|
|
474
|
+
}) {
|
|
475
|
+
// Subscribe to store changes using React 18's useSyncExternalStore
|
|
476
|
+
const stack = React.useSyncExternalStore(
|
|
477
|
+
store.subscribe, // Subscribe function
|
|
478
|
+
store.get, // Get snapshot (client)
|
|
479
|
+
store.get // Get snapshot (server)
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
if (stack.length === 0) return null;
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<modalLayout.Background onClose={() => store.pop()}>
|
|
486
|
+
{stack.map((item, index) => {
|
|
487
|
+
// Calculate depth: top modal = 0, second = 1, etc.
|
|
488
|
+
const depth = stack.length - 1 - index;
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<modalLayout.ModalWrap
|
|
492
|
+
depth={depth}
|
|
493
|
+
isTop={index === stack.length - 1}
|
|
494
|
+
key={index}
|
|
495
|
+
>
|
|
496
|
+
{/* Dynamically render the modal component */}
|
|
497
|
+
{modals[item.key](item.props)}
|
|
498
|
+
</modalLayout.ModalWrap>
|
|
499
|
+
);
|
|
500
|
+
})}
|
|
501
|
+
</modalLayout.Background>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Key features:**
|
|
507
|
+
|
|
508
|
+
- Uses `useSyncExternalStore` for optimal performance and React 18 compatibility
|
|
509
|
+
- Calculates `depth` for each modal (0 = top, higher = deeper in stack)
|
|
510
|
+
- Identifies the `isTop` modal for interaction control
|
|
511
|
+
- Dynamically renders modal components based on the stack state
|
|
512
|
+
|
|
513
|
+
#### 3. Modal Creation Flow
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// 1. Define modals
|
|
517
|
+
const modals = {
|
|
518
|
+
alert: (props) => <AlertComponent {...props} />,
|
|
519
|
+
confirm: (props) => <ConfirmComponent {...props} />,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// 2. Create modal instance
|
|
523
|
+
const modal = createModalStack(modals, preset);
|
|
524
|
+
|
|
525
|
+
// This creates:
|
|
526
|
+
// - A store to manage modal stack
|
|
527
|
+
// - A root DOM element to render modals
|
|
528
|
+
// - Push methods for each modal type
|
|
529
|
+
// - A pop method to close modals
|
|
530
|
+
|
|
531
|
+
// 3. Use modals
|
|
532
|
+
modal.alert.push({ message: "Hello" });
|
|
533
|
+
// -> Adds { key: "alert", props: { message: "Hello" } } to stack
|
|
534
|
+
// -> Triggers re-render
|
|
535
|
+
// -> ModalRender displays the modal
|
|
536
|
+
|
|
537
|
+
modal.pop();
|
|
538
|
+
// -> Removes top item from stack
|
|
539
|
+
// -> Triggers re-render
|
|
540
|
+
// -> Modal disappears
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Rendering Process
|
|
544
|
+
|
|
545
|
+
1. **Initial State**: Stack is empty, nothing renders
|
|
546
|
+
2. **Push Modal**:
|
|
547
|
+
- Modal data added to stack
|
|
548
|
+
- Listeners notified
|
|
549
|
+
- `ModalRender` re-renders with new stack
|
|
550
|
+
- `Background` and `ModalWrap` components render the modal
|
|
551
|
+
3. **Stack Multiple Modals**:
|
|
552
|
+
- Each modal gets a `depth` value
|
|
553
|
+
- Presets use `depth` to position/style modals
|
|
554
|
+
- Only top modal (`isTop={true}`) receives pointer events
|
|
555
|
+
4. **Pop Modal**:
|
|
556
|
+
- Top item removed from stack
|
|
557
|
+
- Component re-renders
|
|
558
|
+
- If stack is empty, returns `null`
|
|
559
|
+
|
|
560
|
+
### Why useSyncExternalStore?
|
|
561
|
+
|
|
562
|
+
`useSyncExternalStore` is a React 18 hook that safely subscribes to external stores:
|
|
563
|
+
|
|
564
|
+
- **Tearing prevention**: Ensures consistent state across concurrent renders
|
|
565
|
+
- **SSR support**: Separate snapshots for server and client
|
|
566
|
+
- **Performance**: Only re-renders when subscribed state changes
|
|
567
|
+
|
|
568
|
+
## Troubleshooting
|
|
569
|
+
|
|
570
|
+
### Modals not appearing?
|
|
571
|
+
|
|
572
|
+
Make sure `createModalStack` is called at the module level, not inside a component:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
// â
Correct
|
|
576
|
+
const modal = createModalStack(modals, preset);
|
|
577
|
+
|
|
578
|
+
function App() {
|
|
579
|
+
return <button onClick={() => modal.alert.push()}>Open</button>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// â Wrong - creates new instance on every render
|
|
583
|
+
function App() {
|
|
584
|
+
const modal = createModalStack(modals, preset);
|
|
585
|
+
return <button onClick={() => modal.alert.push()}>Open</button>;
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## License
|
|
590
|
+
|
|
591
|
+
MIT
|
|
592
|
+
|
|
593
|
+
## Contributing
|
|
594
|
+
|
|
595
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createStackModal: () => createStackModal
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/ensureRoot.ts
|
|
38
|
+
var import_client = require("react-dom/client");
|
|
39
|
+
function ensureRoot(render) {
|
|
40
|
+
const container = document.createElement("div");
|
|
41
|
+
container.setAttribute("data-modal-root", "true");
|
|
42
|
+
document.body.appendChild(container);
|
|
43
|
+
const root = (0, import_client.createRoot)(container);
|
|
44
|
+
root.render(render());
|
|
45
|
+
return {
|
|
46
|
+
destroy: () => {
|
|
47
|
+
root.unmount();
|
|
48
|
+
container.remove();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/ModalRender.tsx
|
|
54
|
+
var import_react = __toESM(require("react"), 1);
|
|
55
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
56
|
+
function ModalRender({
|
|
57
|
+
modals,
|
|
58
|
+
modalLayout,
|
|
59
|
+
store
|
|
60
|
+
}) {
|
|
61
|
+
const stack = import_react.default.useSyncExternalStore(
|
|
62
|
+
store.subscribe,
|
|
63
|
+
store.get,
|
|
64
|
+
store.get
|
|
65
|
+
);
|
|
66
|
+
if (stack.length === 0) return null;
|
|
67
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(modalLayout.Background, { onClose: () => store.pop(), children: stack.map((item, index) => {
|
|
68
|
+
const depth = stack.length - 1 - index;
|
|
69
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
70
|
+
modalLayout.ModalWrap,
|
|
71
|
+
{
|
|
72
|
+
depth,
|
|
73
|
+
isTop: index === stack.length - 1,
|
|
74
|
+
children: modals[item.key](item.props)
|
|
75
|
+
},
|
|
76
|
+
index
|
|
77
|
+
);
|
|
78
|
+
}) });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/createStackStore.ts
|
|
82
|
+
var createStackStore = () => {
|
|
83
|
+
let stack = [];
|
|
84
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
85
|
+
return {
|
|
86
|
+
push(item) {
|
|
87
|
+
stack = [...stack, item];
|
|
88
|
+
listeners.forEach((l) => l());
|
|
89
|
+
},
|
|
90
|
+
pop() {
|
|
91
|
+
stack = stack.slice(0, -1);
|
|
92
|
+
listeners.forEach((l) => l());
|
|
93
|
+
},
|
|
94
|
+
get() {
|
|
95
|
+
return stack;
|
|
96
|
+
},
|
|
97
|
+
subscribe(fn) {
|
|
98
|
+
listeners.add(fn);
|
|
99
|
+
return () => listeners.delete(fn);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/createStackModal.tsx
|
|
105
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
106
|
+
function createStackModal(modals, modalLayout) {
|
|
107
|
+
const store = createStackStore();
|
|
108
|
+
ensureRoot(() => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ModalRender, { modals, modalLayout, store }));
|
|
109
|
+
const modal = {};
|
|
110
|
+
Object.keys(modals).forEach((key) => {
|
|
111
|
+
modal[key] = {
|
|
112
|
+
push: (props) => store.push({ key, props })
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return { ...modal, pop: () => store.pop() };
|
|
116
|
+
}
|
|
117
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
118
|
+
0 && (module.exports = {
|
|
119
|
+
createStackModal
|
|
120
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type ModalRegistry = {
|
|
2
|
+
[key: string]: (props?: unknown) => React.ReactNode;
|
|
3
|
+
};
|
|
4
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
5
|
+
type PushFn<F extends AnyFn> = Parameters<F> extends [] ? () => void : (props?: Parameters<F>[0]) => void;
|
|
6
|
+
type ModalLayout = {
|
|
7
|
+
Background: React.ComponentType<{
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}>;
|
|
11
|
+
ModalWrap: React.ComponentType<{
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
depth: number;
|
|
14
|
+
isTop: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function createStackModal<M extends ModalRegistry = ModalRegistry>(modals: M, modalLayout: ModalLayout): { [K in Extract<keyof M, string>]: {
|
|
19
|
+
push: PushFn<M[K]>;
|
|
20
|
+
}; } & {
|
|
21
|
+
pop: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { type ModalLayout, createStackModal };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type ModalRegistry = {
|
|
2
|
+
[key: string]: (props?: unknown) => React.ReactNode;
|
|
3
|
+
};
|
|
4
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
5
|
+
type PushFn<F extends AnyFn> = Parameters<F> extends [] ? () => void : (props?: Parameters<F>[0]) => void;
|
|
6
|
+
type ModalLayout = {
|
|
7
|
+
Background: React.ComponentType<{
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}>;
|
|
11
|
+
ModalWrap: React.ComponentType<{
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
depth: number;
|
|
14
|
+
isTop: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function createModalStack<M extends ModalRegistry = ModalRegistry>(modals: M, modalLayout: ModalLayout): { [K in Extract<keyof M, string>]: {
|
|
19
|
+
push: PushFn<M[K]>;
|
|
20
|
+
}; } & {
|
|
21
|
+
pop: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { type ModalLayout, createModalStack };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type ModalRegistry = {
|
|
2
|
+
[key: string]: (props?: unknown) => React.ReactNode;
|
|
3
|
+
};
|
|
4
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
5
|
+
type PushFn<F extends AnyFn> = Parameters<F> extends [] ? () => void : (props?: Parameters<F>[0]) => void;
|
|
6
|
+
type ModalLayout = {
|
|
7
|
+
Background: React.ComponentType<{
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}>;
|
|
11
|
+
ModalWrap: React.ComponentType<{
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
depth: number;
|
|
14
|
+
isTop: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function createStackModal<M extends ModalRegistry = ModalRegistry>(modals: M, modalLayout: ModalLayout): { [K in Extract<keyof M, string>]: {
|
|
19
|
+
push: PushFn<M[K]>;
|
|
20
|
+
}; } & {
|
|
21
|
+
pop: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { type ModalLayout, createStackModal };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/ensureRoot.ts
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
function ensureRoot(render) {
|
|
4
|
+
const container = document.createElement("div");
|
|
5
|
+
container.setAttribute("data-modal-root", "true");
|
|
6
|
+
document.body.appendChild(container);
|
|
7
|
+
const root = createRoot(container);
|
|
8
|
+
root.render(render());
|
|
9
|
+
return {
|
|
10
|
+
destroy: () => {
|
|
11
|
+
root.unmount();
|
|
12
|
+
container.remove();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/ModalRender.tsx
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { jsx } from "react/jsx-runtime";
|
|
20
|
+
function ModalRender({
|
|
21
|
+
modals,
|
|
22
|
+
modalLayout,
|
|
23
|
+
store
|
|
24
|
+
}) {
|
|
25
|
+
const stack = React.useSyncExternalStore(
|
|
26
|
+
store.subscribe,
|
|
27
|
+
store.get,
|
|
28
|
+
store.get
|
|
29
|
+
);
|
|
30
|
+
if (stack.length === 0) return null;
|
|
31
|
+
return /* @__PURE__ */ jsx(modalLayout.Background, { onClose: () => store.pop(), children: stack.map((item, index) => {
|
|
32
|
+
const depth = stack.length - 1 - index;
|
|
33
|
+
return /* @__PURE__ */ jsx(
|
|
34
|
+
modalLayout.ModalWrap,
|
|
35
|
+
{
|
|
36
|
+
depth,
|
|
37
|
+
isTop: index === stack.length - 1,
|
|
38
|
+
children: modals[item.key](item.props)
|
|
39
|
+
},
|
|
40
|
+
index
|
|
41
|
+
);
|
|
42
|
+
}) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/createStackStore.ts
|
|
46
|
+
var createStackStore = () => {
|
|
47
|
+
let stack = [];
|
|
48
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
49
|
+
return {
|
|
50
|
+
push(item) {
|
|
51
|
+
stack = [...stack, item];
|
|
52
|
+
listeners.forEach((l) => l());
|
|
53
|
+
},
|
|
54
|
+
pop() {
|
|
55
|
+
stack = stack.slice(0, -1);
|
|
56
|
+
listeners.forEach((l) => l());
|
|
57
|
+
},
|
|
58
|
+
get() {
|
|
59
|
+
return stack;
|
|
60
|
+
},
|
|
61
|
+
subscribe(fn) {
|
|
62
|
+
listeners.add(fn);
|
|
63
|
+
return () => listeners.delete(fn);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/createStackModal.tsx
|
|
69
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
70
|
+
function createStackModal(modals, modalLayout) {
|
|
71
|
+
const store = createStackStore();
|
|
72
|
+
ensureRoot(() => /* @__PURE__ */ jsx2(ModalRender, { modals, modalLayout, store }));
|
|
73
|
+
const modal = {};
|
|
74
|
+
Object.keys(modals).forEach((key) => {
|
|
75
|
+
modal[key] = {
|
|
76
|
+
push: (props) => store.push({ key, props })
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return { ...modal, pop: () => store.pop() };
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
createStackModal
|
|
83
|
+
};
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/ensureRoot.ts
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
function ensureRoot(render) {
|
|
4
|
+
const container = document.createElement("div");
|
|
5
|
+
container.setAttribute("data-modal-root", "true");
|
|
6
|
+
document.body.appendChild(container);
|
|
7
|
+
const root = createRoot(container);
|
|
8
|
+
root.render(render());
|
|
9
|
+
return {
|
|
10
|
+
destroy: () => {
|
|
11
|
+
root.unmount();
|
|
12
|
+
container.remove();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/ModalRender.tsx
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { jsx } from "react/jsx-runtime";
|
|
20
|
+
function ModalRender({
|
|
21
|
+
modals,
|
|
22
|
+
modalLayout,
|
|
23
|
+
store
|
|
24
|
+
}) {
|
|
25
|
+
const stack = React.useSyncExternalStore(
|
|
26
|
+
store.subscribe,
|
|
27
|
+
store.get,
|
|
28
|
+
store.get
|
|
29
|
+
);
|
|
30
|
+
if (stack.length === 0) return null;
|
|
31
|
+
return /* @__PURE__ */ jsx(modalLayout.Background, { onClose: () => store.pop(), children: stack.map((item, index) => {
|
|
32
|
+
const depth = stack.length - 1 - index;
|
|
33
|
+
return /* @__PURE__ */ jsx(
|
|
34
|
+
modalLayout.ModalWrap,
|
|
35
|
+
{
|
|
36
|
+
depth,
|
|
37
|
+
isTop: index === stack.length - 1,
|
|
38
|
+
children: modals[item.key](item.props)
|
|
39
|
+
},
|
|
40
|
+
index
|
|
41
|
+
);
|
|
42
|
+
}) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/createStackStore.ts
|
|
46
|
+
var createStackStore = () => {
|
|
47
|
+
let stack = [];
|
|
48
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
49
|
+
return {
|
|
50
|
+
push(item) {
|
|
51
|
+
stack = [...stack, item];
|
|
52
|
+
listeners.forEach((l) => l());
|
|
53
|
+
},
|
|
54
|
+
pop() {
|
|
55
|
+
stack = stack.slice(0, -1);
|
|
56
|
+
listeners.forEach((l) => l());
|
|
57
|
+
},
|
|
58
|
+
get() {
|
|
59
|
+
return stack;
|
|
60
|
+
},
|
|
61
|
+
subscribe(fn) {
|
|
62
|
+
listeners.add(fn);
|
|
63
|
+
return () => listeners.delete(fn);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/createModalStack.tsx
|
|
69
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
70
|
+
function createModalStack(modals, modalLayout) {
|
|
71
|
+
const store = createStackStore();
|
|
72
|
+
ensureRoot(() => /* @__PURE__ */ jsx2(ModalRender, { modals, modalLayout, store }));
|
|
73
|
+
const modal = {};
|
|
74
|
+
Object.keys(modals).forEach((key) => {
|
|
75
|
+
modal[key] = {
|
|
76
|
+
push: (props) => store.push({ key, props })
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return { ...modal, pop: () => store.pop() };
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
createModalStack
|
|
83
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ivex0002/stack-modal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A flexible and customizable modal stack management library for React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"stack-modal",
|
|
22
|
+
"stack-modal-presets",
|
|
23
|
+
"react",
|
|
24
|
+
"modal",
|
|
25
|
+
"stack",
|
|
26
|
+
"dialog",
|
|
27
|
+
"popup",
|
|
28
|
+
"typescript",
|
|
29
|
+
"ui"
|
|
30
|
+
],
|
|
31
|
+
"author": "ivex0002",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/Ivex0002/stack-modal"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Ivex0002/stack-modal/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Ivex0002/stack-modal#readme",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=18.0.0",
|
|
46
|
+
"react-dom": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "^18.0.0",
|
|
50
|
+
"@types/react-dom": "^18.0.0",
|
|
51
|
+
"react": "^18.0.0",
|
|
52
|
+
"react-dom": "^18.0.0",
|
|
53
|
+
"tsup": "^8.0.0",
|
|
54
|
+
"typescript": "^5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
58
|
+
"pack:test": "npm run build && npm pack"
|
|
59
|
+
}
|
|
60
|
+
}
|