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>
128 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|