@djangocfg/ui-core 2.1.148 → 2.1.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/package.json +11 -4
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useBrowserDetect.ts +330 -0
- package/src/hooks/useDeviceDetect.ts +252 -0
- package/src/hooks/useHotkey.ts +105 -0
- package/src/lib/dialog-service/DialogProvider.tsx +115 -0
- package/src/lib/dialog-service/constants.ts +20 -0
- package/src/lib/dialog-service/dialog-service.story.tsx +263 -0
- package/src/lib/dialog-service/dialogs/AlertDialogUI.tsx +59 -0
- package/src/lib/dialog-service/dialogs/ConfirmDialogUI.tsx +81 -0
- package/src/lib/dialog-service/dialogs/PromptDialogUI.tsx +103 -0
- package/src/lib/dialog-service/dialogs/index.ts +3 -0
- package/src/lib/dialog-service/events.ts +73 -0
- package/src/lib/dialog-service/hooks/index.ts +1 -0
- package/src/lib/dialog-service/hooks/use-dialog.ts +79 -0
- package/src/lib/dialog-service/index.ts +16 -0
- package/src/lib/dialog-service/types.ts +59 -0
- package/src/lib/index.ts +1 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { defineStory } from '@djangocfg/playground';
|
|
3
|
+
import { Button } from '../../components/button';
|
|
4
|
+
import { DialogProvider } from './DialogProvider';
|
|
5
|
+
import { useDialog } from './hooks';
|
|
6
|
+
|
|
7
|
+
export default defineStory({
|
|
8
|
+
title: 'Lib/DialogService',
|
|
9
|
+
component: DialogProvider,
|
|
10
|
+
description: 'Global dialog service for alert, confirm, and prompt dialogs. Uses CustomEvents to work from anywhere in the app. Supports i18n and keyboard shortcuts.',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Wrapper component to provide DialogProvider context
|
|
14
|
+
function StoryWrapper({ children }: { children: React.ReactNode }) {
|
|
15
|
+
return <DialogProvider>{children}</DialogProvider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Demo component using the hook
|
|
19
|
+
function AlertDemo() {
|
|
20
|
+
const { alert } = useDialog();
|
|
21
|
+
|
|
22
|
+
const handleSimpleAlert = useCallback(async () => {
|
|
23
|
+
await alert('This is a simple alert message!');
|
|
24
|
+
console.log('Alert closed');
|
|
25
|
+
}, [alert]);
|
|
26
|
+
|
|
27
|
+
const handleAlertWithTitle = useCallback(async () => {
|
|
28
|
+
await alert({
|
|
29
|
+
title: 'Success!',
|
|
30
|
+
message: 'Your changes have been saved successfully.',
|
|
31
|
+
});
|
|
32
|
+
console.log('Alert with title closed');
|
|
33
|
+
}, [alert]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex gap-2">
|
|
37
|
+
<Button onClick={handleSimpleAlert}>Simple Alert</Button>
|
|
38
|
+
<Button variant="outline" onClick={handleAlertWithTitle}>
|
|
39
|
+
Alert with Title
|
|
40
|
+
</Button>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Alert = () => (
|
|
46
|
+
<StoryWrapper>
|
|
47
|
+
<AlertDemo />
|
|
48
|
+
</StoryWrapper>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Confirm demo
|
|
52
|
+
function ConfirmDemo() {
|
|
53
|
+
const { confirm, alert } = useDialog();
|
|
54
|
+
|
|
55
|
+
const handleSimpleConfirm = useCallback(async () => {
|
|
56
|
+
const result = await confirm('Are you sure you want to proceed?');
|
|
57
|
+
await alert(`You clicked: ${result ? 'Confirm' : 'Cancel'}`);
|
|
58
|
+
}, [confirm, alert]);
|
|
59
|
+
|
|
60
|
+
const handleDestructiveConfirm = useCallback(async () => {
|
|
61
|
+
const result = await confirm({
|
|
62
|
+
title: 'Delete Item',
|
|
63
|
+
message: 'This action cannot be undone. Are you sure you want to delete this item?',
|
|
64
|
+
confirmText: 'Yes, Delete',
|
|
65
|
+
cancelText: 'Keep It',
|
|
66
|
+
variant: 'destructive',
|
|
67
|
+
});
|
|
68
|
+
await alert(`You clicked: ${result ? 'Delete' : 'Keep'}`);
|
|
69
|
+
}, [confirm, alert]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex gap-2">
|
|
73
|
+
<Button onClick={handleSimpleConfirm}>Simple Confirm</Button>
|
|
74
|
+
<Button variant="destructive" onClick={handleDestructiveConfirm}>
|
|
75
|
+
Destructive Confirm
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Confirm = () => (
|
|
82
|
+
<StoryWrapper>
|
|
83
|
+
<ConfirmDemo />
|
|
84
|
+
</StoryWrapper>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Prompt demo
|
|
88
|
+
function PromptDemo() {
|
|
89
|
+
const { prompt, alert } = useDialog();
|
|
90
|
+
|
|
91
|
+
const handleSimplePrompt = useCallback(async () => {
|
|
92
|
+
const result = await prompt('What is your name?');
|
|
93
|
+
if (result) {
|
|
94
|
+
await alert(`Hello, ${result}!`);
|
|
95
|
+
} else {
|
|
96
|
+
await alert('You cancelled the prompt.');
|
|
97
|
+
}
|
|
98
|
+
}, [prompt, alert]);
|
|
99
|
+
|
|
100
|
+
const handlePromptWithDefault = useCallback(async () => {
|
|
101
|
+
const result = await prompt({
|
|
102
|
+
title: 'Edit Name',
|
|
103
|
+
message: 'Enter your display name:',
|
|
104
|
+
defaultValue: 'John Doe',
|
|
105
|
+
placeholder: 'Enter name...',
|
|
106
|
+
confirmText: 'Save',
|
|
107
|
+
cancelText: 'Cancel',
|
|
108
|
+
});
|
|
109
|
+
if (result) {
|
|
110
|
+
await alert(`Name updated to: ${result}`);
|
|
111
|
+
}
|
|
112
|
+
}, [prompt, alert]);
|
|
113
|
+
|
|
114
|
+
const handleEmailPrompt = useCallback(async () => {
|
|
115
|
+
const result = await prompt({
|
|
116
|
+
title: 'Subscribe',
|
|
117
|
+
message: 'Enter your email to subscribe to our newsletter:',
|
|
118
|
+
placeholder: 'email@example.com',
|
|
119
|
+
inputType: 'email',
|
|
120
|
+
confirmText: 'Subscribe',
|
|
121
|
+
});
|
|
122
|
+
if (result) {
|
|
123
|
+
await alert(`Subscribed with: ${result}`);
|
|
124
|
+
}
|
|
125
|
+
}, [prompt, alert]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex gap-2 flex-wrap">
|
|
129
|
+
<Button onClick={handleSimplePrompt}>Simple Prompt</Button>
|
|
130
|
+
<Button variant="outline" onClick={handlePromptWithDefault}>
|
|
131
|
+
Prompt with Default
|
|
132
|
+
</Button>
|
|
133
|
+
<Button variant="secondary" onClick={handleEmailPrompt}>
|
|
134
|
+
Email Prompt
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const Prompt = () => (
|
|
141
|
+
<StoryWrapper>
|
|
142
|
+
<PromptDemo />
|
|
143
|
+
</StoryWrapper>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Queue demo - multiple dialogs in sequence
|
|
147
|
+
function QueueDemo() {
|
|
148
|
+
const { alert, confirm, prompt } = useDialog();
|
|
149
|
+
|
|
150
|
+
const handleQueue = useCallback(async () => {
|
|
151
|
+
await alert('This is step 1 of 3');
|
|
152
|
+
const proceed = await confirm('Do you want to continue to step 2?');
|
|
153
|
+
if (!proceed) {
|
|
154
|
+
await alert('You cancelled the sequence.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const name = await prompt({
|
|
158
|
+
title: 'Step 3',
|
|
159
|
+
message: 'Enter your name to complete:',
|
|
160
|
+
});
|
|
161
|
+
if (name) {
|
|
162
|
+
await alert(`Sequence complete! Hello, ${name}!`);
|
|
163
|
+
} else {
|
|
164
|
+
await alert('Sequence cancelled at step 3.');
|
|
165
|
+
}
|
|
166
|
+
}, [alert, confirm, prompt]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Button onClick={handleQueue}>
|
|
170
|
+
Start Dialog Sequence
|
|
171
|
+
</Button>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const Queue = () => (
|
|
176
|
+
<StoryWrapper>
|
|
177
|
+
<QueueDemo />
|
|
178
|
+
</StoryWrapper>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Window.dialog API demo (vanilla JS style)
|
|
182
|
+
function WindowDialogDemo() {
|
|
183
|
+
const handleWindowAlert = useCallback(() => {
|
|
184
|
+
window.dialog.alert('Called via window.dialog.alert()');
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
const handleWindowConfirm = useCallback(async () => {
|
|
188
|
+
const result = await window.dialog.confirm('Called via window.dialog.confirm()');
|
|
189
|
+
window.dialog.alert(`Result: ${result}`);
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
const handleWindowPrompt = useCallback(async () => {
|
|
193
|
+
const result = await window.dialog.prompt({
|
|
194
|
+
title: 'window.dialog.prompt()',
|
|
195
|
+
message: 'This works from vanilla JS too!',
|
|
196
|
+
defaultValue: 'Hello',
|
|
197
|
+
});
|
|
198
|
+
if (result) {
|
|
199
|
+
window.dialog.alert(`You entered: ${result}`);
|
|
200
|
+
}
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="space-y-4">
|
|
205
|
+
<p className="text-sm text-muted-foreground">
|
|
206
|
+
These buttons use <code>window.dialog.*</code> directly - works from any JS code!
|
|
207
|
+
</p>
|
|
208
|
+
<div className="flex gap-2">
|
|
209
|
+
<Button variant="outline" onClick={handleWindowAlert}>
|
|
210
|
+
window.dialog.alert()
|
|
211
|
+
</Button>
|
|
212
|
+
<Button variant="outline" onClick={handleWindowConfirm}>
|
|
213
|
+
window.dialog.confirm()
|
|
214
|
+
</Button>
|
|
215
|
+
<Button variant="outline" onClick={handleWindowPrompt}>
|
|
216
|
+
window.dialog.prompt()
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const WindowDialogAPI = () => (
|
|
224
|
+
<StoryWrapper>
|
|
225
|
+
<WindowDialogDemo />
|
|
226
|
+
</StoryWrapper>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Keyboard shortcuts demo
|
|
230
|
+
function HotkeysDemo() {
|
|
231
|
+
const { alert, confirm } = useDialog();
|
|
232
|
+
|
|
233
|
+
const handleHotkeysDemo = useCallback(async () => {
|
|
234
|
+
await alert({
|
|
235
|
+
title: 'Keyboard Shortcuts',
|
|
236
|
+
message: 'Press Enter to close this alert, or wait and click OK.',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = await confirm({
|
|
240
|
+
title: 'Confirm with Keyboard',
|
|
241
|
+
message: 'Press Enter to confirm, Escape to cancel.',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await alert(`You pressed: ${result ? 'Enter (Confirm)' : 'Escape (Cancel)'}`);
|
|
245
|
+
}, [alert, confirm]);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
<p className="text-sm text-muted-foreground">
|
|
250
|
+
Dialogs support keyboard shortcuts: <kbd className="px-1.5 py-0.5 bg-muted rounded text-xs">Enter</kbd> to confirm, <kbd className="px-1.5 py-0.5 bg-muted rounded text-xs">Escape</kbd> to cancel.
|
|
251
|
+
</p>
|
|
252
|
+
<Button onClick={handleHotkeysDemo}>
|
|
253
|
+
Test Keyboard Shortcuts
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const KeyboardShortcuts = () => (
|
|
260
|
+
<StoryWrapper>
|
|
261
|
+
<HotkeysDemo />
|
|
262
|
+
</StoryWrapper>
|
|
263
|
+
);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useT } from '@djangocfg/i18n';
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogAction,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
} from '../../../components/alert-dialog';
|
|
14
|
+
import { useHotkey } from '../../../hooks/useHotkey';
|
|
15
|
+
import { I18N_KEYS } from '../constants';
|
|
16
|
+
import type { DialogOptions } from '../types';
|
|
17
|
+
|
|
18
|
+
interface AlertDialogUIProps {
|
|
19
|
+
open: boolean;
|
|
20
|
+
options: DialogOptions;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AlertDialogUI({ open, options, onClose }: AlertDialogUIProps) {
|
|
25
|
+
const t = useT();
|
|
26
|
+
|
|
27
|
+
// Prepare data before render
|
|
28
|
+
const dialogData = useMemo(() => ({
|
|
29
|
+
title: options.title || t(I18N_KEYS.alertTitle),
|
|
30
|
+
message: options.message,
|
|
31
|
+
confirmText: options.confirmText || t(I18N_KEYS.ok),
|
|
32
|
+
preventClose: options.preventClose ?? false,
|
|
33
|
+
}), [options, t]);
|
|
34
|
+
|
|
35
|
+
// Hotkey: Enter to confirm
|
|
36
|
+
useHotkey('enter', onClose, { enabled: open, preventDefault: true });
|
|
37
|
+
|
|
38
|
+
const handleOpenChange = (isOpen: boolean) => {
|
|
39
|
+
if (!isOpen && !dialogData.preventClose) {
|
|
40
|
+
onClose();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
46
|
+
<AlertDialogContent>
|
|
47
|
+
<AlertDialogHeader>
|
|
48
|
+
<AlertDialogTitle>{dialogData.title}</AlertDialogTitle>
|
|
49
|
+
<AlertDialogDescription>{dialogData.message}</AlertDialogDescription>
|
|
50
|
+
</AlertDialogHeader>
|
|
51
|
+
<AlertDialogFooter>
|
|
52
|
+
<AlertDialogAction onClick={onClose}>
|
|
53
|
+
{dialogData.confirmText}
|
|
54
|
+
</AlertDialogAction>
|
|
55
|
+
</AlertDialogFooter>
|
|
56
|
+
</AlertDialogContent>
|
|
57
|
+
</AlertDialog>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useT } from '@djangocfg/i18n';
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogAction,
|
|
8
|
+
AlertDialogCancel,
|
|
9
|
+
AlertDialogContent,
|
|
10
|
+
AlertDialogDescription,
|
|
11
|
+
AlertDialogFooter,
|
|
12
|
+
AlertDialogHeader,
|
|
13
|
+
AlertDialogTitle,
|
|
14
|
+
} from '../../../components/alert-dialog';
|
|
15
|
+
import { buttonVariants } from '../../../components/button';
|
|
16
|
+
import { useHotkey } from '../../../hooks/useHotkey';
|
|
17
|
+
import { cn } from '../../utils';
|
|
18
|
+
import { I18N_KEYS } from '../constants';
|
|
19
|
+
import type { DialogOptions } from '../types';
|
|
20
|
+
|
|
21
|
+
interface ConfirmDialogUIProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
options: DialogOptions;
|
|
24
|
+
onConfirm: () => void;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ConfirmDialogUI({
|
|
29
|
+
open,
|
|
30
|
+
options,
|
|
31
|
+
onConfirm,
|
|
32
|
+
onCancel,
|
|
33
|
+
}: ConfirmDialogUIProps) {
|
|
34
|
+
const t = useT();
|
|
35
|
+
|
|
36
|
+
// Prepare data before render
|
|
37
|
+
const dialogData = useMemo(() => {
|
|
38
|
+
const isDestructive = options.variant === 'destructive';
|
|
39
|
+
return {
|
|
40
|
+
title: options.title || t(I18N_KEYS.confirmTitle),
|
|
41
|
+
message: options.message,
|
|
42
|
+
confirmText: options.confirmText || t(I18N_KEYS.confirm),
|
|
43
|
+
cancelText: options.cancelText || t(I18N_KEYS.cancel),
|
|
44
|
+
preventClose: options.preventClose ?? false,
|
|
45
|
+
isDestructive,
|
|
46
|
+
confirmClassName: cn(isDestructive && buttonVariants({ variant: 'destructive' })),
|
|
47
|
+
};
|
|
48
|
+
}, [options, t]);
|
|
49
|
+
|
|
50
|
+
// Hotkey: Enter to confirm, Escape to cancel
|
|
51
|
+
useHotkey('enter', onConfirm, { enabled: open, preventDefault: true });
|
|
52
|
+
useHotkey('escape', onCancel, { enabled: open });
|
|
53
|
+
|
|
54
|
+
const handleOpenChange = (isOpen: boolean) => {
|
|
55
|
+
if (!isOpen && !dialogData.preventClose) {
|
|
56
|
+
onCancel();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
62
|
+
<AlertDialogContent>
|
|
63
|
+
<AlertDialogHeader>
|
|
64
|
+
<AlertDialogTitle>{dialogData.title}</AlertDialogTitle>
|
|
65
|
+
<AlertDialogDescription>{dialogData.message}</AlertDialogDescription>
|
|
66
|
+
</AlertDialogHeader>
|
|
67
|
+
<AlertDialogFooter>
|
|
68
|
+
<AlertDialogCancel onClick={onCancel}>
|
|
69
|
+
{dialogData.cancelText}
|
|
70
|
+
</AlertDialogCancel>
|
|
71
|
+
<AlertDialogAction
|
|
72
|
+
onClick={onConfirm}
|
|
73
|
+
className={dialogData.confirmClassName}
|
|
74
|
+
>
|
|
75
|
+
{dialogData.confirmText}
|
|
76
|
+
</AlertDialogAction>
|
|
77
|
+
</AlertDialogFooter>
|
|
78
|
+
</AlertDialogContent>
|
|
79
|
+
</AlertDialog>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
4
|
+
import { useT } from '@djangocfg/i18n';
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from '../../../components/dialog';
|
|
13
|
+
import { Button } from '../../../components/button';
|
|
14
|
+
import { Input } from '../../../components/input';
|
|
15
|
+
import { useHotkey } from '../../../hooks/useHotkey';
|
|
16
|
+
import { I18N_KEYS } from '../constants';
|
|
17
|
+
import type { DialogOptions } from '../types';
|
|
18
|
+
|
|
19
|
+
interface PromptDialogUIProps {
|
|
20
|
+
open: boolean;
|
|
21
|
+
options: DialogOptions;
|
|
22
|
+
onConfirm: (value: string) => void;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function PromptDialogUI({
|
|
27
|
+
open,
|
|
28
|
+
options,
|
|
29
|
+
onConfirm,
|
|
30
|
+
onCancel,
|
|
31
|
+
}: PromptDialogUIProps) {
|
|
32
|
+
const t = useT();
|
|
33
|
+
const [value, setValue] = useState(options.defaultValue || '');
|
|
34
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
36
|
+
// Prepare data before render
|
|
37
|
+
const dialogData = useMemo(() => ({
|
|
38
|
+
title: options.title || t(I18N_KEYS.promptTitle),
|
|
39
|
+
message: options.message,
|
|
40
|
+
confirmText: options.confirmText || t(I18N_KEYS.ok),
|
|
41
|
+
cancelText: options.cancelText || t(I18N_KEYS.cancel),
|
|
42
|
+
placeholder: options.placeholder || t(I18N_KEYS.promptPlaceholder),
|
|
43
|
+
inputType: options.inputType || 'text',
|
|
44
|
+
preventClose: options.preventClose ?? false,
|
|
45
|
+
}), [options, t]);
|
|
46
|
+
|
|
47
|
+
// Reset value when dialog opens with new options
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (open) {
|
|
50
|
+
setValue(options.defaultValue || '');
|
|
51
|
+
// Focus input after a short delay to ensure dialog is fully rendered
|
|
52
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
53
|
+
}
|
|
54
|
+
}, [open, options.defaultValue]);
|
|
55
|
+
|
|
56
|
+
const handleSubmit = useCallback((e?: React.FormEvent) => {
|
|
57
|
+
e?.preventDefault();
|
|
58
|
+
onConfirm(value);
|
|
59
|
+
}, [onConfirm, value]);
|
|
60
|
+
|
|
61
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
62
|
+
setValue(e.target.value);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleOpenChange = useCallback((isOpen: boolean) => {
|
|
66
|
+
if (!isOpen && !dialogData.preventClose) {
|
|
67
|
+
onCancel();
|
|
68
|
+
}
|
|
69
|
+
}, [dialogData.preventClose, onCancel]);
|
|
70
|
+
|
|
71
|
+
// Hotkey: Escape to cancel (Enter is handled by form submit)
|
|
72
|
+
useHotkey('escape', onCancel, { enabled: open });
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
76
|
+
<DialogContent>
|
|
77
|
+
<form onSubmit={handleSubmit}>
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle>{dialogData.title}</DialogTitle>
|
|
80
|
+
<DialogDescription>{dialogData.message}</DialogDescription>
|
|
81
|
+
</DialogHeader>
|
|
82
|
+
<div className="py-4">
|
|
83
|
+
<Input
|
|
84
|
+
ref={inputRef}
|
|
85
|
+
type={dialogData.inputType}
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={handleChange}
|
|
88
|
+
placeholder={dialogData.placeholder}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<DialogFooter>
|
|
92
|
+
<Button type="button" variant="outline" onClick={onCancel}>
|
|
93
|
+
{dialogData.cancelText}
|
|
94
|
+
</Button>
|
|
95
|
+
<Button type="submit">
|
|
96
|
+
{dialogData.confirmText}
|
|
97
|
+
</Button>
|
|
98
|
+
</DialogFooter>
|
|
99
|
+
</form>
|
|
100
|
+
</DialogContent>
|
|
101
|
+
</Dialog>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DIALOG_REQUEST_EVENT, DIALOG_RESPONSE_EVENT } from './constants';
|
|
2
|
+
import type {
|
|
3
|
+
DialogType,
|
|
4
|
+
DialogOptions,
|
|
5
|
+
DialogRequestPayload,
|
|
6
|
+
DialogResponsePayload,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
let dialogIdCounter = 0;
|
|
10
|
+
|
|
11
|
+
function generateId(): string {
|
|
12
|
+
return `dialog-${Date.now()}-${++dialogIdCounter}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch dialog request and wait for response via CustomEvent
|
|
17
|
+
*/
|
|
18
|
+
function showDialog(
|
|
19
|
+
type: DialogType,
|
|
20
|
+
messageOrOptions: string | DialogOptions
|
|
21
|
+
): Promise<boolean | string | null> {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const id = generateId();
|
|
24
|
+
const options: DialogOptions =
|
|
25
|
+
typeof messageOrOptions === 'string'
|
|
26
|
+
? { message: messageOrOptions }
|
|
27
|
+
: messageOrOptions;
|
|
28
|
+
|
|
29
|
+
// Listen for response
|
|
30
|
+
const handleResponse = (event: Event) => {
|
|
31
|
+
const customEvent = event as CustomEvent<DialogResponsePayload>;
|
|
32
|
+
if (customEvent.detail.id === id) {
|
|
33
|
+
window.removeEventListener(DIALOG_RESPONSE_EVENT, handleResponse);
|
|
34
|
+
resolve(customEvent.detail.result);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
window.addEventListener(DIALOG_RESPONSE_EVENT, handleResponse);
|
|
39
|
+
|
|
40
|
+
// Dispatch request
|
|
41
|
+
window.dispatchEvent(
|
|
42
|
+
new CustomEvent<DialogRequestPayload>(DIALOG_REQUEST_EVENT, {
|
|
43
|
+
detail: { id, type, options },
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize global window.dialog API
|
|
51
|
+
* Uses singleton pattern - only initializes once
|
|
52
|
+
*/
|
|
53
|
+
export function initDialogAPI(): void {
|
|
54
|
+
if (typeof window === 'undefined') return;
|
|
55
|
+
if (window.dialog) return; // Already initialized
|
|
56
|
+
|
|
57
|
+
window.dialog = {
|
|
58
|
+
alert: (message) => showDialog('alert', message).then(() => undefined),
|
|
59
|
+
confirm: (message) => showDialog('confirm', message) as Promise<boolean>,
|
|
60
|
+
prompt: (message) => showDialog('prompt', message) as Promise<string | null>,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dispatch response event (used by DialogProvider)
|
|
66
|
+
*/
|
|
67
|
+
export function dispatchDialogResponse(id: string, result: boolean | string | null): void {
|
|
68
|
+
window.dispatchEvent(
|
|
69
|
+
new CustomEvent<DialogResponsePayload>(DIALOG_RESPONSE_EVENT, {
|
|
70
|
+
detail: { id, result },
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDialog } from './use-dialog';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import type { DialogOptions } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React hook for using dialog service
|
|
8
|
+
*
|
|
9
|
+
* Provides type-safe access to window.dialog with fallback to native dialogs.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function MyComponent() {
|
|
14
|
+
* const { confirm, alert, prompt } = useDialog();
|
|
15
|
+
*
|
|
16
|
+
* const handleDelete = async () => {
|
|
17
|
+
* const confirmed = await confirm({
|
|
18
|
+
* title: 'Delete Item',
|
|
19
|
+
* message: 'Are you sure?',
|
|
20
|
+
* variant: 'destructive',
|
|
21
|
+
* });
|
|
22
|
+
* if (confirmed) {
|
|
23
|
+
* // delete logic
|
|
24
|
+
* }
|
|
25
|
+
* };
|
|
26
|
+
*
|
|
27
|
+
* return <button onClick={handleDelete}>Delete</button>;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useDialog() {
|
|
32
|
+
const alert = useCallback(
|
|
33
|
+
(message: string | DialogOptions): Promise<void> => {
|
|
34
|
+
if (typeof window === 'undefined') {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
if (!window.dialog) {
|
|
38
|
+
// Fallback to native
|
|
39
|
+
window.alert(typeof message === 'string' ? message : message.message);
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
return window.dialog.alert(message);
|
|
43
|
+
},
|
|
44
|
+
[]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const confirm = useCallback(
|
|
48
|
+
(message: string | DialogOptions): Promise<boolean> => {
|
|
49
|
+
if (typeof window === 'undefined') {
|
|
50
|
+
return Promise.resolve(false);
|
|
51
|
+
}
|
|
52
|
+
if (!window.dialog) {
|
|
53
|
+
// Fallback to native
|
|
54
|
+
return Promise.resolve(
|
|
55
|
+
window.confirm(typeof message === 'string' ? message : message.message)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return window.dialog.confirm(message);
|
|
59
|
+
},
|
|
60
|
+
[]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const prompt = useCallback(
|
|
64
|
+
(message: string | DialogOptions): Promise<string | null> => {
|
|
65
|
+
if (typeof window === 'undefined') {
|
|
66
|
+
return Promise.resolve(null);
|
|
67
|
+
}
|
|
68
|
+
if (!window.dialog) {
|
|
69
|
+
// Fallback to native
|
|
70
|
+
const opts = typeof message === 'string' ? { message } : message;
|
|
71
|
+
return Promise.resolve(window.prompt(opts.message, opts.defaultValue));
|
|
72
|
+
}
|
|
73
|
+
return window.dialog.prompt(message);
|
|
74
|
+
},
|
|
75
|
+
[]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return { alert, confirm, prompt };
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
DialogType,
|
|
4
|
+
DialogVariant,
|
|
5
|
+
DialogOptions,
|
|
6
|
+
DialogAPI,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
// Provider
|
|
10
|
+
export { DialogProvider } from './DialogProvider';
|
|
11
|
+
|
|
12
|
+
// Hooks
|
|
13
|
+
export { useDialog } from './hooks';
|
|
14
|
+
|
|
15
|
+
// Events (for advanced usage)
|
|
16
|
+
export { initDialogAPI } from './events';
|