@nationaldesignstudio/react 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,526 @@
1
+ "use client";
2
+
3
+ import { Dialog as BaseDialog } from "@base-ui-components/react/dialog";
4
+ import * as React from "react";
5
+ import { tv, type VariantProps } from "tailwind-variants";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ /**
9
+ * Dialog backdrop variants
10
+ *
11
+ * Semi-transparent overlay behind the dialog content.
12
+ */
13
+ const dialogBackdropVariants = tv({
14
+ base: [
15
+ // Fixed positioning to cover viewport
16
+ "fixed inset-0",
17
+ // Semi-transparent black background using alpha token
18
+ "bg-alpha-black-50",
19
+ // Smooth opacity transition
20
+ "transition-opacity duration-200",
21
+ // Animation states
22
+ "data-[starting-style]:opacity-0",
23
+ "data-[ending-style]:opacity-0",
24
+ // Ensure backdrop covers full viewport on iOS
25
+ "min-h-dvh",
26
+ ],
27
+ });
28
+
29
+ /**
30
+ * Dialog popup variants
31
+ *
32
+ * Uses semantic overlay tokens for themeable styling:
33
+ * - color.overlay.background - Light background
34
+ * - color.overlay.border - Subtle border
35
+ * - color.overlay.text - Primary text
36
+ * - surface.overlay.radius - Rounded corners
37
+ */
38
+ const dialogPopupVariants = tv({
39
+ base: [
40
+ // Fixed positioning, centered
41
+ "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
42
+ // Layout
43
+ "flex flex-col",
44
+ // Max dimensions with viewport margin (24px on each side = 48px total)
45
+ "max-h-[calc(100vh-48px)] max-w-[calc(100vw-48px)]",
46
+ // Ensure above backdrop
47
+ "z-50",
48
+ // Animation
49
+ "transition-all duration-200",
50
+ "data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
51
+ "data-[ending-style]:scale-95 data-[ending-style]:opacity-0",
52
+ // Focus outline
53
+ "outline-none",
54
+ ],
55
+ variants: {
56
+ size: {
57
+ sm: "w-full sm:max-w-[400px]",
58
+ md: "w-full sm:max-w-[560px]",
59
+ lg: "w-full sm:max-w-[720px]",
60
+ xl: "w-full sm:max-w-[960px]",
61
+ full: "w-[calc(100vw-48px)] h-[calc(100vh-48px)]",
62
+ },
63
+ variant: {
64
+ default: [
65
+ // Background - uses overlay background token
66
+ "bg-overlay-background",
67
+ // Border - uses overlay border token
68
+ "border border-overlay-border",
69
+ // Text - uses overlay text token
70
+ "text-overlay-text",
71
+ // Border radius - uses surface overlay token
72
+ "rounded-surface-overlay",
73
+ // Shadow for elevation
74
+ "shadow-xl",
75
+ ],
76
+ minimal: [
77
+ // Transparent background, no border/shadow
78
+ "bg-black",
79
+ "border-0",
80
+ "text-white",
81
+ "rounded-8",
82
+ "shadow-none",
83
+ "overflow-hidden",
84
+ ],
85
+ },
86
+ },
87
+ compoundVariants: [
88
+ // Default variant padding by size
89
+ { variant: "default", size: "sm", class: "p-24" },
90
+ { variant: "default", size: "md", class: "p-32" },
91
+ { variant: "default", size: "lg", class: "p-32" },
92
+ { variant: "default", size: "xl", class: "p-40" },
93
+ { variant: "default", size: "full", class: "p-40" },
94
+ // Minimal variant has no padding
95
+ { variant: "minimal", size: "sm", class: "p-0" },
96
+ { variant: "minimal", size: "md", class: "p-0" },
97
+ { variant: "minimal", size: "lg", class: "p-0" },
98
+ { variant: "minimal", size: "xl", class: "p-0" },
99
+ { variant: "minimal", size: "full", class: "p-0" },
100
+ ],
101
+ defaultVariants: {
102
+ size: "md",
103
+ variant: "default",
104
+ },
105
+ });
106
+
107
+ // ============================================================================
108
+ // Dialog Root
109
+ // ============================================================================
110
+
111
+ export interface DialogRootProps extends BaseDialog.Root.Props {
112
+ children: React.ReactNode;
113
+ }
114
+
115
+ /**
116
+ * Dialog Root
117
+ *
118
+ * Groups all dialog parts and manages open/close state.
119
+ * Provides focus trap, scroll lock, and escape key handling automatically.
120
+ */
121
+ const DialogRoot = ({ children, ...props }: DialogRootProps) => {
122
+ return <BaseDialog.Root {...props}>{children}</BaseDialog.Root>;
123
+ };
124
+
125
+ // ============================================================================
126
+ // Dialog Trigger
127
+ // ============================================================================
128
+
129
+ export interface DialogTriggerProps
130
+ extends React.ComponentProps<typeof BaseDialog.Trigger> {
131
+ className?: string;
132
+ }
133
+
134
+ /**
135
+ * Dialog Trigger
136
+ *
137
+ * The element that triggers the dialog to open on click.
138
+ * When children is a single React element, uses `render` prop to avoid wrapper element.
139
+ */
140
+ const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
141
+ ({ className, children, ...props }, ref) => {
142
+ // If children is a single React element, use render prop to avoid wrapper
143
+ const isSingleElement = React.isValidElement(children);
144
+
145
+ if (isSingleElement) {
146
+ return (
147
+ <BaseDialog.Trigger
148
+ ref={ref}
149
+ className={className}
150
+ render={children as React.ReactElement<Record<string, unknown>>}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+
156
+ return (
157
+ <BaseDialog.Trigger ref={ref} className={className} {...props}>
158
+ {children}
159
+ </BaseDialog.Trigger>
160
+ );
161
+ },
162
+ );
163
+ DialogTrigger.displayName = "DialogTrigger";
164
+
165
+ // ============================================================================
166
+ // Dialog Portal
167
+ // ============================================================================
168
+
169
+ export interface DialogPortalProps extends BaseDialog.Portal.Props {
170
+ children: React.ReactNode;
171
+ }
172
+
173
+ /**
174
+ * Dialog Portal
175
+ *
176
+ * Renders the dialog in a portal outside the DOM hierarchy.
177
+ * This ensures proper stacking context and avoids z-index issues.
178
+ */
179
+ const DialogPortal = ({ children, ...props }: DialogPortalProps) => {
180
+ return <BaseDialog.Portal {...props}>{children}</BaseDialog.Portal>;
181
+ };
182
+
183
+ // ============================================================================
184
+ // Dialog Backdrop
185
+ // ============================================================================
186
+
187
+ export interface DialogBackdropProps
188
+ extends Omit<React.ComponentProps<typeof BaseDialog.Backdrop>, "className"> {
189
+ className?: string;
190
+ }
191
+
192
+ /**
193
+ * Dialog Backdrop
194
+ *
195
+ * Semi-transparent overlay that covers the page behind the dialog.
196
+ * Clicking the backdrop closes the dialog by default.
197
+ */
198
+ const DialogBackdrop = React.forwardRef<HTMLDivElement, DialogBackdropProps>(
199
+ ({ className, ...props }, ref) => {
200
+ return (
201
+ <BaseDialog.Backdrop
202
+ ref={ref}
203
+ className={cn(dialogBackdropVariants(), className)}
204
+ {...props}
205
+ />
206
+ );
207
+ },
208
+ );
209
+ DialogBackdrop.displayName = "DialogBackdrop";
210
+
211
+ // ============================================================================
212
+ // Dialog Popup
213
+ // ============================================================================
214
+
215
+ export interface DialogPopupProps
216
+ extends Omit<React.ComponentProps<typeof BaseDialog.Popup>, "className">,
217
+ VariantProps<typeof dialogPopupVariants> {
218
+ className?: string;
219
+ }
220
+
221
+ /**
222
+ * Dialog Popup
223
+ *
224
+ * The dialog content container. Centered on screen with configurable size.
225
+ * Use `variant="minimal"` for borderless dialogs (video modals, etc.)
226
+ */
227
+ const DialogPopup = React.forwardRef<HTMLDivElement, DialogPopupProps>(
228
+ ({ className, size, variant, ...props }, ref) => {
229
+ return (
230
+ <BaseDialog.Popup
231
+ ref={ref}
232
+ className={cn(dialogPopupVariants({ size, variant }), className)}
233
+ {...props}
234
+ />
235
+ );
236
+ },
237
+ );
238
+ DialogPopup.displayName = "DialogPopup";
239
+
240
+ // ============================================================================
241
+ // Dialog Title
242
+ // ============================================================================
243
+
244
+ export interface DialogTitleProps
245
+ extends Omit<React.ComponentProps<typeof BaseDialog.Title>, "className"> {
246
+ className?: string;
247
+ }
248
+
249
+ /**
250
+ * Dialog Title
251
+ *
252
+ * Accessible title for the dialog. Should be used for screen reader support.
253
+ */
254
+ const DialogTitle = React.forwardRef<HTMLHeadingElement, DialogTitleProps>(
255
+ ({ className, ...props }, ref) => {
256
+ return (
257
+ <BaseDialog.Title
258
+ ref={ref}
259
+ className={cn("typography-h4-md font-semibold mb-8", className)}
260
+ {...props}
261
+ />
262
+ );
263
+ },
264
+ );
265
+ DialogTitle.displayName = "DialogTitle";
266
+
267
+ // ============================================================================
268
+ // Dialog Description
269
+ // ============================================================================
270
+
271
+ export interface DialogDescriptionProps
272
+ extends Omit<
273
+ React.ComponentProps<typeof BaseDialog.Description>,
274
+ "className"
275
+ > {
276
+ className?: string;
277
+ }
278
+
279
+ /**
280
+ * Dialog Description
281
+ *
282
+ * Accessible description for the dialog content.
283
+ */
284
+ const DialogDescription = React.forwardRef<
285
+ HTMLParagraphElement,
286
+ DialogDescriptionProps
287
+ >(({ className, ...props }, ref) => {
288
+ return (
289
+ <BaseDialog.Description
290
+ ref={ref}
291
+ className={cn("typography-body-md-md text-overlay-text-muted", className)}
292
+ {...props}
293
+ />
294
+ );
295
+ });
296
+ DialogDescription.displayName = "DialogDescription";
297
+
298
+ // ============================================================================
299
+ // Dialog Close
300
+ // ============================================================================
301
+
302
+ export interface DialogCloseProps
303
+ extends Omit<React.ComponentProps<typeof BaseDialog.Close>, "className"> {
304
+ className?: string;
305
+ }
306
+
307
+ /**
308
+ * Dialog Close
309
+ *
310
+ * Close button for the dialog. Can be placed anywhere within the dialog.
311
+ */
312
+ const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
313
+ ({ className, ...props }, ref) => {
314
+ return (
315
+ <BaseDialog.Close
316
+ ref={ref}
317
+ className={cn(
318
+ "absolute right-16 top-16 rounded-surface-ui-small p-8",
319
+ "text-overlay-text-muted hover:text-overlay-text",
320
+ "hover:bg-bg-section",
321
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
322
+ "transition-colors duration-150",
323
+ className,
324
+ )}
325
+ {...props}
326
+ />
327
+ );
328
+ },
329
+ );
330
+ DialogClose.displayName = "DialogClose";
331
+
332
+ // ============================================================================
333
+ // Dialog Body
334
+ // ============================================================================
335
+
336
+ export interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {
337
+ className?: string;
338
+ }
339
+
340
+ /**
341
+ * Dialog Body
342
+ *
343
+ * Container for the main dialog content. Handles overflow scrolling.
344
+ */
345
+ const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(
346
+ ({ className, ...props }, ref) => {
347
+ return (
348
+ <div
349
+ ref={ref}
350
+ className={cn("flex-1 overflow-y-auto", className)}
351
+ {...props}
352
+ />
353
+ );
354
+ },
355
+ );
356
+ DialogBody.displayName = "DialogBody";
357
+
358
+ // ============================================================================
359
+ // Dialog Footer
360
+ // ============================================================================
361
+
362
+ export interface DialogFooterProps
363
+ extends React.HTMLAttributes<HTMLDivElement> {
364
+ className?: string;
365
+ }
366
+
367
+ /**
368
+ * Dialog Footer
369
+ *
370
+ * Container for dialog actions (buttons, etc.). Typically placed at the bottom.
371
+ */
372
+ const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(
373
+ ({ className, ...props }, ref) => {
374
+ return (
375
+ <div
376
+ ref={ref}
377
+ className={cn(
378
+ "flex items-center justify-end gap-12 pt-24 mt-auto",
379
+ className,
380
+ )}
381
+ {...props}
382
+ />
383
+ );
384
+ },
385
+ );
386
+ DialogFooter.displayName = "DialogFooter";
387
+
388
+ // ============================================================================
389
+ // Simple Dialog Component
390
+ // ============================================================================
391
+
392
+ export interface DialogProps {
393
+ /** The content to show in the dialog */
394
+ children: React.ReactNode;
395
+ /** The element that triggers the dialog (optional for controlled mode) */
396
+ trigger?: React.ReactNode;
397
+ /** Title for the dialog */
398
+ title?: React.ReactNode;
399
+ /** Description for the dialog */
400
+ description?: React.ReactNode;
401
+ /** Size of the dialog */
402
+ size?: "sm" | "md" | "lg" | "xl" | "full";
403
+ /** Visual variant: "default" for card style, "minimal" for borderless (video modals) */
404
+ variant?: "default" | "minimal";
405
+ /** Whether to show a close button */
406
+ showClose?: boolean;
407
+ /** Controlled open state */
408
+ open?: boolean;
409
+ /** Default open state */
410
+ defaultOpen?: boolean;
411
+ /** Callback when open state changes */
412
+ onOpenChange?: (open: boolean) => void;
413
+ /** Additional className for the popup */
414
+ className?: string;
415
+ }
416
+
417
+ /**
418
+ * Dialog
419
+ *
420
+ * A simple, pre-composed dialog component for common use cases.
421
+ * For more complex needs, use the compound components directly.
422
+ *
423
+ * @example
424
+ * ```tsx
425
+ * // With trigger
426
+ * <Dialog
427
+ * trigger={<Button>Open Dialog</Button>}
428
+ * title="Dialog Title"
429
+ * description="This is the dialog description."
430
+ * >
431
+ * <p>Dialog content goes here.</p>
432
+ * </Dialog>
433
+ *
434
+ * // Controlled mode
435
+ * <Dialog
436
+ * open={isOpen}
437
+ * onOpenChange={setIsOpen}
438
+ * title="Controlled Dialog"
439
+ * >
440
+ * <p>Content here</p>
441
+ * </Dialog>
442
+ * ```
443
+ */
444
+ const Dialog = ({
445
+ children,
446
+ trigger,
447
+ title,
448
+ description,
449
+ size = "md",
450
+ variant = "default",
451
+ showClose = true,
452
+ open,
453
+ defaultOpen,
454
+ onOpenChange,
455
+ className,
456
+ }: DialogProps) => {
457
+ return (
458
+ <DialogRoot
459
+ open={open}
460
+ defaultOpen={defaultOpen}
461
+ onOpenChange={onOpenChange}
462
+ >
463
+ {trigger && <DialogTrigger>{trigger}</DialogTrigger>}
464
+ <DialogPortal>
465
+ <DialogBackdrop />
466
+ <DialogPopup size={size} variant={variant} className={className}>
467
+ {showClose && (
468
+ <DialogClose>
469
+ <svg
470
+ width="16"
471
+ height="16"
472
+ viewBox="0 0 16 16"
473
+ fill="none"
474
+ aria-hidden="true"
475
+ >
476
+ <path
477
+ d="M2 2L14 14M2 14L14 2"
478
+ stroke="currentColor"
479
+ strokeWidth="2"
480
+ strokeLinecap="round"
481
+ />
482
+ </svg>
483
+ <span className="sr-only">Close</span>
484
+ </DialogClose>
485
+ )}
486
+ {title && <DialogTitle>{title}</DialogTitle>}
487
+ {description && <DialogDescription>{description}</DialogDescription>}
488
+ <DialogBody>{children}</DialogBody>
489
+ </DialogPopup>
490
+ </DialogPortal>
491
+ </DialogRoot>
492
+ );
493
+ };
494
+
495
+ // ============================================================================
496
+ // Compound Component Export
497
+ // ============================================================================
498
+
499
+ export const DialogParts = Object.assign(DialogRoot, {
500
+ Root: DialogRoot,
501
+ Trigger: DialogTrigger,
502
+ Portal: DialogPortal,
503
+ Backdrop: DialogBackdrop,
504
+ Popup: DialogPopup,
505
+ Title: DialogTitle,
506
+ Description: DialogDescription,
507
+ Close: DialogClose,
508
+ Body: DialogBody,
509
+ Footer: DialogFooter,
510
+ });
511
+
512
+ export {
513
+ Dialog,
514
+ DialogRoot,
515
+ DialogTrigger,
516
+ DialogPortal,
517
+ DialogBackdrop,
518
+ DialogPopup,
519
+ DialogTitle,
520
+ DialogDescription,
521
+ DialogClose,
522
+ DialogBody,
523
+ DialogFooter,
524
+ dialogBackdropVariants,
525
+ dialogPopupVariants,
526
+ };