@motor-hero/ui-kit 0.16.1 → 0.18.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/components/row-actions-menu.d.ts +1 -1
- package/dist/components/row-actions-menu.d.ts.map +1 -1
- package/dist/components/user-menu.d.ts +1 -1
- package/dist/components/user-menu.d.ts.map +1 -1
- package/dist/hooks/use-is-desktop.d.ts +7 -0
- package/dist/hooks/use-is-desktop.d.ts.map +1 -0
- package/dist/index.cjs +362 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +356 -156
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/combobox.tsx +108 -70
- package/src/components/row-actions-menu.tsx +113 -36
- package/src/components/user-menu.tsx +157 -48
- package/src/hooks/use-is-desktop.ts +26 -0
- package/src/index.ts +1 -0
|
@@ -2,6 +2,8 @@ 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"
|
|
6
|
+
import { useIsDesktop } from "../hooks/use-is-desktop"
|
|
5
7
|
import { cn } from "../lib/utils"
|
|
6
8
|
|
|
7
9
|
export interface ComboboxOption {
|
|
@@ -13,9 +15,12 @@ export interface ComboboxOption {
|
|
|
13
15
|
export interface ComboboxProps {
|
|
14
16
|
options: ComboboxOption[]
|
|
15
17
|
value?: string
|
|
16
|
-
onChange: (value: string) => void
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
onChange: (value: string) => void
|
|
19
|
+
/** Texto do gatilho quando nada está selecionado */
|
|
20
|
+
placeholder?: string
|
|
21
|
+
/** Placeholder do campo de busca */
|
|
22
|
+
searchPlaceholder?: string
|
|
23
|
+
/** Mensagem quando a busca não retorna nada */
|
|
19
24
|
emptyMessage?: string
|
|
20
25
|
disabled?: boolean
|
|
21
26
|
id?: string
|
|
@@ -24,6 +29,16 @@ export interface ComboboxProps {
|
|
|
24
29
|
"aria-describedby"?: string
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Select com busca (combobox) acessível e responsivo.
|
|
34
|
+
*
|
|
35
|
+
* - Desktop: Popover (modal) + cmdk — o dropdown acompanha a largura do gatilho.
|
|
36
|
+
* - Mobile: Drawer (bottom sheet) + cmdk — evita os problemas de foco/clique de
|
|
37
|
+
* Popover dentro de modal no touch, então a busca funciona no celular.
|
|
38
|
+
*
|
|
39
|
+
* Aceita `aria-invalid`/`aria-describedby` (encaminhados pelo FormField em
|
|
40
|
+
* estado de erro) e fica com a borda vermelha quando inválido.
|
|
41
|
+
*/
|
|
27
42
|
export function Combobox({
|
|
28
43
|
options,
|
|
29
44
|
value,
|
|
@@ -38,75 +53,98 @@ export function Combobox({
|
|
|
38
53
|
"aria-describedby": ariaDescribedby,
|
|
39
54
|
}: ComboboxProps) {
|
|
40
55
|
const [open, setOpen] = React.useState(false)
|
|
56
|
+
const isDesktop = useIsDesktop()
|
|
41
57
|
const selected = options.find((option) => option.value === value)
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const trigger = (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
id={id}
|
|
63
|
+
role="combobox"
|
|
64
|
+
aria-expanded={open}
|
|
65
|
+
aria-invalid={ariaInvalid}
|
|
66
|
+
aria-describedby={ariaDescribedby}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
className={cn(
|
|
69
|
+
"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",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span className={cn("truncate", !selected && "text-muted-foreground")}>
|
|
74
|
+
{selected ? selected.label : placeholder}
|
|
75
|
+
</span>
|
|
76
|
+
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
77
|
+
</button>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const command = (
|
|
81
|
+
<CommandPrimitive className="flex h-full w-full flex-col overflow-hidden">
|
|
82
|
+
<div className="flex items-center border-b px-3">
|
|
83
|
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
84
|
+
<CommandPrimitive.Input
|
|
85
|
+
placeholder={searchPlaceholder}
|
|
86
|
+
className="flex h-11 w-full bg-transparent py-2 text-base outline-none placeholder:text-muted-foreground sm:h-9 sm:text-sm"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<CommandPrimitive.List className="max-h-[50vh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60">
|
|
90
|
+
<CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
|
|
91
|
+
{emptyMessage}
|
|
92
|
+
</CommandPrimitive.Empty>
|
|
93
|
+
{options.map((option) => (
|
|
94
|
+
<CommandPrimitive.Item
|
|
95
|
+
key={option.value}
|
|
96
|
+
value={option.label}
|
|
97
|
+
disabled={option.disabled}
|
|
98
|
+
onSelect={() => {
|
|
99
|
+
onChange(option.value)
|
|
100
|
+
setOpen(false)
|
|
101
|
+
}}
|
|
102
|
+
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"
|
|
103
|
+
>
|
|
104
|
+
<Check
|
|
105
|
+
className={cn(
|
|
106
|
+
"h-4 w-4 shrink-0",
|
|
107
|
+
value === option.value ? "opacity-100" : "opacity-0",
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
<span className="truncate">{option.label}</span>
|
|
111
|
+
</CommandPrimitive.Item>
|
|
112
|
+
))}
|
|
113
|
+
</CommandPrimitive.List>
|
|
114
|
+
</CommandPrimitive>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if (isDesktop) {
|
|
118
|
+
return (
|
|
119
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen} modal>
|
|
120
|
+
<PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
|
|
121
|
+
<PopoverPrimitive.Portal>
|
|
122
|
+
<PopoverPrimitive.Content
|
|
123
|
+
align="start"
|
|
124
|
+
sideOffset={4}
|
|
125
|
+
className="z-50 overflow-hidden rounded-md border bg-popover p-0 text-popover-foreground shadow-md"
|
|
126
|
+
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
61
127
|
>
|
|
62
|
-
{
|
|
63
|
-
</
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
|
|
84
|
-
{emptyMessage}
|
|
85
|
-
</CommandPrimitive.Empty>
|
|
86
|
-
{options.map((option) => (
|
|
87
|
-
<CommandPrimitive.Item
|
|
88
|
-
key={option.value}
|
|
89
|
-
value={option.label}
|
|
90
|
-
disabled={option.disabled}
|
|
91
|
-
onSelect={() => {
|
|
92
|
-
onChange(option.value)
|
|
93
|
-
setOpen(false)
|
|
94
|
-
}}
|
|
95
|
-
className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.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"
|
|
96
|
-
>
|
|
97
|
-
<Check
|
|
98
|
-
className={cn(
|
|
99
|
-
"h-4 w-4 shrink-0",
|
|
100
|
-
value === option.value ? "opacity-100" : "opacity-0",
|
|
101
|
-
)}
|
|
102
|
-
/>
|
|
103
|
-
<span className="truncate">{option.label}</span>
|
|
104
|
-
</CommandPrimitive.Item>
|
|
105
|
-
))}
|
|
106
|
-
</CommandPrimitive.List>
|
|
107
|
-
</CommandPrimitive>
|
|
108
|
-
</PopoverPrimitive.Content>
|
|
109
|
-
</PopoverPrimitive.Portal>
|
|
110
|
-
</PopoverPrimitive.Root>
|
|
128
|
+
{command}
|
|
129
|
+
</PopoverPrimitive.Content>
|
|
130
|
+
</PopoverPrimitive.Portal>
|
|
131
|
+
</PopoverPrimitive.Root>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
137
|
+
<DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
|
|
138
|
+
<DrawerPrimitive.Portal>
|
|
139
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
140
|
+
<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">
|
|
141
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
142
|
+
{placeholder}
|
|
143
|
+
</DrawerPrimitive.Title>
|
|
144
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
145
|
+
{command}
|
|
146
|
+
</DrawerPrimitive.Content>
|
|
147
|
+
</DrawerPrimitive.Portal>
|
|
148
|
+
</DrawerPrimitive.Root>
|
|
111
149
|
)
|
|
112
150
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
|
2
2
|
import { MoreVertical } from "lucide-react"
|
|
3
|
-
import type
|
|
3
|
+
import { type ReactNode, useState } from "react"
|
|
4
|
+
import { Drawer as DrawerPrimitive } from "vaul"
|
|
5
|
+
import { useIsDesktop } from "../hooks/use-is-desktop"
|
|
4
6
|
import { cn } from "../lib/utils"
|
|
5
7
|
import type { RenderLink } from "./types"
|
|
6
8
|
import { defaultRenderLink } from "./types"
|
|
@@ -22,8 +24,14 @@ export interface RowActionsMenuProps {
|
|
|
22
24
|
renderLink?: RenderLink
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
const triggerClass =
|
|
28
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
29
|
+
|
|
30
|
+
// Desktop: itens compactos do dropdown. Mobile: alvos de toque maiores.
|
|
25
31
|
const itemClass =
|
|
26
32
|
"flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
|
33
|
+
const sheetItemClass =
|
|
34
|
+
"flex w-full cursor-pointer select-none items-center gap-3 rounded-md px-3 py-3 text-base outline-none transition-colors hover:bg-accent active:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
27
35
|
|
|
28
36
|
export function RowActionsMenu({
|
|
29
37
|
actions,
|
|
@@ -32,55 +40,124 @@ export function RowActionsMenu({
|
|
|
32
40
|
label = "Ações",
|
|
33
41
|
renderLink = defaultRenderLink,
|
|
34
42
|
}: RowActionsMenuProps) {
|
|
43
|
+
const isDesktop = useIsDesktop()
|
|
44
|
+
const [open, setOpen] = useState(false)
|
|
45
|
+
|
|
35
46
|
if (actions.length === 0) return null
|
|
36
47
|
|
|
48
|
+
const trigger = (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
aria-label={label}
|
|
53
|
+
className={triggerClass}
|
|
54
|
+
>
|
|
55
|
+
<MoreVertical className="h-4 w-4" />
|
|
56
|
+
</button>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (isDesktop) {
|
|
60
|
+
return (
|
|
61
|
+
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
|
62
|
+
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
|
|
63
|
+
<DropdownMenu.Portal>
|
|
64
|
+
<DropdownMenu.Content
|
|
65
|
+
align={align}
|
|
66
|
+
sideOffset={6}
|
|
67
|
+
className="z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
|
68
|
+
>
|
|
69
|
+
{actions.map((action) => {
|
|
70
|
+
const cls = cn(
|
|
71
|
+
itemClass,
|
|
72
|
+
action.destructive &&
|
|
73
|
+
"text-destructive focus:bg-destructive/10",
|
|
74
|
+
)
|
|
75
|
+
return action.href ? (
|
|
76
|
+
<DropdownMenu.Item
|
|
77
|
+
key={action.label}
|
|
78
|
+
asChild
|
|
79
|
+
disabled={action.disabled}
|
|
80
|
+
>
|
|
81
|
+
{renderLink({
|
|
82
|
+
href: action.href,
|
|
83
|
+
className: cls,
|
|
84
|
+
children: (
|
|
85
|
+
<>
|
|
86
|
+
{action.icon}
|
|
87
|
+
{action.label}
|
|
88
|
+
</>
|
|
89
|
+
),
|
|
90
|
+
})}
|
|
91
|
+
</DropdownMenu.Item>
|
|
92
|
+
) : (
|
|
93
|
+
<DropdownMenu.Item
|
|
94
|
+
key={action.label}
|
|
95
|
+
className={cls}
|
|
96
|
+
disabled={action.disabled}
|
|
97
|
+
onClick={action.onClick}
|
|
98
|
+
>
|
|
99
|
+
{action.icon}
|
|
100
|
+
{action.label}
|
|
101
|
+
</DropdownMenu.Item>
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</DropdownMenu.Content>
|
|
105
|
+
</DropdownMenu.Portal>
|
|
106
|
+
</DropdownMenu.Root>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mobile: action sheet (Drawer)
|
|
37
111
|
return (
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
</button>
|
|
48
|
-
</DropdownMenu.Trigger>
|
|
49
|
-
<DropdownMenu.Portal>
|
|
50
|
-
<DropdownMenu.Content
|
|
51
|
-
align={align}
|
|
52
|
-
sideOffset={6}
|
|
53
|
-
className="z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
|
54
|
-
>
|
|
112
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
113
|
+
<DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
|
|
114
|
+
<DrawerPrimitive.Portal>
|
|
115
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
116
|
+
<DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 flex flex-col rounded-t-2xl border bg-popover p-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] text-popover-foreground outline-none">
|
|
117
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
118
|
+
{label}
|
|
119
|
+
</DrawerPrimitive.Title>
|
|
120
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
55
121
|
{actions.map((action) => {
|
|
56
|
-
const cls = cn(
|
|
122
|
+
const cls = cn(
|
|
123
|
+
sheetItemClass,
|
|
124
|
+
action.destructive && "text-destructive",
|
|
125
|
+
)
|
|
126
|
+
const inner = (
|
|
127
|
+
<>
|
|
128
|
+
{action.icon}
|
|
129
|
+
{action.label}
|
|
130
|
+
</>
|
|
131
|
+
)
|
|
57
132
|
return action.href ? (
|
|
58
|
-
<
|
|
133
|
+
<span
|
|
134
|
+
key={action.label}
|
|
135
|
+
onClick={() => setOpen(false)}
|
|
136
|
+
onKeyDown={() => setOpen(false)}
|
|
137
|
+
>
|
|
59
138
|
{renderLink({
|
|
60
139
|
href: action.href,
|
|
61
140
|
className: cls,
|
|
62
|
-
children:
|
|
63
|
-
<>
|
|
64
|
-
{action.icon}
|
|
65
|
-
{action.label}
|
|
66
|
-
</>
|
|
67
|
-
),
|
|
141
|
+
children: inner,
|
|
68
142
|
})}
|
|
69
|
-
</
|
|
143
|
+
</span>
|
|
70
144
|
) : (
|
|
71
|
-
<
|
|
145
|
+
<button
|
|
72
146
|
key={action.label}
|
|
73
|
-
|
|
147
|
+
type="button"
|
|
74
148
|
disabled={action.disabled}
|
|
75
|
-
|
|
149
|
+
className={cls}
|
|
150
|
+
onClick={() => {
|
|
151
|
+
action.onClick?.()
|
|
152
|
+
setOpen(false)
|
|
153
|
+
}}
|
|
76
154
|
>
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
</DropdownMenu.Item>
|
|
155
|
+
{inner}
|
|
156
|
+
</button>
|
|
80
157
|
)
|
|
81
158
|
})}
|
|
82
|
-
</
|
|
83
|
-
</
|
|
84
|
-
</
|
|
159
|
+
</DrawerPrimitive.Content>
|
|
160
|
+
</DrawerPrimitive.Portal>
|
|
161
|
+
</DrawerPrimitive.Root>
|
|
85
162
|
)
|
|
86
163
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
|
2
2
|
import { LogOut, User } from "lucide-react"
|
|
3
|
-
import type
|
|
3
|
+
import { type ReactNode, useState } from "react"
|
|
4
|
+
import { Drawer as DrawerPrimitive } from "vaul"
|
|
5
|
+
import { useIsDesktop } from "../hooks/use-is-desktop"
|
|
4
6
|
import { cn } from "../lib/utils"
|
|
5
7
|
import type { RenderLink } from "./types"
|
|
6
8
|
import { defaultRenderLink } from "./types"
|
|
@@ -37,6 +39,8 @@ function initials(user?: AppUser): string {
|
|
|
37
39
|
|
|
38
40
|
const itemClass =
|
|
39
41
|
"flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent"
|
|
42
|
+
const sheetItemClass =
|
|
43
|
+
"flex w-full cursor-pointer select-none items-center gap-3 rounded-md px-3 py-3 text-base outline-none transition-colors hover:bg-accent active:bg-accent"
|
|
40
44
|
|
|
41
45
|
export function UserMenu({
|
|
42
46
|
user,
|
|
@@ -46,71 +50,176 @@ export function UserMenu({
|
|
|
46
50
|
renderLink = defaultRenderLink,
|
|
47
51
|
align = "end",
|
|
48
52
|
}: UserMenuProps) {
|
|
53
|
+
const isDesktop = useIsDesktop()
|
|
54
|
+
const [open, setOpen] = useState(false)
|
|
49
55
|
const label = initials(user)
|
|
50
56
|
|
|
57
|
+
const trigger = (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className="inline-flex h-9 w-9 items-center justify-center overflow-hidden rounded-full border border-input bg-background text-xs font-semibold shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
61
|
+
aria-label="Menu do usuário"
|
|
62
|
+
data-testid="user-menu"
|
|
63
|
+
>
|
|
64
|
+
{user?.avatarUrl ? (
|
|
65
|
+
<img
|
|
66
|
+
src={user.avatarUrl}
|
|
67
|
+
alt={user.name ?? user.email ?? "Avatar"}
|
|
68
|
+
className="h-full w-full object-cover"
|
|
69
|
+
/>
|
|
70
|
+
) : label ? (
|
|
71
|
+
<span>{label}</span>
|
|
72
|
+
) : (
|
|
73
|
+
<User className="h-4 w-4" />
|
|
74
|
+
)}
|
|
75
|
+
</button>
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const userInfo = (user?.name || user?.email) && (
|
|
79
|
+
<div className="px-2 py-1.5">
|
|
80
|
+
{user.name && (
|
|
81
|
+
<p className="truncate text-sm font-medium">{user.name}</p>
|
|
82
|
+
)}
|
|
83
|
+
{user.email && (
|
|
84
|
+
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (isDesktop) {
|
|
90
|
+
return (
|
|
91
|
+
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
|
92
|
+
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
|
|
93
|
+
<DropdownMenu.Portal>
|
|
94
|
+
<DropdownMenu.Content
|
|
95
|
+
align={align}
|
|
96
|
+
sideOffset={6}
|
|
97
|
+
className="z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
|
98
|
+
>
|
|
99
|
+
{userInfo && (
|
|
100
|
+
<>
|
|
101
|
+
{userInfo}
|
|
102
|
+
<DropdownMenu.Separator className="-mx-1 my-1 h-px bg-border" />
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{items.map((item) =>
|
|
107
|
+
item.href ? (
|
|
108
|
+
<DropdownMenu.Item key={item.label} asChild>
|
|
109
|
+
{renderLink({
|
|
110
|
+
href: item.href,
|
|
111
|
+
className: itemClass,
|
|
112
|
+
children: (
|
|
113
|
+
<>
|
|
114
|
+
{item.icon}
|
|
115
|
+
{item.label}
|
|
116
|
+
</>
|
|
117
|
+
),
|
|
118
|
+
})}
|
|
119
|
+
</DropdownMenu.Item>
|
|
120
|
+
) : (
|
|
121
|
+
<DropdownMenu.Item
|
|
122
|
+
key={item.label}
|
|
123
|
+
className={itemClass}
|
|
124
|
+
onClick={item.onClick}
|
|
125
|
+
>
|
|
126
|
+
{item.icon}
|
|
127
|
+
{item.label}
|
|
128
|
+
</DropdownMenu.Item>
|
|
129
|
+
),
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{onLogout && (
|
|
133
|
+
<>
|
|
134
|
+
{items.length > 0 && (
|
|
135
|
+
<DropdownMenu.Separator className="-mx-1 my-1 h-px bg-border" />
|
|
136
|
+
)}
|
|
137
|
+
<DropdownMenu.Item
|
|
138
|
+
className={cn(
|
|
139
|
+
itemClass,
|
|
140
|
+
"text-destructive focus:bg-destructive/10",
|
|
141
|
+
)}
|
|
142
|
+
onClick={onLogout}
|
|
143
|
+
>
|
|
144
|
+
<LogOut className="h-4 w-4" />
|
|
145
|
+
{logoutLabel}
|
|
146
|
+
</DropdownMenu.Item>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</DropdownMenu.Content>
|
|
150
|
+
</DropdownMenu.Portal>
|
|
151
|
+
</DropdownMenu.Root>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Mobile: bottom sheet (Drawer)
|
|
51
156
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
) : label ? (
|
|
63
|
-
<span>{label}</span>
|
|
64
|
-
) : (
|
|
65
|
-
<User className="h-4 w-4" />
|
|
66
|
-
)}
|
|
67
|
-
</button>
|
|
68
|
-
</DropdownMenu.Trigger>
|
|
69
|
-
<DropdownMenu.Portal>
|
|
70
|
-
<DropdownMenu.Content
|
|
71
|
-
align={align}
|
|
72
|
-
sideOffset={6}
|
|
73
|
-
className="z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
|
74
|
-
>
|
|
75
|
-
{(user?.name || user?.email) && (
|
|
157
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
158
|
+
<DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
|
|
159
|
+
<DrawerPrimitive.Portal>
|
|
160
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
161
|
+
<DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 flex flex-col rounded-t-2xl border bg-popover p-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] text-popover-foreground outline-none">
|
|
162
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
163
|
+
Menu do usuário
|
|
164
|
+
</DrawerPrimitive.Title>
|
|
165
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
166
|
+
{userInfo && (
|
|
76
167
|
<>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{user.email && <p className="truncate text-xs text-muted-foreground">{user.email}</p>}
|
|
80
|
-
</div>
|
|
81
|
-
<DropdownMenu.Separator className="-mx-1 my-1 h-px bg-border" />
|
|
168
|
+
{userInfo}
|
|
169
|
+
<div className="-mx-1 my-1 h-px bg-border" />
|
|
82
170
|
</>
|
|
83
171
|
)}
|
|
84
|
-
|
|
85
172
|
{items.map((item) =>
|
|
86
173
|
item.href ? (
|
|
87
|
-
<
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
174
|
+
<span
|
|
175
|
+
key={item.label}
|
|
176
|
+
onClick={() => setOpen(false)}
|
|
177
|
+
onKeyDown={() => setOpen(false)}
|
|
178
|
+
>
|
|
179
|
+
{renderLink({
|
|
180
|
+
href: item.href,
|
|
181
|
+
className: sheetItemClass,
|
|
182
|
+
children: (
|
|
183
|
+
<>
|
|
184
|
+
{item.icon}
|
|
185
|
+
{item.label}
|
|
186
|
+
</>
|
|
187
|
+
),
|
|
188
|
+
})}
|
|
189
|
+
</span>
|
|
95
190
|
) : (
|
|
96
|
-
<
|
|
191
|
+
<button
|
|
192
|
+
key={item.label}
|
|
193
|
+
type="button"
|
|
194
|
+
className={sheetItemClass}
|
|
195
|
+
onClick={() => {
|
|
196
|
+
item.onClick?.()
|
|
197
|
+
setOpen(false)
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
97
200
|
{item.icon}
|
|
98
201
|
{item.label}
|
|
99
|
-
</
|
|
202
|
+
</button>
|
|
100
203
|
),
|
|
101
204
|
)}
|
|
102
|
-
|
|
103
205
|
{onLogout && (
|
|
104
206
|
<>
|
|
105
|
-
{items.length > 0 && <
|
|
106
|
-
<
|
|
207
|
+
{items.length > 0 && <div className="-mx-1 my-1 h-px bg-border" />}
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className={cn(sheetItemClass, "text-destructive")}
|
|
211
|
+
onClick={() => {
|
|
212
|
+
onLogout()
|
|
213
|
+
setOpen(false)
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
107
216
|
<LogOut className="h-4 w-4" />
|
|
108
217
|
{logoutLabel}
|
|
109
|
-
</
|
|
218
|
+
</button>
|
|
110
219
|
</>
|
|
111
220
|
)}
|
|
112
|
-
</
|
|
113
|
-
</
|
|
114
|
-
</
|
|
221
|
+
</DrawerPrimitive.Content>
|
|
222
|
+
</DrawerPrimitive.Portal>
|
|
223
|
+
</DrawerPrimitive.Root>
|
|
115
224
|
)
|
|
116
225
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const DESKTOP_QUERY = "(min-width: 640px)"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retorna `true` no desktop (>= 640px, breakpoint `sm`) e `false` no mobile.
|
|
7
|
+
* Usado para alternar Popover/DropdownMenu (desktop) por Drawer/bottom sheet
|
|
8
|
+
* (mobile) em componentes responsivos como Combobox e os menus de ação.
|
|
9
|
+
*/
|
|
10
|
+
export function useIsDesktop(): boolean {
|
|
11
|
+
const [isDesktop, setIsDesktop] = React.useState(() =>
|
|
12
|
+
typeof window !== "undefined"
|
|
13
|
+
? window.matchMedia(DESKTOP_QUERY).matches
|
|
14
|
+
: true,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const mql = window.matchMedia(DESKTOP_QUERY)
|
|
19
|
+
const onChange = () => setIsDesktop(mql.matches)
|
|
20
|
+
onChange()
|
|
21
|
+
mql.addEventListener("change", onChange)
|
|
22
|
+
return () => mql.removeEventListener("change", onChange)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
return isDesktop
|
|
26
|
+
}
|
package/src/index.ts
CHANGED