@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,196 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useCallback, useEffect, useState } from "react";
6
+ import type { GoogleAuthConfig } from "./google/config.ts";
7
+ import type { GitHubAuthConfig } from "./github/config.ts";
8
+ import { GoogleAuthProvider } from "./google/AuthProvider.tsx";
9
+ import { GitHubAuthProvider } from "./github/AuthProvider.tsx";
10
+ import { GoogleAuthClient } from "./google/client.ts";
11
+ import { GitHubAuthClient } from "./github/client.ts";
12
+ import { initiateGoogleLogin as defaultInitiateGoogleLogin } from "./google/default-client.ts";
13
+ import { initiateGitHubLogin as defaultInitiateGitHubLogin } from "./github/default-client.ts";
14
+
15
+ import {
16
+ type AuthUser,
17
+ MultiAuthContext,
18
+ type MultiAuthProviderProps,
19
+ } from "./context.ts";
20
+
21
+ /**
22
+ * Multi-Provider Auth Provider component
23
+ * Wraps individual providers and provides unified auth interface
24
+ */
25
+ export function MultiAuthProvider({
26
+ children,
27
+ providers,
28
+ storageKey,
29
+ apiBaseUrl,
30
+ onUserUpdate,
31
+ }: MultiAuthProviderProps) {
32
+ const [user, setUser] = useState<AuthUser | null>(null);
33
+ const [loading, setLoading] = useState(true);
34
+ const [activeProvider, setActiveProvider] = useState<
35
+ "google" | "github" | null
36
+ >(null);
37
+
38
+ const googleConfig = providers.find(
39
+ (p) => p.provider === "google" && p.enabled
40
+ );
41
+ const githubConfig = providers.find(
42
+ (p) => p.provider === "github" && p.enabled
43
+ );
44
+
45
+ useEffect(() => {
46
+ // Initial load from localStorage
47
+ const key = storageKey ?? "user";
48
+ const storedUser = localStorage.getItem(key);
49
+ if (storedUser) {
50
+ try {
51
+ const parsedUser = JSON.parse(storedUser) as AuthUser;
52
+ // Validate that parsed user is actually a valid user object
53
+ // Reject null, empty objects, or invalid values
54
+ if (
55
+ typeof parsedUser === "object" &&
56
+ Object.keys(parsedUser).length > 0
57
+ ) {
58
+ setUser(parsedUser);
59
+ setActiveProvider(
60
+ (parsedUser as { provider?: "google" | "github" }).provider ?? null
61
+ );
62
+ } else {
63
+ // Invalid user data, clear it
64
+ localStorage.removeItem(key);
65
+ }
66
+ } catch (error) {
67
+ console.error("Error parsing stored user:", error);
68
+ localStorage.removeItem(key);
69
+ }
70
+ }
71
+ setLoading(false);
72
+ }, [storageKey]);
73
+
74
+ const updateUser = useCallback(
75
+ (newUser: AuthUser | null, provider?: "google" | "github") => {
76
+ setUser(newUser);
77
+ const key = storageKey ?? "user";
78
+ if (newUser) {
79
+ const userWithProvider = {
80
+ ...newUser,
81
+ provider:
82
+ provider ??
83
+ (newUser as { provider?: "google" | "github" }).provider,
84
+ };
85
+ localStorage.setItem(key, JSON.stringify(userWithProvider));
86
+ setActiveProvider(
87
+ provider ??
88
+ (newUser as { provider?: "google" | "github" }).provider ??
89
+ null
90
+ );
91
+ } else {
92
+ localStorage.removeItem(key);
93
+ setActiveProvider(null);
94
+ }
95
+ onUserUpdate?.(newUser);
96
+ },
97
+ [storageKey, onUserUpdate]
98
+ );
99
+
100
+ const signOut = useCallback(() => {
101
+ setLoading(true);
102
+ const key = storageKey ?? "user";
103
+ localStorage.removeItem(key);
104
+ setUser(null);
105
+ setActiveProvider(null);
106
+ onUserUpdate?.(null);
107
+ setLoading(false);
108
+ }, [storageKey, onUserUpdate]);
109
+
110
+ const signInWithGoogle = useCallback(() => {
111
+ if (googleConfig) {
112
+ if (googleConfig.config) {
113
+ const client = new GoogleAuthClient(
114
+ googleConfig.config as GoogleAuthConfig
115
+ );
116
+ client.initiateLogin();
117
+ } else {
118
+ defaultInitiateGoogleLogin();
119
+ }
120
+ }
121
+ }, [googleConfig]);
122
+
123
+ const signInWithGitHub = useCallback(() => {
124
+ if (githubConfig) {
125
+ if (githubConfig.config) {
126
+ const client = new GitHubAuthClient(
127
+ githubConfig.config as GitHubAuthConfig
128
+ );
129
+ client.initiateLogin();
130
+ } else {
131
+ defaultInitiateGitHubLogin();
132
+ }
133
+ }
134
+ }, [githubConfig]);
135
+
136
+ // Render providers based on configuration
137
+ let content = children;
138
+
139
+ // Wrap with GitHub provider if enabled
140
+ if (githubConfig) {
141
+ content = (
142
+ <GitHubAuthProvider
143
+ apiBaseUrl={apiBaseUrl}
144
+ storageKey={storageKey}
145
+ config={githubConfig.config as GitHubAuthConfig}
146
+ onUserUpdate={(user) => {
147
+ if (user) {
148
+ updateUser(user as AuthUser, "github");
149
+ } else if (activeProvider === "github") {
150
+ updateUser(null);
151
+ }
152
+ }}
153
+ >
154
+ {content}
155
+ </GitHubAuthProvider>
156
+ );
157
+ }
158
+
159
+ // Wrap with Google provider if enabled
160
+ if (googleConfig) {
161
+ content = (
162
+ <GoogleAuthProvider
163
+ apiBaseUrl={apiBaseUrl}
164
+ storageKey={storageKey}
165
+ config={googleConfig.config as GoogleAuthConfig}
166
+ onUserUpdate={(user) => {
167
+ if (user) {
168
+ updateUser(user as AuthUser, "google");
169
+ } else if (activeProvider === "google") {
170
+ updateUser(null);
171
+ }
172
+ }}
173
+ >
174
+ {content}
175
+ </GoogleAuthProvider>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <MultiAuthContext.Provider
181
+ value={{
182
+ user,
183
+ loading,
184
+ provider: activeProvider,
185
+ signInWithGoogle,
186
+ signInWithGitHub,
187
+ signOut,
188
+ updateUser: (user) => {
189
+ updateUser(user, activeProvider ?? undefined);
190
+ },
191
+ }}
192
+ >
193
+ {content}
194
+ </MultiAuthContext.Provider>
195
+ );
196
+ }
@@ -0,0 +1,317 @@
1
+ # Multi-Provider Authentication
2
+
3
+ This package provides a unified authentication system that supports multiple
4
+ OAuth providers (Google, GitHub) with a single interface. Apps can configure
5
+ which providers are available, and users can choose which provider to use.
6
+
7
+ ## Features
8
+
9
+ - 🔐 Support for multiple OAuth providers (Google, GitHub)
10
+ - 🎛️ Configure which providers are enabled per app
11
+ - 🎨 Unified `AuthSelector` component for provider selection
12
+ - 🔄 Single user identity at a time (one provider active)
13
+ - ⚙️ Both configurable and default client options
14
+ - 📦 TypeScript types for type safety
15
+
16
+ ## Installation
17
+
18
+ This package is part of the `@chesapeake/shared-components` package. Import
19
+ from:
20
+
21
+ ```typescript
22
+ import { ... } from "@shared/components/ux/auth";
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Setup MultiAuthProvider
28
+
29
+ Wrap your app with `MultiAuthProvider` and configure which providers are
30
+ enabled:
31
+
32
+ ```tsx
33
+ import {
34
+ type AuthProviderConfig,
35
+ MultiAuthProvider,
36
+ } from "@shared/components/ux/auth";
37
+
38
+ function App() {
39
+ // Configure which providers are available
40
+ const authProviders: AuthProviderConfig[] = [
41
+ { provider: "github", enabled: true },
42
+ { provider: "google", enabled: true }, // Enable both
43
+ ];
44
+
45
+ return (
46
+ <MultiAuthProvider providers={authProviders}>
47
+ <YourApp />
48
+ </MultiAuthProvider>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ### 2. Use AuthSelector Component
54
+
55
+ The `AuthSelector` component provides buttons for enabled providers:
56
+
57
+ ```tsx
58
+ import { AuthSelector } from "@shared/components/ux/auth";
59
+
60
+ function LoginPage() {
61
+ return (
62
+ <AuthSelector
63
+ enableGithub={true}
64
+ enableGoogle={true}
65
+ onAuthSuccess={(user, provider) => {
66
+ console.log(`User authenticated with ${provider}:`, user);
67
+ }}
68
+ />
69
+ );
70
+ }
71
+ ```
72
+
73
+ ### 3. Use Auth Hook
74
+
75
+ Access auth state and methods with the unified `useAuth` hook:
76
+
77
+ ```tsx
78
+ import { useAuth } from "@shared/components/ux/auth";
79
+
80
+ function UserProfile() {
81
+ const { user, loading, provider, signOut } = useAuth();
82
+
83
+ if (loading) return <div>Loading...</div>;
84
+ if (!user) return <div>Not authenticated</div>;
85
+
86
+ return (
87
+ <div>
88
+ <p>Signed in with {provider}</p>
89
+ <button onClick={signOut}>Sign Out</button>
90
+ </div>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ## Configuration Examples
96
+
97
+ ### Single Provider (GitHub only)
98
+
99
+ ```tsx
100
+ const authProviders: AuthProviderConfig[] = [
101
+ { provider: "github", enabled: true },
102
+ { provider: "google", enabled: false },
103
+ ];
104
+
105
+ <MultiAuthProvider providers={authProviders}>
106
+ <AuthSelector enableGithub={true} enableGoogle={false} />
107
+ </MultiAuthProvider>;
108
+ ```
109
+
110
+ ### Single Provider (Google only)
111
+
112
+ ```tsx
113
+ const authProviders: AuthProviderConfig[] = [
114
+ { provider: "google", enabled: true },
115
+ { provider: "github", enabled: false },
116
+ ];
117
+
118
+ <MultiAuthProvider providers={authProviders}>
119
+ <AuthSelector enableGoogle={true} enableGithub={false} />
120
+ </MultiAuthProvider>;
121
+ ```
122
+
123
+ ### Custom Configuration
124
+
125
+ You can provide custom configuration for each provider:
126
+
127
+ ```tsx
128
+ const authProviders: AuthProviderConfig[] = [
129
+ {
130
+ provider: "github",
131
+ enabled: true,
132
+ config: {
133
+ clientId: "your-github-client-id",
134
+ redirectUri: "http://localhost:5173/auth/callback",
135
+ backendInitEndpoint: "/api/auth/github", // Optional: if backend handles OAuth
136
+ },
137
+ },
138
+ {
139
+ provider: "google",
140
+ enabled: true,
141
+ config: {
142
+ clientId: "your-google-client-id",
143
+ redirectUri: "http://localhost:5173/auth/callback",
144
+ scope: "openid email profile",
145
+ },
146
+ },
147
+ ];
148
+ ```
149
+
150
+ ### Using Default Clients
151
+
152
+ If you don't provide config, the providers will use default clients that read
153
+ from environment variables:
154
+
155
+ ```tsx
156
+ // Uses VITE_GITHUB_CLIENT_ID or VITE_GITHUB_BACKEND_INIT_ENDPOINT
157
+ const authProviders: AuthProviderConfig[] = [
158
+ { provider: "github", enabled: true }, // Uses default client
159
+ { provider: "google", enabled: true }, // Uses VITE_GOOGLE_CLIENT_ID
160
+ ];
161
+ ```
162
+
163
+ ## AuthSelector Component
164
+
165
+ The `AuthSelector` component provides a ready-to-use UI for authentication:
166
+
167
+ ### Props
168
+
169
+ ```typescript
170
+ interface AuthSelectorProps {
171
+ /** Enable Google OAuth provider */
172
+ enableGoogle?: boolean;
173
+ /** Enable GitHub OAuth provider */
174
+ enableGithub?: boolean;
175
+ /** Custom styling/className */
176
+ className?: string;
177
+ /** Callback when user successfully authenticates */
178
+ onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
179
+ /** Custom button components (optional) */
180
+ googleButton?: React.ReactNode;
181
+ githubButton?: React.ReactNode;
182
+ }
183
+ ```
184
+
185
+ ### Behavior
186
+
187
+ - Renders buttons for enabled providers
188
+ - Buttons are disabled when user is already authenticated
189
+ - Shows current authenticated provider state
190
+ - Provides sign out button when authenticated
191
+ - Only one provider can be active at a time
192
+
193
+ ## useAuth Hook
194
+
195
+ The unified `useAuth` hook provides access to auth state and methods:
196
+
197
+ ```typescript
198
+ interface MultiAuthContextType {
199
+ user: AuthUser | null;
200
+ loading: boolean;
201
+ provider: "google" | "github" | null;
202
+ signInWithGoogle: () => void;
203
+ signInWithGitHub: () => void;
204
+ signOut: () => void;
205
+ updateUser: (user: AuthUser | null) => void;
206
+ }
207
+ ```
208
+
209
+ ## Backend Integration
210
+
211
+ ### Backend-Handled OAuth Flow
212
+
213
+ If your backend handles the entire OAuth flow (common for server-side session
214
+ management), configure providers with `backendInitEndpoint`:
215
+
216
+ ```tsx
217
+ const authProviders: AuthProviderConfig[] = [
218
+ {
219
+ provider: "github",
220
+ enabled: true,
221
+ config: {
222
+ backendInitEndpoint: "/api/auth/github", // Backend handles OAuth initiation
223
+ redirectUri: window.location.origin + "/api/auth/github/callback",
224
+ },
225
+ },
226
+ ];
227
+ ```
228
+
229
+ The frontend will redirect to the backend endpoint, which then redirects to the
230
+ OAuth provider. After successful authentication, the backend should redirect
231
+ back to your app, and the frontend will sync with the backend session.
232
+
233
+ ### Frontend-Handled OAuth Flow
234
+
235
+ If your frontend initiates OAuth and backend only handles the callback:
236
+
237
+ ```tsx
238
+ const authProviders: AuthProviderConfig[] = [
239
+ {
240
+ provider: "github",
241
+ enabled: true,
242
+ config: {
243
+ clientId: "your-github-client-id",
244
+ redirectUri: "http://localhost:5173/auth/callback",
245
+ apiBaseUrl: "", // Backend API base URL
246
+ },
247
+ },
248
+ ];
249
+ ```
250
+
251
+ ## Individual Provider Documentation
252
+
253
+ - [Google Auth](./google/README.md) - Google OAuth setup and usage
254
+ - [GitHub Auth](./github/README.md) - GitHub OAuth setup and usage
255
+
256
+ ## Type Safety
257
+
258
+ All components and hooks are fully typed with TypeScript:
259
+
260
+ ```typescript
261
+ import type {
262
+ AuthProviderConfig,
263
+ AuthSelectorProps,
264
+ AuthUser,
265
+ MultiAuthContextType,
266
+ MultiAuthProviderProps,
267
+ } from "@shared/components/ux/auth";
268
+ ```
269
+
270
+ ## Examples
271
+
272
+ ### Complete Example: Todo App with GitHub Auth
273
+
274
+ ```tsx
275
+ import {
276
+ AuthSelector,
277
+ MultiAuthProvider,
278
+ useAuth,
279
+ } from "@shared/components/ux/auth";
280
+
281
+ function App() {
282
+ const authProviders: AuthProviderConfig[] = [
283
+ {
284
+ provider: "github",
285
+ enabled: true,
286
+ config: {
287
+ backendInitEndpoint: "/api/auth/github",
288
+ redirectUri: window.location.origin + "/api/auth/github/callback",
289
+ },
290
+ },
291
+ { provider: "google", enabled: false },
292
+ ];
293
+
294
+ return (
295
+ <MultiAuthProvider providers={authProviders}>
296
+ <TodoApp />
297
+ </MultiAuthProvider>
298
+ );
299
+ }
300
+
301
+ function ProfilePage() {
302
+ const { user, signOut } = useAuth();
303
+
304
+ return (
305
+ <div>
306
+ {user
307
+ ? (
308
+ <div>
309
+ <p>Signed in as {user.login || user.email}</p>
310
+ <button onClick={signOut}>Sign Out</button>
311
+ </div>
312
+ )
313
+ : <AuthSelector enableGithub={true} enableGoogle={false} />}
314
+ </div>
315
+ );
316
+ }
317
+ ```
@@ -0,0 +1,46 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import React, { createContext } from "react";
5
+ import type { GoogleAuthConfig } from "./google/config.ts";
6
+ import type { GitHubAuthConfig } from "./github/config.ts";
7
+
8
+ /**
9
+ * Generic user type that applications can extend
10
+ */
11
+ export interface AuthUser {
12
+ [key: string]: unknown;
13
+ provider?: "google" | "github";
14
+ }
15
+
16
+ export interface MultiAuthContextType {
17
+ user: AuthUser | null;
18
+ loading: boolean;
19
+ provider: "google" | "github" | null;
20
+ signInWithGoogle: () => void;
21
+ signInWithGitHub: () => void;
22
+ signOut: () => void;
23
+ updateUser: (user: AuthUser | null) => void;
24
+ }
25
+
26
+ export const MultiAuthContext = createContext<MultiAuthContextType | null>(
27
+ null
28
+ );
29
+
30
+ export interface AuthProviderConfig {
31
+ provider: "google" | "github";
32
+ enabled: boolean;
33
+ config?: GoogleAuthConfig | GitHubAuthConfig;
34
+ }
35
+
36
+ export interface MultiAuthProviderProps {
37
+ children: React.ReactNode;
38
+ /** Configuration for which providers are enabled */
39
+ providers: AuthProviderConfig[];
40
+ /** Storage key for user data in localStorage (default: "user") */
41
+ storageKey?: string;
42
+ /** Backend API base URL (default: auto-detect from window.location) */
43
+ apiBaseUrl?: string;
44
+ /** Callback when user is updated */
45
+ onUserUpdate?: (user: AuthUser | null) => void;
46
+ }