'use client' import React, { useMemo, useState } from 'react' import { ChevronRight, ChevronDown, Folder, FolderOpen, Check, Search, StickyNote, } from 'lucide-react' import { motion, AnimatePresence } from 'motion/react' export type NotebookPickerItem = { id: string name: string icon?: string | null parentId?: string | null trashedAt?: Date | string | null } const PANEL_WIDTH = 280 const PANEL_HEIGHT = 350 const VIEWPORT_PADDING = 8 export function getNotebookPickerPosition( rect: DOMRect, options?: { preferDropUp?: boolean; align?: 'start' | 'end'; panelWidth?: number; panelHeight?: number }, ): React.CSSProperties { const panelWidth = options?.panelWidth ?? PANEL_WIDTH const panelHeight = options?.panelHeight ?? PANEL_HEIGHT const align = options?.align ?? 'start' const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight let left = align === 'end' ? rect.right - panelWidth : rect.left left = Math.max(VIEWPORT_PADDING, Math.min(left, viewportWidth - panelWidth - VIEWPORT_PADDING)) const spaceBelow = viewportHeight - rect.bottom - VIEWPORT_PADDING const spaceAbove = rect.top - VIEWPORT_PADDING const preferDropUp = options?.preferDropUp ?? false const openUp = preferDropUp || (spaceBelow < panelHeight && spaceAbove > spaceBelow) if (openUp) { const bottom = viewportHeight - rect.top + VIEWPORT_PADDING return { position: 'fixed', bottom: Math.max(VIEWPORT_PADDING, bottom), left, width: panelWidth, maxHeight: Math.min(panelHeight, rect.top - VIEWPORT_PADDING * 2), zIndex: 9999, } } const top = rect.bottom + VIEWPORT_PADDING return { position: 'fixed', top: Math.min(top, viewportHeight - VIEWPORT_PADDING), left, width: panelWidth, maxHeight: Math.min(panelHeight, viewportHeight - top - VIEWPORT_PADDING), zIndex: 9999, } } type NotebookHierarchyPanelProps = { notebooks: NotebookPickerItem[] selectedId?: string | null onSelect: (id: string) => void onClose: () => void searchPlaceholder?: string footerLabel?: string closeLabel?: string showGeneralNotes?: boolean generalNotesLabel?: string onSelectGeneralNotes?: () => void className?: string style?: React.CSSProperties } export function NotebookHierarchyPanel({ notebooks, selectedId = null, onSelect, onClose, searchPlaceholder = 'Filter notebooks…', footerLabel, closeLabel = 'Close', showGeneralNotes = false, generalNotesLabel = 'General Notes', onSelectGeneralNotes, className = '', style, }: NotebookHierarchyPanelProps) { const [searchQuery, setSearchQuery] = useState('') const [expandedIds, setExpandedIds] = useState>(new Set()) const activeNotebooks = useMemo( () => notebooks.filter((nb) => !nb.trashedAt), [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): React.ReactNode => { const children = activeNotebooks.filter((nb) => (nb.parentId ?? null) === (parentId ?? null)) if (children.length === 0) return null return (
0 ? 'ms-4 border-s border-border/40 ps-2' : ''}> {children.map((notebook) => { const isExpanded = expandedIds.has(notebook.id) || searchQuery.length > 0 const hasChildren = activeNotebooks.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 = activeNotebooks.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) onClose() }} className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group ${isSelected ? 'bg-brand-accent/15 text-brand-accent font-bold dark:bg-brand-accent/20' : 'hover:bg-muted dark:hover:bg-white/5 text-ink'}`} >
{hasChildren ? ( ) : null}
{isExpanded && hasChildren ? : }
{notebook.name} {isSelected && }
{isExpanded && ( {renderTree(notebook.id, level + 1)} )}
) })}
) } return ( e.stopPropagation()} >
setSearchQuery(e.target.value)} placeholder={searchPlaceholder} className="w-full bg-card border border-border rounded-lg ps-9 pe-4 py-2 text-xs outline-none focus:border-brand-accent transition-colors" />
{showGeneralNotes && onSelectGeneralNotes && ( )} {renderTree(null)}
{footerLabel ? ( {footerLabel} ) : ( )}
) }