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,
|
BookMarked,
|
||||||
Bot,
|
Bot,
|
||||||
Inbox,
|
Inbox,
|
||||||
CalendarDays,
|
|
||||||
FlaskConical,
|
FlaskConical,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
Archive,
|
Archive,
|
||||||
@@ -68,6 +67,12 @@ import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainsto
|
|||||||
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
|
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
|
||||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
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({
|
function NoteLink({
|
||||||
title,
|
title,
|
||||||
isActive,
|
isActive,
|
||||||
@@ -635,6 +640,119 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
const [notebookSearchQuery, setNotebookSearchQuery] = useState('')
|
const [notebookSearchQuery, setNotebookSearchQuery] = useState('')
|
||||||
const [trashCount, setTrashCount] = useState(0)
|
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)
|
const [draggedId, setDraggedId] = useState<string | null>(null)
|
||||||
// Ref stable pour éviter les stale closures dans les handlers de drop
|
// Ref stable pour éviter les stale closures dans les handlers de drop
|
||||||
const draggedIdRef = useRef<string | null>(null)
|
const draggedIdRef = useRef<string | null>(null)
|
||||||
@@ -855,21 +973,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
router.push('/home?forceList=1')
|
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 handleNoteClick = (noteId: string, notebookId: string) => {
|
||||||
const params = new URLSearchParams(searchParams.toString())
|
const params = new URLSearchParams(searchParams.toString())
|
||||||
params.set('notebook', notebookId)
|
params.set('notebook', notebookId)
|
||||||
@@ -1180,11 +1283,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
|
ref={asideRef}
|
||||||
|
style={sidebarWidth !== null ? { width: sidebarWidth } : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
isImmersiveRoute && userCollapsed
|
isImmersiveRoute && userCollapsed
|
||||||
? 'fixed inset-y-0 start-0 z-[70]'
|
? 'fixed inset-y-0 start-0 z-[70]'
|
||||||
: 'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
|
: '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',
|
'transition-transform duration-300 ease-in-out',
|
||||||
isImmersiveRoute && userCollapsed
|
isImmersiveRoute && userCollapsed
|
||||||
? '-translate-x-full'
|
? '-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 ── */}
|
{/* ── 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 */}
|
{/* Top : Logo + navigation */}
|
||||||
<div className="flex flex-col items-center gap-[18px] w-full">
|
<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]">
|
<div className="flex-1 h-full flex flex-col overflow-hidden bg-[#FCFCFA] dark:bg-[#111111]">
|
||||||
|
|
||||||
{/* ── Scrollable content ── */}
|
{/* ── 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">
|
<AnimatePresence mode="wait">
|
||||||
{activeView === 'notebooks' ? (
|
{activeView === 'notebooks' ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="notebooks"
|
key="notebooks"
|
||||||
|
ref={notebooksContainerRef}
|
||||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="flex flex-col flex-1 min-h-0 overflow-hidden"
|
className="flex flex-col flex-1 min-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-4 pt-4 shrink-0">
|
<div data-sidebar-notebooks-top className="shrink-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="px-4 pt-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<BookMarked size={14} className="text-brand-accent" />
|
<BookMarked size={14} className="text-brand-accent" />
|
||||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
<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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar min-h-0 px-4 pb-4">
|
<div
|
||||||
<button
|
role="separator"
|
||||||
type="button"
|
aria-orientation="horizontal"
|
||||||
onClick={handleInboxClick}
|
aria-label={t('sidebar.resizeNotebooksPanel')}
|
||||||
className={cn('sidebar-inbox-item', isInboxActive && 'active')}
|
onPointerDown={handleNotebooksResizeStart}
|
||||||
>
|
className="flex shrink-0 h-3 mx-3 cursor-row-resize touch-none items-center justify-center group"
|
||||||
<div
|
>
|
||||||
className={cn(
|
<div className="h-1 w-10 rounded-full bg-border/70 group-hover:bg-brand-accent/60 group-active:bg-brand-accent transition-colors" />
|
||||||
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0',
|
</div>
|
||||||
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
|
||||||
|
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" />
|
<div className="my-3 h-px bg-border/40" />
|
||||||
|
|
||||||
{/* Zone de dépôt racine — toujours présente pour éviter tout décalage DOM */}
|
{/* 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>
|
||||||
|
|
||||||
</div>{/* fin colonne 2 */}
|
</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>
|
</aside>
|
||||||
|
|
||||||
<CreateNotebookDialog
|
<CreateNotebookDialog
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
"insightsPanelBody": "Semantic map of your notes: thematic clusters, bridge notes, and connection suggestions.",
|
"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.",
|
"revisionPanelBody": "Review flashcards with the SM-2 algorithm. Decks are generated from your notes.",
|
||||||
"backToNotebooks": "Back to notebooks",
|
"backToNotebooks": "Back to notebooks",
|
||||||
|
"resizeNotebooksPanel": "Resize notebooks panel",
|
||||||
|
"resizeSidebar": "Resize sidebar width",
|
||||||
"dailyNote": "Daily Note",
|
"dailyNote": "Daily Note",
|
||||||
"dailyNoteError": "Could not open today's note",
|
"dailyNoteError": "Could not open today's note",
|
||||||
"dailyNoteTooltip": "Open or create your personal daily journal note for today"
|
"dailyNoteTooltip": "Open or create your personal daily journal note for today"
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
"insightsPanelBody": "Cartographie sémantique de vos notes : clusters thématiques, notes-ponts et suggestions de connexion.",
|
"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.",
|
"revisionPanelBody": "Révisez vos flashcards avec l'algorithme SM-2. Les decks sont générés depuis vos notes.",
|
||||||
"backToNotebooks": "Retour aux carnets",
|
"backToNotebooks": "Retour aux carnets",
|
||||||
|
"resizeNotebooksPanel": "Redimensionner la zone des carnets",
|
||||||
|
"resizeSidebar": "Redimensionner la largeur de la barre latérale",
|
||||||
"dailyNote": "Note du jour",
|
"dailyNote": "Note du jour",
|
||||||
"dailyNoteError": "Impossible d'ouvrir la 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"
|
"dailyNoteTooltip": "Ouvrir ou créer la note personnelle pour le journal d'aujourd'hui"
|
||||||
|
|||||||
Reference in New Issue
Block a user