feat(sidebar): introduce SidebarListItem component for consistent sidebar item styling

- Added SidebarListItem component to standardize the appearance of sidebar items across DocumentNode and FolderNode.
- Refactored DocumentNode and FolderNode to utilize SidebarListItem for improved layout and interaction.
- Updated ChatListItem to leverage sidebarListItemClassName for consistent styling.
- Enhanced SidebarSection to improve content rendering and visibility transitions.
This commit is contained in:
Anish Sarkar 2026-07-06 11:44:45 +05:30
parent 312ae6f753
commit 53259b6105
6 changed files with 104 additions and 40 deletions

View file

@ -40,6 +40,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { cn } from "@/lib/utils";
import { SidebarListItem } from "../layout/ui/sidebar/SidebarListItem";
import { DND_TYPES } from "./FolderNode";
import { isVersionableType } from "./version-history";
@ -165,13 +166,11 @@ export const DocumentNode = React.memo(function DocumentNode({
return (
<ContextMenu onOpenChange={onContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<div
<SidebarListItem
ref={attachRef}
className={cn(
"group flex h-8 w-full items-center gap-2.5 rounded-md px-1 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer select-none text-left",
isMentioned && "bg-accent text-accent-foreground",
isDragging && "opacity-40"
)}
active={isMentioned}
dragging={isDragging}
className="gap-2.5 px-1"
style={{ paddingLeft: `${depth * 16 + 4}px` }}
>
{(() => {
@ -358,7 +357,7 @@ export const DocumentNode = React.memo(function DocumentNode({
</DropdownMenuContent>
</DropdownMenu>
</span>
</div>
</SidebarListItem>
</ContextMenuTrigger>
{contextMenuOpen && (

View file

@ -5,7 +5,6 @@ import {
ChevronDown,
ChevronRight,
Download,
Eye,
EyeOff,
Folder,
FolderOpen,
@ -18,6 +17,7 @@ import {
} from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import { SidebarListItem } from "@/components/layout/ui/sidebar/SidebarListItem";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
@ -254,15 +254,14 @@ export const FolderNode = React.memo(function FolderNode({
return (
<ContextMenu onOpenChange={onContextMenuOpenChange}>
<ContextMenuTrigger asChild disabled={isRenaming}>
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
<div
<SidebarListItem
ref={attachRef}
role="button"
tabIndex={0}
dragging={isDragging}
className={cn(
"group relative flex h-8 items-center gap-1 rounded-md px-1 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer select-none",
"relative gap-1 px-1",
isExpanded && "font-medium",
isDragging && "opacity-40",
isOver && canDrop && dropZone === "middle" && "bg-accent ring-1 ring-primary/40",
isOver && canDrop && dropZone === "top" && "border-t-2 border-primary",
isOver && canDrop && dropZone === "bottom" && "border-b-2 border-primary",
@ -434,7 +433,7 @@ export const FolderNode = React.memo(function FolderNode({
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarListItem>
</ContextMenuTrigger>
{!isRenaming && contextMenuOpen && (

View file

@ -14,6 +14,7 @@ import { useLongPress } from "@/hooks/use-long-press";
import { useIsMobile } from "@/hooks/use-mobile";
import { useTypewriter } from "@/hooks/use-typewriter";
import { cn } from "@/lib/utils";
import { sidebarListItemClassName } from "./SidebarListItem";
interface ChatListItemProps {
name: string;
@ -66,12 +67,11 @@ export function ChatListItem({
onMouseEnter={onPrefetch}
onFocus={onPrefetch}
{...(isMobile ? longPressHandlers : {})}
className={cn(
"h-auto w-full justify-start gap-2 overflow-hidden px-2 py-1.5 text-left font-normal",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground"
)}
className={sidebarListItemClassName({
active: isActive,
className:
"justify-start gap-2 overflow-hidden px-2 py-1.5 font-normal focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
})}
>
<span className="min-w-0 flex-1 truncate">{animatedName}</span>
</Button>

View file

@ -1556,22 +1556,31 @@ function AuthenticatedDocumentsSidebarBase({
if (embedded) {
return (
<SidebarSection
title={t("title") || "Documents"}
defaultOpen={true}
fillHeight
contentClassName="px-0"
persistentAction={
<EmbeddedDocumentsMenu
typeCounts={typeCounts}
activeTypes={activeTypes}
onToggleType={onToggleType}
onCreateFolder={() => handleCreateFolder(null)}
/>
}
>
{renderDocumentTree()}
</SidebarSection>
<>
<SidebarSection
title={t("title") || "Documents"}
defaultOpen={true}
fillHeight
contentClassName="px-0"
persistentAction={
<EmbeddedDocumentsMenu
typeCounts={typeCounts}
activeTypes={activeTypes}
onToggleType={onToggleType}
onCreateFolder={() => handleCreateFolder(null)}
/>
}
>
{renderDocumentTree()}
</SidebarSection>
<CreateFolderDialog
open={createFolderOpen}
onOpenChange={setCreateFolderOpen}
parentFolderName={createFolderParentName}
onConfirm={handleCreateFolderConfirm}
/>
</>
);
}

View file

@ -0,0 +1,47 @@
"use client";
import type React from "react";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
type SidebarListItemElementProps = React.HTMLAttributes<HTMLDivElement>;
interface SidebarListItemProps extends SidebarListItemElementProps {
active?: boolean;
dragging?: boolean;
interactive?: boolean;
}
export const SidebarListItem = forwardRef<HTMLDivElement, SidebarListItemProps>(
({ active, dragging, interactive = true, className, children, ...props }, ref) => (
<div
ref={ref}
className={sidebarListItemClassName({ active, dragging, interactive, className })}
{...props}
>
{children}
</div>
)
);
SidebarListItem.displayName = "SidebarListItem";
export function sidebarListItemClassName({
active,
dragging,
interactive = true,
className,
}: {
active?: boolean;
dragging?: boolean;
interactive?: boolean;
className?: string;
}) {
return cn(
"group group/sidebar-list-item flex h-8 w-full items-center rounded-md text-left text-sm select-none",
interactive && "cursor-pointer hover:bg-accent hover:text-accent-foreground",
active && "bg-accent text-accent-foreground",
dragging && "opacity-40",
className
);
}

View file

@ -2,7 +2,7 @@
import { ChevronRight } from "lucide-react";
import { useState } from "react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
interface SidebarSectionProps {
@ -78,11 +78,21 @@ export function SidebarSection({
)}
</div>
<CollapsibleContent className={cn("overflow-hidden flex-1 flex flex-col min-h-0")}>
<div className={cn("px-2 flex-1 flex flex-col min-h-0 overflow-hidden", contentClassName)}>
{children}
<div
className={cn(
"grid flex-1 overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out",
isOpen ? "grid-rows-[1fr] opacity-100" : "pointer-events-none grid-rows-[0fr] opacity-0"
)}
aria-hidden={!isOpen}
>
<div className="min-h-0 overflow-hidden">
<div
className={cn("px-2 flex h-full min-h-0 flex-col overflow-hidden", contentClassName)}
>
{children}
</div>
</div>
</CollapsibleContent>
</div>
</Collapsible>
);
}