diff --git a/surfsense_web/app/dashboard/[workspace_id]/playground/layout-shell.tsx b/surfsense_web/app/dashboard/[workspace_id]/playground/layout-shell.tsx index c680cf6d4..08ca498af 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/playground/layout-shell.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/playground/layout-shell.tsx @@ -1,12 +1,20 @@ "use client"; -import { History, LayoutGrid } from "lucide-react"; +import { ChevronRight, History, LayoutGrid } from "lucide-react"; import Link from "next/link"; import { useSelectedLayoutSegments } from "next/navigation"; import type React from "react"; -import { useCallback, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerContent, + DrawerHandle, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; import { Separator } from "@/components/ui/separator"; -import { PLAYGROUND_PLATFORMS, type PlatformIcon } from "@/lib/playground/catalog"; +import { PLAYGROUND_PLATFORMS, type PlaygroundPlatform, type PlaygroundVerb } from "@/lib/playground/catalog"; import { cn } from "@/lib/utils"; interface PlaygroundLayoutShellProps { @@ -14,28 +22,21 @@ interface PlaygroundLayoutShellProps { children: React.ReactNode; } -type PlaygroundNavItem = - | { - type: "item"; - value: string; - label: string; - href: string; - icon: React.ReactNode; - indented?: boolean; - } - | { - type: "section"; - value: string; - label: string; - icon: PlatformIcon; - }; +interface TopLevelNavItem { + value: "overview" | "runs"; + label: string; + href: string; + icon: React.ReactNode; +} -function PlaygroundNavLink({ +function TopLevelNavLink({ item, activeValue, + onNavigate, }: { - item: Extract; + item: TopLevelNavItem; activeValue: string; + onNavigate?: () => void; }) { const isActive = activeValue === item.value; @@ -45,9 +46,9 @@ function PlaygroundNavLink({ replace scroll={false} prefetch + onClick={onNavigate} className={cn( "inline-flex h-auto items-center justify-start gap-3 rounded-md px-3 py-2.5 text-left text-sm font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none", - item.indented && "pl-9", isActive ? "bg-accent text-accent-foreground" : "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground" @@ -59,50 +60,109 @@ function PlaygroundNavLink({ ); } +function ProviderNavGroup({ + platform, + base, + activeValue, + isExpanded, + onToggle, + onNavigate, +}: { + platform: PlaygroundPlatform; + base: string; + activeValue: string; + isExpanded: boolean; + onToggle: () => void; + onNavigate?: () => void; +}) { + const Icon = platform.icon; + + return ( +
+ +
+
+
+ {platform.verbs.map((verb) => { + const value = `${platform.id}/${verb.verb}`; + const isActive = activeValue === value; + return ( + + {verb.label} + + ); + })} +
+
+
+
+ ); +} + export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayoutShellProps) { const segments = useSelectedLayoutSegments(); - const [tabScrollPos, setTabScrollPos] = useState<"start" | "middle" | "end">("start"); const base = `/dashboard/${workspaceId}/playground`; + const activePlatform = PLAYGROUND_PLATFORMS.some((platform) => platform.id === segments[0]) + ? segments[0] + : null; + const [expandedProvider, setExpandedProvider] = useState(activePlatform); + const [mobileNavOpen, setMobileNavOpen] = useState(false); - const handleTabScroll = useCallback((e: React.UIEvent) => { - const el = e.currentTarget; - const atStart = el.scrollLeft <= 2; - const atEnd = el.scrollWidth - el.scrollLeft - el.clientWidth <= 2; - setTabScrollPos(atStart ? "start" : atEnd ? "end" : "middle"); - }, []); + useEffect(() => { + if (activePlatform) { + setExpandedProvider(activePlatform); + } + }, [activePlatform]); - const navItems = useMemo( + const topLevelItems = useMemo( () => [ { - type: "item", value: "overview", label: "Overview", href: base, icon: , }, { - type: "item", value: "runs", label: "Runs", href: `${base}/runs`, icon: , }, - ...PLAYGROUND_PLATFORMS.flatMap((platform) => [ - { - type: "section", - value: platform.id, - label: platform.label, - icon: platform.icon, - }, - ...platform.verbs.map((verb) => ({ - type: "item" as const, - value: `${platform.id}/${verb.verb}`, - label: verb.label, - href: `${base}/${platform.id}/${verb.verb}`, - icon: , - indented: true, - })), - ]), ], [base] ); @@ -110,14 +170,39 @@ export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayou const activeValue = segments.length >= 2 ? `${segments[0]}/${segments[1]}` - : segments[0] && navItems.some((item) => item.type === "item" && item.value === segments[0]) + : segments[0] && topLevelItems.some((item) => item.value === segments[0]) ? segments[0] : "overview"; - const selectedLabel = - navItems.find((item) => item.type === "item" && item.value === activeValue)?.label ?? - "API Playground"; - const mobileItems = navItems.filter( - (item): item is Extract => item.type === "item" + + const selectedLabel = getSelectedLabel(activeValue, topLevelItems); + + const renderNav = (onNavigate?: () => void) => ( + <> + {topLevelItems.map((item) => ( + + ))} + +
+ {PLAYGROUND_PLATFORMS.map((platform) => ( + + setExpandedProvider((current) => (current === platform.id ? null : platform.id)) + } + onNavigate={onNavigate} + /> + ))} +
+ ); return ( @@ -127,52 +212,27 @@ export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayou API Playground -
-
- {mobileItems.map((item) => ( - - {item.icon} - {item.label} - - ))} -
-
+ + + + + + + API Playground navigation + + +
@@ -185,3 +245,15 @@ export function PlaygroundLayoutShell({ workspaceId, children }: PlaygroundLayou ); } + +function getSelectedLabel(activeValue: string, topLevelItems: TopLevelNavItem[]): string { + const topLevelItem = topLevelItems.find((item) => item.value === activeValue); + if (topLevelItem) return topLevelItem.label; + + const [platformId, verbId] = activeValue.split("/"); + const platform = PLAYGROUND_PLATFORMS.find((item) => item.id === platformId); + const verb: PlaygroundVerb | undefined = platform?.verbs.find((item) => item.verb === verbId); + + if (platform && verb) return `${platform.label} / ${verb.label}`; + return "API Playground"; +}