feat: Enhance report panel state management and dropdown functionality in sidebar

This commit is contained in:
Anish Sarkar 2026-03-11 12:25:04 +05:30
parent 4a576f7347
commit f73c1d83a8
4 changed files with 62 additions and 37 deletions

View file

@ -1,5 +1,4 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom"; import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
interface ReportPanelState { interface ReportPanelState {
@ -25,11 +24,14 @@ export const reportPanelAtom = atom<ReportPanelState>(initialState);
/** Derived read-only atom for checking if panel is open */ /** Derived read-only atom for checking if panel is open */
export const reportPanelOpenAtom = atom((get) => get(reportPanelAtom).isOpen); export const reportPanelOpenAtom = atom((get) => get(reportPanelAtom).isOpen);
/** Snapshot of `rightPanelCollapsedAtom` taken before the report opens */
const preReportCollapsedAtom = atom<boolean | null>(null);
/** Action atom to open the report panel with a specific report */ /** Action atom to open the report panel with a specific report */
export const openReportPanelAtom = atom( export const openReportPanelAtom = atom(
null, null,
( (
_get, get,
set, set,
{ {
reportId, reportId,
@ -38,6 +40,9 @@ export const openReportPanelAtom = atom(
shareToken, shareToken,
}: { reportId: number; title: string; wordCount?: number; shareToken?: string | null } }: { reportId: number; title: string; wordCount?: number; shareToken?: string | null }
) => { ) => {
if (!get(reportPanelAtom).isOpen) {
set(preReportCollapsedAtom, get(rightPanelCollapsedAtom));
}
set(reportPanelAtom, { set(reportPanelAtom, {
isOpen: true, isOpen: true,
reportId, reportId,
@ -47,12 +52,16 @@ export const openReportPanelAtom = atom(
}); });
set(rightPanelTabAtom, "report"); set(rightPanelTabAtom, "report");
set(rightPanelCollapsedAtom, false); set(rightPanelCollapsedAtom, false);
set(documentsSidebarOpenAtom, true);
} }
); );
/** Action atom to close the report panel */ /** Action atom to close the report panel */
export const closeReportPanelAtom = atom(null, (_get, set) => { export const closeReportPanelAtom = atom(null, (get, set) => {
set(reportPanelAtom, initialState); set(reportPanelAtom, initialState);
set(rightPanelTabAtom, "sources"); set(rightPanelTabAtom, "sources");
const prev = get(preReportCollapsedAtom);
if (prev !== null) {
set(rightPanelCollapsedAtom, prev);
set(preReportCollapsedAtom, null);
}
}); });

View file

@ -627,7 +627,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
side="bottom" side="bottom"
align="start" align="start"
sideOffset={12} sideOffset={12}
className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0" className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0 select-none"
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b"> <div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">

View file

@ -8,7 +8,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useLongPress } from "@/hooks/use-long-press"; import { useLongPress } from "@/hooks/use-long-press";
@ -20,6 +19,8 @@ interface ChatListItemProps {
name: string; name: string;
isActive?: boolean; isActive?: boolean;
archived?: boolean; archived?: boolean;
dropdownOpen?: boolean;
onDropdownOpenChange?: (open: boolean) => void;
onClick?: () => void; onClick?: () => void;
onRename?: () => void; onRename?: () => void;
onArchive?: () => void; onArchive?: () => void;
@ -30,6 +31,8 @@ export function ChatListItem({
name, name,
isActive, isActive,
archived, archived,
dropdownOpen: controlledOpen,
onDropdownOpenChange,
onClick, onClick,
onRename, onRename,
onArchive, onArchive,
@ -37,11 +40,13 @@ export function ChatListItem({
}: ChatListItemProps) { }: ChatListItemProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [dropdownOpen, setDropdownOpen] = useState(false); const [internalOpen, setInternalOpen] = useState(false);
const dropdownOpen = controlledOpen ?? internalOpen;
const setDropdownOpen = onDropdownOpenChange ?? setInternalOpen;
const animatedName = useTypewriter(name); const animatedName = useTypewriter(name);
const { handlers: longPressHandlers, wasLongPress } = useLongPress( const { handlers: longPressHandlers, wasLongPress } = useLongPress(
useCallback(() => setDropdownOpen(true), []) useCallback(() => setDropdownOpen(true), [setDropdownOpen])
); );
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
@ -68,12 +73,12 @@ export function ChatListItem({
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */} {/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
<div <div
className={cn( className={cn(
"absolute right-0 top-0 bottom-0 flex items-center pr-1 pl-6 rounded-r-md", "pointer-events-none absolute right-0 top-0 bottom-0 flex items-center pr-1 pl-6 rounded-r-md",
isActive isActive
? "bg-gradient-to-l from-accent from-60% to-transparent" ? "bg-gradient-to-l from-accent from-60% to-transparent"
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent", : "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
isMobile isMobile
? "opacity-0 pointer-events-none" ? "opacity-0"
: isActive : isActive
? "opacity-100" ? "opacity-100"
: "opacity-0 group-hover/item:opacity-100" : "opacity-0 group-hover/item:opacity-100"
@ -81,7 +86,7 @@ export function ChatListItem({
> >
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="pointer-events-auto h-6 w-6">
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" /> <MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
<span className="sr-only">{t("more_options")}</span> <span className="sr-only">{t("more_options")}</span>
</Button> </Button>
@ -118,8 +123,7 @@ export function ChatListItem({
)} )}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{onArchive && onDelete && <DropdownMenuSeparator />} {onDelete && (
{onDelete && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View file

@ -2,6 +2,7 @@
import { FolderOpen, PenSquare } from "lucide-react"; import { FolderOpen, PenSquare } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@ -89,6 +90,7 @@ export function Sidebar({
isResizing = false, isResizing = false,
}: SidebarProps) { }: SidebarProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
return ( return (
<div <div
@ -103,6 +105,12 @@ export function Sidebar({
{/* Resize handle on right border */} {/* Resize handle on right border */}
{!isCollapsed && onResizeMouseDown && ( {!isCollapsed && onResizeMouseDown && (
<div <div
role="slider"
aria-label="Resize sidebar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
tabIndex={0}
onMouseDown={onResizeMouseDown} onMouseDown={onResizeMouseDown}
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10" className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-border active:bg-border z-10"
/> />
@ -209,18 +217,20 @@ export function Sidebar({
<div <div
className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-8" : ""}`} className={`flex flex-col gap-0.5 max-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) => ( {sharedChats.slice(0, 20).map((chat) => (
<ChatListItem <ChatListItem
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
isActive={chat.id === activeChatId} isActive={chat.id === activeChatId}
archived={chat.archived} archived={chat.archived}
onClick={() => onChatSelect(chat)} dropdownOpen={openDropdownChatId === chat.id}
onRename={() => onChatRename?.(chat)} onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
onArchive={() => onChatArchive?.(chat)} onClick={() => onChatSelect(chat)}
onDelete={() => onChatDelete?.(chat)} onRename={() => onChatRename?.(chat)}
/> onArchive={() => onChatArchive?.(chat)}
))} onDelete={() => onChatDelete?.(chat)}
/>
))}
</div> </div>
{/* Gradient fade indicator when more than 4 items */} {/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && ( {sharedChats.length > 4 && (
@ -281,18 +291,20 @@ export function Sidebar({
<div <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" : ""}`} 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) => ( {chats.slice(0, 20).map((chat) => (
<ChatListItem <ChatListItem
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
isActive={chat.id === activeChatId} isActive={chat.id === activeChatId}
archived={chat.archived} archived={chat.archived}
onClick={() => onChatSelect(chat)} dropdownOpen={openDropdownChatId === chat.id}
onRename={() => onChatRename?.(chat)} onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
onArchive={() => onChatArchive?.(chat)} onClick={() => onChatSelect(chat)}
onDelete={() => onChatDelete?.(chat)} onRename={() => onChatRename?.(chat)}
/> onArchive={() => onChatArchive?.(chat)}
))} onDelete={() => onChatDelete?.(chat)}
/>
))}
</div> </div>
{/* Gradient fade indicator when more than 4 items */} {/* Gradient fade indicator when more than 4 items */}
{chats.length > 4 && ( {chats.length > 4 && (