'use client' import React, { useState, useMemo, useRef, useCallback, useLayoutEffect } from 'react' import { ChevronRight, ChevronDown, Folder, FolderOpen, Check, Search, } from 'lucide-react' import { Notebook } from '@/lib/types' import { motion, AnimatePresence } from 'motion/react' import { cn } from '@/lib/utils' import { createPortal } from 'react-dom' interface HierarchicalNotebookSelectorProps { notebooks: Notebook[] selectedId: string | null onSelect: (id: string) => void className?: string placeholder?: string dropUp?: boolean } export function HierarchicalNotebookSelector({ notebooks, selectedId, onSelect, className = '', placeholder = 'Select a notebook...', dropUp = false, }: HierarchicalNotebookSelectorProps) { const [isOpen, setIsOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [expandedIds, setExpandedIds] = useState>(new Set()) const triggerRef = useRef(null) const [dropdownStyle, setDropdownStyle] = useState(null) const computeStyle = useCallback(() => { if (!triggerRef.current) return null const rect = triggerRef.current.getBoundingClientRect() if (dropUp) { return { position: 'fixed' as const, bottom: window.innerHeight - rect.top + 8, left: rect.left, width: Math.max(rect.width, 280), zIndex: 9999, } } return { position: 'fixed' as const, top: rect.bottom + 8, left: rect.left, width: Math.max(rect.width, 280), zIndex: 9999, } }, [dropUp]) useLayoutEffect(() => { if (isOpen) { setDropdownStyle(computeStyle()) const handleResize = () => setDropdownStyle(computeStyle()) window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) } else { setDropdownStyle(null) } }, [isOpen, computeStyle]) const selectedNotebook = notebooks.find(nb => nb.id === selectedId) const path = useMemo(() => { if (!selectedNotebook) return [] const trail: Notebook[] = [] let current: Notebook | undefined = selectedNotebook while (current) { trail.unshift(current) if (!current.parentId) break const parent = notebooks.find(nb => nb.id === current!.parentId) if (!parent) break current = parent } return trail }, [selectedNotebook, notebooks]) const toggleExpand = (e: React.MouseEvent, id: string) => { e.stopPropagation() const next = new Set(expandedIds) if (next.has(id)) next.delete(id) else next.add(id) setExpandedIds(next) } const renderTree = (parentId: string | null | undefined, level = 0) => { const children = notebooks.filter( nb => (nb.parentId ?? null) === (parentId ?? null) ) if (children.length === 0) return null return (
0 ? 'ml-4 border-l border-border/40 pl-2' : ''}> {children.map(notebook => { const isExpanded = expandedIds.has(notebook.id) || searchQuery.length > 0 const hasChildren = notebooks.some(nb => nb.parentId === notebook.id) const isSelected = selectedId === notebook.id if (searchQuery && !notebook.name.toLowerCase().includes(searchQuery.toLowerCase())) { const hasMatchingChild = (id: string): boolean => { const kids = notebooks.filter(nb => nb.parentId === id) return kids.some(nb => nb.name.toLowerCase().includes(searchQuery.toLowerCase()) || hasMatchingChild(nb.id)) } if (!hasMatchingChild(notebook.id)) return null } return (
{ onSelect(notebook.id) if (!searchQuery) setIsOpen(false) }} className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group ${isSelected ? 'bg-blueprint/10 text-blueprint font-bold dark:bg-blueprint/10' : 'hover:bg-slate-50 dark:hover:bg-white/5 text-ink'}`} >
{hasChildren ? ( ) : null}
{isExpanded && hasChildren ? : }
{notebook.name} {isSelected && }
{isExpanded && ( {renderTree(notebook.id, level + 1)} )}
) })}
) } return (
setIsOpen(!isOpen)} className="w-full bg-slate-50 dark:bg-white/5 border border-border/80 rounded-xl px-4 py-4 text-sm outline-none focus:ring-4 ring-blueprint/5 focus:border-blueprint/40 transition-all cursor-pointer text-ink flex items-center gap-3" >
{path.length > 0 ? (
{path.map((item, i) => ( {i > 0 && /} {item.name} ))}
) : ( {placeholder} )}
{isOpen && dropdownStyle && typeof window !== 'undefined' && createPortal( <>
setIsOpen(false)} />
setSearchQuery(e.target.value)} placeholder={placeholder} className="w-full bg-card border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-blueprint transition-colors" />
{renderTree(null)}
{notebooks.length} notebooks
, document.body )}
) }