| 1 | "use client"; |
| 2 | |
| 3 | import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; |
| 4 | |
| 5 | import { cn } from "~/lib/utils"; |
| 6 | |
| 7 | function getVirtualizedScrollFadeClassName({ top, bottom }: { top: boolean; bottom: boolean }) { |
| 8 | if (!top && !bottom) return undefined; |
| 9 | |
| 10 | return cn( |
| 11 | "virtualized-scroll-fade [--fade-size:1.5rem]", |
| 12 | top && |
| 13 | bottom && |
| 14 | "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,transparent,black_var(--fade-size),black_calc(100%-var(--fade-size)),transparent)]", |
| 15 | top && |
| 16 | !bottom && |
| 17 | "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,transparent,black_var(--fade-size))]", |
| 18 | !top && |
| 19 | bottom && |
| 20 | "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,black_calc(100%-var(--fade-size)),transparent)]", |
| 21 | ); |
| 22 | } |
| 23 | |
| 24 | function ScrollArea({ |
| 25 | className, |
| 26 | children, |
| 27 | scrollFade = false, |
| 28 | scrollFadePadding = true, |
| 29 | scrollbarGutter = false, |
| 30 | hideScrollbars = false, |
| 31 | chainVerticalScroll = false, |
| 32 | radius = "inherit", |
| 33 | viewportTabIndex, |
| 34 | ...props |
| 35 | }: ScrollAreaPrimitive.Root.Props & { |
| 36 | scrollFade?: boolean; |
| 37 | /** Keep focused and highlighted items clear of the fade. Off for lists |
| 38 | * whose rows take focus on click, where the scroll would nudge the list. */ |
| 39 | scrollFadePadding?: boolean; |
| 40 | scrollbarGutter?: boolean; |
| 41 | hideScrollbars?: boolean; |
| 42 | chainVerticalScroll?: boolean; |
| 43 | /** The viewport clips to the parent's radius; "none" for a region flush to an edge. */ |
| 44 | radius?: "inherit" | "none"; |
| 45 | /** Override Base UI's focusable viewport when focusable descendants provide scroll access. */ |
| 46 | viewportTabIndex?: number; |
| 47 | }) { |
| 48 | return ( |
| 49 | <ScrollAreaPrimitive.Root |
| 50 | className={cn( |
| 51 | "relative size-full min-h-0 overflow-hidden", |
| 52 | radius === "none" ? "rounded-none" : "rounded-[inherit]", |
| 53 | className, |
| 54 | )} |
| 55 | {...props} |
| 56 | > |
| 57 | <ScrollAreaPrimitive.Viewport |
| 58 | {...(viewportTabIndex === undefined ? {} : { tabIndex: viewportTabIndex })} |
| 59 | className={cn( |
| 60 | "h-full max-h-[inherit] overflow-auto overscroll-contain rounded-[inherit] outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-x:overscroll-x-contain", |
| 61 | chainVerticalScroll && "overscroll-y-auto", |
| 62 | scrollFade && |
| 63 | "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] focus-visible:mask-none [--fade-size:1.5rem]", |
| 64 | scrollFade && scrollFadePadding && "scroll-p-[var(--fade-size)]", |
| 65 | scrollFade && !scrollFadePadding && "has-focus-visible:mask-none", |
| 66 | scrollbarGutter && "scrollbar-gutter-stable", |
| 67 | hideScrollbars && |
| 68 | "[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", |
| 69 | )} |
| 70 | data-slot="scroll-area-viewport" |
| 71 | > |
| 72 | {children} |
| 73 | </ScrollAreaPrimitive.Viewport> |
| 74 | {!hideScrollbars && ( |
| 75 | <> |
| 76 | <ScrollBar orientation="vertical" /> |
| 77 | <ScrollBar orientation="horizontal" /> |
| 78 | <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" /> |
| 79 | </> |
| 80 | )} |
| 81 | </ScrollAreaPrimitive.Root> |
| 82 | ); |
| 83 | } |
| 84 | |
| 85 | function ScrollBar({ |
| 86 | className, |
| 87 | orientation = "vertical", |
| 88 | ...props |
| 89 | }: ScrollAreaPrimitive.Scrollbar.Props) { |
| 90 | return ( |
| 91 | <ScrollAreaPrimitive.Scrollbar |
| 92 | className={cn( |
| 93 | "flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-px data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:my-1 data-[orientation=vertical]:mr-px data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100", |
| 94 | className, |
| 95 | )} |
| 96 | data-slot="scroll-area-scrollbar" |
| 97 | orientation={orientation} |
| 98 | {...props} |
| 99 | > |
| 100 | <ScrollAreaPrimitive.Thumb |
| 101 | className="relative flex-1 rounded-full bg-[var(--app-scrollbar-thumb)] transition-colors hover:bg-[var(--app-scrollbar-thumb-hover)]" |
| 102 | data-slot="scroll-area-thumb" |
| 103 | /> |
| 104 | </ScrollAreaPrimitive.Scrollbar> |
| 105 | ); |
| 106 | } |
| 107 | |
| 108 | export { getVirtualizedScrollFadeClassName, ScrollArea, ScrollBar }; |