Files
Momento/memento-note/components/note-editor/note-editor-full-page.tsx
Antigravity 330c0c61b6
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
feat: standardize UI theme, fix dark mode consistency, and implement editorial tags
2026-05-10 18:43:13 +00:00

178 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useNoteEditorContext } from './note-editor-context'
import { NoteEditorToolbar } from './note-editor-toolbar'
import { NoteTitleBlock } from './note-title-block'
import { NoteContentArea } from './note-content-area'
import { EditorImages } from '@/components/editor-images'
import { ComparisonModal } from '@/components/comparison-modal'
import { FusionModal } from '@/components/fusion-modal'
import { ReminderDialog } from '@/components/reminder-dialog'
import { ContextualAIChat } from '@/components/contextual-ai-chat'
import { NoteDocumentInfoPanel } from '@/components/note-document-info-panel'
import { format } from 'date-fns'
import { ChevronRight } from 'lucide-react'
import { toast } from 'sonner'
import { Note } from '@/lib/types'
import { GhostTags } from '@/components/ghost-tags'
import { LabelBadge } from '@/components/label-badge'
interface NoteEditorFullPageProps {
onClose: () => void
}
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
const { state, actions, note, readOnly, notebooks, fileInputRef, globalLabels } = useNoteEditorContext()
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
const getLabelType = (name: string): 'ai' | 'user' => {
const found = globalLabels.find(l => l.name.toLowerCase() === name.toLowerCase())
return (found as any)?.type === 'ai' ? 'ai' : 'user'
}
return (
<>
{/* ── outer container ── */}
<div className="h-full flex items-stretch overflow-hidden transition-all duration-500">
{/* ── main scrollable column ── */}
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-background">
{/* TOOLBAR */}
<NoteEditorToolbar mode="fullPage" onClose={onClose} />
{/* BODY — max-w-4xl, responsive px, py-16 */}
<div className="max-w-4xl mx-auto w-full px-6 sm:px-12 py-16 space-y-12 min-w-0">
{/* Breadcrumb + Title block */}
<div className="space-y-4">
{/* Breadcrumb: Notebook Date */}
<div className="flex items-center gap-3 text-[12px] uppercase tracking-[.25em] font-bold">
{notebookName && <span style={{ color: 'var(--color-ink)' }}>{notebookName}</span>}
{notebookName && <ChevronRight size={10} style={{ color: 'var(--color-concrete)' }} />}
<span suppressHydrationWarning style={{ color: 'var(--color-concrete)' }}>
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
</span>
</div>
{/* Title */}
<NoteTitleBlock />
{(state.labels.length > 0 || (state.filteredSuggestions.length > 0)) && (
<div className="flex flex-wrap gap-2 pt-2">
{state.labels.map((label) => (
<LabelBadge
key={label}
label={label}
type={getLabelType(label)}
onRemove={() => actions.handleRemoveLabel(label)}
/>
))}
{!readOnly && (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
)}
</div>
)}
</div>
{/* Hero image — show first note image if present */}
{state.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={state.allImages[0]}
alt={state.title}
className="w-full h-full object-cover grayscale contrast-110 hover:grayscale-0 transition-all duration-500"
/>
</div>
)}
{/* Content area — max-w-3xl for wider reading column */}
<div className="max-w-3xl mx-auto w-full pb-32">
<NoteContentArea />
</div>
</div>
</div>
{/* ── Side panel: AI Chat ── */}
{state.aiOpen && (
<div className="h-full self-stretch bg-background flex flex-col z-50 shrink-0">
<ContextualAIChat
onClose={() => actions.setAiOpen(false)}
noteTitle={state.title}
noteContent={state.content}
noteImages={state.allImages}
noteId={note.id}
onApplyToNote={(nc: string) => {
actions.setPreviousContentForCopilot(state.content)
actions.setContent(nc)
if (state.isMarkdown) actions.setShowMarkdownPreview(true)
}}
onUndoLastAction={state.previousContentForCopilot !== null ? () => { actions.setContent(state.previousContentForCopilot!); actions.setPreviousContentForCopilot(null) } : undefined}
lastActionApplied={state.previousContentForCopilot !== null}
notebooks={notebooks}
notebookId={note.notebookId ?? undefined}
notebookName={notebookName ?? undefined}
diagramInsertFormat={state.isMarkdown ? 'markdown' : 'html'}
onGenerateTitle={async () => {
const plain = state.content.replace(/<[^>]+>/g, ' ').trim()
const wordCount = plain.split(/\s+/).filter(Boolean).length
if (wordCount < 10) {
toast.error('Ajoutez au moins 10 mots avant de générer un titre.')
return
}
actions.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.suggestions?.[0]?.title ?? ''
if (s) {
actions.setTitle(s)
toast.success('Titre généré !')
} else {
toast.error('Impossible de générer un titre.')
}
} else {
toast.error('Erreur lors de la génération du titre.')
}
} catch { toast.error('Erreur réseau.') } finally { actions.setIsProcessingAI(false) }
}}
/>
</div>
)}
{/* ── Side panel: Document Info ── */}
{state.infoOpen && (
<div className="w-[400px] h-full self-stretch border-l border-black/10 dark:border-white/10 bg-background flex flex-col z-50 shrink-0">
<NoteDocumentInfoPanel
note={note}
content={state.content}
onClose={() => actions.setInfoOpen(false)}
onNoteRestored={(r: Note) => { actions.setContent(r.content || ''); actions.setTitle(r.title || ''); actions.setIsDirty(false) }}
/>
</div>
)}
</div>
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={actions.handleImageUpload} />
<ReminderDialog
open={state.showReminderDialog}
onOpenChange={actions.setShowReminderDialog}
currentReminder={state.currentReminder}
onSave={actions.handleReminderSave}
onRemove={actions.handleRemoveReminder}
/>
</>
)
}