@moloodev/ui 0.0.0-stage → 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.
Files changed (93) hide show
  1. package/package.json +139 -3
  2. package/registry/components.ts +135 -0
  3. package/registry/fonts.css +81 -0
  4. package/registry/suit-OFL.txt +94 -0
  5. package/registry/wanted-sans-OFL.txt +93 -0
  6. package/src/components/ui/accordion.tsx +74 -0
  7. package/src/components/ui/alert-dialog.tsx +187 -0
  8. package/src/components/ui/alert.tsx +83 -0
  9. package/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/src/components/ui/attachment.tsx +207 -0
  11. package/src/components/ui/avatar.tsx +109 -0
  12. package/src/components/ui/badge.tsx +54 -0
  13. package/src/components/ui/breadcrumb.tsx +126 -0
  14. package/src/components/ui/bubble.tsx +128 -0
  15. package/src/components/ui/button-group.tsx +103 -0
  16. package/src/components/ui/button.tsx +68 -0
  17. package/src/components/ui/calendar.tsx +222 -0
  18. package/src/components/ui/card.tsx +103 -0
  19. package/src/components/ui/carousel.tsx +242 -0
  20. package/src/components/ui/chart.tsx +398 -0
  21. package/src/components/ui/checkbox.tsx +28 -0
  22. package/src/components/ui/collapsible.tsx +40 -0
  23. package/src/components/ui/combobox.tsx +299 -0
  24. package/src/components/ui/command.tsx +218 -0
  25. package/src/components/ui/context-menu.tsx +271 -0
  26. package/src/components/ui/data-table.tsx +919 -0
  27. package/src/components/ui/date-picker.tsx +65 -0
  28. package/src/components/ui/dialog.tsx +224 -0
  29. package/src/components/ui/drawer.tsx +231 -0
  30. package/src/components/ui/dropdown-menu.tsx +268 -0
  31. package/src/components/ui/dropzone.tsx +186 -0
  32. package/src/components/ui/empty.tsx +101 -0
  33. package/src/components/ui/field.tsx +238 -0
  34. package/src/components/ui/form.tsx +83 -0
  35. package/src/components/ui/highlighted-source.tsx +200 -0
  36. package/src/components/ui/hover-card.tsx +51 -0
  37. package/src/components/ui/image-field.tsx +796 -0
  38. package/src/components/ui/input-group.tsx +149 -0
  39. package/src/components/ui/input-otp.tsx +86 -0
  40. package/src/components/ui/input-step.ts +19 -0
  41. package/src/components/ui/input.tsx +100 -0
  42. package/src/components/ui/item.tsx +201 -0
  43. package/src/components/ui/kbd.tsx +91 -0
  44. package/src/components/ui/kind-icon-glob.ts +24 -0
  45. package/src/components/ui/kind-icon-load.ts +52 -0
  46. package/src/components/ui/kind-icon-static.ts +46 -0
  47. package/src/components/ui/kind-icon.tsx +84 -0
  48. package/src/components/ui/kind-mark.tsx +65 -0
  49. package/src/components/ui/label.tsx +29 -0
  50. package/src/components/ui/marker.tsx +71 -0
  51. package/src/components/ui/menubar.tsx +277 -0
  52. package/src/components/ui/message-scroller.tsx +137 -0
  53. package/src/components/ui/message.tsx +92 -0
  54. package/src/components/ui/moloo-logo.tsx +45 -0
  55. package/src/components/ui/native-select.tsx +61 -0
  56. package/src/components/ui/navigation-menu.tsx +168 -0
  57. package/src/components/ui/outline-list.tsx +73 -0
  58. package/src/components/ui/pagination.tsx +130 -0
  59. package/src/components/ui/panel.tsx +108 -0
  60. package/src/components/ui/popover.tsx +90 -0
  61. package/src/components/ui/progress.tsx +117 -0
  62. package/src/components/ui/radio-group.tsx +38 -0
  63. package/src/components/ui/resizable.tsx +52 -0
  64. package/src/components/ui/scroll-area.tsx +54 -0
  65. package/src/components/ui/select.tsx +205 -0
  66. package/src/components/ui/separator.tsx +25 -0
  67. package/src/components/ui/sheet.tsx +140 -0
  68. package/src/components/ui/sidebar.tsx +746 -0
  69. package/src/components/ui/skeleton.tsx +13 -0
  70. package/src/components/ui/slider.tsx +52 -0
  71. package/src/components/ui/spinner.tsx +18 -0
  72. package/src/components/ui/switch.tsx +32 -0
  73. package/src/components/ui/table.tsx +116 -0
  74. package/src/components/ui/tabs.tsx +83 -0
  75. package/src/components/ui/tag-list-input.tsx +194 -0
  76. package/src/components/ui/textarea.tsx +18 -0
  77. package/src/components/ui/timeline.tsx +265 -0
  78. package/src/components/ui/toast.tsx +237 -0
  79. package/src/components/ui/toggle-group.tsx +90 -0
  80. package/src/components/ui/toggle.tsx +45 -0
  81. package/src/components/ui/tooltip.tsx +66 -0
  82. package/src/css.d.ts +1 -0
  83. package/src/hooks/use-clipboard.ts +82 -0
  84. package/src/hooks/use-mobile.ts +21 -0
  85. package/src/index.ts +106 -0
  86. package/src/kind-icon.ts +4 -0
  87. package/src/lib/format-relative-time.ts +41 -0
  88. package/src/lib/list-selection.ts +61 -0
  89. package/src/lib/utils.ts +6 -0
  90. package/src/moloo.css +360 -0
  91. package/src/styles.css +36 -0
  92. package/src/typeset.css +518 -0
  93. package/README.md +0 -3
