feat(sidebar): redimensionnement largeur et zone carnets, retrait note du jour
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user