@bupple/vss-ui 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 (65) hide show
  1. package/.turbo/turbo-lint.log +4 -0
  2. package/components.json +21 -0
  3. package/eslint.config.js +4 -0
  4. package/index.ts +2 -0
  5. package/package.json +67 -0
  6. package/postcss.config.mjs +6 -0
  7. package/src/components/accordion.tsx +84 -0
  8. package/src/components/alert-dialog.tsx +198 -0
  9. package/src/components/alert.tsx +77 -0
  10. package/src/components/aspect-ratio.tsx +11 -0
  11. package/src/components/avatar.tsx +109 -0
  12. package/src/components/badge.tsx +50 -0
  13. package/src/components/breadcrumb.tsx +118 -0
  14. package/src/components/button-group.tsx +84 -0
  15. package/src/components/button.tsx +68 -0
  16. package/src/components/calendar.tsx +223 -0
  17. package/src/components/card.tsx +102 -0
  18. package/src/components/carousel.tsx +241 -0
  19. package/src/components/chart.tsx +373 -0
  20. package/src/components/checkbox.tsx +32 -0
  21. package/src/components/collapsible.tsx +33 -0
  22. package/src/components/combobox.tsx +299 -0
  23. package/src/components/command.tsx +194 -0
  24. package/src/components/context-menu.tsx +272 -0
  25. package/src/components/dialog.tsx +171 -0
  26. package/src/components/direction.tsx +20 -0
  27. package/src/components/drawer.tsx +130 -0
  28. package/src/components/dropdown-menu.tsx +278 -0
  29. package/src/components/empty.tsx +102 -0
  30. package/src/components/field.tsx +237 -0
  31. package/src/components/hover-card.tsx +43 -0
  32. package/src/components/input-group.tsx +157 -0
  33. package/src/components/input.tsx +18 -0
  34. package/src/components/item.tsx +197 -0
  35. package/src/components/kbd.tsx +26 -0
  36. package/src/components/label.tsx +21 -0
  37. package/src/components/menubar.tsx +283 -0
  38. package/src/components/native-select.tsx +64 -0
  39. package/src/components/navigation-menu.tsx +166 -0
  40. package/src/components/pagination.tsx +131 -0
  41. package/src/components/popover.tsx +88 -0
  42. package/src/components/progress.tsx +30 -0
  43. package/src/components/radio-group.tsx +46 -0
  44. package/src/components/resizable.tsx +49 -0
  45. package/src/components/scroll-area.tsx +52 -0
  46. package/src/components/select.tsx +209 -0
  47. package/src/components/separator.tsx +25 -0
  48. package/src/components/sheet.tsx +152 -0
  49. package/src/components/sidebar.tsx +703 -0
  50. package/src/components/skeleton.tsx +13 -0
  51. package/src/components/slider.tsx +58 -0
  52. package/src/components/sonner.tsx +45 -0
  53. package/src/components/spinner.tsx +15 -0
  54. package/src/components/switch.tsx +32 -0
  55. package/src/components/table.tsx +115 -0
  56. package/src/components/tabs.tsx +89 -0
  57. package/src/components/textarea.tsx +17 -0
  58. package/src/components/toggle-group.tsx +86 -0
  59. package/src/components/toggle.tsx +48 -0
  60. package/src/components/tooltip.tsx +56 -0
  61. package/src/hooks/use-mobile.ts +19 -0
  62. package/src/lib/portal-container.ts +11 -0
  63. package/src/lib/utils.ts +8 -0
  64. package/src/theme.css +125 -0
  65. package/tsconfig.json +15 -0
