@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,345 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useState } from "react";
6
+ import { Button } from "../../../components/ui/button";
7
+ import { useAuth } from "../auth";
8
+ import { ProfilePage } from "./ProfilePage";
9
+ import type { AuthUser } from "../auth/context";
10
+
11
+ export type MaxWidthPreset = "narrow" | "medium" | "wide";
12
+
13
+ export interface StandardLayoutProps {
14
+ children: React.ReactNode;
15
+ headerContent?: React.ReactNode; // Custom header content (title, buttons, etc.)
16
+ showProfileButton?: boolean; // Default: true
17
+ profilePageChildren?: React.ReactNode; // App-specific settings for ProfilePage
18
+ // ProfilePage props
19
+ enableGoogle?: boolean;
20
+ enableGithub?: boolean;
21
+ onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
22
+ friendsEndpoint?: string;
23
+ friendRequestsEndpoint?: string;
24
+ userSearchEndpoint?: string;
25
+ className?: string;
26
+ maxWidth?: MaxWidthPreset; // Default: "wide" (100%)
27
+ }
28
+
29
+ /**
30
+ * StandardLayout provides a complete single-page application layout
31
+ * with header, main content area, and integrated profile page.
32
+ * Matches the todo app's layout structure.
33
+ */
34
+ export function StandardLayout({
35
+ children,
36
+ headerContent,
37
+ showProfileButton,
38
+ profilePageChildren,
39
+ enableGoogle,
40
+ enableGithub,
41
+ onAuthSuccess,
42
+ friendsEndpoint,
43
+ friendRequestsEndpoint,
44
+ userSearchEndpoint,
45
+ className,
46
+ maxWidth,
47
+ }: StandardLayoutProps) {
48
+ const [showProfile, setShowProfile] = useState(false);
49
+ const { user: authUser } = useAuth();
50
+
51
+ // Determine if user is logged in based on authUser
52
+ const isLoggedIn = !!(
53
+ authUser &&
54
+ typeof authUser === "object" &&
55
+ Object.keys(authUser).length > 0
56
+ );
57
+
58
+ // Get user avatar from authUser if available
59
+ const userAvatar =
60
+ authUser && typeof authUser === "object" && "avatarUrl" in authUser
61
+ ? (authUser.avatarUrl as string)
62
+ : authUser && typeof authUser === "object" && "photoURL" in authUser
63
+ ? (authUser.photoURL as string)
64
+ : undefined;
65
+
66
+ const handleProfileClick = () => {
67
+ setShowProfile(true);
68
+ };
69
+
70
+ const handleBackToMain = () => {
71
+ setShowProfile(false);
72
+ };
73
+
74
+ // Get responsive max-width class
75
+ // 100% width at 1080p and below (1920px), 50% width above 1080p
76
+ const getMaxWidthClass = (preset: MaxWidthPreset | undefined): string => {
77
+ const p = preset ?? "wide";
78
+ switch (p) {
79
+ case "narrow":
80
+ // Responsive: 100% below 1920px (1080p), 50% at 1920px and above
81
+ return "max-w-full [1920px]:max-w-[50%]";
82
+ case "medium":
83
+ // Responsive: 100% below 1920px, 75% at 1920px and above
84
+ return "max-w-full [1920px]:max-w-[75%]";
85
+ case "wide":
86
+ return "max-w-full";
87
+ default:
88
+ return "max-w-full";
89
+ }
90
+ };
91
+
92
+ // Show profile page if profile is open
93
+ if (showProfile) {
94
+ return (
95
+ <div
96
+ className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
97
+ >
98
+ {/* Toolbar/Header */}
99
+ <div className="border-b bg-background flex-shrink-0">
100
+ <div className="flex justify-center">
101
+ <div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
102
+ <div className="flex justify-between items-center">
103
+ {headerContent ? (
104
+ <div className="flex items-center gap-2 w-full">
105
+ {headerContent}
106
+ {showProfileButton && (
107
+ <div className="ml-auto">
108
+ <Button
109
+ variant="outline"
110
+ size="icon"
111
+ className={isLoggedIn ? "ring-2 ring-green-500" : ""}
112
+ onClick={handleProfileClick}
113
+ title={isLoggedIn ? "View Profile" : "Login"}
114
+ >
115
+ {isLoggedIn && userAvatar ? (
116
+ <img
117
+ src={userAvatar}
118
+ alt="Profile"
119
+ className="w-6 h-6 rounded-full"
120
+ />
121
+ ) : (
122
+ <svg
123
+ xmlns="http://www.w3.org/2000/svg"
124
+ width="24"
125
+ height="24"
126
+ viewBox="0 0 24 24"
127
+ fill="none"
128
+ stroke="currentColor"
129
+ strokeWidth="2"
130
+ strokeLinecap="round"
131
+ strokeLinejoin="round"
132
+ >
133
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
134
+ <circle cx="12" cy="7" r="4" />
135
+ </svg>
136
+ )}
137
+ </Button>
138
+ </div>
139
+ )}
140
+ </div>
141
+ ) : (
142
+ <>
143
+ <div className="flex items-center gap-2">
144
+ <h1 className="text-2xl font-bold">App</h1>
145
+ </div>
146
+ <div className="flex items-center gap-2">
147
+ {showProfileButton && (
148
+ <Button
149
+ variant="outline"
150
+ size="icon"
151
+ className={isLoggedIn ? "ring-2 ring-green-500" : ""}
152
+ onClick={handleProfileClick}
153
+ title={isLoggedIn ? "View Profile" : "Login"}
154
+ >
155
+ {isLoggedIn && userAvatar ? (
156
+ <img
157
+ src={userAvatar}
158
+ alt="Profile"
159
+ className="w-6 h-6 rounded-full"
160
+ />
161
+ ) : (
162
+ <svg
163
+ xmlns="http://www.w3.org/2000/svg"
164
+ width="24"
165
+ height="24"
166
+ viewBox="0 0 24 24"
167
+ fill="none"
168
+ stroke="currentColor"
169
+ strokeWidth="2"
170
+ strokeLinecap="round"
171
+ strokeLinejoin="round"
172
+ >
173
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
174
+ <circle cx="12" cy="7" r="4" />
175
+ </svg>
176
+ )}
177
+ </Button>
178
+ )}
179
+ </div>
180
+ </>
181
+ )}
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ {/* Main content area - Profile Page */}
188
+ <div className="flex-1 overflow-hidden flex justify-center min-h-0">
189
+ <div
190
+ className={`w-full ${getMaxWidthClass(maxWidth)} flex flex-col h-full min-h-0`}
191
+ >
192
+ {/* Fixed Profile Header */}
193
+ <div className="flex-shrink-0 border-b bg-background p-4">
194
+ <div className="flex items-center justify-between">
195
+ <Button
196
+ variant="ghost"
197
+ onClick={handleBackToMain}
198
+ className="p-2"
199
+ >
200
+ <svg
201
+ xmlns="http://www.w3.org/2000/svg"
202
+ width="20"
203
+ height="20"
204
+ viewBox="0 0 24 24"
205
+ fill="none"
206
+ stroke="currentColor"
207
+ strokeWidth="2"
208
+ strokeLinecap="round"
209
+ strokeLinejoin="round"
210
+ >
211
+ <path d="m15 18-6-6 6-6" />
212
+ </svg>
213
+ </Button>
214
+ <h1 className="text-2xl font-bold">Profile</h1>
215
+ <div className="w-10" /> {/* Spacer for centering */}
216
+ </div>
217
+ </div>
218
+
219
+ {/* Scrollable Profile Content */}
220
+ <div className="flex-1 overflow-y-auto min-h-0">
221
+ <ProfilePage
222
+ onBack={undefined}
223
+ enableGoogle={enableGoogle}
224
+ enableGithub={enableGithub}
225
+ onAuthSuccess={onAuthSuccess}
226
+ friendsEndpoint={friendsEndpoint}
227
+ friendRequestsEndpoint={friendRequestsEndpoint}
228
+ userSearchEndpoint={userSearchEndpoint}
229
+ className={className}
230
+ hideHeader
231
+ >
232
+ {profilePageChildren}
233
+ </ProfilePage>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ // Render main layout
242
+ return (
243
+ <div
244
+ className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
245
+ >
246
+ {/* Toolbar/Header */}
247
+ <div className="border-b bg-background flex-shrink-0">
248
+ <div className="flex justify-center">
249
+ <div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
250
+ <div className="flex justify-between items-center">
251
+ {headerContent ? (
252
+ <div className="flex items-center gap-2 w-full">
253
+ {headerContent}
254
+ {showProfileButton && (
255
+ <div className="ml-auto">
256
+ <Button
257
+ variant="outline"
258
+ size="icon"
259
+ className={isLoggedIn ? "ring-2 ring-green-500" : ""}
260
+ onClick={handleProfileClick}
261
+ title={isLoggedIn ? "View Profile" : "Login"}
262
+ >
263
+ {isLoggedIn && userAvatar ? (
264
+ <img
265
+ src={userAvatar}
266
+ alt="Profile"
267
+ className="w-6 h-6 rounded-full"
268
+ />
269
+ ) : (
270
+ <svg
271
+ xmlns="http://www.w3.org/2000/svg"
272
+ width="24"
273
+ height="24"
274
+ viewBox="0 0 24 24"
275
+ fill="none"
276
+ stroke="currentColor"
277
+ strokeWidth="2"
278
+ strokeLinecap="round"
279
+ strokeLinejoin="round"
280
+ >
281
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
282
+ <circle cx="12" cy="7" r="4" />
283
+ </svg>
284
+ )}
285
+ </Button>
286
+ </div>
287
+ )}
288
+ </div>
289
+ ) : (
290
+ <>
291
+ <div className="flex items-center gap-2">
292
+ <h1 className="text-2xl font-bold">App</h1>
293
+ </div>
294
+ <div className="flex items-center gap-2">
295
+ {showProfileButton && (
296
+ <Button
297
+ variant="outline"
298
+ size="icon"
299
+ className={isLoggedIn ? "ring-2 ring-green-500" : ""}
300
+ onClick={handleProfileClick}
301
+ title={isLoggedIn ? "View Profile" : "Login"}
302
+ >
303
+ {isLoggedIn && userAvatar ? (
304
+ <img
305
+ src={userAvatar}
306
+ alt="Profile"
307
+ className="w-6 h-6 rounded-full"
308
+ />
309
+ ) : (
310
+ <svg
311
+ xmlns="http://www.w3.org/2000/svg"
312
+ width="24"
313
+ height="24"
314
+ viewBox="0 0 24 24"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ strokeWidth="2"
318
+ strokeLinecap="round"
319
+ strokeLinejoin="round"
320
+ >
321
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
322
+ <circle cx="12" cy="7" r="4" />
323
+ </svg>
324
+ )}
325
+ </Button>
326
+ )}
327
+ </div>
328
+ </>
329
+ )}
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+
335
+ {/* Main content area */}
336
+ <div className="flex-1 overflow-hidden flex justify-center min-h-0">
337
+ <div
338
+ className={`w-full ${getMaxWidthClass(maxWidth)} flex flex-col h-full min-h-0`}
339
+ >
340
+ {children}
341
+ </div>
342
+ </div>
343
+ </div>
344
+ );
345
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // StandardLayout exports
5
+ export { StandardLayout } from "./StandardLayout.tsx";
6
+ export type { StandardLayoutProps } from "./StandardLayout.tsx";
7
+
8
+ export { ProfilePage } from "./ProfilePage.tsx";
9
+ export type { ProfilePageProps } from "./ProfilePage.tsx";
10
+
11
+ export {
12
+ PageContainer,
13
+ PageContent,
14
+ PageHeader,
15
+ PageLayout,
16
+ } from "./PageLayout.tsx";
17
+ export type {
18
+ PageContainerProps,
19
+ PageContentProps,
20
+ PageHeaderProps,
21
+ PageLayoutProps,
22
+ } from "./PageLayout.tsx";
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import React, { useState } from "react";
6
+
7
+ interface TutorialSlide {
8
+ id: number;
9
+ title: string;
10
+ description: string;
11
+ icon: React.ComponentType<{ className?: string }>;
12
+ color: string;
13
+ }
14
+
15
+ interface TutorialCarouselProps {
16
+ onComplete: () => void;
17
+ Button: React.ComponentType<{
18
+ variant?: string;
19
+ size?: string;
20
+ onClick?: () => void;
21
+ disabled?: boolean;
22
+ className?: string;
23
+ children?: React.ReactNode;
24
+ }>;
25
+ slides: TutorialSlide[];
26
+ }
27
+
28
+ const colorMap: Record<string, string> = {
29
+ blue: "text-blue-500",
30
+ green: "text-green-500",
31
+ purple: "text-purple-500",
32
+ red: "text-red-500",
33
+ orange: "text-orange-500",
34
+ yellow: "text-yellow-500",
35
+ };
36
+
37
+ export function TutorialCarousel({
38
+ onComplete,
39
+ Button,
40
+ slides,
41
+ }: TutorialCarouselProps) {
42
+ const [currentSlide, setCurrentSlide] = useState(0);
43
+
44
+ const nextSlide = () => {
45
+ if (currentSlide < slides.length - 1) {
46
+ setCurrentSlide(currentSlide + 1);
47
+ } else {
48
+ onComplete();
49
+ }
50
+ };
51
+
52
+ const prevSlide = () => {
53
+ if (currentSlide > 0) {
54
+ setCurrentSlide(currentSlide - 1);
55
+ }
56
+ };
57
+
58
+ const skipTutorial = () => {
59
+ onComplete();
60
+ };
61
+
62
+ const currentSlideData = slides[currentSlide];
63
+ const IconComponent = currentSlideData.icon;
64
+ const iconColor = colorMap[currentSlideData.color] || "text-primary";
65
+
66
+ return (
67
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
68
+ <div className="bg-background rounded-lg p-6 max-w-md w-full mx-4 max-h-[80vh] overflow-hidden">
69
+ {/* Skip button */}
70
+ <div className="flex justify-end mb-4">
71
+ <Button
72
+ variant="ghost"
73
+ size="sm"
74
+ onClick={skipTutorial}
75
+ className="text-muted-foreground hover:text-foreground"
76
+ >
77
+ Skip
78
+ </Button>
79
+ </div>
80
+
81
+ {/* Slide content */}
82
+ <div className="text-center space-y-6">
83
+ {/* Icon */}
84
+ <div className="flex justify-center">
85
+ <div className="p-4 rounded-full bg-muted">
86
+ <IconComponent className={`h-12 w-12 ${iconColor}`} />
87
+ </div>
88
+ </div>
89
+
90
+ {/* Title */}
91
+ <h2 className="text-2xl font-bold">{currentSlideData.title}</h2>
92
+
93
+ {/* Description */}
94
+ <p className="text-muted-foreground leading-relaxed">
95
+ {currentSlideData.description}
96
+ </p>
97
+
98
+ {/* Progress dots */}
99
+ <div className="flex justify-center space-x-2 mb-6">
100
+ {slides.map((_, index) => (
101
+ <div
102
+ key={index}
103
+ className={`h-2 w-2 rounded-full transition-colors ${
104
+ index === currentSlide ? "bg-primary" : "bg-muted"
105
+ }`}
106
+ />
107
+ ))}
108
+ </div>
109
+ </div>
110
+
111
+ {/* Navigation buttons */}
112
+ <div className="flex gap-3">
113
+ <Button
114
+ variant="outline"
115
+ onClick={prevSlide}
116
+ disabled={currentSlide === 0}
117
+ className="flex-1"
118
+ >
119
+ Previous
120
+ </Button>
121
+ <Button onClick={nextSlide} className="flex-1">
122
+ {currentSlide === slides.length - 1 ? "Get Started!" : "Next"}
123
+ </Button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }