Files
Momento/memento-note/components/move-to-notebook-picker.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

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,
)
}