@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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/package.json +86 -0
- package/src/hooks/use-copy-to-clipboard.ts +44 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scrolled.ts +30 -0
- package/src/index.ts +99 -0
- package/src/lib/date-locale.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation-messages.ts +19 -0
- package/src/spree/address-block.tsx +74 -0
- package/src/spree/back-button.tsx +48 -0
- package/src/spree/bulk-dialog.tsx +71 -0
- package/src/spree/bulk-price-table.tsx +254 -0
- package/src/spree/calculator-summary.tsx +120 -0
- package/src/spree/color-picker.tsx +202 -0
- package/src/spree/confirm-dialog.tsx +82 -0
- package/src/spree/copy-to-clipboard-button.tsx +90 -0
- package/src/spree/country-flag.tsx +30 -0
- package/src/spree/data-grid/cells.tsx +415 -0
- package/src/spree/data-grid/context.ts +59 -0
- package/src/spree/data-grid/data-grid.tsx +214 -0
- package/src/spree/data-grid/fill-handle.tsx +153 -0
- package/src/spree/data-grid/helpers.ts +18 -0
- package/src/spree/data-grid/index.ts +5 -0
- package/src/spree/data-grid/types.ts +36 -0
- package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
- package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
- package/src/spree/drag-handle.tsx +51 -0
- package/src/spree/form-actions.tsx +140 -0
- package/src/spree/json-preview-drawer.tsx +254 -0
- package/src/spree/json-value-view.tsx +149 -0
- package/src/spree/language-menu-items.tsx +54 -0
- package/src/spree/metadata/metadata-card.tsx +62 -0
- package/src/spree/relative-time.tsx +49 -0
- package/src/spree/resource-combobox.tsx +108 -0
- package/src/spree/resource-layout.tsx +31 -0
- package/src/spree/resource-multi-autocomplete.tsx +180 -0
- package/src/spree/resource-name-cell.tsx +44 -0
- package/src/spree/route-error-boundary.tsx +102 -0
- package/src/spree/row-actions.tsx +85 -0
- package/src/spree/row-click-bridge.tsx +17 -0
- package/src/spree/secret-input.tsx +137 -0
- package/src/spree/storefront-visible-switch.tsx +58 -0
- package/src/spree/tag-list.tsx +26 -0
- package/src/spree/theme-provider.tsx +83 -0
- package/src/spree/theme-toggle.tsx +35 -0
- package/src/styles.css +338 -0
- package/src/ui/attachment.tsx +187 -0
- package/src/ui/avatar.tsx +94 -0
- package/src/ui/badge.tsx +161 -0
- package/src/ui/breadcrumb.tsx +109 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/calendar.tsx +66 -0
- package/src/ui/card.tsx +72 -0
- package/src/ui/chart.tsx +94 -0
- package/src/ui/checkbox.tsx +50 -0
- package/src/ui/collapsible.tsx +18 -0
- package/src/ui/combobox.tsx +270 -0
- package/src/ui/command.tsx +172 -0
- package/src/ui/data-table.tsx +60 -0
- package/src/ui/date-picker.tsx +253 -0
- package/src/ui/date-range-picker.tsx +150 -0
- package/src/ui/dialog.tsx +214 -0
- package/src/ui/dropdown-menu.tsx +345 -0
- package/src/ui/empty.tsx +94 -0
- package/src/ui/field.tsx +221 -0
- package/src/ui/input-group.tsx +141 -0
- package/src/ui/input.tsx +19 -0
- package/src/ui/label.tsx +29 -0
- package/src/ui/pagination.tsx +123 -0
- package/src/ui/popover.tsx +76 -0
- package/src/ui/progress.tsx +73 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/rich-text-editor.tsx +263 -0
- package/src/ui/select.tsx +213 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/sheet.tsx +208 -0
- package/src/ui/sidebar.tsx +679 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/slot.tsx +35 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/switch.tsx +39 -0
- package/src/ui/textarea.tsx +18 -0
- 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 }
|