From 50410011d142b507a8555e38262c58a3c1541fcb Mon Sep 17 00:00:00 2001 From: Light Liu <1012072118@qq.com> Date: Wed, 4 Mar 2026 14:57:58 -0800 Subject: [PATCH 1/2] feat(conversation): customizable scrollbar for conversation content --- packages/elements/src/conversation.tsx | 46 ++++++++++++++++++++------ 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/elements/src/conversation.tsx b/packages/elements/src/conversation.tsx index 8bf18fda..d18caa21 100644 --- a/packages/elements/src/conversation.tsx +++ b/packages/elements/src/conversation.tsx @@ -5,8 +5,9 @@ import type { ComponentProps } from "react"; import { Button } from "@repo/shadcn-ui/components/ui/button"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { ArrowDownIcon, DownloadIcon } from "lucide-react"; -import { useCallback } from "react"; +import { useCallback, useRef, useEffect } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; +import { ScrollArea } from "@repo/shadcn-ui/components/ui/scroll-area"; export type ConversationProps = ComponentProps; @@ -20,19 +21,44 @@ export const Conversation = ({ className, ...props }: ConversationProps) => ( /> ); -export type ConversationContentProps = ComponentProps< - typeof StickToBottom.Content ->; +export type ConversationContentProps = ComponentProps & { + scrollClassName?: string; +}; export const ConversationContent = ({ + children, className, + scrollClassName, ...props -}: ConversationContentProps) => ( - -); +}: ConversationContentProps) => { + const context = useStickToBottomContext(); + const scrollAreaRef = useRef(null); + + useEffect(() => { + if (scrollAreaRef.current && context.scrollRef) { + const viewportElement = scrollAreaRef.current.querySelector( + '[data-slot="scroll-area-viewport"]' + ); + if (viewportElement) { + const ContentElement = viewportElement as HTMLElement; + ContentElement.className = cn("size-full overflow-y-auto", scrollClassName); + ContentElement.style.scrollbarGutter = "stable both-edges"; + context.scrollRef.current = ContentElement; + } + } + }, [context.scrollRef, scrollClassName]); + + return ( + +
+ {children} +
+
+ ); +} export type ConversationEmptyStateProps = ComponentProps<"div"> & { title?: string; From 0254dc20c8569af9f0c29d4e40c3f0908e63e2ba Mon Sep 17 00:00:00 2001 From: Light Liu <1012072118@qq.com> Date: Wed, 4 Mar 2026 16:53:57 -0800 Subject: [PATCH 2/2] fix(conversation): use ScrollArea primitive from radix-ui directly --- packages/elements/package.json | 1 + packages/elements/src/conversation.tsx | 47 ++++++++++++-------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/elements/package.json b/packages/elements/package.json index 35e209dc..a6cbf4e2 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -28,6 +28,7 @@ "media-chrome": "^4.17.2", "motion": "^12.26.2", "nanoid": "^5.1.6", + "radix-ui": "latest", "react": "19.2.3", "react-dom": "19.2.3", "react-jsx-parser": "^2.2.0", diff --git a/packages/elements/src/conversation.tsx b/packages/elements/src/conversation.tsx index d18caa21..92669e83 100644 --- a/packages/elements/src/conversation.tsx +++ b/packages/elements/src/conversation.tsx @@ -5,9 +5,10 @@ import type { ComponentProps } from "react"; import { Button } from "@repo/shadcn-ui/components/ui/button"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { ArrowDownIcon, DownloadIcon } from "lucide-react"; -import { useCallback, useRef, useEffect } from "react"; +import { useCallback } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; -import { ScrollArea } from "@repo/shadcn-ui/components/ui/scroll-area"; +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"; +import { ScrollBar } from "@repo/shadcn-ui/components/ui/scroll-area"; export type ConversationProps = ComponentProps; @@ -21,44 +22,38 @@ export const Conversation = ({ className, ...props }: ConversationProps) => ( /> ); -export type ConversationContentProps = ComponentProps & { +export type ConversationContentProps = ComponentProps & { scrollClassName?: string; + scrollBarClassName?: string; }; export const ConversationContent = ({ children, className, scrollClassName, + scrollBarClassName, ...props }: ConversationContentProps) => { const context = useStickToBottomContext(); - const scrollAreaRef = useRef(null); - - useEffect(() => { - if (scrollAreaRef.current && context.scrollRef) { - const viewportElement = scrollAreaRef.current.querySelector( - '[data-slot="scroll-area-viewport"]' - ); - if (viewportElement) { - const ContentElement = viewportElement as HTMLElement; - ContentElement.className = cn("size-full overflow-y-auto", scrollClassName); - ContentElement.style.scrollbarGutter = "stable both-edges"; - context.scrollRef.current = ContentElement; - } - } - }, [context.scrollRef, scrollClassName]); return ( - -
+ - {children} -
-
+
+ {children} +
+ + + ); -} +}; export type ConversationEmptyStateProps = ComponentProps<"div"> & { title?: string;