@authdog/react-elements 0.0.12 → 0.0.14

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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +47 -23
  2. package/CHANGELOG.md +12 -0
  3. package/dist/components/ui/alert.js +3 -0
  4. package/dist/components/ui/alert.js.map +1 -0
  5. package/dist/components/ui/alert.mjs +3 -0
  6. package/dist/components/ui/alert.mjs.map +1 -0
  7. package/dist/components/ui/badge.js +3 -0
  8. package/dist/components/ui/badge.js.map +1 -0
  9. package/dist/components/ui/badge.mjs +3 -0
  10. package/dist/components/ui/badge.mjs.map +1 -0
  11. package/dist/components/ui/card.js +3 -0
  12. package/dist/components/ui/card.js.map +1 -0
  13. package/dist/components/ui/card.mjs +3 -0
  14. package/dist/components/ui/card.mjs.map +1 -0
  15. package/dist/components/ui/input.js +3 -0
  16. package/dist/components/ui/input.js.map +1 -0
  17. package/dist/components/ui/input.mjs +3 -0
  18. package/dist/components/ui/input.mjs.map +1 -0
  19. package/dist/components/ui/label.js +3 -0
  20. package/dist/components/ui/label.js.map +1 -0
  21. package/dist/components/ui/label.mjs +3 -0
  22. package/dist/components/ui/label.mjs.map +1 -0
  23. package/dist/components/ui/separator.js +3 -0
  24. package/dist/components/ui/separator.js.map +1 -0
  25. package/dist/components/ui/separator.mjs +3 -0
  26. package/dist/components/ui/separator.mjs.map +1 -0
  27. package/dist/index.d.mts +18 -1
  28. package/dist/index.d.ts +18 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/postcss.config.mjs +2 -0
  34. package/dist/styles.css +406 -6
  35. package/dist/tailwind.config.ts +5 -5
  36. package/ladle.config.mjs +21 -0
  37. package/package.json +16 -4
  38. package/postcss.config.mjs +2 -0
  39. package/src/components/core/navbar.tsx +44 -39
  40. package/src/components/core/user-profile.tsx +245 -0
  41. package/src/components/flow/login.tsx +158 -0
  42. package/src/components/ui/alert.tsx +66 -0
  43. package/src/components/ui/badge.tsx +46 -0
  44. package/src/components/ui/card.tsx +92 -0
  45. package/src/components/ui/input.tsx +21 -0
  46. package/src/components/ui/label.tsx +24 -0
  47. package/src/components/ui/separator.tsx +28 -0
  48. package/src/index.ts +3 -2
  49. package/src/main.tsx +11 -0
  50. package/src/preview.tsx +10 -0
  51. package/src/stories/Button.stories.tsx +24 -0
  52. package/src/stories/LoginForm.stories.tsx +29 -0
  53. package/src/stories/Navbar.stories.tsx +22 -0
  54. package/src/stories/UserProfile.stories.tsx +56 -0
  55. package/tailwind.config.ts +5 -5
