@lerx/promise-modal 0.10.4 → 0.11.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.
@@ -0,0 +1,1185 @@
1
+ # @lerx/promise-modal Specification
2
+
3
+ > Universal React Modal Utility with Promise-based API
4
+
5
+ ## Overview
6
+
7
+ `@lerx/promise-modal` is a React-based universal modal utility that provides:
8
+
9
+ - **Promise-based interactions**: Alert, confirm, and prompt modals that return promises
10
+ - **Universal usage**: Can be used both inside and outside React components
11
+ - **High customizability**: Every component can be customized
12
+ - **Automatic lifecycle management**: Handles mount/unmount and animations
13
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ 1. [Installation](#installation)
19
+ 2. [Quick Start](#quick-start)
20
+ 3. [Architecture](#architecture)
21
+ 4. [Core API](#core-api)
22
+ - [alert](#alert)
23
+ - [confirm](#confirm)
24
+ - [prompt](#prompt)
25
+ 5. [Hooks](#hooks)
26
+ - [useModal](#usemodal)
27
+ - [useActiveModalCount](#useactivemodalcount)
28
+ - [useModalAnimation](#usemodalanimation)
29
+ - [useModalDuration](#usemodalduration)
30
+ - [useDestroyAfter](#usedestroyafter)
31
+ - [useSubscribeModal](#usesubscribemodal)
32
+ - [useInitializeModal](#useinitializemodal)
33
+ - [useModalOptions](#usemodaloptions)
34
+ - [useModalBackdrop](#usemodalbackdrop)
35
+ 6. [Components](#components)
36
+ - [ModalProvider](#modalprovider)
37
+ - [Custom Components](#custom-components)
38
+ 7. [Type Definitions](#type-definitions)
39
+ 8. [Usage Patterns](#usage-patterns)
40
+ 9. [Advanced Examples](#advanced-examples)
41
+ 10. [AbortSignal Support](#abortsignal-support)
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ # Using yarn
49
+ yarn add @lerx/promise-modal
50
+
51
+ # Using npm
52
+ npm install @lerx/promise-modal
53
+ ```
54
+
55
+ ### Peer Dependencies
56
+
57
+ - React 18-19
58
+ - React DOM 18-19
59
+
60
+ ### Compatibility
61
+
62
+ - Node.js 16.11.0 or later
63
+ - Modern browsers (Chrome 94+, Firefox 93+, Safari 15+)
64
+
65
+ ---
66
+
67
+ ## Quick Start
68
+
69
+ ### 1. Setup Provider
70
+
71
+ ```tsx
72
+ import { ModalProvider } from '@lerx/promise-modal';
73
+
74
+ function App() {
75
+ return (
76
+ <ModalProvider>
77
+ <YourApp />
78
+ </ModalProvider>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### 2. Use Modal Functions
84
+
85
+ ```tsx
86
+ import { alert, confirm, prompt } from '@lerx/promise-modal';
87
+
88
+ // Alert
89
+ await alert({
90
+ title: 'Notice',
91
+ content: 'Operation completed.',
92
+ });
93
+
94
+ // Confirm
95
+ const result = await confirm({
96
+ title: 'Confirm',
97
+ content: 'Are you sure?',
98
+ });
99
+
100
+ // Prompt
101
+ const name = await prompt<string>({
102
+ title: 'Enter Name',
103
+ defaultValue: '',
104
+ Input: ({ value, onChange }) => (
105
+ <input value={value} onChange={(e) => onChange(e.target.value)} />
106
+ ),
107
+ });
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Architecture
113
+
114
+ ### Layer Structure
115
+
116
+ ```
117
+ ┌─────────────────────────────────────────────────────────────┐
118
+ │ Your Application │
119
+ ├─────────────────────────────────────────────────────────────┤
120
+ │ Core API Layer │
121
+ │ ├── alert() │
122
+ │ ├── confirm() │
123
+ │ └── prompt() │
124
+ ├─────────────────────────────────────────────────────────────┤
125
+ │ Application Layer │
126
+ │ └── ModalManager (Singleton) │
127
+ │ ├── DOM anchoring │
128
+ │ ├── Style injection │
129
+ │ └── Modal lifecycle │
130
+ ├─────────────────────────────────────────────────────────────┤
131
+ │ Bootstrap Layer │
132
+ │ └── ModalProvider │
133
+ │ ├── Initialization │
134
+ │ └── Component setup │
135
+ ├─────────────────────────────────────────────────────────────┤
136
+ │ Provider Layer │
137
+ │ ├── ModalManagerContext │
138
+ │ ├── ConfigurationContext │
139
+ │ └── UserDefinedContext │
140
+ ├─────────────────────────────────────────────────────────────┤
141
+ │ Component Layer │
142
+ │ ├── Anchor │
143
+ │ ├── Background │
144
+ │ ├── Foreground │
145
+ │ └── Fallback Components │
146
+ └─────────────────────────────────────────────────────────────┘
147
+ ```
148
+
149
+ ### Design Patterns
150
+
151
+ | Pattern | Usage |
152
+ |---------|-------|
153
+ | **Promise-based API** | Modal functions return promises |
154
+ | **Provider Pattern** | Context providers for configuration |
155
+ | **Factory Pattern** | Node factory for modal types |
156
+ | **Observer Pattern** | Subscription system for state |
157
+ | **Singleton Pattern** | ModalManager for global state |
158
+
159
+ ### Modal Node System
160
+
161
+ ```
162
+ AbstractNode (Base Class)
163
+ ├── AlertNode → Simple notifications
164
+ ├── ConfirmNode → Yes/no confirmations
165
+ └── PromptNode → Input collection
166
+ ```
167
+
168
+ Each node provides:
169
+ - Subscription-based state management
170
+ - Promise resolution handling
171
+ - Lifecycle management
172
+
173
+ ### Configuration Priority
174
+
175
+ Configuration is applied hierarchically, with lower levels overriding higher levels:
176
+
177
+ ```
178
+ Provider Settings (lowest) < Hook Settings < Handler Settings (highest)
179
+ ```
180
+
181
+ | Level | Location | Description |
182
+ |-------|----------|-------------|
183
+ | **Provider** | `ModalProvider` props | App-wide default settings |
184
+ | **Hook** | `useModal(config)` | Component-level settings |
185
+ | **Handler** | `alert/confirm/prompt(options)` | Per-modal settings |
186
+
187
+ #### Example
188
+
189
+ ```typescript
190
+ // Provider level: global defaults
191
+ <ModalProvider options={{ duration: '500ms', closeOnBackdropClick: true }}>
192
+ <App />
193
+ </ModalProvider>
194
+
195
+ // Hook level: component defaults (overrides Provider settings)
196
+ const modal = useModal({
197
+ ForegroundComponent: CustomForeground,
198
+ });
199
+
200
+ // Handler level: individual modal (overrides Hook settings)
201
+ modal.alert({
202
+ title: 'Notice',
203
+ duration: 200, // 500ms → 200ms override
204
+ ForegroundComponent: SpecialForeground, // CustomForeground override
205
+ });
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Core API
211
+
212
+ ### alert
213
+
214
+ Opens a simple notification modal.
215
+
216
+ #### Signature
217
+
218
+ ```typescript
219
+ function alert<B = any>(options: AlertProps<B>): Promise<void>;
220
+ ```
221
+
222
+ #### Parameters
223
+
224
+ | Option | Type | Default | Description |
225
+ |--------|------|---------|-------------|
226
+ | `title` | `ReactNode` | - | Modal title |
227
+ | `subtitle` | `ReactNode` | - | Subtitle below title |
228
+ | `content` | `ReactNode \| ComponentType<AlertContentProps>` | - | Modal content |
229
+ | `subtype` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | Modal type |
230
+ | `dimmed` | `boolean` | `true` | Dim background |
231
+ | `closeOnBackdropClick` | `boolean` | `true` | Close on backdrop |
232
+ | `manualDestroy` | `boolean` | `false` | Manual destroy mode |
233
+ | `duration` | `number \| string` | - | Animation duration (Handler level override) |
234
+ | `background` | `ModalBackground<B>` | - | Background settings |
235
+ | `footer` | `AlertFooterRender \| FooterOptions \| false` | - | Footer config |
236
+ | `ForegroundComponent` | `ComponentType<ModalFrameProps>` | - | Custom foreground |
237
+ | `BackgroundComponent` | `ComponentType<ModalFrameProps>` | - | Custom background |
238
+ | `signal` | `AbortSignal` | - | AbortSignal for canceling modal |
239
+
240
+ #### Example
241
+
242
+ ```typescript
243
+ await alert({
244
+ title: 'Success',
245
+ content: 'Your changes have been saved.',
246
+ subtype: 'success',
247
+ footer: { confirm: 'OK' },
248
+ });
249
+ ```
250
+
251
+ ---
252
+
253
+ ### confirm
254
+
255
+ Opens a confirmation modal for user decisions.
256
+
257
+ #### Signature
258
+
259
+ ```typescript
260
+ function confirm<B = any>(options: ConfirmProps<B>): Promise<boolean>;
261
+ ```
262
+
263
+ #### Parameters
264
+
265
+ All options from `alert`, plus:
266
+
267
+ | Option | Type | Default | Description |
268
+ |--------|------|---------|-------------|
269
+ | `footer` | `ConfirmFooterRender \| FooterOptions \| false` | - | Footer config |
270
+
271
+ #### FooterOptions for confirm
272
+
273
+ ```typescript
274
+ interface FooterOptions {
275
+ confirm?: string;
276
+ cancel?: string;
277
+ hideConfirm?: boolean;
278
+ hideCancel?: boolean;
279
+ }
280
+ ```
281
+
282
+ #### Returns
283
+
284
+ - `true` - User clicked confirm button
285
+ - `false` - User clicked cancel or backdrop
286
+
287
+ #### Example
288
+
289
+ ```typescript
290
+ const shouldDelete = await confirm({
291
+ title: 'Delete Item',
292
+ content: 'This action cannot be undone.',
293
+ subtype: 'warning',
294
+ footer: {
295
+ confirm: 'Delete',
296
+ cancel: 'Keep',
297
+ },
298
+ });
299
+
300
+ if (shouldDelete) {
301
+ await deleteItem();
302
+ }
303
+ ```
304
+
305
+ ---
306
+
307
+ ### prompt
308
+
309
+ Opens a prompt modal to collect user input.
310
+
311
+ #### Signature
312
+
313
+ ```typescript
314
+ function prompt<T, B = any>(options: PromptProps<T, B>): Promise<T>;
315
+ ```
316
+
317
+ #### Parameters
318
+
319
+ All options from `alert`, plus:
320
+
321
+ | Option | Type | Required | Description |
322
+ |--------|------|----------|-------------|
323
+ | `Input` | `(props: PromptInputProps<T>) => ReactNode` | Yes | Input component |
324
+ | `defaultValue` | `T` | No | Default value |
325
+ | `disabled` | `(value: T) => boolean` | No | Disable confirm |
326
+ | `returnOnCancel` | `boolean` | No | Return default on cancel |
327
+
328
+ #### PromptInputProps
329
+
330
+ ```typescript
331
+ interface PromptInputProps<T> {
332
+ value?: T;
333
+ defaultValue?: T;
334
+ onChange: (value: T | undefined) => void;
335
+ onConfirm: () => void;
336
+ onCancel: () => void;
337
+ context: any;
338
+ }
339
+ ```
340
+
341
+ #### Example
342
+
343
+ ```typescript
344
+ // Simple input
345
+ const email = await prompt<string>({
346
+ title: 'Enter Email',
347
+ defaultValue: '',
348
+ Input: ({ value, onChange }) => (
349
+ <input
350
+ type="email"
351
+ value={value}
352
+ onChange={(e) => onChange(e.target.value)}
353
+ />
354
+ ),
355
+ disabled: (value) => !value?.includes('@'),
356
+ });
357
+
358
+ // Complex object
359
+ interface UserData {
360
+ name: string;
361
+ age: number;
362
+ }
363
+
364
+ const userData = await prompt<UserData>({
365
+ title: 'User Info',
366
+ defaultValue: { name: '', age: 0 },
367
+ Input: ({ value, onChange }) => (
368
+ <form>
369
+ <input
370
+ value={value.name}
371
+ onChange={(e) => onChange({ ...value, name: e.target.value })}
372
+ />
373
+ <input
374
+ type="number"
375
+ value={value.age}
376
+ onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
377
+ />
378
+ </form>
379
+ ),
380
+ });
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Hooks
386
+
387
+ ### useModal
388
+
389
+ Returns modal handlers tied to component lifecycle.
390
+
391
+ ```typescript
392
+ function useModal(config?: Partial<ModalOptions>): {
393
+ alert: typeof alert;
394
+ confirm: typeof confirm;
395
+ prompt: typeof prompt;
396
+ };
397
+ ```
398
+
399
+ #### Key Feature
400
+
401
+ Modals automatically cleanup when the component unmounts.
402
+
403
+ #### Comparison
404
+
405
+ | Feature | Static Functions | useModal Hook |
406
+ |---------|-----------------|---------------|
407
+ | Lifecycle | Independent | Tied to component |
408
+ | Cleanup | Manual | Automatic |
409
+ | Usage | Anywhere | Inside components |
410
+
411
+ #### Example
412
+
413
+ ```typescript
414
+ function DeleteButton({ id }) {
415
+ const modal = useModal();
416
+
417
+ const handleDelete = async () => {
418
+ if (await modal.confirm({ content: 'Delete?' })) {
419
+ await deleteItem(id);
420
+ }
421
+ };
422
+
423
+ return <button onClick={handleDelete}>Delete</button>;
424
+ }
425
+ ```
426
+
427
+ ---
428
+
429
+ ### useActiveModalCount
430
+
431
+ Returns the count of active modals.
432
+
433
+ ```typescript
434
+ function useActiveModalCount(
435
+ validate?: (modal?: ModalNode) => boolean,
436
+ refreshKey?: string | number
437
+ ): number;
438
+ ```
439
+
440
+ #### Parameters
441
+
442
+ | Parameter | Type | Description |
443
+ |-----------|------|-------------|
444
+ | `validate` | `(modal) => boolean` | Filter function |
445
+ | `refreshKey` | `string \| number` | Force refresh key |
446
+
447
+ #### Example
448
+
449
+ ```typescript
450
+ function ModalCounter() {
451
+ const total = useActiveModalCount();
452
+ const alerts = useActiveModalCount((m) => m?.type === 'alert');
453
+
454
+ return <div>Total: {total}, Alerts: {alerts}</div>;
455
+ }
456
+ ```
457
+
458
+ ---
459
+
460
+ ### useModalAnimation
461
+
462
+ Provides animation state callbacks.
463
+
464
+ ```typescript
465
+ function useModalAnimation(
466
+ visible: boolean,
467
+ options: {
468
+ onVisible?: () => void;
469
+ onHidden?: () => void;
470
+ }
471
+ ): void;
472
+ ```
473
+
474
+ #### Features
475
+
476
+ - Uses `requestAnimationFrame` for optimal timing
477
+ - Separates enter/exit animations
478
+ - Works with CSS transitions
479
+
480
+ #### Example
481
+
482
+ ```typescript
483
+ function AnimatedModal({ visible, children }) {
484
+ const ref = useRef<HTMLDivElement>(null);
485
+
486
+ useModalAnimation(visible, {
487
+ onVisible: () => {
488
+ ref.current?.classList.add('fade-in');
489
+ },
490
+ onHidden: () => {
491
+ ref.current?.classList.remove('fade-in');
492
+ },
493
+ });
494
+
495
+ return <div ref={ref}>{children}</div>;
496
+ }
497
+ ```
498
+
499
+ ---
500
+
501
+ ### useModalDuration
502
+
503
+ Returns modal animation duration.
504
+
505
+ ```typescript
506
+ function useModalDuration(modalId?: number): {
507
+ duration: string; // e.g., '300ms'
508
+ milliseconds: number; // e.g., 300
509
+ };
510
+ ```
511
+
512
+ ---
513
+
514
+ ### useDestroyAfter
515
+
516
+ Auto-destroys modal after specified time.
517
+
518
+ ```typescript
519
+ function useDestroyAfter(
520
+ modalId: number,
521
+ duration: string | number
522
+ ): void;
523
+ ```
524
+
525
+ #### Behavior
526
+
527
+ - Starts timer when modal becomes hidden
528
+ - Cancels timer if modal becomes visible again
529
+ - Removes modal from DOM after duration
530
+
531
+ #### Example
532
+
533
+ ```typescript
534
+ function ToastMessage({ id }) {
535
+ useDestroyAfter(id, 300); // Destroy 300ms after hidden
536
+ return <div>Toast</div>;
537
+ }
538
+ ```
539
+
540
+ ---
541
+
542
+ ### useSubscribeModal
543
+
544
+ Subscribes to modal state changes.
545
+
546
+ ```typescript
547
+ function useSubscribeModal(modal?: ModalNode): number;
548
+ ```
549
+
550
+ #### Returns
551
+
552
+ Version number that increments on each state change.
553
+
554
+ #### Example
555
+
556
+ ```typescript
557
+ function ModalDebugger({ modal }) {
558
+ const version = useSubscribeModal(modal);
559
+
560
+ useEffect(() => {
561
+ console.log('State changed:', modal?.visible);
562
+ }, [version]);
563
+ }
564
+ ```
565
+
566
+ ---
567
+
568
+ ### useInitializeModal
569
+
570
+ Manually initializes modal service.
571
+
572
+ ```typescript
573
+ function useInitializeModal(options?: {
574
+ mode?: 'auto' | 'manual';
575
+ }): {
576
+ initialize: (anchor?: HTMLElement) => void;
577
+ portal: ReactPortal | null;
578
+ };
579
+ ```
580
+
581
+ #### Modes
582
+
583
+ | Mode | Description |
584
+ |------|-------------|
585
+ | `auto` | Initializes automatically |
586
+ | `manual` | Requires calling `initialize()` |
587
+
588
+ ---
589
+
590
+ ### useModalOptions
591
+
592
+ Returns modal options configuration.
593
+
594
+ ```typescript
595
+ function useModalOptions(): ModalOptions;
596
+ ```
597
+
598
+ #### Returns
599
+
600
+ ```typescript
601
+ interface ModalOptions {
602
+ duration?: number | string; // Animation duration
603
+ backdrop?: string; // Backdrop overlay color
604
+ manualDestroy?: boolean; // Manual destroy mode
605
+ closeOnBackdropClick?: boolean; // Close on backdrop click
606
+ zIndex?: number; // CSS z-index
607
+ }
608
+ ```
609
+
610
+ #### Example
611
+
612
+ ```typescript
613
+ function ModalDebugInfo() {
614
+ const options = useModalOptions();
615
+
616
+ return (
617
+ <div>
618
+ <p>Duration: {options.duration}</p>
619
+ <p>Backdrop: {options.backdrop}</p>
620
+ </div>
621
+ );
622
+ }
623
+ ```
624
+
625
+ ---
626
+
627
+ ### useModalBackdrop
628
+
629
+ Returns only modal backdrop configuration.
630
+
631
+ ```typescript
632
+ function useModalBackdrop(): string | CSSProperties;
633
+ ```
634
+
635
+ #### Example
636
+
637
+ ```typescript
638
+ function BackdropInfo() {
639
+ const backdrop = useModalBackdrop();
640
+
641
+ return <p>Current backdrop: {backdrop}</p>;
642
+ }
643
+ ```
644
+
645
+ ---
646
+
647
+ ## Components
648
+
649
+ ### ModalProvider
650
+
651
+ Main provider component for initialization.
652
+
653
+ ```typescript
654
+ interface ModalProviderProps {
655
+ children: ReactNode;
656
+ ForegroundComponent?: ComponentType<ModalFrameProps>;
657
+ BackgroundComponent?: ComponentType<ModalFrameProps>;
658
+ TitleComponent?: ComponentType<WrapperComponentProps>;
659
+ SubtitleComponent?: ComponentType<WrapperComponentProps>;
660
+ ContentComponent?: ComponentType<WrapperComponentProps>;
661
+ FooterComponent?: ComponentType<FooterComponentProps>;
662
+ options?: ModalOptions;
663
+ context?: Record<string, any>;
664
+ usePathname?: () => { pathname: string }; // Router integration
665
+ root?: HTMLElement; // Custom root element
666
+ }
667
+ ```
668
+
669
+ #### ModalOptions
670
+
671
+ ```typescript
672
+ interface ModalOptions {
673
+ duration?: number | string;
674
+ backdrop?: string;
675
+ manualDestroy?: boolean;
676
+ closeOnBackdropClick?: boolean;
677
+ }
678
+ ```
679
+
680
+ #### usePathname (Router Integration)
681
+
682
+ The `usePathname` prop enables router integration. Modals will automatically close when the route changes.
683
+
684
+ ```typescript
685
+ import { useLocation } from 'react-router-dom';
686
+
687
+ <ModalProvider
688
+ usePathname={useLocation} // react-router-dom integration
689
+ // ...
690
+ >
691
+ <App />
692
+ </ModalProvider>
693
+ ```
694
+
695
+ #### Example
696
+
697
+ ```typescript
698
+ import { useLocation } from 'react-router-dom';
699
+
700
+ <ModalProvider
701
+ usePathname={useLocation}
702
+ ForegroundComponent={CustomForeground}
703
+ TitleComponent={CustomTitle}
704
+ SubtitleComponent={CustomSubtitle}
705
+ ContentComponent={CustomContent}
706
+ FooterComponent={CustomFooter}
707
+ options={{
708
+ duration: '200ms',
709
+ backdrop: 'rgba(0, 0, 0, 0.35)',
710
+ manualDestroy: true,
711
+ closeOnBackdropClick: true,
712
+ }}
713
+ context={{
714
+ theme: 'dark',
715
+ locale: 'en-US',
716
+ }}
717
+ >
718
+ <App />
719
+ </ModalProvider>
720
+ ```
721
+
722
+ ---
723
+
724
+ ### Custom Components
725
+
726
+ #### ModalFrameProps
727
+
728
+ Props passed to Foreground/Background components.
729
+
730
+ ```typescript
731
+ interface ModalFrameProps<Context = any, B = any> {
732
+ id: number;
733
+ type: 'alert' | 'confirm' | 'prompt';
734
+ alive: boolean;
735
+ visible: boolean;
736
+ initiator: string;
737
+ manualDestroy: boolean;
738
+ closeOnBackdropClick: boolean;
739
+ background?: ModalBackground<B>;
740
+ onConfirm: () => void;
741
+ onClose: () => void;
742
+ onChange: (value: any) => void;
743
+ onDestroy: () => void;
744
+ onChangeOrder: Function;
745
+ context: Context;
746
+ children: ReactNode;
747
+ }
748
+ ```
749
+
750
+ #### FooterComponentProps
751
+
752
+ Props for footer components.
753
+
754
+ ```typescript
755
+ interface FooterComponentProps {
756
+ type: 'alert' | 'confirm' | 'prompt';
757
+ onConfirm: (value?: any) => void;
758
+ onClose: () => void;
759
+ onCancel?: () => void;
760
+ disabled?: boolean;
761
+ footer?: FooterOptions;
762
+ context: any;
763
+ }
764
+ ```
765
+
766
+ #### WrapperComponentProps
767
+
768
+ Props for title/subtitle/content components.
769
+
770
+ ```typescript
771
+ interface WrapperComponentProps {
772
+ children: ReactNode;
773
+ context: any;
774
+ }
775
+ ```
776
+
777
+ #### Custom Component Example
778
+
779
+ ```typescript
780
+ const CustomForeground: FC<ModalFrameProps> = ({
781
+ id,
782
+ visible,
783
+ children,
784
+ onClose,
785
+ }) => {
786
+ const ref = useRef<HTMLDivElement>(null);
787
+ const { duration } = useModalDuration();
788
+
789
+ useModalAnimation(visible, {
790
+ onVisible: () => ref.current?.classList.add('visible'),
791
+ onHidden: () => ref.current?.classList.remove('visible'),
792
+ });
793
+
794
+ useDestroyAfter(id, duration);
795
+
796
+ return (
797
+ <div
798
+ ref={ref}
799
+ style={{
800
+ background: 'white',
801
+ borderRadius: 12,
802
+ padding: 24,
803
+ opacity: 0,
804
+ transition: `opacity ${duration}ms`,
805
+ }}
806
+ >
807
+ {children}
808
+ </div>
809
+ );
810
+ };
811
+ ```
812
+
813
+ ---
814
+
815
+ ## Type Definitions
816
+
817
+ ### Modal Types
818
+
819
+ ```typescript
820
+ type ModalType = 'alert' | 'confirm' | 'prompt';
821
+ type ModalSubtype = 'info' | 'success' | 'warning' | 'error';
822
+ ```
823
+
824
+ ### ModalBackground
825
+
826
+ ```typescript
827
+ interface ModalBackground<T = any> {
828
+ data?: T;
829
+ [key: string]: any;
830
+ }
831
+ ```
832
+
833
+ ### Content Props
834
+
835
+ ```typescript
836
+ interface AlertContentProps {
837
+ onConfirm: () => void;
838
+ context: any;
839
+ }
840
+
841
+ interface ConfirmContentProps {
842
+ onConfirm: () => void;
843
+ onCancel: () => void;
844
+ context: any;
845
+ }
846
+
847
+ interface PromptContentProps<T> {
848
+ value?: T;
849
+ onChange: (value: T) => void;
850
+ onConfirm: () => void;
851
+ onCancel: () => void;
852
+ context: any;
853
+ }
854
+ ```
855
+
856
+ ---
857
+
858
+ ## Usage Patterns
859
+
860
+ ### Pattern 1: Static API (Simplest)
861
+
862
+ ```typescript
863
+ import { alert, confirm, prompt } from '@lerx/promise-modal';
864
+
865
+ // Use anywhere, even outside React
866
+ async function handleSubmit() {
867
+ if (await confirm({ content: 'Save changes?' })) {
868
+ await saveData();
869
+ await alert({ content: 'Saved!' });
870
+ }
871
+ }
872
+ ```
873
+
874
+ ### Pattern 2: useModal Hook (Recommended for Components)
875
+
876
+ ```typescript
877
+ function EditForm() {
878
+ const modal = useModal();
879
+
880
+ const handleSave = async () => {
881
+ if (await modal.confirm({ content: 'Save?' })) {
882
+ // Modals auto-cleanup if component unmounts
883
+ }
884
+ };
885
+ }
886
+ ```
887
+
888
+ ### Pattern 3: Full Customization
889
+
890
+ ```typescript
891
+ <ModalProvider
892
+ ForegroundComponent={CustomForeground}
893
+ FooterComponent={CustomFooter}
894
+ options={{ duration: 400 }}
895
+ context={{ theme: 'dark' }}
896
+ >
897
+ <App />
898
+ </ModalProvider>
899
+ ```
900
+
901
+ ### Pattern 4: Per-Modal Override
902
+
903
+ ```typescript
904
+ await alert({
905
+ content: 'Special modal',
906
+ ForegroundComponent: SpecialForeground,
907
+ background: { variant: 'special' },
908
+ });
909
+ ```
910
+
911
+ ---
912
+
913
+ ## Advanced Examples
914
+
915
+ ### Toast Notifications
916
+
917
+ ```typescript
918
+ const ToastForeground: FC<ModalFrameProps> = ({
919
+ id,
920
+ visible,
921
+ children,
922
+ onClose,
923
+ }) => {
924
+ const ref = useRef<HTMLDivElement>(null);
925
+ const { duration } = useModalDuration();
926
+
927
+ useEffect(() => {
928
+ const timer = setTimeout(onClose, 3000);
929
+ return () => clearTimeout(timer);
930
+ }, [onClose]);
931
+
932
+ useModalAnimation(visible, {
933
+ onVisible: () => ref.current?.classList.add('visible'),
934
+ onHidden: () => ref.current?.classList.remove('visible'),
935
+ });
936
+
937
+ useDestroyAfter(id, duration);
938
+
939
+ return (
940
+ <div
941
+ ref={ref}
942
+ style={{
943
+ position: 'fixed',
944
+ bottom: 20,
945
+ left: '50%',
946
+ transform: 'translateX(-50%) translateY(100px)',
947
+ opacity: 0,
948
+ transition: `all ${duration}ms`,
949
+ }}
950
+ >
951
+ {children}
952
+ </div>
953
+ );
954
+ };
955
+
956
+ export const toast = (message: ReactNode) => {
957
+ return alert({
958
+ content: message,
959
+ ForegroundComponent: ToastForeground,
960
+ footer: false,
961
+ dimmed: false,
962
+ closeOnBackdropClick: false,
963
+ });
964
+ };
965
+ ```
966
+
967
+ ### Multi-Step Wizard
968
+
969
+ ```typescript
970
+ async function registrationWizard() {
971
+ // Step 1: Terms acceptance
972
+ const accepted = await confirm({
973
+ title: 'Terms of Service',
974
+ content: <TermsContent />,
975
+ footer: { confirm: 'I Accept', cancel: 'Decline' },
976
+ });
977
+
978
+ if (!accepted) return null;
979
+
980
+ // Step 2: User information
981
+ const userInfo = await prompt<{
982
+ name: string;
983
+ email: string;
984
+ }>({
985
+ title: 'Your Information',
986
+ defaultValue: { name: '', email: '' },
987
+ Input: RegistrationForm,
988
+ disabled: (v) => !v.name || !v.email?.includes('@'),
989
+ });
990
+
991
+ if (!userInfo) return null;
992
+
993
+ // Step 3: Confirmation
994
+ await alert({
995
+ title: 'Welcome!',
996
+ content: `Account created for ${userInfo.name}`,
997
+ subtype: 'success',
998
+ });
999
+
1000
+ return userInfo;
1001
+ }
1002
+ ```
1003
+
1004
+ ### Custom Anchor (Iframe/Portal)
1005
+
1006
+ ```typescript
1007
+ function IframedModals() {
1008
+ const { initialize } = useInitializeModal({ mode: 'manual' });
1009
+ const containerRef = useRef<HTMLDivElement>(null);
1010
+
1011
+ useEffect(() => {
1012
+ if (containerRef.current) {
1013
+ initialize(containerRef.current);
1014
+ }
1015
+ }, [initialize]);
1016
+
1017
+ return (
1018
+ <div style={{ position: 'relative', height: 600 }}>
1019
+ <div ref={containerRef} style={{ height: '100%' }} />
1020
+ <ModalTriggerButtons />
1021
+ </div>
1022
+ );
1023
+ }
1024
+ ```
1025
+
1026
+ ---
1027
+
1028
+ ## Lifecycle
1029
+
1030
+ ```
1031
+ Creation → Show → Hide → Destroy
1032
+ ↓ ↓ ↓ ↓
1033
+ open() visible onHide onDestroy
1034
+ ↓ true ↓ ↓
1035
+ nodeFactory ↓ visible alive
1036
+ ↓ ↓ false false
1037
+ Promise animation ↓ ↓
1038
+ ↓ starts duration removed
1039
+ ... ↓ passes from DOM
1040
+ ↓ ↓
1041
+ interaction destroy
1042
+
1043
+ resolve
1044
+ ```
1045
+
1046
+ ---
1047
+
1048
+ ## Best Practices
1049
+
1050
+ 1. **Wrap app with ModalProvider** at the root level
1051
+ 2. **Use useModal in components** for automatic cleanup
1052
+ 3. **Use static API for utilities** outside React
1053
+ 4. **Customize at provider level** for consistent styling
1054
+ 5. **Use subtype for semantics** (info, success, warning, error)
1055
+ 6. **Always await or handle** promise rejections
1056
+ 7. **Keep modal content simple** - avoid complex state
1057
+ 8. **Test accessibility** - ensure keyboard navigation
1058
+
1059
+ ---
1060
+
1061
+ ## AbortSignal Support
1062
+
1063
+ Provides `AbortSignal` support for programmatically canceling modals.
1064
+
1065
+ ### Basic Usage
1066
+
1067
+ ```typescript
1068
+ const controller = new AbortController();
1069
+
1070
+ alert({
1071
+ title: 'Cancelable Modal',
1072
+ content: 'This will auto-close in 3 seconds.',
1073
+ signal: controller.signal,
1074
+ });
1075
+
1076
+ // Cancel modal after 3 seconds
1077
+ setTimeout(() => {
1078
+ controller.abort();
1079
+ }, 3000);
1080
+ ```
1081
+
1082
+ ### Manual Abort Control
1083
+
1084
+ ```typescript
1085
+ function ManualAbortControl() {
1086
+ const [controller, setController] = useState<AbortController | null>(null);
1087
+
1088
+ const handleOpen = () => {
1089
+ const newController = new AbortController();
1090
+ setController(newController);
1091
+
1092
+ alert({
1093
+ title: 'Manual Cancel',
1094
+ content: 'Click "Cancel" button to close this modal.',
1095
+ signal: newController.signal,
1096
+ closeOnBackdropClick: false,
1097
+ }).then(() => {
1098
+ setController(null);
1099
+ });
1100
+ };
1101
+
1102
+ const handleAbort = () => {
1103
+ if (controller) {
1104
+ controller.abort();
1105
+ }
1106
+ };
1107
+
1108
+ return (
1109
+ <div>
1110
+ <button onClick={handleOpen} disabled={!!controller}>
1111
+ Open Modal
1112
+ </button>
1113
+ <button onClick={handleAbort} disabled={!controller}>
1114
+ Cancel Modal
1115
+ </button>
1116
+ </div>
1117
+ );
1118
+ }
1119
+ ```
1120
+
1121
+ ### Batch Cancel Multiple Modals
1122
+
1123
+ ```typescript
1124
+ function MultipleModalsAbort() {
1125
+ const [controllers, setControllers] = useState<AbortController[]>([]);
1126
+
1127
+ const handleOpenMultiple = () => {
1128
+ const newControllers: AbortController[] = [];
1129
+
1130
+ for (let i = 0; i < 3; i++) {
1131
+ const controller = new AbortController();
1132
+ newControllers.push(controller);
1133
+
1134
+ alert({
1135
+ title: `Modal ${i + 1}`,
1136
+ content: `This is modal number ${i + 1}.`,
1137
+ signal: controller.signal,
1138
+ closeOnBackdropClick: false,
1139
+ });
1140
+ }
1141
+
1142
+ setControllers(newControllers);
1143
+ };
1144
+
1145
+ const handleAbortAll = () => {
1146
+ controllers.forEach((controller) => controller.abort());
1147
+ setControllers([]);
1148
+ };
1149
+
1150
+ return (
1151
+ <div>
1152
+ <button onClick={handleOpenMultiple}>Open 3 Modals</button>
1153
+ <button onClick={handleAbortAll}>Cancel All Modals</button>
1154
+ </div>
1155
+ );
1156
+ }
1157
+ ```
1158
+
1159
+ ### Pre-Aborted Signal Handling
1160
+
1161
+ ```typescript
1162
+ // If the signal is already aborted, the modal closes immediately
1163
+ const controller = new AbortController();
1164
+ controller.abort(); // Abort first
1165
+
1166
+ alert({
1167
+ title: 'Instant Close',
1168
+ content: 'Signal is already aborted, closing immediately.',
1169
+ signal: controller.signal,
1170
+ }).then(() => {
1171
+ console.log('Modal closed immediately.');
1172
+ });
1173
+ ```
1174
+
1175
+ ---
1176
+
1177
+ ## License
1178
+
1179
+ MIT License
1180
+
1181
+ ---
1182
+
1183
+ ## Version
1184
+
1185
+ Current: See package.json