1"use client";
2
3import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
4
5import { cn } from "~/lib/utils";
6
7const PopoverCreateHandle = PopoverPrimitive.createHandle;
8
9const Popover = PopoverPrimitive.Root;
10
11function PopoverTrigger({ className, children, ...props }: PopoverPrimitive.Trigger.Props) {
12 return (
13 <PopoverPrimitive.Trigger className={className} data-slot="popover-trigger" {...props}>
14 {children}
15 </PopoverPrimitive.Trigger>
16 );
17}
18
19// Popovers hold prose and forms, so a width is fixed rather than a minimum,
20// and every width is capped to the viewport.
21const popoverPopupWidthClassName = {
22 auto: "",
23 sm: "w-64",
24 md: "w-80",
25 lg: "w-96",
26} as const;
27
28// The inset around the content. "compact" suits dense content (a list, a code excerpt, a
29// row of reactions); "none" is for content that draws its own frame edge to edge.
30const popoverViewportPaddingClassName = {
31 default: "py-4 [--viewport-inline-padding:--spacing(4)]",
32 compact: "py-2 [--viewport-inline-padding:--spacing(3)]",
33 // Rounded to the popup so edge-to-edge content clips to its corners.
34 none: "rounded-[calc(var(--radius-lg)-1px)] py-0 [--viewport-inline-padding:0px]",
35} as const;
36
37function PopoverPopup({
38 children,
39 className,
40 padding = "default",
41 variant = "default",
42 width = "auto",
43 side = "bottom",
44 align = "center",
45 sideOffset = 4,
46 alignOffset = 0,
47 collisionAvoidance,
48 tooltipStyle = false,
49 keepMounted = false,
50 anchor,
51 ...props
52}: PopoverPrimitive.Popup.Props & {
53 padding?: keyof typeof popoverViewportPaddingClassName;
54 variant?: "default" | "panel";
55 side?: PopoverPrimitive.Positioner.Props["side"];
56 align?: PopoverPrimitive.Positioner.Props["align"];
57 sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
58 alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"];
59 collisionAvoidance?: PopoverPrimitive.Positioner.Props["collisionAvoidance"];
60 tooltipStyle?: boolean;
61 keepMounted?: PopoverPrimitive.Portal.Props["keepMounted"];
62 anchor?: PopoverPrimitive.Positioner.Props["anchor"];
63 width?: keyof typeof popoverPopupWidthClassName;
64}) {
65 // Viewport rekeys its children when the active trigger clears on close. Persistent
66 // single-trigger forms need a stable container to retain drafts and submit guards.
67 const Viewport = keepMounted ? "div" : PopoverPrimitive.Viewport;
68 return (
69 <PopoverPrimitive.Portal keepMounted={keepMounted}>
70 <PopoverPrimitive.Positioner
71 align={align}
72 alignOffset={alignOffset}
73 anchor={anchor}
74 collisionAvoidance={collisionAvoidance}
75 // A panel's card already sits inside its own padding, so the panel can reach the edge.
76 collisionPadding={variant === "panel" ? 0 : undefined}
77 className={cn(
78 "h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-transform data-instant:transition-none",
79 variant === "panel"
80 ? // The card inside is the panel width, or the anchor less 12px per side; the viewport
81 // pads it by --spacing(2) per side. Panels slide when their offset changes, e.g. to make
82 // room for a bar below the header, even when opened from the keyboard (data-instant).
83 "z-(--z-sheet) w-[min(calc(var(--thread-details-panel-width)+--spacing(4)),calc(var(--anchor-width)---spacing(2)))] transition-[top] duration-150 ease-out data-instant:transition-[top] motion-reduce:transition-none motion-reduce:data-instant:transition-none"
84 : "z-[130]",
85 )}
86 data-slot="popover-positioner"
87 side={side}
88 sideOffset={sideOffset}
89 >
90 <PopoverPrimitive.Popup
91 className={cn(
92 "relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg text-popover-foreground outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
93 variant !== "panel" && "dropdown-glass",
94 tooltipStyle &&
95 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
96 !tooltipStyle &&
97 variant !== "panel" &&
98 "shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",
99 width !== "auto" && ["max-w-[calc(100vw-2rem)]", popoverPopupWidthClassName[width]],
100 variant === "panel" &&
101 "w-full overflow-visible rounded-none border-0 bg-transparent shadow-none before:hidden [backdrop-filter:none] [-webkit-backdrop-filter:none]",
102 className,
103 )}
104 data-slot="popover-popup"
105 {...props}
106 >
107 <Viewport
108 className={cn(
109 "relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) has-data-[slot=calendar]:p-2 data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity",
110 tooltipStyle && padding === "default"
111 ? "py-1 [--viewport-inline-padding:--spacing(2)]"
112 : popoverViewportPaddingClassName[padding],
113 !tooltipStyle && "not-data-transitioning:overflow-y-auto",
114 variant === "panel" &&
115 "overflow-visible py-2 [--viewport-inline-padding:--spacing(2)]",
116 )}
117 data-slot="popover-viewport"
118 >
119 {children}
120 </Viewport>
121 </PopoverPrimitive.Popup>
122 </PopoverPrimitive.Positioner>
123 </PopoverPrimitive.Portal>
124 );
125}
126
127function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
128 return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
129}
130
131function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
132 return (
133 <PopoverPrimitive.Title
134 className={cn("font-semibold text-sm leading-none", className)}
135 data-slot="popover-title"
136 {...props}
137 />
138 );
139}
140
141export {
142 PopoverCreateHandle,
143 Popover,
144 PopoverTrigger,
145 PopoverPopup,
146 PopoverPopup as PopoverContent,
147 PopoverTitle,
148 PopoverClose,
149};