@motor-hero/ui-kit 0.22.0 → 0.23.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/dist/components/multi-combobox.d.ts +32 -0
- package/dist/components/multi-combobox.d.ts.map +1 -0
- package/dist/index.cjs +297 -148
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +303 -155
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/multi-combobox.tsx +189 -0
- package/src/components/row-actions-menu.tsx +2 -2
- package/src/index.ts +2 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
2
|
+
import { Command as CommandPrimitive } from "cmdk"
|
|
3
|
+
import { Check, ChevronsUpDown, Search, X } from "lucide-react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import { Drawer as DrawerPrimitive } from "vaul"
|
|
6
|
+
import { useIsDesktop } from "../hooks/use-is-desktop"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import type { ComboboxOption } from "./combobox"
|
|
9
|
+
|
|
10
|
+
export interface MultiComboboxProps {
|
|
11
|
+
options: ComboboxOption[]
|
|
12
|
+
value: string[]
|
|
13
|
+
onChange: (value: string[]) => void
|
|
14
|
+
/** Texto do gatilho quando nada está selecionado */
|
|
15
|
+
placeholder?: string
|
|
16
|
+
/** Placeholder do campo de busca */
|
|
17
|
+
searchPlaceholder?: string
|
|
18
|
+
/** Mensagem quando a busca não retorna nada */
|
|
19
|
+
emptyMessage?: string
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
id?: string
|
|
22
|
+
className?: string
|
|
23
|
+
"aria-invalid"?: boolean | "true" | "false"
|
|
24
|
+
"aria-describedby"?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Combobox de múltipla seleção com chips, acessível e responsivo.
|
|
29
|
+
*
|
|
30
|
+
* - Desktop: Popover (modal) + cmdk — o dropdown acompanha a largura do gatilho.
|
|
31
|
+
* - Mobile: Drawer (bottom sheet) + cmdk — evita os problemas de foco/clique de
|
|
32
|
+
* Popover dentro de modal no touch, então a busca funciona no celular.
|
|
33
|
+
*
|
|
34
|
+
* As opções selecionadas aparecem como chips dentro do gatilho; clicar numa
|
|
35
|
+
* opção da lista alterna a seleção (a lista permanece aberta) e o `×` de cada
|
|
36
|
+
* chip remove a opção. Aceita `aria-invalid`/`aria-describedby` (encaminhados
|
|
37
|
+
* pelo FormField em estado de erro) e fica com a borda vermelha quando inválido.
|
|
38
|
+
*/
|
|
39
|
+
export function MultiCombobox({
|
|
40
|
+
options,
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
placeholder = "Selecione...",
|
|
44
|
+
searchPlaceholder = "Buscar...",
|
|
45
|
+
emptyMessage = "Nenhum resultado encontrado.",
|
|
46
|
+
disabled,
|
|
47
|
+
id,
|
|
48
|
+
className,
|
|
49
|
+
"aria-invalid": ariaInvalid,
|
|
50
|
+
"aria-describedby": ariaDescribedby,
|
|
51
|
+
}: MultiComboboxProps) {
|
|
52
|
+
const [open, setOpen] = React.useState(false)
|
|
53
|
+
const isDesktop = useIsDesktop()
|
|
54
|
+
const selected = options.filter((option) => value.includes(option.value))
|
|
55
|
+
|
|
56
|
+
const toggleValue = (optionValue: string) => {
|
|
57
|
+
onChange(
|
|
58
|
+
value.includes(optionValue)
|
|
59
|
+
? value.filter((v) => v !== optionValue)
|
|
60
|
+
: [...value, optionValue],
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const removeValue = (optionValue: string) => {
|
|
65
|
+
onChange(value.filter((v) => v !== optionValue))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const trigger = (
|
|
69
|
+
<div
|
|
70
|
+
id={id}
|
|
71
|
+
role="combobox"
|
|
72
|
+
tabIndex={disabled ? -1 : 0}
|
|
73
|
+
aria-expanded={open}
|
|
74
|
+
aria-invalid={ariaInvalid}
|
|
75
|
+
aria-describedby={ariaDescribedby}
|
|
76
|
+
aria-disabled={disabled}
|
|
77
|
+
onKeyDown={(event) => {
|
|
78
|
+
if (disabled) return
|
|
79
|
+
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
setOpen(true)
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
className={cn(
|
|
85
|
+
"flex min-h-9 w-full cursor-pointer flex-wrap items-center gap-1 rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{selected.length === 0 ? (
|
|
90
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
91
|
+
) : (
|
|
92
|
+
selected.map((option) => (
|
|
93
|
+
<span
|
|
94
|
+
key={option.value}
|
|
95
|
+
className="inline-flex items-center gap-1 rounded bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground"
|
|
96
|
+
>
|
|
97
|
+
{option.label}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
tabIndex={-1}
|
|
101
|
+
aria-label={`Remover ${option.label}`}
|
|
102
|
+
onClick={(event) => {
|
|
103
|
+
event.stopPropagation()
|
|
104
|
+
removeValue(option.value)
|
|
105
|
+
}}
|
|
106
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
107
|
+
className="rounded-sm opacity-60 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
108
|
+
>
|
|
109
|
+
<X className="h-3 w-3" />
|
|
110
|
+
</button>
|
|
111
|
+
</span>
|
|
112
|
+
))
|
|
113
|
+
)}
|
|
114
|
+
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const command = (
|
|
119
|
+
<CommandPrimitive className="flex h-full w-full flex-col overflow-hidden">
|
|
120
|
+
<div className="flex items-center border-b px-3">
|
|
121
|
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
122
|
+
<CommandPrimitive.Input
|
|
123
|
+
placeholder={searchPlaceholder}
|
|
124
|
+
className="flex h-11 w-full bg-transparent py-2 text-base outline-none placeholder:text-muted-foreground sm:h-9 sm:text-sm"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<CommandPrimitive.List className="max-h-[50vh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60">
|
|
128
|
+
<CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
|
|
129
|
+
{emptyMessage}
|
|
130
|
+
</CommandPrimitive.Empty>
|
|
131
|
+
{options.map((option) => (
|
|
132
|
+
<CommandPrimitive.Item
|
|
133
|
+
key={option.value}
|
|
134
|
+
value={option.label}
|
|
135
|
+
disabled={option.disabled}
|
|
136
|
+
onSelect={() => toggleValue(option.value)}
|
|
137
|
+
className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-2.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 sm:py-1.5"
|
|
138
|
+
>
|
|
139
|
+
<Check
|
|
140
|
+
className={cn(
|
|
141
|
+
"h-4 w-4 shrink-0",
|
|
142
|
+
value.includes(option.value) ? "opacity-100" : "opacity-0",
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
<span className="truncate">{option.label}</span>
|
|
146
|
+
</CommandPrimitive.Item>
|
|
147
|
+
))}
|
|
148
|
+
</CommandPrimitive.List>
|
|
149
|
+
</CommandPrimitive>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if (isDesktop) {
|
|
153
|
+
return (
|
|
154
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen} modal>
|
|
155
|
+
<PopoverPrimitive.Trigger asChild disabled={disabled}>
|
|
156
|
+
{trigger}
|
|
157
|
+
</PopoverPrimitive.Trigger>
|
|
158
|
+
<PopoverPrimitive.Portal>
|
|
159
|
+
<PopoverPrimitive.Content
|
|
160
|
+
align="start"
|
|
161
|
+
sideOffset={4}
|
|
162
|
+
className="z-50 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-md"
|
|
163
|
+
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
164
|
+
>
|
|
165
|
+
{command}
|
|
166
|
+
</PopoverPrimitive.Content>
|
|
167
|
+
</PopoverPrimitive.Portal>
|
|
168
|
+
</PopoverPrimitive.Root>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
174
|
+
<DrawerPrimitive.Trigger asChild disabled={disabled}>
|
|
175
|
+
{trigger}
|
|
176
|
+
</DrawerPrimitive.Trigger>
|
|
177
|
+
<DrawerPrimitive.Portal>
|
|
178
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
179
|
+
<DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
|
|
180
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
181
|
+
{placeholder}
|
|
182
|
+
</DrawerPrimitive.Title>
|
|
183
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
184
|
+
{command}
|
|
185
|
+
</DrawerPrimitive.Content>
|
|
186
|
+
</DrawerPrimitive.Portal>
|
|
187
|
+
</DrawerPrimitive.Root>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
@@ -58,7 +58,7 @@ export function RowActionsMenu({
|
|
|
58
58
|
|
|
59
59
|
if (isDesktop) {
|
|
60
60
|
return (
|
|
61
|
-
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
|
61
|
+
<DropdownMenu.Root open={open} onOpenChange={setOpen} modal={false}>
|
|
62
62
|
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
|
|
63
63
|
<DropdownMenu.Portal>
|
|
64
64
|
<DropdownMenu.Content
|
|
@@ -70,7 +70,7 @@ export function RowActionsMenu({
|
|
|
70
70
|
const cls = cn(
|
|
71
71
|
itemClass,
|
|
72
72
|
action.destructive &&
|
|
73
|
-
|
|
73
|
+
"text-destructive focus:bg-destructive/10",
|
|
74
74
|
)
|
|
75
75
|
return action.href ? (
|
|
76
76
|
<DropdownMenu.Item
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { SearchInput } from "./components/search-input"
|
|
|
28
28
|
export { PasswordInput } from "./components/password-input"
|
|
29
29
|
export { Combobox } from "./components/combobox"
|
|
30
30
|
export type { ComboboxOption, ComboboxProps } from "./components/combobox"
|
|
31
|
+
export { MultiCombobox } from "./components/multi-combobox"
|
|
32
|
+
export type { MultiComboboxProps } from "./components/multi-combobox"
|
|
31
33
|
export { ColorPicker } from "./components/color-picker"
|
|
32
34
|
export type { ColorPickerProps } from "./components/color-picker"
|
|
33
35
|
export { StatCard } from "./components/stat-card"
|