feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

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:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -30,11 +30,19 @@ import {
Sparkles,
Home,
Network,
Search,
GraduationCap,
Scissors,
FileText,
Folder,
FolderOpen,
} from 'lucide-react'
import { useSearchModal } from '@/context/search-modal-context'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { applyDocumentTheme } from '@/lib/apply-document-theme'
import { getAllNotes, getTrashCount } from '@/app/actions/notes'
import { NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook, Note } from '@/lib/types'
import { toast } from 'sonner'
@@ -50,20 +58,21 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { performSignOut } from '@/lib/auth-client'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
import { UsageMeter } from './usage-meter'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision'
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
function NoteLink({
title,
isActive,
isPinned,
onClick,
}: {
title: string
isActive: boolean
isPinned?: boolean
onClick: () => void
}) {
const { language } = useLanguage()
@@ -74,15 +83,18 @@ function NoteLink({
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={cn(
'w-full flex items-center gap-2 ps-12 pe-4 py-2 text-[12px] transition-colors rounded-lg text-start',
isActive ? 'bg-white/50 text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-white/30'
'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'
: 'text-muted-foreground hover:text-foreground hover:bg-white/30 dark:hover:bg-white/5',
)}
>
<div className={cn(
'w-1.5 h-1.5 rounded-full shrink-0',
isActive ? 'bg-foreground' : 'bg-transparent border border-muted-foreground/30'
)} />
<span className="break-words line-clamp-2 leading-tight">{title}</span>
<FileText
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-muted-foreground/70')}
/>
<span className="truncate flex-1">{title}</span>
{isPinned && <Pin size={10} className="text-amber-500 fill-amber-500 shrink-0" />}
</motion.button>
)
}
@@ -187,7 +199,7 @@ function SidebarCarnetItem({
}: {
carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
isActive: boolean
notes: { id: string; title: string }[]
notes: { id: string; title: string; isPinned?: boolean }[]
activeNoteId: string | null
onCarnetClick: () => void
onNoteClick: (noteId: string, carnetId: string) => void
@@ -207,7 +219,7 @@ function SidebarCarnetItem({
}) {
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const hasChildren = hasChildNotebooks || React.Children.count(children) > 0
const hasChildren = hasChildNotebooks || React.Children.count(children) > 0 || notes.length > 0
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
// Close context menu on outside click
@@ -274,12 +286,10 @@ function SidebarCarnetItem({
/>
)}
<div className={cn(
'w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold border shrink-0 transition-all',
isActive
? 'bg-brand-accent text-white border-brand-accent'
: 'bg-white/60 dark:bg-white/5 text-ink dark:text-foreground border-border'
'w-5 h-5 flex items-center justify-center shrink-0 transition-colors',
isActive ? 'text-brand-accent' : 'text-muted-foreground/80',
)}>
{carnet.initial}
{isExpanded ? <FolderOpen size={13} /> : <Folder size={13} />}
</div>
<div className="flex-1 text-start flex items-center gap-2 min-w-0">
@@ -395,17 +405,18 @@ function SidebarCarnetItem({
<div className="space-y-0.5 py-1">
{children}
{isActive && notes.map(note => (
{isExpanded && notes.map(note => (
<NoteLink
key={note.id}
title={note.title}
isPinned={note.isPinned}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id, carnet.id)}
/>
))}
{isActive && notes.length === 0 && !hasChildren && (
<p className="ps-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
{t('common.noResults')}
{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>
@@ -423,8 +434,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const router = useRouter()
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const { notebooks, trashNotebook, updateNotebookOrderOptimistic, moveNotebookToParent } = useNotebooks()
const { refreshKey } = useNoteRefresh()
const { notebooks, trashNotebook, updateNotebookOrderOptimistic, moveNotebookToParent, refreshNotebooks } = useNotebooks()
const { open: openSearch } = useSearchModal()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [createParentId, setCreateParentId] = useState<string | null>(null)
const [renamingNotebook, setRenamingNotebook] = useState<Notebook | null>(null)
@@ -460,7 +471,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 }[]>>({})
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string; isPinned?: boolean }[]>>({})
const [activeView, setActiveView] = useState<NavigationView>('notebooks')
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
const [showSortMenu, setShowSortMenu] = useState(false)
@@ -487,6 +498,20 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const currentNotebookId = searchParams.get('notebook')
const currentNoteId = searchParams.get('openNote')
useEffect(() => {
if (!currentNotebookId) return
setExpandedIds(prev => {
const next = new Set(prev)
let id: string | null | undefined = currentNotebookId
while (id) {
next.add(id)
const nb = notebooks.find(n => n.id === id)
id = nb?.parentId ?? null
}
return next
})
}, [currentNotebookId, notebooks])
const isInboxActive =
pathname === '/home' &&
!searchParams.get('notebook') &&
@@ -523,7 +548,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
let cancelled = false
getTrashCount().then(count => { if (!cancelled) setTrashCount(count) })
return () => { cancelled = true }
}, [refreshKey])
}, [])
const notebookIdsKey = useMemo(() => notebooks.map(nb => nb.id).sort().join(','), [notebooks])
@@ -537,6 +562,7 @@ 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,
}))
return [nb.id, mapped] as const
})
@@ -546,8 +572,57 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
}
load()
return () => { cancelled = true }
// refreshKey: reload note titles whenever any note is saved/created/deleted
}, [notebookIdsKey, refreshKey, t])
}, [notebookIdsKey, t])
useEffect(() => {
const onNoteChange = (e: Event) => {
const detail = (e as CustomEvent<NoteChangeEvent>).detail
if (detail.type === 'deleted') {
setNotebookNotes((prev) => {
const next = { ...prev }
for (const key of Object.keys(next)) {
next[key] = next[key].filter((n) => n.id !== detail.noteId)
}
return next
})
setTrashCount((count) => count + 1)
return
}
if (detail.type === 'created' && detail.note.notebookId) {
const nbId = detail.note.notebookId
const title = getNoteDisplayTitle(detail.note, t('notes.untitled'))
setNotebookNotes((prev) => {
const list = prev[nbId] || []
if (list.some((n) => n.id === detail.note.id)) return prev
return {
...prev,
[nbId]: [{ id: detail.note.id, title, isPinned: detail.note.isPinned }, ...list],
}
})
return
}
if (detail.type === 'updated') {
const note = detail.note
const title = getNoteDisplayTitle(note, t('notes.untitled'))
setNotebookNotes((prev) => {
const next: Record<string, { id: string; title: string; isPinned?: boolean }[]> = {}
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 },
...(next[note.notebookId] || []),
]
}
return next
})
}
}
window.addEventListener(NOTE_CHANGE_EVENT, onNoteChange)
return () => window.removeEventListener(NOTE_CHANGE_EVENT, onNoteChange)
}, [t])
const handleCarnetClick = (notebookId: string) => {
const params = new URLSearchParams()
@@ -684,13 +759,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
if (!res.ok) throw new Error('Rename failed')
setRenamingNotebook(null)
setRenameValue('')
router.refresh()
await refreshNotebooks()
} catch (err) {
console.error('Rename failed:', err)
} finally {
setIsRenaming(false)
}
}, [renamingNotebook, renameValue, router])
}, [renamingNotebook, renameValue, refreshNotebooks])
const getDescendantIds = useCallback((notebookId: string): string[] => {
const ids: string[] = []
@@ -834,26 +909,26 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className={cn(
// Mobile: fixed overlay, slide in/out
'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
'h-full min-h-0 w-72 lg:w-80 shrink-0 flex flex-col',
'h-full min-h-0 w-72 lg:w-80 shrink-0 flex flex-row overflow-hidden',
'transition-transform duration-300 ease-in-out',
isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full md:translate-x-0',
'border-e border-border/40 bg-white/95 md:bg-white/30 backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#151515] dark:backdrop-blur-none',
className
)}
>
{/* ── Top: Logo + Icons + View Toggle ── */}
<div className="p-6 mb-8 space-y-4">
<div className="flex items-center justify-between">
{/* ── Column 1 : Rail d'icônes (54px) — inspiré du prototype ── */}
<div className="w-[54px] border-e border-border/40 bg-[#FAF9F5] dark:bg-[#0E0E0E] flex flex-col items-center justify-between py-4 shrink-0 select-none">
{/* Top : Logo + navigation */}
<div className="flex flex-col items-center gap-3 w-full">
{/* Logo avec dropdown profil */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex items-center gap-2 group/logo cursor-pointer">
<div className="w-10 h-10 bg-brand-accent flex items-center justify-center rounded-xl shadow-lg shadow-brand-accent/10 rotate-3 group-hover/logo:rotate-0 transition-all duration-500">
<span className="text-white font-serif text-xl font-bold">M</span>
</div>
<span className="text-lg font-serif font-bold tracking-tight text-ink dark:text-paper">Memento</span>
<div className="w-9 h-9 bg-brand-accent hover:rotate-6 active:scale-95 flex items-center justify-center rounded-xl shadow-md transition-all cursor-pointer mb-1">
<span className="text-white font-serif font-bold text-sm">M</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-52 bg-popover border-border">
<DropdownMenuContent align="start" className="w-52 bg-popover border-border ms-2">
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="flex items-center gap-2 cursor-pointer">
<User className="h-4 w-4" />
@@ -879,70 +954,129 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-1.5">
<Link
href="/settings"
className={cn(
'p-1.5 transition-all rounded-lg border flex items-center justify-center',
pathname.startsWith('/settings')
? 'bg-brand-accent text-white border-brand-accent shadow-lg shadow-brand-accent/20'
: 'text-muted-ink hover:text-ink hover:bg-white/50 dark:hover:bg-white/10 border-transparent hover:border-border'
)}
title={t('nav.settings')}
>
<Settings size={14} />
</Link>
<button
onClick={() => { router.push('/home') }}
className="p-1.5 text-muted-ink hover:text-ink transition-all hover:bg-white/50 dark:hover:bg-white/10 rounded-lg border border-transparent hover:border-border"
title={t('nav.home')}
>
<Home size={14} />
</button>
<button
onClick={toggleTheme}
className="p-1.5 text-muted-ink hover:text-ink transition-all hover:bg-white/50 dark:hover:bg-white/10 rounded-lg border border-transparent hover:border-border"
>
{isDark ? <Sun size={14} /> : <Moon size={14} />}
</button>
<NotificationPanel />
{/* Boutons de navigation principaux */}
<div className="flex flex-col gap-1.5 w-full px-1.5">
{([
{ id: 'notebooks', icon: BookOpen, label: t('nav.notebooks'), onClick: () => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }, isActive: activeView === 'notebooks' && !pathname.startsWith('/settings') },
{ id: 'graph', icon: Network, label: 'Vue graphe', onClick: () => router.push('/graph'), isActive: pathname === '/graph' },
{ id: 'revision', icon: GraduationCap, label: 'Révisions', onClick: () => setActiveView('revision'), isActive: activeView === 'revision' },
{ id: 'agents', icon: Bot, label: t('agents.intelligenceOS') || 'Intelligence IA', onClick: () => { setActiveView('agents'); router.push('/agents') }, isActive: activeView === 'agents' || (pathname.startsWith('/agents') && activeView !== 'notebooks') },
{ id: 'reminders', icon: Bell, label: t('sidebar.reminders'), onClick: () => setActiveView('reminders'), isActive: activeView === 'reminders' },
] as { id: string; icon: React.FC<{ size?: number }>; label: string; onClick: () => void; isActive: boolean }[]).map(item => (
<button
key={item.id}
onClick={item.onClick}
className={cn(
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
item.isActive
? 'bg-brand-accent/10 text-brand-accent border border-brand-accent/25'
: 'text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04]'
)}
>
{item.isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-brand-accent rounded-r-full" />
)}
<item.icon size={16} />
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{item.label}
</span>
</button>
))}
</div>
</div>
<div className="flex bg-white/50 dark:bg-white/10 p-1 rounded-xl border border-border dark:border-white/10">
<button
onClick={() => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }}
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'notebooks' ? 'bg-brand-accent text-white shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
title={t('nav.notebooks')}
{/* Bottom : utilitaires */}
<div className="flex flex-col gap-1.5 w-full px-1.5 items-center">
<NotificationPanel />
<Link
href="/trash"
className={cn(
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
pathname === '/trash'
? 'bg-rose-500/10 text-rose-500 border border-rose-500/25'
: 'text-concrete hover:text-rose-500 hover:bg-rose-500/5'
)}
>
<BookOpen size={14} />
{pathname === '/trash' && <div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-rose-500 rounded-r-full" />}
<Trash2 size={16} />
{trashCount > 0 && (
<span className="absolute top-1.5 right-1.5 w-1.5 h-1.5 bg-rose-500 rounded-full border-2 border-[#FAF9F5] dark:border-[#0E0E0E]" />
)}
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{t('sidebar.trash')}
</span>
</Link>
<Link
href="/home?shared=1&forceList=1"
className={cn(
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
searchParams.get('shared') === '1' && pathname === '/home'
? 'bg-sky-500/10 text-sky-500 border border-sky-500/25'
: 'text-concrete hover:text-sky-500 hover:bg-sky-500/5'
)}
>
<Users size={16} />
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{t('sidebar.sharedWithMe')}
</span>
</Link>
<button
onClick={openSearch}
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-all relative group"
title="Ctrl+K"
>
<Search size={15} />
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
Recherche (Ctrl+K)
</span>
</button>
<button
onClick={() => setActiveView('reminders')}
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'reminders' ? 'bg-brand-accent text-white shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
title={t('sidebar.reminders')}
onClick={toggleTheme}
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-all relative group"
>
<Clock size={14} />
{isDark ? <Sun size={15} /> : <Moon size={15} />}
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{isDark ? 'Mode clair' : 'Mode sombre'}
</span>
</button>
<button
onClick={() => { setActiveView('agents'); router.push('/agents') }}
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'agents' ? 'bg-brand-accent text-white shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
title={t('nav.agents')}
<Link
href="/settings"
className={cn(
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
pathname.startsWith('/settings')
? 'bg-brand-accent/10 text-brand-accent border border-brand-accent/25'
: 'text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04]'
)}
>
<Bot size={14} />
</button>
{pathname.startsWith('/settings') && <div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-brand-accent rounded-r-full" />}
<Settings size={15} />
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{t('nav.settings')}
</span>
</Link>
<button
onClick={() => { setActiveView('brainstorms'); router.push('/brainstorm') }}
className={cn('flex-1 flex items-center justify-center py-1.5 rounded-lg transition-all', activeView === 'brainstorms' ? 'bg-brand-accent text-white shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-white/50')}
title={t('brainstorm.sessions')}
onClick={() => performSignOut('/login')}
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-red-500 hover:bg-rose-500/5 transition-all relative group"
>
<Sparkles size={14} />
<LogOut size={14} />
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
{t('sidebar.signOut')}
</span>
</button>
</div>
</div>
{/* ── Column 2 : Panneau de contenu dynamique ── */}
<div className="flex-1 h-full flex flex-col overflow-hidden bg-[#FCFCFA] dark:bg-[#111111]">
{/* ── Scrollable content ── */}
<div className="flex-1 overflow-y-auto space-y-6 -mx-2 px-2 custom-scrollbar pb-4">
<div className="flex-1 overflow-y-auto space-y-6 -mx-0 custom-scrollbar pb-4">
<AnimatePresence mode="wait">
{activeView === 'notebooks' ? (
@@ -1123,71 +1257,37 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<SidebarBrainstorms />
</motion.div>
)}
{/* ── Vue Révisions (placeholder en attendant US-FLASHCARDS) ── */}
{activeView === 'revision' && (
<motion.div
key="revision"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.2 }}
className="px-4"
>
<div className="flex items-center gap-1.5 mb-4">
<GraduationCap size={13} className="text-brand-accent" />
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">Révisions</p>
</div>
<div className="flex flex-col items-center justify-center text-center p-6 border border-dashed border-border/50 rounded-2xl bg-paper/20 space-y-3">
<GraduationCap size={24} className="text-concrete/40" />
<p className="text-[11px] font-medium text-concrete/70">Flashcards bientôt disponibles</p>
<p className="text-[10px] text-concrete/50">Les decks de révision IA (SM-2) arrivent dans la prochaine itération.</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* ── Footer ── */}
<div className="pt-4 border-t border-border/40 mt-auto pb-4 space-y-4">
{/* ── Usage meter en bas du panneau ── */}
<div className="border-t border-border/20 px-3 py-3 mt-auto">
<UsageMeter />
<div className="px-2 space-y-0.5">
<Link
href="/home?shared=1&forceList=1"
className={cn(
'w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl',
searchParams.get('shared') === '1' && pathname === '/home'
? 'bg-accent/5 text-accent'
: 'text-muted-ink hover:text-ink hover:bg-black/5'
)}
>
<Users size={14} className={searchParams.get('shared') === '1' && pathname === '/home' ? 'text-accent' : 'text-muted-ink group-hover:text-ink'} />
<span>{t('sidebar.sharedWithMe')}</span>
</Link>
<Link
href="/archive"
className="w-full flex items-center gap-3 px-3 py-2 text-[12px] text-muted-ink hover:text-ink hover:bg-black/5 transition-all font-medium group rounded-xl"
>
<Archive size={14} className="text-muted-ink group-hover:text-ink" />
<span>{t('sidebar.archive')}</span>
</Link>
<Link
href="/trash"
className={cn(
'w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl',
pathname === '/trash'
? 'bg-rose-50 text-rose-500'
: 'text-muted-ink hover:text-rose-500 hover:bg-rose-50/50'
)}
>
<Trash2 size={14} className={pathname === '/trash' ? 'text-rose-500' : 'text-muted-ink group-hover:text-rose-500'} />
<span>{t('sidebar.trash')}</span>
{trashCount > 0 && (
<span className="ms-auto w-1.5 h-1.5 rounded-full bg-rose-400" />
)}
</Link>
{/* ── Intelligence section ── */}
<div className="pt-3 border-t border-border/20 mx-2 mt-1 space-y-0.5">
<p className="text-[9px] font-bold text-muted-ink tracking-[0.2em] uppercase px-1 mb-1 opacity-60">Intelligence</p>
<Link
href="/graph"
className={cn(
'w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl',
pathname === '/graph'
? 'bg-indigo-500/10 text-indigo-500'
: 'text-muted-ink hover:text-indigo-500 hover:bg-indigo-500/5'
)}
>
<Network
size={14}
className={pathname === '/graph' ? 'text-indigo-500' : 'text-muted-ink group-hover:text-indigo-500'}
/>
<span className="flex-1">Vue en graphe</span>
</Link>
</div>
</div>
</div>
</div>{/* fin colonne 2 */}
</aside>
<CreateNotebookDialog