feat(sidebar): enhance chat sections layout and functionality with improved height management

This commit is contained in:
Anish Sarkar 2026-01-27 16:32:05 +05:30
parent ba304be977
commit b148731fda
3 changed files with 139 additions and 108 deletions

View file

@ -87,10 +87,10 @@ export function LayoutDataProvider({
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });
// Fetch threads // Fetch threads (40 total to allow up to 20 per section - shared/private)
const { data: threadsData } = useQuery({ const { data: threadsData } = useQuery({
queryKey: ["threads", searchSpaceId, { limit: 4 }], queryKey: ["threads", searchSpaceId, { limit: 40 }],
queryFn: () => fetchThreads(Number(searchSpaceId), 4), queryFn: () => fetchThreads(Number(searchSpaceId), 40),
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });

View file

@ -3,7 +3,6 @@
import { FolderOpen, MessageSquare, PenSquare } from "lucide-react"; import { FolderOpen, MessageSquare, PenSquare } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
@ -121,101 +120,109 @@ export function Sidebar({
)} )}
</div> </div>
{/* Scrollable content */} {/* Chat sections - fills available space */}
<ScrollArea className="flex-1"> {isCollapsed ? (
{isCollapsed ? ( <div className="flex-1 flex flex-col items-center gap-2 py-2 w-[60px]">
<div className="flex flex-col items-center gap-2 py-2 w-[60px]"> {(chats.length > 0 || sharedChats.length > 0) && (
{(chats.length > 0 || sharedChats.length > 0) && ( <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" size="icon"
size="icon" className="h-10 w-10"
className="h-10 w-10" onClick={() => onToggleCollapse?.()}
onClick={() => onToggleCollapse?.()} >
> <MessageSquare className="h-4 w-4" />
<MessageSquare className="h-4 w-4" /> <span className="sr-only">{t("chats")}</span>
<span className="sr-only">{t("chats")}</span> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent side="right">
<TooltipContent side="right"> {t("chats")} ({chats.length + sharedChats.length})
{t("chats")} ({chats.length + sharedChats.length}) </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> )}
</div>
) : (
<div className="flex-1 flex flex-col gap-1 py-2 w-[240px] min-h-0 overflow-hidden">
{/* Shared Chats Section - takes half the space */}
<SidebarSection
title={t("shared_chats")}
defaultOpen={true}
fillHeight={true}
action={
onViewAllSharedChats ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
onClick={onViewAllSharedChats}
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
{t("view_all_shared_chats") || "View all shared chats"}
</TooltipContent>
</Tooltip>
) : undefined
}
>
{sharedChats.length > 0 ? (
<div className="relative flex-1 min-h-0">
<div className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? 'pb-8' : ''}`}>
{sharedChats.slice(0, 20).map((chat) => (
<ChatListItem
key={chat.id}
name={chat.name}
isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
</div>
{/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && (
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-sidebar via-sidebar/90 to-transparent" />
)}
</div>
) : (
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_shared_chats")}</p>
)} )}
</div> </SidebarSection>
) : (
<div className="flex flex-col gap-1 py-2 w-[240px]">
{/* Shared Chats Section */}
<SidebarSection
title={t("shared_chats")}
defaultOpen={true}
action={
onViewAllSharedChats ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
onClick={onViewAllSharedChats}
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
{t("view_all_shared_chats") || "View all shared chats"}
</TooltipContent>
</Tooltip>
) : undefined
}
>
{sharedChats.length > 0 ? (
<div className="flex flex-col gap-0.5">
{sharedChats.map((chat) => (
<ChatListItem
key={chat.id}
name={chat.name}
isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
</div>
) : (
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_shared_chats")}</p>
)}
</SidebarSection>
{/* Private Chats Section */} {/* Private Chats Section - takes half the space */}
<SidebarSection <SidebarSection
title={t("chats")} title={t("chats")}
defaultOpen={true} defaultOpen={true}
action={ fillHeight={true}
onViewAllPrivateChats ? ( action={
<Tooltip> onViewAllPrivateChats ? (
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
variant="ghost" <Button
size="icon" variant="ghost"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0" size="icon"
onClick={onViewAllPrivateChats} className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
> onClick={onViewAllPrivateChats}
<FolderOpen className="h-4 w-4" /> >
</Button> <FolderOpen className="h-4 w-4" />
</TooltipTrigger> </Button>
<TooltipContent side="top"> </TooltipTrigger>
{t("view_all_private_chats") || "View all private chats"} <TooltipContent side="top">
</TooltipContent> {t("view_all_private_chats") || "View all private chats"}
</Tooltip> </TooltipContent>
) : undefined </Tooltip>
} ) : undefined
> }
{chats.length > 0 ? ( >
<div className="flex flex-col gap-0.5"> {chats.length > 0 ? (
{chats.map((chat) => ( <div className="relative flex-1 min-h-0">
<div className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${chats.length > 4 ? 'pb-8' : ''}`}>
{chats.slice(0, 20).map((chat) => (
<ChatListItem <ChatListItem
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
@ -227,13 +234,17 @@ export function Sidebar({
/> />
))} ))}
</div> </div>
) : ( {/* Gradient fade indicator when more than 4 items */}
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p> {chats.length > 4 && (
)} <div className="pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-sidebar via-sidebar/90 to-transparent" />
</SidebarSection> )}
</div> </div>
)} ) : (
</ScrollArea> <p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p>
)}
</SidebarSection>
</div>
)}
{/* Footer */} {/* Footer */}
<div className="mt-auto border-t"> <div className="mt-auto border-t">

View file

@ -11,6 +11,8 @@ interface SidebarSectionProps {
children: React.ReactNode; children: React.ReactNode;
action?: React.ReactNode; action?: React.ReactNode;
persistentAction?: React.ReactNode; persistentAction?: React.ReactNode;
className?: string;
fillHeight?: boolean;
} }
export function SidebarSection({ export function SidebarSection({
@ -19,12 +21,22 @@ export function SidebarSection({
children, children,
action, action,
persistentAction, persistentAction,
className,
fillHeight = false,
}: SidebarSectionProps) { }: SidebarSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen); const [isOpen, setIsOpen] = useState(defaultOpen);
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="overflow-hidden"> <Collapsible
<div className="flex items-center group/section"> open={isOpen}
onOpenChange={setIsOpen}
className={cn(
"overflow-hidden",
fillHeight && "flex flex-col flex-1 min-h-0",
className
)}
>
<div className="flex items-center group/section shrink-0">
<CollapsibleTrigger className="flex flex-1 items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0"> <CollapsibleTrigger className="flex flex-1 items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0">
<ChevronRight <ChevronRight
className={cn( className={cn(
@ -48,8 +60,16 @@ export function SidebarSection({
)} )}
</div> </div>
<CollapsibleContent className="overflow-hidden"> <CollapsibleContent className={cn(
<div className="px-2 pb-2">{children}</div> "overflow-hidden",
fillHeight && "flex-1 flex flex-col min-h-0"
)}>
<div className={cn(
"px-2 pb-2",
fillHeight && "flex-1 flex flex-col min-h-0 overflow-hidden"
)}>
{children}
</div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );