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 { cn } from "@/util/utils";
import { NavLink, useMatches } from "react-router-dom";
import { Badge } from "./ui/badge";
import { useIsMobile } from "@/hooks/useIsMobile.ts";
import { ChevronDownIcon } from "@radix-ui/react-icons";
type Props = {
title: string;
@@ -14,21 +16,108 @@ type Props = {
beta?: boolean;
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 { collapsed } = useSidebarStore();
const { collapsed: sidebarCollapsed } = useSidebarStore();
const matches = useMatches();
const [isExpanded, setIsExpanded] = useState(false);
const groupIsActive = matches.some((match) => {
const inputs = links.map((link) => link.to);
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 (
<div
className={cn("flex flex-col gap-[0.625rem]", {
"items-center": collapsed,
"items-center": sidebarCollapsed,
})}
>
<div
@@ -39,54 +128,88 @@ function NavLinkGroup({ title, links }: Props) {
>
<div
className={cn({
"text-center": collapsed,
"text-center": sidebarCollapsed,
})}
>
{title}
</div>
</div>
<div className="space-y-[1px]]">
{links.map((link) => {
return (
<NavLink
key={link.to}
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": collapsed,
},
);
<div className="relative space-y-[1px]">
{/* Always visible links */}
{alwaysVisibleLinks.map((link) => (
<NavLinkItem
key={link.to}
link={link}
isMobile={isMobile}
sidebarCollapsed={sidebarCollapsed}
groupIsActive={groupIsActive}
/>
))}
{/* Collapsible section */}
{shouldCollapse && !sidebarCollapsed && (
<>
{/* Peek item - fades out when collapsed */}
<div
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">
<div className="flex items-center gap-2">
{link.icon}
{!collapsed && link.label}
</div>
{!collapsed && link.disabled && (
<Badge
className="rounded-[40px] px-2 py-1"
style={{
backgroundColor: groupIsActive ? "#301615" : "#1E1016",
color: groupIsActive ? "#EA580C" : "#8D3710",
}}
>
{link.beta ? "Beta" : "Training"}
</Badge>
)}
{peekLink && (
<NavLinkItem
link={peekLink}
isMobile={isMobile}
sidebarCollapsed={sidebarCollapsed}
groupIsActive={groupIsActive}
isPartiallyHidden={true}
/>
)}
</div>
{/* Expandable content using CSS grid animation */}
<div
className="grid transition-[grid-template-rows] duration-300 ease-in-out"
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>
</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>
);