"use client" /** * Library secondary sidebar — All / My / folder tree (Font Awesome only). * Scope syncs to the main hub via `?scope=` + optional `folderId=` (`lib/library-nav.ts`). */ import * as React from "react" import { Link } from "react-router-dom" import { useLocation, useNavigate, useSearchParams } from "react-router-dom" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Shortcut, } from "@/components/ui/dropdown-menu" import { Kbd, KbdGroup } from "@/components/ui/kbd" import { Tip } from "@/components/ui/tip" import { SidebarNavLabel } from "@/components/ui/sidebar-nav-label" import { useSidebar } from "@/components/ui/sidebar" import { cn } from "@/lib/utils" import type { LibraryFolder } from "@/lib/mock/library-folders" import { DEFAULT_LIBRARY_FOLDERS, newFolderId, collectFolderDescendantIds } from "@/lib/mock/library-folders" import { useSecondaryPanel } from "@/components/sidebar" import { useIsMobile } from "@/hooks/use-mobile" import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom" import { LibraryNewFolderSheet } from "@/components/library-new-folder-sheet" import { isLibraryNavActive, parseLibraryNav, coerceLibraryNav, libraryNavStatesEqual, LIBRARY_FAVORITES_FOLDER_ID, libraryFavoritesFolderHref, libraryHubScopeHref, } from "@/lib/library-nav" import { LibraryFolderTreeBranch } from "@/components/data-views/library-folder-tree-branch" import { outlineTreeBranchDepthStyle } from "@/components/data-views/outline-tree-menu" function NavRow({ href, active, iconClass, label, onClick, }: { href: string active: boolean iconClass: string label: string /** e.g. reopen secondary panel on same-route “All questions” */ onClick?: () => void }) { const { dismissNavFlyout } = useSidebar() return (
  • { onClick?.() dismissNavFlyout() }} aria-current={active ? "page" : undefined} className={cn( "flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-left text-sm transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", active ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground ring-1 ring-inset ring-sidebar-border/80" : "text-sidebar-foreground hover:bg-sidebar-accent/50", )} > {label}
  • ) } /** Icon-rail row — matches primary sidebar collapsed hit target (`size-9`). */ function IconNavRow({ href, active, iconClass, label, onClick, }: { href: string active: boolean iconClass: string label: string onClick?: () => void }) { const { dismissNavFlyout } = useSidebar() return (
  • { onClick?.() dismissNavFlyout() }} aria-current={active ? "page" : undefined} className={cn( "flex size-9 shrink-0 items-center justify-center rounded-md transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent/50", )} >
  • ) } export function LibrarySecondaryNav() { const pathname = useLocation().pathname const navigate = useNavigate() const [searchParams] = useSearchParams() const searchParamsKey = searchParams.toString() const { openPanel, libraryFolderBridge, libraryAccessBridge, secondaryPanelCompact } = useSecondaryPanel() const isMobile = useIsMobile() const reflowZoom = useSidebarReflowZoom() const navFlyout = isMobile || reflowZoom const showCompactRail = secondaryPanelCompact && !navFlyout const { dismissNavFlyout } = useSidebar() const [newFolderOpen, setNewFolderOpen] = React.useState(false) const [newFolderParentId, setNewFolderParentId] = React.useState(null) const [customizingFolder, setCustomizingFolder] = React.useState(null) const [deleteFolder, setDeleteFolder] = React.useState(null) const folders = libraryFolderBridge?.folders ?? DEFAULT_LIBRARY_FOLDERS const nav = React.useMemo(() => { const parsed = parseLibraryNav(new URLSearchParams(searchParamsKey)) return coerceLibraryNav(parsed, folders) }, [searchParamsKey, folders]) React.useEffect(() => { const parsed = parseLibraryNav(new URLSearchParams(searchParamsKey)) const coerced = coerceLibraryNav(parsed, folders) if (libraryNavStatesEqual(parsed, coerced)) return navigate( libraryHubScopeHref(pathname, searchParams, { scope: coerced.scope, folderId: coerced.folderId, }), { replace: true }, ) }, [folders, navigate, pathname, searchParams, searchParamsKey]) const folderTreeScopeActive = nav.scope === "folder" && nav.folderId != null && nav.folderId !== LIBRARY_FAVORITES_FOLDER_ID const canManageFolders = libraryFolderBridge != null const canManageAccess = libraryAccessBridge != null /** Favorites is a primary nav row (with All / My), not under “Folders”. */ const folderTreeRoots = React.useMemo( () => folders .filter(f => f.parentId === null && f.id !== LIBRARY_FAVORITES_FOLDER_ID) .sort((a, b) => a.name.localeCompare(b.name)), [folders], ) const openTopLevelFolder = React.useCallback(() => { setCustomizingFolder(null) setNewFolderParentId(nav.scope === "folder" ? nav.folderId : null) setNewFolderOpen(true) }, [nav.folderId, nav.scope]) const openSubfolder = React.useCallback((parentId: string) => { setCustomizingFolder(null) setNewFolderParentId(parentId) setNewFolderOpen(true) }, []) const openCustomizeFolder = React.useCallback((folder: LibraryFolder) => { setCustomizingFolder(folder) setNewFolderParentId(folder.parentId) setNewFolderOpen(true) }, []) const openManageAccess = React.useCallback(() => { libraryAccessBridge?.openManageAccess() }, [libraryAccessBridge]) const openDeleteFolder = React.useCallback((folder: LibraryFolder) => { setDeleteFolder(folder) }, []) const commitDeleteFolder = React.useCallback(() => { if (!deleteFolder || !libraryFolderBridge) return const victim = deleteFolder const parent = victim.parentId const desc = collectFolderDescendantIds(folders, victim.id) const remaining = folders.filter(f => !desc.has(f.id)) if (remaining.length === 0) { setDeleteFolder(null) return } const parentStillExists = parent !== null && remaining.some(f => f.id === parent) const fallbackRoot = remaining.find(f => f.parentId === null)?.id const reassignTarget = parentStillExists ? parent : (fallbackRoot ?? remaining[0]!.id) libraryFolderBridge.onFoldersChange(remaining) libraryFolderBridge.onItemsChange(prev => prev.map(item => (desc.has(item.folderId) ? { ...item, folderId: reassignTarget } : item)), ) if (nav.scope === "folder" && nav.folderId && desc.has(nav.folderId)) { navigate( libraryHubScopeHref( pathname, new URLSearchParams(searchParamsKey), parentStillExists ? { scope: "folder", folderId: parent! } : { scope: "all" }, ), { replace: true }, ) } setDeleteFolder(null) }, [deleteFolder, folders, nav.folderId, nav.scope, pathname, libraryFolderBridge, navigate, searchParamsKey]) const sheetParentId = customizingFolder?.parentId ?? newFolderParentId const flattenedFolderLinks = React.useMemo(() => { const out: LibraryFolder[] = [] const walk = (folder: LibraryFolder) => { out.push(folder) folders .filter(c => c.parentId === folder.id) .sort((a, b) => a.name.localeCompare(b.name)) .forEach(walk) } folderTreeRoots.forEach(walk) return out }, [folderTreeRoots, folders]) const hubNavModals = ( <> { setNewFolderOpen(open) if (!open) { setCustomizingFolder(null) setNewFolderParentId(null) } }} parentFolderId={sheetParentId} customizingFolder={customizingFolder} descriptionText={ customizingFolder ? "Update the folder name, color, and icon shown in the navigation and folder views." : sheetParentId ? "The folder is created inside the folder selected in the navigation." : "Add a top-level folder to the library." } onCreated={folder => { if (customizingFolder) { libraryFolderBridge?.onFoldersChange(prev => prev.map(item => item.id === customizingFolder.id ? { ...item, name: folder.name, icon: folder.icon, colorKey: folder.colorKey, } : item, ), ) } else { libraryFolderBridge?.onFoldersChange(prev => [...prev, { ...folder, id: newFolderId() }]) } setNewFolderOpen(false) setCustomizingFolder(null) setNewFolderParentId(null) }} /> {deleteFolder ? : null} !open && setDeleteFolder(null)}> Delete folder? {deleteFolder ? `${deleteFolder.name} and its subfolders will be removed. Questions inside move to the parent folder (or the first top-level folder).` : null} ) if (showCompactRail) { return ( <>