@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,260 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ "use client";
5
+
6
+ import React from "react";
7
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
8
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
9
+
10
+ import { cn } from "../../lib/utils";
11
+
12
+ function DropdownMenu({
13
+ ...props
14
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
15
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
16
+ }
17
+
18
+ function DropdownMenuPortal({
19
+ ...props
20
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
21
+ return (
22
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
23
+ );
24
+ }
25
+
26
+ function DropdownMenuTrigger({
27
+ ...props
28
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
29
+ return (
30
+ <DropdownMenuPrimitive.Trigger
31
+ data-slot="dropdown-menu-trigger"
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function DropdownMenuContent({
38
+ className,
39
+ sideOffset,
40
+ ...props
41
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
42
+ return (
43
+ <DropdownMenuPrimitive.Portal>
44
+ <DropdownMenuPrimitive.Content
45
+ data-slot="dropdown-menu-content"
46
+ sideOffset={sideOffset}
47
+ className={cn(
48
+ "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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ </DropdownMenuPrimitive.Portal>
54
+ );
55
+ }
56
+
57
+ function DropdownMenuGroup({
58
+ ...props
59
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
60
+ return (
61
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
62
+ );
63
+ }
64
+
65
+ function DropdownMenuItem({
66
+ className,
67
+ inset,
68
+ variant,
69
+ ...props
70
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
71
+ inset?: boolean;
72
+ variant?: "default" | "destructive";
73
+ }) {
74
+ return (
75
+ <DropdownMenuPrimitive.Item
76
+ data-slot="dropdown-menu-item"
77
+ data-inset={inset}
78
+ data-variant={variant}
79
+ className={cn(
80
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
81
+ className
82
+ )}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ function DropdownMenuCheckboxItem({
89
+ className,
90
+ children,
91
+ checked,
92
+ ...props
93
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
94
+ return (
95
+ <DropdownMenuPrimitive.CheckboxItem
96
+ data-slot="dropdown-menu-checkbox-item"
97
+ className={cn(
98
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
99
+ className
100
+ )}
101
+ checked={checked}
102
+ {...props}
103
+ >
104
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
105
+ <DropdownMenuPrimitive.ItemIndicator>
106
+ <CheckIcon className="size-4" />
107
+ </DropdownMenuPrimitive.ItemIndicator>
108
+ </span>
109
+ {children}
110
+ </DropdownMenuPrimitive.CheckboxItem>
111
+ );
112
+ }
113
+
114
+ function DropdownMenuRadioGroup({
115
+ ...props
116
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
117
+ return (
118
+ <DropdownMenuPrimitive.RadioGroup
119
+ data-slot="dropdown-menu-radio-group"
120
+ {...props}
121
+ />
122
+ );
123
+ }
124
+
125
+ function DropdownMenuRadioItem({
126
+ className,
127
+ children,
128
+ ...props
129
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
130
+ return (
131
+ <DropdownMenuPrimitive.RadioItem
132
+ data-slot="dropdown-menu-radio-item"
133
+ className={cn(
134
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
135
+ className
136
+ )}
137
+ {...props}
138
+ >
139
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
140
+ <DropdownMenuPrimitive.ItemIndicator>
141
+ <CircleIcon className="size-2 fill-current" />
142
+ </DropdownMenuPrimitive.ItemIndicator>
143
+ </span>
144
+ {children}
145
+ </DropdownMenuPrimitive.RadioItem>
146
+ );
147
+ }
148
+
149
+ function DropdownMenuLabel({
150
+ className,
151
+ inset,
152
+ ...props
153
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
154
+ inset?: boolean;
155
+ }) {
156
+ return (
157
+ <DropdownMenuPrimitive.Label
158
+ data-slot="dropdown-menu-label"
159
+ data-inset={inset}
160
+ className={cn(
161
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
162
+ className
163
+ )}
164
+ {...props}
165
+ />
166
+ );
167
+ }
168
+
169
+ function DropdownMenuSeparator({
170
+ className,
171
+ ...props
172
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
173
+ return (
174
+ <DropdownMenuPrimitive.Separator
175
+ data-slot="dropdown-menu-separator"
176
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
177
+ {...props}
178
+ />
179
+ );
180
+ }
181
+
182
+ function DropdownMenuShortcut({
183
+ className,
184
+ ...props
185
+ }: React.ComponentPropsWithoutRef<"span">) {
186
+ return (
187
+ <span
188
+ data-slot="dropdown-menu-shortcut"
189
+ className={cn(
190
+ "text-muted-foreground ml-auto text-xs tracking-widest",
191
+ className
192
+ )}
193
+ {...props}
194
+ />
195
+ );
196
+ }
197
+
198
+ function DropdownMenuSub({
199
+ ...props
200
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
201
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
202
+ }
203
+
204
+ function DropdownMenuSubTrigger({
205
+ className,
206
+ inset,
207
+ children,
208
+ ...props
209
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
210
+ inset?: boolean;
211
+ }) {
212
+ return (
213
+ <DropdownMenuPrimitive.SubTrigger
214
+ data-slot="dropdown-menu-sub-trigger"
215
+ data-inset={inset}
216
+ className={cn(
217
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
218
+ className
219
+ )}
220
+ {...props}
221
+ >
222
+ {children}
223
+ <ChevronRightIcon className="ml-auto size-4" />
224
+ </DropdownMenuPrimitive.SubTrigger>
225
+ );
226
+ }
227
+
228
+ function DropdownMenuSubContent({
229
+ className,
230
+ ...props
231
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
232
+ return (
233
+ <DropdownMenuPrimitive.SubContent
234
+ data-slot="dropdown-menu-sub-content"
235
+ className={cn(
236
+ "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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
237
+ className
238
+ )}
239
+ {...props}
240
+ />
241
+ );
242
+ }
243
+
244
+ export {
245
+ DropdownMenu,
246
+ DropdownMenuCheckboxItem,
247
+ DropdownMenuContent,
248
+ DropdownMenuGroup,
249
+ DropdownMenuItem,
250
+ DropdownMenuLabel,
251
+ DropdownMenuPortal,
252
+ DropdownMenuRadioGroup,
253
+ DropdownMenuRadioItem,
254
+ DropdownMenuSeparator,
255
+ DropdownMenuShortcut,
256
+ DropdownMenuSub,
257
+ DropdownMenuSubContent,
258
+ DropdownMenuSubTrigger,
259
+ DropdownMenuTrigger,
260
+ };
@@ -0,0 +1,86 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // UI Components - shadcn/ui primitives and styled components
5
+ export { Avatar, AvatarFallback, AvatarImage } from "./avatar.tsx";
6
+
7
+ export { Badge } from "./badge.tsx";
8
+ export type { BadgeProps } from "./badge.tsx";
9
+ export { badgeVariants } from "./badgeVariants.ts";
10
+ export type { BadgeVariantsProps } from "./badgeVariants.ts";
11
+
12
+ export { Button } from "./button.tsx";
13
+ export { buttonVariants } from "./buttonVariants.ts";
14
+ export type { ButtonVariantsProps } from "./buttonVariants.ts";
15
+
16
+ export { ButtonGroup } from "./button-group.tsx";
17
+ export { buttonGroupVariants } from "./buttonGroupVariants.ts";
18
+
19
+ export { Calendar } from "./calendar.tsx";
20
+
21
+ export {
22
+ Card,
23
+ CardContent,
24
+ CardDescription,
25
+ CardFooter,
26
+ CardHeader,
27
+ CardTitle,
28
+ } from "./card.tsx";
29
+
30
+ export {
31
+ Dialog,
32
+ DialogContent,
33
+ DialogDescription,
34
+ DialogFooter,
35
+ DialogHeader,
36
+ DialogTitle,
37
+ DialogTrigger,
38
+ } from "./dialog.tsx";
39
+
40
+ export {
41
+ Drawer,
42
+ DrawerContent,
43
+ DrawerDescription,
44
+ DrawerFooter,
45
+ DrawerHeader,
46
+ DrawerTitle,
47
+ DrawerTrigger,
48
+ } from "./drawer.tsx";
49
+
50
+ export {
51
+ DropdownMenu,
52
+ DropdownMenuContent,
53
+ DropdownMenuItem,
54
+ DropdownMenuLabel,
55
+ DropdownMenuSeparator,
56
+ DropdownMenuTrigger,
57
+ } from "./dropdown-menu.tsx";
58
+
59
+ export { Input } from "./input.tsx";
60
+
61
+ export { Label } from "./label.tsx";
62
+
63
+ export { LocationInput } from "./location-input.tsx";
64
+
65
+ export { Popover, PopoverContent, PopoverTrigger } from "./popover.tsx";
66
+
67
+ export {
68
+ Select,
69
+ SelectContent,
70
+ SelectItem,
71
+ SelectTrigger,
72
+ SelectValue,
73
+ } from "./select.tsx";
74
+
75
+ export { Separator } from "./separator.tsx";
76
+
77
+ export { Toaster } from "./sonner.tsx";
78
+
79
+ export { Textarea } from "./textarea.tsx";
80
+
81
+ export {
82
+ Tooltip,
83
+ TooltipContent,
84
+ TooltipProvider,
85
+ TooltipTrigger,
86
+ } from "./tooltip.tsx";
@@ -0,0 +1,59 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ /**
9
+ * Input component for text entry.
10
+ *
11
+ * Supports all standard HTML input types and provides consistent styling
12
+ * with focus states, disabled states, and error states.
13
+ * Forwards ref to the underlying native input.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // Basic text input
18
+ * <Input type="text" placeholder="Enter your name" />
19
+ *
20
+ * // Email input
21
+ * <Input type="email" placeholder="email@example.com" />
22
+ *
23
+ * // Password input
24
+ * <Input type="password" placeholder="Enter password" />
25
+ *
26
+ * // With error state
27
+ * <Input aria-invalid="true" placeholder="Invalid input" />
28
+ *
29
+ * // Disabled input
30
+ * <Input disabled placeholder="Disabled input" />
31
+ *
32
+ * // With label
33
+ * <div>
34
+ * <Label htmlFor="email">Email</Label>
35
+ * <Input id="email" type="email" />
36
+ * </div>
37
+ * ```
38
+ */
39
+ const Input = React.forwardRef<
40
+ HTMLInputElement,
41
+ React.ComponentPropsWithoutRef<"input">
42
+ >(function Input({ className, type, ...props }, ref) {
43
+ return (
44
+ <input
45
+ ref={ref}
46
+ type={type}
47
+ data-slot="input"
48
+ className={cn(
49
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
50
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
51
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
52
+ className
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ });
58
+
59
+ export { Input };
@@ -0,0 +1,47 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import * as React from "react";
5
+ import * as LabelPrimitive from "@radix-ui/react-label";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ /**
10
+ * Label component for form elements.
11
+ *
12
+ * Built on top of Radix UI's Label primitive for accessibility.
13
+ * Provides proper association with form controls and disabled state handling.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // Basic label
18
+ * <Label htmlFor="email">Email Address</Label>
19
+ * <Input id="email" type="email" />
20
+ *
21
+ * // With required indicator
22
+ * <Label htmlFor="name">
23
+ * Name <span className="text-red-500">*</span>
24
+ * </Label>
25
+ *
26
+ * // Associated with disabled input
27
+ * <Label htmlFor="disabled">Disabled Field</Label>
28
+ * <Input id="disabled" disabled />
29
+ * ```
30
+ */
31
+ function Label({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
35
+ return (
36
+ <LabelPrimitive.Root
37
+ data-slot="label"
38
+ className={cn(
39
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ export { Label };
@@ -0,0 +1,194 @@
1
+ # LocationInput Component
2
+
3
+ A reusable React component that provides Google Maps autocomplete functionality
4
+ for location lookup services.
5
+
6
+ ## Features
7
+
8
+ - **Google Maps Autocomplete**: Real-time location suggestions as you type
9
+ - **Keyboard Navigation**: Use arrow keys to navigate through suggestions
10
+ - **Debounced Search**: Configurable debounce to prevent excessive API calls
11
+ - **Loading States**: Visual loading indicator during API requests
12
+ - **Accessible**: Full keyboard support and ARIA-compliant
13
+ - **Customizable**: Styling and behavior can be customized via props
14
+ - **TypeScript Support**: Full type safety with TypeScript definitions
15
+
16
+ ## Prerequisites
17
+
18
+ 1. **Google Maps API Key**: You need a valid Google Maps API key with the Places
19
+ API enabled
20
+ 2. **API Key Setup**: Enable the following APIs in your Google Cloud Console:
21
+ - Places API
22
+ - Maps JavaScript API
23
+
24
+ ## Installation
25
+
26
+ The component is already included in your project at
27
+ `src/components/ui/location-input.tsx`.
28
+
29
+ ## Usage
30
+
31
+ ### Basic Usage
32
+
33
+ ```tsx
34
+ import {
35
+ LocationInput,
36
+ LocationSuggestion,
37
+ } from "./components/ui/location-input";
38
+
39
+ function MyComponent() {
40
+ const handleLocationSelect = (location: LocationSuggestion) => {
41
+ console.log("Selected location:", location);
42
+ };
43
+
44
+ return (
45
+ <LocationInput
46
+ apiKey="YOUR_GOOGLE_MAPS_API_KEY"
47
+ onLocationSelect={handleLocationSelect}
48
+ placeholder="Search for a location..."
49
+ />
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### Advanced Usage
55
+
56
+ ```tsx
57
+ import {
58
+ LocationInput,
59
+ LocationSuggestion,
60
+ } from "./components/ui/location-input";
61
+
62
+ function MyComponent() {
63
+ const [selectedLocation, setSelectedLocation] = useState<
64
+ LocationSuggestion | null
65
+ >(null);
66
+
67
+ const handleLocationSelect = (location: LocationSuggestion) => {
68
+ setSelectedLocation(location);
69
+ // You can now use the location data for your application
70
+ console.log("Place ID:", location.place_id);
71
+ console.log("Description:", location.description);
72
+ };
73
+
74
+ const handleInputChange = (value: string) => {
75
+ // Handle input changes (e.g., for form validation)
76
+ console.log("Input value:", value);
77
+ };
78
+
79
+ return (
80
+ <LocationInput
81
+ apiKey="YOUR_GOOGLE_MAPS_API_KEY"
82
+ onLocationSelect={handleLocationSelect}
83
+ onInputChange={handleInputChange}
84
+ placeholder="Enter your location..."
85
+ debounceMs={500} // Custom debounce time
86
+ className="w-full max-w-md"
87
+ suggestionsClassName="custom-suggestions"
88
+ />
89
+ );
90
+ }
91
+ ```
92
+
93
+ ## Props
94
+
95
+ ### LocationInputProps
96
+
97
+ | Prop | Type | Default | Description |
98
+ | ---------------------- | ---------------------------------------- | ---------------------------- | ---------------------------------------- |
99
+ | `apiKey` | `string` | - | **Required**. Your Google Maps API key |
100
+ | `onLocationSelect` | `(location: LocationSuggestion) => void` | - | Callback when a location is selected |
101
+ | `onInputChange` | `(value: string) => void` | - | Callback when input value changes |
102
+ | `placeholder` | `string` | `"Search for a location..."` | Input placeholder text |
103
+ | `className` | `string` | - | CSS classes for the input container |
104
+ | `suggestionsClassName` | `string` | - | CSS classes for the suggestions dropdown |
105
+ | `debounceMs` | `number` | `300` | Debounce time in milliseconds |
106
+ | `...props` | `HTMLInputElement` | - | All standard HTML input props |
107
+
108
+ ### LocationSuggestion Interface
109
+
110
+ ```typescript
111
+ interface LocationSuggestion {
112
+ place_id: string;
113
+ description: string;
114
+ structured_formatting: {
115
+ main_text: string;
116
+ secondary_text: string;
117
+ };
118
+ }
119
+ ```
120
+
121
+ ## Keyboard Navigation
122
+
123
+ - **Arrow Down**: Navigate to next suggestion
124
+ - **Arrow Up**: Navigate to previous suggestion
125
+ - **Enter**: Select the currently highlighted suggestion
126
+ - **Escape**: Close the suggestions dropdown
127
+
128
+ ## Styling
129
+
130
+ The component uses Tailwind CSS classes and follows your existing design system.
131
+ You can customize the appearance by:
132
+
133
+ 1. **Container Styling**: Use the `className` prop to style the input container
134
+ 2. **Suggestions Styling**: Use the `suggestionsClassName` prop to style the
135
+ dropdown
136
+ 3. **CSS Customization**: Override the default classes in your CSS
137
+
138
+ ## API Key Security
139
+
140
+ **Important**: Never expose your API key in client-side code for production
141
+ applications. Consider:
142
+
143
+ 1. **Environment Variables**: Store the API key in environment variables
144
+ 2. **Backend Proxy**: Create a backend endpoint that proxies requests to Google
145
+ Maps API
146
+ 3. **API Key Restrictions**: Set up proper restrictions in Google Cloud Console
147
+
148
+ ### Environment Variable Example
149
+
150
+ ```tsx
151
+ <LocationInput
152
+ apiKey={import.meta.env.VITE_GOOGLE_MAPS_API_KEY}
153
+ onLocationSelect={handleLocationSelect}
154
+ />;
155
+ ```
156
+
157
+ ## Error Handling
158
+
159
+ The component includes built-in error handling for:
160
+
161
+ - Network errors
162
+ - API quota exceeded
163
+ - Invalid API key
164
+ - No results found
165
+
166
+ Errors are logged to the console and the suggestions list will be empty.
167
+
168
+ ## Performance Considerations
169
+
170
+ - **Debouncing**: Default 300ms debounce prevents excessive API calls
171
+ - **Lazy Loading**: Google Maps API is only loaded when needed
172
+ - **Memory Management**: Proper cleanup of timeouts and event listeners
173
+
174
+ ## Browser Support
175
+
176
+ - Modern browsers with ES6+ support
177
+ - Requires internet connection for Google Maps API
178
+
179
+ ## Troubleshooting
180
+
181
+ ### Common Issues
182
+
183
+ 1. **No suggestions appearing**: Check your API key and ensure Places API is
184
+ enabled
185
+ 2. **CORS errors**: Ensure your API key has proper domain restrictions
186
+ 3. **Quota exceeded**: Check your Google Cloud Console for usage limits
187
+
188
+ ### Debug Mode
189
+
190
+ Enable browser developer tools to see detailed error messages and API responses.
191
+
192
+ ## Example Implementation
193
+
194
+ See `src/components/LocationInputExample.tsx` for a complete working example.