package/package.json CHANGED
@@ -1,21 +1,29 @@
1
1
  {
2
2
  "name": "@authdog/react-elements",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
7
7
  "peerDependencies": {
8
- "react": "^18.3.1"
8
+ "react": "^19.1.0",
9
+ "react-dom": "^19.1.0"
9
10
  },
10
11
  "devDependencies": {
12
+ "@ladle/react": "^2.0.0",
11
13
  "@types/node": "^20",
12
- "@types/react": "^18.3.11",
14
+ "@types/react": "^19.1.0",
15
+ "@vitejs/plugin-react": "^4.4.1",
13
16
  "autoprefixer": "^10",
17
+ "css-loader": "^6.8.1",
14
18
  "postcss": "^8",
15
19
  "postcss-cli": "^11.0.1",
20
+ "postcss-import": "^16.1.0",
16
21
  "postcss-load-config": "^6",
22
+ "style-loader": "^3.3.3",
17
23
  "tailwindcss": "3.4.17",
24
+ "ts-loader": "^9.5.1",
18
25
  "typescript": "^5.6.3",
26
+ "webpack": "^5.89.0",
19
27
  "@authdog/eslint-config": "0.0.0",
20
28
  "@authdog/typescript-config": "0.0.0"
21
29
  },
@@ -23,6 +31,8 @@
23
31
  "@radix-ui/react-avatar": "^1.1.9",
24
32
  "@radix-ui/react-dialog": "^1.1.13",
25
33
  "@radix-ui/react-dropdown-menu": "^2.1.14",
34
+ "@radix-ui/react-label": "^2.1.6",
35
+ "@radix-ui/react-separator": "^1.1.6",
26
36
  "@radix-ui/react-slot": "^1.2.2",
27
37
  "class-variance-authority": "^0.7.0",
28
38
  "clsx": "^2.1.1",
@@ -60,6 +70,8 @@
60
70
  "ui": "pnpm dlx shadcn@latest",
61
71
  "lint": "eslint .",
62
72
  "build:styles": "postcss src/global.css -o dist/styles.css",
63
- "build": "pnpm tsup && pnpm build:styles"
73
+ "build": "pnpm tsup && pnpm build:styles",
74
+ "ladle": "ladle serve",
75
+ "ladle:build": "ladle build"
64
76
  }
65
77
  }
@@ -1,6 +1,8 @@
1
1
  /** @type {import('postcss-load-config').Config} */
