feat(insights): peek panel au clic sur un nœud — slide droite, contenu note
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

- handleNoteClick fetch la note + ouvre peek panel au lieu de naviguer
- Panel fixed right (left en RTL), spring animation (stiffness 340 damping 34)
- Header avec titre + boutons Maximize2 (ouvrir note complète) et X (fermer)
- Content rendu en read-only via dangerouslySetInnerHTML
- prefers-reduced-motion: animation désactivée
- responsive: w-full sur mobile, 50vw/640px max sur desktop
This commit is contained in:
Antigravity
2026-07-04 20:29:21 +00:00
parent f9d79365f3
commit d48312dfc2
6 changed files with 98 additions and 20 deletions

View File

@@ -25,6 +25,9 @@ import {
} from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import { getNoteById } from '@/app/actions/notes'
import type { Note as NoteFull } from '@/lib/types'
import { X, Maximize2 } from 'lucide-react'
const NetworkGraph = dynamic(
() => import('@/components/network-graph').then(m => ({ default: m.NetworkGraph })),
@@ -99,6 +102,8 @@ export default function InsightsPage() {
const [viewMode, setViewMode] = useState<'graph' | 'dashboard'>('dashboard')
const [graphMode, setGraphMode] = useState<'visual' | 'list'>('visual')
const [lastSyncTime, setLastSyncTime] = useState<string>('')
const [peekNote, setPeekNote] = useState<NoteFull | null>(null)
const [peekLoading, setPeekLoading] = useState(false)
const prefersReducedMotion = useReducedMotion()
useEffect(() => {
@@ -257,10 +262,28 @@ export default function InsightsPage() {
}
}
const handleNoteClick = (noteId: string) => {
router.push(`/home?openNote=${noteId}`)
const handleNoteClick = async (noteId: string) => {
setPeekLoading(true)
try {
const note = await getNoteById(noteId)
if (note) setPeekNote(note)
} catch {
toast.error(t('general.error'))
} finally {
setPeekLoading(false)
}
}
const closePeek = () => setPeekNote(null)
const openPeekFully = () => {
if (!peekNote) return
router.push(`/home?openNote=${peekNote.id}`)
setPeekNote(null)
}
const isRtl = locale === 'fa' || locale === 'ar'
const motionConfig = prefersReducedMotion
? { initial: false as const, animate: { opacity: 1, y: 0 }, transition: { duration: 0 } }
: {}
@@ -868,6 +891,60 @@ export default function InsightsPage() {
</div>
)}
{/* ── Peek panel (slide from right) ── */}
<AnimatePresence>
{peekNote && (
<motion.aside
key="insights-peek"
initial={{ x: isRtl ? '-100%' : '100%', opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: isRtl ? '-100%' : '100%', opacity: 0 }}
transition={prefersReducedMotion ? { duration: 0 } : { type: 'spring', stiffness: 340, damping: 34 }}
className={`fixed top-0 ${isRtl ? 'left-0' : 'right-0'} z-[80] h-full w-full sm:w-[min(50vw,640px)] bg-[#fafaf9] dark:bg-zinc-950 flex flex-col overflow-hidden shadow-2xl ${isRtl ? 'border-r' : 'border-l'} border-black/10 dark:border-white/10`}
>
{/* Peek header */}
<div className="shrink-0 px-5 py-3 flex items-center justify-between gap-3 border-b border-black/[0.06] dark:border-white/[0.06] bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm">
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-concrete truncate">
{peekLoading ? t('insightsView.loading') : (peekNote.title || t('insightsView.unknownNote'))}
</span>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={openPeekFully}
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wide text-blue-600 dark:text-blue-400 hover:bg-blue-500/10 transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none"
>
<Maximize2 size={12} />
</button>
<button
onClick={closePeek}
className="p-1.5 rounded-lg text-concrete hover:text-ink dark:hover:text-dark-ink hover:bg-black/5 dark:hover:bg-white/5 transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none"
aria-label="Close"
>
<X size={16} />
</button>
</div>
</div>
{/* Peek content */}
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
<div className="max-w-2xl mx-auto w-full px-6 sm:px-8 py-8 pb-24">
<h2 className="text-xl font-serif font-medium text-ink dark:text-dark-ink mb-6">
{peekNote.title || t('insightsView.unknownNote')}
</h2>
{peekNote.content ? (
<div
dir="auto"
className="prose prose-sm dark:prose-invert max-w-none text-ink dark:text-dark-ink [&_a]:text-blue-600 [&_blockquote]:border-l-blue-400"
dangerouslySetInnerHTML={{ __html: peekNote.content }}
/>
) : (
<p className="text-sm text-concrete italic"></p>
)}
</div>
</div>
</motion.aside>
)}
</AnimatePresence>
</div>
)
}

View File

@@ -37,7 +37,7 @@ export default async function MainLayout({
>
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
<div className="flex h-screen overflow-hidden bg-memento-desk dark:bg-background">
<Suspense fallback={<div className="hidden w-80 shrink-0 md:block" />}>
<Suspense fallback={<div className="hidden w-80 xl:w-[22rem] 2xl:w-[26rem] shrink-0 md:block" />}>
<Sidebar user={session?.user} />
</Suspense>

View File

@@ -1159,7 +1159,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className={cn(
// Mobile: fixed overlay, slide in/out
'fixed inset-y-0 start-0 z-[70] md:relative md:z-auto',
'h-full min-h-0 w-80 shrink-0 flex flex-row overflow-hidden',
'h-full min-h-0 w-80 xl:w-[22rem] 2xl:w-[26rem] shrink-0 flex flex-row overflow-hidden',
'transition-transform duration-300 ease-in-out',
isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full md:translate-x-0',
'border-e border-border/40 bg-white/95 md:bg-white/30 backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#151515] dark:backdrop-blur-none',