@hilum/ui 3.2.2 → 3.2.4

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
- import { Popover } from "radix-ui";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
6
6
 
7
7
  interface ColorPickerProps {
8
8
  value: string;
@@ -38,8 +38,8 @@ function ColorPicker({
38
38
  };
39
39
 
40
40
  return (
41
- <Popover.Root>
42
- <Popover.Trigger asChild>
41
+ <Popover>
42
+ <PopoverTrigger asChild>
43
43
  <button
44
44
  type="button"
45
45
  disabled={disabled}
@@ -58,52 +58,46 @@ function ColorPicker({
58
58
  style={{ backgroundColor: value }}
59
59
  />
60
60
  </button>
61
- </Popover.Trigger>
62
- <Popover.Portal>
63
- <Popover.Content
64
- align="start"
65
- sideOffset={6}
66
- className="z-50 w-56 rounded-xl bg-card p-3 shadow-natural outline-none"
67
- >
61
+ </PopoverTrigger>
62
+ <PopoverContent align="start" sideOffset={6} className="w-56 p-3 max-sm:px-3">
63
+ <input
64
+ type="color"
65
+ value={isValidHex(hex) ? hex : "#000000"}
66
+ onChange={(e) => {
67
+ setHex(e.target.value);
68
+ onChange(e.target.value);
69
+ }}
70
+ className="h-10 w-full cursor-pointer rounded-md border border-border bg-card p-0.5"
71
+ />
72
+ <div className="mt-2 flex items-center gap-2">
73
+ <span className="caption text-muted-foreground">Hex</span>
68
74
  <input
69
- type="color"
70
- value={isValidHex(hex) ? hex : "#000000"}
71
- onChange={(e) => {
72
- setHex(e.target.value);
73
- onChange(e.target.value);
75
+ type="text"
76
+ value={hex}
77
+ onChange={(e) => setHex(e.target.value)}
78
+ onBlur={(e) => commit(e.target.value)}
79
+ onKeyDown={(e) => {
80
+ if (e.key === "Enter") commit((e.target as HTMLInputElement).value);
74
81
  }}
75
- className="h-10 w-full cursor-pointer rounded-md border border-border bg-card p-0.5"
82
+ className="h-10 flex-1 rounded-md border border-border bg-card px-2 caption text-foreground focus:outline-none focus:border-brand-primary"
76
83
  />
77
- <div className="mt-2 flex items-center gap-2">
78
- <span className="caption text-muted-foreground">Hex</span>
79
- <input
80
- type="text"
81
- value={hex}
82
- onChange={(e) => setHex(e.target.value)}
83
- onBlur={(e) => commit(e.target.value)}
84
- onKeyDown={(e) => {
85
- if (e.key === "Enter") commit((e.target as HTMLInputElement).value);
86
- }}
87
- className="h-10 flex-1 rounded-md border border-border bg-card px-2 caption text-foreground focus:outline-none focus:border-brand-primary"
88
- />
84
+ </div>
85
+ {presets && presets.length > 0 && (
86
+ <div className="mt-3 grid grid-cols-4 gap-2">
87
+ {presets.map((p) => (
88
+ <button
89
+ key={p}
90
+ type="button"
91
+ onClick={() => onChange(p)}
92
+ aria-label={p}
93
+ className="size-10 rounded-md border border-black/10 transition-transform hover:scale-105 active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30"
94
+ style={{ backgroundColor: p }}
95
+ />
96
+ ))}
89
97
  </div>
90
- {presets && presets.length > 0 && (
91
- <div className="mt-3 grid grid-cols-4 gap-2">
92
- {presets.map((p) => (
93
- <button
94
- key={p}
95
- type="button"
96
- onClick={() => onChange(p)}
97
- aria-label={p}
98
- className="size-10 rounded-md border border-black/10 transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30"
99
- style={{ backgroundColor: p }}
100
- />
101
- ))}
102
- </div>
103
- )}
104
- </Popover.Content>
105
- </Popover.Portal>
106
- </Popover.Root>
98
+ )}
99
+ </PopoverContent>
100
+ </Popover>
107
101
  );
108
102
  }
109
103
 
@@ -174,74 +174,93 @@ function Combobox({
174
174
 
175
175
  {/* Dropdown */}
176
176
  {open && (
177
- <div className="absolute z-50 mt-1 w-full overflow-hidden rounded-lg border border-border bg-card shadow-elevated">
178
- <ul id={listboxId} role="listbox" className="max-h-60 overflow-auto py-1">
179
- {filtered.length === 0 ? (
180
- <li className="px-3 py-2 body text-muted-foreground">{emptyText}</li>
181
- ) : (
182
- filtered.map((option, idx) => {
183
- const isSelected = option.value === value;
184
- const isActive = idx === activeIndex;
185
- return (
186
- <li
187
- key={option.value}
188
- id={`${listboxId}-option-${idx}`}
189
- role="option"
190
- aria-selected={isSelected}
191
- className={cn(
192
- "flex min-h-10 cursor-pointer select-none items-center gap-2.5 px-3 py-2 body transition-colors",
193
- isSelected
194
- ? "bg-brand-primary text-background"
195
- : isActive
196
- ? "bg-muted text-foreground"
197
- : "text-foreground hover:bg-muted",
198
- )}
199
- onMouseDown={(e) => e.preventDefault()}
200
- onMouseEnter={() => setActiveIndex(idx)}
201
- onClick={() => selectOption(option)}
202
- >
203
- {/* Avatar */}
204
- {option.avatar && (
205
- <div
206
- className={cn(
207
- "flex size-6 shrink-0 items-center justify-center rounded-full caption-xs font-semibold",
208
- isSelected
209
- ? "bg-card/20 text-background"
210
- : "bg-muted text-muted-foreground",
211
- )}
212
- >
213
- {option.avatar}
214
- </div>
215
- )}
216
- {/* Status dot */}
217
- {option.statusColor && !option.avatar && (
218
- <div
219
- className="size-2 shrink-0 rounded-full"
220
- style={{ backgroundColor: option.statusColor }}
221
- />
222
- )}
223
- <div className="min-w-0 flex-1">
224
- <p className={cn("truncate", isSelected && "font-semibold")}>
225
- {option.label}
226
- </p>
227
- {option.description && (
228
- <p
177
+ <>
178
+ <button
179
+ type="button"
180
+ aria-label="Close options"
181
+ className="fixed inset-0 z-40 hidden bg-black/30 backdrop-blur-sm max-sm:block"
182
+ onClick={closeDropdown}
183
+ />
184
+ <div
185
+ className={cn(
186
+ "absolute z-50 mt-1 w-full overflow-hidden rounded-lg border border-border bg-card shadow-elevated",
187
+ "max-sm:fixed max-sm:inset-x-3 max-sm:bottom-3 max-sm:[bottom:max(0.75rem,env(safe-area-inset-bottom))] max-sm:top-auto max-sm:mt-0 max-sm:w-auto",
188
+ "max-sm:max-h-[min(70dvh,28rem)] max-sm:rounded-2xl max-sm:p-2 max-sm:pt-5",
189
+ "max-sm:before:absolute max-sm:before:left-1/2 max-sm:before:top-2 max-sm:before:h-1 max-sm:before:w-9 max-sm:before:-translate-x-1/2 max-sm:before:rounded-full max-sm:before:bg-muted-foreground/35",
190
+ )}
191
+ >
192
+ <ul
193
+ id={listboxId}
194
+ role="listbox"
195
+ className="max-h-60 overflow-auto py-1 max-sm:max-h-[calc(min(70dvh,28rem)-3rem)]"
196
+ >
197
+ {filtered.length === 0 ? (
198
+ <li className="px-3 py-2 body text-muted-foreground">{emptyText}</li>
199
+ ) : (
200
+ filtered.map((option, idx) => {
201
+ const isSelected = option.value === value;
202
+ const isActive = idx === activeIndex;
203
+ return (
204
+ <li
205
+ key={option.value}
206
+ id={`${listboxId}-option-${idx}`}
207
+ role="option"
208
+ aria-selected={isSelected}
209
+ className={cn(
210
+ "flex min-h-10 cursor-pointer select-none items-center gap-2.5 px-3 py-2 body transition-colors",
211
+ isSelected
212
+ ? "bg-brand-primary text-background"
213
+ : isActive
214
+ ? "bg-muted text-foreground"
215
+ : "text-foreground hover:bg-muted",
216
+ )}
217
+ onMouseDown={(e) => e.preventDefault()}
218
+ onMouseEnter={() => setActiveIndex(idx)}
219
+ onClick={() => selectOption(option)}
220
+ >
221
+ {/* Avatar */}
222
+ {option.avatar && (
223
+ <div
229
224
  className={cn(
230
- "caption truncate",
231
- isSelected ? "text-background/70" : "text-muted-foreground",
225
+ "flex size-6 shrink-0 items-center justify-center rounded-full caption-xs font-semibold",
226
+ isSelected
227
+ ? "bg-card/20 text-background"
228
+ : "bg-muted text-muted-foreground",
232
229
  )}
233
230
  >
234
- {option.description}
235
- </p>
231
+ {option.avatar}
232
+ </div>
236
233
  )}
237
- </div>
238
- {isSelected && <Check size={14} className="shrink-0" />}
239
- </li>
240
- );
241
- })
242
- )}
243
- </ul>
244
- </div>
234
+ {/* Status dot */}
235
+ {option.statusColor && !option.avatar && (
236
+ <div
237
+ className="size-2 shrink-0 rounded-full"
238
+ style={{ backgroundColor: option.statusColor }}
239
+ />
240
+ )}
241
+ <div className="min-w-0 flex-1">
242
+ <p className={cn("truncate", isSelected && "font-semibold")}>
243
+ {option.label}
244
+ </p>
245
+ {option.description && (
246
+ <p
247
+ className={cn(
248
+ "caption truncate",
249
+ isSelected ? "text-background/70" : "text-muted-foreground",
250
+ )}
251
+ >
252
+ {option.description}
253
+ </p>
254
+ )}
255
+ </div>
256
+ {isSelected && <Check size={14} className="shrink-0" />}
257
+ </li>
258
+ );
259
+ })
260
+ )}
261
+ </ul>
262
+ </div>
263
+ </>
245
264
  )}
246
265
  </div>
247
266
  );
@@ -3,6 +3,13 @@
3
3
  import * as React from "react";
4
4
  import { Search } from "lucide-react";
5
5
  import { cn } from "../lib/utils";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "./dialog";
6
13
 
7
14
  interface CommandContextValue {
8
15
  query: string;
@@ -238,10 +245,38 @@ function CommandShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanE
238
245
  }
239
246
  CommandShortcut.displayName = "CommandShortcut";
240
247
 
248
+ interface CommandDialogProps extends React.ComponentProps<typeof Dialog> {
249
+ title?: string;
250
+ description?: string;
251
+ className?: string;
252
+ }
253
+
254
+ function CommandDialog({
255
+ title = "Command Palette",
256
+ description = "Search for a command to run...",
257
+ children,
258
+ className,
259
+ ...props
260
+ }: CommandDialogProps) {
261
+ return (
262
+ <Dialog {...props}>
263
+ <DialogContent className={cn("gap-0 overflow-hidden p-0 sm:max-w-xl", className)}>
264
+ <DialogHeader className="sr-only">
265
+ <DialogTitle>{title}</DialogTitle>
266
+ <DialogDescription>{description}</DialogDescription>
267
+ </DialogHeader>
268
+ <Command className="rounded-none border-0 shadow-none">{children}</Command>
269
+ </DialogContent>
270
+ </Dialog>
271
+ );
272
+ }
273
+ CommandDialog.displayName = "CommandDialog";
274
+
241
275
  Command.displayName = "Command";
242
276
 
243
277
  export {
244
278
  Command,
279
+ CommandDialog,
245
280
  CommandInput,
246
281
  CommandList,
247
282
  CommandEmpty,
@@ -4,6 +4,12 @@ import * as React from "react";
4
4
  import { ContextMenu } from "radix-ui";
5
5
  import { Check, ChevronRight, Circle } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
+ import {
8
+ mobilePopperSheetMotionClassName,
9
+ mobilePopperSheetPositionClassName,
10
+ mobilePopperSheetStyle,
11
+ mobilePopperSheetSurfaceClassName,
12
+ } from "../lib/mobile-popper-sheet";
7
13
 
8
14
  const ContextMenuRoot = ContextMenu.Root;
9
15
  const ContextMenuTrigger = ContextMenu.Trigger;
@@ -16,21 +22,29 @@ const ContextMenuContent = React.forwardRef<
16
22
  React.ComponentRef<typeof ContextMenu.Content>,
17
23
  React.ComponentPropsWithoutRef<typeof ContextMenu.Content>
18
24
  >(({ className, ...props }, ref) => (
19
- <ContextMenu.Portal>
20
- <ContextMenu.Content
21
- ref={ref}
22
- className={cn(
23
- "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-card-foreground shadow-elevated outline-none",
24
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
25
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
26
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
27
- "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
28
- "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
29
- className,
30
- )}
31
- {...props}
32
- />
33
- </ContextMenu.Portal>
25
+ <>
26
+ <style>{mobilePopperSheetStyle}</style>
27
+ <ContextMenu.Portal>
28
+ <ContextMenu.Content
29
+ ref={ref}
30
+ data-hilum-mobile-sheet="true"
31
+ className={cn(
32
+ "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-card-foreground shadow-elevated outline-none",
33
+ mobilePopperSheetPositionClassName,
34
+ mobilePopperSheetSurfaceClassName,
35
+ "max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
36
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
37
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
38
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
39
+ mobilePopperSheetMotionClassName,
40
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
41
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ </ContextMenu.Portal>
47
+ </>
34
48
  ));
35
49
  ContextMenuContent.displayName = "ContextMenuContent";
36
50
 
@@ -161,18 +175,26 @@ const ContextMenuSubContent = React.forwardRef<
161
175
  React.ComponentRef<typeof ContextMenu.SubContent>,
162
176
  React.ComponentPropsWithoutRef<typeof ContextMenu.SubContent>
163
177
  >(({ className, ...props }, ref) => (
164
- <ContextMenu.SubContent
165
- ref={ref}
166
- className={cn(
167
- "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-card-foreground shadow-elevated outline-none",
168
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
169
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
170
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
171
- "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
172
- className,
173
- )}
174
- {...props}
175
- />
178
+ <>
179
+ <style>{mobilePopperSheetStyle}</style>
180
+ <ContextMenu.SubContent
181
+ ref={ref}
182
+ data-hilum-mobile-sheet="true"
183
+ className={cn(
184
+ "z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card p-1 text-card-foreground shadow-elevated outline-none",
185
+ mobilePopperSheetPositionClassName,
186
+ mobilePopperSheetSurfaceClassName,
187
+ "max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
188
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
189
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
190
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
191
+ mobilePopperSheetMotionClassName,
192
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
193
+ className,
194
+ )}
195
+ {...props}
196
+ />
197
+ </>
176
198
  ));
177
199
  ContextMenuSubContent.displayName = "ContextMenuSubContent";
178
200
 
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Download, Loader2, MoreHorizontal, Upload } from "lucide-react";
5
+ import { Button } from "./button";
6
+ import { ButtonGroup, ButtonGroupItem } from "./button-group";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from "./dropdown-menu";
13
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
14
+ import { cn } from "../lib/utils";
15
+
16
+ interface DataTransferScopeOption {
17
+ value: string;
18
+ label: string;
19
+ }
20
+
21
+ interface DataTransferAction {
22
+ label: string;
23
+ shortLabel?: string;
24
+ ariaLabel?: string;
25
+ icon?: React.ReactNode;
26
+ loading?: boolean;
27
+ disabled?: boolean;
28
+ onSelect: () => void;
29
+ }
30
+
31
+ interface DataTransferControlsProps {
32
+ scopeValue?: string;
33
+ onScopeChange?: (value: string) => void;
34
+ scopeOptions?: DataTransferScopeOption[];
35
+ scopePlaceholder?: string;
36
+ actions: DataTransferAction[];
37
+ compact?: boolean;
38
+ compactLabel?: string;
39
+ className?: string;
40
+ selectClassName?: string;
41
+ actionGroupClassName?: string;
42
+ }
43
+
44
+ function normalizeIcon(icon: React.ReactNode, fallback: React.ReactNode, loading?: boolean) {
45
+ if (loading) return <Loader2 className="size-4 animate-spin" aria-hidden="true" />;
46
+ return icon ?? fallback;
47
+ }
48
+
49
+ function DataTransferControls({
50
+ scopeValue,
51
+ onScopeChange,
52
+ scopeOptions = [],
53
+ scopePlaceholder = "Scope",
54
+ actions,
55
+ compact = false,
56
+ compactLabel = "Import and export actions",
57
+ className,
58
+ selectClassName,
59
+ actionGroupClassName,
60
+ }: DataTransferControlsProps) {
61
+ const hasScope = scopeOptions.length > 0 && scopeValue !== undefined && onScopeChange;
62
+
63
+ const renderScopeSelect = () => {
64
+ if (!hasScope) return null;
65
+
66
+ return (
67
+ <Select value={scopeValue} onValueChange={onScopeChange}>
68
+ <SelectTrigger
69
+ aria-label={scopePlaceholder}
70
+ className={cn("min-h-10 w-full max-w-full bg-background sm:w-[180px]", selectClassName)}
71
+ >
72
+ <SelectValue placeholder={scopePlaceholder} />
73
+ </SelectTrigger>
74
+ <SelectContent>
75
+ {scopeOptions.map((option) => (
76
+ <SelectItem key={option.value} value={option.value}>
77
+ {option.label}
78
+ </SelectItem>
79
+ ))}
80
+ </SelectContent>
81
+ </Select>
82
+ );
83
+ };
84
+
85
+ const defaultActionIcon = (index: number) =>
86
+ index === 0 ? (
87
+ <Upload className="size-4" aria-hidden="true" />
88
+ ) : (
89
+ <Download className="size-4" aria-hidden="true" />
90
+ );
91
+
92
+ return (
93
+ <div
94
+ className={cn(
95
+ compact
96
+ ? "grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2"
97
+ : "grid w-full min-w-0 gap-2 sm:flex sm:w-auto sm:flex-wrap sm:items-center",
98
+ className,
99
+ )}
100
+ >
101
+ {renderScopeSelect()}
102
+
103
+ {compact ? (
104
+ <DropdownMenu>
105
+ <DropdownMenuTrigger asChild>
106
+ <Button
107
+ type="button"
108
+ variant="outline"
109
+ size="icon"
110
+ className="size-10"
111
+ aria-label={compactLabel}
112
+ >
113
+ <MoreHorizontal className="size-4" aria-hidden="true" />
114
+ </Button>
115
+ </DropdownMenuTrigger>
116
+ <DropdownMenuContent align="end" className="w-56">
117
+ {actions.map((action, index) => {
118
+ const disabled = Boolean(action.disabled || action.loading);
119
+
120
+ return (
121
+ <DropdownMenuItem
122
+ key={`${action.label}-${index}`}
123
+ disabled={disabled}
124
+ onClick={action.onSelect}
125
+ >
126
+ {normalizeIcon(action.icon, defaultActionIcon(index), action.loading)}
127
+ {action.label}
128
+ </DropdownMenuItem>
129
+ );
130
+ })}
131
+ </DropdownMenuContent>
132
+ </DropdownMenu>
133
+ ) : (
134
+ <ButtonGroup className={cn("grid w-full grid-cols-3 sm:inline-flex sm:w-auto", actionGroupClassName)}>
135
+ {actions.map((action, index) => (
136
+ <ButtonGroupItem
137
+ key={`${action.label}-${index}`}
138
+ className="min-h-10 justify-center px-3 disabled:pointer-events-none disabled:opacity-50"
139
+ onClick={action.onSelect}
140
+ disabled={action.disabled || action.loading}
141
+ aria-label={action.ariaLabel ?? action.label}
142
+ >
143
+ {normalizeIcon(action.icon, defaultActionIcon(index), action.loading)}
144
+ {action.shortLabel ?? action.label}
145
+ </ButtonGroupItem>
146
+ ))}
147
+ </ButtonGroup>
148
+ )}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ DataTransferControls.displayName = "DataTransferControls";
154
+
155
+ export { DataTransferControls };
156
+ export type { DataTransferAction, DataTransferControlsProps, DataTransferScopeOption };
@@ -4,6 +4,11 @@ import * as React from "react";
4
4
  import { Dialog } from "radix-ui";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
+ import {
8
+ desktopDialogContentClassName,
9
+ dialogSheetMotionClassName,
10
+ mobileDialogSheetContentClassName,
11
+ } from "../lib/mobile-popper-sheet";
7
12
 
8
13
  const DialogRoot = Dialog.Root;
9
14
  const DialogTrigger = Dialog.Trigger;
@@ -35,11 +40,10 @@ const DialogContent = React.forwardRef<
35
40
  <Dialog.Content
36
41
  ref={ref}
37
42
  className={cn(
38
- "fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
39
- "w-full max-w-lg bg-card rounded-2xl shadow-elevated p-6",
40
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
41
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
42
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
43
+ mobileDialogSheetContentClassName,
44
+ desktopDialogContentClassName,
45
+ "sm:max-w-lg",
46
+ dialogSheetMotionClassName,
43
47
  className,
44
48
  )}
45
49
  {...props}
@@ -60,7 +64,16 @@ function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
60
64
  DialogHeader.displayName = "DialogHeader";
61
65
 
62
66
  function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
63
- return <div className={cn("flex items-center justify-end gap-2 mt-6", className)} {...props} />;
67
+ return (
68
+ <div
69
+ className={cn(
70
+ "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end",
71
+ "[&>*]:w-full sm:[&>*]:w-auto",
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
64
77
  }
65
78
  DialogFooter.displayName = "DialogFooter";
66
79