@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,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
+ }