@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.
Files changed (99) hide show
  1. package/package.json +78 -0
  2. package/src/components/ui/accordion.tsx +79 -0
  3. package/src/components/ui/alert-dialog.tsx +198 -0
  4. package/src/components/ui/alert.tsx +105 -0
  5. package/src/components/ui/aspect-ratio.tsx +30 -0
  6. package/src/components/ui/avatar.tsx +125 -0
  7. package/src/components/ui/badge-variants.ts +166 -0
  8. package/src/components/ui/badge.tsx +45 -0
  9. package/src/components/ui/banner.tsx +84 -0
  10. package/src/components/ui/breadcrumb.tsx +127 -0
  11. package/src/components/ui/button-group.tsx +99 -0
  12. package/src/components/ui/button-variants.ts +108 -0
  13. package/src/components/ui/button.tsx +54 -0
  14. package/src/components/ui/calendar.tsx +266 -0
  15. package/src/components/ui/card.tsx +155 -0
  16. package/src/components/ui/carousel.tsx +319 -0
  17. package/src/components/ui/chat.tsx +896 -0
  18. package/src/components/ui/checkbox-group.tsx +44 -0
  19. package/src/components/ui/checkbox.tsx +34 -0
  20. package/src/components/ui/chip.tsx +60 -0
  21. package/src/components/ui/collapsible.tsx +44 -0
  22. package/src/components/ui/color-picker.tsx +191 -0
  23. package/src/components/ui/combobox.tsx +317 -0
  24. package/src/components/ui/command.tsx +187 -0
  25. package/src/components/ui/confirm-dialog.tsx +125 -0
  26. package/src/components/ui/context-menu.tsx +254 -0
  27. package/src/components/ui/copyable-text.tsx +65 -0
  28. package/src/components/ui/date-picker.tsx +316 -0
  29. package/src/components/ui/date-time-picker.tsx +1071 -0
  30. package/src/components/ui/dialog.tsx +176 -0
  31. package/src/components/ui/direction.tsx +15 -0
  32. package/src/components/ui/drawer.tsx +178 -0
  33. package/src/components/ui/dropdown-menu.tsx +247 -0
  34. package/src/components/ui/empty.tsx +131 -0
  35. package/src/components/ui/field.tsx +257 -0
  36. package/src/components/ui/formatted-input.tsx +219 -0
  37. package/src/components/ui/gallery.tsx +161 -0
  38. package/src/components/ui/hover-card.tsx +77 -0
  39. package/src/components/ui/icon-picker.tsx +193 -0
  40. package/src/components/ui/image-cropper.tsx +787 -0
  41. package/src/components/ui/image-upload-field.tsx +344 -0
  42. package/src/components/ui/input-group.tsx +175 -0
  43. package/src/components/ui/input-otp.tsx +83 -0
  44. package/src/components/ui/input.tsx +21 -0
  45. package/src/components/ui/item.tsx +212 -0
  46. package/src/components/ui/kbd.tsx +62 -0
  47. package/src/components/ui/label.tsx +19 -0
  48. package/src/components/ui/list.tsx +46 -0
  49. package/src/components/ui/menu-variants.ts +71 -0
  50. package/src/components/ui/menubar.tsx +226 -0
  51. package/src/components/ui/money-input.tsx +96 -0
  52. package/src/components/ui/native-date-picker.tsx +120 -0
  53. package/src/components/ui/native-select.tsx +74 -0
  54. package/src/components/ui/navigation-menu.tsx +183 -0
  55. package/src/components/ui/pagination.tsx +150 -0
  56. package/src/components/ui/popover.tsx +104 -0
  57. package/src/components/ui/profile-header.tsx +196 -0
  58. package/src/components/ui/progress.tsx +71 -0
  59. package/src/components/ui/radio-group.tsx +115 -0
  60. package/src/components/ui/rating.tsx +200 -0
  61. package/src/components/ui/resizable.tsx +61 -0
  62. package/src/components/ui/responsive-dialog.tsx +278 -0
  63. package/src/components/ui/responsive-select.tsx +289 -0
  64. package/src/components/ui/responsive-sheet.tsx +225 -0
  65. package/src/components/ui/scroll-area.tsx +51 -0
  66. package/src/components/ui/section.tsx +106 -0
  67. package/src/components/ui/segmented-control.tsx +148 -0
  68. package/src/components/ui/select.tsx +216 -0
  69. package/src/components/ui/separator.tsx +69 -0
  70. package/src/components/ui/sheet.tsx +154 -0
  71. package/src/components/ui/shortcut.tsx +41 -0
  72. package/src/components/ui/sidebar.tsx +739 -0
  73. package/src/components/ui/skeleton.tsx +23 -0
  74. package/src/components/ui/slider.tsx +74 -0
  75. package/src/components/ui/sonner.tsx +64 -0
  76. package/src/components/ui/spinner.tsx +19 -0
  77. package/src/components/ui/stat-card.tsx +138 -0
  78. package/src/components/ui/stepper.tsx +181 -0
  79. package/src/components/ui/sticky-action-bar.tsx +42 -0
  80. package/src/components/ui/switch.tsx +30 -0
  81. package/src/components/ui/table.tsx +132 -0
  82. package/src/components/ui/tabs.tsx +77 -0
  83. package/src/components/ui/textarea.tsx +24 -0
  84. package/src/components/ui/time-picker.tsx +221 -0
  85. package/src/components/ui/toggle-group.tsx +86 -0
  86. package/src/components/ui/toggle.tsx +43 -0
  87. package/src/components/ui/tooltip.tsx +79 -0
  88. package/src/components/ui/translation-input.tsx +126 -0
  89. package/src/components/ui/user-avatar.tsx +74 -0
  90. package/src/css-modules.d.ts +4 -0
  91. package/src/hooks/use-command-palette.ts +28 -0
  92. package/src/hooks/use-mobile.ts +21 -0
  93. package/src/hooks/use-pagination.ts +81 -0
  94. package/src/hooks/use-picker-state.ts +44 -0
  95. package/src/lib/cva.ts +18 -0
  96. package/src/lib/time-math.ts +62 -0
  97. package/src/lib/utils.ts +14 -0
  98. package/src/styles/globals.css +36 -0
  99. 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 };