feat: add right-click context menu with freeze/unfreeze notebook state in sidebar
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m23s

This commit is contained in:
Antigravity
2026-05-10 19:04:31 +00:00
parent 085a2246f6
commit 892a697cb9

View File

@@ -25,6 +25,8 @@ import {
Clock,
Moon,
Sun,
Pin,
PinOff,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -88,6 +90,8 @@ function SidebarCarnetItem({
onAddSubNotebook,
onRename,
onDelete,
onTogglePin,
isPinned,
children,
isDragging,
dragHandleProps,
@@ -104,6 +108,8 @@ function SidebarCarnetItem({
onAddSubNotebook: () => void
onRename: () => void
onDelete: () => void
onTogglePin: () => void
isPinned: boolean
children?: React.ReactNode
isDragging?: boolean
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
@@ -113,12 +119,26 @@ function SidebarCarnetItem({
}) {
const { t } = useLanguage()
const hasChildren = React.Children.count(children) > 0
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
// Close context menu on outside click
useEffect(() => {
if (!contextMenu) return
const handler = () => setContextMenu(null)
window.addEventListener('click', handler)
return () => window.removeEventListener('click', handler)
}, [contextMenu])
return (
<div className={cn('transition-opacity', isDragging && 'opacity-40')}>
<div
className="flex items-center group relative h-10"
style={{ paddingLeft: `${level * 16}px` }}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
setContextMenu({ x: e.clientX, y: e.clientY })
}}
>
{level > 0 && (
<div className="absolute left-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
@@ -184,6 +204,11 @@ function SidebarCarnetItem({
</div>
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
{isPinned && (
<span className="text-blueprint" title="Carnet figé">
<Pin size={9} className="opacity-70" />
</span>
)}
<button
onClick={(e) => { e.stopPropagation(); onAddSubNotebook() }}
className="p-1 hover:bg-ink/10 rounded-md transition-all text-concrete hover:text-ink"
@@ -216,6 +241,54 @@ function SidebarCarnetItem({
</div>
</div>
{/* Right-click context menu */}
<AnimatePresence>
{contextMenu && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.1 }}
style={{ position: 'fixed', top: contextMenu.y, left: contextMenu.x, zIndex: 9999 }}
className="bg-card border border-border rounded-xl shadow-xl py-1.5 min-w-[180px] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => { onTogglePin(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
{isPinned
? <><PinOff size={13} className="text-blueprint" /><span>Défiger l'état du carnet</span></>
: <><Pin size={13} className="text-blueprint" /><span>Figer l'état du carnet</span></>
}
</button>
<div className="mx-3 my-1 border-t border-border/50" />
<button
onClick={() => { onAddSubNotebook(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Plus size={13} className="text-concrete" />
<span>Nouveau sous-carnet</span>
</button>
<button
onClick={() => { onRename(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Pencil size={13} className="text-concrete" />
<span>Renommer</span>
</button>
<div className="mx-3 my-1 border-t border-border/50" />
<button
onClick={() => { onDelete(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
>
<Trash2 size={13} />
<span>Supprimer</span>
</button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
@@ -280,6 +353,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const [isDeleting, setIsDeleting] = useState(false)
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 [activeView, setActiveView] = useState<NavigationView>('notebooks')
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
@@ -460,6 +534,29 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
})
}, [])
// Load pinned notebooks from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem('momento-pinned-notebooks')
if (stored) setPinnedIds(new Set(JSON.parse(stored)))
} catch {}
}, [])
const togglePin = useCallback((id: string) => {
setPinnedIds(prev => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
// Ensure it's also expanded when pinned
setExpandedIds(e => { const ne = new Set(e); ne.add(id); return ne })
}
try { localStorage.setItem('momento-pinned-notebooks', JSON.stringify([...next])) } catch {}
return next
})
}, [])
const handleStartRename = useCallback((notebook: Notebook) => {
setRenamingNotebook(notebook)
setRenameValue(notebook.name)
@@ -524,7 +621,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const hasActiveDescendant = children.some(c =>
currentNotebookId === c.id || (childNotebooks.get(c.id) || []).some(gc => currentNotebookId === gc.id)
)
const isExpanded = expandedIds.has(notebook.id) || hasActiveDescendant
// A notebook stays expanded if: manually expanded, has active descendant, OR is pinned
const isExpanded = expandedIds.has(notebook.id) || hasActiveDescendant || pinnedIds.has(notebook.id)
return (
<motion.div
@@ -562,6 +660,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
}}
onRename={() => handleStartRename(notebook)}
onDelete={() => setDeletingNotebook(notebook)}
onTogglePin={() => togglePin(notebook.id)}
isPinned={pinnedIds.has(notebook.id)}
isDragging={isDragging}
level={level}
isExpanded={isExpanded}