@@ -0,0 +1,278 @@
1
+ import { usePortalContainer } from '@bupple/vss-ui/lib/portal-container'
2
+ import { cn } from '@bupple/vss-ui/lib/utils'
3
+ import { CheckIcon, ChevronRightIcon } from 'lucide-react'
4
+ import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
5
+ import * as React from 'react'
6
+
7
+ function DropdownMenu({
8
+ ...props
9
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
10
+ return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />
11
+ }
12
+
13
+ function DropdownMenuPortal({
14
+ ...props
15
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
16
+ return (
17
+ <DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
18
+ )
19
+ }
20
+
21
+ function DropdownMenuTrigger({
22
+ ...props
23
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
24
+ return (
25
+ <DropdownMenuPrimitive.Trigger
26
+ data-slot='dropdown-menu-trigger'
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function DropdownMenuContent({
33
+ className,
34
+ align = 'start',
35
+ sideOffset = 4,
36
+ ...props
37
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
38
+ const container = usePortalContainer()
39
+ return (
40
+ <DropdownMenuPrimitive.Portal container={container ?? undefined}>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot='dropdown-menu-content'
43
+ sideOffset={sideOffset}
44
+ align={align}
45
+ className={cn(
46
+ 'z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 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-[state=closed]:overflow-hidden 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',
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ </DropdownMenuPrimitive.Portal>
52
+ )
53
+ }
54
+
55
+ function DropdownMenuGroup({
56
+ ...props
57
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
58
+ return (
59
+ <DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
60
+ )
61
+ }
62
+
63
+ function DropdownMenuItem({
64
+ className,
65
+ inset,
66
+ variant = 'default',
67
+ ...props
68
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
69
+ inset?: boolean
70
+ variant?: 'default' | 'destructive'
71
+ }) {
72
+ return (
73
+ <DropdownMenuPrimitive.Item
74
+ data-slot='dropdown-menu-item'
75
+ data-inset={inset}
76
+ data-variant={variant}
77
+ className={cn(
78
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm 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",
79
+ className,
80
+ )}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function DropdownMenuCheckboxItem({
87
+ className,
88
+ children,
89
+ checked,
90
+ inset,
91
+ checkIcon,
92
+ ...props
93
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
94
+ inset?: boolean
95
+ checkIcon?: React.ReactNode
96
+ }) {
97
+ return (
98
+ <DropdownMenuPrimitive.CheckboxItem
99
+ data-slot='dropdown-menu-checkbox-item'
100
+ data-inset={inset}
101
+ className={cn(
102
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm 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",
103
+ className,
104
+ )}
105
+ checked={checked}
106
+ {...props}
107
+ >
108
+ <span
109
+ className='pointer-events-none absolute right-2 flex items-center justify-center'
110
+ data-slot='dropdown-menu-checkbox-item-indicator'
111
+ >
112
+ <DropdownMenuPrimitive.ItemIndicator>
113
+ {checkIcon ?? <CheckIcon />}
114
+ </DropdownMenuPrimitive.ItemIndicator>
115
+ </span>
116
+ {children}
117
+ </DropdownMenuPrimitive.CheckboxItem>
118
+ )
119
+ }
120
+
121
+ function DropdownMenuRadioGroup({
122
+ ...props
123
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
124
+ return (
125
+ <DropdownMenuPrimitive.RadioGroup
126
+ data-slot='dropdown-menu-radio-group'
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ function DropdownMenuRadioItem({
133
+ className,
134
+ children,
135
+ inset,
136
+ radioIcon,
137
+ ...props
138
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
139
+ inset?: boolean
140
+ radioIcon?: React.ReactNode
141
+ }) {
142
+ return (
143
+ <DropdownMenuPrimitive.RadioItem
144
+ data-slot='dropdown-menu-radio-item'
145
+ data-inset={inset}
146
+ className={cn(
147
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm 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",
148
+ className,
149
+ )}
150
+ {...props}
151
+ >
152
+ <span
153
+ className='pointer-events-none absolute right-2 flex items-center justify-center'
154
+ data-slot='dropdown-menu-radio-item-indicator'
155
+ >
156
+ <DropdownMenuPrimitive.ItemIndicator>
157
+ {radioIcon ?? <CheckIcon />}
158
+ </DropdownMenuPrimitive.ItemIndicator>
159
+ </span>
160
+ {children}
161
+ </DropdownMenuPrimitive.RadioItem>
162
+ )
163
+ }
164
+
165
+ function DropdownMenuLabel({
166
+ className,
167
+ inset,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
170
+ inset?: boolean
171
+ }) {
172
+ return (
173
+ <DropdownMenuPrimitive.Label
174
+ data-slot='dropdown-menu-label'
175
+ data-inset={inset}
176
+ className={cn(
177
+ 'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7',
178
+ className,
179
+ )}
180
+ {...props}
181
+ />
182
+ )
183
+ }
184
+
185
+ function DropdownMenuSeparator({
186
+ className,
187
+ ...props
188
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
189
+ return (
190
+ <DropdownMenuPrimitive.Separator
191
+ data-slot='dropdown-menu-separator'
192
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
193
+ {...props}
194
+ />
195
+ )
196
+ }
197
+
198
+ function DropdownMenuShortcut({
199
+ className,
200
+ ...props
201
+ }: React.ComponentProps<'span'>) {
202
+ return (
203
+ <span
204
+ data-slot='dropdown-menu-shortcut'
205
+ className={cn(
206
+ 'ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground',
207
+ className,
208
+ )}
209
+ {...props}
210
+ />
211
+ )
212
+ }
213
+
214
+ function DropdownMenuSub({
215
+ ...props
216
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
217
+ return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />
218
+ }
219
+
220
+ function DropdownMenuSubTrigger({
221
+ className,
222
+ inset,
223
+ children,
224
+ subTriggerIcon,
225
+ ...props
226
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
227
+ inset?: boolean
228
+ subTriggerIcon?: React.ReactNode
229
+ }) {
230
+ return (
231
+ <DropdownMenuPrimitive.SubTrigger
232
+ data-slot='dropdown-menu-sub-trigger'
233
+ data-inset={inset}
234
+ className={cn(
235
+ "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
236
+ className,
237
+ )}
238
+ {...props}
239
+ >
240
+ {children}
241
+ {subTriggerIcon ?? <ChevronRightIcon className='ml-auto' />}
242
+ </DropdownMenuPrimitive.SubTrigger>
243
+ )
244
+ }
245
+
246
+ function DropdownMenuSubContent({
247
+ className,
248
+ ...props
249
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
250
+ return (
251
+ <DropdownMenuPrimitive.SubContent
252
+ data-slot='dropdown-menu-sub-content'
253
+ className={cn(
254
+ 'z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 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',
255
+ className,
256
+ )}
257
+ {...props}
258
+ />
259
+ )
260
+ }
261
+
262
+ export {
263
+ DropdownMenu,
264
+ DropdownMenuPortal,
265
+ DropdownMenuTrigger,
266
+ DropdownMenuContent,
267
+ DropdownMenuGroup,
268
+ DropdownMenuLabel,
269
+ DropdownMenuItem,
270
+ DropdownMenuCheckboxItem,
271
+ DropdownMenuRadioGroup,
272
+ DropdownMenuRadioItem,
273
+ DropdownMenuSeparator,
274
+ DropdownMenuShortcut,
275
+ DropdownMenuSub,
276
+ DropdownMenuSubTrigger,
277
+ DropdownMenuSubContent,
278
+ }
@@ -0,0 +1,102 @@
1
+ import type { VariantProps } from 'class-variance-authority'
2
+
3
+ import { cn } from '@bupple/vss-ui/lib/utils'
4
+ import { cva } from 'class-variance-authority'
5
+
6
+ function Empty({ className, ...props }: React.ComponentProps<'div'>) {
7
+ return (
8
+ <div
9
+ data-slot='empty'
10
+ className={cn(
11
+ 'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
20
+ return (
21
+ <div
22
+ data-slot='empty-header'
23
+ className={cn('flex max-w-sm flex-col items-center gap-2', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ const emptyMediaVariants = cva(
30
+ 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
31
+ {
32
+ variants: {
33
+ variant: {
34
+ default: 'bg-transparent',
35
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: 'default',
40
+ },
41
+ },
42
+ )
43
+
44
+ function EmptyMedia({
45
+ className,
46
+ variant = 'default',
47
+ ...props
48
+ }: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
49
+ return (
50
+ <div
51
+ data-slot='empty-icon'
52
+ data-variant={variant}
53
+ className={cn(emptyMediaVariants({ variant, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
60
+ return (
61
+ <div
62
+ data-slot='empty-title'
63
+ className={cn('text-sm font-medium tracking-tight', className)}
64
+ {...props}
65
+ />
66
+ )
67
+ }
68
+
69
+ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
70
+ return (
71
+ <div
72
+ data-slot='empty-description'
73
+ className={cn(
74
+ 'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
75
+ className,
76
+ )}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
83
+ return (
84
+ <div
85
+ data-slot='empty-content'
86
+ className={cn(
87
+ 'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ export {
96
+ Empty,
97
+ EmptyHeader,
98
+ EmptyTitle,
99
+ EmptyDescription,
100
+ EmptyContent,
101
+ EmptyMedia,
102
+ }
@@ -0,0 +1,237 @@
1
+ import type { VariantProps } from 'class-variance-authority'
2
+
3
+ import { Label } from '@bupple/vss-ui/components/label'
4
+ import { Separator } from '@bupple/vss-ui/components/separator'
5
+ import { cn } from '@bupple/vss-ui/lib/utils'
6
+ import { cva } from 'class-variance-authority'
7
+ import { useMemo } from 'react'
8
+
9
+ function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
10
+ return (
11
+ <fieldset
12
+ data-slot='field-set'
13
+ className={cn(
14
+ 'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function FieldLegend({
23
+ className,
24
+ variant = 'legend',
25
+ ...props
26
+ }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
27
+ return (
28
+ <legend
29
+ data-slot='field-legend'
30
+ data-variant={variant}
31
+ className={cn(
32
+ 'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
41
+ return (
42
+ <div
43
+ data-slot='field-group'
44
+ className={cn(
45
+ 'group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ const fieldVariants = cva(
54
+ 'group/field flex w-full gap-2 data-[invalid=true]:text-destructive',
55
+ {
56
+ variants: {
57
+ orientation: {
58
+ vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
59
+ horizontal:
60
+ 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
61
+ responsive:
62
+ 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
63
+ },
64
+ },
65
+ defaultVariants: {
66
+ orientation: 'vertical',
67
+ },
68
+ },
69
+ )
70
+
71
+ function Field({
72
+ className,
73
+ orientation = 'vertical',
74
+ ...props
75
+ }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
76
+ return (
77
+ <div
78
+ role='group'
79
+ data-slot='field'
80
+ data-orientation={orientation}
81
+ className={cn(fieldVariants({ orientation }), className)}
82
+ {...props}
83
+ />
84
+ )
85
+ }
86
+
87
+ function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
88
+ return (
89
+ <div
90
+ data-slot='field-content'
91
+ className={cn(
92
+ 'group/field-content flex flex-1 flex-col gap-0.5 leading-snug',
93
+ className,
94
+ )}
95
+ {...props}
96
+ />
97
+ )
98
+ }
99
+
100
+ function FieldLabel({
101
+ className,
102
+ ...props
103
+ }: React.ComponentProps<typeof Label>) {
104
+ return (
105
+ <Label
106
+ data-slot='field-label'
107
+ className={cn(
108
+ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10',
109
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
110
+ className,
111
+ )}
112
+ {...props}
113
+ />
114
+ )
115
+ }
116
+
117
+ function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
118
+ return (
119
+ <div
120
+ data-slot='field-label'
121
+ className={cn(
122
+ 'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
123
+ className,
124
+ )}
125
+ {...props}
126
+ />
127
+ )
128
+ }
129
+
130
+ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
131
+ return (
132
+ <p
133
+ data-slot='field-description'
134
+ className={cn(
135
+ 'text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
136
+ 'last:mt-0 nth-last-2:-mt-1',
137
+ '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
138
+ className,
139
+ )}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ function FieldSeparator({
146
+ children,
147
+ className,
148
+ ...props
149
+ }: React.ComponentProps<'div'> & {
150
+ children?: React.ReactNode
151
+ }) {
152
+ return (
153
+ <div
154
+ data-slot='field-separator'
155
+ data-content={!!children}
156
+ className={cn(
157
+ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
158
+ className,
159
+ )}
160
+ {...props}
161
+ >
162
+ <Separator className='absolute inset-0 top-1/2' />
163
+ {children && (
164
+ <span
165
+ className='relative mx-auto block w-fit bg-background px-2 text-muted-foreground'
166
+ data-slot='field-separator-content'
167
+ >
168
+ {children}
169
+ </span>
170
+ )}
171
+ </div>
172
+ )
173
+ }
174
+
175
+ function FieldError({
176
+ className,
177
+ children,
178
+ errors,
179
+ ...props
180
+ }: React.ComponentProps<'div'> & {
181
+ errors?: Array<{ message?: string } | undefined>
182
+ }) {
183
+ const content = useMemo(() => {
184
+ if (children) {
185
+ return children
186
+ }
187
+
188
+ if (!errors?.length) {
189
+ return null
190
+ }
191
+
192
+ const uniqueErrors = [
193
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
194
+ ]
195
+
196
+ if (uniqueErrors?.length == 1) {
197
+ return uniqueErrors[0]?.message
198
+ }
199
+
200
+ return (
201
+ <ul className='ml-4 flex list-disc flex-col gap-1'>
202
+ {uniqueErrors.map(
203
+ (error, index) =>
204
+ error?.message && <li key={index}>{error.message}</li>,
205
+ )}
206
+ </ul>
207
+ )
208
+ }, [children, errors])
209
+
210
+ if (!content) {
211
+ return null
212
+ }
213
+
214
+ return (
215
+ <div
216
+ role='alert'
217
+ data-slot='field-error'
218
+ className={cn('text-sm font-normal text-destructive', className)}
219
+ {...props}
220
+ >
221
+ {content}
222
+ </div>
223
+ )
224
+ }
225
+
226
+ export {
227
+ Field,
228
+ FieldLabel,
229
+ FieldDescription,
230
+ FieldError,
231
+ FieldGroup,
232
+ FieldLegend,
233
+ FieldSeparator,
234
+ FieldSet,
235
+ FieldContent,
236
+ FieldTitle,
237
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@bupple/vss-ui/lib/utils'
4
+ import { HoverCard as HoverCardPrimitive } from 'radix-ui'
5
+ import * as React from 'react'
6
+
7
+ function HoverCard({
8
+ ...props
9
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
10
+ return <HoverCardPrimitive.Root data-slot='hover-card' {...props} />
11
+ }
12
+
13
+ function HoverCardTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
16
+ return (
17
+ <HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
18
+ )
19
+ }
20
+
21
+ function HoverCardContent({
22
+ className,
23
+ align = 'center',
24
+ sideOffset = 4,
25
+ ...props
26
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
27
+ return (
28
+ <HoverCardPrimitive.Portal data-slot='hover-card-portal'>
29
+ <HoverCardPrimitive.Content
30
+ data-slot='hover-card-content'
31
+ align={align}
32
+ sideOffset={sideOffset}
33
+ className={cn(
34
+ 'z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden 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',
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ </HoverCardPrimitive.Portal>
40
+ )
41
+ }
42
+
43
+ export { HoverCard, HoverCardTrigger, HoverCardContent }