@manhphi1309/dialog 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,34 +1,100 @@
1
- "use client";
1
+ import * as React from "react";
2
2
  import { XIcon } from "lucide-react";
3
3
  import { Dialog as Dialog$1 } from "radix-ui";
4
4
  import { Button } from "@manhphi1309/button";
5
5
  import { cn } from "@manhphi1309/utils";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
- //#region index.tsx
7
+ import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@manhphi1309/drawer";
8
+ import { useIsMobile } from "@manhphi1309/hooks";
9
+ //#region src/dialog.tsx
10
+ /**
11
+ * Root dialog component. Controls open/close state and provides context
12
+ * to all child primitives via Radix `Dialog.Root`.
13
+ *
14
+ * Can be used as **uncontrolled** (via `defaultOpen`) or **controlled**
15
+ * (via `open` + `onOpenChange`).
16
+ *
17
+ * @example
18
+ * // Uncontrolled
19
+ * <Dialog>
20
+ * <DialogTrigger>Open</DialogTrigger>
21
+ * <DialogContent>...</DialogContent>
22
+ * </Dialog>
23
+ *
24
+ * @example
25
+ * // Controlled
26
+ * const [open, setOpen] = useState(false)
27
+ * <Dialog open={open} onOpenChange={setOpen}>
28
+ * <DialogContent>...</DialogContent>
29
+ * </Dialog>
30
+ */
8
31
  function Dialog({ ...props }) {
9
32
  return /* @__PURE__ */ jsx(Dialog$1.Root, {
10
33
  "data-slot": "dialog",
11
34
  ...props
12
35
  });
13
36
  }
37
+ /**
38
+ * The element that opens the dialog when activated (clicked / keyboard).
39
+ * Thin wrapper around Radix `Dialog.Trigger`.
40
+ *
41
+ * Use `asChild` to compose with your own element instead of the default button.
42
+ *
43
+ * @example
44
+ * <DialogTrigger asChild>
45
+ * <Button variant="outline">Open</Button>
46
+ * </DialogTrigger>
47
+ */
14
48
  function DialogTrigger({ ...props }) {
15
49
  return /* @__PURE__ */ jsx(Dialog$1.Trigger, {
16
50
  "data-slot": "dialog-trigger",
17
51
  ...props
18
52
  });
19
53
  }
54
+ /**
55
+ * Renders its children into a **portal** appended to `document.body`,
56
+ * outside the current DOM tree. This ensures the dialog always appears
57
+ * above other content and is never clipped by parent `overflow` or `z-index`.
58
+ *
59
+ * You rarely need to use this directly — `DialogContent` already wraps its
60
+ * output in a portal automatically.
61
+ */
20
62
  function DialogPortal({ ...props }) {
21
63
  return /* @__PURE__ */ jsx(Dialog$1.Portal, {
22
64
  "data-slot": "dialog-portal",
23
65
  ...props
24
66
  });
25
67
  }
68
+ /**
69
+ * A primitive close trigger. Closes the dialog when activated.
70
+ * Use `asChild` to compose with a custom element.
71
+ *
72
+ * Prefer the built-in `showCloseButton` prop on `DialogContent` or
73
+ * `DialogFooter` for standard close affordances. Use this component
74
+ * when you need a completely custom close action inside the dialog body.
75
+ *
76
+ * @example
77
+ * <DialogClose asChild>
78
+ * <Button variant="ghost">Cancel</Button>
79
+ * </DialogClose>
80
+ */
26
81
  function DialogClose({ ...props }) {
27
82
  return /* @__PURE__ */ jsx(Dialog$1.Close, {
28
83
  "data-slot": "dialog-close",
29
84
  ...props
30
85
  });
31
86
  }
