@hilum/ui 3.2.3 → 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.
- package/dist/index.d.ts +348 -32
- package/dist/index.js +2511 -676
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-dialog.tsx +19 -6
- package/src/components/callout.tsx +99 -0
- package/src/components/card.tsx +13 -0
- package/src/components/chart.tsx +398 -29
- package/src/components/color-picker.tsx +39 -45
- package/src/components/combobox.tsx +83 -64
- package/src/components/command.tsx +35 -0
- package/src/components/context-menu.tsx +49 -27
- package/src/components/data-transfer-controls.tsx +156 -0
- package/src/components/dialog.tsx +19 -6
- package/src/components/dropdown-menu.tsx +50 -28
- package/src/components/file-dropzone.tsx +171 -0
- package/src/components/help-tooltip.tsx +90 -0
- package/src/components/media-asset-card.tsx +118 -0
- package/src/components/menubar.tsx +52 -30
- package/src/components/popover.tsx +31 -17
- package/src/components/rich-text-editor.tsx +448 -0
- package/src/components/searchable-table.tsx +412 -0
- package/src/components/select.tsx +44 -28
- package/src/components/sidebar.tsx +105 -14
- package/src/components/stat-card.tsx +122 -8
- package/src/components/status-badge.tsx +144 -0
- package/src/components/titled-card.tsx +102 -0
- package/src/components/url-redirect-prompt.tsx +85 -0
- package/src/index.ts +10 -0
- package/src/lib/mobile-popper-sheet.ts +78 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
|
-
import { Popover } from "
|
|
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
|
|
42
|
-
<
|
|
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
|
-
</
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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="
|
|
70
|
-
value={
|
|
71
|
-
onChange={(e) =>
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
231
|
-
isSelected
|
|
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.
|
|
235
|
-
</
|
|
231
|
+
{option.avatar}
|
|
232
|
+
</div>
|
|
236
233
|
)}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
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
|
|
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
|
|