@chesapeake/stack 0.1.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +190 -0
  3. package/README.md +440 -0
  4. package/components/ui/avatar.tsx +87 -0
  5. package/components/ui/badge.tsx +49 -0
  6. package/components/ui/badgeVariants.ts +26 -0
  7. package/components/ui/button-group.tsx +67 -0
  8. package/components/ui/button.tsx +70 -0
  9. package/components/ui/buttonGroupVariants.ts +23 -0
  10. package/components/ui/buttonVariants.ts +38 -0
  11. package/components/ui/calendar.tsx +220 -0
  12. package/components/ui/card.tsx +162 -0
  13. package/components/ui/dialog.tsx +290 -0
  14. package/components/ui/drawer.tsx +227 -0
  15. package/components/ui/dropdown-menu.tsx +260 -0
  16. package/components/ui/index.ts +86 -0
  17. package/components/ui/input.tsx +59 -0
  18. package/components/ui/label.tsx +47 -0
  19. package/components/ui/location-input.md +194 -0
  20. package/components/ui/location-input.tsx +384 -0
  21. package/components/ui/popover.tsx +110 -0
  22. package/components/ui/select.tsx +306 -0
  23. package/components/ui/separator.tsx +50 -0
  24. package/components/ui/sonner-api.ts +8 -0
  25. package/components/ui/sonner.tsx +69 -0
  26. package/components/ui/textarea.tsx +53 -0
  27. package/components/ui/tooltip.tsx +125 -0
  28. package/components/ux/ActivityAnimation.tsx +135 -0
  29. package/components/ux/EmptyState.tsx +22 -0
  30. package/components/ux/FeedbackDrawer.tsx +141 -0
  31. package/components/ux/FireAnimation.tsx +189 -0
  32. package/components/ux/FriendList.tsx +111 -0
  33. package/components/ux/FriendRequestList.tsx +104 -0
  34. package/components/ux/GifSearch.tsx +264 -0
  35. package/components/ux/ImageUpload.tsx +829 -0
  36. package/components/ux/SelectedUsersList.tsx +91 -0
  37. package/components/ux/StandardLayout/PageLayout.tsx +107 -0
  38. package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
  39. package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
  40. package/components/ux/StandardLayout/index.ts +22 -0
  41. package/components/ux/TutorialCarousel.tsx +128 -0
  42. package/components/ux/UserSearch.tsx +324 -0
  43. package/components/ux/UserSelector.tsx +116 -0
  44. package/components/ux/auth/AuthSelector.tsx +154 -0
  45. package/components/ux/auth/MultiAuthProvider.tsx +196 -0
  46. package/components/ux/auth/README.md +317 -0
  47. package/components/ux/auth/context.ts +46 -0
  48. package/components/ux/auth/github/AuthCallback.tsx +177 -0
  49. package/components/ux/auth/github/AuthProvider.tsx +111 -0
  50. package/components/ux/auth/github/README.md +208 -0
  51. package/components/ux/auth/github/client.ts +127 -0
  52. package/components/ux/auth/github/config.ts +68 -0
  53. package/components/ux/auth/github/context.ts +34 -0
  54. package/components/ux/auth/github/default-client.ts +74 -0
  55. package/components/ux/auth/github/index.ts +43 -0
  56. package/components/ux/auth/github/types.ts +44 -0
  57. package/components/ux/auth/github/useGitHubAuth.ts +16 -0
  58. package/components/ux/auth/google/AuthCallback.tsx +170 -0
  59. package/components/ux/auth/google/AuthProvider.tsx +108 -0
  60. package/components/ux/auth/google/README.md +450 -0
  61. package/components/ux/auth/google/client.ts +109 -0
  62. package/components/ux/auth/google/config.ts +65 -0
  63. package/components/ux/auth/google/context.ts +34 -0
  64. package/components/ux/auth/google/default-client.ts +69 -0
  65. package/components/ux/auth/google/index.ts +43 -0
  66. package/components/ux/auth/google/types.ts +42 -0
  67. package/components/ux/auth/google/useGoogleAuth.ts +16 -0
  68. package/components/ux/auth/index.ts +72 -0
  69. package/components/ux/auth/useAuth.ts +16 -0
  70. package/components/ux/index.ts +46 -0
  71. package/components/ux/theme-provider-context.ts +20 -0
  72. package/components/ux/theme-provider.tsx +114 -0
  73. package/components/ux/useTheme.ts +42 -0
  74. package/index.ts +29 -0
  75. package/lib/utils.ts +57 -0
  76. package/package.json +170 -0
  77. package/styles/base.css +38 -0
  78. package/styles/components.css +54 -0
  79. package/styles/index.css +7 -0
  80. package/styles/tokens.css +189 -0
  81. package/tailwind.preset.ts +169 -0
  82. package/types/user.ts +69 -0
