@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/index.d.mts +583 -0
  4. package/dist/index.d.ts +583 -0
  5. package/dist/index.js +5189 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +5189 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +83 -3
  10. package/src/blocks/app-shell.tsx +441 -0
  11. package/src/blocks/billing-card.tsx +2 -0
  12. package/src/blocks/data-table.tsx +479 -0
  13. package/src/blocks/filter-bar.tsx +134 -0
  14. package/src/blocks/header-navbar.tsx +7 -0
  15. package/src/blocks/metric-grid.tsx +5 -0
  16. package/src/blocks/profile-form.tsx +2 -0
  17. package/src/blocks/security-settings.tsx +2 -0
  18. package/src/blocks/settings-blocks.tsx +406 -0
  19. package/src/blocks/sidebar.tsx +6 -0
  20. package/src/blocks/stat-card.tsx +7 -0
  21. package/src/blocks/stat-cards.tsx +212 -0
  22. package/src/blocks/team-members.tsx +5 -0
  23. package/src/index.ts +114 -0
  24. package/src/primitives/accordion.tsx +61 -0
  25. package/src/primitives/announcement.tsx +33 -0
  26. package/src/primitives/avatar.tsx +156 -0
  27. package/src/primitives/badge.tsx +108 -0
  28. package/src/primitives/breadcrumb.tsx +45 -0
  29. package/src/primitives/button-group.tsx +79 -0
  30. package/src/primitives/button.tsx +165 -0
  31. package/src/primitives/carousel.tsx +84 -0
  32. package/src/primitives/chart.tsx +831 -0
  33. package/src/primitives/checkbox.tsx +26 -0
  34. package/src/primitives/chip.tsx +37 -0
  35. package/src/primitives/close-button.tsx +29 -0
  36. package/src/primitives/color-picker.tsx +62 -0
  37. package/src/primitives/combobox.tsx +123 -0
  38. package/src/primitives/command-palette.tsx +216 -0
  39. package/src/primitives/context-menu.tsx +84 -0
  40. package/src/primitives/date-calendar.tsx +76 -0
  41. package/src/primitives/date-picker.tsx +400 -0
  42. package/src/primitives/dialog.tsx +279 -0
  43. package/src/primitives/divider.tsx +31 -0
  44. package/src/primitives/drawer.tsx +95 -0
  45. package/src/primitives/dropdown-menu.tsx +164 -0
  46. package/src/primitives/file-upload.tsx +123 -0
  47. package/src/primitives/icon-button.tsx +23 -0
  48. package/src/primitives/input.tsx +124 -0
  49. package/src/primitives/modal.tsx +26 -0
  50. package/src/primitives/pagination.tsx +58 -0
  51. package/src/primitives/popover.tsx +28 -0
  52. package/src/primitives/progress.tsx +145 -0
  53. package/src/primitives/radio-group.tsx +63 -0
  54. package/src/primitives/select.tsx +207 -0
  55. package/src/primitives/skeleton.tsx +61 -0
  56. package/src/primitives/slider.tsx +44 -0
  57. package/src/primitives/switch.tsx +146 -0
  58. package/src/primitives/table.tsx +47 -0
  59. package/src/primitives/tabs.tsx +86 -0
  60. package/src/primitives/textarea.tsx +40 -0
  61. package/src/primitives/toast.tsx +71 -0
  62. package/src/primitives/tooltip.tsx +58 -0
  63. package/src/styles/globals.css +49 -0
  64. package/src/templates/control-tower.tsx +41 -0
  65. package/src/templates/executive-analytics.tsx +272 -0
  66. package/src/templates/workspace-settings.tsx +179 -0
  67. 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
+