@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authdog/react-elements",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -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-gray-900 dark:text-gray-100">Account</h1>
57
- <p className="text-sm text-gray-500 dark:text-gray-400">Manage your account info.</p>
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-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100"
66
- : "text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
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
- {/* <button
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" ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"
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-gray-900 dark:text-gray-100">
87
- {activeTab === "profile" ? "Profile details" : "Security settings"}
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-gray-900 dark:text-gray-100">Profile</h3>
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-gray-900 dark:text-gray-100">{user.displayName}</span>
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-gray-900 dark:text-gray-100">Email addresses</h3>
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
- <div className="flex items-center justify-between" key={email.value}>
133
- <span className="text-gray-900 dark:text-gray-100">{email.value}</span>
134
- {idx === 0 && (
135
- <Badge
136
- variant="outline"
137
- className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-800"
138
- >
139
- Primary
140
- </Badge>
141
- )}
142
- </div>
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
- {/* Security Settings */}
186
- <div key="two-factor">
187
- <h3 className="text-sm font-medium mb-3 text-gray-900 dark:text-gray-100">Two-factor authentication</h3>
188
- <div className="space-y-2.5">
189
- <div className="flex items-center justify-between">
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
- <div key="password">
202
- <h3 className="text-sm font-medium mb-3 text-gray-900 dark:text-gray-100">Password</h3>
203
- <div className="space-y-2.5">
204
- <div className="flex items-center justify-between">
205
- <div>
206
- <p className="font-medium text-gray-900 dark:text-gray-100">Change password</p>
207
- <p className="text-sm text-gray-500 dark:text-gray-400">Last changed 3 months ago</p>
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">+&nbsp;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">+&nbsp;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
- <Button variant="outline" size="sm">
210
- Change
211
- </Button>
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
- <div key="sessions">
217
- <h3 className="text-sm font-medium mb-3 text-gray-900 dark:text-gray-100">Active sessions</h3>
218
- <div className="space-y-2.5">
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
- <div>
221
- <p className="font-medium text-gray-900 dark:text-gray-100">Current session</p>
222
- <p className="text-sm text-gray-500 dark:text-gray-400">Chrome on Windows • Active now</p>
223
- </div>
224
- <Button variant="outline" size="sm">
225
- Sign out
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 "@authdog/react-elements/lib/utils"
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
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import '../dist/styles.css';
2
+ import './global.css';
3
3
 
4
4
  // This is the main entry point for Ladle
5
5
  export default function Main({ children }: { children: React.ReactNode }) {
package/src/preview.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import './global.css';
2
3
 
3
4
  // This is the preview component for Ladle
4
5
  export default function Preview({ children }: { children: React.ReactNode }) {
@@ -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
+