"use client"; import { usePathname, useRouter } from "next/navigation"; import { useState, useEffect, useLayoutEffect, useRef } from "react"; import { ROUTES, EachRoute } from "@/lib/routes"; import { cn } from "@/lib/utils"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import * as LucideIcons from "lucide-react"; import { ChevronsUpDown, Check, type LucideIcon } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; interface ContextPopoverProps { className?: string; } // Get all root-level routes with context function getContextRoutes(): EachRoute[] { return ROUTES.filter(route => route.context); } // Get the first item's href from a route function getFirstItemHref(route: EachRoute): string { return route.items?.[0]?.href ? `${route.href}${route.items[0].href}` : route.href; } // Get the active context route from the current path function getActiveContextRoute(path: string): EachRoute | undefined { if (!path.startsWith('/docs')) return undefined; const docPath = path.replace(/^\/docs/, ''); return getContextRoutes().find(route => docPath.startsWith(route.href)); } // Get icon component by name function getIcon(name: string) { const Icon = LucideIcons[name as keyof typeof LucideIcons] as LucideIcon | undefined; if (!Icon) return ; return ; } export default function ContextPopover({ className }: ContextPopoverProps) { const pathname = usePathname(); const router = useRouter(); const [mounted, setMounted] = useState(false); const [activeRoute, setActiveRoute] = useState(); const [useDefaultTitle, setUseDefaultTitle] = useState(false); const [triggerWidth, setTriggerWidth] = useState(null); const triggerRef = useRef(null); const contextRoutes = getContextRoutes(); const fallbackRoute = ROUTES[0]; const displayRoute = useDefaultTitle ? fallbackRoute : activeRoute; useEffect(() => { // Mount-only state (used for client-only rendering) and intentionally set after first render. // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); }, []); useEffect(() => { if (pathname.startsWith("/docs")) { // eslint-disable-next-line react-hooks/set-state-in-effect setActiveRoute(getActiveContextRoute(pathname)); } else { setActiveRoute(undefined); } }, [pathname]); useEffect(() => { const hasTitle = Boolean( activeRoute?.context?.title || activeRoute?.title ); if (!hasTitle) { const timer = window.setTimeout(() => { setUseDefaultTitle(true); }, 300); return () => window.clearTimeout(timer); } // Avoid calling setState synchronously inside the effect body. // Using a micro task to reset state avoids the react-hooks/set-state-in-effect lint error. const resetTimer = window.setTimeout(() => { setUseDefaultTitle(false); }, 0); return () => window.clearTimeout(resetTimer); }, [activeRoute?.context?.title, activeRoute?.title]); // Keep the popover width in sync with the trigger width when the trigger text changes // (e.g. when navigating between docs contexts) and when the window/resizing changes. useLayoutEffect(() => { if (!triggerRef.current) return; const updateWidth = () => { if (triggerRef.current) { setTriggerWidth(triggerRef.current.offsetWidth); } }; // Make sure the width is updated when the trigger text/route changes. updateWidth(); if (typeof ResizeObserver !== "undefined") { const observer = new ResizeObserver(() => { updateWidth(); }); observer.observe(triggerRef.current); return () => { observer.disconnect(); }; } else { const handleResize = () => { updateWidth(); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; } }, [displayRoute]); if (!mounted || !pathname.startsWith("/docs") || contextRoutes.length === 0) { return null; } return ( {displayRoute?.context?.icon && ( {getIcon(displayRoute.context.icon)} )} {displayRoute?.context?.title || displayRoute?.title || (useDefaultTitle ? fallbackRoute?.context?.title || fallbackRoute?.title : )} {contextRoutes.map((route) => { const isActive = activeRoute?.href === route.href; const firstItemPath = getFirstItemHref(route); const contextPath = `/docs${firstItemPath}`; return ( router.push(contextPath)} className={cn( "relative flex w-full items-center gap-2 cursor-pointer rounded px-2 py-1.5 text-sm", "text-left outline-none transition-colors", isActive ? "bg-primary/20 text-primary dark:bg-accent/20 dark:text-accent" : "text-foreground/80 hover:bg-primary/20 dark:text-foreground/60 dark:hover:bg-accent/20" )} > {route.context?.icon && ( {getIcon(route.context.icon)} )} {route.context?.title || route.title} {route.context?.description && ( {route.context.description} )} {isActive && ( )} ); })} ); }