@lerx/promise-modal 0.2.8 → 0.3.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/dist/bootstrap/BootstrapProvider/BootstrapProvider.d.ts +115 -0
- package/dist/bootstrap/BootstrapProvider/useBootstrap.d.ts +182 -0
- package/dist/core/handle/alert.d.ts +144 -3
- package/dist/core/handle/confirm.d.ts +150 -3
- package/dist/core/handle/prompt.d.ts +219 -7
- package/dist/hooks/useActiveModalCount.d.ts +163 -0
- package/dist/hooks/useDestroyAfter.d.ts +127 -0
- package/dist/hooks/useModalAnimation.d.ts +212 -0
- package/dist/hooks/useSubscribeModal.d.ts +114 -0
- package/dist/index.cjs +12 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +13 -13
- package/dist/providers/ConfigurationContext/useConfigurationContext.d.ts +273 -0
- package/package.json +9 -10
|
@@ -1,21 +1,233 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from 'react';
|
|
2
2
|
import type { BackgroundComponent, FooterOptions, ForegroundComponent, ModalBackground, PromptContentProps, PromptFooterRender, PromptInputProps } from '../../types';
|
|
3
|
-
interface PromptProps<
|
|
3
|
+
interface PromptProps<InputValue, BackgroundValue = any> {
|
|
4
4
|
group?: string;
|
|
5
5
|
title?: ReactNode;
|
|
6
6
|
subtitle?: ReactNode;
|
|
7
7
|
content?: ReactNode | ComponentType<PromptContentProps>;
|
|
8
|
-
defaultValue?:
|
|
9
|
-
Input: (props: PromptInputProps<
|
|
10
|
-
disabled?: (value:
|
|
8
|
+
defaultValue?: InputValue;
|
|
9
|
+
Input: (props: PromptInputProps<InputValue>) => ReactNode;
|
|
10
|
+
disabled?: (value: InputValue) => boolean;
|
|
11
11
|
returnOnCancel?: boolean;
|
|
12
|
-
background?: ModalBackground<
|
|
13
|
-
footer?: PromptFooterRender<
|
|
12
|
+
background?: ModalBackground<BackgroundValue>;
|
|
13
|
+
footer?: PromptFooterRender<InputValue> | FooterOptions | false;
|
|
14
14
|
dimmed?: boolean;
|
|
15
15
|
manualDestroy?: boolean;
|
|
16
16
|
closeOnBackdropClick?: boolean;
|
|
17
17
|
ForegroundComponent?: ForegroundComponent;
|
|
18
18
|
BackgroundComponent?: BackgroundComponent;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Displays a promise-based prompt modal that collects user input.
|
|
22
|
+
*
|
|
23
|
+
* Creates a modal dialog with a custom input component and confirmation/cancel buttons.
|
|
24
|
+
* The promise resolves with the input value when confirmed, or rejects when cancelled
|
|
25
|
+
* (unless `returnOnCancel` is true).
|
|
26
|
+
*
|
|
27
|
+
* @typeParam InputValue - Type of the value collected from user input
|
|
28
|
+
* @typeParam BackgroundValue - Type of background data passed to BackgroundComponent
|
|
29
|
+
* @param props - Prompt dialog configuration options
|
|
30
|
+
* @returns Promise that resolves with the user's input value
|
|
31
|
+
* @throws Rejects when cancelled (unless returnOnCancel is true)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Basic text input:
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const name = await prompt<string>({
|
|
37
|
+
* title: 'Enter Your Name',
|
|
38
|
+
* content: 'Please enter your full name for the certificate.',
|
|
39
|
+
* Input: ({ value, onChange, onConfirm }) => (
|
|
40
|
+
* <input
|
|
41
|
+
* type="text"
|
|
42
|
+
* value={value || ''}
|
|
43
|
+
* onChange={(e) => onChange(e.target.value)}
|
|
44
|
+
* onKeyPress={(e) => e.key === 'Enter' && onConfirm()}
|
|
45
|
+
* autoFocus
|
|
46
|
+
* />
|
|
47
|
+
* ),
|
|
48
|
+
* defaultValue: 'John Doe',
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* Number input with validation:
|
|
54
|
+
* ```tsx
|
|
55
|
+
* const age = await prompt<number>({
|
|
56
|
+
* title: 'Enter Your Age',
|
|
57
|
+
* content: 'Must be between 18 and 100',
|
|
58
|
+
* Input: ({ value, onChange }) => (
|
|
59
|
+
* <input
|
|
60
|
+
* type="number"
|
|
61
|
+
* min="18"
|
|
62
|
+
* max="100"
|
|
63
|
+
* value={value || ''}
|
|
64
|
+
* onChange={(e) => onChange(parseInt(e.target.value))}
|
|
65
|
+
* />
|
|
66
|
+
* ),
|
|
67
|
+
* disabled: (value) => !value || value < 18 || value > 100,
|
|
68
|
+
* defaultValue: 25,
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* Select dropdown:
|
|
74
|
+
* ```tsx
|
|
75
|
+
* interface Theme {
|
|
76
|
+
* id: string;
|
|
77
|
+
* name: string;
|
|
78
|
+
* primary: string;
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* const theme = await prompt<Theme>({
|
|
82
|
+
* title: 'Choose Theme',
|
|
83
|
+
* Input: ({ value, onChange }) => (
|
|
84
|
+
* <select
|
|
85
|
+
* value={value?.id || ''}
|
|
86
|
+
* onChange={(e) => {
|
|
87
|
+
* const selected = themes.find(t => t.id === e.target.value);
|
|
88
|
+
* onChange(selected);
|
|
89
|
+
* }}
|
|
90
|
+
* >
|
|
91
|
+
* <option value="">Select a theme...</option>
|
|
92
|
+
* {themes.map(theme => (
|
|
93
|
+
* <option key={theme.id} value={theme.id}>
|
|
94
|
+
* {theme.name}
|
|
95
|
+
* </option>
|
|
96
|
+
* ))}
|
|
97
|
+
* </select>
|
|
98
|
+
* ),
|
|
99
|
+
* disabled: (value) => !value,
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* Multi-field form:
|
|
105
|
+
* ```tsx
|
|
106
|
+
* interface UserData {
|
|
107
|
+
* username: string;
|
|
108
|
+
* email: string;
|
|
109
|
+
* subscribe: boolean;
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* const userData = await prompt<UserData>({
|
|
113
|
+
* title: 'Create Account',
|
|
114
|
+
* Input: ({ value, onChange }) => {
|
|
115
|
+
* const data = value || { username: '', email: '', subscribe: false };
|
|
116
|
+
*
|
|
117
|
+
* return (
|
|
118
|
+
* <div className="form">
|
|
119
|
+
* <input
|
|
120
|
+
* placeholder="Username"
|
|
121
|
+
* value={data.username}
|
|
122
|
+
* onChange={(e) => onChange({ ...data, username: e.target.value })}
|
|
123
|
+
* />
|
|
124
|
+
* <input
|
|
125
|
+
* type="email"
|
|
126
|
+
* placeholder="Email"
|
|
127
|
+
* value={data.email}
|
|
128
|
+
* onChange={(e) => onChange({ ...data, email: e.target.value })}
|
|
129
|
+
* />
|
|
130
|
+
* <label>
|
|
131
|
+
* <input
|
|
132
|
+
* type="checkbox"
|
|
133
|
+
* checked={data.subscribe}
|
|
134
|
+
* onChange={(e) => onChange({ ...data, subscribe: e.target.checked })}
|
|
135
|
+
* />
|
|
136
|
+
* Subscribe to newsletter
|
|
137
|
+
* </label>
|
|
138
|
+
* </div>
|
|
139
|
+
* );
|
|
140
|
+
* },
|
|
141
|
+
* disabled: (value) => !value?.username || !value?.email,
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* File upload:
|
|
147
|
+
* ```tsx
|
|
148
|
+
* const file = await prompt<File>({
|
|
149
|
+
* title: 'Upload Avatar',
|
|
150
|
+
* content: 'Select an image file (max 5MB)',
|
|
151
|
+
* Input: ({ value, onChange }) => (
|
|
152
|
+
* <div>
|
|
153
|
+
* <input
|
|
154
|
+
* type="file"
|
|
155
|
+
* accept="image/*"
|
|
156
|
+
* onChange={(e) => {
|
|
157
|
+
* const file = e.target.files?.[0];
|
|
158
|
+
* if (file && file.size <= 5 * 1024 * 1024) {
|
|
159
|
+
* onChange(file);
|
|
160
|
+
* }
|
|
161
|
+
* }}
|
|
162
|
+
* />
|
|
163
|
+
* {value && (
|
|
164
|
+
* <div>
|
|
165
|
+
* <img src={URL.createObjectURL(value)} alt="Preview" />
|
|
166
|
+
* <p>{value.name} ({(value.size / 1024).toFixed(1)} KB)</p>
|
|
167
|
+
* </div>
|
|
168
|
+
* )}
|
|
169
|
+
* </div>
|
|
170
|
+
* ),
|
|
171
|
+
* disabled: (value) => !value,
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* With error handling:
|
|
177
|
+
* ```tsx
|
|
178
|
+
* try {
|
|
179
|
+
* const email = await prompt<string>({
|
|
180
|
+
* title: 'Reset Password',
|
|
181
|
+
* content: 'Enter your email to receive a reset link',
|
|
182
|
+
* Input: ({ value, onChange, context }) => (
|
|
183
|
+
* <EmailInput
|
|
184
|
+
* value={value}
|
|
185
|
+
* onChange={onChange}
|
|
186
|
+
* error={context.error}
|
|
187
|
+
* />
|
|
188
|
+
* ),
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* await sendResetEmail(email);
|
|
192
|
+
* } catch (error) {
|
|
193
|
+
* // User cancelled the prompt
|
|
194
|
+
* console.log('Password reset cancelled');
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* Custom footer with validation:
|
|
200
|
+
* ```tsx
|
|
201
|
+
* const password = await prompt<string>({
|
|
202
|
+
* title: 'Set New Password',
|
|
203
|
+
* Input: ({ value, onChange }) => (
|
|
204
|
+
* <PasswordInput value={value} onChange={onChange} />
|
|
205
|
+
* ),
|
|
206
|
+
* footer: ({ value, onChange, onConfirm, onCancel, disabled }) => (
|
|
207
|
+
* <div>
|
|
208
|
+
* <PasswordStrength password={value} />
|
|
209
|
+
* <div className="buttons">
|
|
210
|
+
* <button onClick={onCancel}>Cancel</button>
|
|
211
|
+
* <button
|
|
212
|
+
* onClick={onConfirm}
|
|
213
|
+
* disabled={disabled}
|
|
214
|
+
* className={disabled ? 'disabled' : 'primary'}
|
|
215
|
+
* >
|
|
216
|
+
* Set Password
|
|
217
|
+
* </button>
|
|
218
|
+
* </div>
|
|
219
|
+
* </div>
|
|
220
|
+
* ),
|
|
221
|
+
* disabled: (value) => !value || value.length < 8,
|
|
222
|
+
* });
|
|
223
|
+
* ```
|
|
224
|
+
*
|
|
225
|
+
* @remarks
|
|
226
|
+
* - The Input component receives value, onChange, onConfirm, onCancel, and context props
|
|
227
|
+
* - Use the `disabled` function to control when the confirm button is enabled
|
|
228
|
+
* - Set `returnOnCancel: true` to return defaultValue instead of rejecting on cancel
|
|
229
|
+
* - The promise rejects when cancelled (unless returnOnCancel is set)
|
|
230
|
+
* - Use onConfirm prop in Input to handle Enter key submission
|
|
231
|
+
*/
|
|
232
|
+
export declare const prompt: <InputValue, BackgroundValue = any>({ group, title, subtitle, content, defaultValue, Input, disabled, returnOnCancel, background, footer, dimmed, manualDestroy, closeOnBackdropClick, ForegroundComponent, BackgroundComponent, }: PromptProps<InputValue, BackgroundValue>) => Promise<InputValue>;
|
|
21
233
|
export {};
|
|
@@ -1,3 +1,166 @@
|
|
|
1
1
|
import type { Fn } from '../@aileron/declare';
|
|
2
2
|
import type { ModalNode } from '../core';
|
|
3
|
+
/**
|
|
4
|
+
* Hook that counts the number of active modals based on a validation function.
|
|
5
|
+
*
|
|
6
|
+
* Provides a reactive count of modals that match specific criteria. By default,
|
|
7
|
+
* counts visible modals, but can be customized with any validation logic.
|
|
8
|
+
* Useful for managing overlays, z-index stacking, or conditional UI based on modal count.
|
|
9
|
+
*
|
|
10
|
+
* @param validate - Function to determine if a modal should be counted (default: checks visibility)
|
|
11
|
+
* @param refreshKey - Optional key to force recalculation when changed
|
|
12
|
+
* @returns Number of modals that pass the validation
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* Basic usage - count visible modals:
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function ModalOverlay() {
|
|
18
|
+
* const activeCount = useActiveModalCount();
|
|
19
|
+
*
|
|
20
|
+
* if (activeCount === 0) return null;
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <div
|
|
24
|
+
* className="modal-backdrop"
|
|
25
|
+
* style={{
|
|
26
|
+
* opacity: Math.min(activeCount * 0.3, 0.8),
|
|
27
|
+
* zIndex: 1000 + activeCount
|
|
28
|
+
* }}
|
|
29
|
+
* />
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* Count modals by type:
|
|
36
|
+
* ```tsx
|
|
37
|
+
* function ModalStats() {
|
|
38
|
+
* const alertCount = useActiveModalCount(
|
|
39
|
+
* (modal) => modal?.type === 'alert' && modal.visible
|
|
40
|
+
* );
|
|
41
|
+
*
|
|
42
|
+
* const confirmCount = useActiveModalCount(
|
|
43
|
+
* (modal) => modal?.type === 'confirm' && modal.visible
|
|
44
|
+
* );
|
|
45
|
+
*
|
|
46
|
+
* const promptCount = useActiveModalCount(
|
|
47
|
+
* (modal) => modal?.type === 'prompt' && modal.visible
|
|
48
|
+
* );
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <div className="modal-stats">
|
|
52
|
+
* <p>Alerts: {alertCount}</p>
|
|
53
|
+
* <p>Confirms: {confirmCount}</p>
|
|
54
|
+
* <p>Prompts: {promptCount}</p>
|
|
55
|
+
* </div>
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* Prevent interactions when modals are active:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* function App() {
|
|
64
|
+
* const hasActiveModals = useActiveModalCount() > 0;
|
|
65
|
+
*
|
|
66
|
+
* return (
|
|
67
|
+
* <div className={hasActiveModals ? 'app-disabled' : 'app'}>
|
|
68
|
+
* <nav className={hasActiveModals ? 'pointer-events-none' : ''}>
|
|
69
|
+
* <button disabled={hasActiveModals}>Navigate</button>
|
|
70
|
+
* </nav>
|
|
71
|
+
* <main inert={hasActiveModals}>
|
|
72
|
+
* // Main content
|
|
73
|
+
* </main>
|
|
74
|
+
* </div>
|
|
75
|
+
* );
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* Count alive modals (including hidden ones):
|
|
81
|
+
* ```tsx
|
|
82
|
+
* function ModalMemoryMonitor() {
|
|
83
|
+
* const aliveCount = useActiveModalCount(
|
|
84
|
+
* (modal) => modal?.alive === true
|
|
85
|
+
* );
|
|
86
|
+
*
|
|
87
|
+
* const hiddenCount = useActiveModalCount(
|
|
88
|
+
* (modal) => modal?.alive && !modal.visible
|
|
89
|
+
* );
|
|
90
|
+
*
|
|
91
|
+
* return (
|
|
92
|
+
* <div className="debug-panel">
|
|
93
|
+
* <p>Total alive modals: {aliveCount}</p>
|
|
94
|
+
* <p>Hidden (animating out): {hiddenCount}</p>
|
|
95
|
+
* <button
|
|
96
|
+
* onClick={cleanupHiddenModals}
|
|
97
|
+
* disabled={hiddenCount === 0}
|
|
98
|
+
* >
|
|
99
|
+
* Cleanup Hidden Modals
|
|
100
|
+
* </button>
|
|
101
|
+
* </div>
|
|
102
|
+
* );
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* Dynamic refresh with dependencies:
|
|
108
|
+
* ```tsx
|
|
109
|
+
* function FilteredModalCount({ filter }) {
|
|
110
|
+
* const [refreshKey, setRefreshKey] = useState(0);
|
|
111
|
+
*
|
|
112
|
+
* // Count modals matching dynamic filter
|
|
113
|
+
* const count = useActiveModalCount(
|
|
114
|
+
* (modal) => {
|
|
115
|
+
* if (!modal?.visible) return false;
|
|
116
|
+
* if (filter.type && modal.type !== filter.type) return false;
|
|
117
|
+
* if (filter.group && modal.group !== filter.group) return false;
|
|
118
|
+
* return true;
|
|
119
|
+
* },
|
|
120
|
+
* refreshKey // Force recalculation when key changes
|
|
121
|
+
* );
|
|
122
|
+
*
|
|
123
|
+
* // Refresh count when filter changes
|
|
124
|
+
* useEffect(() => {
|
|
125
|
+
* setRefreshKey(prev => prev + 1);
|
|
126
|
+
* }, [filter]);
|
|
127
|
+
*
|
|
128
|
+
* return <div>Matching modals: {count}</div>;
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* Limit modal stacking:
|
|
134
|
+
* ```tsx
|
|
135
|
+
* function LimitedModalProvider({ children, maxModals = 3 }) {
|
|
136
|
+
* const activeCount = useActiveModalCount();
|
|
137
|
+
*
|
|
138
|
+
* const openModal = useCallback((modalConfig) => {
|
|
139
|
+
* if (activeCount >= maxModals) {
|
|
140
|
+
* alert({
|
|
141
|
+
* title: 'Too Many Modals',
|
|
142
|
+
* content: `Maximum of ${maxModals} modals can be open at once.`,
|
|
143
|
+
* subtype: 'warning',
|
|
144
|
+
* });
|
|
145
|
+
* return Promise.reject(new Error('Modal limit exceeded'));
|
|
146
|
+
* }
|
|
147
|
+
*
|
|
148
|
+
* return originalOpenModal(modalConfig);
|
|
149
|
+
* }, [activeCount, maxModals]);
|
|
150
|
+
*
|
|
151
|
+
* return (
|
|
152
|
+
* <ModalContext.Provider value={{ openModal }}>
|
|
153
|
+
* {children}
|
|
154
|
+
* </ModalContext.Provider>
|
|
155
|
+
* );
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* - Recalculates whenever modal list changes or refreshKey updates
|
|
161
|
+
* - Default validation counts visible modals only
|
|
162
|
+
* - Custom validation can check any modal properties
|
|
163
|
+
* - Efficient memoization prevents unnecessary recalculations
|
|
164
|
+
* - Use refreshKey to force updates based on external dependencies
|
|
165
|
+
*/
|
|
3
166
|
export declare const useActiveModalCount: (validate?: Fn<[node?: ModalNode], boolean | undefined>, refreshKey?: string | number) => number;
|
|
@@ -1,3 +1,130 @@
|
|
|
1
1
|
import type { Duration } from '../@aileron/declare';
|
|
2
2
|
import type { ModalNode } from '../core';
|
|
3
|
+
/**
|
|
4
|
+
* Hook that automatically destroys a modal after it becomes hidden.
|
|
5
|
+
*
|
|
6
|
+
* Monitors the modal's visibility state and schedules destruction after a specified
|
|
7
|
+
* duration once the modal is hidden but still alive. Useful for cleanup after
|
|
8
|
+
* closing animations complete.
|
|
9
|
+
*
|
|
10
|
+
* @param modalId - ID of the modal to monitor
|
|
11
|
+
* @param duration - Delay before destruction (ms or duration string like '300ms')
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* Basic usage with milliseconds:
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function AnimatedModal({ modalId }) {
|
|
17
|
+
* // Destroy modal 300ms after it becomes hidden
|
|
18
|
+
* useDestroyAfter(modalId, 300);
|
|
19
|
+
*
|
|
20
|
+
* return (
|
|
21
|
+
* <div className="modal-with-exit-animation">
|
|
22
|
+
* // Modal content
|
|
23
|
+
* </div>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* Using duration string:
|
|
30
|
+
* ```tsx
|
|
31
|
+
* function FadeOutModal({ modalId }) {
|
|
32
|
+
* // Destroy after CSS animation completes
|
|
33
|
+
* useDestroyAfter(modalId, '500ms');
|
|
34
|
+
*
|
|
35
|
+
* const { modal } = useModal(modalId);
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <div
|
|
39
|
+
* className={modal?.visible ? 'fade-in' : 'fade-out'}
|
|
40
|
+
* style={{ animationDuration: '500ms' }}
|
|
41
|
+
* >
|
|
42
|
+
* // Content
|
|
43
|
+
* </div>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* Conditional destruction based on modal type:
|
|
50
|
+
* ```tsx
|
|
51
|
+
* function SmartModal({ modalId }) {
|
|
52
|
+
* const { modal } = useModal(modalId);
|
|
53
|
+
*
|
|
54
|
+
* // Different timings for different modal types
|
|
55
|
+
* const destroyDelay = useMemo(() => {
|
|
56
|
+
* switch (modal?.type) {
|
|
57
|
+
* case 'alert': return '200ms';
|
|
58
|
+
* case 'confirm': return '300ms';
|
|
59
|
+
* case 'prompt': return '400ms';
|
|
60
|
+
* default: return 300;
|
|
61
|
+
* }
|
|
62
|
+
* }, [modal?.type]);
|
|
63
|
+
*
|
|
64
|
+
* useDestroyAfter(modalId, destroyDelay);
|
|
65
|
+
*
|
|
66
|
+
* return <ModalContent modalId={modalId} />;
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* With custom animations and effects:
|
|
72
|
+
* ```tsx
|
|
73
|
+
* function NotificationModal({ modalId }) {
|
|
74
|
+
* const { modal } = useModal(modalId);
|
|
75
|
+
* const [particles, setParticles] = useState([]);
|
|
76
|
+
*
|
|
77
|
+
* // Cleanup after particle animation
|
|
78
|
+
* useDestroyAfter(modalId, '1000ms');
|
|
79
|
+
*
|
|
80
|
+
* useEffect(() => {
|
|
81
|
+
* if (!modal?.visible && modal?.alive) {
|
|
82
|
+
* // Trigger particle effect on close
|
|
83
|
+
* setParticles(generateParticles());
|
|
84
|
+
* }
|
|
85
|
+
* }, [modal?.visible, modal?.alive]);
|
|
86
|
+
*
|
|
87
|
+
* return (
|
|
88
|
+
* <>
|
|
89
|
+
* <div className={`notification ${!modal?.visible ? 'explode' : ''}`}>
|
|
90
|
+
* // Content
|
|
91
|
+
* </div>
|
|
92
|
+
* {particles.map(particle => (
|
|
93
|
+
* <Particle key={particle.id} {...particle} />
|
|
94
|
+
* ))}
|
|
95
|
+
* </>
|
|
96
|
+
* );
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* Coordinated with manual destruction:
|
|
102
|
+
* ```tsx
|
|
103
|
+
* function PersistentModal({ modalId }) {
|
|
104
|
+
* const { modal } = useModal(modalId);
|
|
105
|
+
* const [shouldPersist, setShouldPersist] = useState(false);
|
|
106
|
+
*
|
|
107
|
+
* // Only auto-destroy if not persisting
|
|
108
|
+
* if (!shouldPersist) {
|
|
109
|
+
* useDestroyAfter(modalId, '300ms');
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* return (
|
|
113
|
+
* <div>
|
|
114
|
+
* <button onClick={() => setShouldPersist(true)}>
|
|
115
|
+
* Keep in Background
|
|
116
|
+
* </button>
|
|
117
|
+
* // Modal content
|
|
118
|
+
* </div>
|
|
119
|
+
* );
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* - Only triggers when modal is hidden (visible: false) but still alive
|
|
125
|
+
* - Automatically cancels if modal becomes visible again
|
|
126
|
+
* - Useful for cleanup after exit animations
|
|
127
|
+
* - Works with both millisecond numbers and duration strings
|
|
128
|
+
* - The timer resets if modal visibility changes
|
|
129
|
+
*/
|
|
3
130
|
export declare const useDestroyAfter: (modalId: ModalNode["id"], duration: Duration | number) => void;
|