Add collapsible Agents section in sidebar (#4605)
Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user