@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +190 -0
- package/README.md +440 -0
- package/components/ui/avatar.tsx +87 -0
- package/components/ui/badge.tsx +49 -0
- package/components/ui/badgeVariants.ts +26 -0
- package/components/ui/button-group.tsx +67 -0
- package/components/ui/button.tsx +70 -0
- package/components/ui/buttonGroupVariants.ts +23 -0
- package/components/ui/buttonVariants.ts +38 -0
- package/components/ui/calendar.tsx +220 -0
- package/components/ui/card.tsx +162 -0
- package/components/ui/dialog.tsx +290 -0
- package/components/ui/drawer.tsx +227 -0
- package/components/ui/dropdown-menu.tsx +260 -0
- package/components/ui/index.ts +86 -0
- package/components/ui/input.tsx +59 -0
- package/components/ui/label.tsx +47 -0
- package/components/ui/location-input.md +194 -0
- package/components/ui/location-input.tsx +384 -0
- package/components/ui/popover.tsx +110 -0
- package/components/ui/select.tsx +306 -0
- package/components/ui/separator.tsx +50 -0
- package/components/ui/sonner-api.ts +8 -0
- package/components/ui/sonner.tsx +69 -0
- package/components/ui/textarea.tsx +53 -0
- package/components/ui/tooltip.tsx +125 -0
- package/components/ux/ActivityAnimation.tsx +135 -0
- package/components/ux/EmptyState.tsx +22 -0
- package/components/ux/FeedbackDrawer.tsx +141 -0
- package/components/ux/FireAnimation.tsx +189 -0
- package/components/ux/FriendList.tsx +111 -0
- package/components/ux/FriendRequestList.tsx +104 -0
- package/components/ux/GifSearch.tsx +264 -0
- package/components/ux/ImageUpload.tsx +829 -0
- package/components/ux/SelectedUsersList.tsx +91 -0
- package/components/ux/StandardLayout/PageLayout.tsx +107 -0
- package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
- package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
- package/components/ux/StandardLayout/index.ts +22 -0
- package/components/ux/TutorialCarousel.tsx +128 -0
- package/components/ux/UserSearch.tsx +324 -0
- package/components/ux/UserSelector.tsx +116 -0
- package/components/ux/auth/AuthSelector.tsx +154 -0
- package/components/ux/auth/MultiAuthProvider.tsx +196 -0
- package/components/ux/auth/README.md +317 -0
- package/components/ux/auth/context.ts +46 -0
- package/components/ux/auth/github/AuthCallback.tsx +177 -0
- package/components/ux/auth/github/AuthProvider.tsx +111 -0
- package/components/ux/auth/github/README.md +208 -0
- package/components/ux/auth/github/client.ts +127 -0
- package/components/ux/auth/github/config.ts +68 -0
- package/components/ux/auth/github/context.ts +34 -0
- package/components/ux/auth/github/default-client.ts +74 -0
- package/components/ux/auth/github/index.ts +43 -0
- package/components/ux/auth/github/types.ts +44 -0
- package/components/ux/auth/github/useGitHubAuth.ts +16 -0
- package/components/ux/auth/google/AuthCallback.tsx +170 -0
- package/components/ux/auth/google/AuthProvider.tsx +108 -0
- package/components/ux/auth/google/README.md +450 -0
- package/components/ux/auth/google/client.ts +109 -0
- package/components/ux/auth/google/config.ts +65 -0
- package/components/ux/auth/google/context.ts +34 -0
- package/components/ux/auth/google/default-client.ts +69 -0
- package/components/ux/auth/google/index.ts +43 -0
- package/components/ux/auth/google/types.ts +42 -0
- package/components/ux/auth/google/useGoogleAuth.ts +16 -0
- package/components/ux/auth/index.ts +72 -0
- package/components/ux/auth/useAuth.ts +16 -0
- package/components/ux/index.ts +46 -0
- package/components/ux/theme-provider-context.ts +20 -0
- package/components/ux/theme-provider.tsx +114 -0
- package/components/ux/useTheme.ts +42 -0
- package/index.ts +29 -0
- package/lib/utils.ts +57 -0
- package/package.json +170 -0
- package/styles/base.css +38 -0
- package/styles/components.css +54 -0
- package/styles/index.css +7 -0
- package/styles/tokens.css +189 -0
- package/tailwind.preset.ts +169 -0
- package/types/user.ts +69 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { createGoogleAuthConfig } from "./config.ts";
|
|
5
|
+
import { GoogleAuthClient } from "./client.ts";
|
|
6
|
+
import type { GoogleOAuthProfile } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default Google OAuth client (convention-over-configuration)
|
|
10
|
+
* Uses sensible defaults and environment variables for easy setup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let defaultClient: GoogleAuthClient | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets or creates the default Google Auth client
|
|
17
|
+
*/
|
|
18
|
+
function getDefaultClient(): GoogleAuthClient {
|
|
19
|
+
if (!defaultClient) {
|
|
20
|
+
const config = createGoogleAuthConfig();
|
|
21
|
+
defaultClient = new GoogleAuthClient(config);
|
|
22
|
+
}
|
|
23
|
+
return defaultClient;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initiates Google OAuth login with default configuration
|
|
28
|
+
* Reads from VITE_GOOGLE_CLIENT_ID and VITE_REDIRECT_URI environment variables
|
|
29
|
+
*/
|
|
30
|
+
export function initiateGoogleLogin(): void {
|
|
31
|
+
getDefaultClient().initiateLogin();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handles OAuth callback with default configuration
|
|
36
|
+
* Calls POST /api/auth/google/callback by default
|
|
37
|
+
* @param code Authorization code from Google OAuth callback
|
|
38
|
+
* @returns User profile from backend
|
|
39
|
+
*/
|
|
40
|
+
export async function handleGoogleCallback(
|
|
41
|
+
code: string
|
|
42
|
+
): Promise<GoogleOAuthProfile> {
|
|
43
|
+
return await getDefaultClient().handleCallback(code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default Google Auth class for common use cases
|
|
48
|
+
*/
|
|
49
|
+
export class DefaultGoogleAuth {
|
|
50
|
+
private client: GoogleAuthClient;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.client = getDefaultClient();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initiates Google OAuth login
|
|
58
|
+
*/
|
|
59
|
+
login(): void {
|
|
60
|
+
this.client.initiateLogin();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handles OAuth callback
|
|
65
|
+
*/
|
|
66
|
+
async handleCallback(code: string): Promise<GoogleOAuthProfile> {
|
|
67
|
+
return await this.client.handleCallback(code);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type { GoogleOAuthProfile, GoogleUserProfile } from "./types.ts";
|
|
6
|
+
export { toUserProfile } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
// Configuration
|
|
9
|
+
export type { GoogleAuthCallbackResponse, GoogleAuthConfig } from "./config.ts";
|
|
10
|
+
export { createGoogleAuthConfig, DEFAULT_SCOPE } from "./config.ts";
|
|
11
|
+
|
|
12
|
+
// Configurable client (config-over-convention)
|
|
13
|
+
export { GoogleAuthClient, initiateGoogleLogin } from "./client.ts";
|
|
14
|
+
|
|
15
|
+
// Default client (convention-over-configuration)
|
|
16
|
+
export {
|
|
17
|
+
DefaultGoogleAuth,
|
|
18
|
+
handleGoogleCallback,
|
|
19
|
+
initiateGoogleLogin as defaultInitiateGoogleLogin,
|
|
20
|
+
} from "./default-client.ts";
|
|
21
|
+
|
|
22
|
+
// Auth Provider
|
|
23
|
+
export {
|
|
24
|
+
DefaultGoogleAuthProvider,
|
|
25
|
+
GoogleAuthProvider,
|
|
26
|
+
} from "./AuthProvider.tsx";
|
|
27
|
+
// Hook
|
|
28
|
+
export { useGoogleAuth } from "./useGoogleAuth.ts";
|
|
29
|
+
export type {
|
|
30
|
+
AuthUser,
|
|
31
|
+
GoogleAuthContextType,
|
|
32
|
+
GoogleAuthProviderProps,
|
|
33
|
+
} from "./context.ts";
|
|
34
|
+
|
|
35
|
+
// Auth Callback
|
|
36
|
+
export {
|
|
37
|
+
DefaultGoogleAuthCallback,
|
|
38
|
+
GoogleAuthCallback,
|
|
39
|
+
} from "./AuthCallback.tsx";
|
|
40
|
+
export type {
|
|
41
|
+
DefaultGoogleAuthCallbackProps,
|
|
42
|
+
GoogleAuthCallbackProps,
|
|
43
|
+
} from "./AuthCallback.tsx";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Google OAuth user profile returned from Google's userinfo endpoint
|
|
6
|
+
*/
|
|
7
|
+
export interface GoogleOAuthProfile {
|
|
8
|
+
sub: string; // Google user ID
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
picture?: string;
|
|
12
|
+
email_verified?: boolean;
|
|
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 GoogleUserProfile {
|
|
20
|
+
id: string;
|
|
21
|
+
displayName: string;
|
|
22
|
+
email: string;
|
|
23
|
+
profilePicture?: string;
|
|
24
|
+
googleOAuthId: string;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
updatedAt: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Converts Google OAuth profile to application user profile format
|
|
31
|
+
*/
|
|
32
|
+
export function toUserProfile(profile: GoogleOAuthProfile): GoogleUserProfile {
|
|
33
|
+
return {
|
|
34
|
+
id: profile.sub,
|
|
35
|
+
displayName: profile.name,
|
|
36
|
+
email: profile.email,
|
|
37
|
+
profilePicture: profile.picture,
|
|
38
|
+
googleOAuthId: profile.sub,
|
|
39
|
+
createdAt: new Date(),
|
|
40
|
+
updatedAt: new Date(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useContext } from "react";
|
|
5
|
+
import { GoogleAuthContext, type GoogleAuthContextType } from "./context.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access Google Auth context
|
|
9
|
+
*/
|
|
10
|
+
export function useGoogleAuth(): GoogleAuthContextType {
|
|
11
|
+
const context = useContext(GoogleAuthContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useGoogleAuth must be used within a GoogleAuthProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Multi-Provider Auth
|
|
5
|
+
export { MultiAuthProvider } from "./MultiAuthProvider.tsx";
|
|
6
|
+
export { useAuth } from "./useAuth.ts";
|
|
7
|
+
export type {
|
|
8
|
+
AuthProviderConfig,
|
|
9
|
+
AuthUser,
|
|
10
|
+
MultiAuthContextType,
|
|
11
|
+
MultiAuthProviderProps,
|
|
12
|
+
} from "./context.ts";
|
|
13
|
+
|
|
14
|
+
// Auth Selector Component
|
|
15
|
+
export { AuthSelector } from "./AuthSelector.tsx";
|
|
16
|
+
export type { AuthSelectorProps } from "./AuthSelector.tsx";
|
|
17
|
+
|
|
18
|
+
// Google Auth (re-export for convenience)
|
|
19
|
+
export {
|
|
20
|
+
createGoogleAuthConfig,
|
|
21
|
+
DEFAULT_SCOPE as GOOGLE_DEFAULT_SCOPE,
|
|
22
|
+
DefaultGoogleAuth,
|
|
23
|
+
DefaultGoogleAuthCallback,
|
|
24
|
+
DefaultGoogleAuthProvider,
|
|
25
|
+
defaultInitiateGoogleLogin,
|
|
26
|
+
GoogleAuthCallback,
|
|
27
|
+
GoogleAuthClient,
|
|
28
|
+
GoogleAuthProvider,
|
|
29
|
+
handleGoogleCallback,
|
|
30
|
+
initiateGoogleLogin,
|
|
31
|
+
toUserProfile as toGoogleUserProfile,
|
|
32
|
+
useGoogleAuth,
|
|
33
|
+
} from "./google/index.ts";
|
|
34
|
+
export type {
|
|
35
|
+
AuthUser as GoogleAuthUser,
|
|
36
|
+
DefaultGoogleAuthCallbackProps,
|
|
37
|
+
GoogleAuthCallbackProps,
|
|
38
|
+
GoogleAuthCallbackResponse,
|
|
39
|
+
GoogleAuthConfig,
|
|
40
|
+
GoogleAuthContextType,
|
|
41
|
+
GoogleAuthProviderProps,
|
|
42
|
+
GoogleOAuthProfile,
|
|
43
|
+
GoogleUserProfile,
|
|
44
|
+
} from "./google/index.ts";
|
|
45
|
+
|
|
46
|
+
// GitHub Auth (re-export for convenience)
|
|
47
|
+
export {
|
|
48
|
+
createGitHubAuthConfig,
|
|
49
|
+
DEFAULT_SCOPE as GITHUB_DEFAULT_SCOPE,
|
|
50
|
+
DefaultGitHubAuth,
|
|
51
|
+
DefaultGitHubAuthCallback,
|
|
52
|
+
DefaultGitHubAuthProvider,
|
|
53
|
+
defaultInitiateGitHubLogin,
|
|
54
|
+
GitHubAuthCallback,
|
|
55
|
+
GitHubAuthClient,
|
|
56
|
+
GitHubAuthProvider,
|
|
57
|
+
handleGitHubCallback,
|
|
58
|
+
initiateGitHubLogin,
|
|
59
|
+
toUserProfile as toGitHubUserProfile,
|
|
60
|
+
useGitHubAuth,
|
|
61
|
+
} from "./github/index.ts";
|
|
62
|
+
export type {
|
|
63
|
+
AuthUser as GitHubAuthUser,
|
|
64
|
+
DefaultGitHubAuthCallbackProps,
|
|
65
|
+
GitHubAuthCallbackProps,
|
|
66
|
+
GitHubAuthCallbackResponse,
|
|
67
|
+
GitHubAuthConfig,
|
|
68
|
+
GitHubAuthContextType,
|
|
69
|
+
GitHubAuthProviderProps,
|
|
70
|
+
GitHubOAuthProfile,
|
|
71
|
+
GitHubUserProfile,
|
|
72
|
+
} from "./github/index.ts";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useContext } from "react";
|
|
5
|
+
import { MultiAuthContext, type MultiAuthContextType } from "./context.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access Multi Auth context
|
|
9
|
+
*/
|
|
10
|
+
export function useAuth(): MultiAuthContextType {
|
|
11
|
+
const context = useContext(MultiAuthContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useAuth must be used within a MultiAuthProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// User Search and Selection Components
|
|
5
|
+
export { UserSearch } from "./UserSearch.tsx";
|
|
6
|
+
export type { UserSearchProps } from "./UserSearch.tsx";
|
|
7
|
+
|
|
8
|
+
export { SelectedUsersList } from "./SelectedUsersList.tsx";
|
|
9
|
+
export type {
|
|
10
|
+
SelectedUser,
|
|
11
|
+
SelectedUsersListProps,
|
|
12
|
+
} from "./SelectedUsersList.tsx";
|
|
13
|
+
|
|
14
|
+
export { UserSelector } from "./UserSelector.tsx";
|
|
15
|
+
export type { UserSelectorProps } from "./UserSelector.tsx";
|
|
16
|
+
|
|
17
|
+
// Friend Management Components
|
|
18
|
+
export { FriendList } from "./FriendList.tsx";
|
|
19
|
+
export type { Friend, FriendListProps } from "./FriendList.tsx";
|
|
20
|
+
|
|
21
|
+
export { FriendRequestList } from "./FriendRequestList.tsx";
|
|
22
|
+
export type {
|
|
23
|
+
FriendRequest,
|
|
24
|
+
FriendRequestListProps,
|
|
25
|
+
} from "./FriendRequestList.tsx";
|
|
26
|
+
|
|
27
|
+
// StandardLayout Components (backward compatibility for PageLayout)
|
|
28
|
+
export {
|
|
29
|
+
PageContainer,
|
|
30
|
+
PageContent,
|
|
31
|
+
PageHeader,
|
|
32
|
+
PageLayout,
|
|
33
|
+
} from "./StandardLayout/index.ts";
|
|
34
|
+
export type {
|
|
35
|
+
PageContainerProps,
|
|
36
|
+
PageContentProps,
|
|
37
|
+
PageHeaderProps,
|
|
38
|
+
PageLayoutProps,
|
|
39
|
+
} from "./StandardLayout/index.ts";
|
|
40
|
+
|
|
41
|
+
// StandardLayout exports
|
|
42
|
+
export { ProfilePage, StandardLayout } from "./StandardLayout/index.ts";
|
|
43
|
+
export type {
|
|
44
|
+
ProfilePageProps,
|
|
45
|
+
StandardLayoutProps,
|
|
46
|
+
} from "./StandardLayout/index.ts";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { createContext } from "react";
|
|
5
|
+
|
|
6
|
+
export type Theme = "dark" | "light" | "system";
|
|
7
|
+
|
|
8
|
+
export interface ThemeProviderState {
|
|
9
|
+
theme: Theme;
|
|
10
|
+
setTheme: (theme: Theme) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const initialState: ThemeProviderState = {
|
|
14
|
+
theme: "system",
|
|
15
|
+
setTheme: () => null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const ThemeProviderContext = createContext<ThemeProviderState | null>(
|
|
19
|
+
null
|
|
20
|
+
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
import { ThemeProviderContext, type Theme } from "./theme-provider-context.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Props for the ThemeProvider component
|
|
10
|
+
*/
|
|
11
|
+
interface ThemeProviderProps {
|
|
12
|
+
/** Child components to be wrapped by the provider */
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/** Default theme to use if none is stored in localStorage */
|
|
15
|
+
defaultTheme?: Theme;
|
|
16
|
+
/** Key used for storing theme preference in localStorage */
|
|
17
|
+
storageKey?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provider component for managing application theme (light/dark/system).
|
|
22
|
+
*
|
|
23
|
+
* This component manages the theme state and applies the appropriate
|
|
24
|
+
* CSS class to the document root element. It persists the user's
|
|
25
|
+
* preference in localStorage.
|
|
26
|
+
*
|
|
27
|
+
* The theme system supports:
|
|
28
|
+
* - `light`: Always use light theme
|
|
29
|
+
* - `dark`: Always use dark theme
|
|
30
|
+
* - `system`: Follow the user's OS preference
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* function App() {
|
|
35
|
+
* return (
|
|
36
|
+
* <ThemeProvider defaultTheme="system" storageKey="app-theme">
|
|
37
|
+
* <YourApp />
|
|
38
|
+
* </ThemeProvider>
|
|
39
|
+
* );
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* // Using the theme in a component
|
|
46
|
+
* import { useTheme } from "./useTheme";
|
|
47
|
+
*
|
|
48
|
+
* function ThemeToggle() {
|
|
49
|
+
* const { theme, setTheme } = useTheme();
|
|
50
|
+
*
|
|
51
|
+
* return (
|
|
52
|
+
* <select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
53
|
+
* <option value="light">Light</option>
|
|
54
|
+
* <option value="dark">Dark</option>
|
|
55
|
+
* <option value="system">System</option>
|
|
56
|
+
* </select>
|
|
57
|
+
* );
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @sideeffect
|
|
62
|
+
* - Modifies `localStorage` to persist theme preference
|
|
63
|
+
* - Modifies `document.documentElement.classList` to apply theme class
|
|
64
|
+
* - Listens to system preference changes when theme is "system"
|
|
65
|
+
*/
|
|
66
|
+
export function ThemeProvider({
|
|
67
|
+
children,
|
|
68
|
+
defaultTheme,
|
|
69
|
+
storageKey,
|
|
70
|
+
...props
|
|
71
|
+
}: ThemeProviderProps) {
|
|
72
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
73
|
+
const stored = localStorage.getItem(storageKey ?? "theme");
|
|
74
|
+
if (
|
|
75
|
+
stored &&
|
|
76
|
+
(stored === "dark" || stored === "light" || stored === "system")
|
|
77
|
+
) {
|
|
78
|
+
return stored as Theme;
|
|
79
|
+
}
|
|
80
|
+
return defaultTheme ?? "system";
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const root = globalThis.document.documentElement;
|
|
85
|
+
|
|
86
|
+
root.classList.remove("light", "dark");
|
|
87
|
+
|
|
88
|
+
if (theme === "system") {
|
|
89
|
+
const systemTheme = globalThis.matchMedia("(prefers-color-scheme: dark)")
|
|
90
|
+
.matches
|
|
91
|
+
? "dark"
|
|
92
|
+
: "light";
|
|
93
|
+
|
|
94
|
+
root.classList.add(systemTheme);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
root.classList.add(theme);
|
|
99
|
+
}, [theme]);
|
|
100
|
+
|
|
101
|
+
const value = {
|
|
102
|
+
theme,
|
|
103
|
+
setTheme: (theme: Theme) => {
|
|
104
|
+
localStorage.setItem(storageKey ?? "dark", theme);
|
|
105
|
+
setTheme(theme);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ThemeProviderContext.Provider {...props} value={value}>
|
|
111
|
+
{children}
|
|
112
|
+
</ThemeProviderContext.Provider>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useContext } from "react";
|
|
5
|
+
import {
|
|
6
|
+
ThemeProviderContext,
|
|
7
|
+
type ThemeProviderState,
|
|
8
|
+
} from "./theme-provider-context.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook for accessing the current theme and theme setter.
|
|
12
|
+
*
|
|
13
|
+
* Must be used within a ThemeProvider component.
|
|
14
|
+
*
|
|
15
|
+
* @returns {ThemeProviderState} Object containing the current theme and setTheme function
|
|
16
|
+
* @throws {Error} If used outside of a ThemeProvider
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function ThemeToggle() {
|
|
21
|
+
* const { theme, setTheme } = useTheme();
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <div className="flex items-center gap-2">
|
|
25
|
+
* <span>Current theme: {theme}</span>
|
|
26
|
+
* <Button onClick={() => setTheme("dark")}>Dark</Button>
|
|
27
|
+
* <Button onClick={() => setTheme("light")}>Light</Button>
|
|
28
|
+
* <Button onClick={() => setTheme("system")}>System</Button>
|
|
29
|
+
* </div>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const useTheme = (): ThemeProviderState => {
|
|
35
|
+
const context = useContext(ThemeProviderContext);
|
|
36
|
+
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context;
|
|
42
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @chesapeake/stack
|
|
6
|
+
*
|
|
7
|
+
* A reusable React component library and design system built on top of
|
|
8
|
+
* shadcn/ui with Tailwind CSS.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { Button } from '@chesapeake/stack/components/ui/button';
|
|
13
|
+
* import { ThemeProvider } from '@chesapeake/stack/components/ux/theme-provider';
|
|
14
|
+
*
|
|
15
|
+
* function App() {
|
|
16
|
+
* return (
|
|
17
|
+
* <ThemeProvider>
|
|
18
|
+
* <Button variant="default">Click me</Button>
|
|
19
|
+
* </ThemeProvider>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Re-export everything from subdirectories for convenient imports
|
|
26
|
+
export * from "./components/ui/index.ts";
|
|
27
|
+
export * from "./components/ux/index.ts";
|
|
28
|
+
export * from "./lib/utils.ts";
|
|
29
|
+
export { default as tailwindPreset } from "./tailwind.preset.ts";
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { type ClassValue, clsx } from "clsx";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A utility function for conditionally joining class names together.
|
|
9
|
+
*
|
|
10
|
+
* Combines the power of `clsx` (for conditional class joining) with
|
|
11
|
+
* `tailwind-merge` (for resolving Tailwind CSS class conflicts).
|
|
12
|
+
*
|
|
13
|
+
* This function is used throughout the component library to handle
|
|
14
|
+
* className merging with proper Tailwind CSS specificity resolution.
|
|
15
|
+
*
|
|
16
|
+
* @param inputs - Class values to be merged. Can be strings, objects, arrays, or falsy values.
|
|
17
|
+
* @returns A merged and deduplicated className string with resolved Tailwind conflicts.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* // Basic usage
|
|
22
|
+
* cn('px-4 py-2', 'bg-blue-500')
|
|
23
|
+
* // Result: 'px-4 py-2 bg-blue-500'
|
|
24
|
+
*
|
|
25
|
+
* // Conditional classes
|
|
26
|
+
* cn('px-4', isActive && 'bg-blue-500', !isActive && 'bg-gray-200')
|
|
27
|
+
* // Result when isActive=true: 'px-4 bg-blue-500'
|
|
28
|
+
*
|
|
29
|
+
* // Resolving Tailwind conflicts (tailwind-merge)
|
|
30
|
+
* cn('px-4 py-2', 'px-6')
|
|
31
|
+
* // Result: 'py-2 px-6' (px-6 wins over px-4)
|
|
32
|
+
*
|
|
33
|
+
* // Object syntax
|
|
34
|
+
* cn('base-class', {
|
|
35
|
+
* 'text-red-500': hasError,
|
|
36
|
+
* 'text-green-500': isSuccess,
|
|
37
|
+
* 'hidden': !isVisible
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* // Array syntax
|
|
41
|
+
* cn(['flex', 'items-center'], 'justify-between')
|
|
42
|
+
*
|
|
43
|
+
* // Combining all features
|
|
44
|
+
* cn(
|
|
45
|
+
* 'base-styles',
|
|
46
|
+
* isLarge && 'text-lg',
|
|
47
|
+
* {
|
|
48
|
+
* 'bg-blue-500': variant === 'primary',
|
|
49
|
+
* 'bg-gray-500': variant === 'secondary'
|
|
50
|
+
* },
|
|
51
|
+
* className // External className prop
|
|
52
|
+
* )
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
56
|
+
return twMerge(clsx(inputs));
|
|
57
|
+
}
|