"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}
>
)
if (showCompactRail) {
return (
<>
{hubNavModals}
>
)
}
return (
<>
openPanel("library")}
/>
openPanel("library")}
/>
-
Folders
{folderTreeRoots.map(folder => (
-
))}
{hubNavModals}
>
)
}