Add collapsible Agents section in sidebar (#4605)

Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
This commit is contained in:
Suchintan
2026-02-02 19:50:59 -05:00
committed by GitHub
parent 7f2c78c990
commit 9108481f82

View File

@@ -1,8 +1,10 @@
import { useState } from "react";
import { useSidebarStore } from "@/store/SidebarStore"; import { useSidebarStore } from "@/store/SidebarStore";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { NavLink, useMatches } from "react-router-dom"; import { NavLink, useMatches } from "react-router-dom";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { useIsMobile } from "@/hooks/useIsMobile.ts"; import { useIsMobile } from "@/hooks/useIsMobile.ts";
import { ChevronDownIcon } from "@radix-ui/react-icons";
type Props = { type Props = {
title: string; title: string;
@@ -14,21 +16,108 @@ type Props = {
beta?: boolean; beta?: boolean;
icon?: React.ReactNode; icon?: React.ReactNode;
}>; }>;
collapsible?: boolean;
initialVisibleCount?: number;
}; };
function NavLinkGroup({ title, links }: Props) { type LinkItem = Props["links"][number];
function NavLinkItem({
link,
isMobile,
sidebarCollapsed,
groupIsActive,
isPartiallyHidden,
}: {
link: LinkItem;
isMobile: boolean;
sidebarCollapsed: boolean;
groupIsActive: boolean;
isPartiallyHidden?: boolean;
}) {
return (
<NavLink
to={link.to}
target={link.newTab ? "_blank" : undefined}
rel={link.newTab ? "noopener noreferrer" : undefined}
className={({ isActive }) => {
return cn(
"block rounded-lg py-2 pl-3 text-slate-400 hover:bg-muted hover:text-primary",
{ "py-1 pl-0 text-[0.8rem]": isMobile },
{
"bg-muted": isActive,
},
{
"text-primary": groupIsActive,
"px-3": sidebarCollapsed,
},
);
}}
style={
isPartiallyHidden
? {
maskImage:
"linear-gradient(to bottom, black 0%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 0%, transparent 100%)",
pointerEvents: "none",
}
: undefined
}
tabIndex={isPartiallyHidden ? -1 : undefined}
>
<div className="flex justify-between">
<div className="flex items-center gap-2">
{link.icon}
{!sidebarCollapsed && link.label}
</div>
{!sidebarCollapsed && link.disabled && (
<Badge
className="rounded-[40px] px-2 py-1"
style={{
backgroundColor: groupIsActive ? "#301615" : "#1E1016",
color: groupIsActive ? "#EA580C" : "#8D3710",
}}
>
{link.beta ? "Beta" : "Training"}
</Badge>
)}
</div>
</NavLink>
);
}
function NavLinkGroup({
title,
links,
collapsible = false,
initialVisibleCount = 3,
}: Props) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { collapsed } = useSidebarStore(); const { collapsed: sidebarCollapsed } = useSidebarStore();
const matches = useMatches(); const matches = useMatches();
const [isExpanded, setIsExpanded] = useState(false);
const groupIsActive = matches.some((match) => { const groupIsActive = matches.some((match) => {
const inputs = links.map((link) => link.to); const inputs = links.map((link) => link.to);
return inputs.includes(match.pathname); return inputs.includes(match.pathname);
}); });
const shouldCollapse =
collapsible && !sidebarCollapsed && links.length > initialVisibleCount;
const alwaysVisibleLinks = shouldCollapse
? links.slice(0, initialVisibleCount)
: links;
const collapsibleLinks = shouldCollapse
? links.slice(initialVisibleCount)
: [];
const peekLink = collapsibleLinks[0];
const hiddenCount = collapsibleLinks.length;
return ( return (
<div <div
className={cn("flex flex-col gap-[0.625rem]", { className={cn("flex flex-col gap-[0.625rem]", {
"items-center": collapsed, "items-center": sidebarCollapsed,
})} })}
> >
<div <div
@@ -39,54 +128,88 @@ function NavLinkGroup({ title, links }: Props) {
> >
<div <div
className={cn({ className={cn({
"text-center": collapsed, "text-center": sidebarCollapsed,
})} })}
> >
{title} {title}
</div> </div>
</div> </div>
<div className="space-y-[1px]]"> <div className="relative space-y-[1px]">
{links.map((link) => { {/* Always visible links */}
return ( {alwaysVisibleLinks.map((link) => (
<NavLink <NavLinkItem
key={link.to} key={link.to}
to={link.to} link={link}
target={link.newTab ? "_blank" : undefined} isMobile={isMobile}
rel={link.newTab ? "noopener noreferrer" : undefined} sidebarCollapsed={sidebarCollapsed}
className={({ isActive }) => { groupIsActive={groupIsActive}
return cn( />
"block rounded-lg py-2 pl-3 text-slate-400 hover:bg-muted hover:text-primary", ))}
{ "py-1 pl-0 text-[0.8rem]": isMobile },
{ {/* Collapsible section */}
"bg-muted": isActive, {shouldCollapse && !sidebarCollapsed && (
}, <>
{ {/* Peek item - fades out when collapsed */}
"text-primary": groupIsActive, <div
"px-3": collapsed, className="transition-all duration-300 ease-in-out"
}, style={{
); opacity: isExpanded ? 0 : 1,
height: isExpanded ? 0 : "auto",
overflow: "hidden",
pointerEvents: isExpanded ? "none" : "auto",
}} }}
> >
<div className="flex justify-between"> {peekLink && (
<div className="flex items-center gap-2"> <NavLinkItem
{link.icon} link={peekLink}
{!collapsed && link.label} isMobile={isMobile}
</div> sidebarCollapsed={sidebarCollapsed}
{!collapsed && link.disabled && ( groupIsActive={groupIsActive}
<Badge isPartiallyHidden={true}
className="rounded-[40px] px-2 py-1" />
style={{ )}
backgroundColor: groupIsActive ? "#301615" : "#1E1016", </div>
color: groupIsActive ? "#EA580C" : "#8D3710",
}} {/* Expandable content using CSS grid animation */}
> <div
{link.beta ? "Beta" : "Training"} className="grid transition-[grid-template-rows] duration-300 ease-in-out"
</Badge> style={{
)} gridTemplateRows: isExpanded ? "1fr" : "0fr",
}}
>
<div className="overflow-hidden">
{collapsibleLinks.map((link) => (
<NavLinkItem
key={link.to}
link={link}
isMobile={isMobile}
sidebarCollapsed={sidebarCollapsed}
groupIsActive={groupIsActive}
/>
))}
</div> </div>
</NavLink> </div>
);
})} {/* Expand/collapse button */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className={cn(
"flex w-full items-center gap-2 rounded-lg py-2 pl-3 text-slate-400 hover:bg-muted hover:text-primary",
{ "py-1 pl-0 text-[0.8rem]": isMobile },
)}
>
<span
className="inline-flex transition-transform duration-300 ease-in-out"
style={{
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
}}
>
<ChevronDownIcon className="size-6" />
</span>
<span>{isExpanded ? "Show less" : `${hiddenCount} more`}</span>
</button>
</>
)}
</div> </div>
</div> </div>
); );