diff --git a/skyvern-frontend/src/components/NavLinkGroup.tsx b/skyvern-frontend/src/components/NavLinkGroup.tsx index 8d9902d5..91b0dbb8 100644 --- a/skyvern-frontend/src/components/NavLinkGroup.tsx +++ b/skyvern-frontend/src/components/NavLinkGroup.tsx @@ -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 ( + { + 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} + > +
+
+ {link.icon} + {!sidebarCollapsed && link.label} +
+ {!sidebarCollapsed && link.disabled && ( + + {link.beta ? "Beta" : "Training"} + + )} +
+
+ ); +} + +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 (
{title}
-
- {links.map((link) => { - return ( - { - 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, - }, - ); +
+ {/* Always visible links */} + {alwaysVisibleLinks.map((link) => ( + + ))} + + {/* Collapsible section */} + {shouldCollapse && !sidebarCollapsed && ( + <> + {/* Peek item - fades out when collapsed */} +
-
-
- {link.icon} - {!collapsed && link.label} -
- {!collapsed && link.disabled && ( - - {link.beta ? "Beta" : "Training"} - - )} + {peekLink && ( + + )} +
+ + {/* Expandable content using CSS grid animation */} +
+
+ {collapsibleLinks.map((link) => ( + + ))}
- - ); - })} +
+ + {/* Expand/collapse button */} + + + )}
);