import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import type { CSSProperties, ReactNode } from "react"; import { Link, useLocation, useParams } from "react-router"; import ViewTransitionLink from "@/components/ViewTransitionLink"; import { getProfileMenuSide, groupAppNavLinks, isSidebarExpanded, isSidebarRailCollapsed, libraryRowShift, sidebarSurfaceStyle, type AppNavLink, } from "@/components/AppSidebar.logic"; import { SiloBrand } from "@/components/SiloBrand"; import { useAuth } from "@/hooks/useAuth"; import { useCurrentProfile } from "@/hooks/useCurrentProfile"; import { useIsActingAdmin } from "@/hooks/useIsActingAdmin"; import { navigateToPluginRoute } from "@/lib/buildPluginHref"; import { useUserLibraries } from "@/hooks/queries/libraries"; import { useUnreadNotificationCount } from "@/hooks/queries/notifications"; import { useNotificationCapability } from "@/hooks/queries/notificationWebhooks"; import { usePluginSettingsList } from "@/hooks/queries/pluginSettings"; import { useRequestFeatureStatus } from "@/hooks/queries/useRequests"; import { useSidebarPins, useToggleSidebarPin } from "@/hooks/queries/sidebarPins"; import { useViewTransitionNavigate } from "@/hooks/useViewTransition"; import { pluginRouteHref } from "@/lib/pluginRouteHref"; import { buildLibraryCollectionCatalogHref, buildPersonalCatalogHref, buildQueryCatalogHref, buildSectionCatalogHref, parseCatalogSearchParams, } from "@/pages/catalogSearchParams"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Film, Tv, Library, Search, Heart, List, Clock, FolderOpen, LogOut, UserCircle, Shield, Settings, Sparkles, CalendarDays, Home, UsersRound, ChevronDown, ChevronRight, PinOff, LayoutGrid, Puzzle, BookHeadphones, Send, Bell, } from "lucide-react"; import { useTheme } from "@/hooks/useTheme"; import { CURATED_THEME_IDS, THEMES } from "@/lib/themes"; import { cn } from "@/lib/utils"; function getLibraryIcon(type: string) { switch (type) { case "movies": return ; case "series": return ; case "audiobook": case "audiobooks": return ; default: return ; } } function getLibraryIdFromPathname(pathname: string): number | null { const match = pathname.match(/^\/library\/(\d+)(?:\/|$)/); if (!match) return null; const id = Number(match[1]); return Number.isInteger(id) && id > 0 ? id : null; } /** * Nav row label. The surface never changes width, so labels keep their full * layout box in both states and only fade — the moving frame hides them. Animating * `max-width` here used to reflow the whole nav subtree on every frame. */ function SidebarLabel({ children, show }: { children: ReactNode; show: boolean }) { return ( {children} ); } function SidebarSectionHeader({ children, show, collapsible = false, expanded = false, onToggle, }: { children: ReactNode; show: boolean; collapsible?: boolean; expanded?: boolean; onToggle?: () => void; }) { const textClass = "text-muted-foreground text-[10px] font-semibold tracking-[0.22em] uppercase"; // Centred on the 64px rail, not on the 260px surface. `-left-3` cancels the // nav's own `px-3`, so this box starts at the sidebar's left edge and spans // exactly the rail — otherwise the dash sits 12px right of centre. const collapsedDivider = (