@authdog/react-elements 0.0.32 → 0.0.34
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/.turbo/turbo-build.log +34 -34
- package/CHANGELOG.md +12 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/dropdown-menu.mjs.map +1 -1
- package/dist/index.d.mts +43 -5
- package/dist/index.d.ts +43 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +82 -11
- package/package.json +1 -1
- package/src/components/core/user-dropdown.tsx +99 -0
- package/src/components/core/user-profile.tsx +159 -63
- package/src/components/ui/dropdown-menu.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/main.tsx +1 -1
- package/src/preview.tsx +1 -0
- package/src/stories/UserDropdown.stories.tsx +36 -0
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../../components/ui/dropdown-menu"
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
|
|
5
|
+
import { cn } from "@authdog/react-elements/lib/utils"
|
|
6
|
+
import { LogOut, Settings, ExternalLink } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
export type UserDropdownLink = {
|
|
9
|
+
label: string
|
|
10
|
+
href?: string
|
|
11
|
+
onClick?: () => void
|
|
12
|
+
icon?: React.ComponentType<any>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UserDropdownProps {
|
|
16
|
+
trigger: React.ReactElement
|
|
17
|
+
user: {
|
|
18
|
+
displayName?: string
|
|
19
|
+
name?: string
|
|
20
|
+
email?: string
|
|
21
|
+
emails?: { value: string }[]
|
|
22
|
+
photos?: { value: string }[]
|
|
23
|
+
avatar?: string
|
|
24
|
+
}
|
|
25
|
+
className?: string
|
|
26
|
+
onManageAccount?: () => void
|
|
27
|
+
onSignout?: () => void
|
|
28
|
+
links?: UserDropdownLink[]
|
|
29
|
+
side?: "top" | "right" | "bottom" | "left"
|
|
30
|
+
align?: "start" | "center" | "end"
|
|
31
|
+
sideOffset?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getInitials = (name?: string) => {
|
|
35
|
+
if (!name) return "?"
|
|
36
|
+
const parts = String(name).trim().split(/\s+/)
|
|
37
|
+
const initials = parts.slice(0, 2).map((p) => p[0]?.toUpperCase()).join("")
|
|
38
|
+
return initials || "?"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const UserDropdown = ({ trigger, user, className, onManageAccount, onSignout, links = [], side = "bottom", align = "end", sideOffset = 8 }: UserDropdownProps) => {
|
|
42
|
+
const primaryEmail = user?.emails?.[0]?.value || user?.email || ""
|
|
43
|
+
const displayName = user?.displayName || user?.name || ""
|
|
44
|
+
const avatar = user?.photos?.[0]?.value || user?.avatar || ""
|
|
45
|
+
|
|
46
|
+
const handleLink = (item: UserDropdownLink) => {
|
|
47
|
+
if (item.onClick) return item.onClick()
|
|
48
|
+
if (item.href) {
|
|
49
|
+
if (item.href.startsWith("http")) {
|
|
50
|
+
window.open(item.href, "_blank")
|
|
51
|
+
} else {
|
|
52
|
+
window.location.assign(item.href)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const IconExternal = ExternalLink as any
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<DropdownMenu>
|
|
61
|
+
<DropdownMenuTrigger className="inline-flex items-center justify-center h-10 w-10 rounded-full focus:outline-none">
|
|
62
|
+
{trigger}
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
<DropdownMenuContent align={align} side={side} sideOffset={sideOffset} className={cn("w-72 p-2 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md", className)}>
|
|
65
|
+
<div className="flex items-center gap-3 px-4 pt-4 pb-3">
|
|
66
|
+
<Avatar className="h-9 w-9 rounded-full">
|
|
67
|
+
<AvatarImage src={avatar} alt={displayName} />
|
|
68
|
+
<AvatarFallback className="rounded-full">{getInitials(displayName)}</AvatarFallback>
|
|
69
|
+
</Avatar>
|
|
70
|
+
<div className="min-w-0">
|
|
71
|
+
<div className="text-sm font-semibold truncate">{displayName}</div>
|
|
72
|
+
<div className="text-xs text-muted-foreground truncate">{primaryEmail}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<DropdownMenuSeparator />
|
|
76
|
+
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onManageAccount?.()}>
|
|
77
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
78
|
+
<span>Manage account</span>
|
|
79
|
+
</DropdownMenuItem>
|
|
80
|
+
{links.map((item, idx) => {
|
|
81
|
+
const Icon = (item.icon ?? IconExternal) as any
|
|
82
|
+
return (
|
|
83
|
+
<DropdownMenuItem key={`${item.label}-${idx}`} className="cursor-pointer py-2" onClick={() => handleLink(item)}>
|
|
84
|
+
<Icon className="mr-2 h-4 w-4" />
|
|
85
|
+
<span>{item.label}</span>
|
|
86
|
+
</DropdownMenuItem>
|
|
87
|
+
)
|
|
88
|
+
})}
|
|
89
|
+
<DropdownMenuSeparator />
|
|
90
|
+
<DropdownMenuItem className="cursor-pointer text-red-600 focus:text-red-700 py-2" onClick={() => onSignout?.()}>
|
|
91
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
92
|
+
<span>Sign out</span>
|
|
93
|
+
</DropdownMenuItem>
|
|
94
|
+
</DropdownMenuContent>
|
|
95
|
+
</DropdownMenu>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
@@ -4,22 +4,28 @@ import { useEffect, useState } from "react"
|
|
|
4
4
|
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
|
|
5
5
|
import { Button } from "../../components/ui/button"
|
|
6
6
|
import { Badge } from "../../components/ui/badge"
|
|
7
|
-
import { User, LucideProps } from "lucide-react"
|
|
7
|
+
import { User, Shield, SlidersHorizontal, LucideProps } from "lucide-react"
|
|
8
8
|
|
|
9
9
|
export interface UserProfileProps {
|
|
10
10
|
loading: boolean;
|
|
11
11
|
user: any;
|
|
12
12
|
emails?: { address: string; isPrimary?: boolean }[];
|
|
13
13
|
handleAuthenticated?: () => void;
|
|
14
|
+
onRequestEmailVerification?: (email: string) => Promise<{ success: boolean; message?: string } | void>;
|
|
15
|
+
onVerifyEmail?: (email: string, code: string) => Promise<{ success: boolean; message?: string } | void>;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export const UserProfile = ({
|
|
17
19
|
loading,
|
|
18
20
|
user,
|
|
19
|
-
handleAuthenticated
|
|
21
|
+
handleAuthenticated,
|
|
22
|
+
onRequestEmailVerification,
|
|
23
|
+
onVerifyEmail,
|
|
20
24
|
}: UserProfileProps) => {
|
|
21
25
|
const [isMounted, setIsMounted] = useState(false)
|
|
22
|
-
const [activeTab, setActiveTab] = useState<"profile" | "security">("profile");
|
|
26
|
+
const [activeTab, setActiveTab] = useState<"profile" | "security" | "preferences">("profile");
|
|
27
|
+
const [verifyingEmail, setVerifyingEmail] = useState<string | null>(null)
|
|
28
|
+
const [codeByEmail, setCodeByEmail] = useState<Record<string, string>>({})
|
|
23
29
|
|
|
24
30
|
useEffect(() => {
|
|
25
31
|
setIsMounted(true);
|
|
@@ -51,10 +57,10 @@ export const UserProfile = ({
|
|
|
51
57
|
|
|
52
58
|
return (
|
|
53
59
|
<div className="grid grid-cols-[14rem,1fr] w-full bg-transparent">
|
|
54
|
-
<div className="h-full border-r p-3 md:p-4 bg-transparent flex flex-col min-w-0">
|
|
60
|
+
<div className="h-full border-r border-border p-3 md:p-4 bg-transparent flex flex-col min-w-0">
|
|
55
61
|
<div className="mb-3 md:mb-4">
|
|
56
|
-
<h1 className="text-xl font-bold text-
|
|
57
|
-
<p className="text-sm text-
|
|
62
|
+
<h1 className="text-xl font-bold text-foreground">Account</h1>
|
|
63
|
+
<p className="text-sm text-muted-foreground">Manage your account info.</p>
|
|
58
64
|
</div>
|
|
59
65
|
|
|
60
66
|
<nav className="space-y-1 flex-1">
|
|
@@ -62,29 +68,46 @@ export const UserProfile = ({
|
|
|
62
68
|
onClick={() => setActiveTab("profile")}
|
|
63
69
|
className={`flex items-center w-full px-3 py-2 text-sm rounded-md ${
|
|
64
70
|
activeTab === "profile"
|
|
65
|
-
? "bg-
|
|
66
|
-
: "text-
|
|
71
|
+
? "bg-muted text-foreground"
|
|
72
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
67
73
|
}`}
|
|
68
74
|
>
|
|
69
75
|
{renderIcon(User)}
|
|
70
76
|
Profile
|
|
71
77
|
</button>
|
|
72
|
-
|
|
78
|
+
<button
|
|
73
79
|
onClick={() => setActiveTab("security")}
|
|
74
80
|
className={`flex items-center w-full px-3 py-2 text-sm rounded-md ${
|
|
75
|
-
activeTab === "security"
|
|
81
|
+
activeTab === "security"
|
|
82
|
+
? "bg-muted text-foreground"
|
|
83
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
76
84
|
}`}
|
|
77
85
|
>
|
|
78
86
|
{renderIcon(Shield)}
|
|
79
87
|
Security
|
|
80
|
-
</button>
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => setActiveTab("preferences")}
|
|
91
|
+
className={`flex items-center w-full px-3 py-2 text-sm rounded-md ${
|
|
92
|
+
activeTab === "preferences"
|
|
93
|
+
? "bg-muted text-foreground"
|
|
94
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{renderIcon(SlidersHorizontal)}
|
|
98
|
+
Preferences
|
|
99
|
+
</button>
|
|
81
100
|
</nav>
|
|
82
101
|
</div>
|
|
83
102
|
|
|
84
103
|
<div className="h-full p-3 md:p-5 min-w-0 bg-transparent">
|
|
85
104
|
<div className="flex justify-between items-center mb-3 md:mb-4">
|
|
86
|
-
<h2 className="text-xl font-semibold text-
|
|
87
|
-
{activeTab === "profile"
|
|
105
|
+
<h2 className="text-xl font-semibold text-foreground">
|
|
106
|
+
{activeTab === "profile"
|
|
107
|
+
? "Profile details"
|
|
108
|
+
: activeTab === "security"
|
|
109
|
+
? "Security settings"
|
|
110
|
+
: "Preferences"}
|
|
88
111
|
</h2>
|
|
89
112
|
{/* <button className="text-gray-500 hover:text-gray-700">
|
|
90
113
|
{renderIcon(X)}
|
|
@@ -95,14 +118,14 @@ export const UserProfile = ({
|
|
|
95
118
|
<div className="space-y-5 md:space-y-6">
|
|
96
119
|
{/* Profile Section */}
|
|
97
120
|
<div>
|
|
98
|
-
<h3 className="text-sm font-medium mb-3 text-
|
|
121
|
+
<h3 className="text-sm font-medium mb-3 text-foreground">Profile</h3>
|
|
99
122
|
<div className="flex items-center justify-between">
|
|
100
123
|
<div className="flex items-center">
|
|
101
124
|
<Avatar className="h-12 w-12 mr-4 border">
|
|
102
125
|
<AvatarImage src={user.photos?.[0]?.value} alt="Profile picture" />
|
|
103
126
|
<AvatarFallback>{user.displayName?.split(" ").map((n: string) => n[0]).join("")}</AvatarFallback>
|
|
104
127
|
</Avatar>
|
|
105
|
-
<span className="font-medium text-
|
|
128
|
+
<span className="font-medium text-foreground">{user.displayName}</span>
|
|
106
129
|
</div>
|
|
107
130
|
{/* <Button variant="outline" size="sm">
|
|
108
131
|
Edit profile
|
|
@@ -112,7 +135,7 @@ export const UserProfile = ({
|
|
|
112
135
|
|
|
113
136
|
{/* Email Addresses Section */}
|
|
114
137
|
<div>
|
|
115
|
-
<h3 className="text-sm font-medium mb-3 text-
|
|
138
|
+
<h3 className="text-sm font-medium mb-3 text-foreground">Email addresses</h3>
|
|
116
139
|
<div className="space-y-2.5">
|
|
117
140
|
|
|
118
141
|
{/* {JSON.stringify(user)} */}
|
|
@@ -128,19 +151,72 @@ export const UserProfile = ({
|
|
|
128
151
|
</div>
|
|
129
152
|
))} */}
|
|
130
153
|
|
|
131
|
-
{user.emails.map((email: any, idx: number) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
{user.emails.map((email: any, idx: number) => {
|
|
155
|
+
const v = (user?.verifications || []).find((ve: any) => ve.email === email.value)
|
|
156
|
+
const isVerified = v?.verified === true
|
|
157
|
+
const codeInput = codeByEmail[email.value] || ""
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex items-start justify-between gap-2" key={email.value}>
|
|
160
|
+
<div className="flex flex-col">
|
|
161
|
+
<span className="text-foreground">{email.value}</span>
|
|
162
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
163
|
+
{isVerified ? "Verified" : "Not verified"}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
{idx === 0 && (
|
|
168
|
+
<Badge
|
|
169
|
+
variant="outline"
|
|
170
|
+
className="text-xs bg-muted text-foreground hover:bg-muted"
|
|
171
|
+
>
|
|
172
|
+
Primary
|
|
173
|
+
</Badge>
|
|
174
|
+
)}
|
|
175
|
+
{!isVerified && (
|
|
176
|
+
<>
|
|
177
|
+
{verifyingEmail === email.value ? (
|
|
178
|
+
<div className="flex items-center gap-1">
|
|
179
|
+
<input
|
|
180
|
+
className="h-7 w-24 text-sm rounded-md border border-border bg-background px-2 text-foreground"
|
|
181
|
+
placeholder="Code"
|
|
182
|
+
value={codeInput}
|
|
183
|
+
onChange={(e) => setCodeByEmail((m) => ({ ...m, [email.value]: e.target.value }))}
|
|
184
|
+
/>
|
|
185
|
+
<button
|
|
186
|
+
className="h-7 rounded-md border border-border px-2 text-xs"
|
|
187
|
+
onClick={async () => {
|
|
188
|
+
if (!onVerifyEmail) return
|
|
189
|
+
await onVerifyEmail(email.value, codeInput)
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
Verify
|
|
193
|
+
</button>
|
|
194
|
+
<button
|
|
195
|
+
className="h-7 rounded-md border border-border px-2 text-xs"
|
|
196
|
+
onClick={() => setVerifyingEmail(null)}
|
|
197
|
+
>
|
|
198
|
+
Cancel
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
) : (
|
|
202
|
+
<>
|
|
203
|
+
<button
|
|
204
|
+
className="h-7 rounded-md border border-border px-2 text-xs"
|
|
205
|
+
onClick={async () => {
|
|
206
|
+
if (onRequestEmailVerification) await onRequestEmailVerification(email.value)
|
|
207
|
+
setVerifyingEmail(email.value)
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
Send code
|
|
211
|
+
</button>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
})}
|
|
144
220
|
{/* <Button variant="ghost" size="sm" className="flex items-center text-gray-700">
|
|
145
221
|
{renderIcon(PlusCircle)}
|
|
146
222
|
Add email address
|
|
@@ -180,50 +256,70 @@ export const UserProfile = ({
|
|
|
180
256
|
</div>
|
|
181
257
|
</div>
|
|
182
258
|
</div>
|
|
183
|
-
) : (
|
|
259
|
+
) : activeTab === "security" ? (
|
|
184
260
|
<div className="space-y-5 md:space-y-6">
|
|
185
|
-
{/*
|
|
186
|
-
<div
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
<div>
|
|
191
|
-
<p className="font-medium text-gray-900 dark:text-gray-100">Two-factor authentication</p>
|
|
192
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">Add an extra layer of security to your account</p>
|
|
193
|
-
</div>
|
|
194
|
-
<Button variant="outline" size="sm">
|
|
195
|
-
Enable
|
|
196
|
-
</Button>
|
|
197
|
-
</div>
|
|
261
|
+
{/* Password row */}
|
|
262
|
+
<div className="border rounded-md overflow-hidden">
|
|
263
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
264
|
+
<div className="text-sm text-gray-700 dark:text-gray-300">Password</div>
|
|
265
|
+
<button className="text-sm text-indigo-600 hover:underline">Set password</button>
|
|
198
266
|
</div>
|
|
199
267
|
</div>
|
|
200
268
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
<div className="
|
|
204
|
-
<div className="
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
269
|
+
{/* Passkeys row */}
|
|
270
|
+
<div className="border rounded-md overflow-hidden">
|
|
271
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
272
|
+
<div className="text-sm text-gray-700 dark:text-gray-300">Passkeys</div>
|
|
273
|
+
<button className="text-sm text-indigo-600 hover:underline">+ Add a passkey</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* Two-step verification row */}
|
|
278
|
+
<div className="border rounded-md overflow-hidden">
|
|
279
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
280
|
+
<div className="text-sm text-gray-700 dark:text-gray-300">Two-step verification</div>
|
|
281
|
+
<button className="text-sm text-indigo-600 hover:underline">+ Add two-step verification</button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Active devices list (scaffold) */}
|
|
286
|
+
<div className="border rounded-md overflow-hidden">
|
|
287
|
+
<div className="px-4 py-3 border-b text-sm font-medium text-gray-900 dark:text-gray-100">Active devices</div>
|
|
288
|
+
<div className="p-4 space-y-3">
|
|
289
|
+
<div className="text-sm">
|
|
290
|
+
<div className="flex items-center gap-2">
|
|
291
|
+
<span className="inline-block h-5 w-5 rounded-sm bg-gray-900 dark:bg-white" />
|
|
292
|
+
<span className="font-medium">X11</span>
|
|
293
|
+
<span className="text-xs rounded-md border px-2 py-0.5 text-gray-600 dark:text-gray-300">This device</span>
|
|
208
294
|
</div>
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
</
|
|
295
|
+
<div className="text-gray-600 dark:text-gray-400 mt-1">Firefox 142.0</div>
|
|
296
|
+
<div className="text-gray-600 dark:text-gray-400">127.0.0.1 (Local), (Your City)</div>
|
|
297
|
+
<div className="text-gray-600 dark:text-gray-400">Today at 7:08 PM</div>
|
|
212
298
|
</div>
|
|
213
299
|
</div>
|
|
214
300
|
</div>
|
|
215
301
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<div className="
|
|
302
|
+
{/* Delete account */}
|
|
303
|
+
<div className="border rounded-md overflow-hidden">
|
|
304
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
305
|
+
<div className="text-sm text-gray-700 dark:text-gray-300">Delete account</div>
|
|
306
|
+
<button className="text-sm text-red-600 hover:underline">Delete account</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
) : (
|
|
311
|
+
<div className="space-y-5 md:space-y-6">
|
|
312
|
+
{/* Preferences */}
|
|
313
|
+
<div>
|
|
314
|
+
<h3 className="text-sm font-medium mb-3 text-gray-900 dark:text-gray-100">Preferences</h3>
|
|
315
|
+
<div className="space-y-3 text-sm">
|
|
219
316
|
<div className="flex items-center justify-between">
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
</Button>
|
|
317
|
+
<span className="text-gray-700 dark:text-gray-300">Locale</span>
|
|
318
|
+
<span className="text-gray-500 dark:text-gray-400">Auto</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="flex items-center justify-between">
|
|
321
|
+
<span className="text-gray-700 dark:text-gray-300">Theme</span>
|
|
322
|
+
<span className="text-gray-500 dark:text-gray-400">System</span>
|
|
227
323
|
</div>
|
|
228
324
|
</div>
|
|
229
325
|
</div>
|
|
@@ -5,7 +5,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
6
|
import type { ComponentType } from "react"
|
|
7
7
|
|
|
8
|
-
import { cn } from "
|
|
8
|
+
import { cn } from "../../lib/utils"
|
|
9
9
|
|
|
10
10
|
const CheckIcon = Check as ComponentType<React.SVGProps<SVGSVGElement>>
|
|
11
11
|
const CircleIcon = Circle as ComponentType<React.SVGProps<SVGSVGElement>>
|
package/src/index.ts
CHANGED
|
@@ -2,5 +2,6 @@ export {Button} from "./components/ui/button";
|
|
|
2
2
|
export { ClientOnly } from "./components/core/client-only";
|
|
3
3
|
export {Navbar} from "./components/core/navbar";
|
|
4
4
|
export {UserProfile} from "./components/core/user-profile";
|
|
5
|
+
export {UserDropdown} from "./components/core/user-dropdown";
|
|
5
6
|
export {PlaceholderAlert} from "./components/core/placeholder-alert";
|
|
6
7
|
export {TOTPValidator} from "./components/flow/totp-validator";
|
package/src/main.tsx
CHANGED
package/src/preview.tsx
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { UserDropdown } from "../components/core/user-dropdown"
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar"
|
|
4
|
+
|
|
5
|
+
const DemoTrigger = () => (
|
|
6
|
+
<span className="inline-flex items-center justify-center h-10 w-10 rounded-full border bg-white shadow">
|
|
7
|
+
<Avatar className="h-8 w-8 rounded-full">
|
|
8
|
+
<AvatarImage src="https://i.pravatar.cc/100" />
|
|
9
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
10
|
+
</Avatar>
|
|
11
|
+
</span>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const demoUser = {
|
|
15
|
+
displayName: "Jane Doe",
|
|
16
|
+
emails: [{ value: "jane.doe@example.com" }],
|
|
17
|
+
photos: [{ value: "https://i.pravatar.cc/100" }],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default { title: "Core/UserDropdown" }
|
|
21
|
+
|
|
22
|
+
export const Basic = () => (
|
|
23
|
+
<div className="p-10">
|
|
24
|
+
<UserDropdown
|
|
25
|
+
trigger={<DemoTrigger />}
|
|
26
|
+
user={demoUser}
|
|
27
|
+
onManageAccount={() => alert("Manage account")}
|
|
28
|
+
onSignout={() => alert("Sign out")}
|
|
29
|
+
links={[{ label: "My Organizations", href: "/organizations" }]}
|
|
30
|
+
side="bottom"
|
|
31
|
+
align="start"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|