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