- brainstorm-page.tsx: lit urlNotebookId depuis URL params - useConvertIdea accepte targetNotebookId (hook mis à jour) - handleConvert prend le carnet ciblé en paramètre - URL notebookId propage via router.push depuis le modal de confirmation - toast inclut le nom du carnet et un bouton 'Open notebook' qui redirige vers /home?notebook=X - peek panel affiche un lien vers le carnet source en haut du titre - useNotebooks pour le lookup du nom depuis notebooks Vrai fix: les notes vont maintenant là où l'utilisateur a choisi, pas dans un carnet auto-créé sans qu'il s'en aperçoive.
146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
'use client'
|
|
|
|
import { type ReactNode } from 'react'
|
|
import { motion, AnimatePresence, useReducedMotion } from 'motion/react'
|
|
import { X, Maximize2, Loader2, FolderOpen } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import type { Note } from '@/lib/types'
|
|
import { NotePeekContent } from './note-peek-content'
|
|
|
|
interface NotePeekPanelProps {
|
|
note: Note | null
|
|
blockId?: string
|
|
loading?: boolean
|
|
mode: 'overlay' | 'inline'
|
|
onClose: () => void
|
|
onOpenFully?: (note: Note) => void
|
|
labelKey?: string
|
|
renderContent?: (note: Note, blockId?: string) => ReactNode
|
|
}
|
|
|
|
export function NotePeekPanel({
|
|
note,
|
|
blockId,
|
|
loading = false,
|
|
mode,
|
|
onClose,
|
|
onOpenFully,
|
|
labelKey = 'notePeek.label',
|
|
renderContent,
|
|
}: NotePeekPanelProps) {
|
|
const { t, language } = useLanguage()
|
|
const isRtl = language === 'fa' || language === 'ar'
|
|
const prefersReducedMotion = useReducedMotion()
|
|
const isOpen = note !== null || loading
|
|
|
|
const spring = prefersReducedMotion
|
|
? { duration: 0 }
|
|
: { type: 'spring' as const, stiffness: 340, damping: 34 }
|
|
|
|
const content = (
|
|
<>
|
|
<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">
|
|
{loading
|
|
? (t('notePeek.loading') || 'Loading…')
|
|
: (note?.title || t('notePeek.untitled') || t('insightsView.unknownNote'))}
|
|
</span>
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
{onOpenFully && note && (
|
|
<button
|
|
onClick={() => onOpenFully(note)}
|
|
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-brand-accent/50 focus-visible:outline-none"
|
|
title={t('notePeek.openFullyHelp') || 'Open full screen'}
|
|
aria-label={t('notePeek.openFully') || 'Open fully'}
|
|
>
|
|
<Maximize2 size={12} />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
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-brand-accent/50 focus-visible:outline-none"
|
|
aria-label={t('notePeek.close') || 'Close'}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<Loader2 className="animate-spin text-brand-accent/40" size={28} />
|
|
</div>
|
|
) : note ? (
|
|
<div className="max-w-2xl mx-auto w-full px-6 sm:px-8 py-8 pb-24">
|
|
{renderContent ? (
|
|
renderContent(note, blockId)
|
|
) : (
|
|
<>
|
|
{note.notebookId && (
|
|
<a
|
|
href={`/home?notebook=${note.notebookId}`}
|
|
className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-brand-accent hover:text-brand-accent/70 transition-colors mb-2 cursor-pointer"
|
|
>
|
|
<FolderOpen size={11} />
|
|
{t('brainstorm.openNotebook') || 'Open notebook'}
|
|
</a>
|
|
)}
|
|
<h2 className="text-xl font-serif font-medium text-ink dark:text-dark-ink mb-6">
|
|
{note.title || t('notePeek.untitled') || 'Untitled'}
|
|
</h2>
|
|
<NotePeekContent note={note} />
|
|
</>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</>
|
|
)
|
|
|
|
if (mode === 'overlay') {
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.aside
|
|
key="note-peek-overlay"
|
|
initial={{ x: isRtl ? '-100%' : '100%', opacity: 0 }}
|
|
animate={{ x: 0, opacity: 1 }}
|
|
exit={{ x: isRtl ? '-100%' : '100%', opacity: 0 }}
|
|
transition={spring}
|
|
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`}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t(labelKey) || 'Note preview'}
|
|
>
|
|
{content}
|
|
</motion.aside>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
// inline mode
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.aside
|
|
key="note-peek-inline"
|
|
initial={{ width: 0, opacity: 0 }}
|
|
animate={{ width: 'min(50vw, 720px)', opacity: 1 }}
|
|
exit={{ width: 0, opacity: 0 }}
|
|
transition={spring}
|
|
className={`shrink-0 h-full min-h-0 bg-[#fafaf9] dark:bg-zinc-950 flex flex-col overflow-hidden z-40 ${
|
|
isRtl
|
|
? 'border-r border-black/10 dark:border-white/10 shadow-[4px_0_24px_-12px_rgba(0,0,0,0.12)]'
|
|
: 'border-l border-black/10 dark:border-white/10 shadow-[-4px_0_24px_-12px_rgba(0,0,0,0.12)]'
|
|
}`}
|
|
aria-label={t(labelKey) || 'Note preview'}
|
|
>
|
|
{content}
|
|
</motion.aside>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|