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:
@@ -32,7 +32,6 @@ import {
|
||||
Home,
|
||||
Search,
|
||||
GraduationCap,
|
||||
FileText,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
LayoutGrid,
|
||||
@@ -56,7 +55,7 @@ import { Notebook, Note } from '@/lib/types'
|
||||
import { toast } from 'sonner'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
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'
|
||||
|
||||
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_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() {
|
||||
const { data: sessions, isLoading } = useBrainstormSessions()
|
||||
const deleteBrainstorm = useDeleteBrainstorm()
|
||||
@@ -377,17 +311,14 @@ function SidebarReminders({ onOpenNote }: { onOpenNote: (noteId: string, noteboo
|
||||
function SidebarCarnetItem({
|
||||
carnet,
|
||||
isActive,
|
||||
notes,
|
||||
activeNoteId,
|
||||
noteCount,
|
||||
onCarnetClick,
|
||||
onNoteClick,
|
||||
onAddSubNotebook,
|
||||
onRename,
|
||||
onDelete,
|
||||
onTogglePin,
|
||||
isPinned,
|
||||
isAiCreated,
|
||||
children,
|
||||
isDragging,
|
||||
dragHandleProps,
|
||||
level,
|
||||
@@ -398,10 +329,8 @@ function SidebarCarnetItem({
|
||||
}: {
|
||||
carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
|
||||
isActive: boolean
|
||||
notes: { id: string; title: string; isPinned?: boolean; autoGenerated?: boolean; isRecent?: boolean }[]
|
||||
activeNoteId: string | null
|
||||
noteCount: number
|
||||
onCarnetClick: () => void
|
||||
onNoteClick: (noteId: string, carnetId: string) => void
|
||||
onAddSubNotebook: () => void
|
||||
onRename: () => void
|
||||
onDelete: () => void
|
||||
@@ -409,7 +338,6 @@ function SidebarCarnetItem({
|
||||
isPinned: boolean
|
||||
/** Carnet créé via wizard IA (repère temporaire) */
|
||||
isAiCreated?: boolean
|
||||
children?: React.ReactNode
|
||||
isDragging?: boolean
|
||||
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
level: number
|
||||
@@ -420,7 +348,8 @@ function SidebarCarnetItem({
|
||||
}) {
|
||||
const { t, language } = useLanguage()
|
||||
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)
|
||||
|
||||
// Close context menu on outside click
|
||||
@@ -434,6 +363,7 @@ function SidebarCarnetItem({
|
||||
return (
|
||||
<div className={cn('transition-opacity', isDragging && 'opacity-40')}>
|
||||
<div
|
||||
data-notebook-id={carnet.id}
|
||||
className="flex items-center group relative h-10"
|
||||
style={{ paddingInlineStart: `${level * 24 + 8}px` }}
|
||||
onContextMenu={(e) => {
|
||||
@@ -461,6 +391,8 @@ function SidebarCarnetItem({
|
||||
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
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"
|
||||
>
|
||||
@@ -500,7 +432,7 @@ function SidebarCarnetItem({
|
||||
'w-5 h-5 flex items-center justify-center shrink-0 transition-colors',
|
||||
isActive ? 'text-brand-accent' : 'text-muted-foreground/80',
|
||||
)}>
|
||||
{isExpanded ? <FolderOpen size={13} /> : <Folder size={13} />}
|
||||
{showOpenFolder ? <FolderOpen size={13} /> : <Folder size={13} />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-start flex items-center gap-2 min-w-0">
|
||||
@@ -523,9 +455,9 @@ function SidebarCarnetItem({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{notes.length}
|
||||
{noteCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -609,42 +541,6 @@ function SidebarCarnetItem({
|
||||
</motion.div>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -693,7 +589,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [expandedIds, setExpandedIds] = 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 [activeView, setActiveView] = useState<NavigationView>('notebooks')
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
|
||||
@@ -838,19 +734,23 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const filteredNotebookIds = useMemo(() => {
|
||||
const q = notebookSearchQuery.trim().toLowerCase()
|
||||
if (!q) return null
|
||||
return new Set(
|
||||
notebooks
|
||||
.filter(
|
||||
(nb) =>
|
||||
nb.name.toLowerCase().includes(q) ||
|
||||
(notebookNotes[nb.id] || []).some((n) => n.title.toLowerCase().includes(q)),
|
||||
)
|
||||
.map((nb) => nb.id),
|
||||
)
|
||||
const byId = new Map(notebooks.map((nb) => [nb.id, nb]))
|
||||
const ids = new Set<string>()
|
||||
for (const nb of notebooks) {
|
||||
const nameHit = nb.name.toLowerCase().includes(q)
|
||||
const noteHit = (notebookNotes[nb.id] || []).some((n) => n.title.toLowerCase().includes(q))
|
||||
if (!nameHit && !noteHit) continue
|
||||
ids.add(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])
|
||||
|
||||
const currentNotebookId = searchParams.get('notebook')
|
||||
const currentNoteId = searchParams.get('openNote')
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentNotebookId) return
|
||||
@@ -866,6 +766,29 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
})
|
||||
}, [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 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) => ({
|
||||
id: n.id,
|
||||
title: getNoteDisplayTitle(n, t('notes.untitled')),
|
||||
isPinned: n.isPinned,
|
||||
autoGenerated: Boolean(n.autoGenerated),
|
||||
isRecent: isRecentlyCreated(n.createdAt, 48),
|
||||
}))
|
||||
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
|
||||
return {
|
||||
...prev,
|
||||
[nbId]: [{ id: detail.note.id, title, isPinned: detail.note.isPinned }, ...list],
|
||||
[nbId]: [{ id: detail.note.id, title }, ...list],
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -1027,14 +947,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const note = detail.note
|
||||
const title = getNoteDisplayTitle(note, t('notes.untitled'))
|
||||
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)) {
|
||||
const filtered = list.filter((n) => n.id !== note.id)
|
||||
if (filtered.length > 0) next[key] = filtered
|
||||
}
|
||||
if (note.notebookId) {
|
||||
next[note.notebookId] = [
|
||||
{ id: note.id, title, isPinned: note.isPinned },
|
||||
{ id: note.id, title },
|
||||
...(next[note.notebookId] || []),
|
||||
]
|
||||
}
|
||||
@@ -1070,14 +990,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
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 ──
|
||||
const [dropTarget, setDropTarget] = useState<string | 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,
|
||||
}}
|
||||
isActive={isActive}
|
||||
notes={notes}
|
||||
activeNoteId={currentNoteId}
|
||||
noteCount={notes.length}
|
||||
onCarnetClick={() => {
|
||||
if (currentNotebookId === notebook.id) {
|
||||
toggleExpand(notebook.id)
|
||||
if (children.length > 0) toggleExpand(notebook.id)
|
||||
} else {
|
||||
handleCarnetClick(notebook.id)
|
||||
}
|
||||
}}
|
||||
onNoteClick={handleNoteClick}
|
||||
onAddSubNotebook={() => {
|
||||
setCreateParentId(notebook.id)
|
||||
setIsCreateDialogOpen(true)
|
||||
@@ -1361,7 +1271,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</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 (
|
||||
<>
|
||||
@@ -2137,9 +2047,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
const mapped = notes.map((n: Note) => ({
|
||||
id: n.id,
|
||||
title: getNoteDisplayTitle(n, t('notes.untitled')),
|
||||
isPinned: n.isPinned,
|
||||
autoGenerated: Boolean(n.autoGenerated),
|
||||
isRecent: true,
|
||||
}))
|
||||
setNotebookNotes((prev) => ({ ...prev, [notebookId]: mapped }))
|
||||
} catch { /* ignore */ }
|
||||
|
||||
Reference in New Issue
Block a user