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
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m23s
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user