@stampui/blocks 1.0.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.
Files changed (107) hide show
  1. package/dist/components/ai-chat-shell.d.ts +1 -0
  2. package/dist/components/ai-chat-shell.js +23 -0
  3. package/dist/components/prompt-input.d.ts +5 -0
  4. package/dist/components/prompt-input.js +47 -0
  5. package/dist/components/registry-card.d.ts +6 -0
  6. package/dist/components/registry-card.js +15 -0
  7. package/dist/components/registry-explorer.d.ts +8 -0
  8. package/dist/components/registry-explorer.js +38 -0
  9. package/dist/components/token-stream.d.ts +7 -0
  10. package/dist/components/token-stream.js +21 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +23 -0
  13. package/dist/manifests.d.ts +3 -0
  14. package/dist/manifests.js +1666 -0
  15. package/dist/types.d.ts +44 -0
  16. package/dist/types.js +2 -0
  17. package/package.json +28 -0
  18. package/src/components/blocks/ai-chat-shell.tsx +97 -0
  19. package/src/components/blocks/auth-panel.tsx +203 -0
  20. package/src/components/blocks/feature-grid.tsx +122 -0
  21. package/src/components/blocks/hero-section.tsx +73 -0
  22. package/src/components/blocks/notification-center.tsx +185 -0
  23. package/src/components/blocks/onboarding-flow.tsx +230 -0
  24. package/src/components/blocks/pricing-section.tsx +135 -0
  25. package/src/components/blocks/project-command-center.tsx +188 -0
  26. package/src/components/blocks/prompt-input.tsx +81 -0
  27. package/src/components/blocks/registry-card.tsx +104 -0
  28. package/src/components/blocks/registry-explorer.tsx +78 -0
  29. package/src/components/blocks/settings-layout.tsx +178 -0
  30. package/src/components/blocks/stats-strip.tsx +100 -0
  31. package/src/components/blocks/token-stream.tsx +42 -0
  32. package/src/components/blocks/usage-card.tsx +116 -0
  33. package/src/components/core/accordion.tsx +58 -0
  34. package/src/components/core/alert-dialog.tsx +113 -0
  35. package/src/components/core/alert.tsx +48 -0
  36. package/src/components/core/animated-number.tsx +77 -0
  37. package/src/components/core/aspect-ratio.tsx +20 -0
  38. package/src/components/core/avatar-stack.tsx +61 -0
  39. package/src/components/core/avatar.tsx +90 -0
  40. package/src/components/core/badge.tsx +39 -0
  41. package/src/components/core/breadcrumb.tsx +63 -0
  42. package/src/components/core/button-group.tsx +37 -0
  43. package/src/components/core/button.tsx +110 -0
  44. package/src/components/core/calendar.tsx +143 -0
  45. package/src/components/core/card.tsx +60 -0
  46. package/src/components/core/carousel.tsx +170 -0
  47. package/src/components/core/chart.tsx +377 -0
  48. package/src/components/core/checkbox.tsx +64 -0
  49. package/src/components/core/collapsible.tsx +30 -0
  50. package/src/components/core/combobox.tsx +114 -0
  51. package/src/components/core/command-box.tsx +22 -0
  52. package/src/components/core/command.tsx +165 -0
  53. package/src/components/core/confirm-action.tsx +94 -0
  54. package/src/components/core/context-menu.tsx +139 -0
  55. package/src/components/core/copy-button.tsx +41 -0
  56. package/src/components/core/data-table.tsx +173 -0
  57. package/src/components/core/date-picker.tsx +73 -0
  58. package/src/components/core/dialog.tsx +83 -0
  59. package/src/components/core/drawer.tsx +87 -0
  60. package/src/components/core/dropdown-menu.tsx +147 -0
  61. package/src/components/core/empty.tsx +34 -0
  62. package/src/components/core/field.tsx +39 -0
  63. package/src/components/core/file-upload.tsx +143 -0
  64. package/src/components/core/hover-card.tsx +31 -0
  65. package/src/components/core/inline-edit.tsx +104 -0
  66. package/src/components/core/input-group.tsx +47 -0
  67. package/src/components/core/input-otp.tsx +108 -0
  68. package/src/components/core/input.tsx +37 -0
  69. package/src/components/core/kbd.tsx +47 -0
  70. package/src/components/core/label.tsx +28 -0
  71. package/src/components/core/marquee.tsx +61 -0
  72. package/src/components/core/menubar.tsx +120 -0
  73. package/src/components/core/multi-select.tsx +145 -0
  74. package/src/components/core/native-select.tsx +27 -0
  75. package/src/components/core/navigation-menu.tsx +130 -0
  76. package/src/components/core/number-stepper.tsx +80 -0
  77. package/src/components/core/pagination.tsx +80 -0
  78. package/src/components/core/password-input.tsx +90 -0
  79. package/src/components/core/popover.tsx +34 -0
  80. package/src/components/core/progress.tsx +63 -0
  81. package/src/components/core/radio-group.tsx +77 -0
  82. package/src/components/core/resizable.tsx +250 -0
  83. package/src/components/core/scroll-area.tsx +38 -0
  84. package/src/components/core/select.tsx +128 -0
  85. package/src/components/core/separator.tsx +47 -0
  86. package/src/components/core/sheet.tsx +118 -0
  87. package/src/components/core/sidebar.tsx +129 -0
  88. package/src/components/core/skeleton.tsx +32 -0
  89. package/src/components/core/slider.tsx +97 -0
  90. package/src/components/core/sonner.tsx +29 -0
  91. package/src/components/core/spinner.tsx +60 -0
  92. package/src/components/core/status-pulse.tsx +67 -0
  93. package/src/components/core/stepper.tsx +111 -0
  94. package/src/components/core/switch.tsx +72 -0
  95. package/src/components/core/table.tsx +104 -0
  96. package/src/components/core/tabs.tsx +55 -0
  97. package/src/components/core/tag-input.tsx +93 -0
  98. package/src/components/core/textarea.tsx +44 -0
  99. package/src/components/core/timeline.tsx +81 -0
  100. package/src/components/core/toggle-group.tsx +56 -0
  101. package/src/components/core/toggle.tsx +66 -0
  102. package/src/components/core/tooltip.tsx +31 -0
  103. package/src/components/core/typing-indicator.tsx +51 -0
  104. package/src/index.ts +8 -0
  105. package/src/manifests.ts +1682 -0
  106. package/src/types.ts +58 -0
  107. package/src/ui.ts +13 -0
