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 (
-
- {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 */}
+
+ >
+ )}
);