@modern-admin/ui 0.1.0
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/components/accordion.d.ts +7 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.jsx +19 -0
- package/dist/components/accordion.jsx.map +1 -0
- package/dist/components/alert-dialog.d.ts +22 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.jsx +27 -0
- package/dist/components/alert-dialog.jsx.map +1 -0
- package/dist/components/audit-timeline.d.ts +24 -0
- package/dist/components/audit-timeline.d.ts.map +1 -0
- package/dist/components/audit-timeline.jsx +60 -0
- package/dist/components/audit-timeline.jsx.map +1 -0
- package/dist/components/avatar.d.ts +6 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.jsx +10 -0
- package/dist/components/avatar.jsx.map +1 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.jsx +19 -0
- package/dist/components/badge.jsx.map +1 -0
- package/dist/components/breadcrumb.d.ts +17 -0
- package/dist/components/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb.jsx +27 -0
- package/dist/components/breadcrumb.jsx.map +1 -0
- package/dist/components/button.d.ts +12 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.jsx +37 -0
- package/dist/components/button.jsx.map +1 -0
- package/dist/components/calendar.d.ts +9 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.jsx +102 -0
- package/dist/components/calendar.jsx.map +1 -0
- package/dist/components/card.d.ts +8 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.jsx +18 -0
- package/dist/components/card.jsx.map +1 -0
- package/dist/components/chart.d.ts +97 -0
- package/dist/components/chart.d.ts.map +1 -0
- package/dist/components/chart.jsx +233 -0
- package/dist/components/chart.jsx.map +1 -0
- package/dist/components/checkbox.d.ts +4 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.jsx +11 -0
- package/dist/components/checkbox.jsx.map +1 -0
- package/dist/components/combobox.d.ts +46 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.jsx +145 -0
- package/dist/components/combobox.jsx.map +1 -0
- package/dist/components/command.d.ts +80 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/command.jsx +32 -0
- package/dist/components/command.jsx.map +1 -0
- package/dist/components/date-picker.d.ts +24 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.jsx +149 -0
- package/dist/components/date-picker.jsx.map +1 -0
- package/dist/components/date-range-input.d.ts +22 -0
- package/dist/components/date-range-input.d.ts.map +1 -0
- package/dist/components/date-range-input.jsx +202 -0
- package/dist/components/date-range-input.jsx.map +1 -0
- package/dist/components/dialog.d.ts +19 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.jsx +30 -0
- package/dist/components/dialog.jsx.map +1 -0
- package/dist/components/diff-view.d.ts +24 -0
- package/dist/components/diff-view.d.ts.map +1 -0
- package/dist/components/diff-view.jsx +69 -0
- package/dist/components/diff-view.jsx.map +1 -0
- package/dist/components/dropdown-menu.d.ts +27 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.jsx +48 -0
- package/dist/components/dropdown-menu.jsx.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.d.ts.map +1 -0
- package/dist/components/empty.jsx +27 -0
- package/dist/components/empty.jsx.map +1 -0
- package/dist/components/field.d.ts +23 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.jsx +60 -0
- package/dist/components/field.jsx.map +1 -0
- package/dist/components/file-input.d.ts +50 -0
- package/dist/components/file-input.d.ts.map +1 -0
- package/dist/components/file-input.jsx +104 -0
- package/dist/components/file-input.jsx.map +1 -0
- package/dist/components/form.d.ts +20 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.jsx +66 -0
- package/dist/components/form.jsx.map +1 -0
- package/dist/components/info-tooltip.d.ts +11 -0
- package/dist/components/info-tooltip.d.ts.map +1 -0
- package/dist/components/info-tooltip.jsx +17 -0
- package/dist/components/info-tooltip.jsx.map +1 -0
- package/dist/components/input.d.ts +13 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.jsx +19 -0
- package/dist/components/input.jsx.map +1 -0
- package/dist/components/json-editor.d.ts +23 -0
- package/dist/components/json-editor.d.ts.map +1 -0
- package/dist/components/json-editor.jsx +143 -0
- package/dist/components/json-editor.jsx.map +1 -0
- package/dist/components/kbd.d.ts +15 -0
- package/dist/components/kbd.d.ts.map +1 -0
- package/dist/components/kbd.jsx +23 -0
- package/dist/components/kbd.jsx.map +1 -0
- package/dist/components/key-value-editor.d.ts +92 -0
- package/dist/components/key-value-editor.d.ts.map +1 -0
- package/dist/components/key-value-editor.jsx +187 -0
- package/dist/components/key-value-editor.jsx.map +1 -0
- package/dist/components/keyboard-shortcuts-help.d.ts +17 -0
- package/dist/components/keyboard-shortcuts-help.d.ts.map +1 -0
- package/dist/components/keyboard-shortcuts-help.jsx +97 -0
- package/dist/components/keyboard-shortcuts-help.jsx.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.jsx +8 -0
- package/dist/components/label.jsx.map +1 -0
- package/dist/components/media-preview.d.ts +30 -0
- package/dist/components/media-preview.d.ts.map +1 -0
- package/dist/components/media-preview.jsx +189 -0
- package/dist/components/media-preview.jsx.map +1 -0
- package/dist/components/multi-file-input.d.ts +76 -0
- package/dist/components/multi-file-input.d.ts.map +1 -0
- package/dist/components/multi-file-input.jsx +131 -0
- package/dist/components/multi-file-input.jsx.map +1 -0
- package/dist/components/password-input.d.ts +10 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.jsx +18 -0
- package/dist/components/password-input.jsx.map +1 -0
- package/dist/components/popover.d.ts +7 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.jsx +11 -0
- package/dist/components/popover.jsx.map +1 -0
- package/dist/components/revision-timeline.d.ts +30 -0
- package/dist/components/revision-timeline.d.ts.map +1 -0
- package/dist/components/revision-timeline.jsx +42 -0
- package/dist/components/revision-timeline.jsx.map +1 -0
- package/dist/components/richtext-editor.d.ts +43 -0
- package/dist/components/richtext-editor.d.ts.map +1 -0
- package/dist/components/richtext-editor.jsx +319 -0
- package/dist/components/richtext-editor.jsx.map +1 -0
- package/dist/components/richtext-mode.d.ts +23 -0
- package/dist/components/richtext-mode.d.ts.map +1 -0
- package/dist/components/richtext-mode.js +36 -0
- package/dist/components/richtext-mode.js.map +1 -0
- package/dist/components/richtext-render.d.ts +8 -0
- package/dist/components/richtext-render.d.ts.map +1 -0
- package/dist/components/richtext-render.jsx +33 -0
- package/dist/components/richtext-render.jsx.map +1 -0
- package/dist/components/richtext-sync.d.ts +37 -0
- package/dist/components/richtext-sync.d.ts.map +1 -0
- package/dist/components/richtext-sync.js +46 -0
- package/dist/components/richtext-sync.js.map +1 -0
- package/dist/components/scroll-area.d.ts +5 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.jsx +16 -0
- package/dist/components/scroll-area.jsx.map +1 -0
- package/dist/components/select.d.ts +36 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.jsx +87 -0
- package/dist/components/select.jsx.map +1 -0
- package/dist/components/separator.d.ts +4 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.jsx +6 -0
- package/dist/components/separator.jsx.map +1 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.jsx +44 -0
- package/dist/components/sheet.jsx.map +1 -0
- package/dist/components/sidebar.d.ts +70 -0
- package/dist/components/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar.jsx +245 -0
- package/dist/components/sidebar.jsx.map +1 -0
- package/dist/components/skeleton.d.ts +3 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.jsx +6 -0
- package/dist/components/skeleton.jsx.map +1 -0
- package/dist/components/sonner.d.ts +6 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.jsx +29 -0
- package/dist/components/sonner.jsx.map +1 -0
- package/dist/components/switch.d.ts +4 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.jsx +8 -0
- package/dist/components/switch.jsx.map +1 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.jsx +21 -0
- package/dist/components/table.jsx.map +1 -0
- package/dist/components/tabs.d.ts +7 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.jsx +14 -0
- package/dist/components/tabs.jsx.map +1 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.jsx +5 -0
- package/dist/components/textarea.jsx.map +1 -0
- package/dist/components/tooltip.d.ts +7 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.jsx +11 -0
- package/dist/components/tooltip.jsx.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/theme.d.ts +11 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +44 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +242 -0
- package/package.json +85 -0
- package/src/components/accordion.tsx +48 -0
- package/src/components/alert-dialog.tsx +113 -0
- package/src/components/audit-timeline.tsx +102 -0
- package/src/components/avatar.tsx +42 -0
- package/src/components/badge.tsx +34 -0
- package/src/components/breadcrumb.tsx +99 -0
- package/src/components/button.tsx +58 -0
- package/src/components/calendar.tsx +176 -0
- package/src/components/card.tsx +60 -0
- package/src/components/chart.tsx +558 -0
- package/src/components/checkbox.tsx +23 -0
- package/src/components/combobox.tsx +264 -0
- package/src/components/command.tsx +120 -0
- package/src/components/date-picker.tsx +221 -0
- package/src/components/date-range-input.tsx +295 -0
- package/src/components/dialog.tsx +94 -0
- package/src/components/diff-view.tsx +182 -0
- package/src/components/dropdown-menu.tsx +165 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +168 -0
- package/src/components/file-input.tsx +233 -0
- package/src/components/form.tsx +152 -0
- package/src/components/info-tooltip.tsx +40 -0
- package/src/components/input.tsx +55 -0
- package/src/components/json-editor.tsx +210 -0
- package/src/components/kbd.tsx +35 -0
- package/src/components/key-value-editor.tsx +423 -0
- package/src/components/keyboard-shortcuts-help.tsx +136 -0
- package/src/components/label.tsx +16 -0
- package/src/components/media-preview.tsx +278 -0
- package/src/components/multi-file-input.tsx +315 -0
- package/src/components/password-input.tsx +50 -0
- package/src/components/popover.tsx +26 -0
- package/src/components/revision-timeline.tsx +93 -0
- package/src/components/richtext-editor.tsx +624 -0
- package/src/components/richtext-mode.ts +39 -0
- package/src/components/richtext-render.tsx +51 -0
- package/src/components/richtext-sync.ts +57 -0
- package/src/components/scroll-area.tsx +41 -0
- package/src/components/select.tsx +200 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +109 -0
- package/src/components/sidebar.tsx +660 -0
- package/src/components/skeleton.tsx +9 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/switch.tsx +24 -0
- package/src/components/table.tsx +93 -0
- package/src/components/tabs.tsx +57 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/tooltip.tsx +25 -0
- package/src/index.ts +342 -0
- package/src/lib/theme.ts +45 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +242 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
|
3
|
+
import { Check, ChevronRight, Circle } from 'lucide-react'
|
|
4
|
+
import { cn } from '../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
export const DropdownMenu = DropdownMenuPrimitive.Root
|
|
7
|
+
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
8
|
+
export const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
9
|
+
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
10
|
+
export const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
11
|
+
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
12
|
+
|
|
13
|
+
export const DropdownMenuSubTrigger = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
16
|
+
inset?: boolean
|
|
17
|
+
}
|
|
18
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
19
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
'flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
|
23
|
+
inset && 'pl-8',
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
30
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
31
|
+
))
|
|
32
|
+
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
|
33
|
+
|
|
34
|
+
export const DropdownMenuSubContent = React.forwardRef<
|
|
35
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
36
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
37
|
+
>(({ className, ...props }, ref) => (
|
|
38
|
+
<DropdownMenuPrimitive.SubContent
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn(
|
|
41
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
))
|
|
47
|
+
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
|
48
|
+
|
|
49
|
+
export const DropdownMenuContent = React.forwardRef<
|
|
50
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
51
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
52
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
53
|
+
<DropdownMenuPrimitive.Portal>
|
|
54
|
+
<DropdownMenuPrimitive.Content
|
|
55
|
+
ref={ref}
|
|
56
|
+
sideOffset={sideOffset}
|
|
57
|
+
className={cn(
|
|
58
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
</DropdownMenuPrimitive.Portal>
|
|
64
|
+
))
|
|
65
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
66
|
+
|
|
67
|
+
export const DropdownMenuItem = React.forwardRef<
|
|
68
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
70
|
+
inset?: boolean
|
|
71
|
+
}
|
|
72
|
+
>(({ className, inset, ...props }, ref) => (
|
|
73
|
+
<DropdownMenuPrimitive.Item
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(
|
|
76
|
+
'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
77
|
+
inset && 'pl-8',
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
))
|
|
83
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
84
|
+
|
|
85
|
+
export const DropdownMenuCheckboxItem = React.forwardRef<
|
|
86
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
87
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
88
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
89
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cn(
|
|
92
|
+
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
checked={checked}
|
|
96
|
+
{...props}
|
|
97
|
+
>
|
|
98
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
99
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
100
|
+
<Check className="h-4 w-4" />
|
|
101
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
102
|
+
</span>
|
|
103
|
+
{children}
|
|
104
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
105
|
+
))
|
|
106
|
+
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
|
107
|
+
|
|
108
|
+
export const DropdownMenuRadioItem = React.forwardRef<
|
|
109
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
110
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
111
|
+
>(({ className, children, ...props }, ref) => (
|
|
112
|
+
<DropdownMenuPrimitive.RadioItem
|
|
113
|
+
ref={ref}
|
|
114
|
+
className={cn(
|
|
115
|
+
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
121
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
122
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
123
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
124
|
+
</span>
|
|
125
|
+
{children}
|
|
126
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
127
|
+
))
|
|
128
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
129
|
+
|
|
130
|
+
export const DropdownMenuLabel = React.forwardRef<
|
|
131
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
132
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
133
|
+
inset?: boolean
|
|
134
|
+
}
|
|
135
|
+
>(({ className, inset, ...props }, ref) => (
|
|
136
|
+
<DropdownMenuPrimitive.Label
|
|
137
|
+
ref={ref}
|
|
138
|
+
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
))
|
|
142
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
143
|
+
|
|
144
|
+
export const DropdownMenuSeparator = React.forwardRef<
|
|
145
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
146
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
147
|
+
>(({ className, ...props }, ref) => (
|
|
148
|
+
<DropdownMenuPrimitive.Separator
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
))
|
|
154
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
155
|
+
|
|
156
|
+
export const DropdownMenuShortcut = ({
|
|
157
|
+
className,
|
|
158
|
+
...props
|
|
159
|
+
}: React.HTMLAttributes<HTMLSpanElement>): React.ReactElement => (
|
|
160
|
+
<span
|
|
161
|
+
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// shadcn-style Empty — placeholder for empty states.
|
|
2
|
+
// Compose: <Empty><EmptyHeader><EmptyMedia/><EmptyTitle/><EmptyDescription/></EmptyHeader><EmptyContent>…</EmptyContent></Empty>
|
|
3
|
+
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
6
|
+
import { cn } from '../lib/utils.js'
|
|
7
|
+
|
|
8
|
+
export const Empty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
9
|
+
({ className, ...props }, ref) => (
|
|
10
|
+
<div
|
|
11
|
+
ref={ref}
|
|
12
|
+
data-slot="empty"
|
|
13
|
+
className={cn(
|
|
14
|
+
'flex w-full flex-col items-center justify-center gap-6 rounded-lg border border-dashed border-border bg-card/40 p-8 text-center',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
Empty.displayName = 'Empty'
|
|
22
|
+
|
|
23
|
+
export const EmptyHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
24
|
+
({ className, ...props }, ref) => (
|
|
25
|
+
<div
|
|
26
|
+
ref={ref}
|
|
27
|
+
data-slot="empty-header"
|
|
28
|
+
className={cn('flex flex-col items-center gap-3 text-center', className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
EmptyHeader.displayName = 'EmptyHeader'
|
|
34
|
+
|
|
35
|
+
const emptyMediaVariants = cva(
|
|
36
|
+
'flex shrink-0 items-center justify-center [&>svg]:size-6',
|
|
37
|
+
{
|
|
38
|
+
variants: {
|
|
39
|
+
variant: {
|
|
40
|
+
default: 'text-muted-foreground',
|
|
41
|
+
icon: 'flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:size-6',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: { variant: 'icon' },
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export interface EmptyMediaProps
|
|
49
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
50
|
+
VariantProps<typeof emptyMediaVariants> {}
|
|
51
|
+
|
|
52
|
+
export const EmptyMedia = React.forwardRef<HTMLDivElement, EmptyMediaProps>(
|
|
53
|
+
({ className, variant, ...props }, ref) => (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
data-slot="empty-media"
|
|
57
|
+
className={cn(emptyMediaVariants({ variant }), className)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
EmptyMedia.displayName = 'EmptyMedia'
|
|
63
|
+
|
|
64
|
+
export const EmptyTitle = React.forwardRef<
|
|
65
|
+
HTMLHeadingElement,
|
|
66
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<h3
|
|
69
|
+
ref={ref}
|
|
70
|
+
data-slot="empty-title"
|
|
71
|
+
className={cn('text-base font-semibold tracking-tight', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
))
|
|
75
|
+
EmptyTitle.displayName = 'EmptyTitle'
|
|
76
|
+
|
|
77
|
+
export const EmptyDescription = React.forwardRef<
|
|
78
|
+
HTMLParagraphElement,
|
|
79
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
80
|
+
>(({ className, ...props }, ref) => (
|
|
81
|
+
<p
|
|
82
|
+
ref={ref}
|
|
83
|
+
data-slot="empty-description"
|
|
84
|
+
className={cn('max-w-sm text-sm text-muted-foreground', className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
))
|
|
88
|
+
EmptyDescription.displayName = 'EmptyDescription'
|
|
89
|
+
|
|
90
|
+
export const EmptyContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
91
|
+
({ className, ...props }, ref) => (
|
|
92
|
+
<div
|
|
93
|
+
ref={ref}
|
|
94
|
+
data-slot="empty-content"
|
|
95
|
+
className={cn('flex flex-wrap items-center justify-center gap-2', className)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
EmptyContent.displayName = 'EmptyContent'
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// shadcn-style Field — composable form-field layout primitives.
|
|
2
|
+
// Pure layout; no react-hook-form binding here. Pair with FormField from
|
|
3
|
+
// `./form.js` when wiring to RHF, or use standalone for lightweight forms.
|
|
4
|
+
//
|
|
5
|
+
// Conventions match the canonical shadcn `field` recipe:
|
|
6
|
+
// <Field>
|
|
7
|
+
// <FieldLabel>…</FieldLabel>
|
|
8
|
+
// <Input />
|
|
9
|
+
// <FieldDescription>…</FieldDescription>
|
|
10
|
+
// <FieldError>…</FieldError>
|
|
11
|
+
// </Field>
|
|
12
|
+
//
|
|
13
|
+
// <FieldGroup>…</FieldGroup>
|
|
14
|
+
// <FieldSet><FieldLegend/><FieldGroup/></FieldSet>
|
|
15
|
+
|
|
16
|
+
import * as React from 'react'
|
|
17
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
18
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
19
|
+
import { cn } from '../lib/utils.js'
|
|
20
|
+
import { Label } from './label.js'
|
|
21
|
+
|
|
22
|
+
const fieldVariants = cva('group/field flex w-full', {
|
|
23
|
+
variants: {
|
|
24
|
+
orientation: {
|
|
25
|
+
vertical: 'flex-col gap-2',
|
|
26
|
+
horizontal: 'flex-row items-center gap-3',
|
|
27
|
+
responsive: 'flex-col gap-2 sm:flex-row sm:items-center sm:gap-3',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { orientation: 'vertical' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export interface FieldProps
|
|
34
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
35
|
+
VariantProps<typeof fieldVariants> {
|
|
36
|
+
/** Render with a custom element via Radix Slot. */
|
|
37
|
+
asChild?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
|
41
|
+
({ className, orientation, asChild, ...props }, ref) => {
|
|
42
|
+
const Comp = asChild ? Slot : 'div'
|
|
43
|
+
return (
|
|
44
|
+
<Comp
|
|
45
|
+
ref={ref}
|
|
46
|
+
data-slot="field"
|
|
47
|
+
data-orientation={orientation ?? 'vertical'}
|
|
48
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
Field.displayName = 'Field'
|
|
55
|
+
|
|
56
|
+
export const FieldLabel = React.forwardRef<
|
|
57
|
+
React.ElementRef<typeof Label>,
|
|
58
|
+
React.ComponentPropsWithoutRef<typeof Label>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<Label
|
|
61
|
+
ref={ref}
|
|
62
|
+
data-slot="field-label"
|
|
63
|
+
className={cn(
|
|
64
|
+
'group-data-[invalid=true]/field:text-destructive flex items-center gap-2 text-sm font-medium leading-none',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
))
|
|
70
|
+
FieldLabel.displayName = 'FieldLabel'
|
|
71
|
+
|
|
72
|
+
export const FieldDescription = React.forwardRef<
|
|
73
|
+
HTMLParagraphElement,
|
|
74
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
75
|
+
>(({ className, ...props }, ref) => (
|
|
76
|
+
<p
|
|
77
|
+
ref={ref}
|
|
78
|
+
data-slot="field-description"
|
|
79
|
+
className={cn('text-sm text-muted-foreground leading-snug', className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
))
|
|
83
|
+
FieldDescription.displayName = 'FieldDescription'
|
|
84
|
+
|
|
85
|
+
export const FieldError = React.forwardRef<
|
|
86
|
+
HTMLParagraphElement,
|
|
87
|
+
React.HTMLAttributes<HTMLParagraphElement> & { errors?: ReadonlyArray<string | undefined> }
|
|
88
|
+
>(({ className, children, errors, ...props }, ref) => {
|
|
89
|
+
const list = errors?.filter((e): e is string => Boolean(e))
|
|
90
|
+
const body = list && list.length > 0 ? list.join(', ') : children
|
|
91
|
+
if (!body) return null
|
|
92
|
+
return (
|
|
93
|
+
<p
|
|
94
|
+
ref={ref}
|
|
95
|
+
data-slot="field-error"
|
|
96
|
+
className={cn('text-sm font-medium text-destructive', className)}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
{body}
|
|
100
|
+
</p>
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
FieldError.displayName = 'FieldError'
|
|
104
|
+
|
|
105
|
+
export const FieldGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
106
|
+
({ className, ...props }, ref) => (
|
|
107
|
+
<div
|
|
108
|
+
ref={ref}
|
|
109
|
+
data-slot="field-group"
|
|
110
|
+
role="group"
|
|
111
|
+
className={cn('flex flex-col gap-5', className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
FieldGroup.displayName = 'FieldGroup'
|
|
117
|
+
|
|
118
|
+
export const FieldSet = React.forwardRef<HTMLFieldSetElement, React.FieldsetHTMLAttributes<HTMLFieldSetElement>>(
|
|
119
|
+
({ className, ...props }, ref) => (
|
|
120
|
+
<fieldset
|
|
121
|
+
ref={ref}
|
|
122
|
+
data-slot="field-set"
|
|
123
|
+
className={cn('flex flex-col gap-4 rounded-lg border border-border p-4', className)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
FieldSet.displayName = 'FieldSet'
|
|
129
|
+
|
|
130
|
+
export const FieldLegend = React.forwardRef<
|
|
131
|
+
HTMLLegendElement,
|
|
132
|
+
React.HTMLAttributes<HTMLLegendElement>
|
|
133
|
+
>(({ className, ...props }, ref) => (
|
|
134
|
+
<legend
|
|
135
|
+
ref={ref}
|
|
136
|
+
data-slot="field-legend"
|
|
137
|
+
className={cn('px-1 text-sm font-medium leading-none', className)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
))
|
|
141
|
+
FieldLegend.displayName = 'FieldLegend'
|
|
142
|
+
|
|
143
|
+
export const FieldSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
144
|
+
({ className, ...props }, ref) => (
|
|
145
|
+
<div
|
|
146
|
+
ref={ref}
|
|
147
|
+
data-slot="field-separator"
|
|
148
|
+
role="separator"
|
|
149
|
+
aria-orientation="horizontal"
|
|
150
|
+
className={cn('h-px w-full bg-border', className)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
FieldSeparator.displayName = 'FieldSeparator'
|
|
156
|
+
|
|
157
|
+
/** Wrapper for label + description + control + error stacked vertically. */
|
|
158
|
+
export const FieldContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
159
|
+
({ className, ...props }, ref) => (
|
|
160
|
+
<div
|
|
161
|
+
ref={ref}
|
|
162
|
+
data-slot="field-content"
|
|
163
|
+
className={cn('flex min-w-0 flex-col gap-1.5', className)}
|
|
164
|
+
{...props}
|
|
165
|
+
/>
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
FieldContent.displayName = 'FieldContent'
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileInput — styled file picker with optional drag-and-drop, current-file
|
|
3
|
+
* display, and a remove button. Purely presentational: upload logic lives in
|
|
4
|
+
* the `packages/react` property renderer.
|
|
5
|
+
*
|
|
6
|
+
* Mobile-first: full-width by default, tap-friendly hit areas.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
import { Upload, X, FileText, Loader2 } from 'lucide-react'
|
|
11
|
+
import { cn } from '../lib/utils.js'
|
|
12
|
+
import { Button } from './button.js'
|
|
13
|
+
|
|
14
|
+
export interface FileInputLabels {
|
|
15
|
+
/** Dropzone aria-label. Default: "Choose file". */
|
|
16
|
+
chooseFile?: string
|
|
17
|
+
/** Drop zone hint before the link. Default: "Drag and drop or". */
|
|
18
|
+
dragAndDrop?: string
|
|
19
|
+
/** Link text when no file selected. Default: "choose a file". */
|
|
20
|
+
chooseAFile?: string
|
|
21
|
+
/** Upload spinner text (no filename). Default: "Uploading…". */
|
|
22
|
+
uploading?: string
|
|
23
|
+
/** Upload spinner text with filename — `{name}` is replaced. Default: "Uploading {name}…". */
|
|
24
|
+
uploadingFile?: string
|
|
25
|
+
/** Remove button aria-label. Default: "Remove file". */
|
|
26
|
+
removeFile?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FileInputProps {
|
|
30
|
+
/** Current stored value (storage key or URL). Shown as the "current file". */
|
|
31
|
+
value?: string | null
|
|
32
|
+
/** Human-readable display name for the current file. Falls back to `value`. */
|
|
33
|
+
displayName?: string | null
|
|
34
|
+
/** Public URL used for image thumbnail previews. */
|
|
35
|
+
previewUrl?: string | null
|
|
36
|
+
/** HTML `accept` attribute (e.g. `'image/*'` or `'.pdf,.docx'`). */
|
|
37
|
+
accept?: string
|
|
38
|
+
/** Whether a file is currently being uploaded. Shows a spinner. */
|
|
39
|
+
uploading?: boolean
|
|
40
|
+
/** Upload progress 0–100 (overrides the spinner with a determinate bar when set). */
|
|
41
|
+
uploadProgress?: number
|
|
42
|
+
/** Local file name shown next to the progress indicator. */
|
|
43
|
+
uploadingName?: string
|
|
44
|
+
/** Upload error message. */
|
|
45
|
+
error?: string
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
className?: string
|
|
48
|
+
/** Translated UI labels. All optional — English strings are the defaults. */
|
|
49
|
+
labels?: FileInputLabels
|
|
50
|
+
/** Called when the user picks a new file. */
|
|
51
|
+
onFileSelect: (file: File) => void
|
|
52
|
+
/** Called when the user removes the current file. */
|
|
53
|
+
onRemove?: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function FileInput({
|
|
57
|
+
value,
|
|
58
|
+
displayName,
|
|
59
|
+
previewUrl,
|
|
60
|
+
accept,
|
|
61
|
+
uploading = false,
|
|
62
|
+
uploadProgress,
|
|
63
|
+
uploadingName,
|
|
64
|
+
error,
|
|
65
|
+
disabled = false,
|
|
66
|
+
className,
|
|
67
|
+
labels,
|
|
68
|
+
onFileSelect,
|
|
69
|
+
onRemove,
|
|
70
|
+
}: FileInputProps): React.ReactElement {
|
|
71
|
+
const l = {
|
|
72
|
+
chooseFile: labels?.chooseFile ?? 'Choose file',
|
|
73
|
+
dragAndDrop: labels?.dragAndDrop ?? 'Drag and drop or',
|
|
74
|
+
chooseAFile: labels?.chooseAFile ?? 'choose a file',
|
|
75
|
+
uploading: labels?.uploading ?? 'Uploading…',
|
|
76
|
+
uploadingFile: labels?.uploadingFile ?? 'Uploading {name}…',
|
|
77
|
+
removeFile: labels?.removeFile ?? 'Remove file',
|
|
78
|
+
}
|
|
79
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
80
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
81
|
+
|
|
82
|
+
const handleFiles = (files: FileList | null) => {
|
|
83
|
+
const first = files?.[0]
|
|
84
|
+
if (!first || disabled || uploading) return
|
|
85
|
+
onFileSelect(first)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
89
|
+
handleFiles(e.target.files)
|
|
90
|
+
|
|
91
|
+
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
setIsDragging(false)
|
|
94
|
+
handleFiles(e.dataTransfer.files)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
if (!disabled && !uploading) setIsDragging(true)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleDragLeave = () => setIsDragging(false)
|
|
103
|
+
|
|
104
|
+
const label = displayName || (value ? value.split('/').pop() : null)
|
|
105
|
+
|
|
106
|
+
// Detect image for thumbnail preview
|
|
107
|
+
const isImage =
|
|
108
|
+
previewUrl
|
|
109
|
+
? /\.(jpe?g|png|gif|webp|avif|svg|bmp)(\?|$)/i.test(previewUrl)
|
|
110
|
+
: false
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className={cn('w-full space-y-2', className)}>
|
|
114
|
+
{/* Drop zone / trigger */}
|
|
115
|
+
<div
|
|
116
|
+
role="button"
|
|
117
|
+
tabIndex={disabled ? -1 : 0}
|
|
118
|
+
aria-label={l.chooseFile}
|
|
119
|
+
onClick={() => !disabled && !uploading && inputRef.current?.click()}
|
|
120
|
+
onKeyDown={(e) => {
|
|
121
|
+
if ((e.key === 'Enter' || e.key === ' ') && !disabled && !uploading) {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
inputRef.current?.click()
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
onDrop={handleDrop}
|
|
127
|
+
onDragOver={handleDragOver}
|
|
128
|
+
onDragLeave={handleDragLeave}
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex min-h-[5rem] w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed border-border bg-muted/30 px-4 py-6 text-center transition-colors',
|
|
131
|
+
'hover:border-primary/50 hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
132
|
+
isDragging && 'border-primary bg-primary/5',
|
|
133
|
+
(disabled || uploading) && 'cursor-not-allowed opacity-60',
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{uploading ? (
|
|
137
|
+
<div className="flex w-full max-w-sm flex-col items-center gap-2">
|
|
138
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
139
|
+
<Loader2 className="size-5 animate-spin" />
|
|
140
|
+
<span className="truncate" title={uploadingName ?? undefined}>
|
|
141
|
+
{uploadingName
|
|
142
|
+
? l.uploadingFile.replace('{name}', uploadingName)
|
|
143
|
+
: l.uploading}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
{uploadProgress != null && (
|
|
147
|
+
<div className="flex w-full items-center gap-2">
|
|
148
|
+
<div
|
|
149
|
+
className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted"
|
|
150
|
+
role="progressbar"
|
|
151
|
+
aria-valuemin={0}
|
|
152
|
+
aria-valuemax={100}
|
|
153
|
+
aria-valuenow={uploadProgress}
|
|
154
|
+
>
|
|
155
|
+
<div
|
|
156
|
+
className="h-full rounded-full bg-primary transition-[width] duration-150 ease-out"
|
|
157
|
+
style={{ width: `${uploadProgress}%` }}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<span className="w-10 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
|
|
161
|
+
{uploadProgress}%
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
<Upload className="size-6 text-muted-foreground" />
|
|
169
|
+
<span className="text-sm font-medium text-foreground">
|
|
170
|
+
<span className="hidden sm:inline">{l.dragAndDrop} </span>
|
|
171
|
+
<span className="text-primary underline-offset-2 hover:underline">
|
|
172
|
+
{l.chooseAFile}
|
|
173
|
+
</span>
|
|
174
|
+
</span>
|
|
175
|
+
{accept && (
|
|
176
|
+
<span className="text-xs text-muted-foreground">{accept}</span>
|
|
177
|
+
)}
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Current file row */}
|
|
183
|
+
{value && !uploading && (
|
|
184
|
+
<div className="flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2">
|
|
185
|
+
{isImage && previewUrl ? (
|
|
186
|
+
<img
|
|
187
|
+
src={previewUrl}
|
|
188
|
+
alt={label ?? 'preview'}
|
|
189
|
+
className="size-10 shrink-0 rounded object-cover"
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
<FileText className="size-5 shrink-0 text-muted-foreground" />
|
|
193
|
+
)}
|
|
194
|
+
<span className="min-w-0 flex-1 truncate text-sm text-foreground" title={label ?? value}>
|
|
195
|
+
{label ?? value}
|
|
196
|
+
</span>
|
|
197
|
+
{onRemove && !disabled && (
|
|
198
|
+
<Button
|
|
199
|
+
type="button"
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="icon"
|
|
202
|
+
className="size-7 shrink-0 text-muted-foreground hover:text-destructive"
|
|
203
|
+
aria-label={l.removeFile}
|
|
204
|
+
onClick={(e) => {
|
|
205
|
+
e.stopPropagation()
|
|
206
|
+
onRemove()
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<X className="size-4" />
|
|
210
|
+
</Button>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Error message */}
|
|
216
|
+
{error && (
|
|
217
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Hidden native input */}
|
|
221
|
+
<input
|
|
222
|
+
ref={inputRef}
|
|
223
|
+
type="file"
|
|
224
|
+
accept={accept}
|
|
225
|
+
className="sr-only"
|
|
226
|
+
onChange={handleInputChange}
|
|
227
|
+
disabled={disabled || uploading}
|
|
228
|
+
tabIndex={-1}
|
|
229
|
+
aria-hidden
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|