aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/browser-extension/package.json1
-rw-r--r--apps/browser-extension/src/BookmarkDeletedPage.tsx2
-rw-r--r--apps/browser-extension/src/BookmarkSavedPage.tsx30
-rw-r--r--apps/browser-extension/src/Layout.tsx2
-rw-r--r--apps/browser-extension/src/components/BookmarkLists.tsx45
-rw-r--r--apps/browser-extension/src/components/ListsSelector.tsx107
-rw-r--r--apps/browser-extension/src/components/TagList.tsx32
-rw-r--r--apps/browser-extension/src/components/TagsSelector.tsx117
-rw-r--r--apps/browser-extension/src/components/ui/badge.tsx37
-rw-r--r--apps/browser-extension/src/components/ui/command.tsx155
-rw-r--r--apps/browser-extension/src/components/ui/popover.tsx29
11 files changed, 549 insertions, 8 deletions
diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json
index 9b204519..6c40d372 100644
--- a/apps/browser-extension/package.json
+++ b/apps/browser-extension/package.json
@@ -23,6 +23,7 @@
"@trpc/next": "11.0.0-next-beta.308",
"@trpc/react-query": "11.0.0-next-beta.308",
"@trpc/server": "11.0.0-next-beta.308",
+ "@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^1.0.0",
diff --git a/apps/browser-extension/src/BookmarkDeletedPage.tsx b/apps/browser-extension/src/BookmarkDeletedPage.tsx
index 23e1d9da..c26e9616 100644
--- a/apps/browser-extension/src/BookmarkDeletedPage.tsx
+++ b/apps/browser-extension/src/BookmarkDeletedPage.tsx
@@ -1,3 +1,3 @@
export default function BookmarkDeletedPage() {
- return <p className="text-lg">Bookmark Deleted!</p>;
+ return <p className="text-xl">Bookmark Deleted!</p>;
}
diff --git a/apps/browser-extension/src/BookmarkSavedPage.tsx b/apps/browser-extension/src/BookmarkSavedPage.tsx
index 3535ade8..2c594ad8 100644
--- a/apps/browser-extension/src/BookmarkSavedPage.tsx
+++ b/apps/browser-extension/src/BookmarkSavedPage.tsx
@@ -4,7 +4,13 @@ import { Link, useNavigate, useParams } from "react-router-dom";
import { useDeleteBookmark } from "@hoarder/shared-react/hooks/bookmarks";
+import BookmarkLists from "./components/BookmarkLists";
+import { ListsSelector } from "./components/ListsSelector";
+import TagList from "./components/TagList";
+import { TagsSelector } from "./components/TagsSelector";
+import { Button, buttonVariants } from "./components/ui/button";
import Spinner from "./Spinner";
+import { cn } from "./utils/css";
import usePluginSettings from "./utils/settings";
export default function BookmarkSavedPage() {
@@ -31,10 +37,13 @@ export default function BookmarkSavedPage() {
<div className="flex flex-col gap-2">
{error && <p className="text-red-500">{error}</p>}
<div className="flex items-center justify-between gap-2">
- <p className="text-lg">Bookmarked!</p>
+ <p className="text-xl">Hoarded!</p>
<div className="flex gap-2">
<Link
- className="flex gap-2 rounded-md p-3 text-black hover:text-black"
+ className={cn(
+ buttonVariants({ variant: "link" }),
+ "flex gap-2 rounded-md p-3",
+ )}
target="_blank"
rel="noreferrer"
to={`${settings.address}/dashboard/preview/${bookmarkId}`}
@@ -42,9 +51,10 @@ export default function BookmarkSavedPage() {
<ArrowUpRightFromSquare className="my-auto" size="20" />
<p className="my-auto">Open</p>
</Link>
- <button
- onClick={() => deleteBookmark({ bookmarkId: bookmarkId })}
- className="flex gap-2 bg-transparent text-red-500 hover:text-red-500"
+ <Button
+ variant="link"
+ onClick={() => deleteBookmark({ bookmarkId })}
+ className="flex gap-2 text-red-500 hover:text-red-500"
>
{!isPending ? (
<>
@@ -56,9 +66,17 @@ export default function BookmarkSavedPage() {
<Spinner />
</span>
)}
- </button>
+ </Button>
</div>
</div>
+ <hr />
+ <p className="text-lg">Tags</p>
+ <TagList bookmarkId={bookmarkId} />
+ <TagsSelector bookmarkId={bookmarkId} />
+ <hr />
+ <p className="text-lg">Lists</p>
+ <BookmarkLists bookmarkId={bookmarkId} />
+ <ListsSelector bookmarkId={bookmarkId} />
</div>
);
}
diff --git a/apps/browser-extension/src/Layout.tsx b/apps/browser-extension/src/Layout.tsx
index 0431f550..5c6eb3d2 100644
--- a/apps/browser-extension/src/Layout.tsx
+++ b/apps/browser-extension/src/Layout.tsx
@@ -18,7 +18,7 @@ export default function Layout() {
return (
<div className="flex flex-col space-y-2">
- <div className="rounded-md bg-yellow-100 p-4 text-black">
+ <div className="rounded-md bg-gray-100 p-4 dark:bg-gray-900">
<Outlet />
</div>
<hr />
diff --git a/apps/browser-extension/src/components/BookmarkLists.tsx b/apps/browser-extension/src/components/BookmarkLists.tsx
new file mode 100644
index 00000000..9ccd8951
--- /dev/null
+++ b/apps/browser-extension/src/components/BookmarkLists.tsx
@@ -0,0 +1,45 @@
+import { X } from "lucide-react";
+
+import {
+ useBookmarkLists,
+ useRemoveBookmarkFromList,
+} from "@hoarder/shared-react/hooks/lists";
+
+import { api } from "../utils/trpc";
+import { Button } from "./ui/button";
+
+export default function BookmarkLists({ bookmarkId }: { bookmarkId: string }) {
+ const { data: allLists } = useBookmarkLists();
+
+ const { mutate: deleteFromList } = useRemoveBookmarkFromList();
+
+ const { data: lists } = api.lists.getListsOfBookmark.useQuery({ bookmarkId });
+ if (!lists || !allLists) {
+ return null;
+ }
+
+ return (
+ <ul className="flex flex-col gap-1">
+ {lists.lists.map((l) => (
+ <li
+ key={l.id}
+ className="flex items-center justify-between rounded border border-border bg-background p-2 text-sm text-foreground"
+ >
+ <span>
+ {allLists
+ .getPathById(l.id)!
+ .map((l) => `${l.icon} ${l.name}`)
+ .join(" / ")}
+ </span>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => deleteFromList({ bookmarkId, listId: l.id })}
+ >
+ <X className="size-4" />
+ </Button>
+ </li>
+ ))}
+ </ul>
+ );
+}
diff --git a/apps/browser-extension/src/components/ListsSelector.tsx b/apps/browser-extension/src/components/ListsSelector.tsx
new file mode 100644
index 00000000..adddf4b4
--- /dev/null
+++ b/apps/browser-extension/src/components/ListsSelector.tsx
@@ -0,0 +1,107 @@
+import * as React from "react";
+import { useSet } from "@uidotdev/usehooks";
+import { Check, ChevronsUpDown } from "lucide-react";
+
+import {
+ useAddBookmarkToList,
+ useBookmarkLists,
+ useRemoveBookmarkFromList,
+} from "@hoarder/shared-react/hooks/lists";
+
+import { cn } from "../utils/css";
+import { api } from "../utils/trpc";
+import { Button } from "./ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "./ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
+
+export function ListsSelector({ bookmarkId }: { bookmarkId: string }) {
+ const currentlyUpdating = useSet<string>();
+ const [open, setOpen] = React.useState(false);
+
+ const { mutate: addToList } = useAddBookmarkToList();
+ const { mutate: removeFromList } = useRemoveBookmarkFromList();
+ const { data: existingLists } = api.lists.getListsOfBookmark.useQuery({
+ bookmarkId,
+ });
+
+ const { data: allLists } = useBookmarkLists();
+
+ const existingListIds = new Set(existingLists?.lists.map((list) => list.id));
+
+ const toggleList = (listId: string) => {
+ currentlyUpdating.add(listId);
+ if (existingListIds.has(listId)) {
+ removeFromList(
+ { bookmarkId, listId },
+ {
+ onSettled: (_resp, _err, req) => currentlyUpdating.delete(req.listId),
+ },
+ );
+ } else {
+ addToList(
+ { bookmarkId, listId },
+ {
+ onSettled: (_resp, _err, req) => currentlyUpdating.delete(req.listId),
+ },
+ );
+ }
+ };
+
+ return (
+ <Popover open={open} onOpenChange={setOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={open}
+ className="justify-between"
+ >
+ Add to List...
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[200px] p-0">
+ <Command>
+ <CommandInput placeholder="Search Lists ..." />
+ <CommandList>
+ <CommandEmpty>You don&apos;t have any lists.</CommandEmpty>
+ <CommandGroup>
+ {allLists?.allPaths.map((path) => {
+ const lastItem = path[path.length - 1];
+
+ return (
+ <CommandItem
+ key={lastItem.id}
+ value={lastItem.id}
+ keywords={[lastItem.name, lastItem.icon]}
+ onSelect={toggleList}
+ disabled={currentlyUpdating.has(lastItem.id)}
+ >
+ <Check
+ className={cn(
+ "mr-2 size-4",
+ existingListIds.has(lastItem.id)
+ ? "opacity-100"
+ : "opacity-0",
+ )}
+ />
+ {path
+ .map((item) => `${item.icon} ${item.name}`)
+ .join(" / ")}
+ </CommandItem>
+ );
+ })}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+}
diff --git a/apps/browser-extension/src/components/TagList.tsx b/apps/browser-extension/src/components/TagList.tsx
new file mode 100644
index 00000000..96b8501e
--- /dev/null
+++ b/apps/browser-extension/src/components/TagList.tsx
@@ -0,0 +1,32 @@
+import { useAutoRefreshingBookmarkQuery } from "@hoarder/shared-react/hooks/bookmarks";
+import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils";
+
+import { Badge } from "./ui/badge";
+
+export default function TagList({ bookmarkId }: { bookmarkId: string }) {
+ const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });
+ if (!bookmark) {
+ return null;
+ }
+
+ return (
+ <div className="flex flex-wrap gap-1">
+ {bookmark.tags.length === 0 && !isBookmarkStillTagging(bookmark) && (
+ <Badge variant="secondary">No tags</Badge>
+ )}
+ {bookmark.tags.map((tag) => (
+ <Badge
+ key={tag.id}
+ className={
+ tag.attachedBy == "ai" ? "bg-purple-500 text-white" : undefined
+ }
+ >
+ {tag.name}
+ </Badge>
+ ))}
+ {isBookmarkStillTagging(bookmark) && (
+ <Badge variant="secondary">AI tags loading...</Badge>
+ )}
+ </div>
+ );
+}
diff --git a/apps/browser-extension/src/components/TagsSelector.tsx b/apps/browser-extension/src/components/TagsSelector.tsx
new file mode 100644
index 00000000..45cf11d5
--- /dev/null
+++ b/apps/browser-extension/src/components/TagsSelector.tsx
@@ -0,0 +1,117 @@
+import * as React from "react";
+import { useSet } from "@uidotdev/usehooks";
+import { Check, ChevronsUpDown, Plus } from "lucide-react";
+
+import {
+ useAutoRefreshingBookmarkQuery,
+ useUpdateBookmarkTags,
+} from "@hoarder/shared-react/hooks/bookmarks";
+
+import { cn } from "../utils/css";
+import { api } from "../utils/trpc";
+import { Button } from "./ui/button";
+import {
+ Command,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "./ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
+
+export function TagsSelector({ bookmarkId }: { bookmarkId: string }) {
+ const { data: allTags } = api.tags.list.useQuery();
+ const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });
+
+ const existingTagIds = new Set(bookmark?.tags.map((t) => t.id) ?? []);
+
+ const [input, setInput] = React.useState("");
+ const [open, setOpen] = React.useState(false);
+ const currentlyUpdating = useSet<string>();
+
+ const { mutate } = useUpdateBookmarkTags({
+ onMutate: (req) => {
+ req.attach.forEach((t) => currentlyUpdating.add(t.tagId ?? ""));
+ req.detach.forEach((t) => currentlyUpdating.add(t.tagId));
+ },
+ onSettled: (_resp, _err, req) => {
+ if (!req) {
+ return;
+ }
+ req.attach.forEach((t) => currentlyUpdating.delete(t.tagId ?? ""));
+ req.detach.forEach((t) => currentlyUpdating.delete(t.tagId));
+ },
+ });
+
+ const toggleTag = (tagId: string) => {
+ mutate({
+ bookmarkId,
+ attach: existingTagIds.has(tagId) ? [] : [{ tagId }],
+ detach: existingTagIds.has(tagId) ? [{ tagId }] : [],
+ });
+ };
+
+ return (
+ <Popover open={open} onOpenChange={setOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={open}
+ className="justify-between"
+ >
+ Add Tags...
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[200px] p-0">
+ <Command>
+ <CommandInput
+ value={input}
+ onValueChange={setInput}
+ placeholder="Search Tags ..."
+ />
+ <CommandList>
+ <CommandGroup>
+ {allTags?.tags
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((tag) => (
+ <CommandItem
+ key={tag.id}
+ value={tag.id}
+ keywords={[tag.name]}
+ onSelect={toggleTag}
+ disabled={currentlyUpdating.has(tag.id)}
+ >
+ <Check
+ className={cn(
+ "mr-2 size-4",
+ existingTagIds.has(tag.id)
+ ? "opacity-100"
+ : "opacity-0",
+ )}
+ />
+ {tag.name}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ <CommandGroup>
+ <CommandItem
+ onSelect={() =>
+ mutate({
+ bookmarkId,
+ attach: [{ tagName: input }],
+ detach: [],
+ })
+ }
+ >
+ <Plus className="mr-2 size-4" />
+ Create &quot;{input}&quot; ...
+ </CommandItem>
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+}
diff --git a/apps/browser-extension/src/components/ui/badge.tsx b/apps/browser-extension/src/components/ui/badge.tsx
new file mode 100644
index 00000000..0b043596
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/badge.tsx
@@ -0,0 +1,37 @@
+import type { VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cva } from "class-variance-authority";
+
+import { cn } from "../../utils/css";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes<HTMLDivElement>,
+ VariantProps<typeof badgeVariants> {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/browser-extension/src/components/ui/command.tsx b/apps/browser-extension/src/components/ui/command.tsx
new file mode 100644
index 00000000..2d5cd64e
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/command.tsx
@@ -0,0 +1,155 @@
+import type { DialogProps } from "@radix-ui/react-dialog";
+import * as React from "react";
+import { Command as CommandPrimitive } from "cmdk";
+import { Search } from "lucide-react";
+
+import { cn } from "../../utils/css";
+import { Dialog, DialogContent } from "./dialog";
+
+const Command = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive
+ ref={ref}
+ className={cn(
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
+ className,
+ )}
+ {...props}
+ />
+));
+Command.displayName = CommandPrimitive.displayName;
+
+type CommandDialogProps = DialogProps;
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+ <Dialog {...props}>
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+ {children}
+ </Command>
+ </DialogContent>
+ </Dialog>
+ );
+};
+
+const CommandInput = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Input>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
+>(({ className, ...props }, ref) => (
+ // https://github.com/shadcn-ui/ui/issues/3366
+ // eslint-disable-next-line react/no-unknown-property
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+ <CommandPrimitive.Input
+ ref={ref}
+ className={cn(
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ {...props}
+ />
+ </div>
+));
+
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+
+const CommandList = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.List
+ ref={ref}
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
+ {...props}
+ />
+));
+
+CommandList.displayName = CommandPrimitive.List.displayName;
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Empty>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
+>((props, ref) => (
+ <CommandPrimitive.Empty
+ ref={ref}
+ className="py-6 text-center text-sm"
+ {...props}
+ />
+));
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Group>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Group
+ ref={ref}
+ className={cn(
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
+ className,
+ )}
+ {...props}
+ />
+));
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 h-px bg-border", className)}
+ {...props}
+ />
+));
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const CommandItem = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
+ className,
+ )}
+ {...props}
+ />
+));
+
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+ return (
+ <span
+ className={cn(
+ "ml-auto text-xs tracking-widest text-muted-foreground",
+ className,
+ )}
+ {...props}
+ />
+ );
+};
+CommandShortcut.displayName = "CommandShortcut";
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+};
diff --git a/apps/browser-extension/src/components/ui/popover.tsx b/apps/browser-extension/src/components/ui/popover.tsx
new file mode 100644
index 00000000..8b20f11b
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/popover.tsx
@@ -0,0 +1,29 @@
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "../../utils/css";
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef<typeof PopoverPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+ <PopoverPrimitive.Portal>
+ <PopoverPrimitive.Content
+ ref={ref}
+ align={align}
+ sideOffset={sideOffset}
+ className={cn(
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
+ className,
+ )}
+ {...props}
+ />
+ </PopoverPrimitive.Portal>
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };