@lerx/promise-modal 0.0.59 → 0.0.61

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.
Files changed (2) hide show
  1. package/README.md +1197 -10
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -10,34 +10,1221 @@
10
10
 
11
11
  `@lerx/promise-modal` is a universal modal utility based on React.
12
12
 
13
- It can be used even in places not included in React components,
13
+ Key features include:
14
14
 
15
- and after the modal is opened, you can retrieve the result as a promise.
15
+ - Can be used even in places not included in React components
16
+ - After opening a modal, you can retrieve the result as a promise
17
+ - Supports various modal types (alert, confirm, prompt)
18
+ - Highly customizable component structure
16
19
 
17
20
  ---
18
21
 
19
- ## How to use
22
+ ## Installation
20
23
 
21
24
  ```bash
22
25
  yarn add @lerx/promise-modal
23
26
  ```
24
27
 
25
- _Under Preparation_
28
+ or
29
+
30
+ ```bash
31
+ npm install @lerx/promise-modal
32
+ ```
26
33
 
27
34
  ---
28
35
 
29
- ## Contribution Guidelines
36
+ ## How to Use
37
+
38
+ ### 1. Setting up the Modal Provider
39
+
40
+ Install `ModalProvider` at the root of your application:
41
+
42
+ ```tsx
43
+ import { ModalProvider } from '@lerx/promise-modal';
44
+
45
+ function App() {
46
+ return (
47
+ <ModalProvider>
48
+ <YourApp />
49
+ </ModalProvider>
50
+ );
51
+ }
52
+ ```
53
+
54
+ You can also apply custom options and components:
55
+
56
+ ```tsx
57
+ import { ModalProvider } from '@lerx/promise-modal';
58
+
59
+ import {
60
+ CustomBackground,
61
+ CustomContent,
62
+ CustomFooter,
63
+ CustomForeground,
64
+ CustomSubtitle,
65
+ CustomTitle,
66
+ } from './components';
67
+
68
+ function App() {
69
+ return (
70
+ <ModalProvider
71
+ ForegroundComponent={CustomForeground}
72
+ BackgroundComponent={CustomBackground}
73
+ TitleComponent={CustomTitle}
74
+ SubtitleComponent={CustomSubtitle}
75
+ ContentComponent={CustomContent}
76
+ FooterComponent={CustomFooter}
77
+ options={{
78
+ duration: '250ms', // Animation duration
79
+ backdrop: 'rgba(0, 0, 0, 0.35)', // Background overlay color
80
+ manualDestroy: false, // Default auto-destroy behavior
81
+ closeOnBackdropClick: true, // Default backdrop click behavior
82
+ }}
83
+ context={{
84
+ // Context values accessible in all modal components
85
+ theme: 'light',
86
+ locale: 'en-US',
87
+ }}
88
+ >
89
+ <YourApp />
90
+ </ModalProvider>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ### 2. Using Basic Modals
96
+
97
+ #### Alert Modal
98
+
99
+ Alert modals provide simple information display with a confirmation button.
100
+
101
+ ```tsx
102
+ import { alert } from '@lerx/promise-modal';
103
+
104
+ // Basic usage
105
+ async function showAlert() {
106
+ await alert({
107
+ title: 'Notification',
108
+ content: 'The task has been completed.',
109
+ });
110
+ console.log('User closed the modal.');
111
+ }
112
+
113
+ // Using various options
114
+ async function showDetailedAlert() {
115
+ await alert({
116
+ subtype: 'success', // 'info' | 'success' | 'warning' | 'error'
117
+ title: 'Success',
118
+ subtitle: 'Details',
119
+ content: 'The task has been successfully completed.',
120
+ dimmed: true, // Dim the background
121
+ closeOnBackdropClick: true, // Close on backdrop click
122
+ // If use close animation, you need to set manualDestroy to true
123
+ manualDestroy: false, // Auto-destroy (false: auto, true: manual)
124
+ // Data to pass to the background
125
+ background: {
126
+ data: 'custom-data',
127
+ },
128
+ });
129
+ }
130
+ ```
131
+
132
+ #### Confirm Modal
133
+
134
+ Confirm modals are used for actions that require user confirmation.
135
+
136
+ ```tsx
137
+ import { confirm } from '@lerx/promise-modal';
138
+
139
+ async function showConfirm() {
140
+ const result = await confirm({
141
+ title: 'Confirm',
142
+ content: 'Are you sure you want to delete this?',
143
+ // Custom footer text
144
+ footer: {
145
+ confirm: 'Delete',
146
+ cancel: 'Cancel',
147
+ },
148
+ });
149
+
150
+ if (result) {
151
+ console.log('User clicked confirm.');
152
+ // Execute delete logic
153
+ } else {
154
+ console.log('User clicked cancel.');
155
+ }
156
+ }
157
+ ```
158
+
159
+ #### Prompt Modal
160
+
161
+ Prompt modals are used to receive input from users.
162
+
163
+ ```tsx
164
+ import { prompt } from '@lerx/promise-modal';
165
+
166
+ async function showPrompt() {
167
+ // Text input
168
+ const name = await prompt<string>({
169
+ title: 'Enter Name',
170
+ content: 'Please enter your name.',
171
+ defaultValue: '', // Default value
172
+ Input: ({ value, onChange }) => {
173
+ // Important: value is the current value, onChange is the function to update the value
174
+ return (
175
+ <input
176
+ value={value}
177
+ onChange={(e) => onChange(e.target.value)}
178
+ placeholder="Enter name"
179
+ />
180
+ );
181
+ },
182
+ // Validate input to control the confirmation button
183
+ disabled: (value) => value.length < 2,
184
+ });
185
+
186
+ console.log('Entered name:', name);
187
+
188
+ // Complex data input
189
+ const userInfo = await prompt<{ name: string; age: number }>({
190
+ title: 'User Information',
191
+ defaultValue: { name: '', age: 0 },
192
+ Input: ({ value, onChange }) => (
193
+ <div>
194
+ <input
195
+ value={value.name}
196
+ onChange={(e) => onChange({ ...value, name: e.target.value })}
197
+ placeholder="Name"
198
+ />
199
+ <input
200
+ type="number"
201
+ value={value.age}
202
+ onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
203
+ placeholder="Age"
204
+ />
205
+ </div>
206
+ ),
207
+ });
208
+
209
+ console.log('User info:', userInfo);
210
+ }
211
+ ```
212
+
213
+ ### 3. Using Custom Components
214
+
215
+ You can customize the appearance and behavior of modals:
216
+
217
+ ```tsx
218
+ import { css } from '@emotion/css';
219
+ import { ModalProvider, alert, confirm } from '@lerx/promise-modal';
220
+
221
+ // Custom foreground component
222
+ const CustomForegroundComponent = ({ children, type, ...props }) => {
223
+ return (
224
+ <div
225
+ className={css`
226
+ background-color: #ffffff;
227
+ border-radius: 12px;
228
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
229
+ width: 90%;
230
+ max-width: 500px;
231
+ padding: 24px;
232
+ position: relative;
233
+ `}
234
+ >
235
+ {children}
236
+ </div>
237
+ );
238
+ };
239
+
240
+ // Custom background component (utilizing background data)
241
+ const CustomBackgroundComponent = ({ children, onClick, background }) => {
242
+ // Apply different styles based on background data
243
+ const getBgColor = () => {
244
+ if (background?.data === 'alert') return 'rgba(0, 0, 0, 0.7)';
245
+ if (background?.data === 'confirm') return 'rgba(0, 0, 0, 0.5)';
246
+ if (background?.data === 'prompt') return 'rgba(0, 0, 0, 0.6)';
247
+ return 'rgba(0, 0, 0, 0.4)';
248
+ };
249
+
250
+ return (
251
+ <div
252
+ className={css`
253
+ position: fixed;
254
+ top: 0;
255
+ left: 0;
256
+ right: 0;
257
+ bottom: 0;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ background-color: ${getBgColor()};
262
+ z-index: 1000;
263
+ `}
264
+ onClick={onClick}
265
+ >
266
+ {children}
267
+ </div>
268
+ );
269
+ };
270
+
271
+ // Custom title component
272
+ const CustomTitleComponent = ({ children, context }) => {
273
+ return (
274
+ <h2
275
+ className={css`
276
+ font-size: 24px;
277
+ margin-bottom: 8px;
278
+ color: ${context?.theme === 'dark' ? '#ffffff' : '#333333'};
279
+ `}
280
+ >
281
+ {children}
282
+ </h2>
283
+ );
284
+ };
285
+
286
+ // Custom subtitle component
287
+ const CustomSubtitleComponent = ({ children, context }) => {
288
+ return (
289
+ <h3
290
+ className={css`
291
+ font-size: 16px;
292
+ margin-bottom: 16px;
293
+ color: ${context?.theme === 'dark' ? '#cccccc' : '#666666'};
294
+ `}
295
+ >
296
+ {children}
297
+ </h3>
298
+ );
299
+ };
300
+
301
+ // Custom content component
302
+ const CustomContentComponent = ({ children, context }) => {
303
+ return (
304
+ <div
305
+ className={css`
306
+ margin-bottom: 24px;
307
+ color: ${context?.theme === 'dark' ? '#eeeeee' : '#444444'};
308
+ `}
309
+ >
310
+ {children}
311
+ </div>
312
+ );
313
+ };
314
+
315
+ // Custom footer component
316
+ const CustomFooterComponent = ({
317
+ onConfirm,
318
+ onClose,
319
+ onCancel,
320
+ type,
321
+ disabled,
322
+ }) => {
323
+ return (
324
+ <div
325
+ className={css`
326
+ display: flex;
327
+ justify-content: flex-end;
328
+ gap: 12px;
329
+ margin-top: 24px;
330
+ `}
331
+ >
332
+ {/* Display cancel button for confirm and prompt modals */}
333
+ {(type === 'confirm' || type === 'prompt') && (
334
+ <button
335
+ className={css`
336
+ padding: 8px 16px;
337
+ border: 1px solid #ccc;
338
+ background: none;
339
+ border-radius: 4px;
340
+ cursor: pointer;
341
+ &:hover {
342
+ background-color: #f3f3f3;
343
+ }
344
+ `}
345
+ onClick={type === 'confirm' ? () => onConfirm(false) : onCancel}
346
+ >
347
+ Cancel
348
+ </button>
349
+ )}
350
+ <button
351
+ className={css`
352
+ padding: 8px 16px;
353
+ background-color: #4a90e2;
354
+ color: white;
355
+ border: none;
356
+ border-radius: 4px;
357
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
358
+ opacity: ${disabled ? 0.6 : 1};
359
+ &:hover {
360
+ background-color: ${disabled ? '#4a90e2' : '#357ac7'};
361
+ }
362
+ `}
363
+ onClick={() => onConfirm(type === 'confirm' ? true : undefined)}
364
+ disabled={disabled}
365
+ >
366
+ Confirm
367
+ </button>
368
+ </div>
369
+ );
370
+ };
371
+
372
+ // Global setup in provider
373
+ function App() {
374
+ return (
375
+ <ModalProvider
376
+ ForegroundComponent={CustomForegroundComponent}
377
+ BackgroundComponent={CustomBackgroundComponent}
378
+ TitleComponent={CustomTitleComponent}
379
+ SubtitleComponent={CustomSubtitleComponent}
380
+ ContentComponent={CustomContentComponent}
381
+ FooterComponent={CustomFooterComponent}
382
+ options={{
383
+ duration: 300, // Animation duration (ms)
384
+ }}
385
+ context={{
386
+ // Context accessible in all modals
387
+ theme: 'light',
388
+ locale: 'en-US',
389
+ }}
390
+ >
391
+ <YourApp />
392
+ </ModalProvider>
393
+ );
394
+ }
395
+
396
+ // Applying custom components to specific modals
397
+ async function showCustomAlert() {
398
+ await alert({
399
+ title: 'Notification',
400
+ content: 'Content',
401
+ // Custom component only for this modal
402
+ ForegroundComponent: ({ children }) => (
403
+ <div
404
+ className={css`
405
+ background-color: #f0f8ff;
406
+ padding: 30px;
407
+ border-radius: 16px;
408
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
409
+ `}
410
+ >
411
+ {children}
412
+ </div>
413
+ ),
414
+ // Pass background data
415
+ background: {
416
+ data: 'custom-alert',
417
+ },
418
+ });
419
+ }
420
+ ```
421
+
422
+ ### 4. Using Custom Anchors and Initialization
423
+
424
+ You can specify the DOM element where modals will be rendered:
425
+
426
+ ```tsx
427
+ import { useEffect, useRef } from 'react';
428
+
429
+ import {
430
+ ModalProvider,
431
+ ModalProviderHandle,
432
+ alert,
433
+ useInitializeModal,
434
+ } from '@lerx/promise-modal';
30
435
 
