@voila.dev/ui 1.1.9
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/package.json +78 -0
- package/src/components/ui/accordion.tsx +79 -0
- package/src/components/ui/alert-dialog.tsx +198 -0
- package/src/components/ui/alert.tsx +105 -0
- package/src/components/ui/aspect-ratio.tsx +30 -0
- package/src/components/ui/avatar.tsx +125 -0
- package/src/components/ui/badge-variants.ts +166 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/banner.tsx +84 -0
- package/src/components/ui/breadcrumb.tsx +127 -0
- package/src/components/ui/button-group.tsx +99 -0
- package/src/components/ui/button-variants.ts +108 -0
- package/src/components/ui/button.tsx +54 -0
- package/src/components/ui/calendar.tsx +266 -0
- package/src/components/ui/card.tsx +155 -0
- package/src/components/ui/carousel.tsx +319 -0
- package/src/components/ui/chat.tsx +896 -0
- package/src/components/ui/checkbox-group.tsx +44 -0
- package/src/components/ui/checkbox.tsx +34 -0
- package/src/components/ui/chip.tsx +60 -0
- package/src/components/ui/collapsible.tsx +44 -0
- package/src/components/ui/color-picker.tsx +191 -0
- package/src/components/ui/combobox.tsx +317 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/confirm-dialog.tsx +125 -0
- package/src/components/ui/context-menu.tsx +254 -0
- package/src/components/ui/copyable-text.tsx +65 -0
- package/src/components/ui/date-picker.tsx +316 -0
- package/src/components/ui/date-time-picker.tsx +1071 -0
- package/src/components/ui/dialog.tsx +176 -0
- package/src/components/ui/direction.tsx +15 -0
- package/src/components/ui/drawer.tsx +178 -0
- package/src/components/ui/dropdown-menu.tsx +247 -0
- package/src/components/ui/empty.tsx +131 -0
- package/src/components/ui/field.tsx +257 -0
- package/src/components/ui/formatted-input.tsx +219 -0
- package/src/components/ui/gallery.tsx +161 -0
- package/src/components/ui/hover-card.tsx +77 -0
- package/src/components/ui/icon-picker.tsx +193 -0
- package/src/components/ui/image-cropper.tsx +787 -0
- package/src/components/ui/image-upload-field.tsx +344 -0
- package/src/components/ui/input-group.tsx +175 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +212 -0
- package/src/components/ui/kbd.tsx +62 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/list.tsx +46 -0
- package/src/components/ui/menu-variants.ts +71 -0
- package/src/components/ui/menubar.tsx +226 -0
- package/src/components/ui/money-input.tsx +96 -0
- package/src/components/ui/native-date-picker.tsx +120 -0
- package/src/components/ui/native-select.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +183 -0
- package/src/components/ui/pagination.tsx +150 -0
- package/src/components/ui/popover.tsx +104 -0
- package/src/components/ui/profile-header.tsx +196 -0
- package/src/components/ui/progress.tsx +71 -0
- package/src/components/ui/radio-group.tsx +115 -0
- package/src/components/ui/rating.tsx +200 -0
- package/src/components/ui/resizable.tsx +61 -0
- package/src/components/ui/responsive-dialog.tsx +278 -0
- package/src/components/ui/responsive-select.tsx +289 -0
- package/src/components/ui/responsive-sheet.tsx +225 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/section.tsx +106 -0
- package/src/components/ui/segmented-control.tsx +148 -0
- package/src/components/ui/select.tsx +216 -0
- package/src/components/ui/separator.tsx +69 -0
- package/src/components/ui/sheet.tsx +154 -0
- package/src/components/ui/shortcut.tsx +41 -0
- package/src/components/ui/sidebar.tsx +739 -0
- package/src/components/ui/skeleton.tsx +23 -0
- package/src/components/ui/slider.tsx +74 -0
- package/src/components/ui/sonner.tsx +64 -0
- package/src/components/ui/spinner.tsx +19 -0
- package/src/components/ui/stat-card.tsx +138 -0
- package/src/components/ui/stepper.tsx +181 -0
- package/src/components/ui/sticky-action-bar.tsx +42 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +132 -0
- package/src/components/ui/tabs.tsx +77 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/time-picker.tsx +221 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +79 -0
- package/src/components/ui/translation-input.tsx +126 -0
- package/src/components/ui/user-avatar.tsx +74 -0
- package/src/css-modules.d.ts +4 -0
- package/src/hooks/use-command-palette.ts +28 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-pagination.ts +81 -0
- package/src/hooks/use-picker-state.ts +44 -0
- package/src/lib/cva.ts +18 -0
- package/src/lib/time-math.ts +62 -0
- package/src/lib/utils.ts +14 -0
- package/src/styles/globals.css +36 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { CheckIcon, MagnifyingGlassIcon } from "@phosphor-icons/react";
|
|
2
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "#/components/ui/dialog.tsx";
|
|
11
|
+
import { InputGroup, InputGroupAddon } from "#/components/ui/input-group.tsx";
|
|
12
|
+
import {
|
|
13
|
+
menuItemVariants,
|
|
14
|
+
menuSeparatorVariants,
|
|
15
|
+
} from "#/components/ui/menu-variants.ts";
|
|
16
|
+
import { Shortcut } from "#/components/ui/shortcut.tsx";
|
|
17
|
+
import { cn } from "#/lib/utils.ts";
|
|
18
|
+
|
|
19
|
+
function Command({
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
23
|
+
return (
|
|
24
|
+
<CommandPrimitive
|
|
25
|
+
data-slot="command"
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CommandDialog({
|
|
36
|
+
title = "Command Palette",
|
|
37
|
+
description = "Search for a command to run...",
|
|
38
|
+
children,
|
|
39
|
+
className,
|
|
40
|
+
showCloseButton = false,
|
|
41
|
+
...props
|
|
42
|
+
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
|
|
43
|
+
title?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
showCloseButton?: boolean;
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<Dialog {...props}>
|
|
51
|
+
<DialogContent
|
|
52
|
+
className={cn(
|
|
53
|
+
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
showCloseButton={showCloseButton}
|
|
57
|
+
>
|
|
58
|
+
<DialogHeader className="sr-only">
|
|
59
|
+
<DialogTitle>{title}</DialogTitle>
|
|
60
|
+
<DialogDescription>{description}</DialogDescription>
|
|
61
|
+
</DialogHeader>
|
|
62
|
+
{children}
|
|
63
|
+
</DialogContent>
|
|
64
|
+
</Dialog>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function CommandInput({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
72
|
+
return (
|
|
73
|
+
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
74
|
+
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
|
75
|
+
<CommandPrimitive.Input
|
|
76
|
+
data-slot="command-input"
|
|
77
|
+
className={cn(
|
|
78
|
+
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
<InputGroupAddon>
|
|
84
|
+
<MagnifyingGlassIcon className="size-4 shrink-0 opacity-50" />
|
|
85
|
+
</InputGroupAddon>
|
|
86
|
+
</InputGroup>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function CommandList({
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
95
|
+
return (
|
|
96
|
+
<CommandPrimitive.List
|
|
97
|
+
data-slot="command-list"
|
|
98
|
+
className={cn(
|
|
99
|
+
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function CommandEmpty({
|
|
108
|
+
className,
|
|
109
|
+
...props
|
|
110
|
+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
111
|
+
return (
|
|
112
|
+
<CommandPrimitive.Empty
|
|
113
|
+
data-slot="command-empty"
|
|
114
|
+
className={cn(
|
|
115
|
+
"py-6 text-center text-sm text-muted-foreground",
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function CommandGroup({
|
|
124
|
+
className,
|
|
125
|
+
...props
|
|
126
|
+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
127
|
+
return (
|
|
128
|
+
<CommandPrimitive.Group
|
|
129
|
+
data-slot="command-group"
|
|
130
|
+
className={cn(
|
|
131
|
+
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-1.5 **:[[cmdk-group-heading]]:py-1 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CommandSeparator({
|
|
140
|
+
className,
|
|
141
|
+
...props
|
|
142
|
+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
143
|
+
return (
|
|
144
|
+
<CommandPrimitive.Separator
|
|
145
|
+
data-slot="command-separator"
|
|
146
|
+
className={cn(menuSeparatorVariants(), className)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function CommandItem({
|
|
153
|
+
className,
|
|
154
|
+
children,
|
|
155
|
+
...props
|
|
156
|
+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
157
|
+
return (
|
|
158
|
+
<CommandPrimitive.Item
|
|
159
|
+
data-slot="command-item"
|
|
160
|
+
className={cn(
|
|
161
|
+
menuItemVariants(),
|
|
162
|
+
"in-data-[slot=dialog-content]:rounded-lg!",
|
|
163
|
+
className,
|
|
164
|
+
)}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/menu-item:hidden group-data-[checked=true]/menu-item:opacity-100" />
|
|
169
|
+
</CommandPrimitive.Item>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function CommandShortcut({ ...props }: React.ComponentProps<typeof Shortcut>) {
|
|
174
|
+
return <Shortcut data-slot="command-shortcut" {...props} />;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
Command,
|
|
179
|
+
CommandDialog,
|
|
180
|
+
CommandEmpty,
|
|
181
|
+
CommandGroup,
|
|
182
|
+
CommandInput,
|
|
183
|
+
CommandItem,
|
|
184
|
+
CommandList,
|
|
185
|
+
CommandSeparator,
|
|
186
|
+
CommandShortcut,
|
|
187
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
AlertDialog,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
type AlertDialogContentSize,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogMedia,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
AlertDialogTrigger,
|
|
13
|
+
} from "#/components/ui/alert-dialog.tsx";
|
|
14
|
+
import { Button, type ButtonVariant } from "#/components/ui/button.tsx";
|
|
15
|
+
|
|
16
|
+
type ConfirmDialogProps = {
|
|
17
|
+
/**
|
|
18
|
+
* Element rendered as the dialog trigger, carrying its own label, e.g.
|
|
19
|
+
* `<Button variant="destructive">Delete</Button>`. Omit it to drive the
|
|
20
|
+
* dialog through `open`/`onOpenChange` instead.
|
|
21
|
+
*/
|
|
22
|
+
trigger?: React.ReactElement<Record<string, unknown>>;
|
|
23
|
+
open?: boolean;
|
|
24
|
+
onOpenChange?: (open: boolean) => void;
|
|
25
|
+
title: React.ReactNode;
|
|
26
|
+
description?: React.ReactNode;
|
|
27
|
+
/** Icon slot rendered in `AlertDialogMedia` beside the title. */
|
|
28
|
+
media?: React.ReactNode;
|
|
29
|
+
size?: AlertDialogContentSize;
|
|
30
|
+
// English defaults to match the kit's other built-in labels; pass localized
|
|
31
|
+
// strings from the app (e.g. paraglide messages).
|
|
32
|
+
confirmLabel?: React.ReactNode;
|
|
33
|
+
cancelLabel?: React.ReactNode;
|
|
34
|
+
/** Confirm button variant - use `destructive` for irreversible actions. */
|
|
35
|
+
variant?: ButtonVariant;
|
|
36
|
+
/**
|
|
37
|
+
* Confirm handler. When it returns a promise the dialog enters a pending
|
|
38
|
+
* state (spinner on the confirm button, cancel disabled, dismissal blocked)
|
|
39
|
+
* and closes once it resolves. On rejection the dialog stays open so the
|
|
40
|
+
* user can retry - surface the failure inside `onConfirm` (e.g. a toast),
|
|
41
|
+
* the dialog does not report it.
|
|
42
|
+
*/
|
|
43
|
+
onConfirm?: () => undefined | Promise<unknown>;
|
|
44
|
+
/** Called when the cancel button is clicked (not on Escape/backdrop). */
|
|
45
|
+
onCancel?: () => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function ConfirmDialog({
|
|
49
|
+
trigger,
|
|
50
|
+
open: controlledOpen,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
media,
|
|
55
|
+
size,
|
|
56
|
+
confirmLabel = "Confirm",
|
|
57
|
+
cancelLabel = "Cancel",
|
|
58
|
+
variant = "default",
|
|
59
|
+
onConfirm,
|
|
60
|
+
onCancel,
|
|
61
|
+
}: ConfirmDialogProps) {
|
|
62
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
|
|
63
|
+
const [pending, setPending] = React.useState(false);
|
|
64
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
65
|
+
|
|
66
|
+
const setOpen = (nextOpen: boolean) => {
|
|
67
|
+
if (controlledOpen === undefined) {
|
|
68
|
+
setUncontrolledOpen(nextOpen);
|
|
69
|
+
}
|
|
70
|
+
onOpenChange?.(nextOpen);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
74
|
+
if (pending) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setOpen(nextOpen);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleConfirm = async () => {
|
|
81
|
+
if (onConfirm === undefined) {
|
|
82
|
+
setOpen(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setPending(true);
|
|
86
|
+
try {
|
|
87
|
+
await onConfirm();
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
} finally {
|
|
91
|
+
setPending(false);
|
|
92
|
+
}
|
|
93
|
+
setOpen(false);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
98
|
+
{trigger ? <AlertDialogTrigger render={trigger} /> : null}
|
|
99
|
+
<AlertDialogContent size={size}>
|
|
100
|
+
<AlertDialogHeader>
|
|
101
|
+
{media ? <AlertDialogMedia>{media}</AlertDialogMedia> : null}
|
|
102
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
103
|
+
{description ? (
|
|
104
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
105
|
+
) : null}
|
|
106
|
+
</AlertDialogHeader>
|
|
107
|
+
<AlertDialogFooter>
|
|
108
|
+
<AlertDialogCancel disabled={pending} onClick={onCancel}>
|
|
109
|
+
{cancelLabel}
|
|
110
|
+
</AlertDialogCancel>
|
|
111
|
+
<Button
|
|
112
|
+
data-slot="confirm-dialog-confirm"
|
|
113
|
+
variant={variant}
|
|
114
|
+
loading={pending}
|
|
115
|
+
onClick={handleConfirm}
|
|
116
|
+
>
|
|
117
|
+
{confirmLabel}
|
|
118
|
+
</Button>
|
|
119
|
+
</AlertDialogFooter>
|
|
120
|
+
</AlertDialogContent>
|
|
121
|
+
</AlertDialog>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { ConfirmDialog, type ConfirmDialogProps };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
|
|
2
|
+
import { CaretRightIcon, CheckIcon, CircleIcon } from "@phosphor-icons/react";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
menuContentVariants,
|
|
6
|
+
menuIndicatorVariants,
|
|
7
|
+
menuItemVariants,
|
|
8
|
+
menuLabelVariants,
|
|
9
|
+
menuSeparatorVariants,
|
|
10
|
+
menuSubTriggerVariants,
|
|
11
|
+
} from "#/components/ui/menu-variants.ts";
|
|
12
|
+
import { Shortcut } from "#/components/ui/shortcut.tsx";
|
|
13
|
+
import { cn } from "#/lib/utils.ts";
|
|
14
|
+
|
|
15
|
+
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
|
16
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
|
|
20
|
+
return (
|
|
21
|
+
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ContextMenuTrigger({
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: ContextMenuPrimitive.Trigger.Props) {
|
|
29
|
+
return (
|
|
30
|
+
<ContextMenuPrimitive.Trigger
|
|
31
|
+
data-slot="context-menu-trigger"
|
|
32
|
+
className={cn("select-none", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ContextMenuContent({
|
|
39
|
+
className,
|
|
40
|
+
align = "start",
|
|
41
|
+
alignOffset = 4,
|
|
42
|
+
side = "right",
|
|
43
|
+
sideOffset = 0,
|
|
44
|
+
...props
|
|
45
|
+
}: ContextMenuPrimitive.Popup.Props &
|
|
46
|
+
Pick<
|
|
47
|
+
ContextMenuPrimitive.Positioner.Props,
|
|
48
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
49
|
+
>) {
|
|
50
|
+
return (
|
|
51
|
+
<ContextMenuPrimitive.Portal>
|
|
52
|
+
<ContextMenuPrimitive.Positioner
|
|
53
|
+
className="isolate z-50 outline-none"
|
|
54
|
+
align={align}
|
|
55
|
+
alignOffset={alignOffset}
|
|
56
|
+
side={side}
|
|
57
|
+
sideOffset={sideOffset}
|
|
58
|
+
>
|
|
59
|
+
<ContextMenuPrimitive.Popup
|
|
60
|
+
data-slot="context-menu-content"
|
|
61
|
+
className={cn(menuContentVariants(), "min-w-36", className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
</ContextMenuPrimitive.Positioner>
|
|
65
|
+
</ContextMenuPrimitive.Portal>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
|
|
70
|
+
return (
|
|
71
|
+
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Plain div, not GroupLabel: Base UI's GroupLabel throws when rendered
|
|
76
|
+
// outside a Group, and labels are routinely used standalone.
|
|
77
|
+
function ContextMenuLabel({
|
|
78
|
+
className,
|
|
79
|
+
inset,
|
|
80
|
+
...props
|
|
81
|
+
}: React.ComponentProps<"div"> & {
|
|
82
|
+
inset?: boolean;
|
|
83
|
+
}) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
data-slot="context-menu-label"
|
|
87
|
+
data-inset={inset}
|
|
88
|
+
className={cn(menuLabelVariants(), className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ContextMenuItem({
|
|
95
|
+
className,
|
|
96
|
+
inset,
|
|
97
|
+
variant = "default",
|
|
98
|
+
...props
|
|
99
|
+
}: ContextMenuPrimitive.Item.Props & {
|
|
100
|
+
inset?: boolean;
|
|
101
|
+
variant?: "default" | "destructive";
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<ContextMenuPrimitive.Item
|
|
105
|
+
data-slot="context-menu-item"
|
|
106
|
+
data-inset={inset}
|
|
107
|
+
data-variant={variant}
|
|
108
|
+
className={cn(menuItemVariants({ variant }), className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
|
|
115
|
+
return (
|
|
116
|
+
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ContextMenuSubTrigger({
|
|
121
|
+
className,
|
|
122
|
+
inset,
|
|
123
|
+
children,
|
|
124
|
+
...props
|
|
125
|
+
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
|
|
126
|
+
inset?: boolean;
|
|
127
|
+
}) {
|
|
128
|
+
return (
|
|
129
|
+
<ContextMenuPrimitive.SubmenuTrigger
|
|
130
|
+
data-slot="context-menu-sub-trigger"
|
|
131
|
+
data-inset={inset}
|
|
132
|
+
className={cn(menuSubTriggerVariants(), className)}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
<CaretRightIcon className="ml-auto" />
|
|
137
|
+
</ContextMenuPrimitive.SubmenuTrigger>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ContextMenuSubContent({
|
|
142
|
+
className,
|
|
143
|
+
side = "right",
|
|
144
|
+
...props
|
|
145
|
+
}: React.ComponentProps<typeof ContextMenuContent>) {
|
|
146
|
+
return (
|
|
147
|
+
<ContextMenuContent
|
|
148
|
+
data-slot="context-menu-sub-content"
|
|
149
|
+
className={cn("shadow-lg", className)}
|
|
150
|
+
side={side}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ContextMenuCheckboxItem({
|
|
157
|
+
className,
|
|
158
|
+
children,
|
|
159
|
+
checked,
|
|
160
|
+
inset,
|
|
161
|
+
...props
|
|
162
|
+
}: ContextMenuPrimitive.CheckboxItem.Props & {
|
|
163
|
+
inset?: boolean;
|
|
164
|
+
}) {
|
|
165
|
+
return (
|
|
166
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
167
|
+
data-slot="context-menu-checkbox-item"
|
|
168
|
+
data-inset={inset}
|
|
169
|
+
className={cn(menuItemVariants({ indicator: "end" }), className)}
|
|
170
|
+
checked={checked}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<span className={menuIndicatorVariants()}>
|
|
174
|
+
<ContextMenuPrimitive.CheckboxItemIndicator>
|
|
175
|
+
<CheckIcon />
|
|
176
|
+
</ContextMenuPrimitive.CheckboxItemIndicator>
|
|
177
|
+
</span>
|
|
178
|
+
{children}
|
|
179
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ContextMenuRadioGroup({
|
|
184
|
+
...props
|
|
185
|
+
}: ContextMenuPrimitive.RadioGroup.Props) {
|
|
186
|
+
return (
|
|
187
|
+
<ContextMenuPrimitive.RadioGroup
|
|
188
|
+
data-slot="context-menu-radio-group"
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ContextMenuRadioItem({
|
|
195
|
+
className,
|
|
196
|
+
children,
|
|
197
|
+
inset,
|
|
198
|
+
...props
|
|
199
|
+
}: ContextMenuPrimitive.RadioItem.Props & {
|
|
200
|
+
inset?: boolean;
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<ContextMenuPrimitive.RadioItem
|
|
204
|
+
data-slot="context-menu-radio-item"
|
|
205
|
+
data-inset={inset}
|
|
206
|
+
className={cn(menuItemVariants({ indicator: "end" }), className)}
|
|
207
|
+
{...props}
|
|
208
|
+
>
|
|
209
|
+
<span className={menuIndicatorVariants()}>
|
|
210
|
+
<ContextMenuPrimitive.RadioItemIndicator>
|
|
211
|
+
<CircleIcon weight="fill" className="size-2" />
|
|
212
|
+
</ContextMenuPrimitive.RadioItemIndicator>
|
|
213
|
+
</span>
|
|
214
|
+
{children}
|
|
215
|
+
</ContextMenuPrimitive.RadioItem>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ContextMenuSeparator({
|
|
220
|
+
className,
|
|
221
|
+
...props
|
|
222
|
+
}: ContextMenuPrimitive.Separator.Props) {
|
|
223
|
+
return (
|
|
224
|
+
<ContextMenuPrimitive.Separator
|
|
225
|
+
data-slot="context-menu-separator"
|
|
226
|
+
className={cn(menuSeparatorVariants(), className)}
|
|
227
|
+
{...props}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ContextMenuShortcut({
|
|
233
|
+
...props
|
|
234
|
+
}: React.ComponentProps<typeof Shortcut>) {
|
|
235
|
+
return <Shortcut data-slot="context-menu-shortcut" {...props} />;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
ContextMenu,
|
|
240
|
+
ContextMenuCheckboxItem,
|
|
241
|
+
ContextMenuContent,
|
|
242
|
+
ContextMenuGroup,
|
|
243
|
+
ContextMenuItem,
|
|
244
|
+
ContextMenuLabel,
|
|
245
|
+
ContextMenuPortal,
|
|
246
|
+
ContextMenuRadioGroup,
|
|
247
|
+
ContextMenuRadioItem,
|
|
248
|
+
ContextMenuSeparator,
|
|
249
|
+
ContextMenuShortcut,
|
|
250
|
+
ContextMenuSub,
|
|
251
|
+
ContextMenuSubContent,
|
|
252
|
+
ContextMenuSubTrigger,
|
|
253
|
+
ContextMenuTrigger,
|
|
254
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "#/lib/utils.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A monospace value that copies itself when clicked — for the identifiers,
|
|
7
|
+
* addresses and phone numbers that are meant to be carried to another screen.
|
|
8
|
+
* The copied value can differ from the rendered label (a truncated id copies
|
|
9
|
+
* in full), and `stopPropagation` keeps a copy click from also triggering an
|
|
10
|
+
* enclosing clickable row.
|
|
11
|
+
*/
|
|
12
|
+
function CopyableText({
|
|
13
|
+
value,
|
|
14
|
+
label = value,
|
|
15
|
+
className,
|
|
16
|
+
muted = false,
|
|
17
|
+
copyLabel = "Copy",
|
|
18
|
+
copiedLabel = "Copied",
|
|
19
|
+
}: {
|
|
20
|
+
/** The text written to the clipboard. */
|
|
21
|
+
value: string;
|
|
22
|
+
/** What's rendered, when it differs from the copied value (e.g. a short id). */
|
|
23
|
+
label?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Render the label in muted secondary text. */
|
|
26
|
+
muted?: boolean;
|
|
27
|
+
copyLabel?: string;
|
|
28
|
+
copiedLabel?: string;
|
|
29
|
+
}) {
|
|
30
|
+
const [copied, setCopied] = React.useState(false);
|
|
31
|
+
|
|
32
|
+
const copy = () => {
|
|
33
|
+
void navigator.clipboard?.writeText(value).then(() => {
|
|
34
|
+
setCopied(true);
|
|
35
|
+
setTimeout(() => setCopied(false), 1500);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
data-slot="copyable-text"
|
|
43
|
+
aria-label={copied ? copiedLabel : copyLabel}
|
|
44
|
+
title={copied ? copiedLabel : copyLabel}
|
|
45
|
+
className={cn(
|
|
46
|
+
"group inline-flex max-w-full cursor-pointer items-center gap-1.5 rounded-sm text-left font-mono focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2",
|
|
47
|
+
muted && "text-muted-foreground",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
onClick={(event) => {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
copy();
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<span className="truncate">{label}</span>
|
|
56
|
+
{copied ? (
|
|
57
|
+
<CheckIcon className="shrink-0 text-success" weight="bold" />
|
|
58
|
+
) : (
|
|
59
|
+
<CopyIcon className="shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
|
|
60
|
+
)}
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { CopyableText };
|