Files
Momento/memento-note/components/hierarchical-notebook-selector.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

128 lines
4.3 KiB
TypeScript

'use client'
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react'
import { ChevronRight, ChevronDown, Folder, Check } from 'lucide-react'
import { createPortal } from 'react-dom'
import { AnimatePresence } from 'motion/react'
import {
getNotebookPickerPosition,
NotebookHierarchyPanel,
type NotebookPickerItem,
} from '@/components/notebook-hierarchy-panel'
interface HierarchicalNotebookSelectorProps {
notebooks: NotebookPickerItem[]
selectedId: string | null
onSelect: (id: string) => void
className?: string
placeholder?: string
dropUp?: boolean
size?: 'default' | 'sm'
}
export function HierarchicalNotebookSelector({
notebooks,
selectedId,
onSelect,
className = '',
placeholder = 'Select a notebook...',
dropUp = false,
size = 'default',
}: HierarchicalNotebookSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const triggerRef = useRef<HTMLDivElement>(null)
const [panelStyle, setPanelStyle] = useState<React.CSSProperties | undefined>()
const updatePosition = useCallback(() => {
if (!triggerRef.current) return
setPanelStyle(getNotebookPickerPosition(triggerRef.current.getBoundingClientRect(), { preferDropUp: dropUp }))
}, [dropUp])
useEffect(() => {
if (!isOpen) {
setPanelStyle(undefined)
return
}
updatePosition()
window.addEventListener('scroll', updatePosition, true)
window.addEventListener('resize', updatePosition)
return () => {
window.removeEventListener('scroll', updatePosition, true)
window.removeEventListener('resize', updatePosition)
}
}, [isOpen, updatePosition])
const selectedNotebook = notebooks.find((nb) => nb.id === selectedId)
const path = useMemo(() => {
if (!selectedNotebook) return []
const trail: typeof notebooks = []
let current: typeof notebooks[0] | 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])
return (
<div className={`relative ${className}`}>
<div
ref={triggerRef}
onClick={() => setIsOpen((prev) => !prev)}
className={`w-full bg-card dark:bg-white/5 border border-border/80 rounded-xl outline-none focus:ring-4 ring-brand-accent/10 focus:border-brand-accent/40 transition-all cursor-pointer text-ink flex items-center gap-3 ${size === 'sm' ? 'px-3 py-2 text-xs' : 'px-4 py-3 text-sm'}`}
>
<Folder size={size === 'sm' ? 14 : 16} className="text-brand-accent/70 shrink-0" />
<div className="flex-1 flex items-center gap-1 min-w-0">
{path.length > 0 ? (
<div className="flex items-center gap-1.5 truncate">
{path.map((item, i) => (
<React.Fragment key={item.id}>
{i > 0 && <span className="text-concrete/40 text-[10px]">/</span>}
<span className={`truncate ${i === path.length - 1 ? 'font-bold text-ink' : 'text-concrete'}`}>
{item.name}
</span>
</React.Fragment>
))}
</div>
) : (
<span className="text-concrete italic">{placeholder}</span>
)}
</div>
<ChevronDown
size={14}
className={`transition-transform duration-300 text-concrete shrink-0 ${isOpen ? 'rotate-180' : ''}`}
/>
</div>
{isOpen &&
typeof window !== 'undefined' &&
panelStyle &&
createPortal(
<>
<div className="fixed inset-0 z-[9998]" onClick={() => setIsOpen(false)} aria-hidden />
<AnimatePresence>
<NotebookHierarchyPanel
key="hierarchical-notebook-selector"
notebooks={notebooks}
selectedId={selectedId}
onSelect={(id) => {
onSelect(id)
setIsOpen(false)
}}
onClose={() => setIsOpen(false)}
searchPlaceholder={placeholder}
footerLabel={`${notebooks.filter((nb) => !nb.trashedAt).length} notebooks`}
style={panelStyle}
/>
</AnimatePresence>
</>,
document.body,
)}
</div>
)
}