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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user