feat: fullPage layout 1:1 prototype - white bg, px-12 py-8 toolbar, rounded-full buttons, breadcrumb notebook>date, hero image, prose-lg content
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { NoteTypeSelector } from '@/components/note-type-selector'
|
import { NoteTypeSelector } from '@/components/note-type-selector'
|
||||||
import { RichTextEditor } from '@/components/rich-text-editor'
|
import { RichTextEditor } from '@/components/rich-text-editor'
|
||||||
import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut, ArrowLeft, Info, Check, Loader2 } from 'lucide-react'
|
import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut, ArrowLeft, ChevronRight, Info, Check, Loader2 } from 'lucide-react'
|
||||||
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes'
|
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
|
import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
|
||||||
@@ -658,187 +658,257 @@ export function NoteEditor({ note, readOnly = false, onClose, fullPage = false }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── fullPage mode: early return with editorial layout ──
|
// ── fullPage mode: editorial layout (fidèle au prototype) ──
|
||||||
if (fullPage) {
|
if (fullPage) {
|
||||||
|
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||||
|
const plainFirstSentence = content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().split(/[.!?]/)[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full flex flex-col overflow-hidden bg-white dark:bg-zinc-950">
|
{/* ── outer container: white like prototype ── */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="h-full flex overflow-hidden transition-all duration-500">
|
||||||
{/* main scrollable column */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-zinc-950">
|
|
||||||
|
|
||||||
{/* sticky toolbar — matches prototype */}
|
{/* ── main scrollable column ── */}
|
||||||
<div className="px-12 py-6 flex items-center justify-between sticky top-0 bg-white/95 dark:bg-zinc-950/95 backdrop-blur-sm z-40 border-b border-border/40">
|
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-zinc-950">
|
||||||
<button onClick={onClose} className="flex items-center gap-2 text-foreground hover:opacity-60 transition-opacity shrink-0">
|
|
||||||
<ArrowLeft size={18} />
|
{/* TOOLBAR — px-12 py-8, bg-white/90, rounded-full buttons */}
|
||||||
<span className="text-sm font-medium">{'← ' + (t('notes.backToCollection') || 'Retour')}</span>
|
<div className="px-12 py-8 flex items-center justify-between sticky top-0 bg-white/90 dark:bg-zinc-950/90 backdrop-blur-sm z-40 border-b border-black/10 dark:border-white/10">
|
||||||
|
{/* Left: back */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex items-center gap-2 text-foreground hover:opacity-60 transition-opacity"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
<span className="text-sm font-medium">Back to collection</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Right: status + type + AI + Info */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Save status */}
|
||||||
|
<span className="hidden sm:flex items-center gap-1.5 text-[11px] text-foreground/40 select-none">
|
||||||
|
{isSaving
|
||||||
|
? <><Loader2 className="h-3 w-3 animate-spin" /><span>Saving…</span></>
|
||||||
|
: isDirty
|
||||||
|
? <><span className="h-1.5 w-1.5 rounded-full bg-amber-400 inline-block" /><span>Modified</span></>
|
||||||
|
: <><Check className="h-3 w-3 text-emerald-500" /><span>Saved</span></>}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Note type */}
|
||||||
|
<NoteTypeSelector
|
||||||
|
value={noteType}
|
||||||
|
onChange={(newType) => { setNoteType(newType); setShowMarkdownPreview(newType === 'markdown'); setIsDirty(true) }}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* AI — rounded-full, exact prototype style */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setAiOpen(v => !v); setInfoOpen(false) }}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||||
|
aiOpen
|
||||||
|
? 'bg-foreground text-background border-foreground'
|
||||||
|
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Sparkles size={16} />
|
||||||
|
<span>AI Assistant</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
|
||||||
{/* Save status */}
|
|
||||||
<span className="hidden sm:flex items-center gap-1.5 text-[11px] text-muted-foreground/60 select-none">
|
|
||||||
{isSaving
|
|
||||||
? <><Loader2 className="h-3 w-3 animate-spin" /><span>{t('notes.saving') || 'Enregistrement...'}</span></>
|
|
||||||
: isDirty
|
|
||||||
? <><span className="h-1.5 w-1.5 rounded-full bg-amber-400 inline-block" /><span>{t('notes.dirtyStatus') || 'Modifié'}</span></>
|
|
||||||
: <><Check className="h-3 w-3 text-emerald-500" /><span>{t('notes.savedStatus') || 'Enregistré'}</span></>}
|
|
||||||
</span>
|
|
||||||
{/* Type selector */}
|
|
||||||
<NoteTypeSelector
|
|
||||||
value={noteType}
|
|
||||||
onChange={(newType) => { setNoteType(newType); setShowMarkdownPreview(newType === 'markdown'); setIsDirty(true) }}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
{/* AI button — rounded-full like prototype */}
|
|
||||||
<button
|
|
||||||
onClick={() => { setAiOpen(v => !v); setInfoOpen(false) }}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium transition-all duration-300',
|
|
||||||
aiOpen
|
|
||||||
? 'bg-foreground text-background border-foreground'
|
|
||||||
: 'border-border text-foreground hover:bg-muted/50'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Sparkles size={14} />
|
|
||||||
<span>IA</span>
|
|
||||||
</button>
|
|
||||||
{/* Info button */}
|
|
||||||
<button
|
|
||||||
onClick={() => { setInfoOpen(v => !v); setAiOpen(false) }}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium transition-all duration-300',
|
|
||||||
infoOpen
|
|
||||||
? 'bg-foreground text-background border-foreground'
|
|
||||||
: 'border-border text-foreground hover:bg-muted/50'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Info size={14} />
|
|
||||||
<span>Info</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* body */}
|
{/* Info — rounded-full */}
|
||||||
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12">
|
<button
|
||||||
{/* meta */}
|
onClick={() => { setInfoOpen(v => !v); setAiOpen(false) }}
|
||||||
<div className="text-[12px] text-muted-foreground uppercase tracking-[.25em] font-bold" suppressHydrationWarning>
|
className={cn(
|
||||||
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
|
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||||
</div>
|
infoOpen
|
||||||
|
? 'bg-foreground text-background border-foreground'
|
||||||
{/* title + AI */}
|
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="group relative">
|
|
||||||
<input
|
|
||||||
dir="auto" type="text"
|
|
||||||
placeholder={t('notes.titlePlaceholder')}
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => { setTitle(e.target.value); setIsDirty(true); setDismissedTitleSuggestions(true) }}
|
|
||||||
disabled={readOnly}
|
|
||||||
className="w-full text-5xl md:text-6xl font-memento-serif font-bold border-0 outline-none px-0 bg-transparent text-foreground leading-tight placeholder:text-muted-foreground/30 pr-14"
|
|
||||||
/>
|
|
||||||
{!title && !readOnly && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
const plain = content.replace(/<[^>]+>/g, ' ').trim()
|
|
||||||
if (plain.split(/\s+/).filter(Boolean).length < 3) return
|
|
||||||
setIsProcessingAI(true)
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/ai/title-suggestions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: plain }) })
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
const s = data.title || data.suggestedTitle || (data.suggestions?.[0]?.title ?? '')
|
|
||||||
if (s) { setTitle(s); setIsDirty(true) }
|
|
||||||
}
|
|
||||||
} catch {} finally { setIsProcessingAI(false) }
|
|
||||||
}}
|
|
||||||
disabled={isProcessingAI}
|
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 opacity-30 hover:opacity-100 transition-opacity rounded-lg p-2 text-muted-foreground hover:bg-muted hover:text-primary"
|
|
||||||
title={t('ai.suggestTitle') || 'Générer un titre IA'}
|
|
||||||
>
|
|
||||||
{isProcessingAI ? <Loader2 className="h-5 w-5 animate-spin" /> : <Sparkles className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!title && !dismissedTitleSuggestions && autoTitleSuggestions.length > 0 && (
|
|
||||||
<TitleSuggestions
|
|
||||||
suggestions={autoTitleSuggestions}
|
|
||||||
onSelect={(s) => { setTitle(s); setDismissedTitleSuggestions(true); setIsDirty(true) }}
|
|
||||||
onDismiss={() => setDismissedTitleSuggestions(true)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
|
<Info size={16} />
|
||||||
{/* editor */}
|
<span>Document Info</span>
|
||||||
<div className="max-w-2xl mx-auto pb-32">
|
</button>
|
||||||
{noteType === 'richtext' ? (
|
|
||||||
<RichTextEditor
|
|
||||||
content={content}
|
|
||||||
onChange={(v) => { setContent(v); setIsDirty(true) }}
|
|
||||||
className="min-h-[200px] text-lg font-light leading-relaxed"
|
|
||||||
onImageUpload={uploadImageFile}
|
|
||||||
/>
|
|
||||||
) : noteType === 'markdown' && showMarkdownPreview ? (
|
|
||||||
<div
|
|
||||||
className="min-h-[200px] cursor-text prose prose-sm dark:prose-invert max-w-none text-base leading-relaxed"
|
|
||||||
onClick={() => setShowMarkdownPreview(false)}
|
|
||||||
>
|
|
||||||
<MarkdownContent content={content} />
|
|
||||||
<p className="text-[11px] text-muted-foreground/40 mt-4 select-none">Cliquez pour éditer</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="relative">
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
dir="auto"
|
|
||||||
placeholder={t('notes.takeNote') || "Tapez '/' pour les commandes..."}
|
|
||||||
value={content}
|
|
||||||
onFocus={() => setShowMarkdownPreview(false)}
|
|
||||||
onChange={(e) => { setContent(e.target.value); setIsDirty(true) }}
|
|
||||||
disabled={readOnly}
|
|
||||||
className="w-full min-h-[400px] border-0 outline-none px-0 bg-transparent text-lg leading-relaxed font-light resize-none placeholder:text-muted-foreground/30"
|
|
||||||
/>
|
|
||||||
{noteType === 'markdown' && content && !readOnly && (
|
|
||||||
<button type="button" onClick={() => setShowMarkdownPreview(true)}
|
|
||||||
className="mt-2 text-[11px] text-muted-foreground/50 hover:text-foreground flex items-center gap-1 transition-colors">
|
|
||||||
<Eye className="h-3 w-3" /> Prévisualiser le Markdown
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!readOnly && (
|
|
||||||
<MarkdownSlashCommands
|
|
||||||
textareaRef={textareaRef as React.RefObject<HTMLTextAreaElement>}
|
|
||||||
value={content}
|
|
||||||
onChange={(v) => { setContent(v); setIsDirty(true) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* side panels */}
|
{/* BODY — max-w-4xl, px-12, py-16 */}
|
||||||
{aiOpen && (
|
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12">
|
||||||
|
|
||||||
|
{/* Breadcrumb + Title block */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Breadcrumb: Notebook › Date */}
|
||||||
|
<div className="flex items-center gap-3 text-[12px] text-foreground/50 uppercase tracking-[.25em] font-bold">
|
||||||
|
{notebookName && <span>{notebookName}</span>}
|
||||||
|
{notebookName && <ChevronRight size={10} />}
|
||||||
|
<span suppressHydrationWarning>
|
||||||
|
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title — editable input styled as h1 */}
|
||||||
|
<div className="group relative">
|
||||||
|
<input
|
||||||
|
dir="auto"
|
||||||
|
type="text"
|
||||||
|
placeholder={t('notes.titlePlaceholder') || 'Untitled…'}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => { setTitle(e.target.value); setIsDirty(true); setDismissedTitleSuggestions(true) }}
|
||||||
|
disabled={readOnly}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-5xl md:text-6xl font-memento-serif font-bold border-0 outline-none px-0 bg-transparent text-foreground leading-tight',
|
||||||
|
'placeholder:text-foreground/20',
|
||||||
|
!readOnly && 'pr-14'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* AI title generation — shown when title is empty */}
|
||||||
|
{!title && !readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
const plain = content.replace(/<[^>]+>/g, ' ').trim()
|
||||||
|
if (plain.split(/\s+/).filter(Boolean).length < 3) return
|
||||||
|
setIsProcessingAI(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/ai/title-suggestions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content: plain }),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
const s = data.title || data.suggestedTitle || (data.suggestions?.[0]?.title ?? '')
|
||||||
|
if (s) { setTitle(s); setIsDirty(true) }
|
||||||
|
}
|
||||||
|
} catch {} finally { setIsProcessingAI(false) }
|
||||||
|
}}
|
||||||
|
disabled={isProcessingAI}
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-60 hover:!opacity-100 transition-opacity rounded-lg p-2 text-foreground/50 hover:bg-black/5"
|
||||||
|
title="Generate AI title"
|
||||||
|
>
|
||||||
|
{isProcessingAI ? <Loader2 className="h-5 w-5 animate-spin" /> : <Sparkles className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto title suggestions */}
|
||||||
|
{!title && !dismissedTitleSuggestions && autoTitleSuggestions.length > 0 && (
|
||||||
|
<TitleSuggestions
|
||||||
|
suggestions={autoTitleSuggestions}
|
||||||
|
onSelect={(s) => { setTitle(s); setDismissedTitleSuggestions(true); setIsDirty(true) }}
|
||||||
|
onDismiss={() => setDismissedTitleSuggestions(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero image — show first note image if present */}
|
||||||
|
{allImages.length > 0 && (
|
||||||
|
<div className="aspect-[16/9] w-full bg-slate-100 dark:bg-zinc-900 rounded-xl overflow-hidden shadow-xl">
|
||||||
|
<img
|
||||||
|
src={allImages[0]}
|
||||||
|
alt={title}
|
||||||
|
className="w-full h-full object-cover grayscale contrast-110 hover:grayscale-0 transition-all duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content area — max-w-2xl, like prototype */}
|
||||||
|
<div className="max-w-2xl mx-auto space-y-8 pb-32">
|
||||||
|
{noteType === 'richtext' ? (
|
||||||
|
<RichTextEditor
|
||||||
|
content={content}
|
||||||
|
onChange={(v) => { setContent(v); setIsDirty(true) }}
|
||||||
|
className="min-h-[300px] text-lg font-light leading-relaxed text-foreground/80"
|
||||||
|
onImageUpload={uploadImageFile}
|
||||||
|
/>
|
||||||
|
) : noteType === 'markdown' && showMarkdownPreview ? (
|
||||||
|
<div
|
||||||
|
className="min-h-[300px] cursor-text prose prose-lg dark:prose-invert max-w-none leading-relaxed"
|
||||||
|
onClick={() => !readOnly && setShowMarkdownPreview(false)}
|
||||||
|
>
|
||||||
|
<MarkdownContent content={content} />
|
||||||
|
{!readOnly && (
|
||||||
|
<p className="text-[11px] text-foreground/30 mt-6 select-none not-prose">
|
||||||
|
Click to edit
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : noteType === 'text' || noteType === 'markdown' ? (
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
dir="auto"
|
||||||
|
placeholder={t('notes.takeNote') || "Type '/' for commands…"}
|
||||||
|
value={content}
|
||||||
|
onFocus={() => setShowMarkdownPreview(false)}
|
||||||
|
onChange={(e) => { setContent(e.target.value); setIsDirty(true) }}
|
||||||
|
disabled={readOnly}
|
||||||
|
className="w-full min-h-[400px] border-0 outline-none px-0 bg-transparent text-lg leading-relaxed font-light resize-none placeholder:text-foreground/20 text-foreground/80"
|
||||||
|
/>
|
||||||
|
{noteType === 'markdown' && content && !readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMarkdownPreview(true)}
|
||||||
|
className="mt-2 text-[11px] text-foreground/40 hover:text-foreground flex items-center gap-1.5 transition-colors"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" /> Preview Markdown
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!readOnly && (
|
||||||
|
<MarkdownSlashCommands
|
||||||
|
textareaRef={textareaRef as React.RefObject<HTMLTextAreaElement>}
|
||||||
|
value={content}
|
||||||
|
onChange={(v) => { setContent(v); setIsDirty(true) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Side panel: AI Chat (prototype style: w-[400px], border-l, bg-white) ── */}
|
||||||
|
{aiOpen && (
|
||||||
|
<div className="w-[400px] border-l border-black/10 dark:border-white/10 bg-white dark:bg-zinc-950 shadow-2xl flex flex-col z-50 shrink-0">
|
||||||
<ContextualAIChat
|
<ContextualAIChat
|
||||||
onClose={() => setAiOpen(false)}
|
onClose={() => setAiOpen(false)}
|
||||||
noteTitle={title} noteContent={content} noteImages={allImages} noteId={note.id}
|
noteTitle={title}
|
||||||
onApplyToNote={(nc) => { setPreviousContentForCopilot(content); setContent(nc); setIsDirty(true); if (noteType === 'markdown') setShowMarkdownPreview(true) }}
|
noteContent={content}
|
||||||
|
noteImages={allImages}
|
||||||
|
noteId={note.id}
|
||||||
|
onApplyToNote={(nc) => {
|
||||||
|
setPreviousContentForCopilot(content)
|
||||||
|
setContent(nc)
|
||||||
|
setIsDirty(true)
|
||||||
|
if (noteType === 'markdown') setShowMarkdownPreview(true)
|
||||||
|
}}
|
||||||
onUndoLastAction={previousContentForCopilot !== null ? () => { setContent(previousContentForCopilot!); setPreviousContentForCopilot(null) } : undefined}
|
onUndoLastAction={previousContentForCopilot !== null ? () => { setContent(previousContentForCopilot!); setPreviousContentForCopilot(null) } : undefined}
|
||||||
lastActionApplied={previousContentForCopilot !== null}
|
lastActionApplied={previousContentForCopilot !== null}
|
||||||
notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))}
|
notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))}
|
||||||
diagramInsertFormat={noteType === 'richtext' ? 'html' : 'markdown'}
|
diagramInsertFormat={noteType === 'richtext' ? 'html' : 'markdown'}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{infoOpen && (
|
)}
|
||||||
|
|
||||||
|
{/* ── Side panel: Document Info ── */}
|
||||||
|
{infoOpen && (
|
||||||
|
<div className="w-[400px] border-l border-black/10 dark:border-white/10 bg-white dark:bg-zinc-950 shadow-2xl flex flex-col z-50 shrink-0">
|
||||||
<NoteDocumentInfoPanel
|
<NoteDocumentInfoPanel
|
||||||
note={note} content={content}
|
note={note}
|
||||||
|
content={content}
|
||||||
onClose={() => setInfoOpen(false)}
|
onClose={() => setInfoOpen(false)}
|
||||||
onNoteRestored={(r) => { setContent(r.content || ''); setTitle(r.title || ''); setIsDirty(false) }}
|
onNoteRestored={(r) => { setContent(r.content || ''); setTitle(r.title || ''); setIsDirty(false) }}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={handleImageUpload} />
|
|
||||||
</div>
|
</div>
|
||||||
<ReminderDialog open={showReminderDialog} onOpenChange={setShowReminderDialog} currentReminder={currentReminder} onSave={handleReminderSave} onRemove={handleRemoveReminder} />
|
|
||||||
|
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={handleImageUpload} />
|
||||||
|
<ReminderDialog
|
||||||
|
open={showReminderDialog}
|
||||||
|
onOpenChange={setShowReminderDialog}
|
||||||
|
currentReminder={currentReminder}
|
||||||
|
onSave={handleReminderSave}
|
||||||
|
onRemove={handleRemoveReminder}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user