@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,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.
|