| 1 | "use client"; |
| 2 | |
| 3 | import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; |
| 4 | |
| 5 | import { cn } from "~/lib/utils"; |
| 6 | |
| 7 | const PopoverCreateHandle = PopoverPrimitive.createHandle; |
| 8 | |
| 9 | const Popover = PopoverPrimitive.Root; |
| 10 | |
| 11 | function 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. |
| 21 | const 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. |
| 30 | const 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 | |
| 37 | function 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 | |
| 127 | function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) { |
| 128 | return <PopoverPrimitive.Close data-slot="popover-close" {...props} />; |
| 129 | } |
| 130 | |
| 131 | function 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 | |
| 141 | export { |
| 142 | PopoverCreateHandle, |
| 143 | Popover, |
| 144 | PopoverTrigger, |
| 145 | PopoverPopup, |
| 146 | PopoverPopup as PopoverContent, |
| 147 | PopoverTitle, |
| 148 | PopoverClose, |
| 149 | }; |