@motor-hero/ui-kit 0.17.0 → 0.19.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 +41 -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/theme-provider.d.ts +3 -1
- package/dist/components/theme-provider.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 +302 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +295 -91
- package/dist/index.js.map +1 -1
- package/dist/lib/tenant-theme.d.ts +15 -0
- package/dist/lib/tenant-theme.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/combobox.tsx +1 -20
- package/src/components/row-actions-menu.tsx +113 -36
- package/src/components/theme-provider.tsx +18 -8
- package/src/components/user-menu.tsx +157 -48
- package/src/hooks/use-is-desktop.ts +26 -0
- package/src/index.ts +3 -0
- package/src/lib/tenant-theme.ts +42 -0
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useState } from "react"
|
|
2
|
+
import { applyTenantTheme, type TenantTheme } from "../lib/tenant-theme"
|
|
2
3
|
|
|
3
4
|
type Theme = "dark" | "light" | "system"
|
|
4
5
|
|
|
@@ -6,6 +7,7 @@ type ThemeProviderProps = {
|
|
|
6
7
|
children: React.ReactNode
|
|
7
8
|
defaultTheme?: Theme
|
|
8
9
|
storageKey?: string
|
|
10
|
+
tenantTheme?: TenantTheme
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
type ThemeProviderState = {
|
|
@@ -24,6 +26,7 @@ export function ThemeProvider({
|
|
|
24
26
|
children,
|
|
25
27
|
defaultTheme = "system",
|
|
26
28
|
storageKey = "ui-theme",
|
|
29
|
+
tenantTheme,
|
|
27
30
|
...props
|
|
28
31
|
}: ThemeProviderProps) {
|
|
29
32
|
const [theme, setTheme] = useState<Theme>(
|
|
@@ -32,16 +35,23 @@ export function ThemeProvider({
|
|
|
32
35
|
|
|
33
36
|
useEffect(() => {
|
|
34
37
|
const root = window.document.documentElement
|
|
35
|
-
|
|
38
|
+
const applyResolved = (resolved: "light" | "dark") => {
|
|
39
|
+
root.classList.remove("light", "dark")
|
|
40
|
+
root.classList.add(resolved)
|
|
41
|
+
applyTenantTheme(root, tenantTheme, resolved)
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
if (theme === "system") {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const mql = window.matchMedia("(prefers-color-scheme: dark)")
|
|
46
|
+
applyResolved(mql.matches ? "dark" : "light")
|
|
47
|
+
const onChange = (e: MediaQueryListEvent) =>
|
|
48
|
+
applyResolved(e.matches ? "dark" : "light")
|
|
49
|
+
mql.addEventListener("change", onChange)
|
|
50
|
+
return () => mql.removeEventListener("change", onChange)
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
applyResolved(theme)
|
|
54
|
+
}, [theme, tenantTheme])
|
|
45
55
|
|
|
46
56
|
const value = {
|
|
47
57
|
theme,
|
|
@@ -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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Theme
|
|
2
2
|
export { ThemeProvider, useTheme } from "./components/theme-provider"
|
|
3
3
|
export type { Theme, ThemeProviderProps, ThemeProviderState } from "./components/theme-provider"
|
|
4
|
+
export { applyTenantTheme } from "./lib/tenant-theme"
|
|
5
|
+
export type { TenantTheme, TenantTokenSet } from "./lib/tenant-theme"
|
|
4
6
|
|
|
5
7
|
// Components
|
|
6
8
|
export { ModeToggle } from "./components/mode-toggle"
|
|
@@ -45,4 +47,5 @@ export { extractApiError } from "./lib/api-error"
|
|
|
45
47
|
|
|
46
48
|
// Hooks
|
|
47
49
|
export { useDisclosure } from "./hooks/use-disclosure"
|
|
50
|
+
export { useIsDesktop } from "./hooks/use-is-desktop"
|
|
48
51
|
export { useCustomToast, toast } from "./hooks/use-toast"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TenantTokenSet = {
|
|
2
|
+
primary?: string
|
|
3
|
+
primaryForeground?: string
|
|
4
|
+
secondary?: string
|
|
5
|
+
secondaryForeground?: string
|
|
6
|
+
accent?: string
|
|
7
|
+
accentForeground?: string
|
|
8
|
+
ring?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TenantTheme = {
|
|
12
|
+
light: TenantTokenSet
|
|
13
|
+
dark?: Partial<TenantTokenSet>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const VAR_MAP: Record<keyof TenantTokenSet, string> = {
|
|
17
|
+
primary: "--color-primary",
|
|
18
|
+
primaryForeground: "--color-primary-foreground",
|
|
19
|
+
secondary: "--color-secondary",
|
|
20
|
+
secondaryForeground: "--color-secondary-foreground",
|
|
21
|
+
accent: "--color-accent",
|
|
22
|
+
accentForeground: "--color-accent-foreground",
|
|
23
|
+
ring: "--color-ring",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyTenantTheme(
|
|
27
|
+
root: HTMLElement,
|
|
28
|
+
tenantTheme: TenantTheme | undefined,
|
|
29
|
+
mode: "light" | "dark"
|
|
30
|
+
) {
|
|
31
|
+
const tokens: TenantTokenSet = tenantTheme
|
|
32
|
+
? mode === "dark"
|
|
33
|
+
? { ...tenantTheme.light, ...tenantTheme.dark }
|
|
34
|
+
: tenantTheme.light
|
|
35
|
+
: {}
|
|
36
|
+
|
|
37
|
+
for (const key of Object.keys(VAR_MAP) as (keyof TenantTokenSet)[]) {
|
|
38
|
+
const value = tokens[key]
|
|
39
|
+
if (value) root.style.setProperty(VAR_MAP[key], value)
|
|
40
|
+
else root.style.removeProperty(VAR_MAP[key])
|
|
41
|
+
}
|
|
42
|
+
}
|