@rokku-x/react-hook-dialog 0.0.2 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rokku-x
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # react-hook-dialog
2
2
 
3
+ [![CI](https://github.com/rokku-x/react-hook-dialog/actions/workflows/ci.yml/badge.svg)](https://github.com/rokku-x/react-hook-dialog/actions/workflows/ci.yml)
4
+
3
5
  A powerful and flexible React dialog hook library for confirmation dialogs, alerts, and modals. Built on top of `@rokku-x/react-hook-modal` with a focus on dialog-specific features like action buttons, variants, and customizable styling.
4
6
 
5
7
  ## Features
@@ -131,6 +133,7 @@ Configuration for individual dialog calls.
131
133
  | `classNames` | `DialogClassNames` | Custom CSS classes for elements |
132
134
  | `styles` | `DialogStyles` | Custom inline styles for elements |
133
135
  | `variantStyles` | `DialogVariantStyles` | Custom variant button styles |
136
+ | `isReturnSubmit` | `boolean` | When `true` and `content` is a `<form>`, clicking a submit action returns the serialized form values as the dialog result |
134
137
 
135
138
  ### ModalAction
136
139
 
@@ -140,11 +143,14 @@ Individual action button configuration.
140
143
  |---|---|---|---|
141
144
  | `title` | `React.ReactNode` | ✓ | Button label (string or React element) |
142
145
  | `value` | `ValidValue` | | Value to return when clicked |
143
- | `isCancel` | `boolean` | | Treat as cancel button (respects rejectOnCancel) |
146
+ | `isCancel` | `boolean` | | Treat as cancel button (respects `rejectOnCancel`) |
144
147
  | `isOnLeft` | `boolean` | | Position button on left side of row |
148
+ | `isFocused` | `boolean` | | Request initial focus when the dialog opens (highest focus priority) |
149
+ | `isSubmit` | `boolean` | | Render as `type="submit"` and trigger form submit if `content` is a `<form>` |
150
+ | `noActionReturn` | `boolean` | | Run `onClick` but do _not_ perform default dialog action (`handleAction`) — useful for custom flows |
145
151
  | `variant` | `ModalVariant` | | Visual style variant |
146
- | `className` | `string` | | Additional CSS class |
147
- | `style` | `React.CSSProperties` | | Custom inline styles |
152
+ | `className` | `string` | | Additional CSS class applied to the button |
153
+ | `style` | `React.CSSProperties` | | Custom inline styles applied to the button (highest style priority) |
148
154
  | `onClick` | `((event, action) => void) \| (() => void)` | | Click handler called before default handling |
149
155
 
150
156
  ### ModalVariant
@@ -165,6 +171,23 @@ type ModalVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' |
165
171
  | `info` | Sky (#0ea5e9) | White |
166
172
  | `neutral` | Gray (#6b7280) | White |
167
173
 
174
+ ### Action Flags — Priority & Behavior 🔀
175
+
176
+ - **Focus priority**: `isFocused` (per-action) > close button (if shown) > first action button > dialog container
177
+ - **Click ordering**: `action.onClick` is executed first. Then:
178
+ 1. If `isSubmit` is true and `isReturnSubmit` is also enabled (and `content` is a `<form>`), the form is serialized and its values are returned as the dialog result immediately (no native submit is triggered).
179
+ 2. Else if `isSubmit` is true, the form's `requestSubmit()` is invoked (useful for native validation flows).
180
+ 3. Else if `noActionReturn` is true, the default dialog action is skipped (use to perform custom flows and close the dialog manually).
181
+ 4. Otherwise `handleAction` is called and the dialog resolves/rejects using the action's `value`.
182
+ - **isCancel**: marks a cancel action — dialog resolves or rejects according to `rejectOnCancel` and `defaultCancelValue` settings.
183
+ - **Placement**: `isOnLeft` positions the action on the left side; other actions render on the right.
184
+ - **Style & class precedence** (lowest → highest):
185
+ 1. built-in `baseVariantStyles` (library defaults)
186
+ 2. `ConfirmConfig.variantStyles` (per-call variant overrides)
187
+ 3. `ConfirmConfig.styles.actionButton` (per-call default action button styles)
188
+ 4. `ModalAction.style` (per-action inline style — highest precedence)
189
+ 5. `className` values are appended so per-action `className` and `ConfirmConfig.classNames.actionButton` both apply (per-action classes appear last).
190
+
168
191
  ### DialogClassNames
169
192
 
170
193
  Customize CSS classes for all elements:
@@ -564,6 +587,43 @@ const handleFormSubmit = async (formData: any) => {
564
587
  };
565
588
  ```
566
589
 
590
+ ### Example 15: Form Dialog Returning Values (isReturnSubmit)
591
+
592
+ ```tsx
593
+ const [requestDialog] = useHookDialog();
594
+
595
+ async function openProfileDialog() {
596
+ const result = await requestDialog({
597
+ title: 'Edit Profile',
598
+ content: (
599
+ <form>
600
+ <label>
601
+ Name
602
+ <input name="name" defaultValue="Alice" />
603
+ </label>
604
+ <label>
605
+ Email
606
+ <input name="email" defaultValue="alice@example.com" />
607
+ </label>
608
+ </form>
609
+ ),
610
+ actions: [[
611
+ { title: 'Cancel', isCancel: true },
612
+ // `isSubmit` triggers native submit; when `isReturnSubmit` is enabled on the dialog config, the dialog returns the form values object
613
+ { title: 'Save', isSubmit: true }
614
+ ]],
615
+ isReturnSubmit: true
616
+ });
617
+
618
+ if (result && typeof result === 'object') {
619
+ // `result` is the object built from the submitted form
620
+ console.log('Form values:', result);
621
+ // e.g. result.name, result.email
622
+ }
623
+ }
624
+ ```
625
+
626
+ > Note: `isReturnSubmit` overrides `noActionReturn` and returns the serialized form values as the action `value`. `isSubmit` still triggers `requestSubmit()` to allow native validation flows.
567
627
  ### Example 14: Boolean Result with Custom Values
568
628
 
569
629
  ```tsx
package/dist/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rokku-x
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/README.md ADDED
@@ -0,0 +1,704 @@
1
+ # react-hook-dialog
2
+
3
+ [![CI](https://github.com/rokku-x/react-hook-dialog/actions/workflows/ci.yml/badge.svg)](https://github.com/rokku-x/react-hook-dialog/actions/workflows/ci.yml)
4
+
5
+ A powerful and flexible React dialog hook library for confirmation dialogs, alerts, and modals. Built on top of `@rokku-x/react-hook-modal` with a focus on dialog-specific features like action buttons, variants, and customizable styling.
6
+
7
+ ## Features
8
+
9
+ - 🎯 **Hook-based API** - Simple and intuitive `useHookDialog()` hook
10
+ - 🎨 **Rich Variants** - 7 button variants (primary, secondary, danger, success, warning, info, neutral)
11
+ - 🧩 **Modular Components** - Composed from reusable Backdrop and DialogWindow components
12
+ - 📝 **Dialog Actions** - Flexible action button system with left/right positioning
13
+ - 💅 **Full Customization** - Injectable className and styles at every level
14
+ - ⌨️ **Rich Configuration** - Default configs with per-call overrides
15
+ - 🎁 **Zero Dependencies** - Only requires React, Zustand, and @rokku-x/react-hook-modal
16
+ - 📱 **TypeScript Support** - Full type safety out of the box
17
+ - ♿ **Backdrop Control** - Configurable backdrop click behavior
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @rokku-x/react-hook-dialog
23
+ ```
24
+
25
+ or with yarn:
26
+
27
+ ```bash
28
+ yarn add @rokku-x/react-hook-dialog
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ### 1. Setup BaseModalRenderer
34
+
35
+ First, add the `BaseModalRenderer` at the root of your application (from `@rokku-x/react-hook-modal`):
36
+
37
+ ```tsx
38
+ import { BaseModalRenderer } from '@rokku-x/react-hook-modal';
39
+
40
+ function App() {
41
+ return (
42
+ <>
43
+ <YourComponents />
44
+ <BaseModalRenderer />
45
+ </>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ### 2. Use useHookDialog Hook
51
+
52
+ ```tsx
53
+ import { useHookDialog } from '@rokku-x/react-hook-dialog';
54
+
55
+ function MyComponent() {
56
+ const [requestDialog] = useHookDialog();
57
+
58
+ const handleConfirm = async () => {
59
+ const result = await requestDialog({
60
+ title: 'Confirm Action',
61
+ content: 'Are you sure you want to proceed?',
62
+ actions: [[
63
+ { title: 'Cancel', isCancel: true },
64
+ { title: 'Confirm', variant: 'primary' }
65
+ ]]
66
+ });
67
+
68
+ console.log('User chose:', result);
69
+ };
70
+
71
+ return <button onClick={handleConfirm}>Open Dialog</button>;
72
+ }
73
+ ```
74
+
75
+ ## Bundle Size
76
+
77
+ - ESM: ~4.06 kB gzipped (13.48 kB raw)
78
+ - CJS: ~3.48 kB gzipped (9.21 kB raw)
79
+
80
+ Measured with Vite build for v0.0.1.
81
+
82
+ ## API Reference
83
+
84
+ ### useHookDialog
85
+
86
+ Main hook for displaying confirmation dialogs and alerts.
87
+
88
+ #### Parameters
89
+
90
+ ```typescript
91
+ useHookDialog(defaultConfig?: UseHookDialogConfig)
92
+ ```
93
+
94
+ | Parameter | Type | Description |
95
+ |---|---|---|
96
+ | `defaultConfig` | `UseHookDialogConfig` | Optional default configuration applied to all dialogs |
97
+
98
+ #### Returns
99
+
100
+ ```typescript
101
+ [requestDialog]
102
+ ```
103
+
104
+ | Return Value | Type | Description |
105
+ |---|---|---|
106
+ | `requestDialog` | `(config: ConfirmConfig) => Promise<ValidValue>` | Function to open a dialog and get user response |
107
+
108
+ #### Default Config Options
109
+
110
+ | Property | Type | Default | Description |
111
+ |---|---|---|---|
112
+ | `backdropCancel` | `boolean` | `false` | Allow closing via backdrop click |
113
+ | `rejectOnCancel` | `boolean` | `true` | Reject promise on cancel instead of resolving |
114
+ | `defaultCancelValue` | `ValidValue` | `undefined` | Value to return/reject on cancel |
115
+ | `showCloseButton` | `boolean` | `false` | Show X close button |
116
+ | `classNames` | `DialogClassNames` | `undefined` | Custom CSS classes |
117
+ | `styles` | `DialogStyles` | `undefined` | Custom inline styles |
118
+ | `variantStyles` | `DialogVariantStyles` | `undefined` | Custom variant button styles |
119
+
120
+ ### ConfirmConfig
121
+
122
+ Configuration for individual dialog calls.
123
+
124
+ | Property | Type | Description |
125
+ |---|---|---|
126
+ | `title` | `React.ReactNode` | Dialog title (string or React element) |
127
+ | `content` | `React.ReactNode` | Dialog content (string or React element) |
128
+ | `actions` | `ModalAction[][]` | Array of action button rows |
129
+ | `backdropCancel` | `boolean` | Allow closing via backdrop click |
130
+ | `rejectOnCancel` | `boolean` | Reject promise on cancel |
131
+ | `defaultCancelValue` | `ValidValue` | Value to return/reject on cancel |
132
+ | `showCloseButton` | `boolean` | Show X close button |
133
+ | `classNames` | `DialogClassNames` | Custom CSS classes for elements |
134
+ | `styles` | `DialogStyles` | Custom inline styles for elements |
135
+ | `variantStyles` | `DialogVariantStyles` | Custom variant button styles |
136
+ | `isReturnSubmit` | `boolean` | When `true` and `content` is a `<form>`, clicking a submit action returns the serialized form values as the dialog result |
137
+
138
+ ### ModalAction
139
+
140
+ Individual action button configuration.
141
+
142
+ | Property | Type | Required | Description |
143
+ |---|---|---|---|
144
+ | `title` | `React.ReactNode` | ✓ | Button label (string or React element) |
145
+ | `value` | `ValidValue` | | Value to return when clicked |
146
+ | `isCancel` | `boolean` | | Treat as cancel button (respects `rejectOnCancel`) |
147
+ | `isOnLeft` | `boolean` | | Position button on left side of row |
148
+ | `isFocused` | `boolean` | | Request initial focus when the dialog opens (highest focus priority) |
149
+ | `isSubmit` | `boolean` | | Render as `type="submit"` and trigger form submit if `content` is a `<form>` |
150
+ | `noActionReturn` | `boolean` | | Run `onClick` but do _not_ perform default dialog action (`handleAction`) — useful for custom flows |
151
+ | `variant` | `ModalVariant` | | Visual style variant |
152
+ | `className` | `string` | | Additional CSS class applied to the button |
153
+ | `style` | `React.CSSProperties` | | Custom inline styles applied to the button (highest style priority) |
154
+ | `onClick` | `((event, action) => void) \| (() => void)` | | Click handler called before default handling |
155
+
156
+ ### ModalVariant
157
+
158
+ Available button variants:
159
+
160
+ ```typescript
161
+ type ModalVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'neutral';
162
+ ```
163
+
164
+ | Variant | Color | Text Color |
165
+ |---|---|---|
166
+ | `primary` | Blue (#2563eb) | White |
167
+ | `secondary` | Gray (#e5e7eb) | Black |
168
+ | `danger` | Red (#dc2626) | White |
169
+ | `success` | Green (#16a34a) | White |
170
+ | `warning` | Amber (#f59e0b) | Black |
171
+ | `info` | Sky (#0ea5e9) | White |
172
+ | `neutral` | Gray (#6b7280) | White |
173
+
174
+ ### Action Flags — Priority & Behavior 🔀
175
+
176
+ - **Focus priority**: `isFocused` (per-action) > close button (if shown) > first action button > dialog container
177
+ - **Click ordering**: `action.onClick` is executed first. Then:
178
+ 1. If `isSubmit` is true and `isReturnSubmit` is also enabled (and `content` is a `<form>`), the form is serialized and its values are returned as the dialog result immediately (no native submit is triggered).
179
+ 2. Else if `isSubmit` is true, the form's `requestSubmit()` is invoked (useful for native validation flows).
180
+ 3. Else if `noActionReturn` is true, the default dialog action is skipped (use to perform custom flows and close the dialog manually).
181
+ 4. Otherwise `handleAction` is called and the dialog resolves/rejects using the action's `value`.
182
+ - **isCancel**: marks a cancel action — dialog resolves or rejects according to `rejectOnCancel` and `defaultCancelValue` settings.
183
+ - **Placement**: `isOnLeft` positions the action on the left side; other actions render on the right.
184
+ - **Style & class precedence** (lowest → highest):
185
+ 1. built-in `baseVariantStyles` (library defaults)
186
+ 2. `ConfirmConfig.variantStyles` (per-call variant overrides)
187
+ 3. `ConfirmConfig.styles.actionButton` (per-call default action button styles)
188
+ 4. `ModalAction.style` (per-action inline style — highest precedence)
189
+ 5. `className` values are appended so per-action `className` and `ConfirmConfig.classNames.actionButton` both apply (per-action classes appear last).
190
+
191
+ ### DialogClassNames
192
+
193
+ Customize CSS classes for all elements:
194
+
195
+ | Property | Type | Description |
196
+ |---|---|---|
197
+ | `backdrop` | `string` | Backdrop overlay |
198
+ | `dialog` | `string` | Dialog container |
199
+ | `closeButton` | `string` | Close button |
200
+ | `title` | `string` | Title element |
201
+ | `content` | `string` | Content container |
202
+ | `actions` | `string` | Actions container |
203
+ | `actionsRow` | `string` | Individual action row |
204
+ | `actionButton` | `string` | Action button |
205
+
206
+ ### DialogStyles
207
+
208
+ Customize inline styles for all elements:
209
+
210
+ | Property | Type | Description |
211
+ |---|---|---|
212
+ | `backdrop` | `React.CSSProperties` | Backdrop overlay styles |
213
+ | `dialog` | `React.CSSProperties` | Dialog container styles |
214
+ | `closeButton` | `React.CSSProperties` | Close button styles |
215
+ | `title` | `React.CSSProperties` | Title element styles |
216
+ | `content` | `React.CSSProperties` | Content container styles |
217
+ | `actions` | `React.CSSProperties` | Actions container styles |
218
+ | `actionsRow` | `React.CSSProperties` | Action row styles |
219
+ | `actionButton` | `React.CSSProperties` | Action button styles |
220
+
221
+ ## Components
222
+
223
+ ### Backdrop
224
+
225
+ Overlay component that wraps dialog windows.
226
+
227
+ ```tsx
228
+ <Backdrop
229
+ onClick={() => handleClose()}
230
+ className="custom-backdrop"
231
+ style={{ backdropFilter: 'blur(5px)' }}
232
+ >
233
+ {children}
234
+ </Backdrop>
235
+ ```
236
+
237
+ ### DialogWindow
238
+
239
+ Main dialog container component.
240
+
241
+ ```tsx
242
+ <DialogWindow
243
+ className="custom-dialog"
244
+ style={{ backgroundColor: '#f5f5f5' }}
245
+ >
246
+ {children}
247
+ </DialogWindow>
248
+ ```
249
+
250
+ ## Examples
251
+
252
+ ### Example 1: Basic Confirmation Dialog
253
+
254
+ ```tsx
255
+ import { useHookDialog } from '@rokku-x/react-hook-dialog';
256
+
257
+ function DeleteConfirm() {
258
+ const [requestDialog] = useHookDialog();
259
+
260
+ const handleDelete = async () => {
261
+ const result = await requestDialog({
262
+ title: 'Delete Item?',
263
+ content: 'This action cannot be undone.',
264
+ actions: [[
265
+ { title: 'Cancel', isCancel: true, variant: 'secondary' },
266
+ { title: 'Delete', variant: 'danger', value: true }
267
+ ]]
268
+ });
269
+
270
+ if (result === true) {
271
+ console.log('Item deleted!');
272
+ }
273
+ };
274
+
275
+ return <button onClick={handleDelete}>Delete</button>;
276
+ }
277
+ ```
278
+
279
+ ### Example 2: Multiple Action Rows
280
+
281
+ ```tsx
282
+ const [requestDialog] = useHookDialog();
283
+
284
+ await requestDialog({
285
+ title: 'Choose Action',
286
+ content: 'What would you like to do?',
287
+ actions: [
288
+ [{ title: 'Back', isOnLeft: true, variant: 'secondary' }],
289
+ [
290
+ { title: 'Cancel', isCancel: true },
291
+ { title: 'Save', variant: 'primary' }
292
+ ]
293
+ ]
294
+ });
295
+ ```
296
+
297
+ ### Example 3: Custom Styling
298
+
299
+ ```tsx
300
+ const [requestDialog] = useHookDialog({
301
+ styles: {
302
+ dialog: {
303
+ borderRadius: '20px',
304
+ backgroundColor: '#f9fafb'
305
+ },
306
+ actionButton: {
307
+ fontWeight: 'bold'
308
+ }
309
+ },
310
+ classNames: {
311
+ dialog: 'my-custom-dialog'
312
+ }
313
+ });
314
+
315
+ await requestDialog({
316
+ title: 'Styled Dialog',
317
+ content: 'This dialog has custom styles'
318
+ });
319
+ ```
320
+
321
+ ### Example 4: Custom Button Variants
322
+
323
+ ```tsx
324
+ const [requestDialog] = useHookDialog({
325
+ variantStyles: {
326
+ primary: {
327
+ backgroundColor: '#7c3aed', // Purple
328
+ color: '#fff'
329
+ }
330
+ }
331
+ });
332
+
333
+ await requestDialog({
334
+ title: 'Custom Colors',
335
+ actions: [[
336
+ { title: 'Confirm', variant: 'primary' }
337
+ ]]
338
+ });
339
+ ```
340
+
341
+ ### Example 5: Button Click Handlers
342
+
343
+ ```tsx
344
+ await requestDialog({
345
+ title: 'Action Dialog',
346
+ actions: [[
347
+ {
348
+ title: 'Log to Console',
349
+ onClick: (e) => console.log('Button clicked!'),
350
+ variant: 'info'
351
+ },
352
+ {
353
+ title: 'Proceed',
354
+ variant: 'primary'
355
+ }
356
+ ]]
357
+ });
358
+ ```
359
+
360
+ ### Example 6: Rich Content
361
+
362
+ ```tsx
363
+ await requestDialog({
364
+ title: <span style={{ color: 'blue' }}>Custom <strong>Title</strong></span>,
365
+ content: (
366
+ <div>
367
+ <p>This dialog has rich content:</p>
368
+ <ul>
369
+ <li>Item 1</li>
370
+ <li>Item 2</li>
371
+ </ul>
372
+ </div>
373
+ ),
374
+ actions: [[
375
+ { title: 'OK', variant: 'primary' }
376
+ ]]
377
+ });
378
+ ```
379
+
380
+ ### Example 7: Default Configuration
381
+
382
+ ```tsx
383
+ const [requestDialog] = useHookDialog({
384
+ showCloseButton: false,
385
+ backdropCancel: false,
386
+ styles: {
387
+ dialog: { maxWidth: '500px' }
388
+ }
389
+ });
390
+
391
+ // All subsequent dialogs will use these defaults
392
+ await requestDialog({
393
+ title: 'Will use defaults',
394
+ content: 'No close button, backdrop click disabled'
395
+ });
396
+ ```
397
+
398
+ ### Example 8: Alert Dialog
399
+
400
+ ```tsx
401
+ async function showAlert(message: string) {
402
+ const [requestDialog] = useHookDialog();
403
+
404
+ await requestDialog({
405
+ title: 'Alert',
406
+ content: message,
407
+ actions: [[
408
+ { title: 'OK', variant: 'primary' }
409
+ ]]
410
+ });
411
+ }
412
+
413
+ showAlert('Operation completed successfully!');
414
+ ```
415
+
416
+ ### Example 9: Multiple Choice with Different Values
417
+
418
+ ```tsx
419
+ const [requestDialog] = useHookDialog();
420
+
421
+ const handleSaveOptions = async () => {
422
+ const result = await requestDialog({
423
+ title: 'Save Options',
424
+ content: 'How would you like to save?',
425
+ actions: [[
426
+ { title: 'Cancel', isCancel: true },
427
+ { title: 'Save Draft', variant: 'secondary', value: 'draft' },
428
+ { title: 'Publish', variant: 'primary', value: 'publish' }
429
+ ]]
430
+ });
431
+
432
+ if (result === 'draft') {
433
+ console.log('Saving as draft...');
434
+ } else if (result === 'publish') {
435
+ console.log('Publishing...');
436
+ } else {
437
+ console.log('Cancelled');
438
+ }
439
+ };
440
+ ```
441
+
442
+ ### Example 10: Numeric Rating Dialog
443
+
444
+ ```tsx
445
+ const [requestDialog] = useHookDialog();
446
+
447
+ const handleRating = async () => {
448
+ const rating = await requestDialog({
449
+ title: 'Rate Your Experience',
450
+ content: 'How would you rate our service?',
451
+ actions: [
452
+ [
453
+ { title: '1 Star', variant: 'danger', value: 1 },
454
+ { title: '2 Stars', variant: 'warning', value: 2 },
455
+ { title: '3 Stars', variant: 'neutral', value: 3 }
456
+ ],
457
+ [
458
+ { title: '4 Stars', variant: 'info', value: 4 },
459
+ { title: '5 Stars', variant: 'success', value: 5 }
460
+ ]
461
+ ],
462
+ showCloseButton: false,
463
+ backdropCancel: false
464
+ });
465
+
466
+ console.log(`User rated: ${rating} stars`);
467
+ // Send rating to API
468
+ };
469
+ ```
470
+
471
+ ### Example 11: Conditional Actions Based on Result
472
+
473
+ ```tsx
474
+ const [requestDialog] = useHookDialog();
475
+
476
+ const handleFileOperation = async () => {
477
+ const action = await requestDialog({
478
+ title: 'File Actions',
479
+ content: 'What would you like to do with this file?',
480
+ actions: [[
481
+ { title: 'Download', variant: 'info', value: 'download' },
482
+ { title: 'Share', variant: 'primary', value: 'share' },
483
+ { title: 'Delete', variant: 'danger', value: 'delete', isOnLeft: true }
484
+ ]]
485
+ });
486
+
487
+ switch (action) {
488
+ case 'download':
489
+ // Download file logic
490
+ window.location.href = '/api/download/file.pdf';
491
+ break;
492
+ case 'share':
493
+ // Open share dialog
494
+ await requestDialog({
495
+ title: 'Share File',
496
+ content: 'File link copied to clipboard!',
497
+ actions: [[{ title: 'OK', variant: 'primary' }]]
498
+ });
499
+ break;
500
+ case 'delete':
501
+ // Confirm deletion
502
+ const confirm = await requestDialog({
503
+ title: 'Confirm Delete',
504
+ content: 'Are you sure? This cannot be undone.',
505
+ actions: [[
506
+ { title: 'Cancel', isCancel: true },
507
+ { title: 'Delete', variant: 'danger', value: true }
508
+ ]]
509
+ });
510
+ if (confirm) {
511
+ console.log('File deleted');
512
+ }
513
+ break;
514
+ }
515
+ };
516
+ ```
517
+
518
+ ### Example 12: Handle Cancel vs Reject
519
+
520
+ ```tsx
521
+ const [requestDialog] = useHookDialog({
522
+ rejectOnCancel: true // Reject promise on cancel
523
+ });
524
+
525
+ const handleWithErrorHandling = async () => {
526
+ try {
527
+ const result = await requestDialog({
528
+ title: 'Important Action',
529
+ content: 'This requires your confirmation.',
530
+ actions: [[
531
+ { title: 'Cancel', isCancel: true },
532
+ { title: 'Proceed', variant: 'primary', value: 'proceed' }
533
+ ]]
534
+ });
535
+
536
+ if (result === 'proceed') {
537
+ console.log('User proceeded');
538
+ // Perform action
539
+ }
540
+ } catch (error) {
541
+ console.log('User cancelled or closed dialog');
542
+ // Handle cancellation
543
+ }
544
+ };
545
+ ```
546
+
547
+ ### Example 13: Form Submission with Validation
548
+
549
+ ```tsx
550
+ const [requestDialog] = useHookDialog();
551
+
552
+ const handleFormSubmit = async (formData: any) => {
553
+ const action = await requestDialog({
554
+ title: 'Review Changes',
555
+ content: (
556
+ <div>
557
+ <p>You are about to submit the following changes:</p>
558
+ <ul>
559
+ <li>Name: {formData.name}</li>
560
+ <li>Email: {formData.email}</li>
561
+ </ul>
562
+ </div>
563
+ ),
564
+ actions: [[
565
+ { title: 'Edit', variant: 'secondary', value: 'edit' },
566
+ { title: 'Cancel', isCancel: true },
567
+ { title: 'Submit', variant: 'success', value: 'submit' }
568
+ ]]
569
+ });
570
+
571
+ if (action === 'submit') {
572
+ // Submit form
573
+ const response = await fetch('/api/submit', {
574
+ method: 'POST',
575
+ body: JSON.stringify(formData)
576
+ });
577
+
578
+ await requestDialog({
579
+ title: 'Success',
580
+ content: 'Your changes have been saved!',
581
+ actions: [[{ title: 'OK', variant: 'success' }]]
582
+ });
583
+ } else if (action === 'edit') {
584
+ // Return to form
585
+ console.log('User wants to edit');
586
+ }
587
+ };
588
+ ```
589
+
590
+ ### Example 15: Form Dialog Returning Values (isReturnSubmit)
591
+
592
+ ```tsx
593
+ const [requestDialog] = useHookDialog();
594
+
595
+ async function openProfileDialog() {
596
+ const result = await requestDialog({
597
+ title: 'Edit Profile',
598
+ content: (
599
+ <form>
600
+ <label>
601
+ Name
602
+ <input name="name" defaultValue="Alice" />
603
+ </label>
604
+ <label>
605
+ Email
606
+ <input name="email" defaultValue="alice@example.com" />
607
+ </label>
608
+ </form>
609
+ ),
610
+ actions: [[
611
+ { title: 'Cancel', isCancel: true },
612
+ // `isSubmit` triggers native submit; when `isReturnSubmit` is enabled on the dialog config, the dialog returns the form values object
613
+ { title: 'Save', isSubmit: true }
614
+ ]],
615
+ isReturnSubmit: true
616
+ });
617
+
618
+ if (result && typeof result === 'object') {
619
+ // `result` is the object built from the submitted form
620
+ console.log('Form values:', result);
621
+ // e.g. result.name, result.email
622
+ }
623
+ }
624
+ ```
625
+
626
+ > Note: `isReturnSubmit` overrides `noActionReturn` and returns the serialized form values as the action `value`. `isSubmit` still triggers `requestSubmit()` to allow native validation flows.
627
+ ### Example 14: Boolean Result with Custom Values
628
+
629
+ ```tsx
630
+ const [requestDialog] = useHookDialog();
631
+
632
+ const handleLogout = async () => {
633
+ const shouldLogout = await requestDialog({
634
+ title: 'Confirm Logout',
635
+ content: 'Are you sure you want to log out?',
636
+ actions: [[
637
+ { title: 'Stay Logged In', variant: 'secondary', value: false },
638
+ { title: 'Log Out', variant: 'danger', value: true }
639
+ ]]
640
+ });
641
+
642
+ if (shouldLogout) {
643
+ // Perform logout
644
+ sessionStorage.clear();
645
+ window.location.href = '/login';
646
+ }
647
+ };
648
+ ```
649
+
650
+ ## Best Practices
651
+
652
+ 1. **Mount `BaseModalRenderer` at root level** - Required for modals to render
653
+ 2. **Use default configs for consistency** - Set common styles/behaviors once
654
+ 3. **Provide meaningful button labels** - Users should know what each button does
655
+ 4. **Use appropriate variants** - Use `danger` for destructive actions, `success` for confirmations
656
+ 5. **Keep content concise** - Dialogs should be focused and brief
657
+ 6. **Handle both resolve and reject** - Account for cancellation scenarios
658
+ 7. **Use `isOnLeft` for secondary actions** - Helps with visual hierarchy
659
+ 8. **Customize responsibly** - Maintain accessibility and usability standards
660
+
661
+ ## Types
662
+
663
+ ### ValidValue
664
+
665
+ ```typescript
666
+ type ValidValue = string | number | boolean | undefined;
667
+ ```
668
+
669
+ The type of value returned from dialog actions.
670
+
671
+ ### DialogVariantStyles
672
+
673
+ ```typescript
674
+ type DialogVariantStyles = Partial<Record<ModalVariant, React.CSSProperties>>;
675
+ ```
676
+
677
+ Custom styles for each variant type.
678
+
679
+ ## Accessibility
680
+
681
+ - Backdrop close can be enabled with `backdropCancel: true`
682
+ - Close button can be shown with `showCloseButton: true`
683
+ - All buttons are keyboard accessible
684
+ - ARIA labels provided for interactive elements
685
+ - Supports custom ARIA attributes via className injection
686
+
687
+ ## Troubleshooting
688
+
689
+ ### Dialog not appearing
690
+ - Ensure `BaseModalRenderer` is mounted at root level
691
+ - Check that `useHookDialog()` is called within the component tree
692
+
693
+ ### Styles not applying
694
+ - Verify className/style props are passed to `ConfirmConfig`
695
+ - Check CSS specificity - inline styles take precedence
696
+ - Use browser dev tools to inspect applied styles
697
+
698
+ ### Promise never resolves
699
+ - Ensure action buttons have appropriate `value` or are configured as cancel buttons
700
+ - Check that action click handlers don't prevent default behavior
701
+
702
+ ## License
703
+
704
+ MIT
@@ -1,3 +1,4 @@
1
+ import { ModalWindowProps } from '../types';
1
2
  /**
2
3
  * Main modal window component that renders the dialog UI.
3
4
  *
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default styles for button variants.
3
+ * Can be overridden via the variantStyles config option.
4
+ */
5
+ export declare const baseVariantStyles: Record<string, React.CSSProperties>;
@@ -1,3 +1,4 @@
1
+ import { FormDataObject, ConfirmConfig, UseHookDialogConfig, ValidValue } from '../types';
1
2
  /**
2
3
  * Hook for displaying confirmation dialogs and alerts.
3
4
  *
@@ -33,4 +34,9 @@
33
34
  * });
34
35
  * ```
35
36
  */
36
- export default function useHookDialog(defaultConfig?: UseHookDialogConfig): ((config: ConfirmConfig) => Promise<ValidValue>)[];
37
+ export default function useHookDialog<T = ValidValue>(defaultConfig?: UseHookDialogConfig): {
38
+ <U = FormDataObject>(config: ConfirmConfig & {
39
+ isReturnSubmit: true;
40
+ }): Promise<U>;
41
+ <U = T>(config: ConfirmConfig): Promise<U>;
42
+ }[];
package/dist/index.cjs.js CHANGED
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("@rokku-x/react-hook-modal"),k=require("react"),l=require("react/jsx-runtime"),$=require("zustand"),w={primary:{backgroundColor:"#2563eb",color:"#fff"},secondary:{backgroundColor:"#e5e7eb",color:"#111"},danger:{backgroundColor:"#dc2626",color:"#fff"},success:{backgroundColor:"#16a34a",color:"#fff"},warning:{backgroundColor:"#f59e0b",color:"#111"},info:{backgroundColor:"#0ea5e9",color:"#fff"},neutral:{backgroundColor:"#6b7280",color:"#fff"}};function R({modalWindowId:s,handleAction:y,handleClose:a,config:n}){const{actions:r=[],title:i,content:m,backdropCancel:h,showCloseButton:v,classNames:o={},styles:e={},variantStyles:c={}}=n,d=(r.length?r:[[{title:"OK",variant:"primary"}]]).filter(g=>g&&g.length),u={...w,...c},p=()=>{h===!0&&a(s)};return l.jsx(b.ModalBackdrop,{onClick:p,className:o.backdrop||"",style:e.backdrop,children:l.jsxs(b.ModalWindow,{className:o.dialog||"",style:e.dialog,children:[v&&l.jsx("button",{type:"button",className:`hook-dialog-close-button ${o.closeButton||""}`,"aria-label":"Close",onClick:()=>a(s),style:{position:"absolute",top:0,right:0,transform:"translate(75%, -75%)",width:32,height:32,background:"none",border:"none",fontSize:21,cursor:"pointer",lineHeight:1,color:"#555",...e.closeButton},children:"×"}),i&&l.jsx("h3",{className:`hook-dialog-title ${o.title||""}`,style:{margin:"0 0 15px",fontSize:20,...e.title},children:i}),m&&l.jsx("div",{className:`hook-dialog-content ${o.content||""}`,style:{marginBottom:15,color:"#555",...e.content},children:m}),l.jsx("div",{className:`hook-dialog-actions ${o.actions||""}`,style:{display:"flex",flexDirection:"column",gap:10,...e.actions},children:d.map((g,x)=>{const j=g.filter(t=>t.isOnLeft),N=g.filter(t=>!t.isOnLeft),C=(t,f)=>{const S=t.variant||"secondary",M=u[S]||u.secondary;return l.jsx("button",{type:"button",className:`hook-dialog-action-button hook-dialog-action-${t.variant||"secondary"} ${o.actionButton||""} ${t.className||""}`,onClick:B=>{t.onClick?.(B,t),y(s,t)},style:{border:"none",borderRadius:15,padding:"10px 18px",fontSize:14,fontWeight:800,cursor:"pointer",...M,...e.actionButton,...t.style||{}},children:t.title},`${t.title}-${f}`)};return l.jsxs("div",{className:`hook-dialog-actions-row ${o.actionsRow||""}`,style:{display:"flex",gap:8,justifyContent:"space-between",marginTop:10,...e.actionsRow},children:[l.jsx("div",{className:"hook-dialog-actions-left",style:{display:"flex",gap:8},children:j.map((t,f)=>C(t,f))}),l.jsx("div",{className:"hook-dialog-actions-right",style:{display:"flex",gap:8},children:N.map((t,f)=>C(t,f))})]},x)})})]})})}const I=$.create((s,y)=>({instances:[],addInstance:a=>s(n=>({instances:[...n.instances,a]})),removeInstance:a=>s(n=>({instances:n.instances.filter(r=>r.id!==a)})),getInstance:a=>y().instances.find(n=>n.id===a)}));function O(s){const{instances:y,addInstance:a,removeInstance:n,getInstance:r}=I(),i=b.useBaseModal(),m=k.useCallback(o=>{const e=r(o);e&&(i.popModal(o),e.config.rejectOnCancel!==!1?e.reject(e.config.defaultCancelValue):e.resolve(e.config.defaultCancelValue),n(o))},[r,n,i]),h=k.useCallback((o,e)=>{const c=r(o);c&&(i.popModal(o),e.isCancel&&c.config.rejectOnCancel!==!1?c.reject(e.value):c.resolve(e.value),n(o))},[r,n,i]);return[o=>new Promise((e,c)=>{const d=Math.random().toString(36).substring(2,6),u={...s,...o,classNames:{...s?.classNames,...o.classNames},styles:{...s?.styles,...o.styles},variantStyles:{...s?.variantStyles,...o.variantStyles}},p={id:d,config:u,resolve:e,reject:c};a(p),p.id=i.pushModal(d,k.createElement(R,{config:u,modalWindowId:d,handleClose:m,handleAction:h}))})]}Object.defineProperty(exports,"BaseModalRenderer",{enumerable:!0,get:()=>b.BaseModalRenderer});exports.useHookDialog=O;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const j=require("@rokku-x/react-hook-modal"),p=require("react"),d=require("react/jsx-runtime"),$=require("zustand"),D={primary:{backgroundColor:"#2563eb",color:"#fff"},secondary:{backgroundColor:"#e5e7eb",color:"#111"},danger:{backgroundColor:"#dc2626",color:"#fff"},success:{backgroundColor:"#16a34a",color:"#fff"},warning:{backgroundColor:"#f59e0b",color:"#111"},info:{backgroundColor:"#0ea5e9",color:"#fff"},neutral:{backgroundColor:"#6b7280",color:"#fff"}};function w(t){const u={};return t.forEach((n,s)=>{if(s in u){const a=u[s];Array.isArray(a)?a.push(n):u[s]=[a,n]}else u[s]=n}),u}const A=t=>p.isValidElement(t)&&(t.type==="form"||typeof t.type=="string"&&t.type.toLowerCase()==="form"),q=j.ModalWindow;function O({modalWindowId:t,handleAction:u,handleClose:n,config:s}){const{actions:a=[],title:g,backdropCancel:R,showCloseButton:N,classNames:f={},styles:o={},variantStyles:r={}}=s;let{content:l}=s;const y=(a.length?a:[[{title:"OK",variant:"primary"}]]).filter(c=>c&&c.length),h={...D,...r},x=()=>R&&n(t),M=p.useRef(null),B=p.useRef(null),C=p.useRef(null);return p.useEffect(()=>{const c=M.current;if(!c)return;const k=document.activeElement;if(B.current)B.current.focus();else{const i=c.querySelector("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])");i?i.focus():(c.setAttribute("tabindex","-1"),c.focus())}const S=i=>{if(i.key==="Escape")i.stopPropagation(),n(t);else if(i.key==="Tab"){const b=Array.from(c.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")).filter(Boolean);if(b.length===0){i.preventDefault();return}const e=b[0],m=b[b.length-1];i.shiftKey?document.activeElement===e&&(i.preventDefault(),m.focus()):document.activeElement===m&&(i.preventDefault(),e.focus())}};return c.addEventListener("keydown",S),()=>{c.removeEventListener("keydown",S),k&&k.focus&&k.focus()}},[n,t]),A(l)&&(l=p.cloneElement(l,{ref:C})),d.jsx(j.ModalBackdrop,{onClick:x,className:f.backdrop||"",style:o.backdrop,children:d.jsxs(q,{ref:M,role:"dialog","aria-modal":"true","aria-labelledby":g?`${t}-title`:void 0,className:f.dialog||"",style:o.dialog,children:[N&&d.jsx("button",{type:"button",className:`hook-dialog-close-button ${f.closeButton||""}`,"aria-label":"Close",onClick:()=>n(t),style:{position:"absolute",top:0,right:0,transform:"translate(75%, -75%)",width:32,height:32,background:"none",border:"none",fontSize:21,cursor:"pointer",lineHeight:1,color:"#555",...o.closeButton},children:"×"}),g&&d.jsx("h3",{id:`${t}-title`,className:`hook-dialog-title ${f.title||""}`,style:{margin:"0 0 15px",fontSize:20,...o.title},children:g}),l&&d.jsx("div",{className:`hook-dialog-content ${f.content||""}`,style:{marginBottom:15,color:"#555",...o.content},children:l}),d.jsx("div",{className:`hook-dialog-actions ${f.actions||""}`,style:{display:"flex",flexDirection:"column",gap:10,paddingTop:15,...o.actions},children:y.map((c,k)=>{const S=c.filter(e=>e.isOnLeft),i=c.filter(e=>!e.isOnLeft),b=(e,m)=>{const E=h[e.variant||"secondary"]||h.secondary;return d.jsx("button",{ref:v=>{e.isFocused&&v&&(B.current=v)},"data-action-focused":e.isFocused?"true":void 0,className:`hook-dialog-action-button hook-dialog-action-${e.variant||"secondary"} ${f.actionButton||""} ${e.className||""}`,onClick:v=>{if(e.onClick?.(v,e),e.isSubmit&&s.isReturnSubmit&&C.current)return u(t,e,w(new FormData(C.current)));if(e.isSubmit&&C.current?.requestSubmit(),e.noActionReturn)return v.stopPropagation();u(t,e)},style:{border:"none",borderRadius:15,padding:"10px 18px",fontSize:14,fontWeight:800,cursor:"pointer",...E,...o.actionButton,...e.style||{}},children:e.title},`${e.title}-${m}`)};return d.jsxs("div",{className:`hook-dialog-actions-row ${f.actionsRow||""}`,style:{display:"flex",gap:8,justifyContent:"space-between",...o.actionsRow},children:[d.jsx("div",{className:"hook-dialog-actions-left",style:{display:"flex",gap:8},children:S.map((e,m)=>b(e,m))}),d.jsx("div",{className:"hook-dialog-actions-right",style:{display:"flex",gap:8},children:i.map((e,m)=>b(e,m))})]},k)})})]})})}const F=$.create((t,u)=>({instances:[],addInstance:n=>t(s=>({instances:[...s.instances,n]})),removeInstance:n=>t(s=>({instances:s.instances.filter(a=>a.id!==n)})),getInstance:n=>u().instances.find(s=>s.id===n)}));function L(t){const{instances:u,addInstance:n,removeInstance:s,getInstance:a}=F(),g=j.useBaseModal(),R=p.useCallback(o=>{const r=a(o);r&&(g.popModal(o),r.config.rejectOnCancel!==!1?r.reject(r.config.defaultCancelValue):r.resolve(r.config.defaultCancelValue),s(o))},[a,s,g]),N=p.useCallback((o,r)=>{const l=a(o);l&&(g.popModal(o),r.isCancel&&l.config.rejectOnCancel!==!1?l.reject(r.value):l.resolve(r.value),s(o))},[a,s,g]);function f(o){return new Promise((r,l)=>{const y=Math.random().toString(36).substring(2,6),h={...t,...o,classNames:{...t?.classNames,...o.classNames},styles:{...t?.styles,...o.styles},variantStyles:{...t?.variantStyles,...o.variantStyles}},x={id:y,config:h,resolve:r,reject:l};n(x),x.id=g.pushModal(y,p.createElement(O,{config:h,modalWindowId:y,handleClose:R,handleAction:N}))})}return[f]}Object.defineProperty(exports,"BaseModalRenderer",{enumerable:!0,get:()=>j.BaseModalRenderer});exports.useHookDialog=L;
package/dist/index.esm.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { ModalBackdrop as w, ModalWindow as j, useBaseModal as I } from "@rokku-x/react-hook-modal";
3
- import { BaseModalRenderer as P } from "@rokku-x/react-hook-modal";
4
- import R, { useCallback as v } from "react";
5
- import { jsx as i, jsxs as C } from "react/jsx-runtime";
6
- import { create as D } from "zustand";
7
- const O = {
2
+ import { ModalBackdrop as A, ModalWindow as j, useBaseModal as F } from "@rokku-x/react-hook-modal";
3
+ import { BaseModalRenderer as X } from "@rokku-x/react-hook-modal";
4
+ import R, { useRef as E, useEffect as O, useCallback as M } from "react";
5
+ import { jsx as p, jsxs as w } from "react/jsx-runtime";
6
+ import { create as L } from "zustand";
7
+ const V = {
8
8
  primary: { backgroundColor: "#2563eb", color: "#fff" },
9
9
  secondary: { backgroundColor: "#e5e7eb", color: "#111" },
10
10
  danger: { backgroundColor: "#dc2626", color: "#fff" },
@@ -13,29 +13,70 @@ const O = {
13
13
  info: { backgroundColor: "#0ea5e9", color: "#fff" },
14
14
  neutral: { backgroundColor: "#6b7280", color: "#fff" }
15
15
  };
16
- function V({ modalWindowId: a, handleAction: m, handleClose: s, config: n }) {
17
- const { actions: l = [], title: c, content: p, backdropCancel: y, showCloseButton: b, classNames: o = {}, styles: e = {}, variantStyles: r = {} } = n, d = (l.length ? l : [[{ title: "OK", variant: "primary" }]]).filter((u) => u && u.length), f = { ...O, ...r };
18
- return /* @__PURE__ */ i(
19
- w,
16
+ function q(t) {
17
+ const u = {};
18
+ return t.forEach((s, n) => {
19
+ if (n in u) {
20
+ const a = u[n];
21
+ Array.isArray(a) ? a.push(s) : u[n] = [a, s];
22
+ } else
23
+ u[n] = s;
24
+ }), u;
25
+ }
26
+ const z = (t) => R.isValidElement(t) && (t.type === "form" || typeof t.type == "string" && t.type.toLowerCase() === "form"), I = j;
27
+ function K({ modalWindowId: t, handleAction: u, handleClose: s, config: n }) {
28
+ const { actions: a = [], title: d, backdropCancel: S, showCloseButton: N, classNames: f = {}, styles: o = {}, variantStyles: r = {} } = n;
29
+ let { content: l } = n;
30
+ const y = (a.length ? a : [[{ title: "OK", variant: "primary" }]]).filter((i) => i && i.length), b = { ...V, ...r }, k = () => S && s(t), $ = E(null), B = E(null), x = E(null);
31
+ return O(() => {
32
+ const i = $.current;
33
+ if (!i) return;
34
+ const h = document.activeElement;
35
+ if (B.current)
36
+ B.current.focus();
37
+ else {
38
+ const c = i.querySelector("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])");
39
+ c ? c.focus() : (i.setAttribute("tabindex", "-1"), i.focus());
40
+ }
41
+ const C = (c) => {
42
+ if (c.key === "Escape")
43
+ c.stopPropagation(), s(t);
44
+ else if (c.key === "Tab") {
45
+ const g = Array.from(i.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")).filter(Boolean);
46
+ if (g.length === 0) {
47
+ c.preventDefault();
48
+ return;
49
+ }
50
+ const e = g[0], m = g[g.length - 1];
51
+ c.shiftKey ? document.activeElement === e && (c.preventDefault(), m.focus()) : document.activeElement === m && (c.preventDefault(), e.focus());
52
+ }
53
+ };
54
+ return i.addEventListener("keydown", C), () => {
55
+ i.removeEventListener("keydown", C), h && h.focus && h.focus();
56
+ };
57
+ }, [s, t]), z(l) && (l = R.cloneElement(l, { ref: x })), /* @__PURE__ */ p(
58
+ A,
20
59
  {
21
- onClick: () => {
22
- y === !0 && s(a);
23
- },
24
- className: o.backdrop || "",
25
- style: e.backdrop,
26
- children: /* @__PURE__ */ C(
27
- j,
60
+ onClick: k,
61
+ className: f.backdrop || "",
62
+ style: o.backdrop,
63
+ children: /* @__PURE__ */ w(
64
+ I,
28
65
  {
29
- className: o.dialog || "",
30
- style: e.dialog,
66
+ ref: $,
67
+ role: "dialog",
68
+ "aria-modal": "true",
69
+ "aria-labelledby": d ? `${t}-title` : void 0,
70
+ className: f.dialog || "",
71
+ style: o.dialog,
31
72
  children: [
32
- b && /* @__PURE__ */ i(
73
+ N && /* @__PURE__ */ p(
33
74
  "button",
34
75
  {
35
76
  type: "button",
36
- className: `hook-dialog-close-button ${o.closeButton || ""}`,
77
+ className: `hook-dialog-close-button ${f.closeButton || ""}`,
37
78
  "aria-label": "Close",
38
- onClick: () => s(a),
79
+ onClick: () => s(t),
39
80
  style: {
40
81
  position: "absolute",
41
82
  top: 0,
@@ -49,23 +90,28 @@ function V({ modalWindowId: a, handleAction: m, handleClose: s, config: n }) {
49
90
  cursor: "pointer",
50
91
  lineHeight: 1,
51
92
  color: "#555",
52
- ...e.closeButton
93
+ ...o.closeButton
53
94
  },
54
95
  children: "×"
55
96
  }
56
97
  ),
57
- c && /* @__PURE__ */ i("h3", { className: `hook-dialog-title ${o.title || ""}`, style: { margin: "0 0 15px", fontSize: 20, ...e.title }, children: c }),
58
- p && /* @__PURE__ */ i("div", { className: `hook-dialog-content ${o.content || ""}`, style: { marginBottom: 15, color: "#555", ...e.content }, children: p }),
59
- /* @__PURE__ */ i("div", { className: `hook-dialog-actions ${o.actions || ""}`, style: { display: "flex", flexDirection: "column", gap: 10, ...e.actions }, children: d.map((u, N) => {
60
- const x = u.filter((t) => t.isOnLeft), B = u.filter((t) => !t.isOnLeft), k = (t, g) => {
61
- const S = t.variant || "secondary", M = f[S] || f.secondary;
62
- return /* @__PURE__ */ i(
98
+ d && /* @__PURE__ */ p("h3", { id: `${t}-title`, className: `hook-dialog-title ${f.title || ""}`, style: { margin: "0 0 15px", fontSize: 20, ...o.title }, children: d }),
99
+ l && /* @__PURE__ */ p("div", { className: `hook-dialog-content ${f.content || ""}`, style: { marginBottom: 15, color: "#555", ...o.content }, children: l }),
100
+ /* @__PURE__ */ p("div", { className: `hook-dialog-actions ${f.actions || ""}`, style: { display: "flex", flexDirection: "column", gap: 10, paddingTop: 15, ...o.actions }, children: y.map((i, h) => {
101
+ const C = i.filter((e) => e.isOnLeft), c = i.filter((e) => !e.isOnLeft), g = (e, m) => {
102
+ const D = b[e.variant || "secondary"] || b.secondary;
103
+ return /* @__PURE__ */ p(
63
104
  "button",
64
105
  {
65
- type: "button",
66
- className: `hook-dialog-action-button hook-dialog-action-${t.variant || "secondary"} ${o.actionButton || ""} ${t.className || ""}`,
67
- onClick: ($) => {
68
- t.onClick?.($, t), m(a, t);
106
+ ref: (v) => {
107
+ e.isFocused && v && (B.current = v);
108
+ },
109
+ "data-action-focused": e.isFocused ? "true" : void 0,
110
+ className: `hook-dialog-action-button hook-dialog-action-${e.variant || "secondary"} ${f.actionButton || ""} ${e.className || ""}`,
111
+ onClick: (v) => {
112
+ if (e.onClick?.(v, e), e.isSubmit && n.isReturnSubmit && x.current) return u(t, e, q(new FormData(x.current)));
113
+ if (e.isSubmit && x.current?.requestSubmit(), e.noActionReturn) return v.stopPropagation();
114
+ u(t, e);
69
115
  },
70
116
  style: {
71
117
  border: "none",
@@ -74,19 +120,19 @@ function V({ modalWindowId: a, handleAction: m, handleClose: s, config: n }) {
74
120
  fontSize: 14,
75
121
  fontWeight: 800,
76
122
  cursor: "pointer",
77
- ...M,
78
- ...e.actionButton,
79
- ...t.style || {}
123
+ ...D,
124
+ ...o.actionButton,
125
+ ...e.style || {}
80
126
  },
81
- children: t.title
127
+ children: e.title
82
128
  },
83
- `${t.title}-${g}`
129
+ `${e.title}-${m}`
84
130
  );
85
131
  };
86
- return /* @__PURE__ */ C("div", { className: `hook-dialog-actions-row ${o.actionsRow || ""}`, style: { display: "flex", gap: 8, justifyContent: "space-between", marginTop: 10, ...e.actionsRow }, children: [
87
- /* @__PURE__ */ i("div", { className: "hook-dialog-actions-left", style: { display: "flex", gap: 8 }, children: x.map((t, g) => k(t, g)) }),
88
- /* @__PURE__ */ i("div", { className: "hook-dialog-actions-right", style: { display: "flex", gap: 8 }, children: B.map((t, g) => k(t, g)) })
89
- ] }, N);
132
+ return /* @__PURE__ */ w("div", { className: `hook-dialog-actions-row ${f.actionsRow || ""}`, style: { display: "flex", gap: 8, justifyContent: "space-between", ...o.actionsRow }, children: [
133
+ /* @__PURE__ */ p("div", { className: "hook-dialog-actions-left", style: { display: "flex", gap: 8 }, children: C.map((e, m) => g(e, m)) }),
134
+ /* @__PURE__ */ p("div", { className: "hook-dialog-actions-right", style: { display: "flex", gap: 8 }, children: c.map((e, m) => g(e, m)) })
135
+ ] }, h);
90
136
  }) })
91
137
  ]
92
138
  }
@@ -94,50 +140,53 @@ function V({ modalWindowId: a, handleAction: m, handleClose: s, config: n }) {
94
140
  }
95
141
  );
96
142
  }
97
- const z = D((a, m) => ({
143
+ const P = L((t, u) => ({
98
144
  instances: [],
99
- addInstance: (s) => a((n) => ({
145
+ addInstance: (s) => t((n) => ({
100
146
  instances: [...n.instances, s]
101
147
  })),
102
- removeInstance: (s) => a((n) => ({
103
- instances: n.instances.filter((l) => l.id !== s)
148
+ removeInstance: (s) => t((n) => ({
149
+ instances: n.instances.filter((a) => a.id !== s)
104
150
  })),
105
- getInstance: (s) => m().instances.find((n) => n.id === s)
151
+ getInstance: (s) => u().instances.find((n) => n.id === s)
106
152
  }));
107
- function K(a) {
108
- const { instances: m, addInstance: s, removeInstance: n, getInstance: l } = z(), c = I(), p = v((o) => {
109
- const e = l(o);
110
- e && (c.popModal(o), e.config.rejectOnCancel !== !1 ? e.reject(e.config.defaultCancelValue) : e.resolve(e.config.defaultCancelValue), n(o));
111
- }, [l, n, c]), y = v((o, e) => {
112
- const r = l(o);
113
- r && (c.popModal(o), e.isCancel && r.config.rejectOnCancel !== !1 ? r.reject(e.value) : r.resolve(e.value), n(o));
114
- }, [l, n, c]);
115
- return [(o) => new Promise((e, r) => {
116
- const d = Math.random().toString(36).substring(2, 6), f = {
117
- ...a,
118
- ...o,
119
- classNames: {
120
- ...a?.classNames,
121
- ...o.classNames
122
- },
123
- styles: {
124
- ...a?.styles,
125
- ...o.styles
126
- },
127
- variantStyles: {
128
- ...a?.variantStyles,
129
- ...o.variantStyles
130
- }
131
- }, h = {
132
- id: d,
133
- config: f,
134
- resolve: e,
135
- reject: r
136
- };
137
- s(h), h.id = c.pushModal(d, R.createElement(V, { config: f, modalWindowId: d, handleClose: p, handleAction: y }));
138
- })];
153
+ function Q(t) {
154
+ const { instances: u, addInstance: s, removeInstance: n, getInstance: a } = P(), d = F(), S = M((o) => {
155
+ const r = a(o);
156
+ r && (d.popModal(o), r.config.rejectOnCancel !== !1 ? r.reject(r.config.defaultCancelValue) : r.resolve(r.config.defaultCancelValue), n(o));
157
+ }, [a, n, d]), N = M((o, r) => {
158
+ const l = a(o);
159
+ l && (d.popModal(o), r.isCancel && l.config.rejectOnCancel !== !1 ? l.reject(r.value) : l.resolve(r.value), n(o));
160
+ }, [a, n, d]);
161
+ function f(o) {
162
+ return new Promise((r, l) => {
163
+ const y = Math.random().toString(36).substring(2, 6), b = {
164
+ ...t,
165
+ ...o,
166
+ classNames: {
167
+ ...t?.classNames,
168
+ ...o.classNames
169
+ },
170
+ styles: {
171
+ ...t?.styles,
172
+ ...o.styles
173
+ },
174
+ variantStyles: {
175
+ ...t?.variantStyles,
176
+ ...o.variantStyles
177
+ }
178
+ }, k = {
179
+ id: y,
180
+ config: b,
181
+ resolve: r,
182
+ reject: l
183
+ };
184
+ s(k), k.id = d.pushModal(y, R.createElement(K, { config: b, modalWindowId: y, handleClose: S, handleAction: N }));
185
+ });
186
+ }
187
+ return [f];
139
188
  }
140
189
  export {
141
- P as BaseModalRenderer,
142
- K as useHookDialog
190
+ X as BaseModalRenderer,
191
+ Q as useHookDialog
143
192
  };
@@ -1,17 +1,18 @@
1
+ import { ConfirmInstance, ValidValue } from '../types';
1
2
  /**
2
3
  * Zustand store interface for managing dialog instances.
3
4
  * Maintains a centralized list of active dialog instances.
4
5
  * @internal
5
6
  */
6
- interface DialogStore {
7
+ interface DialogStore<T = ValidValue> {
7
8
  /** Array of currently active dialog instances */
8
- instances: ConfirmInstance[];
9
+ instances: ConfirmInstance<T>[];
9
10
  /** Add a new dialog instance to the store */
10
- addInstance: (instance: ConfirmInstance) => void;
11
+ addInstance: (instance: ConfirmInstance<T>) => void;
11
12
  /** Remove a dialog instance from the store by ID */
12
13
  removeInstance: (id: string) => void;
13
14
  /** Get a dialog instance by ID */
14
- getInstance: (id: string) => ConfirmInstance | undefined;
15
+ getInstance: (id: string) => ConfirmInstance<T> | undefined;
15
16
  }
16
17
  /**
17
18
  * Zustand store for managing dialog instances.
@@ -23,5 +24,5 @@ interface DialogStore {
23
24
  * const { addInstance, removeInstance, getInstance } = useDialogStore();
24
25
  * ```
25
26
  */
26
- export declare const useDialogStore: import('zustand').UseBoundStore<import('zustand').StoreApi<DialogStore>>;
27
+ export declare const useDialogStore: import('zustand').UseBoundStore<import('zustand').StoreApi<DialogStore<any>>>;
27
28
  export {};
@@ -0,0 +1 @@
1
+ export { }
@@ -1 +1,24 @@
1
- export {};
1
+ import { default as React } from 'react';
2
+ /**
3
+ * Value stored in FormData entries (string or File)
4
+ */
5
+ export type FormDataValue = FormDataEntryValue;
6
+ /**
7
+ * Resulting object from converting FormData to a plain object.
8
+ * Repeated keys become arrays of values.
9
+ */
10
+ export type FormDataObject = Record<string, FormDataValue | FormDataValue[]>;
11
+ /**
12
+ * Convert a FormData instance into a plain object.
13
+ * Repeated keys become arrays of values.
14
+ *
15
+ * @example
16
+ * const data = new FormData();
17
+ * data.append('name', 'Alice');
18
+ * data.append('tags', 'a');
19
+ * data.append('tags', 'b');
20
+ * const obj = FormDataToObject(data);
21
+ * // { name: 'Alice', tags: ['a','b'] }
22
+ */
23
+ export declare function FormDataToObject<T extends FormDataObject = FormDataObject>(formData: FormData): T;
24
+ export declare const IsForm: (content: React.ReactNode) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokku-x/react-hook-dialog",
3
- "version": "0.0.2",
3
+ "version": "1.0.2",
4
4
  "author": "rokku-x",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,6 +9,7 @@
9
9
  "main": "dist/index.cjs.js",
10
10
  "module": "dist/index.esm.js",
11
11
  "devDependencies": {
12
+ "@changesets/cli": "^2.29.8",
12
13
  "@testing-library/jest-dom": "^6.9.1",
13
14
  "@testing-library/react": "^14.3.1",
14
15
  "@testing-library/user-event": "^14.6.1",
@@ -20,7 +21,8 @@
20
21
  "typescript": "^5.9.3",
21
22
  "vite": "^7.3.1",
22
23
  "vite-plugin-dts": "^4.5.4",
23
- "vitest": "^1.6.1"
24
+ "vitest": "^1.6.1",
25
+ "copyfiles": "^2.4.1"
24
26
  },
25
27
  "peerDependencies": {
26
28
  "@rokku-x/react-hook-modal": "^0.7.8",
@@ -69,12 +71,14 @@
69
71
  "provenance": true
70
72
  },
71
73
  "scripts": {
72
- "build": "vite build",
73
- "prepare": "npm run build",
74
+ "build": "vite build && copyfiles -u 1 \"src/types/**/*.d.ts\" dist/ && copyfiles README.md LICENSE dist/",
74
75
  "publish:first": "npm publish --access public --provenance false",
75
76
  "dev": "vite",
76
77
  "preview": "vite preview",
77
- "test": "vitest"
78
+ "test": "vitest",
79
+ "changeset": "changeset",
80
+ "version:changeset": "changeset version",
81
+ "publish:changeset": "changeset publish --access public"
78
82
  },
79
83
  "sideEffects": false,
80
84
  "types": "dist/index.d.ts",