87
+ /**
88
+ * The semi-transparent **backdrop** rendered behind the dialog panel.
89
+ * Covers the full viewport (`fixed inset-0`) with a slight blur and dark tint.
90
+ *
91
+ * Animates:
92
+ * - **Open** → `fade-in-0`
93
+ * - **Close** → `fade-out-0`
94
+ *
95
+ * You rarely need to use this directly — `DialogContent` renders it
96
+ * automatically via `DialogPortal`.
97
+ */
32
98
  function DialogOverlay({ className, ...props }) {
33
99
  return /* @__PURE__ */ jsx(Dialog$1.Overlay, {
34
100
  "data-slot": "dialog-overlay",
@@ -36,6 +102,29 @@ function DialogOverlay({ className, ...props }) {
36
102
  ...props
37
103
  });
38
104
  }
105
+ /**
106
+ * The **visible panel** of the dialog. Rendered centred on screen via
107
+ * `position: fixed; top: 50%; left: 50%; translate(-50%, -50%)`.
108
+ * Automatically wraps itself in `DialogPortal` and renders `DialogOverlay`
109
+ * behind it.
110
+ *
111
+ * By default a ghost ✕ button is positioned in the top-right corner.
112
+ * Pass `showCloseButton={false}` to remove it when you want to control
113
+ * close behaviour exclusively from the footer.
114
+ *
115
+ * Animates:
116
+ * - **Open** → `fade-in-0 + zoom-in-95`
117
+ * - **Close** → `fade-out-0 + zoom-out-95`
118
+ *
119
+ * @param showCloseButton - Whether to render the ✕ ghost button in the
120
+ * top-right corner. Defaults to `true`.
121
+ *
122
+ * @example
123
+ * <DialogContent showCloseButton={false}>
124
+ * <DialogHeader>...</DialogHeader>
125
+ * <DialogFooter showCloseButton>...</DialogFooter>
126
+ * </DialogContent>
127
+ */
39
128
  function DialogContent({ className, children, showCloseButton = true, ...props }) {
40
129
  return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(Dialog$1.Content, {
41
130
  "data-slot": "dialog-content",
@@ -57,6 +146,17 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
57
146
  })]
58
147
  })] });
59
148
  }
149
+ /**
150
+ * Layout wrapper for the **top section** of a dialog.
151
+ * Stacks children vertically with `gap-2`. Typically contains
152
+ * `DialogTitle` and optionally `DialogDescription`.
153
+ *
154
+ * @example
155
+ * <DialogHeader>
156
+ * <DialogTitle>Confirm deletion</DialogTitle>
157
+ * <DialogDescription>This action is irreversible.</DialogDescription>
158
+ * </DialogHeader>
159
+ */
60
160
  function DialogHeader({ className, ...props }) {
61
161
  return /* @__PURE__ */ jsx("div", {
62
162
  "data-slot": "dialog-header",
@@ -64,6 +164,20 @@ function DialogHeader({ className, ...props }) {
64
164
  ...props
65
165
  });
66
166
  }
167
+ /**
168
+ * Layout wrapper for the **bottom action area** of a dialog.
169
+ * Bleeds to the content panel edges (`-mx-4 -mb-4`), adds a top border
170
+ * and a subtle muted background tint. Actions stack vertically on mobile
171
+ * and align to the right on `sm+` screens.
172
+ *
173
+ * @param showCloseButton - When `true`, appends an outline `Close` button
174
+ * wired to `DialogClose` after any provided `children`. Defaults to `false`.
175
+ *
176
+ * @example
177
+ * <DialogFooter showCloseButton>
178
+ * <Button type="submit">Save</Button>
179
+ * </DialogFooter>
180
+ */
67
181
  function DialogFooter({ className, showCloseButton = false, children, ...props }) {
68
182
  return /* @__PURE__ */ jsxs("div", {
69
183
  "data-slot": "dialog-footer",
@@ -78,6 +192,16 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
78
192
  })]
79
193
  });
80
194
  }
