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>
185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { AnimatePresence } from 'motion/react'
|
|
import {
|
|
getNotebookPickerPosition,
|
|
NotebookHierarchyPanel,
|
|
type NotebookPickerItem,
|
|
} from '@/components/notebook-hierarchy-panel'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
type MoveToNotebookPickerProps = {
|
|
notebooks: NotebookPickerItem[]
|
|
currentNotebookId?: string | null
|
|
onSelect: (notebookId: string | null) => void
|
|
children: React.ReactElement
|
|
align?: 'start' | 'end'
|
|
preferDropUp?: boolean
|
|
}
|
|
|
|
export function MoveToNotebookPicker({
|
|
notebooks,
|
|
currentNotebookId = null,
|
|
onSelect,
|
|
children,
|
|
align = 'end',
|
|
preferDropUp = false,
|
|
}: MoveToNotebookPickerProps) {
|
|
const { t } = useLanguage()
|
|
const [open, setOpen] = useState(false)
|
|
const triggerRef = useRef<HTMLElement>(null)
|
|
const [panelStyle, setPanelStyle] = useState<React.CSSProperties | undefined>()
|
|
|
|
const close = useCallback(() => setOpen(false), [])
|
|
|
|
const updatePosition = useCallback(() => {
|
|
if (!triggerRef.current) return
|
|
setPanelStyle(
|
|
getNotebookPickerPosition(triggerRef.current.getBoundingClientRect(), { align, preferDropUp }),
|
|
)
|
|
}, [align, preferDropUp])
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setPanelStyle(undefined)
|
|
return
|
|
}
|
|
updatePosition()
|
|
window.addEventListener('scroll', updatePosition, true)
|
|
window.addEventListener('resize', updatePosition)
|
|
return () => {
|
|
window.removeEventListener('scroll', updatePosition, true)
|
|
window.removeEventListener('resize', updatePosition)
|
|
}
|
|
}, [open, updatePosition])
|
|
|
|
const handleSelect = (notebookId: string | null) => {
|
|
onSelect(notebookId)
|
|
close()
|
|
}
|
|
|
|
const child = React.cloneElement(children, {
|
|
ref: (node: HTMLElement | null) => {
|
|
triggerRef.current = node
|
|
const childRef = (children as React.ReactElement & { ref?: React.Ref<HTMLElement> }).ref
|
|
if (typeof childRef === 'function') childRef(node)
|
|
else if (childRef && typeof childRef === 'object') {
|
|
;(childRef as React.MutableRefObject<HTMLElement | null>).current = node
|
|
}
|
|
},
|
|
onClick: (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
children.props.onClick?.(e)
|
|
setOpen((prev) => !prev)
|
|
},
|
|
})
|
|
|
|
return (
|
|
<>
|
|
{child}
|
|
{typeof window !== 'undefined' &&
|
|
createPortal(
|
|
<AnimatePresence>
|
|
{open && panelStyle && (
|
|
<>
|
|
<div className="fixed inset-0 z-[9998]" onClick={close} aria-hidden />
|
|
<NotebookHierarchyPanel
|
|
key="move-notebook-picker"
|
|
notebooks={notebooks}
|
|
selectedId={currentNotebookId}
|
|
onSelect={(id) => handleSelect(id)}
|
|
onClose={close}
|
|
showGeneralNotes
|
|
generalNotesLabel={t('notebookSuggestion.generalNotes') || 'Notes générales'}
|
|
onSelectGeneralNotes={() => handleSelect(null)}
|
|
searchPlaceholder={t('notebookSuggestion.filterNotebooks') || 'Filtrer les carnets…'}
|
|
closeLabel={t('general.close') || 'Fermer'}
|
|
style={panelStyle}
|
|
/>
|
|
</>
|
|
)}
|
|
</AnimatePresence>,
|
|
document.body,
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
type MoveToNotebookPickerPortalProps = {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
anchorRef: React.RefObject<HTMLElement | null>
|
|
notebooks: NotebookPickerItem[]
|
|
currentNotebookId?: string | null
|
|
onSelect: (notebookId: string | null) => void
|
|
align?: 'start' | 'end'
|
|
preferDropUp?: boolean
|
|
}
|
|
|
|
export function MoveToNotebookPickerPortal({
|
|
open,
|
|
onOpenChange,
|
|
anchorRef,
|
|
notebooks,
|
|
currentNotebookId = null,
|
|
onSelect,
|
|
align = 'end',
|
|
preferDropUp = false,
|
|
}: MoveToNotebookPickerPortalProps) {
|
|
const { t } = useLanguage()
|
|
const [panelStyle, setPanelStyle] = useState<React.CSSProperties | undefined>()
|
|
|
|
useEffect(() => {
|
|
if (!open || !anchorRef.current) {
|
|
setPanelStyle(undefined)
|
|
return
|
|
}
|
|
const update = () => {
|
|
if (!anchorRef.current) return
|
|
setPanelStyle(
|
|
getNotebookPickerPosition(anchorRef.current.getBoundingClientRect(), { align, preferDropUp }),
|
|
)
|
|
}
|
|
update()
|
|
window.addEventListener('scroll', update, true)
|
|
window.addEventListener('resize', update)
|
|
return () => {
|
|
window.removeEventListener('scroll', update, true)
|
|
window.removeEventListener('resize', update)
|
|
}
|
|
}, [open, anchorRef, align, preferDropUp])
|
|
|
|
const handleSelect = (notebookId: string | null) => {
|
|
onSelect(notebookId)
|
|
onOpenChange(false)
|
|
}
|
|
|
|
if (typeof window === 'undefined') return null
|
|
|
|
return createPortal(
|
|
<AnimatePresence>
|
|
{open && panelStyle && (
|
|
<>
|
|
<div className="fixed inset-0 z-[9998]" onClick={() => onOpenChange(false)} aria-hidden />
|
|
<NotebookHierarchyPanel
|
|
key="move-notebook-picker-portal"
|
|
notebooks={notebooks}
|
|
selectedId={currentNotebookId}
|
|
onSelect={(id) => handleSelect(id)}
|
|
onClose={() => onOpenChange(false)}
|
|
showGeneralNotes
|
|
generalNotesLabel={t('notebookSuggestion.generalNotes') || 'Notes générales'}
|
|
onSelectGeneralNotes={() => handleSelect(null)}
|
|
searchPlaceholder={t('notebookSuggestion.filterNotebooks') || 'Filtrer les carnets…'}
|
|
closeLabel={t('general.close') || 'Fermer'}
|
|
style={panelStyle}
|
|
/>
|
|
</>
|
|
)}
|
|
</AnimatePresence>,
|
|
document.body,
|
|
)
|
|
}
|