@@ -0,0 +1,173 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronUp, ChevronDown, ChevronsUpDown, ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ // ── Types ─────────────────────────────────────────────────────────────────────
8
+
9
+ export type SortDirection = "asc" | "desc" | null
10
+
11
+ export interface Column<T> {
12
+ key: string
13
+ header: string
14
+ accessor: (row: T) => React.ReactNode
15
+ sortable?: boolean
16
+ className?: string
17
+ headerClassName?: string
18
+ }
19
+
20
+ export interface DataTableProps<T> {
21
+ columns: Column<T>[]
22
+ data: T[]
23
+ rowKey?: (row: T, index: number) => string
24
+ pageSize?: number
25
+ searchable?: boolean
26
+ searchPlaceholder?: string
27
+ searchFn?: (row: T, query: string) => boolean
28
+ className?: string
29
+ emptyMessage?: string
30
+ }
31
+
32
+ // ── Component ─────────────────────────────────────────────────────────────────
33
+
34
+ export function DataTable<T>({
35
+ columns,
36
+ data,
37
+ rowKey,
38
+ pageSize = 10,
39
+ searchable = false,
40
+ searchPlaceholder = "Search...",
41
+ searchFn,
42
+ className,
43
+ emptyMessage = "No results.",
44
+ }: DataTableProps<T>) {
45
+ const [sortKey, setSortKey] = React.useState<string | null>(null)
46
+ const [sortDir, setSortDir] = React.useState<SortDirection>(null)
47
+ const [query, setQuery] = React.useState("")
48
+ const [page, setPage] = React.useState(0)
49
+
50
+ const filtered = React.useMemo(() => {
51
+ if (!query || !searchable) return data
52
+ return data.filter((row) =>
53
+ searchFn
54
+ ? searchFn(row, query)
55
+ : columns.some((col) => String(col.accessor(row)).toLowerCase().includes(query.toLowerCase()))
56
+ )
57
+ }, [data, query, searchable, searchFn, columns])
58
+
59
+ const sorted = React.useMemo(() => {
60
+ if (!sortKey || !sortDir) return filtered
61
+ const col = columns.find((c) => c.key === sortKey)
62
+ if (!col) return filtered
63
+ return [...filtered].sort((a, b) => {
64
+ const av = String(col.accessor(a))
65
+ const bv = String(col.accessor(b))
66
+ const cmp = av.localeCompare(bv, undefined, { numeric: true })
67
+ return sortDir === "asc" ? cmp : -cmp
68
+ })
69
+ }, [filtered, sortKey, sortDir, columns])
70
+
71
+ const totalPages = Math.ceil(sorted.length / pageSize)
72
+ const paged = sorted.slice(page * pageSize, (page + 1) * pageSize)
73
+
74
+ React.useEffect(() => { setPage(0) }, [query, sortKey, sortDir])
75
+
76
+ function toggleSort(key: string) {
77
+ if (sortKey !== key) { setSortKey(key); setSortDir("asc") }
78
+ else if (sortDir === "asc") setSortDir("desc")
79
+ else { setSortKey(null); setSortDir(null) }
80
+ }
81
+
82
+ return (
83
+ <div className={cx("space-y-3", className)}>
84
+ {searchable && (
85
+ <input
86
+ value={query}
87
+ onChange={(e) => setQuery(e.target.value)}
88
+ placeholder={searchPlaceholder}
89
+ className="h-9 w-full max-w-xs rounded-lg border border-border bg-surface-2 px-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-border-strong focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background"
90
+ />
91
+ )}
92
+ <div className="rounded-xl border border-border overflow-hidden">
93
+ <div className="overflow-x-auto">
94
+ <table className="w-full text-sm">
95
+ <thead>
96
+ <tr className="border-b border-border bg-surface-2">
97
+ {columns.map((col) => (
98
+ <th
99
+ key={col.key}
100
+ className={cx(
101
+ "px-4 py-3 text-left text-xs font-medium text-muted-foreground",
102
+ col.sortable && "cursor-pointer select-none hover:text-foreground transition-colors",
103
+ col.headerClassName
104
+ )}
105
+ onClick={col.sortable ? () => toggleSort(col.key) : undefined}
106
+ >
107
+ <div className="flex items-center gap-1">
108
+ {col.header}
109
+ {col.sortable && (
110
+ <span className="text-muted-foreground">
111
+ {sortKey === col.key && sortDir === "asc" ? (
112
+ <ChevronUp className="h-3.5 w-3.5" />
113
+ ) : sortKey === col.key && sortDir === "desc" ? (
114
+ <ChevronDown className="h-3.5 w-3.5" />
115
+ ) : (
116
+ <ChevronsUpDown className="h-3.5 w-3.5 opacity-40" />
117
+ )}
118
+ </span>
119
+ )}
120
+ </div>
121
+ </th>
122
+ ))}
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ {paged.length === 0 ? (
127
+ <tr>
128
+ <td colSpan={columns.length} className="px-4 py-10 text-center text-sm text-muted-foreground">
129
+ {emptyMessage}
130
+ </td>
131
+ </tr>
132
+ ) : (
133
+ paged.map((row, i) => (
134
+ <tr key={rowKey ? rowKey(row, i) : i} className="border-b border-border last:border-0 hover:bg-surface-2 transition-colors">
135
+ {columns.map((col) => (
136
+ <td key={col.key} className={cx("px-4 py-3", col.className)}>
137
+ {col.accessor(row)}
138
+ </td>
139
+ ))}
140
+ </tr>
141
+ ))
142
+ )}
143
+ </tbody>
144
+ </table>
145
+ </div>
146
+ </div>
147
+ {totalPages > 1 && (
148
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
149
+ <span>{sorted.length} row{sorted.length !== 1 ? "s" : ""}</span>
150
+ <div className="flex items-center gap-1">
151
+ <button
152
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
153
+ disabled={page === 0}
154
+ className="flex h-7 w-7 items-center justify-center rounded-md border border-border hover:bg-surface-2 disabled:opacity-40 disabled:pointer-events-none transition-colors"
155
+ >
156
+ <ChevronLeft className="h-3.5 w-3.5" />
157
+ </button>
158
+ <span className="px-2 font-medium text-foreground">
159
+ {page + 1} / {totalPages}
160
+ </span>
161
+ <button
162
+ onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
163
+ disabled={page >= totalPages - 1}
164
+ className="flex h-7 w-7 items-center justify-center rounded-md border border-border hover:bg-surface-2 disabled:opacity-40 disabled:pointer-events-none transition-colors"
165
+ >
166
+ <ChevronRight className="h-3.5 w-3.5" />
167
+ </button>
168
+ </div>
169
+ </div>
170
+ )}
171
+ </div>
172
+ )
173
+ }
@@ -0,0 +1,73 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixPopover from "@radix-ui/react-popover"
5
+ import { CalendarDays } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+ import { Calendar, type CalendarProps } from "@/components/core/calendar"
8
+
9
+ function formatDate(date: Date): string {
10
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
11
+ }
12
+
13
+ export interface DatePickerProps extends Omit<CalendarProps, "className" | "disabled"> {
14
+ placeholder?: string
15
+ disabled?: CalendarProps["disabled"] | boolean
16
+ className?: string
17
+ }
18
+
19
+ export function DatePicker({
20
+ selected,
21
+ onSelect,
22
+ placeholder = "Pick a date...",
23
+ disabled,
24
+ fromDate,
25
+ toDate,
26
+ className,
27
+ }: DatePickerProps) {
28
+ const [open, setOpen] = React.useState(false)
29
+ const isDisabledTrigger = typeof disabled === "boolean" ? disabled : false
30
+ const disabledFn = typeof disabled === "function" ? disabled : undefined
31
+
32
+ function handleSelect(date: Date) {
33
+ onSelect?.(date)
34
+ setOpen(false)
35
+ }
36
+
37
+ return (
38
+ <RadixPopover.Root open={open} onOpenChange={setOpen}>
39
+ <RadixPopover.Trigger asChild>
40
+ <button
41
+ type="button"
42
+ disabled={isDisabledTrigger}
43
+ className={cx(
44
+ "flex h-9 w-full items-center justify-start gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm outline-none",
45
+ "hover:border-border-strong transition-colors text-left",
46
+ "focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background",
47
+ "disabled:cursor-not-allowed disabled:opacity-50",
48
+ !selected && "text-muted-foreground",
49
+ className
50
+ )}
51
+ >
52
+ <CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
53
+ <span>{selected ? formatDate(selected) : placeholder}</span>
54
+ </button>
55
+ </RadixPopover.Trigger>
56
+ <RadixPopover.Portal>
57
+ <RadixPopover.Content
58
+ align="start"
59
+ sideOffset={4}
60
+ className="z-50 w-72 rounded-xl border border-border bg-card shadow-lg outline-none animate-in fade-in-0 zoom-in-95"
61
+ >
62
+ <Calendar
63
+ selected={selected}
64
+ onSelect={handleSelect}
65
+ disabled={disabledFn}
66
+ fromDate={fromDate}
67
+ toDate={toDate}
68
+ />
69
+ </RadixPopover.Content>
70
+ </RadixPopover.Portal>
71
+ </RadixPopover.Root>
72
+ )
73
+ }
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixDialog from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+
8
+ export const Dialog = RadixDialog.Root
9
+ export const DialogTrigger = RadixDialog.Trigger
10
+ export const DialogPortal = RadixDialog.Portal
11
+ export const DialogClose = RadixDialog.Close
12
+
13
+ export const DialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof RadixDialog.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <RadixDialog.Overlay
18
+ ref={ref}
19
+ className={cx(
20
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
21
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
22
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ DialogOverlay.displayName = "DialogOverlay"
29
+
30
+ export const DialogContent = React.forwardRef<
31
+ React.ElementRef<typeof RadixDialog.Content>,
32
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Content> & { hideClose?: boolean }
33
+ >(({ className, children, hideClose, ...props }, ref) => (
34
+ <RadixDialog.Portal>
35
+ <DialogOverlay />
36
+ <RadixDialog.Content
37
+ ref={ref}
38
+ className={cx(
39
+ "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
40
+ "rounded-2xl border border-border bg-card p-6 shadow-xl outline-none",
41
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
42
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {children}
48
+ {!hideClose && (
49
+ <RadixDialog.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-2 hover:text-foreground outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background">
50
+ <X className="h-4 w-4" />
51
+ <span className="sr-only">Close</span>
52
+ </RadixDialog.Close>
53
+ )}
54
+ </RadixDialog.Content>
55
+ </RadixDialog.Portal>
56
+ ))
57
+ DialogContent.displayName = "DialogContent"
58
+
59
+ export const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div className={cx("flex flex-col gap-1.5 mb-4", className)} {...props} />
61
+ )
62
+ DialogHeader.displayName = "DialogHeader"
63
+
64
+ export const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
65
+ <div className={cx("flex items-center justify-end gap-2 mt-6", className)} {...props} />
66
+ )
67
+ DialogFooter.displayName = "DialogFooter"
68
+
69
+ export const DialogTitle = React.forwardRef<
70
+ React.ElementRef<typeof RadixDialog.Title>,
71
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
72
+ >(({ className, ...props }, ref) => (
73
+ <RadixDialog.Title ref={ref} className={cx("text-base font-semibold leading-tight text-foreground", className)} {...props} />
74
+ ))
75
+ DialogTitle.displayName = "DialogTitle"
76
+
77
+ export const DialogDescription = React.forwardRef<
78
+ React.ElementRef<typeof RadixDialog.Description>,
79
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
80
+ >(({ className, ...props }, ref) => (
81
+ <RadixDialog.Description ref={ref} className={cx("text-sm text-muted-foreground leading-relaxed", className)} {...props} />
82
+ ))
83
+ DialogDescription.displayName = "DialogDescription"
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixDialog from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+
8
+ export const Drawer = RadixDialog.Root
9
+ export const DrawerTrigger = RadixDialog.Trigger
10
+ export const DrawerClose = RadixDialog.Close
11
+ export const DrawerPortal = RadixDialog.Portal
12
+
13
+ export const DrawerOverlay = React.forwardRef<
14
+ React.ElementRef<typeof RadixDialog.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <RadixDialog.Overlay
18
+ ref={ref}
19
+ className={cx(
20
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
21
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
22
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ DrawerOverlay.displayName = "DrawerOverlay"
29
+
30
+ export const DrawerContent = React.forwardRef<
31
+ React.ElementRef<typeof RadixDialog.Content>,
32
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Content>
33
+ >(({ className, children, ...props }, ref) => (
34
+ <RadixDialog.Portal>
35
+ <DrawerOverlay />
36
+ <RadixDialog.Content
37
+ ref={ref}
38
+ className={cx(
39
+ "fixed inset-x-0 bottom-0 z-50 flex flex-col rounded-t-2xl border-t border-border bg-card pb-safe shadow-xl outline-none",
40
+ "max-h-[85svh]",
41
+ "data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom data-[state=open]:duration-300",
42
+ "data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=closed]:duration-300",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <div className="mx-auto mt-3 mb-2 h-1.5 w-10 shrink-0 rounded-full bg-border" />
48
+ {children}
49
+ <RadixDialog.Close className="absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-2 hover:text-foreground outline-none focus-visible:ring-1 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background">
50
+ <X className="h-4 w-4" />
51
+ <span className="sr-only">Close</span>
52
+ </RadixDialog.Close>
53
+ </RadixDialog.Content>
54
+ </RadixDialog.Portal>
55
+ ))
56
+ DrawerContent.displayName = "DrawerContent"
57
+
58
+ export const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
59
+ <div className={cx("flex flex-col gap-1.5 px-6 pb-3", className)} {...props} />
60
+ )
61
+ DrawerHeader.displayName = "DrawerHeader"
62
+
63
+ export const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div className={cx("flex flex-col gap-2 px-6 pt-0 pb-6 mt-auto", className)} {...props} />
65
+ )
66
+ DrawerFooter.displayName = "DrawerFooter"
67
+
68
+ export const DrawerTitle = React.forwardRef<
69
+ React.ElementRef<typeof RadixDialog.Title>,
70
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
71
+ >(({ className, ...props }, ref) => (
72
+ <RadixDialog.Title ref={ref} className={cx("text-base font-semibold leading-tight text-foreground", className)} {...props} />
73
+ ))
74
+ DrawerTitle.displayName = "DrawerTitle"
75
+
76
+ export const DrawerDescription = React.forwardRef<
77
+ React.ElementRef<typeof RadixDialog.Description>,
78
+ React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
79
+ >(({ className, ...props }, ref) => (
80
+ <RadixDialog.Description ref={ref} className={cx("text-sm text-muted-foreground leading-relaxed", className)} {...props} />
81
+ ))
82
+ DrawerDescription.displayName = "DrawerDescription"
83
+
84
+ export const DrawerBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
85
+ <div className={cx("flex-1 overflow-y-auto px-6 py-2", className)} {...props} />
86
+ )
87
+ DrawerBody.displayName = "DrawerBody"
@@ -0,0 +1,147 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadixDropdown from "@radix-ui/react-dropdown-menu"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+ import { cx } from "@/lib/cx"
7
+
8
+ export const DropdownMenu = RadixDropdown.Root
9
+ export const DropdownMenuTrigger = RadixDropdown.Trigger
10
+ export const DropdownMenuGroup = RadixDropdown.Group
11
+ export const DropdownMenuPortal = RadixDropdown.Portal
12
+ export const DropdownMenuSub = RadixDropdown.Sub
13
+ export const DropdownMenuRadioGroup = RadixDropdown.RadioGroup
14
+
15
+ export const DropdownMenuContent = React.forwardRef<
16
+ React.ElementRef<typeof RadixDropdown.Content>,
17
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.Content>
18
+ >(({ className, sideOffset = 6, ...props }, ref) => (
19
+ <RadixDropdown.Portal>
20
+ <RadixDropdown.Content
21
+ ref={ref}
22
+ sideOffset={sideOffset}
23
+ className={cx(
24
+ "z-50 min-w-[180px] overflow-hidden rounded-xl border border-border bg-card p-1.5 shadow-lg",
25
+ "animate-in fade-in-0 zoom-in-95",
26
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
27
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
28
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ </RadixDropdown.Portal>
34
+ ))
35
+ DropdownMenuContent.displayName = "DropdownMenuContent"
36
+
37
+ const itemBase = cx(
38
+ "relative flex cursor-default select-none items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none transition-colors",
39
+ "text-foreground focus:bg-surface-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
40
+ )
41
+
42
+ export const DropdownMenuItem = React.forwardRef<
43
+ React.ElementRef<typeof RadixDropdown.Item>,
44
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.Item> & { inset?: boolean }
45
+ >(({ className, inset, ...props }, ref) => (
46
+ <RadixDropdown.Item
47
+ ref={ref}
48
+ className={cx(itemBase, inset && "pl-8", className)}
49
+ {...props}
50
+ />
51
+ ))
52
+ DropdownMenuItem.displayName = "DropdownMenuItem"
53
+
54
+ export const DropdownMenuCheckboxItem = React.forwardRef<
55
+ React.ElementRef<typeof RadixDropdown.CheckboxItem>,
56
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.CheckboxItem>
57
+ >(({ className, children, checked, ...props }, ref) => (
58
+ <RadixDropdown.CheckboxItem
59
+ ref={ref}
60
+ checked={checked}
61
+ className={cx(itemBase, "pl-8", className)}
62
+ {...props}
63
+ >
64
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
65
+ <RadixDropdown.ItemIndicator>
66
+ <Check className="h-4 w-4" />
67
+ </RadixDropdown.ItemIndicator>
68
+ </span>
69
+ {children}
70
+ </RadixDropdown.CheckboxItem>
71
+ ))
72
+ DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"
73
+
74
+ export const DropdownMenuRadioItem = React.forwardRef<
75
+ React.ElementRef<typeof RadixDropdown.RadioItem>,
76
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.RadioItem>
77
+ >(({ className, children, ...props }, ref) => (
78
+ <RadixDropdown.RadioItem
79
+ ref={ref}
80
+ className={cx(itemBase, "pl-8", className)}
81
+ {...props}
82
+ >
83
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
84
+ <RadixDropdown.ItemIndicator>
85
+ <Circle className="h-2 w-2 fill-current" />
86
+ </RadixDropdown.ItemIndicator>
87
+ </span>
88
+ {children}
89
+ </RadixDropdown.RadioItem>
90
+ ))
91
+ DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"
92
+
93
+ export const DropdownMenuLabel = React.forwardRef<
94
+ React.ElementRef<typeof RadixDropdown.Label>,
95
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.Label> & { inset?: boolean }
96
+ >(({ className, inset, ...props }, ref) => (
97
+ <RadixDropdown.Label
98
+ ref={ref}
99
+ className={cx("px-2.5 py-1.5 text-xs font-medium text-muted-foreground", inset && "pl-8", className)}
100
+ {...props}
101
+ />
102
+ ))
103
+ DropdownMenuLabel.displayName = "DropdownMenuLabel"
104
+
105
+ export const DropdownMenuSeparator = React.forwardRef<
106
+ React.ElementRef<typeof RadixDropdown.Separator>,
107
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.Separator>
108
+ >(({ className, ...props }, ref) => (
109
+ <RadixDropdown.Separator ref={ref} className={cx("-mx-1.5 my-1.5 h-px bg-border", className)} {...props} />
110
+ ))
111
+ DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
112
+
113
+ export const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
114
+ <span className={cx("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
115
+ )
116
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
117
+
118
+ export const DropdownMenuSubTrigger = React.forwardRef<
119
+ React.ElementRef<typeof RadixDropdown.SubTrigger>,
120
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.SubTrigger> & { inset?: boolean }
121
+ >(({ className, inset, children, ...props }, ref) => (
122
+ <RadixDropdown.SubTrigger
123
+ ref={ref}
124
+ className={cx(itemBase, "data-[state=open]:bg-surface-2", inset && "pl-8", className)}
125
+ {...props}
126
+ >
127
+ {children}
128
+ <ChevronRight className="ml-auto h-4 w-4" />
129
+ </RadixDropdown.SubTrigger>
130
+ ))
131
+ DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"
132
+
133
+ export const DropdownMenuSubContent = React.forwardRef<
134
+ React.ElementRef<typeof RadixDropdown.SubContent>,
135
+ React.ComponentPropsWithoutRef<typeof RadixDropdown.SubContent>
136
+ >(({ className, ...props }, ref) => (
137
+ <RadixDropdown.SubContent
138
+ ref={ref}
139
+ className={cx(
140
+ "z-50 min-w-[160px] overflow-hidden rounded-xl border border-border bg-card p-1.5 shadow-lg",
141
+ "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
142
+ className
143
+ )}
144
+ {...props}
145
+ />
146
+ ))
147
+ DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
@@ -0,0 +1,34 @@
1
+ import * as React from "react"
2
+ import { cx } from "@/lib/cx"
3
+
4
+ export interface EmptyProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ icon?: React.ReactNode
6
+ title: string
7
+ description?: string
8
+ action?: React.ReactNode
9
+ }
10
+
11
+ export function Empty({ icon, title, description, action, className, ...props }: EmptyProps) {
12
+ return (
13
+ <div
14
+ className={cx(
15
+ "flex flex-col items-center justify-center gap-3 px-6 py-12 text-center",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ {icon && (
21
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl border border-border bg-surface-2 text-muted-foreground">
22
+ {icon}
23
+ </div>
24
+ )}
25
+ <div className="space-y-1 max-w-xs">
26
+ <p className="text-sm font-medium text-foreground">{title}</p>
27
+ {description && (
28
+ <p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
29
+ )}
30
+ </div>
31
+ {action && <div className="mt-1">{action}</div>}
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react"
2
+ import * as RadixLabel from "@radix-ui/react-label"
3
+ import { cx } from "@/lib/cx"
4
+
5
+ export const Field = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div ref={ref} className={cx("flex flex-col gap-1.5", className)} {...props} />
8
+ )
9
+ )
10
+ Field.displayName = "Field"
11
+
12
+ export const FieldLabel = React.forwardRef<
13
+ React.ElementRef<typeof RadixLabel.Root>,
14
+ React.ComponentPropsWithoutRef<typeof RadixLabel.Root> & { required?: boolean }
15
+ >(({ className, children, required, ...props }, ref) => (
16
+ <RadixLabel.Root
17
+ ref={ref}
18
+ className={cx("text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)}
19
+ {...props}
20
+ >
21
+ {children}
22
+ {required && <span className="ml-1 text-muted-foreground">*</span>}
23
+ </RadixLabel.Root>
24
+ ))
25
+ FieldLabel.displayName = "FieldLabel"
26
+
27
+ export const FieldHint = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
28
+ ({ className, ...props }, ref) => (
29
+ <p ref={ref} className={cx("text-xs text-muted-foreground", className)} {...props} />
30
+ )
31
+ )
32
+ FieldHint.displayName = "FieldHint"
33
+
34
+ export const FieldError = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <p ref={ref} className={cx("text-xs text-danger font-medium", className)} {...props} />
37
+ )
38
+ )
39
+ FieldError.displayName = "FieldError"