@motor-hero/ui-kit 0.16.0 → 0.17.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/combobox.d.ts +13 -0
- package/dist/components/combobox.d.ts.map +1 -1
- package/dist/index.cjs +98 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +98 -73
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/combobox.tsx +127 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motor-hero/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
|
|
5
5
|
"author": "MotorHero",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"@radix-ui/react-slot": "^1.1.0",
|
|
63
63
|
"class-variance-authority": "^0.7.0",
|
|
64
64
|
"cmdk": "^1.1.1",
|
|
65
|
-
"sonner": "^2.0.7"
|
|
65
|
+
"sonner": "^2.0.7",
|
|
66
|
+
"vaul": "^1.1.2"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@commitlint/cli": "^20.5.3",
|
|
@@ -2,6 +2,7 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
|
2
2
|
import { Command as CommandPrimitive } from "cmdk"
|
|
3
3
|
import { Check, ChevronsUpDown, Search } from "lucide-react"
|
|
4
4
|
import * as React from "react"
|
|
5
|
+
import { Drawer as DrawerPrimitive } from "vaul"
|
|
5
6
|
import { cn } from "../lib/utils"
|
|
6
7
|
|
|
7
8
|
export interface ComboboxOption {
|
|
@@ -13,9 +14,12 @@ export interface ComboboxOption {
|
|
|
13
14
|
export interface ComboboxProps {
|
|
14
15
|
options: ComboboxOption[]
|
|
15
16
|
value?: string
|
|
16
|
-
onChange: (value: string) => void
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
onChange: (value: string) => void
|
|
18
|
+
/** Texto do gatilho quando nada está selecionado */
|
|
19
|
+
placeholder?: string
|
|
20
|
+
/** Placeholder do campo de busca */
|
|
21
|
+
searchPlaceholder?: string
|
|
22
|
+
/** Mensagem quando a busca não retorna nada */
|
|
19
23
|
emptyMessage?: string
|
|
20
24
|
disabled?: boolean
|
|
21
25
|
id?: string
|
|
@@ -24,6 +28,36 @@ export interface ComboboxProps {
|
|
|
24
28
|
"aria-describedby"?: string
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
const DESKTOP_QUERY = "(min-width: 640px)"
|
|
32
|
+
|
|
33
|
+
function useIsDesktop() {
|
|
34
|
+
const [isDesktop, setIsDesktop] = React.useState(() =>
|
|
35
|
+
typeof window !== "undefined"
|
|
36
|
+
? window.matchMedia(DESKTOP_QUERY).matches
|
|
37
|
+
: true,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const mql = window.matchMedia(DESKTOP_QUERY)
|
|
42
|
+
const onChange = () => setIsDesktop(mql.matches)
|
|
43
|
+
onChange()
|
|
44
|
+
mql.addEventListener("change", onChange)
|
|
45
|
+
return () => mql.removeEventListener("change", onChange)
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
return isDesktop
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Select com busca (combobox) acessível e responsivo.
|
|
53
|
+
*
|
|
54
|
+
* - Desktop: Popover (modal) + cmdk — o dropdown acompanha a largura do gatilho.
|
|
55
|
+
* - Mobile: Drawer (bottom sheet) + cmdk — evita os problemas de foco/clique de
|
|
56
|
+
* Popover dentro de modal no touch, então a busca funciona no celular.
|
|
57
|
+
*
|
|
58
|
+
* Aceita `aria-invalid`/`aria-describedby` (encaminhados pelo FormField em
|
|
59
|
+
* estado de erro) e fica com a borda vermelha quando inválido.
|
|
60
|
+
*/
|
|
27
61
|
export function Combobox({
|
|
28
62
|
options,
|
|
29
63
|
value,
|
|
@@ -38,75 +72,98 @@ export function Combobox({
|
|
|
38
72
|
"aria-describedby": ariaDescribedby,
|
|
39
73
|
}: ComboboxProps) {
|
|
40
74
|
const [open, setOpen] = React.useState(false)
|
|
75
|
+
const isDesktop = useIsDesktop()
|
|
41
76
|
const selected = options.find((option) => option.value === value)
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
const trigger = (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
id={id}
|
|
82
|
+
role="combobox"
|
|
83
|
+
aria-expanded={open}
|
|
84
|
+
aria-invalid={ariaInvalid}
|
|
85
|
+
aria-describedby={ariaDescribedby}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive",
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<span className={cn("truncate", !selected && "text-muted-foreground")}>
|
|
93
|
+
{selected ? selected.label : placeholder}
|
|
94
|
+
</span>
|
|
95
|
+
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
96
|
+
</button>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const command = (
|
|
100
|
+
<CommandPrimitive className="flex h-full w-full flex-col overflow-hidden">
|
|
101
|
+
<div className="flex items-center border-b px-3">
|
|
102
|
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
103
|
+
<CommandPrimitive.Input
|
|
104
|
+
placeholder={searchPlaceholder}
|
|
105
|
+
className="flex h-11 w-full bg-transparent py-2 text-base outline-none placeholder:text-muted-foreground sm:h-9 sm:text-sm"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<CommandPrimitive.List className="max-h-[50vh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60">
|
|
109
|
+
<CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
|
|
110
|
+
{emptyMessage}
|
|
111
|
+
</CommandPrimitive.Empty>
|
|
112
|
+
{options.map((option) => (
|
|
113
|
+
<CommandPrimitive.Item
|
|
114
|
+
key={option.value}
|
|
115
|
+
value={option.label}
|
|
116
|
+
disabled={option.disabled}
|
|
117
|
+
onSelect={() => {
|
|
118
|
+
onChange(option.value)
|
|
119
|
+
setOpen(false)
|
|
120
|
+
}}
|
|
121
|
+
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"
|
|
61
122
|
>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</CommandPrimitive>
|
|
108
|
-
</PopoverPrimitive.Content>
|
|
109
|
-
</PopoverPrimitive.Portal>
|
|
110
|
-
</PopoverPrimitive.Root>
|
|
123
|
+
<Check
|
|
124
|
+
className={cn(
|
|
125
|
+
"h-4 w-4 shrink-0",
|
|
126
|
+
value === option.value ? "opacity-100" : "opacity-0",
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
<span className="truncate">{option.label}</span>
|
|
130
|
+
</CommandPrimitive.Item>
|
|
131
|
+
))}
|
|
132
|
+
</CommandPrimitive.List>
|
|
133
|
+
</CommandPrimitive>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (isDesktop) {
|
|
137
|
+
return (
|
|
138
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen} modal>
|
|
139
|
+
<PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
|
|
140
|
+
<PopoverPrimitive.Portal>
|
|
141
|
+
<PopoverPrimitive.Content
|
|
142
|
+
align="start"
|
|
143
|
+
sideOffset={4}
|
|
144
|
+
className="z-50 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-md"
|
|
145
|
+
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
146
|
+
>
|
|
147
|
+
{command}
|
|
148
|
+
</PopoverPrimitive.Content>
|
|
149
|
+
</PopoverPrimitive.Portal>
|
|
150
|
+
</PopoverPrimitive.Root>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
156
|
+
<DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
|
|
157
|
+
<DrawerPrimitive.Portal>
|
|
158
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
159
|
+
<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">
|
|
160
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
161
|
+
{placeholder}
|
|
162
|
+
</DrawerPrimitive.Title>
|
|
163
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
164
|
+
{command}
|
|
165
|
+
</DrawerPrimitive.Content>
|
|
166
|
+
</DrawerPrimitive.Portal>
|
|
167
|
+
</DrawerPrimitive.Root>
|
|
111
168
|
)
|
|
112
169
|
}
|