@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,34 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import React, { createContext } from "react";
5
+ import type { GitHubAuthConfig } from "./config.ts";
6
+
7
+ /**
8
+ * Generic user type that applications can extend
9
+ */
10
+ export type AuthUser = Record<string, unknown>;
11
+
12
+ export interface GitHubAuthContextType {
13
+ user: AuthUser | null;
14
+ loading: boolean;
15
+ signInWithGitHub: () => void;
16
+ signOut: () => void;
17
+ updateUser: (user: AuthUser | null) => void;
18
+ }
19
+
20
+ export const GitHubAuthContext = createContext<GitHubAuthContextType | null>(
21
+ null
22
+ );
23
+
24
+ export interface GitHubAuthProviderProps {
25
+ children: React.ReactNode;
26
+ /** Backend API base URL (default: auto-detect from window.location) */
27
+ apiBaseUrl?: string;
28
+ /** Storage key for user data in localStorage (default: "user") */
29
+ storageKey?: string;
30
+ /** Callback when user is updated */
31
+ onUserUpdate?: (user: AuthUser | null) => void;
32
+ /** Custom GitHub Auth configuration (if not provided, uses default-client) */
33
+ config?: GitHubAuthConfig;
34
+ }
@@ -0,0 +1,74 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { createGitHubAuthConfig } from "./config.ts";
5
+ import { GitHubAuthClient } from "./client.ts";
6
+ import type { GitHubOAuthProfile } from "./types.ts";
7
+
8
+ /**
9
+ * Default GitHub OAuth client (convention-over-configuration)
10
+ * Uses sensible defaults and environment variables for easy setup
11
+ */
12
+
13
+ let defaultClient: GitHubAuthClient | null = null;
14
+
15
+ /**
16
+ * Gets or creates the default GitHub Auth client
17
+ */
18
+ function getDefaultClient(): GitHubAuthClient {
19
+ if (!defaultClient) {
20
+ const config = createGitHubAuthConfig();
21
+ defaultClient = new GitHubAuthClient(config);
22
+ }
23
+ return defaultClient;
24
+ }
25
+
26
+ /**
27
+ * Initiates GitHub OAuth login with default configuration
28
+ * Reads from VITE_GITHUB_CLIENT_ID and VITE_REDIRECT_URI environment variables
29
+ */
30
+ export function initiateGitHubLogin(): void {
31
+ getDefaultClient().initiateLogin();
32
+ }
33
+
34
+ /**
35
+ * Handles OAuth callback with default configuration
36
+ * Calls POST /api/auth/github/callback by default
37
+ * @param code Authorization code from GitHub OAuth callback
38
+ * @param state State parameter from GitHub OAuth callback (optional)
39
+ * @returns User profile from backend
40
+ */
41
+ export async function handleGitHubCallback(
42
+ code: string,
43
+ state?: string
44
+ ): Promise<GitHubOAuthProfile> {
45
+ return await getDefaultClient().handleCallback(code, state);
46
+ }
47
+
48
+ /**
49
+ * Default GitHub Auth class for common use cases
50
+ */
51
+ export class DefaultGitHubAuth {
52
+ private client: GitHubAuthClient;
53
+
54
+ constructor() {
55
+ this.client = getDefaultClient();
56
+ }
57
+
58
+ /**
59
+ * Initiates GitHub OAuth login
60
+ */
61
+ login(): void {
62
+ this.client.initiateLogin();
63
+ }
64
+
65
+ /**
66
+ * Handles OAuth callback
67
+ */
68
+ async handleCallback(
69
+ code: string,
70
+ state?: string
71
+ ): Promise<GitHubOAuthProfile> {
72
+ return await this.client.handleCallback(code, state);
73
+ }
74
+ }
@@ -0,0 +1,43 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Types
5
+ export type { GitHubOAuthProfile, GitHubUserProfile } from "./types.ts";
6
+ export { toUserProfile } from "./types.ts";
7
+
8
+ // Configuration
9
+ export type { GitHubAuthCallbackResponse, GitHubAuthConfig } from "./config.ts";
10
+ export { createGitHubAuthConfig, DEFAULT_SCOPE } from "./config.ts";
11
+
12
+ // Configurable client (config-over-convention)
13
+ export { GitHubAuthClient, initiateGitHubLogin } from "./client.ts";
14
+
15
+ // Default client (convention-over-configuration)
16
+ export {
17
+ DefaultGitHubAuth,
18
+ handleGitHubCallback,
19
+ initiateGitHubLogin as defaultInitiateGitHubLogin,
20
+ } from "./default-client.ts";
21
+
22
+ // Auth Provider
23
+ export {
24
+ DefaultGitHubAuthProvider,
25
+ GitHubAuthProvider,
26
+ } from "./AuthProvider.tsx";
27
+ // Hook
28
+ export { useGitHubAuth } from "./useGitHubAuth.ts";
29
+ export type {
30
+ AuthUser,
31
+ GitHubAuthContextType,
32
+ GitHubAuthProviderProps,
33
+ } from "./context.ts";
34
+
35
+ // Auth Callback
36
+ export {
37
+ DefaultGitHubAuthCallback,
38
+ GitHubAuthCallback,
39
+ } from "./AuthCallback.tsx";
40
+ export type {
41
+ DefaultGitHubAuthCallbackProps,
42
+ GitHubAuthCallbackProps,
43
+ } from "./AuthCallback.tsx";
@@ -0,0 +1,44 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * GitHub OAuth user profile returned from GitHub's user API endpoint
6
+ */
7
+ export interface GitHubOAuthProfile {
8
+ id: number; // GitHub user ID
9
+ login: string; // GitHub username
10
+ name?: string; // User's display name
11
+ email?: string; // User's email
12
+ avatar_url: string; // User's avatar URL
13
+ }
14
+
15
+ /**
16
+ * User profile structure that can be used by applications
17
+ * This is the format returned from the backend after OAuth callback
18
+ */
19
+ export interface GitHubUserProfile {
20
+ id: string;
21
+ displayName: string;
22
+ email?: string;
23
+ profilePicture?: string;
24
+ githubOAuthId: number;
25
+ githubUsername: string;
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ }
29
+
30
+ /**
31
+ * Converts GitHub OAuth profile to application user profile format
32
+ */
33
+ export function toUserProfile(profile: GitHubOAuthProfile): GitHubUserProfile {
34
+ return {
35
+ id: profile.id.toString(),
36
+ displayName: profile.name ?? profile.login,
37
+ email: profile.email,
38
+ profilePicture: profile.avatar_url,
39
+ githubOAuthId: profile.id,
40
+ githubUsername: profile.login,
41
+ createdAt: new Date(),
42
+ updatedAt: new Date(),
43
+ };
44
+ }
@@ -0,0 +1,16 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { useContext } from "react";
5
+ import { GitHubAuthContext, type GitHubAuthContextType } from "./context.ts";
6
+
7
+ /**
8
+ * Hook to access GitHub Auth context
9
+ */
10
+ export function useGitHubAuth(): GitHubAuthContextType {
11
+ const context = useContext(GitHubAuthContext);
12
+ if (!context) {
13
+ throw new Error("useGitHubAuth must be used within a GitHubAuthProvider");
14
+ }
15
+ return context;
16
+ }
@@ -0,0 +1,170 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useEffect, useRef } from "react";
6
+ import type { GoogleOAuthProfile } from "./types.ts";
7
+ import { GoogleAuthClient } from "./client.ts";
8
+ import { handleGoogleCallback as defaultHandleCallback } from "./default-client.ts";
9
+ import type { GoogleAuthConfig } from "./config.ts";
10
+
11
+ export interface GoogleAuthCallbackProps {
12
+ /** Authorization code from URL query params */
13
+ code: string;
14
+ /** Backend API base URL (default: auto-detect) */
15
+ apiBaseUrl?: string;
16
+ /** Callback when user profile is successfully retrieved */
17
+ onSuccess: (profile: GoogleOAuthProfile) => void | Promise<void>;
18
+ /** Callback when an error occurs */
19
+ onError: (error: Error) => void;
20
+ /** Custom Google Auth configuration (if not provided, uses default-client) */
21
+ config?: GoogleAuthConfig;
22
+ }
23
+
24
+ /**
25
+ * Configurable Google Auth Callback component
26
+ * Handles OAuth callback and calls appropriate callbacks
27
+ */
28
+ export function GoogleAuthCallback({
29
+ code,
30
+ apiBaseUrl,
31
+ onSuccess,
32
+ onError,
33
+ config,
34
+ }: GoogleAuthCallbackProps) {
35
+ const processedRef = useRef(false);
36
+
37
+ useEffect(() => {
38
+ if (processedRef.current) return;
39
+ processedRef.current = true;
40
+
41
+ const handleCallback = async () => {
42
+ try {
43
+ let profile: GoogleOAuthProfile;
44
+ if (config) {
45
+ const client = new GoogleAuthClient(config);
46
+ profile = await client.handleCallback(code);
47
+ } else {
48
+ profile = await defaultHandleCallback(code);
49
+ }
50
+ await onSuccess(profile);
51
+ } catch (error) {
52
+ onError(error instanceof Error ? error : new Error(String(error)));
53
+ }
54
+ };
55
+
56
+ void handleCallback();
57
+ }, [code, apiBaseUrl, onSuccess, onError, config]);
58
+
59
+ return (
60
+ <div className="flex items-center justify-center min-h-screen bg-background">
61
+ <div className="text-center">
62
+ <h1 className="text-2xl font-bold mb-4">Completing sign in...</h1>
63
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export interface DefaultGoogleAuthCallbackProps {
70
+ /** Authorization code from URL query params */
71
+ code: string;
72
+ /** Callback when existing user is found */
73
+ onUserFound: (profile: GoogleOAuthProfile) => void | Promise<void>;
74
+ /** Callback when new user (not found in system) */
75
+ onUserNotFound: (profile: GoogleOAuthProfile) => void | Promise<void>;
76
+ /** Callback when an error occurs */
77
+ onError: (error: Error) => void;
78
+ /** Backend API endpoint for user lookup (default: /api/users) */
79
+ userLookupEndpoint?: string;
80
+ /** Redirect path after successful login (default: /home) */
81
+ redirectPath?: string;
82
+ /** Redirect path for new users (default: /create-user) */
83
+ newUserRedirectPath?: string;
84
+ }
85
+
86
+ /**
87
+ * Default Google Auth Callback component with convention-over-configuration
88
+ * Handles common flow: callback -> lookup user -> redirect
89
+ */
90
+ export function DefaultGoogleAuthCallback({
91
+ code,
92
+ onUserFound,
93
+ onUserNotFound,
94
+ onError,
95
+ userLookupEndpoint,
96
+ redirectPath,
97
+ newUserRedirectPath,
98
+ }: DefaultGoogleAuthCallbackProps) {
99
+ const processedRef = useRef(false);
100
+
101
+ useEffect(() => {
102
+ if (processedRef.current) return;
103
+ processedRef.current = true;
104
+
105
+ const handleCallback = async () => {
106
+ try {
107
+ // Get user profile from backend
108
+ const profile = await defaultHandleCallback(code);
109
+
110
+ // Look up existing user
111
+ try {
112
+ const response = await fetch(userLookupEndpoint ?? "");
113
+ if (response.ok) {
114
+ const userResponse = (await response.json()) as {
115
+ value?: { googleOAuthId?: string }[];
116
+ };
117
+ const users = userResponse.value ?? [];
118
+ const existingUser = users.find(
119
+ (u: { googleOAuthId?: string }) => u.googleOAuthId === profile.sub
120
+ );
121
+
122
+ if (existingUser) {
123
+ await onUserFound(profile);
124
+ // Store redirect path if using localStorage
125
+ const storedRedirect =
126
+ localStorage.getItem("redirectAfterLogin") ?? redirectPath;
127
+ if (storedRedirect) {
128
+ // Redirect logic would go here
129
+ }
130
+ localStorage.removeItem("redirectAfterLogin");
131
+ // Note: Actual navigation should be handled by the app's router
132
+ return;
133
+ }
134
+ }
135
+ } catch (lookupError) {
136
+ console.warn(
137
+ "User lookup failed, treating as new user:",
138
+ lookupError
139
+ );
140
+ }
141
+
142
+ // New user flow
143
+ localStorage.setItem("oauth_profile", JSON.stringify(profile));
144
+ await onUserNotFound(profile);
145
+ // Note: Actual navigation should be handled by the app's router
146
+ } catch (error) {
147
+ onError(error instanceof Error ? error : new Error(String(error)));
148
+ }
149
+ };
150
+
151
+ void handleCallback();
152
+ }, [
153
+ code,
154
+ onUserFound,
155
+ onUserNotFound,
156
+ onError,
157
+ userLookupEndpoint,
158
+ redirectPath,
159
+ newUserRedirectPath,
160
+ ]);
161
+
162
+ return (
163
+ <div className="flex items-center justify-center min-h-screen bg-background">
164
+ <div className="text-center">
165
+ <h1 className="text-2xl font-bold mb-4">Completing sign in...</h1>
166
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,108 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import React, { useCallback, useEffect, useState } from "react";
6
+ import { GoogleAuthClient } from "./client.ts";
7
+ import { initiateGoogleLogin as defaultInitiateLogin } from "./default-client.ts";
8
+
9
+ import {
10
+ type AuthUser,
11
+ GoogleAuthContext,
12
+ type GoogleAuthProviderProps,
13
+ } from "./context.ts";
14
+
15
+ /**
16
+ * Generic Google Auth Provider component
17
+ * Manages user state and provides auth context to children
18
+ */
19
+ export function GoogleAuthProvider({
20
+ children,
21
+ storageKey,
22
+ onUserUpdate,
23
+ config,
24
+ }: GoogleAuthProviderProps) {
25
+ const [user, setUser] = useState<AuthUser | null>(null);
26
+ const [loading, setLoading] = useState(true);
27
+
28
+ useEffect(() => {
29
+ // Initial load from localStorage
30
+ const storedUser = localStorage.getItem(storageKey ?? "user");
31
+ if (storedUser) {
32
+ try {
33
+ const parsedUser = JSON.parse(storedUser) as AuthUser;
34
+ // Validate that parsed user is actually a valid user object
35
+ // Reject null, empty objects, or invalid values
36
+ if (
37
+ typeof parsedUser === "object" &&
38
+ Object.keys(parsedUser).length > 0
39
+ ) {
40
+ setUser(parsedUser);
41
+ } else {
42
+ // Invalid user data, clear it
43
+ localStorage.removeItem(storageKey ?? "user");
44
+ }
45
+ } catch (error) {
46
+ console.error("Error parsing stored user:", error);
47
+ localStorage.removeItem(storageKey ?? "user");
48
+ }
49
+ }
50
+ setLoading(false);
51
+ }, [storageKey]);
52
+
53
+ const updateUser = useCallback(
54
+ (newUser: AuthUser | null) => {
55
+ setUser(newUser);
56
+ if (newUser) {
57
+ localStorage.setItem(storageKey ?? "user", JSON.stringify(newUser));
58
+ } else {
59
+ localStorage.removeItem(storageKey ?? "user");
60
+ }
61
+ onUserUpdate?.(newUser);
62
+ },
63
+ [storageKey, onUserUpdate]
64
+ );
65
+
66
+ const signInWithGoogle = useCallback(() => {
67
+ if (config) {
68
+ const client = new GoogleAuthClient(config);
69
+ client.initiateLogin();
70
+ } else {
71
+ defaultInitiateLogin();
72
+ }
73
+ }, [config]);
74
+
75
+ const signOut = useCallback(() => {
76
+ setLoading(true);
77
+ localStorage.removeItem(storageKey ?? "user");
78
+ setUser(null);
79
+ onUserUpdate?.(null);
80
+ setLoading(false);
81
+ }, [storageKey, onUserUpdate]);
82
+
83
+ return (
84
+ <GoogleAuthContext.Provider
85
+ value={{ user, loading, signInWithGoogle, signOut, updateUser }}
86
+ >
87
+ {children}
88
+ </GoogleAuthContext.Provider>
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Default Google Auth Provider with convention-over-configuration
94
+ * Uses sensible defaults: /api/auth/google/callback, storage key "user"
95
+ */
96
+ export function DefaultGoogleAuthProvider({
97
+ children,
98
+ onUserUpdate,
99
+ }: {
100
+ children: React.ReactNode;
101
+ onUserUpdate?: (user: AuthUser | null) => void;
102
+ }) {
103
+ return (
104
+ <GoogleAuthProvider storageKey="user" onUserUpdate={onUserUpdate}>
105
+ {children}
106
+ </GoogleAuthProvider>
107
+ );
108
+ }