feat(sidebar): redimensionnement largeur et zone carnets, retrait note du jour
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m5s
CI / Deploy production (on server) (push) Successful in 23s

Ajoute une poignée drag pour la largeur de la sidebar (persistée) et sépare inbox/arbre carnets avec slider vertical ; supprime le bouton Note du jour devenu inutile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-07-14 11:35:03 +00:00
parent 6e13b6d207
commit d38a99586b
3 changed files with 196 additions and 60 deletions

View File

@@ -12,7 +12,6 @@ import {
BookMarked,
Bot,
Inbox,
CalendarDays,
FlaskConical,
ArrowUpDown,
Archive,
@@ -68,6 +67,12 @@ import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainsto
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
const NOTEBOOKS_PANEL_HEIGHT_KEY = 'memento-sidebar-notebooks-height'
const NOTEBOOKS_PANEL_MIN_PX = 120
const SIDEBAR_WIDTH_KEY = 'memento-sidebar-width'
const SIDEBAR_WIDTH_MIN_PX = 280
const SIDEBAR_WIDTH_MAX_PX = 560
function NoteLink({
title,
isActive,
@@ -635,6 +640,119 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const [notebookSearchQuery, setNotebookSearchQuery] = useState('')
const [trashCount, setTrashCount] = useState(0)
const notebooksContainerRef = useRef<HTMLDivElement>(null)
const notebooksPanelRef = useRef<HTMLDivElement>(null)
const notebooksPanelHeightRef = useRef<number | null>(null)
const [notebooksPanelHeight, setNotebooksPanelHeight] = useState<number | null>(null)
const asideRef = useRef<HTMLElement>(null)
const sidebarWidthRef = useRef<number | null>(null)
const [sidebarWidth, setSidebarWidth] = useState<number | null>(null)
useEffect(() => {
try {
const stored = localStorage.getItem(NOTEBOOKS_PANEL_HEIGHT_KEY)
if (stored) {
const n = parseInt(stored, 10)
if (!Number.isNaN(n) && n >= NOTEBOOKS_PANEL_MIN_PX) {
setNotebooksPanelHeight(n)
notebooksPanelHeightRef.current = n
}
}
const storedW = localStorage.getItem(SIDEBAR_WIDTH_KEY)
if (storedW) {
const w = parseInt(storedW, 10)
if (!Number.isNaN(w) && w >= SIDEBAR_WIDTH_MIN_PX && w <= SIDEBAR_WIDTH_MAX_PX) {
setSidebarWidth(w)
sidebarWidthRef.current = w
}
}
} catch {
/* ignore */
}
}, [])
const handleNotebooksResizeStart = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
const panel = notebooksPanelRef.current
const container = notebooksContainerRef.current
if (!panel || !container) return
const target = e.currentTarget
target.setPointerCapture(e.pointerId)
const startY = e.clientY
const startH = panel.getBoundingClientRect().height
const topBlock = container.querySelector('[data-sidebar-notebooks-top]') as HTMLElement | null
const handleH = 12
const containerH = container.getBoundingClientRect().height
const maxH = Math.max(
NOTEBOOKS_PANEL_MIN_PX,
containerH - (topBlock?.offsetHeight ?? 160) - handleH,
)
const onMove = (ev: PointerEvent) => {
const delta = ev.clientY - startY
const newH = Math.min(maxH, Math.max(NOTEBOOKS_PANEL_MIN_PX, startH + delta))
notebooksPanelHeightRef.current = newH
setNotebooksPanelHeight(newH)
}
const onUp = (ev: PointerEvent) => {
target.releasePointerCapture(ev.pointerId)
document.removeEventListener('pointermove', onMove)
document.removeEventListener('pointerup', onUp)
const h = notebooksPanelHeightRef.current
if (h !== null) {
try {
localStorage.setItem(NOTEBOOKS_PANEL_HEIGHT_KEY, String(h))
} catch {
/* ignore */
}
}
}
document.addEventListener('pointermove', onMove)
document.addEventListener('pointerup', onUp)
}, [])
const handleSidebarWidthResizeStart = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
const aside = asideRef.current
if (!aside) return
const target = e.currentTarget
target.setPointerCapture(e.pointerId)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
const startX = e.clientX
const startW = aside.getBoundingClientRect().width
const onMove = (ev: PointerEvent) => {
const delta = isRtl ? startX - ev.clientX : ev.clientX - startX
const newW = Math.min(SIDEBAR_WIDTH_MAX_PX, Math.max(SIDEBAR_WIDTH_MIN_PX, startW + delta))
sidebarWidthRef.current = newW
setSidebarWidth(newW)
}
const onUp = (ev: PointerEvent) => {
target.releasePointerCapture(ev.pointerId)
document.body.style.cursor = ''
document.body.style.userSelect = ''
document.removeEventListener('pointermove', onMove)
document.removeEventListener('pointerup', onUp)
const w = sidebarWidthRef.current
if (w !== null) {
try {
localStorage.setItem(SIDEBAR_WIDTH_KEY, String(w))
} catch {
/* ignore */
}
}
}
document.addEventListener('pointermove', onMove)
document.addEventListener('pointerup', onUp)
}, [isRtl])
const [draggedId, setDraggedId] = useState<string | null>(null)
// Ref stable pour éviter les stale closures dans les handlers de drop
const draggedIdRef = useRef<string | null>(null)
@@ -855,21 +973,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
router.push('/home?forceList=1')
}
const handleDailyNoteClick = async () => {
try {
const res = await fetch('/api/notes/daily')
const data = await res.json()
if (data.success && data.note) {
const params = new URLSearchParams()
if (data.note.notebookId) params.set('notebook', data.note.notebookId)
params.set('openNote', data.note.id)
router.push(`/home?${params.toString()}`)
}
} catch {
toast.error(t('sidebar.dailyNoteError') || 'Impossible d\'ouvrir la note du jour')
}
}
const handleNoteClick = (noteId: string, notebookId: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set('notebook', notebookId)
@@ -1180,11 +1283,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</AnimatePresence>
<aside
ref={asideRef}
style={sidebarWidth !== null ? { width: sidebarWidth } : undefined}
className={cn(
isImmersiveRoute && userCollapsed
? 'fixed inset-y-0 start-0 z-[70]'
: 'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
'h-full min-h-0 w-80 md:w-[26rem] 2xl:w-[30rem] shrink-0 flex flex-row overflow-hidden self-stretch',
sidebarWidth === null && 'w-80 md:w-[26rem] 2xl:w-[30rem]',
'h-full min-h-0 shrink-0 flex flex-row self-stretch relative overflow-visible',
'transition-transform duration-300 ease-in-out',
isImmersiveRoute && userCollapsed
? '-translate-x-full'
@@ -1194,7 +1300,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
)}
>
{/* ── 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-5 shrink-0 select-none">
<div className="w-[54px] border-e border-border/40 bg-[#FAF9F5] dark:bg-[#0E0E0E] flex flex-col items-center justify-between py-5 shrink-0 select-none overflow-hidden">
{/* Top : Logo + navigation */}
<div className="flex flex-col items-center gap-[18px] w-full">
@@ -1401,20 +1507,29 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<div className="flex-1 h-full flex flex-col overflow-hidden bg-[#FCFCFA] dark:bg-[#111111]">
{/* ── Scrollable content ── */}
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto space-y-6 -mx-0 custom-scrollbar pb-4">
<div
className={cn(
'flex-1 flex flex-col min-h-0 -mx-0 pb-4',
activeView === 'notebooks'
? 'overflow-hidden'
: 'overflow-y-auto custom-scrollbar space-y-6',
)}
>
<AnimatePresence mode="wait">
{activeView === 'notebooks' ? (
<motion.div
key="notebooks"
ref={notebooksContainerRef}
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
className="flex flex-col flex-1 min-h-0 overflow-hidden"
>
<div className="px-4 pt-4 shrink-0">
<div className="flex items-center justify-between mb-4">
<div data-sidebar-notebooks-top className="shrink-0">
<div className="px-4 pt-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-1.5">
<BookMarked size={14} className="text-brand-accent" />
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
@@ -1506,48 +1621,54 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</button>
)}
</div>
</div>
<div className="px-4 pb-2">
<button
type="button"
onClick={handleInboxClick}
className={cn('sidebar-inbox-item', isInboxActive && 'active')}
>
<div
className={cn(
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0',
isInboxActive
? 'bg-brand-accent text-white border-brand-accent'
: 'bg-paper dark:bg-white/5 text-muted-ink border-border group-hover:border-brand-accent/20',
)}
>
<Inbox size={14} />
</div>
<span
className={cn(
'text-[13px] font-medium truncate',
isInboxActive ? 'text-ink' : 'text-muted-ink',
)}
>
{t('sidebar.inbox')}
</span>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar min-h-0 px-4 pb-4">
<button
type="button"
onClick={handleInboxClick}
className={cn('sidebar-inbox-item', isInboxActive && 'active')}
>
<div
className={cn(
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0',
isInboxActive
? 'bg-brand-accent text-white border-brand-accent'
: 'bg-paper dark:bg-white/5 text-muted-ink border-border group-hover:border-brand-accent/20',
)}
>
<Inbox size={14} />
</div>
<span
className={cn(
'text-[13px] font-medium truncate',
isInboxActive ? 'text-ink' : 'text-muted-ink',
)}
>
{t('sidebar.inbox')}
</span>
</button>
<button
type="button"
onClick={handleDailyNoteClick}
className="sidebar-inbox-item"
title={t('sidebar.dailyNoteTooltip') || "Ouvrir ou créer la note personnelle pour le journal d'aujourd'hui"}
>
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0 bg-paper dark:bg-white/5 text-muted-ink border-border">
<CalendarDays size={14} />
</div>
<span className="text-[13px] font-medium truncate text-muted-ink">
{t('sidebar.dailyNote') || 'Note du jour'}
</span>
</button>
<div
role="separator"
aria-orientation="horizontal"
aria-label={t('sidebar.resizeNotebooksPanel')}
onPointerDown={handleNotebooksResizeStart}
className="flex shrink-0 h-3 mx-3 cursor-row-resize touch-none items-center justify-center group"
>
<div className="h-1 w-10 rounded-full bg-border/70 group-hover:bg-brand-accent/60 group-active:bg-brand-accent transition-colors" />
</div>
<div
ref={notebooksPanelRef}
className={cn(
'overflow-y-auto custom-scrollbar min-h-0 px-4 pb-4 shrink-0',
notebooksPanelHeight === null && 'flex-1',
)}
style={notebooksPanelHeight !== null ? { height: notebooksPanelHeight } : undefined}
>
<div className="my-3 h-px bg-border/40" />
{/* Zone de dépôt racine — toujours présente pour éviter tout décalage DOM */}
@@ -1722,6 +1843,17 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</div>
</div>{/* fin colonne 2 */}
{/* Poignée redimensionnement largeur sidebar (bord vers le contenu) */}
<div
role="separator"
aria-orientation="vertical"
aria-label={t('sidebar.resizeSidebar')}
onPointerDown={handleSidebarWidthResizeStart}
className="hidden md:flex absolute inset-y-0 end-0 translate-x-1/2 w-4 cursor-col-resize touch-none z-30 items-center justify-center group"
>
<div className="h-10 w-1 rounded-full bg-border/80 group-hover:bg-brand-accent/70 group-active:bg-brand-accent transition-colors shadow-sm" />
</div>
</aside>
<CreateNotebookDialog

View File

@@ -78,6 +78,8 @@
"insightsPanelBody": "Semantic map of your notes: thematic clusters, bridge notes, and connection suggestions.",
"revisionPanelBody": "Review flashcards with the SM-2 algorithm. Decks are generated from your notes.",
"backToNotebooks": "Back to notebooks",
"resizeNotebooksPanel": "Resize notebooks panel",
"resizeSidebar": "Resize sidebar width",
"dailyNote": "Daily Note",
"dailyNoteError": "Could not open today's note",
"dailyNoteTooltip": "Open or create your personal daily journal note for today"

View File

@@ -78,6 +78,8 @@
"insightsPanelBody": "Cartographie sémantique de vos notes : clusters thématiques, notes-ponts et suggestions de connexion.",
"revisionPanelBody": "Révisez vos flashcards avec l'algorithme SM-2. Les decks sont générés depuis vos notes.",
"backToNotebooks": "Retour aux carnets",
"resizeNotebooksPanel": "Redimensionner la zone des carnets",
"resizeSidebar": "Redimensionner la largeur de la barre latérale",
"dailyNote": "Note du jour",
"dailyNoteError": "Impossible d'ouvrir la note du jour",
"dailyNoteTooltip": "Ouvrir ou créer la note personnelle pour le journal d'aujourd'hui"