Architecture: - components/note-peek/use-note-peek.ts: hook (fetch + state + events) - components/note-peek/note-peek-content.tsx: rendu markdown/richtext/KaTeX - components/note-peek/note-peek-panel.tsx: panel (overlay + inline modes) - components/note-peek/index.ts: exports - lib/use-scroll-to-block.ts: utilitaire scroll vers data-id insights/page.tsx: - ~95 lignes (state + fetch + KaTeX useEffect + AnimatePresence) → 10 lignes - peek.open(noteId) remplace handleNoteClick complexe - <NotePeekPanel mode=overlay /> remplace tout le JSX du panel NotePeekPanel gère: - markdown (MarkdownContent) + richtext (DOMPurify + KaTeX lazy) - RTL (slide gauche pour fa/ar) - prefers-reduced-motion - role=dialog + aria-modal (overlay mode) - loading spinner - bouton Maximize2 + X - renderContent prop pour custom (éditeur read-only)
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, type ReactNode } from 'react'
|
|
import DOMPurify from 'dompurify'
|
|
import { MarkdownContent } from '@/components/markdown-content'
|
|
import 'katex/dist/katex.min.css'
|
|
import type { Note } from '@/lib/types'
|
|
|
|
interface NotePeekContentProps {
|
|
note: Note
|
|
className?: string
|
|
children?: ReactNode
|
|
}
|
|
|
|
export function NotePeekContent({ note, className, children }: NotePeekContentProps) {
|
|
const htmlRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (!htmlRef.current || note.isMarkdown) return
|
|
let cancelled = false
|
|
void (async () => {
|
|
const katex = (await import('katex')).default
|
|
if (cancelled || !htmlRef.current) return
|
|
htmlRef.current.querySelectorAll('.math-equation-block[data-latex], .inline-math[data-latex]').forEach(el => {
|
|
const latex = el.getAttribute('data-latex') || ''
|
|
const isDisplay = el.classList.contains('math-equation-block')
|
|
try { el.innerHTML = katex.renderToString(latex, { displayMode: isDisplay, throwOnError: false }) } catch {}
|
|
})
|
|
})()
|
|
return () => { cancelled = true }
|
|
}, [note.id, note.content, note.isMarkdown])
|
|
|
|
if (children) return <>{children}</>
|
|
|
|
if (note.isMarkdown) {
|
|
return <MarkdownContent content={note.content} className={className} />
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={htmlRef}
|
|
dir="auto"
|
|
className={`editor-body prose prose-lg dark:prose-invert max-w-none leading-relaxed text-ink dark:text-dark-ink
|
|
[&_h1]:text-2xl [&_h1]:font-serif [&_h1]:font-semibold [&_h1]:mt-8 [&_h1]:mb-4
|
|
[&_h2]:text-xl [&_h2]:font-serif [&_h2]:font-semibold [&_h2]:mt-6 [&_h2]:mb-3
|
|
[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:mt-5 [&_h3]:mb-2
|
|
[&_p]:my-3 [&_p]:leading-relaxed
|
|
[&_ul]:list-disc [&_ul]:ps-6 [&_ul]:my-3
|
|
[&_ol]:list-decimal [&_ol]:ps-6 [&_ol]:my-3
|
|
[&_li]:my-1
|
|
[&_blockquote]:border-l-4 [&_blockquote]:border-brand-accent [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground [&_blockquote]:my-4
|
|
[&_pre]:bg-zinc-100 [&_pre]:dark:bg-zinc-900 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_pre]:text-sm [&_pre]:my-4
|
|
[&_code]:bg-zinc-100 [&_code]:dark:bg-zinc-800 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_code]:font-mono
|
|
[&_a]:text-blue-600 [&_a]:underline
|
|
[&_img]:rounded-lg [&_img]:max-w-full [&_img]:my-4
|
|
[&_table]:w-full [&_table]:my-4 [&_table]:border-collapse
|
|
[&_th]:border [&_th]:border-border [&_th]:p-2 [&_th]:bg-muted [&_th]:font-semibold [&_th]:text-left
|
|
[&_td]:border [&_td]:border-border [&_td]:p-2
|
|
[&_hr]:border-border [&_hr]:my-6
|
|
[&_[data-callout-type]]:p-4 [&_[data-callout-type]]:rounded-lg [&_[data-callout-type]]:my-4
|
|
[&_div[data-type='toggle-block']]:my-3
|
|
[&_div[data-type='toggle-block']_details]:rounded-lg [&_div[data-type='toggle-block']_details]:border [&_div[data-type='toggle-block']_details]:border-border ${className || ''}`}
|
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(note.content) }}
|
|
/>
|
|
)
|
|
}
|