@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,171 @@
1
+ ---
2
+ name: promise-modal-expert
3
+ description: "@lerx/promise-modal library expert. Guide users on React-based Promise modal utilities (alert, confirm, prompt), architecture, and customization."
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Promise Modal Expert Skill
8
+
9
+ ## Role
10
+
11
+ You are an expert on the `@lerx/promise-modal` library. Help users effectively implement modal dialogs using React-based Promise modal utilities.
12
+
13
+ ## Core Knowledge
14
+
15
+ ### Library Overview
16
+
17
+ `@lerx/promise-modal` is a universal modal utility for React that provides:
18
+ - Promise-based modal interactions (alert, confirm, prompt)
19
+ - Usage both inside and outside React components
20
+ - High-level component customization
21
+ - Automatic lifecycle management
22
+ - Programmatic modal cancellation via AbortSignal
23
+
24
+ ### Architecture
25
+
26
+ The library uses a layered architecture:
27
+
28
+ 1. **Core Layer** - Main API functions (alert, confirm, prompt)
29
+ 2. **Application Layer** - ModalManager singleton for lifecycle and DOM management
30
+ 3. **Bootstrap Layer** - ModalProvider component for initialization
31
+ 4. **Provider Layer** - Context providers for configuration and state
32
+ 5. **Component Layer** - Customizable UI components
33
+
34
+ ### Configuration Priority
35
+
36
+ Configuration is applied hierarchically, with lower levels overriding higher levels:
37
+
38
+ ```
39
+ Provider Config (Lowest) < Hook Config < Handler Config (Highest)
40
+ ```
41
+
42
+ | Level | Location | Description |
43
+ |------|------|------|
44
+ | **Provider** | `ModalProvider` props | App-wide default configuration |
45
+ | **Hook** | `useModal(config)` | Component-level configuration |
46
+ | **Handler** | `alert/confirm/prompt(options)` | Individual modal configuration |
47
+
48
+ ---
49
+
50
+ ## Basic Usage Patterns
51
+
52
+ ### Pattern 1: Basic Static API
53
+
54
+ Static functions usable outside React components.
55
+
56
+ ```typescript
57
+ import { alert, confirm, prompt } from '@lerx/promise-modal';
58
+
59
+ // Simple alert
60
+ await alert({
61
+ title: 'Alert',
62
+ content: 'Task completed successfully.',
63
+ subtype: 'success'
64
+ });
65
+
66
+ // Confirmation modal
67
+ if (await confirm({
68
+ title: 'Confirm',
69
+ content: 'Are you sure you want to delete this?'
70
+ })) {
71
+ // User confirmed
72
+ }
73
+
74
+ // Prompt input
75
+ const name = await prompt({
76
+ title: 'Enter Name',
77
+ defaultValue: '',
78
+ Input: ({ value, onChange }) => (
79
+ <input
80
+ value={value}
81
+ onChange={(e) => onChange(e.target.value)}
82
+ placeholder="Enter your name"
83
+ />
84
+ ),
85
+ });
86
+ ```
87
+
88
+ ### Pattern 2: Component-Scoped with useModal
89
+
90
+ Tied to component lifecycle, automatically cleaned up on unmount.
91
+
92
+ ```typescript
93
+ import { useModal } from '@lerx/promise-modal';
94
+
95
+ function MyComponent() {
96
+ const modal = useModal({
97
+ ForegroundComponent: CustomForeground, // Hook-level config
98
+ });
99
+
100
+ const handleDelete = async () => {
101
+ if (await modal.confirm({ content: 'Delete this item?' })) {
102
+ // Delete logic
103
+ }
104
+ };
105
+
106
+ return <button onClick={handleDelete}>Delete</button>;
107
+ }
108
+ ```
109
+
110
+ **Key Differences**:
111
+
112
+ | Feature | Static Handlers | useModal Hook |
113
+ |------|------------|-------------|
114
+ | Lifecycle | Independent | Tied to component |
115
+ | Cleanup | Manual | Auto on unmount |
116
+ | Usage Location | Anywhere | Inside React components |
117
+
118
+ ---
119
+
120
+ ## Troubleshooting
121
+
122
+ ### Modal Not Appearing
123
+
124
+ 1. Verify `ModalProvider` is at app root
125
+ 2. (For manual mode) Ensure `initialize()` was called
126
+ 3. Check for z-index and CSS conflicts
127
+
128
+ ### Modal Not Closing
129
+
130
+ 1. Check `manualDestroy` option
131
+ 2. Verify `closeOnBackdropClick` configuration
132
+ 3. Ensure `onClose` or `onConfirm` is being called
133
+
134
+ ### Animation Not Working
135
+
136
+ 1. Verify `useModalAnimation` hook is used correctly
137
+ 2. Check CSS transition properties
138
+ 3. Confirm Provider's `duration` option
139
+
140
+ ### prompt Type Errors
141
+
142
+ 1. Specify generic type: `prompt<string>(...)`
143
+ 2. Ensure `defaultValue` matches type
144
+ 3. Check `onChange` handler type
145
+
146
+ ---
147
+
148
+ ## Best Practices
149
+
150
+ 1. **Place ModalProvider at Root**: Wrap entire app
151
+ 2. **Use useModal in Components**: For automatic cleanup
152
+ 3. **Use Static API in Utilities**: For non-component code
153
+ 4. **Customize at Provider Level**: Set global styles once
154
+ 5. **Use subtype Semantically**: info, success, warning, error
155
+ 6. **Handle Promises Properly**: Always await or handle rejection
156
+ 7. **Keep Modal Content Simple**: Avoid complex state in modals
157
+ 8. **Test Accessibility**: Verify keyboard navigation
158
+ 9. **Leverage AbortSignal**: When programmatic modal closure is needed
159
+
160
+ ---
161
+
162
+ ## Knowledge Files Reference
163
+
164
+ For detailed API, advanced patterns, and type definitions, refer to these knowledge files:
165
+
166
+ | File | Contents |
167
+ |------|------|
168
+ | `knowledge/api-reference.md` | Static functions (alert, confirm, prompt) and ModalProvider detailed API |
169
+ | `knowledge/hooks-reference.md` | Complete reference for 8 hooks (useModal, useActiveModalCount, etc.) |
170
+ | `knowledge/advanced-patterns.md` | Advanced patterns including AbortSignal, toast, nested modals, custom anchors |
171
+ | `knowledge/type-definitions.md` | TypeScript interface definitions (ModalFrameProps, etc.) |
@@ -0,0 +1,294 @@
1
+ # Advanced Patterns
2
+
3
+ ## Pattern 3: Modal Cancellation with AbortSignal
4
+
5
+ Programmatically cancel modals.
6
+
7
+ ```typescript
8
+ import { alert } from '@lerx/promise-modal';
9
+ import { useState } from 'react';
10
+
11
+ function ManualAbortControl() {
12
+ const [controller, setController] = useState<AbortController | null>(null);
13
+
14
+ const handleOpen = () => {
15
+ const newController = new AbortController();
16
+ setController(newController);
17
+
18
+ alert({
19
+ title: 'Manually Cancelable',
20
+ content: 'Click "Cancel" button to close this modal.',
21
+ signal: newController.signal,
22
+ closeOnBackdropClick: false,
23
+ }).then(() => {
24
+ setController(null);
25
+ });
26
+ };
27
+
28
+ const handleAbort = () => {
29
+ if (controller) {
30
+ controller.abort();
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div>
36
+ <button onClick={handleOpen} disabled={!!controller}>Open Modal</button>
37
+ <button onClick={handleAbort} disabled={!controller}>Cancel Modal</button>
38
+ </div>
39
+ );
40
+ }
41
+ ```
42
+
43
+ **Use Cases**:
44
+ - Timer-based auto-close
45
+ - Close modals in response to external events
46
+ - Programmatic modal control
47
+
48
+ ---
49
+
50
+ ## Pattern 4: Toast Implementation
51
+
52
+ Implement auto-dismissing toast notifications.
53
+
54
+ ```typescript
55
+ import { alert, useModalAnimation, useModalDuration, useDestroyAfter } from '@lerx/promise-modal';
56
+ import { useRef, useEffect } from 'react';
57
+
58
+ const ToastForeground = ({ id, visible, children, onClose, onDestroy }) => {
59
+ const ref = useRef(null);
60
+ const { duration } = useModalDuration();
61
+
62
+ // Auto-close after 3 seconds
63
+ useEffect(() => {
64
+ const timer = setTimeout(onClose, 3000);
65
+ return () => clearTimeout(timer);
66
+ }, [onClose]);
67
+
68
+ // Handle animations
69
+ useModalAnimation(visible, {
70
+ onVisible: () => ref.current?.classList.add('visible'),
71
+ onHidden: () => ref.current?.classList.remove('visible'),
72
+ });
73
+
74
+ // Remove from DOM after animation
75
+ useDestroyAfter(id, duration);
76
+
77
+ return <div ref={ref}>{children}</div>;
78
+ };
79
+
80
+ // Pattern for removing previous toast
81
+ let onDestroyPrevToast: () => void;
82
+
83
+ export const toast = (message: ReactNode, duration = 1250) => {
84
+ onDestroyPrevToast?.(); // Remove previous toast
85
+
86
+ return alert({
87
+ content: message,
88
+ ForegroundComponent: (props) => {
89
+ onDestroyPrevToast = props.onDestroy;
90
+ return <ToastForeground {...props} />;
91
+ },
92
+ footer: false,
93
+ dimmed: false,
94
+ closeOnBackdropClick: false,
95
+ });
96
+ };
97
+ ```
98
+
99
+ **Usage Example**:
100
+ ```typescript
101
+ toast('Task completed successfully!');
102
+ toast('An error occurred.', 2000);
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Pattern 5: Nested Modals
108
+
109
+ Display multiple modals sequentially in steps.
110
+
111
+ ```typescript
112
+ import { alert, confirm, prompt } from '@lerx/promise-modal';
113
+
114
+ async function multiStepProcess() {
115
+ // Step 1: Start confirmation
116
+ if (!await confirm({
117
+ title: 'Start?',
118
+ content: 'Do you want to continue?'
119
+ })) return;
120
+
121
+ // Step 2: Collect user info
122
+ const name = await prompt({
123
+ title: 'Name',
124
+ defaultValue: '',
125
+ Input: ({ value, onChange }) => (
126
+ <input
127
+ value={value}
128
+ onChange={(e) => onChange(e.target.value)}
129
+ placeholder="Enter your name"
130
+ />
131
+ ),
132
+ });
133
+
134
+ if (!name) return;
135
+
136
+ // Step 3: Completion notification
137
+ await alert({
138
+ title: 'Complete',
139
+ content: `Hello, ${name}!`,
140
+ subtype: 'success',
141
+ });
142
+ }
143
+ ```
144
+
145
+ **Notes**:
146
+ - Each modal opens after the previous one closes
147
+ - Natural flow through Promise chaining
148
+ - Entire process stops if user cancels
149
+
150
+ ---
151
+
152
+ ## Pattern 6: Custom Anchor
153
+
154
+ Render modals inside a specific DOM element.
155
+
156
+ ```typescript
157
+ import { useInitializeModal } from '@lerx/promise-modal';
158
+ import { useRef, useEffect } from 'react';
159
+
160
+ function CustomAnchorExample() {
161
+ const { initialize, portal } = useInitializeModal({ mode: 'manual' });
162
+ const containerRef = useRef(null);
163
+
164
+ useEffect(() => {
165
+ if (containerRef.current) {
166
+ initialize(containerRef.current);
167
+ }
168
+ }, [initialize]);
169
+
170
+ return (
171
+ <div>
172
+ <h1>Custom Modal Container</h1>
173
+ <div
174
+ ref={containerRef}
175
+ style={{
176
+ position: 'relative',
177
+ height: 500,
178
+ border: '2px solid #ccc',
179
+ }}
180
+ />
181
+ {portal}
182
+ </div>
183
+ );
184
+ }
185
+ ```
186
+
187
+ **Use Cases**:
188
+ - Restrict modals to specific area
189
+ - Manage multiple modal containers
190
+ - Use modals inside iframe or Shadow DOM
191
+
192
+ ---
193
+
194
+ ## Pattern 7: Complex Form Input
195
+
196
+ Collect complex form data using prompt.
197
+
198
+ ```typescript
199
+ import { prompt } from '@lerx/promise-modal';
200
+
201
+ interface UserForm {
202
+ name: string;
203
+ email: string;
204
+ age: number;
205
+ agree: boolean;
206
+ }
207
+
208
+ async function collectUserInfo() {
209
+ const userInfo = await prompt<UserForm>({
210
+ title: 'Enter User Information',
211
+ defaultValue: {
212
+ name: '',
213
+ email: '',
214
+ age: 18,
215
+ agree: false,
216
+ },
217
+ Input: ({ value, onChange }) => (
218
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
219
+ <input
220
+ type="text"
221
+ placeholder="Name"
222
+ value={value.name}
223
+ onChange={(e) => onChange({ ...value, name: e.target.value })}
224
+ />
225
+ <input
226
+ type="email"
227
+ placeholder="Email"
228
+ value={value.email}
229
+ onChange={(e) => onChange({ ...value, email: e.target.value })}
230
+ />
231
+ <input
232
+ type="number"
233
+ placeholder="Age"
234
+ value={value.age}
235
+ onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
236
+ />
237
+ <label>
238
+ <input
239
+ type="checkbox"
240
+ checked={value.agree}
241
+ onChange={(e) => onChange({ ...value, agree: e.target.checked })}
242
+ />
243
+ I agree to the terms
244
+ </label>
245
+ </div>
246
+ ),
247
+ disabled: (value) => !value.name || !value.email || !value.agree,
248
+ });
249
+
250
+ console.log('Collected info:', userInfo);
251
+ }
252
+ ```
253
+
254
+ **Key Points**:
255
+ - Type-safe handling of complex objects
256
+ - Validation with `disabled` function
257
+ - Confirm button disabled until all fields are filled
258
+
259
+ ---
260
+
261
+ ## Pattern 8: Conditional Modal Styles
262
+
263
+ Display different styled modals based on situation.
264
+
265
+ ```typescript
266
+ import { alert } from '@lerx/promise-modal';
267
+
268
+ type NotificationType = 'info' | 'success' | 'warning' | 'error';
269
+
270
+ function notify(type: NotificationType, message: string) {
271
+ return alert({
272
+ title: {
273
+ info: 'Information',
274
+ success: 'Success',
275
+ warning: 'Warning',
276
+ error: 'Error',
277
+ }[type],
278
+ content: message,
279
+ subtype: type,
280
+ footer: {
281
+ confirm: 'OK',
282
+ },
283
+ });
284
+ }
285
+
286
+ // Usage examples
287
+ notify('success', 'Data saved successfully.');
288
+ notify('error', 'Network error occurred.');
289
+ ```
290
+
291
+ **Advantages**:
292
+ - Consistent notification interface
293
+ - Automatic styling by type
294
+ - Reusable utility function
@@ -0,0 +1,175 @@
1
+ # API Reference
2
+
3
+ ## Static Functions
4
+
5
+ ### `alert(options)`
6
+
7
+ Opens a simple notification modal.
8
+
9
+ ```typescript
10
+ import { alert } from '@lerx/promise-modal';
11
+
12
+ await alert({
13
+ title: 'Alert',
14
+ content: 'Task completed successfully.',
15
+ subtype: 'success', // 'info' | 'success' | 'warning' | 'error'
16
+ dimmed: true,
17
+ closeOnBackdropClick: true,
18
+ manualDestroy: false,
19
+ signal: abortController.signal, // Optional AbortSignal
20
+ });
21
+ ```
22
+
23
+ **Options**:
24
+
25
+ | Option | Type | Description |
26
+ |------|------|------|
27
+ | `title` | `ReactNode` | Modal title |
28
+ | `subtitle` | `ReactNode` | Subtitle below title |
29
+ | `content` | `ReactNode \| ComponentType` | Modal content |
30
+ | `subtype` | `'info' \| 'success' \| 'warning' \| 'error'` | Modal styling type |
31
+ | `dimmed` | `boolean` | Whether to darken background |
32
+ | `closeOnBackdropClick` | `boolean` | Close on backdrop click |
33
+ | `manualDestroy` | `boolean` | Manual destruction mode |
34
+ | `duration` | `number \| string` | Animation duration |
35
+ | `background` | `ModalBackground` | Background configuration |
36
+ | `footer` | `Function \| Object \| false` | Footer configuration |
37
+ | `ForegroundComponent` | `ComponentType` | Custom foreground component |
38
+ | `BackgroundComponent` | `ComponentType` | Custom background component |
39
+ | `signal` | `AbortSignal` | AbortSignal for modal cancellation |
40
+
41
+ **Return Value**: `Promise<void>`
42
+
43
+ ---
44
+
45
+ ### `confirm(options)`
46
+
47
+ Opens a confirmation modal for user decision.
48
+
49
+ ```typescript
50
+ import { confirm } from '@lerx/promise-modal';
51
+
52
+ const result = await confirm({
53
+ title: 'Confirm',
54
+ content: 'Are you sure you want to delete this?',
55
+ footer: {
56
+ confirm: 'Delete',
57
+ cancel: 'Cancel',
58
+ },
59
+ });
60
+
61
+ if (result) {
62
+ console.log('User confirmed');
63
+ } else {
64
+ console.log('User cancelled');
65
+ }
66
+ ```
67
+
68
+ **Options**: Same options available as `alert()`
69
+
70
+ **Return Value**: `Promise<boolean>` - `true` if confirmed, `false` if cancelled
71
+
72
+ ---
73
+
74
+ ### `prompt<T>(options)`
75
+
76
+ Opens a prompt modal to collect user input.
77
+
78
+ ```typescript
79
+ import { prompt } from '@lerx/promise-modal';
80
+
81
+ // Simple text input
82
+ const name = await prompt<string>({
83
+ title: 'Enter Name',
84
+ defaultValue: '',
85
+ Input: ({ value, onChange }) => (
86
+ <input
87
+ value={value}
88
+ onChange={(e) => onChange(e.target.value)}
89
+ placeholder="Enter your name"
90
+ />
91
+ ),
92
+ disabled: (value) => value.length < 2,
93
+ });
94
+
95
+ // Complex object input
96
+ const userInfo = await prompt<{ name: string; age: number }>({
97
+ title: 'User Information',
98
+ defaultValue: { name: '', age: 0 },
99
+ Input: ({ value, onChange }) => (
100
+ <div>
101
+ <input
102
+ value={value.name}
103
+ onChange={(e) => onChange({ ...value, name: e.target.value })}
104
+ />
105
+ <input
106
+ type="number"
107
+ value={value.age}
108
+ onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
109
+ />
110
+ </div>
111
+ ),
112
+ });
113
+ ```
114
+
115
+ **Additional Options**:
116
+
117
+ | Option | Type | Description |
118
+ |------|------|------|
119
+ | `Input` | `(props: PromptInputProps<T>) => ReactNode` | Input component (required) |
120
+ | `defaultValue` | `T` | Default input value |
121
+ | `disabled` | `(value: T) => boolean` | Condition to disable confirm button |
122
+ | `returnOnCancel` | `boolean` | Whether to return default value on cancel |
123
+
124
+ **Return Value**: `Promise<T>`
125
+
126
+ ---
127
+
128
+ ## ModalProvider
129
+
130
+ Main Provider component for initialization.
131
+
132
+ ```typescript
133
+ import { ModalProvider } from '@lerx/promise-modal';
134
+ import { useLocation } from 'react-router-dom';
135
+
136
+ function App() {
137
+ return (
138
+ <ModalProvider
139
+ usePathname={useLocation} // Router integration (auto-close on path change)
140
+ ForegroundComponent={CustomForeground}
141
+ BackgroundComponent={CustomBackground}
142
+ TitleComponent={CustomTitle}
143
+ SubtitleComponent={CustomSubtitle}
144
+ ContentComponent={CustomContent}
145
+ FooterComponent={CustomFooter}
146
+ options={{
147
+ duration: '200ms',
148
+ backdrop: 'rgba(0, 0, 0, 0.35)',
149
+ manualDestroy: true,
150
+ closeOnBackdropClick: true,
151
+ }}
152
+ context={{
153
+ theme: 'light',
154
+ locale: 'en-US',
155
+ }}
156
+ >
157
+ <YourApp />
158
+ </ModalProvider>
159
+ );
160
+ }
161
+ ```
162
+
163
+ **Props**:
164
+
165
+ | Prop | Type | Description |
166
+ |------|------|------|
167
+ | `usePathname` | `() => { pathname: string }` | Router path hook (closes modals on path change) |
168
+ | `ForegroundComponent` | `ComponentType<ModalFrameProps>` | Custom foreground component |
169
+ | `BackgroundComponent` | `ComponentType<ModalFrameProps>` | Custom background component |
170
+ | `TitleComponent` | `ComponentType` | Custom title component |
171
+ | `SubtitleComponent` | `ComponentType` | Custom subtitle component |
172
+ | `ContentComponent` | `ComponentType` | Custom content component |
173
+ | `FooterComponent` | `ComponentType<FooterComponentProps>` | Custom footer component |
174
+ | `options` | `ModalOptions` | Global modal options |
175
+ | `context` | `any` | User-defined context data |