31
- _Under Preparation_
436
+ // Using refs
437
+ function CustomAnchorExample() {
438
+ // Modal provider handle reference
439
+ const modalProviderRef = useRef<ModalProviderHandle>(null);
440
+ // Container reference for modal display
441
+ const modalContainerRef = useRef<HTMLDivElement>(null);
442
+
443
+ useEffect(() => {
444
+ // Initialize modal when container is ready
445
+ if (modalContainerRef.current && modalProviderRef.current) {
446
+ modalProviderRef.current.initialize(modalContainerRef.current);
447
+ }
448
+ }, []);
449
+
450
+ return (
451
+ <ModalProvider ref={modalProviderRef}>
452
+ <div>
453
+ {/* Modals will render inside this div */}
454
+ <div
455
+ ref={modalContainerRef}
456
+ style={{
457
+ backgroundColor: '#f0f0f0',
458
+ width: '100%',
459
+ height: '500px',
460
+ position: 'relative',
461
+ overflow: 'hidden',
462
+ }}
463
+ />
464
+
465
+ <button
466
+ onClick={() =>
467
+ alert({ title: 'Notice', content: 'Displayed in custom anchor.' })
468
+ }
469
+ >
470
+ Show Modal
471
+ </button>
472
+ </div>
473
+ </ModalProvider>
474
+ );
475
+ }
476
+
477
+ // Using the useInitializeModal hook
478
+ function CustomAnchorWithHookExample() {
479
+ // useInitializeModal hook (manual mode: manual initialization)
480
+ const { initialize, portal } = useInitializeModal({ mode: 'manual' });
481
+ const containerRef = useRef<HTMLDivElement>(null);
482
+
483
+ useEffect(() => {
484
+ if (containerRef.current) {
485
+ // Initialize modal with container element
486
+ initialize(containerRef.current);
487
+ }
488
+ }, [initialize]);
489
+
490
+ return (
491
+ <div>
492
+ {/* Container for modal rendering */}
493
+ <div
494
+ ref={containerRef}
495
+ style={{
496
+ backgroundColor: '#f0f0f0',
497
+ width: '100%',
498
+ height: '500px',
499
+ }}
500
+ />
501
+
502
+ <button
503
+ onClick={() =>
504
+ alert({ title: 'Notice', content: 'Displayed in custom anchor.' })
505
+ }
506
+ >
507
+ Show Modal
508
+ </button>
509
+
510
+ {/* Portal rendering in another location (optional) */}
511
+ <div id="another-container">{portal}</div>
512
+ </div>
513
+ );
514
+ }
515
+ ```
516
+
517
+ ### 5. Implementing Toast Messages
518
+
519
+ You can implement toast message functionality using `promise-modal`. This example is based on actual implementation in the project:
520
+
521
+ ```tsx
522
+ import React, { type ReactNode, useEffect, useRef } from 'react';
523
+
524
+ import { css } from '@emotion/css';
525
+ import {
526
+ ModalFrameProps,
527
+ alert,
528
+ useDestroyAfter,
529
+ useModalAnimation,
530
+ useModalDuration,
531
+ } from '@lerx/promise-modal';
532
+
533
+ // Toast foreground component definition
534
+ const ToastForeground = ({
535
+ id,
536
+ visible,
537
+ children,
538
+ onClose,
539
+ hideAfterMs = 3000,
540
+ }) => {
541
+ const modalRef = useRef(null);
542
+ const { duration } = useModalDuration();
543
+
544
+ // Auto-close after specified time
545
+ useEffect(() => {
546
+ const timer = setTimeout(onClose, hideAfterMs);
547
+ return () => clearTimeout(timer);
548
+ }, [onClose, hideAfterMs]);
549
+
550
+ // Animation handling
551
+ useModalAnimation(visible, {
552
+ onVisible: () => {
553
+ modalRef.current?.classList.add('visible');
554
+ },
555
+ onHidden: () => {
556
+ modalRef.current?.classList.remove('visible');
557
+ },
558
+ });
559
+
560
+ // Destroy after closing
561
+ useDestroyAfter(id, duration);
562
+
563
+ return (
564
+ <div
565
+ ref={modalRef}
566
+ className={css`
567
+ position: fixed;
568
+ bottom: 20px;
569
+ left: 50%;
570
+ transform: translateX(-50%) translateY(100px);
571
+ opacity: 0;
572
+ transition:
573
+ transform ${duration}ms,
574
+ opacity ${duration}ms;
575
+ &.visible {
576
+ transform: translateX(-50%) translateY(0);
577
+ opacity: 1;
578
+ }
579
+ `}
580
+ >
581
+ {children}
582
+ </div>
583
+ );
584
+ };
585
+
586
+ // Toast message interface
587
+ interface ToastProps {
588
+ message: ReactNode;
589
+ duration?: number;
590
+ }
591
+
592
+ // Handler to remove previous toast
593
+ let onDestroyPrevToast: () => void;
594
+
595
+ // Toast display function
596
+ export const toast = ({ message, duration = 1250 }: ToastProps) => {
597
+ // Remove previous toast if exists
598
+ onDestroyPrevToast?.();
599
+
600
+ return alert({
601
+ content: message,
602
+ ForegroundComponent: (props: ModalFrameProps) => {
603
+ // Store destroy function of new toast
604
+ onDestroyPrevToast = props.onDestroy;
605
+ return <ToastForeground {...props} hideAfterMs={duration} />;
606
+ },
607
+ footer: false, // Hide footer
608
+ dimmed: false, // Disable background dim
609
+ closeOnBackdropClick: false, // Disable closing on backdrop click
610
+ });
611
+ };
612
+
613
+ // Usage example
614
+ function showToastExample() {
615
+ toast({
616
+ message: (
617
+ <div
618
+ className={css`
619
+ background-color: #333;
620
+ color: white;
621
+ padding: 12px 24px;
622
+ border-radius: 8px;
623
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
624
+ `}
625
+ >
626
+ Task completed!
627
+ </div>
628
+ ),
629
+ duration: 2000, // Auto-close after 2 seconds
630
+ });
631
+ }
632
+ ```
633
+
634
+ ### 6. Nested Modal Providers
635
+
636
+ You can use multiple modal providers with different settings within the same application:
637
+
638
+ ```tsx
639
+ import { ModalProvider, alert } from '@lerx/promise-modal';
640
+
641
+ function NestedModalProviders() {
642
+ return (
643
+ <ModalProvider
644
+ options={{
645
+ duration: 300,
646
+ backdrop: 'rgba(0, 0, 0, 0.5)',
647
+ }}
648
+ >
649
+ {/* Modal using outer provider settings */}
650
+ <button
651
+ onClick={() =>
652
+ alert({
653
+ title: 'Outer Modal',
654
+ content: 'This uses outer modal provider settings.',
655
+ })
656
+ }
657
+ >
658
+ Open Outer Modal
659
+ </button>
660
+
661
+ {/* Inner provider with different settings */}
662
+ <div className="inner-section">
663
+ <ModalProvider
664
+ SubtitleComponent={() => (
665
+ <div className="custom-subtitle">Inner Modal</div>
666
+ )}
667
+ options={{
668
+ duration: 500,
669
+ backdrop: 'rgba(0, 0, 0, 0.8)',
670
+ }}
671
+ >
672
+ {/* Modal using inner provider settings */}
673
+ <button
674
+ onClick={() =>
675
+ alert({
676
+ title: 'Inner Modal',
677
+ content: 'This uses inner modal provider settings.',
678
+ })
679
+ }
680
+ >
681
+ Open Inner Modal
682
+ </button>
683
+ </ModalProvider>
684
+ </div>
685
+ </ModalProvider>
686
+ );
687
+ }
688
+ ```
689
+
690
+ ### 7. Various Modal Configuration Options
691
+
692
+ Examples utilizing various modal configuration options:
693
+
694
+ ```tsx
695
+ import { alert, confirm } from '@lerx/promise-modal';
696
+
697
+ // Basic alert modal
698
+ async function showBasicAlert() {
699
+ await alert({
700
+ title: 'Basic Notification',
701
+ content: 'This is an alert modal with default settings.',
702
+ });
703
+ }
704
+
705
+ // Modal settings by type
706
+ async function showModalByType() {
707
+ // Success notification
708
+ await alert({
709
+ title: 'Success',
710
+ content: 'Task completed successfully.',
711
+ subtype: 'success',
712
+ dimmed: true,
713
+ });
714
+
715
+ // Warning confirmation
716
+ const result = await confirm({
717
+ title: 'Warning',
718
+ content: 'This action cannot be undone. Continue?',
719
+ subtype: 'warning',
720
+ closeOnBackdropClick: false, // Prevent closing by backdrop click
721
+ });
722
+
723
+ // Error notification
724
+ await alert({
725
+ title: 'Error',
726
+ content: 'Unable to complete the task.',
727
+ subtype: 'error',
728
+ // Custom footer text
729
+ footer: {
730
+ confirm: 'I understand',
731
+ },
732
+ });
733
+ }
734
+
735
+ // Using manual destroy mode
736
+ async function showManualDestroyModal() {
737
+ await alert({
738
+ title: 'Notification',
739
+ content:
740
+ "This modal won't close on backdrop click and requires confirmation button to close.",
741
+ manualDestroy: true, // Enable manual destroy mode
742
+ closeOnBackdropClick: false, // Prevent closing by backdrop click
743
+ });
744
+ }
745
+
746
+ // Using background data
747
+ async function showModalWithBackground() {
748
+ await alert({
749
+ title: 'Background Data Usage',
750
+ content: 'This modal passes data to the background component.',
751
+ background: {
752
+ data: 'custom-background-data',
753
+ opacity: 0.8,
754
+ blur: '5px',
755
+ },
756
+ });
757
+ }
758
+ ```
32
759
 
33
760
  ---
34
761
 
35
- ## License
762
+ ## API Reference
763
+
764
+ ### Core Functions
765
+
766
+ #### `alert(options)`
767
+
768
+ Opens a simple alert modal to display information to the user.
769
+
770
+ **Parameters:**
771
+
772
+ - `options`: Alert modal configuration object
773
+ - `title?`: Modal title (ReactNode)
774
+ - `subtitle?`: Subtitle (ReactNode)
775
+ - `content?`: Modal content (ReactNode or component)
776
+ - `subtype?`: Modal type ('info' | 'success' | 'warning' | 'error')
777
+ - `background?`: Background settings (ModalBackground object)
778
+ - `footer?`: Footer settings
779
+ - Function: `(props: FooterComponentProps) => ReactNode`
780
+ - Object: `{ confirm?: string; hideConfirm?: boolean }`
781
+ - `false`: Hide footer
782
+ - `dimmed?`: Whether to dim the background (boolean)
783
+ - `manualDestroy?`: Enable manual destroy mode (boolean)
784
+ - `closeOnBackdropClick?`: Whether to close on backdrop click (boolean)
785
+ - `ForegroundComponent?`: Custom foreground component
786
+ - `BackgroundComponent?`: Custom background component
787
+
788
+ **Returns:** `Promise<void>` - Resolved when the modal is closed
789
+
790
+ ```typescript
791
+ // Example
792
+ await alert({
793
+ title: 'Notification',
794
+ content: 'Content',
795
+ subtype: 'info',
796
+ closeOnBackdropClick: true,
797
+ });
798
+ ```
799
+
800
+ #### `confirm(options)`
801
+
802
+ Opens a confirmation modal that requests user confirmation.
803
+
804
+ **Parameters:**
36
805
 
37
- This repository is licensed under the MIT License. Please refer to the [`LICENSE`](./LICENSE) file for details.
806
+ - `options`: Confirm modal configuration
807
+ - Same options as `alert`, plus:
808
+ - `footer?`: Footer settings
809
+ - Function: `(props: FooterComponentProps) => ReactNode`
810
+ - Object: `{ confirm?: string; cancel?: string; hideConfirm?: boolean; hideCancel?: boolean }`
811
+ - `false`: Hide footer
812
+
813
+ **Returns:** `Promise<boolean>` - Resolves to true if confirmed, false if canceled
814
+
815
+ ```typescript
816
+ // Example
817
+ const result = await confirm({
818
+ title: 'Confirm',
819
+ content: 'Do you want to proceed?',
820
+ footer: {
821
+ confirm: 'Confirm',
822
+ cancel: 'Cancel',
823
+ },
824
+ });
825
+ ```
826
+
827
+ #### `prompt<T>(options)`
828
+
829
+ Opens a prompt modal to receive input from the user.
830
+
831
+ **Parameters:**
832
+
833
+ - `options`: Prompt modal configuration object
834
+ - Same options as `alert`, plus:
835
+ - `Input`: Function to render input field
836
+ - `(props: PromptInputProps<T>) => ReactNode`
837
+ - props: `{ value: T; onChange: (value: T) => void }`
838
+ - `defaultValue?`: Default value (T)
839
+ - `disabled?`: Function to determine if confirm button should be disabled
840
+ - `(value: T) => boolean`
841
+ - `returnOnCancel?`: Whether to return default value on cancel (boolean)
842
+ - `footer?`: Footer settings (similar to confirm)
843
+
844
+ **Returns:** `Promise<T>` - Resolves to the input value
845
+
846
+ ```typescript
847
+ // Example
848
+ const value = await prompt<string>({
849
+ title: 'Input',
850
+ defaultValue: '',
851
+ Input: ({ value, onChange }) => (
852
+ <input
853
+ value={value}
854
+ onChange={(e) => onChange(e.target.value)}
855
+ />
856
+ ),
857
+ disabled: (value) => value.trim() === '',
858
+ });
859
+ ```
860
+
861
+ ### Components
862
+
863
+ #### `ModalProvider` (or `BootstrapProvider`)
864
+
865
+ Component that initializes and provides the modal service.
866
+
867
+ **Props:**
868
+
869
+ - `ForegroundComponent?`: Custom foreground component
870
+ - `(props: WrapperComponentProps) => ReactNode`
871
+ - `BackgroundComponent?`: Custom background component
872
+ - `(props: WrapperComponentProps) => ReactNode`
873
+ - `TitleComponent?`: Custom title component
874
+ - `SubtitleComponent?`: Custom subtitle component
875
+ - `ContentComponent?`: Custom content component
876
+ - `FooterComponent?`: Custom footer component
877
+ - `(props: FooterComponentProps) => ReactNode`
878
+ - `options?`: Global modal options
879
+ - `duration?`: Animation duration (milliseconds)
880
+ - `backdrop?`: Backdrop click handling
881
+ - Other options...
882
+ - `context?`: Context object to pass to modal components
883
+ - `usePathname?`: Custom pathname hook function
884
+
885
+ **Handle:**
886
+
887
+ - `initialize`: Method to manually initialize modal service
888
+
889
+ ```typescript
890
+ // Example using ref for control
891
+ import { useRef } from 'react';
892
+ import { ModalProvider, ModalProviderHandle } from '@lerx/promise-modal';
893
+
894
+ function App() {
895
+ const modalProviderRef = useRef<ModalProviderHandle>(null);
896
+
897
+ const handleInitialize = () => {
898
+ modalProviderRef.current?.initialize();
899
+ };
900
+
901
+ return (
902
+ <ModalProvider ref={modalProviderRef}>
903
+ <YourApp />
904
+ </ModalProvider>
905
+ );
906
+ }
907
+ ```
908
+
909
+ ### Hooks
910
+
911
+ #### `useModalOptions`
912
+
913
+ Read global options for modals.
914
+
915
+ ```typescript
916
+ import { useModalOptions } from '@lerx/promise-modal';
917
+
918
+ function Component() {
919
+ // ConfigurationContextProps
920
+ const options = useModalOptions();
921
+
922
+ // ...
923
+ }
924
+ ```
925
+
926
+ #### `useModalDuration`
927
+
928
+ Read modal animation duration.
929
+
930
+ ```typescript
931
+ import { useModalDuration } from '@lerx/promise-modal';
932
+
933
+ function Component() {
934
+ // duration is 300ms, milliseconds is 300
935
+ const { duration, milliseconds } = useModalDuration();
936
+ // ...
937
+ }
938
+ ```
939
+
940
+ #### `useModalBackdrop`
941
+
942
+ Read modal backdrop settings.
943
+
944
+ ```typescript
945
+ import { useModalBackdrop } from '@lerx/promise-modal';
946
+
947
+ function Component() {
948
+ // backdrop is Color(#000000~#ffffff or rgba(0,0,0,0.5))
949
+ const backdrop = useModalBackdrop();
950
+ // ...
951
+ }
952
+ ```
953
+
954
+ #### `useInitializeModal`
955
+
956
+ Initializes the modal service. Usually called automatically by `ModalProvider`.
957
+
958
+ ```typescript
959
+ import { useInitializeModal } from '@lerx/promise-modal';
960
+
961
+ function Component() {
962
+ const { initialize } = useInitializeModal();
963
+
964
+ // Manually initialize if needed
965
+ useEffect(() => {
966
+ initialize();
967
+ }, [initialize]);
968
+
969
+ // ...
970
+ }
971
+ ```
972
+
973
+ #### `useSubscribeModal`
974
+
975
+ Subscribes to modal state changes.
976
+
977
+ ```typescript
978
+ import { useSubscribeModal } from '@lerx/promise-modal';
979
+
980
+ function Component({ modal }) {
981
+ // Component rerenders when modal state changes
982
+ const version = useSubscribeModal(modal);
983
+
984
+ // ...
985
+ }
986
+ ```
987
+
988
+ #### `useDestroyAfter`
989
+
990
+ Automatically destroys a modal after specified time.
991
+
992
+ ```typescript
993
+ import { useDestroyAfter } from '@lerx/promise-modal';
994
+
995
+ function Component({ modalId }) {
996
+ // Auto-close modal after 3 seconds
997
+ useDestroyAfter(modalId, 3000);
998
+
999
+ // ...
1000
+ }
1001
+ ```
1002
+
1003
+ #### `useActiveModalCount`
1004
+
1005
+ Returns the number of currently active modals.
1006
+
1007
+ ```typescript
1008
+ import { useActiveModalCount } from '@lerx/promise-modal';
1009
+
1010
+ function Component() {
1011
+ const count = useActiveModalCount();
1012
+
1013
+ return (
1014
+ <div>
1015
+ Currently open modals: {count}
1016
+ </div>
1017
+ );
1018
+ }
1019
+ ```
1020
+
1021
+ #### `useModalAnimation`
1022
+
1023
+ Provides modal animation state and control.
1024
+
1025
+ ```typescript
1026
+ import { useModalAnimation } from '@lerx/promise-modal';
1027
+
1028
+ function Component() {
1029
+ const { isAnimating, animate } = useModalAnimation();
1030
+
1031
+ // ...
1032
+ }
1033
+ ```
1034
+
1035
+ ### Type Definitions
1036
+
1037
+ The library provides various types for TypeScript compatibility:
1038
+
1039
+ #### `ModalFrameProps`
1040
+
1041
+ Properties passed to the modal frame component.
1042
+
1043
+ ```typescript
1044
+ interface ModalFrameProps<Context = any, B = any> {
1045
+ id: number;
1046
+ type: 'alert' | 'confirm' | 'prompt';
1047
+ alive: boolean;
1048
+ visible: boolean;
1049
+ initiator: string;
1050
+ manualDestroy: boolean;
1051
+ closeOnBackdropClick: boolean;
1052
+ background?: ModalBackground<B>;
1053
+ onConfirm: () => void;
1054
+ onClose: () => void;
1055
+ onChange: (value: any) => void;
1056
+ onDestroy: () => void;
1057
+ onChangeOrder: Function;
1058
+ context: Context;
1059
+ }
1060
+ ```
1061
+
1062
+ #### `FooterComponentProps`
1063
+
1064
+ Properties passed to the footer component.
1065
+
1066
+ ```typescript
1067
+ interface FooterComponentProps {
1068
+ onConfirm: () => void;
1069
+ onClose: () => void;
1070
+ // Other props...
1071
+ }
1072
+ ```
1073
+
1074
+ #### `PromptInputProps<T>`
1075
+
1076
+ Properties passed to the input component in prompt modals.
1077
+
1078
+ ```typescript
1079
+ interface PromptInputProps<T> {
1080
+ value: T;
1081
+ onChange: (value: T) => void;
1082
+ }
1083
+ ```
1084
+
1085
+ Additional types provided:
1086
+
1087
+ - `ModalBackground`: Modal background settings type
1088
+ - `AlertContentProps`: Alert modal content component props
1089
+ - `ConfirmContentProps`: Confirm modal content component props
1090
+ - `PromptContentProps`: Prompt modal content component props
1091
+ - `WrapperComponentProps`: Modal wrapper component props
38
1092
 
39
1093
  ---
40
1094
 
41
- ## Contact
1095
+ ## Advanced Usage Examples
1096
+
1097
+ ### 1. Nested Modals (Opening Modals inside Other Modals)
1098
+
1099
+ You can open modals inside other modals to create complex user interactions.
1100
+
1101
+ ```tsx
1102
+ import { alert, confirm, prompt } from '@lerx/promise-modal';
1103
+
1104
+ // Multi-step modal workflow example
1105
+ async function multiStepProcess() {
1106
+ // First modal: confirm to proceed
1107
+ const shouldProceed = await confirm({
1108
+ title: 'Start Task',
1109
+ content: 'This task involves multiple steps. Do you want to continue?',
1110
+ footer: {
1111
+ confirm: 'Proceed',
1112
+ cancel: 'Cancel',
1113
+ },
1114
+ });
42
1115
 
43
- For inquiries or suggestions related to the project, please create an issue.
1116
+ if (!shouldProceed) return;
1117
+
1118
+ // Second modal: get user input
1119
+ const userName = await prompt<string>({
1120
+ title: 'User Information',
1121
+ content: 'Please enter your name.',
1122
+ defaultValue: '',
1123
+ Input: ({ value, onChange }) => (
1124
+ <input
1125
+ value={value}
1126
+ onChange={(e) => onChange(e.target.value)}
1127
+ placeholder="Name"
1128
+ />
1129
+ ),
1130
+ });
1131
+
1132
+ if (!userName) return;
1133
+
1134
+ // Third modal: final confirmation
1135
+ const confirmed = await confirm({
1136
+ title: 'Final Confirmation',
1137
+ content: `${userName}, are you sure you want to proceed?`,
1138
+ subtype: 'warning',
1139
+ });
1140
+
1141
+ if (confirmed) {
1142
+ // Last modal: completion notification
1143
+ await alert({
1144
+ title: 'Complete',
1145
+ content: `${userName}, the task has been completed successfully.`,
1146
+ subtype: 'success',
1147
+ });
1148
+ }
1149
+ }
1150
+
1151
+ // Opening modal from within a footer
1152
+ async function nestedModalInFooter() {
1153
+ const result = await confirm({
1154
+ title: 'Confirmation Required',
1155
+ content: 'Do you want to proceed with this task?',
1156
+ // Custom footer that opens another modal
1157
+ footer: ({ onConfirm, onClose }) => {
1158
+ const handleConfirm = async () => {
1159
+ // Open another modal when confirm button is clicked
1160
+ const isConfirmed = await confirm({
1161
+ title: 'Final Confirmation',
1162
+ content: 'Are you really sure? This action cannot be undone.',
1163
+ closeOnBackdropClick: false,
1164
+ // Apply different design to second modal
1165
+ ForegroundComponent: CustomForegroundComponent,
1166
+ });
1167
+
1168
+ // Process first modal result based on second modal result
1169
+ if (isConfirmed) onConfirm();
1170
+ else onClose();
1171
+ };
1172
+
1173
+ return (
1174
+ <div>
1175
+ <button onClick={onClose}>Cancel</button>
1176
+ <button onClick={handleConfirm}>Next Step</button>
1177
+ </div>
1178
+ );
1179
+ },
1180
+ });
1181
+
1182
+ if (result) {
1183
+ console.log('Proceeding with the task.');
1184
+ }
1185
+ }
1186
+
1187
+ // Opening modal from within a prompt modal
1188
+ async function promptWithNestedModal() {
1189
+ const value = await prompt<{ title: string; description: string }>({
1190
+ title: 'Create Content',
1191
+ defaultValue: { title: '', description: '' },
1192
+ Input: ({ value, onChange }) => {
1193
+ // Show help modal when help button is clicked
1194
+ const showHelp = () => {
1195
+ alert({
1196
+ title: 'Help',
1197
+ content: 'Enter a title and description. Title is required.',
1198
+ });
1199
+ };
1200
+
1201
+ return (
1202
+ <div>
1203
+ <div>
1204
+ <label>Title</label>
1205
+ <input
1206
+ value={value.title}
1207
+ onChange={(e) => onChange({ ...value, title: e.target.value })}
1208
+ />
1209
+ </div>
1210
+ <div>
1211
+ <label>Description</label>
1212
+ <textarea
1213
+ value={value.description}
1214
+ onChange={(e) =>
1215
+ onChange({ ...value, description: e.target.value })
1216
+ }
1217
+ />
1218
+ </div>
1219
+ <button type="button" onClick={showHelp}>
1220
+ Help
1221
+ </button>
1222
+ </div>
1223
+ );
1224
+ },
1225
+ disabled: (value) => !value.title,
1226
+ });
1227
+
1228
+ console.log('Input value:', value);
1229
+ }
1230
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lerx/promise-modal",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vincent-kk/albatrion.git",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@emotion/css": "^11.0.0",
51
- "@winglet/common-utils": "^0.0.59",
52
- "@winglet/react-utils": "^0.0.59"
51
+ "@winglet/common-utils": "^0.0.61",
52
+ "@winglet/react-utils": "^0.0.61"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@babel/core": "^7.26.0",