feat: introduce SidebarButton component for improved sidebar interactions

This commit is contained in:
Anish Sarkar 2026-03-21 12:06:48 +05:30
parent 9a20db7fc4
commit affc89dd5c
3 changed files with 126 additions and 74 deletions

View file

@ -2,9 +2,9 @@
import { CheckCircle2, CircleAlert } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { NavItem } from "../../types/layout.types";
import { SidebarButton } from "./SidebarButton";
interface NavSectionProps {
items: NavItem[];
@ -66,71 +66,48 @@ function StatusIcon({
return <FallbackIcon className={cn("shrink-0", className)} />;
}
function CollapsedOverlay({ item }: { item: NavItem }) {
const indicator = item.statusIndicator;
if (indicator && indicator !== "idle") {
return <StatusBadge status={indicator} />;
}
if (item.badge) {
return (
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
{item.badge}
</span>
);
}
return null;
}
export function NavSection({ items, onItemClick, isCollapsed = false }: NavSectionProps) {
return (
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
{items.map((item) => {
const Icon = item.icon;
const indicator = item.statusIndicator;
const joyrideAttr =
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
? { "data-joyride": "inbox-sidebar" }
? { "data-joyride": "inbox-sidebar" as const }
: {};
if (isCollapsed) {
return (
<Tooltip key={item.url}>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onItemClick?.(item)}
className={cn(
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
)}
{...joyrideAttr}
>
<Icon className="h-4 w-4" />
{indicator && indicator !== "idle" ? (
<StatusBadge status={indicator} />
) : item.badge ? (
<span className="absolute top-0.5 right-0.5 inline-flex items-center justify-center min-w-[14px] h-[14px] px-0.5 rounded-full bg-red-500 text-white text-[9px] font-medium">
{item.badge}
</span>
) : null}
<span className="sr-only">{item.title}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
{item.title}
{item.badge && ` (${item.badge})`}
</TooltipContent>
</Tooltip>
);
}
return (
<button
<SidebarButton
key={item.url}
type="button"
icon={item.icon}
label={item.title}
onClick={() => onItemClick?.(item)}
className={cn(
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
)}
{...joyrideAttr}
>
<StatusIcon status={indicator} FallbackIcon={Icon} className="h-4 w-4" />
<span className="flex-1 truncate">{item.title}</span>
{item.badge && (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{item.badge}
</span>
)}
</button>
isCollapsed={isCollapsed}
badge={item.badge}
collapsedOverlay={<CollapsedOverlay item={item} />}
expandedIconNode={
<StatusIcon
status={item.statusIndicator}
FallbackIcon={item.icon}
className="h-4 w-4"
/>
}
buttonProps={joyrideAttr}
/>
);
})}
</div>

View file

@ -3,15 +3,14 @@
import { PenSquare } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { ChatListItem } from "./ChatListItem";
import { NavSection } from "./NavSection";
import { PageUsageDisplay } from "./PageUsageDisplay";
import { SidebarButton } from "./SidebarButton";
import { SidebarCollapseButton } from "./SidebarCollapseButton";
import { SidebarHeader } from "./SidebarHeader";
import { SidebarSection } from "./SidebarSection";
@ -132,23 +131,13 @@ export function Sidebar({
)}
{/* New chat button */}
<div className="p-2">
{isCollapsed ? (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" className="w-full h-10" onClick={onNewChat}>
<PenSquare className="h-4 w-4" />
<span className="sr-only">{t("new_chat")}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{t("new_chat")}</TooltipContent>
</Tooltip>
) : (
<Button variant="outline" className="w-full justify-start gap-2" onClick={onNewChat}>
<PenSquare className="h-4 w-4" />
{t("new_chat")}
</Button>
)}
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
<SidebarButton
icon={PenSquare}
label={t("new_chat")}
onClick={onNewChat}
isCollapsed={isCollapsed}
/>
</div>
{/* Chat sections - fills available space */}

View file

@ -0,0 +1,86 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type React from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
interface SidebarButtonProps {
icon: LucideIcon;
label: string;
onClick?: () => void;
isCollapsed?: boolean;
badge?: React.ReactNode;
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
collapsedOverlay?: React.ReactNode;
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
expandedIconNode?: React.ReactNode;
className?: string;
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
}
const expandedClassName = cn(
"flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
);
const collapsedClassName = cn(
"relative flex h-10 w-10 items-center justify-center rounded-md transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
);
export function SidebarButton({
icon: Icon,
label,
onClick,
isCollapsed = false,
badge,
collapsedOverlay,
expandedIconNode,
className,
buttonProps,
}: SidebarButtonProps) {
if (isCollapsed) {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onClick}
className={cn(collapsedClassName, className)}
{...buttonProps}
>
<Icon className="h-4 w-4" />
{collapsedOverlay}
<span className="sr-only">{label}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
{label}
{typeof badge === "string" && ` (${badge})`}
</TooltipContent>
</Tooltip>
);
}
return (
<button
type="button"
onClick={onClick}
className={cn(expandedClassName, className)}
{...buttonProps}
>
{expandedIconNode ?? <Icon className="h-4 w-4 shrink-0" />}
<span className="flex-1 truncate">{label}</span>
{badge && typeof badge !== "string" ? badge : null}
{badge && typeof badge === "string" ? (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{badge}
</span>
) : null}
</button>
);
}