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>
This commit is contained in:
184
memento-note/components/move-to-notebook-picker.tsx
Normal file
184
memento-note/components/move-to-notebook-picker.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user