fix(ui): l’arbre latéral n’affiche plus que les carnets

Les notes restent dans la page. Le chiffre à droite du nom, la recherche par titre et les sous-carnets sont inchangés.
This commit is contained in:
Antigravity
2026-09-05 21:04:44 +00:00
parent 3bdbec575a
commit 889e63165b

View File

@@ -32,7 +32,6 @@ import {
Home, Home,
Search, Search,
GraduationCap, GraduationCap,
FileText,
Folder, Folder,
FolderOpen, FolderOpen,
LayoutGrid, LayoutGrid,
@@ -56,7 +55,7 @@ import { Notebook, Note } from '@/lib/types'
import { toast } from 'sonner' import { toast } from 'sonner'
import { motion, AnimatePresence } from 'motion/react' import { motion, AnimatePresence } from 'motion/react'
import { getNoteDisplayTitle } from '@/lib/note-preview' import { getNoteDisplayTitle } from '@/lib/note-preview'
import { listAiCreatedNotebookIds, isRecentlyCreated } from '@/lib/ai-created-highlight' import { listAiCreatedNotebookIds } from '@/lib/ai-created-highlight'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
const CreateNotebookDialog = dynamic(() => import('./create-notebook-dialog').then(m => ({ default: m.CreateNotebookDialog })), { ssr: false }) const CreateNotebookDialog = dynamic(() => import('./create-notebook-dialog').then(m => ({ default: m.CreateNotebookDialog })), { ssr: false })
@@ -84,71 +83,6 @@ const SIDEBAR_WIDTH_KEY = 'memento-sidebar-width'
const SIDEBAR_WIDTH_MIN_PX = 280 const SIDEBAR_WIDTH_MIN_PX = 280
const SIDEBAR_WIDTH_MAX_PX = 560 const SIDEBAR_WIDTH_MAX_PX = 560
function NoteLink({
title,
isActive,
isPinned,
autoGenerated,
isRecent,
onClick,
}: {
title: string
isActive: boolean
isPinned?: boolean
/** Note créée par l'IA (agents, wizard, …) */
autoGenerated?: boolean
/** Créée récemment (< 48h) — point discret « nouveau » */
isRecent?: boolean
onClick: () => void
}) {
const { language, t } = useLanguage()
const slideX = language === 'fa' || language === 'ar' ? 10 : -10
return (
<motion.button
initial={{ opacity: 0, x: slideX }}
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={cn(
'w-full flex items-center gap-2 ps-6 pe-3 py-1.5 text-[11px] transition-all rounded-lg text-start',
isActive
? 'bg-white dark:bg-white/10 shadow-sm border border-border/50 text-foreground font-semibold'
: autoGenerated
? 'text-foreground/90 hover:text-foreground hover:bg-brand-accent/5 border border-transparent'
: 'text-muted-foreground hover:text-foreground hover:bg-white/30 dark:hover:bg-white/5',
autoGenerated && !isActive && 'bg-brand-accent/[0.04]',
)}
title={autoGenerated ? (t('sidebar.aiGeneratedNote') || 'Note générée par l\'IA') : undefined}
>
{autoGenerated ? (
<Sparkles
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-brand-accent/80')}
aria-hidden
/>
) : (
<FileText
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-muted-foreground/70')}
/>
)}
<span className="truncate flex-1">{title}</span>
{autoGenerated && (
<span className="shrink-0 text-[8px] font-bold uppercase tracking-wider text-brand-accent/90 bg-brand-accent/10 px-1.5 py-0.5 rounded">
{t('sidebar.aiBadge') || 'IA'}
</span>
)}
{isRecent && !autoGenerated && (
<span
className="shrink-0 w-1.5 h-1.5 rounded-full bg-brand-accent"
title={t('sidebar.recentNote') || 'Récemment créée'}
aria-label={t('sidebar.recentNote') || 'Récemment créée'}
/>
)}
{isPinned && <Pin size={10} className="text-amber-500 fill-amber-500 shrink-0" />}
</motion.button>
)
}
function SidebarBrainstorms() { function SidebarBrainstorms() {
const { data: sessions, isLoading } = useBrainstormSessions() const { data: sessions, isLoading } = useBrainstormSessions()
const deleteBrainstorm = useDeleteBrainstorm() const deleteBrainstorm = useDeleteBrainstorm()
@@ -377,17 +311,14 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo
function SidebarCarnetItem({ function SidebarCarnetItem({
carnet, carnet,
isActive, isActive,
notes, noteCount,
activeNoteId,
onCarnetClick, onCarnetClick,
onNoteClick,
onAddSubNotebook, onAddSubNotebook,
onRename, onRename,
onDelete, onDelete,
onTogglePin, onTogglePin,
isPinned, isPinned,
isAiCreated, isAiCreated,
children,
isDragging, isDragging,
dragHandleProps, dragHandleProps,
level, level,
@@ -398,10 +329,8 @@ function SidebarCarnetItem({
}: { }: {
carnet: { id: string; name: string; initial: string; isPrivate?: boolean } carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
isActive: boolean isActive: boolean
notes: { id: string; title: string; isPinned?: boolean; autoGenerated?: boolean; isRecent?: boolean }[] noteCount: number
activeNoteId: string | null
onCarnetClick: () => void onCarnetClick: () => void
onNoteClick: (noteId: string, carnetId: string) => void
onAddSubNotebook: () => void onAddSubNotebook: () => void
onRename: () => void onRename: () => void
onDelete: () => void onDelete: () => void
@@ -409,7 +338,6 @@ function SidebarCarnetItem({
isPinned: boolean isPinned: boolean
/** Carnet créé via wizard IA (repère temporaire) */ /** Carnet créé via wizard IA (repère temporaire) */
isAiCreated?: boolean isAiCreated?: boolean
children?: React.ReactNode
isDragging?: boolean isDragging?: boolean
dragHandleProps?: React.HTMLAttributes<HTMLDivElement> dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
level: number level: number
@@ -420,7 +348,8 @@ function SidebarCarnetItem({
}) { }) {
const { t, language } = useLanguage() const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar' const isRtl = language === 'fa' || language === 'ar'
const hasChildren = hasChildNotebooks || React.Children.count(children) > 0 || notes.length > 0 const hasChildren = Boolean(hasChildNotebooks)
const showOpenFolder = hasChildren && isExpanded
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null) const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
// Close context menu on outside click // Close context menu on outside click
@@ -434,6 +363,7 @@ function SidebarCarnetItem({
return ( return (
<div className={cn('transition-opacity', isDragging && 'opacity-40')}> <div className={cn('transition-opacity', isDragging && 'opacity-40')}>
<div <div
data-notebook-id={carnet.id}
className="flex items-center group relative h-10" className="flex items-center group relative h-10"
style={{ paddingInlineStart: `${level * 24 + 8}px` }} style={{ paddingInlineStart: `${level * 24 + 8}px` }}
onContextMenu={(e) => { onContextMenu={(e) => {
@@ -461,6 +391,8 @@ function SidebarCarnetItem({
{hasChildren ? ( {hasChildren ? (
<button <button
type="button"
aria-expanded={isExpanded}
onClick={(e) => { e.stopPropagation(); toggleExpand() }} onClick={(e) => { e.stopPropagation(); toggleExpand() }}
className="shrink-0 w-5 h-5 flex items-center justify-center hover:bg-foreground/5 rounded-md transition-colors text-muted-foreground" className="shrink-0 w-5 h-5 flex items-center justify-center hover:bg-foreground/5 rounded-md transition-colors text-muted-foreground"
> >
@@ -500,7 +432,7 @@ function SidebarCarnetItem({
'w-5 h-5 flex items-center justify-center shrink-0 transition-colors', 'w-5 h-5 flex items-center justify-center shrink-0 transition-colors',
isActive ? 'text-brand-accent' : 'text-muted-foreground/80', isActive ? 'text-brand-accent' : 'text-muted-foreground/80',
)}> )}>
{isExpanded ? <FolderOpen size={13} /> : <Folder size={13} />} {showOpenFolder ? <FolderOpen size={13} /> : <Folder size={13} />}
</div> </div>
<div className="flex-1 text-start flex items-center gap-2 min-w-0"> <div className="flex-1 text-start flex items-center gap-2 min-w-0">
@@ -523,9 +455,9 @@ function SidebarCarnetItem({
</div> </div>
{/* Compteur de notes (toujours visible, à droite du nom) */} {/* Compteur de notes (toujours visible, à droite du nom) */}
{notes.length > 0 && ( {noteCount > 0 && (
<span className="text-[11px] font-medium tabular-nums text-foreground/75 px-1.5 bg-brand-accent/5 border border-border/40 rounded-full transition-colors shrink-0 ms-auto"> <span className="text-[11px] font-medium tabular-nums text-foreground/75 px-1.5 bg-brand-accent/5 border border-border/40 rounded-full transition-colors shrink-0 ms-auto">
{notes.length} {noteCount}
</span> </span>
)} )}
@@ -609,42 +541,6 @@ function SidebarCarnetItem({
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden"
>
<div className="relative" style={{ marginInlineStart: `${(level + 1) * 16 + 10}px` }}>
<div className="absolute start-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="space-y-0.5 py-1">
{children}
{isExpanded && notes.map(note => (
<NoteLink
key={note.id}
title={note.title}
isPinned={note.isPinned}
autoGenerated={note.autoGenerated}
isRecent={note.isRecent}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id, carnet.id)}
/>
))}
{isExpanded && notes.length === 0 && !hasChildren && (
<p className="ps-6 py-1 text-[9px] italic text-muted-foreground/40 font-light">
{t('sidebar.notebookEmpty')}
</p>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
) )
} }
@@ -693,7 +589,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const [isRenaming, setIsRenaming] = useState(false) const [isRenaming, setIsRenaming] = useState(false)
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set()) const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set()) const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set())
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string; isPinned?: boolean; autoGenerated?: boolean; isRecent?: boolean }[]>>({}) const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string }[]>>({})
const [aiNotebookIds, setAiNotebookIds] = useState<Set<string>>(() => new Set()) const [aiNotebookIds, setAiNotebookIds] = useState<Set<string>>(() => new Set())
const [activeView, setActiveView] = useState<NavigationView>('notebooks') const [activeView, setActiveView] = useState<NavigationView>('notebooks')
const [sortOrder, setSortOrder] = useState<SortOrder>('newest') const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
@@ -838,19 +734,23 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const filteredNotebookIds = useMemo(() => { const filteredNotebookIds = useMemo(() => {
const q = notebookSearchQuery.trim().toLowerCase() const q = notebookSearchQuery.trim().toLowerCase()
if (!q) return null if (!q) return null
return new Set( const byId = new Map(notebooks.map((nb) => [nb.id, nb]))
notebooks const ids = new Set<string>()
.filter( for (const nb of notebooks) {
(nb) => const nameHit = nb.name.toLowerCase().includes(q)
nb.name.toLowerCase().includes(q) || const noteHit = (notebookNotes[nb.id] || []).some((n) => n.title.toLowerCase().includes(q))
(notebookNotes[nb.id] || []).some((n) => n.title.toLowerCase().includes(q)), if (!nameHit && !noteHit) continue
) ids.add(nb.id)
.map((nb) => nb.id), let parentId = nb.parentId ?? null
) while (parentId && !ids.has(parentId)) {
ids.add(parentId)
parentId = byId.get(parentId)?.parentId ?? null
}
}
return ids
}, [notebooks, notebookNotes, notebookSearchQuery]) }, [notebooks, notebookNotes, notebookSearchQuery])
const currentNotebookId = searchParams.get('notebook') const currentNotebookId = searchParams.get('notebook')
const currentNoteId = searchParams.get('openNote')
useEffect(() => { useEffect(() => {
if (!currentNotebookId) return if (!currentNotebookId) return
@@ -866,6 +766,29 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
}) })
}, [currentNotebookId, notebooks]) }, [currentNotebookId, notebooks])
useEffect(() => {
if (!filteredNotebookIds) return
setExpandedIds((prev) => {
const next = new Set(prev)
for (const id of filteredNotebookIds) {
const kids = childNotebooks.get(id) || []
if (kids.some((c) => filteredNotebookIds.has(c.id))) next.add(id)
}
return next
})
}, [filteredNotebookIds, childNotebooks])
useEffect(() => {
if (!currentNotebookId) return
const timer = window.setTimeout(() => {
const el = notebooksPanelRef.current?.querySelector(
`[data-notebook-id="${currentNotebookId}"]`,
) as HTMLElement | null
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}, 80)
return () => window.clearTimeout(timer)
}, [currentNotebookId])
const isDashboardRoute = isDashboardHomeRoute(pathname, searchParams) const isDashboardRoute = isDashboardHomeRoute(pathname, searchParams)
const panelView: NavigationView = pathname.startsWith('/settings') ? 'settings' : activeView const panelView: NavigationView = pathname.startsWith('/settings') ? 'settings' : activeView
@@ -981,9 +904,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const mapped = notes.map((n: Note) => ({ const mapped = notes.map((n: Note) => ({
id: n.id, id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled')), title: getNoteDisplayTitle(n, t('notes.untitled')),
isPinned: n.isPinned,
autoGenerated: Boolean(n.autoGenerated),
isRecent: isRecentlyCreated(n.createdAt, 48),
})) }))
return [nb.id, mapped] as const return [nb.id, mapped] as const
}) })
@@ -1018,7 +938,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
if (list.some((n) => n.id === detail.note.id)) return prev if (list.some((n) => n.id === detail.note.id)) return prev
return { return {
...prev, ...prev,
[nbId]: [{ id: detail.note.id, title, isPinned: detail.note.isPinned }, ...list], [nbId]: [{ id: detail.note.id, title }, ...list],
} }
}) })
return return
@@ -1027,14 +947,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const note = detail.note const note = detail.note
const title = getNoteDisplayTitle(note, t('notes.untitled')) const title = getNoteDisplayTitle(note, t('notes.untitled'))
setNotebookNotes((prev) => { setNotebookNotes((prev) => {
const next: Record<string, { id: string; title: string; isPinned?: boolean }[]> = {} const next: Record<string, { id: string; title: string }[]> = {}
for (const [key, list] of Object.entries(prev)) { for (const [key, list] of Object.entries(prev)) {
const filtered = list.filter((n) => n.id !== note.id) const filtered = list.filter((n) => n.id !== note.id)
if (filtered.length > 0) next[key] = filtered if (filtered.length > 0) next[key] = filtered
} }
if (note.notebookId) { if (note.notebookId) {
next[note.notebookId] = [ next[note.notebookId] = [
{ id: note.id, title, isPinned: note.isPinned }, { id: note.id, title },
...(next[note.notebookId] || []), ...(next[note.notebookId] || []),
] ]
} }
@@ -1070,14 +990,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
router.push('/home?forceList=1') router.push('/home?forceList=1')
} }
const handleNoteClick = (noteId: string, notebookId: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set('notebook', notebookId)
params.set('openNote', noteId)
params.delete('forceList')
router.push(`/home?${params.toString()}`)
}
// ── Drag state ── // ── Drag state ──
const [dropTarget, setDropTarget] = useState<string | null>(null) const [dropTarget, setDropTarget] = useState<string | null>(null)
const [dropAction, setDropAction] = useState<'into' | 'before' | 'after' | null>(null) const [dropAction, setDropAction] = useState<'into' | 'before' | 'after' | null>(null)
@@ -1329,16 +1241,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
isPrivate: notebook.isPrivate, isPrivate: notebook.isPrivate,
}} }}
isActive={isActive} isActive={isActive}
notes={notes} noteCount={notes.length}
activeNoteId={currentNoteId}
onCarnetClick={() => { onCarnetClick={() => {
if (currentNotebookId === notebook.id) { if (currentNotebookId === notebook.id) {
toggleExpand(notebook.id) if (children.length > 0) toggleExpand(notebook.id)
} else { } else {
handleCarnetClick(notebook.id) handleCarnetClick(notebook.id)
} }
}} }}
onNoteClick={handleNoteClick}
onAddSubNotebook={() => { onAddSubNotebook={() => {
setCreateParentId(notebook.id) setCreateParentId(notebook.id)
setIsCreateDialogOpen(true) setIsCreateDialogOpen(true)
@@ -1361,7 +1271,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</motion.div> </motion.div>
) )
}) })
}, [rootNotebooks, childNotebooks, filteredNotebookIds, currentNotebookId, currentNoteId, notebookNotes, draggedId, dropTarget, dropAction, expandedIds, pinnedIds, aiNotebookIds, toggleExpand, handleCarnetClick, handleNoteClick, handleDragStart, handleDragEnd, handleDropOnNotebook, handleStartRename]) }, [rootNotebooks, childNotebooks, filteredNotebookIds, currentNotebookId, notebookNotes, draggedId, dropTarget, dropAction, expandedIds, pinnedIds, aiNotebookIds, toggleExpand, handleCarnetClick, handleDragStart, handleDragEnd, handleDropOnNotebook, handleStartRename])
return ( return (
<> <>
@@ -2137,9 +2047,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const mapped = notes.map((n: Note) => ({ const mapped = notes.map((n: Note) => ({
id: n.id, id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled')), title: getNoteDisplayTitle(n, t('notes.untitled')),
isPinned: n.isPinned,
autoGenerated: Boolean(n.autoGenerated),
isRecent: true,
})) }))
setNotebookNotes((prev) => ({ ...prev, [notebookId]: mapped })) setNotebookNotes((prev) => ({ ...prev, [notebookId]: mapped }))
} catch { /* ignore */ } } catch { /* ignore */ }