@etherealui/react 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- package/src/utils/cn.ts +7 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../utils/cn"
|
|
3
|
+
|
|
4
|
+
export interface DividerProps {
|
|
5
|
+
label?: string
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Divider: React.FC<DividerProps> = ({ label, className }) => {
|
|
10
|
+
if (!label) {
|
|
11
|
+
return (
|
|
12
|
+
<hr
|
|
13
|
+
className={cn(
|
|
14
|
+
"border-t border-zinc-200 dark:border-zinc-800",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn("relative flex items-center py-2", className)}>
|
|
22
|
+
<div className="grow border-t border-zinc-200 dark:border-zinc-800" />
|
|
23
|
+
<span className="mx-3 shrink text-[11px] font-medium text-zinc-400 uppercase tracking-wider">
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
<div className="grow border-t border-zinc-200 dark:border-zinc-800" />
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
Divider.displayName = "Divider"
|
|
31
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
+
import { X } from "lucide-react"
|
|
4
|
+
import { cn } from "../utils/cn"
|
|
5
|
+
|
|
6
|
+
const Drawer = DialogPrimitive.Root
|
|
7
|
+
const DrawerTrigger = DialogPrimitive.Trigger
|
|
8
|
+
const DrawerClose = DialogPrimitive.Close
|
|
9
|
+
|
|
10
|
+
export interface DrawerContentProps
|
|
11
|
+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
12
|
+
side?: "right" | "left"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DrawerContent = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
17
|
+
DrawerContentProps
|
|
18
|
+
>(({ side = "right", className, children, ...props }, ref) => (
|
|
19
|
+
<DialogPrimitive.Portal>
|
|
20
|
+
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity" />
|
|
21
|
+
<DialogPrimitive.Content
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn(
|
|
24
|
+
"fixed inset-y-0 z-50 flex h-full w-full max-w-md flex-col border-zinc-200 bg-white p-6 text-zinc-900 shadow-2xl focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
25
|
+
side === "right" ? "right-0 border-l" : "left-0 border-r",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer">
|
|
32
|
+
<X className="h-4 w-4" />
|
|
33
|
+
<span className="sr-only">Close drawer</span>
|
|
34
|
+
</DialogPrimitive.Close>
|
|
35
|
+
</DialogPrimitive.Content>
|
|
36
|
+
</DialogPrimitive.Portal>
|
|
37
|
+
))
|
|
38
|
+
DrawerContent.displayName = "DrawerContent"
|
|
39
|
+
|
|
40
|
+
const DrawerHeader = ({
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
44
|
+
<div
|
|
45
|
+
className={cn("flex flex-col space-y-1.5 border-b border-zinc-200 pb-4 dark:border-zinc-800", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
DrawerHeader.displayName = "DrawerHeader"
|
|
50
|
+
|
|
51
|
+
const DrawerTitle = React.forwardRef<
|
|
52
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
53
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
54
|
+
>(({ className, ...props }, ref) => (
|
|
55
|
+
<DialogPrimitive.Title
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn("text-base font-semibold text-zinc-900 dark:text-zinc-100", className)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
))
|
|
61
|
+
DrawerTitle.displayName = "DrawerTitle"
|
|
62
|
+
|
|
63
|
+
const DrawerDescription = React.forwardRef<
|
|
64
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
65
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
66
|
+
>(({ className, ...props }, ref) => (
|
|
67
|
+
<DialogPrimitive.Description
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn("text-xs text-zinc-500 dark:text-zinc-400", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
))
|
|
73
|
+
DrawerDescription.displayName = DialogPrimitive.Description.displayName
|
|
74
|
+
|
|
75
|
+
const DrawerFooter = ({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
79
|
+
<div
|
|
80
|
+
className={cn("mt-auto flex items-center justify-end gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-800", className)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
DrawerFooter.displayName = "DrawerFooter"
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
Drawer,
|
|
88
|
+
DrawerTrigger,
|
|
89
|
+
DrawerClose,
|
|
90
|
+
DrawerContent,
|
|
91
|
+
DrawerHeader,
|
|
92
|
+
DrawerTitle,
|
|
93
|
+
DrawerDescription,
|
|
94
|
+
DrawerFooter,
|
|
95
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
|
|
5
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
6
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
7
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
8
|
+
|
|
9
|
+
const DropdownMenuContent = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
12
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
13
|
+
<DropdownMenuPrimitive.Portal>
|
|
14
|
+
<DropdownMenuPrimitive.Content
|
|
15
|
+
ref={ref}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
"z-50 min-w-[11rem] overflow-hidden rounded-xl border border-zinc-200 bg-white p-1 text-zinc-900 shadow-lg focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
</DropdownMenuPrimitive.Portal>
|
|
24
|
+
))
|
|
25
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
26
|
+
|
|
27
|
+
const DropdownMenuItem = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
30
|
+
shortcut?: string
|
|
31
|
+
destructive?: boolean
|
|
32
|
+
}
|
|
33
|
+
>(({ className, shortcut, destructive, children, ...props }, ref) => (
|
|
34
|
+
<DropdownMenuPrimitive.Item
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
"relative flex cursor-pointer select-none items-center justify-between gap-3 rounded-lg px-2.5 py-1.5 text-xs outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
38
|
+
destructive
|
|
39
|
+
? "text-rose-600 focus:bg-rose-50 dark:text-rose-400 dark:focus:bg-rose-500/15"
|
|
40
|
+
: "text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900 dark:text-zinc-200 dark:focus:bg-zinc-800 dark:focus:text-white",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<span className="flex items-center gap-2">{children}</span>
|
|
46
|
+
{shortcut && (
|
|
47
|
+
<span className="font-mono text-[10px] text-zinc-400">{shortcut}</span>
|
|
48
|
+
)}
|
|
49
|
+
</DropdownMenuPrimitive.Item>
|
|
50
|
+
))
|
|
51
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
52
|
+
|
|
53
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
54
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
55
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
56
|
+
>(({ className, ...props }, ref) => (
|
|
57
|
+
<DropdownMenuPrimitive.Separator
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn("-mx-1 my-1 h-px bg-zinc-200 dark:bg-zinc-800", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
))
|
|
63
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
64
|
+
|
|
65
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
66
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
67
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
|
68
|
+
>(({ className, ...props }, ref) => (
|
|
69
|
+
<DropdownMenuPrimitive.Label
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn("px-2.5 py-1.5 text-[11px] font-semibold text-zinc-400", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
))
|
|
75
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
76
|
+
|
|
77
|
+
export interface ContextMenuAction {
|
|
78
|
+
label: string
|
|
79
|
+
shortcut?: string
|
|
80
|
+
icon?: React.ReactNode
|
|
81
|
+
destructive?: boolean
|
|
82
|
+
onClick?: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ContextMenuBoxProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
86
|
+
actions: ContextMenuAction[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ContextMenuBox: React.FC<ContextMenuBoxProps> = ({
|
|
90
|
+
actions,
|
|
91
|
+
children,
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}) => {
|
|
95
|
+
const [coords, setCoords] = React.useState<{ x: number; y: number } | null>(null)
|
|
96
|
+
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
const close = () => setCoords(null)
|
|
99
|
+
window.addEventListener("click", close)
|
|
100
|
+
return () => window.removeEventListener("click", close)
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
onContextMenu={(e) => {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
108
|
+
setCoords({
|
|
109
|
+
x: Math.min(e.clientX - rect.left, rect.width - 180),
|
|
110
|
+
y: Math.min(e.clientY - rect.top, rect.height - 140),
|
|
111
|
+
})
|
|
112
|
+
}}
|
|
113
|
+
className={cn("relative select-none", className)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
{coords && (
|
|
118
|
+
<div
|
|
119
|
+
style={{ left: coords.x, top: coords.y }}
|
|
120
|
+
className="absolute z-40 min-w-[180px] rounded-xl border border-zinc-200 bg-white p-1 text-zinc-900 shadow-xl dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
|
|
121
|
+
>
|
|
122
|
+
{actions.map((act, idx) => (
|
|
123
|
+
<button
|
|
124
|
+
key={idx}
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => {
|
|
127
|
+
act.onClick?.()
|
|
128
|
+
setCoords(null)
|
|
129
|
+
}}
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-xs transition-colors cursor-pointer",
|
|
132
|
+
act.destructive
|
|
133
|
+
? "text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-500/15"
|
|
134
|
+
: "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
<span className="flex items-center gap-2">
|
|
138
|
+
{act.icon}
|
|
139
|
+
{act.label}
|
|
140
|
+
</span>
|
|
141
|
+
{act.shortcut && (
|
|
142
|
+
<span className="font-mono text-[10px] text-zinc-400">
|
|
143
|
+
{act.shortcut}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</button>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
ContextMenuBox.displayName = "ContextMenuBox"
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
DropdownMenu,
|
|
157
|
+
DropdownMenuTrigger,
|
|
158
|
+
DropdownMenuGroup,
|
|
159
|
+
DropdownMenuContent,
|
|
160
|
+
DropdownMenuItem,
|
|
161
|
+
DropdownMenuSeparator,
|
|
162
|
+
DropdownMenuLabel,
|
|
163
|
+
ContextMenuBox,
|
|
164
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { UploadCloud, FileText, CheckCircle2, Trash2 } from "lucide-react"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
import { Progress } from "./progress"
|
|
5
|
+
|
|
6
|
+
export interface UploadedFileItem {
|
|
7
|
+
name: string
|
|
8
|
+
size: string
|
|
9
|
+
progress: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FileDropzoneProps {
|
|
13
|
+
className?: string
|
|
14
|
+
initialFiles?: UploadedFileItem[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FileDropzone: React.FC<FileDropzoneProps> = ({
|
|
18
|
+
className,
|
|
19
|
+
initialFiles = [
|
|
20
|
+
{ name: "quarterly-report-2026.csv", size: "1.4 MB", progress: 100 },
|
|
21
|
+
{ name: "customer-invoices.pdf", size: "420 KB", progress: 76 },
|
|
22
|
+
],
|
|
23
|
+
}) => {
|
|
24
|
+
const [files, setFiles] = React.useState<UploadedFileItem[]>(initialFiles)
|
|
25
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={cn("space-y-3", className)}>
|
|
29
|
+
<div
|
|
30
|
+
onDragOver={(e) => {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
setIsDragging(true)
|
|
33
|
+
}}
|
|
34
|
+
onDragLeave={() => setIsDragging(false)}
|
|
35
|
+
onDrop={(e) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
setIsDragging(false)
|
|
38
|
+
const dropped = Array.from(e.dataTransfer.files)
|
|
39
|
+
if (dropped.length > 0) {
|
|
40
|
+
setFiles((prev) => [
|
|
41
|
+
...prev,
|
|
42
|
+
...dropped.map((f) => ({
|
|
43
|
+
name: f.name,
|
|
44
|
+
size: `${Math.max(1, Math.round(f.size / 1024))} KB`,
|
|
45
|
+
progress: 100,
|
|
46
|
+
})),
|
|
47
|
+
])
|
|
48
|
+
}
|
|
49
|
+
}}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
setFiles((prev) => [
|
|
52
|
+
...prev,
|
|
53
|
+
{
|
|
54
|
+
name: `attachment-${prev.length + 1}.pdf`,
|
|
55
|
+
size: "310 KB",
|
|
56
|
+
progress: 100,
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
}}
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex flex-col items-center justify-center rounded-xl border border-dashed p-6 text-center transition-colors cursor-pointer",
|
|
62
|
+
isDragging
|
|
63
|
+
? "border-blue-500 bg-blue-50/50 dark:bg-blue-500/10"
|
|
64
|
+
: "border-zinc-300 bg-zinc-50/50 hover:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900/40 dark:hover:border-zinc-700"
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<div className="mb-2.5 flex h-9 w-9 items-center justify-center rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
|
|
68
|
+
<UploadCloud className="h-4 w-4" />
|
|
69
|
+
</div>
|
|
70
|
+
<p className="text-xs font-medium text-zinc-800 dark:text-zinc-200">
|
|
71
|
+
Click to upload or drag and drop
|
|
72
|
+
</p>
|
|
73
|
+
<p className="mt-1 text-[11px] text-zinc-400">
|
|
74
|
+
CSV, PDF, JSON, or PNG up to 25MB
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{files.length > 0 && (
|
|
79
|
+
<div className="space-y-2">
|
|
80
|
+
{files.map((file, idx) => (
|
|
81
|
+
<div
|
|
82
|
+
key={`${file.name}-${idx}`}
|
|
83
|
+
className="rounded-xl border border-zinc-200 bg-white p-3 space-y-2 dark:border-zinc-800 dark:bg-zinc-900"
|
|
84
|
+
>
|
|
85
|
+
<div className="flex items-center justify-between gap-2">
|
|
86
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
87
|
+
<FileText className="h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />
|
|
88
|
+
<div className="min-w-0">
|
|
89
|
+
<p className="truncate text-xs font-medium text-zinc-800 dark:text-zinc-200">
|
|
90
|
+
{file.name}
|
|
91
|
+
</p>
|
|
92
|
+
<p className="text-[11px] text-zinc-400">{file.size}</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
{file.progress >= 100 && (
|
|
97
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
|
98
|
+
)}
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() =>
|
|
102
|
+
setFiles((prev) => prev.filter((_, i) => i !== idx))
|
|
103
|
+
}
|
|
104
|
+
className="rounded-md p-1 text-zinc-400 hover:bg-zinc-100 hover:text-rose-500 dark:hover:bg-zinc-800 cursor-pointer"
|
|
105
|
+
>
|
|
106
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<Progress
|
|
111
|
+
value={file.progress}
|
|
112
|
+
size="sm"
|
|
113
|
+
variant={file.progress >= 100 ? "emerald" : "blue"}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
FileDropzone.displayName = "FileDropzone"
|
|
123
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Button, type ButtonProps } from "./button"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
|
|
5
|
+
export interface IconButtonProps extends Omit<ButtonProps, "size"> {
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
10
|
+
({ label, className, children, ...props }, ref) => (
|
|
11
|
+
<Button
|
|
12
|
+
ref={ref}
|
|
13
|
+
size="icon"
|
|
14
|
+
aria-label={label}
|
|
15
|
+
className={cn("shrink-0", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</Button>
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
IconButton.displayName = "IconButton"
|
|
23
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../utils/cn"
|
|
3
|
+
|
|
4
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
leftIcon?: React.ReactNode
|
|
6
|
+
rightIcon?: React.ReactNode
|
|
7
|
+
prefixText?: string
|
|
8
|
+
suffixText?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
12
|
+
({ className, type = "text", leftIcon, rightIcon, prefixText, suffixText, ...props }, ref) => {
|
|
13
|
+
if (leftIcon || rightIcon || prefixText || suffixText) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
"flex h-9 w-full items-center rounded-lg border border-zinc-200 bg-white text-sm text-zinc-900 transition-colors focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500/20 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{prefixText && (
|
|
22
|
+
<span className="flex h-full items-center rounded-l-lg border-r border-zinc-200 bg-zinc-50 px-3 text-xs text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-300 select-none">
|
|
23
|
+
{prefixText}
|
|
24
|
+
</span>
|
|
25
|
+
)}
|
|
26
|
+
{leftIcon && (
|
|
27
|
+
<span className="pl-3 text-zinc-400 flex items-center shrink-0">
|
|
28
|
+
{leftIcon}
|
|
29
|
+
</span>
|
|
30
|
+
)}
|
|
31
|
+
<input
|
|
32
|
+
type={type}
|
|
33
|
+
ref={ref}
|
|
34
|
+
className="h-full w-full bg-transparent px-3 py-1.5 text-sm text-zinc-900 placeholder:text-zinc-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-zinc-100 dark:placeholder:text-zinc-500"
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
{rightIcon && (
|
|
38
|
+
<span className="pr-3 text-zinc-400 flex items-center shrink-0">
|
|
39
|
+
{rightIcon}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
{suffixText && (
|
|
43
|
+
<span className="flex h-full items-center border-l border-zinc-200 bg-zinc-50 px-3 text-xs text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800/50 dark:text-zinc-400 select-none">
|
|
44
|
+
{suffixText}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<input
|
|
53
|
+
type={type}
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex h-9 w-full rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder:text-zinc-400 transition-colors focus-visible:outline-none focus-visible:border-blue-500 focus-visible:ring-2 focus-visible:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder:text-zinc-500",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
ref={ref}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
Input.displayName = "Input"
|
|
65
|
+
|
|
66
|
+
export interface TextareaProps
|
|
67
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
68
|
+
autoResize?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
72
|
+
({ className, autoResize = false, onChange, ...props }, ref) => {
|
|
73
|
+
const innerRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
74
|
+
|
|
75
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
76
|
+
if (autoResize && innerRef.current) {
|
|
77
|
+
innerRef.current.style.height = "auto"
|
|
78
|
+
innerRef.current.style.height = `${innerRef.current.scrollHeight}px`
|
|
79
|
+
}
|
|
80
|
+
onChange?.(e)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<textarea
|
|
85
|
+
ref={(node) => {
|
|
86
|
+
innerRef.current = node
|
|
87
|
+
if (typeof ref === "function") ref(node)
|
|
88
|
+
else if (ref) ref.current = node
|
|
89
|
+
}}
|
|
90
|
+
onChange={handleChange}
|
|
91
|
+
className={cn(
|
|
92
|
+
"flex min-h-[84px] w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 transition-colors focus-visible:outline-none focus-visible:border-blue-500 focus-visible:ring-2 focus-visible:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder:text-zinc-500",
|
|
93
|
+
autoResize && "resize-none overflow-hidden",
|
|
94
|
+
className
|
|
95
|
+
)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
Textarea.displayName = "Textarea"
|
|
102
|
+
|
|
103
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
104
|
+
optional?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
108
|
+
({ className, optional, children, ...props }, ref) => (
|
|
109
|
+
<label
|
|
110
|
+
ref={ref}
|
|
111
|
+
className={cn(
|
|
112
|
+
"text-xs font-medium text-zinc-700 dark:text-zinc-300 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 flex items-center justify-between",
|
|
113
|
+
className
|
|
114
|
+
)}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
<span>{children}</span>
|
|
118
|
+
{optional && <span className="text-[11px] font-normal text-zinc-400">Optional</span>}
|
|
119
|
+
</label>
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
Label.displayName = "Label"
|
|
123
|
+
|
|
124
|
+
export { Input, Textarea, Label }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog as Modal,
|
|
3
|
+
DialogPortal as ModalPortal,
|
|
4
|
+
DialogOverlay as ModalOverlay,
|
|
5
|
+
DialogClose as ModalClose,
|
|
6
|
+
DialogTrigger as ModalTrigger,
|
|
7
|
+
DialogContent as ModalContent,
|
|
8
|
+
DialogHeader as ModalHeader,
|
|
9
|
+
DialogFooter as ModalFooter,
|
|
10
|
+
DialogTitle as ModalTitle,
|
|
11
|
+
DialogDescription as ModalDescription,
|
|
12
|
+
} from "./dialog"
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
Modal,
|
|
16
|
+
ModalPortal,
|
|
17
|
+
ModalOverlay,
|
|
18
|
+
ModalClose,
|
|
19
|
+
ModalTrigger,
|
|
20
|
+
ModalContent,
|
|
21
|
+
ModalHeader,
|
|
22
|
+
ModalFooter,
|
|
23
|
+
ModalTitle,
|
|
24
|
+
ModalDescription,
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
|
|
5
|
+
export interface PaginationProps {
|
|
6
|
+
totalPages?: number
|
|
7
|
+
initialPage?: number
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Pagination: React.FC<PaginationProps> = ({
|
|
12
|
+
totalPages = 5,
|
|
13
|
+
initialPage = 1,
|
|
14
|
+
className,
|
|
15
|
+
}) => {
|
|
16
|
+
const [page, setPage] = React.useState(initialPage)
|
|
17
|
+
return (
|
|
18
|
+
<div className={cn("inline-flex items-center gap-1", className)}>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
disabled={page <= 1}
|
|
22
|
+
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
23
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 cursor-pointer"
|
|
24
|
+
>
|
|
25
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
26
|
+
</button>
|
|
27
|
+
{Array.from({ length: totalPages }).map((_, idx) => {
|
|
28
|
+
const num = idx + 1
|
|
29
|
+
const active = num === page
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
key={num}
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => setPage(num)}
|
|
35
|
+
className={cn(
|
|
36
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-lg text-xs font-medium transition-colors cursor-pointer",
|
|
37
|
+
active
|
|
38
|
+
? "bg-blue-600 text-white"
|
|
39
|
+
: "border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{num}
|
|
43
|
+
</button>
|
|
44
|
+
)
|
|
45
|
+
})}
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
disabled={page >= totalPages}
|
|
49
|
+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
|
50
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 cursor-pointer"
|
|
51
|
+
>
|
|
52
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
Pagination.displayName = "Pagination"
|
|
58
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
3
|
+
import { cn } from "../utils/cn"
|
|
4
|
+
|
|
5
|
+
const Popover = PopoverPrimitive.Root
|
|
6
|
+
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
7
|
+
|
|
8
|
+
const PopoverContent = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
11
|
+
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
|
12
|
+
<PopoverPrimitive.Portal>
|
|
13
|
+
<PopoverPrimitive.Content
|
|
14
|
+
ref={ref}
|
|
15
|
+
align={align}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
"z-50 w-72 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-900 shadow-lg outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
</PopoverPrimitive.Portal>
|
|
24
|
+
))
|
|
25
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
26
|
+
|
|
27
|
+
export { Popover, PopoverTrigger, PopoverContent }
|
|
28
|
+
|