@strategicos/ui 0.6.1 → 0.8.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/README.md +36 -2
- package/THIRD_PARTY_NOTICES.md +187 -0
- package/dist/components/breadcrumb.d.ts +30 -0
- package/dist/components/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb.js +43 -0
- package/dist/components/breadcrumb.js.map +1 -0
- package/dist/components/calendar.d.ts +24 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +81 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/combobox.d.ts +73 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.js +193 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/date-picker.d.ts +78 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.js +142 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/multi-select.d.ts.map +1 -1
- package/dist/components/multi-select.js +2 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/select.d.ts +16 -1
- package/dist/components/select.d.ts.map +1 -1
- package/dist/components/select.js +24 -2
- package/dist/components/select.js.map +1 -1
- package/package.json +2 -1
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/calendar.tsx +139 -0
- package/src/components/combobox.tsx +390 -0
- package/src/components/date-picker.tsx +414 -0
- package/src/components/multi-select.tsx +3 -1
- package/src/components/select.tsx +29 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, Loader2Icon, PlusIcon, SearchIcon, XIcon } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils.js"
|
|
7
|
+
import { Button } from "./button.js"
|
|
8
|
+
import { useFieldControl } from "./field.js"
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js"
|
|
10
|
+
import { selectTriggerVariants } from "./select.js"
|
|
11
|
+
|
|
12
|
+
export type ComboboxOption = {
|
|
13
|
+
value: string
|
|
14
|
+
label: string
|
|
15
|
+
/** Texto secundário (ex.: e-mail), só na lista. */
|
|
16
|
+
description?: string
|
|
17
|
+
/** Agrupa as opções sob um título. */
|
|
18
|
+
group?: string
|
|
19
|
+
/** Bolinha de cor antes do rótulo. */
|
|
20
|
+
color?: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Entry = { kind: "option"; option: ComboboxOption } | { kind: "create" }
|
|
25
|
+
|
|
26
|
+
function errorMessage(err: unknown, fallback: string) {
|
|
27
|
+
return err instanceof Error && err.message ? err.message : fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Seleção de um item com busca, como o Select `searchable` do Mantine. Use no
|
|
32
|
+
* lugar do Select quando a lista é longa ou quando a tela precisa criar ou
|
|
33
|
+
* excluir opções ali mesmo.
|
|
34
|
+
*
|
|
35
|
+
* <Combobox
|
|
36
|
+
* options={setores.map(s => ({ value: s.id, label: s.name }))}
|
|
37
|
+
* value={setorId}
|
|
38
|
+
* onValueChange={setSetorId}
|
|
39
|
+
* clearable
|
|
40
|
+
* onCreate={async nome => (await criarSetor(nome)).id}
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* - `clearable`: um X no gatilho volta o valor para `null`.
|
|
44
|
+
* - `onCreate`: com texto na busca que não bate com nenhuma opção, aparece
|
|
45
|
+
* "Criar “texto”". Se a função devolver um valor, ele fica selecionado.
|
|
46
|
+
* - `onRemove`: cada opção ganha um X que pede confirmação na própria linha.
|
|
47
|
+
* Se a opção excluída era a selecionada, o valor volta para `null`.
|
|
48
|
+
*
|
|
49
|
+
* Erros de `onCreate`/`onRemove` (promessa rejeitada) aparecem no rodapé da
|
|
50
|
+
* lista, com a mensagem do erro.
|
|
51
|
+
*
|
|
52
|
+
* Teclado: digite para filtrar, ↑/↓ para andar, Enter para escolher, Delete
|
|
53
|
+
* (com a busca vazia) para excluir a opção ativa, Esc para fechar. Dentro de
|
|
54
|
+
* um <Field>, o gatilho recebe o id e os aria-*.
|
|
55
|
+
*/
|
|
56
|
+
function Combobox({
|
|
57
|
+
options,
|
|
58
|
+
value,
|
|
59
|
+
onValueChange,
|
|
60
|
+
placeholder = "Selecione",
|
|
61
|
+
searchPlaceholder = "Buscar…",
|
|
62
|
+
emptyText = "Nada encontrado.",
|
|
63
|
+
searchable = true,
|
|
64
|
+
clearable = false,
|
|
65
|
+
onCreate,
|
|
66
|
+
createLabel = q => <>Criar “{q}”</>,
|
|
67
|
+
onRemove,
|
|
68
|
+
removeLabel = o => `Excluir ${o.label}`,
|
|
69
|
+
removeConfirmLabel = o => <>Excluir “{o.label}”?</>,
|
|
70
|
+
size = "default",
|
|
71
|
+
disabled = false,
|
|
72
|
+
className,
|
|
73
|
+
wrapperClassName,
|
|
74
|
+
contentClassName,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
...props
|
|
77
|
+
}: {
|
|
78
|
+
options: ComboboxOption[]
|
|
79
|
+
value: string | null
|
|
80
|
+
onValueChange: (value: string | null) => void
|
|
81
|
+
placeholder?: string
|
|
82
|
+
searchPlaceholder?: string
|
|
83
|
+
emptyText?: string
|
|
84
|
+
searchable?: boolean
|
|
85
|
+
clearable?: boolean
|
|
86
|
+
/** Cria uma opção com o texto da busca. Devolva o valor dela para
|
|
87
|
+
* selecioná-la. */
|
|
88
|
+
onCreate?: (label: string) => string | void | Promise<string | void>
|
|
89
|
+
createLabel?: (query: string) => React.ReactNode
|
|
90
|
+
/** Exclui a opção (o X na linha, depois de confirmar). */
|
|
91
|
+
onRemove?: (option: ComboboxOption) => void | Promise<void>
|
|
92
|
+
/** Rótulo acessível do X de cada opção. */
|
|
93
|
+
removeLabel?: (option: ComboboxOption) => string
|
|
94
|
+
/** Pergunta mostrada na linha ao confirmar a exclusão. */
|
|
95
|
+
removeConfirmLabel?: (option: ComboboxOption) => React.ReactNode
|
|
96
|
+
size?: "sm" | "default"
|
|
97
|
+
disabled?: boolean
|
|
98
|
+
/** Classes do gatilho. */
|
|
99
|
+
className?: string
|
|
100
|
+
/** Classes do invólucro, que só existe com `clearable`. Use para largura e
|
|
101
|
+
* posição no layout. */
|
|
102
|
+
wrapperClassName?: string
|
|
103
|
+
contentClassName?: string
|
|
104
|
+
onOpenChange?: (open: boolean) => void
|
|
105
|
+
id?: string
|
|
106
|
+
"aria-label"?: string
|
|
107
|
+
"aria-describedby"?: string
|
|
108
|
+
"aria-invalid"?: React.AriaAttributes["aria-invalid"]
|
|
109
|
+
}) {
|
|
110
|
+
const controlProps = useFieldControl(props, { withRequired: false })
|
|
111
|
+
const [open, setOpen] = React.useState(false)
|
|
112
|
+
const [query, setQuery] = React.useState("")
|
|
113
|
+
const [active, setActive] = React.useState(0)
|
|
114
|
+
const [creating, setCreating] = React.useState(false)
|
|
115
|
+
const [confirming, setConfirming] = React.useState<string | null>(null)
|
|
116
|
+
const [removing, setRemoving] = React.useState<string | null>(null)
|
|
117
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
118
|
+
const listId = React.useId()
|
|
119
|
+
const searchRef = React.useRef<HTMLInputElement>(null)
|
|
120
|
+
|
|
121
|
+
const selected = options.find(o => o.value === value)
|
|
122
|
+
const q = query.trim().toLowerCase()
|
|
123
|
+
const filtered = q ? options.filter(o => o.label.toLowerCase().includes(q)) : options
|
|
124
|
+
const canCreate = !!onCreate && !!q && !options.some(o => o.label.trim().toLowerCase() === q)
|
|
125
|
+
|
|
126
|
+
// Ordem do teclado: as opções habilitadas e, por último, o "Criar".
|
|
127
|
+
const entries: Entry[] = filtered.filter(o => !o.disabled).map(option => ({ kind: "option" as const, option }))
|
|
128
|
+
if (canCreate) entries.push({ kind: "create" })
|
|
129
|
+
const activeIndex = Math.min(active, entries.length - 1)
|
|
130
|
+
const entryId = (i: number) => `${listId}-${i}`
|
|
131
|
+
|
|
132
|
+
// Grupos na ordem em que aparecem nas opções.
|
|
133
|
+
const groups: { name: string | undefined; items: ComboboxOption[] }[] = []
|
|
134
|
+
for (const o of filtered) {
|
|
135
|
+
const g = groups.find(x => x.name === o.group)
|
|
136
|
+
if (g) g.items.push(o)
|
|
137
|
+
else groups.push({ name: o.group, items: [o] })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Mantém a opção ativa à vista ao andar com as setas.
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (!open || activeIndex < 0) return
|
|
143
|
+
document.getElementById(entryId(activeIndex))?.scrollIntoView({ block: "nearest" })
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, [open, activeIndex])
|
|
146
|
+
|
|
147
|
+
function handleOpenChange(o: boolean) {
|
|
148
|
+
setOpen(o)
|
|
149
|
+
if (!o) {
|
|
150
|
+
setQuery("")
|
|
151
|
+
setActive(0)
|
|
152
|
+
setConfirming(null)
|
|
153
|
+
setError(null)
|
|
154
|
+
} else {
|
|
155
|
+
// Abre com a opção selecionada ativa.
|
|
156
|
+
const i = entries.findIndex(e => e.kind === "option" && e.option.value === value)
|
|
157
|
+
setActive(Math.max(i, 0))
|
|
158
|
+
}
|
|
159
|
+
onOpenChange?.(o)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function choose(v: string) {
|
|
163
|
+
onValueChange(v)
|
|
164
|
+
handleOpenChange(false)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function create() {
|
|
168
|
+
if (!onCreate || creating) return
|
|
169
|
+
setCreating(true)
|
|
170
|
+
setError(null)
|
|
171
|
+
try {
|
|
172
|
+
const created = await onCreate(query.trim())
|
|
173
|
+
if (typeof created === "string") onValueChange(created)
|
|
174
|
+
handleOpenChange(false)
|
|
175
|
+
} catch (err) {
|
|
176
|
+
setError(errorMessage(err, "Não foi possível criar."))
|
|
177
|
+
} finally {
|
|
178
|
+
setCreating(false)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function remove(option: ComboboxOption) {
|
|
183
|
+
if (!onRemove) return
|
|
184
|
+
setRemoving(option.value)
|
|
185
|
+
setError(null)
|
|
186
|
+
try {
|
|
187
|
+
await onRemove(option)
|
|
188
|
+
if (option.value === value) onValueChange(null)
|
|
189
|
+
} catch (err) {
|
|
190
|
+
setError(errorMessage(err, "Não foi possível excluir."))
|
|
191
|
+
} finally {
|
|
192
|
+
setRemoving(null)
|
|
193
|
+
setConfirming(null)
|
|
194
|
+
searchRef.current?.focus()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cancelConfirm() {
|
|
199
|
+
setConfirming(null)
|
|
200
|
+
searchRef.current?.focus()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function activate(entry: Entry | undefined) {
|
|
204
|
+
if (!entry) return
|
|
205
|
+
if (entry.kind === "create") void create()
|
|
206
|
+
else choose(entry.option.value)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function onKeyDown(e: React.KeyboardEvent) {
|
|
210
|
+
// Na confirmação, o teclado é dos botões Excluir/Cancelar.
|
|
211
|
+
if (confirming) return
|
|
212
|
+
if (e.key === "ArrowDown") { e.preventDefault(); setActive(Math.min(activeIndex + 1, entries.length - 1)) }
|
|
213
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); setActive(Math.max(activeIndex - 1, 0)) }
|
|
214
|
+
else if (e.key === "Enter") { e.preventDefault(); activate(entries[activeIndex]) }
|
|
215
|
+
else if (e.key === "Delete" && onRemove && !query) {
|
|
216
|
+
const entry = entries[activeIndex]
|
|
217
|
+
if (entry?.kind === "option") { e.preventDefault(); setConfirming(entry.option.value) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const showClear = clearable && !!selected && !disabled
|
|
222
|
+
|
|
223
|
+
const trigger = (
|
|
224
|
+
<PopoverTrigger asChild disabled={disabled}>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
role="combobox"
|
|
228
|
+
aria-expanded={open}
|
|
229
|
+
aria-controls={listId}
|
|
230
|
+
aria-haspopup="listbox"
|
|
231
|
+
data-slot="combobox-trigger"
|
|
232
|
+
data-size={size}
|
|
233
|
+
data-placeholder={selected ? undefined : ""}
|
|
234
|
+
{...controlProps}
|
|
235
|
+
className={cn(
|
|
236
|
+
selectTriggerVariants({ size }),
|
|
237
|
+
"w-full min-w-0 cursor-pointer text-left",
|
|
238
|
+
showClear && "pr-9",
|
|
239
|
+
className
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
243
|
+
{selected?.color && <span className="size-2 shrink-0 rounded-full" style={{ background: selected.color }} />}
|
|
244
|
+
<span className="truncate">{selected?.label ?? placeholder}</span>
|
|
245
|
+
</span>
|
|
246
|
+
{!showClear && <ChevronDownIcon className="size-4 shrink-0 opacity-70" />}
|
|
247
|
+
</button>
|
|
248
|
+
</PopoverTrigger>
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
253
|
+
{clearable ? (
|
|
254
|
+
<div data-slot="combobox-wrapper" className={cn("relative w-full min-w-0", wrapperClassName)}>
|
|
255
|
+
{trigger}
|
|
256
|
+
{showClear && (
|
|
257
|
+
// Irmão do gatilho, não filho: botão dentro de botão não é HTML válido.
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={() => onValueChange(null)}
|
|
261
|
+
aria-label="Limpar"
|
|
262
|
+
className="absolute inset-y-0 right-1.5 my-auto flex size-6 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
263
|
+
>
|
|
264
|
+
<XIcon className="size-4" />
|
|
265
|
+
</button>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
) : trigger}
|
|
269
|
+
|
|
270
|
+
<PopoverContent
|
|
271
|
+
className={cn("w-(--radix-popover-trigger-width) min-w-60 p-0", contentClassName)}
|
|
272
|
+
onKeyDown={onKeyDown}
|
|
273
|
+
onEscapeKeyDown={e => { if (confirming) { e.preventDefault(); cancelConfirm() } }}
|
|
274
|
+
>
|
|
275
|
+
{searchable && (
|
|
276
|
+
<div className="flex items-center gap-2 border-b border-border px-3">
|
|
277
|
+
<SearchIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
278
|
+
<input
|
|
279
|
+
ref={searchRef}
|
|
280
|
+
autoFocus
|
|
281
|
+
value={query}
|
|
282
|
+
onChange={e => { setQuery(e.target.value); setActive(0); setError(null) }}
|
|
283
|
+
placeholder={searchPlaceholder}
|
|
284
|
+
aria-label={searchPlaceholder}
|
|
285
|
+
aria-controls={listId}
|
|
286
|
+
aria-activedescendant={activeIndex >= 0 && !confirming ? entryId(activeIndex) : undefined}
|
|
287
|
+
aria-autocomplete="list"
|
|
288
|
+
className="h-9 w-full bg-transparent text-base outline-none placeholder:text-muted-foreground md:text-sm"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
<div id={listId} role="listbox" className="max-h-64 overflow-y-auto overscroll-contain p-1">
|
|
294
|
+
{filtered.length === 0 && !canCreate && (
|
|
295
|
+
<p className="px-2.5 py-6 text-center text-sm text-muted-foreground">{emptyText}</p>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{groups.map(g => (
|
|
299
|
+
<div key={g.name ?? "_"} role="group" aria-label={g.name}>
|
|
300
|
+
{g.name && <p className="px-2.5 pt-2 pb-1 text-xs font-medium text-muted-foreground">{g.name}</p>}
|
|
301
|
+
{g.items.map(o => {
|
|
302
|
+
const index = entries.findIndex(e => e.kind === "option" && e.option.value === o.value)
|
|
303
|
+
const isSelected = o.value === value
|
|
304
|
+
const isActive = index === activeIndex
|
|
305
|
+
|
|
306
|
+
if (confirming === o.value) return (
|
|
307
|
+
<div key={o.value} role="alert" className="flex items-center gap-1.5 rounded-md bg-destructive/10 py-1 pr-1 pl-2.5 text-sm">
|
|
308
|
+
<span className="min-w-0 flex-1 truncate">{removeConfirmLabel(o)}</span>
|
|
309
|
+
<Button
|
|
310
|
+
type="button"
|
|
311
|
+
size="sm"
|
|
312
|
+
variant="destructive"
|
|
313
|
+
autoFocus
|
|
314
|
+
disabled={removing === o.value}
|
|
315
|
+
onClick={() => remove(o)}
|
|
316
|
+
>
|
|
317
|
+
{removing === o.value ? <Loader2Icon className="animate-spin" /> : "Excluir"}
|
|
318
|
+
</Button>
|
|
319
|
+
<Button type="button" size="sm" variant="ghost" disabled={removing === o.value} onClick={cancelConfirm}>
|
|
320
|
+
Cancelar
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div
|
|
327
|
+
key={o.value}
|
|
328
|
+
id={index >= 0 ? entryId(index) : undefined}
|
|
329
|
+
role="option"
|
|
330
|
+
aria-selected={isSelected}
|
|
331
|
+
aria-disabled={o.disabled || undefined}
|
|
332
|
+
data-active={isActive || undefined}
|
|
333
|
+
onMouseEnter={() => { if (index >= 0) setActive(index) }}
|
|
334
|
+
onClick={() => { if (!o.disabled) choose(o.value) }}
|
|
335
|
+
className="group/option flex cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 pl-2.5 text-sm select-none data-active:bg-accent data-active:text-accent-foreground aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
|
|
336
|
+
>
|
|
337
|
+
{o.color && <span className="size-2 shrink-0 rounded-full" style={{ background: o.color }} />}
|
|
338
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
339
|
+
<span className="truncate">{o.label}</span>
|
|
340
|
+
{o.description && <span className="truncate text-[11px] text-muted-foreground">{o.description}</span>}
|
|
341
|
+
</span>
|
|
342
|
+
{isSelected && <CheckIcon className="size-4 shrink-0" />}
|
|
343
|
+
{onRemove && !o.disabled && (
|
|
344
|
+
// Fora da ordem do Tab (a lista anda pelas setas); no
|
|
345
|
+
// teclado, a exclusão é o Delete na opção ativa.
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
tabIndex={-1}
|
|
349
|
+
aria-label={removeLabel(o)}
|
|
350
|
+
title={removeLabel(o)}
|
|
351
|
+
onClick={e => { e.stopPropagation(); setConfirming(o.value) }}
|
|
352
|
+
className="-m-1 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground opacity-60 outline-none hover:text-destructive hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/option:opacity-60 [@media(hover:hover)]:group-data-[active]/option:opacity-60"
|
|
353
|
+
>
|
|
354
|
+
<XIcon className="size-3.5" />
|
|
355
|
+
</button>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
})}
|
|
360
|
+
</div>
|
|
361
|
+
))}
|
|
362
|
+
|
|
363
|
+
{canCreate && (() => {
|
|
364
|
+
const index = entries.length - 1
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
id={entryId(index)}
|
|
368
|
+
role="option"
|
|
369
|
+
aria-selected={false}
|
|
370
|
+
data-active={index === activeIndex || undefined}
|
|
371
|
+
onMouseEnter={() => setActive(index)}
|
|
372
|
+
onClick={() => void create()}
|
|
373
|
+
className="flex cursor-pointer items-center gap-2 rounded-md py-1.5 pr-2 pl-2.5 text-sm font-medium text-primary-strong select-none data-active:bg-primary/10"
|
|
374
|
+
>
|
|
375
|
+
{creating ? <Loader2Icon className="size-4 shrink-0 animate-spin" /> : <PlusIcon className="size-4 shrink-0" />}
|
|
376
|
+
<span className="truncate">{createLabel(query.trim())}</span>
|
|
377
|
+
</div>
|
|
378
|
+
)
|
|
379
|
+
})()}
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{error && (
|
|
383
|
+
<p role="alert" className="border-t border-border px-3 py-2 text-xs text-destructive">{error}</p>
|
|
384
|
+
)}
|
|
385
|
+
</PopoverContent>
|
|
386
|
+
</Popover>
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export { Combobox }
|