@@ -0,0 +1,324 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+
6
+ import { useCallback, useEffect, useRef, useState } from "react";
7
+ import { Input } from "../ui/input.tsx";
8
+ import { Button } from "../ui/button.tsx";
9
+ import { Label } from "../ui/label.tsx";
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.tsx";
11
+ import type { BaseUser } from "../../types/user.ts";
12
+
13
+ /**
14
+ * Props for the UserSearch component
15
+ */
16
+ export interface UserSearchProps {
17
+ /** Callback when a user is selected from search results */
18
+ onUserSelect: (user: BaseUser) => void;
19
+ /** Array of user IDs to exclude from search results */
20
+ excludeUserIds?: string[];
21
+ /** Current user's ID (excluded from results) */
22
+ currentUserId?: string;
23
+ /** API endpoint for searching users. Should accept a 'q' query parameter */
24
+ searchEndpoint?: string;
25
+ /** Placeholder text for the search input */
26
+ placeholder?: string;
27
+ /** Minimum characters required before searching. @default 2 */
28
+ minQueryLength?: number;
29
+ /** Debounce delay in milliseconds. @default 300 */
30
+ debounceMs?: number;
31
+ /** Label displayed above the search input */
32
+ label?: string;
33
+ /** Whether to show an "Add" button next to results */
34
+ showAddButton?: boolean;
35
+ /** Function to check if a user is already selected */
36
+ isSelected?: (userId: string) => boolean;
37
+ /** Label displayed for selected users. @default "Selected" */
38
+ selectedLabel?: string;
39
+ }
40
+
41
+ /**
42
+ * A searchable input component for finding and selecting users.
43
+ *
44
+ * This component provides a search interface with debounced API calls,
45
+ * displaying results in a dropdown. Supports excluding users, marking
46
+ * selected users, and customizable appearance.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // Basic usage
51
+ * <UserSearch
52
+ * onUserSelect={(user) => console.log("Selected:", user)}
53
+ * searchEndpoint="/api/users/search"
54
+ * placeholder="Search users..."
55
+ * />
56
+ *
57
+ * // With exclusions and selected state
58
+ * <UserSearch
59
+ * onUserSelect={handleSelect}
60
+ * searchEndpoint="/api/users/search"
61
+ * currentUserId="123"
62
+ * excludeUserIds={["456", "789"]}
63
+ * isSelected={(id) => selectedIds.includes(id)}
64
+ * showAddButton
65
+ * />
66
+ *
67
+ * // In a form with label
68
+ * <UserSearch
69
+ * label="Add Team Member"
70
+ * onUserSelect={addMember}
71
+ * searchEndpoint="/api/users/search"
72
+ * minQueryLength={3}
73
+ * debounceMs={500}
74
+ * />
75
+ * ```
76
+ *
77
+ * @sideeffect
78
+ * - Makes fetch requests to searchEndpoint
79
+ * - Adds/removes document event listeners for click-outside handling
80
+ * - Uses setTimeout for debouncing
81
+ */
82
+ export function UserSearch({
83
+ onUserSelect,
84
+ excludeUserIds,
85
+ currentUserId,
86
+ searchEndpoint,
87
+ placeholder,
88
+ minQueryLength,
89
+ debounceMs,
90
+ label,
91
+ showAddButton,
92
+ isSelected,
93
+ selectedLabel,
94
+ }: UserSearchProps) {
95
+ const [searchQuery, setSearchQuery] = useState("");
96
+ const [searchResults, setSearchResults] = useState<BaseUser[]>([]);
97
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
98
+ const [isLoading, setIsLoading] = useState(false);
99
+ const searchRef = useRef<HTMLDivElement>(null);
100
+ const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
101
+ undefined
102
+ );
103
+
104
+ const performSearch = useCallback(
105
+ async (query: string) => {
106
+ const minLen = minQueryLength ?? 2;
107
+ if (query.length < minLen) {
108
+ setSearchResults([]);
109
+ return;
110
+ }
111
+
112
+ const endpoint = searchEndpoint;
113
+ if (!endpoint) {
114
+ setSearchResults([]);
115
+ return;
116
+ }
117
+
118
+ setIsLoading(true);
119
+ try {
120
+ const url = new URL(endpoint, globalThis.location.origin);
121
+ url.searchParams.set("q", query);
122
+
123
+ const headers: HeadersInit = {};
124
+ if (currentUserId) {
125
+ headers["X-User-ID"] = currentUserId;
126
+ }
127
+
128
+ const response = await fetch(url.toString(), {
129
+ headers,
130
+ credentials: "include", // Include cookies for authentication
131
+ });
132
+ if (!response.ok) {
133
+ throw new Error("Failed to search users");
134
+ }
135
+
136
+ const data = (await response.json()) as { value?: BaseUser[] };
137
+ const users = data.value ?? [];
138
+
139
+ // Filter out excluded users
140
+ const excludeIds = excludeUserIds ?? [];
141
+ const filtered = users.filter(
142
+ (u) => u.id !== currentUserId && !excludeIds.includes(u.id)
143
+ );
144
+
145
+ setSearchResults(filtered);
146
+ } catch (error) {
147
+ console.error("Error searching users:", error);
148
+ setSearchResults([]);
149
+ } finally {
150
+ setIsLoading(false);
151
+ }
152
+ },
153
+ [searchEndpoint, currentUserId, excludeUserIds, minQueryLength]
154
+ );
155
+
156
+ const handleSearch = useCallback(
157
+ (query: string) => {
158
+ setSearchQuery(query);
159
+ setIsSearchOpen(true);
160
+
161
+ // Clear previous timeout
162
+ if (debounceTimeoutRef.current) {
163
+ clearTimeout(debounceTimeoutRef.current);
164
+ }
165
+
166
+ // Set new timeout for debounced search
167
+ debounceTimeoutRef.current = setTimeout(() => {
168
+ void performSearch(query);
169
+ }, debounceMs);
170
+ },
171
+ [performSearch, debounceMs]
172
+ );
173
+
174
+ const handleUserClick = useCallback(
175
+ (user: BaseUser) => {
176
+ onUserSelect(user);
177
+ setSearchQuery("");
178
+ setSearchResults([]);
179
+ setIsSearchOpen(false);
180
+ },
181
+ [onUserSelect]
182
+ );
183
+
184
+ // Close search when clicking outside
185
+ useEffect(() => {
186
+ const handleClickOutside = (event: MouseEvent) => {
187
+ if (
188
+ searchRef.current &&
189
+ !searchRef.current.contains(event.target as Node)
190
+ ) {
191
+ setTimeout(() => {
192
+ setSearchQuery("");
193
+ setSearchResults([]);
194
+ setIsSearchOpen(false);
195
+ }, 200);
196
+ }
197
+ };
198
+
199
+ document.addEventListener("mousedown", handleClickOutside);
200
+ return () => {
201
+ document.removeEventListener("mousedown", handleClickOutside);
202
+ if (debounceTimeoutRef.current) {
203
+ clearTimeout(debounceTimeoutRef.current);
204
+ }
205
+ };
206
+ }, []);
207
+
208
+ const userIsSelected = (userId: string) => {
209
+ return isSelected ? isSelected(userId) : false;
210
+ };
211
+
212
+ return (
213
+ <div className="relative" ref={searchRef}>
214
+ {label && <Label htmlFor="user-search">{label}</Label>}
215
+ <div className="relative">
216
+ <Input
217
+ id="user-search"
218
+ value={searchQuery}
219
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
220
+ handleSearch(e.currentTarget.value);
221
+ }}
222
+ placeholder={placeholder}
223
+ autoComplete="off"
224
+ onFocus={() => {
225
+ setIsSearchOpen(true);
226
+ const minLen = minQueryLength ?? 2;
227
+ if (searchQuery.length >= minLen) {
228
+ void performSearch(searchQuery);
229
+ }
230
+ }}
231
+ />
232
+ {isSearchOpen && (
233
+ <Button
234
+ type="button"
235
+ variant="ghost"
236
+ size="sm"
237
+ className="absolute right-2 top-1/2 -translate-y-1/2"
238
+ onClick={() => {
239
+ setSearchQuery("");
240
+ setSearchResults([]);
241
+ setIsSearchOpen(false);
242
+ }}
243
+ >
244
+ ✕
245
+ </Button>
246
+ )}
247
+ </div>
248
+
249
+ {isSearchOpen && (
250
+ <div className="absolute z-10 w-full mt-1 bg-background border rounded-lg shadow-lg max-h-96 overflow-y-auto">
251
+ {isLoading ? (
252
+ <div className="p-4 text-center text-muted-foreground">
253
+ Searching...
254
+ </div>
255
+ ) : searchResults.length > 0 ? (
256
+ searchResults.map((user) => (
257
+ <div
258
+ key={user.id}
259
+ className="flex items-center justify-between p-2 border-b last:border-b-0 hover:bg-muted/50 cursor-pointer"
260
+ onClick={() => {
261
+ handleUserClick(user);
262
+ }}
263
+ >
264
+ <div className="flex items-center gap-3 flex-1 min-w-0">
265
+ {user.profilePicture && (
266
+ <Tooltip>
267
+ <TooltipTrigger asChild>
268
+ <img
269
+ src={user.profilePicture}
270
+ alt={user.displayName}
271
+ className="w-8 h-8 rounded-full object-cover"
272
+ />
273
+ </TooltipTrigger>
274
+ <TooltipContent>
275
+ <p>{user.displayName}</p>
276
+ {user.email && (
277
+ <p className="text-xs text-muted-foreground">
278
+ {user.email}
279
+ </p>
280
+ )}
281
+ </TooltipContent>
282
+ </Tooltip>
283
+ )}
284
+ <div className="flex-1 min-w-0">
285
+ <div className="font-medium truncate">
286
+ {user.displayName}
287
+ </div>
288
+ {user.email && (
289
+ <div className="text-sm text-muted-foreground truncate">
290
+ {user.email}
291
+ </div>
292
+ )}
293
+ </div>
294
+ </div>
295
+ {userIsSelected(user.id) ? (
296
+ <div className="px-3 py-1 text-sm text-primary bg-primary/10 rounded-full flex-shrink-0">
297
+ {selectedLabel}
298
+ </div>
299
+ ) : showAddButton ? (
300
+ <Button
301
+ type="button"
302
+ variant="default"
303
+ size="sm"
304
+ onClick={(e) => {
305
+ e.stopPropagation();
306
+ handleUserClick(user);
307
+ }}
308
+ className="flex-shrink-0"
309
+ >
310
+ Add
311
+ </Button>
312
+ ) : null}
313
+ </div>
314
+ ))
315
+ ) : searchQuery.length >= (minQueryLength ?? 2) ? (
316
+ <div className="p-4 text-center text-muted-foreground">
317
+ No users found
318
+ </div>
319
+ ) : null}
320
+ </div>
321
+ )}
322
+ </div>
323
+ );
324
+ }
@@ -0,0 +1,116 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useCallback, useState } from "react";
6
+ import { UserSearch } from "./UserSearch.tsx";
7
+ import { type SelectedUser, SelectedUsersList } from "./SelectedUsersList.tsx";
8
+ import type { BaseUser } from "../../types/user.ts";
9
+
10
+ export interface UserSelectorProps {
11
+ selectedUsers: BaseUser[];
12
+ onUsersChange: (users: BaseUser[]) => void;
13
+ excludeUserIds?: string[];
14
+ currentUserId?: string;
15
+ searchEndpoint?: string;
16
+ placeholder?: string;
17
+ minQueryLength?: number;
18
+ debounceMs?: number;
19
+ showStatus?: boolean;
20
+ maxHeight?: string;
21
+ emptyMessage?: string;
22
+ searchLabel?: string;
23
+ listTitle?: string;
24
+ selectedLabel?: string;
25
+ }
26
+
27
+ export function UserSelector({
28
+ selectedUsers,
29
+ onUsersChange,
30
+ excludeUserIds,
31
+ currentUserId,
32
+ searchEndpoint,
33
+ placeholder,
34
+ minQueryLength,
35
+ debounceMs,
36
+ showStatus,
37
+ maxHeight,
38
+ emptyMessage,
39
+ searchLabel,
40
+ listTitle,
41
+ selectedLabel,
42
+ }: UserSelectorProps) {
43
+ const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(
44
+ new Set(selectedUsers.map((u) => u.id))
45
+ );
46
+
47
+ const handleUserSelect = useCallback(
48
+ (user: BaseUser) => {
49
+ if (selectedUserIds.has(user.id)) {
50
+ return; // Already selected
51
+ }
52
+
53
+ const newUsers = [...selectedUsers, user];
54
+ const newIds = new Set(selectedUserIds);
55
+ newIds.add(user.id);
56
+ setSelectedUserIds(newIds);
57
+ onUsersChange(newUsers);
58
+ },
59
+ [selectedUsers, selectedUserIds, onUsersChange]
60
+ );
61
+
62
+ const handleRemove = useCallback(
63
+ (userId: string) => {
64
+ const newUsers = selectedUsers.filter((u) => u.id !== userId);
65
+ const newIds = new Set(selectedUserIds);
66
+ newIds.delete(userId);
67
+ setSelectedUserIds(newIds);
68
+ onUsersChange(newUsers);
69
+ },
70
+ [selectedUsers, selectedUserIds, onUsersChange]
71
+ );
72
+
73
+ const isSelected = useCallback(
74
+ (userId: string) => selectedUserIds.has(userId),
75
+ [selectedUserIds]
76
+ );
77
+
78
+ // Convert BaseUser to SelectedUser format
79
+ const selectedUsersList: SelectedUser[] = selectedUsers.map((u) => ({
80
+ userId: u.id,
81
+ displayName: u.displayName,
82
+ email: u.email,
83
+ profilePicture: u.profilePicture,
84
+ }));
85
+
86
+ // Combine excludeUserIds with selected user IDs
87
+ const allExcludedIds = [
88
+ ...(excludeUserIds ?? []),
89
+ ...Array.from(selectedUserIds),
90
+ ];
91
+
92
+ return (
93
+ <div className="space-y-4">
94
+ <UserSearch
95
+ onUserSelect={handleUserSelect}
96
+ excludeUserIds={allExcludedIds}
97
+ currentUserId={currentUserId}
98
+ searchEndpoint={searchEndpoint}
99
+ placeholder={placeholder}
100
+ minQueryLength={minQueryLength}
101
+ debounceMs={debounceMs}
102
+ label={searchLabel}
103
+ isSelected={isSelected}
104
+ selectedLabel={selectedLabel}
105
+ />
106
+ <SelectedUsersList
107
+ users={selectedUsersList}
108
+ onRemove={handleRemove}
109
+ showStatus={showStatus}
110
+ maxHeight={maxHeight}
111
+ emptyMessage={emptyMessage}
112
+ title={listTitle}
113
+ />
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,154 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import React from "react";
6
+ import { Button } from "../../ui/button";
7
+ import { useAuth } from "./useAuth.ts";
8
+ import type { AuthUser } from "./context.ts";
9
+
10
+ export interface AuthSelectorProps {
11
+ /** Enable Google OAuth provider */
12
+ enableGoogle?: boolean;
13
+ /** Enable GitHub OAuth provider */
14
+ enableGithub?: boolean;
15
+ /** Custom styling/className */
16
+ className?: string;
17
+ /** Callback when user successfully authenticates */
18
+ onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
19
+ /** Custom button components (optional) */
20
+ googleButton?: React.ReactNode;
21
+ githubButton?: React.ReactNode;
22
+ }
23
+
24
+ /**
25
+ * AuthSelector component for choosing authentication provider
26
+ * Renders buttons for enabled providers and disables them when authenticated
27
+ */
28
+ export function AuthSelector({
29
+ enableGoogle,
30
+ enableGithub,
31
+ className,
32
+ onAuthSuccess,
33
+ googleButton,
34
+ githubButton,
35
+ }: AuthSelectorProps) {
36
+ const {
37
+ user,
38
+ loading,
39
+ provider,
40
+ signInWithGoogle,
41
+ signInWithGitHub,
42
+ signOut,
43
+ } = useAuth();
44
+
45
+ // Validate that user is actually a valid user object, not just truthy
46
+ const isAuthenticated = !!(
47
+ user &&
48
+ typeof user === "object" &&
49
+ Object.keys(user).length > 0
50
+ );
51
+ const hasMultipleProviders = enableGoogle && enableGithub;
52
+
53
+ // Call onAuthSuccess when user becomes authenticated
54
+ React.useEffect(() => {
55
+ if (user && provider && onAuthSuccess) {
56
+ onAuthSuccess(user, provider);
57
+ }
58
+ }, [user, provider, onAuthSuccess]);
59
+
60
+ if (loading) {
61
+ return (
62
+ <div className={`flex items-center justify-center ${className ?? ""}`}>
63
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (isAuthenticated) {
69
+ return (
70
+ <div className={`flex flex-col gap-2 ${className ?? ""}`}>
71
+ <div className="text-sm text-muted-foreground">
72
+ Signed in with {provider === "google" ? "Google" : "GitHub"}
73
+ </div>
74
+ <Button onClick={signOut} variant="outline" size="sm">
75
+ Sign Out
76
+ </Button>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <div className={`flex flex-col gap-2 ${className ?? ""}`}>
83
+ {hasMultipleProviders && (
84
+ <p className="text-sm text-muted-foreground mb-2">
85
+ Choose an authentication provider:
86
+ </p>
87
+ )}
88
+ <div className="flex flex-col gap-2">
89
+ {enableGoogle &&
90
+ (googleButton ?? (
91
+ <Button
92
+ onClick={signInWithGoogle}
93
+ disabled={isAuthenticated}
94
+ variant="outline"
95
+ className="w-full"
96
+ >
97
+ <svg
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ width="20"
100
+ height="20"
101
+ viewBox="0 0 24 24"
102
+ fill="currentColor"
103
+ className="mr-2"
104
+ >
105
+ <path
106
+ 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"
107
+ fill="#4285F4"
108
+ />
109
+ <path
110
+ 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"
111
+ fill="#34A853"
112
+ />
113
+ <path
114
+ 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"
115
+ fill="#FBBC05"
116
+ />
117
+ <path
118
+ 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"
119
+ fill="#EA4335"
120
+ />
121
+ </svg>
122
+ Sign in with Google
123
+ </Button>
124
+ ))}
125
+ {enableGithub &&
126
+ (githubButton ?? (
127
+ <Button
128
+ onClick={signInWithGitHub}
129
+ disabled={isAuthenticated}
130
+ variant="outline"
131
+ className="w-full"
132
+ >
133
+ <svg
134
+ xmlns="http://www.w3.org/2000/svg"
135
+ width="20"
136
+ height="20"
137
+ viewBox="0 0 24 24"
138
+ fill="none"
139
+ stroke="currentColor"
140
+ strokeWidth="2"
141
+ strokeLinecap="round"
142
+ strokeLinejoin="round"
143
+ className="mr-2"
144
+ >
145
+ <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
146
+ <path d="M9 18c-4.51 2-5-2-7-2" />
147
+ </svg>
148
+ Sign in with GitHub
149
+ </Button>
150
+ ))}
151
+ </div>
152
+ </div>
153
+ );
154
+ }