195
+ /**
196
+ * The **accessible title** of the dialog. Rendered as a Radix `Dialog.Title`,
197
+ * which is automatically linked to the dialog content via `aria-labelledby`.
198
+ * Screen readers announce this text when the dialog opens.
199
+ *
200
+ * Styled with the heading font token at `text-base font-medium`.
201
+ *
202
+ * @remarks Required for accessibility. Every dialog should have a title,
203
+ * even if visually hidden with `className="sr-only"`.
204
+ */
81
205
  function DialogTitle({ className, ...props }) {
82
206
  return /* @__PURE__ */ jsx(Dialog$1.Title, {
83
207
  "data-slot": "dialog-title",
@@ -85,6 +209,17 @@ function DialogTitle({ className, ...props }) {
85
209
  ...props
86
210
  });
87
211
  }
212
+ /**
213
+ * The **accessible description** of the dialog. Rendered as a Radix
214
+ * `Dialog.Description`, which is linked to the dialog content via
215
+ * `aria-describedby`. Screen readers read this after the title.
216
+ *
217
+ * Styled as small muted text. Anchor tags (`<a>`) inside the description
218
+ * are automatically underlined and change colour on hover.
219
+ *
220
+ * @remarks Optional but recommended. Omit only if the dialog content itself
221
+ * is sufficiently self-descriptive.
222
+ */
88
223
  function DialogDescription({ className, ...props }) {
89
224
  return /* @__PURE__ */ jsx(Dialog$1.Description, {
90
225
  "data-slot": "dialog-description",
@@ -93,4 +228,255 @@ function DialogDescription({ className, ...props }) {
93
228
  });
94
229
  }
95
230
  //#endregion
96
- export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger };
231
+ //#region src/responsive-dialog.tsx
232
+ const ResponsiveDialogContext = React.createContext({ snap: true });
233
+ /**
234
+ * Adaptive root component that switches between `Dialog` and `Drawer`
235
+ * depending on screen width:
236
+ * - **Desktop (≥ 768 px)** → renders `Dialog` (centred modal overlay)
237
+ * - **Mobile (< 768 px)** → renders `Drawer` (bottom-sheet panel)
238
+ *
239
+ * All props are forwarded unchanged to the active primitive, so `open`,
240
+ * `defaultOpen`, `onOpenChange`, and `modal` all behave identically to
241
+ * the standard `Dialog` root.
242
+ *
243
+ * **Drawer-Specific Props (Mobile Only)**
244
+ * You can pass Vaul-specific props like `snapPoints`. They are cleanly ignored on desktop.
245
+ * - **IMPORTANT:** If you provide `snapPoints`, you must also provide `fadeFromIndex` as a strict number.
246
+ * - **IMPORTANT:** If you pass `activeSnapPoint`, you must also pass `setActiveSnapPoint` and manage it via state (e.g. `useState`). Do not hardcode `activeSnapPoint` without a setter, otherwise the Drawer will glitch and aggressively snap back when dragged.
247
+ *
248
+ * @example
249
+ * <ResponsiveDialog>
250
+ * <ResponsiveDialogTrigger asChild>
251
+ * <Button>Open</Button>
252
+ * </ResponsiveDialogTrigger>
253
+ * <ResponsiveDialogContent>
254
+ * <ResponsiveDialogHeader>
255
+ * <ResponsiveDialogTitle>Title</ResponsiveDialogTitle>
256
+ * </ResponsiveDialogHeader>
257
+ * </ResponsiveDialogContent>
258
+ * </ResponsiveDialog>
259
+ */
260
+ function ResponsiveDialog({ children, snapPoints, activeSnapPoint, setActiveSnapPoint, fadeFromIndex, shouldScaleBackground, dismissible, snap = true, ...props }) {
261
+ const isMobile = useIsMobile();
262
+ const [internalOpen, setInternalOpen] = React.useState(props.defaultOpen ?? false);
263
+ const isControlled = props.open !== void 0;
264
+ const open = isControlled ? props.open : internalOpen;
265
+ const { onOpenChange } = props;
266
+ const handleOpenChange = React.useCallback((newOpen) => {
267
+ if (!isControlled) setInternalOpen(newOpen);
268
+ onOpenChange?.(newOpen);
269
+ }, [isControlled, onOpenChange]);
270
+ if (isMobile) {
271
+ const drawerProps = {
272
+ ...props,
273
+ open,
274
+ onOpenChange: handleOpenChange,
275
+ snapPoints: snap ? snapPoints ?? [.5, 1] : snapPoints,
276
+ activeSnapPoint,
277
+ setActiveSnapPoint,
278
+ fadeFromIndex: snap ? fadeFromIndex ?? 0 : fadeFromIndex,
279
+ shouldScaleBackground,
280
+ dismissible
281
+ };
282
+ return /* @__PURE__ */ jsx(ResponsiveDialogContext.Provider, {
283
+ value: { snap },
284
+ children: /* @__PURE__ */ jsx(Drawer, {
285
+ ...drawerProps,
286
+ children
287
+ })
288
+ });
289
+ }
290
+ return /* @__PURE__ */ jsx(ResponsiveDialogContext.Provider, {
291
+ value: { snap },
292
+ children: /* @__PURE__ */ jsx(Dialog, {
293
+ ...props,
294
+ open,
295
+ onOpenChange: handleOpenChange,
296
+ children
297
+ })
298
+ });
299
+ }
300
+ /**
301
+ * The element that opens the responsive dialog/drawer when activated.
302
+ *
303
+ * - **Desktop** → `DialogTrigger` (Radix `Dialog.Trigger`)
304
+ * - **Mobile** → `DrawerTrigger` (Vaul `Drawer.Trigger`)
305
+ *
306
+ * Use `asChild` to render your own element as the trigger instead of
307
+ * the default button wrapper.
308
+ *
309
+ * @example
310
+ * <ResponsiveDialogTrigger asChild>
311
+ * <Button variant="outline">Open</Button>
312
+ * </ResponsiveDialogTrigger>
313
+ */
314
+ function ResponsiveDialogTrigger({ children, ...props }) {
315
+ if (useIsMobile()) return /* @__PURE__ */ jsx(DrawerTrigger, {
316
+ ...props,
317
+ children
318
+ });
319
+ return /* @__PURE__ */ jsx(DialogTrigger, {
320
+ ...props,
321
+ children
322
+ });
323
+ }
324
+ /**
325
+ * A close trigger that targets the correct primitive for the current
326
+ * viewport:
327
+ * - **Desktop** → `DialogClose` (Radix `Dialog.Close`)
328
+ * - **Mobile** → `DrawerClose` (Vaul `Drawer.Close`)
329
+ *
330
+ * Use `asChild` to wrap a custom element.
331
+ *
332
+ * @example
333
+ * <ResponsiveDialogClose asChild>
334
+ * <Button variant="ghost">Cancel</Button>
335
+ * </ResponsiveDialogClose>
336
+ */
337
+ function ResponsiveDialogClose({ children, ...props }) {
338
+ if (useIsMobile()) return /* @__PURE__ */ jsx(DrawerClose, {
339
+ ...props,
340
+ children
341
+ });
342
+ return /* @__PURE__ */ jsx(DialogClose, {
343
+ ...props,
344
+ children
345
+ });
346
+ }
347
+ /**
348
+ * The visible panel of the responsive dialog. Delegates to the
349
+ * appropriate primitive:
350
+ * - **Desktop** → `DialogContent` — centred modal with fade + zoom animations.
351
+ * The ✕ close button is shown by default (`showCloseButton={true}`).
352
+ * - **Mobile** → `DrawerContent` — bottom-sheet panel with a drag handle.
353
+ * `showCloseButton` has **no effect** on mobile; the drawer is closed
354
+ * via swipe gesture or the drag handle.
355
+ *
356
+ * @param showCloseButton - Show/hide the ✕ button in the top-right corner
357
+ * on desktop. Has no effect on mobile. Defaults to `true`.
358
+ *
359
+ * @example
360
+ * // Hide the ✕ button and force close via footer only (desktop)
361
+ * <ResponsiveDialogContent showCloseButton={false}>
362
+ * ...
363
+ * </ResponsiveDialogContent>
364
+ */
365
+ function ResponsiveDialogContent({ className, children, showCloseButton = true, ...props }) {
366
+ const isMobile = useIsMobile();
367
+ const { snap } = React.useContext(ResponsiveDialogContext);
368
+ if (isMobile) return /* @__PURE__ */ jsx(DrawerContent, {
369
+ className: cn(snap && "h-[96dvh] !max-h-[96dvh]", className),
370
+ ...props,
371
+ children
372
+ });
373
+ return /* @__PURE__ */ jsx(DialogContent, {
374
+ className,
375
+ showCloseButton,
376
+ ...props,
377
+ children
378
+ });
379
+ }
380
+ /**
381
+ * Layout wrapper for the **top section** of the responsive dialog/drawer.
382
+ *
383
+ * - **Desktop** → `DialogHeader` — vertical stack with `gap-2`
384
+ * - **Mobile** → `DrawerHeader` — vertically centred text for bottom/top
385
+ * drawers, left-aligned for side drawers
386
+ *
387
+ * Typically contains `ResponsiveDialogTitle` and optionally
388
+ * `ResponsiveDialogDescription`.
389
+ */
390
+ function ResponsiveDialogHeader({ className, leftNode, rightNode, children, ...props }) {
391
+ if (useIsMobile()) return /* @__PURE__ */ jsx(DrawerHeader, {
392
+ leftNode,
393
+ rightNode,
394
+ className,
395
+ ...props,
396
+ children
397
+ });
398
+ return /* @__PURE__ */ jsx(DialogHeader, {
399
+ className,
400
+ ...props,
401
+ children
402
+ });
403
+ }
404
+ /**
405
+ * Layout wrapper for the **bottom action area** of the responsive
406
+ * dialog/drawer.
407
+ *
408
+ * - **Desktop** → `DialogFooter` — bleeds to panel edges, top border,
409
+ * actions aligned right on `sm+`
410
+ * - **Mobile** → `DrawerFooter` — `mt-auto` stacked column at the bottom
411
+ * of the drawer
412
+ *
413
+ * @param showCloseButton - When `true`, appends a close button after
414
+ * `children`. On **desktop** this is a `DialogClose`-wrapped outline
415
+ * button; on **mobile** this is a `DrawerClose`-wrapped outline button.
416
+ * Defaults to `false`.
417
+ *
418
+ * @example
419
+ * <ResponsiveDialogFooter showCloseButton>
420
+ * <Button type="submit">Save</Button>
421
+ * </ResponsiveDialogFooter>
422
+ */
423
+ function ResponsiveDialogFooter({ className, children, showCloseButton = false, ...props }) {
424
+ if (useIsMobile()) return /* @__PURE__ */ jsxs(DrawerFooter, {
425
+ className,
426
+ ...props,
427
+ children: [children, showCloseButton && /* @__PURE__ */ jsx(DrawerClose, {
428
+ asChild: true,
429
+ children: /* @__PURE__ */ jsx(Button, {
430
+ variant: "outline",
431
+ children: "Close"
432
+ })
433
+ })]
434
+ });
435
+ return /* @__PURE__ */ jsx(DialogFooter, {
436
+ className,
437
+ showCloseButton,
438
+ ...props,
439
+ children
440
+ });
441
+ }
442
+ /**
443
+ * The **accessible title** of the responsive dialog/drawer.
444
+ *
445
+ * - **Desktop** → `DialogTitle` (linked via `aria-labelledby`)
446
+ * - **Mobile** → `DrawerTitle` (linked via `aria-labelledby`)
447
+ *
448
+ * Screen readers announce this text when the panel opens.
449
+ *
450
+ * @remarks Required for accessibility on both desktop and mobile.
451
+ */
452
+ function ResponsiveDialogTitle({ className, ...props }) {
453
+ if (useIsMobile()) return /* @__PURE__ */ jsx(DrawerTitle, {
454
+ className,
455
+ ...props
456
+ });
457
+ return /* @__PURE__ */ jsx(DialogTitle, {
458
+ className,
459
+ ...props
460
+ });
461
+ }
462
+ /**
463
+ * The **accessible description** of the responsive dialog/drawer.
464
+ *
465
+ * - **Desktop** → `DialogDescription` (linked via `aria-describedby`)
466
+ * - **Mobile** → `DrawerDescription` (linked via `aria-describedby`)
467
+ *
468
+ * @remarks Optional but recommended. Provides supplementary context for
469
+ * screen reader users after the title is announced.
470
+ */
471
+ function ResponsiveDialogDescription({ className, ...props }) {
472
+ if (useIsMobile()) return /* @__PURE__ */ jsx(DrawerDescription, {
473
+ className,
474
+ ...props
475
+ });
476
+ return /* @__PURE__ */ jsx(DialogDescription, {
477
+ className,
478
+ ...props
479
+ });
480
+ }
481
+ //#endregion
482
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle, ResponsiveDialogTrigger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manhphi1309/dialog",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "sideEffects": false,
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -29,6 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@manhphi1309/button": "*",
32
+ "@manhphi1309/drawer": "*",
33
+ "@manhphi1309/hooks": "*",
32
34
  "@manhphi1309/utils": "*",
33
35
  "class-variance-authority": "*"
34
36
  }