@@ -0,0 +1,268 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { IconChevronRight, IconCheck } from "@tabler/icons-react"
8
+
9
+ function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
10
+ return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
11
+ }
12
+
13
+ function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
14
+ return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
15
+ }
16
+
17
+ function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
18
+ return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
19
+ }
20
+
21
+ function DropdownMenuContent({
22
+ align = "start",
23
+ alignOffset = 0,
24
+ side = "bottom",
25
+ sideOffset = 4,
26
+ className,
27
+ ...props
28
+ }: MenuPrimitive.Popup.Props &
29
+ Pick<
30
+ MenuPrimitive.Positioner.Props,
31
+ "align" | "alignOffset" | "side" | "sideOffset"
32
+ >) {
33
+ return (
34
+ <MenuPrimitive.Portal>
35
+ <MenuPrimitive.Positioner
36
+ className="isolate z-50 outline-none"
37
+ align={align}
38
+ alignOffset={alignOffset}
39
+ side={side}
40
+ sideOffset={sideOffset}
41
+ >
42
+ <MenuPrimitive.Popup
43
+ data-slot="dropdown-menu-content"
44
+ className={cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md moloo-surface bg-popover text-popover-foreground shadow-moloo-lg duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className )}
45
+ {...props}
46
+ />
47
+ </MenuPrimitive.Positioner>
48
+ </MenuPrimitive.Portal>
49
+ )
50
+ }
51
+
52
+ function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
53
+ return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
54
+ }
55
+
56
+ function DropdownMenuLabel({
57
+ className,
58
+ inset,
59
+ ...props
60
+ }: MenuPrimitive.GroupLabel.Props & {
61
+ inset?: boolean
62
+ }) {
63
+ return (
64
+ <MenuPrimitive.GroupLabel
65
+ data-slot="dropdown-menu-label"
66
+ data-inset={inset}
67
+ className={cn(
68
+ "px-2 py-2 text-sm text-muted-foreground data-inset:pl-7",
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ )
74
+ }
75
+
76
+ function DropdownMenuItem({
77
+ className,
78
+ inset,
79
+ variant = "default",
80
+ ...props
81
+ }: MenuPrimitive.Item.Props & {
82
+ inset?: boolean
83
+ variant?: "default" | "destructive"
84
+ }) {
85
+ return (
86
+ <MenuPrimitive.Item
87
+ data-slot="dropdown-menu-item"
88
+ data-inset={inset}
89
+ data-variant={variant}
90
+ className={cn(
91
+ "group/dropdown-menu-item relative flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm font-semibold outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ )
97
+ }
98
+
99
+ function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
100
+ return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
101
+ }
102
+
103
+ function DropdownMenuSubTrigger({
104
+ className,
105
+ inset,
106
+ children,
107
+ ...props
108
+ }: MenuPrimitive.SubmenuTrigger.Props & {
109
+ inset?: boolean
110
+ }) {
111
+ return (
112
+ <MenuPrimitive.SubmenuTrigger
113
+ data-slot="dropdown-menu-sub-trigger"
114
+ data-inset={inset}
115
+ className={cn(
116
+ "flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm font-semibold outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
117
+ className
118
+ )}
119
+ {...props}
120
+ >
121
+ {children}
122
+ <IconChevronRight className="ml-auto" />
123
+ </MenuPrimitive.SubmenuTrigger>
124
+ )
125
+ }
126
+
127
+ function DropdownMenuSubContent({
128
+ align = "start",
129
+ alignOffset = -3,
130
+ side = "right",
131
+ sideOffset = 0,
132
+ className,
133
+ ...props
134
+ }: React.ComponentProps<typeof DropdownMenuContent>) {
135
+ return (
136
+ <DropdownMenuContent
137
+ data-slot="dropdown-menu-sub-content"
138
+ className={cn("w-auto min-w-[96px] rounded-md moloo-surface bg-popover text-popover-foreground shadow-moloo-lg duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
139
+ align={align}
140
+ alignOffset={alignOffset}
141
+ side={side}
142
+ sideOffset={sideOffset}
143
+ {...props}
144
+ />
145
+ )
146
+ }
147
+
148
+ function DropdownMenuCheckboxItem({
149
+ className,
150
+ children,
151
+ checked,
152
+ inset,
153
+ ...props
154
+ }: MenuPrimitive.CheckboxItem.Props & {
155
+ inset?: boolean
156
+ }) {
157
+ return (
158
+ <MenuPrimitive.CheckboxItem
159
+ data-slot="dropdown-menu-checkbox-item"
160
+ data-inset={inset}
161
+ className={cn(
162
+ "relative flex cursor-pointer items-center gap-2 rounded-md py-2 pr-8 pl-2 text-sm font-semibold outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
163
+ className
164
+ )}
165
+ checked={checked}
166
+ {...props}
167
+ >
168
+ <span
169
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
170
+ data-slot="dropdown-menu-checkbox-item-indicator"
171
+ >
172
+ <MenuPrimitive.CheckboxItemIndicator>
173
+ <IconCheck
174
+ />
175
+ </MenuPrimitive.CheckboxItemIndicator>
176
+ </span>
177
+ {children}
178
+ </MenuPrimitive.CheckboxItem>
179
+ )
180
+ }
181
+
182
+ function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
183
+ return (
184
+ <MenuPrimitive.RadioGroup
185
+ data-slot="dropdown-menu-radio-group"
186
+ {...props}
187
+ />
188
+ )
189
+ }
190
+
191
+ function DropdownMenuRadioItem({
192
+ className,
193
+ children,
194
+ inset,
195
+ ...props
196
+ }: MenuPrimitive.RadioItem.Props & {
197
+ inset?: boolean
198
+ }) {
199
+ return (
200
+ <MenuPrimitive.RadioItem
201
+ data-slot="dropdown-menu-radio-item"
202
+ data-inset={inset}
203
+ className={cn(
204
+ "relative flex cursor-pointer items-center gap-2 rounded-md py-2 pr-8 pl-2 text-sm font-semibold outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
205
+ className
206
+ )}
207
+ {...props}
208
+ >
209
+ <span
210
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
211
+ data-slot="dropdown-menu-radio-item-indicator"
212
+ >
213
+ <MenuPrimitive.RadioItemIndicator>
214
+ <IconCheck
215
+ />
216
+ </MenuPrimitive.RadioItemIndicator>
217
+ </span>
218
+ {children}
219
+ </MenuPrimitive.RadioItem>
220
+ )
221
+ }
222
+
223
+ function DropdownMenuSeparator({
224
+ className,
225
+ ...props
226
+ }: MenuPrimitive.Separator.Props) {
227
+ return (
228
+ <MenuPrimitive.Separator
229
+ data-slot="dropdown-menu-separator"
230
+ className={cn("-mx-1 h-px bg-border", className)}
231
+ {...props}
232
+ />
233
+ )
234
+ }
235
+
236
+ function DropdownMenuShortcut({
237
+ className,
238
+ ...props
239
+ }: React.ComponentProps<"span">) {
240
+ return (
241
+ <span
242
+ data-slot="dropdown-menu-shortcut"
243
+ className={cn(
244
+ "ml-auto text-sm tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
245
+ className
246
+ )}
247
+ {...props}
248
+ />
249
+ )
250
+ }
251
+
252
+ export {
253
+ DropdownMenu,
254
+ DropdownMenuPortal,
255
+ DropdownMenuTrigger,
256
+ DropdownMenuContent,
257
+ DropdownMenuGroup,
258
+ DropdownMenuLabel,
259
+ DropdownMenuItem,
260
+ DropdownMenuCheckboxItem,
261
+ DropdownMenuRadioGroup,
262
+ DropdownMenuRadioItem,
263
+ DropdownMenuSeparator,
264
+ DropdownMenuShortcut,
265
+ DropdownMenuSub,
266
+ DropdownMenuSubTrigger,
267
+ DropdownMenuSubContent,
268
+ }
@@ -0,0 +1,186 @@
1
+ import * as React from "react"
2
+ import { IconUpload } from "@tabler/icons-react"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ export type DropzoneRejectReason = "type" | "size" | "count"
7
+
8
+ export type DropzoneRejection = {
9
+ file: File
10
+ reason: DropzoneRejectReason
11
+ }
12
+
13
+ export type DropzoneLimits = {
14
+ accept: string
15
+ multiple: boolean
16
+ maxSize?: number
17
+ }
18
+
19
+ export type DropzoneProps = {
20
+ accept: string
21
+ multiple?: boolean
22
+ maxSize?: number
23
+ disabled?: boolean
24
+ label?: string
25
+ description?: string
26
+ onFiles: (files: File[]) => void
27
+ onReject?: (rejections: readonly DropzoneRejection[]) => void
28
+ children?: React.ReactNode
29
+ className?: string
30
+ id?: string
31
+ "aria-invalid"?: React.AriaAttributes["aria-invalid"]
32
+ }
33
+
34
+ const singularLabel = "Drop a file here"
35
+ const pluralLabel = "Drop files here"
36
+ const singularDescription = "Drag and drop a file here, or click to select."
37
+ const pluralDescription = "Drag and drop files here, or click to select."
38
+
39
+ function acceptTokens(accept: string) {
40
+ return accept
41
+ .split(",")
42
+ .map((token) => token.trim().toLowerCase())
43
+ .filter((token) => token.length > 0)
44
+ }
45
+
46
+ function matchesAccept(file: File, tokens: readonly string[]) {
47
+ if (tokens.length === 0) return true
48
+ const type = file.type.toLowerCase()
49
+ const name = file.name.toLowerCase()
50
+ return tokens.some((token) => {
51
+ if (token.startsWith(".")) return name.endsWith(token)
52
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1))
53
+ return type === token
54
+ })
55
+ }
56
+
57
+ function rejectsSize(file: File, maxSize: number | undefined) {
58
+ if (maxSize === undefined || !Number.isFinite(maxSize) || maxSize < 0) return false
59
+ return file.size <= 0 || file.size > maxSize
60
+ }
61
+
62
+ export function partitionDropzoneFiles(
63
+ files: readonly File[],
64
+ limits: DropzoneLimits,
65
+ ): { accepted: File[]; rejected: DropzoneRejection[] } {
66
+ if (!limits.multiple && files.length > 1) {
67
+ return {
68
+ accepted: [],
69
+ rejected: files.map((file) => ({ file, reason: "count" })),
70
+ }
71
+ }
72
+
73
+ const tokens = acceptTokens(limits.accept)
74
+ const accepted: File[] = []
75
+ const rejected: DropzoneRejection[] = []
76
+ for (const file of files) {
77
+ if (!matchesAccept(file, tokens)) {
78
+ rejected.push({ file, reason: "type" })
79
+ continue
80
+ }
81
+ if (rejectsSize(file, limits.maxSize)) {
82
+ rejected.push({ file, reason: "size" })
83
+ continue
84
+ }
85
+ accepted.push(file)
86
+ }
87
+ return { accepted, rejected }
88
+ }
89
+
90
+ function Dropzone({
91
+ accept,
92
+ multiple = false,
93
+ maxSize,
94
+ disabled,
95
+ label,
96
+ description,
97
+ onFiles,
98
+ onReject,
99
+ children,
100
+ className,
101
+ id,
102
+ "aria-invalid": ariaInvalid,
103
+ }: DropzoneProps) {
104
+ const depth = React.useRef(0)
105
+ const [dragging, setDragging] = React.useState(false)
106
+ const generatedId = React.useId()
107
+ const inputId = id ?? generatedId
108
+ const labelId = React.useId()
109
+ const descriptionId = React.useId()
110
+ const resolvedLabel = label ?? (multiple ? pluralLabel : singularLabel)
111
+ const resolvedDescription =
112
+ description ?? (multiple ? pluralDescription : singularDescription)
113
+
114
+ function deliver(files: File[]) {
115
+ const result = partitionDropzoneFiles(files, { accept, multiple, maxSize })
116
+ if (result.rejected.length > 0) onReject?.(result.rejected)
117
+ onFiles(result.accepted)
118
+ }
119
+
120
+ return (
121
+ <div
122
+ data-slot="dropzone"
123
+ data-dragging={dragging ? "" : undefined}
124
+ className={cn(
125
+ "moloo-focus-within relative flex min-h-44 w-full flex-col items-center justify-center rounded-md border-2 border-dashed border-border bg-[var(--field-bg)] text-center shadow-moloo transition-colors hover:border-primary has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)] data-dragging:border-primary data-dragging:bg-primary/5",
126
+ disabled && "cursor-not-allowed opacity-50 hover:border-border",
127
+ className,
128
+ )}
129
+ onDragEnter={(event) => {
130
+ event.preventDefault()
131
+ depth.current += 1
132
+ if (!disabled && depth.current === 1) setDragging(true)
133
+ }}
134
+ onDragOver={(event) => {
135
+ event.preventDefault()
136
+ event.dataTransfer.dropEffect = disabled ? "none" : "copy"
137
+ }}
138
+ onDragLeave={(event) => {
139
+ event.preventDefault()
140
+ depth.current = Math.max(0, depth.current - 1)
141
+ if (depth.current === 0) setDragging(false)
142
+ }}
143
+ onDrop={(event) => {
144
+ event.preventDefault()
145
+ depth.current = 0
146
+ setDragging(false)
147
+ if (!disabled) deliver(Array.from(event.dataTransfer.files))
148
+ }}
149
+ >
150
+ <input
151
+ id={inputId}
152
+ type="file"
153
+ accept={accept}
154
+ multiple={multiple}
155
+ disabled={disabled}
156
+ aria-invalid={ariaInvalid}
157
+ aria-labelledby={labelId}
158
+ aria-describedby={descriptionId}
159
+ className="sr-only"
160
+ onChange={(event) => {
161
+ deliver(Array.from(event.target.files ?? []))
162
+ event.target.value = ""
163
+ }}
164
+ />
165
+ <label
166
+ htmlFor={inputId}
167
+ className={cn(
168
+ "flex min-h-44 w-full cursor-pointer flex-col items-center justify-center gap-3 px-5 py-8",
169
+ disabled && "pointer-events-none",
170
+ )}
171
+ >
172
+ {children ?? (
173
+ <IconUpload className="size-6 text-muted-foreground" aria-hidden />
174
+ )}
175
+ <span id={labelId} className="font-display text-sm font-bold tracking-[-0.02em]">
176
+ {resolvedLabel}
177
+ </span>
178
+ <span id={descriptionId} className="text-sm text-muted-foreground">
179
+ {resolvedDescription}
180
+ </span>
181
+ </label>
182
+ </div>
183
+ )
184
+ }
185
+
186
+ export { Dropzone }
@@ -0,0 +1,101 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-md border-2 border-dashed p-6 text-center text-balance",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn("flex max-w-sm flex-col items-center gap-2", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ const emptyMediaVariants = cva(
29
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
30
+ {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-transparent",
34
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ },
40
+ }
41
+ )
42
+
43
+ function EmptyMedia({
44
+ className,
45
+ variant = "default",
46
+ ...props
47
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
48
+ return (
49
+ <div
50
+ data-slot="empty-icon"
51
+ data-variant={variant}
52
+ className={cn(emptyMediaVariants({ variant, className }))}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="empty-title"
62
+ className={cn("font-heading text-sm font-medium", className)}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
69
+ return (
70
+ <div
71
+ data-slot="empty-description"
72
+ className={cn(
73
+ "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="empty-content"
85
+ className={cn(
86
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ export {
95
+ Empty,
96
+ EmptyHeader,
97
+ EmptyTitle,
98
+ EmptyDescription,
99
+ EmptyContent,
100
+ EmptyMedia,
101
+ }