2
2
  const config = {
3
3
  plugins: {
4
+ 'postcss-import': {},
5
+ 'tailwindcss/nesting': {},
4
6
  tailwindcss: {},
5
7
  autoprefixer: {},
6
8
  },
@@ -1,9 +1,9 @@
1
1
  "use client"
2
2
 
3
3
  import type React from "react"
4
-
5
- import { useState } from "react"
6
- import * as LucideIcons from "lucide-react"
4
+ import { useState, useEffect } from "react"
5
+ import { User, Settings, LogOut, Menu } from "lucide-react"
6
+ import type { LucideProps } from "lucide-react"
7
7
 
8
8
  import { cn } from "../../lib/utils"
9
9
  import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
@@ -56,33 +56,39 @@ export function Navbar({
56
56
  onLogout = () => console.log("Logout clicked"),
57
57
  }: NavbarProps) {
58
58
  const [open, setOpen] = useState(false)
59
- const UserIcon = LucideIcons.User as any
60
- const SettingsIcon = LucideIcons.Settings as any
61
- const LogOutIcon = LucideIcons.LogOut as any
62
- const MenuIcon = LucideIcons.Menu as any
59
+ const [isMounted, setIsMounted] = useState(false)
60
+
61
+ useEffect(() => {
62
+ setIsMounted(true)
63
+ }, [])
64
+
65
+ const iconProps: LucideProps = {
66
+ className: "mr-2 h-4 w-4",
67
+ "aria-hidden": "true"
68
+ }
69
+
70
+ const renderIcon = (Icon: any) => {
71
+ if (!isMounted) return null
72
+ return <Icon {...iconProps} />
73
+ }
63
74
 
64
75
  return (
65
76
  <header className={cn("border-b bg-background", className)}>
66
77
  <div className="container flex h-16 items-center justify-between px-4 md:px-6">
67
78
  <div className="flex items-center gap-4">
68
- {/* <Link href="/" className="flex items-center gap-2">
69
- <span className="text-xl font-bold">{logoText}</span>
70
- </Link> */}
79
+ <span className="text-xl font-bold">{logoText}</span>
71
80
  <nav className="hidden md:flex gap-6">
72
81
  {items?.map((item, index) => (
73
- // <Link
74
- // key={index}
75
- // href={item.href}
76
- // className={cn(
77
- // "text-sm font-medium transition-colors hover:text-primary",
78
- // item.disabled && "cursor-not-allowed opacity-80",
79
- // )}
80
- // >
81
- // {item.title}
82
- // </Link>
83
- <a href={item?.href}>
84
- {item.title}
85
- </a>
82
+ <a
83
+ key={index}
84
+ href={item.href}
85
+ className={cn(
86
+ "text-sm font-medium transition-colors hover:text-primary",
87
+ item.disabled && "cursor-not-allowed opacity-80"
88
+ )}
89
+ >
90
+ {item.title}
91
+ </a>
86
92
  ))}
87
93
  </nav>
88
94
  </div>
@@ -107,17 +113,17 @@ export function Navbar({
107
113
  <DropdownMenuSeparator />
108
114
  <DropdownMenuGroup>
109
115
  <DropdownMenuItem>
110
- <UserIcon className="mr-2 h-4 w-4" />
116
+ {renderIcon(User)}
111
117
  <span>Profile</span>
112
118
  </DropdownMenuItem>
113
119
  <DropdownMenuItem>
114
- <SettingsIcon className="mr-2 h-4 w-4" />
120
+ {renderIcon(Settings)}
115
121
  <span>Settings</span>
116
122
  </DropdownMenuItem>
117
123
  </DropdownMenuGroup>
118
124
  <DropdownMenuSeparator />
119
125
  <DropdownMenuItem onClick={onLogout}>
120
- <LogOutIcon className="mr-2 h-4 w-4" />
126
+ {renderIcon(LogOut)}
121
127
  <span>Log out</span>
122
128
  </DropdownMenuItem>
123
129
  </DropdownMenuContent>
@@ -125,24 +131,23 @@ export function Navbar({
125
131
  <Sheet open={open} onOpenChange={setOpen}>
126
132
  <SheetTrigger asChild>
127
133
  <Button variant="ghost" size="icon" className="md:hidden" aria-label="Open Menu">
128
- <MenuIcon className="h-5 w-5" />
134
+ {renderIcon(Menu)}
129
135
  </Button>
130
136
  </SheetTrigger>
131
137
  <SheetContent side="left" className="pr-0">
132
138
  <nav className="grid gap-2 py-6">
133
139
  {items?.map((item, index) => (
134
- // <Link
135
- // key={index}
136
- // href={item.href}
137
- // className={cn(
138
- // "flex w-full items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent",
139
- // item.disabled && "cursor-not-allowed opacity-80",
140
- // )}
141
- // onClick={() => setOpen(false)}
142
- // >
143
- // {item.title}
144
- // </Link>
145
- <a href="https://www.goo.bar" />
140
+ <a
141
+ key={index}
142
+ href={item.href}
143
+ className={cn(
144
+ "flex w-full items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent",
145
+ item.disabled && "cursor-not-allowed opacity-80"
146
+ )}
147
+ onClick={() => setOpen(false)}
148
+ >
149
+ {item.title}
150
+ </a>
146
151
  ))}
147
152
  </nav>
148
153
  </SheetContent>
@@ -0,0 +1,245 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
5
+ import { Button } from "../../components/ui/button"
6
+ import { Card } from "../../components/ui/card"
7
+ import { Badge } from "../../components/ui/badge"
8
+ import { PlusCircle, User, Shield, X, LucideProps } from "lucide-react"
9
+
10
+ export interface UserProfileProps {
11
+ user: {
12
+ name: string;
13
+ email: string;
14
+ image: string;
15
+ };
16
+ emails?: { address: string; isPrimary?: boolean }[];
17
+ connectedAccounts?: { provider: string; email: string }[];
18
+ }
19
+
20
+ export const UserProfile = ({
21
+ user,
22
+ emails = [],
23
+ connectedAccounts = [],
24
+ }: UserProfileProps) => {
25
+ const [isMounted, setIsMounted] = useState(false)
26
+ const [activeTab, setActiveTab] = useState<"profile" | "security">("profile");
27
+
28
+ useEffect(() => {
29
+ setIsMounted(true)
30
+ }, []);
31
+
32
+ const iconProps: LucideProps = {
33
+ className: "mr-2 h-4 w-4",
34
+ "aria-hidden": "true"
35
+ }
36
+
37
+ const renderIcon = (Icon: any) => {
38
+ if (!isMounted) return null
39
+ return <Icon {...iconProps} />
40
+ }
41
+
42
+ return (
43
+ <div className="grid grid-cols-[16rem,1fr] h-screen bg-gray-100">
44
+ <div className="h-full border-r p-6 bg-white flex flex-col min-w-0">
45
+ <div className="mb-6">
46
+ <h1 className="text-xl font-bold">Account</h1>
47
+ <p className="text-sm text-gray-500">Manage your account info.</p>
48
+ </div>
49
+
50
+ <nav className="space-y-1 flex-1">
51
+ <button
52
+ onClick={() => setActiveTab("profile")}
53
+ className={`flex items-center w-full px-3 py-2 text-sm rounded-md ${
54
+ activeTab === "profile" ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"
55
+ }`}
56
+ >
57
+ {renderIcon(User)}
58
+ Profile
59
+ </button>
60
+ <button
61
+ onClick={() => setActiveTab("security")}
62
+ className={`flex items-center w-full px-3 py-2 text-sm rounded-md ${
63
+ activeTab === "security" ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"
64
+ }`}
65
+ >
66
+ {renderIcon(Shield)}
67
+ Security
68
+ </button>
69
+ </nav>
70
+ </div>
71
+
72
+ <div className="h-full p-10 overflow-y-auto min-w-0 bg-white">
73
+ <div className="flex justify-between items-center mb-6">
74
+ <h2 className="text-xl font-semibold">
75
+ {activeTab === "profile" ? "Profile details" : "Security settings"}
76
+ </h2>
77
+ <button className="text-gray-500 hover:text-gray-700">
78
+ {renderIcon(X)}
79
+ </button>
80
+ </div>
81
+
82
+ {activeTab === "profile" ? (
83
+ <div className="space-y-8">
84
+ {/* Profile Section */}
85
+ <div>
86
+ <h3 className="text-sm font-medium mb-4">Profile</h3>
87
+ <div className="flex items-center justify-between">
88
+ <div className="flex items-center">
89
+ <Avatar className="h-12 w-12 mr-4 border">
90
+ <AvatarImage src={user.image} alt="Profile picture" />
91
+ <AvatarFallback>{user.name.split(" ").map(n => n[0]).join("")}</AvatarFallback>
92
+ </Avatar>
93
+ <span className="font-medium">{user.name}</span>
94
+ </div>
95
+ <Button variant="outline" size="sm">
96
+ Edit profile
97
+ </Button>
98
+ </div>
99
+ </div>
100
+
101
+ {/* Email Addresses Section */}
102
+ <div>
103
+ <h3 className="text-sm font-medium mb-4">Email addresses</h3>
104
+ <div className="space-y-3">
105
+ {(emails.length > 0 ? emails : [{ address: user.email, isPrimary: true }]).map((email, i) => (
106
+ <div className="flex items-center justify-between" key={email.address}>
107
+ <span>{email.address}</span>
108
+ {email.isPrimary && (
109
+ <Badge variant="outline" className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100">
110
+ Primary
111
+ </Badge>
112
+ )}
113
+ </div>
114
+ ))}
115
+ <Button variant="ghost" size="sm" className="flex items-center text-gray-700">
116
+ {renderIcon(PlusCircle)}
117
+ Add email address
118
+ </Button>
119
+ </div>
120
+ </div>
121
+
122
+ {/* Phone Number Section */}
123
+ <div>
124
+ <h3 className="text-sm font-medium mb-4">Phone number</h3>
125
+ <div className="space-y-3">
126
+ <div className="flex items-center justify-between">
127
+ <span>+1 (555) 123-4567</span>
128
+ <Badge variant="outline" className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100">
129
+ Primary
130
+ </Badge>
131
+ </div>
132
+ <Button variant="ghost" size="sm" className="flex items-center text-gray-700">
133
+ {renderIcon(PlusCircle)}
134
+ Add phone number
135
+ </Button>
136
+ </div>
137
+ </div>
138
+
139
+ {/* Connected Accounts Section */}
140
+ <div>
141
+ <h3 className="text-sm font-medium mb-4">Connected accounts</h3>
142
+ <div className="space-y-3">
143
+ {connectedAccounts.length > 0 ? connectedAccounts.map((acc, i) => (
144
+ <div className="flex items-center justify-between" key={acc.provider + acc.email}>
145
+ <div className="flex items-center">
146
+ <div className="mr-2">
147
+ <span>{acc.provider}</span>
148
+ </div>
149
+ <span>{acc.provider}</span>
150
+ </div>
151
+ <span className="text-sm text-gray-500">{acc.email}</span>
152
+ </div>
153
+ )) : (
154
+ <div className="flex items-center justify-between">
155
+ <div className="flex items-center">
156
+ <div className="mr-2">
157
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
158
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
159
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
160
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
161
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
162
+ </svg>
163
+ </div>
164
+ <span>Google</span>
165
+ </div>
166
+ <span className="text-sm text-gray-500">{user.email}</span>
167
+ </div>
168
+ )}
169
+ <Button variant="ghost" size="sm" className="flex items-center text-gray-700">
170
+ {renderIcon(PlusCircle)}
171
+ Connect account
172
+ </Button>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ ) : (
177
+ <div className="space-y-8">
178
+ {/* Security Settings */}
179
+ <div>
180
+ <h3 className="text-sm font-medium mb-4">Two-factor authentication</h3>
181
+ <div className="space-y-3">
182
+ <div className="flex items-center justify-between">
183
+ <div>
184
+ <p className="font-medium">Two-factor authentication</p>
185
+ <p className="text-sm text-gray-500">Add an extra layer of security to your account</p>
186
+ </div>
187
+ <Button variant="outline" size="sm">
188
+ Enable
189
+ </Button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <div>
195
+ <h3 className="text-sm font-medium mb-4">Password</h3>
196
+ <div className="space-y-3">
197
+ <div className="flex items-center justify-between">
198
+ <div>
199
+ <p className="font-medium">Change password</p>
200
+ <p className="text-sm text-gray-500">Last changed 3 months ago</p>
201
+ </div>
202
+ <Button variant="outline" size="sm">
203
+ Change
204
+ </Button>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <div>
210
+ <h3 className="text-sm font-medium mb-4">Active sessions</h3>
211
+ <div className="space-y-3">
212
+ <div className="flex items-center justify-between">
213
+ <div>
214
+ <p className="font-medium">Current session</p>
215
+ <p className="text-sm text-gray-500">Chrome on Windows • Active now</p>
216
+ </div>
217
+ <Button variant="outline" size="sm">
218
+ Sign out
219
+ </Button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )}
225
+ </div>
226
+
227
+ <div className="absolute bottom-4 text-xs text-gray-500 flex items-center">
228
+ Secured by
229
+ <span className="ml-1 font-medium flex items-center">
230
+ <svg
231
+ width="14"
232
+ height="14"
233
+ viewBox="0 0 16 16"
234
+ fill="none"
235
+ xmlns="http://www.w3.org/2000/svg"
236
+ className="mr-1"
237
+ >
238
+ <path d="M8 0L14.9282 4V12L8 16L1.07179 12V4L8 0Z" fill="#6C47FF" />
239
+ </svg>
240
+ Authdog
241
+ </span>
242
+ </div>
243
+ </div>
244
+ )
245
+ }
@@ -0,0 +1,158 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState } from "react"
6
+ import { Button } from "../../components/ui/button"
7
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../../components/ui/card"
8
+ import { Input } from "../../components/ui/input"
9
+ import { Label } from "../../components/ui/label"
10
+ import { Separator } from "../../components/ui/separator"
11
+ import { Github, AlertCircle } from "lucide-react"
12
+ import { Alert, AlertDescription } from "../../components/ui/alert"
13
+
14
+ export const LoginForm = () => {
15
+ const [email, setEmail] = useState("")
16
+ const [password, setPassword] = useState("")
17
+ const [error, setError] = useState("")
18
+ const [isLoading, setIsLoading] = useState(false)
19
+
20
+ const handleEmailLogin = async (e: React.FormEvent) => {
21
+ e.preventDefault()
22
+ setIsLoading(true)
23
+ setError("")
24
+
25
+ try {
26
+ // Simulate API call
27
+ await new Promise((resolve) => setTimeout(resolve, 1000))
28
+
29
+ // Redirect after successful login
30
+ // router.push('/dashboard')
31
+ } catch (err) {
32
+ setError("Invalid email or password")
33
+ } finally {
34
+ setIsLoading(false)
35
+ }
36
+ }
37
+
38
+ const handleOAuthLogin = async (provider: string) => {
39
+ setIsLoading(true)
40
+ setError("")
41
+
42
+ try {
43
+
44
+ await new Promise((resolve) => setTimeout(resolve, 1000))
45
+ } catch (err) {
46
+ setError(`Failed to login with ${provider}`)
47
+ } finally {
48
+ setIsLoading(false)
49
+ }
50
+ }
51
+
52
+ return (
53
+ <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
54
+ <Card className="w-full max-w-md">
55
+ <CardHeader className="space-y-1">
56
+ <CardTitle className="text-2xl font-bold text-center">Login</CardTitle>
57
+ <CardDescription className="text-center">Choose your preferred login method</CardDescription>
58
+ </CardHeader>
59
+ <CardContent className="space-y-4">
60
+ {error && (
61
+ <Alert variant="destructive">
62
+ <AlertCircle className="h-4 w-4" />
63
+ <AlertDescription>{error}</AlertDescription>
64
+ </Alert>
65
+ )}
66
+
67
+ <div className="space-y-2">
68
+ <Button
69
+ variant="outline"
70
+ className="w-full"
71
+ onClick={() => handleOAuthLogin("google")}
72
+ disabled={isLoading}
73
+ >
74
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
75
+ <path
76
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
77
+ fill="#4285F4"
78
+ />
79
+ <path
80
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
81
+ fill="#34A853"
82
+ />
83
+ <path
84
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
85
+ fill="#FBBC05"
86
+ />
87
+ <path
88
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
89
+ fill="#EA4335"
90
+ />
91
+ <path d="M1 1h22v22H1z" fill="none" />
92
+ </svg>
93
+ Continue with Google
94
+ </Button>
95
+
96
+ <Button
97
+ variant="outline"
98
+ className="w-full"
99
+ onClick={() => handleOAuthLogin("github")}
100
+ disabled={isLoading}
101
+ >
102
+ <Github className="mr-2 h-4 w-4" />
103
+ Continue with GitHub
104
+ </Button>
105
+ </div>
106
+
107
+ <div className="flex items-center">
108
+ <Separator className="flex-1" />
109
+ <span className="mx-2 text-xs text-muted-foreground">OR</span>
110
+ <Separator className="flex-1" />
111
+ </div>
112
+
113
+ <form onSubmit={handleEmailLogin} className="space-y-4">
114
+ <div className="space-y-2">
115
+ <Label htmlFor="email">Email</Label>
116
+ <Input
117
+ id="email"
118
+ type="email"
119
+ placeholder="m@example.com"
120
+ value={email}
121
+ onChange={(e) => setEmail(e.target.value)}
122
+ required
123
+ disabled={isLoading}
124
+ />
125
+ </div>
126
+ <div className="space-y-2">
127
+ <div className="flex items-center justify-between">
128
+ <Label htmlFor="password">Password</Label>
129
+ <a href="/forgot-password" className="text-xs text-primary hover:underline">
130
+ Forgot password?
131
+ </a>
132
+ </div>
133
+ <Input
134
+ id="password"
135
+ type="password"
136
+ value={password}
137
+ onChange={(e) => setPassword(e.target.value)}
138
+ required
139
+ disabled={isLoading}
140
+ />
141
+ </div>
142
+ <Button type="submit" className="w-full" disabled={isLoading}>
143
+ {isLoading ? "Loading..." : "Sign in with Email"}
144
+ </Button>
145
+ </form>
146
+ </CardContent>
147
+ <CardFooter className="flex justify-center">
148
+ <p className="text-xs text-muted-foreground">
149
+ Don't have an account?{" "}
150
+ <a href="/signup" className="text-primary hover:underline">
151
+ Sign up
152
+ </a>
153
+ </p>
154
+ </CardFooter>
155
+ </Card>
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@authdog/react-elements/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }