@spree/dashboard-ui 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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/package.json +86 -0
  4. package/src/hooks/use-copy-to-clipboard.ts +44 -0
  5. package/src/hooks/use-mobile.ts +19 -0
  6. package/src/hooks/use-scrolled.ts +30 -0
  7. package/src/index.ts +99 -0
  8. package/src/lib/date-locale.ts +29 -0
  9. package/src/lib/utils.ts +6 -0
  10. package/src/lib/validation-messages.ts +19 -0
  11. package/src/spree/address-block.tsx +74 -0
  12. package/src/spree/back-button.tsx +48 -0
  13. package/src/spree/bulk-dialog.tsx +71 -0
  14. package/src/spree/bulk-price-table.tsx +254 -0
  15. package/src/spree/calculator-summary.tsx +120 -0
  16. package/src/spree/color-picker.tsx +202 -0
  17. package/src/spree/confirm-dialog.tsx +82 -0
  18. package/src/spree/copy-to-clipboard-button.tsx +90 -0
  19. package/src/spree/country-flag.tsx +30 -0
  20. package/src/spree/data-grid/cells.tsx +415 -0
  21. package/src/spree/data-grid/context.ts +59 -0
  22. package/src/spree/data-grid/data-grid.tsx +214 -0
  23. package/src/spree/data-grid/fill-handle.tsx +153 -0
  24. package/src/spree/data-grid/helpers.ts +18 -0
  25. package/src/spree/data-grid/index.ts +5 -0
  26. package/src/spree/data-grid/types.ts +36 -0
  27. package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
  28. package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
  29. package/src/spree/drag-handle.tsx +51 -0
  30. package/src/spree/form-actions.tsx +140 -0
  31. package/src/spree/json-preview-drawer.tsx +254 -0
  32. package/src/spree/json-value-view.tsx +149 -0
  33. package/src/spree/language-menu-items.tsx +54 -0
  34. package/src/spree/metadata/metadata-card.tsx +62 -0
  35. package/src/spree/relative-time.tsx +49 -0
  36. package/src/spree/resource-combobox.tsx +108 -0
  37. package/src/spree/resource-layout.tsx +31 -0
  38. package/src/spree/resource-multi-autocomplete.tsx +180 -0
  39. package/src/spree/resource-name-cell.tsx +44 -0
  40. package/src/spree/route-error-boundary.tsx +102 -0
  41. package/src/spree/row-actions.tsx +85 -0
  42. package/src/spree/row-click-bridge.tsx +17 -0
  43. package/src/spree/secret-input.tsx +137 -0
  44. package/src/spree/storefront-visible-switch.tsx +58 -0
  45. package/src/spree/tag-list.tsx +26 -0
  46. package/src/spree/theme-provider.tsx +83 -0
  47. package/src/spree/theme-toggle.tsx +35 -0
  48. package/src/styles.css +338 -0
  49. package/src/ui/attachment.tsx +187 -0
  50. package/src/ui/avatar.tsx +94 -0
  51. package/src/ui/badge.tsx +161 -0
  52. package/src/ui/breadcrumb.tsx +109 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/calendar.tsx +66 -0
  55. package/src/ui/card.tsx +72 -0
  56. package/src/ui/chart.tsx +94 -0
  57. package/src/ui/checkbox.tsx +50 -0
  58. package/src/ui/collapsible.tsx +18 -0
  59. package/src/ui/combobox.tsx +270 -0
  60. package/src/ui/command.tsx +172 -0
  61. package/src/ui/data-table.tsx +60 -0
  62. package/src/ui/date-picker.tsx +253 -0
  63. package/src/ui/date-range-picker.tsx +150 -0
  64. package/src/ui/dialog.tsx +214 -0
  65. package/src/ui/dropdown-menu.tsx +345 -0
  66. package/src/ui/empty.tsx +94 -0
  67. package/src/ui/field.tsx +221 -0
  68. package/src/ui/input-group.tsx +141 -0
  69. package/src/ui/input.tsx +19 -0
  70. package/src/ui/label.tsx +29 -0
  71. package/src/ui/pagination.tsx +123 -0
  72. package/src/ui/popover.tsx +76 -0
  73. package/src/ui/progress.tsx +73 -0
  74. package/src/ui/radio-group.tsx +42 -0
  75. package/src/ui/rich-text-editor.tsx +263 -0
  76. package/src/ui/select.tsx +213 -0
  77. package/src/ui/separator.tsx +31 -0
  78. package/src/ui/sheet.tsx +208 -0
  79. package/src/ui/sidebar.tsx +679 -0
  80. package/src/ui/skeleton.tsx +13 -0
  81. package/src/ui/slot.tsx +35 -0
  82. package/src/ui/sonner.tsx +47 -0
  83. package/src/ui/switch.tsx +39 -0
  84. package/src/ui/textarea.tsx +18 -0
  85. package/src/ui/tooltip.tsx +100 -0
@@ -0,0 +1,263 @@
1
+ import Link from '@tiptap/extension-link'
2
+ import Placeholder from '@tiptap/extension-placeholder'
3
+ import { EditorContent, useEditor } from '@tiptap/react'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import {
6
+ BoldIcon,
7
+ ItalicIcon,
8
+ LinkIcon,
9
+ ListIcon,
10
+ ListOrderedIcon,
11
+ QuoteIcon,
12
+ RedoIcon,
13
+ StrikethroughIcon,
14
+ UndoIcon,
15
+ } from 'lucide-react'
16
+ import { useCallback, useEffect, useRef } from 'react'
17
+ import { useTranslation } from 'react-i18next'
18
+ import { cn } from '../lib/utils'
19
+
20
+ interface RichTextEditorProps {
21
+ value?: string
22
+ onChange?: (html: string) => void
23
+ /**
24
+ * Fires when the editor loses focus. Useful for commit-on-blur flows where
25
+ * onChange is only for live updates and persistence happens once the user
26
+ * moves away.
27
+ */
28
+ onBlur?: () => void
29
+ placeholder?: string
30
+ className?: string
31
+ disabled?: boolean
32
+ /** Accessible name for the editor (e.g. matching a sibling FieldLabel). */
33
+ ariaLabel?: string
34
+ /**
35
+ * DOM id for the editable surface so callers can associate it with a
36
+ * `<FieldLabel htmlFor={...}>` for screen readers and `getByLabel`-style
37
+ * test locators.
38
+ */
39
+ id?: string
40
+ }
41
+
42
+ export function RichTextEditor({
43
+ value = '',
44
+ onChange,
45
+ onBlur,
46
+ placeholder,
47
+ className,
48
+ disabled = false,
49
+ ariaLabel,
50
+ id,
51
+ }: RichTextEditorProps) {
52
+ const { t } = useTranslation()
53
+ const resolvedPlaceholder = placeholder ?? t('admin.components.rich_text_editor.placeholder')
54
+ const wrapperRef = useRef<HTMLDivElement | null>(null)
55
+ // tiptap's `useEditor` registers `onUpdate`/`onBlur` only at create time —
56
+ // later renders keep the first closure. Stash the latest callbacks in refs
57
+ // and call through them so commit-on-blur consumers (e.g. ApiBacked custom
58
+ // fields) see updated state after the first save: without this, the
59
+ // post-save `commit` closure runs with the pre-save definition id and
60
+ // attempts another create instead of an update.
61
+ const onChangeRef = useRef(onChange)
62
+ const onBlurRef = useRef(onBlur)
63
+ useEffect(() => {
64
+ onChangeRef.current = onChange
65
+ onBlurRef.current = onBlur
66
+ })
67
+
68
+ const editor = useEditor({
69
+ extensions: [
70
+ StarterKit.configure({ link: false }),
71
+ Placeholder.configure({ placeholder: resolvedPlaceholder }),
72
+ Link.configure({ openOnClick: false }),
73
+ ],
74
+ content: value,
75
+ editable: !disabled,
76
+ editorProps: {
77
+ attributes: {
78
+ ...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
79
+ ...(id ? { id } : {}),
80
+ },
81
+ },
82
+ onUpdate: ({ editor }) => {
83
+ onChangeRef.current?.(editor.getHTML())
84
+ },
85
+ onBlur: ({ event }) => {
86
+ // Tiptap fires `onBlur` whenever the contenteditable loses focus —
87
+ // including when the user clicks a toolbar button INSIDE this same
88
+ // component, which then re-focuses the editor. Suppress those
89
+ // intra-component blurs so commit-on-blur callers don't persist
90
+ // stale HTML on every toolbar click.
91
+ //
92
+ // We check `relatedTarget` first (synchronous, covers the common
93
+ // case). When it's null — toolbar buttons that don't take focus,
94
+ // or click sequences that don't transfer focus — defer to a
95
+ // microtask and re-check `document.activeElement`: if focus is
96
+ // still inside the wrapper, the editor will get it back in the
97
+ // next tick and the blur is intra-component.
98
+ const relatedTarget = (event as FocusEvent).relatedTarget as Node | null
99
+ if (relatedTarget) {
100
+ if (wrapperRef.current?.contains(relatedTarget)) return
101
+ onBlurRef.current?.()
102
+ return
103
+ }
104
+ queueMicrotask(() => {
105
+ const active = document.activeElement as Node | null
106
+ if (active && wrapperRef.current?.contains(active)) return
107
+ onBlurRef.current?.()
108
+ })
109
+ },
110
+ })
111
+
112
+ // Sync external value changes (e.g. form reset)
113
+ useEffect(() => {
114
+ if (!editor) return
115
+ if (editor.getHTML() !== value) {
116
+ editor.commands.setContent(value, { emitUpdate: false })
117
+ }
118
+ }, [editor, value])
119
+
120
+ useEffect(() => {
121
+ editor?.setEditable(!disabled)
122
+ }, [editor, disabled])
123
+
124
+ const setLink = useCallback(() => {
125
+ if (!editor) return
126
+ const previous = editor.getAttributes('link').href
127
+ const url = window.prompt(t('admin.components.rich_text_editor.link_url_prompt'), previous)
128
+ if (url === null) return
129
+ if (url === '') {
130
+ editor.chain().focus().extendMarkRange('link').unsetLink().run()
131
+ } else {
132
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
133
+ }
134
+ }, [editor, t])
135
+
136
+ if (!editor) return null
137
+
138
+ return (
139
+ <div
140
+ ref={wrapperRef}
141
+ className={cn(
142
+ 'rounded-lg border border-border bg-card text-foreground shadow-xs transition-all duration-100 ease-in-out focus-within:border-blue-500 focus-within:shadow-[0_0_0_3px_rgba(59,130,246,0.15)]',
143
+ disabled && 'pointer-events-none bg-muted border-border',
144
+ className,
145
+ )}
146
+ >
147
+ {/* Toolbar */}
148
+ <div className="flex items-center gap-0.5 border-b border-border px-2 py-1.5">
149
+ <ToolbarButton
150
+ active={editor.isActive('bold')}
151
+ onClick={() => editor.chain().focus().toggleBold().run()}
152
+ title={t('admin.components.rich_text_editor.bold')}
153
+ >
154
+ <BoldIcon className="size-4" />
155
+ </ToolbarButton>
156
+ <ToolbarButton
157
+ active={editor.isActive('italic')}
158
+ onClick={() => editor.chain().focus().toggleItalic().run()}
159
+ title={t('admin.components.rich_text_editor.italic')}
160
+ >
161
+ <ItalicIcon className="size-4" />
162
+ </ToolbarButton>
163
+ <ToolbarButton
164
+ active={editor.isActive('strike')}
165
+ onClick={() => editor.chain().focus().toggleStrike().run()}
166
+ title={t('admin.components.rich_text_editor.strikethrough')}
167
+ >
168
+ <StrikethroughIcon className="size-4" />
169
+ </ToolbarButton>
170
+
171
+ <ToolbarSeparator />
172
+
173
+ <ToolbarButton
174
+ active={editor.isActive('bulletList')}
175
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
176
+ title={t('admin.components.rich_text_editor.bullet_list')}
177
+ >
178
+ <ListIcon className="size-4" />
179
+ </ToolbarButton>
180
+ <ToolbarButton
181
+ active={editor.isActive('orderedList')}
182
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
183
+ title={t('admin.components.rich_text_editor.ordered_list')}
184
+ >
185
+ <ListOrderedIcon className="size-4" />
186
+ </ToolbarButton>
187
+ <ToolbarButton
188
+ active={editor.isActive('blockquote')}
189
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
190
+ title={t('admin.components.rich_text_editor.blockquote')}
191
+ >
192
+ <QuoteIcon className="size-4" />
193
+ </ToolbarButton>
194
+
195
+ <ToolbarSeparator />
196
+
197
+ <ToolbarButton
198
+ active={editor.isActive('link')}
199
+ onClick={setLink}
200
+ title={t('admin.components.rich_text_editor.link')}
201
+ >
202
+ <LinkIcon className="size-4" />
203
+ </ToolbarButton>
204
+
205
+ <div className="ml-auto flex items-center gap-0.5">
206
+ <ToolbarButton
207
+ onClick={() => editor.chain().focus().undo().run()}
208
+ disabled={!editor.can().undo()}
209
+ title={t('admin.components.rich_text_editor.undo')}
210
+ >
211
+ <UndoIcon className="size-4" />
212
+ </ToolbarButton>
213
+ <ToolbarButton
214
+ onClick={() => editor.chain().focus().redo().run()}
215
+ disabled={!editor.can().redo()}
216
+ title={t('admin.components.rich_text_editor.redo')}
217
+ >
218
+ <RedoIcon className="size-4" />
219
+ </ToolbarButton>
220
+ </div>
221
+ </div>
222
+
223
+ {/* Editor */}
224
+ <EditorContent
225
+ editor={editor}
226
+ className="prose prose-sm max-w-none px-3 py-2 dark:prose-invert [&_.tiptap]:min-h-32 [&_.tiptap]:outline-none [&_.tiptap.is-editor-empty:first-child::before]:text-muted-foreground [&_.tiptap.is-editor-empty:first-child::before]:content-[attr(data-placeholder)] [&_.tiptap.is-editor-empty:first-child::before]:float-left [&_.tiptap.is-editor-empty:first-child::before]:h-0 [&_.tiptap.is-editor-empty:first-child::before]:pointer-events-none"
227
+ />
228
+ </div>
229
+ )
230
+ }
231
+
232
+ function ToolbarButton({
233
+ active,
234
+ disabled,
235
+ onClick,
236
+ children,
237
+ title,
238
+ }: {
239
+ active?: boolean
240
+ disabled?: boolean
241
+ onClick: () => void
242
+ children: React.ReactNode
243
+ title: string
244
+ }) {
245
+ return (
246
+ <button
247
+ type="button"
248
+ onClick={onClick}
249
+ disabled={disabled}
250
+ title={title}
251
+ className={cn(
252
+ 'inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-40 disabled:pointer-events-none',
253
+ active && 'bg-accent text-foreground',
254
+ )}
255
+ >
256
+ {children}
257
+ </button>
258
+ )
259
+ }
260
+
261
+ function ToolbarSeparator() {
262
+ return <div className="mx-1 h-5 w-px bg-border" />
263
+ }
@@ -0,0 +1,213 @@
1
+ import { Select as SelectPrimitive } from '@base-ui/react/select'
2
+ import { CheckIcon, ChevronDownIcon } from 'lucide-react'
3
+ import type * as React from 'react'
4
+ import { cn } from '../lib/utils'
5
+
6
+ function Select({
7
+ children,
8
+ onValueChange,
9
+ ...props
10
+ }: Omit<SelectPrimitive.Root.Props<string, false>, 'onValueChange'> & {
11
+ onValueChange?: (value: string) => void
12
+ }) {
13
+ return (
14
+ <SelectPrimitive.Root
15
+ data-slot="select"
16
+ onValueChange={
17
+ onValueChange
18
+ ? (value: string | null) => {
19
+ if (value !== null) onValueChange(value)
20
+ }
21
+ : undefined
22
+ }
23
+ {...props}
24
+ >
25
+ {children}
26
+ </SelectPrimitive.Root>
27
+ )
28
+ }
29
+
30
+ function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
31
+ return (
32
+ <SelectPrimitive.Group
33
+ data-slot="select-group"
34
+ className={cn('scroll-my-1 p-1', className)}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
41
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
42
+ }
43
+
44
+ function SelectTrigger({
45
+ className,
46
+ size = 'default',
47
+ children,
48
+ ...props
49
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
50
+ size?: 'sm' | 'default'
51
+ }) {
52
+ return (
53
+ <SelectPrimitive.Trigger
54
+ data-slot="select-trigger"
55
+ data-size={size}
56
+ className={cn(
57
+ // `min-h-9.5` (default) and `data-[size=sm]:min-h-[1.9375rem]` lock
58
+ // the trigger to the same natural height as `<Input>` so the field
59
+ // doesn't collapse to chevron-height when the SelectValue render-prop
60
+ // returns an empty string (e.g., when value is unset and the
61
+ // placeholder isn't surfaced through the render-prop).
62
+ "flex w-full min-h-8.5 items-center justify-between gap-1.5 rounded-lg border border-border bg-card py-1.5 pr-2 pl-2.5 text-base font-normal leading-normal text-foreground shadow-xs transition-all duration-100 ease-in-out outline-none select-none focus:border-blue-500 focus:shadow-[0_0_0_3px_rgba(59,130,246,0.15)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:border-border disabled:text-muted-foreground disabled:shadow-none aria-invalid:border-destructive data-[placeholder]:text-muted-foreground data-[size=sm]:py-1 data-[size=sm]:px-2 data-[size=sm]:text-sm data-[size=sm]:min-h-[1.9375rem] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
63
+ className,
64
+ )}
65
+ {...props}
66
+ >
67
+ {children}
68
+ <SelectPrimitive.Icon>
69
+ <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
70
+ </SelectPrimitive.Icon>
71
+ </SelectPrimitive.Trigger>
72
+ )
73
+ }
74
+
75
+ function SelectContent({
76
+ className,
77
+ children,
78
+ position = 'popper',
79
+ align = 'start',
80
+ side = 'bottom',
81
+ ...props
82
+ }: {
83
+ className?: string
84
+ children?: React.ReactNode
85
+ position?: 'item-aligned' | 'popper'
86
+ align?: 'start' | 'center' | 'end'
87
+ side?: 'top' | 'bottom' | 'left' | 'right'
88
+ } & Omit<React.ComponentProps<typeof SelectPrimitive.Popup>, 'className'>) {
89
+ return (
90
+ <SelectPrimitive.Portal>
91
+ <SelectPrimitive.Positioner
92
+ side={side}
93
+ align={align}
94
+ alignItemWithTrigger={position === 'item-aligned'}
95
+ className="z-[100]"
96
+ >
97
+ <SelectPrimitive.Popup
98
+ data-slot="select-content"
99
+ data-align-trigger={position === 'item-aligned'}
100
+ className={cn(
101
+ 'relative z-[100] max-h-[var(--available-height)] min-w-[var(--anchor-width)] 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-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform]',
102
+ position === 'popper' &&
103
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
104
+ className,
105
+ )}
106
+ {...props}
107
+ >
108
+ {children}
109
+ </SelectPrimitive.Popup>
110
+ </SelectPrimitive.Positioner>
111
+ </SelectPrimitive.Portal>
112
+ )
113
+ }
114
+
115
+ function SelectLabel({
116
+ className,
117
+ ...props
118
+ }: React.ComponentProps<typeof SelectPrimitive.GroupLabel>) {
119
+ return (
120
+ <SelectPrimitive.GroupLabel
121
+ data-slot="select-label"
122
+ className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
123
+ {...props}
124
+ />
125
+ )
126
+ }
127
+
128
+ function SelectItem({
129
+ className,
130
+ children,
131
+ ...props
132
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
133
+ return (
134
+ <SelectPrimitive.Item
135
+ data-slot="select-item"
136
+ className={cn(
137
+ // Padding + font sized to match `<Input>` and `<SelectTrigger>` so
138
+ // the dropdown rows feel like a continuation of the trigger field.
139
+ // `pr-8` reserves room for the absolute-positioned check indicator.
140
+ "relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2.5 text-base font-normal leading-normal outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
141
+ className,
142
+ )}
143
+ {...props}
144
+ >
145
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
146
+ <SelectPrimitive.ItemIndicator>
147
+ <CheckIcon className="pointer-events-none" />
148
+ </SelectPrimitive.ItemIndicator>
149
+ </span>
150
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
151
+ </SelectPrimitive.Item>
152
+ )
153
+ }
154
+
155
+ function SelectSeparator({ className, ...props }: React.ComponentProps<'div'>) {
156
+ return (
157
+ <div
158
+ role="separator"
159
+ data-slot="select-separator"
160
+ className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
161
+ {...props}
162
+ />
163
+ )
164
+ }
165
+
166
+ function SelectScrollUpButton({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
170
+ return (
171
+ <SelectPrimitive.ScrollUpArrow
172
+ data-slot="select-scroll-up-button"
173
+ className={cn(
174
+ "z-10 flex cursor-default items-center justify-center bg-popover py-2 [&_svg:not([class*='size-'])]:size-4",
175
+ className,
176
+ )}
177
+ {...props}
178
+ >
179
+ <ChevronDownIcon className="rotate-180" />
180
+ </SelectPrimitive.ScrollUpArrow>
181
+ )
182
+ }
183
+
184
+ function SelectScrollDownButton({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
188
+ return (
189
+ <SelectPrimitive.ScrollDownArrow
190
+ data-slot="select-scroll-down-button"
191
+ className={cn(
192
+ "z-10 flex cursor-default items-center justify-center bg-popover py-2 [&_svg:not([class*='size-'])]:size-4",
193
+ className,
194
+ )}
195
+ {...props}
196
+ >
197
+ <ChevronDownIcon />
198
+ </SelectPrimitive.ScrollDownArrow>
199
+ )
200
+ }
201
+
202
+ export {
203
+ Select,
204
+ SelectContent,
205
+ SelectGroup,
206
+ SelectItem,
207
+ SelectLabel,
208
+ SelectScrollDownButton,
209
+ SelectScrollUpButton,
210
+ SelectSeparator,
211
+ SelectTrigger,
212
+ SelectValue,
213
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
4
+ import * as React from 'react'
5
+
6
+ import { cn } from '../lib/utils'
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = 'horizontal',
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive> & {
14
+ decorative?: boolean
15
+ }) {
16
+ return (
17
+ <SeparatorPrimitive
18
+ data-slot="separator"
19
+ aria-hidden={decorative ? true : undefined}
20
+ orientation={orientation}
21
+ data-orientation={orientation}
22
+ className={cn(
23
+ 'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ export { Separator }