@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,384 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import * as React from "react";
5
+ import { cn } from "../../lib/utils.ts";
6
+
7
+ export interface LocationSuggestion {
8
+ place_id: string;
9
+ description: string;
10
+ structured_formatting: {
11
+ main_text: string;
12
+ secondary_text: string;
13
+ };
14
+ }
15
+
16
+ // Google Maps Places API types (inlined; use @types/google.maps in your project if available)
17
+ type GoogleMapsWindow = Window & {
18
+ google?: {
19
+ maps?: {
20
+ places?: {
21
+ AutocompleteService: new () => AutocompleteService;
22
+ };
23
+ };
24
+ };
25
+ };
26
+
27
+ interface AutocompleteRequest {
28
+ input: string;
29
+ types?: string[];
30
+ }
31
+
32
+ interface AutocompletePrediction {
33
+ place_id: string;
34
+ description: string;
35
+ structured_formatting?: {
36
+ main_text?: string;
37
+ secondary_text?: string;
38
+ };
39
+ }
40
+
41
+ type AutocompleteStatus =
42
+ | "OK"
43
+ | "ZERO_RESULTS"
44
+ | "OVER_QUERY_LIMIT"
45
+ | "REQUEST_DENIED"
46
+ | "INVALID_REQUEST"
47
+ | "UNKNOWN_ERROR";
48
+
49
+ type AutocompleteCallback = (
50
+ predictions: AutocompletePrediction[] | null,
51
+ status: AutocompleteStatus
52
+ ) => void;
53
+
54
+ interface AutocompleteService {
55
+ getPlacePredictions(
56
+ request: AutocompleteRequest,
57
+ callback: AutocompleteCallback
58
+ ): void;
59
+ }
60
+
61
+ export interface LocationInputProps extends Omit<
62
+ React.InputHTMLAttributes<HTMLInputElement>,
63
+ "onChange"
64
+ > {
65
+ onLocationSelect?: (location: LocationSuggestion) => void;
66
+ onInputChange?: (value: string) => void;
67
+ onSearchClose?: () => void;
68
+ placeholder?: string;
69
+ className?: string;
70
+ suggestionsClassName?: string;
71
+ apiKey?: string;
72
+ debounceMs?: number;
73
+ }
74
+
75
+ const LocationInput = React.forwardRef<HTMLInputElement, LocationInputProps>(
76
+ (
77
+ {
78
+ className,
79
+ onLocationSelect,
80
+ onInputChange,
81
+ onSearchClose,
82
+ placeholder,
83
+ suggestionsClassName,
84
+ apiKey,
85
+ debounceMs,
86
+ ...props
87
+ },
88
+ ref
89
+ ) => {
90
+ const [inputValue, setInputValue] = React.useState("");
91
+ const [suggestions, setSuggestions] = React.useState<LocationSuggestion[]>(
92
+ []
93
+ );
94
+ const [isLoading, setIsLoading] = React.useState(false);
95
+ const [showSuggestions, setShowSuggestions] = React.useState(false);
96
+ const [selectedIndex, setSelectedIndex] = React.useState(-1);
97
+ const inputRef = React.useRef<HTMLInputElement>(null);
98
+ const suggestionsRef = React.useRef<HTMLDivElement>(null);
99
+ const debounceTimeoutRef = React.useRef<
100
+ ReturnType<typeof setTimeout> | undefined
101
+ >(undefined);
102
+
103
+ // Initialize Google Maps Autocomplete service
104
+ const [autocompleteService, setAutocompleteService] =
105
+ React.useState<AutocompleteService | null>(null);
106
+
107
+ React.useEffect(() => {
108
+ // Load Google Maps JavaScript API
109
+ if (typeof window === "undefined") {
110
+ return;
111
+ }
112
+ const win = window as unknown as GoogleMapsWindow;
113
+ if (win.google?.maps?.places) {
114
+ setAutocompleteService(
115
+ new win.google.maps.places.AutocompleteService()
116
+ );
117
+ } else if (apiKey) {
118
+ const script = document.createElement("script");
119
+ script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
120
+ script.async = true;
121
+ script.defer = true;
122
+ script.onload = () => {
123
+ const winAfterLoad = window as unknown as GoogleMapsWindow;
124
+ if (winAfterLoad.google?.maps?.places) {
125
+ setAutocompleteService(
126
+ new winAfterLoad.google.maps.places.AutocompleteService()
127
+ );
128
+ }
129
+ };
130
+ document.head.appendChild(script);
131
+ }
132
+ }, [apiKey]);
133
+
134
+ // Debounced search function
135
+ const searchLocations = React.useCallback(
136
+ (query: string) => {
137
+ if (!query.trim() || !autocompleteService) {
138
+ setSuggestions([]);
139
+ setIsLoading(false);
140
+ return;
141
+ }
142
+
143
+ setIsLoading(true);
144
+ try {
145
+ const request: AutocompleteRequest = {
146
+ input: query,
147
+ types: ["establishment", "geocode"],
148
+ };
149
+
150
+ const callback: AutocompleteCallback = (predictions, status) => {
151
+ setIsLoading(false);
152
+ if (status === "OK" && predictions) {
153
+ const formattedSuggestions: LocationSuggestion[] =
154
+ predictions.map((prediction) => ({
155
+ place_id: prediction.place_id,
156
+ description: prediction.description,
157
+ structured_formatting: {
158
+ main_text:
159
+ prediction.structured_formatting?.main_text ?? "",
160
+ secondary_text:
161
+ prediction.structured_formatting?.secondary_text ?? "",
162
+ },
163
+ }));
164
+ setSuggestions(formattedSuggestions);
165
+ } else {
166
+ setSuggestions([]);
167
+ }
168
+ };
169
+
170
+ autocompleteService.getPlacePredictions(request, callback);
171
+ } catch (error) {
172
+ console.error("Error fetching location suggestions:", error);
173
+ setIsLoading(false);
174
+ setSuggestions([]);
175
+ }
176
+ },
177
+ [autocompleteService]
178
+ );
179
+
180
+ // Handle input change with debouncing
181
+ const handleInputChange = React.useCallback(
182
+ (e: React.ChangeEvent<HTMLInputElement>) => {
183
+ const value = e.target.value;
184
+ setInputValue(value);
185
+ onInputChange?.(value);
186
+ setSelectedIndex(-1);
187
+
188
+ // Clear previous timeout
189
+ if (debounceTimeoutRef.current) {
190
+ clearTimeout(debounceTimeoutRef.current);
191
+ }
192
+
193
+ // Set new timeout for debounced search
194
+ debounceTimeoutRef.current = setTimeout(() => {
195
+ searchLocations(value);
196
+ }, debounceMs ?? 300);
197
+ },
198
+ [onInputChange, searchLocations, debounceMs]
199
+ );
200
+
201
+ // Handle suggestion selection
202
+ const handleSuggestionClick = React.useCallback(
203
+ (suggestion: LocationSuggestion) => {
204
+ setInputValue(suggestion.description);
205
+ setSuggestions([]);
206
+ setShowSuggestions(false);
207
+ setSelectedIndex(-1);
208
+ onLocationSelect?.(suggestion);
209
+ onInputChange?.(suggestion.description);
210
+ },
211
+ [onLocationSelect, onInputChange]
212
+ );
213
+
214
+ // Handle close button click
215
+ const handleCloseClick = React.useCallback(() => {
216
+ setSuggestions([]);
217
+ setShowSuggestions(false);
218
+ setSelectedIndex(-1);
219
+ onSearchClose?.();
220
+ }, [onSearchClose]);
221
+
222
+ // Handle keyboard navigation
223
+ const handleKeyDown = React.useCallback(
224
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
225
+ if (!showSuggestions || suggestions.length === 0) return;
226
+
227
+ switch (e.key) {
228
+ case "ArrowDown":
229
+ e.preventDefault();
230
+ setSelectedIndex((prev) =>
231
+ prev < suggestions.length - 1 ? prev + 1 : prev
232
+ );
233
+ break;
234
+ case "ArrowUp":
235
+ e.preventDefault();
236
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
237
+ break;
238
+ case "Enter":
239
+ e.preventDefault();
240
+ if (selectedIndex >= 0 && suggestions[selectedIndex]) {
241
+ handleSuggestionClick(suggestions[selectedIndex]);
242
+ }
243
+ break;
244
+ case "Escape":
245
+ setShowSuggestions(false);
246
+ setSelectedIndex(-1);
247
+ break;
248
+ }
249
+ },
250
+ [showSuggestions, suggestions, selectedIndex, handleSuggestionClick]
251
+ );
252
+
253
+ // Handle focus/blur
254
+ const handleFocus = React.useCallback(() => {
255
+ if (suggestions.length > 0) {
256
+ setShowSuggestions(true);
257
+ }
258
+ }, [suggestions.length]);
259
+
260
+ const handleBlur = React.useCallback(() => {
261
+ // Delay hiding suggestions to allow for clicks
262
+ setTimeout(() => {
263
+ setShowSuggestions(false);
264
+ setSelectedIndex(-1);
265
+ }, 200);
266
+ }, []);
267
+
268
+ // Show suggestions when they arrive
269
+ React.useEffect(() => {
270
+ if (
271
+ suggestions.length > 0 &&
272
+ document.activeElement === inputRef.current
273
+ ) {
274
+ setShowSuggestions(true);
275
+ }
276
+ }, [suggestions.length]);
277
+
278
+ // Scroll selected suggestion into view
279
+ React.useEffect(() => {
280
+ if (
281
+ selectedIndex >= 0 &&
282
+ suggestionsRef.current &&
283
+ selectedIndex < suggestionsRef.current.children.length
284
+ ) {
285
+ const selectedElement = suggestionsRef.current.children[
286
+ selectedIndex
287
+ ] as HTMLElement | null;
288
+ if (selectedElement) {
289
+ selectedElement.scrollIntoView({ block: "nearest" });
290
+ }
291
+ }
292
+ }, [selectedIndex]);
293
+
294
+ // Cleanup timeout on unmount
295
+ React.useEffect(() => {
296
+ return () => {
297
+ if (debounceTimeoutRef.current) {
298
+ clearTimeout(debounceTimeoutRef.current);
299
+ }
300
+ };
301
+ }, []);
302
+
303
+ return (
304
+ <div className={cn("relative", className)}>
305
+ <input
306
+ ref={(node) => {
307
+ // Handle both refs
308
+ if (typeof ref === "function") {
309
+ ref(node);
310
+ } else if (ref) {
311
+ ref.current = node;
312
+ }
313
+ inputRef.current = node;
314
+ }}
315
+ type="text"
316
+ value={inputValue}
317
+ onChange={handleInputChange}
318
+ onKeyDown={handleKeyDown}
319
+ onFocus={handleFocus}
320
+ onBlur={handleBlur}
321
+ placeholder={placeholder}
322
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
323
+ {...props}
324
+ />
325
+
326
+ {/* Close button - only visible when suggestions are shown */}
327
+ {showSuggestions && suggestions.length > 0 && (
328
+ <button
329
+ type="button"
330
+ onClick={handleCloseClick}
331
+ className="absolute right-3 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors z-10"
332
+ aria-label="Close location search"
333
+ >
334
+ ✕
335
+ </button>
336
+ )}
337
+
338
+ {/* Loading indicator */}
339
+ {isLoading && (
340
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
341
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
342
+ </div>
343
+ )}
344
+
345
+ {/* Suggestions dropdown */}
346
+ {showSuggestions && suggestions.length > 0 && (
347
+ <div
348
+ ref={suggestionsRef}
349
+ className={cn(
350
+ "absolute z-50 w-full mt-1 bg-popover border border-border rounded-md shadow-lg max-h-60 overflow-auto",
351
+ suggestionsClassName
352
+ )}
353
+ >
354
+ {suggestions.map((suggestion, index) => (
355
+ <div
356
+ key={suggestion.place_id}
357
+ className={cn(
358
+ "px-3 py-2 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors",
359
+ selectedIndex === index && "bg-accent text-accent-foreground"
360
+ )}
361
+ onClick={() => {
362
+ handleSuggestionClick(suggestion);
363
+ }}
364
+ >
365
+ <div className="font-medium">
366
+ {suggestion.structured_formatting.main_text}
367
+ </div>
368
+ {suggestion.structured_formatting.secondary_text && (
369
+ <div className="text-xs text-muted-foreground">
370
+ {suggestion.structured_formatting.secondary_text}
371
+ </div>
372
+ )}
373
+ </div>
374
+ ))}
375
+ </div>
376
+ )}
377
+ </div>
378
+ );
379
+ }
380
+ );
381
+
382
+ LocationInput.displayName = "LocationInput";
383
+
384
+ export { LocationInput };
@@ -0,0 +1,110 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import * as React from "react";
5
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ /**
10
+ * Root component for the Popover.
11
+ * Manages the open/closed state of the popover.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Popover>
16
+ * <PopoverTrigger asChild>
17
+ * <Button>Open Popover</Button>
18
+ * </PopoverTrigger>
19
+ * <PopoverContent>
20
+ * <p>Popover content goes here</p>
21
+ * </PopoverContent>
22
+ * </Popover>
23
+ * ```
24
+ */
25
+ function Popover({
26
+ ...props
27
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
28
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
29
+ }
30
+
31
+ /**
32
+ * Trigger element that opens/closes the popover when clicked.
33
+ * Can be rendered as a child component using the asChild prop.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <PopoverTrigger asChild>
38
+ * <Button>Open</Button>
39
+ * </PopoverTrigger>
40
+ * ```
41
+ */
42
+ function PopoverTrigger({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
45
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
46
+ }
47
+
48
+ /**
49
+ * Content container for the popover.
50
+ * Displays when the popover is open, positioned relative to the trigger.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <PopoverContent align="start" sideOffset={8}>
55
+ * <div className="grid gap-4">
56
+ * <div className="space-y-2">
57
+ * <h4 className="font-medium">Dimensions</h4>
58
+ * <p className="text-sm text-muted-foreground">
59
+ * Set the dimensions for the layer.
60
+ * </p>
61
+ * </div>
62
+ * </div>
63
+ * </PopoverContent>
64
+ * ```
65
+ */
66
+ function PopoverContent({
67
+ className,
68
+ align,
69
+ sideOffset,
70
+ ...props
71
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
72
+ return (
73
+ <PopoverPrimitive.Portal>
74
+ <PopoverPrimitive.Content
75
+ data-slot="popover-content"
76
+ align={align}
77
+ sideOffset={sideOffset}
78
+ className={cn(
79
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
80
+ className
81
+ )}
82
+ {...props}
83
+ />
84
+ </PopoverPrimitive.Portal>
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Anchor element for the popover.
90
+ * Allows the popover to be positioned relative to a different element than the trigger.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * <Popover>
95
+ * <PopoverAnchor asChild>
96
+ * <div>Anchor element</div>
97
+ * </PopoverAnchor>
98
+ * <PopoverContent>
99
+ * <p>Positioned relative to anchor</p>
100
+ * </PopoverContent>
101
+ * </Popover>
102
+ * ```
103
+ */
104
+ function PopoverAnchor({
105
+ ...props
106
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
107
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
108
+ }
109
+
110
+ export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };